工业视觉这一个行当里泡了几年我发现一个特别拧巴的现象产线上跑得最顺的质检方案往往不是算法最精妙的而是数据准备最省心的。原因很简单工业缺陷样本这东西真的太金贵了。良品满产线都是随手一抓一大把可缺陷件呢要么是生产调试期攒下来的零星几个要么是客户投诉退回来的残次品数量少、种类杂不说有时候几个月都等不来一批顺眼的。偏偏传统的深度学习质检方案多半是拿缺陷样本做有监督的分类或分割训练样本不够模型就没法收敛于是项目周期里最长的往往不是调算法而是等缺陷样本。PatchCore这种基于正常样本特征记忆的无监督异常检测方法就是冲着这个死结来的。它的思路很直接不问“缺陷长什么样”只问“和正常相比哪里不对劲”。模型的训练只吃正常样本推理的时候把测试图像和记忆中的正常特征做比对差异大的地方就是异常区域。这意味着产线冷启动阶段不用再苦哈哈地收集缺陷样本良品图拍一拨就能把基线跑起来。这篇文章我想把这套方法从原理到落地包括我实际踩过的坑完整地讲一遍适合正在给产线做视觉项目、又被缺陷数据愁到头秃的朋友。1. 为什么工业质检不能只依赖“缺陷样本”先聊点实在的。任何一个在产线上做过质检项目的人都不难说出缺陷样本这条路的几个老大难问题。理解了这些问题才能明白为什么PatchCore这类无监督方法在工业场景里这么受关注。1.1 三个让人头疼的现实问题第一个问题是数量。缺陷样本真的很少尤其是在稳定的量产线上。产线稳定运行的时候良率可能做到98%、99%你想收集那些真正有代表性的缺陷得靠长时间的人工目检、客户投诉反馈、甚至专门开停机去制造故障来获取。我见过一个项目做手机中框的划伤检测客户要求模型识别七八种划伤类型结果项目启动了两个月标注好的缺陷图才凑了三百来张其中还有一半是重复度极高的相似样本。第二个问题是种类不可穷尽。工业缺陷这事儿字典里没有“封闭集”这个词。同样是划伤在不同光照下、不同角度下、不同材质上表现千差万别。今天收集到的缺陷明天可能又出现新变种。有监督模型学的都是“见过的缺陷”一旦出现没见过的异常很容易漏检。这就是为什么很多质检项目上线初期效果不错半年后漏检率开始悄悄上升——产线上出现了新的、没见过的缺陷形态。第三个问题是数据复用性差。这个行业的项目大多是定制化的A产线的模型搬到B产线基本不work。换了产品型号、换了相机角度、换了光源缺陷分布就全变了之前积累的缺陷样本几乎作废一切又得重新来。长此以往每个项目都要重复经历漫长的数据收集期人力成本和时间成本都很可观。1.2 传统方案的三个死结以前在工业质检里最主流的方案是有监督的图像分类或者语义分割。听上去没什么问题实际跑起来却有三个绕不过去的死结。第一个死结是标注成本。语义分割是把每个像素都标出来缺陷区域哪怕只占图像面积的1%标注员也得一笔一笔地勾勒。一张缺陷图标注下来快的几分钟慢的十几分钟。几百张图就是几天的工作量还不算验证和纠错的时间。第二个死结是误检率和漏检率难以平衡。有监督模型训练时为了把缺陷类和非缺陷类分开往往会学一个比较“自信”的决策边界。但是这个边界在真实产线上经常出问题——光照一波动、产品角度一偏正常的图像就可能被误判为缺陷导致误检率飙高。要压误检率就得调高判定阈值一调阈值漏检率又开始上升。这个平衡在传统方案里只能反复试错非常让人头疼。第三个死结是换型。工业现场说换型就换型早上做A型号晚上可能就要切到B型号。每次换型有监督模型都需要对应的缺陷样本做微调不然精度就只能归零。而PatchCore这类方法换型的时候只需要重新采集一批正常样本做特征记忆库采集成本低到很多现场负责人可以接受——这就是“冷启动”在实际项目里最直接的吸引力。2. PatchCore的原理让模型知道“正常长什么样”PatchCore这个名字拆开来看就是“Patch”加“Core”核心思想是“以图块的视角记住正常样本的模样”。它不学分类不学分割只做一件事把正常样本的特征密集地保存下来形成一个“正常特征记忆库”然后靠比对距离来判断异常。2.1 从图像到Patch模型到底在看什么要理解PatchCore先要理解它怎么“看”一张图。它不是一个端到端训练出来的网络而是借用了预训练的图像特征提取模型通常用在大规模自然图像数据集上预训练好的ResNet50或者WideResNet50。这里有个关键点这个预训练模型在PatchCore里是完全冻结的、不作为训练参数的它只是被当作一个“特征提取器”来用。为什么一个在通用图像上训练的模型能用在工业缺陷检测上这背后的逻辑是预训练CNN虽然没见过你的产品但它已经学会了非常丰富的底层视觉特征比如边缘、纹理、颜色渐变、局部形状。这些特征具有很强的通用性就像一个人的眼睛已经学会了感知颜色和形状换了新场景以后依然能分辨出这块区域跟周围是不是“长得一样”。工业上的很多表面缺陷本质上就是局部纹理或颜色的突变而预训练模型恰好擅长捕捉这些突变。具体做法是把一张图输入网络取中间某几个卷积层的输出特征图。比如说输入是224x224的图经过ResNet50的第2、3层之后特征图的空间尺寸可能变成56x56和28x28。每一个空间位置上的特征向量就对应原图上一个感受野区域这就是一个“Patch”的特征表示。PatchCore会把这些特征从多个层里抽出来在通道维度上拼接在一起形成每个位置的“局部描述子”。这里描述子携带的信息既包含高层语义又包含低层纹理合在一起就能比较全面地表达这个局部区域长什么样。有些朋友会问为什么不直接用整张图的特征做比对那样计算量小很多。但问题在于整张图的特征太粗糙了划伤、压痕、脏污这些缺陷往往只有几个像素到几十个像素整图比对会把它们彻底淹没掉。而Patch级别的特征保留了几十像素尺度的空间分辨率既能定位缺陷在哪又能敏感地感知局部异常这是PatchCore能在工业场景立足的根基。2.2 记忆库与核心集采样把“正常”压缩成精华正常样本的特征如果全部存下来量是非常恐怖的。举个例子一张224x224的图在ResNet50中提取的Patch特征数量可能是上万甚至更多。一条产线拍数百张正常图特征总量可能达到几百万个特征向量。如果全部塞进内存推理时不仅浪费算力检索速度也会被拖垮。这里就需要核心集采样。PatchCore的做法是使用一种贪心算法从所有正常Patch特征中选出一个规模小得多、但代表性很强的子集用这个子集作为记忆库。这个算法的名字有点学术叫“贪心核心集选择”通俗地讲就是在正常特征的分布空间里尽量均匀地选出一批“地标”。这些地标能让后续每个待测的Patch都能在记忆库中找到足够近的邻居。这个“局部敏感”的采样策略有一个朴素而有效的动机如果只在特征空间里随机选取那些高频出现的正常模式会占据大量名额而边缘的、少见的正常模式可能一个都选不中。一旦测试时遇到这种少见的正常模式最近邻距离就会偏大系统会把它误判成缺陷。贪心采样要的是“覆盖”不是“代表”它宁可牺牲一部分重复模式的精度也要保证异常检测的空间上没有明显的盲区。这个阶段在工程实践上有一个显著的好处——它是非参数化的不需要梯度下降也不需要调学习率。采样过程简单说来就是先把全部特征读进来然后迭代地选择“与已选集合最远”的特征点加入记忆库直到记忆库达到预设规模或者覆盖率达到要求。这个思路和“最远点采样”很像逻辑直白效率也高。我用一个生活化的类比帮你理解一下你手里有一千张城市街景照片想从中挑二十张放在相册里让别人看了就能大致了解这座城市都有哪些景观。如果随机挑可能挑出一堆商业街照片。如果每次挑一张和已选照片差异最大、风格最独特的最后这本相册就能覆盖城市的各个角落。核心集采样就是这个挑照片的过程。指标上是这样算的假设原始特征集合是F记忆库C初始为空每一步遍历F中所有剩余特征计算它和C中所有特征的最近距离然后选那个最近距离最大的特征加入C循环下去。这个过程看似复杂但工程上可以用分块计算和向量化做好加速实际耗时完全可以接受。2.3 检测与定位为什么最近邻距离有效推理阶段是整个流程里最简单的部分。把测试图送进提取器得到一整张图的Patch特征。对每个Patch特征在记忆库里找到与它最相似欧氏距离最近的那个正常Patch记录下这个最近距离。显然距离越近说明这个局部区域和记忆中常见的正常模式越像距离越远就说明这个区域在正常样本里找不到“相似参照物”那它大概率就是异常区域。整张图像的异常分数取所有Patch最近邻距离的最大值。这个设计的直觉是一个缺陷只要存在哪怕只在一个Patch范围内表现明显它的最近邻距离就会显著拉大成为整张图得分的最大值。这个得分就是判断“这张图有没有缺陷”的依据。做定位的时候把每个Patch的距离从小到大归一化映射成一张热力图再上采样到原图尺寸就能看到缺陷出现在哪个位置。有人可能会问为什么不用平均距离或者中位数距离实际原因是工业缺陷往往是小面积的如果拿平均距离算几十个正常Patch会稀释掉一两个异常Patch的贡献缺陷就被隐藏了。最大值统计保留了对局部异常的敏感度这正是工业场景迫切需要的。当然最大值也带来一个副作用对噪声和光照变化更敏感这个我在后面“踩坑”的章节里会详细讲。我讲一下监控距离分布的小技巧。实际部署的时候我一般不仅看最大距离还会统计所有Patch距离的直方图。如果分布呈现出明显的“双峰”形态说明图像里确实有局部异常区域如果整体分布右移但不怎么分峰通常说明整张图的光照或色偏出了问题。这两种情况对应的处理策略完全不同前者可能真的抓到了缺陷后者需要检查成像环境而不是急着调阈值。3. 实操落地全流程从零跑通一个PatchCore质检项目接下来是动手环节。我按自己实际做过的一个模拟项目来串一遍流程项目目标是做某3C部件表面的划伤、脏污、凹坑检测要求不能漏过明显缺陷同时把误检控制在可接受范围内。整个过程不涉及真实产线信息设备和数据都是通用的公开格式重点是把方法路径讲明白。3.1 环境准备与依赖说明第一个建议是别在Windows上硬刚直接用带NVIDIA GPU的Linux环境推荐Ubuntu 20.04或22.04。PyTorch版本建议2.0以上CUDA 11.8或12.1都行关键是跟显卡驱动匹配。需要装的包大概是这些pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install timm opencv-python scipy scikit-learn tqdmtimm这个库是用来加载预训练模型的它里面封装了PyTorch官方没有的一些模型结构包括WideResNet50。PatchCore最常见的基础网络就是WideResNet50_2这里我建议直接用timm里在ImageNet上预训练好的权重不要自己从头训练一个特征提取器效果差距会很明显。显卡方面实际跑下来一张12GB显存的卡比较舒服因为需要加载预训练模型、算特征、做采样显存太小会频繁爆内存。如果没有GPUCPU也能跑只是特征提取和核心集采样的耗时可能会到数小时做实验还行上线就比较吃力了。3.2 数据集准备目录结构决定项目效率PatchCore训练只需要正常样本但评测的时候还是需要一个包含缺陷样本的测试集不然你没法知道模型到底行不行。这里我推荐一个公开的工业视觉异常检测数据集的标准目录结构它也是很多论文通用的格式直接照着建就行data/ bottle/ train/ good/ good_00001.png good_00002.png ... test/ good/ good_00001.png ... broken_large/ broken_00001.png ... broken_small/ broken_00001.png ... ground_truth/ broken_large/ broken_00001_mask.png ...注意几个细节。train目录下只有good一类里面全部是正常样本test目录下既有good文件夹也有各个缺陷类型的文件夹ground_truth里放的是缺陷分割掩码用于评估像素级定位精度。这个结构做项目时也推荐照搬因为不管后续换哪个算法目录都能复用。数据集规模上我的经验是正常样本不要少于100张多一些更好300张以上效果会明显更稳。太少的话特征记忆库的覆盖不够容易被误判。测试集里最好包含若干正常样本以便算误检率不然你很难判断模型是否调得过于敏感。3.3 特征提取与记忆库构建的核心代码构建记忆库的流程核心是特征提取加核心集采样。我给出的是简化版本你完全可以直接复制做原型验证。import torch import timm import torch.nn.functional as F from tqdm import tqdm import numpy as np # 加载预训练模型 model timm.create_model(wide_resnet50_2, pretrainedTrue, features_onlyTrue) model.eval() model.cuda() # 提取层选择 output_layers [2, 3] # 对应中间两层特征 def extract_features(image_paths, model, output_layers, image_size224): features [] for path in tqdm(image_paths): img load_and_preprocess(path, image_size) # 自行实现返回BCHW tensor with torch.no_grad(): feats model(img.cuda()) # 逐层处理 for layer_idx in output_layers: feat feats[layer_idx] # 将特征图转换成 patch 级别的向量 b, c, h, w feat.shape feat feat.permute(0, 2, 3, 1).reshape(-1, c) # L2归一化 feat F.normalize(feat, dim1) features.append(feat.cpu()) return torch.cat(features, dim0) # 核心集采样 def greedy_coreset(features, budget): # features: N x D 的特征矩阵 N, D features.shape if N budget: return features device features.device # 随机选一个起点 indices [np.random.randint(N)] selected features[indices] # 用分块距离加速 for _ in tqdm(range(budget - 1)): # 计算剩余特征到已选集合的最小距离 dist torch.cdist(features, selected) min_dist dist.min(dim1).values # 每个点到已选集合的最短距离 idx min_dist.argmax().item() indices.append(idx) selected torch.cat([selected, features[idx].unsqueeze(0)], dim0) return selected这版代码能跑通但性能不是最优的。工业落地时我强烈建议做两个优化一是用分块策略做cdist因为全量cdist在特征是几十万维的时候内存会爆炸二是使用近似最远点采样的加速实现比如用随机投影树做候选集筛选速度能提升一个量级。原型验证阶段可以直接用朴素版本但心里要清楚这一步是大规模部署的性能瓶颈节点。代码里还有一个细节为什么输入图像要缩放到224x224因为预训练模型的输入尺寸就是224直接用原始尺寸会破坏输入分布影响特征提取的稳定性。如果你的产品纹路非常精细原始图分辨率远大于224x224我建议不要硬缩而是用滑窗切割成多个224x224的小块分别提取最后把特征在空域上拼回去这样既能保留细节又不破坏模型输入约束。3.4 推理打分与阈值标定推理的打分逻辑在代码上非常简单def infer_score(image_path, model, memory_bank, output_layers, image_size224): img load_and_preprocess(image_path, image_size) with torch.no_grad(): feats model(img.cuda()) patch_scores [] for layer_idx in output_layers: feat feats[layer_idx] b, c, h, w feat.shape feat feat.permute(0, 2, 3, 1).reshape(-1, c) feat F.normalize(feat, dim1) # 计算到记忆库的最近邻距离 dist torch.cdist(feat.cpu(), memory_bank) min_dist, _ dist.min(dim1) patch_scores.append(min_dist) # 聚合多层的分数这里用最大值作为图像得分 # 实际中会根据层调整权重或者先上采样再取最大值 return max(patch_scores).numpy().max(), patch_scores阈值的标定是整个流程里最考验经验的一环。我的做法是在训练好的模型上跑一批正常的验证图记录所有正常图的图像得分画一个分布直方图然后用均值 3倍标准差作为初始阈值再在这个基础上结合测试集的误检率微调。不要一上来就追求零误检那样阈值拉太高漏检率会飙升现场客户更不满意。一个比较稳的做法是设两个阈值一个“警告阈值”一个“报警阈值”。得分落在警告阈值和报警阈值之间的先不让产线停机而是弹出来给人工复判超过报警阈值的直接标记为缺陷。这样既能保住漏检率又不会因为偶尔的误检导致产线频繁停机现场接受度会高很多。3.5 上线部署时的性能优化PatchCore推理最大的痛点是每张测试图的所有Patch都要和记忆库里所有的特征向量做一次距离计算。如果记忆库有5万个特征测试图有5000个Patch那就是2.5亿次距离计算虽然批量矩阵运算能扛住但耗时会很难看。部署时的优化手段我比较推荐三个方向第一用faiss代替裸的torch.cdist。faiss是一个高效的相似度检索库构建一个IndexFlatIP或IndexIVFFlat把记忆库的特征塞进去查询的时候直接批量检索最近邻比裸算快好几倍。我做过的项目里这一项优化通常能把单张推理时间从1秒多压到100毫秒级别。第二做embedding的降维比如用PCA把特征从1536维压到256维再建索引。这个操作会损失一点精度但特征维度降下来之后检索速度显著提升且内存占用大幅下降。降维比例和精度损失的权衡需要在自己数据集上实测确定。第三做类目级建模。很多工业项目一个模型对应一个产品型号型号之间差异巨大但同型号内特征相对一致。这时可以按型号分别构建记忆库推理时先路由到对应型号的记忆库。这比把所有型号的特征塞进一个记忆库效果更好因为每个记忆库的特征分布更集中误检率更低。4. 部署中踩过的坑与排查技巧这一部分是我最想分享的因为论文里只会告诉你方法有多好不会告诉你现场有多会“掉链子”。以下都是我亲手试错试出来的。4.1 光照变化是最隐蔽的“缺陷”PatchCore对光照极其敏感。第一次在现场跑的时候我用了实验室里很稳定的光源效果不错。结果上线之后白天窗户有人经过、车间里另一条产线开灯关灯误检率立马飙升。道理并不复杂PatchCore比对的是“和正常特征的最近距离”光照一变整个Patch的特征可能整体偏移距离就变大了模型不会区分这是真实缺陷还是光照变化。而且由于它用的是“最大距离”做图像得分局部光影一波动就可能触发报警。应对方法有几种优先级从高到低是物理层面尽量使用封闭光源减少环境光干扰这是最有效的手段。图像层面做光照归一化预处理比如直方图均衡化、灰度归一化能缓解一部分问题。模型层面在做特征提取前尝试对输入图像做标准化或者训练时对正常样本做数据增强适度改变亮度、对比度增强模型对正常光照波动的容忍度。不要指望一个模型解决所有成像问题成像不稳定再好的算法都会被噪声淹没。这是我反复强调的一条经验。4.2 误检高发时先查“数据边界”而不是阈值如果你发现模型的误检率一直压不下去先别急着调阈值。我碰到的很多情况误检都来源于训练数据和测试数据之间存在分布偏移比如拍照角度、产品批次、表面反光特性发生了变化。这些偏移在肉眼看来“差不多”但在特征空间里可能就是天壤之别。处理思路是把误检图单独拉出来逐个看它们到底是被哪些Patch拉高得分的然后把这个Patch区域对应到原图上。如果发现在正常产品上也存在类似纹理说明模型记忆库对这类纹理的覆盖不够需要往训练集里补充更多包含该类纹理的正常样本。如果发现是偶发环境干扰就需要回到成像端做调整。这个排错流程的核心是养成“先定位到Patch再回溯到图像”的习惯。只看整图得分你是看不出名堂的一定要把Patch级别的高分位置可视化出来很多问题才能一目了然。4.3 换型维护与模型刷新周期PatchCore的训练成本很低因为不需要反向传播只需要对正常样本做一次前向提取加核心集采样。这个特性让模型刷新变得非常便宜。现场的常规做法是每次批量正常件生产后自动抽取一小部分图像加入特征记忆库候选项定期用新样本更新记忆库。这样能不断适应产线上缓慢的渐进式变化比如光源轻微老化、材料批次颜色波动。有人想过要不要把旧的特征全部保留我的建议是不要。随着时间推移一些旧的正常样本可能已经不能代表当前产线状态了硬留着会扩大正常模式的覆盖范围导致真正的新缺陷也被“解释成正常”。更合理的做法是维护一个滑动窗口只保留最近N批产品的特征超过窗口时间的自动淘汰这样记忆库能始终贴合当前生产状态。4.4 一个容易被忽略的细节特征层的选择PatchCore的精度跟选哪些层的特征关系很大。太浅的层只抓到颜色和边缘对细碎的纹理划伤比较敏感但会把正常的纹理变化误判为异常太深的层抓到的语义太抽象分辨率也低细微缺陷容易被磨平。实践中我通常选择第2层和第3层组合使用既保留分辨率又兼顾语义信息。如果你的产品表面纹理很干净可以尝试只选第1层和第2层如果产品表面纹理本身就很复杂可以考虑加深层在抑制纹理噪声和检测真实缺陷之间找到平衡。5. 结果评估与适用边界别把 PatchCore 当成万能钥匙这一节我想把PatchCore的技术边界说透。它确实解决了冷启动问题但也不是什么场景都能硬套。下面是我做过的对比测试和场景复盘供参考。5.1 与常见方案的效果对比我在一个模拟的表面缺陷检测数据上做了对比实验数据包含划伤、脏污、凹坑、氧化等多种缺陷类型训练集只允许使用正常样本PatchCore类方法或正常缺陷样本有监督方法。下表是我实测的典型表现具体数值会因数据不同有浮动但趋势是稳定的。方案训练数据要求图像级AUC像素级定位推理耗时换型成本有监督分类CNN需要大量缺陷样本较高但对新缺陷无效不支持低高需重新采集缺陷有监督分割UNet需要像素级标注最高但依赖标注质量强低很高标注成本大PatchCore仅正常样本高约0.95以上中等偏上中优化后可接受低重新采集正常样本即可传统阈值/频域方法需要人工调参不稳定弱低中依赖特征设计从表里能看出来PatchCore最大的优势不在绝对精度而在于训练数据门槛和换型成本。在缺陷样本极度稀缺的场景下它的实用价值是最高的。如果你的缺陷样本已经积累得很丰富且有充足的标注预算有监督分割方案的上限还是会更高一些这一点不用回避。5.2 什么场景适合什么场景别硬上适合用PatchCore的场景有一个共同特点正常样本容易获取、相对稳定缺陷形态多样且难以穷举。典型如金属表面的划伤、注塑件的缺料飞边、焊点表面的氧化、3C外壳的脏污和轻微形变这些场景里正常样本拍几百张成本极低而缺陷类型五花八门硬写有监督模型等于大海捞针。不适合的场景也有两类。第一类是正常样本本身高度多变的场景比如布料印花、木材纹理、陶瓷釉面这些产品表面天然存在随机纹理正常模式的覆盖范围太大PatchCore会把正常的纹理波动误判为缺陷。第二类是缺陷非常细微、与正常差异极小的场景比如半导体的暗缺陷检测PatchCore的特征分辨率可能不够此时需要更高分辨率的成像系统和针对性的特征提取器仅靠通用预训练模型已经不够用了。另外一个边界是PatchCore擅长“找不一样”但它不告诉你“不一样是什么类别”。实际项目中经常需要把缺陷分门别类比如划伤归一类、脏污归一类这个信息PatchCore给不了需要额外的分类器或者人工复判流程去补充。6. 写在最后的几点体会真要说起来PatchCore不是算法世界里最“聪明”的模型它没有复杂的训练策略没有精巧的网络结构核心原理一页纸就能讲清楚。但真正在工业现场摸爬滚打过的人会明白它最大的价值在哪里它把“数据准备”这个环节从项目的最大瓶颈变成了只需要一个下午就能搞定的杂活。我在实际项目中感受最深的是用PatchCore做冷启动不等于一切都万事大吉。它只是把启动门槛降下来了但成像环境的稳定性、数据采集的规范性、阈值的持续监控、模型记忆库的定期刷新这些脏活累活一样都不能少。尤其是成像端我反复说过光源、角度、遮挡每一个环节出问题模型性能都会直接崩给你看。最后分享一个小经验第一次在一个新品类上跑PatchCore别急着追求最高精度先跑通“正常样本采集—特征提取—记忆库构建—推理打分”这条完整链路把基准分数记下来再迭代优化。先让链路转起来后面每一步的改进都会变得非常清晰。这个习惯帮我避开了很多“纸上谈兵”的坑希望你也能用得上。