基于transformer实现机器翻译(日译中)

一、引言

传统的机器翻译方法依赖于规则或统计模型,而近年来,深度学习技术尤其是神经网络的应用为这一领域带来了革命性的改变。Transformer模型是两种在机器翻译中表现出色的深度学习架构。Transformer模型则凭借其独特的自注意力机制,在处理大规模数据时展现出了卓越的性能。本篇博客将深入剖析Transformer模型在机器翻译中的应用细节。我们将从数据的预处理开始,逐步介绍如何通过编码器将源语言文本转换成富含上下文信息的向量,再通过解码器将这些向量转换为目标语言的翻译。此外,我们还将探讨注意力机制如何在Transformer模型中发挥作用,以实现对翻译准确性和流畅性的进一步优化。特别地,我们将聚焦于基于Transformer的模型,展示如何利用PyTorch框架实现一个具体的翻译任务——从日语到中文的翻译。

二、使用编码器—解码器和注意力机制来实现机器翻译模型

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

2.1编码器
编码器的目的是将输入的源语言文本转换成一个固定大小的向量,这个向量包含了整个输入序列的信息。在传统的Seq2Seq模型中,编码器通常是一个循环神经网络(RNN),如长短期记忆网络(LSTM)或门控循环单元(GRU)。编码器逐个处理输入序列中的每个词,更新其内部状态,最终将最后一个时间步的隐藏状态作为上下文向量输出。

2.2注意力机制
注意力机制是Seq2Seq模型的一个关键改进,它允许解码器在生成每个目标词时,动态地聚焦于源序列中与当前目标词最相关的部分。这种机制通过一个可学习的函数来计算源序列中每个词对于当前解码步骤的重要性或“注意力得分”。然后,这些注意力得分用于加权源序列的表示,生成一个加权的上下文向量,这个向量随后被解码器用作条件信息。

2.3含有注意力机制的解码器
含有注意力机制的解码器结合了传统的RNN结构和注意力机制。在每个解码步骤中,解码器不仅接收来自前一个步骤的隐藏状态,还接收一个从编码器的输出序列中计算得到的上下文向量。这个上下文向量是通过注意力机制得到的,它反映了当前目标词与源序列中各个词的关联程度。

具体来说,解码器的每个时间步都会进行以下操作:

使用前一个时间步的输出作为当前时间步的输入。

  • 计算当前时间步的注意力得分,这些得分基于解码器的当前隐藏状态和编码器的所有隐藏状态。

  • 使用注意力得分对编码器的隐藏状态进行加权求和,得到一个加权的上下文向量。

  • 将这个上下文向量与解码器的当前输入合并,形成解码器的当前输入表示。

  • 将合并后的输入表示输入到解码器的RNN单元中,生成下一个隐藏状态。

  • 最后,解码器的输出层(通常是一个全连接层)将隐藏状态映射到目标词汇表的概率分布上,从而预测下一个词。

通过这种方式,含有注意力机制的解码器能够在生成翻译时,更加灵活和准确地利用源文本中的信息,从而提高翻译的质量和流畅性。这种模型结构特别适合处理长序列,因为它允许解码器在每个步骤中都重新关注源文本的不同部分。

2.1读取和预处理数据

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

!tar -xf d2lzh_pytorch.tar
import collections
import os
import io
import math
import torch
from torch import nn
import torch.nn.functional as F
import torchtext.vocab as Vocab
import torch.utils.data as Data

import sys
# sys.path.append("..") 
import d2lzh_pytorch as d2l  # 导入自定义的 d2lzh_pytorch 模块,包含所需的工具函数和类

# 定义特殊符号:填充符(PAD)、序列开始符(BOS)和序列结束符(EOS)
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)

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

# 将一个序列中所有的词记录在 all_tokens 中以便之后构造词典
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)  # 将当前序列中的所有词添加到 all_tokens 中
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)  # 在序列末尾添加 EOS 和 PAD 以达到 max_seq_len 长度
    all_seqs.append(seq_tokens)  # 将处理后的序列添加到 all_seqs 中

# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造 Tensor
def build_data(all_tokens, all_seqs):
    # 使用 all_tokens 中的词构造词典,并指定特殊符号 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]
    # 返回词典和包含所有索引序列的 Tensor
    return vocab, torch.tensor(indices)

为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用’\t’隔开。在读取数据时,我们在句末附上“”符号,并可能通过添加“”符号使每个序列的长度均为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)  # 处理输出序列
    # 使用所有的词构造输入和输出的词典,并将序列变换为词索引后构造 Tensor
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    # 返回输入和输出的词典,以及包含输入和输出数据的 TensorDataset
    return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)

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

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

