☰
基于ResNet的工业异常检测:从特征提取到归一化流实战
2026/10/5 7:05:10 网站建设 项目流程

简介:这份PDF文档面向从事机器学习、深度学习与数据建模的研究者及工程人员,聚焦异常检测中自编码器易过拟合、误报率偏高的痛点,提出一种基于ResNet深度神经网络的检测模型。资源包共1个文件,为1.59MB的PDF论文,完整呈现模型设计思路与实验验证过程。文档以固定切分规则将数据分为A、B两部分,训练网络学习A到B的映射,并引入L2正则化与相应代价函数抑制过拟合,测试时通过输出误差与阈值划分正常与异常,同时借助ResNet残差层缓解梯度消失。文中还结合KDDCup99数据集给出检测率与误报率结果,并围绕神经网络、深度学习、ResNet、代价函数等概念展开论述,适合需要理解异常检测建模方法、复现实验或撰写相关论文的读者参考。目前已有181人学习。

1. 基于ResNet的异常检测:为什么工业质检都在往这条路走

产线上每分钟流过几百个零件,质检员盯着屏幕找划痕、凹坑、脏污,漏检率随疲劳曲线上升。传统做法是用 OpenCV 做阈值分割、边缘检测,规则写了几百行,换个光照条件就集体翻车。这两年越来越多的团队转向基于 ResNet 深度神经网络的异常检测模型,核心逻辑很简单:用在大规模数据集上预训练好的 ResNet 当特征提取器,把正常样本的特征分布学出来,测试时凡是偏离这个分布的就判为异常。它解决的是“正常样本多、异常样本少甚至没有”这个工业场景里最头疼的问题,适合有少量正常图就能开工、不想标注大量缺陷样本的团队。下面从原理到代码到踩坑,把这条路走通。

2. 为什么选ResNet做特征提取而不是自己搭CNN

2.1 残差连接解决了异常检测里的梯度消失

异常检测模型通常需要在特征空间里做距离度量或密度估计,这要求特征提取网络足够深才能捕捉到细微的纹理差异。但深层网络在反向传播时梯度容易消失,浅层参数更新不动。ResNet 的残差块把输入直接加到输出上,梯度可以走捷径回传,训练深层网络变得可行。在异常检测里,这意味着你可以用 ResNet-18 甚至 ResNet-50 作为骨干,而不必担心底层卷积核学不到东西。

另一个实际好处是:ResNet 的层级特征天然适合多尺度异常检测。浅层特征分辨率高,对边缘、纹理敏感,适合检测细小划痕;深层特征语义强,对结构性异常敏感,适合检测缺失、错位。很多异常检测方法会把 ResNet 的中间层输出拼接起来做多尺度融合,这不是拍脑袋,是因为不同层的感受野和语义粒度确实互补。

2.2 预训练权重让少样本场景直接可用

工业场景里正常样本可能只有几十张,从头训练一个 CNN 根本不现实。ResNet 在 ImageNet 上预训练后,卷积核已经学会了提取边缘、角点、纹理这些通用特征。你只需要冻结前面几层,用正常样本微调后面几层,或者干脆不训练,直接把预训练特征拿来做分布估计。常见做法是:用 ResNet 的前几个 stage 输出作为特征图,对每个空间位置的特征向量建立高斯分布,测试时算马氏距离。这个方法在 MVTec AD 数据集上已经验证过,很多类别的 AUROC 能到 90% 以上。

注意:预训练权重是在自然图像上学的,工业图像如果是灰度图或特殊成像(比如 X 光),直接迁移效果会打折。这时候要么把单通道复制成三通道,要么在预训练权重基础上用正常样本做少量微调。

2.3 用ResNet-18提取多尺度特征的最小代码

下面这段代码展示如何加载预训练 ResNet-18,提取四个 stage 的特征图,并做全局平均池化得到特征向量。这是后续建立正常分布的基础。

import torch import torchvision.models as models import torch.nn as nn # 加载预训练ResNet-18,不加载分类头 resnet = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 去掉最后的全连接层和平均池化层,保留卷积部分 backbone = nn.Sequential(*list(resnet.children())[:-2]) # 输入一张正常样本图像,假设已经resize到224x224 x = torch.randn(1, 3, 224, 224) features = [] # 手动前向,在特定层收集输出 for name, module in backbone.named_children(): x = module(x) if name in ['layer1', 'layer2', 'layer3', 'layer4']: features.append(x) # 对每个stage的特征图做全局平均池化,得到特征向量 for i, f in enumerate(features): pooled = torch.nn.functional.adaptive_avg_pool2d(f, 1).squeeze() print(f"Stage {i+1} feature shape: {f.shape}, pooled dim: {pooled.shape}")

逻辑说明:list(resnet.children())[:-2]去掉了原始 ResNet 最后的平均池化和全连接层,保留从 conv1 到 layer4 的所有卷积结构。手动遍历named_children()是为了在 layer1 到 layer4 的输出处截取特征图。adaptive_avg_pool2d把每个 stage 的 H×W 特征图压成一个向量,四个 stage 的通道数分别是 64、128、256、512,拼接起来就是 960 维的特征。参数方面,输入尺寸 224×224 是 ImageNet 标准,工业图像如果长宽比差异大,可以改成 256×256 或保持原比例 padding。

3. 用正常样本建立特征分布:高斯建模与马氏距离

3.1 为什么用马氏距离而不是欧氏距离

欧氏距离把所有维度同等看待,但 ResNet 提取的特征在不同通道上的方差差异很大。有些通道响应强烈,有些几乎为零。马氏距离用协方差矩阵做归一化,能自动抑制方差大的噪声维度,放大方差小但区分度高的维度。在异常检测里,正常样本的特征分布通常可以用多元高斯分布近似,马氏距离就是在这个分布下的标准化距离。

具体做法:对每个空间位置 (i,j),收集所有正常样本在该位置的特征向量,计算均值 μ_ij 和协方差 Σ_ij。测试时,对每个位置算马氏距离,得到一张异常分数图,取最大值或高分位数作为图像级异常分数。这个方法的假设是:正常样本在每个位置的特征服从高斯分布,异常样本偏离这个分布。

3.2 协方差估计的正则化与数值稳定

实际写代码时,协方差矩阵求逆容易出问题。特征维度 960,正常样本可能只有几十张,协方差矩阵秩不足,直接求逆会报错或得到数值爆炸的结果。常见做法是加一个正则化项:Σ' = Σ + εI,ε 取 0.01 或 0.001。另一个做法是降维,先用 PCA 把 960 维降到 100 维左右,再估计协方差。降维还能减少计算量,马氏距离的计算复杂度从 O(d^3) 降到 O(k^3)。

提示:如果正常样本数量少于特征维度,必须做降维或正则化,否则协方差矩阵不可逆。我一般先用 PCA 保留 95% 方差,再算马氏距离。

3.3 完整可复现的异常检测流程代码

下面代码展示从正常样本集提取特征、PCA降维、计算均值和协方差、到测试图像异常分数计算的完整流程。

import numpy as np from sklearn.decomposition import PCA from scipy.spatial.distance import mahalanobis # 假设 normal_features 是 N 张正常图像提取的特征,形状 (N, 960) # 这里用随机数据模拟 N = 50 normal_features = np.random.randn(N, 960) # 第一步:PCA降维,保留95%方差 pca = PCA(n_components=0.95) normal_pca = pca.fit_transform(normal_features) print(f"PCA降维后维度: {normal_pca.shape[1]}") # 第二步:计算均值和协方差 mu = np.mean(normal_pca, axis=0) cov = np.cov(normal_pca, rowvar=False) # 正则化,防止协方差矩阵奇异 cov_reg = cov + np.eye(cov.shape[0]) * 0.01 cov_inv = np.linalg.inv(cov_reg) # 第三步:对测试样本计算马氏距离 test_feature = np.random.randn(1, 960) test_pca = pca.transform(test_feature) dist = mahalanobis(test_pca[0], mu, cov_inv) print(f"测试样本马氏距离: {dist:.4f}") # 第四步:设定阈值,通常用正常样本距离的99分位数 normal_dists = [mahalanobis(f, mu, cov_inv) for f in normal_pca] threshold = np.percentile(normal_dists, 99) print(f"异常判定阈值: {threshold:.4f}")

逻辑说明:PCA(n_components=0.95)自动选择保留 95% 方差所需的主成分数,避免手动调参。np.cov计算协方差矩阵,cov + eye*0.01是正则化,保证可逆。mahalanobis函数直接算马氏距离。阈值用正常样本距离的 99 分位数,意味着正常样本中 1% 会被误判为异常,这个比例可以根据漏检和误检的代价调整。参数方面,PCA 的方差保留比例可以改成 0.99 提高精度但增加维度,正则化系数 0.01 如果数据量足够可以降到 0.001。

4. 训练策略与数据增强:让模型见到足够多的正常变化

4.1 冻结骨干还是微调:取决于正常样本数量

如果正常样本少于 100 张,建议完全冻结 ResNet 的卷积层,只把它当特征提取器用,不做任何反向传播。这时候模型不会过拟合,因为参数根本没更新。如果正常样本有几百张,可以解冻 layer4 做微调,学习率设小一点,比如 1e-4。微调的目的是让高层特征更适应你的数据分布,比如工业图像的纹理和自然图像差异大,微调后特征区分度更高。

但微调有个风险:如果正常样本里混入了少量异常,模型会把异常特征也学进去,导致漏检。所以微调前一定要做数据清洗,确保训练集里没有异常样本。我一般会先用冻结特征跑一遍,看异常分数分布,把分数明显偏高的样本挑出来人工复核。

4.2 数据增强要模拟真实产线变化

工业场景里光照变化、零件旋转、相机抖动都会影响成像。数据增强要覆盖这些变化,但不能引入异常。常见做法:随机亮度调整 ±20%、随机对比度调整 ±15%、小角度旋转 ±10 度、随机裁剪后 resize 回原尺寸。不要用水平翻转,因为很多工业零件翻转后语义变了,比如文字、缺口方向。

注意:增强后的图像仍然要保证是正常样本。如果增强操作可能把正常样本变成异常(比如过度裁剪导致零件不完整),那这个增强就不能用。

4.3 用增强数据重新估计分布的代码

下面代码展示如何在训练时对正常样本做增强,提取特征后重新计算均值和协方差。

import torchvision.transforms as T from PIL import Image # 定义增强管道 train_transform = T.Compose([ T.Resize((256, 256)), T.RandomCrop(224), T.ColorJitter(brightness=0.2, contrast=0.15), T.RandomRotation(10), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 假设 normal_images 是正常样本PIL图像列表 augmented_features = [] for img in normal_images: for _ in range(5): # 每张图增强5次 aug_img = train_transform(img) # 提取特征,这里用前面定义的backbone with torch.no_grad(): feat = backbone(aug_img.unsqueeze(0)) feat = torch.nn.functional.adaptive_avg_pool2d(feat, 1).squeeze() augmented_features.append(feat.numpy()) augmented_features = np.array(augmented_features) print(f"增强后特征数量: {augmented_features.shape[0]}") # 后续用 augmented_features 重新计算均值和协方差

逻辑说明:RandomCrop(224)在 256×256 的图上随机裁 224×224,模拟零件位置偏移。ColorJitter调整亮度和对比度,模拟光照变化。RandomRotation(10)模拟零件旋转。每张图增强 5 次,50 张正常图就能得到 250 个特征向量,足够估计协方差。参数方面,增强次数可以增加到 10 次,但要注意计算量。Normalize的均值和标准差是 ImageNet 的标准值,如果微调了骨干,应该用自己数据集的统计量。

5. 避坑与排查:异常检测模型翻车的五个血泪教训

5.1 现象:正常样本被大量判为异常,误检率超过 30%

原因:正常样本的多样性不足,模型没见过某种光照或角度的正常图,把没见过的情况当成了异常。解决:增加正常样本的采集覆盖度,或者在训练时用更强的数据增强模拟未见过的情况。我一般会先可视化异常分数最高的几张正常图,看它们和训练集的差异在哪里,然后针对性补数据。

5.2 现象:异常样本漏检,缺陷区域分数和正常区域差不多

原因:缺陷太小,ResNet 深层特征的空间分辨率太低,小缺陷在特征图上被平均掉了。解决:用浅层特征做异常检测,或者用特征金字塔把浅层和深层特征融合。另一个做法是提高输入分辨率,比如从 224 改成 448,但计算量会翻四倍。

5.3 现象:模型在验证集上表现很好,上线后效果急剧下降

原因:验证集和训练集来自同一批次数据,分布一致,但上线后光照、相机参数、零件批次变了。解决:上线前用不同时间段、不同批次的数据做测试,确保模型对分布偏移鲁棒。如果偏移不可避免,要定期用新正常样本更新均值和协方差,或者做在线自适应。

5.4 现象:协方差矩阵求逆时报 LinAlgError: Singular matrix

原因:正常样本数量少于特征维度,协方差矩阵秩不足。解决:加正则化项,或者先做 PCA 降维。我一般先用 PCA 保留 95% 方差,如果还报错就把正则化系数从 0.01 调到 0.1。另一个办法是用伪逆np.linalg.pinv代替inv,但计算更慢。

5.5 现象:GPU 显存不够,batch size 只能设 1

原因:ResNet-50 加上高分辨率输入,显存占用大。解决:换 ResNet-18,或者用混合精度训练,或者把特征提取和分布估计分开做——先离线提取所有正常样本的特征存到磁盘,训练时只加载特征向量,不加载图像。这样显存占用几乎为零。

6. 进阶技巧:用归一化流替代高斯假设,提升复杂分布下的检测精度

高斯分布假设在正常样本特征分布比较单峰时有效,但如果正常样本本身就有多种模式,比如不同型号的零件混在同一条产线上,单峰高斯就拟合不了。这时候可以用归一化流(Normalizing Flow)学习更复杂的分布。归一化流把特征向量映射到一个标准正态分布,映射是可逆的,测试时算映射后的概率密度,低密度区域就是异常。

具体做法:在 ResNet 特征后面接一个 RealNVP 或 Glow 模块,用正常样本训练这个流模型,最大化对数似然。测试时,异常样本经过流模型后概率密度低,取负对数似然作为异常分数。这个方法在 MVTec AD 的多个类别上比高斯方法高 2-5 个点 AUROC,但训练时间更长,需要调超参数。

下面是一个简化的 RealNVP 训练代码框架:

import torch.nn.functional as F class RealNVP(nn.Module): def __init__(self, dim): super().__init__() # 定义缩放和平移网络,这里用简单的全连接 self.scale_net = nn.Sequential(nn.Linear(dim, 256), nn.ReLU(), nn.Linear(256, dim)) self.translate_net = nn.Sequential(nn.Linear(dim, 256), nn.ReLU(), nn.Linear(256, dim)) def forward(self, x): # 简化版,只做一次仿射耦合 s = self.scale_net(x) t = self.translate_net(x) z = x * torch.exp(s) + t log_det = torch.sum(s, dim=1) return z, log_det # 训练循环 flow = RealNVP(dim=960) optimizer = torch.optim.Adam(flow.parameters(), lr=1e-3) for epoch in range(100): for batch in normal_feature_loader: z, log_det = flow(batch) # 标准正态分布的对数似然 log_prob = -0.5 * torch.sum(z**2, dim=1) - 0.5 * z.shape[1] * np.log(2*np.pi) loss = -torch.mean(log_prob + log_det) optimizer.zero_grad() loss.backward() optimizer.step()

逻辑说明:scale_net和translate_net输出缩放和平移量,z = x * exp(s) + t是可逆变换。log_det是雅可比行列式的对数,用于计算概率密度。损失函数是负对数似然,训练目标是让正常样本映射后的 z 接近标准正态分布。测试时,异常样本的 z 偏离标准正态,log_prob低,取负值作为异常分数。参数方面,耦合层数可以增加到 4-6 层,每层的隐藏单元数 256 或 512,学习率 1e-3 到 1e-4。

验证方法:在测试集上算正常和异常的分数分布,画 ROC 曲线,看 AUROC。如果 AUROC 低于高斯方法,检查流模型是否过拟合——正常样本太少时流模型会记住训练样本,泛化差。这时候减少耦合层数,或者加权重衰减。

我自己的习惯是:先用高斯方法跑一个基线,如果 AUROC 已经满足产线要求,就不上流模型,因为流模型训练和调参的时间成本高。只有当高斯方法明显不够用,比如正常样本多模态分布,才考虑归一化流。这个判断帮我省了很多无效实验。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询