目录
三.使用 Transformer 和 PyTorch 的中日文机器翻译模型
3.4 构建 TorchText Vocab 对象并将句子转换为 Torch 张量
一.基础知识
1. 编码器—解码器(seq2seq)
我们已经在前两节中表征并变换了不定长的输入序列。但在自然语言处理的很多应用中,输入和输出都可以是不定长序列。以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列,例如
英语输入:“They”、“are”、“watching”、“.”
法语输出:“Ils”、“regardent”、“.”
当输入和输出都是不定长序列时,我们可以使用编码器—解码器(encoder-decoder)[1] 或者seq2seq模型 [2]。这两个模型本质上都用到了两个循环神经网络,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。
图1描述了使用编码器—解码器将上述英语句子翻译成法语句子的一种方法。在训练数据集中,我们可以在每个句子后附上特殊符号“<eos>”(end of sequence)以表示序列的终止。编码器每个时间步的输入依次为英语句子中的单词、标点和特殊符号“<eos>”。图1中使用了编码器在最终时间步的隐藏状态作为输入句子的表征或编码信息。解码器在各个时间步中使用输入句子的编码信息和上个时间步的输出以及隐藏状态作为输入。我们希望解码器在各个时间步能正确依次输出翻译后的法语单词、标点和特殊符号"<eos>"。需要注意的是,解码器在最初时间步的输入用到了一个表示序列开始的特殊符号"<bos>"(beginning of sequence)。
图1 使用编码器—解码器将句子由英语翻译成法语。编码器和解码器分别为循环神经网络
1.1 .1 编码器
编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。
让我们考虑批量大小为1的时序数据样本。假设输入序列是,例如
是输入句子中的第
个词。在时间步
,循环神经网络将输入
的特征向量
和上个时间步的隐藏状态
变换为当前时间步的隐藏状态
。我们可以用函数𝑓表达循环神经网络隐藏层的变换:
接下来,编码器通过自定义函数𝑞将各个时间步的隐藏状态变换为背景变量
例如,当选择时,背景变量是输入序列最终时间步的隐藏状态ℎ𝑇。
以上描述的编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,编码器每个时间步的隐藏状态同时取决于该时间步之前和之后的子序列(包括当前时间步的输入),并编码了整个序列的信息。
1.1.2 解码器
刚刚已经介绍,编码器输出的背景变量编码了整个输入序列
的信息。给定训练样本中的输出序列
,对每个时间步
(符号与输入序列或编码器的时间步𝑡有区别),解码器输出𝑦𝑡′′的条件概率将基于之前的输出序列𝑦1,…,𝑦𝑡′−1和背景变量
,即
为此,我们可以使用另一个循环神经网络作为解码器。在输出序列的时间步,解码器将上一时间步的输出
以及背景变量
作为输入,并将它们与上一时间步的隐藏状态
变换为当前时间步的隐藏状态
。因此,我们可以用函数𝑔表达解码器隐藏层的变换:
有了解码器的隐藏状态后,我们可以使用自定义的输出层和softmax运算来计算,例如,基于当前时间步的解码器隐藏状态
、上一时间步的输出
以及背景变量
来计算当前时间步输出𝑦𝑡′的概率分布。
1.1.3 训练模型
根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率
并得到该输出序列的损失
在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。在图1所描述的模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上一个时间步的标签作为解码器在当前时间步的输入。这叫作强制教学(teacher forcing)。
2.束搜索
上一节介绍了如何训练输入和输出均为不定长序列的编码器—解码器。本节我们介绍如何使用编码器—解码器来预测不定长的序列。
上一节里已经提到,在准备训练数据集时,我们通常会在样本的输入序列和输出序列后面分别附上一个特殊符号"<eos>"表示序列的终止。我们在接下来的讨论中也将沿用上一节的全部数学符号。为了便于讨论,假设解码器的输出是一段文本序列。设输出文本词典(包含特殊符号"<eos>")的大小为
,输出序列的最大长度为
。所有可能的输出序列一共有
种。这些输出序列中所有特殊符号"<eos>"后面的子序列将被舍弃。
1.2.1 贪婪搜索
让我们先来看一个简单的解决方案:贪婪搜索(greedy search)。对于输出序列任一时间步,我们从
个词中搜索出条件概率最大的词
作为输出。一旦搜索出"<eos>"符号,或者输出序列长度已经达到了最大长度,便完成输出。
我们在描述解码器时提到,基于输入序列生成输出序列的条件概率是
我们将该条件概率最大的输出序列称为最优输出序列。而贪婪搜索的主要问题是不能保证得到最优输出序列。
下面来看一个例子。假设输出词典里面有“A”“B”“C”和“<eos>”这4个词。图2中每个时间步下的4个数字分别代表了该时间步生成“A”“B”“C”和“<eos>”这4个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词。因此,图10.9中将生成输出序列“A”“B”“C”“<eos>”。该输出序列的条件概率是0.5×0.4×0.4×0.6=0.0480.5×0.4×0.4×0.6=0.048。
图2 在每个时间步,贪婪搜索选取条件概率最大的词
接下来,观察图3演示的例子。与图2中不同,图3在时间步2中选取了条件概率第二大的词“C”。由于时间步3所基于的时间步1和2的输出子序列由图10.9中的“A”“B”变为了图10.10中的“A”“C”,图3中时间步3生成各个词的条件概率发生了变化。我们选取条件概率最大的词“B”。此时时间步4所基于的前3个时间步的输出子序列为“A”“C”“B”,与图10.9中的“A”“B”“C”不同。因此,图10.10中时间步4生成各个词的条件概率也与图10.9中的不同。我们发现,此时的输出序列“A”“C”“B”“<eos>”的条件概率是0.5×0.3×0.6×0.6=0.0540.5×0.3×0.6×0.6=0.054,大于贪婪搜索得到的输出序列的条件概率。因此,贪婪搜索得到的输出序列“A”“B”“C”“<eos>”并非最优输出序列。
图3 在时间步2选取条件概率第二大的词“C”
1.2.2 穷举搜索
如果目标是得到最优输出序列,我们可以考虑穷举搜索(exhaustive search):穷举所有可能的输出序列,输出条件概率最大的序列。
虽然穷举搜索可以得到最优输出序列,但它的计算开销很容易过大。例如,当
且
时,我们将评估
个序列:这几乎不可能完成。而贪婪搜索的计算开销是
,通常显著小于穷举搜索的计算开销。例如,当
且
时,我们只需评估
个序列。
1.2.3 束搜索
束搜索(beam search)是对贪婪搜索的一个改进算法。它有一个束宽(beam size)超参数。我们将它设为。在时间步1时,选取当前时间步条件概率最大的
个词,分别组成
个候选输出序列的首词。在之后的每个时间步,基于上个时间步的
个候选输出序列,从
个可能的输出序列中选取条件概率最大的
个,作为该时间步的候选输出序列。最终,我们从各个时间步的候选输出序列中筛选出包含特殊符号“<eos>”的序列,并将它们中所有特殊符号“<eos>”后面的子序列舍弃,得到最终候选输出序列的集合。
图4 束搜索的过程。束宽为2,输出序列最大长度为3。候选输出序列有A、C、AB、CE、ABD和CED
图4通过一个例子演示了束搜索的过程。假设输出序列的词典中只包含5个元素,即,且其中一个为特殊符号“<eos>”。设束搜索的束宽等于2,输出序列最大长度为3。在输出序列的时间步1时,假设条件概率
最大的2个词为
和
。我们在时间步2时将对所有的
都分别计算
和
,并从计算出的10个条件概率中取最大的2个,假设为
和
。那么,我们在时间步3时将对所有的
都分别计算
和
,并从计算出的10个条件概率中取最大的2个,假设为
和
。如此一来,我们得到6个候选输出序列:(1)
;(2)
;(3)
、
;(4)
、
;(5)
、
、
和(6)
、
、
。接下来,我们将根据这6个序列得出最终候选输出序列的集合