基于机器学习的入侵检测系统:从数据集到实时API的完整实现
2026/9/15 2:15:34 网站建设 项目流程

简介:一套基于机器学习的入侵检测系统Python源码项目,面向毕业设计、课程设计与期末大作业场景,依托公开的入侵检测数据集,围绕网络流量的正常与异常分类问题,完整覆盖数据预处理、特征工程、模型训练和效果评估等关键流程。资源包共包含十六个文件,除四个Python程序文件外,还有两个压缩格式的数据集、多个工程配置文件,以及备份和说明文档,整体约17.52MB,目录结构清晰,方便按模块定位源码、数据与结果文件。项目由作者手打实现,在课程实践中获得九十八分的高分,并得到导师认可,目前已有二百三十七人学习下载。代码注释详尽,从数据读取到神经网络训练均有说明,新手也能看懂核心逻辑;下载后简单部署即可直接运行,还能在此基础上调整参数做二次开发,适合需要快速搭建入侵检测原型或参考完整项目完成课程设计、毕业设计的同学。

1. 把机器学习入侵检测系统拆开:这不是一个 sklearn 调用

拿到“基于机器学习的入侵检测系统 python 源码+详细注释”这个压缩包标题,大多数人的第一反应是:加载一份公开数据集,跑个随机森林,输出准确率,完事。但真正能过答辩、能写进简历、甚至能接真实流量的系统,至少要回答四个问题:数据从哪来、特征怎么提、模型怎么选、检测结果如何在网络路径上生效。课程设计之所以被老师一眼看穿是“抄的”,往往就是因为只解决了第三个问题,其余全是硬编码。

这篇文章按我日常做 ML-IDS 的路径来讲:先选数据集和特征,再训模型,然后用评估指标而不是准确率说话,最后把模型落成一个能对流量分类的接口。全程只依赖 Python 生态里最常见的库——pandas、scikit-learn、PyTorch 和 Flask。即使你拿到的源码包和我的做法不完全一样,读完也能理解原作者每一步在干什么,以及哪些地方需要改。

2. 数据与特征:入侵检测系统的“传感器”选型

2.1 用 NSL-KDD 还是 CICIDS2017:课程设计的现实选择

做入侵检测,数据是地基。课程设计里最常出现的两个数据集是 NSL-KDD 和 CICIDS2017。NSL-KDD 是 KDDCUP99 的改进版,去掉了大量冗余记录,规模小,几十 MB,适合在普通笔记本上快速迭代。CICIDS2017 是加拿大网络安全研究所发布的,包含完整流量抓包和 80 多个特征,更接近真实环境,但原始 CSV 有几个 GB,特征多、缺失值多,处理起来麻烦。

我的建议是:课程设计首选 NSL-KDD,因为它的训练集和测试集是分开的,类分布和攻击类型都做了标注,能直接用来对比不同模型的泛化能力。如果你的题目明确要求“对真实抓包流量做检测”,再考虑 CICIDS2017,但一定要先做特征筛选,否则训练时间会很难看。

对比项NSL-KDDCICIDS2017
数据量训练约 12.5 万条,测试约 2.2 万条约 280 万条,按天分 8 个 CSV
特征数41 个80+ 个
攻击类别4 大类,39 种攻击类型14 类攻击,含 DDoS、暴力破解等
文件体积约 70 MB约 50 GB(全量)
是否含原始流量是,可回溯 pcap
适合场景课程设计、算法对比毕业设计进阶、真实流量模拟

无论选哪个,第一步都是把数据读进来,看看有哪些列、缺失多少、分类标签长什么样。下面是用 pandas 做快速探查的代码。

import pandas as pd # 下载 NSL-KDD 后解压,典型文件名是 KDDTrain+.txt train_df = pd.read_csv( "KDDTrain+.txt", header=None, names=[ "duration", "protocol_type", "service", "flag", "src_bytes", "dst_bytes", "land", "wrong_fragment", "urgent", "hot", "num_failed_logins", "logged_in", "num_compromised", "root_shell", "su_attempted", "num_root", "num_file_creations", "num_shells", "num_access_files", "num_outbound_cmds", "is_host_login", "is_guest_login", "count", "srv_count", "serror_rate", "srv_serror_rate", "rerror_rate", "srv_rerror_rate", "same_srv_rate", "diff_srv_rate", "srv_diff_host_rate", "dst_host_count", "dst_host_srv_count", "dst_host_same_srv_rate", "dst_host_diff_srv_rate", "dst_host_same_src_port_rate", "dst_host_srv_diff_host_rate", "dst_host_serror_rate", "dst_host_srv_serror_rate", "dst_host_rerror_rate", "dst_host_srv_rerror_rate", "label" ] ) print(train_df.shape) print(train_df["label"].value_counts()) print(train_df.isnull().sum().sum()) print(train_df.describe())

这段代码做了三件事:按列名读入数据,统计样本总数和类别分布,检查缺失值。label列的值可能是normal或者neptunewarezclient这类攻击名。NSL-KDD 原始标签是具体的攻击类型,课程设计通常把它们二分类成normalattack,也有老师要求做五分类(Normal + 四大攻击类)。你拿到的源码注释里如果直接用了KDDTrain+.txt,大概率也是这套流程。

2.2 数值化、标准化与标签处理的最小代码

机器学习模型只能吃数值,所以三个符号特征protocol_typeserviceflag必须先做编码。常见做法是LabelEncoderOneHotEncoderservice的取值有 60 多种,如果做 One-Hot,特征维度会从 41 扩到 100 多;而LabelEncoder会引入大小关系,比如http编码成 1,smtp编码成 2,这种顺序没有实际意义。我的做法是:protocol_type只有 3 个取值,用 One-Hot;service先用 LabelEncoder 但后续不把它当有序特征;flag同理。

from sklearn.preprocessing import LabelEncoder, StandardScaler # 把攻击标签统统映射成二分类 train_df["label_binary"] = train_df["label"].apply( lambda x: 0 if x == "normal" else 1 ) # 取出特征列和标签列 X_raw = train_df.drop(columns=["label", "label_binary"]) y = train_df["label_binary"].values # 对三个类别特征做标签编码 cate_cols = ["protocol_type", "service", "flag"] encoders = {} for col in cate_cols: le = LabelEncoder() X_raw[col] = le.fit_transform(X_raw[col].astype(str)) encoders[col] = le # 保存,之后测测试集要用同一个编码器 # 数值特征标准化 scaler = StandardScaler() X_scaled = scaler.fit_transform(X_raw)

注意最后几行:fit_transform是在训练集上做的,测试集必须用同一个scaler和同一个encoders去转换,不能在测试集上重新fit,否则会造成数据泄漏,测试结果虚高。很多源码包注释写得不仔细,这里是最容易埋雷的地方。另外 NSL-KDD 里有一个常数特征num_outbound_cmds,全部是 0,标准化后标准差为 0,部分 sklearn 版本会报警告,但不影响结果。

2.3 特征工程常见的坑:类别特征、缺失值、时间窗口

拿到源码后,别急着跑,先看它有没有处理三个细节。第一,num_outbound_cmds这类全零列是否被删除,保留会导致某些算法计算分裂时浪费资源。第二,CICIDS2017 的数据里会有Inf值和 NaN,要用np.isinf配合fillna处理。第三,真实流量是有时间窗口的,TCP 连接的特征如countsrv_error_rate是基于 2 秒窗口统计的,如果做实时检测,你得自己也维护一个滑动窗口,否则模型输入分布和训练时不一致。

课程设计里最容易忽略的特征问题是:测试集和训练集的特征分布不一致。NSL-KDD 的测试集里有一些训练集没出现过的攻击类型,这是故意的。如果你在测试集上经验风险最小化做得过头,比如把某个攻击类型的特征完全记住,那测试集分数会比训练集低很多。这是正常现象,也是评估泛化能力的意义。

3. 模型选型与训练:从“会跑”到“能判”

3.1 为什么先试决策树/随机森林,再上深度模型

基于机器学习的入侵检测,主流算法就三类:树模型、线性模型、神经网络。在 NSL-KDD 这种中小规模表格数据上,随机森林和梯度提升树往往是最优性价比选择,因为特征中存在大量离散类别和互相关联的统计量,树模型天然能处理非线性和特征交互。神经网络 MLP 也能达到接近的表现,但需要更多调参,而且可解释性差。课程设计答辩时老师大概率会问“为什么选这个模型”,你至少得能说出随机森林基于 Bagging 思想、降低方差、天然支持并行化这一个点。

反直觉的结论是:在 NSL-KDD 上,一个决策树(深度不限)的准确率就接近 90%,随机森林能到 98% 以上,而神经网络如果收敛不好,可能还不如决策树。所以课程设计不要一上来就搬出 LSTM 或者 Transformer,先把基线模型跑通,再谈深度网络。

3.2 用 sklearn 训练随机森林分类器的完整脚本

下面给出一个可以直接改的训练脚本,输出分类报告和特征重要性。

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 划分训练集和验证集,保持类别比例 X_train, X_val, y_train, y_val = train_test_split( X_scaled, y, test_size=0.2, random_state=42, stratify=y ) # 初始化随机森林:500 棵树,限制深度防止过拟合 clf = RandomForestClassifier( n_estimators=500, max_depth=20, min_samples_split=10, min_samples_leaf=4, n_jobs=-1, random_state=42, class_weight="balanced" ) clf.fit(X_train, y_train) y_pred = clf.predict(X_val) print(classification_report(y_val, y_pred, target_names=["normal", "attack"])) # 输出前 20 个重要特征 import numpy as np imp = pd.Series(clf.feature_importances_, index=X_raw.columns) print(imp.sort_values(ascending=False).head(20))

几个参数值得说明。class_weight="balanced"是让少数类(攻击样本)在损失计算中获得更高权重,因为流量数据里正常流量往往远多于攻击流量,不平衡会让模型偏向把一切预测为正常。min_samples_split=10min_samples_leaf=4限制了叶子节点的大小,避免树记住单样本的噪声,有效控制过拟合。max_depth=20是为了控制单棵树的复杂度,树模型在 NSL-KDD 上深了之后容易过拟合到训练集的特定攻击模式。n_jobs=-1让多核全跑,500 棵树在这个量级的数据上通常几十秒训练完。

训练完后的feature_importances_很有用。NSL-KDD 里像src_bytesdst_host_srv_count这类特征重要性极高,而前 20 个特征几乎可以解释绝大部分分类表现。课程设计报告里放一张特征重要性条形图,比放训练损失曲线更能说明问题。

3.3 深度模型路线:用 PyTorch 搭一个简单的 MLP 作为基线

如果你的课程设计题目强调了“机器学习”而不是“传统机器学习”,而且你希望在答辩中多一点亮点,可以再补一个多层感知机。代码量不大,但需要处理的就是标准化的特征维度和标签。

import torch import torch.nn as nn X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.long) class MLP(nn.Module): def __init__(self, in_dim): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 2) # 二分类输出 logits ) def forward(self, x): return self.net(x) model = MLP(X_train_t.shape[1]) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss() # 一个正常的 PyTorch 训练循环 for epoch in range(20): model.train() optimizer.zero_grad() out = model(X_train_t) loss = loss_fn(out, y_train_t) loss.backward() optimizer.step() if epoch % 5 == 0: print(f"epoch {epoch}, loss {loss.item():.4f}")

这里CrossEntropyLoss自己包含了 Softmax,所以输出层不用额外加激活。Dropout 在 MLP 中是必须的,因为表格数据维度不高,不加 dropout 很容易训到 100% 训练准确率,验证集却掉下去。训练轮次 20 就够,NSL-KDD 的训练集 12 万条,MLP 在 CPU 上也就几分钟。你拿到的源码里如果有 LSTM 结构,反而要小心——NSL-KDD 的每个样本是一条独立的连接记录,天然没有时序依赖,LSTM 在这里没有信息优势,而且训练慢很多。如果原作者用了 LSTM,多半是把每条记录的特征序列方向当成了时间轴,这种设计说服力不强。

4. 评估与落地:别只报准确率

4.1 混淆矩阵、精确率、召回率、F1 怎么看

入侵检测是一个典型的类别不平衡问题,准确率在这里是骗人的。假设 99% 的流量是正常流量,模型把所有流量都判为正常,准确率就是 99%,但这个系统毫无价值。所以要看你拿到的源码注释里是否打印了混淆矩阵和 F1-score。没有的话,至少补上下面这段。

from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_val, y_pred) print(cm) # 输出格式: # [[TN, FP], # [FN, TP]]

混淆矩阵四个格子决定了核心指标。真正对 IDS 重要的是召回率(TPR),即攻击流量里有多大比例被检出来;精确率(Precision)是报警里有多少是真的攻击。两者有矛盾:把阈值调低,召回率上升但误报增加;调高则反之。F1-score 是两者的调和平均,常用于综合衡量。课程设计里老师说“检测率”通常指召回率,“误报率”是 FP / (FP + TN),这两个数必须同时报告。

我见过不少源码只输出 accuracy,一问 F1 就卡住,这就是没理解评估的本质。建议在代码里加上下面这段:

from sklearn.metrics import precision_recall_fscore_support p, r, f1, _ = precision_recall_fscore_support( y_val, y_pred, average="binary", pos_label=1 ) print(f"Precision: {p:.4f}, Recall: {r:.4f}, F1: {f1:.4f}")

4.2 把模型包成实时检测服务的常见做法

课程设计如果只交一个.ipynb训练文件,那只是“机器学习实验”,不是“系统”。“系统”得有一个能接收输入、返回判断的接口。常见做法是:你抓包后提取特征,拼成一行数据,喂给保存好的模型,模型返回正常或攻击。下面是一个极简的 Flask 示例,适合演示。

import joblib from flask import Flask, request, jsonify # 保存模型时: # joblib.dump(clf, "ids_model.joblib") # joblib.dump(scaler, "scaler.joblib") clf = joblib.load("ids_model.joblib") scaler = joblib.load("scaler.joblib") app = Flask(__name__) @app.route("/predict", methods=["POST"]) def predict(): # 请求体是一行特征的 JSON 数组 data = request.get_json(force=True) feature_vec = list(data["features"]) # 顺序和训练时一致 scaled = scaler.transform([feature_vec]) prob = clf.predict_proba(scaled)[0][1] # 攻击概率 pred = int(prob >= 0.5) return jsonify({"prediction": pred, "attack_prob": round(float(prob), 4)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)

注意代码里的scaler.transform而不是fit_transform,原因前面已经说了。另外特征顺序必须与训练时完全一致,这要求你提取特征的管道(pipeline)是固定的。最好在训练阶段就把Pipeline对象一起保存,避免手写顺序出错。

from sklearn.pipeline import Pipeline pipeline = Pipeline([ ("scaler", scaler), ("clf", clf) ]) joblib.dump(pipeline, "ids_pipeline.joblib") # 后续直接 pipeline.predict(...)

4.3 课程设计答辩时会被问的 3 个问题

第一个问题:“你的模型和直接用防火墙规则有什么区别?”回答要点是:规则只能匹配已知模式,机器学习可以学出未知攻击的相似特征。第二个问题:“NSL-KDD 已经那么老了,为什么还用?”回答要点是:课程设计看重可复现性和算法对比,CICIDS2017 也做了实验但复杂度和训练成本更高。第三个问题:“如果有一个全新的攻击类型,你的模型会不会漏报?”回答要点是:会,所以需要定期用新样本重训模型,这也是后续要做的内容。

5. 参数与调优技巧:让检测率不掉链子的细节

5.1 用 class_weight 和阈值移动处理不平衡流量

NSL-KDD 里攻击样本比例并不算太低(约 46%),但真实网络里正常流量占绝对多数。如果你切换到了 CICIDS2017,会发现 DDoS 类的样本极多,而 Web 攻击类极少。这时除了class_weight="balanced",还可以在预测阶段手动调阈值。默认 scikit-learn 用 0.5 作为决策边界,你可以通过predict_proba自己找最优阈值。

probs = clf.predict_proba(X_val)[:, 1] threshold = 0.3 y_pred_new = (probs >= threshold).astype(int)

阈值调低的代价是误报增多,调高的代价是漏报增多。通常用验证集上 F1 最大化的原则去选阈值。课程设计报告里如果能写一句“我们以 F1 最大化为原则选择了 0.3 的决策阈值,误报率维持在 X%”,就能证明你真正关心了检测效果。

5.2 特征筛选:让模型更快、更稳

前面输出的feature_importances_可以反过来做特征选择。用机器学习做入侵检测,一个重要步骤是砍掉不重要特征,尤其当你要把系统部署到低性能硬件上。可以用 SelectFromModel 做递归筛选:

from sklearn.feature_selection import SelectFromModel selector = SelectFromModel( RandomForestClassifier(n_estimators=200, random_state=42), threshold="median" ) X_selected = selector.fit_transform(X_scaled, y) print(X_raw.columns[selector.get_support()])

筛选后如果特征维度降到 20 左右,训练速度能提升一倍,准确率甚至可能更高,因为噪声减少了。这也印证了一个观点:在 IDS 这类安全领域,特征工程往往比模型结构更值得花时间。

5.3 模型持久化与版本管理

保存模型用joblibpickle更高效,因为 joblib 对 numpy 数组做了优化。注意保存的是整个 Pipeline,而不是分离的 scaler 和分类器,这样可以避免调用时忘记做标准化。每次训练后,把模型文件名带上时间戳:

mv ids_pipeline.joblib ids_pipeline_$(date +%Y%m%d_%H%M%S).joblib

这样你回滚实验时能明确知道哪次改动是有效果的,而不是靠记忆。

5.4 验证技巧:用你抓的本地流量回放测试

课程设计如果能结合抓包,展示效果更有说服力。可以拿 Wireshark 抓一段自己的网页访问流量,用 Scapy 读取 pcap 文件,提取基本特征。

from scapy.all import rdpcap packets = rdpcap("capture.pcap") # 这里按 TCP 流聚合,统计 duration、src_bytes、dst_bytes # 再按 NSL-KDD 的列名顺序构造一条特征向量

注意脱机 pcap 里没有 NSL-KDD 那 41 个特征所需的全部信息,特别是基于 2 秒窗口的统计特征,需要自己实现滑动窗口聚合。课程设计里我一般的做法是:抓取一小段流量,提取 IP、端口、协议、包大小、包数量这些基础字段,然后用countsrv_count等简单计数特征拼接成一条向量,喂给模型。不能指望模型对从未见过的特征模式有好的表现,但至少能证明你的接口链路是通的——从抓包到特征提取到预测返回,全流程走通才叫系统。

最后留一个非常实用的调参技巧:如果你用的是随机森林,把max_features设成"sqrt"而不是None。默认None表示每棵树用全部特征,在 41 维数据上会造成树之间相关性太高,Bagging 的效果被削弱。"sqrt"让每棵树只用约 6 个特征,树与树之间的差异加大,集成的方差降低,最终 F1-score 通常能提升 0.5 到 1 个百分点。这个细节不需要额外训练代价,改一行代码就能看到效果。

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

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

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

立即咨询