目录
1.为什么需要pack pad和mask
本节主要介绍加快训练的两种trcik,一个是循环神经网络中的pack pad,还有一个是在计算注意力权重的mask。以下分别细说为什么需要这样做。
1.1pack pad
循环神经网络的输入序列数据,若一个batch里面每一个样本的序列长度都是一样的,是最理想的状态。但是正常情况下,输入的序列长度参差不齐,若batch=1则不会有什么问题,但是若batch>1的时候,将一个batch带入循环神经网络里面则会报错,因为循环神经网络不知道这批batch数据该执行多少时间步了。因此,通常做法就是将一个batch中的所有样本设置为固定长度,没达到这个长度的则进行pad,而超过这个长度的则进行裁剪。操作如下所示:
import torch
import torch.nn as nn
from torch.nn.utils.rnn import pad_sequence,pack_padded_sequence,pad_packed_sequence
假设我们一个batch的大小为4,按最大长度进行填充。则原来的张量为:
data=[torch.tensor([1,2,3]),torch.tensor([4,5]),torch.tensor([6,7,8,9]),torch.tensor([2])]
pad=pad_sequence(data,batch_first=True)
print(pad)
我们通过pad填充以后得到的张量为(一般都是0填充):
tensor([[1, 2, 3, 0],
[4, 5, 0, 0],
[6, 7, 8, 9],
[2, 0, 0, 0]])
接着,我们会将这个batch序列数据经过嵌入层得到词嵌入的表示:
embed=nn.Embedding(10,2,padding_idx=0)
pad_emb=embed(pad)
print(pad_emb)
得到 结果如下:
tensor([[[-0.6222, 0.4489],
[-1.5293, -1.7078],
[-1.0837, 0.4064],
[ 0.0000, 0.0000]],
[[ 1.7610, -2.5629],
[ 0.0538, 0.2917],
[ 0.0000, 0.0000],
[ 0.0000, 0.0000]],
[[ 0.0538, 0.1189],
[ 0.9783, -1.7305],
[ 1.0734, -0.0437],
[ 0.4617, -1.1012]],
[[-1.5293, -1.7078],
[ 0.0000, 0.0000],
[ 0.0000, 0.0000],
[ 0.0000, 0.0000]]], grad_fn=<EmbeddingBackward>)
过去我们会直接将这个填充嵌入向量(pad_emb)带入RNN循环神经网络计算,但是这样会使得填充数据也参与了计算,并且也参与反向传播的梯度更新。之前我认为其为填充为0,计算结果也为0。但是忽略了bias这个值,所以填充数据计算的最后结果是bias并经过激活函数。所以我们在rnn计算应该对pad_emb向量进行压缩,也可以说去pad,要想去除pad,必须要知道原来batch中真实序列数据的长度。
lens=[len(s) for s in data]#统计源序列长度
pack_pademb=pack_padded_sequence(pad_emb,lens,batch_first=True,enforce_sorted=False)
pack_pademb
pack_padded_sequence有一个奇怪的地方(一直没懂),其要求输入的batch数据必须按照序列长度从大到小排列,若输入的batch数据已经排列好了,则将enforce_sorted关键字设置为True(默认),否则为False,此时pack_padded_sequence会自动帮你排序。上面的输出如下:
PackedSequence(data=tensor([[ 0.0538, 0.1189],
[-0.6222, 0.4489],
[ 1.7610, -2.5629],
[-1.5293, -1.7078],
[ 0.9783, -1.7305],
[-1.5293, -1.7078],
[ 0.0538, 0.2917],
[ 1.0734, -0.0437],
[-1.0837, 0.4064],
[ 0.4617, -1.1012]], grad_fn=<PackPaddedSequenceBackward>), batch_sizes=tensor([4, 3, 2, 1]), sorted_indices=tensor([2, 0, 1, 3]), unsorted_indices=tensor([1, 2, 0, 3]))
其输出为一个元组,第一个元素为我们去掉pad得到的张量,后面元素(我也看不懂)应该是记录张量的原始位置以及在lstm中第一个时间步输入(可以看出张量很乱,张量第一行为最长序列的中第一个词嵌入,第二行为此长序列的第一个单词嵌入...)。lstm在计算中接收这个张量并根据元组后面的一些记录合理带入相应时间步进行计算。如下:
gru=nn.GRU(2,5,batch_first=True)
output,h_n=gru(pack_pademb)
print(h_n.shape,h_n)
这里GRU输出两个变量,其中h_n为每层最后一个时间步的输出,因此其输出维度与我们之前将pad_emb带入RNN模型中输出的维度一样。为[n_layers*directions batch hidden_size]。其可以直接拿到解码器中作为上一时间步隐状态使用。如下:
torch.Size([1, 4, 5]) tensor([[[ 0.3348, 0.5232, 0.1564, -0.2327, 0.1372],
[ 0.2061, 0.2969, -0.2660, 0.0729, 0.1300],
[ 0.2086, 0.3415, -0.4975, 0.2237, 0.1598],
[ 0.4933, 0.5361, -0.1153, -0.0213, 0.2832]]],
grad_fn=<IndexSelectBackward>)
而output则是最后一层每一个时间步的输出,之前我们若按照pad_embed输入,输出的维度则是[batch seq_len hidden],我们输出output查看,其结构如下:
PackedSequence(data=tensor([[ 0.0023, 0.2809, -0.0250, -0.1508, -0.0274],
[ 0.0228, 0.3411, 0.1242, -0.2785, -0.0309],
[ 0.4076, 0.1314, -0.3429, 0.3959, 0.3852],
[ 0.4933, 0.5361, -0.1153, -0.0213, 0.2832],
[ 0.2748, 0.2830, -0.3401, 0.2015, 0.2440],
[ 0.4726, 0.5832, -0.0506, -0.0629, 0.2705],
[ 0.2061, 0.2969, -0.2660, 0.0729, 0.1300],
[ 0.0299, 0.2342, -0.3988, 0.1580, -0.0058],
[ 0.3348, 0.5232, 0.1564, -0.2327, 0.1372],
[ 0.2086, 0.3415, -0.4975, 0.2237, 0.1598]], grad_fn=<CatBackward>), batch_sizes=tensor([4, 3, 2, 1]), sorted_indices=tensor([2, 0, 1, 3]), unsorted_indices=tensor([1, 2, 0, 3]))
我得到了最后 一层每一个时间步的输出,output是这样的原因还是因为我们在输入rnn结构的时候,写法不同,因此我们需要对这样的表现写法解压(pack pad)成我们可以看的容易点的(即存在的pad的形式),元组中后面的元素我认为就是记录原始表达。
output,_=pad_packed_sequence(output,batch_first=True)
#_为每个序列的长度,我们已不在需要
output
输出如下:
tensor([[[ 0.2079, -0.1423, 0.1863, -0.1045, -0.2481],
[ 0.1902, -0.1038, 0.3212, -0.3954, -0.3672],
[ 0.1102, -0.0816, 0.4848, -0.1545, -0.2990],
[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]],
[[-0.0567, 0.0751, 0.1637, -0.4889, -0.1605],
[-0.1150, 0.1383, 0.3863, -0.3787, -0.0737],
[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]],
[[ 0.0762, -0.0836, 0.2109, -0.1296, -0.1923],
[-0.2471, 0.3859, 0.4045, -0.3774, 0.1856],
[ 0.0094, 0.0486, 0.4842, -0.4770, -0.1327],
[-0.0433, 0.1380, 0.5844, -0.4014, -0.0699]],
[[ 0.0550, -0.0665, 0.1680, -0.3898, -0.2490],
[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]]],
grad_fn=<IndexSelectBackward>)
这里我们也对比一下将pad嵌入直接带入rnn模型中计算:
output,h_n=gru(pad_emb)
output
tensor([[[ 0.2079, -0.1423, 0.1863, -0.1045, -0.2481],
[ 0.1902, -0.1038, 0.3212, -0.3954, -0.3672],
[ 0.1102, -0.0816, 0.4848, -0.1545, -0.2990],
[ 0.0638, -0.0372, 0.5586, -0.1819, -0.2878]],
[[-0.0567, 0.0751, 0.1637, -0.4889, -0.1605],
[-0.1150, 0.1383, 0.3863, -0.3787, -0.0737],
[-0.0175, 0.0593, 0.5072, -0.2698, -0.1443],
[ 0.0265, 0.0285, 0.5740, -0.2282, -0.1928]],
[[ 0.0762, -0.0836, 0.2109, -0.1296, -0.1923],
[-0.2471, 0.3859, 0.4045, -0.3774, 0.1856],
[ 0.0094, 0.0486, 0.4842, -0.4770, -0.1327],
[-0.0433, 0.1380, 0.5844, -0.4014, -0.0699]],
[[ 0.0550, -0.0665, 0.1680, -0.3898, -0.2490],
[ 0.0473, -0.0411, 0.3728, -0.2771, -0.2430],
[ 0.0435, -0.0221, 0.4958, -0.2294, -0.2478],
[ 0.0446, -0.0089, 0.5682, -0.2076, -0.2532]]],
grad_fn=<TransposeBackward1>)
可以发现,就算pad的嵌入全为0,其输出也不一定为0。因为神经网络的计算公式:
pad的时候,我们的输入x全为0,由于bias不等于0,其计算结果也不会为0。我们这一节通过pack-pad反复操作主要的目标就是希望pad元素不带入rnn模型结构的计算,加速模型训练的速度。
1.2mask
在最终将attention的energy通过softmax函数变为权重的时候,其中一些pad元素其实也会参与了运算。假设我们的attention计算方式为点积运算:
即,由于我们上面知道pad最后的输出全为0,那么其与任何查询向量q点积为0:
假设我们得到了一个energy向量为:
我们对其进行softmax运算:
energy=torch.tensor([1,5,4,3,0],dtype=torch.float)
torch.nn.functional.softmax(energy,dim=0)
发现结果为:
tensor([0.0120, 0.6543, 0.2407, 0.0886, 0.0044])
pad的权重系数不为0,这是因为:
而,,因此pad也分配了权重系数,这是我们不想看到的,我们可以通过mask操作强制模型忽略某些值,比如忽略对填充元素的关注。我们知道e的无穷(代码中写-1e10)附近于为0。我们只需要将pad的能量换成e的负无穷,这样在softmax的时候,就为忽视此能量。即:
mask=(energy!=0)
energy=energy.masked_fill(mask==0,-1e10)
print(energy)
print(torch.nn.functional.softmax(energy,dim=0))
输出为:
tensor([ 1.0000e+00, 5.0000e+00, 4.0000e+00, 3.0000e+00, -1.0000e+10])
tensor([0.0120, 0.6572, 0.2418, 0.0889, 0.0000])
此时模型中pad的注意力权重为0,即忽视了此元素。
2.Attention机制中实现这两个技巧
2.1数据处理
import torch
import spacy
from torchtext.data import Field,BucketIterator
from torchtext.datasets import Multi30k
de=spacy.load("de_core_news_sm")
en=spacy.load("en_core_web_sm")
def de_seq(text):
return [word.text for word in de.tokenizer(text)]
def en_seq(text):
return [word.text for word in en.tokenizer(text)]
SRC=Field(tokenize=de_seq,
lower=True,
init_token="<sos>",
eos_token="<eos>",
include_lengths=True)
#include_lengths为True后
#src返回一个元组,第一个为序列张量,第二个为序列长度
TRG=Field(tokenize=en_seq,
lower=True,
init_token="<sos>",
eos_token="<eos>")
train_data,val_data,test_data=Multi30k.splits(exts=(".de",".en"),
fields=(SRC,TRG))
SRC.build_vocab(train_data,min_freq=2)
TRG.build_vocab(train_data,min_freq=2)
BATCH=128
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_iter,val_iter,test_iter=BucketIterator.splits(
(train_data,val_data,test_data),
batch_size=BATCH,
sort_within_batch=True,#开启排序
sort_key=lambda x:len(x.src),
device=device
)
取一个样本用于后面的测试:
for example in train_iter:
src=example.src[0].permute(1,0)
src_len=example.src[1]
trg=example.trg.permute(1,0)
break
2.2模型建立
import random
import torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence,pad_packed_sequence
class Encoder(nn.Module):
def __init__(self,src_vocab_size,emb_size,enc_hidden_size,dec_hidden_size,dropout=0.5):
super(Encoder,self).__init__()
self.emb=nn.Embedding(src_vocab_size,emb_size,padding_idx=1)
self.rnn=nn.GRU(emb_size,enc_hidden_size,batch_first=True,bidirectional=True)
self.fc=nn.Linear(2*enc_hidden_size,dec_hidden_size)
self.dropout=nn.Dropout(dropout)
def forward(self,src,src_len):
#src [batch src_len]
#src_len[batch]
src=self.dropout(self.emb(src))
#src[batch src_len emb_size]
#去pad
src=pack_padded_sequence(src,src_len,batch_first=True)
#因为我们的src已经排序过了,所以enforce_sorted=True(默认)
outputs,h_n=self.rnn(src)
#outputs:需要解压为[batch src_len enc_hidden_size*2]
outputs,_=pad_packed_sequence(outputs,batch_first=True)
#h_n[2 batch hidden_size]
for_state=h_n[-2,:,:]
back_state=h_n[-1,:,:]
#for_state[batch enc_hidden_size]
#back_state[batch enc_hidden_size]
concat=torch.cat((for_state,back_state),dim=1)
#concat[batch 2*enc_hidden_size]
h_n=torch.tanh(self.fc(concat))
#h_n[batch dec_hidden_size]
return outputs,h_n
测试:
src_vocab_size=len(SRC.vocab)
trg_vocab_size=len(TRG.vocab)
enc_hidden_size=512
dec_hidden_size=512
emb_size=256
enModel=Encoder(src_vocab_size,emb_size,enc_hidden_size,dec_hidden_size).to(device)
en_outputs,h_n=enModel(src,src_len)
en_outputs.shape,h_n.shape
输出:
(torch.Size([128, 8, 1024]), torch.Size([128, 512]))
class Attention(nn.Module):
def __init__(self,enc_hidden_size,dec_hidden_size):
super(Attention,self).__init__()
self.attn=nn.Linear(2*enc_hidden_size+dec_hidden_size,dec_hidden_size)
self.v=nn.Linear(dec_hidden_size,1,bias=False)
def forward(self,enc_outputs,h_n,mask):
#enc_outputs[batch src_len enc_hidden_size]
#h_n[batch dec_hidden_size]
#mask[batch src_len]
src_len=enc_outputs.shape[1]
h_n=h_n.unsqueeze(1).repeat(1,src_len,1)
#h_n[batch src_len dec_hidden_size]
concat=torch.cat((enc_outputs,h_n),dim=2)
#concat[batch src_len 2*enc_hidden_size+dec_hidden_size]
output=torch.tanh(self.attn(concat))
#output[batch src_len dec_hidden_size]
energy=self.v(output).squeeze(2)
#energy[batch src_len]
energy=energy.masked_fill(mask==0,-1e10)
return nn.functional.softmax(energy,dim=1)
测试:
mask=(src!=1)
attnModel=Attention(enc_hidden_size,dec_hidden_size).to(device)
a=attnModel(en_outputs,h_n,mask)
a.sum(dim=1)
结果:
tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000], device='cuda:0', grad_fn=<SumBackward1>)
class Decoder(nn.Module):
def __init__(self,trg_vocab_size,emb_size,enc_hidden_size,dec_hidden_size,attn,dropout=0.5):
super(Decoder,self).__init__()
self.embed=nn.Embedding(trg_vocab_size,emb_size,padding_idx=1)
self.rnn=nn.GRU(2*enc_hidden_size+emb_size,dec_hidden_size,batch_first=True)
self.fc=nn.Linear(2*enc_hidden_size+emb_size+dec_hidden_size,trg_vocab_size)
self.attn=attn
self.dropout=nn.Dropout(dropout)
def forward(self,trg_i,h_n,en_outputs,mask):
#trg_i[batch]
#h_n[batch dec_hidden_size]
#en_outputs[batch src_len 2*enc_hidden_size]
#mask[batch src_len]
trg_i=trg_i.unsqueeze(1)
#trg_i[batch 1]
trg_i=self.dropout(self.embed(trg_i))
#trg_i[batch 1 embed_size]
a=self.attn(en_outputs,h_n,mask)
#a[batch src_len]
a=a.unsqueeze(1)
#a[batch 1 src_len]
#en_outputs[batch src_len 2*enc_hidden_size]
context=torch.bmm(a,en_outputs)
#context[batch 1 2*enc_hidden_size]
#trg_i[batch 1 embed_size]
concat=torch.cat((trg_i,context),dim=2)
#concat[batch 1 2*enc_hidden_size+embed_size]
h_n=h_n.unsqueeze(0)
#h_n[ 1 batch dec_hidden_size]
outputs,h_n=self.rnn(concat,h_n)
#outputs[batch 1 dec_hidden_size]
#h_n[1 batch dec_hidden_size]
#concat[batch 1 2*enc_hidden_size+embed_size]
#outputs[batch 1 dec_hidden_size]
concat=torch.cat((concat,outputs),dim=2).squeeze(1)
#concat[batch 2*enc_hidden_size+embed_size+dec_hidden_size]
output=self.fc(concat)
#output[batch trg_vocab_size]
#a[batch 1 src_len]
#h_n[1 batch dec_hidden_size]
return output,h_n.squeeze(0),a.squeeze(1)
测试:
trg_i=trg[:,1]
deModel=Decoder(trg_vocab_size,emb_size,enc_hidden_size,dec_hidden_size,attnModel).to(device)
output,h_n,a=deModel(trg_i,h_n,en_outputs,mask)
output.shape,h_n.shape,a.shape
结果:
(torch.Size([128, 5893]), torch.Size([128, 512]), torch.Size([128, 8]))
class Seq2Seq(nn.Module):
def __init__(self,encoder,decoder):
super(Seq2Seq,self).__init__()
self.encoder=encoder
self.decoder=decoder
def forward(self,src,src_len,trg,teach_threshold=0.5):
#src[batch src_len]
#trg[batch trg_len]
batch=trg.shape[0]
trg_len=trg.shape[1]
en_outputs,h_n=self.encoder(src,src_len)
#en_outputs[batch src_len 2*enc_hidden_size]
#h_n[1 batch dec_hidden_size]
#保存输出
outputs=torch.zeros(batch,trg_len,trg_vocab_size).to(device)
#第一个输入:
input_i=trg[:,0]
#input[batch]
mask=(src!=1)
#mask[batch src_len]
for t in range(1,trg_len):
output,h_n,_=self.decoder(input_i,h_n,en_outputs,mask)
#output[batch trg_vocab_size]
#h_n[batch dec_hidden_size]
#a[batch src_len]
p=random.random()
outputs[:,t,:]=output
input_i=trg[:,t] if p<teach_threshold else output.argmax(1)
return outputs,a
测试:
model=Seq2Seq(enModel,deModel).to(device)
outputs,a=model(src,src_len,trg)
outputs[:,1,:]
测试:
tensor([[ 0.3178, -0.0398, 0.1933, ..., -0.2355, 0.4084, -0.5031],
[ 0.3257, 0.2936, -0.1176, ..., 0.3534, -0.2360, -0.5746],
[-0.2967, 0.0736, 0.0659, ..., -0.0246, -0.2941, -0.7550],
...,
[-0.3229, 0.0446, 0.3369, ..., -0.0155, 0.1334, -0.1773],
[-0.6029, -0.0266, 0.4576, ..., -0.0326, -0.7053, -0.5767],
[-0.3136, 0.3114, -0.0677, ..., -0.6238, -0.2722, -0.7812]],
device='cuda:0', grad_fn=<SliceBackward>)
2.3训练
import math,time
from torch.optim import Adam
epochs=10
optim=Adam(model.parameters())
criterion=nn.CrossEntropyLoss(ignore_index=1)
def train(model,data_iter,criterion,optim,clip):
model.train()
lossAll=0
for example in data_iter:
src=example.src[0].permute(1,0)
src_len=example.src[1]
trg=example.trg.permute(1,0)
outputs,_=model(src,src_len,trg)
optim.zero_grad()
outputs=outputs[:,1:,:].reshape(-1,trg_vocab_size)
trg=trg[:,1:].reshape(-1)
loss=criterion(outputs,trg)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(),clip)
optim.step()
lossAll+=loss.item()
return lossAll/len(data_iter)
def evaluate(model,data_iter,criterion):
model.eval()
lossAll=0
with torch.no_grad():
for example in data_iter:
src=example.src[0].permute(1,0)
src_len=example.src[1]
trg=example.trg.permute(1,0)
outputs,_=model(src,src_len,trg,0)
outputs=outputs[:,1:,:].reshape(-1,trg_vocab_size)
trg=trg[:,1:].reshape(-1)
loss=criterion(outputs,trg)
lossAll+=loss.item()
return lossAll/len(data_iter)
def init_weights(model):
for name,param in model.named_parameters():
if "weight" in name:
nn.init.normal_(param.data,mean=0,std=0.01)
else:
nn.init.constant_(param.data,0)
model.apply(init_weights)
def epoch_time(start_time, end_time):
elapsed_time = end_time - start_time
elapsed_mins = int(elapsed_time / 60)
elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
return elapsed_mins, elapsed_secs
N_EPOCHS = 10
CLIP = 1
for epoch in range(N_EPOCHS):
start_time=time.time()
train_loss=train(model,train_iter,criterion,optim,CLIP)
valid_loss=evaluate(model,val_iter,criterion)
end_time=time.time()
epoch_mins,epoch_secs=epoch_time(start_time, end_time)
print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. PPL: {math.exp(valid_loss):7.3f}')
以下给出attention将pad直接带入以及无mask的实验过程:
3.注意力向量可视化
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
BLEU根据n-gram分析预测序列和实际目标序列的重叠部分,因此我们需要对编码器的预测转变为实际的句子翻译
def translate_sentence(sentence,src_field,trg_field,model,max_len=50):
model.eval()
if isinstance(sentence,str):
nlp=spacy.load("de_core_news_sm")
tokens=[token.text.lower() for token in nlp.tokenizer(sentence)]
else:
tokens=[token.lower() for token in sentence]
tokens=[src_field.init_token]+tokens+[src_field.eos_token]
src_indexes=[src_field.vocab.stoi[token] for token in tokens]
src_tensor=torch.LongTensor(src_indexes).unsqueeze(0).to(device)
#src_tensor[1 src_len]
src_len=torch.LongTensor([len(src_indexes)]).to(device)
with torch.no_grad():
en_outputs,hidden=model.encoder(src_tensor,src_len)
mask=(src_tensor!=1)
trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]
attentions=torch.zeros(1,max_len,len(src_indexes)).to(device)
for i in range(max_len):
trg_tensor=torch.LongTensor([trg_indexes[-1]]).to(device)
with torch.no_grad():
output,hidden,attention=model.decoder(trg_tensor,hidden,en_outputs,mask)
attentions[:,i,:]=attention
pred_token=output.argmax(1).item()
trg_indexes.append(pred_token)
if pred_token==trg_field.vocab.stoi[trg_field.eos_token]:
break
trg_tokens=[trg_field.vocab.itos[i] for i in trg_indexes]
return trg_tokens[1:],attentions[:,:len(trg_tokens)-1,:]
为了观察注意力机制的权重系数,采用了热力图进行查看:
def display_attention(sentence, translation, attention):
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
attention = attention.squeeze().cpu().detach().numpy()
cax = ax.matshow(attention, cmap='bone')
ax.tick_params(labelsize=15)
ax.set_xticklabels(['']+['<sos>']+[t.lower() for t in sentence]+['<eos>'],
rotation=45)
ax.set_yticklabels(['']+translation)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
plt.show()
plt.close()
在训练集和测试集各取样本观察:
训练集
example_idx = 12
src = vars(train_data.examples[example_idx])['src']
trg = vars(train_data.examples[example_idx])['trg']
print(f'src = {src}')
print(f'trg = {trg}')
原本的源端和目标端(标签):
src = ['ein', 'schwarzer', 'hund', 'und', 'ein', 'gefleckter', 'hund', 'kämpfen', '.']
trg = ['a', 'black', 'dog', 'and', 'a', 'spotted', 'dog', 'are', 'fighting']
输出翻译结果和热力图:
translation, attention = translate_sentence(src, SRC, TRG, model)
print(f'predicted trg = {translation}')
display_attention(src, translation, attention)
结果为:
predicted trg = ['a', 'black', 'dog', 'and', 'a', 'spotted', 'dog', 'fighting', '.', '<eos>']
可以看到,在翻译a的时候,注意力偏重与 ein schwarzer两个词 翻译black的时候,偏重schwarzer词等。
测试集:
example_idx = 18
src = vars(test_data.examples[example_idx])['src']
trg = vars(test_data.examples[example_idx])['trg']
print(f'src = {src}')
print(f'trg = {trg}')
原本的源端和目标端(标签):
src = ['die', 'person', 'im', 'gestreiften', 'shirt', 'klettert', 'auf', 'einen', 'berg', '.']
trg = ['the', 'person', 'in', 'the', 'striped', 'shirt', 'is', 'mountain', 'climbing', '.']
输出翻译结果和热力图:
translation, attention = translate_sentence(src, SRC, TRG, model)
print(f'predicted trg = {translation}')
display_attention(src, translation, attention)
结果为:
predicted trg = ['the', 'person', 'in', 'a', 'striped', 'shirt', 'climbing', 'climbing', 'a', 'mountain', '.', '<eos>']
4.BLEU评价指标
以前我们只关心模型的损失/困惑。然而,有专门为衡量翻译质量而设计的指标——最受欢迎的是BLEU。BLEU根据n-gram分析预测序列和实际目标序列的重叠部分,并没有过多地讨论细节。它会给我们每个序列一个0到1之间的数字,其中1表示完全重叠,即完全转换,尽管通常显示在0到100之间。BLEU是为每个源序列的多个候选翻译而设计的,但是在这个数据集中,每个源只有一个候选翻译。(具体的计算方式原理网上有太多讲解,这里不赘述)。实际上使用只要明白,BLEU越高越好。
from torchtext.data.metrics import bleu_score
def calculate_bleu(data, src_field, trg_field, model, max_len = 50):
trgs = []
pred_trgs = []
for datum in data:
src = vars(datum)['src']
trg = vars(datum)['trg']
pred_trg, _ = translate_sentence(src, src_field, trg_field, model,max_len)
#cut off <eos> token
pred_trg = pred_trg[:-1]
pred_trgs.append(pred_trg)
trgs.append([trg])
return bleu_score(pred_trgs, trgs)
bleu_score = calculate_bleu(test_data, SRC, TRG, model)
print(f'BLEU score = {bleu_score*100:.2f}')
BLEU score = 28.74