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 特别小比如小于 8batch 统计量的噪声会很大训练和推理的表现会差出一截。我遇到过一次训练精度 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.05batch size 能开多大开多大。早期用过 SGD 动量 0.9收敛后的精度上限有时确实略高一点但对学习率的敏感度高需要配合 warmup 和余弦退火才稳调参成本不划算。除非是在做精度极限的对比实验日常任务我用 AdamW。有个组合禁忌要记住不要用带 L2 正则的 AdamAdam 的自适应学习率会把 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_blockingTrue) labels labels.to(device, non_blockingTrue) optimizer.zero_grad(set_to_noneTrue) logits model(images) loss criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() scheduler.step()set_to_noneTrue比默认的置零更省显存也略快。梯度裁剪的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.1dropout 从 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% 的结果能立刻知道当时用了什么配置、跟哪一次是同一个基线。我在没有这套机制的阶段做过大量无法复现的“好结果”等想在此基础上再往前走的时候发现连当时的增强组合都还原不出来。还有一个更细的体会别太相信单次实验的精度差异。种子换一下同样的配置精度能有一到两个点的浮动。判断一次改动是否真的有效至少跑三个不同种子看均值差距在一两个点以内的我会当成噪声处理不去做过度解读。这个习惯帮我避免了好几次在无意义的改动上浪费一周。