From 71e8c33026f2286811109cec1258595b84bf269e Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Sun, 14 Nov 2021 18:55:39 -0800 Subject: [PATCH] seq2seq polish done --- chapter_recurrent-modern/seq2seq.md | 277 +++++++++++++++++++++------- 1 file changed, 207 insertions(+), 70 deletions(-) diff --git a/chapter_recurrent-modern/seq2seq.md b/chapter_recurrent-modern/seq2seq.md index 365d4308e..1905f2f75 100644 --- a/chapter_recurrent-modern/seq2seq.md +++ b/chapter_recurrent-modern/seq2seq.md @@ -1,30 +1,47 @@ # 序列到序列学习(seq2seq) :label:`sec_seq2seq` -正如我们在 :numref:`sec_machine_translation`中看到的,机器翻译中的输入序列和输出序列都是长度可变的。 -为了解决这类问题,我们在 :numref:`sec_encoder-decoder`中设计了一个通用的”编码器-解码器“架构。 -在本节中,我们将使用两个循环神经网络来设计这个架构中的编码器和解码器,并将其应用于机器翻译的*序列到序列*(sequence to sequence,seq2seq)学习 :cite:`Sutskever.Vinyals.Le.2014,Cho.Van-Merrienboer.Gulcehre.ea.2014`。 - -遵循编码器-解码器架构的设计原则,循环神经网络编码器可以使用长度可变的序列作为输入,将其转换为形状固定的隐状态。 -换言之,输入(源)序列的信息被*编码*到循环神经网络编码器的隐状态中。 -为了连续生成输出序列的词元,独立的循环神经网络解码器是基于输入序列的编码信息和输出序列已经看见的或者生成的词元来预测下一个词元。 - :numref:`fig_seq2seq`演示了如何在机器翻译中使用两个循环神经网络进行序列到序列学习。 - -![使用循环神经网络编码器和循环神经网络解码器的序列到序列学习。](../img/seq2seq.svg) +正如我们在 :numref:`sec_machine_translation`中看到的, +机器翻译中的输入序列和输出序列都是长度可变的。 +为了解决这类问题,我们在 :numref:`sec_encoder-decoder`中 +设计了一个通用的”编码器-解码器“架构。 +本节,我们将使用两个循环神经网络的编码器和解码器, +并将其应用于*序列到序列*(sequence to sequence,seq2seq)类的学习任务 + :cite:`Sutskever.Vinyals.Le.2014,Cho.Van-Merrienboer.Gulcehre.ea.2014`。 + +遵循编码器-解码器架构的设计原则, +循环神经网络编码器使用长度可变的序列作为输入, +将其转换为固定形状的隐状态。 +换言之,输入序列的信息被*编码*到循环神经网络编码器的隐状态中。 +为了连续生成输出序列的词元, +独立的循环神经网络解码器是基于输入序列的编码信息 +和输出序列已经看见的或者生成的词元来预测下一个词元。 + :numref:`fig_seq2seq`演示了 +如何在机器翻译中使用两个循环神经网络进行序列到序列学习。 + +![使用循环神经网络编码器和循环神经网络解码器的序列到序列学习](../img/seq2seq.svg) :label:`fig_seq2seq` -在 :numref:`fig_seq2seq`中,特定的“<eos>”表示序列结束词元。 -一旦输出序列生成此词元,模型就可以停止执行预测。 -在循环神经网络解码器的初始化时间步,有两个特定的设计决定。 +在 :numref:`fig_seq2seq`中, +特定的“<eos>”表示序列结束词元。 +一旦输出序列生成此词元,模型就会停止预测。 +在循环神经网络解码器的初始化时间步,有两个特定的设计决定: 首先,特定的“<bos>”表示序列开始词元,它是解码器的输入序列的第一个词元。 其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。 -例如,在 :cite:`Sutskever.Vinyals.Le.2014`的设计中,正是基于这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。 -在其他一些设计中 :cite:`Cho.Van-Merrienboer.Gulcehre.ea.2014`,如 :numref:`fig_seq2seq`所示,编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 -类似于 :numref:`sec_language_model`中语言模型的训练,可以允许标签成为原始的输出序列, -从源序列词元“<bos>”、“Ils”、“regardent”、“.”到新序列词元 +例如,在 :cite:`Sutskever.Vinyals.Le.2014`的设计中, +正是基于这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。 +在其他一些设计中 :cite:`Cho.Van-Merrienboer.Gulcehre.ea.2014`, +如 :numref:`fig_seq2seq`所示, +编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 +类似于 :numref:`sec_language_model`中语言模型的训练, +可以允许标签成为原始的输出序列, +从源序列词元“<bos>”、“Ils”、“regardent”、“.” +到新序列词元 “Ils”、“regardent”、“.”、“<eos>”来移动预测的位置。 -下面,我们将对 :numref:`fig_seq2seq`的设计进行更详细的解释,并且将基于 :numref:`sec_machine_translation`中介绍的“英-法”数据集来训练这个机器翻译模型。 +下面,我们动手构建 :numref:`fig_seq2seq`的设计, +并将基于 :numref:`sec_machine_translation`中 +介绍的“英-法”数据集来训练这个机器翻译模型。 ```{.python .input} import collections @@ -54,26 +71,54 @@ import tensorflow as tf ## 编码器 -从技术上讲,编码器将长度可变的输入序列转换成形状固定的*上下文变量*$\mathbf{c}$,并且将输入序列的信息在该上下文变量中进行编码。如 :numref:`fig_seq2seq`所示,可以使用循环神经网络来设计编码器。 +从技术上讲,编码器将长度可变的输入序列转换成 +形状固定的上下文变量$\mathbf{c}$, +并且将输入序列的信息在该上下文变量中进行编码。 +如 :numref:`fig_seq2seq`所示,可以使用循环神经网络来设计编码器。 -让我们考虑由一个序列组成的样本(批量大小是$1$)。假设输入序列是$x_1, \ldots, x_T$,其中$x_t$是输入文本序列中的第$t$个词元。在时间步$t$,循环神经网络将词元$x_t$的输入特征向量$\mathbf{x}_t$和$\mathbf{h} _{t-1}$(即上一时间步的隐状态)转换为$\mathbf{h}_t$(即当前隐状态)。使用一个函数$f$来描述循环神经网络的循环层所做的变换: +考虑由一个序列组成的样本(批量大小是$1$)。 +假设输入序列是$x_1, \ldots, x_T$, +其中$x_t$是输入文本序列中的第$t$个词元。 +在时间步$t$,循环神经网络将词元$x_t$的输入特征向量 +$\mathbf{x}_t$和$\mathbf{h} _{t-1}$(即上一时间步的隐状态) +转换为$\mathbf{h}_t$(即当前步的隐状态)。 +使用一个函数$f$来描述循环神经网络的循环层所做的变换: $$\mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}). $$ -总之,编码器通过选定的函数$q$将所有时间步的隐状态转换为上下文变量: +总之,编码器通过选定的函数$q$, +将所有时间步的隐状态转换为上下文变量: $$\mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T).$$ -比如,当选择$q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T$时(就像 :numref:`fig_seq2seq`中一样),上下文变量仅仅是输入序列在最后时间步的隐状态$\mathbf{h}_T$。 - -到目前为止,我们使用的是一个单向循环神经网络来设计编码器,其中隐状态只依赖于输入子序列,这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置(包括隐状态所在的时间步)组成。我们也可以使用双向循环神经网络构造编码器,其中隐状态依赖于两个输入子序列,两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列(包括隐状态所在的时间步),因此隐状态对整个序列的信息都进行了编码。 - -现在,让我们[**实现循环神经网络编码器**]。注意,我们使用了*嵌入层*(embedding layer)来获得输入序列中每个词元的特征向量。嵌入层的权重是一个矩阵,其行数等于输入词表的大小(`vocab_size`),其列数等于特征向量的维度(`embed_size`)。对于任意输入词元的索引$i$,嵌入层获取权重矩阵的第$i$行(从$0$开始)以返回其特征向量。另外,本文选择了一个多层门控循环单元来实现编码器。 +比如,当选择$q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T$时 +(就像 :numref:`fig_seq2seq`中一样), +上下文变量仅仅是输入序列在最后时间步的隐状态$\mathbf{h}_T$。 + +到目前为止,我们使用的是一个单向循环神经网络来设计编码器, +其中隐状态只依赖于输入子序列, +这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置 +(包括隐状态所在的时间步)组成。 +我们也可以使用双向循环神经网络构造编码器, +其中隐状态依赖于两个输入子序列, +两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列 +(包括隐状态所在的时间步), +因此隐状态对整个序列的信息都进行了编码。 + +现在,让我们[**实现循环神经网络编码器**]。 +注意,我们使用了*嵌入层*(embedding layer) +来获得输入序列中每个词元的特征向量。 +嵌入层的权重是一个矩阵, +其行数等于输入词表的大小(`vocab_size`), +其列数等于特征向量的维度(`embed_size`)。 +对于任意输入词元的索引$i$, +嵌入层获取权重矩阵的第$i$行(从$0$开始)以返回其特征向量。 +另外,本文选择了一个多层门控循环单元来实现编码器。 ```{.python .input} #@save class Seq2SeqEncoder(d2l.Encoder): - """用于序列到序列学习的循环神经网络编码器。""" + """用于序列到序列学习的循环神经网络编码器""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) @@ -97,7 +142,7 @@ class Seq2SeqEncoder(d2l.Encoder): #@tab pytorch #@save class Seq2SeqEncoder(d2l.Encoder): - """用于序列到序列学习的循环神经网络编码器。""" + """用于序列到序列学习的循环神经网络编码器""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) @@ -122,7 +167,7 @@ class Seq2SeqEncoder(d2l.Encoder): #@tab tensorflow #@save class Seq2SeqEncoder(d2l.Encoder): - """用于序列到序列学习的循环神经网络编码器。""" + """用于序列到序列学习的循环神经网络编码器""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super().__init__(*kwargs) # 嵌入层 @@ -141,7 +186,14 @@ class Seq2SeqEncoder(d2l.Encoder): return output[0], state ``` -循环层返回变量的说明可以参考 :numref:`sec_rnn-concise`。让我们使用一个具体的例子来说明[**上述编码器的实现**]。下面将实例化一个两层门控循环单元编码器,其隐藏单元数为$16$。给定一小批量的输入序列`X`(批量大小为$4$,时间步为$7$)。在完成所有时间步后,最后一层的隐状态的输出是一个张量(`output `由编码器的循环层返回),其形状为(时间步数,批量大小,隐藏单元数)。 +循环层返回变量的说明可以参考 :numref:`sec_rnn-concise`。 + +下面,我们实例化[**上述编码器的实现**]: +我们使用一个两层门控循环单元编码器,其隐藏单元数为$16$。 +给定一小批量的输入序列`X`(批量大小为$4$,时间步为$7$)。 +在完成所有时间步后, +最后一层的隐状态的输出是一个张量(`output`由编码器的循环层返回), +其形状为(时间步数,批量大小,隐藏单元数)。 ```{.python .input} encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -170,7 +222,10 @@ output, state = encoder(X, training=False) output.shape ``` -由于这里使用的是门控循环单元,所以在最后一个时间步的多层隐状态的形状是(隐藏层的数量,批量大小,隐藏单元的数量)。如果使用长短期记忆网络,`state`中还将包含记忆单元信息。 +由于这里使用的是门控循环单元, +所以在最后一个时间步的多层隐状态的形状是 +(隐藏层的数量,批量大小,隐藏单元的数量)。 +如果使用长短期记忆网络,`state`中还将包含记忆单元信息。 ```{.python .input} len(state), state[0].shape @@ -189,20 +244,43 @@ len(state), [element.shape for element in state] ## [**解码器**] :label:`sec_seq2seq_decoder` -正如上文提到的,编码器输出的上下文变量$\mathbf{c}$对整个输入序列$x_1, \ldots, x_T$进行编码。来自训练数据集的输出序列$y_1, y_2, \ldots, y_{T'}$,对于每个时间步$t'$(与输入序列或编码器的时间步$t$不同),解码器输出$y_{t'}$的概率取决于先前的输出子序列$y_1, \ldots, y_{t'-1}$和上下文变量$\mathbf{c}$,即$P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$。 - -为了在序列上模型化这种条件概率,我们可以使用另一个循环神经网络作为解码器。在输出序列上的任意时间步$t^\prime$,循环神经网络将来自上一时间步的输出$y_{t^\prime-1}$和上下文变量$\mathbf{c}$作为其输入,然后在当前时间步将它们和上一隐状态$\mathbf{s}_{t^\prime-1}$转换为隐状态$\mathbf{s}_{t^\prime}$。因此,可以使用函数$g$来表示解码器的隐藏层的变换: +正如上文提到的,编码器输出的上下文变量$\mathbf{c}$ +对整个输入序列$x_1, \ldots, x_T$进行编码。 +来自训练数据集的输出序列$y_1, y_2, \ldots, y_{T'}$, +对于每个时间步$t'$(与输入序列或编码器的时间步$t$不同), +解码器输出$y_{t'}$的概率取决于先前的输出子序列 +$y_1, \ldots, y_{t'-1}$和上下文变量$\mathbf{c}$, +即$P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$。 + +为了在序列上模型化这种条件概率, +我们可以使用另一个循环神经网络作为解码器。 +在输出序列上的任意时间步$t^\prime$, +循环神经网络将来自上一时间步的输出$y_{t^\prime-1}$ +和上下文变量$\mathbf{c}$作为其输入, +然后在当前时间步将它们和上一隐状态 +$\mathbf{s}_{t^\prime-1}$转换为 +隐状态$\mathbf{s}_{t^\prime}$。 +因此,可以使用函数$g$来表示解码器的隐藏层的变换: $$\mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-1}).$$ :eqlabel:`eq_seq2seq_s_t` -在获得解码器的隐状态之后,我们可以使用输出层和softmax操作来计算在时间步$t^\prime$时输出$y_{t^\prime}$的条件概率分布$P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})$。 +在获得解码器的隐状态之后, +我们可以使用输出层和softmax操作 +来计算在时间步$t^\prime$时输出$y_{t^\prime}$的条件概率分布 +$P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})$。 -根据 :numref:`fig_seq2seq`,当实现解码器时,我们直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。为了进一步包含经过编码的输入序列的信息,上下文变量在所有的时间步与解码器的输入进行拼接(concatenate)。为了预测输出词元的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐状态。 +根据 :numref:`fig_seq2seq`,当实现解码器时, +我们直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。 +这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。 +为了进一步包含经过编码的输入序列的信息, +上下文变量在所有的时间步与解码器的输入进行拼接(concatenate)。 +为了预测输出词元的概率分布, +在循环神经网络解码器的最后一层使用全连接层来变换隐状态。 ```{.python .input} class Seq2SeqDecoder(d2l.Decoder): - """用于序列到序列学习的循环神经网络解码器。""" + """用于序列到序列学习的循环神经网络解码器""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqDecoder, self).__init__(**kwargs) @@ -232,7 +310,7 @@ class Seq2SeqDecoder(d2l.Decoder): ```{.python .input} #@tab pytorch class Seq2SeqDecoder(d2l.Decoder): - """用于序列到序列学习的循环神经网络解码器。""" + """用于序列到序列学习的循环神经网络解码器""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqDecoder, self).__init__(**kwargs) @@ -260,7 +338,7 @@ class Seq2SeqDecoder(d2l.Decoder): ```{.python .input} #@tab tensorflow class Seq2SeqDecoder(d2l.Decoder): - """用于序列到序列学习的循环神经网络解码器。""" + """用于序列到序列学习的循环神经网络解码器""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super().__init__(**kwargs) @@ -287,7 +365,9 @@ class Seq2SeqDecoder(d2l.Decoder): return output, rnn_output[1:] ``` -下面,我们用与前面提到的编码器中相同的超参数,来[**实例化解码器**]。如我们所见,解码器的输出形状变为(批量大小,时间步数,词表大小),其中张量的最后一个维度存储预测的词元分布。 +下面,我们用与前面提到的编码器中相同的超参数来[**实例化解码器**]。 +如我们所见,解码器的输出形状变为(批量大小,时间步数,词表大小), +其中张量的最后一个维度存储预测的词元分布。 ```{.python .input} decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -316,16 +396,27 @@ output, state = decoder(X, state, training=False) output.shape, len(state), state[0].shape ``` -总之,上述循环神经网络“编码器-解码器”模型中的各层如 :numref:`fig_seq2seq_details`所示。 +总之,上述循环神经网络“编码器-解码器”模型中的各层如 + :numref:`fig_seq2seq_details`所示。 -![循环神经网络编码器-解码器模型中的层。](../img/seq2seq-details.svg) +![循环神经网络编码器-解码器模型中的层](../img/seq2seq-details.svg) :label:`fig_seq2seq_details` ## 损失函数 -在每个时间步,解码器预测了输出词元的概率分布。类似于语言模型,可以使用softmax来获得分布,并通过计算交叉熵损失函数来进行优化。回想一下 :numref:`sec_machine_translation`中,特定的填充词元被添加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,应该将填充词元的预测排除在损失函数的计算之外。 +在每个时间步,解码器预测了输出词元的概率分布。 +类似于语言模型,可以使用softmax来获得分布, +并通过计算交叉熵损失函数来进行优化。 +回想一下 :numref:`sec_machine_translation`中, +特定的填充词元被添加到序列的末尾, +因此不同长度的序列可以以相同形状的小批量加载。 +但是,我们应该将填充词元的预测排除在损失函数的计算之外。 -为此,我们可以使用下面的`sequence_mask`函数[**通过零值化屏蔽不相关的项**],以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。例如,如果两个序列的有效长度(不包括填充词元)分别为$1$和$2$,则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。 +为此,我们可以使用下面的`sequence_mask`函数 +[**通过零值化屏蔽不相关的项**], +以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。 +例如,如果两个序列的有效长度(不包括填充词元)分别为$1$和$2$, +则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。 ```{.python .input} X = np.array([[1, 2, 3], [4, 5, 6]]) @@ -336,7 +427,7 @@ npx.sequence_mask(X, np.array([1, 2]), True, axis=1) #@tab pytorch #@save def sequence_mask(X, valid_len, value=0): - """在序列中屏蔽不相关的项。""" + """在序列中屏蔽不相关的项""" maxlen = X.size(1) mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:, None] @@ -351,7 +442,7 @@ sequence_mask(X, torch.tensor([1, 2])) #@tab tensorflow #@save def sequence_mask(X, valid_len, value=0): - """在序列中屏蔽不相关的项。""" + """在序列中屏蔽不相关的项""" maxlen = X.shape[1] mask = tf.range(start=0, limit=maxlen, dtype=tf.float32)[ None, :] < tf.cast(valid_len[:, None], dtype=tf.float32) @@ -384,7 +475,10 @@ X = tf.ones((2,3,4)) sequence_mask(X, tf.constant([1, 2]), value=-1) ``` -现在,我们可以[**通过扩展softmax交叉熵损失函数来遮蔽不相关的预测**]。最初,所有预测词元的掩码都设置为1。一旦给定了有效长度,与填充词元对应的掩码将被设置为0。最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。 +现在,我们可以[**通过扩展softmax交叉熵损失函数来遮蔽不相关的预测**]。 +最初,所有预测词元的掩码都设置为1。 +一旦给定了有效长度,与填充词元对应的掩码将被设置为0。 +最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。 ```{.python .input} #@save @@ -440,7 +534,9 @@ class MaskedSoftmaxCELoss(tf.keras.losses.Loss): return weighted_loss ``` -我们可以创建三个相同的序列来进行[**代码健全性检查**],然后分别指定这些序列的有效长度为$4$、$2$和$0$。结果就是,第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。 +我们可以创建三个相同的序列来进行[**代码健全性检查**], +然后分别指定这些序列的有效长度为$4$、$2$和$0$。 +结果就是,第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。 ```{.python .input} loss = MaskedSoftmaxCELoss() @@ -463,12 +559,18 @@ loss(tf.ones((3,4), dtype = tf.int32), tf.ones((3, 4, 10))).numpy() ## [**训练**] :label:`sec_seq2seq_training` -在下面的循环训练过程中,如 :numref:`fig_seq2seq`所示,特定的序列开始词元(“<bos>”)和原始的输出序列(不包括序列结束词元“<eos>”)拼接在一起作为解码器的输入。这被称为*教师强制*(teacher forcing),因为原始的输出序列(词元的标签)被送入解码器。或者,将来自上一个时间步的*预测*得到的词元作为解码器的当前输入。 +在下面的循环训练过程中,如 :numref:`fig_seq2seq`所示, +特定的序列开始词元(“<bos>”)和 +原始的输出序列(不包括序列结束词元“<eos>”) +拼接在一起作为解码器的输入。 +这被称为*强制教学*(teacher forcing), +因为原始的输出序列(词元的标签)被送入解码器。 +或者,将来自上一个时间步的*预测*得到的词元作为解码器的当前输入。 ```{.python .input} #@save def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """训练序列到序列模型。""" + """训练序列到序列模型""" net.initialize(init.Xavier(), force_reinit=True, ctx=device) trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr}) @@ -483,7 +585,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): x.as_in_ctx(device) for x in batch] bos = np.array([tgt_vocab['']] * Y.shape[0], ctx=device).reshape(-1, 1) - dec_input = np.concatenate([bos, Y[:, :-1]], 1) # 教师强制 + dec_input = np.concatenate([bos, Y[:, :-1]], 1) # 强制教学 with autograd.record(): Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) @@ -502,7 +604,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): #@tab pytorch #@save def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """训练序列到序列模型。""" + """训练序列到序列模型""" def xavier_init_weights(m): if type(m) == nn.Linear: nn.init.xavier_uniform_(m.weight) @@ -526,7 +628,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch] bos = torch.tensor([tgt_vocab['']] * Y.shape[0], device=device).reshape(-1, 1) - dec_input = torch.cat([bos, Y[:, :-1]], 1) # 教师强制 + dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学 Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) l.sum().backward() # 损失函数的标量进行“反向传播” @@ -545,7 +647,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): #@tab tensorflow #@save def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """训练序列到序列模型。""" + """训练序列到序列模型""" optimizer = tf.keras.optimizers.Adam(learning_rate=lr) animator = d2l.Animator(xlabel="epoch", ylabel="loss", xlim=[10, num_epochs]) @@ -556,7 +658,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): X, X_valid_len, Y, Y_valid_len = [x for x in batch] bos = tf.reshape(tf.constant([tgt_vocab['']] * Y.shape[0]), shape=(-1, 1)) - dec_input = tf.concat([bos, Y[:, :-1]], 1) # 教师强制 + dec_input = tf.concat([bos, Y[:, :-1]], 1) # 强制教学 with tf.GradientTape() as tape: Y_hat, _ = net(X, dec_input, X_valid_len, training=True) l = MaskedSoftmaxCELoss(Y_valid_len)(Y, Y_hat) @@ -571,7 +673,8 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): f'tokens/sec on {str(device)}') ``` -现在,在机器翻译数据集上,我们可以[**创建和训练一个循环神经网络“编码器-解码器”模型**]用于序列到序列的学习。 +现在,在机器翻译数据集上,我们可以 +[**创建和训练一个循环神经网络“编码器-解码器”模型**]用于序列到序列的学习。 ```{.python .input} #@tab all @@ -590,7 +693,12 @@ train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ## [**预测**] -为了采用一个接着一个词元的方式预测输出序列,每个解码器当前时间步的输入都将来自于前一时间步的预测词元。与训练类似,序列开始词元(“<bos>”)在初始时间步被输入到解码器中。该预测过程如 :numref:`fig_seq2seq_predict`所示。当输出序列的预测遇到序列结束词元(“<eos>”)时,预测就结束了。 +为了采用一个接着一个词元的方式预测输出序列, +每个解码器当前时间步的输入都将来自于前一时间步的预测词元。 +与训练类似,序列开始词元(“<bos>”) +在初始时间步被输入到解码器中。 +该预测过程如 :numref:`fig_seq2seq_predict`所示, +当输出序列的预测遇到序列结束词元(“<eos>”)时,预测就结束了。 ![使用循环神经网络编码器-解码器逐词元地预测输出序列。](../img/seq2seq-predict.svg) :label:`fig_seq2seq_predict` @@ -611,7 +719,8 @@ def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, enc_outputs = net.encoder(enc_X, enc_valid_len) dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) # 添加批量轴 - dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), axis=0) + dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), + axis=0) output_seq, attention_weight_seq = [], [] for _ in range(num_steps): Y, dec_state = net.decoder(dec_X, dec_state) @@ -693,28 +802,54 @@ def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, if pred == tgt_vocab['']: break output_seq.append(pred.numpy()) - return ' '.join(tgt_vocab.to_tokens(tf.reshape(output_seq, shape = -1).numpy().tolist())), attention_weight_seq + return ' '.join(tgt_vocab.to_tokens( + tf.reshape(output_seq, shape = -1).numpy().tolist())), + attention_weight_seq ``` ## 预测序列的评估 -我们可以通过与真实的标签序列进行比较来评估预测序列。虽然BLEU(Bilingual Evaluation Understudy)的提出最先是用于评估机器翻译的结果 :cite:`Papineni.Roukos.Ward.ea.2002`,但现在它已经被广泛用于测量许多应用的输出序列的质量。原则上说,对于预测序列中的任意$n$元语法(n-grams),BLEU的评估都是这个$n$元语法是否出现在标签序列中。 +我们可以通过与真实的标签序列进行比较来评估预测序列。 +虽然 :cite:`Papineni.Roukos.Ward.ea.2002` +提出的BLEU(bilingual evaluation understudy) +最先是用于评估机器翻译的结果, +但现在它已经被广泛用于测量许多应用的输出序列的质量。 +原则上说,对于预测序列中的任意$n$元语法(n-grams), +BLEU的评估都是这个$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$。另外,$\mathrm{len}_{\text{label}}$表示标签序列中的词元数和$\mathrm{len}_{\text{pred}}$表示预测序列中的词元数。那么,BLEU的定义是: +我们将BLEU定义为: $$ \exp\left(\min\left(0, 1 - \frac{\mathrm{len}_{\text{label}}}{\mathrm{len}_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n},$$ :eqlabel:`eq_bleu` -其中$k$是用于匹配的最长的$n$元语法。 - -根据 :eqref:`eq_bleu`中BLEU的定义,当预测序列与标签序列完全相同时,BLEU为$1$。此外,由于$n$元语法越长则匹配难度越大,所以BLEU为更长的$n$元语法的精确度分配更大的权重。具体来说,当$p_n$固定时,$p_n^{1/2^n}$会随着$n$的增长而增加(原始论文使用$p_n^{1/n}$)。而且,由于预测的序列越短获得的$p_n$值越高,所以 :eqref:`eq_bleu`中乘法项之前的系数用于惩罚较短的预测序列。例如,当$k=2$时,给定标签序列$A$、$B$、$C$、$D$、$E$、$F$和预测序列$A$、$B$,尽管$p_1 = p_2 = 1$,惩罚因子$\exp(1-6/2) \approx 0.14$会降低BLEU。 +其中$\mathrm{len}_{\text{label}}$表示标签序列中的词元数和 +$\mathrm{len}_{\text{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$。 + +根据 :eqref:`eq_bleu`中BLEU的定义, +当预测序列与标签序列完全相同时,BLEU为$1$。 +此外,由于$n$元语法越长则匹配难度越大, +所以BLEU为更长的$n$元语法的精确度分配更大的权重。 +具体来说,当$p_n$固定时,$p_n^{1/2^n}$ +会随着$n$的增长而增加(原始论文使用$p_n^{1/n}$)。 +而且,由于预测的序列越短获得的$p_n$值越高, +所以 :eqref:`eq_bleu`中乘法项之前的系数用于惩罚较短的预测序列。 +例如,当$k=2$时,给定标签序列$A$、$B$、$C$、$D$、$E$、$F$ +和预测序列$A$、$B$,尽管$p_1 = p_2 = 1$, +惩罚因子$\exp(1-6/2) \approx 0.14$会降低BLEU。 [**BLEU的代码实现**]如下。 ```{.python .input} #@tab all def bleu(pred_seq, label_seq, k): #@save - """计算 BLEU""" + """计算BLEU""" pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') len_pred, len_label = len(pred_tokens), len(label_tokens) score = math.exp(min(0, 1 - len_label / len_pred)) @@ -730,7 +865,8 @@ def bleu(pred_seq, label_seq, k): #@save return score ``` -最后,利用训练好的循环神经网络“编码器-解码器”模型[**将几个英语句子翻译成法语**],并计算BLEU的最终结果。 +最后,利用训练好的循环神经网络“编码器-解码器”模型, +[**将几个英语句子翻译成法语**],并计算BLEU的最终结果。 ```{.python .input} #@tab mxnet, pytorch @@ -754,18 +890,19 @@ for eng, fra in zip(engs, fras): ## 小结 -* 根据“编码器-解码器”架构的设计,我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。 +* 根据“编码器-解码器”架构的设计, + 我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。 * 在实现编码器和解码器时,我们可以使用多层循环神经网络。 * 我们可以使用遮蔽来过滤不相关的计算,例如在计算损失时。 -* 在“编码器-解码器”训练中,教师强制方法将原始输出序列(而非预测结果)输入解码器。 -* BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的$n$元语法的匹配度来实现。 +* 在“编码器-解码器”训练中,强制教学方法将原始输出序列(而非预测结果)输入解码器。 +* BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的$n$元语法的匹配度来评估预测。 ## 练习 1. 你能通过调整超参数来改善翻译效果吗? 1. 重新运行实验并在计算损失时不使用遮蔽。你观察到什么结果?为什么? 1. 如果编码器和解码器的层数或者隐藏单元数不同,那么如何初始化解码器的隐状态? -1. 在训练中,如果用前一时间步的预测输入到解码器来代替教师强制,对性能有何影响? +1. 在训练中,如果用前一时间步的预测输入到解码器来代替强制教学,对性能有何影响? 1. 用长短期记忆网络替换门控循环单元重新运行实验。 1. 有没有其他方法来设计解码器的输出层?