1.机器翻译:seq2seq最早用来机器翻译的,给定一个源句子,自动翻译成目标语言
①给定一个源语言的句子,自动翻译成目标语言
②输入序列和输出序列的这两个句子的长度可能不一样
2.Seq2seq编码器-解码器架构
①seq2seq指的是一个特定的模型,它的编码器是一个RNN(循环神经网络),使用长度可变的序列作为输入,将其转换为固定形状的隐状态。然后将最终的隐藏状态传给解码器,隐藏状态包括了整个源句子(输入序列)的信息。解码器使用另一个RNN,基于输入序列的编码信息和输出序列已经看见的或者生成的词元来预测下一个词元,从而连续生成输出序列的词元
②编码器将长度可变的输入序列转换成形状固定的上下文变量,并且将输入序列的信息在该上下文变量中进行编码
③编码器可以是单向的循环神经网络,其中的隐藏状态只依赖于输入子序列,这个子序列是由输入序列的开始位置到隐藏状态所在的时间步的位置(包括隐藏状态所在的时间步)组成
④编码器可以是双向的循环神经网络,其中隐藏状态依赖于两个输入子序列,两个子序列是由隐藏状态所在的时间步的位置之前的序列和之后的序列(包含隐藏状态所在的时间步),因此隐藏状态对整个序列的信息进行编码。双向不能做语言模型,但双向可以做翻译。双向可以做编码器,但不能做解码器,解码器需要预测,编码器不需要。
⑤<bos>表示序列开始词元,代表一个句子的开始,是解码器的输入序列的第一个词元
⑥<eos>表示序列的结束词元,代表一个句子的结束(解码器输出的句子长度是可变的,一旦输出序列生成此词元,模型停止预测)
⑦RNN编码器可以输入任意长度的序列,返回最后时刻的隐藏状态,使用RNN编码器最终的隐藏状态来初始化解码器的隐状态,解码器一直输出,直到看到句子的结束标志为止。
3.编码器-解码器的细节。seq2seq可以做可变长度到可变长度句子之间的翻译
最后一层的RNN隐藏状态的传递:
①编码器是没有输出的RNN
②编码器最后时间步的隐藏状态作为解码器的初始隐藏状态
4.训练
①训练时将特定的开始词元<bos>和原始的输出序列(不包括结束词元<eos>)拼接在一起作为编码器的输入,称为强制教学,因为原始的输出序列(词元的标签)送入了解码器
②也可以将来自上一个时间步的预测得到的词元作为解码器的当前输入
③训练和推理不同的是:编码器是相同的,但在训练的时候,解码器直到目标句子的。它直到真正的翻译是什么样子的,所以解码器的输入(每个RNN时刻 的输出)所使用的实际上是真正的目标句子的输入,即使是训练的时候翻译错了。下一个时刻还是正确的输入,在训练的时候所使用的是真正目标句子来帮助训练,这样就降低预测长句子的难度。
5.推理
①推理时没有真正的目标句子作为参考,每一个时刻只能将上一个时刻的输出作为这一时刻的输入,进行不断地预测。每个解码器当前时间步地输入都来自前一个时间步地预测词元,因此能一个词元一个词元地预测输出序列
②和训练类似,序列开始词元<bos>在初始时间步就被输入解码器中
③当前输出序列地预测遇到序列词元<eos>预测结束
6.BLEU
①可以通过与真实标签序列进行比较评估预测序列
②BLEU评估机器翻译的结果,广泛用于测量许多应用的输出序列的质量
③BLEU的值越大越好,最大值是1
④对于预测序列中的任意 n 元语法(n-grams),BLEU 的评估都是这个 n 元语法是否出现在标签序列中
7.BLEU的定义
①pn表示n-grams的精度,它是两个数量的比值:第一个是预测序列与标签序列中匹配的n元语法的数量,第二个是预测序列中n元语法的数量的比率
如:标签序列A B C D E F和预测序列A B B C D
P1=4/5,p2=3/4,p3=1/3,p4=0
- p1:考虑预测序列中所有的 1-gram,有 5 个 1-gram,即 A、B、B、C、D,所以分母为 5 ;再考虑这 5 个 1-gram ,是不是每一个 1-gram 都在标签序列中出现过(预测序列中除了第二个 B 并没有出现,因为在标签序列中 B 只出现了一次,其它都出现了,所以 p1 = 4/5)
- p2:考虑预测序列中所有的 2-gram,有 4 个 2-gram,即AB、BB、BC、CD,所以分母为 4 ;再考虑这 4 个 2-gram ,是不是每一个 2-gram 都在标签序列中出现过(预测序列中除了第二个 BB 并没有出现,因为在标签序列中 B 只出现了一次,其它都出现了,所以 p2 = 3/4)
- p3:考虑预测序列中所有的 3-gram,有 3 个 3-gram,即ABB、BBC、BCD,所以分母为 3 ;再考虑这 3 个 3-gram ,是不是每一个 3-gram 都在标签序列中出现过(预测序列中只有 BCD 在标签序列中出现了一次,其它都没有出现了,所以 p3 = 1/3)
- p4:考虑预测序列中所有的 4-gram,有 2 个 4-gram,即ABBC、BBCD,所以分母为 2 ;再考虑这 2 个 4-gram ,是不是每一个 4-gram 都在标签序列中出现过(预测序列中所有的 4-gram 都没有在标签序列中出现过,所以 p4 = 0/2)
②len(label):标签序列中词元数,len(pred)预测序列中的词元数
- 如果预测的长度比标签(真实)的长度少很多的话,len(label)/len(pred) 就会大于 1 ,整个指数项就会变成一个很小的数
- 所以说真实的标签很长,预测的长度很短的话,会导致前面的指数项比较小,因为预测的长度很短的话,就会越容易命中真实的标签(比如只有标签“A”,预测随便一个就命中了),所以前半部分的指数项是为了惩罚较短的预测序列,防止预测的长度过短
③pn 都是一个小于等于 1 的数,当预测序列和标签序列完全相同时,BLEU 为 1
由于 n 元语法越长则匹配难度越大,所以 BLEU 为更长的 n 元语法的精确度分配了更大的权重
【总结】
①Seq2Seq从一个句子生成另一个句子,机器翻译是应用场景之一
②Seq2Seq使用的是编码器-解码器的架构,编码器和解码器都是RNN
③将编码器最后时间隐藏状态来初始解码器隐状态来完成信息传递
④在编码器-解码器的训练中,强制教学方法将原始输出序列输入到解码器中
⑤BLEU 是一种常用的评估方法,它通过测量预测序列和标签序列之间的 n 元语法的匹配度来衡量生成预测序列的好坏
【代码】
import collections
import math
import torch
from torch import nn
from d2l import torch as d2l
1.编码器 RNN
class Seq2SeqEncoder(d2l.Encoder):
"""序列到序列学习的循环神经网络编码器"""
'''vocab_size-输入词表的大小,embed_size-列数是特征向量的维度'''
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
super(Seq2SeqEncoder, self).__init__(**kwargs)
# 嵌入层:获得输入序列每个词元的特征向量:vocab_size行数是词表的大小,embed_size特征向量的维度
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout)
def forward(self, X, *args):
# 输出“X”的形状(batch_size,num_steps,embed_size)
X = self.embedding(X)
# 循环神经网络,第一个轴对应时间步
X = X.permute(1, 0, 2)
# 未提及状态,默认0
output, state = self.rnn(X)
# output的形状:(num_steps,batch_size,num_hiddens)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state
2.解码器
class Seq2SeqDecoder(d2l.Decoder):
'''解码器'''
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
# 嵌入层:获得输入序列每个词元的特征向量:vocab_size行数是词表的大小,embed_size特征向量的维度
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, *args):
return enc_outputs[1]
def forward(self, X, state):
# 输出X的形状
X = self.embedding(X).permute(1, 0, 2)
# 广播context 具有X相同的num_steps
context = state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), 2)
output, state = self.rnn(X_and_context, state)
output = self.dense(output).permute(1, 0, 2)
return output, state
3.
# 损失函数 ''' sequence_mask,零值化屏蔽不相关的项,后面任何不相关的预测计算都是0的乘积 softmax交叉熵函数屏蔽不相关的预测--预测词元掩码是,给了有效长度,掩码是0.所以的词元损失乘掩码,滤掉不相关预测 '''
def sequence_mask(X, valid_len, value=0):
'''序列中屏蔽不相关的项'''
maxlen = X.size(1)
mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:, None]
X[~mask] = value
return X
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
'''遮蔽的softmax交叉熵'''
# pred形状(batch_size,num_steps,vocab_size)
# label的形状:(batch_size,num_steps)
# valid_len的形状:(batch_size,)
def forward(self, pred, label, valid_len):
weights = torch.ones_like(label)
weights = sequence_mask(weights, valid_len)
self.reduction = 'none'
unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
pred.permute(0, 2, 1), label)
weights_loss = (unweighted_loss * weights).mean(dim=1)
return weights_loss
4.训练
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
"""训练序列到序列模型"""
def xavier_init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
if type(m) == nn.GRU:
for param in m._flat_weights_names:
if "weight" in param:
nn.init.xavier_uniform_(m._parameters[param])
net.apply(xavier_init_weights)
net.to(device)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
loss = MaskedSoftmaxCELoss()
net.train()
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[10, num_epochs])
for epoch in range(num_epochs):
timer = d2l.Timer()
metric = d2l.Accumulator(2) # 训练损失总和,词元数量
for batch in data_iter:
optimizer.zero_grad()
X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
device=device).reshape(-1, 1)
dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学
Y_hat, _ = net(X, dec_input, X_valid_len)
l = loss(Y_hat, Y, Y_valid_len)
l.sum().backward() # 损失函数的标量进行“反向传播”
d2l.grad_clipping(net, 1)
num_tokens = Y_valid_len.sum()
optimizer.step()
with torch.no_grad():
metric.add(l.sum(), num_tokens)
if (epoch + 1) % 10 == 0:
animator.add(epoch + 1, (metric[0] / metric[1],))
print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
f'tokens/sec on {str(device)}')
5.训练开始
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,
dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
6.预测
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
device, save_attention_weights=False):
"""序列到序列模型的预测"""
# 在预测时将net设置为评估模式
net.eval()
src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
src_vocab['<eos>']]
enc_valid_len = torch.tensor([len(src_tokens)], device=device)
src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
# 添加批量轴
enc_X = torch.unsqueeze(
torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
enc_outputs = net.encoder(enc_X, enc_valid_len)
dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
# 添加批量轴
dec_X = torch.unsqueeze(torch.tensor(
[tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
output_seq, attention_weight_seq = [], []
for _ in range(num_steps):
Y, dec_state = net.decoder(dec_X, dec_state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重(稍后讨论)
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['<eos>']:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
7.预测序列的评估
def bleu(pred_seq, label_seq, k): # @save
"""计算BLEU"""
pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
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