简介:这份PDF文档面向从事机器学习、深度学习与数据建模的研究者与工程人员,聚焦异常检测中自编码器易过拟合、误报率偏高的痛点,提出一种基于ResNet深度神经网络的检测模型。资源包共1个文件,为1.59MB的PDF论文,内容涵盖模型原理、训练与测试流程及实验验证,便于读者系统研读与复现思路。文档以固定切分规则将数据分为A、B两部分,训练网络学习A到B的映射,并引入L2正则化与代价函数抑制过拟合,测试时通过输出误差与阈值判定正常与异常;同时采用ResNet残差层缓解梯度消失,在KDDCup99数据集上取得较好检测率与较低误报率。目前已有181人学习,适合希望深入理解深度神经网络异常检测建模、改进误报问题的读者参考借鉴。
1. 基于 ResNet 的异常检测:为什么工业质检场景都在往这条路上挤
产线上每分钟流过几百个零件,质检工盯着屏幕找划痕、凹坑、脏污,眼睛看花了漏检率就往上飙。用传统图像处理做异常检测,规则写了几百条,换个光照条件全部推倒重来。这几年越来越多的团队转向基于 ResNet 深度神经网络的异常检测模型,核心逻辑很简单:ResNet 在大规模数据集上预训练出来的特征提取能力,能捕捉到正常样本的深层纹理和结构规律,一旦输入偏离这个规律,就判定为异常。它解决的不是“分类”问题,而是“这玩意儿跟正常长得不一样”的问题。适合谁?手头有几百张正常样本、标注异常样本成本极高、产线换型频繁的工业视觉团队。如果你手里已经有一批正常品图片,想快速搭一个能跑起来的异常检测基线,ResNet 这条路是目前工程落地性价比最高的选择之一。
2. 为什么 ResNet 能扛异常检测:从残差结构到特征分布建模
2.1 残差连接解决了什么问题
ResNet 最核心的设计是残差块。普通卷积网络堆到几十层,梯度在反向传播时连乘衰减,浅层参数几乎收不到有效更新信号,训练损失降不下去。残差块把输入直接加到输出上,梯度有一条恒等映射的“高速公路”可以回传,深层网络的训练才变得可行。
对于异常检测来说,这个特性意味着你可以用更深的网络提取更有判别力的特征。浅层卷积核看到的是边缘、角点、颜色块,深层看到的是纹理模式、部件结构。异常往往不体现在单个像素上,而是体现在深层特征的分布偏移上。ResNet-18 到 ResNet-50 是异常检测里最常用的几个骨干,再深收益递减,推理延迟也扛不住产线节拍。
2.2 异常检测为什么不用分类网络直接做
有人会问:我直接训一个二分类网络,正常 vs 异常,不就行了?问题出在异常样本的获取上。产线上异常千奇百怪,划痕有横有竖有深有浅,凹坑有大有小,你不可能穷举所有异常类型去标注。而且异常样本天然稀少,正负样本极度不平衡,分类网络会偏向多数类,把异常全判成正常。
基于 ResNet 的异常检测主流做法是只学正常样本的分布。训练阶段只用正常图片,让网络学会重建或预测正常特征。推理阶段,异常区域的特征偏离了正常分布,重建误差或特征距离就会变大,超过阈值就报警。这条路绕开了异常标注的问题,工程上可操作性更强。
2.3 特征提取层的选择策略
ResNet 不同层输出的特征图分辨率不同,语义层次也不同。layer1 输出分辨率高但语义弱,layer4 语义强但空间分辨率低。异常检测通常需要兼顾定位精度和判别能力,常见做法是取 layer2 和 layer3 的输出做多尺度融合。
具体选哪几层,取决于你的异常尺寸。划痕这种细长异常,在浅层高分辨率特征上更明显;大面积脏污,深层特征就能捕捉到。我一般会先用 layer2+layer3 跑一版基线,看漏检集中在哪类缺陷,再决定要不要加 layer1 或 layer4。
import torch import torchvision.models as models # 加载预训练 ResNet-18,去掉最后的全连接层 resnet = models.resnet18(pretrained=True) backbone = torch.nn.Sequential(*list(resnet.children())[:-2]) # 提取多尺度特征 def extract_features(x): features = [] for name, module in backbone.named_children(): x = module(x) # 取 layer2 和 layer3 的输出 if name in ['layer2', 'layer3']: features.append(x) return features dummy_input = torch.randn(1, 3, 256, 256) feats = extract_features(dummy_input) for i, f in enumerate(feats): print(f"特征层 {i}: 形状 {f.shape}")上面代码把 ResNet-18 的前面层拿出来做特征提取器,list(resnet.children())[:-2]去掉了全局平均池化和全连接层,保留卷积部分。named_children()遍历每个大块,在 layer2 和 layer3 后面收集输出。参数上,输入尺寸 256×256 是工业场景常用值,太小会丢失小缺陷细节,太大推理耗时翻倍。pretrained=True加载 ImageNet 预训练权重,这是小样本场景能训起来的关键。
2.4 正常特征分布怎么建模
拿到多尺度特征后,需要为正常样本建立一个分布模型。最简单的方式是高斯分布:对每个空间位置的特征向量,统计训练集正常样本的均值和协方差。推理时算马氏距离,距离大的位置判为异常。
更复杂的做法是用归一化流或自编码器来拟合特征分布,但工程落地时高斯假设已经能覆盖大部分场景。关键是特征要先做降维,ResNet 输出的通道数动辄 256、512,直接算协方差矩阵维度爆炸。常见做法是用 PCA 降到 50-100 维,再建模。
import numpy as np from sklearn.decomposition import PCA # 假设正常样本特征已提取,形状为 [N, C, H, W] # 展平空间维度,对每个位置独立建模 def fit_gaussian(features, n_components=64): N, C, H, W = features.shape feats_flat = features.transpose(0, 2, 3, 1).reshape(-1, C) # PCA 降维 pca = PCA(n_components=n_components) feats_pca = pca.fit_transform(feats_flat) # 统计均值和协方差 mean = np.mean(feats_pca, axis=0) cov = np.cov(feats_pca, rowvar=False) cov_inv = np.linalg.inv(cov + np.eye(n_components) * 1e-6) return pca, mean, cov_inv # 推理时计算马氏距离 def anomaly_score(feature, pca, mean, cov_inv): feat_pca = pca.transform(feature.reshape(-1, feature.shape[1])) diff = feat_pca - mean dist = np.sum(diff @ cov_inv * diff, axis=1) return dist.reshape(feature.shape[2], feature.shape[3])这段代码做了三件事:PCA 降维、统计正常分布参数、推理时算马氏距离。n_components=64是经验值,太小会丢失判别信息,太大协方差矩阵估计不准。cov + np.eye(n_components) * 1e-6是正则化项,防止协方差矩阵奇异导致求逆失败。异常分数图的分辨率是 H×W,需要上采样回原图尺寸才能做像素级定位。
3. 从零搭一个可用的 ResNet 异常检测流程
3.1 数据准备与增强策略
工业场景的数据集通常长这样:一个文件夹全是正常样本,另一个文件夹是待测图片。训练阶段只用正常样本,验证阶段用带标注的异常图片评估指标。
数据增强要克制。翻转、小角度旋转、亮度微调可以用,但别用随机裁剪和大幅缩放,否则正常样本的分布会被破坏,模型学到的“正常”边界变得模糊。我一般用 torchvision 的 Compose 串几个轻量增强:
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=5), transforms.ColorJitter(brightness=0.1, contrast=0.1), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])Resize统一尺寸,RandomHorizontalFlip和RandomRotation增加样本多样性,ColorJitter模拟光照波动。Normalize用的 ImageNet 统计量,因为骨干网络是在 ImageNet 上预训练的,输入分布要匹配。注意旋转角度别超过 10 度,工业零件的方向通常有语义,转多了正常样本就变成异常了。
3.2 特征提取与降维的工程细节
实际跑的时候,不会每张图都单独提特征再拼起来。批量推理效率更高,但显存占用要算好。ResNet-18 在 256×256 输入下,batch size 32 大概占 4-6GB 显存,具体看 layer2 和 layer3 的输出有没有保留梯度。推理阶段用torch.no_grad()能省不少。
特征图的空间尺寸也要注意。256×256 输入经过 ResNet-18 的 layer2 后是 32×32,layer3 后是 16×16。如果缺陷在 layer3 上只占一两个像素,定位精度会很差。这时候要么上采样 layer3 的特征跟 layer2 融合,要么直接用 layer2 做定位、layer3 做判别。
import torch.nn.functional as F def fuse_features(feat_low, feat_high): # feat_low: layer2 输出 [B, C1, H1, W1] # feat_high: layer3 输出 [B, C2, H2, W2] feat_high_up = F.interpolate(feat_high, size=feat_low.shape[2:], mode='bilinear', align_corners=False) # 通道维度拼接 fused = torch.cat([feat_low, feat_high_up], dim=1) return fusedF.interpolate把 layer3 的特征图双线性上采样到跟 layer2 一样大,align_corners=False是 PyTorch 里常用的对齐方式,避免边缘像素偏移。拼接后通道数变成 C1+C2,后续 PCA 降维时计算量会大一些,但定位精度明显提升。
3.3 阈值设定与评估指标
异常分数算出来后,需要一个阈值来判定正常还是异常。阈值设高了漏检,设低了误报。常见做法是在验证集上画 ROC 曲线,取 F1 分数最大的点作为阈值。如果产线对漏检容忍度极低,可以适当降低阈值,牺牲一点误报率。
评估指标别只看准确率。异常检测里正常样本占绝大多数,全判正常也能有 95% 以上的准确率,但漏检全漏了。要看 AUROC、PRO 曲线,以及固定误报率下的召回率。工业场景常用的是 PRO(Per-Region Overlap),它衡量的是预测的异常区域跟真实缺陷区域的重叠程度,比像素级 AUROC 更贴近实际需求。
from sklearn.metrics import roc_auc_score, f1_score import numpy as np def find_best_threshold(scores, labels): # scores: 异常分数,一维数组 # labels: 真实标签,0 正常 1 异常 thresholds = np.percentile(scores, np.arange(50, 100, 0.5)) best_f1 = 0 best_th = thresholds[0] for th in thresholds: preds = (scores >= th).astype(int) f1 = f1_score(labels, preds) if f1 > best_f1: best_f1 = f1 best_th = th auroc = roc_auc_score(labels, scores) print(f"AUROC: {auroc:.4f}, 最佳阈值: {best_th:.4f}, F1: {best_f1:.4f}") return best_thnp.percentile从 50 到 100 百分位取一系列候选阈值,逐个算 F1 取最大。roc_auc_score直接给 AUROC,不用自己实现。实际调的时候,如果发现 AUROC 很高但 F1 很低,说明阈值没选好,或者异常分数在正常样本上的分布拖尾太长,需要检查特征建模环节。
4. 避坑指南:ResNet 异常检测翻车现场实录
4.1 预训练权重加载失败导致特征全乱
现象:训练 loss 正常下降,但验证集上异常分数跟随机猜差不多,AUROC 只有 0.5 左右。
原因:pretrained=True在某些 torchvision 版本里会尝试联网下载权重,产线环境没外网就静默失败,骨干网络用的是随机初始化权重。随机卷积核提取的特征没有语义,正常和异常的分布区分不开。
解决:提前把预训练权重文件下载好放到本地,用model.load_state_dict(torch.load('resnet18.pth'))手动加载。加载后打印几层权重的均值方差,确认不是随机初始化的数值范围。
4.2 特征图尺寸不匹配导致拼接报错
现象:多尺度融合时torch.cat报维度错误,提示空间尺寸不一致。
原因:ResNet 不同层的下采样倍数不同,layer2 是 1/8 原图,layer3 是 1/16。如果输入尺寸不是 32 的整数倍,经过多次下采样后尺寸会出现奇数,上采样回来对不齐。
解决:输入尺寸统一 resize 到 256×256 或 288×288 这种能被 32 整除的尺寸。如果必须用原始尺寸,在融合前用F.interpolate显式对齐到同一个尺寸,别依赖自动广播。
4.3 协方差矩阵奇异导致马氏距离爆炸
现象:推理时异常分数出现 NaN 或极大值,阈值完全没法设。
原因:PCA 降维后的维度还是偏高,而训练样本数量不够,协方差矩阵不满秩,求逆时数值不稳定。
解决:两个方向。一是降维力度加大,n_components从 64 降到 32 甚至 16;二是在协方差矩阵上加正则项,cov + np.eye(n) * 1e-6,这个 1e-6 可以适当调大,比如 1e-4,牺牲一点精度换数值稳定。
4.4 正常样本里混入异常导致阈值偏移
现象:验证集指标还行,上线后误报率飙升,大量正常品被判定为异常。
原因:训练集里的“正常样本”不纯,混入了几张带轻微缺陷的图片。模型把这些缺陷也学成了正常分布的一部分,导致正常分布的边界被撑大,真正的异常反而落在边界内。
解决:训练前做一轮数据清洗。用初始模型对训练集打分,把分数最高的几张图拿出来人工复核,确认是正常还是异常。如果是异常,从训练集剔除。这个步骤迭代两三次,训练集纯度能明显提升。
4.5 推理速度跟不上产线节拍
现象:单张图推理耗时超过 200ms,产线要求 50ms 以内。
原因:ResNet-50 比 ResNet-18 慢一倍多,加上多尺度特征提取和 PCA 变换,累计延迟超标。
解决:换轻量骨干,ResNet-18 甚至 ResNet-10 在多数工业缺陷检测任务上够用。PCA 变换可以提前算好投影矩阵,推理时只做矩阵乘法。另外把模型转成 ONNX 或 TensorRT,推理速度能再提 30%-50%。
5. 把异常检测模型推到产线:阈值自适应与增量更新
模型在实验室跑通只是第一步,推到产线才是真正的考验。产线环境的光照会漂移,镜头会积灰,零件批次之间会有色差。今天调好的阈值,下周可能就误报一片。我一般会在产线部署时加两个机制:阈值自适应和增量更新。
阈值自适应是指模型不依赖固定阈值,而是根据最近一段时间的正常样本分数分布动态调整。具体做法是维护一个滑动窗口,存最近 1000 张判定为正常的图片的异常分数,取 99 分位数作为当前阈值。这样光照整体变暗导致分数整体上移时,阈值跟着上移,不会集体误报。
from collections import deque import numpy as np class AdaptiveThreshold: def __init__(self, window_size=1000, percentile=99): self.scores = deque(maxlen=window_size) self.percentile = percentile def update(self, score, is_normal): # 只把判定为正常的分数加入窗口 if is_normal: self.scores.append(score) def get_threshold(self): if len(self.scores) < 100: return None # 样本不够,用默认阈值 return np.percentile(list(self.scores), self.percentile)deque是固定长度的双端队列,新分数进来旧分数自动弹出。is_normal由当前阈值判定,这里有个冷启动问题:前 100 张没有阈值可用,我一般先用实验室验证集上算出的阈值顶着,等窗口填满再切换。percentile=99意味着允许 1% 的正常样本被误判,这个比例根据产线容忍度调。
增量更新是指模型上线后,定期用新收集的正常样本微调特征分布参数。不用重新训练整个网络,只需要更新 PCA 的投影矩阵和高斯分布的均值和协方差。频率不用太高,一周一次或者换批次时做一次就行。更新前记得备份旧参数,万一新参数导致误报飙升,能快速回滚。
还有一个血泪经验:产线部署时一定要加一个“疑似样本暂存”功能。模型判定为异常的图片,不要直接丢弃,存到一个单独文件夹,每天人工复核一遍。复核结果可以用来评估模型真实表现,也能作为后续增量更新的数据来源。我见过太多团队模型上线后就不管了,三个月后误报率翻了三倍才发现光照早就变了。
最后说一个验证技巧:别只看 AUROC 这种整体指标,把验证集按缺陷类型分组,分别算召回率。划痕召回 95% 但凹坑召回只有 60%,整体 AUROC 可能还是有 0.9,但凹坑漏检在产线上就是批量事故。分组评估能帮你定位到具体哪类缺陷需要补样本或者调特征层。希望帮到你。
本文还有配套的精品资源,点击获取