1. 从物理直觉理解Momentum:它为什么比SGD更“聪明”?
想象一下,你正在一个山谷里推一个沉重的铁球下山。如果你每推一下,就立刻松手,然后重新找位置再推(这就像标准SGD),那么球可能会因为地面不平而左右摇摆,前进得又慢又费力。但如果你持续地推着它,让它自己滚动起来,它会越滚越快,即使遇到小的坑洼或缓坡,也能凭借惯性冲过去,更快地到达谷底。这个“惯性”,就是Momentum优化器的核心思想。
在深度学习的优化世界里,我们训练模型的目标是找到损失函数这个“地形”的最低点。标准随机梯度下降(SGD)就像那个“推一下,松一下手”的人。它只根据当前位置的梯度(坡度)来决定下一步的方向和步长。这带来了几个明显的问题:在陡峭的峡谷地形里,它容易在两侧来回震荡,走“之”字形路线,浪费大量计算;在平坦的平原上,梯度很小,它又走得慢吞吞,半天挪不动一步;而且,由于我们通常用小批量数据计算梯度,这个梯度本身带有噪声,SGD会跟着噪声“跳舞”,路径很不稳定。
Momentum的引入,就是为了解决这些问题。它给优化过程增加了一个“速度”变量。这个速度不是凭空产生的,而是历史梯度的累积。每次更新时,我们不是直接用当前的梯度,而是用这个“速度”来更新参数。速度的更新规则是:新速度 = 动量系数 * 旧速度 + 当前梯度。参数更新则是:新参数 = 旧参数 - 学习率 * 新速度。
这个简单的公式背后,是深刻的物理直觉。动量系数(通常设为0.9)就像地面的“摩擦系数”。如果摩擦系数是1,那就是完全光滑,速度永远不会衰减,这会导致优化过程失控;如果摩擦系数是0,那就完全没有惯性,退化成SGD。0.9是一个经验值,意味着保留90%的旧速度,只加入10%的新梯度信息。这样,优化方向就不再是短视的“当前梯度”,而是过去一段时间内梯度的“平均趋势”。当梯度方向一致时(比如在长长的下坡路上),速度会不断累积,越滚越快,实现加速;当梯度方向频繁变化时(比如在锯齿状的地形),正负梯度会在速度中相互抵消,从而平滑掉高频震荡,让路径更稳定。
我第一次在实战中体会到Momentum的威力,是在训练一个图像分类网络时。使用纯SGD,损失曲线下降得非常缓慢,而且像锯齿一样上下波动,训练了50个epoch,准确率还在原地踏步。当我给SGD加上momentum=0.9,并把学习率调低后,损失曲线几乎是“俯冲”式地下降,变得平滑了许多,同样的epoch数,准确率提升了近10个百分点。那一刻我才真正理解,这个小小的“惯性”机制,是如何让优化过程从“步履蹒跚”变得“健步如飞”的。
2. 理论与实践的鸿沟:那个容易被忽略的(1-β)因子
如果你翻阅一些经典的机器学习教材或早期的论文,可能会看到这样的Momentum公式:v_t = β * v_{t-1} + (1 - β) * g_t。这里多了一个(1-β)的因子,用来对当前梯度进行缩放,使得速度v_t实际上是梯度g_t的指数加权移动平均(EWA)。这个形式在理论分析上很优雅,因为它保证了当梯度稳定时,速度会收敛到梯度的真实均值,物理意义更清晰。
然而,当你兴冲冲地打开PyTorch或TensorFlow的官方文档,查看torch.optim.SGD或tf.keras.optimizers.SGD的源码时,你会惊讶地发现,它们的实现是:v_t = β * v_{t-1} + g_t。那个(1-β)因子不见了!这是框架的bug吗?绝对不是。这是深度学习框架在工程实践中的一个有意设计,也是很多初学者调参时踩的第一个大坑。
为什么主流框架要舍弃这个看似更“正确”的归一化因子呢?主要有几个工程上的考量。首先是为了实现的简洁性和历史兼容性。早期的神经网络库就是这么实现的,沿用下来可以保证大家训练的模型能够复现。其次,当β非常接近1(比如0.99)时,(1-β)会变成一个非常小的数(0.01),在数值计算上可能会带来精度问题。最重要的是,它把学习率和动量系数这两个超参数的作用解耦了。在没有(1-β)的版本里,你可以独立地调节学习率(控制每一步的步长)和动量系数(控制历史信息的保留程度),调参的逻辑更清晰。
但这带来了一个关键问题:两种形式是等价的吗?答案是肯定的,但它们之间差了一个缩放因子。具体来说,PyTorch/TensorFlow的实现版本,其“有效学习率”被放大了大约1/(1-β)倍。当β=0.9时,1/(1-0.9) = 10。这意味着,如果你直接照搬理论公式中的学习率(比如0.01)到PyTorch中,并设置momentum=0.9,那么实际更新步长会是理论预期的10倍大!这很容易导致优化过程不稳定、剧烈震荡甚至发散。
我见过不少朋友抱怨:“我加了Momentum,怎么模型反而炸了(损失变成NaN)?”或


434

被折叠的 条评论
为什么被折叠?



