ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

机器学习入侵检测实战:从NSL-KDD到实时流量异常检测

2026/10/3 2:52:30 拓冰建站 浏览量
机器学习入侵检测实战:从NSL-KDD到实时流量异常检测 简介面向网络安全研究人员与机器学习开发者的入侵检测系统完整源码项目针对传统IDS依赖规则、误报率高、难以识别未知攻击与可解释性弱等痛点实现从数据预处理到检测展示的自动化流程。包内共30个文件以Vue前端、Flask后端、Streamlit交互页面、项目说明文档、数据库脚本及多张界面截图为主要组成整体仅11.21MB便于下载后快速搭建训练与演示环境。资源在模型层面集成SMOTE类别平衡、传统/集成/深度学习及自动机器学习算法并结合SHAP与DALEX完成预测解释兼顾检测性能与透明度。系统经过仿真实验环境验证在实际入侵检测需求中表现出良好性能与实用价值。已有175人学习下载适合课程设计、毕业设计或企业安全实验场景直接参考与二次开发。1. 网络被扫了才知道规则库不够用机器学习入侵检测到底在检测什么一台业务服务器凌晨两点 CPU 跑满top 里多了一个陌生进程netstat 外联一片。这种场景对做运维的人来说不陌生传统入侵检测系统靠规则库匹配特征库没更新新型攻击就拦不住。基于机器学习的入侵检测系统源码要解决的事就是把网络流量转成可计算的特征用机器学习模型学习“正常流量长什么样”偏离正常分布或命中攻击模式时产生告警。这篇笔记按自己做这类系统的顺序展开数据集选型、特征工程、模型训练与调参、常见落地坑最后落到一个能跑的实时检测思路。适合想从零搭一个 IDS 原型的安全运维、在校学生和流量安全方向的研发。2. 数据集选型与特征工程NSL-KDD 和 CICIDS2017 怎么选、怎么处理2.1 先别急着写模型NSL-KDD、CICIDS2017 和 UNSW-NB15 怎么挑做机器学习入侵检测的第一步不是找算法而是找一份能反映攻击行为的流量数据集。很多人一上来就直接套 sklearn 里的示例数据训练完了才发现类别分布、特征含义和真实流量完全对不上。我一般会先在下面三个公开数据集里做选择按使用场景定。NSL-KDD 是目前论文里最常用的入门数据集由 KDD Cup 1999 那套数据改进而来去掉了大量冗余记录训练集约 12.5 万条测试集约 2.2 万条。每条记录有 41 维特征标签分为 Normal、DoS、Probe、R2L、U2R 五类。它的优点是小、轻、社区对比基线多刚接触这个方向时拿它跑通全流程成本极低。缺点是数据来自 1999 年的模拟网络环境和现在真实流量差距明显尤其是 R2L、U2R 两个攻击类别样本极少训练时很容易被模型直接忽略。CICIDS2017 是加拿大网络安全研究所公开的流量数据集连续采集五天包含 14 种攻击类型官方同时提供 PCAP 原始包和 CSV 特征文件特征维度在 80 左右。它的优点是更贴近真实场景CSV 可以直接喂给模型很多人拿它验证模型在“没见过的攻击”上的表现。缺点是数据总量几十 GB类别极不平衡而且处理时你会发现部分样本标签有噪声需要额外花时间清洗。UNSW-NB15 可以当作第三个候选特征是 45 维左右流量由澳大利亚网络安全中心仿真生成攻击类型更现代但社区参考资料比前两个少。如果你打算做完离线实验再考虑上线用 NSL-KDD 入门、用 CICIDS2017 做最终验证是常见组合。数据集特征维度攻击类别文件形式适合场景NSL-KDD414DoS/Probe/R2L/U2RCSV入门、算法对比、教学CICIDS2017约 8014PCAP CSV接近真实场景的研究与验证UNSW-NB15约 459CSV新攻击类型验证选型建议很直接想快速看算法效果选 NSL-KDD想验证系统在真实复杂流量下的表现选 CICIDS2017。不要两个都想省数据选错后面所有评估数字都失去意义。2.2 41 维特征不是全都要数值化、归一化与标签映射拿到原始数据后第一个问题是这 41 列到底代表什么NSL-KDD 的特征可以粗分成四组。第一组是单个 TCP 连接的基本特征比如 duration、protocol_type、service、src_bytes、dst_bytes第二组是连接内容相关的特征比如 hot、num_failed_logins、root_shell这些跟攻击载荷是否成功有关第三组是基于时间的流量特征比如 count、serror_rate、same_srv_rate描述过去两秒内相同目标主机的连接行为第四组是基于主机的流量特征比如 dst_host_count、dst_host_srv_count描述过去若干条连接中目标主机的统计行为。不是所有特征都有用。拿 KDDTrain 来说num_outbound_cmds 这列几乎全为 0留着对模型没有帮助还占内存。特征选择的意义在于减少部署时的计算量因为在线检测时每条流量都要算一遍特征少一列就少一次统计操作。处理字符型特征是绕不开的一步。protocol_type、service、flag 这三列是字符串模型不认。手工标记成整数是可以的也可以用 LabelEncoder 自动转换。需要注意树模型对整数编码不敏感但如果后面换 KNN 或神经网络整数编码会引入虚假的大小关系这时用 one-hot 更合适。我在工程里常用的策略是先保留整数编码跑通全流程确认模型选型后再决定要不要换 one-hot。归一化要看模型。随机森林、XGBoost 这类树模型对特征尺度不敏感不做归一化也能训练但 SVM、KNN、神经网络这类基于距离或梯度的模型特征数值范围差异大会让训练不稳定。统一的做法是先用 StandardScaler 做标准化后面换模型时不用回改数据管道。这里有一个必须记住的规则归一化必须在切分训练集和测试集之后做而且只用训练集的数据分布去 fit否则会把测试集的信息泄漏进模型这个在后面的避坑章节会重点展开。标签映射取决于你要做二分类还是多分类。二分类就是把 Normal 之外的攻击统一标成 1优点是样本量足、模型容易稳定多分类要把 Normal、DoS、Probe、R2L、U2R 分别编码能识别攻击类型但 R2L 和 U2R 的样本可能只有几十条训练效果很难保证。实际项目里我一般先做二分类把检测能力跑起来后续再单独为稀少攻击类型设计专门模型。2.3 一个最小特征处理脚本KDDTrain CSV 到模型输入把上面的思路落成代码一个最小的特征处理脚本长这样。注意每一步的顺序有讲究顺序错了后面数据泄漏的风险就来了。# feature_prep.py import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder, StandardScaler # NSL-KDD 训练集 CSV 没有表头41 列特征 1 列标签 col_names [ duration, protocol_type, service, flag, src_bytes, dst_bytes, land, wrong_fragment, urgent, hot, num_failed_logins, logged_in, num_compromised, root_shell, su_attempted, num_root, num_file_creations, num_shells, num_access_files, num_outbound_cmds, is_host_login, is_guest_login, count, srv_count, serror_rate, srv_serror_rate, rerror_rate, srv_rerror_rate, same_srv_rate, diff_srv_rate, srv_diff_host_rate, dst_host_count, dst_host_srv_count, dst_host_same_srv_rate, dst_host_diff_srv_rate, dst_host_same_src_port_rate, dst_host_srv_diff_host_rate, dst_host_serror_rate, dst_host_srv_serror_rate, dst_host_rerror_rate, dst_host_srv_rerror_rate, label ] df pd.read_csv(KDDTrain.csv, headerNone, namescol_names) # 字符型特征统一整数化 for col in [protocol_type, service, flag]: df[col] LabelEncoder().fit_transform(df[col]) # 二分类标签normal 为 0其余攻击为 1 df[label] (df[label] ! normal).astype(int) X df.drop(columns[label]) y df[label] # 先切分再 fit scaler避免特征穿越 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler().fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test)脚本里有两个容易被新手忽略的点。第一个是stratifyynsmkdd 的攻击类别分布不均衡如果不按标签分层抽样切出来的测试集可能全是正常流量模型评估直接失真。第二个是 scaler 只 fit 在 X_train 上这是为了防止测试集的统计量提前进入模型视野。如果换成 CICIDS2017 的 CSV处理流程几乎一样读进来之后把 NaN 填充、把字符型特征做编码、把 Label 列映射成二分类或多分类剩下的交给同一个数据管道。数据准备这一层做扎实后面模型训练和调参才有意义。3. 用 sklearn 跑通第一个检测模型随机森林的训练、评估与导出3.1 先拿随机森林当基线中小流量特征上为什么不急着上深度学习流量特征数据通常是几百维、样本量在十万级这种规模是树模型的主场。随机森林适合当基线有三个原因第一它对特征尺度不敏感前面做的标准化在树模型里可有可无少一个变量第二它能输出特征重要性方便后续做特征裁剪第三训练速度快n_jobs-1 开满核几十万样本也就几分钟。深度学习在图像、语音、文本上的优势是公认的但把它用在表格化流量特征上不一定比树模型强。网络流量的结构特征是人工统计出来的不是原始字节序列这种输入丢给 CNN 或 LSTM 反而丢失了特征本身的含义。如果真想上深度学习正确方向是拿原始报文或字节序列做端到端学习那是另一套数据管道和训练成本。常见做法是先跑通随机森林确认识别率能达到什么水平再考虑 XGBoost、LightGBM 或者深度学习做增强。3.2 训练脚本随机森林的核心参数不是随便填的随机森林在入侵检测场景里的超参数有六个值得调树的数量、最大深度、最小分裂样本数、类别权重、并行核数和随机种子。下面这段代码是一个可以直接跑的基线。# train_rf.py from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score import joblib clf RandomForestClassifier( n_estimators200, max_depth20, min_samples_split5, class_weightbalanced, random_state42, n_jobs-1, ) clf.fit(X_train_scaled, y_train) y_pred clf.predict(X_test_scaled) y_proba clf.predict_proba(X_test_scaled)[:, 1] print(classification_report(y_test, y_pred, digits4)) print(AUC:, roc_auc_score(y_test, y_proba)) print(Confusion matrix:\n, confusion_matrix(y_test, y_pred)) joblib.dump(clf, ids_rf.joblib) # 输出特征重要性 Top10用于后续特征裁剪 import pandas as pd imp pd.Series(clf.feature_importances_, indexX_train.columns) print(imp.sort_values(ascendingFalse).head(10))参数含义要理解再改n_estimators 控制树林里树的棵数200 棵时预测已经比较稳定加到 500 收益很小但训练时间线性上涨max_depth 限制单棵树深度防止训练集被背下来20 是一个保守值min_samples_split 表示内部节点再分裂至少需要 5 个样本太小容易过拟合class_weightbalanced 会给样本少的攻击类别更高权重这是在数据层面之外最便宜的类别不平衡处理方式。最后两行 joblib.dump 把训练好的模型存成本地文件后面部署检测脚本时直接加载。模型训练到这里只是第一步真正决定系统好不好用的不是准确率而是下一节讲的评估口径。3.3 别只看 accuracy用精确率、召回率、F1 和混淆矩阵给模型体检很多入门教程拿准确率说事但在入侵检测里准确率是最有欺骗性的指标。CICIDS2017 里正常流量可能占到八成以上模型把四分之一攻击分错准确率还能有 90% 以上这种模型上线后等于没做。正确的观察顺序是先看分类报告再盯混淆矩阵。分类报告里的精确率代表“模型告警为攻击的流量里有多少是真的攻击”召回率代表“真实的攻击流量里模型抓到了多少”。入侵检测场景更关心召回率——漏掉一次攻击的代价远比多报一次警严重。# evaluate.py from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score print(classification_report(y_test, y_pred, digits4)) print(Confusion matrix:\n, confusion_matrix(y_test, y_pred)) # 更直观把混淆矩阵的行列含义打印出来 tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() print(f正常流量误报为攻击: {fp}) print(f攻击流量漏报为正常: {fn})AUC 用来快速判断模型有没有区分能力0.95 以上说明模型学到了有效模式但 AUC 高不代表告警能用。真正落地时要根据运维人力去定阈值这一步在第 4 章展开。特征重要性 Top10 也别看完就关它决定了部署版模型可以砍掉哪些特征。常见做法是先带着 Top10 特征重新训练一遍看分类报告掉点多少如果损失可接受在线检测时的计算量能又降一截。4. 调参与防漏报类别不平衡给的四个调整杠杆4.1 GridSearchCV 调参评估口径错了搜出来的参数就是错觉网格搜索是调参的常规操作但用默认配置直接搜会有个大坑GridSearchCV 默认评分是 accuracy在类别严重不平衡的数据上它会把几乎所有决策偏向多数类搜出来的最佳参数组合往往是“预测所有样本为正常”的方案。正确做法是让网格搜索同时观察多个指标再用 F1 决定最终模型。F1 是精确率和召回率的调和平均能同时惩罚误报和漏报。下面这段代码把评分标准拆开来看# tune_rf.py from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestClassifier param_grid { n_estimators: [100, 300], max_depth: [10, 20, None], min_samples_split: [2, 5, 10], } gs GridSearchCV( RandomForestClassifier(class_weightbalanced, random_state42, n_jobs-1), param_grid, scoring{f1: f1, recall: recall, precision: precision}, refitf1, cv5, n_jobs-1, ) gs.fit(X_train_scaled, y_train) print(gs.best_params_, gs.best_score_)scoring 传入一个字典GridSearchCV 会对每个参数组合分别计算 f1、recall、precision 三个分数refitf1 表示最终选参时以 F1 为准。cv5 意味着每个参数组合要训练 5 次2×3×318 组组合就是 90 次拟合数据量大时先减小 n_estimators 再跑。调参耗时看机器但方向比速度重要错误的口径会让搜索结果彻底没法用。4.2 类别不平衡的三个解法class_weight、SMOTE 和样本裁剪类别不平衡在入侵检测里不是“有一点”的问题而是“攻击流量往往只有正常流量的几十分之一甚至更少”。处理手段有三个层次我按成本从低到高讲。第一层是 model 里的 class_weight。前面训练时已经加了 balanced它的原理是给少数类的错分惩罚更大权重不改变样本数量只是让优化目标不那么偏向多数类。这一层几乎是零成本建议保留。第二层是 SMOTE 过采样。它通过在少数类样本之间插值生成新样本让攻击类的数量接近正常类。用 imbalanced-learn 库实现就几行from imblearn.over_sampling import SMOTE sm SMOTE(random_state42) X_train_resampled, y_train_resampled sm.fit_resample(X_train_scaled, y_train)注意 fit_resample 只允许用在训练集上测试集必须保持原始分布。否则验证结果会虚高上线后立刻现原形。SMOTE 的问题在 R2L、U2R 这类样本极少的攻击类型上很明显几十个样本之间插值生成的“新样本”很接近原始样本模型学不到泛化能力这种场景更适合在业务侧收集更多标注样本而不是靠合成数据撑场面。第三层是样本裁剪。正常流量如果实在太多可以下采样一部分让训练集类别比例变成 1:1 或 2:1。代价是会丢失部分正常流量的多样性上线后正常误报可能变高。我在项目里一般保留前两层第三层视数据量决定。4.3 把概率变成告警阈值、白名单与确认机制模型默认以 0.5 概率作为分类边界但这不是金科玉律。入侵检测场景要做的是在召回率和误报率之间找一个业务可接受的平衡点。把 predict_proba 输出的概率拿出来遍历几个阈值分别看精确率和召回率from sklearn.metrics import precision_score, recall_score for threshold in [0.2, 0.3, 0.4, 0.5]: y_pred_t (y_proba threshold).astype(int) pr precision_score(y_test, y_pred_t) rc recall_score(y_test, y_pred_t) print(fthreshold{threshold:.1f} precision{pr:.4f} recall{rc:.4f})阈值降到 0.3通常会多抓出一批概率不高的攻击但正常流量的误报也会同步上升。具体选哪个值要看告警处理能力值班人员一天能处理 50 条告警就不能把误报率压到每天 500 条。我一般会先选召回率明显提升的阈值再叠加白名单和告警聚合把误报的伤害降到可控范围。白名单是必须的。业务系统每天的定时任务、监控探针、备份流量行为模式跟攻击完全不同但模型可能把它们判成异常。把这些资产五元组加进白名单能让告警量立刻降下来。告警聚合也重要同一源 IP 在短时间内连续触发告警应该合并成一条事件而不是让值班电话被打爆。模型输出的是概率但告警系统消费的是事件中间这层转换做好了系统才有人愿意用。5. 落地避坑我在这类系统上踩过的五个问题5.1 特征穿越归一化在切分前做线上模型分数虚高的头号原因现象本地测试集分类报告漂亮得吓人F1 接近 0.99部署到线上模型表现断崖式下降几乎每一分钟都在误报。原因数据管道里把 StandardScaler 或者 LabelEncoder 写在 train_test_split 之前fit 的时候用到了全部样本的统计量测试集的信息已经泄漏进模型。解决严格先切分再 fit 变换器或者直接用 sklearn 的 Pipeline 把流程串起来。# 错误做法切分前就 fit scaler scaler StandardScaler().fit(X) X_scaled scaler.transform(X) X_train, X_test train_test_split(X_scaled, y, test_size0.2) # 正确做法先切分再 fit X_train, X_test train_test_split(X, y, test_size0.2, random_state42) scaler StandardScaler().fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test)除了归一化特征选择也有同样的坑。如果先用全部数据算出特征重要性再基于这个结果裁掉若干特征然后划分数据集也算特征穿越。安全的做法是特征选择只依赖训练集或者把特征选择放进交叉验证的每一折里面。5.2 抓包丢包流量特征从源头就是坏的模型救不回来现象线上环境抓了一小时 pcap用工具转成流量特征后发现小流数量异常少超时流占比高得离谱。模型训练时学到的特征统计规律在线上用不起来。原因在繁忙业务机上直接用 tcpdump 抓包内核缓冲区被塞满后主动丢包丢的恰好是大量短连接而短连接上的异常行为往往正是攻击探测的特征。解决抓包时用合适的 snaplen 和缓冲区参数尽量在交换机镜像口或专用分流设备上抓。# 只看包头部降低拷贝开销调大内核缓冲区减少丢包 tcpdump -i eth0 -nn -s 64 -B 4096 -w capture.pcap-s 64 表示只抓每个包的前 64 字节对流量特征计算来说足够-B 4096 把缓冲区设为 4MB。即便如此在千兆以上链路仍然可能丢包。更稳的做法是直接用 NF 流数据或者 Zeek 在旁路采集让特征提取进程处理的是已经聚合过的流记录而不是原始报文。5.3 随机切分的幻觉同一条流不该同时出现在训练和测试现象离线评估 AUC 0.97上线后模型对某些历史攻击完全没反应。原因用 train_test_split 按行随机切分时同一条五元组流的前半段分进训练集、后半段分进测试集模型在训练时已经见过这条流的另一半评估分数虚高。解决按流 ID 分组切分。from sklearn.model_selection import GroupShuffleSplit # flow_ids 是每条样本对应的五元组哈希同一条流的样本拥有同一个 id gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsflow_ids)) X_train, X_test X.iloc[train_idx], X.iloc[test_idx] y_train, y_test y.iloc[train_idx], y.iloc[test_idx]如果是 CICIDS2017更省事的办法是按日期分组拿前四天的流量训练用第五天的流量测试这样时间维度上的独立性也保证了。分组切分后模型分数通常会掉一截那是真实水平的反映别慌。5.4 部署后的延迟问题特征计算比模型推理更贵现象单条样本预测只要几毫秒但流量一上来管道处理速度跟不上网络速率告警越来越滞后。原因真正的瓶颈不在模型推理而在特征提取。每一条流都要聚合持续时间、包数量、字节数、各类标志位统计纯 Python 逐包计算在并发大时非常慢。解决把特征计算写成批量窗口模式用 pandas 向量化操作替代逐行循环模型用 joblib.load 加载后复用同一个实例不要每次预测都重新加载。clf joblib.load(ids_rf.joblib) features extract_features_batch(flow_buffer) # 向量化批量特征计算 prob clf.predict_proba(features)[:, 1]另一个有效的手段是做特征缓存。多个五元组相同、时间窗口重叠的流量很多统计结果是重复的缓存能省掉大量重复计算。如果模型本身推理时间也不理想可以把随机森林导出成 ONNX 格式推理单条延迟能再压一个量级。5.5 误报把人逼疯模型落地后第一个被关掉的是告警通道现象告警平台每天刷出一千多条红色告警值班同事先看几眼发现大半是误报然后直接把告警静默掉系统形同虚设。原因阈值调太低加上正常业务流量模式多样模型把内部运维系统的批处理任务、监控探针探测全当成攻击。解决分层告警加白名单加反馈闭环。具体做法是给每个告警打置信度标签概率在 0.8 以上走紧急通道0.5 到 0.8 走普通工单低于展示阈值只记录不打扰。同时把目标资产分组内部监控流量单独建模或直接白名单放行。每一条人工确认过“这是误报”的告警回到训练数据里重新参与模型迭代这个闭环才是机器学习系统能持续变好的根本。6. 从离线模型到实时检测滑动窗口特征计算的进阶实践6.1 滑动窗口把离线模型搬到实时流量上的最小思路离线模型跑得很好不代表实时可用因为流量是连续到来的特征必须在时间窗口内动态聚合。常见做法是维护一个滑动窗口窗口内累计五元组、包数、字节数等统计量每隔一段时间或固定包数计算一次特征并预测。核心代码结构是这样from collections import deque class SlidingWindowDetector: def __init__(self, model, window5.0, threshold0.3): self.model model self.window window self.threshold threshold self.buffer deque() def feed(self, ts, flow_id, length): self.buffer.append((ts, flow_id, length)) while self.buffer and self.buffer[0][0] ts - self.window: self.buffer.popleft() if len(self.buffer) % 100 0: features self._aggregate(self.buffer) score self.model.predict_proba([features])[0][1] if score self.threshold: self._alert(flow_id, score)deque 在这里很合适窗口过期数据从左侧弹出新数据从右侧进入时间复杂度是 O(1)。窗口大小直接影响特征时效性5 秒窗口对扫描和爆破类攻击够用短连接攻击可以缩到 2 秒但要容忍更多噪声。6.2 回放验证离线指标说明不了在线延迟做完滑动窗口下一步是验证在线版本和离线版本差距多大。常见做法是把 CICIDS2017 的 CSV 按时间排序后逐条喂给在线检测管道记录每条样本的预测时间再对照离线评估指标的差异。你会发现回放指标通常比离线差一点原因主要来自窗口截断离线样本是完整流切好的在线窗口可能只覆盖了一个攻击流的一半。如果回放结果掉点太多优先检查窗口大小和特征聚合口径而不是怀疑模型。我早期做这类系统时有过一个教训训练阶段全默认参数什么数据都能跑出高分上线后误报率高到没人看最后被迫返工。后来明白做入侵检测的第一目标不是把准确率刷高而是把误报控制在人愿意处理的范围里。先定义清楚这个目标再选模型、调阈值、设计验证流程顺序不能反。希望这些经验能帮你在做基于机器学习的入侵检测系统时少走几步弯路直接碰到问题的正面上。本文还有配套的精品资源点击获取