学习《动手学习深度学习》注意力机制 之后,简单做个总结。
注意力提示
假设坐在一个物品很多的房间里,我们容易受到比较显眼的物品的吸引,进而将注意力倾注在那个物品上,这样我们就忽略了周围物品;换一种情形,如果你想读一本书,那么你进入房间将注意力放在书上,如果情况很急,甚至不论物品有多显眼,也无法博得你的注意力。
前者是一种非自主性注意力,后者是自主性注意力。在注意力的背景下,将自主性提示称为查询(query),给定任何查询,注意力机制通过注意力汇聚(attention pooling),将选择引导至感官输入(sensory inputs, 例如中间特征表示)。在注意力机制中,感官输入称为值(value)。每个值都和一个键(key)配对,可以想象为感官输入的非自主提示。如下图所示(图来自原文),我们通过设计注意力汇聚,将查询(自主性提示)和键(非自主性输入)结合在一起,实现对值(感官输入)的选择倾向。

注意力汇聚
非参数注意力汇聚
Nadaraya-Watson核回归:
其中,K是核。受到Nadaraya-Watson的启发,我们可以归纳出来一个更加通用的注意力汇聚公式:
其中x是查询(自主性提示),是键值对(非意志线索与感觉输入)。注意力汇聚是的加权平均,将查询 x和键之间的关系建模为注意力权重(attention weight) ,这个权重将被分配给每一个对应值。对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布: 它们是非负的,并且总和为1。
如果我们将Nadaraya-Watson核回归中的核换成一个高斯核,其定义如下:
将高斯核代入就可以得到如下公式:
在上式的结果中,我们会发现如果一个键的值越接近与给定的查询x,那么分配给这个键对应值的注意力权重就越大,也就获得越多的注意力。下面用一个示例来将上述概念串联起来:
1 | # 目标函数 |
上面代码生成了长度为n_train的训练集、长度为n_test的测试集,接下来,我们将x_test构造为查询(意志线索),将x_train作为为键(非意志线索),y_train作为值(感官输入),并将查询 x和键之间的关系建模为注意力权重:
1 | # 每一行都包含相同的测试输入(相同的查询) |
为什么选择x_test作为查询(意志线索)呢?因为我们想要得到的是y_hat,输入是x_test,假设我们时刻都保持着有意识的状态,那么我们就根据自己的意志(x_test)结合环境的特征(x_train),处理接收到的感觉输入(y_train)最终找到自己想到的物品(y_hat)。可能会疑惑为什么y_truth不作为感觉输入?因为y_truth是用来衡量我们的输入x_test所得到的结果如何,也就是将其和y_hat进行比对的过程。
将上述矩阵操作过程使用图来表示如下:
那么在表达式中,体现在哪里呢?我们在得到上图的两个矩阵之后,将两个矩阵相减,对于x_repeat矩阵,每一行中的元素都相等,不同行的元素不等,对应的是不同的查询,而右边经过广播机制得到的矩阵中,每一列的元素相等的,不同行的元素表示不同的键,两个矩阵相减之后就实现:给定一个查询,计算所有的键和查询之间的差距。
看上去有点像x_train对x_test进行加权得到attention_weights,但是实际上这也仅仅是矩阵运算,单从矩阵运算来看,将谁看作谁的加权都行,但是结合我们的实际问题背景来看,将x_test看作x_train的加权更加合理,因为我们将x_test作为意志线索,而x_train作为非意志线索。非意志线索-感官输入 对于我们的感官来讲都是时刻存在的,如果加上我们的意识的话可以帮助我们更好的从外界注意到我们想要获得的东西,所以理解为x_test对x_train进行加权更合理。当然这个加权操作之后得到的是attention_weights,这个矩阵又可以作为y_train的加权,最终可以得到y_hat。
那么这种非参数注意力汇聚的操作预测效果如何呢?上述代码的运行结果如下:
上面说到,如果一个键的值越接近于给定的查询x,那么分配给这个键对应值的注意力权重就越大,即注意力汇聚的注意力权重越大,也就获得越多的注意力,将attention_weights可视化得到如下结果,从结果上来看,矩阵的对角线权值更高,即键和查询相等的部分权值更高,表明所使用的方法达到了一定的效果:

**这个注意力权重满足每一行的所有值之和为1,**其实是代码内部将每一行的所有元素一起执行softmax函数运算的结果。
但是效果不是很好,是否和数据量有关呢?将数据量改成500和1000分别测试,得到的结果如下:
数据量为500:
数据量为1000:
发现拟合效果并没有很大的变化,说明与数据量无关。其实也好理解,如果仅仅增加数据量的话,变化的仅仅是权重矩阵的规模,而权重矩阵仅仅是由训练数据和测试数据的差值经过一层线性变换得到,其表达能力不够(欠拟合)。
参数注意力汇聚
和非参数注意力汇聚不同的是,我们在查询和键之间的距离乘以可学习参数就可以得到如下的公式:
有了可学习参数,我们接下来的目标就是学习注意力汇聚的参数。
构建数据
首先我们根据训练集x_train来构建查询、键、值。然后我们根据查询-键构建注意力矩阵,将注意力矩阵与值进行运算,从而得到输出结果。在带参数的注意力汇聚模型中, 任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算, 从而得到其对应的预测输出。
1 | # X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入 |
X_title和Y_title形状如下所示,每一行表示x_train,使用不同的颜色来表示不同的值(蓝、黑、绿、紫红),相同的颜色表示相同的值:
根据X_title和Y_title构建keys和values:
1 | # keys的形状:('n_train','n_train'-1) |
keys和values的形状如下所示:
1 | class NWKernelRegression(nn.Module): |
queries形状如下所示:
那么在表达式中,体现在哪里呢?
queries的每一行中所有元素都相等,表示一个特定的查询x,每一列的元素不等,表示给定不同的查询;keys中每一行的所有元素不等,表示给定不同的键,由于我们的查询矩阵使用的是训练集构造的,而键也是使用训练集构造的,我们构造注意力权重矩阵的目的也是为了计算查询和键之间的差距,所以为了防止值相等的查询和键之间的运算误导权重参数的训练,我们构造X_title,将其中每一行与其相对的查询值相等的值去掉了,从而得到keys。将queries和keys相减,乘以系数还有,然后沿着每一行执行softmax函数操作,就可以得到权重矩阵,同样,在这个权重矩阵中, 每一行的所有元素之和为1。
上述 任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算体现在哪呢?
我们观察函数表达式,有一个queries-keys
的运算,我们尝试将两个矩阵进行相减,通过图来看容易发现没有任何相同颜色的值之间的运算,那么是否能保证所有的情况都包含在内呢?这是一个排列组合问题,从4个数中按顺序随机抽取两个,放回,一共有4$\times$3=12种情况,而我们刚刚又说了没有两个相同的数,所以两个矩阵之间的对应元素相减运算满足了上述的要求。所以这么看来,在这个案例中,注意力机制是将训练数据之间互相运算,得到一个权重矩阵,利用权重矩阵对标签值相乘最终得到预测结果。
在上面代码的forward函数最终返回结果上:
1 | return torch.bmm(self.attention_weights.unsqueeze(1), |
通过torch提供的批量矩阵乘法运算(torch.bmm()),实现了小批量数据中的加权平均值的计算。比如按照我所画的图中矩阵的形状,得到的attention_weights形状为(4, 3),添加一个维度之后就是(4, 1, 3),而values添加一个维度之后形状变为(4, 3, 1),二者相当于一个batch_size=4的数据,每一个batch的大小分别为(1, 3)和(3, 1),那么这一步骤的矩阵运算的含义是什么?
首先我们得到的attention_weights如下所示,我使用相同的颜色表示来自相同查询的运算结果(蓝色、黑色、绿色、紫红),而且相同颜色的一行和为1,因为他们一并经过了softmax运算:
此时attention_weights的形状为(4, 3),values的形状为(4, 3),二者无法进行矩阵相乘,而且我们希望的是将attention_weights与相对应的一行values进行运算,得到4个值。因为我们的attention_weights从某种角度可以看作是keys“演化”过来的,keys和values有必然的联系,因为有。基于这一需求,我们将attention_weights的形状扩充为(4, 1, 3),将values的形状扩充为(4, 3, 1),这样使用函数torch.bmm计算的时候会将第一个维度4看做batch,那么就相当于4个batch的矩阵运算,每一个batch是形状为(1, 3)和(3, 1)的矩阵之间的运算,得到的结果是一个(1, 1)的值。这个值加上批次维度就是(4, 1, 1)。上述运算结果如下图所示:
得到predict output之后,将其和values计算损失值,进而计算梯度,优化权重参数。
训练结果
按照常规的训练过程训练参数,我们可以得到如下的损失值结果:
将训练好的模型用于拟合测试集,得到如下的拟合结果:
从中可以看出这种方法还有许多不足如:曲线在注意力权重较大的区域(看下面的热力图)变得更不平滑(意味着可能不满足连续可导)、损失值不能继续下降等,这就有了后面更多的注意力机制模型来解决这些问题。
那么,学习得到的参数的价值是什么?
我们将参数输出来看一下:
1 | tensor([23.3051], requires_grad=True) |
值大约为23,且参数只是一个标量参数,不是向量,那就说明所有的这一步运算:
中的都是相同的参数23,我们试着将非参数注意力汇聚添加一个23的权重:
1 | attention_weights = nn.functional.softmax(-((x_repeat - x_train) * 23)**2 / 2, dim=1) |
得到的结果如下:
可以发现这样的话拟合效果就更好了,但是也变成了和参数注意力汇聚一样尖锐的加权区域。同样,在这个权重矩阵中, 每一行的所有元素之和为1。
为什么在可视化注意力权重时,它会使加权区域更加尖锐?
我们观察上述公式发现,加在外,平方之内,乘以一个之后就相当于绝对值扩大了264倍左右,我们知道softmax函数在x趋向负无穷的时候值无限趋近于0,所以这样一来就保留了键和查询之间的差距足够小的pair,过滤掉了键和值差距较大的pair,从而达到注意力效果,使得预测结果更加准确。
使用x_test构造queries如何?
其实在构造queries和keys的时候我一直很疑惑为什么不使用x_test来构造queries,我尝试了一下,发现过拟合结果非常严重:
是不是和数据量有关呢?我将训练集和测试集都设置为2000个,得到如下的拟合结果:
可以看到拟合效果非常好,但是也非常的不光滑!但是,随即我又担心,或许增加数据量之后,利用x_train构建queries的情况结果也会很好?测试了一下发现果然如此:
可以看见这种情况下相对于使用x_test构建queries更光滑,所以使用x_test并不会大幅度提高性能,反而容易造成过拟合以及拟合函数结果更加不光滑。
自己设计一个带参数的注意力汇聚模型
考虑到**对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布: 它们是非负的,并且总和为1。**以及softmax函数刚好满足输出的结果之和为1,我保留softmax,修改softmax内部的内容,即
我尝试过参考交叉熵误差,将中间部分换成:
但是没有任何效果,所以我还是保留这里的均方,通过修改参数的数量,即的数量,上面提到前面实验的都是仅有一个值的情况,拟合效果不好可能因为欠拟合,所以下面我将其换成和queries、keys的宽度相同数量,数据量为2000,迭代次数为100,得到如下的实验结果:
可以看见拟合效果非常好,而且光滑,但是损失值比较大。但是我的做法容易让人质疑:在这里使用100个epoch,在数量为1的情况下使用多个epoch会如何呢?结果是更加“不光滑”: