ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

优化算法入门:从梯度下降到学习率避坑指南

优化算法入门:从梯度下降到学习率避坑指南

优化算法是我最近大半年一直在啃的一个方向,说“啃”一点都不夸张。市面上讲它的资料,要么飘在天上全是公式推导,要么一头扎进代码里全是调参,很少人把“为什么这么做”讲明白。这个系列,我打算按自己的理解重新梳理一遍,第一期先把地基打牢:优化算法到底在优化什么、主流方法怎么分类、入门应该先抓住哪些核心直觉,最后把新手最容易踩的几个坑提前摆到桌面上。无论你是做机器学习、运筹规划、自动控制,还是单纯写代码时遇到“怎么调都不收敛”的问题,这期内容都值得你花十分钟读完。

1. 优化算法到底在优化什么

1.1 先从一个生活化的场景说起

想象你要去山顶看日出,可手里只有一张等高线地图,而且山里起了大雾,你连自己在哪都看不清。你唯一能做的,是感受脚下地面的倾斜方向,然后沿着“最陡的上坡方向”迈一步,再感受一下,再迈一步。这个摸着石头往上走的过程,本质上就是优化算法的缩影。

如果换一个方向,你并不是想爬山,而是想下到山谷最低处的水潭边,那就变成“沿着最陡的下坡方向走”。机器学习和深度学习中绝大多数问题,都属于这种“找谷底”的任务:我们定义一个损失函数,比如预测值和真实值之间的差距,优化算法要做的事,就是不断调整模型参数,让这个损失函数的取值尽量小。

把这个故事里的元素拆开,会发现任何一个优化问题都包含三个关键角色:决策变量、目标函数、约束条件。理解了这三个角色,再去读任何一篇优化算法论文,都不会再一头雾水。

1.2 优化问题的数学“三件套”

所谓决策变量,就是你可以主动调整的东西。在训练一个线性回归模型时,决策变量是权重和偏置;在给某公司做排产计划时,决策变量可能是每台机器上分配多少订单;在自动驾驶轨迹规划里,决策变量可能是车辆每一个时刻的方向盘转角和加速度。一句话,它就是你手里能打的牌。

目标函数,是你用来评价“这手牌好不好”的尺子。机器学习里叫损失函数或代价函数,运筹学里叫目标函数,控制领域叫性能指标。名字不同,本质一样:输入一组决策变量,输出一个数值,这个数值越小越好(或越大越好)。

约束条件则是规则边界,比如工厂的原材料总量是有限的,机器的加工时间是有上限的,配送车的载重不能超过额定值。有约束的优化问题,难度会凭空高一大截,因为你要找的最优解,必须同时满足所有限制条件。

提示:我见过不少初学者一上来就抱着公式推导看,效率很低。不如先拿到一个具体问题,问自己三个问题:我想调的是什么?用什么数字衡量好坏?有哪些限制不能越界?把这三件事理清楚,优化问题才真正在你脑子里立起来。

2. 主流优化算法全景:一张地图看懂分类

2.1 无约束与有约束:两条完全不同的路线

优化算法的第一个粗分维度,就是看问题里面有没有约束条件。无约束优化最简单,目标函数定义了整个空间,从任意起点出发都可以自由搜索。这类算法的思路通常围绕“往哪个方向走、走多远”展开,代表方法包括最速下降法、牛顿法、拟牛顿法、共轭梯度法等。

有约束优化就要麻烦得多。因为可行域不再是整个空间,你不仅要把目标函数降下来,还得保证每一步都别越界。经典的处理思路有两派:一派是罚函数法,把违反约束的代价加到目标函数上,把有约束问题“伪装”成无约束问题来解;另一派是投影法,每走一步就把点投影回可行域内部。

不少做机器学习的朋友可能觉得:“我天天用梯度下降,根本没碰到过约束啊。”确实,深度学习中大多数任务是无约束的,但如果你去做资源调度、投资组合、供应链优化这类传统运筹问题,约束条件就会变成主角,这时仅靠梯度下降是远远不够的。

2.2 一阶、二阶与零阶方法怎么选

第二个重要的分类维度,是算法用到了目标函数的几阶信息。这里我尽量说得直观一些。

零阶方法只依赖目标函数本身的取值,不需要任何导数。典型代表有随机搜索、模拟退火、遗传算法、贝叶斯优化。这类方法的优点是适应性极强,哪怕目标函数压根不可导,甚至是个黑盒子,也能用;缺点是收敛通常较慢,适合决策变量不多、每次评估成本相对可接受的场景,比如超参数搜索、电路参数设计。

一阶方法则利用了一阶导数,也就是梯度。梯度告诉我们眼前这个曲面“最陡的上坡方向”,取反就是“最陡的下坡方向”。最典型的自然是梯度下降法,以及动量法、AdaGrad、RMSProp、Adam等一系列改进版本。它是深度学习训练的事实标准,因为深度模型的参数量动辄百万、千万,用二阶方法算出完整二阶导数矩阵根本不现实,而一阶方法只需要算梯度和少量额外量,开销能控制在可接受范围。

二阶方法进一步使用二阶导数信息,最典型的就是牛顿法。它相当于是“不仅看脚下最陡的方向,还估算出前方坡度变化的趋势”,因此能用更少的迭代步数走到收敛点。代价是每一步的计算和存储开销大幅上升,所以实际工程中大家更常用拟牛顿法(比如BFGS、L-BFGS)做近似。对于中小规模、精度要求高的优化问题,二阶方法依然是很有竞争力的选择。

以我自己的经验,选类型的顺序应该是:先看能不能算梯度,能算就走一阶路线;算不了梯度再考虑零阶方法。至于二阶方法,先学会原理,保持手中有一张能打的牌,等你遇到中小规模高精度问题时,它会派上大用场。

3. 入门第一课:梯度下降及其实战细节

3.1 梯度下降的直觉与公式

梯度下降是几乎所有现代优化算法的基石,它做的事情可以压缩成一个非常简洁的更新式:

θ_new = θ_old - η × ∇f(θ_old)

其中θ表示参数向量,η是学习率,∇f(θ_old)是目标函数在旧参数位置的梯度。把这两者的乘积从旧参数上减掉,等价于沿着最陡下坡方向走一步。η控制这个步子迈多大:η越大,每一步试探的距离越远;η越小,走得越谨慎。

举个例子,假设损失函数是f(θ) = (θ - 3)²,梯度是2(θ - 3)。如果初始θ = 0,η取0.1,第一步会更新到θ_new = 0 - 0.1 × (-6) = 0.6,损失从9降到5.76。第二步到θ = 1.08,损失是3.69。随着越来越接近θ = 3,梯度绝对值越来越小,步子也会变得越来越小,最终慢慢逼近最优点。

这个例子虽然简单,却揭示了梯度下降的天然特性:越接近极值点,收敛越慢。在大规模深度学习中,这种“临近收敛时的慢速爬行”是一个被反复吐槽的话题,后面我会讲到现代优化器如何缓解这个问题。

3.2 学习率设置的三个实用技巧

学习率是整个优化过程里最敏感的一个超参数,很多人训练不收敛,一半以上的原因都是学习率设得不对劲。我根据自己的实测经验,分享三个可直接落地的技巧。

第一,用学习率范围测试找到大致量级。随便给一个深度学习模型,先跑几个非常短的训练循环,比如每轮只迭代几十步,观察损失曲线。如果损失震荡、甚至直接发散成NaN,说明学习率过大;如果损失极缓慢下降,几乎原地踏步,说明学习率过小。反复试探几个量级,通常能很快把合理范围缩小到个位数倍差距以内。

第二,配合学习率衰减策略使用。固定学习率最大的问题是:初期步长不够大,容易陷入陡峭却劣质的区域;后期步长不够小,又会在最优点附近来回震荡。所以实际训练中我更常用指数衰减、余弦退火或ReduceLROnPlateau这类自适应衰减策略。简单说就是:让模型在前期快速探索,在后期精细打磨。

第三,不同参数组可以分开设置学习率。比如在神经网络里,对偏置项、BatchNorm层的缩放系数,以及对预训练好的底层特征层,常用的做法是使用比主干网络更小的学习率。这个细节一开始没有统一标准,但实测下来能有效避免底层特征被新任务破坏。你可以把学习率想象成人走路的速度,遇到熟悉的平坦路面可以迈开大步,遇到陌生的碎石路段就得放慢脚步,一个道理。

注意:学习率并不是唯一需要看的参数,但它优先于大多数其他超参数。我始终建议先把学习率这条线调通,再动模型结构、数据增强这些。否则多个变量同时变,出了问题根本没法定位。

4. 跳出局部极小:现代优化器带来的启发

4.1 动量与自适应学习率

梯度下降虽然好用,但它在实际训练里有两个明显弱点:一是容易陷入局部极小点或鞍点;二是在某些方向上震荡很剧烈。现代优化器的改进思路,正是在这几个痛点上下功夫。

动量法,简单来说就是给梯度加上“惯性”。梯度的物理含义是当前的受力方向,动量法则记录了过去一段时间的速度累积,让每次更新不只是看当前的梯度,还要考虑之前参数变化的方向。想象一个小球从山坡滚下来,它经过小坑时不会立刻停住,而是借着惯性继续向前冲。这样一来,模型穿越平坦区域和微小局部极小点的能力会明显变强。

自适应学习率方法,比如AdaGrad、RMSProp和Adam,则更激进:它们会为每个参数单独维护一个学习率。经常接收到大梯度的参数,学习率会自动变小,避免震荡;一直很少更新的参数,学习率会相对变大,防止训练停滞。今天深度学习里用得最多的Adam及其变体,基本就是“动量 + 自适应学习率”的组合。

但是,Adam不是万能的。它虽然在训练前期收敛很快,最终精度有时候却不如带衰减的SGD。我在实际项目里见过不少这样的例子:同一个任务,Adam跑了几十个epoch就压到一个不错的水平,但换用带Momentum的SGD,再多跑一段时间,验证集精度反而能反超。所以很多资深工程师的做法是:先用Adam快速找到一个不错的参数区域,再切换为SGD做最后的精调。

4.2 关于“全局最优”的正确心态

很多刚接触优化算法的同学,最喜欢纠结一个问题:这个算法能不能找到全局最优解?

我的回答可能要让一部分人失望:在绝大多数非凸问题里,不存在一个能够高效保证全局最优的通用算法。深度学习的损失面极度复杂,我们甚至连“模型到底在怎样的损失面上训练”都很难完全讲清楚。这种情况下,如果还执着于“必须找到全局最优才罢休”,项目根本没法推进。

业界普遍接受的姿态是:我们追求的是“足够好”的最小值,而不是数学证明里的“全局最小”。能不能得到一个泛化性能优秀的模型,比是不是某个损失面上的理论全局最低点重要得多。

这其实也解释了为什么初始化、数据顺序、随机种子等看似琐碎的因素会影响训练结果。因为不同的起点,会把优化过程引导到不同的局部区域,这些区域对应的模型质量可能差别很大。于是实践中就有了多种子训练、选验证集最优模型、模型集成等手段。理解这一点后,你会对那些炼丹操作产生一种“原来如此”的通透感。

5. 避坑指南:五个新手最容易踩的坑

5.1 坑一:在错误的地方做归一化

数据归一化几乎和优化算法如影随形。如果输入特征尺度差异巨大,比如一个特征在0到1之间,另一个特征在0到10000之间,损失函数的等高线会变成狭长的椭圆形,这时候梯度下降会走出一条极度震荡的锯齿形路线,收敛极慢。

正确做法是在数据进入模型之前,对每个特征做标准化,比如减均值除以标准差。但要注意,测试阶段也必须使用训练集上计算出的均值和标准差,而不是重新计算测试集的统计量,否则会引入数据泄露,评估结果会失真。我见过有人在这个细节上吃亏:训练集归一化做得漂漂亮亮,测试时顺手又算了一遍统计量,结果模型效果暴跌,排查半天才发现是归一化统计量不一致。

5.2 坑二:只看训练集不看验证集

优化算法负责降低训练损失,但训练损失低绝不等于模型真的好。如果只在训练集上观察损失,你会看到的必然是一条“越来越好”的曲线,但它可能掩盖了严重的过拟合。

正确姿势是始终把训练集损失和验证集损失放在同一张图里看。两者同步下降,说明模型还在正常学习;训练损失继续降但验证损失拐头上升,就是典型的过拟合信号,这时候应当考虑正则化、早停、数据增强等手段。这是一条几乎所有优化教程都会提、但很多人实际训练时就是不照做的原则。

5.3 坑三:不监控梯度

训练不收敛时,绝大多数人第一反应是看损失曲线。但损失曲线是个“结果指标”,它没法告诉你是网络结构出了问题,还是学习率设错了,还是数据管道本身就有bug。这时候,直接检查梯度的范数是非常有效的排查手段。

你可以每隔一段时间打印一次梯度的L2范数。如果梯度范数几乎为零,但损失还是高位横盘,大概率是梯度消失了,网络的学习濒临停滞;如果梯度范数大得离谱,甚至出现NaN,大概率是梯度爆炸。两者对应的调参方向完全不同,前者需要调整初始化、增加残差连接、换激活函数,后者需要降低学习率、做梯度裁剪。不监控梯度,就只能靠猜,效率低好几倍。

