ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

工业质检双料王方案复盘:无监督异常检测与定位

2026/9/29 17:22:38 拓冰建站 浏览量
工业质检双料王方案复盘:无监督异常检测与定位 2020年ECCV线上举办那段时间我们团队把工业质检这块活儿干到了两个榜单第一。当时结果放出来圈里不少人问“双料王”到底怎么做到的。这里说的双料王是指在ECCV 2020工业质检相关的视觉异常检测任务上同一套方案同时拿了异常检测图片级AUROC和异常定位像素级AUROC两个赛道的第一。做工业机器视觉的朋友都清楚这两个指标想同时稳住有多不容易一个管“有没有问题”一个管“问题在哪”前者要全局敏感后者要空间准确经常顾此失彼。这篇文章就把我们当时的整体设计、关键参数、踩坑记录做一次完整复盘不敢说能让你直接复现到第一但至少能让你在自己项目里少走很多弯路。1. 工业质检到底难在哪从两条赛道说起1.1 质检场景下的真实痛点我在产线项目上待得越久越觉得工业质检这个方向的难点从来不在“模型结构不够先进”而在“缺陷样本根本攒不齐”。一片金属表面可能有划痕、凹坑、脏污、锈蚀、漏镀每种缺陷在高光、低光、不同角度下长得完全不一样。让产线工人去标几千张缺陷图成本高不说很多缺陷一个月都碰不到一次数据集天然就是长尾中的长尾。更麻烦的是就算你凑了一批缺陷样本用监督学习训了一个分类网络上线之后遇到没见过的缺陷形态仍然大概率漏检。工业现场最怕的就是这种“未知缺陷”因为一旦漏掉成本可能是整批工件报废。所以从项目第一天起我们就定了一个方向不做闭集分类要做开集异常检测也就是只用“正常样本”学习什么是正常的训练阶段完全不需要缺陷样本。1.2 双赛道评估异常检测和异常定位是两回事这次ECCV 2020工业质检相关的评测把任务拆成了两条赛道正好对应着工业现场的两种需求。第一条赛道是异常检测衡量的是“这张图有没有异常”指标用图片级AUROC。这个赛道对算法要求是正常样本的得分要低异常样本的得分要高整体排序能力强。比如一个瓶盖表面有个几毫米的划痕算法得分必须明显高于正常瓶盖才能在上千个工件的产线上把次品挑出来。第二条赛道是异常定位衡量的是“异常具体在哪个像素区域”指标用像素级AUROC。这个赛道难很多因为你要给每个像素一个异常概率既要精准命中真正的缺陷区域又不能把纹理背景里的正常变化当成异常。很多方法在图片级指标上能刷到很高一到像素级就露馅热图上一片噪点根本下不去眼。我们的目标从一开始就不是只拿单榜第一。我们想要的是同一套特征、同一个模型、同一条推断链路同时把这两条赛道都做到极致。因为这正是工业现场真正需要的一个模型既要判断“有没有问题”又要指出“问题在哪”方便后续机械臂剔除或人工复判。1.3 为什么是ECCV 2020这个舞台ECCV是计算机视觉领域的顶级会议2020年轮到欧洲承办。那一届除了主会议还有很多和工业结合紧密的workshop和挑战赛工业质检就是其中一个非常亮眼的方向。很多团队用的基准都绕不开MVTec AD这个数据集在2019年发布之后迅速成为工业异常检测的事实标准15类物体涵盖瓶盖、坚果、皮革、织物、电缆、晶体管等既有纹理类也有物件类训练集只有正常样本测试集混入各种真实缺陷评测协议干净利落。能在这样一个国际评测上拿到双第一我觉得意义不在于“名字好听”而在于验证了一条基本假设无监督异常检测在工业场景下是真的能打的。要知道当时很多团队还在靠监督分割、逐缺陷检测的老路子我们这套在训练阶段完全没见过缺陷的方法能拿到双第一实际上是在告诉整个行业换一个解题思路可能比堆更多数据更有效。2. 双料王方案整体设计思路与路线选择2.1 为什么弃用监督分类改用无监督异常检测我一开始也试过老老实实上监督方案标注缺陷框、训练目标检测模型、再让分割网络出mask。但实测下来问题非常多。第一个问题是缺陷样本太少正负样本比例往小了说也是1:1000训练出来的模型对少数类严重过拟合。第二个问题是缺陷类型是开放集你训练时标了“划痕”“凹坑”两类现场突然来了一个“氧化斑”模型就会当作背景放过去。这个在学术指标上看不出来在产线上是会出大事故的。无监督异常检测思路完全不同我不需要知道缺陷长什么样我只学习正常样本的特征分布。推理阶段如果一个样本的特征偏离了训练时的正常分布就认为是异常。这种思路有一个天生的优势缺陷形态不需要提前定义只要是和正常形态有明显偏差的都会被揪出来。对于工业现场“不知道下一个缺陷长什么样”的场景这才是真正可落地的方案。2.2 “预训练特征局部密度估计”的路线选择当时可选的技术路线有几种基于重构的自动编码器、基于生成对抗网络的异常修复、基于自监督学习的嵌入向量以及基于预训练CNN特征的密度估计。我们逐一做了对比实验最后选定了“预训练特征patch级密度估计”这条路不是因为它最花哨而是因为它最稳。基于重构的方案用AE或GAN把正常图像重建出来推理时重建误差大的地方算异常。听起来很合理但有个致命问题如果网络的重建能力太强异常区域也可能被重建得像正常区域一样漏检率下不来。如果重建能力弱正常纹理的还原度不够误报率又蹭蹭上涨。这个平衡在工业纹理上非常难调尤其是皮革、织物这类重复性不强的表面。预训练特征路线的逻辑就不一样。我们直接用一个在ImageNet上预训练好的ResNet50提取特征。预训练模型在千万张自然图像上学到了一个非常通用的特征表征这种表征对颜色、形状、纹理的编码能力远超我们自己在小数据集上从头训练的模型。我们的核心假设是正常工业样本的特征在特征空间中会形成某些局部簇而异常样本的特征会偏离这些簇。既然假设是“分布偏差”那就直接用密度估计建模。2.3 检测和定位如何共用一套特征“双料王”不是靠两个模型分别拿第一而是同一套特征链路同时服务两条赛道。这是我们在设计阶段就和很多团队不一样的地方。一张输入图像经过ResNet50我们从不同stage取出特征图得到的是不同分辨率的表征。stage越深语义信息越强对“这到底是不是异常区域”的判断越准stage越浅空间细节越丰富对“异常区域精确边界在哪”的定位越准。拿第3、4、5个stage的特征图来说它们的空间尺寸相差好几倍我们在每个尺度上都独立建立正常的patch级分布模型。推理时一张图的所有patch都算一个异常得分把得分矩阵上采样回原图大小就得到像素级热图这就是定位赛道的输出再把热图里的高得分patch聚合成一个全局分就是检测赛道的输出。一个模型两条分支两个榜单全吃。3. 关键实现细节从特征提取到分数融合3.1 数据与评测协议准备我们用的是MVTec AD这份公开数据集。数据分为15类每类都有独立的训练集和测试集。训练集只有正常样本测试集包含正常和多种缺陷类型的异常样本。这里有个很容易踩的坑MVTec AD各类别图像分辨率差异很大从大约700×700到1024×1024都有如果不统一处理策略后面特征提取和热图恢复都会出问题。我们的做法是先统一缩放到448×448。这个尺寸是我们通过实验试出来的256×256信息损失太大尤其是细小的划痕缺陷在缩小后像素级指标会明显下滑1024×1024原生分辨率定位很准但内存和显存开销翻了好几倍实验迭代速度根本跟不上。448是一个够用的折中。评测时我们严格使用官方提供的train/test划分不额外加任何其他数据。3.2 多尺度特征提取与层选择特征层选择是整条链路里最值得花时间的部分。我们试过单独用stage4、单独用stage3、两两组合、三个stage一起用。单独用stage4的图片级AUROC最高但像素级定位热图过于粗糙单独用stage2的定位边界很好但误报严重。最后我们确定用stage2、stage3、stage4三个尺度一起建模分别赋予不同权重。这里有一个细节stage2的特征通道数是256stage4是1024如果直接全部保留建模时的计算量会非常大而且高维特征在小样本下协方差估计不稳定。我们对每个stage的特征做了通道降维用PCA把stage2压到128维stage3压到256维stage4压到512维。降维之后信息损失很小但计算量和显存占用都降了一个量级后面的密度估计也稳定了很多。3.3 局部密度建模patch级高斯和马氏距离这是整套方法的心脏。对训练集中的每张正常图片提取特征后我们按空间位置把每个stage的特征图切成patch。每个patch对应原图上一个局部区域这个patch的特征向量就是在该位置的局部表征。训练阶段做的事情非常朴素对每一个空间位置统计所有正常训练图片在该位置patch特征的均值向量和协方差矩阵。假如特征图大小是56×56每个位置有一个特征向量那么我们就对每个位置拟合一个多元高斯分布。这里要特别说一下协方差矩阵的正则化因为真实数据里特征向量之间往往存在强相关性协方差矩阵很容易变成奇异矩阵求逆的时候会直接报错。我们的做法是在协方差矩阵对角线加上一个很小的正则项lambda取值范围通常是1e-4到1e-2具体数值通过验证集调。推理阶段的公式也非常直接。对于一个测试patch的特征向量x它和正常分布的马氏距离定义为 d(x) sqrt( (x - μ)^T Σ^{-1} (x - μ) )这个距离和平常的欧氏距离最大的区别在于它考虑了特征不同维度之间的相关性以及尺度差异。形象点说欧氏距离是“地图上直线量距离”马氏距离是“根据山路弯道修正过的实际路程”。在一个特征维度波动很大、另一个维度波动很小的分布里如果你想判断一个点是不是离群必须按各个方向的“可波动范围”做归一化马氏距离就是干这个的。下面是训练和推理的伪代码逻辑import numpy as np def fit_distribution(train_feats): # train_feats: dict {position: list_of_vectors} means {} covs {} for pos, feats in train_feats.items(): arr np.stack(feats) # [N, D] means[pos] arr.mean(axis0) cov np.cov(arr, rowvarFalse) # 对角正则化防止奇异矩阵 cov np.eye(arr.shape[1]) * 1e-3 covs[pos] cov return means, covs def compute_anomaly_map(image_feats, means, covs): # image_feats: dict {position: vector} H, W len(means), len(means[0]) score_map np.zeros((H, W)) for i in range(H): for j in range(W): x image_feats[(i, j)] mu means[(i, j)] cov covs[(i, j)] diff x - mu score_map[i, j] diff np.linalg.inv(cov) diff.T return score_map实际工程里我们不会在推理时用np.linalg.inv去求逆而是训练时先对协方差矩阵做Cholesky分解存下来推理时直接解三角方程能快好几倍。对于工业场景的在线检测这一步的快慢会直接影响到能不能跑进节拍约束。3.4 热图生成、平滑与分数融合拿到每个stage的patch级异常得分矩阵之后下一步是把多尺度得分融合成一个最终的像素级热图。我们的做法是先把每个stage的得分矩阵各自上采样到448×448然后按权重做加权平均。stage2的权重给小一点因为它过于敏感会把一些正常的纹理波动放大stage4的权重大一点因为它的语义判别力最强stage3居中。融合后的热图还有一个关键的平滑操作3×3或5×5的高斯滤波。为什么要平滑因为patch级得分矩阵即使上采样回来在缺陷边缘也会出现很多毛刺和孤立噪点高斯滤波可以显著提升像素级AUROC。我们试过sigma取2到4之间效果最好再大就会把细小的缺陷抹掉。图片级全局得分的生成有两种思路第一种是统计热图上的最大得分第二种是取高分patch的99分位数。最大得分的问题是很不稳定一个孤立噪点就可能把分数顶上天造成正常图片的假阳性。我们最终用的是99分位数这样既保留了对真实缺陷的敏感度又不会被单个噪点带偏。多尺度融合的权重、滤波sigma、分数聚合方式这三个超参数我们都是用一个小的验证集去搜索的搜索量不大但对最终两个指标的提升非常明显。4. 拿第一过程中踩过的坑与排查实录4.1 协方差矩阵求逆不稳定这个坑我们一进去就踩了。第一次跑通训练流程后推理阶段在几十张图片上就报LinAlgError: Singular matrix。原因是很多patch位置的特征在训练集里几乎不变协方差矩阵极小某些特征维度之间的相关性又把数值拉到了机器精度以下最后矩阵变成奇异的。排查思路很简单打印每个位置协方差矩阵的条件数发现确实是大量位置条件数无穷大。解决办法就是对角正则化上面提到的lambda参数。我们最终在代码里写成了cov np.eye(D) * lambda_valuelambda值分类别调整纹理类用1e-3物件类用1e-4因为物件类背景干净协方差估计更稳定不需要那么强的正则。这里给个重要提示lambda并不是越大越好。调大正则确实能让求逆稳定但会把马氏距离中“按分布方差归一化”这个核心能力削弱距离退化成接近欧氏距离异常判别的质量会明显下降。所以遇到求逆报错时不要只想着猛怼正则先去分析是不是某些特征维度本身就是常值可以考虑把常值维度直接删掉。4.2 显存和特征存储爆掉一开始我们用全尺寸1024×1024跑并且把每张图的每个patch特征都保存在内存里用于训练阶段建模15类数据全部加载进来直接吃了快60GB内存。实验机器虽然不差但这种做法完全不可持续迭代一次要等很久。后来做了两个优化。第一只保存聚合后的均值与协方差不在内存里缓存所有patch特征训练阶段分batch提取并在线更新二阶统计量。第二把输入缩放尺寸从1024降到448单个stage特征图大小只有原来的五分之一左右显存和计算量同时降下来。这两个优化之后整个训练流程从“要把服务器跑满”变成“一张消费级显卡也能跑”实验迭代速度翻了好几倍。4.3 纹理类上的误报与漏报MVTec AD里最难啃的不是瓶盖、晶体管这类物件类反而是皮革、织物、木材这类纹理类。物件类背景干净缺陷和背景差异大模型很容易判断纹理类的正常区域本身就包含大量不规则的纹理变化如果把这些变化当成异常误报率会高得离谱。像素级AUROC在纹理类上的下降尤其明显热图经常是一片红绿混杂的噪声。我们针对这个问题做了三件事。第一是增加邻域上下文信息对每个patch特征不只取当前点而是取周围3×3邻域的patch特征做一次融合让模型能区分“局部纹理渐变”和“真正的异常突变”。第二是多尺度融合时给浅层特征更小的权重减少浅层对纹理噪声的放大。第三是高斯滤波参数在纹理类上单独加大sigma到4左右实测误报减少非常明显。这是一个值得记住的经验纹理类问题靠单点特征几乎无解必须引入邻域信息和上下文。你没法只看一个像素判断它是不是在正常纹理上但看它周围一圈区域的统计特征就能明显降低误判。4.4 输入分辨率带来的指标波动我们做了一组对照实验分别用256×256、384×384、448×448、512×512作为输入尺寸。结果很有意思256尺寸下图片级AUROC和像素级AUROC都明显偏低细小划痕根本提不出有效特征升到384之后图片级指标已经接近最优但像素级指标要上到448才开始变好再往上到512像素级指标提升已经非常有限计算量却涨了一大截。最终我们选用448×448作为所有实验的默认输入这也是一个“性价比最高”的选择。如果你只想先跑通流程完全可以从256开始但如果是要刷榜单或者落地产线建议直接用448以上避免后期因为输入尺寸问题重新调参。我给当时排查过程整理了一张速查表方便大家遇到类似问题时对照问题现象可能原因解决建议协方差矩阵奇异patch特征维度过高或存在常值维度对角正则化、删除常值维度、PCA降维训练显存爆掉输入分辨率过高特征图太多降低输入尺寸、分batch提取、在线更新统计量纹理类误报严重单点特征缺乏邻域上下文邻域特征融合、浅层权重调低、加大高斯滤波sigma细小缺陷漏检输入分辨率不够提升到448以上多尺度融合时保留浅层细节图片级得分抖动直接使用最大得分改用99分位数聚合降低孤立噪点影响5. 从比赛到落地的一点建议5.1 离线评测与现场实验是两码事说到这我想聊聊从比赛到产线落地之间那道看不见的坎。MVTec AD的数据拍摄环境很稳定光照基本一致物体姿态也相对固定。到了真正的产线上光照变化、振动模糊、油污遮挡、背景反光各种干扰都会让正常样本的特征分布发生偏移原本在数据集上表现很好的模型可能上线第一天就出现大量误报。我的建议是无论离线指标多好都要在产线部署前做一个“影子模式”验证就是模型不参与实际决策只在后台同步跑和现有的传统检测结果做对比收集至少一周的真实数据再评估指标。这一步相当于给模型上一个“试用期”能过滤掉大量在离线评测中看不出来的问题。工业现场的第二个特点是对可解释性要求很高。产线负责人不会只看一个total score他会问为什么这批工件被判异常缺陷在哪个具体位置面积大概多大所以落地时不要把模型输出一个分数就结束要把像素级热图保存下来配合连通域分析提取缺陷区域的外接矩形和面积生成可视化的质检报告。这在很多场景下甚至比AUROC指标本身更重要。5.2 非深度学习的后处理手段一样重要比赛阶段我们几乎把所有精力都花在了神经网络和特征建模上。但到后来做产线项目才发现传统图像处理手段在工业场景里非常能打而且和深度学习方法配合起来效果出乎意料地好。比如我们用形态学开运算把热图上的小噪点腐蚀掉再用闭运算把断裂的缺陷连通成块再配合面积过滤误报能降一个档次。又比如用连通域分析提取缺陷候选区域之后对每个区域单独计算一个聚合得分远比全局热图最大值判断稳定。这些手段在论文里往往不被重视但工程上极其有效。5.3 复现实验时的工程化习惯最后分享一个工程层面的心得。像这种涉及多阶段、多尺度、多数据集的实验如果不做严格的工程化管理复现结果是相当痛苦的。我们比赛期间吃过亏某一天灵机一动改了一个预处理细节之后几天实验指标都下不来了但找不到原因。后来我们强制自己做三件事。第一所有代码用Git管理每次实验跑之前先commit记录代码版本。第二固定随机种子虽然我们的方法几乎不涉及随机性但特征降维、训练集划分等环节仍然可能有随机因素固定种子让每次结果完全一致。第三离线缓存特征提取好的特征保存到磁盘后续调参只需要重新读特征做密度估计和推理单次实验从十几分钟压缩到几秒极大加快了迭代速度。回看这次ECCV 2020的冲刺我个人最大的体会是双料王从来不是靠某一个模型结构挣来的而是把问题定义、数据协议、特征设计、密度建模、后处理、工程效率这几环全部打通之后水到渠成的结果。如果你也想在自己项目上复现类似效果我建议先从MVTec AD的单个类别入手把这个类别的图片级和像素级指标都刷到稳定再去铺开15类。工业视觉这条路上没有银弹但只要把正常样本的特征分布学扎实了遇到没见过的缺陷你也能第一时间把它揪出来。