外观
常见陷阱与反模式
一句话定义:深度学习项目里 80% 的失败不是"理论不够",而是反复踩进同一批工程陷阱——本页把它们逐个解剖,给出症状、原因与解法,让你踩过一次就能识别。
这些陷阱跨越数据、训练、评估、部署全链路。排查方法论的完整流程见调试与诊断,预防它们的系统性原则见DL 设计原则。
一、数据泄漏:测试集信息混入训练
- 症状:离线指标惊人(99%+),线上效果大幅缩水;或模型在训练集上"过拟合得过于完美"。
- 原因:归一化统计量、特征工程(如标准化用的均值/方差)、类别编码等只该由训练集计算的东西,混入了全量数据(含测试集)。
- 解法:python对时序数据还要防时间泄漏(用未来的信息预测过去);对用户数据要防身份泄漏(同一用户的样本不能同时进训练和测试)。完整清单见评估实践。
# 错误:用全部数据的统计量 mean, std = all_data.mean(), all_data.std() # 正确:只拟合训练集,测试集用同一套统计量 mean, std = train_data.mean(), train_data.std()
二、评估泄漏:在测试集上调参
- 症状:测试集准确率反复"涨"到离谱,但模型换数据后崩盘。
- 原因:测试集被当成验证集用——每跑一次实验看一眼测试集分数,本质上等于把测试集"训练"进了模型选择。
- 解法:严格遵守 train / val / test 三层划分。调参、选模型、早停、选 epoch 全部在验证集上;测试集只在最终验收时用一次。如果必须多次看测试集,至少记录每次结果并意识到指标已经被污染。
三、学习率错误:过大、过小、不调度
| 症状 | 原因 | 解法 |
|---|---|---|
| loss 一开始就 NaN/发散 | 学习率过大 | 范围测试法找到合理区间(见训练配方与调参) |
| loss 下降极慢、像"爬行" | 学习率过小 | 提高 10 倍重试;仍不行检查数据/模型 |
| 训练集拟合了,验证集上不去 | 学习率无调度、后期震荡 | 加余弦退火或按验证集折半 |
| Adam 不收敛 | 忘了 Adam 也要调学习率(默认 1e-3 是起点不是终点) | 范围测试 + 调度 |
学习率是"最便宜、影响最大"的超参数,出错时优先查它。
四、忘记归一化
- 症状:相同配置下 loss 偏高、收敛慢或震荡;换初始化结果剧烈变化。
- 原因:输入范围不对(0–255 或 0–1 混用),权重初始化的尺度与输入尺度不匹配(机理见初始化与归一化)。
- 解法:训练前检查
x.min()/x.max(),统一归一化到 [0,1] 或标准化;图像任务直接用预训练模型时,必须用该模型配套的均值和标准差(如 ImageNet 的(0.485, 0.456, 0.406))。
五、类别不平衡处理不当
- 症状:准确率看着不错(90%+),但小类别全是 0;F1 惨不忍睹。
- 原因:默认的交叉熵损失被多数类主导,模型学会"全猜多数类"。
- 解法(按性价比排序):
- 改评估指标:别用准确率,用 per-class precision/recall/F1(见评估实践)。
- 加权损失:
CrossEntropyLoss(weight=class_weights),权重与类别频率成反比。 - 重采样:过采样少数类 / 欠采样多数类(注意别对测试集做)。
- 换损失:Focal Loss(难样本加权)用于极端不平衡的检测任务。
python# 加权损失示例 counts = torch.bincount(train_labels, minlength=n_classes).float() class_weights = 1.0 / counts class_weights = class_weights / class_weights.sum() * n_classes criterion = nn.CrossEntropyLoss(weight=class_weights.to(device))
六、不固定种子导致不可复现
- 症状:同一份代码两次运行结果不一样;"昨天 90%,今天 87%";无法向别人/自己解释成绩。
- 原因:随机初始化、数据 shuffle、增强采样、cuDNN 算法选择都引入随机性,全都没被固定。
- 解法:固定 torch/numpy/random 种子 +
cudnn.deterministic(代码模板见训练配方与调参)。注意:固定种子是让"实验可复现",不代表模型更好——评估时反而应该用多个种子取均值(评估方法见评估实践)。
七、在验证集上过拟合
- 症状:验证集分数持续上涨直到 99%,换数据立刻暴跌。
- 原因:验证集太小或用了太多次——每个 epoch 都"看"验证集选最佳 checkpoint,选多了验证集本身也被拟合。
- 解法:
- 验证集要足够大且固定(不小于数千样本)。
- 早停、选 checkpoint 时不要反复在同一批验证样本上挑最好的;至少保留一个更"隔离"的最终测试集。
- 用小验证集做高方差决策(如比较两个相近配置)时,必须用置信区间判断差异是否显著。
八、BatchNorm 陷阱
两个高频翻车点:
1. batch size 太小
- 症状:训练 loss 震荡、验证指标差且不稳定;batch 内统计量噪声大。
- 原因:BatchNorm 用当前 batch的均值和方差,batch 太小(<16)时估计失真;推理时又用累计统计量,训练/推理行为撕裂。
- 解法:batch 尽量 ≥32;小 batch 场景换
GroupNorm/LayerNorm,或增大 batch(可用梯度累积)。
2. 训练/推理行为不一致
- 症状:训练集上指标正常,测试时整体偏差。
- 原因:评估时忘记
model.eval(),BatchNorm 还在用 batch 统计量而不是累计统计量。 - 解法:评估前调用
model.eval();反向传播前确认model.train()。
python
# 评估必须这样写
model.eval()
with torch.no_grad():
... # 推理九、混合精度(AMP)bug
- 症状:训练中途偶发 NaN;某些层输出全 0;CPU 与 GPU 结果不一致。
- 原因:float16 动态范围小(上界约 65504),大激活、小梯度都会溢出/下溢;自定义算子没写 fp16 内核,被静默降级或出错。
- 解法:
- 用官方
GradScaler而非手写缩放; - 对自定义损失/算子,检查其 fp16 行为,必要时强制该部分用 fp32;
- 出现 NaN 时,先用纯 fp32 复现,确认问题是否与 AMP 相关;
- 逐个排除:
torch.autocast的enabled=False快速对比。
- 用官方
十、预训练模型微调学习率过大
- 症状:微调前期 loss 剧烈升高后不再恢复,或精度反而不如冻结主干时。
- 原因:预训练权重已经很接近好的解,用从头训练的默认学习率(如
1e-3)会把学好的表征"冲毁"。 - 解法:微调学习率比训练新模型小 1–2 个量级(
1e-4~1e-5);先冻结主干只训分类头,再解冻微调(配方见训练配方与调参,实例见渐进式教程:三版跑起来)。
十一、显存 OOM 处理
- 症状:
CUDA out of memory;或训练到一半爆显存。 - 原因:模型参数量 + 中间激活 + 优化器状态超出显存;或显存泄漏(每 batch 都在累积计算图)。
- 解法(按性价比):
- 减小 batch(同时注意学习率联动)或梯度累积。
- 混合精度:显存立省 30–50%。
- 梯度检查点:空间换时间。
- 检查显存泄漏:
loss.backward()前是否zero_grad();循环里是否保留了.item()之外的 Tensor 引用。 - 用
torch.cuda.max_memory_allocated()定位峰值来源。
别忽略泄漏
"OOM 但减小 batch 没用"通常是显存泄漏——某处把计算图或 Tensor 引用保留在了循环里。用 max_memory_allocated 观察内存是否随 batch 单调增长来确诊。
十二、模型"看起来对"的假象
- 症状:指标合格、demo 演示正常,但换数据/换场景就崩;或 demo 里一直在"作弊"。
- 原因:评估设计缺陷掩盖了真实问题——常见有:
- 训练测试同分布:测试集与训练集来自同一批次,掩盖分布漂移。
- 数据泄漏(见第一条)让指标虚高。
- 在测试集上调参(见第二条)。
- 只看平均值:平均指标被"好区段"掩盖了长尾灾难。
- demo 特供:演示数据经过挑选,正好都是模型擅长的样本。
- 解法:构建分布外测试集(不同时段/地域/人群的数据);按难度/置信度分桶看指标;把"一个模型只能在自己的测试分布上工作"当作默认假设,主动去验证它何时失效。可解释性工具能进一步揭示模型到底"靠什么"做对,见可解释性与公平性。
附:反模式速查表
| 反模式 | 一句话特征 | 核心对策 |
|---|---|---|
| 数据泄漏 | 指标好得不真实 | 统计量只从训练集算 |
| 评估泄漏 | 测试集反复看 | 三层划分,测试只碰一次 |
| 学习率错误 | loss 不降或乱跳 | 范围测试法 |
| 忘记归一化 | 收敛慢且不稳 | 检查输入范围 |
| 类别不平衡 | 准确率高但小类全错 | 加权损失/换指标 |
| 不固定种子 | 结果不可复现 | 固定全链路种子 |
| 验证集过拟合 | 验证 99% 换数据崩 | 固定大验证集+隔离测试集 |
| BatchNorm 陷阱 | 小 batch 震荡/训练推理不一致 | batch≥32、正确 eval() |
| AMP bug | 偶发 NaN | GradScaler + 纯 fp32 对比 |
| 微调 lr 过大 | 微调把模型训废 | 学习率小 1–2 个量级 |
| 显存 OOM | 一爆再爆 | 四件套:batch/AMP/checkpoint/查泄漏 |
| 假象 | 看似都对其实不行 | 分布外测试+分桶指标 |
延伸阅读
- 调试与诊断——本页陷阱的系统排查流程
- DL 设计原则——预防陷阱的实验纪律
- 训练配方与调参——学习率、种子、微调的正确做法
- 评估实践——评估泄漏与"看起来对"的解药
- 从零构建一个深度学习项目——多数陷阱的首次出现场景
- 初始化与归一化——归一化相关陷阱的理论根源
参考资料
- Karpathy. A Recipe for Training Neural Networks——"看起来有效"陷阱的经典论述
- Kaufman et al. Leakage and the Reproducibility Crisis in ML-based Science (2023)——数据泄漏的系统研究
- Paszke et al. Automatic differentiation in PyTorch (2017)——AMP 与 autograd 的机制背景