• 深度学习word2vec笔记之算法篇


    (转)深度学习word2vec笔记之算法篇

    声明:

    1)该博文是Google专家以及多位博主所无私奉献的论文资料整理的。具体引用的资料请看参考文献。具体的版本声明也参考原文献

    2)本文仅供学术交流,非商用。所以每一部分具体的参考资料并没有详细对应,更有些部分本来就是直接从其他博客复制过来的。如果某部分不小心侵犯了大家的利益,还望海涵,并联系老衲删除或修改,直到相关人士满意为止。

    3)本人才疏学浅,整理总结的时候难免出错,还望各位前辈不吝指正,谢谢。

    4)阅读本文需要机器学习、概率统计算法等等基础(如果没有也没关系了,没有就看看,当做跟同学们吹牛的本钱),基础篇url:http://blog.csdn.net/mytestmy/article/details/26961315 。

    5)此属于第一版本,若有错误,还需继续修正与增删。还望大家多多指点。请直接回帖,本人来想办法处理。

    6)本人手上有word版的和pdf版的,有必要的话可以上传到csdn供各位下载

    前言

    在看word2vec的资料的时候,经常会被叫去看那几篇论文,而那几篇论文也没有系统地说明word2vec的具体原理和算法,所以老衲就斗胆整理了一个笔记,希望能帮助各位尽快理解word2vec的基本原理,避免浪费时间。

    当然如果已经了解了,就随便看看得了。

    一. CBOW加层次的网络结构与使用说明

    Word2vec总共有两种类型,每种类型有两个策略,总共4种。这里先说最常用的一种。这种的网络结构如下图。

    其中第一层,也就是最上面的那一层可以称为输入层。输入的是若干个词的词向量(词向量的意思就是把一个词表示成一个向量的形式表达,后面会介绍)。中间那个层可以成为隐层,是输入的若干个词向量的累加和,注意是向量的累加和,结果是一个向量。
    第三层是方框里面的那个二叉树,可以称之为输出层,隐层的那个节点要跟输出层的那个二叉树的所有非叶节点链接的,线太多画不过来了。第三层的这个二叉树是一个霍夫曼树,每个非叶节点也是一个向量,但是这个向量不代表某个词,代表某一类别的词;每个叶子节点代表一个词向量,为了简单只用一个w表示,没有下标。另外要注意的是,输入的几个词向量其实跟这个霍夫曼树中的某几个叶子节点是一样的,当然输入的那几个词跟它们最终输出的到的那个词未必是同一个词,而且基本不会是同一个词,只是这几个词跟输出的那个词往往有语义上的关系。
    还有要注意的是,这个霍夫曼树的所有叶子节点就代表了语料库里面的所有词,而且是每个叶子节点对应一个词,不重复。
    这个网络结构的功能是为了完成一个的事情——判断一句话是否是自然语言。怎么判断呢?使用的是概率,就是计算一下这句话的“一列词的组合”的概率的连乘(联合概率)是多少,如果比较低,那么就可以认为不是一句自然语言,如果概率高,就是一句正常的话。这个其实也是语言模型的目标。前面说的“一列词的组合”其实包括了一个词跟它的上下文的联合起来的概率,一种普通的情况就是每一个词跟它前面所有的词的组合的概率的连乘,这个后面介绍。
    对于上面的那个网络结构来说,网络训练完成后,假如给定一句话s,这句话由词w1,w2,w3,…,wT组成,就可以利用计算这句话是自然语言的概率了,计算的公式是下面的公式
    {
m{p}}left( {
m{s}} 
ight) = {
m{p}}left( {{w_1},{w_2}, cdots {w_T}} 
ight) =  prod limits_{i = 1}^T p({w_i}|Contex{t_i})
    其中的Context表示的是该词的上下文,也就是这个词的前面和后面各若干个词,这个“若干”(后面简称c)一般是随机的,也就是一般会从1到5之间的一个随机数;每个p({w_i}|Contex{t_i})代表的意义是前后的c个词分别是那几个的情况下,出现该词的概率。举个例子就是:“大家 喜欢 吃 好吃 的 苹果”这句话总共6个词,假设对“吃”这个词来说c随机抽到2,则“吃”这个词的context是“大家”、“喜欢”、“好吃”和“的”,总共四个词,这四个词的顺序可以乱,这是word2vec的一个特点。
    计算p({w_i}|Contex{t_i})的时候都要用到上面的那个网络,具体计算的方法用例子说明,假设就是计算“吃”这个词的在“大家”、“喜欢”、“好吃”和“的”这四个词作为上下文的条件概率,又假设“吃”这个词在霍夫曼树中是的最右边那一个叶子节点,那么从根节点到到达它就有两个非叶节点,根节点对应的词向量命名为A,根节点的右孩子节点对应的词向量命名为B,另外再假设“大家”、“喜欢”、“好吃”和“的”这四个词的词向量的和为C,则

    其中{
m{sigma }}left( {
m{x}} 
ight) = 1/left( {1 + {e^{ - x}}} 
ight),是sigmoid公式。
    要注意的是,如果“吃”这个词在非叶节点B的左孩子节点(假设称为E)的右边的那个叶子节点,也就是在图中右边的三个叶子的中间那个,则有

    上面的那句话的每个词都计算p({w_i}|Contex{t_i})后连乘起来得到联合概率,这个概率如果大于某个阈值,就认为是正常的话;否则就认为不是自然语言,要排除掉。
    对于这个神经网络的描述索然无味,因为主角也不是这个概率,这个神经网络最重要的是输出层的那个霍夫曼树的叶子节点上的那些向量,那些向量被称为词向量,词向量就是另外一篇博文里面介绍的,是个好东西。
    怎么得到这些词向量更加是一个重要的过程,也是word2vec这整个算法最重要的东西,后面会认真介绍。

    二.优化目标与解问题

    2.1从霍夫曼树到条件概率的计算

    前面已经提过语言模型的目标就是判断一句话是否是正常的,至于怎么判断则需要计算很多条件概率如p({w_i}|Contex{t_i}),然后还要把这些条件概率连乘起来得到联合概率。这样就带来了问题了——怎么去计算p({w_i}|Contex{t_i}),有很多办法的,后面的章节会介绍。这里的word2vec的计算这个条件概率的方法是利用神经网络的能量函数,因为在能量模型中,能量函数的功能是把神经网络的状态转化为概率表示,这在另外一篇博文RBM里面有提到,具体要看hinton的论文来了解了。能量模型有个特别大的好处,就是能拟合所有的指数族的分布。那么,如果认为这些条件概率是符合某个指数族的分布的话,是可以用能量模型去拟合的。总之word2vec就认为p({w_i}|Contex{t_i})这个条件概率可以用能量模型来表示了。
    既然是能量模型,那么就需要能量函数,word2vec定义了一个非常简单的能量函数
    E(A,C)=-(A∙C)
    其中A可以认为是某个词的词向量,C是这个词的上下文的词向量的和(向量的和),基本上就可以认为C代表Context;中间的点号表示两个向量的内积。
    然后根据能量模型(这个模型假设了温度一直是1,所以能量函数没有分母了),就可以表示出词A的在上下文词向量C下的概率来了
    {
m{p}}left( {{
m{A|C}}} 
ight) = frac{{{e^{ - Eleft( {A,C} 
ight)}}}}{{ sum 
olimits_{v = 1}^V {e^{ - Eleft( {{w_v},C} 
ight)}}}}                 (2.1.2)
    其中V表示语料库里面的的词的个数,这个定义的意思是在上下文C出现的情况下,中间这个词是A的概率,为了计算这个概率,肯定得把语料库里面所有的词的能量都算一次,然后再根据词A的能量,那个比值就是出现A的概率。这种计算概率的方式倒是能量模型里面特有的,这个定义在论文《Hierarchical Probabilistic Neural Network Language Model》里面,这里拿来改了个形式。
    这个概率其实并不好统计,为了算一个词的的概率,得算上这种上下文的情况下所有词的能量,然后还计算指数值再加和。
    这时候科学家们的作用又体现了,假如把语料库的所有词分成两类,分别称为G类和H类,每类一半,其中词A属于G类,那么下面的式子就可以成立了
    p(A│C)=p(A|G,C)p(G|C)                        (2.1.3)
    这个式子的的含义算明确的了,词A在上下文C的条件下出现的概率,与后面的这个概率相等——在上下文C的条件下出现了G类词,同时在上下文为C,并且应该出现的词是G类词的条件下,词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)
    其中d是一个映射函数,把Y里面的元素映射到词的类别D里面的元素。还有个证明
    {
m{P}}left( {{
m{Y|X}}} 
ight) =  sum limits_i Pleft( {Y,D = {
m{i|X}}} 
ight) =  sum limits_i Pleft( {Y{
m{|}}D = i,X} 
ight)Pleft( {D = i{
m{|}}X} 
ight) = P(Y|D = dleft( Y 
ight),X)P(D = dleft( Y 
ight)|X)
    式子(2.1.3)说明了一个问题,计算一个词A在上下文C的情况下出现的概率,可以先对语料库中的词分成两簇,然后能节省计算。现在来展示一下怎么节省计算,假设G,H这两类的簇中心也用G和H表示,那么式子(2.3)中的p(G|C)可以用下面的式子计算
    {
m{p}}left( {{
m{G|C}}} 
ight) = frac{{{e^{ - Eleft( {G,C} 
ight)}}}}{{{e^{ - Eleft( {G,C} 
ight)}} + {e^{ - Eleft( {H,C} 
ight)}}}} = frac{1}{{1 + {e^{ - left( { - left( {H - G} 
ight) cdot C} 
ight)}}}} = frac{1}{{1 + {e^{ - Eleft( {H - G,C} 
ight)}}}}
    也就是说,可以不用关系那个是簇中心,只要利用一个F=H-G的类词向量的一个向量就可以计算P(G|C)了,所以这一步是很节省时间的。再看另外一步
    {
m{p}}left( {{
m{A|G}},{
m{C}}} 
ight) = frac{{{e^{ - Eleft( {A,C} 
ight)}}}}{{ sum 
olimits_{W in G} {e^{ - Eleft( {W,C} 
ight)}}}}
    由于在G内的词数量只有V/2个,也就是说计算分母的时候只要计算V/2个词的能量就可以了。这已经省了一半的计算量了,可惜科学家们是贪得无厌的,所以还要继续省,怎么来呢?把G类词再分成两个簇GG,GH,A在GH里面,然后
    p(A│G,C)=p(A|GH,G,C)p(GH|G,C)
    同样有
    {
m{p}}left( {{
m{GH|G}},{
m{C}}} 
ight) = frac{1}{{1 + {e^{ - Eleft( {GG - GH,C} 
ight)}}}}

    {
m{p}}left( {{
m{A|GH}},{
m{G}},{
m{C}}} 
ight) = frac{{{e^{ - Eleft( {A,C} 
ight)}}}}{{ sum 
olimits_{W in GH} {e^{ - Eleft( {W,C} 
ight)}}}}
    同样可以把GG-GH用一个类词向量表达,这时候
    p(A│C)=p(A|GH,G,C)p(GH|G,C)p(G|C)
    继续下去假设继续分到GHG簇的时候只剩两个词了,再分两簇为GHGG和GHGH,其中的簇GHGG就只有一个词A,那么p(A│C)可以用下面的式子算
    p(A│C)=p(A│GHGG,GHG,GH,G,C)p(GHGG|GHG,GH,G,C)p(GHG|GH,G,C)p(GH|G,C)p(G|C)
    其中p(A|GHGG,GHG,GH,G)是1,因为只有一个单词,代到公式(2.2)就可以得到,那么就有
    p(A│C)=p(GHGG|GHG,GH,G,C)p(GHG|GH,G,C)p(GH|G,C)p(G|C)
    也就是
    {
m{p}}left( {{
m{A|C}}} 
ight) = frac{1}{{1 + {e^{ - Eleft( {GHH - GHG,C} 
ight)}}}} cdot frac{1}{{1 + {e^{ - Eleft( {GG - GH,C} 
ight)}}}} cdot frac{1}{{1 + {e^{ - Eleft( {H - G,C} 
ight)}}}}
    假设再令FFF=GHH-GHG,FF=GG-GH,F=H-G,那么p(A|C)只要算这三个词与上下文C的能量函数了,确实比原来的要节省很多计算的。
    对于上面的霍夫曼树来说假设G表示向右,H表示向左,那么A就是从右边开始数的第二个叶子节点,就是图中右边的三个W的中间那个。那么F,FF,FFF就是这个叶子节点路径上的三个非叶节点。
    但是一个词总是会一会向左,一会向右的,也就是在根节点那里,一会是p(G|C)那么F=H-G,一会又是p(H|C)那么F=G-H,如果F在每个节点都是唯一一个值,就可以直接用一次词向量表示这个非叶节点了。这下难不倒科学家的,令F一直是等于H-G,那么一直有
    {
m{p}}left( {{
m{H|C}}} 
ight) = frac{1}{{1 + {e^{ - Eleft( {F,C} 
ight)}}}}
    并且有p(G|C)=1-p(H|C)。
    这样每个非叶节点就可以用唯一一个词向量表示了。
    看到这里,总该明白为啥p(A|C)要这么算了吧。再换种情况,上面的概率这个概率的计算方法是不是也是同样的道理?
    总结下来,p({w_i}|Contex{t_i})可以用下面的公式计算了
    {
m{p}}left( {{
m{w|Context}}} 
ight) =  prod limits_{k = 1}^K p({d_k}|{q_k},C) =  prod limits_{k = 1}^K left( {{{left( {sigma left( {{q_k} cdot C} 
ight)} 
ight)}^{1 - {d_k}}} cdot {{left( {1 - sigma left( {{q_k} cdot C} 
ight)} 
ight)}^{{d_k}}}} 
ight)
    其中C表示上下文的词向量累加后的向量,qk表示从根节点下来到叶子节点的路径上的那些非叶节点,dk就是编码了,也可以说是分类,因为在霍夫曼树的每个非叶节点都只有两个孩子节点,那可以认为当wi在这个节点的左子树的叶子节点上时dk=0,否则dk=1。这样的话每个词都可以用一组霍夫曼编码来表示,就有了上面的那个式子中间的那个dk,整个p(w│Context)就可以用霍夫曼树上的若干个非叶节点和词w的霍夫曼编码来计算了。
    看到这务必想明白,因为开始要讨论怎么训练了。

    2.2目标函数

    假设语料库是有S个句子组成的一个句子序列(顺序不重要),整个语料库有V个词,似然函数就会构建成下面的样子
    {
m{L}}left( {
m{	heta }} 
ight) =  prod limits_j^{
m{S}} left( { prod limits_{{i_j} = 1}^{{T_j}} pleft( {{w_{{i_j}}}{
m{|}}Contex{t_{{i_j}}}} 
ight)} 
ight)                                                         (2.2.1)
    其中T_j表示第j个句子的词个数,极大似然要对整个语料库去做的。对数似然就会是下面的样子
    {
m{l}}left( {
m{	heta }} 
ight) = {
m{logL}}left( {
m{	heta }} 
ight) = frac{1}{V} sum limits_{j = 1}^{
m{S}} left( { sum limits_{{i_j} = 1}^{{T_j}} logpleft( {{w_{{i_j}}}{
m{|}}Contex{t_{{i_j}}}} 
ight)} 
ight)                              (2.2.2)
    如果前面有个1/V,对数似然还有些人称为交叉熵,这个具体也不了解,就不介绍了;不用1/V的话,就是正常的极大似然的样子。
    有意向的同学可以扩展到有文档的样子,这里就不介绍了。
    但是对于word2vec来说,上面的似然函数得改改,变成下面的样子

    其中的Cij表示上下文相加的那个词向量。对数似然就是下面的

    这里就不要1/V了。
    这个看起来应该比较熟悉了,很像二分类的概率输出的逻辑回归——logistic regression模型。没错了,word2vec就是这么考虑的,把在霍夫曼树向左的情况,也就是dk=0的情况认为是正类,向右就认为是负类(这里的正负类只表示两种类别之一)。这样每当出现了一个上下文C和一个词在左子树的情况,就认为得到了一个正类样本,否则就是一个负类样本,每个样本的属于正类的概率都可以用上面的参数算出来,就是sigma left( {{q_{{i_{jk}}}} cdot Contex{t_{{i_j}}}} 
ight),如果是向右的话,就用1 - sigma left( {{q_{{i_{jk}}}} cdot Contex{t_{{i_j}}}} 
ight)计算其概率。注意每个词可以产生多个样本,因为从霍夫曼树的根节点开始,每个叶子节点都产生一个样本,这个样本的label(也就是属于正类或者负类标志)可以用霍夫曼编码来产生,前面说过了,向左的霍夫曼编码dk=0,所以很自然地可以用1-dk表示每个样本label。
    在这里,霍夫曼编码也变成了一个重要的东西了。
    这样就好多了,问题到这也该清楚了,上面那个l(θ)就是对数似然,然后负对数似然f=-l(θ)就是需要最小化的目标函数了。

    2.3解法

    解法选用的是SGD,博文《在线学习算法FTRL》中说过SGD算法的一些情况。具体说来就是对每一个样本都进行迭代,但是每个样本只影响其相关的参数,跟它无关的参数不影响。对于上面来说,第j个样本的第ij个词的负对数似然是

    第j个样本的第ij个词的在遇到第kij个非叶节点时的负对数似然是
    {f_{{k_{ij}}}} =  - left( {1 - {d_{{k_{ij}}}}} 
ight)logsigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight) - {d_{{k_{ij}}}}logleft( {1 - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight)
    计算{f_{{k_{ij}}}}的梯度,注意参数包括{q_{{k_{ij}}}}{C_{{i_j}}},其中{C_{{i_j}}}的梯度是用来计算{w_{{i_j}}}的时候用到。另外需要注意的是logσ(x)的梯度是1-σ(x),log(1-σ(x))的梯度是-σ(x),
    Fqleft( {{q_{{k_{ij}}}}} 
ight) = frac{{partial {f_{{k_{ij}}}}}}{{partial {q_{{k_{ij}}}}}} =  - left( {1 - {d_{{k_{ij}}}}} 
ight) cdot left( {1 - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {C_{{i_j}}} - {d_{{k_{ij}}}} cdot left( { - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {C_{{i_j}}} =  - left( {1 - {d_{{k_{ij}}}} - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {C_{{i_j}}}

    Fcleft( {{q_{{k_{ij}}}}} 
ight) = frac{{partial {f_{{k_{ij}}}}}}{{partial {C_{{i_j}}}}} =  - left( {1 - {d_{{k_{ij}}}}} 
ight) cdot left( {1 - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {q_{{k_{ij}}}} - {d_{{k_{ij}}}} cdot left( { - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {q_{{k_{ij}}}} =  - left( {1 - {d_{{k_{ij}}}} - sigma left( {{q_{{k_{ij}}}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {q_{{k_{ij}}}}
    上面的Fq和Fc只是简写,有了梯度就可以对每个参数进行迭代了
    q_{{k_{ij}}}^{n + 1} = q_{{k_{ij}}}^n - eta Fqleft( {q_{{k_{ij}}}^n} 
ight)
    同时,每个词的词向量也可以进行迭代了
    {
m{w}}_I^{n + 1} = {
m{w}}_I^n - eta  sum limits_{{k_{ij}} = 1}^{{K_{ij}}} Fcleft( {q_{{k_{ij}}}^n} 
ight)
    注意第二个迭代的wI是代表所有的输入词的,也就是假如输入了4个词,这四个词都要根据这个方式进行迭代。第二个迭代式确实不好理解,因为这里的意思所有非叶节点上的对上下文的梯度全部加和就得到了这个词的上下文的梯度,看起来这个就是BP神经网络的误差反向传播。
    论文《Hierarchical Probabilistic Neural Network Language Model》和《Three New Graphical Models for Statistical Language Modelling》中看起来也是这么样的解释,人家都是Context的几个词首尾连接得到的一个向量,对这个长向量有一个梯度,或者一个超大的V*m矩阵(m是词向量的维度),对这个矩阵每个元素有一个梯度,这些梯度自然也包括了输入词的梯度。
    如果有人发现了这个做法的解释请告知。

    2.4代码中的trick

    如前文,c表示左右各取多少个词,代码中c是一个从0到window-1的一个数,是对每个词都随机生成的,二这个window就是用户自己输入的一个变量,默认值是5。代码实际实现的时候是换了一种方法首先生成一个0到window-1的一个数b,然后训练的词(假设是第i个词)的窗口是从第i-window+b个词开始到第i-window+b个词结束。要注意的是每个词的c都不一样的,都是随机生成的。
    如果有人看过代码,就会发现,其中的q_(k_ij )在代码中用矩阵syn1表示,C_(i_j )在代码中用neu1表示。叶子节点里面的每个词向量在代码中用syn0表示,利用下标移动去读取。
    核心代码如下,其中的vocab[word].code[d]就表示d_(k_ij ),其他就是迭代过程,代码是写得相当简洁啊。

    代码中的419行就是计算Cij,425-428行就是计算f,也就是σ(q_(k_ij )∙C_(i_j ) )的值,432行就是累积Cij的误差,434就是更新q_(k_ij)^(n+1)。

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

    三. CBOW加抽样的网络结构与使用说明

    3.1网络结构与使用说明

    网络结构如下

    如(二)中,中间的那个隐层是把上下文累加起来的一个词向量,然后有一个矩阵R,是在训练过程中用到的临时矩阵,这个矩阵连接隐层与所有输出节点,但是这个矩阵在使用这个网络的时候不怎么用得到,这里也是没弄清楚的一个地方。每个输出节点代表一个词向量。
    同样如(二)中的例子,计算这么一个概率,这里的计算方法就简单多了,就是随机从语料库里面抽取c个词,这里假设c=3,抽中了D,E,F这三个词,又假设“吃”这个词的词向量是A,那么就计算“吃”这个词的概率就用下面的公式

    同样如(二)中,那句话的每个词都计算p({w_i}|Contex{t_i})后连乘起来得到联合概率,这个概率如果大于某个阈值,就认为是正常的话;否则就认为不是自然语言,要排除掉。
    这里只是说明这个网络是怎么样的例子,真正重要的始终是那些词向量。

    四.CBOW加抽样的优化目标与解问题

    4.1抽样方法的意义与目标函数

    为啥要抽样呢?目的跟(二)中的霍夫曼树其实是一样的,都是为了节省计算量,这个计算量就是式(2.1.2)中计算 p(A|C)的概率,因为这个概率实在不好算。论文《Distributed Representations of Words and Phrases and their Compositionality》中提到有一个叫NCE的方法可以来代替上面的那个hierarchical softmax方法(就是使用霍夫曼树的方法),但是由于word2vec只关心怎么学到高质量的词向量,所以就用了一种简单的NCE方法,称为NEG,方法的本质就是在第j个句子的第ij个词wij处使用下面的式子代替logpleft( {{w_{{i_j}}}{
m{|}}Contex{t_{{i_j}}}} 
ight)
    {
m{logsigma }}left( {{w_{{i_j}}} cdot {C_{{i_j}}}} 
ight) +  sum limits_{k = 1}^K {E_{{w_k}~{p_V}left( w 
ight)}}{
m{log}}left( {1 - sigma left( {{w_k} cdot {C_{{i_j}}}} 
ight)} 
ight)
    其中E下面的那个下标的意思是wk是符合某个分布的,在这里p_V (w)表示词频的分布。
    这个式子的第二项是求K个期望,这K个期望中的每一个期望,都是在该上下文的情况下不出现这个词的期望。这里出现一个特别大的偷懒,就是认为这个期望只要抽取一个样本就能计算出来,当然,如果说遍历完整个语料库,其实还可以认为是抽取了多次实验的,因为每次抽取词的时候,是按照词频的分布来抽样的,如果抽样的次数足够多,在总体的结果上,这里计算的期望还是接近这个期望的真实值的,这个可以参考博文中RBM中计算梯度的时候的那个蒙特卡洛抽样来理解。
    在这里,从代码上体现来看,就只用一个样本来估算这个期望的,所有式子被简化成了下面的形式
    {
m{logsigma }}left( {{w_{{i_j}}} cdot {C_{{i_j}}}} 
ight) +  sum limits_{k = 1}^K {
m{log}}left( {1 - sigma left( {{w_k} cdot {C_{{i_j}}}} 
ight)} 
ight)
    用这个式子取代(2.2.2)中的logpleft( {{w_{{i_j}}}{
m{|}}Contex{t_{{i_j}}}} 
ight),就能得到CBOW加抽样的目标函数(去掉1/V的),这个目标函数也是极其像logistic regression的目标函数,其中wij是正类样本,wk是负类样本。
    为了统一表示,正类样本设置一个label为1,负类样本设置label为0,每个样本的负对数似然都变成下面的方式
    {f_w} =  - {
m{label}} cdot {
m{logsigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight) - left( {1 - {
m{label}}} 
ight){
m{log}}left( {1 - sigma left( {w cdot {C_{{i_j}}}} 
ight)} 
ight)

    4.2CBOW加抽样方法的解法

    解法还是用SGD,所以对一个词wij来说,这个词本身是一个正类样本,同时对这个词,还随机抽取了k个负类样本,那么每个词在训练的时候都有k+1个样本,所以要做k+1次SGD。
    对于每个样本求两个梯度
    {
m{Fw}}left( {
m{w}} 
ight) = frac{{partial {f_w}}}{{partial w}} =  - {
m{label}} cdot left( {1 - {
m{sigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {C_{{i_j}}} + left( {1 - label} 
ight) cdot {
m{sigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight) cdot {C_{{i_j}}} =  - left( {label - {
m{sigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot {C_{{i_j}}}

    {
m{Fc}}left( {
m{w}} 
ight) = frac{{partial {f_w}}}{{partial {C_{{i_j}}}}} =  - {
m{label}} cdot left( {1 - {
m{sigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot w + left( {1 - label} 
ight) cdot {
m{sigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight) cdot w =  - left( {label - {
m{sigma }}left( {{
m{w}} cdot {C_{{i_j}}}} 
ight)} 
ight) cdot w
    两个梯度都有这么相似的形式实在太好了,然后开始迭代,代码里面有个奇怪的地方,就是一开的网络结构中的那个V*m的矩阵,用来保存的是每次抽中的负类样本的词向量,每一行代表一个词向量,刚好是所有词的词向量。每次抽样抽中一个词后,拿去进行迭代的(就是计算梯度什么的),就是这个矩阵中对应的那个词向量,而且这个矩阵还参与更新,就是第一个梯度实际更新的是这个矩阵里面的词向量。但总的看来,这个矩阵在迭代计算完了就丢弃了,因为最终更新的词向量,每次只有一个,就是公式一直出现的那个词wij,最终输出的,也是输入里面的词向量(在图中是最下面的输出层的那些词向量),当然这个矩阵可以认为是隐藏层跟输出层的连接矩阵。
    {
m{R}}_w^{n + 1} = {
m{R}}_w^n - eta {
m{Fw}}left( {{
m{R}}_w^n} 
ight)
    其中w代表每个样本对应的词向量,包括wij和抽中的词向量,注意更新的是R这个连接矩阵。词向量的更新公式如下
    {
m{w}}_I^{n + 1} = {
m{w}}_I^n - eta left( {Fcleft( {{
m{R}}_{{w_I}}^n} 
ight) +  sum limits_{k = 1}^K Fcleft( {{
m{R}}_{{w_k}}^n} 
ight)} 
ight)
    注意每个梯度的值的计算都是拿连接矩阵R中对应的那一行来算的,这样看起来可以避免每次都更新输出层的词向量,可能是怕搞乱了吧。

    4.3CBOW加抽样方法代码中的trick

    随机数是自己产生的,代码里面自己写了随机数程序。
    每个词都要执行negative次抽样的,如果遇到了当前词(就是label为1的词)就提前退出抽样,这个步骤就提前完成了,这个也是一个比较费解的trick。
    其中前面说的R矩阵在代码中用syn1neg表示,C_(i_j )在代码中用neu1表示,同样的,叶子节点里面的每个词向量在代码中用syn0表示,利用下标移动去读取。

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


    五.Skip-gram加层次的优化目标与解问题

    5.1网络结构与使用说明

    网络结构如下图

    其中的Wi是相应的词,词Wi与huffman树直接连接,没有隐藏层的。使用方法依然与cbow加层次的相似。
    在判断“大家 喜欢 吃 好吃 的 苹果”这句话是否自然语言的时候,是这么来的,同样比如计算到了“吃”这个词,同样随机抽到的c=2,对吃这个词需要计算的东西比较多,总共要计算的概率是p(大家|吃),p(喜欢|吃),p(好吃|吃)和p(的|吃)总共四个,在计算p(大家|吃)这个概率的时候,要用到上面图中的二叉树,假设“大家”这个词在huffman树根节点的右孩子的最左边的那个节点,就是图中右数第三个叶子节点。再假设从根节点开始到这个叶子节点的路径上的三个非叶节点分别为A,B,C(从高到低排列的),“吃”这个词的词向量设为D,那么p(大家|吃)这个概率可以用下面的公式算概率
    p(大家│吃)=(1-σ(A∙D))∙σ(B∙D)∙σ(C∙D)
    同样的方法计算p(喜欢|吃),p(好吃|吃)和p(的|吃),再把这四个概率连乘,得到了“吃”这个词的上下文概率,注意这只是一个词的概率。
    把一整句话的所有词的概率都计算出来后进行连乘,得到的就是这句话是自然语言的概率。这个概率如果大于某个阈值,就认为是正常的话;否则就认为不是自然语言,要排除掉。
    再声明一下,这里只是说明这个网络是怎么样的例子,真正重要的始终是那些词向量。

    5.2目标函数

    假设语料库是有S个句子组成的一个句子序列(顺序不重要),整个语料库有V个词,似然函数就会构建成下面的样子
            (5.2.1)
    其中T_j表示第j个句子的词个数,w_(u_ij+i_j )表示词w_(i_j )左右的各c_ij个词的其中一个,注意c_ij对每个w_(i_j )都不一样的。极大似然要对整个语料库去做的。对数似然就会是下面的样子
          (5.2.2)
    其中的V表示整个语料库的词没有去重的总个数,整个目标函数也可以叫交叉熵,但是这里对这也不感兴趣,一般去掉。
    这就涉及到计算某个词的概率了,如p({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}}),这个概率变了,条件变成输入中要考察的那个词,计算条件概率的词变成了上下文的词。当然,计算方法没有变,前面介绍的huffman树的计算方法到这里是一摸一样的。
    {
m{p}}left( {{
m{w|I}}} 
ight) =  prod limits_{k = 1}^K p({d_k}|{q_k},{
m{I}}) =  prod limits_{k = 1}^K left( {{{left( {sigma left( {{q_k} cdot {
m{I}}} 
ight)} 
ight)}^{1 - {d_k}}} cdot {{left( {1 - sigma left( {{q_k} cdot I} 
ight)} 
ight)}^{{d_k}}}} 
ight)
    其中I表示输入的那个词,也就是(5.1)的例子中的那个词“吃”,那么w就表示例子中的“大家”;qk表示从根节点下来到“大家”这个词所在的叶子节点的路径上的非叶节点,dk就是编码了,也可以说是分类,当w在某个节点如qk的左子树的叶子节点上时dk=0,否则dk=1。
    用这个式子代替掉上面的(5.2.1)中的似然函数中的p({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}}),当然每个变量都对号入座,就能得到总的似然函数了。
    再对这个式子求个对数,得到


    再利用这个式子替换掉(5.2.2)中的{{
m{log}}p({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}})}就能得到总的对数似然函数,也就是目标函数,剩下的就是怎么解了。
    可以注意的是,计算每个词(例中的“吃”)上下文概率的时候都要计算好几个条件概率的(例子中p(大家|吃),p(喜欢|吃),p(好吃|吃)和p(的|吃)),这每个条件概率又是需要在huffman树上走好几个非叶节点的,每走到一个非叶节点,都需要计算一个{left( {1 - {d_k}} 
ight)logsigma left( {{q_k} cdot {
m{I}}} 
ight) + {d_k} cdot left( {1 - sigma left( {{q_k} cdot I} 
ight)} 
ight)}的。可以看到,走到每一个非叶节点,在总的对数似然函数中,都类似logistic regression的一个样本,为了方便描述,就用样本和label的方式在称呼这些东西。
    跟一般的logistic regression一样,每走到一个非叶节点,如果是向左走的,就定义label为1,为正样本;否则label就是0,是负样本。这样label=1-dk,每一个非叶节点都为整个问题产生了一个样本。

    5.3解法

    解法选用的是SGD,在处理每个样本时,对总目标函数的贡献是
    {
m{lf}} = pleft( {{d_k}{
m{|}}{q_k},{
m{I}}} 
ight) =  - left( {1 - {d_k}} 
ight)logsigma left( {{q_k} cdot {
m{I}}} 
ight) - {d_k} cdot left( {1 - sigma left( {{q_k} cdot I} 
ight)} 
ight)
    计算梯度
    Fqleft( {{q_k}} 
ight) = frac{{partial l{
m{f}}}}{{partial {q_k}}} =  - left( {1 - {d_k}} 
ight) cdot left( {1 - sigma left( {{q_k} cdot I} 
ight)} 
ight) cdot I - {d_k} cdot left( { - sigma left( {{q_k} cdot I} 
ight)} 
ight) cdot I =  - left( {1 - {d_k} - sigma left( {{q_k} cdot I} 
ight)} 
ight) cdot I

    Fileft( {{q_k}} 
ight) = frac{{partial l{
m{f}}}}{{partial I}} =  - left( {1 - {d_k}} 
ight) cdot left( {1 - sigma left( {{q_k} cdot I} 
ight)} 
ight) cdot {q_k} - {d_k} cdot left( { - sigma left( {{q_k} cdot I} 
ight)} 
ight) cdot {q_k} =  - left( {1 - {d_k} - sigma left( {{q_k} cdot I} 
ight)} 
ight) cdot {q_k}
    更新
    q_k^{n + 1} = q_k^n - eta {
m{Fq}}left( {q_k^n} 
ight)
    ^{n + 1} = {I^n} - eta {
m{Fi}}left( {q_k^n} 
ight)

    5.4代码中的trick

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

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

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

    六.Skip-gram加抽样的优化目标与解问题

    这个就简单说说吧,不清楚的看上面的。

    6.1网络结构与使用说明

    网络结构如下

    使用说明就不说的,同样是抽样的。
    如(四)中,有一个矩阵R,是在训练过程中用到的临时矩阵,这个矩阵连接隐层与所有输出节点,但是这个矩阵在使用这个网络的时候不怎么用得到,这里也是没弄清楚的一个地方。每个输出节点代表一个词向量。
    同样如(五)中的例子,计算p(大家│吃)这么一个概率,这里的计算方法就简单多了,就是随机从语料库里面抽取c个词,这里假设c=3,抽中了D,E,F这三个词,又假设“吃”这个词的词向量是A,那么就计算“吃”这个词的概率就用下面的公式

    同样如(五)中,那句话的每个词都计算与上下文几个词的概率(p(大家|吃),p(喜欢|吃),p(好吃|吃)和p(的|吃))后连乘起来得到词“吃”的概率,所有词的概率计算出来再连乘就是整句话是自然语言的概率了。剩下的同上。

    6.2目标函数与解法

    似然函数跟(5.2.1)一样的,对数似然函数跟(5.2.2)是一样的,但是计算{
m{logp}}({w_{{u_{ij}} + {i_j}}}|{w_{{i_j}}})也就是logp(w|I)的的时候不一样,论文《Distributed Representations of Words and Phrases and their Compositionality》中认为logp(w|I)可以用下面的式子
    {
m{logsigma }}left( {w cdot I} 
ight) +  sum limits_{k = 1}^K {E_{w~{p_V}left( w 
ight)}}left[ {logleft( {1 - sigma left( {{w_k} cdot {C_{{i_j}}}} 
ight)} 
ight)} 
ight]
    代替。
    同样可以和(四)中一样,设定正负样本的概念。为了统一表示,正类样本设置一个label为1,负类样本设置label为0,每个样本的负对数似然都变成下面的方式
    {f_w} =  - {
m{label}} cdot {
m{logsigma }}left( {{
m{w}} cdot I} 
ight) - left( {1 - {
m{label}}} 
ight){
m{log}}left( {1 - sigma left( {w cdot I} 
ight)} 
ight)
    梯度
    {
m{Fw}}left( {
m{w}} 
ight) = frac{{partial {f_w}}}{{partial w}} =  - {
m{label}} cdot left( {1 - {
m{sigma }}left( {{
m{w}} cdot I} 
ight)} 
ight) cdot {C_{{i_j}}} + left( {1 - label} 
ight) cdot {
m{sigma }}left( {{
m{w}} cdot I} 
ight) cdot I =  - left( {label - {
m{sigma }}left( {{
m{w}} cdot I} 
ight)} 
ight) cdot I

    {
m{Fc}}left( {
m{w}} 
ight) = frac{{partial {f_w}}}{{partial I}} =  - {
m{label}} cdot left( {1 - {
m{sigma }}left( {{
m{w}} cdot I} 
ight)} 
ight) cdot w + left( {1 - label} 
ight) cdot {
m{sigma }}left( {{
m{w}} cdot I} 
ight) cdot w =  - left( {label - {
m{sigma }}left( {{
m{w}} cdot I} 
ight)} 
ight) cdot w
    更新
    {
m{R}}_w^{n + 1} = {
m{R}}_w^n - eta {
m{Fw}}left( {{
m{R}}_w^n} 
ight)
    {{
m{I}}^{n + 1}} = {{
m{I}}^n} - eta left( {Fcleft( {{
m{R}}_{{{
m{I}}^n}}^n} 
ight) +  sum limits_{k = 1}^K Fcleft( {{
m{R}}_{{w_k}}^n} 
ight)} 
ight)

    6.3代码

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

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

    更新输入层还是一样。

    七.一些总结

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

    致谢

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

    参考文献

    [1] http://techblog.youdao.com/?p=915      Deep Learning实战之word2vec,网易有道的pdf
    [2] http://www.zhihu.com/question/21661274/answer/19331979                @杨超在知乎上的问答《Word2Vec的一些理解》
    [3] http://xiaoquanzi.net/?p=156                  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.

  • 相关阅读:
    【解题报告】 洛谷P1663 山
    【解题报告】 洛谷P6733 间歇泉
    【解题报告】 洛谷P1542 包裹快递
    二分总结
    SmartSchool CC校友录V8(毕业入世版)
    Hide/Show running Console
    Calculate drive total/free/available space
    C# list installed softwares
    How to: Modify a Project System So That Projects Load in Multiple Versions of Visual Studio
    PS:WINRAR制作32位安装程序和64位安装程序选项
  • 原文地址:https://www.cnblogs.com/downtjs/p/3784440.html
Copyright © 2020-2023  润新知