- 深入浅出Embedding:原理解析与应用实践
- 吴茂贵 王红星
- 4262字
- 2021-06-09 18:02:14
1.2 Word Embedding
因机器无法直接接收单词、词语、字符等标识符(token),所以把标识符数值化一直是人们研究的内容。开始时人们用整数表示各标识符,这种方法简单但不够灵活,后来人们开始用独热编码(One-Hot Encoding)来表示。这种编码方法虽然方便,但非常稀疏,属于硬编码,且无法重载更多信息。此后,人们想到用数值向量或标识符嵌入(Token Embedding)来表示,即通常说的词嵌入(Word Embedding),又称为分布式表示。
不过Word Embedding方法真正流行起来,还要归功于Google的word2vec。接下来我们简单了解下word2vec的原理及实现方法。
1.2.1 word2vec之前
从文本、标识符、独热编码到向量表示的整个过程,可以用图1-2表示。
图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模型中的w(t)为目标词,在已知它的上下文w(t-2)、w(t-1)、w(t+1)、w(t+2)的前提下预测词w(t)出现的概率,即p(w/context(w))。目标函数为:
图1-3 CBOW模型
CBOW模型其实就是根据某个词前后的若干词来预测该词,也可以看成是多分类。最朴素的想法就是直接使用Softmax来分别计算每个词对应的归一化的概率。但对于动辄十几万词汇量的场景,使用Softmax计算量太大,此时可以使用一种称为二分类组合形式的Hierarchical Softmax(输出层为一棵二叉树)来优化。
1.2.3 Skip-Gram模型
Skip-Gram模型同样包含三层:输入层、映射层和输出层。具体架构如图1-4所示。Skip-Gram模型中的w(t)为输入词,在已知词w(t)的前提下预测词w(t)的上下文w(t-2)、w(t-1)、w(t+1)、w(t+2),条件概率写为p(context(w)/w)。目标函数为:
图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算法构成的训练数据集
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所示。
图1-5 Skip-Gram模型架构图
在图1-5中,这里语料库只有9个单词,V-dim=9,N-dim=10(词嵌入维度),C=4(该值为2*window-size)。
如果用矩阵来表示图1-5,可写成如图1-6所示的形式。
图1-6 Skip-Gram模型的矩阵表示
注意
生产环境语料库一般比较大,涉及的单词成千上万。这里为便于说明,仅使用一句话作为语料。
在一些文献中,又将矩阵W V×N称为查找表(look up table)。2.1.1节介绍PyTorch的Embedding Layer时,会介绍查找表的相关内容。
3. 生成中心词及其上下文的数据集
根据语料库及window-size,生成中心词与预测上下文的数据集,如图1-7所示。
图1-7 Skip-Gram数据集
图1-7中共有10对数据,X k对应的词为中心词,其左边或右边的词为上下文。
4. 生成训练数据
为便于训练word2vec模型,首先需要把各单词数值化。这里把每个单词转换为独热编码。在前面提到的语料库中,图1-7中显示了10对数据(#1到#10)。每个窗口都由中心词及其上下文单词组成。把图1-7中每个词转换为独热编码后,可以得到如图1-8所示的训练数据集。
图1-8 训练数据集
5. Skip-Gram模型的正向传播
上述1~4步完成了对数据的预处理,接下来开始数据的正向传播,包括输入层到隐藏层、隐藏层到输出层。
(1)输入层到隐藏层
从输入层到隐藏层,用图来表示就是输入向量与权重矩阵W1的内积,如图1-9所示。
图1-9 输入层到隐藏层
这里将矩阵W 9×10先随机初始化为-1到1之间的数。
(2)隐藏层到输出层
从隐藏层到输出层,其实就是求隐含向量与权重矩阵W2的内积,然后使用Softmax激活函数、得到预测值,具体过程如图1-10所示。
图1-10 隐藏层到输出层
(3)计算损失值
损失值即预测值与实际值的差,这里以选择数据集#1为例,即中心词为natural,然后计算对应该中心词的输出,即预测值,再计算预测值与实际值的差,得到损失值EI。中心词natural的上下文(这里只有下文)为language和processing,它们对应的独热编码为w_c=1,w_c=2,具体计算过程如图1-11所示。
图1-11 计算损失值
6. Skip-Gram模型的反向传播
我们使用反向传播函数backprop,根据目标词计算的损失值EI,反向更新W1和W2。
为帮助大家更好地理解,这里简单说明一下反向传播的几个关键公式的推导过程。
假设输出值为u,即W'T·h=u,则预测值为:
(1)定义目标函数
(2)求目标函数关于的偏导数
其中是第c个上下文在字典中对应的索引。
,当时,t c,j=1,否则,t c,j=0。
,表示预测值与真实值的误差。
的计算过程可用图1-12表示。
图1-12 目标函数关于的偏导计算过程示意图
(3)更新矩阵w'(即W2)
利用梯度下降法更新梯度:
式(1.8)的计算过程可用图1-13及图1-14表示。
图1-13 参数更新示意图(一)
图1-14 参数更新示意图(二)
(4)求关于W(即W1)的偏导数
其中。
所以
式(1.10)的计算过程可用图1-15和图1-16表示。
图1-15 偏导计算结果(一)
图1-16 偏导计算结果(二)
更新参数:
更新权重参数的计算过程可用图1-17和图1-18表示。
图1-17 权重参数更新结果(一)
图1-18 权重参数更新结果(二)
1.2.5 Hierarchical Softmax优化
结合上面内容,我们需要更新两个矩阵W和W′,但这两个矩阵涉及的词汇量较大(即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直接与下面的二叉树的根节点相连。
图1-19 哈夫曼树示意图
其中,白色的叶子节点表示词汇表中的所有词(这里有V个),黑色节点表示非叶子节点,每一个叶子节点其实就是一个单词,且都对应唯一的一条从根节点出发的路径。我们用n(w,j)表示从根节点到叶子节点w的路径上的第j个非叶子节点,并且每个非叶子节点都对应一个向量,其维度与h相同。
1.2.6 Negative Sampling优化
训练一个神经网络意味着要输入训练样本并不断调整神经元的权重,从而不断提高对目标预测的准确性。神经网络每训练一个样本,该样本的权重就会调整一次。正如上面所讨论的,vocabulary的大小决定了Skip-Gram神经网络的权重矩阵的具体规模,所有这些权重需要通过数以亿计的训练样本来进行调整,这是非常消耗计算资源的,并且在实际训练过程中,速度会非常慢。
Negative Sampling(负采样)解决了这个问题,它可以提高训练速度并改善所得到词向量的质量。不同于原本需要更新每个训练样本的所有权重的方法,负采样只需要每次更新一个训练样本的一小部分权重,从而在很大程度上降低了梯度下降过程中的计算量。