word2vec 词向量

摘要:

向量

1.向量概念:
2.向量的相似度计算:

词向量:

1: 词向量 概念?
2: 词向量 有什么用?
3: 词向量维度计算?

向量

1. 向量概念: 在数学中,向量(也称为欧几里得向量、几何向量、矢量),指具有大小(magnitude)和方向的量。它可以形象化地表示为带箭头的线段。箭头所指:代表向量的方向;线段长度:代表向量的大小。与向量对应的只有大小,没有方向的量叫做数量(物理学中称标量)

2.向量的相似度计算:
2.1 皮尔逊相关系数(Pearson Correlation Coefficient)
2.2 欧几里德距离(Euclidean Distance)
2.3 Cosine 相似度(Cosine Similarity)
2.4 Tanimoto 系数(Tanimoto Coefficient)
2.5 曼哈顿距离
2.6 马氏距离
2.7 兰氏距离公式
2.8 切比雪夫距离公式
2.9 余弦距离
余弦距离,也称为余弦相似度,是用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小的度量。
向量,是多维空间中有方向的线段,如果两个向量的方向一致,即夹角接近零,那么这两个向量就相近。而要确定两个向量方向是否一致,这就要用到余弦定理计算向量的夹角。
余弦定理描述了三角形中任何一个夹角和三个边的关系。给定三角形的三条边,可以使用余弦定理求出三角形各个角的角度。假定三角形的三条边为a,b和c,对应的三个角为A,B和C,那么角A的余弦为:

如果将三角形的两边b和c看成是两个向量,则上述公式等价于:

词向量

 

词向量好坏的评价:

当前绝大部分工作(比如以各种方式改进word embedding)都是依赖wordsim353等词汇相似性数据集进行相关性度量,并以之作为评价word embedding质量的标准。然而,这种基于similarity的评价方式对训练数据大小、领域、来源以及词表的选择非常敏感。而且数据集太小,往往并不能充分说明问题。

1: 词向量 概念?
把语言单词嵌入到向量空间中就叫词嵌入(word embedding),是一种词的分布式表示技术。

2: 词向量 有什么用?

3: 现有词的分布式表示技术:
3.0 one-hot形式的词向量
一般就是统计词库包含的所有V个词,然后将这V个词固定好顺序,然后每个词就可以用一个V维的稀疏向量来表示,向量中只有在该词出现的位置的元素才为1,其它元素全为0。比如下面这几个词,第一个元素为1的表示中国,第六个元素为1的表示美国,第五个元素为1的表示日本

中国 [1,0,0,0,0,0,0,0,0,……,0,0,0,0,0,0,0]
美国 [0,0,0,0,0,1,0,0,0,……,0,0,0,0,0,0,0]
日本 [0,0,0,0,1,0,0,0,0,……,0,0,0,0,0,0,0]

 

3.1 分布式表示
分布式词向量(distributed word representation)。 分布式词向量则干脆直接用普通的向量来表示词向量,而元素的值为任意实数,该向量的维数可以在事前确定,一般可以为50维或100维。这时的词向量类似如下(这里假设用5维来表示):

中国 [1.25, 0.2, 0.3, 0.5, 0.6]
美国 [0.1, 0.3, 0.5, 0.1, 1.5]
日本 [2.2, 0.2, 0.4, 0.6, 1.0]

3.1.1 基于矩阵的分布表示
3.1.2 基于聚类的分布式表示(分布聚类)
3.1.3 基于神经网路的分布式表示(词向量)
3.2 神经网路词向量表示技术
经网络语言模型即用神经网络来训练语言模型,最经典的模型是Bengio等人提出的三层神经网络,它思路大概是对于语料C任意一个词w,取它的前n-1个词作为输入,这个跟n-gram的思路是同样的,而w则为它的输出,有了输入和输出就组成了训练样本了。

3.2.1 神经网路语言模型 NNLM
3.2.2 log 双线性语言模型 LBL
3.2.3 循环神经网络语言模型 RNNLM
3.2.4 C&W 模型
3.2.5 CBOW模型 和 Skip-gram 模型
3.2.6 Order 模型

 

 

Statistical Language Model

在深入word2vec算法的细节之前,我们首先回顾一下自然语言处理中的一个基本问题:如何计算一段文本序列在某种语言下出现的概率?之所为称其为一个基本问题,是因为它在很多NLP任务中都扮演着重要的角色。例如,在机器翻译的问题中,如果我们知道了目标语言中每句话的概率,就可以从候选集合中挑选出最合理的句子做为翻译结果返回。

统计语言模型给出了这一类问题的一个基本解决框架。对于一段文本序列S=w1,w2,...,wTS=w1,w2,…,wT,它的概率可以表示为:

即将序列的联合概率转化为一系列条件概率的乘积。问题变成了如何去预测这些给定previous words下的条件概率p(wt|w1,w2,...,wt1)

由于其巨大的参数空间,这样一个原始的模型在实际中并没有什么卵用。我们更多的是采用其简化版本——Ngram模型:

常见的如bigram模型(N=2N=2)和trigram模型(N=3N=3)。事实上,由于模型复杂度和预测精度的限制,我们很少会考虑N>3N>3的模型。

我们可以用最大似然法去求解Ngram模型的参数——等价于去统计每个Ngram的条件词频。

为了避免统计中出现的零概率问题(一段从未在训练集中出现过的Ngram片段会使得整个序列的概率为00),人们基于原始的Ngram模型进一步发展出了back-off trigram模型(用低阶的bigram和unigram代替零概率的trigram)和interpolated trigram模型(将条件概率表示为unigram、bigram、trigram三者的线性函数)。此处不再赘述。感兴趣者可进一步阅读相关的文献[3]。

Distributed Representation

不过,Ngram模型仍有其局限性。首先,由于参数空间的爆炸式增长,它无法处理更长程的context(N>3N>3)。其次,它没有考虑词与词之间内在的联系性。例如,考虑”the cat is walking in the bedroom”这句话。如果我们在训练语料中看到了很多类似“the dog is walking in the bedroom”或是“the cat is running in the bedroom”这样的句子,那么,即使我们没有见过这句话,也可以从“cat”和“dog”(“walking”和“running”)之间的相似性,推测出这句话的概率[3]。然而, Ngram模型做不到。

这是因为,Ngram本质上是将词当做一个个孤立的原子单元(atomic unit)去处理的。这种处理方式对应到数学上的形式是一个个离散的one-hot向量(除了一个词典索引的下标对应的方向上是11,其余方向上都是00)。例如,对于一个大小为55的词典:{“I”, “love”, “nature”, “luaguage”, “processing”},“nature”对应的one-hot向量为:[0,0,1,0,0][0,0,1,0,0]。显然,one-hot向量的维度等于词典的大小。这在动辄上万甚至百万词典的实际应用中,面临着巨大的维度灾难问题(the curse of dimensionality)

于是,人们就自然而然地想到,能否用一个连续的稠密向量去刻画一个word的特征呢?这样,我们不仅可以直接刻画词与词之间的相似度,还可以建立一个从向量到概率的平滑函数模型,使得相似的词向量可以映射到相近的概率空间上。这个稠密连续向量也被称为word的distributed representation[3]。

事实上,这个概念在信息检索(Information Retrieval)领域早就已经被广泛地使用了。只不过,在IR领域里,这个概念被称为向量空间模型(Vector Space Model,以下简称VSM)。

VSM是基于一种Statistical Semantics Hypothesis[4]:语言的统计特征隐藏着语义的信息(Statistical pattern of human word usage can be used to figure out what people mean)。例如,两篇具有相似词分布的文档可以被认为是有着相近的主题。这个Hypothesis有很多衍生版本。其中,比较广为人知的两个版本是Bag of Words Hypothesis和Distributional Hypothesis。前者是说,一篇文档的词频(而不是词序)代表了文档的主题;后者是说,上下文环境相似的两个词有着相近的语义。后面我们会看到,word2vec算法也是基于Distributional的假设。

那么,VSM是如何将稀疏离散的one-hot词向量映射为稠密连续的distributional representation的呢?

简单来说,基于Bag of Words Hypothesis,我们可以构造一个term-document矩阵AA:矩阵的行Ai,:Ai,:对应着词典里的一个word;矩阵的列A:,jA:,j对应着训练语料里的一篇文档;矩阵里的元素AijAij代表着word wiwi在文档DjDj中出现的次数(或频率)。那么,我们就可以提取行向量做为word的语义向量(不过,在实际应用中,我们更多的是用列向量做为文档的主题向量)。

类似地,我们可以基于Distributional Hypothesis构造一个word-context的矩阵。此时,矩阵的列变成了context里的word,矩阵的元素也变成了一个context窗口里word的共现次数。

注意,这两类矩阵的行向量所计算的相似度有着细微的差异:term-document矩阵会给经常出现在同一篇document里的两个word赋予更高的相似度;而word-context矩阵会给那些有着相同context的两个word赋予更高的相似度。后者相对于前者是一种更高阶的相似度,因此在传统的信息检索领域中得到了更加广泛的应用。

不过,这种co-occurrence矩阵仍然存在着数据稀疏性和维度灾难的问题。为此,人们提出了一系列对矩阵进行降维的方法(如LSI/LSA等)。这些方法大都是基于SVD的思想,将原始的稀疏矩阵分解为两个低秩矩阵乘积的形式。

关于VSM更多的介绍,可以进一步阅读文末的参考文献[4]。

Neural Network Language Model

接下来,让我们回到对统计语言模型的讨论。鉴于Ngram等模型的不足,2003年,Bengio等人发表了一篇开创性的文章:A neural probabilistic language model[3]。在这篇文章里,他们总结出了一套用神经网络建立统计语言模型的框架(Neural Network Language Model,以下简称NNLM),并首次提出了word embedding的概念(虽然没有叫这个名字),从而奠定了包括word2vec在内后续研究word representation learning的基础。

NNLM模型的基本思想可以概括如下:

  1. 假定词表中的每一个word都对应着一个连续的特征向量;
  2. 假定一个连续平滑的概率模型,输入一段词向量的序列,可以输出这段序列的联合概率;
  3. 同时学习词向量的权重和概率模型里的参数。

值得注意的一点是,这里的词向量也是要学习的参数。

在03年的论文里,Bengio等人采用了一个简单的前向反馈神经网络f(wtn+1,...,wt)f(wt−n+1,…,wt)来拟合一个词序列的条件概率p(wt|w1,w2,...,wt1)p(wt|w1,w2,…,wt−1)。整个模型的网络结构见下图:

我们可以将整个模型拆分成两部分加以理解:

  1. 首先是一个线性的embedding层。它将输入的N1N−1个one-hot词向量,通过一个共享的D×VD×V的矩阵CC,映射为N1N−1个分布式的词向量(distributed vector)。其中,VV是词典的大小,DD是embedding向量的维度(一个先验参数)。CC矩阵里存储了要学习的word vector。
  2. 其次是一个简单的前向反馈神经网络gg。它由一个tanh隐层和一个softmax输出层组成。通过将embedding层输出的N1N−1个词向量映射为一个长度为VV的概率分布向量,从而对词典中的word在输入context下的条件概率做出预估:

我们可以通过最小化一个cross-entropy的正则化损失函数来调整模型的参数θθ

其中,模型的参数θθ包括了embedding层矩阵CC的元素,和前向反馈神经网络模型gg里的权重。这是一个巨大的参数空间。不过,在用SGD学习更新模型的参数时,并不是所有的参数都需要调整(例如未在输入的context中出现的词对应的词向量)。计算的瓶颈主要是在softmax层的归一化函数上(需要对词典中所有的word计算一遍条件概率)。

然而,抛却复杂的参数空间,我们不禁要问,为什么这样一个简单的模型会取得巨大的成功呢?

仔细观察这个模型就会发现,它其实在同时解决两个问题:一个是统计语言模型里关注的条件概率p(wt|context)p(wt|context)的计算;一个是向量空间模型里关注的词向量的表达。而这两个问题本质上并不独立。通过引入连续的词向量和平滑的概率模型,我们就可以在一个连续空间里对序列概率进行建模,从而从根本上缓解数据稀疏性和维度灾难的问题。另一方面,以条件概率p(wt|context)p(wt|context)为学习目标去更新词向量的权重,具有更强的导向性,同时也与VSM里的Distributional Hypothesis不谋而合。

CBoW & Skip-gram Model

铺垫了这么多,终于要轮到主角出场了。

不过在主角正式登场前,我们先看一下NNLM存在的几个问题。

一个问题是,同Ngram模型一样,NNLM模型只能处理定长的序列。在03年的论文里,Bengio等人将模型能够一次处理的序列长度NN提高到了55,虽然相比bigram和trigram已经是很大的提升,但依然缺少灵活性。

因此,Mikolov等人在2010年提出了一种RNNLM模型[7],用递归神经网络代替原始模型里的前向反馈神经网络,并将embedding层与RNN里的隐藏层合并,从而解决了变长序列的问题。

另一个问题就比较严重了。NNLM的训练太慢了。即便是在百万量级的数据集上,即便是借助了40个CPU进行训练,NNLM也需要耗时数周才能给出一个稍微靠谱的解来。显然,对于现在动辄上千万甚至上亿的真实语料库,训练一个NNLM模型几乎是一个impossible mission。

这时候,还是那个Mikolov站了出来。他注意到,原始的NNLM模型的训练其实可以拆分成两个步骤:

  1. 用一个简单模型训练出连续的词向量;
  2. 基于词向量的表达,训练一个连续的Ngram神经网络模型。
    而NNLM模型的计算瓶颈主要是在第二步。

如果我们只是想得到word的连续特征向量,是不是可以对第二步里的神经网络模型进行简化呢?

Mikolov是这么想的,也是这么做的。他在2013年一口气推出了两篇paper,并开源了一款计算词向量的工具——至此,word2vec横空出世,主角闪亮登场。

下面,我将带领大家简单剖析下word2vec算法的原理。有了前文的基础,理解word2vec算法就变得很简单了。

首先,我们对原始的NNLM模型做如下改造:

  1. 移除前向反馈神经网络中非线性的hidden layer,直接将中间层的embedding layer与输出层的softmax layer连接;
  2. 忽略上下文环境的序列信息:输入的所有词向量均汇总到同一个embedding layer;
  3. 将future words纳入上下文环境

得到的模型称之为CBoW模型(Continuous Bag-of-Words Model),也是word2vec算法的第一个模型:

从数学上看,CBoW模型等价于一个词袋模型的向量乘以一个embedding矩阵,从而得到一个连续的embedding向量。这也是CBoW模型名称的由来。

 

那么霍夫曼树有什么好处呢?一般得到霍夫曼树后我们会对叶子节点进行霍夫曼编码,由于权重高的叶子节点越靠近根节点,而权重低的叶子节点会远离根节点,这样我们的高权重节点编码值较短,而低权重值编码值较长。这保证的树的带权路径最短,也符合我们的信息论,即我们希望越常用的词拥有更短的编码。如何编码呢?一般对于一个霍夫曼树的节点(根节点除外),可以约定左子树编码为0,右子树编码为1.如上图,则可以得到c的编码是00。

在word2vec中,约定编码方式和上面的例子相反,即约定左子树编码为1,右子树编码为0,同时约定左子树的权重不小于右子树的权重。

 

word2vec也使用了CBOW与Skip-Gram来训练模型与得到词向量,但是并没有使用传统的DNN模型。最先优化使用的数据结构是用霍夫曼树来代替隐藏层和输出层的神经元,霍夫曼树的叶子节点起到输出层神经元的作用,叶子节点的个数即为词汇表的小大。 而内部节点则起到隐藏层神经元的作用。

具体如何用霍夫曼树来进行CBOW和Skip-Gram的训练我们在下一节讲,这里我们先复习下霍夫曼树。

霍夫曼树的建立其实并不难,过程如下:

输入:权值为(w1,w2,...wn)(w1,w2,…wn)nn个节点

输出:对应的霍夫曼树

1)将(w1,w2,...wn)(w1,w2,…wn)看做是有nn棵树的森林,每个树仅有一个节点。

2)在森林中选择根节点权值最小的两棵树进行合并,得到一个新的树,这两颗树分布作为新树的左右子树。新树的根节点权重为左右子树的根节点权重之和。

3) 将之前的根节点权值最小的两棵树从森林删除,并把新树加入森林。

4)重复步骤2)和3)直到森林里只有一棵树为止。

下面我们用一个具体的例子来说明霍夫曼树建立的过程,我们有(a,b,c,d,e,f)共6个节点,节点的权值分布是(16,4,8,6,20,3)。

首先是最小的b和f合并,得到的新树根节点权重是7.此时森林里5棵树,根节点权重分别是16,8,6,20,7。此时根节点权重最小的6,7合并,得到新子树,依次类推,最终得到下面的霍夫曼树。

—–

 

CBoW模型依然是从context对target word的预测中学习到词向量的表达。反过来,我们能否从target word对context的预测中学习到word vector呢?答案显然是可以的:

这个模型被称为Skip-gram模型(名称源于该模型在训练时会对上下文环境里的word进行采样)。

如果将Skip-gram模型的前向计算过程写成数学形式,我们得到:

其中,ViVi是embedding层矩阵里的列向量,也被称为wiwi的input vector。UjUj是softmax层矩阵里的行向量,也被称为wjwj的output vector。

因此,Skip-gram模型的本质是计算输入word的input vector与目标word的output vector之间的余弦相似度,并进行softmax归一化。我们要学习的模型参数正是这两类词向量。

然而,直接对词典里的VV个词计算相似度并归一化,显然是一件极其耗时的impossible mission。为此,Mikolov引入了两种优化算法:层次Softmax(Hierarchical Softmax)和负采样(Negative Sampling)。

Hierarchical Softmax[5]

层次Softmax的方法最早由Bengio在05年引入到语言模型中。它的基本思想是将复杂的归一化概率分解为一系列条件概率乘积的形式:

其中,每一层条件概率对应一个二分类问题,可以通过一个简单的逻辑回归函数去拟合。这样,我们将对VV个词的概率归一化问题,转化成了对logVlog⁡V个词的概率拟合问题。

我们可以通过构造一颗分类二叉树来直观地理解这个过程。首先,我们将原始字典DD划分为两个子集D1D1D2D2,并假设在给定context下,target word属于子集D1D1的概率p(wtD1|context)p(wt∈D1|context)服从logistical function的形式:

其中,UDrootUDrootVwtVwt都是模型的参数。

接下来,我们可以对子集D1D1D2D2进一步划分。重复这一过程,直到集合里只剩下一个word。这样,我们就将原始大小为VV的字典DD转换成了一颗深度为logVlog⁡V的二叉树。树的叶子节点与原始字典里的word一一对应;非叶节点则对应着某一类word的集合。显然,从根节点出发到任意一个叶子节点都只有一条唯一路径——这条路径也编码了这个叶子节点所属的类别。

同时,从根节点出发到叶子节点也是一个随机游走的过程。因此,我们可以基于这颗二叉树对叶子节点出现的似然概率进行计算。例如,对于训练样本里的一个target word wtwt,假设其对应的二叉树编码为{1,0,1,...,1}{1,0,1,…,1},则我们构造的似然函数为:

乘积中的每一项都是一个逻辑回归的函数。

我们可以通过最大化这个似然函数来求解二叉树上的参数——非叶节点上的向量,用来计算游走到某一个子节点的概率。

层次Softmax是一个很巧妙的模型。它通过构造一颗二叉树,将目标概率的计算复杂度从最初的VV降低到了logVlog⁡V的量级。不过付出的代价是人为增强了词与词之间的耦合性。例如,一个word出现的条件概率的变化,会影响到其路径上所有非叶节点的概率变化,间接地对其他word出现的条件概率带来不同程度的影响。因此,构造一颗有意义的二叉树就显得十分重要。实践证明,在实际的应用中,基于Huffman编码的二叉树可以满足大部分应用场景的需求。

Negative Sampling[6]

负采样的思想最初来源于一种叫做Noise-Contrastive Estimation的算法[6],原本是为了解决那些无法归一化的概率模型的参数预估问题。与改造模型输出概率的层次Softmax算法不同,NCE算法改造的是模型的似然函数。

以Skip-gram模型为例,其原始的似然函数对应着一个Multinomial的分布。在用最大似然法求解这个似然函数时,我们得到一个cross-entropy的损失函数:

式中的p(wt+j|wt)p(wt+j|wt)是一个在整个字典上归一化了的概率。

而在NCE算法中,我们构造了这样一个问题:对于一组训练样本<context, word=””>,我们想知道,target word的出现,是来自于context的驱动,还是一个事先假定的背景噪声的驱动?显然,我们可以用一个逻辑回归的函数来回答这个问题:
</context,>

这个式子给出了一个target word ww来自于context驱动的概率。其中,kk是一个先验参数,表明噪声的采样频率。p(w|context)p(w|context)是一个非归一化的概率分布,这里采用softmax归一化函数中的分子部分。pn(w)pn(w)则是背景噪声的词分布。通常采用word的unigram分布。

通过对噪声分布的kk采样,我们得到一个新的数据集:<context, word,=”” label=””>。其中,label标记了数据的来源(真实数据分布还是背景噪声分布?)。在这个新的数据集上,我们就可以用最大化上式中逻辑回归的似然函数来求解模型的参数。</context,>

而Mikolov在2013年的论文里提出的负采样算法, 是NCE的一个简化版本。在这个算法里,Mikolov抛弃了NCE似然函数中对噪声分布的依赖,直接用原始softmax函数里的分子定义了逻辑回归的函数,进一步简化了计算:

此时,模型相应的目标函数变为:

除了这里介绍的层次Softmax和负采样的优化算法,Mikolov在13年的论文里还介绍了另一个trick:下采样(subsampling)。其基本思想是在训练时依概率随机丢弃掉那些高频的词:

其中,tt是一个先验参数,一般取为10510−5f(w)f(w)ww在语料中出现的频率。

实验证明,这种下采样技术可以显著提高低频词的词向量的准确度。

Beyond the Word Vector

介绍完word2vec模型的算法和原理,我们来讨论一些轻松点的话题——模型的应用。

13年word2vec模型横空出世后,人们最津津乐道的是它学到的向量在语义和语法相似性上的应用——尤其是这种相似性居然对数学上的加减操作有意义[8]!最经典的一个例子是,v(King)v(Man)+v(Woman)=v(Queen)v(“King”)−v(“Man”)+v(“Woman”)=v(“Queen”)。然而,这种例子似乎并没有太多实际的用途。

除此之外,word2vec模型还被应用于机器翻译和推荐系统领域。

Machine Translation[9]

与后来提出的在sentence level上进行机器翻译的RNN模型不同,word2vec模型主要是用于词粒度上的机器翻译。

具体来说,我们首先从大量的单语种语料中学习到每种语言的word2vec表达,再借助一个小的双语语料库学习到两种语言word2vec表达的线性映射关系WW。构造的损失函数为:

在翻译的过程中,我们首先将源语言的word2vec向量通过矩阵WW映射到目标语言的向量空间上;再在目标语言的向量空间中找出与投影向量距离最近的word做为翻译的结果返回。

其原理是,不同语言学习到的word2vec向量空间在几何上具有一定的同构性。映射矩阵WW本质上是一种空间对齐的线性变换。

Item2Vec[11]

本质上,word2vec模型是在word-context的co-occurrence矩阵基础上建立起来的。因此,任何基于co-occurrence矩阵的算法模型,都可以套用word2vec算法的思路加以改进。

比如,推荐系统领域的协同过滤算法。

协同过滤算法是建立在一个user-item的co-occurrence矩阵的基础上,通过行向量或列向量的相似性进行推荐。如果我们将同一个user购买的item视为一个context,就可以建立一个item-context的矩阵。进一步的,可以在这个矩阵上借鉴CBoW模型或Skip-gram模型计算出item的向量表达,在更高阶上计算item间的相似度。

关于word2vec更多应用的介绍,可以进一步参考这篇文献[10]。

Word Embedding

最后,我想简单阐述下我对word embedding的几点思考。不一定正确,也欢迎大家提出不同的意见。

Word embedding最早出现于Bengio在03年发表的开创性文章中[3]。通过嵌入一个线性的投影矩阵(projection matrix),将原始的one-hot向量映射为一个稠密的连续向量,并通过一个语言模型的任务去学习这个向量的权重。这一思想后来被广泛应用于包括word2vec在内的各种NLP模型中。

Word embedding的训练方法大致可以分为两类:一类是无监督或弱监督的预训练;一类是端对端(end to end)的有监督训练。

无监督或弱监督的预训练以word2vec和auto-encoder为代表。这一类模型的特点是,不需要大量的人工标记样本就可以得到质量还不错的embedding向量。不过因为缺少了任务导向,可能和我们要解决的问题还有一定的距离。因此,我们往往会在得到预训练的embedding向量后,用少量人工标注的样本去fine-tune整个模型。

相比之下,端对端的有监督模型在最近几年里越来越受到人们的关注。与无监督模型相比,端对端的模型在结构上往往更加复杂。同时,也因为有着明确的任务导向,端对端模型学习到的embedding向量也往往更加准确。例如,通过一个embedding层和若干个卷积层连接而成的深度神经网络以实现对句子的情感分类,可以学习到语义更丰富的词向量表达。

Word embedding的另一个研究方向是在更高层次上对sentence的embedding向量进行建模。

我们知道,word是sentence的基本组成单位。一个最简单也是最直接得到sentence embedding的方法是将组成sentence的所有word的embedding向量全部加起来——类似于CBoW模型。

显然,这种简单粗暴的方法会丢失很多信息。

另一种方法借鉴了word2vec的思想——将sentence或是paragraph视为一个特殊的word,然后用CBoW模型或是Skip-gram进行训练[12]。这种方法的问题在于,对于一篇新文章,总是需要重新训练一个新的sentence2vec。此外,同word2vec一样,这个模型缺少有监督的训练导向。

个人感觉比较靠谱的是第三种方法——基于word embedding的端对端的训练。Sentence本质上是word的序列。因此,在word embedding的基础上,我们可以连接多个RNN模型或是卷积神经网络,对word embedding序列进行编码,从而得到sentence embedding。

这方面的工作已有很多。有机会,我会再写一篇关于sentence embedding的综述。

 

 

任何一门语言,都是由一堆的词组成,所有的词,构成了一个词汇表。词汇表,可以用一个长长的向量来表示。词的个数,就是词汇表向量的维度。那么,任何一个词,都可以表示成一个向量,词在词汇表中出现的位置设为1,其它的位置设为0。但是这种词向量的表示,词和词之间没有交集,用处不大。

Word2Vec 的训练模型,看穿了,是具有一个隐含层的神经元网络(如下图)。它的输入是词汇表向量,当看到一个训练样本时,对于样本中的每一个词,就把相应的在词汇表中出现的位置的值置为1,否则置为0。它的输出也是词汇表向量,对于训练样本的标签中的每一个词,就把相应的在词汇表中出现的位置的值置为1,否则置为0。那么,对所有的样本,训练这个神经元网络。收敛之后,将从输入层到隐含层的那些权重,作为每一个词汇表中的词的向量。比如,第一个词的向量是(w1,1 w1,2 w1,3 … w1,m),m是表示向量的维度。所有虚框中的权重就是所有词的向量的值。有了每个词的有限维度的向量,就可以用到其它的应用中,因为它们就像图像,有了有限维度的统一意义的输入。

训练 Word2Vec 的思想,是利用一个词和它在文本中的上下文的词,这样就省去了人工去标注。论文中给出了 Word2Vec 的两种训练模型,CBOW (Continuous Bag-of-Words Model) 和 Skip-gram (Continuous Skip-gram Model)。

首先看CBOW,它的做法是,将一个词所在的上下文中的词作为输入,而那个词本身作为输出,也就是说,看到一个上下文,希望大概能猜出这个词和它的意思。通过在一个大的语料库训练,得到一个从输入层到隐含层的权重模型。如下图所示,第l个词的上下文词是i,j,k,那么i,j,k作为输入,它们所在的词汇表中的位置的值置为1。然后,输出是l,把它所在的词汇表中的位置的值置为1。训练完成后,就得到了每个词到隐含层的每个维度的权重,就是每个词的向量。

Word2Vec 代码库中关于CBOW训练的代码,其实就是神经元网路的标准反向传播算法。

接着,看看Skip-gram,它的做法是,将一个词所在的上下文中的词作为输出,而那个词本身作为输入,也就是说,给出一个词,希望预测可能出现的上下文的词。通过在一个大的语料库训练,得到一个从输入层到隐含层的权重模型。如下图所示,第l个词的上下文词是i,j,k,那么i,j,k作为输出,它们所在的词汇表中的位置的值置为1。然后,输入是l,把它所在的词汇表中的位置的值置为1。训练完成后,就得到了每个词到隐含层的每个维度的权重,就是每个词的向量。

Word2Vec 代码库中关于Skip-gram训练的代码,其实就是神经元网路的标准反向传播算法。

一个人读书时,如果遇到了生僻的词,一般能根据上下文大概猜出生僻词的意思,而 Word2Vec 正是很好的捕捉了这种人类的行为,利用神经元网络模型,发现了自然语言处理的一颗原子弹。

 

算法跟哈夫曼树有关系。哈夫曼树可以比较准确的表达这边文章的结构。

a,b,c,d分别表示不同词,并附加找个词出现的频率,这些词就能有自己的路径和编码。

关于哈夫曼树就不仔细详细说明了,他是一种压缩算法,能很好的保持一篇文章的特性。

 

 

1 word2vec 是word embedding 最好的工具吗?

word2vec并非是效果最好的word embedding 工具。最容易看出的就是word2vec没有考虑语序,这里会有训练效果损失。

由于 word2vec 训练速度快 ,易用,google出品 等,使得word2vec使用的人多。

训练快是因为 word2vec只有输入层和输出层,砍去了神经网络中,隐藏层的耗时计算(所以word2vec并不算是一个深度学习算法)。另外,阅读word2vec的google的源码,会发现里面有一些提速的trick。如 sigmod函数,采用一次计算,以后查表,减去了大量的重复计算。如词典hash存储, 层次softmax等。

易用是因为word2vec 公布了word2vec的代码。在tensorflow,gensim,spark mllib包中都有集成,使用方便。

2 word2vec 训练结果的差异主要来自什么因素?

2.1 语料影响最大

语料的场景,比如微博的语料和新闻语料训练的结果差别很大。因为微博属于个人发帖,比较随意。而新闻比较官方正式,另外新闻句式相对复杂。经过训练对比:微博这种短文,训练的相似词更多是同级别的相关词。比如 深圳 相关的是 广州 。而用新闻语料,训练得到 深圳 相关的词 更多是与 深圳 有关联的词,比如 深圳大学。

为什么会出现这种情况呢?

因为 word2vec 的原理就是 一个词预测 前后词 或者 前后词 预测 当前词,使得概率最大化。

这就导致如下两个结果:

2.1.1 相似的句子,相同部位的词 会相似。

比如 句子1 w1 w2 w3 w4 X w5 w6 w7.

句子2 w1 w2 w3 w5 Y w5 w6 w7.

因为 X 的向量 受 w1 w2 w3 w4 w5 w6 w7 向量影响决定, Y也是受这几个词影响决定。

所以 X Y 是相似的。

2.1.2 挨着近的词,也是相似的。

比如 句子 w1 w2 w3 w4 X Y w5 w6 w7. 这样 X Y 都是受到 来自 w1 w2 w3 w4 w5 w6 w7 向量影响决定。

所以X Y是相似的。

所以,微博和新闻的句子的整体分布是不一样的。 这里影响 结论 2.1.1.

其次,新闻长文多,句式复杂,微博短文多,这里影响结论2.1.2.

2.2 算法参数的影响。

算法参数对总体效果影响不大。相对来说,比较重要的参数有以下:

2.2.1 负采样

负采样越低,对高频词越不利,对低频词有利。可以这么理解,本来高频词 词被迭代50次,低频词迭代10次,如果采样频率降低一半,高频词失去了25次迭代,而低频词只失去了5次。一般设置成le-5

2.2. 2 语言模型

skip-gram 和cbow,之前有对比,切词效果偏重各不相同。 从效果来看,感觉cbow对词频低的词更有利。这是因为 cbow是基于周围词来预测某个词,虽然这个词词频低,但是他是基于 周围词训练的基础上,通过算法来得到这个词的向量。通过周围词的影响,周围词训练的充分,这个词就会收益。

2.2. 3 窗口大小

窗口大小影响 词 和前后多少个词的关系,和语料中语句长度有关,建议可以统计一下语料中,句子长度的分布,再来设置window大小。一般设置成8。

2.2. 4 min-count

最小词频训练阀值,这个根据训练语料大小设置,只有词频超过这个阀值的词才能被训练。

根据经验,如果切词效果不好,会切错一些词,比如 “在深圳”,毕竟切错的是少数情况,使得这种错词词频不高,可以通过设置相对大一点的 min-count 过滤掉切错的词。

2.2. 5 向量维度

如果词量大,训练得到的词向量还要做语义层面的叠加,比如 句子 的向量表示 用 词的向量叠加,为了有区分度,语义空间应该要设置大一些,所以维度要偏大。一般 情况下200维够用。

2.2. 6 其他参数

比如学习率 可以根据需要调。

3 word2vec 影响速度的因素有哪些?

3.1 语言模型:cbow 比skip-gram 更快

为什么 cbow更快,很重要的一个原因,cbow是基于周围词来预测这个单词本身 。而skip-gram是基于本身词去预测周围词。 那么,cbow只要 把窗口内的其他词相加一次作为输入来预测 一个单词。不管窗口多大,只需要一次运算。而skip-gram直接受窗口影响,窗口越大,需要预测的周围词越多。在训练中,通过调整窗口大小明显感觉到训练速度受到很大影响。

3.2 迭代次数

影响训练次数,语料不够的情况下,可以调大迭代次数。spark 版本有bug,迭代次数超过1,训练得到的词向量维度值超大。

3.3 线程数

单机版(google word2vec)可以通过设置多线程跑,集群版(spark mllib)可以设置多个 partitions.但是从经验来看,在集群上设置partitions 过多,会影响训练的效果。

3.4 其他参数

采样频率 影响词的训练频率 min-count 最小词频 影响 训练词的数量 Window大小 影响 skip-gram 的 预测次数。 向量维度 维度决定了训练过程中计算的维度

4 怎样评估word2vec训练的好坏?

4.1 词聚类

可以采用 kmeans 聚类,看聚类簇的分布

4.2 词cos 相关性

查找cos相近的词

4.3 Analogy对比

a:b 与 c:d的cos距离 (man-king woman-queen )

4.4 使用tnse,pca等降维可视化展示

词的分布,推荐用google的tensorboard,可以多视角查看,如果不想搭建服务,直接访问这里。另外可以用python的matplotlib。

4.5 Categorization 分类 看词在每个分类中的概率

动物

食物

汽车

电子

橘子

0.11

0.68

0.12

0.11

0.66

0.11

0.13

0.11

雅阁

0.14

0.23

0.67

0.11

苹果

0.11

0.65

0.11

0.65

前三条来自官网的评测方法

网上也有相关的word embedding 的评估方法,可以参考这里

 

 

CBOW 是 Continuous Bag-of-Words Model 的缩写,是一种根据上下文的词语预测当前词语的出现概率的模型。其图示如上图左。

CBOW是已知上下文,估算当前词语的语言模型。其学习目标是最大化对数似然函数:

屏幕快照 2016-07-14 下午7.45.11.png

其中,w表示语料库C中任意一个词。从上图可以看出,对于CBOW,

输入层是上下文的词语的词向量(什么!我们不是在训练词向量吗?不不不,我们是在训练CBOW模型,词向量只是个副产品,确切来说,是CBOW模型的一个参数。训练开始的时候,词向量是个随机值,随着训练的进行不断被更新)。

投影层对其求和,所谓求和,就是简单的向量加法。

输出层输出最可能的w。由于语料库中词汇量是固定的|C|个,所以上述过程其实可以看做一个多分类问题。给定特征,从|C|个分类中挑一个。

对于神经网络模型多分类,最朴素的做法是softmax回归:

神经网络依存句法分析29.png

softmax回归需要对语料库中每个词语(类)都计算一遍输出概率并进行归一化,在几十万词汇量的语料上无疑是令人头疼的。

不用softmax怎么样?比如SVM中的多分类,我们都知道其多分类是由二分类组合而来的:

svm_多分类.gif

这是一种二叉树结构,应用到word2vec中被作者称为Hierarchical Softmax:

屏幕快照 2016-07-17 上午9.13.40.png

上图输出层的树形结构即为Hierarchical Softmax。

非叶子节点相当于一个神经元(感知机,我认为逻辑斯谛回归就是感知机的输出代入f(x)=1/(1+e^x)),二分类决策输出1或0,分别代表向下左转或向下右转;每个叶子节点代表语料库中的一个词语,于是每个词语都可以被01唯一地编码,并且其编码序列对应一个事件序列,于是我们可以计算条件概率屏幕快照 2016-07-17 上午10.05.33.png

在开始计算之前,还是得引入一些符号:

  1. 屏幕快照 2016-07-17 上午9.59.45.png从根结点出发到达w对应叶子结点的路径.
  2. 屏幕快照 2016-07-17 上午10.00.06.png路径中包含结点的个数
  3. 屏幕快照 2016-07-17 上午10.01.17.png路径屏幕快照 2016-07-17 上午9.59.45.png中的各个节点
  4. 屏幕快照 2016-07-17 上午10.02.33.png词w的编码,屏幕快照 2016-07-17 上午10.03.27.png表示路径屏幕快照 2016-07-17 上午9.59.45.png第j个节点对应的编码(根节点无编码)
  5. 屏幕快照 2016-07-17 上午10.04.18.png路径屏幕快照 2016-07-17 上午9.59.45.png中非叶节点对应的参数向量于是可以给出w的条件概率:

屏幕快照 2016-07-17 上午10.07.18.png

这是个简单明了的式子,从根节点到叶节点经过了屏幕快照 2016-07-17 上午10.00.06.png-1个节点,编码从下标2开始(根节点无编码),对应的参数向量下标从1开始(根节点为1)。

其中,每一项是一个逻辑斯谛回归:

屏幕快照 2016-07-17 上午10.15.37.png

考虑到d只有0和1两种取值,我们可以用指数形式方便地将其写到一起:

屏幕快照 2016-07-17 上午10.21.31.png

我们的目标函数取对数似然:

屏幕快照 2016-07-17 上午10.23.25.png

屏幕快照 2016-07-17 上午10.05.33.png代入上式,有

屏幕快照 2016-07-17 上午10.25.37.png

这也很直白,连乘的对数换成求和。不过还是有点长,我们把每一项简记为:

屏幕快照 2016-07-17 上午10.27.15.png

怎么最大化对数似然函数呢?分别最大化每一项即可(这应该是一种近似,最大化某一项不一定使整体增大,具体收敛的证明还不清楚)。怎么最大化每一项呢?先求函数对每个变量的偏导数,对每一个样本,代入偏导数表达式得到函数在该维度的增长梯度,然后让对应参数加上这个梯度,函数在这个维度上就增长了。这种白话描述的算法在学术上叫随机梯度上升法,详见更规范的描述

每一项有两个参数,一个是每个节点的参数向量屏幕快照 2016-07-17 上午10.58.10.png,另一个是输出层的输入屏幕快照 2016-07-17 上午10.52.10.png,我们分别对其求偏导数:

屏幕快照 2016-07-17 上午10.52.59.png

因为sigmoid函数的导数有个很棒的形式:

屏幕快照 2016-07-17 上午10.54.30.png

于是代入上上式得到:

屏幕快照 2016-07-17 上午10.56.15.png

合并同类项得到:

屏幕快照 2016-07-17 上午10.57.17.png

于是屏幕快照 2016-07-17 上午10.58.10.png的更新表达式就得到了:

屏幕快照 2016-07-17 上午10.59.08.png

其中,屏幕快照 2016-07-17 上午10.59.48.png是机器学习的老相好——学习率,通常取0-1之间的一个值。学习率越大训练速度越快,但目标函数容易在局部区域来回抖动。

再来屏幕快照 2016-07-17 上午10.52.10.png的偏导数,注意到屏幕快照 2016-07-17 上午10.27.15.png屏幕快照 2016-07-17 上午10.52.10.png屏幕快照 2016-07-17 上午10.58.10.png是对称的,所有直接将屏幕快照 2016-07-17 上午10.58.10.png的偏导数中的屏幕快照 2016-07-17 上午10.58.10.png替换为屏幕快照 2016-07-17 上午10.52.10.png,得到关于屏幕快照 2016-07-17 上午10.52.10.png的偏导数:

屏幕快照 2016-07-17 上午11.04.49.png

于是屏幕快照 2016-07-17 上午10.52.10.png的更新表达式也得到了。

不过屏幕快照 2016-07-17 上午10.52.10.png是上下文的词向量的和,不是上下文单个词的词向量。怎么把这个更新量应用到单个词的词向量上去呢?word2vec采取的是直接将屏幕快照 2016-07-17 上午10.52.10.png的更新量整个应用到每个单词的词向量上去:

屏幕快照 2016-07-17 上午11.11.33.png

其中,屏幕快照 2016-07-17 上午11.11.46.png代表上下文中某一个单词的词向量。我认为应该也可以将其平均后更新到每个词向量上去,无非是学习率的不同,欢迎指正。

代码分析

于是就可以得到两个参数更新的伪码:

屏幕快照 2016-07-17 上午11.15.50.png

在原版C代码中的对应关系是:

  1. = 0;
  2. // Propagate hidden -> output
  3. for (= 0; c < layer1_size; c++)
  4.     f += neu1[c] * syn1[+ l2];

f对应q,neu1对应屏幕快照 2016-07-17 上午10.52.10.png,syn1对应屏幕快照 2016-07-17 上午10.58.10.png

  1. // ‘g’ is the gradient multiplied by the learning rate
  2. = (1  vocab[word].code[d]  f) * alpha;

对应伪码中的g。

  1. // Propagate errors output -> hidden
  2. for (= 0; c < layer1_size; c++)
  3.     neu1e[c] += g * syn1[+ l2];

对应伪码中的e。

  1. // Learn weights hidden -> output
  2. for (= 0; c < layer1_size; c++)
  3.     syn1[+ l2] += g * neu1[c];

对应伪码中的屏幕快照 2016-07-17 上午10.58.10.png

Skip-gram

原理

Skip-gram只是逆转了CBOW的因果关系而已,即已知当前词语,预测上下文。

其网络结构如下图所示:

屏幕快照 2016-07-17 上午11.33.31.png

上图与CBOW的两个不同在于

  1. 输入层不再是多个词向量,而是一个词向量
  2. 投影层其实什么事情都没干,直接将输入层的词向量传递给输出层

在对其推导之前需要引入一个新的记号:

u:表示w的上下文中的一个词语。

于是语言模型的概率函数可以写作:

屏幕快照 2016-07-17 上午11.42.19.png

注意这是一个词袋模型,所以每个u是无序的,或者说,互相独立的。

在Hierarchical Softmax思想下,每个u都可以编码为一条01路径:

屏幕快照 2016-07-17 上午11.45.43.png

类似地,每一项都是如下简写:

屏幕快照 2016-07-17 上午11.47.23.png

把它们写到一起,得到目标函数:

屏幕快照 2016-07-17 下午1.38.22.png

类似CBOW的做法,将每一项简记为:

屏幕快照 2016-07-17 下午1.40.34.png

虽然上式对比CBOW多了一个u,但给定训练实例(一个词w和它的上下文{u}),u也是固定的。所以上式其实依然只有两个变量屏幕快照 2016-07-17 上午10.52.10.png屏幕快照 2016-07-17 上午10.58.10.png,对其求偏导数:

屏幕快照 2016-07-17 下午1.47.40.png屏幕快照 2016-07-17 下午1.47.16.png

具体求导过程类似CBOW,略过。

于是得到屏幕快照 2016-07-17 上午10.58.10.png的更新表达式:

屏幕快照 2016-07-17 下午1.49.16.png

同理利用对称性得到对屏幕快照 2016-07-17 上午10.52.10.png的偏导数:

屏幕快照 2016-07-17 下午1.50.19.png

于是得到屏幕快照 2016-07-17 上午10.52.10.png的更新表达式:

屏幕快照 2016-07-17 下午1.51.20.png

训练伪码如下:

屏幕快照 2016-07-17 下午1.53.06.png

word2vec源码中并没有等屏幕快照 2016-07-17 上午10.58.10.png更新完再更新屏幕快照 2016-07-17 上午10.52.10.png,而是即时地更新:

屏幕快照 2016-07-17 下午1.55.05.png

具体对应源码中的

  1. // Propagate hidden -> output
  2. for (= 0; c < layer1_size; c++)
  3.     f += syn0[+ l1] * syn1[+ l2];
  4. // ‘g’ is the gradient multiplied by the learning rate
  5. = (1  vocab[word].code[d]  f) * alpha;
  6. // Propagate errors output -> hidden
  7. for (= 0; c < layer1_size; c++)
  8.     neu1e[c] += g * syn1[+ l2];
  9. // Learn weights hidden -> output
  10. for (= 0; c < layer1_size; c++)
  11.     syn1[+ l2] += g * syn0[+ l1];

f对应q,syn0对应v,syn1对应屏幕快照 2016-07-17 上午10.58.10.png,neu1e对应e。

Negative Sampling

通过上一章的学习,我们知道无论是CBOW还是Skip-gram模型,其实都是分类模型。对于机器学习中的分类任务,在训练的时候不但要给正例,还要给负例。对于Hierarchical Softmax,负例放在二叉树的根节点上。对于Negative Sampling,负例是随机挑选出来的。据说Negative Sampling能提高速度、改进模型质量。

CBOW

给定训练样本,即一个词w和它的上下文Context(w),Context(w)是输入,w是输出。那么w就是正例,词汇表中其他的词语的就是负例。假设我们通过某种采样方法获得了负例子集NEG(w)。对于正负样本,分别定义一个标签:

屏幕快照 2016-07-17 下午2.18.20.png

也即正样本为1,负样本为0。

对于给定正样本屏幕快照 2016-07-17 下午2.20.18.png,我们希望最大化:

屏幕快照 2016-07-17 下午2.21.21.png

其中,

屏幕快照 2016-07-17 下午2.22.46.png

也就是说,当u是正例时,屏幕快照 2016-07-17 下午2.25.48.png越大越好,当u是负例时,屏幕快照 2016-07-17 下午2.25.48.png越小越好。因为屏幕快照 2016-07-17 下午2.25.48.png等于模型预测样本为正例的概率,当答案就是正的时候,我们希望这个概率越大越好,当答案是负的时候,我们希望它越小越好,这样才能说明该模型是个明辨是非的好同志。

每个词都是如此,语料库有多个词,我们将g累积得到优化目标。因为对数方便计算,我们对其取对数得到目标函数:

屏幕快照 2016-07-17 下午2.30.13.png屏幕快照 2016-07-17 下午2.30.29.png

屏幕快照 2016-07-17 下午2.30.41.png

记双重求和中的每一项为:

屏幕快照 2016-07-17 下午2.35.00.png

求梯度:

屏幕快照 2016-07-17 下午2.35.20.png屏幕快照 2016-07-17 下午2.36.01.png

于是屏幕快照 2016-07-17 下午2.37.02.png的更新方法为:

屏幕快照 2016-07-17 下午2.37.34.png

利用对称性得到关于屏幕快照 2016-07-17 上午10.52.10.png的梯度:

屏幕快照 2016-07-17 下午2.38.12.png

将该更新应用到每个词向量上去:

屏幕快照 2016-07-17 下午2.40.01.png

训练伪码为:

屏幕快照 2016-07-17 下午2.40.41.png

对应原版C代码的片段:

  1. = 0;
  2. for (= 0; c < layer1_size; c++)
  3.     f += neu1[c] * syn1neg[+ l2];
  4. if (> MAX_EXP)
  5.     g = (label  1) * alpha;
  6. else if (< MAX_EXP)
  7.     g = (label  0) * alpha;
  8. else
  9.     g = (label  expTable[(int) ((+ MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
  10. for (= 0; c < layer1_size; c++)
  11.     neu1e[c] += g * syn1neg[+ l2];
  12. for (= 0; c < layer1_size; c++)
  13.     syn1neg[+ l2] += g * neu1[c];

Skip-gram

有了前三次的经验,这次轻车熟路地给出结论吧。颠倒样本的x和y部分,也即对屏幕快照 2016-07-17 下午2.48.48.png,我们希望最大化:

屏幕快照 2016-07-17 下午2.49.32.png

其中,

屏幕快照 2016-07-17 下午2.50.05.png

最终目标函数为:

屏幕快照 2016-07-17 下午2.52.14.png屏幕快照 2016-07-17 下午2.52.27.png屏幕快照 2016-07-17 下午2.52.39.png

其中,

屏幕快照 2016-07-17 下午2.53.36.png

分别求出梯度:

屏幕快照 2016-07-17 下午2.54.58.png屏幕快照 2016-07-17 下午2.55.09.png

屏幕快照 2016-07-17 下午2.57.39.png

得到两者的更新方法:

屏幕快照 2016-07-17 下午3.00.14.png+=屏幕快照 2016-07-17 下午2.58.34.png

屏幕快照 2016-07-17 下午3.01.01.png屏幕快照 2016-07-17 下午3.01.34.png

训练伪码为:

屏幕快照 2016-07-17 下午3.02.35.png

对应原版C代码片段:

  1. = 0;
  2. for (= 0; c < layer1_size; c++)
  3.     f += syn0[+ l1] * syn1neg[+ l2];
  4. if (> MAX_EXP)
  5.     g = (label  1) * alpha;
  6. else if (< MAX_EXP)
  7.     g = (label  0) * alpha;
  8. else
  9.     g = (label  expTable[(int) ((+ MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
  10. for (= 0; c < layer1_size; c++)
  11.     neu1e[c] += g * syn1neg[+ l2];
  12. for (= 0; c < layer1_size; c++)
  13.     syn1neg[+ l2] += g * syn0[+ l1];

syn0对应屏幕快照 2016-07-17 上午10.52.10.png,syn1neg对应屏幕快照 2016-07-17 下午2.37.02.png,f运算后得到q,代码中有优化(后文分解),neu1e对应e。

更多细节

Huffman树

上文一直在用二叉树描述Hierarchical Softmax,这是因为我不想仿照大部分tutorial那样一下子拿出Huffman这么具体的细节。初期对word2vec的大框架还没把握住的时候突然看到这些细节的话,人会抓不住重点,造成学习成本无谓的上升。我当时看到有些tutorial第一节就在讲Huffman编码,还以为实现word2vec一定要用Huffman树呢。

其实根本不是的,任何二叉树都可以。Huffman树只是二叉树中具体的一种,特别适合word2vec的训练。

word2vec训练的时候按照词频将每个词语Huffman编码,由于Huffman编码中词频越高的词语对应的编码越短。所以越高频的词语在Hierarchical Softmax过程中经过的二分类节点就越少,整体计算量就更少了。

负采样算法

任何采样算法都应该保证频次越高的样本越容易被采样出来。基本的思路是对于长度为1的线段,根据词语的词频将其公平地分配给每个词语:

屏幕快照 2016-07-17 下午3.16.37.png

counter就是w的词频。

于是我们将该线段公平地分配了:

屏幕快照 2016-07-17 下午3.18.09.png

接下来我们只要生成一个0-1之间的随机数,看看落到哪个区间,就能采样到该区间对应的单词了,很公平。

但怎么根据小数找区间呢?速度慢可不行。

word2vec用的是一种查表的方式,将上述线段标上M个“刻度”,刻度之间的间隔是相等的,即1/M:

屏幕快照 2016-07-17 下午3.22.12.png

接着我们就不生成0-1之间的随机数了,我们生成0-M之间的整数,去这个刻度尺上一查就能抽中一个单词了。

在word2vec中,该“刻度尺”对应着table数组。具体实现时,对词频取了0.75次幂:

屏幕快照 2016-07-17 下午4.02.32.png

这个幂实际上是一种“平滑”策略,能够让低频词多一些出场机会,高频词贡献一些出场机会,劫富济贫。

sigmoid函数

类似的查表方法还有sigmoid函数的计算,因为该函数使用太频繁,而其值仅仅在靠近0的时候才会剧烈变化,远离0的方向很快趋近0和1。所以源码中也采用了“刻度查表”的方法,先算出了很多个刻度对应的函数值,运算中直接查表。这部分对应:

  1. expTable = (real *) malloc((EXP_TABLE_SIZE + 1) * sizeof(real));
  2. for (= 0; i < EXP_TABLE_SIZE; i++)
  3. {
  4.     expTable[i] = exp((/ (real) EXP_TABLE_SIZE * 2  1) * MAX_EXP); // Precompute the exp() table
  5.     expTable[i] = expTable[i] / (expTable[i] + 1);                   // Precompute f(x) = x / (x + 1)
  6. }

多线程

关于如何多线程并行训练的问题,我没看代码之前也想过。大致就是将语料按照线程数均分,大家分头算,更新参数的过程中做好线程同步的工作。

后来看了原版C代码,原来作者压根就没做线程同步,一个全局的数组,大家都往里面写,万一下标冲突了怎么办?那就让它冲突呗……数组那么大(在text8语料上是一千万),线程那么少,冲突的概率不大吧。

一些开源实现

C

https://github.com/dav/word2vec

在原版的基础上打了一些社区的patch,可以在macos上make编译

https://github.com/bpiwowar/word2vec

原版的CMake迁移,可以在macos下编译

https://github.com/RaRe-Technologies/gensim/blob/develop/gensim/models/word2vec.py

大名鼎鼎的gensim,适合Python用户。

https://github.com/h10r/word2vec-macosx-maverics

改了几个文件头,可以在macos上make编译。

C++

https://github.com/eske/multivec

C++实现的各种各样的XXvector,包括paragraph vector等,其word2vec只是对原版C代码的包装,没有多少改进。

*https://github.com/jdeng/word2vec

又一份C++11的实现,虽然星星很多,但据说准确率惨不忍睹,并且作者没有解释。在较早的一份issue(就是由一份Java版的作者siegfang提出的)中,作者表示“I am not sure if my implementation is accurate”。另外Google论坛上有人表示该C++11实现只有原版C实现速度的一半。所以我认为这两个版本都应该谨慎使用。

Java

https://github.com/medallia

一份Java实现,使用了很多Google的库,校正了一些原版的错误,阉割掉了k-means,从代码质量上讲总体是一份不错的实现。其输出的bin模型与原版C程序直接兼容,然而并不支持宽字符(需要改改,有个pull request做了,但作者一直没merge)。我测试了其准确率,比原版低20%左右。从这一点来讲,该实现没有多大价值。

https://github.com/kojisekig/word2vec-lucene

一份Java实现,是我见过最忠于原版的Java实现。卖点是不但可以用文本文件训练,还可以直接用Lucene的index训练。对于文本,由于Java没有“读取下一个String”的IO接口,作者实现了一个TextFileCorpus.nextWord。该方法读取一行并且拆分成字符串数组,然而text8整个文件也就一行,所以会频繁地多次读取(多个线程),然后OOM。作者提供一个切割程序,将text8切成多行,这样才能训练text8。作者并没有做准确率评测,我移植了谷歌的评测程序,并提交给了作者。我还将评测结果做了对比,比原版低10%左右,也报告给了作者,有志于开源项目的朋友可以持续参与讨论。事实上,这份实现的价值是最高的,因为它的准确率是Java中最高的。

*https://github.com/siegfang/word2vec

一份Java实现,卖点是并行化(其实上所有开源的都支持并行化);内存占用较大(Java的通病),据作者siegfang讲参考了上述C++11实现。然而上梁不正,下梁能好到哪去。既不支持negative sampling,又不能保证准确率,毫无亮点。

其他不在此列表中的方案要么没被我看到,要么不值得一试。

事实上,上述实现都没入这位日本友人的评测单,他建议或者原版,或者gensim。

我的Java方案

在C和Python界,我们分别有原版C程序和gensim可以用;在Java界,没有足够好用的开源实现。鉴于此,我决定在kojisekig的基础上优化一下,目前效果如下——

参数

使用相同的参数

  1. word2vec train text8 output vectors.bin cbow 1 size 200 window 8 negative 25 hs 0 sample 1e-4 threads 8 binary 1 iter 15
  2. com.hankcs.word2vec.Train input text8 output vectors.txt cbow 1 size 200 window 8 negative 25 hs 0 sample 1e-4 threads 8 binary 1 iter 15

训练速度

训练过程中的对比——

原版C代码

  1. Alpha: 0.048220  Progress: 3.56%  Words/thread/sec: 78.33k

我的Java移植

  1. Alpha: 0.048824  Progress: 2.39%  Words/thread/sec: 182.56k

每个线程每秒训练的词语稳定在180-190K,比原版C程序要快2.5倍左右;训练速度比C程序要快的原因是,原版C程序读取单词后需要去char数组里遍历查找id;而我的Java实现直接读取缓存文件中的id,当然开始训练前要先进行词->id的转换并输出到缓存文件,这个过程大约多花一两分钟时间,相较于训练时间,无疑是值得的。这样改进之后还可以直接读取类似text8那样的变态语料,一举多得。

准确率

由于并没有在算法层找出问题所在,所以准确率依然和kojisekig的一样,欢迎围观

Reference

《word2vec中的数学原理详解》

《Deep Learning 实战之 word2vec》

 

 

1. 词向量基础

用词向量来表示词并不是word2vec的首创,在很久之前就出现了。最早的词向量是很冗长的,它使用是词向量维度大小为整个词汇表的大小,对于每个具体的词汇表中的词,将对应的位置置为1。比如我们有下面的5个词组成的词汇表,词”Queen”的序号为2, 那么它的词向量就是(0,1,0,0,0)(0,1,0,0,0)。同样的道理,词”Woman”的词向量就是(0,0,0,1,0)(0,0,0,1,0)。这种词向量的编码方式我们一般叫做1-of-N representation或者one hot representation.

One hot representation用来表示词向量非常简单,但是却有很多问题。最大的问题是我们的词汇表一般都非常大,比如达到百万级别,这样每个词都用百万维的向量来表示简直是内存的灾难。这样的向量其实除了一个位置是1,其余的位置全部都是0,表达的效率不高,能不能把词向量的维度变小呢?

Dristributed representation可以解决One hot representation的问题,它的思路是通过训练,将每个词都映射到一个较短的词向量上来。所有的这些词向量就构成了向量空间,进而可以用普通的统计学的方法来研究词与词之间的关系。这个较短的词向量维度是多大呢?这个一般需要我们在训练时自己来指定。

比如下图我们将词汇表里的词用”Royalty”,”Masculinity”, “Femininity”和”Age”4个维度来表示,King这个词对应的词向量可能是(0.99,0.99,0.05,0.7)(0.99,0.99,0.05,0.7)。当然在实际情况中,我们并不能对词向量的每个维度做一个很好的解释。

 

有了用Dristributed representation表示的较短的词向量,我们就可以较容易的分析词之间的关系了,比如我们将词的维度降维到2维,有一个有趣的研究表明,用下图的词向量表示我们的词时,我们可以发现:

KingMan+Woman=QueenKing→−Man→+Woman→=Queen→

可见我们只要得到了词汇表里所有词对应的词向量,那么我们就可以做很多有趣的事情了。不过,怎么训练得到合适的词向量呢?一个很常见的方法是使用神经网络语言模型。

2. CBOW与Skip-Gram用于神经网络语言模型

在word2vec出现之前,已经有用神经网络DNN来用训练词向量进而处理词与词之间的关系了。采用的方法一般是一个三层的神经网络结构(当然也可以多层),分为输入层,隐藏层和输出层(softmax层)。

这个模型是如何定义数据的输入和输出呢?一般分为CBOW(Continuous Bag-of-Words 与Skip-Gram两种模型。

CBOW模型的训练输入是某一个特征词的上下文相关的词对应的词向量,而输出就是这特定的一个词的词向量。比如下面这段话,我们的上下文大小取值为4,特定的这个词是”Learning”,也就是我们需要的输出词向量,上下文对应的词有8个,前后各4个,这8个词是我们模型的输入。由于CBOW使用的是词袋模型,因此这8个词都是平等的,也就是不考虑他们和我们关注的词之间的距离大小,只要在我们上下文之内即可。

这样我们这个CBOW的例子里,我们的输入是8个词向量,输出是所有词的softmax概率(训练的目标是期望训练样本特定词对应的softmax概率最大),对应的CBOW神经网络模型输入层有8个神经元,输出层有词汇表大小个神经元。隐藏层的神经元个数我们可以自己指定。通过DNN的反向传播算法,我们可以求出DNN模型的参数,同时得到所有的词对应的词向量。这样当我们有新的需求,要求出某8个词对应的最可能的输出中心词时,我们可以通过一次DNN前向传播算法并通过softmax激活函数找到概率最大的词对应的神经元即可。

Skip-Gram模型和CBOW的思路是反着来的,即输入是特定的一个词的词向量,而输出是特定词对应的上下文词向量。还是上面的例子,我们的上下文大小取值为4, 特定的这个词”Learning”是我们的输入,而这8个上下文词是我们的输出。

这样我们这个Skip-Gram的例子里,我们的输入是特定词, 输出是softmax概率排前8的8个词,对应的Skip-Gram神经网络模型输入层有1个神经元,输出层有词汇表大小个神经元。隐藏层的神经元个数我们可以自己指定。通过DNN的反向传播算法,我们可以求出DNN模型的参数,同时得到所有的词对应的词向量。这样当我们有新的需求,要求出某1个词对应的最可能的8个上下文词时,我们可以通过一次DNN前向传播算法得到概率大小排前8的softmax概率对应的神经元所对应的词即可。

以上就是神经网络语言模型中如何用CBOW与Skip-Gram来训练模型与得到词向量的大概过程。但是这和word2vec中用CBOW与Skip-Gram来训练模型与得到词向量的过程有很多的不同。

word2vec为什么 不用现成的DNN模型,要继续优化出新方法呢?最主要的问题是DNN模型的这个处理过程非常耗时。我们的词汇表一般在百万级别以上,这意味着我们DNN的输出层需要进行softmax计算各个词的输出概率的的计算量很大。有没有简化一点点的方法呢?

3. word2vec基础之霍夫曼树

word2vec也使用了CBOW与Skip-Gram来训练模型与得到词向量,但是并没有使用传统的DNN模型。最先优化使用的数据结构是用霍夫曼树来代替隐藏层和输出层的神经元,霍夫曼树的叶子节点起到输出层神经元的作用,叶子节点的个数即为词汇表的小大。 而内部节点则起到隐藏层神经元的作用。

具体如何用霍夫曼树来进行CBOW和Skip-Gram的训练我们在下一节讲,这里我们先复习下霍夫曼树。

霍夫曼树的建立其实并不难,过程如下:

输入:权值为(w1,w2,...wn)n个节点

输出:对应的霍夫曼树

1)将(w1,w2,...wn)看做是有n棵树的森林,每个树仅有一个节点。

2)在森林中选择根节点权值最小的两棵树进行合并,得到一个新的树,这两颗树分布作为新树的左右子树。新树的根节点权重为左右子树的根节点权重之和。

3) 将之前的根节点权值最小的两棵树从森林删除,并把新树加入森林。

4)重复步骤2)和3)直到森林里只有一棵树为止。

下面我们用一个具体的例子来说明霍夫曼树建立的过程,我们有(a,b,c,d,e,f)共6个节点,节点的权值分布是(16,4,8,6,20,3)。

首先是最小的b和f合并,得到的新树根节点权重是7.此时森林里5棵树,根节点权重分别是16,8,6,20,7。此时根节点权重最小的6,7合并,得到新子树,依次类推,最终得到下面的霍夫曼树。

那么霍夫曼树有什么好处呢?一般得到霍夫曼树后我们会对叶子节点进行霍夫曼编码,由于权重高的叶子节点越靠近根节点,而权重低的叶子节点会远离根节点,这样我们的高权重节点编码值较短,而低权重值编码值较长。这保证的树的带权路径最短,也符合我们的信息论,即我们希望越常用的词拥有更短的编码。如何编码呢?一般对于一个霍夫曼树的节点(根节点除外),可以约定左子树编码为0,右子树编码为1.如上图,则可以得到c的编码是00。

在word2vec中,约定编码方式和上面的例子相反,即约定左子树编码为1,右子树编码为0,同时约定左子树的权重不小于右子树的权重。

 

1. 基于Hierarchical Softmax的模型概述

我们先回顾下传统的神经网络词向量语言模型,里面一般有三层,输入层(词向量),隐藏层和输出层(softmax层)。里面最大的问题在于从隐藏层到输出的softmax层的计算量很大,因为要计算所有词的softmax概率,再去找概率最大的值。这个模型如下图所示。其中VV是词汇表的大小,

 

word2vec对这个模型做了改进,首先,对于从输入层到隐藏层的映射,没有采取神经网络的线性变换加激活函数的方法,而是采用简单的对所有输入词向量求和并取平均的方法。比如输入的是三个4维词向量:(1,2,3,4),(9,6,11,8),(5,10,7,12)(1,2,3,4),(9,6,11,8),(5,10,7,12),那么我们word2vec映射后的词向量就是(5,6,7,8)(5,6,7,8)。由于这里是从多个词向量变成了一个词向量。

第二个改进就是从隐藏层到输出的softmax层这里的计算量个改进。为了避免要计算所有词的softmax概率,word2vec采样了霍夫曼树来代替从隐藏层到输出softmax层的映射。我们在上一节已经介绍了霍夫曼树的原理。如何映射呢?这里就是理解word2vec的关键所在了。

由于我们把之前所有都要计算的从输出softmax层的概率计算变成了一颗二叉霍夫曼树,那么我们的softmax概率计算只需要沿着树形结构进行就可以了。如下图所示,我们可以沿着霍夫曼树从根节点一直走到我们的叶子节点的词w2w2

和之前的神经网络语言模型相比,我们的霍夫曼树的所有内部节点就类似之前神经网络隐藏层的神经元,其中,根节点的词向量对应我们的投影后的词向量,而所有叶子节点就类似于之前神经网络softmax输出层的神经元,叶子节点的个数就是词汇表的大小。在霍夫曼树中,隐藏层到输出层的softmax映射不是一下子完成的,而是沿着霍夫曼树一步步完成的,因此这种softmax取名为”Hierarchical Softmax”。

如何“沿着霍夫曼树一步步完成”呢?在word2vec中,我们采用了二元逻辑回归的方法,即规定沿着左子树走,那么就是负类(霍夫曼树编码1),沿着右子树走,那么就是正类(霍夫曼树编码0)。判别正类和负类的方法是使用sigmoid函数,即:

其中xwxw是当前内部节点的词向量,而θθ则是我们需要从训练样本求出的逻辑回归的模型参数。

使用霍夫曼树有什么好处呢?首先,由于是二叉树,之前计算量为VV,现在变成了log2Vlog2V。第二,由于使用霍夫曼树是高频的词靠近树根,这样高频词需要更少的时间会被找到,这符合我们的贪心优化思想。

容易理解,被划分为左子树而成为负类的概率为P()=1P(+)P(−)=1−P(+)。在某一个内部节点,要判断是沿左子树还是右子树走的标准就是看P(),P(+)P(−),P(+)谁的概率值大。而控制P(),P(+)P(−),P(+)谁的概率值大的因素一个是当前节点的词向量,另一个是当前节点的模型参数θθ

对于上图中的w2w2,如果它是一个训练样本的输出,那么我们期望对于里面的隐藏节点n(w2,1)n(w2,1)P()P(−)概率大,n(w2,2)n(w2,2)P()P(−)概率大,n(w2,3)n(w2,3)P(+)P(+)概率大。

回到基于Hierarchical Softmax的word2vec本身,我们的目标就是找到合适的所有节点的词向量和所有内部节点θθ, 使训练样本达到最大似然。那么如何达到最大似然呢?

2. 基于Hierarchical Softmax的模型梯度计算

我们使用最大似然法来寻找所有节点的词向量和所有内部节点θθ。先拿上面的w2w2例子来看,我们期望最大化下面的似然函数:

i=13P(n(wi),i)=(111+exTwθ1)(111+exTwθ2)11+exTwθ3∏i=13P(n(wi),i)=(1−11+e−xwTθ1)(1−11+e−xwTθ2)11+e−xwTθ3

对于所有的训练样本,我们期望最大化所有样本的似然函数乘积。

为了便于我们后面一般化的描述,我们定义输入的词为ww,其从输入层词向量求和平均后的霍夫曼树根节点词向量为xwxw, 从根节点到ww所在的叶子节点,包含的节点总数为lwlwww在霍夫曼树中从根节点开始,经过的第ii个节点表示为pwipiw,对应的霍夫曼编码为dwi{0,1}diw∈{0,1},其中i=2,3,...lwi=2,3,…lw。而该节点对应的模型参数表示为θwiθiw, 其中i=1,2,...lw1i=1,2,…lw−1,没有i=lwi=lw是因为模型参数仅仅针对于霍夫曼树的内部节点。

那么对于某一个目标输出词ww,其最大似然为:

在word2vec中,由于使用的是随机梯度上升法,所以并没有把所有样本的似然乘起来得到真正的训练集最大似然,仅仅每次只用一个样本更新梯度,这样做的目的是减少梯度计算量。这样我们可以得到ww的对数似然函数LL如下:

 

要得到模型中ww词向量和内部节点的模型参数θθ, 我们使用梯度上升法即可。首先我们求模型参数θwj1θj−1w的梯度:

如果大家看过之前写的逻辑回归原理小结,会发现这里的梯度推导过程基本类似。

同样的方法,可以求出xwxw的梯度表达式如下:

有了梯度表达式,我们就可以用梯度上升法进行迭代来一步步的求解我们需要的所有的θwj1θj−1wxwxw

3. 基于Hierarchical Softmax的CBOW模型

由于word2vec有两种模型:CBOW和Skip-Gram,我们先看看基于CBOW模型时, Hierarchical Softmax如何使用。

首先我们要定义词向量的维度大小MM,以及CBOW的上下文大小2c2c,这样我们对于训练样本中的每一个词,其前面的cc个词和后面的cc个词作为了CBOW模型的输入,该词本身作为样本的输出,期望softmax概率最大。

在做CBOW模型前,我们需要先将词汇表建立成一颗霍夫曼树。

对于从输入层到隐藏层(投影层),这一步比较简单,就是对ww周围的2c2c个词向量求和取平均即可,即:

第二步,通过梯度上升法来更新我们的θwj1θj−1wxwxw,注意这里的xwxw是由2c2c个词向量相加而成,我们做梯度更新完毕后会用梯度项直接更新原始的各个xi(i=1,2,,,,2c)xi(i=1,2,,,,2c),即:

其中ηη为梯度上升法的步长。

这里总结下基于Hierarchical Softmax的CBOW模型算法流程,梯度迭代使用了随机梯度上升法:

输入:基于CBOW的语料训练样本,词向量的维度大小MM,CBOW的上下文大小2c2c,步长ηη

输出:霍夫曼树的内部节点模型参数θθ,所有的词向量ww

1. 基于语料训练样本建立霍夫曼树。

2. 随机初始化所有的模型参数θθ,所有的词向量ww

3. 进行梯度上升迭代过程,对于训练集中的每一个样本(context(w),w)(context(w),w)做如下处理:

c) 对于context(w)context(w)中的每一个词向量xixi(共2c个)进行更新:

xi=xi+exi=xi+e

 

d) 如果梯度收敛,则结束梯度迭代,否则回到步骤3继续迭代。

4. 基于Hierarchical Softmax的Skip-Gram模型

现在我们先看看基于Skip-Gram模型时, Hierarchical Softmax如何使用。此时输入的只有一个词ww,输出的为2c2c个词向量context(w)context(w)

我们对于训练样本中的每一个词,该词本身作为样本的输入, 其前面的cc个词和后面的cc个词作为了Skip-Gram模型的输出,,期望这些词的softmax概率比其他的词大。

Skip-Gram模型和CBOW模型其实是反过来的,在上一篇已经讲过。

在做CBOW模型前,我们需要先将词汇表建立成一颗霍夫曼树。

对于从输入层到隐藏层(投影层),这一步比CBOW简单,由于只有一个词,所以,即xwxw就是词ww对应的词向量。

第二步,通过梯度上升法来更新我们的θwj1θj−1wxwxw,注意这里的xwxw周围有2c2c个词向量,此时如果我们期望P(xi|xw),i=1,2…2cP(xi|xw),i=1,2…2c最大。此时我们注意到由于上下文是相互的,在期望P(xi|xw),i=1,2…2cP(xi|xw),i=1,2…2c最大化的同时,反过来我们也期望P(xw|xi),i=1,2…2cP(xw|xi),i=1,2…2c最大。那么是使用P(xi|xw)P(xi|xw)好还是P(xw|xi)P(xw|xi)好呢,word2vec使用了后者,这样做的好处就是在一次迭代时,我们不是更新xwxw一个词,而是xi,i=1,2…2cxi,i=1,2…2c2c2c个词。这样整体的迭代会更加的均衡。因为这个原因,Skip-Gram模型并没有和CBOW模型一样对输入进行迭代更新,而是对2c2c个输出进行迭代更新。

这里总结下基于Hierarchical Softmax的Skip-Gram模型算法流程,梯度迭代使用了随机梯度上升法:

输入:基于Skip-Gram的语料训练样本,词向量的维度大小MM,Skip-Gram的上下文大小2c2c,步长ηη

输出:霍夫曼树的内部节点模型参数θθ,所有的词向量ww

1. 基于语料训练样本建立霍夫曼树。

2. 随机初始化所有的模型参数θθ,所有的词向量ww,

3. 进行梯度上升迭代过程,对于训练集中的每一个样本(w,context(w))(w,context(w))做如下处理:

b)如果梯度收敛,则结束梯度迭代,算法结束,否则回到步骤a继续迭代。

5. Hierarchical Softmax的模型源码和算法的对应

这里给出上面算法和word2vec源码中的变量对应关系。

在源代码中,基于Hierarchical Softmax的CBOW模型算法在435-463行,基于Hierarchical Softmax的Skip-Gram的模型算法在495-519行。大家可以对着源代码再深入研究下算法。

在源代码中,neule对应我们上面的ee, syn0对应我们的xwxw, syn1对应我们的θij1θj−1i, layer1_size对应词向量的维度,window对应我们的cc

另外,vocab[word].code[d]指的是,当前单词word的,第d个编码,编码不含Root结点。vocab[word].point[d]指的是,当前单词word,第d个编码下,前置的结点。

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *