机器学习中的优化方法
哎呀,说到机器学习优化,我第一次上手就差点被气哭。
当时在跑一个简单的线性回归,心想梯度下降不就是沿着坡往下溜嘛,多简单的事儿!结果呢?学习率设大了一丢丢,loss直接飞到天上,跟窜天猴似的!设小了吧,跑了一个小时,loss还在原地打转,离最优解八百里远!你说气不气人?
后来我才恍然大悟——优化器这玩意儿,跟开车一个道理:光知道要往下走没用,关键是怎么走!
先搞清楚我们到底要干嘛
其实把梯度下降掰开看,就三个问题:往哪儿走?走多大?走多久?
导数、偏导、梯度这些概念,当初也绕了我一阵。简单粗暴地说:一个变量叫导数,多个变量叫偏导,把偏导攒成一个向量就是梯度。梯度指向函数增长最快的方向——所以要让参数变小,你就顺着梯度的反方向一路狂奔。
逻辑没毛病吧?但你一动手就发现了——这路它根本不是平的啊!
今天我就想跟你聊聊,这帮优化方法是怎么一步步从“裸奔下山”进化到“自动驾驶”的。
第一步:梯度下降,最原始的版本
最早用的就是批量梯度下降,每次把整个训练集拎出来算梯度。
数据量小还行,一步一个脚印,稳。但有一回我训一个模型,训练集五十万条!每迭代一次,服务器风扇直接起飞,跟直升机似的!等梯度等到快睡着,你看我像搞科研的吗?我像在等下班!
后来换随机梯度下降(SGD),一次只抽一个样本算梯度。这下快了,真的飞起来!但loss曲线跳得呀——跟心电图上房颤一样,根本不收敛!
直到小批量梯度下降出现,batch size设个32或64。嘿,这才像话嘛!计算量能接受,稳定性也凑合。现在回头看,这个“折中”的思路,贯穿了整个优化方法的发展史——没有完美,只有够用。
动量的出现:给下山的人加个惯性
SGD虽然快,但有个毛病:容易在山谷两边来回震荡。我印象特别深,有一次训一个简单的MLP,loss在最优解附近反复横跳,就是下不去!
后来设momentum=0.9,你猜怎么着?
效果立竿见影!就好像下山的人被推了一把,带着惯性冲过小坑,滚进去还能弹出来。这个“惯性”后来成了几乎所有高级优化器的核心,你说重不重要!
很多朋友可能不知道,在CV任务里,SGD + Momentum 仍然是很多比赛的王者。我亲测过,ResNet50训ImageNet那个规模,SGD配上momentum和step decay,精度真能跟Adam掰手腕。代价就是你得多花时间调学习率——Adam自适应确实省心,但省心的东西总得付出点别的代价,对吧?
自适应学习率:从手动挡到自动挡
Adam出现之前,有两个铺路的:AdaGrad 和 RMSProp。
AdaGrad的想法很聪明:每个参数给不同的学习率,频繁更新的步子小点,稀疏的步子大点。听着是挺美,但你实际用就发现——它在凸问题上还行,在神经网络里呢?到了后期学习率直接衰减到零,模型干脆就不学了!我第一次试完就放弃了,什么玩意儿嘛!
RMSProp改良了这一点,加了个指数移动平均,学习率不会单调衰减了。在RNN这类任务上效果不错,但还不够惊艳。
然后Adam来了!把动量和RMSProp的思路揉在一起!
第一次用Adam,我差点感动哭了——基本不用调学习率,默认1e-3就能出不错的结果!对当时的我来说,简直是革命啊!终于不用整天蹲在屏幕前盯着loss曲线,看到拐点就手忙脚乱改学习率了!
但后来我发现Adam有个坑:权重衰减(weight decay)的实现有问题。标准的L2正则跟Adam结合效果不好,泛化性能有时候反而不如SGD。所以后来大神们搞出了AdamW,把权重衰减和自适应学习率分离开。
现在的实操惯例是啥? 新任务从AdamW起步,学习率1e-3,weight decay 1e-2,再配上Warmup加Cosine Annealing。这一套组合已经成了大模型时代的标配。
如果你是Transformer的铁粉,下面这句话你一定不陌生——
训练刚开始那几步,必须给学习率加一个warmup阶段!
为什么?你想想,刚初始化的时候,参数全是随机数,梯度的方向乱得跟无头苍蝇似的。这时候一脚大油门踩下去,模型直接飞到某个坏区域,后面怎么救都回不来!Warmup就是先让模型用小碎步在附近探探路,方向感稳了再踩油门冲。
学习率调度:你不能一直用同样的速度开车
很多教程讲完优化器就收工了,但我跟你说,调度策略同样重要!优化器选对了,schedule选错照样训不好。
我干过最蠢的事:固定学习率从头训到尾。AdamW训100个epoch,前20个epoch收敛还行,后面呢?原地踏步,loss纹丝不动!白白浪费了多少算力啊!
后来换成Step Decay,每30个epoch除以10。简单粗暴吧?但问题在于衰减时刻是硬定的。如果你第30个epoch还没收敛到平台期,衰减就过早了。我有个任务设step=20,结果第15个epoch正好要进入稳定状态,被学习率一拉小,反而震荡起来了。你说气不气?
Cosine Annealing是我现在用得最多的。学习率从最大值平滑下降,没有“断崖”。实测在好几个分类任务上,最终准确率比Step Decay高了0.5~1个点。再配上Warmup,基本成了官方标配。
说一个Warmup的详细配置:我之前训练一个BERT小模型,总步数10000,warmup设了1000步,也就是10%。前1000步学习率从0线性上升到3e-4,之后按余弦退火降到1e-6。如果我用固定学习率3e-4从头训?loss直接飞到NaN,连救的机会都没有!
大batch训练时warmup几乎是必须的。Google那篇BERT原始论文里,batch size 256、warmup 10000步,这个比例在当时算保守了。现在很多做法直接把warmup比例设在总步数的5%到10%。
还有一种叫OneCycleLR,比较激进:学习率先增大后减小,允许中间阶段用更大的学习率。我在一个细粒度分类任务上试过,收敛速度确实快了,但需要仔细调参,不然容易在峰值那一下崩掉——就像开车开到悬崖边才发现刹车不好使。
关于凸优化,我说得直白点
你训练神经网络,基本上是在非凸的地形上跑。那为什么还要了解凸优化?
因为很多经典模型——线性回归、逻辑回归、SVM——它们是凸的。凸函数的好处是:局部最优就是全局最优!你不用担心掉进某个坑里就出不来了。
我实际感受最深的是,如果问题本身是凸的,随机搜索几下就能找到不错的结果。而神经网络这种非凸地形,你反复跑五次可能得到五个不同的结果,方差大得吓人!理解这个差异,会让你更清楚:调参不能光靠运气,你得知道你的地图是什么样的。
当梯度不够用的时候
前面聊的方法都依赖梯度。但有些问题,梯度压根算不出来,或者算出来也白搭——比如超参数搜索、网络架构搜索、复杂的工程设计问题。
这时候就要上启发式算法了:遗传算法、粒子群算法、模拟退火……它们不依赖梯度,靠随机性和自然界的智慧来探索空间。
我之前用贝叶斯优化调过一组CNN的超参数——学习率、dropout、滤波核大小——你猜怎么着?比手动调参效率高多了!框架自动帮你找下一组最有希望的值,不用像无头苍蝇一样瞎试。但话说回来,它也是随机搜索的升级版,算力消耗也不小。
从裸奔下山到自动驾驶,我们走了多远?
你想想,从批量梯度下降那种“扛着整个数据集一步一步挪”,到SGD的“疯跑”,再到小批量的“稳中求快”,加上动量的“惯性”,自适应学习率的“自动换挡”,学习率调度的“路况提醒”……每一步都是在解决上一个方法留下的坑。
到今天,你用AdamW + Warmup + Cosine Annealing,基本就是开着自动驾驶下山——路况好、体验顺、不操心。但开车的老司机都知道,再好的自动驾驶,你也要理解它什么时候可能失灵。
优化不仅是技术,更是智慧——知道什么时候加速,什么时候拐弯,什么时候停下来看看路。
这一点,用到学机器学习上,用到任何一个领域里,都一样!
读者评论 5