首页 / 资讯中心 / 文章详情

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

读懂loss曲线:深度学习网络优化实验的调参与异常排查 ★ FEATURED ARTICLE
批改本科深度学习课程的实验报告最容易看出一个人有没有真正跑过训练循环的不是代码写得多漂亮而是他能不能解释自己那条 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基线参照E1SGDMomentum0.10momentum0.9只改动量E2Adam1e-30默认 betas只改优化器E3AdamW1e-35e-4解耦衰减只改衰减方式E4SGDMomentum0.050减半学习率学习率敏感性这张表的每一行都能单独回答一个问题。写报告的时候把表格和对应的曲线图放在一起结论就是自洽的。我个人的经验是一张五行的实验矩阵比十张孤立的精度柱状图有说服力得多。还有一点每个配置至少跑 3 个随机种子报告里给均值和标准差。单次运行的精度差异在 1% 以内基本是噪声你拿着一组 90.2% 和 90.5% 的数字说A 比 B 好在严谨的评审眼里等于没做实验。2. 优化器不是越新越好先把朴素 SGD 跑明白2.1 为什么课程实验坚持让你先跑一遍 SGD很多同学一上手就torch.optim.Adam(model.parameters(), lr1e-3)跑出来曲线很漂亮然后就不管了。这个习惯在工程上没问题但在学习阶段是个坑。SGD 的更新规则是θ θ - lr * grad它只看当前这一步的梯度方向。这个笨方法有两个非常宝贵的性质。第一它对学习率极度敏感你必须认真对待学习率的量级而这个训练过程会逼你去观察 loss 曲线的形态。第二SGD 的梯度噪声本身带正则化效果在小数据集上SGD 收敛到的解往往在验证集上表现更好因为它倾向于找到平坦的极小值区域。我带的实验里有个反复出现的现象同一组配置Adam 训练集精度能到 99.5%验证集卡在 87%SGDMomentum 训练集只有 96%验证集反而到 89%。这不是偶然而是优化器特性决定的。所以我的建议是先用 Adam 快速验证你的模型和数据处理没写错因为它对初始化、学习率都不太挑再用 SGDMomentum 做最终结果两套数字都写进报告对比讨论泛化差距。import torch # 阶段一用 Adam 快速验证 pipeline 正确 opt_probe torch.optim.Adam(model.parameters(), lr1e-3) # 阶段二最终结果用 SGD Momentum Nesterov opt_final torch.optim.SGD( model.parameters(), lr0.1, momentum0.9, weight_decay5e-4, nesterovTrue, )2.2 Momentum 和 Adam 在损失曲面上的实际差别Momentum 的核心是把历史梯度做一个指数加权累积相当于给更新方向加了惯性。直观类比SGD 是一个人每走一步都重新看方向容易在峡谷状的损失曲面两侧来回横跳Momentum 是这个人推着一辆有质量的推车走横向的来回抖动会被平均掉纵向的下降会被加速。Adam 则是在 Momentum 的基础上再除以梯度平方的滑动平均的平方根等于给每个参数一个自适应步长。梯度一直很大的参数步长会自动缩小梯度一直很小的参数步长会被放大。这个机制对稀疏梯度特别友好比如 Embedding 层、或者特征稀疏的场景。但 Adam 有两个实际问题。一是它默认的 weight decay 是直接加在梯度上的L2 正则这和真正的权重衰减不等价会和大梯度参数耦合在一起修正方案是 AdamW把衰减从梯度更新里解耦出来。二是 Adam 在训练的后期容易在极小值附近来回震荡因为它对每个参数都用自适应步长缺乏逐渐收敛的机制这也是为什么很多流程最后会用学习率衰减来收尾。优化器典型学习率收敛速度泛化倾向适用场景常见坑SGD0.1~0.01慢较好有充足训练预算的最终训练学习率选错直接不收敛SGDMomentum0.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}, ], lr1e-3, ) print(fdecay: {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_ratio0.05, min_lr_ratio0.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_lambdabuild_lr_lambda(warmup_ratio0.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 是 batch64、lr0.01改成 batch256 时学习率可以开到 0.04。但这个规则有前提一定要配合 warmup。因为大学习率在训练初期非常危险前几步就可能把参数推到饱和区。我一般会设成lr base_lr * batch_size / 256并且把 warmup 比例提到 8%~10%。反过来如果你显存不够只能把小 batch 跑起来比如 batch16那学习率也跟着降否则梯度噪声太大训练会非常抖。这时候可以配合梯度累积来模拟大 batchaccum_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_norm1.0) optimizer.step() scheduler.step() optimizer.zero_grad()注意 loss 要除以积累步数否则梯度会被放大相应倍数等效于把学习率乘了 4——这个错误我见过不止一次表现出来就是用了梯度累积之后 loss 突然变 NaN。4. 训练早期的稳定性初始化、归一化与梯度裁剪的配合4.1 初始化为什么能决定前一百步的生死参数初始化经常被当成一个不用管的默认项因为 PyTorch 的nn.Linear和nn.Conv2d自带初始化。但当你在实验里手写多层 MLP、或者加自定义模块时问题就出来了。最典型的表现是网络加深到 8 层以上激活函数用 ReLUloss 卡在某个值完全不动比如十分类任务卡在 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, modefan_out, nonlinearityrelu) 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, modefan_out, nonlinearityrelu) elif isinstance(m, torch.nn.BatchNorm2d): torch.nn.init.ones_(m.weight) torch.nn.init.zeros_(m.bias) model.apply(init_weights)modefan_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_norm1.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_smoothing0.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(seed42): 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_size64, shuffleTrue, num_workers4, worker_init_fnworker_init_fn, drop_lastTrue, )drop_lastTrue这一点也值得说。如果最后一个 batch 只有 1 个样本BatchNorm 在训练时算出来的方差是 0会直接除以零loss 变 NaN。这类 bug 只在数据集大小不能被 batch size 整除时出现调试起来极其痛苦。至于cudnn.deterministicTrue会损失多少速度实测在中小规模模型上大约是 10%~20%对课程实验完全可接受。如果实在想快就关掉它但要在报告里注明结果存在少量波动。6.2 调参记录表的写法把每次实验当成一条实验记录做这个实验最常见的浪费是跑了一堆配置然后忘了哪个对应哪个。我建议从一开始就建一个 CSV 或者 Markdown 表格每跑一次追加一行实验号优化器lrbswd调度best_val_accbest_epoch备注E00SGD0.1640无0.871258基线后期震荡E01SGDM0.1640无0.893447收敛明显更快E02Adam1e-3640无0.885121前期快后期平台E03AdamW1e-3645e-4无0.901226泛化改善E04AdamW1e-3645e-4余弦0.914532最优这张表本身就是报告的核心内容。每一行的备注栏写清楚你观察到的现象比任何华丽的可视化都有价值。写论文的朋友应该有体会实验记录本记得细的人写起来又快又准确。日志方面用 TensorBoard 或 WB 都行但最保险的是同时往文件里写一份纯文本。我吃过一次亏某个可视化工具版本更新之后旧日志读不出来了一整晚的实验记录全废。现在我的训练循环里一定会有这样几行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 不降到 NaN7.1 loss 完全不下降从过拟合一个 batch 开始这是最高频的问题也是排查成本最低的。第一件事永远是拿 8 到 16 个样本把模型过拟合到 100% 精度。如果这么小的数据都过拟合不了说明代码有 bug和优化策略无关。def overfit_one_batch(model, loader, criterion, optimizer, device, steps200): 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(fstep {step:3d} loss{loss.item():.4f} acc{acc:.3f})正常情况下200 步之内 loss 应该降到接近 0acc 到 1.0。如果降不下去按这个顺序查标签对不对。把几个样本的图和数据打出来看一眼确认图片和标签没错位。数据加载器里 shuffle 之后索引错位是很隐蔽的 bug。有没有忘记optimizer.zero_grad()。梯度会一直累加参数更新方向完全错了。表现是 loss 前期抖一下然后卡住。有没有忘记loss.backward()。这个更明显参数根本不变loss 一条直线。学习率是否合适。用上面说的对数扫描从 1e-2 开始试。输入是否归一化。像素值 0~255 直接喂进网络梯度尺度会大得离谱用 0.1 的学习率也会爆。损失函数和输出是否匹配。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(fgrad 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 觉得有希望顺手改了行代码想再试一个想法结果覆盖了最优模型只能重跑。网络优化这个题目看着朴素但真正跑过、认真对比过、能解释清楚每个参数为什么这么取的人对训练这件事的理解会上一个台阶。哪怕最后精度只提升了一个点只要那一个点能归因到你改的某个具体设置上这个实验就是做成了。
阅读完成 · 觉得有帮助?
咨询建站