一. 引言
1.1机器翻译的定义
机器翻译是一种利用计算机程序将文本或语音从一种自然语言转换成另一种自然语言的技术。它是计算语言学的一个分支,旨在解决跨语言交流的障碍。在深度学习和人工智能技术迅速发展的今天,基于神经网络的机器翻译已经成为了这一领域的主流方法。
在神经网络机器翻译(Neural Machine Translation, NMT)中,编码器-解码器(Encoder-Decoder)模型是最常见的架构之一。在这种架构中,编码器负责将源语言句子编码成一个固定长度的向量,这个向量包含了句子的语义信息。解码器则负责根据这个向量生成目标语言的句子。
由于源语言和目标语言的句子长度可能不同,直接使用固定长度的向量作为编码器的输出可能会损失信息。为了解决这个问题,注意力机制(Attention Mechanism)被引入到NMT中。注意力机制允许解码器在生成每个词的时候,根据当前需要“关注”源句子中的不同部分,而不是仅仅依赖于一个固定的上下文向量。这样,模型能够更有效地利用源语言句子的信息,提高翻译的准确性。
随着技术的不断进步,机器翻译系统在准确性、流畅性和速度方面都有了显著的提高,已经成为人们日常生活中不可或缺的工具之一。
1.2编码器—解码器和注意力机制的应用
编码器-解码器(Encoder-Decoder)模型和注意力机制(Attention Mechanism)是近年来在机器翻译领域中广泛应用的两种技术。它们在处理序列到序列(sequence to sequence)的任务,如机器翻译、文本摘要、问答系统等,表现出色。下面将详细介绍这两种技术的应用。
1. 编码器-解码器模型:
编码器-解码器模型是一种典型的序列到序列学习模型。它由两部分组成:编码器(Encoder)和解码器(Decoder)。
2. 注意力机制:
注意力机制是一种在编码器-解码器模型中提高翻译质量的技术。它的核心思想是在解码过程中,让解码器关注输入序列的不同部分。这样,解码器可以在生成每个词时,根据当前需要“关注”的输入序列部分来调整权重,提高翻译的准确性。
3. 应用:
在机器翻译任务中,编码器-解码器模型和注意力机制的应用如下:
(1)编码器-解码器模型:将源语言句子编码成一个固定长度的向量,然后解码成目标语言句子。这个过程实现了从源语言到目标语言的映射。
(2)注意力机制:在解码过程中,让解码器关注输入序列的不同部分,提高翻译的准确性。注意力机制使得解码器在生成每个词时,能够根据当前输出的需要,动态调整对输入序列的关注。
总之,编码器-解码器模型和注意力机制在机器翻译任务中的应用,大大提高了翻译质量。这两种技术的结合使得机器翻译系统能够更好地处理长句子和复杂结构,为跨语言交流提供了有力支持。
二,数据读取和预处理
2.1特殊符号的定义
我们先定义一些特殊符号。其中“<pad>”(padding)符号用来添加在较短序列后,直到每个序列等长,而“<bos>”和“<eos>”符号分别表示序列的开始和结束。
!tar -xf d2lzh_pytorch.tar
import collections # 用于创建特殊的数据结构,如OrderedDict、Counter等
import os # 用于操作系统相关的操作,如环境变量等
import io # 用于I/O操作,如文件读写等
import math # 用于数学运算
import torch # PyTorch框架的主要库
from torch import nn # PyTorch的神经网络模块
import torch.nn.functional as F # PyTorch提供的一些常用的函数
import torchtext.vocab as Vocab # 用于加载预训练的词向量
import torch.utils.data as Data # 用于数据加载和处理的工具
import d2lzh_pytorch as d2l # 导入自定义的辅助函数库
# 定义一些特殊符号的标记
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
# 设置环境变量,指定使用的GPU设备编号,如果没有GPU则使用CPU
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 打印PyTorch的版本和当前使用的设备
print(torch.__version__, device)
2.2辅助函数的创建
定义两个辅助函数对后面读取的数据进行预处理。
# 定义一个函数来处理一个序列,将其中的词添加到all_tokens中,
# 并将序列填充到指定长度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中
# 在序列后面添加EOS标记表示序列结束,然后使用PAD标记填充序列
# 直到序列长度达到max_seq_len
seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
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]) # 添加特殊标记到词典中
# 将每个序列中的词转换为词索引
indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
return vocab, torch.tensor(indices) # 返回构造的词典和索引化的Tensor
2.3数据样本的查看
首先读取并预处理用于机器翻译的数据集。
我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用'\t'
隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为max_seq_len
。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。
最后,设置了max_seq_len
为7,并调用了read_data
函数来读取数据。然后,它尝试访问数据集中的第一个元素以查看其内容。这通常是为了验证数据是否正确加载和预处理。
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', encoding='utf-8') 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(' ')
# 检查序列长度是否超过最大序列长度减1(因为要加上EOS标记)
if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
continue # 如果超过,则忽略该样本
# 处理输入序列并保存
process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
# 处理输出序列并保存
process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
# 构造输入和输出词典,并转换序列中的词为词索引
in_vocab, in_data = build_data(in_tokens, in_seqs)
out_vocab, out_data = build_data(out_tokens, out_seqs)
# 返回构造的词典和转换后的TensorDataset
return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
三,含注意力机制的编码器—解码器
使用含注意力机制的编码器—解码器来将一段简短的法语翻译成英语。
3.1编码器的实现
这个编码器模型将输入序列的词索引映射为嵌入向量,然后通过一个多层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) # 调用父类的初始化方法
# 定义嵌入层,将词汇表中的词映射到embedding层
self.embedding = nn.Embedding(vocab_size, embed_size)
# 定义RNN层,使用GRU,输入维度为embed_size,隐藏层维度为num_hiddens,
# 层数为num_layers,dropout概率为drop_prob
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)
def begin_state(self):
# 返回一个None值,表示在开始时不需要任何状态,
# 或者可以返回一个初始化状态的数组,取决于RNN的具体实现
return None
3.1.1编码器的前向计算
以下创建一个编码器(Encoder)实例,并使用一个模拟的小批量数据来执行前向计算。
演示了如何创建一个编码器实例,如何构造一个模拟的输入批量,以及如何执行前向计算来获取输出和最终隐藏状态。
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
# 创建一个长度为7的序列,每个元素都是零向量,
# 表示4个不同的样本,每个样本有一个长度为7的输入序列
output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
# 输出shape是(7, 4, 16),因为GRU的输出是每一时间步的隐藏状态,
# 而输入序列长度为7,批量大小为4,隐藏层大小为16
output.shape, state.shape # GRU的state是h,而LSTM的是一个元组(h, c)
![](https://img-blog.csdnimg.cn/direct/aee31633f89249ed9336bbb68cdd2d63.png)
3.2 注意力机制
接收一个输入向量,通过两个线性层和Tanh激活函数来计算注意力权重。这个权重用于在编码器-解码器模型中衡量编码器输出(即隐藏状态)对解码器当前输出的影响。注意力模型帮助解码器关注输入序列中与当前输出最相关的部分,从而提高翻译质量。
# 定义一个注意力模型
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
3.2.1注意力机制的输入和输出
定义了一个注意力机制的前向传播函数`attention_forward`,它计算编码器和解码器状态的注意力权重,并返回加权后的编码器状态。下面是注意力机制的输入和输出:
(1)输入:
1. model:这是注意力模型,通常是一个神经网络,它接收编码器和解码器状态的连结作为输入,并输出每个时间步的注意力权重。
2. enc_states:这是编码器的隐藏状态,形状为(时间步数, 批量大小, 隐藏单元个数)。它包含了整个输入序列的隐藏状态。
3. dec_state:这是解码器的当前隐藏状态,形状为(批量大小, 隐藏单元个数)。它代表了当前正在解码的输出序列的时间步的隐藏状态。
(2)输出:
1. 加权后的编码器状态:这是通过注意力机制加权后的编码器状态,形状为(批量大小, 隐藏单元个数)。它是解码器下一个时间步的输入之一,帮助解码器生成下一个输出。
注意力机制的工作流程如下:
1. 将解码器的隐藏状态`dec_state`广播到与编码器隐藏状态`enc_states`相同的形状,然后将它们在特征维度上连结。
2. 将连结后的状态输入到注意力模型`model`中,计算每个时间步的注意力权重`e`。
3. 对注意力权重`e`进行softmax运算,得到归一化的注意力权重`alpha`。
4. 将归一化的注意力权重`alpha`与编码器状态`enc_states`相乘,并在时间步维度上进行求和,得到加权后的编码器状态。
最后,调用`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) # 返回背景变量# 设置序列长度、批量大小和隐藏层大小
seq_len, batch_size, num_hiddens = 10, 4, 8
# 创建一个注意力模型,输入大小是编码器和解码器状态的总和,注意力内部层大小为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
3.3含注意力机制的解码器
解码器是序列到序列学习中的一部分,通常与编码器模型一起使用。编码器负责将输入序列(如源语言句子)编码成一个固定长度的向量,而解码器则负责将这个向量解码成目标序列(如目标语言句子)。注意力机制在这里用于帮助解码器关注输入序列中与当前输出最相关的部分,从而提高翻译质量。
它是用于序列到序列学习中的一个关键组件。解码器接收一个初始状态和一个编码器状态,并输出一个目标序列的翻译。代码中的解码器模型继承自nn.Module
,并具有以下关键组成部分
-
嵌入层:
:
nn.Embedding
:这是一个嵌入层,它将目标词汇表中的每个词映射为一个固定大小的向量。 -
注意力模型:
attention_model
:这是一个注意力模型,它计算编码器状态和解码器状态之间的注意力权重。 -
循环神经网络层:
nn.GRU
:这是一个门控循环单元(GRU)层,它用于处理序列数据。它的输入是嵌入层的输出和注意力输出的加权状态。 -
输出层:
nn.Linear
:这是一个全连接层,它将GRU的输出映射到目标词汇表的大小,即词汇表中的每个词的索引
解码器的forward
方法定义了模型的前向传播过程:
- 计算注意力权重:使用注意力模型计算编码器状态和解码器状态之间的注意力权重,并使用这些权重来加权编码器状态,得到背景向量。
- 创建输入和背景向量的连结:将嵌入后的当前输入词与背景向量在特征维度上连结。
- 执行GRU前向传播:将连结后的向量输入到GRU中,得到GRU的输出。
- 生成输出:将GRU的输出通过全连接层映射到词汇表大小,得到当前时间步的输出。
解码器的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
四. 训练模型
4.1 batch_loss函数的实现
损失函数用于计算解码器生成的翻译与真实翻译之间的差异,batch_loss
函数则用于计算一个批次数据的总损失。
通过这种方式,模型可以不断迭代,通过最小化损失函数来改进其翻译质量。batch_loss
函数是训练过程中计算损失的重要部分,它确保了模型能够有效地学习翻译任务。
这个函数接收编码器、解码器、输入数据、输出数据和损失函数作为参数,并返回一个批次的损失值。值得注意的是,我们在这里也使用掩码变量避免填充项对损失函数计算的影响。
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
4.2 训练函数的创建
在训练函数中,我们需要同时迭代编码器和解码器的模型参数。
# 定义一个训练函数
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)
# 定义损失函数为交叉熵损失,并且不进行损失缩减(即保持每个样本的损失)
loss = nn.CrossEntropyLoss(reduction='none')
# 创建数据迭代器,每次从数据集中随机抽取batch_size个样本
data_iter = DataLoader(dataset, batch_size=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)))
4.3模型的训练
创建模型实例并设置超参数。然后,我们就可以训练模型了。
# 定义嵌入层大小、隐藏层大小和层数
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
# 实例化编码器模型,输入词汇表大小为in_vocab的长度,其他参数为之前定义的 embed_size, num_hiddens, num_layers, drop_prob
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers, drop_prob)
# 实例化解码器模型,输出词汇表大小为out_vocab的长度,其他参数为之前定义的 embed_size, num_hiddens, num_layers, attention_size, drop_prob
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)
五. 预测不定长的序列
5.1贪婪搜索的实现
贪婪搜索是一种简单的解码方法,它只考虑每个时间步上最可能的词,而不考虑其他可能的词。这种方法简单但可能不够准确,因为它不考虑其他可能的词对翻译质量的影响。
功能:
- translate函数实现了机器翻译任务的贪婪搜索解码过程。
- 它接收编码器、解码器、输入序列和最大序列长度作为参数。
- 输出是翻译后的序列,它是一个单词列表。
上下文联系:
- - translate函数通常在训练序列到序列模型之后使用,用于将源语言句子翻译为目标语言句子。
- - 它依赖于编码器和解码器模型,这些模型在训练过程中被优化以提高翻译质量。
- - translate函数是机器翻译系统中的一个重要组成部分,它允许用户输入源语言句子,并得到目标语言的翻译结果。
# 定义一个翻译函数
def translate(encoder, decoder, input_seq, max_seq_len):
# 将输入序列分割成单词(假设由空格分隔)
in_tokens = input_seq.split(' ')
# 在输入序列末尾添加结束符号EOS,并使用填充符号PAD填充至最大序列长度
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 = []
# 进行最大序列长度的迭代,生成输出序列
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)
![](https://img-blog.csdnimg.cn/direct/54b7625b28d447c3a0bd902973d9eb9a.png)
六. 评价翻译结果
6.1 BLEU的计算
Bleu 全称为 Bilingual Evaluation Understudy(双语评估研究),意为双语评估替换,是衡量一个有多个正确输出结果的模型的精确度的评估指标。比较候选译文和参考译文里的 n-gram 的重合程度。多用于翻译质量评估。
可以说是所有评价指标的鼻祖,它的核心思想是比较候选译文和参考译文里的 n-gram 的重合程度,重合程度越高就认为译文质量越高。unigram用于衡量单词翻译的准确性,高阶n-gram用于衡量句子翻译的流畅性。 实践中,通常是取N=1~4,然后对进行加权平均。
BLEU 需要计算译文 1-gram,2-gram,...,N-gram 的精确率,一般 N 设置为 4 即可,公式中的 Pn 指 n-gram 的精确率。
其中,Wn 指 n-gram 的权重,一般设为均匀权重,即对于任意 n 都有 Wn = 1/N。BP 是惩罚因子,如果译文的长度小于最短的参考译文,则 BP 小于 1。lc机器翻译的长度,lr最短的参考译文的长度。
BLEU 的 1-gram 精确率表示译文忠于原文的程度,而其他 n-gram 表示翻译的流畅程度。不过BLEU对词重复和短句有着非常不好的表现,所以改进的BLEU分别使用改进的多元精度(n-gram precision) 和短句惩罚因子进行了优化。
BLEU评分的范围是从0到1。很少有翻译得分为1,除非它们与参考翻译完全相同。
示例一
candidate: paddlepaddle is a deep learning framework made by Baidu
reference: paddlepaddle is one of the products made by Baidu
就BLEU2BLEU_2BLEU2来说,对 candidate中的8个词{paddlepaddle is, is a, a deep, deep learning, learning framework, framework made, made by, by Baidu}。查找是否在reference中,发现有3个词{paddlepaddle is, made by, by Baidu}在reference中,所以占比就是3 / 8。
各个BLEU的值如下:
示例二
candidate: is is is is is is is is is
reference: paddlepaddle is one of the products made by Baidu
上一部分的计算公式中,分子的Count有个clip操作,如果没有clip,对于示例二计算出来的BLEU将是9 / 9,因为candidate中的每个词都出现在了reference中。
# 定义一个计算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):
# 初始化匹配的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):
# 如果预测序列的n-gram在标签序列中出现过,则增加匹配数量
if label_subs[''.join(pred_tokens[i: i + n])] > 0:
num_matches += 1
label_subs[''.join(pred_tokens[i: i + n])] -= 1
# 计算当前n-gram长度的精度,并更新总分数
# 精度是匹配的n-gram数量除以预测序列中可能的n-gram数量
# 分数通过几何平均的方式累积
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
# 返回最终的BLEU分数
return score
6.2 辅助打印函数的实现
# 定义一个评分函数
def score(input_seq, label_seq, k):
# 使用编码器和解码器翻译输入序列,得到预测的输出序列
pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
# 将标签序列分割成单词(假设由空格分隔)
label_tokens = label_seq.split(' ')
# 打印BLEU分数和预测的序列
# BLEU分数是通过比较预测序列和标签序列之间的n-gram重叠度来计算的
print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k),
' '.join(pred_tokens)))
score('ils regardent .', 'they are watching .', k=2)
score('ils sont canadienne .', 'they are canadian .', k=2)
七. 实验总结:
1. 数据预处理:定义了特殊符号,如`<pad>`、`<bos>`和`<eos>`,用于序列的填充、开始和结束。预处理函数将原始文本分割成单词,并添加特殊符号以确保序列长度一致。
2. 编码器-解码器模型:构建了一个包含注意力机制的编码器-解码器模型。编码器将输入序列的词索引转换为词的表征,并通过多层GRU进行处理。注意力机制帮助解码器关注输入序列中与当前输出最相关的部分。
3. 训练模型:实现了计算小批量损失的函数,并使用掩码变量来忽略填充项的损失。在训练函数中,同时迭代编码器和解码器的模型参数,并使用优化器进行参数更新。
4. 预测不定长的序列:实现了贪婪搜索来预测不定长的序列。
5. 评价翻译结果:计算了BLEU分数来评价机器翻译结果的质量。BLEU考虑了预测序列和真实序列之间的匹配子序列,并赋予较长子序列更大的权重。
八. 参考资源
https://cloud.tencent.com/developer/article/1159767