Word Embedding

导言

在做 NLP 的时候,我们处理的 features 一般都是字词(words),如何表述这些字词便成为了一个非常基础且十分重要的问题。接下来要介绍的是在搭建NLP模型过程中如何用 PyTorch 这个工具对 words 进行预处理。

One-Hot有用吗?

假设我们有一组词汇\(V\),其大小为\(|V|\)。采用独热编码(one-hot)的方式,对于单词\( w=V_{i} \),则

$$ w=\underbrace{[0,0,\cdots,1,\cdots,0,0]}_{|V|}$$

也就是说,在词汇表 \(V\) 中,仅有一个维度的值为1,其余都是0。这种表现方式有不少问题:

  • 每一个单词的维度巨大。One-Hot 编码将每一个单词视为词汇集的不同方向,那么每个单词的向量长度就取决于词汇集 \(V\) 的大小,在NLP任务中,这将会是巨大的。
  • 各个单词之间没有联系。由于 One-Hot 将不同的单词直接指定到不同的方向,而且各单词之间是独立存在的,这就会导致单词在句群中的语义丢失。

对于语义联系丢失,我们举一个简单的例子,假设在训练文本中,出现了以下的句子:

  • 程序员在加班。
  • 产品经理在加班。
  • 程序员解决了客户提出的一个问题。

而在测试文本中,出现了一个训练文本中没有出现过的句子:

  • 产品经理解决了客户提出的一个问题。

正常的思维应当是“程序员”和“产品经理”有着相似性,我们希望模型也能分析出这样的相似性,然而如果用 One-Hot 编码的方法似乎无法理解“程序员”和“产品经理”有哪些相似之处(我们说的相似是语义相似,而不仅仅是拥有简单的正交表示)。

稠密的 Word Embedding

我们怎么才能把语义相似性嵌入到词向量中去呢?我们希望能在语义层面上对词向量做分解,首先肯定是摒弃掉 One-Hot 这种丢失相关性的编码方式了。依然使用之前的例子,我们对“程序员”和“产品经理”做语义层面的分解,例如“程序员”和“产品经理”都是“IT相关人员”,都会“加班”,那么它们在“IT相关人员”和“加班”语义维度上的取值就更大,而“产品经理”在“编写代码”这个语义维度上的取值就比“程序员”要小得多。

只要我们找到一些语义维度,那么每个单词就可以在这些维度上分解,形成一个词向量,比如:

$$w_{Programmer}=[4.3_{(\text{IT})},4.2_{(\text{Overtime})},4.6_{(\text{Coding})}]\\ w_{Product Manager}=[3.9_{(\text{IT})},3.6_{(\text{Overtime})},-2.3_{(\text{Coding})}]$$

通过这样的分解方式,我们就能够衡量出各个词向量之间的相似程度,比如:

$$Similarity(\text{Programmer}, \text{Product Manager})=w_{Programmer}\cdot w_{Product Manager}$$

再将其标准化:

$$Similarity(\text{Programmer}, \text{Product Manager})=\frac{w_{Programmer}\cdot w_{Product Manager}}{||w_{Programmer}||\cdot||w_{Product Manager}||}=\cos(\phi)$$

上式中的 \(\phi\) 即两个词向量的夹角,显然,两个极相似的词的 \(Similarity\) 应当趋近于1,而两个完全相反的词的 \(Similarity\) 应当趋近于-1。

通过这种在许多语义维度上进行分解的方式,对于每个单词,我们得到的是一个稠密的向量(完全不同于 One-Hot 得到的稀疏向量)

语义维度的选择与取值

从上面的分析我们可以看出按语义分解的优势所在,但是随之而来的是一个更难受的问题:如何确定在哪些语义维度上对某个单词进行分解?

如果摆在眼前的是一个词汇量巨大的文本,里面的单词可能包含方方面面(比一两个程序员和产品经理不知道高明到哪里去了),我们完全可以设定出成千上万个语义维度,而各个单词在这些维度上的取值范围更是难以调配。

这时,深度学习就可以派上用场了,让神经网络模型去学习这些分类。当然咯,如果让神经网络模型去学习这些潜在的语义维度的话,它的分类标准就不会像我们之前手写的例子那样清晰明了(至少你还知道“程序员”和“产品经理”之间还有 IT、Coding 这些语义维度),你根本不知道它会按照怎样的维度去学习,毕竟机器还是不知道某个维度究竟代表着什么意思,反正能分解就是了(玄学++)。

在 PyTorch 中使用 word embedding

由于最近实验室采用的也是 PyTorch 这个模块,那么我就用这个来讲好了。

首先要用一个以 word 为 key,id 为 value 的 lookup 顺序表,建立 word 与 index 之间的映射关系,用于查找各个单词的 id。

import torch
from torch.autograd import Variable
import torch.nn as nn

word2index = {
    "programmer": 0, 
    "product manager": 1
}

接下来我们用一个模型来跑出 "programmer" 的初始词向量,假设我们的语义维度为 8。

embedding = nn.Embedding(len(word2index), 8)
index_variable = Variable(torch.LongTensor([word2index['programmer']]))
embedded = embedding(index_variable)

embedded 即通过 nn.Embedding 初始出的词向量:

Variable containing:
-0.0148 -0.7849  1.2612  0.0015  0.9238 -1.2201  1.5755 -0.8467
[torch.FloatTensor of size 1x8]

一定要注意的是,这里的词向量是初始化出来的,并没有经过网络训练优化,因为我们没有指定优化方法,在 embedding 之后,我们还要进行其他的网络训练(比如 N-Gram 模型进行文本预测),才能给这些 enbedded word 赋予特殊的语义。