ARTICLE DETAIL

资讯详情

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

基于ResNet的异常检测:从特征提取到工业质检实战

基于ResNet的异常检测:从特征提取到工业质检实战 简介这份PDF文档面向从事机器学习、深度学习与数据建模的算法工程师及安全方向研究者聚焦异常检测中自编码器易过拟合、误报率偏高的痛点提出一种基于ResNet深度神经网络的检测模型。资源包共1个文件为1.59MB的pdf格式便于直接阅读与存档。文档系统阐述了用固定切分规则将数据分为A、B两部分、训练网络学习A到B映射的思路并引入L2正则化与相应代价函数抑制过拟合测试阶段通过输出误差与阈值划分正常与异常同时借助ResNet残差层缓解梯度消失。文中还给出在KDDCup99数据集上的检测率与误报率表现并围绕神经网络、深度学习、ResNet、代价函数等概念展开说明适合希望理解模型设计动机、复现实验思路或撰写相关论文的读者参考。目前已有181人学习。1. 从一份 PDF 标题说起ResNet 做异常检测到底解决什么问题工业质检线上一台相机每秒拍 30 张图缺陷样本可能一千张里才有一张而且缺陷形态每次都不一样——划痕、脏污、缺角、色差你根本没法用传统分类网络去训。这就是异常检测的经典场景正常样本管够异常样本稀少且不可穷举。标题里这份《基于ResNet深度神经网络的异常检测模型.pdf》要讲的正是用 ResNet 当特征提取骨干只学好样本的分布然后把偏离分布的输入判为异常。它适合两类人一是手里有产线图像、想快速搭一套可用检测流水线的工程师二是已经用过 AutoEncoder 或 PatchCore但发现小缺陷漏检严重、想换更强骨干的人。ResNet 在这里不是拿来分类的而是当“特征尺子”用——预训练权重冻结中间层输出拿来做嵌入异常分数靠距离或重构误差算。搞清这个定位后面所有参数才有意义。2. ResNet 为什么适合当异常检测的骨干从残差连接到特征金字塔2.1 残差结构解决了“深层特征退化”这个老问题异常检测对特征的要求很矛盾既要有足够语义知道这是“螺丝”还是“背景”又要保留足够空间细节知道缺陷在哪个像素。浅层网络语义弱深层网络又容易退化。ResNet 的残差块把输入直接加到输出上梯度可以走捷径回传训练几百层也不会出现“越深越差”的玄学。实际用的时候我们通常不训练而是直接加载 ImageNet 预训练权重把 ResNet 当固定特征提取器。原因很简单产线缺陷样本太少微调容易过拟合而 ImageNet 学到的边缘、纹理、颜色统计对工业图像同样有效。常见做法是取 layer1 到 layer4 四个 stage 的输出分别对应不同感受野。2.2 多尺度特征融合粗粒度看整体细粒度看局部热搜里提到的“resnet fpn 位置编码 粗粒度特征 细粒度特征 自注意力”其实点出了异常检测的核心矛盾。粗粒度特征layer4stride 32语义强能判断“这个区域整体像不像正常件”但分辨率低小划痕直接消失。细粒度特征layer1/layer2stride 4/8分辨率高能定位微小缺陷但语义弱容易把正常纹理波动当成异常。所以主流方案是 FPN 式的自顶向下融合把 layer4 上采样和 layer3、layer2 逐元素相加或拼接得到既有语义又有细节的特征图。位置编码和自注意力不是必须的但在大图、缺陷位置敏感的场景下加一个轻量注意力能明显降低误报。2.3 特征嵌入的三种主流用法与选型对比方法核心思路适合场景缺点重构式AE/VAE ResNet用 ResNet 编码再解码看重构误差缺陷类型未知、样本极少正常纹理复杂时重构也差误报高记忆库式PatchCore 类存正常特征测试时算最近邻距离中等数据量、缺陷局部特征库大推理慢归一化流式学正常特征的概率密度对分数阈值敏感的场景训练不稳定调参成本高我一般会先跑 PatchCore ResNet50 作为基线因为它不需要训练半天就能出第一版指标。如果误报压不下去再考虑加 FPN 融合或换更细的 layer2 特征。3. 动手搭最小可跑系统从加载预训练 ResNet 到输出异常热力图3.1 环境与依赖固定版本避免翻车# 建议 Python 3.8PyTorch 1.12 以上 pip install torch torchvision opencv-python scikit-learn matplotlib # 如果要用 faiss 加速最近邻检索 pip install faiss-cpu这里不写具体版本号因为不同 CUDA 驱动对应不同 torch 版本硬写反而容易装不上。原则是torch 和 torchvision 版本必须匹配否则加载预训练权重会报 key 不匹配。装完先跑一句torch.cuda.is_available()确认 GPU 可用CPU 也能跑只是特征提取慢 5 到 10 倍。3.2 用 ResNet 提取多尺度特征四个 stage 的输出怎么取import torch import torchvision.models as models import torch.nn as nn class ResNetFeatureExtractor(nn.Module): def __init__(self, backboneresnet50, pretrainedTrue): super().__init__() resnet getattr(models, backbone)(pretrainedpretrained) # 拆出四个 stage分别对应不同分辨率 self.stem nn.Sequential( resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool ) self.layer1 resnet.layer1 # stride 4, channels 256 self.layer2 resnet.layer2 # stride 8, channels 512 self.layer3 resnet.layer3 # stride 16, channels 1024 self.layer4 resnet.layer4 # stride 32, channels 2048 # 冻结参数不参与训练 for p in self.parameters(): p.requires_grad False 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 [f1, f2, f3, f4]逻辑说明stem把输入降采样 4 倍layer1 到 layer4 逐级降采样并增加通道。返回列表而不是单个张量是为了后面做多尺度融合。参数上pretrainedTrue会下载 ImageNet 权重第一次运行需要联网如果产线环境离线提前把权重文件放到~/.cache/torch/hub/checkpoints/下。冻结参数这步很关键不冻结的话即使你不写 optimizerBN 层的 running_mean 也会在 forward 时被更新导致特征漂移——这是很多人踩过的坑。3.3 构建正常特征记忆库PatchCore 的核心步骤import numpy as np from sklearn.random_projection import SparseRandomProjection def build_memory_bank(features_list, ratio0.1, proj_dim128): features_list: 多尺度特征列表每个形状 [N, C, H, W] 返回降维后的正常特征库 # 只取 layer2 和 layer3 做融合兼顾细节和语义 f2, f3 features_list[1], features_list[2] # 把 f3 上采样到 f2 尺寸 f3_up torch.nn.functional.interpolate( f3, sizef2.shape[-2:], modebilinear, align_cornersFalse ) fused torch.cat([f2, f3_up], dim1) # [N, 5121024, H, W] N, C, H, W fused.shape # 展平成 [N*H*W, C] patches fused.permute(0, 2, 3, 1).reshape(-1, C).cpu().numpy() # 随机投影降维加速最近邻搜索 proj SparseRandomProjection(n_componentsproj_dim, random_state42) patches_proj proj.fit_transform(patches) # 随机采样 ratio 比例控制记忆库大小 idx np.random.choice(len(patches_proj), int(len(patches_proj)*ratio), replaceFalse) return patches_proj[idx], proj逻辑说明融合 layer2 和 layer3 是权衡后的选择——只用 layer4 小缺陷会丢只用 layer1 误报会爆。interpolate用双线性上采样不用最近邻因为最近邻会产生棋盘伪影影响距离计算。随机投影把 1536 维降到 128 维推理速度能快 5 倍以上精度损失通常在 1% 以内。ratio0.1是经验值正常样本多的时候可以降到 0.01样本少就提到 0.3。注意fit_transform只能在正常样本上做测试样本必须用同一个proj做transform否则距离没有可比性。3.4 异常分数计算与热力图生成def anomaly_score(query_feat, memory_bank, proj, image_size(256, 256)): query_feat: 单张图的融合特征 [1, C, H, W] 返回异常分数图 [H, W] _, C, H, W query_feat.shape patches query_feat.permute(0, 2, 3, 1).reshape(-1, C).cpu().numpy() patches_proj proj.transform(patches) # 欧氏距离最近邻 dists np.linalg.norm( patches_proj[:, None, :] - memory_bank[None, :, :], axis2 ) min_dists dists.min(axis1) score_map min_dists.reshape(H, W) # 归一化到 0-1 score_map (score_map - score_map.min()) / (score_map.max() - score_map.min() 1e-8) return score_map逻辑说明patches_proj[:, None, :] - memory_bank[None, :, :]会生成一个 [H*W, M, D] 的大矩阵M 是记忆库大小。如果 M 超过 10 万内存会爆这时候要用 faiss 的IndexFlatL2替代。min_dists取每个 patch 到正常库的最近距离距离越大越异常。归一化用 min-max但实际部署时阈值不能靠单张图归一化要用正常验证集的分位数定阈值比如 99% 分位数。热力图直接 reshape 回 H×W 再上采样到原图尺寸即可。4. 避坑与排查ResNet 异常检测最常见的 5 个翻车现场4.1 现象正常样本也报异常误报率超过 20%原因特征融合时用了 layer1而 layer1 对纹理和光照变化太敏感产线光源轻微波动就产生大距离。解决去掉 layer1只用 layer2layer3或者在预处理加直方图均衡化把光照影响压下去。我一般会先可视化正常样本的分数图如果高分区集中在边缘或反光处基本就是这个问题。4.2 现象小缺陷漏检分数图和背景没区别原因只用了 layer4 特征stride 32 意味着原图 32 像素的缺陷在特征图上不到 1 像素直接被平均掉了。解决把 layer2 加进来或者把输入分辨率从 256 提到 512。注意提高分辨率会线性增加显存和推理时间512 输入下 ResNet50 单张约 80msV100要权衡产线节拍。4.3 现象换了批次正常样本阈值全失效原因记忆库只用了第一批正常样本特征分布没覆盖新批次的自然波动。解决记忆库要定期更新但不能全量替换。常见做法是滑动窗口保留最近 N 批的正常特征每批采样相同数量保证分布均衡。更新频率看产线换型周期一般每周一次。4.4 现象推理速度从 30ms 涨到 300ms原因记忆库太大最近邻搜索变成瓶颈。解决先降维128 维够用再控制记忆库大小在 5 万条以内。如果还不够用 faiss 的 IVF 索引把搜索复杂度从 O(M) 降到 O(sqrt(M))。实测 5 万条 128 维faiss 单张查询约 3msnumpy 暴力算约 40ms。4.5 现象同一张图两次推理分数不一样原因模型没切 eval 模式BN 层还在用 batch 统计量。解决推理前必须model.eval()并且torch.no_grad()。这个坑很隐蔽因为 ResNet 冻结参数后看起来“没训练”但 BN 的 running_mean 在 train 模式下依然会变。加上这两行分数就稳定了。5. 把阈值定准从分位数法到自适应阈值的进阶技巧阈值定不好前面所有工作都白搭。我见过太多人用 0.5 当默认阈值结果产线要么全放行要么全报警。正确做法是拿一批正常验证集至少 50 张算每张图的最高异常分数取 99% 分位数作为初始阈值。这样理论上正常样本误报率控制在 1%。但产线往往要求更低比如 0.1%那就取 99.9% 分位数。代价是漏检会上升需要根据业务容忍度调。# 用正常验证集定阈值 normal_scores [] for img in normal_val_loader: feat extractor(img) score_map anomaly_score(feat, memory_bank, proj) normal_scores.append(score_map.max()) threshold np.percentile(normal_scores, 99.9) print(f建议阈值: {threshold:.4f})如果产线有少量已知缺陷样本可以画 ROC 曲线找最佳平衡点。但注意缺陷样本不能参与记忆库构建只能用于验证。另外阈值不是一劳永逸的换批次、换光源、换镜头都要重新校准。我的习惯是每周一早上跑一次正常样本看分数分布有没有漂移如果 99% 分位数比上周高了 20% 以上就触发记忆库更新。还有一个进阶技巧对分数图做形态学开运算去掉孤立的高分噪点。产线图像常有灰尘或反光单个像素的高分不代表真缺陷。用 3×3 核做一次开运算误报能再降 30% 左右。这个操作在 OpenCV 里一行代码但效果立竿见影。最后说个血泪教训别在测试集上调阈值。我早期为了指标好看在测试集上反复调分位数结果上线后误报爆炸。正确做法是留一个独立的校准集和测试集完全隔离。这个习惯让我后来少加了很多班。希望帮到你。本文还有配套的精品资源点击获取
返回列表