ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PatchCore工业异常检测实战:从数据准备到99.3% AUC的完整调优路径

PatchCore工业异常检测实战:从数据准备到99.3% AUC的完整调优路径

做缺陷检测的朋友应该都遇到过这种场景:项目拿到了,客户要求检测精度拉到99%以上,但手里只有一个类别几百张正常样本,缺陷图片掰着手指头数得过来。我前段时间处理某条产线的金属零件表面检测项目,正卡在这个坎上。试过几版方案,都不太满意,后来把PatchCore放到Anomalib 2.2.0框架里跑了一轮,图像级AUC直接冲到了99.3%,像素级AUC也有96.8%。这篇就把我的完整实践过程拆开讲,包括数据准备、参数调试、踩坑记录和部署建议,希望能帮到正在做类似异常检测项目的同行。

1. 为什么工业异常检测我最后选了PatchCore

1.1 传统方案在真实产线上的三个痛点

先说背景。大部分工业质检场景和学术公开数据集有个本质区别:缺陷样本极度稀缺。客户可能只能提供几十张划痕、压伤样本,而且缺陷形态五花八门,一个类别几个样本,压根不够训练一个分类网络。更麻烦的是,缺陷不是静态的——今天这批料表面是黑点,明天可能就变成亮斑,靠固定缺陷类别去监督训练,等新缺陷形态出现时模型直接失效。

我早期试过基于重建的方案,比如自编码器和生成对抗网络。理论上这些方法只需要正常样本参与训练,推理时看重建误差判定异常。但实际调试时,重建模型经常把细微缺陷也“脑补”成正常纹理,尤其是金属表面的拉丝纹、喷砂颗粒这类与缺陷高度相似的背景,误报率高得离谱。后来还试过传统图像处理方案,光照一变化,算法阈值就得重新标定,让现场工程师每天维护参数,根本不现实。

1.2 PatchCore的核心思路:把“记忆”变成“标尺”

PatchCore属于基于特征记忆库的异常检测方法。训练阶段,它只使用正常样本,通过预训练网络提取特征,构建一个“正常特征库”。推理阶段,把测试样本的特征与这个特征库做最近邻距离比较,距离越大,异常分数越高。

这个方法最妙的地方是它不学“什么是缺陷”,只学“什么是正常”。新缺陷形态完全不需要收集,只要与正常状态差异大,就会被高距离捕获。在处理我那个金属零件项目时,这就是决定性优势——客户在验收前一周又提供了几个新缺陷样本,换监督学习方案已经来不及做迁移学习了,但PatchCore根本不受影响。

PatchCore具体做了三件事:

  • 特征分层聚合:不像某些方法只取最后一层卷积特征,PatchCore会从不同分辨率的特征层提取信息,这样既能保留大范围结构信息,又能捕捉细微纹理差异。
  • 局部邻域聚合:把特征图上某个点周围邻域的信息合并进当前特征,防止空间位置上的信息丢失。
  • Coreset贪心采样:正常样本特征数量动辄几十万,全存下来推理时要算几十万次距离,太慢了。Coreset采样用贪心策略从特征库中选出一个有代表性的子集,既压缩了存储,又保留了大部分信息。

第三点是它在工程上能落地的关键。我记得第一次跑了完整特征库,两万个特征patch,推理一张图用了快三秒,完全没法用。加上Coreset采样,采样比例压到10%,推理速度提升了一个数量级,精度几乎没有下降。

1.3 Anomalib 2.2.0让我少写三千行代码

算法模型我看完论文后本来想自己复现,但无意间看到了Anomalib这个开源框架,当前用的是2.2.0版本。它把数据加载、模型定义、训练流程、评估可视化全封装好了,最友好的是支持命令行方式直接跑训练:

anomalib fit --model Patchcore --data MVTec_AD

基于Anomalib的分层设计,我只需要把精力集中在数据处理和参数配置上,不需要自己实现特征提取、得分归一化那一堆工程细节。尤其是评估和可视化部分,它自带热力图生成,对向客户汇报异常定位结果非常有帮助。

2. 自定义数据集的准备:目录、格式与数据增强

2.1 MVTec风格目录结构是硬约定

Anomalib不少数据模块按MVTec数据集的目录结构识别数据,自定义数据最好也往这个结构上靠,能省去很多麻烦。我这边最终整理的目录长这样:

dataset/ ├── train/ │ └── good/ │ ├── norm_001.png │ ├── norm_002.png │ └── ... ├── test/ │ ├── good/ │ │ ├── norm_101.png │ │ └── ... │ └── defect/ │ ├── def_201.png │ ├── def_202.png │ └── ... └── ground_truth/ └── defect/ ├── def_201_mask.png └── ...

这里有几个细节需要说明:

  • train/good:只放正常样本,这是PatchCore训练的唯一数据源。
  • test/good:测试时的正常样本,用于评估误报率。
  • test/defect:测试时的缺陷样本,标注为异常。每张异常图最好配一张同名的二值掩膜图放在ground_truth/defect下,这样既能评估图像级AUC,也能评估像素级AUC,客户对定位效果更认可。

如果你只有正常样本,没有缺陷样本,也能训练,只是无法量化评估性能。所以有条件的话,哪怕收集几十张历史缺陷图片也要把测试集凑出来。

2.2 背景多样性和光照变化不能忽视

我的第一版训练数据只有600张正常样本,都是在车间固定工位拍的,背景高度一致。结果在客户现场试跑时,发现换了工位后误报率飙升。原因很简单:PatchCore把没见过的“背景”也当成异常了。

解决办法是主动制造“变化”。我对正常样本做了数据增强,包括平移、旋转、亮度变化、模糊模拟,让模型从有限的正常样本中学习到更宽的分布范围。对于金属零件这类反光严重的物体,尤其建议增加亮度和对比度扰动,模拟不同角度光照下拍摄的画面。注意不要做翻转增强,产线上零件的拍摄方向通常是固定的,过强的增强反而会让模型把真正的位置异常也当成正常。

2.3 分辨率取舍与推理窗口设计

PatchCore对输入分辨率比较敏感。分辨率太小时,细小划痕、针孔这类缺陷在特征图里可能只剩几个像素,几乎无法被捕捉;分辨率太大,显存占用成倍增加,推理耗时也完全接受不了。

我在项目里对比了256x256和512x512两种输入,结果如下:

输入分辨率图像级AUC像素级AUC单张推理耗时峰值显存
256×25696.1%88.7%0.31s2.8GB
512×51299.3%96.8%0.87s7.6GB

可以看出,检测精度提升非常明显,但推理耗时也涨了近三倍。实际落地时,如果工件表面很大,一个零件拍一张照片不够,我建议把大图划分成多个重叠窗口分别推理,再把得分图拼回去。这样做还有个好处:窗口变小,显存压力也更可控。

3. 训练配置的关键参数:从默认值到调优

3.1 backbone选型:为什么WideResNet-50是首选

PatchCore论文里的主力backbone是WideResNet-50,它与普通ResNet-50相比,每层通道数扩大了几倍,特征图的宽度更大,能编码更多纹理细节。在金属零件这类纹理密集的数据集上,通道数不足会直接导致缺陷特征与正常特征混在一起。

我在Anomalib里实践时,保持官方推荐的wide_resnet_50作为backbone。如果你显存有限,也可以换resnet_18,但精度会明显下降,尤其对于微小缺陷基本不可用。Anomalib 2.2.0的PatchCore实现中,特征提取模块默认使用WideResNet-50在ImageNet上的预训练权重。刚开始我怀疑通用预训练模型在工业图像上效果会打折扣,但实测下来,ImageNet预训练特征在多种工业异常检测基准上表现依然出色,这可能是早期视觉特征具有通用性的缘故。

3.2 coreset采样比例与特征尺寸的平衡

PatchCore提取出的特征数量是图像patch数量的数倍。举个例子,输入512×512图像,经过backbone后multi-scale聚合特征patch大概有几百个;再加上600张训练图,总特征数可达几十万。这种情况下直接算最近邻距离,单张推理需要做几十万次距离运算,单靠CPU推理根本无法接受。Coreset采样比例在这里是核心旋钮。

Anomalib的config.yaml里对应配置如下:

model: class_path: anomalib.models.Patchcore init_args: backbone: wide_resnet_50 layers: - layer1 - layer2 - layer3 pre_trained: true coreset_sampling_ratio: 0.1 neighbors: 9

我对不同采样比例做了对比:

coreset比例特征库大小图像级AUC推理耗时
1.0(不采样)~58万99.4%2.4s
0.1~5.8万99.3%0.87s
0.01~580097.8%0.45s

0.1是论文推荐值,也是我在项目里最终采用的。它把特征库压缩到原来的十分之一,图像级AUC基本不掉,推理时间从2.4秒降到了0.87秒。如果你的场景对速度要求极高,可以考虑0.05,再往下就损失过大了。

neighbors=9表示在特征库中搜索9个最近邻,取这些距离的某种统计量作为异常得分。调大这个值会略微增加鲁棒性,但计算量也增大,我在项目上没有改动这个参数。

3.3 训练流程与评估指标解读

PatchCore的训练过程没有传统意义上的梯度下降,它做的事情可以理解为“特征记忆”:

  • 加载预训练backbone,冻结权重,不对其做梯度更新。
  • 对训练集中的正常样本提取特征,构建记忆库。
  • 通过Coreset采样压缩记忆库,得到最终的稀疏特征集。
  • 推理时对测试样本提取同样特征,计算与记忆库的距离,得到得分。

Anomalib 2.2.0将这一过程也封装成了常规的训练评估循环,使用Lightning的接口,方便记录日志和回调。正式评估之前,一定要确认评估指标的含义。Anomalib的评估默认输出图像级AUROC和像素级AUROC,这两个指标对最终验收很关键。图像级AUROC衡量的是“整张图是否异常”的排序能力,像素级AUROC衡量的是“缺陷位置是否被高亮”的定位能力。客户通常关心前者,但验收时后者往往是加分项。

4. 从94%到99.3%的调优路径

4.1 第一轮效果:图像级AUC只有94%

第一次跑通流程我非常兴奋,用默认配置训练,数据集是加工后的600张正常样本,测试集是100张正常图片加80张缺陷图片,缺陷类型有划痕、凹坑、黑点、异物。默认设置下图像级AUC只有94.2%,和网上看到的99%差距明显。

我把检测结果翻出来逐类分析,发现AUROC不高的原因主要集中在一个地方:划痕类缺陷大量漏检。划痕面积小、对比度低,有的甚至和零件本身拉丝纹理看起来非常像。另一个原因是样本太少,特征库无法覆盖足够的正常纹理变化,微小的局部偏差就被淹没了。

4.2 找瓶颈:先看热力图再动手

与其漫无目的地调参数,我建议先用Anomalib自带可视化工具把几个漏检样本的热力图导出来看。结果发现,漏检样本的异常分数都不是零,只是没有超过当前阈值。这说明模型其实感觉到了“不对劲”,只是分数偏低。问题很可能出在分辨率不足或特征粒度不够,而不是模型完全失效。

确认方向后,我把输入分辨率从256×256提到512×512,并把coreset采样比例从0.1保持到0.1,但改进了数据增强,尤其是增加了局部随机遮挡和亮度扰动。这里插一句:局部随机遮挡是个相当好用的技巧,能让模型对某个区域的高光、遮挡不那么敏感,间接减少背景因素的干扰。

4.3 阈值校准:AUROC高不等于实际可用

在调优过程中,我一度只盯AUROC,后来发现这是不够的。AUROC衡量的是排序质量,但客户端要求的是“给一个判定阈值”。Anomalib支持通过F1 adaptive threshold在验证集上自动搜索最优阈值,我一开始用它,效果不错,但把阈值放回现场时发现实际误报率比预期高一些。

原因在于客户对误报的容忍度和缺陷检出率有偏向性,他们宁可多判几单复检,也不愿意放走一个缺陷。于是我没有用F1最大的阈值,而是对thickness分数做了百分位校准:在正常验证集上选择一个足够高的分数阈值,使正常样本误报率控制在0.5%以内。这一步是检测精度99%的另一个关键——不要只看排序指标,要针对业务需求调阈值。

4.4 最终参数组合与实测结果

最终定下来的配置组合如下:

  • backbone:WideResNet-50,ImageNet预训练
  • 输入分辨率:512×512
  • 数据增强:随机平移、亮度抖动、对比度调整、局部遮挡
  • coreset采样比例:0.1
  • neighbors:9
  • 阈值策略:正常样本误报率0.5%对应的分位数阈值

最终在自制测试集上:

指标数值
图像级AUROC99.3%
像素级AUROC96.8%
正常样本误报率0.6%
缺陷检出率98.8%

这里的“缺陷检出率”是按固定阈值计算的,而不是AUROC。对客户来说,这个数字更直观:80张缺陷图里,漏掉了1张。那一张漏检的缺陷确实很隐蔽,人工肉眼都要反复确认才能发现,但对于要求99%以上精度的项目,我后续计划通过针对性采集此类缺陷样本、加入伪缺陷生成来进一步兜底。

5. 生产落地:踩坑清单与推理加速

5.1 显存溢出:coreset变小不是唯一解法

特征库构建好之后,推理阶段最吃显存的是“测试特征与记忆库特征的距离矩阵”计算。512×512分辨率下,一张测试图约产生几百个特征patch,而记忆库有几个万特征。计算全量距离矩阵时,如果一次并行的推理批量太大,显存会被打爆。

我遇到的第一次OOM是推理时batch_size设成了8,直接爆了。建议推理时把batch_size调小,或者把得分计算拆成循环分批处理。Anomalib内置的推理接口对这个问题处理得不错,但如果你自定义了特征处理流程,一定要关注这个距离矩阵的形状。

5.2 重复纹理印刷品的误报处理

如果你处理的是塑料包装、印刷品这类周期性强纹理,实验时经常出现误报判断。PatchCore原本的工作机制认为“所有没见过的纹理都是异常”,但如果正常纹理本身就是周期性很强的条纹或网格,小偏移也会造成误报。

我的建议是:对这类数据加强训练集中的纹理对齐。采集正常样本时尽量让对象姿态一致,减少旋转造成的伪变化。还可以在后处理阶段加一个简单的形态学滤波,把面积小于特定像素的异常区域过滤掉,这在真实场景下很有效。

5.3 推理速度优化:ONNX导出与特征缓存

PatchCore在Anomalib 2.2.0里支持导出模型用于部署,但我实际操作时发现,比导出模型更有效的做法是“分阶段缓存”。

对于固定相机位姿的产线来说,正常样本的特征库可以在设备开机时一次性加载到内存,推理时只需要跑backbone提取一张图的特征,再做距离计算。这两个步骤瓶颈都在backbone的卷积计算上,因此用GPU推理是很必要的。如果目标工控机没有GPU,可以考虑把backbone导出为ONNX并用CPU优化模式推理,特征提取速度能提升40%左右,距离计算部分用向量化库加速,实测整体推理可以跑到1.2秒以内。我的经验是先用patch core模型本身已经提取的特征做一次缓存,等确认效果稳定后再花时间做ONNX导出,避免反复返工。

5.4 与产线集成时容易被忽视的样本一致性

我在这类项目里最常碰到的问题是现场相机角度、焦距、曝光参数和训练时不一致。PatchCore对图像分布偏移非常敏感,如果现场拍出来的正常图像整体偏亮或偏暗,误报率会直线上升。最稳妥的流程是部署后用现场采集的正常样本来验证阈值,如果误报率偏高,先排查图像采集条件,再做阈值调整,而不是回头改模型。

5.5 版本升级的迁移小提示

如果你是之前用过Anomalib 1.x版本的老用户,升级到2.2.0后会发现API变化挺大。旧的--train_mode等参数已经被新的Anomalib CLI范式替代,配置文件也变得统一起来。我的建议是直接以2.2.0的官方文档为准,不要保留旧版本的习惯。官方repo里的examples目录基本覆盖了从训练到导出的全流程,照着跑一遍再改动,比自己猜API省事得多。

6. 一些实际体会与后续扩展思路

这个项目跑完之后,我最大的体会是:PatchCore的“零缺陷训练”思路非常适合产线初期缺陷样本不足的阶段,但它不是一劳永逸的。它需要与数据清洗、阈值标定、特征采样策略配合,才拿得出99%以上的可靠检测精度。

后续如果要进一步提升,我准备尝试几个方向:一是把不同相机、不同工位的正常样本都纳入训练库,让记忆库覆盖更广;二是对比用相同特征库跑FastFlow等生成式模型,看是否能互补检测盲区;三是研究小样本下缺陷样本的伪标注策略,利用半监督方式对漏检案例做在线反馈。还有一点很实际的小建议:项目验收时,记得把设置阈值用的代码和配置文件一起交付,免得现场工程师拿到模型却不知道判定的边界在哪里。这是工程落地里最容易被忽略、却最影响效果的一环。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进