spark TF-IDF特征提取生成文章关键词
首先介绍下TF-IDF
TF-IDF

TF-IDF(term frequency–inverse document frequency)中文”词频-逆向文件频率”,通过它将文本特征向量化,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
词频(TF)表示某关键词在文本中出现次数:
TF=某单词在文档中出现次数文档中所有单词出现次数总和TF=某单词在文档中出现次数文档中所有单词出现次数总和 TF = \dfrac{某单词在文档中出现次数}{文档中所有单词出现次数总和}
逆向文件频率(IDF)是一个词语普遍重要性的度量:
IDF=log文档总数n包含单词w的文档数目+1IDF=log文档总数n包含单词w的文档数目+1 IDF = log\dfrac{文档总数n}{包含单词w的文档数目+1}

其中加1是为了避免分母为0
TF−IDF=TF∗IDFTF−IDF=TF∗IDF TF-IDF=TF*IDF
在n个文档中,如果一个单词在文档DiDiD_i中出现次数比较多,而很少出现在其它的文档中,那么TF值越大,IDF值越大,单词就更能作为文档DiDiD_i的关键词。

数据准备

可以到http://qwone.com/~jason/20Newsgroups/获取一些外国的英文新闻内容。为了比较好观察运行结果,随便从百度找了几篇新闻
测试代码

val conf = new SparkConf().setMaster("local[2]").setAppName("TF_IDF_NEWS")
var sc = new SparkContext(conf);
val sqlContext = new SQLContext(sc)

val path = "G:\\news\\*"
val rdd = sc.wholeTextFiles(path) //读取目录下所有文件:[(filename1:content1),(filename2:context2)]
val filename = rdd.map(_._1); //保存所有文件名称:[filename1,filename2],为输出结果做准备

import scala.collection.JavaConverters._
val stopWords = sc.textFile("zh-stopWords.txt").collect().toSeq.asJava //构建停词

val filter = new StopRecognition().insertStopWords(stopWords) //过滤停词
filter.insertStopNatures("w", null, "null") //根据词性过滤
val splitWordRdd = rdd.map(file => { //使用中文分词器将内容分词:[(filename1:w1 w3 w3...),(filename2:w1 w2 w3...)]
  val str = ToAnalysis.parse(file._2).recognition(filter).toStringWithOutNature(" ")
  (file._1, str.split(" "))
})
val df = sqlContext.createDataFrame(splitWordRdd).toDF("fileName", "words");

//val tokenizer = new org.apache.spark.ml.feature.RegexTokenizer().setInputCol("context").setOutputCol("words").setMinTokenLength(3)
//val words = tokenizer.transform(df)
//val stopWordsRemover = new org.apache.spark.ml.feature.StopWordsRemover().setInputCol("words").setOutputCol("stopWords");
//val stopWords = stopWordsRemover.transform(df);
//stopWords.select("stopWords").show(10, 200)
val hashingTF = new org.apache.spark.ml.feature.HashingTF()
  .setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(200000)
val documents = hashingTF.transform(df)
val idf = new org.apache.spark.ml.feature.IDF().setInputCol("rawFeatures").setOutputCol("features")
val idfModel = idf.fit(documents)
val idfData = idfModel.transform(documents)
var wordMap = df.select("words").rdd.flatMap { row => //保存所有单词以及经过hasingTf后的索引值:{单词1:索引,单词2:索引...}
  {
    row.getAs[Seq[String]](0).map { w => (hashingTF.indexOf(w), w) }
  }
}.collect().toMap
val keyWords = idfData.select("features").rdd.map { x =>
  {
    val v = x.getAs[org.apache.spark.ml.linalg.SparseVector](0)//idf结果以稀疏矩阵保存
    v.indices.zip(v.values).sortWith((a, b) => { a._2 > b._2 }).take(10).map(x => (wordMap.get(x._1).get, x._2))//根据idf值从大到小排序,取前10个,并通过索引反查到词
  }
} //[(文章1的关键词索引1:tf-idf值,文章1的关键词索引2:tf-idf值),(文章n的关键词索引1:tf-idf值,文章n的关键词索引2:tf-idf值)...],每组()表示一个新闻的关键词

filename.zip(keyWords).collect().foreach(x => {
  println(x._1)
  x._2.foreach(x => println(x._1 + ":" + x._2 + " "))
})

-代码基于spark2.1.0中的ml库api进行编写,使用ansj进行对中文内容进行分词

  1. 通过wholeTextFiles读取目录下所有的文件,本次的试验文件点击,或者随便从网上复制几个新闻
  2. 分别对每个文件的内容使用ansj进行分词,并把一些常用的中文停词过滤,还根据词性过滤了标记符号和空值,最终得到每个文档对应的独立字词
  3. 将rdd转换成DataFrame后,通过HashingTF计算词频,同时会得用哈稀转化成固定长度的特征向量
  4. 构建IDFModel 并接收由HashingTF产生的特征向量,得到每个词(索引值表示)的TF-IDF值
  5. 由于在结果中,词已经被数值化,用索引值来表示,所以需要根据索引值反射获得原始的词,需要把索引值与词的对应关系保存到wordMap中,hashingTF.indexOf即是计算词的索引值,这个方法在ml.HashingTF中是不存在的,这里稍微修改了原始类,参考https://github.com/apache/spark/pull/18736
  6. 最后根据计算得到的tf-idf值进行排序,获取最大的前10个词并输出

运行结果

file:/G:/news/世界杯后的中超怎么看?恒大有了新玩法
球员:10.995488782489861
队:10.079198050615705
预备队:8.246616586867397
奖金:7.330325854993241
球队:7.330325854993241
一线:6.414035123119086
恒:5.497744391244931
处罚:5.497744391244931
表现:5.497744391244931
下放:4.5814536593707755
file:/G:/news/人工智能为什么要从本科生抓起?
ai:83.38245660054811
人工智能:17.409523905608946
本科:15.576942441860638
人才:15.576942441860638
研究:12.828070246238171
发展:10.995488782489861
本科生:10.995488782489861
专业:10.216512475319814
科学:10.079198050615705
国内:9.162907318741551
file:/G:/news/农业农村部猪价将处下降通道 养殖户需合理安排
新闻:4.5814536593707755
农村:4.5814536593707755
农业:4.5814536593707755
市场:4.5814536593707755
部:4.5814536593707755
生猪:3.6651629274966204
减:3.6651629274966204
同比:3.6651629274966204
价:3.6651629274966204
唐:3.6651629274966204
file:/G:/news/欧盟《通用数据保护条例》影响医疗卫生领域
医疗:11.911779514364017
患者:9.162907318741551
gdpr:8.246616586867397
收集:5.497744391244931
保健:4.5814536593707755
信息:4.086604990127926
安全:3.6651629274966204
欧盟:3.6651629274966204
了解:2.7488721956224653
使:2.7488721956224653 1234567891011121314151617181920212223242526272829303132333435363738394041424344

由于选取的几个新闻都是类型区别比较大,所以最终得到的关键词还是有比较明显的区别。分词不是十分准确,一些过滤规则应该根据业务做相应的调整。后面可以结合余弦相似性来判断不同文档的相似性来做自动分类。