
调 PyTorch 模型调到我这个阶段被同行问得最多的问题之一就是正则化到底该加哪几个。这个问题看着基础实际上坑很深有人把weight_decay一设就以为万事大吉结果验证集照样崩有人在 CNN 里堆了三层Dropout训练损失都压不下去还有人把BatchNorm当万能药小 batch 训练时验证指标一路飘。PyTorch 里的正则化不是一个模块、一个参数能解决的事它散落在优化器、网络层、数据管道和训练循环四个地方每一处都有各自的作用机制和适用边界。这篇文章我把这些年踩过的坑、做过的对比实验、以及每个方法背后为什么这么设计的逻辑梳理一遍从参数层、结构层、数据层到流程层逐个拆开讲最后给一张按任务类型选型的对照表。不管你是刚接手的模型训练新手还是已经在调大模型微调的老手应该都能从里面找到能直接抄作业的配置和判断依据。1. 正则化在 PyTorch 里的真实分布先看信号再选药很多人对正则化的第一反应是加个 L2 就行这套思路在传统机器学习里成立在 PyTorch 里就有点想当然了。原因是 PyTorch 把正则化的实现拆得很散优化器管一部分torch.nn里的层管一部分torchvision或自定义的 Dataset 管一部分剩下的一大块要靠你在训练循环里手写。你如果只盯着优化器的weight_decay等于只用了这套工具箱里最钝的一把刀。我一般跟人讲选正则化方法之前先别急着看代码先看你的训练曲线在说什么。过拟合不是一个抽象概念它在日志里有非常具体的形状你认得出来才知道该往哪个方向加东西。1.1 过拟合的三种典型信号认清再动手第一种信号最典型训练损失还在稳稳下降验证损失降到某个点之后开始反弹两条曲线张成一个喇叭口。这个形态说明模型已经开始记忆训练集里的噪声此时增加数据、加强增强、加 Dropout 都有意义而单纯调小学习率只能延缓不能解决。第二种信号是 gap 固定但不收敛。训练准确率 99%验证准确率卡在 86% 不动两条线平行。这种情况往往不是过拟合太严重而是训练集和验证集的分布本身有差异或者模型容量对当前数据量来说偏大。我先怀疑数据划分和预处理是否一致再去考虑模型容量最后才轮到正则化。第三种信号容易被忽略验证指标方差极大。同一个模型跑三次验证准确率分别是 84.2%、86.9%、83.5%波动超过两个点。这不是过拟合这是训练不稳定可能是 batch size 太小、学习率太高、或者 BN 统计量在波动。此时加正则化只会让情况更糟你应该先处理优化过程的稳定性。提示在动手加任何正则化之前先把训练集和验证集的 loss、指标、学习率、梯度范数都记到日志里。没有这几条曲线选正则化就是盲猜。1.2 PyTorch 把正则化拆成了四层别只盯一层我把 PyTorch 里的正则化手段按作用位置分成四层这个分类方式对我自己排查问题特别有用层级作用位置典型手段是否改变模型结构参数层优化器更新时weight_decay、L1 惩罚、max-norm 约束否结构层前向传播内部Dropout、DropPath、DropBlock、噪声层是数据层输入与标签数据增强、Mixup、CutMix、Label Smoothing否流程层训练循环Early Stopping、EMA、梯度裁剪否这四层的成本完全不一样。参数层几乎零成本改一行就行数据层的增强会拖慢数据加载但对泛化的收益通常最大结构层改动要重新调网络流程层最容易被低估一个正确的 EMA 往往比堆两层 Dropout 更有效。还有一个经常被搞混的点归一化层到底算不算正则化。BatchNorm 在训练时用当前 batch 的均值方差做归一化这个统计噪声确实带来了正则化效应但它本质是优化稳定手段不能当正则化主菜来用。这一点我在第 6 节会展开讲。2. 参数层正则化weight_decay、L1 与 AdamW 的取舍参数层是最容易上手、也最容易用错的一层。用错的核心原因在于很多人默认weight_decay就等于 L2 正则这个等式在 SGD 下成立在自适应优化器下不成立。这个差别不是学术细节它直接决定了你的模型在大学习率下会不会被正则化过头或者根本没正则化。2.1 weight_decay 加在优化器里而不是损失函数里L2 正则的数学形式是在损失上加一项λ/2 * ||w||²梯度里就多出λw。在 SGD 里w w - lr*(g λw)和权重每次更新时乘一个衰减因子在数学上等价当 λ 与 lr 匹配时所以 PyTorch 的optim.SGD(weight_decay...)直接把它实现在更新步骤里你不需要手动往 loss 里加。在优化器里实现有个隐性好处weight_decay只作用于你传给优化器的参数组。这就引出一个非常实用的技巧bias 和归一化层的参数不应该被衰减。原因很直接bias 的衰减会破坏模型的平移能力BN 的gamma衰减会破坏归一化的表达能力。我一般的写法是这样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): no_decay_params.append(param) else: decay_params.append(param) optimizer torch.optim.AdamW([ {params: decay_params, weight_decay: 0.01}, {params: no_decay_params, weight_decay: 0.0}, ], lr1e-4)判断条件是param.ndim 1因为归一化层的weight、bias以及所有 bias 都是一维张量卷积和线性层的权重是高维的。这个写法在视觉和 NLP 任务里通吃我用了好几年没出过问题。weight_decay的量级需要按优化器区分优化器常用 weight_decay说明SGD momentum5e-4 ~ 1e-4与 L2 严格等价量级偏小Adam1e-4 ~ 1e-5等价性被破坏需更小AdamW0.01 ~ 0.1解耦实现量级大得多LAMB / Lion0.01 ~ 0.1大体与 AdamW 接近看到这张表就知道为什么很多人从 SGD 换到 Adam 之后效果变差他们把weight_decay5e-4原样抄过去在 Adam 的自适应分母下这个衰减强度被大幅削弱甚至扭曲等于没正则化。2.2 L1 没人替你实现稀疏性诉求要想清楚PyTorch 没有内置 L1 正则。想要它你得自己在训练循环里加l1_lambda 1e-5 for batch in loader: optimizer.zero_grad() loss criterion(model(x), y) l1 sum(p.abs().sum() for p in model.parameters() if p.ndim 1) (loss l1_lambda * l1).backward() optimizer.step()注意两点。第一L1 对高维权重逐元素加绝对值计算开销不小参数量上亿的模型每个 step 多一次全参数遍历会明显拖慢训练。第二abs()在零点不可导PyTorch 的 autograd 在零点取的次梯度是 0实践中没出过问题但你得知道这不是严格数学意义上的次梯度实现。L1 的价值在于产生稀疏权重适合特征维度极高、你希望模型自动做特征选择的场景比如传统特征工程出来的上千维表格特征或者你需要给出可解释的权重重要性排序。但我要提醒一句稀疏权重不等于推理加速。现在主流硬件的稀疏计算支持很有限p.abs() 1e-6的比例到 80% 也不会让你的矩阵乘变快。如果你的目的是压缩模型剪枝比 L1 直接得多。注意L1 和 L2 不要同时大剂量加。我试过weight_decay1e-3配l1_lambda1e-4模型在第三个 epoch 就把大部分权重压到接近零训练损失直接躺平。真要混用L1 取 1e-6 到 1e-5 这个量级。2.3 AdamW 和 Adam 的差别不是一行代码的事Adam 的更新里梯度会除以梯度平方的滑动平均的平方根。当你在梯度里加上λw之后这一项也会被这个自适应分母缩放导致大权重的惩罚被削弱、小权重的惩罚被放大整体行为偏离了真正的 L2。AdamW 的做法是把衰减从梯度里拿出来直接在参数更新时做w w - lr * λ * w不再经过自适应分母。这个差别在小学习率下不明显学习率上到 3e-4 以上、训练步数上万时差距就出来了。我在一个中等规模图像任务上做过对比同样的网络、同样的数据、同样的学习率调度只换优化器配置训练损失验证准确率验证损失Adam, wd1e-40.02187.4%0.62AdamW, wd0.010.03489.1%0.48AdamW, wd0.050.04188.6%0.51AdamW 的训练损失更高、验证指标更好这就是正则化起作用的典型表现。如果你发现自己换用 AdamW 之后训练损失降不下来了别慌先看验证集很可能是在往好的方向走。3. 结构层正则化Dropout 家族与随机深度结构层正则化改的是前向传播本身所以它和模型架构绑得最紧。nn.Dropout是大家最熟的但它只是这个家族里的一个成员卷积网络、循环网络、Transformer 各自有更合适的变体用错版本的效果差距非常大。3.1 nn.Dropout 的 train/eval 语义误用率最高的地方nn.Dropout(p)在训练时以概率p把元素置零并把保留下来的元素除以1-p这叫 inverted dropout目的是让推理时的期望值不变所以推理阶段直接恒等映射。翻译成人话训练时随机关掉一部分神经元推理时全部打开。这里有个必须记住的细节缩放发生在训练阶段不是推理阶段。早期有些框架是反过来做的推理时乘1-p如果你从别的框架迁移代码千万别把缩放逻辑抄错。model.train()和model.eval()的切换是所有误用里最高频的。我见过不止一次验证阶段忘了model.eval()Dropout 还在随机丢弃BN 还在用 batch 统计量验证指标自然一塌糊涂。正确的验证循环长这样model.eval() with torch.no_grad(): for x, y in val_loader: logits model(x) ... model.train()反过来的错误更隐蔽训练时忘了model.train()。模型从加载开始默认是 training 模式一般情况下没问题但如果你在验证之后没有切回来后续所有训练都在 eval 模式下跑Dropout 完全失效BN 用 running stats 更新训练曲线看起来特别平滑实际泛化能力很差。关于p的取值我的经验是全连接层用 0.5 是经典配置但在小数据集上样本量几千级别0.5 经常导致欠拟合0.2 到 0.3 更稳卷积层后面接 Dropout 收益有限不如换成 DropBlock 或直接靠数据增强。3.2 Dropout2d、AlphaDropout、DropBlock 各管一段nn.Dropout2d是以通道为单位丢弃而不是逐元素。原因很直观卷积特征图在空间上是高度相关的相邻像素往往同时激活逐元素丢弃相当于只是给特征加了点噪声整条通道丢弃才能真正阻断某条特征通路。用在全卷积网络、分割网络里效果比普通 Dropout 好得多。nn.Dropout3d同理用于视频或体数据。nn.AlphaDropout是给 SELU 激活配套的它保证丢弃后输出的均值和方差不变从而维持自归一化性质。如果你用的是 ReLU 系激活别用它没有意义。DropBlock不在torch.nn里需要自己实现或从外部引入。它的思路是丢弃一个连续的方形区域而不是单个位置对卷积层更有效。核心实现大概是这样def drop_block(x, block_size7, gamma0.1, keep_prob0.9): N, C, H, W x.shape gamma gamma * (H * W) / (block_size ** 2) / ((H - block_size 1) ** 2) mask torch.bernoulli(torch.full((N, C, H, W), gamma, devicex.device)) mask -F.max_pool2d(-mask, block_size, stride1, paddingblock_size // 2) mask 1 - mask return x * mask * (mask.numel() / mask.sum())这个实现里max_pool2d的技巧值得说一下对负 mask 做最大池化等价于对正 mask 做膨胀能把单个采样点扩展成块。归一化系数保证期望不变。块大小一般取 7gamma从 0.1 开始调训练后期可以线性降到 0这个 schedule 在不少视觉任务上有额外收益。3.3 DropPath 与 Stochastic Depth深层网络的标配DropPath又叫随机深度在 ResNet 类残差结构和 Transformer 里几乎是标配。它的做法是以一定概率把整个残差分支的输出置零只保留 shortcut。对 Transformer 来说就是把整个 block 跳过。def drop_path(x, drop_prob0.0, trainingFalse): if drop_prob 0.0 or not training: return x keep_prob 1 - drop_prob shape (x.shape[0],) (1,) * (x.ndim - 1) mask x.new_empty(shape).bernoulli_(keep_prob) return x * mask / keep_prob注意 mask 的形状是第一维batch加若干 1也就是按样本丢弃同一个样本的所有通道一起丢。这和 Dropout 逐元素丢完全不是一回事。drop_prob的设置有个经典方案浅层小、深层大。因为深层冗余度高丢一点不影响浅层特征还没提取好丢了伤害大。常见做法是按层线性从 0 递增到 0.1 或 0.2。我在一个 24 层的 Transformer 上试过统一 0.1 和线性递增后者收敛更稳最终指标高 0.6 个点左右。提示DropPath 和 Dropout 不要在同一个 block 里叠加使用。两个随机丢弃叠加等效丢弃率会超过你的预期训练很容易卡住。4. 数据层正则化增强、Mixup、CutMix 与标签平滑我一直认为数据层是性价比最高的一层。参数层改一行收益有限数据层的增强做对了往往能把验证指标往上顶好几个点。代价是数据管道的复杂度上升以及训练时间变长这个 trade-off 需要你自己权衡。4.1 数据增强是最便宜也最有效的正则化增强的本质是扩大训练分布的覆盖范围让模型见不到的样本变少从而降低过拟合。分类任务里的常规组合是随机裁剪加翻转加颜色抖动torchvision.transforms里有现成实现。这里我强调两个经常被做错的地方。第一个是验证集不能加随机增强。训练用RandomResizedCrop验证必须用确定性的Resize CenterCrop否则你每次评估的输入都不一样指标没有可比性。这个问题在新手里出镜率极高。第二个是增强强度要和训练轮数匹配。RandAugment的num_ops设成 2、magnitude设成 9在 300 个 epoch 的训练里是合理的你只跑 30 个 epoch 还这么加模型根本学不动。增强太强会导致欠拟合表现是训练损失降不下去、验证损失也不降这时候不是加正则化是减。RandomErasing也就是随机擦除对分类任务有稳定收益p0.5、擦除面积比例 0.02 到 0.4 是比较通用的设置。它和 DropBlock 的思路类似都是在输入或特征上制造局部缺失逼迫模型不要依赖单一判别区域。4.2 Mixup 与 CutMix 的实现要点损失函数要跟着改Mixup 的做法是把两个样本按比例线性混合标签也按同样比例混合lam np.random.beta(alpha, alpha) # alpha 常取 0.2 或 0.4 x_mix lam * x1 (1 - lam) * x2 y_mix lam * y1 (1 - lam) * y2 loss lam * ce(logits, y1) (1 - lam) * ce(logits, y2)alpha的取值决定了混合比例分布的形状。alpha0.2时lam大概率接近 0 或 1混合程度轻alpha1.0时分布均匀混合程度重。视觉分类任务里0.2用得最多0.4适合数据量更大的场景。CutMix 不混合像素而是在一张图上贴另一张图的矩形区域标签按面积比例混合lam np.random.beta(1.0, 1.0) bbox rand_bbox(x1.size(), lam) x_mix x1.clone() x_mix[:, :, bbox[0]:bbox[2], bbox[1]:bbox[3]] x2[:, :, bbox[0]:bbox[2], bbox[1]:bbox[3]] lam 1 - ((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]) / (H * W))CutMix 相比 Mixup 的优势是不产生半透明的怪异图像更接近真实分布在检测和分割任务上迁移性更好。这两招在目标检测和语义分割里要格外小心因为标签不是单一类别混合后的框和掩码需要同步处理直接套用分类的实现会出错。另外 Mixup 和 Label Smoothing 同时用的时候要留意两者都在软化标签叠加过强会让模型学不到明确的决策边界。4.3 Label Smoothing 的坑比你想的多nn.CrossEntropyLoss(label_smoothing0.1)一行就能开。它的原理是把 one-hot 标签[0, 0, 1, 0]改成[0.025, 0.025, 0.925, 0.025]防止模型对正确类别输出过高的置信度。这个改动抑制了 logits 无限增大的趋势间接起到正则化作用。坑在于它和几个东西冲突。第一知识蒸馏。蒸馏需要教师模型的软标签分布Label Smoothing 会把软标签里的类别间相似性信息抹平学生模型学到的东西变少。第二模型校准。Label Smoothing 会让输出的置信度偏低如果你的下游需要置信度 0.9 就代表 90% 准确这个假设会失效。第三与 Focal Loss 叠加。Focal Loss 的权重依赖于预测概率标签被平滑后这个权重计算会偏离设计意图。我的建议是只在单纯的分类任务里用smoothing取 0.05 到 0.1超过 0.2 收益递减甚至有害。做过蒸馏或者需要概率校准的场景直接关掉。5. 训练流程层正则化早停、EMA、梯度处理流程层是最容易被低估的一层。我见过太多人把精力全花在网络结构上训练循环写得非常朴素没有早停、没有 EMA、没有梯度监控白白浪费了已经训练出来的好权重。5.1 Early Stopping 的正确姿势不只是 patience早停的逻辑很简单监控验证指标连续若干个 epoch 没提升就停同时保存最好的权重。真正的细节在三个参数上。patience决定了你能容忍多长的平台期。太小会在指标还在震荡上升时就停掉太大则浪费算力。我的经验值是学习率调度有 step 下降的patience至少要覆盖一个调度周期用 cosine 退火的干脆别早停直接跑完。min_delta设一个最小提升阈值比如 1e-3。不设的话验证指标在 0.0001 量级上反复波动也会被当成提升best_score一直在更新早停永远不触发。mode要设对验证准确率是max验证损失是min。这个参数写反了早停会在第一个 epoch 就触发非常隐蔽。best_score, counter None, 0 for epoch in range(epochs): val_metric validate(model, val_loader) score val_metric if mode max else -val_metric if best_score is None or score best_score min_delta: best_score, counter score, 0 torch.save(model.state_dict(), best.pt) else: counter 1 if counter patience: break有一个认知必须纠正用验证集做早停等于在验证集上做了模型选择。如果你的报告里用的还是这个验证集的指标数字会偏乐观。严谨的做法是切出 train/val/test 三份val 用来早停和调参test 只在最后跑一次。5.2 EMA权重平均为什么能提升泛化指数滑动平均维护一份参数的影子副本每一步用shadow decay * shadow (1 - decay) * param更新。因为 SGD 在损失曲面上的轨迹会来回震荡平均之后的点往往落在更平坦的区域泛化更好。class EMA: def __init__(self, model, decay0.999): self.shadow {k: v.clone().detach() for k, v in model.state_dict().items()} self.decay decay torch.no_grad() def update(self, model): for k, v in model.state_dict().items(): if v.dtype.is_floating_point: self.shadow[k].mul_(self.decay).add_(v, alpha1 - self.decay) else: self.shadow[k].copy_(v)decay的取值和训练步数有关。步数上万0.999合适步数只有几千0.99更稳。还有个很多人忽略的点训练初期要做 warmup因为初始权重离最优很远直接以 0.999 的衰减平均影子权重会被早期垃圾值拖住很久。常见做法是decay min(0.999, (1 step) / (10 step))。还有一个坑EMA 权重里的 BN 统计量是训练过程中累积的和影子权重本身不匹配。严格做法是在训练结束后用一部分训练数据重新跑一遍前向重估 BN 的 running stats。不做这一步EMA 的收益会打折扣。5.3 梯度裁剪与梯度噪声稳定性和泛化的两件事torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)这行代码几乎所有 Transformer 训练脚本里都有。它的作用是防止个别 batch 产生巨大梯度把参数打飞属于稳定性手段不是严格意义上的正则化但它间接让训练更平滑泛化也更稳。调用顺序很容易写错必须在loss.backward()之后、optimizer.step()之前调用且要在optimizer.zero_grad()之后的新一轮梯度上调用。我见过有人把它放在zero_grad前面裁的是上一轮的梯度完全无效。max_norm的取值要看梯度范数的实际分布。我一般先跑几百步把clip_grad_norm_的返回值打出来看中位数取中位数的 2 到 3 倍作为阈值。直接抄 1.0 有时候过紧会拖慢收敛。梯度噪声是另一个思路给梯度加高斯噪声让优化轨迹更随机有理论工作表明它能带来泛化收益。实际操作里加噪强度很难调加多了不收敛加少了没效果我一般只在对抗训练或隐私相关的场景用普通任务收益不明显。6. 归一化层到底算不算正则化这个争论值得说清BatchNorm 在训练时用当前 batch 的均值和方差归一化每个 batch 的统计量都不一样这给前向传播引入了随机性客观上起到了正则化作用。这也是为什么很多网络加了 BN 之后Dropout 可以适当减少的原因。但把 BN 当正则化用是会出问题的我给你算一笔账。BN 的归一化公式是(x - μ) / σ * γ β其中γ和β是可学习参数。假设 weight decay 想把某一层的权重压小输出变小BN 之后又被归一化回去了γ会自适应地放大来补偿。也就是说BN 在某种程度上吸收了 weight decay 的效果这也是一部分研究认为 BN 网络里 weight decay 作用被削弱的原因。更实际的问题是 batch size。BN 依赖 batch 统计量batch 小于 8 的时候均值和方差的估计噪声很大训练和推理的行为差异明显验证指标会抖。这种场景下我会直接换GroupNorm或LayerNorm它们不依赖 batch 维度小 batch 下稳定得多。检测和分割任务普遍用小 batch所以这两个领域 GroupNorm 用得很多。归一化层依赖 batch 统计小 batch 表现典型场景BatchNorm是差大 batch 图像分类LayerNorm否好Transformer、NLPGroupNorm否好检测、分割、小 batchInstanceNorm否好风格迁移、生成模型还有一个组合细节BN 层后面接 Dropout 的顺序。经典 CNN 里的顺序是 Conv → BN → ReLU → DropoutDropout 放在激活之后。如果反过来把 Dropout 放在 BN 之前被丢弃的位置会参与 BN 的统计量计算导致均值方差估计有偏。这个顺序错了不会报错但会影响效果。7. 场景化选型从任务类型反推组合方案讲完各个方法最实际的问题还是我的任务该选哪几个。我给一张按场景划分的组合表这些都是我自己实际跑过、或者从同行那里验证过的配置可以直接作为起点。7.1 小数据集图像分类增强为主Dropout 为辅样本量在几千到几万这个区间过拟合是主要矛盾。我的默认组合是RandomResizedCrop加水平翻转加ColorJitter加RandomErasing优化器用 AdamW 配weight_decay0.05网络里在全连接层前加Dropout(0.3)训练用 cosine 退火不早停最后接 EMA。这个组合里增强承担了大部分正则化压力Dropout 只是补刀。我试过在这个配置下把 Dropout 开到 0.5训练损失明显降不下去最终指标反而低了 1 个多点。小数据集上模型容量本来就吃紧Dropout 太狠会让它学不动。如果数据量再小一些比如每类只有几十张那就该上迁移学习了把主干冻结只训分类头这时候正则化的需求会大幅下降weight_decay调到 1e-4 就够。7.2 大模型微调与 NLPweight_decay 分层加 DropPathTransformer 类模型的微调我基本固定用 AdamWweight_decay分两组bias 和 LayerNorm 参数设 0其余设 0.01。这是因为预训练权重已经很成熟过大的衰减会破坏预训练学到的表示。结构上DropPath按层递增 0 到 0.1attention 和 FFN 内部各加一个Dropout(0.1)embedding 后加Dropout(0.1)。这个配置在微调场景下比较温和不会把预训练特征冲掉。还有一个容易忽略的点微调时不要开 Label Smoothing 的大剂量。预训练阶段可能已经用过微调再叠一层模型对下游标签的拟合能力会被压制。我一般在微调时把它关掉或者只留 0.05。7.3 时序与表格数据别照搬图像那一套表格数据我踩过的最大坑是把图像的正则化方案直接搬过来。表格特征各列量纲不同、相关性结构复杂BatchNorm在这里经常帮倒忙Dropout加在 MLP 里效果也一般。我的做法是先用LayerNorm或者直接标准化输入weight_decay适度加大到 1e-3 级别表格模型参数少承受得住重点靠早停控制过拟合训练轮数不要设太多。时序任务里Dropout要小心用在 RNN 的时间维度上同一个序列的不同时间步应该用同一个 mask否则会破坏时序一致性。PyTorch 的nn.RNN系列有dropout参数但它只作用在层与层之间层内的时序 Dropout 需要自己实现。7.4 组合对照表场景参数层结构层数据层流程层小数据图像分类AdamW 0.05Dropout 0.3强增强 RandomErasingEMA cosine大数据图像分类SGD 5e-4DropBlock 或不用RandAugment MixupEMATransformer 微调AdamW 0.01分层DropPath 0~0.1 Dropout 0.1轻增强梯度裁剪 warmup表格数据Adam 1e-3一般不用特征标准化早停为主检测分割SGD 1e-4GroupNorm DropBlockMosaic 类增强梯度裁剪这张表不是教条起点而已。真正的调参过程是根据验证曲线动态调整gap 大就加数据层训练不稳就调流程层收敛太慢就减结构层。8. 常见问题与排查实录最后这部分是我这些年被问最多、自己也踩最多的问题整理成速查形式。每一个都是真实遇到过的现象排查顺序按我的实际习惯排。8.1 训练损失降不下去先别怀疑正则化现象训练 loss 从第一个 epoch 就很高降得非常慢验证 loss 也高。很多人第一反应是正则化太强了于是把weight_decay调小、Dropout 关掉结果还是不动。正确的排查顺序是先看学习率是不是太小再看数据预处理是不是有 bug比如归一化参数写错、标签和图像错位再看损失函数用得对不对分类用了 MSE 之类最后才怀疑正则化。正则化过强的典型表现是训练 loss 能降但降不到很低和根本不降是两回事。我在一个项目里遇到过训练 loss 卡在 2.3 不动查了两小时发现是transforms.Normalize的 mean 和 std 写反了输入分布完全错乱。这种问题加多少正则化都救不回来。8.2 训练好验证差按 gap 大小分档处理这是过拟合的标准场景但处理方式要看 gap 有多大。gap 在 5 个点以内加数据增强和适度weight_decay通常够gap 在 5 到 15 个点要上 Mixup、CutMix 或者加大 Dropoutgap 超过 15 个点说明模型容量相对数据量严重过剩减层或者换小模型比加正则化更直接。还有一个隐蔽原因训练和验证的预处理不一致。训练用了随机裁剪验证忘了改成确定性裁剪模型在验证时看到的图像尺度和训练时不一样指标自然差。这个坑我见过至少五次每次都有人排查半天网络结构。8.3 常见问题速查表现象最可能原因处理方式验证指标剧烈震荡batch 太小、BN 不稳、学习率过高换 GroupNorm、降学习率、加大 batch换 AdamW 后训练 loss 不降weight_decay 设太大从 0.01 起试或暂时设 0Dropout 加了没效果忘了 model.train() 或 p 太小检查模式切换p 提到 0.2 以上Mixup 后收敛变慢很多alpha 太大从 0.2 起调配合更长训练轮数EMA 权重指标不如原始权重没做 warmup 或没重估 BN加 warmup训练后重估 running stats梯度裁剪没效果调用顺序错了确认在 backward 之后、step 之前早停从不触发min_delta 太小或 mode 写反设 1e-3检查 max/min再说两个文档里不会写的心得。第一正则化参数不要一次调多个。我一般固定其他所有设置只动一个参数跑三到五个 seed 看均值因为单次训练的方差经常比参数带来的差异还大。第二验证集指标提升 0.3 个点以内不要当成真实收益。小数据集的评估噪声很容易超过这个量级你需要多次重复才能确认。还有一个我个人的习惯每次实验都存下完整的配置文件包括所有正则化参数、随机种子、数据划分方式。过一个月回头复现的时候你会感谢当时的自己。模型训练这件事可复现性比单次指标重要得多。