简介:基于NSL-KDD数据集的网络入侵检测Python源码与运行说明打包为一体,是一个经导师指导并获评审98分的高分设计项目,主要面向计算机专业毕设、课程设计及期末大作业场景。压缩包共27个文件、约29.98MB,其中含10个CSV数据集文件、4个Jupyter Notebook(覆盖数据获取、PCA降维建模、无PCA建模与KDD数据集评估)、3个MATLAB模型文件、1个Python脚本,以及7个txt、1个md和1个docx格式的运行说明与文档,结构清晰便于查阅。项目提供带GUI界面的演示程序,核心流程涵盖数据预处理、特征降维、模型训练与性能评估,帮助使用者快速复现网络入侵检测实验。模型层同时给出MATLAB与Python双版本,并附有评估脚本,便于对照不同工具链的结果。目前已有314人学习下载,适合需要完整项目参考的学生直接改编或扩展,实现从数据到模型的可视化落地。
1. NSL-KDD入侵检测:为什么课程设计都选它,以及它到底能做什么
如果你打开任何一个招聘网站的“网络安全算法工程师”岗位描述,大概率能看到“熟悉KDD Cup/NSL-KDD等公开数据集”这一条。NSL-KDD作为KDD Cup 99数据集的去冗余版本,训练集125973条、测试集22544条,每条记录带41维特征和1个攻击标签,恰好覆盖了分类任务的全部标准流程。对要做课程大作业的本科生来说,它的价值不只是“数据集现成”,而是能在一个项目里同时演示数据清洗、特征工程、模型交叉验证和结果可视化——这几样正好是评分老师最爱看的模块。
这套方案不挑显卡、不依赖深度学习框架,用sklearn和pandas就能跑通。我见过太多同学做完后只会说“准确率99%”,但回答不出“对哪类攻击漏报最多”。本文就是从数据读取到模型评估、再到避坑的完整落地路径,按着走能把这份大作业做成能讲清楚、能扛住答辩的项目。
2. 拿到NSL-KDD数据集先做三件事:格式解析、标签映射、数据清洗
2.1 NSL-KDD的文件构成与41维特征含义
NSL-KDD数据集常见的文件命名是KDDTrain+.txt和KDDTest+.txt,不用.csv后缀,这点和大部分教科书示例不一样。文件里没有表头,每一行是一整条网络连接记录,字段之间用逗号分隔,最后一列是标签。所以pd.read_csv读取时不能直接默认第一行当列名。
我一般先把41个特征名列出来放成一个列表,再传给names参数:
import pandas as pd # NSL-KDD的41个特征名,按原数据集列顺序排列 cols = [ "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" ] train_df = pd.read_csv("KDDTrain+.txt", header=None, names=cols + ["label"]) test_df = pd.read_csv("KDDTest+.txt", header=None, names=cols + ["label"]) print(train_df.shape, test_df.shape) print(train_df["label"].value_counts())names=cols + ["label"]是把41个特征名和标签列一起指定。header=None很关键,否则第一行数据会被当成列名,后面所有列访问都会错位。打印label分布是为了确认各类攻击是否齐全。
这份数据41维特征按语义分三组:基本连接特征(duration、protocol_type、service、flag、src_bytes、dst_bytes等),内容特征(hot、num_failed_logins、logged_in等,记录连接内容层面的行为),流量统计特征(count、srv_count、serror_rate等,按时间窗统计的历史流量行为)。最后10个dst_host_开头的特征进一步刻画目标主机维度的聚合统计。第一次做的人不需要逐列理解,但要能说清“这些特征从三个层面描述一次网络连接”,答辩时这就是加分项。
2.2 标签处理:从五分类到二分类的映射逻辑
标签列不是干净的“attack”和“normal”,而是满屏的具体攻击名。常见的映射做法是把所有攻击名归属到4大类:DoS、Probe、R2L、U2R,和正常流量合并后就是5分类。如果大作业只要求检测异常流量,则退化成二分类——正常标记0,其余攻击统一标记1。
attack_map = { "normal": "normal", "back": "dos", "land": "dos", "neptune": "dos", "pod": "dos", "smurf": "dos", "teardrop": "dos", "apache2": "dos", "udpstorm": "dos", "mailbomb": "dos", "ipsweep": "probe", "nmap": "probe", "portsweep": "probe", "satan": "probe", "saint": "probe", "mscan": "probe", "guess_passwd": "r2l", "ftp_write": "r2l", "imap": "r2l", "multihop": "r2l", "phf": "r2l", "spy": "r2l", "warezclient": "r2l", "warezmaster": "r2l", "named": "r2l", "sendmail": "r2l", "snmpgetattack": "r2l", "snmpguess": "r2l", "worm": "r2l", "xlock": "r2l", "xsnoop": "r2l", "httptunnel": "r2l", "buffer_overflow": "u2r", "loadmodule": "u2r", "perl": "u2r", "rootkit": "u2r", "sqlattack": "u2r", "xterm": "u2r", "ps": "u2r" } def map_label(label): return attack_map.get(label, "unknown") for df in (train_df, test_df): df["attack_type"] = df["label"].apply(map_label) df["is_attack"] = (df["attack_type"] != "normal").astype(int) print(train_df["attack_type"].value_counts()) print(test_df["attack_type"].value_counts())get(label, "unknown")的兜底逻辑很重要,测试集里可能出现训练集没见过的攻击名变体,兜底能防止映射中断。映射后做两个新列:attack_type保留类别语义,is_attack给二分类器用。打印两个分布对照,能直接看出测试集里U2R和R2L样本极少——这个现象会决定后面模型的评估方式。
这里还有个小细节:训练集和测试集的攻击子类分布不同,比如测试集新增了若干种R2L攻击名。这是NSL-KDD人为设计的难度,意味着模型如果只是“背答案”,在测试集上会明显露馅。大作业的高分关键就在这里——不是训练集准确率多高,而是测试集上的漏报率多低。
2.3 缺失值与异常值处理:别让脏数据带偏模型
NSL-KDD整体上缺失值很少,但“少”不等于“没有”。读取后第一件事应该是检查NaN和无穷值,否则后面StandardScaler会直接报错或者算出NaN。我没少吃这种亏,现在每拿到一份数据都先跑一遍体检。
import numpy as np def check_data_quality(df, name): print(f"--- {name} ---") print("缺失值总数:", df.isnull().sum().sum()) print("无穷值总数:", np.isinf(df.select_dtypes(include=[np.number])).sum().sum()) print("每列缺失值分布:") print(df.isnull().sum()[df.isnull().sum() > 0]) check_data_quality(train_df, "训练集") check_data_quality(test_df, "测试集")如果发现缺失值,策略分两种:数值列用中位数填充比均值更稳,避免被极值带偏;类别列用众数填充,或者直接补一个“unknown”类别。NSL-KDD里num_outbound_cmds这一列在大样本上全是0,属于几乎无意义的特征,但不要急着删——先带着,看到特征重要性输出后再决定。
异常值方面,src_bytes和dst_bytes会出现远大于均值的大数值。这类流量特征做二分类时,异常值会在标准化后变成极端z-score,影响树模型的阈值切分。常见的处理是取对数:np.log1p(df["src_bytes"]),既压缩量纲又保留大小顺序,尤其适合字节数这类长尾分布数据。我建议把这步放进特征工程而不是数据清洗,留给模型对比时做AB测试。
3. 特征工程与模型选型:把41维特征变成模型能吃的输入
3.1 数值特征标准化与类别特征独热编码
41维特征里混着连续数值、离散数值和字符串类别。protocol_type、service、flag这三列是字符串,必须转数值。常见的坑是把它们直接LabelEncoder成整数一列——这会让树模型误认为不同取值之间有大小关系,比如tcp=2比udp=1“更大”。正确做法是独热编码。
from sklearn.preprocessing import StandardScaler from sklearn.compose import ColumnTransformer cat_cols = ["protocol_type", "service", "flag"] num_cols = [c for c in cols if c not in cat_cols] train_df["src_bytes_log"] = np.log1p(train_df["src_bytes"]) test_df["src_bytes_log"] = np.log1p(test_df["src_bytes"]) preprocessor = ColumnTransformer( transformers=[ ("num", StandardScaler(), num_cols), ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols) ] )ColumnTransformer把数值列和类别列一条流水线处理完。注意OneHotEncoder里的handle_unknown="ignore"是必填项:测试集可能出现训练集没有见过的service值,如果没有这个参数,transform阶段会直接抛异常。这也是“不同分布”的数据集之间最常见的兼容性问题。
src_bytes_log这列我建议单独加,不加也行,但加了之后随机森林的OOB准确率通常能涨零点几个百分点。逻辑是把极端长尾压缩,让模型更容易学到“正常流量和攻击流量的字节数分布差异”。
独热编码后特征维度会从41增加到120左右,service取值最多,贡献了大头。这个维度规模对随机森林和MLP都不构成压力,不需要额外做PCA降维。大作业里如果有人硬要做PCA,我一般不建议——它会破坏特征的业务可解释性,答辩时老师问“第一主成分代表什么”,答不好反而扣分。
3.2 模型选型对比:随机森林、MLP、KNN的取舍
NSL-KDD这个大作业的模型选择,遵循一个简单原则:先跑基线模型,再上复杂模型。常见的基线是逻辑回归和决策树,拿到准确率和F1的下限,再换随机森林或MLP做提升。
我一般会对比三个模型:随机森林(训练快、有特征重要性)、MLP(能拟合非线性关系、答辩有话讲)、KNN(原理简单但预测慢、特征维度敏感)。三者的取舍直接写进报告里就是一份很好的实验对比章节。
from sklearn.ensemble import RandomForestClassifier from sklearn.neural_network import MLPClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.pipeline import Pipeline models = { "random_forest": Pipeline([ ("preprocess", preprocessor), ("clf", RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)) ]), "mlp": Pipeline([ ("preprocess", preprocessor), ("clf", MLPClassifier( hidden_layer_sizes=(64, 32), max_iter=50, random_state=42 )) ]), "knn": Pipeline([ ("preprocess", preprocessor), ("clf", KNeighborsClassifier(n_neighbors=5, n_jobs=-1)) ]) }把预处理器和模型绑进Pipeline是避免数据泄露的标准做法:fit时只让模型看到训练集,transform统一在Pipeline内部完成,不能先对整个数据集做标准化再划分训练测试集,否则测试集的信息提前混进了训练过程,评估结果虚高。这个错误在大作业里出现频率极高,答辩时属于一问一个准的扣分点。
MLP的max_iter=50是保守配置,NSL-KDD在CPU上跑100轮大概几分钟,50轮能看趋势但可能没收敛。我建议初跑时设50看loss曲线,确认下降趋势后再加到200。KNN在独热编码后必须做标准化,否则service的取值频次会把距离度量带偏——Pipeline里的StandardScaler就是干这个的。
4. 训练与评估:从混淆矩阵到检测率,大作业评分看什么
4.1 训练集划分与交叉验证设置
NSL-KDD自带训练集和测试集,很多同学直接拿这两个文件训完就交,不划分验证集。这有个隐患:所有调参决策都在测试集上做,最后报告的测试集分数是有偏的。我一般把官方训练集再切出20%当验证集,官方测试集只保留最终评估用途。
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score X = train_df.drop(columns=["label", "attack_type", "is_attack"]) y = train_df["is_attack"] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) print("训练集攻击占比:", y_train.mean()) print("验证集攻击占比:", y_val.mean())stratify=y按类别比例划分,保证训练集和验证集的攻击占比一致。NSL-KDD里攻击和正常的比例大约是46:53,如果不分层,随机划分可能让验证集里某一类样本特别少,F1波动剧烈。
交叉验证用来估分,这个阶段不会重新训练。StratifiedKFold按类别比例折分,适合类别不平衡的场景。大作业报告里写“5折交叉验证平均F1为0.9x”比单次划分更有说服力,也能防止某一次运气差的划分让结果很难看。
4.2 评估指标:准确率、精确率、召回率、F1的读法
大作业里最容易被挑战的地方是“只看准确率”。NSL-KDD测试集本身的类别分布与训练集不同,而且R2L和U2R的样本量极少。二分类准确率即使到99%,可能只因为U2R样本在测试集里占0.5%,全判错也不影响准确率多少。
| 指标 | 含义 | 大作业里的读法 |
|---|---|---|
| 准确率 | 全部预测正确的比例 | 只能作参考,不能作唯一指标 |
| 精确率 | 预测为攻击中真攻击的比例 | 误报率高时精确率低 |
| 召回率 | 真攻击中被识别出的比例 | 也叫检测率,漏报率=1-召回率 |
| F1 | 精确率与召回率的调和平均 | 类别不平衡时的核心指标 |
from sklearn.metrics import classification_report, confusion_matrix, f1_score y_pred = model.predict(X_val) print(classification_report(y_val, y_pred, target_names=["normal", "attack"]))classification_report一行输出精确率、召回率、F1和一列support(各类样本数)。答辩时老师只要看到你关注attack这一行的召回率,就不会质疑你“只会刷准确率”。如果验证集上attack的召回率低于0.9,先调整阈值或换模型,不要急着上测试集。
这里我特别强调:多次运行结果可能有小幅波动,不要惊慌,先固定random_state=42再做对比实验,否则你无法判断效果提升来自模型改进还是随机种子漂移。
4.3 用matplotlib把结果可视化,报告不用愁
大作业的排版和可视化质量,经常比模型本身的高一个档次更影响评分。一张标准的混淆矩阵热力图加一条ROC曲线,能把“模型有效”这件事直观地讲清楚。
import matplotlib.pyplot as plt from sklearn.metrics import RocCurveDisplay # 混淆矩阵 cm = confusion_matrix(y_val, y_pred) fig, ax = plt.subplots(figsize=(5, 4)) im = ax.imshow(cm, cmap="Blues") ax.set_xticks([0, 1], ["normal", "attack"]) ax.set_yticks([0, 1], ["normal", "attack"]) plt.colorbar(im) for i in range(2): for j in range(2): ax.text(j, i, cm[i, j], ha="center", va="center", fontsize=14) ax.set_xlabel("预测值") ax.set_ylabel("真实值") plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150) plt.show()画完后保存PNG,报告里直接插入。dpi=150保证打印不模糊。矩阵四个格子的读法是:左上为正常流量正确识别,右下为攻击流量正确识别,右上为正常被误判为攻击(误报),左下为攻击被判成正常(漏报)。多数高分报告的亮点就是在左下角做文章——说明你对“漏报”这个安全场景有理解。
ROC曲线我用RocCurveDisplay.from_estimator,一行代码画完:
RocCurveDisplay.from_estimator(pipeline, X_val, y_val) plt.savefig("roc_curve.png", dpi=150) plt.show()AUC达到0.9以上是常态,大作业能稳定复现这个结论就足够。比0.99更高的追求,对课程设计来说边际收益不高,不如把时间花在错误分析上。
5. 避坑指南:NSL-KDD项目最常见的5个翻车现场
5.1 坑1:pandas读取文件路径含中文或空格直接报错
现象:pd.read_csv("作业 数据/KDDTrain+.txt")抛ParserError或FileNotFoundError,但文件明明就在那里。
原因:Windows下路径含中文或空格时,pandas默认引擎解析CSV的分隔符识别可能失效,常见于文件名是中文、路径带空格的情况。
解决:统一用绝对路径且不带空格,或者指定分隔符。我一般在读取时显式传sep=",",并建议把数据集直接解压成纯英文路径。如果路径里有中文实在改不掉,加engine="python"兜底:
train_df = pd.read_csv("KDDTrain+.txt", header=None, names=cols + ["label"], sep=",", engine="python")5.2 坑2:独热编码后特征维度爆炸,训练直接卡死
现象:service独热编码后特征维度暴涨到700多,KNN训练和预测都慢得无法忍受。
原因:如果把service直接get_dummies而没处理训练集和测试集的类别对齐,或者编码器没有复用,维度会跟着数据膨胀。更隐蔽的情况是训练集和测试集分别做独热编码,列拼接顺序不一致。
解决:用Pipeline里的OneHotEncoder统一编码,靠handle_unknown="ignore"吸收新类别。需要复现编码器时,保存而不是重新fit:
import joblib joblib.dump(preprocessor, "preprocessor.pkl") # 后续加载复用KNN对高维极其敏感,维度从41涨到120以后距离计算质量下降明显,建议要么降维要么换随机森林。大作业里为了“KNN原理简单”硬用KNN,结果预测极慢的案例我见过太多了。
5.3 坑3:测试集和训练集分布不一致导致评估严重失真
现象:训练集F1为0.93,测试集F1掉到0.81,明明同一个模型,差距却很大。
原因:NSL-KDD的测试集包含训练集未出现的攻击子类,且类别比例不同。这是数据集刻意设计的泛化挑战,不是bug。另一个常见原因是标准化和编码时,测试集信息泄露进了训练集,导致验证分数虚高。
解决:严格按fit_transform对训练集、transform对测试集的流程走。用Pipeline是最省心的方案,它能确保这个纪律不需要手动维持。对评估失真问题,把测试集按攻击类型分组分别算召回率,看哪一类掉了,再决定要不要补数据或调模型复杂度。
5.4 坑4:准确率99%但U2R攻击一条没识别出来
现象:classification_report里attack的F1很高,但单独看U2R类别的召回率为0。
原因:U2R在训练集里占比极低(约0.04%),二分类模型学不到它的模式,倾向把所有样本判为正常来降低整体误差。准确率指标完全掩盖了这个问题。
解决:使用class_weight="balanced"给少数类更高权重,或者对U2R单独做特征分析。输出每个类别的分类报告是第一步:
rf = RandomForestClassifier( n_estimators=200, class_weight="balanced", random_state=42, n_jobs=-1 )加了class_weight后U2R召回率会有所提升,但通常还是远低于DoS。答辩时主动承认“U2R类因为样本过少识别率偏低,后续可以通过数据增强或异常检测思路改进”,比被老师问住体面得多,而且能体现你真的理解类别不平衡问题。
5.5 坑5:sklearn版本差异导致代码在老师电脑上跑不起来
现象:本地跑通,发给老师后在对方机器上报AttributeError: 'RandomForestClassifier' object has no attribute 'n_estimators_'之类错误。
原因:sklearn版本跨度大,部分模型参数名或属性在旧版本不存在,比如max_features的默认值在0.22和1.0之间就有变化,None和"auto"的行为不同。
解决:项目根目录放一个requirements.txt,写明版本即可:
numpy>=1.21.0 pandas>=1.3.0 scikit-learn>=1.0.0 matplotlib>=3.5.0如果老师环境版本跨度过大,代码里用sklearn.__version__做条件分支不现实。稳妥做法是用Pipeline封装、少用新API,并把运行说明写清楚——创建虚拟环境、按requirements.txt安装、再跑main.py。
6. 把大作业做成高分交付:调参策略、可视化报告与代码结构优化
6.1 用GridSearchCV做参数搜索的最省心配置
随机森林的高分关键不在树的数量,而在max_depth和min_samples_leaf。树太多只会让训练变慢。我一般用GridSearchCV扫一组小范围参数,控制在10分钟以内出结果:
from sklearn.model_selection import GridSearchCV param_grid = { "clf__max_depth": [10, 20, None], "clf__min_samples_leaf": [1, 5, 10] } grid = GridSearchCV(models["random_forest"], param_grid, cv=3, scoring="f1", n_jobs=-1) grid.fit(X_train, y_train) print(grid.best_params_, grid.best_score_)scoring="f1"让搜索目标对准类别不平衡敏感的F1而不是准确率,cv=3比5折快一半。运行说明里要写清这个搜索大概耗时多久——我在本地跑约5到8分钟,让使用者有心理预期。
6.2 分类报告与混淆矩阵自动导出
改写成脚本后,输出不能只停留在控制台。我习惯把指标自动写入文本文件,报告直接用:
with open("evaluation_report.txt", "w", encoding="utf-8") as f: f.write("验证集分类报告:\n") f.write(classification_report(y_val, y_pred, target_names=["normal", "attack"])) f.write("\n混淆矩阵:\n") f.write(str(confusion_matrix(y_val, y_pred)))同样,混淆矩阵热力图和ROC曲线各存一张PNG。这样报告截图、数据表格、结果图三样素材一步到位,不需要训练完再手动到处翻数字。运行说明里直接写明“运行后会生成evaluation_report.txt和两张图片”,老师一眼就能复现整个实验过程。
6.3 代码组织:一个命令跑通的模块划分
这份大作业如果有一个坑,就是源码只有一个超长notebook,老师打开后不知道从哪里点起。我更推荐按功能拆成三个脚本:data_preprocess.py负责读取和清洗,train_model.py负责训练与调参,evaluate_model.py负责评估和可视化,根目录一个main.py按顺序调用。配合运行说明里的注释,别人拿到压缩包后解压、pip install -r requirements.txt、python main.py三步就能看到全部结果。
做这个项目的过程中我最大的教训是:别把时间耗在反复调参刷那零点几个百分点的准确率上,把“不同模型在验证集上的对比表格”“U2R类别为什么识别率低”“如何用class_weight缓解不平衡”这三个问题想清楚,答辩的深度完全不一样。大作业最终评的是你能不能讲明白每一步为什么这么做,而不只是数字好看。希望这些经验帮到你,少走我踩过的弯路。
本文还有配套的精品资源,点击获取