简介:面向网络安全与机器学习方向研究者,提供一套基于机器学习的入侵检测系统完整源码,以解决传统检测规则依赖性强、对未知攻击识别弱、误报率高及模型解释性差等问题。项目覆盖SMOTE数据平衡、传统机器学习、集成学习、深度学习与自动机器学习多种建模方式,并集成SHAP与DALEX解释工具提升模型透明度。压缩包共30个文件,约11.21MB,包含md说明文档、sql数据脚本、png界面截图以及多层zip源码包,目录清晰区分vue前端界面、flask后端服务、数据库脚本与相关工具模块。通过仿真环境验证,系统兼顾检测性能与可解释性,在真实生产条件下表现出良好实用性。已有175人学习下载,适合需要快速部署可解释型入侵检测系统、进行模型对比实验或论文复现的开发者。
1. 入侵检测为什么要机器学习:从规则匹配到未知攻击识别
规则库翻车、误报轰炸,传统 IDS 在未知攻击面前基本靠猜——这是我在实际部署中反复碰到的现实。这套《基于机器学习的入侵检测系统》源码,把数据预处理、SMOTE 平衡、模型训练、Streamlit 可视化整条链路完整串起来了,前端用 Vue 做管理界面,后端用 Flask 提供推理接口,适合正在做安全方向课设、毕设,或者已经受够了 Snort 规则维护想换一条路的开发者。你不需要从零搭框架,拿到就能跑通整个检测闭环,重点观察它怎么处理类别不平衡、怎么把 SHAP 和 DALEX 的解释能力接进界面,这两点比模型本身更值钱。
2. 项目架构与数据流:Streamlit、Vue、Flask 的分工逻辑
这套资源在架构上最值得先说清楚的一点:它没有把所有功能塞进一个框架,而是拆成了三块。Streamlit 负责数据分析与模型演示,Vue 负责后台管理界面,Flask 负责后端服务和模型推理。拆开的好处是职责清晰,坏处是两个 Web 服务同时跑的时候端口和依赖都容易打架,这个我在第 5 章会专门讲。
检测链路可以从一条请求来理解:管理员在 Bootstrap 或 Vue 页面登录,上传或选择要检测的数据;Flask 收到请求后调用训练好的模型做预测,结果写回 MySQL;Streamlit 页面读同一份数据来做特征分析和 SHAP 解释。也就是说,数据库是三个模块之间的共享状态,sql/model-d.sql就是初始化这个库的建表脚本。如果你只想验证最核心的检测能力,最小跑通路径是:先导入 model-d.sql 建库,再启动 Flask 服务,最后分别拉起 Vue 和 Streamlit。整个过程大概十分钟能完成,难的不是顺序,而是环境依赖。
2.1 文件结构与模块边界
解压 ids-master.zip 之后,第一步是把嵌套的 zip 也解压。项目正文里能看到 vue_manage.zip、0my_pro_flask.zip、bootstrap_login.zip 和 pages.zip 都是压缩包形式,说明作者在分发时把不同模块独立打包了。我用一张表把目录和职责对应起来:
| 路径 | 模块 | 职责 |
|---|---|---|
| vue/vue_manage.zip | Vue 管理前端 | 用户管理、检测结果展示 |
| flask/0my_pro_flask.zip | Flask 后端 | 模型推理 API、登录鉴权、数据库交互 |
| bootstrap/bootstrap_login.zip | Bootstrap 登录页 | 登录/注册入口 |
| streamlit/pages.zip | Streamlit 多页面 | 数据分析、特征可视化、SHAP 展示 |
| sql/model-d.sql | 数据库脚本 | MySQL 建表与初始数据 |
| images/*.png | 截图 | README 配图、UI 预览 |
| README.md / README.en.md | 文档 | 中英文项目说明 |
解压操作在 Linux 下用 unzip 一条命令就能完成:
cd ids-master unzip vue/vue_manage.zip -d vue/vue_manage unzip flask/0my_pro_flask.zip -d flask/0my_pro_flask unzip bootstrap/bootstrap_login.zip -d bootstrap/bootstrap_login unzip streamlit/pages.zip -d streamlit/pages ls -l vue vue/vue_manage flask streamlit这里每一行把 zip 解压到同名目录,避免压缩包内文件直接散落到上级目录。我一般会加-d参数强制指定解压目录,这样就算压缩包内部带了嵌套文件夹也不会乱。最后一个 ls 是为了确认解压后目录结构符合预期,别直接跳到启动那一步。解压之后先翻 README.md 里写的启动命令,因为不同作者习惯不同:有的用python app.py,有的用flask run,有的在 streamlit 目录下直接streamlit run main.py,以 README 为准,不要凭经验猜。
2.2 Streamlit 多页面与 pages 机制
streamlit/pages.zip 解压后的目录是 Streamlit 的多页面模式。Streamlit 从 1.0 开始支持pages/目录,放置在 pages 下的.py文件会自动出现在侧边栏,每个文件对应一个独立页面。这种组织方式比在一个.py里堆十几个 if 分支清爽得多。pages 目录下通常会有数据预览页、特征分布页、模型对比页和 SHAP 解释页,除了四类页面之间要共享同一个模型加载逻辑,避免重复训练——你可以把这个设计借鉴到自己的项目里,把模型加载函数抽成公共模块,页面文件只做展示和交互。
在 Streamlit 多页面结构里,页面之间传数据用st.session_state是最省事的做法。例如在第一个页面选定模型后,把模型对象存进 session_state,后续页面直接读取,不需要重新加载。注意 session_state 存模型对象时要确保模型文件路径是绝对路径,因为 Streamlit 的工作目录可能不是streamlit/而是项目的根目录。
import streamlit as st import joblib if "model" not in st.session_state: st.session_state["model"] = joblib.load("../models/ids_model.joblib") model = st.session_state["model"]这段代码里,joblib.load加载一次模型后放进 session_state,后续页面复用。路径../models/ids_model.joblib是相对于 streamlit 运行目录的常见位置;如果加载失败,把模型文件放在与页面相同目录下,改成相对路径即可。
2.3 Flask 如何承接模型推理
Flask 部分的核心是加载训练好的模型文件并提供 predict 接口。正常流程是模型文件在启动时加载到内存,然后每个请求通过 POST JSON 把特征数组传过来,后端返回类别和概率。这里有一个细节:模型文件和 Flask 代码分离存放,模型文件放在models/或 flask 同级目录下,这样前端页面更新不会影响模型文件。
from flask import Flask, request, jsonify import joblib import numpy as np app = Flask(__name__) model = joblib.load("../models/ids_model.joblib") @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json() features = np.array(data["features"]).reshape(1, -1) proba = model.predict_proba(features)[0] return jsonify({ "prediction": int(model.predict(features)[0]), "probability": float(max(proba)) }) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)reshape(1, -1)是为了把单条样本的列表转成二维数组,sklearn 模型不接受一维输入。predict_proba返回的数组里每个元素代表一个类别的概率,取最大值作为置信度返回前端。启动参数host="0.0.0.0"是让服务监听所有网卡,这样 Vue 或 Streamlit 在同一局域网内也能访问。如果你想限制只本机能用,改成127.0.0.1即可。
3. 数据预处理与 SMOTE:类别不平衡是第一个翻车点
入侵检测数据集最典型的问题是类别不平衡:正常流量占绝大多数,攻击流量可能只有百分之几甚至千分之几。直接拿原始数据训练,模型只要全部预测为正常就能拿到 99% 的准确率,但这没有任何意义。这套资源里专门提了 SMOTE 技术,说明作者对这个问题是认真处理过的。
SMOTE 的全称是 Synthetic Minority Over-sampling Technique,核心思路是对少数类样本做插值,生成新的合成样本,而不是简单地复制现有样本。它先在少数类样本的 k 近邻里随机选一个邻居,然后在两者连线上随机取一个点作为新样本。和随机过采样相比,SMOTE 生成的样本不完全重复,模型不容易过拟合。不过 SMOTE 也有它自己的坑,最典型的是:必须先划分训练集和测试集,再对训练集做 SMOTE,千万不能对整个数据集做了 SMOTE 再切分。原因在第 5 章会展开,这里先记住结论。
3.1 数据清洗:先看标签分布和缺失值
拿到数据第一步我一般会做三件事:看标签列分布,确认少数类占比;检查缺失值和无穷值;把类别特征编码、数值特征标准化。
import pandas as pd df = pd.read_csv("data/ids_data.csv") print(df["label"].value_counts(normalize=True)) print(df.isnull().sum()[df.isnull().sum() > 0]) print(df.describe()) # 把标签列转为 0/1:正常为 0,攻击为 1 df["label"] = (df["label"] != "normal").astype(int)value_counts(normalize=True)输出的是各类别占比,能直接从数字上看出不平衡程度。isnull().sum()对每一列统计缺失值数量,当某列缺失占比超过三成时我一般直接丢弃,因为补出来的噪声往往比信息更多。describe()是为了快速看数值范围,很多网络流量特征的分布差异极大,比如数据包长度和连接时长,不标准化会让梯度类模型收敛很慢。
3.2 SMOTE 参数选择与实际采样
用 imbalanced-learn 库的 SMOTE 做采样,关键参数是sampling_strategy和k_neighbors。
from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split X = df.drop(columns=["label"]) y = df["label"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) smote = SMOTE(sampling_strategy="auto", k_neighbors=5, random_state=42) X_train_res, y_train_res = smote.fit_resample(X_train, y_train) print("SMOTE 前:", y_train.value_counts().to_dict()) print("SMOTE 后:", y_train_res.value_counts().to_dict())sampling_strategy="auto"意味着把所有少数类补到与多数类一样多,如果想让少数类只补到多数类的 50%,就传0.5。k_neighbors是生成样本时参考的近邻数量,默认 5;当少数类样本特别稀疏时,不要调太小,否则生成的样本容易贴着边界甚至变成噪声。切分时stratify=y是必须的,它保证训练集和测试集里正负样本比例与全量数据基本一致,不然切分本身就会引入新的不平衡。
3.3 特征列处理与标准化顺序
SMOTE 只能处理数值特征,如果原始数据里有 IP 地址、协议类型这种类别特征,需要先做 One-Hot 编码或标签编码。顺序上要记住:先编码,再切分,再 SMOTE,再做标准化。顺序错了,最容易出现的是标准化用的均值和方差把测试集信息混进来,这属于另一个容易被忽略的信息泄漏点。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler().fit(X_train_res) X_train_scaled = scaler.transform(X_train_res) X_test_scaled = scaler.transform(X_test)这里fit只放在 SMOTE 后的训练集上,测试集用同一个 scaler 做transform,严禁重新 fit。你也可以把 scaler 保存成文件,和模型一起部署,接口里复用同一份归一化参数。
4. 模型构建与训练:从逻辑回归到自动机器学习的选型路径
这一章说模型怎么选。资源里提到的四个层次我很认同:传统机器学习、集成学习、深度学习、自动机器学习。在实际做入侵检测时,我的习惯是先跑一个逻辑回归或者决策树做基线,再上随机森林和梯度提升树,如果数据集规模足够大再考虑深度学习,最后如果时间预算充足,用 AutoML 工具自动搜一下参数。
为什么按这个顺序?因为入侵检测对误报率很敏感,模型的可解释性也非常重要。逻辑回归和决策树的解释性最强,集成学习在性能和解释性之间有一个平衡,深度学习效果可能最好但解释性最差。自动机器学习则是把超参数搜索交给工具,省人力但可控性下降,而且搜索过程消耗大量计算资源。这是选型时的核心考量:不是模型越复杂越好,而是要在可解释性和误报率之间找到你能接受的点。
4.1 传统机器学习与集成学习做基线
随机森林和 XGBoost 是入侵检测场景里最常用的两个模型。随机森林是 Bagging 思路,对每棵树的预测结果投票,抗过拟合能力强;XGBoost 是 Boosting 思路,通过加法模型拟合残差,在大多数表格数据上表现优于随机森林,但对参数更敏感,调参不好反而过拟合。
from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import classification_report rf = RandomForestClassifier( n_estimators=200, max_depth=12, min_samples_leaf=4, class_weight="balanced", random_state=42, n_jobs=-1, ) rf.fit(X_train_scaled, y_train_res) print(classification_report(y_test, rf.predict(X_test_scaled))) xgb = XGBClassifier( n_estimators=200, max_depth=6, learning_rate=0.1, scale_pos_weight=1, eval_metric="logloss", random_state=42, ) xgb.fit(X_train_scaled, y_train_res) print(classification_report(y_test, xgb.predict(X_test_scaled)))class_weight="balanced"是随机森林自带的对抗类别不平衡的手段,它会给少数类更高的惩罚权重。scale_pos_weight是 XGBoost 里等价的做法,通常设为多数类样本数除以少数类样本数,例如 95:5 就填 19。min_samples_leaf控制叶子节点最小样本数,调大这个值能明显抑制过拟合,入侵检测数据噪声多,建议从 3 到 5 起步。如果训练后测试集 F1 明显低于训练集,优先检查是不是 SMOTE 顺序出了问题,而不是急着加正则化。
4.2 深度学习模型什么时候该上
深度学习在入侵检测里主要是多层感知机和基于自编码器的异常检测。MLP 的优势是能自动学习特征交互,缺点是训练时间长、可解释性差,而且在小数据集上容易被调参好的 XGBoost 反超。一般只有当训练数据足够大,比如超过 50 万条流量记录,才值得花精力调神经网络。在资源提供的流程里,深度学习更多是作为对比实验存在,用来证明集成学习在中小数据集上性价比更高。
4.3 自动机器学习:AutoML 的定位
资源里提到自动机器学习,我理解其定位不是替代主模型,而是帮你找一个更好的参数组合或模型组合。常见做法是用 TPOT 做流程搜索,它会自动尝试预处理方式、模型选择和超参数组合。
from tpot import TPOTClassifier tpot = TPOTClassifier( generations=5, population_size=20, cv=5, scoring="f1", random_state=42, verbosity=2, ) tpot.fit(X_train_scaled, y_train_res) print(tpot.fitted_pipeline_)generations和population_size越大效果越好,但时间成倍增加,第一次跑建议用generations=3, population_size=10验证流程能走通。scoring用f1而不是accuracy,因为类别不平衡场景下 f1 更能代表真实性能。TPOT 搜索出来的模型可以直接通过tpot.fitted_pipeline_导出,保存成 joblib 文件用于部署。
5. 避坑排查:部署这套 IDS 最容易翻车的五个地方
这一章专门记录实际运行这套系统时容易踩进去的坑。如果你是自己下载下来研究,第 2 章说的目录解压其实是小事,真正让人头疼的是环境、泄漏和打包。每条我都按现象、原因、解决三个步骤写清楚,方便你出问题时快速定位。
5.1 端口冲突:Streamlit 与 Flask 抢同一个端口
现象是 Flask 启动后 Streamlit 启动直接报 Address already in use,或者反过来。原因是 Streamlit 默认端口是 8501,Flask 开发服务器默认是 5000,本来不冲突,但某些机器上 8501 已经被占用,Flask 里也可能有人显式写了端口 8501。解决方法是启动前先查端口占用,然后强制指定不同端口:
lsof -i:8501 lsof -i:5000 streamlit run main.py --server.port 8502 python flask/0my_pro_flask/app.py如果是因为一个 Python 进程同时拉起多个服务,也会出现这种问题。尽量分开进程跑,不要在一个脚本里用 subprocess 串行启动。
5.2 SMOTE 信息泄漏:训练集和测试集互相污染
现象是训练时 F1 很高,但用新采集的流量做验证时效果断崖式下跌。原因基本可以锁定为先 SMOTE 后切分,或者先标准化后切分。SMOTE 根据少数类样本的邻居生成新样本,如果测试集数据参与了合成,模型在训练时已经见过测试集的邻居信息,测试分数自然虚高。解决方法是严格固定处理顺序:切分 → SMOTE → 标准化。验证时用新的测试集重新评估,如果分数明显下降,说明之前泄漏问题已经把模型污染了,需要重新走一遍流程。
5.3 Vue 打包后接口 404
现象是开发环境npm run dev一切正常,打包部署后接口全部 404。原因是开发环境下 Vite 或 webpack 的代理配置会转发/api请求到 Flask,打包后这个代理失效,请求落到静态服务器或 Flask 错误路径。解决方法是把 Vue 构建产物dist目录交给 Flask 托管,并确认 Flask 路由统一以/api前缀声明。如果你不想改代码,最省事的方案是开发阶段用 dev 模式跑 Vue,生产环境把 dist 放到 Flask static 目录下面。
5.4 model-d.sql 导入报字符集错误
现象是mysql < model-d.sql时报 Invalid utf8mb4 character string 或 collation 相关错误。原因是 SQL 文件内部用 utf8mb4 字符集写入中文或特殊字符,本地 MySQL 默认字符集不一致。解决方法是先设置连接字符集再导入,同时确认数据库版本支持 utf8mb4:
mysql -u root -p --default-character-set=utf8mb4 < sql/model-d.sql导入后用SHOW TABLES;验证表是否建立成功,别直接启动 Flask,否则连接数据库会报 Table doesn't exist。
5.5 模型文件换机器后无法加载
现象是训练机加载模型没问题,换到部署机就报 ModuleNotFoundError 或 ValueError。原因是直接用 pickle 保存模型时,模型内部引用了训练环境的自定义类,换机器后类缺失。解决方法是统一用joblib.dump和joblib.load,并把依赖库打包到 requirements.txt。部署前在新环境里先写一个最简加载脚本测试成功后再启动 Flask,不要等请求来了才暴露问题。
6. 可解释性验证:SHAP 与 DALEX 的正确打开方式
最后一章落在可解释性上。很多入侵检测项目能做到高精度,但说不出每个决策为什么,生产环境根本不敢用。这套资源里提到用 SHAP 和 DALEX 提升透明度,这一步才是它能落地的关键。
6.1 SHAP 全局与局部解释
SHAP 输出的全局特征重要性比随机森林自带的feature_importances_可靠得多,它给每个样本、每个特征一个贡献值,还可以画依赖图看某个特征在不同取值下如何影响预测。实际部署时我会保存一张 SHAP 摘要图,在告警页面展示给安全人员看。
import shap explainer = shap.TreeExplainer(rf) shap_values = explainer.shap_values(X_test_scaled[:1000]) shap.summary_plot(shap_values, X_test_scaled[:1000], feature_names=X_test.columns)对树模型用TreeExplainer是速度最快且精确的做法。shap_values可能是二维数组(二分类)或三维数组(多分类),多分类时需要按类别索引。summary_plot生成的图里,横坐标是 SHAP 值,正负代表对预测结果的推动方向,颜色代表特征取值高低。
6.2 单条流量解释的工程实现
安全人员更关心的是某一条流量为什么被判定为攻击。把单样本解释封装成接口,是让模型从黑匣子变成可追责工具的关键一步。
sample = X_test_scaled.iloc[[0]] shap_val = explainer.shap_values(sample)[0] if isinstance(shap_values, list) else shap_values[0][0] local_fi = pd.DataFrame({"feature": X_test.columns, "shap": shap_val}) print(local_fi.reindex(local_fi["shap"].abs().sort_values(ascending=False).index).head(10))取单条样本,输出 SHAP 绝对值最大的前 10 个特征,这就是这条流量被判为攻击的主要依据。
6.3 DALEX 交叉验证解释结论
DALEX 是模型无关的可解释性工具包,和 SHAP 最大的区别是它不依赖模型内部结构。做验证时我会把 SHAP 和 DALEX 的解释结果对照,如果两条路径给出的特征排名差异很大,说明模型内部不稳定,需要回头检查数据泄漏或特征冗余。去年我维护一个 IDS 服务时,就靠这种方式定位到一个特征泄漏问题:源 IP 的编码出现在训练和测试里,SHAP 把它排到第一位,DALEX 却排在十名开外。查下去才发现是切分时没有按时间分组,同一 IP 的流量同时出现在训练测试两端。从那以后我每次上线前都强制走一遍 SHAP 与 DALEX 特征排名一致性检查,希望帮到你。
本文还有配套的精品资源,点击获取