
简介基于NSL-KDD数据集的网络入侵检测Python完整项目面向计算机相关专业准备毕业设计、课程设计或期末大作业的学生也适合希望系统上手网络入侵检测实战的Python学习者。项目由导师指导并获评98分内含带GUI界面的可运行检测程序、详细运行说明、预处理后的NSL-KDD原始与校正数据集以及PCA降维与无降维两套模型构建与评估脚本覆盖数据读取、特征工程、模型训练和效果对比等关键流程可完整复现实验结论并支持在此基础上扩展优化。压缩包共27个文件主要包含csv数据、ipynb/m/py脚本、txt/md/docx说明文档整体仅29.98MB便于快速下载和本地复现数据与代码分离目录结构清晰。已有312人学习下载适合需要完整项目方案进行对照练习、二次开发或作为高分作业参考的读者。1. 用NSL-KDD做网络入侵检测别被“高分大作业”带偏了如果你在找“基于NSL-KDD数据集的网络入侵检测python源码”八成是接到了机器学习或网络安全的课程大作业。NSL-KDD是KDD Cup 99的改进版去掉了大量冗余记录让训练集和测试集的比例更合理是目前做入侵检测实验最常用的公开数据集。它包含正常流量和四大类攻击流量DoS、Probe、R2L、U2R每条记录有41个特征和1个标签列。不过我先把话说在前面这个任务真正的难点不在训练一个能跑的模型而在于你能否把特征预处理、类别不均衡、评估指标这几件事讲清楚。网上很多“高分大作业”源码只有几十行逻辑回归准确率能到75%以上看着挺像回事但拿到验证集上一测就露馅。我写这篇文章会给你一套完整的代码路径从数据加载到特征筛选、模型对比再到评估指标选择每一段都可直接运行也能帮助你回答答辩时的追问。2. 数据加载与探索先看清NSL-KDD长什么样再谈建模2.1 下载数据集并检查文件结构NSL-KDD官方提供的文件通常在KDDTrain.txt和KDDTest.txt中有的压缩包会带KDDTrain_20Percent.txt训练集的20%子集。你解压后先确认CSV里没有表头字段以逗号分隔最后一列是标签。我们先用pandas把它读进来看看基本信息import pandas as pd # 定义列名NSL-KDD共42列最后一列是标签 columns [ 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_train pd.read_csv(KDDTrain.txt, headerNone, namescolumns) df_test pd.read_csv(KDDTest.txt, headerNone, namescolumns) print(f训练集形状: {df_train.shape}) print(f测试集形状: {df_test.shape}) print(df_train[label].value_counts())运行这段代码后训练集会显示约12.5万条记录其中normal约占一半其余是neptune、satan、ipsweep、portsweep等具体攻击类型。注意这里label列的值不是只有normal和attack两个值而是具体的攻击名称这是NSL-KDD和KDD99的共同特点。参数说明headerNone意思是数据文件本身没有表头namescolumns手动指定列名避免第一行被误当作数据。2.2 把攻击标签映射为二分类完成作业时你通常有两种做法一种是预测具体攻击类型多分类另一种是只区分正常和异常二分类。大多数课程作业要求“能检测入侵”所以二分类就够了但答辩时可能会问你为什么不区分攻击类型你可以说“NSL-KDD中R2L和U2R类别样本极不均衡多分类会导致这两个类别完全学不出来二分类能保证基本检测率”这个说法在技术上是站得住脚的。import numpy as np # 把非normal的值统一标记为attack df_train[label_binary] np.where(df_train[label] normal, 0, 1) df_test[label_binary] np.where(df_test[label] normal, 0, 1) # 查看映射结果 print(df_train[label_binary].value_counts())这段代码用np.where做条件映射返回0和1两个值1代表攻击。这里有个容易踩的坑NSL-KDD训练集和测试集的攻击类型并不是完全一致的比如测试集包含saint、mscan等训练集中没出现过的攻击类型因此实际应用中测试集检测率一般会比训练集低5到10个百分点这是数据集的固有特性不是你的模型出了问题。2.3 特征类型拆分与缺失值检查41个特征里只有3个是类别型protocol_type、service、flag其余38个都是数值型。有些源码会把service直接删掉理由是它的取值太多约70个但这会损失大量信息因为像http和private服务的攻击分布差异很大。我建议保留后续用独热编码处理。# 检查缺失值 print(df_train.isnull().sum().sum()) # NSL-KDD没有缺失值 # 区分数值列和类别列 categorical_cols [protocol_type, service, flag] numeric_cols [c for c in columns if c not in categorical_cols and c ! label] # 查看类别特征的基数 for col in categorical_cols: print(f{col}: {df_train[col].nunique()} 种取值)输出结果中service大约有70种取值flag有11种。NSL-KDD没有缺失值这省去了填充的麻烦。数值列里有几个特征需要特别留意num_outbound_cmds全为0is_host_login基本全为0这类特征在训练时不会提供有效信息但也不会有害多数模型会自动忽略它们。如果你做特征选择可以直接去掉num_outbound_cmds它是KDD99遗留的无效字段。3. 数据预处理与特征工程这一步直接决定你分数的高低3.1 用ColumnTransformer一步完成独热编码与标准化现在要把类别特征转成数值同时把数值特征缩放到同一量纲。最常见的错误是先做标准化再做独热编码这样会把0/1编码的列也缩放到均值附近破坏稀疏性。正确做法是区分对待类别特征用OneHotEncoder数值特征用StandardScaler。sklearn的ColumnTransformer可以把这两步拼在同一个流水线里from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.model_selection import train_test_split X_train df_train.drop([label, label_binary], axis1) y_train df_train[label_binary] X_test df_test.drop([label, label_binary], axis1) y_test df_test[label_binary] # 定义预处理流程 preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), numeric_cols), (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ] ) # 先拟合训练集 X_train_processed preprocessor.fit_transform(X_train) X_test_processed preprocessor.transform(X_test) print(f预处理后训练集形状: {X_train_processed.shape})这段代码里handle_unknownignore很关键测试集里可能出现训练集没见过的service取值如果不设置这个参数程序会直接报错。预处理后的特征维度大约是41个数值特征加上类别展开后的120列左右总共160多个特征。用fit_transform在训练集上拟合编码器和标准化参数然后只用transform处理测试集这是防止数据泄漏的基础操作但很多初写大作业的人会在这里犯迷糊。3.2 划分验证集不要用测试集调参如果你只有“训练集”和“测试集”两份数据建议从训练集里切出20%作为验证集。为什么因为你需要调参比如决策树深度、随机森林的树数量用测试集调参就等于偷看了答案最后报出来的测试集分数是有水分的。答辩时老师通常会问“你的模型在没见过的数据上表现如何”这时候你能说出“我在训练集中留了20%做验证最终测试集只评估一次”会显得专业不少。X_train_fit, X_val, y_train_fit, y_val train_test_split( X_train_processed, y_train, test_size0.2, random_state42, stratifyy_train ) print(f训练子集: {X_train_fit.shape}, 验证集: {X_val.shape})参数说明test_size0.2表示取20%做验证集stratifyy_train表示按标签比例分层抽样这样攻击和正常的比例在训练集与验证集中保持一致不会出现验证集里全是正常流量导致准确率虚高的情况。random_state42固定随机种子保证每次运行结果一致作业提交时也方便复现。这里有个小建议如果你做了分层抽样后续训练和验证的类别分布是平衡的可放心使用。3.3 数值特征分布检查为什么有的特征要截断或取对数NSL-KDD的数值特征里有几个极端情况需要单列说明src_bytes和dst_bytes的取值范围从0到几十亿最小值与最大值相差9个数量级以上直接做标准化并不能解决分布偏斜问题因为均值会被个别巨大值拉高导致大多数样本缩放到负值范围。常见的处理方式是对这类长尾特征做log1p变换后再标准化其中log1p即log(x1)好处是零值取对数后仍为0不会产生负无穷。from scipy.special import log1p # 定义一个新的预处理流程 preprocessor_with_log ColumnTransformer( transformers[ (log_num, passthrough, [src_bytes, dst_bytes]), (num, StandardScaler(), [c for c in numeric_cols if c not in [src_bytes, dst_bytes]]), (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ] )但更简洁的方案是直接在数据框中把src_bytes和dst_bytes替换成它们的对数变换值然后照常走一遍之前的预处理流程。在做大作业时你可以同时在文档里贴出变换前后的分布图说明“原始特征右偏严重log变换后近似正态模型收敛更快”。这一步能明显拉开你与套模板同学的差距因为绝大多数人不会在特征工程层面思考到这一层。4. 模型训练与评估逻辑回归打底随机森林提分4.1 先跑逻辑回归建立基准线逻辑回归虽然简单但在NSL-KDD上并不弱原因是许多特征对攻击类型有很强的区分度比如same_srv_rate高且serror_rate高的流量大概率是DoS攻击这类线性边界逻辑回归就能捕获。逻辑回归还有一个好处训练速度快十几秒就能跑完适合作为基准模型用来验证数据预处理有没有问题——如果逻辑回归的准确率低于70%说明前面的特征处理有BUG。from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score # 训练逻辑回归调高max_iter和惩罚系数 lr LogisticRegression(max_iter1000, C1.0, solverlbfgs, random_state42) lr.fit(X_train_fit, y_train_fit) y_val_pred lr.predict(X_val) print(f准确率: {accuracy_score(y_val, y_val_pred):.4f}) print(f精确率: {precision_score(y_val, y_val_pred):.4f}) print(f召回率: {recall_score(y_val, y_val_pred):.4f}) print(fF1分数: {f1_score(y_val, y_val_pred):.4f})参数说明max_iter1000是因为特征维度有160多列默认的100次迭代可能不收敛训练时如果出现“ConvergenceWarning”就加大这个值C1.0是正则化强度的倒数默认值在多数情况下够用solverlbfgs适合中小型数据集内存占用小且收敛快。为什么同时看精确率和召回率因为入侵检测中你既不想把正常流量误报成攻击精确率低又不想漏掉真正的攻击召回率低只看准确率在类别不平衡时会失真。NSL-KDD中攻击样本约占46%类别还算平衡但测试集中某些攻击类别的召回率会明显低于训练集。4.2 用随机森林看上限在哪里逻辑回归的缺陷在于无法自动捕捉特征之间的非线性交互。比如dst_host_srv_count和dst_host_same_srv_rate的关系对识别特定Probe攻击很重要这类交互逻辑回归需要手工构造特征才能学到而随机森林天然能做特征交互。在NSL-KDD上随机森林的F1通常比逻辑回归高3到5个百分点训练时间也不算长是大作业的主力模型。from sklearn.ensemble import RandomForestClassifier # 训练随机森林 rf RandomForestClassifier( n_estimators200, max_depth20, min_samples_split5, min_samples_leaf2, n_jobs-1, random_state42 ) rf.fit(X_train_fit, y_train_fit) y_val_pred_rf rf.predict(X_val) print(f准确率: {accuracy_score(y_val, y_val_pred_rf):.4f}) print(fF1分数: {f1_score(y_val, y_val_pred_rf):.4f})参数说明n_estimators200用200棵树再多准确率提升有限但训练时间线性增长大作业场景200是个省时间的平衡点max_depth20限制每棵树的深度防止过拟合对160维特征来说20层足够min_samples_leaf2让叶子节点至少2个样本进一步抗过拟合。n_jobs-1让sklearn使用所有CPU核心并行训练如果你的笔记本有8核训练时间能缩短到原来的1/8。训练完成后可以用一行代码查看特征重要性找出逻辑回归无法体现的细节# 获取特征名称ColumnTransformer处理后 feature_names preprocessor.get_feature_names_out() importance_df pd.DataFrame({ feature: feature_names, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance_df.head(10))你会看到排在前面的特征通常是count、src_bytes、dst_host_srv_count、same_srv_rate等这说明网络流量在短时间内重复连接的特征对识别攻击非常关键。如果答辩时老师问“为什么选这些特征”你可以说“随机森林的特征重要性排序显示连接频率与速率类特征贡献最大与入侵检测的领域知识一致”这个回答很有说服力。4.3 输出完整分类报告并绘制混淆矩阵评估时不要只看一个准确率。NSL-KDD的测试集包含训练集中未出现的攻击类型直接报测试集准确率是不完整的。应该输出sklearn的classification_report和混淆矩阵这样能看出模型在正常流量和攻击流量上的具体表现。from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt # 在测试集上评估 y_test_pred rf.predict(X_test_processed) print(classification_report(y_test, y_test_pred, target_names[normal, attack])) # 绘制混淆矩阵 cm confusion_matrix(y_test, y_test_pred) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labels[normal, attack]) disp.plot(cmapBlues) plt.savefig(confusion_matrix.png, dpi150, bbox_inchestight)混淆矩阵在测试集上会出现一个典型现象假阴性把攻击漏判为正常通常比假阳性多。原因是NSL-KDD测试集中包含了训练集从未见过的攻击变种模型对它们的特征模式缺乏认知。此时你有两个选择一是提醒自己这篇大作业做到“能检测已知攻击外加一部分变种”即可二是用下一章的处理方法尝试改善。5. 处理类别不均衡与阈值调整让R2L和U2R不再被无视5.1 查看具体攻击类型在训练集的数量分布前面用二分类隐藏了一个重要信息NSL-KDD中的R2L和U2R攻击样本极少。warezclientR2L大约有890条warezmasterR2L约20条而psU2R只有十几条。随机森林在训练时会倾向于牺牲这些少数类把样本预测为多数类来降低整体损失在分类报告中体现为R2L和U2R的召回率极低。attack_dist df_train[df_train[label] ! normal][label].value_counts() print(attack_dist.head(15))如果你要详细分析可以看训练集中具体攻击类型的数量分布其中neptune约4万条占据压倒性多数satan约3600条ipsweep约2000条而大部分U2R不足10条。这就是为什么多分类困难的核心原因。解决办法有三种第一用class_weightbalanced让模型自动给少数类更高权重第二用SMOTE过采样少数类第三降低判断为正类的阈值。其中第一种实现成本最低对大作业足够但也不能完全解决问题。5.2 设置class_weight并对比效果class_weight是随机森林和逻辑回归都支持的参数它会根据类别频率自动调整惩罚权重。频率越低的类别分错时的损失越大模型就会更“注意”少数类。代价是整体准确率可能会小幅下降因为模型会牺牲一部分多数类的判断正确率来换取少数类的召回率。# 带类别权重的随机森林 rf_balanced RandomForestClassifier( n_estimators200, max_depth20, min_samples_split5, min_samples_leaf2, class_weightbalanced, n_jobs-1, random_state42 ) rf_balanced.fit(X_train_fit, y_train_fit) y_test_pred_bal rf_balanced.predict(X_test_processed) print(classification_report(y_test, y_test_pred_bal, target_names[normal, attack]))运行后你通常会发现normal的精确率略有下降attack的召回率有所提升特别是在原本漏报较多的攻击类型上改善明显。如果你的重心是“模型能抓攻击”这往往是更好的结果而如果你更看重“总准确率”的数值不平衡权重可能会让准确率下降1%这时你要在文档里解释你的权衡依据入侵检测场景里漏报的代价高于误报所以倾向提高召回率。5.3 用阈值搜索找到适合业务场景的判定边界分类模型的默认阈值是0.5即预测概率大于等于0.5就判为攻击。但当少数类比例极低时0.5不是最优选择。你可以遍历0.3到0.7之间的阈值画出F1随阈值变化的曲线选择一个同时照顾精确率和召回率的点。这在作业中属于加分项写成代码也很简洁from sklearn.metrics import precision_recall_curve import numpy as np # 取预测概率 y_val_proba rf.predict_proba(X_val)[:, 1] # 计算不同阈值下的精确率和召回率 precisions, recalls, thresholds precision_recall_curve(y_val, y_val_proba) # 找到让F1最大的阈值 f1_scores 2 * (precisions * recalls) / (precisions recalls 1e-9) best_idx np.argmax(f1_scores) best_threshold thresholds[best_idx] print(f最佳阈值: {best_threshold:.3f}) print(f最高F1: {f1_scores[best_idx]:.4f})这段代码先取验证集上攻击类别的预测概率再用precision_recall_curve输出每个可能阈值下的精确率和召回率最后手动计算F1并取最大值。得到最佳阈值后把这个阈值代入测试集预测y_test_pred_custom (rf.predict_proba(X_test_processed)[:, 1] best_threshold).astype(int)。5.4 模型保存与运行说明的写法大作业提交时不只是交一个.ipynb文件通常还需要模型文件和说明文档。模型可以用joblib保存这样下次直接加载预测不用重新训练import joblib # 保存模型和预处理流水线 joblib.dump(rf_balanced, rf_model.joblib) joblib.dump(preprocessor, preprocessor.joblib)运行说明里建议包含这几项内容环境依赖列表pandas,numpy,scikit-learn,matplotlib全部用pip install可装、数据集文件的放置路径、按顺序执行的步骤先预处理再训练再评估、不同模型的结果对比表格。很多大作业的失分点不在模型效果而在“复现不出来”你把运行步骤写清楚尤其在代码开头加一个版本检查命令等于帮老师降低了验收成本。import sys print(fPython版本: {sys.version}) import sklearn print(fsklearn版本: {sklearn.__version__})5.5 一个能快速出图的时序型特征可视化技巧NSL-KDD不包含时间戳但count和srv_count本身就是时间窗口内的统计量可以画成堆叠直方图观察攻击发生时这些统计量的分布差异。这里我分享一下我常用的做法取正常样本和neptune样本各500条画同一特征的双直方图你能直观看到两类样本在count特征上几乎不重叠这是模型能学好的根本原因。import matplotlib.pyplot as plt # 从训练集中抽样 normal_sample df_train[df_train[label_binary] 0][count].sample(500, random_state42) attack_sample df_train[df_train[label_binary] 1][count].sample(500, random_state42) plt.hist(normal_sample, bins30, alpha0.7, labelnormal, colorgreen) plt.hist(attack_sample, bins30, alpha0.7, labelattack, colorred) plt.xlabel(count) plt.ylabel(frequency) plt.legend() plt.savefig(count_distribution.png, dpi150, bbox_inchestight)这里sample(500, random_state42)是随机抽500条alpha0.7设置透明度避免直方图重叠时看不清楚bins30定义直方图的柱子数量。输出图后可以复制到你的文档里配合一句话“正常流量和攻击流量在连接频率上存在显著差异这解释了模型能够以高召回率识别DoS攻击”。画这种图的技巧能让你的报告看起来更像一个完整的分析流程。本文还有配套的精品资源点击获取