1.2 Word Embedding

因机器无法直接接收单词、词语、字符等标识符(token),所以把标识符数值化一直是人们研究的内容。开始时人们用整数表示各标识符,这种方法简单但不够灵活,后来人们开始用独热编码(One-Hot Encoding)来表示。这种编码方法虽然方便,但非常稀疏,属于硬编码,且无法重载更多信息。此后,人们想到用数值向量或标识符嵌入(Token Embedding)来表示,即通常说的词嵌入(Word Embedding),又称为分布式表示。

不过Word Embedding方法真正流行起来,还要归功于Google的word2vec。接下来我们简单了解下word2vec的原理及实现方法。

1.2.1 word2vec之前

从文本、标识符、独热编码到向量表示的整个过程,可以用图1-2表示。

015-1

图1-2 从文本、标识符、独热编码到向量表示

从图1-2可以看出,独热编码是稀疏、高维的硬编码,如果一个语料有一万个不同的词,那么每个词就需要用一万维的独热编码表示。如果用向量或词嵌入表示,那么这些向量就是低维、密集的,且这些向量值都是通过学习得来的,而不是硬性给定的。至于词嵌入的学习方法,大致可以分为两种。

1. 利用平台的Embedding层学习词嵌入

在完成任务的同时学习词嵌入,例如,把Embedding作为第一层,先随机初始化这些词向量,然后利用平台(如PyTorch、TensorFlow等平台)不断学习(包括正向学习和反向学习),最后得到需要的词向量。代码清单1-1为通过PyTorch的nn.Embedding层生成词嵌入的简单示例。

代码清单1-1 使用Embedding的简单示例

from torch import nn
import torch
import jieba
import numpy as np

raw_text = """越努力就越幸运"""
#利用jieba进行分词
words = list(jieba.cut(raw_text))
print(words)
#对标识符去重,生成由索引:标识符构成的字典
word_to_ix = { i: word for i, word in enumerate(set(words))}
#定义嵌入维度,并用正态分布,初始化词嵌入
#nn.Embedding模块的输入是一个标注的下标列表,输出是对应的词嵌入
embeds = nn.Embedding(4, 3)
print(embeds.weight[0])
#获取字典的关键字
keys=word_to_ix.keys()
keys_list=list(keys)
#把所有关键字构成的列表转换为张量
tensor_value=torch.LongTensor(keys_list)
#把张量输入Embedding层,通过运算得到各标识符的词嵌入
embeds(tensor_value)

运行结果:

['越','努力','就','越','幸运']
tensor([-0.5117,  -0.5395,  0.7305], grad_fn=<SelectBackward>)
tensor([[-0.5117, -0.5395,  0.7305],
        [-0.7689,  0.0985, -0.7398],
        [-0.3772,  0.7987,  2.1869],
        [-0.4592,  1.0422, -1.4532]], grad_fn=<EmbeddingBackward>)

2. 使用预训练的词嵌入

利用在较大语料上预训练好的词嵌入或预训练模型,把这些词嵌入加载到当前任务或模型中。预训练模型很多,如word2vec、ELMo、BERT、XLNet、ALBERT等,这里我们先介绍word2vec,后续将介绍其他预训练模型,具体可参考1.6节。

1.2.2 CBOW模型

在介绍word2vec原理之前,我们先看一个简单示例。示例展示了对一句话的两种预测方式:

假设:今天 下午 2点钟 搜索 引擎 组 开 组会。

方法1(根据上下文预测目标值)

对于每一个单词或词(统称为标识符),使用该标识符周围的标识符来预测当前标识符生成的概率。假设目标值为“2点钟”,我们可以使用“2点钟”的上文“今天、下午”和“2点钟”的下文“搜索、引擎、组”来生成或预测目标值。

方法2(由目标值预测上下文)

对于每一个标识符,使用该标识符本身来预测生成其他词汇的概率。如使用“2点钟”来预测其上下文“今天、下午、搜索、引擎、组”中的每个词。

两种预测方法的共同限制条件是,对于相同的输入,输出每个标识符的概率之和为1。

它们分别对应word2vec的两种模型,即CBOW模型(Continuous Bag-Of-Words Model)和Skip-Gram模型。根据上下文生成目标值(即方法1)时,使用CBOW模型;根据目标值生成上下文(即方法2)时,采用Skip-Gram模型。

CBOW模型包含三层:输入层、映射层和输出层。具体架构如图1-3所示。CBOW模型中的wt)为目标词,在已知它的上下文wt-2)、wt-1)、wt+1)、wt+2)的前提下预测词wt)出现的概率,即pw/context(w))。目标函数为:

017-1
017-2

图1-3 CBOW模型

CBOW模型其实就是根据某个词前后的若干词来预测该词,也可以看成是多分类。最朴素的想法就是直接使用Softmax来分别计算每个词对应的归一化的概率。但对于动辄十几万词汇量的场景,使用Softmax计算量太大,此时可以使用一种称为二分类组合形式的Hierarchical Softmax(输出层为一棵二叉树)来优化。

1.2.3 Skip-Gram模型

Skip-Gram模型同样包含三层:输入层、映射层和输出层。具体架构如图1-4所示。Skip-Gram模型中的wt)为输入词,在已知词wt)的前提下预测词wt)的上下文wt-2)、wt-1)、wt+1)、wt+2),条件概率写为p(context(w)/w)。目标函数为:

018-1
018-2

图1-4 Skip-Gram模型

我们通过一个简单的例子来说明Skip-Gram的基本思想。假设有一句话:

the quick brown fox jumped over the lazy dog

接下来,我们根据Skip-Gram模型的基本思想,按这条语句生成一个由序列(输入,输出)构成的数据集。那么,如何构成这样一个数据集呢?我们首先对一些单词以及它们的上下文环境建立一个数据集。可以以任何合理的方式定义“上下文”,这里是把目标单词的左右单词视作一个上下文,使用大小为1的窗口(即window_size=1)定义,也就是说,仅选输入词前后各1个词和输入词进行组合,就得到一个由(上下文,目标单词)组成的数据集,具体如表1-1所示。

表1-1 由Skip-Gram算法构成的训练数据集

018-3

1.2.4 可视化Skip-Gram模型实现过程

前面我们简单介绍了Skip-Gram的原理及架构,至于Skip-Gram如何把输入转换为词嵌入、其间有哪些关键点、面对大语料库可能出现哪些瓶颈等,并没有展开说明。而了解Skip-Gram的具体实现过程,有助于更好地了解word2vec以及其他预训练模型,如BLMo、BERT、ALBERT等。所以,本节将详细介绍Skip-Gram的实现过程,加深读者对其原理与实现的理解。对于CBOW模型,其实现机制与Skip-Gram模型类似,本书不再赘述,感兴趣的读者可以自行实践。

1. 预处理语料库

先来看下面的语料库:

text = "natural language processing and machine learning is fun and exciting"
corpus = [[word.lower() for word in text.split()]]

这个语料库就是一句话,共10个单词,其中and出现两次,共有9个不同单词。因单词较少,这里暂不设置停用词,而是根据空格对语料库进行分词,分词结果如下:

["natural", "language", "processing", "and", "machine", "learning", "is", "fun",
    "and", "exciting"]

2. Skip-Gram模型架构图

使用Skip-Gram模型,设置window-size=2,以目标词确定其上下文,即根据目标词预测其左边2个和右边2个单词。具体模型如图1-5所示。

019-1

图1-5 Skip-Gram模型架构图

在图1-5中,这里语料库只有9个单词,V-dim=9,N-dim=10(词嵌入维度),C=4(该值为2*window-size)。

如果用矩阵来表示图1-5,可写成如图1-6所示的形式。

020-1

图1-6 Skip-Gram模型的矩阵表示

注意

生产环境语料库一般比较大,涉及的单词成千上万。这里为便于说明,仅使用一句话作为语料。

在一些文献中,又将矩阵W V×N称为查找表(look up table)。2.1.1节介绍PyTorch的Embedding Layer时,会介绍查找表的相关内容。

3. 生成中心词及其上下文的数据集

根据语料库及window-size,生成中心词与预测上下文的数据集,如图1-7所示。

020-2

图1-7 Skip-Gram数据集

图1-7中共有10对数据,X k对应的词为中心词,其左边或右边的词为上下文。

4. 生成训练数据

为便于训练word2vec模型,首先需要把各单词数值化。这里把每个单词转换为独热编码。在前面提到的语料库中,图1-7中显示了10对数据(#1到#10)。每个窗口都由中心词及其上下文单词组成。把图1-7中每个词转换为独热编码后,可以得到如图1-8所示的训练数据集。

021-1

图1-8 训练数据集

5. Skip-Gram模型的正向传播

上述1~4步完成了对数据的预处理,接下来开始数据的正向传播,包括输入层到隐藏层、隐藏层到输出层。

(1)输入层到隐藏层

从输入层到隐藏层,用图来表示就是输入向量与权重矩阵W1的内积,如图1-9所示。

021-2

图1-9 输入层到隐藏层

这里将矩阵W 9×10先随机初始化为-1到1之间的数。

(2)隐藏层到输出层

从隐藏层到输出层,其实就是求隐含向量与权重矩阵W2的内积,然后使用Softmax激活函数、得到预测值,具体过程如图1-10所示。

022-1

图1-10 隐藏层到输出层

(3)计算损失值

损失值即预测值与实际值的差,这里以选择数据集#1为例,即中心词为natural,然后计算对应该中心词的输出,即预测值,再计算预测值与实际值的差,得到损失值EI。中心词natural的上下文(这里只有下文)为language和processing,它们对应的独热编码为w_c=1,w_c=2,具体计算过程如图1-11所示。

022-2

图1-11 计算损失值

6. Skip-Gram模型的反向传播

我们使用反向传播函数backprop,根据目标词计算的损失值EI,反向更新W1和W2。

为帮助大家更好地理解,这里简单说明一下反向传播的几个关键公式的推导过程。

假设输出值为u,即W'T·h=u,则预测值为:

022-3

(1)定义目标函数

023-1

(2)求目标函数关于023-2的偏导数

023-3

其中023-4是第c个上下文在字典中对应的索引。

023-5

023-6,当023-7时,t cj=1,否则,t cj=0。

023-8,表示预测值与真实值的误差。

023-9的计算过程可用图1-12表示。

023-10

图1-12 目标函数关于023-11的偏导计算过程示意图

(3)更新矩阵w'(即W2)

利用梯度下降法更新梯度:

024-1

式(1.8)的计算过程可用图1-13及图1-14表示。

024-2

图1-13 参数更新示意图(一)

024-3

图1-14 参数更新示意图(二)

(4)求关于W(即W1)的偏导数

024-4

其中024-5

所以

024-6

式(1.10)的计算过程可用图1-15和图1-16表示。

024-7

图1-15 偏导计算结果(一)

025-1

图1-16 偏导计算结果(二)

更新参数:

025-2

更新权重参数的计算过程可用图1-17和图1-18表示。

025-3

图1-17 权重参数更新结果(一)

025-4

图1-18 权重参数更新结果(二)

1.2.5 Hierarchical Softmax优化

结合上面内容,我们需要更新两个矩阵WW′,但这两个矩阵涉及的词汇量较大(即V较大),所以更新时需要消耗大量资源,尤其是更新矩阵W′。正如前面一直提到的,无论是CBOW模型还是Skip-Gram模型,每个训练样本(或者Mini Batch)从梯度更新时都需要对W′的所有V×N个元素进行更新,这个计算成本是巨大的。此外,在计算Softmax函数时,计算量也很大。为此,人们开始思考如何优化这些计算。

考虑到计算量大的部分都是在隐藏层到输出层阶段,尤其是W′的更新。因此word2vec使用了两种优化策略:Hierarchical Softmax和Negative Sampling。二者的出发点一致,即在每个训练样本中,不再完全计算或者更新W′矩阵,换句话说,两种策略中均不再显式使用W′这个矩阵。同时,考虑到上述训练和推理的复杂度高是因Softmax分母上的∑(求和)过程导致,因此上述的两种优化策略是对Softmax的优化,而不仅仅是对word2vec的优化。

通过优化,word2vec的训练速度大大提升,词向量的质量也几乎没有下降,这也是word2vec在NLP领域如此流行的原因。

Hierarchical SoftMax(以下简称HS)并不是由word2vec首先提出的,而是由Yoshua Bengio在2005年最早提出来的专门用于加速计算神经语言模型中的Softmax的一种方式。这里主要介绍如何在word2vec中使用HS优化。HS的实质是基于哈夫曼树(一种二叉树)将计算量大的部分变为一种二分类问题。如图1-19所示,原来的模型在隐藏层之后通过W′连接输出层,经过HS优化后则去掉了W′,由隐藏层h直接与下面的二叉树的根节点相连。

026-1

图1-19 哈夫曼树示意图

其中,白色的叶子节点表示词汇表中的所有词(这里有V个),黑色节点表示非叶子节点,每一个叶子节点其实就是一个单词,且都对应唯一的一条从根节点出发的路径。我们用nwj)表示从根节点到叶子节点w的路径上的第j个非叶子节点,并且每个非叶子节点都对应一个向量026-2,其维度与h相同。

1.2.6 Negative Sampling优化

训练一个神经网络意味着要输入训练样本并不断调整神经元的权重,从而不断提高对目标预测的准确性。神经网络每训练一个样本,该样本的权重就会调整一次。正如上面所讨论的,vocabulary的大小决定了Skip-Gram神经网络的权重矩阵的具体规模,所有这些权重需要通过数以亿计的训练样本来进行调整,这是非常消耗计算资源的,并且在实际训练过程中,速度会非常慢。

Negative Sampling(负采样)解决了这个问题,它可以提高训练速度并改善所得到词向量的质量。不同于原本需要更新每个训练样本的所有权重的方法,负采样只需要每次更新一个训练样本的一小部分权重,从而在很大程度上降低了梯度下降过程中的计算量。