简介:这份资源是面向高校学生与初学者的机器学习入侵检测系统完整项目源码,适用于毕业设计、期末大作业与课程设计等场景,帮助读者快速搭建一个可运行、可讲解的入侵检测实验项目。压缩包共16个文件,约17.52MB,以py源码、xml配置、gz数据集及iml工程文件为主,其中Python脚本承担模型训练与检测主流程,xml与iml用于IDE工程配置,gz文件为KDD Cup数据集,便于直接复现实验。项目围绕KDD99数据展开,包含CNN等模型实现,代码附有详细注释,新手也能看懂整体逻辑与关键步骤。目前已有237人学习下载,说明其具备一定参考价值。读者可获得一套结构清晰的课程设计级方案,理解数据预处理、特征处理、模型训练与结果评估的完整链路,并借助注释与README快速部署运行,适合作为入门机器学习安全应用的实践起点。
1. 从一份课程设计源码说起:机器学习入侵检测到底在做什么
很多人第一次接触「基于机器学习的入侵检测系统」,是在课程设计选题表里看到它——名字听着唬人,真拿到一份 python 源码加详细注释的压缩包,打开却不知道从哪看起。我当年也是这样,把train.py从头翻到尾,模型跑通了,准确率 99%,结果答辩老师一句「你这 99% 是怎么来的」就把我问住了。后来做久了才明白,入侵检测这个方向,模型只是冰山一角,真正决定成败的是数据怎么来、特征怎么选、评估怎么算。这份源码能帮你省掉搭骨架的时间,但骨架背后的判断,得自己补上。
这篇笔记面向三类人:正在做课程设计、需要一份能跑通、能讲清楚的项目;刚入门机器学习、想找一个真实场景练手的同学;以及已经会调库、但没系统做过安全类检测任务的工程师。我会顺着「数据 → 特征 → 模型 → 评估 → 落地」这条线,把一份入侵检测源码里每个模块为什么这么写、参数怎么调、哪里最容易翻车讲透。你不需要先成为安全专家,但需要愿意动手改代码、看输出、对着指标反思。
2. 入侵检测的数据底座:KDD/NSL-KDD 到 CICIDS 怎么选、怎么读
2.1 为什么数据集选错,后面全白搭
入侵检测本质是一个分类问题:给一条网络流量记录,判断它是正常还是某种攻击。所以第一件事不是写模型,而是搞清楚你的数据长什么样。课程设计里最常见的是 KDD Cup 99 和它的改进版 NSL-KDD,这两个数据集年纪比很多读者都大,但胜在格式规整、标注清晰、网上资料多。缺点是太老,很多攻击类型在现代网络里已经过时,模型学到的可能是「历史规律」而不是「通用检测能力」。
如果想让项目看起来更贴近真实场景,可以换 CICIDS2017 或 CIC-IDS2018,它们由加拿大网络安全研究所发布,包含 DDoS、暴力破解、Web 攻击等更现代的流量类型。代价是数据量大、类别不平衡严重、预处理更麻烦。我的建议是:课程设计用 NSL-KDD 把流程跑通,想加分再补一个 CICIDS 的对比实验。不要一上来就啃 CICIDS,清洗阶段就能劝退一半人。
选好数据集后,读数据这一步就有坑。NSL-KDD 的KDDTrain+.txt没有表头,官方文档里给了 43 列的含义,前 41 列是特征,第 42 列是标签,第 43 列是难度分数。很多人直接pd.read_csv不指定列名,结果后面取特征时对不上号。正确做法是把列名显式写出来,或者至少把标签列单独拎出来。
import pandas as pd # NSL-KDD 官方 43 列定义,前 41 特征 + label + difficulty 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 = pd.read_csv('KDDTrain+.txt', names=col_names) test = pd.read_csv('KDDTest+.txt', names=col_names) # 标签里 normal 是正常,其余都是攻击;先看分布再决定怎么处理 print(train['label'].value_counts().head(10))这段代码的关键在names=col_names,不写的话 pandas 会把第一行数据当表头,后面所有特征偏移一列,模型训练出来指标虚高但毫无意义。value_counts()用来确认类别分布,NSL-KDD 训练集里正常流量约占 53%,攻击类型有 20 多种,其中neptune、smurf这类占大头,长尾类别样本极少。看到这个分布,你就该意识到后面不能用 accuracy 当唯一指标。
2.2 类别不平衡与标签二值化:先想清楚检测目标
拿到标签后要做的第一个决策是:做多分类还是二分类。多分类是把neptune、smurf、portsweep等每种攻击单独识别,看起来更细,但长尾类别样本太少,模型根本学不动,最后指标很难看。二分类是把所有攻击归为attack,正常归为normal,任务简单、指标稳定,适合课程设计。我一般会先做二分类把流程跑通,再尝试多分类作为进阶。
二值化代码很简单,但要注意别把normal误伤:
# 二值化:normal 为 0,其余全部为 1 train['binary_label'] = train['label'].apply(lambda x: 0 if x == 'normal' else 1) test['binary_label'] = test['label'].apply(lambda x: 0 if x == 'normal' else 1) # 检查一下有没有意外情况 assert set(train['binary_label'].unique()) == {0, 1} print(train['binary_label'].mean()) # 攻击占比,NSL-KDD 训练集约 0.47apply里的 lambda 判断的是字符串完全相等,如果数据里出现Normal或带空格的情况就会漏掉,所以后面加一个assert确认标签只有 0 和 1。mean()输出的是攻击样本比例,接近 0.47 说明二值化正确。如果这个值明显偏离,回去检查原始标签里是不是有拼写差异。
特征工程部分,NSL-KDD 有 3 个类别型特征:protocol_type、service、flag。常见做法是 One-Hot 编码,但service有 70 多个取值,One-Hot 后维度爆炸,而且测试集里可能出现训练集没见过的服务类型。更稳的做法是用pd.get_dummies后对齐列,或者直接用LabelEncoder做序号编码再交给树模型。我一般用前者,因为树模型对 One-Hot 不敏感,而 One-Hot 能避免人为引入大小关系。
from sklearn.preprocessing import OneHotEncoder cat_cols = ['protocol_type', 'service', 'flag'] encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False) # 只在训练集上 fit,测试集 transform,避免数据泄漏 encoder.fit(train[cat_cols]) train_cat = encoder.transform(train[cat_cols]) test_cat = encoder.transform(test[cat_cols]) print(train_cat.shape, test_cat.shape)handle_unknown='ignore'是关键参数,它保证测试集里出现训练集没见过的类别时不会报错,而是全部编码为 0。sparse_output=False让输出是稠密数组,方便后面和数值特征拼接。注意fit只能用在训练集上,如果对全量数据 fit 再切分,测试集的信息就泄漏进编码器了,指标会虚高,这是很多人翻车的地方。
3. 特征处理与模型训练:从 RandomForest 到 XGBoost 的最小可跑通流程
3.1 数值特征标准化:不是所有模型都需要
数值特征里有duration、src_bytes、dst_bytes这种跨度极大的列,最大值可能上百万,而wrong_fragment这种只有 0 或 1。如果直接用 KNN、SVM 或逻辑回归,量纲差异会让距离计算失真,必须做标准化。但如果你用 RandomForest、XGBoost 这类树模型,标准化其实不影响结果,因为树只关心特征的排序和分裂点,不关心绝对数值。
我一般会先跑一个 RandomForest 作为 baseline,因为它对参数不敏感、训练快、能输出特征重要性,非常适合快速验证流程。代码大致如下:
import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix # 数值特征:去掉类别列、标签列、难度列 drop_cols = cat_cols + ['label', 'difficulty', 'binary_label'] num_cols = [c for c in train.columns if c not in drop_cols] X_train = np.hstack([train[num_cols].values, train_cat]) X_test = np.hstack([test[num_cols].values, test_cat]) y_train = train['binary_label'].values y_test = test['binary_label'].values rf = RandomForestClassifier( n_estimators=100, # 树的数量,100 是性价比很高的起点 max_depth=None, # 不限制深度,让树充分生长 min_samples_leaf=1, # 叶子最小样本数,类别不平衡时可调到 2~5 n_jobs=-1, # 用满所有 CPU 核心 random_state=42 # 固定随机种子,保证结果可复现 ) 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))n_estimators=100是常见起点,再往上加收益递减但训练时间线性增长。random_state=42必须固定,否则每次跑出来的指标都不一样,答辩时说不清楚。n_jobs=-1在 Windows 上有时会出问题,如果报错就改成n_jobs=1。训练完看classification_report,重点看 attack 类的 recall 和 f1-score,因为入侵检测里漏报一个攻击比误报一个正常流量代价大得多。
3.2 从 RandomForest 到 XGBoost:指标提升与参数怎么调
RandomForest 跑通后,通常会换 XGBoost 或 LightGBM 再试一轮,因为梯度提升树在表格数据上往往比随机森林强一截。XGBoost 的参数比 RandomForest 多,但核心就几个:n_estimators、max_depth、learning_rate、subsample、colsample_bytree。我的习惯是先设learning_rate=0.1、max_depth=6、n_estimators=200跑一版,再看指标往哪个方向调。
from xgboost import XGBClassifier xgb = XGBClassifier( n_estimators=200, max_depth=6, # 树深,6 是表格数据的常用值,太深容易过拟合 learning_rate=0.1, # 学习率,调小到 0.05 通常能涨点但训练变慢 subsample=0.8, # 每棵树用 80% 样本,防过拟合 colsample_bytree=0.8, # 每棵树用 80% 特征,防过拟合 eval_metric='logloss', random_state=42, n_jobs=-1 ) xgb.fit(X_train, y_train) y_pred_xgb = xgb.predict(X_test) print(classification_report(y_test, y_pred_xgb, digits=4))subsample和colsample_bytree是 XGBoost 防过拟合的两个重要开关,设成 0.8 意味着每棵树只随机看 80% 的样本和特征,能显著降低方差。learning_rate和n_estimators是一对:学习率调小,树的数量要相应增加,否则模型欠拟合。如果指标卡住了,先动max_depth,从 6 降到 4 或升到 8 各跑一次,看验证集指标变化,比盲目调learning_rate更有效。
特征重要性可以用xgb.feature_importances_看,但要注意它给的是分裂增益,不是因果重要性。NSL-KDD 上排名靠前的通常是src_bytes、dst_bytes、same_srv_rate、serror_rate这几个,和直觉一致:攻击流量在字节数和连接错误率上确实和正常流量有差异。如果发现某个特征重要性异常高,先怀疑是不是标签泄漏,比如把difficulty列误当成特征喂进去了。
4. 评估指标与结果解读:为什么 99% 准确率可能是假的
4.1 准确率陷阱与混淆矩阵的正确读法
入侵检测数据集里正常流量占多数,如果模型把所有样本都预测成正常,准确率也能到 50% 以上。NSL-KDD 测试集里攻击比例和训练集不同,直接看 accuracy 很容易被误导。真正该看的是混淆矩阵和基于它的 precision、recall、f1-score。
混淆矩阵四个格子:TP(攻击判为攻击)、TN(正常判为正常)、FP(正常判为攻击,误报)、FN(攻击判为正常,漏报)。入侵检测里 FN 的代价远大于 FP,因为漏掉一个攻击可能导致系统被入侵,而误报只是多弹一个告警。所以 recall(TP/(TP+FN))比 precision 更重要,但也不能只看 recall,否则模型全判攻击就能拿满分。
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score cm = confusion_matrix(y_test, y_pred_xgb) tn, fp, fn, tp = cm.ravel() precision = precision_score(y_test, y_pred_xgb) recall = recall_score(y_test, y_pred_xgb) f1 = f1_score(y_test, y_pred_xgb) print(f"TP={tp}, FP={fp}, FN={fn}, TN={tn}") print(f"Precision={precision:.4f}, Recall={recall:.4f}, F1={f1:.4f}")cm.ravel()按[tn, fp, fn, tp]顺序展开,这是 sklearn 的约定,别记反。如果 FN 明显偏高,说明模型对攻击类不够敏感,可以尝试调低分类阈值、增加攻击类样本权重、或者换用scale_pos_weight参数。如果 FP 偏高,说明模型太激进,可以调高阈值或增加正则化。
4.2 交叉验证与数据泄漏排查
单次划分训练集和测试集的结果波动可能很大,尤其是样本量不大时。更稳的做法是 K 折交叉验证,把训练集分成 K 份,轮流做验证,最后取平均。这样能看出模型指标是否稳定,也能发现过拟合。
from sklearn.model_selection import cross_val_score # 用 5 折交叉验证评估 RandomForest cv_scores = cross_val_score( rf, X_train, y_train, cv=5, # 5 折 scoring='f1', # 关注 f1,而不是 accuracy n_jobs=-1 ) print("CV F1 scores:", cv_scores) print("Mean F1:", cv_scores.mean(), "Std:", cv_scores.std())scoring='f1'指定用 f1 作为评估标准,比默认的 accuracy 更贴合入侵检测场景。如果 5 折的 f1 标准差很大,比如超过 0.05,说明模型对数据划分敏感,可能是样本太少或类别不平衡太严重。这时候可以考虑分层抽样StratifiedKFold,保证每折里攻击和正常的比例一致。
数据泄漏是另一个常见问题。典型表现是训练集指标接近完美,测试集一塌糊涂。排查方法:检查有没有在划分数据前做了全局标准化、有没有把标签相关的列混进特征、有没有用测试集调参。我一般会在预处理阶段就把训练集和测试集彻底分开,所有 fit 操作只在训练集上做,测试集只做 transform。
5. 避坑与排查:入侵检测项目里最容易翻车的 5 个地方
5.1 标签编码不一致导致指标虚高
现象:训练时 accuracy 99%,换一份测试数据掉到 60%。原因:训练集和测试集的标签编码方式不同,比如训练集里normal=0、attack=1,测试集里顺序反了,模型学到的映射对不上。解决:在代码里显式定义标签映射字典,训练和测试共用同一个函数,并在加载数据后打印两边的标签分布做对比。
5.2 One-Hot 编码在测试集上维度对不上
现象:transform时报错feature_names mismatch或维度不一致。原因:训练集和测试集分别 fit 了编码器,或者测试集出现了训练集没有的类别。解决:编码器只在训练集上 fit,测试集用同一个编码器 transform,并设置handle_unknown='ignore'。如果维度还是对不上,检查是不是在拼接时把类别列和数值列的顺序搞混了。
5.3 用 accuracy 选模型导致漏报严重
现象:模型 accuracy 很高,但实际部署后大量攻击没被检出。原因:数据集不平衡,模型倾向于预测多数类(正常),accuracy 掩盖了 attack 类的低 recall。解决:把评估指标换成 recall 或 f1,并在交叉验证时指定scoring='f1'。如果 recall 仍然低,尝试设置class_weight='balanced'或调整分类阈值。
5.4 特征里混入标签泄漏列
现象:模型指标异常高,特征重要性里某个不相关的列排第一。原因:把difficulty、label的某种编码、或者数据集的索引列当成了特征。解决:在构造特征矩阵前,显式列出要排除的列,用drop删掉,并打印最终特征列名人工检查一遍。NSL-KDD 的difficulty列和标签有相关性,绝对不能当特征。
5.5 随机种子不固定导致结果不可复现
现象:每次跑代码指标都不一样,答辩时说不清哪个是最终结果。原因:train_test_split、模型初始化、采样过程都没有固定random_state。解决:在所有涉及随机的环节都加上random_state=42,包括数据划分、模型初始化、交叉验证的 shuffle。固定种子后,同一份代码在任何机器上跑出来的指标应该一致。
6. 把项目从「能跑」推到「能讲」:特征重要性分析与阈值调优的实战技巧
课程设计答辩时,老师最常问的两个问题是「为什么选这个模型」和「这个指标意味着什么」。光跑通代码不够,你得能解释模型学到了什么、误报漏报发生在哪里、怎么改进。这里分享两个我常用的技巧。
第一个是特征重要性可视化。XGBoost 训练完后,把feature_importances_和特征名对应起来,取前 15 个画条形图。NSL-KDD 上通常src_bytes、dst_bytes、same_srv_rate、serror_rate、count排前面。如果发现某个类别型特征编码后的某一维排名很高,比如service_http,可以结合业务解释:HTTP 服务流量大,攻击者常利用 Web 漏洞,所以这个特征有区分度。这种解释能让答辩从「我调了包」变成「我理解了数据」。
import matplotlib.pyplot as plt # 拼接后的特征名:数值列 + One-Hot 后的类别列 feature_names = num_cols + list(encoder.get_feature_names_out(cat_cols)) importances = xgb.feature_importances_ idx = np.argsort(importances)[-15:] # 取前 15 plt.figure(figsize=(8, 6)) plt.barh(np.array(feature_names)[idx], importances[idx]) plt.xlabel('Importance') plt.tight_layout() plt.show()encoder.get_feature_names_out(cat_cols)能拿到 One-Hot 后每一维对应的原始类别名,比如service_http、flag_SF,这样画出来的图才有可读性。如果版本较老没有这个方法,可以手动拼cat_cols和encoder.categories_。
第二个技巧是阈值调优。默认分类阈值是 0.5,即预测概率大于 0.5 判为攻击。但在入侵检测里,你可以调低阈值提高 recall,代价是 FP 增加。具体调多少取决于业务容忍度:如果系统只是告警、人工复核,可以容忍高 FP;如果自动阻断,就必须控制 FP。做法是遍历 0.1 到 0.9 的阈值,画 precision-recall 曲线,选一个 recall 满足要求且 precision 不太低的点。
from sklearn.metrics import precision_recall_curve y_prob = xgb.predict_proba(X_test)[:, 1] # 取攻击类的概率 precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob) # 找 recall >= 0.95 时 precision 最高的阈值 target_recall = 0.95 valid_idx = np.where(recalls[:-1] >= target_recall)[0] best_idx = valid_idx[np.argmax(precisions[valid_idx])] best_threshold = thresholds[best_idx] print(f"Best threshold: {best_threshold:.4f}") print(f"Precision: {precisions[best_idx]:.4f}, Recall: {recalls[best_idx]:.4f}")predict_proba返回两列,第二列是攻击类概率,别取错。precision_recall_curve返回的thresholds长度比precisions和recalls少 1,所以索引时要用[:-1]对齐。找到阈值后,在实际预测时用(y_prob >= best_threshold).astype(int)替代predict(),就能按自定义阈值输出结果。
最后说个我自己的习惯:每次改完参数或特征,我都会把指标、阈值、特征重要性截图存到一个文件夹里,命名带上日期和改动点。做课程设计时这些截图就是答辩素材,做工程项目时这些记录就是排查问题的后悔药。入侵检测这个方向,模型换来换去就那几种,真正拉开差距的是对数据的理解和对指标的判断。希望帮到你。
本文还有配套的精品资源,点击获取