☰
基于欠采样与随机森林的入侵检测模型实战解析
2026/10/18 10:03:46 网站建设 项目流程

简介:面向毕业设计场景,这份Python入侵检测项目完整方案主要解决网络流量数据不平衡与恶意行为识别问题,适合信息安全、计算机相关专业学生作为课程设计或毕业论文的参考范例。包内含源码、预训练模型、部署文档及全部数据资料,本地编译可运行,评审分达95分以上。压缩包共172个文件、约53.66MB,核心包括39个Python脚本、31个CSV数据表、16个pkl模型文件、4个KDD ARFF数据集,另有HTML/CSS/JS可视化前端与SQLite数据库等,覆盖数据预处理、欠采样、随机森林训练评估及页面展示的完整链路。用户可通过ARFF标准数据集快速复现实验,也可直接加载pkl模型完成推理演示。项目难度适中,配合部署文档与全部数据资料,便于对照阅读和二次开发;目前已有260人学习下载,适合需要完成开题、中期检查、答辩演示或深入理解入侵检测建模流程的同学。

1. 入侵检测为什么需要欠采样-随机森林:准确率99%也可能全盘翻车

一个常见的翻车现场:训练集准确率99%,答辩PPT放出来很好看,但把测试集里的攻击流量单独拎出来一算,检测率只有30%,R2L这类攻击几乎全漏。原因就藏在数据里——入侵检测的流量数据天然不平衡,正常样本占绝大多数,攻击样本是小部分。直接把不平衡数据丢给随机森林,模型学到的几乎全是“正常流量长什么样”。基于Python的欠采样-随机森林入侵检测模型,核心思路是先做多数类欠采样,再用平衡数据训练随机森林,换来的是真正有价值的检测率,而不是虚高的准确率。这套方案对毕业设计来说,数据、源码、模型和部署文档都有成熟的落地路径,适合想快速跑通、又希望实验结果能在答辩时站得住的人。

2. 数据准备:从原始流量到可训练的平衡数据集

2.1 入侵检测数据集怎么选:以NSL-KDD为基础的二分类与多分类取舍

做入侵检测,第一步不是建模,而是选数据。公开的流量数据集里,NSL-KDD 是最常用的基准之一,训练集约12.6万条样本,每条包含41个特征和1个标签,标签分为 normal 以及 DoS、Probe、R2L、U2R 四类攻击。分布上,normal 约6.7万、DoS 约4.6万、Probe 约1.2万,而 R2L 只有约1000条,U2R 甚至只有52条——这就是不平衡的根源。

简单做一个二分类(normal vs attack)是上手最快的选择,因为只需要把四类攻击合并成一类,欠采样逻辑也最简单。多分类(normal、DoS、Probe、R2L、U2R)能拿更多分析内容,但 R2L 和 U2R 样本太少,即使欠采样后,随机森林也很难学到足够的判别边界。我的建议是:如果目标是先跑通链路,用二分类;如果论文需要亮点,再做多分类对比实验。两份数据可以共用一套预处理代码,后续切换成本很低。

另外要注意,这套数据集的测试集并不完全来自训练集的同分布抽样,而是刻意包含了一些训练集里没出现过的攻击变种。也就是说,模型在测试集上表现差一点,不一定是你的代码有问题,可能是数据本身就设计了泛化难度。这一点在论文里可以大方写出来,反而是个分析点。

2.2 欠采样实现:随机欠采样与 Tomek Links 的取舍

欠采样的目标很简单:把多数类样本抽到与少数类相当的数量,让分类器不再“偏科”。最常见的是随机欠采样——从多数类里无放回随机抽取,达到目标数量。它的优点是快、可控、可复现;缺点是可能丢掉一些有价值的多数类边界样本。Tomek Links 是另一种思路:找出两类之间互为最近邻的样本对,删除其中多数类的点,只清理边界噪声,保留整体分布。但它在六万多个正常样本上计算量很大,跑一次够你喝完一杯水。

对毕业设计来说,随机欠采样是性价比最高的方案。用 scikit-learn 的 resample 就能完成:

import pandas as pd from sklearn.utils import resample # 假设 df 已加载,label 列中 0 表示正常,1 表示攻击 df_benign = df[df['label'] == 0] df_attack = df[df['label'] == 1] # 把多数类(正常样本)采样到与攻击样本相同数量 df_benign_down = resample( df_benign, replace=False, # 无放回采样,避免重复样本干扰模型 n_samples=len(df_attack), # 目标数量 = 攻击样本数 random_state=42 # 固定随机种子,保证每次运行结果一致 ) # 合并并打乱,让训练时正负样本交错出现 df_balanced = pd.concat([df_benign_down, df_attack]).sample( frac=1, random_state=42 ).reset_index(drop=True) print(df_balanced['label'].value_counts())

这里最关键的参数是replace=False。若多数类数量少于目标采样数,无放回抽样会直接报错,所以在执行前最好加一个数量判断;如果确实需要放回,可以改成replace=True,但会造成样本重复,模型会对这部分数据过拟合。random_state=42也不是玄学——毕设论文里所有实验结果都要求可复现,不固定种子的话,每次跑出来的 F1 都可能不同,后面做对比实验时你会非常被动。

Tomek Links 可以这样理解对比:它不改变多数类的整体数量,只删除容易被混淆的边界样本,更“精细”但慢。随机欠采样则会让多数类数量锐减到少数类数量,如果二分类时攻击样本只有几万条,正常样本会被抽到只剩同样规模。这个信息量损失换来的,是训练时间大幅缩短、模型能看到均衡的类别分布。对入侵检测这个场景来说,优先用随机欠采样,如果你有多余时间,再跑一版 Tomek Links 做对比。

2.3 特征编码与归一化:类别特征和数值特征的预处理顺序

NSL-KDD 的41维特征里,protocol_type、service、flag是类别特征,其余是数值特征。一个常见误操作是拿get_dummies对类别特征做独热编码,会把维度从41推到上百维,随机森林虽然能处理,但训练速度会明显变慢。树模型本身不要求数值归一化,所以对随机森林来说,类别特征用 LabelEncoder 或者 OrdinalEncoder 就够了;这一点和神经网络、SVM 有本质区别。

预处理顺序要固定:先处理类别特征,再做数值特征清洗。否则类别编码后,原本的数值列位置会变,后面部署时特征顺序容易对不上。一个可复现的预处理片段如下:

from sklearn.preprocessing import LabelEncoder cat_cols = ['protocol_type', 'service', 'flag'] num_cols = [c for c in df.columns if c not in cat_cols + ['label']] # 类别特征编码:LabelEncoder 把字符串映射成整数 for c in cat_cols: df[c] = LabelEncoder().fit(df[c]).transform(df[c]) # 数值特征离群值裁剪:把 src_bytes 这类极大值压到 99.9% 分位 for c in num_cols: cap = df[c].quantile(0.999) df[c] = df[c].clip(upper=cap)

LabelEncoder 有个坑:它只在当前数据上 fit,一旦测试集里出现训练集没见过的类别,直接 transform 会抛异常。处理办法是合并训练集和测试集一起编码,或者用OrdinalEncoder(handle_unknown='use_encoded_value')。对树模型来说,合并后编码不会引入多少未来信息,是可行的;如果你担心数据泄漏,至少做好 try-except 兜底,别让程序在预测时才崩。

数值特征里src_bytes可能出现极端大值,比如几千万的字节数,虽然树模型对单调变换不敏感,但直接留着一个巨大的离群值会让部分树的分裂点选择不稳定。按 99.9% 分位裁剪是一种温和的解决方案;再狠一点可以直接取对数,但那样需要记录变换函数,部署时对新数据也要做同样处理,我一般优先用裁剪,因为它不需要在部署端保留额外参数。

3. 随机森林训练:模型构建、参数说明与训练脚本

3.1 为什么是随机森林:与 SVM、逻辑回归在入侵检测上的对比

入侵检测特征里相关性强的列很多,比如src_bytes和dst_bytes往往同时大、同时小。逻辑回归对特征共线性敏感,需要额外做特征选择;SVM 在六万级别的数据上训练非常慢,调参也很折腾。随机森林的好处是:它对特征缩放不敏感,能自动处理非线性关系,还自带特征重要性输出——这个输出可以直接写进论文,不用额外画图工具。

随机森林的随机性来自两个层面:每棵树用 Bagging 策略从训练集里随机放回抽样,同时每次节点分裂只考虑随机特征子集。这种设计天然抵抗特征之间的高相关性,即使有些列重复了,也不会像线性模型那样出现系数漂移。再加上预测时可以输出predict_proba概率,后面做阈值调整、部署告警都方便。这就是为什么很多毕设会把随机森林作为入侵检测的主分类器。

需要注意随机森林不是黑匣子到完全不可解释的程度。feature_importances_会告诉你哪些特征对分类贡献最大,比如dst_host_srv_count这类主机侧统计特征通常排在前列。把 Top 10 特征列一个表放进论文,比单纯贴混线矩阵更能让评委看到你的理解。

3.2 训练脚本:从加载数据到保存模型的完整链路

这里给出可以直接跑的完整训练脚本。假设你已经把上一章的预处理结果保存成data/balanced_train.csv,接下来的流程是切分、训练、评估、保存。

import pandas as pd import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix # 1. 读取平衡后的数据 df = pd.read_csv('data/balanced_train.csv') X = df.drop(columns=['label']) y = df['label'] # 2. 切分训练/测试集,按类别比例分层抽样 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y # 保证测试集里正负样本比例与训练集接近 ) # 3. 初始化随机森林,先用一组稳妥的基线参数 model = RandomForestClassifier( n_estimators=200, # 树的数量,越大越稳,但训练越慢 max_depth=None, # 不限深度,靠叶子节点约束防过拟合 min_samples_split=5, # 内部节点再划分所需最小样本数 min_samples_leaf=2, # 叶子节点最小样本数 max_features='sqrt', # 每次分裂只看 sqrt(特征数) 个特征 n_jobs=-1, # 用满所有 CPU 核心 random_state=42, # 固定随机性,保证可复现 class_weight='balanced_subsample' # 对当前采样样本的类别比例再做一次补偿 ) model.fit(X_train, y_train) # 4. 评估 y_pred = model.predict(X_test) print(classification_report(y_test, y_pred, digits=4)) print(confusion_matrix(y_test, y_pred)) # 5. 保存模型和特征列,部署时需要用它们保持特征顺序一致 import joblib joblib.dump(model, 'model/rf_undersample.pkl') joblib.dump(list(X_train.columns), 'model/feature_cols.pkl')

class_weight='balanced_subsample'这里值得多说一句。如果我们已经做了1:1欠采样,数据本身是平衡的,这个参数加不加对结果影响不大;但它的存在能在采样后仍有一定随机波动时增加一点鲁棒性。如果你跑出来的结果和我不一样,先检查随机种子是否固定,再看是否用了stratify=y。这两处不设,每次实验结果都会漂移,后面写论文表格时你会很痛苦。

3.3 参数怎么设:一组可复制、可解释的调参基线

参数调优不一定要上网格搜索。很多人的血泪经验是:随机森林在入侵检测数据上,参数稍作调整就能达到不错的水平,真正决定上限的是前面的数据预处理和类别平衡,不是后面的调参。下面这组基线可以作为起点:

参数基线值参考范围说明
n_estimators200100-500超过500后准确率提升非常缓慢,耗时却线性增长
max_depthNone10-50特征多且样本量大时,限制深度能明显加速
min_samples_split52-10越小模型越细,越容易过拟合少数类
min_samples_leaf21-10调大可以抑制过拟合,但太大会漏掉 R2L
max_features'sqrt''sqrt' / 'log2'设为 None 则退化成普通 Bagging,不建议
class_weight'balanced_subsample'None / 'balanced'欠采样后加不加都行,可做对比实验

