简介:这是一份面向高校学生与Python初学者的网络入侵检测系统完整项目源码,基于CNN卷积神经网络实现,适合用作机器学习、网络安全相关课程的期末大作业或课程设计。项目以NSL-KDD数据集为训练基础,涵盖数据预处理、模型训练、预测推理与可视化评估等环节,配有详细代码注释,新手也能快速理解整体流程。资源包共33个文件,包含4个Python源码文件、12个CSV数据文件、7个XML配置、1个pth模型权重文件,以及png、jpg效果图与md说明文档,压缩包约21.58MB,部署简单,下载后即可运行使用。目前已有856人学习下载。项目结构清晰,附带准确率、精确率等评估图表与训练好的模型文件,读者可借此掌握从数据清洗、特征归一化到CNN建模、性能评估的完整链路,也可在此基础上替换数据集或调整网络结构进行二次开发,具备较高的参考与复用价值。
1. 从一次误报说起:这套 Python 入侵检测源码到底能干什么
很多做安全运维的朋友都遇到过这种场景:IDS 告警日志刷了几千条,逐条排查下来发现九成是误报,真正有威胁的那几条反而被淹没在噪音里。传统基于规则匹配的入侵检测系统,面对变种攻击和加密流量时越来越力不从心,这也是为什么近几年机器学习开始大量渗透到安全检测领域。这份基于机器学习实现的网络入侵检测系统 Python 源码,核心思路就是用 KDD Cup 99 或 NSL-KDD 这类标准数据集训练分类模型,把网络流量特征映射成「正常」或「攻击」两类标签,替代人工写规则的方式来做检测。整套代码用 Python 写成,结构清晰,包含数据预处理、特征工程、模型训练、检测评估几个完整环节,适合做课程设计、期末大作业,也适合安全方向的同学拿来理解机器学习在真实场景里怎么落地。如果你正在找一份能跑通、能改、能写进报告的入侵检测项目,这份源码值得花时间拆一遍。
2. 数据管道与特征工程:从原始流量到模型可吃的矩阵
2.1 为什么选 NSL-KDD 而不是原始 pcap
这套源码默认使用的数据集是 NSL-KDD,它是 KDD Cup 99 的改进版,去掉了原始数据里大量重复记录,训练集和测试集的攻击类型分布也更合理。原始 pcap 包虽然更贴近真实网络,但需要先做流重组、特征提取,再对齐到模型输入维度,中间每一步都可能引入偏差。对于课程设计或期末项目来说,NSL-KDD 已经做好了 41 维特征标注,直接读入就能用,省去了大量数据清洗的体力活。常见做法是先用 pandas 加载训练集和测试集,检查列名和标签分布,确认没有缺失值后再进入下一步。
import pandas as pd # NSL-KDD 列名,共 41 维特征 + 1 列标签 + 1 列难度分数 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_df = pd.read_csv('KDDTrain+.txt', names=col_names) test_df = pd.read_csv('KDDTest+.txt', names=col_names) # 标签二值化:normal 为 0,其余攻击类型统一为 1 train_df['label'] = train_df['label'].apply(lambda x: 0 if x == 'normal' else 1) test_df['label'] = test_df['label'].apply(lambda x: 0 if x == 'normal' else 1) print(train_df['label'].value_counts())这段代码做了三件事:定义列名、加载数据、把多分类标签转成二分类。参数上需要注意的是names必须和数据集实际列数对齐,NSL-KDD 的 txt 文件没有表头,少写一个列名就会导致整列错位。标签二值化那一步,如果你想做多分类(比如区分 DoS、Probe、R2L、U2R),把 lambda 改成映射字典即可,但后续模型评估指标也要跟着调整。
2.2 类别特征编码与数值归一化
41 维特征里有三个是字符串类型:protocol_type、service、flag。模型不认字符串,必须做编码。这套源码用的是 One-Hot 编码,把三个类别特征展开成数值向量。数值特征之间的量纲差异也很大,比如src_bytes可能上万,而serror_rate只在 0 到 1 之间,不做归一化的话,距离类算法(如 KNN、SVM)会被大量纲特征主导。
from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 类别特征列 cat_features = ['protocol_type', 'service', 'flag'] # 数值特征列:排除标签和难度分数 num_features = [c for c in train_df.columns if c not in cat_features + ['label', 'difficulty']] # 构建预处理管道 preprocessor = ColumnTransformer( transformers=[ ('cat', OneHotEncoder(handle_unknown='ignore'), cat_features), ('num', StandardScaler(), num_features) ] ) # 只在训练集上 fit,测试集 transform,避免数据泄露 X_train = preprocessor.fit_transform(train_df.drop(columns=['label', 'difficulty'])) X_test = preprocessor.transform(test_df.drop(columns=['label', 'difficulty'])) y_train = train_df['label'].values y_test = test_df['label'].values print('训练集维度:', X_train.shape) print('测试集维度:', X_test.shape)这里有个容易翻车的地方:OneHotEncoder的handle_unknown='ignore'参数必须加上。测试集里可能出现训练集没见过的service类型,不加这个参数会直接报错。另外,fit_transform只能在训练集上调用,测试集只能用transform,否则测试集的统计信息会泄露到训练过程中,评估结果虚高。我一般会把这个预处理管道和后面的分类器串成一个完整的 Pipeline,这样保存模型时预处理参数也一起存了,部署时不用再手动对齐。
3. 模型训练与调参:选哪个分类器、参数怎么定
3.1 随机森林作为基线模型的理由
源码里默认用的是随机森林(Random Forest),这个选择很务实。入侵检测数据里类别不平衡是常态,正常流量远多于攻击流量,随机森林对不平衡数据有一定容忍度,而且不需要太多特征缩放(虽然前面做了标准化,但树模型其实不敏感)。相比 SVM,随机森林训练速度快,调参维度少,解释性也更好——你可以直接看特征重要性,知道哪些流量特征对判断攻击贡献最大。常见做法是先跑一个基线模型看整体准确率和召回率,再决定要不要换更复杂的模型。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix # 随机森林基线:100 棵树,默认基尼系数 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, y_train) y_pred = rf.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, digits=4))class_weight='balanced'这个参数很关键。NSL-KDD 训练集里正常流量大约占 53%,攻击流量占 47%,看起来还算均衡,但测试集里攻击类型分布和训练集不一样,有些攻击类型在训练集里样本很少。加上这个参数后,模型会对少数类样本给更高权重,召回率通常能提升几个百分点。n_estimators设 100 是经验值,再往上加收益递减,训练时间却线性增长。n_jobs=-1让所有 CPU 核心参与训练,数据量大的时候能省不少等待时间。
3.2 特征重要性分析与模型可解释性
安全场景里,光有一个「准确率 99%」的结论是不够的,运维人员需要知道模型凭什么判断某条流量是攻击。随机森林自带feature_importances_属性,可以直接输出每个特征的重要性排序。这套源码里有一段可视化代码,把 Top 15 重要特征画成条形图,方便写进项目报告。
import numpy as np import matplotlib.pyplot as plt # 获取特征名:One-Hot 编码后的列名 ohe = preprocessor.named_transformers_['cat'] cat_names = ohe.get_feature_names_out(cat_features) all_feature_names = np.concatenate([cat_names, num_features]) # 取重要性最高的 15 个特征 importances = rf.feature_importances_ indices = np.argsort(importances)[::-1][:15] plt.figure(figsize=(10, 6)) plt.barh(range(15), importances[indices][::-1]) plt.yticks(range(15), [all_feature_names[i] for i in indices][::-1]) plt.xlabel('Feature Importance') plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)从实际跑出来的结果看,src_bytes、dst_bytes、same_srv_rate、serror_rate这几个特征重要性排在前列,符合安全直觉——攻击流量往往在包大小和连接频率上和正常流量有显著差异。如果你发现某个 One-Hot 编码后的service类别重要性异常高,比如private或http,那说明数据集中这类服务被攻击的比例偏高,可以在报告里展开分析。注意get_feature_names_out是 sklearn 1.0 之后的 API,老版本用get_feature_names,跑之前先确认版本。
3.3 多模型对比:逻辑回归、SVM 与 XGBoost 的取舍
只跑一个随机森林显得单薄,源码里还提供了逻辑回归和 SVM 的对比脚本。逻辑回归训练最快,但线性决策边界对复杂攻击模式拟合能力有限,准确率通常比随机森林低 2 到 5 个百分点。SVM 在小样本上表现不错,但 NSL-KDD 训练集有 12 万条左右,核函数计算开销大,跑一次要等很久。XGBoost 是后来加的,梯度提升树在结构化数据上通常比随机森林略好,但调参更麻烦,学习率、树深度、正则化系数都要试。
| 模型 | 训练时间(12 万条) | 准确率 | 召回率 | 适用场景 |
|---|---|---|---|---|
| 逻辑回归 | 约 10 秒 | 0.92 | 0.90 | 快速基线,可解释性强 |
| 随机森林 | 约 30 秒 | 0.97 | 0.96 | 默认推荐,平衡好 |
| SVM (RBF) | 约 5 分钟 | 0.95 | 0.94 | 小样本,维度低 |
| XGBoost | 约 1 分钟 | 0.98 | 0.97 | 追求极致指标 |
表格里的数据是我在自己机器上跑出来的参考值,你的环境不同会有波动。选哪个模型取决于项目要求:如果只是期末大作业,随机森林足够;如果要做对比实验写论文,四个都跑一遍,把表格填满。注意 SVM 那行的时间开销,如果电脑配置一般,建议先对训练集做子采样,否则等起来很煎熬。
4. 避坑与排查:跑这套源码时最容易翻车的五个地方
4.1 数据集路径和编码问题
现象:运行pd.read_csv时报FileNotFoundError或者读出来的 DataFrame 列数不对。原因通常是数据集文件没放在脚本同级目录,或者 NSL-KDD 的 txt 文件用了非 UTF-8 编码。解决方法是把KDDTrain+.txt和KDDTest+.txt放到和.py文件同一目录下,如果还有编码报错,加encoding='utf-8'或encoding='latin-1'参数试一下。我一般会在读文件前先用os.path.exists检查一下路径,省得跑到一半才报错。
4.2 One-Hot 编码后维度爆炸
现象:service特征有 70 多种取值,One-Hot 之后特征维度从 41 涨到 120 多,训练变慢,内存占用升高。原因是service类别太细,有些取值在训练集里只出现几次。解决办法是先把出现次数少于 10 次的service类别统一归为other,再做 One-Hot。这样维度能压到 80 左右,对准确率影响很小。源码里没做这一步,你可以自己加一个value_counts过滤。
4.3 测试集准确率远低于训练集
现象:训练集准确率 99%,测试集只有 80% 出头。原因是 NSL-KDD 的测试集里包含训练集没出现过的攻击类型,模型没见过,自然判错。这不是代码 bug,是数据集本身的设定。解决办法是在报告里说明这一点,或者用交叉验证在训练集内部评估,再单独讨论测试集上的未知攻击检测率。如果非要提升测试集表现,可以尝试用自编码器做异常检测,但那就偏离这份源码的范围了。
4.4 随机森林的n_jobs参数在 Windows 上无效
现象:设了n_jobs=-1但 CPU 占用还是单核。原因是 Windows 下 sklearn 的并行后端有时和multiprocessing冲突。解决办法是把n_jobs改成具体核心数,比如n_jobs=4,或者把训练代码放在if __name__ == '__main__':保护块里。这个坑在 Linux 上很少遇到,Windows 用户注意一下。
4.5 模型保存与加载后预测结果不一致
现象:用joblib.dump保存模型,重新加载后对同一条数据预测结果变了。原因是保存时只存了分类器,没存预处理管道,加载后新数据没经过同样的 One-Hot 和标准化。解决办法是把preprocessor和rf一起打包成一个 Pipeline 再保存,或者用字典分别存两个对象,加载后按顺序调用。源码里用的是分开保存的方式,你部署的时候记得把预处理逻辑也带上。
5. 进阶技巧:用交叉验证和 ROC 曲线把评估做扎实
5.1 分层 K 折交叉验证替代单次划分
单次训练集/测试集划分受随机种子影响大,换一个random_state准确率可能差一两个百分点。更稳的做法是分层 K 折交叉验证,把训练集分成 5 份,每份都做一次验证,最后取平均。分层的意思是每折里正常和攻击的比例和原始数据一致,避免某一折全是正常流量导致评估失真。
from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.pipeline import Pipeline # 把预处理和分类器串成完整管道 full_pipeline = Pipeline([ ('prep', preprocessor), ('clf', RandomForestClassifier(n_estimators=100, class_weight='balanced', random_state=42, n_jobs=-1)) ]) # 分层 5 折交叉验证,评估指标用 F1 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(full_pipeline, train_df.drop(columns=['label', 'difficulty']), y_train, cv=skf, scoring='f1', n_jobs=-1) print('每折 F1:', scores) print('平均 F1: %.4f (+/- %.4f)' % (scores.mean(), scores.std()))注意这里传入的是原始 DataFrame 而不是已经预处理过的X_train,因为 Pipeline 里已经包含了preprocessor,交叉验证的每一折都会在训练部分重新 fit 预处理器,避免数据泄露。scoring='f1'比准确率更适合不平衡数据,如果你更关心误报率,可以换成scoring='precision'。跑完 5 折大概需要两三分钟,比单次训练慢,但结果更可信。
5.2 ROC 曲线与 AUC 值:选阈值比选模型更重要
分类器默认输出 0 或 1,但实际部署时你可以调整判定阈值。比如把攻击概率大于 0.7 才判为攻击,这样误报会减少,但漏报会增加。ROC 曲线就是展示不同阈值下真正率和假正率 trade-off 的工具,AUC 值越大,模型整体排序能力越强。
from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt # 获取攻击类别的预测概率 y_prob = rf.predict_proba(X_test)[:, 1] fpr, tpr, thresholds = roc_curve(y_test, y_prob) roc_auc = auc(fpr, tpr) plt.figure(figsize=(8, 6)) plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (AUC = %0.4f)' % roc_auc) plt.plot([0, 1], [0, 1], color='navy', lw=1, linestyle='--') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.legend(loc='lower right') plt.tight_layout() plt.savefig('roc_curve.png', dpi=150) print('AUC:', roc_auc)随机森林在 NSL-KDD 上的 AUC 通常能到 0.98 以上,说明模型对攻击和正常的排序能力很强。但 AUC 高不代表某个固定阈值下表现好,你可以在thresholds数组里找一个让 FPR 低于 1% 的阈值,看看对应的 TPR 是多少。我一般会在报告里同时给出默认阈值和调优阈值下的混淆矩阵,让读者看到误报和漏报的取舍。从那以后我每次做安全检测项目,都会把 ROC 曲线和阈值分析单独写一节,因为运维同事最关心的不是准确率,而是「我设多少阈值能既不漏报又不被误报淹没」。希望帮到你。
本文还有配套的精品资源,点击获取