结果:

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

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

1.编码器
在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。正如我们在6.5节(循环神经网络的简洁实现)中提到的,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)  # 初始化父类 nn.Module
        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)  # (seq_len, batch, input_size)
        return self.rnn(embedding, state)  # 将嵌入层输出传递给 GRU 网络

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

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

# 创建一个编码器实例,指定词汇表大小、嵌入维度、隐藏层单元数和层数
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
# 创建一个形状为 (批量大小, 时间步数) 的全零输入张量,并传入编码器
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
# 输出形状和状态形状
output.shape, state.shape  # GRU 的 state 是 h,而 LSTM 的是一个元组 (h, c)

结果:

(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))

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

def attention_model(input_size, attention_size):
    # 定义一个注意力模型
    # Sequential 容器将多个层组合成一个模型
    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
# 创建注意力模型,输入维度为 2*num_hiddens,注意力维度为 10
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 函数并获取输出形状
attention_forward(model, enc_states, dec_state).shape

结果:

torch.Size([4, 8])

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 的输入包含注意力输出的 c 和实际输入,所以尺寸是 num_hiddens + embed_size
        self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens, num_layers, dropout=drop_prob)  # 定义多层 GRU 网络
        self.out = nn.Linear(num_hiddens, vocab_size)  # 定义输出层,将 GRU 输出映射到词汇表大小

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

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

2.3 训练模型

我们先实现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])
    
    # Y 形状: (batch, seq_len)
    for y in Y.permute(1, 0):  # 将序列维度移到第一个维度
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)  # 解码器输出
        l = l + (mask * loss(dec_output, y)).sum()  # 计算损失,并用 mask 忽略 PAD 项
        dec_input = y  # 使用强制教学,下一步的输入是当前真实标签
        num_not_pad_tokens += mask.sum().item()  # 累计非 PAD 标签的数量
        # EOS 后面全是 PAD,下面一行保证一旦遇到 EOS,接下来的循环中 mask 就一直是 0
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    
    return l / num_not_pad_tokens  # 返回平均损失

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

def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    # 初始化编码器和解码器的优化器
    enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
    dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)

    # 定义损失函数为交叉熵损失,并且不进行内部的均值计算(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个epoch输出一次损失
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

结果:

epoch 10, loss 0.433
epoch 20, loss 0.191
epoch 30, loss 0.089
epoch 40, loss 0.083
epoch 50, loss 0.041

2.4预测不定长的序列

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

def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列拆分为词,并添加 EOS 和 PAD 以使长度达到 max_seq_len
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    # 将输入序列转换为索引,并创建输入张量(batch size 为 1)
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]])  # 形状为 (1, max_seq_len)
    # 初始化编码器的隐藏状态
    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  # 返回输出序列

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

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

结果:

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

2.5评价翻译结果

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

具体来说,设词数为 n n n的子序列的精度为 p n p_n pn。它是预测序列与标签序列匹配词数为 n n n的子序列的数量与预测序列中词数为 n n n的子序列的数量之比。举个例子,假设标签序列为 A A A B B B C C C D D D E E E F F F,预测序列为 A A A B B B B B B C C C D D D,那么 p 1 = 4 / 5 , p 2 = 3 / 4 , p 3 = 1 / 3 , p 4 = 0 p_1 = 4/5, p_2 = 3/4, p_3 = 1/3, p_4 = 0 p1=4/5,p2=3/4,p3=1/3,p4=0。设 l e n label len_{\text{label}} lenlabel l e n pred len_{\text{pred}} lenpred分别为标签序列和预测序列的词数,那么,BLEU的定义为

exp ⁡ ( min ⁡ ( 0 , 1 − l e n label l e n pred ) ) ∏ n = 1 k p n 1 / 2 n \exp\left(\min\left(0, 1 - \frac{len_{\text{label}}}{len_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n} exp(min(0,1lenpredlenlabel))n=1kpn1/2n
其中 k k k是我们希望匹配的子序列的最大词数。可以看到当预测序列和标签序列完全一致时,BLEU为1。

因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。例如,当 p n p_n pn固定在0.5时,随着 n n n的增大, 0. 5 1 / 2 ≈ 0.7 , 0. 5 1 / 4 ≈ 0.84 , 0. 5 1 / 8 ≈ 0.92 , 0. 5 1 / 16 ≈ 0.96 0.5^{1/2} \approx 0.7, 0.5^{1/4} \approx 0.84, 0.5^{1/8} \approx 0.92, 0.5^{1/16} \approx 0.96 0.51/20.7,0.51/40.84,0.51/80.92,0.51/160.96。另外,模型预测较短序列往往会得到较高 p n p_n pn值。因此,上式中连乘项前面的系数是为了惩罚较短的输出而设的。举个例子,当 k = 2 k=2 k=2时,假设标签序列为 A A A B B B C C C D D D E E E F F F,而预测序列为 A A A B B B。虽然 p 1 = p 2 = 1 p_1 = p_2 = 1 p1=p2=1,但惩罚系数 exp ⁡ ( 1 − 6 / 2 ) ≈ 0.14 \exp(1-6/2) \approx 0.14 exp(16/2)0.14,因此BLEU也接近0.14。

下面来实现BLEU的计算。

def bleu(pred_tokens, label_tokens, k):
    # 计算预测序列和标签序列的长度
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    # 计算BLEU得分的前缀部分,防止预测序列长度小于标签序列长度的情况
    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)
        # 构建标签序列的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的精度,并更新BLEU得分
        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(' ')
    # 计算BLEU得分
    bleu_score = bleu(pred_tokens, label_tokens, k)
    # 打印BLEU得分和预测的翻译结果
    print('bleu %.3f, predict: %s' % (bleu_score, ' '.join(pred_tokens)))

预测正确则分数为1。

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

结果:

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

结果:

bleu 0.658, predict: they are russian .

三、使用Transformer架构和PyTorch深度学习库来实现的日中机器翻译模型

3.1、导入必要的库

在开始项目之前,我们需要设置开发环境并导入必要的库。我们将使用 Python 编程语言和 PyTorch 深度学习框架。如果你的环境中缺少某些包,可以使用 pip 命令来安装它们。

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

3.2、数据集准备

在本博客中,我们将使用从 JParaCrawl 下载的日语-英语并行数据集![ http://www.kecl.NTT.co.jp/icl/lirg/jparacrawl ]被描述为“ NTT 创建的最大的公开可用的英日平行语料库。它主要是通过网络爬行和自动对齐并行句创建的。”

# 从指定路径读取CSV文件,使用'\t'作为分隔符,采用Python引擎解析,不使用header行
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 提取第2列数据并转换为列表,存储在trainen变量中(可选:仅提取前10000行)
trainen = df[2].values.tolist()#[:10000]
# 提取第3列数据并转换为列表,存储在trainja变量中(可选:仅提取前10000行)
trainja = df[3].values.tolist()#[:10000]

# trainen.pop(5972)
# trainja.pop(5972)

在导入所有的日语和英语对应项之后,我删除了数据集中的最后一个数据,因为它缺少一个值。两个网站的句子总数为5,973,071,但是,为了学习的目的,经常建议在一次性使用所有数据之前抽样数据并确保一切正常,以节省时间。

下面是数据集中包含的句子示例:

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

结果如下:

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

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

这两行代码分别打印出了trainen列表(存储中文句子的列表)和trainja列表(存储日文句子的列表)中索引为500的元素,也就是第501个句子(因为Python列表索引是从0开始的)。这样做通常是为了检查数据加载是否正确,以及直观地了解数据集中句子的内容。

3.3、准备分词器

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

# 使用指定的模型文件初始化一个SentencePieceProcessor对象,用于处理英语文本
en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
# 使用指定的模型文件初始化一个SentencePieceProcessor对象,用于处理日语文本
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/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')

结果如下:

['▁All',
 '▁residents',
 '▁aged',
 '▁20',
 '▁to',
 '▁59',
 '▁years',
 '▁who',
 '▁live',
 '▁in',
 '▁Japan',
 '▁must',
 '▁enroll',
 '▁in',
 '▁public',
 '▁pension',
 '▁system',
 '.']

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

结果如下:

['▁',
 '年',
 '金',
 '▁日本',
 'に住んでいる',
 '20',
 '歳',
 '~',
 '60',
 '歳の',
 '全ての',
 '人は',
 '、',
 '公的',
 '年',
 '金',
 '制度',
 'に',
 '加入',
 'しなければなりません',
 '。']

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

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

def build_vocab(sentences, tokenizer):
  counter = Counter()
  for sentence in sentences:
    # 使用分词器将句子编码为字符串,并更新计数器
    counter.update(tokenizer.encode(sentence, out_type=str))
  # 使用计数器创建一个 TorchText 的 Vocab 对象,同时指定特殊符号
  return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用日语分词器和日语句子构建日语的词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 使用英语分词器和英语句子构建英语的词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

这段代码定义了一个build_vocab函数,其作用是根据给定的句子列表和一个分词器(tokenizer),统计句子中所有单词的出现频次,并创建一个词汇表(Vocab)对象。
这个词汇表不仅包括了正常的单词,还特别包含了四个特殊标记:未知词标记、填充标记、句子开始标记和句子结束标记。
之后,该函数被调用来分别构建了日语和英语的词汇表。

在获得词汇表对象之后,我们可以使用词汇表和 tokenizer 对象为我们的训练数据构建张量。

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

这段代码定义了一个data_process函数,其目的是将原始的日语和英语句子转换为模型可以直接处理的张量格式。函数首先遍历给定的日语和英语句子列表,对每一对句子进行以下处理步骤:去除行尾换行符,使用对应的分词器进行编码,然后将每个token(单词或子词)转换为词汇表中的索引,并将这些索引构建成Long类型的PyTorch张量。处理完成后,将每一对日语和英语的张量作为一个元素添加到结果列表中。最后,调用这个函数处理训练数据,生成train_data,为模型训练做好数据准备。

3.5、创建DataLoader用于在训练过程中批量加载和处理数据

在这里,我将 BATCH _ SIZE 设置为16,以防止“内存不足”,但这取决于各种事情,如您的机器内存容量,数据大小等,所以随时根据您的需要更改批量大小(注意: PyTorch 的教程设置批量大小为128使用 Multi30k 德语-英语数据集)

BATCH_SIZE = 8
# 获取<pad>、<bos>和<eos>在日语词汇表中的索引
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:
    # 在日语句子的开头和结尾添加<eos>和<bos>标记,并将其拼接到日语批处理列表中
    ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
    # 在英语句子的开头和结尾添加<eos>和<bos>标记,并将其拼接到英语批处理列表中
    en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
  # 对日语批次进行填充,使用<pad>标记
  ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
  # 对英语批次进行填充,使用<pad>标记
  en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
  return ja_batch, en_batch
# 创建用于训练迭代的 DataLoader 对象
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

3.6、定义模型并初始化

接下来的几段代码和文本解释(用斜体写成)摘自最初的 PyTorch 教程[ https://PyTorch.org/tutorials/beginner/translation_transformer.html ]。除了 BATCH _ SIZE 和单词 de _ ocabwhich 被更改为 ja _ ocabb 之外,我没有做任何更改。

Transformer是一个 Seq2Seq 模型介绍了“注意力是你所需要的一切”文件,以解决机器翻译任务。Transformer模型由一个编码器和解码器块组成,每个编码器和解码器块包含固定数量的层。

编码器处理输入序列的传播,通过一系列的多头注意和前馈网络层。编码器的输出称为存储器,与目标张量一起被馈送到解码器。编码器和解码器是在一个端到端的方式使用强制教学培训。

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

# 定义一个基于 Transformer 的 Seq2Seq 模型类
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))
        # 编码器处理源语言序列并生成记忆
        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)

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

# 定义位置编码器模块
class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
        super(PositionalEncoding, self).__init__()
        # 计算位置编码的分母部分
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)
        # 生成位置索引
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)
        # 初始化位置编码张量
        pos_embedding = torch.zeros((maxlen, emb_size))
        # 使用正弦和余弦函数生成位置编码
        pos_embedding[:, 0::2] = torch.sin(pos * den)
        pos_embedding[:, 1::2] = torch.cos(pos * den)
        pos_embedding = pos_embedding.unsqueeze(-2)

        # 初始化丢弃层和位置编码张量
        self.dropout = nn.Dropout(dropout)
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        # 将位置编码和词嵌入张量相加,并应用丢弃层
        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):
        # 将输入的 tokens 转换为词嵌入表示,并乘以 sqrt(emb_size)
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

这段代码定义了两个类,PositionalEncoding 和 TokenEmbedding,它们是用于自然语言处理任务中Transformer模型的核心组件,旨在增强模型对序列中单词顺序的理解能力。
这两个类共同作用于NLP模型的输入层,其中TokenEmbedding负责将单词转换为词嵌入,而PositionalEncoding则在此基础上添加位置信息,两者结合使模型能够理解文本中单词的语义以及它们在句子中的相对位置,这是Transformer模型理解序列数据的基础。

我们创建一个后续的单词掩码来阻止目标单词注意到它的后续单词。我们还为屏蔽源和目标填充标记创建了掩码。

def generate_square_subsequent_mask(sz):
    # 生成一个对角线及以上为0的矩阵
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(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]
    # 生成目标语言序列的 mask
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
    # 生成源语言序列的 mask(全为0)
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
    # 生成源语言和目标语言的填充 mask(PAD_IDX 为填充符号的索引)
    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

这段代码主要实现了两种类型的掩码生成函数,用于Transformer模型中的注意力机制:generate_square_subsequent_mask 生成一个上三角矩阵,用于在解码器的自注意力层屏蔽未来时刻的信息,确保预测第i个词时只能看到i时刻之前的词。
create_mask 函数综合生成了源序列自注意力掩码、目标序列的后续子序列掩码以及源序列和目标序列的填充掩码。填充掩码用于忽略输入中的PAD符号,避免它们对注意力分数产生影响。

当你使用自己的GPU的时候,NUM_ENCODER_LAYERS 和 NUM_DECODER_LAYERS 设置为3或者更高,NHEAD设置8,EMB_SIZE设置为512。

# 定义源语言词汇表大小和目标语言词汇表大小
SRC_VOCAB_SIZE = len(ja_vocab)
TGT_VOCAB_SIZE = len(en_vocab)
# 定义词嵌入维度大小
EMB_SIZE = 512
# 定义注意力头的数量
NHEAD = 8
# 定义前馈网络隐藏层维度大小
FFN_HID_DIM = 512
# 定义批处理大小
BATCH_SIZE = 16
# 定义编码器层数和解码器层数
NUM_ENCODER_LAYERS = 3
NUM_DECODER_LAYERS = 3
# 定义训练周期数量
NUM_EPOCHS = 16

# 初始化 Transformer 模型
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

# 对模型参数进行初始化
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)

# 定义优化器
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)

3.7、开始训练

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

当我使用来自 JParaCrawl 的完整句子列表来训练这个模型时,每种语言大约有590万个句子。

代码如下:

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
  start_time = time.time()
  train_loss = train_epoch(transformer, train_iter, optimizer)
  end_time = time.time()
  print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
          f"Epoch time = {(end_time - start_time):.3f}s"))

此段代码执行模型的训练循环,利用tqdm.tqdm库动态地展示训练进度。对于每一个epoch(训练轮次),它记录开始时间,调用train_epoch函数进行训练并计算训练损失,然后记录结束时间以计算该轮次的持续时间。最后,打印出当前轮次的编号、训练损失值以及完成该轮次所需的总时间,帮助监控训练过程。

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

这两段代码定义了贪婪解码(greedy_decode)和翻译(translate)的过程。

greedy_decode 函数实现了贪心解码策略,从给定的源序列逐步生成目标序列。它首先对源序列进行编码,然后基于开始符号逐步生成下一个最可能的词,直到达到最大长度或生成结束符号。
translate 函数则是将整个翻译流程封装起来,包括准备输入数据、设置模型为评估模式、调用贪心解码函数生成目标序列,最后将目标序列的索引转换回实际的单词序列,并去除特殊标记符号,返回翻译结果。

然后,我们可以调用平移函数并传递所需的参数。

 #翻译调用实例
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
trainen.pop(5)  # 从训练的英文数据集中移除第5个元素
'Chinese HS Code Harmonized Code System < HS编码 8515 : 电气(包括电热气体)、激光、其他光、光子束、超声波、电子束、磁脉冲或等离子弧焊接机器及装置,不论是否 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'
trainja.pop(5)  # 从训练的日文数据集中移除第5个元素
'Japanese HS Code Harmonized Code System < HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)、レーザーその他の光子ビーム式、超音波式、電子ビーム式、 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'

3.9、保存模型

最后,在训练结束后,我们将首先使用 Pickle 保存词汇表对象(en _ voab 和 ja _ voab)。

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()

这段代码使用Python的pickle模块将两个词汇表(en_vocab 和 ja_vocab)分别保存到两个文件中 (en_vocab.pkl 和 ja_vocab.pkl)。pickle模块允许将Python对象转化为字节流,从而可以保存到文件或者通过网络传输。这里采用二进制写入模式(‘wb’)打开文件,完成序列化过程后关闭文件,确保数据安全写入磁盘。

最后,我们还可以使用 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')

ance, Japan, Russia, Germany, Korea, Canada …’


### 3.9、保存模型

最后,在训练结束后,我们将首先使用 Pickle 保存词汇表对象(en _ voab 和 ja _ voab)。

```py
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()

这段代码使用Python的pickle模块将两个词汇表(en_vocab 和 ja_vocab)分别保存到两个文件中 (en_vocab.pkl 和 ja_vocab.pkl)。pickle模块允许将Python对象转化为字节流,从而可以保存到文件或者通过网络传输。这里采用二进制写入模式(‘wb’)打开文件,完成序列化过程后关闭文件,确保数据安全写入磁盘。

最后,我们还可以使用 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')
  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值