拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于特征嵌入的工业缺陷检测:PatchCore无监督异常检测实战

基于特征嵌入的工业缺陷检测:PatchCore无监督异常检测实战

简介:面向机器学习与图像处理方向的本科生、研究生及数据科学从业者,这是一份以PatchCore算法为核心的工业缺陷检测完整实践方案。内容基于特征嵌入方法,并与基于重构的方法进行对比,系统阐述数据预处理(填充、裁剪、归一化)、特征提取、核心集采样、最近邻搜索、异常热力图生成等关键环节。针对MVTec AD及真实工业数据集,设计了可执行的实验流程与评估体系,包括像素级AUC、样本级AUC、ROC/PR曲线绘制,以及核心集采样比例、分块大小与重叠率等参数调优分析。PDF中同时提供了评分标准、代码规范要求和扩展方向(如EfficientNet/ViT替换、缺陷分类等),有助于读者深入掌握算法原理并应用于产品质量检测项目。资源共1个PDF文件,压缩包大小约192KB,已有459人浏览学习,适合需要快速理解PatchCore实现思路或完成相关课程作业的读者参考。

1. 基于特征嵌入的工业缺陷检测:为什么传统阈值法先输一局

一条金属外壳产线,喷砂纹理在固定光源下会出现和划痕几乎一样的反光。传统方案用灰度梯度加边缘拟合做了三个月,误杀率压在3%就再也下不去。我接手后换了思路:不直接比对像素,而是把图像切成无数个小块,用预训练网络把每个块映射成高维向量,再统计“正常样本的向量在空间里分布在哪里”。缺陷样本的特征天然会远离这片区域,离得越远,缺陷越明显。这就是基于特征嵌入的工业缺陷检测,PatchCore算法是这条技术路线上最经典的一版落地实现。它只建正常样本特征库,用最近邻距离打分,不需要标注缺陷也能召回未知缺陷,适合缺陷样本稀缺、纹理复杂、缺陷类型开放的质检场景。

2. 特征嵌入的底层逻辑:PatchCore算法把图像变成了空间坐标

2.1 为什么工业缺陷检测必须换掉像素比对

传统视觉检测的核心假设是“缺陷在像素层面和正常区域有可测量的差异”。这个假设在真实产线上经常不成立。金属拉丝表面的正常纹理,在光照角度变化时会呈现和划痕几乎一样的灰度分布;透明容器的内部气泡和表面灰尘,在像素维度上都是边缘加暗区;织物表面的正常经纬纹理,本身就有周期性高频梯度。

更麻烦的是缺陷样本的收集成本。一个细分品类可能一个月才积累几十张真实缺陷图,不够训练任何有监督分类网络。而正常样本永不缺,一条产线一天就能扫几万张。所以工业界对缺陷检测算法的真实诉求从来不是“认出见过的缺陷”,而是“在没见过这种缺陷的情况下,也能把它和正常样本区分开”。

特征嵌入把这个问题换了一个问法:不去学缺陷长什么样,只去学正常样本的特征分布。做法是把一张图切成若干小区域,每个区域通过深度卷积网络映射成一个高维向量,这个向量就是该区域的嵌入特征。正常区域在高维空间里会聚成若干个致密簇,缺陷区域因为纹理、结构、亮度偏离正常分布,会落在这些簇的外围。判断一张图有没有缺陷,就变成判断它的嵌入特征离最近的正常簇有多远。

这个思路有一层关键的工程红利:特征嵌入层用的是ImageNet预训练权重,不需要在自己的产线数据上重新训练分类头。预训练网络在前几层学到的是边缘、角点、纹理基元这类通用视觉结构,这些结构在工业图像上同样成立。下游只需要做两件事:收集正常样本的特征,算距离。这两件事都不需要标注,也不需要反向传播。

2.2 嵌入空间的几何含义:正常簇与离群点

假设一张224×224的输入图,经过特征提取器后得到28×28的空间分辨率,那么这张图就会被拆成784个位置的特征向量。每个位置对应原图8×8的一个感受野区域。把一段时间的正常样本全部过一遍网络,会得到几十万到几百万条这样的向量,它们分布在一个高维空间中,通常叫特征库或记忆库。

正常样本的特征分布并不总是均匀的。光照、焦距、物料批次变化,会让正常特征形成多个子簇。比如同一工件磨砂面和亮面区域的纹理特征就分属不同的簇。缺陷检测的本质就是测量“新样本的每个局部区域特征,到特征库中最近邻正常特征的距离”。距离大,说明这个局部区域在正常样本中没有出现过结构相似的内容,大概率是缺陷。

距离本身有两种用法。做图像级检测时,取全图所有局部距离的均值或最大值,和阈值比较,输出有没有缺陷。做像素级定位时,把每个局部位置的距离图patch-map上采样回原图尺寸,得到缺陷区域的分割热图。PatchCore算法用的是k近邻距离,默认取k=4左右,这样比取最近邻距离更抗噪。

从工程视角看,这个几何解释还有一个实际好处:阈值和检测能力可以通过可视化特征空间来校准。我常用t-SNE把特征库降维到二维平面,正常样本整体呈现一团或几团,缺陷样本会散落在外围。如果画出来缺陷样本反而嵌在正常簇内部,说明特征提取器的底层特征没有捕获该缺陷的区分信息,这时换更大的主干网络或调整输入分辨率,比硬调评分阈值有效得多。

2.3 无监督检测路线选型:重建、GAN与特征库

目前工业界无监督缺陷检测有三条成熟路线,选型前值得先把账算清楚。

路线典型算法训练成本推理成本对未知缺陷的召回行业落地特点
自编码器重建AE、VAE中等,需要训练收敛低一般重建误差受纹理复杂度影响大,容易把正常纹理细节当缺陷
生成对抗重建AnoGAN、cGAN高,训练不稳定是常态高一般对分布外样本响应好,但训练玄学,工业现场难维护
特征嵌入PatchCore、SPADE极低,只需一次前向低好无训练、可增量更新,是当前最容易被产线接受的路线

重建类方法的基本假设是“正常样本能被模型很好地重建,缺陷样本重建不好”。这个假设在纹理简单场景成立,在喷砂、编织、压花这类高纹理场景经常翻车,因为正常纹理本身就存在大量高频细节,自编码器无法完全重建,重建误差一放大就把正常区域误判成缺陷。

PatchCore所属的特征嵌入路线完全绕开了重建误差问题。它不尝试生成图像,只依赖预训练网络的特征响应。推理阶段的计算主要包括一次特征提取和一次近邻检索,没有迭代优化,延迟是可控的。

选型建议是:如果产线缺陷类型固定且能持续收集样本,AE路线也有生存空间;如果缺陷类型开放、正常纹理复杂、上线节奏紧张,特征嵌入路线基本是优先解。真正的短板在后面会提到:特征库规模大、显存压力高、跨设备迁移时有精度波动。这些坑在熟悉机制之后都能解决。

3. 正常特征库与coreset采样:PatchCore的算法骨架怎么搭

3.1 特征提取:预训练Wide ResNet-50当编码器

PatchCore算法最常用的特征主干是Wide ResNet-50在ImageNet上的预训练版本。选择它而不是普通ResNet-50,是因为宽度扩展带来的感受野多样性更适合工业纹理的局部判别。timm库里有现成权重,加载时用features_only=True拿到特征金字塔输出即可。

特征层级的选择直接影响检测效果。我的习惯是取layer2和layer3输出,原因有三:layer1的步长和分辨率太高,保留了大量光照、绝对灰度信息,产线换灯管就可能误报;layer4分辨率只有7×7,空间位置信息太少,小缺陷会被平均掉;layer2加layer3的拼接既有空间细节又有语义抽象,平衡度最好。

不同层级特征拼接前要统一空间尺寸。layer2输出56×56,layer3输出28×28,做法是把layer2下采样到28×28再拼接。通道数方面,Wide ResNet-50的layer2是512通道,layer3是1024通道,拼接后每条特征向量是1536维。这个维度对后续近邻检索来说不算大,faiss处理起来很轻松。

3.2 coreset采样:特征库压缩的贪心策略

正常样本跑一遍会得到海量特征,直接全部存下来有两个问题。第一是检索速度,几百万条1536维向量的暴力近邻检索,即使有faiss也要吃掉大量内存和算力;第二是冗余,同一工件的同一纹理区域在不同光照下产生的特征高度相似,存下来对判定没有增益。

PatchCore论文提出的coreset采样,核心思路是在保留空间覆盖度的前提下,从海量特征中挑一个尽量小的子集。理想情况下,这个子集中的特征应该均匀分布在正常样本的特征空间里,让任何新的正常局部特征都能在子集中找到近邻,同时子集规模足够小,推理延迟可控。

论文里用的贪心最远点采样可以理解成这样一个过程:先随机选一个特征作为第一个中心,然后每次选一个新的特征,使得它到当前所有中心的最近距离最大,直到达到预设的比例。这个算法比随机采样更稳,因为它保证特征空间中没有大片未覆盖的区域。

# coreset贪心最远点采样精简实现,便于理解采样逻辑 import numpy as np def greedy_coreset(features, sample_ratio=0.01): # features: (N, D),N为原始特征库规模 n = len(features) k = max(1, int(n * sample_ratio)) # 记录每个特征到已有中心集合的最近距离 min_dist = np.full(n, np.inf) # 随机选起始点 center_idx = [np.random.randint(n)] min_dist = np.minimum(min_dist, np.linalg.norm(features - features[center_idx[-1]], axis=1)) while len(center_idx) < k: # 取距离当前中心集合最远的点作为新中心 new_center = int(np.argmax(min_dist)) center_idx.append(new_center) # 更新最近距离 dist_to_new = np.linalg.norm(features - features[new_center], axis=1) min_dist = np.minimum(min_dist, dist_to_new) return np.array(center_idx)

这段代码是教材式的朴素实现,每轮要计算全部特征到新中心的距离,复杂度O(NK),特征量超过几十万条时跑得很慢。工程上不会直接这么用,更常见的做法是把特征空间分块,每块内先做局部采样生成候选集,再在候选集上做贪心最远点采样,这样能在损失极小精度的情况下把采样速度压到秒级。

采样比例直接决定特征库大小和检测精度的平衡。当前工业落地项目的经验区是0.5%到1%。一个中等规模的产线,正常样本特征总量在三百万条左右,采样1%得到三万条中心特征,检索延迟在毫秒级,精度接近全量特征库的98%以上。如果硬件资源紧张,可以降到0.1%,但我会建议同步把评分阈值放宽,否则小缺陷容易被高维空间的距离噪声吞掉。

3.3 评分与阈值:k近邻距离和分位数设定

采样完成后的特征库会作为近邻检索的底库。推理时,测试图的每个patch特征去底库中找最近的k个正常特征,取第k近的距离作为该patch的异常分数。使用第k近而不是最近邻,是为了避免单个离群正常特征造成误判。k太小对噪声敏感,k太大会漏掉小缺陷,当前实践常用k=4。

异常分数图(anomaly map)生成后,图像级分数取所有patch分数的平均值,像素级定位则把28×28的分数图上采样回原图尺寸再平滑。这个双路输出的设计在产线上很实用:图像级分数做自动判停,像素级热图给人工复判提供参考区域。

阈值设定的常见做法是取正常验证集分数的差分位数,比如p99.9。具体流程是:留出一定数量的正常样本作为验证集,用训练好的特征库给它们打分,统计分数分布,取99.9分位数作为上线阈值。实际产线上我会再加一道人工抽检:拿100张无缺陷图和20张已知缺陷图跑一遍,看误报率和漏报率能不能同时压住,压不住就往回调阈值,优先保漏报率,因为缺陷漏到客户手里的成本远高于产线停线复检的成本。

4. 本地跑通PatchCore最小流程:环境、数据、训练与评估命令

4.1 环境与依赖安装

PatchCore没有复杂的编译依赖,核心组件是PyTorch、timm、faiss和OpenCV。训练阶段其实不需要梯度计算,只需要前向推理提取特征。

# 创建虚拟环境并安装依赖 python -m venv patchcore_env source patchcore_env/bin/activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install timm faiss-cpu opencv-python tqdm scikit-learn

这里把faiss装在CPU版本上不影响小规模验证,因为PatchCore的实际瓶颈在特征提取,近邻检索在几万条的特征库上CPU也很快。注意timm会自动联网下载ImageNet预训练权重,如果网络状况不稳定,建议先用timm自带工具下载到缓存目录,再离线加载,避免后面踩权重加载失败的坑。

4.2 数据集准备:MVTec AD的目录结构

MVTec AD是工业缺陷检测最常用的公开基准,它把每个物体类别独立成一个文件夹,内部区分训练集和测试集。

# MVTec AD 目录结构示例(bottle类别) mvtec/bottle/ ├── train/ │ └── good/ # 只有正常样本 │ ├── 000.png │ └── ... └── test/ ├── good/ # 正常测试图 ├── broken_large/ # 各类已知缺陷 ├── broken_small/ ├── contamination/ └── ...

训练集只有good目录,这是无监督检测的核心约束。跑通流程后,把自己的产线数据也按这个格式整理,一个类别一个根目录,train/good放正常样本,test目录放带标签或待测样本即可。

4.3 训练侧特征提取代码

PatchCore的核心代码高度集中,训练阶段就是遍历正常样本,提取patch级特征,存入内存。

# 训练阶段:对正常样本提取patch级特征并构建特征库 import torch import timm import numpy as np import cv2 from pathlib import Path device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = timm.create_model('wide_resnet50_2', pretrained=True, features_only=True) model = model.to(device).eval() # 一定要保持eval模式,影响BN统计量 # 取layer2和layer3输出 feat_layers = [2, 3] target_size = 28 # 统一到28x28 def load_image(path): img = cv2.imread(str(path)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (224, 224)) img = torch.from_numpy(img).float() / 255.0 # 用ImageNet均值方差做归一化 mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1) std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1) return (img.permute(2, 0, 1).unsqueeze(0) - mean) / std def extract_features(img_tensor): with torch.no_grad(): feats = model(img_tensor) # 输出顺序: [stem, layer1, layer2, layer3, layer4] feats = [feats[i] for i in feat_layers] feats = [ torch.nn.functional.interpolate(f, size=(target_size, target_size), mode='bilinear') for f in feats ] patch = torch.cat(feats, dim=1) # (B, 1536, 28, 28) patch = patch.permute(0, 2, 3, 1).cpu().numpy() return patch.reshape(-1, patch.shape[-1]) memory_bank = [] for img_path in Path('data/mvtec/bottle/train/good').glob('*.png'): tensor = load_image(img_path).to(device) features = extract_features(tensor) # (784, 1536) memory_bank.append(features) memory_bank = np.concatenate(memory_bank, axis=0) print(f'特征库规模: {memory_bank.shape}')

这段代码的关键在于model.eval(),因为预训练ResNet里的BatchNorm在train模式下会更新均值和方差,直接污染特征分布。另一个细节是归一化需要用ImageNet的均值方差,不能只做除以255,否则特征分布偏移,后续距离阈值全都不准。

4.4 推理与评估代码:AUROC怎么算

推理阶段对每一张测试图提取同样的patch特征,用近邻检索计算每个patch到正常特征库的距离。

# 推理阶段:近邻检索与异常评分 from sklearn.neighbors import NearestNeighbors # 对特征库做coreset采样,控制规模 # 这里直接用全量特征库演示思路,实际可调用第3章的coreset采样函数 nbrs = NearestNeighbors(n_neighbors=4, algorithm='brute') nbrs.fit(memory_bank) def compute_scores(img_tensor): patch_feats = extract_features(img_tensor) # (784, 1536) # 返回每个patch到最近4个正常特征的距离 dist, _ = nbrs.kneighbors(patch_feats) patch_scores = dist[:, -1] # 取第4近邻距离 return patch_scores.reshape(28, 28) gt_labels = [] # 1为缺陷 pred_scores = [] for defect_dir in ['contamination']: for img_path in Path(f'test/{defect_dir}').glob('*.png'): tensor = load_image(img_path).to(device) anomaly_map = compute_scores(tensor) gt_labels.append(1) pred_scores.append(anomaly_map.mean()) # 图像级分数用均值 for img_path in Path('test/good').glob('*.png'): tensor = load_image(img_path).to(device) anomaly_map = compute_scores(tensor) gt_labels.append(0) pred_scores.append(anomaly_map.mean()) from sklearn.metrics import roc_auc_score print(f'图像级AUROC: {roc_auc_score(gt_labels, pred_scores):.4f}')

这里用sklearn的brute近邻检索可以保证在几万条特征库上不掉链子。更大规模的特征库建议改用faiss的IndexFlatL2,代码只差一行,检索速度快一个数量级。图像级分数用均值而不是最大值,因为像素级噪声会让最大值频繁误报;缺陷区域通常占整图面积的5%以下,均值虽然把缺陷分数拉低,但对多数真实缺陷仍然远高于正常分布,且稳定性更好。

4.5 完整命令与关键参数

把训练和推理两段代码整合成脚本后,命令行入口如下。

python run_patchcore.py \ --data_root /data/mvtec \ --category bottle \ --backbone wide_resnet50_2 \ --sample_ratio 0.01 \ --k 4 \ --image_size 224 \ --threshold_percentile 99.9

值得解释的参数就四个。sample_ratio控制特征库压缩比例,0.01对应1%,显存紧张时降到0.005;k是近邻个数,设大了对噪声更鲁棒但小缺陷容易被淹没;image_size建议固定224,因为预训练权重的感受野和归一化参数都针对它设计,擅自改成192或256会带来不可控的特征偏移;threshold_percentile是阈值分位数,只在困境不确定时用来快速试跑,真正上线前一定要用人工抽检验证。

5. PatchCore实践避坑:五个高频故障与排查清单

5.1 换设备后精度波动大的玄学问题

现象:在同一份数据上,开发机和产线工控机分别跑一遍,AUROC差了两个点以上,有几个缺陷类别甚至完全丢失。

原因:这个现象十次有九次是预训练权重或PyTorch版本不一致。timm在加载权重时会校验配置,如果缓存目录里已有同名但不同哈希的权重文件,会静默复用旧文件。另一个因素是和BatchNorm相关的浮点运算差异在CPU和GPU间放大,特征分布整体偏移后,近邻距离的排序不改,但绝对数值变了。

解决:先把timm缓存的权重文件删除,重新离线下载固定版本,并在代码里打印权重来源的URL或MD5,确保所有设备加载的是同一个文件。再把推理代码固定为eval模式,CPU和GPU分别跑一遍正常样本,看特征库距离分布的均值是否一致,允许微小差异但不允许偏移量超过5%。

5.2 预训练权重加载失败被静默忽略

现象:训练阶段正常跑通,但测试分数全部贴近同一个值,AUROC接近0.5,特征库距离分布只有极窄的区间。

原因:timm加载权重时如果网络失败会抛异常等待重试,但如果在某些集成项目里捕获了异常并继续执行,model的权重会保持随机初始化状态,提取出来的特征没有任何语义信息,正常与缺陷在高维空间里都是随机噪声。

解决:代码里显式检查权重的加载状态,最直接的办法是打印model.default_cfg中的url字段是否与预期一致,或者直接用torch.load手动加载权重文件再赋值给模型。另一个笨办法是在训练前用同一张图跑两次特征提取,对比两次输出是否完全一致,因为随机权重在网络输入不变时输出也是确定的,这个检查不能发现全部问题,加上权重文件的MD5校验最稳。

5.3 显存爆炸与内存溢出的连锁反应

现象:特征库构建时跑了上千张正常样本,程序在某个批次后直接OOM,或者faiss建立索引时报内存不足。

原因:一张224×224的图会生成784条1536维特征向量,5万张正常样本不采样就是近四千万条特征,仅存储就要占用超过180GB内存,显存更是无从谈起。这是把“全量特征库”错误地当成了可实施方案。

解决:特征提取前先评估内存预算。一次处理一个batch,在内存里累积到一定规模就调用coreset采样压缩,再把采样结果追加到总特征库。实际落地时把sample_ratio从1%调低到0.1%可以极大缓解内存压力,代价是检测精度轻微下降。还有一个保守的偏好:优先用CPU内存存特征库而不是显存,因为显存还要留给模型前向推理。

5.4 新品类上线后误报率突然升高

现象:已有品类检测正常,但换了新材质或新光源后,整条产线连续触发的误报让质检员直接把系统停掉。

原因:特征库完全由正常样本构成,而新材质的正常纹理可能落在已有特征库覆盖不到的空白区域。比如之前全是金属磨砂面的特征,现在上线的是黑色橡胶表面,其特征距离所有正常中心都很远,被判成缺陷。这不是算法坏了,是特征库没覆盖新材料。

解决:新品类一定要单独建特征库,在一个分类下检测,而不是把多个材质混到同一个库里。上线后的增量更新也有学问:把误报样本的特征加入特征库,但不要直接追加,这样会让特征库向误报区域漂移;更稳的做法是按批次重新采样,让新样本和老样本一起参与贪心最远点选择,避免历史特征被新样本挤掉。

5.5 训练-推理不一致导致评估虚高

现象:离线验证时AUROC很高,上线后实际漏报率却明显偏大,人工复判的patch和热图对不上。

原因:代码里反复切换模型的train/eval状态,或者在自动调参脚本中忘记恢复eval,导致BatchNorm在推理时继续更新统计量。特征提取用的interpolate模式不统一也会造成空间对齐偏移,训练侧用bilinear而上线侧用nearest,patch的位置整体错位,近邻距离自然失真。

解决:推理路径全部走同一个函数,不允许有双份实现。模型统一加torch.no_grad()和model.eval()。还有一个容易忽略的细节:interpolate的尺寸参数必须固定写死,不能用h//2这类动态表达式,因为不同分辨率推断时可能产生不同的对齐结果。建议在离线评估前先跑一组正常的样本,确认推理侧的输出tensor形状与训练侧完全一致再进入批量的评估流程。

6. 验证与进阶:动态阈值、多类缺陷与产线部署建议

6.1 动态阈值切片:按类别与不同光源条件分开统计

固定阈值在单光源、单材质的场景够用,但实际线体的光源亮度会随时间漂移,白天和夜班的色温也略有差异。我的做法是把正常验证集按时间切片,每两小时收集一批正常样本,给每个切片独立算p99.9分位数,形成一条随时间变化的阈值曲线。上线时根据当前时间片查表取阈值。这个办法比单一阈值多了一步维护,但能把光源漂移造成的整体分数抬高直接消化掉,误报率稳定很多。

6.2 多类缺陷统一建模:多个特征库并行检索

真实产线通常同时检测多种部件,部件之间的特征空间差异很大,不适合塞进同一个特征库。工程上常见的做法是每类部件建一个单独的faiss索引,推理时先用分类模型判断部件类型,再路由到对应索引。分流失败的情况并不少见,所以我会在分类模型输出的置信度低于阈值时同时检索所有索引,取最低的异常分数作为输出,保留了一个兜底通道。

6.3 部署形态与推理提速

推理链路中最贵的部分是特征提取,不是近邻检索。全精度Wide ResNet-50在GPU上处理一张224×224图大约需要15到30毫秒,CPU上则要数百毫秒。产线速度达不到要求的场景,先把输入分辨率降到192验证精度损失,可以接受就采用,再把模型导出为半精度或INT8。近邻检索部分用faiss的IndexFlatL2序列化特征库到磁盘,启动时加载,几千条特征库的查询耗时在亚毫秒级。

一个容易忽略的部署细节是内存对齐。faiss索引的向量维度必须和训练侧完全一致,包括dtype。训练侧如果用了np.float32,索引加载时就不能用np.float64的特征强行查询,否则会报维度不匹配或返回全零距离。我会在序列化文件里附加一个配置文件,记录特征维度、采样比例、k值和阈值,部署端启动时先读取配置再加载索引,避免人工传参引入不一致。

我真正吃过大亏的一次是上线前改了图像输入尺寸,把局部特征的空间坐标直接从56×56改成了28×28对齐,结果部署端忘记同步修改特征库生成代码,导致上线后所有good样本的分数都比正常高50%,整条产线误报警刷屏。后来我把特征提取、特征库生成、索引加载、阈值查表全部收敛到一个配置文件里,每次改动必须连带更新配置版本号,才彻底杜绝了这类低级事故。特征嵌入这条路的上限不在算法,而在工程纪律:什么版本的特征、什么来源的权重、什么标准的数据预处理,每一项都要能追踪。希望这些踩坑记录能帮你在PatchCore方向上少走几段弯路。

本文还有配套的精品资源,点击获取

返回列表