作者:William Falcon



导读


之前通过动画介绍了RNN,介绍了attention,那么,今天再用动画给大家介绍下如何在RNN中使用attention来构建transformer。


cnn attention lstm_权重

给你的神经网络增加注意力机制有点像想在工作的时候睡个午觉。你知道这样对你更好,每个人都想做,但是每个人都害怕

我今天的目标是不做任何假设,用动画来解释细节,让数学再次变得伟大!

循环神经网络(RNN)

RNNs让我们在神经网络中对序列建模。虽然还有其他序列建模的方法,但是RNNs特别有用。RNNs有两种类型,LSTMs和GRUs。

让我们来看看机器翻译,这是一个RNN的具体例子。

1. 想象一下我们有一个具有56个隐藏单元的RNN。

rnn_cell = rnn_cell(input_dim=100, output_dim=56)

2. 我们有个单词“NYU”,用一个整数11来表示,意思是这是我自己构建的词典中的第12个单词。

# 'NYU' is the 12th word in my vocab	
word = 'NYU'	
word = VOCAB[word]	
print(word)	
# 11

我们不在RNN里面使用这样的整数,我们使用了一个更高维度的表示,我们目前通过embeddings获得这个表示。嵌入使我们能够将离散的序列映射到连续空间。

embedding_layer = Embedding(vocab_size=120, embedding_dim=10)	
# project our word to 10 dimensions	
x = embedding_layer(x)

RNN单元接受两个输入,一个单词x,和一个来自前一个时间步骤h的隐藏状态。在每个时间步上,它输出一个新的h

cnn attention lstm_神经网络_02



RNN CELL: next h= f(x, prevh).

提示:第一步的h通常是全0

# 1 word, RNN has 56 hidden units 	
h_0 = np.zeros(1, 56)

有一点重要:RNN单元与RNN是不一样的

RNN术语中有一个主要的困惑点。在PyTorch和TensorFlow这样的深层学习框架中,RNN单元是进行这种计算的单元:

h1 = rnn_cell(x, h0)

RNN网络是RNN单元在时间步上的循环

def RNN(sentence):  	
  prev_h = h_0	
  all_h = []	
  for word in sentence:	
    # use the RNN CELL at each time step	
    current_h = rnn_cell(embed(word), prev_h)	
    all_h.append(current_h)	
  # RNNs output a hidden vector h at each time step	
  return all_h

下面是一个RNN随时间移动相同RNN单元的例子:

cnn attention lstm_神经网络_03


RNN会随着时间移动RNN单元。注意,我们将使用每一步生成的所有h

序列到序列的模型(Seq2Seq)

现在你已经是RNNs的专家了,但是让我们放松一下。

cnn attention lstm_cnn attention lstm_04

RNNs可以作为更大的深度学习系统的模块。

其中一个系统是Bengio的团队和谷歌引入的Seq2Seq模型,该模型可用于将序列翻译成另一个序列。你可以把很多问题归结为翻译:

  1. 英语翻译成西班牙语
  2. 视频序列转换成另外一种序列
  3. 把指令序列转换成代码
  4. 把用户行为转换成用户行为特征
  5. 唯一的限制就是你的创造力!

seq2seq模型其实就是2个RNNs、一个编码器(E)和一个解码器(D)。

class Seq2Seq(object):	
  def __init__():	
      self.encoder = RNN(...)	
      self.decoder = RNN(...)

seq2seq模型有两个主要步骤:

Step 1: 对序列进行编码:

sentence = ["NYU", "NLP", "rocks", "!"]	
all_h = Seq2Seq.encoder(sentence)	
# all_h now has 4 h (activations)


cnn attention lstm_神经网络_03


Encoding

Step 2: 解码并生成“translation.”

这部分非常复杂。前一步中的编码器一次性处理了整个序列(即:它是一个普通的RNN)。

在第二步中,我们每次运行解码器RNN的一个时间步,以生成自回归预测(使用上一步的输出作为下一步的输入是很有趣的)。

解码有两种主要方法:

Option 1: 贪心解码

  1. 运行解码器的一个时间步
  2. 选择概率最高的输出
  3. 把这个输出作为下个时间步的输入
# you have to seed the first x since there are no predictions yet	
# SOS means start of sentence	
current_X_token = '<SOS>'	
# we also use the last hidden output of the encoder (or set to zero)	
h_option_1 = hs[-1]	
h_option_2 = zeros(...)	
# let's use option 1 where it's the last h produced by the encoder	
dec_h = h_option_1	
# run greedy search until the RNN generates an End-of-Sentence token	
while current_X_token != 'EOS':	
   # keep the output h for next step	
   next_h = decoder(dec_h, current_X_token)	
   # use new h to find most probable next word using classifier	
   next_token = max(softmax(fully_connected_layer(next_h)))	
   # *KEY* prepare for next pass by updating pointers	
   current_X_token = next_token	
   dec_h = next_h

这叫做贪心算法,因为我们总是用概率最高的作为下一个词。

Option 2: Beam Search

还有一种更好的技术叫做Beam Search,它考虑解码过程中的多条路径。通俗地说,宽度为5的Beam Search意味着我们考虑具有最大对数似然的5个可能序列(5个最可能序列的数学语言)。

在高层次上,我们不进行最高概率的预测,而是保留前k个(beam size = k)。请注意,在每个时间步中我们有5个选项(概率最高的前5个)。

cnn attention lstm_神经网络_06

因此,完整的seq2seq过程,把贪心解码方法通过动画来描述,将“NYU NLP is awesome”* 翻译成西班牙语,是这样的:

cnn attention lstm_编码器_07


Seq2Seq由2个RNNs,一个编码器和一个解码器组成 该模型由以下几个部分组成:


  1. 蓝色RNN是编码器。
  2. 红色RNN是解码器。
  3. 解码器上方的蓝色矩形是带softmax的全连接层,选择概率最大的最为下一个单词。

注意力机制

好了,现在我们已经讨论了所有的先决条件,让我们来看看好的方面。

如果你注意到前面的动画,解码器只查看由编码器生成的最后一个隐藏向量。

cnn attention lstm_权重_08

结果,RNN很难记住在这个向量序列上发生的所有事情 。例如,当编码器处理完输入序列时,可能已经忘记了“NYU”这个单词。

Attention试着去解决这个问题

当你给一个模型加上注意力机制时,你允许它在每一个解码步中查看编码器产生的所有h。

为了做到这一点,我们使用一个单独的网络,通常是一个全连接的层来计算解码器想要查看所有h的哪些内容。这就是所谓的“注意机制”。

想象一下,对于我们生成的所有h,我们实际上只取其中的一小部分。它们的和称为上下文向量c.

cnn attention lstm_神经网络_09

标量0.3、0.2、0.4、0.1称为注意力权重,在原始论文中,你会在第三页找到同样的方程:

cnn attention lstm_编码器_10


α是通过神经网络+softmax生成的

这些权值是由一个小的神经网络以这种方式产生的:

