NLP—机器翻译

目录

一.机器翻译的定义

二.基于attention的机器翻译代码实现

1.读取和预处理数据

2.含注意力机制的编码器—解码器的实现

(1)编码器

(2)注意力机制

​编辑(3)含注意力机制的解码器

3. 训练模型 

4.预测不定长的序列

​编辑 5.评价翻译结果

三.基于self-attention的机器翻译代码实现

1.导入所需的软件包 

2.获取平行数据集

3.准备分词器

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

5.创建用于训练期间迭代的数据加载器对象。

6.实现Sequence-to-sequence Transformer

7.训练模型

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


一.机器翻译的定义

  机器翻译是指将一段文本从一种语言自动翻译到另一种语言。

二.基于attention的机器翻译代码实现

1.读取和预处理数据

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

import collections  # 导入collections模块,提供了特殊的容器数据类型,如OrderedDict等。
import os  # 导入os模块,用于与操作系统进行交互。
import io  # 导入io模块,提供了对文件和流的基本操作。
import math  # 导入math模块,提供了数学运算相关的函数。
import torch  # 导入PyTorch深度学习框架的核心库。
from torch import nn  # 从torch中导入nn模块,用于构建神经网络模型。
import torch.nn.functional as F  # 导入torch.nn.functional,包含了各种神经网络的函数接口。
import torchtext.vocab as Vocab  # 导入torchtext.vocab模块,用于处理文本的词汇表。
import torch.utils.data as Data  # 导入torch.utils.data模块,用于处理数据集。

import sys  # 导入sys模块,提供对Python解释器的访问。
# sys.path.append("..")  # (可选)将上级目录添加到sys.path中,使得可以导入上级目录中的模块。
import d2lzh_pytorch as d2l  # 导入d2lzh_pytorch模块,这是一个封装了Deep Learning相关工具和方法的库。

# 定义常量,用于在序列处理中表示填充符、序列起始符和序列结束符。
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'

# 设置环境变量,指定使用哪块GPU进行计算。这里设置为使用第一个GPU(编号为0)。
os.environ["CUDA_VISIBLE_DEVICES"] = "0"

# 检查当前是否支持CUDA(即GPU加速),并根据结果选择在CPU还是GPU上进行计算。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 打印PyTorch的版本信息和当前选择的计算设备。
print(torch.__version__, device)

     函数 process_one_seq 将一个序列中的所有词记录到 all_tokens 中,方便后续构建词典。同时,它在该序列末尾添加特殊符号 EOS 和 PAD,使得序列长度达到 max_seq_len,然后将处理后的序列保存在 all_seqs 中;函数 build_data 使用 all_tokens 构造一个词典 vocab,然后将 all_seqs 中每个词变换为该词在词典中的索引,并将这些索引构造成一个 Tensor。

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

# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
    # 使用collections.Counter计算每个词的频率,并基于此构造词典。
    # 值得注意的是,这里指定了特别的词汇:PAD、BOS和EOS。
    vocab = Vocab.Vocab(collections.Counter(all_tokens),
                        specials=[PAD, BOS, EOS])
    
    # 将所有序列中的每个词替换为它在词典中的索引,得到一个索引序列的列表
    indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
    
    # 将索引序列列表转换为PyTorch的Tensor类型,用于后续的模型输入
    return vocab, torch.tensor(indices)

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

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')  # 将每行按制表符'\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)  # 构建输入序列的词典并转换为Tensor数据
    out_vocab, out_data = build_data(out_tokens, out_seqs)  # 构建输出序列的词典并转换为Tensor数据
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

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

结果输出如下:

2.含注意力机制的编码器—解码器的实现
(1)编码器

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

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循环神经网络层

    def forward(self, inputs, state):
        # 前向计算函数,描述了数据在模型中的传播过程
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        embedding = self.embedding(inputs.long()).permute(1, 0, 2)  # 对输入进行词嵌入并进行维度变换
        return self.rnn(embedding, state)  # 返回GRU层的输出结果以及更新后的状态

    def begin_state(self):
        # 初始化RNN的隐藏状态
        return None

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计算背景变量。首先,函数𝑎𝑎根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。 

 

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

def attention_model(input_size, attention_size):
    # 定义一个简单的注意力机制模块
    model = nn.Sequential(
        nn.Linear(input_size, attention_size, bias=False),  # 输入线性变换,将输入维度变换到注意力维度,没有偏置
        nn.Tanh(),  # 使用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

输出结果如下: 

(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
3. 训练模型 

  我们先实现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

  在训练函数中,我们需要同时迭代编码器和解码器的模型参数。我们定义了一个训练函数,用于训练编码器和解码器模型。它使用Adam优化器进行参数更新,并在每个epoch结束时打印平均损失值,以监控训练过程。 

def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    # 创建优化器,用于更新编码器和解码器的参数,使用Adam优化算法
    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)
    
    # 训练循环,遍历每个epoch
    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个epoch打印一次平均损失
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

  创建模型实例并设置超参数,并开始训练模型。

# 定义模型和训练参数
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)
"""
Encoder初始化参数:
- len(in_vocab): 输入词汇表的大小,用于嵌入层的输入维度
- embed_size: 嵌入层的维度
- num_hiddens: LSTM隐藏单元的数量
- num_layers: LSTM的层数
- drop_prob: Dropout的概率
"""

# 初始化解码器
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers, attention_size, drop_prob)
"""
Decoder初始化参数:
- len(out_vocab): 输出词汇表的大小,用于嵌入层的输入维度
- embed_size: 嵌入层的维度
- num_hiddens: LSTM隐藏单元的数量
- num_layers: LSTM的层数
- attention_size: 注意力机制的维度
- drop_prob: Dropout的概率
"""

# 开始训练模型
train(encoder, decoder, dataset, lr, batch_size, num_epochs)
"""
train函数的参数:
- encoder: 编码器模型
- decoder: 解码器模型
- dataset: 训练数据集
- lr: 学习率
- batch_size: 批处理大小
- num_epochs: 训练轮数
"""

训练结果如下:

4.预测不定长的序列

  我们实现用贪婪搜索来生成解码器在每个时间步的输出。

  首先来介绍贪婪搜索的实现原理。对于输出序列任一时间步𝑡′,我们从所有词中搜索出条件概率最大的词作为输出。一旦搜索出"<eos>"符号,或者输出序列长度已经达到了最大长度𝑇′,便完成输出。

  我们将该条件概率最大的输出序列称为最优输出序列。而贪婪搜索的主要问题是不能保证得到最优输出序列。

  下面来看一个例子。假设输出词典里面有“A”“B”“C”和“<eos>”这4个词。下图中每个时间步下的4个数字分别代表了该时间步生成“A”“B”“C”和“<eos>”这4个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词。因此,图中将生成输出序列“A”“B”“C”“<eos>”。该输出序列的条件概率是0.5×0.4×0.4×0.6=0.0480.5×0.4×0.4×0.6=0.048。

  

 接下来,观察下图演示的例子。与上图中不同,下图在时间步2中选取了条件概率第二大的词“C”。由于时间步3所基于的时间步1和2的输出子序列由上图中的“A”“B”变为了下图中的“A”“C”,下图中时间步3生成各个词的条件概率发生了变化。我们选取条件概率最大的词“B”。此时时间步4所基于的前3个时间步的输出子序列为“A”“C”“B”,与上图中的“A”“B”“C”不同。因此,下图中时间步4生成各个词的条件概率也与上图中的不同。我们发现,此时的输出序列“A”“C”“B”“<eos>”的条件概率是0.5×0.3×0.6×0.6=0.054,大于贪婪搜索得到的输出序列的条件概率。因此,贪婪搜索得到的输出序列“A”“B”“C”“<eos>”并非最优输出序列。

  接下来给出实现代码,下面代码定义了一个用于将输入序列翻译成目标语言的函数。函数首先对输入序列进行编码,然后使用解码器生成目标语言的单词序列,直到遇到EOS(结束标记)或达到最大序列长度为止。最后,返回生成的翻译结果单词列表。简单测试一下模型。输入法语句子“ils regardent.”,翻译后的英语句子应该是“they are watching.”。

def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列分割为单词,并添加EOS(结束标记)和PAD(填充标记)以及对应的填充
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    
    # 将输入序列转换为索引张量并添加一个维度表示batch,这里假设batch大小为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:
            break
        else:
            output_tokens.append(pred_token)  # 将预测的单词添加到输出序列
            dec_input = pred  # 更新解码器的输入为当前预测的单词索引
    
    return output_tokens  # 返回生成的翻译结果单词列表

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

输出结果如下:

 5.评价翻译结果

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

  具体来说,设词数为𝑛的子序列的精度为𝑝𝑛。它是预测序列与标签序列匹配词数为𝑛的子序列的数量与预测序列中词数为𝑛的子序列的数量之比。因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。

  下面来实现BLEU的计算,我们定义了一个计算BLEU分数的函数,用于评估机器翻译结果与参考翻译之间的相似度。在该函数中,pred_tokens 是预测的翻译结果,label_tokens 是参考翻译,k 是n-gram的最大长度。具体地,我们会计算从1-gram到k-gram的匹配情况,并结合长度惩罚项最终得到BLEU评分。

def bleu(pred_tokens, label_tokens, k):
    # 获取预测序列和参考序列的长度
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    
    # 计算长度惩罚项:如果预测序列较短,则惩罚得分
    # 长度惩罚项: exp(min(0, 1 - len_label / len_pred))
    score = math.exp(min(0, 1 - len_label / len_pred))
    
    # 遍历从1到k的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个数并更新参考序列中的n-gram计数
        for i in range(len_pred - n + 1):
            n_gram = ''.join(pred_tokens[i: i + n])
            if label_subs[n_gram] > 0:
                num_matches += 1
                label_subs[n_gram] -= 1
        
        # 计算每一个n-gram匹配的分数并累乘到总得分
        # math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) 用于计算加权的n-gram精度
        precision_n = num_matches / (len_pred - n + 1)
        score *= math.pow(precision_n, math.pow(0.5, n))
    
    return score  # 返回最终的BLEU分数

#定义一个辅助打印函数
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)))

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

输出结果如下: 

  

三.基于self-attention的机器翻译代码实现

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

  在本教程中,我们将使用从 JParaCrawl 下载的日英平行数据集![JParaCrawl] 该数据集被描述为“由 NTT 创建的最大公开可用的英日平行语料库。它主要通过网络爬取并自动对齐平行句子。” 

df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainen = df[2].values.tolist()#[:10000]  # 从第三列(索引为2)获取英语数据并转换为列表
trainja = df[3].values.tolist()#[:10000]  # 从第四列(索引为3)获取日语数据并转换为列表
# trainen.pop(5972)  # 注释掉删除索引为5972的英语数据的行
# trainja.pop(5972)  # 注释掉删除索引为5972的日语数据的行

  在导入所有日语和对应的英语句子后,我删除了数据集中最后一条数据,因为它有一个缺失值。总共有 5,973,071 条句子在 trainen 和 trainja 中。然而,出于学习目的,通常建议先对数据进行采样,并确保一切按预期工作,然后再一次性使用所有数据,以节省时间。以下是数据集中包含的句子的示例。我们也可以使用不同的平行数据集来跟随本文,只需确保我们可以将数据处理成上面显示的两个字符串列表,其中包含日语和英语句子。

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

运行结果如下:

3.准备分词器

  与英语或其他字母语言不同,日语句子中没有空格来分隔单词。我们可以使用 JParaCrawl 提供的分词器,这些分词器是使用 SentencePiece 为日语和英语创建的。你可以访问 JParaCrawl 网站下载它们。 

# 导入 SentencePieceProcessor 类,用于加载分词模型
en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
# 使用指定的模型文件 'spm.en.nopretok.model' 加载用于英文句子的分词器

ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.ja.nopretok.model')
# 使用指定的模型文件 'spm.ja.nopretok.model' 加载用于日文句子的分词器

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

en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')
# 使用 en_tokenizer 对给定的英文句子进行编码
# encode() 方法将句子转换为对应的编码表示
# 参数 out_type='str' 指定输出类型为字符串格式
4.构建 TorchText 词汇对象并将句子转换为 Torch 张量

  使用分词器和原始句子,我们接着构建从 TorchText 导入的 Vocab 对象。这个过程可能需要几秒钟或几分钟,具体取决于数据集的大小和计算能力。不同的分词器也会影响构建词汇表所需的时间。我尝试了几种其他的日语分词器,但 SentencePiece 对我来说效果很好并且速度足够快。 

  

def build_vocab(sentences, tokenizer):
    # 定义一个函数 build_vocab,用于构建词汇对象
    # 参数 sentences 是一个包含句子的列表
    # 参数 tokenizer 是用于对句子进行分词的分词器

    counter = Counter()
    # 创建一个 Counter 对象,用于统计词频

    for sentence in sentences:
        # 遍历每个句子
        counter.update(tokenizer.encode(sentence, out_type=str))
        # 使用分词器对句子进行编码,输出类型为字符串
        # 并将编码后的词语更新到 Counter 中

    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
    # 使用 Counter 中统计的词频构建 Vocab 对象,
    # 同时指定一些特殊标记词,如 '<unk>'(未知词)、'<pad>'(填充词)、'<bos>'(句子开头)和 '<eos>'(句子结尾)
    # 最终返回构建好的 Vocab 对象

# 使用训练集的日语句子和日语分词器构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)

# 使用训练集的英语句子和英语分词器构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

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

def data_process(ja, en):
    # 定义一个函数 data_process,用于处理数据并构建张量
    # 参数 ja 是包含日语句子的列表
    # 参数 en 是包含英语句子的列表

    data = []
    # 创建一个空列表,用于存储处理后的数据

    for (raw_ja, raw_en) in zip(ja, en):
        # 使用 zip 函数将日语句子和英语句子一一对应地组合在一起进行遍历

        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 使用日语分词器对日语句子进行编码,并根据词汇表将编码后的词语转换为索引
        # 然后将索引构建成一个张量,数据类型为 long(整数型)

        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                                  dtype=torch.long)
        # 使用英语分词器对英语句子进行编码,并根据词汇表将编码后的词语转换为索引
        # 然后将索引构建成一个张量,数据类型为 long(整数型)

        data.append((ja_tensor_, en_tensor_))
        # 将处理后的日语张量和英语张量组成一个元组,并添加到 data 列表中

    return data
    # 返回处理后的数据列表

# 使用训练集的日语句子和英语句子调用 data_process 函数,处理数据并构建张量
train_data = data_process(trainja, trainen)
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):
    # 定义一个函数 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)
    
    # 使用 pad_sequence 函数对英语批量数据进行填充,使它们具有相同的长度
    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)
# DataLoader 参数解释:
# train_data: 训练数据集
# batch_size: 每个批次的数据量大小
# shuffle: 是否在每个epoch开始前打乱数据
# collate_fn: 用于将一批数据拼接成一个batch的函数
6.实现Sequence-to-sequence Transformer

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

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

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)
        # 创建Transformer编码器
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        # 初始化解码器层
        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))
        # 使用Transformer编码器对源语言序列进行编码得到memory
        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
        # 使用Transformer解码器对目标语言序列进行解码得到输出outs
        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)

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

class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout: float, maxlen: int = 5000):
        """
        初始化位置编码模块。
        
        参数:
        emb_size (int): 词嵌入的维度大小。
        dropout (float): 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计算
        pos_embedding[:, 0::2] = torch.sin(pos * den)
        
        # 为奇数索引位置进行cos计算
        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 embedding添加位置编码并应用dropout。
        
        参数:
        token_embedding (Tensor): 输入的token embedding张量。
        
        返回:
        Tensor: 添加了位置编码并应用了dropout的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: int):
        """
        初始化TokenEmbedding模块。
        
        参数:
        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转换为嵌入表示并进行缩放。
        
        参数:
        tokens (Tensor): 输入的token张量。
        
        返回:
        Tensor: 缩放后的token嵌入张量。
        """
        # 将tokens转换为嵌入表示,并乘以嵌入维度的平方根进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

  我们创建了一个后续词屏蔽(mask),以阻止目标词关注其后续的词。我们还创建了用于屏蔽(source)源和(target)目标填充(padding)标记的屏蔽。

def generate_square_subsequent_mask(sz):
    """
    生成一个方形的后续词屏蔽矩阵,用于防止目标词关注其后续的词。
    
    参数:
    sz (int): 矩阵的大小,即目标序列的长度。
    
    返回:
    Tensor: 一个sz x sz的屏蔽矩阵。
    """
    # 创造一个上三角矩阵,其中元素全为1
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    
    # 将矩阵元素类型转换为float,并将值为0的地方填充为负无穷,值为1的地方填充为0.0
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src, tgt):
    """
    创建用于源和目标填充标记的屏蔽,以及目标序列的后续词屏蔽。
    
    参数:
    src (Tensor): 源序列张量。
    tgt (Tensor): 目标序列张量。
    
    返回:
    src_mask (Tensor): 源序列的屏蔽矩阵(全零矩阵,不进行屏蔽)。
    tgt_mask (Tensor): 目标序列的后续词屏蔽矩阵。
    src_padding_mask (Tensor): 源序列的填充屏蔽矩阵。
    tgt_padding_mask (Tensor): 目标序列的填充屏蔽矩阵。
    """
    # 获取源和目标序列的长度
    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)
    
    # 创建目标序列的填充屏蔽矩阵,填充标记位置为True,其余为False
    tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
    
    # 返回四个屏蔽矩阵
    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask
# 定义一些超参数和模型参数
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                 # 训练轮数

# 初始化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 = 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 (nn.Module): 要训练的模型。
    train_iter (DataLoader): 训练数据的迭代器。
    optimizer (Optimizer): 优化器。

    返回:
    float: 当前轮次的平均损失。
    """
    model.train()  # 设置模型为训练模式
    losses = 0     # 初始化损失

    for idx, (src, tgt) in enumerate(train_iter):
        src = src.to(device)  # 将源序列移到设备上
        tgt = tgt.to(device)  # 将目标序列移到设备上

        tgt_input = tgt[:-1, :]  # 目标输入不包括最后一个token

        # 创建屏蔽矩阵
        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:, :]  # 目标输出不包括第一个token
        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 (nn.Module): 要评估的模型。
    val_iter (DataLoader): 验证数据的迭代器。

    返回:
    float: 验证集上的平均损失。
    """
    model.eval()  # 设置模型为评估模式
    losses = 0    # 初始化损失

    with torch.no_grad():  # 评估时不需要计算梯度
        for idx, (src, tgt) in enumerate(val_iter):
            src = src.to(device)  # 将源序列移到设备上
            tgt = tgt.to(device)  # 将目标序列移到设备上

            tgt_input = tgt[:-1, :]  # 目标输入不包括最后一个token

            # 创建屏蔽矩阵
            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:, :]  # 目标输出不包括第一个token
            loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  # 计算损失
            losses += loss.item()  # 累积损失

    return losses / len(val_iter)  # 返回平均损失

7.训练模型

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

  当我使用JParaCrawl的完整句子列表(每种语言大约有590万句)来训练模型时,在单个NVIDIA GeForce RTX 3070 GPU上,每个epoch大约需要5个小时。

以下是代码:

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    """
    贪婪解码函数,用于从编码器输出生成目标序列。
    
    参数:
    model (nn.Module): 训练好的Seq2Seq模型。
    src (Tensor): 源语言序列张量。
    src_mask (Tensor): 源语言序列的屏蔽矩阵。
    max_len (int): 生成目标序列的最大长度。
    start_symbol (int): 目标序列的起始符号索引。
    
    返回:
    Tensor: 生成的目标序列张量。
    """
    src = src.to(device)  # 将源序列移动到设备上(如GPU)
    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()  # 将张量转换为Python标量
        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 (nn.Module): 训练好的Seq2Seq模型。
    src (str): 源语言句子。
    src_vocab (Vocab): 源语言词汇表。
    tgt_vocab (Vocab): 目标语言词汇表。
    src_tokenizer (Tokenizer): 源语言分词器。
    
    返回:
    str: 翻译后的目标语言句子。
    """
    model.eval()  # 设置模型为评估模式
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]  # 将源句子分词并转换为索引列表,添加起始和结束符号
    num_tokens = len(tokens)  # 计算序列的长度
    src = (torch.LongTensor(tokens).reshape(num_tokens, 1))  # 转换为张量并调整形状
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)  # 创建源序列的屏蔽矩阵
    tgt_tokens = greedy_decode(model,  src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()  # 生成目标序列
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")  # 将目标序列索引转换为单词并拼接成句子,去掉起始和结束符号
  8.保存词汇表对象和训练好的模型

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

import pickle

# 打开一个文件,用于存储数据(以二进制写模式打开)
file = open('en_vocab.pkl', 'wb')
# 将en_vocab对象序列化并写入到文件中
pickle.dump(en_vocab, file)
# 关闭文件
file.close()

# 再次打开一个文件,用于存储另一个数据(以二进制写模式打开)
file = open('ja_vocab.pkl', 'wb')
# 将ja_vocab对象序列化并写入到文件中
pickle.dump(ja_vocab, file)
# 关闭文件
file.close()

  最后,我们也可以使用PyTorch的保存和加载功能来保存模型以供以后使用。通常,有两种保存模型的方法,具体取决于我们以后想要如何使用它们。第一种方法是仅用于推理,我们可以稍后加载模型并用它来将日语翻译成英语。

torch.save(transformer.state_dict(), 'inference_model')

  第二种方法也适用于推理,但同时也适用于以后希望加载模型并恢复训练的情况。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值