word2vec整理

一、前言

因为Word2Vec无论是源作者还是国内ansj的实现均有一些工程性的东西给阅读造成了一些困扰,所以我将所有工程性的东西都尽可能删去,只保留最核心的一些算法原理。这一版实现仅使用了skip-gram,结合代码谈谈我个人的一些理解。

如果说电力和石油是现代工业的基础,那么自然语言处理的电和油就是文字关系。语言是一种载体明确但关系复杂的东西,如果我们说世界是海,那么语言就是海面。海面有时风平浪静,有时波涛汹涌,有时清澈透明,有时浑浊不堪——规律在于,海面的这些特点和海面本身是无关的,语言也是一样。语言的种种特点和语言本身是无关的,而是来源于对客观世界的指代。在自然语言处理领域,文字是海面,可达可观可触,那么文字下面的海洋就可以统称为文字关系,至于文字有哪些关系?这些关系是什么样的?这些关系和现实世界有什么联系?等等这些问题每一个都是现代自然语言处理的研究前沿问题。

说到文字关系,最经典的就是语言模型。现代语言模型已经有好多实现了:Word2Vec、ELMo、Glove、BERT和刚大火的XLNet,以及很多存在但没有列举的,这些模型特点不一,且无法通用,结果无法解释,但均有一些特点,先从这一点说起。

二、关于语言模型

语言模型来源于信息论,信息论的鼻祖是香农,但限于时代的认识,香农的想法没有得到重视,直到后来贾里尼克才在工程上第一次运用这些知识。按照语言模型的定义,任意一段长度为n的文字的概率是:

1
P(S) = P(w1)P(w2|w1)P(w3|w1w2)…P(wn|w1w2…wn-1)

但这一公式只在信息论上有概念性的意义,计算每一个P将是一个几乎无法实现的工作,于是工程中将这个公式简化为我们今天常用的:

1
P(S) = P(w1)P(w2|w1)P(w3|w2)…P(wn|wn-1)

这样一来需要求解的量就大大减少了。

这个模型有啥用?据统计汉字常用字有3000个,我们假设一个不懂中文的老外,让他从3000个汉字中找出5个字简单叙述一下今天的天气,他可能做出的选择是3000X3000X3000X3000X3000个选择,但是正确答案往往不外乎几种,比如“今天天气好”、“今天是晴天”、“今天下雨了”等等,而且当前几个字确定下来以后,后面的字可选项就更少,这就是这个概率公式的由来,即被装载的信息确定的前提下,文字的可能选项有多少。从这个模型上看,只要求得所有P(Wi|Wj),我们就可以得到所有的可能选项。这一P(Wi|Wj)称作条件概率反应了事件的因果关系,如果我们再简单一点假设P(Wi|Wj)=P(Wj|Wi),那么这一公式将简化为只反应相关性关系。

知道文字的可能选项有多少有啥用?第一大用处是用在翻译上,根据一句话的信息,选择另一种语言描述下的最大可选文字。第二大用处用在语音上(和翻译类似),将音频转换为文字或文字转换为音频。第三大用处用在如siri一样的聊天机器人上,用户的问题可以看做一种特殊形式的翻译原句,只要对照翻译为回答即可(其实这样容易导致数据集欺骗,在真实场景下往往还需要条件语句做补充)。第四大用处用在自动写作上,比如根据上联写下联,根据大纲写故事,根据短文写标题之类的。

以上这些,word2vec都可以干。在说word2vec之前,先说说这个算法解决的问题是什么:

三、最简化的问题描述

先看以下三句话:

1
2
3
鸡吃虫
猫吃鱼
羊吃草

现在我们发明一种语言,这种语言由7个字母组成(abcdefgx),用这种语言描述一下上面三句话:

1
2
3
axb
cxd
exf

任何一个地球人看到这种描述都是看不懂的。但是你可以通过文字之间的顺序关系猜测到一些规律:
1、a、c、e可能差不多,b、d、f可能也差不多
2、x在这种语言中往往多次出现
3、a和b之间的关系、c和d之间的关系、e和f之间的关系可能差不多

猜到这就够了,计算机看待人类的语言也是这样的,在当前语言模型下,我们只希望计算机能够猜到类似的这样的关系就够了,word2vec就能猜出这样的关系,对于这样的数据,利用本文的代码对数据源训练后的结果是:

1
2
3
4
5
6
7
x:	-0.04,-0.43,
a: -3.13,3.38,
b: 0.33,-0.02,
c: -2.68,4.34,
d: 0.06,0.26,
e: -5.21,2.19,
f: 0.38,-0.39,

可以看到a、c、e之间较为相似,b、d、f之间也较为相似。下面说一下word2vec为啥能做这些。

四、关于word2vec

和绝大多数机器学习算法类似,word2vec本质上也是计算梯度-更新向量的套路,只不过中间加了一个huffman树,这个huffman树在训练的过程中起的用处非常大,这一点很多网上的教程都没有说清楚,甚至原始论文中提到的也不够详细。

一般我们涉及向量的时候,分类模型都是类似y=k1x1+k2x2+k3x3+…这样出现的(不同模型可能有弯弯绕绕,但你总能找到这个东西),但词向量这个东西本身就是x,你要求得所有的x,最麻烦的是所有的k怎么找。

没有k,也没有x,第一想法就是让它们自训练(GAN),但任何一种自训练算法都一定有外部信息介入的,这个huffman树的角色就是外部信息,而且是唯一的外部信息(请百度huffman相关知识)。huffman树本身是根据词频产生的,对于上面一个问题,huffman树的结果是:

1
2
3
4
5
6
7
x:	01
a: 001
b: 0001
c: 00001
d: 000001
e: 0000001
f: 0000000

即词频越高,编码越短,在树上越靠近根节点。我们看huffman编码的表示形式,路径节点由一堆0和最后一个1组成,0代表了隐藏层路径节点,1是最后的具体词。

训练的时候,比如让axb这句话进入,窗口长度为1,那么在skip-gram下,训练到词x的时候,会取x的每一个路径节点的k向量乘以窗口中第一个词a的词节点的x向量,得到的就是分类结果,对于隐藏层来说,分类结果是0,对于最后的具体词来说,分类结果是1。

有分类结果了,剩下的就是计算梯度-更新向量的套路了,不再多说。使用上面的样本训练100万次后,最终能算出各字之间的欧氏距离,画入下表如下:

沿斜对角并排除x的行列去看,a、c、e之间呈现出很明显的相关性。不过有趣的是e的数据不太“干净”,为什么这样,注意我们在做huffman树的时候是按照词频从大到小排列的,如果反过来从小到大排列的话,结果如下:

1
2
3
4
5
6
7
x:	0000000
a: 0000001
b: 000001
c: 00001
d: 0001
e: 001
f: 01

我们看这次轮到a的数据不太干净了,为什么?因为对于huffman树来说越靠近根节点的路径节点被训练的次数一定是最多的(注意这和是否负采样无关),反过来讲,谁靠近根节点,谁的路径准确率就好一点,而越远离的节点则越不好。那么问题来了,如果这样干脆做比如100个隐藏层向量作为k,每个词都和这100个隐藏层向量进行相互学习不就行了?答案是huffman树对高频词非常友好,而依据28原则,高频词只要做好了,模型就好了一大半,至于剩下的长尾词,能做就做做不好就算。

这样的考量也是没有办法的,注意我之前说了,语言模型从来是不可通用的,并且这次训练的模型也没办法整合进其它模型中(虽然有很多人在做迁移研究,但都是折衷方案,没有哪一种突破了模型本身的限制),这就意味着word2vec模型的高频强低频弱的现象其实是基本上无解的。当然在实际应用中有一种解决办法,就是在新数据集上对之前训练好的word2vec模型继续训练,但这样做会影响到所有词,也就是高频低频会变换顺序,依然不是好的折衷,这同时也牵涉到预训练模型为什么会出现了。