目录
1.attention机制的引入
2.attention机制的计算方式
3.模型实现
3.1数据准备
3.2模型建立
3.3训练
1.attention机制的引入
在上一节的seq2seq模型中,为了在解码的时候,使得解码器以及线性分类层可以有直接获取更多信息,我们将原始上下文向量提供给每一个解码器,将原始上下文向量和词嵌入提供给每一个线性层,如下所示 :
但是依然没能改变一个本质的地方,上下文向量被过度压缩了。即经过多个时间步的编码器压缩,之前的信息保留的越小越少。而解码器在翻译的时候,对之前信息获取很少,进一步导致模型质量下降。《NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE》提出attention机制,对每一个编码器和线性层输入的原始context向量进行了改进。所谓的上下文向量不再是最后经过编码器压缩以后的稠密向量,而是将每一个编码器的输出与当前解码器输入的隐藏状态进行一个“匹配”计算,为每个编码器的输出计算出一个“匹配”分数,然后将所有的匹配分数进行softmax计算转变为其权重概率,将权重概率与对应的编码器输出进行加权求和得到上下文向量,模型如下所示:
首先,s1输入的隐藏状态为原始上下文向量z(后面解码器则为s_t-1),将z与每一个编码器的输出[h1,h2,h3,h4]进行一个“匹配”计算(计算方式很多),得到一个匹配分数[score1,score2,score3,score4]。我们将其转变为权重形式,即对这个匹配分数进行softmax计算得到权重矩阵a[a1,a2,a3,a4]。然后计算其当前编码器对应注意力的上下文向量w=a1*h1+a2h2+a3h3+a4h4。这个注意力得出的上下文向量优势在于:(1)其没有经过过多压缩,降低了信息的损失。之前输入的context向量都是编码器最后的一个输出,经过编码器多重编码造成了之前有用信息的损失,而注意力向量是每一个编码器输出直接参与最后的上下文向量计算,每一个编码器信息的损失是根据解码器的实际需求来进行的。(2)每一个解码器都有其特有的注意力权重矩阵,因为每一个解码器输入的隐藏状态不同,其得到的权重矩阵也不相同。例如我们在解码第一个词的时候,其有用信息大概率与第一个编码器的输出有关系。之前的原始上下文向量对第一个编码器信息的保留很少,而注意力向量可以通过其权重矩阵,决定偏向哪一个编码器(权重越接近1,保留越多。越接近0保留越少)。
此外,我们考虑到LSTM偏向于最新的编码器,所以之前采用倒序的手段,这里介绍另一个新的技巧:双向LSTM,如下所示:
正序LSTM偏向句子的后半段,在反向LSTM偏向于句子的前半段。将两者的输出进行一个拼接得到双向LSTM的最终输出。切记双向LSTM不是双层,可以理解为两个独立的LSTM,一个将句子从左往右输入,另一个将句子从右往左输入,最终将二者的输出进行拼接得到最终输出,所以其输出的维度变为原来的两倍。最后编码器输出的隐状态之前可以直接用来作为原始上下文向量(假设编码器解码器的隐状态为一个维度),但是现在不可以了,将最后编码器输出的隐状态通过一个线性层降维到解码的维度,并经过一个tanh激活函数。
2.attention机制的计算方式
这里按照一般情况介绍attention机制:
attention机制的基本流程:对一个句子序列
,其由单词序列
构成。(1)应用某种方法将
中的每个单词
编码为一个独特向量
(2)解码时,使用学习到的注意力权重
对1中所得的所有向量做加权线性组合
(3)在解码器进行下个词预测时,使用(2)中得到的线性组合。
具体attention核心解释如下:
我们的最终目标是要能够帮助decoder在生成词语时,有一个不同词语的权重参考。在训练时,对于decoder我们是有训练目标的,此时将decoder中的信息定义为一个Query(学习目标)。在encoder中包含了所有可能出现的词语,我们将其作为一个字典,该字典的key为所有encoder的序列信息。N个单词相当于当前字典中有n条记录,而字典的value通常也是所有encoder的序列信息。(一般情况下,KV相等)下一步是注意力权重计算,由于我们要让模型自己去学习应该对那些语素重点关注,因此我们要用学习目标Query来参与这个过程,因此对于Query的每个向量,通过一个函数来计算预测t时刻时候,注意力的权重。由于encoder序列长度为n,因此,应该是一个n维的向量,由于权重和为1,所以需要对该向量进行softmax归一化,让向量的每一维元素都是一个概率值。
权重注意力计算方式:
(1)多层感知机方法(常用)
主要是先将query和key进行拼接,然后接一个激活函数tanh的全连接(本文就到这一步),然后再与一个网络定义的权重矩阵做乘积。这种方法据说对于大规模的数据特别有效。
(2)Bilinear方法
通过一个句子句子中直接建立q和k的映射关系,比较直接,且计算速度较快。
(3)Dot Product
这个方法更直接,连权重矩阵都省了,直接建立q和k的关系映射,优点是计算速度更快了,且不需要参数,降低了模型的复杂度。但是需要q和k的维度要相同。
(4)scaled-dot Product(常用,self-attention也用到了)
上面的点积方法有一个问题,就是随着向量维度的增加,最后计算得到的分数会很大,softmax得到改了后,在反向传播时在进行softmax计算等反向求导的时候,梯度会很小(趋近于0)。因此对其进行归一化:(可以参考资料)
d_k表示q,k的维度。
3.模型实现
3.1数据准备
工具:Jupyter
数据依然和前面的一样,处理过程也是一样的。
import torch
import spacy
from torchtext.data import Field,BucketIterator
from torchtext.datasets import Multi30k
de_seq=spacy.load("de_core_news_sm")
en_seq=spacy.load("en_core_web_sm")
def src_token(text):
return [word.text for word in de_seq.tokenizer(text)]
def trg_token(text):
return [word.text for word in en_seq.tokenizer(text)]
SRC=Field(tokenize=src_token,
init_token="<sos>",
eos_token="<eos>",
lower=True)
TRG=Field(tokenize=trg_token,
init_token="<sos>",
eos_token="<eos>",
lower=True)
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)
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
batch_size=128
train_iter,val_iter,test_iter=BucketIterator.splits(
(train_data,val_data,test_data),
batch_size=batch_size,
device=device
)
测试:
for example in train_iter:
print(example.src.shape,example.trg.shape)
break
结果:
torch.Size([37, 128]) torch.Size([35, 128])
提前将测试的batch放前面:
src=example.src.permute(1,0)
trg=example.trg.permute(1,0)
3.2模型建立
import torch.nn as nn
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.embeding=nn.Embedding(src_vocab_size,emb_size,padding_idx=0)
self.rnn=nn.GRU(emb_size,enc_hidden_size,bidirectional=True,batch_first=True)
self.fc=nn.Linear(2*enc_hidden_size,dec_hidden_size)
self.dropout=nn.Dropout(dropout)
def forward(self,src):
#src[bactch src_len]
src=self.dropout(self.embeding(src))
#src[batch src_len emb_size]
outputs,f_n=self.rnn(src)
#outputs[batch src_len enc_hidden_size*2]
#f_n[2 batch enc_hidden_size]
forward_hidden=f_n[-2,:,:]
backward_hidden=f_n[-1,:,:]
#forward_hidden[batch enc_hidden_size]
#backward_hidden[batch enc_hidden_size]
f_b_hidden=torch.cat((forward_hidden,backward_hidden),dim=1)
#f_b_hidden[batch enc_hidden_size*2]
f_n=self.fc(f_b_hidden).unsqueeze(0)
#f_b_hidden[1 batch dec_hidden_size]
return outputs,f_n
src_vocab_size=len(SRC.vocab)
trg_vocab_size=len(TRG.vocab)
emb_size=256
enc_hidden_size=512
dec_hidden_size=512
测试:
enModel=Encoder(src_vocab_size,emb_size,enc_hidden_size,dec_hidden_size).to(device)
enOutputs,h_n=enModel(src)
print(enOutputs.shape,h_n.shape)
结果:
torch.Size([128, 37, 1024]) torch.Size([1, 128, 512])
class Attention(nn.Module):
def __init__(self,enc_hidden_size,dec_hidden_size):
super(Attention,self).__init__()
self.attn=nn.Linear(enc_hidden_size*2+dec_hidden_size,dec_hidden_size)
self.v=nn.Linear(dec_hidden_size,1,bias=False)
def forward(self,encode_outputs,h_n):
#encode_outputs[batch src_len 2*enc_hidden_size]
#h_n[1 batch dec_hidden_size]
src_len=encode_outputs.shape[1]
h_n=h_n.permute(1,0,2).repeat(1,src_len,1)#复制src_len隐状态分别于encoder输出进行attn
#h_n[batch src_len dec_hidden_size]
inputs=torch.cat((encode_outputs,h_n),dim=2)
#inputs[batch src_len 2*enc_hidden_size+dec_hidden_size]
energy=self.attn(inputs)
#energy[batch sec_len dec_hidden_size]
scores=self.v(energy).squeeze()
#score[batch sec_len]
return nn.functional.softmax(scores,dim=1)
测试:
attnModel=Attention(enc_hidden_size,dec_hidden_size).to(device)
a=attnModel(enOutputs,h_n)
print(a.shape,a.sum(dim=1))
结果:
torch.Size([128, 37]) 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,en_hidden_size,dec_hidden_size,attention,dropout=0.5):
super(Decoder,self).__init__()
self.attn=attention
self.emb=nn.Embedding(trg_vocab_size,emb_size)
self.rnn=nn.GRU(emb_size+en_hidden_size*2,dec_hidden_size,batch_first=True)
self.fn=nn.Linear(emb_size+en_hidden_size*2+dec_hidden_size,trg_vocab_size)
self.dropout=nn.Dropout(dropout)
def forward(self,input_trg,h_n,en_outputs):
#input_trg[batch]
#h_n[batch dec_hidden_size]
#en_outputs[batch src_len en_hidden_size*2]
input_trg=input_trg.unsqueeze(1)
#input_trg[batch 1]
input_trg=self.dropout(self.emb(input_trg))
#input_trg[batch 1 emb_size]
a=self.attn(en_outputs,h_n)
#a[batch src_len]
a=a.unsqueeze(1)
#a[batch 1 src_len]
weighted=torch.bmm(a,en_outputs)
#weighted[batch 1 en_hidden_size*2]
concat=torch.cat((input_trg,weighted),dim=2)
#concat[batch 1 en_hidden_size*2+emb_size]
h_n=h_n.unsqueeze(0)
#h_n[1 batch de_hidden_size]
output,h_n=self.rnn(concat,h_n)
#output[batch 1 dec_hidden_size],h_n[1 batch de_hidden_size]
concat=torch.cat((concat,output),dim=2)
#concat[batch 1 en_hidden_size*2+emb_size+de_hidden_size]
concat=concat.squeeze()
#concat[batch en_hidden_size*2+emb_size+de_hidden_size]
output=self.fn(concat)
#output[batch trg_vocab_size]
return output,h_n.squeeze()
测试:
trg_i=trg[:,1]
deModel=Decoder(trg_vocab_size,emb_size,enc_hidden_size,dec_hidden_size,attnModel).to(device)
deOutputs,h_n=deModel(trg_i,h_n,enOutputs)
print(deOutputs.shape,h_n.shape)
结果:
(torch.Size([128, 5893]), torch.Size([1, 128, 512]))
import random
class Seq2Seq(nn.Module):
def __init__(self,encoder,decoder):
super(Seq2Seq,self).__init__()
self.encoder=encoder
self.decoder=decoder
def forward(self,src,trg,teach_threshold=0.5):
#src[batch src_len]
#trg[batch trg_len]
en_outputs,h_n=self.encoder(src)
#en_outputs[batch src_len enc_hidden_size*2]
#h_n[1 batch dec_hidden_size]
batch=trg.shape[0]
trg_len=trg.shape[1]
#保存输出
outputs=torch.zeros(batch,trg_len,trg_vocab_size).to(device)
inputs=trg[:,0]
for t in range(1,trg_len):
output,h_n=self.decoder(inputs,h_n,en_outputs)
#output[batch trg_vocab_size]
#h_n[1 batch dec_hidden_size]
outputs[:,t,:]=output
probabilty=random.random()
inputs=trg[:,t] if probabilty<teach_threshold else output.argmax(1)
return outputs
测试:
model=Seq2Seq(enModel,deModel).to(device)
outputs=model(src,trg)
outputs[0,0,:],outputs[0,1,:]
结果:
(tensor([0., 0., 0., ..., 0., 0., 0.], device='cuda:0',
grad_fn=<SliceBackward>),
tensor([-0.0026, 0.5602, -0.0122, ..., 0.2537, -0.6007, -0.0582],
device='cuda:0', grad_fn=<SliceBackward>))
3.3训练
import time,math
from torch.optim import Adam
epochs=10
optim=Adam(model.parameters())
criterion=nn.CrossEntropyLoss(ignore_index=1)
def init_weights(m):
for name, param in m.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
def train(model,data_iter,optim,criterion,clip):
model.train()
lossAll=0
for example in data_iter:
src=example.src.permute(1,0)
trg=example.trg.permute(1,0)
output=model(src,trg)
optim.zero_grad()
#output[batch trg_len trg_vocab_size]
output=output[:,1:,:].reshape(-1,trg_vocab_size)
trg=trg[:,1:].reshape(-1)
loss=criterion(output,trg)
loss.backward()
#截断梯度,对梯度高于1的直接设置为1
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.permute(1,0)
trg=example.trg.permute(1,0)
output=model(src,trg)
#output[batch trg_len trg_vocab_size]
output=output[:,1:,:].reshape(-1,trg_vocab_size)
trg=trg[:,1:].reshape(-1)
loss=criterion(output,trg)
lossAll+=loss.item()
return lossAll/len(data_iter)
for epoch in range(epochs):
start_time = time.time()
train_loss = train(model,train_iter, optim, criterion,clip=1)
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}')
实验结果: