文本特征提取_02:Word2Vec词嵌入矩阵

版权声明:本文为王小草原创文章,要转载请先联系本人哦 https://blog.csdn.net/sinat_33761963/article/details/54631367

王小草SparkML笔记


笔记整理时间:2017年1月10日
笔记整理者:王小草

今日计事:
除开上周五在家工作,2017年的工作日从3号开始今天第5次上班迟到,无论起多早每天都是会迟几分钟。第一次挤不上地铁,第二次地铁延误,第三次地铁卡刷不出去到服务台排队,第四次上错了终点站的列车,于是今天提早半小时出门,绕远路到起点站,带上了两张充满钱的地铁卡,上车前看准了对的终点站的车,而且还侥幸偶遇了一个空位,怎么说都是万无一失的呢。然而由于地铁冷风太大,肚子疼得死去活来终于在半路舍弃我的爱座下车出站四处找厕所。。回来车站居然3次眼睁睁得经历了看着车门打开,尝试把自己塞进去,关门警报声响,赶紧把自己扯出来,等待下一班列车的残酷过程。
“滴~9点04分迟到打卡”
哈哈哈,我在笑着流泪。


上一章讲了一个文本特征的提取方法:TF-IDF词项文档矩阵。在之前的许多项目与业务中,这是选择的最优方法,对于那些建立基于文本的机器学习模型的时候,我们都会先用TF-IDF提取文档的特征,将文档变成一个词向量的形式,然后再进行模型的学习与训练。

这一章,我要讲一个更优的方法,通过Word2Vec的方法建立词嵌入矩阵,以此来作为词的表征。
之所以暂且称之为最优,是因为Word2Vec是基于神经网络来训练得到的,每个词拥有一个向量来表征它,词和词之间可以通过向量来求相似性,并且向量是非离散的,这都是是TF-IDF所不具备的优势。当然优势也不是绝对的,Word2Vec的训练需要较多的资源,耗时也相对较长。总体上而言,经过实践的检验,Word2Vec的确表现甚好。

本文的结构分成4部分:首先介绍最初的神经网络语言模型NNLM,然后介绍Word2Vec的CBOM模型, 和介绍Word2Vec的skip-gram模型,最后给出spark实现Word2Vec的代码和解释。
关于理论部分其实在“王小草深度学习笔记”中专门有一章是介绍自然语言的向量模型的,故本文将其部分内容搬运过来。要了解其他传统的文档与词的表征方式,可以直接阅读该文。

1. 神经网络语言模型NNLM

NNLM全称Neural Network Language model,直接从语言模型出发,将模型最优化过程转化为求词向量表示的过程。
既然离散的表示有辣么多缺点,于是有小伙伴就尝试着用模型最优化的过程去转换词向量了。

1.1 目标函数

NNLM的目标函数如下:
QQ截图20160822170452.png-4.3kB

比如”我/是/中国/人“这句话,Wt是“人”,QQ截图20160822170651.png-1.3kB是“人”前面的词,前面的词的长度我们叫前向窗口函数,窗口长度为n-1,因为只有前面的词,所以是非对称的前向窗口函数。也就是说目标函数求的是,当“我”“是”“中国”这几个词出现的时候,后面出现“人”的概率的最大值。
这个窗口会滑动遍历整个语料库并且求和,计算量正比与语料库的大小。

概率P满足归一化条件,这样不同位置t处的概率才能相加,即:
QQ截图20160822171131.png-4.8kB
也就是说,当出现“我”“是”“中国”这几个词时,会计算后面出现语料库中的每一个词的概率,所有的概率相加为1.

1.2 NNLM的结构

QQ截图20160822171600.png-263.2kB

首先,不要方。

然后,请看左边的神经网络图,解读这个NNLM的神经网络总共可以分成4步:
第一步:模型的输入
最底层的小绿方块是输入的数据,每一个输入是一个词,但不是一个文本形式的词,而是一个One-hot的词向量,即一个向量中,只有这个词所在的索引处为1,其他位置都为0。假设我们又40000个词,那么就有40000个词向量的输入。

第二步:词嵌入
从最后一层的小绿到最后第二层的过程叫做word-embedding,词嵌入。
是这样的,首先我们要自己初始化一个投影矩阵C(用稠密向量表示)。这个投影矩阵的行数是“维度”一般设置为500,列数是输入的词向量的大小,40000。(500*40000)
矩阵中的权值w是可以事先人为初始化,在训练模型的时候会找到最优的w值的,所以初始化的时候随意。

将输入的每个One-hot词向量都分别乘以投影矩阵C,因为每个词向量上只有自己的索引处为1,所以相乘后C中只会有对应的一列被保留,这一列就是导数第二层所得到的数据。此时One-hot的向量变成了一个500*1的向量。也就是说整一个词嵌入层有500*1*40000维。

QQ截图20160822174022.png-67.7kB

第三步:隐藏层
将词嵌入向量输入隐藏层。这里隐藏层和我们之前学的神经网络中的隐藏层是一样的,同时也是个全链接。如果隐藏层有100个神经元,那么权重θ的个数就是500*40000*100。在进行线性转换后输入激励函数tanh,激励函数的输出为隐藏层的输出。

第四步:输出层
最上面的一层是输出层,输出层的个数与输入的词的个数相同,为40000。
从激励层出来进过softmax转换,就会有40000个输出结果,每个结果是一个向量,对应一个词,向量里是这个词属于每个词的概率,如果效果好的话,应该是one-hot向量里本来是1的位置,在输出的概率向量里,应该是概率越大越接近于1才好(语无伦次了。。。)。

以上就是NNLM的结构了。然后根据上面提到的目标函数求解最大值,利用BP+SGD去寻找最优的权重θ和投影矩阵中的W值。

最后,NNLM就做好了。。。

1.3 计算复杂度

计算的复杂度如下计算
N * D + N * D * H + H * V

N是输入的词的个数,D是投影矩阵的维度,H是隐藏层的维度,V是词数

可以看出,计算的复杂度相当高啊,而且语料库中的词越多的话复杂度越大,这么逆天的复杂度要让工业界实战的小伙伴分分钟泪崩的。谷歌的高智商童鞋们肯定耐不住寂寞,于是的于是有了接下来要讲的内容。

2. Word2vec – CBOM(连续词袋)

2.1 结构

QQ截图20160823093617.png-22.5kB

由于NNLM的计算复杂度,谷歌的小伙伴们提出了word2vec的两个方法,一个是CBOM,中文一般叫连续的词袋。

计算量最大的应该是从词嵌入层到隐藏层的这一步,因为词嵌入层的输出是500*40000,隐藏层如果有100维的话,权重的数量就达到了500*40000*100个了,于是小伙伴们就想能不能压缩这个词嵌入层呢?再于是,他们将40000个维度相加成了1个维度,也就是将500*40000压缩成了500*1,现在词嵌入后的输出就变成了500维,隐藏层的计算复杂度就减小到了500*100了。

但是这样的复杂度还是没有达到完美主义学者的要求,他们又将原来的隐藏层也去掉了,直接将词嵌入的结果相加降维后输入了softmax到达输出层。这样计算的维度就从500*100 + 100 *40000变成了500*40000了。

除了以上两个变化,CBOM还有一些特殊特性。CBOM不再采用之前NNLM的前向窗口函数,而是使用双向上下文窗口,比如“我/是/中国/人”中的“中国”的概率是由左右两边的词共同影响的:P(中国/我,是,人),并且是没有顺序的:P(中国/我,是,人) = P(中国/是,我,人)

2.2 目标函数

QQ截图20160823111226.png-16.9kB

仍然是通过求目标函数的最大值来获取最优的参数。

第一个公式,w是某个词,context(w)是w的上下文,也就是左右的词,词数根据设置的窗口大小而定。

第二个公式是由第一个公式转化而来,在求最优化时我们一般将它转化成log的形式。并且代入了降维后的词嵌入矩阵到输出层的运算公式。

第三个公式是第二个公式转化而来,因为一个词的出现有多个上下文词,所以用j去遍历。

2.3 层次Softmax

虽然通过以上若干的改进,CBOM已经比NNLM要简化与优化很多了。但是虽然通过去隐层,求和等方式将维度将至了500*40000,但是好不容易降到了500,在最后一步又上升到了40000,让人内心略感不爽。所以,之后就提出了两种解决办法来降低最后一步的计算复杂度。首先介绍一下“层次Softmax”

QQ截图20160823112600.png-102.4kB

我们来看output layer的这一步,这里使用了Huffman树。如果要找到“足球”这个词的输出结果,只需要沿着这棵树的根,一直下来走4步就到了“足球”的叶子节点。如果按照之前的方法,我们需要去对40000个词都平铺计算一遍才能获得“足球”这个词的输出概率向量。

霍夫曼树上的每个节点其实可以看成是一个分类器,“足球”这个词就是经过了4个二分类的分类器计算出来的。通过这种方式,只需要计算路径傻瓜所有非叶子结点词向量的贡献即可。计算量降为树的深度:V => log_2(V)

下面是层次Softmax的目标函数:
QQ截图20160823113144.png-51.8kB

2.4 负例采样

负例采样是另一种改进输出层的方式。

在原来的输出层中,会输出40000个向量,每个向量又会有40000个维度,然后只有一个维度是正样本,V-1个(39999)为负样本,但其实我们要得到的只是哪一个正样本,所以对于那么多的负样本我们没有必要全部去计算,只需要通过某种方式去采样然后计算即可。

QQ截图20160823113501.png-33.7kB

它的目标函数是:对语料库中所有词W求和
QQ截图20160823113551.png-13.4kB

词典中的每个词对应一条线段。这些线段组成了[0,1]这个区间。
现在将[0,1]划分成M=10^8等分,每次随机生成[1,M-1]间的整数,看看这些整数落在哪个词对应的部分上。

3 Word2vec – Skip-Gram 模型

这是 Word2vec的另一个模型(Spark mmlib中提供的方法包就是依赖于这个模型的)

与CBOM不同的地方只有一个。CBOM是通过上下文去求中心词的最大概率。而Skip-Gram是通过中心词去求上下文词的最大概率。

目标函数是:
QQ截图20160823114105.png-4.3kB

概率密度有Softmax给出:
QQ截图20160823114137.png-6kB

因为其他过程与CBOM一致,故再次不赘述了。

Skip-Gram模型采取CBOW的逆过程的动机在于:CBOW算法对于很多分布式信息进行了平滑处理(例如将一整段上下文信息视为一个单一观察量)。很多情况下,对于小型的数据集,这一处理是有帮助的。相形之下,Skip-Gram模型将每个“上下文-目标词汇”的组合视为一个新观察量,这种做法在大型数据集中会更为有效。

Word2Vec存在的问题:
1.对每个local context window单独训练,没有利用包含在global cocurrence矩阵中的统计信息。

2.对多义词无法很好的表示和处理,因为使用了唯一的词向量。

4 spark实现代码

Spark只实现了Skip-Gram模型,下面来看看官方给出的案例

import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer, Word2Vec}
import org.apache.spark.sql.{Row, SparkSession}


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

  def main(args: Array[String]) {

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

    val conf = new SparkConf().setAppName("FeatureExtraction").setMaster("local")
    val sc = new SparkContext(conf)

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

    // 人为创建3篇文档,每一行一个文档,并分割成由词组成的词袋
    val documentDF = spark.createDataFrame(Seq(
      "Hi I heard about Spark".split(" "),
      "I wish Java could use case classes".split(" "),
      "Logistic regression models are neat".split(" ")
    ).map(Tuple1.apply)).toDF("text")


    // 创建word2vec模型
    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[_], feature) =>
      println(s"Text: [${text.mkString(",")}] => \nVector: $feature\n")}


    spark.stop()
  }

}

输出的结果:

Text: [Hi,I,heard,about,Spark] => 
Vector: [-0.028139343485236168,0.04554025698453188,-0.013317196490243079]

Text: [I,wish,Java,could,use,case,classes] => 
Vector: [0.06872416580361979,-0.02604914902310286,0.02165239889706884]

Text: [Logistic,regression,models,are,neat] => 
Vector: [0.023467857390642166,0.027799883112311366,0.0331136979162693]

从结果中可见,每句话输出了一个对应的大小为3的向量。
咦?这我就不懂了,不是说word2vec是为每个词建立向量吗,那一句话如果有5个词就应该有5个长度为3的向量呀。
是酱紫的,以上方法默认将一个文档中的所有词向量都求了平均数,所有最后输出1个代表文档的向量。
(这里再留个疑问,我觉得不一定词向量的均值就能很好得表示这一整篇文档)

展开阅读全文

没有更多推荐了,返回首页