当前位置:   article > 正文

NLP学习笔记——基于注意力机制的机器翻译

NLP学习笔记——基于注意力机制的机器翻译

NLP学习笔记——基于注意力机制的机器翻译

一、什么是机器翻译

机器翻译,作为自然语言处理的一个核心领域,一直都是研究者们关注的焦点。其目标是实现计算机自动将一种语言翻译成另一种语言,而不需要人类的参与。

机器翻译(MT)是一种自动将源语言文本翻译成目标语言的技术。它使用特定的算法和模型,尝试在不同语言之间实现最佳的语义映射。

二、具体实现

2.1 读取和预处理数据

首先,我们需要定义一些特殊符号:

<pad>:用于填充较短的序列,使每个序列等长。

<bos>:序列的开始符号。

<eos>:序列的结束符号。

接下来,我们导入必要的库并进行设置:

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"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

print(torch.__version__, device)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
2.1.1 辅助函数

接着定义两个辅助函数对后面读取的数据进行预处理

# 将一个序列中所有的词记录在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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
2.1.2 读取数据

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

def read_data(max_seq_len):
    # in和out分别是input和output的缩写
    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')
        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,则忽略掉此样本
        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)
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]
  • 1
  • 2
  • 3

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

含注意力机制的编码器-解码器模型在神经机器翻译(NMT)中得到了广泛应用。注意力机制的引入极大地改善了机器翻译模型的性能。

2.2.1 编码器

编码器的主要功能是将输入序列转换为一个固定长度的上下文向量(Context Vector),这个向量包含了输入序列中的所有信息。编码器通常由一系列的神经网络层组成,比如RNN(循环神经网络)、LSTM(长短期记忆网络)或GRU(门控循环单元)。

编码器的工作流程:

1.接受输入序列(如一个句子的单词序列)。

2.通过神经网络层逐步处理输入,提取特征。

3.生成上下文向量或一系列隐状态(Hidden States),这些隐状态将被传递给解码器。

在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。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)

    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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

下面我们来创建一个批量大小为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)
  • 1
  • 2
  • 3
2.2.2 什么是注意力机制

在传统的编码器-解码器架构中,编码器将输入序列压缩成一个固定大小的上下文向量,该向量被传递给解码器。然而,这种方法在处理长句子时效果并不理想,因为固定大小的上下文向量无法有效地捕捉输入序列中的所有信息。注意力机制解决了这个问题。

注意力机制的核心思想是,在生成输出序列的每一个词时,解码器不再仅仅依赖于固定的上下文向量,而是动态地关注输入序列的不同部分。通过计算注意力权重,解码器可以选择性地关注输入序列中的相关部分,从而生成更精确的翻译。

我们将实现一个函数:将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。两个Linear实例均不使用偏差。其中函数 a a a定义里向量 v \boldsymbol{v} v的长度是一个超参数,即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
  • 1
  • 2
  • 3
  • 4
  • 5

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

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)  # 返回背景变量
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

seq_len, batch_size, num_hiddens = 10, 4, 8 # 设置序列长度、批量大小和隐藏单元数量
model = attention_model(2*num_hiddens, 10) # 初始化注意力模型,其中第一个参数是2倍隐藏单元数,第二个参数是某个超参数
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 # 计算注意力模型的前向传播,并获取其输出的形状
  • 1
  • 2
  • 3
  • 4
  • 5

结果输出

torch.Size([4, 8])
  • 1
2.2.3 解码器

解码器的任务是从编码器生成的上下文向量中生成目标序列。传统的解码器与编码器相似,通常由RNN、LSTM、GRU等组成。解码器在每一步都生成一个输出,并将这个输出作为下一步的输入,直到生成整个序列。

2.2.4 含注意力机制的解码器

注意力机制(Attention Mechanism)是在解码过程中引入的一种方法,它使解码器在生成每一个输出时能够动态地“关注”输入序列的不同部分。这样,模型可以在生成每个输出词时,更加灵活地利用输入序列中的信息。

含注意力机制的解码器工作流程:

1.解码器在每一步生成一个输出时,首先计算当前输出与编码器所有隐状态之间的相关性(注意力权重)。

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
  • 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

2.3 模型训练

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

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    # 定义编码器和解码器的优化器,使用Adam优化算法,学习率为lr
    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)))
  • 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

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

# 定义编码器和解码器的参数
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

# 初始化编码器
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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

模型训练后得到输出结果为

epoch 10, loss 0.459
epoch 20, loss 0.165
epoch 30, loss 0.078
epoch 40, loss 0.036
epoch 50, loss 0.010
  • 1
  • 2
  • 3
  • 4
  • 5

2.4 预测不定长序列

在生成解码器在每个时间步的输出时,我们有三种方法可供选择,分别是贪心搜索(Greedy Search)、束搜索(Beam Search)和抽样(Sampling)。

2.4.1 贪心搜索

贪心搜索是一种简单直接的方法,它在每个时间步选择概率最高的单词作为输出。

优点:

1.简单易实现。

2.计算速度快,因为每一步只选择一个最优单词。

缺点:

可能错过全局最优解,因为每一步都只考虑当前最优,可能导致局部最优。

过程:

1.在每个时间步,计算所有可能单词的概率分布。

2.选择概率最高的单词作为当前时间步的输出。

3.将这个单词作为下一时间步的输入,重复上述过程,直到生成结束符或达到最大长度。

2.4.2 束搜索

束搜索是一种更复杂的方法,它在每个时间步保留多个(通常是k个)最有可能的候选序列,从而在一定程度上平衡了计算复杂性和生成质量。

优点:

1.能够更好地找到全局最优解,因为它考虑了多个候选序列。

2.通常生成的序列质量比贪心搜索高。

缺点:

1.计算复杂度较高,因为需要同时跟踪多个候选序列。

2.束宽度(beam width)选择不当可能导致性能下降。

过程:

1.初始化k个候选序列,每个序列只包含起始符。

2.在每个时间步,对每个候选序列,计算所有可能单词的概率分布,并扩展成新的候选序列。

3.保留前k个概率最高的候选序列,作为下一时间步的输入。

4.重复上述过程,直到生成结束符或达到最大长度。

2.4.3 抽样

抽样方法是在每个时间步根据概率分布随机选择一个单词,而不是选择概率最高的单词。常见的抽样方法包括随机抽样、温度抽样和Top-k抽样等。

优点:

1.能够生成更具多样性的序列,适用于需要创意和多样性的生成任务。

2.可以避免重复和过于固定的生成结果。

缺点:

1.序列质量可能不如贪心搜索和束搜索,因为随机性可能导致不合理的选择。

2.生成结果不确定,可能需要多次尝试。

过程:

1.在每个时间步,计算所有可能单词的概率分布。

2.根据概率分布随机选择一个单词作为当前时间步的输出。

3.将这个单词作为下一时间步的输入,重复上述过程,直到生成结束符或达到最大长度。

2.4.4 具体实现

在这里,我们使用最简单的贪心搜索

def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列按空格分词
    in_tokens = input_seq.split(' ')
    # 在输入序列末尾添加EOS标记,并用PAD标记补齐到最大序列长度
    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)
    
    # 初始化解码器输入为BOS标记
    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())]
        # 如果预测标记为EOS,停止生成
        if pred_token == EOS:  # 当任一时间步搜索出EOS时,输出序列即完成
            break
        else:
            # 否则将预测标记添加到输出标记列表中
            output_tokens.append(pred_token)
            dec_input = pred
    return output_tokens
  • 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

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

input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)
  • 1
  • 2

结果输出

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

2.5 评估翻译结果

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

2.5.1 BLEU

Bilingual Evaluation Understudy(简称BLEU)是一种用于评估机器翻译质量的指标。它通过比较机器翻译生成的译文与一个或多个参考译文来评估翻译的准确性和流畅性。BLEU的评分机制主要基于n-gram的精确度,即生成译文中n-gram与参考译文中n-gram的匹配情况。

计算步骤

1.n-gram匹配

BLEU评分首先计算生成译文和参考译文之间的n-gram匹配度。n-gram是指连续n个词语的序列,常用的n值有1到4,即1-gram(单词)、2-gram(双词)、3-gram(三词)和4-gram(四词)

2.精确度:

精确度衡量的是生成译文中有多少n-gram在参考译文中出现。具体计算方法如下:
P r e c i s i o n n = 生成译文中匹配的 n − g r a m 数量 生成译文中的 n − g r a m 总数量 Precision_n=\frac{生成译文中匹配的n-gram数量}{生成译文中的n-gram总数量} Precisionn=生成译文中的ngram总数量生成译文中匹配的ngram数量
3.惩罚机制:

