您的位置:首页 > 其它

文本特征提取_01:词项文档矩阵

2017-01-18 17:49 162 查看
王小草SparkML笔记

笔记整理时间:2017年1月8日

笔记整理者:王小草

今日计事:

阴天,不寒冷。给古筝换上断了的新弦,调音,然后“当”地在第一根就又断了。

恩,天意,坐回电脑前。

1. 特征提取概述

什么是特征提取?

特征提取在英文的表述里就做feature extraction。

我们在做机器学习模型的时候,都会用的“特征”,对于有些对象,比如征信系统中预测贷款人是否会违约,可以使用特征:违约人的年龄,收入,家庭状况,固定资产价值等等指标来作为模型的特征,这些特征都是显而易见的可以被直接用数字与向量来表示的。但是如果我们要对一篇文本来做分析,就需要我们使用一些方法来提取文本中隐含的特征了。也就是说,模型本省是无法识别整篇文本在讲什么,有什么特征,需要我们人为地去提取特征,将文本转换成计算机能读懂的数字与向量的表征,才能进入下一步的建模。

那么我们要如何去提取文本的特征呢?关于文本的表征有许多方法,本文主要介绍一种最常用,效果也是比较好的方法:建立词项-文档矩阵。

2.词项-文档矩阵

2.1 概念

词项-文档矩阵,简写为TF-IDF(term frequency times inverse document frequency)

矩阵中的每个元素值代表了相应行上的词项对应于相应列上的文档的权重,即这个词对于这篇文章来说的重要程度。

2.2 原理

一个词对于一篇文章是否重要,体现在两个方面:

一个词项在一个文档中出现次数越多,他相对于文档的重要性就越大,这个指标就是我们的TF值

TF = 词项在该文档中出现的次数/这篇文档所有的词项数

若词项在整个语料库中出现的次数越多,那么对于某一篇文章而言这个词就越没有意义,即越不重要。我们用逆文档频率idf来表示这个词在整个语料库中的重要程度,故,出现越多的词,idf值会越低,出现越少的词,idf值会越高

IDF = log(N/d)

N为所有文档的总数;d为出现过某个单词的文档的总数。

在现实中,词项在语料中的频率往往呈指数型。一个常用词出现的次数往往是一个次常用词出现次数的几十倍,这样常用词的权重会非常低(如“的”这样的词N/D几乎可能几乎等于1),故我们对逆文档频率取对数log,如此,文档频率的差别就从乘数变成了加数级了。

综上所述,我们在计算词项在一个文档中的重要程度,要考虑以上两个因素,用TF-IDF值来共同衡量

TF-IDF = TF * IDF

2.3 假设

该算法将每个文档看成是词项的集合,并没有考虑词项的顺序,句式结构和否定情况

2.4 存储格式

由于词项的数目非常之大,而文档数目远远小于词项,所以词项-文档矩阵应该是一个稀疏向量组成的行矩阵,每个向量代表一个文档

2.5 计算过程

(1)读入文档

在本地存储着的数据应该是一篇文章占一行,读入后,每篇文章是一个元素。

富途证券提供了安全性与可靠性的投资方式。
王宝强离婚案遭广大网友关注。大家表示心疼宝宝。
陆家嘴事件网友疑是炒作嫌疑。优衣库表示不服。
...
...


(2)分词

在计算词项-文档矩阵之前,我们需要将每篇文章进行分词,使得每篇文档都转换为了由词项组成的向量。因为我们往往是针对中文分词,比英文分词的难度与复杂度都大许多。一般使用词库加隐马模型或条件随机场来做中文分词与词性标注,这个在我的机器学习笔记中都有详细介绍,在此不赘述。分词后的效果如下:

[富途证券,提供,了,安全性,与,可靠性,的,投资,方式]
[王宝强,离婚案,遭,广,大网友,关注,大家,表示,心疼,宝宝]
[陆家嘴,事件,网友,疑,是,炒作,嫌疑,优衣库,表示,不服]
...
...


每一篇文章变成了一个文档向量,向量是由词组成的。如此我们就可以去计算词频与逆文档频率。

(3)建立字典

将语料库中的所有词去重后建立索引,就形成了一个(索引,词)对应的字典。

((0,富途证券),(1,提供),(2,了),(3,安全性),(4,与),(5,可靠性),(6,的),(7,投资),(8,方式),(9,王宝强),(10,离婚案),(11,遭),(12,广大),(13,关注),(14,大家),(15,表示),(16,心疼),(17,宝宝),(18,陆家嘴),(19,事件),(20,网友),(21,疑),(22,是),(23,炒作),(24,嫌疑),(25,优衣库),(26,表示),(27,不服))


语料库总共有29个词,去重后是28个词,从0开始建索引。

(4)计算TF值

在分词后的文档向量中,用索引来代替每个词

[0,1,2,3,4,5,6,7,8]
[9,10,11,12,20,13,14,15,16,17]
[18,19,20,21,22,23,24,25,26,27]
...
...


然后对每篇文档中每个词出现的频率进行计算.现在每篇文章仍然是一个向量,向量的长度是词典的长度(28),每篇文档的向量长度都一致。于是就有了n*m的一个矩阵,n是文档的数量,m是词典的长度,矩阵中的每一个值表示在该文档中该词出现的频数。

