【Seq2Seq】卷积序列到序列学习

 🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

 简介

准备数据

构建模型

编码器

卷积块

编码器实现

解码器

解码器卷积块

解码器冲击

Seq2Seq

Training the Seq2Seq Model

 BLEU


在本笔记本中,我们将实现 Convolutional Sequence to Sequence Learning 模型.

 简介

此模型与这些教程中使用的先前模型截然不同。根本没有使用经常性成分。相反,它使用卷积层,通常用于图像处理。有关用于情绪分析的文本卷积层的简介,请参阅此教程。

简而言之,卷积层使用滤波器。这些滤镜具有宽度(在图像中也有高度,但通常没有文本)。如果筛选器的宽度为 3,则它可以看到 3 个连续的标记。每个卷积层都有许多这样的过滤器(本教程中有 1024 个)。每个过滤器将在整个序列中滑动,从开始到结束,一次查看所有3个连续令牌。这个想法是,这1024个过滤器中的每一个都将学习从文本中提取不同的特征。然后,模型将使用这种特征提取的结果 - 可能作为另一个卷积层的输入。

然后,这可以全部用于从源句子中提取特征,以将其翻译成目标语言。

准备数据


首先,让我们导入所有必需的模块,并设置随机种子以实现可重复性。

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

from torchtext.legacy.datasets import Multi30k
from torchtext.legacy.data import Field, BucketIterator

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

import spacy
import numpy as np

import random
import math
import time
SEED = 1234

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

接下来,我们将加载 spaCy 模型,并为源语言和目标语言定义分词器。

spacy_de = spacy.load('de_core_news_sm')
spacy_en = spacy.load('en_core_web_sm')
def tokenize_de(text):
    """
    将字符串中的德语文本标记化为字符串列表
    """
    return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
    """
    将字符串中的英语文本标记化为字符串列表
    """
    return [tok.text for tok in spacy_en.tokenizer(text)]

接下来,我们将设置字段,用于决定如何处理数据。默认情况下,PyTorch 中的 RNN 模型要求序列是形状 [sequence length, batch size]的张量,因此默认情况下,TorchText 将返回相同形状的张量批次。但是,在本笔记本中,我们使用的是 CNN,它期望批处理维度是第一位的。我们告诉 TorchText 通过设置 batch_first = True 来使批次为 [batch size, sequence length]

我们还附加序列标记的开头和结尾,以及减小所有文本的大小写。

SRC = Field(tokenize = tokenize_de, 
            init_token = '<sos>', 
            eos_token = '<eos>', 
            lower = True, 
            batch_first = True)

TRG = Field(tokenize = tokenize_en, 
            init_token = '<sos>', 
            eos_token = '<eos>', 
            lower = True, 
            batch_first = True)

然后我们加载数据

train_data, valid_data, test_data = Multi30k.splits(exts=('.de', '.en'), 
                                                    fields=(SRC, TRG))

我们像以前一样建立词汇表,将任何出现次数少于2次的<unk>token转换为token。

SRC.build_vocab(train_data, min_freq = 2)
TRG.build_vocab(train_data, min_freq = 2)

数据准备的最后一点是定义设备,然后构建迭代器。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
BATCH_SIZE = 128

train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data), 
     batch_size = BATCH_SIZE,
     device = device)

构建模型

接下来是构建模型。和以前一样,该模型由编码器和解码器组成。编码器将源语言中的输入句子编码为上下文向量。解码器对上下文向量进行解码,以生成目标语言的输出句子。

编码器

这些教程中的先前模型具有一个编码器,该编码器可将整个输入句子压缩为单个上下文向量z,卷积序列到序列模型略有不同 - 它为输入句子中的每个标记获取两个上下文向量。因此,如果我们的输入句子有6个标记,我们将得到12个上下文向量,每个标记两个。

每个令牌的两个上下文向量是一个凸向量和一个组合向量。凸向量是每个令牌通过几层的结果 - 我们很快就会解释。组合向量来自卷积向量和该令牌的嵌入之和。这两者都由编码器返回,以供解码器使用。

下图显示了输入句子的结果 - zwei menschen fechten。- 通过编码器。

 首先,令牌通过令牌嵌入层 - 这是自然语言处理中神经网络的标准。但是,由于此模型中没有循环连接,因此它不知道序列中令牌的顺序。为了解决这个问题,我们有第二个嵌入层,即位置嵌入层。这是一个标准的嵌入层,其中输入不是令牌本身,而是令牌在序列中的位置 - 从位置为0的第一个令牌<sos>开始,即(序列的开始)令牌。

