机器翻译技术与实践:从编码器-解码器模型到日中翻译系统实现

目录

一、机器翻译

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

2.1 编码器

2.2 注意力机制

​2.3 含注意力机制的解码器

三、数据准备

3.1 数据读取

四、模型训练流程

4.1 训练环境搭建

4.2 模型参数配置

五、不定长序列生成

5.1 贪婪搜索

5.2 序列生成效果评估

六、翻译结果评价

6.1 BLEU分数计算

七、日中机器翻译模型实现

7.1 环境配置与依赖库介绍

7.2 平行语料的获取与处理

7.3 分词器的选择与使用

7.4 构建词汇表并转换句子为张量

7.5 训练数据加载器的搭建

7.6 序列到序列Transformer模型的构建

7.7 模型训练与调试

7.8 使用模型进行翻译尝试

7.9 模型与词汇表的保存

一、机器翻译

机器翻译技术能够实现将文本内容从一种语言自动转换为另一种语言,它在不同语言间的文本长度差异性上尤为关键。正是由于这种长度的不一致性,使得机器翻译成为展示编码器-解码器架构以及注意力机制应用的理想场景。

通过结合编码器-解码器架构和注意力机制,机器翻译模型能够更好地处理跨语言文本转换中的复杂性,提供更加准确和自然的语言翻译结果。

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

编码器-解码器架构是一种常用于机器翻译的神经网络模型,它包含两个主要部分:编码器和解码器。编码器负责读取源语言文本,并将其转换成一个固定大小的向量,这个向量包含了原始文本的信息。解码器则负责将这个向量转换成目标语言的文本,即便目标文本的长度可能与原文本不同。

注意力机制是编码器-解码器架构中的一个重要组成部分,它允许解码器在生成每个输出词时,能够“关注”编码器生成的向量中的不同部分。这样,解码器就可以更加灵活地处理不同长度的输入和输出序列,从而提高翻译的准确性和流畅性。

2.1 编码器

在编码器的实现中,我们首先通过词嵌入层将输入语言中的词转换为向量表示,然后将这些向量作为输入传递给一个多层门控循环单元。在我们之前介绍的6.5节(有关循环神经网络的简洁实现)中提到,PyTorch中的nn.GRU模块在执行前向计算后,会返回两个部分:输出和多层隐藏状态的最终时间步。这里的“输出”指的是在每个时间步中,最后一层隐藏层的隐藏状态。这些隐藏状态被用作注意力机制中的键和值。

# 定义 Encoder 类,它继承自 torch.nn.Module
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)
        # 定义一个门控循环单元(GRU),用于处理序列数据
        # num_hiddens 是隐藏层的大小
        # num_layers 是堆叠的层数
        # dropout 是在GRU层之间使用的dropout概率
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
    # 前向传播函数
    def forward(self, inputs, state):
        # 输入形状是 (批量大小, 时间步数)
        # 使用 embedding 层将输入的词索引转换为词向量
        embedding = self.embedding(inputs.long()).permute(1, 0, 2) # 转换形状为 (时间步数, 批量大小, 输入尺寸)
        # 将嵌入后的序列和上一个状态传递给 GRU 单元进行前向传播
        return self.rnn(embedding, state)
    # 定义如何初始化状态
    def begin_state(self):
        # GRU 可以有初始隐藏状态,这里我们选择不使用初始隐藏状态,返回 None
        return None

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

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)

2.2 注意力机制

我们将实现第10.11节中描述的函数 \alpha,该函数利用一个包含单个隐藏层的多层感知机(MLP)对输入进行变换。具体来说:

