Bootstrap

如何通俗理解Word2Vec (23年修订版)

前言 

今年上半年,我在我的上一篇LSTM博客中写道:“众所周知,我们已经把SVM、CNN、xgboost、LSTM等很多技术,写的/讲的国内最通俗易懂了,接下来,我们要把BERT等技术也写的/讲的国内最通俗易懂,成为入门标准,而且不单单是从NNLM \rightarrow Word2Vec \rightarrow Seq2Seq \rightarrow Seq2Seq with Attention \rightarrow Transformer \rightarrow Elmo \rightarrow GPT \rightarrow BERT,我们希望给所有AI初学者铺路:一步一个台阶,而不是出现理解断层。”

如今惊觉,这简直就是有意无意给自己挖了个大坑,单一个word2vec就让我断断续续看了好几个月(一方面 创业嘛 你懂的,二方面 涉及的东西实在是太多了┭┮﹏┭┮)

  1. 今年8月份,因为要写word2vec的笔记,先看我司机器学习集训营上讲word2vec的视频,让对word2vec有个初步了解(等看过很多资料后,会觉得这个视频讲的足够透彻);
  2. 网上翻阅各种资料,比如类似浅显易懂的word2vec原理讲解、以及用图形象的解释word2vec(一图胜千言)等等。心想,肯定还能写得更通俗易懂的;
  3. ​​​​继续如饿狼似的在网上查找有没更加通俗易懂的资料,包括本文的最重要的参考之一《word2vec中的数学原理详解》,部分不错的文章则注明作者和链接 录入题库
  4. 9月底,发现写清楚word2vec确实没那么简单,涉及的知识点包括神经网络、语言模型、n-gram、One-Hot、NNLM、到CBOW、Skip-gram、哈夫曼编码、反向传播、Hierarchical SoftMax、负采样等等,各种概念,一有不懂,就容易卡壳;
  5. 10月下旬,再看张俊林老师关于BERT的文章:NLP中的预训练技术发展史:从Word Embedding到Bert模型,觉得高屋建瓴、通俗细致。其实,之前bert刚火起来的时候,就看过俊林老师这篇文章,当时看的不甚了解,及至后来先学习word2vec和Transformer之后,再看此文,才觉得真是关于bert中文介绍少有的好文章

至此,拖了近半年之久,终于可以开写word2vec笔记了(首发于2019年10月,修订于2023年7月),本笔记从NNLM起步,会类似铺路一样,逐级介绍相关概念,最终完成整篇文章,且本篇笔记重点对比了2013年Mikolov的原始论文与2014年Xin Rong的文章 (主要是符号表示,比如前者用N表示词汇表的大小,后者用V表示词汇表的大小,以及网络结构表示上的差别,但本质无差别),以及对训练方法HS的解析,以通俗易懂

OK,还是那句话,若发现有何问题,欢迎随时不吝指正,thanks

第一部分 神经网络与语言模型

1.1 神经网络

引入word2vec之前需要先对神经网络的知识有一定了解

下图这个是一个简单的三层神经网络,x = [x_1,x_2,...x_K]是模型的输入,中间经过与权重矩阵{w_{ki}} 运算,矩阵运算结果再经过非线性激活函数得到隐层的结果h, 从隐层到输出层同理

这样从输入层到输出层既有线性变换,又有非线性变换,因此可以更好刻画出输入变量的特征

顺带提一嘴,有的神经网络表示中会把“隐藏层”写为“投影层”,一个意思,没有任何区别

1.2 语言模型:分为统计语言模型和神经语言模型

语言模型的用处很广泛,比如机器翻译中要经常面对的一个问题就是如何挑选一个概率尽可能大的句子也就是尽量靠谱的句子返回,说的直白点,就是要让整条语句看起来是一句人话。

语言模型有统计语言模型,也有神经语言模型。

1.2.1 统计语言模型:N-gram模型

什么是所谓的统计语言模型(Language Model)呢?简单来说,统计语言模型就是用来计算句子概率的概率模型。计算句子概率的概率模型很多,n-gram模型便是其中的一种。

假设一个长度为m的句子,包含这些词:(w_1,w_2,w_3,..,w_m),那么这个句子的概率(也就是这m个词共现的概率)是:

一般来说,语言模型都是为了使得条件概率:P(w_t|w_1,w_2,..,w_{t-1})最大化,不过考虑到近因效应,当前词只与距离它比较近的n个词更加相关(一般n不超过5),而非前面所有的词都有关。

因此上述公式可以近似为:

P\left(w_{t} \mid w_{1}, w_{2}, \ldots, w_{t-1}\right)=P\left(w_{t} \mid w_{t-1}, w_{t-2} \ldots w_{t-(n-1)}\right)

上述便是经典的n-gram模型的表示方式

不过,N-gram模型仍有其局限性

  • 首先,由于参数空间的爆炸式增长,它无法处理更长程的context(N>3)
  • 其次,它没有考虑词与词之间内在的联系性

    例如,考虑"the cat is walking in the bedroom"这句话
    如果我们在训练语料中看到了很多类似“the dog is walking in the bedroom”或是“the cat is running in the bedroom”这样的句子,那么,哪怕我们此前没有见过这句话"the cat is walking in the bedroom",也可以从“cat”和“dog”(“walking”和“running”)之间的相似性,推测出这句话的概率

    然而, Ngram模型做不到
    这是因为Ngram本质上是将词当做一个个孤立的原子单元(atomic unit)去处理的。这种处理方式对应到数学上的形式是一个个离散的one-hot向量 (除了词典索引的下标对应的方向上是1,其余方向上都是0)
    例如,对于一个大小为5的词典:{"I", "love", "nature", "language", "processing"},其中的i、love、nature、language、processing分别对应的one-hot向量为:
    i10000
    love01000
    nature00100
    language00010
    processing00001
    显然,one-hot向量的维度等于词典的大小

1.2.2 神经语言模型:NNLM模型

接下来,咱们来看一下什么是神经语言模型(NNLM),NNLM最初由Bengio在2003年发表的一篇论文《A Neural Probabilistic Language Mode》中提出来的,word2vec便是从其中简化训练而来。

咱们来考虑一个词表D的大小为N(相当于总共有N个词,词用w表示,比如N为一万,则是一万个词),词表里每个词w的维度为m,即C\epsilon R^{Nm},如果词的输入是one hot编码,则N = m,另外n表示词w的上下文中包含的词数,不超过5。

Bengio通过下面的一个三层神经网络来计算P(w_t | w_{t-1},w_{t-2} \cdots w_{t - n+1})

  1. 首先第一层输入就是前n-1个词“w_{t-n+1},\cdots ,w_{t-2},w_{t-1}”去预测第 t 个词是 w_t 的概率;
  2. 然后根据输入的前n-1个词,在同一个词汇表D中一一找到它们对应的词向量;
  3. 最后把所有词向量直接串联起来成为一个维度为(n-1)m的向量x 作为接下来三层神经网络的输入 (注意这里的“串联”,其实就是n-1个向量按顺序首尾拼接起来形成一个长向量)
  4. 隐藏层到输出层之间有大量的矩阵向量计算,在输出层之后还需要做softmax归一化计算 (使用softmax函数归一化输出之后的值是[0,1],代表可能的每个词的概率。至于为什么要用softmax,请看此文)

常用于神经网络输出层的激励函数softmax 长什么样子呢?如下图所示

从图的样子上看,和普通的全连接方式并无差异,但激励函数的形式却大不一样

首先后面一层作为预测分类的输出节点,每一个节点就代表一个分类,如图所示,那么这7个节点就代表着7个分类的模型,任何一个节点的激励函数都是:

\sigma_{i}(z)=\frac{e^{z_{i}}}{\sum_{j=1}^{m} e^{z_{i}}}

其中i 就是节点的下标次序,而z_{i} = w_{i}x + b_{i},也就说这是一个线性分类器的输出作为自然常数e的指数。最有趣的是最后一层有这样的特性:

\sum_{i=1}^{J} \sigma_{i}(z)=1

也就是说最后一层的每个节点的输出值的加和是1。这种激励函数从物理意义上可以解释为一个样本通过网络进行分类的时候在每个节点上输出的值都是小于等于1的,是它从属于这个分类的概率

训练数据由训练样本和分类标签组成,如下图所示,假设有7张图,分别为飞机、汽车、轮船、猫、狗、鸟、太阳,则图像的分类标签如下表示:

飞机1000000
汽车0100000
轮船0010000
0001000
0000100
0000010
太阳0000001

这种激励函数通常用在神经网络的最后一层作为分类器的输出,有7个节点就可以做7个不同类别的判别,有1000个节点就可以做1000个不同样本类别的判断

神经语言模型构建完成之后,就是训练参数了,这里的参数包括:

  • 词向量矩阵C;
  • 神经网络的权重;
  • 偏置等参数

训练数据就是大堆大堆的语料库。训练结束之后,语言模型得到了:通过“w_{t-(n-1)},\cdots ,w_{t-2},w_{t-1}”去预测第 t 个词是 w_t 的概率,但有点意外收获的是词向量“ w_{t-(n-1)},\cdots ,w_{t-2},w_{t-1}”也得到了。换言之,词向量是这个语言模型的副产品

当然,这个模型的缺点就是速度问题, 因为词汇表往往很大,几十万几百万,训练起来就很耗时,Bengo仅仅训练5个epoch就花了3周,这还是40个CPU并行训练的结果。因此才会有了后续好多的优化工作, word2vec便是其中一个

第二部分 Word2Vec

2.1 什么是词嵌入

在继续聊 Word2vec 之前,先聊聊 NLP (自然语言处理)。NLP 里面,最细粒度的是词语,词语组成句子,句子再组成段落、篇章、文档。所以处理 NLP 的问题,首先就要拿词语开刀。

咱们居住在各个国家的人们通过各自的语言进行交流,但机器无法直接理解人类的语言,所以需要先把人类的语言“计算机化”,那如何变成计算机可以理解的语言呢?

我们可以从另外一个角度上考虑。举个例子,对于计算机,它是如何判断一个词的词性,是动词还是名词的呢?

我们有一系列样本(x,y),对于计算机技术机器学习而言,这里的 x 是词语,y 是它们的词性,我们要构建 f(x)->y 的映射:

  1. 首先,这个数学模型 f(比如神经网络、SVM)只接受数值型输入;
  2. 而 NLP 里的词语,是人类语言的抽象总结,是符号形式的(比如中文、英文、拉丁文等等);
  3. 如此一来,咱们便需要把NLP里的词语转换成数值形式,或者嵌入到一个数学空间里;
  4. 我们可以把文本分散嵌入到另一个离散空间,称作分布式表示,又称为词嵌入(word embedding)或词向量
  5. 一种简单的词向量是one-hot encoder,其思想跟特征工程里处理类别变量的 one-hot 一样 (如之前所述,本质上是用一个只含一个 1、其他都是 0 的向量来唯一表示词语)

2.2 Word2Vec登场

当然,传统的one-hot 编码仅仅只是将词符号化,不包含任何语义信息。而且词的独热表示(one-hot representation)是高维的,且在高维向量中只有一个维度描述了词的语义 (高到什么程度呢?词典有多大就有多少维,一般至少上万的维度)。所以我们需要解决两个问题:1 需要赋予词语义信息,2 降低维度

这就轮到Word2Vec出场了

word2vec是Google研究团队里的Tomas Mikolov等人于2013年的《Distributed Representations ofWords and Phrases and their Compositionality》以及后续的《Efficient Estimation of Word Representations in Vector Space》两篇文章中提出的一种高效训练词向量的模型,基本出发点是上下文相似的两个词,它们的词向量也应该相似,比如香蕉和梨在句子中可能经常出现在相同的上下文中,因此这两个词的表示向量应该就比较相似

  • 实际上,大部分的有监督机器学习模型,都可以归结为:f(x) \rightarrow y
    在有些NLP问题中,把 x 看做一个句子里的一个词语,y 是这个词语的上下文词语,那么这里的 f 便是上文中所谓的『语言模型』(language model),这个语言模型的目的就是判断(x,y) 这个样本是否符合自然语言的法则,更通俗点说就是:有了语言模型之后,我们便可以判断出:词语 x 和词语 y 放在一起,是不是人话
  • 当然,前面也说了,这个语言模型还得到了一个副产品:词向量矩阵
    而对于word2vec 而言,词向量矩阵的意义就不一样了,因为Word2Vec的最终目的不是为了得到一个语言模型,也不是要把 f 训练得多么完美,而是只关心模型训练完后的副产物:模型参数(这里特指神经网络的权重),并将这些参数作为输入 x 的某种向量化的表示,这个向量便叫做——词向量

我们来看个例子,如何用 Word2vec 寻找相似词:

  1. 对于一句话:她们 夸 吴彦祖 帅 到 没朋友,如果输入 x 是吴彦祖,那么 y 可以是:“她们、夸、帅、没朋友”这些词
  2. 现有另一句话:她们 夸 我 帅 到 没朋友,如果输入 x 是:我,那么不难发现,这里的上下文 y 跟上面一句话一样
  3. 从而 f(吴彦祖) = f(我) = y,所以大数据告诉我们:我 = 吴彦祖(完美的结论)

所以说,word2vec模型中比较重要的概念是词汇的上下文 (说白了就是一个词周围的词,比如w_t的范围为1的上下文就是w_{t-1}w_{t+1})

2.3 word2vec模式下的两个模型:CBOW和SkipGram

2.3.1 2013年Mikolov的原始论文中的架构图

在word2vec中提出了两个模型「假设上下文窗口为3,下图来自2013年Mikolov的原始论文,注意这里没有隐藏层,只有输入层、投影层、输出层,且输入层到投影层不带权重(勘误:输入层到投影层带权重,该权重对所有单词是共享的),投影层到输出层带权重」:

  • CBOW(Continuous Bag-of-Word):以上下文词汇预测当前词,即用w_{t-2}w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2}去预测 w_{t}
  • SkipGram:以当前词预测其上下文词汇,即用w_{t}去预测w_{t-2}w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2}

2.3.2 2014年Xin Rong:输入输出都只有一个词的简单版本

假定整个词汇表的大小为V (一个小小提醒:2013年Mikolov用的N表示词表大小,此处用V,当然 不影响本质),即假设全世界所有的词语总共有 V 个,这 V 个词语有自己的先后顺序,x 是one-hot编码形式的输入,y 是在这整个词汇表里 V 个词上输出的概率,我们希望跟真实的 y 的 one-hot encoder 一样

且假定『吴彦祖』这个词是第1个词,『我』这个单词是第2个词,那么『吴彦祖』就可以表示为一个 V 维全零向量、把第1个位置的0变成1,而『我』同样表示为 V 维全零向量、把第2个位置的0变成1,这样,每个词语都可以找到属于自己的唯一表示

“吴彦祖”100....第V个维度
“我”010....第V个维度
....
第V个词

我们先来看个最简单的例子,输入输出都只有一个词。上面说到, yx 的上下文,所以 y 只取上下文里一个词语的时候,语言模型就变成:用当前词 x 预测它的下一个词 y概率最大的index所指示的单词为预测出的target word,然后与true label的onehot做比较,误差越小越好

2014年,Xin Rong在他关于Word2Vec的文章中指出:Word2Vec的网络结构里涉及神经网络中经典的输入层、隐藏层、输出层,通过从输入层到隐藏层或隐藏层到输出层的权重矩阵去向量化表示词的输入,学习迭代的是两个权重矩阵:如上图,可分别用W_{V\times N}W'_{N \times V}当然,也可以分别简写为WW',此外,其中V: 词汇表长度,N: 隐层神经元个数,同时也是词向量维度

  • 输入层到隐藏层的权重矩阵,其实就是词向量矩阵,其中每一行代表一个词的词向量

    这个从输入层到隐藏层的权重矩阵W_{V\times N}的计算量还好
    因为隐层的激活函数其实是线性的,相当于没做任何处理,我们要训练这个神经网络,可以用反向传播算法,本质上是链式求导梯度下降那一套,关于如何理解反向传播,请点击此文

  • 隐藏层到输出层的权重矩阵,其中每一列也可以看作额外的一种词向量
    但这个从隐藏层到输出层的权重矩阵W'_{N \times V}的计算量巨大,所以训练时一般用这两种加速算法:
    \rightarrow  Hierarchical softmax「简称HS,是softmax 的一种近似形式,本质是把 N 分类问题变成 log(N)次二分类,具体下文会介绍
    \rightarrow  或负采样 (即negative sampling,本质是预测总体类别的一个子集)

训练完毕后,输入层的每个单词与矩阵W相乘得到的向量的就是我们想要的词向量(word embedding),这个矩阵(所有单词的word embedding)也叫做look up table (说白了,其实这个look up table就是矩阵W自身),也就是说,任何一个单词的onehot乘以这个矩阵都将得到自己的词向量,有了look up table就可以免去训练过程直接查表得到单词的词向量了

对此,其中有两点值得特别注意下:

第一点,“通过从输入层到隐藏层或隐藏层到输出层的权重矩阵去向量化表示词的输入” 这句说的是啥意思呢?

  • 当模型训练完后,最后得到的其实是神经网络的权重,比如现在输入一个 x 的 one-hot encoder: [1,0,0,…,0],对应刚说的那个词语『我』,则在输入层到隐含层的权重里,只有对应 1 这个位置的权重被激活,当前词和隐藏层的结点一一进行带权重的相乘,相乘后的结果组成一个向量 vx 来表示x,而因为每个词语的 one-hot encoder 里面 1 的位置是不同的,所以,这个向量 vx 就可以用来唯一表示 x
    而这个向量vx 即是Mikolov 在论文里所提到的『输入向量』
  • 类似的,输出 y 也是用 V 个节点表示的,对应V个词语,所以我们把输出结点置成 [1,0,0,…,0],它也能表示『我』这个单词,但是激活的是隐藏层到输出层的权重,当前词和输出层的结点一一进行带权重的相乘,相乘后的结果组成一个向量 vy,跟上面提到的 vx 维度一样,并且可以看做是词语『我』的另一种词向量
    这个向量 vy 即是 Mikolov 在论文里所提到的『输出向量』

需要提到一点的是,这个词向量的维度(与隐含层节点数一致) 一般情况下要远远小于词语总数 V 的大小,所以 word2vec 本质上是一种降维操作——把词语从 one-hot encoder 形式的表示降维到 Word2vec 形式的表示

第二点,Xin Rong说,Word2Vec学习迭代的是两个权重矩阵:WW'
学习W还好,但学习W'的计算量巨大,所以改用的HS或负采样,但对于喜欢刨根问底的同学来说,到底具体是怎么个“还好”,怎么个“计算量巨大”呢?

  1. 首先,整个网络过程,我们需要做的是用输入的词去预测输出的词
    其中输入层的单词w_{i}使用one-hot来表示的, 即在上图中x_1,x_2,x_3,...,x_V只有x_k为1,其余为0,其中k可以是输入的词在词汇表中的索引下标
    之后就是经过词向量矩阵W连接输入层和隐藏层
    其中由于X中只有一个1,因此经过与W相乘 (相当于取出W中的的第k行,实际也就是输入单词的w_{i}N维的词向量,使用v_{wI}表示,来作为隐藏层的值):
  2. 然后考虑从隐层的h到输出层Y,同样h经过矩阵W'相乘,得到一个V\times 1的向量u

    其中u中的每个元素u_j,就是W'的第j 列用表示, 与h做内积得到:u_{j}=v_{w_{j}}^{\prime T} \cdot h,含义就是词汇表中第j 个词的分数

    我们的目的就是要根据输入词w_{i}去预测输出的词,因此预测的词就取分数最高的即可
    为了方便概率表示,使用softmax将u归一化到[0,1]之间,从而作为输出词的概率, 其实是一个多项分布, 也就是上图中的y

    其中输入向量v_{w}与输出向量都称为词w 的词向量 (当然前面有说,一般使用前者输入向量作为词向量,而非后者)
    至此前向过程完成,就是给定一个词作为输入,来预测它的上下文词,还是比较简单的,属于简化版的神经语言模型,这个过程中需要用到的参数有两个词向量矩阵WW'

    下面就是重点了,介绍如何根据语料库来训练模型、更新参数、得到最终的词向量
  3. 接着,明确训练数据的格式
    对于一个训练样本(w_i, w_o),输入是词wi的one hot编码,其维度定义为V的向量x,模型预测的输出同样也是一个维度为V的向量y
    同时真实值w_o也是用one-hot表示,记为t= [ 0,0,0..1,0,0],其中假设tj^*= 1,也就是说j^*是真实单词在词汇表中的下标,那么根据最大似然或者上面的语言模型,目标函数可以定义如下:

    一般我们习惯于最小化损失函数,因此定义损失函数:
  4. 然后结合反向传播一层层求梯度,使用梯度下降来更新参数
    先求隐层到输出层的向量矩阵W'的梯度

    这里面的y_{j}t_{j}分别是预测和真实值的第j 项,h_i是隐层的第 i
    考虑:,直接对原始求导, 如下:
    先考虑E 的对数部分:

    再看对u_{j^{}}的梯度,综合求导,这个减法可以理解为输出层的第 j 项为预测值与真实值的差
    因此梯度下降更新公式为:

    整合为W'的列向量的形式如下:

    也就是说对每个训练样本都需要做一次复杂度为V的操作去更新W'
  5. 最后,考虑隐层h的更新,其实也是输入层到隐层的矩阵W的更新,继续反向传播,跟神经网络的相同,输出层的V个神经元都会影响h_{i}

    其中W'的第 i 行,这里为了方便书写,令,因此整合成整个隐层的向量h

    得到一个N维的向量,上面已经介绍过,h就是词向量矩阵W的一行:,但是因为X中只有一 个1,因此每次只能更新的一行v_{w_I},其余行的梯度= 0,所以v_{w_I}的更新公式为:

到此为止, 一个训练样本的反向传播训练过程就为止了。 我们可以看到,对于输入层到隐层的矩阵W,我们每次训练只需要更新一行向量即可,而对于隐层到输出层的矩阵W'的所有N\times V个元素都需要更新一遍,毕竟因为是全连接,这里的计算量还是很大的

OK,结果相信大家已经看到了,最终词向量的维度(与隐含层结点数一致)一般情况下要远远小于词语总数 V 的大小,所以 Word2vec 最有价值的是

  • 让不带语义信息的词带上了语义信息
  • 其次把词语从 one-hot encoder 形式的表示降维到 Word2vec 形式的表示

什么?没整明白?没关系! 咱们接下来看一个例子就一目了然了

2.4 CBOW的一个示例:让你对word2vec的训练过程明明白白

上文我们看了输入输出只有一个词的情况,接下来,我们看下Word2Vec中上下文预测中心词的情况,即CBOW

CBOW 模型训练的基本步骤包括:

  1. 将上下文词进行 one-hot 表征作为模型的输入,其中词汇表的维度为 V,上下文单词数量为C ;
  2. 然后将所有上下文词汇的 one-hot 向量分别乘以输入层到隐层的权重矩阵W;
  3. 将上一步得到的各个向量相加取平均作为隐藏层向量;
  4. 将隐藏层向量乘以隐藏层到输出层的权重矩阵W’;
  5. 将计算得到的向量做 softmax 激活处理得到 V 维的概率分布,取概率最大的索引作为预测的目标词

下面以知乎网友 crystalajj 提供的 PPT 为例看一下 CBOW 模型训练流程

假设我们现在的Corpus是这一个简单的只有四个单词的document:{ I drink coffee everyday }
我们选coffee作为中心词,window size设为2,也就是说,我们要根据单词"I","drink"和"everyday"来预测一个单词,并且我们希望这个单词是coffee,而coffee的groundtruth就是coffee一开始的one-hot编码[0,0,1,0]

  1. 将上下文词进行 one-hot 表征作为输入:
    I:                [1,0,0,0]
    drink:         [0,1,0,0]
    coffee:        ?
    everyday:  [0,0,0,1]

  2. 然后将 one-hot 表征结果[1,0,0,0]、[0,1,0,0]、[0,0,0,1],分别乘以:3×4的输入层到隐藏层的权重矩阵W这个矩阵也叫嵌入矩阵,可以随机初始化生成」,比如可以是
    1  2  3  0
    1  2  1  2
    -1 1  1  1

  3. 将得到的结果向量求平均作为隐藏层向量:[1, 1.67, 0.33]

  4. 然后将隐藏层[1, 1.67, 0.33]向量乘以:4×3的隐藏层到输出层的权重矩阵W'这个矩阵也是嵌入矩阵,也可以初始化得到」,得到输出向量:[4.01, 2.01, 5.00, 3.34]

  5. 最后对输出向量[4.01, 2.01, 5.00, 3.34] 做 softmax 激活处理得到实际输出[0.23, 0.03, 0.62, 0.12],并将其与真实标签[0, 0, 1, 0]做比较,然后基于损失函数做梯度优化训练

以上便是完整的 CBOW 模型计算过程,也是 word2vec 将词汇训练为词向量的基本方法之一,总结如下

无论是CBOW 模型还是skip-gram 模型,word2vec 一般而言都能提供较高质量的词向量表达,下图是以 50000 个单词训练得到的 128 维的 skip-gram 词向量压缩到 2 维空间中的可视化展示图:

 可以看到,意思相近的词基本上被聚到了一起,也证明了 word2vec 是一种可靠的词向量表征方式

第三部分 Word2Vec之CBOW

3.1 CBOW的三层正式结构(2013年Mikolov版)

我们已经知道,CBOW(Continuous Bag-of-Word)的目标是:以上下文词汇预测当前词,即用w_{t-2}w_{t-2},w_{t-1},w_{t+1},w_{t+2}去预测w_{t},我们事先定义context(w)表示词w的上下文,即w周边的词的集合

依据2013年Mikolov的原始论文,如之前所述,CBOW包括以下三层:

  • 输入层:包含context(w)中2c个词的词向量v\left(\operatorname{Context}\left(w_{1}\right)\right), v(\operatorname{Context}(w 2)), v(\operatorname{Context}(w 2 c)) \epsilon R^{m},其中,v表示单词的向量化表示函数,相当于此函数把一个个单词转化成了对应的向量化表示(类似one-hot编码似的),2c表示上下文取的总词数,m表示向量的维度;
  • 投影层:将输入层的2c个向量做累加求和;
  • 输出层:按理我们要通过确定的上下文决定一个我们想要的中心词w_t,但怎么决定想要的中心词具体是 {w_1 w_2 .. w_t .. w_N} 中的哪个呢?
    通过计算各个可能中心词的概率大小,取概率最大的词便是我们想要的中心词,相当于是针对一个N维数组进行多分类,但计算复杂度太大,所以输出层改造成了一棵Huffman树,以语料中出现过的词当叶子结点,然后各个词出现的频率大小做权重

    输出层为何要改造成Huffman树呢,相信有的读者已经意识到了,为了用优化后的训练方法Hierarchical SoftMax

3.2 优化策略:从Huffman树到Hierarchical SoftMax

因为前面我们提到不论是2013年Mikolov的原始论文中,还是2014年Xin Rong的文章中

  • 虽然两者从输入层到投影层的权重计算,或输入层到隐藏层的权重W的计算量都还好 (对于每一个训练样本,CBOW更新W的C行,SG更新W其中一行,也就是每次更新有限个词的词向量 )
  • 但前者从投影层到输出层以及后者从隐藏层到输出层的权重W'则不同了

考虑到计算量大的部分都是在隐层到输出层上,尤其是W'的更新。因此word2vec使用了两种优化策略:Hierarchical Softmax 和 Negative Sampling。二者的出发点一致,就是在每个训练样本中,二者都不再使用W'这个矩阵,这里只介绍其中一种优化算法:Hierarchical SoftMax

3.2.1 如何基于Huffman树把预测的复杂度从O(V)降到O(logV)

首先,Hierarchical SoftMax(HS)并不是word2vec提出来的, 而是之前Bengio在2005年最早提出来专门为了加速计算神经语言模型中的softmax的一种方式。HS基于哈夫曼树(一种二叉树)将N维的多分类问题变为了一个log次的二分类的问题

霍夫曼编码 基于二叉树结构,从而把复杂度从从O(V)降到O(logV),其中V是词汇表的大小


总之,将最常见的词放在霍夫曼树的较低层,而不常见的词则放在较高层
如此,模型在做词预测时
\rightarrow  对于常见词,只需要较少的步骤就可以达到预测结果
\rightarrow  而对于不常见的词,虽然需要更多的步骤,但由于出现频率较低,对总体计算量的影响较小

举个例子,比如围绕“我喜欢观看巴西足球世界杯”这句话构造Huffman树,而对其中的“足球”一词进行Huffman编码,当然,在编码的过程中我们得事先定义一套符号:

  • 红色边串起来构成的路径便是足球一词的路径p^{w}
  • 路径p^{w}的长度定义为l^w,即路径p^{w}中包含的结点个数,l^{w}=5
  • p_{l}^{w}表示词w对应的结点,所以p_{1}^{w}, p_{2}^{w}, p_{3}^{w}, p_{4}^{w}, p_{5}^{w}便为路径p^{w}上的五个结点,其中p_{1}^{w}对应根结点
  • d_{j}^{w}定义为路径p^{w}中第j 个结点所对应的编码,所以d_{2}^{w} d_{3}^{w} d_{4}^{w} d_{5}^{w}分别为1 0 0 1

所以足球一词对应的Huffman编码便是1001 (如上所述,频率越大的常见词 编码越短 ),至于图中的\theta_{1}^{w} \theta_{2}^{w} \theta_{3}^{w} \theta_{4}^{w}分别表示p^{w}上4个非叶子结点对应的向量

我们看到,从根结点到叶子结点足球一词,咱们经历了4次分支,每一次分支都相当于一二分类,而每一次分类做的决定:往左走还是往右走,都决定了其编码是0还是1,进一步,可以假定往左走为负 设为1,往右走为正 设为0(当然我们也可以往左为正 设为1,往右走为负 设为0,但本质上这不关键,考虑到2013年原始论文中Word2Vec约定左为负 右为正,咱们便采取这种规则)

  1. 首先,一个结点被分为正类的概率是多大呢?采用logistic回归方法分类(对应的激活函数为sigmoid函数),一个结点被分为正类的概率是

    于是被分为负类的概率自然就是


    对于从根结点出发到达“足球”这个叶子结点所经历的4次二分类(左右右左,相当于负正正负),将每次分类结果的概率写出来就是
    第1次:
    第2次:
    第3次:
    第4次:
    但是,我们要求的是p(足球|Contex(足球)),它跟这4个概率值有什么关系呢?关系就是

    至此,通过w = “足球”的这个例子,Hierarchical Softmax的基本思想其实就已经介绍完了
    总之,如前所述,隐层到输出层的计算量从O(V),利用Huffman树降为了O(logV),相当于本质上咱们把 N 分类问题变成了log(N)次二分类问题
  2. 相当于对于词典中的任意词w,Hufman树中必存在一条从根结点到词w对应结点的路径p^{w}(且这条路径是唯一的)。路径p^{w}上存l^{w}-1个分支,将每个分支看做一次二分类,每一次二分类就产生一个概率,将这些概率乘起来,就是所要求的p(w|Context(w))

    条件概率p(w|Context(w))的一般公式可写为
    p(w \mid \operatorname{Context}(w))=\prod_{j=2}^{l^{w}} p\left(d_{j}^{w} \mid \mathbf{x}_{w}, \theta_{j-1}^{w}\right)
    其中

    或者写成整体表达式
  3. 通过之前对n-gram模型的分析,已知基于神经网络语言模型的目标函数通常取为如下的对数似然函数

    p(w|Context(w))的公式代入上述对数似然函数的目标函数式中,可得

    为下面梯度推导方便起见,将上式中双重求和符号下花括号里的内容简记为L(w, j),即

    至此,得到的这个对数似然函数,便是CBOW模型的目标函数,接下来讨论它的优化:即如何将这个函数最大化
  4. word2vec里面采用的是随机梯度上升法,而梯度类算法的关键是给出相应的梯度计算公式,因此接下来重点讨论梯度的计算
    随机梯度上升法的做法是:每取一个样本(Context(w),w),就对目标函数中的所有相关的参数做一一次更新,观察目标函数L(w, j)易知,该函数中的参数包括向量x_{w}, \theta_{j-1}^{w}, w, j

    为此,先给出函数L(w, j)关于这些向量的梯度


    于是,\theta_{j-1}^{w}的更新公式可写为

    其中,\eta表示梯度
  5. 接下来,接下来考虑L(w, j)关于x_{w}的梯度
    观察之前这个式子 可发现,L(w, j)中关于变量x_{w}\theta_{j-1}^{w}是对称的(即两者可交换位置),因此,相应的梯度也只需在的基础上对这两个向量交换位置就可以了,即


    到这里,细心的读者可能已经看出问题来了:我们的最终目的是要求词典中每个词的词向量,而这里的x_{w}表示的是Context(w)中各词词向量的累加
    那么,如何利用进行更新呢? word2vec中的做法很简单,直接取

    即把贡献到Context(w)中每一个词的词向量上
    这个应该很好理解,既然x_{w}本身就是Context(w)中各词词向量的累加,求完梯度后当然也应该将其贡献到每个分量上去
  6. 最后,以样本(Context(w),w)为例,给出CBOW模型中采用随机梯度上升法更新各参数的伪代码:

    注意,上述步骤3.3和步骤3.4不能交换次序,即\theta_{j-1}^{w}应等贡献到e后再做更新

3.2.2 CBOW中对词向量训练的编码实现

我们再次总结下CBOW中对词向量(对应下面代码中的syn0)的训练步骤,如下

  1. 读取语料,统计词频信息
  2. 构建词典,并初始化Huffman树以及随机初始化每个词的对应向量(维度默认是200)
  3. 以行为单位训练模型(输入文件都在一行上,会按照最大1000个词切割为多行)
  4. 获取当前行中的一个输入样本(当前词向量以及相邻几个的词的词向量)
  5. 累加上下文词向量中每个维度的值并求平均得到投影层向量X(w)(对应代码中的neu1)
  6. 遍历当前词到根节点(输出层的Huffman树)经过的每个中间节点
  7. 计算中间节点对应的梯度 g * 学习速率(与中间节点的权重向量 syn1 和投影层向量 neu1 相关)
  8. 刷新投影层到该中间节点的误差向量(与梯度和中间节点向量相关)
  9. 刷新中间结点向量(与梯度和投影层向量相关)
  10. 刷新上下文词向量(其实就是将误差向量累加到初始向量中)

