目录
1、什么是机器翻译?
机器翻译是指使用计算机程序将一种语言的文本自动翻译成另一种语言的文本。这是自然语言处理领域的一个重要研究方向,也是实际应用中非常有价值的技术之一。
在机器翻译中,通常会使用深度学习模型,如神经网络和注意力机制,来实现从一个语言到另一个语言的翻译。其中,编码器-解码器结构是一种常见的模型架构,它由两部分组成:编码器负责将源语言文本编码成一个语义表示,解码器则将这个语义表示解码成目标语言的文本。本文就会以编码器-解码器为例来对机器翻译进行研究。
总的来说,机器翻译是一项复杂而富有挑战的任务,涉及到多个领域的知识和技术,包括自然语言处理、深度学习、并行计算等。通过不断的研究和实践,机器翻译技术在实际应用中已经取得了许多进展,为跨语言沟通提供了便利和支持。
2、模型介绍——含注意力机制的编码器-解码器
带有注意力机制的编码器-解码器(Encoder-Decoder with Attention)是一种常用于机器翻译等任务的深度学习模型结构。它通过编码器将输入序列编码成一个语义表示,并通过解码器将这个语义表示解码成目标序列。同时,注意力机制可以帮助模型在解码时更好地关注输入序列中与当前位置相关的部分,提升翻译的准确性和流畅性。
2.1注意力机制
注意力机制(Attention Mechanism)是一种用于深度学习模型的技术,旨在让模型能够在处理序列数据时,动态地关注输入序列中与当前任务相关的部分,从而提高模型对输入的理解和处理能力。
在自然语言处理任务中,特别是在机器翻译领域,注意力机制被广泛应用。传统的编码器-解码器模型在处理长序列时可能会出现信息丢失或者模糊的问题,因为编码器生成的固定长度的上下文向量无法充分表达源语言句子的所有内容。为了解决这个问题,注意力机制被引入到编码器-解码器模型中。
在带有注意力机制的编码器-解码器模型中,解码器在生成每个目标语言单词时,都会动态地计算一个注意力分布,指示解码器应该在源语言句子的哪些部分进行"注意",以便更好地理解和翻译源语言句子。这样,模型可以在解码的过程中,根据当前的上下文和输入序列的不同部分,调整其对输入序列的关注程度,从而更准确地生成翻译结果。
常见的注意力机制包括点乘注意力(Dot-Product Attention)、缩放点乘注意力(Scaled Dot-Product Attention)、加性注意力(Additive Attention)等。这些不同类型的注意力机制在计算注意力权重时有不同的计算方式,但都能够实现模型对输入序列的动态关注。
注意力机制的引入大大提升了机器翻译等任务的性能,使得模型能够更好地处理长距离依赖关系和复杂的语言结构。除了机器翻译,注意力机制也在语音识别、文本摘要、对话生成等领域得到了广泛的应用,并成为深度学习模型中不可或缺的组成部分。
2.2编码器
在深度学习中,编码器(Encoder)是一种神经网络结构,主要用于将输入数据转换成一个固定长度的表示。这种表示通常包含了输入数据的关键信息,使得后续的解码或其他处理步骤能够更容易地利用这些信息。编码器是很多序列到序列(sequence-to-sequence)模型的关键组件,尤其是在自然语言处理(NLP)任务中。
2.2.1基本原理
-
输入嵌入层(Embedding Layer):
首先,将输入序列中的每个词(或字符)转换为一个密集的向量表示。这通常使用预训练的词向量(如Word2Vec、GloVe)或者通过嵌入层随机初始化并在训练过程中进行优化。 -
编码器层(Encoder Layers):
由多层神经网络组成,常见的选择有循环神经网络(RNN)、长短期记忆网络(LSTM)、门控循环单元(GRU)以及基于自注意力机制的Transformer编码器。每一层都会对输入的向量序列进行处理,提取出更高级别的特征。 -
上下文向量(Context Vector):
将处理后的特征序列压缩(如取最后一个时间步的输出,或通过某种聚合操作)成一个固定长度的向量,这个向量作为整个输入序列的语义表示
2.2.2常见架构
-
循环神经网络(RNN):
RNN是一种经典的序列处理模型,通过隐状态来捕捉序列中的时间依赖信息。 -
长短期记忆网络(LSTM)和门控循环单元(GRU):
LSTM和GRU是RNN的改进版本,解决了传统RNN在处理长期依赖问题时的梯度消失问题。它们通过引入门控机制,可以更有效地捕捉和保留长距离的依赖关系。 -
Transformer编码器:
Transformer模型采用自注意力机制而不是递归结构来处理序列信息。每一个词在计算自己的表示时都可以直接访问整个输入序列,从而更好地捕捉全局依赖关系。Transformer编码器由多层堆叠的自注意力和前馈神经网络组成,每一层都对输入序列进行处理和转化。
2.2.3应用
- 机器翻译:在机器翻译系统中,编码器将源语言句子编码成上下文向量,然后解码器基于这个向量生成目标语言句子。
- 文本摘要:编码器将长文档压缩成一个固定长度的表示,然后解码器基于这个表示生成摘要。
- 语音识别:编码器将音频信号转换成特征向量,然后解码器将这些特征向量转换成文本。
2.3解码器
解码器(Decoder)是深度学习模型中的另一个重要组件,通常与编码器一起构成序列到序列(sequence-to-sequence)模型。解码器的作用是将编码器生成的表示(通常是一个固定长度的向量)转换为目标序列或其他形式的输出。在很多任务中,如机器翻译、文本摘要、对话生成等,解码器扮演着至关重要的角色。
2.3.1基本原理
-
初始状态(Initial State):
解码器需要一个初始的隐藏状态向量,这通常是由编码器生成的上下文向量。 -
目标嵌入层(Target Embedding Layer):
与编码器类似,解码器通常使用嵌入层来将目标序列中的词(或字符)转换为密集的向量表示。 -
解码器层(Decoder Layers):
也由多个神经网络层组成,可以是循环神经网络(RNN)、长短期记忆网络(LSTM)、门控循环单元(GRU)或者基于自注意力机制的Transformer解码器。每一层都会根据前一个时间步的输出以及上一个隐藏状态来生成新的隐藏状态和输出。 -
输出层(Output Layer):
解码器的最后一层通常是一个全连接层,用于将最终的隐藏状态映射为目标序列中每个词的概率分布,通常使用softmax函数进行归一化。
2.3.2常见架构
-
基于RNN的解码器:
可以使用简单的循环神经网络(RNN)作为解码器的主体结构,接受编码器生成的上下文向量作为初始隐藏状态。 -
基于注意力机制的解码器:
在面对长序列和全局信息对解码有重要影响的任务中,通常会采用基于注意力机制的解码器,例如使用Bahdanau注意力或Luong注意力机制。 -
Transformer解码器:
Transformer解码器与编码器类似,采用自注意力机制来处理输入序列和输出序列的关系,同时通过位置嵌入来保留序列的顺序信息。
3、代码实现
3.1读取和预处理数据
import collections # 导入collections模块,用于高性能容器数据类型
import os # 导入os模块,用于与操作系统交互
import io # 导入io模块,用于处理I/O流
import math # 导入math模块,提供数学运算函数
import torch # 导入PyTorch库
from torch import nn # 从torch模块导入神经网络模块(nn)
import torch.nn.functional as F # 导入神经网络常用的功能函数模块F
import torchtext.vocab as Vocab # 导入torchtext库中的词汇表模块Vocab
import torch.utils.data as Data # 导入PyTorch中的数据处理工具模块Data
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>' # 定义三个特殊的标记,分别表示填充、序列开头和序列结尾
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 设置环境变量,指定GPU设备编号
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 根据是否有GPU可用,选择设备为cuda或cpu
print(torch.__version__, device) # 打印PyTorch版本号以及当前设备类型
# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
# 向列表all_tokens中添加当前序列的所有词
all_tokens.extend(seq_tokens)
# 将当前序列补齐为固定长度max_seq_len,并加上EOS结束标记和PAD填充标记
seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
# 将处理后的序列加入到all_seqs列表中
all_seqs.append(seq_tokens)
# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
def build_data(all_tokens, all_seqs):
# 使用Vocab.Vocab构造函数基于所有词构建词典vocab,并指定特殊标记为[PAD, BOS, EOS]
vocab = Vocab.Vocab(collections.Counter(all_tokens),
specials=[PAD, BOS, EOS])
# 遍历所有序列,将其中每个词通过词典转换为对应的索引,并构造成张量(Tensor)返回
indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
return vocab, torch.tensor(indices)
def read_data(max_seq_len):
# in_tokens和out_tokens分别用于存储所有输入和输出的词汇
# in_seqs和out_seqs分别用于存储所有处理后的输入和输出序列
in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
# 打开文件 'fr-en-small.txt' 并读取所有行
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(' ')
# 检查输入序列或输出序列的长度是否超出最大序列长度max_seq_len
# 如果加上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)
# 返回输入词典、输出词典以及由输入数据和输出数据构成的张量数据集
return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
dataset[0]
3.2模型创建
class Encoder(nn.Module):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, drop_prob=0, **kwargs):
"""
初始化Encoder类
参数:
vocab_size (int): 词汇表的大小
embed_size (int): 嵌入向量的维度大小
num_hiddens (int): 隐藏层神经元的数量
num_layers (int): GRU的层数
drop_prob (float): dropout的概率
"""
super(Encoder, self).__init__(**kwargs)
# 定义词嵌入层,将输入的词索引转化为词向量
self.embedding = nn.Embedding(vocab_size, embed_size)
# 定义GRU层
# 输入维度是embed_size,隐藏层维度是num_hiddens,GRU层数是num_layers,dropout概率是drop_prob
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
def forward(self, inputs, state):
"""
前向传播函数
参数:
inputs (Tensor): 输入张量,形状为(批量大小, 时间步数)
state (Tensor): 初始隐藏状态
返回:
output (Tensor): 编码器的输出,形状为(时间步数, 批量大小, 隐藏层大小)
state (Tensor): 最后的隐藏状态
"""
# 将输入张量转换为长整型,并通过嵌入层得到嵌入表示
embedding = self.embedding(inputs.long()) # (batch_size, seq_len, embed_size)
# permute操作将张量的维度顺序从(批量大小, 时间步数, 嵌入维度)变为(时间步数, 批量大小, 嵌入维度)
embedding = embedding.permute(1, 0, 2) # (seq_len, batch_size, embed_size)
# 通过GRU层进行前向传播
output, state = self.rnn(embedding, state) # output: (seq_len, batch_size, num_hiddens)
return output, state
def begin_state(self):
"""
初始化隐藏状态
返回:
None (GRU会自动初始化隐藏状态)
"""
return None
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)
def attention_model(input_size, attention_size):
"""
构建一个简单的注意力机制模型。
参数:
input_size (int): 输入特征的维度大小
attention_size (int): 注意力机制中的隐藏层大小
返回:
model (nn.Sequential): 包含注意力机制的神经网络模型
"""
# 使用nn.Sequential创建一个顺序容器,其中包含多个神经网络层。
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):
"""
在注意力机制中前向传播计算注意力权重并应用于编码器状态。
参数:
model: 用于计算注意力分数的神经网络模型
enc_states (torch.Tensor): 编码器的隐藏状态,形状为 (时间步数, 批量大小, 隐藏单元个数)
dec_state (torch.Tensor): 解码器的隐藏状态,形状为 (批量大小, 隐藏单元个数)
返回:
torch.Tensor: 经过注意力加权求和后的背景变量,形状为 (批量大小, 隐藏单元个数)
"""
# 将解码器隐藏状态扩展一个维度,使其可以与编码器隐藏状态连结
dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
# 将编码器状态和解码器状态在最后一个维度上进行连结
enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
# 使用注意力模型计算注意力分数 e,形状为 (时间步数, 批量大小, 1)
e = model(enc_and_dec_states)
# 在时间步维度上对注意力分数进行 softmax 运算,得到注意力权重 alpha
alpha = F.softmax(e, dim=0)
# 将注意力权重 alpha 应用于编码器状态 enc_states,并在时间步维度上求和
# 最终返回的形状为 (批量大小, 隐藏单元个数)
return (alpha * enc_states).sum(dim=0)
# 定义模型参数
seq_len, batch_size, num_hiddens = 10, 4, 8
# 创建注意力模型
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 函数计算注意力加权求和后的背景变量
output = attention_forward(model, enc_states, dec_state)
# 输出背景变量的形状
print(output.shape)
class Decoder(nn.Module):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
attention_size, drop_prob=0):
"""
解码器初始化函数。
参数:
vocab_size (int): 输出词典大小
embed_size (int): 嵌入维度大小
num_hiddens (int): 隐藏单元个数
num_layers (int): GRU层数
attention_size (int): 注意力模型大小
drop_prob (float, optional): Dropout概率,默认为0
"""
super(Decoder, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.attention = attention_model(2*num_hiddens, attention_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 (torch.Tensor): 当前时间步的输入,形状为 (批量大小, )
state (torch.Tensor): 解码器当前的隐藏状态,形状为 (层数, 批量大小, 隐藏单元个数)
enc_states (torch.Tensor): 编码器所有时间步的隐藏状态,形状为 (时间步数, 批量大小, 隐藏单元个数)
返回:
torch.Tensor: 当前时间步的输出,形状为 (批量大小, 输出词典大小)
torch.Tensor: 更新后的解码器隐藏状态,形状同输入的state
"""
# 使用注意力机制计算背景向量 c
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):
"""
返回解码器的初始隐藏状态。
参数:
enc_state (torch.Tensor): 编码器最终时间步的隐藏状态,形状为 (层数, 批量大小, 隐藏单元个数)
返回:
torch.Tensor: 解码器的初始隐藏状态,形状同输入的enc_state
"""
# 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
return enc_state
3.3模型训练
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优化算法,设置学习率
enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)
# 定义损失函数,使用交叉熵损失,并且不进行内部的损失求和
loss = nn.CrossEntropyLoss(reduction='none')
# 创建数据迭代器,从dataset中按批次读取数据,并且打乱顺序
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
# 开始训练循环,遍历每个epoch
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和平均损失
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)
def translate(encoder, decoder, input_seq, max_seq_len):
in_tokens = input_seq.split(' ') # 将输入序列按空格分割为单词列表
in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1) # 将输入序列扩展到指定的最大长度,并添加EOS和PAD标记
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 = [] # 初始化输出的单词列表
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())] # 将预测结果的索引转换为对应的单词
if pred_token == EOS: # 当任一时间步搜索出EOS时,输出序列即完成
break
else:
output_tokens.append(pred_token) # 将当前时间步的输出单词加入到输出序列中
dec_input = pred # 将当前时间步的输出作为下一时间步的输入
return output_tokens # 返回生成的输出序列
3.4结果评价
评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)。BLEU通过比较机器翻译系统生成的候选翻译与多个参考翻译之间的n-gram重叠来计算相似度得分。具体而言,BLEU考虑了候选翻译中与参考翻译中相同n-gram的频率,并将这些频率归一化为候选翻译中对应n-gram的最高频率。然后,BLEU将这些归一化的频率求和并进行加权平均,其中权重是根据n-gram的长度来确定的。
BLEU的取值范围从0到1,越接近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)) # 初始化BLEU分数为长度惩罚因子
for n in range(1, k + 1): # 对于每个n-gram(n从1到k)
num_matches, label_subs = 0, collections.defaultdict(int) # 初始化匹配数量和参考序列的n-gram子序列字典
for i in range(len_label - n + 1): # 统计参考序列中所有长度为n的子序列的出现次数
label_subs[''.join(label_tokens[i: i + n])] += 1
for i in range(len_pred - n + 1): # 统计预测序列中所有长度为n的子序列与参考序列的匹配情况
if label_subs[''.join(pred_tokens[i: i + n])] > 0: # 如果预测序列中的子序列在参考序列中出现过
num_matches += 1 # 增加匹配数量
label_subs[''.join(pred_tokens[i: i + n])] -= 1 # 更新参考序列的子序列字典,减少对应子序列的出现次数
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) # 根据匹配数量和n-gram长度计算BLEU分数的一部分
return 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)))