ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

CNN图像分类实战:从网络结构到训练调优与故障排查

CNN图像分类实战:从网络结构到训练调优与故障排查

做图像分类这件事,我从一开始的手写数字识别做起,中间反复推倒过三次网络结构,才慢慢把 CNN 这套东西的脾气摸清楚。这篇笔记不打算把教科书上的定义再抄一遍,而是把我从“能跑通”到“跑得稳”过程中积累的东西摊开讲:卷积层到底在学什么、参数该怎么算、数据管线为什么比模型结构更影响最终精度、训练不收敛时先看哪里。全文围绕CNNs 图像分类这条主线,涉及卷积核设计、池化与下采样、批归一化、数据增强、损失函数与优化器配置、学习率调度、迁移学习的取舍、评估指标选择和故障排查。适合刚学完反向传播、准备动手搭第一个分类网络的读者,也适合已经把模型跑起来但精度卡在某个坎上、不知道往哪儿调的人。读完你应该能独立搭出一条从数据加载到精度评估的完整链路,并且知道每个环节出问题时该怎么定位。

1. 先把问题定义清楚:图像分类到底在做什么

1.1 从像素矩阵到类别概率

图像分类这个任务的输入输出关系,说穿了非常简单:输入是一个三维张量,形状是通道数 × 高 × 宽,输出是一个长度等于类别数的向量,每个位置对应一个类别的得分或者概率。中间那坨网络做的事情,本质是把原始像素空间里的点,映射到语义空间里去。这里有个容易被忽略的认知:原始像素之间的距离是没有语义意义的,两张同类的图片在像素空间的欧氏距离可能比两张异类图片还远。卷积网络做的事情,就是通过一层层局部感受野和权值共享,把这个距离度量逐步“改造”成语义层面上的距离。

我自己第一次搭网络时,最大的误区是把这件事想得太抽象。实际上你完全可以把它理解成一个可微的特征工程流水线:浅层学边缘和色块,中层学纹理和局部形状,深层学物体部件和整体轮廓,最后接一个线性分类器在特征空间里画超平面。理解了这个分层结构,你就知道为什么浅层不适合放太多通道、为什么深层要用更大的通道数、为什么全局池化之后不需要再接巨大的全连接层。

还有一个实操层面的定义问题必须先想清楚:是单标签多分类还是多标签。单标签用 softmax 加交叉熵,多标签用 sigmoid 加二元交叉熵,这两条路从损失函数到评估指标完全不同,选错了后面怎么调都别扭。我在一个多标签场景里误用了 softmax,训练损失看着降得挺漂亮,实际测试时模型永远只输出一个高置信度类别,排第二的类别概率被压到几乎为零,查了两天才反应过来是损失函数选错了。

1.2 为什么不用全连接网络直接怼

把图像拉平接全连接层,理论上只要参数量够、激活函数够非线性,也能拟合任意函数。但这条路在实际场景里基本走不通,原因有三个,我按重要性排一下。

第一是参数量爆炸。一张 224×224 的彩色图,拉平之后是 150528 维。如果第一层全连接只输出 512 维,参数量就是 150528 × 512 ≈ 7700 万,这一个层就顶掉了一个中等规模网络的全部参数量。而同样输入下,一个 64 通道、3×3 卷积核的卷积层参数量只有 3×64×3×3 + 64 = 1792 个。差了四个数量级。

第二是丢失空间结构。拉平操作把二维邻接关系彻底打散了,相邻像素之间的强相关性信息在全连接层里得靠网络自己从零学起。卷积的局部连接天然保留了这个先验,属于把人的先验知识直接编码进结构里。

第三是没有平移等变性。同一个物体出现在图像左上角和右下角,对全连接网络来说是两套完全不同的输入模式,需要分别学习。卷积的权值共享让同一个特征检测器在整个图像上滑动,只要学会在某个位置识别出纹理,就能在所有位置识别出来。这个特性对图像任务太重要了,值得为它放弃全连接的“通用性”。

