PatchCore:论文和工程代码细节详细解读

工业异常检测和普通图像分类有一个很大的区别:训练阶段通常很容易获得大量正常样本,但异常样本往往稀少、种类未知,而且很难覆盖未来可能出现的所有缺陷。

PatchCore 采用了一条非常直接的思路:

不去学习"异常长什么样",而是尽可能完整地记录"正常长什么样"。

训练阶段,PatchCore 使用 ImageNet 预训练网络提取正常图像的局部 Patch 特征,并将这些特征保存到 Memory Bank 中;测试阶段,再将测试图像的 Patch 特征与正常 Memory Bank 做最近邻匹配。

如果某个测试 Patch 在正常特征库中找不到足够相似的特征,那么它就更可能是异常。

整个过程可以概括为:

复制代码
正常图像
   ↓
Pretrained Encoder
   ↓
中间层 Feature Map
   ↓
Locally-aware Patch Feature
   ↓
多层特征对齐与融合
   ↓
正常 Patch Memory Bank
   ↓
Greedy Coreset Subsampling
   ↓
Compact Memory Bank
   ↓
────────────────────────
测试图像
   ↓
提取相同的 Patch Feature
   ↓
Nearest Neighbour Search
   ↓
Patch 到正常 Memory Bank 的最近距离
   ↓
├── Image-level Anomaly Score
└── Pixel-level Anomaly Segmentation

论文 Figure 2 展示的就是这一完整流程:正常样本首先被转换为 neighbourhood-aware patch-level features,构成 Memory Bank,然后通过 greedy coreset 减少冗余;测试阶段则使用最近邻搜索同时完成异常检测和异常定位。


1. 为什么使用中间层特征

PatchCore 使用 ImageNet 预训练 CNN 作为特征提取器,但并不直接使用网络最后一层的 Feature Map。

原因在于不同网络层提取的信息不同:

  • 浅层 Feature:空间分辨率高,包含大量纹理、边缘等细节信息;
  • 深层 Feature:感受野更大、语义更强,但空间分辨率降低,同时更偏向 ImageNet 分类语义;
  • 中间层 Feature:在局部细节、上下文信息和空间分辨率之间取得较好的平衡。

论文默认使用 ResNet 类网络的中间层,例如 block 2 和 block 3。

结合官方实现,以 WideResNet50、输入图像大小 224×224 为例:

复制代码
layer2:[B, 512, 28, 28]

layer3:[B, 1024, 14, 14]

其中:

  • B:Batch Size;
  • 512 / 1024:Feature Channel;
  • 28×28 / 14×14:Feature Map 的空间尺寸。

PatchCore 接下来并不是直接使用这些 Feature Map,而是进一步构造 Locally-aware Patch Feature


2. Locally-aware Patch Feature

2.1 为什么不直接使用单个空间位置

假设 layer2 的 Feature Map 为:

复制代码
[B, 512, 28, 28]

那么某一个空间位置 (h,w) 对应:

复制代码
[512]

也就是一个 512 维特征向量。

如果直接使用这个位置作为 Patch Feature,它所包含的局部上下文仍然有限,并且对轻微的空间位移比较敏感。

PatchCore 因此在每一个空间位置附近取一个局部邻域。

默认:

复制代码
patchsize = 3

即对每一个位置取一个 3×3 邻域。

论文将这个过程定义为 neighbourhood aggregation,并使用 adaptive average pooling 构造 locally-aware representation。


2.2 从数据维度理解 Patchify

以 layer2 为例:

复制代码
输入 Feature Map:

[B, 512, 28, 28]

对每一个 28×28 空间位置提取 3×3 邻域:

复制代码
[B, 512, 28, 28]

        ↓ 3×3 Patchify

[B, 28×28, 512, 3, 3]

        ↓

[B, 784, 512, 3, 3]

这里:

复制代码
784 = 28 × 28

所以一张图仍然对应 784 个空间位置,只不过原来每个位置是:

复制代码
[512]

现在变成:

复制代码
[512, 3, 3]

也就是说,每个位置对应的 Feature 不再只包含中心点本身,而是包含周围 3×3 的局部上下文。

关键作用: Local Awareness扩大的是每个 Patch 所包含的局部信息,而不是减少 Feature Map 的空间分辨率。

在 PatchCore 默认 stride=1 的情况下,Patchify 之后 Patch 网格仍然保持原来的空间尺寸。

因此:

复制代码
28×28
↓
仍然是 28×28 个 Patch

而不是变成26×26

同理,layer3:

复制代码
[B, 1024, 14, 14]

        ↓ 3×3 Patchify

[B, 14×14, 1024, 3, 3]

        ↓

[B, 196, 1024, 3, 3]

因此:

复制代码
layer2:784 个 Patch

layer3:196 个 Patch

论文也明确指出,该局部聚合会在所有 (h,w) 位置执行,因此能够保留 Feature Map 的空间分辨率。


3. layer2 和 layer3 如何融合

PatchCore 不只使用一个 Block,而是联合使用两个相邻的中间层。

论文中的基本思想是:

复制代码
layer2 Patch Feature
        +
layer3 Patch Feature
        ↓
对齐空间位置
        ↓
构造最终 Patch Representation

由于两个 Block 的空间分辨率不同,因此首先需要进行空间对齐。论文明确说明,会将较深层 Feature 双线性缩放到较浅层、也就是更高分辨率的 Patch 网格上

结合官方代码,可以通过完整的数据维度变化来理解这一过程。


3.1 两个 Block 分别 Patchify

layer2:

复制代码
[B, 512, 28, 28]

        ↓ 3×3 Patchify

[B, 784, 512, 3, 3]

layer3:

复制代码
[B, 1024, 14, 14]

        ↓ 3×3 Patchify

[B, 196, 1024, 3, 3]

此时两层 Patch 数量不同:

复制代码
layer2:28×28 = 784

layer3:14×14 = 196

因此不能直接进行融合。

3.2 layer3 空间对齐到 layer2

PatchCore 以高分辨率的 layer2 Patch 网格为基准,将 layer3:

复制代码
14×14

通过双线性插值对齐到:

复制代码
28×28

因此:

复制代码
layer3:

[B, 196, 1024, 3, 3]

        ↓ Spatial Alignment

[B, 784, 1024, 3, 3]

此时:

复制代码
layer2:[B, 784, 512, 3, 3]

layer3:[B, 784, 1024, 3, 3]

两层现在都拥有 784 个 Patch。

第 i 个 layer2 Patch 和第 i 个 layer3 Patch 可以理解为对应原图中的大致相同位置。


3.3 各层 Patch 独立转换到固定维度

官方实现接下来会把每个 Patch 展平成一维向量,然后使用 AdaptiveAvgPool1d 将它压缩到统一的 1024维

layer2

每个 Patch:

复制代码
[512, 3, 3]

展开:

复制代码
512 × 3 × 3 = 4608

于是:

复制代码
[B×784, 4608]

        ↓ AdaptiveAvgPool1d

[B×784, 1024]

得到:

复制代码
f2:[B×784, 1024]

layer3

每个 Patch:

复制代码
[1024, 3, 3]

展开:

复制代码
1024 × 3 × 3 = 9216

于是:

复制代码
[B×784, 9216]

        ↓ AdaptiveAvgPool1d

[B×784, 1024]

得到:

复制代码
f3:[B×784, 1024]

这里的 AdaptiveAvgPool1d 不包含可学习参数,它只是把展平后的 Patch Feature 按固定输出长度进行平均池化。


3.4 多层特征拼接

此时,对应空间位置具有两个 1024 维 Feature:

复制代码
layer2:1024维

layer3:1024维

官方实现不是执行:

复制代码
f2 + f3

而是执行:

t 复制代码
Concat(f2, f3)

因此:

复制代码
[B×784, 1024]
          +
[B×784, 1024]

        ↓ Concat

[B×784, 2048]

然后再进行一次:

复制代码
AdaptiveAvgPool1d

将:

复制代码
2048 → 1024

最终:

复制代码
[B×784, 1024]

也就是说,一张 224×224 图像最终得到:

复制代码
784 个 Patch

每个 Patch 1024 维

整个 Patch Feature 构建过程可以总结为:

复制代码
layer2
[B, 512, 28, 28]
      ↓
3×3 Patchify
      ↓
[B, 784, 512, 3, 3]
      ↓
Flatten + AdaptiveAvgPool1d
      ↓
[B×784, 1024]
             \
              \
               → Concat → 2048 → AdaptiveAvgPool1d → 1024
              /
             /
layer3
[B, 1024, 14, 14]
      ↓
3×3 Patchify
      ↓
[B, 196, 1024, 3, 3]
      ↓
