一、TF-IDF (HashingTF and IDF)

   “词频-逆向文件频率”(TF-IDF)是一种在文本挖掘中广泛使用的特征向量化方法,它可以体现一个文档中词语在语料库中的重要程度。在Spark ML库中,TF-IDF被分成两部分:TF (+hashing) 和 IDF。

  TF: HashingTF 是一个Transformer,在文本处理中,接收词条的集合然后把这些集合转化成固定长度的特征向量。这个算法在哈希的同时会统计各个词条的词频。

  IDF: IDF是一个Estimator,在一个数据集上应用它的fit()方法,产生一个IDFModel。 该IDFModel 接收特征向量(由HashingTF产生),然后计算每一个词在文档中出现的频次。IDF会减少那些在语料库中出现频率较高的词的权重。

代码示例:(注意:本文代码是在zeppelin上实现的)

%spark
// 特征抽取方法—— —— TF-IDF
// TF-IDF是一个统计方法,用来评估某个词语对于一个文件集或文档库中的其中一份文件的重要程度。TF-IDF=TF*IDF

import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}

val sentenceData = spark.createDataFrame(Seq(
  (0.0, "Hi I heard about Spark I love Spark"),
  (0.0, "I wish Java could use case classesL"),
  (1.0, "Logistic regression models are neat")
)).toDF("label", "sentence")

// 创建分词器对象
val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
val wordsData = tokenizer.transform(sentenceData)
wordsData.select("label","words").show(false)

// 创建词频映射,计算某个词在文件中出现的频率
// hashingTF是一个Transformers
val hashingTF = new HashingTF()
  .setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(1000)
// HashingTF().tranform()函数把词哈希成特征向量。返回结果是Vectors.sparse()类型的,这里设置哈希表的桶数(setNumFeatures)为1000(注意:该值设置太小会造成哈希冲突)。
val featurizedData = hashingTF.transform(wordsData)
featurizedData.select("label","rawFeatures").show(false)
// 输出的结果中,240,286分别代表hi, i, 的哈希值
// 1.0,2.0代表其出现的次数


// IDF类:计算给定文档集合的反文档频率 ,是一个词普遍重要性的度量。(即:一个词存在多少个文档中)
// 某一特定词语的IDF,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到。
// idf是一个Estimator
val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
val idfModel = idf.fit(featurizedData)
// 计算每一个单词对应的IDF 度量值
val rescaledData = idfModel.transform(featurizedData)
rescaledData.select("label", "features").show(false)
// 输出的结果中,240,286分别代表hi, i的哈希值
// 0.6931471805599453,0.5753641449035617是hi, i对应的IDF值,出现的次数越多,值越小

输出结果:
+-----+--------------------------------------------+
|label|words                                       |
+-----+--------------------------------------------+
|0.0  |[hi, i, heard, about, spark, i, love, spark]|
|0.0  |[i, wish, java, could, use, case, classesl] |
|1.0  |[logistic, regression, models, are, neat]   |
+-----+--------------------------------------------+

+-----+-----------------------------------------------------------------+
|label|rawFeatures                                                      |
+-----+-----------------------------------------------------------------+
|0.0  |(1000,[240,286,568,673,756,956],[1.0,2.0,1.0,1.0,2.0,1.0])       |
|0.0  |(1000,[80,133,342,495,748,756,967],[1.0,1.0,1.0,1.0,1.0,1.0,1.0])|
|1.0  |(1000,[59,286,604,763,871],[1.0,1.0,1.0,1.0,1.0])                |
+-----+-----------------------------------------------------------------+

+-----+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|label|features                                                                                                                                                                   |
+-----+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|0.0  |(1000,[240,286,568,673,756,956],[0.6931471805599453,0.5753641449035617,0.6931471805599453,0.6931471805599453,0.5753641449035617,0.6931471805599453])                       |
|0.0  |(1000,[80,133,342,495,748,756,967],[0.6931471805599453,0.6931471805599453,0.6931471805599453,0.6931471805599453,0.6931471805599453,0.28768207245178085,0.6931471805599453])|
|1.0  |(1000,[59,286,604,763,871],[0.6931471805599453,0.28768207245178085,0.6931471805599453,0.6931471805599453,0.6931471805599453])                                              |
+-----+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

二、 Word2Vec 

   Word2Vec 是一种著名的 词嵌入(Word Embedding) 方法,它可以计算每个单词在其给定语料库环境下的 分布式词向量(Distributed Representation,亦直接被称为词向量)。词向量表示可以在一定程度上刻画每个单词的语义。

  如果词的语义相近,它们的词向量在向量空间中也相互接近,这使得词语的向量化建模更加精确,可以改善现有方法并提高鲁棒性。词向量已被证明在许多自然语言处理问题,如:机器翻译,标注问题,实体识别等问题中具有非常重要的作用。

  Word2vec是一个Estimator,它采用一系列代表文档的词语来训练word2vecmodel。该模型将每个词语映射到一个固定大小的向量。word2vecmodel使用文档中每个词语的平均数来将文档转换为向量,然后这个向量可以作为预测的特征,来计算文档相似度计算等等。

示例代码:

%spark
// 特征抽取方法—— —— Word2Vec
// Word2Vec是用来计算词向量的工具,词向量表示可以在一定程度上刻画每个单词的语义,如果词的语义相近,它们的词向量在向量空间中也相互接近。
// 词向量可以用于许多自然语言处理问题,如:计算文档相似度、机器翻译,标注问题,实体识别等问题
// Word2vec是一个Estimator

import org.apache.spark.ml.feature.Word2Vec
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.sql.Row

// 输入数据:每一行是一个句子或文档中的一个词袋。
val documentDF = spark.createDataFrame(Seq(
  "Hi I heard about Spark I love Spark".split(" "),
  "I wish Java could use case classes".split(" "),
  "Logistic regression models are neat".split(" ")
).map(Tuple1.apply)).toDF("text")

// 学习从单词到向量的映射。 
// 特征向量的维度为3
val word2Vec = new Word2Vec()
  .setInputCol("text")
  .setOutputCol("result")
  .setVectorSize(3)
  .setMinCount(0)
val model = word2Vec.fit(documentDF)

val result = model.transform(documentDF)
result.collect().foreach { case Row(text: Seq[_], features: Vector) =>
  println(s"Text: [${text.mkString(", ")}] => \nVector: $features\n") }
  
输出结果:
Text: [Hi, I, heard, about, Spark, I, love, Spark] => 
Vector: [-0.06175751599948853,-0.029941751156002283,0.0647793048992753]

Text: [I, wish, Java, could, use, case, classes] => 
Vector: [-0.02761765463011605,-0.00916134805551597,0.0755475967723344]

Text: [Logistic, regression, models, are, neat] => 
Vector: [-0.010655752956517973,0.001399220246821642,-0.016871685534715655]

三、CountVectorizer

CountVectorizer旨在通过计数来将一个文档转换为向量。当不存在先验字典时,Countvectorizer作为Estimator提取词汇进行训练,并生成一个CountVectorizerModel用于存储相应的词汇向量空间。该模型产生文档关于词语的稀疏表示,其表示可以传递给其他算法,例如LDA。

示例代码:

%spark
// 特征抽取方法—— —— CountVectorizer
// 通过计数来将一个文档转换为向量。当先验字典不可用时,可以用作提取词汇表,并生成该模型在词汇表上为文档生成稀疏表示形式,然后可以将这些表示形式传递给其他算法(如 LDA)
import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel}

val df = spark.createDataFrame(Seq(
  (0, Array("a", "b", "c","d")),
  (1, Array("a", "b", "b", "c", "a"))
)).toDF("id", "words")

// 拟合语料库的 CountVectorizerModel 
// 设定词汇表的最大量为4,设定词汇表中的词至少要在2个文档中出现过,以过滤那些偶然出现的词汇
val cvModel: CountVectorizerModel = new CountVectorizer()
  .setInputCol("words")
  .setOutputCol("features")
  .setVocabSize(4)
  .setMinDF(2)
  .fit(df)

// 使用先验词汇定义 CountVectorizerModel 
val cvm = new CountVectorizerModel(Array("b", "c","d"))
  .setInputCol("words")
  .setOutputCol("features")

cvModel.transform(df).show(false)
cvm.transform(df).show(false)

输出结果:
+---+---------------+-------------------------+
|id |words          |features                 |
+---+---------------+-------------------------+
|0  |[a, b, c, d]   |(3,[0,1,2],[1.0,1.0,1.0])|
|1  |[a, b, b, c, a]|(3,[0,1,2],[2.0,2.0,1.0])|
+---+---------------+-------------------------+

+---+---------------+-------------------------+
|id |words          |features                 |
+---+---------------+-------------------------+
|0  |[a, b, c, d]   |(3,[0,1,2],[1.0,1.0,1.0])|
|1  |[a, b, b, c, a]|(3,[0,1],[2.0,1.0])      |
+---+---------------+-------------------------+

四、FeatureHasher

  特征散列将一组分类或数字特征投影到指定维度的特征向量中(通常远小于原始特征空间的特征向量)。这是使用散列技巧将特征映射到特征向量中的索引来完成的。FeatureHasher 转换器在多个列上运行。每列可能包含数字或分类特征。列数据类型的行为和处理如下:

  • 数字列:对于数字特征,列名的哈希值用于将特征值映射到其在特征向量中的索引。默认情况下,数字特征不被视为分类(即使它们是整数)。要将它们视为分类,请使用 categoricalCols 参数指定相关列。
  • 字符串列:对于分类特征,使用字符串“column_name=value”的哈希值映射到向量索引,指标值为1.0。因此,分类特征是“one-hot”编码的(类似于使用带有 dropLast=false 的 OneHotEncoder)。
  • 布尔列:布尔值的处理方式与字符串列相同。即布尔特征表示为“column_name=true”或“column_name=false”,指标值为1.0。 Null(缺失)值被忽略(在结果特征向量中隐式为零)。

这里使用的哈希函数也是HashingTF中使用的MurmurHash 3。由于使用散列值的简单模来确定向量索引,因此建议使用 2 的幂作为 numFeatures 参数;否则特征将不会均匀地映射到向量索引。

示例代码:

// 特征抽取 —— —— FeatureHasher
// 特征哈希是一个Transformers在多个列上运行,用来将一组分类特征或数字投影到指定维度的特征向量中(通常远小于原始特征空间的特征向量)。
import org.apache.spark.ml.feature.FeatureHasher

val dataset = spark.createDataFrame(Seq(
  (2.2, true, "1", "foo"),
  (3.3, false, "2", "bar"),
  (4.4, false, "3", "baz"),
  (5.5, false, "4", "foo")
)).toDF("real", "bool", "stringNum", "string")

val hasher = new FeatureHasher()
  .setInputCols("real", "bool", "stringNum", "string")
  .setOutputCol("features")
//   .setNumFeatures(4) //默认是262144
// 默认情况下,数字不会用于分类。要想将数字也视为分类特征,需要使用categoricalCols 指定该列
  .setCategoricalCols(Array("real"))

val featurized = hasher.transform(dataset)
featurized.show(false)

输出结果;
+----+-----+---------+------+-------------------------------------------------------+
|real|bool |stringNum|string|features                                               |
+----+-----+---------+------+-------------------------------------------------------+
|2.2 |true |1        |foo   |(262144,[39132,247670,257907,262126],[1.0,1.0,1.0,1.0])|
|3.3 |false|2        |bar   |(262144,[42660,70644,89673,173866],[1.0,1.0,1.0,1.0])  |
|4.4 |false|3        |baz   |(262144,[8994,22406,70644,187923],[1.0,1.0,1.0,1.0])   |
|5.5 |false|4        |foo   |(262144,[45071,70644,101499,257907],[1.0,1.0,1.0,1.0]) |
+----+-----+---------+------+-------------------------------------------------------+