ARTICLE DETAIL

资讯详情

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

ResNet工业异常检测实战:多尺度特征提取与评分方案

ResNet工业异常检测实战:多尺度特征提取与评分方案 简介这份PDF文档面向从事机器学习、数据建模与网络安全方向的研究者与工程人员聚焦异常检测中自编码器易过拟合、误报率偏高的痛点提出一种基于ResNet深度神经网络的改进模型。资源包内仅含1个PDF文件大小约1.59MB为期刊论文全文涵盖摘要、引言、模型设计、代价函数与L2正则化、KDDCup99数据集实验及结论等完整章节便于系统研读与引用。文档详细阐述了用固定切分规则将数据分为A、B两部分、训练网络学习A到B映射的思路并借助ResNet残差层缓解梯度消失测试阶段通过输出误差与阈值划分正常与异常。目前已有181人学习适合希望深入理解深度学习异常检测建模、复现实验或撰写相关论文的读者参考可从中获取模型构造、正则化策略与评估指标的完整方案。1. 从一张工业质检图说起ResNet 做异常检测到底在解决什么产线上每分钟过 60 个零件缺陷样本一年攒不出 200 张正常样本却有几百万张——这是绝大多数工厂质检场景的真实分布。你拿这种数据去训一个二分类网络它会把所有样本都判成正常准确率还能到 99.9%但一个缺陷都抓不出来。异常检测要解决的就是这个问题只有正常样本或者正负极度不平衡怎么让模型学会什么是不对的。ResNet 在这里的角色不是分类器而是特征提取的骨架。它的残差连接让梯度能穿过几十甚至上百层提取到的深层特征比浅层网络稳定得多。工业图像里缺陷往往只占几十个像素浅层特征分辨率高但语义弱深层特征语义强但空间信息丢得厉害ResNet 的中间层输出恰好卡在一个可用的平衡点上。常见做法是取 layer2 和 layer3 的特征图做多尺度融合再在上面跑异常评分。这套方案适合谁手里有正常样本、缺陷样本极少甚至没有、又不想从零训一个大模型的团队。用 ImageNet 预训练的 ResNet 权重做初始化几百张正常图就能跑出一个可用的基线。下面从原理到代码把这条路走通。2. 为什么选 ResNet 而不是 VGG 或自编码器特征提取的选型账2.1 残差连接对异常检测的实际意义异常检测的核心假设是正常样本在特征空间里聚成一团异常样本偏离这个分布。特征空间的质量直接决定检测上限。VGG 堆到 19 层就开始退化训练集准确率反而下降这不是过拟合是梯度在深层衰减了。ResNet 的恒等映射让第 n 层的输出至少能保留第 n-1 层的信息梯度可以走捷径回传。对异常检测来说这意味着两件事。第一你可以放心用 layer3、layer4 的深层特征不用担心它们退化成噪声。第二预训练权重迁移过来后浅层边缘纹理和深层语义都能保留缺陷既可能是纹理异常划痕也可能是语义异常缺件两种信号都在。我一般会取 layer2 输出stride 8通道 512和 layer3 输出stride 16通道 1024做融合。layer2 分辨率够高小缺陷不会丢layer3 语义够强能区分正常纹理变化和真缺陷。layer4 的 stride 是 32对小于 32 像素的缺陷基本没响应除非你的缺陷都很大否则不建议单独用。2.2 自编码器路线和 ResNet 特征路线的边界自编码器AE是异常检测的经典路线训一个网络重建正常样本测试时重建误差大的判为异常。它在 MNIST、纹理数据集上表现不错但工业图像一上来就翻车。原因是 AE 的重建能力太强缺陷区域它也能重建得差不多误差区分度不够。而且 AE 需要从零训练几百张正常图训出来的 decoder 泛化很差。ResNet 特征路线不需要训练特征提取器直接用预训练权重。你只需要在特征空间上建一个分布模型——高斯、KNN、或者一个小的密度估计头。训练成本从几天降到几小时数据需求从几万降到几百。代价是特征空间不是为你的任务定制的如果缺陷和正常样本在 ImageNet 特征空间里差异极小比如纯色表面的微小色差效果会打折扣。这时候可以在特征提取器后面接一个轻量的微调头用少量标注样本做对比学习。2.3 用 torchvision 加载预训练 ResNet 并抽出多尺度特征import torch import torch.nn as nn import torchvision.models as models class ResNetFeatureExtractor(nn.Module): def __init__(self, pretrainedTrue): super().__init__() # 加载 ImageNet 预训练 ResNet50 resnet models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2 if pretrained else None) # 拆出各 stage保留中间层输出 self.stem nn.Sequential( resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool ) # stride 4 self.layer1 resnet.layer1 # stride 4, 256 channels self.layer2 resnet.layer2 # stride 8, 512 channels self.layer3 resnet.layer3 # stride 16, 1024 channels self.layer4 resnet.layer4 # stride 32, 2048 channels def forward(self, x): x self.stem(x) f1 self.layer1(x) f2 self.layer2(f1) f3 self.layer3(f2) f4 self.layer4(f3) return f2, f3, f4 # 返回多尺度特征这段代码的关键点是拆开 ResNet 的 stage而不是直接调resnet.forward。resnet50默认返回 1000 维分类输出中间特征拿不到。拆开之后f2的分辨率是输入的 1/8f3是 1/16f4是 1/32。输入 256×256 时f2是 32×32f3是 16×16f4是 8×8。参数上weights用IMAGENET1K_V2比V1在工业图像上通常好 1-2 个点因为 V2 的训练配方更强。如果你的图像是灰度图把conv1的输入通道改成 1权重取三个通道的均值这样比直接复制单通道效果好。注意layer4的 stride 32 意味着 256 输入下特征图只有 8×8小于 8 像素的缺陷在这个尺度上完全消失。如果你的缺陷普遍小于 16 像素只用f2和f3别用f4。3. 从特征图到异常分数三种可落地的评分方案3.1 基于高斯分布的马氏距离评分正常样本的特征在每一维上不一定独立但可以假设它们服从一个多元高斯分布。对每个空间位置 (i,j)收集所有正常样本在该位置的特征向量估计均值 μ 和协方差 Σ。测试时算马氏距离import numpy as np class GaussianScorer: def __init__(self, feat_dim, reg1e-5): self.feat_dim feat_dim self.reg reg # 协方差正则化防止奇异 self.mean None self.cov_inv None def fit(self, features): # features: (N, C, H, W) 正常样本特征 N, C, H, W features.shape feats features.transpose(0, 2, 3, 1).reshape(-1, C) # (N*H*W, C) self.mean feats.mean(axis0) cov np.cov(feats, rowvarFalse) cov np.eye(C) * self.reg # 正则化 self.cov_inv np.linalg.inv(cov) def score(self, feature): # feature: (C, H, W) 单张测试图特征 C, H, W feature.shape feats feature.transpose(1, 2, 0).reshape(-1, C) diff feats - self.mean dist np.sum(diff self.cov_inv * diff, axis1) return dist.reshape(H, W) # 每个位置的异常分数reg参数很关键。特征维度 512 时如果正常样本只有 200 张协方差矩阵的秩最多 200直接求逆会炸。reg1e-5是经验值数据越少调越大到1e-3也能用。马氏距离的好处是考虑了特征间的相关性比欧氏距离稳。缺点是每个位置独立估计空间上下文没用上。3.2 用 KNN 做无参数异常评分如果不想估计协方差KNN 更省事。把正常样本的所有位置特征存成一个库测试时算每个位置到最近 K 个正常特征的欧氏距离取平均作为异常分数。from sklearn.neighbors import NearestNeighbors class KNNScorer: def __init__(self, k5): self.k k self.nn NearestNeighbors(n_neighborsk, metriceuclidean) def fit(self, features): N, C, H, W features.shape feats features.transpose(0, 2, 3, 1).reshape(-1, C) # 降采样避免库太大 if feats.shape[0] 100000: idx np.random.choice(feats.shape[0], 100000, replaceFalse) feats feats[idx] self.nn.fit(feats) def score(self, feature): C, H, W feature.shape feats feature.transpose(1, 2, 0).reshape(-1, C) dist, _ self.nn.kneighbors(feats) return dist.mean(axis1).reshape(H, W)k5是常用起点。k 太小对噪声敏感k 太大异常区域会被正常邻居稀释。库大小超过 10 万时查询会慢随机降采样到 10 万通常不掉点。KNN 的优势是不用假设分布缺点是推理时要存整个正常特征库内存占用大。512 维、10 万条特征约 200MB可以接受。3.3 多尺度融合与最终异常图生成单独用一层特征容易漏检。f2对小缺陷敏感但误报多f3语义强但定位粗。把两层的异常分数图上采样到同一分辨率后加权平均import torch.nn.functional as F def fuse_anomaly_maps(score_f2, score_f3, weight_f20.4, weight_f30.6): # score_f2: (H2, W2), score_f3: (H3, W3) h, w score_f2.shape # 把 f3 的分数图上采样到 f2 的分辨率 s3_up F.interpolate( torch.tensor(score_f3).unsqueeze(0).unsqueeze(0), size(h, w), modebilinear, align_cornersFalse ).squeeze().numpy() # 各自归一化到 [0,1] s2_norm (score_f2 - score_f2.min()) / (score_f2.max() - score_f2.min() 1e-8) s3_norm (s3_up - s3_up.min()) / (s3_up.max() - s3_up.min() 1e-8) return weight_f2 * s2_norm weight_f3 * s3_norm权重0.4/0.6是我在几个纹理数据集上的经验值缺陷偏小时把f2权重提到 0.6。归一化必须做因为两层分数的量纲不同直接加权会被大量纲的那层主导。融合后的异常图做高斯平滑σ4再取最大值或 99 分位数作为图像级异常分数比直接取 max 稳能压掉单像素噪声。4. 训练与推理的工程细节数据、增强和阈值怎么定4.1 正常样本的采集与预处理规范异常检测对正常样本的纯度极其敏感。如果正常训练集里混了 5% 的缺陷图模型会把缺陷特征当成正常分布的一部分检测率直接掉一半。采集时宁可少而精200 张干净的正常图比 2000 张混了缺陷的图有用。预处理统一到 256×256ImageNet 的均值和方差做归一化。如果原始图像长宽比差异大直接 resize 会变形建议短边 resize 到 256 再中心裁剪。灰度图复制成三通道别用单通道改网络预训练权重的第一层是按三通道训的改单通道会丢信息。注意训练集和测试集的预处理必须完全一致。我见过测试时忘了做同样的归一化异常分数整体偏移阈值完全失效。把预处理写成一个函数训练和推理都调它。4.2 数据增强在异常检测里的正确用法分类任务里常用的随机裁剪、旋转、颜色抖动在异常检测里要慎用。旋转 90 度可能把正常纹理变成异常纹理颜色抖动可能让正常样本的色差超过缺陷的色差。安全的增强只有水平翻转、小角度旋转±10 度、亮度微调±5%。import torchvision.transforms as T train_transform T.Compose([ T.Resize(256), T.CenterCrop(256), T.RandomHorizontalFlip(p0.5), T.RandomRotation(degrees10), T.ColorJitter(brightness0.05, contrast0.05), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])增强只用于扩充正常样本的多样性让特征分布估计更鲁棒。不要用 CutMix、MixUp 这类会破坏空间结构的增强异常检测依赖空间位置信息混了之后位置对应关系就乱了。4.3 阈值选取从验证集分位数到业务对齐异常分数是连续的落地必须有一个二值化阈值。没有标注缺陷的情况下用正常验证集的分数分布定阈值取 99 分位数意味着正常样本有 1% 的误报率。这个阈值在产线上通常可接受因为后续还有人工复检。如果有少量标注缺陷画 ROC 曲线找最佳工作点。但要注意产线关心的不是 AUC是召回率在 95% 以上时误报率能不能压到 5% 以下。我一般会先定召回率下限再在满足召回的前提下找最低误报的阈值。def find_threshold(normal_scores, defect_scoresNone, target_recall0.95): if defect_scores is not None: # 有缺陷样本按目标召回率定阈值 thresh np.percentile(defect_scores, (1 - target_recall) * 100) else: # 无缺陷样本按正常分数分位数 thresh np.percentile(normal_scores, 99) return thresh阈值不是定一次就完事。换批次、换光照、换镜头分数分布都会漂。产线部署时每周用一批新正常样本重新估一次 99 分位数漂移超过 20% 就报警。5. 避坑与排查ResNet 异常检测最常见的五个翻车点5.1 现象所有测试图异常分数都很高阈值完全失效原因通常是预处理不一致。训练时用了 ImageNet 归一化推理时忘了做或者 resize 的插值方式不同训练用 bilinear推理用 nearest。特征分布整体偏移马氏距离算出来全是大值。解决把预处理固化成一个函数训练和推理共用。在推理脚本开头打印一张正常图的异常分数和训练时的正常分数均值对比偏差超过 30% 就检查预处理。5.2 现象小缺陷全部漏检大缺陷能检出layer4的 stride 32 是元凶。256 输入下f4只有 8×8一个 10 像素的缺陷在f4上不到一个像素特征响应被平均掉了。如果你只用了f4或者融合时f4权重过大小缺陷必然漏。解决去掉f4只用f2和f3。如果必须用深层语义把输入分辨率提到 512f4变成 16×16小缺陷至少能占一个格子。代价是显存和推理时间翻倍。5.3 现象正常样本的异常分数波动很大同一类产品有的 0.1 有的 0.8正常样本本身多样性太高比如金属表面反光角度不同特征分布不是单峰的。用单一高斯估计协方差矩阵被拉大所有样本的马氏距离都差不多区分度下降。解决先对正常样本做聚类K3 到 5每个簇单独估一个高斯。测试时取到最近簇的马氏距离。或者换 KNNKNN 对多峰分布天然更鲁棒。如果多样性是光照引起的先做直方图均衡化再提特征。5.4 现象推理速度太慢单张图超过 500msKNN 库太大是常见原因。10 万条 512 维特征每次查询要算 10 万次欧氏距离CPU 上单张图要几百毫秒。另外f2特征图 32×321024 个位置每个位置都查 KNN计算量是 1024×10 万。解决用 FAISS 建 IVF 索引查询速度能提 10 倍以上。或者对特征做 PCA 降到 128 维精度掉 1-2 个点速度提 4 倍。再不行就只在f3上做 KNNf2只做高斯评分两者计算量差 4 倍。5.5 现象换了一个新批次的产品误报率突然飙升域偏移。新批次的材质、光照、相机参数和训练集不同正常样本的特征分布整体平移了。马氏距离的均值还是旧的算出来全偏大。解决产线部署时保留一个在线更新机制。每批新正常样本确认无缺陷的提特征后用滑动平均更新高斯均值协方差也做指数加权更新。更新率设 0.01 到 0.05太快会被噪声带偏太慢跟不上漂移。同时监控正常样本异常分数的均值连续 3 批上升就触发重新估计。6. 进阶用注意力加权的多尺度特征提升定位精度基础版的多尺度融合是固定权重但不同图像里缺陷尺度不一样。一张图里可能同时有划痕小和缺件大固定权重没法同时最优。一个实用的改进是加一个轻量的注意力模块让网络自己学每个位置的尺度权重。具体做法把f2和f3上采样到同一分辨率后拼接过一个 1×1 卷积降到 1 通道sigmoid 激活得到空间注意力图。用这个图对两层的异常分数做加权class AttentionFusion(nn.Module): def __init__(self, c2512, c31024): super().__init__() self.proj2 nn.Conv2d(c2, 128, 1) self.proj3 nn.Conv2d(c3, 128, 1) self.attn nn.Sequential( nn.Conv2d(256, 64, 3, padding1), nn.ReLU(), nn.Conv2d(64, 1, 1), nn.Sigmoid() ) def forward(self, f2, f3): # f3 上采样到 f2 分辨率 f3_up F.interpolate(f3, sizef2.shape[2:], modebilinear, align_cornersFalse) p2 self.proj2(f2) p3 self.proj3(f3_up) cat torch.cat([p2, p3], dim1) attn_map self.attn(cat) # (B, 1, H, W) # 用注意力图加权两层特征 fused attn_map * p2 (1 - attn_map) * p3 return fused, attn_map这个模块参数量很小proj2和proj3加起来不到 20 万参数可以在正常样本上自监督训练让融合后的特征在正常区域重建误差最小异常区域自然响应大。训练时只更新这个融合模块ResNet 主干冻结。注意力图本身有诊断价值。可视化出来高响应区域就是模型认为需要关注的地方。如果注意力图在正常区域也大面积激活说明融合模块过拟合了需要加 dropout 或减小学习率。验证这套改进有没有用别只看 AUC。在产线数据上我习惯看三个指标召回率误报率5%、缺陷定位 IoU、以及单张推理延迟。注意力融合通常能把 IoU 从 0.3 提到 0.45 左右召回率提 2-3 个点延迟增加 15%。如果延迟敏感把注意力模块的通道数从 128 降到 64精度掉不到 1 个点。最后说个血泪教训别在测试集上调阈值。我早期做的一个项目在测试集上把阈值调到最优上线第一周就翻车因为测试集和产线分布有细微差异。后来改成用正常验证集定阈值产线数据只做监控不做调参误报率反而稳了。模型上线不是终点是监控的起点。希望帮到你。本文还有配套的精品资源点击获取
返回列表