为了避免生成短而准确的译文获得高分,BLEU引入了惩罚机制来处理生成译文过短的情况。惩罚机制的定义如下:
B P = { 1 , c > r e ( 1 − r c ) , c ≤ r BP =

{1,c>re(1rc),cr
BP={1,c>re(1cr),cr
其中,c是生成译文的长度,r是参考译文的长度。

4.BLEU评分:

最终的BLEU评分通过计算各个n-gram精确度的几何平均值,并结合惩罚机制得到:
B L E U = B P ⋅ e x p ( ∑ n = 1 N w n l o g P r e c i s i o n n ) BLEU=BP\cdot exp(\sum_{n=1}^N w_nlogPrecision_n) BLEU=BPexp(n=1NwnlogPrecisionn)
其中, w n w_n wn是权重,通常设置为均等权重,即 w n = 1 N w_n=\frac{1}{N} wn=N1(N为最大n-gram的长度)

BLEU的优缺点

优点:

1.简单直观,易于实现。

2.自动化程度高,可以快速评估大规模数据集。

3.广泛使用,便于比较不同模型的性能。

缺点:

1.过于依赖n-gram的精确匹配,忽略了语义信息。

2.对词序和小的词汇变化敏感,可能导致评分不准确。

3.不适合评估需要较高语言理解的任务,如对话系统的回答质量。

2.5.2 BLEU的实现
def bleu(pred_tokens, label_tokens, k):
    # 获取预测序列和标签序列的长度
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    # 计算BLEU得分中的brevity penalty(简短惩罚)部分
    score = math.exp(min(0, 1 - len_label / len_pred))
    # 计算n-gram匹配度得分
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        # 统计标签序列中的n-gram
        for i in range(len_label - n + 1):
            label_subs[''.join(label_tokens[i: i + n])] += 1
        # 统计预测序列中的n-gram匹配数量
        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
        # 将n-gram匹配度得分乘以得分
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

def score(input_seq, label_seq, k):
    # 使用translate函数生成预测序列
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    # 将标签序列按空格分词
    label_tokens = label_seq.split(' ')
    # 计算预测序列的BLEU得分并打印
    print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
                                      ' '.join(pred_tokens)))
   
score('ils regardent .', 'they are watching .', k=2)
score('ils sont canadienne .', 'they are canadian .', k=2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

运行结果为

bleu 1.000, predict: they are watching .
bleu 0.658, predict: they are actors .
  • 1
  • 2

三、基于Transformer实现日译中机器翻译

3.1 导入相关软件包

首先,我们确保在系统中安装了以下软件包,如果发现缺少某些软件包,请确保进行安装。

import math
import torchtext
import torch
import torch.nn as nn
from torch import 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
import io
import time
import pandas as pd
import numpy as np
import pickle
import tqdm
import sentencepiece as spm
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3.2 获取数据集

我们将使用从 JParaCrawl 下载的日英并行数据集[http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl]它被描述为“NTT创建的最大的公开可用的英日平行语料库。它是通过大量抓取网络并自动对齐平行句子而创建的。

# 读取以制表符分隔的文本文件,没有列头
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 提取第3列(英语/其他语言句子)并转换为列表
trainen = df[2].values.tolist()#[:10000]
# 提取第4列(日语句子)并转换为列表
trainja = df[3].values.tolist()#[:10000]
# trainen.pop(5972)
# trainja.pop(5972)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

# 打印第500个英语/其他语言句子
print(trainen[500])
# 打印第500个日语句子
print(trainja[500])
  • 1
  • 2
  • 3
  • 4

运行结果为

Chinese HS Code Harmonized Code System < HS编码 2905 无环醇及其卤化、磺化、硝化或亚硝化衍生物 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
Japanese HS Code Harmonized Code System < HSコード 2905 非環式アルコール並びにそのハロゲン化誘導体、スルホン化誘導体、ニトロ化誘導体及びニトロソ化誘導体 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
  • 1
  • 2

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

3.3 准备标记符号

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

# 加载英文SentencePiece模型
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')
  • 1
  • 2
  • 3
  • 4

加载标记化器后,可以对其进行测试,例如执行下面的代码。

# 使用英文SentencePiece模型对句子进行编码
en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
# 使用日文SentencePiece模型对句子进行编码
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
  • 1
  • 2
  • 3
  • 4

测试结果为

['▁All',
 '▁residents',
 '▁aged',
 '▁20',
 '▁to',
 '▁59',
 '▁years',
 '▁who',
 '▁live',
 '▁in',
 '▁Japan',
 '▁must',
 '▁enroll',
 '▁in',
 '▁public',
 '▁pension',
 '▁system',
 '.']
 ['▁',
 '年',
 '金',
 '▁日本',
 'に住んでいる',
 '20',
 '歳',
 '~',
 '60',
 '歳の',
 '全ての',
 '人は',
 '、',
 '公的',
 '年',
 '金',
 '制度',
 'に',
 '加入',
 'しなければなりません',
 '。']
  • 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

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

3.4.1 TorchText

'torchtext’中的词汇表(vocab)是一个用于管理文本数据中的词汇及其相关信息的类。它用于将文本数据中的词语映射到整数索引,这些索引可以作为输入传递给神经网络模型。在处理自然语言处理(NLP)任务时,词汇表是非常重要的,因为它可以将词语转换为可以被模型处理的数值表示。

主要功能

1.词语到索引的映射torchtext.vocab.Vocab类创建了词语到整数索引的映射。这种映射允许我们将文本数据中的词语转换为数值表示,方便输入到模型中进行处理。

2.索引到词语的映射: 词汇表还提供了从整数索引到词语的反向映射,方便在模型生成输出后将数值结果转换回文本形式。

3.词频统计: 词汇表可以记录词语在数据集中出现的频率,帮助我们了解哪些词语是常见的,哪些词语是罕见的。这对于后续的文本处理和模型训练非常有用。

4.特殊符号管理: 词汇表可以管理特殊符号,例如用于表示未知词语()、填充词语()以及序列的起始和结束符号(和)。这些特殊符号在文本处理和模型训练中具有特殊意义。

3.4.2 具体实现

我们使用标记符号和原始句子构建从 TorchText 导入的 Vocab 对象。根据数据集的大小和计算能力,这个过程可能需要几秒或几分钟。不同的标记符号化器也会影响构建词汇表所需的时间,我尝试过其他几种日语标记符号化器,但 SentencePiece 对我来说似乎效果不错,速度也够快。

def build_vocab(sentences, tokenizer):
    # 创建一个Counter对象来统计词频
  counter = Counter()
  for sentence in sentences:
        # 使用tokenizer对句子进行编码,并更新词频计数器
    counter.update(tokenizer.encode(sentence, out_type=str))
    # 返回包含特殊符号(<unk>, <pad>, <bos>, <eos>)的词汇表
  return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.5 创建数据加载器对象,以便在训练过程中进行迭代

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

# 定义批量大小
BATCH_SIZE = 8

# 获取特殊符号的索引
PAD_IDX = ja_vocab['<pad>']
BOS_IDX = ja_vocab['<bos>']
EOS_IDX = ja_vocab['<eos>']

def generate_batch(data_batch):
  ja_batch, en_batch = [], []
  for (ja_item, en_item) in data_batch:
    # 在日语句子的开头和结尾添加BOS和EOS标记,并添加到批量列表
    ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
    # 在英语句子的开头和结尾添加BOS和EOS标记,并添加到批量列表
    en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
  # 使用pad_sequence对日语句子进行填充,填充值为PAD_IDX
  ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
  # 使用pad_sequence对英语句子进行填充,填充值为PAD_IDX
  en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
  # 返回填充后的批量数据
  return ja_batch, en_batch
# 创建训练数据迭代器
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

3.6 序列到序列的Transformer

以下是这些代码和文本解释的中文翻译:


接下来的代码和文字解释(以斜体书写)摘自PyTorch教程[https://pytorch.org/tutorials/beginner/translation_transformer.html]。除了BATCH_SIZE和ja_vocab这个词以外,我没有做任何改变。

Transformer 是在“Attention is all you need”论文中提出的用于解决机器翻译任务的序列到序列(Seq2Seq)模型。Transformer 模型由一个编码器和一个解码器块组成,每个块包含固定数量的层。

编码器通过多头注意力机制和前馈网络层的一系列传播处理输入序列。编码器的输出称为记忆(memory),它与目标张量一起输入到解码器。编码器和解码器使用教师强制技术进行端到端训练。

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__()
        # 定义Transformer编码器层
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        # 创建Transformer编码器
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        # 定义Transformer解码器层
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        # 创建Transformer解码器
        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_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_padding_mask)
        # 通过编码器处理源语言嵌入
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)
        return self.generator(outs)

    # 编码函数
    def encode(self, src: Tensor, src_mask: Tensor):
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)

    # 解码函数
    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)
  • 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
  • 49
  • 50
  • 51
  • 52

文本标记通过使用标记嵌入来表示。位置编码被添加到标记嵌入中,以引入词序的概念。

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))
        # 根据公式计算位置编码的sin和cos值
        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)
        # 将位置编码矩阵注册为模型的缓冲区
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        # 将位置编码矩阵与输入的token_embedding相加,并应用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):
        # 返回词嵌入向量,并乘以math.sqrt(self.emb_size)进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
  • 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

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

def generate_square_subsequent_mask(sz):
    # 创建一个上三角矩阵
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    # 将上三角矩阵转换为float类型,并对mask中的元素进行填充
    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]

  # 生成目标语言序列的mask
  tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
  # 创建一个全零张量作为源语言序列的mask
  src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

  # 创建源语言和目标语言的填充mask
  src_padding_mask = (src == PAD_IDX).transpose(0, 1)
  tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
  # 返回源语言mask、目标语言mask、源语言填充mask和目标语言填充mask
  return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.7 定义模型参数并将模型实例化。

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)

# 使用 Xavier 初始化方法初始化模型参数
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

# 将模型移动到合适的设备
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):
      src = src.to(device)
      tgt = tgt.to(device)

      # 准备目标输入,移除最后一个标记(用于预测)
      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()

      # 准备目标输出,从第二个标记开始
      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(valid_iter)):
    src = src.to(device)
    tgt = tgt.to(device)

    # 准备目标输入,移除最后一个标记(用于预测)
    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)
    # 准备目标输出,从第二个标记开始
    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)
  • 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
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

