注意力机制


注意力机制

这是Dzmitry Bahdanai等人在2014年的突破性论文中的核心思想。他们介绍了一种技术,该技术允许编码器在每个时间步长中专注于适当的单词(由编码器编码)。例如,在编码器需要输出单词'lait'的时间步长上,它会把注意力集中在单词'milk'上。这意味着从输入单词到其翻译的路径变短了,因此RNN的短期记忆限制的影响变小了。注意力机制彻底改变了神经机器翻译(一般来说是NLP),极大地改善了现有技术,特别是对于长句子。

现在,不是把编码器的最终隐藏状态发送给解码器,而是把其所有输出发送给解码器。在每个时间步长,解码器的记忆单元会计算这些编码器输出的加权总和:这确定了该步长会把重点关注在哪个单词。权重\(\alpha_{(t,i)}\)是在第t个解码器时间步长处的第\(i\)个编码器输出的权重。例如,如果权重\(\alpha_{(3,2)}\)远大于权重\(\alpha_{(3,0)}\)和权重\(\alpha_{(3,1)}\),则解码器将更注意第二个单词,而不是其他两个单词,至少在这个时间步长。解码器的其余部分的工作方式与之前类似:在每个时间步长,记忆单元都会接收输入,再加上前一个时间步长的隐藏状态,最后,它接收前一个时间步长中的目标单词。

这些\(\alpha_{(t,i)}\)权重从何而来:它们是由一种对齐模型(或注意力层)的小型神经网络生成,该网络与其余的编码器-解码器一起进行训练。它始于有单个神经元的时间分布Dense层,该层接受所有编码器输出作为输入,与解码器先前的隐藏状态合并。该层为每个编码器的输出而输出一个分数:该分数用于衡量每个输出与解码器先前的隐藏状态对齐程度。最后,所有分数都通过softmax层,以获取每个编码器输出的最终权重。给定编码器时间步长的所有权重加起来为1(因为softmax层未按时间分布)。这种特殊的注意力机制称为Bahdanau注意力。由于它将编码器的输出和解码器的先前隐藏状态合并在一起,因此有时称为合并注意力(或加法注意力)

如果输入句子的长度为n个单词,并且假设输出句子的长度大致相同,则此模型需要计算大约\(n^2\)个权重。幸运的是,这种二次计算复杂度仍然是很容易处理的,因为即使长句子也没有成千上万个单词。

之后不久,Minh-Thang Luong等人在2015年发表的论文中提出了另一种常见的注意力机制。由于注意力机制的目的是测量编码器的输出之一与解码器的先前隐藏状态之间的相似性,因此作者提出了简单计算这两个向量的点积。因为这通常是一个相当好的相似度度量,当前的硬件可以更快地进行计算。为此,两个向量必须具有相同的维度。这被称为Luong注意力,有时也称为乘法注意力。点积给出一个分数,所有的分数都经过softmax层以给出最终的权重,就像在Bahdanau注意力中一样。他们提出的另一种简化方法是在每个时间步长而不是在前一个时间步长使用解码器的隐藏状态,然后使用注意力机制的输出直接计算解码器的预测(而不是使用它计算解码器的当前隐藏状态)。他们还提出了一种点积机制的变体,其中编码器的输出在计算点积之前先经过线性变换(即没有偏置项地时间分布Dense层)。这成为“通用”点积方法。他们将两种点积方法与合并注意力机制进行了对比(添加了一个重新缩放参数向量\(v\)),他们观察到,点积变体的效果要好于合并注意力。因此,现在很少使用合并注意力

注意力机制:

\[\tilde{h}_{(t)}=\sum_i\alpha_{(t,i)}y_{(i)}\\with\,\alpha_{(t,i)}=\frac{\exp(e_{(t,i)})}{\sum_{i'}\exp(e_{(t,i')})}and\\\, e_{(t,i)}=\left\{\begin{align*} &h_{(t)}^T\quad\mbox{点}\\ &h_{(t)}^TWy_{(i)}\quad\mbox{通用}\\ &v^Ttanh(W[h_{(t)};y_{(i)]})\quad\mbox{合并}\end{align*}\right.\]

# 使用TensorFlow Addons将Lulong注意力添加到编码器-解码器模型的方法
import tensorflow_addons as tfa
from tensorflow import keras
import tensorflow as tf

attention_mechanism = tfa.seq2seq.attention_wrapper.LuongAttention(
    units, encoder_state, memory_sequence_length=encoder_sequence_length
)
attention_decoder_cell = tfa.seq2seq.attention_wrapper.AttentionWrapper(
    decoder_cell, attention_mechanism, attention_layer_size=n_units
)
# 简单地将解码器单元包在AttentionWrapper中,提供了所需的注意力机制

视觉注意力

注意力机制可以用于多种目的。它们在NMT(神经网络机器翻译)之外的第一个应用之一是使用视觉注意力生成图像标题:卷积神经网络首先处理图像并输出一些特征图,然后具有注意力机制的解码器RNN生成标题,一次生成一个单词。在每个解码器时间步长(每个单词),解码器使用注意力模型将注意力集中在图像的正确部分

可解释性

注意力机制的另一个好处是,它们使人们更容易理解导致模型产生其输出的原因。这成为可解释性。当模型出错时,它特别有用:例如,如果在雪地里行走的狗的图片被标记为“在雪地里行走的狼”,那么可以返回并检查模型输出单词:狼“时所关注的是什么。可能会发现它不仅关注狗,还关注大雪,还暗示了可能的解释:也许许多模型学会区分狼和狗的方式是通过检查周围是否有大雪。然后,可以通过使用更多没有雪的狼和有雪的狗的图像训练模型来解决此问题。它对可解释性采用了不同的方法:在分类器的预测局部周围学习一个可解释的模型

在某些应用程序中,可解释性不仅仅是调试模型的工具,还可能是一项法律要求

Transformer架构

在2017年的一篇开创性论文中,一个Google研究团队提出了”注意力就是你所需要的一切。他们设法创建了一个名为Transformer的架构,该架构显著改善了NMT的现有水平,没有使用任何循环层或卷积层,只有注意力机制(加上嵌入层、密集层、归一化层以及其它一些小东西)。额外的好处是该架构的训练速度更快且更易于并行化,因此他们使用了比以前最先进的模型更少的时间和成本来训练。以下是Transformer架构图:

  • 左边是编码器。就像前面一样,它以一批表示为单词ID序列的句子作为输入(输如形状为[批处理大小,最大输出句子长度]),并将每个单词编码为512维的表征(因此编码器的输出形状为[批处理大小,最大输出句子长度,512])。编码器的顶部堆叠了N次
  • 右侧是解码器。在训练期间,它以目标句子作为输入(也表示为单词ID的序列),向右移动一个时间步长(即在序列的开头插入一个序列开始令牌)。它也接收编码器的输出(即来自左侧的箭头)。解码器顶部也堆叠了N次,编码器堆的最终输出在N层的每层馈送到解码器。解码器在每个时间步长输出每个可能的下一个单词的概率(其输出形状为[批处理大小,最大输出句子长度,词汇表长度])
  • 在训练后,无法向编码器提供目标值,因此需要向其提供先前输出的单词(从序列开始令牌开始),因此,需要反复调用该模型,并在每个回合中预测下一个字
  • 有两个嵌入层;5XN个跳过连接,每个后面都有一个归一化层;2XN个前馈模块,每个模块由两个密集层组成(第一个使用ReLU激活函数,第二个没有激活函数),最后输出层是使用softmax激活函数的密集层,如何一次只看一个单词就能翻译一个句子,这就需要新组件出现:
    • 编码器的多头注意力(Multi-Head Attention)层对同一句子中每个单词与其他单词之间的关系进行编码,更关注最相关的单词。例如,句子“They welcomed the Queen of the United Kingdom”中的“Queen”单词的这一层的输出取决于句子中的所有单词,它可能会更关注“United”和“Kingdom”,而不是“They”或“Welcomed”。这个注意力机制被称为“自我注意力”(句子关注自身)。解码器的掩码多头注意力(Masked Multi-Head Attention)层执行相同的操作,但是每个单词只能被允许关注位于其前面的单词。最后,解码器的多头注意力层上部是解码器关注输入句子中单词的地方。例如,当解码器要输出这个单词的翻译时,解码器可能会密切注意输入句子中的单词“Queen”
    • 位置嵌入只是表示单词在句子中的位置的密集向量(就像词嵌入)。第n个位置嵌入被添加到每个句子中的第n个单词的词嵌入中。这是的模型可以访问每个单词的位置,这是必须的,因为“多头注意力”层不考虑单词的顺序或位置,只看它们的关系。由于所有其他层都是由于时间分布的,它们无法直到每个单词的位置(无论是相对还是绝对)。显然,相对和绝对的词位置很重要,所以需要将此信息提供给Transformer,而位置嵌入是实现此目的的好方法

位置嵌入

位置嵌入是对单词在句子中的位置进行编码的密集向量:第i个位置嵌入被添加到句子中第i个单词的词嵌入中。这些位置嵌入可以通过模型学习,但是在本论文中,作者更喜欢使用固定的位置嵌入,该固定位置嵌入是使用不同频率的正弦和余弦函数定义的。

正弦/余弦位置嵌入:

\[P_{p,2i}=\sin(p/10000^{2i/d})\\ P_{p,2i+1}=\cos(p/10000^{2i/d}) \]

其中,\(P_{p,i}\)是位于句子中第\(p\)个位置的单词嵌入的第\(i\)个分量

该方法具有与学习的位置相同的性能,但可以拓展到任意长的句子,因此受到青睐。在将位置嵌入添加到单词嵌入之后,模型的其余部分可以访问句子中每个单词的绝对位置,因为每个位置都有唯一的位置嵌入。此外,震荡函数(正弦和余弦)的选择使模型也可以学习相对位置。例如,相隔38个单词的单词在嵌入维度i=100和i=101中始终具有相同的位置嵌入值

TensorFlow中没有PositionalEmbedding层,但是创建起来很容易。处于效率原因,预先计算了构造函数中的位置嵌入矩阵(因此需要知道最大句子长度max_steps和每个单词表示的维度max_dims)。然后call()方法将该嵌入矩阵裁剪为输入的大小,将其添加到输入中。由于在创建位置嵌入矩阵时增加了一个额外的大小为1的维度,因此广播法则确保把矩阵添加到输入的每个句子中:

import tensorflow as tf
from tensorflow import keras
import numpy as np


class PositionalEncoding(keras.layers.Layer):
    def __init__(self, max_steps, max_dims, dtype=tf.float32, **kwargs):
        super().__init__(dtype=dtype, **kwargs)
        if max_dims % 2 == 1: max_dims += 1
        p, i = np.meshgrid(np.arange(max_steps), np.arange(max_dims // 2))
        pos_emb = np.empty((1, max_steps, max_dims))
        pos_emb[0, :, ::2] = np.sin(p / 10000 ** (2 * i / max_dims)).T
        pos_emb[0, :, 1::2] = np.cos(p / 10000 ** (2 * i / max_dims)).T
        self.positional_embedding = tf.constant(pos_emb.astype(self.dtype))

    def call(self, inputs):
        shape = tf.shape(inputs)
        return inputs + self.positional_embedding[:, :shape[-2], :shape[-1]]


# 然后可以创建Transformer的第一层:
embed_size = 512
max_steps = 500
vocab_size = 10000
encoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
decoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
embeddings = keras.layers.Embedding(vocab_size, embed_size)
encoder_embeddings = embeddings(encoder_inputs)
decoder_embeddings = embeddings(decoder_inputs)
positional_encoding = PositionalEncoding(max_steps, max_dims=embed_size)
encoder_in = positional_encoding(encoder_embeddings)
decoder_in = positional_encoding(decoder_embeddings)

多头注意力

要理解“多头注意力”层如何工作,必须理解“缩放点积注意力”层。假设编码器分析了输入句子‘The played chess’,并设法理解单词‘They’是主语,单词‘played’是动词,因此用这些词的表征来编码这些信息。现在假设解码器已经翻译了主语,认为接下来应该翻译动词。为此,它需要从输入句子中获取动词。这类似于字典查找:好像编码器创建了一个字典{‘subject’:‘They’,‘verb’:‘played’,···},解码器想要查找与键‘verb’相对应的值。但是,该模型没有具体的令牌来表示键;它具有这些概念的向量化表示(在训练期间学到的),因此用于查找的键不完全匹配字典中的任何键。解决方法是计算查询和字典中每个键的相似度,然后使用softmax函数将这些相似度分数转换为加起来为1的权重。如果表示动词的键到目前为止是最相似的查询,那么该键的权重将接近1。然后,该模型可以计算相应值的加权和,因此,如果‘verb’键的权重接近于1,则该加权和将非常接近单词‘played‘的表征。简而言之,可以将整个过程视为可区分的字典查找。像Luong注意力一样,Transformer使用的相似性度量只是点积。实际上,除了比例因子外,该公式与Luong注意力的相同

缩放点积注意力:$$Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_{keys}}})V$$
在此等式中:

  • \(Q\)是一个矩阵,每个查询包含一行。其形状为\([n_{queries},d_{keys}]\),其中\(n_{queries}\)是查询数,而\(d_{keys}\)是每个查询和键的维度
  • \(K\)是一个矩阵,每个键包含一行。其形状为\([n_{keys},d_{keys}]\),其中\(n_{keys}\)是键和值的数量
  • \(V\)是一个矩阵,每个值包含一行。其形状为\([n_{keys},d_{values}]\),其中\(d_{values}\)是每个值的数量
  • \(QK^T\)的形状是\([n_{queries},n_{keys}]\):每一对查询/键有一个相似性分数。\(softmax\)函数的输出具有相同的形状,但所有行的总和为1。最终输出的形状为\([n_{queries},d_{values}]\):每个查询有一行,其中每一行代表了查询结果(值的加权和)
  • 比例因子会缩小相似度分数,以避免softmax函数饱和,这会导致很小的梯度
  • 可以在计算softmax之前,通过一个非常大的负值加到相应的相似性分数中来屏蔽一些键/值对。这在”掩码多头注意力“层中很有用

在编码器中,这个等式应用于批处理中的每个输入句子,其中\(Q,K,V\)均等于输入句子中的单词列表(因此,这个句子里的每个单词都会和同一个句子里的每个单词进行比较,包括它自己)。类似地,在解码器的掩码注意力层中,该等式会应用于批处理中的每个目标句子,其中\(Q,K,V\)都等于目标句子中的单词列表,但这次使用掩码来防止任何单词将自己与其后面的单词进行比较(在推理时,解码器只能访问它已经输出的单词,而不能访问将来的单词,因此在训练期间,必须屏蔽掉以后输出的令牌)。在解码器的注意力层上部,键\(K\)和值\(V\)仅是编码器生成的单词编码的列表,而查询\(Q\)是解码器生成的单词编码的列表

keras.layers.Attention层实现了缩放点积注意力,将等式有效地应用于一个批量中的多个句子。它的输入就像\(Q,K,V\)一样,除了有额外的批处理维度

如果忽略跳过连接、归一化层、前馈块,那事实是这是缩放点积注意力而不是多头注意力,可以像下面这样实现Transformer模型的其余部分:

z = encoder_in
for N in range(6):
    Z = keras.layers.Attention(use_scale=True)([Z, Z])
encoder_outputs = Z
Z = decoder_in
for N in range(6):
    Z = keras.layers.Attention(use_scale=True, causal=True)([Z, Z])
    Z = keras.layers.Attention(use_scale=True, causal=True)([Z, encoder_outputs])
outputs = keras.layers.TimeDistributed(
    keras.layers.Dense(vocab_size, activation='softmax'))(Z)

use_scale=True参数创建了一个附加参数,该参数可以让层来学习如何适当降低相似性分数。这个Transformer模型有所不同,后者始终使用相同的因子\(\sqrt{d_{keys}}\)来降低相似度分数。创建第二个注意力层时,causal=True参数可确保每个输出令牌仅注意先前的输出令牌,而不是将来的

相关