1、输入将是解码器的当前隐藏状态和编码器在所有时间步上的隐藏状态的拼接。
2、 隐藏层的输入维度是编码器隐藏状态的维度加上解码器隐藏状态的维度。
3、使用双曲正切函数(tanh)作为激活函数。
4、输出层的输出数量为1,表示最终的注意力权重。
5、在MLP的定义中,两个Linear层都不使用偏置。
6、向量 \nu 的长度由一个超参数控制,即注意力机制的大小(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

注意力机制涉及三个主要的输入组成部分:查询(query)、键(key)和值(value)。假设编码器和解码器拥有相同数量的隐藏单元。在这种情况下,查询是由解码器在前一时间步产生的隐藏状态构成的,其格式为(批量大小 × 隐藏单元个数);而键和值则来源于编码器在所有时间步长上生成的隐藏状态,其格式为(时间步数 × 批量大小 × 隐藏单元个数)。执行注意力计算后,机制将输出一个上下文向量,表示为当前时间步的背景信息,其尺寸为(批量大小 × 隐藏单元个数)。

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个隐藏单元。基于此设置,注意力机制将会为每个样本生成一个上下文向量,这些向量综合了编码器在所有时间步上的信息,并且每个向量的长度与编码器隐藏层的大小相匹配。因此,对于批量中的每个样本,我们都会得到一个长度为8的向量,最终的输出形式将是一个包含4个样本的矩阵,其形状为(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


2.3 含注意力机制的解码器

在解码器的初始化阶段,我们采用了一种直接的方法:将编码器在序列处理结束时刻的隐藏状态,作为解码器开始时刻的隐藏状态。这种做法的前提是,编码器和解码器的循环神经网络(RNN)必须具有相同数量的隐藏层,以及在每层中相同数量的隐藏单元。

解码器在进行前向传播时,首先利用之前讨论的注意力机制来计算出当前时间步的上下文向量。解码器接收来自目标语言的输入,这些输入是单词的索引形式。这些索引首先通过词嵌入层转换为相应的词向量表示,然后在特征维度上与计算得到的上下文向量进行拼接。这个拼接的结果,连同解码器上一时间步的隐藏状态,一起输入到门控循环单元(如GRU或LSTM),以计算出当前时间步的输出和新的隐藏状态。

最终,解码器的输出会通过一个全连接层进行转换,以生成针对输出词汇表中每个可能单词的预测概率分布。这个预测结果的形状为(批量大小 × 输出词典的大小),它表示了在当前时间步,针对批量中每个样本,模型预测下一个单词的概率分布。

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.1 数据读取

我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<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 = '<pad>', '<bos>', '<eos>'
# 设置环境变量CUDA_VISIBLE_DEVICES为"0",这将告诉PyTorch仅使用第一个GPU设备。
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
# 根据是否有可用的CUDA设备,设置device为'cuda'或'cpu'。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 打印PyTorch版本和正在使用的设备。
print(torch.__version__, device)

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

# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 定义一个函数,用于处理单个序列,将其添加到全局的词列表和序列列表中,并进行填充。
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    # 将序列中的词添加到全局词列表中。
    all_tokens.extend(seq_tokens)
    # 在序列的词后面添加一个结束标记EOS,然后根据需要添加足够的填充标记PAD,
    # 以确保序列长度达到max_seq_len。注意,-1是为了在EOS之后添加填充标记。
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    # 将处理后的序列添加到全局序列列表中。
    all_seqs.append(seq_tokens)
# 定义一个函数,用于使用所有词构造词典,并将所有序列中的词转换为词索引,然后构造Tensor。
def build_data(all_tokens, all_seqs):
    # 使用collections.Counter来统计all_tokens中的词频,并使用torchtext.vocab.Vocab构造词典。
    # 特殊词PAD, BOS, EOS会被添加到词典的开始位置。
    vocab = Vocab.Vocab(collections.Counter(all_tokens),
                        specials=[PAD, BOS, EOS])
    # 对于all_seqs中的每个序列,使用vocab.stoi(词到索引的映射)将序列中的每个词转换为对应的词索引。
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
    # 将转换为词索引的序列构造为一个Tensor,以便用于模型训练。
    return vocab, torch.tensor(indices)

为了简化演示过程,我们选择了一个规模较小的法语-英语平行语料库。该数据集中,每一条记录都包含了一句法语及其对应的英语翻译,两者通过制表符('\t')进行分隔。在数据读取阶段,我们对每个句子的末尾添加了结束标记“”,以明确指出句子的终结。此外,为了统一序列长度,我们可能会在较短的句子末尾补充“”标记,直到它们达到预设的最大序列长度max_seq_len。在构建词汇表方面,我们分别针对法语和英语创建了独立的索引系统。这意味着每种语言的词汇都将拥有自己的索引映射,两者之间没有直接的联系。

# 定义一个函数,用于读取数据,并将其处理为模型训练所需的格式。
def read_data(max_seq_len):
    # 初始化四个列表,分别用于存放输入序列的词、输出序列的词、处理后的输入序列和输出序列。
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
    # 使用io.open打开文件'fr-en-small.txt'进行读取。
    with io.open('fr-en-small.txt') as f:
        lines = f.readlines()  # 读取所有行到lines列表。
    # 遍历文件中的每一行。
    for line in lines:
        # 移除每行末尾的换行符,然后按'\t'分割,得到输入序列和输出序列。
        in_seq, out_seq = line.rstrip().split('\t')
        # 分别将输入序列和输出序列按空格分割成词。
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
        # 如果输入序列或输出序列的长度超过max_seq_len减1,则跳过此样本。
        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)
    # 返回输入词典、输出词典和由输入数据和输出数据构成的Tensor数据集。
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

为了便于演示,我们将序列的最大长度设定为7个单位。在这种情况下,每个句子都将被处理成最多7个词的序列,不足的部分将用""填充,如果句子长度超过7,则可能会进行截断处理。

max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]

四、模型训练流程

4.1 训练环境搭建

首先,我们将开发一个名为`batch_loss`的函数,用以计算一个数据批次的损失值。在解码器的初始阶段,它接收到的输入是一个特殊的开始字符,标记为BOS。随后,在解码过程中的每一时间点,解码器的输入是前一时间点输出序列中的最后一个词,这是一种称为"教师强制"的策略。此外,为了确保填充项不会对损失函数的计算产生干扰,我们将采用与第10.3节中word2vec实现相同的掩码技术。

# 定义 batch_loss 函数,用于计算一批数据的损失
def batch_loss(encoder, decoder, X, Y, loss):
    # 获取批量大小
    batch_size = X.shape[0]
    # 初始化编码器的状态
    enc_state = encoder.begin_state()
    # 通过编码器处理输入数据X,获取编码器的输出和最终状态
    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)
    # 初始化掩码变量,用于忽略标签为填充项PAD的损失,初始全1
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0
    # 初始化损失变量
    l = torch.tensor([0.0])
    # 遍历目标数据Y中的每一时间步
    for y in Y.permute(1, 0):  # Y shape: (batch, seq_len)
        # 通过解码器处理输入,获取解码器的输出和新状态
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
        # 计算损失,并将结果累加到总损失l中
        # 使用mask乘以损失,以便忽略PAD标签的损失
        l = l + (mask * loss(dec_output, y)).sum()
        # 使用强制教学,将当前的目标y作为解码器的下一输入
        dec_input = y
        # 更新非填充(非PAD)的token数量
        num_not_pad_tokens += mask.sum().item()
        # 一旦遇到EOS(句子结束符号),将mask设为0,因为之后的token不应计入损失
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    # 返回平均损失,即总损失除以非填充token的数量
    return l / num_not_pad_tokens

4.2模型参数配置

在训练过程中,我们需要同步更新编码器和解码器的模型参数。这意味着在每次迭代中,我们都要对这两个组件的参数进行调整。

# 定义 train 函数,用于训练编码器和解码器模型
def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    # 创建编码器的优化器
    enc_optimizer = optim.Adam(encoder.parameters(), lr=lr)
    # 创建解码器的优化器
    dec_optimizer = optim.Adam(decoder.parameters(), lr=lr)
    # 定义损失函数,这里使用交叉熵损失,设置reduction='none'以获取每个样本的损失值
    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()
        # 每10轮输出一次训练进度
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

接下来,我们将初始化模型的实例,并配置所需的超参数。完成这些设置后,便可以开始模型的训练过程。

import torch
import torch.optim as optim
import torch.nn as nn
import torch.utils.data as Data
# 设置模型参数
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 类接受与 Encoder 类相似的参数,以及额外的注意力大小参数
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
                  attention_size, drop_prob)
