9.7.3 损失函数
解码器预测了输出词元的概率分布,类似于语言模型,可以使用softmax来获得分布,并通过计算交叉墒损失函数来进行优化。回想一下9.5节中,特定的填充词元被添加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,我们应该将填充词元的预测在损失函数的计算中剔除。
我们可以使用下面的sequence_mask 函数通过零值化屏蔽不想管的项,以便后面任何不想管预测的计算都是与零的乘积,结果都等于零。例如,如果两个序列的有效长度分别为1和2,则第一个蓄力哦额的第一项和第二个序列的前两项之后的剩余项将被清零。
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
X=torch.tensor([1,2,3], [4,5,6])
sequence_mask(X, torch.tensor([1,2]))
我们还可以使用此函数屏蔽最后几个轴上的所有项,如果需要,也可以使用指定的非零值来替换这些项。
X=torch.ones(2,3,4)
sequence_mask(X, torch.tensor([1,2]), value=-1)
我们可以通过扩展softmax交叉墒损失函数来屏蔽不相关的预测,最初,所有预测词元的掩码都设置为1,一旦给定了有效长度,与填充词元对应的掩码将被设置为0,最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。
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)
weighted_loss = (unweigthed_loss * weights).mean(dim=1)
return weighted_loss
我们可以创建3个相同的序列来进行代码健全性检查,然后制定这些序列的有效长度分别为4,2 和0,结果是,第一个序列的损失应为第二个序列的2倍,而第三个序列的损失应为0.
loss = MaskedSoftmaxCELoss()
loss(torch.ones(3,4,10), torch.ones((3,4), dtype=torch.long), torch.tensor([4,2,0]))
9.7.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()
matric = 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]
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, l)
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]/metirc[1],))
在机器翻译数据集上,我们可以创建和训练一个循环神经网络编码器-解码器模型用于序列到序列学习
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)
9.7.5 预测
为了采用一个接着一个词元的方式预测输出序列,每个解码器当前时间步的输入都将来自前一个时间步的预测词元,与训练类似,序列开始词元在初始时间步呗输入解码器中。
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([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
读预测过程如图9-14所示,当输出序列的预测遇到序列结束词元时,预测就结束了。
编码器
they are watching eos
解码器
bos lls regardent eos
图9-14 使用循环神经网络编码器-解码器逐个词元地预测输出序列
我们将9.8节中介绍不同的序列生成策略。
9.7.6 预测序列的评估
可以通过与真实的标签序列进行比较来评估预测序列,虽然参考文献中提出的BLEU最先被用于评估机器翻译的结果,现在它已经被广泛用于度量许多应用的输出序列的质量,原则上,对于预测序列中的任意n元语法,BLEU都能评估这个n元语法是否出现在标签序列中。
我们将BLEU定义为
exp(min(0,1 - LENlabel/LENpred)) PI pn1/2^n
LENlabel表示标签序列中的词元数,LENpred表示预测序列中的词元数,k是用于匹配的最长的n元语法,用Pn表示n元语法的精确率,是两个数量的比值,第一个是预测序列与标签序列中匹配的n元语法的数量,第二个是预测序列中n元语法的数量,给定标签序列A,B,C,D,E,F和预测序列A,B,B,C,D,我们有Pl = 4/5,P2 = 3/4, P3=1/3 P4=0
根据式中的BLEU的定义,当预测序列与标签序列完全相同的,BLEU为1,此外,由于n元语法越长则匹配难度越大,因此BLEU为更长的n元语法的精确率分配更大的权重,具体来说,当Pn固定时,Pn会随着n的增加而增加,而且,优于预测的序列越短获得的Pn的值越大,因此式9.21中乘法项之前的系数用于惩罚较短的预测序列,例如k = 2时,给定标签序列A,B,C,D,E,F 和预测序列A,B, 尽管P1=P2=l 但是惩罚因子exp(1-6/2) =0.14 会降低BLEU
BLEU的代码实现如下
def bleu(pred_seq, label_seq, k):
计算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(l, 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])] -= l
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
return score
利用训练好的循环神经网络编码器 - 解码器模型,将几个英语句子翻译成法语,并计算BLEU的最终结果
engs = ['go .', "i lost .", 'he\'s calm .]
for eng, fra in zip(engs, fras):
translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)
小结:
根据编码器-解码器架构的设计,我们可以使用两个循环神经网络设计一个序列学习的模型
在实现编码器和解码器时,我们可以使用多层循环神经网络
我们可以使用屏蔽来过滤不相关的计算,例如在计算损失时
在 编码器-解码器训练中,强制教学方法将原始输出序列输入解码器。
BLEU是一种常用的评估方法,通过测量预测序列和标签序列之间的n元语法的匹配度来评估预测。