5.3 词嵌入 word2vec、GloVe与fastText
1.从one-hot说起:为什么词需要被嵌入
说起来,前面我们聊的多数是图像、是数值,输入到网络里就是一个个浮点数。可NLP(自然语言处理)这边遇到的输入是词、字、句子,本身并不是一串数字。要把它们喂给神经网络,第一步就得想办法把词变成向量。这件事听起来简单,做起来却暗藏门道,词嵌入(word embedding)就是其中最关键的一招。
最早大家用的办法很直白,叫one-hot(独热)编码。词典里有多少个词,每个词就编一个多长的向量。假设词典大小为 , 表示词典里词的总数,那么每个词对应一个长度为 的向量,这个向量里只有自己那个位置是 ,其余全是 。比方说猫这个词是词典里第 个, 表示这个词在词典里的序号,那它的one-hot向量就是第 位为 、其他位全为 的那一根细长的条。
这种表示有两个明显的毛病。一是稀疏,一个向量里成千上万个 ,只有孤零零一个 ,存起来费空间,算起来也费力气。二是更致命的,任意两个不同词的one-hot向量彼此正交,点乘为 。点乘为零意味着什么呢,意味着在机器眼里,猫和狗、高兴和愉快之间的相似度,跟猫和汽车一模一样,毫无亲疏之分。这显然不合常理,近义词本该靠得近一些才对。
不妨换个思路,让每个词用一个低维的稠密向量来表示,比方说 维, 表示向量的维度,通常取几十到几百,每一维都是个实数。再把语义相近的词放到空间里相近的位置,让猫和狗的向量夹角小一些,让猫和汽车的夹角大一些。这种思路就叫分布式表示(distributed representation),说穿了就是把一个词的语义分散到很多维度上去共同编码,而不再用孤零零一个 来标记。这么一来,向量之间的余弦相似度(两个向量夹角的余弦值)就能反映词义上的远近。词嵌入要解决的,正是怎么把这堆稠密向量学出来。
2.word2vec:CBOW与Skip-gram
词嵌入最经典的一套做法,是Mikolov他们在2013年提出的word2vec。这套方法的核心直觉特别朴素:一个词的语义,其实就藏在它周围常跟它一起出现的那些词里。这句话有个更学术的说法,叫分布式假设(distributional hypothesis),大意是上下文相近的词,意思也相近。我记得一位语言学家说过类似的一句话,大意是一个词的意思由它的朋友圈决定,这话放在词嵌入这里再合适不过。
word2vec有两种主要做法。一种叫CBOW(Continuous Bag-of-Words,连续词袋模型),它做的事情是拿一段上下文(中心词前后各若干个词)去预测中间那个中心词本身。另一种叫Skip-gram(跳字模型),方向反过来,拿中心词去预测它周围的上下文词。两种做法互为镜像,目标都是让经常搭伴出现的词,它们的向量在空间里靠得更近。
我们以Skip-gram为例细看一下。设中心词的向量是 , 表示词向量,下标 表示这个词扮演中心词的角色。设某个上下文词的向量是 ,下标 表示这个词扮演上下文(outside)的角色。Skip-gram希望给定中心词 时,上下文词 出现的概率 尽量大。这个概率通常用下面这个式子来算:
这里 是指数函数, 是两个向量的内积(一个向量转置后乘另一个), 是转置符号, 是求和符号,把词典里所有词(一共 个)都当作可能的上下文逐个加起来, 是求和时用到的临时下标。整个式子就是softmax的形式,把每个候选词的得分先做指数、再归一化成概率。训练时,所有的 和 反复调整,让真实出现的上下文词拿到更高的概率。训练完之后,习惯上把中心词向量 拿出来当作每个词最终的词向量。
实际算这个分母要把整个词典扫一遍,词典一大就吃不消。所以工程上还会用负采样(negative sampling)或者层次softmax(hierarchical softmax)来近似,把那个昂贵的全词典求和换成便宜得多的形式。这一步是个工程妥协,让word2vec能在普通机器上飞快地跑起来。
学完之后,向量里冒出一些特别迷人的性质。最经典的莫过于这个类比:
这里的减号和加号就是向量的加减。意思是说,king(国王)这个词的向量,减去man(男人)的向量,再加上woman(女人)的向量,得到的结果离queen(王后)的向量非常近。换句话说,词向量里居然把性别这个抽象维度给学了出来。这就像一个棋手复盘,把每一手棋拆开看,才发现看似混沌的盘面里其实藏着清晰的脉络。小明当年第一次跑通word2vec,看到这个结果愣了半天,回头跟我感慨说原来数学真的能把意思量出来。
3.GloVe:把全局共现也用上
word2vec是从局部上下文窗口里一点一点学的,每个窗口只看一小撮词。GloVe(Pennington等人2014年提出)走的另一条路,它盯上的是全局的共现统计。
先说共现矩阵是什么。我们扫一遍整个语料,统计每一对词在某个窗口大小内一起出现的次数,把结果填到一张大表里,这张表就是共现矩阵。设这一对词是 和 ,下标 、 是它们各自在词典里的编号,矩阵里第 行第 列的那个元素记作 , 表示共现次数。整张表把全局范围内哪些词爱跟哪些词搭伴都记了下来。
GloVe要做的,是让词向量的内积去拟合这个共现次数的对数。具体说,它希望下面这个近似成立:
这里 是词 作为中心词时的向量, 是词 作为上下文时的向量(同一套词,两套向量,跟word2vec里 和 是一样的思路), 和 是两个偏置项(每个词各自的偏移), 是自然对数。训练目标就是让这个近似在整个共现矩阵上尽量准。
GloVe和word2vec各有各的好处。word2vec是在局部窗口上滑着学的,对局部的细微搭配比较敏感。GloVe则把全局统计一次性都纳入考虑,语料整体的共现关系学得更全面。打个比方,word2vec像是在一条街上挨家挨户串门,每家聊几句,对邻里细节摸得清楚。GloVe更像拿着一份全市人口普查表,从大局上把握哪些人经常聚在一起。两者学到的高质量向量,在下游任务上表现都不错,工程实践里也常把它们当备选方案各试一遍。
4.fastText:把词拆开来学
word2vec和GloVe都有一个共同的短板,就是对没见过的生词无能为力。词典里没有的词,模型连个向量都给不出来。另外像英文这种形态比较丰富的语言,一个词根加上各种前缀后缀能变出好几十个词,传统词嵌入把它们当成完全不同的几个词来学,等于把长得像、意思也像这条线索白白丢了。
fastText(Facebook在2016年提出)就是为了解决这件事。它的做法是把每个词进一步拆成更小的子词(subword),具体说就是字符级别的n-gram。n-gram指的是连续 个字符组成的片段, 表示片段长度,比方说 就是连续三个字符。比如单词where,加上词边界的尖括号 和 ,可以拆出 wh、whe、her、ere、re 这一类三元片段。一个词的最终向量,就是它本身这条词向量和它所有子词向量的求和平均。
这么一拆,好处立刻就显出来了。一个生词哪怕从没在语料里出现过,只要它能被拆成若干个见过的子词,模型就能把这些子词向量加起来,拼出一个还不错的向量给它用。形态相近的词(比方run、running、runner)共享相当一部分子词,向量自然也就靠得近。这对形态丰富的语言(俄语、芬兰语、土耳其语这些)尤其友好。我之前看过一本讲斯拉夫语的书,里头说俄语一个动词变位能有几十种形式,要是每种形式都从头学一个独立向量,数据再大也吃不消。fastText这种子词思路,恰好对上了这种语言的脾气。
5.词嵌入是NLP深度学习的基石
回过头来看,从one-hot到word2vec、GloVe、fastText,词嵌入这一套把NLP的输入从一堆彼此无关的稀疏符号,变成了充满语义的稠密向量。这件事的意义怎么说都不为过,它是整个NLP深度学习的基石,后续几乎所有模型(不管是RNN、CNN还是Transformer)的输入层,都是建立在词向量之上的。
下游用处极多。最直接的,拿两个词向量算余弦相似度,就能判断这两个词意思近不近,做近义词聚类、文本相似度匹配都靠它。小张前阵子做毕设,给一批客户评论做情感聚类,第一步就是把每条评论里的词向量平均一下得到句向量,再丢给聚类算法,分出来的类居然还真有模有样。再往上一层,我们在5.1 节讲预训练语言模型时也见过这些向量,BERT和GPT那一套虽然换了更复杂的训练目标,但词嵌入作为输入起点的思路是一脉相承的。说起来,1.4 节讲概率信息论时我们提到的各种分布和期望,在理解词向量训练时的损失函数和采样过程里也都会回来帮忙。
最后提一句局限。词嵌入本质上是给每个词分配一个固定的向量,可同一个词在不同语境下意思常常不一样,苹果可以是水果也可以是公司。这种一词多义的问题,静态的词嵌入处理不了,要等后来的上下文词嵌入(像ELMo、BERT的输出)来补上这一课。那是后话了,今天先到这儿,下一篇见。
练习
Q1. one-hot编码最大的两个毛病是什么,词嵌入又是怎么解决的?
两个毛病:一是稀疏,向量里成千上万个0只有一个1,费空间又费算力;二是任意两个不同词的one-hot向量正交、点乘为0,导致猫和狗、猫和汽车在模型眼里毫无亲疏之分。词嵌入改成低维稠密的实数向量( 维,几十到几百),让语义相近的词落在空间相近的位置,向量之间的余弦相似度就能反映词义远近,这就是分布式表示。
Q2. 已知 word2vec 学出 。如果改成 Skip-gram 的概率公式 ,分母里对整个词典 求和,工程上为什么要换成负采样?
因为词典 动辄几十万,每算一个概率都要把整个词典扫一遍做softmax,训练根本跑不动。负采样的做法是不算完整归一化的概率,而是把问题改成二分类:让真正出现的上下文词得分高,再随机抽几个负样本词(比如 个)让它们得分低。这样计算量从 降到 ( 一般取5到20),在普通机器上也能飞快跑起来,是工程妥协换可训练性。
Q3. 易错点:word2vec、GloVe、fastText 都能学词向量,它们是不是可以随便换着用?
不能完全随便换。word2vec是在局部上下文窗口上滑着学,对局部搭配敏感;GloVe盯的是全局共现统计,对语料整体的共现关系学得更全面,两者适合的场景侧重不同。fastText 把词拆成字符n-gram子词,能处理生词和形态丰富的语言(run/running/runner向量靠近),这点是前两者做不到的。所以要根据任务挑:要处理生词选fastText,要全局共现选GloVe,要轻量快速选word2vec,常常几种都试一遍再选最好的。
Q4.(面试题) 请比较 word2vec 和 GloVe 在建模思路上的根本区别,并说明为什么 GloVe 训练时要有中心词向量和上下文词向量两套。
根本区别在于信息来源。word2vec 从局部上下文窗口学,每次只看中心词周围一小撮词,目标是让真实出现的上下文词概率大,本质是局部预测。GloVe 不做这种逐窗口预测,它先统计整个语料里每对词 在窗口内的共现次数 ,构成全局共现矩阵,然后让词向量的内积去拟合共现次数的对数:,本质是拟合全局统计量。至于为什么要两套向量( 当中心词、 当上下文),是因为同一个词在扮演中心词和上下文角色时,它和邻居的关系方向不同,分开建模能让优化更顺、表达更充分,训练完后习惯上把两套向量相加或取其一作为最终词向量。简单说,word2vec 是"局部窗口+预测",GloVe 是"全局共现+回归",这是两者最核心的差异。