综述
自然语言处理概述
自然语言处理(NLP)是为了让计算机理解自然语言。NLP和编译器是有联系的,人类分析编译器的洞察力也可以被应用到NLP上面,不过由于编程语言是无二义性的,或者可以通过简单的规则,比如优先级,消除二义性,如此一来,研究如何设计编译器,更多的是研究精确的文法。相比较而言,自然语言的意思和形式会灵活变化。不过可以从人的思维角度设计编译器,也可以从人的思维角度设计NLP。
语言是由词语组成的,那么无论是计算机还是人,要理解自然语言都需要先理解词语,不过注意,如果意识无法掌控句子的产生过程(这是很可能的,意识无法知道一句话怎么产生,只是知道一句话被产生、被说出),那么对于意识来说,句子可能不是由词语拼接起来的,而是大脑制造的一种模式,意识对于句子的分析是事后的,而不是反映了大脑的实际运行机制。
实际上,任何一个自然语言可以变为非二义性的语言,对于意识而言,自然语言是无二义性的,意识很轻易的把语言理解为知觉的对应物,并且做出判断:词语的意义来源于经验。但这不过是大脑的无意识处理的强大而已,大脑在意识之外处理了二义性,这意味着如果要设计程序理解语言,这个程序是理解大脑,而不是意识本身,也不怎么需要意识的直觉、理解。所以不是自然语言多么巧妙,而是大脑处理语言的方式实在是巧妙,大脑自动从上下文推断出多义性的词语在一个确定的环境中代表什么意思。
另外,即使意识通过反思推断出,词语的意义来源于知觉,人类通过各种知觉学习新的词语,但是如何记忆知觉这是意识之外的,所以计算机如何实现这种直觉,这是复杂的。这种程序的设计依旧要模仿大脑,而不是意识的直觉。
但是无论如何,计算机需要表征词语,人类也需要,不过在意识看来,词语的表示和词语符号感受联系十分紧密,比如视觉、听觉、触觉。但是,词语的符号和语义的关系是很小的,意识已经在这种掌控之中丧失了对于语义的把握了。
对词语进行表示,显然需要展示符号的差异,一种自然甚至必须的表示,是为每个词语进行编号,这样每个词语对应一个实数。这种编号是合理的,因为词语的符号本身就是一种记法,记为实数就行了。当然,单个实数的表示是有精度限制的,所以,为了方便,也可以把词语表示为实数对,这可以被视为向量。向量的好处在于引入了几何结构,这对于意识而言,操作更为直观,更加容易启发人的思维。
这样一来,自然语言的处理就是单词向量的处理,抹杀了意识的理解模式,虽然这是不关键的。
怎么建立向量之间的联系?
比如同义词词典,一般词典运用一些词语定义或者说明一个词语,这可以被视为一种理解方式。同义词词典则是把意思相近的词语放在一组,与此同时,进行概念的分层,以此形成词语网络。WordNet是最为著名的同义词词典。这种方式的缺点在于:
- 新的词语可能出现和旧的词语可能被遗忘,另外词语的含义也会随着时间的推移而变化,但是词典没办法自己更新。
- 人力成本高。
- 无法表示单词的微妙差异。
根据人类产生的句子、文章可以自动推断词语之间的联系,比如在一句话、一段话或者一篇文章中,两个词语一起出现,这意味着词语之间的关系比较接近,这里要注意一些无含义的词语比如‘的’ 、‘是’ 、‘之’ 这样的词语,这种词语几乎可以被忽略,或者使用一种算法把词语本身出现的频率考虑进来,比如计算词语之间的关系时,计算两个词语共同出现频率除以单独出现频率。
但是这种方式会让向量维数变得很大——当词语数量很多的时候,显然不太可行,况且这和人类大脑也不相似。虽然由于向量很可能比较稀疏——毕竟一个词语不可能与所有词语有着紧密的关系,可以采取一些措施进行降维,比如SVD,但是维数依旧很大,而且对于很多维的矩阵进行降维几乎是不可能的,因为需要很长时间。
解决维数过大的问题是不难的,毕竟词语被向量表示,并不需要每一个词语占有一个维度,这实在太过于奢侈,因为一个维度是实数范围的,它可以存储的信息是远远超出一个比特,即一个词语的表征的。
所以可以直接把词语编码为低维的向量,而不是先设置为超高维,再进行降维。
此时应该怎么找到向量之间的关系呢?须知,现在一个维度不是单独属于一个词语的了,那么直接统计就不太可行。现在能够使用内积计算词语之间的距离,可以把向量视为参数,对参数进行优化。
word2vec采用两种策略构造这种优化。一种策略是,输入是上下文的两个单词,要预测中间的单词是什么(输出一个数字表示哪个词语,通过多分类衡量损失),这样可以设定一个目标,使得优化得以进行。另外一种策略是输入一个单词,预测左右两个单词。
即使如此,词语数很大时,对全部词语进行判断也需要很久时间,这可以通过直接判断词语是不是训练数据指定的,即变为二分类。于此同时要使用负采样,对于不正确的答案要给予训练,这可以通过频率采样几个不正确的词语。把损失加起来,就可以实现参数改变,最终向量十分满足这种上下文模式。
这种方式的缺点是,虽然考虑了词语之间的关系,但是只有左右相邻的几个词语,虽然这个范围可以任意大,但是过大也不好训练,并且,人类可以构造很复杂的词语,使得代词指向很远的位置的词语。另外这个方式没有考虑顺序问题,左右的词语是对称处理的。这个缺点虽然可以通过拼接词语向量来解决,但是这依旧会让复杂度变大。
实际上这种词向量已经表现很好了,不过在一些场景不那么令人满意。有人使用循环神经网络,实现编码器和解码器,然后使用Attention机制使用更多、更加专注特定的信息来解决这些问题。
除了对于词语进行编码,还可以直接对于句子进行编码,这让计算量变得更大,但是似乎是打破瓶颈的一种方式,这被成为思想向量。
和人对比
虽然词向量无疑十分强大,但是词向量究竟能达到什么地步,没有人能够给出分析,毕竟词向量学习的是人类书写的句子,在句子中发现联系词语之间的关系。那么怎么知道词语的关系决定了词语本身的全部呢,有没有可能,人类使用词语的方式和这种词向量的方式并不一致?
首先,人不可能根据这么大量的数据来建立词语之间的关系——无论如何,这些数据是通过无数人创造出来的,而且就意识的体验而言,试想一下小孩如何掌握母语,虽然学会一门语言需要不断使用,但是词语是一步步掌握的,不需要一开始就掌握所有词语,词语也能慢慢扩展,与其说人类一下子建立词语之间的联系,不如说大脑掌握了一种模式,这种模式使得学过的词语之间拥有联系,而且更加容易学会一个新的词语,人类学习一门语言,掌握词语的速度会越来越快。
与其说是人学习语言,不如说人决定语言。人类觉得说话简单,看懂句子很容易,这是大脑自动处理的结果——没人知道自己想的下一句是什么话,意识也只是从经验上理解单个的词语,把它和知觉对应起来,但是实际上很有可能这是大脑分析之后给出的解释,大脑可能不是根据这种知觉意义进行分析的。
大脑产生句子容易似乎只是句子被说了很多次,这被编码了。分析句子也是如此。一个常说的句子很容易被说出,一个很新的句子会很难被说出来,说起来大脑只是组合句子——这是不精确的,但是人们却认为大脑具有弹性,这种不精确被视为自然语言的神奇,这是巨大的讽刺!
存在这样一种情况,自然语言具有歧义性不过是因为大脑产生句子也是不精确运作的,人类为什么觉得一个不严格的句子是合理的,为什么一个不合理的句子可以被别人理解?这并不难以解释,因为如果大脑解释一切,所谓合理是由大脑决定的,所以合理是不需要解释的,至于别人的大脑怎么理解这一切,这也不难解释,毕竟如果假设大脑之间的结构是类似的,大脑之间是等价的,一个大脑产生的句子被别人理解和被自己理解是一样的,所以只要一个大脑对自己产生的句子感到满意,没有什么可以阻止别人的大脑会认为这个句子很奇怪。
中文
分词
齐夫定律:在自然语言的语料库里,一个单词出现的频率与它在频率表里的排名成反比。所以,频率最高的单词出现的频率大约是出现频率第二位的单词的2倍,而出现频率第二位的单词则是出现频率第四位的单词的2倍。
从而可以使用词典分词。
中文需要分词,以下记录简单的分词算法。
正向最长匹配:正向匹配,输出最长的字组成的单词。
逆向最长匹配:正向匹配,输出最长的字组成的单词。
双向最长匹配:执行两种匹配,输出最短词数的方式,如果词数一样,则输出单字更少的方式(单字词语远远少于非单字的词语);如果还是一样,优先选择逆向的。
从符号角度(偏向英文)
这里的大部分内容基于《深度学习进阶:自然语言处理》
基于计数的方法
基于计数的方法的目标就是从富有实践知识的语料库中,自动且高效地提取本质。**语料库(corpus)**就是大量的文本数据。不过,语料库并不是胡乱收集数据,一般收集的都是用于自然语言处理研究和应用的文本数据。语料库中包含了大量的关于自然语言的实践知识,即文章的写作方法、单词的选择方法和单词含义等。
自然语言处理领域中使用的语料库有时会给文本数据添加额外的信息。比如,可以给文本数据的各个单词标记词性。在这种情况下,为了方便计算机处理,语料库通常会被结构化(比如,采用树结构等数据形式)。
“某个单词的含义由它周围的单词形成”,称为分布式假设(distributional hypothesis)。分布式假设所表达的理念非常简单。单词本身没有含义,单词含义由它所在的上下文(语境)形成。上下文的大小(即周围的单词有多少个)称为窗口大小(window size)。
在关注某个单词的情况下,对它的周围出现了多少次什么单词进行计数,然后再汇总。这里,我们将这种做法称为“基于计数的方法”,在有的文献中也称为“基于统计的方法”。
汇总所有单词的共现单词成为一个矩阵,矩阵行和列都是单词,对应元素表示两者同时出现次数。各行对应相应单词的向量。这被称为共现矩阵(co-occurence matrix)。可以使用余弦衡量单词之间的相似度。
共现矩阵的元素表示两个单词同时出现的次数。但是,这种“原始”的次数并不具备好的性质。比如,我们来考虑某个语料库中the和car共现的情况。在这种情况下,我们会看到很多“…the car…”这样的短语。因此,它们的共现次数将会很大。另外,car和drive也明显有很强的相关性。但是,如果只看单词的出现次数,那么与drive相比,the和car的相关性更强。这意味着,仅仅因为the是个常用词,它就被认为与car有很强的相关性。
为了解决这一问题,可以使用点互信息(Pointwise Mutual Information, PMI)这一指标。对于随机变量x和y,它们的PMI定义如下:PMI(x,y)=log2P(x,y)P(x)P(y)PMI(x,y)=log_2\frac{P(x,y)}{P(x)P(y)}PMI(x,y)=log2P(x)P(y)P(x,y)。PMI也有一个问题。那就是当两个单词的共现次数为0时,log20log_20log20=-∞。为了解决这个问题,实践上我们会使用正的点互信息(Positive PMI, PPMI):PPMI(x,y)=max(0,PMI(x,y))PPMI(x,y)=max(0,PMI(x,y))PPMI(x,y)=max(0,PMI(x,y))。
但是,这个PPMI矩阵还是存在很多的问题:
- 随着语料库的词汇量增加,各个单词向量的维数也会增加。如果语料库的词汇量达到10万,则单词向量的维数也同样会达到10万,处理10万维向量是不现实的。
- 这个矩阵中很多元素都是0。这表明向量中的绝大多数元素并不重要,也就是说,每个元素拥有的“重要性”很低。
- 这样的向量也容易受到噪声影响,稳健性差。
对于这些问题,一个常见的方法是向量降维。降维(dimensionality reduction),顾名思义,就是减少向量维度。但是,并不是简单地减少,而是在尽量保留“重要信息”的基础上减少。我们要观察数据的分布,并发现重要的“轴”。
降维的方法有很多,比如奇异值分解(Singular Value Decomposition,SVD)。可以使用NumPy的linalg模块中的svd方法。
如果矩阵大小是N,SVD的计算的复杂度将达到O($N^3 $)。这意味着SVD需要与N的立方成比例的计算量。因为现实中这样的计算量是做不到的,所以往往会使用Truncated SVD等更快的方法。Truncated SVD通过截去(truncated)奇异值较小的部分,从而实现高速化。作为另一个选择,可以使用sklearn库的Truncated SVD。
Penn Treebank语料库(以下简称为PTB)经常被用作评价提案方法的基准。PTB语料库在word2vec的发明者托马斯·米科洛夫(Tomas Mikolov)的网页上有提供。这个PTB语料库是以文本文件的形式提供的,与原始的PTB的文章相比,多了若干预处理,包括将稀有单词替换成特殊字符(unk是unknown的简称),将具体的数字替换成“N”等。
基于推理的方法
如果词汇量超过100万个,那么使用基于计数的方法就需要生成一个100万×100万的庞大矩阵,但对如此庞大的矩阵执行SVD显然是不现实的。
基于推理的方法使用了推理机制(神经网络实现),它也使用了分布式假设。基于推理的方法使用神经网络,通常在mini-batch数据上进行学习。这意味着神经网络一次只需要看一部分学习数据(mini-batch),并反复更新权重。神经网络的学习可以使用多台机器、多个GPU并行执行,从而加速整个学习过程。
基于推理的方法的主要操作是“推理”。当给出周围的单词(上下文)时,预测当前处会出现什么单词,这就是推理。基于推理的方法引入了某种模型,我们将神经网络用于此模型。这个模型接收上下文信息作为输入,并输出(可能出现的)各个单词的出现概率。在这样的框架中,使用语料库来学习模型,使之能做出正确的预测。另外,作为模型学习的产物,我们得到了单词的分布式表示。这就是基于推理的方法的全貌。
没有偏置的全连接层相当于在计算矩阵乘积。在很多深度学习的框架中,在生成全连接层时,都可以选择不使用偏置。
这里使用由原版word2vec提出的名为**continuous bag-of-words(CBOW)**的模型作为神经网络。word2vec一词最初用来指程序或者工具,但是随着该词的流行,在某些语境下,也指神经网络的模型。正确地说,CBOW模型和skip-gram模型是word2vec中使用的两个神经网络。关于这两个模型的差异之后有讨论。
CBOW模型的输入是上下文。这个上下文用[‘you’, ‘goodbye’]这样的单词列表表示。将其转换为one-hot表示,以便CBOW模型可以进行处理。
同样将语料库中的目标单词作为目标词,将其周围的单词作为上下文提取出来。我们对语料库中的所有单词都执行该操作(两端的单词除外),可以得到contexts(上下文)和target(目标词)。contexts的各行成为神经网络的输入,target的各行成为正确解标签(要预测出的单词)。上下文和目标词的数量是任意的,目标词一般为一个,所以采用单数。
例如,一个CBOW网络有两层,输入,中间,输出,最后使用Softmax函数将得分转化为概率,再求这些概率和监督标签之间的交叉熵误差,并将其作为损失进行学习。输入到中间、中间到输出都是全连接。
输入是两个1×7的单词向量(上下文两个单词one-hot,一共7个单词),从输入层到中间层的变换由全连接层(权重是Win)完成,可以把两个向量单独计算,把结果取平均形成中间层。此时,全连接层的权重Win是一个7×3的矩阵(WoutW_{out}Wout是3×7),权重Win的各行保存着各个单词的分布式表示。通过反复学习,不断更新各个单词的分布式表示,以正确地从上下文预测出应当出现的单词。令人惊讶的是,如此获得的向量很好地对单词含义进行了编码。这就是word2vec的全貌。
中间层的神经元数量比输入层少这一点很重要。中间层需要将预测单词所需的信息压缩保存,从而产生密集的向量表示。这时,中间层被写入了我们人类无法解读的代码,这相当于“编码”工作。而从中间层的信息获得期望结果的过程则称为“解码”。这一过程将被编码的信息复原为我们可以理解的形式。
word2vec中使用的网络有两个权重,分别是输入侧的全连接层的权重(Win)和输出侧的全连接层的权重(Wout)。一般而言,输入侧的权重Win的每一行对应于各个单词的分布式表示。另外,输出侧的权重Wout也同样保存了对单词含义进行了编码的向量,只是输出侧的权重在列方向上保存了各个单词的分布式表示。
那么,我们最终应该使用哪个权重作为单词的分布式表示呢?这里有三个选项。A.只使用输入侧的权重 B.只使用输出侧的权重 C.同时使用两个权重方案
A和方案B只使用其中一个权重。而在采用方案C的情况下,根据如何组合这两个权重,存在多种方式,其中一个方式就是简单地将这两个权重相加。就word2vec(特别是skip-gram模型)而言,最受欢迎的是方案A。许多研究中也都仅使用输入侧的权重Win作为最终的单词的分布式表示。遵循这一思路,我们也使用Win作为单词的分布式表示。有人通过实验证明了word2vec的skip-gram模型中Win的有效性。另外,在与word2vec相似的GloVe方法中,通过将两个权重相加,也获得了良好的结果。
CBOW模型进行的处理是,当给定某个上下文时,输出目标词的概率。这里,我们使用包含单词w1,w2,⋅⋅⋅,wTw_1, w_2,···, w_Tw1,w2,⋅⋅⋅,wT的语料库。对第t个单词,考虑窗口大小为1的上下文。用数学式来表示当给定上下文wt−1和wt+1时目标词为wtw_{t-1}和w_{t+1}时目标词为w_twt−1和wt+1时目标词为wt的概率。使用后验概率表示为P(wt∣wt−1,wt+1)P(w_t|w_{t-1}, w_{t+1})P(wt∣wt−1,wt+1),
交叉熵误差函数是L=−∑ktklog ykL=-\sum_kt_klog\space y_kL=−∑ktklog yk,yky_kyk表示第k个事件发生的概率。tkt_ktk是监督标签,它是one-hot向量的元素。这里需要注意的是,“wtw_twt发生”这一事件是正确解,它对应的one-hot向量的元素是1,其他元素都是0(也就是说,当wtw_twt之外的事件发生时,对应的one-hot向量的元素均为0)。考虑到这一点,可以推导出下式:
L=−log P(wt∣wt−1,wt+1)L=-log\space P(w_t|w_{t-1}, w_{t+1})L=−log P(wt∣wt−1,wt+1)
这也称为负对数似然(negative log likelihood)。这是一笔样本数据的损失函数。如果将其扩展到整个语料库,则损失函数可以写为:
L=−1T∑t=1Tlog P(wt∣wt−1,wt+1)L=-\frac{1}{T}\sum_{t=1}^Tlog\space P(w_t|w_{t-1}, w_{t+1})L=−T1∑t=1Tlog P(wt∣wt−1,wt+1)
CBOW模型学习的任务就是让这个损失函数尽可能地小。那时的权重参数就是我们想要的单词的分布式表示。这里,我们只考虑了窗口大小为1的情况,不过其他的窗口大小(或者窗口大小为m的一般情况)也很容易用数学式表示。
word2vec有两个模型:一个是我们已经讨论过的CBOW模型;另一个是被称为skip-gram的模型。CBOW模型从上下文的多个单词预测中间的单词(目标词),而skip-gram模型则从中间的单词(目标词)预测周围的多个单词(上下文)。
skip-gram模型的输入层只有一个,输出层的数量则与上下文的单词个数相等。因此,首先要分别求出各个输出层的损失(通过Softmax with Loss层等),然后将它们加起来作为最后的损失。现在,我们使用概率的表示方法来表示skip-gram模型。skip-gram可以建模为:P(wt−1,wt+1∣wt)P(w_{t-1}, w_{t+1}|w_t)P(wt−1,wt+1∣wt)
假定上下文的单词之间没有相关性(正确地说是假定“条件独立”),可以如下进行分解:P(wt−1,wt+1∣wt)=P(wt−1∣wt)P(wt+1∣wt)P(w_{t-1}, w_{t+1}|w_t)=P(w_{t-1}|w_t)P(w_{t+1}|w_t)P(wt−1,w