尧图精选

深度学习优化器选型与调参实践:从SGD到AdamW的完整指南

🕒 发布时间:2026/10/1 6:25:57 📁 来源:尧图网络
同一份代码、同一种模型结构、同一批训练数据为什么别人跑出来的精度就是比你高两个点我在刚转机器学习那会儿经常卡在这个问题上。起初我以为是初始化种子的问题后来逐一排查才发现差距往往集中在两处优化器选型和优化器配参。当时我用的是一套压根没调过的Adam默认参数而对方的配置里做了warmup、调了weight decay、甚至换了更匹配任务的优化器。那一次对比实验之后我才真正开始认真对待Model-Optimizer这个词——它远不是model.compile(optimizeradam)那么轻飘飘的一句话。这篇内容我把它定位成一份从原理到实操的优化器与模型优化笔记。适合刚入门的算法工程师、自己跑模型的学生也适合那些模型能跑但总差一口气、想系统梳理优化思路的从业者。你会看到优化器内部到底在干什么、不同任务应该怎么选、参数之间怎么联动以及训练收敛之后还能做哪些推理侧的优化。1. 先从根上理解优化器它到底在调什么很多教程上来就甩公式搞得优化器像个黑盒。但优化器要说复杂也确实复杂要说本质其实一句话就能概括它决定了参数往哪个方向走、每一步走多远以及如何根据走过的路调整后续的步伐。1.1 损失曲面与盲人下山的比喻把训练过程想象成一个盲人站在山坡上要摸到山谷的最低点。损失函数就是地形模型参数就是盲人的坐标。每一步盲人都要靠脚下的坡度感来判断方向——坡度就是损失对参数的梯度。但现实里这个山坡极度崎岖。有平坦的高原梯度几乎为零、有狭窄的峡谷一个方向陡峭另一个方向平缓、有局部凹陷的坑局部极小值、还有马鞍形的隘口鞍点。如果只靠原始梯度硬走盲人会在峡谷两侧来回震荡在马鞍点原地打转在高原上寸步难行。优化器家族这些年一直在做的事情本质上就是给这个盲人配不同的装备——帮他记住惯性Momentum、帮他感知哪条路更陡自适应学习率、帮他在平地上迈大步二阶动量。1.2 三种改进方向动量、自适应、二阶近似我把优化器的发展梳理成三条主线动量方向SGDMomentum、NAG。核心是让更新方向不仅看当前梯度还叠加历史梯度的指数衰减平均。你可以理解为给盲人一根手杖帮他记住刚才的走向遇到小坑时能借着惯性冲过去。这条线解决的是震荡和局部极小值问题。自适应学习率方向AdaGrad、RMSprop、Adam、AdamW。核心是每个参数单独分配学习率梯度大的参数步子迈小点梯度小的参数步子迈大点。这解决的是稀疏特征和大坡度峡谷并存时的平衡问题。二阶近似方向KFAC、L-BFGS以及一些自然梯度方法。核心是直接估计损失曲面的曲率用二阶信息修正更新方向。效果通常很好但计算和内存开销太大在深度学习里没有成为主流。大部分人的认知集中在第二条线也就导致了一个常见误区以为Adam就是最优解。实际上这条线里不同算法的行为差异非常大直接替换往往能让训练曲线大幅变化。1.3 一张表看懂SGD、Momentum、RMSprop、Adam的关系我用一个表格来对比主流优化器的更新逻辑方便你理解它们之间的血缘关系优化器核心更新逻辑关键超参典型短板SGD参数扣掉学习率乘梯度lr收敛慢容易卡在鞍点或局部极小SGDMomentum维护速度变量衰减叠加梯度lr, momentum默认0.9对稀疏特征不友好AdaGrad累计梯度平方学习率除以累计量lr, initial_accumulator累计量不断增大学习率会衰减到零RMSprop用指数衰减代替累加解决AdaGrad学习率消失lr, decay(通常0.9)参数多需要手调细节AdamRMSprop Momentum同时维护一阶和二阶动量lr, beta1, beta2, eps泛化性不如SGD有权重衰减实现偏差AdamW修正Adam的权重衰减实现位置lr, beta1, beta2, weight_decay对lr的敏感度仍然偏高看到这里你应该能感受到所谓的选择优化器实质上是在选择一整套更新策略而不只是换一个优化器名字。2. 我的Model-Optimizer选型经验不同模型该配什么优化器选型没有银弹但确实存在比较可靠的默认路线。我自己这些年跑下来的经验是先明确任务类型和数据规模再决定优化器家族最后细调参数。2.1 小样本和CV任务带动量的SGD依然是王者如果你做图像分类、目标检测这一类任务并且数据集不算大比如几万张图以内我强烈建议你先跑一版带动量的SGD。PyTorch里就是optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay1e-4)这个经典配方。为什么SGD在这种场景下还这么能打核心在于它的简单更新方向主要由当前梯度主导不会像自适应方法那样被历史梯度平均干扰。这让它更容易收敛到更平坦的极小值区域而平坦的极小值往往对应着更好的泛化性能。深度学习里一个很出名的现象就是SGDMomentum训出来的权重即便人为加一点扰动损失变化也很平缓而Adam训出来的权重往往处于尖锐的极小值里扰动一下损失就飙上去了。2.2 NLP和Transformer结构AdamW几乎是默认答案BERT、GPT这些Transformer架构出来之后AdamW几乎成了标配。原因有两点第一Transformer里大量使用LayerNorm和残差连接网络的各个层之间梯度尺度差异非常大自适应学习率的能力可以有效应对这种尺度不均衡。第二正确的权重衰减实现方式很重要。最早PyTorch里optim.Adam支持weight_decay参数但这个参数当时是在更新完参数之后直接把lr * weight_decay * param加到参数上——也就是说权重衰减项被藏在学习率里面。AdamW把这部分拆出来让权重衰减与学习率解耦让衰减量是固定的比例而不是随学习率变化。这个细节直接影响了Transformer训练时的稳定性与最终精度。我自己的经验是用Hugging Face的transformers库或者自己手写训练循环时AdamW配合lr2e-5到5e-5weight_decay0.01就能在大多数NLP任务上稳定收敛。不要乱动beta参数尤其是beta2。2.3 超大batch训练LAMB让你敢把batch size拉大做大规模预训练时往往要把batch size推到几万甚至几十万。普通Adam在小batch下表现良好但batch size一大学习率如果跟着线性放大收敛就极其不稳定。LAMBLayer-wise Adaptive Moments for Batch training的核心做法是对每一层单独计算信任比例再乘上该层的更新量。实际使用中LAMB可以把batch size推到几十万同时保持线性缩放学习率。我在一次大规模文本预训练实验里把batch size从4096提到32768用LAMB之后收敛速度几乎线性提升训练时间肉眼可见地缩短。如果项目里遇到必须上超大batch的场景直接考虑LAMB。2.4 我踩过的选型翻车案例有一个项目让我印象很深图像生成任务数据量中等我当时贪方便直接用了Adam默认配置。训练了大概10个epoch发现生成质量一直不理想图像总是模糊且颜色偏灰。后来换了SGDMomentum做了学习率warmup和cosine退火同样epoch数下生成清晰度明显提升。这个案例让我深刻意识到Adam并不适合所有任务尤其是数据量不大、模型不是特别深的情况下SGD族反而能带来更好的泛化结果。另一个翻车是在Transformer模型上用普通Adam而不是AdamW导致weight decay行为异常训练起来loss下降正常但验证集指标怎么也上不去后来横向对比才发现是权重衰减位置引起的正则化效果差异。3. 把这些超参调明白模型效果立刻上一个台阶选对了优化器只是开始。同一套优化器不同参数配出来可能是一个天上一个地下。这一节我把最影响结果的几个超参逐个拆开说。3.1 learning rate永远是最该花时间的超参学习率决定了每步参数更新的步长。我习惯把学习率想象成步子大小步子太大了容易在最优值附近震荡甚至发散步子太小了训练半天还在原地磨蹭。实际操作里有一个几乎能work的流程先做学习率热身扫描learning rate finder也就是从小学习率到大学习率跑几个小batch记录loss变化找到loss下降最快且最平稳的那段区间对应的学习率然后在这个范围内选一个初始值。以SGD为例ImageNet分类任务常用0.1的初始学习率配合bs256但如果你用bs64那就把学习率缩成原来的四分之一左右即0.025。线性缩放规则是学习率大约随batch size等比例变化但实际项目中要考虑梯度噪声的变化通常不会严格线性而是略微保守一点。3.2 weight decay和L2正则之间的区别很多人以为weight decay就是L2正则其实这两个在SGD里等价在Adam里完全不同。L2正则是在loss函数里加权重平方和然后求梯度相当于把权重乘以一个小于1的系数而weight decay是参数更新之后直接对参数做一次比例缩小。区别在Adam里被放大了。因为Adam会对梯度做归一化处理L2正则项的梯度也会被归一化导致L2正则在Adam里成了一个动态变化的惩罚效果很不稳定。AdamW的修正正是为了让权重衰减表现得像真正的L2正则但又不依赖于学习率和梯度尺度。实际经验Transformer训练时weight_decay0.01是个稳妥起点图像分类用SGD的时候weight_decay1e-4到5e-4之间比较合适。太大会欠拟合太小又没有正则化效果。3.3 beta1/beta2和epsNan损失的一大来源Adam里的beta1和beta2分别控制一阶动量均值和二阶动量方差的指数衰减速率。默认值是beta10.9, beta20.999大多数情况下不用动。但如果你训练的是长序列模型或者强化学习这类梯度方差很大的场景beta2太大会让二阶动量更新过慢导致学习率在前期偏大出现NaN损失。eps是防止分母为零的小常数默认1e-8。如果你用的是混合精度训练eps太小会导致梯度过小的参数更新不稳定这时可以调大到1e-6到1e-7。我还遇到过一种情况损失在前几百步是正常的突然变成NaN查了很久发现是eps太小模型某一层的梯度在fp16下直接被裁成了0二阶动量为0更新量爆炸。把eps从1e-8调成1e-6之后问题立刻消失。3.4 gradient clipping什么时候必须开梯度裁剪不是优化器的参数但它和优化器配合紧密。当你的模型存在梯度爆炸风险时RNN、Transformer、GAN这一步几乎是保命的。PyTorch里的写法是torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)放在loss.backward()之后、optimizer.step()之前。max_norm可以理解成梯度的上限——把所有参数的梯度拼接成一个大向量计算整体范数超过上限就等比缩放。这个操作能防止某个大梯度过大把更新方向带偏。实际案例我在训练一个深度Transformer时如果没有梯度裁剪训练到中后期经常出现损失突然飙升的情况加了max_norm1.0之后训练曲线变得非常稳定最终精度也更高了。对于SGD和Adamclip的值有所不同SGD可以宽松一点比如5.0Adam建议从1.0试起。4. 学习率调度和训练策略优化器之外的另一半工程优化器负责这一步怎么走学习率调度负责整个路程的步伐计划。二者必须配合否则优化器参数调得再好也白搭。4.1 warmup到底是玄学还是数学warmup指训练初期把学习率从零或很小的值线性或指数地升到目标值。很多人理解成让模型先稳定一下但实际机制没那么玄。尤其是Adam类优化器训练初期二阶动量还没积累起来一开始就用大学习率会导致参数更新量被异常放大warmup就是在等动量估计稳定下来。具体做法前t_total * warmup_ratio个step学习率从0线性升到峰值。一个常见设置是总训练步数10000warmup_ratio0.06即前600步线性warmup。Transformer训练时这个比例很重要一般设置在5%到10%之间。SGD在极小数据集上可以不warmup但在大中型数据集和深度模型上warmup依然能提升稳定性。4.2 cosine annealing和one cycle怎么选训练到后期学习率应该下降让参数在局部极小值附近精细搜索。最常见的两种方式Cosine annealing学习率按余弦曲线从峰值降到极小值附近。实现平滑、好用配合warmup基本是万金油。One Cycle先在warmup阶段升到峰值再在剩余阶段降到比初始值还低的数值同时还可以配合学习率和动量的一体化调度。One Cycle的收敛速度通常比cosine更快但对超参更敏感。我自己的习惯兼顾效果和稳定的场景选warmup cosine需要快速跑通实验、或者尝试打破当前指标的瓶颈时会特意试一次One Cycle。后者经常能在精度上带来意外惊喜。4.3 混合精度训练时优化器的隐藏配合混合精度AMP现在几乎是训练的标配。它让模型前向和反向使用fp16但优化器状态和主权重保留在fp32。这就不只是节省显存的问题了它还改变了优化器对梯度的感知精度。实际操作里有一个关键点在AMP下梯度是fp16的数值范围有限很小的梯度会被冲掉。这时eps的作用比fp32训练时更大。PyTorch的GradScaler会动态调整loss缩放因子避免梯度下溢。如果你发现自己训练时Scaler的缩放系数持续走低那大概率是模型本身梯度就不稳而不是优化器的问题。还有一个常见误区混合精度训练时为了让损失保持稳定有些人不加思考地把学习率调低。这种做法经常得不偿失因为AMP本身并不需要调低学习率应该保持和fp32一致的配置。我踩过的坑就是以为AMP像换了一个优化器一样改变一切结果把学习率调低了一个量级收敛速度肉眼可见地变慢。4.4 EMA不改变loss曲线也能提分的trick指数移动平均Exponential Moving Average不算优化器但它和梯度动量在数学上是同一思想对参数做移动平均得到的影子参数往往比当前参数更平滑、泛化更好。PyTorch里可以用torch.optim.swa_utils或者自己维护一份影子权重。我在图像分类、目标检测任务里加了EMA之后普遍能提升0.2到0.5个点几乎零成本。需要特别注意的是EMA要在训练后期开始才有效果而且尽量不要从头就EMA否则影子参数会被前期不稳定的权重污染。轻量做法是每训练N步更新一次影子参数在验证阶段用影子参数去推理。5. 训练完不算完推理侧模型优化的三板斧很多人把Model-Optimizer理解成训练优化器但在实际工程里模型优化还包含推理侧的速度与体量优化。训练收敛只是第一步能不能跑在用户的设备上、能不能实时响应是另一场硬仗。5.1 剪枝把冗余参数删掉剪枝做的是找到模型中不太重要的权重或通道把它们直接去掉减少计算量。最粗放的做法是幅度剪枝——绝对值小的权重直接置零。但实践中更常用的是结构化剪枝比如对卷积通道做剪枝因为这样真的能减少计算时间而稀疏矩阵在GPU上不一定更快。我做过一次BERT的通道剪枝实验把FFN层的中间维度从3072剪到2048精度掉了大约1%但在CPU推理时速度提升了30%。如果你的业务对时延敏感这往往是性价比很高的一步。关键在剪枝之后一定要做微调fine-tuning否则精度会掉得很难看。5.2 量化从FP32到INT8的实战价值量化是当前推理优化里最有效的通用手段。把权重从FP324字节/参数压到INT81字节/参数模型体积直接降到四分之一推理速度在支持INT8的硬件上能有2到4倍的提升。常见的两种路线PTQ训练后量化拿少量校准数据跑一遍统计权重和激活的数值范围直接转INT8。简单快速但部分模型精度损失明显。QAT量化感知训练在训练过程中模拟量化误差让模型去适应低精度。精度损失小但要重训成本高。以Transformer为例PTQ在一些任务上精度损失在2%以内QAT可以把损失控制在0.5%以内。我的建议是先做PTQ碰到精度问题再上QAT不要一上来就重训模型。5.3 蒸馏让小模型继承大模型能力知识蒸馏的核心思想是训练一个小模型学生让它去模仿大模型教师的软输出。教师输出的概率分布里往往包含了类别之间的相似性——比如一张猫的照片模型认为它是猫的概率0.7是狐狸的概率0.2这种软信息是独立训练小模型时根本无法获得的。蒸馏在NLP和CV里都是非常成熟的套路。我用BERT蒸馏过一个小模型保留了99%的精度但速度提升3倍。实际实施时要注意温度参数的选择——温度太高软标签太平滑学生会学不到关键差异温度太低又成了硬标签没起到蒸馏的作用。常规做法是温度取2到4损失函数里同时加权教师软目标和真实标签的交叉熵。5.4 部署友好的优化组合剪枝量化蒸馏一起上的顺序做完剪枝、量化、蒸馏之后应该按什么顺序组合它们我的经验是先蒸馏再剪枝最后量化。蒸馏是能力转移属于第一层让模型骨架小下来剪枝是结构精简让模型计算量进一步下降量化是精度压缩在真正的部署平台上压体积。如果你先做量化再做剪枝量化的统计参数会被剪枝破坏导致精度下降更严重。这个顺序我踩过坑才得出的结论一开始先剪枝再量化最终精度掉了4%换成反过来的顺序后整体只掉了1.5%左右。在部署阶段还要根据具体推理引擎做适配验证比如不同的推理后端对量化算子的支持程度不同需要逐个基准测试。这些内容适用于需要把模型发布到生产环境的场景如果你只是自己跑研究实验可以忽略这一节。一些实操里的碎碎念写了这么多最后分享几个我在实际实验里沉淀下来的习惯。第一跑任何新模型之前先花十分钟做一次学习率扫描。这个成本极低却能直接决定你后续所有实验的有效性。我见过太多人一上来就用一个拍脑袋的学习率训练了十几个epoch发现loss不降才开始怀疑模型结构问题。其实换个学习率问题直接就解决了。第二改动一次只动一个变量。优化器、学习率、weight decay、batch size、epoch数这些参数之间高度耦合。一次动两个以上的变量即使效果变好了你也没法确定是哪一步起了作用。实验记录里参数变化的乱账最难还。第三怀疑优化器问题时先回退到SGDMomentum做对照。SGD虽然收敛慢但它的行为最可预测几乎不会出现莫名其妙的NaN或者loss飞升。如果SGD都表现很差问题大概率在数据或模型结构不在优化器配置上。我之前踩过的最深一个坑是训练一个Seq2Seq模型用了默认Adamconv层和attention层梯度尺度差距极大模型始终不收敛。后来把学习率调低、把conv层单独设置更小学习率才慢慢跑出效果。这也让我养成一个习惯——当网络结构里有不同尺度的子模块时要主动考虑给它们配不同学习率或者用模型不同区域分组设置参数。最后再分享一个我这几年一直沿用的配参范式Transformer用AdamW warmup cosine weight_decay0.01CNN图像任务用SGDMomentum weight_decay1e-4 One Cycle生成模型先试AdamW再回头对比SGD强化学习优先考虑梯度裁剪和保守学习率。这套范式不一定是最优的但能让你在一个稳定且可预测的基础上开始工作避免从头踩坑。希望这篇笔记能帮你少走几段弯路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →