拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ResNet异常检测模型:A→B映射与多输出代价函数实战

简介这份PDF文档面向从事机器学习、深度学习与数据建模的研究者及工程人员聚焦异常检测中自编码器易过拟合、误报率偏高的痛点提出一种基于ResNet深度神经网络的检测模型。资源包共1个文件为1.59MB的PDF论文内容涵盖模型原理、训练与测试流程及实验验证适合需要深入理解残差网络在入侵检测场景应用的读者研读。文档以固定切分规则将数据分为A、B两部分训练网络学习A到B的映射并引入L2正则化与代价函数抑制过拟合测试时通过输出误差与阈值判定正常与异常同时利用ResNet网络层缓解梯度消失问题。文中还给出KDDCup99数据集上的测试结果表明该方法能取得较好检测率与较低误报率并配有引言、关键词及学术研究背景便于读者把握异常检测、自编码器与ResNet结合的技术脉络。目前已有181人学习可作为相关方向研究选题与模型改进的参考材料。1. 从自编码器的误报泥潭说起为什么这份 ResNet 异常检测模型值得拆做入侵检测的同行大概率都经历过这种场景拿 KDDCup99 或者自家流量日志训了一个自编码器训练 loss 压得很低一上测试集正常流量被标成攻击的比例高得离谱。误报率动辄三四个点运维那边一天收到几百条告警最后没人看。根子在于自编码器要求网络对正常数据“输入等于输出”这个约束太强网络参数一多就容易把训练集背下来泛化能力反而差。这份《基于ResNet深度神经网络的异常检测模型》给的思路不一样它不要求重构整条数据而是把每条数据按固定规则切成 A、B 两段让网络学 A 到 B 的映射。正常数据里 A 和 B 的对应关系是稳定的异常数据里这个关系会被打破误差自然就大。再叠加 ResNet 残差层解决深层网络的梯度消失用 L2 正则压过拟合最终在 KDDCup99 上把误报率从自编码器的 3.91% 压到 1.00%检测率还从 93.26% 提到 94.5%。这套东西适合谁做网络流量异常检测、工业设备时序异常检测、日志异常识别的工程师只要你手头有带标签的正常数据就能照着复现。2. 模型原理拆解A→B 映射、多输出代价函数与 ResNet 残差层2.1 为什么放弃重构改学 A 到 B 的映射自编码器的逻辑是 $g(f(X)) \approx X$编码器把 X 压到低维再解压回来。问题在于如果正常数据分布没覆盖全遇到分布边缘的正常样本重构误差也会很大直接被误判。论文里点得很清楚自编码器对与训练集相似的数据重构能力好对差异大的数据效果差而异常数据恰恰和正常数据差异大——但问题是某些正常数据也可能差异大。改成 A→B 映射后网络学的是数据内部两部分之间的函数关系 $g(A)B$。正常数据里这个关系是数据本身固有的规律比如网络连接记录里前 82 维的协议、服务、标志等特征和后面 41 维的统计特征之间存在稳定关联。异常数据打破了这个关联所以误差大。这个思路的好处是网络不需要记住整条数据只需要学部分到部分的映射参数利用效率更高L2 正则也更容易起作用。常见做法是直接把 41 维原始特征做切分但论文做了独热编码和归一化后变成 123 维切分点选在 82 维。为什么是 82因为 1-82 维里除了第 1 维是连续数值2-82 维实际是 3 个类别型特征独热编码来的大部分位是 0表达能力远小于 83-123 维的连续数值特征。所以让信息量大的部分做输入信息量小的部分做标签减少一对多映射的歧义。2.2 多输出机制与 NearCost 代价函数即便切分得当A 到 B 仍然可能存在一对多同一个 A 对应几个不同的 B。如果网络只输出一个 B遇到这种样本就会学出一个“平均”结果误差反而大。论文的解法是让网络对每个输入 A 输出 n 个 B 候选n 取 3。但多输出带来一个新问题如果只用最小距离作为代价网络会发现只要 n 个输出里有一个接近真实 B 就行另外两个可以乱跑。对于异常数据n 越大蒙中一个低误差输出的概率越高检测率反而下降。所以论文加了 NearCost强制 n 个输出互相靠拢import torch import torch.nn as nn class AnomalyCost(nn.Module): def __init__(self, n_outputs3, alpha1e-3, beta1e-2): super().__init__() self.n n_outputs self.alpha alpha # NearCost 权重 self.beta beta # L2 正则权重 def forward(self, preds, target): # preds: [batch, n, b_dim], target: [batch, b_dim] # Dist: 每个输出到真实 B 的欧式距离取最小 dist torch.norm(preds - target.unsqueeze(1), dim2) # [batch, n] dist_cost torch.min(dist, dim1)[0].mean() # NearCost: n 个输出两两之间的距离强制靠拢 near_cost 0.0 for i in range(self.n): for j in range(i 1, self.n): near_cost torch.norm(preds[:, i] - preds[:, j], dim1).mean() near_cost near_cost / (self.n * (self.n - 1) / 2) # L2 正则由 optimizer 的 weight_decay 处理这里只算数据项 total dist_cost self.alpha * near_cost return total这段代码里dist_cost取的是 n 个输出中距离真实 B 最近的那个保证至少有一个输出对准目标near_cost把 n 个输出往一起拉防止它们分散后给异常数据可乘之机。alpha控制靠拢强度论文给的范围是 1e-3 到 5e-3beta是 L2 权重取 1e-2。实际调参时如果发现异常检测率偏低先把 alpha 调大如果正常数据误报多把 beta 调大。2.3 ResNet 残差层怎么接进全连接网络论文用了 9 层网络其中 3 个普通全连接层6 个 ResNet 层。ResNet 层的结构是class ResNetBlock(nn.Module): def __init__(self, dim): super().__init__() self.fc nn.Linear(dim, dim) self.act nn.LeakyReLU(0.01) def forward(self, x): residual x out self.fc(x) out self.act(out) return out residual # 残差连接残差连接让梯度可以直接绕过全连接层回传避免深层网络训练时梯度消失。激活函数选 LeakyReLU 而不是 ReLU是因为 ReLU 在负半轴梯度为 0反向传播时这部分参数永远不更新LeakyReLU 给负半轴一个 0.01 的斜率保证梯度有通路。论文里所有 ResNet 层的输入输出维度一致所以可以直接相加不需要额外的投影层。3. 从 KDDCup99 到可运行代码数据预处理、网络搭建与训练全流程3.1 数据预处理独热编码与归一化的具体操作KDDCup99 原始数据有 41 维特征其中 7 维是类别型protocol_type、service、flag 等34 维是连续数值。论文的处理流程是7 个类别型特征做独热编码展开成 82 维34 个连续特征做 min-max 归一化最终拼成 123 维。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 假设 df 是原始 KDDCup99 数据列名按标准数据集 categorical_cols [protocol_type, service, flag, land, logged_in, is_host_login, is_guest_login] numerical_cols [c for c in df.columns if c not in categorical_cols [label]] # 独热编码 df_cat pd.get_dummies(df[categorical_cols], columnscategorical_cols) # 归一化 scaler MinMaxScaler() df_num pd.DataFrame(scaler.fit_transform(df[numerical_cols]), columnsnumerical_cols) # 拼接注意列顺序要和论文一致先数值后类别或反之影响切分点 X pd.concat([df_num, df_cat], axis1).values # shape: [N, 123]这里有个细节论文说 1-82 维做标签83-123 维做输入。但独热编码后类别特征展开的维度顺序取决于pd.get_dummies的列排序。如果你把数值列放前面那 1-34 维是数值35-123 是类别独热切分点就要相应调整。我一般会先把类别独热放在前面数值放后面这样 1-82 维正好是类别独热部分83-123 是数值部分和论文一致。3.2 网络搭建9 层结构的 PyTorch 实现class ResNetAnomalyNet(nn.Module): def __init__(self, input_dim41, hidden_dim60, output_dim123, n_outputs3): super().__init__() self.n n_outputs # 第1层输入全连接 self.fc1 nn.Linear(input_dim, hidden_dim) # 第2-4层ResNet 块 self.res_blocks nn.ModuleList([ResNetBlock(hidden_dim) for _ in range(3)]) # 第5层全连接升维到 123 self.fc5 nn.Linear(hidden_dim, output_dim) # 第6-8层ResNet 块 self.res_blocks2 nn.ModuleList([ResNetBlock(output_dim) for _ in range(3)]) # 第9层输出123 - 246 (n_outputs * 82) self.fc9 nn.Linear(output_dim, n_outputs * 82) self.act nn.LeakyReLU(0.01) def forward(self, x): x self.act(self.fc1(x)) for block in self.res_blocks: x block(x) x self.act(self.fc5(x)) for block in self.res_blocks2: x block(x) x self.fc9(x) # 重塑为 [batch, n_outputs, 82] return x.view(-1, self.n, 82)输入维度是 41因为论文把 83-123 维41 维作为输入。输出是 246 维重塑成 3 个 82 维的 B 候选。注意第 5 层把 60 维升到 123 维是为了和第 6-8 层的 ResNet 块维度匹配。训练时用 RMSProp 优化器学习率默认 0.001weight_decay 设 1e-2 对应 L2 正则。3.3 训练循环与阈值计算model ResNetAnomalyNet().cuda() optimizer torch.optim.RMSprop(model.parameters(), lr1e-3, weight_decay1e-2) criterion AnomalyCost(n_outputs3, alpha1e-3) for epoch in range(150): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.cuda(), batch_y.cuda() preds model(batch_x) loss criterion(preds, batch_y) optimizer.zero_grad() loss.backward() optimizer.step() # 阈值计算用训练数据过网络后的误差均值和标准差 model.eval() train_dists [] with torch.no_grad(): for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.cuda(), batch_y.cuda() preds model(batch_x) dist torch.norm(preds - batch_y.unsqueeze(1), dim2).min(dim1)[0] train_dists.append(dist.cpu()) train_dists torch.cat(train_dists) mean train_dists.mean().item() std train_dists.std().item() threshold mean 1.5 * std阈值取均值加 1.5 倍标准差这是论文实测效果较好的组合。测试时每条数据的误差超过阈值就判为异常。训练 150 轮后停止batch size 设 256和论文一致。如果显存不够可以降到 128 或 64但太小会影响 BatchNorm 效果——不过这个网络没用 BatchNorm所以影响不大。4. 避坑与排查数据切分、阈值选取和训练不收敛的五个血泪经验4.1 切分点选错导致检测率暴跌现象训练 loss 正常下降但测试时检测率只有 70% 左右误报率也高。原因切分点没对齐论文的 82 维。如果你把数值特征放在前面1-34 维是数值35-123 是类别独热此时按 82 切分标签部分混入了大量数值特征输入部分反而信息量不足A→B 映射的歧义增大。解决打印前 10 条数据的各维度取值范围确认 1-82 维里大部分是 0/1 二值83-123 维是连续值。如果不是调整列拼接顺序把类别独热放前面。4.2 阈值取固定值导致误报率失控现象换了测试集后误报率从 1% 跳到 8%。原因阈值写死成训练集算出来的数值但新测试集的误差分布可能整体偏移。论文的阈值是基于训练数据误差的均值和标准差算的如果测试集和训练集分布差异大这个阈值就不适用。解决每次换数据集都重新用当前训练集的误差分布算阈值。如果测试集有少量标注数据可以用 5% 分位数做校准。我一般会保留训练集误差的均值和标准差测试时先跑一遍正常样本看误差分布是否偏移超过 20%超过就重新校准。4.3 NearCost 权重过大导致异常数据也被拉低误差现象检测率上不去异常数据的误差和正常数据差不多。原因alpha设太大n 个输出被强行拉在一起网络学出一个“万能输出”不管输入是什么都输出相似的 B异常数据的误差自然小。解决把alpha从 1e-3 降到 1e-4 试试或者观察训练后期 n 个输出的方差如果方差小于 1e-4说明靠拢过度。论文给的范围是 1e-3 到 5e-3但实际数据不同需要微调。4.4 残差块维度不匹配报错现象RuntimeError: The size of tensor a (60) must match the size of tensor b (123)。原因ResNet 块要求输入输出维度一致才能相加。如果你在第 5 层升维后直接接 ResNet 块但块内部还是 60 维的全连接就会报错。解决升维后重新初始化一组 ResNet 块维度设成升维后的维度。代码里res_blocks2就是专门为 123 维准备的。4.5 训练 150 轮后 loss 还在震荡现象loss 曲线上下跳动没有收敛趋势。原因RMSProp 的学习率设太大或者 batch size 太小导致梯度噪声大。解决先把学习率降到 5e-4如果还震荡检查数据归一化是否到位——KDDCup99 的src_bytes和dst_bytes取值范围跨度极大没归一化的话梯度会爆炸。另外LeakyReLU 的负斜率 0.01 如果改成 0.1训练会更稳定但可能过拟合建议保持 0.01。5. 进阶技巧用误差分布重叠度验证模型上限与调参方向训练完模型后怎么判断还有没有提升空间论文里提了一个很实在的方法构建 k-d 树拿异常数据去正常数据里找完全相同的样本。结果发现有 7593 条异常数据和正常数据完全一样占异常总数的 3.03%反过来有 7273 条正常数据和异常数据完全一样占正常总数的 12%。这意味着即使模型完美这部分重叠数据也无法区分检测率的上限被卡死了。我一般会先跑一遍这个重叠度分析再决定要不要继续调参from scipy.spatial import KDTree # normal_X: 正常数据特征anomaly_X: 异常数据特征 tree_normal KDTree(normal_X) # 对每个异常样本找正常样本里距离为 0 的 dist_anomaly_to_normal, _ tree_normal.query(anomaly_X, k1) overlap_anomaly (dist_anomaly_to_normal 0).sum() tree_anomaly KDTree(anomaly_X) dist_normal_to_anomaly, _ tree_anomaly.query(normal_X, k1) overlap_normal (dist_normal_to_anomaly 0).sum() print(f异常中与正常完全相同的比例: {overlap_anomaly / len(anomaly_X):.2%}) print(f正常中与异常完全相同的比例: {overlap_normal / len(normal_X):.2%})如果重叠比例超过 5%说明数据本身有标注噪声或者特征表达能力不足这时候再调网络结构收益很小应该回头做特征工程或者清洗数据。如果重叠比例低于 1%但检测率还是上不去那才是模型的问题可以尝试加深网络、调整切分点或者换激活函数。另一个实用技巧是画 ROC 曲线对比自编码器。论文里本文模型的 ROC 曲线在误报率 1% 时检测率还有 94% 左右而自编码器在同样误报率下检测率只有 85% 左右。如果你复现出来的曲线和自编码器差不多先检查是不是 L2 正则没生效——PyTorch 的weight_decay只对 optimizer 里的参数起作用如果你手动实现了 L2 但没加到 loss 里正则就是摆设。从那以后我每次复现这类异常检测模型都强制先跑一遍重叠度分析和 ROC 对比确认数据本身的上限和模型的实际增益再决定要不要继续投入时间调参。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门