《动手学深度学习 Pytorch版》 9.7 序列到序列学习(seq2seq)

发布时间 2023-10-20 11:54:53作者: AncilunKiang

循环神经网络编码器使用长度可变的序列作为输入,将其编码到循环神经网络编码器固定形状的隐状态中。

为了连续生成输出序列的词元,独立的循环神经网络解码器是基于输入序列的编码信息和输出序列已经看见的或者生成的词元来预测下一个词元。

image

要点:

  • “<eos>”表示序列结束词元,一旦输出序列生成此词元,模型就会停止预测。

  • “<bos>”表示序列开始词元,它是解码器的输入序列的第一个词元。

  • 使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。

  • 允许标签成为原始的输出序列

import collections
import math
import torch
from torch import nn
from d2l import torch as d2l

9.7.1 编码器

使用函数 \(f\) 描述循环神经网络的循环层所做的变换:

\[\boldsymbol{h}_t=f(\boldsymbol{x}_t,\boldsymbol{h}_{t-1}) \]

参数字典:

  • \(\boldsymbol{x}_t\) 表示词元 \(x_t\) 的输入特征向量

  • \(\boldsymbol{h}_{t-1}\) 是词元 \(x_t\) 的另一个输入向量,即上一时间步的隐状态

  • \(\boldsymbol{h}_t\) 表示当前步的隐状态

总之,编码器通过选定的函数 \(q\) 将所有时间步的隐状态转换为上下文变量:

\[\boldsymbol{c}=q(\boldsymbol{h}_t,\dots,\boldsymbol{h}_T) \]

到目前为止使用单向循环神经网络设计的编码器中的隐状态只依赖于由输入序列的开始位置到隐状态所在的时间步的位置 (包括隐状态所在的时间步)组成的输入子序列。

使用双向循环神经网络构造的编码器中隐状态依赖于由隐状态所在的时间步的位置之前的序列和之后的序列(包括隐状态所在的时间步)组成的两个输入子序列,因此隐状态对整个序列的信息都进行了编码。

以下实现的循环神经网络编码器使用了嵌入层(embedding layer)来获得输入序列中每个词元的特征向量。

  • 嵌入层的权重是一个矩阵,其行数等于输入词表的大小(vocab_size),其列数等于特征向量的维度(embed_size)。

  • 对于任意输入词元的索引 \(i\), 嵌入层获取权重矩阵的第 \(i\) 行(从 0 开始)以返回其特征向量。

  • 另外,本文选择了一个多层门控循环单元来实现编码器。

#@save
class Seq2SeqEncoder(d2l.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)  # 嵌入层
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)

    def forward(self, X, *args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)  # 前两个轴互换
        # 如果未提及状态,则默认为0
        output, state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state的形状:(num_layers,batch_size,num_hiddens)
        return output, state
# 实例化编码器

encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,  # 隐藏单元数为 16
                         num_layers=2)
encoder.eval()  # 不启用 Batch Normalization 和 Dropout
X = torch.zeros((4, 7), dtype=torch.long)  # 批量大小为 4,时间步为7
output, state = encoder(X)
output.shape  # 形状为(时间步数,批量大小,隐藏单元数)
torch.Size([7, 4, 16])
state.shape  # 最后一个时间步的多层隐状态的形状是(隐藏层的数量,批量大小,隐藏单元的数量)
torch.Size([2, 4, 16])

9.7.2 解码器

对于解码器的输出来说,概率取决于:

\[P(y_{t'}|y_1,\dots,y_{t'-1},\boldsymbol{c}) \]

参数字典:

  • \(y_{t'}\) 表示时间步 \(t'\) 的输出(用 ' 是为了和编码器的量区分)

  • \(y_1,y_2,\dots,y_{T'}\) 表示训练数据集的输出序列

  • \(\boldsymbol{c}\) 表示上下文变量

简言之,码器输出的概率取决于先前的输出子序列和上下文变量。

为了在序列上模型化这种条件概率,需要使用另一个循环神经网络作为解码器。在输出序列上的任意时间步 \(t'\),循环神经网络将来自上一时间步的输出 \(y_{t'-1}\) 和上下文变量 \(\boldsymbol{c}\) 作为其输入,然后在当前时间步将它们和上一隐状态 \(\boldsymbol{s}_{t'-1}\) 转换为隐状态 \(\boldsymbol{s}_t\)。 因此,可以使用函数 \(g\) 来表示解码器的隐藏层的变换:

\[\boldsymbol{s}_{t'}=g(y_{t'-1},\boldsymbol{c},\boldsymbol{s}_{t'-1}) \]

获得解码器的隐状态之后,可以使用输出层和 softmax 操作来计算在时间步 \(t'\) 时输出 \(y_{t'}\) 的条件概率分布 \(P(y_{t'}|y_1,\dots,y_{t'-1},\boldsymbol{c})\)

实现要点:

  • 直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。

    • 因此要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。
  • 上下文变量在所有的时间步与解码器的输入进行拼接(concatenate),以进一步包含经过编码的输入序列的信息。

  • 在循环神经网络解码器的最后一层使用全连接层来变换隐状态,以预测输出词元的概率分布。

class Seq2SeqDecoder(d2l.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)  # 嵌入层
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,
                          dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)  # 解码器是有输出层的

    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]  # 获取 states

    def forward(self, X, state):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1, 0, 2)  # 时间步放在前面
        # 广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0], 1, 1)  # 最后一次的隐藏状态拿出来按解码器单元数复制
        X_and_context = torch.cat((X, context), 2)  # 拼接输入和上下文变量
        output, state = self.rnn(X_and_context, state)
        output = self.dense(output).permute(1, 0, 2)
        # output的形状:(batch_size,num_steps,vocab_size)
        # state的形状:(num_layers,batch_size,num_hiddens)
        return output, state
# 与前文编码器超参数一样

decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,
                         num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape
(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))

总结构图:

image

9.7.3 损失函数

使用 softmax 来获得分布,并通过计算交叉熵损失函数来进行优化。需要注意,应该将填充词元的预测排除在损失函数的计算之外。

下面的 sequence_mask 函数通过零值化屏蔽不相关的项实现。

#@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]  # 优雅,比较 arange 生成张量(即列号序列)的列和 valid_len 的行
    X[~mask] = value  # 按位反转 仅有效位赋值
    return X

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))
tensor([[1, 0, 0],
        [4, 5, 0]])
X = torch.ones(2, 3, 4)
sequence_mask(X, torch.tensor([1, 2]), value=-1)  # 用非零值替代也可以
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.]]])
#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""
    # pred的形状:(batch_size,num_steps,vocab_size)
    # label的形状:(batch_size,num_steps)
    # valid_len的形状:(batch_size,)
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)  # 同型全一掩码矩阵
        weights = sequence_mask(weights, valid_len)  # 生成过滤填充词元的掩码矩阵
        self.reduction='none'  # 不进行值归并,原样输出
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
            pred.permute(0, 2, 1), label)  # 计算交叉熵损失
        weighted_loss = (unweighted_loss * weights).mean(dim=1)  # 过滤并求均值
        return weighted_loss
loss = MaskedSoftmaxCELoss()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),  # 使用三个相同的序列进行检查
     torch.tensor([4, 2, 0]))  # 设定有效长度为 4,2,0 则第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零
tensor([2.3026, 1.1513, 0.0000])

9.7.4 训练

#@save
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""
    def xavier_init_weights(m):  # xavier 初始化
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])

    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)  # 使用 Adam 优化器
    loss = MaskedSoftmaxCELoss()  # 使用改造的交叉熵损失
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                     xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # 设置两个累加器:训练损失总和,词元数量
        for batch in data_iter:
            optimizer.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]  # 加载数据
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],  # 获取特定的开始词元
                          device=device).reshape(-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()  # 损失函数的标量进行“反向传播”
            d2l.grad_clipping(net, 1)  # 梯度裁剪
            num_tokens = Y_valid_len.sum()
            optimizer.step()  # 优化
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
        f'tokens/sec on {str(device)}')
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,
                        dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
                        dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
loss 0.020, 15597.5 tokens/sec on cuda:0

image

9.7.5 预测

序列开始词元(“<bos>”)在初始时间步被输入到解码器中。当输出序列的预测遇到序列结束词元(“<eos>”)时,预测就结束了。

image

#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    net.eval()  # 在预测时将net设置为评估模式 不启用 Batch Normalization 和 Dropout
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['<eos>']]  # 预处理源语言
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])  # 进行截断与填充
    # 添加批量轴
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)  # 进行编码
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)  # 初始化解码器
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)  # 解码
        dec_X = Y.argmax(dim=2)  # 使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        if save_attention_weights:  # 保存注意力权重(稍后讨论)
            attention_weight_seq.append(net.decoder.attention_weights)
        if pred == tgt_vocab['<eos>']:  # 一旦序列结束词元被预测,输出序列的生成就完成了
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

9.7.6 预测序列的评估

BLEU(bilingual evaluation understudy)最先是用于评估机器翻译的结果,但现在它已经被广泛用于测量许多应用的输出序列的质量。

原则上说,对于预测序列中的任意 n 元语法(n-grams),BLEU 的评估都是这个 n 元语法是否出现在标签序列中。BLEU 定义为:

\[\exp{\left(\min{\left(0,1-\frac{len_{label}}{len_{pred}}\right)}\right)}\prod^k_{n=1}p_n^{1/2^n} \]

参数字典:

  • \(len_{label}\) 表示标签序列中的词元数

  • \(len_{pred}\) 表示预测序列中的词元数

  • \(k\) 用于匹配的最长的 n 元语法

  • \(p_n\) 表示 n 元语法的精确度它是两个数量的比值:

    • 第一个是预测序列与标签序列中匹配的 n 元语法的数量

    • 第二个是预测序列中 n 元语法的数量的比率。

设计要点:

  • 当预测序列与标签序列完全相同时,BLEU 为 1。

  • 此外,由于 n 元语法越长则匹配难度越大,所以 BLEU 为更长的元语法的精确度分配更大的权重。具体来说,当 \(p_n\) 固定时,\(p_n^{1/2^n}\) 会随着 n 的增长而增加(原始论文使用 \(p_n^{1/n}\))。

  • 由于预测的序列越短获得的 \(p_n\) 值越高,所以 BLEU 定义式中乘法项之前的系数用于惩罚较短的预测序列。