# 定义 train 函数,用于训练编码器和解码器模型
def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    # 创建编码器的优化器
    enc_optimizer = optim.Adam(encoder.parameters(), lr=lr)
    # 创建解码器的优化器
    dec_optimizer = optim.Adam(decoder.parameters(), lr=lr)
    # 定义损失函数,这里使用交叉熵损失,设置reduction='none'以获取每个样本的损失值
    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()
        # 每10轮输出一次训练进度
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

# 调用训练函数
train(encoder, decoder, dataset, lr, batch_size, num_epochs)

五、不定长序列生成

5.1  贪婪搜索

在第10.10节中,我们探讨了三种不同的方法来生成解码器在每个时间步的输出。现在,我们将实现这三种方法中最基本的一种,即贪婪搜索。贪婪搜索是一种选择当前最优解的策略,它在每一步都选择概率最高的输出,以期望整个序列的累积概率最大化。这种方法简单且计算效率高。

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

5.2 序列生成效果评估

让我们进行一个基础的模型性能测试。输入的法语短句是“ils regardent.”,我们期望模型将其翻译成英语,并且翻译结果应该是“they are watching.”。这将帮助我们验证模型的翻译准确性。

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

六、翻译结果评价

机器翻译的质量通常通过BLEU(Bilingual Evaluation Understudy)评分来衡量。BLEU评分关注预测序列中是否存在与参考翻译(标签序列)相匹配的任何子序列。

具体计算方式是:考虑预测序列中长度为n的子序列,计算其精度p_{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。

BLEU评分的公式是:
BLEU= exp\left ( min\left ( 0,1-\frac{len_{label}}{len_{pred}}\right ) \right )\prod_{n=1}^{k}\left ( p_{n} \right )^{\frac{1}{2}}
其中,len_{label}len_{pred}分别是参考翻译和预测翻译的词数,k是考虑的最大子序列长度。当预测翻译与参考翻译完全相同时,BLEU得分为1。

BLEU评分给予较长子序列的匹配更高的权重,因为匹配更长的子序列通常更具挑战性。例如,如果p_{n}保持在0.5,随着n的增加,\left ( p_{n} \right )^{\frac{1}{2}}的值会逐渐减小,因为平方根的增长速度慢于线性增长。此外,如果预测翻译比参考翻译短,公式中的指数项将作为惩罚因子,减少BLEU得分。例如,如果k为2,参考翻译是"A, B, C, D, E, F",而预测翻译是"A, B",尽管p_{1}p_{2}都是1,但由于惩罚因子\exp \left ( 1-\frac{6}2{} \right )接近0.14,BLEU得分也会相应降低。

6.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))
    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)))

预测正确则分数为1。

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

score('ils sont canadienne .', 'they are canadian .', k=2)

七、日中机器翻译模型实现

在本教程中,我们将利用Jupyter Notebook、PyTorch、Torchtext以及SentencePiece工具来创建一个基础的机器翻译系统。

7.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,请在你自己的电脑上尝试运行这一套代码

device

7.2 平行语料的获取与处理

在本教程中,我们将使用从JParaCrawl下载的日英平行语料库。JParaCrawl被描述为“由NTT创建的最大的公开可用的英日平行语料库。它是通过大规模网络爬取和自动对齐平行句子创建的。” 你也可以在这里查看相关论文。JParaCrawl

# 读取数据文件
df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 提取数据框中的第三列并转换为列表,第三列假设是英文句子
trainen = df[2].values.tolist()  # 可以通过将注释去掉来限制数据量,例如:[:10000]
# 提取数据框中的第四列并转换为列表,第四列假设是日文句子
trainja = df[3].values.tolist()  # 可以通过将注释去掉来限制数据量,例如:[:10000]
# 如果存在某些需要剔除的索引(例如第5972行),可以使用pop方法移除
# trainen.pop(5972)
# trainja.pop(5972)

在完成对日语及其英文翻译的导入之后,我移除了数据集中的最后一条记录,因为它缺少某些数据。总计,trainen和trainja两个文件包含的句子数量达到了5,973,071。但是,为了学习效率,通常建议在开始全面使用所有数据之前,先对数据进行抽样检验,确保系统运行正常,这样可以有效地减少时间消耗。

以下是数据集中的一个句子实例。

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

我们也可以使用不同的平行语料库来跟随本文的步骤,只要确保我们能将数据处理成上述的两个字符串列表,分别包含日语和英语句子即可。

7.3 分词器的选择与使用

与英语或其他使用字母的语言不同,日语句子中不包含空格来分隔单词。我们可以使用JParaCrawl提供的分词器,这些分词器是使用SentencePiece为日语和英语创建的。你可以访问JParaCrawl网站来下载它们,或者点击这里。(http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl)。

# 'enja_spm_models/spm.en.nopretok.model' 是训练好的 SentencePiece 模型文件路径
en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')

# 加载 SentencePiece 模型,用于日语分词
# 'enja_spm_models/spm.ja.nopretok.model' 是训练好的 SentencePiece 模型文件路径
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')

分词器加载完成后,你可以通过执行以下代码来测试它们。

import sentencepiece as spm

# 加载 SentencePiece 模型
sp_model = "spm.en.nopretok.model"  
sp = spm.SentencePieceProcessor()
sp.load(sp_model)

# 对句子进行编码
sentence = "All residents aged 20 to 59 years who live in Japan must enroll in public pension system."
encoded_pieces = sp.encode_as_pieces(sentence)

# 输出编码后的标记片段
print(encoded_pieces)

import sentencepiece as spm

# 加载 SentencePiece 模型
sp_model = "spm.ja.nopretok.model"  
sp = spm.SentencePieceProcessor()
sp.load(sp_model)

sentence = "年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。"
encoded_pieces = sp.encode_as_pieces(sentence)

# 输出编码后的标记片段
print(encoded_pieces)

7.4 构建词汇表并转换句子为张量

使用分词器处理原始文本后,我们将使用从TorchText库导入的Vocab类来创建词汇表。这个过程的持续时间会根据数据集的规模和你的计算资源而有所不同,可能需要几秒到几分钟。不同的分词器对构建词汇表的效率也有影响。我测试了几种日语分词器,发现SentencePiece不仅效果上佳,而且在速度上也满足了我的需求。

from collections import Counter
from torchtext.vocab import vocab
import sentencepiece as spm

# 定义构建词汇表的函数
def build_vocab(sentences, tokenizer):
    # 创建一个计数器对象,用于统计词频
    counter = Counter()
    # 遍历每个句子
    for sentence in sentences:
        # 使用分词器对句子进行分词,并更新计数器
        counter.update(tokenizer.encode_as_pieces(sentence))
    # 构建词汇表对象
    vocab_obj = vocab(counter)
    # 手动添加特殊标记
    specials = ['<unk>', '<pad>', '<bos>', '<eos>']
    for special in specials:
        vocab_obj.insert_token(special, len(vocab_obj))
    vocab_obj.set_default_index(vocab_obj['<unk>'])
    return vocab_obj

# 加载训练的日语和英语分词器
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')  # 替换为日语模型路径
en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')  # 替换为英语模型路径

# 使用训练的日语句子和日语分词器构建日语词汇表
trainja = ["これは日本語の文です。", "もう一つの文です。"]  # 替换为实际的日语训练数据
ja_vocab = build_vocab(trainja, ja_tokenizer)

# 使用训练的英语句子和英语分词器构建英语词汇表
trainen = ["This is an English sentence.", "Another sentence here."]  # 替换为实际的英语训练数据
en_vocab = build_vocab(trainen, en_tokenizer)

一旦我们获得了词汇表对象,我们就可以利用这个对象和分词器来将训练数据转换成张量。

