动手学深度学习 7.3 小批量随机梯度下降

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)


在每一次迭代中,梯度下降使用整个训练数据集来计算梯度,因此它有时也被称为批量梯度下降(batch gradient descent)。

随机梯度下降每次迭代中只随机采样一个样本来计算梯度

正如我们在前几章中所看到的,我们还可以在每轮迭代中随机均匀采样多个样本来组成一个小批量,然后使用这个小批量来计算梯度。下面就来描述小批量随机梯度下降

设目标函数 \(f(x)\) 。在迭代开始前的时间步设为0。该时间步的自变量记为 \(x_0\) ,通常由随机初始化得到。在接下来的每一个时间步 \(t>0\) 中,小批量随机梯度下降随机均匀采样一个由训练数据样本索引组成的小批量 \(B_t\)

我们可以通过重复采样(sampling with replacement)或者不重复采样(sampling without replacement)得到一个小批量中的各个样本。前者允许同一个小批量中出现重复的样本,后者则不允许如此,且更常见。

对于这两者间的任一种方式,都可以使用以下公式(每一个梯度加和然后求平均)

来计算时间步 \(t\) 的小批量 \(B_t\) 上目标函数位于 \(x_{t-1}\) 处的梯度 \(g_t\) 。这里 \(∣B∣\) 代表批量大小,即小批量中样本的个数,是一个超参数。同随机梯度一样,重复采样所得的小批量随机梯度 \(g_t\) 也是对梯度 \(\nabla f(x_{t-1})\) 的无偏估计。给定学习率 \({\eta}_t\) ,小批量随机梯度下降对自变量的迭代如下: \[ x_t \leftarrow x_{t-1} - \eta_tg_t \] 基于随机采样得到的梯度的方差在迭代过程中无法减小,因此在实际中,(小批量)随机梯度下降的学习率可以在迭代过程中自我衰减,例如 \(\eta_t=\eta * t^\alpha\) (同从 \(\alpha=-1或者-0.5\) )、\(\eta_t=\eta * \alpha^t\)(如α=0.95)或者每迭代若干次后将学习率衰减一次。

如此一来,学习率和(小批量)随机梯度乘积的方差会减小( \(\eta_tg_t\) 的方差会减小 )。而梯度下降在迭代过程中一直使用目标函数的真实梯度,无须自我衰减学习率

以之前章节的图说明,梯度下载一直使用全部样本的梯度,因此在符合泰勒展开的条件下,无需衰减学习率。

小批量随机梯度下降中每次迭代的计算开销为 \(O(∣B∣)\)

批量大小为1时,该算法即为随机梯度下降

批量大小等于训练数据样本数时,该算法即为梯度下降

当批量较小时,每次迭代中使用的样本少,这会导致并行处理和内存使用效率变低。这使得在计算同样数目样本的情况下比使用更大批量时所花时间更多。

当批量较大时,每个小批量梯度里可能含有更多的冗余信息。为了得到较好的解,批量较大时比批量较小时需要计算的样本数目可能更多,例如增大迭代周期数。

1. 读取数据

本章里我们将使用一个来自NASA的测试不同飞机机翼噪音的数据集来比较各个优化算法 。我们使用该数据集的前1500个样本和5个特征,并使用标准化对数据进行预处理。

1
2
3
4
5
6
7
8
9
10
11
def get_data_ch7():  # 本函数已保存在d2lzh_pytorch包中方便以后使用
data = np.genfromtxt('./data/airfoil_self_noise.dat', delimiter='\t')
data = (data - data.mean(axis=0)) / data.std(axis=0)

return (
torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32)
) # 前1500个样本(每个样本5个特征)

features, labels = get_data_ch7()
print(features.shape) # torch.Size([1500, 5])

2. 从零开始实现

3.2节(线性回归的从零开始实现)中已经实现过小批量随机梯度下降算法。

我们在这里将它的输入参数变得更加通用,主要是为了方便本章后面介绍的其他优化算法也可以使用同样的输入。具体来说,我们添加了一个状态输入states并将超参数放在字典hyperparams里。此外,我们将在训练函数里对各个小批量样本的损失求平均,因此优化算法里的梯度不需要除以批量大小。

1
2
3
def sgd(params, states, hyperparams):
for p in params:
p.data -= hyperparams['lr'] * p.grad.data

下面实现一个通用的训练函数,以方便本章后面介绍的其他优化算法使用。它初始化一个线性回归模型,然后可以使用小批量随机梯度下降以及后续小节介绍的其他算法来训练模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def train_ch7(optimizer_fn, states, hyperparams, features, labels,
batch_size=10, num_epochs=2):
# 初始化模型
net, loss = d2l.linreg, d2l.squared_loss

w = torch.nn.Parameter(torch.tensor(np.random.normal(0, 0.01, size=(features.shape[1], 1)), dtype=torch.float32),
requires_grad=True)
b = torch.nn.Parameter(torch.zeros(1, dtype=torch.float32), requires_grad=True)

def eval_loss():
return loss(net(features, w, b), labels).mean().item()

ls = [eval_loss()]
data_iter = torch.utils.data.DataLoader(
torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)

for _ in range(num_epochs):
start = time.time()
for batch_i, (X, y) in enumerate(data_iter):
l = loss(net(X, w, b), y).mean() # 使用平均损失

# 梯度清零
if w.grad is not None:
w.grad.data.zero_()
b.grad.data.zero_()

l.backward()
optimizer_fn([w, b], states, hyperparams) # 迭代模型参数
if (batch_i + 1) * batch_size % 100 == 0:
ls.append(eval_loss()) # 每100个样本记录下当前训练误差
# 打印结果和作图
print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
d2l.set_figsize()
d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
d2l.plt.xlabel('epoch')
d2l.plt.ylabel('loss')
d2l.plt.show()

def train_sgd(lr, batch_size, num_epochs=2):
train_ch7(sgd, None, {'lr': lr}, features, labels, batch_size, num_epochs)

train_sgd(1, 1500, 6)

当批量大小为样本总数1500时,优化使用的是梯度下降。梯度下降的1个迭代周期对模型参数只迭代1次。可以看到6次迭代后目标函数值(训练损失)的下降趋向了平稳。

当批量大小为1时,优化使用的是随机梯度下降。为了简化实现,有关(小批量)随机梯度下降的实验中,我们未对学习率进行自我衰减,而是直接采用较小的常数学习率。随机梯度下降中,每处理一个样本会更新一次自变量(模型参数),一个迭代周期里会对自变量进行1500次更新。可以看到,目标函数值的下降在1个迭代周期后就变得较为平缓。

1
train_sgd(0.005, 1)

虽然随机梯度下降和梯度下降在一个迭代周期里都处理了1500个样本,但实验中随机梯度下降的一个迭代周期耗时更多。这是因为随机梯度下降在一个迭代周期里做了更多次的自变量迭代,而且单样本的梯度计算难以有效利用矢量计算。

批量大小为10时,优化使用的是小批量随机梯度下降。它在每个迭代周期的耗时介于梯度下降和随机梯度下降的耗时之间

1
train_sgd(0.05, 10)

3. 简洁实现

在PyTorch里可以通过创建optimizer实例来调用优化算法。这能让实现更简洁。下面实现一个通用的训练函数,它通过优化算法的函数optimizer_fn和超参数optimizer_hyperparams来创建optimizer实例。

loss计算会自动求均值,因此不用手动像之前在 \(l\) 中平均。

使用PyTorch重复上一个实验。

1
train_pytorch_ch7(optim.SGD, {"lr": 0.05}, features, labels, 10)