Seq2Seq (Attention )的PyTorch 实现(超级详细)
⽂本主要介绍⼀下如何使⽤PyTorch复现Seq2Seq(with Attention),实现简单的机器翻译任务,请先阅读论⽂,之后花上15分钟阅读我的这两篇⽂章,,最后再来看⽂本,⽅能达到醍醐灌顶,事半功倍的效果
数据预处理
数据预处理的代码其实就是调⽤各种API,我不希望读者被这些不太重要的部分分散了注意⼒,因此这⾥我不贴代码,仅⼝述⼀下带过即可如下图所⽰,本⽂使⽤的是德语→英语数据集,输⼊是德语,并且输⼊的每个句⼦开头和结尾都带有特殊的标识符。输出是英语,并且输出的每个句⼦开头和结尾也都带有特殊标识符
不管是英语还是德语,每句话长度都是不固定的,所以我对于每个batch内的句⼦,将它们的长度通过加<PAD>变得⼀样,也就说,⼀个batch内的句⼦,长度都是相同的,不同batch内的句⼦长度不⼀定相同。下图维度表⽰分别是[seq_len, batch_size]随便打印⼀条数据,看⼀下数据封装的形式
在数据预处理的时候,需要将源句⼦和⽬标句⼦分开构建字典,也就是单独对德语构建⼀个词库,对英语构建⼀个词库
Encoder Encoder我是⽤的单层双向GRU 双向GRU的隐藏状态输出由两个向量拼接⽽成,例如,…所有
时刻的最后⼀层隐藏状态就构成了GRU的output
假设这是个m层GRU,那么最后⼀个时刻所有层中的隐藏状态就构成了GRU的final hidden states
其中
所以
根据论⽂,或者你看了我的图解Attention这篇⽂章就会知道,我们需要的是hidden的最后⼀层输出(包括正向和反向),因此我们可以通过hidden[-2,:,:]和hidden[-1,:,:]取出最后⼀层的hidden states,将它们拼接起来记作h =1[;]h 1h T h =2[;]h 2h T −1output ={h ,h ,...h }
12T hidden ={h ,h ,...,h }T 1T 2T m
h =T i [;]h T i h 0i
hidden ={[;],[;],...,[;]}h T 1h 01h T 2h 02h T m h 0m
s 0
最后⼀个细节之处在于,的维度是[batch_size, en_hid_dim*2],即便是没有Attention机制,将作为De
coder的初始隐藏状态也不对,因为维度不匹配,需要将的维度转为[batch_size, src_len, dec_hid_dim],中间的src_len 暂且不谈,⾸先要做的是转为[batch_size,
dec_hid_dim],所以这⾥需要将通过⼀个全连接神经⽹络,进⾏维度转换
Encoder的细节就这么多,下⾯直接上代码,我的代码风格是,注释在上,代码在下
class  Encoder (nn .Module ):
def  __init__(self , input_dim , emb_dim , enc_hid_dim , dec_hid_dim , dropout ):
super ().__init__()
self .embedding = nn .Embedding (input_dim , emb_dim )
self .rnn = nn .GRU (emb_dim , enc_hid_dim , bidirectional = True )
self .fc = nn .Linear (enc_hid_dim * 2, dec_hid_dim )
self .dropout = nn .Dropout (dropout )
def  forward (self , src ):
'''
src = [src_len, batch_size]
'''
src = src .transpose (0, 1) # src = [batch_size, src_len]
embedded = self .dropout (self .embedding (src )).transpose (0, 1) # embedded = [src_len, batch_size, emb_dim]
# enc_output = [src_len, batch_size, hid_dim * num_directions]
# enc_hidden = [n_layers * num_directions, batch_size, hid_dim]
enc_output , enc_hidden = self .rnn (embedded ) # if h_0 is not give, it will be set 0 acquiescently
# enc_hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
# enc_output are always from the last layer
# enc_hidden [-2, :, : ] is the last of the forwards RNN
# enc_hidden [-1, :, : ] is the last of the backwards RNN
# initial decoder hidden is final hidden state of the forwards and backwards
# encoder RNNs fed through a linear layer
# s = [batch_size, dec_hid_dim]
s = torch .tanh (self .fc (torch .cat ((enc_hidden [-2,:,:], enc_hidden [-1,:,:]), dim = 1)))
return  enc_output , s
Attention
attention⽆⾮就是三个公式其中指的就是Encoder中的变量s ,指的就是Encoder中的变量enc_output ,其实就是⼀个简单的全连接神经⽹络
我们可以从最后⼀个公式反推各个变量的维度是什么,或者维度有什么要求
⾸先的维度应该是[batch_size, src_len],这是⽏庸置疑的,那么的维度也应该是[batch_size, src_len],或者是个三维的,但是某个维度值为1,可以通过squeeze()变成两维的。这⾥我们先假设的
维度是[batch_size, src_len, 1],等会⼉我再解释为什么要这样假设继续往上推,变量的维度就应该是[?, 1],?表⽰我暂时不知道它的值应该是多少。的维度应该是[batch_size, src_len, ?]现在已知的维度是[batch_size, src_len, enc_hid_dim*2],⽬前的维度是[batch_size, dec_hid_dim],这两个变量需要做拼接,送⼊全连接神经⽹络,因此我们⾸先需要将的维度变成[batch_size, src_len, dec_hid_dim],拼接之后的维度就变成[batch_size, src_len,
enc_hid_dim*2+enc_hid_dim],于是这个函数的输⼊输出值也就有了
attn = nn .Linear (enc_hid_dim *2+enc_hid_dim , ?)
s 0s 0s 0s 0E =t tanh (attn (s ,H ))t −1=a t ~vE t a =t softmax ()a
t ~s t −1H attn ()a t a t ~a t ~a
t ~v E t H s t −1s t −1attn ()
到此为⽌,除了?部分的值不清楚,其它所有维度都推导出来了。现在我们回过头思考⼀下?设置成多少,好像其实并没有任何限制,所以我们可以设置?为任何值(在代码中我设置?为dec_hid_dim)
Attention细节就这么多,下⾯给出代码
class Attention(nn.Module):
def__init__(self, enc_hid_dim, dec_hid_dim):
super().__init__()
self.attn = nn.Linear((enc_hid_dim *2)+ dec_hid_dim, dec_hid_dim, bias=False)
self.v = nn.Linear(dec_hid_dim,1, bias =False)
def forward(self, s, enc_output):
# s = [batch_size, dec_hid_dim]
# enc_output = [src_len, batch_size, enc_hid_dim * 2]
batch_size = enc_output.shape[1]
src_len = enc_output.shape[0]
# repeat decoder hidden state src_len times
# s = [batch_size, src_len, dec_hid_dim]
# enc_output = [batch_size, src_len, enc_hid_dim * 2]
s = s.unsqueeze(1).repeat(1, src_len,1)
enc_output = anspose(0,1)
# energy = [batch_size, src_len, dec_hid_dim]
energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim =2)))
# attention = [batch_size, src_len]
attention = self.v(energy).squeeze(2)
return F.softmax(attention, dim=1)
Seq2Seq(with Attention)
我调换⼀下顺序,先讲Seq2Seq,再讲Decoder的部分
传统Seq2Seq是直接将句⼦中每个词连续不断输⼊Decoder进⾏训练,⽽引⼊Attention机制之后,我需要能够⼈为控制⼀个词⼀个词进⾏输⼊(因为输⼊每个词到Decoder,需要再做⼀些运算),所以在代码中会看到我使⽤了for循环,循环trg_len-1次(开头的<SOS>我⼿动输⼊,所以循环少⼀次)
并且训练过程中我使⽤了⼀种叫做Teacher Forcing的机制,保证训练速度的同时增加鲁棒性,如果不了解Teacher Forcing可以看我的这篇
思考⼀下for循环中应该要做哪些事?⾸先要将变量传⼊Decoder,由于Attention的计算是在Decoder的内部进⾏的,所以我需要
将dec_input、s、enc_output这三个变量传⼊Decoder,Decoder会返回dec_output以及新的s。之后根据概率对dec_output做Teacher Forcing即可
Seq2Seq细节就这么多,下⾯给出代码
class Seq2Seq(nn.Module):
def__init__(self, encoder, decoder, device):
super().__init__()
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio =0.5):
# src = [src_len, batch_size]
# trg = [trg_len, batch_size]
# teacher_forcing_ratio is probability to use teacher forcing
batch_size = src.shape[1]
trg_len = trg.shape[0]
trg_vocab_size = self.decoder.output_dim
# tensor to store decoder outputs
outputs = s(trg_len, batch_size, trg_vocab_size).to(self.device)
# enc_output is all hidden states of the input sequence, back and forwards
# s is the final forward and backward hidden states, passed through a linear layer
enc_output, s = der(src)
# first input to the decoder is the <sos> tokens
dec_input = trg[0,:]
for t in range(1, trg_len):
# insert dec_input token embedding, previous hidden state and all encoder hidden states # receive output tensor (predictions) and new hidden state
dec_output, s = self.decoder(dec_input, s, enc_output)
# place predictions in a tensor holding predictions for each token
outputs[t]= dec_output
# decide if we are going to use teacher forcing or not
teacher_force = random.random()< teacher_forcing_ratio
# get the highest predicted token from our predictions
top1 = dec_output.argmax(1)
# if teacher forcing, use actual next token as next input
# if not, use predicted token
dec_input = trg[t]if teacher_force else top1
decoder
return outputs
Decoder
Decoder我⽤的是单向单层GRU
Decoder部分实际上也就是三个公式指的是Encoder中的变量enc_output ,指的是将dec_input 经过W
ordEmbedding后得到的结果,函数实际上就是为了转换维度,因为需要的输出是TRG_VOCAB_SIZE ⼤⼩。其中有个细节,GRU的参数只有两个,⼀个输⼊,⼀个隐藏层输⼊,但是上⾯的公式有三个变量,所以我们应该选⼀个作为隐藏层输⼊,另外两个"整合"⼀下,作为输⼊我们从第⼀个公式正推各个变量的维度是什么
⾸先在Encoder中最开始先调⽤⼀次Attention,得到权重,它的维度是[batch_size, src_len],⽽的维度是[src_len, batch_size,enc_hid_dim*2],它俩要相乘,同时应该保留batch_size 这个维度,所以应该先将扩展⼀维,然后调换⼀下维度的顺序,之后再按照batch相乘(即同⼀个batch内的矩阵相乘)
a = a .unsqueeze (1) # [batch_size, 1, src_len]
H = H .transpose (0, 1) # [batch_size, src_len, enc_hid_dim*2]
c = torch .bmm (a , h ) # [batch_size, 1, enc_hid_dim*2]
前⾯也说了,由于GRU不需要三个变量,所以需要将和整合⼀下,实际上就是Seq2Seq类中的dec_input 变量,它的维度是[batch_size],因此先将扩展⼀个维度,再通过WordEmbedding,这样他就变成[batch_size, 1, emb_dim]。最后对和进⾏concat
y = y .unsqueeze (1) # [batch_size, 1]
emb_y = self .emb (y ) # [batch_size, 1, emb_dim]
rnn_input = torch .cat ((emb_y , c ), dim =2) # [batch_size, 1, emb_dim+enc_hid_dim*2]
的维度是[batch_size, dec_hid_dim],所以应该先将其拓展⼀个维度
rnn_input = rnn_input .transpose (0, 1) # [1, batch_size, emb_dim+enc_hid_dim*2]
s = s .unsqueeze (1) # [batch_size, 1, dec_hid_dim]
# dec_output = [1, batch_size, dec_hid_dim]
# dec_hidden = [1, batch_size, dec_hid_dim] = s (new, is not s previously)
dec_output , dec_hidden = self .rnn (rnn_input , s )
最后⼀个公式,需要将三个变量全部拼接在⼀起,然后通过⼀个全连接神经⽹络,得到最终的预测。我们先分析下这个三个变量的维度,的维度是[batch_size, 1, emb_dim],的维度是[batch_size, 1, enc_hid_dim],的维度是[1, batch_size, dec_hid_dim],因此我们可以像下⾯这样把他们全部拼接起来
emd_y = emb_y .squeeze (1) # [batch_size, emb_dim]
c = w .squeeze (1) # [batch_size, enc_hid_dim*2]
s = s .squeeze (0) # [batch_size, dec_hid_dim]
fc_input = torch .cat ((emb_y , c , s ), dim =1) # [batch_size, enc_hid_dim*2+dec_hid_dim+emb_hid]
以上就是Decoder部分的细节,下⾯给出代码(上⾯的那些只是⽰例代码,和下⾯代码变量名可能不⼀样)c =a H
t s =t GRU (emb (y ),c ,s )
t t −1=y t ^f (emb (y ),c ,s )
t t H emb (y )t f ()a t H a t H emb (y )t c y t y t c emb (y )t s t −1emb (y )t c s t

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。