空间对齐至 28×28
      ↓
[B, 784, 1024, 3, 3]
      ↓
Flatten + AdaptiveAvgPool1d
      ↓
[B×784, 1024]

最终得到的 1024维 Patch Embedding,就是 PatchCore 后续构建正常 Memory Bank 的基本单位。


4. Memory Bank 与 Greedy Coreset

4.1 Memory Bank:记录正常 Patch

训练阶段只使用正常图像。

每张 224×224 图像产生:

复制代码
[784, 1024]

假设训练集包含 N 张正常图片,那么所有 Patch 合并后得到:

复制代码
[N×784, 1024]

这些正常 Patch Feature 共同构成:

复制代码
Memory Bank M

可以把 Memory Bank 理解成:

一个记录正常产品各种局部结构和纹理模式的特征库。

测试时只需要判断:

当前测试 Patch 能不能在这个正常库中找到一个足够相似的 Patch?

但是完整 Memory Bank 往往非常大,而且大量 Patch 特征彼此非常相似。

所以 PatchCore 接下来使用 Greedy Coreset Subsampling 对 Memory Bank 进行压缩。


4.2 Coreset 的目标

假设完整 Memory Bank:

复制代码
M:[N, 1024]

如果希望只保留 10%:

复制代码
N → 0.1N

随机选择虽然简单,但可能会出现:

复制代码
某些常见区域被重复保留很多次

某些数量较少但合法的正常模式被完全丢失

因此 PatchCore 希望选择一组更具有空间覆盖能力的 Patch。

其目标可以写成:

MC∗=arg⁡min⁡MC⊆Mmax⁡m∈Mmin⁡n∈MC∥m−n∥2 M_C^*=\arg\min_{M_C\subseteq M}\max_{m\in M}\min_{n\in M_C}\left\lVert m-n\right\rVert_2 MC∗=argMC⊆Mminm∈Mmaxn∈MCmin∥m−n∥2

直观理解就是:

maxmin的作用是每个 Patch 先找离自己最近的代表,然后再从所有 Patch 中找"最近距离最大"的那个。

所以 Greedy 下一步就选那个最远的点,说明覆盖情况不太好,argmin是在所有可能的 Coreset 方案里,选一个方案,让"覆盖得最差的那个 Patch"的距离也尽可能小。

因此 Coreset 关注的是 正常特征空间覆盖率,而不是随机保留数据。


4.3 随机投影降低选择成本

PatchCore 的最终 Patch Feature 为 1024 维。

为了降低 Coreset Selection 时的距离计算成本,官方实现首先使用随机线性映射:

复制代码
[N, 1024]

     ↓ Random Projection

[N, 128]

论文使用 Johnson-Lindenstrauss 随机投影思想,希望降维后仍大致保持 Feature 之间的距离关系。

这里要注意:

128维 Feature 只用于决定"选择哪些 Patch",最终 Memory Bank 保存的仍然是原始1024维 Feature。

因此:

复制代码
原始 Feature
[N,1024]
     │
     ├──────────────────────────┐
     ↓                          │
随机投影                       │
[N,128]                        │
     ↓                          │
Greedy Coreset                  │
     ↓                          │
Selected Indices               │
     │                          │
     └────────────→ 原始 Feature
                                ↓
                            [K,1024]

4.4 Greedy Coreset 怎么选择

假设当前已经选择了一组 Coreset:S

对于 Memory Bank 中每一个 Feature xᵢ,计算它到当前 Coreset 最近 Feature 的距离:

di=min⁡s∈S∥xi−s∥2 d_i=\min_{s\in S}\left\lVert x_i-s\right\rVert_2 di=s∈Smin∥xi−s∥2

如果 dᵢ 很小:

复制代码
说明 Coreset 中已经存在和 xᵢ 很相似的 Feature

如果 dᵢ 很大:

复制代码
说明当前 Coreset 还没有很好地覆盖 xᵢ

因此下一轮选择:

i∗=arg⁡max⁡idi i^*=\arg\max_i d_i i∗=argimaxdi

其中 argmax 表示:

找到最大距离对应的 Feature Index。

也就是说,每一次都挑选:

当前最没有被已有 Coreset 覆盖的那个 Patch。

新 Patch 加入后,最近距离更新为:

dinew=min⁡(diold,∥xi−xnew∥2) d_i^{\mathrm{new}}=\min\left(d_i^{\mathrm{old}},\left\lVert x_i-x_{\mathrm{new}}\right\rVert_2\right) dinew=min(diold,∥xi−xnew∥2)

然后继续:

复制代码
选择最大最近距离
        ↓
加入 Coreset
        ↓
更新所有 Feature 的最近距离
        ↓
再次选择最大距离
        ↓
...

最终得到一个数量更少、但能够较好覆盖正常 Feature Space 的 Memory Bank。


5. 测试阶段:Nearest Neighbour Anomaly Detection

测试图像会经过与训练阶段完全相同的 Patch Feature 构建过程:

复制代码
Test Image
   ↓
Pretrained Encoder
   ↓
layer2 + layer3
   ↓
Locally-aware Patch Feature
   ↓
空间对齐与融合
   ↓
[784, 1024]

对于每一个测试 Patch qᵢ,在正常 Memory Bank 中寻找最近邻:

di=min⁡m∈M∥qi−m∥2 d_i=\min_{m\in M}\left\lVert q_i-m\right\rVert_2 di=m∈Mmin∥qi−m∥2

其中:

  • qᵢ:测试 Patch Feature;
  • m:正常 Memory Bank 中的 Patch Feature;
  • dᵢ:该 Patch 的异常距离。

如果:dᵢ 很小,说明正常 Memory Bank 中存在非常类似的局部结构,因此该 Patch 更可能正常。

如果:dᵢ 很大,说明在正常 Feature Space 中找不到相似 Patch,因此异常可能性更高。

所以 PatchCore 最核心的异常定义就是:

测试 Patch 到正常 Feature Space 的最近邻距离。


5.1 Image-level Anomaly Score

一张图会产生很多 Patch:

复制代码
q₁, q₂, ..., q₇₈₄

对应异常距离:

复制代码
d₁, d₂, ..., d₇₈₄

工业缺陷通常具有一个特点:

即使只有一个局部区域异常,整个产品也应该被判为异常。

因此最基本的图像异常分数来自异常程度最大的 Patch:

s∗=max⁡idi s^*=\max_i d_i s∗=imaxdi

论文实际还会根据该异常 Patch 对应的 Memory Bank 邻域,对最终 Image-level Score 进行一次重加权,但是当前官方代码默认实现直接使用 Patch 最近邻分数的最大值生成 image-level score


5.2 Pixel-level Anomaly Segmentation

由于每一个 Patch 都拥有一个异常分数:

复制代码
d₁, d₂, ..., d₇₈₄

同时这些 Patch 对应原 Feature Map 中的:

复制代码
28×28

空间位置,因此可以将这些分数重新排列为:

复制代码
[28, 28]

的 Anomaly Map。

然后:

复制代码
Patch Anomaly Score
        ↓
28×28 Anomaly Map
        ↓
Bilinear Upsampling
        ↓
恢复到输入图像大小
        ↓
Gaussian Smoothing
        ↓
最终异常热力图

论文采用双线性插值恢复空间分辨率,并对结果进行 Gaussian smoothing。

因此 PatchCore 不需要额外训练一个 Segmentation Decoder。

Image-level Detection 和 Pixel-level Localization 实际上都来源于同一组 Patch Nearest-Neighbour Distance。


6. 如何评价 PatchCore

PatchCore 同时需要评价:整张图片能不能正确判断异常 以及:异常区域能不能正确定位

因此论文主要使用 Image-level AUROCPixel-wise AUROCPRO


6.1 Image-level AUROC

模型会为每张图片输出一个连续的 Anomaly Score。

选择不同阈值,会得到不同的:

复制代码
TPR:真正率

FPR:假正率

其中:

TPR=TPTP+FN \mathrm{TPR}=\frac{\mathrm{TP}}{\mathrm{TP}+\mathrm{FN}} TPR=TP+FNTP

FPR=FPFP+TN \mathrm{FPR}=\frac{\mathrm{FP}}{\mathrm{FP}+\mathrm{TN}} FPR=FP+TNFP

随着阈值不断变化,以 FPR 为横轴、TPR 为纵轴,可以得到 ROC Curve

ROC 曲线下面的面积就是:AUROC

AUROC 越接近 1,说明模型越容易让:

复制代码
异常图像得到较高分数

正常图像得到较低分数

6.2 Pixel-wise AUROC

Pixel-wise AUROC 的计算方式和 Image-level AUROC 相同。

区别只是分类对象从一张图变成一个像素

每一个像素都有自己的 Anomaly Score,通过不断改变阈值,可以得到像素级 ROC Curve。

因此:

Pixel-wise AUROC 衡量的是异常像素和正常像素在分数上的区分能力。


6.3 PRO / AUPRO

Pixel-wise AUROC 会受到异常区域面积的影响。

例如:

复制代码
大缺陷:5000个像素

小缺陷:20个像素

大缺陷会在像素统计中占据更大的权重。

因此 PRO 更关注:

每一个独立异常区域有多少比例被检测出来。

对于第 k 个真实异常区域 Cₖ:

PROk=∣P∩Ck∣∣Ck∣ \mathrm{PRO}_k=\frac{\left|P\cap C_k\right|}{\left|C_k\right|} PROk=∣Ck∣∣P∩Ck∣

其中 P 表示当前阈值下预测出的异常区域。

然后对所有异常区域等权平均:

PRO=1K∑k=1KPROk \mathrm{PRO}=\frac{1}{K}\sum_{k=1}^{K}\mathrm{PRO}_k PRO=K1k=1∑KPROk

这样一个很大的缺陷区域 和一个很小的划痕区域在区域统计中具有相同权重。

实际评估时会不断改变阈值,得到一系列:

复制代码
FPR
+
PRO

然后形成 PRO-FPR Curve,再计算曲线面积,即通常所说的 AUPRO

因此:

  • Image-level AUROC:整张图是否异常;
  • Pixel-wise AUROC:像素是否异常;
  • PRO / AUPRO:每个真实缺陷区域是否被完整定位。

7. 总结

PatchCore 的核心并不是训练一个新的异常分类网络,而是构建一个能够充分描述正常样本的 Memory Bank

训练过程可以总结为:

复制代码
Normal Image
   ↓
Pretrained Encoder
   ↓
layer2 / layer3
   ↓
3×3 Locally-aware Patch
   ↓
空间对齐
   ↓
多层 Feature 融合
   ↓
每个 Patch → 1024维 Embedding
   ↓
所有正常 Patch
   ↓
完整 Memory Bank
   ↓
Random Projection
   ↓
Greedy Coreset Selection
   ↓
Compact Memory Bank

测试阶段:

复制代码
Test Image
   ↓
相同的 Patch Feature Extraction
   ↓
Test Patch Embedding
   ↓
Nearest Neighbour Search
   ↓
到正常 Memory Bank 的最近距离
   ↓
├── Image-level Anomaly Score
└── Pixel-level Anomaly Map

总结:

PatchCore 不直接学习"异常是什么",而是通过预训练网络构建一个覆盖充分且紧凑的正常 Patch 特征库;测试 Patch 越难在正常特征库中找到相似邻居,它就越可能是异常。

相关推荐
leoZ23114 小时前
Vue3 还原一个企业级后台-08-API注册管理
目标检测·机器学习·分类·状态模式·迁移学习·集成学习·figma
欧特克_Glodon17 小时前
OpenCV计算机视觉开发入门与实践<十一>:矩阵的复制和矩形类Rect
c++·opencv·计算机视觉·矩阵
深圳雨林凯AI19 小时前
无缝平铺的图像处理原理:边缘匹配、重复单元与密度控制|雨林凯AI 四方连图
图像处理·人工智能·计算机视觉
AndrewHZ1 天前
图像处理入门008 | 阶段总结:环境测试与基础概念测验
图像处理·python·opencv·计算机视觉·cv
江畔柳前堤1 天前
AgentScope 设计与原理全解:从消息原语到分布式智能体工程底座
大数据·人工智能·分布式·目标检测·机器学习·语言模型·架构
jay神1 天前
深度学习为什么需要反向传播
人工智能·深度学习·yolo·目标检测·cnn·毕业设计
汐ya~1 天前
【PageEyes Agent 跨端 UI 自动化】开源Pydantic AI、OmniParser 多源感知的UI 自动化Agent
计算机视觉·agent·ui自动化·github开源
动物园猫2 天前
猪危险行为目标检测数据集:3类别、5,000+张图像 | 目标检测
人工智能·目标检测·计算机视觉
嘶哈哈哈2 天前
使用 Labelme 标注遥感目标检测数据集:从类别表、矩形框到 group_id 完整流程
人工智能·目标检测·目标跟踪