深度学习word2vec笔记之算法篇

本文转载自《深度学习word2vec笔记之算法篇》对排版和内容作了部分调整,感谢大佬分享。

PDF版本关注微信公众号:【终南樵】,回复:【word2vec基础】获取

1. 声明

  1. 该博文是Google专家以及多位博主所无私奉献的论文资料整理的。具体引用的资料请看参考文献。具体的版本声明也参考原文献
  2. 本文仅供学术交流,非商用。所以每一部分具体的参考资料并没有详细对应,更有些部分本来就是直接从其他博客复制过来的。
    如果某部分不小心侵犯了大家的利益,还望海涵,并联系老衲删除或修改,直到相关人士满意为止。
  3. 本人才疏学浅,整理总结的时候难免出错,还望各位前辈不吝指正,谢谢。
  4. 阅读本文需要机器学习、概率统计算法等等基础(如果没有也没关系了,没有就看看,当做跟同学们吹牛的本钱),基础篇 《深度学习word2vec笔记之基础篇》
  5. 此属于第一版本,若有错误,还需继续修正与增删。还望大家多多指点。请直接回帖,本人来想办法处理。
  6. word版的和pdf版的文档已上传到csdn。点击下载1
    或者点击下载2,资源分1分,评论后据说可以返还的,就有劳各位帮忙攒点分吧。如果有必要可以回复或者发邮件到邮箱:beiliude@163.com。

2. 前言

在看word2vec的资料的时候,经常会被叫去看那几篇论文,而那几篇论文也没有系统地说明word2vec的具体原理和算法。

所以老衲就斗胆整理了一个笔记,希望能帮助各位尽快理解word2vec的基本原理,避免浪费时间。
当然如果已经了解了,就随便看看得了。

3. CBOW的网络结构与使用说明

3.1 网络结构

word2vec总共有两种类型,每种类型有两个策略,总共4种。

这里先说最常用的一种。这种的网络结构如下图。
CBOW网络结构

第一层:输入层
也就是最上面的那一层可以称为输入层。输入的是若干个词的词向量(词向量的意思就是把一个词表示成一个向量的形式表达,后面会介绍)。

第二层:隐藏层
中间那个层可以成为隐藏层,是输入的若干个词向量的累加和,注意是向量的累加和,结果是一个向量。

第三层:输出层
第三层是方框里面的那个二叉树,可以称之为输出层,隐层的那个节点要跟输出层的那个二叉树的所有非叶节点链接的,线太多画不过来了。

第三层的这个二叉树是一个霍夫曼树,每个非叶节点也是一个向量,但是这个向量不代表某个词,代表某一类别的词;每个叶子节点代表一个词向量,为了简单只用一个 w w w表示,没有下标。另外要注意的是,输入的几个词向量其实跟这个霍夫曼树中的某几个叶子节点是一样的。当然输入的那几个词跟它们最终输出的到的那个词未必是同一个词,而且基本不会是同一个词,只是这几个词跟输出的那个词往往有语义上的关系。

还有要注意的是,这个霍夫曼树的所有叶子节点就代表了语料库里面的所有词,而且是每个叶子节点对应一个词,不重复。

这个网络结构的功能是为了完成一个的事情——判断一句话是否是自然语言。 怎么判断呢?使用的是概率,就是计算一下这句话的“一列词的组合”的概率的连乘(联合概率)是多少,如果比较低,那么就可以认为不是一句自然语言,如果概率高,就是一句正常的话。这个其实也是语言模型的目标。前面说的“一列词的组合”其实包括了一个词跟它的上下文的联合起来的概率,一种普通的情况就是每一个词跟它前面所有的词的组合的概率的连乘,这个后面介绍。

对于上面的那个网络结构来说,网络训练完成后,假如给定一句话 s s s,这句话由 w 1 , w 2 , w 3 ⋯   , w T w_1,w_2,w_3\cdots,w_T w1,w2,w3,wT组成,就可以利用计算这句话是自然语言的概率了,计算的公式是下面的公式。

P ( s ) = P ( w 1 , w 2 , ⋯   , w r ) = ∏ T i = 1 P ( w i ∣ c o n t e x t i )                  ( 1 ) P(s)=P(w_1,w_2,\cdots,w_r)=\prod_{T}^{i=1}P(w_i|context_i) \;\;\;\; \;\;\;\;(1) P(s)=P(w1,w2,,wr)=Ti=1P(wicontexti)(1)

其中 c o n t e x t i context_i contexti表示词汇 i i i的上下文信息,也就是这个词的前面和后面各若干个词,这个“若干”(后面简称 c c c)一般是随机的,也就是一般会从1到5之间的一个随机数,每个 P ( w i ∣ c o n t e x t i ) P({w_i}|contex{t_i}) P(wicontexti)代表的意义是前后的c个词分别是那几个的情况下,出现该词的概率。

举个例子就是:“大家 喜欢 吃 好吃 的 苹果”这句话总共6个词,假设对“吃”这个词来说 c c c随机抽到2,则“吃”这个词的 c o n t e x t context context是“大家”、“喜欢”、“好吃”和“的”,总共四个词,这四个词的顺序可以乱,这是word2vec的一个特点。计算 P ( w i ∣ c o n t e x t i ) P({w_i}|contex{t_i}) P(wicontexti)的时候都要用到上面的那个网络,具体计算的方法用例子说明,假设就是计算“吃”这个词的在“大家”、“喜欢”、“好吃”和“的”这四个词作为上下文的条件概率,又假设“吃”这个词在霍夫曼树中是的最右边那一个叶子节点,那么从根节点到到达它就有两个非叶节点,根节点对应的词向量命名为 A A A,根节点的右孩子节点对应的词向量命名为 B B B,另外再假设“大家”、“喜欢”、“好吃”和“的”这四个词的词向量的和为 C C C,则

P ( χ ∣ c o n t e x t χ ) = ( 1 − σ ( A ⋅ C ) ) ⋅ ( 1 − σ ( B ⋅ C ) )                  ( 2 ) P(\chi|context_\chi) = (1- \sigma (A\cdot C)) \cdot (1-\sigma(B\cdot C)) \;\;\;\; \;\;\;\;(2) P(χcontextχ)=(1σ(AC))(1σ(BC))(2)

其中 σ ( x ) = 1 / ( 1 + e − x ) {\rm{\sigma }}\left( {\rm{x}} \right) = 1/\left( {1 + {e^{ - x}}} \right) σ(x)=1/(1+ex),是sigmoid公式, χ \chi χ表示词汇“吃”。

为什么这么算,看完后面就明白了,这里仅仅说个流程,让后面的描述流畅起来。要注意的是,如果“吃”这个词在非叶节点B的左孩子节点(假设称为 E E E)的右边的那个叶子节点,也就是在图中右边的三个叶子的中间那个,则有

P ( χ ∣ c o n t e x t χ ) = ( 1 − σ ( A ⋅ C ) ⋅ σ ( B ⋅ C ) ⋅ ( 1 − σ ( E ⋅ C ) ) )                  ( 3 ) P(\chi|context_\chi) = (1- \sigma (A\cdot C) \cdot \sigma(B \cdot C) \cdot (1-\sigma(E\cdot C)))\;\;\;\; \;\;\;\;(3) P(χcontextχ)=(1σ(AC)σ(BC)(1σ(EC)))(3)

上面的那句话的每个词都计算 P ( w i ∣ C o n t e x t i ) P({w_i}|Contex{t_i}) P(wiContexti)后连乘起来得到联合概率,这个概率如果大于某个阈值,就认为是正常的话;否则就认为不是自然语言,要排除掉。

对于这个神经网络的描述索然无味,因为主角也不是这个概率,这个神经网络最重要的是输出层的那个霍夫曼树的叶子节点上的那些向量,那些向量被称为词向量,词向量就是另外一篇博文里面介绍的,是个好东西。怎么得到这些词向量更加是一个重要的过程,也是word2vec这整个算法最重要的
东西,后面会认真介绍。

至于霍夫曼树,其实是一个优化的解法,后面再提。

3.2 优化目标与解问题

前面已经提过语言模型的目标就是判断一句话是否是正常的,至于怎么判断则需要计算很多条件概率如 P ( w i ∣ c o n t e x t i ) P({w_i}|contex{t_i}) P(wicontexti),然后还要把这些条件概率连乘起来得到联合概率。这样就带来了问题了——怎么去计算 P ( w i ∣ c o n t e x t i ) P({w_i}|contex{t_i}) P(wicontexti),有很多办法的,后面的章节会介绍。

这里的word2vec的计算这个条件概率的方法是利用神经网络的能量函数,因为在能量模型中,能量函数的功能是把神经网络的状态转化为概率表示,这在另外一篇博文RBM里面有提到,具体要看hinton的论文来了解了。能量模型有个特别大的好处,就是能拟合所有的指数族的分布。那么,如果认为这些条件概率是符合某个指数族的分布的话,是可以用能量模型去拟合的。总之word2vec就认为 P ( w i ∣ c o n t e x t i ) P({w_i}|contex{t_i}) P(wicontexti)这个条件概率可以用能量模型来表示了。

既然是能量模型,那么就需要能量函数,word2vec定义了一个非常简单的能量函数
E ( A , C ) = − ( A ⋅ C )                  ( 4 ) E(A,C)=-(A\cdot C) \;\;\;\; \;\;\;\;(4) E(A,C)=(AC)(4)

