很多做模型训练的朋友应该都有过这种体会:同一个网络结构,别人跑出来收敛又快又稳,自己跑起来要么loss死活降不下去,要么直接起飞变成NaN。这时候大多数人第一反应是调学习率、换网络结构,但我这几年调模型的经验是——问题很可能出在你对优化器的理解和配置上。今天想聊的"Model-Optimizer",在业界通常指代两层意思:一是深度学习训练过程中负责更新权重的优化算法(比如SGD、Adam、AdamW),二是训练完成后对模型本身做精简压缩的优化工具链(比如剪枝、量化、蒸馏)。这两件事一个管"训得动",一个管"跑得快",共同决定了你的模型能不能真正落地。这篇文章会从优化器的底层原理讲起,结合我踩过的一些坑,把训练侧和推理侧的模型优化完整梳理一遍。不管你是刚入门的新手,还是已经被loss折磨了好几天的老手,这篇都应该能帮你少走不少弯路。
1. 先搞清楚优化器到底在干什么
1.1 优化器的核心逻辑就是"下山"
把训练模型这件事做一个生活化的类比:你站在一座漆黑的山顶,手里只有一张局部地图,任务是摸黑走到山脚下的最低点。每一步你只能感受到脚下的坡度(梯度),你要决定往哪个方向迈多大一步——这个"迈步决策器",就是优化器。
具体到数学层面,深度学习的训练目标是最小化损失函数 L(θ),θ 是模型的全部参数。每次迭代我们计算梯度 ∇L(θ),然后用某种规则更新参数:
θ_{t+1} = θ_t - η · update(∇L)
其中 η 是学习率,update(∇L) 这个变换就由不同的优化器来定义。SGD直接把梯度当作更新方向,Adam会额外估计梯度的一阶矩(动量)和二阶矩(梯度平方的指数移动平均),Per-parameter的自适应学习率就是这样来的。
我见过太多人把优化器当成一个黑盒:反正PyTorch里一行optimizer = torch.optim.Adam(model.parameters())就完事了。但实际训练中,优化器的每一个内部状态都在影响收敛路径。举个例子,Adam默认的 eps 是 1e-8,这个值在FP32训练中基本够用,但切到混合精度训练时,如果你的 eps 不跟着调整,梯度更新可能直接被精度截断吃掉,loss在一个平台期卡上几十个epoch纹丝不动。
1.2 从SGD到AdamW,优化器的演进到底解决了什么
SGD是最朴素的策略,它的更新公式是 θ ← θ - η·g。它的问题是:梯度方向在深谷区域会震荡,收敛慢;遇到鞍点容易卡住;对学习率极其敏感。但它的优势是泛化性好,而且理论性质清晰。
动量SGD(Momentum SGD)在SGD基础上引入了速度项 v_t = γ·v_{t-1} + η·g,相当于给下山的球加了惯性,能冲过一些小坑和平台区。Nesterov动量更进一步,让球先"看一眼"前方的坡度再迈步。
Adam的思路是给每个参数一个自适应学习率:更新幅度除以梯度二阶矩的平方根。这相当于给每个参数配了一把"自动调节的尺子"。梯度大的方向步子自动变小,梯度小的方向步子自动变大。这让Adam在稀疏梯度场景(比如NLP、embedding层)表现极好,也让它对初始学习率不那么敏感,所以在很多实际工程里,Adam几乎是默认选择。
但Adam有个著名的坑:它会把权重衰减(weight decay)实现成L2正则化,这导致带大weight decay训练时,Adam的泛化性能低于SGD。Ilya Loshchilov和Frank Hutter在ICLR 2019上发表了AdamW,把weight decay从梯度计算中解耦出来,直接在参数更新时减去 decay·θ。现在AdamW在预训练大模型、ViT、BERT类模型里基本是标配,原因就是它在large-batch和强正则化场景下能同时保证训练速度和泛化能力。
2. 常用优化器的对比与选型思路
2.1 一张表看懂主流优化器的脾气
| 优化器 | 核心参数 | 典型适用场景 | 学习率参考范围 | 主要风险 |
|---|---|---|---|---|
| SGD | lr, momentum | CV分类、目标检测、模型微调 | 0.01~0.1(配合warmup) | 收敛慢、对lr敏感 |
| Momentum SGD | lr, momentum(0.9) | 大多数CNN任务 | 0.01~0.1 | 超参多一个,需要调 |
| Adam | lr, betas(0.9, 0.999), eps(1e-8) | NLP、推荐系统、稀疏梯度 | 0.0001~0.001 | 泛化略差、可能不收敛 |
| AdamW | lr, weight_decay | Transformer、ViT、预训练 | 0.0001~0.0003 | 对大模型lr仍偏保守 |
| LAMB | lr, weight_decay | 超大batch训练(BERT预训练) | 0.001~0.01 | 工程实现复杂度高 |
| Lion | lr, beta1, beta2 | 大模型训练、扩散模型 | 0.0001~0.0003 | 较新,社区经验少 |
选优化器的第一原则不是"哪个最好",而是"哪个最不容易让你的训练失败"。做CV任务,batch size在256以内,SGD+momentum配合cosine衰减通常能拿到最好的泛化精度;做NLP或大规模稀疏模型,AdamW是更稳妥的选择;如果你的batch size大到上千甚至上万,那LAMB这类专门适配large-batch的优化器就该考虑了。
2.2 从训练稳定性反推优化器配置
我调模型时有个习惯:先看loss曲线的形态,反推优化器配置哪里出了问题。
如果loss持续不下降,先把优化器换成Adam配 lr=3e-4 试试。如果loss下降很快但验证集精度上不去,大概率是学习率太大或正则化不够,这时候可以切到SGD+momentum,把lr调到0.01~0.05,配合weight decay 5e-4,通常能改善泛化。如果loss直接变NaN,90%是学习率爆了,其余可能是数据里有异常值、梯度里有inf、或者混合精度训练的scale因子没处理好。
还有一个容易被忽略的点:优化器状态的内存开销。Adam要维护一阶矩和二阶矩两个状态,显存消耗约等于模型参数量的2倍(FP32下相当于每参数8字节)。一个7B参数的模型,光Adam状态就要占56GB显存,这还没算梯度和参数本身。所以做超大模型训练或微调时,很多人会用Adafactor或者8-bit Adam这类省内存的变体。我的建议是:如果是单卡微调7B模型,优先考虑Adafactor或bfloat16下的AdamW,这能直接决定你能不能塞进一张A100。
2.3 工程选型要看的不是指标而是收益
关于优化器选型,很多测试报告只对比最终精度,但我建议多关注两个工程指标:到达目标精度需要的epoch数,以及单step的耗时。SGD可能最终精度略高,但可能需要1.5倍的epoch才能到达同等精度;AdamW虽然单step略慢(多两个状态更新),但收敛步数少,训练的总电费和时间成本反而更低。从项目收益角度考虑,迭代速度快、好调参的AdamW经常是更划算的选择。
3. 学习率策略:比优化器本身更能影响结果
3.1 学习率到底怎么设,从哪里开始试
很多朋友喜欢固定一个学习率从头训到尾,我说句实在话:这会浪费掉你模型的很大一部分性能。学习率策略和优化器是配套的,在模型优化的整个链路里,它们俩必须一起设计。
标准做法是先做学习率范围测试(learning rate range test):用一个较小的batch,让学习率从一个极小的值线性增长到一个较大的值,观察loss曲线的变化。loss开始明显下降的学习率就是你的下限,loss开始震荡或爆炸的位置就是你的上限,取它们之间的某个值作为初始学习率。
对于不同优化器,经验初始值也不一样:SGD系通常从0.01开始搜索,衰减到0.001甚至更低;Adam系从3e-4开始搜索,这也是为什么很多开源项目的默认lr都写着3e-4——它确实是在大量任务上比较稳的起点。
3.2 Warmup和Decay的搭配,要像做饭放盐一样讲究
为什么需要warmup?因为训练初期模型参数是随机的,梯度方向噪声很大,如果一上来就用大学习率,参数会被推到一个很糟糕的区域,后面再难拉回来。warmup的作用是先让优化器积累足够的梯度统计信息(尤其是对Adam这种自适应优化器),再逐步放开学习率。
我的习惯配置是:
- AdamW + 线性warmup + 余弦退火。warmup步数占总步数的5%~10%。这个组合在多数Transformer类任务上都很稳。
- SGD系 + 线性warmup + 多步衰减(step decay)。warmup约占总步数的3%~5%,分别在总步数的50%和75%处把学习率降到原来的1/10。
- 大模型预训练场景,warmup比例可以降到1%~3%,因为总步数太长,过度warmup反而浪费时间。
实际调参中,我踩过最亏的一个坑是:模型训练中期过拟合了,然后我去调weight decay,结果发现学习率衰减策略才是主要矛盾。后来我养成一个习惯,每次调整优化器配置时,先画一张完整的学习率曲线看全貌,再决定动哪个旋钮。
3.3 学习率调参时的几个细节
第一,不同层可以设置不同学习率。特别是做迁移学习微调时,backbone部分用较小的学习率(比如主干lr的0.1倍),新加的分类头用完整学习率。这就是常说的分层学习率(layer-wise learning rate)。用PyTorch实现时,把参数按名字分组,传给优化器就行。
第二,如果你在训练过程中发现后期loss波动很大,可以把betas里的beta2调大,比如从0.999调到0.9999,这样梯度二阶矩的估计更平稳,自适应学习率的波动也更小。这个技巧对长时间训练特别有用。
第三,要注意不同精度训练下的eps。混合精度训练(AMP)下,Adam的eps建议从1e-8调到1e-6或更高,因为FP16的精度有限,太小的eps会让更新值被舍入误差淹没。
4. 从训练侧到推理侧:Model-Optimizer的另一面
4.1 训练优化之外,部署前的压缩与加速
"Model-Optimizer"在工程语境下还有一个很常见的指向:对已经训练好的模型做推理优化,包括剪枝、量化和蒸馏。这跟训练优化器的关系是递进的——前者解决的是"怎么把模型训好",后者解决的是"怎么把模型用好"。
为什么需要推理侧优化?因为训练好的模型往往参数量大、计算量大,直接部署到手机、边缘设备或高并发服务上,延迟和吞吐都扛不住。比如一个ResNet50有约25M参数,FP32推理单张图在CPU上可能需要几十毫秒,这对实时视频分析场景就不够用。
我自己的经验是:先剪枝,再量化,最后做蒸馏兜底。剪枝可以砍掉冗余结构,量化能把计算量再压一个量级,如果压缩后精度损失太大,就用蒸馏把大模型的知识迁回小模型。
4.2 结构化剪枝与量化,实操时优先做哪些
结构化剪枝是直接去掉整个filter或者整个channel,好处是得到的模型是规则形状,可以用标准推理库加速。PyTorch的torch.nn.utils.prune可以帮你快速做局部剪枝实验,但生产环境更推荐用Intel OpenVINO的pot工具链或者NVIDIA的TensorRT做训练后剪枝。
量化的优先级我认为最高。INT8量化能把模型体积减小75%,推理速度提升2~4倍,在大部分任务上精度损失控制在1%以内。具体做法是先做校准(calibration),用一小批有代表性的数据统计每层激活值的分布,然后确定量化尺度。如果不做量化感知训练(QAT),直接训练后量化(PTQ)的话,需要关注哪些层是敏感层——通常是attention里的softmax层和最后的分类层,这些层建议保持FP16或FP32精度,混合精度推理的效果比全INT8要稳得多。
4.3 蒸馏救场,但不要空想一步到位
知识蒸馏是另一个思路:用一个大的teacher模型指导一个小student模型学习,让student去模仿teacher的输出分布。蒸馏的损失通常是学生与教师输出之间的KL散度加上一点ground truth的交叉熵。
这里想提醒一点:蒸馏不是"小模型就一定亏",很多时候小模型+好的蒸馏结果比直接训练同等大小的模型要强。我之前做过一个项目,把BERT-base蒸馏到6层的TinyBERT上,在相似度计算任务上精度只掉了0.8%,但推理延迟从12ms降到3ms。但注意,蒸馏需要搞定teacher模型的数据集访问和推理管线,工程成本不低,如果项目时间紧,建议先从量化和剪枝入手,蒸馏作为加餐。
5. 一套完整的模型优化实操流程
5.1 训练阶段的优化器配置实战
下面用PyTorch给大家一个可以直接套用的训练配置范例,以ImageNet分类训练为例:
import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR model = resnet50(num_classes=1000) train_loader = build_dataloader(train_data, batch_size=256) # 优化器配置:AdamW + weight decay optimizer = AdamW( model.parameters(), lr=3e-3, betas=(0.9, 0.999), eps=1e-6, # 混合精度训练时适当增大eps weight_decay=0.05 # 注意AdamW的weight decay是解耦的 ) # 一个周期学习率:先warmup再退火 total_steps = len(train_loader) * 100 # 100个epoch scheduler = OneCycleLR( optimizer, max_lr=3e-3, total_steps=total_steps, pct_start=0.05, # 前5%步数warmup anneal_strategy='cos' ) scaler = torch.cuda.amp.GradScaler() for epoch in range(100): for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs = model(images) loss = nn.CrossEntropyLoss()(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()OneCycleLR这种"先升后降"的学习率策略,配合AdamW,在CV任务上往往能达到接近SGD+cosine的精度,但收敛速度快得多。这里pct_start对应warmup比例,5%在100个epoch里大约是5个epoch的warmup,对大多数CNN任务够用了。
5.2 推理侧的量化压缩实战
用TensorRT或OpenVINO做PTQ量化的核心步骤其实是一致的。我以OpenVINO为例,因为它对CPU部署更友好,配置最简单:
- 准备校准数据集:从训练集中随机抽200~500张,覆盖不同类别和光照条件,最好能代表真实部署场景的数据分布。
- 创建量化配置:指定量化精度为INT8,针对敏感层设置白名单或黑名单。
- 执行量化:工具会自动统计每层激活值的min/max或百分位分布,决定scale和zero_point。
- 精度校验:用验证集跑一遍量化前后模型的精度对比。如果精度下降超过1%,考虑加入QAT或用敏感层回退策略。
实操中我遇到过很典型的问题:校准数据太少,量化后某些层的scale估不准,导致输出异常。后来我统一把校准数据量提高到500张以上,并且做一次shuffle,基本没有再出过这种问题。
5.3 优化前后的收益量化
这里列一个我之前做过的实际案例数据供大家参考:
| 优化阶段 | 模型体积 | 推理延迟(ms) | Top-1精度 |
|---|---|---|---|
| 原始FP32模型 | 98MB | 45.2 | 76.3% |
| 剪枝40%后 | 56MB | 31.8 | 75.1% |
| INT8量化后 | 27MB | 18.6 | 74.9% |
| 量化+精调后 | 27MB | 18.6 | 75.6% |
从表格可以清晰看到,剪枝加量化的组合能把模型体积压缩到原来的三分之一不到,延迟减半以上,精度损失控制在1个点以内,最后通过一个epoch的精调甚至能从75.1%回升到75.6%。这说明推理侧优化的收益是实打实的,不是纸面空谈。
6. 优化器相关的典型问题与排查思路
6.1 loss不降、震荡、爆炸的排查顺序
训练时遇到loss问题,别急着换模型结构,按照以下顺序排查优化器配置:
第一,确认学习率。如果是Adam系,先从3e-4开始,如果是SGD系,先从0.01开始。loss不降时,把学习率提高一个量级试试;loss震荡时,把学习率降低一个量级。
第二,确认优化器状态是否正确初始化。特别是从checkpoint恢复训练时,如果只恢复了模型权重而没恢复optimizer的state_dict,Adam的动量状态等于重新开始,这会造成训练初期剧烈波动。
第三,看梯度范数。在训练循环里加一行代码,定期打印total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)的返回值。如果梯度范数超过10倍于正常值,说明有梯度爆炸风险,需要梯度裁剪或调低学习率。
我写过一个快速诊断代码片段,贴在训练脚本里特别方便:
def log_grad_norm(model): total_norm = 0.0 for p in model.parameters(): if p.grad is not None: param_norm = p.grad.data.norm(2) total_norm += param_norm.item() ** 2 total_norm = total_norm ** 0.5 print(f"grad norm: {total_norm:.4f}")6.2 混合精度训练下的优化器问题
混合精度训练是现在大模型训练的标配,但它跟优化器的搭配有点讲究。AMP下梯度是FP16的,更新值会被舍入到FP16精度,如果学习率太小或eps太小,更新量可能直接被变为0。我之前在一篇BERT微调任务里遇到过:loss前500步下降正常,之后进入平台期一动不动,排查了一整天,最后发现是eps的问题——把eps从1e-8调到1e-6,loss立刻开始继续下降。
另外,AMP下optimizer的param_groups里保存的参数是FP32 master copy,梯度反传到FP32时会做一次cast。如果你用的是bnb的8-bit Adam,要确认它支持AMP的GradScaler,否则scale因子不会正确更新。
6.3 训练恢复与超参数持久化
最后提醒一个工程细节:每次训练任务的超参数(包括优化器类型、lr、weight decay、betas、epoch计划)一定要完整记录。不要觉得这麻烦——我见过太多人跑完一组实验,过两周想复现,结果翻遍log找不回当时的学习率和weight decay。推荐直接在训练脚本的开头用一个字典定义所有超参数,训练结束时把字典存成json,长期下来这就是你最宝贵的实验资产。
之前在一家公司做模型优化时,我们团队就是靠这种方式,把300多组实验的超参全部结构化管理,后续做任何模型调优都能在上千组历史数据里检索相似配置,开发效率直接翻倍。现在我自己无论做多大或多小的模型,都会坚持把优化器配置、学习率策略、数据增强策略完整地沉淀下来,这个习惯救了我很多次。
7. 一些更进阶的优化器技巧
7.1 参数分组与Layer-wise Learning Rate
处理复杂模型时,强制所有层共用同一个学习率其实并不合理。一般来说,靠近输入的层提取的是基础特征,更新应该更保守;靠近输出的层更接近目标任务,更新可以更激进。
在PyTorch里实现参数分组很简单:
optimizer = AdamW([ {'params': model.backbone.parameters(), 'lr': 1e-5}, {'params': model.classifier.parameters(), 'lr': 3e-4}, ], weight_decay=0.01)做迁移学习时,我通常把backbone的学习率设为主学习率的1/10到1/3,分类头用主学习率。如果做的是全量微调,backbone全部解冻,但epoch不超过20个,学习率不超过1e-4,这样能尽量保留预训练特征。
另一个进阶技巧是给bias和BatchNorm的scale参数单独设置weight_decay=0,因为它们的维度很小,施加L2正则化容易导致内部协变量偏移放大。视觉模型用这个配置,通常能提高1~2个点的稳定性。
7.2 大batch训练的优化器适配:从AdamW到LAMB
大batch训练是加快训练进度最直接的手段之一,但盲目加大batch size,模型精度往往会下降。这是因为学习率和batch size之间有联动:当batch size增大到原来的k倍,学习率也应相应改变。对SGD,线性缩放学习率是合理的(lr_new = lr_old × k);对Adam,经验上是按比例开根号(lr_new = lr_old × √k)更稳。
如果你要用数千级别的batch规模预训练模型,建议直接切换到LAMB(GPU版)或者LARS。LAMB的核心是把Adam的自适应更新扩展到了layer级别:对每一层计算一个信任比例,再乘以该层的全局学习率。这保证了大batch下每层更新的幅度不会因为梯度尺度差异被压制。
实际操作LAMB时,learning rate可以一下子设到0.001甚至更高,不需要像AdamW那样从3e-4起步。我当时用LAMB在1024的batch size下预训练BERT,收敛速度比AdamW快了接近一倍,精度还保持在同一水平。
7.3 优化器与loss scaling配合
在混合精度训练里,GradScaler是优化器最好的搭档。PyTorch的torch.cuda.amp.GradScaler默认策略是:当连续若干步没有出现inf梯度时,scale因子缓慢增大;一旦出现inf,立即回退。优化器配置要配合这个机制——如果打开了GradScaler,就不需要再做额外的梯度裁剪(scaler内部对梯度缩放会影响裁剪阈值),只用它自带的保护机制即可。
反之,如果使用完全FP16训练(比如某些低端设备上),GradScaler的scale因子会频繁调整,这时优化器的状态更新也要特别注意:当scale因子回退导致梯度值瞬间缩小时,Adam的动量状态不会自动适应,训练会出现一个明显的loss尖峰。规避办法是在每一步scaler.update()之后,重置optimizer的param_groups里对应的状态(不推荐但有效),或者干脆用bfloat16替代FP16,bfloat16的动态范围比FP16宽很多,几乎不需要动态scaling。
8. 个人经验总结与操作建议
在模型优化这个方向上折腾了这些年,我自己最大的体会是:模型优化不是某一个点上的炫技,而是一整套组合拳。训练侧的优化器选型、学习率策略、混合精度配置,推理侧的剪枝、量化、蒸馏,每一步都要为目标服务——你的目标可能是精度、延迟、显存占用,也可能是三者之间的某个平衡点。
如果你现在正要开始一个模型优化任务,我建议的执行顺序是:先用AdamW+OneCycle跑通基线,确认模型能收敛,再用SGD系调优到目标精度,最后做推理侧的剪枝和量化。如果时间有限,不要从零做蒸馏,优先量化,因为它是投入产出比最高的一步。
另外想特别强调一点:做优化实验一定要有记录意识。不只是记录超参数,还要记录loss曲线形态、优化器配置、显存占用、每epoch耗时。这些信息在你复现、调整、向同事解释时,每一份都值回时间。我在内部工作流里一直保留一个实验记录模板,每次训练前填好配置表,训练后补上曲线截图和结论,长期积累下来就是一本完整的模型优化手册。
最后再分享一个小技巧:如果你用的是PyTorch,想快速验证不同优化器对同一个模型的影响,可以封装一个小工具函数,输入模型、数据、优化器名称和超参,自动跑20个step并返回loss曲线。几十行代码,能帮你在半天内筛完主流优化器的初始配置,比在完整数据集上反复试错高效得多。优化器这件事,说穿了就是"理解原理+动手验证+记录沉淀"的循环,把这个循环跑顺了,你的模型优化能力一定会有质的提升。