十、注意力机制
10.1. 注意力机制
卷积、全连接、池化层都只考虑非自主性提示
注意力机制则显示的考虑随意线索
自主性提示被称为查询(query)
给定任何查询,注意力机制通过注意力汇聚(attention pooling) 将选择引导至感官输入(sensory inputs,例如中间特征表示)
在注意力机制中,这些感官输入被称为值(value)
每个值都与一个键(key)配对, 这可以想象为感官输入的非自主提示
通过注意力池化层(attention pooling)来有偏向性的选择选择某些输入
非参注意力池化层
Nadaraya-Watson核回归
根据输入的位置对输出进行加权
其中是核(kernel)
高斯核(Gaussian kernel)
代入得
带参数注意力汇聚
非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点: 如果有足够的数据,此模型会收敛到最优结果。 尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。
是可学习参数,是query,是key
10.3. 注意力评分函数
α(x,xi):注意力权重(权重一般是一组大于等于零,相加和为 1 的数)
注意力分数:高斯核的指数部分(相当于是注意力权重归一化之前的版本)
上图所展示的是:假设已知一些 key-value 对和一个 query,首先将 query 和每一个 key 通过注意力分数函数 a 和 softmax 运算得到注意力权重(与 key 对应的值的概率分布),将这些注意力权重再与已知的 value 进行加权求和,最终就得到了输出
拓展到更高的维度
假设 query 是一个长为 q 的向量,ki 是长为 k 的向量,vi 是长为 v 的向量(这里 key 和 value 的长度可以不同)
其中 query 和 ki 的注意力权重(标量)是通过注意力评分函数 a 将两个向量映射成标量,再经过 softmax 运算得到的
掩蔽 softmax 操作(masked softmax operation)
softmax 操作用于输出一个概率分布作为注意力权重,但是在某些情况下,并非所有的值都应该被纳入到注意力汇聚中
在处理文本数据集的时候,为了提高计算效率,可能会采用填充的方式使每个文本序列具有相同的长度,便于以相同形状的小批量进行加载,因此可能会存在一些文本序列被填充了没有意义的特殊词源(比如“<pad>”词元)
因此,为了仅仅将有意义的词元作为值来获取注意力汇聚,可以指定一个有效序列长度(即词元的个数),任何超出有效长度的位置都被掩蔽并置于 0,便于在计算 softmax 的时候过滤掉超出指定范围的位置,这也就是掩蔽 softmax 操作
注意力分数函数 α 的设计
加性注意力(Additive attention)
等价于将 query 和 key 合并起来变成一个长度为 k+q 的向量,然后将其输入到一个隐藏层大小为 h ( h 是一个超参数),输出大小为 1 的但隐藏层的 MLP(没有偏置项),最终得到输出
优点是 key、value 向量可以是任意的长度,可以不同
缩放点积注意力(Scaled Dot-Product Attention)
假设 query 和 key 的所有元素都是独立的随机变量,并且都满足零均值和单位方差,那么两个向量的点积的均值为 0 ,方差为 d
这里不需要学习任何东西,直接利用 <q,ki> 将 q 和 ki 做内积然后除以根号 d (除以根号 d 的目的是为了降低对 ki 的长度的敏感度,使得无论向量的长度如何,点积的方差在不考虑向量长度的情况下仍然是 1 )
10.4. Bahdanau 注意力
动机
在机器翻译的时候,每个生成的词可能相关于源句子中不同的词
在语言翻译的时候,中文和英文之间的翻译可能会存在倒装,但是可能在西方语言之间,相同意思的句子中的词的位置可能近似地是对应的,所以在翻译句子的某个部位的时候,只需要去看源句子中对应的位置就可以了
然而,Seq2Seq 模型中不能对此直接建模。Seq2Seq 模型中编码器向解码器中传递的信息是编码器最后时刻的隐藏状态(例如图片中的.,但实际bonjour应该根据hello),解码器只用到了编码器最后时刻的隐藏状态作为初始化,从而进行预测,所以解码器看不到编码器最后时刻的隐藏状态之前的其他隐藏状态
源句子中的所有信息虽然都包含在这个隐藏状态中,但是要想在翻译某个词的时候,每个解码步骤使用编码相同的上下文变量,但是并非所有输入(源)词元都对解码某个词元有用。将注意力关注在源句子中的对应位置,这也是将注意力机制应用在Seq2Seq 模型中的动机
加入注意力
编码器对每次词的输出(隐藏状态)作为 key 和 value,序列中有多少个词元,就有多少个 key-value 对,它们是等价的,都是第 i 个词元的 RNN 的输出
解码器 RNN 对上一个词的预测输出(隐藏状态)是 query(假设 RNN 的输出都是在同一个语义空间中,所以在解码器中对某个词元进行解码的时候,需要用到的是 RNN 的输出,而不能使用词嵌入之后的输入,因为 key 和 value 也是 RNN 的输出,所以 key 和 query 做匹配的时候,最好都使用 RNN 的输出,这样能够保证它们差不多在同一个语义空间)
注意力的输出和下一个词的词嵌入合并进入 RNN 解码器
对 Seq2Seq 的改进之处在于:之前 Seq2Seq 的 RNN 解码器的输入是 RNN 编码器最后时刻的隐藏状态,加入注意力机制之后的模型相当于是对所有的词进行了加权平均,根据翻译的词的不同使用不同时刻的 RNN 编码器输出的隐藏状态
总结
Seq2Seq 中通过编码器最后时刻的隐藏状态在编码器和解码器中传递信息
注意力机制可以根据解码器 RNN 的输出来匹配到合适的编码器 RNN 的输出来更有效地传递信息
在预测词元时,如果不是所有输入词元都是相关的,加入注意力机制能够使 RNN 编码器-解码器有选择地统计输入序列的不同部分(通过将上下文变量视为加性注意力池化的输出来实现)
10.6. 自注意力和位置编码
自注意力
- 在深度学习中,经常使用卷积神经网络或者循环神经网络对序列进行编码
- 对于 key 、value 和 query ,自注意力有自己的一套选法,因为 key 、value 和 query 的值来自同一组输入,因此被称为自注意力(self-attention)或者内部注意力(intra-attention)
给定序列是一个长为 n 的序列,每个 xi 是一个长为 d 的向量
自注意力将 xi 同时作为 key 、value 和 query ,以此来对序列抽取特征
基本上可以认为给定一个序列,会对序列中的每一个元素进行输出,也就是说,每个查询都会关注所有的键-值对并生成一个注意力输出
自注意力之所以叫做自注意力,是因为 key,value,query 都是来自于自身,xi 既作为 key ,又作为 value ,同时还作为 query (self-attention 中的 self 所强调的是 key,value,query 的取法)
和 CNN,RNN 对比
CNN、RNN、自注意力都可以用来处理序列
CNN 如何处理序列:给定一个序列,将其看作是一个一维的输入(之前在处理图片时,图片具有高和宽,而且每个像素都具有 chanel 数,也就是特征数),如果用 CNN 做序列的话,经过一个 1d 的卷积(只有宽没有高)之后,将每个元素的特征看作是 channel 数,这样就可以用来处理文本序列了
k:窗口大小,每次看到的长度为 k n:长度 d:dimension,每个 x 的维度(长度)
并行度:每个输出( yi )可以自己并行做运算,因为 GPU 有大量的并行单元,所以并行度越高,计算的速度就越快
最长路径:对于最长的那个序列,前面时刻的信息通过神经元传递到后面时刻,对应于计算机视觉中的感受野的概念(每一个神经元的输出对应的图片中的视野)
自注意力机制比较适合处理长文本,但与之对应的计算量也非常的大
位置编码(position encoding)
和 CNN / RNN 不同,自注意力并没有记录位置信息:
CNN 中其实是有记录位置信息的,从输出可以反推出输入所在的窗口的位置,窗口大小可以看成是位置信息
RNN 本身就是序列相关的,它是通过逐个的重复地处理词元
对于自注意力来说,如果将输入进行随机打乱,对应输出的位置可能会发生变化,但是每个输出的内容不会发生变化
所以如果是想纯用自注意力机制来做序列模型的话,没有位置信息的话可能会出现问题,所以可以通过加入位置编码来加入位置信息(不是加到模型里,
位置编码不是将位置信息加入到模型中,一旦位置信息加入到模型中,会出现各种问题(比如在 CNN 中就需要看一个比较长的序列,RNN 中会降低模型的并行度) 位置编码将位置信息注入到输入里
P 中的每个元素根据对应的 X 中元素位置的不同而不同
P 的元素具体计算如下:
对于 P 中的每一列,奇数列是一个 cos 函数,偶数列是一个 sin 函数,不同的列之间的周期是不一样的
位置编码矩阵
X 轴横坐标表示 P 矩阵中的行数
图中不同颜色的曲线表示 P 矩阵中不同的列
这里可以理解为 X 轴上任意一点对应的 j 列的曲线上在 Y 轴的值,就表示 P 矩阵第 X 行第 j 列的元素的值
图中的四条曲线分别代表了第 6 、7 、8 、9 列,从图中可以看出,第 6 列是一个 sin 函数,第 7 列在第 6 列的基础上发生了位移,变成了 cos 函数,第 8 列在第 6 列的基础上周期变长了一倍,仍然是 sin 函数,第 9 列在第 8 列的基础上发生了唯一,变成了 cos 函数
从图中可以看出,对于 P 矩阵中同一行,不同的列的元素的数值是不同的,也就是说,对于输入序列(X + P 作为自编码输入)来讲,每个 dimension 所加的值是不同的;同样的,对于同一个输入序列,不同的样本所加的值也是不同的(对于同一条曲线,X 不同的情况下,即不同的行,元素的值也是不同的,这里 sin 函数和 cos 函数都是周期函数,应该讲的是在同一个周期内的样本)
P 实际上是对每一个样本(row)、每一个维度(dimension)添加一点不一样的值,使得模型能够分辨这种细微的差别,作为位置信息
这种方式跟之前的方式的不同之处在于,之前是将位置信息放进模型中或者将位置信息与数据分开然后进行拼接(concat),位置编码是直接将位置信息加入到了数据中,这样做的好处是不改变模型和数据的大小,缺点是需要模型对于 P 中元素的细微信息进行辨认,取决于模型是否能够有效地使用 P 中的位置信息
绝对位置信息
计算机使用的是二进制编码
可以认为,假设计算机要表示八个数字的话,可以用一个长为 3 的特征来表示,可以认为是一个三维的特征,每一个维度都在 0 和 1 之间进行变化,而且变化的频率不同,最后一维变化的频率最快,最前面一维变化的频率最慢
位置矩阵编码可以认为和计算机的二进制编码类似
首先,位置编码是实数(因为对应的输入也是实数),是在 1 和 -1 之间进行实数的变化,所以能编码的范围更广,可以在任意多的维度上进行编码
其次,因为位置编码中所使用的 sin 函数和 cos 函数都是周期函数,所以位置编码也是存在周期性的
上图是一个热度图,和上一个图是一样的,只不过将 X 轴和 Y 轴进行了翻转(X 轴表示特征,Y 轴表示样本)
可以认为是对每一行的位置信息进行了编码,将第 i 个样本用一个长为 d 的向量进行编码
这里和计算机的二进制编码有一点不同,最前面的维度变化频率比较高,越到后面变化频率越来越慢
核心思想是对序列中的第 i 个样本,给定长为 d 的独一无二的位置信息,然后加入到数据中作为自编码输入,使得模型能够看到数据的位置信息
相对位置信息
为什么要使用 sin 函数和 cos 函数?
编码的是一个相对位置信息,位置位于 i + σ 处的位置编码可以线性投影位置 i 处的位置编码来表示,也就是说位置信息和绝对位置 i 无关,只是和相对位置 σ 有关
投影矩阵和序列中的位置 i 是无关的,但是和 j 是相关的(和 dimension 的信息是相关的),意味着在一个序列中,假设一个词出现在另外一个词两个或者三个位置的时候,不管这对词出现在序列中的什么位置,对于位置信息来讲,都是可以通过一个同样的线性变换查找出来的
相对来讲,这样编码的好处在于模型能够更加关注相对的位置信息,而不是关注一个词出现在一个句子中的绝对位置
总结
自注意力池化层将 xi 当作 key ,value query 来对序列抽取特征
完全并行、最长序列为 1 、但对长序列计算复杂度高
- 可以完全并行,和 CNN 是一样的,所以计算效率比较高
- 最长序列为 1 ,对于任何一个输出都能够看到整个序列信息,所以这也是为什么当处理的文本比较大、序列比较长的时候,通常会用注意力和自注意力
- 但是问题是对长序列的计算复杂度比较高,这也是一大痛点
位置编码在输入中加入位置信息,使得自注意力能够记忆位置信息
- 类似于计算机的数字编码,对每个样本,给定一个长为 d 的编码
- 编码使用的是 sin 函数或者是 cos 函数,使得它对于序列中两个固定距离的位置编码,不管它们处于序列中的哪个位置,他们的编码信息都能够通过一个线性变换进行转换
10.7. Transformer
Transformer 模型是完全基于注意力机制,没有任何卷积层或循环神经网络
Transformer 最初应用在文本数据上的序列到序列学习,现在已经推广到各种现代的深度学习中,如语言、视觉、语音和强化学习领域
Transformer 架构
基于编码器-解码器的架构来处理序列对,Transformer 的编码器和解码器是基于自注意力的模块叠加而成的
源(source,输入)序列和目标(target,输出)序列的嵌入(embedding)表示通过加上位置编码(positional encoding)加入位置信息,再分别输入到编码器和解码器中
Transformer 的编码器是由多个相同的层叠加而成的,每个层都有两个子层(每个子层都采用了残差连接,并且在残差连接的加法计算之后,都使用了层归一化,因此 Transformer 编码器都将输出一个 d 维表示向量)
第一个子层是多头自注意力汇聚
- Transformer 块中的多头注意力实际上就是自注意力(自注意力同时具有并行计算和最短的最大路径长度这两个优势)
- 在计算编码器的自注意力时,key 、value 和 query 的值都来自前一个编码器层的输出
第二个子层是基于位置的前馈网络
- Positionwise FFN 实际上是全连接
- 本质上和编码器-解码器的架构没有本质上的区别,将 Transformer 编码器最后一层的输出作为解码器的输入来完成信息的传递
Transformer 解码器也是由多个相同的层叠加而成的,每层都有三个子层,并且在每个子层中也使用了残差连接和层归一化
第一个子层是解码器自注意力(带掩码的多头自注意力)
- 在解码器自注意力中,key 、value 和 query 都来自上一个解码器层的输出
- 解码器中的每个位置只能考虑该位置之前的所有位置
- 带掩码的自注意力保留了自回归的属性,确保预测仅仅依赖于已生成的输出词元(为了在解码器中保留自回归的属性,带掩码的自注意力设定了有效长度(dec_valid_lens)作为参数,以便任何查询都只会与解码器中所有已经生成的词元的位置(即直到该查询为止)进行注意力计算,而不会对当前位置之后的 key-value 对进行注意力计算)
第二个子层是编码器-解码器注意力
- 除了编码器中所描述的两个子层之外,解码器还在这两个子层之间插入了编码器-解码器注意力层,作为第三个子层,它的 query 来自上一个解码器层的输出,key 和 value 来自整个编码器的输出
第三个子层是基于位置的前馈网络
对比 seq2seq
和使用注意力的 seq2seq 的不同之处在于:Transformer 是纯基于注意力(具体来讲,它是一个纯基于自注意力的架构,里面没有 RNN)
Transformer 将使用注意力的 seq2seq 中的 RNN 全部换成了 Transformer 块
多头注意力(Multi-head attention)
对同一个 key 、value 、query 抽取不同的信息(例如短距离关系和长距离关系)
多头注意力使用 h 个独立的注意力池化,合并各个头(head)输出得到最终输出
key 、value 、query 都是长为 1 的向量,通过全连接层映射到一个低一点的维度,然后进入到注意力模块中
f可以是加性注意力和缩放点积注意力
带掩码的多头注意力(Masked Multi-head attention)
解码器对序列中一个元素输出时,不应该考虑该元素之后的元素
注意力中是没有时间信息的,在输出中间第 i 个信息的时候,也能够看到后面的所有信息,这在编码的时候是可以的,但是在解码的时候是不行的,在解码的时候不应该考虑该元素本身或者该元素之后的元素
可以通过掩码来实现,也就是计算 xi 输出时,假装当前序列长度为 i
基于位置的前馈网络(Positionwise FFN)
- 基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是基于位置的原因
其实就是全连接层,将输入形状由(b,n,d)变成(bn,d),然后作用两个全连接层,最后输出形状由(bn,d)变回(b,n,d),等价于两层核窗口为 1 的一维卷积层
- b:batchsize
- n:序列长度
- d:dimension
- 在做卷积的时候是将 n 和 d 合成一维,变成 nd ;但是现在 n 是序列的长度,会变化,要使模型能够处理任意的特征,所以不能将 n 作为一个特征,因此对每个序列中的每个元素作用一个全连接(将每个序列中的 xi 当作是一个样本)
残差连接和层归一化(Add & norm)
Add 就是一个 Residual_block(上图左一)
加入归一化能够更好地训练比较深的网络,但是这里不能使用批量归一化,批量归一化对每个特征/通道里元素进行归一化
- 这里的特征指的是每个序列中 D 中的一维,所以在做归一化的时候就是将其方差变 1 ,均值变 0
- 在做 NLP 的时候,如果选择将 d 作为特征的话,那么批量归一化的输入是 n*b ,b 是批量大小,n 是序列长度,序列的长度是会变的,所以每次做批量归一化的输入大小都不同,所以会导致不稳定,训练和预测的长度本来就不一样,预测的长度会慢慢变长,所以批量归一化不适合长度会变的 NLP 应用
层归一化对每个样本里的元素进行归一化
- b 代表 batchsize
- d 代表特征维度
- len 表示序列长度
- 层归一化和批量归一化的目标相同,但是层归一化是基于特征维度进行归一化的
- 层归一化和批量归一化的区别在于:批量归一化在 d 的维度上找出一个矩阵,将其均值变成 0 ,方差变成 1,层归一化每次选的是一个元素,也就是每个 batch 里面的一个样本进行归一化
- 尽管批量归一化在计算机视觉中被广泛应用,但是在自然语言处理任务中,批量归一化通常不如层归一化的效果好,因为在自然语言处理任务中,输入序列的长度通常是变化的
- 虽然在做层归一化的时候,长度也是变化的,但是至少来说还是在一个单样本中,不管批量多少,都给定一个特征,这样对于变化的长度来讲,稍微稳定一点,不会因为长度变化,导致稳定性发生很大的变化
信息传递
假设编码器中的输出是 y1,... ,yn ,将其作为解码中第 i 个 Transformer 块中多头注意力的 key 和 value
- 一共有三个多头注意力(包括一个带掩码的多头注意力),位于带掩码的多头注意力与其它两个不同,其他两个都是自注意力(key 、value 和 query 都相同),而它是普通的注意力(它的 key 和 value 来自编码器的输出, query 来自目标序列)
这就意味着编码器和解码器中块的个数和输出维度都是一样的
位于解码器中间的multi-head attention的key和value来自编码器的输出(即其他两个都是自注意力,而它是普通的注意力)
预测
预测第 t+1 个输出时,解码器中输入前 t 个预测值
- 在自注意力中,前 t 个预测值作为 key 和 value ,第 t 个预测值还作为 query
关于序列到序列模型,在训练阶段,输出序列的所有位置(时间步)的词元都是已知的;但是在预测阶段,输出序列的次元是逐个生成的
- 在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中
总结
和 seq2seq 有点类似,不同之处在于 Transformer 是一个纯使用注意力的编码-解码器
编码器和解码器都有 n 个 Transformer 块
每个块里使用多头(自)注意力(multi-head attention),基于位置的前馈网络(Positionwise FFN),残差连接和层归一化
- 编码器和解码器中各有一个自注意力,但是在编码器和解码器中传递信息的是一个正常的注意力
- 基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换,实际上就是一个全连接,等价于 1*1 的卷积
- Add & norm:Add 实际上就是 Residual block 可以帮助将网络做的更深,norm 使用的是 Layer Norm 使得训练起来更加容易;Transformer 中的残差连接和层规范化是训练非常深度模型的重要工具
在 Transformer 中,多头注意力用于表示输入序列和输出序列,但是解码器必须通过掩码机制来保留自回归属性