如果你要做网格搜索,建议只对n_estimators和min_samples_leaf两个参数搜索,其他字段按基线固定。max_features='sqrt'是随机森林的默认推荐配置,对入侵检测这种约40维特征的数据,每次只看6~7个特征,树之间的相关性降低,集成效果更好。

一个常见误区是盲目追求大n_estimators。800棵树和200棵树在检测率上的差异可能不到1个百分点,但训练时间翻了四倍。论文里如果能画出“树数量 vs F1”的曲线,反而比单纯堆算力更显示你的工程判断。

4. 评估与调优:准确率会骗人,召回率和误报率才是关键

4.1 混淆矩阵与四个核心指标:检测率、误报率、精确率、F1

入侵检测场景下,最不能信的就是准确率。如果测试集里90%是正常流量,模型把全部样本判为正常就有90%准确率,但一个攻击也没抓到。所以必须分开看四个指标:检测率(也叫召回率,DR/TPR)衡量攻击样本有多少被识别出来;误报率(FAR)衡量正常流量中有多少被错判为攻击;精确率看预测为攻击的样本里真正是攻击的比例;F1 是精确率和召回率的调和平均。

毕业设计论文里,建议把二分类的混淆矩阵画出来,再按攻击类别分别算召回率。R2L 和 U2R 的召回率往往很低,你要做的不是掩盖,而是分析为什么低——它们训练样本太少,即使过采样也学不到足够模式。把这个分析写进论文,比一个虚高的准确率更有说服力。

from sklearn.metrics import classification_report, confusion_matrix # 训练完成后直接打印 print(classification_report(y_test, y_pred, digits=4)) cm = confusion_matrix(y_test, y_pred) print(cm)

classification_report会给出每一类的精确率、召回率、F1,以及一个加权平均值。做二分类时,你要重点看攻击类(label=1)的召回率,如果低于85%,说明还有不少攻击漏掉。这时优先调min_samples_leaf或者换更复杂的采样策略,而不是盲目加深模型。

4.2 交叉验证与随机种子:实验结果怎么才可信

单次切分训练/测试存在偶然性,尤其是 U2R 只有几十条样本,随便一切可能某一折里完全没有。更可信的做法是交叉验证。用 5 折分层交叉验证,每一折的类别比例和整体一致,最后取均值作为实验结果。

from sklearn.model_selection import StratifiedKFold, cross_val_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(model, X_train, y_train, cv=cv, scoring='f1') print('F1 mean:', scores.mean().round(4), 'std:', scores.std().round(4))

scoring='f1'是二分类不平衡场景下最合适的评分指标。shuffle=True会让数据先打乱再分折,避免原始数据里同一时间段的流量扎堆。这里有个坑:入侵检测数据往往按会话顺序排列,如果不打乱直接分折,前4折可能全是 normal,第5折全是 attack,模型性能分数会被严重低估。random_state=42固定分折结果,保证论文里的数字可复现。

4.3 特征重要性分析:找出真正起作用的流量特征

随机森林训练完成后,feature_importances_可以直接拿来分析。这一步对毕设答辩特别有用,因为它把问题从“模型能检测攻击”推向“模型靠什么检测攻击”。

import pandas as pd importances = pd.Series(model.feature_importances_, index=X_train.columns) top = importances.sort_values(ascending=False).head(10) print(top.round(4))

拿 Top 10 特征去和领域知识对照,你会发现dst_host_srv_count、dst_host_same_srv_rate、src_bytes这类特征排名靠前,这符合入侵检测的直觉——攻击行为往往会产生异常的连接次数和字节数。把这些特征列成表格写进论文,再补一段解释,比单纯说“模型效果好”有说服力得多。如果特征重要性分布过于集中在前两三个特征,说明数据预处理可能出了问题,或者某个特征存在严重的类别泄漏,需要回头检查编码逻辑。

5. 部署排查:模型持久化、接口封装与四个必踩的坑

5.1 模型持久化与推理脚本:joblib 保存、加载与单条预测

训练完成只是第一步,后续要把模型部署成可调用的检测服务,或者至少能对单条新流量做预测。保存模型时建议用joblib.dump而不是pickle.dump,前者对大 numpy 数组和树结构的压缩更好,加载速度也更快。

import joblib import pandas as pd # 加载保存的模型和特征列 model = joblib.load('model/rf_undersample.pkl') feature_cols = joblib.load('model/feature_cols.pkl') def predict_one(raw_sample: dict) -> dict: # 关键:用训练时的特征列顺序重建 DataFrame df = pd.DataFrame([raw_sample])[feature_cols] prob = model.predict_proba(df)[0, 1] pred = int(prob >= 0.5) return {'prediction': pred, 'attack_probability': round(prob, 4)}

这里最重要的细节是pd.DataFrame([raw_sample])[feature_cols]。feature_cols保存的是训练时的特征列顺序,用它做索引可以把输入字典重新排成模型需要的顺序。如果你直接pd.DataFrame([raw_sample]),列顺序可能不一致,模型会静默预测出错误结果——这是部署中最隐蔽的坑,因为程序不会报错,但预测值全偏。

5.2 接口封装:用 Flask 提供检测 API 的最小实现

模型推理脚本写好后,套一个 Flask 接口就能变成可调用的 HTTP 服务。这个方案对毕设演示非常合适,答辩现场可以用一行curl或者浏览器插件直接调用,比命令行跑脚本演示效果好得多。

from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/detect', methods=['POST']) def detect(): raw = request.get_json(force=True) result = predict_one(raw) return jsonify(result) if __name__ == '__main__': app.run(host='127.0.0.1', port=5000)

host='127.0.0.1'表示只允许本机访问,答辩时最安全。如果你需要在局域网内演示,可以改成0.0.0.0,但这样任何能访问到你 IP 的人都可能调用接口,存在一定的资源占用风险。request.get_json(force=True)是让 Flask 强制解析 JSON 请求体,哪怕请求头里没写Content-Type: application/json。

5.3 典型踩坑:四个必踩的坑

1. 现象:部署后单条预测全部偏向某一个类别,准确率骤降。原因:输入样本的列顺序和训练时不一致,或者类别特征被重新编码成了不同整数。解决:统一用feature_cols做列顺序校正;预测前打印一行raw_sample.keys() == set(feature_cols)做断言,不满足就直接报错,不要静默放过去。

2. 现象:LabelEncoder 在预测新数据时报 “unknown class” 异常。原因:新样本里的service字段出现了训练集里没有的字符串。解决:预测前先检查类别值,不在已知集合里的统一映射成一个<unknown>占位;或者训练前把所有类别合并后编码,让编码器见过所有可能的取值。

3. 现象:测试集准确率很高,但实际演示时发现攻击样本被漏检。原因:阈值直接用 0.5,但你训练的模型概率输出可能整体偏低,比如攻击类概率集中在 0.3~0.4。解决:在验证集上重新找最优阈值,部署时用这个阈值替代默认 0.5,下一章会给出具体搜索代码。

4. 现象:换一台机器加载模型报错或结果对不上。原因:不同环境下 joblib 或 sklearn 版本不一致,树结构反序列化后行为改变。解决:部署环境固定scikit-learn版本,保存模型时顺便把sklearn.__version__记到部署文档里;项目包里写明依赖版本,是这份部署文档最有价值的部分。

5.4 性能基线:单条推理延迟与批量检测吞吐的参考

入侵检测不一定要求实时响应,但至少要知道模型处理一条数据有多快。下面这段代码可以测出单条预测和批量预测的时间:

import time # 单条预测耗时:取 500 条测试样本逐条预测的平均 t0 = time.time() for i in range(500): model.predict_proba(X_test.iloc[[i]]) single_time = (time.time() - t0) / 500 print('single predict:', round(single_time * 1000, 2), 'ms') # 批量预测耗时:一次预测全部测试集 t0 = time.time() model.predict(X_test) batch_time = time.time() - t0 print('batch predict:', round(batch_time, 4), 's')

200 棵树的随机森林,单条预测应该在几十毫秒级别,超过 100 毫秒时要考虑减树或者做特征选择。批量预测 X_test(几千条)一般能在一秒内完成。如果性能不达标,优先检查是否n_jobs没有设为 -1,以及是不是把整个 DataFrame 的 dtype 弄成了 object 导致底层计算变慢。

6. 进阶:阈值调整、滑动窗口与增量更新,让模型更贴近真实流量

6.1 决策阈值:默认 0.5 不一定是最优的

predict_proba输出的是攻击类概率,默认以 0.5 为界分类。但在不平衡数据上训练出的模型,即使在欠采样后平衡了训练集,预测概率也未必以 0.5 为最佳分界点。用验证集搜索最优阈值,是性价比最高的改进手段:

import numpy as np from sklearn.metrics import precision_recall_curve # 在验证集上计算不同阈值下的精确率和召回率 probs = model.predict_proba(X_val)[:, 1] precisions, recalls, thresholds = precision_recall_curve(y_val, probs) # 找一个 F1 最大的阈值 f1_scores = 2 * precisions * recalls / (precisions + recalls + 1e-9) best_idx = np.argmax(f1_scores) # 注意:thresholds 长度比 precisions 少一个,越界时回退到 0.5 best_thr = thresholds[best_idx] if best_idx < len(thresholds) else 0.5 print('best threshold:', round(best_thr, 3))

precision_recall_curve返回的thresholds数组长度比precisions少一个,直接索引会越界,所以加了一个判断。如果你希望误报率更低,可以选一个 F1 稍低但 FAR 更小的阈值;如果希望攻击一个不落,就选召回率更高点的阈值。这个权衡要结合你的应用场景,没有绝对正确答案。

6.2 滑动窗口批量预测:处理连续流量的一个简化做法

真实的流量是连续到达的,而不是一条一条孤立地看。一种务实的简化是:按会话聚合出每条样本,再以固定窗口滑动,对窗口内的多条样本做批量预测,根据攻击比例决定是否告警。窗口的意义是降低单条样本误报带来的抖动。

window_size = 100 attack_ratio_threshold = 0.3 for start in range(0, len(session_data), window_size): window = session_data.iloc[start:start + window_size] preds = model.predict(window[feature_cols]) attack_cnt = (preds == 1).sum() if attack_cnt > len(window) * attack_ratio_threshold: # 达到告警条件,记录窗口时间戳和攻击占比 alert(start, attack_cnt / len(window))

attack_ratio_threshold=0.3是经验值,可以根据验证集上的误报率调整。如果误报率过高,就提高这个比例;如果检测率太低,就降低它。窗口大小的选择会影响延迟:窗口越小,发现攻击越快,但单条误报的影响越大。

6.3 模型更新策略:周期性重训练与数据回流

流量数据的分布会随时间变化,新的攻击方式不断出现,训练好的模型几个月后性能下降是正常现象,不是你的方案错了。务实的做法是:把高置信度的预测结果回灌到训练集,每隔一段时间重新跑一遍欠采样-随机森林的训练流程,同时保留旧模型作为回滚选项。

这里我最想提醒的一点是:保存模型时文件名带上时间戳或版本号,不要只留一个model.pkl。我曾经因为没有留旧版本,新版模型误报率变高后只能重新花半天调参,根本没有后悔药。保留旧模型、对比新旧模型在验证集上的表现,再决定上线哪个,这是一个简单但极其有效的工程习惯。

希望这篇笔记能让你少走一些弯路,把欠采样-随机森林这个方案从数据准备到部署跑通,而不是卡在某一个看似玄学的坑里。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询