动手学深度学习11.6. 动量法-笔记&练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记,以及对课后练习的一些思考,自留回顾,也供同学之人交流参考。

本节课程地址:72 优化算法【动手学深度学习v2】_哔哩哔哩_bilibili

本节教材地址:11.6. 动量法 --- 动手学深度学习 2.0.0 documentation

本节开源代码:...>d2l-zh>pytorch>chapter_optimization>momentum.ipynb


动量法

11.4节 一节中,我们详述了如何执行随机梯度下降,即在只有嘈杂的梯度可用的情况下执行优化时会发生什么。 对于嘈杂的梯度,我们在选择学习率需要格外谨慎。 如果衰减速度太快,收敛就会停滞。 相反,如果太宽松,我们可能无法收敛到最优解。

基础

本节将探讨更有效的优化算法,尤其是针对实验中常见的某些类型的优化问题。

泄漏平均值

上一节中我们讨论了小批量随机梯度下降作为加速计算的手段。 它也有很好的副作用,即平均梯度减小了方差。 小批量随机梯度下降可以通过以下方式计算:

为了保持记法简单,在这里我们使用 作为样本 的随机梯度下降,使用时间 时更新的权重 。 如果我们能够从方差减少的影响中受益,甚至超过小批量上的梯度平均值,那很不错。 完成这项任务的一种选择是用泄漏平均值(leaky average)取代梯度计算:

其中 。 这有效地将瞬时梯度替换为多个"过去"梯度的平均值。 被称为动量 (momentum), 它累加了过去的梯度。 为了更详细地解释,让我们递归地将 扩展到

其中,较大的 相当于长期平均值,而较小的 相对于梯度法只是略有修正。 新的梯度替换不再指向特定实例下降最陡的方向,而是指向过去梯度的加权平均值的方向。 这使我们能够实现对单批量计算平均值的大部分好处,而不产生实际计算其梯度的代价。

上述推理构成了"加速"梯度方法的基础,例如具有动量的梯度。 在优化问题条件不佳的情况下(例如,有些方向的进展比其他方向慢得多,类似狭窄的峡谷),"加速"梯度还额外享受更有效的好处。 此外,它们允许我们对随后的梯度计算平均值,以获得更稳定的下降方向。 诚然,即使是对于无噪声凸问题,加速度这方面也是动量如此起效的关键原因之一。

正如人们所期望的,由于其功效,动量是深度学习及其后优化中一个深入研究的主题。 例如,请参阅文章,观看深入分析和互动动画。 动量是由(Polyak, 1964)提出的。 (Nesterov, 2018)在凸优化的背景下进行了详细的理论讨论。 长期以来,深度学习的动量一直被认为是有益的。 有关实例的详细信息,请参阅 (Sutskeveret al., 2013)的讨论。

条件不佳的问题

为了更好地了解动量法的几何属性,我们复习一下梯度下降,尽管它的目标函数明显不那么令人愉快。 回想我们在 11.3节 中使用了 ,即中度扭曲的椭球目标。 我们通过向 方向伸展它来进一步扭曲这个函数

与之前一样, 有最小值, 该函数在 的方向上非常平坦。 让我们看看在这个新函数上执行梯度下降时会发生什么。

%matplotlib inline
import torch
from d2l import torch as d2l

eta = 0.4
def f_2d(x1, x2):
    return 0.1 * x1 ** 2 + 2 * x2 ** 2
def gd_2d(x1, x2, s1, s2):
    return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)

d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))

输出结果:

epoch 20, x1: -0.943467, x2: -0.000073

从构造来看, 方向的梯度比水平 方向的梯度大得多,变化也快得多。 因此,我们陷入两难:如果选择较小的学习率,我们会确保解不会在 方向发散,但要承受在 方向的缓慢收敛。相反,如果学习率较高,我们在 方向上进展很快,但在 方向将会发散。 下面的例子说明了即使学习率从0.4略微提高到0.6,也会发生变化。 方向上的收敛有所改善,但整体来看解的质量更差了。

eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))

输出结果:

epoch 20, x1: -0.387814, x2: -1673.365109

动量法

动量法 (momentum)使我们能够解决上面描述的梯度下降问题。 观察上面的优化轨迹,我们可能会直觉到计算过去的平均梯度效果会很好。 毕竟,在 方向上,这将聚合非常对齐的梯度,从而增加我们在每一步中覆盖的距离。 相反,在梯度振荡的 方向,由于相互抵消了对方的振荡,聚合梯度将减小步长大小。 使用 而不是梯度 可以生成以下更新等式:

请注意,对于 ,我们恢复常规的梯度下降。 在深入研究它的数学属性之前,让我们快速看一下算法在实验中的表现如何。

def momentum_2d(x1, x2, v1, v2):
    v1 = beta * v1 + 0.2 * x1
    v2 = beta * v2 + 4 * x2
    return x1 - eta * v1, x2 - eta * v2, v1, v2

eta, beta = 0.6, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))

输出结果:

epoch 20, x1: 0.007188, x2: 0.002553

正如所见,尽管学习率与我们以前使用的相同,动量法仍然很好地收敛了。 让我们看看当降低动量参数时会发生什么。 将其减半至 会导致一条几乎没有收敛的轨迹。 尽管如此,它比没有动量时解将会发散要好得多。

eta, beta = 0.6, 0.25
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))

输出结果:

epoch 20, x1: -0.126340, x2: -0.186632

请注意,我们可以将动量法与随机梯度下降,特别是小批量随机梯度下降结合起来。 唯一的变化是,在这种情况下,我们将梯度 替换为 。 为了方便起见,我们在时间 初始化

有效样本权重

回想一下 。 极限条件下, 。 换句话说,不同于在梯度下降或者随机梯度下降中取步长 ,我们选取步长 ,同时处理潜在表现可能会更好的下降方向。 这是集两种好处于一身的做法。 为了说明 的不同选择的权重效果如何,请参考下面的图表。

