李沐动手学深度学习V2-seq2seq和代码实现

一. seq2seq序列到序列学习

1. 介绍

机器翻译输入序列和输出序列都是长度可变的。 为了解决这类问题,我们将使用两个循环神经网络的编码器和解码器Auto-Encoder架构, 并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务 .
遵循编码器-解码器架构的设计原则, 循环神经网络编码器使用长度可变的序列作为输入, 将其转换为固定形状的隐状态。 换言之输入序列的信息被编码到循环神经网络编码器的隐状态中。 为了连续生成输出序列的词元, 独立的循环神经网络解码器是基于输入序列的编码信息和label序列已经看见的或者生成的词元来预测下一个词元。 下图演示了 如何在机器翻译中使用两个循环神经网络进行序列到序列学习。
seq2seq架构
在上图解码器结构中特定的“”表示序列结束词元,在预测时一旦输出序列生成此词元,模型就会停止预测。 在循环神经网络解码器的初始化时间步,有两个特定的设计决定: 首先特定的“”表示序列开始词元,它是解码器的输入序列的第一个词元;其次使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态,因此这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。 在其他一些设计中 将编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 类似于语言模型的训练, 可以允许标签成为原始的输入序列, 从源序列词元“”、“Ils”、“regardent”、“.” 到新序列词元 “Ils”、“regardent”、“.”、“”来移动预测的位置。

2. 编码器

从技术上讲,编码器将长度可变的输入序列转换成形状固定的上下文变量 c \mathbf{c} c,并且将输入序列的信息在该上下文变量中进行编码,使用循环神经网络来设计编码器。
考虑由一个序列组成的样本(批量大小是 1 1 1)。假设输入序列是 x 1 , … , x T x_1, \ldots, x_T x1,,xT,其中 x t x_t xt是输入文本序列中的第 t t t个词元。在时间步 t t t,循环神经网络将词元 x t x_t xt的输入特征向量 x t \mathbf{x}_t xt h t − 1 \mathbf{h} _{t-1} ht1(即上一时间步的隐状态)转换为 h t \mathbf{h}_t ht(即当前步的隐状态)。使用一个函数 f f f来描述循环神经网络的循环层所做的变换:

h t = f ( x t , h t − 1 ) . \mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}). ht=f(xt,ht1).

总之,编码器通过选定的函数 q q q将所有时间步的隐状态转换为上下文变量:
c = q ( h 1 , … , h T ) . \mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T). c=q(h1,,hT).
比如,当选择 q ( h 1 , … , h T ) = h T q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T q(h1,,hT)=hT时,上下文变量仅仅是输入序列在最后时间步最后一层的隐状态 h T \mathbf{h}_T hT,这个也是本节选择的上下文变量函数。

到目前为止,我们使用的是一个单向循环神经网络来设计编码器,其中隐状态只依赖于输入子序列,这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置(包括隐状态所在的时间步)组成。我们也可以使用双向循环神经网络构造编码器,其中隐状态依赖于两个输入子序列,两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列(包括隐状态所在的时间步),因此隐状态对整个序列的信息都进行了编码。
实现循环神经网络编码器:使用了嵌入层(embedding layer)来获得输入序列中每个词元的特征向量,嵌入层的权重是一个矩阵,其行数等于输入词表的大小(vocab_size),其列数等于特征向量的维度(embed_size)。
对于任意输入词元的索引 i i i,嵌入层获取权重矩阵的第 i i i行(从 0 0 0开始)以返回其特征向量。另外选择一个多层门控循环单元来实现编码器。

import torch
import d2l.torch
import math
import collections
from torch import nn
class Seq2SeqEncoder(d2l.torch.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""
    def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout=0):
        super(Seq2SeqEncoder,self).__init__()
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size,embed_size)
        self.rnn = nn.GRU(embed_size,num_hiddens,num_layers,dropout=dropout,bidirectional=False)
    def forward(self,X,*args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1,0,2)
        # 如果未传入状态,则默认为初始隐状态state为0
        output,state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output,state

下面实例化上述编码器的实现: 使用一个两层门控循环单元编码器,其隐藏单元数为 16 。 给定一小批量的输入序列X(批量大小为 4 ,时间步为 7 )。 在完成所有时间步后, 最后一层的隐状态的输出是一个张量(output由编码器的循环层返回), 其形状为(时间步数,批量大小,隐藏单元数)。
由于使用的是门控循环单元, 所以在最后一个时间步的多层隐状态的形状是 (隐藏层的数量,批量大小,隐藏单元的数量)。 如果使用长短期记忆网络,state形状中还将包含记忆单元信息。

encoder = Seq2SeqEncoder(vocab_size=10,embed_size=8,num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros(size=(4,7),dtype=torch.long)
output,state = encoder(X)
output.shape,state.shape
输出结果如下:
(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))

3.解码器

正如上面提到的,编码器输出的上下文变量 c \mathbf{c} c对解码器整个输入序列 x 1 , … , x T x_1, \ldots, x_T x1,,xT进行编码。来自训练数据集的输出序列 y 1 , y 2 , … , y T ′ y_1, y_2, \ldots, y_{T'} y1,y2,,yT,对于每个时间步 t ′ t' t(与输入序列或编码器的时间步 t t t不同),解码器输出 y t ′ y_{t'} yt的概率取决于先前的输出子序列 y 1 , … , y t ′ − 1 y_1, \ldots, y_{t'-1} y1,,yt1和上下文变量 c \mathbf{c} c(也即是解码器中上个时间步的隐状态),即 P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) P(y_{t'} \mid y_1, \ldots, y_{t'-1},\mathbf{c}) P(yty1,,yt1,c)
为了在序列上模型化这种条件概率,可以使用另一个循环神经网络作为解码器。在输出序列上的任意时间步 t ′ t^\prime t,循环神经网络将来自上一时间步的输出 y t ′ − 1 y_{t^\prime-1} yt1和上下文变量 c \mathbf{c} c作为其输入,然后在当前时间步将它们和上一步隐状态 s t ′ − 1 \mathbf{s}_{t^\prime-1} st1转换为当前时间步的隐状态 s t ′ \mathbf{s}_{t^\prime} st。因此可以使用函数 g g g来表示解码器的隐藏层的变换( y t ′ − 1 y_{t^\prime-1} yt1指的是法语label序列在当前时间步上一个时间步对应的词元或者是预测序列时上一个时间步预测出来的词元):
s t ′ = g ( y t ′ − 1 , c , s t ′ − 1 ) . \mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-1}). st=g(yt1,c,st1).

在获得解码器的隐状态之后,可以使用输出层和softmax操作
来计算在时间步 t ′ t^\prime t时输出 y t ′ y_{t^\prime} yt的条件概率分布 P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c}) P(yty1,,yt1,c)
实现解码器时,直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态,这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。
为了进一步包含经过编码的输入序列的信息,上下文变量在解码器所有的时间步与解码器的输入进行拼接(concatenate)。
为了预测输出词元的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐状态。

class Seq2SeqDecoder1(d2l.torch.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""
    def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout=0):
        super(Seq2SeqDecoder,self).__init__()
        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]
    def forward(self,X,state):
        # 'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1,0,2)
        #state[-1]表示将encoder中经过最后一个时刻后得到的两个隐状态层中最后最后一层拿出来作为上下文context,也就相当于这个批量序列的浓缩概括,
        #形状为(batch_size,num_hidddens),然后复制num_steps次再组合起来,然后与decoder输入在embed_size维度(dim=2)上面进行连接,因此decoder每个输入样本序列中每个词embed_size都带上了encoder最后隐状态层中最后一层上下文context
        # 广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0],1,1)
        X_context = torch.cat((X,context),dim=2)
        output,state = self.rnn(X_context,state)
        y_hat = self.dense(output)
        y_hat = y_hat.permute(1,0,2) #y_hat中每一个batch_size样本就是将每个英文样本翻译成法语的句子
        # y_hat的形状:(batch_size,num_steps,vocab_size)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return y_hat,state

