0. 前言
灵长类动物的视觉系统接受了大量的感官输入,这些感官输入远远超过了大脑能够完全处理的程度。然而,并非所有刺激的影响都是相等的。意识的聚集和专注使灵长类动物能够在复杂的视觉环境中将注意力引向感兴趣的物体,例如猎物和天敌。只关注一小部分信息的能力对进化更加有意义,使人类得以生存和成功。
自19世纪以来,科学家们一直致力于研究认知神经科学领域的注意力。本章的很多章节将涉及到一些研究。
本章首先回顾一个经典注意力框架,解释如何在视觉场景中展开注意力。受此框架中的注意力提示(attention cues)的启发,我们将设计能够利用这些注意力提示的模型。1964年的Nadaraya-Waston核回归(kernel regression)正是具有注意力机制(attention mechanism)的机器学习的简单演示。
然后继续介绍注意力函数,它们在深度学习的注意力模型设计中被广泛使用。具体来说将展示如何使用这些函数来设计Bahdanau注意力。Bahdanau注意力是深度学习中的具有突破性价值的注意力模型,它双向对齐并且可以微分。
最后将描述仅仅基于注意力机制的Transformer架构,该架构中使用了多头注意力(multi-head attention)和自注意力(self-attention)。自2017年横空出世,Transformer一直都普遍存在于现代的深度学习应用中,例如语言、视觉、语音和强化学习领域。
这一章目前只做了解,关于NLP的内容没有实验,也没有详细调查。
0.1. 小结
- 人类的注意力是有限的、有价值和稀缺的资源。
- 受试者使用非自主性和自主性提示有选择性地引导注意力。前者基于突出性,后者则依赖于主体的意识。
- 注意力机制与全连接层或者池化层的区别源于增加的自主提示。
- 由于包含了自主性提示,注意力机制与全连接的层或池化层不同。
- 注意力机制通过注意力池化使选择偏向于值(感官输入),其中包含查询(自主性提示)和键(非自主性提示)。键和值是成对的。
- 我们可以可视化查询和键之间的注意力权重。
- Nadaraya-Watson核回归是具有注意力机制的机器学习范例。
- Nadaraya-Watson核回归的注意力池化是对训练数据中输出的加权平均。从注意力的角度来看,分配给每个值的注意力权重取决于将值所对应的键和查询作为输入的函数。
- 注意力池化可以分为非参数型和带参数型
- 注意力池化的输出可以计算为值的加权平均,选择不同的注意力评分函数会带来不同的注意力池化操作。
- 当查询和键是不同长度的矢量时,可以使用加性注意力评分函数。当它们的长度相同时,使用缩放的“点-积”注意力评分函数的计算效率更高。
- 在预测词元时,如果不是所有输入词元都是相关的,那么具有Bahdanau注意力的循环神经网络编码器-解码器会有选择地统计输入序列的不同部分。这是通过将上下文变量视为加性注意力池化的输出来实现的。
- 在循环神经网络编码器-解码器中,Bahdanau注意力将上一时间步的解码器隐状态视为查询,在所有时间步的编码器隐状态同时视为键和值。
- 多头注意力融合了来自于多个注意力池化的不同知识,这些知识的不同来源于相同的查询、键和值的不同的子空间表示。
- 基于适当的张量操作,可以实现多头注意力的并行计算。
- 在自注意力中,查询、键和值都来自同一组输入。
- 卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。
- 为了使用序列的顺序信息,我们可以通过在输入表示中添加位置编码,来注入绝对的或相对的位置信息。
- transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。
- 在transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。
- transformer中的残差连接和层规范化是训练非常深度模型的重要工具。
- transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。
1. 注意力提示
感谢读者对本书的关注,因为读者的注意力是一种稀缺的资源:此刻读者正在阅读本书(而忽略了其他的书),因此读者的注意力是用机会成本(与金钱类似)来支付的。为了确保读者现在投入的注意力是值得的,作者们尽全力(全部的注意力)创作一本好书。
自经济学研究稀缺资源分配以来,人们正处在“注意力经济”时代,即人类的注意力被视为可以交换的、有限的、有价值的且稀缺的商品。许多商业模式也被开发出来去利用这一点:在音乐或视频流媒体服务上,人们要么消耗注意力在广告上,要么付钱来隐藏广告;为了在网络游戏世界的成长,人们要么消耗注意力在游戏战斗中,从而帮助吸引新的玩家,要么付钱立即变得强大。总之,注意力不是免费的。
注意力是稀缺的,而环境中的干扰注意力的信息却并不少。比如人类的视觉神经系统大约每秒收到$10^8$位的信息,这远远超过了大脑能够完全处理的水平。幸运的是,人类的祖先已经从经验(也称为数据)中认识到“并非感官的所有输入都是一样的”。在整个人类历史中,这种只将注意力引向感兴趣的一小部分信息的能力,使人类的大脑能够更明智地分配资源来生存、成长和社交,例如发现天敌、找寻食物和伴侣。
1.1. 生物学中的注意力提示
注意力是如何应用于视觉世界中的呢?这要从当今十分普及的双组件(two-component)的框架开始讲起:这个框架的出现可以追溯到19世纪90年代的威廉·詹姆斯,他被认为是“美国心理学之父” [James.2007
]。在这个框架中,受试者基于非自主性提示和自主性提示有选择地引导注意力的焦点。
非自主性提示是基于环境中物体的突出性和易见性。想象一下,假如我们面前有五个物品:一份报纸、一篇研究论文、一杯咖啡、一本笔记本和一本书,就像下图。所有纸制品都是黑白印刷的,但咖啡杯是红色的。换句话说,这个咖啡杯在这种视觉环境中是突出和显眼的,不由自主地引起人们的注意。所以我们会把视力最敏锐的地方放到咖啡上,如下图所示。
喝咖啡后,我们会变得兴奋并想读书,所以转过头,重新聚焦眼睛,然后看看书,就像下图中描述那样。与上图中由于突出性导致的选择不同,此时选择书是受到了认知和意识的控制,因此注意力在基于自主性提示去辅助选择时将更为谨慎。受试者的主观意愿推动,选择的力量也就更强大。
1.2. 查询、键和值
自主性的与非自主性的注意力提示解释了人类注意力的方式,下面来看看如何通过这两种注意力提示,用神经网络来设计注意力机制的框架。
首先,对于只使用非自主性提示的情况。要想将选择偏向于感官输入,则可以简单地使用参数化的全连接层,甚至是非参数化的最大池化层或平均池化层。个人理解,这就是说,这种情况下不需要在以往的神经网络上做出修改,因为非自主性提示来自客体的差异。
因此,“是否包含自主性提示”将注意力机制与全连接层或池化层区别开来。在注意力机制的背景下,自主性提示被称为查询(query)。给定任何查询,注意力机制通过注意力池化(attention pooling)将选择引导至感官输入(sensory inputs,例如中间特征表示)。在注意力机制中,这些感官输入被称为值(value)。更通俗的解释是,每个值都与一个感官输入的非自主提示配对,这些对应的非自主性提示称为键(key)。如下图所示,可以通过设计注意力池化的方式,使给定的查询(自主性提示)与键(非自主性提示)进行匹配,这将引导得出最匹配的值(感官输入)。
鉴于上面所提框架在上图中的主导地位,因此这个框架下的模型将成为本章的中心。然而,注意力机制的设计有许多替代方案。例如可以设计一个不可微的注意力模型,该模型可以使用强化学习方法 [Mnih.Heess.Graves.ea.2014
]进行训练。
1.3. 注意力的可视化
平均池化层可以被视为输入的加权平均值,其中各输入的权重是一样的。实际上,注意力池化得到的是加权平均的总和值,其中权重是在给定的查询和不同的键之间计算得出的。
为了可视化注意力权重,需要定义一个show_heatmaps
函数。其输入matrices
的形状是(要显示的行数,要显示的列数,查询的数目,键的数目)。
1 | import torch |
下面使用一个简单的例子进行演示,本例中,仅当查询和键相同时(即客体特征符合主体意识时),注意力权重为1,否则为0。
1 | attention_weights = torch.eye(10).reshape((1, 1, 10, 10)) |
后面的章节将经常调用show_heatmaps
函数来显示注意力权重。
2. 注意力池化:Nadaraya-Watson 核回归
上节介绍了框架下的注意力机制的主要成分:查询(自主提示)和键(非自主提示)之间的交互形成了注意力池化;注意力池化有选择地聚合了值(感官输入)以生成最终的输出。本节将介绍注意力池化的更多细节,以便从宏观上了解注意力机制在实践中的运作方式。1964年提出的Nadaraya-Watson核回归模型是一个简单但完整的例子,可以用于演示具有注意力机制的机器学习。
2.1. 生成数据集
简单起见,考虑这个回归问题:给定的成对的“输入-输出”数据集${(x_1, y_1), \ldots, (x_n, y_n)}$,如何学习$f$来预测任意新输入$x$的输出$\hat{y} = f(x)$?
根据下面的非线性函数生成一个人工数据集,其中加入的噪声项为$\epsilon$:
其中$\epsilon$服从均值为$0$和标准差为$0.5$的正态分布。在这里生成了$50$个训练样本和$50$个测试样本。为了更好地可视化之后的注意力模式,需要将训练样本进行排序。
1 | import torch |
下面的函数将绘制所有的训练样本(样本由圆圈表示),不带噪声项的真实数据生成函数$f$(标记为“Truth”),以及学习得到的预测函数(标记为“Pred”)。
1 | def plot_kernel_reg(y_hat): |
2.2. 平均池化
先使用最简单的估计器来解决回归问题。基于平均池化来计算所有训练样本输出值的平均值:
如下图所示,这个估计器确实不够聪明。真实函数$f$(“Truth”)和预测函数(“Pred”)相差很大。
1 | # 这里先求了一下训练集标签的平均值,得到一个单值张量(无维度)。 |
2.3. 非参数注意力池化
显然,平均池化忽略了输入$x_i$。于是Nadaraya[Nadaraya.1964
]和Watson[Watson.1964
]提出了一个更好的想法,根据输入的位置对输出$y_i$进行加权:
其中$K$是核(kernel)。上式所描述的估计器被称为Nadaraya-Watson核回归(Nadaraya-Watson kernel regression)。这里不会深入讨论核函数的细节,但受此启发,我们可以从第一节图中的注意力机制框架的角度重写上式,成为一个更加通用的注意力池化(attention pooling)公式:
其中$x$是查询,$(x_i, y_i)$是键值对。比较两个公式,注意力池化是$y_i$的加权平均。将查询$x$和键$x_i$之间的关系建模为注意力权重(attention weight)$\alpha(x, x_i)$,如上式所示,这个权重将被分配给每一个对应值$y_i$。对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布:它们是非负的,并且总和为1。
为了更好地理解注意力池化,考虑一个高斯核(Gaussian kernel),其定义为:
将高斯核代入Nadaraya-Watson核回归公式 和 注意力池化公式 可以得到:
在上式中,如果一个键$x_i$越是接近给定的查询$x$,那么分配给这个键对应值$y_i$的注意力权重就会越大,也就“获得了更多的注意力”。
这里穿插解释一下参数模型和非参数模型。
参数模型
: 在统计学中,参数模型通常假设总体服从某个分布,这个分布可以由一些参数确定,如正态分布由均值和标准差确定,在此基础上构建的模型称为参数模型。
非参数模型
: 非参数模型对于总体的分布不做任何假设或者说是数据分布假设自由,只知道其分布是存在的,所以就无法得到其分布的相关参数,只能通过非参数统计的方法进行推断。
总之,参数模型和非参数模型中的“参数”并不是模型中的参数,而是数据分布的参数。
Nadaraya-Watson核回归是一个非参数模型。因此,上式是非参数的注意力池化(nonparametric attention pooling)模型。接下来将基于这个非参数的注意力池化模型来绘制预测结果。从绘制的结果会发现新的模型预测线是平滑的,并且比平均池化的预测更接近真实。
1 | # X_repeat的形状:(n_test,n_train), |
现在来观察注意力的权重。 这里测试数据的输入相当于查询,而训练数据的输入相当于键。 因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近,注意力池化的注意力权重就越高。
1 | d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), |
2.4. 带参数注意力池化
非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点:如果有足够的数据,此模型会收敛到最优结果。尽管如此,我们还是可以轻松地将可学习的参数集成到注意力池化中。
例如,与上一节略有不同,在下面的查询$x$和键$x_i$之间的距离乘以可学习参数$w$:
本节的余下部分将通过训练这个模型来学习注意力池化的参数。
2.4.1. 批量矩阵乘法
为了更有效地计算小批量数据的注意力,可以利用深度学习开发框架中提供的批量矩阵乘法。
假设第一个小批量数据包含$n$个矩阵$\mathbf{X}_1,\ldots, \mathbf{X}_n$,第二个小批量包含$n$个矩阵$\mathbf{Y}_1, \ldots, \mathbf{Y}_n$,形状为$a\times b$,形状为$b\times c$。它们的批量矩阵乘法得到$n$个矩阵$\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n$,形状为$a\times c$。因此,假定两个张量的形状分别是$(n,a,b)$和$(n,b,c)$,它们的批量矩阵乘法输出的形状为$(n,a,c)$。
1 | X = torch.ones((2, 1, 4)) |
在注意力机制的背景中,我们可以使用小批量矩阵乘法来计算小批量数据中的加权平均值。
1 | weights = torch.ones((2, 10)) * 0.1 |
2.4.2. 定义模型
基于上述的带参数的注意力池化,使用小批量矩阵乘法,定义Nadaraya-Watson核回归的带参数版本为:
1 | class NWKernelRegression(nn.Module): |
2.4.3. 训练
接下来,将训练数据集变换为键和值用于训练注意力模型。 在带参数的注意力池化模型中, 任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算, 从而得到其对应的预测输出。
1 | # X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入 |
训练带参数的注意力池化模型时,使用平方损失函数和随机梯度下降。
1 | net = NWKernelRegression() |
如下所示,训练完带参数的注意力池化模型后,我们发现: 在尝试拟合带噪声的训练数据时, 预测结果绘制的线不如之前非参数模型的平滑。
1 | # keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键) |
为什么新的模型更不平滑了呢? 来看一下输出结果的绘制图: 与非参数的注意力池化模型相比,带参数的模型加入可学习的参数后, 曲线在注意力权重较大的区域变得更不平滑。
1 | d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0), |
3. 注意力评分函数
上一节中,我们使用高斯核来对查询和键之间的关系建模。可以将其中的高斯核指数部分视为注意力评分函数(attention scoring function), 简称评分函数(scoring function),然后把这个函数的输出结果输入到softmax函数中进行运算。 通过上述步骤,我们将得到与键对应的值的概率分布(即注意力权重)。 最后,注意力池化的输出就是基于这些注意力权重的值的加权和。
从宏观来看,我们可以使用上述算法来实现1.2中的注意力机制框架。下图说明了如何将注意力池化的输出计算成为值的加权和,其中 $a$ 表示注意力评分函数。 由于注意力权重是概率分布, 因此加权和其本质上是加权平均值。
用数学语言描述,假设有一个查询 $\mathbf{q} \in \mathbb{R}^q$ 和 $m$ 个“键-值”对 $(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)$,其中$\mathbf{k}_i \in \mathbb{R}^k$,$\mathbf{v}_i \in \mathbb{R}^v$。注意力池化函数 $f$ 就被表示成值的加权和:
其中查询$\mathbf{q}$和键$\mathbf{k}_i$的注意力权重(标量)是通过注意力评分函数$a$将两个向量映射成标量,再经过softmax运算得到的:
正如上图所示,选择不同的注意力评分函数 $a$ 会导致不同的注意力池化操作。本节将介绍两个流行的评分函数,稍后将用他们来实现更复杂的注意力机制。
1 | import math |
3.1. 掩蔽softmax操作
正如上面提到的,softmax操作用于输出一个概率分布作为注意力权重。在某些情况下,并非所有的值都应该被纳入到注意力池化中。例如,为了在“机器翻译与数据集”一节中高效处理小批量数据集,某些文本序列被填充了没有意义的特殊词元。为了仅将有意义的词元作为值来获取注意力池化,可以指定一个有效序列长度(即词元的个数),以便在计算softmax时过滤掉超出指定范围的位置。下面的masked_softmax
函数实现了这样的掩蔽softmax操作(masked softmax operation),其中任何超出有效长度的位置都被掩蔽并置为0。
1 | def masked_softmax(X, valid_lens): |
为了演示此函数是如何工作的,考虑由两个$2 \times 4$矩阵表示的样本,这两个样本的有效长度分别为$2$和$3$。经过掩蔽softmax操作,超出有效长度的值都被掩蔽为0。
1 | masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3])) |
也可以使用二维张量,为矩阵样本中的每一行指定有效长度。
1 | masked_softmax(torch.rand(2, 2, 4), torch.tensor([[1, 3], [2, 4]])) |
3.2. 加性注意力
一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数。给定查询$\mathbf{q} \in \mathbb{R}^q$和键$\mathbf{k} \in \mathbb{R}^k$,加性注意力(additive attention)的评分函数为
其中可学习的参数是$\mathbf W_q\in\mathbb R^{h\times q}$、$\mathbf W_k\in\mathbb R^{h\times k}$和$\mathbf w_v\in\mathbb R^{h}$。如上式所示,将查询和键连结起来后输入到一个多层感知机(MLP)中,感知机包含一个隐藏层,其隐藏单元数是一个超参数$h$。通过使用$\tanh$作为激活函数,并且禁用偏置项。
下面来实现加性注意力。
1 | class AdditiveAttention(nn.Module): |
用一个小例子来演示上面的AdditiveAttention
类,其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小),实际输出为$(2,1,20)$、$(2,10,2)$和$(2,10,4)$。注意力池化输出的形状为(批量大小,查询的步数,值的维度)。
1 | queries, keys = torch.normal(0, 1, (2, 1, 20)), torch.ones((2, 10, 2)) |
尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的, 所以注意力权重是均匀的,由指定的有效长度决定。
1 | d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)), |
3.3. 缩放点积注意力
使用点积可以得到计算效率更高的评分函数,但是点积操作要求查询和键具有相同的长度$d$。假设查询和键的所有元素都是独立的随机变量,并且都满足零均值和单位方差,那么两个向量的点积的均值为$0$,方差为$d$。为确保无论向量长度如何,点积的方差在不考虑向量长度的情况下仍然是$1$,我们再将点积除以$\sqrt{d}$,则缩放点积注意力(scaled dot-product attention)评分函数为:
在实践中,我们通常从小批量的角度来考虑提高效率,例如基于$n$个查询和$m$个键-值对计算注意力,其中查询和键的长度为$d$,值的长度为$v$。查询$\mathbf Q\in\mathbb R^{n\times d}$、键$\mathbf K\in\mathbb R^{m\times d}$和值$\mathbf V\in\mathbb R^{m\times v}$的缩放点积注意力是:
下面的缩放点积注意力的实现使用了暂退法进行模型正则化。
1 | class DotProductAttention(nn.Module): |
为了演示上述的DotProductAttention类, 我们使用与先前加性注意力例子中相同的键、值和有效长度。 对于点积操作,我们令查询的特征维度与键的特征维度大小相同。
1 | queries = torch.normal(0, 1, (2, 1, 2)) |
与加性注意力演示相同,由于键包含的是相同的元素, 而这些元素无法通过任何查询进行区分,因此获得了均匀的注意力权重。
1 | d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)), |
4. Bahdanau 注意力
之前章节中探讨了机器翻译问题:通过设计一个基于两个循环神经网络的编码器-解码器架构,用于序列到序列学习(seq2seq)。具体来说,循环神经网络编码器将长度可变的序列转换为固定形状的上下文变量,然后循环神经网络解码器根据生成的词元和上下文变量按词元生成输出(目标)序列词元。然而,即使并非所有输入(源)词元都对解码某个词元都有用,在每个解码步骤中仍使用编码相同的上下文变量。有什么方法能改变上下文变量呢?
试着从Graves.2013
中找到灵感:在为给定文本序列生成手写的挑战中,Graves设计了一种可微注意力模型,将文本字符与更长的笔迹对齐,其中对齐方式仅向一个方向移动。受学习对齐想法的启发,Bahdanau等人提出了一个没有严格单向对齐限制的可微注意力模型 Bahdanau.Cho.Bengio.2014
。在预测词元时,如果不是所有输入词元都相关,模型将仅对齐(或参与)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力集中的输出来实现的。
由于这段是在NLP方向上加注意力机制,就粗略浏览一下,没做什么笔记。 —SZ
4.1. 模型
下面描述的Bahdanau注意力模型将遵循之前seq2seq中的相同符号表达。这个新的基于注意力的模型与seq2seq中的模型相同,只不过上下文变量$\mathbf{c}$在任何解码时间步$t’$都会被$\mathbf{c}_{t’}$替换。假设输入序列中有$T$个词元,解码时间步$t’$的上下文变量是注意力集中的输出:
其中,时间步$t’ - 1$时的解码器隐状态$\mathbf{s}_{t’ - 1}$是查询,编码器隐状态$\mathbf{h}_t$既是键,也是值,注意力权重$\alpha$是使用加性注意力打分函数计算的。
与之前描述的循环神经网络编码器-解码器架构略有不同,下图描述了Bahdanau注意力的架构。
1 | import torch |
4.2. 定义注意力解码器
下面我们看看如何定义Bahdanau注意力,实现循环神经网络编码器-解码器。 其实,我们只需重新定义解码器即可。 为了更方便地显示学习的注意力权重, 以下AttentionDecoder类定义了带有注意力机制解码器的基本接口。
1 | class AttentionDecoder(d2l.Decoder): |
接下来,让我们在接下来的Seq2SeqAttentionDecoder
类中实现带有Bahdanau注意力的循环神经网络解码器。首先,初始化解码器的状态,需要下面的输入:
- 编码器在所有时间步的最终层隐状态,将作为注意力的键和值;
- 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态;
- 编码器有效长度(排除在注意力池中填充词元)。
在每个解码时间步骤中,解码器上一个时间步的最终层隐状态将用作查询。
因此,注意力输出和输入嵌入都连结为循环神经网络解码器的输入。
1 | class Seq2SeqAttentionDecoder(AttentionDecoder): |
接下来,我们使用包含7个时间步的4个序列输入的小批量测试Bahdanau注意力解码器。
1 | encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, |
4.3. 训练
在这里指定超参数,实例化一个带有Bahdanau注意力的编码器和解码器, 并对这个模型进行机器翻译训练。 由于新增的注意力机制,训练要比没有注意力机制的慢得多。
1 | embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1 |
模型训练后,我们用它将几个英语句子翻译成法语并计算它们的BLEU分数。
1 | engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .'] |
1 | attention_weights = torch.cat([step[0][0][0] for step in dec_attention_weight_seq], 0).reshape(( |
训练结束后,通过可视化注意力权重你会发现,每个查询都会在键值对上分配不同的权重,这说明 在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。
1 | # 加上一个包含序列结束词元 |
5. 多头注意力
在实践中,当给定相同的查询、键和值的集合时,我们希望模型可以基于相同的注意力机制学习到不同的行为,然后将不同的行为作为知识组合起来,捕获序列内各种范围的依赖关系(例如,短距离依赖和长距离依赖关系)。因此,允许注意力机制组合使用查询、键和值的不同子空间表示(representation subspaces)可能是有益的。
为此,与其只使用单独一个注意力池化,我们可以用独立学习得到的$h$组不同的线性投影(linear projections)来变换查询、键和值。然后,这$h$组变换后的查询、键和值将并行地送到注意力池化中。最后,将这$h$个注意力池化的输出拼接在一起,并且通过另一个可以学习的线性投影进行变换,以产生最终输出。这种设计被称为多头注意力(multihead attention)Vaswani.Shazeer.Parmar.ea.2017
。对于$h$个注意力池化输出,每一个注意力池化都被称作一个头(head)。 下图展示了使用全连接层来实现可学习的线性变换的多头注意力。
5.1. 模型
在实现多头注意力之前,让我们用数学语言将这个模型形式化地描述出来。给定查询$\mathbf{q} \in \mathbb{R}^{d_q}$、键$\mathbf{k} \in \mathbb{R}^{d_k}$和值$\mathbf{v} \in \mathbb{R}^{d_v}$,每个注意力头$\mathbf{h}_i$($i = 1, \ldots, h$)的计算方法为:
其中,可学习的参数包括$\mathbf W_i^{(q)}\in\mathbb R^{p_q\times d_q}$、$\mathbf W_i^{(k)}\in\mathbb R^{p_k\times d_k}$和$\mathbf W_i^{(v)}\in\mathbb R^{p_v\times d_v}$,以及代表注意力池化的函数$f$。$f$可以是注意力评分函数中的加性注意力和缩放点积注意力。多头注意力的输出需要经过另一个线性转换,它对应着$h$个头连结后的结果,因此其可学习参数是$\mathbf W_o\in\mathbb R^{p_o\times h p_v}$:
基于这种设计,每个头都可能会关注输入的不同部分,可以表示比简单加权平均值更复杂的函数。
1 | import math |
5.2. 实现
在实现过程中通常选择缩放点积注意力作为每一个注意力头。为了避免计算代价和参数代价的大幅增长,我们设定$p_q = p_k = p_v = p_o / h$。值得注意的是,如果将查询、键和值的线性变换的输出数量设置为$p_q h = p_k h = p_v h = p_o$,则可以并行计算$h$个头。在下面的实现中,$p_o$是通过参数num_hiddens
指定的。
1 | class MultiHeadAttention(nn.Module): |
为了能够使多个头并行计算,上面的MultiHeadAttention
类将使用下面定义的两个转置函数。具体来说,transpose_output
函数反转了transpose_qkv
函数的操作。
1 | def transpose_qkv(X, num_heads): |
下面我们使用键和值相同的小例子来测试我们编写的MultiHeadAttention类。 多头注意力输出的形状是(batch_size,num_queries,num_hiddens)。
1 | num_hiddens, num_heads = 100, 5 |
1 | batch_size, num_queries = 2, 4 |
6. 自注意力和位置编码
在深度学习中,经常使用卷积神经网络(CNN)或循环神经网络(RNN)对序列进行编码。想象一下,有了注意力机制之后,我们将词元序列输入注意力池化中,以便同一组词元同时充当查询、键和值。具体来说,每个查询都会关注所有的键-值对并生成一个注意力输出。由于查询、键和值来自同一组输入,因此被称为自注意力(self-attention)Lin.Feng.Santos.ea.2017,Vaswani.Shazeer.Parmar.ea.2017
,也被称为内部注意力(intra-attention)Cheng.Dong.Lapata.2016,Parikh.Tackstrom.Das.ea.2016,Paulus.Xiong.Socher.2017
。本节将使用自注意力进行序列编码,以及如何使用序列的顺序作为补充信息。
1 | import math |
6.1. 自注意力
给定一个由词元组成的输入序列$\mathbf{x}_1, \ldots, \mathbf{x}_n$,其中任意$\mathbf{x}_i \in \mathbb{R}^d$($1 \leq i \leq n$)。该序列的自注意力输出为一个长度相同的序列
$\mathbf{y}_1, \ldots, \mathbf{y}_n$,其中:
根据2.4中定义的注意力池化函数$f$。下面的代码片段是基于多头注意力对一个张量完成自注意力的计算,张量的形状为(批量大小,时间步的数目或词元序列的长度,$d$)。输出与输入的张量形状相同。
1 | num_hiddens, num_heads = 100, 5 |
1 | batch_size, num_queries, valid_lens = 2, 4, torch.tensor([3, 2]) |
6.2. 比较卷积神经网络、循环神经网络和自注意力
接下来比较下面几个架构,目标都是将由$n$个词元组成的序列映射到另一个长度相等的序列,其中的每个输入词元或输出词元都由$d$维向量表示。具体来说,将比较的是卷积神经网络、循环神经网络和自注意力这几个架构的计算复杂性、顺序操作和最大路径长度。请注意,顺序操作会妨碍并行计算,而任意的序列位置组合之间的路径越短,则能更轻松地学习序列中的远距离依赖关系 Hochreiter.Bengio.Frasconi.ea.2001
。
考虑一个卷积核大小为$k$的卷积层。在后面的章节将提供关于使用卷积神经网络处理序列的更多详细信息。目前只需要知道的是,由于序列长度是$n$,输入和输出的通道数量都是$d$,所以卷积层的计算复杂度为$\mathcal{O}(knd^2)$。如上图所示,卷积神经网络是分层的,因此为有$\mathcal{O}(1)$个顺序操作,最大路径长度为$\mathcal{O}(n/k)$。例如,$\mathbf{x}_1$和$\mathbf{x}_5$处于上图中卷积核大小为3的双层卷积神经网络的感受野内。
当更新循环神经网络的隐状态时,$d \times d$权重矩阵和$d$维隐状态的乘法计算复杂度为$\mathcal{O}(d^2)$。由于序列长度为$n$,因此循环神经网络层的计算复杂度为$\mathcal{O}(nd^2)$。根据上图,有$\mathcal{O}(n)$个顺序操作无法并行化,最大路径长度也是$\mathcal{O}(n)$。
在自注意力中,查询、键和值都是$n \times d$矩阵。考虑3.3中缩放的”点-积“注意力,其中$n \times d$矩阵乘以$d \times n$矩阵。之后输出的$n \times n$矩阵乘以$n \times d$矩阵。因此,自注意力具有$\mathcal{O}(n^2d)$计算复杂性。正如在上图中所讲,每个词元都通过自注意力直接连接到任何其他词元。因此,有$\mathcal{O}(1)$个顺序操作可以并行计算,最大路径长度也是$\mathcal{O}(1)$。
总而言之,卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。
6.3. 位置编码
在处理词元序列时,循环神经网络是逐个的重复地处理词元的,而自注意力则因为并行计算而放弃了顺序操作。为了使用序列的顺序信息,通过在输入表示中添加位置编码(positional encoding)来注入绝对的或相对的位置信息。位置编码可以通过学习得到也可以直接固定得到。接下来描述的是基于正弦函数和余弦函数的固定位置编码Vaswani.Shazeer.Parmar.ea.2017
。
假设输入表示 $\mathbf{X} \in \mathbb{R}^{n \times d}$ 包含一个序列中$n$个词元的$d$维嵌入表示。位置编码使用相同形状的位置嵌入矩阵 $\mathbf{P} \in \mathbb{R}^{n \times d}$输出$\mathbf{X} + \mathbf{P}$,矩阵第$i$行、第$2j$列和$2j+1$列上的元素为:
乍一看,这种基于三角函数的设计看起来很奇怪。在解释这个设计之前,让我们先在下面的PositionalEncoding
类中实现它。
1 | class PositionalEncoding(nn.Module): |
在位置嵌入矩阵$\mathbf{P}$中,行代表词元在序列中的位置,列代表位置编码的不同维度。从下面的例子中可以看到位置嵌入矩阵的第$6$列和第$7$列的频率高于第$8$列和第$9$列。第$6$列和第$7$列之间的偏移量(第$8$列和第$9$列相同)是由于正弦函数和余弦函数的交替。
1 | encoding_dim, num_steps = 32, 60 |
6.3.1. 绝对位置信息
为了明白沿着编码维度单调降低的频率与绝对位置信息的关系,让我们打印出$0, 1, \ldots, 7$的[二进制表示]形式。正如所看到的,每个数字、每两个数字和每四个数字上的比特值在第一个最低位、第二个最低位和第三个最低位上分别交替。
1 | for i in range(8): |
在二进制表示中,较高比特位的交替频率低于较低比特位, 与下面的热图所示相似,只是位置编码通过使用三角函数在编码维度上降低频率。 由于输出是浮点数,因此此类连续表示比二进制表示法更节省空间。
1 | P = P[0, :, :].unsqueeze(0).unsqueeze(0) |
6.3.2. 相对位置信息
除了捕获绝对位置信息之外,上述的位置编码还允许模型学习得到输入序列中相对位置信息。这是因为对于任何确定的位置偏移$\delta$,位置$i + \delta$处的位置编码可以线性投影位置$i$处的位置编码来表示。
这种投影的数学解释是,令$\omega_j = 1/10000^{2j/d}$,对于任何确定的位置偏移$\delta$,位置编码中的任何一对$(p_{i, 2j}, p_{i, 2j+1})$都可以线性投影到$(p_{i+\delta, 2j}, p_{i+\delta, 2j+1})$:
$2\times 2$投影矩阵不依赖于任何位置的索引$i$。
7. Transformer
在6.2中比较了卷积神经网络(CNN)、循环神经网络(RNN)和自注意力(self-attention)。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,使用自注意力来设计深度架构是很有吸引力的。对比之前仍然依赖循环神经网络实现输入表示的自注意力模型 Cheng.Dong.Lapata.2016,Lin.Feng.Santos.ea.2017,Paulus.Xiong.Socher.2017
,Transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层 Vaswani.Shazeer.Parmar.ea.2017
。尽管Transformer最初是应用于在文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。
7.1. 模型
Transformer作为编码器-解码器架构的一个实例,其整体架构图如下所示。正如所见到的,Transformer是由编码器和解码器组成的。与带有Bahdanau注意力的循环神经网络编码器-解码器模型中基于Bahdanau注意力实现的序列到序列的学习相比,Transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的嵌入(embedding)表示将加上位置编码(positional encoding),再分别输入到编码器和解码器中。
上图概述了Transformer的架构。从宏观角度来看,Transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为$\mathrm{sublayer}$)。第一个子层是多头自注意力(multi-head self-attention)池化;第二个子层是基于位置的前馈网络(positionwise feed-forward network)。具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。受ResNet中残差网络的启发,每个子层都采用了残差连接(residual connection)。在Transformer中,对于序列中任何位置的任何输入$\mathbf{x} \in \mathbb{R}^d$,都要求满足$\mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$,以便残差连接满足$\mathbf{x} + \mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$。在残差连接的加法计算之后,紧接着应用层规范化(layer normalization)Ba.Kiros.Hinton.2016
。因此,输入序列对应的每个位置,Transformer编码器都将输出一个$d$维表示向量。
Transformer解码器也是由多个相同的层叠加而成的,并且层中使用了残差连接和层规范化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入了第三个子层,称为编码器-解码器注意力(encoder-decoder attention)层。在编码器-解码器注意力中,查询来自前一个解码器层的输出,而键和值来自整个编码器的输出。在解码器自注意力中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。这种掩蔽(masked)注意力保留了自回归(auto-regressive)属性,确保预测仅依赖于已生成的输出词元。
在此之前已经描述并实现了基于缩放点积多头注意力和位置编码。接下来将实现Transformer模型的剩余部分。
1 | import math |
7.2. 基于位置的前馈网络
基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是基于位置的(positionwise)的原因。在下面的实现中,输入X
的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,ffn_num_outputs
)的输出张量。
1 | class PositionWiseFFN(nn.Module): |
下面的例子显示,改变张量的最里层维度的尺寸,会改变成基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。
1 | ffn = PositionWiseFFN(4, 4, 8) |
7.3. 残差连接和层规范化
现在让我们关注加法和规范化(add&norm)组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。
“批量规范化”章节中解释了在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化和批量规范化的目标相同,但层规范化是基于特征维度进行规范化。尽管批量规范化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。
以下代码对比不同维度的层规范化和批量规范化的效果。
1 | ln = nn.LayerNorm(2) |
现在我们可以使用残差连接和层规范化来实现AddNorm类。暂退法也被作为正则化方法使用。
1 | class AddNorm(nn.Module): |
残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同。
1 | add_norm = AddNorm([3, 4], 0.5) |
7.4. 编码器
有了组成transformer编码器的基础组件,现在可以先实现编码器中的一个层。下面的EncoderBlock类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。
1 | class EncoderBlock(nn.Module): |
正如我们所看到的,transformer编码器中的任何层都不会改变其输入的形状。
1 | X = torch.ones((2, 100, 24)) |
下面实现的Transformer编码器的代码中,堆叠了num_layers
个EncoderBlock
类的实例。由于这里使用的是值范围在$-1$和$1$之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。
1 | class TransformerEncoder(d2l.Encoder): |
下面我们指定了超参数来创建一个两层的transformer编码器。 Transformer编码器输出的形状是(批量大小,时间步数目,num_hiddens)。
1 | encoder = TransformerEncoder( |
7.5. 解码器
如模型图所示,Transformer解码器也是由多个相同的层组成。在DecoderBlock
类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。
正如在本节前面所述,在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于序列到序列模型(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数dec_valid_lens
,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。
1 | class DecoderBlock(nn.Module): |
为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,编码器和解码器的特征维度都是num_hiddens。
1 | decoder_blk = DecoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5, 0) |
现在我们构建了由num_layers个DecoderBlock实例组成的完整的transformer解码器。最后,通过一个全连接层计算所有vocab_size个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。
1 | class TransformerDecoder(d2l.AttentionDecoder): |
7.6. 训练
依照Transformer架构来实例化编码器-解码器模型。在这里,指定Transformer的编码器和解码器都是2层,都使用4头注意力。为了进行序列到序列的学习,下面在“英语-法语”机器翻译数据集上训练Transformer模型。
1 | num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10 |
训练结束后,使用transformer模型将一些英语句子翻译成法语,并且计算它们的BLEU分数。
1 | engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .'] |
当进行最后一个英语到法语的句子翻译工作时,让我们可视化transformer的注意力权重。编码器自注意力权重的形状为(编码器层数,注意力头数,num_steps或查询的数目,num_steps或“键-值”对的数目)。
1 | enc_attention_weights = torch.cat(net.encoder.attention_weights, 0).reshape((num_layers, num_heads, |
在编码器的自注意力中,查询和键都来自相同的输入序列。因为填充词元是不携带信息的,因此通过指定输入序列的有效长度可以避免查询与使用填充词元的位置计算注意力。接下来,将逐行呈现两层多头注意力的权重。每个注意力头都根据查询、键和值的不同的表示子空间来表示不同的注意力。
1 | d2l.show_heatmaps( |
为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。例如用零填充被掩蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以序列开始词元(beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。
1 | dec_attention_weights_2d = [head[0].tolist() |
由于解码器自注意力的自回归属性,查询不会对当前位置之后的“键-值”对进行注意力计算。
1 | # Plusonetoincludethebeginning-of-sequencetoken |
与编码器的自注意力的情况类似,通过指定输入序列的有效长度,输出序列的查询不会与输入序列中填充位置的词元进行注意力计算。
1 | d2l.show_heatmaps( |
尽管transformer架构是为了“序列到序列”的学习而提出的,但正如我们将在本书后面提及的那样,transformer编码器或transformer解码器通常被单独用于不同的深度学习任务中。