CNNs图像分类全链路:原理、数据管道、训练排错与部署 学习笔记利用CNNs进行图像分类前段时间在整理自己的实验记录翻到最早那批卷积神经网络的代码注释比代码还多全是调试过程中的疑问和临时结论。索性把这一路从“模型定义完就等着出结果”到“能大致预判问题出在哪一层”的过程重新梳理成一份完整笔记。利用CNNs进行图像分类这件事说起来就一句话把一张图片喂进去让网络吐出它属于哪一类真动手做卡住人的往往不是卷积怎么写而是参数量算不明白、数据管道埋了雷、损失曲线看不懂。这份笔记覆盖的是一条完整的链子——从问题本质拆解、核心层原理、数据准备、网络搭建与训练、到最后的评估部署中间夹着我自己踩过的坑和几个可以直接抄的排查顺序。适合刚上手图像分类的同学当路线图看也适合已经能跑通demo、但模型效果总比预想差一截的朋友对照着找原因。文中代码用主流深度学习框架写风格偏工程实操参数和公式都给了推导过程不要求你有很强的数学背景。1. 先把问题拆清楚图像分类到底在做什么1.1 从“给图片打标签”到“学习一个映射函数”新手最常见的误解是把图像分类当成某种规则匹配——看到猫耳朵就是猫看到轮子就是车。这种思路在简单场景勉强能用一旦光照变了、角度歪了、物体被遮了一半规则就全崩了。CNN做的事情本质上完全不同它把一张图片看作一个三维数组高 × 宽 × 通道然后去学习一个从数组到类别概率的映射函数。假设输入是 224×224 的RGB图那就是 150528 个数值输出是 K 个概率加起来等于1。这个视角的转变很关键。一旦你把它看成函数拟合问题很多决策就有依据了参数量决定函数能表达多复杂的关系数据量决定你能约束住多少个参数损失函数决定你在优化什么目标正则化决定你怎么防止函数把训练集的噪声也学进去。我在笔记里给每一类决策都标了它对应的“函数意义”后面调参的时候顺着这条线找比盲目试参数效率高得多。还有一个必须先想清楚的问题分类任务的难度边界在哪里。类别之间是互斥的还是多标签的类别数有多少类间差异是细粒度的比如不同型号的同一类物体还是粗粒度的猫和汽车样本量是几百还是几万这些答案直接决定你该从零训练还是用预训练模型微调也决定了你对准确率的合理预期。我最开始就没做这一步直接拿一个十类的小数据集上了一个不算小的网络训练准确率一路冲到99%验证集却卡在六成多白折腾了两天。1.2 为什么是卷积全连接层的三个致命问题要理解CNN为什么适合图像最好的方式是看全连接网络在图像上会撞到什么墙。假设我们老老实实用全连接层处理一张 224×224×3 的图第一层设 1024 个神经元那么这一层的参数量就是 150528 × 1024 ≈ 1.54 亿还没算偏置。这还只是第一层如果堆三层参数量直接到十亿量级。对于动辄上亿参数但样本只有几千张的任务过拟合几乎是必然的。第二个问题是空间结构被彻底打散。全连接层把图片拉平成一维向量原本相邻的像素在输入里的位置关系消失了。模型需要自己从零学出“左上角的像素和它右边那个像素有关”这种先验而这类先验其实是任何人都知道的常识白白浪费了大量参数和样本。第三个问题是缺乏平移等变性。同一只猫出现在画面左上角和右下角对全连接网络来说是完全不同的两组输入它得分别学一遍。卷积核则天然解决了这一点同一个卷积核在整张图上滑动无论目标出现在哪里响应方式一致。这背后其实是三个先验假设——局部性相邻像素相关性更强、平移等变性目标位置变化不该改变类别、层次性边缘组成纹理纹理组成部件部件组成物体。CNN的结构就是把这三点写进了网络的归纳偏置里。下面这张表是我自己在笔记里做的对比用来向别人解释为什么要换架构维度全连接网络卷积网络首层参数量224×224×3 输入1024 隐单元约 1.54 亿3×3 卷积、32 通道约 896空间信息拉平后丢失保留二维结构平移等变性无需从数据中学由权重共享天然获得对输入尺寸的约束固定尺寸可适应不同分辨率配合全局池化1.3 项目整体设计思路与选型考量动手之前我给自己定了一条原则先把整条链路跑通再谈精度。具体做法是用一个公开的小规模多类别图像数据集先验证数据管道和训练循环没问题网络故意做得小一点几层卷积加全局平均池化能在几分钟内训完一轮。很多人一上手就搬一个深层网络结果训练一次要几小时出了问题根本不知道该改哪里迭代速度被拖死。选型上还有几个考量值得说清楚。第一损失函数默认用交叉熵因为类别互斥且输出是softmax概率这是最自然的匹配如果遇到类别严重不平衡或者标注有噪声再考虑带权重的交叉熵或者更平滑的标签。第二优化器我一开始习惯用自适应方法收敛快、对学习率不敏感适合快速验证等结构定下来、要冲最后一两个点精度时再换成带动量的随机梯度下降配合学习率调度泛化往往更好。第三是否用预训练权重取决于你的数据量和目标任务与自然图像的相似度这个判断我在第4节会展开。注意设计阶段就要把“验证集怎么用”定下来。验证集只能用来做模型选择和超参调整一旦你反复根据验证集结果改结构、改参数它其实已经被间接训练了最终报告的数字会偏乐观。真正干净的评估要留一份从头到尾不碰的测试集。2. CNN核心组件拆解每一层到底在干什么2.1 卷积层感受野、通道、步长与填充的计算卷积层是整套结构的地基但有三个参数必须能自己算不能靠试。第一个是输出尺寸。设输入边长为 W卷积核大小 F填充 P步长 S输出边长就是 (W − F 2P)/S 1除不尽时向下取整。这个公式我建议在纸上推一遍先把输入两端各补 P 个像素得到 W 2P然后一个 F 大小的窗口从最左端开始每次移动 S 格能放下的位置数就是结果。举例输入 32卷积核 3填充 1步长 1输出是 (32 − 3 2)/1 1 32尺寸不变这就是所谓的 same padding。第二个是参数量。一个卷积层的参数是 F × F × C_in × C_out 再加 C_out 个偏置。注意这里跟输入的空间尺寸无关这是卷积最迷人的地方——输入从 32×32 变成 256×256参数一个都不增加。这也是为什么深层网络能处理高分辨率图像而不爆炸。第三个是感受野。它指的是输出特征图上一个点对应回原图能看到多大区域。单层 3×3 卷积的感受野是 3如果后面再跟一层 3×3第二层的感受野就是 3 (3 − 1) × 1 5也就是两个 3×3 堆叠等效于一个 5×5。但参数量上假设通道数都是 C两个 3×3 是 2 × 9C² 18C²一个 5×5 是 25C²前者少了近三成而且中间多了一次非线性激活表达能力更强。这就是现代网络几乎清一色用 3×3 小卷积堆叠的原因不是跟风是算过账的。卷积配置感受野参数量通道数均为 C非线性次数1 层 5×5525C²12 层 3×3518C²23 层 3×3727C²31 层 7×7749C²12.2 池化层与下采样为什么不能一味堆卷积如果网络里只有卷积而且全部用 same padding那特征图尺寸永远不变计算量会一直停留在高分辨率上。224×224 的特征图上做 256 通道卷积计算量是相当可观的。所以必须有下采样环节把空间尺寸逐步压缩、通道数逐步增加让网络从“看细节”过渡到“看语义”。传统做法是最大池化2×2 窗口、步长 2尺寸直接减半。它的好处是没有参数、计算便宜、保留了窗口内最显著的响应。平均池化则更适合需要对整体强度做统计的场景。另一种做法是用步长为 2 的卷积代替池化让网络自己学习怎么降采样信息损失理论上更可控代价是多了一部分参数。这里有个我踩过的坑值得单独讲不要过早、过猛地降采样。我早期的一个网络第一层就是 7×7 卷积加步长 2紧接着一个 3×3 最大池化输入 224 的图到这里只剩 55×55小目标的细节基本被抹平了后面怎么调都补不回来。后来改成前两层都用 3×3、步长 1把降采样推后小目标的召回率明显改善。经验上浅层负责提取边缘和纹理这类细粒度信息应该尽量保留分辨率深层负责语义才适合大幅压缩。还有一个容易被忽略的细节池化层在反向传播时的梯度分配。最大池化只把梯度传给窗口内取值最大的那个位置其余位置梯度为零。这本身没问题但如果初始化不好或者学习率过大某些位置可能长期拿不到梯度形成类似“死区”的现象。这也是后来很多结构用步长卷积替代池化的一个隐性理由。2.3 激活函数与归一化层训练能不能收敛的关键没有激活函数的堆叠卷积在数学上等价于一个线性变换无论堆多少层都拟合不了非线性关系。所以每个卷积后面都要接激活。ReLU 是目前最常用的选择计算简单、正区间梯度恒为1、不会饱和能有效缓解深层网络的梯度消失。但它有个副作用输入为负时输出恒为0梯度也是0如果某个神经元的输入长期为负它就再也不更新了这就是常说的“神经元死亡”。学习率设太大时这个现象特别明显我遇到过一整层输出全零的情况排查了半天才发现是学习率的问题。应对办法有几个把学习率调小用带泄漏的版本负区间给一个很小的斜率或者用更平滑的激活函数。选择上没有绝对优劣小数据集上差别不大深层网络里平滑激活往往收敛更稳。归一化层的作用更值得说清楚。它把每个通道的激活值重新标准化到均值0、方差1再乘一个可学习的缩放、加一个可学习的偏移。这样做的好处是让各层输入的分布保持稳定允许用更大的学习率收敛速度明显加快。但这里有个必须记住的工程细节训练模式和推理模式的行为不一样。训练时用当前批次的统计量推理时用训练过程中累积的滑动平均值。如果你在推理时忘了切换到评估模式模型会用单个批次的统计量做归一化结果会随批次组成而波动指标莫名其妙地掉。我第一次部署时就栽在这上面离线评估好好的上线后同一张图连续请求两次结果都不一样查了很久才发现是这个开关的问题。2.4 全连接层与输出层从特征图到概率分布网络走到最后得到的是一组特征图通道数可能几百尺寸可能 7×7。传统做法是拉平后接一到两层全连接最后映射到类别数。但全连接的参数量很大7×7×512 拉到 4096 个神经元就是上亿参数而且它对输入尺寸敏感换个分辨率就得改结构。现在更常见的做法是全局平均池化对每个通道的所有空间位置取平均直接得到长度等于通道数的向量再接到输出层。这样参数量降到几乎为零还能起到正则化作用同时让网络支持任意输入尺寸。代价是空间信息的聚合方式比较粗暴如果任务对目标位置敏感可能需要保留更多结构。输出层的形式取决于任务。单标签多分类用 softmax输出各类概率配合交叉熵损失多标签分类则对每个类别独立做 sigmoid用二元交叉熵。初学者最容易犯的错是把多标签任务按多分类来做softmax 会强制各类概率之和为1等于假设样本只能属于一个类这在很多实际场景里是错的。提示输出层的数值稳定性不能忽略。直接对很大的 logits 做指数运算会溢出框架里的交叉熵实现通常会内部做 log-sum-exp 变换但如果你自己手写损失一定要记得先减去最大值。3. 数据准备八成的效果在写模型之前就定了3.1 数据集划分与类别平衡数据这块我见过太多问题而且大多数在训练曲线上一时半会儿看不出来。先说划分。常见比例是训练、验证、测试按 8:1:1 或 7:1.5:1.5 分但比例不是重点重点是划分必须随机且互斥。如果数据是按采集批次给的比如同一天拍的图放在一起那随机打散会让训练集和验证集出现高度相似的样本验证指标虚高上线就露馅。更稳妥的做法是按采集批次或场景分组划分保证验证集里的场景在训练时没见过。类别不平衡是另一个高频问题。假设十类数据里有一类占了七成模型什么都不学、全预测这一类准确率也有七成。这种情况下准确率完全失去意义得看每一类的召回和宏平均指标。处理方式上简单重采样对少数类过采样、多数类欠采样见效快但可能引入重复样本给损失加类别权重更温和权重一般按类别频率的倒数来设数据增强则针对少数类多做扰动从根上补充多样性。我的习惯是先看混淆矩阵确认不平衡到底影响了哪些类再决定用哪种手段而不是一上来就把所有方法都堆上去。3.2 数据增强的取舍数据增强是提升泛化最便宜的手段但增强方式必须和任务语义一致。水平翻转在大多数自然图像任务里是安全的因为左右翻转不改变物体类别但在文字识别、方向敏感的工业检测里翻转会直接造出错误标签。颜色抖动对光照变化大的场景有帮助但如果颜色本身就是判别依据比如区分不同颜色的同类产品抖动幅度就要收着。裁剪和缩放也很讲究。随机裁剪再缩放回原尺寸能让模型对位置和尺度变化更鲁棒但如果裁得太狠目标可能被裁掉一半甚至完全裁没标签还是原来的这就成了噪声。我的经验是裁剪比例下限控制在原图面积的六成以上并且对目标占比较大的数据集适当放宽。更进阶的混合类增强比如把两张图按比例叠加、标签也按比例混合能显著改善校准性和鲁棒性但对训练轮数要求更高小数据集上有时反而拖慢收敛。我的做法是先用基础增强把基线跑出来确认模型没有明显过拟合再逐步加混合类增强做对比一次只改一个变量。3.3 数据管道的工程细节这部分最枯燥但出问题最难查。第一个坑是归一化统计量。均值方差必须用训练集算然后原封不动地应用到验证集和测试集。我见过有人图省事对整个数据集算统计量这等于把测试集信息泄漏进了训练过程指标会虚高一点点虽然影响不大但属于原则性错误。第二个坑是尺寸和通道处理。同一批数据里混着不同分辨率的图不统一尺寸直接喂进网络会报错统一尺寸时要注意插值方式训练和推理必须一致训练用双线性、推理用最近邻结果会有偏差。灰度图要复制成三通道带透明通道的图要去掉 alpha否则通道数对不上。第三个坑是文件本身的脏数据。损坏的图片、零字节文件、旋转方向元数据不一致的手机照片这些在加载时才暴露往往训练跑了一半才崩。我的习惯是训练前先跑一遍扫描脚本把能正常解码的图筛出来顺便统计尺寸分布和通道分布花十分钟省掉后面几小时的排查。处理项训练集验证/测试集常见错误均值方差由训练集统计沿用训练集数值用全量数据统计导致泄漏随机增强开启关闭验证时也做随机裁剪指标抖动resize 插值与推理一致与训练一致两端不一致造成分布偏移尺寸统一是是混合分辨率直接进网络4. 网络结构搭建与训练流程实操4.1 从零搭一个小型CNN我建议第一次上手就写一个五层以内的小网络参数少、训练快、每一层的作用都看得见。下面这段是我笔记里的基线结构输入 64×64 的三通道图输出类别数可变import torch import torch.nn as nn class SmallCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1, biasFalse), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, 3, padding1, biasFalse), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 64 - 32 nn.Conv2d(32, 64, 3, padding1, biasFalse), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, 3, padding1, biasFalse), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 32 - 16 nn.Conv2d(64, 128, 3, padding1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1), # 任意尺寸 - 1x1 ) self.classifier nn.Linear(128, num_classes) def forward(self, x): x self.features(x) x torch.flatten(x, 1) return self.classifier(x)几个细节解释一下。卷积层关掉偏置是因为后面紧跟批归一化归一化会减掉均值偏置项被完全抵消留着纯属浪费参数。激活用原地操作能省一点显存。最后用自适应平均池化而不是固定尺寸的池化这样同一套权重可以接受不同分辨率的输入推理时比较灵活。整个网络参数量算下来在十几万量级单卡几分钟就能跑完一轮非常适合做快速验证。4.2 损失函数、优化器、学习率调度的选型逻辑损失函数默认交叉熵。如果标签有噪声或者类别不平衡严重可以给每个类设权重权重按频率倒数的平方根来设比直接用倒数更稳因为完全按倒数会给极少数类过大的权重训练容易震荡。优化器方面我做对比实验的结论是小数据集、快速验证阶段用自适应方法省心默认学习率千分之一左右基本能跑追求最终精度时带动量的随机梯度下降配合余弦退火往往能比自适应方法高零点几个点到一两个点尤其是数据量充足的情况。原因是自适应方法在训练后期的有效学习率衰减不够平滑容易停在比较尖锐的极小值附近。学习率调度我用得最多的是先预热再余弦衰减前几百步把学习率从很小的值线性升到峰值避免训练初期梯度方向混乱把参数带偏之后按余弦曲线平滑降到接近零。峰值学习率可以先用一小段数据做范围扫描从很小的值开始指数增长观察损失在哪个量级开始发散取发散点往下三到十倍作为峰值。optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr0.01, epochs50, steps_per_epochlen(train_loader), pct_start0.1)权重衰减这个参数容易被忽略它等价于对参数做二范数正则能抑制权重变得过大。用自适应方法时要注意权重衰减和它在数学上并不完全等价需要的话应该用解耦版本否则正则强度会随自适应缩放而失真。4.3 训练循环的写法与关键记录项训练循环本身不复杂但记录什么决定了你后面能不能快速定位问题。我每一轮至少记五项训练损失、验证损失、验证准确率、当前学习率、梯度范数。前三项是常规学习率用来核对调度是否按预期生效梯度范数用来发现梯度爆炸或者消失——如果它在前几十步就飙到很大说明初始化或者学习率有问题如果一路趋近于零说明网络太深或者激活饱和了。for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() grad_norm torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step() running_loss loss.item() model.eval() correct, total, val_loss 0, 0, 0.0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) val_loss criterion(outputs, labels).item() correct (outputs.argmax(1) labels).sum().item() total labels.size(0) print(fepoch {epoch}: train {running_loss/len(train_loader):.4f} fval {val_loss/len(val_loader):.4f} acc {correct/total:.4f})两个实操提醒。第一验证阶段务必用不计算梯度的上下文否则显存占用会明显上升还可能因为归一化层没切模式导致结果不稳。第二训练损失报批次数平均还是样本数平均前后保持一致就行但最好统一成样本数平均方便和验证损失横向比较。4.4 迁移学习什么时候该用预训练模型判断标准其实就两条数据量和任务相似度。数据量在一万张以下、且目标类别是自然图像里的常见物体直接用预训练模型微调几乎总是比从零训练好因为浅层的边缘和纹理特征本来就通用。数据量大到几十万张以上从零训练就有竞争力了甚至可能更好因为预训练模型的归纳偏置未必匹配你的领域。微调也有讲究。一开始应该冻结主干只训练最后的分类头等损失降下来再逐步解冻。解冻时给主干设置更小的学习率通常是分类头的十分之一到百分之一因为预训练权重已经接近一个好的解大步长会把它破坏掉。批归一化的统计量在微调时要不要更新取决于你的数据分布和预训练数据差多远差得多就让它跟着更新差得少就冻结住更稳。我在一次小样本任务里试过全程冻结归一化层只微调最后两层效果比全解冻好了一截原因应该是小样本的批次统计量噪声太大。5. 训练过程中的常见问题与排查实录5.1 损失不降或震荡的排查顺序遇到损失不降别急着改结构按这个顺序查八成问题在前三步就能定位。第一步用一个小批次做健全性检查取十张图关掉所有增强和正则看模型能不能把这十张过拟合到接近零损失。如果连这都做不到说明网络结构或者训练循环有根本问题跟数据量、正则化都没关系。我遇到过数据标签从1开始编号、而交叉熵期望从0开始导致标签越界被静默截断的情况类别的对应关系全乱了这种错误只能靠过拟合小批次暴露出来。第二步检查数据管道的输出。把一批图取出来反归一化、还原成图片看一眼确认没有全黑、全白、通道错位或者标签错配。这一步看着傻但真的有用我曾经因为归一化均值方差写反了顺序训练了三小时才发现输入是负值主导的。第三步看学习率。学习率过大表现为损失剧烈震荡甚至发散过小表现为损失几乎不动。用一个很宽的区间做几次短跑就能判断当前值是不是在合理范围。第四步才轮到结构问题。层数过深、没有归一化、激活函数选错都可能导致梯度传不回去。这时候看梯度范数最直接前几层梯度接近零就是梯度消失加归一化或者换激活梯度爆炸就加裁剪或者降学习率。5.2 过拟合与欠拟合的区分与处理判断标准很朴素训练损失一直在降、验证损失先降后升就是过拟合两个都降不下去就是欠拟合或者优化出了问题两个都降得很好但验证准确率就是上不去那多半是评估方式或者数据划分有问题。过拟合的处理手段按性价比排序先加数据增强成本最低再考虑权重衰减和丢弃法还不行就减模型容量或者早停。早停是最直接的但要注意别用验证损失最小值那一点去报告测试集结果那同样是选择性偏差。欠拟合反而少见但新手常把它误判成过拟合。如果训练损失都降不到一个较低水平加正则只会更糟。这时候应该加大模型容量、延长训练、提高学习率峰值或者检查数据标注是否有大量错误。注意训练和验证的损失比较必须在同一套损失定义下做。有些人训练时用了带标签平滑的损失、验证时用原始交叉熵两个数根本不可比看着验证损失比训练低其实是定义不同造成的假象。5.3 常见问题速查表这张表是我自己整理的遇到问题时从上往下扫一遍命中率还不错现象最可能的原因优先尝试的动作损失完全不动学习率过小、标签错位、数据全零过拟合小批次验证链路损失剧烈震荡学习率过大、批次太小降学习率、增大批次训练损失降验证不降过拟合加增强、加权重衰减、早停验证准确率随机跳动验证时开了增强、归一化模式错关闭随机操作、确认评估模式某一类召回极低类别不平衡、标注错误看混淆矩阵、加类别权重推理结果每次不同归一化层未切评估模式显式切换到评估模式上线后指标断崖下跌预处理与训练不一致逐项核对均值方差和插值方式6. 评估、部署与持续迭代的几个坑6.1 别只看准确率混淆矩阵与分类报告准确率只是一个汇总数它掩盖了错误的结构。我习惯在验证完成后立刻输出混淆矩阵和每一类的精确率、召回率、F1。混淆矩阵能告诉你错误集中在哪几对类别之间如果两类之间互相错得特别多通常是它们的特征确实接近要么补充更有区分度的数据要么在损失里对这两类加权。分类报告则能暴露“某一类召回接近零”的情况而这种问题在总体准确率上可能只体现为掉了两三个点很容易被忽略。还有一个指标值得关注置信度和准确率的对应关系。模型给出的概率如果是0.9那它这一类判断的准确率是不是真的接近九成如果不是说明模型过度自信部署到需要按置信度做筛选的场景会出问题。改善方式包括标签平滑、温度缩放校准或者用混合类增强训练。6.2 部署时的预处理对齐问题模型离线评估很好、上线就崩十有八九是预处理没对齐。要逐项核对的清单包括输入尺寸和缩放方式是否一致、均值方差的数值和顺序是否一致、通道顺序是RGB还是BGR、归一化是在0到1之间还是减均值除方差、是否做了中心裁剪而训练时用的随机裁剪。这些细节在训练脚本里是几行代码在部署服务里可能分散在不同模块非常容易错位。我的做法是把预处理写成一个独立函数训练和推理共用同一份代码杜绝两边各写一遍。另外在服务里加一个启动自检拿一张固定的测试图跑一遍推理把输出的类别和置信度打印出来和离线结果比对数值差异超过阈值就报警。这个自检拦住过我不止一次配置错误。6.3 结构定下来之后还能往哪走基线跑通、指标稳定之后提升方向大致几个。一是数据侧补充难例、清理错标、针对混淆严重的类别做定向采集收益通常比调模型大。二是训练策略侧试更长训练配合更强增强、试不同的输入分辨率分辨率提升对小目标帮助明显、试测试时增强把多尺度多裁剪的预测做平均。三是模型侧换更强的骨干、加注意力模块、做模型集成。顺序上我一般按数据、策略、模型的优先级来因为越往前投入产出比越高而且改动风险小。还有一件事值得提前做把实验记录下来。每次只改一个变量记录配置、指标和结论哪怕只是一句话。我早期不做记录两周后完全想不起来某个提升到底来自哪个改动只能重跑一遍。后来用一个简单的配置文件加结果表格虽然土但省下的时间非常可观。最后分享一个我自己反复验证过的小技巧任何时候觉得模型“有点怪”先别改代码把一批训练数据和一个批次的预测结果可视化出来看看。我遇到的大部分疑难问题答案都藏在某张被错误归一化的图、或者某个被错标的样本里看一眼比读十遍代码管用。