ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

欠采样与随机森林:解决入侵检测不平衡数据实战指南

欠采样与随机森林:解决入侵检测不平衡数据实战指南 简介一套基于Python的欠采样-随机森林入侵检测模型毕业设计项目面向计算机、网络安全专业学生及需要快速完成相关课题的研究者。项目源码均在本地编译通过、可直接运行评审分达95分以上且经过助教审定难度适中适合用于课程设计或论文实验。压缩包内含172个文件其中包含39个Python脚本、31个CSV数据文件、30个pyc编译文件、16个pkl模型文件以及用于结果可视化的HTML/CSS/JS页面和SQLite数据库整体大小约53.66MB目录结构清晰便于按模块检索。资源附带部署文档和说明文本从环境配置到底层建模逻辑均有覆盖可帮助快速搭建运行环境理解欠采样与随机森林在入侵检测中的实际应用内置的KDD数据集与可视化页面能支撑从训练、测试到结果展示的全流程复现。目前已有260人浏览学习适合需要获取完整可运行方案并提高开发效率的毕设人群。1. 入侵检测中的不平衡问题欠采样和随机森林为什么常被组合网络流量数据天然是不平衡的正常请求可能占 85% 以上而 U2R、R2L 这类攻击在公开数据集里只有几十条。直接把这样的数据喂给分类器模型会偏向多数类预测准确率可以做到 99%但真正的攻击流量几乎全部漏报。欠采样加随机森林的组合是解决这个问题最直接的路线之一欠采样在数据层面压缩多数类样本随机森林作为集成分类器对处理后的数据有较好的鲁棒性和可解释性。这个方案适合准备毕业设计、做安全分析基线实验以及想快速验证“不平衡数据集成学习”路线的工程师。下面按一条完整可行的路径展开从数据处理到欠采样策略再到随机森林的参数设置和部署验证。2. 数据准备与特征处理从原始流量到稳定训练集2.1 数据集选型为什么用 NSL-KDD 而不是 KDDCUP99早期论文基本都在 KDDCUP99 上做实验但这个数据集有两个被反复提到的毛病训练集里大量重复记录约 78% 的记录是冗余的测试集里还包含训练集没出现过的攻击类型导致有的方案在测试集上表现好靠的是记忆而非泛化。NSL-KDD 是 KDDCUP99 的去冗余版本剔除了重复记录重新划分训练集和测试集使每个难度级别的样本都保留一定数量。毕业设计里使用 NSL-KDD 作为公开数据集审阅老师基本都能接受结果也更容易和其他论文做横向对比。数据可以从标准渠道下载到两个文件KDDTrain.txt 和 KDDTest.txt。训练集带标签测试集也带标签方便做最终评估。每条记录是 41 个特征加一个类别标签。41 个特征里有连续型数值duration、src_bytes、dst_bytes有离散型符号protocol_type、service、flag还有一部分是二进制标志位land、logged_in 等。固定特征顺序是后面所有步骤的前提建议在读入数据时就显式指定列名不要依赖默认索引。2.2 类别分布检查与统计把数据读进来后第一件事是看标签分布确认攻击样本到底少到什么程度。这一步看起来简单但能避免后面好几个坑。比如某些攻击类型样本数是个位数这类样本在欠采样时很容易被清掉导致模型根本没“见过”它们。用 pandas 读入后按标签分组统计import pandas as pd cols [duration,protocol_type,service,flag,src_bytes, dst_bytes,land,wrong_fragment,urgent,hot] # 实际共有41个特征名需要和数据集列顺序一一对应这里只展示前10个 # df pd.read_csv(KDDTrain.txt, headerNone, namescols_full) label_counts df[label].value_counts() print(label_counts)这段代码里cols_full必须是完整的 41 个特征名列表加上最后的label列。执行后通常会看到normal最多neptune、smurf这类 DoS 攻击也有不少而u2r或某些 R2L 类别只有几十条甚至个位数。有了这个统计才能决定后续欠采样策略是否需要做类别分组。如果直接用整体欠采样把多数类压到和最少类一样的数量那些本身只有 20 条的类别会导致整个训练集只剩几百条样本模型容量不够效果会明显变差。2.3 符号特征编码与数值特征标准化机器学习模型读不了字符串protocol_type、service、flag这三个离散特征必须先转成数值。常见做法是直接对这几个列做LabelEncoder编码成整数然后用StandardScaler对数值列做标准化。有人会问为什么不对离散特征做 One-Hotservice字段在 NSL-KDD 里有 60 多个取值One-Hot 之后特征维度会膨胀到 120 个以上而随机森林对特征缩放不敏感但对高维稀疏特征会比较吃力。做整数编码特征数量不膨胀树模型按阈值切分时并不受编码整数大小关系的影响。我这里用scikit-learn的ColumnTransformer把编码、标准化、标签二值化封装成一个流水线。标签部分做二分类映射normal映射为 0其余攻击类型统一映射为 1。如果毕业设计要求做多分类可以保留原始标签继续往下走但基线模型通常从二分类开始实验变量更干净不平衡效果也看得更直观。from sklearn.preprocessing import LabelEncoder lb LabelEncoder() y_binary lb.fit_transform( df[label].apply(lambda x: normal if x normal else attack) ) print(pd.Series(y_binary).value_counts())apply里的 lambda 把原来的几十种类别压缩成两类这是入侵检测二分类最常见的标签处理方式。之后所有模型的训练目标都是这个y_binary测试集也需要用同一个lb做相同映射。如果你在训练集上用LabelEncoder编码符号特征测试集直接用训练好的编码器做transform一定不要重新fit否则整数编码的映射关系可能对不上模型输入分布就乱了。2.4 训练集/测试集划分与“先切再采”原则欠采样操作必须发生在训练集划分之后这是整个流程里最容易出错也最影响结果评估的一个顺序问题。如果你先把全部数据做了欠采样再划分训练集和测试集测试集里多数类已经被人为删掉一部分不再代表真实网络流量的类别比例最终评测出来的召回率、F1 都会虚高。正确流程是先用train_test_split把原始数据切成两份测试集原样保存只在训练集分支里做欠采样from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_all, y_binary, test_size0.3, stratifyy_binary, random_state42 )参数里stratifyy_binary表示按类别比例分层抽样测试集和训练集中正常与攻击的比例基本保持一致。random_state固定下来多次运行结果可复现论文里可以把数据集划分方式写清楚。做完这一步后面所有欠采样和随机森林训练都只操作X_train和y_trainX_test、y_test保持原始分布直到最终评估时才拿出来用。3. 欠采样实现三种主流策略的代码与对比3.1 随机欠采样简单但有效的基线随机欠采样的思路是从多数类样本里随机抽取和少数类数量相当的子集和少数类拼在一起形成新的训练集。imbalanced-learn库提供RandomUnderSampler用法非常直接from imblearn.under_sampling import RandomUnderSampler rus RandomUnderSampler(sampling_strategyauto, random_state42) X_res, y_res rus.fit_resample(X_train, y_train) print(X_res.shape, y_res.shape)sampling_strategyauto会把所有多数类类别降到和最少类相同。random_state固定随机种子保证欠采样后的样本集合可复现。随机欠采样的优点是快、可控、不引入额外计算缺点也明显——多数类信息大量丢失随机丢弃时可能把对识别边界重要的样本丢掉。如果多数类和少数类在特征空间里本来就有重叠丢掉的样本恰好落在决策边界附近模型泛化能力会受影响。所以随机欠采样适合作为基线先跑通一整条流程再判断是否需要更精细的采样策略。3.2 NearMiss基于距离的欠采样NearMiss 系列是随机欠采样的改进版它通过样本之间的距离来选择要保留的多数类样本。imbalanced-learn实现了三种变体变体选择逻辑适用场景NearMiss-1保留距离每个少数类样本最近的多数类样本多数类与少数类边界重叠大时效果较好NearMiss-2保留距离每个少数类样本最远的多数类样本更在意多数类内部结构时使用保留与少数类差异更大的多数类样本NearMiss-3为每个少数类保留固定数量的最近多数类样本少数类样本极小时能保证每个少数类都有对应多数类参考用法上NearMiss只需要替换采样器。有两个参数值得注意n_neighbors控制邻居数量默认是 3数据量较大时可以调大version选择变体1对应 NearMiss-12和3分别对应另外两种。距离计算默认用欧氏距离数据没有标准化时算出来的距离会失真所以欠采样一定要放在标准化之后。NearMiss 的计算量比随机欠采样大很多NSL-KDD 训练集有 12 万条左右记录我一般先跑一次随机欠采样的快速基线确认数据规模可接受后再跑 NearMiss避免一次实验等太久。3.3 Tomek Link清洗边界样本Tomek Link 的思路是如果两个不同类别的样本互为最近邻它们很可能分布在决策边界附近或者相互重叠这类样本会让分类器在边界上犹豫不决。Tomek Links 方法找到所有这样的最近邻对然后删除其中属于多数类的样本或者在组合策略里把两个都删掉。imbalanced-learn里对应TomekLinks。它不直接做“降低到特定比例”的采样而是作为清洗工具和随机欠采样配合使用。我常用的做法是先用RandomUnderSampler降低多数类比例再用TomekLinks清洗掉边界上的噪声样本两步组合成一条流水线。注意TomekLinks默认sampling_strategyauto只会移除多数类样本不会动少数类。少数类本来就少边界上的少数类样本如果被清洗掉类别信息损失会更大所以让多数类承担清理成本更合理。3.4 三种策略怎么选选择不只看效果也要看复现成本。随机欠采样一步到位适合做基线NearMiss 效果好但速度慢适合放到最终方案里TomekLinks 单独使用只是轻度清洗必须和其它采样器组合。下面这段代码把“随机欠采样 Tomek Links”组合成一个流水线from imblearn.pipeline import make_pipeline from imblearn.under_sampling import RandomUnderSampler, TomekLinks pipe make_pipeline( RandomUnderSampler(sampling_strategyauto, random_state42), TomekLinks(sampling_strategyauto) ) X_final, y_final pipe.fit_resample(X_train, y_train)make_pipeline在这里有两个好处一是把两步采样串成一个可复用对象调参时不用分开操作二是imblearn.pipeline对交叉验证的处理比手动拼装更安全。两个采样器的顺序也有讲究先做随机欠采样把类别比例拉平再做 Tomek Links 清理边界这样边界样本的最近邻计算不会因为多数类数量过大而变得很慢。4. 随机森林模型训练与调参参数怎么设、坑在哪里4.1 先跑一个默认参数的基线随机森林的实现以scikit-learn的RandomForestClassifier最常用。先不调参直接训练一个n_estimators150的模型确认整条链路能跑通from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier(n_estimators150, random_state42, n_jobs-1) rf.fit(X_final, y_final)n_estimators表示森林里决策树的数量。先从 150 开始太少比如 10 或 20方差大太多超过 500收益衰减明显训练时间线性增长。random_state固定下来确保复现结果。n_jobs-1让随机森林并行训练在 Windows 的 Jupyter 环境里偶尔会踩到多进程异常如果遇到就改成n_jobs4或去掉这个参数。4.2 必调的四个参数随机森林最值得手动调的是max_depth、min_samples_split、min_samples_leaf、max_features。这组参数直接控制单棵树的复杂度和森林整体的多样性。max_depth默认是 None树可以无限生长。欠采样后的数据集规模不大树太深容易在少数类上学到噪声设成 10 到 30 之间通常能在测试集上看到明显改善。min_samples_split控制内部节点继续分裂的最小样本数默认是 2。数据量不大时调到 5 到 10 可以抑制过拟合。min_samples_leaf控制叶子节点最少样本数默认是 1。欠采样后少数类样本本来就不多设得太大比如超过 10会让叶子覆盖过多样本类别分辨率下降一般取 1 到 5 比较合适。max_features控制每次分裂时考虑的特征数。默认的sqrt对分类任务已经不错NSL-KDD 的特征之间有相关性把它固定为平方根或0.6附近能增加树之间的差异性。这四个参数不是独立起作用的。比如max_depth设得较深时min_samples_leaf也要相应调大否则深层树的叶子会切得太碎max_features太小会让单棵树性能变差太大又会让树之间相关性升高。调参时先固定random_state每次只动一个变量记录训练集 F1 和测试集召回率的变化比一次改多个参数更容易定位问题。4.3 用交叉验证找参数注意采样器不能一起泄露在入侵检测里“数据泄露”是最容易被审稿人挑出来的问题之一。欠采样必须只应用在训练集上验证集或测试集要保持原始分布。如果你直接写RandomUnderSampler().fit_resample(X, y)拿到采样后的数据再去做交叉验证那么每一折的验证集其实已经被多数类样本污染过效果会被高估。正确做法是把采样器和随机森林一起放进imblearn.pipelinefrom sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier from imblearn.pipeline import make_pipeline from imblearn.under_sampling import RandomUnderSampler pipeline make_pipeline( RandomUnderSampler(random_state42), RandomForestClassifier(n_estimators150, max_depth15, random_state42, n_jobs-1) ) scores cross_val_score(pipeline, X_train, y_train, cv5, scoringrecall) print(scores.mean())imblearn.pipeline在每折交叉验证时会把采样器重新拟合在训练折上验证折不参与采样从而避免数据泄露。评分用recall而不是accuracy因为在不平衡测试集上准确率对漏报非常不敏感。入侵检测更关心攻击流量能否被抓到召回率才直接反映这个目标。如果要做更充分的调参可以把n_estimators、max_depth、min_samples_leaf放进GridSearchCV或RandomizedSearchCV里跑网格别铺得太大否则一次调参可能跑半小时以上。我一般先用RandomizedSearchCV在较粗的网格上跑 30 到 50 组组合确定趋势后再微调最敏感的参数。4.4 类别权重与欠采样的关系class_weight是随机森林自带的一个处理不平衡的选项它给少数类样本分配更高权重。很多人在用了欠采样之后又把class_weight设成balanced这等于做了双重类别重平衡反而可能让模型过度偏向少数类在测试集上拉高假阳率。我的习惯是用了欠采样就把class_weight保持默认的None只有当数据量实在太小、欠采样后少数类样本不足百条时才考虑用class_weightbalanced补齐。这两者是交换关系不是叠加关系同时使用会改变模型输出的概率分布后续再做阈值调整时不容易解释。5. 模型持久化与部署验证从pkl到可用的检测接口5.1 用 Pipeline 把预处理和模型打包部署阶段最怕的是训练时的预处理和预测时不一致。特征列顺序、编码映射、标准化参数任何一个对不上预测结果都会偏移。解决办法是把特征编码、标准化、欠采样器和随机森林整个放进一个 Pipeline训练完成后直接用joblib.dump保存整个对象import joblib from imblearn.pipeline import make_pipeline from sklearn.ensemble import RandomForestClassifier from imblearn.under_sampling import RandomUnderSampler final_pipe make_pipeline( # ColumnTransformer(...) # 符号特征编码 数值标准化按训练时的配置 RandomUnderSampler(random_state42), RandomForestClassifier(n_estimators180, max_depth18, n_jobs-1) ) final_pipe.fit(X_train, y_train) joblib.dump(final_pipe, ids_model.pkl)注意这里把RandomUnderSampler放在了 Pipeline 内部。预测时predict会先执行前面的编码和标准化再进入采样器。欠采样器在预测阶段只会调用transform而不是fit_resample所以输入的每条样本都会原样进入随机森林不会因为采样逻辑改变预测结果。保存下来的ids_model.pkl里面已经包含了训练时的特征列顺序、编码器映射和模型权重部署时只需要加载这一个文件。5.2 单条流量数据预测与特征对齐加载模型后输入一条新流量的特征进行预测。关键是特征顺序必须和训练时完全一致最好调用方按固定字段顺序传参。这里用一个示例数据展示完整过程import joblib import pandas as pd loaded joblib.load(ids_model.pkl) # 按41个特征名构造一条原始记录协议类型等离散特征保持字符串 raw_record { duration: 0, protocol_type: tcp, service: http, flag: SF, src_bytes: 181, dst_bytes: 5450, } # 其余特征默认填0实际部署时应从流量解析结果读取完整字段 record_df pd.DataFrame([raw_record], columnscols_full[:-1]) pred loaded.predict(record_df) prob loaded.predict_proba(record_df)[0][1] print(f模型输出: {pred[0]} 攻击概率: {prob:.4f})predict_proba返回的是二维数组[0][1]表示这条样本属于攻击类的概率。阈值不一定要用默认的 0.5你可以根据部署环境的告警容忍度调整。比如把阈值降到 0.3检测率提高、误报变多调到 0.7误报减少但漏报风险增大。这个阈值可以在测试集上画出 ROC 曲线后根据期望的假阳率选定。5.3 验证模型没“白练”用独立测试集复盘NSL-KDD 提供的KDDTest.txt是独立测试集里面有些攻击类型是训练时没见过的正好用来检验泛化能力。在测试集上至少看四个指标召回率、精确率、F1 和 ROC-AUCfrom sklearn.metrics import classification_report, roc_auc_score y_pred loaded.predict(X_test) y_prob loaded.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred, target_names[normal,attack])) print(fROC-AUC: {roc_auc_score(y_test, y_prob):.4f})classification_report会同时给出每个类别的召回率和精确率先看attack这一行的 recall再看normal的 precision。如果 attack 的召回率低于 90%说明检测能力不够回到第 4 章调整max_depth或采样策略重点观察提升的是哪一类攻击样本的召回。调试过程建议控制在三轮以内每轮固定random_state并记录结果否则换一次随机种子结果变了最后很难解释清楚实验结论。本文还有配套的精品资源点击获取
返回列表