word2vec背后的数学原理+从零开始纯Python实现(上)

引言

本文是对近日学习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。

word2vec背后的数学原理+从零开始纯Python实现(上)_第1张图片

CBOW模型中,通过一个上下文(比如说一个句子)来预测目标词;而Skip-Gram模型则相反,根据给定的输入词来预测上下文。

Skip-Gram:能够很好地处理少量的训练数据,而且能够很好地表示不常见的单词或短语
CBOW:比skip-gram训练快几倍,对出现频率高的单词的准确度稍微更好一些

Simple CBOW模型

要想理解CBOW和SkipGram模型,我们先从最简单版本的CBOW模型开始介绍,又被称为One Word模型,上下文只有一个单词,目标词也是一个单词。
意味着给定一个上下文词来预测一个目标词。有点类似bigram模型。

word2vec背后的数学原理+从零开始纯Python实现(上)_第2张图片

在上图中 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=w11w21wV1w12w22wV2w1Nw2NwVN

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=x1x2xV

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=w11w12w1Nw21w22w2Nwk1wk2wkNwV1wV2wVNN×Vx1x2xkxV=wk1wk2wkN

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=vwjTh(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 vwjT维度就是 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=WTh,得到的是 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(wjwI)=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(wjwI)=j=1Vexp(vwjTvwI)exp(vwjTvwI)(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(wOwI)=maxyj=maxlogyj=maxlogexp(uj)logj=1Vexp(uj)=ujlogj=1Vexp(uj):=E

: = := :=是记作的意思,即整个式子记作 − E -E E,也就是 E = − log ⁡   p ( w O ∣ w I ) E = -\log \, p(w_O|w_I) E=logp(wOwI),因为我们习惯最小化损失函数。

现在我们更新隐藏层和输出层之间的权重。

下面求 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} ujE=yjtj:=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} ujE=uj(ujlogj=1Vexp(uj))=ujuj+uj(logj=1Vexp(uj))=tj+j=1Vexp(uj)exp(uj)=yjtj

其中

∂ ( 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(logj=1Vexp(uj))
是通过复合函数的求导法则来求的, ∂ log ⁡ f ( x ) ∂ x = f ( x ) ′ f ( x ) \frac{\partial \log f(x)}{\partial x} = \frac{f(x)^{\prime}}{f(x)} xlogf(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 yjtj

结果简单地就是预测值与真实值之差。

下一步就是对 w i j ′ w^′_{ij} wij求导来获取它的梯度。

来看下 ∂ u j ∂ w i j ′ \frac{\partial u_j}{\partial w^′_{ij}} wijuj

由公式 ( 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}] vwjT=[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=h1w1j+h2w2j++hiwij++hNwNj

所以
∂ u j ∂ w i j ′ = h i \frac{\partial u_j}{\partial w^′_{ij}} = h_i wijuj=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} wijE=ujEwijuj=ejhi(6)

现在就可以使用梯度下降来更新隐藏层到输出层的权重:
w i j ′ = w i j ′ − η ⋅ e j ⋅ h i w^′_{ij} = w^′_{ij} - \eta \cdot e_j \cdot h_i wij=wijηejhi
或者向量的形式为:
v w j ′ = v w j ′ − η ⋅ e j ⋅ h v^′_{w_j} = v^′_{w_j} - \eta \cdot e_j \cdot h vwj=vwjηejh

h i h_i hi是隐藏层的第 i i i个单元, v ′ w j v′_{w_j} vwj是单词 w j w_j wj的输出向量。对每个训练样本都需要做一次复杂度为 V V V的操作去更新 W ′ W^′ W

更新权重:输入层到隐藏层

接着我们关注输入层到隐藏层的权重。首先求 ∂ E ∂ h i \frac{\partial E}{\partial h_i} hiE

∂ 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 hiE=j=1VujEhiuj=j=1Vejwij:=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=1Vxkwki

然后我们用链式法则来求 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 wkiE=hiEwkihi=EHixk

向量化形式等价于 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 WE=xEH=xEHT

这样就得到了一个 V × N V \times N V×N的矩阵,因为 x x x向量中只有一个元素为 1 1 1,其他都为 0 0 0,所以在 ∂ E ∂ W \frac{\partial E}{\partial W} WE的矩阵中,只有一行是非零的。并且这一行的值是 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模型的图示如下:

word2vec背后的数学原理+从零开始纯Python实现(上)_第3张图片

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ηejhforj=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,cC1ηEHTforc=1,2,,C

这里每次会更新 W W W中的 C C C行。

Skipgram模型

word2vec背后的数学原理+从零开始纯Python实现(上)_第4张图片

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,jwI)=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=vwjThforc=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,CwI)=logc=1CP(wO,cwi)=logc=1Cj=1Vexp(uj)exp(uc,jc)=logc=1Cexp(uc,jc)+logc=1Cj=1Vexp(uj)=c=1Cujc+log(j=1Vexp(uj))C=c=1Cujc+Clogj=1Vexp(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,CwI)=P(wO,cwI)

下面我们求梯度,对第 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,jE=yc,jtc,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=1Cec,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 wijE=c=1Cuc,jEwijuc,j=EIjhi

所以更新隐藏层到输出层权重的式子为:

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ηEIjhi
或者
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ηEIjhforj=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} hiE=c=1Cj=1Vuc,jEhiuc,j=c=1Cj=1Vec,jwij=j=1VEIjwij:=EHi

和简单CBOW模型一样,整成向量化的形式为:
∂ E ∂ h = E H T \frac{\partial E}{\partial h} = EH^T hE=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背后的数学原理+从零开始纯Python实现(上)_第5张图片

至此我们知道了word2vec的原理和代码实现,但训练效率低是它的一个缺点,在下篇文章将会介绍两种优化的方法。

参考

  1. Word2vec from Scratch with Python and NumPy
  2. word2vec Parameter Learning Explained
  3. 自然语言处理与词嵌入

你可能感兴趣的:(人工智能,读书笔记,自然语言处理,深度学习,word2vec,python,word2vec,python实现词向量)