7 序列到序列学习(seq2seq)
- 使用两个循环神经网络的编码器和解码器,并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务
- “
”表示序列结束词元。一旦输出序列生成此词元,模型就会停止预测。在循环神经网络解码器的初始化时间步,有两个特定的设计决定:首先,特定的“ ”表示序列开始词元,它是解码器的输入序列的第一个词元。其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。
1 | import collections |
7.1 编码器
编码器将长度可变的输入序列转换成形状固定的上下文变量c,并且将输入序列的信息在该上下文变量中进行编码。
假设一个序列组成的样本(批量大小是1)。假设输入序列是$x_ 1, \ldots, x_ T$,其中$x_ t$是输入文本序列中的第$t$个词元。在时间步$t$,循环神经网络编码器将$x_ t$的词元特征向量$\boldsymbol{x}_ t$和$\boldsymbol{h}_ {t-1}$ 转换为隐状态$\boldsymbol{h}_ t$。使用一个函数f来描述rnn的循环层所做的变换:
$$\boldsymbol{h}_ t = f(\boldsymbol{x}_ t, \boldsymbol{h}_ {t-1}).$$
- 编码器通过选定的函数q,将所有时间步的隐状态转换为上下文变量:
$$\boldsymbol{c} = q(\boldsymbol{h}_ 1, \ldots, \boldsymbol{h}_ T).$$
到目前为止,我们使用的是一个单向循环神经网络来设计编码器,其中隐状态只依赖于输入子序列,这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置(包括隐状态所在的时间步)组成。
也可以使用双向循环神经网络构造编码器,其中隐状态依赖于两个输入子序列,两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列(包括隐状态所在的时间步),因此隐状态对整个序列的信息都进行了编码。
我们使用了嵌入层(embedding layer)来获得输入序列中每个词元的特征向量。嵌入层的权重是一个矩阵,其行数等于输入词表的大小(vocab_size),其列数等于特征向量的维度(embed_size)。对于任意输入词元的索引i,嵌入层获取权重矩阵的第i行(从0开始)以返回其特征向量。另外,本文选择了一个多层门控循环单元来实现编码器。
1 | #@save |
- 实例化上述编码器的实现:我们使用一个两层门控循环单元编码器,其隐藏单元数为16。给定一小批量的输入序列X(批量大小为4,时间步为7)。在完成所有时间步后,最后一层的隐状态的输出是一个张量(output由编码器的循环层返回),其形状为(时间步数,批量大小,隐藏单元数)。由于这里使用的是门控循环单元,所以在最后一个时间步的多层隐状态的形状是(隐藏层的数量,批量大小,隐藏单元的数量)。如果使用长短期记忆网络,state中还将包含记忆单元信息。
1 | encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2) |
torch.Size([7, 4, 16])
7.2 解码器
来自训练数据集的输出序列$y_ 1, y_ 2, \ldots, y_ T’$,对于每个时间步$t’$(与输入序列或编码器的时间步t不同),解码器输出$y_ {t’}$的概率取决于之前的输出子序列$y_ 1, \ldots, y_ {t’-1}$和上下文变量c,即$p(y_ {t’} \mid y_ 1, \ldots, y_ {t’-1}, \boldsymbol{c})$。
在输出序列上的任意时间步$t’$,rnn将来自上一时间步的输出$y_ {t’-1}$和上下文变量c作为其输入,然后在当前时间步将它们和上一隐状态$\boldsymbol{s}_ {t’-1}$转换为当前隐状态$\boldsymbol{s}_ {t’}$。因此,我们使用一个函数g来描述rnn的隐藏层的变换:
$$\boldsymbol{s}_ {t’} = g(y_ {t’-1}, \boldsymbol{s}_ {t’-1}, \boldsymbol{c}).$$
获得解码器的隐状态之后,我们可以使用输出层和softmax操作来计算在时间步$t’$时输出$y_ {t’}$的条件概率分布$p(y_ {t’} \mid y_ 1, \ldots, y_ {t’-1}, \boldsymbol{c})$。
实现解码器时,我们直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。为了进一步包含经过编码的输入序列的信息,上下文变量在所有的时间步与解码器的输入进行拼接(concatenate)。为了预测输出词元的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐状态。
1 | class Seq2SeqDecoder(d2l.Decoder): |
- 用与前面提到的编码器中相同的超参数来实例化解码器。
1 | decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2) |
(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))
- 模型结构
7.3 损失函数
在每个时间步,解码器预测了输出词元的概率分布。类似于语言模型,可以使用softmax来获得分布,并通过计算交叉熵损失函数来进行优化。特定的填充词元被添加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,我们应该将填充词元的预测排除在损失函数的计算之外。
sequence_mask函数通过零值化屏蔽不相关的项,以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。例如,如果两个序列的有效长度(不包括填充词元)分别为1和2,则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。
1 | #@save |
tensor([[1, 0, 0],
[4, 5, 0]])
- 还可以使用此函数屏蔽最后几个轴上的所有项。如果愿意,也可以使用指定的非零值来替换这些项。
1 | X = torch.ones(2, 3, 4) |
tensor([[[ 1., 1., 1., 1.],
[-1., -1., -1., -1.],
[-1., -1., -1., -1.]],
[[ 1., 1., 1., 1.],
[ 1., 1., 1., 1.],
[-1., -1., -1., -1.]]])
- 通过扩展softmax交叉熵损失函数来遮蔽不相关的预测。最初,所有预测词元的掩码都设置为1。一旦给定了有效长度,与填充词元对应的掩码将被设置为0。最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。
1 | #@save |
- 检测:创建三个相同的序列,分别指定这些序列的有效长度为4、2和0。第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。
1 | loss = MaskedSoftmaxCELoss() |
tensor([2.3026, 1.1513, 0.0000])
7.4 训练
- 开始词元(“<bos>”)和原始的输出序列(不包括序列结束词元“<eos>”)拼接在一起作为解码器的输入。这被称为强制教学(teacher forcing),因为原始的输出序列(词元的标签)被送入解码器。或者,将来自上一个时间步的预测得到的词元作为解码器的当前输入。
1 | #@save |
- 创建和训练一个循环神经网络“编码器-解码器”模型用于序列到序列的学习。
1 | import os |
loss 0.019, 8666.0 tokens/sec on cpu
7.5 预测
- 为了采用一个接着一个词元的方式预测输出序列,每个解码器当前时间步的输入都将来自于前一时间步的预测词元。与训练类似,序列开始词元(“
”)在初始时间步被输入到解码器中。当输出序列的预测遇到序列结束词元(“ ”)时,预测就结束了。
1 | #@save |
7.6 预测序列的评估
- 可以通过与真实的标签序列进行比较来评估预测序列。对于预测序列中的任意n元语法(n‐grams),BLEU的评估都是这个n元语法是否出现在标签序列中。我们将BLEU定义为:
$$ exp(min(0, 1 - \frac{len_ { label }}{len_ { pred }})) \prod_ {n=1} ^k P_ n^ {1/2^ n}.$$
其中$len_ { label }$是标签序列中的词元数,$len_ { pred }$是预测序列的词元数,k是用于匹配的最长的n元语法。$P_ n$是n元语法的精确度,它是两个数量的比值:第一个是预测序列与标签序列中匹配的n元语法的数量,第二个是预测序列中n元语法的数量的比率。给定标签序列A、B、C、D、E、F 和预测序列A、B、B、C、D,我们有$p_ 1 = 4/5, p_ 2 = 3/4, p_ 3 = 1/3, p_ 4 = 0$。
当预测序列与标签序列完全相同时,BLEU为1。此外,由于n元语法越长则匹配难度越大,所以BLEU为更长的n元语法的精确度分配更大的权重。具体来说,当$P_ n$固定时,$P_ n^ {1/2^ n}$随着n的增加而减小。而且,由于预测的序列越短获得的$p_ n$值越高,所以乘法项之前的系数用于惩罚较短的预测序列。例如,当k = 2时,给定标签序列A、B、C、D、E、F 和预测序列A、B,尽管$p_ 1 = 2/2, p_ 2 = 1/1, p_ 3 = p_ 4 = 0$,乘法因子$exp(1-6/2) \approx 0.14$将降低BLEU的值。
1 | def bleu(pred_seq, label_seq, k): |
- 利用训练好的循环神经网络“编码器-解码器”模型,将几个英语句子翻译成法语,并计算BLEU的最终结果。
1 | engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .'] |
go . => va !, bleu 1.000
i lost . => j'ai perdu maintenant ., bleu 0.658
he's calm . => il est paresseux ., bleu 0.658
i'm home . => je suis chez bon bien ., bleu 0.649