从零复现机器学习入侵检测系统:NSL-KDD数据集实战与XGBoost调参
简介这份资源是面向计算机、软件工程、人工智能等专业学生的高分毕业设计参考包主题为基于Python与机器学习的入侵检测系统适合用作毕设、课程设计或项目立项演示也便于初学者进阶学习。压缩包共23个文件约19KB以py源码、xml配置、md说明文档为主另含gitignore、license等工程辅助文件整体结构清晰便于快速理解项目组织方式。资源内含完整源码、数据集与详细文档核心模块涵盖数据预处理、SVM等机器学习算法实现以及网络数据包嗅探功能可帮助读者掌握从流量采集、特征处理到模型训练与检测的完整流程。目前已有549人学习下载适合需要完整赛题方案、可运行代码与排错思路的读者参考也可在此基础上修改扩展实现更多功能。1. 从零复现一套机器学习入侵检测系统这套毕业设计源码到底能跑出什么结果很多同学拿到「基于机器学习的入侵检测系统」这个题目时第一反应是去搜现成的源码包下载下来发现跑不起来或者跑起来了但说不清模型到底在干什么。我当年做类似课题时也踩过这个坑数据集加载报编码错误、特征列对不上、训练完准确率 99% 但换个数据集直接崩盘。这套毕业设计方向的核心价值不在于「有一个能跑的 demo」而在于让你真正理解从原始网络流量到分类决策的完整链路——数据怎么清洗、特征怎么选、模型怎么训、结果怎么解释。它适合正在做计算机毕业设计、软件工程毕业设计或大数据毕业设计的同学也适合刚入门机器学习想找一个完整项目练手的开发者。下面我按实际落地顺序把这套系统的每个环节拆开讲清楚包括参数怎么设、代码怎么写、哪里容易翻车。2. 入侵检测系统的数据底座NSL-KDD 数据集加载与特征工程2.1 为什么选 NSL-KDD 而不是自己抓包做入侵检测数据集的选取直接决定你后面所有工作的上限。常见做法是用 NSL-KDD 或 CIC-IDS2017前者体量小、标注清晰、学术界引用多适合毕业设计这种需要快速验证且要写论文的场景。KDD Cup 99 虽然经典但存在大量冗余记录训练出来的模型会偏向高频攻击类型答辩时被问到泛化能力容易翻车。NSL-KDD 去掉了重复样本训练集和测试集的攻击类型分布也更合理。我一般会先把数据集的文件结构理清楚。NSL-KDD 通常包含 KDDTrain.txt 和 KDDTest.txt 两个文件每行 43 列前 41 列是特征第 42 列是标签第 43 列是难度分数。特征分为四类TCP 连接基本特征如 duration、protocol_type、service、内容特征如 logged_in、num_failed_logins、基于时间的流量特征如 count、srv_count、基于主机的流量特征如 dst_host_count、dst_host_srv_count。理解这四类特征的物理含义比盲目调参重要得多。2.2 用 pandas 加载数据并处理类别不平衡直接读入数据后你会发现标签列有 20 多种具体攻击类型但毕业设计通常只需要区分正常和异常或者按四大类攻击DoS、Probe、R2L、U2R归类。下面这段代码是我常用的加载和预处理模板import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, StandardScaler # 列名定义NSL-KDD 官方文档给出的 41 个特征名 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,difficulty] train pd.read_csv(KDDTrain.txt, namescol_names) test pd.read_csv(KDDTest.txt, namescol_names) # 把具体攻击类型归为五大类方便后续多分类或二分类 attack_map { normal: normal, back: DoS, land: DoS, neptune: DoS, pod: DoS, smurf: DoS, teardrop: DoS, apache2: DoS, udpstorm: DoS, processtable: DoS, worm: DoS, satan: Probe, ipsweep: Probe, nmap: Probe, portsweep: Probe, mscan: Probe, saint: Probe, guess_passwd: R2L, ftp_write: R2L, imap: R2L, phf: R2L, multihop: R2L, warezmaster: R2L, warezclient: R2L, spy: R2L, xlock: R2L, xsnoop: R2L, snmpguess: R2L, snmpgetattack: R2L, httptunnel: R2L, sendmail: R2L, named: R2L, buffer_overflow: U2R, loadmodule: U2R, rootkit: U2R, perl: U2R, sqlattack: U2R, xterm: U2R, ps: U2R } for df in [train, test]: df[attack_category] df[label].map(attack_map) df.drop([label, difficulty], axis1, inplaceTrue) # 类别型特征编码 cat_cols [protocol_type, service, flag] for col in cat_cols: le LabelEncoder() train[col] le.fit_transform(train[col]) # 测试集用训练集的编码器遇到未见过的类别统一归为 -1 test[col] test[col].map(lambda x: le.transform([x])[0] if x in le.classes_ else -1) # 数值特征标准化 num_cols [c for c in train.columns if c not in cat_cols [attack_category]] scaler StandardScaler() train[num_cols] scaler.fit_transform(train[num_cols]) test[num_cols] scaler.transform(test[num_cols]) print(训练集形状:, train.shape) print(测试集形状:, test.shape) print(训练集类别分布:\n, train[attack_category].value_counts())这段代码的关键点有三个。第一测试集的 LabelEncoder 必须复用训练集拟合好的编码器否则同一类 service 在训练集和测试集里可能被编成不同数字模型直接学废。第二StandardScaler 同理只能 fit 训练集再 transform 测试集这是防止数据泄露的基本功。第三num_outbound_cmds 这一列在 NSL-KDD 里全是 0训练前可以直接删掉减少无效特征。2.3 特征选择用随机森林看哪些特征真正在起作用41 个特征不是每个都有用。我一般会先跑一个随机森林看 feature_importances_把重要性低于 0.005 的特征砍掉通常能压到 20 个左右训练速度提升明显准确率基本不掉。这一步在答辩时也很加分因为你能说清楚「为什么选这些特征」。from sklearn.ensemble import RandomForestClassifier import matplotlib.pyplot as plt X_train train.drop(attack_category, axis1) y_train train[attack_category] rf RandomForestClassifier(n_estimators100, random_state42, n_jobs-1) rf.fit(X_train, y_train) importances pd.Series(rf.feature_importances_, indexX_train.columns) importances.sort_values(ascendingFalse).head(20).plot(kindbarh) plt.title(Top 20 Feature Importances) plt.tight_layout() plt.show() # 筛选重要性大于阈值的特征 selected_features importances[importances 0.005].index.tolist() print(保留特征数:, len(selected_features))参数上n_estimators 设 100 是性价比比较高的选择再往上加收益递减。random_state 固定住保证结果可复现n_jobs-1 用满 CPU 核数。如果跑完发现 src_bytes、dst_bytes、count、srv_count 这几个特征重要性排前几说明模型确实在学流量统计规律而不是靠标签泄露作弊。3. 模型训练与调参从逻辑回归到 XGBoost 的完整对比3.1 先跑一个逻辑回归当基线不要一上来就上深度学习。我见过太多毕业设计直接堆 LSTM结果数据量不够过拟合到亲妈都不认识。先用逻辑回归跑一个基线知道「简单模型能到多少分」后面复杂模型的提升才有说服力。from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, accuracy_score X_test test.drop(attack_category, axis1) y_test test[attack_category] # 只用选出来的特征 X_train_sel train[selected_features] X_test_sel test[selected_features] lr LogisticRegression(max_iter1000, multi_classmultinomial, n_jobs-1) lr.fit(X_train_sel, y_train) y_pred_lr lr.predict(X_test_sel) print(逻辑回归准确率:, accuracy_score(y_test, y_pred_lr)) print(classification_report(y_test, y_pred_lr))max_iter 设 1000 是因为默认的 100 在 NSL-KDD 上经常不收敛会报 ConvergenceWarning。multi_classmultinomial 做多分类比默认的 ovr 效果通常好一点。跑完你会看到准确率大概在 75% 到 80% 之间U2R 和 R2L 的召回率很低这是正常现象因为这两类样本太少。3.2 XGBoost 调参三个必须盯住的参数XGBoost 在表格数据上基本是绕不开的。我一般会重点调三个参数n_estimators、max_depth、learning_rate。下面是一个我常用的参数搜索框架import xgboost as xgb from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import LabelEncoder # XGBoost 需要标签是 0 到 n-1 的整数 le_target LabelEncoder() y_train_enc le_target.fit_transform(y_train) y_test_enc le_target.transform(y_test) param_grid { n_estimators: [100, 200, 300], max_depth: [4, 6, 8], learning_rate: [0.01, 0.05, 0.1], subsample: [0.8, 1.0], colsample_bytree: [0.8, 1.0] } xgb_clf xgb.XGBClassifier( objectivemulti:softmax, num_classlen(le_target.classes_), random_state42, n_jobs-1, eval_metricmlogloss ) grid GridSearchCV(xgb_clf, param_grid, cv3, scoringf1_macro, verbose1, n_jobs-1) grid.fit(X_train_sel, y_train_enc) print(最佳参数:, grid.best_params_) print(最佳 F1-macro:, grid.best_score_) best_model grid.best_estimator_ y_pred_xgb best_model.predict(X_test_sel) print(测试集准确率:, accuracy_score(y_test_enc, y_pred_xgb)) print(classification_report(y_test_enc, y_pred_xgb, target_namesle_target.classes_))这里有几个血泪经验。第一scoring 用 f1_macro 而不是 accuracy因为类别不平衡时 accuracy 会骗人。第二cv3 是速度和稳定性的折中毕业设计的数据量用 5 折会很慢。第三learning_rate 设 0.01 时 n_estimators 要相应加大否则欠拟合设 0.1 时 100 到 200 棵树通常够用。跑完 XGBoost 的准确率一般能到 85% 以上F1-macro 在 0.7 到 0.8 之间。3.3 用混淆矩阵定位模型到底错在哪准确率只是一个数字答辩老师更关心你知不知道模型为什么错。画混淆矩阵是最直接的方式from sklearn.metrics import confusion_matrix import seaborn as sns cm confusion_matrix(y_test_enc, y_pred_xgb) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsle_target.classes_, yticklabelsle_target.classes_) plt.xlabel(Predicted) plt.ylabel(True) plt.title(XGBoost Confusion Matrix) plt.tight_layout() plt.show()你会看到 U2R 和 R2L 经常被误判成 normal原因是这两类在训练集里样本极少模型倾向于把不确定的样本扔给多数类。解决办法有两个一是用 SMOTE 过采样少数类二是调 class_weight 或 scale_pos_weight。我一般先试 class_weightbalanced不行再上 SMOTE因为 SMOTE 在特征空间里插值对类别型特征编码后的数据可能生成不合理的样本。4. 避坑与排查这套系统最容易翻车的五个地方4.1 测试集编码出现 -1 导致模型报错现象跑测试集预测时XGBoost 报 feature mismatch 或预测结果全错。原因测试集里出现了训练集没有的 service 或 flag 类别LabelEncoder 映射成了 -1而 -1 在训练集里不存在模型没见过这个值。解决在编码阶段就把未见过的类别统一映射到一个训练集里存在的默认值比如用训练集里出现频率最高的类别填充或者直接删掉这些样本如果数量很少。4.2 标准化时 fit 了全量数据造成数据泄露现象交叉验证分数很高但测试集分数骤降。原因在划分训练测试之前就做了 StandardScaler.fit_transform测试集的统计信息泄露到了训练过程。解决永远先划分数据集再 fit 训练集transform 测试集。如果做交叉验证标准化要放在 Pipeline 里让每一折单独 fit。4.3 类别不平衡导致 U2R 召回率为零现象分类报告里 U2R 的 recall 是 0.00。原因U2R 在训练集里只有几十条模型完全忽略了这一类。解决先试 class_weightbalanced再试 SMOTE 过采样最后考虑把 U2R 和 R2L 合并成一个大类或者只做二分类normal vs attack在论文里说明多分类的局限性。4.4 特征重要性筛选后测试集特征对不上现象用 selected_features 筛选训练集后测试集用同样的列表筛选报 KeyError。原因测试集经过 LabelEncoder 后列名没变但可能因为某些列全为 0 被 pandas 推断成了不同 dtype或者筛选时列顺序不一致。解决筛选后统一用 X_train_sel.columns 重新索引测试集确保列顺序完全一致。4.5 模型保存后加载预测结果不一致现象用 joblib 保存模型重新加载后预测结果和保存前不一样。原因XGBoost 的 n_jobs 在多线程下可能有浮点精度差异或者保存时没把 LabelEncoder 一起存。解决保存时用 joblib.dump 把模型、scaler、label_encoder 打包成一个字典一起存加载时按同样顺序还原。n_jobs 设 1 可以保证完全可复现但速度会慢。5. 把模型跑成可演示的系统Flask 接口与结果可视化技巧5.1 用 Flask 包一个预测接口毕业设计答辩时光有 notebook 不够最好能演示一个「输入一条流量记录输出是否异常」的界面。用 Flask 包一层是最快的方式from flask import Flask, request, jsonify import joblib import numpy as np app Flask(__name__) # 加载打包好的模型和预处理器 bundle joblib.load(ids_model_bundle.pkl) model bundle[model] scaler bundle[scaler] encoders bundle[encoders] selected_features bundle[features] target_encoder bundle[target_encoder] app.route(/predict, methods[POST]) def predict(): data request.get_json() # data 是一个字典包含 41 个原始特征 df pd.DataFrame([data]) # 用保存的编码器处理类别特征 for col, le in encoders.items(): df[col] df[col].map(lambda x: le.transform([x])[0] if x in le.classes_ else -1) # 标准化 num_cols [c for c in df.columns if c not in encoders] df[num_cols] scaler.transform(df[num_cols]) # 筛选特征 X df[selected_features] pred model.predict(X)[0] label target_encoder.inverse_transform([pred])[0] return jsonify({prediction: label}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这段代码的关键是把训练阶段所有预处理器和模型打包成一个 bundle预测时按训练时的顺序依次处理。debugFalse 是防止生产环境暴露调试信息虽然毕业设计演示无所谓但养成习惯没坏处。5.2 用混淆矩阵和特征重要性图撑起论文的「实验结果」章节论文里的实验结果不能只有一行准确率。我一般会放三张图混淆矩阵、Top 20 特征重要性条形图、不同模型的 F1-macro 对比柱状图。这三张图能覆盖「模型效果」「特征解释」「选型依据」三个维度答辩时被问到任何一个都有东西可讲。图表类型横轴纵轴说明什么混淆矩阵预测类别真实类别哪些类容易混特征重要性特征名重要性分数模型依赖哪些特征模型对比模型名F1-macro选型依据5.3 一个我常用的演示技巧用真实流量样本做端到端测试答辩前我会准备几条从测试集里挑出来的样本分别属于 normal、DoS、Probe用 curl 直接打接口现场看返回结果。这样比在 notebook 里跑 cell 更有说服力也能证明系统是真的能对外服务。命令大概是这样curl -X POST http://127.0.0.1:5000/predict \ -H Content-Type: application/json \ -d {duration:0,protocol_type:tcp,service:http,flag:SF,src_bytes:215,dst_bytes:45076,land:0,wrong_fragment:0,urgent:0,hot:0,num_failed_logins:0,logged_in:1,num_compromised:0,root_shell:0,su_attempted:0,num_root:0,num_file_creations:0,num_shells:0,num_access_files:0,num_outbound_cmds:0,is_host_login:0,is_guest_login:0,count:1,srv_count:1,serror_rate:0,srv_serror_rate:0,rerror_rate:0,srv_rerror_rate:0,same_srv_rate:1,diff_srv_rate:0,srv_diff_host_rate:0,dst_host_count:0,dst_host_srv_count:0,dst_host_same_srv_rate:0,dst_host_diff_srv_rate:0,dst_host_same_src_port_rate:0,dst_host_srv_diff_host_rate:0,dst_host_serror_rate:0,dst_host_srv_serror_rate:0,dst_host_rerror_rate:0,dst_host_srv_rerror_rate:0}注意 JSON 里的字段名要和训练时的列名完全一致少一个字段 Flask 里构造 DataFrame 就会缺列。我一般会在接口里加一个字段校验缺字段直接返回 400避免现场演示时尴尬。这套系统从数据加载到接口演示完整跑通大概需要两到三天其中调参和排查占一半时间。我的习惯是先把基线跑通再逐步加特征选择和模型对比每改一步就存一次模型和结果免得后面改崩了没有后悔药。希望帮到你。本文还有配套的精品资源点击获取