注意力机制(含pytorch代码及各函数详解)
目录注意力机制非参注意力汇聚概述(不需要学习参数)参数化注意力机制概述正式系统学习1.平均汇聚(池化)2.非参数注意力汇聚(池化)3.带参数注意力汇聚注意力机制不随意线索:不需要有想法,一眼就看到的东西随意线索:想看书,所以去找了一本书1.卷积、全连接、池化层都只考虑不随意线索2.注意力机制则显示的考虑随意线索随意线索被称之为查询(query)每个输入是一个值(value)和不随意线索(key)的
注意力机制
不随意线索:不需要有想法,一眼就看到的东西
随意线索:想看书,所以去找了一本书
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)
在尝试拟合带噪声的训练数据时, 预测结果绘制的线不如之前非参数模型的平滑。
为什么新的模型更不平滑了呢? 我们看一下输出结果的绘制图: 与非参数的注意力汇聚模型相比, 带参数的模型加入可学习的参数后, 曲线在注意力权重较大的区域变得更不平滑。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)