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_{{ ilde{x}}}$。
同时,为了得到更好的语义、语法信息,采用窗口扫描上下文法,即预测第$i$个词,不仅和前N个词有关,还和后N个词有关。
对于单个句子,需要优化的目标函数:$argmax limits_{Vec&W}frac{1}{T}sum_{t=1}^{T}logP(W_{t}|W_{{ ilde{x}}})$
T为滑动窗口数。
2.3.1 Hierarchical Softmax近似优化求解$P(W_{obj}|W_{{ ilde{x}}})$
传统的Softmax可以看成是一个线性表,平均查找时间$O(n)$
HS方法将Softmax做成一颗平衡的满二叉树,维护词频后,变成Huffman树。
这样,原本的Softmax问题,被近似退化成了近似$log(K)$个Logistic回归组合成决策树。
Softmax的K组$ heta$,现在变成了K-1组,代表着二叉树的K-1个非叶结点。在Word2Vec中,由syn1数组存放,。
范围$[0*layerSizesim(vocabSize - 2)*layerSize]$。因为Huffman树是倒着编码的,所以数组尾正好是树的头。
如:syn1数组中,$syn1[(vocabSize - 2)*layerSize]$就是Root的参数$ heta$。(不要问我为什么要-2,因为下标从零开始)
Word2Vec规定,每次Logistic回归,$Label=1-HuffmanCode$,Label和编码正好是相反的。
比如现在要利用$W_{{ ilde{x}}}$预测$love$这个词, 从Root到love这个词的路径上,有三个结点(Node 1、2、3),两个编码01。
那么(注意,Node指的是Huffman编码,而后面Sigmoid算出的是标签,所以和Logistic回归正好相反):
Step1: $P(Node_{2}=0|W_{{ ilde{x}}}, heta_{1})=sigma( heta_{1}W_{{ ilde{x}}})$
Step2: $P(Node_{3}=1|W_{{ ilde{x}}}, heta_{2})=1-sigma( heta_{2}W_{{ ilde{x}}})$
则$P(W_{love}|W_{{ ilde{x}}})=P(Node_{2}=0|W_{{ ilde{x}}}, heta_{1}) cdot P(Node_{3}=1|W_{{ ilde{x}}}, heta_{2})$
将每个Node写成完整的判别模型概率式: $P(Nodemid W_{{ ilde{x}}}, heta)=sigma( heta W_{{ ilde{x}}})^{1-y} cdot (1-sigma( heta W_{{ ilde{x}}}))^{y}$
将路径上所有Node连锁起来,得到概率积:
$P(W_{obj}|W_{{ ilde{x}}})=prod_{i=0}^{len(Code)-1}sigma( heta_{i}W_{{ ilde{x}}})^{1-y} cdot (1-sigma( heta_{i}W_{{ ilde{x}}}))^{y}qquad ypropto egin{Bmatrix}{{0,1}}end{Bmatrix}quad and quad y=HuffmanCode$
Word2Vec中vocab_word结构体有两个数组变量负责这部分:
$int *pointquad--quad Node\char *codequad--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(Nodemid W_{{ ilde{x}}}, heta)=sigma( heta W_{{ ilde{x}}})^{1-y} cdot (1-sigma( heta W_{{ ilde{x}}}))^{y}$
这种避免回溯搜索对应路径的预处理trick在 $CreateBinaryTree()$ 函数中实现。
2.3.2 Hierarchical Softmax的随机梯度更新
判别模型 $P(W_{obj}|W_{{ ilde{x}}})$ 需要更新的是 $W_{{ ilde{x}}}$,由于 $W_{{ ilde{x}}}$ 是个平均项。
源码中的做法是对于原始SUM的全部输入,逐一且统一更新 $W_{{ ilde{x}}}$ 的梯度项。(注意,这种近似不是一个好主意)
先对目标函数取对数:
$zeta =frac{1}{T}sum_{t=1}^{T}sum_{i=0}^{len(Code)-1}quad(1-y)cdotlog[sigma( heta_{i}W_{{ ilde{x}}})]+ycdotlog[1-sigma( heta_{i}W_{{ ilde{x}}})]$
当然,Word2Vec中没有去实现麻烦的批梯度更新,而是对于每移动到一个中心词t,就更新一下,单样本目标函数:
$zeta^{'} =sum_{i=0}^{len(Code)-1}quad(1-y)cdotlog[sigma( heta_{i}W_{{ ilde{x}}})]+ycdotlog[1-sigma( heta_{i}W_{{ ilde{x}}})]$
对$W_{{ ilde{x}}}$的梯度:
$frac{partial zeta^{'}}{partial W_{{ ilde{x}}}}=sum_{i=0}^{len(Code)-1}frac{partial [\,(1-y)cdotlog[sigma( heta_{i}W_{{ ilde{x}}})]+ycdotlog[1-sigma( heta_{i}W_{{ ilde{x}}})]\,]}{partial W_{{ ilde{x}}}}\\quad \, \, \, \, =\,sum_{i=0}^{len(Code)-1}(1-y)cdot heta_{i}cdot[(1-sigma( heta_{i}W_{{ ilde{x}}})]-ycdot heta_{i}cdotsigma( heta_{i}W_{{ ilde{x}}})\\quad \, \, \, \, =\,sum_{i=0}^{len(Code)-1}(1-y-sigma( heta_{i}W_{{ ilde{x}}}))cdot heta_{i}$
对$ heta_{i}$的梯度,和上面有点对称,只不过没有 $sum$ 了,所以源码里,每pass一个Node,就更新:
$frac{partial zeta^{'}}{partial heta_{i}}=\,(1-y-sigma( heta_{i}W_{{ ilde{x}}}))cdot W_{{ ilde{x}}}$
更新流程:
$UPDATE\_CBOW\_HIERARCHICAL\,SOFTMAX(W_{t})\neu1e=0\W_{{ ilde{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_{{ ilde{x}}} heta_{i})\qquad g=(1-code-f)cdot LearningRate\qquad neu1e=neu1e+gcdot heta_{i}\qquad heta_{i}= heta_{i}+gcdot W_{{ ilde{x}}}\for quad W quad in quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\ qquadqquad 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的单个词打分目标函数:
$sumlimits_{xin mathfrak{X}} { sumlimits_{win 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,nin Negative$
而$frac{p_{d}}{p_{n}}approx Softmax$ (个人理解,[Gutmann12]中并没有这么说,但是[Mikolov13]中一提而过)
[Gutmann12]中,Logistic回归整体的目标函数为:
$J( heta)=frac{1}{T_{d}}egin{Bmatrix} sum_{t=1}^{T_{d}}ln[h(x_{t}; heta)]+sum_{t=1}^{T_{n}}ln[1-h(y_{t}; heta)]] 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}{sumlimits_{Uin 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_{{ ilde{x}}})approxprod_{i=0}^{5}P(W_{Sampling}^{i}|W_{{ ilde{x}}})\\left{egin{matrix}W_{Sampling}^{i}=Positive quad (i=0)\
W_{Sampling}^{i}=Negative quad (i>0)end{matrix}
ight.$
接下来的Logistic回归就比较简单了,由于此时不是Huffman编码,所以标签不用颠倒了,有单样本对数似然函数:
$zeta^{'}=sum_{i=0}^{5}ycdot log [sigma( heta_{neg}^{i}W_{{ ilde{x}}})]+(1-y)cdot log[1-sigma( heta_{neg}^{i}W_{{ ilde{x}}})]$
如果你熟悉Logistic回归,应该已经熟记Logistic回归参数梯度的优美式子。
对 $W_{{ ilde{x}}}$ 的梯度:
$frac{partial zeta^{'}}{partial W_{{ ilde{x}}}}=sum_{i=0}^{5}[y-sigma( heta_{neg}^{i}W_{{ ilde{x}}})]cdot heta_{neg}^{i}$
对 $ heta_{neg}^{i}$ 的梯度:
$frac{partial zeta^{'}}{partial heta_{neg}^{i}}=[y-sigma( heta_{neg}^{i}W_{{ ilde{x}}})]cdot W_{{ ilde{x}}}$
更新流程:
$UPDATE\_CBOW\_NEGATIVE\,SAMPLING(W_{t})\neu1e=0\W_{{ ilde{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_{{ ilde{x}}} heta_{neg}^{i})\qquad g=(label-f)cdot LearningRate\qquad neu1e=neu1e+gcdot heta_{neg}^{i}\qquad heta_{neg}^{i}= heta_{neg}^{i}+gcdot W_{{ ilde{x}}}\for quad W quad in quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\ qquadqquad W=W+neu1e$
Word2Vec:Skip-Gram 模型
3.1 起源
Skip-Gram是Mikolov在Word2Vec中祭出的大杀器,用于解决CBOW的精度问题。
它不再将输入求和平均化,而是对称预测。
需要优化目标函数:$argmax limits_{Vec&W}frac{1}{T}sum_{t=1}^{T}sum_{-c<=j<=c,j eq 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\qquadqquad f=sigma(W heta_{i})\qquadqquad g=(1-code-f)cdot LearningRate\qquadqquad neu1e=neu1e+gcdot heta_{i}\qquadqquad heta_{i}= heta_{i}+gcdot 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\qquadqquad f=sigma(W_{t} heta_{i})\qquadqquad g=(1-code-f)cdot LearningRate\qquadqquad neu1e=neu1e+gcdot heta_{i}\qquadqquad heta_{i}= heta_{i}+gcdot 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\qquadqquad f=sigma(W heta_{neg}^{i})\qquadqquad g=(label-f)cdot LearningRate\qquadqquad neu1e=neu1e+gcdot heta_{neg}^{i}\qquadqquad heta_{neg}^{i}= heta_{neg}^{i}+gcdot W\qquad W=W+neule$