Seq2Seq with Attention(注意力模型介绍)

  • A+
所属分类:深度学习
摘要这一篇文章会介绍关于基本的encode-decode-attention的内容。主要内容基于pytorch官网的例子进行修改和加上一些自己的理解。

简介

之前我们介绍过Seq2Seq的模型,Sequence to Sequence Learning with Neural Networks–使用Seq2Seq完成翻译。这一篇文章完成同样的工作,但是我们加上attention来完成。

加上attention其实就是pytorch官网的例子,他就是加上attention来进行实现的。TRANSLATION WITH A SEQUENCE TO SEQUENCE NETWORK AND ATTENTION。在这里详细说明一下这个例子。

这一篇还参考下面的链接:

更多参考资料

关于更多Attention的资料,可以参考下面的两个链接,这两个链接是论文的复现

下面链接是使用Attention做的另外一个应用。

Understanding emotions — from Keras to pyTorch

Seq2Seq with Attention介绍

关于Attention的解释,可以看一下这一篇文章, Attention and Memory in Deep Learning and NLP,我在下面简单说一下。

别人的一些叙述

In the picture above, "Echt",  "Dicke"  and "Kiste" words are fed into an encoder, and after a special signal (not shown) the decoder starts producing a translated sentence. The decoder keeps generating words until a special end of sentence token is produced. Here, the h vectors represent the internal state of the encoder.(Encode-Decode解释)

Seq2Seq with Attention(注意力模型介绍)

Still, it seems somewhat unreasonable to assume that we can encode all information about a potentially very long sentence into a single vector and then have the decoder produce a good translation based on only that.(Encode-Decode不合理的地方)

Let's say your source sentence is 50 words long. The first word of the English translation is probably highly correlated with the first word of the source sentence. But that means decoder has to consider information from 50 steps ago, and that information needs to be somehow encoded in the vector. (有的时候句子很长, 只靠vector是不够的)

We allow the decoder to "attend" to different parts of the source sentence at each step of the output generation. Importantly, we let the model learn what to attend to based on the input sentence and what it has produced so far.(解决办法, 我们使得decode可以得到encoder的每一个输出, 让他自己决定各使用多少权重)

The important part is that each decoder output word y_t now depends on a weighted combination of all the input states, not just the last state. (核心思想)

Seq2Seq with Attention(注意力模型介绍)

A big advantage of attention is that it gives us the ability to interpret and visualize what the model is doing. For example, by visualizing the attention weight matrix when a sentence is translated, we can understand how the model is translating.

自己的想法

下面是一些我的想法,结合了上面的一些内容。我们在没有加入attention的时候,前面整个句子的输入最后就使用encoder的一个output来表示,相当于是没有考虑到对应的关系。就用翻译来举一个例子,如果"i am a boy"->"我是男孩",其中"I"应该和"我"的关系是比较大的。所以我们希望在encode的时候, "I"的输出的权重高一些。于是就有了下面最简单的attention的想法。

我们在decode输入的时候,不仅有单词的embedding,还有前面encoder的输出乘上权重。如下图所示:

Seq2Seq with Attention(注意力模型介绍)

关于attention weight的权重的计算,我们可以直接将embedding和hidden state合并在一起,再经过一个全连接网络,输出的就是权重(具体可以看一下下面decode的实现)

Seq2Seq with Attention(注意力模型介绍)

Attention模型的实现

因为要处理的事情是一样的,在这里我们就不写数据处理的部分了。数据处理见Sequence to Sequence Learning with Neural Networks–使用Seq2Seq完成翻译。我们主要实现网络的部分。

Encoder部分

Encoder部分,与没有Attention的时候是一样的。将每一个word依次输入网络,每个word都会有一个output和hidden state. 使用前一个单词的hidden state作为下一个单词的hidden state.

The encoder of a seq2seq network is a RNN that outputs some value for every word from the input sentence. For every input word the encoder outputs a vector and a hidden state, and uses the hidden state for the next input word.

  1. class EncoderRNN(nn.Module):
  2.     def __init__(self, input_size, hidden_size, batch_size=1, n_layers=1):
  3.         super(EncoderRNN, self).__init__()
  4.         self.input_size = input_size
  5.         self.hidden_size = hidden_size
  6.         self.batch_size = batch_size # 输入的时候batch_size
  7.         self.n_layers = n_layers # RNN中的层数
  8.         self.embedding = nn.Embedding(self.input_size, self.hidden_size)
  9.         self.gru = nn.GRU(self.hidden_size, self.hidden_size)
  10.     def forward(self, x):
  11.         self.sentence_length = x.size(0) # 获取一句话的长度
  12.         embedded = self.embedding(x).view(self.sentence_length, 1, -1) # seq_len * batch_size * word_size
  13.         output = embedded
  14.         self.hidden = self.initHidden()
  15.         output, hidden = self.gru(output, self.hidden)
  16.         return output, hidden
  17.     def initHidden(self):
  18.         return torch.zeros(self.n_layers, self.batch_size, self.hidden_size).to(device)

Decoder with Attention

