引言
本文是对近日学习word2vec的一个总结,期间看了不少博客和论文。
word2vec是一种高效的训练词向量的模型,基于上下文相似的两个词,它们的词向量也应该相似, 比如,“A dog is running in the room"和"A cat is running in the room”。这两个句子,只是"cat"和"dog"不同,word2vec认为它们是相似的,而n-gram模型做不到这一点。
word2vec有两个模型:CBOW(COntinuous Bag of Words)和Skip-Gram。
CBOW模型中,通过一个上下文(比如说一个句子)来预测目标词;而Skip-Gram模型则相反,根据给定的输入词来预测上下文。
Skip-Gram:能够很好地处理少量的训练数据,而且能够很好地表示不常见的单词或短语
CBOW:比skip-gram训练快几倍,对出现频率高的单词的准确度稍微更好一些
Simple CBOW模型
要想理解CBOW和SkipGram模型,我们先从最简单版本的CBOW模型开始介绍,又被称为One Word模型,上下文只有一个单词,目标词也是一个单词。
意味着给定一个上下文词来预测一个目标词。有点类似bigram模型。
在上图中 V V V是词典大小, N N N是一个超参数,是隐藏层中单元数量,也是我们要学的词向量的维度,一般最多设置到300。
输入向量 x x x是 V × 1 V \times 1 V×1的one-hot向量,只有 x k = 1 \color{red}{ x_k=1} xk=1,其他都是 0 0 0。
输入层和输出层之间的权重是一个 V × N V \times N V×N的矩阵 W W W,给定一个上下文单词,隐藏层 h h h计算如下:
h = W T x = W ( k , ⋅ ) T : = v w I T (1) h = W^T x = W_{(k,\cdot)}^T := v_{w_I}^T \tag{1} h=WTx=W(k,⋅)T:=vwIT(1)
W W W是 V × N V \times N V×N。 h h h的维度是 N × 1 N \times 1 N×1
这个公式详细描述一下,展开上面的 W W W矩阵:
W V × N = [ w 11 w 12 ⋯ w 1 N w 21 w 22 ⋯ w 2 N ⋮ ⋮ ⋱ ⋮ w V 1 w V 2 ⋯ w V N ] W_{V \times N} = \left[ \begin{matrix} w_{11} & w_{12} & \cdots & w_{1N} \\ w_{21} & w_{22} & \cdots & w_{2N} \\ \vdots & \vdots & \ddots & \vdots \\ w_{V1} & w_{V2} & \cdots & w_{VN} \end{matrix} \right] WV×N=⎣⎢⎢⎢⎡w11w21⋮wV1w12w22⋮wV2⋯⋯⋱⋯w1Nw2N⋮wVN⎦⎥⎥⎥⎤
x x x:
x = [ x 1 x 2 ⋮ x V ] x = \left[ \begin{matrix} x_1 \\ x_2 \\ \vdots \\ x_V \end{matrix} \right] x=⎣⎢⎢⎢⎡x1x2⋮xV⎦⎥⎥⎥⎤
h = W T x = [ w 11 w 21 ⋯ w k 1 ⋯ w V 1 w 12 w 22 ⋯ w k 2 ⋯ w V 2 ⋮ ⋮ ⋱ ⋮ ⋮ w 1 N w 2 N ⋯ w k N ⋯ w V N ] N × V [ x 1 x 2 ⋮ x k ⋮ x V ] = [ w k 1 w k 2 ⋮ w k N ] h = W^T x = \left[ \begin{matrix} w_{11} & w_{21} & \cdots & w_{k1} \cdots & w_{V1} \\ w_{12} & w_{22} & \cdots & w_{k2} \cdots & w_{V2} \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ w_{1N} & w_{2N} & \cdots & w_{kN} \cdots & w_{VN} \end{matrix} \right]_{N \times V} \left[ \begin{matrix} x_1 \\ x_2 \\ \vdots \\ x_k \\ \vdots \\ x_V \end{matrix} \right] = \left[ \begin{matrix} w_{k1} \\ w_{k2} \\ \vdots \\ w_{kN} \end{matrix} \right] \\ h=WTx=⎣⎢⎢⎢⎡w11w12⋮w1Nw21w22⋮w2N⋯⋯⋱⋯wk1⋯wk2⋯⋮wkN⋯wV1wV2⋮wVN⎦⎥⎥⎥⎤N×V⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡x1x2⋮xk⋮xV⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎡wk1wk2⋮wkN⎦⎥⎥⎥⎤
W W W的第 i i i行用 v w v_w vw表示,相当于是 w w w的词向量,是 1 × N 1 \times N 1×N的。
W T x W^T x WTx得到 N × 1 N \times 1 N×1的列向量,相当于是 W W W中 x k = 1 x_k=1 xk=1对应的那一行。
基本上就是拷贝了 W W W的第 k k k行到 h h h去了。
输入单词 w I w_I wI的向量表示是 v w I v_{w_I} vwI,维度是 N × 1 N \times 1 N×1。
从隐藏层到输出层,有一个不同的权重矩阵 W ′ W^′ W′,它是 N × V N \times V N×V的。使用这个权重矩阵,可以计算第 j j j个单词的得分 u j u_j uj:
u j = v w j ′ T ⋅ h (2) u_j = {v^{\prime} _{w_j}}^T \cdot h \tag{2} uj=vwj′T⋅h(2)
v w j ′ v^′_{w_j} vwj′是矩阵 W ′ W^′ W′的第 j j j列,维度是 N × 1 N \times 1 N×1的, v w j ′ T {v^′_{w_j}}^T vwj′T维度就是 1 × N 1 \times N 1×N。因此 u j u_j uj是这两个向量的内积,结果是一个标量,代表某个单词的分数。
这个得分可以理解为衡量中心词与输出词的相似度, h h h其实就是输入词的向量 v w I v_{w_I} vwI。
我们可以一次性求出所有单词的得分: u = W ′ T ⋅ h u = {W^′}^T \cdot h u=W′T⋅h,得到的是 V × 1 V \times 1 V×1的向量, V V V是词典大小。
接着对 u u u进行softmax就可以得到每个单词得分的概率分布:
p ( w j ∣ w I ) = y j = e x p ( u j ) ∑ j ′ = 1 V e x p ( u j ′ ) (3) p(w_j|w_I) = y_j = \frac{exp(u_j)}{\sum_{j^{\prime} = 1}^V exp(u_{j^{\prime}})} \tag{3} p(wj∣wI)=yj=∑j′=1Vexp(uj′)exp(uj)(3)
y j y_j yj是输出层第 j j j个单元的输出。把 ( 1 ) (1) (1), ( 2 ) (2) (2)代入到 ( 3 ) (3) (3)得:
p ( w j ∣ w I ) = e x p ( v w j ′ T ⋅ v w I ) ∑ j ′ = 1 V e x p ( v w j ′ ′ T v w I ) (4) p(w_j|w_I) = \frac{ exp ({v^{\prime} _{w_j}}^T \cdot v_{w_I} )}{ \sum^V_{j^′=1} exp({v^{\prime} _{w_{j^′}}}^T v_{w_I} ) } \tag{4} p(wj∣wI)=∑j′=1Vexp(vwj′′TvwI)exp(vwj′T⋅vwI)(4)
这里要注意的是:
- 输入单词 x x x和输出单词 y y y都是one-hot向量
- v w v_w vw和 v w ′ v^′_w vw′是输入单词 w w w的两种表示,分别称为输入向量和输出向量
- v w v_w vw来自 W W W的行
- v w ′ v^′_w vw′来自 W ′ W^′ W′的列
更新权重:隐藏层到输出层
下面我们就可以根据上面的式子来求梯度了。
训练目标是最大化公式 ( 4 ) (4) (4),即给定输入单词 w I w_I wI,最大化观察到输出单词 w O w_O wO的条件概率(用 j ∗ j^* j∗表示它输出层的索引)。
max p ( w O ∣ w I ) = max y j ∗ = max log y j ∗ = max log exp ( u j ∗ ) − log ∑ j ′ = 1 V e x p ( u j ′ ) = u j ∗ − l o g ∑ j ′ = 1 V e x p ( u j ′ ) : = − E \begin{aligned} \max p(w_O|w_I) &= \max \, y_{j^*} \\ &= \max \, \log \, y_{j^*} \\ &= \max \, \log \exp (u_{j^*}) - \log \sum_{j^{\prime} = 1}^V exp(u_{j^{\prime}}) \\ &= u_j^* - log \sum_{j^{\prime} = 1}^V exp(u_{j^{\prime}}) := -E \end{aligned} maxp(wO∣wI)=maxyj∗=maxlogyj∗=maxlogexp(uj∗)−logj′=1∑Vexp(uj′)=uj∗−logj′=1∑Vexp(uj′):=−E
: = := :=是记作的意思,即整个式子记作 − E -E −E,也就是 E = − log p ( w O ∣ w I ) E = -\log \, p(w_O|w_I) E=−logp(wO∣wI),因为我们习惯最小化损失函数。
现在我们更新隐藏层和输出层之间的权重。
下面求 E E E对 u j u_j uj的偏导,得到了
∂ E ∂ u j = y j − t j : = e j (5) \frac{\partial E}{\partial u_j} = y_j - t_j := e_j \tag{5} ∂uj∂E=yj−tj:=ej(5)
当 j = j ∗ j=j^* j=j∗时, t j = 1 t_j=1 tj=1,否则 t j = 0 t_j=0 tj=0。
下面给出公式推导:
∂ E ∂ u j = − ∂ ( u j ∗ − l o g ∑ j ′ = 1 V e x p ( u j ′ ) ) ∂ u j = − ∂ u j ∗ ∂ u j + ∂ ( log ∑ j ′ = 1 V exp ( u j ′ ) ) ∂ u j = − t j + e x p ( u j ) ∑ j ′ = 1 V e x p ( u j ) = y j − t j \begin{aligned} \frac{\partial E}{\partial u_j} &=- \frac{ \partial \left( u_j^* - log \sum_{j^{\prime} = 1}^V exp(u_{j^{\prime}}) \right) }{\partial u_j} \\ &= -\frac{\partial u_{j^*}}{\partial u_j} + \frac{\partial \left(\log \sum_{j^{\prime} = 1}^V \exp (u_{j^{\prime}}) \right)}{\partial u_j} \\ &= - t_j + \frac{exp(u_j)}{\sum_{j^{\prime} = 1}^V exp(u_j)} \\ &= y_j - t_j \end{aligned} ∂uj∂E=−∂uj∂(uj∗−log∑j′=1Vexp(uj′))=−∂uj∂uj∗+∂uj∂(log∑j′=1Vexp(uj′))=−tj+∑j′=1Vexp(uj)exp(uj)=yj−tj
其中
∂ ( log ∑ j ′ = 1 V exp ( u j ′ ) ) ∂ u j \frac{\partial \left(\log \sum_{j^{\prime} = 1}^V \exp (u_{j^{\prime}}) \right)}{\partial u_j} ∂uj∂(log∑j′=1Vexp(uj′))
是通过复合函数的求导法则来求的, ∂ log f ( x ) ∂ x = f ( x ) ′ f ( x ) \frac{\partial \log f(x)}{\partial x} = \frac{f(x)^{\prime}}{f(x)} ∂x∂logf(x)=f(x)f(x)′ ,这里把 f ( x ) = ∑ j ′ = 1 V exp ( u j ′ ) f(x)=\sum_{j^{\prime} = 1}^V \exp (u_{j^{\prime}}) f(x)=∑j′=1Vexp(uj′)
要求 ∑ j ′ = 1 V exp ( u j ′ ) \sum_{j^{\prime} = 1}^V \exp (u_{j^{\prime}}) ∑j′=1Vexp(uj′)对 u j u_j uj的偏导,其实很简单,把求和符号展开即可。
∂ ( e x p ( u 1 ) + e x p ( u 2 ) + ⋯ + e x p ( u j ) + ⋯ + e x p ( u V ) ) ∂ u j = e x p ( u j ) \frac{ \partial \left(exp(u_1) + exp(u_2) + \cdots + exp(u_j) + \cdots +exp(u_V) \right)}{\partial u_j} = exp(u_j) ∂uj∂(exp(u1)+exp(u2)+⋯+exp(uj)+⋯+exp(uV))=exp(uj)
把 u j u_j uj看成一个变量,其他 u 1 , u 2 , ⋯ u_1,u_2, \cdots u1,u2,⋯都是与 u j u_j uj无关的,因此求导结果为0。
根据公式 ( 3 ) (3) (3)就可以化简为 y j − t j y_j - t_j yj−tj。
结果简单地就是预测值与真实值之差。
下一步就是对 w i j ′ w^′_{ij} wij′求导来获取它的梯度。
来看下 ∂ u j ∂ w i j ′ \frac{\partial u_j}{\partial w^′_{ij}} ∂wij′∂uj
由公式 ( 2 ) (2) (2)知道 u j u_j uj与 w i j ′ w^′_{ij} wij′的关系。 h = v w I = [ h 1 , h 2 , ⋯ , h N ] h=v_{w_I}=[h_1,h_2,\cdots,h_N] h=vwI=[h1,h2,⋯,hN]
v w j ′ T = [ w 1 j ′ , w 2 j ′ , ⋯ , w 1 N ′ ] {v^′_{w_j}}^T = [w^′_{1j},w^′_{2j},\cdots,w^′_{1N}] vwj′T=[w1j′,w2j′,⋯,w1N′]
u j = h 1 ⋅ w 1 j ′ + h 2 ⋅ w 2 j ′ + ⋯ + h i ⋅ w i j ′ + ⋯ + h N ⋅ w N j ′ u_j = h_1 \cdot w^′_{1j} + h_2 \cdot w^′_{2j} + \cdots + h_i \cdot w^′_{ij} + \cdots + h_N \cdot w^′_{Nj} uj=h1⋅w1j′+h2⋅w2j′+⋯+hi⋅wij′+⋯+hN⋅wNj′
所以
∂ u j ∂ w i j ′ = h i \frac{\partial u_j}{\partial w^′_{ij}} = h_i ∂wij′∂uj=hi
∂ E ∂ w i j ′ = ∂ E ∂ u j ⋅ ∂ u j ∂ w i j ′ = e j ⋅ h i (6) \frac{\partial E}{\partial w^′_{ij}} = \frac{\partial E}{\partial u_j} \cdot \frac{\partial u_j}{\partial w^′_{ij}} = e_j \cdot h_i \tag{6} ∂wij′∂E=∂uj∂E⋅∂wij′∂uj=ej⋅hi(6)
现在就可以使用梯度下降来更新隐藏层到输出层的权重:
w i j ′ = w i j ′ − η ⋅ e j ⋅ h i w^′_{ij} = w^′_{ij} - \eta \cdot e_j \cdot h_i wij′=wij′−η⋅ej⋅hi
或者向量的形式为:
v w j ′ = v w j ′ − η ⋅ e j ⋅ h v^′_{w_j} = v^′_{w_j} - \eta \cdot e_j \cdot h vwj′=vwj′−η⋅ej⋅h
h i h_i hi是隐藏层的第 i i i个单元, v ′ w j v′_{w_j} v′wj是单词 w j w_j wj的输出向量。对每个训练样本都需要做一次复杂度为 V V V的操作去更新 W ′ W^′ W′。
更新权重:输入层到隐藏层
接着我们关注输入层到隐藏层的权重。首先求 ∂ E ∂ h i \frac{\partial E}{\partial h_i} ∂hi∂E
∂ E ∂ h i = ∑ j = 1 V ∂ E ∂ u j ⋅ ∂ u j ∂ h i = ∑ j = 1 V e j ⋅ w i j ′ : = E H i \frac{\partial E}{\partial h_i} = \sum_{j=1}^V \frac{\partial E}{\partial u_j} \cdot \frac{\partial u_j}{\partial h_i} \\ = \sum_{j=1}^V e_j \cdot w^′_{ij}\\ := EH_i ∂hi∂E=j=1∑V∂uj∂E⋅∂hi∂uj=j=1∑Vej⋅wij′:=EHi
E H EH EH是一个 N N N维的向量( N × 1 N \times 1 N×1),就是所有输出单词的权重之和,权重是它们的预测错误。
下一步就是要求 E E E对 W W W的导数,首先回顾下隐藏层就是输入层的线性变换:
h i = ∑ k = 1 V x k ⋅ w k i h_i = \sum_{k=1}^V x_k \cdot w_{ki} hi=k=1∑Vxk⋅wki
然后我们用链式法则来求 E E E对 W W W的导数:
∂ E ∂ w k i = ∂ E ∂ h i ⋅ ∂ h i ∂ w k i = E H i ⋅ x k \frac{\partial E}{\partial w_{ki}} = \frac{\partial E}{\partial h_i} \cdot \frac{\partial h_i}{\partial w_{ki}} \\ = EH_i \cdot x_k ∂wki∂E=∂hi∂E⋅∂wki∂hi=EHi⋅xk
向量化形式等价于 x x x和 E H EH EH的张量积:
∂ E ∂ W = x ⊗ E H = x ⋅ E H T \frac{\partial E}{\partial W} = x \otimes EH = x \cdot EH^T ∂W∂E=x⊗EH=x⋅EHT
这样就得到了一个 V × N V \times N V×N的矩阵,因为 x x x向量中只有一个元素为 1 1 1,其他都为 0 0 0,所以在 ∂ E ∂ W \frac{\partial E}{\partial W} ∂W∂E的矩阵中,只有一行是非零的。并且这一行的值是 E H T EH^T EHT。
现在我们就可以写出 W W W的更新式子了:
v w I = v w I − η ⋅ E H T v_{w_I} = v_{w_I} - \eta \cdot EH^T vwI=vwI−η⋅EHT
因为只有一行是非零的,所以一次也只会更新一行。
CBOW模型
CBOW模型的图示如下:
CBOW模型由多个单词作为输入,每个输入都是one-hot模型,同样输出一个单词。由多个上下文单词来预测中心词。计算隐藏层的时候,取输入单词的平均向量,然后乘以权重 W W W作为输出:
h = 1 C ( x 1 T + x 2 T + ⋯ + x C T ) W = 1 C ( v w 1 + v w 2 + ⋯ + v w C ) h = \frac{1}{C} (x_1^T + x_2^T + \cdots + x_C^T) W \\ = \frac{1}{C}(v_{w_1} + v_{w_2} + \dots + v_{w_C}) h=C1(x1T+x2T+⋯+xCT)W=C1(vw1+vw2+⋯+vwC)
C C C是上下文单词数量,因为是把 C C C个输入单词的平均向量作为输入向量,损失函数的定义和上面一个单词的模型一样。
更新隐藏层到输出层的式子也是一样的:
v w j ′ = v w j ′ − η ⋅ e j ⋅ h f o r j = 1 , 2 , ⋯ , V v^′_{w_j} = v^′_{w_j} - \eta \cdot e_j \cdot h \,\,\,\, for\, j = 1,2, \cdots,V vwj′=vwj′−η⋅ej⋅hforj=1,2,⋯,V
更新输入层到隐藏层的权重和之前一样,除了我们需要将梯度均摊到每个输入单词上:
v w I , c = v w I , c − 1 C ⋅ η ⋅ E H T f o r c = 1 , 2 , ⋯ , C v_{w_{I,c}} = v_{w_{I,c}} - \frac{1}{C} \cdot \eta \cdot EH^T \,\,\,\, for\, c = 1,2,\cdots,C vwI,c=vwI,c−C1⋅η⋅EHTforc=1,2,⋯,C
这里每次会更新 W W W中的 C C C行。
Skipgram模型
Skip-Gram模型和CBOW模型相反,把中心词放到输入层中,输出层输出的是上下文词。即用中心词来预测上下文词。
我们仍然使用 v w I v_{w_I} vwI来表示Skip-gram模型的唯一输入向量。然后隐藏层输出 h h h的定义也和 ( 1 ) (1) (1)一样。
h = W T x = W ( k , ⋅ ) T : = v w I T h = W^T x = W_{(k,\cdot)}^T := v_{w_I}^T h=WTx=W(k,⋅)T:=vwIT
在输出层,不是输出一个多项式分布,而是输出 C C C个多项式分布。但每个分布使用同样的权重矩阵来计算:
p ( w c , j ∣ w I ) = y c , j = e x p ( u c , j ) ∑ j ′ = 1 V e x p ( u j ′ ) p(w_{c,j}|w_I) = y_{c,j} = \frac{exp(u_{c,j})}{\sum_{j^′=1}^V exp(u_{j^′})} p(wc,j∣wI)=yc,j=∑j′=1Vexp(uj′)exp(uc,j)
需要注意的是,这 C C C个输出是相互独立的。 w c , j w_{c,j} wc,j是第 c c c个panel(输出)中的第 j j j个单词。 w I w_I wI是输入单词。 y c , j y_{c,j} yc,j是第 c c c个输出层中的第 j j j个单元。
u c , j u_{c,j} uc,j是第 c c c个输出的第 j j j个单元的得分。因为这些输出都共享同样的权重,因此
u c , j = u j = v w j ′ T ⋅ h f o r c = 1 , 2 , ⋯ , C u_{c,j} = u_j = {v^′_{w_j}}^T \cdot h \,\, \, for \, c = 1,2,\cdots,C uc,j=uj=vwj′T⋅hforc=1,2,⋯,C
v w j ′ v^′_{w_j} vwj′是词典中第 j j j个单词的输出向量,它是矩阵 W ′ W^′ W′中的第 j j j列。
参数更新的式子和简单CBOW模型有点不同,
E = − log p ( w O , 1 , w O , 2 , ⋯ , w O , C ∣ w I ) = − log ∏ c = 1 C P ( w O , c ∣ w i ) = − log ∏ c = 1 C e x p ( u c , j c ∗ ) ∑ j ′ = 1 V e x p ( u j ′ ) = − log ∏ c = 1 C e x p ( u c , j c ∗ ) + log ∏ c = 1 C ∑ j ′ = 1 V e x p ( u j ′ ) = − ∑ c = 1 C u j c ∗ + log ( ∑ j ′ = 1 V e x p ( u j ′ ) ) C = − ∑ c = 1 C u j c ∗ + C ⋅ log ∑ j ′ = 1 V e x p ( u j ′ ) \begin{aligned} E &= -\log p(w_{O,1},w_{O,2},\cdots,w_{O,C}|w_I) \\ &= - \log \prod_{c=1}^C P(w_{O,c}|w_i) \\ &= - \log \prod_{c=1}^C \frac{exp(u_{c,j^*_c})}{\sum_{j^′=1}^V exp(u_{j^′})} \\ &= - \log \prod_{c=1}^C exp(u_{c,j^*_c}) + \log \prod_{c=1}^C \sum_{j^′=1}^V exp(u_{j^′})\\ &= - \sum_{c=1}^C u_{j^*_c} + \log (\sum_{j^′=1}^V exp(u_{j^′}))^C\\ &= - \sum_{c=1} ^ C u_{j^*_c} + C \cdot \log \sum_{j^′=1}^V exp(u_{j^′}) \end{aligned} E=−logp(wO,1,wO,2,⋯,wO,C∣wI)=−logc=1∏CP(wO,c∣wi)=−logc=1∏C∑j′=1Vexp(uj′)exp(uc,jc∗)=−logc=1∏Cexp(uc,jc∗)+logc=1∏Cj′=1∑Vexp(uj′)=−c=1∑Cujc∗+log(j′=1∑Vexp(uj′))C=−c=1∑Cujc∗+C⋅logj′=1∑Vexp(uj′)
w O , c w_{O,c} wO,c代表第 c c c个输出单词, j c ∗ j^*_c jc∗表示第 c c c个输出单词的索引。
因为这 C C C个输出是相互独立的,因此 p ( w O , 1 , w O , 2 , ⋯ , w O , C ∣ w I ) = ∏ P ( w O , c ∣ w I ) p(w_{O,1},w_{O,2},\cdots,w_{O,C}|w_I) = \prod P(w_{O,c}|w_I) p(wO,1,wO,2,⋯,wO,C∣wI)=∏P(wO,c∣wI)
下面我们求梯度,对第 c c c个多项分布的第 j j j项的梯度为:
∂ E ∂ u c , j = y c , j − t c , j : = e c , j \frac{\partial E}{\partial u_{c,j}} = y_{c,j} - t_{c,j} := e_{c,j} ∂uc,j∂E=yc,j−tc,j:=ec,j
就是某个输出的预测错误,考虑到 C C C个多项分布产生的影响,所以需要求和。
为了简化,我们定义一个 V V V维的向量 E I = E I 1 , ⋯ , E I V EI = {EI_1,\cdots,EI_V} EI=EI1,⋯,EIV作为所有上下文单词的预测错误之和。
对第 j j j个单词的预测错误之和为:
E I j = ∑ c = 1 C e c , j EI_j = \sum_{c=1}^C e_{c,j} EIj=c=1∑Cec,j
接下来,对隐藏层到输出层矩阵 W ′ W^\prime W′求导:
∂ E ∂ w i j ′ = ∑ c = 1 C ∂ E ∂ u c , j ⋅ ∂ u c , j ∂ w i j ′ = E I j ⋅ h i \frac{\partial E}{\partial w^\prime_{ij}} = \sum_{c=1}^C \frac{\partial E}{\partial u_{c,j}} \cdot \frac{\partial u_{c,j}}{\partial w^\prime_{ij}} = EI_j \cdot h_i ∂wij′∂E=c=1∑C∂uc,j∂E⋅∂wij′∂uc,j=EIj⋅hi
所以更新隐藏层到输出层权重的式子为:
w i j ′ = w i j ′ − η ⋅ E I j ⋅ h i w^\prime_{ij} = w^\prime_{ij} -\eta \cdot EI_j \cdot h_i wij′=wij′−η⋅EIj⋅hi
或者
v w j ′ = v w j ′ − η ⋅ E I j ⋅ h f o r j = 1 , 2 , ⋯ , V v^\prime_{w_j} = v^\prime_{w_j} - \eta \cdot EI_j \cdot h \,\,\, for\, j=1,2,\cdots,V vwj′=vwj′−η⋅EIj⋅hforj=1,2,⋯,V
下面考虑对隐藏层的梯度:
∂ E ∂ h i = ∑ c = 1 C ∑ j = 1 V ∂ E ∂ u c , j ∂ u c , j ∂ h i = ∑ c = 1 C ∑ j = 1 V e c , j ⋅ w i j ′ = ∑ j = 1 V E I j ⋅ w i j ′ : = E H i \begin{aligned} \frac{\partial E}{\partial h_i} &= \sum_{c=1}^C \sum_{j=1}^V \frac{\partial E}{\partial u_{c,j}} \frac{\partial u_{c,j}}{\partial h_i } \\ &= \sum_{c=1}^C \sum_{j=1}^V e_{c,j} \cdot w^\prime_{ij} \\ &= \sum_{j=1}^V EI_j \cdot w^\prime_{ij} := EH_i \end{aligned} ∂hi∂E=c=1∑Cj=1∑V∂uc,j∂E∂hi∂uc,j=c=1∑Cj=1∑Vec,j⋅wij′=j=1∑VEIj⋅wij′:=EHi
和简单CBOW模型一样,整成向量化的形式为:
∂ E ∂ h = E H T \frac{\partial E}{\partial h} = EH^T ∂h∂E=EHT
由于输入只有一个词, h = v w I T h=v_{w_I}^T h=vwIT,每次也是更新 W W W的一行:
v w I = v w I − η ⋅ E H T v_{w_I} = v_{w_I} - \eta \cdot EH^T vwI=vwI−η⋅EHT
简单代码实现
# -*- coding: utf-8 -*-
# @Author : Jue
from collections import defaultdict
import numpy as np
class word2vec:
def __init__(self, settings):
self.n = settings['n']
self.eta = settings['learning_rate']
self.epochs = settings['epochs']
self.window = settings['window_size']
# true:cbow ; false:skipgram
self.cbow = settings['model'] == 'cbow'
def generate_training_data(self, corpus):
# 单词计数
word_counts = defaultdict(int)
for row in corpus:
for word in row:
word_counts[word] += 1
# 词典大小V
self.v_count = len(word_counts.keys())
# 生成LOOKUP 词典
self.words_list = sorted(list(word_counts.keys()), reverse=False)
# 单词对应的索引
self.word_index = dict((word, i) for i, word in enumerate(self.words_list))
# 索引对应的单词
self.index_word = dict((i, word) for word, i in self.word_index.items())
training_data = []
for sentence in corpus:
sent_len = len(sentence)
for i, word in enumerate(sentence):
# 目标词
w_target = self.word2onehot(sentence[i])
# 上下文词
w_context = []
for j in range(i - self.window, i + self.window + 1):
if j != i and sent_len - 1 >= j >= 0:
w_context.append(self.word2onehot(sentence[j]))
training_data.append([w_target, w_context]) # 中心词,上下文词
return np.array(training_data, dtype=object)
def train(self, training_data, debug=False):
# 初始化权重矩阵
self.w1 = np.random.uniform(-0.8, 0.8, (self.v_count, self.n)) # 目标词矩阵 W v x n
self.w2 = np.random.uniform(-0.8, 0.8, (self.n, self.v_count)) # 上下文词矩阵 W′ n x v
# 迭代epochs次
for i in range(self.epochs):
self.loss = 0
# 中心词,上下文词
for w_t, w_c in training_data:
if self.cbow:
x = np.mean(w_c, axis=0)
else:
x = w_t
# 前向传播
y_pred, h, u = self.forward_pass(x)
# 计算损失 e_j
if self.cbow:
e = y_pred - w_t # dE/du
else:
e = np.sum([np.subtract(y_pred, word) for word in w_c], axis=0)
# 反向传播
self.backprop(e, h, x)
if self.cbow:
self.loss += -float(u[w_t == 1]) + np.log(np.sum(np.exp(u)))
else:
self.loss += -np.sum([u[word == 1] for word in w_c]) + len(w_c) * np.log(np.sum(np.exp(u)))
if i % 100 == 0 and debug:
print('EPOCH:', i, 'LOSS:', self.loss)
def forward_pass(self, x):
''' :param x: vx1 one-hot向量 :return: '''
h = np.dot(self.w1.T, x) # (nxv) (vx1) -> nx1
u = np.dot(self.w2.T, h) # (v x n) (n x 1) -> vx1 计算每个单词的得分
y_c = self.softmax(u) # 通过softmax进行归一化,得到每个单词对应的概率
return y_c, h, u
def backprop(self, e, h, x):
''' :param e: v x 1 :param h: n x 1 :param x: v x 1 :return: '''
dw2 = np.outer(h, e) # n x v W′的梯度
dw1 = np.outer(x, np.dot(self.w2, e)) # (vx1) (nxv vx1)->nx1
self.w1 -= self.eta * dw1
self.w2 -= self.eta * dw2
def word2onehot(self, word):
word_vec = np.zeros((self.v_count, 1))
word_vec[self.word_index[word]] = 1
return word_vec
def softmax(self, x):
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum(axis=0)
def word_2_vec(self, word):
w_index = self.word_index[word]
return self.w1[w_index]
def cos_similarity(v1, v2):
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
if __name__ == '__main__':
settings = {}
settings['n'] = 2 # dimension of word embeddings
settings['window_size'] = 2 # context window +/- center word
settings['min_count'] = 0 # minimum word count
settings['epochs'] = 5000 # number of training epochs
settings['neg_samp'] = 5 # number of negative words to use during training
settings['learning_rate'] = 0.1 # learning rate
settings['model'] = 'skipgram' # cbow or skipgram
np.random.seed(0) # set the seed for reproducibility
corpus = [['A', 'dog', 'is', 'running', 'in', 'the', 'room'],
['A', 'cat', 'is', 'running', 'in', 'the', 'room']]
# corpus = []
# corpus = [['natural', 'language', 'processing', 'and', 'machine', 'learning', 'is', 'fun', 'and', 'exciting']]
# I like playing football with my friends
w2v = word2vec(settings)
# 生成训练数据
training_data = w2v.generate_training_data(corpus)
# print(training_data)
# 训练
w2v.train(training_data, debug=True)
for w1 in w2v.word_index.keys():
for w2 in w2v.word_index.keys():
print("%s & %s similarity is %s" % (w1, w2, cos_similarity(w2v.word_2_vec(w1), w2v.word_2_vec(w2))))
vecs = np.array([w2v.word_2_vec(vec) for vec in w2v.word_index.keys()])
import matplotlib.pyplot as plt
plt.scatter(vecs[:, 0], vecs[:, 1])
words = list(w2v.word_index.keys())
for i, word in enumerate(words):
plt.annotate(word, xy=(vecs[i, 0], vecs[i, 1]))
plt.show()
至此我们知道了word2vec的原理和代码实现,但训练效率低是它的一个缺点,在下篇文章将会介绍两种优化的方法。
参考
- Word2vec from Scratch with Python and NumPy
- word2vec Parameter Learning Explained