其中
A A A:可以认为是某个词的词向量。
C C C:是这个词的上下文的词向量的和(向量的和),基本上就可以认为 C C C代表context。
中间的点号:表示两个向量的内积。
然后根据能量模型(这个模型假设了温度一直是1,所以能量函数没有分母了),就可以表示出词A的在上下文词向量 C C C下的概率来了
P ( A ∣ C ) = e − E ( A , C ) ∑ v = 1 V e − E ( w v , C )                  ( 5 ) P(A|C)=\frac{e^{-E(A,C)}}{\sum_{v=1}^V e^{-E(w_v,C)}} \;\;\;\; \;\;\;\;(5) P(AC)=v=1VeE(wv,C)eE(A,C)(5)
其中 V V V表示语料库里面的的词的个数,这个定义的意思是在上下文 C C C出现的情况下,中间这个词是 A A A的概率,为了计算这个概率,肯定得把语料库里面所有的词的能量都算一次,然后再根据词 A A A的能量,那个比值就是出现 A A A的概率。这种计算概率的方式倒是能量模型里面特有的,这个定义在论文《Hierarchical Probabilistic Neural Network Language Model》里面,这里拿来改了个形式。

这个概率其实并不好统计,为了算一个词的的概率,得算上这种上下文的情况下所有词的能量,然后还计算指数值再加和。注意那个分母,对语料库里面的每个词,分母都要算上能量函数,而且再加和,假如有 V V V个词汇,整个语料库有 W W W个词,那么一轮迭代中光计算分母就有 W ∗ V ∗ D W*V*D WVD个乘法,如果词向量维度是 D D D的话。比如,语料库有100000000个词,词汇量是10000,计算100维的词向量,一轮迭代要 1 0 14 10^{14} 1014次乘法,计算机计算能力一般是 1 0 9 10^9 109每秒,然后一轮迭代就要跑100000秒,大约27小时,一天多吧。1000轮迭代就三年了。

这时候科学家们的作用又体现了,假如把语料库的所有词分成两类,分别称为 G G G类和 H H H类,每类一半,其中词 A A A属于 G G G类,那么下面的式子就
可以成立了
P ( A ∣ C ) = P ( A ∣ G , C ) P ( G ∣ C )                  ( 6 ) P(A|C)=P(A|G,C)P(G|C) \;\;\;\; \;\;\;\;(6) P(AC)=P(AG,C)P(GC)(6)

这个式子的的含义算明确的了,词 A A A在上下文 C C C的条件下出现的概率,与后面的这个概率相等——在上下文 C C C的条件下出现了 G G G类词,同时在上下文为 C C C,并且应该出现的词是 G G G类词的条件下,词 A A A出现的概率。列出这么一个式子在论文《Hierarchical Probabilistic Neural Network Language Model》里面也有个证明的,看原始的情况
P ( Y = y ∣ X = x ) = P ( Y = y ∣ D = d ( y ) , X ) P ( D = d ( y ) ∣ X = x )                  ( 7 ) P(Y=y|X=x)=P(Y=y|D=d(y),X)P(D=d(y)|X=x) \;\;\;\; \;\;\;\;(7) P(Y=yX=x)=P(Y=yD=d(y),X)P(D=d(y)X=x)(7)
其中, d d d是一个映射函数,把 Y Y Y里面的元素映射到词的类别 D D D里面的元素。还有个证明
P ( Y ∣ X ) = ∑ i P ( Y , D = i ∣ X ) = ∑ P ( Y ∣ D = i , X ) P ( D = i ∣ X ) = P ( Y ∣ D = d ( Y ) , X ) P ( D = d ( Y ) ∣ X )                  ( 8 ) \begin{aligned} P(Y|X)&=\sum_i P(Y,D=i|X) \\ & = \sum P(Y|D=i,X)P(D=i|X) \\ &= P(Y|D=d(Y),X)P(D=d(Y)|X) \end{aligned} \;\;\;\; \;\;\;\;(8) P(YX)=iP(Y,D=iX)=P(YD=i,X)P(D=iX)=P(YD=d(Y),X)P(D=d(Y)X)(8)
式子(6)说明了一个问题,计算一个词 A A A在上下文C的情况下出现的概率,可以先对语料库中的词分成两簇,然后能节省计算。现在来展示一下怎么节省计算,假设 G G G H H H这两类的簇就用 G G G H H H表示( G G G H H H也是一个词向量,意思就是 G G G表示了其中一簇的词, H H H表示了另外一簇的词, G G G H H H只是一个代表,也不是什么簇中心的说法。其实如果情况极端点,只有两个词,看下面的式子就完全没问题了。在多个词的情况下,就可以认为词被分成了两团, G G G H H H个表示一团的词,计算概率什么的都以一整团为单位),那么式子(6)中的 p ( G ∣ C ) p(G|C) p(GC)可以用下面的式子计算
P ( G ∣ C ) = e − E ( G , C ) e − E ( G , C ) + e − E ( H , C ) = 1 1 + e − ( − ( H − G ) ⋅ C ) = 1 1 + e − E ( H − G , C )                  ( 9 ) \begin{aligned} P(G|C) &=\frac{e^{-E(G,C)}}{e^{-E(G,C)} + e^{-E(H,C)}} \\ &=\frac{1}{1+e^{-(-(H-G)\cdot C)}} \\ &=\frac{1}{1+e^{-E(H-G,C)}} \end{aligned} \;\;\;\; \;\;\;\;(9) P(GC)=eE(G,C)+eE(H,C)eE(G,C)=1+e((HG)C)1=1+eE(HG,C)1(9)

也就是说,可以不用关心这两个簇用什么表示,只要利用一个 F = H − G F=H-G F=HG的类词向量的一个向量就可以计算 P ( G ∣ C ) P(G|C) P(GC)了,所以这一步是很节省时间的。再看另外一步

P ( A ∣ G , C ) = e − E ( A , C ) ∑ W ∈ G e − E ( W , C )                  ( 10 ) \begin{aligned} P(A|G,C) = \frac{e^{-E(A,C)}}{\sum_{W\in G}e^{-E(W,C)}} \end{aligned}\;\;\;\; \;\;\;\;(10) P(AG,C)=WGeE(W,C)eE(A,C)(10)
由于在 G G G内的词数量只有 V 2 \frac{V}{2} 2V个,也就是说计算分母的时候只要计算 V 2 \frac{V}{2} 2V个词的能量就可以了。这已经省了一半的计算量了,可惜科学家们是贪得无厌的,所以还要继续省,怎么来呢?把G类词再分成两个簇 G G GG GG G H GH GH A A A G H GH GH里面,然后
P ( A ∣ G , C ) = P ( A ∣ G H , G , C ) P ( G H ∣ G , C )                  ( 11 ) \begin{aligned} P(A|G,C) = P(A|GH,G,C)P(GH|G,C) \end{aligned}\;\;\;\; \;\;\;\;(11) P(AG,C)=P(AGH,G,C)P(GHG,C)(11)
同样有
P ( G H ∣ G , C ) = 1 1 + e − E ( G G − G H , C )                  ( 12 ) \begin{aligned} P(GH|G,C) = \frac{1}{1 + e^{-E(GG-GH,C)}} \end{aligned}\;\;\;\; \;\;\;\;(12) P(GHG,C)=1+eE(GGGH,C)1(12)

P ( A ∣ G H , G , C ) = e − E ( A , C ) ∑ W ∈ G H e − E ( W , C )                  ( 13 ) \begin{aligned} P(A|GH,G,C) = \frac{e^{-E(A,C)}}{\sum_{W\in GH}e^{-E(W,C)}} \end{aligned} \;\;\;\; \;\;\;\;(13) P(AGH,G,C)=WGHeE(W,C)eE(A,C)(13)

