简介:这份资源面向计算机、网络安全相关专业的本科生与研究生,用于课程设计、期末大作业或入门级科研实践,核心是借助NSL-KDD数据集训练网络入侵检测模型,并分别在KDDCup与NSL-KDD两个数据集上完成模型评估,帮助读者理解入侵检测的完整流程与跨数据集泛化验证思路。压缩包共27个文件,约29.98MB,以csv数据文件、txt说明、ipynb实验笔记、m脚本及py代码为主,另附md与docx文档,覆盖数据处理、建模、评估等环节,代码注释较为完整,新手也能对照理解。目前已有403人学习下载。资源提供从数据读取、特征处理到PCA降维与无PCA对比建模的多套实验脚本,并给出KDDCup与NSL-KDD的评估流程,便于直接部署运行、复现结果,也可作为报告撰写与答辩展示的参考素材,具备较高的实际应用与学习价值。
1. 从 NSL-KDD 到 KDDCup:一个入侵检测大作业的完整落地路径
很多同学做网络入侵检测大作业时,第一反应是找个现成模型跑一遍 NSL-KDD 就交差,结果答辩被问「KDDCup 和 NSL-KDD 的评估结果差异怎么解释」直接卡壳。这个标题真正要解决的不是「训一个模型」,而是用 NSL-KDD 训练、用两个数据集交叉评估,并说清楚模型评估与选择背后的逻辑。NSL-KDD 是 KDDCup99 的去冗余修订版,训练集 125973 条、测试集 22544 条,类别分布更均衡,但两个数据集的特征空间和攻击类型覆盖并不完全一致。适合谁?正在做网络安全、机器学习课程大作业,需要一套能跑通、能解释、能写进报告的全流程方案的人。下面按「数据理解 → 模型训练 → 双数据集评估 → 避坑 → 进阶技巧」推进。
2. NSL-KDD 与 KDDCup 的数据结构差异:先搞懂再动手
2.1 两个数据集到底差在哪
NSL-KDD 和 KDDCup99 都是 41 维特征加 1 个标签,特征分四类:TCP 连接基本特征(如 duration、protocol_type)、内容特征(如 num_failed_logins)、基于时间的流量特征(如 count、srv_count)、基于主机的流量特征(如 dst_host_count)。标签分五大类:Normal、DoS、Probe、R2L、U2R。
关键差异在于:KDDCup99 训练集有约 490 万条记录,冗余度极高,同一类攻击大量重复,导致模型在测试集上虚高;NSL-KDD 去掉了重复记录,训练集降到 12 万条左右,测试集里的攻击类型还刻意加入了训练集未出现的变种。这意味着在 NSL-KDD 上训练、在 KDDCup 上评估时,不能假设标签体系完全对齐——KDDCup 原始标签有 23 种细分类别,需要先映射到五大类才能和 NSL-KDD 的评估口径一致。
常见做法是:读入 KDDCup 的kddcup.data_10_percent或完整版,把back、land、neptune、pod、smurf、teardrop等归为 DoS;ipsweep、nmap、portsweep、satan归为 Probe;ftp_write、guess_passwd、imap、multihop、phf、spy、warezclient、warezmaster归为 R2L;buffer_overflow、loadmodule、perl、rootkit归为 U2R;normal保持 Normal。这个映射表必须写死在代码里,否则评估结果没法对齐。
2.2 数据加载与标签映射的最小代码
import pandas as pd import numpy as np # NSL-KDD 列名(41 特征 + label + difficulty) nsl_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','difficulty' ] # 读取 NSL-KDD train_df = pd.read_csv('KDDTrain+.txt', names=nsl_columns) test_df = pd.read_csv('KDDTest+.txt', names=nsl_columns) # KDDCup 标签映射到五大类 kddcup_map = { 'normal': 'Normal', 'back': 'DoS', 'land': 'DoS', 'neptune': 'DoS', 'pod': 'DoS', 'smurf': 'DoS', 'teardrop': 'DoS', 'apache2': 'DoS', 'udpstorm': 'DoS', 'processtable': 'DoS', 'worm': 'DoS', 'ipsweep': 'Probe', 'nmap': 'Probe', 'portsweep': 'Probe', 'satan': 'Probe', 'mscan': 'Probe', 'saint': 'Probe', 'ftp_write': 'R2L', 'guess_passwd': 'R2L', 'imap': 'R2L', 'multihop': 'R2L', 'phf': 'R2L', 'spy': 'R2L', 'warezclient': 'R2L', 'warezmaster': 'R2L', 'sendmail': 'R2L', 'named': 'R2L', 'snmpgetattack': 'R2L', 'snmpguess': 'R2L', 'xlock': 'R2L', 'xsnoop': 'R2L', 'buffer_overflow': 'U2R', 'loadmodule': 'U2R', 'perl': 'U2R', 'rootkit': 'U2R', 'httptunnel': 'U2R', 'ps': 'U2R', 'sqlattack': 'U2R', 'xterm': 'U2R' } def map_label(label): label = label.strip().rstrip('.') return kddcup_map.get(label, 'Unknown') # 对 NSL-KDD 也做同样映射(NSL-KDD 标签已是五大类,但部分带子类名) def map_nsl_label(label): label = label.strip() if label == 'normal': return 'Normal' # NSL-KDD 训练集标签直接是五大类,测试集可能带子类 for k, v in kddcup_map.items(): if label == k: return v return label # 已是 DoS/Probe/R2L/U2R这段代码的核心是统一标签口径。nsl_columns必须和文件实际列数对齐,NSL-KDD 的KDDTrain+.txt是 43 列(41 特征 + label + difficulty),少写一列后面全错位。kddcup_map覆盖了 KDDCup99 全部 23 种攻击加 normal,映射时先strip().rstrip('.')去掉末尾句点,因为 KDDCup 原始文件标签带点号。参数上,difficulty列在建模时要 drop 掉,它只是难度分级,不是特征。
3. 用 NSL-KDD 训练入侵检测模型:从特征工程到模型选型
3.1 特征编码与归一化的具体做法
41 维里有 3 个分类特征:protocol_type(tcp/udp/icmp)、service(70 种左右)、flag(11 种)。常见做法是 One-Hot 编码,但service维度高,直接 One-Hot 会让特征维度膨胀到 120 以上。我一般用pd.get_dummies对三个分类列一起处理,然后对齐训练集和测试集的列——测试集里可能出现训练集没有的 service 类别,这时要保证列对齐,缺失的补 0。
数值特征量纲差异大,src_bytes可能到上亿,duration可能只有 0 或 1。用StandardScaler做 z-score 归一化,注意 scaler 只能在训练集上 fit,然后 transform 测试集和 KDDCup 数据,否则数据泄露。
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 合并训练和测试做统一编码,再切分(仅用于编码对齐,不用于训练) full_df = pd.concat([train_df, test_df], axis=0, ignore_index=True) full_df['label'] = full_df['label'].apply(map_nsl_label) # One-Hot 编码分类特征 cat_cols = ['protocol_type', 'service', 'flag'] full_encoded = pd.get_dummies(full_df, columns=cat_cols, prefix=cat_cols) # 切回训练和测试 train_encoded = full_encoded.iloc[:len(train_df)].copy() test_encoded = full_encoded.iloc[len(train_df):].copy() # 特征列 = 去掉 label 和 difficulty feature_cols = [c for c in train_encoded.columns if c not in ['label', 'difficulty']] # 归一化 scaler = StandardScaler() X_train = scaler.fit_transform(train_encoded[feature_cols]) X_test = scaler.transform(test_encoded[feature_cols]) y_train = train_encoded['label'].values y_test = test_encoded['label'].values # 标签编码 from sklearn.preprocessing import LabelEncoder le = LabelEncoder() y_train_enc = le.fit_transform(y_train) y_test_enc = le.transform(y_test)这里有个关键点:pd.get_dummies在合并数据上做,保证训练集和测试集列完全一致。如果分开做,测试集多一个 service 类别就会导致列数不同,模型直接报错。StandardScaler的 fit 只在训练集上,这是铁律。LabelEncoder的 fit 也只在训练集标签上,测试集如果出现训练集没有的标签会抛异常——NSL-KDD 测试集确实有训练集未出现的攻击子类,但映射到五大类后通常不会超出,如果超出需要手动处理。
3.2 模型选型:随机森林、XGBoost 还是深度学习
大作业场景下,我推荐随机森林作为 baseline,XGBoost 作为提升。原因:NSL-KDD 是表格数据,树模型天然适合,训练快、可解释性强(能输出特征重要性),答辩时好讲。深度学习(MLP、CNN、RNN)在 NSL-KDD 上未必比 XGBoost 好,而且调参成本高,大作业时间有限。
随机森林的关键参数:n_estimators=100起步,max_depth不设或设 20 左右防止过拟合,class_weight='balanced'处理类别不平衡(U2R 和 R2L 样本极少)。XGBoost 用multi:softmax做五分类,eval_metric='mlogloss',early_stopping_rounds防止过拟合。
from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import classification_report, confusion_matrix, accuracy_score # 随机森林 rf = RandomForestClassifier( n_estimators=100, max_depth=20, class_weight='balanced', random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train_enc) rf_pred = rf.predict(X_test) print("Random Forest on NSL-KDD Test:") print(classification_report(y_test_enc, rf_pred, target_names=le.classes_)) # XGBoost xgb = XGBClassifier( n_estimators=200, max_depth=6, learning_rate=0.1, objective='multi:softmax', num_class=5, eval_metric='mlogloss', early_stopping_rounds=20, random_state=42 ) xgb.fit(X_train, y_train_enc, eval_set=[(X_test, y_test_enc)], verbose=False) xgb_pred = xgb.predict(X_test) print("XGBoost on NSL-KDD Test:") print(classification_report(y_test_enc, xgb_pred, target_names=le.classes_))class_weight='balanced'对随机森林很重要,因为 Normal 占约 53%,U2R 只有几十条,不加权重模型会完全忽略 U2R。XGBoost 的early_stopping_rounds需要eval_set,这里用测试集做早停其实有轻微泄露,严格做法是从训练集再切一个验证集。大作业里如果追求严谨,可以train_test_split切 10% 做验证。
4. 用 KDDCup 和 NSL-KDD 做交叉评估:指标、口径与结果解读
4.1 评估指标不能只看准确率
入侵检测是不平衡分类,准确率会被 Normal 主导。必须看每类的 precision、recall、F1,尤其是 U2R 和 R2L 的 recall。另外,安全场景下**误报率(False Positive Rate)和漏报率(False Negative Rate)**比准确率更重要。常见做法是输出混淆矩阵,再算 macro-F1 和 weighted-F1。
在 NSL-KDD 测试集上,随机森林通常能到 75%~80% 准确率,XGBoost 略高。但注意:NSL-KDD 测试集里有很多训练集未出现的攻击变种,所以 recall 不会太高,这是数据集设计使然,不是模型不行。在 KDDCup 上评估时,因为 KDDCup 冗余度高、攻击类型和训练集重叠大,准确率往往能到 99% 以上——这个差异必须在报告里解释清楚,否则会被质疑「为什么两个数据集差这么多」。
4.2 在 KDDCup 上评估的完整流程
# 读取 KDDCup 数据(以 10% 版本为例) kddcup_columns = nsl_columns[:-1] # 去掉 difficulty,KDDCup 没有这一列 kddcup_df = pd.read_csv('kddcup.data_10_percent', names=kddcup_columns) # 标签映射 kddcup_df['label'] = kddcup_df['label'].apply(map_label) # 用训练好的编码器和 scaler 做同样的预处理 kddcup_encoded = pd.get_dummies(kddcup_df, columns=cat_cols, prefix=cat_cols) # 对齐列:缺失的补 0,多余的丢掉 for col in feature_cols: if col not in kddcup_encoded.columns: kddcup_encoded[col] = 0 kddcup_encoded = kddcup_encoded[feature_cols] X_kddcup = scaler.transform(kddcup_encoded) y_kddcup = le.transform(kddcup_df['label'].values) # 用 NSL-KDD 训练的模型预测 KDDCup kddcup_pred = rf.predict(X_kddcup) print("Random Forest on KDDCup:") print(classification_report(y_kddcup, kddcup_pred, target_names=le.classes_)) # 交叉评估汇总表 results = pd.DataFrame({ 'Dataset': ['NSL-KDD Test', 'KDDCup 10%'], 'Accuracy': [accuracy_score(y_test_enc, rf_pred), accuracy_score(y_kddcup, kddcup_pred)], 'Macro-F1': [ classification_report(y_test_enc, rf_pred, output_dict=True)['macro avg']['f1-score'], classification_report(y_kddcup, kddcup_pred, output_dict=True)['macro avg']['f1-score'] ] }) print(results)关键在列对齐:feature_cols是从 NSL-KDD 训练集来的,KDDCup 经过 One-Hot 后列名可能不完全一致,必须用reindex或手动补 0 对齐。scaler和le都复用 NSL-KDD 训练时的对象,不能重新 fit。le.transform如果遇到未知标签会报错,KDDCup 映射后如果出现 Unknown,需要先过滤或单独处理。
评估结果解读:NSL-KDD 上 macro-F1 通常 0.5~0.6,因为 U2R/R2L 召回低;KDDCup 上 macro-F1 能到 0.9 以上。这个差距说明模型对已知攻击模式识别好,对未知变种泛化差,这正是入侵检测的核心挑战,也是报告里可以展开讨论的点。
5. 避坑与排查:双数据集评估最容易翻车的 5 个地方
5.1 现象:KDDCup 评估准确率 99%,NSL-KDD 只有 75%,被质疑模型造假
原因:KDDCup 冗余度高,测试集和训练集攻击模式高度重叠,模型只是记住了模式;NSL-KDD 去冗余且测试集含未知攻击,准确率自然低。解决:在报告里明确说明两个数据集的构造差异,把 KDDCup 的高分解释为「对已知攻击的检测能力」,NSL-KDD 的低分解释为「对未知变种的泛化能力」,并给出两类 recall 对比。
5.2 现象:One-Hot 后训练集和测试集列数不一致,模型 predict 报错
原因:分开做get_dummies,测试集多出训练集没有的 service 类别。解决:合并后统一编码再切分,或用reindex(columns=feature_cols, fill_value=0)对齐。我一般直接在合并数据上做编码,省事且不会错。
5.3 现象:U2R 和 R2L 的 recall 为 0,模型完全不预测这两类
原因:类别极度不平衡,U2R 在 NSL-KDD 训练集只有 52 条,R2L 约 995 条,模型倾向于全部预测 Normal 和 DoS。解决:class_weight='balanced',或用 SMOTE 过采样少数类,或调整决策阈值。注意 SMOTE 只能在训练集上做,不能对测试集过采样。
5.4 现象:scaler 在全部数据上 fit,评估结果虚高
原因:数据泄露,测试集的均值和方差信息进入了训练过程。解决:scaler.fit(X_train)只在训练集上,然后transform测试集和 KDDCup。同理,LabelEncoder和get_dummies的列对齐也要基于训练集。
5.5 现象:KDDCup 标签映射遗漏,出现 Unknown 类别导致评估报错
原因:KDDCup99 有 23 种攻击,映射表没写全。解决:先value_counts()看所有唯一标签,对照映射表补全。常见遗漏:apache2、udpstorm、processtable、worm、mscan、saint、snmpgetattack、snmpguess、xlock、xsnoop、httptunnel、ps、sqlattack、xterm。这些在 10% 版本里可能不出现,但完整版里有。
6. 进阶技巧:用特征重要性和混淆矩阵把评估做深
6.1 特征重要性分析:哪些特征真正在起作用
随机森林和 XGBoost 都能输出feature_importances_。在 NSL-KDD 上,通常src_bytes、dst_bytes、count、srv_count、same_srv_rate、dst_host_srv_count排前面。但注意:One-Hot 后的特征重要性要映射回原始特征名,否则报告里写「feature_23 最重要」没人看得懂。
import matplotlib.pyplot as plt # 随机森林特征重要性 importances = rf.feature_importances_ indices = np.argsort(importances)[::-1][:15] plt.figure(figsize=(10, 6)) plt.title("Top 15 Feature Importances (Random Forest)") plt.bar(range(15), importances[indices]) plt.xticks(range(15), [feature_cols[i] for i in indices], rotation=45, ha='right') plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)这张图放进报告,能直接说明模型依赖哪些流量特征,答辩时被问「为什么选这些特征」也有据可依。
6.2 混淆矩阵对比:两个数据集的错误分布差异
import seaborn as sns fig, axes = plt.subplots(1, 2, figsize=(14, 5)) cm_nsl = confusion_matrix(y_test_enc, rf_pred) sns.heatmap(cm_nsl, annot=True, fmt='d', cmap='Blues', xticklabels=le.classes_, yticklabels=le.classes_, ax=axes[0]) axes[0].set_title('NSL-KDD Test Confusion Matrix') axes[0].set_xlabel('Predicted') axes[0].set_ylabel('True') cm_kdd = confusion_matrix(y_kddcup, kddcup_pred) sns.heatmap(cm_kdd, annot=True, fmt='d', cmap='Greens', xticklabels=le.classes_, yticklabels=le.classes_, ax=axes[1]) axes[1].set_title('KDDCup 10% Confusion Matrix') axes[1].set_xlabel('Predicted') axes[1].set_ylabel('True') plt.tight_layout() plt.savefig('confusion_matrices.png', dpi=150)对比两个混淆矩阵,通常能看到:KDDCup 上 Normal 和 DoS 几乎全对,R2L 和 U2R 也有不错召回;NSL-KDD 上 R2L 大量被误判为 Normal,U2R 几乎全漏。这个对比直接支撑「模型对未知攻击泛化不足」的结论。
6.3 一个我踩过的坑:别在测试集上调参
早期我做这个作业时,为了让 NSL-KDD 测试集准确率好看,反复调整 XGBoost 的max_depth和learning_rate,结果测试集准确率上去了,但换到 KDDCup 上反而下降。后来才意识到这是在测试集上过拟合。正确做法是从训练集切 10%~15% 做验证集,用验证集调参,测试集只在最后评估一次。这个习惯我后来一直保持:验证集调参,测试集只读一次。希望帮到你。
本文还有配套的精品资源,点击获取