ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于欠采样与随机森林的入侵检测模型实战:从KDD数据集到Flask部署

基于欠采样与随机森林的入侵检测模型实战:从KDD数据集到Flask部署 简介这份毕业设计资源面向计算机、网络安全方向的学生与自学者围绕Python欠采样与随机森林算法构建入侵检测模型解决类别不平衡场景下检测精度不足的问题适合作为课程设计、毕业设计或算法实践参考。压缩包共172个文件约53.66MB包含39个py源码、31个csv数据集、16个pkl模型文件、4个arff格式的KDD数据集以及部署文档、sqlite3数据库、前端页面与图表资源覆盖数据处理、模型训练到可视化展示的完整链路。已有260人学习下载说明其具备一定参考价值。资源经过本地编译验证可运行评审分达95分以上内容经助教审定难度适中。读者可获得可复现的源码工程、训练好的模型文件、KDDTrain与KDDTest等标准数据集以及部署说明与排错思路便于快速理解欠采样与随机森林在入侵检测中的落地方式并在此基础上完成二次开发或论文撰写。1. 从一份 95 分毕设拆起欠采样 随机森林做入侵检测到底靠不靠谱如果你正在做网络安全方向的毕业设计或者需要一套能跑通、能答辩、能写进论文的入侵检测代码这份「基于 Python 欠采样-随机森林的入侵检测模型」资源包值得先看一眼。它把 KDDTrain、KDDTrain_20Percent、KDDTest、KDDTest-21 四个经典 ARFF 数据集、训练好的模型、部署文档和前端展示页面highcharts.css、stat.css、index.css 等全部打包在一起属于那种「拿到手就能复现、改改就能写论文」的完整项目。入侵检测的本质是二分类或多分类问题把网络流量特征喂给模型判断这条连接是正常还是攻击。KDD Cup 99 数据集虽然老但特征工程清晰、类别标注完整至今仍是教学和毕设的首选。问题在于正常流量样本远多于攻击样本直接训练随机森林会导致模型偏向多数类攻击召回率惨不忍睹。这份资源用欠采样undersampling先平衡数据分布再用随机森林做分类思路简单但有效评审分能到 95 分以上不是没道理的。适合谁用做毕设的本科生、需要快速搭一个 IDS 原型的初级安全工程师、想学随机森林实战的 Python 学习者。不适合谁指望直接上生产环境扛真实流量的——KDD 数据集和真实网络流量差距很大这个后面会细说。2. 欠采样与随机森林的配合逻辑为什么不是直接上模型2.1 类别不平衡对随机森林的真实影响KDDTrain 里正常流量normal大约占 53%各类攻击加起来 47%看起来不算极端不平衡。但如果你做多分类U2R 和 R2L 这两类攻击的样本量只有几十到几百条而 normal 有六万多条比例可能到 1:1000 以上。随机森林的基尼不纯度或信息增益在划分节点时会天然偏向多数类因为多数类能带来更大的信息增益。结果就是模型在测试集上整体准确率很高比如 99%但 U2R 的召回率可能只有 10% 甚至更低。这不是随机森林的锅任何基于经验风险最小化的分类器都有这个问题。常见做法有三种过采样SMOTE、欠采样、代价敏感学习。这份资源选了欠采样原因很直接——KDD 数据集本身够大欠采样后仍有足够样本训练而且欠采样实现简单、不引入合成样本的噪声。2.2 欠采样的具体策略与代码实现欠采样不是随便删多数类样本。常见策略有随机欠采样RandomUnderSampler、NearMiss、Tomek Links、Edited Nearest Neighbours。这份资源用的是随机欠采样把 normal 类样本随机抽到和攻击类样本数量相当的水平。下面是我一般会用的实现方式基于 imbalanced-learn 库from imblearn.under_sampling import RandomUnderSampler from collections import Counter import numpy as np # 假设 X_train 是特征矩阵y_train 是标签 # 先看原始分布 print(原始分布:, Counter(y_train)) # 随机欠采样把多数类降到和少数类一样多 # sampling_strategy 可以指定目标比例比如 1.0 表示 1:1 rus RandomUnderSampler( sampling_strategy1.0, # 多数类:少数类 1:1 random_state42 # 固定随机种子保证可复现 ) X_resampled, y_resampled rus.fit_resample(X_train, y_train) print(欠采样后分布:, Counter(y_resampled))逻辑说明fit_resample会返回新的特征矩阵和标签原始数据不变。sampling_strategy1.0表示把每个多数类样本降到与少数类样本数量相同。如果你的攻击类别有多个比如 DoS、Probe、U2R、R2L欠采样会分别对每个类别做平衡最终所有类别样本数等于最小的那个类别的样本数。这可能导致总样本量大幅减少所以random_state一定要固定否则每次跑出来的结果都不一样论文里的数据就对不上了。参数说明sampling_strategy还可以传字典比如{0: 5000, 1: 5000, 2: 5000, 3: 5000}手动指定每个类别的目标数量。random_state建议固定为 42 或 0方便复现。如果欠采样后样本太少可以考虑sampling_strategy0.5让多数类是少数类的两倍保留更多信息。2.3 随机森林的关键参数怎么设随机森林在 scikit-learn 里的核心参数有n_estimators、max_depth、min_samples_split、min_samples_leaf、max_features、class_weight。这份资源里的模型大概率用了默认参数或简单调参但你要写论文的话最好把参数调优过程写进去。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV # 基础模型 rf RandomForestClassifier( n_estimators100, # 树的数量太少欠拟合太多训练慢 max_depthNone, # 不限制深度让树充分生长 min_samples_split2, # 节点分裂最小样本数 min_samples_leaf1, # 叶子节点最小样本数 max_featuressqrt, # 每次分裂考虑的特征数sqrt(n_features) class_weightNone, # 欠采样后已经平衡不需要再设权重 random_state42, n_jobs-1 # 用所有 CPU 核心并行训练 ) # 网格搜索调参可选论文里可以写 param_grid { n_estimators: [50, 100, 200], max_depth: [10, 20, None], min_samples_split: [2, 5, 10] } grid_search GridSearchCV( estimatorrf, param_gridparam_grid, cv5, # 5 折交叉验证 scoringf1_macro, # 多分类用 macro F1 n_jobs-1, verbose1 ) grid_search.fit(X_resampled, y_resampled) print(最佳参数:, grid_search.best_params_)逻辑说明n_estimators从 100 开始试200 通常够用再往上收益递减。max_depthNone让每棵树完全生长随机森林靠 bagging 和特征随机性来防过拟合不需要像单棵决策树那样剪枝。max_featuressqrt是分类任务的默认值回归任务用log2或None。class_weight在欠采样后可以不设但如果你的欠采样比例不是 1:1可以设balanced让模型自动调整权重。注意网格搜索很耗时KDD 数据集有 40 多万条样本5 折交叉验证 × 9 组参数 45 次训练每次训练 100 棵树普通笔记本可能要跑几十分钟。建议先用小样本比如 KDDTrain_20Percent调参确定大致范围后再用全量数据跑最终模型。3. 从 ARFF 到模型完整训练流程与部署文档拆解3.1 ARFF 文件读取与特征工程KDD 数据集是 ARFF 格式scikit-learn 不直接支持需要用 scipy 或手动解析。常见做法是用scipy.io.arff读取然后转成 numpy 数组。但 KDD 的 ARFF 文件里有符号特征protocol_type、service、flag需要做独热编码或标签编码。from scipy.io import arff import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, OneHotEncoder # 读取 ARFF 文件 data, meta arff.loadarff(KDDTrain.arff) df pd.DataFrame(data) # 符号特征列 categorical_cols [protocol_type, service, flag] # 标签列是 class先分离特征和标签 X df.drop(class, axis1) y df[class] # 对符号特征做独热编码 # 注意KDD 的 service 有 70 种取值独热后维度会膨胀 encoder OneHotEncoder(sparse_outputFalse, handle_unknownignore) X_encoded encoder.fit_transform(X[categorical_cols]) X_numeric X.drop(categorical_cols, axis1).values X_final np.hstack([X_numeric, X_encoded]) # 标签编码把攻击类型转成数字 le LabelEncoder() y_encoded le.fit_transform(y) print(标签映射:, dict(zip(le.classes_, le.transform(le.classes_))))逻辑说明arff.loadarff返回的是结构化数组转成 DataFrame 方便处理。handle_unknownignore很重要——测试集里可能出现训练集没见过的 service 类型忽略后该维度全为 0不会报错。np.hstack把数值特征和独热特征拼在一起最终特征维度大约是 41 原始特征 70 多维独热 110 多维。参数说明sparse_outputFalse返回稠密数组方便后续处理如果内存不够可以设True返回稀疏矩阵但随机森林不支持稀疏输入需要先.toarray()。LabelEncoder对标签编码后le.classes_的顺序是字母序比如[back, buffer_overflow, ftp_write, ...]写论文时要注明。3.2 训练集/测试集划分与模型评估KDD 数据集已经分好了 KDDTrain 和 KDDTest但 KDDTest 里有一些训练集没出现过的攻击类型这是故意的用来测试模型的泛化能力。常见做法是直接用 KDDTrain 训练KDDTest 测试不做额外的划分。from sklearn.metrics import classification_report, confusion_matrix, accuracy_score # 用 KDDTrain 训练KDDTest 测试 # 注意测试集也要做同样的编码 data_test, _ arff.loadarff(KDDTest.arff) df_test pd.DataFrame(data_test) X_test df_test.drop(class, axis1) y_test df_test[class] X_test_encoded encoder.transform(X_test[categorical_cols]) X_test_numeric X_test.drop(categorical_cols, axis1).values X_test_final np.hstack([X_test_numeric, X_test_encoded]) y_test_encoded le.transform(y_test) # 训练随机森林 rf RandomForestClassifier(n_estimators100, random_state42, n_jobs-1) rf.fit(X_resampled, y_resampled) # 预测 y_pred rf.predict(X_test_final) # 评估 print(准确率:, accuracy_score(y_test_encoded, y_pred)) print(\n分类报告:) print(classification_report(y_test_encoded, y_pred, target_namesle.classes_)) print(\n混淆矩阵:) print(confusion_matrix(y_test_encoded, y_pred))逻辑说明encoder.transform而不是fit_transform保证测试集和训练集用同一套编码规则。le.transform同理。如果测试集出现训练集没有的标签le.transform会报错这时候需要把新标签归到unknown类或者忽略。参数说明classification_report会输出每个类别的 precision、recall、f1-score。重点关注 U2R 和 R2L 的 recall这两个是难点。confusion_matrix可以看到具体哪些类别被混淆了比如 R2L 经常被误判为 normal。3.3 部署文档里的前端展示与模型持久化资源包里带了 highcharts.css、stat.css、index.css 等前端文件说明有一个 Web 展示页面。常见做法是用 Flask 或 Django 搭一个简单后端加载训练好的模型接收前端传来的特征数据返回预测结果。import joblib from flask import Flask, request, jsonify # 保存模型 joblib.dump(rf, rf_ids_model.pkl) joblib.dump(encoder, encoder.pkl) joblib.dump(le, label_encoder.pkl) # Flask 部署示例 app Flask(__name__) app.route(/predict, methods[POST]) def predict(): data request.json # 假设 data 是包含 41 个特征的字典 features pd.DataFrame([data]) features_encoded encoder.transform(features[categorical_cols]) features_numeric features.drop(categorical_cols, axis1).values features_final np.hstack([features_numeric, features_encoded]) prediction rf.predict(features_final) label le.inverse_transform(prediction)[0] return jsonify({prediction: label}) if __name__ __main__: app.run(host0.0.0.0, port5000)逻辑说明joblib.dump比 pickle 更适合保存 numpy 数组和大型模型。Flask 的/predict接口接收 JSON 格式的特征数据返回预测标签。前端页面用 highcharts 展示统计图表比如各类攻击的占比、模型准确率趋势等。参数说明host0.0.0.0让服务监听所有网卡方便局域网访问。port5000是 Flask 默认端口可以改成 8080 或其他。生产环境不要用app.run应该用 gunicorn 或 uWSGI。4. 避坑与排查那些答辩前夜才发现的翻车点4.1 欠采样后模型在测试集上表现暴跌现象训练集上准确率 99%测试集上只有 80% 多U2R 召回率几乎为 0。原因欠采样把训练集的 normal 样本删得太多模型没见过足够多的正常流量模式泛化能力下降。KDDTest 里的 normal 样本分布和 KDDTrain 不完全一样欠采样后的模型对 normal 的识别能力变弱。解决不要用 1:1 的欠采样比例改成 0.5 或 0.3保留更多 normal 样本。或者改用 SMOTE 欠采样组合SMOTEENN先过采样少数类再欠采样多数类。我一般会对比几组比例选测试集 macro F1 最高的那个。4.2 独热编码后特征维度爆炸导致内存不足现象OneHotEncoder转换后内存直接爆掉或者训练时卡死。原因KDD 的 service 特征有 70 种取值flag 有 11 种protocol_type 有 3 种独热后增加 80 多维。如果再加上其他特征总维度可能到 120 以上。40 万条样本 × 120 维 × 8 字节 约 384 MB看起来不大但随机森林训练时会复制多份数据内存占用可能翻几倍。解决用sparse_outputTrue返回稀疏矩阵训练前再.toarray()。或者用pd.get_dummies代替 OneHotEncoder效果一样但更省内存。如果还是不够用 KDDTrain_20Percent 先跑通流程。4.3 测试集出现训练集没有的标签导致 LabelEncoder 报错现象le.transform(y_test)报ValueError: y contains previously unseen labels。原因KDDTest 里有 14 种攻击类型KDDTrain 里只有 22 种但两者不完全重合。比如 KDDTest 里的apache2、udpstorm、processtable在 KDDTrain 里没有。解决在LabelEncoder里加一个unknown类把所有未见过的标签映射到它。或者用le.classes_手动构建映射字典遇到未知标签就跳过或归为other。写论文时要说明这一点否则答辩老师会问「测试集里的新攻击类型你怎么处理的」。4.4 随机森林模型文件太大导致部署失败现象joblib.dump保存的模型文件超过 1 GBFlask 加载时内存溢出。原因100 棵树 × 每棵树深度不限 × 40 万样本模型文件确实可能很大。如果n_estimators200或max_depthNone文件更大。解决限制max_depth20或min_samples_leaf5减小树的大小。或者用n_estimators50准确率下降不多但模型小一半。部署时用joblib.load的mmap_moder参数内存映射加载不占实际内存。4.5 前端页面跨域请求被浏览器拦截现象Flask 后端跑在 5000 端口前端页面跑在 8080 端口请求/predict时报 CORS 错误。原因浏览器同源策略限制不同端口视为不同源。解决安装flask-cors在 Flask 里加一行CORS(app)。或者把前端页面直接放到 Flask 的static目录下用同一个端口访问。我一般用后者省事。5. 进阶技巧用特征重要性做特征选择把模型压到 50 MB 以内训练完随机森林后feature_importances_属性会给出每个特征的重要性分数。KDD 数据集里有些特征是冗余的比如num_outbound_cmds全是 0is_host_login几乎全是 0。把这些特征删掉模型体积和推理时间都能降下来。import matplotlib.pyplot as plt # 获取特征重要性 importances rf.feature_importances_ # 特征名数值特征名 独热编码后的特征名 feature_names list(X.drop(categorical_cols, axis1).columns) \ list(encoder.get_feature_names_out(categorical_cols)) # 按重要性排序 indices np.argsort(importances)[::-1] # 取前 30 个特征 top_k 30 top_indices indices[:top_k] top_features [feature_names[i] for i in top_indices] top_importances importances[top_indices] # 打印 for i in range(top_k): print(f{i1}. {top_features[i]}: {top_importances[i]:.4f}) # 用前 30 个特征重新训练 X_top X_final[:, top_indices] X_test_top X_test_final[:, top_indices] rf_top RandomForestClassifier(n_estimators100, random_state42, n_jobs-1) rf_top.fit(X_top, y_encoded) y_pred_top rf_top.predict(X_test_top) print(特征选择后准确率:, accuracy_score(y_test_encoded, y_pred_top))逻辑说明feature_importances_是每棵树不纯度减少量的平均值归一化后总和为 1。取前 30 个特征通常能保留 95% 以上的重要性。重新训练后模型输入维度从 120 降到 30模型文件大小可能从 500 MB 降到 100 MB 以内。参数说明top_k可以根据实际情况调整20 到 50 之间试。如果准确率下降超过 1%说明删多了加回去几个。encoder.get_feature_names_out返回独热编码后的特征名格式是protocol_type_tcp、service_http这种。还有一个技巧用max_features参数控制每棵树分裂时考虑的特征数。设max_features0.3比sqrt更激进树之间的相关性更低但单棵树更弱。我一般会对比sqrt、log2、0.3、0.5 几组选交叉验证 F1 最高的。最后说一个血泪经验答辩前一定要把整个流程从头跑一遍包括数据读取、编码、欠采样、训练、评估、保存模型、启动 Flask、前端请求。我见过太多人模型训练好了结果部署时发现encoder.pkl忘了保存或者前端传过来的特征顺序和训练时不一致预测结果全是错的。从那以后我每次部署前都强制走一遍端到端测试用一条已知标签的样本验证预测结果是否正确。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进