关键词:自然语言处理(NLP),词向量(Word Vectors),奇异值分解(Singular Value Decompositon),Skip-gram,CBOW,负抽样(Negative Sampling),层次Softmax(Hierachical Softmax),Word2Vec。
这章首先主要介绍自然语言处理(NLP)的概念,以及目前工作中所遇到的难点。紧接着,进一步讨论词语(words)的数值型向量表示概念。最后,我们讨论几种设计词向量的主流方法。
1 自然语言处理简介
我们首先简单讨论下什么是自然语言处理。
1.1 自然语言处理有什么特别之处?
人类语言(自然语言)有什么特别之处?人类语言是用来传递信息,表达意图的特有系统,它不是由任何物理表现产生的。也就是说,它不同于视觉以及其他任何机器学习任务。
大部分词仅仅是非语言实体的符号:这个词是一个映射到特定思想或是事物的符号。
例如,单词rocket
是指一个rocket
的概念,通过拓展可以把它当做rocket
的一个实例。不过,也有一些例外,当我们使用单词和字母表达信号时,例如Whooomppa
。上述提到的语言符号,可以通过向大脑神经传递连续信号,编译为一些形式:声音,手势,书写等。
1.2 自然语言处理任务的一些例子
在自然语言处理中有不同层次的任务,从语音处理到语义和篇章层次的处理。自然语言处理的目的是能够设计一个算法,使得计算机可以理解自然语言,从而去执行一些任务。下面为各种难度等级的任务实例:
简单
- 拼写检查(Spell Checking)
- 关键词搜索(Keyword Search)
- 查找同义词(Finding Synonyms)
中度
- 从网站,文献中解析信息(Parsing information from websites, documents, etc.)
困难
- 机器翻译(Machine Translation) - 例如,将中文文本翻译为英文。
- 语义分析(Semantic Analysis)- 查询语句到底是什么意思?
- 指代消解(Coreference)- 给定文档中'他'或者'它'代指什么?
1.3 如何表示单词?
贯穿所有的自然语言处理任务中,最重要的,也是无可争论的共同点就是我们该如何表示一个单词,让它作为我们模型的输入。为了让自然语言处理任务处理地更好,首先我们需要定义单词之间的相似性和差异性概念。使用词向量(word vectors),我们可以很容易地把这种特性编译到向量中去(使用距离方法,例如杰卡德距离,与余弦距离,欧几里得距离等等)。
2 词向量
英语中大约有1300万字符,但是,它们之间都是无关的,相互独立的吗?比如,猫科动物(feline)和猫(cat),旅馆(hotel)和汽车旅馆(motel)?答案是并不都无关联的。因此,我们希望把这些单词字符一一编译为向量,来表示‘单词’(word)空间里的一个点。我们有很多的理由认为单词字符的向量化至关重要,那么最直观的理由就是:或许真的存在一些N维空间(例如N << 1300万)能够充分编译我们语言的所有语义。每一个维度将会编译我们言语中所传达的含义。例如,语义维度可以指示时态,单复数,以及性别。
让我们来研究一下我们的第一个词向量,可以说是最简单的词向量:one-hot vector。one-hot vector 使用只有一个元素为1,其余都为0的向量,1位于该单词所在的索引位置。在这个概念中,|V|是我们词汇的大小。以这种方式编码的词向量形如下面的单词表示形式:
我们把每一个单词(word)表示为完全独立的实体。就像我们之前所谈论的,这种单词的表示方法并没有向我们提供任何相似(词与词之间的相似度)的概念。例如,
所以,或许我们可以尝试削减R|V|空间的大小,从而找到一个子空间,用来编码词与词之间的联系。
3 基于奇异值分解(SVD)的方法
使用这类方法去寻找词嵌入(word embeedings),或者说是词向量(word vectors),我们首先需要遍历一个大规模的数据集,以矩阵X的方式计算并存储词共现数(word co-occurrence counts),然后对X进行奇异值分解,得到USVT。接着,我们使用U的行向量作为我们词典中所有单词的词嵌入(词向量)。让我们讨论下关于X的选择。
3.1 词-文档矩阵(Word-Document Matrix)
作为第一次尝试,我们大胆地猜想相关的单词将会出现同一篇文档中。例如,banks
,bonds
, stocks
, money
等单词很有可能一起出现。但是banks
, octopus
, banana
,以及hockey
可能不会始终一起出现在同篇文档中。我们基于这个事实,通过以下方法建立一个词-文档矩阵X:遍历数十亿的文档,单词i在文档j中每出现一次,便将元素Xij加上1。显然这是一个非常巨大的矩阵(R|V|×M),它与文档的数量(M)成比例。所以,或许我们可以尝试下一些更好的方法。
3.2 基于窗口的共现矩阵(Window based Co-occurrence Matrix)
上面的逻辑同样适于这里,不过矩阵X存储的是单词与附近单词的共现,从而得到一个关联矩阵(affinity matrix)。在这种方法中,我们计算感兴趣的那个单词特定大小窗口里的每个单词出现的次数。同样,我们计算语料库中所有单词的这个数目
。下面,我们展示一个样例。假定我们的语料库中仅包含三个句子,窗口大小为1:
- I enjoy flying.
- I like NLP.
- I like deep learning.
由此,形成的计数矩阵为:
3.3 共现矩阵奇异值分解(Applying SVD to the cooccurrence matrix)
现在,我们对X进行奇异值分解,观察奇异值(矩阵S的对角线元素),然后根据我们期望捕获到的方差占比在某个索引k处将其截断:
然后,我们将U1:|V|,1:k的子矩阵作为我们的词嵌入矩阵。这将会为我们词汇表中每个单词提供一个k维向量表示形式。
总结起来,一共分下面几个步骤:
- 生成一个|V|×|V|共现矩阵X
- 对X进行奇异值分解,得到X = USVT
- 选择U的前k列,得到一个k维词向量
- 被前k维所捕获到的方差为:上图中的表达式
对X进行奇异值分解:
通过选择前k个奇异向量进行降维:
这两种方法都给我们提供了足以编码语义和句法(词性)信息的词向量,但也存在了许多其它问题:
- 新单词的频繁添加以及语料库大小的变换使得矩阵的维数变换频繁。
- 大部分词组不存在共现使得矩阵特别稀疏。
- 矩阵维度过高,通常为10^6 × 10^6。
- 训练的开销过大,呈二次型(如执行奇异值分解)。
- 需要一些技巧解决词频的急剧失衡。
针对上述存在的一些问题,有以下解决方法:
- 忽略一些虚词,如“the”,“he”,“has”等。
- 利用倾斜窗口——根据文档中词与词之间的距离来加权共现次数。
- 使用Pearson相关矩阵,并把负计数设置为0,而不是使用原来的计数方式。
在下面的章节中,基于迭代的方法能够以更优雅的方式解决以上的一些问题。
基于迭代的方法 - Word2Vec
我们可以尝试创建一个模型,一次性只能学习一次迭代,最终可以根据上下文语境给出一个词出现的概率;而不是像之前一样计算和存储一些庞大数据集的全部信息(可能是数10亿句)。
这一方法的主要思路就是:设计一个模型,用该模型的参数来表示词向量,然后基于语料库进行训练。在每一次迭代中,我们评估模型的误差,然后对参数进行更新。我们称这种方法为‘反向传播’误差。模型和任务越简单,模型训练的速度越快。
[Collobert et al., 2011]设计NLP模型的第一步就是将每一个单词转化为向量的形式。在一些特殊任务中(命名实体识别,词性标注等),他们不仅仅训练模型的参数,还训练更好的词向量以追求更高的性能。
接下来,我们主要介绍一个更简单的,更前沿的,基于概率方法的模型:word2vec。Word2vec是一个软件包,主要包括:
2个算法: continuous bag-of-words (CBOW)和skip-gram。CBOW旨在根据上下文窗口语境预测中间词。Skip-gram与之相反,他根据中心词预测该词的上下文语境。
2个训练方法:negative sampling和hierarchical softmax。negative sampling通过负抽样来定义一个目标词。然而,hierarchical softmax通过使用一个高效的树结构计算所有词汇的概率来定义目标词。
4.1 语言模型(Language Models)
首先,我们需要创建一个可以计算字符序列出现概率的模型。让我们看下下面的例子:
"The cat jumped over the puddle."
一个好的语言模型可以赋予这个句子高的概率。因为这句话符合句法和语义,通俗一点,他是一句人话。相反,应该赋予"stock boil fish is toy"这句话低的概率,因为这句话不是人话,没有任何意义。我们可以用以下数学表达式来表示给定n个单词序列的概率:
我们假定每个单词的出现时相互独立的,那么上面的一元模型的概率可以拆散为以下形式:
然而,我们知道这有一些荒唐,因为下一个词的出现高度依赖于先前的单词序列。有可能导致那些愚蠢的,不像人话的句子被赋予的分数较高。因此,也许我们可以让序列的概率取决于序列中一个单词的成对概率和它旁边一个单词的概率。我们称之为二元语言模型(bigram model),表示如下:
当然,这还是有点幼稚,因为我们只考虑一对相邻的词,而不是计算整个句子。但是,我们将会看到,这种表示方法使我们能够前行的更远。通过一个上下文大小为1的词矩阵,我们基本上能够获取这些成对词语的概率。但是,这需要计算和储存一个海量数据集的全部信息。
既然我们已经明白如何理解一个拥有概率性质的字符序列,那么接下来,我们将会介绍一些可以自主学习这些概率的模型。
4.2 CBOW模型
一种方法是把{'The', 'cat', 'over', 'the', 'puddle'}作为上下文,来预测中心词语'jumped'。我们称这类模型为Bag of Words (CBOW)模型。
让我们详细地讨论下CBOW模型。首先,我们设定一些已知的参数。这些已知参数为用于表示单词的one-hot向量。我们用x(c)表示输入的one_hot向量(上下文),用y(c)表示输出。在CBOW模型中,只有一个输出,所以我们称y为已知中心词的one-hot向量。接下来,让我们定义模型中未知的参数。
我们创建两个矩阵,v∈Rn×|V|和u∈R|V|×n。n为词嵌入向量的大小,v为输入词矩阵,其中,当wi为模型的一个输入时,v的第i列为单词wi的n维词嵌入向量。我们把这个n×1向量定义为vi。相似的,u为输出词矩阵,当wi模型的一个输出时,u的第j行为单词wj的n维词嵌入向量。我们把u的第j行定义为uj。请注意:我们实际上为每个单词wi学习了两个词向量(输入词向量vi和输出词向量ui)。
CBOW模型参数:
- wi: 词汇V中的单词i
- v∈Rn×|V|: 输入词矩阵
- vi: v的第i列,单词wi的输入词向量表示
- u∈R|V|×n: 输出词矩阵
- ui: u的第i列,单词wi的输出词向量表示
我们将模型的工作流程分解为以下几个步骤:
- 获取输入的one-hot向量,窗口大小为m : (x(c-m),..., x(c-1), x(c+1),..., x(c+m)∈R|V|)。
- 我们得到输入窗口语料的词嵌入向量 (v(c-m)=vx(c-m), v(c-m+1)=vx(c-m+1), ... ,v(c+m)=vx(c+m)∈Rn)
- 求取这些向量的平均值,得到:
- 生成一个分数向量(score vector):z,由于向量之间越相似,其点积越高,所以相似的单词将会离得更近以得到更高的分数。
- 利用Softmax函数将分数转换为概率的形式:
- 我们期望预测的概率y_hat能够与真实概率y相匹配,即巧好是目标单词的one-hot向量。
既然我们已经了解了当我们拥有v和u两个矩阵后,我们的模型是怎么工作的。那么,我们该怎么去学习获取这两个未知参数矩阵呢?这时,我们需要创建一个目标函数。通常,当我们从某种真实概率中学习概率时,我们会借助信息理论来度量两个分布之间的距离。这里,我们使用最常用的距离/损失度量,交叉熵。在离散情况下,使用交叉熵的灵感来自于下面的损失函数:
让我们考虑下CBOW这个案例,y为one-hot向量。因此,上面的损失函数可以简化为:
在这个公式中,c是指目标单词one-hot向量为1的索引。假设我们的预测非常完美时,我们可以算得:
因此,对于一个完美的预测,不会得到惩罚或者损失。假设我们的预测很差,为0.01,则我们可以得到:
因此,我们可以看出对于概率分布,交叉熵为我们提供了一个很好的度量距离(误差)的方法。因此,我们把我们模型的优化目标设置为:
我们采用随机梯度下降法(stochastic gradient descent)更新所有相关的词向量uc和vj。
4.3 Skip-Gram模型 (CBOW)
另一种方法是创建一个模型,给定一个中心单词'jumped',该模型能够预测周边单词(上下文):'The', 'cat', 'over', 'the', 'puddle'。这里,我们称单词'jumped'为语境(context)。我们称这类模型为Skip-Gram模型。
Skip—Gram模型和CBOW模型很相似,不过这里我们把x和y进行了交换,即CBOW中的x现在变为了y,反之亦然。我们用x表示输入的one-hot向量(中心单词),尽管这里只有一个单词。用y(j)表示输出向量。v和u的定义和CBOW中的定义相同。
我们把Skip-Gram模型的工作流程拆分为以下6个步骤:
- 获取中心单词的one-hot输入向量x∈R|V|。
- 求取中心单词的词嵌入向量vc =vx ∈ Rn。
- 计算分数向量z = uvc。
- 通过Softmax函数将分数向量转换为概率:
注意:每个上下文词的概率为:
- 我们期望计算出的概率和真实概率y(c-m),..., y(c-1), y(c+1),..., y(c+m)相匹配。
正如CBOW模型那样,我们同样也需要定义一个目标函数来衡量我们的模型。最重要的一个区别是,在Skip-Gram模型中我们引入朴素贝叶斯假设来分解概率。简单地说,就是一个强条件独立假设。换句话说,给定中心单词,所有的输出单词完全相互独立。
通过目标函数,我们计算未知参数的相应梯度,以及在每次迭代中通过随机梯度下降法更新这些未知参数。
注意:
注:在Skip-Gram模型中,只输出了一个概率向量。Skip-Gram模型同等对待每一个背景词:模型计算每一个背景词的出现概率,和背景词到中心词的距离无关。
4.4 负抽样(Negative Sampling)
接下来,让我们探讨一下目标函数。在$|V|$维数据上进行求和的计算量巨大!目标函数的任何更新后者计算将会消耗大量的时间。我们可以尝试通过近似的方法去提高效率。
在每次训练过程中,我们可以仅仅抽取几个负样本,而不是循环遍历整个词汇表!我们从一个噪声分布({% math %}P_n(w){% endmath %})中抽样,其概率与词汇表中词频相匹配。如果把负抽样机制应用到模型中,我们需要调整以下几点:
- 目标函数
- 梯度
- 更新规则
虽然负抽样基于Skip-Gram模型,但实际上它优化了一个不同的目标函数。(w, c)(w, c分别为单词(word)和上下文(context))是否真的来自训练数据集(即w, c是否分别对应为训练集的中心词(输入)和背景词(输出)?记P(D = 1|w, c)为(w, c)来自语料库数据的概率,相应的P(D = 0|w, c)为(w, c)不是来自语料库的概率。首先,我们用sigmoid函数来模拟P(D = 0|w, c):
说明:sigmoid函数为softmax的1维特殊形式,可以用于表示概率。如下图所示:
现在,我们建立了一个新的目标函数,试图当(w, c)来自语料库时最大化概率P(D = 1|w, c),当(w, c)不是来自语料库时,最大化概率P(D = 0|w, c)。我们采用最大似然估计(MLE)来计算这两个概率。(这里,我们记θ为模型的参数,在word2vec模型里对应着v和u。)
所以最大化似然函数等同于最小化下面的负对数似然函数:
记Dfalse为错误(false)/负(negative)语料库。当我们有一个句子,例如"stock boil fish is toy"。不自然的句子出现的概率应该很低。我们可以从词库里随机抽取生成负样本Dfalse。
对于Skip-Gram模型,给定中心词c观察到上下文c-m+j的目标函数为:
对于CBOW模型,给定(上下文)背景词向量,观察到中心词uc的目标函数为:
其中,负样本服从Pn(w)分布。但是Pn(w)该是什么样的呢?经过多次地讨论以及实验如何做出最好的近似,似乎一元文法模型(Unigram Model)的3/4次方的效果最好。为什么是3/4呢?下面的例子可能会帮助你理解一下:
is: 0.93/4 = 0.92
Constitution: 0.093/4 = 0.16
bombastic: 0.013/4 = 0.032
可以看出:"Bombastic"现在被抽到的可能性是之前的3倍,然而"is"仅仅提升了一点。
4.4 层次Softmax(Hierarchical Softmax)
在实践中,层次softmax对低频单词的效果较好。然而负抽样对高频单词以及低维向量的效果较好。
层次softmax采用二叉树展示词汇表中的所有单词。树的每一个叶子节点代表一个单词,并且从根节点到叶节点的路径是唯一的。在这个模型中,树的每一个节点(除了根节点和叶节点)都对应着一个向量,这个向量需要模型去学习。
给定一个向量wi,单词w的概率P(w | wi)等于从根节点随机游走,最终到达叶节点w的概率。这种计算概率的方式的主要优点是花销仅仅为O(log(|V|))。
用L(w)表示从根节点到叶节点w的节点数。例如(如下图所示):
L(w2)为3。用n(w, i)表示路径中第i个节点,对应的向量表示为vn(w,i)。所以n(w, 1)为根节点,n(w, L(w))为w的父节点。现在,对于每一个内层节点n,我们任意挑选一个子节点,用ch(n)表示(例如,总抽取左节点)。然后,我们可以计算概率:
σ(.)为sigmoid函数。
这个公式有点复杂,让我们来好好地研究一下:
首先,我们根据从根节点(n(w,1))到叶节点(w)的路径形状计算每一项的乘积。如果我们假设ch(n)总是n的左节点,那么当路径沿着左侧行走时,[n(w, j+1) = ch(n(w, j))]为1,当沿着右侧时,为-1。
此外,[n(w, j+1) = ch(n(w, j))]起到了正则化的作用(概率的公理化定义)。即在节点n处,向左和向右的概率相加为1:
同时正则化就像在原始softmax中那样也保证了:
最后,我们使用点积计算输入向量vwi和每个内节点向量vTn(w,j)的相似性。让我们看下示例:在上图中选取w2,我们必须通过两个左节点和一个右节点才能从根节点到达w2,所以:
为了训练模型,我们的目标依然是最小化负对数释然-log P(w| w_i)。但是我们在训练过程不再更新每个单词的输出向量,而是更新二叉树中从根节点到叶节点路径上的节点单词的向量。