原文地址
在上完CS224N头三节课后,我对word2vec没有产生很好的理解,于是在网上搜寻资料,无意间看到有人推荐这篇论文,于是立刻找来拜读,但第一次读是云里雾里的,于是在积攒了一些读论文的经验后,重看了一遍,觉得舒坦多了,特此记录一下。
这是Word2Vec原论文中提出的第一个训练任务,使用连续的上下文向量(context vector)来预测中心向量。
结构如下:
首先指定一些符号表示:
V V V:代表词表的大小,即总共有多少个词
N N N:隐藏层的单元数
W I W_I WI:从输入层到隐藏层的权重矩阵,形状为 V × N V\times N V×N
W O W_O WO:从隐藏层到输出层的权重矩阵,形状为 N × V N\times V N×V
w t w_t wt:代表输入的上下文词汇,其中 t ∈ V t\in V t∈V
w o w_o wo:代表预测的中心词
h h h:代表隐藏层
v ′ v^{'} v′:代表 W O W_O WO中的列向量
v v v:代表 W I W_I WI中的行向量
这篇博客按照该论文的思路来叙述模型,所以先从最简单的情况入手,我们假设每一次仅通过一个上下文向量来预测中心词向量,于是,现在模型输入情况如下:
输入是one-hot形式的词向量,经过矩阵 W I W_I WI映射至中间的隐藏层,由于输入是one-hot形式,因此实际上这个操作相当于按该单词在词典中的位置从矩阵 W I W_I WI取出对应行向量。这个行向量也就是我们最后希望得到的词向量。然后经过第二个矩阵 W O W_O WO,来到输出层,注意隐藏层没有使用非线性的激活函数,而是直接将多项式送到输出层,最后用softmax得到单词的概率分布。
这里,输入只有一个上下文向量,因此,我们的目标是最大化条件概率 p ( w o ∣ w t ) p(w_o\vert w_t) p(wo∣wt),即给定输入上下文向量,预测正确中心词的概率。而最大化概率,又等效于最小化概率的负对数。因此优化目标为最小化负对数似然 − log ( p ( w o ∣ w t ) ) -\log\left(p(w_o\vert w_t)\right) −log(p(wo∣wt)),其中,条件概率的计算由下式得出:
p ( w o ∣ w ) = e x p ( v j ′ T h ) ∑ j = 1 V e x p ( v j ′ T h ) p(w_o\vert w)=\frac{exp(v_j^{'T}h)}{ {\displaystyle\sum_{j=1}^V}exp(v_j^{'T}h)} p(wo∣w)=j=1∑Vexp(vj′Th)exp(vj′Th)
于是,负对数似然 E E E又可以表示为:
− log ( p ( w o ∣ w t ) ) = − ( v j ′ T h − log ( ∑ j = 1 V e x p ( v j ′ T h ) ) ) -\log\left(p(w_o\vert w_t)\right) = -(v_j^{'T}h\;-\;\log\left(\sum_{j=1}^Vexp(v_j^{'T}h)\right)) −log(p(wo∣wt))=−(vj′Th−log(∑j=1Vexp(vj′Th)))
这就是训练时我们的目标函数。其中 v j T h v_j^Th vjTh实际上是最后输出层上每个单元的输出值,相当于是每个位置的一个分数。以下用 u j u_j uj代表每个位置的分数。
然后来观察一下参数是如何更新的:
模型中我们有两套矩阵需要更新参数,首先来看输出层到隐藏层的矩阵 W O W_O WO,对其中每个元素,梯度通过以下过程求得:
首先得到 E E E对每个输出单元分数 u j u_j uj的导数
∂ E ∂ u j = y j − t = e j \frac{\partial E}{\partial u_j}\;=\;y_j\;-\;t = e_j ∂uj∂E=yj−t=ej
其中, y j y_j yj表示每个输出单元经过softmax后输出的概率, t t t在 y j y_j yj表示正确中心词时为1,其余为0。我们把这步产生的结果视作每个输出单元与真实分布的误差 e j e_j ej。
对于输出矩阵 W O W_O WO中的每个参数 w i j w_{ij} wij我们都有:
u j = ∑ i ∈ ∣ h ∣ w i j h i u_j\;=\;\sum_{i\in\left|h\right|}w_{ij}h_i uj=∑i∈∣h∣wijhi
于是由链式求导法则,得到下式:
∂ E ∂ w i j = ∂ E ∂ u j × ∂ u j ∂ w i j = e j h i \frac{\partial E}{\partial w_{ij}} = \frac{\partial E}{\partial u_j}\times\frac{\partial u_j}{\partial w_{ij}}\;=\;e_jh_i ∂wij∂E=∂uj∂E×∂wij∂uj=ejhi
对应参数使用梯度下降更新( α \alpha α代表学习率):
w i j n e w = w i j o l d − α e j h i w_{ij}^{new}\;=\;w_{ij}^{old}\;-\;\alpha e_jh_i wijnew=wijold−αejhi
也可以用向量形式表示为:
v j ′ n e w = v j ′ o l d − α e j h v_j^{'new}\;=\;v_j^{'old}\;-\;\alpha e_jh vj′new=vj′old−αejh
实际上,从公式中也可以看出,每一次更新,我们都会将输出中心词与词典中每一个词作比较,如果输出词的预测概率大于其真实概率(1或者0),即该词被错误预测,那么此时误差会大于0,更新时 W O W_O WO对应的词向量 v j ′ v_j^{'} vj′会减去这部分信息,相反,若该词是正确的预测,误差会小于0,此时 v j ′ v_j^{'} vj′会加上这部分的信息。同时,作为系数的一部分,误差 e j e_j ej也会随着预测概率与真实概率之间的接近程度而变化,从而影响信息的传递。
此外,虽然 W I W_I WI/ W O W_O WO中的行/列向量均可以视为词向量,但我们一般使用的是 W I W_I WI中的行向量。
接下来,梯度信号由隐藏层传至输出层,此时对 W I W_I WI进行参数更新,同理,我们由链式法则得到:
∂ E ∂ h i = ∑ j ∈ ∣ V ∣ ∂ E ∂ u j × ∂ u j ∂ h i = ∑ j ∈ ∣ V ∣ w i j e j \frac{\partial E}{\partial h_i}\;=\;\sum_{j\in\left|V\right|}\frac{\partial E}{\partial u_j}\times\frac{\partial u_j}{\partial h_i} = \sum_{j\in\left|V\right|}w_{ij}e_j ∂hi∂E=∑j∈∣V∣∂uj∂E×∂hi∂uj=∑j∈∣V∣wijej
∂ h i ∂ w j i = x j \frac{\partial h_i}{\partial w_{ji}}\;=\;x_j ∂wji∂hi=xj
两式相乘,就得到了更新值。注意到此处的 x j x_j xj这代表输出层的第 j j j个单元,但由于输入是one-hot形式的编码。因此上式实际上只会对输入的词汇对应的权重进行更新,也就是说,仅有输入词对应的词向量会得到更新。
于是,两套矩阵就在不断的迭代中更新。在更新过程中,可以发现, W O W_O WO中的词向量基于中心词而不断被更新,而 W I W_I WI中则只有输入词的向量在被更新。
v j n e w = v j o l d − α E T v_j^{new}\;=\;v_j^{old}\;-\;\alpha E^T vjnew=vjold−αET
向量E中的每个元素都是对应隐单元对输入词向量位置的导数。
有了单个词的经验,CBOW中的更新就比较好理解了,由于使用词袋模型,此时输入变成:
对于 C C C个输入向量,我们取其向量之和 ∑ C x j \sum_Cx_j ∑Cxj作为模型输入,此时隐藏层得到 C C C个词向量的均值。
从输出层到隐藏层到更新与单个上下文词汇的情况是完全一致的。唯一的区别在于隐藏层到输入层到更新。由于我们使用了 C C C个向量,因此在更新时需要对这 C C C个词向量进行更新。此时更新由下式完成:
v j , c n e w = v j , c o l d − 1 C α E T v_{j,c}^{new}\;=\;v_{j,c}^{old}\;-\;\frac1C\alpha E^T vj,cnew=vj,cold−C1αET
这个翻译是我瞎整的,不知道中文应该怎么翻译,skip-gram模型与CBOW相反,其任务是用给定中心词预测上下文向量,模型结构与CBOW一样,都是一个三层前馈网络。
预测单个上下文向量的情形与CBOW中单个词的情形是完全等价的。就不再多写了。
当我们要预测c个上下文向量时,情况会有所不同。在CBOW中,我们通过最终预测与单个真实中心向量的误差来更新梯度。skip-gram中,我们有多个上下文向量。因此梯度更新时需要利用多个上下文向量的误差叠加。
此时,最小化目标变为以下对数似然:
− ∑ j ∈ ∣ C ∣ u j + C log ( ∑ j ∈ ∣ V ∣ e x p ( u j ) ) -\sum_{j\in\left|C\right|}u_j\;+\;C\log\left(\sum_{j\in\left|V\right|}exp(u_j)\right) −∑j∈∣C∣uj+Clog(∑j∈∣V∣exp(uj))
CBOW中,目标函数对每个预测位置的误差 e j e_j ej变成 ∑ ∣ C ∣ e c , j \sum_{\left|C\right|}e_{c,j} ∑∣C∣ec,j,即累计 C C C个上下文向量在对应位置的误差,作为最终误差。
因此,由隐藏层到输出层的更新变为:
w i j n e w = w i j o l d − α h i ∑ ∣ C ∣ e c , j w_{ij}^{new}\;=\;w_{ij}^{old}\;-\;\alpha h_i\sum_{\left|C\right|}e_{c,j} wijnew=wijold−αhi∑∣C∣ec,j
隐藏层到输入层到更新沿用前文的形式:
v j n e w = v j o l d − α E T v_j^{new}\;=\;v_j^{old}\;-\;\alpha E^T vjnew=vjold−αET
但此时,E中的元素变为:
∑ j ∈ ∣ V ∣ E h i w i j \sum_{j\in\left|V\right|}E_{h_i}w_{ij} ∑j∈∣V∣Ehiwij
其中
E h i = ∑ ∣ C ∣ e c , j E_{h_i} = \sum_{\left|C\right|}e_{c,j} Ehi=∑∣C∣ec,j
从前文中的梯度更新公式可以看出,计算开销最大的部分就是计算softmax时的分母,每一次都是O(V)的操作,而在实践中,词典大小很可能会处于百万级别,这样的话,softmax将是一个开销很大的操作,导致训练速度变慢。
于是,在原文中,作者便提出了两个计算时的优化技巧。
层次softmax,这个操作能够将原本O(V)的操作变为O(logV)。在层次softmax中,每一个词汇被预测到的概率由一条路径决定。具体如下:
如图所示,每个叶节点代表一个词典中的词,每一个内部结点都代表一个向量 w i ′ ( i ∈ L ) w_i^{'} (i\in L) wi′(i∈L)其中 L L L代表树的深度。这些内部节点的向量是我们用来计算得分的向量,设预测词的词向量为 v v v,则每个节点代表的概率用sigmoid函数计算为 σ ( v T w i ′ ) \sigma(v{^T}w_i^{'}) σ(vTwi′),其中,每个路径的概率由下式计算:
p ( w O = w I ) = ∏ i = 1 L σ ( e ′ v T w i ′ ) p(w_O=w_I)=\prod_{i\;=\;1}^L\sigma(e^{'}v^{T}w_i^{'}) p(wO=wI)=∏i=1Lσ(e′vTwi′)
其中 e ′ e^{'} e′在当前节点为父节点的左孩子时取1,右孩子时取-1,这跟sigmoid函数的性质也有关,左右孩子的概率之和为1,且 σ ( − x ) = 1 − σ ( x ) \sigma(-x) = 1 - \sigma(x) σ(−x)=1−σ(x)。
因此,路径上的概率之积就是对应预测词的概率。这里也容易知道所有预测词概率之和为1。
原文中,这棵树被构造为二叉赫夫曼树。这样,我们就将计算复杂度降到O(logV),是一个极大的提升。
负采样也是一种降低计算复杂度的技巧,其思想是这样:每一次都要更新 W O W_O WO中的所有向量太耗时了,我们可以考虑用一部分样本的更新来取代整体的更新,感觉跟dropout的想法类似,即每次仅更新一部分权重。
这部分样本就是所谓的负样本,CS224N中这样解释这些负样本:存在一些词,他们同时出现的概率很小,甚至根本不会一同出现,比如"I the a an cold"这样毫无逻辑的序列。因此,我们目标函数就是使这些词同时出现的概率最小化。
因此目标函数变为:
E = − log ( σ ( v T h ) ) − ∑ w ∈ w n e g a t i v e log ( σ ( − w T h ) ) E\;=\;-\log\left(\sigma(v^Th)\right)\;\;-\;\sum_{w\in w_{negative}}\log\left(\sigma(-w^Th)\right) E=−log(σ(vTh))−∑w∈wnegativelog(σ(−wTh))
这样,每一次更新参数仅有负样本对应的向量得到更新,计算速度大大加快。