这篇博客是对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。
下面几种的核心问题在于:如何使用编码向量组,为了达到较好的效果,网络至少要学习到以下两点:

  1. 不同的编码向量组的不同用法
  2. 不同层对编码向量组的不同用法

最简单的一种是直接把encoder的out组输入到每层decoder中

cnn lstm pytorch实现 pytorch lstm attention_词向量

如果输入仅仅是前一层的编码向量组,网络压力可想而知。

我们来想一下,此时可用信息有什么?

cnn lstm pytorch实现 pytorch lstm attention_权重_02


需要辅助信息。

在gru的一层,可用信息有哪些?

  1. 前一层的输出,也就是前一层的预测结果
  2. 前一层的状态
  3. 编码向量组

gru是没有状态的,单层gru的out等于hidden

cnn lstm pytorch实现 pytorch lstm attention_ci_03


信息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。