news 2026/10/27 15:03:57

读懂loss曲线:深度学习网络优化实验的调参与异常排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
读懂loss曲线:深度学习网络优化实验的调参与异常排查

批改本科深度学习课程的实验报告,最容易看出一个人有没有真正跑过训练循环的,不是代码写得多漂亮,而是他能不能解释自己那条 loss 曲线为什么长成那样。实验3 网络优化这个题目,表面上是让你换几个优化器、调几个学习率,实际上考的是你能不能把"模型不收敛"这件事拆成可验证的假设,一个个排除掉。我见过太多报告,正文写着"使用 Adam 优化器获得了更好的效果",配一张平滑下降的曲线,然后就没了——这种报告在评分表上很难拿到高分,因为它没有回答"为什么"。

这篇内容面向正在做这个实验的本科生,也面向需要带实验课的助教。我会按实际的调试顺序来讲:先搞清楚实验到底在评什么,再讲优化器和学习率的选型逻辑,然后是训练稳定性、正则化、复现性工程,最后给一套完整的异常排查链路。代码用 PyTorch 写,因为绝大多数课程实验都是这个技术栈;但思路换成任何框架都成立。文中所有参数取值都给出了推理依据,你可以直接抄,但更建议你理解之后再改。

1. 实验3的评分表其实围着三件事转

1.1 网络优化到底在优化什么:三个可观测的抓手

先把概念理清楚。"网络优化"这个词在课程语境下,指的不是去改网络结构,而是在网络结构固定的前提下,通过调整优化策略让模型收敛得更快、更稳、泛化更好。这三件事对应三个完全不同的观测指标,很多同学把它们混为一谈,导致报告写得含糊。

第一个抓手是收敛速度,衡量方式是"达到目标验证精度需要多少个 epoch"。同一个 ResNet 风格的骨干,用朴素 SGD 可能需要 60 个 epoch 才爬到 90%,用 Adam 可能 20 个 epoch 就到了。这个指标最好量化,也最容易被误用——收敛快不等于最终效果好。

第二个抓手是训练稳定性,看的是 loss 曲线的抖动幅度和是否出现异常。一条锯齿状的曲线、中途突然飙升、或者干脆变成 NaN,都说明优化过程不稳定。稳定性问题在浅层网络上不明显,一旦你把网络加深到十几层、或者加了残差之外的复杂连接,问题就会集中爆发。

第三个抓手是泛化差距,也就是训练集精度和验证集精度之间的落差。优化器调得好,训练 loss 会降得很快,但如果验证 loss 在第 15 个 epoch 就开始往上走,说明你优化的方向偏了,模型在记忆训练集而不是学习规律。

提示:写实验报告时,这三个抓手要分别给证据。只报最终精度是最偷懒的做法,也是最容易被扣分的做法。

1.2 对照组设计:单变量原则比结果好看更重要

课程实验和真实项目的最大区别在于,实验的价值在对照,不在绝对值。你的模型跑到 92% 还是 88%,其实不重要;重要的是你能不能证明"因为我改了 X,所以 Y 变了"。

这里有个特别常见的错误:一次改三个东西。比如某位同学想验证 Adam 比 SGD 好,同时把学习率从 0.01 改成 1e-3,又加了 weight decay,最后 Adam 赢了,他能得出什么结论?什么都得不出。因为三个变量同时动了,赢的原因可能是学习率更合适,也可能是正则化起了作用。

正确的做法是设计一张实验矩阵,每一行只改一个变量:

实验编号优化器学习率weight decay其他目的
E0SGD0.10基线参照
E1SGD+Momentum0.10momentum=0.9只改动量
E2Adam1e-30默认 betas只改优化器
E3AdamW1e-35e-4解耦衰减只改衰减方式
E4SGD+Momentum0.050减半学习率学习率敏感性

