ARTICLE DETAIL

资讯详情

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

深度学习优化器实战:从SGD到AdamW的选型、调参与显存优化

深度学习优化器实战:从SGD到AdamW的选型、调参与显存优化 做训练这一年多我被人问得最多的一个词是优化器。不是网上抄来的那种定义而是实实在在的问题到底用 SGD 还是 Adam学习率设多少为什么 loss 一直震荡为什么换了优化器之后收敛完全变样。后来我把这些实际问题整理成了一个叫 Model-Optimizer 的实践库专门记录模型训练里优化器选型、超参数配置和排查经验。这篇文章就是这套实践库的核心笔记适合刚入门训练不久、想搞懂优化器原理的读者也适合已经在调模型但总觉得差点火候的工程师。我会尽量把优化器说得不像天书每条结论背后都附上我自己的实测依据。1. 优化器不玄学先搞懂它到底在干什么优化器的本质说起来就一句话告诉模型参数往哪个方向走、走多大一步。只是这个“方向”和“步长”的计算方式不同才有了各种优化器的区别。这一章我从最朴素的梯度下降讲起把几代优化器的演进逻辑串起来。1.1 梯度下降是“下山”优化器决定你怎么迈腿想象你在一个雾气腾腾的山顶需要摸黑走下山谷脚底传来的坡度就是你算出来的梯度。最简单的策略是哪边陡就往哪边跨一步步长固定这就是最原始的 SGD随机梯度下降。更新公式就是参数减去学习率乘以梯度也就是常说的 w w - lr * g。SGD 在 CV 领域统治了很多年尤其配 Momentum 之后在小模型上表现很稳。但它在 Transformer 这类结构上很吃力。原因在于 Transformer 各层梯度的尺度差异特别大用一个全局学习率去适配所有参数容易出现某些层更新太快、某些层几乎不动的情况。我实测过一个 6 层的 Transformer用裸 SGD 训练loss 下降到一定程度就卡住换 AdamW 之后同样步数能继续下降一个量级。这里面不是 SGD 不能用而是你的模型结构决定了它容易踩“地形不均匀”的坑。1.2 动量给下山过程加个惯性SGD 的另一个痛点是噪声大。每个 batch 的梯度都是真实梯度的带噪估计参数轨迹会左右乱跳。Momentum动量的改进很直观你不是每步都重新决定方向而是让之前的移动方向保留惯性再叠加当前梯度修正。公式上就是维护一个速度变量 v每次更新的时候把上一轮的 v 乘一个系数加上当前梯度再用这个 v 去更新参数。这个惯性系数通常取 0.9含义是保留上一轮 90% 的速度。效果上相当于把高频震荡平滑掉了让更新方向更一致。我在 CIFAR 上对比过SGD 加 Momentum 之后收敛速度和最终精度都比纯 SGD 有明显提升训练曲线也更顺滑。不过动量也不是越多越好系数太接近 1 会让参数冲出最优区域训练后期出现绕圈下不去的现象。1.3 自适应学习率每个参数脚下踩的地形不一样回到 Transformer 那个问题不同参数梯度尺度差异大能不能给每个参数单独配一个步长这就是自适应学习率优化器的思路。AdaGrad 先尝试了一版做法是把每个参数的历史梯度平方累加起来再用它去缩放学习率梯度累积大的参数走得慢累积小的走得快。想法很对但 AdaGrad 有个致命问题历史梯度平方是一路累加、永不衰减的越到训练后期缩放因子越夸张学习率会趋近于零模型直接“冻住”。RMSProp 修复了这一点它用滑动平均替代累加只关心近一段时间的梯度大小而不是从开训到现在的总和。到了这一步自适应学习率的框架基本成熟了。用生活类比来说AdaGrad 像是记了一本一辈子都不翻篇的账RMSProp 则是只看最近几天的流水显然后者更适合长期训练。2. Adam 与 AdamW为什么大模型都离不开它们如果说前面几代优化器是各管一摊Adam 就是把动量、自适应学习率这些成熟组件打包到一起还修了几个细节问题。它现在是深度学习训练的事实标准尤其是 Transformer 系模型几乎都是 AdamW 打底。2.1 Adam 是如何同时处理步长和方向的Adam 的核心逻辑是维护两个状态变量一个是一阶动量用于确定更新方向相当于带惯性的梯度平均另一个是二阶动量用于调整每个参数的更新幅度相当于自适应学习率。同时它还引入了一个“修正项”解决训练初期动量估计偏差过大的问题让前几步的更新不至于过于激进。默认参数一般是 betas(0.9, 0.999)也就是说方向参考最近 10 步左右步长参考最近 1000 步左右的梯度波动。这个组合在大多数场景表现稳定。但要注意Adam 的自适应机制是很强的平滑器它会让 loss 降得很快但这种快速下降不一定代表泛化效果好。我见过很多用 Adam 训出来的模型训练集指标漂亮验证集表现一般。所以后来大家开始关注 Adam 的变体其中一个最重要的就是 AdamW。2.2 AdamW 的权重衰减解耦差在哪里Adam 里加权重衰减传统做法是把它混进梯度里做 L2 正则也就是每次求梯度时加上权重本身。问题在于 Adam 的更新幅度受二阶动量缩放L2 正则项也被缩放了导致不同参数的衰减力度不一致权重衰减的实际效果变得不可控。AdamW 的改进是让权重衰减独立于梯度自适应过程直接在对参数做衰减不经过二阶动量的缩放。这个设计上的差异在 Transformer 上体现得很明显。很多开源代码里用 AdamW 搭配 0.01 的 weight_decay 训 LLM这已经成了默认配置。我在 1B 规模的模型上对比过 AdamL2 和 AdamW同样步数下 AdamW 的验证集困惑度更低而且训练更稳。后来 PyTorch 官方也把 AdamW 作为推荐选择说明这个方向的改进是被大量实验验证过的。2.3 从 CNN 到 Transformer选型逻辑的变化选优化器不能只看哪个“更先进”还得看模型结构。我在不同模型上做过对比结论大致是这样传统 CNN 网络比如 ResNet 这类搭配 SGDMomentum 加上一套成熟的学习率调度完全能训出很不错的效果而且代码简单、超参数敏感度低但换成 Transformer、BERT、GPT 这类结构SGD 的普适性就差很多AdamW 几乎是必选。原因在于 Transformer 的梯度方差分布和 CNN 差异太大自注意力层和前馈层的梯度量级经常相差几个数量级自适应学习率优化器能天然适配这种差异。从我维护的 Model-Optimizer 笔记来看一条经验是如果你在跑的是 2020 年之后出的主流模型架构直接选 AdamW 做基线通常不会踩大坑。3. Model-Optimizer 的配套工程超参数与调度器实战优化器本身只是发动机超参数和调度器是方向盘和油门。很多训练不稳定问题根源不是优化器选错而是周边配置没搭配好。这一章我把超参数设置、调度策略和配套组件拆开来讲。3.1 学习率、warmup 和 cosine 调度怎么定才稳学习率永远是最重要的超参数没有之一。经验上CNN 训练可以从 0.1 往下试Transformer 这类模型则集中在 1e-4 到 3e-4 区间。我在实际项目里习惯这么起步小规模数据上先用 3e-4 的 AdamW 跑几百步观察 loss 的下降速度如果一开始就震荡就降一倍学习率如果下降太慢就升一倍按这个方式收敛到一个可用的区间。warmup 是我强烈建议保留的配置。训练刚开始时模型参数是随机初始化的梯度统计完全不可靠此时直接用大学习率很容易让参数一步冲出合理范围。warmup 的思路是在前 N 步把学习率从 0 或很小的值线性升到目标值给模型一个“热身”的过程。N 通常设为总步数的 1% 到 5%大模型项目里 500 到 2000 步都是常见值。学习率调度器推荐 cosine 退火它让学习率按余弦曲线从峰值逐步降低到最小值到训练后期缓慢收敛比固定学习率或线性衰减都更平滑。我常用 PyTorch 的 CosineAnnealingLR配合 LinearLR 做 warmup用一个 SequentialLR 串联起来配置很简洁效果稳定。3.2 beta、epsilon、weight decay 这些细节参数很多人只调学习率忽略了优化器的细节参数这不对。beta1 控制动量平滑程度一般保持 0.9 就行beta2 控制二阶动量估计的窗口训练时间很长时可以考虑从 0.999 调到 0.95 或 0.98让二阶动量更快适应梯度变化避免被历史梯度带偏。epsilon 是最容易被忽略的。它出现在对所有参数缩放学习率的分母里作用是避免除零。默认值 1e-8 在 fp32 下没问题但如果你开了混合精度训练梯度在低精度下会引入量化误差epsilon 太小会让更新幅度不稳定。我在项目里混合精度训练一律把 epsilon 调到 1e-6 或 1e-7这个调整成本很低却能明显减少偶发的 loss 尖峰。weight decay 也不是摆设。AdamW 里常用 0.01CV 任务甚至用到 0.05。它的作用是约束参数不变得太大变相增强泛化能力。我有个建议先固定 weight_decay 为 0.01把钱花在学习率和 warmup 上等模型能正常收敛之后再回头细调 weight_decay这个顺序能少走很多弯路。3.3 梯度裁剪与混合精度优化器的两条保险绳梯度裁剪是我在所有大模型项目里都会开的开关做法是设定一个全局梯度范数上限超过就把梯度整体按比例缩小。这能防止某个极端 batch 产生超大梯度导致参数一步跳飞。常用上限是 1.0。裁剪对稳定性的提升立竿见影代价几乎为零属于性价比最高的保险丝。混合精度训练则是另一层保障。它让梯度用低精度存储计算、用高精度更新参数既省显存又提速但要注意它和优化器的交互。优化器的状态量比如 Adam 的动量和方差应该保持在 fp32更新过程也在 fp32 完成否则低精度误差会累积。PyTorch 的 GradScaler 会自动处理大部分情况你只需要保证模型里的关键数值操作在 fp32 下完成就行。4. 大模型训练里的优化器内存账本与省显存方案模型训练吃显存很多人第一反应是“模型参数太大”但真正的大头往往在优化器状态上。这一章算一笔显存账再看怎么用省显存方案把账做小。4.1 显存大头在哪参数、梯度、优化器状态的三角关系以 7B 参数模型、全参数混合精度训练为例我用的是 bf16。先算参数7B 乘以 2 字节约 14GB。梯度也是同样的尺寸再占 14GB。剩下的是优化器状态Adam 要维护一份 fp32 的主参数副本、一阶动量 m、二阶动量 v每个都是 4 字节加起来 12 字节每参数也就是 84GB。三项合计约 112GB。看到这个数字你应该明白了优化器状态占了总显存的 75%。这也是为什么很多人明明模型不大但一开训练就 OOM。省显存的第一优先级不是压缩模型而是压缩优化器状态。明白了这一点后面所有的优化手段都围绕它展开。4.2 8 比特优化器与 ZeRO 分区能省多少最直接的省法是把优化器状态从 fp32 压成 8 比特。bitsandbytes 这类库提供了 8 比特 Adam把 m 和 v 用量化方式存储可以将优化器状态从 84GB 压到 21GB 左右总显存账从 112GB 降到 49GB节省超过一半。在实际使用中8 比特优化器对收敛效果的影响很小训练曲线和 fp32 版本几乎一致代价是偶尔需要调一下量化参数。我建议在显存紧张、又不想改动并行方案时优先尝试这个方案。另一种思路是 ZeRO 分区把优化器状态、梯度甚至参数切分到多张卡上而不是每张卡都存全量副本。ZeRO-1 只分区优化器状态7B 模型用 8 张卡时每张卡的优化器显存降到原来的八分之一。ZeRO-2 加上梯度分区ZeRO-3 更进一步把参数也分区。配合 8 比特优化器7B 模型的全参数训练可以压缩到非常可观的程度。4.3 显存受限场景下的选型建议如果你的单卡显存只有 24GB又需要训 7B 模型我建议按优先级组合先开 8 比特优化器再用 ZeRO-3 分区再加梯度检查点。这个组合能勉强跑起来但训练速度会下降因为引入了更多通信和重计算。如果显存还是不够就得考虑参数高效微调比如 LoRA它只训练一小部分注入的参数优化器状态自然就小很多。LoRA 背后的逻辑是“优化器状态只跟可训练参数挂钩”理解了这一点你就能明白为什么大模型微调几乎都走 LoRA 路线。5. 我踩过的优化器坑问题现象与排查实录最后一部分我按照真实踩坑的经历整理成了一份排查速查。这些问题我都实际遇到过排查方式也是反复试出来的希望帮你省掉几晚上的 debug 时间。5.1 loss 一开始就震荡然后飞掉最常见的现象是训练第一波 loss 忽高忽低幅度越拉越大最终 loss 变成 NaN。先别急着换优化器按这个顺序排查第一确认学习率大了没有如果初始学习率超过 1e-3先降到 3e-4 试一轮第二检查 warmup 有没有开启如果没有先加到总步数的 3%第三看混合精度下的 epsilon调大到 1e-6 试试。我遇到过一个案例模型隔几百步就出一个 loss 尖峰最后发现是 epsilon 取默认 1e-8 加上 bf16 数值溢出改到 1e-6 之后问题彻底消失。如果这三步都没解决再考虑减小 batch size 或调整 beta2。5.2 loss 停滞不动怎么调都不降这个情况比震荡更折磨人。我的排查顺序是先看模型是不是根本没更新偶尔有人不小心把 requires_grad 设错了再看学习率是否太低尤其是 warmup 到后期但调度器没有切换学习率一直卡在很小的值最后检查数据流数据顺序不对或者标签错位也会让 loss 停在某个值附近。如果都正常建议尝试用 AdamW 替换当前优化器或者临时把 weight_decay 调成 0 跑几十步排除正则项的影响。5.3 GPU 显存总是差那么一点差一点就 OOM这种是最让人崩溃的。优先级最高的解法是换 8 比特优化器基本能立刻腾出几十 GB。如果不想动优化器可以开梯度检查点用计算换显存通常会让显存峰值下降可观的比例。另外注意检查优化器是否保存了 fp32 的主参数副本有些实现里如果你不小心把参数的 dtype 设置得很高优化器状态会成倍增长。用 torch.no_grad 推理或者关闭梯度计算的临时操作也能省下一点但跟优化器状态比都是小头。5.4 复现对比实验时结果不稳定做消融实验时同一份代码跑两次结果差很多这个多半不是优化器的问题而是随机性控制没做好。在训练脚本里固定几个关键的随机种子包括 Python、NumPy、PyTorch 和 CUDA 的同时注意确定性算法的设置能明显提升复现度。另外多个卡并行训练时数据加载顺序会影响结果要么统一数据顺序要么接受一定范围的波动。优化器本身的初始化也可能引入不确定性如果启用了非确定性操作先关掉再复现。结尾从我这些年的训练经验来看优化器相关的问题八成不是“选哪个”的问题而是“怎么配”的问题。我自己也是踩了无数个 loss 飞掉的坑之后才总结出这套 Model-Optimizer 实践库的。每换一个新模型架构我都会花小半天时间在小数据上把学习率和 warmup 试出合理的区间再上全量数据跑正式实验。这个习惯帮助我避免了很多无意义的全量训练浪费。如果你现在正被训练不稳定或显存不够折磨我建议先别急着改模型回去检查优化器和它周围的配置往往问题就出在这里。
返回列表