资讯动态

基于Python欠采样与随机森林的入侵检测模型实战

发布时间:2026/10/18 10:12:13 来源:尧图企业网站定制
简介一套基于Python欠采样与随机森林的入侵检测毕业设计完整项目面向网络安全和机器学习方向的本科生、研究生及开发者可用于快速复现入侵检测实验、完成课设或毕设参考。包内共172个文件含39个Python源码、多份CSV与ARFF标准数据集、16个训练好的模型pkl文件、18张结果图以及HTML/CSS/JS展示页面整体约53.66MB覆盖从数据预处理、欠采样、随机森林建模到可视化部署的完整链路。项目内置KDDTrain、KDDTest等经典数据集其中CSV/ARFF负责训练与验证pkl保存训练好的分类器py脚本包含欠采样与评估逻辑png输出ROC曲线、混淆矩阵等分析图表部署文档与目录结构能帮助按步骤操作。已有260人学习下载项目经本地编译通过、评审分95分以上适合希望直接落地或二次开发的网络安全学习者。1. 入侵检测模型的第一个敌人是数据失衡欠采样随机森林为什么能打基于Python欠采样-随机森林的入侵检测模型我第一次在真实流量日志上跑通时最深的感受是随机森林本身不会自动处理好“正常多、攻击少”的数据。网络流量里正常访问占了绝大多数攻击样本不仅少还分散在好几种攻击类型里直接训练模型很快就学会“手一滑全判正常”。欠采样把多数类样本压到和少数类同一量级随机森林再通过多棵树投票去拟合少数类的边界这套组合在毕业设计和中小规模工程原型里都相当能打。这篇文章按项目推进顺序讲先读懂数据为什么失衡再给出基于 Python 的可复现代码最后讲评估、调参和几个常见的翻车现场。2. 入侵检测数据天然不平衡欠采样前先把数据和指标读透2.1 正常流量占多数攻击样本少且类型不均一个典型的流量数据集长什么样以公开的 NSL-KDD 这类流量数据集为例单条记录描述一次网络连接包括协议类型、服务类型、连接状态、时长、收发字节数、登录失败次数等几十个字段。标签则标记这条连接是正常行为还是某类攻击。把这些记录按类别数一遍就能看到非常典型的“长尾”正常样本可能占六七成剩下的攻击样本里DoS 这类量大且容易构造的攻击占了大头Probe扫描探测类其次而 R2L远程登录、U2R提权这类攻击样本往往少到只有几条、几十条。频次差距达到几十倍并不罕见。这种分布会给分类器带来一个很具体的麻烦决策树在切分节点的时候追求的是整体不纯度下降最快。多数类样本量大贡献的错误也多模型自然倾向于把边界往少数类那边推最终学成一个“把绝大多数流量判为正常”的懒模型。表面上看 accuracy 很高实际对攻击几乎没有区分能力。我一般会在建模前一小时先做一件事把标签分布画出来然后记录三个数——多数类样本数、少数类样本数、两者比值。这个比例决定了后面采样策略怎么选也是后面判断“模型有没有真的学到少数类”的参照系。2.2 欠采样 vs 过采样 vs SMOTE这个场景为什么一般首选随机欠采样处理不平衡数据有三条主流路线随机欠采样从多数类里随机抽掉一部分让两类数量接近随机过采样把少数类样本复制多份SMOTE 则在少数类样本之间插值合成新样本。三者的目的都是把决策边界拉回合理位置但在入侵检测场景里我一般会优先试欠采样理由很实际。第一多数类样本量通常足够大随机丢掉一部分不会让模型“没东西可学”。第二少数类样本虽然有但数量往往也就几千条这时用过采样灌重复样本、或用 SMOTE 造合成样本收益不大还可能把噪声放大。第三欠采样把训练集变小随机森林这种集成模型训练起来明显更快迭代调参体验好很多。欠采样的代价也很明确它会丢弃大量多数类信息单次采样得到的模型可能对多数类“记忆不全”。常见的补救办法有两种一种是跑多次采样、训练多个模型再投票类似 EasyEnsemble 的思路另一种是把采样比例设得保守一点比如让多数类数量保持在少数类的一到两倍而不是强行 1:1。入侵检测场景里正常流量的形态本来就很多样削得太狠反而会让模型漏掉一些少见但合法的流量模式。如果少数类样本少到只有几十条纯欠采样会让训练集太小这时我才会考虑 SMOTE或者把欠采样和 SMOTE 串起来用但多数情况第一次建模先用最简单的随机欠采样把基线打通再谈花式采样这个顺序能省下很多调试时间。三种方法在入侵检测里的取舍可以按下表对号入座方法做法优点缺点适用情况随机欠采样随机删除多数类样本训练集小、速度快、实现简单可能丢失多数类信息多数类样本量大随机过采样复制少数类样本不丢失多数类信息容易过拟合训练集变大少数类过少SMOTE少数类之间插值生成新样本样本更多样可能生成不真实的流量模式聚类特征明显时这张表解释了为什么很多公开工程方案里随机欠采样都是第一个 baseline。它不聪明但稳能让你先把训练、评估、部署这一整条链路跑通。2.3 指标选错会白干recall、precision、F1 在入侵检测里的真实含义很多第一次做入侵检测的同学习惯直接看 accuracy这是最容易吃亏的地方。在 97% 正常、3% 攻击的数据上模型只要全判正常accuracy 就已经是 97%但这个模型在真实环境里等于没做。对入侵检测来说真正该盯的是少数类攻击上的查全率和查准率。recall 衡量的是“所有攻击里我抓住了多少”。攻击类召回率低说明漏报严重攻击都绕过去了这是入侵检测最不能接受的结果。precision 衡量的是“我报警的那些里有多少是真攻击”。精确率低说明误报多安全人员一天到晚处理假告警很快就不信任这个系统了。F1 是两者的调和平均单独看 accuracy 没有意义。指标关心的问题在入侵检测里的偏好accuracy总共判对了多少会被多数类带偏慎用recall攻击里抓住多少尽量高漏报代价更大precision告警里对多少尽量高防止告警疲劳F1recall 和 precision 的综合作为调参的主要优化目标代码层面我一般直接打印 classification_report重点看攻击类别那一行的 recall 和 F1不要只看最后那个 macro avg。如果攻击类别的 recall 低于 0.5说明模型基本还在“靠猜”后面采样和调参都还有很大空间。3. 用 Python 复现欠采样-随机森林入侵检测模型从数据到首轮预测3.1 数据读取与预处理把类别型特征换算成树模型能消化的一列数字这里以 NSL-KDD 的 KDDTrain 文件为例。文件是 CSV每行一条流量记录特征是前 41 列最后一列是标签。按第 2 章说的办法先确认类别分布然后把标签处理成二分类攻击样本标记为 1正常样本标记为 0后续采样和评估都围绕“异常 vs 正常”展开。原始标签二分类标记含义normal0正常流量dos / probe / r2l / u2r 等1攻击流量下列代码展示读取、预处理和训练测试切分import pandas as pd from sklearn.model_selection import train_test_split # 读取 CSV 数据假设最后一列是标签 df pd.read_csv(KDDTrain.csv) X df.iloc[:, :-1] y df.iloc[:, -1] # 把标签统一成二分类normal 记为 0其余攻击记为 1 y_bin (y ! normal).astype(int) print(标签分布) print(y_bin.value_counts()) # 对类别特征做独热编码这里是 NSL-KDD 里的几个常见列 cat_cols [protocol_type, service, flag] X pd.get_dummies(X, columnscat_cols, drop_firstFalse) # 先切分欠采样只允许在训练集内部做 X_train, X_test, y_train, y_test train_test_split( X, y_bin, test_size0.2, random_state42 ) print(训练集特征维度, X_train.shape)逻辑说明先把多类别标签映射成二分类以便聚焦“攻击 vs 正常”再用 get_dummies 把协议类型、服务、连接状态等文本列转成独热码。drop_firstFalse 表示不丢弃第一列类别后续列对齐更省心。这里有个隐藏坑pd.get_dummies 如果遇到测试集里出现训练集没见过的类别会生成不同的列导致模型推理时维度对不上。更稳妥的方案是用 OneHotEncoder(handle_unknownignore)先 fit 训练集再 transform 测试集。数据集结构简单时 get_dummies 够用但请留意这个边界。3.2 随机欠采样的核心实现RandomUnderSampler 与抽样比例用 imbalanced-learn 库的 RandomUnderSampler 实现随机欠采样它比手写 sample 更稳接口也直接兼容 sklearn 生态。代码如下from imblearn.under_sampling import RandomUnderSampler # 默认 sampling_strategyauto把多数类采样到与少数类数量一致 rus RandomUnderSampler(sampling_strategyauto, random_state42) X_res, y_res rus.fit_resample(X_train, y_train) # 打印采样前后的类别数量 print(采样前) print(y_train.value_counts().to_dict()) print(采样后) print(pd.Series(y_res).value_counts().to_dict())参数说明sampling_strategy 可以填 auto、浮点数或字典。auto 是 1:1适合先跑通链路如果想保留更多正常流量形态改成 1.5 或 2.0含义是采样后多数类数量是少数类的 1.5 或 2 倍模型不容易丢掉正常流形的多样性。random_state 必须固定否则后续对结果复现会非常头疼。3.3 训练随机森林n_estimators、max_depth、class_weight 的搭配在采样后的数据上训练随机森林。随机森林对高维特征和无量纲数据比较宽容但有三组参数直接影响最终效果n_estimators 控制树的数量太少容易抖太多训练变慢且收益递减max_depth 控制树深不限制的话单棵树容易把欠采样后的少量样本背得滚瓜烂熟造成过拟合class_weight 是最后一层保险让训练在类的层面继续纠偏。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators100, max_depth12, min_samples_leaf2, class_weightbalanced, n_jobs-1, random_state42, ) rf.fit(X_res, y_res)参数说明max_depth12 是比较常见的起点树深不超过这个值时模型不容易记住单个攻击样本的琐碎细节。min_samples_leaf2 要求叶子节点至少两个样本作用类似。class_weightbalanced 会根据类别频率自动加权虽然采样后两类数量一致保留它对真实场景中的小范围类别偏移仍有帮助。n_jobs-1 让多核并行训练训练速度快不少。3.4 第一时间看混淆矩阵判断模型“漏没漏”而不是“准不准”训练结束后先打印混淆矩阵和分类报告比看 accuracy 实在得多。混淆矩阵能直接告诉你漏报和误报各占多少真正类 1 但预测成 0 的数量是漏报真正类 0 但预测成 1 的数量是误报。from sklearn.metrics import confusion_matrix, classification_report y_pred rf.predict(X_test) print(混淆矩阵行真实列预测) print(confusion_matrix(y_test, y_pred)) print(\n分类报告) print(classification_report(y_test, y_pred, digits3))逻辑说明这段代码在测试集上做预测再输出标准评估报告。如果分类报告里 1 类的 recall 明显偏低说明欠采样没有帮模型抓住攻击模式下一步要去调随机森林参数或检查特征里是否缺少有区分度的字段。到这一步一个最小可用的欠采样-随机森林入侵检测模型已经跑通下面两章把它升级成能说明白、能上线验证的版本。4. 评估与调参让模型在少数类攻击上真正可用4.1 交叉验证里最容易犯的错欠采样发生在整个数据集上第 3 章把欠采样放在训练集内部是最基本的正确姿势。但如果实际操作时先对全量数据 fit_resample再切分训练集和测试集就会造成数据泄漏测试集也被动过不再是真实流量分布评估结果会虚高。正确做法是把欠采样作为训练流程的一部分只在当前训练折内部执行。imbalanced-learn 提供了专门的处理方式配合交叉验证代码很干净from imblearn.pipeline import Pipeline as ImbPipeline from sklearn.model_selection import cross_val_score # 定义包含欠采样和随机森林的流水线 pipeline ImbPipeline([ (sampling, RandomUnderSampler(random_state42)), (classifier, RandomForestClassifier( n_estimators80, max_depth12, random_state42 )), ]) # 对原始特征做交叉验证欠采样只发生在每一折的训练部分 scores cross_val_score(pipeline, X, y_bin, cv5, scoringrecall) print(5折recall, scores) print(平均recall, scores.mean())逻辑说明cross_val_score 每一步都会复制一份数据在训练部分先做欠采样再训练分类器测试部分保持原始分布。不用 ImbPipeline 而用普通 sklearn Pipeline 会发生什么普通 Pipeline 不会阻止采样器在交叉验证里被错误调用但 imblearn 的 Pipeline 对采样器的 fit_resample 语义支持更完整所以这里明确用 ImbPipeline。4.2 随机森林调参顺序先限制复杂度再搜 n_estimators随机森林参数很多没必要一上来就穷举。我一般按三个层次调先把 max_depth 和 min_samples_leaf 限制住让模型不过拟合然后搜索 n_estimators最后再回来调 class_weight 和采样比例。下面的网格搜索沿用 4.1 的 Pipeline把采样比例也放进参数空间from sklearn.model_selection import GridSearchCV param_grid { sampling__sampling_strategy: [1.0, 1.5], classifier__max_depth: [8, 12, 16], classifier__min_samples_leaf: [1, 2, 4], } grid GridSearchCV( pipeline, param_grid, cv3, scoringf1, n_jobs-1, ) grid.fit(X, y_bin) print(最优参数, grid.best_params_) print(最优F1, grid.best_score_)这里没有把 n_estimators 放进去因为随机森林在 100 棵之后增益很小先固定 80 或 100 让调参过程变快。注意 scoring 不要用 accuracy我用 f1 是因为它同时约束漏报和误报。如果现场对误报更敏感可以换成 scoringprecision再对比两组结果的召回率变化。4.3 用概率阈值代替默认 0.5把漏报的那批攻击找回来随机森林输出的概率值并不一定要拿 0.5 当门槛。在不平衡数据上模型给出的攻击概率往往整体偏低默认阈值可能把所有样本都挡在报警线外。这时可以画出 precision-recall 曲线找一个让 F1 最大的阈值。import numpy as np from sklearn.metrics import precision_recall_curve # 拿到测试集上预测为攻击类的概率 proba grid.best_estimator_.predict_proba(X_test)[:, 1] precision, recall, thresholds precision_recall_curve(y_test, proba) # precision_recall_curve 返回的 precision/recall 比 thresholds 多一个元素 f1_scores 2 * precision[:-1] * recall[:-1] / np.maximum( precision[:-1] recall[:-1], 1e-9 ) best_idx np.argmax(f1_scores) best_threshold thresholds[best_idx] # 用新阈值重新判一遍 y_pred_adj (proba best_threshold).astype(int) print(最优阈值, round(best_threshold, 3))逻辑说明先取模型对“攻击”类的概率再在阈值序列上逐个计算 F1找到最大值对应的阈值。索引处理要小心precision_recall_curve 返回的 precision 和 recall 长度比 thresholds 多一个所以计算 f1_scores 时必须切片去掉最后一个元素否则阈值对不上。少数类样本极少时这个技巧算出的阈值可能直接变成 0含义是“所有样本都报警”此时不能机械套用要回到业务上限制最小 precision。5. 避坑欠采样-随机森林建模的五个翻车现场5.1 现象欠采样后训练指标很好一到测试集或线上误报暴涨原因欠采样把多数类削得太狠模型对正常流量的形态记忆不足把很多正常但特征偏极端的流量判成了攻击。更隐蔽的原因是训练集和测试集来自不同时间段流量分布不完全一致。解决把 sampling_strategy 从 1.0 改成 1.5 或 2.0保留更多正常样本同时检查训练和测试的时间范围是否重叠。是时间跨度问题的话按时间切分验证而非随机切分。误报特别严重时在 4.2 的网格里加一个 precision 约束重新选参。5.2 现象整体 accuracy 很高攻击类别检出数却是 0原因模型被多数类彻底带偏少数类在预测时全部被压到正常类别。通常发生在完全没做采样、也没设 class_weight 的情况下或者用了默认 0.5 阈值而训练数据又极度不平衡。解决补上 RandomUnderSampler 和 class_weightbalanced训练后用 4.3 的概率阈值方法调门槛。如果加了采样还是 0先检查 y_test 里是否真的存在正类样本有时候标签预处理阶段就把攻击样本全过滤掉了。5.3 现象把欠采样套到全量数据再切训练测试结果虚高原因这是典型的数据泄漏。先对整个数据集做 fit_resample再切分训练测试测试集也被动过真实流量分布被污染评估结果自然好看但线上没这个待遇。解决严格保持测试集的原始分布欠采样只允许发生在训练流程内部。交叉验证统一用 ImbPipeline不要手写“先采样再切分”的流程。这个错误在代码 review 里几乎一抓一个准。5.4 现象同一份数据两次训练结果每次跑出来都差一截原因随机森林自身有随机性RandomUnderSampler 也有随机性两处都没有固定随机种子。少数类样本本来就少随机因素被放大结果自然来回跳。解决在 RandomUnderSampler 和 RandomForestClassifier 里都传 random_state42数据切分也固定 random_state。正式出指标时跑 5 次取均值和方差拿一次结果当结论很容易被随机涨落误导在写毕业设计验收结论时尤其要避免。5.5 现象特征重要性却被少数高基数特征带偏原因随机森林计算特征重要性时受候选切分数量的影响取值特别多的列更容易被选去分裂数值范围大不代表业务上更重要。入侵检测里很容易遇到类似会话 ID、时间戳这类列排在最前面业务上却说不通的情况。解决把高基数或纯标识类特征做分箱或直接剔除再对比一次重要性排序。不要只依赖 feature_importances_条件允许的话用 SHAP 辅助验证看具体某个特征值在什么范围内会推动模型判为攻击。6. 上线前的最后一道把关用特征重要性和置信度门槛保住告警质量6.1 用特征重要性验证模型学到的是不是真特征在把欠采样-随机森林模型交付之前我习惯先看一眼特征重要性排序和预期的攻击特征做比对。排序靠前的如果出现连接时长、目标端口、连接状态这类和网络行为强相关的字段模型学的东西基本可信如果排在最前的是各种编号列说明数据清洗偷了懒模型学到了和 ID 相关的映射需要重新做特征工程。import pandas as pd importance pd.Series( rf.feature_importances_, indexX_train.columns ).sort_values(ascendingFalse) print(importance.head(10))6.2 把预测概率分三档减少无用告警线上部署时我没有直接把 predict 的 0/1 当最终结果而是把概率值分档低置信度样本自动进入人工复核。这样既保留自动告警的实时性又把误报控制在一定范围内。def decide(prob, high0.85, low0.3): if prob high: return high_confidence_flag if prob low: return manual_review return normal阈值怎么定先看 4.3 算出的最优阈值如果它在 0.5 附近说明模型比较自信如果最优阈值只有 0.2 左右说明模型输出整体偏保守high 阈值设在 0.8 以上更合理low 阈值再往 0.2 附近放一放。三档规则的意义是高置信告警直接进工单系统低置信告警进人工复核队列normal 直接放行这一层规则能让告警量肉眼可见地下降。我习惯在上线后每天记录三档的分布数量变化当 high_confidence_flag 的样本量开始连续下滑时说明流量环境在变化该准备重训数据了。希望这些落地的细节和踩过的坑能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