☰
深度学习优化器全面解析:从SGD到AdamW的选型与调参实战
2026/10/21 15:57:36 网站建设 项目流程

1. 从一次训练翻车说起:为什么优化器值得单独拎出来聊

刚入行那会儿,我对优化器的认知基本停留在“一行代码的事”——optimizer = Adam(model.parameters(), lr=1e-3),复制粘贴,收工。直到有一次跑一个图像分类任务,loss曲线前200个epoch看着挺正常,突然在某一个batch之后直接飙到NaN,模型权重全废。我花了整整两天排查数据、检查网络结构、甚至怀疑显卡坏了,最后发现罪魁祸首是学习率设成了1e-2,而Adam在这种量级下对某些层的梯度反应过于激进,直接把参数推到了数值不稳定区域。

那次之后我才真正意识到,优化器远不是一行代码那么简单。它是整个训练过程的“驾驶系统”——模型往哪个方向走、走多快、遇到坑洼怎么调整,全由它决定。同一个网络结构,换一个优化器或者调一组超参,最终精度可能差出好几个百分点,收敛速度可能差出几倍。

这篇文章想做的事情很明确:把“optimizer优化器”这个主题从里到外拆开讲透。不管你是刚接触深度学习的新手,还是已经跑过几个项目但总觉得调参靠玄学的朋友,我都会从最核心的直觉出发,把常见优化器的设计逻辑、适用场景、参数怎么设、坑在哪里,一条一条讲清楚。读完你至少能做到两件事:第一,拿到一个新任务时知道该选哪个优化器;第二,训练出问题时知道往哪个方向排查,而不是盲目试。

2. 优化器到底在做什么:从下山比喻到数学本质

2.1 一个不需要数学基础就能懂的类比

想象你被蒙住眼睛放在一座山上,任务是走到山谷最低点。你手里只有一个高度计,能知道自己当前的海拔,但看不到全局地形。你该怎么走?

最朴素的做法是:感受脚下哪个方向最陡,然后朝那个方向迈一步。这就是梯度下降(Gradient Descent)的核心思想——梯度告诉你loss上升最快的方向,那我们就反着走,loss就会下降。

但问题来了:步子迈多大?迈太小,走到天荒地老也到不了谷底;迈太大,可能一步跨过谷底直接冲到对面山坡上,来回震荡甚至越走越高。更麻烦的是,真实的地形不是光滑的碗型,可能有平原、有峡谷、有鞍点、有局部凹陷,蒙着眼睛只靠脚下感觉走,很容易困在某个小坑里出不来。

优化器要解决的就是这些问题:方向怎么定、步长怎么调、遇到特殊地形怎么处理。不同的优化器,本质上就是不同的“下山策略”。

2.2 数学形式:所有优化器的共同骨架

抛开具体算法,几乎所有优化器的更新逻辑都可以写成同一个形式:

θ_t = θ_{t-1} - lr * update_direction

其中θ是模型参数,lr是学习率,update_direction是更新方向。不同优化器的区别,全在update_direction怎么算。

  • 朴素SGD:update_direction = gradient
  • Momentum:update_direction = 动量累积的梯度
  • Adam:update_direction = 一阶动量 / sqrt(二阶动量)

这个统一视角很重要,因为它意味着:你理解了update_direction的计算方式,就理解了这个优化器的全部性格。后面我们逐个拆解时,都会回到这个框架。

2.3 为什么优化器选择会影响这么大

有人可能会想:不就是下山吗,随便选一个能走不就行了?实际项目中差异巨大的原因在于——

第一,深度学习的loss landscape极其复杂。高维空间中存在大量鞍点(梯度为零但不是最优点)、平坦区域(梯度极小导致几乎不更新)、陡峭峡谷(某些方向梯度极大导致震荡)。不同优化器对这些地形的应对能力完全不同。

第二,不同任务对收敛速度和最终精度的偏好不同。有些场景(比如快速原型验证)你希望几十个epoch就出结果,那自适应优化器明显更合适;有些场景(比如追求极致精度的比赛)你愿意花几天慢慢磨,SGD配合精细的学习率调度往往能到更好的最终效果。

第三,优化器和正则化、batch size、网络结构之间存在耦合。同一个优化器在不同batch size下的表现可能截然相反,这一点很多人容易忽略。

3. 主流优化器逐个拆解:性格、适用场景与参数设置

3.1 SGD与Momentum:老派但不过时

朴素SGD的更新方向就是当前batch的梯度。它最大的优点是简单、内存占用小、理论分析清晰。但缺点也很明显:对学习率极其敏感,而且在鞍点附近容易停滞——因为梯度接近零,参数几乎不更新。

Momentum的引入就是为了解决这个问题。它的思路很直觉:就像一颗球从山上滚下来,即使当前脚下是平的,之前积累的速度也能带着它继续往前冲。具体实现上,它维护一个速度变量v,每次更新时把当前梯度累加进去:

v = momentum * v + gradient θ = θ - lr * v

momentum通常设0.9,意思是保留90%的历史速度,加入10%的新梯度。这个设计让Momentum在峡谷地形中表现特别好——沿峡谷方向的速度会累积加速,垂直峡谷方向的震荡会被平均掉。

实操心得:如果你用SGD系列优化器,学习率一定要配合学习率调度器(如StepLR、CosineAnnealing)使用。固定学习率的SGD几乎不可能跑到最优效果。我通常的起点是lr=0.1配合CosineAnnealing,然后根据loss曲线微调。

3.2 AdaGrad与RMSProp:自适应学习率的先驱

AdaGrad的想法是:不同参数应该有不同的学习率。频繁更新的参数,说明它已经被充分训练了,学习率应该减小;不常更新的参数,学习率应该保持较大,让它们有机会被训练到。

具体做法是维护每个参数的梯度平方累积和,然后用它来缩放学习率:

cache += gradient^2 θ = θ - lr * gradient / (sqrt(cache) + eps)

这个设计在稀疏数据场景(如NLP中的词嵌入)效果很好,但有个致命问题:cache只增不减,训练到后期学习率会被压到几乎为零,模型提前“僵死”。

RMSProp改进了这一点,它把累积方式从“全部累加”改成“指数移动平均”:

cache = decay * cache + (1 - decay) * gradient^2

这样cache不会无限增长,学习率可以保持活跃。decay通常设0.9或0.99。RMSProp在RNN系列模型上曾经是标配,现在虽然被Adam抢了风头,但在某些特定场景下仍然好用。

3.3 Adam:当前最主流的默认选择

Adam可以理解为Momentum和RMSProp的结合体——同时维护一阶动量(梯度的指数移动平均,类似Momentum)和二阶动量(梯度平方的指数移动平均,类似RMSProp),然后用两者的比值作为更新方向。

它的核心公式:

m = β1 * m + (1 - β1) * gradient # 一阶动量 v = β2 * v + (1 - β2) * gradient^2 # 二阶动量 m_hat = m / (1 - β1^t) # 偏差修正 v_hat = v / (1 - β2^t) # 偏差修正 θ = θ - lr * m_hat / (sqrt(v_hat) + eps)

默认参数:β1=0.9, β2=0.999, eps=1e-8, lr=1e-3。

Adam之所以成为默认选择,核心原因是它对学习率不那么敏感。你用1e-3、5e-4、2e-4通常都能跑起来,不像SGD那样lr差一个数量级就完全不能用。这对新手和快速实验非常友好。

但Adam也有自己的问题。最被诟病的是泛化性能有时不如精调后的SGD+Momentum。在一些图像分类基准上,Adam训出来的模型最终精度可能比SGD低0.5到1个百分点。原因至今没有完全定论,比较主流的解释是Adam的自适应学习率相当于一种隐式正则化,在某些场景下反而限制了模型拟合能力。