同样可以把 G G − G H GG-GH GGGH用一个类词向量表达,这时候
P ( A ∣ C ) = P ( A ∣ G H , G , C ) P ( G H ∣ G , C ) P ( G ∣ C )                  ( 14 ) \begin{aligned} P(A|C) = P(A|GH,G,C)P(GH|G,C)P(G|C) \end{aligned} \;\;\;\; \;\;\;\;(14) P(AC)=P(AGH,G,C)P(GHG,C)P(GC)(14)
继续下去假设继续分到GHG簇的时候只剩两个词了,再分两簇为 G H G G GHGG GHGG G H G H GHGH GHGH,其中的簇 G H G G GHGG GHGG就只有一个词 A A A,那么 p ( A ∣ C ) p(A|C) p(AC)可以用下面的式子算
P ( A ∣ C ) = P ( A ∣ G H G G , G H G , G H , G , C ) P ( G H G G ∣ G H G , G H , G , C ) × P ( G H G ∣ G H , G , C ) P ( G H ∣ G , C ) P ( G ∣ C )                  ( 15 ) \begin{aligned} P(A|C) & =P(A|GHGG,GHG,GH,G,C)P(GHGG|GHG,GH,G,C)\\ &\times P(GHG|GH,G,C)P(GH|G,C)P(G|C) \end{aligned} \;\;\;\; \;\;\;\;(15) P(AC)=P(AGHGG,GHG,GH,G,C)P(GHGGGHG,GH,G,C)×P(GHGGH,G,C)P(GHG,C)P(GC)(15)
其中 p ( A ∣ G H G G , G H G , G H , G ) p(A|GHGG,GHG,GH,G) p(AGHGG,GHG,GH,G)是1,因为只有一个单词,代到公式(15)就可以得到,那么就有
P ( A ∣ C ) = P ( G H G G ∣ G H G , G H , G , C ) P ( G H G ∣ G H , G , C ) P ( G H ∣ G , C ) P ( G ∣ C )                  ( 16 ) \begin{aligned} P(A|C)=P(GHGG|GHG,GH,G,C)P(GHG|GH,G,C)P(GH|G,C)P(G|C) \end{aligned} \;\;\;\; \;\;\;\;(16) P(AC)=P(GHGGGHG,GH,G,C)P(GHGGH,G,C)P(GHG,C)P(GC)(16)
也就是
P ( A ∣ C ) = 1 1 + e − E ( G H H − G H G , C ) ⋅ 1 1 + e − E ( G G − G H , C ) ⋅ 1 1 + e − E ( H − G , C )                  ( 17 ) \begin{aligned} P(A|C)=\frac{1}{1+e^{-E(GHH-GHG,C)}}\cdot \frac{1}{1+e^{-E(GG-GH,C)}} \cdot \frac{1}{1 + e^{-E(H-G,C)}} \end{aligned} \;\;\;\; \;\;\;\;(17) P(AC)=1+eE(GHHGHG,C)11+eE(GGGH,C)11+eE(HG,C)1(17)
假设再令 F F F = G H H − G H G , F F = G G − G H , F = H − G FFF=GHH-GHG,FF=GG-GH,F=H-G FFF=GHHGHG,FF=GGGH,F=HG,那么 P ( A ∣ C ) P(A|C) P(AC)只要算这三个词与上下文C的能量数了,确实比原来的要节省很多计算的。对于上面的霍夫曼树来说假设 G G G表示向右,H表示向左,那么A就是从右边开始数的第二个叶子节点,就是图中右边的三个W的中间那个。那么 F F F F F FF FF F F F FFF FFF就是这个叶子节点路径上的三个非叶节点。但是一个词总是会一会向左,一会向右的,也就是在根节点那里,一会是 P ( G ∣ C ) P(G|C) P(GC)那么 F = H − G F=H-G F=HG,一会又是 P ( H ∣ C ) P(H|C) P(HC)
那么 F = G − H F=G-H F=GH,如果 F F F在每个节点都是唯一一个值,就可以直接用一次词向量表示这个非叶节点了。这下难不倒科学家的,令 F F F一直是等于 H − G H-G HG,那么一直有
P ( H ∣ C ) = 1 1 + e − E ( F , C )                  ( 18 ) \begin{aligned} P(H|C)= \frac{1}{1 + e^{-E(F,C)}} \end{aligned} \;\;\;\; \;\;\;\;(18) P(HC)=1+eE(F,C)1(18)
并且有
P ( G ∣ C ) = 1 − P ( H ∣ C )                  ( 19 ) \begin{aligned} P(G|C)= 1 - P(H|C) \end{aligned} \;\;\;\; \;\;\;\;(19) P(GC)=1P(HC)(19)
这样每个非叶节点就可以用唯一一个词向量表示了。看到这里,总该明白为啥 p ( A ∣ C ) p(A|C) p(AC)要这么算了吧。再换种情况,上面的概率 P ( χ ∣ c o n t e x t χ ) P(\chi|context_\chi) P(χcontextχ)这个概率的计算方法是不是也是同样的道理?总结下来, P ( w i ∣ C o n t e x t i ) P({w_i}|Contex{t_i}) P(wiContexti)可以用下面的公式计算了
P ( w ∣ c o n t e x t ) = ∏ k = 1 K P ( d k ∣ q k , C ) = ∏ k = 1 K ( ( σ ( q k ⋅ C ) ) 1 − d k ⋅ ( 1 − σ ( q k ⋅ C ) ) d k )                  ( 20 ) \begin{aligned} P(w|context)& = \prod_{k=1}^{K}P(d_k|q_k,C)\\ &=\prod_{k=1}^K((\sigma (q_k \cdot C))^{1-d_k} \cdot (1-\sigma(q_k \cdot C))^{d_k}) \end{aligned} \;\;\;\; \;\;\;\;(20) P(wcontext)=k=1KP(dkqk,C)=k=1K((σ(qkC))1dk(1σ(qkC))dk)(20)
其中 C C C表示上下文的词向量累加后的向量, q k q_k qk表示从根节点下来到叶子节点的路径上的那些非叶节点, d k d_k dk就是编码了,也可以说是分类,因为在霍夫曼树的每个非叶节点都只有两个孩子节点,那可以认为当 w i w_i wi在这个节点的左子树的叶子节点上时 d k = 0 d_k=0 dk=0,否则 d k = 1 d_k=1 dk=1。这样的话每个词都可以用一组霍夫曼编码来表示,就有了上面的那个式子中间的那个 d k d_k dk,整个 p ( w ∣ c o n t e x t ) p(w|context) p(wcontext)就可以用霍夫曼树上的若干个非叶节点和词 w w w的霍夫曼编码来计算了。看到这务必想明白,因为开始要讨论怎么训练了。

3.3 霍夫曼树

上面输出层的二叉树是霍夫曼树,其实并没有要求是霍夫曼树,随便一个不太离谱的二叉树都可以的,但是用霍夫曼树能达到最优的计算效果。

根据之前的讨论,已经知道了语料库里面每个词都要从根节点下来,一直走到叶子节点,每经过一个非叶节点,就要计算一个sigmoid函数。
随便乱分也能达到效果,但是信息熵理论给出了最优的方案——霍夫曼树。具体可以查看其它资料。

3.4 目标函数

假设语料库是有 S S S个句子组成的一个句子序列(顺序不重要),整个语料库有 V V V个词,似然函数就会构建成下面的样子
L ( θ ) = ∏ j S ( ∏ i j = 1 T j P ( w i j ∣ c o n t e x t j ) )                  ( 21 ) \begin{aligned} L(\theta) = \prod_j^S(\prod_{i_j=1}^{T_j} P(w_{i_j}|context_j)) \end{aligned} \;\;\;\; \;\;\;\;(21) L(θ)=jS(ij=1TjP(wijcontextj))(21)
其中 T j T_j Tj表示第 j j j个句子的词个数,极大似然要对整个语料库去做的。对数似然就会是下面的样子。

L ( θ ) = l o g L ( θ ) = 1 V ∑ j = 1 S ( ∑ i j = 1 T j l o g    P ( w i j ∣ c o n t e x t i j ) )                  ( 22 ) \begin{aligned} L(\theta) &= log L(\theta) \\ & = \frac{1}{V}\sum_{j=1}^S(\sum_{i_j=1}^{T_j}log\;P(w_{i_j}|context_{i_j})) \end{aligned} \;\;\;\; \;\;\;\;(22) L(θ)=logL(θ)=V1j=1S(ij=1TjlogP(wijcontextij))(22)
如果前面有个 1 V \frac{1}{V} V1,对数似然还有些人称为交叉熵,这个具体也不了解,就不介绍了;不用 1 V \frac{1}{V} V1的话,就是正常的极大似然的样子。有意向的同学
可以扩展到有文档的样子,这里就不介绍了。但是对于word2vec来说,上面的似然函数得改改,变成下面的样子。
L ( θ ) = ∏ j S ( ∏ i j = 1 T j P ( w i j ∣ c o n t e x t i j ) ) = ∏ j S ( ∏ i j = 1 T j ( ∏ k i j = 1 K i j ( ( ( σ ( q k i j ⋅ C i j ) ) 1 − d k i j ⋅ ( 1 − σ ( q k i j ⋅ C i j ) ) 1 − d k i j ) ) )                  ( 23 ) \begin{aligned} L(\theta) &= \prod_j^S (\prod_{i_j=1}^{T_j}P(w_{i_j}|context_{i_j})) \\ & = \prod_j^S(\prod_{i_j=1}^{T_j}(\prod_{k_{i_j=1}}^{K_{i_j}}(((\sigma (q_{k_{i_j}}\cdot C_{i_j}))^{1-d_{k_{i_j}}}\cdot(1 - \sigma (q_{k_{i_j}}\cdot C_{i_j}))^{1-d_{k_{i_j}}}))) \end{aligned} \;\;\;\; \;\;\;\;(23) L(θ)=jS(ij=1TjP(wijcontextij))=jS(ij=1Tj(kij=1Kij(((σ(qkijCij))1dkij(1σ(qkijCij))1dkij)))(23)
其中的 C i j C_{i_j} Cij表示上下文相加的那个词向量。对数似然就是下面的
L ( θ ) = l o g L ( θ ) = ∑ j = 1 S ( ∑ i j = 1 T j ( ∑ k i j = 1 K i j l o g ( ( σ ( q k i j ⋅ C i j ) ) 1 − d k i j ⋅ ( 1 − σ ( q k i j ⋅ C i j ) ) 1 − d k i j ) ) ) = ∑ j = 1 S ( ∑ i j = 1 T j ( ∑ k i j K i j [ ( 1 − d k i j ) l o g σ ( q k i j ⋅ C i j ) + d k i j l o g ( 1 − σ ( q k i j ) ) ] ) )                  ( 24 ) \begin{aligned} L(\theta) &=log L(\theta) \\ &=\sum_{j=1}^S (\sum_{i_{j=1}}^{T_j}(\sum_{k_{i_j}=1}^{K_{i_j}}log((\sigma (q_{k_{i_j}}\cdot C_{i_j}))^{1-d_{k_{i_j}}}\cdot(1 - \sigma (q_{k_{i_j}}\cdot C_{i_j}))^{1-d_{k_{i_j}}})))\\ & = \sum_{j=1}^S(\sum_{i_j=1}^{T_j}(\sum_{k_{i_j}}^{K_{i_j}}[(1-d_{k_{i_j}})log\sigma(q_{k_{i_j}}\cdot C_{i_j})+d_{k_{i_j}} log(1-\sigma(q_{k_{i_j}}))])) \end{aligned} \;\;\;\; \;\;\;\;(24) L(θ)=logL(θ)=j=1S(ij=1Tj(kij=1Kijlog((σ(qkijCij))1dkij(1σ(qkijCij))1dkij)))=j=1S(ij=1Tj(kijKij[(1dkij)logσ(qkijCij)+dkijlog(1σ(qkij))]))(24)
这里就不要 1 V \frac{1}{V} V1了。这个看起来应该比较熟悉了,很像二分类的概率输出的逻辑回归——logistic regression模型。没错了,word2vec就是这么考虑的,把在霍夫曼树
向左的情况,也就是 d k = 0 d_k=0 dk=0的情况认为是正类,向右就认为是负类(这里的正负类只表示两种类别之一)。这样每当出现了一个上下文 C C C和一个词在左子树的情况,就认为得
到了一个正类样本,否则就是一个负类样本,每个样本的属于正类的概率都可以用上面的参数算出来,就是 σ ( q k i j ⋅ C i j ) \sigma (q_{k_{i_j}}\cdot C_{i_j}) σ(qkijCij)
如果是向右的话,就用 σ ( q k i j ⋅ C i j ) ) 1 − d k i j \sigma (q_{k_{i_j}}\cdot C_{i_j}))^{1-d_{k_{i_j}}} σ(qkijCij))1dkij计算其概率。注意每个词可以产生多个样本,因为从霍夫曼树的根节点开始,
每个叶子节点都产生一个样本,这个样本的label(也就是属于正类或者负类标志)可以用霍夫曼编码来产生,前面说过了,向左的霍夫曼编码 d k = 0 d_k=0 dk=0,所以很自然地可以用 1 − d k 1-d_k 1dk
表示每个样本label。在这里,霍夫曼编码也变成了一个重要的东西了。这样就好多了,问题到这也该清楚了,上面那个 L ( θ ) L(\theta) L(θ)就是对数似然,然后负对数似然 f = − L ( θ ) f=-L(\theta) f=L(θ)就是需要
最小化的目标函数了。

3.5 解法

解法选用的是 S G D SGD SGD,博文《在线学习算法FTRL》中说过 S G D SGD SGD算法的一些情况。具体说来就是对每一个样本都进行迭代,但是每个样本只影响其相关的参数,跟它无关的参数不影响。对于上面来说,
j j j个样本的第 i j i_j ij个词的负对数似然是
f i j = P ( w i j ∣ C i j ) = − ∑ k i j = 1 k i j [ ( 1 − d k i j l o g σ ( q k i j ⋅ C i j ) + d k i j l o g ( 1 − σ ( q k i j ⋅ C i j ) ) ]                  ( 25 ) \begin{aligned} f_{i_j} &=P(w_{i_j}|C_{i_j}) \\ &= - \sum_{k_{i_j}=1}^{k_{i_j}}[(1-d_{k_{i_j}}log \sigma(q_{k_{i_j}} \cdot C_{i_j}) + d_{k_{i_j}}log(1-\sigma(q_{k_{i_j}}\cdot C_{i_j}))] \end{aligned} \;\;\;\; \;\;\;\;(25) fij=P(wijCij)=kij=1kij[(1dkijlogσ(qkijCij)+dkijlog(1σ(qkijCij))](25)
j j j个样本的第 i j i_j ij个词的在遇到第 k i j k_{i_j} kij个非叶节点时的负对数似然是
f k i j = − ( 1 − d k i j ) l o g σ ( q k i j ) − d k i j l o g ( 1 − σ ( q k i j ⋅ C i j ) )                  ( 26 ) \begin{aligned} f_{k_{i_j}} &=-(1-d_{k_{i_j}})log \sigma(q_{k_{i_j}}) - d_{k_{i_j}} log(1-\sigma(q_{k_{i_j}\cdot C_{i_j}})) \end{aligned} \;\;\;\; \;\;\;\;(26) fkij=(1dkij)logσ(qkij)dkijlog(1σ(qkijCij))(26)
计算 f k i j {f_{{k_{i_j}}}} fkij的梯度,注意参数包括 q k i j 和 C i j {q_{{k_{i_j}}}}和{C_{{i_j}}} qkijCij,其中 C i j {C_{{i_j}}} Cij的梯度是用来计算 w i j {w_{{i_j}}} wij的时候用到。另外需要注意的是 l o g    σ ( x ) log\;\sigma(x) logσ(x)的梯度是 1 − σ ( x ) 1-\sigma(x) 1σ(x)
l o g    ( 1 − σ ( x ) ) log\;(1-\sigma(x)) log(1σ(x))的梯度是 − σ ( x ) -\sigma(x) σ(x)
F q ( q k i j ) = ∂ f k i j ∂ q k i j = − ( 1 − d k i j ) ⋅ ( 1 − σ ( q k i j ⋅ C i j ) ) ⋅ C i j − d i j ⋅ ( − σ ( q k i j ) ) ⋅ C i j = − ( 1 − d k i j − σ ( q k i j ⋅ C i j ) ) ⋅ C i j                  ( 27 ) \begin{aligned} F_q(q_{k_{i_j}}) & =\frac{\partial f_{k_{i_j}}}{\partial q_{k_{i_j}}} \\ & = -(1-d_{k_{i_j}})\cdot(1-\sigma(q_{k_{i_j}} \cdot C_{i_j}))\cdot C_{i_j} - d_{i_j} \cdot (-\sigma(q_{k_{i_j}})) \cdot C_{i_j}\\ & = -(1-d_{k_{i_j}}-\sigma(q_{k_{i_j}\cdot C_{i_j}}))\cdot C_{i_j} \end{aligned} \;\;\;\; \;\;\;\;(27) Fq(qkij)=qkijfkij=(1dkij)(1σ(qkijCij))Cijdij(σ(qkij))Cij=(1dkijσ(qkijCij))Cij(27)

F c ( q k i j ) = ∂ f k i j ∂ q C i j = − ( 1 − d k i j ) ⋅ ( 1 − σ ( q k i j ⋅ C i j ) ) ⋅ q i j − d i j ⋅ ( − σ ( q k i j ) ) ⋅ q i j = − ( 1 − d k i j − σ ( q k i j ⋅ C i j ) ) ⋅ q i j                  ( 28 ) \begin{aligned} F_c(q_{k_{i_j}}) & =\frac{\partial f_{k_{i_j}}}{\partial q_{C_{i_j}}} \\ & = -(1-d_{k_{i_j}})\cdot(1-\sigma(q_{k_{i_j}} \cdot C_{i_j}))\cdot q_{i_j} - d_{i_j} \cdot (-\sigma(q_{k_{i_j}})) \cdot q_{i_j}\\ & = -(1-d_{k_{i_j}}-\sigma(q_{k_{i_j}\cdot C_{i_j}}))\cdot q_{i_j} \end{aligned} \;\;\;\; \;\;\;\;(28) Fc(qkij)=qCijfkij=(1dkij)(1σ(qkijCij))qijdij(σ(qkij))qij=(1dkijσ(qkijCij))qij(28)
上面的 F q F_q Fq F c F_c Fc只是简写,有了梯度就可以对每个参数进行迭代了
q k i j n + 1 = q k i j n − η F q ( q k i j n )                  ( 29 ) \begin{aligned} q_{k_{i_j}}^{n+1} = q_{k_{i_j}}^{n}-\eta F_q(q_{k_{i_j}^n}) \end{aligned} \;\;\;\; \;\;\;\;(29) qkijn+1=qkijnηFq(qkijn)(29)
同时,每个词的词向量也可以进行迭代了
W I n + 1 = W I n − η ∑ k i j = 1 K i j F c q k i j n                  ( 30 ) \begin{aligned} W_I^{n+1} = W_I^n-\eta \sum_{k_{i_j}=1}^{K_{i_j}}F_c{q_{k_{i_j}}^{n}} \end{aligned} \;\;\;\; \;\;\;\;(30) WIn+1=WInηkij=1KijFcqkijn(30)

注意第二个迭代的 W I W_I WI是代表所有的输入词的,也就是假如输入了4个词,这四个词都要根据这个方式进行迭代(注意是上下文的词才是输入词,才根据梯度更新了,但是 w i j w_{i_j} wij这个词本身不更新的,
就是轮到它自己在中间的时候就不更新了)。第二个迭代式确实不好理解,因为这里的意思是所有非叶节点上的对上下文的梯度全部加和就得到了这个词的上下文的梯度,看起来这个就是BP神经网络的误差反向传播。

论文《Hierarchical Probabilistic Neural Network Language Model》和《Three New Graphical Models for Statistical Language Modelling》中看起来也是这么样的解释,人家都是context的几个
词首尾连接得到的一个向量,对这个长向量有一个梯度,或者一个超大的 V × m V\times m V×m矩阵( m m m是词向量的维度),对这个矩阵每个元素有一个梯度,这些梯度自然也包括了输入词的梯度。如果有人发现了这个做法的解释请告知。

3.6 代码中的trick

如前文, c c c表示左右各取多少个词,代码中 c c c是一个从 0 0 0 w i n d o w − 1 window-1 window1的一个数,是对每个词都随机生成的,而这个 w i n d o w window window就是用户自己输入的一个变量,默认值是5(这里得换换概念了,代码中的 c c c代表当

前处理到了那个词的下标,实际的随机变量是 b b b)。代码实际实现的时候是换了一种方法首先生成一个 0 0 0 w i n d o w − 1 window-1 window1的一个数 b b b,然后训练的词(假设是第 i i i个词)的窗口是从第 i − ( w i n d o w − b ) i-(window-b) i(windowb)个词开始到第

i + ( w i n d o w − b ) i+(window-b) i+(windowb)个词结束,中间用 a ! = w i n d o w a != window a!=window这个判断跳过了这个词他自己,也就是更新词向量的时候只更新上下文相关的几个输入词,这个词本身是不更新的。要注意的是每个词的 b b b都不一样的,都是随机

生成的,这就意味着连窗口大小都是随机的。如果有人看过代码,就会发现,其中的 q k i j q_{k_{i_j}} qkij在代码中用矩阵 s y n 1 syn1 syn1表示, C i j C_{i_j} Cij在代码中用neu1表示。叶子节点里面的每个词向量在代码中用 s y n 0 syn0 syn0表示,
利用下标移动去读取。核心代码如下,其中的vocab[word].code[d]就表示 d k i j d_{k_{i_j}} dkij,其他就是迭代过程,代码是写得相当简洁啊。

for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {
        c = sentence_position - window + a;
        if (c < 0) continue;
        if (c >= sentence_length) continue;
        last_word = sen[c];
        if (last_word == -1) continue;
        l1 = last_word * layer1_size;
        for (c = 0; c < layer1_size; c++) neu1e[c] = 0;
        // HIERARCHICAL SOFTMAX
        if (hs) for (d = 0; d < vocab[word].codelen; d++) {
          f = 0;
          l2 = vocab[word].point[d] * layer1_size;
          // Propagate hidden -> output
          for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1[c + l2];
          if (f <= -MAX_EXP) continue;
          else if (f >= MAX_EXP) continue;
          else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
          // 'g' is the gradient multiplied by the learning rate
          g = (1 - vocab[word].code[d] - f) * alpha;
          // Propagate errors output -> hidden
          for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
          // Learn weights hidden -> output
          for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * syn0[c + l1];
        }
        // NEGATIVE SAMPLING
        if (negative > 0) for (d = 0; d < negative + 1; d++) {
          if (d == 0) {
            target = word;
            label = 1;
          } else {
            next_random = next_random * (unsigned long long)25214903917 + 11;
            target = table[(next_random >> 16) % table_size];
            if (target == 0) target = next_random % (vocab_size - 1) + 1;
            if (target == word) continue;
            label = 0;
          }
          l2 = target * layer1_size;
          f = 0;
          for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2];
          if (f > MAX_EXP) g = (label - 1) * alpha;
          else if (f < -MAX_EXP) g = (label - 0) * alpha;
          else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
          for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
          for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * syn0[c + l1];
        }
        // Learn weights input -> hidden
        for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];

代码中的419行就是计算 C i j C_{i_j} Cij,425-428行就是计算f,也就是 σ ( q k i j ⋅ C i j ) \sigma(q_{k_{i_j}}\cdot C_{i_j}) σ(qkijCij)的值,432行就是累积 C i j C_{i_j} Cij的误差,434就是更新 q k i j n + 1 q_{k_{i_j}}^{n+1} qkijn+1

// hidden -> in
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {
	c = sentence_position - window + a;
	if (c < 0) continue;
	if (c >= sentence_length) continue;
	last_word = sen[c];
	if (last_word == -1) continue;
	for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];
	}

注意上面,对每个输入词都进行了更新,更新的幅度就是432行中误差累计的结果。

4. CBOW负采样的网络结构与使用说明

4.1 网络结构与使用说明

网络结构如下
在这里插入图片描述

如图2中,中间的那个隐藏层是把上下文累加起来的一个词向量,然后有一个矩阵 R R R,是在训练过程中用到的临时矩阵,这个矩阵连接隐层与所有输出节点,但是这个矩阵在使用这个网络的时候不怎么用得到,这里也是没弄清楚的一个地方。

每个输出节点代表一个词向量。同样如(二)中的例子,计算这么一个概率,这里的计算方法就简单多了,就是随机从语料库里面抽取 c c c个词,这里假设 c = 3 c=3 c=3,抽中了 D D D E E E F F F这三个词,又假设“吃”这个词的词向量是 A A A,那么就计算“吃”这个词的概率就用下面的公式同样如图2中,那句话的每个词都计算 p ( w i ∣ c o n t e x t i ) p({w_i}|contex{t_i}) p(wicontexti)后连乘起来得到联合概率,这个概率如果大于某个阈值,就认为是正常的话;否则就认为不是自然语言,要排除掉。这里只是说明这个网络是怎么样的例子,真正重要的始终是那些词向量。

4.2 CBOW负采样的优化目标

为啥要抽样呢?目的跟图2中的霍夫曼树其实是一样的,都是为了节省计算量,这个计算量就是式(5)中计算 P ( A ∣ C ) P(A|C) P(AC)的概率,因为这个概率实在不好算。

论文《Distributed Representations of Words and Phrases and their Compositionality》中提到有一个叫NCE的方法可以来代替上面的那个hierarchical softmax方法(就是使用霍夫曼树的方法),但是由于word2vec只关心怎么学到高质量的词向量,所以就用了一种简单的NCE方法,称为NEG,方法的本质就是在第 j j j个句子的第 i j i_j ij个词 w i j w_{ij} wij处使用下面的式子代替
l o g    σ ( w i j ⋅ C i j ) + ∑ k = 1 K E w k ∼ P v ( w ) l o g ( 1 − σ ( w k ⋅ C i j ) )                  ( 31 ) \begin{aligned} log\;\sigma(w_{i_j} \cdot C_{i_j}) + \sum_{k=1}^K E_{w_k\sim P_v(w)}log(1-\sigma(w_k\cdot C_{i_j})) \end{aligned} \;\;\;\; \;\;\;\;(31) logσ(wijCij)+k=1KEwkPv(w)log(1σ(wkCij))(31)

其中 E E E下面的那个下标的意思是 w k w_k wk是符合某个分布的,在这里 P V ( w ) P_V (w) PV(w)表示词频的分布。这个式子的第二项是求 K K K个期望,这 K K K个期望中的每一个期望,都是在该上下文的情况下不出现这个词的期望。这里出现一个特别大的偷懒,就是认为这个期望只要抽取一个样本就能计算出来,当然,如果说遍历完整个语料库,其实还可以认为是抽取了多次实验的,因为每次抽取词的时候,是按照词频的分布来抽样的,如果抽样的次数足够多,在总体的结果上,这里计算的期望还是接近这个期望的真实值的,这个可以参考博文中 R B M RBM RBM中计算梯度的时候的那个蒙特卡洛抽样来理解。

在这里,从代码上体现来看,就只用一个样本来估算这个期望的,所有式子被简化成了下面的形式

l o g    σ ( w i j ⋅ C i j ) + ∑ k = 1 K l o g    ( 1 − σ ( w k ⋅ C i j ) )                  ( 32 ) \begin{aligned} log\; \sigma(w_{i_j} \cdot C_{i_j}) + \sum_{k=1}^Klog\;(1-\sigma(w_k\cdot C_{i_j})) \end{aligned} \;\;\;\; \;\;\;\;(32) logσ(wijCij)+k=1Klog(1σ(wkCij))(32)
用这个式子取代(21)中的 l o g    P ( w i j ∣ c o n t e x t i j ) log\;P(w_{i_j}|context_{i_j}) logP(wijcontextij),就能得到CBOW加抽样的目标函数(去掉 1 V \frac{1}{V} V1的),这个目标函数也是极其像logistic regression的目标函数,其中 w i j w_{i_j} wij是正类样本, w k w_k wk是负类样本。为了统一表示,正类样本设置一个label为1,负类样本设置label为0,每个样本的负对数似然都变成下面的方式 为了统一表示,正类样本设置一个label为1,负类样本设置label为0,每个样本的负对数似然都变成下面的方式
f w = − l a b e l ⋅ l o g σ ( w ⋅ C i j ) − ( 1 − l a b e l ) l o g    ( 1 − σ ( w ⋅ C i j ) )                  ( 33 ) \begin{aligned} f_w=-label \cdot log \sigma(w \cdot C_{i_j}) - (1-label)log\; (1- \sigma(w \cdot C_{i_j})) \end{aligned} \;\;\;\; \;\;\;\;(33) fw=labellogσ(wCij)(1label)log(1σ(wCij))(33)

4.3 CBOW 负采样求解

解法还是用 S G D SGD SGD,所以对一个词 w i j w_{i_j} wij来说,这个词本身是一个正类样本,同时对这个词,还随机抽取了 k k k个负类样本,那么每个词在训练的时候都有 k + 1 k+1 k+1个样本,所以要做 k + 1 k+1 k+1 S G D SGD SGD
对于每个样本求两个梯度
F w ( w ) = ∂ f w ∂ w = − l a b e l ⋅ ( 1 − σ ( w ⋅ C i j ) ) ⋅ C i j + ( 1 − l a b e l ) ⋅ σ ( w ⋅ C i j ) ⋅ C i j = − ( l a b e l − σ ( w ⋅ C i j ) ) ⋅ C i j                  ( 34 ) \begin{aligned} F_w(w) &= \frac{\partial f_w}{\partial w} \\ & = -label \cdot (1-\sigma(w \cdot C_{i_j})) \cdot C_{i_j} + (1-label) \cdot \sigma(w\cdot C_{i_j}) \cdot C_{i_j} \\ & = -(label - \sigma(w\cdot C_{i_j})) \cdot C_{i_j} \end{aligned} \;\;\;\; \;\;\;\;(34) Fw(w)=wfw=label(1σ(wCij))Cij+(1label)σ(wCij)Cij=(labelσ(wCij))Cij(34)

F w ( w ) = ∂ f w ∂ C i j = − l a b e l ⋅ ( 1 − σ ( w ⋅ C i j ) ) ⋅ w + ( 1 − l a b e l ) ⋅ σ ( w ⋅ C i j ) ⋅ w = − ( l a b e l − σ ( w ⋅ C i j ) ) ⋅ w                  ( 35 ) \begin{aligned} F_w(w) &= \frac{\partial f_w}{\partial C_{i_j}} \\ & = -label \cdot (1-\sigma(w \cdot C_{i_j})) \cdot w + (1-label) \cdot \sigma(w\cdot C_{i_j}) \cdot w \\ & = -(label - \sigma(w\cdot C_{i_j})) \cdot w \end{aligned} \;\;\;\; \;\;\;\;(35) Fw(w)=Cijfw=label(1σ(wCij))w+(1label)σ(wCij)w=(labelσ(wCij))w(35)
两个梯度都有这么相似的形式实在太好了,然后开始迭代,代码里面有个奇怪的地方,就是一开的网络结构中的那个 V ∗ m V*m Vm的矩阵,用来保存的是每次抽中的负类样本的词向量,每一行代表一个词向量,刚好是所有词的词向量。每次抽样抽中一个词后,拿去进行迭代的(就是计算梯度什么的),就是这个矩阵中对应的那个词向量,而且这个矩阵还参与更新,就是第一个梯度实际更新的是这个矩阵里面的词向量。

但总的看来,这个矩阵在迭代计算完了就丢弃了,因为最终更新的词向量,每次只有一个,就是公式一直出现的那个词 w i j w_{i_j} wij,最终输出的,也是输入里面的词向量(在图中是最下面的输出层的那些词向量),当然这个矩阵可以认为是隐藏层跟输出层的连接矩阵。

R w n + 1 = R w n − η F w ( R w n )                  ( 36 ) \begin{aligned} R_{w}^{n+1} = R_w^n - \eta F_w(R_w^n) \end{aligned} \;\;\;\; \;\;\;\;(36) Rwn+1=RwnηFw(Rwn)(36)

其中 w w w代表每个样本对应的词向量,包括 w i j w_{i_j} wij和抽中的词向量,注意更新的是 R R R这个连接矩阵。词向量的更新公式如下
W I n + 1 = W I n − η ( F c ( R W I n ) + ∑ k = 1 K F c ( R w k n ) )                  ( 37 ) \begin{aligned} W_{I}^{n+1} = W_I^n- \eta (F_c(R_{W_I}^n) + \sum^K_{k=1}F_c(R_{w_k}^n)) \end{aligned} \;\;\;\; \;\;\;\;(37) WIn+1=WInη(Fc(RWIn)+k=1KFc(Rwkn))(37)
注意每个梯度的值的计算都是拿连接矩阵R中对应的那一行来算的,这样看起来可以避免每次都更新输出层的词向量,可能是怕搞乱了吧。

4.4 CBOW负采样方法代码中的trick

随机数是自己产生的,代码里面自己写了随机数程序。

每个词都要执行negative次抽样的,如果遇到了当前词(就是label为1的词)就提前退出抽样,这个步骤就提前完成了,这个也是一个比较费解的trick。

其中前面说的R矩阵在代码中用syn1neg表示, C i j C_{i_j} Cij在代码中用neu1表示,同样的,叶子节点里面的每个词向量在代码中用syn0表示,利用下标移动去读取。

// NEGATIVE SAMPLING
if (negative > 0) for (d = 0; d < negative + 1; d++) {
	if (d == 0) {
	target = word;
	label = 1;
	} else {
		next_random = next_random * (unsigned long long)25214903917 + 11;
		target = table[(next_random >> 16) % table_size];
		if (target == 0) target = next_random % (vocab_size - 1) + 1;
		if (target == word) continue;
		label = 0;
	}
	l2 = target * layer1_size;
	f = 0;
	for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];
	if (f > MAX_EXP) g = (label - 1) * alpha;
	else if (f < -MAX_EXP) g = (label - 0) * alpha;
	else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
	for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
	for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];
	}

其中442-446就是抽样的代码了,是作者自己写的模块,然后label这个变量跟上文的label意思是一样的, f f f就表示 σ ( w ⋅ C i j ) \sigma(w\cdot C_{i_j}) σ(wCij)的值,syn1neg就保存了矩阵R每一行的值,neu1e还是累积误差,直到一轮抽样完了后再更新输入层的词向量。对输入层的更新模块和上面的(二)中一样的,都是更新所有的输入词。

// hidden -> in
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {
	c = sentence_position - window + a;
	if (c < 0) continue;
	if (c >= sentence_length) continue;
	last_word = sen[c];
	if (last_word == -1) continue;
	for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];
	}

5 Skip-gram 分层softmax优化目标与解问题

在这里插入图片描述

其中的 W i W_i Wi是相应的词,词 W i W_i Wi h u f f m a n huffman huffman树直接连接,没有隐藏层的。使用方法依然与cbow加层次的相似。在判断“大家 喜欢 吃 好吃 的 苹果”这句话是否自然语言的时候,是这么来的,同样比如计算到了“吃”这个词,同样随机抽到的 c = 2 c=2 c=2,对吃这个词需要计算的东西比较多,总共要计算的概率是p(大家 ∣ | 吃),p(喜欢 ∣ | 吃),p(好吃 ∣ | 吃)和p(的 ∣ | 吃)总共四个,在计算p(大家 ∣ | 吃)这个概率的时候,要用到上面图中的二叉树,假设“大家”这个词在huffman树根节点的右孩子的最左边的那个节点,就是图中右数第三个叶子节点。再假设从根节点开始到这个叶子节点的路径上的三个非叶节点分别为A,B,C(从高到低排列的),“吃”这个词的词向量设为D,那么p(大家 ∣ | 吃)这个概率可以用下面的公式算概率

p(大家 ∣ | 吃)= 1 − σ ( A ⋅ D ) ⋅ σ ( B ⋅ D ) σ ( C D ˙ ) 1 - \sigma(A\cdot D) \cdot \sigma(B \cdot D) \sigma(C \dot D) 1σ(AD)σ(BD)σ(CD˙)

同样的方法计算p(喜欢 ∣ | 吃),p(好吃 ∣ | 吃)和p(的 ∣ | 吃),再把这四个概率连乘,得到了“吃”这个词的上下文概率,注意这只是一个词的概率。

把一整句话的所有词的概率都计算出来后进行连乘,得到的就是这句话是自然语言的概率。这个概率如果大于某个阈值,就认为是正常的话;否则就认为不是自然语言,要排除掉。
再声明一下,这里只是说明这个网络是怎么样的例子,真正重要的始终是那些词向量。

5.1 目标函数

假设语料库是有 S S S个句子组成的一个句子序列(顺序不重要),整个语料库有 V V V个词,似然函数就会构建成下面的样子。
L ( θ ) = ∏ j S ( ∏ i j = 1 T j ( ∏ − c i j < u i j < c i j , j ≠ 0 P ( w u i j + i j ∣ w i j ) ) )                  ( 38 ) \begin{aligned} L(\theta) = \prod_j^S(\prod_{i_j=1}^{T_j}(\prod_{-c_{i_j}<u_{i_j}<c_{i_j},j\ne0} P(w_{u_{i_j}+i_j} | w_{i_j}))) \end{aligned} \;\;\;\; \;\;\;\;(38) L(θ)=jS(ij=1Tj(cij<uij<cij,j=0P(wuij+ijwij)))(38)
其中 T j T_j Tj表示第 j j j个句子的词个数, w u i j + i j w_{u_{i_j}+i_j} wuij+ij表示词 w i j w_{i_j} wij左右的各 c i j c_{i_j} cij个词的其中一个,注意 c i j c_{i_j} cij对每个 w i j w_{i_j} wij都不一样的。极大似然要对整个语料库去做的。但是,Google这公司,为了代码风格的统一,用了一个trick,我举例说明吧。对于一开始的那句话:大家 喜欢 吃 好吃 的 苹果,总共6个词,假设每次的 c i j c_{i_j} cij都抽到了2,按照上面的公式中的 ∏ i j = 1 T j ( ∏ − c i j < u i j < c i j , j ≠ 0 p ( w u i j + i j ∣ w i j ) ) \prod_{i_j=1}^{T_j}(\prod_{-c_{i_j}< u_{i_j} < c_{i_j},j\ne0} p(w_{u_{i_j}+i_j}|w_{i_j})) ij=1Tj(cij<uij<cij,j=0p(wuij+ijwij))部分按每个词作为条件 w i j w_{i_j} wij展开,看到得到什么吧。
大家:P(喜欢 ∣ | 大家) × \times × P(吃 ∣ | 大家)
喜欢:P(大家 ∣ | 喜欢) × \times × P(吃 ∣ | 喜欢) × \times × P(好吃 ∣ | 喜欢)
吃: P(大家 ∣ | 吃) × \times × P(喜欢 ∣ | 吃) × \times × P(好吃 ∣ | 吃) × \times × P(的 ∣ | 吃)
好吃:P(喜欢 ∣ | 好吃) × \times × P(吃 ∣ | 好吃) × \times × P(的 ∣ | 好吃) × \times × P(苹果 ∣ | 好吃)
的: P(吃 ∣ | 的) × \times × P(好吃 ∣ | 的) × \times × P(苹果 ∣ | 的)
苹果:P(好吃 ∣ | 苹果) × \times × P(的 ∣ | 苹果)

把结果重新组合一下,得到下面的组合方式。
大家:P(大家 ∣ | 喜欢) × \times × P(大家 ∣ | 吃)
喜欢:P(喜欢 ∣ | 大家) × \times × P(喜欢 ∣ | 吃) × \times × P(喜欢 ∣ | 好吃)
吃: P(吃 ∣ | 大家) × \times × P(吃 ∣ | 喜欢) × \times × P(吃 ∣ | 好吃) × \times × P(吃 ∣ | 的)
好吃:P(好吃 ∣ | 喜欢) × \times × P(好吃 ∣ | 吃) × \times × P(好吃 ∣ | 的) × \times × P(好吃|苹果)
的: P(的 ∣ | 吃) × \times × P(的 ∣ | 好吃) × \times × P(的 ∣ | 苹果)
苹果:P(苹果 ∣ | 好吃) × \times × P(苹果 ∣ | 的)
不证明,不推导,直接得到下面的结论
∏ i j T j ( ∏ − c i j < u i j < c i j , j ≠ 0 P ( w u i j + i j ∣ w i j ) ) = ∏ i j = 1 T j ( ∏ − c i j < u i j < c i j , j ≠ 0 p ( w i j ∣ w u i j + i j ) )                  ( 39 ) \begin{aligned} \prod_{i_j}^{T_j}(\prod_{-c_{i_j}<u_{i_j}<c_{i_j},j\ne0}P(w_{u_{i_j}+i_j}|w_{i_j}))=\prod_{i_j=1}^{T_j}(\prod_{-c_{i_j} < u_{i_j}<c_{i_j},j\ne 0}p(w_{i_j}|w_{u_{i_j}+i_j})) \end{aligned} \;\;\;\; \;\;\;\;(39) ijTj(cij<uij<cij,j=0P(wuij+ijwij))=ij=1Tj(cij<uij<cij,j=0p(wijwuij+ij))(39)
这个是网易有道的那个资料给出的结论,这里是变成了本文的方式来描述,具体参考网易有道的原文。这个变化倒是莫名其妙的,不过也能理解,Google公司的代码是要求很优美的,这样做能把代码极大地统一起来。

对数似然就会是下面的样子
L ( θ ) = l o g    L ( θ ) = 1 V ∑ j = 1 S ( ∑ i j = 1 T j ( ∑ − c i j < u i j < c i j , j ≠ 0 l o g    P ( w i j ∣ w u i j + i j ) ) )                  ( 40 ) \begin{aligned} L(\theta) = log\;L(\theta) = \frac{1}{V} \sum_{j=1}^S(\sum_{i_j=1}^{T_j}(\sum_{-c_{i_j}<u_{i_j}<c_{i_j},j\ne 0}log\; P(w_{i_j}|w_{u_{i_j}+i_j}))) \end{aligned} \;\;\;\; \;\;\;\;(40) L(θ)=logL(θ)=V1j=1S(ij=1Tj(cij<uij<cij,j=0logP(wijwuij+ij)))(40)
其中的 V V V表示整个语料库的词没有去重的总个数,整个目标函数也可以叫交叉熵,但是这里对这也不感兴趣,一般去掉。
这就涉及到计算某个词的概率了,如 p ( w u i j + i j ∣ w i j ) p({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}}) p(wuij+ijwij),这个概率变了,条件变成输入中要考察的那个词,计算条件概率的词变成了上下文的词。当然,计算方法没有变,前面介绍的huffman树的计算方法到这里是一摸一样的。

P ( w ∣ I ) = ∏ k = 1 K p ( d k ∣ q k , I ) = ∏ k = 1 K ( ( σ ( q k ⋅ I ) ) 1 − d k ⋅ ( 1 − σ ( q k ⋅ I ) ) d k )                  ( 41 ) \begin{aligned} P(w|I)=\prod_{k=1}^{K}p(d_k|q_k,I) = \prod_{k=1}^{K}((\sigma(q_k\cdot I))^{1-d_k} \cdot (1-\sigma(q_k\cdot I))^{d_k}) \end{aligned} \;\;\;\; \;\;\;\;(41) P(wI)=k=1Kp(dkqk,I)=k=1K((σ(qkI))1dk(1σ(qkI))dk)(41)

其中 I I I表示输入的那个词,那么 w w w就表示例子中的输出的词汇; q k q_k qk表示从根节点下来到输出词汇 w w w,所在的叶子节点的路径上的非叶节点, d k d_k dk就是编码了,也可以说是分类,当 w w w在某个节点如 q k q_k qk的左子树的叶子节点上时 d k = 0 d_k=0 dk=0,否则 d k = 1 d_k=1 dk=1

用这个式子代替掉上面的(38)中的似然函数中的 p ( w u i j + i j ∣ w i j ) p({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}}) p(wuij+ijwij),当然每个变量都对号入座,就能得到总的似然函数了。

再对这个式子求个对数,得到:
l o g    P ( w ∣ I ) = ∑ k = 1 K ( ( 1 − d k ) l o g    σ ( q k ⋅ I ) + d k ⋅ ( 1 − σ ( q k ⋅ I ) ) )                  ( 42 ) \begin{aligned} log\;P(w|I) = \sum_{k=1}^{K}((1-d_k)log\;\sigma(q_k\cdot I) + d_k \cdot (1-\sigma (q_k\cdot I))) \end{aligned} \;\;\;\; \;\;\;\;(42) logP(wI)=k=1K((1dk)logσ(qkI)+dk(1σ(qkI)))(42)
再利用这个式子替换掉(40)中的 l o g p ( w u i j + i j ∣ w i j ) {{\rm{log}}p({w_{{u_{i_j}} + {i_j}}}|{w_{{i_j}}})} logp(wuij+ijwij)就能得到总的对数似然函数,也就是目标函数,剩下的就是怎么解了。可以注意的是,计算每个词(例中的“吃”)上下文概率的时候都要计算好几个条件概率的(例子中p(大家 ∣ | 吃),p(喜欢 ∣ | 吃),p(好吃 ∣ | 吃)和p(的 ∣ | 吃)),这每个条件概率又是需要在huffman树上走好几个非叶节点的,每走到一个非叶节点,都需要计算一个
( 1 − d k ) l o g σ ( q k ⋅ I ) + d k ⋅ ( 1 − σ ( q k ⋅ I ) ) {\left( {1 - {d_k}} \right)log\sigma \left( {{q_k} \cdot {\rm{I}}} \right) + {d_k} \cdot \left( {1 - \sigma \left( {{q_k} \cdot I} \right)} \right)} (1dk)logσ(qkI)+dk(1σ(qkI))的。可以看到,走到每一个非叶节点,在总的对数似然函数中,都类似logistic regression的一个样本,为了方便描述,就用样本和label的方式在称呼这些东西。跟一般的logistic regression一样,每走到一个非叶节点,如果是向左走的,就定义label为1,为正样本;否则label就是0,是负样本。这样label= 1 − d k 1-d_k 1dk,每一个非叶节点都为整个问题产生了一个样本。

5.2 解法

解法选用的是 S G D SGD SGD,在处理每个样本时,对总目标函数的贡献是
l f = P ( d k ∣ q k , I ) = − ( 1 − d k ) l o g    σ ( q k ⋅ I ) − d k ⋅ ( 1 − σ ( q k ⋅ I ) )                  ( 43 ) \begin{aligned} lf = P(d_k|q_k,I) = -(1-d_k)log\; \sigma(q_k \cdot I) - d_k \cdot (1-\sigma(q_k \cdot I)) \end{aligned} \;\;\;\; \;\;\;\;(43) lf=P(dkqk,I)=(1dk)logσ(qkI)dk(1σ(qkI))(43)
计算梯度
F q ( q k ) = l f q k = − ( 1 − d k ) ⋅ ( 1 − σ ( q k ⋅ I ) ) ⋅ I − d k ⋅ ( − σ ( q k ⋅ I ) ) ⋅ I = − ( 1 − d k − σ ( q k ⋅ I ) ) ⋅ I                  ( 44 ) \begin{aligned} F_q(q_k) &= \frac{lf}{q_k} \\ &= -(1-d_k) \cdot (1-\sigma(q_k \cdot I)) \cdot I - d_k \cdot (-\sigma(q_k\cdot I)) \cdot I \\ &=-(1-d_k-\sigma(q_k\cdot I)) \cdot I \end{aligned} \;\;\;\; \;\;\;\;(44) Fq(qk)=qklf=(1dk)(1σ(qkI))Idk(σ(qkI))I=(1dkσ(qkI))I(44)

F i ( q k ) = ∂ l f ∂ I = − ( 1 − d k ) ⋅ ( 1 − σ ( q k ⋅ I ) ) ⋅ q k − d k ⋅ ( − σ ( q k ⋅ I ) ) ⋅ q k = − ( 1 − d k − σ ( q k ⋅ I ) ) ⋅ q k                  ( 45 ) \begin{aligned} F_i(q_k) &= \frac{\partial lf}{\partial I} \\ &=-(1-d_k) \cdot (1-\sigma(q_k \cdot I)) \cdot q_k - d_k \cdot (-\sigma(q_k \cdot I)) \cdot q_k \\ &=-(1-d_k - \sigma(q_k \cdot I)) \cdot q_k \end{aligned} \;\;\;\; \;\;\;\;(45) Fi(qk)=Ilf=(1dk)(1σ(qkI))qkdk(σ(qkI))qk=(1dkσ(qkI))qk(45)
更新
q n + 1 k = q k n − η F q ( q n k ) I n + 1 = I n − η F i ( q k n )                  ( 46 ) \begin{aligned} &q_{n+1}^{k}=q_{k}^{n} - \eta F_q(q_{n}^{k})\\ &I^{n+1} = I^n - \eta F_i(q_k^{n}) \end{aligned} \;\;\;\; \;\;\;\;(46) qn+1k=qknηFq(qnk)In+1=InηFi(qkn)(46)