接下来,将令牌和位置嵌入按元素相加,得到一个向量,其中包含有关令牌及其在序列中的位置的信息 - 我们简单地称之为嵌入向量。接下来是一个线性层,该层将嵌入向量转换为具有所需隐藏维度大小的向量。

下一步是将此隐藏向量传递到N卷积块中。这就是这个模型中“魔术”发生的地方,我们很快就会详细介绍卷积块的内容。在通过卷积块后,向量随后通过另一个线性层,将其从隐藏的维度大小转换回嵌入维度大小。这是我们的对流向量 - 我们在输入序列中的每个令牌都有一个这样的向量。

最后,通过残差连接将凸向量按元素与嵌入向量求和,以获得每个令牌的组合向量。同样,输入序列中的每个令牌都有一个组合向量。

卷积块

那么,这些卷积块是如何工作的呢?下图显示了 2 个卷积块,其中有一个过滤器(蓝色),该过滤器在序列中的令牌上滑动。在实际实现中,我们将有 10 个卷积块,每个块中有 1024 个过滤器。

 首先,输入句子是填充的。这是因为卷积层将减少输入句子的长度,我们希望进入卷积块的句子的长度等于它从卷积块中出来的长度。如果不填充,从卷积层出来的序列的长度将比进入卷积层的序列短filter_size - 1,例如,如果我们的滤波器大小为 3,则序列将缩短 2 个元素。因此,我们在每侧填充一个填充元素填充句子。对于奇数大小的过滤器,我们只需执行(filter_size - 1)/2即可计算每侧的填充量 - 在本教程中,我们不会涵盖偶数大小的过滤器。

这些滤波器的设计使其输出隐藏维度是输入隐藏维度的两倍。在计算机视觉术语中,这些隐藏的维度被称为通道 - 但我们将坚持将它们称为隐藏维度。为什么我们要将离开卷积滤波器的隐藏维度的大小加倍?这是因为我们使用的是一种称为门控线性单元(GLU)的特殊激活函数。GLUs具有包含在激活函数中的门控机制(类似于LSTM和GRU),实际上是隐藏维度大小的一半 - 而激活函数通常保持隐藏维度的大小相同。

通过GLU激活后,每个令牌的隐藏维度大小与进入卷积块时相同。现在,在通过卷积层之前,它用自己的向量进行元素求和。

一个卷积块到此结束。后续块获取前一个块的输出并执行相同的步骤。每个块都有自己的参数,它们不在块之间共享。最后一个块的输出返回到主编码器 - 在那里它通过线性层馈送以获得凸次输出,然后按元素求和,嵌入令牌以获得组合输出。

编码器实现

为了保持实现的简单性,我们只允许奇数大小的内核。这允许将填充平均添加到源序列的两侧。

作者使用尺度变量来“确保整个网络的差异不会发生巨大变化”。如果不使用,模型的性能似乎使用不同的种子而有很大差异。

位置嵌入被初始化为具有 100 的“词汇量”。这意味着它可以处理长达 100 个元素的序列,索引范围为 0 到 99。如果在具有较长序列的数据集上使用,则可以增加此值。

