基于Transformer实现机器翻译

NLP - 基于Transformer实现机器翻译

一、引言

1.1 机器翻译概论

机器翻译(Machine Translation,简称MT)是指运用机器,通过特定的计算机程序将一种书写形式或声音形式的自然语言,翻译成另一种书写形式或声音形式的自然语言的过程。机器翻译是一门交叉学科,由计算机语言学、人工智能和数理逻辑等子学科构成,这些子学科又分别建立在语言学、计算机科学和数学的基础之上。机器翻译可以实现不同国家不同语言间的低成本交流,其主要优点包括成本低、易把控和速度快。 机器翻译技术的发展经历了从基于规则的翻译方法,到基于统计的翻译方法,再到当前基于神经网络的翻译方法的演变。随着深度学习等先进技术的发展,机器翻译的质量得到了显著提升,已经能够处理更加复杂和自然的语言表达。

1.2 发展前景

随着深度学习技术的迅猛发展和计算能力的提升,机器翻译系统在翻译质量和速度上已经取得了显著的进展。未来,我们可以期待更加智能化和个性化的翻译服务,能够根据上下文和用户偏好动态调整翻译策略。同时,随着多模态数据(例如图像和语音)翻译的需求增加,多模态机器翻译也将成为一个重要的研究方向。此外,跨语言理解和语境感知的能力将进一步提升,使得机器翻译在跨文化交流、全球化业务和教育等领域发挥更大作用。然而,尽管技术进步迅猛,机器翻译仍面临着挑战,如处理低资源语言、保持翻译的语义准确性和文化敏感性等问题,这些都需要持续的研究和创新来解决。总体而言,机器翻译的未来充满了希望,有望进一步推动全球语言交流和理解的进程。

二、实验步骤

2.1 读取和预处理数据

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

!tar -xf d2lzh_pytorch.tar  # 使用 tar 命令的 -xf 选项来解压缩一个 tar 文件

import collections
import os
import io
import math
import torch
from torch import nn
import torch.nn.functional as F
import torchtext.vocab as Vocab
import torch.utils.data as Data

import sys
# sys.path.append("..") 
import d2lzh_pytorch as d2l

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'  # 定义三个特殊标记,用于表示填充、句子开始和句子结束
os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 设置环境变量,指定CUDA可见的GPU设备编号。这里设置为"0",表示只使用第一个GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 根据是否可用设置设备变量device。如果CUDA可用,则使用GPU;否则,使用CPU

print(torch.__version__, device)

然后我们定义两个辅助函数对后面读取的数据进行预处理。

# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    all_seqs.append(seq_tokens)

# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
    vocab = Vocab.Vocab(collections.Counter(all_tokens),
                        specials=[PAD, BOS, EOS])
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
    return vocab, torch.tensor(indices)

为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“”符号,并可能通过添加“”符号使每个序列的长度均为max_seq_len。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。

def read_data(max_seq_len):  
    # 初始化输入和输出的token列表,以及序列列表  
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []  
  
    # 打开文件并读取所有行  
    with io.open('fr-en-small.txt') as f:  
        lines = f.readlines()  
  
    # 遍历每一行  
    for line in lines:  
        # 分割输入和输出序列  
        in_seq, out_seq = line.rstrip().split('\t')  
        # 分割token  
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')  
  
        # 如果序列长度(加上EOS后)超过max_seq_len,则忽略此样本  
        if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:  
            continue  
  
        # 处理输入序列  
        process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)  
        # 处理输出序列  
        process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)  
  
    # 构建输入数据的词汇表和Tensor数据  
    in_vocab, in_data = build_data(in_tokens, in_seqs)  
    # 构建输出数据的词汇表和Tensor数据  
    out_vocab, out_data = build_data(out_tokens, out_seqs)  
  
    # 返回输入和输出的词汇表,以及TensorDataset(用于PyTorch训练)  
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

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

max_seq_len = 7  # 设置最大序列长度为7 
in_vocab, out_vocab, dataset = read_data(max_seq_len)  # 调用read_data函数读取数据并返回输入和输出的词汇表以及数据集  
dataset[0]

结果如下

(tensor([ 5,  4, 45,  3,  2,  0,  0]), tensor([ 8,  4, 27,  3,  2,  0,  0]))

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

2.2.1 编码器

编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。

我们可以用函数 f 表达循环神经网络隐藏层的变换:
h t = f ( x t , h t − 1 ) . \boldsymbol{h}_t = f(\boldsymbol{x}_t, \boldsymbol{h}_{t-1}). ht=f(xt,ht1).

接下来,编码器通过自定义函数 q 将各个时间步的隐藏状态变换为背景变量

c = q ( h 1 , … , h T ) . \boldsymbol{c} = q(\boldsymbol{h}_1, \ldots, \boldsymbol{h}_T). c=q(h1,,hT).

以上描述的编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,编码器每个时间步的隐藏状态同时取决于该时间步之前和之后的子序列(包括当前时间步的输入),并编码了整个序列的信息。

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

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)  # 初始化GRU层,使用dropout在多层RNN中减少过拟合 

    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

比如下面我们来创建一个批量大小为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)

结果如下

(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))
2.2.2 注意力机制

在编码器-解码器(seq2seq)模型中,注意力机制是一种关键技术,用于提升翻译或生成任务的性能。它允许模型在生成目标语言序列时动态地关注输入序列中不同部分的信息,而不是像传统的固定长度表示那样静态地压缩整个输入序列信息。注意力机制通过计算每个输入位置的注意力权重,使得解码器能够根据当前正在生成的目标语言单词,有针对性地访问编码器的不同隐藏状态。这种方式使得模型能够更好地处理长距离依赖和复杂语法结构,从而提升翻译质量和生成效果。

​图1 编码器—解码器上的注意力机制

​ 图1 编码器—解码器上的注意力机制

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

# 定义一个注意力模型,包含两个线性层和一个Tanh激活函数 
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

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

def attention_forward(model, enc_states, dec_state):
    """
    enc_states: (时间步数, 批量大小, 隐藏单元个数)
    dec_state: (批量大小, 隐藏单元个数)
    """
    # 将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结
    dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
    enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
    e = model(enc_and_dec_states)  # 形状为(时间步数, 批量大小, 1)
    alpha = F.softmax(e, dim=0)  # 在时间步维度做softmax运算
    return (alpha * enc_states).sum(dim=0)  # 返回背景变量

如在下面的例子中,编码器的时间步数为10,批量大小为4,编码器和解码器的隐藏单元个数均为8。注意力机制返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数。因此输出的形状为(4, 8)。

seq_len, batch_size, num_hiddens = 10, 4, 8
model = attention_model(2*num_hiddens, 10)  # 注意力模型
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))  # 编码器状态
dec_state = torch.zeros((batch_size, num_hiddens))  # 解码器状态 
attention_forward(model, enc_states, dec_state).shape

结果如下

torch.Size([4, 8])
2.2.3 含注意力机制的解码器

含注意力机制的解码器与传统的解码器不同之处在于,能够根据当前正在生成的目标语言单词动态地调整对输入序列的关注度。在每个时间步,解码器会计算注意力权重,这些权重反映了每个输入位置对当前生成位置的重要性。通过将编码器的隐藏状态与这些权重加权求和,解码器构建出一个上下文向量,该向量集中于与当前生成内容相关的输入信息。这种动态调整的能力使得模型能够更有效地处理长距离依赖和复杂的语义结构,从而提升了生成序列的质量和流畅度。

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

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

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

2.3 训练模型

根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率

P ( y 1 , … , y T ′ ∣ x 1 , … , x T ) = ∏ t ′ = 1 T ′ P ( y t ′ ∣ y 1 , … , y t ′ − 1 , x 1 , … , x T ) = ∏ t ′ = 1 T ′ P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) , \begin{aligned} P(y_1, \ldots, y_{T'} \mid x_1, \ldots, x_T) &= \prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, x_1, \ldots, x_T)\\ &= \prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \boldsymbol{c}), \end{aligned} P(y1,,yTx1,,xT)=t=1TP(yty1,,yt1,x1,,xT)=t=1TP(yty1,,yt1,c),

并得到该输出序列的损失

− log ⁡ P ( y 1 , … , y T ′ ∣ x 1 , … , x T ) = − ∑ t ′ = 1 T ′ log ⁡ P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) , -\log P(y_1, \ldots, y_{T'} \mid x_1, \ldots, x_T) = -\sum_{t'=1}^{T'} \log P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \boldsymbol{c}), logP(y1,,yTx1,,xT)=t=1TlogP(yty1,,yt1,c),

在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。在模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上一个时间步的标签作为解码器在当前时间步的输入。这叫作强制教学(teacher forcing)。

我们先实现batch_loss函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符BOS。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。此外,我们在这里使用掩码变量避免填充项对损失函数计算的影响。

def batch_loss(encoder, decoder, X, Y, loss):
    batch_size = X.shape[0]
    enc_state = encoder.begin_state()
    enc_outputs, enc_state = encoder(X, enc_state)
    # 初始化解码器的隐藏状态
    dec_state = decoder.begin_state(enc_state)
    # 解码器在最初时间步的输入是BOS
    dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
    # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0
    l = torch.tensor([0.0])
    for y in Y.permute(1,0): # Y shape: (batch, seq_len)
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
        l = l + (mask * loss(dec_output, y)).sum()
        dec_input = y  # 使用强制教学
        num_not_pad_tokens += mask.sum().item()
        # EOS后面全是PAD. 下面一行保证一旦遇到EOS接下来的循环中mask就一直是0
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    return l / num_not_pad_tokens

在训练函数中,我们需要同时迭代编码器和解码器的模型参数。

def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
    dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)

    loss = nn.CrossEntropyLoss(reduction='none')  # 损失函数
    data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)  # 创建数据加载器
    # 训练循环 
    for epoch in range(num_epochs):
        l_sum = 0.0
        for X, Y in data_iter:
            # 清零梯度 
            enc_optimizer.zero_grad()
            dec_optimizer.zero_grad()
            l = batch_loss(encoder, decoder, X, Y, loss)  # 计算当前批次的损失
            l.backward()  # 反向传播计算梯度
            # 更新编码器和解码器的参数
            enc_optimizer.step()
            dec_optimizer.step()
            l_sum += l.item()  # 累计损失
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))  # 每10个epoch打印一次平均损失 

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

# 定义嵌入大小、隐藏层大小、隐藏层数等超参数 
embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
# 初始化编码器、解码器,参数包括输入词汇表大小、嵌入大小、隐藏层大小、隐藏层数、dropout概率 
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
                  drop_prob)
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
                  attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)

结果如下

epoch 10, loss 0.454
epoch 20, loss 0.193
epoch 30, loss 0.085
epoch 40, loss 0.059
epoch 50, loss 0.022

2.4 预测不定长的序列

预测不定长序列的方法包括贪婪搜索、穷举搜索和束搜索。

贪婪搜索(Greedy Search):
贪婪搜索是一种简单直观的方法,每次在当前时间步选择预测概率最高的单词作为输出,然后将该单词作为下一个时间步的输入继续预测,直到遇到结束标记或达到最大长度。

穷举搜索(Exhaustive Search):
穷举搜索是一种完备的搜索方法,它会遍历所有可能的序列组合,然后选择具有最高概率的序列作为最终的输出。这种方法虽然理论上完美,但计算成本非常高,在实际应用中几乎不可行。

束搜索(Beam Search):
束搜索是一种介于贪婪搜索和穷举搜索之间的方法。它保留一定数量(称为束宽,beam width)的备选输出序列,每步根据预测概率筛选出概率最高的几个候选序列,然后继续扩展这些候选序列。束搜索通常能在较低的计算成本下找到接近最优的解决方案。

这里我们实现最简单的贪婪搜索。

def translate(encoder, decoder, input_seq, max_seq_len):
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]])  # batch=1
    enc_state = encoder.begin_state()
    enc_output, enc_state = encoder(enc_input, enc_state)
    dec_input = torch.tensor([out_vocab.stoi[BOS]])
    dec_state = decoder.begin_state(enc_state)
    output_tokens = []
    for _ in range(max_seq_len):
        dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
        pred = dec_output.argmax(dim=1)
        pred_token = out_vocab.itos[int(pred.item())]
        if pred_token == EOS:  # 当任一时间步搜索出EOS时,输出序列即完成
            break
        else:
            output_tokens.append(pred_token)
            dec_input = pred
    return output_tokens

简单测试一下模型。输入法语句子“ils regardent.”,翻译后的英语句子应该是“they are watching.”。

input_seq = 'ils regardent .'  # 定义输入序列 0
translate(encoder, decoder, input_seq, max_seq_len)

结果如下

['they', 'are', 'watching', '.']

2.5 评价翻译结果

评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

具体来说,设词数为n的子序列的精度为p_n。它是预测序列与标签序列匹配词数为 n n n的子序列的数量与预测序列中词数为 n n n的子序列的数量之比。举个例子,假设标签序列为A、B、C、D、E、F,预测序列为A、B、B、C、D,那么p_1 = 4/5, p_2 = 3/4, p_3 = 1/3, p_4 = 0。


l e n label 和 l e n pred len_{\text{label}}和len_{\text{pred}} lenlabellenpred
分别为标签序列和预测序列的词数,那么,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{len_{\text{label}}}{len_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n} exp(min(0,1lenpredlenlabel))n=1kpn1/2n
其中 k k k是我们希望匹配的子序列的最大词数。可以看到当预测序列和标签序列完全一致时,BLEU为1。

因为匹配较长子序列比匹配较短子序列更难,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))  
      
    # 遍历从1到k的所有n-gram  
    for n in range(1, k + 1):  
        # 初始化n-gram匹配数和标签n-gram的计数  
        num_matches, label_subs = 0, collections.defaultdict(int)  
          
        # 遍历标签序列的所有n-gram  
        for i in range(len_label - n + 1):  
            # 将每个n-gram加入字典计数  
            label_subs[''.join(label_tokens[i: i + n])] += 1  
          
        # 遍历预测序列的所有n-gram  
        for i in range(len_pred - n + 1):  
            # 如果预测序列的n-gram在标签序列中出现过  
            if label_subs[''.join(pred_tokens[i: i + n])] > 0:  
                # 增加匹配数并减少标签n-gram的计数  
                num_matches += 1  
                label_subs[''.join(pred_tokens[i: i + n])] -= 1  
          
        # 计算n-gram的BLEU得分部分,并累乘到总分中  
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))  
      
    # 返回BLEU得分  
    return score

接下来,定义一个辅助打印函数。

def score(input_seq, label_seq, k):  
    # 使用预训练的encoder和decoder模型对输入序列进行翻译  
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)  
  
    # 将标签序列分割成token列表  
    label_tokens = label_seq.split(' ')  
  
    # 计算BLEU分数并打印结果,同时打印预测的token序列  
    print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),  
                                      ' '.join(pred_tokens)))

预测正确则分数为1。

score('ils regardent .', 'they are watching .', k=2)

结果如下

bleu 1.000, predict: they are watching .
score('ils sont canadienne .', 'they are canadian .', k=2)

结果如下

bleu 0.658, predict: they are russian .

三、基于Transformer和PyTorch深度学习库来实现的日中机器翻译模型

3.1 导入相关的库

首先,在开始实验项目前,我们需要确保系统中已安装以下软件包,如果发现缺少某些软件包,可以使用conda终端控制台输入相关命令进行安装。

import math  # 导入math库,用于数学运算
import torch  # 导入PyTorch库
import torch.nn as nn  # 导入PyTorch的神经网络模块
from torch import Tensor  # 导入Tensor类型
from torch.nn.utils.rnn import pad_sequence  # 导入序列填充函数
from torch.utils.data import DataLoader  # 导入数据加载器
from collections import Counter  # 导入计数器模块
from torchtext.vocab import Vocab  # 导入词汇表类
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer  # 导入Transformer模型相关类
import io  # 导入IO操作库
import time  # 导入时间库
import pandas as pd  # 导入pandas库,用于数据处理
import numpy as np  # 导入NumPy库,用于数值计算
import pickle  # 导入pickle库,用于对象序列化
import tqdm  # 导入进度条库,用于显示进度条
import sentencepiece as spm  # 导入句子分割库,用于文本处理

torch.manual_seed(0)  # 设置PyTorch随机种子,以确保可复现性
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 检测是否有GPU,若有则使用GPU,否则使用CPU

# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码

3.2 获取并行数据集

我们将使用从 JParaCrawl [[http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl]] 下载的日英并行数据集。它主要是通过抓取网络并自动对齐平行句子而创建的。

df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainen = df[2].values.tolist()  #[:10000]
trainja = df[3].values.tolist()  #[:10000]
# trainen.pop(5972)
# trainja.pop(5972)

在导入所有日语及其英语对应句子后,我们需要删除了数据集中的最后一个数据,因为它有一个缺失值。trainen 和 trainja 中的句子总数为 5,973,071 个,不过,出于学习目的,通常建议在一次性使用所有数据之前,先对数据进行采样,确保一切正常,以节省时间。

下面是数据集中的一个句子示例。

print(trainen[500])
print(trainja[500])

我们也可以使用不同的并行数据集来跟进本文,只需确保我们能将数据处理成如上所示的两个字符串列表,其中包含日语和英语句子。

3.3 准备分词器

与英语或其他字母语言不同,日语句子不包含分隔单词的空格。我们可以使用 JParaCrawl 提供的分词器,该分词器是使用 SentencePiece 创建的,适用于日语和英语,我们可以访问 JParaCrawl 网站下载。

en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')  # 加载英文的SentencePiece模型 
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.ja.nopretok.model')  # 加载日文的SentencePiece模型 

加载分词器后,可以执行下面的代码对其进行测试。

en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')

3.4 构建 TorchText 词汇表对象并将句子转换为 Torch 张量

然后,我们使用分词器和原始句子构建从 TorchText 导入的词汇表对象。根据数据集的大小和计算能力,这个过程可能需要几秒或几分钟。不同的分词器也会影响构建词汇表所需的时间。

def build_vocab(sentences, tokenizer):  
    """  
    构建词汇表。  
      
    :param sentences: 句子列表,用于提取词汇。  
    :param tokenizer: 用于分词的tokenizer对象。  
    :return: 构建的词汇表对象。  
    """  
    counter = Counter()  # 创建一个计数器对象,用于统计词频  
    for sentence in sentences:  # 遍历句子列表  
        # 使用tokenizer对句子进行编码,并更新计数器  
        # out_type=str表示输出字符串类型的编码结果  
        counter.update(tokenizer.encode(sentence, out_type=str))  
    # 返回根据计数器构建的词汇表,同时添加四个特殊标记:未知词标记、填充标记、句子开始标记和句子结束标记。  
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])  
  
# 使用日文训练数据和日文tokenizer构建日文词汇表  
ja_vocab = build_vocab(trainja, ja_tokenizer)  
  
# 使用英文训练数据和英文tokenizer构建英文词汇表  
en_vocab = build_vocab(trainen, en_tokenizer)

获得词汇表对象后,我们就可以使用词汇表和标记符对象为训练数据构建张量。

def data_process(ja, en):  
    """  
    处理日文和英文句子对,将其转换为tensor并组成数据列表。  
  
    :param ja: 日文句子列表  
    :param en: 英文句子列表  
    :return: 处理后的数据列表,每个元素是一个元组(ja_tensor_, en_tensor_)  
    """  
    data = []  
    for (raw_ja, raw_en) in zip(ja, en):  
        # 去除日文句子末尾的换行符,并进行编码  
        ja_tokens = ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)  
        # 将编码后的token转换为对应的ID,并转换为torch.long类型的tensor  
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokens], dtype=torch.long)  
  
        # 去除英文句子末尾的换行符,并进行编码  
        en_tokens = en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)  
        # 将编码后的token转换为对应的ID,并转换为torch.long类型的tensor  
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokens], dtype=torch.long)  
  
        # 将处理好的tensor对添加到数据列表中  
        data.append((ja_tensor_, en_tensor_))  
  
    # 返回处理后的数据列表  
    return data  
  
# 使用data_process函数处理日文和英文训练数据  
train_data = data_process(trainja, trainen)

3.5 创建数据加载器(DataLoader)在训练过程中进行迭代

在这里,我们将 BATCH_SIZE 设置为 16,以防止出现 "cuda 内存不足 "的情况,但这取决于多种因素,如机器内存容量、数据大小等,因此我们应该根据自己的需要更改批处理大小。

# 设定批次大小  
BATCH_SIZE = 8  
# 获取<pad>(填充)标记的索引  
PAD_IDX = ja_vocab['<pad>']  
# 获取<bos>(句子开始)标记的索引  
BOS_IDX = ja_vocab['<bos>']  
# 获取<eos>(句子结束)标记的索引  
EOS_IDX = ja_vocab['<eos>']  
  
# 生成一个批次的数据的函数  
def generate_batch(data_batch):  
  # 初始化日语和英语的数据批次列表  
  ja_batch, en_batch = [], []  
  # 遍历数据批次中的每一项(日语和英语的配对)  
  for (ja_item, en_item) in data_batch:  
    # 为日语项添加<bos>和<eos>标记,并连接成新的tensor  
    ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))  
    # 为英语项添加<bos>和<eos>标记,并连接成新的tensor  
    en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))  
  # 使用pad_sequence函数,将日语批次中的tensor填充到相同长度,填充值为PAD_IDX  
  ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)  
  # 使用pad_sequence函数,将英语批次中的tensor填充到相同长度,填充值为PAD_IDX  
  en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)  
  # 返回处理后的日语和英语批次  
  return ja_batch, en_batch  
  
# 使用DataLoader来加载训练数据,设定批次大小为BATCH_SIZE,并启用随机打乱,以及自定义的批次生成函数generate_batch  
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,  
                        shuffle=True, collate_fn=generate_batch)

3.6 Seq2Seq Transformer

Transformer 是用于解决机器翻译任务的 Seq2Seq 模型。转换器模型由编码器和解码器模块组成,每个模块包含固定层数。

编码器通过一系列多头注意和前馈网络层传播输入序列,从而处理输入序列。编码器的输出被称为内存,与目标张量一起输入解码器。编码器和解码器采用强制教学技术,以端到端的方式进行训练。

from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)


class Seq2SeqTransformer(nn.Module):
    def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
                 emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
                 dim_feedforward:int = 512, dropout:float = 0.1):
        super(Seq2SeqTransformer, self).__init__()
        
        # 编码器层  
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=nhead,  
                                                dim_feedforward=dim_feedforward, dropout=dropout)  
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)  
          
        # 解码器层  
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=nhead,  
                                                dim_feedforward=dim_feedforward, dropout=dropout)  
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)  
  
        # 生成器,用于将解码器的输出转换为目标词汇表大小的向量  
        self.generator = nn.Linear(emb_size, tgt_vocab_size)  
          
        # 源语言和目标语言的词嵌入层  
        self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)  
        self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)  
          
        # 位置编码层  
        self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)  

    def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,  
                tgt_mask: Tensor, src_padding_mask: Tensor,  
                tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):  
        """  
        定义模型的前向传播。  
  
        参数:  
        src: 源序列的嵌入Tensor。  
        trg: 目标序列的嵌入Tensor。  
        src_mask: 源序列的掩码Tensor。  
        tgt_mask: 目标序列的掩码Tensor。  
        src_padding_mask: 源序列的填充掩码Tensor。  
        tgt_padding_mask: 目标序列的填充掩码Tensor。  
        memory_key_padding_mask: 记忆键的填充掩码Tensor(在解码器中用于跨注意力机制)。  
  
        返回:  
        生成的目标序列词汇预测logits。  
        """  
        src_emb = self.positional_encoding(self.src_tok_emb(src))  
        tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))  
        memory = self.transformer_encoder(src_emb, src_mask=src_mask, src_key_padding_mask=src_padding_mask)  
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask=tgt_mask, memory_key_padding_mask=memory_key_padding_mask)  
        return self.generator(outs)  
  
    def encode(self, src: Tensor, src_mask: Tensor):  
        """  
        仅对源序列进行编码。  
  
        参数:  
        src: 源序列的嵌入Tensor。  
        src_mask: 源序列的掩码Tensor。  
  
        返回:  
        编码后的记忆(memory)。  
        """  
        src_emb = self.positional_encoding(self.src_tok_emb(src))  
        return self.transformer_encoder(src_emb, src_mask=src_mask, src_key_padding_mask=src.eq(self.src_tok_emb.padding_idx))  
  
    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):  
        """  
        使用给定的记忆(memory)对目标序列进行解码。  
  
        参数:  
        tgt: 目标序列的嵌入Tensor。  
        memory: 编码后的记忆(memory)。  
        tgt_mask: 目标序列的掩码Tensor。  
  
        返回:  
        解码器的输出Tensor。  
        """  
        tgt_emb = self.positional_encoding(self.tgt_tok_emb(tgt))  
        return self.transformer_decoder(tgt_emb, memory, tgt_mask=tgt_mask)  

文本标记通过标记嵌入来表示。标记嵌入中加入了位置编码,以引入词序概念。

# 位置编码类,为模型中的每个位置添加独特的编码  
class PositionalEncoding(nn.Module):  
    def __init__(self, emb_size: int, dropout, maxlen: int = 5000):  
        super(PositionalEncoding, self).__init__()  
        # 计算位置编码的分母部分  
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)  
        # 生成位置序列  
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)  
        # 初始化位置嵌入矩阵  
        pos_embedding = torch.zeros((maxlen, emb_size))  
        # 使用正弦函数计算偶数位置的位置编码  
        pos_embedding[:, 0::2] = torch.sin(pos * den)  
        # 使用余弦函数计算奇数位置的位置编码  
        pos_embedding[:, 1::2] = torch.cos(pos * den)  
        # 增加一个维度以适应模型输入  
        pos_embedding = pos_embedding.unsqueeze(-2)  
  
        # 定义dropout层  
        self.dropout = nn.Dropout(dropout)  
        # 注册位置嵌入作为buffer,不需要进行梯度更新  
        self.register_buffer('pos_embedding', pos_embedding)  
  
    def forward(self, token_embedding: Tensor):  
        # 将位置嵌入加到词嵌入上,并应用dropout  
        return self.dropout(token_embedding +  
                            self.pos_embedding[:token_embedding.size(0),:])# 词嵌入类,将单词转换为固定维度的向量  
class TokenEmbedding(nn.Module):  
    def __init__(self, vocab_size: int, emb_size):  
        super(TokenEmbedding, self).__init__()  
        # 定义词嵌入层  
        self.embedding = nn.Embedding(vocab_size, emb_size)  
        self.emb_size = emb_size  
      
    def forward(self, tokens: Tensor):  
        # 根据单词索引获取词嵌入,并乘以嵌入维度的平方根进行缩放  
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

我们创建一个后续词掩码,以阻止目标词关注其后续词。我们再创建掩码,用于掩码源填充标记和目标填充标记。

# 生成一个用于掩盖序列中后续位置的方形掩码  
def generate_square_subsequent_mask(sz):  
    # 创建一个sz x sz的全1矩阵,并取其上三角部分(包括对角线)  
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)  
    # 将掩码转换为浮点数类型,并将0值替换为负无穷(用于softmax时掩盖这些位置)  
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))  
    return mask  
  
# 根据源序列和目标序列创建掩码  
def create_mask(src, tgt):  
    # 获取源序列和目标序列的长度  
    src_seq_len = src.shape[0]  
    tgt_seq_len = tgt.shape[0]  
  
    # 生成目标序列的方形后续掩码  
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)  
  
    # 源序列的掩码通常不需要(因为Transformer在编码时不应看到未来的信息),这里设为全False  
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)  
  
    # 创建源序列的填充掩码,用于标记填充的token位置  
    src_padding_mask = (src == PAD_IDX).transpose(0, 1)  
  
    # 创建目标序列的填充掩码,同样用于标记填充的token位置  
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)  
  
    # 返回源掩码、目标掩码、源填充掩码和目标填充掩码  
    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

如果你的电脑有GPU,你可以将NUM_ENCODER_LAYERS 和 NUM_DECODER_LAYERS 设置为3或者更高,NHEAD设置8,EMB_SIZE设置为512。

# 定义源语言(日语)的词汇表大小  
SRC_VOCAB_SIZE = len(ja_vocab)  
# 定义目标语言(英语)的词汇表大小  
TGT_VOCAB_SIZE = len(en_vocab)  
# 嵌入层维度  
EMB_SIZE = 512  
# 注意力机制的头数  
NHEAD = 8  
# 前馈神经网络的隐藏层维度  
FFN_HID_DIM = 512  
# 批处理大小  
BATCH_SIZE = 16  
# 编码器层数  
NUM_ENCODER_LAYERS = 3  
# 解码器层数  
NUM_DECODER_LAYERS = 3  
# 训练周期数  
NUM_EPOCHS = 16  
  
# 实例化Seq2SeqTransformer模型  
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,  
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,  
                                 FFN_HID_DIM)  
  
# 初始化模型参数  
for p in transformer.parameters():  
    # 如果参数的维度大于1(即非偏置项),使用Xavier均匀分布初始化  
    if p.dim() > 1:  
        nn.init.xavier_uniform_(p)# 将模型转移到指定的设备(如GPU)  
transformer = transformer.to(device)  
  
# 定义损失函数,并设置忽略填充索引  
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)  
  
# 定义优化器,使用Adam算法  
optimizer = torch.optim.Adam(  
    transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9  
)  
  
def train_epoch(model, train_iter, optimizer):  
  # 设置模型为训练模式  
    model.train()  
    losses = 0  
  # 遍历训练数据集  
  for idx, (src, tgt) in enumerate(train_iter):  
      # 将数据移动到设备(如GPU)  
      src = src.to(device)  
      tgt = tgt.to(device)  
  
      # 获取目标序列的输入(不包括最后一个token)  
      tgt_input = tgt[:-1, :]  
  
      # 创建掩码  
      src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)  
  
      # 前向传播  
      logits = model(src, tgt_input, src_mask, tgt_mask,  
                              src_padding_mask, tgt_padding_mask, src_padding_mask)  
  
      # 清空之前的梯度  
      optimizer.zero_grad()  
  
      # 获取目标序列的真实输出(从第二个token开始)  
      tgt_out = tgt[1:,:]  
      # 计算损失  
      loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  
      # 反向传播  
      loss.backward()  
  
      # 更新模型参数  
      optimizer.step()  
      # 累加损失  
      losses += loss.item()  
  # 返回平均损失  
  return losses / len(train_iter)  
  
def evaluate(model, val_iter):  
  # 设置模型为评估模式  
    model.eval()  
    losses = 0  
  # 遍历验证数据集  
    for idx, (src, tgt) in enumerate(val_iter):  
    src = src.to(device)  
    tgt = tgt.to(device)  
  
    # 获取目标序列的输入(不包括最后一个token)  
    tgt_input = tgt[:-1, :]  
  
    # 创建掩码  
    src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)  
  
    # 前向传播  
    logits = model(src, tgt_input, src_mask, tgt_mask,  
                              src_padding_mask, tgt_padding_mask, src_padding_mask)  
    # 获取目标序列的真实输出(从第二个token开始)  
    tgt_out = tgt[1:,:]  
    # 计算损失  
    loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  
    # 累加损失  
    losses += loss.item()  
  # 返回平均损失  
  return losses / len(val_iter)

3.7 开始训练

最后,在准备好必要的类和函数后,我们就可以训练模型了。完成训练所需的时间会因计算能力、参数和数据集大小等诸多因素而有很大差异。

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):  # 使用tqdm库显示进度条,遍历训练周期  
  # 记录每个epoch的开始时间  
  start_time = time.time()  
  
  # 调用train_epoch函数进行一个epoch的训练,并获取训练损失  
  train_loss = train_epoch(transformer, train_iter, optimizer)  
  
  # 记录每个epoch的结束时间  
  end_time = time.time()  
  
  # 打印当前epoch的编号、训练损失以及epoch所用时间  
  print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "  
          f"Epoch time = {(end_time - start_time):.3f}s"))  # f-string格式化字符串输出

3.8 尝试使用训练好的模型翻译日语句子

首先,我们创建翻译一个新句子的函数,包括获取日语句子、分词、转换为张量、推理等步骤,然后将结果解码为一个句子,但这次是英语句子。

def greedy_decode(model, src, src_mask, max_len, start_symbol):  
    # 将源数据和掩码移动到指定设备(如GPU)  
    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 = 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):  
    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>", "")

然后,我们只需调用 translate 函数并传递所需的参数即可。

translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)

结果如下

' ▁H S ▁ 代 码 ▁85 15 ▁ 焊 接 设 备 ( 包 括 电 气 加 热 ) 。 '
trainen.pop(5)

结果如下

'Chinese HS Code Harmonized Code System < HS编码 8515 : 电气(包括电热气体)、激光、其他光、光子束、超声波、电子束、磁脉冲或等离子弧焊接机器及装置,不论是否 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'
trainja.pop(5)

结果如下

'Japanese HS Code Harmonized Code System < HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)、レーザーその他の光子ビーム式、超音波式、電子ビーム式、 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'

3.9 保存词汇表对象和训练好的模型

最后,在训练完成后,我们将首先使用 Pickle 保存词汇表对象(en_vocab 和 ja_vocab)。

import pickle
# 打开一个文件,在其中存储数据
file = open('en_vocab.pkl', 'wb')
# 将信息转储到该文件
pickle.dump(en_vocab, file)
file.close()
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()

最后,我们还可以使用 PyTorch 的保存和加载函数保存模型,以便以后使用。一般来说,有两种保存模型的方法,这取决于我们以后想把它们用于什么用途。第一种只用于推理,我们可以在以后加载模型,并用它将日语翻译成英语。

# 保存模型以便推理
torch.save(transformer.state_dict(), 'inference_model')

第二种也是为了推理,但也是为了以后我们想加载模型并恢复训练时使用。

# 保存模型 + 检查点,以便以后继续训练
torch.save({
  'epoch': NUM_EPOCHS,
  'model_state_dict': transformer.state_dict(),
  'optimizer_state_dict': optimizer.state_dict(),
  'loss': train_loss,
  }, 'model_checkpoint.tar')

四、总结

Transformer模型在机器翻译领域有几个显著的优势:

1.长距离依赖处理能力:传统的循环神经网络(RNNs)在处理长距离依赖时存在梯度消失和梯度爆炸的问题,而Transformer通过自注意力机制(Self-Attention)能够直接捕捉输入序列中各个位置的依赖关系,因此能够更有效地处理长距离的依赖关系。
2.并行计算能力:传统的RNNs和基于RNNs的序列到序列模型(如Seq2Seq模型)在训练时需要按顺序处理序列,而Transformer可以通过自注意力机制并行计算所有位置的表示,使得在训练和推理时都能够大幅度加速计算速度,尤其是对于长序列的处理更为明显。
3.学习全局语义信息:Transformer中的自注意力机制允许模型在生成每个输出位置时动态地关注输入序列的不同部分,从而能够更全面地理解和利用全局的语义信息。这使得模型能够在翻译过程中更准确地捕捉句子之间的复杂关系,提高翻译质量。
4.可扩展性:Transformer模型的结构相对清晰简单,由多个重复的Encoder和Decoder层构成,每一层又由多头注意力和前馈神经网络组成。这种模块化的设计使得Transformer模型非常适合在不同规模的数据集上进行扩展和应用,并且便于进行各种变体和优化。
5.适应多语言处理:由于Transformer模型的自注意力机制和分层结构,它在处理不同语言对之间的翻译时表现出色。它能够处理不同语言的语法结构和词汇差异,同时通过预训练和微调等技术,可以进一步提高其在特定语言对上的性能。

PyTorch 的保存和加载函数保存模型,以便以后使用。一般来说,有两种保存模型的方法,这取决于我们以后想把它们用于什么用途。第一种只用于推理,我们可以在以后加载模型,并用它将日语翻译成英语。

# 保存模型以便推理
torch.save(transformer.state_dict(), 'inference_model')

第二种也是为了推理,但也是为了以后我们想加载模型并恢复训练时使用。

# 保存模型 + 检查点,以便以后继续训练
torch.save({
  'epoch': NUM_EPOCHS,
  'model_state_dict': transformer.state_dict(),
  'optimizer_state_dict': optimizer.state_dict(),
  'loss': train_loss,
  }, 'model_checkpoint.tar')

四、总结

Transformer模型在机器翻译领域有几个显著的优势:

1.长距离依赖处理能力:传统的循环神经网络(RNNs)在处理长距离依赖时存在梯度消失和梯度爆炸的问题,而Transformer通过自注意力机制(Self-Attention)能够直接捕捉输入序列中各个位置的依赖关系,因此能够更有效地处理长距离的依赖关系。
2.并行计算能力:传统的RNNs和基于RNNs的序列到序列模型(如Seq2Seq模型)在训练时需要按顺序处理序列,而Transformer可以通过自注意力机制并行计算所有位置的表示,使得在训练和推理时都能够大幅度加速计算速度,尤其是对于长序列的处理更为明显。
3.学习全局语义信息:Transformer中的自注意力机制允许模型在生成每个输出位置时动态地关注输入序列的不同部分,从而能够更全面地理解和利用全局的语义信息。这使得模型能够在翻译过程中更准确地捕捉句子之间的复杂关系,提高翻译质量。
4.可扩展性:Transformer模型的结构相对清晰简单,由多个重复的Encoder和Decoder层构成,每一层又由多头注意力和前馈神经网络组成。这种模块化的设计使得Transformer模型非常适合在不同规模的数据集上进行扩展和应用,并且便于进行各种变体和优化。
5.适应多语言处理:由于Transformer模型的自注意力机制和分层结构,它在处理不同语言对之间的翻译时表现出色。它能够处理不同语言的语法结构和词汇差异,同时通过预训练和微调等技术,可以进一步提高其在特定语言对上的性能。

总的来说,Transformer模型作为一种革命性的神经网络架构,在机器翻译领域带来了显著的性能提升,尤其是在处理长距离依赖、提高并行计算能力和学习全局语义信息等方面具有明显优势。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值