资讯动态

深度学习与机器学习在入侵检测系统中的实战:从特征工程到模型部署

发布时间:2026/9/26 7:02:53 来源:尧图企业网站定制
简介这是一份基于深度学习和机器学习实现入侵检测系统IDS的工程资料包面向网络安全方向的研究者、学生及入门实践者帮助理解从KDD数据集预处理、特征筛选到模型训练与评估的完整流程。压缩包共90个文件约18.48MB以Python脚本为主涵盖DNN、CNN等深度学习模型以及决策树、随机森林、SVM、朴素贝叶斯等经典机器学习算法并配有训练/测试CSV数据、各模型预测标签与概率结果、README说明和一篇神经网络评估参考论文便于直接复现和对比实验。已有132人学习下载。资料对于想掌握机器学习与深度学习在异常流量识别、网络攻击检测中落地方法的读者具有直接参考价值尤其适合开展课程设计或课题实验时使用。1. 入侵检测系统为什么需要深度学习先别急着上模型我见过不少安全运维同学被这样折腾单位里的入侵检测系统IDS每天吐出上千条规则告警分析组点开看八成是误报真正出事的那个晚上规则库里并没有对应签名流量异常直到业务方反馈才被发现。基于深度学习和机器学习的入侵检测系统解决的就是这类问题——不靠人工写死规则而是让算法从历史流量里学出“什么形状的流量像攻击”再用模型去过滤、排序、告警。它适合做智能告警的运维、选型的安全负责人以及拿算法做毕设方向的学生。但模型不是万能药后面几章会聊怎么把模型喂进真实检测链路。2. 数据集与特征工程让模型先生成一张能学入侵行为的特征表在开始训练前很多人第一反应是“搞个模型跑一下”但真正决定入侵检测效果好坏的往往是训练数据本身。这一章我们先解决“数据从哪里来、怎么变成模型能吃的样子”这个命门。2.1 选公开数据集不是越新越好CICIDS2017与UNSW-NB15的取舍做入侵检测方向的算法验证绕不开几个公开数据集。KDD99和NSL-KDD年代太早流量场景和现代攻击差别大只当教学玩具还行。实际项目里我一般优先看CICIDS2017或UNSW-NB15前者是实验室环境中采集的真实流量覆盖良性和DoS、端口扫描、DDoS等常见类别还带TCP标志位、包长统计、流时长等几十上百个流特征后者混合了真实流量和仿真攻击更贴近内网有杂质的环境。选型时不要只盯着“新”。比新更重要的是三类问题你的业务是看外网威胁还是内网横移你需要二分类还是多分类判断攻击类型你的在线数据能否在特征上对齐公开数据集比如公开数据集里的“Source IP”和“Destination IP”在建模时通常要剔除因为换一个网络环境这些IP就失效了但如果你做内网威胁检测可能反而要保留本机名、端口惯性等特征。我自己会做一个小实验拿两周真实流量的特征分布与公开数据集比对看数值范围是否在一个量级。跨得太远模型迁移过去就只是自欺欺人。2.2 把原始流量改造成特征矩阵数值化、归一化与标签处理公开数据集通常已经做了流量特征提取但真实场景里你拿到的是pcap或Zeek日志。常见做法是先用CICFlowMeter/FlowManager这类工具把原始流量聚合成流输出csv每一行是一条双向流字段包括Flow Duration、Total Fwd Packets、Fwd Packet Length Max、Bwd Packet Length Mean、SYN/FIN/RST标记计数等。也可以让Zeek直接生成conn.log再自己写脚本聚合。拿到特征表后有三个动作必须做。第一把非数值特征处理掉像协议名、服务名要用编码器转成数值不参与数值计算的ID、IP列直接删。第二处理缺失值和无穷值流量工具偶尔会产出NaN或Inf我一般对数值列填充中位数对类别列填充众数Inf替换为该列最大值或直接标记为异常值。第三归一化只在要喂神经网络时做树模型对量纲不敏感但MLP/CNN/LSTM都要做标准化否则网络容易不收敛。别忘了标签列也要转成从0开始的整数比如良性0DDoS1端口扫描2。2.3 一个可直接跑的偏工程特征处理脚本Python下面这段脚本是我处理这类特征表的基本盘可以直接替换路径跑通import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.model_selection import train_test_split df pd.read_csv(ids_flow.csv) # 替换成你自己的原始特征文件 # 1) 去掉全空列和与目标强相关的标识列 df.drop(columns[Flow ID, Source IP, Destination IP], errorsignore, inplaceTrue) # 2) 缺失值数值列中位数填充类别列众数填充 num_cols df.select_dtypes(include[np.number]).columns for col in num_cols: df[col] df[col].fillna(df[col].median()) cat_cols df.select_dtypes(include[object]).columns for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0]) # 3) 标签编码 label_enc LabelEncoder() df[Label] label_enc.fit_transform(df[Label]) # 4) 切分并归一化 X df.drop(columns[Label]) y df[Label] feature_names X.columns.tolist() # 先切分再fit scaler避免测试集信息进入训练阶段 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)代码里先说两件事IP列直接drop是因为IP在流量数据里几乎是最强的“身份泄漏”——同一批IP可能只在某一天出现模型记住了IP编号就能拿高分部署到新网段立刻失效。如果你坚持保留IP也要用哈希分桶之类的办法泛化而不是直接把字符串喂给模型。stratifyy是为了让训练集和测试集里的类别比例保持一致不然碰到DDoS占95%的情况随机切分很可能把少数攻击类别全分到测试集训练集就变成“纯洁的无攻击数据”。StandardScaler只对训练集做fit_transform测试集上只用transform。很多新手在这里翻车对全量数据拟合后再切分等于测试集的均值和方差已经参与了模型训练这属于一种隐蔽的特征泄漏。保存时记得连label_enc和scaler一起存后面做推理服务要用到。如果你的特征文件里有Inf建议洗数据时先np.isinf查一下补上再缩放否则神经网络里会出现NaN梯度。到这里一张干净的特征表就准备好了。3. 机器学习基线与深度学习模型从随机森林到CNN的选型对比模型选型不是一上来就上深度网络。真实入侵检测场景里数据量从几万行到几百万行都有深度学习在特征充足且量大的时候有优势但用树模型做基线能帮你快速定位“特征工程有没有问题”。这一章把两条路线都走一遍并说清它们的分工。3.1 先拿机器学习算法做基线随机森林/逻辑回归的正确打开方式我开始的惯例是用随机森林跑通全流程。它不容易过拟合能天然处理非线性关系训练前也不需要把所有特征缩放到同一量纲但为了后面跟深度学习模型做公平对比我通常还是喂之前已经标准化好的数据。from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report rf RandomForestClassifier( n_estimators200, max_depthNone, class_weightbalanced_subsample, n_jobs-1, random_state42, ) rf.fit(X_train_scaled, y_train) y_pred rf.predict(X_test_scaled) print(classification_report(y_test, y_pred, target_nameslabel_enc.classes_))class_weightbalanced_subsample是随机森林里处理不平衡数据的常用参数它在每个子采样里重新计算类别权重比手动设置固定权重更稳。n_estimators200是经验值更大的森林收益不显著但训练时间线性增长。如果看到单棵树的精度都很高反而不一定是好事可能泄漏了不该有的字段这时候要进feature_importances_看一眼排名靠前的特征是否合理。随机森林跑完还可以顺手跑一个逻辑回归或线性SVM。它们性能通常不如随机森林但胜在可解释性强——如果线性模型和随机森林性能差距极大说明特征和攻击行为之间的关系高度非线性如果线性模型也不错那说明特征工程本身已经抓到了关键信息可以省掉上深度模型的精力。3.2 深度学习模型怎么接入少量代码MLP的一个最小实现当数据量来到百万级、或者特征之间组合复杂时我才会把神经网络端出来。最朴素的做法是全连接网络MLP把流特征直接映射到类别概率。下面是一个可以直接套在上一章特征矩阵上面的实现框架用TensorFlow/Kerasimport tensorflow as tf from tensorflow.keras import layers, models model models.Sequential([ layers.Input(shape(X_train_scaled.shape[1],)), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(64, activationrelu), layers.Dropout(0.3), layers.Dense(len(label_enc.classes_), activationsoftmax), ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy], ) history model.fit( X_train_scaled, y_train, validation_split0.2, batch_size256, epochs30, verbose1, )中间层的神经元数量从特征数开始翻倍或减半是一条经验曲线比如128、64就是特征维度在几十个时的常用配置。Dropout(0.3)用来防止过拟合入侵检测特征多而杂不加Dropout到第20个epoch很容易看到训练准确率接近100%验证集却停止上升。sparse_categorical_crossentropy对应的是我们整数标签如果你的标签做了one-hot就要换成categorical_crossentropy这是最常见的小错误。validation_split0.2是在训练集内部再切20%出来做早停参考。注意这里的验证集并不是前面切出来的测试集千万不要拿测试集来早停否则测试集的信息会不自觉地影响模型选择。训练完成后用model.save(ids_nn.keras)保存后面推理时候再加载。3.3 特征向量输入CNN/LSTM的两种常见改造很多人听说“深度CNN识别恶意软件”“LSTM检测时序攻击”于是直接把一维特征强塞进Conv2D翻车率极高。要搞清楚改造的前提CNN和LSTM都是在数据里存在空间或时间结构时才有效。流统计特征本身没有顺序这时把二维矩阵reshape成(1, n_features)让Conv1D走一遍其实等价于一个沿特征方向的卷积能学到局部交互特征但没什么时序含义。# 把每条流的特征做成 (1, n_features) 的序列窗口 X_cnn X_train_scaled.reshape((-1, 1, X_train_scaled.shape[1])) cnn models.Sequential([ layers.Conv1D(filters64, kernel_size1, activationrelu, input_shape(1, X_train_scaled.shape[1])), layers.Flatten(), layers.Dropout(0.3), layers.Dense(64, activationrelu), layers.Dense(len(label_enc.classes_), activationsoftmax), ]) cnn.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])如果你手里不是流统计特征而是原始会话内连续N个数据包的行那么更合理的做法是维护一个时间窗口把窗口内发生的流按先后顺序排序每个窗口样本的形状是(window_len, n_features)然后用LSTM或Conv1D去提取跨时间的模式。这时候kernel_size才应该大于1比如3或5表示窗口内连续几行流量之间有交互。判断依据很简单特征行之间调换顺序后模型结果是否明显变化。变了才说明时序结构有效没变就不要为LSTM而LSTM。3.4 模型对比维度不只盯着准确率在入侵检测里准确率是最不可信的指标。假设全流量里只有1%是攻击一个“永远预测正常”的模型会有99%准确率看起来很好实际一记重拳打在空气上。评估的时候我至少输出分类报告和混淆矩阵重点关注宏平均F1和每个攻击类别的召回率。模型攻击类平均召回率正常类误报率单条推理耗时CPU方向逻辑回归中高微秒级快速基线随机森林较高中毫秒级通常首选MLP高数据量大时低毫秒级折中方案1D-CNN/LSTM高有时序特征时可能低毫秒~十毫秒复杂攻击这张表不是固定结论而是提醒你对比时别只看一行。正常类的误报率决定了安全分析师每天要不要点开上百条无谓告警攻击类召回率决定了会不会漏。如果模型把DDoS的召回率从0.2拉到了0.9哪怕整体准确率掉了0.5个百分点我也认为值得换模型。4. 实战中的五大翻车点与排查清单前面几章把流程走通了但在真实跑起来时翻车点往往不在模型结构而在数据处理和验证方式。这一章把我在项目里踩过和替别人排查过的五类高频问题列成清单每条按照“现象 - 原因 - 解决”讲。4.1 数据不平衡攻击样本只占0.1%时模型学会了“永远说安全”现象训练loss一直在降测试准确率飙升到99.8%点开分类报告攻击样本的召回率是0模型把所有预测输出成了“正常”。原因交叉熵损失函数会让模型倾向于把高概率预测放到样本量大的类别上。攻击流量在真实环境里本来就稀有如果直接用原始比例训练模型的最优解就是把所有流量判为正常因为这样整体损失最小根本没有学到攻击的边界。解决至少做三件事训练集按攻击类别分层采样保证每个Batch里都能看到少量攻击样本给少数类更大的类权重Keras里用class_weight随机森林用class_weightbalanced_subsample最后是评估时单独输出每个攻击类的精确率和召回率不要只盯着Accuracy。如果重采样只对训练集做别对测试集做。4.2 特征泄漏把“是否告警”混进特征矩阵训练时99分上线就翻车现象离线测试AUC 0.999看起来无懈可击部署到新流量上分数跟随机猜测差不多。原因特征表里有和标签“近亲”的列比如这条流是否已经触发了规则告警、目标IP是否出现在威胁情报库里、该连接的后向字节数是否被安全设备拦截后重传。训练时这些列能完美区分攻击但它们本质上是标签的化身换一个没有这些机制的环境模型就失去了定向导航。解决把特征表交给安全组同事做一轮“语义审查”凡是字段含义里包含“是否”“告警”“封禁”“信誉”等字样的一律慎重。再用RandomForestClassifier().feature_importances_或mutual_info_classif看排序前20的特征如果出现你一眼觉得“这不就是直接告诉我结果吗”的字段直接删掉重新训练。4.3 时间穿越打乱数据集训练会高估模型真实流量必须按时间切分现象数据集随机切分时F1能到0.94换成“前80%时间训练后20%测试”只剩下0.61排查了很久才发现是数据切分方式在作怪。原因攻击流量是时变的扫描工具、恶意IP、漏洞利用手法都在不断变化。随机切分相当于允许模型在训练时窥视未来它记住了某个IP段、某个特征统计出现在哪段时间测试集里这段信息还在分数自然虚高。真实部署面对的是“明天”而不是从今天随机抽出的30%。解决把数据按时间戳排序训练集取前70%-80%验证集取从训练集末尾再往后一段比如7天测试集留最后一段期间不要来回调整阈值。用TimeSeriesSplit可以帮你做多折验证但业务上最简单的是按时间硬切。这种做法牺牲了一点数据利用效率换来的是上线前比较靠谱的性能估计。4.4 参数玄学深度学习不收敛时八成是学习率和缩放没处理好现象训练没几轮loss变成NaN或者一直震荡不下降也有另一种翻车loss一直缓慢下降但验证集准确率纹丝不动整体像一个没通电的机器人。原因最常见是学习率太大Adam在1e-2时对稀疏特征容易发散其次是没有做标准化输入里某个端口号数值是几千在多层网络里产生的梯度过大还有可能是batch size太小比如8梯度噪声大到无法稳定下降。解决先确认特征已经过StandardScaler学习率从1e-3开始如果NaN就降到1e-4加clipnorm1.0在Adam(..., clipnorm1.0)里限制梯度范数。如果验证集始终不涨减少到单层Dense64节点做冒烟测试看模型能不能学到训练集的一小批样本。能学再逐层加复杂度。4.5 线上特征对齐问题训练用的流水线参数与推理不一致现象模型离线评分一切正常接上网络流量后接口报“feature count mismatch”或者预测概率全部接近0.5没有任何区分度。原因训练时pipeline里面的StandardScaler均值方差没保存在线推理时输入列顺序和训练时不一致或者直接把训练时丢弃的源IP字符串传了进去导致编码映射崩溃。解决训练完用joblib.dump(scaler, scaler.joblib)、joblib.dump(rf_model, rf_model.joblib)、joblib.dump(feature_names, feature_names.joblib)三件套一起保存。推理前先校验列集合是否完全一致再按训练时的feature_names顺序重排。如果你用Keras同样要把预处理pipeline单独存一份。这样即使换了环境推理端还是同一套“配方”。5. 从模型到入侵检测系统实时推理与混合架构落地模型能出分数只是第一步把它嵌进真正的入侵检测系统里才会面对工程问题。这一章聊最常见的落地架构和推理服务写法。5.1 规则引擎与模型打分结合用Suricata/Zeek做预过滤模型做二次判断纯模型系统通常不是最佳方案因为模型的解释性弱误报后分析师不知道该信还是不信。主流做法是规则引擎和模型并行或串行。以Zeek为例Zeek负责解析协议、还原连接生成细粒度的事件日志你从conn.log里提取特征喂给模型。模型打分后只有分数超过阈值的连接才进入告警队列如果这条连接同时命中了规则引擎的某条签名告警级别再提一档。这样既保留了规则的可解释性又减少低水平误报。也有相反的做法让模型做第一层筛选规则引擎只对模型筛出的可疑流量做二次验证。两种都行但不要两种都同时全部放行否则告警量会爆炸。我一般建议做模型后置过滤把规则引擎的告警按源IP、目的IP、端口对与模型特征关联模型判定为“正常”的规则告警降级或自动关闭只有模型也认为异常的才转到人工。这个流程上线门槛低不用改检测引擎主体。5.2 把训练好的模型包装成推理服务Python代码示例在线环境里最常见的形态是一个HTTP接口流量特征生成组件把每一行features POST过来服务返回类别和置信度。下面是最小的FastAPI实现import joblib from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() scaler joblib.load(scaler.joblib) model joblib.load(rf_model.joblib) feature_names joblib.load(feature_names.joblib) class Item(BaseModel): values: list[float] app.post(/predict) def predict(item: Item): if len(item.values) ! len(feature_names): raise HTTPException(status_code400, detailfeature count mismatch) x scaler.transform([item.values]) pred model.predict(x)[0] prob model.predict_proba(x)[0] return {label: int(pred), score: round(float(prob.max()), 4)}scaler.transform用的必须是训练时保存的scaler不能在这里fit_transform否则又会把在线数据的统计量混进来等于改变模型输入的分布语义。feature_names的作用不只是校验列数后面接不同数据源时用它做列顺序重排就非常稳。返回值里prob.max()取最大类别的概率作为“可信度”这个分数会给你后面设阈值用如果想判断模型有多“慌”也可以返回top1和top2的概率差差值小代表类别间模糊这种样本通常要人工复核。5.3 延迟与资源占用滑动窗口和批量推理的取舍入侵检测的实时性要求高但“实时”不等于“每条流都立刻推理”。常见做法是维护一个时间窗口比如10秒或30秒把窗口内已结束的流聚合起来做一次判定能显著减少推理次数。对于长连接可以按每5分钟切一个子窗口单独生成一条“连接片段”特征。这样模型看到的不只是单个连接本身还有同一源IP在窗口内的行为序列对慢速扫描和低频C2这类时间型攻击更敏感。资源上随机森林跑单条流在毫秒级MLP在CPU上也就几毫秒完全可以扛住中小规模网络。真到了万兆流量不要把所有包都送模型先做采样一个会话的前几个包、DNS请求、TLS握手特征都是高信息密度对象把这些对象送模型比把全包都推给深度学习要便宜得多。GPU在这里不是必需品别为了深度学习而堆显卡。5.4 告警可解释性从黑匣子到可操作的处置建议最后是安全分析师的痛就算模型准确你不知道这个告警为什么来。我用两个办法缓解。一个是树模型自带feature_importances_对单条预测用treeinterpreter或shap.TreeExplainer输出贡献最大的特征比如“Bwd Packet Length Max超大”和“Connection Duration极短”这两项直接给分析人员一条理解路径。另一个是给模型打分结果配上上下文规则模板当score0.9且top特征与端口扫描相关自动生成“疑似扫描行为建议封禁源IP 24小时”的处置建议如果score在0.6-0.9只打上“低置信攻击行为需要结合告警上下文判断”不自动处置。前者帮助应急响应用了后者避免分析组被无意义的自动封禁惹火。6. 一个值得养成的验证习惯用新攻击流量测模型而不是用旧测试集模型训练完很多人会习惯性地把测试集反复重放看指标有没有更好一点。这其实是在污染验证集你调参调得越久测试集的结果就越不能代表真实分布。我自己的习惯是在数据切分时留出一段“最后一周的流量”整个调参周期内都不碰它只在模型完全定稿后跑一次作为最终成绩。如果这时候分数比之前下降超过5个点说明此前存在过拟合。更进一步可以做一个更真实的压力测试用实验室里抓取的新攻击工具流量或者直接在测试网络中重放攻击样本测试模型对未见过的变体的泛化力。这时最好的工具不是看分类报告而是画ROC曲线并重新找阈值因为随着新流量分布变化默认0.5的决策边界不会再是最优。下面这段代码帮我找过很多次合适的阈值from sklearn.metrics import roc_curve import numpy as np # proba是模型输出取攻击类别的概率 fpr, tpr, thresholds roc_curve(y_test, proba[:, 1]) j_scores tpr - fpr best_idx np.argmax(j_scores) best_threshold thresholds[best_idx] print(f最优阈值: {best_threshold:.4f}, J指数: {j_scores[best_idx]:.4f})这个J指数Youdens J就是“召回率 - 误报率”最大化是平衡漏报和误报的一个简单经验法则。我在两个项目里用这个办法把误报率从每周300条压到80条代价是攻击类召回率从0.97掉到0.93对于安全运营来说这个交换通常值得。不要等上线后再调阈值。一个成熟点的习惯是模型容量和特征确定后专门用一份独立的新流量来校准阈值而不是用测试集反复调。说到底入侵检测系统的价值是能不能在新攻击面前预测到风险而不是在旧数据上刷出漂亮分数。这个坑我踩过不止一次希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