下面用与前面提到的编码器中相同的超参数来实例化解码器,解码器的输出形状变为(批量大小,时间步数,词表大小), 其中张量的最后一个维度存储预测的词元分布。

decoder = Seq2SeqDecoder(vocab_size=10,embed_size=8,num_hiddens=16,num_layers=2)
decoder.eval()
encoder_state = decoder.init_state(encoder(X))
y_hat,decoder_state = decoder(X,encoder_state)
y_hat.shape,decoder_state[0].shape
输出结果如下:
(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))

总之循环神经网络“编码器-解码器”模型中的各层如下图所示:
编码器-解码器架构

4. 损失函数

在每个时间步,解码器预测输出词元的概率分布,类似于语言模型可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化。由于特定的填充词元被添加到序列的末尾, 因此不同长度的序列可以以相同形状的小批量加载,但是我们应该将填充词元的预测排除在损失函数的计算之外
因此使用下面的sequence_mask函数通过零值化屏蔽不相关的项, 以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。 例如如果两个序列的有效长度(不包括填充词元)分别为 1 和 2 , 则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。

def sequence_mask(X,valid_len,value=0):
    """在序列中屏蔽不相关的项"""
    maxlen = X.size(dim=1) #X.shape[1],第1维的长度
    mask = torch.arange((maxlen),dtype=torch.float32,device=X.device)[None,:]< valid_len[:,None] #[:,None]在None的位置增加一维,维度大小为1,比大小使用了广播机制
    #print(mask)
    # ~mask表示对mask取反
    X[~mask] = value
    return X
X = torch.tensor([[1,2,3],[4,5,6]])
valid_len = torch.tensor([1,2])
sequence_mask(X,valid_len,value=0)
输出结果如下:
tensor([[1, 0, 0],
        [4, 5, 0]])

使用此函数屏蔽最后几个轴上的所有项,也可以使用指定的非零值来替换这些项。

X = torch.ones(size=(2,3,4))
sequence_mask(X,valid_len=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.]]])

现在,通过扩展softmax交叉熵损失函数来遮蔽不相关的预测。 最初所有预测词元的掩码都设置为1。 一旦给定了有效长度,与填充词元对应的掩码将被设置为0。 最后将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。

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().forward(pred.permute(0,2,1),label) #pred.permute(0,2,1)这里将embed_size拿到第二维,将batch_size拿到第三维是因为Pytorch中cross entropy()中forward()函数要求pred数据需要是这种格式类型
        weighted_loss = (unweighted_loss*weights).mean(dim=1)
        return weighted_loss

创建三个相同的序列来进行代码健全性检查, 然后分别指定这些序列的有效长度为 4 、 2 和 0 。 结果就是第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。

loss = MaskedSoftmaxCELoss()
pred = torch.ones(size=(3,4,10))
label = torch.ones(size=(3,4),dtype=torch.long) #因为label里面元素是作为cross entropy损失函数中pred的索引,因此label里面元素必须是整数型,如果没有dtype=torch.long则默认lable元素数据类型是float型
valid_len = torch.tensor([4,2,0])
loss(pred,label,valid_len)
输出结果如下:
tensor([2.3026, 1.1513, 0.0000])

5. 训练

在下面循环训练过程中,特定的序列开始词元(“”)和法语label原始的输出序列(不包括label原始序列最后一个词元)拼接在一起作为解码器的输入,这被称为强制教学(teacher forcing)。(也即是原始的输出序列(词元的标签)被送入解码器,或者将来自上一个时间步的预测得到的词元作为解码器的当前输入和上下文context变量拼接在一起作为解码器的输入)

def train_seq2seq(net,train_iter,lr,num_epochs,tgt_vocab,device):
    """训练序列到序列模型"""
    def xavier_init_weights(m):
        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 = net.to(device)
    loss = MaskedSoftmaxCELoss()
    optim = torch.optim.Adam(net.parameters(),lr=lr)
    animator = d2l.torch.Animator(xlabel='epoch',ylabel='loss',xlim=[10,num_epochs])
    net.train()
    for epoch in range(num_epochs):
        timer = d2l.torch.Timer()
        accumulator = d2l.torch.Accumulator(2) # 训练损失总和,词元数量
        for batch in train_iter:
            optim.zero_grad()
            X,X_valid_len,Y,Y_valid_len = [data.to(device) for data in batch]
            bos = torch.tensor([tgt_vocab['<bos>']]*Y.shape[0],device=device).reshape(-1,1)
            dec_inputs = torch.cat([bos,Y[:,:-1]],dim=1) # 强制教学
            Y_hat,_= net(X,dec_inputs,X_valid_len)
            l = loss(Y_hat,Y,Y_valid_len)
            l.sum().backward() # 损失函数的标量进行“反向传播”
            d2l.torch.grad_clipping(net,theta=1)
            num_tokens = Y_valid_len.sum()
            optim.step()
            with torch.no_grad():
                accumulator.add(l.sum(),num_tokens)
        if (epoch+1)%10==0:
            animator.add(epoch+1,(accumulator[0]/accumulator[1],)) #accumulator[1]为标签样本序列tokens总数
    print('loss: ',accumulator[0]/accumulator[1],accumulator[1]/timer.stop(),'tokens/s',' on device:'+str(device))

在机器翻译数据集上创建和训练一个循环神经网络“编码器-解码器”模型用于序列到序列的学习,训练结果如下图所示。

batch_size,num_steps = 64,10
embed_size,num_hiddens,num_layers,dropout = 32,32,2,0.1
lr,num_epochs,device = 0.005,300,d2l.torch.try_gpu()
train_iter,src_vocab,tgt_vocab = d2l.torch.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.torch.EncoderDecoder(encoder,decoder)
train_seq2seq(net,train_iter,lr,num_epochs,tgt_vocab,device)

seq2seq训练结果

6. 预测

为了采用一个接着一个词元的方式预测输出序列, 每个解码器当前时间步的输入都将来自于上一时间步的输出预测词元。 与训练类似序列开始词元(“”) 在初始时间步被输入到解码器中。 该预测过程如下图所示, 当输出序列的预测遇到序列结束词元(“”)时,预测结束。
seq2seq预测过程

def predict_seq2seq(net,src_sentence,src_vocab,tgt_vocab,num_steps,device,save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为eval模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')]+[src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)],device=device) #Encoder输入格式列表形式,里面每个元素是每个样本序列实际长度
    src_tokens = d2l.torch.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 #' '.join(List)表示把list列表里面的元素以空格连接起来

7. 预测序列的评估

通过与真实的标签序列进行比较来评估预测序列,虽然提出的BLEU(bilingual evaluation understudy)最先是用于评估机器翻译的结果,但现在它已经被广泛用于测量许多应用的输出序列的质量。原则上说对于预测序列中的任意 n n n元语法(n-grams),BLEU的评估都是这个 n n n元语法是否出现在标签序列中,将BLEU定义为:

exp ⁡ ( min ⁡ ( 0 , 1 − l e n label l e n pred ) ) ∏ n = 1 k p n 1 / 2 n , \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}, exp(min(0,1lenpredlenlabel))n=1kpn1/2n,

其中 l e n label \mathrm{len}_{\text{label}} lenlabel表示标签序列中的词元数和 l e n pred \mathrm{len}_{\text{pred}} lenpred表示预测序列中的词元数, k k k是用于匹配的最长的 n n n元语法。另外,用 p n p_n pn表示 n n n元语法的精确度,它是两个数量的比值:第一个是预测序列与标签序列中匹配的 n n n元语法的数量,第二个是预测序列中 n n n元语法的数量的比率。具体地说,给定标签序列 A A A B B B C C C D D D E E E F F F和预测序列 A A A B B B B B B C C C D D D,我们有 p 1 = 4 / 5 p_1 = 4/5 p1=4/5 p 2 = 3 / 4 p_2 = 3/4 p2=3/4 p 3 = 1 / 3 p_3 = 1/3 p3=1/3 p 4 = 0 p_4 = 0 p4=0

根据BLEU的定义,当预测序列与标签序列完全相同时,BLEU为 1 1 1。此外由于 n n n元语法越长则匹配难度越大,所以BLEU为更长的 n n n元语法的精确度分配更大的权重。
具体来说当 p n p_n pn固定时, p n 1 / 2 n p_n^{1/2^n} pn1/2n会随着 n n n的增长而增加(原始论文使用 p n 1 / n p_n^{1/n} pn1/n),而且由于预测的序列越短获得的 p n p_n pn值越高,所以BLEU中乘法项之前的系数用于惩罚较短的预测序列。
例如,当 k = 2 k=2 k=2时,给定标签序列 A A A B B B C C C D D D E E E F F F和预测序列 A A A B B B,尽管 p 1 = p 2 = 1 p_1 = p_2 = 1 p1=p2=1,惩罚因子 exp ⁡ ( 1 − 6 / 2 ) ≈ 0.14 \exp(1-6/2) \approx 0.14 exp(16/2)0.14会降低BLEU。

def bleu(pred_seq,label_seq,k):
    """计算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
        #print('......',len_pred)
        score *= math.pow(num_matches/(len_pred-n+1),math.pow(0.5,n))
    return score

8. 预测结果

最后利用训练好的循环神经网络“编码器-解码器”模型, 将几个英语句子翻译成法语,并计算BLEU的最终结果。

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .','Hello world !']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .','Bonjour ,le monde .']
for eng,fra in zip(engs,fras):
    translation,attention_weight_seq = predict_seq2seq(net,eng,src_vocab,tgt_vocab,num_steps,device)
    print(eng,"==>",translation,"bleu:",bleu(translation,fra,k=2))

9. 小结

  • 根据“编码器-解码器”架构的设计,可以使用两个循环神经网络来设计一个序列到序列学习的模型
  • 在实现编码器和解码器时,可以使用多层循环神经网络
  • 使用遮蔽来过滤不相关的计算,例如在计算解码器输出序列和标签序列损失时
  • 在“编码器-解码器”训练中,强制教学方法将原始输出序列(而非预测结果)输入解码器
  • BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的 n n n元语法的匹配度来评估预测结果是否好坏。根据BLEU的定义,当预测序列与标签序列完全相同时,BLEU为 1 1 1。原则上说BLEU对于预测序列中的任意 n n n元语法(n-grams),BLEU的评估都是这个 n n n元语法是否出现在标签序列中

10. 问题:需要针对decoder改进

预测序列存在疑问
问题:如上图所示,每个时间步调用一次decoder, 其中的dec_state参数随时间步变化而更新,那么decoder中的context变量会根据dec_state的变化而变化,不再是encoder中的上下文信息。这里是不是一个bug?
回答:预测逻辑有错误,需要被cat的encode输出状态在后面预测的时候就成了每个时间步都变化的decoder最新隐状态了,所以我修改了下Seq2SeqDecoder类的实现,修改了init_state函数的输出和forward函数的一部分,使得运行逻辑和文字部分描述相符。我看到后面“Bahadanau注意力”章节时注意到预测函数的本意是想让state携带着decoder的最新时间步隐状态和encoder输出状态的,所以这里也把Seq2SeqDecoder修改为这个逻辑。

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]
        return (enc_outputs[1], enc_outputs[1][-1])

    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)
        # new
        encode = state[1]
        state = state[0]
        # new end
        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[0]的形状:(num_layers,batch_size,num_hiddens)
        #return output, state
        return output, (state, encode)

这样修改并训练之后,最后的预测部分bleu会获得提升,预测序列输出结果为:

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

问题回答截图:
问题回答截图

10.全部代码

import torch
import d2l.torch
import math
import collections
from torch import nn


class Seq2SeqEncoder(d2l.torch.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""

    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0):
        super(Seq2SeqEncoder, self).__init__()
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout, bidirectional=False)

    def forward(self, X, *args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)
        # 如果未传入状态,则默认为初始隐状态state为0
        output, state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state


encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
encoder.eval()
X = torch.zeros(size=(4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape, state.shape


class Seq2SeqDecoder1(d2l.torch.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""

    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0):
        super(Seq2SeqDecoder, self).__init__()
        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]

    def forward(self, X, state):
        # 'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1, 0, 2)
        #state[-1]表示将encoder中经过最后一个时刻后得到的两个隐状态层中最后最后一层拿出来作为上下文context,也就相当于这个批量序列的浓缩概括,
        #形状为(batch_size,num_hidddens),然后复制num_steps次再组合起来,然后与decoder输入在embed_size维度(dim=2)上面进行连接,因此decoder每个输入样本序列中每个词embed_size都带上了encoder最后隐状态层中最后一层上下文context
        # 广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0], 1, 1)
        X_context = torch.cat((X, context), dim=2)
        output, state = self.rnn(X_context, state)
        y_hat = self.dense(output)
        y_hat = y_hat.permute(1, 0, 2)  #y_hat中每一个batch_size样本就是将每个英文样本翻译成法语的句子
        # y_hat的形状:(batch_size,num_steps,vocab_size)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return y_hat, state


class Seq2SeqDecoder(d2l.torch.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):
        #print(enc_outputs[1].shape)
        # m = enc_outputs[1]
        # s = torch.cat([m[:2],m[2:]],dim=2)
        #print(s.shape)
        #return (s,s[-1])
        return (enc_outputs[1], enc_outputs[1][-1])

    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)
        # new
        encode = state[1]
        state = state[0]
        # new end
        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[0]的形状:(num_layers,batch_size,num_hiddens)
        #return output, state
        return output, (state, encode)


decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
decoder.eval()
encoder_state = decoder.init_state(encoder(X))
y_hat, decoder_state = decoder(X, encoder_state)
y_hat.shape, decoder_state[0].shape


def sequence_mask(X, valid_len, value=0):
    """在序列中屏蔽不相关的项"""
    maxlen = X.size(dim=1)  #X.shape[1],第1维的长度
    mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:,
                                                                                   None]  #[:,None]在None的位置增加一维,维度大小为1,比大小使用了广播机制
    #print(mask)
    # ~mask表示对mask取反
    X[~mask] = value
    return X


X = torch.tensor([[1, 2, 3], [4, 5, 6]])
valid_len = torch.tensor([1, 2])
sequence_mask(X, valid_len, value=0)
X = torch.ones(size=(2, 3, 4))
sequence_mask(X, valid_len=torch.tensor([1, 2]), value=-1)


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().forward(pred.permute(0, 2, 1),
                                          label)  #pred.permute(0,2,1)这里将embed_size拿到第二维,将batch_size拿到第三维是因为Pytorch中cross entropy()中forward()函数要求pred数据需要是这种格式类型
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss


loss = MaskedSoftmaxCELoss()
pred = torch.ones(size=(3, 4, 10))
label = torch.ones(size=(3, 4),
                   dtype=torch.long)  #因为label里面元素是作为cross entropy损失函数中pred的索引,因此label里面元素必须是整数型,如果没有dtype=torch.long则默认lable元素数据类型是float型
valid_len = torch.tensor([4, 2, 0])
loss(pred, label, valid_len)


def train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""

    def xavier_init_weights(m):
        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 = net.to(device)
    loss = MaskedSoftmaxCELoss()
    optim = torch.optim.Adam(net.parameters(), lr=lr)
    animator = d2l.torch.Animator(xlabel='epoch', ylabel='loss', xlim=[10, num_epochs])
    net.train()
    for epoch in range(num_epochs):
        timer = d2l.torch.Timer()
        accumulator = d2l.torch.Accumulator(2)  # 训练损失总和,词元数量
        for batch in train_iter:
            optim.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [data.to(device) for data in batch]
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0], device=device).reshape(-1, 1)
            dec_inputs = torch.cat([bos, Y[:, :-1]], dim=1)  # 强制教学
            Y_hat, _ = net(X, dec_inputs, X_valid_len)
            l = loss(Y_hat, Y, Y_valid_len)
            l.sum().backward()  # 损失函数的标量进行“反向传播”
            d2l.torch.grad_clipping(net, theta=1)
            num_tokens = Y_valid_len.sum()
            optim.step()
            with torch.no_grad():
                accumulator.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (accumulator[0] / accumulator[1],))  #accumulator[1]为标签样本序列tokens总数
    print('loss: ', accumulator[0] / accumulator[1], accumulator[1] / timer.stop(), 'tokens/s',
          ' on device:' + str(device))


batch_size, num_steps = 64, 10
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
lr, num_epochs, device = 0.005, 300, d2l.torch.try_gpu()
train_iter, src_vocab, tgt_vocab = d2l.torch.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.torch.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)


def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为eval模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)  #Encoder输入格式列表形式,里面每个元素是每个样本序列实际长度
    src_tokens = d2l.torch.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  #' '.join(List)表示把list列表里面的元素以空格连接起来


def bleu(pred_seq, label_seq, k):
    """计算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
        #print('......',len_pred)
        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 .', 'Hello world !']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .', 'Bonjour ,le monde .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)
    print(eng, "==>", translation, "bleu:", bleu(translation, fra, k=2))

11.Encoder编码器使用双向循环神经网络实现全部代码(需要特别注意隐状态和隐藏层大小,以及初始化解码器的状态state等形状格式大小问题)

#encoder使用双向循环神经网络,注意num_hiddens数目
import torch
import d2l.torch
import math
import collections
from torch import nn


class Seq2SeqEncoder(d2l.torch.Encoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0):
        super(Seq2SeqEncoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout, bidirectional=True)

    def forward(self, X, *args):
        X = self.embedding(X)
        X = X.permute(1, 0, 2)
        output, state = self.rnn(X)
        return output, state


encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
encoder.eval()
X = torch.zeros(size=(4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape, state.shape


class Seq2SeqDecoder1(d2l.torch.Decoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0):
        super(Seq2SeqDecoder, self).__init__()
        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]

    def forward(self, X, state):
        X = self.embedding(X).permute(1, 0, 2)
        #state[-1]表示将encoder中经过最后一个时刻后得到的两个隐状态层中最后最后一层拿出来作为上下文context,也就相当于这个批量序列的浓缩概括,
        #形状为(batch_size,num_hidddens),然后复制num_steps次再组合起来,然后与decoder输入在embed_size维度(dim=2)上面进行连接,因此decoder每个输入样本序列中每个词embed_size都带上了encoder最后隐状态层中最后一层上下文context
        context = state[-1].repeat(X.shape[0], 1, 1)
        X_context = torch.cat((X, context), dim=2)
        output, state = self.rnn(X_context, state)
        y_hat = self.dense(output)
        y_hat = y_hat.permute(1, 0, 2)  #y_hat中每一个batch_size样本就是将每个英文样本翻译成法语的句子
        return y_hat, state


class Seq2SeqDecoder(d2l.torch.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):
        #print(enc_outputs[1].shape)
        m = enc_outputs[1]
        s = torch.cat([m[:2], m[2:]], dim=2)
        #print(s.shape)
        #return (enc_outputs[1], enc_outputs[1][-1])
        return (s, s[-1])

    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)
        # new
        encode = state[1]
        state = state[0]
        # new end
        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[0]的形状:(num_layers,batch_size,num_hiddens)
        #return output, state
        return output, (state, encode)


decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=32, num_layers=2)
decoder.eval()
encoder_state = decoder.init_state(encoder(X))
y_hat, decoder_state = decoder(X, encoder_state)
y_hat.shape, decoder_state[0].shape


def sequence_mask(X, valid_len, value=0):
    maxlen = X.size(dim=1)  #X.shape[1]
    mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:, None]
    #print(mask)
    # ~mask表示对mask取反
    X[~mask] = value
    return X


X = torch.tensor([[1, 2, 3], [4, 5, 6]])
valid_len = torch.tensor([1, 2])
sequence_mask(X, valid_len, value=0)
X = torch.ones(size=(2, 3, 4))
sequence_mask(X, valid_len=torch.tensor([1, 2]), value=-1)


class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)
        self.reduction = 'none'
        unweighted_loss = super().forward(pred.permute(0, 2, 1),
                                          label)  #pred.permute(0,2,1)这里将embed_size拿到第二维,将batch_size拿到第三维是因为Pytorch中cross entropy()中forward()函数要求pred数据需要是这种格式类型
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss


loss = MaskedSoftmaxCELoss()
pred = torch.ones(size=(3, 4, 10))
label = torch.ones(size=(3, 4),
                   dtype=torch.long)  #因为label里面元素是作为cross entropy损失函数中pred的索引,因此label里面元素必须是整数型,如果没有dtype=torch.long则默认lable元素数据类型是float型
valid_len = torch.tensor([4, 2, 0])
loss(pred, label, valid_len)


def train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device):
    def xavier_init_weights(m):
        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 = net.to(device)
    loss = MaskedSoftmaxCELoss()
    optim = torch.optim.Adam(net.parameters(), lr=lr)
    animator = d2l.torch.Animator(xlabel='epoch', ylabel='loss', xlim=[10, num_epochs])
    net.train()
    for epoch in range(num_epochs):
        timer = d2l.torch.Timer()
        accumulator = d2l.torch.Accumulator(2)
        for batch in train_iter:
            optim.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [data.to(device) for data in batch]
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0], device=device).reshape(-1, 1)
            dec_inputs = torch.cat([bos, Y[:, :-1]], dim=1)
            Y_hat, _ = net(X, dec_inputs, X_valid_len)
            l = loss(Y_hat, Y, Y_valid_len)
            l.sum().backward()
            d2l.torch.grad_clipping(net, theta=1)
            num_tokens = Y_valid_len.sum()
            optim.step()
            with torch.no_grad():
                accumulator.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (accumulator[0] / accumulator[1],))  #accumulator[1]为标签样本序列tokens总数
    print('loss: ', accumulator[0] / accumulator[1], accumulator[1] / timer.stop(), 'tokens/s',
          ' on device:' + str(device))


batch_size, num_steps = 64, 10
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
num_hiddens2 = 64
lr, num_epochs, device = 0.005, 300, d2l.torch.try_gpu()
train_iter, src_vocab, tgt_vocab = d2l.torch.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_hiddens2, num_layers, dropout)
net = d2l.torch.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)


def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=False):
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)  #Encoder输入格式列表形式,里面每个元素是每个样本序列实际长度
    src_tokens = d2l.torch.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  #' '.join(List)表示把list列表里面的元素以空格连接起来


def bleu(pred_seq, label_seq, k):
    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
        #print('......',len_pred)
        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 .', 'Hello world !']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .', 'Bonjour ,le monde .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)
    print(eng, "==>", translation, "bleu:", bleu(translation, fra, k=2))

12. 相关链接

机器翻译第一篇:李沐动手学深度学习V2-机器翻译和数据集
机器翻译第二篇:李沐动手学深度学习V2-Encoder-Decoder编码器和解码器架构
机器翻译第三篇:李沐动手学深度学习V2-seq2seq和代码实现

猜你喜欢

转载自blog.csdn.net/flyingluohaipeng/article/details/125658573