注意力机制——注意力提示与汇聚
当我们在观察图像时,我们会将注意力集中在物体的关键部分,而忽略掉无关紧要的信息。而注意力机制则可以帮助神经网络模型实现类似的功能,从而提高图像分类和识别的准确性。
本章首先了解一下注意力提示与汇聚。
参考书:
《动手学深度学习》
注意力是如何应用于视觉世界中的呢?
这要从当今十分普及的双组件的框架开始讲起。在这个框架中,受试者基于非自主性提示和自主性提示有选择地引导注意力的焦点。
非自主性提示是基于环境中物体的突出性和易见性。
假如我们眼前有一朵鲜红的玫瑰花、一张白纸、一本书、一份报纸、一台手机。因为玫瑰花的颜色是鲜红的,其余物品色彩不明显。
那么玫瑰花在这种视觉环境中是突出和显眼的, 不由自主地引起我们的注意。 所以我们会把视力最敏锐的地方放到花上。
等回过神来,重新聚焦眼睛时,我们看到了手机。
这与前面由于突出性导致的选择不同, 此时选择手机是受到了认知和意识的控制, 因此注意力在基于自主性提示去辅助选择时将更为谨慎。
自主性的与非自主性的注意力提示解释了人类的注意力的方式, 下面来看看如何通过这两种注意力提示, 用神经网络来设计注意力机制的框架,
首先,考虑一个相对简单的状况,即只使用非自主性提示。要想将选择偏向于感官输入,
则可以简单地使用参数化的全连接层,甚至是非参数化的汇聚层。
因此,是否包含自主性提示将注意力机制与全连接层或汇聚层区别开来。
在注意力机制的背景下,自主性提示被称为查询。给定任何查询,注意力机制通过注意力汇聚将选择引导至感官输入(例如中间特征表示)。
在注意力机制中,这些感官输入被称为值(value)。
更通俗的解释,每个值都与一个键(key)配对,这可以想象为感官输入的非自主提示。
如图所示,可以通过设计注意力汇聚的方式, 便于给定的查询(自主性提示)与键(非自主性提示)进行匹配, 这将引导得出最匹配的值(感官输入)。
平均汇聚层可以被视为输入的加权平均值,其中各输入的权重是一样的。实际上,注意力汇聚得到的是加权平均的总和值,
其中权重是在给定的查询和不同的键之间计算得出的。
(也就是说感官输入是在给定的自主性提示和不同的非自主性提示之间计算相似度得出的)
为了可视化注意力权重,需要定义一个show_heatmaps函数。 其输入matrices的形状是 (要显示的行数,要显示的列数)即查询的数目,键的数目。
import torch
from d2l import torch as d2l
# 返回的热图是以行数为y轴,列数为x轴。
def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(3, 3), cmap="Reds"):
# 显示矩阵热图
d2l.use_svg_display() #设置使用SVG格式显示图像
num_rows, num_cols = matrices.shape[0], matrices.shape[1]
fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, sharex=True, sharey=True, squeeze=False)
for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)):
for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)):
pcm = ax.imshow(matrix.detach().numpy(), cmap=cmap) #使用imshow函数将矩阵转换为图像,并指定颜色映射为cmap
if i == num_rows - 1:
ax.set_xlabel(xlabel)
if j == 0:
ax.set_ylabel(ylabel)
if titles:
ax.set_title(titles[j])
fig.colorbar(pcm, ax=axes, shrink=0.6) #在图像上添加颜色条
# 下面使用一个简单的例子进行演示。 在本例子中,仅当查询和键相同时,注意力权重为1,否则为0。
attention_weights = torch.eye(10).reshape((1, 1, 10, 10))
show_heatmaps(attention_weights, xlabel="Keys", ylabel="Queries", titles="heatmaps")
d2l.plt.show()
受试者使用非自主性和自主性提示有选择性地引导注意力。前者基于突出性,后者则依赖于意识。
注意力机制与全连接层或者汇聚层的区别源于增加的自主提示。
注意力机制通过注意力汇聚使选择偏向于值(感官输入),其中包含查询(自主性提示)和键(非自主性提示)。键和值是成对的。
查询(自主提示)和键(非自主提示)之间的交互形成了注意力汇聚;**注意力汇聚有选择地聚合了值(感官输入)**以生成最终的输出。
1964年提出的Nadaraya-Watson核回归模型是一个简单但完整的例子,可以用于演示具有注意力机制的机器学习。
给定的成对的“输入-输出”数据集 { ( x 1 , y 1 ) , … , ( x n , y n ) } \{(x_1, y_1), \ldots, (x_n, y_n)\} {(x1,y1),…,(xn,yn)},学习 f f f来预测任意新输入 x x x的输出 y ^ = f ( x ) \hat{y} = f(x) y^=f(x)
根据下面的非线性函数生成一个人工数据集,
其中加入的噪声项为 ϵ \epsilon ϵ:
y i = 2 sin ( x i ) + x i 0.8 + ϵ , y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon, yi=2sin(xi)+xi0.8+ϵ,
n_train = 50 # 训练样本数
#torch.sort()函数返回了两个值,第一个是排序后的张量,第二个是排序后的索引
x_train, _ = torch.sort(torch.rand(n_train) * 5)
# print(_)
# print(x_train)
def f(x):
return 2 * torch.sin(x) + x**0.8
y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,)) #添加随机噪声项,生成训练样本的输出y_train。
x_test = torch.arange(0, 5, 0.1) # 测试样本
y_truth = f(x_test) # 测试样本的真实输出
n_test = len(x_test) # 测试样本数
print(n_test)
#下面的函数将绘制所有的训练样本(样本由圆圈表示),
#不带噪声项的真实数据生成函数f(标记为“Truth”), 以及学习得到的预测函数(标记为“Pred”)
def plot_kernel_reg(y_hat):
d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'],
xlim=[0, 5], ylim=[-1, 5])
d2l.plt.plot(x_train, y_train, 'o', alpha=0.5)
先使用最简单的估计器来解决回归问题。 基于平均汇聚来计算所有训练样本输出值的平均值
y_hat = torch.repeat_interleave(y_train.mean(),n_test)
#y_train.mean()计算了训练样本的平均值,然后使用torch.repeat_interleave()函数将这个平均值按照n_test的值进行重复,
# 生成一个与测试样本数量相同的张量。这样可以得到一个预测结果y_hat,其中所有的值都是训练样本平均值。
plot_kernel_reg(y_hat)
d2l.plt.show()
显然,平均汇聚忽略了输入 x i x_i xi。可以根据输入的位置对输出 y i y_i yi进行加权:
f ( x ) = ∑ i = 1 n K ( x − x i ) ∑ j = 1 n K ( x − x j ) y i , f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i, f(x)=i=1∑n∑j=1nK(x−xj)K(x−xi)yi,
我们可以从注意力机制框架的角度重写上式,成为一个更加通用的注意力汇聚公式:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i , f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i, f(x)=i=1∑nα(x,xi)yi,
其中 x x x是查询, ( x i , y i ) (x_i, y_i) (xi,yi)是键值对。
将查询 x x x和键 x i x_i xi之间的关系建模为注意力权重 α ( x , x i ) \alpha(x, x_i) α(x,xi),
这个权重将被分配给每一个对应值 y i y_i yi。
对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布:它们是非负的,并且总和为1。
为了更好地理解注意力汇聚, 下面考虑一个高斯核,其定义为:
K ( u ) = 1 2 π exp ( − u 2 2 ) . K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}). K(u)=2π1exp(−2u2).
将高斯核代入前面两个式子可以得到:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i = ∑ i = 1 n exp ( − 1 2 ( x − x i ) 2 ) ∑ j = 1 n exp ( − 1 2 ( x − x j ) 2 ) y i = ∑ i = 1 n s o f t m a x ( − 1 2 ( x − x i ) 2 ) y i . \begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}(x - x_i)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}(x - x_j)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}(x - x_i)^2\right) y_i. \end{aligned} f(x)=i=1∑nα(x,xi)yi=i=1∑n∑j=1nexp(−21(x−xj)2)exp(−21(x−xi)2)yi=i=1∑nsoftmax(−21(x−xi)2)yi.
故如果一个键 x i x_i xi越是接近给定的查询 x x x,那么分配给这个键对应值 y i y_i yi的注意力权重就会越大,也就“获得了更多的注意力”。
from torch import nn
# X_repeat的形状:(n_test,n_train),每一行都包含着相同的测试输入(例如:同样的查询)
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))
# x_train包含着键。attention_weights的形状:(n_test,n_train),
# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重
attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1)
# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = torch.matmul(attention_weights, y_train)
plot_kernel_reg(y_hat)
d2l.plt.show()
d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')
d2l.plt.show()
与非参数的Nadaraya-Watson核回归略有不同,在下面的查询 x x x和键 x i x_i xi之间的距离乘以可学习参数 w w w:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i = ∑ i = 1 n exp ( − 1 2 ( ( x − x i ) w ) 2 ) ∑ j = 1 n exp ( − 1 2 ( ( x − x j ) w ) 2 ) y i = ∑ i = 1 n s o f t m a x ( − 1 2 ( ( x − x i ) w ) 2 ) y i . \begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_j)w)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.\end{aligned} f(x)=i=1∑nα(x,xi)yi=i=1∑n∑j=1nexp(−21((x−xj)w)2)exp(−21((x−xi)w)2)yi=i=1∑nsoftmax(−21((x−xi)w)2)yi.
为了更有效地计算小批量数据的注意力,
我们可以利用深度学习开发框架中提供的批量矩阵乘法。
假设我们有一个形状为(batch_size, n, m)的张量A,表示有batch_size个矩阵,每个矩阵的形状是(n, m)。另外,假设我们有一个形状为(batch_size, m, p)的张量B,表示有batch_size个矩阵,每个矩阵的形状是(m, p)
那么,批量矩阵乘法的结果是一个形状为(batch_size, n, p)的张量C,表示有batch_size个矩阵,每个矩阵的形状是(n, p)
X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
print(torch.bmm(X, Y).shape)
#结果:
torch.Size([2, 1, 6])
在注意力机制的背景中,我们可以[使用小批量矩阵乘法来计算小批量数据中的加权平均值]。
weights = torch.ones((2, 10)) * 0.1
values = torch.arange(20.0).reshape((2, 10))
torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1))
基于带参数的注意力汇聚公式,使用小批量矩阵乘法, 定义Nadaraya-Watson核回归的带参数版本为:
class NWKernelRegression(nn.Module):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = nn.Parameter(torch.rand((1,), requires_grad=True))
def forward(self, queries, keys, values):
# queries和attention_weights的形状为(查询个数,“键-值”对个数)
queries = queries.repeat_interleave(keys.shape[1]).reshape((-1, keys.shape[1]))
self.attention_weights = nn.functional.softmax(
-((queries - keys) * self.w)**2 / 2, dim=1)
# values的形状为(查询个数,“键-值”对个数)
return torch.bmm(self.attention_weights.unsqueeze(1),
values.unsqueeze(-1)).reshape(-1)
接下来,将训练数据集变换为键和值,用于训练注意力模型。 在带参数的注意力汇聚模型中, 任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算, 从而得到其对应的预测输出。
# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入
X_tile = x_train.repeat((n_train, 1))
# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出
Y_tile = y_train.repeat((n_train, 1))
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
net = NWKernelRegression()
loss = nn.MSELoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.5)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])
for epoch in range(5):
trainer.zero_grad()
l = loss(net(x_train, keys, values), y_train)
l.sum().backward()
trainer.step()
print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
animator.add(epoch + 1, float(l.sum()))
# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键)
keys = x_train.repeat((n_test, 1))
# value的形状:(n_test,n_train)
values = y_train.repeat((n_test, 1))
y_hat = net(x_test, keys, values).unsqueeze(1).detach()
plot_kernel_reg(y_hat)
d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')
d2l.plt.show()
Nadaraya-Watson核回归的注意力汇聚是对训练数据中输出的加权平均。
从注意力的角度来看,分配给每个值的注意力权重取决于将值所对应的键和查询作为输入的函数。
注意力汇聚可以分为非参数型和带参数型。
学习了注意力机制:通过注意力汇聚使选择偏向于值(感官输入),其中包含查询(自主性提示)和键(非自主性提示)。键和值是成对的。
注意力汇聚可以分为非参数型和带参数型。
分配给每个值的注意力权重取决于将值所对应的键和查询作为输入的函数。
鸡犬之声相闻,民至老死,不相往来。
–2023-10-17 进阶篇