小批量随机梯度下降
到目前为止,我们在基于梯度的学习方法中遇到了两个极端情况:使用完整数据集来计算梯度并更新参数、一次处理一个训练样本来取得进展。二者各有利弊:每当数据非常相似时,梯度下降并不是非常“数据高效”。而由于CPU和GPU无法充分利用向量化,随机梯度下降并不特别“计算高效”。这暗示了两者之间可能有折中方案,这便涉及到小批量随机梯度下降(minibatch gradient descent)
1 - 向量化和缓存
%matplotlib inline
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
timer = d2l.Timer()
A = torch.zeros(256,256)
B = torch.randn(256,256)
C = torch.randn(256,256)
按元素分配只需遍历分别为B和C的所有行和列,即可将该值分配给A
# 逐元素计算A=BC
timer.start()
for i in range(256):
for j in range(256):
A[i,j] = torch.dot(B[i,:],C[:,j])
timer.stop()
1.2868151664733887
更快的策略是执行按列分配
# 逐列计算A=BC
timer.start()
for j in range(256):
A[:,j] = torch.mv(B,C[:,j])
timer.stop()
0.027237653732299805
最有效的⽅法是在⼀个区块中执⾏整个操作。让我们看看它们各⾃的操作速度是多少
# ⼀次性计算A=BC
timer.start()
A = torch.mm(B, C)
timer.stop()
# 乘法和加法作为单独的操作(在实践中融合)
gigaflops = [2/i for i in timer.times]
print(f'performance in Gigaflops: element {gigaflops[0]:.3f}, '
f'column {gigaflops[1]:.3f}, full {gigaflops[2]:.3f}')
performance in Gigaflops: element 1.554, column 73.428, full 668.841
2 - 小批量
timer.start()
for j in range(0,256,64):
A[:,j:j+64] = torch.mm(B,C[:,j:j+64])
timer.stop()
print(f'performance in Gigaflops: block {2 / timer.times[3]:.3f}')
performance in Gigaflops: block 2021.839
显而易见,小批量上的计算基本上与完整矩阵一样有效,需要注意的是,在7.5节中,我们使⽤了⼀种在很⼤程度上取决于⼩批量中的⽅差的正则化。随着后者增加,⽅差会减少,随之⽽来的是批量规范化带来的噪声注⼊的好处。关于实例,请参阅 [Ioffe, 2017],了解有关如何重新缩放并计算适当项⽬
3 - 读取数据集
让我们来看看如何从数据中有效地生成小批量。下面我们使用NASA开发的测试机翼的数据集(不同飞行器产生的噪声)来比较这些优化算法。为方便起见,我们只使用前1500样本。数据已做预处理:我们移除了均值并将方差重新缩放到每个坐标为1
d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat','76e5be1548fd8222e5074cf0faae75edff8cf93f')
def get_data_ch11(batch_size=10,n=1500):
data = np.genfromtxt(d2l.download('airfoil'),dtype=np.float32,delimiter='\t')
data = torch.from_numpy((data - data.mean(axis = 0)) / data.std(axis=0))
data_iter = d2l.load_array((data[:n,:-1],data[:n,-1]),batch_size,is_train=True)
return data_iter,data.shape[1]-1
4 - 从零开始实现
在以前我们已经实现过小批量随机梯度下降算法,我们在这里将它的输入参数变得更加通用,主要是为了方便本章后面介绍的其他优化算法也可以使用同样的输入。具体来说,我们添加了一个状态输入states并将超参数放在字典hyperparams中。此外,我们将在训练函数里对各个小批量样本的损失求平均,因此优化算法中的梯度不需要除以批量大小
def sgd(params,states,hyperparams):
for p in params:
p.data.sub_(hyperparams['lr'] * p.grad)
p.grad.data.zero_()
下面实现一个通用的训练函数,以方便本章后面介绍的其他优化算法使用。它初始化了一个线性回归模型,然后可以使用小批量随机梯度下降以及后续小结介绍的其他算法来训练模型
def train_ch11(trainer_fn,states,hyperparams,data_iter,feature_dim,num_epochs=2):
# 初始化模型
w = torch.normal(mean=0.0,std=0.01,size=(feature_dim,1),requires_grad=True)
b = torch.zeros((1),requires_grad=True)
net,loss = lambda X : d2l.linreg(X,w,b),d2l.squared_loss
# 训练模型
animator = d2l.Animator(xlabel='epoch',ylabel='loss',xlim=[0,num_epochs],ylim=[0.22,0.35])
n,timer = 0,d2l.Timer()
for _ in range(num_epochs):
for X,y in data_iter:
l = loss(net(X),y).mean()
l.backward()
trainer_fn([w,b],states,hyperparams)
n += X.shape[0]
if n % 200 == 0:
timer.stop()
animator.add(n/X.shape[0]/len(data_iter),(d2l.evaluate_loss(net,data_iter,loss),))
timer.start()
print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch')
return timer.cumsum(), animator.Y[0]
让我们来看看批量梯度下降的优化是如何进行的。这可以通过将小批量设置为1500(即样本总数)来实现。因此,模型参数每个迭代轮数只迭代一次
def train_sgd(lr,batch_size,num_epochs=2):
data_iter,feature_dim = get_data_ch11(batch_size)
return train_ch11(sgd, None, {'lr': lr}, data_iter, feature_dim, num_epochs)
gd_res = train_sgd(1,1500,10)
loss: 0.244, 0.019 sec/epoch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXqTmR0W-1663162322861)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121715.svg)]
当批量⼤⼩为1时,优化使⽤的是随机梯度下降。为了简化实现,我们选择了很⼩的学习率。在随机梯度下降的实验中,每当⼀个样本被处理,模型参数都会更新。在这个例⼦中,这相当于每个迭代轮数有1500次更新。可以看到,⽬标函数值的下降在1个迭代轮数后就变得较为平缓。尽管两个例⼦在⼀个迭代轮数内都处理了1500个样本,但实验中随机梯度下降的⼀个迭代轮数耗时更多。这是因为随机梯度下降更频繁地更新了参数,⽽且⼀次处理单个观测值效率较低
sgd_res = train_sgd(0.005,1)
loss: 0.245, 0.038 sec/epoch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fxpGQHeL-1663162322862)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121716.svg)]
最后,当批量⼤⼩等于100时,我们使⽤⼩批量随机梯度下降进⾏优化。每个迭代轮数所需的时间⽐随机梯度下降和批量梯度下降所需的时间短
mini1_res = train_sgd(.4, 100)
loss: 0.247, 0.002 sec/epoch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wKGo3gYf-1663162322862)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121717.svg)]
将批量⼤⼩减少到10,每个迭代轮数的时间都会增加,因为每批⼯作负载的执⾏效率变得更低
mini2_res = train_sgd(.05, 10)
loss: 0.243, 0.010 sec/epoch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULCFS0i6-1663162322862)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121718.svg)]
现在我们可以⽐较前四个实验的时间与损失。可以看出,尽管在处理的样本数⽅⾯,随机梯度下降的收敛速度快于梯度下降,但与梯度下降相⽐,它需要更多的时间来达到同样的损失,因为逐个样本来计算梯度并不那么有效。⼩批量随机梯度下降能够平衡收敛速度和计算效率。⼤⼩为10的⼩批量⽐随机梯度下降有效;⼤⼩为100的⼩批量在运⾏时间上甚⾄优于梯度下降
d2l.set_figsize([6, 3])
d2l.plot(*list(map(list, zip(gd_res, sgd_res, mini1_res, mini2_res))),
'time (sec)', 'loss', xlim=[1e-2, 10],
legend=['gd', 'sgd', 'batch size=100', 'batch size=10'])
d2l.plt.gca().set_xscale('log')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJrOIJRD-1663162322862)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121719.svg)]
5 - 简洁实现
下面用深度学习框架自带算法实现一个通用的训练函数,我们将在本章中其他小节使用它
def train_concise_ch11(trainer_fn,hyperparams,data_iter,num_epochs=4):
# 初始化模型
net = nn.Sequential(nn.Linear(5,1))
def init_weights(m):
if type(m) == nn.Linear:
torch.nn.init.normal_(m.weight,std=0.01)
net.apply(init_weights)
optimizer = trainer_fn(net.parameters(),**hyperparams)
loss = nn.MSELoss(reduction='none')
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[0, num_epochs], ylim=[0.22, 0.35])
n,timer = 0,d2l.Timer()
for _ in range(num_epochs):
for X,y in data_iter:
optimizer.zero_grad()
out = net(X)
y = y.reshape(out.shape)
l = loss(out,y)
l.mean().backward()
optimizer.step()
n += X.shape[0]
if n % 200 == 0:
timer.stop()
# MSELoss计算平方误差时不带系数1/2
animator.add(n/X.shape[0]/len(data_iter),(d2l.evaluate_loss(net, data_iter, loss) / 2,))
timer.start()
print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch')
下面使用这个训练函数,复原之前的实验
data_iter,_ = get_data_ch11(10)
trainer = torch.optim.SGD
train_concise_ch11(trainer,{'lr':0.01},data_iter)
loss: 0.243, 0.008 sec/epoch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sj6I4xL5-1663162322863)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121721.svg)]
6 - 小结
- 由于减少了深度学习框架的额外开销,使用更好的内存定位以及CPU和GPU上的缓存,向量化使代码更加高效
- 随机梯度下降的“统计效率”与大批量一次处理数据的“计算效率”之间存在权衡。小批量随机梯度下降提供了两全其美的答案:计算和统计效率
- 在小批量随机梯度下降中,我们处理通过训练数据的随机排列获得的批量数据(即每个观测值只处理一次,但按随机顺序)
- 在训练期间降低学习率有助于训练
- 一般来说,小批量随机梯度下降比随机梯度下降和梯度下降的速度快,收敛风险较小