5.4 坑四:用一个固定学习率走到底

有些项目为了省事,直接用一个学习率硬着头皮从头训到尾。结果要么前期训练太慢,要么后期一直震荡,最终模型精度始终上不去。这种情况被称为“使用固定学习率导致的次优收敛点”。

我的建议是优先使用带衰减的调度方式。最简单的做法是每隔一定epoch数把学习率乘以一个衰减因子,比如0.1;稍微复杂一点可以用余弦退火调度;如果连调度都不想写,直接用PyTorch里的ReduceLROnPlateau,当验证损失连续N个epoch不下降时自动降低学习率,这也是一个非常省心的方案。

5.5 坑五:盲目照搬参数组合

网上有很多开源模型的超参数组合,什么batch size、学习率、优化器、权重衰减一整套摆在那里。照搬一时爽,迁移火葬场。因为模型结构、数据集、任务类型只要有一处不同,最优超参数组合就会跟着变。batch size变了,学习率的合理范围也会变;数据变多或变少,训练轮数、学习率衰减节奏都要重调。

正确的做法是拿别人的参数当作起始点,然后围绕自己的数据和模型做小范围搜索。我习惯先固定模型结构,用一个小规模子集把优化器、学习率、batch size这组核心参数跑出量产区间,再放到全量数据上精调。这个流程虽然多花一些时间,但它能让你对每个参数的作用形成自己的体感,比盲目拷贝别人参数可靠得多。

6. 学习路径建议与自测清单

6.1 三个阶段的成长路线

优化算法这个方向,光看论文和博客是学不会的,必须配合亲手实验。我按照自己的学习心得,把路径拆成三个阶段。

第一阶段,掌握基础原理并手动实现。不要一开始就用现成的优化器,而是自己写一个朴素的批量梯度下降,再写一个带动量的版本,跑在一个二维凸函数上,观察参数点和损失值的轨迹。这一步能帮你把“更新公式到底在做什么”从抽象符号变成直觉。

第二阶段,在真实任务上对比不同算法。选一个规模适中的机器学习任务,分别在SGD、Momentum、AdaGrad、RMSProp、Adam下做实验,记录损失下降曲线和最终验证精度。通过这个对比,你会亲眼看到不同算法的收敛速度和最终精度的差异,也会真正理解“没有免费的午餐”这句话。

第三阶段,探索问题结构对算法选择的影响。这一步开始触及优化算法的深层逻辑:比如条件数很大时,为什么牛顿方法比梯度下降更稳?大规模稀疏特征下,为什么自适应学习率方法更好用?这些问题未必都能从实验中得到确定答案,但带着问题去查阅资料,学习效率远高于大海捞针式的泛读。

6.2 一份拿来即用的自测清单

学完这一期内容,你可以拿下面几个问题检验自己是否真的建立了初步框架。如果你能不看资料就回答出要点,说明基础已经打牢,可以继续看第二期。

  • 什么是决策变量、目标函数、约束条件?给你的实际问题分别举例。
  • 零阶、一阶、二阶优化方法各有什么取舍,分别适用于什么场景?
  • 梯度下降的更新公式是什么?学习率太大会怎样,太小又会怎样?
  • 动量法和自适应学习率分别解决了什么痛点?
  • 在深度学习里,“找到全局最优”为什么不是首要目标?
  • 训练时为什么要同时关注训练损失和验证损失?
  • 梯度范数接近零和梯度范数发散分别说明什么,应该怎么调?

如果你现在打开手头的机器学习项目,把上面这些问题对照一遍,大概率会发现自己以前很多“差不多就行”的设定,其实都有更合理的方案。这就是优化算法学习的价值所在:它不是让你多记几个公式,而是让你对“模型为什么能学到东西”建立自己的判断力。

我自己在这一路学下来的最大感受是:优化算法不是一个“背下来就能用”的知识点,而是一套极其依赖场景的工程方法论。同一个算法在某个任务上效果惊艳,换一个任务可能表现平平,这不代表算法有问题,而是说明你需要更仔细地理解问题结构。每当你发现自己调参调到怀疑人生,不妨退一步,从目标函数和梯度的角度重新审视问题,往往能找到比“再调一个epoch”更本质的突破口。希望这一期内容能让你少走一些我走过的弯路,下一篇我会开始深入讲一阶优化方法的家族谱系,到时候我们再见。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进