深度学习中的优化器之SGD with Hessian Averaging算法原理与实现细节
题目描述:
在深度学习的优化过程中,随机梯度下降(SGD)是广泛使用的方法,但它在处理非光滑、高曲率或噪声敏感的问题时,可能收敛缓慢或不稳定。一种改进方向是引入二阶信息(Hessian矩阵)的近似,以调整更新方向。然而,直接计算Hessian矩阵在深度学习里计算开销巨大。SGD with Hessian Averaging算法旨在通过平均历史梯度外积来近似Hessian矩阵的逆,从而在几乎不增加计算成本的前提下,模拟类似牛顿法的二阶优化效果。本题目将详细解释该算法的动机、数学原理、迭代步骤及实现细节。
解题过程循序渐进讲解
步骤1:理解问题的本质与动机
在深度学习中,损失函数 \(L(\theta)\) 的优化通常使用一阶梯度信息。标准的SGD更新规则为:
\[\theta_{t+1} = \theta_t - \eta \nabla L(\theta_t) \]
其中 \(\eta\) 是学习率。然而,梯度只包含一阶变化信息,如果损失函数曲面在不同方向曲率差异大(例如峡谷状),SGD会沿陡峭方向震荡,收敛缓慢。
- 牛顿法利用了Hessian矩阵 \(H(\theta)\)(二阶导数矩阵)来调整更新步长:
\[ \theta_{t+1} = \theta_t - H^{-1} \nabla L(\theta_t) \]
它能自动在曲率小的方向走大步,曲率大的方向走小步,收敛更快。但问题在于:
- 计算 \(H\) 需要 \(O(n^2)\) 内存(n为参数量),深度学习模型参数动辄数百万,不可行。
- 计算 \(H^{-1}\) 需要 \(O(n^3)\) 计算量,更不可能。
- 因此,我们需要一种低代价近似 \(H^{-1}\) 的方法。SGD with Hessian Averaging 的核心思想是:用历史梯度外积的平均来估计Hessian矩阵,再利用在线平均技巧避免存储所有历史梯度。
步骤2:从梯度外积到Hessian近似
对于一个平滑函数,Hessian矩阵是二阶导数。但在随机优化中,我们可以利用以下观察:
- 在损失函数的极小点附近,梯度服从均值为0的分布,其协方差矩阵与Hessian矩阵有一定关系。
- 更直接的思路:考虑梯度 \(g_t = \nabla L(\theta_t)\),其外积 \(g_t g_t^T\) 的期望在一定条件下可关联到Hessian(例如,当损失为平方误差时)。但更常用的是采用经验费雪信息矩阵(Empirical Fisher) 作为近似:
\[ F_t = \frac{1}{t} \sum_{i=1}^t g_i g_i^T \]
在一定的正则条件下,当梯度噪声均值为0时,\(F_t\) 趋近于Hessian的平均。
- 但直接存储 \(F_t\) 仍需要 \(O(n^2)\) 内存。为了压缩,算法通常采用对角近似或低秩近似。这里我们重点讲对角近似(即假设Hessian是对角矩阵),这在实践中常用且有效。
步骤3:对角Hessian平均的迭代公式
我们维护一个向量 \(h_t\)(维度与参数 \(\theta\) 相同),作为对角Hessian近似的运行平均。迭代过程:
- 在每一步 t,计算当前小批量梯度 \(g_t\)。
- 更新对角Hessian近似 \(h_t\) 为历史梯度平方的指数移动平均(EMA):
\[ h_t = \beta h_{t-1} + (1-\beta) (g_t \odot g_t) \]
其中 \(\odot\) 表示逐元素乘法(即 \(g_t^2\) ), \(\beta \in (0,1)\) 是平均系数(通常取0.9~0.99)。这相当于近似对角Hessian矩阵 \(H_t\) 的第 j 个对角元为 \(h_{t,j}\)。
3. 用 \(h_t\) 调整更新步长:
\[ \theta_{t+1} = \theta_t - \eta \frac{g_t}{\sqrt{h_t} + \epsilon} \]
这里 \(\epsilon\) 是为数值稳定加的小常数(如1e-8)。
这形式看起来很像RMSprop或AdaGrad,但注意它们的目的不同:
- AdaGrad/RMSprop 是为了自适应调整学习率,使每个参数更新幅度平衡。
- Hessian Averaging 的目标是模拟牛顿法,用 \(h_t\) 估计曲率,因此理论上 \(h_t\) 应逼近二阶信息,而不仅仅是梯度平方的平均。但实践中,由于深度学习的高维非凸性,直接用梯度平方平均作为曲率估计是一种有效启发式。
步骤4:算法伪代码
输入:初始参数 θ₀,学习率 η,平均系数 β,小批量大小 m,常数 ε
初始化:h₀ = 0
for t = 1, 2, ... do
采样小批量样本 B_t
计算梯度 g_t = ∇L(θ_{t-1}; B_t)
更新对角Hessian近似:h_t = β h_{t-1} + (1-β) (g_t ⊙ g_t)
计算调整后的更新:Δθ_t = -η * g_t / (sqrt(h_t) + ε)
更新参数:θ_t = θ_{t-1} + Δθ_t
end for
步骤5:与现有优化器的联系
- 若 β 接近1,h_t 是长期梯度平方的加权平均,类似RMSprop。
- 若 β 很小,h_t 更关注近期梯度,能快速适应曲率变化。
- 与AdaHessian(2019年提出的显式Hessian近似优化器)的区别:AdaHessian 通过Hutchinson随机估计计算Hessian对角,计算成本更高但更精确;而SGD with Hessian Averaging 仅用梯度外积,成本极低。
步骤6:实现细节与注意事项
- 初始化 h₀:通常设为0,避免初始步长过大。
- 偏差校正:由于 h_t 是EMA,初期 h_t 偏向0。可做偏差校正:
\[ \hat{h}_t = \frac{h_t}{1 - \beta^t} \]
然后用 \(\hat{h}_t\) 代替 h_t 在更新中使用,这与Adam的偏差校正类似。
3. 与动量结合:可额外加入一阶动量(动量法),形成类似Adam的变体,但此时 h_t 仍作为二阶矩估计。
4. 适用场景:适合损失曲面曲率变化明显的问题,如训练循环神经网络或Transformer的后期微调。但对噪声大的任务,梯度平方平均可能引入过估计,需调小 β。
步骤7:代码示例(PyTorch风格)
import torch
def sgd_hessian_averaging(params, lr=0.01, beta=0.9, eps=1e-8):
# params: 模型参数列表
# 初始化h字典
h = {p: torch.zeros_like(p.data) for p in params}
for p in params:
if p.grad is None:
continue
grad = p.grad.data
# 更新对角Hessian近似
h[p] = beta * h[p] + (1 - beta) * (grad * grad)
# 计算调整后的更新
denom = torch.sqrt(h[p]) + eps
p.data.addcdiv_(-lr, grad, denom)
总结:
SGD with Hessian Averaging 通过历史梯度平方的平均来近似对角Hessian,以调整每个参数方向的步长,模拟牛顿法的缩放效果。它在几乎不增加计算负担的前提下,提供了比普通SGD更稳定的收敛,尤其适用于曲率变化较大的优化问题。