在当今信息全球化的背景下,语言障碍的克服变得尤为重要。机器翻译,它将一段文本从一种语言自动翻译到另一种语言。作为连接不同语言和文化的桥梁,其发展受到了前所未有的关注。在本次实验中,我们将重点探讨如何利用序列到序列(seq2seq)模型来实现机器翻译。并通过引入注意力机制,使模型能够更好地捕捉源语言和目标语言之间的复杂映射关系,从而提高翻译的准确性和流畅性。
一、实验原理
1.1、机器翻译
机器翻译,作为自然语言处理的一个核心领域,一直都是研究者们关注的焦点。其目标是实现计算机自动将一种语言翻译成另一种语言,而不需要人类的参与。它使用特定的算法和模型,尝试在不同语言之间实现最佳的语义映射。
机器翻译的核心是翻译模型,它可以基于规则、基于统计或基于神经网络。这些模型都试图找到最佳的翻译,但它们的工作原理和侧重点有所不同。
1.1.1基于规则的机器翻译 (RBMT)
基于规则的机器翻译(RBMT)是一种利用语言学规则将源语言文本转换为目标语言文本的技术。这些规则通常由语言学家手工编写,覆盖了语法、词汇和其他语言相关的特性。
1.1.2基于统计的机器翻译 (SMT)
基于统计的机器翻译 (SMT) 利用统计模型从大量双语文本数据中学习如何将源语言翻译为目标语言。与依赖语言学家手工编写规则的RBMT不同,SMT自动从数据中学习翻译规则和模式。
1.1.3基于神经网络的机器翻译
基于神经网络的机器翻译(NMT)使用深度学习技术,特别是递归神经网络(RNN)、长短时记忆网络(LSTM)或Transformer结构,以端到端的方式进行翻译。它直接从源语言到目标语言的句子或序列进行映射,不需要复杂的特性工程或中间步骤。也是本文中重点讲解的技术。
1.2、编码器—解码器(seq2seq)
在自然语言处理的很多应用中,输入和输出都可以是不定长序列。以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列。当输入和输出都是不定长序列时,我们可以使用编码器—解码器(encoder-decoder)。Encoder–Decoder是一种框架,许多算法中都有该种框架,在这个框架下可以使用不同的算法来解决不同的任务。
seq2seq模型属于encoder-decoder框架的范围,Seq2Seq 强调目的,不特指具体方法,满足输入序列,输出序列的目的,都可以统称为 Seq2Seq 模型。模型本质上用到了两个循环神经网络,分别叫做编码器和解码器。前者负责把序列编码成一个固定长度的向量,这个向量作为输入传给后者,输出可变长度的向量。
在Decoder中,每一时刻的输入为Encoder输出的c和Decoder前一时刻的输出,还有前一时刻预测的词向量
(如果是预测第一个词的话,此时输入的词向量为“_GO”的词向量,标志着解码的开始),用g函数表达解码器的隐藏层变换,即:
直到解码解出“_EOS”,标志着解码的结束。
1.2.1编码器
编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量𝑐,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。
让我们考虑批量大小为1的时序数据样本。假设用表示转换成词向量的输入,例如
是输入句子中的第𝑖个词。在时间步𝑡,循环神经网络将输入
的特征向量
和上个时间步的隐藏状态
变换为当前时间步的隐藏状态
。我们可以用函数𝑓表达循环神经网络隐藏层的变换:
接下来,编码器通过自定义函数𝑞将各个时间步的隐藏状态变换为背景变量
例如,当选择时,背景变量是输入序列最终时间步的隐藏状态
。
就相当于从输入中提取出来大概意思,包含了输入的含义。
以上描述的编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,编码器每个时间步的隐藏状态同时取决于该时间步之前和之后的子序列(包括当前时间步的输入),并编码了整个序列的信息。
1.2.2解码器
刚刚已经介绍,编码器输出的背景变量𝑐编码了整个输入序列的信息。给定训练样本中的输出序列
,对每个时间步𝑡′(符号与输入序列或编码器的时间步𝑡有区别),解码器输出𝑦𝑡′的条件概率将基于之前的输出序列
和背景变量𝑐,即
。
为此,我们可以使用另一个循环神经网络作为解码器。在输出序列的时间步𝑡′,解码器将上一时间步的输出以及背景变量𝑐作为输入,并将它们与上一时间步的隐藏状态
变换为当前时间步的隐藏状态
。因此,我们可以用函数𝑔表达解码器隐藏层的变换:
有了解码器的隐藏状态后,我们可以使用自定义的输出层和softmax运算来计算,例如,基于当前时间步的解码器隐藏状态
、上一时间步的输出
以及背景变量𝑐来计算当前时间步输出
的概率分布。
直到解码解出“_EOS”,标志着解码的结束。
1.2.3训练模型
根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率
并得到该输出序列的损失
在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。在图10.8所描述的模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上一个时间步的标签作为解码器在当前时间步的输入。这叫作强制教学(teacher forcing)。
1.3、注意力机制
深度学习中的注意力机制正是借鉴了人类视觉的注意力思维方式。一般来说,人类在观察外界环境时会迅速的扫描全景,然后根据大脑信号的处理快速的锁定重点关注的目标区域,最终形成注意力焦点。该机制可以帮助人类在有限的资源下,从大量无关背景区域中筛选出具有重要价值信息的目标区域,帮助人类更加高效的处理视觉信息。
在机器翻译中,注意力机制允许模型在解码时“关注”源句子中的不同部分。这使得翻译更加准确,尤其是对于长句子。下面将着重介绍基于Encoder-Decoder
的注意力机制。
1.3.1基于Encoder-Decoder
的注意力机制
人类视觉注意力机制,在处理信息时注意力的分布是不一样的。而 Encoder-Decoder
框架将输入X都编码转化为语义表示C,这样会导致所有输入的处理权重都一样,没有体现出注意力集中。因此,也可看成是“分心模型”。
为了能体现注意力机制,将语义表示C进行扩展,用不同的C来表示不同注意力的集中程度,每个C的权重不一样。扩展后的 Encoder-Decoder
框架变为:
下面通过一个英文翻译成中文的例子说明“注意力机制”:
例如,输入的英文句子是:Tom chase Jerry
,目标的翻译结果是:”汤姆追逐杰瑞”。那么在语言翻译中,Tom,chase,Jerry这三个词对翻译结果的影响程度是不同的。其中,Tom是主语,Jerry是宾语,是两个人名,chase是谓语,是动词,这三个词的影响程度大小顺序分别是Jerry>Tom>chase,例如(Tom,0.3),(chase,0.2),(Jerry,0.5)。不同的影响程度代表模型在翻译时分配给不同单词的注意力大小,即分配的概率大小。
生成目标句子单词的过程,计算形式如下:
其中,f1是 Decoder
的非线性变换函数。每个 C i C_iCi 对应不同单词的注意力分配概率分布,计算形式如:
其中函数表示
Encoder
节点中对输入英文单词的转换函数,g函数表示 Encoder
合成整个句子中间语义表示的变换函数,一般采用加权求和的方式,如下式:
其中,表示权重,
表示
Encoder
的转换函数,即 h1 = f2("Tom"), h2 = f2("chase"), h3 = f2("Jerry")
,表示输入句子的长度。
当i是“汤姆”时,则注意力模型权重 分别是0.6,0.2,0.2。那么这个权重是如何得到的呢?
可以看做是一个概率,反映了
对
的重要性,可使用softmax来表示:
其中, ,这里的f表示一个匹配度的打分函数,可以是一个简单的相似度计算,也可以是一个复杂的神经网络计算结果。在这里,由于在计算
时还没有
,因此使用最接近的
代替。当匹配度越高,则
的概率越大。因此,得出
的过程如下图:
其中,表示
Encoder
的转换函数,F(hj,Hi)
表示预测与目标的匹配打分函数。将以上过程串起来,则注意力模型的结构如下图所示:
二、实验介绍
2.1实验目的
- 使用编码器—解码器和注意力机制来实现机器翻译模型
- 使用Transformer架构和PyTorch深度学习库来实现的日中机器翻译模型
2.1GPU准备
在UCLOUD上购买云服务器
输入账户名和密码;购买后,选择图片中Ubuntu版本的系统,镜像选择图中的镜像
设置可用端口(端口号会在购买后给你)
在终端使用ssh命令访问远程计算机,其中
ubuntu
: 这是远程服务器上自己使用的用户名。@
: 用来连接用户名和服务器地址。117.50.174.158
: 远程服务器的 IP 地址。(也就在上图的“外”旁边)
ssh ubuntu@117.50.174.158
出现这个代表访问成功。
再输入下面的命令在远程服务器上运行 Jupyter Lab,ip 0.0.0.0表示能在任意端口访问
jupyter lab --ip 0.0.0.0
出现这些表示创建成功
点击生成的连接 ,输入密码(token后的内容),就可以进入界面,点击终端
输入
nvidia-smi
显示GPU配置
GPU准备完成!
三、具体代码实现
3.1使用编码器—解码器和注意力机制来实现机器翻译模型
3.1.1读取和数据预处理
读取压缩包内容
# 使用tar命令解压缩文件
!tar -xf d2lzh_pytorch.tar
首先导入Python模块和PyTorch库,
其次定义用于处理序列数据的PAD(填充)、BOS(句子开始)、EOS(句子结束)标记。
最后设置环境变量,指定GPU设备(如果可用)
import collections # 导入collections模块,提供了许多有用的容器类型
import os # 导入os模块,用于与操作系统交互
import io # 导入io模块,用于处理输入输出流
import math # 导入math模块,提供数学相关的函数
import torch # 导入PyTorch库,用于深度学习任务
from torch import nn # 从PyTorch库中导入nn模块,包含构建神经网络所需的类
import torch.nn.functional as F # 从PyTorch库中导入nn.functional模块,包含神经网络层的函数接口
import torchtext.vocab as Vocab # 从torchtext库中导入vocab模块,用于创建和管理词汇表
import torch.utils.data as Data # 从PyTorch库中导入data模块,包含数据加载和处理的工具
import sys # 导入sys模块,用于访问与Python解释器相关的变量和函数
# sys.path.append("..")
import d2lzh_pytorch as d2l # 导入d2lzh_pytorch模块,并重命名为d2l,这是一个假设的模块,可能包含深度学习相关的辅助函数或类
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>' # 定义特殊标记
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 设置环境变量
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 根据是否有可用的GPU,设置设备为'cuda'或'cpu'
print(torch.__version__, device) # 打印PyTorch的版本和正在使用的设备
process_one_seq
:处理单个序列,添加EOS和PAD标记,并将处理后的序列保存。
build_data
:使用所有词构建词典,并将序列中的词转换为索引,然后转换为PyTorch张量。
# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
all_tokens.extend(seq_tokens)
seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
all_seqs.append(seq_tokens)
# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
# 使用collections.Counter统计all_tokens中的词频,并用这些词频来创建词汇表
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张量,用于后续的模型训练
return vocab, torch.tensor(indices)
read_data:
读取数据文件,使用process_one_seq
和build_data
函数预处理数据,并返回词汇表和数据集。
# 定义read_data函数,用于读取数据,并对其进行预处理
def read_data(max_seq_len):
# in和out分别是input和output的缩写
in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
# 使用io.open打开文件'fr-en-small.txt'进行读取
with io.open('fr-en-small.txt') as f:
lines = f.readlines()
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,则忽略掉此样本
# 对输入序列进行处理,包括添加EOS标记和PAD标记,并添加到全局列表
process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
# 对输出序列进行处理,同样包括添加EOS和PAD,并添加到全局列表
process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
# 使用所有输入序列的词构建词典,并转换为索引张量
in_vocab, in_data = build_data(in_tokens, in_seqs)
# 使用所有输出序列的词构建词典,并转换为索引张量
out_vocab, out_data = build_data(out_tokens, out_seqs)
# 返回构建的输入词汇表、输出词汇表和TensorDataset数据集
return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
查看dataset数据文件是否被正确读取
# 定义序列的最大长度为7
max_seq_len = 7
# 调用read_data函数读取数据,并获取词汇表和数据集
in_vocab, out_vocab, dataset = read_data(max_seq_len)
# 访问dataset对象的第一个元素
dataset[0]
读取成功!
3.1.2含注意力机制的编码器—解码器
定义编码器:Encoder
类是一个使用GRU单元的循环神经网络编码器。
class Encoder(nn.Module):
# 定义Encoder类
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层,用于处理序列数据
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
# 定义模型的前向传播
def forward(self, inputs, state):
# 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
return self.rnn(embedding, state)
# begin_state:定义或初始化RNN的初始状态
def begin_state(self):
return None
注意力机制中
attention_model
:定义一个简单的前馈神经网络作为注意力模型。
attention_forward
:执行前向传播的注意力机制,计算上下文向量。
# 定义函数attention_model:创建注意力机制模型
def attention_model(input_size, attention_size):
# 创建一个顺序模型,包含线性层、激活函数和输出层
model = nn.Sequential(
# 第一个线性层,将输入尺寸input_size的向量变换到attention_size尺寸
nn.Linear(input_size, attention_size, bias=False),
# Tanh激活函数,用于压缩输出到-1和1之间
nn.Tanh(),
# 第二个线性层,将attention_size尺寸的向量变换到尺寸1
nn.Linear(attention_size, 1, bias=False)
)
# 返回创建的注意力模型
return model
# 定义函数attention_forward用于执行前向传播的注意力机制
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) # 返回背景变量
定义解码器:Decoder
类是一个带有注意力机制的循环神经网络解码器。
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层,输入尺寸为编码器输出和嵌入层输出的和
self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens,
num_layers, dropout=drop_prob)
# 创建一个线性层,用于将GRU的输出转换为词汇表大小的输出
self.out = nn.Linear(num_hiddens, vocab_size)
def forward(self, cur_input, state, enc_states):
"""
cur_input: 当前输入词的索引,形状为(batch, )
state: 隐藏状态,形状为(num_layers, batch, num_hiddens)
enc_states: 编码器的输出状态,形状为(seq_len, batch, num_hiddens)
"""
# 使用注意力模型和编码器状态计算上下文向量c
c = attention_forward(self.attention, enc_states, state[-1])
# 将当前输入词的嵌入向量和上下文向量连结
input_and_c = torch.cat((self.embedding(cur_input), c), dim=1)
# 增加时间步维,准备输入到GRU层
output, state = self.rnn(input_and_c.unsqueeze(0), state)
# 通过输出层得到词汇表上的分数
output = self.out(output).squeeze(dim=0)
return output, state
def begin_state(self, enc_state):
# 将编码器的最终状态作为解码器的初始状态
return enc_state
3.1.3训练模型
批次损失计算
batch_loss
函数计算一个批次数据的损失值。
训练过程
train
函数负责训练编码器和解码器模型,使用交叉熵损失和Adam优化器。
# 定义函数batch_loss,用于计算一个批次数据的损失值
def batch_loss(encoder, decoder, X, Y, loss):
# 获取批次大小
batch_size = X.shape[0]
# 初始化编码器的隐藏状态
enc_state = encoder.begin_state()
# 通过编码器处理输入X,获取编码器输出和最终的隐藏状态
enc_outputs, enc_state = encoder(X, enc_state)
# 初始化解码器的隐藏状态
dec_state = decoder.begin_state(enc_state)
# 解码器在最初时间步的输入是BOS
dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
# 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
mask, num_not_pad_tokens = torch.ones(batch_size,), 0
# 初始化损失累计变量
l = torch.tensor([0.0])
# 遍历Y中的每个时间步,Y经过permute变换后形状为(seq_len, batch)
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 = 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
# 定义训练函数,用于训练编码器和解码器模型
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轮输出一次训练进度
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
# 定义注意力层的大小、dropout概率、学习率、批量大小和训练轮次
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
# 创建编码器实例,使用输入词汇表大小、词嵌入层大小、隐藏层维度、层数和dropout概率
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers, drop_prob)
# 创建解码器实例,使用输出词汇表大小、词嵌入层大小、隐藏层维度、层数、注意力层大小和dropout概率
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers, attention_size, drop_prob)
# 调用train函数训练编码器和解码器,传入模型、数据集和超参数
train(encoder, decoder, dataset, lr, batch_size, num_epochs)
训练完成!
3.1.4预测不定长的序列
translate
函数使用训练好的模型将输入序列翻译成输出序列。
# 定义translate函数,用于将输入序列翻译成输出序列
def translate(encoder, decoder, input_seq, max_seq_len):
# 将输入序列split成单独的词,并添加EOS标记
in_tokens = input_seq.split(' ')
# 确保输入序列长度符合要求,不足部分用PAD填充
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]])
# 初始化编码器的状态
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函数,传入编码器、解码器、输入序列和最大序列长度
translate(encoder, decoder, input_seq, max_seq_len)
所以ils regardent.=they are watching.
3.1.5评价翻译结果
评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。
在代码中:
bleu
函数计算两个序列之间的BLEU分数,用于评估翻译质量。
score
函数评估翻译结果,打印出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))
# 遍历不同的n-gram
for n in range(1, k + 1):
# 初始化匹配的n-gram数量和标签序列的n-gram出现次数
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分数,根据匹配的n-gram比例
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
# 返回BLEU分数
return score
# 定义score函数用于评估翻译结果并打印BLEU分数和预测序列
def score(input_seq, label_seq, k):
# 使用translate函数获取模型对输入序列的翻译结果
pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
# 将标签序列按空格分割成词列表
label_tokens = label_seq.split(' ')
# 打印出BLEU分数和预测的词序列
print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
' '.join(pred_tokens)))
使用translate
和score
函数对输入序列进行翻译,并评估翻译结果。
# 调用score函数
# input_seq: 输入序列,用于模型翻译的源文本
# label_seq: 标签序列,正确的翻译文本
# k: 在BLEU分数计算中考虑的最大n-gram长度
score('ils regardent .', 'they are watching .', k=2)
# 调用score函数
# input_seq: 输入序列,用于模型翻译的源文本
# label_seq: 标签序列,正确的翻译文本
# k: 在BLEU分数计算中考虑的最大n-gram长度
score('ils sont canadienne .', 'they are canadian .', k=2)
第一个预测正确,分数为1;而第二个预测错误,把加拿大人翻译成了俄罗斯人。
3.2使用Transformer架构和PyTorch深度学习库来实现的日中机器翻译模型
3.2.1导入依赖库
导入处理文本数据、构建神经网络、数据处理和分析、数值计算等所需的库。
为确保结果可复现,并设置使用GPU或CPU。
import math # 导入数学模块,用于数学运算
import torchtext # 导入torchtext,用于处理文本数据的库
import torch # 导入PyTorch库,用于构建和训练神经网络
import torch.nn as nn # 从PyTorch库中导入神经网络模块
from torch import Tensor # 从PyTorch中导入Tensor类,Tensor是PyTorch中的基本数据结构
from torch.nn.utils.rnn import pad_sequence # 导入pad_sequence函数,用于填充序列数据
from torch.utils.data import DataLoader # 导入DataLoader,用于加载数据集
from collections import Counter # 导入Counter,用于统计数据频率
from torchtext.vocab import Vocab # 从torchtext.vocab中导入Vocab类,用于构建词汇表
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer # 导入Transformer模型相关的类
import io # 导入io模块,用于处理输入输出流
import time # 导入time模块,用于时间相关的操作
import pandas as pd # 导入pandas库,用于数据处理和分析
import numpy as np # 导入numpy库,用于数值计算
import pickle # 导入pickle模块,用于序列化和反序列化Python对象
import tqdm # 导入tqdm模块,用于显示进度条
import sentencepiece as spm # 导入sentencepiece库,用于文本的分词
torch.manual_seed(0) # 设置随机种子,确保结果可复现
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设置设备,优先使用GPU
device
# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码
(注意!这里需要用GPU才能运行)
3.2.2数据预处理
使用pandas
读取训练数据集
# read_csv函数读取文件
# 指定分隔符为'\t',不使用文件中的表头(header),使用Python的解析引擎
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 将数据框(df)的第三列(索引为2)转换为NumPy数组,然后转换为Python列表
trainen = df[2].values.tolist()#[:10000]
# 将数据框(df)的第四列(索引为3)转换为NumPy数组,然后转换为Python列表
trainja = df[3].values.tolist()#[:10000]
# trainen.pop(5972)
# trainja.pop(5972)
# 打印出列表中索引为500的元素
print(trainen[500])
print(trainja[500])
列表前500的元素如图:
对数据集进行处理:
1、创建英文和日文的分词器实例。
2、使用分词器对句子进行编码。
# 创建分词器对象
en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
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')
# 使用日文分词器对句子进行编码
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
编码后的结果如图:
build_vocab:构建英文和日文的词汇表
data_process:构建英文和日文的数据集
# 定义build_vocab函数:用于构建词汇表
def build_vocab(sentences, tokenizer):
# 统计词频
counter = Counter()
# 遍历传入的所有句子
for sentence in sentences:
# 使用传入的tokenizer对每个句子进行编码,然后更新词频计数器
counter.update(tokenizer.encode(sentence, out_type=str))
# 使用统计得到的词频和特殊标记构建一个Vocab对象
# 特殊标记:未知词标记'<unk>', 填充标记'<pad>', 句子开始标记'<bos>'和句子结束标记'<eos>'
return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用build_vocab函数和分词器为句子构建词汇表
ja_vocab = build_vocab(trainja, ja_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):
# 对日语句子进行处理:去除末尾的换行符,使用分词器进行分词,然后根据词汇表转换为数值索引
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
# 调用data_process函数
train_data = data_process(trainja, trainen)
3.2.3创建DataLoader用于在训练过程中批量加载和处理数据
generate_batch:
用于生成训练批次的数据,包括填充和添加特殊标记。
再创建DataLoader
# 定义批量大小
BATCH_SIZE = 8
# 从日语词汇表中获取'<pad>'标记的索引,用于填充
PAD_IDX = ja_vocab['<pad>']
# 从日语词汇表中获取'<bos>'标记的索引,表示句子开始
BOS_IDX = ja_vocab['<bos>']
# 从日语词汇表中获取'<eos>'标记的索引,表示句子结束
EOS_IDX = ja_vocab['<eos>']
# 定义函数generate_batch:生成训练批次的数据
def generate_batch(data_batch):
ja_batch, en_batch = [], []
# 遍历数据批次中的每对句子
for (ja_item, en_item) in data_batch:
# 将'<bos>'和'<eos>'标记添加到句子的开始和结束,并进行拼接
ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
# 使用pad_sequence函数对日语英语批次数据进行填充,使得所有句子长度一致
ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
# 返回处理后的日语和英语批次数据
return ja_batch, en_batch
# 使用DataLoader创建一个迭代器,用于在训练过程中批量加载和处理数据
train_iter = DataLoader(
train_data, # 传入处理后的数据列表
batch_size=BATCH_SIZE, # 指定批量大小
shuffle=True, # 是否在每个epoch开始时打乱数据
collate_fn=generate_batch # 指定如何将多个数据样本合并为一个批次的函数
)
3.2.4定义模型并初始化
实现Seq2SeqTransformer
类,构建基于Transformer的编码器和解码器。
from torch.nn import (TransformerEncoder, TransformerDecoder,
TransformerEncoderLayer, TransformerDecoderLayer)
# 从torch.nn模块导入Transformer相关的类
# 定义Seq2SeqTransformer类
class Seq2SeqTransformer(nn.Module):
# 初始化父类nn.Module
def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
dim_feedforward:int = 512, dropout:float = 0.1):
super(Seq2SeqTransformer, self).__init__()
# 创建Transformer编码器层
encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
dim_feedforward=dim_feedforward)
# 创建Transformer编码器,传入编码器层和层数
self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
# 创建Transformer解码器层
decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
dim_feedforward=dim_feedforward)
# 创建Transformer解码器,传入解码器层和层数
self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
# 创建一个线性层,用于最后的输出生成
self.generator = nn.Linear(emb_size, tgt_vocab_size)
# 创建源语言的词嵌入层
self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
# 创建目标语言的词嵌入层
self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)
# 创建位置编码层,用于给模型提供位置信息
self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)
# 定义模型的前向传播过程
def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
tgt_mask: Tensor, src_padding_mask: Tensor,
tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
# 对源语言的词嵌入应用位置编码
src_emb = self.positional_encoding(self.src_tok_emb(src))
# 对目标语言的词嵌入应用位置编码
tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))
# 通过编码器处理源语言的词嵌入
memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
# 通过解码器生成输出序列
outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
tgt_padding_mask, memory_key_padding_mask)
# 通过生成器线性层得到最终的输出
return self.generator(outs)
# 定义编码函数
def encode(self, src: Tensor, src_mask: Tensor):
# 对源语言的词嵌入应用位置编码,然后通过编码器处理
return self.transformer_encoder(self.positional_encoding(
self.src_tok_emb(src)), src_mask)
# 定义解码函数
def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
# 对目标语言的词嵌入应用位置编码,然后通过解码器生成输出序列
return self.transformer_decoder(self.positional_encoding(
self.tgt_tok_emb(tgt)), memory,
tgt_mask)
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)
# 创建一个0到maxlen的整数序列
pos = torch.arange(0, maxlen).reshape(maxlen, 1)
# 初始化一个maxlen×emb_size的位置编码矩阵,初始值设为0
pos_embedding = torch.zeros((maxlen, emb_size))
# 将0, 2, 4, ... 列的编码设置为正弦函数的值
pos_embedding[:, 0::2] = torch.sin(pos * den)
# 将1, 3, 5, ... 列的编码设置为余弦函数的值
pos_embedding[:, 1::2] = torch.cos(pos * den)
# 增加维度
pos_embedding = pos_embedding.unsqueeze(-2)
# 创建Dropout层
self.dropout = nn.Dropout(dropout)
# 注册位置编码矩阵为模型的缓冲区
self.register_buffer('pos_embedding', pos_embedding)
# 定义位置编码层的前向传播函数
def forward(self, token_embedding: Tensor):
# 将位置编码添加到词嵌入上,并应用Dropout
return self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0),:])
class TokenEmbedding(nn.Module):
# 定义词嵌入类
def __init__(self, vocab_size: int, emb_size):
super(TokenEmbedding, self).__init__()
# 创建一个词嵌入层,用于将词汇表中的每个词映射到一个固定大小的向量
self.embedding = nn.Embedding(vocab_size, emb_size)
self.emb_size = emb_size # 保存词嵌入的维度
# 定义词嵌入层的前向传播函数
def forward(self, tokens: Tensor):
# 将输入的token索引转换为长整型,并通过嵌入层得到词向量,然后进行缩放
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
def generate_square_subsequent_mask(sz):
# 使用torch.triu创建一个上三角矩阵,然后通过比较生成一个上三角的掩码
mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
# 将上三角掩码中的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_seq_len = src.shape[0]
tgt_seq_len = tgt.shape[0]
# 为目标语言序列生成一个尺寸为tgt_seq_len的后续掩码
tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
# 初始化源语言序列掩码为全0,尺寸为src_seq_len x src_seq_len
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
设置模型参数的初始化,定义交叉熵损失函数和Adam优化器。
# 设置源语言和目标语言的词汇表大小
SRC_VOCAB_SIZE = len(ja_vocab)
TGT_VOCAB_SIZE = len(en_vocab)
# 设置词嵌入的维度
EMB_SIZE = 512
# 设置多头注意力机制中的头数
NHEAD = 8
# 设置前馈网络隐藏层的维度
FFN_HID_DIM = 512
# 设置训练时的批量大小
BATCH_SIZE = 16
# 设置编码器层数
NUM_ENCODER_LAYERS = 3
# 设置解码器层数
NUM_DECODER_LAYERS = 3
# 设置训练的轮数
NUM_EPOCHS = 16
# 创建Seq2SeqTransformer模型实例
transformer = Seq2SeqTransformer(
NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
FFN_HID_DIM
)
# 对模型参数进行Xavier均匀初始化
for p in transformer.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
# 将模型移动到指定的设备(我的是CPU)
transformer = transformer.to(device)
# 初始化交叉熵损失函数
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)
# 初始化Adam优化器
optimizer = torch.optim.Adam(
transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
)
def train_epoch(model, train_iter, optimizer):
# 设置模型为训练模式
model.train()
losses = 0
# 遍历训练迭代器
for idx, (src, tgt) in enumerate(train_iter):
# 将源语言和目标语言数据移动到设备
src = src.to(device)
tgt = tgt.to(device)
# 为目标语言创建除最后一个元素外的所有元素的输入序列
tgt_input = tgt[:-1, :]
# 创建掩码
src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)
# 获取模型预测的logits
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
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.2.5训练模型
定义train_epoch
函数执行单个训练周期,计算并打印训练损失。
# 遍历每个训练周期,NUM_EPOCHS定义了训练的总轮数
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
# 记录当前周期开始的时间
start_time = time.time()
# 调用train_epoch函数进行一个训练周期,并获取训练损失
train_loss = train_epoch(transformer, train_iter, optimizer)
# 记录当前周期结束的时间
end_time = time.time()
# 打印当前周期的信息(周期编号、训练损失和周期耗时)
# tqdm.tqdm:显示进度条
print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
f"Epoch time = {(end_time - start_time):.3f}s"))
训练过程如下:
3.2.6预测不定长序列
定义greedy_decode
函数进行贪心解码。
定义translate
函数,使用模型进行实际的翻译操作。
def greedy_decode(model, src, src_mask, max_len, start_symbol):
# 将源语言数据和掩码移动到设备
src = src.to(device)
src_mask = src_mask.to(device)
# 使用模型的encode方法对源语言序列进行编码
memory = model.encode(src, src_mask)
# 创建一个包含起始符号的1x1张量
ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
for i in range(max_len-1):
# 确保memory在设备上
memory = memory.to(device)
# 创建一个遮蔽张量,大小为ys的大小和memory的大小
memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)
# 生成一个后续掩码并转换为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]
num_tokens = len(tokens)
# 将索引转换为张量,并重塑形状
src = (torch.LongTensor(tokens).reshape(num_tokens, 1) )
# 初始化源掩码为全0的bool矩阵
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()
# 将解码的索引转换为目标语言的词汇
return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")
进行翻译
# 使用定义好的translate函数进行翻译操作
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
翻译结果如下图
3.2.7保存模型
使用pickle
保存词汇表,使用torch.save
保存模型的状态字典,以便进行推理。
保存模型和优化器的状态以及训练轮次和损失,以便之后可以恢复训练。
import pickle # 导入pickle模块:将Python对象序列化和反序列化
# 打开一个文件,用来存储要保存的数据
file = open('en_vocab.pkl', 'wb')
# 使用pickle.dump方法将en_vocab对象序列化并写入到文件中
pickle.dump(en_vocab, file)
# 关闭文件
file.close()
# 打开另一个文件,用来存储另一部分要保存的数据
file = open('ja_vocab.pkl', 'wb')
# 使用pickle.dump方法将ja_vocab对象序列化并写入到文件中
pickle.dump(ja_vocab, file)
# 关闭文件
file.close()
# save model for inference
# 保存模型的状态字典
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')
四、总结
本次实验深入探讨并实现了基于深度学习的机器翻译技术。基于python实现了从数据预处理到模型训练、评估、应用和保存的整个流程,实验结果表明,使用注意力机制和Transformer架构能够有效提升翻译质量。通过本实验,不仅可以理解机器翻译的原理和方法,还掌握了实际构建和应用机器翻译模型的技术。