注意事项:Adam的weight_decay参数和L2正则化不等价。Adam中的weight_decay是直接加在梯度上的,而真正的L2正则应该加在loss上。如果你需要L2正则效果,应该用AdamW而不是Adam。这个坑我在早期项目中踩过,调了半天正则化系数发现根本没起作用。

3.4 AdamW:修正权重衰减的正确姿势

AdamW的改动看起来很小——把weight_decay从梯度计算中拿出来,直接作用在参数更新上:

θ = θ - lr * (m_hat / (sqrt(v_hat) + eps) + weight_decay * θ)

但效果差异在某些任务上非常明显。尤其是当你需要较强的正则化时(比如小数据集上训练大模型),AdamW能给你更可控的正则效果。现在主流框架中,AdamW已经基本取代了Adam成为Transformer系列模型的标配。

3.5 其他值得了解的优化器

LAMB:专为超大batch训练设计,在BERT等模型的分布式训练中表现优异。它本质上是对Adam的更新量做了层级归一化,使得不同层可以用相同的学习率。

Lookahead:不是替代品而是包装器,可以和任何优化器组合。它的思路是维护两套权重——快速权重和慢速权重,快速权重用内层优化器更新k步后,慢速权重向快速权重方向插值。实测能让训练更稳定,但内存开销翻倍。

RAdam:在Adam基础上加入了一个自动warmup机制,训练初期用SGD式的更新,等二阶动量统计稳定后再切换到Adam模式。对学习率warmup敏感的场景有帮助。

4. 优化器选型实战:不同场景怎么选怎么调

4.1 选型决策表

场景推荐优化器起始学习率关键参数理由
快速原型验证Adam/AdamW1e-3默认即可对lr不敏感,收敛快
图像分类追求精度SGD+Momentum0.1momentum=0.9, wd=5e-4泛化性能通常更好
Transformer/NLPAdamW1e-4~5e-5wd=0.01, warmup行业标配
小数据集微调AdamW2e-5~5e-5wd=0.01~0.1正则可控
稀疏特征/推荐AdaGrad/RMSProp1e-2decay=0.9自适应稀疏梯度
超大batch分布式LAMB1e-3~1e-2配合warmup层级自适应

4.2 学习率:最重要的超参数没有之一

如果所有超参只能调一个,那一定是学习率。我的经验是:

先用大范围粗搜,再小范围精调。比如从1e-1、1e-2、1e-3、1e-4各跑50个epoch看loss曲线,找到能稳定下降的最大值,然后在这个值附近做2到3次细化。

配合warmup使用。训练初期模型参数随机,梯度方向可能很乱,直接用大学习率容易把模型带偏。warmup就是在最初几百到几千步内把学习率从0线性增加到设定值,给模型一个“热身”过程。Transformer系列基本必用warmup,CNN上用也有帮助。

学习率调度策略选择:

  • StepLR:每N个epoch降为原来的0.1倍,简单粗暴,适合SGD
  • CosineAnnealing:余弦曲线平滑下降,适合大多数场景
  • OneCycle:先升后降,适合快速训练
  • ReduceLROnPlateau:验证loss不降就降lr,省心但反应慢

4.3 Batch Size与学习率的联动关系

这是一个经常被忽略但极其重要的点。当batch size增大时,梯度估计的方差减小,理论上可以用更大的学习率。经验公式是:batch size翻倍,学习率也翻倍(线性缩放规则)。但这个规则有上限,超过某个batch size后继续线性增加lr会导致训练不稳定,这时候需要改用平方根缩放或者配合warmup。

我在实际项目中的做法是:先确定显存能承受的最大batch size,然后从这个batch size对应的推荐lr开始试。比如batch size=256时,SGD的lr可以从0.1开始;batch size=64时,从0.025开始比较稳妥。

4.4 权重衰减的设置逻辑

权重衰减(weight decay)是正则化手段,作用是限制参数变大,防止过拟合。设置逻辑:

  • 大数据集(百万级以上):wd可以小一些,1e-5到1e-4
  • 小数据集(万级以下):wd需要大一些,1e-3到1e-2
  • Transformer系列:通常用0.01配合AdamW
  • 如果发现训练loss和验证loss差距很大(过拟合),优先增大wd
  • 如果训练loss都降不下去(欠拟合),优先减小wd

实操心得:weight decay和dropout不要同时开太大。我见过有人wd=0.1加dropout=0.5,结果模型根本训不动。这两个正则化手段叠加时,各自都要适当减小。

5. 训练出问题时的排查手册

5.1 Loss变成NaN的常见原因与处理

这是最让人心跳骤停的情况。按以下顺序排查:

第一步:检查学习率是否过大。这是最常见的原因。把lr降低10倍再跑几百步看看。如果降低后不NaN了,说明就是lr的问题。

第二步:检查是否有除零或log(0)。如果loss函数里有log操作,检查输入是否可能为0。加一个极小值eps(如1e-7)通常能解决。

第三步:检查梯度是否爆炸。打印梯度范数,如果某层梯度范数超过100甚至1000,说明梯度爆炸了。解决方案是加梯度裁剪:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

第四步:检查数据中是否有异常值。某条数据的某个特征如果是NaN或极大值,前向传播后loss就会NaN。用torch.isnan(data).any()检查一下。

第五步:混合精度训练的问题。如果用了AMP(自动混合精度),loss scaling设置不当也会导致NaN。可以先用纯FP32跑一遍确认模型本身没问题。

5.2 Loss震荡不收敛的排查思路

Loss曲线像心电图一样上下跳,但整体不下降,可能的原因:

  • 学习率过大:最直接的原因,降低lr通常能改善
  • batch size过小:梯度估计方差太大,增大batch size或增大动量系数
  • 数据未归一化:不同特征量级差异大,导致loss landscape扭曲
  • 网络中有BN层但batch size太小:BN在batch size小于8时统计量不稳定,改用GroupNorm或LayerNorm
  • 优化器参数不匹配:比如用Adam但β1设成了0.99,动量太大导致震荡

5.3 训练后期loss降不下去的应对

前几十个epoch降得挺好,后面就平了,常见原因:

  • 学习率没有衰减:加一个CosineAnnealing或StepLR,给模型“精调”的机会
  • 陷入鞍点或局部最优:尝试增大momentum,或者临时提高lr“跳”出去
  • 模型容量不足:如果训练loss本身都降不到很低,说明模型欠拟合,需要增大模型或减少正则化
  • 数据标签噪声:如果训练loss很低但验证loss很高,且差距持续扩大,可能是过拟合;如果训练loss本身就降不下去,检查标签是否有大量错误

5.4 常见问题速查表

现象最可能原因首选解决方案备选方案
Loss变NaN学习率过大lr降低10倍加梯度裁剪
Loss震荡不降lr过大或batch过小降低lr增大batch或momentum
训练loss降但验证不降过拟合增大weight decay加dropout或数据增强
训练loss都降不下去欠拟合增大模型减小正则化
收敛太慢lr过小增大lr换Adam
最终精度差一点优化器泛化不足换SGD+Momentum精调加长训练+CosineAnnealing
显存不够batch太大或模型太大减小batch用梯度累积

6. 几个容易被忽略的进阶技巧

6.1 梯度累积模拟大batch

显存不够放不下大batch时,可以用梯度累积:跑N个小batch,把梯度累加起来,再一次性更新参数。效果等价于用N倍大的batch训练。

accumulation_steps = 4 optimizer.zero_grad() for i, (inputs, labels) in enumerate(dataloader): outputs = model(inputs) loss = criterion(outputs, labels) / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()

注意loss要除以累积步数,否则梯度会放大N倍。这个技巧在单卡训练大模型时非常实用。

6.2 分层学习率设置

对预训练模型微调时,通常希望底层(通用特征)学习率小一些,顶层(任务相关)学习率大一些。实现方式:

optimizer = torch.optim.AdamW([ {'params': model.backbone.parameters(), 'lr': 1e-5}, {'params': model.classifier.parameters(), 'lr': 1e-3} ], weight_decay=0.01)