class Encoder(nn.Module):
    def __init__(self, 
                 input_dim, 
                 emb_dim, 
                 hid_dim, 
                 n_layers, 
                 kernel_size, 
                 dropout, 
                 device,
                 max_length = 100):
        super().__init__()        
        assert kernel_size % 2 == 1, "Kernel size must be odd!"        
        self.device = device        
        self.scale = torch.sqrt(torch.FloatTensor([0.5])).to(device)        
        self.tok_embedding = nn.Embedding(input_dim, emb_dim)
        self.pos_embedding = nn.Embedding(max_length, emb_dim)        
        self.emb2hid = nn.Linear(emb_dim, hid_dim)
        self.hid2emb = nn.Linear(hid_dim, emb_dim)        
        self.convs = nn.ModuleList([nn.Conv1d(in_channels = hid_dim, 
                                              out_channels = 2 * hid_dim, 
                                              kernel_size = kernel_size, 
                                              padding = (kernel_size - 1) // 2)
                                    for _ in range(n_layers)])        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src):        
        #src = [batch size, src len]        
        batch_size = src.shape[0]
        src_len = src.shape[1]        
        #create position tensor
        pos = torch.arange(0, src_len).unsqueeze(0).repeat(batch_size, 1).to(self.device)        
        #pos = [0, 1, 2, 3, ..., src len - 1]        
        #pos = [batch size, src len]
        
        #embed tokens and positions
        tok_embedded = self.tok_embedding(src)
        pos_embedded = self.pos_embedding(pos)        
        #tok_embedded = pos_embedded = [batch size, src len, emb dim]
        
        #combine embeddings by elementwise summing
        embedded = self.dropout(tok_embedded + pos_embedded)
        
        #embedded = [batch size, src len, emb dim]
        
        #pass embedded through linear layer to convert from emb dim to hid dim
        conv_input = self.emb2hid(embedded)
        
        #conv_input = [batch size, src len, hid dim]
        
        #permute for convolutional layer
        conv_input = conv_input.permute(0, 2, 1) 
        
        #conv_input = [batch size, hid dim, src len]
        
        #begin convolutional blocks...
        
        for i, conv in enumerate(self.convs):
        
            #pass through convolutional layer
            conved = conv(self.dropout(conv_input))

            #conved = [batch size, 2 * hid dim, src len]

            #pass through GLU activation function
            conved = F.glu(conved, dim = 1)

            #conved = [batch size, hid dim, src len]
            
            #apply residual connection
            conved = (conved + conv_input) * self.scale

            #conved = [batch size, hid dim, src len]
            
            #set conv_input to conved for next loop iteration
            conv_input = conved
        
        #...end convolutional blocks
        
        #permute and convert back to emb dim
        conved = self.hid2emb(conved.permute(0, 2, 1))
        
        #conved = [batch size, src len, emb dim]
        
        #elementwise sum output (conved) and input (embedded) to be used for attention
        combined = (conved + embedded) * self.scale
        
        #combined = [batch size, src len, emb dim]
        
        return conved, combined

解码器

解码器接收实际的目标句子并尝试预测它。此模型与之前在这些教程中详细介绍的递归神经网络模型不同,因为它并行预测目标句子中的所有标记。没有顺序处理,即没有解码循环。本教程稍后将对此进行进一步详细说明。

解码器类似于编码器,只是对主模型和模型内的卷积块进行了一些更改。

首先,嵌入没有在卷积块和变换之后连接的残余连接。相反,嵌入被馈送到卷积块中,以用作那里的残余连接。

其次,为了从编码器输入解码器信息,使用编码器的卷积和组合输出 - 再次,在卷积块内。

最后,解码器的输出是从嵌入维度到输出维度的线性层。这是用来预测翻译中的下一个单词应该是什么。

解码器卷积块

同样,这些类似于编码器中的卷积块,但有一些变化。

 首先是填充。我们没有在每一侧均匀地填充以确保句子的长度在整个过程中保持不变,我们只在句子的开头填充。由于我们同时并行处理所有目标,而不是按顺序处理,因此我们需要一种方法,仅允许翻译令牌的过滤器仅在单词之前查看令牌i,如果允许他们查看令牌i+1(他们应该输出的令牌),模型将简单地通过直接复制来学习输出序列中的下一个单词,而无需实际学习如何翻译。

让我们看看如果我们错误地在每一侧均匀填充会发生什么,就像我们在编码器中所做的那样。

 第一个位置的过滤器,即尝试使用序列中的第一个单词 <sos> 来预测第二个单词,两个,现在可以直接看到单词2。对于每个位置都是一样的,模型尝试预测的单词是过滤器覆盖的第二个元素。因此,过滤器可以学会简单地在每个位置复制第二个单词,从而实现完美的翻译,而无需实际学习如何翻译。

其次,在GLU激活之后和残余连接之前,块计算并应用注意力 - 使用编码表示和当前单词的嵌入。注意:我们只显示与最右边令牌的连接,但它们实际上连接到所有令牌 - 这是为了清楚起见。每个令牌输入都使用自己的嵌入,并且只使用自己的嵌入来计算自己的注意力。

通过首先使用线性层将隐藏维度更改为与嵌入维度相同的大小来计算注意力。然后通过残余连接对嵌入求和。然后,通过查找它与编码的 convd 的“匹配”程度来应用标准注意计算,然后通过获取编码组合的加权和来应用此计算。然后将其投影回隐藏的维度大小,并应用到注意力层的初始输入的残差连接。

为什么他们首先用编码的凸次计算注意力,然后用它来计算编码组合的加权和?该论文认为,编码的convert有利于在编码序列上获得更大的上下文,而编码的组合具有有关特定令牌的更多信息,因此对于mank预测更有用。

解码器冲击

由于我们只在一侧填充,因此允许解码器同时使用奇数大小和偶数大小的填充。同样,刻度用于减少整个模型的方差,并且位置嵌入被初始化为具有100的“词汇量”。

该模型在其前向方法中采用编码器表示,并将两者都传递给计算和应用注意力的calculate_attention方法。它还返回实际的注意值,但我们当前未使用它们。

class Decoder(nn.Module):
    def __init__(self, 
                 output_dim, 
                 emb_dim, 
                 hid_dim, 
                 n_layers, 
                 kernel_size, 
                 dropout, 
                 trg_pad_idx, 
                 device,
                 max_length = 100):
        super().__init__()
        
        self.kernel_size = kernel_size
        self.trg_pad_idx = trg_pad_idx
        self.device = device
        
        self.scale = torch.sqrt(torch.FloatTensor([0.5])).to(device)
        
        self.tok_embedding = nn.Embedding(output_dim, emb_dim)
        self.pos_embedding = nn.Embedding(max_length, emb_dim)
        
        self.emb2hid = nn.Linear(emb_dim, hid_dim)
        self.hid2emb = nn.Linear(hid_dim, emb_dim)
        
        self.attn_hid2emb = nn.Linear(hid_dim, emb_dim)
        self.attn_emb2hid = nn.Linear(emb_dim, hid_dim)
        
        self.fc_out = nn.Linear(emb_dim, output_dim)
        
        self.convs = nn.ModuleList([nn.Conv1d(in_channels = hid_dim, 
                                              out_channels = 2 * hid_dim, 
                                              kernel_size = kernel_size)
                                    for _ in range(n_layers)])
        
        self.dropout = nn.Dropout(dropout)
      
    def calculate_attention(self, embedded, conved, encoder_conved, encoder_combined):
        
        #embedded = [batch size, trg len, emb dim]
        #conved = [batch size, hid dim, trg len]
        #encoder_conved = encoder_combined = [batch size, src len, emb dim]
        
        #permute and convert back to emb dim
        conved_emb = self.attn_hid2emb(conved.permute(0, 2, 1))
        
        #conved_emb = [batch size, trg len, emb dim]
        
        combined = (conved_emb + embedded) * self.scale
        
        #combined = [batch size, trg len, emb dim]
                
        energy = torch.matmul(combined, encoder_conved.permute(0, 2, 1))
        
        #energy = [batch size, trg len, src len]
        
        attention = F.softmax(energy, dim=2)
        
        #attention = [batch size, trg len, src len]
            
        attended_encoding = torch.matmul(attention, encoder_combined)
        
        #attended_encoding = [batch size, trg len, emd dim]
        
        #convert from emb dim -> hid dim
        attended_encoding = self.attn_emb2hid(attended_encoding)
        
        #attended_encoding = [batch size, trg len, hid dim]
        
        #apply residual connection
        attended_combined = (conved + attended_encoding.permute(0, 2, 1)) * self.scale
        
        #attended_combined = [batch size, hid dim, trg len]
        
        return attention, attended_combined
        
    def forward(self, trg, encoder_conved, encoder_combined):
        
        #trg = [batch size, trg len]
        #encoder_conved = encoder_combined = [batch size, src len, emb dim]
                
        batch_size = trg.shape[0]
        trg_len = trg.shape[1]
            
        #create position tensor
        pos = torch.arange(0, trg_len).unsqueeze(0).repeat(batch_size, 1).to(self.device)
        
        #pos = [batch size, trg len]
        
        #embed tokens and positions
        tok_embedded = self.tok_embedding(trg)
        pos_embedded = self.pos_embedding(pos)
        
        #tok_embedded = [batch size, trg len, emb dim]
        #pos_embedded = [batch size, trg len, emb dim]
        
        #combine embeddings by elementwise summing
        embedded = self.dropout(tok_embedded + pos_embedded)
        
        #embedded = [batch size, trg len, emb dim]
        
        #pass embedded through linear layer to go through emb dim -> hid dim
        conv_input = self.emb2hid(embedded)
        
        #conv_input = [batch size, trg len, hid dim]
        
        #permute for convolutional layer
        conv_input = conv_input.permute(0, 2, 1) 
        
        #conv_input = [batch size, hid dim, trg len]
        
        batch_size = conv_input.shape[0]
        hid_dim = conv_input.shape[1]
        
        for i, conv in enumerate(self.convs):
        
            #apply dropout
            conv_input = self.dropout(conv_input)
        
            #need to pad so decoder can't "cheat"
            padding = torch.zeros(batch_size, 
                                  hid_dim, 
                                  self.kernel_size - 1).fill_(self.trg_pad_idx).to(self.device)
                
            padded_conv_input = torch.cat((padding, conv_input), dim = 2)
        
            #padded_conv_input = [batch size, hid dim, trg len + kernel size - 1]
        
            #pass through convolutional layer
            conved = conv(padded_conv_input)

            #conved = [batch size, 2 * hid dim, trg len]
            
            #pass through GLU activation function
            conved = F.glu(conved, dim = 1)

            #conved = [batch size, hid dim, trg len]
            
            #calculate attention
            attention, conved = self.calculate_attention(embedded, 
                                                         conved, 
                                                         encoder_conved, 
                                                         encoder_combined)
            
            #attention = [batch size, trg len, src len]
            
            #apply residual connection
            conved = (conved + conv_input) * self.scale
            
            #conved = [batch size, hid dim, trg len]
            
            #set conv_input to conved for next loop iteration
            conv_input = conved
            
        conved = self.hid2emb(conved.permute(0, 2, 1))
         
        #conved = [batch size, trg len, emb dim]
            
        output = self.fc_out(self.dropout(conved))
        
        #output = [batch size, trg len, output dim]
            
        return output, attention

Seq2Seq

封装的Seq2Seq模块与以前的笔记本中使用的递归神经网络方法有很大不同,特别是在解码方面。

我们的 trg 将<eos>元素从序列的末尾切掉。这是因为我们不会将<eos>令牌输入解码器。

编码是相似的,插入源序列并接收“上下文向量”。但是,在这里,我们在源序列中的每个单词都有两个上下文向量,encoder_conved和encoder_combined。

由于解码是并行完成的,因此我们不需要解码循环。所有目标序列一次输入到解码器中,并且使用填充来确保解码器中的每个卷积滤波器在序列中滑动时只能看到序列中的当前和先前标记。

然而,这也意味着我们不能使用这种模式来teacher forcing。我们没有一个循环,在这个循环中,我们可以选择是输入预测的令牌还是序列中的实际令牌,因为一切都是并行预测的。

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder):
        super().__init__()
        
        self.encoder = encoder
        self.decoder = decoder
        
    def forward(self, src, trg):
        
        #src = [batch size, src len]
        #trg = [batch size, trg len - 1] (<eos> token sliced off the end)
           
        #calculate z^u (encoder_conved) and (z^u + e) (encoder_combined)
        #encoder_conved is output from final encoder conv. block
        #encoder_combined is encoder_conved plus (elementwise) src embedding plus 
        #  positional embeddings 
        encoder_conved, encoder_combined = self.encoder(src)
            
        #encoder_conved = [batch size, src len, emb dim]
        #encoder_combined = [batch size, src len, emb dim]
        
        #calculate predictions of next words
        #output is a batch of predictions for each word in the trg sentence
        #attention a batch of attention scores across the src sentence for 
        #  each word in the trg sentence
        output, attention = self.decoder(trg, encoder_conved, encoder_combined)
        
        #output = [batch size, trg len - 1, output dim]
        #attention = [batch size, trg len - 1, src len]
        
        return output, attention

Training the Seq2Seq Model

本教程的其余部分与之前的所有教程类似。我们定义所有的超参数,初始化编码器和解码器,并初始化整个模型 - 如果有的话,将其放在GPU上。

在论文中,他们发现使用小过滤器(内核大小为3)和高层数(5 +)更有益。

INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
EMB_DIM = 256
HID_DIM = 512 # each conv. layer has 2 * hid_dim filters
ENC_LAYERS = 10 # number of conv. blocks in encoder
DEC_LAYERS = 10 # number of conv. blocks in decoder
ENC_KERNEL_SIZE = 3 # must be odd!
DEC_KERNEL_SIZE = 3 # can be even or odd
ENC_DROPOUT = 0.25
DEC_DROPOUT = 0.25
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]
    
enc = Encoder(INPUT_DIM, EMB_DIM, HID_DIM, ENC_LAYERS, ENC_KERNEL_SIZE, ENC_DROPOUT, device)
dec = Decoder(OUTPUT_DIM, EMB_DIM, HID_DIM, DEC_LAYERS, DEC_KERNEL_SIZE, DEC_DROPOUT, TRG_PAD_IDX, device)

model = Seq2Seq(enc, dec).to(device)

我们还可以看到,该模型的参数几乎是基于注意力的模型(20m到37m)的两倍。

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')
The model has 37,351,173 trainable parameters

接下来,我们定义优化器和损失函数(标准)。和以前一样,我们忽略了目标序列是填充令牌的损失。

optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX)

然后,我们为模型定义训练循环。

我们对序列的处理方式与之前的教程略有不同。对于所有模型,我们从不<eos>将 放入解码器。这在 RNN 模型中通过使解码器循环无法到达<eos>作为解码器输入来处理。在此模型中,我们只是将<eos>令牌从序列的末尾切除。因此:

xi 表示实际的目标序列元素。然后,我们将其输入到模型中,以获得一个预测序列,该序列有望预测<eos>令牌:

 yi表示预测的目标序列元素。然后,我们使用原始 trg 张量计算损失,将<sos>令牌从前面切下,留下<eos>令牌:

 然后,我们计算损失并按标准更新参数。

def train(model, iterator, optimizer, criterion, clip):
    
    model.train()
    
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):
        
        src = batch.src
        trg = batch.trg
        
        optimizer.zero_grad()
        
        output, _ = model(src, trg[:,:-1])
        
        #output = [batch size, trg len - 1, output dim]
        #trg = [batch size, trg len]
        
        output_dim = output.shape[-1]
        
        output = output.contiguous().view(-1, output_dim)
        trg = trg[:,1:].contiguous().view(-1)
        
        #output = [batch size * trg len - 1, output dim]
        #trg = [batch size * trg len - 1]
        
        loss = criterion(output, trg)
        
        loss.backward()
        
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        
        optimizer.step()
        
        epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

评估循环与训练循环相同,只是没有梯度计算和参数更新。

def evaluate(model, iterator, criterion):
    
    model.eval()
    
    epoch_loss = 0
    
    with torch.no_grad():
    
        for i, batch in enumerate(iterator):

            src = batch.src
            trg = batch.trg

            output, _ = model(src, trg[:,:-1])
        
            #output = [batch size, trg len - 1, output dim]
            #trg = [batch size, trg len]

            output_dim = output.shape[-1]
            
            output = output.contiguous().view(-1, output_dim)
            trg = trg[:,1:].contiguous().view(-1)

            #output = [batch size * trg len - 1, output dim]
            #trg = [batch size * trg len - 1]
            
            loss = criterion(output, trg)

            epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

同样,我们有一个函数,告诉我们每个epoch需要多长时间。

def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs

最后,我们训练我们的模型。请注意,我们已将 CLIP 值从 1 降低到 0.1,以便更可靠地训练此模型。使用较高的 CLIP 值时,渐变偶尔会爆炸。

虽然我们的参数几乎是基于注意力的RNN模型的两倍,但它实际上花费的时间大约是标准版本的一半,大约是打包填充序列版本的时间。这是因为所有计算都是使用卷积过滤器并行完成的,而不是按顺序使用RNNs进行的。

注意:此模型始终具有 1 的教师强制比率,即它将始终使用目标序列中的下一个标记的地面事实。这意味着当以前的模型使用不是1的教师强迫比率时,我们无法将困惑值与以前的模型进行比较。请参阅此处,了解使用教师强迫比为1的基于注意力的RNN的结果。

N_EPOCHS = 10
CLIP = 0.1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):
    
    start_time = time.time()
    
    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)
    
    end_time = time.time()
    
    epoch_mins, epoch_secs = epoch_time(start_time, end_time)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut5-model.pt')
    
    print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')
Epoch: 01 | Time: 0m 31s
	Train Loss: 4.315 | Train PPL:  74.822
	 Val. Loss: 3.116 |  Val. PPL:  22.553
Epoch: 02 | Time: 0m 31s
	Train Loss: 3.097 | Train PPL:  22.123
	 Val. Loss: 2.434 |  Val. PPL:  11.402
Epoch: 03 | Time: 0m 32s
	Train Loss: 2.643 | Train PPL:  14.049
	 Val. Loss: 2.162 |  Val. PPL:   8.689
Epoch: 04 | Time: 0m 33s
	Train Loss: 2.399 | Train PPL:  11.015
	 Val. Loss: 2.025 |  Val. PPL:   7.575
Epoch: 05 | Time: 0m 35s
	Train Loss: 2.238 | Train PPL:   9.374
	 Val. Loss: 1.942 |  Val. PPL:   6.973
Epoch: 06 | Time: 0m 32s
	Train Loss: 2.119 | Train PPL:   8.326
	 Val. Loss: 1.895 |  Val. PPL:   6.653
Epoch: 07 | Time: 0m 32s
	Train Loss: 2.031 | Train PPL:   7.619
	 Val. Loss: 1.847 |  Val. PPL:   6.339
Epoch: 08 | Time: 0m 31s
	Train Loss: 1.956 | Train PPL:   7.069
	 Val. Loss: 1.816 |  Val. PPL:   6.146
Epoch: 09 | Time: 0m 32s
	Train Loss: 1.892 | Train PPL:   6.631
	 Val. Loss: 1.790 |  Val. PPL:   5.991
Epoch: 10 | Time: 0m 32s
	Train Loss: 1.837 | Train PPL:   6.280
	 Val. Loss: 1.775 |  Val. PPL:   5.899

然后,我们加载获得最低验证损失的参数,并计算测试集的损失。

model.load_state_dict(torch.load('tut5-model.pt'))

test_loss = evaluate(model, test_iterator, criterion)

print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')
| Test Loss: 1.834 | Test PPL:   6.261 |

推理
现在,我们可以使用下面的translate_sentence函数从模型中进行转换。

所采取的步骤是:

  • 如果源句子尚未标记化,则对其进行标记化(是字符串)
  • 附加 <sos> 和 <eos> 标记
  • 对源句子进行数值化
  • 将其转换为张量并添加批处理维度
  • 将源句子馈送到编码器中
  • 创建一个列表来保存输出句子,用<sos>标记初始化
  • 虽然我们没有达到最大长度
  • 将当前输出句子预测转换为具有批处理维度的张量
  • 将电流输出和两个编码器输出放入解码器中
  • 从解码器获取下一个输出令牌预测
  • 将预测添加到当前输出句子预测
  • 如果预测是令牌,则中断<eos>
  • 将输出句子从索引转换为标记
  • 返回输出句子(<sos>删除标记)和来自最后一层的注意力
    def translate_sentence(sentence, src_field, trg_field, model, device, max_len = 50):
    
        model.eval()
            
        if isinstance(sentence, str):
            nlp = spacy.load('de_core_news_sm')
            tokens = [token.text.lower() for token in nlp(sentence)]
        else:
            tokens = [token.lower() for token in sentence]
    
        tokens = [src_field.init_token] + tokens + [src_field.eos_token]
            
        src_indexes = [src_field.vocab.stoi[token] for token in tokens]
    
        src_tensor = torch.LongTensor(src_indexes).unsqueeze(0).to(device)
    
        with torch.no_grad():
            encoder_conved, encoder_combined = model.encoder(src_tensor)
    
        trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]
    
        for i in range(max_len):
    
            trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)
    
            with torch.no_grad():
                output, attention = model.decoder(trg_tensor, encoder_conved, encoder_combined)
            
            pred_token = output.argmax(2)[:,-1].item()
            
            trg_indexes.append(pred_token)
    
            if pred_token == trg_field.vocab.stoi[trg_field.eos_token]:
                break
        
        trg_tokens = [trg_field.vocab.itos[i] for i in trg_indexes]
        
        return trg_tokens[1:], attention

    接下来,我们有一个函数,该函数将显示模型在解码的每个步骤中对每个输入令牌的关注程度。

    def display_attention(sentence, translation, attention):
        
        fig = plt.figure(figsize=(10,10))
        ax = fig.add_subplot(111)
            
        attention = attention.squeeze(0).cpu().detach().numpy()
        
        cax = ax.matshow(attention, cmap='bone')
       
        ax.tick_params(labelsize=15)
        ax.set_xticklabels(['']+['<sos>']+[t.lower() for t in sentence]+['<eos>'], 
                           rotation=45)
        ax.set_yticklabels(['']+translation)
    
        ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
        ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
    
        plt.show()
        plt.close()

    然后我们终于开始翻译一些句子。

    首先,我们将从训练集中得到一个示例:

    example_idx = 2
    
    src = vars(train_data.examples[example_idx])['src']
    trg = vars(train_data.examples[example_idx])['trg']
    
    print(f'src = {src}')
    print(f'trg = {trg}')
    src = ['ein', 'kleines', 'mädchen', 'klettert', 'in', 'ein', 'spielhaus', 'aus', 'holz', '.']
    trg = ['a', 'little', 'girl', 'climbing', 'into', 'a', 'wooden', 'playhouse', '.']
    
    
    然后,我们将其传递到translate_sentence函数中,该函数为我们提供了预测的翻译标记以及注意力。
    我们可以看到,我们的模型将木制误译为塑料。
    translation, attention = translate_sentence(src, SRC, TRG, model, device)
    
    print(f'predicted trg = {translation}')
    predicted trg = ['a', 'little', 'girl', 'climbing', 'into', 'a', 'plastic', 'playhouse', '.', '<eos>']

我们可以查看模型的注意力,确保它给出敏感的外观结果。

我们可以看到它在生成代币塑料和剧场时会注意 spielhaus 。

display_attention(src, translation, attention)

 让我们看看它如何很好地翻译不在训练集中的示例。

example_idx = 2

src = vars(valid_data.examples[example_idx])['src']
trg = vars(valid_data.examples[example_idx])['trg']

print(f'src = {src}')
print(f'trg = {trg}')
src = ['ein', 'junge', 'mit', 'kopfhörern', 'sitzt', 'auf', 'den', 'schultern', 'einer', 'frau', '.']
trg = ['a', 'boy', 'wearing', 'headphones', 'sits', 'on', 'a', 'woman', "'s", 'shoulders', '.']

该模型设法在这一个中做了一个体面的工作。

translation, attention = translate_sentence(src, SRC, TRG, model, device)

print(f'predicted trg = {translation}')
predicted trg = ['a', 'boy', 'with', 'headphones', 'sits', 'on', 'the', 'shoulders', 'of', 'a', 'woman', '.', '<eos>']

同样,我们可以看到注意力被应用于合理的单词 - 男孩的接合,einer的和a等。奇怪的是,解码令牌时的注意力<eos>集中在frau上。

display_attention(src, translation, attention)

 最后,让我们检查测试集中的一个示例。

example_idx = 4

src = vars(test_data.examples[example_idx])['src']
trg = vars(test_data.examples[example_idx])['trg']

print(f'src = {src}')
print(f'trg = {trg}')
src = ['leute', 'reparieren', 'das', 'dach', 'eines', 'hauses', '.']
trg = ['people', 'are', 'fixing', 'the', 'roof', 'of', 'a', 'house', '.']

我们在这里得到了正确的翻译,交换是修复修复。

translation, attention = translate_sentence(src, SRC, TRG, model, device)

print(f'predicted trg = {translation}')
predicted trg = ['people', 'repair', 'the', 'roof', 'of', 'a', 'house', '.', '<eos>']

注意力似乎是正确的。我们再次对解码令牌有奇怪的关注<eos>,它似乎集中在句号之前的单词上。一种解释可能是模型正在关注句子中的最终单词,以查看输入序列是否由两个句子组成,如果不是,则生成<eos>标记。

display_attention(src, translation, attention)

 BLEU

最后,我们计算模型的BLEU分数。

from torchtext.data.metrics import bleu_score

def calculate_bleu(data, src_field, trg_field, model, device, max_len = 50):
    
    trgs = []
    pred_trgs = []
    
    for datum in data:
        
        src = vars(datum)['src']
        trg = vars(datum)['trg']
        
        pred_trg, _ = translate_sentence(src, src_field, trg_field, model, device, max_len)
        
        #cut off <eos> token
        pred_trg = pred_trg[:-1]
        
        pred_trgs.append(pred_trg)
        trgs.append([trg])
        
    return bleu_score(pred_trgs, trgs)

我们得到的BLEU分数约为34分,而基于注意力的RNN模型给我们的得分约为28分。这是BLEU分数提高约17%。

bleu_score = calculate_bleu(test_data, SRC, TRG, model, device)

print(f'BLEU score = {bleu_score*100:.2f}')
BLEU score = 34.14

我们现在已经介绍了第一个使用模型的非 RNN 模型!接下来是变形金刚模型,它甚至不使用卷积层 - 只有线性层和很多注意力机制。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值