def bleu(pred_seq, label_seq, k):  #@save
    """计算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))  # 计算惩罚项
    for n in range(1, k + 1):  # 计算乘法项
        num_matches, label_subs = 0, collections.defaultdict(int)  # 匹配数,预测序列内比率(带默认值的字典)
        for i in range(len_label - n + 1):
            label_subs[' '.join(label_tokens[i: i + n])] += 1  # 对各词元进行计数
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:  # 匹配中词元
                num_matches += 1  # 匹配数加一
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1  # 减去已经匹配过的词元,防止重复匹配
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(
        net, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')
go . => va <unk> !, bleu 0.000
i lost . => j'ai perdu perdu ., bleu 0.783
he's calm . => il est <unk> ., bleu 0.658
i'm home . => je suis calme ., bleu 0.512

练习

(1)试着通过调整超参数来改善翻译效果。

embed_size1, num_hiddens1, num_layers1, dropout1 = 64, 64, 2, 0.2
batch_size1, num_steps1 = 128, 10
lr1, num_epochs1, device1 = 0.01, 500, d2l.try_gpu()

train_iter1, src_vocab1, tgt_vocab1 = d2l.load_data_nmt(batch_size1, num_steps1)
encoder1 = Seq2SeqEncoder(len(src_vocab1), embed_size1, num_hiddens1, num_layers1,
                        dropout1)
decoder1 = Seq2SeqDecoder(len(tgt_vocab1), embed_size1, num_hiddens1, num_layers1,
                        dropout1)
net1 = d2l.EncoderDecoder(encoder1, decoder1)
train_seq2seq(net1, train_iter1, lr1, num_epochs1, tgt_vocab1, device1)
loss 0.020, 18634.0 tokens/sec on cuda:0

image

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(
        net1, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')
go . => va !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est paresseux ., bleu 0.658
i'm home . => je suis chez moi ., bleu 1.000

(2)重新运行实验并在计算损失时不使用遮蔽,可以观察到什么结果?为什么会有这个结果?

翻译效果变差,可能是填充词元使翻译的逻辑更困难了。

class MaskedSoftmaxCELoss_test(nn.CrossEntropyLoss):
    def forward(self, pred, label, valid_len):
        self.reduction='none'
        return super(MaskedSoftmaxCELoss_test, self).forward(
            pred.permute(0, 2, 1), label).mean(dim=1)

def train_seq2seq_test(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""
    def xavier_init_weights(m):  # xavier 初始化
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])

    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)  # 使用 Adam 优化器
    loss = MaskedSoftmaxCELoss_test()  # 使用改造的交叉熵损失
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                     xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # 设置两个累加器:训练损失总和,词元数量
        for batch in data_iter:
            optimizer.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]  # 加载数据
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],  # 获取特定的开始词元
                          device=device).reshape(-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()  # 损失函数的标量进行“反向传播”
            d2l.grad_clipping(net, 1)  # 梯度裁剪
            num_tokens = Y_valid_len.sum()
            optimizer.step()  # 优化
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
        f'tokens/sec on {str(device)}')
    
embed_size2, num_hiddens2, num_layers2, dropout2 = 32, 32, 2, 0.1
batch_size2, num_steps2 = 64, 10
lr2, num_epochs2, device2 = 0.005, 300, d2l.try_gpu()

train_iter2, src_vocab2, tgt_vocab2 = d2l.load_data_nmt(batch_size2, num_steps2)
encoder2 = Seq2SeqEncoder(len(src_vocab2), embed_size2, num_hiddens2, num_layers2,
                        dropout2)
decoder2 = Seq2SeqDecoder(len(tgt_vocab2), embed_size2, num_hiddens2, num_layers2,
                        dropout2)
net2 = d2l.EncoderDecoder(encoder2, decoder2)
train_seq2seq_test(net2, train_iter2, lr2, num_epochs2, tgt_vocab, device2)
loss 0.019, 14341.4 tokens/sec on cuda:0

image

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(
        net2, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')
go . => va !, bleu 1.000
i lost . => j'ai perdu perdu ., bleu 0.783
he's calm . => attrapez tom ., bleu 0.000
i'm home . => je suis chez moi mouvement de tom ., bleu 0.640

(3)如果编码器和解码器的层数或者隐藏单元数不同,那么如何初始化解码器的隐状态?

不会,略。


(4)在训练中,如果用前一时间步的预测输入到解码器来代替强制教学,对性能有何影响?

预测会越来越偏吧。


(5)用长短期记忆网络替换门控循环单元重新运行实验。

class Seq2SeqEncoder_test(d2l.Encoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder_test, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.lstm = nn.LSTM(embed_size, num_hiddens, num_layers,  # 更换为 LSTM
                          dropout=dropout)

    def forward(self, X, *args):
        X = self.embedding(X)
        X = X.permute(1, 0, 2)
        output, state = self.lstm(X)
        return output, state
    
class Seq2SeqDecoder_test(d2l.Decoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqDecoder_test, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.lstm = nn.LSTM(embed_size + num_hiddens, num_hiddens, num_layers,  # 更换为 LSTM
                          dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]

    def forward(self, X, state):
        X = self.embedding(X).permute(1, 0, 2)
        # context = state[-1].repeat(X.shape[0], 1, 1)
        context = state[-1][0].repeat(X.shape[0], 1, 1)  # 注意 LSTM 有 hidden state 和 cell state,这里使用 hidden state
        X_and_context = torch.cat((X, context), 2)
        output, state = self.lstm(X_and_context, state)
        output = self.dense(output).permute(1, 0, 2)
        return output, state
    
embed_size3, num_hiddens3, num_layers3, dropout3 = 32, 32, 2, 0.1
batch_size3, num_steps3 = 64, 10
lr3, num_epochs3, device3 = 0.005, 300, d2l.try_gpu()

train_iter3, src_vocab3, tgt_vocab3 = d2l.load_data_nmt(batch_size3, num_steps3)
encoder3 = Seq2SeqEncoder_test(len(src_vocab3), embed_size3, num_hiddens3, num_layers3,
                        dropout3)
decoder3 = Seq2SeqDecoder_test(len(tgt_vocab3), embed_size3, num_hiddens3, num_layers3,
                        dropout3)
net3 = d2l.EncoderDecoder(encoder3, decoder3)
train_seq2seq(net3, train_iter3, lr3, num_epochs3, tgt_vocab3, device3)
loss 0.019, 14508.5 tokens/sec on cuda:0

image

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(
        net2, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')
go . => va !, bleu 1.000
i lost . => j'ai perdu perdu ., bleu 0.783
he's calm . => attrapez tom ., bleu 0.000
i'm home . => je suis chez moi mouvement de tom ., bleu 0.640

(6)有没有其他方法来设计解码器的输出层?

不会,略。