下面是加上Attention的decode的部分。 整个decoder的结构如下所示。其中橘红色的代表变量,蓝色的代表是需要网络层(需要有参数要学习的)

Seq2Seq with Attention(注意力模型介绍)

我们做一下简单的解释, 解释以下变量大小和具体的步骤。

首先有以下的注意:

encoder_outputs是encoder在每一步的输出。也就是说,如果在encoder中一次输入十个单词, encoder_outputs的大小为(10, feature_size)

权重的计算,是将上一层的hidden_state和这一层的input拼接起来, 过一个全连接层, 输出大小为句子长度。比如上面encoder是10个单词,这里输出大小就是10,相当于对应上面10个encoder的输出的权重。这个对应下面的步骤。

  1. attn_weights = torch.cat((embedded[0],hidden[0]),1)
  2. attn_weights = self.attn(attn_weights)
  3. attn_weights = F.softmax(attn_weights,dim=1)

我们再将attention+embedding的值合并,作为输入,过一个全连接层再输入RNN中。对应下面的步骤:

  1. output = torch.cat((embedded[0],attn_applied[0]),1)
  2. output = self.attn_combine(output).unsqueeze(0)

下面是过RNN,最后就是计算hidden state和output,再输入下一个词汇。

  1. output, hidden = self.gru(output, hidden)

下面是完整的decoder的代码。 每一步的理解可以看上面的过程。

  1. class AttenDecoder(nn.Module):
  2.     def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
  3.         super(AttenDecoder, self).__init__()
  4.         self.hidden_size = hidden_size
  5.         self.output_size = output_size
  6.         self.dropout_p = dropout_p
  7.         self.max_length = max_length
  8.         self.embedding = nn.Embedding(self.output_size, self.hidden_size)
  9.         self.attn = nn.Linear(self.hidden_size*2, self.max_length)
  10.         self.attn_combine = nn.Linear(self.hidden_size*2, self.hidden_size)
  11.         self.dropout = nn.Dropout(self.dropout_p)
  12.         self.gru = nn.GRU(self.hidden_size, self.hidden_size)
  13.         self.out = nn.Linear(self.hidden_size, self.output_size)
  14.     def forward(self, x, hidden, encoder_outputs):
  15.         # x是输入, 这里有两种类型
  16.         # hidden是上一层中隐藏层的内容
  17.         # encoder_outputs里面是encoder的RNN的每一步的输出(不是最后一个的输出)
  18.         embedded = self.embedding(x).view(1,1,-1)
  19.         embedded = self.dropout(embedded)
  20.         # print('embedded.shape',embedded.shape)
  21.         # ----------------------------
  22.         # 下面的attention weight表示:
  23.         # 连接输入的词向量和上一步的hide state并建立bp训练,
  24.         # 他们决定了attention权重
  25.         # -----------------------------
  26.         attn_weights = torch.cat((embedded[0],hidden[0]),1)
  27.         # print('attn_weights1',attn_weights.shape)
  28.         attn_weights = self.attn(attn_weights)
  29.         attn_weights = F.softmax(attn_weights,dim=1)
  30.         # print('attn_weights2',attn_weights.shape)
  31.         # 这是做矩阵乘法
  32.         # 施加权重到所有的语义向量上
  33.         attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))
  34.         # print('attn_applied',attn_applied.shape)
  35.         # 加了attention的语义向量和输入的词向量共同作为输
  36.         # 此处对应解码方式三+attention
  37.         output = torch.cat((embedded[0], attn_applied[0]),1)
  38.         # print('output1',output.shape)
  39.         # 进入RNN之前,先过了一个全连接层
  40.         output = self.attn_combine(output).unsqueeze(0)
  41.         # print('output2',output.shape)
  42.         output = F.relu(output)
  43.         output, hidden = self.gru(output, hidden)
  44.         # print('output3',output.shape)
  45.         # 输出分类结果
  46.         output = F.log_softmax(self.out(output[0]),dim=1)
  47.         # print('output4',output.shape)
  48.         return output, hidden, attn_weights

模型的训练

上面就是关于decoder的定义。下面是在训练的时候还有一些与之前讲的Seq2Seq不一样。因为这里decoder需要encoder全部的output.

首先我们要处理以下encoder_output,把他处理成长度是一样的。这里没有做padding.

  1. encoder_output, encoder_hidden = encoder1(input_tensor.unsqueeze(1))
  2. # 因为一个Decoder的MAX_LENGTH是固定长度的, 所以我们需要将encoder_output变为一样长的
  3. encoder_output = encoder_output.squeeze(1)
  4. encoder_outputs = torch.zeros(max_length, encoder_output.size(1)).to(device)
  5. encoder_outputs[:encoder_output.size(0)] = encoder_output

接下来就是将单词一次输入decoder, 这里还是分为两种模式。

  1. # Decoder
  2. loss = 0
  3. decoder_hidden = encoder_hidden # encoder最后的hidden作为decoder的hidden
  4. decoder_input = torch.tensor([[SOS_token]]).to(device)
  5. # 判断是使用哪一种模式
  6. use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
  7. if use_teacher_forcing:
  8.     # Teacher forcing: Feed the target as the next input
  9.     for di in range(target_length):
  10.         decoder_output, decoder_hidden, attn_weights = decoder(decoder_input, decoder_hidden, encoder_outputs)
  11.         loss = loss + criterion(decoder_output, target_tensor[di])
  12.         decoder_input = target_tensor[di] # Teacher Forcing
  13. else:
  14.     # Without teacher forcing: use its own predictions as the next input
  15.     for di in range(target_length):
  16.         decoder_output, decoder_hidden, attn_weights = decoder(decoder_input, decoder_hidden, encoder_outputs)
  17.         topv, topi = decoder_output.topk(1)
  18.         decoder_input = topi.squeeze().detach() # detach from history as input
  19.         loss = loss + criterion(decoder_output, target_tensor[di])
  20.         if decoder_input.item() == EOS_token:
  21.             break

下面是完整代码。

  1. teacher_forcing_ratio = 0.5 # 50%的概率使用teacher_forcing的模式
  2. def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
  3.     encoder_optimizer.zero_grad()
  4.     decoder_optimizer.zero_grad()
  5.     input_length = input_tensor.size(0)
  6.     target_length = target_tensor.size(0)
  7.     # encoder_outputs = torch.zeros(max_length, encoder.hidden_size).to(device)
  8.     # Encoder
  9.     encoder_output, encoder_hidden = encoder1(input_tensor.unsqueeze(1))
  10.     # 因为一个Decoder的MAX_LENGTH是固定长度的, 所以我们需要将encoder_output变为一样长的
  11.     encoder_output = encoder_output.squeeze(1)
  12.     encoder_outputs = torch.zeros(max_length, encoder_output.size(1)).to(device)
  13.     encoder_outputs[:encoder_output.size(0)] = encoder_output
  14.     # Decoder
  15.     loss = 0
  16.     decoder_hidden = encoder_hidden # encoder最后的hidden作为decoder的hidden
  17.     decoder_input = torch.tensor([[SOS_token]]).to(device)
  18.     # 判断是使用哪一种模式
  19.     use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
  20.     if use_teacher_forcing:
  21.         # Teacher forcing: Feed the target as the next input
  22.         for di in range(target_length):
  23.             decoder_output, decoder_hidden, attn_weights = decoder(decoder_input, decoder_hidden, encoder_outputs)
  24.             loss = loss + criterion(decoder_output, target_tensor[di])
  25.             decoder_input = target_tensor[di] # Teacher Forcing
  26.     else:
  27.         # Without teacher forcing: use its own predictions as the next input
  28.         for di in range(target_length):
  29.             decoder_output, decoder_hidden, attn_weights = decoder(decoder_input, decoder_hidden, encoder_outputs)
  30.             topv, topi = decoder_output.topk(1)
  31.             decoder_input = topi.squeeze().detach() # detach from history as input
  32.             loss = loss + criterion(decoder_output, target_tensor[di])
  33.             if decoder_input.item() == EOS_token:
  34.                 break
  35.     # 反向传播, 进行优化
  36.     loss.backward()
  37.     encoder_optimizer.step()
  38.     decoder_optimizer.step()
  39.     return loss.item() / target_length

模型的验证--可视化Attention

关于模型的训练,查看之前的链接,Sequence to Sequence Learning with Neural Networks–使用Seq2Seq完成翻译,这里就不重复写了。或是可以直接查看文末的代码链接。

我们这里注意以下如何可视化Attention,其实就是将输入每个word的时候计算出的attention_weight来打印出来。我们注意到上面写AttenDecoder的时候,是由return attenwights的。所以最简答的方式就可以像下面这样进行输出。

  1. _, attentions = evaluate(encoder1, decoder1, "je suis trop froid .")
  2. plt.matshow(attentions.cpu().numpy())
Seq2Seq with Attention(注意力模型介绍)

或是我们可以将图画的好看一些,可以使用下面的方式进行实现。(函数来源与Pytorch的官方教程)

  1. # 更好的可视化
  2. def showAttention(input_sentence, output_words, attentions):
  3.     # Set up figure with colorbar
  4.     fig = plt.figure()
  5.     ax = fig.add_subplot(111)
  6.     cax = ax.matshow(attentions.numpy(), cmap='bone')
  7.     fig.colorbar(cax)
  8.     # Set up axes
  9.     ax.set_xticklabels([''] + input_sentence.split(' ') +
  10.                        ['<EOS>'], rotation=90)
  11.     ax.set_yticklabels([''] + output_words)
  12.     # Show label at every tick
  13.     ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
  14.     ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
  15.     plt.show()
  16. def evaluateAndShowAttention(input_sentence):
  17.     output_words, attentions = evaluate(encoder1, decoder1, input_sentence)
  18.     print('input =', input_sentence)
  19.     print('output =', ' '.join(output_words))
  20.     showAttention(input_sentence, output_words, attentions)

我们来看一下最终的效果。

  1. evaluateAndShowAttention("c est un jeune directeur plein de talent .")
Seq2Seq with Attention(注意力模型介绍)

到这里,全部的关于attention实现翻译的内容就已经完成了。下面是notebook的链接。

Attention模型的简单实习-Pytorch

 

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: