持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
本系列更多文章见专栏:草履虫都能看懂的白话解析《动手学深度学习》
PS:书我还没看完,专栏还在更新中。
import torch
from torch import nn
from d2l import torch as d2l
最基础的导包,看不懂的python需要回炉重造,不建议继续往下看文章。
n_train = 50 # 训练样本数
x_train, _ = torch.sort(torch.rand(n_train) * 5) # 训练样本的输入
def f(x):
return 2 * torch.sin(x) + x**0.8
y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,)) # 训练样本的输出
x_test = torch.arange(0, 5, 0.1) # 测试样本
y_truth = f(x_test) # 测试样本的真实输出
n_test = len(x_test) # 测试样本数
生成随机数据作为数据集。 设置要生成的训练样本数量为50个。
-
torch.rand(n_train) * 5:生成之间的50个数据x。 -
使用
torch.sort对其进行排序。这里使用x_train和一个下划线_来接受torch.sort的返回值,因为该函数会返回排序后的数据以及排序之前的下标。 -
f(x)定义映射函数,即 -
y_train是使用f(x)生成训练数据的结果y。
生成测试集。
- 使用
torch.arange生成之间的数,步长为0.1 f(x)生成测试集的真实结果n_test存储测试集数据的数量,结果也是50个。
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);
用于画图的一个函数,不用深究什么意思。
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))
attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1)
plot_kernel_reg(y_hat)
这里是使用非参的nadaraya-waston核回归计算attention权重,即这个公式可以根据x的位置对y进行加权。
-
x_test.repeat_interleave()是将测试数据x_test重复n_train次即50次,再使用.reshape((-1, n_train))将其形状重置成50行50列。操作之后X_repeat是一个矩阵,其中一行50个数都是一样的,每列都是x_test>>tensor([[0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000], [0.1000, 0.1000, 0.1000, ..., 0.1000, 0.1000, 0.1000], [0.2000, 0.2000, 0.2000, ..., 0.2000, 0.2000, 0.2000], ..., [4.7000, 4.7000, 4.7000, ..., 4.7000, 4.7000, 4.7000], [4.8000, 4.8000, 4.8000, ..., 4.8000, 4.8000, 4.8000], [4.9000, 4.9000, 4.9000, ..., 4.9000, 4.9000, 4.9000]]) -
attention_weight那里就是将数据丢进一个softmax层里,要计算的核就是之前选定的高斯核。使用广播机制进行计算,在这里:x_train相当于keyX_repeat相当于queryy_train相当于value
-
将计算出来的
attention_weight和y_train进行计算,即计算 -
最终使用
plot_kernel_reg将结果画出来。蓝色的线是真实数据,紫色虚线是我们模型预测的结果,可以看出:在数据量小的时候使用不带参数的核回归,虽然已经能看出大致曲线了但是效果还是差很多。
原书代码有两段可视化代码,这里只讲一段。另一端是heat map,表示注意力效果的。
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 = 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)
return torch.bmm(self.attention_weights.unsqueeze(1),values.unsqueeze(-1)).reshape(-1)
这里是带参的nadaraya-waston核回归,用的是了。
forward定义前向传播,需要传入Q、K、V。-
queries这里计算方法和非参的计算方法一样,将其重复复制成一个矩阵,为后边的softmax计算做准备。 -
self.attention_weights这里是,高斯核加上了参数,w的作用相当于控制高斯核的大小,可以想象成CNN中控制卷积核的大小。 -
最后是用bmm计算乘法。这里涉及到一个mini-batch的矩阵乘法。
补充bmm矩阵计算:使用bmm计算mini-batch矩阵乘法,需要三个参数,第一个参数是批量的数量,剩下两个参数是矩阵的维度。比如:
X = torch.ones((2, 1, 4)) Y = torch.ones((2, 4, 6)) torch.bmm(X, Y).shape输出结果是
torch.Size([2, 1, 6])- x是两个1*4的矩阵,y是两个4*的矩阵,使用bmm相乘之后打印一下结果的维度,是两个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))输出结果为:
tensor([[[ 4.5000]], [[14.5000]]])- 使用小批量矩阵乘法来计算小批量数据中的加权平均值
- unsqueeze是给张量添加维度的,具体可以看→torch.squeeze 和 torch.unsqueeze
- 这一步是假设w和v相乘,维度变化之后是2*1*10的矩阵和2*10*1的矩阵相乘,结果是2*1*1的矩阵,即两个batch。
-
X_tile = x_train.repeat((n_train, 1))
Y_tile = y_train.repeat((n_train, 1))
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
将训练集数据转换成key和value。
-
X_tile是将训练集的数据x_train平铺展开,每一行都包含着相同的训练输入。 -
Y_tile是将训练集的数据y_train平铺展开,每一行都包含着相同的训练输出。 -
torch.eye生成一个维度为n_train的单位矩阵。因为单位矩阵是对角线元素为1其余元素都为0 ,之后使用1 - torch.eye(n_train)将其转化为对角线元素为0其余元素都为1的矩阵,在进行类型转换,type(torch.bool),使其转换为对角元素为false其余元素都为true的矩阵。key和value的这一步是将对角线上的数据mask掉。再将其转化为50行的矩阵,现在就变为50行49列的矩阵了(原来是50行50列)。任何一个训练样本的输入都会和除自己以外的所有训练样本进行计算,从而得到其对应的预测输出
画个单间的图你们理解一下:
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()
# 注意:L2 Loss = 1/2 * MSE Loss。
# PyTorch 的 MSE Loss 与 MXNet 的 L2Loss 差一个 2 的因子,因此被除2。
l = loss(net(x_train, keys, values), y_train) / 2
l.sum().backward()
trainer.step()
print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
animator.add(epoch + 1, float(l.sum()))
训练过程:
- 使用我们的带参数的计算方法
- loss使用MSE loss
- 优化器SGD
d2l.Animator一个梯度下降过程的可视化,不用深究- 之后就是训练5个epoch
keys = x_train.repeat((n_test, 1))
values = y_train.repeat((n_test, 1))
y_hat = net(x_test, keys, values).unsqueeze(1).detach()
plot_kernel_reg(y_hat)
训练之后再将结果进行可视化。拟合效果比不带参数的变好了,但是在注意力较大的区域参数变得不平滑。侧面也展示了注意力权重的影响。