动手学深度学习11.4. 随机梯度下降-笔记练习(PyTorch)
以下内容为结合李沐老师的课程和教材补充的学习笔记,以及对课后练习的一些思考,自留回顾,也供同学之人交流参考。
本节课程地址:72 优化算法【动手学深度学习v2】_哔哩哔哩_bilibili
本节教材地址:11.4. 随机梯度下降 — 动手学深度学习 2.0.0 documentation
本节开源代码:...>d2l-zh>pytorch>chapter_multilayer-perceptrons>sgd.ipynb
随机梯度下降
在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在11.3节 中描述了梯度下降的基本原则。本节继续更详细地说明随机梯度下降(stochastic gradient descent)。
%matplotlib inline
import math
import torch
from d2l import torch as d2l
随机梯度更新
在深度学习中,目标函数通常是训练数据集中每个样本的损失函数的平均值。给定 个样本的训练数据集,我们假设 是关于索引 的训练样本的损失函数,其中 是参数向量。然后我们得到目标函数
(11.4.1)
x 的目标函数的梯度计算为
(11.4.2)
如果使用梯度下降法,则每个自变量迭代的计算代价为 ,它随 线性增长。因此,当训练数据集较大时,每次迭代的梯度下降计算代价将较高。
随机梯度下降(SGD)可降低每次迭代时的计算代价。在随机梯度下降的每次迭代中,我们对数据样本随机均匀采样一个索引 ,其中 ,并计算梯度 以更新 :
(11.4.3)
其中 是学习率。我们可以看到,每次迭代的计算代价从梯度下降的 降至常数 。此外,我们要强调,随机梯度 是对完整梯度 的无偏估计,因为
(11.4.4)
这意味着,平均而言,随机梯度是对梯度的良好估计。
现在,我们将把它与梯度下降进行比较,方法是向梯度添加均值为0、方差为1的随机噪声,以模拟随机梯度下降。
def f(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2
def f_grad(x1, x2): # 目标函数的梯度
return 2 * x1, 4 * x2
def sgd(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
# 模拟有噪声的梯度
# 原代码运行作图会报错,这里g1和g2后要加.item()把张量变为标量,用于作图
g1 += torch.normal(0.0, 1, (1,)).item()
g2 += torch.normal(0.0, 1, (1,)).item()
eta_t = eta * lr()
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
def constant_lr():
return 1
eta = 0.1
lr = constant_lr # 常数学习速度
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50, f_grad=f_grad))
输出结果:
epoch 50, x1: -0.118161, x2: 0.083995
正如我们所看到的,随机梯度下降中变量的轨迹比我们在 11.3节 中观察到的梯度下降中观察到的轨迹嘈杂得多。这是由于梯度的随机性质。也就是说,即使我们接近最小值,我们仍然受到通过 的瞬间梯度所注入的不确定性的影响。即使经过50次迭代,质量仍然不那么好。更糟糕的是,经过额外的步骤,它不会得到改善。这给我们留下了唯一的选择:改变学习率 。但是,如果我们选择的学习率太小,我们一开始就不会取得任何有意义的进展。另一方面,如果我们选择的学习率太大,我们将无法获得一个好的解决方案,如上所示。解决这些相互冲突的目标的唯一方法是在优化过程中动态降低学习率。
这也是在sgd
步长函数中添加学习率函数lr
的原因。在上面的示例中,学习率调度的任何功能都处于休眠状态,因为我们将相关的lr
函数设置为常量。
动态学习率
用与时间相关的学习率 取代 增加了控制优化算法收敛的复杂性。特别是,我们需要弄清 的衰减速度。如果太快,我们将过早停止优化。如果减少的太慢,我们会在优化上浪费太多时间。以下是随着时间推移调整 时使用的一些基本策略(稍后我们将讨论更高级的策略):
(11.4.5)
在第一个分段常数(piecewise constant)场景中,我们会降低学习率,例如,每当优化进度停顿时。这是训练深度网络的常见策略。或者,我们可以通过指数衰减(exponential decay)来更积极地减低它。不幸的是,这往往会导致算法收敛之前过早停止。一个受欢迎的选择是 的多项式衰减(polynomial decay)。在凸优化的情况下,有许多证据表明这种速率表现良好。
让我们看看指数衰减在实践中是什么样子。
def exponential_lr():
# 在函数外部定义,而在内部更新的全局变量
global t
t += 1
return math.exp(-0.1 * t)
t = 1
lr = exponential_lr
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=1000, f_grad=f_grad))
输出结果:
epoch 1000, x1: -0.817639, x2: 0.006659
正如预期的那样,参数的方差大大减少。但是,这是以未能收敛到最优解 为代价的。即使经过1000个迭代步骤,我们仍然离最优解很远。事实上,该算法根本无法收敛。另一方面,如果我们使用多项式衰减,其中学习率随迭代次数的平方根倒数衰减,那么仅在50次迭代之后,收敛就会更好。
def polynomial_lr():
# 在函数外部定义,而在内部更新的全局变量
global t
t += 1
return (1 + 0.1 * t) ** (-0.5)
t = 1
lr = polynomial_lr
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50, f_grad=f_grad))
输出结果:
epoch 50, x1: -0.146179, x2: 0.011034
关于如何设置学习率,还有更多的选择。例如,我们可以从较小的学习率开始,然后使其迅速上涨,再让它降低,尽管这会更慢。我们甚至可以在较小和较大的学习率之间切换。现在,让我们专注于可以进行全面理论分析的学习率计划,即凸环境下的学习率。对一般的非凸问题,很难获得有意义的收敛保证,因为总的来说,最大限度地减少非线性非凸问题是NP(Nondeterministic polynominal,非确定性多项式)困难的。有关的研究调查,请参阅例如2015年Tibshirani的优秀讲义笔记。
凸目标的收敛性分析
以下对凸目标函数的随机梯度下降的收敛性分析是可选读的,主要用于传达对问题的更多直觉。我们只限于最简单的证明之一 (Nesterov and Vial, 2000)。存在着明显更先进的证明技术,例如,当目标函数表现特别好时。
假设所有 的目标函数 在 中都是凸的。更具体地说,我们考虑随机梯度下降更新:
(11.4.6)
其中 是训练样本 的目标函数: 从第 步的某个分布中提取, 是模型参数。用
(11.4.7)
表示期望风险, 表示对于 的最低风险。最后让 表示最小值(我们假设它存在于定义 x 的域中)。在这种情况下,我们可以跟踪时间 处的当前参数 和风险最小化器 之间的距离,看看它是否随着时间的推移而改善:
(11.4.8)
我们假设随机梯度 的 范数受到某个常数 的限制,因此我们有
(11.4.9)
我们最感兴趣的是 和 之间的距离如何变化的期望。事实上,对于任何具体的步骤序列,距离可能会增加,这取决于我们遇到的 。因此我们需要点积的边界。因为对于任何凸函数 ,所有 和 都满足 ,按凸性我们有
(11.4.10)
将不等式 (11.4.9) 和 (11.4.10) 代入 (11.4.8) 我们在时间 t+1 时获得参数之间距离的边界,如下所示:
(11.4.11)
这意味着,只要当前损失和最优损失之间的差异超过 ,我们就会取得进展。由于这种差异必然会收敛到零,因此学习率 也需要消失。
接下来,我们根据 (11.4.11) 取期望。得到
(11.4.12)
最后一步是对 的不等式求和。在求和过程中抵消中间项,然后舍去低阶项,可以得到
(11.4.13)
请注意,我们利用了给定的 ,因而可以去掉期望。最后定义
(11.4.14)
因为有
(11.4.15)
根据詹森不等式(令 (11.2.3) 中 , )和 的凸性使其满足的 ,因此,
(11.4.16)
将其代入不等式 (11.4.13) 得到边界
(11.4.17)
其中 是初始选择参数与最终结果之间距离的边界。简而言之,收敛速度取决于随机梯度标准的限制方式( )以及初始参数值与最优结果的距离( )。请注意,边界由 而不是 表示。因为 是优化路径的平滑版本。只要知道 和 ,我们就可以选择学习率 。这个就是上界 。也就是说,我们将按照速度 收敛到最优解。
学习率 与迭代次数 成反比,意味着 需要随着时间减少,以保证算法的收敛。如果学习率减少得太快,算法可能在达到最优解之前就停止了;如果减少得太慢,则会浪费太多时间在优化上。
随机梯度和有限样本
到目前为止,在谈论随机梯度下降时,我们进行得有点快而松散。我们假设从分布 中采样得到样本 (通常带有标签 ),并且用它来以某种方式更新模型参数。特别是,对于有限的样本数量,我们仅仅讨论了由某些允许我们在其上执行随机梯度下降的函数 和 组成的离散分布 。
但是,这不是我们真正做的。在本节的简单示例中,我们只是将噪声添加到其他非随机梯度上,也就是说,我们假装有成对的 。事实证明,这种做法在这里是合理的(有关详细讨论,请参阅练习)。更麻烦的是,在以前的所有讨论中,我们显然没有这样做。相反,我们遍历了所有实例恰好一次。要了解为什么这更可取,可以反向考虑一下,即我们有替换地从离散分布中采样 个观测值。随机选择一个元素 的概率是 。因此选择它至少一次就是
类似的推理表明,挑选一些样本(即训练示例)恰好一次的概率是
这导致与无替换采样相比,方差增加并且数据效率降低。因此,在实践中我们执行后者(这是本书中的默认选择)。最后一点注意,重复采用训练数据集的时候,会以不同的随机顺序遍历它。
小结
- 对于凸问题,我们可以证明,对于广泛的学习率选择,随机梯度下降将收敛到最优解。
- 对于深度学习而言,情况通常并非如此。但是,对凸问题的分析使我们能够深入了解如何进行优化,即逐步降低学习率,尽管不是太快。
- 如果学习率太小或太大,就会出现问题。实际上,通常只有经过多次实验后才能找到合适的学习率。
- 当训练数据集中有更多样本时,计算梯度下降的每次迭代的代价更高,因此在这些情况下,首选随机梯度下降。
- 随机梯度下降的最优性保证在非凸情况下一般不可用,因为需要检查的局部最小值的数量可能是指数级的。
练习
- 尝试不同的随机梯度下降学习率计划和不同的迭代次数进行实验。特别是,根据迭代次数的函数来绘制与最优解(0, 0)的距离。
解:
绘制不同的随机梯度下降学习率计划下,位置与最优解之间的距离关于迭代次数的函数,结果表明:
- 对于常数学习率,距离曲线下降到0附近的某个值波动,因为学习率保持不变,导致算法接近最优解但可能无法完全收敛到最优解。
- 对于指数衰减学习率,距离曲线快速下降后逐渐趋于平稳,因为学习率随迭代次数指数衰减,使得算法在开始时快速收敛,然后逐渐减慢,导致算法无法收敛到接近最优解。
- 对于多项式衰减学习率,距离曲线可以下降到最接近0,因为学习率随迭代次数的多项式衰减,使得算法在整个训练过程中保持稳定的收敛速度,逐渐收敛到最优解。
代码如下:
import numpy as np
def sgd_distance(sgd, steps):
x1, x2 = -5, -2
distances = [np.sqrt(x1**2 + x2**2)]
for i in range(steps):
g1, g2 = f_grad(x1, x2)
g1 += np.random.normal(0, 1)
g2 += np.random.normal(0, 1)
eta_t = eta * sgd()
x1 -= eta_t * g1
x2 -= eta_t * g2
distances.append(np.sqrt(x1**2 + x2**2))
print(f'epoch {i + 1}, x1: {float(x1):f}, x2: {float(x2):f}')
return distances
steps = 50
# 不同迭代次数,常数学习率下的与远点距离
distances_const = sgd_distance(constant_lr, steps)
# 不同迭代次数,指数衰减学习率下的与远点距离
t = 1
distances_exp = sgd_distance(exponential_lr, steps)
# 不同迭代次数,指多项式衰减学习率下的与远点距离
t = 1
distances_poly = sgd_distance(polynomial_lr, steps)
d2l.set_figsize()
d2l.plot(np.arange(0, steps + 1), [distances_const, distances_exp, distances_poly],
'Steps', 'Distances to (0,0)',
legend= ['Constant LR', 'Exponential LR', 'Polynomial LR'])
输出结果:
epoch 50, x1: -0.140426, x2: -0.065989
epoch 50, x1: -0.843649, x2: -0.038701
epoch 50, x1: -0.069057, x2: -0.002503
2. 证明对于函数 而言,向梯度添加正态噪声等同于最小化损失函数 ,其中 是从正态分布中提取的。
解:
函数 的梯度为:
在梯度中添加正态噪声,更新规则变为:
其中 和 是从正态分布 中抽取的随机噪声。
对于损失函数 ,其梯度为:
在梯度下降中,通过计算损失函数梯度的期望值来更新参数,如果 是从正态分布中提取的,那么样本 和 的期望值即为 和 。因此,梯度的期望值为:
因此,当最小化 时,实际上是在最小化 ,并且在梯度中添加了正态噪声。所以,向梯度添加正态噪声等同于最小化损失函数 ,其中 是从正态分布中提取的。这种等价性是由正态分布的性质和梯度下降的更新规则决定的。
3. 从 分别使用替换方法以及不替换方法进行采样时,比较随机梯度下降的收敛性。
解:
创建一个简单的数据集,函数为 ,其中 。初始 。
对于给定的样本 ,预测值为 。
采用最小化均方误差(MSE)损失函数 ,损失函数关于 和 的梯度为:
在此基础上比较替换方法和不替换方法采样的差别。
代码如下:
# 创建一个简单的数据集,函数为y=2x+1
numbers= 100
X = np.random.rand(numbers)
Y = 2 * X + 1 + np.random.randn(numbers) *0.1
def sgd_sampling(X, Y, sampling_method, sgd, steps):
# 令w和b的初始值为0
w, b = 0, 0
loss = []
for i in range(steps):
idx = np.random.randint(0, len(X))
x, y = X[idx], Y[idx]
# 不替换方法采样,不允许样本重复
if sampling_method == 'without_replacement':
X = np.delete(X, idx)
Y = np.delete(Y, idx)
y_hat = w * x + b
loss.append((y_hat-y)**2)
eta_t = eta * sgd()
w -= eta_t * 2*(y_hat-y)*x
b -= eta_t * 2*(y_hat-y)
print(f'epoch {i + 1}, w: {float(w):f}, b: {float(b):f}')
return loss
steps = 100
# 替换方法采样
t = 1
replacement_loss = sgd_sampling(X.copy(), Y.copy(), 'with_replacement', polynomial_lr, steps)
# 不替换方法采样
t = 1
without_replacement_loss = sgd_sampling(X.copy(), Y.copy(), 'without_replacement', polynomial_lr, steps)
d2l.set_figsize()
d2l.plot(np.arange(1, steps + 1), [replacement_loss, without_replacement_loss],
'Steps', 'loss',
legend= ['with_replacement', 'without_replacement'])
输出结果:
epoch 100, w: 1.486664, b: 1.322170
epoch 100, w: 1.487577, b: 1.257618
理论上而言,使用替换方法采样时,由于每个样本可以被多次选择,这会增加梯度估计的方差,从而可能降低数据效率;而不替换方法采样确保每个样本在每个epoch中只被使用一次,这有助于降低方差,提高数据效率。
但从上面的实验结果看,两个方法的收敛性差别不大(不替换方法的收敛速度略快一些),可能是因为样本量较小且数据复杂性低。
4. 如果某些梯度(或者更确切地说与之相关的某些坐标)始终比所有其他梯度都大,将如何更改随机梯度下降求解器?
解:
如果某些梯度(或者更确切地说与之相关的某些坐标)始终比所有其他梯度都大,可能会导致参数更新过快,从而使得算法难以收敛或者在最优解附近震荡。可以通过以下方式更改SGD:
- 使用较小的学习率来减缓参数的更新速度,这有助于避免因梯度过大而导致的震荡。或者可以使用学习率衰减策略,随着迭代次数的增加逐渐减小学习率。
- 对梯度进行裁剪,将梯度的大小限制在一个合理的范围内。这可以通过将梯度除以其大小(如果超过某个阈值)来实现。
- 通过标准化或归一化对特征进行缩放,使得所有特征的梯度大小相似。
5. 假设 。 有多少局部最小值?请试着改变 以尽量减少它需要评估所有局部最小值的方式。
解:
时,有局部最小值,可解得此时:
因此, 有无穷多个局部最小值,函数图像如下:
为了减少函数 的局部最小值数量,可以对函数进行修改,使其变得更加凸,方法如下:
- 在函数中添加一个正的二次项, ,将使函数更加凸,因为二次项会主导函数的行为,尤其是在 的绝对值较大时。函数图像如下:
- 调整正弦项的系数为小于1的正数,使其对函数的影响减小,比如 。函数图像如下: