自然语言处理技术博客

1*实验4:自然语言处理前馈网络(使用MLP)

1.MLP的基本概念

多层感知器(MLP)被认为是最基本的神经网络构建模块之一。MLP可以被看作是一个有向图,由多个的节点层所组成,每一层都全连接到下一层。除了输入节点,每个节点都是一个带有非线性激活函数的神经元。使用反向传播算法的监督学习方法用来训练MLP。MLP是感知器的推广,克服了感知器不能对线性不可分数据进行识别的弱点。多层感知器可以实现非线性判别式,如果用于回归可以逼近输入的非线性函数。且具有连续输入和输出的任何函数都可以用MLP近似。具有一个隐藏层的MLP可以学习输入的任意非线性函数(隐藏节点个数不限)。

2.MLP的基本框架

2.1感知器

每个感知器单元有一个输入(x),一个输出(y),和三个“旋钮”(knobs):一组权重(w),偏量(b),和一个激活函数(f)。权重和偏量都从数据学习,激活函数是精心挑选的取决于网络的网络设计师的直觉和目标输出。

数学上,我们可以这样表示:

𝑦=𝑓(𝑤𝑥+𝑏)𝑦=𝑓(𝑤𝑥+𝑏)

通常情况下感知器有不止一个输入。我们可以用向量表示这个一般情况;即,x和w是向量,w和x的乘积替换为点积:

𝑦=𝑓(𝑤⃗ 𝑇𝑥⃗ +𝑏)𝑦=𝑓(𝑤→𝑇𝑥→+𝑏)

激活函数,这里用f表示,通常是一个非线性函数。

2.2计算层

MLP除了简单的感知器之外,还有一个额外的计算层。我们可以使用PyTorch的两个线性模块实例化了这个想法。线性对象被命名为fc1和fc2,它们遵循一个通用约定,即将线性模块称为“完全连接层”,简称为“fc层”。其中每个神经元都与前一层的所有神经元相连接。在全连接层中,每个输入都与每个输出相关联,因此每个输出都是所有输入的加权和。除了这两个线性层外,还有一个修正的线性单元(ReLU)非线性,它在被输入到第二个线性层之前应用于第一个线性层的输出。由于层的顺序性,必须确保层中的输出数量等于下一层的输入数量。使用两个线性层之间的非线性是必要的,因为没有它,两个线性层在数学上等价于一个新线性层,因此不能建模复杂的模式。MLP的实现只实现反向传播的前向传递。这是因为PyTorch根据模型的定义和向前传递的实现,自动计算出如何进行向后传递和梯度更新。全连接层能够从前一层捕获和整合所有信息,并将其传递到每个输出神经元,这使得全连接层在处理数据时具有很高的灵活性。

建立该fc层代码如下

self.fc1 = nn.Linear(input_dim, hidden_dim)#第一个全连接层fc1,连接输入层和隐藏层
self.fc2 = nn.Linear(hidden_dim, output_dim)#第二个全连接层fc2,连接隐藏层和输出层

2.3前向传播

输入层到隐藏层:输入数据 𝑋 被送入第一个全连接层(通常是fc1)。
第一个全连接层中的每个神经元都会计算输入数据与该神经元的权重 𝑊1 的点积,然后加上一个偏置 𝑏1。
数学表达式为:𝑍1=𝑋⋅𝑊1+𝑏1,其中 𝑍1 是第一个全连接层的加权和加上偏置后的结果。
应用激活函数:然后,激活函数(如ReLU)被应用于 𝑍1 的每个元素上,以引入非线性。
ReLU激活函数的定义是:ReLU(𝑥)=max(0,𝑥)。
应用激活函数后的结果记为 𝐴1。
隐藏层到输出层:激活后的隐藏层输出 𝐴1 被送入第二个全连接层(通常是fc2)。
第二个全连接层中的每个神经元同样计算隐藏层输出与该神经元的权重 𝑊2 的点积,然后加上一个偏置 𝑏2。
数学表达式为:𝑍2=𝐴1⋅𝑊2+𝑏2,其中 𝑍2 是第二个全连接层的加权和加上偏置后的结果。

2.4处理XOR

让我们看一下XOR示例,看看感知器与MLP之间会发生什么。在这个例子中,我们在一个二元分类任务中训练感知器和MLP:星和圆。每个数据点是一个二维坐标。在不深入研究实现细节的情况下,最终的模型预测如图所示。在这个图中,错误分类的数据点用黑色填充,而正确分类的数据点没有填充。在左边的面板中,从填充的形状可以看出,感知器在学习一个可以将星星和圆分开的决策边界方面有困难。然而,MLP(右面板)学习了一个更精确地对恒星和圆进行分类的决策边界。

3.MLP的使用

3.1The Surname Dataset

姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,这些姓氏是作者从互联网上不同的姓名来源收集的。该数据集将在本课程实验的几个示例中重用,并具有一些使其有趣的属性。

数据集具有两个主要特点:

  1. 不平衡性:数据集中,排名前三的姓氏(英语、俄语和阿拉伯语)占据了超过60%的数据,而其他15个民族的姓氏频率则较低。

  2. 国籍与姓氏拼写之间的关系:某些姓氏的拼写变体与其原籍国之间存在紧密的联系。

为了创建最终的数据集,我们首先对原始数据集进行了处理,以减少不平衡性。原始数据集中俄语姓氏的比例过高,因此我们通过随机选择俄语姓氏的子集来解决这个问题。接着,再根据国籍将数据集分为三个部分:70%作为训练数据集,15%作为验证数据集,最后15%作为测试数据集。

3.2Vocabulary, Vectorizer, and DataLoader

为了使用字符对姓氏进行分类,我们需要将姓氏字符串转换为向量化的minibatches。这个过程涉及到使用词汇表、向量化器和DataLoader。这些数据结构与“Example: Classifying Sentiment of Restaurant Reviews”中使用的数据结构相同,它们展示了姓氏的字符标记与Yelp评论的单词标记之间的多态性。在这个例子中,数据不是通过将字令牌映射到整数来向量化的,而是通过将字符映射到整数来向量化的。

词汇表类(Vocabulary Class)

本例中使用的词汇类与“Example: Classifying Sentiment of Restaurant Reviews”中的词汇类完全相同。词汇表是两个Python字典的协调,这两个字典在令牌(在本例中是字符)和整数之间形成一个双射。第一个字典将字符映射到整数索引,第二个字典将整数索引映射到字符。add_token方法用于向词汇表中添加新的令牌,lookup_token方法用于检索索引,lookup_index方法用于检索给定索引的令牌(在推断阶段很有用)。在这个例子中,我们使用的是one-hot词汇表,不计算字符出现的频率,只对频繁出现的条目进行限制。这主要是因为数据集很小,而且大多数字符足够频繁。

姓氏向量化器(SurnameVectorizer)

虽然词汇表将单个令牌(字符)转换为整数,但SurnameVectorizer负责应用词汇表并将姓氏转换为向量。实例化和使用非常类似于“示例:对餐馆评论的情绪进行分类”中的ReviewVectorizer,但有一个关键区别:字符串没有在空格上分割。姓氏是字符的序列,每个字符在我们的词汇表中是一个单独的标记。然而,在“卷积神经网络”出现之前,我们将忽略序列信息,通过迭代字符串输入中的每个字符来创建输入的收缩one-hot向量表示。我们为以前未遇到的字符指定一个特殊的令牌,即UNK。由于我们仅从训练数据实例化词汇表,而且验证或测试数据中可能有惟一的字符,所以在字符词汇表中仍然使用UNK符号。

虽然我们在这个示例中使用了收缩的one-hot,但是在后面的实验中,将了解其他向量化方法,它们是one-hot编码的替代方法,有时甚至更好。具体来说,在“示例:使用CNN对姓氏进行分类”中,将看到一个热门矩阵,其中每个字符都是矩阵中的一个位置,并具有自己的热门向量。然后,在实验5中,将学习嵌入层,返回整数向量的向量化,以及如何使用它们创建密集向量矩阵。

def vectorize(self, surname):
        """Vectorize the provided surname

        Args:
            surname (str): the surname
        Returns:
            one_hot (np.ndarray): a collapsed one-hot encoding
        """
        vocab = self.surname_vocab
        one_hot = np.zeros(len(vocab), dtype=np.float32)#创建一个全零的向量,长度为词汇表的大小
        #对于姓氏中的每个token,将对应的词汇表索引位置设为1
        for token in surname:
            one_hot[vocab.lookup_token(token)] = 1
        return one_hot

3.3The Surname Classifier Model

SurnameClassifier可以基于多种特征来区分不同的姓氏,例如语言、文化、地理起源等。例如,一个姓氏分类器模型可能能够识别一个姓氏是来自中文、英文、西班牙文还是其他语言背景。SurnameClassifier是多层感知机(MLP)的实现,它包含两个线性层,第一个线性层将输入向量映射到中间向量,并应用非线性激活函数,第二个线性层将中间向量映射到预测向量。在最后一步中,可以选择应用softmax操作以确保输出和为1,这提供了概率分布。然而,是否应用softmax取决于所使用的损失函数,因为交叉熵损失在多类分类中是最理想的,但在训练过程中,softmax的计算可能是不必要的,并且在很多情况下是不稳定的。

3.4DROPOUT

Dropout是一种在训练深度神经网络时使用的正则化技术,其目的是通过随机丢弃网络中的部分神经元,来减少模型对某些特定训练样本或特征的过度依赖,从而提高模型的泛化能力,防止过拟合。

在训练过程中,Dropout会按照一定的概率(drop probability,通常设置为0.5)随机选择一些神经元,暂时从网络中移除,即切断这些神经元与其他神经元之间的连接。这样,网络中的其他神经元必须适应这种变化,学习到更加泛化的特征表示。

当训练完成后,网络中的神经元之间的依赖关系减少,模型对训练数据的依赖也减少,因此模型在未见过的数据上的表现会更好。

Dropout不会改变模型的参数,它只是通过随机性来干扰训练过程,从而提高模型的泛化能力。因此,Dropout是一个简单且有效的正则化方法,被广泛应用于深度学习模型的训练中。

intermediate = F.relu(self.fc1(x_in))#第一个全连接层的输出,通过ReLU激活函数
output = self.fc2(F.dropout(intermediate, p=0.5))#第二个全连接层的输出,通过Dropout层和ReLU激活函数,随机失活概率为0.5

dropout只适用于训练期间,不适用于评估期间。

3.5Code

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

class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): the size of the input vectors
            hidden_dim (int): the output size of the first Linear layer
            output_dim (int): the output size of the second Linear layer
        """
        super(MultilayerPerceptron, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)

    def forward(self, x_in, apply_softmax=False):
        """The forward pass of the MLP

        Args:
            x_in (torch.Tensor): an input data tensor.
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        intermediate = F.relu(self.fc1(x_in))#第一个全连接层的输出,通过ReLU激活函数
        output = self.fc2(F.dropout(intermediate, p=0.5))#第二个全连接层的输出,通过Dropout层和ReLU激活函数,随机失活概率为0.5

        if apply_softmax:
            output = F.softmax(output, dim=1)
        return output

2*实验13:机器翻译

1.机器翻译的概念

机器翻译是指将一段文本从一种语言自动翻译到另一种语言。因为一段文本序列在不同语言中的长度不一定相同,所以我们使用机器翻译为例来介绍编码器—解码器和注意力机制的应用。机器翻译的基本思路是:分析源语言文本,将其分解成可以被计算机理解的单元,如词语、短语或语法结构。将这些单元映射到目标语言的相应单元上。重新组合目标语言的单元,生成最终的翻译文本。

2.机器翻译的流程

2.1读取和预处理数据

我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束。

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'

接着定义两个辅助函数对后面读取的数据进行预处理。为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为max_seq_len

in_seq, out_seq = line.rstrip().split('\t')
in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
     continue  # 如果加上EOS后长于max_seq_len,则忽略掉此样本

我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。将序列的最大长度设成7,然后查看读取到的第一个样本。该样本分别包含法语词索引序列和英语词索引序列。

2.2含注意力机制的编码器—解码器

含注意力机制的编码器-解码器模型是神经机器翻译(NMT)中的一种重要结构,特别是在序列到序列(Seq2Seq)模型中。它最初由Google的Dzmitry Bahdanau等人在2014年提出,以解决传统编码器-解码器模型在处理长距离依赖时的困难。在传统的编码器-解码器模型中,编码器将输入序列(源语言)编码成一个固定长度的向量,然后解码器使用这个向量生成目标语言的序列。然而,对于长句子,编码器可能无法完全捕获源语言的所有相关信息,特别是对于长距离的依赖关系。注意力机制引入了动态的机制,它允许解码器在生成目标语言的每个词时,根据当前正在生成的词,对源语言的编码向量进行“加权”(attention)选择,而不是仅仅依赖于固定长度的编码向量。这样,解码器可以根据需要“聚焦”在源语言的不同部分,提高了翻译的准确性。

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。正如我们在此前中提到的,PyTorch的nn.GRU实例在前向计算后也会分别返回输出和最终时间步的多层隐藏状态。其中的输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制将这些输出作为键项和值项。

下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,state就是一个元素,即隐藏状态;如果使用长短期记忆,state是一个元组,包含两个元素即隐藏状态和记忆细胞。

encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
output.shape, state.shape # GRU的state是h, 而LSTM的是一个元组(h, c)

我们将实现此前中定义的函数𝑎𝑎:将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。两个Linear实例均不使用偏差。其中函数𝑎𝑎定义里向量𝑣𝑣的长度是一个超参数,即attention_size

def attention_model(input_size, attention_size):
    model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False),
                          nn.Tanh(),
                          nn.Linear(attention_size, 1, bias=False))
    return model

注意力机制的输入包括查询项、键项和值项。设编码器和解码器的隐藏单元个数相同。这里的查询项为解码器在上一时间步的隐藏状态,形状为(批量大小, 隐藏单元个数);键项和值项均为编码器在所有时间步的隐藏状态,形状为(时间步数, 批量大小, 隐藏单元个数)。注意力机制返回当前时间步的背景变量,形状为(批量大小, 隐藏单元个数)。

2.3含注意力机制的解码器

我们直接将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态。这要求编码器和解码器的循环神经网络使用相同的隐藏层个数和隐藏单元个数。

在解码器的前向计算中,我们先通过刚刚介绍的注意力机制计算得到当前时间步的背景向量。由于解码器的输入来自输出语言的词索引,我们将输入通过词嵌入层得到表征,然后和背景向量在特征维连结。我们将连结后的结果与上一时间步的隐藏状态通过门控循环单元计算出当前时间步的输出与隐藏状态。最后,我们将输出通过全连接层变换为有关各个输出词的预测,形状为(批量大小, 输出词典大小)。

在 Decoder 类的构造函数中,我们定义了嵌入层、注意力机制、RNN 和输出层的参数。在 forward 函数中,使用这些参数计算输入序列中下一个单词的概率分布。在 begin_state 函数中,直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态。

2.4code(编码器—解码器)

class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 drop_prob=0, **kwargs):
        super(Encoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)

    def forward(self, inputs, state):
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
        return self.rnn(embedding, state)

    def begin_state(self):
        return None
class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 attention_size, drop_prob=0):
        super(Decoder, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.attention = attention_model(2*num_hiddens, attention_size)
        # GRU的输入包含attention输出的c和实际输入, 所以尺寸是 num_hiddens+embed_size
        self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens, 
                          num_layers, dropout=drop_prob)
        self.out = nn.Linear(num_hiddens, vocab_size)

    def forward(self, cur_input, state, enc_states):
        """
        cur_input shape: (batch, )
        state shape: (num_layers, batch, num_hiddens)
        """
        # 使用注意力机制计算背景向量
        c = attention_forward(self.attention, enc_states, state[-1])
        # 将嵌入后的输入和背景向量在特征维连结, (批量大小, num_hiddens+embed_size)
        input_and_c = torch.cat((self.embedding(cur_input), c), dim=1) 
        # 为输入和背景向量的连结增加时间步维,时间步个数为1
        output, state = self.rnn(input_and_c.unsqueeze(0), state)
        # 移除时间步维,输出形状为(批量大小, 输出词典大小)
        output = self.out(output).squeeze(dim=0)
        return output, state

    def begin_state(self, enc_state):
        # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
        return enc_state

3.训练模型

我们先实现batch_loss函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符BOS。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。我们在这里也使用掩码变量避免填充项对损失函数计算的影响。首先使用编码器处理输入序列 X,获取编码器的输出 enc_outputs 和最终状态 enc_state。然后初始化解码器的隐藏状态 dec_state 为编码器的最终状态,初始化解码器的输入为特殊字符 BOS。遍历目标序列 Y,每次迭代中:使用解码器处理当前输入,获取输出 dec_output 和新的隐藏状态 dec_state。接着计算损失,使用掩码 mask 忽略填充项 PAD 的损失并更新解码器的输入为当前目标序列的词,更新掩码 mask,确保遇到 EOS 后不再计算损失。最后返回平均损失。

在训练函数中,我们需要同时迭代编码器和解码器的模型参数。首先为编码器和解码器分别创建优化器。再定义交叉熵损失函数。接着使用数据加载器加载训练数据,遍历每个 epoch,在每个 batch 中:清零编码器和解码器的梯度。最后计算损失并反向传播并更新编码器和解码器的参数,累加损失。

接下来,创建模型实例并设置超参数。然后,我们就可以训练模型了。

4.评价翻译结果

评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)。所谓Understudy (替补),意思是代替人进行翻译结果的评估。尽管这项指标是为翻译而发明的,但它可以用于评估一组自然语言处理任务生成的文本。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

下面来实现BLEU的计算。

def bleu(pred_tokens, label_tokens, k):
    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
def score(input_seq, label_seq, k):
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    label_tokens = label_seq.split(' ')
    print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
                                      ' '.join(pred_tokens)))

3*实验14:Japanese-Chinese Machine Translation Model with Transformer & PyTorch

1.流程

3.1数据准备

在本次实验中,我们将使用从 JParaCrawl 下载的日语-英语平行数据集,该数据集被描述为“由 NTT 创建的最大的公开可用的英语-日语平行语料库,主要通过网络爬虫和自动对齐平行句子创建”。下载数据集: 访问 JParaCrawl 的官方网站,下载日语-英语平行数据集。确保下载的数据集包含日语和英语的平行句子。我们需要删除数据集中最后一个有缺失值的样本,在数据集明显很大的情况下(5,973,071)确实可以采取抽样策略来验证模型的性能和稳定性。

3.2模型架构

与英语或其他字母语言不同,日语句子中不包含用于分隔单词的空格。我们可以使用JParaCrawl提供的分词器,它使用SentencePiece创建,适用于日语和英语。使用分词器和原始句子,我们可以构建TorchText中的Vocab对象。这个过程可能需要几秒或几分钟,这取决于我们数据集的大小和计算能力。不同的分词器可能需要的时间也不同。SentencePiece对我来说工作得很好且足够快。

def build_vocab(sentences, tokenizer):
  #构建词汇表  
   counter = Counter()  #创建一个计数器对象,用于统计词频
    for sentence in sentences:
       #使用tokenizer将句子分词,并更新计数器
       counter.update(tokenizer.encode(sentence, out_type=str))
   #返回一个Vocab对象,包含词汇表和特殊标记
   return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

#构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
#构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

在我们得到词汇对象之后,我们可以使用词汇对象和分词器对象来构建训练数据的张量。

def data_process(ja, en):
  #处理日语和英语数据,将句子转换为张量  
data = []  #用于存储处理后的数据
   for raw_ja, raw_en in zip(ja, en):  #遍历日语和英语句子对
       #将日语句子转换为张量
       ja_tensor_ = torch.tensor(
           [ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\\n"), out_type=str)],
           dtype=torch.long
       )
       #将英语句子转换为张量
       en_tensor_ = torch.tensor(
           [en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\\n"), out_type=str)],
           dtype=torch.long
       )
       #将日语和英语句子张量对添加到数据列表中
       data.append((ja_tensor_, en_tensor_))
    return data
#处理训练数据
train_data = data_process(trainja, trainen)

Transformer 是一种由“Attention is all you need”一文介绍的 Seq2Seq 模型,用于解决机器翻译任务。Transformer 模型由一个编码器和一个解码器组成,每个块包含固定数量的层。编码器通过一系列多头注意力和前馈网络层来处理输入序列,输出从编码器被称为记忆,并被馈送到解码器以及目标张量。编码器和解码器都是进行端到端的训练。前文我们有详细介绍,这里不再过多赘述。Text tokens使用token嵌入表示。为了引入词序的概念,将位置编码添加到token嵌入中。在序列建模中,掩码是一个重要的部分。它允许模型忽略填充的部分,只关注实际的词汇。我们将实现一些函数来生成掩码,以便模型能够正确处理输入和目标序列。在序列到序列(Seq2Seq)模型中,创建后续词掩码(future mask)或前瞻掩码(lookahead mask),目的是在解码阶段防止目标词访问后续的词,从而实现自回归的预测,即一次只预测一个词,不能看到后续的输入。对于源和目标的填充token,我们也会创建填充掩码。填充token通常用于填充序列的结尾,以便模型知道哪些部分是实际的输入,哪些部分是填充的。填充掩码通常为二进制,1表示实际的词,0表示填充,这样模型在计算时可以忽略填充部分,只处理有效信息。

def generate_square_subsequent_mask(sz):
   """
   生成一个方形的自注意力掩码,用于Transformer模型中的解码器部分,以防止位置看到后续位置的信息。

   Args:
       sz (int): 掩码的大小,即序列的长度。

   Returns:
       torch.Tensor: 一个大小为 (sz, sz) 的掩码张量,其中上三角部分为 -inf,下三角部分为 0.0。
   """
   #创建一个大小为(sz, sz)的上三角矩阵,元素为1
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
   #将上三角部分填充为-inf,下三角部分填充为0.0
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src, tgt):
   """
   创建用于Transformer模型的掩码,包括源序列和目标序列的掩码。

   Args:
       src (torch.Tensor): 源序列张量。
       tgt (torch.Tensor): 目标序列张量。

   Returns:
       tuple: 包含四个掩码张量的元组 (src_mask, tgt_mask, src_padding_mask, tgt_padding_mask)。
   """
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]

   #生成目标序列的自注意力掩码
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
   #源序列的自注意力掩码为全0,因为编码器不需要防止看到后续位置
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

   #生成源序列和目标序列的填充掩码
    src_padding_mask = (src == PAD_IDX).transpose(0, 1)
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

3.3训练模型

最后,在准备好必要的类和函数之后,我们就可以开始训练我们的模型了。这一点不言而喻,但完成训练所需的时间可能会因许多因素而有很大的差异,比如计算能力、参数设置和数据集的大小。首先,我们创建一个函数来翻译新句子,包括获取日语句子、分词、转换为张量、推理以及将结果解码回英文句子的步骤,其中使用贪心算法进行解码。

def greedy_decode(model, src, src_mask, max_len, start_symbol):
   """
   使用贪心算法进行解码。

   Args:
       model (nn.Module): 要使用的模型。
       src (torch.Tensor): 源序列张量。
       src_mask (torch.Tensor): 源序列掩码。
       max_len (int): 解码的最大长度。
       start_symbol (int): 开始符号的索引。

   Returns:
       torch.Tensor: 解码后的目标序列张量。
   """
    src = src.to(device)#将源序列移动到指定设备
    src_mask = src_mask.to(device)#将源序列掩码移动到指定设备
    memory = model.encode(src, src_mask)#对源序列进行编码
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)#初始化目标序列,填充开始符号
    for i in range(max_len-1):
        memory = memory.to(device)#将编码记忆移动到指定设备
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)#创建记忆掩码
        tgt_mask = (generate_square_subsequent_mask(ys.size(0))
                                   .type(torch.bool)).to(device)#创建目标序列掩码
        out = model.decode(ys, memory, tgt_mask)#对目标序列进行解码
        out = out.transpose(0, 1)#转置输出
        prob = model.generator(out[:, -1])#生成下一个词的概率
        _, next_word = torch.max(prob, dim=1)#选择概率最大的词
        next_word = next_word.item()#获取下一个词的索引
        ys = torch.cat([ys,
                       torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)#将下一个词添加到目标序列
        if next_word == EOS_IDX:#如果下一个词是结束符号,停止解码
            break
    return ys

def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
   """
   将源语言句子翻译成目标语言句子。

   Args:
       model (nn.Module): 要使用的模型。
       src (str): 源语言句子。
       src_vocab (Vocab): 源语言词汇表。
       tgt_vocab (Vocab): 目标语言词汇表。
       src_tokenizer (Tokenizer): 源语言分词器。

   Returns:
       str: 翻译后的目标语言句子。
   """
    model.eval()#设置模型为评估模式
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]#将源句子分词并转换为索引
    num_tokens = len(tokens)#获取分词后的长度
    src = (torch.LongTensor(tokens).reshape(num_tokens, 1))#将分词后的索引转换为张量
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)#创建源序列掩码
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()#使用贪心算法进行解码
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")#将解码后的索引转换为句子并去除开始和结束符号

然后,我们只需调用翻译函数并传入所需的参数。最后,在训练完成后,我们将首先使用Pickle保存Vocab对象(en_vocab和ja_vocab),我们还可以使用PyTorch的保存和加载功能来将模型保存起来以备后续使用。通常,根据我们之后想要如何使用模型,有两种保存模型的方法。

第一种方法仅用于推理,我们可以稍后加载模型并使用它来进行日语到英语的翻译。

# save model for inference
torch.save(transformer.state_dict(), 'inference_model')

第二种方法也是用于推理的,但是当我们想在稍后加载模型并继续训练时也是有用的。

# save model + checkpoint to resume training later
torch.save({
  'epoch': NUM_EPOCHS,
  'model_state_dict': transformer.state_dict(),
  'optimizer_state_dict': optimizer.state_dict(),
  'loss': train_loss,
  }, 'model_checkpoint.tar')

3.4总结

通过这篇文章,我们可以获得从零开始构建和训练一个基于Transformer的机器翻译模型的全面指导,That’s it!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值