1,1,1,1,1,1,1,1,1,0,0,0,...,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0...,0
0,0,0,0,0,0,0,0,0,0,0,...0,1,1,1,1,1,1,1,1,1,1


如果将频数都除以词典的长度,也就是向量的长度,就是文档的词频率,也就是前文所说的TF值。

(5)计算IDF值

根据IDF的公式,我们可以去计算出逆文档频率:IDF = log(N/d)

对于每一个词,计算N:所有文档的总是,d:出现改词的文档的总数,然后代入上式,求得每个词的idf值

求出每个词的idf,也可以表示成一个n*m的矩阵。

(6)计算TF-IDF值

将每个词的TF值与IDF值相乘,即得到最终我们想求得的TF-IDF值。

这个值代表着这个词对所在文档的重要程度。只要那些在自己文档中出现得多,在其它文档中出现得少的才会更重要。

2.6 代码实现

spark mllib已经为我们封装好了计算tf 与 idf的 函数。我们直接调用计算即可:

import org.apache.spark.mllib.feature.{HashingTF, IDF}
import org.apache.spark.mllib.linalg.{SparseVector => SV, Vectors}

//转换格式
val totalWords = data.map(x=>x(1).split(",")).map(x=>x.toSeq)

//默认的特征维度为2^20,此处我们可自定义2^18
val dim = math.pow(2,10).toInt

//创建一个特征维度为2^8的HashingTF
val hashingTF = new HashingTF(dim)

//利用transform方法将输入文档映射到Vector对象
val tf = hashingTF.transform(totalWords)
tf.cache()

//创建IDF实例并调用RDD中的fit方法,利用词频向量输入来对文库中的每个单词计算逆文档频率
val idf = new IDF().fit(tf)

//使用IDF的transform方法转换词频向量为TF-IDF向量
val tfidf = idf.transform(tf)
tfidf.cache()


打印tfidf:

val V2 = idf.first.asInstanceOf[SV]   //取第一组数据(即第一篇文档)
println(v2.values.szie)  //打印value的大小,就是文档中词项的数目
println(v2.values.take(10).toSeq)  //value指的是tfidf值
println(v2.indices.take(10).toSeq) //indices对应的是每个词的索引


spark2.0出来之后,在官网上写了说明,说以后mllib接下去都不会再更新,直到将ml更新到mllib的程度时,将会删除mllib。也就是说使用dataframe比rdd在处理数据与机器学习中更加用户友好,所以以后都会侧重于datarame。于是,我们就来关注dataframe的形式如何去做TFIDF的特征提取。

/**
* Created by cc on 17-1-10.
*/
object FeatureExtraction {

def main(args: Array[String]) {

Logger.getLogger("org.apache.spark").setLevel(Level.WARN)

val spark = SparkSession
.builder()
.appName("Feature Extraction")
.config("spark.some.config.option", "some-value")
.getOrCreate()

// 1.TF-IDF
// 人为地创建一个df,由两列组成,第二列为句子,并添加列名
val sentenceData = spark.createDataFrame(Seq(
(0.0, "Hi I heard about Spark"),
(0.0, "I wish Java could use case classes"),
(1.0, "Logistic regression models are neat")
)).toDF("label", "sentence")

// 将句子进行分词
val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
val wordsData = tokenizer.transform(sentenceData)
// 打印出来看下下,多了一列为分好词的句子,并且都转换成了小写
wordsData.show()

// 将每个词建立索引,并计算每个文档中的词频
val hashingTF = new HashingTF()
.setInputCol("words").setOutputCol("rawFeature").setNumFeatures(20)
val featurizedData = hashingTF.transform(wordsData)
featurizedData.show()

// 计算文档中每个词的idf值
// 利用所有语料库建立idf模型
val idf = new IDF().setInputCol("rawFeature").setOutputCol("feature")
val idfModel = idf.fit(featurizedData)
// 调用模型预测idf值
val rescaledData = idfModel.transform(featurizedData)
rescaledData.select("label", "feature").show()

spark.stop()
}

}


对应的打印结果:

+-----+--------------------+--------------------+
|label|            sentence|               words|
+-----+--------------------+--------------------+
|  0.0|Hi I heard about ...|[hi, i, heard, ab...|
|  0.0|I wish Java could...|[i, wish, java, c...|
|  1.0|Logistic regressi...|[logistic, regres...|
+-----+--------------------+--------------------+

+-----+--------------------+--------------------+--------------------+
|label|            sentence|               words|          rawFeature|
+-----+--------------------+--------------------+--------------------+
|  0.0|Hi I heard about ...|[hi, i, heard, ab...|(20,[0,5,9,17],[1...|
|  0.0|I wish Java could...|[i, wish, java, c...|(20,[2,7,9,13,15]...|
|  1.0|Logistic regressi...|[logistic, regres...|(20,[4,6,13,15,18...|
+-----+--------------------+--------------------+--------------------+

+--------------------+
|             feature|
+--------------------+
|(20,[0,5,9,17],[0...|
|(20,[2,7,9,13,15]...|
|(20,[4,6,13,15,18...|
+--------------------+
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: