机器翻译技术解析与实战案例:编码器—解码器与注意力机制
前言
在人工智能的诸多领域中,机器翻译无疑是最具挑战性的任务之一。它不仅要求模型理解源语言的深层语义,还要能够准确、流畅地表达目标语言。随着深度学习技术的发展,机器翻译的质量得到了显著提升。本文将带您从传统的编码器—解码器架构出发,探索到最新的Transformer模型,深入了解它们在中日语翻译中的应用。
机器翻译的发展历程
机器翻译的早期尝试可以追溯到20世纪中叶,当时的模型主要基于规则和统计方法。然而,这些方法在处理复杂语言结构时显得力不从心。随着神经网络的兴起,特别是循环神经网络(RNN)的出现,机器翻译开始进入一个新的时代。编码器—解码器架构成为了这一时期的核心技术,它通过序列到序列(Seq2Seq)的转换,显著提高了翻译的准确性。
编码器—解码器架构的局限性
尽管编码器—解码器架构在机器翻译领域取得了巨大成功,但它仍然存在一些局限性。最明显的是,当处理长文本时,RNN的梯度消失或爆炸问题会导致模型性能下降。此外,RNN的序列处理方式在长距离依赖问题上也存在挑战。
Transformer模型的突破
为了解决这些问题,2017年,由Vaswani等人提出的Transformer模型横空出世。它摒弃了传统的循环层,转而使用自注意力(Self-Attention)机制来处理序列数据。这种机制使得模型能够并行处理序列中的所有元素,从而大大提高了训练效率,并在长距离依赖问题上表现出色。
机器翻译基础
在人工智能领域,机器翻译是一项令人兴奋的技术,它使得跨语言的交流变得无比便捷。本文将深入探讨机器翻译中的核心技术——编码器—解码器架构以及注意力机制,并展示如何使用PyTorch实现这些技术。
编码器—解码器架构
编码器—解码器架构是机器翻译中的一个基础模型,它通过两个主要组件——编码器和解码器——来实现语言之间的转换。
编码器
编码器的任务是读取源语言文本,并将其转换成一个固定大小的向量,这个向量能够捕捉文本的主要信息。在实现中,我们通常使用词嵌入层和循环神经网络(如GRU或LSTM)来完成这一任务。
解码器
解码器则负责将编码器的输出向量转换成目标语言的文本。它通常也是一个循环神经网络,但在每个时间步,它还会接收来自编码器的信息,以生成准确的翻译。
注意力机制
注意力机制是编码器—解码器架构中的关键创新,它允许解码器在生成每个词时,能够“关注”编码器输出的特定部分。这大大提高了翻译的准确性,尤其是在处理长句子时。
实现细节
以下是使用PyTorch实现编码器—解码器和注意力机制的一些关键步骤:
数据预处理
包括读取数据、定义特殊符号(如<pad>、<bos>、<eos>),以及构建词汇表。
代码首先定义了三个特殊的标记:`<pad>`、`<bos>` 和 `<eos>`。这些标记在序列处理中扮演着重要角色:
- `<pad>` 用于填充较短的序列,使所有序列长度统一,便于批量处理。
- `<bos>` 表示句子的开始,有助于模型识别序列的起始点。
- `<eos>` 表示句子的结束,有助于模型识别序列的终点。
接着,代码设置了CUDA环境变量并初始化了PyTorch的设备,以确保代码可以在GPU上运行以加速训练过程。同时,导入了必要的PyTorch模块和函数,以及自定义的`d2lzh_pytorch`模块,这可能包含了一些辅助函数或类。
数据预处理函数
定义了两个关键的辅助函数,`process_one_seq` 和 `build_data`,用于处理和构建数据集。
`process_one_seq` 函数接收一个序列及其分词列表,并执行以下操作:
- 将序列中的所有词添加到`all_tokens`列表,这有助于构建词汇表。
- 在序列末尾添加`<EOS>`标记,并根据需要添加`<PAD>`标记,以确保所有序列长度一致。
- 将处理后的序列添加到`all_seqs`列表,供后续处理。
`build_data` 函数使用`all_tokens`构建词汇表,并使用该词汇表将序列中的词转换为索引:
- 使用`collections.Counter`统计词频,并创建词汇表对象,同时指定特殊词。
- 将序列中的每个词转换为对应的索引,并创建索引列表。
数据读取与预处理
`read_data` 函数负责读取数据文件,并对其进行预处理:
- 初始化存放输入序列和输出序列的列表。
- 使用`io.open`读取文件,并分割每一行以获取输入和输出序列。
- 对每个序列使用`process_one_seq`函数进行处理,确保序列长度一致并添加特殊标记。
- 对处理后的输入和输出序列分别构建词汇表,并转换为索引张量。
- 返回构建好的词汇表和数据集对象,数据集对象包含输入和输出的索引张量。
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)
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 设置环境变量,指定CUDA设备,用于GPU加速
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 根据是否有CUDA环境,设置设备为GPU或CPU
print(torch.__version__, device)
# 定义一个函数,用于处理单个序列,使其符合数据预处理的要求
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)
# 定义一个函数,用于使用所有词构建词汇表,并将序列中的词转换为词索引
def build_data(all_tokens, all_seqs):
# 使用collections.Counter对all_tokens中的词进行计数,得到每个词出现的频率
# 并利用这些频率创建一个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]
# 返回构建好的词汇表vocab和包含序列索引的Tensor
return vocab, torch.tensor(indices)
# 定义一个函数,用于读取数据并进行预处理,以便用于训练
def read_data(max_seq_len):
# 初始化存放输入序列、输出序列、处理后的输入序列和输出序列的列表
in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
# 使用io.open打开文件,读取所有行到lines列表中
with io.open('fr-en-small.txt') as f:
lines = f.readlines()
# 遍历文件中的每一行
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(' ')
# 如果输入或输出序列长度超过最大序列长度减1(减1是因为要添加EOS标记)
# 则跳过这个样本,不进行处理
if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
continue
# 调用process_one_seq函数处理输入序列,并更新in_tokens和in_seqs列表
process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
# 调用process_one_seq函数处理输出序列,并更新out_tokens和out_seqs列表
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的Dataset
return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
最后,代码设置了序列的最大长度为7,并调用`read_data`函数来读取和预处理数据。通过访问数据集对象的第一个样本,展示了预处理后的数据形式,其中包括了法语和英语句子的索引序列。
max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]

模型定义
定义编码器和解码器的网络结构,包括嵌入层、循环神经网络以及注意力模型。
含注意力机制的编码器
编码器部分的代码定义了一个Encoder类,它是一个继承自nn.Module的PyTorch模型,具体功能如下:
- 初始化 (__init__ 方法): 初始化编码器,包括嵌入层和GRU层。嵌入层将输入的词索引转换为词向量,GRU层处理序列数据并包含dropout以防止过拟合。
- 前向传播 (forward 方法): 实现编码器的前向传播,接受输入序列和隐藏状态,通过嵌入层和GRU层生成输出和新的状态。
- 初始化状态 (begin_state 方法): 提供一个初始化隐藏状态的方法,对于GRU来说通常返回None,因为它内部会处理状态的初始化。
class Encoder(nn.Module):
# 初始化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层,用于序列数据的处理
# dropout参数用于在训练过程中随机丢弃一些网络连接以防止过拟合
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
# 定义Encoder类的前向传播函数
def forward(self, inputs, state):
# 输入inputs是词的索引,形状为(批量大小, 时间步数)
# 使用嵌入层将词索引转换为词向量,并交换样本维和时间步维
embedding = self.embedding(inputs.long()).permute(1, 0, 2) # 转换后形状为(seq_len, batch, input_size)
# 将嵌入后的输入和状态state传递给GRU层进行处理
return self.rnn(embedding, state)
# 定义Encoder类的初始化状态函数
def begin_state(self):
# 由于GRU层可以自己处理隐藏状态,这里返回None
return None
注意力机制
注意力机制的代码实现了一个自定义的注意力模型,包括:
- 注意力模型 (attention_model 函数): 创建一个序列到序列的注意力模型,使用两个线性层包裹着一个tanh激活函数。
- 注意力前向传播 (attention_forward 函数): 执行注意力机制的前向传播,计算解码器的隐藏状态与编码器所有时间步的隐藏状态之间的注意力权重,并生成背景向量。
# 定义一个注意力模型
def attention_model(input_size, attention_size):
# 创建一个顺序模型
model = nn.Sequential(
# 第一层线性层,输入维度为input_size,输出维度为attention_size,不使用偏置项
nn.Linear(input_size, attention_size, bias=False),
# 使用tanh激活函数
nn.Tanh(),
# 第二层线性层,将attention_size维度的输入转换为1维,不使用偏置项
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) # 返回背景变量
# 定义编码器时间步数、批量大小和隐藏单元个数
seq_len, batch_size, num_hiddens = 10, 4, 8
# 调用注意力模型函数,输入维度为2*num_hiddens,输出维度为seq_len
model = attention_model(2 * num_hiddens, seq_len)
# 创建编码器状态的张量
# 表示编码器在每个时间步的隐藏状态
enc_states = torch.zeros((seq_len, batch_size, num_hiddens))
# 创建解码器初始状态的张量
# 表示解码器的初始隐藏状态
dec_state = torch.zeros((batch_size, num_hiddens))
# 调用注意力机制的前向传播函数,传入模型、编码器状态和解码器状态
# 该函数将返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数
# 即输出形状为(batch_size, num_hiddens)
attention_result = attention_forward(model, enc_states, dec_state)
# 获取注意力机制前向传播结果的形状,预期为(4, 8)
print(attention_result.shape)
![]()
含注意力机制的解码器
解码器部分的代码定义了一个Decoder类,它也是一个继承自nn.Module的PyTorch模型,具体功能如下:
- 初始化 (__init__ 方法): 初始化解码器,包括嵌入层、注意力模型、GRU层和输出层。GRU层的输入维度是num_hiddens + embed_size,因为输入包括了词嵌入和背景向量。
- 前向传播 (forward 方法): 实现解码器的前向传播,结合当前的输入词、背景向量和前一时间步的隐藏状态来生成当前时间步的输出和新状态。
- 初始化状态 (begin_state 方法): 使用编码器最终时间步的隐藏状态作为解码器的初始隐藏状态。
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
补充说明
编码器的输入是词索引的张量,输出是每个时间步的隐藏状态。解码器的输入是词索引,输出是对输出词典中每个词的预测概率。编码器最终时间步的隐藏状态被用作解码器的初始状态,确保信息的连续性。注意力机制允许解码器在生成每个词时,能够关注编码器输出中与之最相关的部分,从而提高翻译的准确性和流畅性。
训练过程
实现batch_loss函数来计算损失,并使用优化器更新模型参数。
`batch_loss` 函数
这个函数用于计算一个小批量数据的损失值:
- 初始化编码器状态。
- 通过编码器对输入序列 `X` 进行前向传播,获取编码器输出和最终状态。
- 初始化解码器状态,并设置初始输入为BOS(句子开始的特殊标记)。
- 使用掩码变量 `mask` 来忽略标签为填充项PAD的损失,初始设置为1。
- 循环处理目标序列 `Y` 中的每个时间步,执行以下操作:
使用解码器和编码器输出计算当前时间步的输出;累加损失,使用掩码避免填充项对损失的影响;更新解码器的输入为当前时间步的目标词(强制教学);更新掩码,一旦遇到EOS(句子结束的特殊标记),后续的掩码将置为0;返回归一化的损失值,即总损失除以非填充项的数量。
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
`train` 函数
这个函数定义了模型的训练过程:
- 为编码器和解码器分别设置Adam优化器。
- 初始化交叉熵损失函数,设置`reduction='none'`以计算每个样本的损失。
- 创建数据加载器,用于按批量加载数据,并打乱数据顺序。
- 进行多轮训练
# 定义训练模型的函数
def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
# 为编码器设置优化器,使用Adam算法,学习率为lr
enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
# 为解码器设置优化器,使用Adam算法,学习率为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)
# 循环进行训练,num_epochs为训练的轮数
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)))
embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,
drop_prob)
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,
attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)

补充说明
- 训练过程中,损失的计算考虑了掩码以排除填充项的影响,确保模型只关注有效的翻译部分。
- 使用Adam优化器进行参数更新,它是一种自适应学习率优化算法,广泛用于训练深度学习模型。
- 贪婪搜索是一种简单的解码策略,每次选择概率最高的词作为输出,适用于快速翻译但可能不是最优解。
- 模型训练和翻译过程中使用的特殊标记(BOS、EOS、PAD)对于模型正确处理序列数据至关重要。
预测与翻译
使用贪婪搜索策略来生成翻译结果。
`translate` 函数
这个函数实现了使用训练好的模型进行翻译的过程:
- 将输入序列拆分为单词,并添加EOS和PAD以满足序列长度要求。
- 将输入单词转换为索引,并构造成张量。
- 获取编码器和解码器的初始状态。
- 通过编码器对输入序列进行前向传播,获取编码器输出和状态。
- 使用贪婪搜索策略进行解码,直到生成EOS或达到最大序列长度:使用解码器和编码器输出计算当前时间步的输出;选择概率最高的单词作为预测结果;如果预测结果为EOS,则结束翻译过程;否则,将预测的单词加入输出序列,并作为解码器的下一个输入。
def translate(encoder, decoder, input_seq, max_seq_len):
# 将输入序列拆分为单词
in_tokens = input_seq.split(' ')
# 添加结束符EOS,并用填充符PAD填充至max_seq_len长度
in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
# 将输入单词转换为对应的索引,并构造成张量,batch大小为1
enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]])
# 获取编码器的初始状态
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 = []
# 进行解码,直到序列长度达到max_seq_len或生成EOS
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)
评价翻译质量
BLEU,即Bilingual Evaluation Understudy的缩写,是一种用于衡量机器翻译准确性的工具,它相当于一种辅助性的评价系统,专门用于双语翻译质量的评测。这个工具的核心理念是,机器翻译的输出如果与人工翻译的文本越相似,则其翻译质量就越高。BLEU的计算方法主要依赖于两个文本之间句子的相似性,通过比较它们来评估翻译的准确性。
BLEU的评分机制涉及以下几个步骤:
1. 对于每一句翻译,BLEU会检查机器翻译输出中的N-gram(即词的序列)与参考翻译中的N-gram有多少是匹配的。
2. 根据匹配的N-gram长度,给予不同的权重,例如BLEU-4会更多地强调四词序列的匹配。
3. BLEU分数通过计算加权N-gram匹配数量的几何平均值得出,这涉及到将所有匹配的N-gram数量乘起来,然后取其N次方根。
4. BLEU分数介于0到1之间,分数越高,意味着机器翻译与参考翻译的一致性越好。
然而,BLEU分数仅仅是评估机器翻译质量的一个维度,它并不涵盖翻译的语法正确性、文本的可读性或上下文的连贯性。因此,尽管BLEU是一个有用的评估工具,但为了全面评价翻译质量,它应该与其他评估手段结合使用。

# 计算BLEU分数,用于评估机器翻译质量
def bleu(pred_tokens, label_tokens, k):
# 计算预测序列和标签序列的长度
len_pred, len_label = len(pred_tokens), len(label_tokens)
# 计算简短惩罚因子
score = math.exp(min(0, 1 - len_label / len_pred))
# 对于1到k长度的n-gram进行计算
for n in range(1, k + 1):
num_matches, label_subs = 0, collections.defaultdict(int)
# 统计标签中的n-gram出现次数
for i in range(len_label - n + 1):
label_subs[''.join(label_tokens[i: i + n])] += 1
# 统计预测序列中与标签匹配的n-gram数量
for i in range(len_pred - n + 1):
if label_subs[''.join(pred_tokens[i: i + n])] > 0:
num_matches += 1
label_subs[''.join(pred_tokens[i: i + n])] -= 1
# 计算n-gram的精确度,并更新分数
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
# 返回计算得到的BLEU分数
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)))

实战案例:基于Transformer实现机器翻译(日译中)
在自然语言处理领域,机器翻译是一项极具挑战性的任务。随着深度学习技术的发展,特别是Transformer模型的提出,机器翻译的质量得到了显著提升。本文将介绍如何使用PyTorch和Torchtext构建一个日英机器翻译模型。
Transformer模型原理解析
2017年,由Vaswani等人提出的Transformer模型,在自然语言处理(NLP)领域引起了一场革命。它在机器翻译等序列到序列(Seq2Seq)任务中取得了前所未有的成功,其核心创新是自注意力(Self-Attention)机制,这使得模型能够更好地捕捉长距离依赖关系。本文将深入分析Transformer模型的原理,并探讨其在NLP任务中的应用。
Transformer模型概述
Transformer模型完全基于注意力机制,摒弃了传统的循环神经网络(RNN)和卷积神经网络(CNN)结构。它主要由编码器(Encoder)和解码器(Decoder)组成,编码器将输入序列转换为连续表示,解码器则基于编码器的输出和之前生成的输出序列生成目标序列。
编码器和解码器结构
- 编码器:由多个相同的层(通常是6层)堆叠而成,每层包括多头自注意力机制和位置前馈全连接前馈网络。
- 解码器:同样由多个相同的层构成,每层包括遮蔽(Masked)多头自注意力、编码器-解码器注意力和前馈网络。
关键组件
- 多头自注意力机制:允许模型在编码每个单词时同时关注序列中的其他所有单词,通过不同的头并行处理信息,增强了模型的表示能力。
- 位置编码:由于Transformer缺乏递归和卷积结构,位置编码被添加到输入嵌入中,以提供序列中单词的位置信息。
- 前馈网络:在每个编码器和解码器层中,自注意力或遮蔽自注意力的输出会通过一个前馈网络,以进一步提取特征。
- 层归一化和残差连接:每个子层的输出都通过层归一化处理,并与子层的输入相加,形成残差连接,有助于避免深层网络训练中的梯度消失问题。
自注意力机制
自注意力机制是Transformer模型的核心,它允许模型在处理序列时捕捉任意两个位置之间的依赖关系,无论这些位置之间的距离有多远。自注意力可以表示为以下三个步骤:
- 查询(Query)、键(Key)、值(Value)的计算:输入序列首先被分割成多个查询、键和值。
- 注意力权重的计算:通过计算查询与所有键的相似度,得到注意力权重。
- 加权求和:使用得到的注意力权重对值进行加权求和,得到最终的输出。

多头自注意力
Transformer模型使用多头自注意力来并行处理多个子空间的信息,每个头学习到的表示会被合并,以捕获不同的上下文信息。

位置编码
由于Transformer模型缺乏对序列顺序的固有表示,因此需要位置编码来提供单词在序列中的位置信息。通常,位置编码使用正弦和余弦函数的组合来实现,并将这些编码添加到词嵌入中。
前馈网络
在每个编码器和解码器层中,自注意力的输出会通过一个前馈网络,这个网络通常由两个线性层组成,中间使用ReLU激活函数。
优化和训练
Transformer模型通常使用交叉熵损失函数进行训练,优化器通常选择Adam。模型训练过程中,使用梯度裁剪来防止梯度爆炸问题。
结构图

结论
Transformer模型以其创新的自注意力机制和多头注意力结构,在NLP领域取得了显著的成就。它的灵活性和强大的表示能力使其成为许多NLP任务的首选模型。随着研究的深入,Transformer模型及其变体在语言模型、文本分类、命名实体识别等任务中展现出了巨大的潜力。
实现细节
主要实现了一个基于PyTorch和Torchtext的日英机器翻译模型。它使用了Transformer架构,包括编码器和解码器,并通过Jupyter Notebook进行演示。
导入必要的库
包括数学、深度学习、数据处理和机器翻译所需的库。
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,请在你自己的电脑上尝试运行这一套代码
获取平行语料库
使用JParaCrawl下载的日英平行语料库。
# 使用pandas库读取'./zh-ja/zh-ja.bicleaner05.txt'文件,该文件使用制表符作为分隔符
# engine='python'表示使用Python的csv模块来解析文件
# header=None表示文件中没有列名行
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 注释掉的代码表示可以选择只取前10000个元素
trainen = df[2].values.tolist()#[:10000]
# 将数据框df的第四列转换为列表,存储在变量trainja中
# 同样,注释掉的代码表示可以选择只取前10000个元素
trainja = df[3].values.tolist()#[:10000]
# trainen.pop(5972)
# trainja.pop(5972)
准备分词器
准备和初始化两个SentencePieceProcessor对象,分别用于英文和日文的分词处理。这些对象利用SentencePiece技术,通过JParaCrawl提供的模型文件进行分词。SentencePiece是一个开源的分词库,适用于多种语言,包括没有明显单词分隔的日语。在提供的代码中,英文和日文的分词器分别使用不同的模型文件,并且都指定了nopretok参数,意味着在分词时不进行预分词处理。
# 初始化一个SentencePieceProcessor对象,用于英文分词
# 指定模型文件为'enja_spm_models/spm.en.nopretok.model',该模型用于英文分词处理
# nopretok表示模型不使用预分词
en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
# 初始化一个SentencePieceProcessor对象,用于日文分词
# 指定模型文件为'enja_spm_models/spm.ja.nopretok.model',该模型用于日文分词处理
# 同样,nopretok表示模型不使用预分词
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.ja.nopretok.model')
构建词汇表
使用TorchText库中的Vocab对象来构建词汇表,并将句子转换为Torch张量。
- 定义了一个名为build_vocab的函数,它接受句子列表和分词器作为参数,使用分词器对句子进行编码,然后利用collections.Counter统计词频,并创建一个包含特殊标记(如未知词标记<unk>,填充标记<pad>,句子开始标记<bos>和句子结束标记<eos>)的Vocab对象。
- 使用build_vocab函数分别构建日语和英语的词汇表,分别使用日语句子列表trainja和日语分词器ja_tokenizer,以及英语句子列表trainen和英语分词器en_tokenizer。
- 定义了一个名为data_process的函数,它接收日语和英语的句子对作为输入,使用相应的词汇表和分词器将句子转换为Torch的长整型张量(torch.long),并返回一个包含这些张量对的数据列表。
- 调用data_process函数,传入日语和英语的句子列表trainja和trainen,得到处理后的数据,存储在train_data变量中。
# 定义一个函数build_vocab,用于构建词汇表
# 参数sentences是待处理的句子列表
# 参数tokenizer是用于分词的SentencePieceProcessor对象
def build_vocab(sentences, tokenizer):
# 使用collections模块中的Counter类来统计词频
counter = Counter()
# 遍历所有句子
for sentence in sentences:
# 使用tokenizer的encode方法对句子进行编码,out_type=str表示编码结果为字符串
# 将编码结果更新到词频计数器counter中
counter.update(tokenizer.encode(sentence, out_type=str))
# 创建一个Vocab对象,传入词频计数器counter和特殊标记列表
return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用build_vocab函数构建日文词汇表,传入日文句子列表trainja和日文分词器ja_tokenizer
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 使用build_vocab函数构建英文词汇表,传入英文句子列表trainen和英文分词器en_tokenizer
en_vocab = build_vocab(trainen, en_tokenizer)
在我们有了词汇表对象之后,我们可以使用词汇表和标记器对象来为我们的训练数据构建张量。
# 定义一个函数data_process,用于处理日语和英语的句子对
def data_process(ja, en):
# 初始化一个空列表,用于存储处理后的数据
data = []
# 使用zip函数将两个列表中的元素配对,并遍历这些配对
for (raw_ja, raw_en) in zip(ja, en):
# 对日语句子进行分词,去除尾部的换行符,然后将分词结果转换为tensor
# 使用ja_vocab中的词索引来构建tensor,dtype设置为torch.long
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)
# 将处理后的日语和英语tensor作为元组添加到data列表中
data.append((ja_tensor_, en_tensor_))
# 返回处理后的数据列表
return data
# 调用data_process函数,传入日语和英语句子列表trainja和trainen
# 处理后的数据存储在train_data变量中
train_data = data_process(trainja, trainen)
处理数据
将句子转换为张量,并创建DataLoader对象以供训练时迭代使用。
# 每个批次的样本数量
BATCH_SIZE = 8
# 从日语词汇表中获取'<pad>'标记对应的索引,用于填充序列
PAD_IDX = ja_vocab['<pad>']
# 从日语词汇表中获取'<bos>'标记对应的索引,表示句子的开始
BOS_IDX = ja_vocab['<bos>']
# 从日语词汇表中获取'<eos>'标记对应的索引,表示句子的结束
EOS_IDX = ja_vocab['<eos>']
# 定义一个函数generate_batch,用于生成批次数据
# 参数data_batch是当前批次的样本数据
def generate_batch(data_batch):
# 初始化两个空列表,用于存储处理后的日语和英语批次数据
ja_batch, en_batch = [], []
# 遍历data_batch中的每个样本
for (ja_item, en_item) in data_batch:
# 将'<bos>'和'<eos>'标记添加到日语样本的开始和末尾,并转换为tensor
# 然后添加到ja_batch列表中
ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
# 对英语样本执行相同的操作,并添加到en_batch列表中
en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
# 使用pad_sequence函数对ja_batch中的序列进行填充,使它们长度一致
# padding_value设置为PAD_IDX,即'<pad>'标记的索引
ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
# 对en_batch执行相同的操作
en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
# 返回处理后的日语和英语批次数据
return ja_batch, en_batch
# 使用DataLoader类创建一个迭代器train_iter
# 参数train_data是训练数据
# 参数batch_size设置为BATCH_SIZE,即每个批次的样本数量
# 参数shuffle设置为True,表示在每个epoch开始时打乱数据
# 参数collate_fn设置为generate_batch,表示如何生成批次数据
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
shuffle=True, collate_fn=generate_batch)
定义序列到序列的Transformer模型
包括编码器、解码器、位置编码和掩码。
这种模型最初在“Attention is all you need”论文中提出,用于解决机器翻译任务。Transformer模型由编码器(Encoder)和解码器(Decoder)组成,每个部分都包含固定数量的层。
模型结构
编码器通过多头部注意力(Multi-head Attention)和前馈网络层处理输入序列,生成的输出被称为“记忆”(Memory),随后传递给解码器。解码器结合目标张量和记忆进行进一步处理。编码器和解码器采用端到端的方式进行训练,使用教师强制(Teacher Forcing)技术。
以下是Seq2SeqTransformer模型的Python代码实现,其中BATCH_SIZE和词汇表de_vocab被修改为ja_vocab。
from torch.nn import (TransformerEncoder, TransformerDecoder,
TransformerEncoderLayer, TransformerDecoderLayer)
# 定义Seq2SeqTransformer类,它是一个序列到序列的Transformer模型,继承自nn.Module
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编码器,包含num_encoder_layers层
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解码器,包含num_decoder_layers层
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):
# 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)
# 创建位置索引,从0到maxlen-1
pos = torch.arange(0, maxlen).reshape(maxlen, 1)
# 初始化位置编码矩阵,大小为(maxlen, emb_size)
pos_embedding = torch.zeros((maxlen, emb_size))
# 利用正弦函数生成0, 2, ... 位置的编码
pos_embedding[:, 0::2] = torch.sin(pos * den)
# 利用余弦函数生成1, 3, ... 位置的编码
pos_embedding[:, 1::2] = torch.cos(pos * den)
# 增加维度以适配后续操作
pos_embedding = pos_embedding.unsqueeze(-2)
# 定义dropout层,用于在训练时随机丢弃一些神经元的输出
self.dropout = nn.Dropout(dropout)
# 注册pos_embedding为模型的缓冲区,不参与梯度计算
self.register_buffer('pos_embedding', pos_embedding)
# 前向传播函数,将位置编码与token_embedding相加
def forward(self, token_embedding: Tensor):
return self.dropout(token_embedding +
self.pos_embedding[:token_embedding.size(0),:])
class TokenEmbedding(nn.Module):
# TokenEmbedding类继承自nn.Module,用于生成token的嵌入表示
def __init__(self, vocab_size: int, emb_size):
super(TokenEmbedding, self).__init__()
# 初始化一个嵌入层,用于将词汇表中的每个词映射到一个emb_size维的向量
self.embedding = nn.Embedding(vocab_size, emb_size)
self.emb_size = emb_size # 保存嵌入向量的维度
# 前向传播函数,根据输入的tokens生成其嵌入表示
def forward(self, tokens: Tensor):
# 调用嵌入层,并将结果乘以sqrt(emb_size)进行缩放
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
掩码生成
为了防止目标词在自注意力机制中关注到它后面的词,我们创建了一个后续掩码。同时,我们还为源和目标填充标记创建了掩码。
def generate_square_subsequent_mask(sz):
# 使用torch.triu创建上三角矩阵,然后与1比较生成一个上三角的布尔矩阵
mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
# 将布尔矩阵转换为浮点数,然后使用masked_fill方法将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)
# 为源序列创建一个全0的布尔掩码矩阵
src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
# 根据源序列的填充索引创建填充掩码
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的个人电脑上运行此代码。且定义训练一个epoch的函数和评估模型性能的函数,通过这些函数可以对模型进行训练和评估。
模型训练
定义训练循环,包括前向传播、损失计算和参数更新。最后,在准备好必要的类和函数之后,我们准备训练我们的模型。这是不言而喻的,但是完成训练所需的时间可能会有很大的不同,这取决于很多事情,比如计算能力、参数和数据集的大小。当我使用JParaCrawl(每种语言大约有590万个句子)的完整句子列表来训练模型时,使用单个NVIDIA GeForce RTX 3070 GPU,每个epoch大约需要5个小时。
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
# 记录当前epoch开始的时间
start_time = time.time()
# 调用train_epoch函数进行一个epoch的训练,并获取训练损失
train_loss = train_epoch(transformer, train_iter, optimizer)
# 记录当前epoch结束的时间
end_time = time.time()
# 打印当前epoch的编号、训练损失以及该epoch的训练时间
print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
f"Epoch time = {(end_time - start_time):.3f}s"))
翻译新句子
实现翻译函数,使用训练好的模型进行翻译。首先,我们创建翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理,然后将结果解码回句子,但这次是英语。
# 定义贪心解码函数,用于生成翻译结果
def greedy_decode(model, src, src_mask, max_len, start_symbol):
# 将源语言数据和掩码移动到指定设备(CPU或GPU)
src = src.to(device)
src_mask = src_mask.to(device)
# 使用模型的encode部分对源语言数据进行编码
memory = model.encode(src, src_mask)
# 初始化解码序列,以开始符号start_symbol开始
ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
for i in range(max_len-1): # 循环直到达到最大长度减1
# 将memory数据移动到指定设备
memory = memory.to(device)
# 创建目标掩码,防止解码时的未对齐问题
memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)
# 生成解码序列的上三角后续掩码
tgt_mask = (generate_square_subsequent_mask(ys.size(0))
.type(torch.bool)).to(device)
# 使用模型的decode部分进行解码
out = model.decode(ys, memory, tgt_mask)
# 转置输出矩阵,以便于后续处理
out = out.transpose(0, 1)
# 通过模型的generator获取下一个词的概率分布
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.eval()
# 对源语言文本进行分词,并添加开始和结束符号
tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)]+ [EOS_IDX]
# 计算tokens的数量
num_tokens = len(tokens)
# 将tokens转换为张量,并重塑为所需的形状
src = (torch.LongTensor(tokens).reshape(num_tokens, 1) )
# 创建源掩码
src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)
# 使用greedy_decode函数进行解码
tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()
# 将解码得到的tokens转换为目标语言的文本
return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")
保存模型和词汇表
将训练好的模型和构建的词汇表保存到文件。在训练完成后,我们通常会将词汇表对象(例如en_vocab和ja_vocab)保存起来,以备后用。这可以通过Python的pickle模块实现。以下是使用pickle保存词汇表对象的示例代码:
import pickle
# 打开文件,用于存储数据
with open('en_vocab.pkl', 'wb') as file:
# 将信息dump到文件中
pickle.dump(en_vocab, file)
with open('ja_vocab.pkl', 'wb') as file:
pickle.dump(ja_vocab, file)
此外,我们还可以利用PyTorch的保存和加载函数来保存模型,以便将来使用。通常,根据我们将来使用模型的目的,有几种不同的保存方式。以下是两种常见的保存模型的方法:
- 仅用于推理(Inference Only):我们可以保存模型的状态字典,以后加载模型时仅用于翻译(例如从日语翻译到英语)。
# 保存模型,仅用于推理 torch.save(transformer.state_dict(), 'inference_model.pth') - 用于推理和恢复训练:这种方式不仅用于推理,还可以在将来加载模型时恢复训练。
# 保存模型和检查点,以便以后恢复训练 torch.save({ 'epoch': NUM_EPOCHS, 'model_state_dict': transformer.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': train_loss, # 这里假设train_loss是训练过程中记录的损失值 }, 'model_checkpoint.tar')请注意,
torch.save函数默认使用二进制格式保存数据,这使得加载速度快且数据紧凑。在保存模型状态字典时,推荐使用.pth或.pt作为文件扩展名。而在保存检查点时,通常使用.tar作为文件扩展名。
总结
在本文中,我们深入探讨了机器翻译技术的发展,特别是Transformer模型在中日语翻译中的应用和实现。从早期的基于规则和统计的方法,到循环神经网络(RNN)的编码器—解码器架构,再到引入自注意力机制的Transformer模型,我们见证了机器翻译技术的巨大进步。
技术发展
- 早期方法:依赖于规则和统计,处理复杂语言结构时存在局限。
- RNN和编码器—解码器架构:引入序列到序列转换,显著提高翻译准确性。
- Transformer模型:通过自注意力机制,有效处理长距离依赖,提高训练效率。
Transformer模型核心
- 编码器和解码器:由多个层堆叠而成,包含多头自注意力和前馈网络。
- 自注意力机制:允许模型捕捉序列中任意两个位置之间的依赖。
- 位置编码:提供序列中单词的位置信息,解决Transformer缺乏序列顺序的固有表示问题。
- 优化和训练:使用交叉熵损失和Adam优化器,辅以梯度裁剪。
实现细节
- 数据预处理:包括读取数据、构建词汇表、特殊标记定义。
- 模型定义:实现Seq2SeqTransformer类,包括编码器、解码器、位置编码和掩码生成。
- 训练过程:通过定义`train_epoch`和`evaluate`函数,实现模型的训练和评估。
- 预测与翻译:使用贪婪搜索策略生成翻译结果,并计算BLEU分数评估翻译质量。
Transformer模型凭借其创新的架构和自注意力机制,在机器翻译领域取得了突破性成果。本文的实现细节和实战案例展示了如何使用PyTorch和Torchtext构建高效的日英机器翻译模型,证明了Transformer模型在处理复杂NLP任务中的潜力。
参考文献
1. Vaswani, A., et al. (2017). Attention is all you need. In Advances in Neural Information Processing Systems (pp. 5998-6008).
2. Sutskever, I., et al. (2014). Sequence to sequence learning with neural networks. In Advances in Neural Information Processing Systems (pp. 3104-3112).
3. Cho, K., et al. (2014). Learning phrase representations using RNN encoder-decoder for statistical machine translation. arXiv preprint arXiv:1406.1078.
4. Luong, M. T., et al. (2015). Effective approaches to attention-based neural machine translation. In EMNLP.
通过本文的分析和实现,我们不仅理解了Transformer模型的工作原理,还掌握了如何将其应用于实际的机器翻译任务中。随着技术的不断进步,我们期待Transformer模型在未来的NLP领域中发挥更大的作用。
更多推荐


所有评论(0)