Word2Vec源码解析
Reference:http://blog.csdn.net/itplus/article/details/37969519 (Word2Vec解析(部分有错))
源码:http://pan.baidu.com/s/1o6KddOI
Word2Vec中的Coding技巧
1.1 ReadWord()
训练语料每个句子呈一行。ReadWord()逐个对输入流读字符。
特判的换行符,第一次遇到换行符,会把换行符退流。这样下一次单独遇到换行符,
此时a=0,直接生成结尾符单词$</s>$,这个词在Hash表中处于0号位置。
只要Hash到0,说明一个句子处理完了,跳过这个词。进入下一个句子。
1.2 Hash表
为了执行速度,Word2Vec放弃了C++,因而不能用map做Hash。
因而里面手写了Hash表进行词Hash。使用线性探测,默认Hash数组30M。
AddWordToVocab()会根据情况自动扩空间。
亚系字符,因为编码默认使用UTF8,也可以被Hash。但是需要分词。
1.3 排序与词剪枝
由于构建Huffman树的需要,做好Vocab库后,对Hash表,按词频从大到小排序。
并剔除低频词(min_count=5),重新调整Hash表空间。
除此之外,在从文件创建Vocab时候,会自动用ReduceVocab()剪枝。
当VocabSize达到Hash表70%容量时,先剪掉频率1的全部词。然后下次再负荷,则对频率2下手。
1.4 Huffman树
在Hierarchical Softmax优化方法中使用。
直接计算Softmax函数是不切实际的,$P(W_{t}|W_{t-N}....,W_{t-1},W_{t+1}....,W_{t+N})=\frac{e^{W_{t}X+b_{t}}}{\sum_{i=1}^{V}e^{W_{i}X+b_{i}}}$
底部的归一化因子直接关系到Vocab大小,有10^5,每算一个概率都要计算10^5次矩阵乘法,不现实。
最早的Solution是Hierarchical Softmax,即把Softmax做成一颗二叉树,计算一次概率,最坏要跑$O(logV)$结点(矩阵乘法)。
至于为什么采用Huffman树,原因有二:
①Huffman树是满二叉树,从BST角度来看,平衡性最好。
②Huffman树可以构成优先队列,对于非随机访问往往奇效。
为什么是非随机访问?因为根据生活常识,高频词经常被访问(废话=。=)
这样,按照词频降序建立Huffman树,保证了高频词接近Root,这样高频词计算少,低频词计算多,贪心优化思想。
Word2Vec的构建代码非常巧妙,利用数组下标的移动就完成了构建、编码。
它最重要的是只用了parent_node这个数组来标记生成的Parent结点(范围$[VocabSize,VocabSize*2-2]$)
最后对Parent结点减去VocabSize,得到从0开始的Point路径数组。剩余细节在下文描述。
1.5 网络参数、初始化
syn0数组存着Vocab的全部词向量,大小$|V|*|M|$,初始化范围$[\frac{-0.5}{M},\frac{0.5}{M}]$,经验规则。
syn1数组存着Hierarchical Softmax的参数,大小$|V|*|M|$,初始化全为0,经验规则。实际使用$|V-1|$组。
syn1neg数组存着Negative Sampling的参数,大小$|V|*|M|$,初始化全为0,经验规则。
Word2Vec:CBOW(Continues Bag of Word)模型
2.1 One-Hot Represention with BOW
在One-Hot Represention中,如果一个句子中出现相同的词,那么只用0/1来编码明显不准确。
词袋模型(BOW),允许将重复的词叠加,就像把重复的词装进一个袋子一样,以此来增加句子的信度。
它基于朴素贝叶斯的独立性假设,将不同位置出现的相同词,看作是等价的,但是仍然无视了语义、语法、关联性。
2.2 Distributed Represention with CBOW
Bengio的模型中,为了让词向量得到词序信息,输入层对N-Gram的N个词进行拼接,增加了计算压力。
CBOW中取消了训练词序信息,将N个词各个维度求和,并且取平均,构成一个新的平均词向量$W_{{\tilde{x}}}$。
同时,为了得到更好的语义、语法信息,采用窗口扫描上下文法,即预测第$i$个词,不仅和前N个词有关,还和后N个词有关。
对于单个句子,需要优化的目标函数:$arg\max \limits_{Vec\&W}\frac{1}{T}\sum_{t=1}^{T}logP(W_{t}|W_{{\tilde{x}}})$
T为滑动窗口数。
2.3.1 Hierarchical Softmax近似优化求解$P(W_{obj}|W_{{\tilde{x}}})$
传统的Softmax可以看成是一个线性表,平均查找时间$O(n)$
HS方法将Softmax做成一颗平衡的满二叉树,维护词频后,变成Huffman树。
这样,原本的Softmax问题,被近似退化成了近似$log(K)$个Logistic回归组合成决策树。
Softmax的K组$\theta$,现在变成了K-1组,代表着二叉树的K-1个非叶结点。在Word2Vec中,由syn1数组存放,。
范围$[0*layerSize\sim(vocabSize - 2)*layerSize]$。因为Huffman树是倒着编码的,所以数组尾正好是树的头。
如:syn1数组中,$syn1[(vocabSize - 2)*layerSize]$就是Root的参数$\theta$。(不要问我为什么要-2,因为下标从零开始)
Word2Vec规定,每次Logistic回归,$Label=1-HuffmanCode$,Label和编码正好是相反的。
比如现在要利用$W_{{\tilde{x}}}$预测$love$这个词, 从Root到love这个词的路径上,有三个结点(Node 1、2、3),两个编码01。
那么(注意,Node指的是Huffman编码,而后面Sigmoid算出的是标签,所以和Logistic回归正好相反):
Step1: $P(Node_{2}=0|W_{{\tilde{x}}},\theta_{1})=\sigma(\theta_{1}W_{{\tilde{x}}})$
Step2: $P(Node_{3}=1|W_{{\tilde{x}}},\theta_{2})=1-\sigma(\theta_{2}W_{{\tilde{x}}})$
则$P(W_{love}|W_{{\tilde{x}}})=P(Node_{2}=0|W_{{\tilde{x}}},\theta_{1}) \cdot P(Node_{3}=1|W_{{\tilde{x}}},\theta_{2})$
将每个Node写成完整的判别模型概率式: $P(Node\mid W_{{\tilde{x}}},\theta)=\sigma(\theta W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta W_{{\tilde{x}}}))^{y}$
将路径上所有Node连锁起来,得到概率积:
$P(W_{obj}|W_{{\tilde{x}}})=\prod_{i=0}^{len(Code)-1}\sigma(\theta_{i}W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta_{i}W_{{\tilde{x}}}))^{y}\qquad y\propto \begin{Bmatrix}{{0,1}}\end{Bmatrix}\quad and \quad y=HuffmanCode$
Word2Vec中vocab_word结构体有两个数组变量负责这部分:
$int *point\quad--\quad Node\\char *code\quad--\quad HuffmanCode$
一个容易混掉的地方:
$vocab[word].code[d]$ 指的是,当前单词word的,第d个编码,编码不含Root结点
$vocab[word].point[d]$ 指的是,当前单词word,第d个编码下,前置结点。
比如$vocab[word].point[0]$ 肯定是Root结点,而 $vocab[word].code[0]$ 肯定是Root结点走到下一个点的编码。
正好错开了,这样就可以一步计算出 $P(Node\mid W_{{\tilde{x}}},\theta)=\sigma(\theta W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta W_{{\tilde{x}}}))^{y}$
这种避免回溯搜索对应路径的预处理trick在 $CreateBinaryTree()$ 函数中实现。
2.3.2 Hierarchical Softmax的随机梯度更新
判别模型 $P(W_{obj}|W_{{\tilde{x}}})$ 需要更新的是 $W_{{\tilde{x}}}$,由于 $W_{{\tilde{x}}}$ 是个平均项。
源码中的做法是对于原始SUM的全部输入,逐一且统一更新 $W_{{\tilde{x}}}$ 的梯度项。(注意,这种近似不是一个好主意)
先对目标函数取对数:
$\zeta =\frac{1}{T}\sum_{t=1}^{T}\sum_{i=0}^{len(Code)-1}\quad(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]$
当然,Word2Vec中没有去实现麻烦的批梯度更新,而是对于每移动到一个中心词t,就更新一下,单样本目标函数:
$\zeta^{'} =\sum_{i=0}^{len(Code)-1}\quad(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]$
对$W_{{\tilde{x}}}$的梯度:
$\frac{\partial \zeta^{'}}{\partial W_{{\tilde{x}}}}=\sum_{i=0}^{len(Code)-1}\frac{\partial [\,(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]\,]}{\partial W_{{\tilde{x}}}}\\\\\quad \, \, \, \, =\,\sum_{i=0}^{len(Code)-1}(1-y)\cdot\theta_{i}\cdot[(1-\sigma(\theta_{i}W_{{\tilde{x}}})]-y\cdot\theta_{i}\cdot\sigma(\theta_{i}W_{{\tilde{x}}})\\\\\quad \, \, \, \, =\,\sum_{i=0}^{len(Code)-1}(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot\theta_{i}$
对$\theta_{i}$的梯度,和上面有点对称,只不过没有 $\sum$ 了,所以源码里,每pass一个Node,就更新:
$\frac{\partial \zeta^{'}}{\partial \theta_{i}}=\,(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot W_{{\tilde{x}}}$
更新流程:
$UPDATE\_CBOW\_HIERARCHICAL\,SOFTMAX(W_{t})\\neu1e=0\\W_{{\tilde{x}}}\leftarrow Sum\&Avg(W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\for \quad i=0 \quad to \quad len(W_{t}.code-1)\\\qquad f=\sigma(W_{{\tilde{x}}}\theta_{i})\\\qquad g=(1-code-f)\cdot LearningRate\\\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad \theta_{i}=\theta_{i}+g\cdot W_{{\tilde{x}}}\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\ \qquad\qquad W=W+neu1e$
2.4.1 Negative Sampling简述
引入噪声的Negative Examples,可以用来替代$P(W_{t}|W_{t-N}....,W_{t-1},W_{t+1}....,W_{t+N})$。[licstar的Blog]
最早这么做的是[Collobert&Weston08]中的,替代Bengio的Softmax的单个词打分目标函数:
$\sum\limits_{x\in \mathfrak{X}} { \sum\limits_{w\in \mathfrak{D}} {\max \{0 , 1-f(x)+f(x^{(w)})\} } }$
他们的做法是,对于一个N-Gram的输入$x$,枚举整个语料库$\mathfrak{D}$, 每次取出一个词$w$,替换N-Gram的中心词,build成$x^{(w)}$
这样,每次为正样本打$f(x)$分,噪声负样本打$f(x^{(w)})$,训练使得,正样本得分高,负样本得分低而负分滚粗。
理论上的工作,来自[Gutmann12],论文提出NCE方法,用原始集X,生成噪声集Y,通过Logistic回归训练出概率密度函数之比:
$\frac{p_{d}}{p_{n}} \quad where \quad d \in Positive,n\in Negative$
而$\frac{p_{d}}{p_{n}}\approx Softmax$ (个人理解,[Gutmann12]中并没有这么说,但是[Mikolov13]中一提而过)
[Gutmann12]中,Logistic回归整体的目标函数为:
$J(\theta)=\frac{1}{T_{d}}\begin{Bmatrix} \sum_{t=1}^{T_{d}}ln[h(x_{t};\theta)]+\sum_{t=1}^{T_{n}}ln[1-h(y_{t};\theta)]] \end{Bmatrix} \quad \quad \\where \quad T_{d}=Num(Positive),T_{n}=v*T_{d}=Num(Negative)$
对于Softmax函数而言,每次只需要一个给定的预测正样本$W_{t}$,数个随机抽取的词作为预测负样本(源码里默认设定是5)。
2.4.2 采样Negative Examples
随机抽取负样本是件苦差事。
随机数生成满足是均匀分布,而取词概率可不是均匀分布,其概率应当随着词频大小变化。
词频高的词容易被随机到,而词频低的词不容易被随机到。
按照 peghoty 中的理解,源码中做法是将词频转换为线段长度:
$len(W)=\frac{W.cnt}{\sum\limits_{U\in Vocab}U.cnt}$
这样就生成了一条词频线段,接下来就是随机Roll点了:
源码中有几点说明:
①词频默认被幂了3/4 ,这样缩短了每个词线段的长度,增强了Roll出的每个点的分布性。
当然,幂不能过小,否则会导致词线段长度整体不够,后面的Roll出点都映射到最后一个词线段上。
②没有刻意去连接所有词线段,而是动态连接。一旦Roll点进度超出词线段总长度,就扩展一条词线段。
Roll点长度=当前Roll点序号/全部Roll点数(VocabSize)
2.4.3 Negative Sampling的随机梯度更新
源码中的syn1neg数组存着负采样的参数,该参数和HS的数组syn1是独立的。
用6个Logistic回归退化Softmax后,得到:
$P(W_{obj}|W_{{\tilde{x}}})\approx\prod_{i=0}^{5}P(W_{Sampling}^{i}|W_{{\tilde{x}}})\\\\\left\{\begin{matrix}W_{Sampling}^{i}=Positive \quad (i=0)\\
W_{Sampling}^{i}=Negative \quad (i>0)\end{matrix}\right.$
接下来的Logistic回归就比较简单了,由于此时不是Huffman编码,所以标签不用颠倒了,有单样本对数似然函数:
$\zeta^{'}=\sum_{i=0}^{5}y\cdot log [\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]+(1-y)\cdot log[1-\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]$
如果你熟悉Logistic回归,应该已经熟记Logistic回归参数梯度的优美式子。
对 $W_{{\tilde{x}}}$ 的梯度:
$\frac{\partial \zeta^{'}}{\partial W_{{\tilde{x}}}}=\sum_{i=0}^{5}[y-\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]\cdot\theta_{neg}^{i}$
对 $\theta_{neg}^{i}$ 的梯度:
$\frac{\partial \zeta^{'}}{\partial \theta_{neg}^{i}}=[y-\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]\cdot W_{{\tilde{x}}}$
更新流程:
$UPDATE\_CBOW\_NEGATIVE\,SAMPLING(W_{t})\\neu1e=0\\W_{{\tilde{x}}}\leftarrow Sum\&Avg(W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\for \quad i=0 \quad to \quad negative\\\qquad f=\sigma(W_{{\tilde{x}}}\theta_{neg}^{i})\\\qquad g=(label-f)\cdot LearningRate\\\qquad neu1e=neu1e+g\cdot\theta_{neg}^{i}\\\qquad \theta_{neg}^{i}=\theta_{neg}^{i}+g\cdot W_{{\tilde{x}}}\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\ \qquad\qquad W=W+neu1e$
Word2Vec:Skip-Gram 模型
3.1 起源
Skip-Gram是Mikolov在Word2Vec中祭出的大杀器,用于解决CBOW的精度问题。
它不再将输入求和平均化,而是对称预测。
需要优化目标函数:$arg\max \limits_{Vec\&W}\frac{1}{T}\sum_{t=1}^{T}\sum_{-c<=j<=c,j\neq 0}logP(W_{t+j}|W_{t})$
3.2 握手游戏
Skip-Gram是个容易让人误解的模型,主要是因为 $P(W_{t+j}|W_{t})$ ,以及上面那张图。
你可能会不屑一笑:"啊,Skip-Gram不就是用中心词预测两侧词嘛,不就是CBOW的颠倒版!”
实际上,Skip-Gram可不是简单的颠倒版,它是用 每个词,预测窗口内,除它以外的词。
先看一下二年级小朋友写的作文,~Link~:
握手游戏
二年级108班 朱紫曦
今天上数学课我们学了《数学广角》。下课的时候,钟老师带我们做握手游戏。首先,我和廖志杰、董恬恬,还有钟老师,我们四个人来握手。我和他们3个人每人 握了一次手就是3次。钟老师笑眯眯地和廖志杰、董恬恬每人握了一次手。最后,董恬恬和廖志杰友好地我了一次手。钟老师问同学们:“我们4人每两人握一次手 一共握了几次手?”大家齐声说:“6次!”接着,老师让我们5个人,6个人……都来做握手游戏,并能说出每两人握一次手一共握了几次手。在快乐的游戏中钟 老师还教我们一个计算握手次数的计算方法。4个人每两个人握一次手,握手次数是:1+2+3;5个人这样握手次数是:1+2+3+4:;10个人这样握手 次数是1+2+3+4+5+6+7+8+9;100个这样握手次数是1+2+3+4……+98+99。
在这个数学握手游戏,不仅让我们开学,还让我们学到了知识。
Skip-Gram模型是握手游戏的规则修改版,它假设A和B握手、B与A握手是不同的( 贝叶斯条件概率公式不同 )
仔细回想一下目标函数中的t循环了一个句子中的每个词,对于每个t,它和其他的词都握了一次手。
假设一个句子有10个词,第一个词和剩余9个词握手,第二个词和剩余9个词握手.....,请问一共握了多少次手?
噢哟,10*9=90次嘛!对,目标函数里就有90个条件概率。
3.3 Hierarchical Softmax的随机梯度更新
迷人的Skip-Gram,以至于让 http://blog.csdn.net/itplus/article/details/37969979 这篇非常棒的文章都写反写错了。
直接贴算法流程了:
$UPDATE\_SKIPGRAM\_HIERARCHICAL\,SOFTMAX(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad len(W_{t}.Code)-1\\\qquad\qquad f=\sigma(W\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W\\\qquad W=W+neule$
再贴下作者 peghoty 理解错误的算法流程:
$UPDATE\_SKIPGRAM\_HIERARCHICAL\,SOFTMAX(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad len(W.Code)-1\\\qquad\qquad f=\sigma(W_{t}\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W_{t}\\\qquad W_{t}=W_{t}+neule$
至于作者 peghoty 为什么会犯这样的错误,正如前面所说:
大家都被$P(W_{t+j}|W_{t})$给误导了,认为是中心词预测两侧词,所以每次就更新中心词。这是错误的。
引用我在原文里的评论:
回复neopenx:补充一下,LZ的方法主要错在,梯度更新顺序错了。
主要原因是word2vec源码中使用了随机梯度训练,之所以不采样完全梯度,是因为梯度矩阵太难算(Theano等可以直接一步求导,但是没法做HS)
正常情况下,如果是完全梯度的话,需要等这个句子跑完之后,再更新。所以对于一个句子,先算P(4|3)还是P(3|4)其实无所谓,反正都没更新。
也就是说P(w|context(w))=P(context(w)|w)是可以颠倒的。
但是随机梯度则是在每跑一个pos后更新,本身就是一种近似。
LZ的做法是对于每个窗口,总是在更新当前pos的词向量。如P(2|4)、P(3|4)、P(5|4)、P(6|4),更新的都是4,这是一种DP思想,关键真的无后效性嘛?
明显P(5|4)时,5就没更新,却还是算了,这是错误的。
源码中则是依次更新P(4|2)、P(4|3)、P(4|5)、P(4|6),这样,对于每个pos,保证窗口词都能更新一遍,而不是盯在一个词上,有点负载均衡的味道。
最 后就是,不能简单认为Skip-Gram就是CBOW的颠倒。其实两者的差别主要在于,CBOW利用词袋模型的贝叶斯独立性假设,近似将n-gram中的 n个单词sum&avg看成一个。而Skip-Gram则是看成n个。如果完全梯度,至于P(中心|两侧)还是P(两侧|中心)其实无所谓,反正 都是一对一,和握手游戏一样,最后是对称的,肯定全被覆盖到。
如果随机梯度,那么必须先算P(中心|两侧),P(两侧|中心)是没有道理的。
对于批梯度来说,其实先更新谁都无所谓。但是如果是随机梯度,应该同样按照CBOW中的做法:
对于每个t,每次应该把握手的9个词给更新掉,不然就有点负载不均衡了。
这样,更新的时候,实际上用的是$P(W_{t}|W_{t+j})$,而不是$P(W_{t+j}|W_{t})$。
有趣的是,这却恰恰和目标函数相反,然而目前网上居然还没人从这个角度理解源码。
3.4 Negative Sampling的随机梯度更新
在Negative Sampling中, peghoty 还是没有意识到他的错误,他误认为:
源码没有按照目标函数编程,然后Balabala一大堆,实际上,NS只是在HS基础上把Huffman树去掉,主要是他HS理解错了。
但是这次却写对了算法流程。
源码的算法流程:
$UPDATE\_SKIPGRAM\_NEGATIVE\,SAMPLING(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad negative\\\qquad\qquad f=\sigma(W\theta_{neg}^{i})\\\qquad\qquad g=(label-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{neg}^{i}\\\qquad\qquad \theta_{neg}^{i}=\theta_{neg}^{i}+g\cdot W\\\qquad W=W+neule$