Spark数据挖掘-TF-IDF文档矩阵
前言
得到词文档矩阵往往都是文本挖掘算法的第一步,词文档矩阵中行表示语料库中出现过的词(实际代码都是对词进行整数编码),列表示所有的文档,矩阵中的每个值就代表词在文档中的重要程度。目前已经有很多计算词在文档中权重的模型,不过最通用的模型应该就是 词频-逆文档频率(简称:TF-IDF) 矩阵。
TF-IDF
先看一下TF-IDF如何计算每个词在文档中的重要程度,先假设得到了下面几个变量的值:
- termFrequencyInDoc:Int 词在文档中出现的次数
- totalTermsInDoc: Int 文档中所有词的个数
- termFreqInCorpus: Int 语料库中出现这个词的不同文档数
- totalDocs: Int 整个语料库包含的文档数量
利用上面的几个值就可以计算一个词在文档中的重要程度,代码如下:
def termDocWeight(termFrequencyInDoc: Int, totalTermsInDoc: Int,
termFreqInCorpus: Int, totalDocs: Int): Double = {
val tf = termFrequencyInDoc.toDouble / totalTermsInDoc
val docFreq = totalDocs.toDouble / termFreqInCorpus
val idf = math.log(docFreq)
tf * idf
}
TF-IDF含义解读
直观理解,TF-IDF考虑了如下两个方面:
- 首先,词在某篇文档中出现的越多越重要
- 其次,词在其他文档中尽量少出现,如果每篇文档中它都出现的很多,它就没有区分度了。所以这里采用的是文档频率取逆,也就是取倒数。
但是有个问题,首先词频是近似服从指数分布,一些通用词出现的次数可能是低频词几十或者几百倍,直接采用文档频率取逆会导致稀缺词获得一个巨大的权重,实际上就会忽略其他词的影响,这样显然不妥,为了去掉这个影响,算法采用对文档频率取逆之后再取对数,这样就把乘法差异转化为加法差异。使得稀缺词权重降低了。
TF-IDF的局限
这个算法本质还是词袋模型,它没有考虑词与词之间的顺序、语法结构以及语义。通过对每个不同的词计数,这个模型很难区别一词多义、以及同义词。实际编程中往往每篇文档表示为行向量,每个元素代表词对应的索引位置,这样可以适应语料库中文档变的越来越多,通常情况下,语料库文档的增加速度肯定大过词的增长速度。而且这样的行向量也是稀疏表示,节省空间。
TF-IDF实战
TF-IDF原理非常简单,实际工作中还是有很多需要注意的地方,下面就用 Spark 实战的方式讲解如何将一个语料库转为 TF-IDF 矩阵,首先给出具体的步骤:
- 实际工作中对每篇文档往往都有一个唯一的标识主键,这样方便以后的统计
- 文档清洗(去掉不符合要求的文档:比如文档内容为空)
- 文档分词(去掉停用词、按词性过滤一下词、过滤低频词、词干提取),其中词干提取指的是将比如:“大款、大款们”合并为一个词,中文实现这个难度很大。
- 数据加载,词编码、文档编码
- 将数据编码之后转为文档词向量再合并为矩阵
- 计算IF-IDF
数据清洗、停词库每个项目都会有不同的要求,但是分词、词编码、文档编码等工作还是比较通用的,下面会对通用技术通过 Spark 实战详细讲解(尤其是分布式编程需要注意的地方:)
分布式文本分词
分布式文本分词注意两个地方:
- 停词库需要广播到所有机器
- 每个 executor 只需启动一个分词模型即可,分词模型很耗资源,与连接数据库一样,每个executor只需启动一个实例
计算TF-IDF
val documents: RDD[Seq[String]] = sc.textFile("your_data_dir").map(_.split(" ").toSeq)
val hashingTF = new HashingTF()
val tf: RDD[Vector] = hashingTF.transform(documents)
tf.cache()
//没有在两个文档中出现过的词语不要
val idf = new IDF(minDocFreq = 2).fit(tf)
val tfidf: RDD[Vector] = idf.transform(tf)