深度学习笔记36 注意力机制
这里其实有些跳课,最近看沐神看得比较少,在做毕业设计,但是因为要用到注意力机制了,所以抓紧时间过来先学这个,不按顺序学习是对我这个处女座最大的折磨(哭哭)
我在毕业设计中遇到了一个问题,对于HSV三个特征,我发现HS的贡献较小,V的贡献很大,所以我希望在模型训练中强化V所发挥的作用,因此我的第一个想法是:
在数据输入模型之前,要做归一化,比如HSV的范围为0-180,0-255,0-255,那我就全部除以最大值,将所有特征都局限到0-1,这样大家的影响都是一样的,而如果我想强化V的影响,我可以在归一化之后,对V额外乘以一个数字,比如乘以3,具体乘以多少,那就是调参的任务了。
然而,在和鑫哥交流之后,鑫哥告诉我这个类似注意力机制的思想,所以就过来先学习一下注意力机制。
早在60年代就有类似的概念:
拟合一组数据最简单的方法——平均。
K,在这里可以理解成一个核,就是一个计算距离的函数,用来计算x和xi之间的距离。这样的话,虽然仍然是对y求平均,但是有了一个权重(注意分母有求和,所以仍然是求平均),我们会重点考虑距离query更近的数据,而基本忽视其他距离此值很远的数据,从而达到“让注意力集中在某一个区域”的目的。
这个方案我非常喜欢,因为简单——不需要学习参数,直接就实现了注意力。
他用的核函数是高斯核:
当然这个是统计学上的注意力机制,没有可以学习的参数,所以我们可以加入一个w:
随意线索里面的随意,可以翻译为随着意识的意思(翻译学家背锅)
代码实验
1 n_train = 50 # 训练样本数 2 x_train, _ = torch.sort(torch.rand(n_train) * 5) # 排序后的训练样本 3 4 def f(x): 5 return 2 * torch.sin(x) + x**0.8 6 7 y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,)) # 训练样本的输出 8 x_test = torch.arange(0, 5, 0.1) # 测试样本 9 y_truth = f(x_test) # 测试样本的真实输出 10 n_test = len(x_test) # 测试样本数 11 n_test
定义了一个f(x),用来测试加入注意力机制的效果。
# 下面的函数将绘制所有的训练样本(样本由圆圈表示), 不带噪声项的真实数据生成函数(标记为“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) plot_kernel_reg(y_hat)
结果:
自然是完全不拟合。
接下来我们加入前面提到的非参注意力机制:
# 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)
可以看到,拟合曲线还是有那么些意思的,但是因为这是统计学的模型,没有可学习的参数,当然没有机器学习的精度。
我们可以看一下注意力机制的权重:
1 d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), 2 xlabel='Sorted training inputs', 3 ylabel='Sorted testing inputs')
其实就是查看训练数据和测试数据的相关性,相关性高的地方权重值就高,因此可以更好地拟合曲线。
这里试数据的输入相当于查询,而训练数据的输入相当于键。 因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近, 注意力汇聚的注意力权重就越高。
我们可以把这个模型改为带权重的小批量矩阵乘法的注意力池化层
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')
可以看到效果会更好一些。
我的问题
但是正如我开始提到的问题,我想让模型对于HSV三个特征中更加注重V,这个和沐神讲的注意力机制我觉得是没什么关系的。
注意力机制,指的是给模型一种注意力机制,让模型根据test数据,更加注重于对某些训练数据的使用。我们对于模型的干预仅仅是加入了注意力机制,具体要他注意什么,我们不知道,这是模型要自己学习出来的问题。
而我的问题,在于我知道应该让模型注重什么数据,而不需要它自己学,所以不需要动网络结构,归一化之后乘以一个值来扩大权重,我觉得是最轻松的方法。