提示:如果你手头的输入尺寸特别小(比如 16×16 以内),全连接网络其实也不是不能用,但参数量控制和正则化要做得很仔细,性价比不如小卷积网络。

1.3 我的最小可行方案选型

选型这块我踩过的坑是:一上来就想复现大网络的完整结构,结果训练资源不够,改配置改到怀疑人生。后来我固化成一套从小到大的迭代路径,先把小模型跑通再逐步加复杂度。

第一步,先在一个我能够完全过拟合的小规模彩色数据集上(十类、每类几千张、32×32 尺寸)验证整条链路是通的。网络就三个卷积块,每个块3×3 卷积 + 批归一化 + 激活 + 2×2 最大池化,通道数按 32/64/128 递增,最后全局平均池化接线性层。这个模型参数量在几十万量级,在单卡上几分钟一轮,迭代速度非常舒服。

第二步,确认能过拟合之后再上数据增强和正则化,把泛化差距压下去。这一步的改动比改网络结构收益大得多,我的经验是数据增强带来的精度提升通常比把通道数翻倍要明显。

第三步,如果任务确实复杂、数据量也够,再考虑引入残差连接、更深的结构或者迁移学习。顺序不能反,先上大模型再回头找问题,你会在大量相互耦合的因素里晕头转向。

这个路径背后的逻辑是控制变量。深度学习的调参本质上是个高维搜索问题,你能同时观察和调整的变量就三五个,把结构复杂度先压到最低,才能把注意力集中在数据、学习率和正则化这些真正影响结果的因素上。

2. 网络结构的拆解:每一层到底在干什么

2.1 卷积层:参数量与感受野的账要算明白

卷积层的参数量公式是输入通道 × 输出通道 × 核高 × 核宽 + 输出通道,加号后面那项是偏置。输出特征图的空间尺寸公式是(输入尺寸 + 2×填充 - 核尺寸) / 步长 + 1,除不尽要向下取整。这两个公式我建议你手推几遍,直到能在脑子里直接算出来,因为调结构的时候全靠它。

举个具体的账。输入3×32×32,接一层3×3卷积、输出 32 通道、填充 1、步长 1:

  • 参数量 = 3 × 32 × 3 × 3 + 32 =896
  • 输出尺寸 = (32 + 2 - 3) / 1 + 1 =32,空间尺寸不变

再往后接一个 2×2 池化,尺寸变 16×16。第二个卷积块输入 32 通道、输出 64 通道,参数量 = 32 × 64 × 9 + 64 =18496。可以看到通道数翻倍、输入通道也翻倍之后,参数量涨了二十倍。这就是为什么网络的通道数不能随便往上堆。

感受野这块更值得单独说。两层 3×3 卷积堆叠的有效感受野是 5×5,三层是 7×7。而参数量的对比呢?假设输入输出通道都是 C,两层 3×3 的参数量是2 × 9C² = 18C²,一层 5×5 是25C²。感受野一样,参数少了近三成。更重要的是非线性层数:两层堆叠中间多了一次激活,表达能力比单层更强。这是 VGG 系列设计思路的核心动机,也是我后来所有网络里都坚持小卷积核堆叠的原因。

不过小卷积核也不是没代价,层数多了之后梯度回传路径变长,训练起来更依赖批归一化和残差连接来稳住。我在一个 12 层的纯堆叠网络上去掉批归一化试过一次,前三层的梯度范数直接掉到 1e-7 量级,基本等于没在学。

2.2 池化与下采样:为什么不用步长卷积替代

池化的作用是把特征图的空间尺寸降下来,同时保留最显著的响应。2×2 最大池化步长 2,尺寸直接减半,参数量为零,计算量也很低。它带来的一个副作用是平移不变性的增强:在小窗口内,特征的位置稍微挪动一点,最大池化的输出不变。

用步长为 2 的卷积替代池化,是一个常见的设计选择。好处是下采样的过程也可学习,理论上能学到更适合任务的下采样方式;坏处是参数量和计算量上去了,而且我实测下来在中小规模任务上,两者的精度差异经常在噪声范围内,不值得为它增加调参负担。

平均池化和最大池化的选择上,我的经验是:中间层用最大池化,因为它保留纹理和边缘这类稀疏的强响应;最后一层用全局平均池化,因为这里需要的是整个特征图上各类响应的平均强度,用来做分类判据。全局平均池化还有一个隐性好处:它把通道数 × 高 × 宽的特征图压成通道数的向量,直接把分类头的参数量从百万级降到千级,对抑制过拟合非常有用。我第一次把全连接分类头换成全局平均池化,验证集精度直接涨了三个点,参数量少了两个数量级。

2.3 激活函数与批归一化的配合

激活函数我基本只用 ReLU 及其变体。ReLU 的优点不用多说,计算便宜、正区间梯度不衰减。但它有个问题是负区间梯度为零,一旦某个神经元的输入长期为负,它就“死”了,权重再也更新不了。我排查过一次训练中期精度停滞的问题,把各层激活输出打出来看,发现某一层有接近 40% 的通道输出恒为零,这就是典型的神经元死亡。

后来我改成 LeakyReLU(负斜率 0.01)就没再遇到这个问题,代价是需要多调一个超参数。这个斜率别设太大,设到 0.2 以上就接近线性了,非线性表达力会掉。

批归一化的位置现在是公认的做法:放在卷积之后、激活之前。它做的事情是在每个 mini-batch 内对每个通道做标准化,然后用两个可学习参数 γ 和 β 做仿射变换把表达能力还回来。这件事的直接收益是允许更大的学习率,因为标准化把每层输入的分布拉回到均值 0、方差 1 附近,梯度尺度稳定了,学习率设到 0.1 也不容易炸。

但批归一化有两个必须知道的坑。一是训练和推理行为不一致:训练时用当前 batch 的统计量,推理时用滑动平均的统计量。如果你的 batch size 特别小(比如小于 8),batch 统计量的噪声会很大,训练和推理的表现会差出一截。我遇到过一次训练精度 95%、推理精度 78% 的情况,最后定位到就是 batch size 只有 4 导致的。二是不要放在激活之后。虽然早期论文里有这种用法,但现在大量实验都表明放在激活前更稳。

2.4 分类头设计:少即是多

分类头的设计我经历了从“越复杂越好”到“越简单越好”的转变。早期喜欢接两层 512 维的全连接加 dropout,后来发现这部分参数量占了整个网络的一半以上,而且是过拟合的重灾区。

现在的固定做法是:全局平均池化 → (可选的一层 dropout)→ 线性层输出类别数。dropout 的丢弃率我一般设 0.2 到 0.5,数据量越小设得越高。但要注意,如果前面已经用了比较强的数据增强和其他正则化手段,dropout 可以省掉,多重正则化叠加反而会让模型欠拟合,训练损失都降不下去。

有个细节值得提:输出层不加激活。如果是单标签多分类,损失函数用带 logits 的交叉熵(很多框架里的CrossEntropyLoss内部已经包含 softmax),不要再手动加一个 softmax 再取对数,那样数值上不稳定,容易出现 log(0) 的问题。多标签场景才在输出接 sigmoid,配合二元交叉熵。

3. 训练流程的完整实现

3.1 数据管线:被低估的瓶颈

我见过太多人把时间全花在改网络结构上,结果数据管线是同步加载、CPU 单线程解码,GPU 利用率常年 30% 以下。判断方法很简单:训练时盯着 GPU 利用率和显存占用,如果利用率在波动、经常掉到 50% 以下,瓶颈就在数据侧。

数据管线的几个必设项:多进程加载(进程数设成 CPU 核心数的 0.7 倍左右,设太满反而因为进程调度开销变慢)、预取(prefetch值设成 2 到 4 个 batch)、pin_memory开启(配合 GPU 训练的异步拷贝)、把预处理里能离线做的部分提前做掉(比如把原始大图统一缩放并缓存成小尺寸,能省掉大量解码时间)。

