我这两年做得最多的活儿就是从产线上抱回来一堆“说不清道不明”的缺陷图片然后在数据少得可怜的情况下把模型训到能上线跑。工业缺陷检测这个场景最坑的不是算法多难而是数据永远不够、漏检永远背锅、产线永远催你交付。这篇文章就把我最近一次完整落地的项目流程捋一遍重点放在小样本训练和漏检控制这两块给同样被这俩问题折磨的同行一个可以照着走的参考。先交代下这个项目的背景。项目对象是一条手机中框外观检测线需要检测的表面缺陷包含划伤、压伤、异色、脏污、麻点这几类。产线给的采集窗口只有三天最终到手的有效缺陷图大约只有400张而且缺陷面积占比普遍在0.5%到5%之间部分划伤缺陷在500万像素下的宽度只有两三个像素。客户提的要求很直接漏检率必须低于1%过检率可以放宽到8%左右。说白了这就是一个典型的小样本、高漏检成本、弱纹理缺陷检测项目。接下来的内容全部围绕这条主线展开。1. 项目拆解与整体方案设计1.1 先把“检测需求”翻译成“算法能听懂的语言”很多项目翻车不是因为模型不行而是需求从客户嘴里到你耳朵里已经变了形。客户说“把划伤给我检出来”这听起来简单但实际上有十几个细节要敲定什么样的痕迹算划伤抛光纹理和细划伤怎么区分允许的漏检优先发生在哪些缺陷类型上检出了但位置标偏了算不算合格。我一般拿到需求后会先做一件事拉着产线工艺工程师一起过一遍历史不良品把每类缺陷的形态、尺寸范围、发生区域、颜色特征全部登记下来。这个动作看着费时间但后面训练模型的时候能省下几倍的力气。比如这个项目里我们发现“异色”类缺陷主要集中在阳极氧化后的边缘区域而“麻点”类缺陷在全表面随机分布这就直接决定了后面裁剪策略和扩增策略的差异。最终我整理出的检测需求表大概长这样缺陷类型疑似样本数形态特征最小可接受检出尺寸漏检权重划伤160线状、细长、方向随机宽度≥2px长度≥15px高压伤70片状、亮度异常、边界模糊面积≥60px²高异色80区域色差、低对比度面积≥100px²中脏污50点状或块状、高对比面积≥30px²中麻点40细小、密集、低对比面积≥20px²低这个表就是整个项目的“宪法”。后面所有数据策略、模型选择、阈值设置都得对着这个表来。1.2 技术路线的取舍为什么不直接上大型检测网络最初我考虑过两条路线。A路线是端到端的目标检测比如Cascade R-CNN或者VFNet直接在整图上输出缺陷框B路线是两步走——先做图像分块再用轻量级分类网络判断每个块有没有缺陷最后通过后处理合并出缺陷位置。最终选了B路线。原因很实际小样本条件下检测头很难收敛。Cascade R-CNN的RPN需要大量正负样本边界框做回归400张缺陷图连训练集中框的数量都撑不起来强行上只会得到一堆漏检。而分类任务对样本量要宽容得多配合合适的骨干网络几百张图是可以做到不错效果的。另外检测网络在细长划伤这类极端长宽比目标上召回率普遍偏低而分块分类天然绕开了这个问题——分块后细长划伤哪怕只穿过某一块的一角分类模型也能捕捉到局部纹理异常。这套路的输出也很容易转成产线能用的坐标把大图切成固定大小的块每个块得到一个缺陷概率再用非极大值抑制思路合并相邻的异常块还原成缺陷的bbox。整个过程对后续部署也非常友好GPU负载低推理延迟可控在几十毫秒量级。1.3 评估指标的提前约定漏检是1过检是0.3评估指标必须在写第一行代码之前就定死。这个项目里客户接受漏检率小于1%同时希望过检压缩到工程师能承受的范围。我在合同讨论时坚持增加了一个“缺陷类型漏检明细”——整体漏检率达标不算完任何一类缺陷的漏检权重都不同划伤漏检1条比脏污漏检3条更严重。所以模型评估我用了三个维度整体样本维度的漏检率、单类缺陷的漏检率、过检率。在训练过程中每轮epoch后不只盯着总准确率还会单独看每类缺陷的召回率曲线。如果某类缺陷的召回率明显低于其他类别就要立刻停下来分析原因可能是分块尺寸不合适也可能是扩增策略损害了该类缺陷的特征。提示工业项目里指标口径不一致会导致后面验收扯皮。漏检率的分母是“有缺陷的样本数”过检率的分母是“无缺陷的样本数”这两个定义务必和客户书面确认不要用总样本准确率蒙混过关。2. 小样本数据策略扩增、合成与清洗2.1 分块尺寸的确定一个“拍脑袋但可解释”的过程小样本项目里数据不光是“数量”问题更是“粒度”问题。原始大图直接丢给分类网络会损失太多细节直接跑目标检测又欠数据。分块策略能把一个大图拆成几十上百个样本等于把样本量放大了几十倍。分块尺寸不能乱选。我常用的逻辑是让目标缺陷的最小可接受尺寸在分块图中至少占80×80像素以上的面积同时分块不宜过大否则一块里面正常纹理的干扰太强缺陷特征被淹没。这个项目里原始图像是2448×2048我选了512×512重叠率50%步长256这样一张图可以产生约12×7个有效块其中包含完整缺陷的块数由缺陷尺寸决定。分块之后要做的第一件事不是训练而是清洗。分块会把一个缺陷切成很多碎片很多块里只包含缺陷的一部分这类“极端局部块”如果作为正样本会让模型学到错误特征。我的做法是用标注框先粗略地把块分成“完整缺陷块”“边缘截断块”“背景块”只保留“完整缺陷块”和确定不含缺陷的“背景块”。边缘截断块可以单独抽出来做验证集看看模型对不完整目标的反应但不要污染训练集。2.2 数据扩增的边界什么能翻什么不能翻小样本训练离不开扩增但扩增不是越猛越好。工业表面缺陷有很强的方向性。划伤有随机方向但几乎不会垂直翻转成“镜像划伤”——实际上垂直翻转后的划伤形态在真实产线上很少见但水平翻转、垂直翻转和旋转90度对大部分形态影响不大。这个项目里我用了手工设计的扩增组合按缺陷类型分别处理划伤类随机旋转30度到150度、水平翻转、宽度抖动模拟不同深度压伤类亮度抖动、对比度变化、高斯模糊模拟不同光照异色类色相抖动、局部gamma校正通用Cutout随机遮挡但遮挡区域避开缺陷标注框这里提醒一句扩增时不能盲目套用imgaug预设。亮度抖动对划伤和压伤效果明显但对异色类缺陷可能把正常区域也变成“伪异色”。我第一轮跑完发现异色类的过检特别高查下来就是亮度抖动太强导致很多正常氧化色差被判定为缺陷后来把异色类的亮度抖动幅度从0.3降到0.05问题才缓解。另外背景块同样需要扩增。实际产线有不同批次、不同材质的表面纹理差异纯靠原图背景学习背景分布很容易过拟合。我加入了随机擦除背景块、纹理噪声注入让背景块的多样性跟上正样本的扩增强度。2.3 合成数据的胆子可以更大一点扩增解决的是“同一缺陷看更多角度”的问题合成数据解决的是“没见过的新形态”的问题。比如划伤这种缺陷形态变化极大真实数据里只有160张怎么扩也覆盖不了所有可能角度和长度。我用了三期合成策略效果排序下来第一基于真实划伤mask的拼接把划伤区域剪出来旋转后贴到正常背景的新位置上第二用Sketch和Noise组合生成全新的线条纹理加入背景后再叠加光照扰动第三完全程序化生成抽象划痕正弦波随机游走强度调制。合成数据必须遵守一个纪律合成出来的样本如果没有经过人工抽检不能直接进训练集。我会把合成的图批量让现场的质检员做快速分拣保留那些“一眼就是缺陷”的图剔除那些“看着怪但不明确”的图。400张真实图合成补充最后训练集的正样本块数量扩大到2500左右才勉强够把一个小模型喂饱。注意合成样本只能用于“预训练辅助训练”不能完全替代真实样本。纯合成训练的模型在真实批次上的表现会断崖式下跌这个我踩过千万别重蹈覆辙。3. 模型选型与训练细节轻量级分类网络也能扛3.1 为什么选EfficientNet-B3而不是更大的模型小样本场景下模型的容量是双刃剑。容量不够特征表达不足复杂缺陷学不动容量过大几百张图瞬间过拟合训练集100%准确率验证集一塌糊涂。我最初试了EfficientNet-B5和ResNet50验证集的缺陷召回率都不到90%而且训练过程很不稳定。后来换成EfficientNet-B3召回率反而涨了大约两个点。原因简单样本量撑不起大模型。在这个数据规模下B3的特征维度刚好能表达这几类缺陷的区分性同时正则化压力小很多。轻量级骨干还有个隐藏优势——推理快。工业检测和学术比赛不一样产线节拍卡在那儿GPU推理延迟超了100毫秒就可能导致流水线停滞。B3在TensorRT FP16下推理一版512×512分块大约只要8到12毫秒一张2448×2048的图按步长256分块大约80多个块全图推理也就在1秒上下。留出了充分的余量给后处理逻辑。3.2 损失函数与采样策略类别不均衡的两种解法分块之后的类别比例大约控制在1:3正块:背景块直接用交叉熵也能训但效果一般。背景块数量多模型会倾向于保守输出宁可把缺陷块也判成背景也要保证整体的准确率——这恰恰是漏检的根源。我同时用了两招。第一招是Focal Loss它对难分类样本的梯度更大对易分类的背景样本梯度更小特别适合压住背景样本的干扰。第二招是动态难例挖掘——每个epoch结束把验证集里预测概率在0.4到0.7之间的“模糊块”挑出来复制进训练集下个epoch继续训练重点补充模型最困惑的那个区域。这个“十字路口样本”策略在小样本场景下效果奇好。最终模型对真实验证集的漏检率从第一版的4.2%降到了1.8%其中很大功劳就是难例挖掘补充了最缺的信息量。还有一个细节训练阶段不要用移动平均模型的输出作为最终结果。我在测试时发现EMA模型的权重在某些缺陷类型上反而拉低了召回率后来干脆对比实验后选了不加EMA的版本原因可能还是数据量太小EMA平滑掉了个别缺陷的强响应。3.3 训练的监控节奏每个epoch都看“漏检曲线”小样本训练的epoch数不能靠“谷歌搜索到的经验值”必须靠监控。我从第一版开始就记录每个epoch后的验证集整体准确率、正样本召回率、各类缺陷召回率画成曲线。正常情况是总准确率先快速上升然后缓慢波动但正样本召回率会晚几轮才开始上升。如果发现某类缺陷的召回率到第20轮还是纹丝不动而且loss也不降那就说明该类缺陷的特征在模型里就是学不动换了损失函数或者加同类缺陷样本比训练更久有用。这个项目里我遇到的是“麻点”召回率卡在70%。后来查下来发现我在分块时把“麻点密集区域”和“正常喷砂背景”混在了一起因为麻点的粒度和喷砂纹理的粒度太接近分块图下特征几乎重合。解决办法是把麻点类缺陷的判定下放到更细的尺度——重新对疑似区域做二次放大裁剪判断而不是让分类网络硬学。这个思路在早期设计模型时就要预留好不然后面加逻辑会很痛苦。4. 漏检控制的工程化手段不只是调阈值这么简单4.1 漏检主要发生在哪里三类漏检的归因分析模型上线前的漏检分析非常重要但很多项目直接把漏检率当成一个数字来调这是最大的误区。漏检不是单一原因造成的必须分成三类来分析。第一类是“特征级漏检”——缺陷在图像上确实可见但模型没有提取到足够强的特征这类漏检主要靠改进模型和数据解决。第二类是“尺度级漏检”——缺陷真实存在但分块尺寸导致缺陷被切碎每个子块里的特征都太弱单块分类器全部判为背景。第三类是“标注级漏检”——原始标注时本身就没标全模型学到的是不完整的标注空间这类问题在工业现场极其常见。我做了个统计这个项目初期验证集的漏检里尺度级漏检占了将近一半。解决线索很简单看哪些漏检缺陷的bbox横跨多个分块。后来我加了跨块融合的后处理把相邻且同时有异常概率的块合并成一个候选区域并且允许“两个弱响应块拼接成一个强候选”——这个逻辑很管用细长划伤的召回率大幅上升。4.2 软阈值和分级报警别把模型输出的0.5当生死线默认的分类阈值是0.5但工业检测项目千万不要直接用。因为不同缺陷类型的置信度分布完全不同划伤类缺陷置信度普遍高0.8以上很常见但异色和低对比度缺陷的置信度往往在0.4到0.6之间游走如果一刀切把阈值设成0.5等于主动放弃了这批缺陷的召回。我用的策略是“分类型阈值分级报警”。对每种缺陷类型单独统计验证集上的置信度分布画出PR曲线选取每个类型的召回率目标对应的最优阈值。划伤我用0.45压伤用0.5异色用0.35麻点用0.3。然后所有高于阈值的块都作为缺陷候选但置信度在0.3到0.5之间的块最终不会直接作为“缺陷”输出到产线而是标为“可疑”进入一个复检通道由更高分辨率的二次模型复核。这个“可疑复检通道”非常关键。它把漏检风险拆成了两级第一级是模型确实没检出来第二级是模型犹豫了但被复检接住了。实际跑下来可疑通道大约占总样本的3%左右其中约三成是真实缺陷。用3%的过检算力换回30%的缺陷召回在工业现场是非常划算的买卖。4.3 多模型级联第一个模型负责别丢第二个模型负责别错单模型再怎么调在低对比度缺陷上的表现总是有上限。这个项目我最终采用了两级模型结构第一级是前面训练的EfficientNet-B3分块分类器负责最大化召回哪怕多报一些可疑区域也没关系第二级是一个专门对可疑区域做精细分类的ResNet18输入是从原图裁剪出的更高分辨率局部图专门区分“真缺陷”和“伪缺陷”。第二级模型的训练数据来自第一级模型在验证集上的“误报”——把背景块误判成缺陷块的那些样本加上真实缺陷块的局部裁剪图。这样第二级模型的任务变得更纯区分真缺陷长什么样、假缺陷长什么样。两级联调下来整体过检率从10%以上压到了6.5%左右而漏检率保持在1%以内。心得级联模型的本质是把“找目标”和“认目标”拆开。“找目标”的模型要傻一点宁可多看不多看漏“认目标”的模型要精一点专门替傻模型擦屁股。这个分工思路用小模型就能实现不一定要上大模型。5. 部署推断与全流程复盘5.1 推理管线设计把后处理也当“模型”来优化推理阶段的耗时大头往往不在模型本身而在前后处理。图像采集、格式转换、分块、缩放、归一化、后处理合并这些操作如果在CPU和GPU之间来回搬运数据延迟会翻好几倍。我这个项目最终用C封装了整个推理管线CUDA直接在GPU上完成归一化和标准化批量并行处理80多个分块然后用多线程把后处理合并逻辑放到CPU侧和下一帧的GPU推理时间重叠。实测单张图的端到端延迟从原来的1.8秒压到800毫秒左右完全满足产线节拍。后处理里还有一个细节容易被忽略缺陷的“区域连续性约束”。分块分类器输出的异常块在空间上应该是连续的如果每隔一块跳出一个没有规律的异常块大概率是纹理噪声而不是真实缺陷。我加了一个连通域分析把空间上孤立的小块直接滤掉只保留相邻异常块聚集的区域。这个简单的空间约束又干掉了一部分过检。5.2 常见问题排查速查表把这个项目从头到尾遇到的典型问题整理成表格方便各位排查时对号入座现象可能原因排查建议整体准确率高但某类缺陷召回率低该缺陷与正常纹理特征太接近或样本量不足单独统计该类缺陷的置信度分布降低该类阈值适当增加该类别真实样本训练集准确率极高但验证集很差模型容量过大或扩增强度过猛换成更小的骨干网络减少该类缺陷的合成样本比例漏检缺陷大多横跨多个分块分块策略切碎了目标特征增加重叠率加跨块融合后处理允许弱响应的相邻块合并过检集中在一片固定的正常区域该区域的纹理/光照分布接近缺陷特征收集该区域负样本重新训练第二级分类器或在该区域加专用掩码小批次换线后漏检率突然升高新材料表面纹理变化超过训练分布预留快速适配通道新批次少量样本冻结大部分网络做短时微调模型在白天和夜班表现差异大环境光照变化图像统计分布偏移在扩增中加入亮度空间扰动采集时固定光源保持原始图像一致性5.3 对这个项目最值得复盘的三点体会第一小样本项目的成败在数据定义阶段就决定了深度学习只是把数据里的信息榨出来。前期和工艺工程师一起把缺陷类型、形态边界聊透比后期调模型有效十倍。很多团队直接把产线图丢给标注公司做框回来发现框得不合理返工成本非常高。第二漏检控制靠的不是“更准的模型”而是“多级的安全兜底”。分类型阈值、可疑复检、跨块融合、空间约束、级联判定——每一层单独看都只提升一点点但叠加起来才能把漏检率按到产线能接受的范围内。工业场景下没有完美的模型只有设计得足够冗余的系统。第三合成数据是缓解小样本焦虑的加速器但它需要和你对缺陷物理形态的理解深度绑定。没有现场观察和工艺知识支持的合成数据很容易生成一堆“看起来对、实际假”的样本反而拖垮模型泛化能力。我在后续好几个项目里都沿用了“真实数据锚定合成数据扩展人工抽检过滤”这条流水线效果稳定推荐各位也试试。最后再分享一个小技巧每次项目结束把漏检的图例、误检的图例、以及模型置信度的分布图存成一个PPT发给客户。这比单纯报一个“漏检率0.8%”的数字要有说服力得多。客户看到你能清清楚楚说出漏检发生在哪里、为什么可以接受验收流程会顺畅很多后面催款都少费不少嘴皮子。