目录
前言
学习本节知识后,相信你可以对注意力机制有基本的了解和实现的能力。
本节学习时间大概需要一整个下午加上半个晚上,请劳逸结合,合理规划时间,如果你对d2l内置库不够了解,或对深度学习的基础知识不够了解,你可能需要更多的时间。
引入
在深度学习中,我们常常使用“注意力机制”进行学习,那么注意力机制到底是什么呢?在生物学上,注意力是指我们在观察图像或视频时将观察重点放在什么地方。在视觉中,人类基于非自主性提示和自主性提示 有选择地引导注意力的焦点。非自主性提示是基于环境中物体的突出性和易见性。 自主性提示则是受到了认知和意识的主观上的控制产生的。在深度学习中,也可以使用类似的方法来使机器将注意力集中到某处。
基本实现
首先,要实现非自主性提示,可以简单地使用参数化的全连接层, 甚至是非参数化的最大汇聚层或平均汇聚层。而要使用自主性提示,则是要给定一个“查询”(Query),通过注意力池化(Attention Pooling),与键(Key)进行匹配,从而对应相应的值(Value)。
理解基本原理后,我们开始实现注意力机制。注意力池化本质上就是平均池化层加上权重进行池化的过程。 下面首先建立一个10x10对角矩阵,对角线的值为1,其他为0。具体实现较为复杂,使用imshow()来绘制,画出矩阵对应热图,只需要知道输入matrices
的形状是 (要显示的行数,要显示的列数,查询的数目,键的数目)即可。
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5),cmap='Reds'):
d2l.use_svg_display()
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.asnumpy(), cmap=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);
attention_weights = np.eye(10).reshape((1, 1, 10, 10))
show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries')
平均池化
生成数据集
下面代码采用平均汇聚的方法,其数据计算方式为,生成五十个数据作为训练数据集,训练后进行测试
平均池化方法
平均池化使用进行训练。我们当然知道,这样的“训练”其实是十分无效的,但是通过平均池化,可以模拟一种,将注意力放在全局(平均注意力)的方法,产生的状况。
代码实现
from mxnet import autograd, gluon, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
n_train = 50
x_train = np.sort(np.random.rand(n_train) * 5)
def f(x):
return 2 * np.sin(x) + x**0.8
y_train = f(x_train) + np.random.normal(0.0, 0.5, (n_train,)) # 训练样本的输出
x_test = np.arange(0, 5, 0.1)
y_truth = f(x_test)
n_test = len(x_test)
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 = y_train.mean().repeat(n_test)
plot_kernel_reg(y_hat)
运行结果
产生这样的一种预测显然也是在我们预料之内的。
非参数注意力池化
介绍
非参数注意力池化,也称之为Nadaraya-Watson核回归,它提出根据输入的位置对输出yi进行加权。Nadaraya-Watson核回归的定义是这样的:
注意力机制中的非参注意力池化
其中K是核(kernel)。这里不会深入讨论核函数的细节, 但受此启发, 我们可以从注意力机制框架的角度重写本式,使之成为一个更加通用的注意力池化(attention pooling)公式:。
其中x是查询,是键值对,注意力汇聚是的加权平均。将查询x和键xi之间的关系建模为注意力权重(attention weight),这个权重将被分配给每一个对应值。对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布: 它们是非负的,并且总和为1。
相信对深度学习有一些了解的人此刻就会想到Softmax回归。我们考虑一个高斯核(Gaussian kernel),定义为:。
代入注意力计算公式可以得出,。由此,我们得到了我们所熟悉的softmax运算。此时,如果一个键xi越是接近给定的查询x, 那么分配给这个键对应值yi的注意力权重就会越大, 也就“获得了更多的注意力”。
说明
可以发现,在上述公式中已经有了“注意力”的存在,但并不需要进行任何参数的学习,而是直接进行计算,因此这种注意力机制称之为非参数注意力池化。
代码实现
分为两个片段,分别为预测结果展示和注意力热图展示。
X_repeat = x_test.repeat(n_train).reshape((-1, n_train))
attention_weights = npx.softmax(-(X_repeat - x_train)**2 / 2)
y_hat = np.dot(attention_weights, y_train)
plot_kernel_reg(y_hat)
d2l.show_heatmaps(np.expand_dims(np.expand_dims(attention_weights, 0), 0),xlabel='Sorted training inputs',ylabel='Sorted testing inputs')
运行结果
显然,结果有了一定效果,而根据热图可以看出,注意力的位置基本与y=x一致,这也是符合我们的预期的。
带参数注意力池化
介绍
非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点: 如果有足够的数据,此模型会收敛到最优结果。 尽管如此,我们还是可以轻松地将可学习的参数集成到注意力池化中。相对于Nadaraya-Watson核回归,这只是多一个权重参数而已。它的计算是这样的:
代码实现
定义模型
基于带参数的注意力汇聚,使用小批量矩阵乘法, 定义Nadaraya-Watson核回归的带参数版本为:
class NWKernelRegression(nn.Block):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = self.params.get('w', shape=(1,))
def forward(self, queries, keys, values):
queries = queries.repeat(keys.shape[1]).reshape((-1, keys.shape[1]))
self.attention_weights = npx.softmax(
-((queries - keys) * self.w.data())**2 / 2)
return npx.batch_dot(np.expand_dims(self.attention_weights, 1),
np.expand_dims(values, -1)).reshape(-1)
训练
接下来,将训练数据集变换为键和值用于训练注意力模型。 在带参数的注意力汇聚模型中, 任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算, 从而得到其对应的预测输出。
# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入
X_tile = np.tile(x_train, (n_train, 1))
# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出
Y_tile = np.tile(y_train, (n_train, 1))
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - np.eye(n_train)).astype('bool')].reshape((n_train, -1))
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - np.eye(n_train)).astype('bool')].reshape((n_train, -1))
net = NWKernelRegression()
net.initialize()
loss = gluon.loss.L2Loss()
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5})
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])
for epoch in range(5):
with autograd.record():
l = loss(net(x_train, keys, values), y_train)
l.backward()
trainer.step(1)
print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
animator.add(epoch + 1, float(l.sum()))
运行结果
损失函数
拟合曲线
# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键)
keys = np.tile(x_train, (n_test, 1))
# value的形状:(n_test,n_train)
values = np.tile(y_train, (n_test, 1))
y_hat = net(x_test, keys, values)
plot_kernel_reg(y_hat)
注意力热图
d2l.show_heatmaps(np.expand_dims(
np.expand_dims(net.attention_weights, 0), 0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')