ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习训练不稳定?损失景观与优化器设置的调参实践

深度学习训练不稳定?损失景观与优化器设置的调参实践

深度学习入门一段时间后,很多人都会遇到一个奇怪的现象:网络结构搭得清清楚楚,损失函数也写对了,数据预处理没有问题,但模型训练出来的结果就是不稳定——有时精度不错,有时直接崩掉。我当初读到一本经典教材第三十八章后半部分,才意识到问题往往不在“模型写对了没有”,而在“训练过程本身有没有被正确控制”。这篇内容就从这里展开,把训练动力学相关的损失景观、学习率与批次配合、初始化与归一化、以及泛化验证方法,结合自己的实验记录整理一遍,希望对正在调模型的人有用。

1. 从损失景观看训练:同样的代码换一个随机种子,结果能差多少

1.1 高维空间里的“盆地”与“鞍点”

绝大多数人对梯度下降的理解停留在一个低维画面里:损失函数像一个碗,参数从高处滚向碗底,最终停在全局最优或局部最优。但真实深度学习模型的参数空间动辄千万维甚至上亿维,损失函数在这个高维空间中的形状远比你想象的复杂。

低维直觉里,我们觉得“局部极小值”到处都是,这是训练容易卡住的主因。但在高维非凸优化中,真正的局部极小值其实很少见。原因很简单:一个点要被判定为局部极小,必须满足所有方向上都“向上凸”。在千万维空间里,所有方向同时向上的概率极低。更常见的是鞍点——某个方向是下坡,另一个方向是上坡。

我在调试一个图像分类模型时,遇到过非常典型的鞍点表现:loss连续二十多个epoch卡在1.3左右不动,我当时以为模型已经收敛到了局部极小,正准备放弃调参,结果第45个epoch时loss突然开始快速下降,一路从1.3降到0.7。这个现象就是典型的“鞍点逃逸”:模型卡在了一个大部分方向梯度都很小的区域,随机梯度下降的噪声持续累积,加上动量的历史梯度积累,最终找到一个能走出去的方向。

理解鞍点对实际调参有直接帮助。你在tensorboard里看到loss平台期时,第一反应不该是“加学习率”,而是先判断这个平台是鞍点还是真的收敛。一个简单的判断方法:把学习率临时调大一倍,如果loss明显下降,说明之前是陷在鞍点或平坦区域;如果依然纹丝不动,那可能是模型的容量或数据本身限制了进一步优化。

1.2 三种典型训练失败模式:发散、震荡、停滞

我把平时调试中见过的训练失败案例归成三类,每种都有自己的特征和排查路径。

发散最直观,loss曲线要么快速冲向NaN,要么在几个epoch内飙升到天文数字。常见触发原因有三个:学习率过大、输入数据存在异常值、深层网络中梯度爆炸。排查优先级建议从最简单的开始:先把学习率降到原来的十分之一,如果loss马上恢复正常下降,那问题就在学习率;如果依然发散,检查输入数据是否做了归一化,尤其是特征量纲差异过大的tabular数据;再不行就看网络结构,Transformer或深层CNN中梯度爆炸很常见,加一层梯度裁剪(gradient clipping)往往能解决。

震荡的表现是loss上下起伏,不收敛也不发散。这个问题通常和梯度噪声有关。批次大小越小,每次计算的梯度越不稳定,更新方向抖动就越厉害。另一个常见原因是学习率偏高,模型在一个比较窄的谷底附近来回横跳。我的习惯处理顺序是:先把batch size翻倍,看震荡幅度是否收窄;如果还不行,给SGD加动量或用Adam族优化器;最后再考虑学习率退火。

停滞和前面说的鞍点平台期不一样,停滞是无论怎么调,loss都停在一个比较高的位置,而且多次换种子结果都差不多。这种情况多半不是优化问题,而是模型结构或数据本身有问题。比如ReLU导致大量神经元死亡(dead ReLU),某一层的特征全部变成0,梯度也传不下去。另一种常见情况是特征缩放严重不一致,某些维度数值特别大,主导了梯度方向,导致其他维度几乎学不到东西。排查时先检查各层激活值的分布,再看梯度的范数,逐步定位是哪一层出了问题。

1.3 训练日志里值得关注的三个信号

除了loss曲线,我还有几个固定的检查项。梯度范数是第一个——正常情况下,梯度范数应该在一个相对稳定的范围内波动。如果梯度范数瞬间暴增几十倍,说明某一步跳到了损失景观的陡峭区域,这时候要么降低学习率,要么上梯度裁剪。

激活值分布是第二个信号,尤其对深层网络来说,如果某层激活值全部集中在0附近,或者全部饱和在1附近,说明这一层的初始化或归一化出了问题。第三个是参数更新量与参数本身的比值,如果这个比值持续大于0.01,训练往往处于不稳定状态;如果全链路小于0.0001,又说明学习率可能过小。

这三个信号不需要每次训练都盯着看,但在模型表现异常时,它们能帮你快速缩小排查范围,比盲猜超参数有效得多。

2. 学习率、批次大小与动量的配合逻辑

2.1 学习率不是孤立参数:warmup与退火策略

很多人把学习率当成一个独立超参数来调,这是训练不稳定的重要原因之一。学习率实际上和批次大小、优化器动量、调度策略全部耦合在一起,单独调某一个往往顾此失彼。

先说warmup。训练刚启动时,参数是随机初始化的,此时的梯度方向很大程度上由噪声主导,并不代表真实的损失景观走势。如果一开始就使用较大的学习率,模型可能被推到某个不利区域,之后需要很长时间才能绕回来。warmup的思路是让学习率先从一个极小值线性增加到目标值,相当于先让模型找对方向,再放开步幅。实际操作中,5到10个epoch的warmup通常就够用,太长的warmup反而拖慢收敛速度。

经典的线性缩放规则(linear scaling rule)值得了解:batch size翻k倍,学习率也翻k倍。这个规则在小批量区间内是有效的,但有一个前提——训练接近凸优化场景,或者至少损失景观相对平滑。很多人盲目套用这个规则,把batch size从32调到256,学习率也相应翻了8倍,结果模型直接发散。这个规则可以用一个简单的直觉来理解:batch size翻倍后,梯度估计的噪声降低了约√2倍,为了保持同样的“有效步长”,需要放大学习率来补偿。

退火策略里,我现在的默认选择是余弦退火(cosine annealing),公式很简单:

lr_t = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T))

其中T是总训练轮数,t是当前轮数,lr_max是峰值学习率,lr_min通常设为目标学习率的0.01倍甚至直接设0。余弦退火相比传统的step decay(每隔若干个epoch把学习率乘以0.1)更平滑,训练后期依然保持一定的探索能力,不容易在某个阶梯上突然跳变。

2.2 批次大小带来的隐式正则化效应

批次大小对训练的影响经常被低估。小批次带来的梯度噪声并不仅仅是坏事,它其实扮演了一个隐式正则化的角色。因为每次更新方向都不是完全精确的,模型反而有机会逃离那些“锐利”的极小值——这些极小值通常泛化能力较差,落入其中后测试误差偏高。

这个概念可以从极端情况来理解:如果每次都用全量数据计算梯度,更新方向非常精确,模型会直接收敛到最近的极小值,而这个极小值往往是尖锐的;如果用很小的batch,梯度方向带有随机扰动,模型在优化过程中不断“抖动”,反而更可能落入平坦的极小值区域。平坦极小值的泛化能力通常优于尖锐极小值,因为参数的微小扰动不会显著改变输出。

临界批次大小(critical batch size)是我在实践中关注的一个重要概念。它的含义是:超过这个batch size之后,继续增大批次带来的吞吐收益会急剧下降。因为每次更新只需要一定精度的梯度估计,超过某个阈值后,梯度的额外精确度对收敛几乎没有帮助,反而浪费计算资源。

从我的经验看,常规视觉分类任务用32到256之间比较合适,ViT类模型或大规模预训练任务往往用512到2048。实际操作上,优先根据显存选择最大能塞下的batch size,然后配合线性缩放规则调整学习率,再加上warmup缓冲,通常能获得不错的训练稳定性。

2.3 SGD系与Adam系优化器:没有银弹,只有场景

优化器选择的争论持续了很多年,我的结论很简单:没有绝对优劣,只有场景适配。以下是这几年亲测下来比较可靠的判断依据。

SGD加动量(SGD with Momentum)在绝大多数卷积神经网络和结构化数据任务上,泛化效果往往优于Adam族,而且对超参数的敏感度其实没那么高。它的缺点是收敛速度慢,尤其在训练初期,loss下降速度会让人怀疑人生。另一个缺点是它对学习率比较挑剔,通常需要配合warmup和退火策略才能发挥全部潜力。

Adam族优化器的优势在于自适应学习率,对不同参数维度的尺度差异不敏感,天然适合稀疏梯度场景,比如NLP、推荐系统、图神经网络这些梯度分布极不均匀的模型。Adam在Transformer类模型上几乎是标配,因为这类模型对学习率极其敏感,用SGD训练Transformer很容易发散。