5.3 代码中的trick

对输入词I的更新是走完整个huffman树后对整个误差一起计算的,这个误差保存在neu1e这个数组里面。

// HIERARCHICAL SOFTMAX
if (hs) for (d = 0; d < vocab[word].codelen; d++) {
	f = 0;
	l2 = vocab[word].point[d] * layer1_size;
	// Propagate hidden -> output
	for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1[c + l2];
	if (f <= -MAX_EXP) continue;
	else if (f >= MAX_EXP) continue;
	else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
	// 'g' is the gradient multiplied by the learning rate
	g = (1 - vocab[word].code[d] - f) * alpha;
	// Propagate errors output -> hidden
	for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
	// Learn weights hidden -> output
	for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * syn0[c + l1];
	}

其中480-483行是计算 σ ( q k ⋅ I ) \sigma \left( {{q_k} \cdot I} \right) σ(qkI)的值,保存在f中,vocab[word].code[d]表示的就是dk的值,neu1e就保存了从根节点到叶子节点的路径上的所有非叶节点的累积误差。

// Learn weights input -> hidden
for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];

这个误差反向传播就简单多了,每次都是对一个词进行更新的,就是 P ( w ∣ I ) P(w|I) P(wI)中的那个 w w w

6 Skip-gram 负采样的优化目标与解问题

这个就简单说说吧,不清楚的看上面的。
##6.1 网络结构与使用说明在这里插入图片描述

使用说明就不说的,同样是抽样的。

如(四)中,有一个矩阵 R R R,是在训练过程中用到的临时矩阵,这个矩阵连接隐层与所有输出节点,但是这个矩阵在使用这个网络的时候不怎么用得到,这里也是没弄清楚的一个地方。每个输出节点代表一个词向量。
同样如(五)中的例子,计算 p p p(大家 ∣ | 吃)这么一个概率,这里的计算方法就简单多了,就是随机从语料库里面抽取 c c c个词,这里假设 c = 3 c=3 c=3,抽中了D,E,F这三个词,又假设“吃”这个词的词向量是A,那么就计算“吃”这个词的概率就用下面的公式
P ( χ ∣ c o n t e x t χ ) = σ ( A ⋅ C ) ⋅ ( 1 − σ ( D ⋅ C ) ) ⋅ ( 1 − σ ( E ⋅ C ) ) ⋅ ( 1 − σ ( F ⋅ C ) )                  ( 47 ) \begin{aligned} P(\chi|context_{\chi}) = \sigma(A\cdot C) \cdot (1-\sigma(D\cdot C)) \cdot (1-\sigma(E\cdot C)) \cdot (1-\sigma(F\cdot C)) \end{aligned} \;\;\;\; \;\;\;\;(47) P(χcontextχ)=σ(AC)(1σ(DC))(1σ(EC))(1σ(FC))(47)
其中 χ \chi χ表示吃这个单词。同样如(五)中,那句话的每个词都计算与上下文几个词的概率( P P P(大家 ∣ | 吃), P P P(喜欢 ∣ | 吃), P P P(好吃 ∣ | 吃)和 P P P(的 ∣ | 吃))后连乘起来得到词“吃”的概率,所有词的概率计算出来再连乘就是整句话是自然语言的概率了。剩下的同上。

6.2 目标函数与解法

似然函数跟(38)一样的,对数似然函数跟(40)是一样的,但是计算 l o g p ( w u i j + i j ∣ w i j ) {\rm{logp}}({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}}) logp(wuij+ijwij)也就是logp(w|I)的的时候不一样,论文《Distributed Representations of Words and Phrases and their Compositionality》中认为logp(w|I)可以用下面的式子
l o g    σ ( w ⋅ I ) + ∑ k = 1 K E w ∼ p v ( w ) [ l o g ( 1 − σ ( w k ⋅ C i j ) ) ]                  ( 48 ) \begin{aligned} log\; \sigma(w\cdot I) + \sum_{k=1}^{K} E_{w \sim p_v(w)}[log(1-\sigma(w_k \cdot C_{i_j}))] \end{aligned} \;\;\;\; \;\;\;\;(48) logσ(wI)+k=1KEwpv(w)[log(1σ(wkCij))](48)
代替。同样可以和(四)中一样,设定正负样本的概念。为了统一表示,正类样本设置一个label为1,负类样本设置label为0,每个样本的负对数似然都变成下面的方式
f w = − l a b e l ⋅ l o g    σ ( w ⋅ I ) − ( 1 − l a b e l ) l o g    ( 1 − σ ( w ⋅ I ) )                  ( 49 ) \begin{aligned} f_w=-label \cdot log\; \sigma(w\cdot I) - (1-label)log\;(1-\sigma(w\cdot I)) \end{aligned} \;\;\;\; \;\;\;\;(49) fw=labellogσ(wI)(1label)log(1σ(wI))(49)
梯度
F w ( w ) = ∂ f w ∂ w = − l a b e l ⋅ ( 1 − σ ( w ⋅ I ) ) ⋅ C i j + ( 1 − l a b e l ) ⋅ σ ( w ⋅ I ) ⋅ I = − ( l a b e l − σ ( w ⋅ I ) ) ⋅ I                  ( 50 ) \begin{aligned} F_w(w) &=\frac{\partial f_w}{\partial w} \\ &=-label \cdot (1-\sigma(w\cdot I)) \cdot C_{i_j} + (1-label)\cdot \sigma(w\cdot I)\cdot I \\ &=-(label-\sigma(w\cdot I))\cdot I \end{aligned} \;\;\;\; \;\;\;\;(50) Fw(w)=wfw=label(1σ(wI))Cij+(1label)σ(wI)I=(labelσ(wI))I(50)

F w ( w ) = ∂ f w ∂ I = − l a b e l ⋅ ( 1 − σ ( w ⋅ I ) ) ⋅ w + ( 1 − l a b e l ) ⋅ σ ( w ⋅ I ) ⋅ w = − ( l a b e l − σ ( w ⋅ I ) ) ⋅ w                  ( 51 ) \begin{aligned} F_w(w) &=\frac{\partial f_w}{\partial I} \\ &=-label \cdot (1-\sigma(w\cdot I)) \cdot w + (1-label)\cdot \sigma(w\cdot I)\cdot w \\ &=-(label-\sigma(w\cdot I))\cdot w \end{aligned} \;\;\;\; \;\;\;\;(51) Fw(w)=Ifw=label(1σ(wI))w+(1label)σ(wI)w=(labelσ(wI))w(51)
更新
R w n + 1 = R w n − η F w ( R w n ) I n + 1 = I n − η ( F c ( R I n n ) + ∑ k = 1 K F c ( R w k n ) )                  ( 52 ) \begin{aligned} &R_w^{n+1} = R_w^n - \eta F_w(R^n_w) \\ & I^{n+1} = I^n - \eta(F_c(R^n_{I^n}) + \sum_{k=1}^K F_c(R_{w_k}^n)) \end{aligned} \;\;\;\; \;\;\;\;(52) Rwn+1=RwnηFw(Rwn)In+1=Inη(Fc(RInn)+k=1KFc(Rwkn))(52)

6.3 代码

还是每个词都要执行negative次抽样的,如果遇到了当前词(就是label为1的词)就提前退出抽样。

// NEGATIVE SAMPLING
if (negative > 0) for (d = 0; d < negative + 1; d++) {
	if (d == 0) {
		target = word;
		label = 1;
	} else {
		next_random = next_random * (unsigned long long)25214903917 + 11;
		target = table[(next_random >> 16) % table_size];
		if (target == 0) target = next_random % (vocab_size - 1) + 1;
		if (target == word) continue;
	}
		l2 = target * layer1_size;
		f = 0;
		for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2];
		if (f > MAX_EXP) g = (label - 1) * alpha;
		else if (f < -MAX_EXP) g = (label - 0) * alpha;
		else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
		for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
		for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * syn0[c + l1];
	}

其中493-502就是抽样的代码,505-508是计算 σ ( w ⋅ I ) \sigma(w\cdot I) σ(wI)的值,保存在f中,syn1neg就是保存了矩阵R中的每一行的值。而neu1e还是累积这误差,直到一轮抽样完了后再更新输入层的词向量。

// Learn weights input -> hidden
for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];

更新输入层还是一样。

7 一些总结

从代码看来,word2vec的作者Mikolov是个比较实在的人,那种方法效果好就用哪种,也不纠结非常严格的理论证明,代码中的trick也是很实用的,可以参考到其他地方使用。

8 致谢

多位Google公司的研究员无私公开的资料。
多位博主的博客资料,包括@peghoty,就是deeplearning学习群里面的皮果提。

9 参考文献

[1] Deep Learning实战之word2vec,网易有道的pdf
[2]@杨超在知乎上的问答《Word2Vec的一些理解》
[3] hisen博客的博文
[4] Hierarchical probabilistic neural network language model. Frederic Morin and Yoshua Bengio.
[5] Distributed Representations of Words and Phrases and their Compositionality T. Mikolov, I. Sutskever, K. Chen, G. Corrado, and J. Dean.
[6] A neural probabilistic language model Y. Bengio, R. Ducharme, P. Vincent.
[7] Linguistic Regularities in Continuous Space Word Representations. Tomas Mikolov,Wen-tau Yih,Geoffrey Zweig
[8] Efficient Estimation of Word Representations in Vector Space. Tomas Mikolov,Kai Chen,Greg Corrado,Jeffrey Dean.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值