Reference:http://blog.csdn.net/itplus/article/details/37969519 (Word2Vec解析(部分有错))
源码:https://github.com/danielfrg/word2vec (Python封装C版,原Code.Google被墙了)
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()会根据情况自动扩空间。
如果要做中文词向量,需要对Unicode进行Hash,首先你需要一个支持Unicode的类库。
然后用map来Hash QString(Qt)、CString(MFC)。
最后你要把源码Re-Written一遍。
1.3 排序与词剪枝
由于构建Huffman树的需要,做好Vocab库后,对Hash表,按词频从大到小排序。
并剔除低频词(min_count=5),重新调整Hash表空间。
除此之外,在从文件创建Vocab时候,会自动用ReduceVocab()剪枝。
当VocabSize达到Hash表70%容量时,先剪掉频率1的全部词。然后下次再负荷,则对频率2下手。
1.4 Huffman树
在Hierarchical Softmax优化方法中使用。
计算传统Softmax函数是不切实际的,$P(t|t-1,t-2...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,即把Vocab做成一颗二叉树,这样计算一次概率,最坏要跑$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数组存着Softmax用的临时词向量,大小$|V|*|M|$,初始化全为0,经验规则。也就是Softmax分成$|V|$类的参数了。
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组,在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^{‘}}{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$的梯度,和上面有点对称,只不过没有 $\sum$ 了,所以源码里,每pass一个Node,就更新:
$\frac{\partial \zeta^{‘}}{\partial \theta}=\,(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot W_{{\tilde{x}}}$
更新流程:
$UPDATE\_CBOW\_HIERARCHICAL\,SOFTMAX(W_{t})\\1. \quad neu1e=0\\2. \quad W_{{\tilde{x}}}\leftarrow Sum\&Avg(W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\3. \quad for \quad i=0 \quad to \quad len(Code)-1\\\qquad\qquad f=\sigma(W_{{\tilde{x}}}\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_{{\tilde{x}}}\\4. \quad 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(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(Code)-1\\\qquad\qquad f=\sigma(W_{t}\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\quad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W\\\qquad W_{t}=W_{t}+neule$
至于作者 peghoty 为什么会犯这样的错误,正如前面所说,大家都被$P(W_{t+j}|W_{t})$给误导了,认为是中心词预测两侧词,所以每次就更新中心词。