工业异常检测和普通图像分类有一个很大的区别:训练阶段通常很容易获得大量正常样本,但异常样本往往稀少、种类未知,而且很难覆盖未来可能出现的所有缺陷。
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∗=argminMC⊆Mmaxm∈Mminn∈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=mins∈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∗=argmaxidi 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=minm∈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∗=maxidi 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 AUROC、Pixel-wise AUROC 和 PRO。

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 越难在正常特征库中找到相似邻居,它就越可能是异常。