数据划分上,我坚持三份:训练集、验证集、测试集。验证集用于调参和早停,测试集只在最后评估一次。这个纪律很重要,我早期图省事用验证集既调参又报结果,报出来的数字虚高了好几个点,实际部署时打脸。划分比例上,数据量小的时候(每类几百张)用 6:2:2,数据量大(每类上万张)用 98:1:1 就够了。

注意:划分必须按样本来源分层,如果同一个场景采了多张连续帧,一定要保证同一来源的样本不跨集合,否则会出现训练集和验证集信息泄漏,验证精度虚高。

3.2 数据增强:性价比最高的那一环

数据增强是我认为投入产出比最高的环节,没有之一。同样的模型结构,加上合理的增强,验证精度提升三到八个点是常见的事。

我的增强策略分三档,按数据量选择:

数据规模(每类)增强组合说明
< 500随机裁剪(填充4)+ 水平翻转 + 颜色抖动(亮度/对比度/饱和度 0.4)+ 随机旋转 ±15° + 随机擦除强增强,必须配合更长的训练轮次
500 - 5000随机裁剪(填充4)+ 水平翻转 + 轻度颜色抖动(0.2)主力档位,绝大多数任务够用
> 5000随机裁剪(填充4)+ 水平翻转数据本身够,过强增强反而拖慢收敛

这里解释几个“为什么”。

随机裁剪为什么先填充再裁:不填充直接裁,边缘信息会被系统性丢弃,模型会学到“物体永远在中心”这个错误先验。填充 4 像素再裁回原尺寸,相当于让模型见到物体在各种偏移位置的样子,本质上是在做平移的数据增广。

水平翻转要看任务:自然场景的物体左右翻转后语义不变,可以放心用。但涉及文字、方向性标识、某些医学影像的任务,翻转会破坏语义,绝对不能加。我在一个含数字标识的任务里误加了翻转,模型把“6”和“9”彻底搞混了,训练精度都上不去。

颜色抖动幅度要克制:抖动太强,模型会开始依赖颜色不变性,反而丢掉了颜色本身作为判别依据的信息。0.2 到 0.4 是比较安全的区间。

随机擦除的收益不稳定:它在抑制模型过度关注局部显著区域上有用,但对小目标任务可能把目标整个盖掉,导致标签噪声。我一般把它作为可选项,加了之后如果验证精度没提升就去掉。

归一化这块补一句,ImageNet的那组均值方差(0.485/0.456/0.406 和 0.229/0.224/0.225)是个通用起点,但如果你的数据集整体偏暗或者色调特殊,建议自己算一遍训练集的通道均值和标准差替换掉,我实测在某些工业图像上能带来一个点左右的提升。

3.3 损失函数与优化器的配置取舍

损失函数单标签分类就用交叉熵,没什么好纠结的。但类别不均衡是个绕不开的问题,我在一个长尾分布的数据集上吃过亏:最多的类有两万张,最少的类只有一百多张,直接训练的结果是少数类召回率接近零。

处理方案有三个,我按推荐顺序说。

第一个是加权交叉熵,给每个类别按频率的倒数赋权,权重做归一化让总和等于类别数。实现上就一行的事。缺点是极端不均衡时权重差异过大,训练不稳定。权重我一般会在倒数基础上开个方根,把极端值压一压。

第二个是重采样。过采样少数类要注意别简单复制,那样会加剧过拟合,配合强增强使用会好一些;欠采样多数类会损失信息,数据量本来就不多的时候慎用。

第三个是focal loss,通过(1-p)^γ这个调制因子降低易分样本的损失权重,让模型聚焦在难样本上。γ 一般设 2。它在极端的背景-前景不均衡场景(比如检测任务)里效果显著,普通分类任务上未必比加权交叉熵好,我一般作为备选。

优化器方面,我现在的主力是 AdamW,学习率 1e-3,权重衰减 0.01 到 0.05,batch size 能开多大开多大。早期用过 SGD + 动量 0.9,收敛后的精度上限有时确实略高一点,但对学习率的敏感度高,需要配合 warmup 和余弦退火才稳,调参成本不划算。除非是在做精度极限的对比实验,日常任务我用 AdamW。

有个组合禁忌要记住:不要用带 L2 正则的 Adam,Adam 的自适应学习率会把 L2 惩罚的效果削弱,这就是 AdamW 把权重衰减从梯度里解耦出来的原因。如果你在 Adam 上设了weight_decay,换成 AdamW 通常能看到提升。

3.4 学习率调度与训练循环

学习率是影响结果最大的单个超参数,没有之一。我的经验法则是:从 1e-3 开始,如果前 200 步损失震荡或者发散,降到 3e-4;如果损失下降极其缓慢且平稳,往上试 3e-3。判断依据看前几百步的损失曲线,不需要跑完整轮。

warmup 我基本是必加的。前 5% 到 10% 的步数里,学习率从初始值的 0.1 倍线性升到设定值。原因很简单:训练刚开始时权重是随机的,梯度方向和尺度都很混乱,直接上大学习率容易把权重推到坏区域,之后再怎么退火也回不来。加了 warmup 之后,我遇到训练初期 loss 变 NaN 的概率大幅下降。

调度策略我用余弦退火最多,它在中后期缓慢降低学习率,能让模型在损失曲面上精细收敛。如果训练轮次不确定,用带热重启的余弦退火或者ReduceLROnPlateau(验证损失三个 epoch 不降就把学习率乘 0.5)更省心。

训练循环里我固定加的几个东西:

model.train() for epoch in range(num_epochs): for images, labels in train_loader: images = images.to(device, non_blocking=True) labels = labels.to(device, non_blocking=True) optimizer.zero_grad(set_to_none=True) logits = model(images) loss = criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() scheduler.step()

set_to_none=True比默认的置零更省显存也略快。梯度裁剪的max_norm我设 5.0,这个值不是拍脑袋来的:正常训练的梯度范数通常在 1 到 3 之间,设 5 既能兜住偶发的梯度爆炸,又不会干扰正常更新。如果你观察到梯度范数长期贴着裁剪阈值,说明学习率太大了,该往下调而不是把阈值往上抬。

每个 epoch 结束后,我会记录训练损失、训练精度、验证损失、验证精度四个数,并且保存验证精度最高的那组权重,而不是最后一个 epoch 的。这一条在数据量小的时候尤其重要,模型经常在后期开始过拟合,最后一轮的权重未必最好。

4. 评估、调参与迁移学习

4.1 评估指标的坑:准确率会说谎

准确率是最容易误导人的指标。在类别不均衡的场景下,一个永远预测多数类的模型就能拿到很高的准确率,但它毫无价值。我在一个九比一不均衡的二分类任务上,见过准确率 90% 但少数类一个都没识别出来的模型。

现在我的评估组合固定是:整体准确率 + 每类精确率/召回率/F1 + 混淆矩阵 + 宏平均 F1。宏平均 F1 对少数类更敏感,是判断模型是否真的学到了东西的关键指标。混淆矩阵一定要看,它能直接告诉你模型在哪两类之间犯迷糊。我做过一个外观相近的品类分类任务,看混淆矩阵发现两个类的互相误判占了全部错误的六成,回头检查数据才发现这两类的采集条件完全不同(光照、角度),问题出在数据分布上,不是模型能力问题。

另外一个常被忽略的点是推理侧的一致性。如果推理时用的输入分辨率和训练时不一致(比如训练 32×32,推理喂 224×224 进网络后再缩),结果会明显变差。我在部署阶段踩过这个坑,训练脚本和推理脚本的预处理代码是两份独立写的,缩放插值算法不一样(一个双线性一个最近邻),精度掉了两个多点。后来我把预处理抽成公共函数,两边共用,问题消失。

4.2 超参调优的实操顺序

超参调优不是网格搜索,是按影响力排序的串行优化。我的顺序是这样的:

第一步,先做小样本过拟合测试。取 20 到 50 个样本,关掉所有正则化和增强,训练到损失接近零。如果做不到,说明网络结构或者学习率有硬伤,后面所有调优都是白费。这一步我用它抓出过好几次 bug,包括标签错位、损失函数用错、某一层梯度断了。

第二步,调学习率。固定其他所有配置,试 1e-4、3e-4、1e-3、3e-3 四档,每档跑 10 个 epoch 看验证精度和损失曲线的形状。选收敛最快且曲线最平滑的那档。

第三步,调数据增强强度。学习率定下来之后,增强强度的收益比模型宽度大。数据量少就往强了调,数据量足够就减弱。

第四步,调正则化。权重衰减从 0.01 往上试到 0.1,dropout 从 0 试到 0.5。判断标准是训练精度和验证精度的差距,差距大就加正则,差距小但两者都低就是欠拟合,该减正则或者加容量。

第五步,才是调网络容量。通道数按 1.5 倍递增试,深度加一个块试试。这一步放最后是因为它最贵,训练时间翻倍但收益经常只有一两个点。

这套顺序的核心逻辑是从便宜且影响大的往贵且影响小的调。我见过有人一上来就网格搜索通道数和层数的组合,跑了两周,最后发现换个学习率就能超过所有组合。

4.3 迁移学习的取舍:什么时候值得用

迁移学习的价值在小数据场景下极其明显。我做过一个只有每类两百张图的任务,从零训练精度卡在 62% 上不去,换成在通用图像上预训练过的骨干网络、冻结前面的层只训分类头,五个 epoch 就到了 85%。

但迁移学习不是无脑套用,有几个判断点。

数据相似度。如果你的数据和预训练数据的域差异极大(比如工业缺陷图、医学影像、显微镜图像),浅层特征(边缘、纹理)仍然可迁移,但深层语义特征可能完全不适配。这种情况下我通常冻结前两个阶段,后面的层放开训练,并且用较小的学习率(1e-4 量级)做微调,而不是直接用 1e-3。

冻结策略。三个档位:只训分类头(最快,适合数据极少);冻结前若干层、放开后面(折中,最常用);全部放开微调(适合数据量足够且域接近)。全部微调时学习率要比从头训练小一个数量级,否则预训练权重会被快速破坏,反而比如随机初始化。

输入尺寸和归一化必须对齐。预训练模型对输入尺寸和归一化参数是有预期的,我用过的一个骨干网络在输入尺寸偏差较大时精度下降很明显。归一化参数尤其要注意,用预训练模型就用它的均值方差,不要换成本地数据集统计量,除非你打算全部放开微调。

还有一点:迁移学习的收益会随着数据量增加而递减。当每类样本过万时,从零训练和迁移学习的最终精度差距往往在两个点以内,而迁移学习省下的训练时间可能还不够你调微调策略花的时间。这个取舍自己权衡。

5. 常见问题排查实录

5.1 损失不下降或者剧烈震荡

这是最常遇到的,我的排查顺序是固定的,从便宜到贵。

先看标签和输入是否对齐。把一批数据可视化出来,逐个核对图像和标签的对应关系。我遇到过一次数据加载器里的打乱索引写错了,图像和标签错位,损失就一直在 log(类别数) 附近徘徊,等同于随机猜。

再看学习率。损失在前几十步就冲到 NaN,学习率太大;损失下降极其缓慢且几乎不动,学习率太小或者梯度被截断了。把学习率降一个数量级快速试一次,成本很低。

再看损失函数和输出层是否匹配。多分类用了 sigmoid、回归任务用了交叉熵、输出层多套了一层 softmax,这几种错法我都见过,症状都是损失不降。

最后看数据本身是否可分。如果同一个输入在数据集里被标了两个不同的类别(标注噪声),模型会在两者之间反复摇摆,损失卡在一个不低的值上。抽样人工检查一百条,能发现大部分问题。

5.2 过拟合:训练精度高但验证精度上不去

训练精度 98%、验证精度 75% 这种差距,说明模型在死记训练集。处理手段按我的优先级排:

手段典型收益代价备注
加强数据增强大需要更长训练轮次首选,几乎无副作用
增加权重衰减中太小无效,太大欠拟合0.01 → 0.05 → 0.1 逐档试
减小模型容量中可能欠拟合通道数乘 0.75
加 dropout中收敛变慢只在分类头加,0.2-0.5
早停小但稳需要验证集无条件应该做
更多数据最大成本最高有条件就做

这里要说一个反直觉的经验:先考虑加数据增强,而不是加正则化。增强是在改变模型看到的输入分布,让它学到更本质的特征;正则化是在限制模型的表达能力。前者是“教它学对的东西”,后者是“不让它学错的东西”,前者更根本。

还有一点,如果你看到训练精度一路涨到 99.9% 但验证精度在 70% 到 80% 之间大幅波动,很可能是验证集太小了,评估本身的方差就很大。验证集至少要有几百个样本,评估结果才有参考价值。

5.3 训练正常但部署后精度崩了

这类问题的根源几乎全在预处理和推理流程不一致。我整理了一份检查清单,按出现频率排:

  • 输入尺寸和插值方式是否和训练一致
  • 归一化的均值方差是否一致(用预训练模型时尤其容易搞错)
  • 通道顺序是 RGB 还是 BGR(很多图像读取库默认 BGR)
  • 训练时用了model.train()但推理时忘了model.eval()
  • 推理时忘了关闭梯度计算,显存爆了或者行为异常
  • 量化或者导出模型时,某些算子精度损失过大

model.eval()这条特别隐蔽。它影响的是批归一化和 dropout 的行为,忘了切换的话,批归一化会用当前推理 batch 的统计量,单张图推理时这个统计量完全没意义,精度会掉得很惨。我在一次部署里就栽在这上面,训练精度 92%,上线后只有 60% 出头,查了半天代码,就少了一行。

5.4 排查速查表

把上面这些症状和对应动作整理成一张表,出问题的时候按表逐条过一遍,能省掉大量试错时间。

症状首要怀疑快速验证方法处理
损失卡在 log(类别数) 附近标签错位 / 学习率过大可视化一批数据核对修数据管线 / 降学习率
损失变 NaN学习率过大 / 除零 / log(0)打印每层梯度范数加 warmup、梯度裁剪
训练初期短暂下降后停滞学习率过大降一个数量级重试重新预热
训练精度高、验证精度低过拟合看两者曲线差距增强 + 正则 + 早停
训练和验证精度都低欠拟合先做过拟合测试加容量 / 减正则 / 提学习率
少数类完全识别不出类别不均衡看混淆矩阵加权损失 / 重采样
验证精度波动极大验证集太小看不同 epoch 的方差扩大验证集
部署后精度骤降预处理不一致逐行 diff 训练和推理代码抽公共预处理函数
训练中途 loss 突然跳高数据里有脏样本按 loss 排序看 top 样本清洗数据
GPU 利用率低数据管线瓶颈看利用率和显存曲线多进程 + 预取 + 缓存

最后分享一个我一直在用的小技巧:给每次实验编号并记录完整配置。实验目录名里带上日期、关键超参和学习率,配置存成独立文件随权重一起保存。这样做的好处是,三个月后你回头看某个 87.3% 的结果,能立刻知道当时用了什么配置、跟哪一次是同一个基线。我在没有这套机制的阶段,做过大量无法复现的“好结果”,等想在此基础上再往前走的时候,发现连当时的增强组合都还原不出来。

还有一个更细的体会:别太相信单次实验的精度差异。种子换一下,同样的配置精度能有一到两个点的浮动。判断一次改动是否真的有效,至少跑三个不同种子看均值,差距在一两个点以内的,我会当成噪声处理,不去做过度解读。这个习惯帮我避免了好几次在无意义的改动上浪费一周。

RELATED READING

延伸阅读

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