注意力机制


不随意线索:不需要有想法,一眼就看到的东西

随意线索:想看书,所以去找了一本书

1.卷积、全连接、池化层都只考虑不随意线索

2.注意力机制则显示的考虑随意线索

  • 随意线索被称之为查询(query)
  • 每个输入是一个值(value)和不随意线索(key)的对
  • 通过注意力池化层来有偏向性的选择选择某些输入

与之前学习的所有层的区别在于加入了查询(query),根据查询,寻找自己表较感兴趣的东西

非参注意力汇聚概述(不需要学习参数)

在这里插入图片描述

通过K函数(核回归)计算x和xi的距离
在这里插入图片描述

参数化注意力机制概述

在之前的基础上引入可以学习的w
在这里插入图片描述

正式系统学习
1.平均汇聚(池化)

我们先使用最简单的估计器来解决回归问题: 基于平均汇聚来计算所有训练样本输出值的平均值:
在这里插入图片描述

import matplotlib.pyplot as plt
import torch
from torch import nn
from d2l import torch as d2l

n_train = 50
#torch.rand()产生一个服从均匀分布的张量,张量内的数据包含从区间[0,1)的随机数。参数size是一个整数序列,用于定义张量大小
#torch.sort()返回两个值 第一个为排序后的张量,第二个为原来的索引
x_train, _ = torch.sort(torch.rand(n_train) * 5)   # *5表示将[0,1)扩展到[0,5)

def f(x):   #真实的f(x)  需要被拟合
    return 2 * torch.sin(x) + x ** 0.8

y_train = f(x_train) + torch.normal(0, 0.5, (n_train,))
#测试集
x_test = torch.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)
    plt.show()

y_hat = torch.repeat_interleave(y_train.mean(), n_test)
plot_kernel_reg(y_hat)

运行结果如下图所示,这个估计器确效果一般: 真实函数f(“Truth”)和预测函数(“Pred”)相差很大。

在这里插入图片描述

2.非参数注意力汇聚(池化)

平均汇聚忽略了输入xi,我们可以根据输入的位置对输出yi进行加权

在这里插入图片描述

其中K是核,上述公式所描述的估计器被称为Nadaraya-Watson核回归,我们可以从注意力机制框架的角度重写,使之成为一个更加通用的注意力汇聚(attention pooling)公式:
在这里插入图片描述

其中x是查询,(xi, yi)是键值对,注意力汇聚是yi的加权平均,将查询x和键xi之间的关系建模为 注意力权重(attention weight)α(x,xi) 这个权重会被分配给每一个对应值yi。模型在所有键值对注意力权重都是一个有效的概率分布: 它们是非负的,并且总和为1。

高斯核:
在这里插入图片描述

将高斯核带入上面两个式子中得到:

在这里插入图片描述

如上式中,如果一个键xi越是接近给定的查询x, 那么分配给这个键对应值yiyi的注意力权重就会越大, 也就“获得了更多的注意力”。

torch.repeat_interleave()对tensor的特定维度进行复制

torch.repeat_interleave(self: Tensor, repeats: int, dim: Optional[int]=None)

参数说明:

self: 传入的数据为tensor

repeats: 复制的份数

dim: 要复制的维度,可设定为0/1/2…

例:

此处定义了一个4维tensor,要对第2个维度复制,由原来的1变为3,即将设定dim=1

    data1 = torch.rand([2, 1, 3, 3])
    print("data1_shape: ", data1.shape)
    print("data1: ", data1)

    data2 = torch.repeat_interleave(data1, repeats=3, dim=1)
    print("data2_shape: ", data2.shape)
    print("data2: ", data2)

torch.matmul() 支持不同维度tensor进行矩阵乘积(特殊情况会转化为点乘)

torch.matmul(input, other, out=None) → Tensor

torch.matmul()也是一种类似于矩阵相乘操作的tensor联乘操作。但是它可以利用python 中的广播机制,处理一些维度不同的tensor结构进行相乘操作。这也是该函数与torch.bmm()区别所在。

参数:

input,other:两个要进行操作的tensor结构

(1)若两个都是1D(向量)的,则返回两个向量的点积

(2)若两个都是2D(矩阵)的,则按照(矩阵相乘)规则返回2D

(3)若input维度1D,other维度2D,则先将1D的维度扩充到2D(1D的维数前面+1),然后得到结果后再将此维度去掉,得到的与input的维度相同。即使作扩充(广播)处理,input的维度也要和other维度做对应关系。

(4)若input是2D,other是1D,则返回两者的点积结果(可以理解为先对other进行广播,或者other与input的每一行进行点积)。

a = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = torch.tensor([1, 1, 1])
c = torch.matmul(a, b)
#tensor([ 6, 15, 24])

(5)如果一个维度至少是1D,另外一个大于2D,则返回的是一个批矩阵乘法( a batched matrix multiply)。

代码实现:

#X_repeat的形状为(n_test, n_train) 同一行的所有元素(测试输入,查询)都相同 每一行的结果为一个点
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))  #重复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_train = y_train.reshape(-1, 1)  #这一步可以不用做 matmul可以实现不同维度的矩阵成绩 但如果使用mm函数进行矩阵乘积必须转化为一列
y_hat = torch.matmul(attention_weights, y_train)
plot_kernel_reg(y_hat)

在这里插入图片描述

3.带参数注意力汇聚

非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点: 如果有足够的数据,此模型会收敛到最优结果。 尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。

与上一节的例子不同,下式的查询x和键xi之间的距离乘以可学习参数W:

在这里插入图片描述

torch.bmm():批量中进行矩阵乘法

为了更有效地计算小批量数据的注意力, 我们可以利用深度学习开发框架中提供的批量矩阵乘法。

在这里插入图片描述

X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
torch.bmm(X, Y).shape

torch.squeeze()

这个函数主要对数据的维度进行压缩,去掉维数为1的的维度,比如是一行或者一列(只有维度为1时才会去掉)

torch.unsqueeze()

这个函数主要是对数据维度进行扩充。给指定位置加上维数为一的维度

a = torch.arange(0, 6).view(2, 3)  #(2, 3)
print(a.shape)
#在第二个维度增加一个维度变为(2, 1, 3)
b = a.unsqueeze(1)  #a保持不变,将改变赋予b
print(b.shape)
#将b的第二个维度删去变为(2, 3)
c = b.squeeze()
print(c.shape)
#torch.Size([2, 3])
#torch.Size([2, 1, 3])
#torch.Size([2, 3])

模型构建:

class NWKernelRegression(nn.Module):
    def __init__(self, **kwargs):
        super(NWKernelRegression, self).__init__()
        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   #dim=1表示按行计算
        )
        return torch.bmm(self.attention_weights.unsqueeze(1),
                         values.unsqueeze(-1)).reshape(-1)  #并将结果变成一维

torch.nn.parameter.Parameter(data=None, requires_grad=True)

参数:

  • data (Tensor) – parameter tensor.
  • requires_grad (bool*,* optional) – if the parameter requires gradient. See Locally disabling gradient computation for more details. Default: True

torch.nn.Parameter是继承自torch.Tensor的子类,其主要作用是作为nn.Module中的可训练参数使用。它与torch.Tensor的区别就是nn.Parameter会自动被认为是module的可训练参数,即加入到parameter()这个迭代器中去;而module中非nn.Parameter()的普通tensor是不在parameter中的。
注意到,nn.Parameter的对象的requires_grad属性的默认值是True,即是可被训练的,这与torth.Tensor对象的默认值相反。
在nn.Module类中,pytorch也是使用nn.Parameter来对每一个module的参数进行初始化的

训练:

net = NWKernelRegression()
loss = nn.MSELoss(reduction='none')
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])

for epoch in range(5):
    optimizer.zero_grad()  #梯度清零
    l = loss(net(x_train, keys, values), y_train)  #计算损失
    l.sum().backward()  #将损失之和进行反向传播
    optimizer.step()  #更新
    print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
    animator.add(epoch + 1, float(l.sum()))

plt.show()
# 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)

在这里插入图片描述

在尝试拟合带噪声的训练数据时, 预测结果绘制的线不如之前非参数模型的平滑。

在这里插入图片描述

为什么新的模型更不平滑了呢? 我们看一下输出结果的绘制图: 与非参数的注意力汇聚模型相比, 带参数的模型加入可学习的参数后, 曲线在注意力权重较大的区域变得更不平滑。
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