# attention is just a fully connected layer and a final projection	
attention_mechanism = nn.Linear(input=h_size+x_size, attn_dim=20)	
final_proj_V = weight_matrix(attn_dim)	
# encode the full input sentence to get the hs we want to attend to	
all_h = encoder(["NYU", "NLP", "is", "awesome"]	
# greedy decoding 1 step at a time until end of sentence token	
current_token = '<SOS>'	
while current_token != '<EOS>':	
   # attend to the hs first	
    attn_energies = []	
    for h in all_h:	
      attn_score = attention_mechanism([h,current_token])	
      attn_score = tanh(attn_score)	
      attn_score = final_proj_V.dot(attn_score)	
      # attn_score is now a scalar (called an attn energy)	
      attn_energies.append(attn_score)	
   # turn the attention energies into weights by normalizing	
   attn_weights = softmax(attn_energies)	
   # attn_weights = [0.3, 0.2, 0.4, 0.1]

现在我们有了权值,我们使用它们来提取h,这可能与正在解码的特定token有关

context_vector = attn_weights.dot(all_h)	
# this is now a vector which mixes a bit of all the h's

让我们把它分成几个步骤:

  1. 我们编码了完整的输入序列,并生成了h的列表。
  2. 我们开始解码,解码器使用贪心搜索。
  3. 我们没有把h4给解码器,而是给它一个上下文向量。
  4. 为了生成上下文向量,我们使用另一个网络和可学习权值V来评估每个h与当前解码token的相关性。
  5. 我们将这些注意力能量归一化,并使用它们将所有的h综合到一个h中,希望能捕捉到所有h的相关部分,即上下文向量
  6. 现在我们再次执行解码步骤,但这次使用上下文向量而不是h4。

Attention可视化

注意力权重告诉我们每个h有多重要。这意味着我们还可以在每个解码步骤中可视化权重。这里有一个例子,来自最初的注意力论文:

cnn attention lstm_cnn attention lstm_11

在第一行中,为了翻译“L’”,网络在“the”这个词上使用了alpha,并将其余部分归零。

为了产生“economique”这个词,该网络实际上把alphas的一些权重放在了“European Economic”上,并将其余权重归零。这表明当翻译关系是多对一或一对多时,注意力是有用的。

Attention可以变得很复杂

cnn attention lstm_权重_12

不同类型的Attention

这种类型的注意力只使用由编码器生成的h。有大量的研究在改进这个过程。例如:

  1. 只使用其中一些h,也许是你当前解码的时间步周围的那些h(局部注意)。
  2. 除了h的使用解码器已经生成的那些h,我们之前扔掉了。
  3. ...

如何计算attention energies

另一个研究领域是如何计算注意力得分。除了和V做点积,研究人员还尝试了:

  1. 对点积进行缩放。
  2. Cosine(s, h)
  3. 不使用V矩阵,而是将softmax应用于全连接层。
  4. ...

在计算attention energies的时候需要用些什么

这个研究的最后一个领域是研究到底该用什么来和h向量作比较。

为了给大家对我要表达的意思有一些直观的认识,可以把计算注意力看作是一个键值字典。关键是要给你的注意力网络“查”到最相关的上下文。字典的值就是最相关的上下文。

我在这里描述的方法只使用当前token和每个h来计算注意力得分。那就是:

# calculate how relevant that h is	
score_1 = attn_network([embed("<SOS"), h1])	
score_2 = attn_network([embed("<SOS"), h2])	
score_3 = attn_network([embed("<SOS"), h3])	
score_4 = attn_network([embed("<SOS"), h4])

但实际上我们可以给它任何我们认为有用的东西来帮助注意力网络做出最好的决定。也许我们还可以给它最后一个上下文向量!

score_1 = attn_network([embed("<SOS>"), h1, last_context])

或者我们给它一些不同的东西,也许是一个token让它知道它在解码西班牙语

score_1 = attn_network([embed("<SOS>"), h1, last_context, embed('<SPA>')])

有无穷多的可能性!

实现细节

cnn attention lstm_编码器_13

如果你决定自己实现一下,这里有一些建议供你参考。

Here are some tips to think about if you decide to implement your own.

  1. 使用Facebook的实现,这个确实优化的很好了。

好吧,那只是个借口。下面是一些实际的建议。

  1. 记住seq2seq有两个部分:解码器RNN和编码器RNN。这两个是分开的。
  2. 大部分工作都用于构建解码器,编码器只是在整个输入序列上运行编码器。
  3. 记住,解码器RNN每次只执行一个时间步。这是关键!
  4. 记住,解码器RNN每次只执行一个步骤。重要的事情值得再说一遍:)
  5. 解码算法有两种选择,贪心搜索或beam search。贪心更容易实现,但是大多数时候,beam search会给你更好的结果。
  6. Attention是可选的!但是,当你用它的时候,影响是巨大的。
  7. Attention是一个独立的网络……把attention网络想象成字典,其中的key是一组标签,你可以用来决定网络对于特定的h有多相关。
  8. 请记住,你正在计算每个h的注意力。这意味着你有一个for循环用于[h~1~,…,h~n~]。
  9. 注意网络嵌入的亮度可以任意调高。这会炸掉你的内存。确保把它放在一个单独的GPU或保持昏暗小。
  10. 让大型模型运行的一个技巧是将编码器放在一个gpu上,解码器放在第二个gpu上,注意力网络放在第三个gpu上。这样就可以保持较低的内存占用。
  11. 如果要实际部署此模型,则需要批量实现它。我在这里解释的都是批量=1时的情况,但是你可以通过变换成张量积来扩展到更大的批量,并且要对线性代数很在行。

同样,在大多数情况下,你应该只使用开放源码实现,但这是一个很好的学习经验,可以自己试试!

Attention之后的日子Life After Attention

事实证明,注意力网络本身是非常强大的。

cnn attention lstm_神经网络_14


样一来,研究人员决定摆脱RNNs和序列对序列的方法。相反,他们创造了一种叫做Transformer模型的东西。

在高层次上,transformer仍然有一个编码器和解码器,只是各个层是全连接的,并同时查看完整的输入。然后,当输入在网络中移动时,注意力就会集中在重要的事情上。

cnn attention lstm_神经网络_15


Transformer的图

这个模型已经在翻译中很大程度上取代了seq2seq模型,并且是当前最强大的模型BERT和OpenAI的GPT的背后的模型。



END—