这篇博客是对https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html#sphx-glr-intermediate-seq2seq-translation-tutorial-py中一些问题的解惑,可以配合使用,有其他不理解的也欢迎讨论。
原实验运行在老版本的python和torch上,在当前版本已经做不到copy&paste and play了,因此,我修改了一些代码。
整个实验最有趣的是Attention机制的实现方法,即decoder网络搭建。
这篇博客的内容是:Decoder网络结构,Encoder网络结构和训练。
Encoder结构
class Encoder(nn.Module):
def __init__(self,V,H):
super(Encoder,self).__init__()
#emb的size和hidden的size设置相同
self.hidden_size=H
self.embedding=nn.Embedding(V,H)
self.gru=nn.GRU(H,H)
def forward(self, input, hidden):
embedded = self.embedding(input).view(1, 1, -1)
output = embedded
output, hidden = self.gru(output, hidden)
return output, hidden
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size)
encoder依然使用embedding+rnn的结构,这里采用的rnn变种是gru。
网络参数在上篇博客说过,这篇博客主要说一下forward函数的写法。
这里原作者采取的训练方法是每次训练投入单个单词,这一点体现在:
embedded = self.embedding(input).view(1, 1, -1)
实际上encoder的训练可以使用单个单词,也可以使用一个单词序列,二者的本质一样,只要能获得每一步的隐变量就好,作者尝试了后者,效果没有区别。
decoder
decoder是本实验最有趣的部分:
attention的idea,decoder网络结构,forward写法及训练思路
参考:深度解析Pytorch Seq2Seq Toturials 非常感谢!
class AttnDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.dropout_p = dropout_p
self.max_length = max_length
self.embedding = nn.Embedding(self.output_size, self.hidden_size)
self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
self.gru = nn.GRU(self.hidden_size, self.hidden_size)
self.out = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input, hidden, encoder_outputs):
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
#input是上一步的单词与上一步的状态拼接,作为attention的参数,考虑哪些encoder状态是值得参考的
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1) # 连接输入的词向量和上一步的hide state并建立bp训练,他们决定了attention权重
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0)) # 施加权重到所有的语义向量上
#使用前一次的单词和加权过的encoder语义
output = torch.cat((embedded[0], attn_applied[0]), 1) # 加了attention的语义向量和输入的词向量共同作为输入,此处对应解码方式三+attention
output = self.attn_combine(output).unsqueeze(0) # 进入RNN之前,先过了一个全连接层
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1) # 输出分类结果
return output, hidden, attn_weights
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size)
Encoder&Attention
seq2seq模型中encoder的写法是相对固定的,每层lstm的输入都是单词和上一层的状态,但是decoder的输入就有多种形式了。
注意,以下四种方式的decoder的状态都是前一层的状态,第一层decoder是最后一层encoder的状态,他们的不同之处在于输入。
我认为,以下几种在实作中都是可行的,因为网络结构未必是图片上给出的那样简单,他们的本质区别在于输入不同,输入决定信息量,信息决定结构,信息和结构决定performance。
下面几种的核心问题在于:如何使用编码向量组,为了达到较好的效果,网络至少要学习到以下两点:
- 不同的编码向量组的不同用法
- 不同层对编码向量组的不同用法
最简单的一种是直接把encoder的out组输入到每层decoder中
如果输入仅仅是前一层的编码向量组,网络压力可想而知。
我们来想一下,此时可用信息有什么?
需要辅助信息。
在gru的一层,可用信息有哪些?
- 前一层的输出,也就是前一层的预测结果
- 前一层的状态
- 编码向量组
gru是没有状态的,单层gru的out等于hidden
信息1和信息2的不同在于,信息1是gru的out经过linear和softmax之后得到的,他的物理意义是上一次的预测结果。
attention机制的核心就是使用信息1和2,训练一个辅助网络来利用信息3。
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
#input是上一步的单词与上一步的状态拼接,作为attention的参数,考虑哪些encoder状态是值得参考的
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1) # 连接输入的词向量和上一步的hide state并建立bp训练,他们决定了attention权重
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0)) # 施加权重到所有的语义向量上
将上一层单词embedding过之后和上一层的状态拼接,训练一个权重网络(attn),将权重乘到编码向量组(encoder_outputs)上,以编码向量组和上层单词的拼接作为gru的输入。
Train
Encoder
encoder_outputs = torch.zeros(max_length, encoder.hidden_size)
loss = 0
for ei in range(input_length): #
encoder_output, encoder_hidden = encoder(
input_variable[ei], encoder_hidden)
# encoder_outputs[ei] = encoder_output[0][0] #
encoder_outputs[ei] = encoder_hidden
逐个单词投入,手动记录编码向量组(encoder_outputs)。
原文里使用
encoder_outputs[ei] = encoder_output[0][0]
对于单层gru来说,out和hidden是相同的,我改成
encoder_outputs[ei] = encoder_hidden
也顺利完成,encoder_hidden[0][0]也可以。
encoder_outputs = torch.zeros(max_length, encoder.hidden_size)
这一句话很重要,他的本质是给出一个固定长度的编码向量,换句话说,无论本次输入序列的长度如何,投入到decoder的编码向量是固定长度的,不足的地方用零填充。
如果使用这种训练方式,则需要手动填充零tensor。
Decoder
decoder_input = torch.LongTensor([[SOS_token]])
decoder_hidden = encoder_hidden
use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
if use_teacher_forcing:
# 利用已知的上一步真实的单词去预测下一个单词
# Teacher forcing: Feed the target as the next input
for di in range(target_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_outputs)
loss += criterion(decoder_output, target_variable[di])
decoder_input = target_variable[di] # Teacher forcing
else:
# 利用自己上一步预测的单词作为输入预测下一个单词
# Without teacher forcing: use its own predictions as the next input
for di in range(target_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_outputs)
topv, topi = decoder_output.data.topk(1) # topk返回的前k最大值及其索引
ni = topi[0][0]
decoder_input = torch.LongTensor([[ni]])
loss += criterion(decoder_output, target_variable[di])
if ni == EOS_token: # 遇到EOS,表示翻译句子停止了
break
动态长度
encoder长度是输入时确定的,而decoder是不确定的,并非像实验解惑3中tagger那样是一对一的关系,因此,需要一种动态长度机制。
decoder_input = torch.LongTensor([[SOS_token]]) #SOS,翻译开始
if ni == EOS_token: # EOS,翻译停止
break
实验以SOS和EOS标记开始词和结尾词,如果在分析的过程中发现抵达结尾,则翻译停止,实现动态长度。
teacher forcing
# teacher forcing
decoder_input = target_variable[di]
# not teacher forcing
topv, topi = decoder_output.data.topk(1)
ni = topi[0][0]
decoder_input = torch.LongTensor([[ni]])
decoder有两种前向预测,一种是ground truth,一种是自己的预测,使用前者训练被称为teacher forcing,使用后者称为not forcing。