这种设置在小数据集微调时尤其重要,能有效防止底层预训练特征被破坏。

6.3 优化器状态保存与恢复

训练中断后恢复时,不仅要保存模型参数,还要保存优化器状态(动量、二阶动量等)。否则恢复后的前几百步会因为优化器状态丢失而出现loss波动。

# 保存 torch.save({ 'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'epoch': epoch }, 'checkpoint.pth') # 恢复 checkpoint = torch.load('checkpoint.pth') model.load_state_dict(checkpoint['model']) optimizer.load_state_dict(checkpoint['optimizer'])

这个细节在长周期训练中非常关键,我见过不止一次因为没存优化器状态导致恢复后训练崩溃的案例。

6.4 用LR Finder找学习率

如果你完全不知道学习率该设多少,可以用LR Finder:从极小学习率开始,每步指数增大,记录loss变化,画出loss vs lr曲线,选择loss下降最快且尚未反弹的那个点。

# 简化版LR Finder逻辑 lrs = [] losses = [] lr = 1e-7 for batch in dataloader: optimizer.param_groups[0]['lr'] = lr loss = train_step(batch) lrs.append(lr) losses.append(loss) lr *= 1.05 if lr > 1: break # 画图找loss最低点对应的lr,通常取该值的1/10作为最终lr

这个方法虽然多花几分钟,但能帮你省下大量盲目试错的时间。

7. 我踩过的那些坑

说几个真实踩过的坑,都是文档里不会写的。

第一个坑:Adam的默认lr不是万能的。1e-3在大多数CNN上没问题,但在某些Transformer变体上会直接导致训练不稳定。我现在的习惯是:Transformer系列从5e-5开始试,CNN从1e-3开始试,RNN从1e-3开始试。

第二个坑:换了优化器但没换学习率。从SGD换到Adam时,学习率通常要降低一个数量级。因为Adam的自适应机制相当于给每个参数单独调了学习率,整体有效步长比SGD大。我见过有人从SGD lr=0.1直接换到Adam lr=0.1,结果loss直接爆炸。

第三个坑:weight decay和lr的耦合。在SGD中,weight decay的效果和lr是耦合的——lr降了,实际正则强度也降了。但在AdamW中,weight decay是解耦的,改lr不影响正则强度。这意味着从SGD换到AdamW时,weight decay需要重新调。

第四个坑:恢复训练时忘了恢复优化器状态。这个前面提过了,但值得再强调一次。特别是用Adam系列时,二阶动量的恢复对训练稳定性影响很大。

第五个坑:在很小的数据集上用了太大的模型和太强的正则。结果就是模型根本学不动,训练loss和验证loss都很高。这种情况应该先减小模型或减小正则,让模型先能过拟合,再逐步加正则。

8. 关于优化器未来的一些个人判断

优化器这个方向这几年虽然没有网络结构那么热闹,但一直在稳步演进。我个人的观察是,纯自适应优化器和纯SGD之间的界限正在模糊——比如有些新方法尝试在训练初期用自适应方法快速收敛,后期切换到SGD式更新来提升泛化。另外,针对特定硬件和分布式场景的优化器设计也越来越多,比如针对大batch的LAMB系列、针对混合精度的Loss Scale优化等。

对于日常使用来说,我的建议是:不要频繁换优化器,但一定要理解你正在用的那个。把AdamW的参数吃透,比每个新优化器都试一遍但每个都一知半解要有效得多。真正影响最终结果的,往往不是优化器本身,而是学习率调度、batch size、正则化强度这些和优化器配合使用的超参数。

最后分享一个我自己的习惯:每次开新项目,我会先用AdamW+1e-3跑一个baseline,记录loss曲线和最终指标。然后换SGD+Momentum+CosineAnnealing再跑一次。如果两者差距在1%以内,我会选AdamW因为省事;如果SGD明显更好,那就值得花时间精调SGD的学习率和调度策略。这个流程帮我省下了大量无谓的调参时间,也让我对不同类型的任务有了更直观的判断依据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询