资讯动态

深度神经网络异常流量检测:从流特征、损失设计到阈值标定

发布时间:2026/9/17 14:06:09 来源:尧图企业网站定制
简介这份资源为《基于深度神经网络的异常流量检测算法》PDF学术论文面向网络安全、机器学习方向的研究生、算法工程师及数据建模学习者聚焦传统流量检测方法难以应对复杂攻击的问题提供可落地的深度学习检测思路。压缩包仅含1个PDF文件约7.65MB完整收录论文正文、算法设计、数据集对比与实验评估等内容便于直接阅读与引用。文中系统梳理了深度神经网络在异常流量检测中的应用涵盖ISCX数据集的攻击与协议类型特点、与朴素贝叶斯算法的准确率及误报率对比实验并延伸讨论随机森林、支持向量机等机器学习方法以及DDoS、恶意软件、植入式攻击等网络攻击类型与入侵检测、恶意软件检测、IoT及云计算等应用场景。目前已有204人学习适合需要快速掌握该领域技术路线、撰写综述或复现实验的读者参考。1. 异常流量检测为什么绕不开深度神经网络安全运营中心最常见的画面是签名规则库里躺着几千条规则告警面板每天刷出几万条记录真正该看的那几条被埋在底下。基于深度神经网络的异常流量检测算法要解决的就是这件事——把一条流量记录变成定长向量交给多层非线性变换去学“正常流量长什么样”偏离得足够远就判为异常。它对规则写不全、加密流量看不到载荷、攻击变种更新快的场景更有价值。先摆正工程量分布流量没有天然的向量形态得先刻画成统计量标签极度不平衡正常样本常常占九成以上上线之后流量分布还会漂移这个月标定好的阈值下个月未必够用。难点集中在特征口径、损失设计和阈值标定三处网络结构反而排在后面。下面的路线是先整理流特征再选匹配数据规模的网络接着把训练与评估口径定死最后处理阈值和漂移。2. 从 pcap 到张量异常流量检测的特征口径与数据切分2.1 特征口径决定模型上限而不是网络深度深度神经网络在表格型数据上并不天然占优梯度提升树经常能打平甚至超过同规模的 MLP。真正让神经网络在异常流量检测里站住脚的是它能吃下高维、连续、带时序关系的特征并且可以做成多任务或者自监督预训练。前提是特征本身有判别力。我一般按三层组织流特征第一层是流量体积与速率比如持续时长、双向字节数、包数、每秒包数第二层是分布形态比如包长均值、包长标准差、到达间隔IAT的均值与最大值第三层是协议与行为标志比如协议号、TCP 标志位组合、端口分桶、双向字节比。第一层和第二层负责刻画“这条流有多反常”第三层负责给出“它是哪一类反常”的上下文。特征名含义类型处理方式flow_duration流持续时长毫秒数值log1p 后标准化fwd_packet_count正向包数数值log1p 后标准化byte_ratio双向字节数之比数值裁剪到 [0, 10] 再标准化pkt_len_std包长标准差数值直接标准化iat_mean到达间隔均值数值log1p 后标准化protocol协议号类别one-hot 或 embeddingtcp_flags标志位组合类别拆成若干个 0/1 列dst_port_bucket目的端口分桶类别系统端口 / 注册端口 / 动态端口注意最后一行的端口分桶。把 65536 个端口号直接当类别做 one-hot维度会炸掉而且模型会记住“某个端口曾经出现过攻击”换一个环境就失效。按区间分桶之后泛化能力明显更好这是我在多个数据集上反复验证过的做法。2.2 公开数据集之间的字段怎么对齐做异常流量检测的研究或者离线验证绕不开几个公开数据集NSL-KDD、UNSW-NB15、CICIDS 系列这一类。它们的采集年代、拓扑结构、攻击注入方式都不一样字段命名也各说各话。常见做法是先定义一份内部的中间 schema写一个适配层把每个数据集的列映射过来缺的字段用 NaN 占位训练时统一走同一套缺失值填充逻辑。需要提醒的是不同数据集的正常流量基线差别很大。在一个数据集上标定好的阈值搬到另一个数据集上几乎必然失效所以跨数据集的迁移实验要单独做阈值重标否则比较出来的数字没有意义。2.3 特征清洗与标准化的可运行代码import numpy as np import pandas as pd from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer # 数值列先补中位数再做 log1p 压长尾最后标准化 def log1p_clip(x): # 流量特征长尾极重先裁剪到非负再压缩 return np.log1p(np.clip(x, 0, None)) num_cols [flow_duration, fwd_packet_count, byte_ratio, pkt_len_std, iat_mean] cat_cols [protocol, dst_port_bucket] num_pipe Pipeline([ (impute, SimpleImputer(strategymedian)), # 缺失值用中位数填充 (scale, StandardScaler()), # 均值方差标准化 ]) cat_pipe Pipeline([ (impute, SimpleImputer(strategymost_frequent)), (onehot, OneHotEncoder(handle_unknownignore, sparse_outputFalse)), ]) pre ColumnTransformer([ (num, num_pipe, num_cols), (cat, cat_pipe, cat_cols), ]) # 只在训练集上 fit验证集和测试集只 transform避免统计量泄漏 X_train pre.fit_transform(df_train[num_cols cat_cols]) X_val pre.transform(df_val[num_cols cat_cols]) X_test pre.transform(df_test[num_cols cat_cols])这段的关键点有三个。SimpleImputer用中位数而不是均值是因为流量特征的分布左偏严重均值会被少数超大流拖走。handle_unknownignore保证线上出现训练时没见过的协议号时不会直接报错而是编码成全零向量。最重要的一条是fit_transform只作用在训练集上——如果把三个集合拼起来一起 fit标准化用到的均值方差里就混进了验证集的信息实测指标会虚高好几个点上线立刻打回原形。2.4 按时间切分别用随机切分随机切分在异常流量检测里是个隐蔽的坑。同一台主机的一次持续扫描会切出成百上千条流记录随机切分之后训练集和测试集里会同时出现这次扫描的片段。模型看起来准确率很高实际学到的是“认出这次扫描的流量指纹”而不是“识别扫描行为”。正确做法是按采集时间切用前 70% 的时间窗口做训练中间 15% 做验证最后 15% 做测试。df df.sort_values(flow_start_time).reset_index(dropTrue) n len(df) train_end, val_end int(n * 0.7), int(n * 0.85) df_train df.iloc[:train_end] df_val df.iloc[train_end:val_end] df_test df.iloc[val_end:]如果标签本身是按时间累积的还要检查每个切分里的正样本比例。测试段正样本过少的时候指标波动会非常大这时候要拉长测试窗口而不是换个随机种子多跑几遍挑好看的数。3. 用 PyTorch 搭一个可收敛的深度神经网络异常流量检测模型3.1 三条基线路线怎么选动手之前先定路线。异常流量检测里能用的网络结构大致三类选错方向后面调参全是白费力气。路线标签需求适用场景训练成本MLP 二分类需要标注有历史告警可回溯特征已结构化低几分钟一轮自编码器重构只要正常样本标注稀缺只需要“偏离正常”中阈值需单独标定1D-CNN / GRU 时序需要标注有原始包序列或固定长度流序列高需要 GPU绝大多数从零起步的项目应该先做 MLP 二分类把整条链路跑通拿到一个可比较的基线。只有在标注几乎拿不到、或者原始序列里确实藏着统计量表达不出来的模式时才值得上自编码器或时序模型。3.2 Dataset 与 DataLoaderimport torch from torch.utils.data import Dataset, DataLoader class FlowDataset(Dataset): 把已数值化、标准化好的流量特征矩阵包成 Dataset def __init__(self, X, yNone): self.X torch.as_tensor(X, dtypetorch.float32) self.y None if y is None else torch.as_tensor(y, dtypetorch.float32) def __len__(self): return self.X.shape[0] def __getitem__(self, idx): if self.y is None: return self.X[idx] return self.X[idx], self.y[idx] train_loader DataLoader( FlowDataset(X_train, y_train), batch_size512, # 流量样本量级大256~1024 之间收敛都比较稳 shuffleTrue, # 训练集打散避免同类样本连片导致梯度方向偏 num_workers4, # 按机器核数调IO 成瓶颈时可以加到 8 pin_memoryTrue, # 有 GPU 时打开减少主机到显存的拷贝开销 )shuffleTrue在异常流量检测里比一般任务更重要因为按时间切分之后训练集内部仍然是时间有序的连续 batch 之间的样本高度相似不打散会让梯度更新在同一个方向上反复走。pin_memory只有在用 GPU 时才有收益纯 CPU 训练开着反而占内存。3.3 模型定义与损失函数import torch import torch.nn as nn class FlowMLP(nn.Module): def __init__(self, in_dim, hidden(256, 128, 64), dropout0.3): super().__init__() layers, prev [], in_dim for h in hidden: layers [ nn.Linear(prev, h), nn.BatchNorm1d(h), # 流量特征量纲差异大BN 让收敛更稳 nn.ReLU(inplaceTrue), nn.Dropout(dropout), # 0.2~0.5 之间调正样本少时适当加大 ] prev h layers.append(nn.Linear(prev, 1)) # 输出单个 logit配合 BCEWithLogitsLoss self.net nn.Sequential(*layers) def forward(self, x): return self.net(x).squeeze(-1) model FlowMLP(in_dimX_train.shape[1]) # pos_weight 用来抵消类别不平衡值约等于 负样本数 / 正样本数 neg, pos (y_train 0).sum(), (y_train 1).sum() pos_weight torch.tensor([neg / max(pos, 1)], dtypetorch.float32) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4)输出层用单个 logit 而不是加 Sigmoid原因是BCEWithLogitsLoss内部做了数值稳定处理自己先 Sigmoid 再送BCELoss容易在极端概率上炸梯度。pos_weight是最省事的类别不平衡处理方式但它不是越大越好——权重调到几十倍的时候模型会倾向于把所有模糊样本都判成正类精确率会塌掉后面第 4 章会讲怎么选。3.4 训练循环与早停import numpy as np def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total 0.0 for xb, yb in loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total loss.item() * xb.size(0) return total / len(loader.dataset) torch.no_grad() def evaluate(model, loader, device): model.eval() probs, labels [], [] for xb, yb in loader: logits model(xb.to(device)) probs.append(torch.sigmoid(logits).cpu().numpy()) labels.append(yb.numpy()) return np.concatenate(labels), np.concatenate(probs)clip_grad_norm_的max_norm5.0是个经验值。异常流量检测的特征里偶尔会出现极端的速率值即使做过标准化单个 batch 的梯度范数仍可能突然飙到几百不裁剪的话一轮就把权重带偏。早停的标准不要看 loss要看验证集的 PR-AUC原因下一章展开。4. 训练调参与评估异常流量检测算法的必调参数4.1 类别不平衡的四种处理方式方案做法优点代价类权重在损失函数里给正样本加权不动数据改动最小权重过大导致精确率崩塌过采样复制少数类样本实现简单召回直观提升容易过拟合被复制的样本合成采样SMOTE 一类方法生成新样本缓解过拟合合成样本在特征空间未必物理可行Focal Loss对易分样本降权不平衡下表现稳多出两个超参要调我的排序是先试类权重效果不够再上 Focal Loss采样类方法放在最后。原因是流特征的物理约束很强像“包长标准差”这类量在合成之后可能落到现实中不存在的区间模型学到的是合成器的偏置。4.2 关键超参的取值区间超参建议起点调整方向学习率1e-3AdamW验证集 loss 震荡就降到 3e-4batch_size512显存允许就加大梯度更稳dropout0.3训练集指标远高于验证集就往上加隐层宽度(256, 128, 64)特征维度低于 50 时没必要更深pos_weightneg/pos 的 0.5~1 倍精确率掉得快就往下调早停耐心5 个 epoch验证指标波动大就加到 8一个常见的误用是把网络堆得很深。流特征通常只有几十到一百多维三层 MLP 已经能拟合得不错加到八层十层之后训练集指标继续涨验证集指标反而往下掉纯粹是在记训练样本。4.3 指标口径PR-AUC 比 ROC-AUC 诚实正样本占比 1% 的时候ROC-AUC 经常能刷到 0.99 以上看着很漂亮但模型可能只是把所有样本都判成负类再随机挑几个当正类。PR-AUC也就是average_precision_score对少数类敏感得多是异常流量检测里更该盯的指标。汇报的时候至少要给出精确率、召回率、F1 和 PR-AUC 四个数并且明确阈值是多少——脱离阈值的精确率和召回率没有意义。4.4 阈值扫描代码from sklearn.metrics import precision_recall_curve, average_precision_score def pick_threshold(y_true, y_prob, min_precision0.8): 在满足最低精确率的前提下取召回率最高的那个阈值 precision, recall, thresholds precision_recall_curve(y_true, y_prob) # precision 和 recall 比 thresholds 多一个元素对齐时要去掉尾巴 precision, recall precision[:-1], recall[:-1] mask precision min_precision if not mask.any(): return 0.5 # 没有任何阈值能满足精确率要求退回默认值 best np.argmax(recall[mask]) return float(thresholds[mask][best]) y_val, prob_val evaluate(model, val_loader, device) ap average_precision_score(y_val, prob_val) thr pick_threshold(y_val, prob_val, min_precision0.8) print(fval PR-AUC{ap:.4f}, threshold{thr:.4f})min_precision这个参数直接对应运营承受能力。SOC 每天能人工处理 200 条告警日均流量 100 万条那精确率下限大概就是万分之二但这么低的精确率没人受得了实际项目里通常把min_precision设在 0.5 到 0.9 之间再配合后面的告警聚合把量压下来。5. 阈值标定与漂移监控让检测模型在线上待得住5.1 用分位数而不是固定值标定阈值离线算出来的单一阈值搬到线上第一天就可能偏。更稳的做法是维护一个滑动窗口内的分数分布把阈值定在某个高分位数上。class QuantileThreshold: 维护最近 N 条样本的异常分数按分位数给出动态阈值 def __init__(self, window20000, q0.999): self.buf deque(maxlenwindow) self.q q def update(self, scores): self.buf.extend(scores.tolist()) def threshold(self): if len(self.buf) 1000: return 0.5 # 样本不足时退回保守值 return float(np.quantile(self.buf, self.q))q0.999的意思是允许千分之一的流量被判为异常这个值要跟日均流量和人力对齐。窗口window决定阈值对流量变化的响应速度设得太小会被一次突发流量带偏设得太大则漂移响应迟钝我一般在 1 万到 5 万条之间取。5.2 用分数分布偏移触发再训练模型本身的退化比阈值漂移更难发现。监控两个量就够了一是每天异常分数的中位数二是每天被判为异常的流量占比。中位数连续三天偏离基线超过 20%或者异常占比相对基线翻倍并持续两天就该把最近的数据捞出来重新训练了。标签回捞是这条链路的卡点。常见做法是让分析人员在处置告警时顺手标记“真实攻击 / 误报”这批反馈数据积累到几千条之后混进训练集做增量微调效果比重新训练整模型更快也更省资源。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价