# 不同 beta 值的指数衰减曲线
d2l.set_figsize()
betas = [0.95, 0.9, 0.6, 0]
for beta in betas:
    x = torch.arange(40).detach().numpy()
    d2l.plt.plot(x, beta ** x, label=f'beta = {beta:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend()

实际实验

让我们来看看动量法在实验中是如何运作的。 为此,我们需要一个更加可扩展的实现。

从零开始实现

相比于小批量随机梯度下降,动量方法需要维护一组辅助变量,即速度。 它与梯度以及优化问题的变量具有相同的形状。 在下面的实现中,我们称这些变量为states

def init_momentum_states(feature_dim):
    v_w = torch.zeros((feature_dim, 1))
    v_b = torch.zeros(1)
    return (v_w, v_b)
def sgd_momentum(params, states, hyperparams):
    for p, v in zip(params, states):
        with torch.no_grad():
            v[:] = hyperparams['momentum'] * v + p.grad
            p[:] -= hyperparams['lr'] * v
        p.grad.data.zero_()

让我们看看它在实验中是如何运作的。

def train_momentum(lr, momentum, num_epochs=2):
    d2l.train_ch11(sgd_momentum, init_momentum_states(feature_dim),
                   {'lr': lr, 'momentum': momentum}, data_iter,
                   feature_dim, num_epochs)

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
train_momentum(0.02, 0.5)

输出结果:

loss: 0.244, 0.009 sec/epoch

当我们将动量超参数momentum增加到0.9时,它相当于有效样本数量增加到 。 我们将学习率略微降至0.01,以确保可控。

train_momentum(0.01, 0.9)

输出结果:

loss: 0.248, 0.009 sec/epoch

降低学习率进一步解决了任何非平滑优化问题的困难,将其设置为0.005会产生良好的收敛性能。

train_momentum(0.005, 0.9)

输出结果:

loss: 0.245, 0.009 sec/epoch

简洁实现

由于深度学习框架中的优化求解器早已构建了动量法,设置匹配参数会产生非常类似的轨迹。

trainer = torch.optim.SGD
d2l.train_concise_ch11(trainer, {'lr': 0.005, 'momentum': 0.9}, data_iter)

输出结果:

loss: 0.246, 0.010 sec/epoch

理论分析

的2D示例似乎相当牵强。 下面我们将看到,它在实际生活中非常具有代表性,至少最小化凸二次目标函数的情况下是如此。

二次凸函数

考虑这个函数

这是一个普通的二次函数。 对于正定矩阵 ,即对于具有正特征值的矩阵,有最小化器为 ,最小值为 。 因此我们可以将 重写为

梯度由 给出。 也就是说,它是由 和最小化器之间的距离乘以 所得出的。 因此,动量法还是 的线性组合。

由于 是正定的,因此可以通过 分解为正交(旋转)矩阵 和正特征值的对角矩阵 。 这使我们能够将变量从 更改为 ,以获得一个非常简化的表达式:

这里 。 由于 只是一个正交矩阵,因此不会真正意义上扰动梯度。 以 表示的梯度下降变成

这个表达式中的重要事实是梯度下降在不同的特征空间之间不会混合。 也就是说,如果用 的特征系统来表示,优化问题是以逐坐标顺序的方式进行的。 这在动量法中也适用。

在这样做的过程中,我们只是证明了以下定理:带有和带有不凸二次函数动量的梯度下降,可以分解为朝二次矩阵特征向量方向坐标顺序的优化。

标量函数

鉴于上述结果,让我们看看当我们最小化函数 时会发生什么。 对于梯度下降我们有

时,这种优化以指数速度收敛,因为在 步之后我们可以得到 。 这显示了在我们将学习率 提高到 之前,收敛率最初是如何提高的。 超过该数值之后,梯度开始发散,对于 而言,优化问题将会发散。

lambdas = [0.1, 1, 10, 19]
eta = 0.1
d2l.set_figsize((6, 4))
for lam in lambdas:
    t = torch.arange(20).detach().numpy()
    d2l.plt.plot(t, (1 - eta * lam) ** t, label=f'lambda = {lam:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend()

为了分析动量的收敛情况,我们首先用两个标量重写更新方程:一个用于 ,另一个用于动量 。这产生了:

我们用 来表示 管理的收敛表现。 在 t 步之后,最初的值 变为 。 因此,收敛速度是由 R 的特征值决定的。 请参阅文章 (Goh, 2017)了解精彩动画。 请参阅 (Flammarion and Bach, 2015)了解详细分析。 简而言之,当 时动量收敛。 与梯度下降的 相比,这是更大范围的可行参数。 另外,一般而言较大值的 是可取的。

小结

  • 动量法用过去梯度的平均值来替换梯度,这大大加快了收敛速度。
  • 对于无噪声梯度下降和嘈杂随机梯度下降,动量法都是可取的。
  • 动量法可以防止在随机梯度下降的优化过程停滞的问题。
  • 由于对过去的数据进行了指数降权,有效梯度数为
  • 在凸二次问题中,可以对动量法进行明确而详细的分析。
  • 动量法的实现非常简单,但它需要我们存储额外的状态向量(动量 )。

练习

  1. 使用动量超参数和学习率的其他组合,观察和分析不同的实验结果。
    解:
    组合不同动量超参数和学习率后发现,高学习率和低动量参数、低学习率和高动量参数的组合,收敛速度快且loss低,分析原因如下:
  • 高学习率可以加快收敛,而低动量参数减少了由于噪声或震荡带来的不利影响。
  • 高动量参数可以平滑震荡,而低学习率可以防止由于步长过大导致的不利影响。

代码如下:

train_momentum(0.002, 0.9)

输出结果:
loss: 0.243, 0.009 sec/epoch

train_momentum(0.02, 0.2)

输出结果:
loss: 0.243, 0.009 sec/epoch

  1. 试试梯度下降和动量法来解决一个二次问题,其中有多个特征值,即 ,例如 。绘制出 的值在初始化 时如何下降。
    解:
    代码如下:

    定义目标函数

    def f(x, lambdas):
    return 0.5 * torch.sum(lambdas * x**2)

    动量法的更新规则

    def momentum(x, v, lambdas, eta, beta):
    with torch.no_grad():
    v = beta * v + lambdas * x
    x -= x - eta * v
    return x, v

    初始化参数

    feature_dim = 5
    x = torch.ones(feature_dim) # 初始化x
    v = torch.zeros(feature_dim) # 初始化动量v
    lambdas = torch.tensor([2**(-i) for i in range(feature_dim)]) # 特征值
    eta, beta, num_epochs = 0.1, 0.9, 10 # 学习率

    动量法

    x_values = [x.clone().detach().numpy()]
    for _ in range(num_epochs):
    x, v = momentum(x, v, lambdas, eta, beta)
    x_values.append(x.clone().detach().numpy())

    绘制结果

    d2l.set_figsize()
    for i in range(len(x_values[-1])):
    d2l.plt.plot([x_values[j][i] for j in range(num_epochs + 1)],
    label=f'x{i+1}={x_values[-1][i]:.3f}')
    d2l.plt.xlabel('epochs')
    d2l.plt.ylabel('x_values')
    d2l.plt.legend()

  1. 推导 的最小值和最小化器。
    解:
    关于 的梯度为:

    最小值为梯度为0的点,即令上式为0,可解得最小化器为:

    将最小化器代入 可得最小值:

    由于 ,且 是对称的,故 ,则上式可以简化为:

  2. 当我们执行带动量法的随机梯度下降时会有什么变化?当我们使用带动量法的小批量随机梯度下降时会发生什么?试验参数如何?
    解:
    相同lr下,对于sgd,带动量法后由于增加了计算量,计算用时略增加了一些,loss上相差无几。
    相同lr下,对于小批量sgd,带动量法后计算用时和loss都差别不大。
    代码如下:

    def sgd(params, states, hyperparams):
    for p in params:
    p.data.sub_(hyperparams['lr'] * p.grad)
    p.grad.data.zero_()

    def train_sgd(lr, batch_size, num_epochs=2):
    data_iter, feature_dim = d2l.get_data_ch11(batch_size)
    return d2l.train_ch11(
    sgd, None, {'lr': lr}, data_iter, feature_dim, num_epochs)

    sgd

    sgd = train_sgd(0.005, 1)

输出结果:
loss: 0.244, 0.055 sec/epoch

# minibatchsgd
minisgd = train_sgd(0.4, 100)

输出结果:
loss: 0.246, 0.002 sec/epoch

def train_momentum(lr, momentum, batch_size, num_epochs=2):
    data_iter, feature_dim = d2l.get_data_ch11(batch_size)
    return d2l.train_ch11(
        sgd_momentum, init_momentum_states(feature_dim), {'lr': lr, 'momentum': momentum}, data_iter, feature_dim, num_epochs)
# sgd with momentum
sgd_mom = train_momentum(0.005, 0.4, 1)

输出结果:
loss: 0.243, 0.068 sec/epoch

# minibatchsgd with momentum
minisgd_mom = train_momentum(0.4, 0.4, 100)

输出结果:
loss: 0.247, 0.002 sec/epoch

d2l.set_figsize([6, 3])
d2l.plot(*list(map(list, zip(sgd, minisgd, sgd_mom, minisgd_mom))),
         'time (sec)', 'loss', xlim=[1e-2, 1],
         legend=['sgd', 'minisgd', 'sgd with momentum', 'minisgd with momentum'])
d2l.plt.gca().set_xscale('log')
相关推荐
m0_748240542 分钟前
AutoSar架构学习笔记
笔记·学习·架构
坐吃山猪2 小时前
机器学习10-解读CNN代码Pytorch版
pytorch·机器学习·cnn
siy23332 小时前
[c语言日寄]结构体的使用及其拓展
c语言·开发语言·笔记·学习·算法
池央2 小时前
DCGAN - 深度卷积生成对抗网络:基于卷积神经网络的GAN
深度学习·生成对抗网络·cnn
雾里看山2 小时前
【MySQL】数据库基础知识
数据库·笔记·mysql·oracle
安和昂2 小时前
effective Objective—C 第三章笔记
java·c语言·笔记
ThisIsClark3 小时前
【gopher的java学习笔记】Java中Mapper与Entity的关系详解
java·笔记·学习
安冬的码畜日常3 小时前
【Vim Masterclass 笔记25】S10L45:Vim 多窗口的常用操作方法及相关注意事项
笔记·vim·自学笔记·vim多窗口·vim子窗口·vim水平分割·vim垂直分割
m0_548049703 小时前
SpringCloud学习笔记【尚硅谷2024版】
笔记·学习·spring cloud