尧图精选

基于深度神经网络的异常流量检测:ISCX 2012 特征工程与误报率优化

🕒 发布时间:2026/9/17 13:56:04 📁 来源:尧图网络
简介这是一份面向网络安全与机器学习方向学习者、研究人员及工程实践者的学术论文资料聚焦基于深度神经网络的异常流量检测算法适合具备一定数据建模与网络协议基础的读者用于算法选型、实验复现与课题参考。资源为单个PDF文档压缩包总体约7.65MB内容为该领域正式发表的研究论文包含摘要、相关工作、算法设计与实验评估等完整章节便于系统研读与引用。文中以ISCX数据集为实验对象对比朴素贝叶斯等传统机器学习算法验证所提方法在提升准确率与降低误报率方面的改进效果并讨论了深度神经网络在入侵检测、恶意软件识别、IoT与云计算安全等场景的应用潜力同时对恶意软件攻击、DDoS攻击、植入式攻击等网络攻击类型及机器学习算法的优缺点作了梳理。目前已有204人学习适合作为异常流量检测方向的入门研读与方案参考材料。1. 一份 8 页 PDF 里的异常流量检测流水线《基于深度神经网络的异常流量检测算法》是国防科技大学陈冠衡、苏金树发表在《信息网络安全》2019 年第 6 期的一篇论文正文落在 68 到 75 页。篇幅不长却把一条完整的检测链路走通了从 ISCX 2012 流量里挑出 5 个流级字段向量化后送进带多层全连接的深度神经网络用交叉熵当损失最后拿准确率和误报率跟朴素贝叶斯对赌。结论有点反直觉——网络并不深特征只有 5 个测试集准确率报到了 99.73%误报率 4.34%对照组朴素贝叶斯只有 88.23% 的准确率和 3.7% 的误报率。这份 PDF 真正值得拆的地方不在神经网络四个字而在它做取舍的方式什么数据不要、什么特征不要、什么指标必须盯住。适合三类人看正在选 IDS/NDR 方案的安全工程师、要做课程设计或毕设的学生、手上有 Keras 环境想找一份能跑通的二分类样例的开发者。下面从数据集开始把这套流程拆到能复现的程度。2. ISCX 数据集与五维特征工程2.1 为什么放弃 KDD99 和 DARPA选 ISCX 2012异常流量检测的很多论文栽在数据集上不是模型不行是数据本身不能代表真实网络。原文用一节篇幅把公开数据集逐个挑刺DARPA 2000 年代久远、攻击类型单一网络基础设施和攻击手法都跟不上现代 IDS 的评估需求KDD99 是 DARPA98 的翻新版冗余记录多容易把测试结果带偏CAIDA 的几份 trace 匿名化比较重部分数据集协议覆盖不全Kyoto 用蜜罐采集只能观察到打到蜜罐上的攻击正常流量还是重新模拟的只生成 DNS 和邮件两类ADFA 2013 攻击种类少且部分攻击行为和正常行为没有清晰分离。数据集网络配置完整性流量完整性带标记协议覆盖攻击多样性DARPA 2000完整不完整是HTTP/SSH/FTP/E-mail较少KDD99完整不完整否HTTP/SSH/FTP/E-mail中等CAIDA完整完整是HTTPS 不可用中等Kyoto 2006完整不完整是含 DNS含 BackdoorADFA 2013完整完整是含 DNS不含 DoS/ScanISCX 2012完整完整是HTTP/HTTPS/SSH/FTP/E-mail含 Browser/DoS/Scan/BackdoorISCX 2012 的优势是动态生成先在真实抓包中分析出 HTTP、SMTP、SSH、IMAP、POP3、FTP 的代理配置再在受控测试平台上按这些配置重放流量同时执行多阶段攻击生成异常部分。这样正常流量和异常流量在同一个确定性环境里产生标签是环境给的不需要人工打标也没有捕获后插入导致的痕迹。要留意一点ISCX 的带标记是流程带标记不等于可以直接拿来训练。原始抓包是 pcap需要先聚合成流再按五元组和统计量导出成 CSV这一步用 SplitCap 或者 CICFlowMeter 都能做。聚流粒度直接决定后面的上限超时窗口设太短会把一次会话切成好几条流。2.2 五个特征的物理含义与取舍原文明确只取 5 个特征src_port、dst_port、totalSourceBytes、totalDestinationBytes、totalSourcePackets。给出的理由是降低模型复杂性和计算负荷。这个取舍在工程上有两层含义一是特征维度从几十维压到 5 维训练和推理都快模型也更容易收敛二是丢掉了协议标志位、流持续时间、包长分布这些信息模型的判别能力被压在一个很窄的信号面上后面的误报率控制就变得困难——这也是原文承认误报率仍偏高的原因之一。特征类型取值范围原文处理建议改进src_port整数0–65535直接数值化分桶或映射到已知服务dst_port整数0–65535直接数值化80/443/22/53 单独打标totalSourceBytes整数0–10^9直接数值化log1p 压缩长尾totalDestinationBytes整数0–10^9直接数值化log1ptotalSourcePackets整数0–10^6直接数值化log1p端口号当连续整数喂给网络是最容易被质疑的一点65535 和 22 在数值上差得很远在语义上却都是某个服务端口。务实的做法是把端口拆成两类输入——常见服务端口做 one-hot剩下的端口号取 log 后当数值或者直接上 embedding。字节数和包数都是长尾分布log1p 之后再标准化梯度会稳很多。2.3 向量化与量纲统一数据读进来先清洗再做标签映射和标准化。原文说如果检测为正常流量则输出 1如果检测为异常流量则输出 0注意标签方向和大多数安全场景是反的eval 的时候别把正负类搞混。import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler FEATURES [src_port, dst_port, totalSourceBytes, totalDestinationBytes, totalSourcePackets] LABEL_COL label def load_iscx_flow(csv_path: str) - pd.DataFrame: df pd.read_csv(csv_path, low_memoryFalse) # 丢掉特征或标签缺失的流 df df.dropna(subsetFEATURES [LABEL_COL]) # 标签统一成小写再做映射normal - 1, 其余异常类 - 0 norm df[LABEL_COL].astype(str).str.strip().str.lower() df[LABEL_COL] norm.map({normal: 1}).fillna(0).astype(int8) # 字节数/包数走 log1p端口保持原值 for col in [totalSourceBytes, totalDestinationBytes, totalSourcePackets]: df[col] np.log1p(df[col].clip(lower0)) return df def build_dataset(df: pd.DataFrame, test_size: int 28000, seed: int 42): X df[FEATURES].astype(float32).values y df[LABEL_COL].values.astype(float32) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_sizetest_size, random_stateseed, stratifyy) # 标准化统计量只能在训练集上 fit避免测试集信息泄漏 scaler StandardScaler().fit(X_tr) return scaler.transform(X_tr), scaler.transform(X_te), y_tr, y_te, scalerlog1p用clip(lower0)兜底防止脏数据里的负值把 log 搞成 NaN。stratifyy保证切分后两边的正负比例一致ISCX 里异常流占比不低不分层很容易出现某一侧全是正常流的情况。StandardScaler只 fit 训练集这一条是老规矩但在流数据上还有一个更隐蔽的泄漏来源同一条会话被切成多条流后一部分进了训练集一部分进了测试集两条流的字节数几乎一样模型等于背答案。正确做法是按会话或按时间窗口切分而不是按行随机切。2.4 训练集 150000、测试集 28000 的切分原文用 ISCX 中的 178000 条数据做实验150000 条训练、28000 条测试。按 8.4:1.6 的比例切测试集不到 16%比常见的 8:2 小。测试集小意味着准确率、误报率的置信区间更宽99.73% 这个数字背后可能只有十几次判错。如果要复现出能对比的结论建议把测试集固定下来并保存成文件每次实验都跑同一份避免不同随机种子之间互相打架。3. 深度神经网络建模从 Reshape 到交叉熵损失3.1 网络结构全连接层与神经元配置原文对深度神经网络的界定很朴素有很多隐藏层的神经网络第一层是输入层最后一层是输出层中间都是隐藏层层与层之间全连接第 i 层任意一个神经元一定与第 i1 层每个神经元相连。从 TensorBoard 截图能看出图的入口是Reshape[0-6]随后是dense_1到dense_4一串张量规模是10x32也就是批大小 10、每层 32 个单元最后dense_4/Sigmoid出二分类结果。这个配置在 2024 年看确实小但对 5 维输入来说已经够用甚至偏大。5 维输入接 32 单元的全连接参数量是 5×3232192四层加起来不到 5000 个参数。参数少的好处是不容易过拟合到某几条特定流上坏处是拟合能力有限——原文说下一步要靠增加特征维数来提升准确率而不是加层加宽说明瓶颈确实在特征侧。3.2 Reshape、Squeeze、transpose 在图里做了什么原文专门画了三张图讲这三个算子。它们不是模型的一部分而是数据从 numpy 喂进图时形状对不对齐导致的形状搬运Reshape()重新调整行列维度把[batch, 5]之类的输入重塑成图里期望的 rankSqueeze()去掉尺寸为 1 的维度例如[batch, 32, 1]压成[batch, 32]transpose调换数组的行列索引例如把[32, batch]转成[batch, 32]。import tensorflow as tf # 假设 batch 为 10特征为 5 x tf.keras.Input(shape(5,), dtypetf.float32) flat tf.reshape(x, [-1, 5, 1]) # - (10, 5, 1)对应 Reshape[0-6] sq tf.squeeze(flat, axis-1) # - (10, 5)对应 Squeeze[0-7] xt tf.transpose(sq) # - (5, 10)对应 transpose back tf.reshape(xt, [-1, 5]) # - (10, 5)转回来才能接 Dense理解这三步的实用价值在于如果你的输入本来就是[batch, n_features]用 Keras 的Input(shape(n_features,))直接建图这一串 reshape/squeeze/transpose 完全可以省掉。很多人照抄旧版 TensorFlow 的feed_dict写法会莫名其妙在图上多出一堆Subgraph排查时看着像 bug其实只是喂进去的数组多了一维。真要排查最直接的办法是model.summary()看输入层形状以及tf.print出每一步的shape。3.3 交叉熵损失与二分类输出原文用经典相对熵解释损失函数它度量两个概率分布 P 和 Q 的差异距离越大分布差得越远模型就是不断调参数让预测分布贴近真实分布。落到实现上就是二分类交叉熵原文在此基础上做了加权优化给每个样本配一个权重正常流和异常流数量不均衡时权重可以用来补偿稀有类权重为 1 时退化成标准交叉熵训练过程中损失值持续下降并趋于平稳说明梯度已经推不动参数了。import tensorflow as tf from tensorflow.keras import layers, Model def build_model(n_features: int 5, hidden: int 32, lr: float 1e-3) - Model: inp layers.Input(shape(n_features,), nameflow_features) x inp for i in range(1, 5): # dense_1 ~ dense_4 x layers.Dense(hidden, activationrelu, namefdense_{i})(x) out layers.Dense(1, activationsigmoid, namedense_4_sigmoid)(x) model Model(inp, out) model.compile( optimizertf.keras.optimizers.Adam(learning_ratelr), lossbinary_crossentropy, metrics[tf.keras.metrics.Precision(nameprec), tf.keras.metrics.Recall(namerec), tf.keras.metrics.AUC(nameauc)], ) return model model build_model() model.summary()hidden32和四层是照着截图设的实测里三层就能收敛到接近的水平。Adam 的1e-3是稳妥起点如果 loss 在前 5 个 epoch 抖动剧烈降到1e-4。metrics里放 Precision 和 Recall 而不是 Accuracy是因为后面算误报率要靠 FP而 Keras 的Accuracy只给一个笼统的数拆不出混淆矩阵的四个格子。3.4 训练循环与早停import numpy as np X_tr, X_te, y_tr, y_te, scaler build_dataset(load_iscx_flow(ISCX-2012-flow.csv)) # 按类别频率给样本加权权重越大的样本对 loss 贡献越多 pos y_tr.sum() neg len(y_tr) - pos w_pos len(y_tr) / (2.0 * pos) w_neg len(y_tr) / (2.0 * neg) sample_w np.where(y_tr 1, w_pos, w_neg).astype(float32) model build_model() history model.fit( X_tr, y_tr, sample_weightsample_w, batch_size10, # 与图里的 10x32 批大小一致 epochs100, validation_data(X_te, y_te), callbacks[ tf.keras.callbacks.EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), tf.keras.callbacks.CSVLogger(train_log.csv), ], verbose2, )sample_weight是原文优化后损失函数最直接的实现方式等价于给每个样本的 loss 乘一个系数。batch_size10来自截图里的张量形状小批量在 15 万条数据上训练 100 个 epoch 的代价可以接受。CSVLogger把每个 epoch 的 loss 和指标落盘画训练曲线不用重新跑一遍。4. 训练收敛与误报率排错4.1 收敛点怎么读原文的实验曲线给了两个关键数字训练准确率在接近 20 次迭代时趋于稳定达到 97.52%说明模型基本收敛测试准确率在整个测试过程中稳定在 99.73%。训练误报率在接近 8 次迭代时稳定但停在 39.09% 这个偏高的位置测试误报率则随着测试次数增加大幅下降20 次之后稳定在 4.34% 左右伴有小幅震荡。这里最值得琢磨的是一组反常对比训练集误报率 39.09%测试集误报率只有 4.34%。同一个模型在两份数据上的表现差了一个数量级通常有几种解释。一是训练误报率是在训练过程中统计的早期模型还没学到正常流的长尾形态把大量正常流判成了攻击曲线还没来得及回落。二是误报率的分母是FPTN如果训练集里正常流样本基数小同样的 FP 数量会算出大得多的比率。三是模型在训练集上确实偏向判为入侵这类偏置在分类阈值固定为 0.5 时会被放大。原文把这个现象解读为可能存在过拟合即更倾向于将事件判断为入侵方向是对的。4.2 指标口径与混淆矩阵原文给出的评价方法是异常活动被识别为异常记 TP合法活动被识别为合法记 TN合法活动被识别为异常记 FP异常活动被识别为合法记 FN。然后写准确率 TP/(TPFP)误报率 FP/(FPTN)。这两个式子需要按原口径理解TP/(TPFP)在标准术语里是精确率PrecisionFP/(FPTN)是假正率FPR。照这个口径复现才能对上 99.73% 和 4.34%如果用 sklearn 的accuracy_score去算数字对不上别急着怀疑代码写错了。指标公式原文口径建议补充准确率TP/(TPFP)精确率同时报 accuracy (TPTN)/N误报率FP/(FPTN)假正率 FPR同时报漏报率 FN/(FNTP)召回率TP/(TPFN)未报异常检测的核心指标F12PR/(PR)未报不平衡场景下比准确率可靠漏报率是安全场景里比误报率更致命的指标一条真正的攻击流被放过损失远大于多报十条误报。原文只报了准确率和误报率做工程落地时必须把召回率补上否则准确率 99.73%可能掩盖了漏掉了一半攻击。import numpy as np from sklearn.metrics import confusion_matrix def eval_metrics(y_true, y_prob, threshold: float 0.5): y_pred (y_prob threshold).astype(int) tn, fp, fn, tp confusion_matrix(y_true, y_pred, labels[0, 1]).ravel() prec tp / (tp fp) if tp fp else 0.0 # 原文的准确率口径 fpr fp / (fp tn) if fp tn else 0.0 # 原文的误报率口径 rec tp / (tp fn) if tp fn else 0.0 f1 2 * prec * rec / (prec rec) if prec rec else 0.0 return {prec: prec, fpr: fpr, rec: rec, f1: f1, cm: (tn, fp, fn, tp)} probs model.predict(X_te, batch_size1024).ravel() print(eval_metrics(y_te, probs))confusion_matrix的labels[0, 1]必须显式给否则当某一类在预测里完全没出现时ravel()出来的顺序会错位tn/fp/fn/tp 会整体串位算出来的数看着挺合理但全是错的。4.3 与朴素贝叶斯的对照方法训练准确率训练误报率测试准确率测试误报率朴素贝叶斯82.8%17.6%88.23%3.7%深度神经网络97.52%39.09%99.73%4.34%这组数字里有两个容易被忽略的细节。测试准确率上 DNN 比朴素贝叶斯高 11.5 个百分点说明非线性特征组合确实有用测试误报率上 DNN 反而高出 0.64 个百分点说明高准确率和低误报在这个模型里没有同时拿到。原文自己也承认误报率仍然偏高下一步方向是增加特征维数、优化自编码器等算法压低误报率。做方案选型时拿这组数去说服别人之前先在自己的流量上复现一遍ISCX 的分布和你机房的分布差得很远。4.4 复现时最容易踩的四个坑第一是特征分布漂移。ISCX 是 2012 年的数据明文协议占大头现在机房里 TLS 占八九成端口和字节数的分布已经变了模型直接迁移过去表现会明显下滑。第二是类别不平衡处理不当。如果异常流占三成以上sample_weight加权的效果有限反而可能把精确率压下去。稳妥做法是先不加权跑一版看基线再按 F1 决定要不要调权重。第三是标准化统计量泄漏。前面提过StandardScaler的fit只能用训练集而且线上推理时必须把训练时的 scaler 用joblib.dump存下来复用不能每批流量重新 fit。第四是训练/测试切分方式。按行随机切分会让同一会话的流出现在两边测试准确率虚高。改成按时间窗口切比如前 6 小时训练、后 1 小时测试得到的数字才接近上线后的真实水平。5. 从论文到线上阈值扫描与特征扩展5.1 用阈值扫描把误报率压到可接受区间模型输出的是 sigmoid 概率0.5 只是默认阈值不是最优阈值。运维现场通常先定一个误报率上限比如 1%再在这个约束下找召回率最高的阈值。这一段扫描代码比调模型结构有用得多import numpy as np def sweep_threshold(y_true, y_prob, fps(0.005, 0.01, 0.02, 0.05)): rows [] for t in np.arange(0.05, 0.96, 0.01): m eval_metrics(y_true, y_prob, thresholdfloat(t)) rows.append((round(float(t), 2), round(m[prec], 4), round(m[fpr], 4), round(m[rec], 4))) # 只打印误报率落在目标区间内的点 for t, prec, fpr, rec in rows: if any(abs(fpr - f) 0.005 for f in fps): print(fthreshold{t} prec{prec} fpr{fpr} recall{rec}) return rows sweep_threshold(y_te, probs)阈值从 0.05 扫到 0.95步长 0.01输出里保留误报率贴近 0.5%、1%、2%、5% 的那几个点。工程上一般取召回率还能接受、误报率已经达标的那个交点而不是精确率最高的点。要注意阈值是和数据集绑定的换了流量段要重新扫最好做成配置项写进部署脚本。5.2 增加特征维数原文下一步的方向原文结束语里说下一步要增加特征维数以提升检测准确率。按这个思路补最值得加的是流持续时间和包长统计量比如flowDuration、minPacketLength、maxPacketLength、meanPacketLength、flowIAT的均值和标准差。这几维对区分扫描和正常短连接特别有效——扫描流的包长高度一致、IAT 极小正常浏览行为的包长分布散得多。第二个方向是端口语义化。把dst_port按已知服务映射成类别特征比如 22/23/3389 归为远程管理类80/443/8080 归为 Web 类其余走 log 数值分支然后用layers.Embedding接一个小维度向量。端口从一个整数变成一小组可分特征模型学起来会容易不少。第三个方向是自编码器做无监督预筛。先用正常流训练自编码器重构误差高的流送进监督模型二次判定相当于把没见过的攻击先捞出来。原文末尾提到的正是这条路代价是推理链路变长单机吞吐会掉一截。5.3 批量打分脚本与上线前检查项推理阶段把模型、scaler、阈值打包成一个可加载的对象避免线上和训练环境对不上import joblib import numpy as np import tensorflow as tf class FlowScorer: def __init__(self, model_path: str, scaler_path: str, threshold: float 0.5): self.model tf.keras.models.load_model(model_path) self.scaler joblib.load(scaler_path) self.threshold threshold def score(self, df): x df[[src_port, dst_port, totalSourceBytes, totalDestinationBytes, totalSourcePackets]].astype(float32).values for i, col in enumerate([totalSourceBytes, totalDestinationBytes, totalSourcePackets]): x[:, 2 i] np.log1p(np.clip(x[:, 2 i], 0, None)) x self.scaler.transform(x) p self.model.predict(x, batch_size4096, verbose0).ravel() df[prob_abnormal] 1.0 - p # 模型输出 1 表示正常取反得到异常概率 df[is_abnormal] (df[prob_abnormal] self.threshold).astype(int) return df1.0 - p这一步别省原文的标签约定是正常输出 1、异常输出 0如果直接拿p当异常概率阈值方向和告警方向会整体反过来现场表现就是满屏正常流量告警、真攻击一条不报。上线前把这三件事过一遍拿一份标注过的近期流量跑一次eval_metrics确认 FPR 和召回率落在承诺区间检查 scaler 与模型是否为同一次训练产出的版本把阈值、模型版本号、特征清单写进配置文件并纳入变更记录任何一项改了都要重新走一遍指标验证。流量是活的模型上线只是开始按周做一次漂移检测——统计线上异常概率的分布一旦和训练时的分布偏离超过阈值就该考虑重训了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →