一、什么是机器翻译?
机器翻译是指使用计算机程序将一种语言的文本自动翻译成另一种语言的文本。这是自然语言处理领域的一个重要研究方向,也是实际应用中非常有价值的技术之一。
在机器翻译中,通常会使用深度学习模型,如神经网络和注意力机制,来实现从一个语言到另一个语言的翻译。其中,编码器-解码器结构是一种常见的模型架构,它由两部分组成:编码器负责将源语言文本编码成一个语义表示,解码器则将这个语义表示解码成目标语言的文本。本文就会以编码器-解码器为例来对机器翻译进行研究。
总的来说,机器翻译是一项复杂而富有挑战的任务,涉及到多个领域的知识和技术,包括自然语言处理、深度学习、并行计算等。通过不断的研究和实践,机器翻译技术在实际应用中已经取得了许多进展,为跨语言沟通提供了便利和支持。
二、模型介绍——含注意力机制的编码器-解码器
带有注意力机制的编码器-解码器(Encoder-Decoder with Attention)是一种常用于机器翻译等任务的深度学习模型结构。它通过编码器将输入序列编码成一个语义表示,并通过解码器将这个语义表示解码成目标序列。同时,注意力机制可以帮助模型在解码时更好地关注输入序列中与当前位置相关的部分,提升翻译的准确性和流畅性。
1.注意力机制
注意力机制(Attention Mechanism)是一种用于深度学习模型的技术,旨在让模型能够在处理序列数据时,动态地关注输入序列中与当前任务相关的部分,从而提高模型对输入的理解和处理能力。
在自然语言处理任务中,特别是在机器翻译领域,注意力机制被广泛应用。传统的编码器-解码器模型在处理长序列时可能会出现信息丢失或者模糊的问题,因为编码器生成的固定长度的上下文向量无法充分表达源语言句子的所有内容。为了解决这个问题,注意力机制被引入到编码器-解码器模型中。
在带有注意力机制的编码器-解码器模型中,解码器在生成每个目标语言单词时,都会动态地计算一个注意力分布,指示解码器应该在源语言句子的哪些部分进行"注意",以便更好地理解和翻译源语言句子。这样,模型可以在解码的过程中,根据当前的上下文和输入序列的不同部分,调整其对输入序列的关注程度,从而更准确地生成翻译结果。
常见的注意力机制包括点乘注意力(Dot-Product Attention)、缩放点乘注意力(Scaled Dot-Product Attention)、加性注意力(Additive Attention)等。这些不同类型的注意力机制在计算注意力权重时有不同的计算方式,但都能够实现模型对输入序列的动态关注。
注意力机制的引入大大提升了机器翻译等任务的性能,使得模型能够更好地处理长距离依赖关系和复杂的语言结构。除了机器翻译,注意力机制也在语音识别、文本摘要、对话生成等领域得到了广泛的应用,并成为深度学习模型中不可或缺的组成部分。
2.编码器
在深度学习中,编码器(Encoder)是一种神经网络结构,主要用于将输入数据转换成一个固定长度的表示。这种表示通常包含了输入数据的关键信息,使得后续的解码或其他处理步骤能够更容易地利用这些信息。编码器是很多序列到序列(sequence-to-sequence)模型的关键组件,尤其是在自然语言处理(NLP)任务中。
2.1基本原理
输入嵌入层(Embedding Layer):
首先,将输入序列中的每个词(或字符)转换为一个密集的向量表示。这通常使用预训练的词向量(如Word2Vec、GloVe)或者通过嵌入层随机初始化并在训练过程中进行优化。
编码器层(Encoder Layers):
由多层神经网络组成,常见的选择有循环神经网络(RNN)、长短期记忆网络(LSTM)、门控循环单元(GRU)以及基于自注意力机制的Transformer编码器。每一层都会对输入的向量序列进行处理,提取出更高级别的特征。
上下文向量(Context Vector):
将处理后的特征序列压缩(如取最后一个时间步的输出,或通过某种聚合操作)成一个固定长度的向量,这个向量作为整个输入序列的语义表示
2.2常见架构
循环神经网络(RNN):
RNN是一种经典的序列处理模型,通过隐状态来捕捉序列中的时间依赖信息。
长短期记忆网络(LSTM)和门控循环单元(GRU):
LSTM和GRU是RNN的改进版本,解决了传统RNN在处理长期依赖问题时的梯度消失问题。它们通过引入门控机制,可以更有效地捕捉和保留长距离的依赖关系。
Transformer编码器:
Transformer模型采用自注意力机制而不是递归结构来处理序列信息。每一个词在计算自己的表示时都可以直接访问整个输入序列,从而更好地捕捉全局依赖关系。Transformer编码器由多层堆叠的自注意力和前馈神经网络组成,每一层都对输入序列进行处理和转化。
2.3应用
机器翻译:在机器翻译系统中,编码器将源语言句子编码成上下文向量,然后解码器基于这个向量生成目标语言句子。
文本摘要:编码器将长文档压缩成一个固定长度的表示,然后解码器基于这个表示生成摘要。
语音识别:编码器将音频信号转换成特征向量,然后解码器将这些特征向量转换成文本。
3.解码器
解码器(Decoder)是深度学习模型中的另一个重要组件,通常与编码器一起构成序列到序列(sequence-to-sequence)模型。解码器的作用是将编码器生成的表示(通常是一个固定长度的向量)转换为目标序列或其他形式的输出。在很多任务中,如机器翻译、文本摘要、对话生成等,解码器扮演着至关重要的角色。
3.1基本原理
初始状态(Initial State):
解码器需要一个初始的隐藏状态向量,这通常是由编码器生成的上下文向量。
目标嵌入层(Target Embedding Layer):
与编码器类似,解码器通常使用嵌入层来将目标序列中的词(或字符)转换为密集的向量表示。
解码器层(Decoder Layers):
也由多个神经网络层组成,可以是循环神经网络(RNN)、长短期记忆网络(LSTM)、门控循环单元(GRU)或者基于自注意力机制的Transformer解码器。每一层都会根据前一个时间步的输出以及上一个隐藏状态来生成新的隐藏状态和输出。
输出层(Output Layer):
解码器的最后一层通常是一个全连接层,用于将最终的隐藏状态映射为目标序列中每个词的概率分布,通常使用softmax函数进行归一化。
3.2常见架构
基于RNN的解码器:
可以使用简单的循环神经网络(RNN)作为解码器的主体结构,接受编码器生成的上下文向量作为初始隐藏状态。
基于注意力机制的解码器:
在面对长序列和全局信息对解码有重要影响的任务中,通常会采用基于注意力机制的解码器,例如使用Bahdanau注意力或Luong注意力机制。
Transformer解码器:
Transformer解码器与编码器类似,采用自注意力机制来处理输入序列和输出序列的关系,同时通过位置嵌入来保留序列的顺序信息。
三、读取和预处理数据
我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束。
接着定义两个辅助函数对后面读取的数据进行预处理。
import collections
import os
import io
import math
import torch
from torch import nn
import torch.nn.functional as F
import torchtext.vocab as Vocab
import torch.utils.data as Data
import sys
# sys.path.append("..")
import d2lzh_pytorch as d2l
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'#定义了三个变量,分别是PAD、BOS和EOS,它们分别表示填充、起始和结束标记
os.environ["CUDA_VISIBLE_DEVICES"] = "0"#设置了环境变量CUDA_VISIBLE_DEVICES的值为"0"。这个环境变量通常用于指定程序在运行时可以使用的GPU设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')#这行代码创建了一个torch.device对象,用于指定张量的计算设备(GPU或CPU)。它首先检查torch.cuda.is_available()的返回值,如果返回True,表示当前环境支持CUDA(即有可用的GPU),则将设备设置为cuda,否则设置为cpu。
print(torch.__version__, device)
接着定义两个辅助函数(process_one_seq、build_data)对后面读取的数据进行预处理。
# 将一个序列中所有的词记录在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)# 将当前序列的所有词添加到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):
vocab = Vocab.Vocab(collections.Counter(all_tokens),
specials=[PAD, BOS, EOS])# 使用 collections.Counter 统计所有词的出现次数,并通过 Vocab 类构建词典 vocab,其中包括特殊标记 [PAD, BOS, EOS]
indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]# 将所有序列中的词转换为对应的索引,构成索引序列 indices
return vocab, torch.tensor(indices)# 返回构建的词典 vocab 和转换为张量后的索引 indices
为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为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:#打开文件 fr-en-small.txt,使用 io.open 是为了在读取文件时处理字符编码
lines = f.readlines()# 读取文件所有行,并存储在 lines 列表中
for line in lines:# 遍历每一行数据
in_seq, out_seq = line.rstrip().split('\t')# 去除行末尾的空白符并按制表符分割,得到输入序列 in_seq 和输出序列 out_seq
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 函数处理输入序列,并将结果存储在 in_tokens 和 in_seqs 中
process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)# 类似地,处理输出序列,并将结果存储在 out_tokens 和 out_seqs 中
in_vocab, in_data = build_data(in_tokens, in_seqs)# 调用 build_data 函数构建输入数据的词典和数据张量
out_vocab, out_data = build_data(out_tokens, out_seqs)# 类似地,构建输出数据的词典和数据张量
return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)# 返回构建好的输入词典 in_vocab、输出词典 out_vocab,以及使用 torch.utils.data.TensorDataset 封装的输入和输出数据张量
四、训练模型
我们先实现batch_loss
函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符BOS
。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。此外,使用掩码变量避免填充项对损失函数计算的影响。
def batch_loss(encoder, decoder, X, Y, loss):
batch_size = X.shape[0]
enc_state = encoder.begin_state()
enc_outputs, enc_state = encoder(X, enc_state)
# 初始化解码器的隐藏状态
dec_state = decoder.begin_state(enc_state)
# 解码器在最初时间步的输入是BOS
dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
# 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
mask, num_not_pad_tokens = torch.ones(batch_size,), 0
l = torch.tensor([0.0])
for y in Y.permute(1,0): # Y shape: (batch, seq_len)
dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
l = l + (mask * loss(dec_output, y)).sum()
dec_input = y # 使用强制教学
num_not_pad_tokens += mask.sum().item()
# EOS后面全是PAD. 下面一行保证一旦遇到EOS接下来的循环中mask就一直是0
mask = mask * (y != out_vocab.stoi[EOS]).float()
return l / num_not_pad_tokens
在训练函数中,我们需要同时迭代编码器和解码器的模型参数。
def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
# 创建编码器和解码器的优化器,使用Adam优化器,学习率为lr
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')
# 创建数据迭代器,用于批量加载数据集,每次加载batch_size大小的数据进行训练
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
for epoch in range(num_epochs):
l_sum = 0.0 # 用于累计每个epoch的总损失
# 遍历数据迭代器,每次加载一个batch的数据进行训练
for X, Y in data_iter:
enc_optimizer.zero_grad() # 清零编码器优化器的梯度
dec_optimizer.zero_grad() # 清零解码器优化器的梯度
l = batch_loss(encoder, decoder, X, Y, loss) # 计算当前batch的损失
l.backward() # 反向传播计算梯度
# 使用编码器和解码器的优化器更新参数
enc_optimizer.step()
dec_optimizer.step()
# 累加当前batch的损失值
l_sum += l.item()
# 每训练完一个epoch,每10个epoch打印一次当前的平均损失
if (epoch + 1) % 10 == 0:
print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))
接下来,创建模型实例并设置超参数。然后,我们就可以训练模型了。
embed_size, num_hiddens, num_layers = 64, 64, 2 # 设置了嵌入大小 (embed_size) 为 64,隐藏单元数 (num_hiddens) 为 64,层数 (num_layers) 为 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50 # 设置了注意力大小 (attention_size) 为 10,dropout概率 (drop_prob) 为 0.5,学习率 (lr) 为 0.01,批量大小 (batch_size) 为 2,训练轮数 (num_epochs) 为 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)# 开始训练
五、预测不定长的序列
贪婪搜索生成解码器在每个时间步的输出。
def translate(encoder, decoder, input_seq, max_seq_len):
# 将输入序列按空格分割成单词,并加入结束标记 EOS 和填充标记 PAD,以便构成指定长度的输入序列
in_tokens = input_seq.split(' ')
in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
# 将输入序列转换为对应的索引序列,并构造成模型所需的张量格式,这里假设 batch 大小为 1
enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]]) # batch=1
enc_state = encoder.begin_state() # 编码器初始状态
enc_output, enc_state = encoder(enc_input, enc_state) # 编码器处理输入序列,得到编码器输出和最终状态
dec_input = torch.tensor([out_vocab.stoi[BOS]]) # 解码器初始输入为起始标记 BOS
dec_state = decoder.begin_state(enc_state) # 解码器初始状态为编码器的最终状态
output_tokens = [] # 存储模型预测的输出 token 序列
for _ in range(max_seq_len): # 迭代生成输出序列的每一个 token
# 解码器生成输出和更新状态
dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
# 预测出最有可能的 token
pred = dec_output.argmax(dim=1)
pred_token = out_vocab.itos[int(pred.item())]
if pred_token == EOS: # 当任一时间步搜索出EOS时,输出序列即完成
break
else:
output_tokens.append(pred_token)
dec_input = pred # 将当前预测的 token 作为下一时刻的输入
return output_tokens
六、评价翻译结果(BLEU)
评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。
具体来说,设词数为𝑛𝑛的子序列的精度为𝑝𝑛𝑝𝑛。它是预测序列与标签序列匹配词数为𝑛𝑛的子序列的数量与预测序列中词数为𝑛𝑛的子序列的数量之比。因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。
下面实现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))
# 计算 n-gram 的匹配数量和参考序列的 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
# 计算 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 分数和模型生成的序列
print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
' '.join(pred_tokens)))
预测正确则分数为1。