梯度下降及其衍生算法#
学习目标与记号#
准确解释小批量梯度、指数移动平均与 Momentum;
根据公式和张量维度分析 RMSprop,并识别常见实现错误;
用
Python实现或验证 Adam;在其他条件相同的情况下,只改变一个需要研究的因素进行比较,并根据实验结果分析该因素可能带来的影响。
本节沿用统一记号:普通小写字母表示标量,粗体小写字母表示向量,粗体大写字母表示矩阵或高阶张量;层编号写作上标 \([l]\),样本或时间编号写作下标;转置写作 \(\trans\)。除非另有说明,批量样本按行存放。正文与练习中的程序都应同时检查数值结果和数组维度。本节练习的参考答案见 梯度下降及自适应优化答案。
本节以 批量梯度下降 为起点;关于训练、验证和测试数据的不同职责,参见 训练集、验证集与测试集。在式 (32) 中,梯度由全部训练样本计算,这称为批量梯度下降(batch gradient descent)。全批量梯度对当前经验风险给出确定的方向,但并不保证每一步都降低损失,也不保证非凸神经网络收敛到全局最优解;这些性质取决于目标函数、学习率和光滑性等条件。其主要限制是单次更新需要遍历全部数据,数据量较大时计算和内存开销很高。
本节依次介绍小批量梯度下降、Momentum、RMSprop 和 Adam。小批量方法改变每次估计梯度所使用的样本;后三种方法则利用梯度历史构造实际更新方向或逐元素步长。为简化记号,先写出全批量梯度下降:
其中,\(\mathrm{d}\bW^{[l](t)}\) 和 \(\mathrm{d}\bb^{[l](t)}\) 表示在第 \(t\) 步由全部训练集计算的梯度。对 Momentum、RMSprop 和 Adam 而言,真正用于参数更新的方向还会经过一阶矩或二阶原点矩的平滑与缩放。
我们将首先介绍小批量梯度下降。它与批量梯度下降的主要区别,是每次迭代使用多少样本来估计梯度。合理的小批量规模通常可以兼顾计算效率与梯度估计的稳定性。随后介绍 Momentum、RMSprop 和 Adam:它们利用历史梯度构造更新方向或逐坐标缩放因子,常能改善训练过程,但效果仍取决于学习率、批量规模和具体任务。
小批量梯度下降法#
记训练集为 \(\{(\bx_i,y_i):i=1,\ldots,n\}\)。小批量梯度下降(mini-batch gradient descent)每次只用 \(m\) 个样本估计梯度。每个训练周期开始时通常先随机打乱样本,再划分为两两不交的小批量 \(S_1,\ldots,S_k\),其中 \(k=\lceil n/m\rceil\),最后一个批量可以少于 \(m\) 个样本。依次使用每个小批量更新一次参数;完整遍历训练集一次称为一个训练周期(epoch)。每个周期重新打乱可避免固定批量分组引入系统性偏差。
小批量梯度下降的两个特殊情况为 批量梯度下降 (\(m=n\))以及 随机梯度下降 (\(m=1\))。
备注
批量大小常取 32、64、128 等便于硬件高效执行的数值,但不要求一定是 2 的幂。较大的批量可提高并行效率并降低梯度噪声,却需要更多显存;较小的批量更新更频繁、梯度噪声更大。训练周期数应结合验证集指标和早停策略选择,而不是仅观察训练损失。
批量梯度下降和小批量梯度下降的比较
批量梯度下降和小批量梯度下降是两种核心的梯度下降算法,它们各自展现出了独特的优势与局限。批量梯度下降在每次参数更新时都会利用全部训练数据,从而确保梯度方向的准确性,其使得在学习率较小的情况下,目标函数下降地更加稳定,且算法实现相对更加直观易懂。然而,其高昂的计算成本和巨大的内存占用,使得它在处理大型数据集时显得力不从心。相比之下,小批量梯度下降则通过每次仅使用一小部分数据来计算梯度,显著降低了计算成本,减少了内存占用,并提高了算法的扩展性,使其更适应大数据环境。同时,小批量梯度下降在某些情境下可能通过引入随机性来加速收敛。但是,这种方法也存在梯度方向可能受噪声影响、需要细致调整批量大小参数以及目标函数下降不稳定等潜在问题。因此,在实际应用中,选择哪种梯度下降算法需根据具体任务、数据集特性以及计算资源等因素进行综合考量,以期在算法效率、内存使用、收敛稳定性和最终模型性能之间找到最佳平衡点。总的来说,批量梯度下降则在处理小规模数据集的场合中仍然具有重要价值,而小批量梯度下降因其灵活性和高效性在深度学习领域得到了广泛应用。
尽管小批量梯度下降能够提高计算效率,但有些损失函数的形式比较复杂,梯度往往不能给出高效的更新策略。例如,图 6 中所展示的损失函数在一个方向变化较大,在另一个方向变化缓慢。如果初始值以及学习率选择得不好,则梯度下降算法的更新结果将非常不稳定,甚至发散。
图 6 低效的更新策略。图中曲线表示了一个二元目标函数等高线,其中横纵坐标分别代表不同的模型参数,中心五角星位置为局部极小值,箭头和实心圆点表示每次迭代更新后的参数值。#
下面介绍的三个计算梯度更新方向的策略,其均能够在一定程度上减轻 图 6 所遇到的问题。
Momentum#
我们仔细观察 图 6 中参数的更新过程可知,我们希望“减小”上下方向的更新幅度,而“增加”左右方向的更新幅度。如果我们将每步的更新方向看成一个序列,相邻两步的更新方向的“上下”方向符号相反,而“左右”方向的符号相同。因此,我们在第 \(t\) 步更新时,如果能够将当前更新方向和前面所有更新方向取均值,并将该均值方向作为参数的更新方向,那么我们很有可能得到一个更优的更新策略。
然而,存储每步更新方向对存储需求巨大,尤其是当我们训练的深度神经网络模型具有海量参数时。在本节中,我们考虑另外一种“平均”方法,即“指数加权移动平均”(Exponential weighted moving average)。记 \(\{\bd_i:i=1,2,\ldots\}\) 为某序列,记 \(\{\bv_i:i=0,1,\ldots\}\) 为其对应的指数加权移动平均值,则该加权平均值的计算公式如下:
其中,\(\beta_1\in[0,1)\) 是衰减系数,\(\bv_0\) 是移动平均的初值,通常取 \(\bzero\)。\(\beta_1\) 越大,历史信息保留越久,当前方向 \(\bd_i\) 的即时权重越小;\(0.9\) 是常用起点而不是固定定律。它属于超参数,应结合学习率、批量大小和任务验证。关于超参数选择,请参见 偏差—方差分析。
简单的数学推导
当 \(\beta_1\in(0,1)\) 时,我们有如下结果
即
记 \(\bv_0=0\),我们有如下结果
当 \(n\) 较小时,上式中权重之和为 \(1-\beta_1^n<1\),因此以零初始化的移动平均会偏向零;后面要介绍的 Adam 使用偏差修正消除这一影响。当 \(n\) 足够大时,\(\bv_n\) 可近似看成历史方向的指数加权平均,其有效记忆长度约为 \((1-\beta_1)^{-1}\)。
Momentum 利用指数加权移动平均计算已有更新方向的“均值”方向,并基于此均值方向对参数进行更新,其过程如下:
其中,\(l=1,\ldots,L\),\(\bv_W^{[l](t+1)} = \beta_1\bv_W^{[l](t)} + (1-\beta_1)\mathrm{d}\bW^{[l](t)}\),\(\mathrm{d}\bW^{[l](t)}\) 为损失函数对模型参数 \(\bW^{[l]}\) 的偏导数在当前参数取值 \(\btheta^{(t)}\) 下的函数值;\(\bv_b^{[l](t)}\) 的计算类似。
图 7 对比了批量梯度下降和 Momentum 对参数的更新结果。可见,Momentum 的更新更加稳健高效。
图 7 梯度下降以及Momentum参数更新路径。#
Momentum 使用历史梯度的指数加权平均作为更新方向,可抑制符号频繁翻转方向上的震荡,并在方向较一致时积累速度。它可能改善优化路径,但不能保证越过所有鞍点或找到全局最小值。
RMSprop#
Momentum 算法能够平滑更新方向,但它并未根据各参数分量的梯度尺度分别调整步长。在 图 6 所示的狭长谷地中,我们希望减小陡峭方向的有效步长,同时保留平缓方向的推进速度。RMSprop 正是通过梯度平方的指数移动平均,对不同坐标进行自适应缩放。
为了符号简洁,我们用 \(\bb\) 代表某一层偏置向量,用 \(\bW\) 代表某一层权重矩阵,即本节不再标注层编号。RMSprop 算法的更新过程为:
初始化 \(\bs_b^{(0)}=0\) 以及 \(\bs_W^{(0)}=0\),其中 \(\bs_b^{(0)}\) 的维度与 \(\bb\) 相同,\(\bs_W^{(0)}\) 的维度与 \(\bW\) 相同。
基于第 \(t\) 步的模型参数,计算损失函数对参数的梯度,即 \(\mathrm{d}\bb^{(t)}\) 以及 \(\mathrm{d}\bW^{(t)}\)。
更新
其中,\(\odot\) 表示 Hadamard 乘积,即对应元素相乘。
更新模型参数
其中,乘法和除法均表示元素乘除。
回到第 2 步直至收敛。
RMSprop 常以 \(\beta_2=0.9\) 或 \(0.99\)、\(\epsilon=10^{-8}\) 作为起点,但这些取值仍可能需要随框架、模型和数据调整。图 8 只展示一个二维目标函数上的示例路径,不能据此断言 RMSprop 在所有任务中都更快。
图 8 梯度下降、Momentum以及RMSprop参数更新路径。#
RMSprop 通过梯度平方的指数移动平均,对每个参数的更新进行逐元素缩放。它能缓和不同方向尺度差异,但不会自动解决全局学习率不合适、非凸局部几何或泛化问题;仍需在相同训练预算下用验证集比较。
Adam#
Adam(adaptive moment estimation)同时使用梯度的一阶矩估计和二阶原点矩估计。前者提供类似 Momentum 的平滑方向,后者依据近期梯度平方的尺度逐坐标缩放更新量。Adam 因而是深度学习中常用的默认优化器之一,但它并不保证在所有任务上都优于随机梯度下降。
更准确地说,Adam 调整的是每个坐标的 有效步长:近期梯度平方较大的坐标会受到更强的缩放。由于两个移动平均量均从零初始化,训练初期会系统性偏向零,因此还需进行偏差修正。Adam 每次更新的计算量与参数量同阶,但需要额外保存两个与参数同形的状态张量。
为了符号简洁,我们仍用 \(\bb\) 代表某一层偏置向量,用 \(\bW\) 代表某一层权重矩阵。Adam 算法的更新过程为:
初始化 \(\bv_b^{(0)}=\bzero\)、\(\bv_W^{(0)}=\bzero\)、\(\bs_b^{(0)}=\bzero\) 和 \(\bs_W^{(0)}=\bzero\);每个状态都与对应参数同形。
基于第 \(t\) 步的模型参数,计算损失函数对参数的梯度,即 \(\mathrm{d}\bb^{(t)}\) 以及 \(\mathrm{d}\bW^{(t)}\)。
更新
更新模型参数
其中,乘法和除法均表示元素乘除。
回到第 2 步直至收敛。
Adam 的常用默认值为 \(\beta_1=0.9\)、\(\beta_2=0.999\) 和 \(\epsilon=10^{-8}\)。图 9 展示了一个二维示例中的更新路径;不同优化器的相对表现会随目标函数、学习率和随机梯度噪声而变化。
图 9 梯度下降、Momentum、RMSprop以及Adam参数更新路径。#
Adam 同时估计梯度的一阶矩和二阶原点矩,并在训练初期进行偏差修正,通常是可靠的默认优化器。但“自适应”不等于无需调参:初始学习率、权重衰减、批量大小和学习率调度仍会显著影响结果。在一些任务中,SGD 加 Momentum 可能得到更好的泛化性能,应以验证集结果比较。
延伸:AdamW
当训练需要权重衰减时,直接把 \(\ell_2\) 惩罚梯度加入 Adam 的损失梯度,会使该惩罚项也进入一阶矩和二阶原点矩的估计。AdamW 将权重衰减与 Adam 的自适应梯度更新分开:矩估计只使用数据损失的梯度,权重则在参数更新时单独收缩。详细推导和实现注意事项见 AdamW:解耦权重衰减。
Shiny 交互演示:优化路径
下面的交互页面按照正文公式,在同一目标函数上比较梯度下降、Momentum、RMSprop 和 Adam 的参数更新路径。读者可以在其他条件相同的情况下改变学习率或移动平均系数,观察不同更新规则对收敛方向、振荡程度和最终目标函数值的影响。
核心推导与实现核验#
核心关系
推导路径。 本节用 \(\bv_t\) 和 \(\bs_t\) 分别表示梯度一阶矩与二阶原点矩的移动平均。在梯度分布近似平稳时,从零初始化得到 \(\mathbb{E}\bv_t=(1-\beta_1^t)\mathbb{E}\bg\),故除以 \(1-\beta_1^t\) 消除早期向零偏差;二阶原点矩同理。
关键条件
对恒定梯度递推指数移动平均并求几何级数,可严格得到 \(m_t=(1-\beta^t)g\),从而验证偏差修正。
数据规模
对于模型中的每一个参数,都要保存一个对应的梯度;Momentum 和 Adam 还要为它保存一阶平均量,Adam 另需保存二阶平均量。这些数组必须与参数一样大,平方、开方和除法都按对应位置逐个计算,不能误写成矩阵乘法。
常见误区
实现 Adam 时,应统一迭代次数的编号方式。如果程序循环从 0 开始,第一次偏差修正应使用 \(t+1=1\);如果公式把第一次更新编号为 \(t=1\),则直接使用 \(t\)。此外,把 \(\epsilon\) 放在平方根外,即使用 \(\sqrt{\widehat{\bs}_t}+\epsilon\),与把它放在平方根内,即使用 \(\sqrt{\widehat{\bs}_t+\epsilon}\),会得到不同的参数更新结果。推导和程序必须采用同一种 Adam 更新公式。
数值稳定性与规模
计算 Adam 更新时,应先确认二阶原点矩估计中的每个数值都不小于 0,再进行开方,并在分母中加入适当的小正数 \(\epsilon\),防止出现除以 0。第一次参数更新的编号应为 \(t=1\)。一阶矩和二阶原点矩只是优化器为了计算参数更新而保存的辅助量,不是神经网络的权重,不能把它们当作模型参数使用。
本节小结#
优化器状态不是模型参数。
Adam 的早期矩估计需要偏差修正。
算法比较应同时报告更新次数与实际计算成本。
综合练习#
本组练习从统一的参数更新视角比较梯度下降、Momentum、RMSprop 和 Adam,重点检查移动平均、偏差修正和程序状态是否正确,并在数据划分、随机种子和训练预算相同的情况下比较计算效率。程序题应报告运行环境。全部参考答案见 梯度下降及自适应优化答案。
四种更新规则。 设第 \(t\) 次更新前的参数为 \(\btheta_{t-1}\),由该参数计算得到的同形梯度为 \(\bg_t\)。用正文记号推导梯度下降、Momentum、RMSprop 和 Adam 的参数更新公式。指出各算法需要保存哪些与参数具有相同规模的变量,并区分矩阵乘法与逐元素平方、开方和除法。
指数移动平均与偏差修正。 设 \(\bv_0=\boldsymbol 0\) 且 \(0\leq\beta<1\),展开 \(\bv_t=\beta\bv_{t-1}+(1-\beta)\bg_t\)。当梯度恒为 \(\bg\) 时,证明 \(\bv_t=(1-\beta^t)\bg\),并说明 Adam 为什么使用 \(\widehat{\bv}_t=\bv_t/(1-\beta_1^t)\) 与 \(\widehat{\bs}_t=\bs_t/(1-\beta_2^t)\)。
两步手算。 令单个参数初值 \(\theta_0=1\),梯度依次为 \(g_1=2\)、\(g_2=4\),取 \(\alpha=0.1\)、\(\beta_1=0.9\)、\(\beta_2=0.99\),暂令 \(\epsilon=0\)。分别手算梯度下降、Momentum、RMSprop 和 Adam 的前两步状态与参数;Adam 应同时给出实际保存的原始状态 \(v_t\)、\(s_t\) 以及偏差修正后的 \(\widehat v_t\)、\(\widehat s_t\)。
有效更新与边界情况。 写出 RMSprop 和 Adam 每个坐标的有效更新量。在状态均从 0 初始化的前提下,分别分析整个梯度序列乘以正常数 \(c\)、梯度序列恒为 0、\(\beta_1=0\)、\(\beta_2=0\) 以及 \(\epsilon=0\) 时的结果;另说明已有非零历史后,当前梯度变为 0 时会发生什么。说明 \(\epsilon\) 的位置为何属于算法定义的一部分。
统一优化器接口。 仅使用
NumPy实现init_state与step,通过method参数支持gd、momentum、rmsprop和adam。其中,init_state(params, method)应根据模型参数的名称、维度和数据类型,为指定优化算法建立初始状态,例如已经完成的更新次数以及初值为 0 的一阶矩或二阶原点矩;step应接收当前模型参数、梯度、优化器状态和超参数,完成一次参数更新,并返回更新后的参数与状态。模型参数、梯度与优化器状态应分别保存;程序还应检查键、维度、数据类型、有限值和更新步数,保持输入参数的数据类型,并且不能在原位置修改传入的参数和状态。允许 \(\epsilon=0\) 以核验第 3 题,但此时若分母为 0,程序应明确报告错误;实际训练通常取 \(\epsilon>0\)。程序核验。 使用第 3 题的梯度序列和 \(\epsilon=0\) 逐步核验统一接口,再用随机向量和非方阵矩阵参数验证每个状态与参数同形。检查程序是否保持参数的数据类型、是否修改了输入对象,并为非法的键、维度、数据类型、有限值和更新步数编写测试。另构造能够发现以下错误的测试:Adam 在第一次更新时把步数错当成 0、漏做偏差修正、把逐元素平方写成矩阵乘法以及把 \(\epsilon\) 放到错误位置。
病态二次函数。 对 \(f(\theta_1,\theta_2)=\theta_1^2+100\theta_2^2\),预先确定同一起点、非学习率超参数、候选学习率集合、用于选择学习率的更新次数 \(K_{\mathrm{tune}}\) 和选择规则。每个候选学习率运行 \(K_{\mathrm{tune}}\) 次更新,排除出现无穷大或无效数值的运行;对每种算法选择第 \(K_{\mathrm{tune}}\) 步函数值最小的学习率,若结果相同,则选择较小者。确定学习率后重新从同一起点运行,画出等高线上的更新轨迹,并逐步记录函数值、梯度范数、更新范数 \(\lVert\btheta_t-\btheta_{t-1}\rVert_2\),以及首次达到 \(f(\btheta_t)\leq\tau\) 所需的实际运行时间;在给定预算内未达到阈值时,应明确记录。分别在更新次数相同和运行时间相同的条件下比较四种算法,并根据结果分析算法差异可能带来的影响。
在相同条件下比较小型网络的优化算法。 在固定的二分类数据划分、网络结构、批量顺序、初始参数和种子集合上比较四种优化器。除优化算法以及按统一规则选出的学习率外,保持其他条件相同;为每种算法仅用验证集从数量相同的候选学习率中进行选择,方案确定后只在测试集上评价一次。报告多个随机种子下的测试集准确率、对数损失、训练时间、达到给定验证损失所需时间和峰值内存;若某次运行没有达到目标损失,应记录为未达到,而不能将其从结果中删除。根据结果分析优化算法可能带来的影响。
学习率敏感性。 对四种算法分别考察一组对数间隔学习率,并确定其他超参数与训练预算。例如,可以用
np.logspace(-4, -1, 7)生成 \(10^{-4}\) 到 \(10^{-1}\) 之间的 7 个学习率,其近似值为 \(\{0.0001,0.000316,0.001,0.00316,0.01,0.0316,0.1\}\),相邻两个数的比值均约为 \(\sqrt{10}\)。分别画出学习率与最终验证损失、发散比例和达到目标损失所需时间的关系;没有达到目标的运行应单独标记,并报告每个学习率达到目标的比例。说明“自适应优化器”为什么仍然需要选择全局学习率。训练与预测效率。 选择一种神经网络,并通过增加隐藏层神经元数量得到几个参数量不同的模型,例如分别使用 32、128 和 512 个隐藏神经元。对于每个模型,分别使用梯度下降、Momentum、RMSprop 和 Adam 进行训练;在其他条件相同的情况下,记录四种优化器每次更新所需的时间、总训练时间、优化器状态占用的内存,以及达到指定验证指标所需的时间。训练完成后,将模型切换到预测状态,使用相同的数据类型、设备和批量大小,先进行预热,再重复测量各模型完成一次批量预测所需的时间;若使用异步计算设备,计时前后应等待设备完成计算。根据实验结果说明:优化器为什么会影响训练时间和训练内存,却通常不会改变结构相同的网络在预测阶段所需的运算。