在机器翻译中,编码器-解码器模型是一种常用的架构,它使用注意力机制来提高翻译的准确性。注意力机制允许模型在不同语言的序列之间分配注意力,从而更好地理解源语言的含义。
以下是使用PyTorch实现一个简单的编码器-解码器模型和注意力机制的步骤:
1. 定义数据预处理函数
首先,我们需要定义一些特殊符号,如“”、“”和“”,以及两个辅助函数来预处理数据。这些函数将读取文本数据,将其转换为词索引,并构造词汇表。
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
# 定义特殊符号
PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 读取和预处理数据的辅助函数
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)
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)
2. 读取数据
接着,我们读取一个很小的法语-英语数据集,并为法语词和英语词分别创建词典。
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:
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,则忽略掉此样本
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)
3. 构建编码器-解码器模型
接下来,我们将构建编码器-解码器模型,并使用注意力机制。这通常包括编码器、注意力机制和解码器三个部分。
class Encoder(nn.Module):
def __init__(self, input_dim, hidden_dim):
super(Encoder, self).__init__()
self.embedding = nn.Embedding(input_dim, hidden_dim)
self.rnn = nn.GRU(hidden_dim, hidden_dim)
def forward(self, input_seq):
embedded = self.embedding(input_seq)
outputs, hidden = self.rnn(embedded)
return outputs, hidden
class Decoder(nn.Module):
def __init__(self, hidden_dim, output_dim):
super(Decoder, self).__init__()
self.embedding = nn.Embedding(output_dim, hidden_dim)
self.rnn = nn.GRU(hidden_dim, hidden_dim)
self.out = nn.Linear(hidden_dim, output_dim)
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, input_seq, hidden):
embedded = self.embedding(input_seq).view(1, 1, -1)
output, hidden = self.rnn(embedded, hidden)
target_vocab_size = self.out.weight.size(0)
target_batch_size = output.size(1)
output = output.view(-1, hidden_dim)
output = self.softmax(self.out(output))
return output, hidden
class Attention(nn.Module):
def __init__(self, hidden_dim):
super(Attention, self).__init__()
self.attn = nn.Linear(hidden_dim, hidden_dim)
self.v = nn.Parameter(torch.rand(hidden_dim))
def forward(self, hidden, encoder_outputs):
seq_len = encoder_outputs.size(0)
hidden = hidden.expand(seq_len, -1, -1)
encoder_outputs = encoder_outputs.permute(1, 0, 2)
attn_energies = torch.tanh(self.attn(encoder_outputs))
attn_energies = attn_energies.permute(1, 0, 2)
v = self.v.repeat(encoder_outputs.size(1), 1, 1)
energies = torch.tanh(v.expand_as(attn_energies))
attn_energies = attn_energies.permute(1, 0, 2)
energies = energies.permute(1, 0, 2)
energy = energies.sum(2)
attn_energies = attn_energies.squeeze(2)
attn_energies = attn_energies + energy.expand_as(attn_energies)
attn_energies = F.softmax(attn_energies, dim=1)
attn_energies = attn_energies.permute(1, 0, 2)
context = attn_energies.bmm(encoder_outputs)
context = context.permute(1, 0, 2)
context = context.contiguous().view(-1, hidden_dim)
context = context.expand_as(hidden)
output = hidden + context
return output, attn_energies
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super(Seq2Seq, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, input_seq, target_seq, teacher_forcing_ratio=0.5):
batch_size = input_seq.size(0)
max_len = target_seq.size(1)
target_vocab_size = self.decoder.out.weight.size(0)
encoder_outputs, hidden = self.encoder(input_seq)
input_tensor = torch.tensor([[BOS]] * batch_size, device=self.device)
outputs = torch.zeros(batch_size, max_len, target_vocab_size, device=self.device)
for t in range(max_len):
output, hidden = self.decoder(input_tensor, hidden)
outputoutput, hidden = self.decoder(input_tensor, hidden)
output = output.squeeze(1)
outputs[:, t, :] = output
teacher_force = random.random() < teacher_forcing_ratio
top1 = output.argmax(1)
input_tensor = target_seq[:, t] if teacher_force else top1
return outputs
# 构建模型
encoder = Encoder(input_dim, hidden_dim)
decoder = Decoder(hidden_dim, output_dim)
attention = Attention(hidden_dim)
model = Seq2Seq(encoder, decoder, device)
4. 训练模型
最后,我们将定义一个训练循环,用于训练我们的编码器-解码器模型。这包括定义损失函数、优化器,以及一个迭代过程,其中我们将输入序列传递给模型,计算损失,并更新模型参数。
criterion = nn.NLLLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(num_epochs):
for i, (inp, targ) in enumerate(dataset):
hidden = model.encoder.initHidden(batch_size)
optimizer.zero_grad()
outputs = model(inp, targ, teacher_forcing_ratio=teacher_forcing_ratio)
loss = criterion(outputs, targ)
loss.backward()
optimizer.step()
if i % 100 == 99:
print(f'Epoch {epoch + 1}, Step {i + 1}, Loss: {loss.item():.4f}')
在这个训练循环中,我们使用了教师强制(teacher forcing)策略,即在解码器生成目标序列的下一个词时,我们使用真实的词作为输入,而不是使用模型自己的预测。这种策略可以帮助模型更快地收敛。
含注意力机制的编码器-解码器模型在PyTorch中的应用
在机器翻译领域,编码器-解码器模型是一种常用的架构,它使用注意力机制来提高翻译的准确性。注意力机制允许模型在不同语言的序列之间分配注意力,从而更好地理解源语言的含义。
1. 编码器
编码器部分负责将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元(GRU)中。GRU在前向计算后返回输出和最终时间步的多层隐藏状态。输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。
class Encoder(nn.Module):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, drop_prob=0):
super(Encoder, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
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):
return None
2. 注意力机制
注意力机制是将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。
def attention_model(input_size, attention_size):
model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False),
nn.Tanh(),
nn.Linear(attention_size, 1, bias=False))
return model
3. 含注意力机制的解码器
解码器部分负责将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态。这要求编码器和解码器的循环神经网络使用相同的隐藏层个数和隐藏单元个数。
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)
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):
# 使用注意力机制计算背景向量
c = attention_forward(self.attention, enc_states, state[-1])
# 将嵌入后的输入和背景向量在特征维连结
input_and_c = torch.cat((self.embedding(cur_input), c), dim=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. 构建完整的编码器-解码器模型
为了构建一个完整的编码器-解码器模型,我们需要将上述的编码器、注意力机制和解码器结合起来。这通常涉及到将编码器的输出传递给注意力机制,然后将注意力机制的输出作为解码器的输入。
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super(Seq2Seq, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, tgt, teacher_forcing_ratio=0.5):
batch_size = src.size(1)
max_len = tgt.size(0)
tgt_vocab_size = self.decoder.out.weight.size(0)
encoder_outputs, hidden = self.encoder(src)
input_tensor = torch.tensor([[self.decoder.vocab.stoi['<bos>']]] * batch_size, device=self.device)
outputs = torch.zeros(batch_size, max_len, tgt_vocab_size, device=self.device)
for t in range(max_len):
output, hidden = self.decoder(input_tensor, hidden, encoder_outputs)
output = output.squeeze(1)
outputs[:, t, :] = output
teacher_force = random.random() < teacher_forcing_ratio
top1 = output.argmax(1)
input_tensor = tgt[:, t] if teacher_force else top1
return outputs
在这个模型中,我们首先使用编码器处理源语言的序列,得到编码器的输出和隐藏状态。然后,我们使用解码器处理目标语言的序列,并使用教师强制策略来帮助模型更快地收敛。
5. 训练和评估模型
最后,我们需要定义一个训练循环,用于训练我们的编码器-解码器模型。这包括定义损失函数、优化器,以及一个迭代过程,其中我们将输入序列传递给模型,计算损失,并更新模型参数。
criterion = nn.NLLLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(num_epochs):
for i, (src, tgt) in enumerate(dataset):
optimizer.zero_grad()
outputs = model(src, tgt, teacher_forcing_ratio=teacher_forcing_ratio)
loss = criterion(outputs, tgt)
loss.backward()
optimizer.step()
if i % 100 == 99:
print(f'Epoch {epoch + 1}, Step {i + 1}, Loss: {loss.item():.4f}')
在这个训练循环中,我们使用了教师强制策略,即在解码器生成目标序列的下一个词时,我们使用真实的词作为输入,而不是使用模型自己的预测。这种策略可以帮助模型更快地收敛。
训练含注意力机制的编码器-解码器模型
在机器翻译领域,含注意力机制的编码器-解码器模型是一种强大的工具,它能够有效地处理不同语言之间的翻译任务。在本节中,我们将探讨如何训练这样一个模型,并使用PyTorch实现一个简单的例子。
1. 实现批量损失函数
首先,我们需要实现一个函数来计算一个小批量的损失。这个函数将处理编码器的输出,解码器的输入和输出,以及实际的标签。我们使用掩码变量来忽略填充项的影响。
def batch_loss(encoder, decoder, X, Y, loss):
# ... 省略其他代码 ...
# 使用掩码变量mask来忽略掉标签为填充项PAD的损失
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
2. 训练函数
接着,我们需要定义一个训练循环,用于训练我们的编码器-解码器模型。这包括定义损失函数、优化器,以及一个迭代过程,其中我们将输入序列传递给模型,计算损失,并更新模型参数。
def train(encoder, decoder, dataset, lr, batch_size, 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()
if (epoch + 1) % 10 == 0:
print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))
在这个训练循环中,我们使用了教师强制策略,即在解码器生成目标序列的下一个词时,我们使用真实的词作为输入,而不是使用模型自己的预测。这种策略可以帮助模型更快地收敛。
3. 预测不定长的序列
最后,我们实现了一个简单的贪婪搜索方法来预测不定长的序列。这个函数将处理输入序列,并生成一个翻译后的输出序列。
def translate(encoder, decoder, input_seq, max_seq_len):
# ... 省略其他代码 ...
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())]
if pred_token == EOS: # 当任一时间步搜索出EOS时,输出序列即完成
break
else:
output_tokens.append(pred_token)
dec_input = pred
return output_tokens
在这个函数中,我们首先将输入序列转换为词索引,然后使用编码器来获取编码器的输出和隐藏状态。接着,我们使用解码器来生成输出序列,直到遇到结束符号`<eos>`。
## 4. 测试模型
最后,我们可以测试模型的翻译能力。例如,给定输入法语句子“ils regardent.”,我们希望模型能够输出英语句子“they are watching.”。
```python
input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)
这个函数将返回一个翻译后的英语句子。
使用BLEU评价机器翻译结果
在机器翻译领域,评价翻译结果的质量至关重要。BLEU(Bilingual Evaluation Understudy)是一种常用的评价指标,它通过比较预测序列和标签序列来评估翻译的质量。
1. BLEU的定义
BLEU通过比较预测序列和标签序列中子序列的精度来评价翻译质量。子序列的精度是指预测序列与标签序列匹配词数为n的子序列的数量与预测序列中词数为n的子序列的数量之比。BLEU的计算公式考虑了匹配不同长度子序列的难度,并给予较长的子序列更高的权重。
def bleu(pred_tokens, label_tokens, k):
# ... 省略其他代码 ...
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
return score
2. 实现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))
for n in range(1, k + 1):
num_matches, label_subs = 0, collections.defaultdict(int)
for i in range(len_label - n + 1):
label_subs[''.join(label_tokens[i: i + n])] += 1
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
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
return score
3. 测试翻译结果
最后,我们可以使用BLEU来评价模型的翻译质量。例如,对于输入序列“ils regardent .”和标签序列“they are watching .”,我们期望模型能够输出高质量的翻译结果。
score('ils regardent .', 'they are watching .', k=2)
score('ils sont canadienne .', 'they are canadian .', k=2)
这个函数将返回一个BLEU分数,并打印出预测的翻译结果。