优化算法
优化和深度学习
本节将讨论优化与深度学习之间的关系以及在深度学习中使用优化的挑战。对于深度学习问题,我们通常会先定义损失函数。一旦我们有了损失函数,我们就可以使用优化算法来尝试最小化损失。在优化中,损失函数通常被称为优化问题的目标函数。按照传统惯例,大多数优化算法都关注的是最小化。如果我们需要最大化目标,那么有一个简单的解决方案:在目标函数前加负号即可。
优化的目标
尽管优化提供了一种最大限度地减少深度学习损失函数的方法,但本质上,优化和深度学习的目标是根本不同的。前者主要关注的是最小化目标,后者则关注在给定有限数据量的情况下寻找合适的模型。在 :numref:sec_model_selection中,我们详细讨论了这两个目标之间的区别。例如,训练误差和泛化误差通常不同:由于优化算法的目标函数通常是基于训练数据集的损失函数,因此优化的目标是减少训练误差。但是,深度学习(或更广义地说,统计推断)的目标是减少泛化误差。为了实现后者,除了使用优化算法来减少训练误差之外,我们还需要注意过拟合。
1 | %matplotlib inline |
为了说明上述不同的目标,引入两个概念风险和经验风险。如 :numref:subsec_empirical-risk-and-risk所述,经验风险是训练数据集的平均损失,而风险则是整个数据群的预期损失。下面我们定义了两个函数:风险函数f和经验风险函数g。假设我们只有有限的训练数据。因此,这里的g不如f平滑。
1 | def f(x): |
下图说明,训练数据集的最低经验风险可能与最低风险(泛化误差)不同。
1 | def annotate(text, xy, xytext): #@save |

深度学习中的优化挑战
本章将关注优化算法在最小化目标函数方面的性能,而不是模型的泛化误差。在 :numref:sec_linear_regression中,我们区分了优化问题中的解析解和数值解。在深度学习中,大多数目标函数都很复杂,没有解析解。相反,我们必须使用数值优化算法。本章中的优化算法都属于此类别。
深度学习优化存在许多挑战。其中最令人烦恼的是局部最小值、鞍点和梯度消失。
局部最小值
对于任何目标函数
例如,给定函数
我们可以近似该函数的局部最小值和全局最小值。
1 | x = torch.arange(-1.0, 2.0, 0.01) |

深度学习模型的目标函数通常有许多局部最优解。当优化问题的数值解接近局部最优值时,随着目标函数解的梯度接近或变为零,通过最终迭代获得的数值解可能仅使目标函数*局部最优,而不是*全局最优。只有一定程度的噪声可能会使参数跳出局部最小值。事实上,这是小批量随机梯度下降的有利特性之一**。在这种情况下,小批量上梯度的自然变化能够将参数从局部极小值中跳出。
鞍点
除了局部最小值之外,鞍点是梯度消失的另一个原因。鞍点(saddle point)是指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。考虑这个函数
1 | x = torch.arange(-2.0, 2.0, 0.01) |

如下例所示,较高维度的鞍点甚至更加隐蔽。考虑这个函数
1 | x, y = torch.meshgrid( |

我们假设函数的输入是
- 当函数在零梯度位置处的Hessian矩阵的特征值全部为正值时,我们有该函数的局部最小值;
- 当函数在零梯度位置处的Hessian矩阵的特征值全部为负值时,我们有该函数的局部最大值;
- 当函数在零梯度位置处的Hessian矩阵的特征值为负值和正值时,我们有该函数的一个鞍点。
对于高维度问题,至少部分特征值为负的可能性相当高。这使得鞍点比局部最小值更有可能出现。我们将在下一节介绍凸性时讨论这种情况的一些例外。简而言之,凸函数是Hessian函数的特征值永远不为负值的函数。不幸的是,大多数深度学习问题并不属于这一类。尽管如此,它还是研究优化算法的一个很好的工具。
梯度消失
可能遇到的最隐蔽问题是梯度消失。回想一下我们在 :numref:subsec_activation_functions中常用的激活函数及其衍生函数。例如,假设我们想最小化函数
1 | x = torch.arange(-2.0, 5.0, 0.01) |

正如我们所看到的那样,深度学习的优化充满挑战。幸运的是,有一系列强大的算法表现良好,即使对于初学者也很容易使用。此外,没有必要找到最优解。局部最优解或其近似解仍然非常有用。
小结
- 最小化训练误差并不能保证我们找到最佳的参数集来最小化泛化误差。
- 优化问题可能有许多局部最小值。
- 一个问题可能有很多的鞍点,因为问题通常不是凸的。
- 梯度消失可能会导致优化停滞,重参数化通常会有所帮助。对参数进行良好的初始化也可能是有益的。
练习
- 考虑一个简单的MLP,它有一个隐藏层,比如,隐藏层中维度为
和一个输出。证明对于任何局部最小值,至少有 个等效方案。 - 假设我们有一个对称随机矩阵
,其中条目 各自从某种概率分布 中抽取。此外,假设 ,即分布是对称的(详情请参见 :cite: Wigner.1958)。- 证明特征值的分布也是对称的。也就是说,对于任何特征向量
,关联的特征值 满足 的概率为 。 - 为什么以上没有暗示
?
- 证明特征值的分布也是对称的。也就是说,对于任何特征向量
- 你能想到深度学习优化还涉及哪些其他挑战?
- 假设你想在(真实的)鞍上平衡一个(真实的)球。
- 为什么这很难?
- 能利用这种效应来优化算法吗?
凸性
:label:sec_convexity
凸性(convexity)在优化算法的设计中起到至关重要的作用,
这主要是由于在这种情况下对算法进行分析和测试要容易。
换言之,如果算法在凸性条件设定下的效果很差,
那通常我们很难在其他条件下看到好的结果。
此外,即使深度学习中的优化问题通常是非凸的,
它们也经常在局部极小值附近表现出一些凸性。
这可能会产生一些像 :cite:Izmailov.Podoprikhin.Garipov.ea.2018这样比较有意思的新优化变体。
1 | %matplotlib inline |
定义
在进行凸分析之前,我们需要定义凸集(convex sets)和凸函数(convex functions)。
凸集
凸集(convex set)是凸性的基础。
简单地说,如果对于任何
在数学术语上,这意味着对于所有
这听起来有点抽象,那我们来看一下 :numref:fig_pacman里的例子。
第一组存在不包含在集合内部的线段,所以该集合是非凸的,而另外两组则没有这样的问题。
:label:fig_pacman
接下来来看一下交集 :numref:fig_convex_intersect。
假设
现在考虑任意
因为
所以连接
鉴于此,它们也需要包含在
:label:fig_convex_intersect
我们可以毫不费力地进一步得到这样的结果:
给定凸集$\mathcal{X}i
在 :numref:fig_nonconvex中连接
因此线段也不在
:label:fig_nonconvex
通常,深度学习中的问题是在凸集上定义的。
例如,
在某些情况下,我们使用有界长度的变量,例如球的半径定义为
凸函数
现在我们有了凸集,我们可以引入凸函数(convex function)
给定一个凸集
为了说明这一点,让我们绘制一些函数并检查哪些函数满足要求。
下面我们定义一些函数,包括凸函数和非凸函数。
1 | f = lambda x: 0.5 * x**2 # 凸函数 |

不出所料,余弦函数为非凸的,而抛物线函数和指数函数为凸的。
请注意,为使该条件有意义,
否则可能无法很好地界定
詹森不等式
给定一个凸函数
它是凸性定义的一种推广:
:eqlabel:eq_jensens-inequality
其中
换句话说,凸函数的期望不小于期望的凸函数,其中后者通常是一个更简单的表达式。
为了证明第一个不等式,我们多次将凸性的定义应用于一次求和中的一项。
詹森不等式的一个常见应用:用一个较简单的表达式约束一个较复杂的表达式。
例如,它可以应用于部分观察到的随机变量的对数似然。
具体地说,由于
这里,
例如,在聚类中
性质
下面我们来看一下凸函数一些有趣的性质。
局部极小值是全局极小值
首先凸函数的局部极小值也是全局极小值。
下面我们用反证法给出证明。
假设
现在假设局部极小值
则存在
然而,根据凸性的性质,有
这与
因此,不存在
综上所述,局部最小值
例如,对于凸函数
1 | f = lambda x: (x - 1) ** 2 |

凸函数的局部极小值同时也是全局极小值这一性质是很方便的。
这意味着如果我们最小化函数,我们就不会“卡住”。
但是请注意,这并不意味着不能有多个全局最小值,或者可能不存在一个全局最小值。
例如,函数
相反,函数
凸函数的下水平集是凸的
我们可以方便地通过凸函数的下水平集(below sets)定义凸集。
具体来说,给定一个定义在凸集
是凸的。
让我们快速证明一下。
对于任何
因为
凸性和二阶导数
当一个函数的二阶导数
我们需要做的就是检查
即对于所有
例如,函数
更正式地讲,
对于任意二次可微多维函数
它是凸的当且仅当它的Hessian
首先,我们来证明一下一维情况。
为了证明凸函数的
因为二阶导数是由有限差分的极限给出的,所以遵循
为了证明
我们使用这样一个事实:
假设
其中,
根据中值定理,存在
通过单调性
由于
从而证明了凸性。
第二,我们需要一个引理证明多维情况:
是凸的当且仅当对于所有
是凸的。
为了证明
对于所有的我们可以证明,
为了证明这一点,我们可以证明对
最后,利用上面的引理和一维情况的结果,我们可以证明多维情况:
多维函数
根据一维情况,
此条成立的条件为,当且仅当对于所有
这相当于根据半正定矩阵的定义,
约束
凸优化的一个很好的特性是能够让我们有效地处理约束(constraints)。
即它使我们能够解决以下形式的约束优化(constrained optimization)问题:
这里
例如第一个约束
如果第二个约束
同时满足这两个约束等于选择一个球的切片作为约束集。
拉格朗日函数
通常,求解一个有约束的优化问题是困难的,解决这个问题的一种方法来自物理中相当简单的直觉。
想象一个球在一个盒子里,球会滚到最低的地方,重力将与盒子两侧对球施加的力平衡。
简而言之,目标函数(即重力)的梯度将被约束函数的梯度所抵消(由于墙壁的“推回”作用,需要保持在盒子内)。
请注意,任何不起作用的约束(即球不接触壁)都将无法对球施加任何力。
这里我们简略拉格朗日函数
这里的变量
选择它们的大小足以确保所有
例如,对于
此外,这是一个鞍点(saddlepoint)优化问题。
在这个问题中,我们想要使
有大量的文献解释如何得出函数
我们这里只需要知道
惩罚
一种至少近似地满足约束优化问题的方法是采用拉格朗日函数
这样可以确保不会严重违反约束。
事实上,我们一直在使用这个技巧。
比如权重衰减 :numref:sec_weight_decay,在目标函数中加入
使用约束优化的观点,我们可以看到,对于若干半径
通过调整
通常,添加惩罚是确保近似满足约束的一种好方法。
在实践中,这被证明比精确的满意度更可靠。
此外,对于非凸问题,许多使精确方法在凸情况下的性质(例如,可求最优解)不再成立。
投影
满足约束条件的另一种策略是投影(projections)。
同样,我们之前也遇到过,例如在 :numref:sec_rnn_scratch中处理梯度截断时,我们通过
确保梯度的长度以
这就是
更泛化地说,在凸集
$$\mathrm{Proj}\mathcal{X}(\mathbf{x}) = \mathop{\mathrm{argmin}}{\mathbf{x}’ \in \mathcal{X}} |\mathbf{x} - \mathbf{x}’|.$$
它是
:label:fig_projections
投影的数学定义听起来可能有点抽象,为了解释得更清楚一些,请看 :numref:fig_projections。
图中有两个凸集,一个圆和一个菱形。
两个集合内的点(黄色)在投影期间保持不变。
两个集合(黑色)之外的点投影到集合中接近原始点(黑色)的点(红色)。
虽然对
凸投影的一个用途是计算稀疏权重向量。
在本例中,我们将权重向量投影到一个
这是 :numref:fig_projections中菱形例子的一个广义版本。
小结
在深度学习的背景下,凸函数的主要目的是帮助我们详细了解优化算法。
我们由此得出梯度下降法和随机梯度下降法是如何相应推导出来的。
- 凸集的交点是凸的,并集不是。
- 根据詹森不等式,“一个多变量凸函数的总期望值”大于或等于“用每个变量的期望值计算这个函数的总值“。
- 一个二次可微函数是凸函数,当且仅当其Hessian(二阶导数矩阵)是半正定的。
- 凸约束可以通过拉格朗日函数来添加。在实践中,只需在目标函数中加上一个惩罚就可以了。
- 投影映射到凸集中最接近原始点的点。
练习
假设我们想要通过绘制集合内点之间的所有直线并检查这些直线是否包含来验证集合的凸性。i.证明只检查边界上的点是充分的。ii.证明只检查集合的顶点是充分的。
用
-范数表示半径为 的球,证明 , 对于所有 是凸的。 已知凸函数
和 表明 也是凸函数。证明 是非凸的。 证明Softmax函数的规范化是凸的,即
的凸性。 证明线性子空间
是凸集。 证明在线性子空间
的情况下,对于矩阵 的投影 可以写成 。 证明对于凸二次可微函数
,对于 ,我们可以写成 。 给定一个凸集
和两个向量 和 证明了投影不会增加距离,即 。
梯度下降
:label:sec_gd
尽管梯度下降(gradient descent)很少直接用于深度学习,
但了解它是理解下一节随机梯度下降算法的关键。
例如,由于学习率过大,优化问题可能会发散,这种现象早已在梯度下降中出现。
同样地,预处理(preconditioning)是梯度下降中的一种常用技术,
还被沿用到更高级的算法中。
让我们从简单的一维梯度下降开始。
一维梯度下降
为什么梯度下降算法可以优化目标函数?
一维中的梯度下降给我们很好的启发。
考虑一类连续可微实值函数
利用泰勒展开,我们可以得到
:eqlabel:gd-taylor
即在一阶近似中,
我们可以假设在负梯度方向上移动的
为了简单起见,我们选择固定步长
将其代入泰勒展开式我们可以得到
:eqlabel:gd-taylor-2
如果其导数
此外,我们总是可以令
因此,
这意味着,如果我们使用
来迭代
因此,在梯度下降中,我们首先选择初始值
然后使用它们连续迭代
例如,当梯度
下面我们来展示如何实现梯度下降。为了简单起见,我们选用目标函数
尽管我们知道
但我们仍然使用这个简单的函数来观察
1 | %matplotlib inline |
1 | def f(x): # 目标函数 |
接下来,我们使用
使用梯度下降法迭代
1 | def gd(eta, f_grad): |
1 | epoch 10, x: 0.060466 |
对进行
1 | def show_trace(results, f): |

学习率
:label:subsec_gd-learningrate
学习率(learning rate)决定目标函数能否收敛到局部最小值,以及何时收敛到最小值。
学习率
请注意,如果我们使用的学习率太小,将导致
例如,考虑同一优化问题中
如下所示,尽管经过了10个步骤,我们仍然离最优解很远。
1 | show_trace(gd(0.05, f_grad), f) |
1 | epoch 10, x: 3.486784 |

相反,如果我们使用过高的学习率,
也就是说, :eqref:gd-taylor中的
在这种情况下,
例如,当学习率为
1 | show_trace(gd(1.1, f_grad), f) |
1 | epoch 10, x: 61.917364 |

局部最小值
为了演示非凸函数的梯度下降,考虑函数
这个函数有无穷多个局部最小值。
根据我们选择的学习率,我们最终可能只会得到许多解的一个。
下面的例子说明了(不切实际的)高学习率如何导致较差的局部最小值。
1 | c = torch.tensor(0.15 * np.pi) |
1 | epoch 10, x: -1.528166 |

多元梯度下降
现在我们对单变量的情况有了更好的理解,让我们考虑一下
即目标函数
相应地,它的梯度也是多元的,它是一个由
梯度中的每个偏导数元素
和先前单变量的情况一样,我们可以对多变量函数使用相应的泰勒近似来思考。
具体来说,
:eqlabel:gd-multi-taylor
换句话说,在
最陡下降的方向由负梯度
选择合适的学习率
这个算法在实践中的表现如何呢?
我们构造一个目标函数
并有二维向量
标量作为输出。
梯度由
我们将从初始位置
我们还需要两个辅助函数:
第一个是update函数,并将其应用于初始值20次;
第二个函数会显示
1 | def train_2d(trainer, steps=20, f_grad=None): #@save |
1 | def show_trace_2d(f, results): #@save |
接下来,我们观察学习率
可以看到,经过20步之后,
虽然进展相当顺利,但相当缓慢。
1 | def f_2d(x1, x2): # 目标函数 |
1 | epoch 20, x1: -0.057646, x2: -0.000073 |

随机梯度下降
:label:sec_sgd
在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在 :numref:sec_gd中描述了梯度下降的基本原则。本节继续更详细地说明随机梯度下降(stochastic gradient descent)。
1 | %matplotlib inline |
随机梯度更新
在深度学习中,目标函数通常是训练数据集中每个样本的损失函数的平均值。给定
如果使用梯度下降法,则每个自变量迭代的计算代价为
随机梯度下降(SGD)可降低每次迭代时的计算代价。在随机梯度下降的每次迭代中,我们对**数据样本随机均匀采样一个索引
其中
$$\mathbb{E}i \nabla f_i(\mathbf{x}) = \frac{1}{n} \sum{i = 1}^n \nabla f_i(\mathbf{x}) = \nabla f(\mathbf{x}).$$
这意味着,平均而言,随机梯度是对梯度的良好估计。
现在,我们将把它与梯度下降进行比较,方法是向梯度添加均值为0、方差为1的随机噪声,以模拟随机梯度下降。
1 | def f(x1, x2): # 目标函数 |
1 | def sgd(x1, x2, s1, s2, f_grad): |
1 | def constant_lr(): |
1 | epoch 50, x1: 0.069102, x2: 0.051447 |

小批量随机梯度下降
:label:sec_minibatch_sgd
到目前为止,我们在基于梯度的学习方法中遇到了两个极端情况:
:numref:sec_gd中使用完整数据集来计算梯度并更新参数,
:numref:sec_sgd中一次处理一个训练样本来取得进展。
二者各有利弊:每当数据非常相似时,梯度下降并不是非常“数据高效”。
而由于CPU和GPU无法充分利用向量化,随机梯度下降并不特别“计算高效”。
这暗示了两者之间可能有折中方案,这便涉及到小批量随机梯度下降(minibatch gradient descent)。
向量化和缓存
使用小批量的决策的核心是计算效率。
当考虑与多个GPU和多台服务器并行处理时,这一点最容易被理解。在这种情况下,我们需要向每个GPU发送至少一张图像。
有了每台服务器8个GPU和16台服务器,我们就能得到大小为128的小批量。
当涉及到单个GPU甚至CPU时,事情会更微妙一些:
这些设备有多种类型的内存、通常情况下多种类型的计算单元以及在它们之间不同的带宽限制。
例如,一个CPU有少量寄存器(register),L1和L2缓存,以及L3缓存(在不同的处理器内核之间共享)。
随着缓存的大小的增加,它们的延迟也在增加,同时带宽在减少。
可以说,处理器能够执行的操作远比主内存接口所能提供的多得多。
首先,具有16个内核和AVX-512向量化的2GHz CPU每秒可处理高达
同时,GPU的性能很容易超过该数字100倍。
而另一方面,中端服务器处理器的带宽可能不超过100Gb/s,即不到处理器满负荷所需的十分之一。
更糟糕的是,并非所有的内存入口都是相等的:内存接口通常为64位或更宽(例如,在最多384位的GPU上)。
因此读取单个字节会导致由于更宽的存取而产生的代价。
其次,第一次存取的额外开销很大,而按序存取(sequential access)或突发读取(burst read)相对开销较小。
有关更深入的讨论,请参阅此维基百科文章。
减轻这些限制的方法是使用足够快的CPU缓存层次结构来为处理器提供数据。
这是深度学习中批量处理背后的推动力。
举一个简单的例子:矩阵-矩阵乘法。
比如
- 我们可以计算$\mathbf{A}{ij} = \mathbf{B}{i,:} \mathbf{C}_{:,j}^\top$,也就是说,我们可以通过点积进行逐元素计算。
- 我们可以计算$\mathbf{A}{:,j} = \mathbf{B} \mathbf{C}{:,j}^\top
\mathbf{A} \mathbf{A}_{i,:}$。 - 我们可以简单地计算
。 - 我们可以将
和 分成较小的区块矩阵,然后一次计算 的一个区块。
如果我们使用第一个选择,每次我们计算一个元素$\mathbf{A}{ij}
它将内存带宽需求减半,相应地提高了访问速度。
第三种选择表面上是最可取的,然而大多数矩阵可能不能完全放入缓存中。
第四种选择提供了一个实践上很有用的方案:我们可以将矩阵的区块移到缓存中然后在本地将它们相乘。
让我们来看看这些操作在实践中的效率如何。
除了计算效率之外,Python和深度学习框架本身带来的额外开销也是相当大的。
回想一下,每次我们执行代码时,Python解释器都会向深度学习框架发送一个命令,要求将其插入到计算图中并在调度过程中处理它。
这样的额外开销可能是非常不利的。
总而言之,我们最好用向量化(和矩阵)。
1 | %matplotlib inline |
按元素分配只需遍历分别为
1 | # 逐元素计算A=BC |
1 | 2.1955742835998535 |
更快的策略是执行按列分配。
1 | # 逐列计算A=BC |
1 | 0.01396489143371582 |
最有效的方法是在一个区块中执行整个操作。让我们看看它们各自的操作速度是多少。
1 | # 一次性计算A=BC |
1 | performance in Gigaflops: element 0.911, column 143.216, full 401.043 |
小批量
:label:sec_minibatches
之前我们会理所当然地读取数据的小批量,而不是观测单个数据来更新参数,现在简要解释一下原因。
处理单个观测值需要我们执行许多单一矩阵-矢量(甚至矢量-矢量)乘法,这耗费相当大,而且对应深度学习框架也要巨大的开销。
这既适用于计算梯度以更新参数时,也适用于用神经网络预测。
也就是说,每当我们执行
$$\mathbf{g}t = \partial{\mathbf{w}} f(\mathbf{x}_{t}, \mathbf{w}).$$
我们可以通过将其应用于一个小批量观测值来提高此操作的计算效率。
也就是说,我们将梯度
$$\mathbf{g}t = \partial{\mathbf{w}} \frac{1}{|\mathcal{B}t|} \sum{i \in \mathcal{B}t} f(\mathbf{x}{i}, \mathbf{w}).$$
让我们看看这对
另一方面,方差显著降低。
由于小批量梯度由正在被平均计算的
这本身就是一件好事,因为这意味着更新与完整的梯度更接近了。
直观来说,这表明选择大型的小批量
然而,经过一段时间后,与计算代价的线性增长相比,标准差的额外减少是微乎其微的。
在实践中我们选择一个足够大的小批量,它可以提供良好的计算效率同时仍适合GPU的内存。
下面,我们来看看这些高效的代码。
在里面我们执行相同的矩阵-矩阵乘法,但是这次我们将其一次性分为64列的“小批量”。
1 | timer.start() |
1 | performance in Gigaflops: block 1003.062 |
显而易见,小批量上的计算基本上与完整矩阵一样有效。
需要注意的是,在 :numref:sec_batch_norm中,我们使用了一种在很大程度上取决于小批量中的方差的正则化。
随着后者增加,方差会减少,随之而来的是批量规范化带来的噪声注入的好处。
关于实例,请参阅 :cite:Ioffe.2017,了解有关如何重新缩放并计算适当项目。
读取数据集
让我们来看看如何从数据中有效地生成小批量。
下面我们使用NASA开发的测试机翼的数据集不同飞行器产生的噪声来比较这些优化算法。
为方便起见,我们只使用前
数据已作预处理:我们移除了均值并将方差重新缩放到每个坐标为
1 | #@save |
从零开始实现
:numref:sec_linear_scratch一节中已经实现过小批量随机梯度下降算法。
我们在这里将它的输入参数变得更加通用,主要是为了方便本章后面介绍的其他优化算法也可以使用同样的输入。
具体来说,我们添加了一个状态输入states并将超参数放在字典hyperparams中。
此外,我们将在训练函数里对各个小批量样本的损失求平均,因此优化算法中的梯度不需要除以批量大小。
1 | def sgd(params, states, hyperparams): |
下面实现一个通用的训练函数,以方便本章后面介绍的其他优化算法使用。
它初始化了一个线性回归模型,然后可以使用小批量随机梯度下降以及后续小节介绍的其他算法来训练模型。
1 | #@save |
让我们来看看批量梯度下降的优化是如何进行的。
这可以通过将小批量设置为1500(即样本总数)来实现。
因此,模型参数每个迭代轮数只迭代一次。
1 | def train_sgd(lr, batch_size, num_epochs=2): |

当批量大小为1时,优化使用的是随机梯度下降。
为了简化实现,我们选择了很小的学习率。
在随机梯度下降的实验中,每当一个样本被处理,模型参数都会更新。
在这个例子中,这相当于每个迭代轮数有1500次更新。
可以看到,目标函数值的下降在1个迭代轮数后就变得较为平缓。
尽管两个例子在一个迭代轮数内都处理了1500个样本,但实验中随机梯度下降的一个迭代轮数耗时更多。
这是因为随机梯度下降更频繁地更新了参数,而且一次处理单个观测值效率较低。
1 | sgd_res = train_sgd(0.005, 1) |

最后,当批量大小等于100时,我们使用小批量随机梯度下降进行优化。
每个迭代轮数所需的时间比随机梯度下降和批量梯度下降所需的时间短。
1 | mini1_res = train_sgd(.4, 100) |

将批量大小减少到10,每个迭代轮数的时间都会增加,因为每批工作负载的执行效率变得更低。
1 | mini2_res = train_sgd(.05, 10) |

现在我们可以比较前四个实验的时间与损失。
可以看出,尽管在处理的样本数方面,随机梯度下降的收敛速度快于梯度下降,但与梯度下降相比,它需要更多的时间来达到同样的损失,因为逐个样本来计算梯度并不那么有效。
小批量随机梯度下降能够平衡收敛速度和计算效率。
大小为10的小批量比随机梯度下降更有效;
大小为100的小批量在运行时间上甚至优于梯度下降。
1 | d2l.set_figsize([6, 3]) |

简洁实现
下面用深度学习框架自带算法实现一个通用的训练函数,我们将在本章中其它小节使用它。
1 | #@save |
下面使用这个训练函数,复现之前的实验。
1 | data_iter, _ = get_data_ch11(10) |

小结
- 由于减少了深度学习框架的额外开销,使用更好的内存定位以及CPU和GPU上的缓存,向量化使代码更加高效。
- 随机梯度下降的“统计效率”与大批量一次处理数据的“计算效率”之间存在权衡。小批量随机梯度下降提供了两全其美的答案:计算和统计效率。
- 在小批量随机梯度下降中,我们处理通过训练数据的随机排列获得的批量数据(即每个观测值只处理一次,但按随机顺序)。
- 在训练期间降低学习率有助于训练。
- 一般来说,小批量随机梯度下降比随机梯度下降和梯度下降的速度快,收敛风险较小。
练习
- 修改批量大小和学习率,并观察目标函数值的下降率以及每个迭代轮数消耗的时间。
- 将小批量随机梯度下降与实际从训练集中取样替换的变体进行比较。会看出什么?
- 一个邪恶的精灵在没通知你的情况下复制了你的数据集(即每个观测发生两次,数据集增加到原始大小的两倍,但没有人告诉你)。随机梯度下降、小批量随机梯度下降和梯度下降的表现将如何变化?
动量法
:label:sec_momentum
在 :numref:sec_sgd一节中,我们详述了如何执行随机梯度下降,即在只有嘈杂的梯度可用的情况下执行优化时会发生什么。
对于嘈杂的梯度,我们在选择学习率需要格外谨慎。
如果衰减速度太快,收敛就会停滞。
相反,如果太宽松,我们可能无法收敛到最优解。
基础
本节将探讨更有效的优化算法,尤其是针对实验中常见的某些类型的优化问题。
泄漏平均值
上一节中我们讨论了小批量随机梯度下降作为加速计算的手段。
它也有很好的副作用,即平均梯度减小了方差。
小批量随机梯度下降可以通过以下方式计算:
$$\mathbf{g}{t, t-1} = \partial{\mathbf{w}} \frac{1}{|\mathcal{B}t|} \sum{i \in \mathcal{B}t} f(\mathbf{x}{i}, \mathbf{w}_{t-1}) = \frac{1}{|\mathcal{B}t|} \sum{i \in \mathcal{B}t} \mathbf{h}{i, t-1}.
$$
为了保持记法简单,在这里我们使用$\mathbf{h}{i, t-1} = \partial{\mathbf{w}} f(\mathbf{x}i, \mathbf{w}{t-1})
如果我们能够从方差减少的影响中受益,甚至超过小批量上的梯度平均值,那很不错。
完成这项任务的一种选择是用泄漏平均值(leaky average)取代梯度计算:
$$\mathbf{v}t = \beta \mathbf{v}{t-1} + \mathbf{g}_{t, t-1}$$
其中
这有效地将瞬时梯度替换为多个“过去”梯度的平均值。
它累加了过去的梯度。
为了更详细地解释,让我们递归地将
$$\begin{aligned}
\mathbf{v}t = \beta^2 \mathbf{v}{t-2} + \beta \mathbf{g}{t-1, t-2} + \mathbf{g}{t, t-1}
= \ldots, = \sum_{\tau = 0}^{t-1} \beta^{\tau} \mathbf{g}_{t-\tau, t-\tau-1}.
\end{aligned}$$
其中,较大的
新的梯度替换不再指向特定实例下降最陡的方向,而是指向过去梯度的加权平均值的方向。
这使我们能够实现对单批量计算平均值的大部分好处,而不产生实际计算其梯度的代价。
上述推理构成了”加速”梯度方法的基础,例如具有动量的梯度。
在优化问题条件不佳的情况下(例如,有些方向的进展比其他方向慢得多,类似狭窄的峡谷),”加速”梯度还额外享受更有效的好处。
此外,它们允许我们对随后的梯度计算平均值,以获得更稳定的下降方向。
诚然,即使是对于无噪声凸问题,加速度这方面也是动量如此起效的关键原因之一。
正如人们所期望的,由于其功效,动量是深度学习及其后优化中一个深入研究的主题。
例如,请参阅[文章](https://distill.pub/2017/momentum/)(作者是 :cite:Goh.2017),观看深入分析和互动动画。
动量是由 :cite:Polyak.1964提出的。
:cite:Nesterov.2018在凸优化的背景下进行了详细的理论讨论。
长期以来,深度学习的动量一直被认为是有益的。
有关实例的详细信息,请参阅 :cite:Sutskever.Martens.Dahl.ea.2013的讨论。
条件不佳的问题
为了更好地了解动量法的几何属性,我们复习一下梯度下降,尽管它的目标函数明显不那么令人愉快。
回想我们在 :numref:sec_gd中使用了
我们通过向
与之前一样,
该函数在
让我们看看在这个新函数上执行梯度下降时会发生什么。
1 | %matplotlib inline |
1 | epoch 20, x1: -0.943467, x2: -0.000073 |

1 | eta = 0.6 |
1 | epoch 20, x1: -0.387814, x2: -1673.365109 |

动量法
动量法(momentum)使我们能够解决上面描述的梯度下降问题。
观察上面的优化轨迹,我们可能会直觉到计算过去的平均梯度效果会很好。
毕竟,在
相反,在梯度振荡的
使用
$$
\begin{aligned}
\mathbf{v}t &\leftarrow \beta \mathbf{v}{t-1} + \mathbf{g}_{t, t-1}, \
\mathbf{x}t &\leftarrow \mathbf{x}{t-1} - \eta_t \mathbf{v}_t.
\end{aligned}
$$
请注意,对于
在深入研究它的数学属性之前,让我们快速看一下算法在实验中的表现如何。
1 | def momentum_2d(x1, x2, v1, v2): |
1 | epoch 20, x1: 0.007188, x2: 0.002553 |

正如所见,尽管学习率与我们以前使用的相同,动量法仍然很好地收敛了。
让我们看看当降低动量参数时会发生什么。
将其减半至
尽管如此,它比没有动量时解将会发散要好得多。
1 | eta, beta = 0.6, 0.25 |
1 | epoch 20, x1: -0.126340, x2: -0.186632 |

请注意,我们可以将动量法与随机梯度下降,特别是小批量随机梯度下降结合起来。
唯一的变化是,在这种情况下,我们将梯度
为了方便起见,我们在时间
有效样本权重
回想一下$\mathbf{v}t = \sum{\tau = 0}^{t-1} \beta^{\tau} \mathbf{g}{t-\tau, t-\tau-1}
1 | d2l.set_figsize() |

实际实验
让我们来看看动量法在实验中是如何运作的。
为此,我们需要一个更加可扩展的实现。
从零开始实现
相比于小批量随机梯度下降,动量方法需要维护一组辅助变量,即速度。
它与梯度以及优化问题的变量具有相同的形状。
在下面的实现中,我们称这些变量为states。
1 | def init_momentum_states(feature_dim): |
1 | def sgd_momentum(params, states, hyperparams): |
让我们看看它在实验中是如何运作的。
1 | def train_momentum(lr, momentum, num_epochs=2): |

当我们将动量超参数momentum增加到0.9时,它相当于有效样本数量增加到
我们将学习率略微降至
1 | train_momentum(0.01, 0.9) |

降低学习率进一步解决了任何非平滑优化问题的困难,将其设置为
1 | train_momentum(0.005, 0.9) |

简洁实现
由于深度学习框架中的优化求解器早已构建了动量法,设置匹配参数会产生非常类似的轨迹。
1 | trainer = torch.optim.SGD |

理论分析
下面我们将看到,它在实际生活中非常具有代表性,至少最小化凸二次目标函数的情况下是如此。
二次凸函数
考虑这个函数
这是一个普通的二次函数。
对于正定矩阵
因此我们可以将
梯度由
也就是说,它是由
因此,动量法还是
由于
这使我们能够将变量从
这里
由于
以
$$\mathbf{z}t = \mathbf{z}{t-1} - \boldsymbol{\Lambda} \mathbf{z}{t-1} = (\mathbf{I} - \boldsymbol{\Lambda}) \mathbf{z}{t-1}.$$
这个表达式中的重要事实是梯度下降在不同的特征空间之间不会混合。
也就是说,如果用
这在动量法中也适用。
$$\begin{aligned}
\mathbf{v}t & = \beta \mathbf{v}{t-1} + \boldsymbol{\Lambda} \mathbf{z}{t-1} \
\mathbf{z}t & = \mathbf{z}{t-1} - \eta \left(\beta \mathbf{v}{t-1} + \boldsymbol{\Lambda} \mathbf{z}{t-1}\right) \
& = (\mathbf{I} - \eta \boldsymbol{\Lambda}) \mathbf{z}{t-1} - \eta \beta \mathbf{v}_{t-1}.
\end{aligned}$$
在这样做的过程中,我们只是证明了以下定理:带有和带有不凸二次函数动量的梯度下降,可以分解为朝二次矩阵特征向量方向坐标顺序的优化。
标量函数
鉴于上述结果,让我们看看当我们最小化函数
对于梯度下降我们有
每
这显示了在我们将学习率
超过该数值之后,梯度开始发散,对于
1 | lambdas = [0.1, 1, 10, 19] |

为了分析动量的收敛情况,我们首先用两个标量重写更新方程:一个用于
我们用
在
因此,收敛速度是由
请参阅文章 :cite:Goh.2017了解精彩动画。
请参阅 :cite:Flammarion.Bach.2015了解详细分析。
简而言之,当
与梯度下降的
另外,一般而言较大值的
小结
- 动量法用过去梯度的平均值来替换梯度,这大大加快了收敛速度。
- 对于无噪声梯度下降和嘈杂随机梯度下降,动量法都是可取的。
- 动量法可以防止在随机梯度下降的优化过程停滞的问题。
- 由于对过去的数据进行了指数降权,有效梯度数为
。 - 在凸二次问题中,可以对动量法进行明确而详细的分析。
- 动量法的实现非常简单,但它需要我们存储额外的状态向量(动量
)。
练习
- 使用动量超参数和学习率的其他组合,观察和分析不同的实验结果。
- 试试梯度下降和动量法来解决一个二次问题,其中有多个特征值,即
,例如 。绘制出 的值在初始化 时如何下降。 - 推导
的最小值和最小化器。 - 当我们执行带动量法的随机梯度下降时会有什么变化?当我们使用带动量法的小批量随机梯度下降时会发生什么?试验参数如何?
AdaGrad算法
:label:sec_adagrad
我们从有关特征学习中并不常见的问题入手。
稀疏特征和学习率
假设我们正在训练一个语言模型。
为了获得良好的准确性,我们大多希望在训练的过程中降低学习率,速度通常为
现在讨论关于稀疏特征(即只在偶尔出现的特征)的模型训练,这对自然语言来说很常见。
例如,我们看到“预先条件”这个词比“学习”这个词的可能性要小得多。
但是,它在计算广告学和个性化协同过滤等其他领域也很常见。
只有在这些不常见的特征出现时,与其相关的参数才会得到有意义的更新。
鉴于学习率下降,我们可能最终会面临这样的情况:常见特征的参数相当迅速地收敛到最佳值,而对于不常见的特征,我们仍缺乏足够的观测以确定其最佳值。
换句话说,学习率要么对于常见特征而言降低太慢,要么对于不常见特征而言降低太快。
解决此问题的一个方法是记录我们看到特定特征的次数,然后将其用作调整学习率。
即我们可以使用大小为
在这里
这其实很容易实施且不产生额外损耗。
AdaGrad算法 :cite:Duchi.Hazan.Singer.2011通过将粗略的计数器
它使用
这有两个好处:首先,我们不再需要决定梯度何时算足够大。
其次,它会随梯度的大小自动变化。通常对应于较大梯度的坐标会显著缩小,而其他梯度较小的坐标则会得到更平滑的处理。
在实际应用中,它促成了计算广告学及其相关问题中非常有效的优化程序。
但是,它遮盖了AdaGrad固有的一些额外优势,这些优势在预处理环境中很容易被理解。
预处理
凸优化问题有助于分析算法的特点。
毕竟对大多数非凸问题来说,获得有意义的理论保证很难,但是直觉和洞察往往会延续。
让我们来看看最小化
正如在 :numref:sec_momentum中那样,我们可以根据其特征分解
在这里我们使用了
修改后优化器为
这样更容易计算,因为
如果稍微扰动
遗憾的是,情况并非如此。
虽然
每当特征值
相反,对小的
最大和最小的特征值之比称为优化问题的条件数(condition number)。
如果条件编号
我们需要确保在获取大量动态的特征值范围时足够谨慎:难道我们不能简单地通过扭曲空间来“修复”这个问题,从而使所有特征值都是
理论上这很容易:我们只需要
在新的坐标系中,
可惜,这是一个相当不切实际的想法。
一般而言,计算特征值和特征向量要比解决实际问题“贵”得多。
虽然准确计算特征值可能会很昂贵,但即便只是大致猜测并计算它们,也可能已经比不做任何事情好得多。
特别是,我们可以使用
这比计算特征值开销小的多。
在这种情况下,我们得到了$\tilde{\mathbf{Q}}{ij} = \mathbf{Q}{ij} / \sqrt{\mathbf{Q}{ii} \mathbf{Q}{jj}}
在大多数情况下,这大大简化了条件数。
例如我们之前讨论的案例,它将完全消除眼下的问题,因为问题是轴对齐的。
遗憾的是,我们还面临另一个问题:在深度学习中,我们通常情况甚至无法计算目标函数的二阶导数:对于
AdaGrad算法巧妙的思路是,使用一个代理来表示黑塞矩阵(Hessian)的对角线,既相对易于计算又高效。
为了了解它是如何生效的,让我们来看看
我们有
其中
因此,梯度的大小取决于
如果$\bar{\mathbf{x}} - \bar{\mathbf{x}}0
由于AdaGrad算法是一种随机梯度下降算法,所以即使是在最佳值中,我们也会看到具有非零方差的梯度。
因此,我们可以放心地使用梯度的方差作为黑塞矩阵比例的廉价替代。
详尽的分析(要花几页解释)超出了本节的范围,请读者参考 :cite:Duchi.Hazan.Singer.2011。
算法
让我们接着上面正式开始讨论。
我们使用变量
$$
\begin{aligned}
\mathbf{g}t & = \partial{\mathbf{w}} l(y_t, f(\mathbf{x}_t, \mathbf{w})), \
\mathbf{s}t & = \mathbf{s}{t-1} + \mathbf{g}_t^2, \
\mathbf{w}t & = \mathbf{w}{t-1} - \frac{\eta}{\sqrt{\mathbf{s}_t + \epsilon}} \cdot \mathbf{g}_t.
\end{aligned}
$$
在这里,操作是按照坐标顺序应用。
也就是说,
同样,
并且
与之前一样,
最后,我们初始化
就像在动量法中我们需要跟踪一个辅助变量一样,在AdaGrad算法中,我们允许每个坐标有单独的学习率。
与SGD算法相比,这并没有明显增加AdaGrad的计算代价,因为主要计算用在
请注意,在
这产生了一个学习率
对于凸问题,这完全足够了。
然而,在深度学习中,我们可能希望更慢地降低学习率。
这引出了许多AdaGrad算法的变体,我们将在后续章节中讨论它们。
眼下让我们先看看它在二次凸问题中的表现如何。
我们仍然以同一函数为例:
我们将使用与之前相同的学习率来实现AdaGrad算法,即
可以看到,自变量的迭代轨迹较平滑。
但由于
1 | %matplotlib inline |
1 | def adagrad_2d(x1, x2, s1, s2): |
1 | epoch 20, x1: -2.382563, x2: -0.158591 |

1 | eta = 2 |
1 | epoch 20, x1: -0.002295, x2: -0.000000 |

从零开始实现
同动量法一样,AdaGrad算法需要对每个自变量维护同它一样形状的状态变量。
1 | def init_adagrad_states(feature_dim): |
与 :numref:sec_minibatch_sgd一节中的实验相比,这里使用更大的学习率来训练模型。
1 | data_iter, feature_dim = d2l.get_data_ch11(batch_size=10) |

简洁实现
我们可直接使用深度学习框架中提供的AdaGrad算法来训练模型。
1 | trainer = torch.optim.Adagrad |

小结
- AdaGrad算法会在单个坐标层面动态降低学习率。
- AdaGrad算法利用梯度的大小作为调整进度速率的手段:用较小的学习率来补偿带有较大梯度的坐标。
- 在深度学习问题中,由于内存和计算限制,计算准确的二阶导数通常是不可行的。梯度可以作为一个有效的代理。
- 如果优化问题的结构相当不均匀,AdaGrad算法可以帮助缓解扭曲。
- AdaGrad算法对于稀疏特征特别有效,在此情况下由于不常出现的问题,学习率需要更慢地降低。
- 在深度学习问题上,AdaGrad算法有时在降低学习率方面可能过于剧烈。我们将在 :numref:
sec_adam一节讨论缓解这种情况的策略。
练习
- 证明对于正交矩阵
和向量 ,以下等式成立: 。为什么这意味着在变量的正交变化之后,扰动的程度不会改变? - 尝试对函数
、以及它旋转45度后的函数即 使用AdaGrad算法。它的表现会不同吗? - 证明格什戈林圆盘定理,其中提到,矩阵
的特征值 在至少一个 的选项中满足$|\lambda_i - \mathbf{M}{jj}| \leq \sum{k \neq j} |\mathbf{M}_{jk}|$的要求。 - 关于对角线预处理矩阵
的特征值,格什戈林的定理告诉了我们什么? - 尝试对适当的深度网络使用AdaGrad算法,例如,:numref:
sec_lenet中应用于Fashion-MNIST的深度网络。 - 要如何修改AdaGrad算法,才能使其在学习率方面的衰减不那么激进?
RMSProp算法
:label:sec_rmsprop
:numref:sec_adagrad中的关键问题之一,是学习率按预定时间表
虽然这通常适用于凸问题,但对于深度学习中遇到的非凸问题,可能并不理想。
但是,作为一个预处理器,Adagrad算法按坐标顺序的适应性是非常可取的。
:cite:Tieleman.Hinton.2012建议以RMSProp算法作为将速率调度与坐标自适应学习率分离的简单修复方法。
问题在于,Adagrad算法将梯度
解决此问题的一种方法是使用
对
遗憾的是,限制行为生效可能需要很长时间,因为该流程记住了值的完整轨迹。
另一种方法是按动量法中的方式使用泄漏平均值,即$\mathbf{s}t \leftarrow \gamma \mathbf{s}{t-1} + (1-\gamma) \mathbf{g}_t^2
保持所有其它部分不变就产生了RMSProp算法。
算法
让我们详细写出这些方程式。
$$\begin{aligned}
\mathbf{s}t & \leftarrow \gamma \mathbf{s}{t-1} + (1 - \gamma) \mathbf{g}_t^2, \
\mathbf{x}t & \leftarrow \mathbf{x}{t-1} - \frac{\eta}{\sqrt{\mathbf{s}_t + \epsilon}} \odot \mathbf{g}_t.
\end{aligned}$$
常数
鉴于这种扩展,我们现在可以自由控制学习率
就泄漏平均值而言,我们可以采用与之前在动量法中适用的相同推理。
扩展
$$
\begin{aligned}
\mathbf{s}t & = (1 - \gamma) \mathbf{g}t^2 + \gamma \mathbf{s}{t-1} \
& = (1 - \gamma) \left(\mathbf{g}t^2 + \gamma \mathbf{g}{t-1}^2 + \gamma^2 \mathbf{g}{t-2} + \ldots, \right).
\end{aligned}
$$
同之前在 :numref:sec_momentum小节一样,我们使用
因此,权重总和标准化为
让我们图像化各种数值的
1 | import math |
1 | d2l.set_figsize() |

从零开始实现
和之前一样,我们使用二次函数
回想在 :numref:sec_adagrad一节中,当我们使用学习率为0.4的Adagrad算法时,变量在算法的后期阶段移动非常缓慢,因为学习率衰减太快。
RMSProp算法中不会发生这种情况,因为
1 | def rmsprop_2d(x1, x2, s1, s2): |
1 | epoch 20, x1: -0.010599, x2: 0.000000 |

1 | def init_rmsprop_states(feature_dim): |
1 | def rmsprop(params, states, hyperparams): |
我们将初始学习率设置为0.01,加权项
也就是说,
1 | data_iter, feature_dim = d2l.get_data_ch11(batch_size=10) |

简洁实现
我们可直接使用深度学习框架中提供的RMSProp算法来训练模型。
1 | trainer = torch.optim.RMSprop |

小结
- RMSProp算法与Adagrad算法非常相似,因为两者都使用梯度的平方来缩放系数。
- RMSProp算法与动量法都使用泄漏平均值。但是,RMSProp算法使用该技术来调整按系数顺序的预处理器。
- 在实验中,学习率需要由实验者调度。
- 系数
决定了在调整每坐标比例时历史记录的时长。
练习
- 如果我们设置
,实验会发生什么?为什么? - 旋转优化问题以最小化
。收敛会发生什么? - 试试在真正的机器学习问题上应用RMSProp算法会发生什么,例如在Fashion-MNIST上的训练。试验不同的取值来调整学习率。
- 随着优化的进展,需要调整
吗?RMSProp算法对此有多敏感?
Adadelta
:label:sec_adadelta
Adadelta是AdaGrad的另一种变体( :numref:sec_adagrad),
主要区别在于前者减少了学习率适应坐标的数量。
此外,广义上Adadelta被称为没有学习率,因为它使用变化量本身作为未来变化的校准。
Adadelta算法是在 :cite:Zeiler.2012中提出的。
Adadelta算法
简而言之,Adadelta使用两个状态变量,
以下是Adadelta的技术细节。鉴于参数du jour是sec_rmsprop类似的以下泄漏更新:
$$\begin{aligned}
\mathbf{s}t & = \rho \mathbf{s}{t-1} + (1 - \rho) \mathbf{g}_t^2.
\end{aligned}$$
与 :numref:sec_rmsprop的区别在于,我们使用重新缩放的梯度
$$\begin{aligned}
\mathbf{x}t & = \mathbf{x}{t-1} - \mathbf{g}_t’. \
\end{aligned}$$
那么,调整后的梯度
$$\begin{aligned}
\mathbf{g}t’ & = \frac{\sqrt{\Delta\mathbf{x}{t-1} + \epsilon}}{\sqrt{\mathbf{s}_t + \epsilon}} \odot \mathbf{g}_t, \
\end{aligned}$$
其中
$$\begin{aligned}
\Delta \mathbf{x}t & = \rho \Delta\mathbf{x}{t-1} + (1 - \rho) {\mathbf{g}_t’}^2,
\end{aligned}$$
和
代码实现
Adadelta需要为每个变量维护两个状态变量,即
1 | %matplotlib inline |
对于每次参数更新,选择
1 | data_iter, feature_dim = d2l.get_data_ch11(batch_size=10) |

为了简洁实现,我们只需使用高级API中的Adadelta算法。
1 | trainer = torch.optim.Adadelta |

小结
- Adadelta没有学习率参数。相反,它使用参数本身的变化率来调整学习率。
- Adadelta需要两个状态变量来存储梯度的二阶导数和参数的变化。
- Adadelta使用泄漏的平均值来保持对适当统计数据的运行估计。
练习
- 调整
的值,会发生什么? - 展示如何在不使用
的情况下实现算法。为什么这是个好主意? - Adadelta真的是学习率为0吗?能找到Adadelta无法解决的优化问题吗?
- 将Adadelta的收敛行为与AdaGrad和RMSProp进行比较。
Adam算法
:label:sec_adam
本章我们已经学习了许多有效优化的技术。
在本节讨论之前,我们先详细回顾一下这些技术:
- 在 :numref:
sec_sgd中,我们学习了:随机梯度下降在解决优化问题时比梯度下降更有效。 - 在 :numref:
sec_minibatch_sgd中,我们学习了:在一个小批量中使用更大的观测值集,可以通过向量化提供额外效率。这是高效的多机、多GPU和整体并行处理的关键。 - 在 :numref:
sec_momentum中我们添加了一种机制,用于汇总过去梯度的历史以加速收敛。 - 在 :numref:
sec_adagrad中,我们通过对每个坐标缩放来实现高效计算的预处理器。 - 在 :numref:
sec_rmsprop中,我们通过学习率的调整来分离每个坐标的缩放。
Adam算法 :cite:Kingma.Ba.2014将所有这些技术汇总到一个高效的学习算法中。
不出预料,作为深度学习中使用的更强大和有效的优化算法之一,它非常受欢迎。
但是它并非没有问题,尤其是 :cite:Reddi.Kale.Kumar.2019表明,有时Adam算法可能由于方差控制不良而发散。
在完善工作中, :cite:Zaheer.Reddi.Sachan.ea.2018给Adam算法提供了一个称为Yogi的热补丁来解决这些问题。
下面我们了解一下Adam算法。
算法
Adam算法的关键组成部分之一是:它使用指数加权移动平均值来估算梯度的动量和二次矩,即它使用状态变量
$$\begin{aligned}
\mathbf{v}t & \leftarrow \beta_1 \mathbf{v}{t-1} + (1 - \beta_1) \mathbf{g}_t, \
\mathbf{s}t & \leftarrow \beta_2 \mathbf{s}{t-1} + (1 - \beta_2) \mathbf{g}_t^2.
\end{aligned}$$
这里
常将它们设置为
也就是说,方差估计的移动远远慢于动量估计的移动。
注意,如果我们初始化$\mathbf{v}_0 = \mathbf{s}0 = 0
相应地,标准化状态变量由下式获得
有了正确的估计,我们现在可以写出更新方程。
首先,我们以非常类似于RMSProp算法的方式重新缩放梯度以获得
与RMSProp不同,我们的更新使用动量
此外,由于使用
前者在实践中效果略好一些,因此与RMSProp算法有所区分。
通常,我们选择
最后,我们简单更新:
$$\mathbf{x}t \leftarrow \mathbf{x}{t-1} - \mathbf{g}_t’.$$
回顾Adam算法,它的设计灵感很清楚:
首先,动量和规模在状态变量中清晰可见,
它们相当独特的定义使我们移除偏项(这可以通过稍微不同的初始化和更新条件来修正)。
其次,RMSProp算法中两项的组合都非常简单。
最后,明确的学习率
实现
从头开始实现Adam算法并不难。
为方便起见,我们将时间步hyperparams字典中。
除此之外,一切都很简单。
1 | %matplotlib inline |
现在,我们用以上Adam算法来训练模型,这里我们使用
1 | data_iter, feature_dim = d2l.get_data_ch11(batch_size=10) |

此外,我们可以用深度学习框架自带算法应用Adam算法,这里我们只需要传递配置参数。
1 | trainer = torch.optim.Adam |

Yogi
Adam算法也存在一些问题:
即使在凸环境下,当
:cite:Zaheer.Reddi.Sachan.ea.2018为
论文中建议我们重写Adam算法更新如下:
$$\mathbf{s}t \leftarrow \mathbf{s}{t-1} + (1 - \beta_2) \left(\mathbf{g}t^2 - \mathbf{s}{t-1}\right).$$
每当
一个有效的解决方法是将$\mathbf{g}t^2 - \mathbf{s}{t-1}
这就是Yogi更新,现在更新的规模不再取决于偏差的量。
$$\mathbf{s}t \leftarrow \mathbf{s}{t-1} + (1 - \beta_2) \mathbf{g}_t^2 \odot \mathop{\mathrm{sgn}}(\mathbf{g}t^2 - \mathbf{s}{t-1}).$$
论文中,作者还进一步建议用更大的初始批量来初始化动量,而不仅仅是初始的逐点估计。
1 | def yogi(params, states, hyperparams): |

小结
- Adam算法将许多优化算法的功能结合到了相当强大的更新规则中。
- Adam算法在RMSProp算法基础上创建的,还在小批量的随机梯度上使用EWMA。
- 在估计动量和二次矩时,Adam算法使用偏差校正来调整缓慢的启动速度。
- 对于具有显著差异的梯度,我们可能会遇到收敛性问题。我们可以通过使用更大的小批量或者切换到改进的估计值
来修正它们。Yogi提供了这样的替代方案。
练习
- 调节学习率,观察并分析实验结果。
- 试着重写动量和二次矩更新,从而使其不需要偏差校正。
- 收敛时为什么需要降低学习率
? - 尝试构造一个使用Adam算法会发散而Yogi会收敛的例子。
学习率调度器
:label:sec_scheduler
到目前为止,我们主要关注如何更新权重向量的优化算法,而不是它们的更新速率。
然而,调整学习率通常与实际算法同样重要,有如下几方面需要考虑:
- 首先,学习率的大小很重要。如果它太大,优化就会发散;如果它太小,训练就会需要过长时间,或者我们最终只能得到次优的结果。我们之前看到问题的条件数很重要(有关详细信息,请参见 :numref:
sec_momentum)。直观地说,这是最不敏感与最敏感方向的变化量的比率。 - 其次,衰减速率同样很重要。如果学习率持续过高,我们可能最终会在最小值附近弹跳,从而无法达到最优解。 :numref:
sec_minibatch_sgd比较详细地讨论了这一点,在 :numref:sec_sgd中我们则分析了性能保证。简而言之,我们希望速率衰减,但要比慢,这样能成为解决凸问题的不错选择。 - 另一个同样重要的方面是初始化。这既涉及参数最初的设置方式(详情请参阅 :numref:
sec_numerical_stability),又关系到它们最初的演变方式。这被戏称为预热(warmup),即我们最初开始向着解决方案迈进的速度有多快。一开始的大步可能没有好处,特别是因为最初的参数集是随机的。最初的更新方向可能也是毫无意义的。 - 最后,还有许多优化变体可以执行周期性学习率调整。这超出了本章的范围,我们建议读者阅读 :cite:
Izmailov.Podoprikhin.Garipov.ea.2018来了解个中细节。例如,如何通过对整个路径参数求平均值来获得更好的解。
鉴于管理学习率需要很多细节,因此大多数深度学习框架都有自动应对这个问题的工具。
在本章中,我们将梳理不同的调度策略对准确性的影响,并展示如何通过学习率调度器(learning rate scheduler)来有效管理。
一个简单的问题
我们从一个简单的问题开始,这个问题可以轻松计算,但足以说明要义。
为此,我们选择了一个稍微现代化的LeNet版本(激活函数使用relu而不是sigmoid,汇聚层使用最大汇聚层而不是平均汇聚层),并应用于Fashion-MNIST数据集。
此外,我们混合网络以提高性能。
由于大多数代码都是标准的,我们只介绍基础知识,而不做进一步的详细讨论。如果需要,请参阅 :numref:chap_cnn进行复习。
1 | %matplotlib inline |
让我们来看看如果使用默认设置,调用此算法会发生什么。
例如设学习率为
留意在超过了某点、测试准确度方面的进展停滞时,训练准确度将如何继续提高。
两条曲线之间的间隙表示过拟合。
1 | lr, num_epochs = 0.3, 30 |

学习率调度器
我们可以在每个迭代轮数(甚至在每个小批量)之后向下调整学习率。
例如,以动态的方式来响应优化的进展情况。
1 | lr = 0.1 |
1 | learning rate is now 0.10 |
更通常而言,我们应该定义一个调度器。
当调用更新次数时,它将返回学习率的适当值。
让我们定义一个简单的方法,将学习率设置为
1 | class SquareRootScheduler: |
让我们在一系列值上绘制它的行为。
1 | scheduler = SquareRootScheduler(lr=0.1) |

现在让我们来看看这对在Fashion-MNIST数据集上的训练有何影响。
我们只是提供调度器作为训练算法的额外参数。
1 | net = net_fn() |

这比以前好一些:曲线比以前更加平滑,并且过拟合更小了。
遗憾的是,关于为什么在理论上某些策略会导致较轻的过拟合,有一些观点认为,较小的步长将导致参数更接近零,因此更简单。
但是,这并不能完全解释这种现象,因为我们并没有真正地提前停止,而只是轻柔地降低了学习率。
策略
虽然我们不可能涵盖所有类型的学习率调度器,但我们会尝试在下面简要概述常用的策略:多项式衰减和分段常数表。
此外,余弦学习率调度在实践中的一些问题上运行效果很好。
在某些问题上,最好在使用较高的学习率之前预热优化器。
单因子调度器
多项式衰减的一种替代方案是乘法衰减,即
为了防止学习率衰减到一个合理的下界之下,
更新方程经常修改为
1 | class FactorScheduler: |

接下来,我们将使用内置的调度器,但在这里仅解释它们的功能。
多因子调度器
训练深度网络的常见策略之一是保持学习率为一组分段的常量,并且不时地按给定的参数对学习率做乘法衰减。
具体地说,给定一组降低学习率的时间点,例如
每当
假设每步中的值减半,我们可以按如下方式实现这一点。
1 | net = net_fn() |

这种分段恒定学习率调度背后的直觉是,让优化持续进行,直到权重向量的分布达到一个驻点。
此时,我们才将学习率降低,以获得更高质量的代理来达到一个良好的局部最小值。
下面的例子展示了如何使用这种方法产生更好的解决方案。
1 | train(net, train_iter, test_iter, num_epochs, loss, trainer, device, |

余弦调度器
余弦调度器是 :cite:Loshchilov.Hutter.2016提出的一种启发式算法。
它所依据的观点是:我们可能不想在一开始就太大地降低学习率,而且可能希望最终能用非常小的学习率来“改进”解决方案。
这产生了一个类似于余弦的调度,函数形式如下所示,学习率的值在
这里
此外,对于
在下面的示例中,我们设置了最大更新步数
1 | class CosineScheduler: |

在计算机视觉的背景下,这个调度方式可能产生改进的结果。
但请注意,如下所示,这种改进并不一定成立。
1 | net = net_fn() |

预热
在某些情况下,初始化参数不足以得到良好的解。
这对某些高级网络设计来说尤其棘手,可能导致不稳定的优化结果。
对此,一方面,我们可以选择一个足够小的学习率,
从而防止一开始发散,然而这样进展太缓慢。
另一方面,较高的学习率最初就会导致发散。
解决这种困境的一个相当简单的解决方法是使用预热期,在此期间学习率将增加至初始最大值,然后冷却直到优化过程结束。
为了简单起见,通常使用线性递增。
这引出了如下表所示的时间表。
1 | scheduler = CosineScheduler(20, warmup_steps=5, base_lr=0.3, final_lr=0.01) |

注意,观察前5个迭代轮数的性能,网络最初收敛得更好。
1 | net = net_fn() |

预热可以应用于任何调度器,而不仅仅是余弦。
有关学习率调度的更多实验和更详细讨论,请参阅 :cite:Gotmare.Keskar.Xiong.ea.2018。
其中,这篇论文的点睛之笔的发现:预热阶段限制了非常深的网络中参数的发散程度 。
这在直觉上是有道理的:在网络中那些一开始花费最多时间取得进展的部分,随机初始化会产生巨大的发散。
小结
- 在训练期间逐步降低学习率可以提高准确性,并且减少模型的过拟合。
- 在实验中,每当进展趋于稳定时就降低学习率,这是很有效的。从本质上说,这可以确保我们有效地收敛到一个适当的解,也只有这样才能通过降低学习率来减小参数的固有方差。
- 余弦调度器在某些计算机视觉问题中很受欢迎。
- 优化之前的预热期可以防止发散。
- 优化在深度学习中有多种用途。对于同样的训练误差而言,选择不同的优化算法和学习率调度,除了最大限度地减少训练时间,可以导致测试集上不同的泛化和过拟合量。
练习
- 试验给定固定学习率的优化行为。这种情况下可以获得的最佳模型是什么?
- 如果改变学习率下降的指数,收敛性会如何改变?在实验中方便起见,使用
PolyScheduler。 - 将余弦调度器应用于大型计算机视觉问题,例如训练ImageNet数据集。与其他调度器相比,它如何影响性能?
- 预热应该持续多长时间?
- 可以试着把优化和采样联系起来吗?首先,在随机梯度朗之万动力学上使用 :cite:
Welling.Teh.2011的结果。







