• Seq2Seq - 序列到序列的学习(RNN循环神经网络)


    序列到序列学习(Seq2Seq)

    正如我们在之前中看到的,机器翻译中的输入序列和输出序列都是长度可变的。为了解决这类问题,我们设计了一个通用的”编码器-解码器“架构。本节,我们将使用两个循环神经网络的编码器和解码器,并将其应用于序列到序列(sequence to sequence,seq2seq) 类的学习任务

    遵循编码器-解码器架构的设计原则,循环神经网络编码器使用长度可变的序列作为输入,将其转换为固定形状的隐状态。换言之,输入序列的信息被编码到循环神经网络编码器的隐状态中。为了连续生成输出序列的词元,独立的循环神经网络解码器是基于输入序列的编码信息和输出序列已经看见的或者生成的词元来预测下一个词元。 下图演示了如何在机器翻译中使用两个循环神经网络进行序列到序列学习。

    在这里插入图片描述

    在上图中,特定的 表示序列结束词元。一旦输出序列生成此词元,模型就会停止预测。在循环神经网络解码器的初始化时间步,有两个特定的设计决定:首先,特定的 表示序列开始词元,它是解码器的输入序列的第一个词元。其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态

    类似之前的语言模型的训练,可以允许标签成为原始的输出序列,从源序列词元“”、“Ils”、“regardent”、“.”到新序列词元“Ils”、“regardent”、“.”、“”来移动预测的位置。

    下面,我们动手实现上图的设计,并将基于机器翻译与数据集中介绍的“英-法”数据集来训练这个机器翻译模型。

    import collections
    import math
    import torch
    from torch import nn
    from d2l import torch as d2l
    
    • 1
    • 2
    • 3
    • 4
    • 5

    编码器

    从技术上讲,编码器将长度可变的输入序列转换成形状固定的上下文变量 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开始)以返回其特征向量。另外,本文选择了一个多层门控循环单元GRU来实现编码器。

    这个编码器Encoder最终返回的是隐状态信息

    class Seq2SeqEncoder(d2l.Encoder):
        """用于序列到序列学习的循环神经网络编码器"""
        def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, 
                    dropout=0, **kwargs):
            super(Seq2SeqEncoder, self).__init__(**kwargs)
            #嵌入层Embedding,两个参数分别为(vacab_size, embed_size)
            self.embedding = nn.Embedding(vocab_size, embed_size)
            #定义GRU循环神经网络,输入embed_size, 隐藏单元num_hiddens
            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)
            #num_steps即包含所有步数的隐状态信息
            #output的形状: (num_steps, batch_size, num_hiddens)
            #state的形状: (num_layers, batch_size, num_hiddens)
            return output, state
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    下面,我们实例化上述编码器的实现:我们使用一个两层门控循环单元编码器,其隐藏单元数为 16 16 16。给定一小批量的输入序列 X(批量大小为 4 4 4 ,时间步为 7 7 7 )。在完成所有时间步后,最后一层的隐状态的输出是一个张量(output由编码器的循环层返回),其形状为(时间步数,批量大小,隐藏单元数)。

    # 实例化一个编码器 Encoder,词典大小10,词向量维度8,隐单元数16,隐层2
    encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,
                             num_layers=2)
    encoder.eval()
    X = torch.zeros((4, 7), dtype=torch.long)
    # 进行编码操作,获得所有步数隐状态信息和最终隐状态信息
    output, state = encoder(X)
    output.shape, state.shape
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    (torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))
    
    • 1

    解码器

    正如上文提到的,编码器输出的上下文变量 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来表示解码器的隐藏层的变换:

    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 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)
            # 定义GRU循环神经网络,输入特征(embed_size + num_hiddens)
            self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,
                             dropout=dropout)
            # dense即全连接层,输出vocab_size输出维度
            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'的形状: (num_steps, batch_size, embed_size)
            X = self.embedding(X).permute(1, 0, 2)
            
            #广播context,使其具有与X相同的num_steps,扩展以后的形状为(7, 4, 16)
            context = state[-1].repeat(X.shape[0], 1, 1)              #repeat即扩展函数,a.repeat(X, Y, Z)即在一二三维分别扩展a的X、Y、Z倍
            X_and_context = torch.cat((X, context), 2)                #X的形状为(7, 4, 8), 合并之后会变成(7, 4, 24)
            output, state = self.rnn(X_and_context, state)            #执行rnn循环层
            output = self.dense(output).permute(1, 0, 2)              #执行全连接层,计算概率分布
            #output的形状: (batch_size, num_steps, vocab_size)、即(4, 7, vocab_sizes)
            #state的形状:(num_layers, batch_size, num_hiddens)、即(2, 4, 16)
            return output, state
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

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

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

    总之,上述循环神经网络“编码器-解码器”模型中的各层如下所示。

    在这里插入图片描述

    损失函数

    在每个时间步,解码器预测了输出词元的概率分布。类似于语言模型,可以使用softmax来获得分布,并通过计算交叉熵损失函数来进行优化。回想一下之前的操作,特定的填充词元被添加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,我们应该将填充词元的预测排除在损失函数的计算之外。

    为此,我们可以使用下面的 sequence_mask 函数 通过零值化屏蔽不相关的项,以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。例如,如果两个序列的有效长度(不包括填充词元)分别为 1 1 1 2 2 2,则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。

    def sequence_mask(X, valid_len, value=0):
        """在序列中屏蔽不相关的项"""
        maxlen = X.size(1)
        
        #掩蔽列表,即所有符合valid_len条件的数据
        mask = torch.arange((maxlen), dtype=torch.float32, 
                           device=X.device)[None, :] < valid_len[:, None]
    
        #将不符合的数据均清理为value
        X[~mask] = value
        return X
    
    X = torch.tensor([[1, 2, 3], [4, 5, 6]])
    sequence_mask(X, torch.tensor([1, 2]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    tensor([[1, 0, 0],
            [4, 5, 0]])
    
    • 1
    • 2

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

    X = torch.ones(2, 3, 4)
    sequence_mask(X, torch.tensor([1, 2]), value=-1)
    
    • 1
    • 2
    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.]]])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    现在,我们可以通过扩展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矩阵的值均为1
            weights = torch.ones_like(label)
            #weights矩阵即有效位标志,1代表损失有效,0代表损失无效
            weights = sequence_mask(weights, valid_len)
            self.reduction = 'none'
            #unweighted_loss代表真正的损失值
            unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
                pred.permute(0, 2, 1), label)
            #unweighted表示真正的损失值, weight表示真正的损失值
            weighted_loss = (unweighted_loss * weights).mean(dim=1)
            
            #返回平均损失值
            return weighted_loss
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    loss = MaskedSoftmaxCELoss()
    
    # 计算带遮蔽的softmax交叉熵损失函数
    loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),
        torch.tensor([4, 2, 0]))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    tensor([2.3026, 1.1513, 0.0000])
    
    • 1

    训练

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

    #@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)
            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)
        #定义Adam优化方法
        optimizer = torch.optim.Adam(net.parameters(), lr=lr)
        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['']] * Y.shape[0],
                              device=device).reshape(-1, 1)
    #             print('bos:\n', bos, 'bos.shape:\n', bos.shape)
                dec_input = torch.cat([bos, Y[:, :-1]], 1)                     # 强制教学
    #             print('dec_input:\n', dec_input, 'dec_input.shape:\n', dec_input.shape)
                #Y_had即为向后预测的7个时间步数的词的概率分布,形状为(64, 7, 201)
                Y_hat, _ = net(X, dec_input, X_valid_len)
    #             print('Y_hat shape:\n', Y_hat.shape)                         # y_hat_shape应为(64, 7, 201即len(tgt_vocab))
                #进行损失计算
                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)}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    现在,在机器翻译数据集上,我们可以 创建和训练一个循环神经网络“编码器-解码器”模型用于序列到序列的学习。

    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)
    
    """输出X和Y对照的数据集"""
    # for X, X_valid_len, Y, Y_valid_len in train_iter:
    #     for line in X:
    #         print(' '.join([src_vocab.idx_to_token[i] for i in line]))
    #     print('\n\n')
    #     for line in Y:
    #         print(' '.join([tgt_vocab.idx_to_token[i] for i in line]))
            
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    loss 0.021, 5964.3 tokens/sec on cpu
    
    • 1

    在这里插入图片描述

    预测

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

    在这里插入图片描述

    #@save
    def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                        device, save_attention_weights=False):
        """序列到序列模型的预测"""
        # 在预测时将net设置为评估模式
        net.eval()
        src_tokens = src_vocab[src_sentence.lower().split(' ')] + [                    # 源语言tokens列表
            src_vocab['']]
        #其中,源语言将句子结束符号 '' 也作为句子的一部分
        enc_valid_len = torch.tensor([len(src_tokens)], device=device)                 # 源语言的tokens长度
        src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab[''])       # 对源语言的tokens长度进行填充或者裁剪,使其到达长度10
        
        # 添加批量轴
        enc_X = torch.unsqueeze(
            torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)          # 对源语言tokens列表添加一个维度, 即(1, 10)
        enc_outputs = net.encoder(enc_X, enc_valid_len)                                # 通过编码器得到隐状态的信息
        dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)                 # 初始化解码器的隐状态的信息
        
        
        # 添加批量轴
        # 添加序列开始标识 ,此时的输入decX为[''], 形状为(1, 1),使用预测的词预测新的词
        dec_X = torch.unsqueeze(torch.tensor(
            [tgt_vocab['']], dtype=torch.long, device=device), dim=0)
        output_seq, attention_weight_seq = [], []                                     # 输出句子和注意力权重句子为空
        
        for _ in range(num_steps):
            # 通过dec_X和解码器dec_state输入解码器(初始化为编码器最终隐状态、),后续逐步更新
            # Y是神经网络预测的概率分布值
            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['']:
                break
            output_seq.append(pred)                                                  # 将预测到词的索引值添加到输出序列中
        return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq       # 返回预测到的翻译语句,注意力权重语序
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    预测序列的评估

    我们可以通过与真实的标签序列进行比较来评估预测序列。虽然[Papineni.Roukos.Ward.ea.2002]提出的 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 值越高,所以 eq_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。

    BLEU的代码实现如下。

    def bleu(pred_seq, label_seq, k):  #@save
        """计算BLEU"""
        # 将预测句子和实际句子均分隔为tokens数组
        pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
        # 获取预测句子和实际句子的长度
        len_pred, len_label = len(pred_tokens), len(label_tokens)
        # 加入惩罚项系数score, 防止过于短的预测句子得到较高的BLEU值
        score = math.exp(min(0, 1 - len_label / len_pred))
        
        #该循环用于累计K-Gram的BLEU-Score得分, K分别为1, 2, ,,, k
        for n in range(1, k + 1):
            num_matches, label_subs = 0, collections.defaultdict(int)                       #匹配成功次数置0, 字典清空
            
            for i in range(len_label - n + 1):                                              #得到所有的键值对组合,key(出现的词):value(次数)
                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:                         #若真实句子中已经存在该token,即value > 0
                    num_matches += 1                                                        #匹配成功的次数加1
                    label_subs[' '.join(pred_tokens[i: i + n])] -= 1                        #真实句子中该键对应的值减一,匹配成功,且不重复匹配
            
            #根据公式连乘 K-Gram,得到综合的BLEU得分,其中(len_pred - n + 1)表示总共匹配次数,num_matches表示匹配成功次数,即P_n
            score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    
        #返回最终的BLEU评判得分
        return score
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

    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}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    go . => va !, bleu 1.000
    i lost . => j'ai perdu . ., bleu 0.783
    he's calm . => il m'en qui pouvons-nous porte prie ., bleu 0.000
    i'm home . => je suis chez porte porte me porte triste bien en, bleu 0.376
    
    • 1
    • 2
    • 3
    • 4

    小结

    1、根据“编码器-解码器”架构的设计, 我们可以使用两个循环神经网络来设计一个序列到序列学习的模型

    2、在实现编码器和解码器时,我们可以使用多层循环神经网络

    3、我们可以使用遮蔽来过滤不相关的计算,例如在计算损失时。

    4、在“编码器-解码器”训练中,强制教学方法将原始输出序列(而非预测结果)输入解码器

    5、BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的 n n n 元语法的匹配度来评估预测

  • 相关阅读:
    Postman传参后台接收问题
    自媒体短视频运营常见的5个问题及解决方案
    【JVM】引用计数和可达性分析算法详解
    最小生成树
    一种考虑时空关联的深度学习短时交通流预测方法
    你知道MySQL锁,到底在锁什么吗?
    面试题分享---面试八股文
    【OCPP】ocpp1.6协议第4.8章节Start Transaction的介绍及翻译
    【金九银十必问面试题】这应该是面试官最想听到的回答,Mysql如何解决幻读问题?
    【无标题】
  • 原文地址:https://blog.csdn.net/weixin_43479947/article/details/127561347