这张表的每一行都能单独回答一个问题。写报告的时候,把表格和对应的曲线图放在一起,结论就是自洽的。我个人的经验是,一张五行的实验矩阵,比十张孤立的精度柱状图有说服力得多。

还有一点,每个配置至少跑 3 个随机种子,报告里给均值和标准差。单次运行的精度差异在 1% 以内基本是噪声,你拿着一组 90.2% 和 90.5% 的数字说"A 比 B 好",在严谨的评审眼里等于没做实验。

2. 优化器不是越新越好:先把朴素 SGD 跑明白

2.1 为什么课程实验坚持让你先跑一遍 SGD

很多同学一上手就torch.optim.Adam(model.parameters(), lr=1e-3),跑出来曲线很漂亮,然后就不管了。这个习惯在工程上没问题,但在学习阶段是个坑。

SGD 的更新规则是θ = θ - lr * grad,它只看当前这一步的梯度方向。这个"笨"方法有两个非常宝贵的性质。第一,它对学习率极度敏感,你必须认真对待学习率的量级,而这个训练过程会逼你去观察 loss 曲线的形态。第二,SGD 的梯度噪声本身带正则化效果,在小数据集上,SGD 收敛到的解往往在验证集上表现更好,因为它倾向于找到"平坦"的极小值区域。

我带的实验里有个反复出现的现象:同一组配置,Adam 训练集精度能到 99.5%,验证集卡在 87%;SGD+Momentum 训练集只有 96%,验证集反而到 89%。这不是偶然,而是优化器特性决定的。所以我的建议是,先用 Adam 快速验证你的模型和数据处理没写错(因为它对初始化、学习率都不太挑),再用 SGD+Momentum 做最终结果,两套数字都写进报告,对比讨论泛化差距。

import torch # 阶段一:用 Adam 快速验证 pipeline 正确 opt_probe = torch.optim.Adam(model.parameters(), lr=1e-3) # 阶段二:最终结果用 SGD + Momentum + Nesterov opt_final = torch.optim.SGD( model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4, nesterov=True, )

2.2 Momentum 和 Adam 在损失曲面上的实际差别

Momentum 的核心是把历史梯度做一个指数加权累积,相当于给更新方向加了惯性。直观类比:SGD 是一个人每走一步都重新看方向,容易在峡谷状的损失曲面两侧来回横跳;Momentum 是这个人推着一辆有质量的推车走,横向的来回抖动会被平均掉,纵向的下降会被加速。

Adam 则是在 Momentum 的基础上再除以梯度平方的滑动平均的平方根,等于给每个参数一个自适应步长。梯度一直很大的参数,步长会自动缩小;梯度一直很小的参数,步长会被放大。这个机制对稀疏梯度特别友好,比如 Embedding 层、或者特征稀疏的场景。

但 Adam 有两个实际问题。一是它默认的 weight decay 是直接加在梯度上的(L2 正则),这和真正的权重衰减不等价,会和大梯度参数耦合在一起;修正方案是 AdamW,把衰减从梯度更新里解耦出来。二是 Adam 在训练的后期容易在极小值附近来回震荡,因为它对每个参数都用自适应步长,缺乏"逐渐收敛"的机制,这也是为什么很多流程最后会用学习率衰减来收尾。

优化器典型学习率收敛速度泛化倾向适用场景常见坑
SGD0.1~0.01慢较好有充足训练预算的最终训练学习率选错直接不收敛
SGD+Momentum0.1~0.01中等较好CNN 分类的标准选择momentum 过高会震荡
Adam1e-3~1e-4快一般快速验证、稀疏特征后期震荡,需配衰减
AdamW1e-3~1e-4快较好Transformer、大模型weight decay 需分组
RMSprop1e-3较快中等RNN、非平稳目标依赖衰减率设置

2.3 参数分组:weight decay 不该加在所有参数上