3.8 开始训练

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

以下是代码:

# 使用 tqdm 库显示进度条
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
  start_time = time.time() # 记录开始时间
  train_loss = train_epoch(transformer, train_iter, optimizer) # 训练一个 epoch 并获取训练损失
  end_time = time.time() # 记录结束时间
  # 打印当前 epoch 的训练损失和所用时间
  print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
          f"Epoch time = {(end_time - start_time):.3f}s"))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

首先,我们创建用于翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理,然后将结果解码回一个句子,这次是用英语。

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

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

# 翻译日文句子
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
trainen.pop(5) # 移除并返回列表 'trainen' 中索引为 5 的元素
trainja.pop(5) # 移除并返回列表 'trainja' 中索引为 5 的元素
  • 1
  • 2
  • 3
  • 4

测试结果为

'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 ...'
'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 ...'
  • 1
  • 2

3.10 保存词汇表对象和训练过的模型

最后,在训练结束后,我们将首先使用 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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

# 保存模型用于推理
torch.save(transformer.state_dict(), 'inference_model')
  • 1
  • 2

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

# 保存模型和检查点,以便以后恢复训练
torch.save({
  'epoch': NUM_EPOCHS, # 当前的 epoch 数
  'model_state_dict': transformer.state_dict(), # 模型的状态字典,包含所有参数
  'optimizer_state_dict': optimizer.state_dict(), # 优化器的状态字典,包含优化器参数
  'loss': train_loss, # 当前的训练损失
  }, 'model_checkpoint.tar')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

四、总结

本文详细介绍了基于注意力机制的机器翻译模型的实现过程,该模型在自然语言处理领域具有重要的应用价值。通过引入自注意力机制,该模型克服了传统序列模型在处理长距离依赖时的局限性。本文的主要内容包括:

机器翻译概述:首先,本文介绍了机器翻译的基本概念及其在自然语言处理中的重要性。机器翻译的目标是实现计算机自动将一种语言翻译成另一种语言,而无需人类干预。

具体实现:本文详细讲解了如何通过代码实现机器翻译模型,包括数据读取和预处理、模型训练及评估等过程。在数据预处理中,本文介绍了如何定义特殊符号并使用辅助函数对数据进行处理。

代码示例:本文提供了大量的代码示例,详细说明了模型的实现细节和各个步骤。通过使用PyTorch框架,读者可以逐步实现基于注意力机制的机器翻译模型。

通过以上步骤,读者可以了解到基于注意力机制的机器翻译模型的基本原理和实现方法,并能够利用该模型进行实际的自然语言处理任务。

基于注意力机制的机器翻译模型自提出以来,在自然语言处理领域取得了显著的成果。其高效的自注意力机制和并行计算能力,使得该模型在处理大规模数据和复杂任务时表现出色。本文通过详细的代码实现和讲解,帮助读者深入理解了该模型的工作原理和应用场景。未来,随着技术的不断发展,基于注意力机制的机器翻译模型及其衍生模型将在更多领域展现出强大的潜力。希望本文能为读者在学习和应用此类模型方面提供有益的参考和帮助

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/795753
推荐阅读
相关标签
  

闽ICP备14008679号