简介:这份资源面向计算机、网络安全相关专业的本科生与研究生,以及需要完成课程设计或期末大作业的学习者,核心是用NSL-KDD数据集训练一个网络入侵检测模型,并借助KDDCup与NSL-KDD两个数据集完成模型评估,属于可直接复现的满分项目方案。压缩包共27个文件,约29.98MB,包含10个csv数据文件、7个txt说明、4个ipynb实验笔记、3个m脚本、1个py脚本及md、docx文档,覆盖数据处理、建模、评估全流程。已有403人学习下载。资源内含带注释的代码与多份Notebook,分别演示无PCA与含PCA的建模路径,并配有KDDCup数据读取与评估脚本,便于理解特征工程、降维与指标对比。目录结构清晰,部署简单,新手也能看懂,适合作为课程设计、期末大作业或入门网络入侵检测的实践参考。
1. 从 NSL-KDD 到 KDDCup:一个入侵检测大作业为什么值得认真做
很多人做网络安全方向的大作业,第一反应是找个现成模型跑一遍 NSL-KDD,看到准确率 99% 就交差。但真正做过的人都知道,这个数据集上的高准确率是个陷阱——它更多反映的是数据分布本身容易分,而不是模型真的学到了入侵行为的本质。NSL-KDD 作为 KDDCup99 的修正版,去掉了大量冗余记录,平衡了训练集和测试集的难度,但它依然保留了 41 维特征和 5 大类标签(Normal、DoS、Probe、R2L、U2R)。用 NSL-KDD 训练一个网络入侵检测模型,再用 KDDCup 和 NSL-KDD 分别做评估,核心价值在于让你亲眼看到:同一个模型换一个数据分布,性能会掉成什么样。这件事适合已经学过机器学习基础、想真正理解「模型评估与选择」到底在评什么的人。下面我从数据准备、模型训练、双数据集评估、踩坑排查到进阶技巧,把这条链路完整走一遍。
2. 数据准备:NSL-KDD 的 41 维特征怎么读、怎么转、怎么切
2.1 NSL-KDD 与 KDDCup 的文件结构和标签体系
NSL-KDD 的标准分发一般包含这几个文件:KDDTrain+.txt、KDDTest+.txt,以及对应的 20% 子集。KDDCup99 的原始文件通常是 kddcup.data_10_percent 和 kddcup.data.corrected。两者格式一致:每行 41 个特征加 1 个标签,逗号分隔,没有表头。标签分两类写法:一种是具体的攻击名如 neptune、smurf、portsweep,另一种是映射后的 5 大类。做多分类时用具体攻击名,做二分类或五分类时先做映射。
常见做法是先定义一个攻击名到五大类的映射字典。这一步不能省,因为 NSL-KDD 的 KDDTest+ 里有很多训练集没出现过的攻击子类,如果直接按具体攻击名做标签编码,测试集会出现大量未见类别,评估结果会失真。
# 攻击名到五大类的映射,覆盖 NSL-KDD 和 KDDCup 常见攻击 attack_map = { 'normal': 'Normal', # DoS 'back': 'DoS', 'land': 'DoS', 'neptune': 'DoS', 'pod': 'DoS', 'smurf': 'DoS', 'teardrop': 'DoS', 'apache2': 'DoS', 'udpstorm': 'DoS', 'processtable': 'DoS', 'worm': 'DoS', 'mailbomb': 'DoS', # Probe 'ipsweep': 'Probe', 'nmap': 'Probe', 'portsweep': 'Probe', 'satan': 'Probe', 'mscan': 'Probe', 'saint': 'Probe', # R2L 'ftp_write': 'R2L', 'guess_passwd': 'R2L', 'imap': 'R2L', 'multihop': 'R2L', 'phf': 'R2L', 'spy': 'R2L', 'warezclient': 'R2L', 'warezmaster': 'R2L', 'snmpgetattack': 'R2L', 'named': 'R2L', 'xlock': 'R2L', 'xsnoop': 'R2L', 'snmpguess': 'R2L', 'httptunnel': 'R2L', 'sendmail': 'R2L', # U2R 'buffer_overflow': 'U2R', 'loadmodule': 'U2R', 'perl': 'U2R', 'rootkit': 'U2R', 'ps': 'U2R', 'sqlattack': 'U2R', 'xterm': 'U2R' }这段映射的逻辑是:把细粒度攻击名归并到 NSL-KDD 论文定义的五大类。参数上注意,KDDCup 里有些攻击名和 NSL-KDD 不完全一致,比如 KDDCup 的 corrected 文件里有 'snmpgetattack' 但训练集里没有,映射时要统一处理,遇到未在字典中的标签直接归为 'Unknown' 或丢弃,不要硬编码。
2.2 类别特征编码与数值特征归一化
41 维里有 3 个是符号特征:protocol_type(tcp/udp/icmp)、service(http/ftp/smtp 等 70 种)、flag(SF/S0/REJ 等 11 种)。这三个必须做编码。常见做法有两种:一是独热编码,维度会从 41 膨胀到 122 左右;二是标签编码加嵌入,但树模型对标签编码不敏感。我一般用独热编码配合树模型,因为可解释性好,排查问题时能直接看特征重要性。
数值特征里,src_bytes、dst_bytes、duration 这些跨度极大,从 0 到上亿。归一化用 MinMaxScaler 或 StandardScaler 都行,但要注意:scaler 只能在训练集上 fit,然后 transform 测试集。如果先把训练集和测试集合并再 fit,就是典型的数据泄漏,评估结果会虚高。
import pandas as pd from sklearn.preprocessing import MinMaxScaler, OneHotEncoder from sklearn.compose import ColumnTransformer # 读取,无表头,41 特征 + 1 标签 cols = [f'f{i}' for i in range(41)] + ['label'] train = pd.read_csv('KDDTrain+.txt', names=cols) test = pd.read_csv('KDDTest+.txt', names=cols) # 标签映射到五大类 train['category'] = train['label'].map(attack_map).fillna('Unknown') test['category'] = test['label'].map(attack_map).fillna('Unknown') # 符号特征列 cat_cols = ['f1', 'f2', 'f3'] # protocol_type, service, flag num_cols = [c for c in cols if c not in cat_cols + ['label']] # 只在训练集 fit preprocessor = ColumnTransformer([ ('num', MinMaxScaler(), num_cols), ('cat', OneHotEncoder(handle_unknown='ignore'), cat_cols) ]) X_train = preprocessor.fit_transform(train) X_test = preprocessor.transform(test)这里的关键参数是 OneHotEncoder 的 handle_unknown='ignore'。NSL-KDD 测试集里会出现训练集没有的 service 值,如果不设这个参数,transform 时会直接报错。MinMaxScaler 对异常值敏感,如果发现某个数值特征有极端离群点,可以先做对数变换再归一化。
2.3 训练集与测试集的切分策略
NSL-KDD 官方已经给了 KDDTrain+ 和 KDDTest+,直接拿来用就行。但如果你想更严谨,可以从 KDDTrain+ 里再切 20% 做验证集调参,最后用 KDDTest+ 做最终评估。KDDCup 的评估则用 kddcup.data.corrected 作为测试集,或者用 10_percent 训练、corrected 测试。注意 KDDCup 的 corrected 里包含大量 NSL-KDD 训练集没有的攻击类型,这正是检验模型泛化能力的点。
提示:不要用 KDDTest+ 调参后再用 KDDTest+ 报告结果,那样等于把测试集当验证集用。正确顺序是训练集训练、验证集选模型、测试集只评一次。
3. 模型训练:从随机森林到 XGBoost 的选型与参数设置
3.1 为什么树模型在 NSL-KDD 上通常优于深度模型
NSL-KDD 只有 12 万条训练样本、41 维特征,且特征里有大量离散值和类别不平衡。这种规模下,随机森林和 XGBoost 这类集成树模型往往比多层感知机更稳。原因有三:一是树模型对特征尺度不敏感,归一化做不做影响不大;二是树模型能自动处理特征交互,比如 flag=S0 和 count 高的组合往往指向 DoS;三是训练快,调参成本低。深度模型在这个数据量上容易过拟合,尤其是 U2R 和 R2L 样本极少,神经网络很容易全部预测成 Normal。
我一般先用随机森林跑一个 baseline,确认数据管道没问题,再上 XGBoost 或 LightGBM 做提分。如果非要试深度模型,建议用一维卷积或简单的全连接加 Dropout,并且一定要做类别权重平衡。
3.2 随机森林 baseline 的完整训练代码
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix # 训练随机森林,n_estimators 先设 100,max_depth 不限制 rf = RandomForestClassifier( n_estimators=100, max_depth=None, min_samples_split=2, class_weight='balanced', # 缓解类别不平衡 random_state=42, n_jobs=-1 ) rf.fit(X_train, train['category']) # 在 NSL-KDD 测试集上评估 y_pred_nsl = rf.predict(X_test) print(classification_report(test['category'], y_pred_nsl)) print(confusion_matrix(test['category'], y_pred_nsl))class_weight='balanced' 的作用是按类别频率反比给权重,让 U2R 和 R2L 这些少数类在分裂时不被忽略。n_estimators 设 100 是起点,如果发现验证集准确率还在涨,可以加到 200 或 300,但超过 300 后收益递减明显。min_samples_split 默认 2,如果过拟合严重可以调到 5 或 10。
3.3 XGBoost 多分类的参数调优与早停
XGBoost 在多分类上通常比随机森林高 1 到 2 个百分点,尤其是 R2L 的召回率。关键参数有:n_estimators、max_depth、learning_rate、subsample、colsample_bytree。我一般用早停法确定 n_estimators,避免手动试。
import xgboost as xgb from sklearn.preprocessing import LabelEncoder # 标签编码 le = LabelEncoder() y_train_enc = le.fit_transform(train['category']) y_test_enc = le.transform(test['category']) # 从训练集切 20% 做验证集用于早停 from sklearn.model_selection import train_test_split X_tr, X_val, y_tr, y_val = train_test_split( X_train, y_train_enc, test_size=0.2, random_state=42, stratify=y_train_enc ) model = xgb.XGBClassifier( objective='multi:softmax', num_class=5, max_depth=6, learning_rate=0.1, subsample=0.8, colsample_bytree=0.8, eval_metric='mlogloss', early_stopping_rounds=20, random_state=42 ) model.fit( X_tr, y_tr, eval_set=[(X_val, y_val)], verbose=False ) # 用最佳迭代次数在 NSL-KDD 测试集上预测 y_pred_xgb = model.predict(X_test)max_depth=6 是 NSL-KDD 上的经验值,再深容易过拟合少数类。learning_rate=0.1 配合 early_stopping_rounds=20 是比较稳的组合。subsample 和 colsample_bytree 设 0.8 是为了增加随机性、降低方差。注意 early_stopping_rounds 需要验证集,不能直接用测试集。
3.4 类别不平衡的处理:过采样、欠采样与代价敏感
NSL-KDD 训练集里 Normal 约 6.7 万,DoS 约 4.6 万,Probe 约 1.1 万,R2L 约 1000,U2R 只有 52。这种极端不平衡下,模型很容易把 R2L 和 U2R 全预测错。常见做法有三种:一是 SMOTE 过采样少数类,但要注意 SMOTE 只能在训练集上做,且对高维稀疏的独热特征效果一般;二是随机欠采样多数类,会丢信息;三是用 class_weight 或 scale_pos_weight 做代价敏感。我一般先试 class_weight,不行再上 SMOTE,并且用 imblearn 的 Pipeline 确保采样只发生在训练折内。
4. 双数据集评估:KDDCup 和 NSL-KDD 上的指标差异与解读
4.1 评估指标不能只看准确率
在入侵检测里,准确率是最容易骗人的指标。因为 Normal 样本占多数,一个全预测 Normal 的模型在 NSL-KDD 测试集上也能有 40% 多的准确率。真正要看的是每个类别的召回率和 F1,尤其是 U2R 和 R2L。另外,二分类视角下要看检测率(Recall)和误报率(False Positive Rate)。我一般会输出一张按类别分组的 classification_report,再单独算宏平均 F1 和加权平均 F1。
from sklearn.metrics import f1_score, recall_score # 宏平均 F1,各类别等权,能反映少数类表现 macro_f1 = f1_score(test['category'], y_pred_xgb, average='macro') # 加权平均 F1,按样本数加权,反映整体 weighted_f1 = f1_score(test['category'], y_pred_xgb, average='weighted') # 各类别召回率 recall_per_class = recall_score(test['category'], y_pred_xgb, average=None, labels=le.classes_) print(dict(zip(le.classes_, recall_per_class)))宏平均 F1 和加权平均 F1 的差距越大,说明类别不平衡越严重、少数类拖后腿越明显。如果宏平均 F1 只有 0.5 而加权 F1 有 0.95,那模型基本没学到少数类。
4.2 在 KDDCup 上评估时的标签对齐问题
KDDCup 的 corrected 文件里有很多 NSL-KDD 训练集没有的攻击名,比如 'snmpgetattack'、'httptunnel'、'named'。如果直接用 NSL-KDD 训练好的 LabelEncoder 去 transform,会报未见标签错误。正确做法是:先用同一套 attack_map 把 KDDCup 的标签映射到五大类,遇到映射不到的归为 'Unknown',评估时可以选择忽略 Unknown 或单独报告。另外,KDDCup 的 corrected 里 Normal 比例和 NSL-KDD 不同,直接比较准确率意义不大,重点看各类别召回率的变化。
# 读取 KDDCup corrected kddcup = pd.read_csv('kddcup.data.corrected', names=cols) kddcup['category'] = kddcup['label'].map(attack_map).fillna('Unknown') # 用同一个 preprocessor transform,注意 handle_unknown='ignore' 已设 X_kddcup = preprocessor.transform(kddcup) # 用 NSL-KDD 训练的模型预测 y_pred_kddcup = model.predict(X_kddcup) # 只评估五大类,忽略 Unknown mask = kddcup['category'] != 'Unknown' print(classification_report( kddcup.loc[mask, 'category'], le.inverse_transform(y_pred_kddcup)[mask] ))这里有个细节:le.inverse_transform 把预测的整数标签转回字符串,再和真实标签对比。如果 KDDCup 里某类样本数为 0,classification_report 会报警告,可以加 zero_division=0 参数。
4.3 两个数据集上的性能对比表与差异归因
跑完两个数据集后,建议做一张对比表,按类别列出召回率和 F1。典型结果是:NSL-KDD 上 DoS 和 Probe 的 F1 能到 0.95 以上,R2L 和 U2R 只有 0.3 到 0.6;换到 KDDCup 后,DoS 和 Probe 可能还稳,但 R2L 会进一步掉,因为 KDDCup 里 R2L 的攻击模式更多样。这个差异不是模型不行,而是数据分布偏移。NSL-KDD 的测试集虽然去掉了冗余,但攻击类型和训练集重叠度高;KDDCup corrected 则故意引入了新攻击,考的是泛化。
| 类别 | NSL-KDD 召回率 | KDDCup 召回率 | 差异原因 |
|---|---|---|---|
| Normal | 0.95+ | 0.90+ | 分布接近 |
| DoS | 0.95+ | 0.85+ | KDDCup 有新的 DoS 子类 |
| Probe | 0.90+ | 0.80+ | 扫描模式变化 |
| R2L | 0.30-0.60 | 0.10-0.40 | 新攻击多,样本少 |
| U2R | 0.20-0.50 | 0.05-0.30 | 样本极少,泛化难 |
这张表的意义在于:如果你的大作业只报 NSL-KDD 准确率,等于只展示了模型在「见过类似攻击」时的表现。加上 KDDCup 评估,才能说明模型面对未知攻击时的真实能力边界。
5. 避坑与排查:入侵检测大作业里最容易翻车的 5 个点
5.1 现象:测试集准确率 99%,但 U2R 召回率为 0
原因:U2R 在训练集只有 52 条,模型直接学会了全部预测成 Normal 或 DoS,因为这样整体损失最小。解决:加 class_weight='balanced',或者对 U2R 单独做 SMOTE 过采样,再不行就二分类先分 Normal/Attack,再在 Attack 里做多分类。
5.2 现象:KDDCup 评估时报 ValueError: y contains previously unseen labels
原因:KDDCup 的标签里有 NSL-KDD 训练集没出现的攻击名,LabelEncoder 没见过。解决:不要用 LabelEncoder 直接 transform KDDCup 标签,而是用同一套 attack_map 先映射到五大类,映射不到的归 Unknown 并排除评估。
5.3 现象:归一化后模型性能反而下降
原因:把训练集和测试集合并做 fit,或者对已经独热编码的 0/1 特征又做了标准化。解决:scaler 只在训练集 fit;独热后的 0/1 列不要再归一化,ColumnTransformer 里只对数值列做缩放。
5.4 现象:随机森林训练极慢,内存爆掉
原因:n_estimators 设太大,或者 max_depth 不限制导致树无限生长,加上 n_jobs=-1 开满核。解决:先设 n_estimators=100、max_depth=20 跑通,再看验证集曲线决定是否加树。内存不够就把 n_jobs 降到 4。
5.5 现象:两个数据集上评估结果几乎一样,没有差异
原因:很可能 KDDCup 的标签映射错了,或者 preprocessor 对 KDDCup 做了 fit 而不是 transform,导致数据泄漏。解决:检查 preprocessor 是否只在训练集 fit;检查 KDDCup 的 category 分布,确认 R2L 和 U2R 的样本数是否合理。
6. 进阶技巧:用混淆矩阵和特征重要性定位模型的真实短板
跑通基础流程后,真正拉开差距的是对模型错误的分析。我一般做两件事:一是画归一化混淆矩阵,看错分集中在哪;二是看特征重要性,确认模型有没有学到不该学的东西。
混淆矩阵不要只看绝对值,要看按行归一化的版本。比如 R2L 有 1000 条,其中 600 条被预测成 Normal,那召回率就是 0.4,比看总数直观得多。用 sklearn 的 confusion_matrix 加 normalize='true' 就能得到。
import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix cm = confusion_matrix(test['category'], y_pred_xgb, labels=le.classes_, normalize='true') plt.figure(figsize=(8, 6)) sns.heatmap(cm, annot=True, fmt='.2f', xticklabels=le.classes_, yticklabels=le.classes_, cmap='Blues') plt.xlabel('Predicted') plt.ylabel('True') plt.title('NSL-KDD Normalized Confusion Matrix') plt.show()特征重要性方面,随机森林的 feature_importances_ 和 XGBoost 的 get_score 都能看。如果发现 src_bytes 或 dst_bytes 重要性异常高,要警惕:这两个特征在 DoS 和 Normal 上差异大,但在 R2L 上区分度低,模型可能过度依赖它们。这时候可以试着去掉这两个特征重训,看少数类召回率是否提升。
另一个进阶点是做跨数据集的特征对齐验证。NSL-KDD 和 KDDCup 的 41 维特征定义一致,但取值范围可能不同。可以分别统计两个数据集上每个数值特征的均值和方差,如果某个特征差异超过一个数量级,说明分布偏移严重,模型在这个特征上学到的阈值在另一个数据集上会失效。这时候可以考虑做分位数归一化,或者用对抗验证判断哪些样本来自哪个数据集。
我自己的习惯是:每次跑完模型,先看混淆矩阵,再看特征重要性,最后做一次跨数据集的特征分布对比。这三步做完,基本能定位 80% 的性能问题。大作业里如果能把这三步写进报告,比单纯堆模型分数更有说服力。希望帮到你。
本文还有配套的精品资源,点击获取