多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

工业缺陷检测实战:小样本训练与漏检控制的完整思路

工业缺陷检测实战:小样本训练与漏检控制的完整思路 做工业视觉缺陷检测这几年我最怕听到的不是“模型跑崩了”而是“测试集上准确率99.6%一上产线就漏检”。准确率这个数字在学术任务里是勋章在产线上却经常是陷阱——一个班次几万件产品哪怕漏检率只有0.1%流出去的不良品也是几十件。工业缺陷检测、小样本训练、漏检控制这三件事本质上是一条绳上的三个结数据少导致模型学不充分学不充分就容易漏漏了又不敢轻易放宽阈值一放宽误检跟着涨。这篇文章我想把这几年做缺陷检测项目的完整思路拆开讲从为什么难、怎么用少量缺陷样本把模型喂饱到漏检控制的工程化手段再到一套可以直接落地的全流程框架给正在跟产线死磕的同行一个参考。1. 为什么工业缺陷检测里“99%准确率”反而是事故导火索1.1 漏检的代价与误检的代价这个选择题基本没有悬念先算一笔账。假设一条产线一天生产2万件产品算法准确率99.6%看起来很高对吧。可这0.4%的错误就是80件产品如果这80件里有一批带有贯穿性裂纹或者会影响装配的毛刺流到客户装配线上就是批量投诉赔款动辄几十万还会影响后续订单。反过来看误检也就是把良品拦下来成本相对轻一些——产线多一个复检工位人工再确认一遍浪费的是工时和效率但不至于造成品质事故。所以工业缺陷检测里漏检和误检的代价天然不对称。我要说的是这个不对称会直接影响后面所有技术决策损失函数的权重怎么设、阈值怎么调、二阶段复核怎么设计全都围绕“宁可多拦、不可漏放”展开。很多刚入行的朋友习惯性地追求准确率和精确率的平衡这在学术数据集上没问题但在产线上漏检是红灯误检是黄灯优先级完全不一样。1.2 产线上的三个天然阻力样本少、类型偏、节拍快第一个阻力是缺陷样本真的很少。缺陷在正常生产里是小概率事件一个良品率99.5%的车间跑一整天可能只有几十个真正的缺陷产品而且类型还不一样有划痕、有脏污、有压伤、有气泡每一种可能就几张图。标注还得靠有经验的质检员一张图画框加分类快也要几十秒一天标不了多少张。第二个阻力是缺陷类型呈长尾分布。常见缺陷就那么三四类偶尔冒出一个新形态——比如换了批次原材料之后表面纹理变化导致原来能检出的缺陷现在长得完全不一样。模型碰到这种样本外缺陷基本就是盲区。第三个阻力是产线节拍。很多检测工位的节拍是每秒1到2件也就是算法要在几百毫秒甚至几十毫秒内完成拍照、推理、判定。你没法把一个超大模型堆上去计算资源是受限的。这三个阻力叠加在一起决定了工业缺陷检测不可能像互联网场景那样“数据不够、算力来凑”必须走小样本训练和工程化漏检控制的路子。1.3 小样本训练为什么是这一行的宿命我见过不少团队一上来就想搞大模型、搞自监督预训练结果训练数据就两三百张缺陷图模型学了两天就开始过拟合。说实话工业场景里“数据不够”不是暂时现象而是常态。新项目启动时客户给的历史缺陷样本通常不到一千张有些稀有缺陷可能就靠几张“经典照片”撑场面。如果团队的训练方法论是建立在“海量数据”前提上的那基本做不了产线项目。所以小样本训练不是研究课题是吃饭的手艺。它的核心目标不是刷SOTA而是在有限样本下做到“稳定的召回”同时给后续的漏检控制留出空间。数据增强、迁移学习、损失函数调整、阈值标定这些手段组合起来才能在两三百张样本的基础上搭出一个能上线、可迭代的检测系统。2. 小样本训练的破局从数据和模型两头把自己“喂饱”2.1 数据不够增强来凑但增强要“像真的”而不是“花活”小样本训练的第一个抓手是数据增强。很多人的第一反应是“加噪声、旋转、裁剪”但实际上工业场景的增强需要认真设计否则增强出来的是另一种东西。我常用的基础增强组合是这样的增强手段适用场景需要小心的坑亮度/对比度扰动模拟产线光照波动幅度过大会让缺陷特征失真高斯噪声 / 斑点噪声模拟传感器底噪别覆盖掉细小缺陷随机平移、小角度旋转模拟产品摆放偏移方向性纹理要谨慎旋转随机擦除CutOut模拟遮挡、脏污擦除区域别总在同一个位置Mosaic拼接增加单图缺陷密度拼接边缘易产生伪影关键原则是增强后的图像看起来仍然像是产线上会出现的真实样本。比如电池表面的划痕检测做旋转时要考虑电池极片有方向性随机翻转90度可能把“横向划痕”变成“纵向划痕”这对于某些对方向敏感的工艺来说就不是同一种缺陷了。增强不是越多越好而是越“贴近真实”越好。2.2 合成缺陷数据让工厂的“废品库”变成你的训练资源当真实缺陷样本实在不够时合成数据是一条很实用的路。具体做法是拿一批确定无缺陷的良品图像用图像处理手段人工生成缺陷形态再叠加到良品图上。比如划痕类缺陷可以用OpenCV画随机方向的曲线加上高斯模糊和亮度变化模拟真实划痕在光照下的表现。import cv2 import numpy as np def generate_scratch(img, max_length200): h, w img.shape[:2] overlay img.copy() start_x, start_y np.random.randint(0, w), np.random.randint(0, h) # 生成随机走向的划痕轨迹 points [(start_x, start_y)] for _ in range(np.random.randint(3, 8)): dx np.random.randint(-50, 50) dy np.random.randint(-50, 50) points.append((points[-1][0] dx, points[-1][1] dy)) # 用折线画划痕然后高斯模糊模拟真实视觉 cv2.polylines(overlay, [np.array(points)], False, (0, 0, 0), np.random.randint(2, 6)) overlay cv2.GaussianBlur(overlay, (5, 5), 0) return overlay合成数据要注意“域差距”。纯合成的图像纹理太干净直接放进训练集可能让模型学到合成痕迹而不是缺陷本身。我习惯的做法是合成数据和真实数据按比例混用同时给合成图像叠加光照变化、JPEG压缩伪影、镜头模糊等降质操作让合成图更接近相机成像的真实效果。我们之前在一个表面划痕项目上用这种思路生成了几千张训练图配合几百张真实缺陷图效果比只用真实图硬训好了不止一个档次。2.3 别从零硬训迁移学习和骨干冻结撑起早期训练小样本条件下最忌讳从随机初始化开始训练整个检测网络。一个检测模型有几千万参数几百张图根本撑不起充分训练结果就是严重的过拟合。这时候迁移学习的价值就体现出来了用在大规模数据集上预训练过的模型做初始化底层特征边缘、纹理、角点是通用的只需要用缺陷数据微调上层与任务相关的部分。实际操作上我的习惯是加载预训练权重后先把骨干网络backbone整体冻结只训练检测头head和特征融合层。用低学习率训练几十个epoch之后再解冻骨干网络的后几层用更小的学习率做全局微调。这个策略相当于先让模型在“已有视觉常识”的基础上去理解缺陷而不是从零开始连“什么是边缘”都要靠少量样本去悟。类比一下的话相当于教小孩认字之前先让他学会笔画后面的效率完全不一样。2.4 训练策略的细活损失函数、学习率与早停小样本训练的另一个关键在损失函数。缺陷检测普遍面临类别不均衡一张图里大部分区域是背景真正的缺陷区域可能只占几个像素。默认的交叉熵损失会被大量易分类的负样本主导模型学不到缺陷特征。Focal Loss是这个场景下的常用手段它在交叉熵基础上引入调制因子让模型把注意力集中在难分类的缺陷样本上。class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, pred, target): bce F.binary_cross_entropy_with_logits(pred, target, reductionnone) p torch.sigmoid(pred) alpha_t self.alpha * target (1 - self.alpha) * (1 - target) modulating (1 - p) ** self.gamma loss alpha_t * modulating * bce return loss.mean()学习率和早停也要同步配合。小样本训练时我会把初始学习率设得比常规偏低比如1e-4并加上短warmup防止早期权重剧烈震荡把预训练学到的特征破坏掉。早停的监控指标也不会选训练loss而是验证集上的召回率——因为漏检就是工业项目的生死线。训练过程中如果召回率连续多个epoch不涨就果断停下来继续拖只会过拟合。3. 漏检控制不是调一个阈值那么简单三层防线的工程化设计3.1 模型层让模型在训练时就“偏袒”少数类漏检控制的第一道防线不是部署时调阈值而是训练时就要让模型对缺陷足够敏感。手段有两个一个是用类别权重或代价敏感损失另一个是难例挖掘。类别权重的思路很直接训练时把缺陷类别的损失权重调高相当于人为告诉模型“漏掉这个类别代价巨大”。比如缺陷类别的权重设为5背景类别权重设为1模型会更倾向于把不确定的区域判成缺陷。这个策略在缺陷形态比较稳定、数量少的情况下特别好用。难例挖掘则是训练过程中动态找出那些被判错的缺陷样本把它们放进下一轮的训练重点里。两者可以结合先用类别权重训练几个epoch再用难例挖掘聚焦那些难以识别的漏检样本。这里我想强调一点模型层的偏袒要掌握好度。把权重拉得太高模型会出现严重的误检倾向后续阈值标定也会很难做。我一般从2到3倍的权重起步观察验证集上recall和precision的变化再逐步调整。3.2 阈值层对召回率做显式标定而不是停留在默认0.5模型输出的置信度阈值很多人习惯性地用默认的0.5这在工业场景里是个很大的坑。0.5这个值是在“假阳性与假阴性代价对等”的假设下得到的而工业场景的代价明显不对等。阈值标定的思路应该是先根据业务定一个目标召回率比如99.5%然后在验证集上搜索能达到这个召回率的最低置信度阈值。from sklearn.metrics import precision_recall_curve def search_threshold(confidences, labels, target_recall0.995): precision, recall, thresholds precision_recall_curve(labels, confidences) for p, r, t in zip(precision, recall, thresholds): if r target_recall: return t return 0.1这种搜索在实践中很常见。我们把阈值从0.5一路往下调召回率从96%升到99.6%同时误检从0.2%涨到3%。表面上看误检很吓人但在产品设计上这是可接受的——第一道模型阈值放低目的是覆盖所有可疑样本误检交给后面的工序处理。如果直接在这个环节追求低误检率阈值就会拉高漏检率必然跟着上去。3.3 决策层粗筛加精检把召回拉满再用第二道闸门消除误报阈值一放低误检数量必然上升这一步不是退步而是给下一道工序留出操作空间。我在大多数项目里都会引入“粗筛精检”的两级结构第一级是粗筛模型阈值放得很低目标是把所有可疑目标都找出来宁可多抓。第二级是精检模型对第一级的候选区域重新判断用更高的阈值或更精细的特征做最终判定。如果精检模型也拿不准最后的兜底是人工复检。举个例子某项目上线初期粗筛模型的误检每天有几百个全堆到人工复核工位根本看不过来。我们后来在精检层引入了一个轻量分类器专门对粗筛切出来的候选区域做二次分类把明显是纹理反光、油渍残影的误检大量过滤掉最终发往人工复核的候选只剩每天几十个同时漏检率一直控制在目标线以下。除了二级复核还有几种常用的工业场景决策手段。一个是对同一产品从多个角度拍摄综合多帧结果投票另一个是在时间维度上做连续帧判断因为产线上的缺陷在产品运动过程中会在连续帧里重复出现单帧的不稳定判断可以通过多帧平滑来抵消。3.4 漏检复盘机制建“事故档案”让模型每季度都变聪明一点漏检控制在工程上永远没有终点。我见过很多项目上线第一天效果很好跑了两个月漏检率悄悄往上走原因是产线换了批次材料、改了光源角度或者干脆出现了全新的缺陷形态。应对这个问题的核心机制是建立漏检样本的“事故档案”。具体做法产线上凡是流到后道并且被确认漏检的样本都要回传算法团队登记缺陷类型、图像、当时的光照和批次信息。每周或每月做一次复盘统计漏检原因归入几个常见类别漏检根因常见表现对应手段样本外新缺陷缺陷形态从未见过新样本入训练库增量训练环境变化光照、角度、吹气方向改变更新预处理参数重新标定阈值模型过拟合训练集效果好现场召回下降降低训练集中某些类别的权重阈值漂移置信度分布整体偏移周期性用新数据重新搜索阈值这些“事故档案”不只是被动记录更是下一轮训练数据的重要来源。项目运行半年后模型每次迭代都会融入这些复盘样本漏检率会随着闭环轮次增加而逐步收敛。4. 完整流程实录从缺陷定义到产线监控的一次落地4.1 需求定义阶段把“看着不对劲”翻译成可计算的规则很多项目一开始就栽在需求模糊上。“把这个表面检一下”“这里不能有缺陷”这种话听着简单落实到算法上根本无从下手。我的建议是项目启动时拉着质量部、产线工艺和算法团队开一次需求对齐会把每个缺陷类型写成可计算的判定规则。比如划痕类缺陷的判定规则可以是长度≥3毫米或面积≥0.5平方毫米且灰度对比度超过某个值才判NG。脏污类的规则可以是直径大于1毫米或覆盖面积超过产品总面积的0.1%判NG。这些规则不需要写得像算法论文那么精确但一定要让所有人知道“这个缺陷到底长什么样、大到什么程度才算不良”。4.2 数据基建抽帧、清洗、标注规范缺一不可训练数据从产线上获取时通常不是直接拿缺陷照片而是录视频再抽帧。抽帧的时候要注意不要只挑“完美角度”的帧那样会让模型对视角变化很敏感。要在工件经过检测工位的完整过程中均匀抽帧把不同光照、不同偏移位置的形态都覆盖到。标注规范同样不能省。我习惯推动团队建一本简洁的标注手册哪些缺陷类型算一类、画框还是画分割掩码、有歧义的样本单独建“争议池”找老师傅仲裁。画框适合目标型缺陷脏污、划痕区域分割掩码适合形态不规则或与背景混淆的缺陷纹理异常、边缘缺损。另外就是不要只标注缺陷一批干净的良品图也要保留下来它们不仅能当负样本还是后期做异常检测、验证误检率的底料。4.3 基线模型与迭代循环别急着调参先让流程转起来小样本项目最容易犯的错误是“想一步到位”花两三个星期调模型结构、抠增强参数结果连一套完整的训练-验证-评估闭环都没跑通。正确做法是先用一个成熟开箱即用的检测框架比如YOLOv8这类单阶段检测模型或者nnU-Net这类分割框架取决于缺陷类型快速搭建一个基线模型把训练管线、验证脚本、指标统计全部跑通。这时候模型的精度低一点没关系关键是建立流程。基线模型跑通之后进入迭代循环训练 → 在验证集上找漏检样本 → 分析漏检原因 → 补数据或调策略 → 重训。每一轮迭代的目标都很单纯把上一轮漏掉的那几类样本捞回来。前面说的合成数据、加权损失、阈值标定都是在这个循环里逐步加进来的。经过几轮迭代漏检率通常会以肉眼可见的速度下降。4.4 部署与监控模型上车之后才是真正的考试模型上线部署时性能优化是绕不开的一环。检测模型在GPU服务器上可能跑得飞快但产线边缘设备的算力往往有限。常用的手段是把模型导出为ONNX再通过TensorRT做FP16量化或INT8量化把推理延迟压到几十毫秒以内。量化之后精度一般会有小幅下降所以上线前要在验证集上重新确认漏检率没有恶化。部署之后还要建立监控机制。我在一些项目里会加一个简单的统计看板记录每天的总检测量、拦截量、复检确认的不良量和疑似漏检量。同时监控置信度分数的分布变化如果分布出现明显漂移说明产线环境或产品形态发生了变化该安排重新采集数据做增量训练了。5. 踩坑纪实与小样本训练的几个反直觉结论5.1 标注标准不统一同一个缺陷两位老师傅吵起来了这个坑几乎每个项目都会遇到。我们有一次做注塑件外观检测同一个缺陷照片一位老师傅说这是“拉伤”要判NG另一位说这是“正常模具痕”可以放行。算法的训练数据在这种互相矛盾的标准下会变得非常“精神分裂”。解决方法是把争议样本集中起来召开标注评审会把判定规则落到纸面上形成标注手册之后所有标注人员统一执行。标注质量比标注数量更要紧十几张错误标注造成的破坏力可能超过几十张有效标注带来的收益。5.2 别用花式增强炫技逼真性大于多样性我一开始做小样本训练时总想把所有增强手段都堆上去旋转90度、颜色反转、极强噪声、放射畸变一起上。结果训练出来的模型在合成增强样本上表现得很好一到产线上真实样本就认不出来。原因很简单增强过猛之后图像的特征分布已经偏离了真实相机的成像分布模型学到的是“增强后的纹理”而不是“真实的缺陷特征”。增强要围绕产线真实变化来做光源亮度波动、角度偏移、工件脏污程度而不是为了数据“多样性”去强行扭曲。5.3 大模型在小数据上并不香这是一个反直觉但很重要的结论。很多人以为模型越大表达能力越强小样本下也应该更强。实际体验是几千万参数的大模型在几百张缺陷图上大概率比几百万参数的小模型过拟合得更严重。特征表示能力强意味着模型有更大的自由度去记住训练数据里的噪声细节而小样本训练恰恰要的是约束和泛化。所以在实际项目里我更倾向于在骨干网络规模上做减法——用轻量级骨干加精心的训练策略而不是堆大模型。原因很简单小模型更容易在少量数据上收敛稳定配合阈值标定和后续迭代加上部署时延迟也更低整个链路都更顺。5.4 让良品数据也参与建模异常的另一种识别思路还有一个思路值得尝试别只盯着缺陷样本良品数据也可以反向利用。对于某些非常难以用画框方式定义的缺陷类型比如不规则的纹理异常可以用无监督异常检测的思路只拿良品图训练一个重构模型或特征嵌入模型当输入样本的重构误差或特征距离明显偏离良品分布时判定为异常。这个方法不走“见多识广”的路线而是走“熟能生巧”的路线模型太清楚正常长什么样稍微不对就会触发警报。所以我的建议是不要把异常检测当成替代方案而是作为检测系统的补充兜底专门用来接住那些没见过的“样本外缺陷”。尤其在长尾分布的场景里它是很值得关注的一条路。5.5 一点实操心得先保召回、再降误报闭环要转起来最后分享一点个人感受小样本工业缺陷检测项目的核心其实不是算法炫技而是要把“召回优先、工程兜底、闭环迭代”这几个原则贯彻到底。首次上线宁可误报多一点也要先把漏检压到目标线以下。误报可以靠复检工位、粗筛精检结构来消化漏检流到客户手里则是真正的事故。然后靠漏检复盘机制让每一轮闭环都成为下一次迭代的养料。经历过几次从一片混沌到稳定运行的项目之后我越来越确信一件事比模型更重要的是你有没有一套能持续变好的流程。
返回列表