把 PatchCore 跑在 Anomalib v2.2.0 上,用完全自定义的工业数据集把图像级 AUROC 干到 99% 以上,这件事我最近真的干成了。说“干成”不是玄学吹嘘,而是整个流程走下来之后,我发现很多关键动作其实是可以复现的,难的部分往往不是你想象中那些“高大上的模型结构”,而是对数据、目录、参数和上下文的理解。
我手头这个项目是产线某个部件的表面缺陷检测,异常类型包括划伤、压伤、脏污和一小部分缺料。整套可用样本大概只有一千多张正常图,异常图更少,单类缺陷类别最多不到两百张。这种数据量如果直接扔给分类网络训,很容易过拟合到背景和光照上;而 PatchCore 的思路正好不需要学“异常长什么样”,它只记住正常长什么样,推理时拿特征距离说事。配合 Anomalib 的开箱流程,确实能省掉很多造轮子的时间。
下面我会按一个完整的实战路径来复盘:从任务拆解、数据整理、原理与关键参数,到实际训练、精度调优,以及最后我在真实数据上踩过的一堆坑。内容尽量写得直接,能抄作业的地方绝不绕弯子。
1. 项目拆解与算法选型思路
1.1 先把任务定义清楚:图像级判异,不是像素级分割
很多刚接触异常检测的朋友上来就问“能不能定位到缺陷像素”,但实际产线需求通常只有一句话:这一张图到底要不要判不良。我一样是被迫先想清楚这件事,才接着往下走。
这个项目最核心的指标是图像级的分类效果,即区分“OK”和“NG”两个大类。至于缺陷具体在哪个坐标,是后续人工复判环节的事。所以我在 Anomalib 里把任务模式设成 classification,而不是 segmentation。这样训练流程更轻,不需要准备像素级标注 mask,数据准备成本瞬间降下来。
如果你是缺陷定位需求,那就要老老实实为异常样本标注 mask,并且任务模式改为 segmentation。但如果你像我一样,手头异常图数量少、没有标注工具,先做图像级判异是最理性的方案,也是 PatchCore 最能发挥优势的场景。
1.2 为什么不是分类网络,也不是重构类算法
我最早试过直接用 ResNet 的预训练模型做二分类微调,效果并不稳定。原因不是模型不行,而是分类网络天然把“正常样本”当成一个类别来学,大量正常图之间高度相似,网络很容易偷懒去拟合背景中的无关细节;异常类别数量又极少,类别不平衡很严重,训练过程稍有不慎就把所有异常都判成正常。
我也短暂考虑过基于重构的算法,比如自编码器或生成模型。这类算法的逻辑是“正常样本能重构好,异常样本重构误差大”,听起来很合理,但真放到产线数据上,很多正常纹理本身也有光照变化和细微波动,重构误差经常忽大忽小,阈值很难稳定。PatchCore 实际上是另一种思路:它不去学“正常如何生成”,而是把正常样本的深层特征以核心集形式保存下来,推理时直接寻找最近邻的距离。
距离远就是异常,距离近就是正常。这个逻辑朴素,但极其适合小样本和类别不均衡的场景。
1.3 PatchCore 和同类算法的速览对比
Anomalib 里集成了很多主流算法,我只把几个最有代表性的拉出来对比,方便你选型:
| 算法 | 原理核心 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| PatchCore | 预训练特征 + 核心集记忆 + 最近邻检索 | 精度高、训练快、无需异常样 | 特征库占内存 | 小样本、类别不均衡 |
| PaDiM | 预训练特征 + 多元高斯分布建模 | 内存随特征维度增长可控 | 对特征分布假设敏感 | 纹理类表面检查 |
| STFPM | 教师-学生特征对齐 | 推理快、内存占用小 | 精度上限略低 | 边缘部署 |
| CFLOW | 条件归一化流建模 | 表达能力强 | 训练较慢、容易过拟合小数据 | 数据量大、像素级要求高 |
我选 PatchCore,看中的是它对异常样本的“零依赖”和训练收敛速度。实际训练一个阶段只花几分钟到十几分钟,你会有大量时间去做阈值校准和数据分析,这在项目里比模型本身更值钱。
2. 环境准备与数据集工程
2.1 环境版本怎么搭配才少踩坑
Anomalib 的 v2.2.0 给我的整体体验是:接口比老版本清爽,命令行和 Python API 能顺畅混用,但依赖版本一样有讲究。
我实际使用的环境组合是这样:
- Python 3.9
- PyTorch 2.0.1 + CUDA 11.8
- Anomalib 2.2.0
- 一张 8GB 显存的显卡即可满足需求
安装 Anomalib 时我推荐从源码安装的方式:
git clone https://github.com/open-edge-platform/anomalib.git cd anomalib pip install -e .注意不要贪新上 Python 3.12,我当时试过,某些编译依赖会有兼容问题,最后还是回到 3.9 最省心。另外,Anomalib 会依赖 Lightning 框架,PyTorch Lightning 的版本建议使用较新的 2.x 版本,和 v2.2.0 配套更稳。
安装完成后可以快速跑一遍环境检查,确认有权访问预训练模型下载源,因为 PatchCore 的 backbone 需要在首次运行时下载权重文件,网络不畅会直接卡在下载这一步。
2.2 数据集目录必须按 MVTec 格式整理
Anomalib 有一套自己的数据集索引方式,最省力的是按 MVTec 的目录结构来摆放自定义数据。MVTec 的经典结构是这样的:
data/ defect_data/ train/ good/ 001.png 002.png test/ good/ 001.png 002.png defect/ 001.png这里有几个细节必须注意。
good这个类别名是约定俗成的正常类标记,不要改成normal或ok。Anomalib 会识别good目录作为正常样本,其他所有目录名都会被视为异常类别。我一开始把正常类命名成normal,结果日志里直接把它当成缺陷类处理,折腾了半天才发现是命名惹的祸。
分辨率方面,我建议把原始图像统一缩放到不超过 512 的长边,并尽量保持原始宽高比。PatchCore 在推理时是整图进网络,如果你的数据集里有大量超大分辨率图像,显存和内存都会吃不消。
2.3 数据清洗:比调参更重要的步骤
在把图片塞进train/good之前,我做了一轮比较辛苦的清洗工作。产线采回来的图像并不干净,有些图拍摄时对焦不准,有些带着明显反光,还有些连零件边缘都没完整进入画面。
这类模糊图和截断图如果混进正常样本库,等价于在记忆银行里存入了“带噪音的正常特征”,推理时会让正常区域的最近邻距离偏大,最终阈值被迫抬升,导致轻微缺陷漏检。
我的经验是用一个非常笨但有效的方法:用滑窗裁剪的方式把图像缩略图过一遍,凡是肉眼都看不清纹理的图,直接丢弃。虽然土,但对精度提升贡献非常直接。整理完的正常样本数量大约减少了百分之十,但最终检测效果反而提升了。
另外,如果有条件,尽量保证正常样本覆盖多个光照条件和多个生产批次。PatchCore 不是靠“记住缺陷”来工作,它是在用正常样本撑开一个“正常流形”,覆盖范围越广,误判越少。
3. PatchCore 核心原理与关键参数
3.1 一句话读懂 PatchCore 的运作方式
PatchCore 的思路可以用一个生活场景来类比:你不需要背下所有小偷的样子,只需要记住自己家每件物品的正常摆放位置;一旦哪天发现椅子移动了、抽屉开着,就知道出问题了。
它先拿一个在 ImageNet 上预训练好的卷积网络,对输入图片提取多尺度特征。这里不是用全连接层输出了一个全局特征,而是把中间层特征图上每一个空间位置都看作一个“局部 patch 描述子”。正常样本的全部描述子会被收入一个记忆库,这个记忆库在论文里叫 coreset。
推理阶段,一张新图会提取出同样类型的 patch 描述子,然后在新图和记忆库里找每个 patch 的最近邻距离。距离越大,代表这个位置的局部纹理越不像任何正常样本,异常分数就越高。
这种设计的巧妙之处在于,测试图像本身不需要来自训练过的类别,也不需要异常样本参与训练。你只需要保证正常样本足够有代表性,算法就能正常工作。
3.2 backbone、输入尺寸和层选择的经验值
Anomalib 的 PatchCore 配置中,最影响结果的是这几个参数:
- backbone:默认是
wide_resnet50_2,效果通常最好,但内存和耗时也更高。 - layers_to_extract:默认提取
layer1、layer2、layer3三个阶段的特征。 - input_size:默认值是 224,但可以往上调。
- coreset_sampling_ratio:默认 0.1,即只保留整个记忆库的 10% 作为核心集。
- nearest_neighbours:默认 4,计算异常分数时参考的最近邻个数。
我实际调参后发现,backbone 换成wide_resnet50_2比resnet18在图像级 AUROC 上能带来两到三个百分点的提升。虽然训练特征提取的时间变长了,但推理只涉及一次前向和检索,整体完全可接受。如果你的数据偏纹理型,resnet18可能已经足够;如果缺陷非常细微,建议优先使用wide_resnet50_2。
输入尺寸上,我把默认的 224 提升到 256,用了中心裁剪而非拉伸填充。原因是 224 在缩小时会丢失一些细划痕的纹理信息,而 256 作为中间值不会让显存压力暴增。如果你的数据本身就是 512 级别的工业图,硬生生缩到 224 其实很吃亏,建议至少保持 256 或 288。
3.3 coreset 抽样比例为什么是 0.1
很多人不理解,为什么明明存了所有正常 patch 特征,还要抽样丢掉一部分。原因是 PatchCore 的分类分数依赖于记忆库中的最近邻距离,而完整库可能包含几十万甚至上百万个 patch 特征,检索代价很高。
coreset 抽样不是随机抽,而是使用了一种贪心策略:先随机选一个特征点作为第一个核,然后每次从剩余特征中选出一个“离当前已选集合最近邻距离最大”的点加入核心集。这样做可以保证选出来的特征点在特征空间上尽量均匀覆盖原来的正常流形。
我在实践中对比过 0.1 和 0.5 两个比例,结果图像级 AUROC 提升非常有限。所以在内存可以接受的情况下,0.1 到 0.15 之间是一个性价比极高的区间。比例太高不仅没有额外收益,还会让推理显著变慢。
3.4 异常分数怎么来的
PatchCore 的最终异常分数取的是所有 patch 中最大的最近邻距离,而不是平均值。这个设计也很好理解:如果一处微小划痕在图片中间,其他区域都正常,平均距离会被拉平,最大值才能保证不放过任何局部异常。
这也提醒了你一件事:PatchCore 对“局部小缺陷”非常敏感,但对“全局模糊、过曝这类全图异常”反而可能不太灵。因为全图异常会让所有 patch 距离都偏大,分数一样会很高,但视觉解释上不如局部缺陷那么直观。
4. 训练实战:从 YAML 配置跑到 99%
4.1 一个可以直接用的 YAML 配置
Anomalib v2.2.0 支持通过 YAML 文件配置整个实验。下面是我在这个项目上最终使用的核心配置,去掉了不重要的内容,保留骨架:
model: class_path: anomalib.models.Patchcore init_args: backbone: wide_resnet50_2 layers_to_extract: - layer1 - layer2 - layer3 coreset_sampling_ratio: 0.1 nearest_neighbours: 4 data: class_path: anomalib.data.MVTec init_args: root: ./data category: defect_data image_size: 256 train_batch_size: 16 test_batch_size: 16 num_workers: 8 task: classification train_ratio: 0.8 val_ratio: 0.2 trainer: max_epochs: 1 accelerator: auto devices: 1这个配置里有个很关键的点:PatchCore 本质上不需要梯度训练,所谓“训练”只是前向提取特征并构建记忆库,所以max_epochs设成 1 完全够用。如果设大了,后面 epoch 会重复提取同样特征,纯属浪费。
train_ratio: 0.8是 Anomalib 对自定义数据集按图像比例划分 train 和 validation 的开关。注意最终指标是在test目录上评估的,与 val 无关,所以我会保证test/good和test/defect都有足够的真实新图。
4.2 跑起来看看日志怎么说
训练命令只需要一行:
anomalib fit --config ./configs/patchcore_defect.yamlFit 阶段会清空日志目录、检查数据、提取特征。典型的输出包括每个正常样本的路径和特征数量统计。你会在终端里看到类似这样的信息:
Dataset train: 872 images Memory bank: 38912 features Coreset size: 3891 features这些数字合理,说明记忆库被成功压缩到了大约十分之一。
随后模型会使用测试集进行推理并生成指标,图像级 AUROC 就是在这个阶段计算出来的。我在这套配置下的最终结果,图像级 AUROC 跑到了 99.3%,pixel AUROC 在 97.8% 左右。对产线判异来说,这个精度已经足够满足产线实时拦截需求。
4.3 精度上不去的调优路径顺序
如果你的精度没有达到预期,先别急着换模型。我总结的调优路径顺序如下:
- 先检查正常样本质量,排除模糊、过曝、截断图。
- 把输入尺寸从 224 提高到 256。
- 把 backbone 从
resnet18换成wide_resnet50_2。 - 把
coreset_sampling_ratio从 0.1 提升到 0.2 或 0.5。 - 再考虑增加正常样本的数量和多样性。
- 最后才是调整
nearest_neighbours或切换不同层组合。
这些动作按顺序做下来,通常比疯狂调学习率有用得多。因为 PatchCore 没有端到端学习的权重更新,它的全部命脉都在特征质量和记忆库覆盖度上。
我也试过把layers_to_extract加上layer4,图像级指标几乎没有变化,内存却涨了一大截。可见不是层越多越好,浅层特征负责纹理细节,中层特征负责结构,到了layer4语义太抽象,对表面缺陷检测反而帮助有限。
4.4 用 Python API 做一次性推理
如果你不想每次跑完整套 CLI,可以直接在 Python 里加载 checkpoint 做推理。Anomalib v2.2.0 的兼容性做得不错,加载方式很直接:
from anomalib.models import Patchcore model = Patchcore.load_from_checkpoint( checkpoint_path="./results/patchcore/defect_data/0/checkpoints/last.ckpt" ).eval().cuda() predictions = model.predict( image_path="./data/defect_data/test/defect/001.png" )推理返回的对象里包含了pred_score和pred_label,可以直接用分数做后续判定。我这里只强调一点:推理时图像输入要使用和训练时完全相同的预处理流程,Anomalib 内部已经封装好了,你只要不手动做额外 resize,就不会出问题。
5. 常见问题与排查技巧实录
5.1 “精度一直不动”是怎么回事
我在一开始调试时,图像级 AUROC 卡在 0.91 上下怎么都上不去,后来发现是因为正常训练目录里混入了三张带轻微划伤的图。PatchCore 会把这些带缺陷的 patch 特征也存进记忆库,导致推理时相似缺陷的最近邻距离很小,漏检率自然下不来。
这个问题非常隐蔽,因为带缺陷的图通常只有微小的纹理变化,当时从缩略图里扫过去根本没发现。我的教训是:正常样本清洗这一关再怎么强调都不过分。
5.2 显存或内存不足怎么办
PatchCore 的显著缺点是记忆库占内存比较大,尤其当正常样本数量多、输入尺寸大时。如果你在推理时出现 OOM,首选降低coreset_sampling_ratio,从 0.1 降到 0.05 通常能把内存减半。
如果显存不够,则优先降低图像输入尺寸,而不是换轻量骨干网络。因为尺寸直接影响所有层级的特征图分辨率,对显存的影响比切换 backbone 敏感得多。8GB 显存跑 256 输入完全没问题,如果到 288 就不太稳了。
5.3 阈值怎么选才能让产线误报可控
Anomalib 默认会输出 AUROC 指标,但在真实产线中,你真正需要的是一个具体的分数阈值。高于阈值判 NG,低于阈值判 OK。
我最开始直接用训练集分数的 95% 分位数作为固定阈值,结果上线试运行后误报率偏高。后来改成在验证集上计算 F1-max 阈值,也就是找到让 F1 分数最大的那个阈值点,效果明显更稳。操作方法是先收集验证集所有图像的pred_score和真实标签,然后扫一遍阈值区间,取 F1 最大的位置。
如果你的产线对漏检容忍度极低,可以在这个阈值基础上再下调一点,换取更激进的异常召回。具体下调多少没有标准答案,只能根据你对“误报人工复判成本”和“漏检风险”的权衡来定。
5.4 常见问题速查表
我把项目过程中遇到的高频问题整理成一个表,方便直接查阅:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模型把所有图都判为 NG | 正常训练集里混入异常样本 | 重新清洗正常样本目录 |
| 轻微缺陷全部漏检 | 输入尺寸过小,纹理细节丢失 | 输入尺寸升到 256 或 288 |
| 图像级 AUROC 高但产线误报多 | 阈值选取不合理 | 用验证集 F1-max 调阈值 |
| 推理很慢 | coreset 比例过高或特征库过大 | 将 coreset 比例降到 0.1 以下 |
| 训练时报“no mask found” | 数据集没有 mask 目录 | 任务模式切到 classification |
| 首轮下载权重失败 | 网络受限 | 手动下载预训练权重并放到缓存目录 |
5.5 几个值得记住的实战心得
最后补几个零碎但很关键的经验。
PatchCore 对图像缩放方式非常敏感。我建议固定使用中心缩放而不是拉伸填充,拉伸会改变局部纹理的宽高比,破坏特征的语义稳定性。Anomalib 内部默认使用中心缩放,但你如果用 OpenCV 自己预处理,就要格外小心。
不要在训练集做太多随机增强如旋转、平移和颜色抖动。PatchCore 靠的是精准的空间纹理特征,旋转和裁剪会引入不存在的局部结构,反而污染记忆库。我实测加了轻微颜色抖动影响不算大,但旋转增强会让 AURoc 掉约一个百分点。
还有一点,正常样本的批次覆盖度非常重要。如果你的产线白天和夜晚光照不同,建议把不同光照下的正常样本都整理进训练集,否则你会看到某些批次整体分数偏高,误报率在特定时段突然上升。
PatchCore 不是一个参数特别多的模型,真正决定上限的是正常样本的积累广度和特征理解深度。你对数据理解得越细,跑出来的精度就越漂亮。等你把这一套流程跑顺,再回头处理其他异常检测需求,会发现思路是通用的:先定义清楚“正常”,剩下的交给特征距离。