这一条是实操里最容易被忽略的细节。BatchNorm 的缩放参数 γ 和偏置 β,以及所有层的 bias,理论上不应该做权重衰减。因为对这些参数施加衰减,等于强行把它们拉向 0,会破坏归一化层的表达能力。正确的做法是把参数分成两组:

decay_params, no_decay_params = [], [] for name, param in model.named_parameters(): if not param.requires_grad: continue if param.ndim <= 1 or name.endswith(".bias"): # BN 的 weight/bias、所有层的 bias 都不衰减 no_decay_params.append(param) else: decay_params.append(param) optimizer = torch.optim.AdamW( [ {"params": decay_params, "weight_decay": 5e-4}, {"params": no_decay_params, "weight_decay": 0.0}, ], lr=1e-3, ) print(f"decay: {len(decay_params)} 组, no_decay: {len(no_decay_params)} 组")

判断依据是param.ndim <= 1,因为卷积核是 4 维、线性层权重是 2 维,而 BN 的 γ/β 是 1 维,bias 也是 1 维。这个小技巧在实验报告里写出来,是很能加分的细节。

3. 学习率是整个实验里唯一值得花两小时扫的超参

3.1 从 loss 曲线的三种形态反推学习率量级

如果说网络优化实验只能认真调一个超参,那一定是学习率。它的影响远超优化器选择、初始化方式、正则化强度。我的经验是,学习率选错一个数量级,模型直接废掉;优化器选错,最多差一两个点。

判断学习率是否合适,最直接的方法是看前 3 个 epoch 的 loss 曲线形态:

曲线形态诊断处理
几乎水平,缓慢线性下降学习率太小乘 3~10 倍
快速下降后立刻平台学习率偏小或已到局部极小尝试乘 3,或加衰减
剧烈震荡、锯齿明显学习率偏大除以 2~5
前几步 loss 上升学习率明显过大除以 10,或加 warmup
直接 NaN 或 inf学习率过大 / 数据未归一化除以 10~100,检查输入

具体操作上,我一般做一次对数网格扫描:[1e-1, 3e-2, 1e-2, 3e-3, 1e-3, 3e-4, 1e-4],每个配置只跑 3 个 epoch,记录第三步和最后一个 epoch 的 loss。挑选下降最快且未发散的区间,然后在区间内再细扫一次。这个流程总共花不到二十分钟,比反复改模型结构高效得多。

3.2 warmup 加余弦退火:一个几乎通用的调度模板

固定学习率在实验里够用,但如果想让曲线更漂亮、最终精度再高一点,加一个调度器是性价比很高的做法。我常用的是线性 warmup + 余弦退火:前 5% 的步数里学习率从 0 线性爬到峰值,避免一开始的大梯度把参数带偏;后面的步数里按余弦曲线平滑降到接近 0,让模型在训练末期能精细收敛。

import math def build_lr_lambda(warmup_ratio=0.05, min_lr_ratio=0.01): def lr_lambda(step_ratio): # step_ratio 是当前进度占全部训练步数的比例 [0, 1] if step_ratio < warmup_ratio: return step_ratio / warmup_ratio progress = (step_ratio - warmup_ratio) / (1.0 - warmup_ratio) cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) return min_lr_ratio + (1.0 - min_lr_ratio) * cosine return lr_lambda scheduler = torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambda=build_lr_lambda(warmup_ratio=0.05) ) for epoch in range(epochs): for batch in train_loader: optimizer.zero_grad() loss = criterion(model(batch["x"]), batch["y"]) loss.backward() optimizer.step() scheduler.step() # 按 step 更新,不是按 epoch

这里有个坑要说清楚:LambdaLR传入的 lambda 参数是"已经走过的步数",不是比例。上面这段写法是为了可读性做了包装,如果你直接用官方接口,需要自己把 lambda 写成lambda step: f(step / total_steps)的形式。另外,调度器按 step 更新还是按 epoch 更新,一定要和你的 lambda 设计对应上,两者混用会导致学习率曲线和你预期完全不一样——这种错误在实验报告里非常常见。

3.3 学习率与 batch size 的耦合关系

大 batch 训练时,梯度估计的方差变小,方向更准,理论上可以用更大的步长。经验法则是线性缩放:batch size 翻倍,学习率也翻倍。比如 baseline 是 batch=64、lr=0.01,改成 batch=256 时学习率可以开到 0.04。

但这个规则有前提:一定要配合 warmup。因为大学习率在训练初期非常危险,前几步就可能把参数推到饱和区。我一般会设成lr = base_lr * batch_size / 256,并且把 warmup 比例提到 8%~10%。

反过来,如果你显存不够只能把小 batch 跑起来,比如 batch=16,那学习率也跟着降,否则梯度噪声太大,训练会非常抖。这时候可以配合梯度累积来模拟大 batch:

accum_steps = 4 optimizer.zero_grad() for i, batch in enumerate(train_loader): loss = criterion(model(batch["x"]), batch["y"]) / accum_steps loss.backward() if (i + 1) % accum_steps == 0: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() optimizer.zero_grad()

注意 loss 要除以积累步数,否则梯度会被放大相应倍数,等效于把学习率乘了 4——这个错误我见过不止一次,表现出来就是"用了梯度累积之后 loss 突然变 NaN"。

4. 训练早期的稳定性:初始化、归一化与梯度裁剪的配合

4.1 初始化为什么能决定前一百步的生死

参数初始化经常被当成一个不用管的默认项,因为 PyTorch 的nn.Linear和nn.Conv2d自带初始化。但当你在实验里手写多层 MLP、或者加自定义模块时,问题就出来了。

最典型的表现是:网络加深到 8 层以上,激活函数用 ReLU,loss 卡在某个值完全不动,比如十分类任务卡在 2.30 左右(正好是ln(10),说明模型输出接近均匀分布)。原因是默认初始化下,每层输出的方差会逐层衰减或放大,深层几乎没有梯度回传。

判断方法很简单,在训练开始前打印每一层输出的统计量:

def check_activation_stats(model, sample_input): hooks = [] stats = {} def make_hook(name): def hook(module, inp, out): stats[name] = (out.mean().item(), out.std().item()) return hook for name, module in model.named_modules(): if isinstance(module, (torch.nn.Conv2d, torch.nn.Linear, torch.nn.BatchNorm2d)): hooks.append(module.register_forward_hook(make_hook(name))) model(sample_input) for h in hooks: h.remove() for name, (m, s) in stats.items(): print(f"{name:30s} mean={m:+.4f} std={s:.4f}")

健康的网络,各层输出的 std 应该在同一量级上下浮动,不至于从 1.0 衰减到 0.01。如果发现逐层衰减,就手动换成 Kaiming 初始化:

def init_weights(m): if isinstance(m, torch.nn.Linear): torch.nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") if m.bias is not None: torch.nn.init.zeros_(m.bias) elif isinstance(m, torch.nn.Conv2d): torch.nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") elif isinstance(m, torch.nn.BatchNorm2d): torch.nn.init.ones_(m.weight) torch.nn.init.zeros_(m.bias) model.apply(init_weights)

mode="fan_out"是配合 ReLU 的推荐选择,它保证前向传播时激活值的方差大致守恒。如果激活函数用的是 tanh 或 sigmoid,就改成 Xavier 初始化,因为那类激活函数关心的是输入输出方差都守恒。

4.2 BatchNorm 的位置与 eval 模式的陷阱

BatchNorm 的作用是对每一层的输入做标准化,让激活值分布稳定在合理区间,从而允许更大的学习率。但它放错位置会让效果大打折扣。标准的摆放顺序是Conv → BN → ReLU,千万不要写成 Conv → ReLU → BN,因为 ReLU 之后的值有一半是零,分布严重偏斜,再标准化会破坏掉稀疏性。