注意其中的对应关系:词向量syn0 =》投影层向量neu1 =》中间结点向量syn1,流程清晰了,coding则再不是难事:

word = sen[sentence_position];      //从句子中获取当前位置的词
if (word == -1) continue;           //如果获取的词是-1,就跳过这次循环

for (c = 0; c < layer1_size; c++) neu1[c] = 0;      //初始化隐藏层向量neu1
for (c = 0; c < layer1_size; c++) neu1e[c] = 0;     //初始化误差项向量neu1e
next_random = next_random * (unsigned long long)25214903917 + 11;  //生成新的随机数
b = next_random % window;          //将随机数对窗口大小取余,确定中心词的随机窗口大小

//如果使用的是cbow模型
if (cbow) {  
  // in -> hidden,输入层到隐藏层
  cw = 0;      //初始化上下文词的数量为0

  //在中心词周围的窗口中遍历词
  for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {
    c = sentence_position - window + a;      //计算当前词的位置
    if (c < 0) continue;                     //如果当前词的位置小于0,跳过这次循环
    if (c >= sentence_length) continue;      //如果当前词的位置大于句子长度,跳过这次循环
    last_word = sen[c];                      //获取当前位置的词

    if (last_word == -1) continue;        //如果获取的词是-1,跳过这次循环
    //cbow 将上下文词的vector 相加,即计算上下文词的向量和
    for (c = 0; c < layer1_size; c++) neu1[c] += syn0[c + last_word * layer1_size];                      
    cw++;                                 //上下文词的数量加1
  }

  if (cw) {
    //将上下文词向量和进行归一化处理
    for (c = 0; c < layer1_size; c++) neu1[c] /= cw;  

    //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 += neu1[c] * syn1[c + l2];      //计算隐藏层到输出层的传播值
        if (f <= -MAX_EXP) continue;        //如果传播值小于最小值,跳过这次循环
        else if (f >= MAX_EXP) continue;    //如果传播值大于最大值,跳过这次循环

        //计算sigmoid函数的输出
        else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];  

        // 'g' is the gradient multiplied by the learning rate
        // g 是梯度乘以学习速率
        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 * neu1[c];  
      }
    }
}

OK,大功告成。至于对于来自2014年Xin Rong的文章来说,CBOW 用上下文预测这个词的网络结构如下(跟 Skip-gram 相似,只不过:Skip-gram 是预测一个词的上下文):

跟Skip-gram 的模型并联不同,这里是输入变成了多个单词,所以要对输入处理下(一般是求和然后平均),输出的 cost function 不变。

考虑到接下来,也是在输出层改造成了Huffman树,然后用优化后的训练方法Hierarchical SoftMax(HS)便不再赘述了

第四部分 Word2Vec之Skip-gram

上面讨论过Skip-gram的最简单情形,即 y 只有一个词。当 y 有多个词时,网络结构如下:可以看成是 单个x->单个y 模型的并联,cost function 是单个 cost function 的累加(取log之后,图来自2014年Xin Rong的文章)。

 因求解过程与CBOW大同小异,故不再赘述

参考资料与推荐阅读

  1. Tomas Mikolov.(2013). Distributed Representations of Words and Phrases and their Compositionality.
  2. Tomas Mikolov.(2013). Efficient Estimation of Word Representations in Vector Space.
  3. Xin Rong.(2014). word2vec Parameter Learning Explained.
  4. 如何通俗理解word2vec:AI笔试面试题库 - 七月在线
  5. 请详细推导下word2vec(Xin Rong牛论文的解读):AI笔试面试题库 - 七月在线
  6. word2vec 中的数学原理详解word2vec 中的数学原理详解 - peghoty - 博客园
  7. 七月在线AI LAB陈博士在机器学习集训营上讲word2vec的视频
  8. 用图形象的解释word2vec(一图胜千言)
  9. NLP中的预训练技术发展史:从Word Embedding到Bert模型
  10. 「强大的BERT模型:BERT, RoBERTa和ALBERT串讲」的直播回放:http://www.julyedu.com/video/play/87/4846
  11. 如何理解反向传播:AI笔试面试题库 - 七月在线
  12. 神经网络输出层为什么通常使用softmax?
  13. 白话Word2Vec:白话word2vec - 知乎
  14. CBOW的一个例子:深度学习笔记 | 第13讲:word2vec词向量 - Python社区
  15. word2vec是如何得到词向量的?针对该问题的回答2
  16. Quick Notes: Useful Terms & Concepts in NLP: BOW, POS, Chunking, Word Embedding
  17. 秒懂词向量Word2vec的本质
    Word2vec 正是来源于这个思想,但它的最终目的,不是要把 f 训练得多么完美,而是只关心模型训练完后的副产物——模型参数(这里特指神经网络的权重),并将这些参数,作为输入 x 的某种向量化的表示,这个向量便叫做——词向量
  18. 万物皆Embedding,从经典的word2vec到深度学习基本操作item2vec
  19. Word2Vec的开源实现:word2vec/word2vec.c at master · tankle/word2vec · GitHub
  20. 七月在线NLP班中关于Word2Vec的讲义

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;