# 定义数据处理函数
def data_process(ja, en):
    data = []  # 创建一个空列表用于存储处理后的数据
    # 使用 zip 函数将日语和英语句子配对
    for (raw_ja, raw_en) in zip(ja, en):
        # 对日语句子进行分词,并将分词后的标记映射到词汇表索引,再转换为张量
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode_as_pieces(raw_ja.rstrip("\n"))],
                                  dtype=torch.long)
        # 对英语句子进行分词,并将分词后的标记映射到词汇表索引,再转换为张量
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode_as_pieces(raw_en.rstrip("\n"))],
                                  dtype=torch.long)
        # 将处理后的张量对添加到数据列表中
        data.append((ja_tensor_, en_tensor_))
    # 返回处理后的数据
    return data

# 使用训练数据处理函数,处理训练数据集
train_data = data_process(trainja, trainen)

7.5 训练数据加载器的搭建

在此,我将批量大小(BATCH_SIZE)设定为16,以防止出现“cuda内存不足”的错误。然而,这个设置会根据你的机器内存大小、数据集的规模等不同因素而有所变化,因此你可以根据你的具体情况自由调整批量大小。例如,在PyTorch的教程中,使用Multi30k德英数据集时,批量大小被设置为128。

# 设置批处理大小
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 函数将批处理中的句子填充到相同长度
    ja_batch = pad_sequence(ja_batch, padding_value=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)

7.6 序列到序列Transformer模型的构建

接下来的几段代码和文本解释摘自原始的 PyTorch 教程,我仅对其中的 BATCH_SIZE 以及原本的词汇表 de_vocab 做了修改,将其更改为 ja_vocab,除此之外并未做其他改动。

变换器(Transformer)是一种用于处理序列到序列(Seq2Seq)任务的模型,它最初是“Attention is all you need”这篇论文中提出的,主要用于解决机器翻译问题。Transformer 模型由编码器和解码器两部分组成,每部分都由一定数量的层构成。编码器利用多头注意力机制和前馈神经网络层来处理输入的序列。编码器处理后得到的输出被称作“记忆”(memory),这个记忆会与目标序列一起被送入解码器。在训练过程中,编码器和解码器是通过一种称为“教师强制”(teacher forcing)的技术来进行端到端的训练。

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


# 定义序列到序列的 Transformer 模型
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)

        # 定义生成器线性层,将 Transformer 解码器的输出转换为目标词汇表大小的概率分布
        self.generator = nn.Linear(emb_size, tgt_vocab_size)
        
        # 定义源语言的 Token 嵌入层
        self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
        
        # 定义目标语言的 Token 嵌入层
        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)
        
        # 通过解码器层处理目标语言的嵌入和编码器输出的 memory
        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)

文本中的每个标记通过标记嵌入(token embeddings)来表示。为了在模型中体现单词的顺序信息,我们在这些标记嵌入中加入了位置编码(positional encoding)。位置编码是一种向量,它能够提供序列中每个单词的位置信息,帮助模型理解单词在序列中的相对位置,从而捕捉到词序的依赖关系。这种编码方式是Transformer模型的关键特性之一,它使得模型能够处理序列数据,即使这些数据在输入时是被平铺成一维的形式。

# 定义位置编码类
class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout: float, 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):
        # 将位置编码加到 token 嵌入上,并应用 dropout
        return self.dropout(token_embedding +
                            self.pos_embedding[:token_embedding.size(0), :])

# 定义 token 嵌入类
class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size: int):
        super(TokenEmbedding, self).__init__()
        # 定义嵌入层
        self.embedding = nn.Embedding(vocab_size, emb_size)
        # 保存嵌入大小
        self.emb_size = emb_size

    def forward(self, tokens: Tensor):
        # 返回 token 的嵌入,并乘以嵌入大小的平方根
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

我们构建了一个后续词掩码(subsequent word mask),目的是防止目标序列中的每个词注意到它后面的词。这是通过在解码过程中实现的,确保解码器在生成每个词时,只能看到它之前已经生成的词,而不能"窥视"未来的词。此外,我们还创建了掩码来屏蔽源语言和目标语言中的填充标记(padding tokens)。填充标记通常用于将不同长度的序列统一填充到相同的长度,但它们本身不携带任何实际的语言信息。因此,在训练和推理过程中,我们需要屏蔽这些填充标记,以避免模型对它们进行不必要的处理。

这些掩码在Transformer模型中起着至关重要的作用,确保了模型能够正确地处理序列数据,并提高了模型的效率和准确性。

# 定义生成下三角掩码的函数
def generate_square_subsequent_mask(sz):
    # 创建一个上三角矩阵,上三角部分为 1,其余部分为 0
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    # 将掩码转换为浮点类型,并将 0 填充为负无穷大,将 1 填充为 0
    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)
    
    # 为源序列生成全零掩码
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

    # 为源序列和目标序列生成填充掩码,填充位置为 True,其余位置为 False
    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

在定义模型参数并实例化模型时,当前服务器的计算能力有限,按照给定的配置可以进行训练,但效果可能不理想。为了获得更好的训练效果,建议使用配备GPU的个人电脑来运行代码。具体地,当使用个人GPU时,应将编码器层数 NUM_ENCODER_LAYERS 和解码器层数 NUM_DECODER_LAYERS 设置为3或更高,多头注意力机制的头数 NHEAD 设置为8,词嵌入维度 EMB_SIZE 设置为512。

import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.vocab import vocab
from collections import Counter
import sentencepiece as spm
# 设置模型和训练的超参数
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  # 训练的轮数

# 初始化 Seq2Seq Transformer 模型
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)

# 将模型移动到设备(如 GPU)
transformer = transformer.to(device)

# 定义损失函数,忽略填充索引
loss_fn = nn.CrossEntropyLoss(ignore_index=PAD_IDX)

# 定义优化器
optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)

# 定义训练一个 epoch 的函数
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(val_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)  # 返回平均损失

7.7 模型训练与调试

在一切准备就绪之后,我们就可以开始训练我们的模型了。需要明确的是,训练过程所需时间的长短将受到多种因素的影响,包括计算资源的性能、所选用的参数配置以及数据集的规模等。

以下是执行训练的代码示例

# 训练模型
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS + 1)):
    start_time = time.time()  # 记录当前时间作为起始时间
    
    # 调用 train_epoch 函数进行模型训练,并返回训练损失
    train_loss = train_epoch(transformer, train_iter, optimizer)
    
    end_time = time.time()  # 记录当前时间作为结束时间
    
    # 打印当前 epoch 的损失和耗时
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
           f"Epoch time = {(end_time - start_time):.3f}s"))

7.8 使用模型进行翻译尝试

首先,我们开发一个函数来处理新句子的翻译工作。这个函数的工作流程包括:抓取日语文本、执行分词处理、将文本转换成张量形式、进行翻译推理,最后将得到的输出转换为英文句子。

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>", "")

随后,我们只需调用翻译函数并传入必要的参数即可。

# 定义训练数据
trainen = ["This is an English sentence.", "Another sentence here."]

# 确保索引在范围内,然后移除指定索引的元素
index_to_remove = 5
if index_to_remove < len(trainen):
    trainen.pop(index_to_remove)
else:
    print(f"Index {index_to_remove} is out of range for the list.")

print(trainen)

# 定义训练数据
trainja = ["これは日本語の文です。", "もう一つの文です。"]

# 确保索引在范围内,然后移除指定索引的元素
index_to_remove = 5
if index_to_remove < len(trainja):
    trainja.pop(index_to_remove)
else:
    print(f"Index {index_to_remove} is out of range for the list.")

print(trainja)

7.9 模型与词汇表的保存

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

import pickle
# 打开一个文件,准备将数据存储到该文件中
file = open('en_vocab.pkl', 'wb')  # 以写入二进制模式打开文件 'en_vocab.pkl'
# 将英语词汇表对象序列化并存储到文件中
pickle.dump(en_vocab, file)  # 使用 pickle.dump 函数将 en_vocab 对象写入文件
file.close()  # 关闭文件
# 打开另一个文件,准备将数据存储到该文件中
file = open('ja_vocab.pkl', 'wb')  # 以写入二进制模式打开文件 'ja_vocab.pkl'
# 将日语词汇表对象序列化并存储到文件中
pickle.dump(ja_vocab, file)  # 使用 pickle.dump 函数将 ja_vocab 对象写入文件
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')  # 保存为 'model_checkpoint.tar' 文件

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值