特别要提一下AdamW。它和标准Adam的核心区别不在于优化器本身,而在于weight decay的实现方式。标准Adam在实现L2正则时,会把weight decay项混入梯度,再被自适应学习率缩放,导致正则效果被扭曲;AdamW把weight decay直接应用到参数更新上,不受梯度缩放影响。实测下来,AdamW在Transformer和大部分深层网络上比Adam更稳,这也是主流预训练框架默认使用AdamW的原因。

场景推荐优化器参考配置
CNN图像分类SGD + Momentumlr=0.1×batch_size/256, momentum=0.9
Transformer类模型AdamWlr=1e-4到3e-4, weight_decay=0.01到0.1
稀疏梯度任务(NLP/推荐)Adam / AdamWlr=1e-4左右, 配合warmup
生成模型(GAN)Adamlr=1e-4到2e-4, betas=(0.5, 0.999)

3. 初始化与归一化:训练稳定性的地基

3.1 权重初始化:Xavier与He背后的数学直觉

权重初始化是训练稳定性中最容易被忽略的环节。很多初学者直接套用框架默认初始化,从来没想过为什么需要这些复杂的策略。核心目标其实只有一个:让信号在深度网络中双向流动时保持方差稳定。

前向传播时,每一层输出的方差如果逐层放大,深层网络的值会指数级膨胀,最终饱和在激活函数的极端区域,梯度变成0;如果逐层缩小,信号又会衰减到几乎为0,模型根本学不到东西。Xavier初始化就是从这个角度设计的:假设线性激活(或tanh这类关于原点对称的激活),要让前向和反向传播的方差保持一致,权重方差应设置为:

Var(W) = 2 / (fan_in + fan_out)

fan_in是上一层神经元数量,fan_out是当前层神经元数量。对应到代码,Xavier初始化通常写作:

import torch.nn as nn def init_weights(m): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) nn.init.zeros_(m.bias)

He初始化是ReLU系列的适配版本。ReLU会把一半的激活值压成0,信号方差天然减半,所以把权重方差放大到2/fan_in来补偿。实际代码:

import torch.nn as nn def init_weights(m): if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu') nn.init.zeros_(m.bias)

我见过一个很实用的技巧:在残差网络中对最后一个归一化层或最后一个卷积层做zero-init。这样残差分支的初始输出为0,整个残差块在初始阶段相当于恒等映射,网络的训练起点非常稳定。这个技巧在很多开源预训练代码中都能看到,对超深网络的收敛帮助明显。

3.2 BatchNorm、LayerNorm与RMSNorm怎么选

归一化层的选择直接影响训练稳定性,尤其在深层网络中。BatchNorm曾经是视觉模型的标配,但使用场景其实有边界。它沿着batch维度做归一化,这就要求batch size不能太小,否则统计量估计不准,训练和推理的行为会不一致。另一个坑在于train模式和eval模式的差异:BatchNorm在训练时使用当前batch的均值和方差,在推理时使用训练过程中累积的运行均值/方差。如果训练时batch size很小,或者数据分布和真实场景差异大,模型在eval模式下性能会明显下滑。

LayerNorm沿特征维度做归一化,与batch size无关,训练和推理行为一致,天然适合Transformer和RNN这类序列模型。这也是为什么NLP领域几乎清一色使用LayerNorm。它在小批次甚至batch size为1时也能稳定工作。

RMSNorm是LayerNorm的一个简化变体,去掉了均值中心化这一步,只保留根方归一化。计算量更小,效果在大部分场景下和LayerNorm相当,尤其受大型语言模型训练框架青睐。

类型归一化维度适用场景注意事项
BatchNormbatch维度CNN、较大batch sizebatch过小时统计量不准,train/eval行为不同
LayerNorm特征维度Transformer、RNN、任意batch size对序列长度变化不敏感
RMSNorm特征维度(简化)大规模Transformer、LLM少了均值中心化,效果相近,计算更省

3.3 一套稳妥的启动配置

很多工程团队实际上有一套固定的“起步配置”,在这个基础上做微调,而不是每次从零开始试。我整理了两套常用组合,适合作为基线,不要一上来就追求最优,先确保训练稳定。

第一套适合ResNet类视觉模型:He初始化 + BatchNorm + SGD动量(momentum=0.9)+ 学习率按线性缩放规则设为0.1×batch_size/256 + weight decay设为1e-4 + 5个epoch的warmup + 余弦退火。这套配置在大多数图像分类任务上都能扎扎实实收敛。

第二套适合Transformer类模型:默认初始化(或截断正态分布)+ LayerNorm + AdamW(lr=1e-4到3e-4,weight_decay=0.01到0.1)+ warmup比例设为总步数的5%到10% + 线性或余弦退火。这套配置在语言模型和大部分序列任务上都是稳妥的起点。

这套配置不能保证最优,但它的意义在于:当你在此基础上调整任何单一因素时,结果变化是可解释的。如果你一次性换了网络结构、换了优化器、又换了归一化层,出了问题根本不知道是哪一个环节导致的。

4. 泛化能力的实测验证与正则化误区

4.1 训练精度与验证精度的差距从哪来

训练集loss很低、验证集loss很高,很多人第一反应就是过拟合,然后忙着加正则化。但实际中,这个“泛化差距”的来源比想象的复杂得多。

数据分布偏移是一个经常被忽略的因素。训练集和验证集如果来源不一致,比如训练集是白天拍摄的图像,验证集有大量夜间图像,这种差距不是任何正则化手段能弥补的,只能通过数据收集和增强来解决。标注噪声也会造成泛化差距,训练集中一定比例的错标会让模型学到错误边界,训练loss很难降到理想水平,验证集上自然表现不佳。

还有一个反直觉的现象值得了解:双下降(double descent)。传统观念认为模型容量越大越容易过拟合,所以测试误差随容量增大先降后升。但实验表明,当模型参数超过某个“插值阈值”之后,测试误差反而可能再次下降。也就是说,大模型不一定比小模型更容易过拟合,关键看你处在容量曲线的哪个阶段。这也是为什么现代大规模预训练模型常常表现出超出预期的泛化能力。

所以,当你面对泛化差距时,不要条件反射式地去砍模型容量或堆正则化,先确认数据分布是否一致、标注质量是否可靠,再考虑正则化策略。

4.2 正则化手段的配合优先级

正则化手段有很多种,但它们不是等价的,性价比差异很大。我的实践结论是:数据增强永远是优先级最高的投入方向。

旋转、翻转、裁剪、颜色抖动这些基础增强在视觉任务中带来的提升,往往比精心调一整套正则化参数还要明显。对于文本任务,同义词替换、回译、随机mask等增强也有类似效果。数据增强的本质是扩大有效训练样本的分布覆盖,让模型学到更鲁棒的特征,这比任何“惩罚项”都来得直接。

weight decay排在第二位,它实现简单、计算开销小,而且几乎不会损伤训练集精度。推荐作为默认开着的正则化项,数值从1e-4起步,Transformer类模型可以设到0.01甚至0.1。

标签平滑在小数据集或噪声标签场景下很有用,它让softmax的目标不再那么“自信”,相当于给模型注入了一点保守性。实际使用时,标签平滑参数0.1是常见选择。

Dropout放在更靠后的位置,尤其在BatchNorm网络里要谨慎使用。BN本身有正则化效果,再叠Dropout并不一定能带来额外收益,有些实验甚至显示效果变差。在纯全连接网络中Dropout更可靠。

最后是早停,它虽然最简单,但在对抗过拟合上非常有效,应该作为所有训练的默认保护手段。

4.3 验证泛化能力的实验方法:别让运气替你决定结果

我见过太多人用一个随机种子跑一次实验,然后根据结果写结论。这样做最大的问题是:深度学习的训练过程充满随机性,同样的配置换一个随机种子,精度可能相差几个百分点。一次实验的结果可能只是运气好或运气差,不具可重复性。

标准做法是固定随机种子跑3到5次,报告平均值和标准差。如果两次不同配置的结果差异小于标准差,那这个差异在统计上是不显著的,不能轻易得出“A比B好”的结论。

消融实验是另一个关键习惯。每次只改变一个因素,其余全部保持不动。这个道理说起来简单,但在实际操作中很容易失控——我今天调了学习率、明天又改了weight decay、后天换了个初始化,最后模型精度确实提高了,但你根本不知道是哪一步起了作用。一次只改一个变量看起来很慢,但从长期来看是效率最高的调试方式。

我还习惯在实验日志里记录梯度范数、激活分布、更新量与参数比值这类指标,它们能帮助理解某个改动为什么有效或无效。这些过程指标在最终实验报告里往往不被展示,但在排查问题时的价值远超一张loss曲线。

我自己在这上面踩过的坑很多,最深刻的一条是:不要同时调整多个超参数。表面上看省时间,实际上出了问题根本无法定位。另一个体会是,训练稳定性的优先级要高于测试精度——如果多次运行的结果方差很大,再高的平均精度也没有意义。先保证同一个配置在不同随机种子下都能稳定收敛,再去优化精度,这才是省时间的路。希望这份记录对正在调试自己模型的人有一点帮助。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进