ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

模型优化器全解析:原理、选型与实战调参

模型优化器全解析:原理、选型与实战调参 1. 优化器到底是什么为什么每个模型训练都绕不开它这两年做大模型和各类深度学习项目的人越来越多但很多新手第一次接触训练流程时往往会卡在一个问题上Model-Optimizer到底在做什么有人觉得优化器只是更新参数的工具随便选个 Adam 就完事有人则把它当成调参玄学靠感觉改学习率。实际上优化器是整个模型训练里最贴近数学本质的一个环节——它决定了你的模型能不能收敛、收敛得快不快、最终落在什么质量的位置上甚至直接决定了一次实验是小时级还是天级。我用一个生活化的类比来解释训练模型就像在连绵起伏的山脉里找最低点损失函数的最小值。模型本身是一张地图梯度像指南针告诉你哪边是下坡而优化器就是你的走路策略。你可以每一步都老老实实顺着最陡的地方走SGD也可以给自己加个惯性冲过小坑Momentum还可以在平坦的地方自动迈大步伐Adam。步子迈多大、什么时候该收力、要不要带点阻尼、怎么绕过那些看起来很低但其实不是最低的陷阱——这些全是优化器管的。这篇内容面向的对象很明确正在用 PyTorch、TensorFlow 或 PaddlePaddle 训练模型的人不管你是刚开始写第一个 CNN还是已经在跑大模型微调只要涉及训练脚本里的optimizer ...这一行都值得把优化器的原理、选型和调参逻辑搞清楚。先说结论优化器选型没有绝对的标准答案不同任务、不同模型结构、不同显存条件下最优选择完全不同。但如果你掌握了它的底层层逻辑任何项目里你都能在十分钟内做出一个至少不差的选择并且知道问题出现时该往哪个方向排查。2. 主流优化器一网打尽原理、特性与选型建议聊优化器之前得先统一一个认知——所谓优化器本质上是梯度下降算法的一种工程化实现。它的核心公式可以概括成一句话[ \theta_{t1} \theta_t - \eta \cdot g_t ]其中 (\theta) 是参数(\eta) 是学习率(g_t) 是第 (t) 步的梯度或梯度修正量。所有优化器无论叫 SGD 还是叫 Lion本质上都在回答三个问题用什么梯度、怎么修正梯度、学习率怎么变。2.1 经典派SGD 与 MomentumSGD随机梯度下降是最朴素的优化器。它每次从训练集里抽一个 batch 计算梯度然后直接沿着负梯度方向更新参数。优点极突出计算开销小、显存占用低、在收敛后期往往能到达比自适应优化器更锐利的最优点。缺点是收敛慢、容易在鞍点和局部极小值附近徘徊、学习率敏感。我在实际项目中观察到很多提升模型精度的论文最终实验阶段都会切回 SGD 加 Momentum就是因为它能帮模型收敛到更平滑的平坦区域泛化能力更好。Momentum动量则是对 SGD 的一个经典改良。它的思想很朴素给梯度更新加一个惯性让参数更新方向不仅参考当前梯度还参考历史梯度的累积方向。公式变成了[ v_t \beta v_{t-1} g_t ] [ \theta_{t1} \theta_t - \eta \cdot v_t ]这里的 (\beta)通常取 0.9就是动量系数。在峡谷型损失曲面一个方向陡、另一个方向平缓上SGD 会来回震荡导致前进缓慢而 Momentum 会在震荡方向上互相抵消、在一致方向上加速效果立竿见影。我个人的经验是如果做图像分类、目标检测这类 CV 任务SGDMomentum 往往比 Adam 更稳尤其是 fine-tune 预训练模型的时候。训练精度上限更高损失曲线也更光滑。代价是需要多花时间调初始学习率。2.2 自适应家族AdaGrad、RMSProp 与 AdamSGD 家族的痛点在于学习率是一个全局固定标量而实际模型中每个参数的梯度尺度差异很大。有的参数梯度大而稀疏有的梯度小而密集。用同一个学习率去更新所有参数显然不够聪明。AdaGrad 做了第一波尝试给每个参数单独维护一个历史梯度平方和学习率按这个累积值的倒数缩放。梯度大的参数自动变小步长梯度小的参数自动变大步长。效果是收敛更稳定但累积平方和不断增大学习率会趋向于零导致训练提前停滞——这也是它后来没被广泛使用的原因。RMSProp 改进了这一点用滑动平均替换了累积平方和这样学习率不会单调衰减到零而是能持续保持自适应能力。RMSProp 在 RNN 训练上表现很好也是当年的标配。真正让自适应优化器走入千家万户的是AdamAdaptive Moment Estimation。它把 Momentum 的一次矩估计和 RMSProp 的二次矩估计结合在一起并且加入了偏差修正机制解决了训练初期的估计偏差问题。PyTorch 里一行代码就能用上import torch.optim as optim optimizer optim.Adam(model.parameters(), lr1e-3, betas(0.9, 0.999), eps1e-8)Adam 的优点不用多说收敛快、对学习率不敏感默认 1e-3 通常就能跑起来、适应各种任务。如果做 Transformer、BERT 这类 NLP 模型Adam 几乎是无脑选择。但我也得提醒一句Adam 在部分任务上最终的泛化性能会略逊于精心调过的 SGDMomentum这一点在很多研究里都被反复验证过。2.3 改良先锋AdamW 与 LAMB以及那些被低估的细节如果说 Adam 是优化器界的大众情人那AdamW就是现在的正统西装。AdamW 的核心改动是把权重衰减L2 正则从损失函数项中剥离出来直接作用于参数更新。原文里 Adam 的权重衰减实现方式会导致大梯度参数被过度惩罚AdamW 修正了这个耦合问题。这一改动对大规模预训练模型特别重要。现在主流的大模型训练基本都用 AdamW配合权重衰减系数 5e-2 到 1e-2 左右。optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay0.01)再往后就是面向超大规模训练的LAMBLayer-wise Adaptive Moments。LAMB 的思路是给每一层计算独立的 Adam 更新量再按层的尺度做归一化。它的意义在于当 batch size 达到几千甚至几万时梯度噪声和局部最优陷阱对全局学习率的敏感性急剧增加LAMB 通过逐层适配解决了大 batch 训练不稳定的问题。BERT 预训练用 LAMB 可以把 batch size 从 256 提到 65536 而保持精度不显著下降。另外还有两个近期值得关注的优化器Lion和Sophia。Lion 只用符号方向sign更新显存占用更小在大模型训练中能省接近一半的优化器状态显存Sophia 通过引入二阶信息做裁剪在大模型上可以达到 AdamW 的 1.5—2 倍迭代效率。这些新优化器在特定场景下很强但生态和稳定性还在验证中现阶段主力项目我依然推荐 AdamW 或 SGD。2.4 选型速查表任务类型推荐优化器优势注意事项图像分类 / 检测SGD Momentum泛化精度高、易 fine-tune对学习率敏感需要 warmup 和衰减NLP 任务 / TransformerAdam / AdamW收敛快、鲁棒性好显存占用比 SGD 高约 2 倍大模型预训练 / 微调AdamW / LAMB稳定支持大规模并行关注 weight decay 与 lr 配合强化学习Adam能处理非平稳目标建议调低学习率 1e-4 左右小数据集 / 简单模型SGD / AdaGrad简单直接、显存友好小心学习率设置过大导致发散选型之外还有个常被忽略的点优化器状态显存开销。SGD 只需要保存参数和梯度Momentum 多一份动量Adam 需要保存一阶矩和二阶矩——这意味着一份模型参数要另外占用两份同等大小的显存。以 7B 参数的模型为例用 Adam 仅仅优化器状态就是 56GB 以上的显存开销因此大模型项目通常会用 AdamW 的分片版本比如 ZeRO 或 FSDP或者直接换用 Lion、Sophia 这类低状态优化器。3. 实操环节一次模型训练中优化器的完整配置理论说再多不如把一次真实的训练脚本拆开看。下面我以 PyTorch 为例写一个图像分类模型微调的完整优化器配置流程边写边解释每个参数的含义和取值依据。3.1 配置前的几个关键参数理解优化器构造看起来只有一行代码但里面每一个参数都值得拆解清楚。最核心的包括lr学习率决定了每一步参数更新的幅度。过大直接发散过小收敛缓慢。合理的初始学习率通常需要根据优化器类型和任务数据量来确定SGD 常见 1e-2 到 1e-1Adam 常见 1e-4 到 1e-3。betasAdam 的动量衰减系数。第一个值是梯度一阶矩的衰减率第二个是梯度二阶矩的衰减率。默认值 (0.9, 0.999) 覆盖绝大多数场景但在稀疏梯度占比较高的任务中可以适当下调到 (0.85, 0.95)。weight_decay权重衰减对参数做 L2 惩罚防止模型过拟合。对 AdamW 而言这个参数直接影响最终模型的泛化性能1e-2 是一个很好的起点。eps数值稳定项防止除零操作的极小值。默认 1e-8 就够用但混合精度训练时建议提高到 1e-6否则可能遇到数值不稳。一个完整的例子如下import torch import torchvision.models as models import torch.optim as optim model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) optimizer optim.SGD( model.parameters(), lr0.01, # 初始学习率微调通常比从头训练小一个量级 momentum0.9, # 动量系数 weight_decay5e-4, # L2 正则强度 nesterovTrue # Nesterov 加速版本略优于普通动量 )如果选择 Adam 系则对应optimizer optim.AdamW( model.parameters(), lr1e-4, # 微调用 1e-4从头训练可以尝试 1e-3 betas(0.9, 0.999), weight_decay0.01 )重点提醒一个常见误区从头训练和微调的学习率必须区分对待。预训练模型已经落在损失曲面的一个还不错的位置学习率太大会一步跨出这个优质区域导致灾难性遗忘。我见过不少同学拿 Adam 的默认 lr1e-3 去微调 ImageNet 预训练模型结果损失不降反升这就是典型的学习率过量问题。3.2 训练过程中的监控与判断配置好优化器之后训练不是丢进去跑就完事。我习惯每 50 步打一次日志实时监控三样东西loss 数值、学习率当前值、梯度范数grad norm。梯度范数是一个被很多人忽略的重要指标。如果梯度范数突然变成 NaN 或者线性膨胀到 1e10基本可以判定训练已经爆炸大概率是学习率过高、数据有 NaN、或者混合精度下的 eps 太小。在代码层面可以这样加钩子def log_grad_norm(model, logger, step): 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 logger.info(fstep {step}, grad_norm {total_norm:.4f})如果训练前期 loss 一直没有明显下降可以先用一个很小的学习率比如 1e-6跑 20 步确认 loss 能降——这一步的目的是排除数据流程和模型 forward/backward 的 bug。确认通了再逐步调学习率。这个小步快跑的思路能帮你节省大量排查时间。3.3 学习率调度配合优化器的节奏控制优化器负责怎么走学习率调度器scheduler负责什么时候走多大步。两者必须配合常见的搭配方案有线性 Warmup 线性 Decay大模型预训练的标配。前 1%—3% 的 step 里学习率从 0 线性升到目标值之后再线性降到最小学习率。Warmup 的作用是避免训练初期参数剧烈变动导致不稳定。Cosine Decay学习率按余弦曲线从高到低变化。它能让训练后期以小步幅精细寻优比 Step Decay阶梯式下降更平滑目前在视觉模型微调里用得最多。ReduceLROnPlateau当 loss 长时间不下降时自动把学习率乘以一个因子比如 0.5。对于小实验、快速验证场景非常实用但大规模训练不推荐因为不可预知性太强。一个完整的 PyTorch 搭配如下from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR, SequentialLR total_steps 10000 warmup_steps int(total_steps * 0.03) # 前 300 步线性 warmup后 9700 步余弦衰减 scheduler_1 LinearLR(optimizer, start_factor0.01, end_factor1.0, total_iterswarmup_steps) scheduler_2 CosineAnnealingLR(optimizer, T_maxtotal_steps - warmup_steps) scheduler SequentialLR(optimizer, schedulers[scheduler_1, scheduler_2], milestones[warmup_steps]) for step in range(total_steps): loss train_one_step() optimizer.step() scheduler.step()有个容易出错的小细节PyTorch 里scheduler.step()调用时机要放在optimizer.step()之后。如果是多个 epoch 的循环还要注意 scheduler 的 step 单位是 epoch 还是 iteration一旦搞混学习率就会跳变训练曲线会出现奇怪的突刺。4. 训练调优的真实经验常见问题与排查技巧优化器相关的坑我前前后后踩了不少。有些问题一眼能看出来有些则隐藏得很深。这一部分我按真实场景整理了最高频的四类问题以及对应的排查思路和解决手段。4.1 损失不下降的排查思路模型训练到第 500 步loss 纹丝不动这是新手最容易慌的情况。先别急着换优化器按顺序查这几项确认数据是正确的输入标签是否对齐、标签有没有错位。我曾经在一个文本分类任务里发现 label 做了一个 unseen 的映射导致模型没有梯度可学loss 退化成常数丢了好几天时间。确认模型在学习打印前几层参数的梯度范数如果梯度全是 0大概率是模型某一层输出把梯度阻断了比如 ReLU 全死区、或者有 zero 初始化层。小幅调高学习率降低正则强度、把 weight decay 暂时归零看 loss 是否能动。如果还是不动需要回到模型和数据流程排查。用一个可控子集做 sanity check选取 10—20 条样本数据量小到模型可以完全过拟合如果 loss 能够降到接近 0说明数据和模型没问题问题在优化器或更大范围的数据分布上。用 SGD 系优化器时我还特别推荐一个经典 trick先随便把某个层的参数手动改成常数比如全零或全一跑一次 forward 对比预期输出和实际输出可以快速定位是网络结构问题还是传播问题。4.2 训练震荡与不收敛训练曲线上下乱跳、看似在下降但始终不收敛这个现象几乎人人都会遇到。出现这类问题我第一个想到的就是学习率过大或者 batch size 过小。学习率过大导致参数在最优解附近反复横跳无法收敛解决手段很简单把学习率除以 10 再继续观察。batch size 过小导致梯度估计噪声大可以尝试把 batch size 加倍同时按比例放大学习率——注意线性缩放规则不是在所有场景下都成立batch 翻了倍学习率并不建议同样翻倍一般乘 1.2—1.5 就够。如果用了 Adam 还有震荡可以关注一下betas 参数。默认 betas(0.9, 0.999) 在一阶矩上衰减偏快导致更新方向容易受近期梯度影响。处理长尾数据分布的任务时把一阶矩衰减系数调大到 0.95 甚至 0.99往往能有效降低震荡。另外一个隐蔽原因是数据顺序。如果训练集没有做充分 shuffle模型会学到样本顺序带来的伪相关性loss 曲线周期性波动。训练前把数据 loader 的 shuffle 打开、并且设置drop_lastTrue当最后一个 batch 过小时都能缓解。4.3 分布式训练下的优化器行为多卡训练时优化器的行为和大家单卡经验完全不同这是很多人容易翻车的第二道坎。数据并行DP/DDP下每张卡各自前向反向得到梯度DDP 会对梯度做 all-reduce 平均然后统一更新。此时优化器的行为在数学上等效于放大 batch size 后的单卡训练只要学习率随之调整收敛行为基本一致。问题通常出在梯度不同步某一张卡的数据出了问题比如空数据、NaN 样本它的梯度会污染全局。梯度累积Gradient Accumulation也是常踩的坑模拟大 batch 时如果忘了在累积到预设步数后清零梯度梯度就会一直叠加优化器行为完全失控。正确写法是scaler.scale(loss).backward() if (step 1) % accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()FSDP / ZeRO 分片优化器状态下优化器状态被分到不同卡上每张卡只维护自己分片参数的优化器状态。这会带来一个微妙的问题不同卡上的更新步骤必须完全同步如果数据不均匀导致某张卡提前optimizer.step()整个训练就会开始发散。所以使用 FSDP 时务必保证所有卡的 step 数量一致。4.4 常见问题速查表问题现象最可能原因快速处理方案loss 为 NaN学习率过大 / 数据含 NaN / 混合精度 eps 过小降低 lr、清洗数据、eps 调至 1e-6loss 几乎不变数据或标签有问题 / 梯度阻断跑 sanity batch检查梯度流动loss 震荡不收敛学习率大 / batch 过小 / shuffle 未开lr 降 10 倍batch 加倍开启 shuffle收敛速度极慢学习率过小 / 优化器选型不合适换 Adam 从 1e-3 起跑或用 warmup收敛值不够低过拟合 / 模型容量不足 / 用了不合适的优化器试试 SGDMomentum 做后期微调多卡训练曲线异常梯度不同步 / scheduler step 次数不一致检查 DDP 同步逻辑统一 step 计数4.5 换优化器的正确姿势项目中期想从 Adam 换到 SGD或者从 SGD 切到 AdamW直接改一行代码往往会让训练立即崩掉。原因是两种优化器的有效更新尺度完全不同Adam 的更新量约等于学习率本身SGD 的更新量等于学习率乘梯度范数梯度范数量级大时同样 lr 下 SGD 的实际步长远大于 Adam。正确的过渡方式是加载已有的模型权重但把优化器状态丢空、重新构造同时把学习率调整到新优化器的经验值范围。常见做法是从 AdamW 转 SGD 时 lr 从 1e-3 降到 1e-2 附近具体值用 5 步小规模试跑来确定。这里还有一个更稳妥的方案用余弦退火把原优化器跑到低谷再用新优化器从低谷继续避免两种优化器在中间直接切换引入的脉冲式更新。5. 关于 Model-Optimizer 的几点个人体会最后聊点我在项目里反复验证过的实战经验。第一优化器不是越高级越好。我见过不少团队跟风换新优化器Lion、Sophia有的确实省显存、有的在特定任务上迭代速度翻倍但一旦换回 AdamW之前的训练技巧、调度器配置、学习率经验全部作废。如果团队已经很熟悉 AdamW 的调参手感没有强力的显存或速度诉求不要轻易动优化器的根子。第二学习率始终是远比优化器更值得调的超参数。同一优化器下老手和新手对学习率的把控可以造成 5 个点以上的精度差异。梯度范数的实时监控 warmup cosine decay 这套组合在绝大多数任务中都能稳定逼近最优效果。第三optimizer 的二进制兼容性值得注意。PyTorch 升级小版本时Adam 的 eps 和偏置修正实现有细微变化会导致复现实验时 loss 曲线有微小偏移。如果你的项目需要精确复现建议锁定 PyTorch 版本并保存优化器状态字典用于继续训练torch.save(optimizer.state_dict(), ...)。第四记录实验时除了记录 lr 和 optimizer 类型我还习惯把 betas、weight_decay、eps、warmup steps、scheduler 类型都记到训练配置里。一次训练实验要想复现必须保证优化器的所有参数、调度器的所有参数和模型结构、数据 pipeline 一起锁定少了任何一环都会得到看起来差不多但完全对不上的结果。做模型训练这些年优化器给我的感觉就是它既是数学也是手艺活。理解公式能让你快速定位方向积累手感能让你少走弯路。希望这篇梳理能帮你把优化器这扇门推开少踩一些我当年踩过的坑。
返回列表