您好、欢迎来到现金彩票网!
当前位置:刘伯温高手心水论坛1 > 推导树 >

word2vec原理推导与代码分析

发布时间:2019-06-26 21:49 来源:未知 编辑:admin

  本文摘录整编了一些理论介绍,推导了word2vec中的数学原理;并考察了分析了word2vec原版C代码;针对没有好用的Java实现的现状,移植了原版C程序到Java。时间和水平有限,本文没有就其发展历史展开多谈,只记录了必要的知识点,并着重关注工程实践。

  虽然我的Java方案速度比原版C程序高出1倍,在算法代码与原版C程序一致的情况下准确率仍然略低于原版C程序(不过依然是目前准确率最高的Java实现),并非完美,还有待改进。

  本文的理论部分大量参考《word2vec中的数学原理详解》,按照我这种初学者方便理解的顺序重新编排、重新叙述。题图来自siegfang的博客。我提出的Java方案基于kojisekig,我们还在跟进准确率的问题。

  注:优化版word2vec-Java已经集成到HanLP中开源,用户tiandiweizun测试反馈准确率与原本C程序没有什么区别。

  传统的语言模型中词的表示是原始的、面向字符串的。两个语义相似的词的字符串可能完全不同,比如“番茄”和“西红柿”。这给所有NLP任务都带来了挑战——字符串本身无法储存语义信息。该挑战突出表现在模型的平滑问题上:标注语料是有限的,而语言整体是无限的,传统模型无法借力未标注的海量语料,只能靠人工设计平滑算法,而这些算法往往效果甚微。

  神经概率语言模型(Neural Probabilistic Language Model)中词的表示是向量形式、面向语义的。两个语义相似的词对应的向量也是相似的,具体反映在夹角或距离上。甚至一些语义相似的二元词组中的词语对应的向量做线性减法之后得到的向量依然是相似的。词的向量表示可以显著提高传统NLP任务的性能,例如《基于神经网络的高性能依存句法分析器》中介绍的词、词性、依存关系的向量化对正确率的提升等。

  从向量的角度来看,字符串形式的词语其实是更高维、更稀疏的向量。若词汇表大小为N,每个字符串形式的词语字典序为i,则其被表示为一个N维向量,该向量的第i维为1,其他维都为0。汉语的词汇量大约在十万这个量级,十万维的向量对计算来讲绝对是个维度灾难。而word2vec得到的词的向量形式(下文简称“词向量”,更学术化的翻译是“词嵌入”)则可以自由控制维度,一般是100左右。

  word2vec作为神经概率语言模型的输入,其本身其实是神经概率模型的副产品,是为了通过神经网络学习某个语言模型而产生的中间结果。具体来说,“某个语言模型”指的是“CBOW”和“Skip-gram”。具体学习过程会用到两个降低复杂度的近似方法——Hierarchical Softmax或Negative Sampling。两个模型乘以两种方法,一共有四种实现。这些内容就是本文理论部分要详细阐明的全部了。

  无论是哪种模型,其基本网络结构都是在下图的基础上,省略掉hidden layer:

  为什么要去掉这一层呢?据说是因为word2vec的作者嫌从hidden layer到output layer的矩阵运算太多了。于是两种模型的网络结构是:

  其中w(t)代表当前词语位于句子的位置t,同理定义其他记号。在窗口内(上图为窗口大小为5),除了当前词语之外的其他词语共同构成上下文。

  CBOW 是 Continuous Bag-of-Words Model 的缩写,是一种根据上下文的词语预测当前词语的出现概率的模型。其图示如上图左。

  CBOW是已知上下文,估算当前词语的语言模型。其学习目标是最大化对数似然函数:

  输入层是上下文的词语的词向量(什么!我们不是在训练词向量吗?不不不,我们是在训练CBOW模型,词向量只是个副产品,确切来说,是CBOW模型的一个参数。训练开始的时候,词向量是个随机值,随着训练的进行不断被更新)。

  输出层输出最可能的w。由于语料库中词汇量是固定的C个,所以上述过程其实可以看做一个多分类问题。给定特征,从C个分类中挑一个。

  softmax回归需要对语料库中每个词语(类)都计算一遍输出概率并进行归一化,在几十万词汇量的语料上无疑是令人头疼的。

  不用softmax怎么样?比如SVM中的多分类,我们都知道其多分类是由二分类组合而来的:

  非叶子节点相当于一个神经元(感知机,我认为逻辑斯谛回归就是感知机的输出代入f(x)=1/(1+e^x)),二分类决策输出1或0,分别代表向下左转或向下右转;每个叶子节点代表语料库中的一个词语,于是每个词语都可以被01唯一地编码,并且其编码序列对应一个事件序列,于是我们可以计算条件概率

  -1个节点,编码从下标2开始(根节点无编码),对应的参数向量下标从1开始(根节点为1)。

  怎么最大化对数似然函数呢?分别最大化每一项即可(这应该是一种近似,最大化某一项不一定使整体增大,具体收敛的证明还不清楚)。怎么最大化每一项呢?先求函数对每个变量的偏导数,对每一个样本,代入偏导数表达式得到函数在该维度的增长梯度,然后让对应参数加上这个梯度,函数在这个维度上就增长了。这种白话描述的算法在学术上叫随机梯度上升法,详见更规范的描述。

  是机器学习的老相好——学习率,通常取0-1之间的一个值。学习率越大训练速度越快,但目标函数容易在局部区域来回抖动。

  是上下文的词向量的和,不是上下文单个词的词向量。怎么把这个更新量应用到单个词的词向量上去呢?word2vec采取的是直接将

  代表上下文中某一个单词的词向量。我认为应该也可以将其平均后更新到每个词向量上去,无非是学习率的不同,欢迎指正。

  Skip-gram只是逆转了CBOW的因果关系而已,即已知当前词语,预测上下文。

  虽然上式对比CBOW多了一个u,但给定训练实例(一个词w和它的上下文{u}),u也是固定的。所以上式其实依然只有两个变量

  通过上一章的学习,我们知道无论是CBOW还是Skip-gram模型,其实都是分类模型。对于机器学习中的分类任务,在训练的时候不但要给正例,还要给负例。对于Hierarchical Softmax,负例是二叉树的其他路径。对于Negative Sampling,负例是随机挑选出来的。据说Negative Sampling能提高速度、改进模型质量。

  给定训练样本,即一个词w和它的上下文Context(w),Context(w)是输入,w是输出。那么w就是正例,词汇表中其他的词语的就是负例。假设我们通过某种采样方法获得了负例子集NEG(w)。对于正负样本,分别定义一个标签:

  等于模型预测样本为正例的概率,当答案就是正的时候,我们希望这个概率越大越好,当答案是负的时候,我们希望它越小越好,这样才能说明该模型是个明辨是非的好同志。

  每个词都是如此,语料库有多个词,我们将g累积得到优化目标。因为对数方便计算,我们对其取对数得到目标函数:

  上文一直在用二叉树描述Hierarchical Softmax,这是因为我不想仿照大部分tutorial那样一下子拿出Huffman这么具体的细节。初期对word2vec的大框架还没把握住的时候突然看到这些细节的话,人会抓不住重点,造成学习成本无谓的上升。我当时看到有些tutorial第一节就在讲Huffman编码,还以为实现word2vec一定要用Huffman树呢。

  其实根本不是的,任何二叉树都可以。Huffman树只是二叉树中具体的一种,特别适合word2vec的训练。

  word2vec训练的时候按照词频将每个词语Huffman编码,由于Huffman编码中词频越高的词语对应的编码越短。所以越高频的词语在Hierarchical Softmax过程中经过的二分类节点就越少,整体计算量就更少了。

  任何采样算法都应该保证频次越高的样本越容易被采样出来。基本的思路是对于长度为1的线段,根据词语的词频将其公平地分配给每个词语:

  接下来我们只要生成一个0-1之间的随机数,看看落到哪个区间,就能采样到该区间对应的单词了,很公平。

  word2vec用的是一种查表的方式,将上述线段标上M个“刻度”,刻度之间的间隔是相等的,即1/M:

  接着我们就不生成0-1之间的随机数了,我们生成0-M之间的整数,去这个刻度尺上一查就能抽中一个单词了。

  在word2vec中,该“刻度尺”对应着table数组。具体实现时,对词频取了0.75次幂:

  这个幂实际上是一种“平滑”策略,能够让低频词多一些出场机会,高频词贡献一些出场机会,劫富济贫。

  类似的查表方法还有sigmoid函数的计算,因为该函数使用太频繁,而其值仅仅在靠近0的时候才会剧烈变化,远离0的方向很快趋近0和1。所以源码中也采用了“刻度查表”的方法,先算出了很多个刻度对应的函数值,运算中直接查表。这部分对应:

  关于如何多线程并行训练的问题,我没看代码之前也想过。大致就是将语料按照线程数均分,大家分头算,更新参数的过程中做好线程同步的工作。

  后来看了原版C代码,原来作者压根就没做线程同步,一个全局的数组,大家都往里面写,万一下标冲突了怎么办?那就让它冲突呗……数组那么大(在text8语料上是一千万),线程那么少,冲突的概率不大吧。

  又一份C++11的实现,虽然星星很多,但据说准确率惨不忍睹,并且作者没有解释。在较早的一份issue(就是由一份Java版的作者siegfang提出的)中,作者表示“I am not sure if my implementation is accurate”。另外Google论坛上有人表示该C++11实现只有原版C实现速度的一半。所以我认为这两个版本都应该谨慎使用。

  一份Java实现,使用了很多Google的库,校正了一些原版的错误,阉割掉了k-means,从代码质量上讲总体是一份不错的实现。其输出的bin模型与原版C程序直接兼容,然而并不支持宽字符(需要改改,有个pull request做了,但作者一直没merge)。我测试了其准确率,比原版低20%左右。从这一点来讲,该实现没有多大价值。

  一份Java实现,是我见过最忠于原版的Java实现。卖点是不但可以用文本文件训练,还可以直接用Lucene的index训练。对于文本,由于Java没有“读取下一个String”的IO接口,作者实现了一个TextFileCorpus.nextWord。该方法读取一行并且拆分成字符串数组,然而text8整个文件也就一行,所以会频繁地多次读取(多个线程),然后OOM。作者提供一个切割程序,将text8切成多行,这样才能训练text8。作者并没有做准确率评测,我移植了谷歌的评测程序,并提交给了作者。我还将评测结果做了对比,比原版低10%左右,也报告给了作者,有志于开源项目的朋友可以持续参与讨论。事实上,这份实现的价值是最高的,因为它的准确率是Java中最高的。

  一份Java实现,卖点是并行化(其实上所有开源的都支持并行化);内存占用较大(Java的通病),据作者siegfang讲参考了上述C++11实现。然而上梁不正,下梁能好到哪去。既不支持negative sampling,又不能保证准确率,毫无亮点。

  事实上,上述实现都没入这位日本友人的评测单,他建议或者原版,或者gensim。

  在C和Python界,我们分别有原版C程序和gensim可以用;在Java界,没有足够好用的开源实现。鉴于此,我决定在kojisekig的基础上优化一下,目前效果如下——

  每个线程每秒训练的词语稳定在180-190K,比原版C程序要快2.5倍左右;训练速度比C程序要快的原因是,原版C程序读取单词后需要去char数组里遍历查找id;而我的Java实现直接读取缓存文件中的id,当然开始训练前要先进行词-id的转换并输出到缓存文件,这个过程大约多花一两分钟时间,相较于训练时间,无疑是值得的。这样改进之后还可以直接读取类似text8那样的变态语料,一举多得。

  由于并没有在算法层找出问题所在,所以准确率依然和kojisekig的一样,欢迎围观。

  讲的挺清晰的,看网上别的文章看的有点迷糊,看到这篇算是理解word2vec了,感谢博主。

  关于文章上的Negative Sampling 一节中,有一句话:“对于机器学习中的分类任务,在训练的时候不但要给正例,还要给负例。对于Hierarchical Softmax,负例放在二叉树的根节点上。”这句话中的:“对于Hierarchical Softmax,负例放在二叉树的根节点上。”让人很难理解。以CBOW模型为例,采用Hierarchical Softmax进行训练,其目的是最大化p(wcontext(w)),负例应该是针对某个特定的词w而言的,某个结点是词w1的负例,也可以是另一个词w2的正例。负例定义应该是:从Huffman树根结点到 词w 的路径上的 那些 没有往词w 分支上走的结点。因为在《word2vec中的数学原理详解》讲到:“对于词典D中的任意词w,在Huffman树中存在唯一条从根结点到w的路径,这条路径上的每个分支视为一次二分类,这些二分类的概率乘起来就是p(wcontext(w))”

  非常好,对于Hierarchical Softmax 对中提到了为什么对每一项最优化等于全局最优化,我的认识是如果将所有二分类所有的误差作为总的误差,然后对每一项求导,结果对于\theta来说,就是L(w,j)对\theta的偏导,对X_w的偏导就是所有这些之和,不清楚我的理解是否正确。

  您好,博主,非常感谢您的分享。关于理论部分,您也是参考的《word2vec_中的数学原理详解》吧。关于Negative Sample中的Skip-gram那的目标函数,我有一点疑问。 Skip-gram的优化目标是:

  P(Context(w)w)吧,那么采样的画,是不是应该采样的是Context(w)而不是w呢,但是公式g(w)中,采样是对u采样的(u包含了w),但是w^确实固定的。麻烦您有空的时候帮忙解答一下,我理解上是否有误。非常感谢

http://ivansolano.com/tuidaoshu/247.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有