做工业视觉项目的朋友应该都有过这种体验:模型在测试集上的指标漂亮得能截图发朋友圈,一到产线就被工艺工程师堵在门口。去年我在一个金属外壳外观检测项目里,YOLOv5在验证集上的mAP超过0.95,缺陷召回率也稳在0.93,看起来“准”得不得了。但产线实际跑下来,误检率一直在15%上下晃悠,每检测100个报警里有15个是完好工件,产线频繁停机,人工复判岗位忙到冒烟。后来我用了一套“分层决策”重新设计检测后处理流程,把误检率压到了2%以内,同时召回率没有明显往下掉。这篇文章想把当时的完整思路、实施细节和踩过的坑都复盘一遍,给同样被误检折磨的人一个可参考的路线。
1. 为什么YOLO看起来准,现场却不买账
1.1 算法指标和产线口径不是一回事
项目刚启动时,我手上的离线数据集是白天在实验室里一批一批拍好的,光照稳定、工件摆放位置固定、背景干净。模型在这个测试集上的mAP超过0.95,缺陷召回率0.93,误检率大概只有4%到5%。但一到现场,问题就全冒出来了。
产线是连续来料,机台有振动,环境光会变,工件表面偶尔带着油污、水渍、灰尘,有时候还有上一道工序留下的定位压痕。这些在离线数据集里都没出现过,模型看到之后很容易把它们当成缺陷,输出一个“高置信度”的误检框。现场统计的方式也很直接:他们不看mAP,只看一天报警多少次,其中多少次拆开看是好的。按压缺、划痕、脏污这些类别口径一算,误检率确实到了15%。
这个现象给我最大的提醒是:YOLO这类检测器解决的是“看得见”的问题,但工业现场要解决的还有“拿得准”的问题。离线指标是在干净环境下算出来的,而产线关心的是误检带来的停机、复判和漏放成本。如果只闷头提mAP,不把评价口径对齐,项目做得再辛苦也不被认可。
1.2 高召回反而让误检的代价更大
一开始我以为是模型还不够准,于是把置信度阈值调低,想多召回一些真正的缺陷。结果缺陷召回率稍微上去一点,误检率跟着暴涨,产线反而更不配合了。原因不难理解:YOLO为了不放过真正的缺陷,会把很多可疑区域都框出来,尤其像反光点、纹理边界、倒角高光这类东西,在局部图像上和浅划痕非常像。阈值越低,这些假框出来得越多。
更麻烦的是误检会形成“狼来了”效应。操作工一天看到几十次报警,每次都要停下设备去确认,确认了十几次全是误检,后面再报警就懒得理了。这时候如果有一个真缺陷漏过去,反而更危险,因为人的注意力已经被训练成“报警=没事”。所以从业务角度看,十个漏检可能都没有五十个误检造成的实际损失大。这让我意识到,如果只优化检测器本身,不改变后端的决策逻辑,召回率越高、误检越频繁,项目就越难落地。
1.3 调阈值、堆数据、换模型为什么都解决不了
当时我试过三条路,基本都走不通。第一条是调置信度阈值,把阈值从0.25调到0.5、0.6,误检率会降一些,但缺陷召回率也开始掉。因为很多真实缺陷的置信度本来就不高,整体一刀切只会把好样本和坏样本一起“切掉”。第二条是堆数据,把现场拍的误检图一张张标注好了加进训练集,结果模型是变好了一点,但新误检又冒出来,每个种类的误检都是长尾分布,标了一个还有下一个,数据标注成本高到项目组受不了。第三条是换模型,尝试过更大的YOLOv5变体和YOLOv8,指标确实略有提升,但换汤不换药,误检模式没有本质变化。
后来我想明白了:检测器在特征提取上已经很强大,真正的问题出在“一张图只能给一个置信度”这种决策方式上。它缺少工艺知识,也没有多帧或上下文信息,更没有结合业务成本来做判断。继续在模型结构上卷,不如在决策层做文章。这就是后面“分层决策”的出发点。
2. 分层决策的核心思路:把“一锤子买卖”拆成三道闸门
2.1 一个模型干所有事,其实承担了太多任务
单层YOLO方案里,检测器必须同时干三件事:找到可疑区域、判断缺陷类别、判断“这到底是不是缺陷”。问题是最后一件事实质上属于工艺判断,需要知道工件表面原本长什么样、这个位置允不允许有这种特征、光照是不是造成的干扰。这些信息很难全部塞进一个检测头的局部特征里。
我当时的误检里有一大类是“模具压痕”,这其实是良品自带的特征,但在局部图里和“压伤”长得非常像。YOLO没有见过整张工件的模板,也没有人告诉它“背面区域不许有压伤”,它只能靠局部纹理猜。在这种场景下,一个模型同时做检测和判定,等于让一个没有工作经验的实习生只看一小块皮肤就判断是不是疤痕,误判率高是必然的。
2.2 三层结构:找出来、验一下、再拍板
分层决策的思想很简单:不要让一个模型做完所有事,而是把判断拆成三个阶段。
第一层是检测层,还是用YOLO输出候选框和初步类别,但阈值可以故意调低一点,目标是“宁可多找,不要漏掉”。这一层负责的是召回率,允许输出一些可疑框,反正后面有处理。
第二层是规则校验层,把那些明显不符合物理规律和工艺约束的框过滤掉。比如面积太小、长宽比离谱、位置出现在不允许缺陷的区域、与当前工件模板的差值不够明显等。这一层不追求全,只追求“每条规则都能解释清楚为什么是误检”。
第三层是仲裁层,用一个轻量级分类模型,输入候选框裁剪图和上下文特征,输出“真实缺陷概率”和风险等级。这一层学习的是“误检长什么样”,而不是“缺陷长什么样”,它能结合检测框的置信度、类别、区域、形态特征做综合判断。
三层分别解决不同的问题,缺一不可。检测层保证不漏,规则层保证不蠢,仲裁层保证不偏。
2.3 为什么分层结构能同时保住召回率和准确率
很多朋友第一反应是:多包一层会不会把缺陷也过滤掉?这里的关键是各层目标不同。第一层把阈值调低,实际上是在刻意“牺牲精度换召回”,真正决定精确率的是后面两层。第二层规则只针对那些有明确原因的误检,比如“反光点、水渍、模具压痕”,它们有很强的手工特征,用规则过滤不会伤害大多数真实缺陷。第三层仲裁模型的训练数据来自第一层输出,而不是原始标注集,所以它见过大量“看起来像缺陷的干扰”,有能力做最终裁决。
我用一个比较生活化的类比:以前是一个保安同时负责“发现可疑人员”和“判断是不是坏人”,现在改成三步——巡逻发现所有形迹可疑的人,安检根据证件和行李规则排除明显没问题的人,最后主管根据监控和背景信息做决定。每一步都在降低误判,而且每一步用的信息源都不一样,不需要某一个环节“全能”。
3. 把误检率从15%压到2%的落地过程
3.1 第一步:先给误检分类,别急着写代码
我先花了一周时间,把项目上线两周内保存下来的所有报警截图全部翻了一遍。这里有个经验:开始动手之前,一定要把“误检是什么”搞清楚,否则后面做的规则和仲裁模型都是在闭着眼睛优化。
我把误检样本按照外观特征聚成几类:反光点、倒角高光、模具压痕、水渍油污、灰尘颗粒、载具背景纹理。每一类下面再记录出现时的机位、光照方向、候选框位置和大小。统计之后发现,反光点和模具压痕加在一起占了全部误检的70%,这个结论直接决定了后面优先做什么。
这个步骤的重要性在于,它把“误检率15%”这个模糊数字拆成了一个个能动手解决的问题。后面每解决一类,误检率就会肉眼可见地下降,而不是对着一个综合指标瞎试。
3.2 第二步:用规则把一眼假的误检先干掉
规则层我只写了五条,目标是覆盖最容易识别的那部分误检,每条规则都反复验证过,确保不会误伤真实缺陷。
第一条:候选框面积小于某个绝对阈值,直接过滤。因为现场真正需要管控的压伤和划痕都有最小尺寸,小于0.2平方毫米的特征即使存在也不影响功能,没必要报警。第二条:候选框的长宽比大于10或者小于0.1,直接过滤。这是为了一些图像噪点形成的极端细长框。第三条:候选框中心落在指定禁检区域之外,但某些工位只有中心区域需要检测,边缘区域是夹具遮挡或倒角,缺陷类别不允许出现在这里。第四条:与当前工件标准模板的灰度差太小,过滤掉。如果候选区域内灰度和正常区域差不多,说明只是轻微纹理变化,不是真缺陷。第五条:如果同一位置在连续两帧中出现,但位置偏移超过一个较大范围,且面积变化率超过50%,判断为动态干扰,过滤掉。
规则不能贪多。我一开始写了十几条,什么边缘BLOB面积、最小外接矩形倾斜角、灰度均值阈值全加进去了,结果误检率看着降了,真实缺陷召回率却掉了一截。后来每加一条规则,都用“两百个真缺陷+三百个误检样本”跑一遍,如果会误杀任何真缺陷,就改条件或者干脆不要。
3.3 第三步:训练一个仲裁模型,学“误检长什么样”
规则把最蠢的误检过滤掉之后,剩下的误检就比较狡猾了,比如模具压痕和压伤,单靠几个数值分不开。这时候上仲裁模型。
仲裁模型不重新训练YOLO,而是只处理检测层输出的候选框。从每个候选框里裁出固定尺寸的图片,比如64乘64像素,同时保留检测框的坐标、面积、长宽比、原始置信度、类别编号这些结构化特征。我还叠加了候选框区域的梯度直方图和与局部模板的差值图,作为额外通道。标签就是“真缺陷”和“误检”二分类。
训练数据来自第一层输出,这个很关键。不能只从公开数据集里找正样本,一定要从现场的真实输出里采样,因为仲裁模型需要学的不是“缺陷是什么”,而是“在这个产线上,哪些东西看着像缺陷其实是干扰”。我先手工标注了两万多个样本,其中真缺陷占30%,误检占70%,正负样本差距不算离谱。如果误检样本太多,后面可以调整损失权重。
模型结构不用复杂,我当时用一个很小的分类网络,输入图像加特征向量,输出两个分数,参数量只有十几万,单张图片推理时间不到2毫秒。重点是特征怎么组合,而不是网络多深。这个仲裁模型跑起来之后,误检率一下子从8%降到了3%左右。
3.4 第四步:让决策和业务成本联动,按风险分级放行
模型输出的概率是一个0到1的分数,如果还按以前“大于0.5就报警”来用,最多只能把误检率压到3%,到不了2%。后来我想到一个更实际的思路:不一定每个疑似缺陷都要引起停线,可以按风险等级走不同的业务路径。
我设置了三个决策等级。风险分大于0.9,进入红色等级,直接触发停机并自动分拣;0.6到0.9之间,进入黄色等级,报警但不立刻停机,弹窗通知线边复判员复核;低于0.6,进入绿色等级,只在系统后台记录日志,不打扰产线。这样算下来,真正让产线停机的只有高分报警,误检造成的影响就大幅缩小了。
同时,不同工位的阈值可以单独配置。比如在最终出货前的外观检测工位,缺陷后果严重,我把红色阈值设成0.75,宁可多报警;在中间加工工序后的检测工位,反正后面还有清洗和打磨,我把红色阈值设到0.95,尽量减少不必要停机。这种“决策和业务成本联动”的思路,几乎不增加任何算力成本,但对实际体验的提升非常明显。
3.5 落地效果:误检率掉到2%,召回率基本没伤
上线后的完整数据我整理成了表格,前后对比一目了然。
| 方案阶段 | 缺陷召回率 | 误检率 | 平均单帧耗时 |
|---|---|---|---|
| 单层YOLO,阈值0.25 | 0.93 | 15% | 12毫秒 |
| YOLO + 规则过滤 | 0.92 | 8% | 13毫秒 |
| YOLO + 规则过滤 + 仲裁模型 | 0.91 | 3% | 15毫秒 |
| 加风险分级决策后的最终方案 | 0.91 | 2% | 15毫秒 |
缺陷召回率从0.93掉到0.91,原因是仲裁模型确实会误杀极少一部分置信度不高但真实存在的缺陷。但产线日常更在意的误检率,从15%降到了2%。原来一天停机30多次,现在不到5次,复判人员的工作量减少了一大半。更重要的是,操作工不再天天被报警轰炸,真正出现报警时他们愿意停下来看了。
4. 踩过的坑和排查技巧实录
4.1 规则越写越多,误检率不降反升
我前面提到我最初写了十几条规则,那个版本差点让项目翻车。当时我只盯着误检样本,每发现一个新的误检模式,就迫不及待加一条规则去挡。结果规则之间互相叠加,有些真实缺陷被组合条件误杀,有些误检样本虽然被挡了,但出现了新的“钻规则空子”的误检。
后来我总结出一个工作习惯:每条规则必须有正反例验证清单,正面放十个真实缺陷,反面放十个误检样本,改动之后两个集合都得过,否则不接受这条规则。这等于给规则层加了一个“回归测试”,能拦住很多拍脑袋的决策。
4.2 仲裁模型的正负样本比例,千万别失衡得太夸张
训练仲裁模型时,因为第一层输出里大部分是误检,如果不加控制,正负样本比例会到1比9甚至1比20。模型训练出来之后,几乎所有输出都会偏向“误检”,真实缺陷都被压到低分区间,召回率掉得厉害。后来我把真缺陷样本通过过采样和裁剪增强抬到占比30%左右,同时在损失函数里给正样本加了1.5倍权重,才把召回率稳住。
还有个小坑:仲裁模型的训练样本不能只收集一次。产线换料、调整光照、更换夹具之后,会出现新的误检模式。我建议每隔两周从现场报警记录里抽样,补充到训练集里做增量训练,否则模型用久了会逐渐“脱节”。
4.3 上线前一定要跑一遍“误检回归测试”
这是我最想强调的经验。仲裁模型更新或者YOLO权重更新之后,不能只看精度和召回率,一定要准备一个固定的回归样本集,把历史出现过的误检类型都放进去。这个回归集不需要太大,我用了大概三千张图,每次模型更新后跑一遍,要求误检率不能高于上一版本,更不能让某些旧误检重新冒出来。
有一次我更新了一个检测头之后,mAP和召回率都涨了,但模具压痕误检突然又冒出来,仔细一看是新的模型把某个纹理特征学得更敏感了。如果没有回归集,这种问题一上线就会被产线立刻发现,非常被动。
4.4 常见问题排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 仲裁层输入候选框太多,处理时间变长 | 第一层YOLO阈值太低 | 适当提高第一层置信度下限,或者增加“可疑度初筛” |
| 规则过滤掉了真实缺陷 | 规则条件过强或叠加过多 | 用正反例回归集逐条验证,给仲裁高分样本加豁免逻辑 |
| 光照一变化就误检暴增 | 现场光照未归一化,参考模板过期 | 增加光照归一化,定期更新模板图 |
| 仲裁模型对某类误检失效 | 该类新误检没进训练集 | 补充最新误检样本做增量训练 |
| 风险分级后产线还是嫌误报多 | 红色阈值设置过严 | 按工位重新配置阈值,平衡召回与停机影响 |
这些排查经验不是一次到位的,都是在反复救火过程中总结出来的。尤其第一条,第一层如果阈值设到0.1,候选框量会是0.25时的两三倍,仲裁层再快也会成为瓶颈。实际操作中我会在第一层和仲裁层之间加一个极低成本的初筛,比如置信度小于0.05的框直接丢掉,能省很多事。
在我个人经验里,工业视觉项目真正难的点往往不在模型有多强,而在怎么把模型输出和现场业务缝起来。YOLO这类检测器已经足够好用,但“一个模型一把梭”的思路在复杂产线上确实走不远。分层决策不是推翻检测器,而是在检测器外面包一层懂工艺、懂业务、懂成本的控制逻辑。如果你也在被误检率折磨,建议先别急着换模型,试着把现场报警截图分类归档,你可能会有新的发现。