更隐蔽的问题是train/eval 模式切换。BatchNorm 在训练时用当前 batch 的均值方差,在验证时用滑动平均的 running stats。如果你验证时忘了model.eval(),它就会继续用验证 batch 自己的统计量,验证精度会显得异常高而且不稳定;反过来,如果你训练时误用了model.eval(),running stats 不会更新,验证时会用一组错误的统计量,精度直接崩。

def evaluate(model, loader, criterion, device): model.eval() # 必须 total_loss, correct, total = 0.0, 0, 0 with torch.no_grad(): for batch in loader: x, y = batch["x"].to(device), batch["y"].to(device) logits = model(x) total_loss += criterion(logits, y).item() * y.size(0) correct += (logits.argmax(1) == y).sum().item() total += y.size(0) model.train() # 别忘了切回来 return total_loss / total, correct / total

这个函数看起来平平无奇,但model.eval()和model.train()这两行是无数实验精度异常的根源。有个特别典型的现象:如果验证时没切成 eval,第一个 epoch 的验证精度可能高得不正常(因为用了验证集自身的统计量),后面又莫名其妙掉下来,曲线非常诡异。

4.3 梯度裁剪的阈值该定在哪

梯度裁剪主要解决梯度爆炸,常见于 RNN、深层 Transformer,或者你用了很大的学习率。写法很简单:

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

关键是max_norm取多少。我的经验值是按网络类型分:

网络类型建议 max_norm说明
CNN 图像分类1.0~5.0一般不用,学习率大时加保险
RNN / LSTM0.5~1.0基本必加
Transformer1.0标配
自写深层 MLP1.0防止早期爆炸

判断阈值是否合理,可以先把裁剪关掉,打印梯度范数的分布。如果绝大多数步的梯度范数在 2 左右,偶尔飙到 50,那把阈值设成 5 就够了;如果梯度范数长期在 100 以上,说明问题不在梯度爆炸,而在学习率或初始化,裁剪只是掩盖症状。

注意:裁剪要在loss.backward()之后、optimizer.step()之前。顺序错了等于没裁。

5. 验证集开始掉头之后的处理:正则化组合拳

5.1 判断过拟合的三个信号

训练跑起来之后,什么时候该停下来处理过拟合?看三个信号,出现任意两个就该动手了。

第一个信号是train loss 和 val loss 的走势分叉。正常情况下两条曲线平行下降,过拟合时 train loss 继续降,val loss 在第 N 个 epoch 之后开始上升,形成一个 U 型。这个 N 就是你模型的"最佳早停点"。

第二个信号是精度落差超过 10 个百分点。训练集 99%、验证集 85%,这个差距基本就是记住了训练样本。

第三个信号是验证 loss 的方差变大。同一个配置换三个随机种子,验证精度从 84% 跳到 89%,说明模型处在不稳定的边界上,稍微扰动就变。

顺便说一句,如果训练集精度本身就上不去(比如只有 70%),那不是过拟合,是欠拟合,方向完全反了——你该做的是加容量、调学习率、检查数据,而不是加正则化。这个判断经常被搞反。

5.2 正则化手段的优先级排序

过拟合确认之后,按性价比排序,我一般这样加:

第一优先级是数据增强,因为它从根本上扩充了数据分布,而不是靠约束模型。图像任务里RandomResizedCrop、RandomHorizontalFlip、ColorJitter三件套就够用。这里有个必须注意的点:验证集的 transform 不能包含随机增强,只能做 resize 和 normalize。如果验证集也随机裁剪,你的验证精度每次评估都不一样,无法比较。

from torchvision import transforms train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

第二优先级是 weight decay,取值 1e-4 到 5e-4 之间,配合前面说的参数分组。它的作用是限制权重范数,让决策边界更平滑。

第三优先级是 Dropout,全连接层之间放 0.2~0.5,卷积层后面一般不用(要用的话用Dropout2d,按通道丢弃)。Dropout 的位置也有讲究:放在全连接层之间、激活函数之后比较合理;如果和 BN 相邻,建议顺序是 Linear → BN → ReLU → Dropout。

第四优先级是 label smoothing,交叉熵损失里把 one-hot 标签软化一点点:

criterion = torch.nn.CrossEntropyLoss(label_smoothing=0.1)

它防止模型对训练标签过度自信,对验证集精度通常有 0.5~1 个点的提升,改动成本极低,性价比很高。

最后才是早停。严格说早停不算正则化,但它在实验里最实用:保存验证精度最高的那个 checkpoint,而不是最后一个 epoch 的。

这几种手段叠加使用时要注意,不要一次全上。每加一个,跑一次实验,看验证集变化,这样报告里才有可解释的因果链。

6. 复现性工程:随机种子、日志与调参表

6.1 同一份代码跑出三个结果的常见原因

实验报告最尴尬的场景,是助教要复现你的结果,跑出来精度差了 3 个点。原因通常是种子没固定。深度学习里的随机性来自四个地方:Python 的random、NumPy 的随机数、PyTorch 的 CPU 与 GPU 种子、以及 DataLoader 多进程时的 worker 种子。

import os, random import numpy as np import torch def set_seed(seed=42): os.environ["PYTHONHASHSEED"] = str(seed) random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 强制确定性算法,牺牲一点速度换可复现 torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False def worker_init_fn(worker_id): worker_seed = torch.initial_seed() % 2**32 np.random.seed(worker_seed) random.seed(worker_seed)

DataLoader 那边:

train_loader = torch.utils.data.DataLoader( train_set, batch_size=64, shuffle=True, num_workers=4, worker_init_fn=worker_init_fn, drop_last=True, )

drop_last=True这一点也值得说。如果最后一个 batch 只有 1 个样本,BatchNorm 在训练时算出来的方差是 0,会直接除以零,loss 变 NaN。这类 bug 只在数据集大小不能被 batch size 整除时出现,调试起来极其痛苦。

至于cudnn.deterministic=True会损失多少速度,实测在中小规模模型上大约是 10%~20%,对课程实验完全可接受。如果实在想快,就关掉它,但要在报告里注明"结果存在少量波动"。

6.2 调参记录表的写法:把每次实验当成一条实验记录

做这个实验最常见的浪费,是跑了一堆配置然后忘了哪个对应哪个。我建议从一开始就建一个 CSV 或者 Markdown 表格,每跑一次追加一行:

实验号优化器lrbswd调度best_val_accbest_epoch备注
E00SGD0.1640无0.871258基线,后期震荡
E01SGD+M0.1640无0.893447收敛明显更快
E02Adam1e-3640无0.885121前期快,后期平台
E03AdamW1e-3645e-4无0.901226泛化改善
E04AdamW1e-3645e-4余弦0.914532最优

这张表本身就是报告的核心内容。每一行的"备注"栏写清楚你观察到的现象,比任何华丽的可视化都有价值。写论文的朋友应该有体会,实验记录本记得细的人,写起来又快又准确。

日志方面,用 TensorBoard 或 W&B 都行,但最保险的是同时往文件里写一份纯文本。我吃过一次亏,某个可视化工具版本更新之后旧日志读不出来了,一整晚的实验记录全废。现在我的训练循环里一定会有这样几行:

with open("train_log.csv", "a") as f: f.write(f"{exp_id},{epoch},{train_loss:.4f},{train_acc:.4f}," f"{val_loss:.4f},{val_acc:.4f},{lr:.6f}\n")

7. 曲线异常排查链路:从 loss 不降到 NaN

7.1 loss 完全不下降:从过拟合一个 batch 开始

这是最高频的问题,也是排查成本最低的。第一件事永远是:拿 8 到 16 个样本,把模型过拟合到 100% 精度。如果这么小的数据都过拟合不了,说明代码有 bug,和优化策略无关。

def overfit_one_batch(model, loader, criterion, optimizer, device, steps=200): model.train() batch = next(iter(loader)) x, y = batch["x"].to(device), batch["y"].to(device) for step in range(steps): optimizer.zero_grad() logits = model(x) loss = criterion(logits, y) loss.backward() optimizer.step() if step % 20 == 0: acc = (logits.argmax(1) == y).float().mean().item() print(f"step {step:3d} loss={loss.item():.4f} acc={acc:.3f}")

正常情况下,200 步之内 loss 应该降到接近 0,acc 到 1.0。如果降不下去,按这个顺序查:

  1. 标签对不对。把几个样本的图和数据打出来看一眼,确认图片和标签没错位。数据加载器里 shuffle 之后索引错位是很隐蔽的 bug。
  2. 有没有忘记optimizer.zero_grad()。梯度会一直累加,参数更新方向完全错了。表现是 loss 前期抖一下然后卡住。
  3. 有没有忘记loss.backward()。这个更明显,参数根本不变,loss 一条直线。
  4. 学习率是否合适。用上面说的对数扫描,从 1e-2 开始试。
  5. 输入是否归一化。像素值 0~255 直接喂进网络,梯度尺度会大得离谱,用 0.1 的学习率也会爆。
  6. 损失函数和输出是否匹配。CrossEntropyLoss接收的是 logits,如果你的模型最后加了softmax,等于做了两次归一化,梯度会被压得极小。

按这个顺序走,绝大多数问题能在十分钟内定位。

7.2 loss 变 NaN:三类根因与对应验证方法

NaN 的排查要比"不下降"更系统一点,因为它的成因跨越好几个层面。

第一类是数值溢出。学习率过大、输入没归一化、exp或log出现在不该出现的地方。验证方法:把学习率除以 10 再跑一遍,如果 NaN 消失,那就是这个问题。另外,如果你自己实现了损失函数,检查有没有log(0),标准写法是log(p + eps)。

第二类是梯度爆炸。关键是看梯度范数:

total_norm = 0.0 for p in model.parameters(): if p.grad is not None: total_norm += p.grad.data.norm(2).item() ** 2 total_norm = total_norm ** 0.5 print(f"grad norm: {total_norm:.4f}")

正常情况下这个值应该在 0.1 到 10 之间。如果跑到几百上千,加梯度裁剪,同时检查学习率。

第三类是数据本身有问题。比如某个样本的标注是 -1(越界),CrossEntropyLoss会直接给出 inf;或者输入里有 NaN,前向一传就全崩。加一行断言就能防住:

assert torch.isfinite(x).all(), "输入里有 NaN 或 inf" assert (y >= 0).all() and (y < num_classes).all(), "标签越界"

混合精度训练(AMP)也是 NaN 的高发区。GradScaler本身有跳过溢出 step 的机制,但如果初始化时 scale 太大,会连续跳过很多步,表现出来就是"训练不动"。可以手动调小初始 scale。

7.3 训练集一直降、验证集不降:四种可能

这个现象见过太多次,根因通常在这四个里:

其一,训练集和验证集的预处理不一致。最常见的是验证集忘了做 Normalize,或者两步的 resize 尺寸不同。检查方法是分别取一个 batch 打印 mean 和 std。

其二,训练集和验证集有重叠。划分数据集时索引处理不当,同一个样本同时出现在两边。这个错误会让验证精度虚高,通常不会出现"验证不降",但如果重叠比例很小,也可能表现为验证曲线异常平坦。

其三,就是标准的过拟合。按第 5 节的顺序加正则化。

其四,BatchNorm 的统计量问题。如果训练集和验证集分布差异大(比如验证集样本很少),running stats 估计不准。这时候可以考虑换成 GroupNorm 或者 LayerNorm,它们不依赖 batch 统计量。

判断到底是哪个,最快的办法是把训练集的一个 batch 拿去评估,如果训练 batch 的验证精度也很低,那是模型或预处理问题;如果训练 batch 精度 100%,那基本就是过拟合或分布差异。

最后分享两个我在实验里反复用的小技巧

第一个是把训练曲线的关键点写进报告正文,而不是只贴一张图。比如"验证精度在 epoch 21 达到峰值 0.9012,此后持续下降,epoch 40 时降到 0.8731",这种带数字的描述比图更容易被评审抓住重点,也证明你真的读过自己的曲线。

第二个是每次改配置之前先备份 checkpoint 和日志。文件名带上实验编号和时间戳,比如E04_adamw_cosine_20240612.pt。本科实验里最心痛的情况,是跑到第 50 个 epoch 觉得有希望,顺手改了行代码想再试一个想法,结果覆盖了最优模型,只能重跑。

网络优化这个题目看着朴素,但真正跑过、认真对比过、能解释清楚每个参数为什么这么取的人,对训练这件事的理解会上一个台阶。哪怕最后精度只提升了一个点,只要那一个点能归因到你改的某个具体设置上,这个实验就是做成了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 15:03:47

数字序列3333333的技术解析与应用实践

1. 项目概述 "3333333"这个看似简单的数字序列&#xff0c;实际上蕴含着丰富的可能性。作为从业多年的数字文化观察者&#xff0c;我注意到这类重复数字组合往往在以下领域具有特殊意义&#xff1a; 游戏领域&#xff1a;可能代表角色等级、道具数量或特殊代码 密码…

作者头像 李华
网站建设 2026/10/27 15:03:42

DebugInOut插件:方法调用追踪与调试利器

1. DebugInOut 插件&#xff1a;开发者的调试利器在IDE插件生态中&#xff0c;调试工具一直占据着重要地位。DebugInOut作为一款专注于方法调用追踪的插件&#xff0c;通过轻量级注入的方式&#xff0c;为开发者提供了前所未有的运行时洞察能力。不同于传统调试器需要手动设置断…

作者头像 李华
网站建设 2026/10/27 15:03:18

AI编码代理前端设计质量工具impeccable:CLI与浏览器扩展实战

1. 从"impeccable"这个词说起&#xff1a;为什么前端设计总在AI编码环节掉链子 第一次看到"impeccable"这个词被拿来命名一个开发工具&#xff0c;我愣了一下。这个词在英文里是"无可挑剔的、完美的"意思&#xff0c;语气相当重。一个工具敢叫这…

作者头像 李华
网站建设 2026/10/27 15:03:18

C语言qsort对字符串内部字符排序的正确用法

1. 项目概述&#xff1a;为什么字符串字符排序不能直接套用qsort的常规写法&#xff1f;“C语言用qsort对字符串字符排序”——这句话乍看平平无奇&#xff0c;但括号里那句“解决字符串长度不统一的问题”才是真正的题眼。我第一次在某高校嵌入式课程助教岗位上看到学生交来的…

作者头像 李华
网站建设 2026/10/27 15:02:06

MVC架构安全防护全解析:从原理到实践

1. MVC安全架构深度解析在Web开发领域&#xff0c;MVC&#xff08;Model-View-Controller&#xff09;架构的安全性问题始终是开发者关注的焦点。我经历过多次因MVC安全漏洞导致的数据泄露事件&#xff0c;深刻理解安全防护必须贯穿整个架构的每一层。本文将基于实战经验&#…

作者头像 李华
网站建设 2026/10/27 15:01:50

Delft3D-MATLAB接口实战:高效提取FLOW结果与批量建模

简介&#xff1a;Delft3D-FLOW的MATLAB接口脚本包&#xff0c;面向水利、海洋与近海工程领域的科研和工程人员&#xff0c;旨在解决Delft3D模拟结果在MATLAB环境中读取、分析、可视化与二次处理不便的问题&#xff0c;能够灵活适配不同模拟边界条件与区域配置。该压缩包内共含6…

作者头像 李华