KDD-CUP99网络攻击检测实战:从特征工程到随机森林与Web部署
2026/9/23 11:15:37 网站建设 项目流程

简介:面向数据挖掘与网络安全相关的课程设计与期末大作业场景,这份基于KDD-CUP99数据集的网络攻击检测项目,提供了完整的Python实现和配套资料。项目源码经过本地编译验证,难度适中,内容由助教老师审定,可直接用于学习与验收展示。压缩包体积约6.93MB,共75个文件,核心包括14个Python脚本与10个pyc编译文件,同时含有7个Vue前端组件、4个CSS样式、4个JSON配置、HTML入口页面、Markdown说明文档以及SQLite数据库等,覆盖数据预处理、特征提取、模型训练与可视化展示的完整链路。前后端分离的目录结构清晰,便于按模块理解入侵检测流程。已有349人浏览学习,适合需要快速搭建网络攻击检测系统原型、完成期末汇报或入门机器学习安全应用的开发者参考。

1. KDD-CUP99 网络攻击检测:一门课设如何把 IDS 从理论拉到工程

大四做网络攻击检测的项目,老师递过来一份基于 KDD-CUP99 数据集的源码包。当时我第一反应是这东西太老——1999 年 DARPA 采集的 9 周网络流量,和今天动辄百 G 的骨干网不在同一个量级。可真把它拆完,我发现这门课设考的不是算法多新,而是三件事:懂特征、会评估、能把模型包成系统。每个连接 41 维特征、四类攻击(DoS、Probe、R2L、U2R)加一条正常标签,这个分类框架二十多年后仍然是 IDS 行业的标准姿势。适合期末大作业、毕设起步、以及想弄清入侵检测数据链路怎么走的人。更难得的是,这份资料把数据预处理、模型训练、Django 后端和 Vue 前端串成了一条完整链路,不是那种只丢给你一个 notebook 的半成品。

2. 数据先行的第一关:读懂 41 维特征,再谈分类怎么落地

2.1 数据集结构:KDD-CUP99 的四类攻击到底在检测什么

KDD-CUP99 每条记录对应一次 TCP 连接,是一个 41 维特征加一条标签的 tabular 数据。41 维内部可以分成四组:第一组是基础特征(duration、protocol_type、service、flag、src_bytes、dst_bytes 等 9 个),描述一条连接的时长、协议、上下行字节数;第二组是内容特征(hot、failed_logins、num_compromised、root_shell 等 13 个),主要设计用来捕捉 U2R 和 R2L 攻击在会话里留下的痕迹;第三组是基于时间的流量特征,以 2 秒窗口统计相同目标主机的连接次数、错误比例;第四组是基于主机的流量特征,窗口扩大成 100 条连接。四组特征测的是不同粒度的行为画像。你如果只用前 9 个特征,拿到 40% 准确率都属于正常——攻击检验大量分布在流量统计特征上。

标签侧,项目把攻击归成四类,加正常共五类。DoS 是最常见的拒绝服务,样本量最大;Probe 是端口扫描与漏洞探测,属于侦查行为;R2L 是远程未授权访问,U2R 是本地提权,这两类样本占比极小,但恰恰是一般分类器最容易漏掉的部分。做这项网络攻击检测,我建议先把标签分布打印出来再往下走,这一步决定你后面要不要做重采样,也决定答辩时你能不能讲清楚为什么某些攻击类型检测不出来。

2.2 预处理代码:字符串编码、数值标准化与标签映射

把 CSV 读成矩阵并不难,麻烦的是类别特征。protocol_type(tcp/udp/icmp)、service(http/ftp/…几十种取值)、flag(SF/REJ/S0/…)都是字符串,数值列里 src_bytes 能从 0 到上亿,不标准化会让树模型之外的算法直接崩。下面是资源的预处理核心逻辑,我用 sklearn Pipeline 重构过一版:

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder, StandardScaler, LabelEncoder from sklearn.compose import ColumnTransformer # 假设 train_df 是读入的原始 DataFrame,列包含 41 个特征 + label num_cols = ['duration', 'src_bytes', 'dst_bytes', 'wrong_fragment', 'urgent', 'hot', 'num_failed_logins', 'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations', 'num_shells', 'num_access_files', 'num_outbound_cmds', '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'] cat_cols = ['protocol_type', 'service', 'flag'] X = train_df.drop('label', axis=1) y = LabelEncoder().fit_transform(train_df['label']) preprocessor = ColumnTransformer([ ('num', StandardScaler(), num_cols), ('cat', OneHotEncoder(handle_unknown='ignore'), cat_cols) ]) X_processed = preprocessor.fit_transform(X) # 严格流程:先 fit_transform 训练集,保存 preprocessor, # 之后对测试集只调用 preprocessor.transform(X_test)

这段代码里最容易踩的坑是 pipeline 的 fit 边界。ColumnTransformer 里 StandardScaler 和 OneHotEncoder 必须只在训练集上 fit,之后测试集复用同一个 preprocessor。如果你把全量数据丢进去然后一次性划分,均值、方差和类别字典都包含了未来信息,交叉验证分数会虚高,换到真实流量上立刻现形。handle_unknown='ignore' 是为了测试集里出现训练集没见过的 service 值时能安全跳过,编码矩阵里这一列会全部为 0。

标签映射上,原始文件里是字符串(normal、neptune、satan…),LabelEncoder 会把它们压成 0-4 的整数,配合后面可视化时你需要在代码里维护一张真实名字的对照表,不要靠数字猜攻击类型。我做网络攻击检测时习惯把 normal 定为 0,后面分别对应 DoS、Probe、R2L、U2R。源码里自带的清洗脚本已经处理过行尾点号这些脏数据,我复现时基本只改了下文件路径。

2.3 看一眼类别分布再动手:不平衡比模型更重要

训练集包含了几十万条记录,但分布极其倾斜:DoS 占了大头,U2R 只有几十到几百条。这意味着即使什么都不学,把所有样本预测成 DoS,整体准确率也能到 60% 以上。所以预处理阶段我会额外做一件事——分层抽样(stratify),把各类比例在训练集/验证集里保持一致:

X_train, X_val, y_train, y_val = train_test_split( X_processed, y, test_size=0.2, stratify=y, random_state=42 )

stratify 参数在样本不均衡时属于必选,否则运气差一点,验证集里可能一条 U2R 都没有,模型报告直接失去参考意义。分完层之后我还会用 value_counts() 核对每类样本数,U2R 如果少于 10 条,说明这份数据切得太狠,考虑换成 k 折分层交叉验证,或者把 10% 训练集和 corrected 测试集合并起来重新划分。重采样方案先放后面:先用类权重(class_weight='balanced')跑通基线,U2R/R2L 实在拉不起来再上 SMOTE,别一上来就生成合成样本,不然边界容易更乱。

3. 模型主线:随机森林跑五分类,准确率不是唯一指标

3.1 为什么选树模型而不是一上来就上神经网络

KDD-CUP99 这类表格型数据里,树模型有天然优势:对特征尺度和单调变换不敏感,特征交互靠规则分裂就能表达,训练速度快到可以在笔记本上反复试参数。相比之下,深度学习最出效果的是高维稀疏或序列数据,在这里往往需要大量调参才能追平随机森林的基线。资源源码里用的方案正是 sklearn 的 RandomForestClassifier,配合少量特征筛选,效果足够支撑一场答辩。

我在复现时保留了数据集原始特征组合,没有做 PCA。原因是 PCA 会把单个特征的可解释性抹掉,后面答辩讲「为什么 count 和 srv_count 组合能识别端口扫描」时根本说不出人话。模型的 joblib 文件从源码里直接导出来后是十几 MB 级别,Django 后端加载完全可行,这也是我当时没用 XGBoost 的一个考量。如果你后面想换 LightGBM,数据链路不用动,只换训练脚本和模型文件即可,这也是这套工程结构比较舒服的地方。

3.2 训练主代码:参数边界与 OOB 验证

随机森林有两个参数最容易纠结:n_estimators 和 max_depth。n_estimators 太少会抖,太多训练慢且收益递减;max_depth 限制过深会过拟合,但树模型在 41 维特征上即使不限制,也通常不会太离谱。我在源码基础上跑过的配置如下:

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix import joblib model = RandomForestClassifier( n_estimators=200, # 我习惯先固定 200,看 OOB 是否收敛 max_depth=20, # 限制深度,防止单棵树记住噪声 min_samples_split=5, # 节点再划分所需最少样本数,控制分裂风险 class_weight='balanced', # 缓解 U2R/R2L 的倾斜 n_jobs=-1, # 全核并行 random_state=42, oob_score=True # 用袋外样本评估泛化,不用额外切验证集 ) model.fit(X_train, y_train) print("OOB score:", model.oob_score_) joblib.dump(model, "ids_model.joblib") joblib.dump(preprocessor, "preprocessor.joblib")

这里的 oob_score=True 是白捡的泛化参考:每棵树只用约三分之二样本训练,剩下三分之一袋外样本做预测,对随机森林来说等价于内置了一次交叉验证。我一般拿它和验证集分数对照,两者差超过两个百分点就说明有泄漏或过拟合。class_weight='balanced' 会按类别频率反比放大少数类权重,代价是 DoS 的精确率可能轻微下降,但从「攻击检测」的目标看值得。model 和 preprocessor 分开保存有个好处:接口层换模型不影响预处理,改模型不改代码。

3.3 评估指标该怎么看:混淆矩阵里藏着问题

一个典型翻车现场是整体准确率 99%,但 U2R 的 recall 是 0。原因很简单:U2R 样本太少,分类器把所有样本都判成 Normal 或 DoS,全局 accuracy 几乎不受影响。应对方案是打印 per-class 报告再谈优化:

y_pred = model.predict(X_val) report = classification_report( y_val, y_pred, target_names=['Normal', 'DoS', 'Probe', 'R2L', 'U2R'], digits=3 ) print(report) print(confusion_matrix(y_val, y_pred))

对于本项目的四类攻击场景,重点看两个数:R2L 和 U2R 的 recall。只要这两个不过半,系统在真实场景就约等于没装。Probe 和 DoS 通常不难,难的是少数类。如果 val 集上 U2R recall 实在拉不起来,先确认是不是分层抽样抽到了极少数样本,再考虑在 pipeline 里给少数类扩样。模型训练完我会顺手把当时的参数和 per-class 指标写进一个 JSON 文件,跟模型放同目录,以后换数据集再跑,至少知道这个基线是怎么来的,这个习惯帮我避免了好几次「这个分数怎么复现不出来」的尴尬。

4. Django + Vue 双端联动:把模型包成能演示的检测系统

4.1 Django 端:模型加载与预测接口设计

资源工程里 Django 目录承担后端服务,manage.py 在根目录,配置里注册了数据库(sqlite3),static 和 templates 负责传统服务端渲染页面。既然前端用 Vue 写了 net-analyze 这个工程,实际联调时我会让 Django 只暴露 API,页面交给 Vue 去拉。预测接口的核心是避免重复加载模型——sklearn 的 joblib.load 在机械硬盘上动不动几百毫秒,放进 view 函数里每次请求都执行,接口延迟会高到难以接受。项目源码里我看到的是把模型加载放在模块层,这个习惯建议保留:

# views.py import json import joblib import numpy as np from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt _model = None _pre = None def _load_model(): global _model, _pre if _model is None: _model = joblib.load("ids_model.joblib") _pre = joblib.load("preprocessor.joblib") @csrf_exempt def predict_api(request): if request.method != "POST": return JsonResponse({"error": "need POST"}, status=405) data = json.loads(request.body) features = data.get("features") if len(features) != 41: return JsonResponse({"error": "feature length must be 41"}, status=422) _load_model() x = _pre.transform([features]) y = _model.predict(x)[0] proba = _model.predict_proba(x)[0].tolist() return JsonResponse({ "prediction": int(y), "probabilities": proba })

注意这里我把 _load_model() 的调用放在拿到请求并校验完字段之后才执行,是懒加载写法。第一次请求慢一点,后续请求全部复用内存中的模型对象。URL 配置很简单,path("api/predict/", views.predict_api)。校验 features 长度是一个低成本高收益的约定:前端传错维度时接口直接返回 422,而不是让模型去抛一个让人看不懂的维度错误。另外,sqlite3 数据库我用来记录每次预测的日志,包括时间、原始特征、预测结果,后面画趋势图不用重新跑模型。

4.2 Vue 端:仪表盘、攻击分布图与跨域联调

net-analyze 的 Vue 工程包含 babel.config.js、vue.config.js、jsconfig.json 和 package.json,是标准 Vue CLI 结构。前端页面要做的事有三件:上传或粘贴连接特征、调用 Django 接口拿预测结果、把结果渲染成攻击分布图。核心调用代码并不复杂:

// src/api/detect.js import axios from 'axios' export function detectAttack(features) { return axios.post('/api/predict/', { features }) .then(res => res.data) .catch(err => { console.error('predict failed:', err.message) throw err }) }

这里的关键是开发环境不能直接写死http://127.0.0.1:8000,否则会撞上跨域。项目在 vue.config.js 里配了 devServer 代理,把 /api 前缀转发给 Django:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } }

配好 proxy 之后,前端 axios 请求 /api/predict/ 时浏览器看到的请求是同源的,由开发服务器转发给后端,跨域被化解在 dev server 这一层。jsconfig.json 里通常还有 @ 开头的路径别名,import 时可以写@/api/detect而不是一串相对路径。babel.config.js 保持默认即可,如果你在项目里用了比较新的 JavaScript 语法,preset-env 会自动做降级处理。

4.3 前后端数据的字段约定与图表渲染

图表部分我建议前端自己做聚合,后端不要塞一堆 label 映射给前端。后端只返回整数预测结果和概率数组,前端维护一张攻击类型对照表。用 ECharts 的话,饼图和柱状图可以直接消费 probabilities 数组。做网络攻击检测演示时,把 DoS 和 Probe 的概率画成悬浮条,是答辩时的加分项——评审一眼就能看出系统不是黑匣子。

这套字段约定里最容易出问题的是「前端和后端各维护一份攻击类型映射表」,时间一长两边必然对不上。我习惯以 Django 端的一个常量类为唯一事实源,前端从接口拿类型名称而不是自己写死。整套源码包从预处理脚本到 Vue 页面是一次完整闭环,换掉模型文件就能复用到别的数据集上,这也是我愿意把它完整跑一遍的原因。

5. 避坑实录:从数据泄漏到跨域预检,五个翻车点逐一拆解

5.1 one-hot 编码导致特征数量对不上

现象:训练时准确率正常,模型保存后新数据进来,transform 报出特征数量不符,或者预测结果全乱。

原因:预处理用 pd.get_dummies 直接对全量数据编码,如果新数据里某个 service 值没有出现在训练集,那么新数据编码后的列数和训练时不一致,模型无法对齐特征矩阵。

解决:统一走 sklearn 的 OneHotEncoder,在 preprocessor 里 fit 训练数据,之后所有新数据都用同一个 transform。参数 handle_unknown='ignore' 会跳过新类别而不是报错,代价是这一列的编码全部为 0,但至少接口不会崩。

5.2 StandardScaler 在全量数据上 fit,造成信息泄漏

现象:交叉验证分数 99%,但把模型放到 corrected 测试集或新流量上,检测率掉到 80% 以下。

原因:StandardScaler 的均值和方差在全量数据上计算,测试数据的信息早已进入训练流程。这在课设里最隐蔽,因为分数很好看,很难察觉。

解决:把预处理器和模型绑在同一个 Pipeline 里,对训练集 fit,对验证集/测试集只 transform。我复现时把 preprocessor 和 model 分开保存只是为了接口方便,但 fit 边界必须严格,这是一个原则问题。

5.3 整体准确率 99%,查 R2L/U2R 的召回率却是 0

现象:classification_report 里前三行都漂亮,后两行全是 0,看起来像模型坏了。

原因:类别严重不平衡,max_depth 又设得比较浅,树的分裂完全被 Normal 和 DoS 主导,少数类样本一片叶子都分不到。单独优化整体 accuracy 会掩盖这个问题。

解决:先用 class_weight='balanced' 跑一遍,看 U2R/R2L 的 recall 有没有变化;不行再对训练集做 SMOTE 过采样。注意 SMOTE 必须在 train_test_split 之后做,只对训练集生成合成样本,否则又是一轮泄漏。如果项目方只要整体准确率好看,那另说,但做入侵检测的人应该知道这个坑。

5.4 Django 接口每次请求都重新加载模型,响应要 3 秒

现象:第一次请求慢还能忍,之后每次都慢,看日志发现 joblib.load 被反复执行。

原因:模型加载写在 view 函数体内,Django 每个请求都会重新走一遍,机械硬盘上加载一个十几 MB 的模型当然慢。

解决:把模型加载提升到模块顶层或者用 lru_cache 包裹加载函数,进程启动时加载一次,之后只读内存。另外如果开着 debug 模式,代码变更会自动重启进程,也会让人觉得模型加载慢,部署时记得关掉 autoreload。

5.5 Vue 联调报跨域,OPTIONS 预检请求失败

现象:前端 axios 请求 Django 接口,控制台报 CORS error,Network 面板里有一个失败的 OPTIONS 请求。

原因:Django 默认不允许跨域,前端直连 127.0.0.1:8000 时浏览器先发预检请求,被后端拒绝。有人会在 settings.py 里闷头加 django-cors-headers,其实开发期更稳妥的是用 vue.config.js 的 proxy。

解决:开发环境配 devServer.proxy,把 /api 转发到后端;如果一定要跨域直连,再装 django-cors-headers,在 CORS_ALLOWED_ORIGINS 里明确写 http://localhost:8080。写 * 在课设演示里能跑通,但答辩被问安全策略时很难圆。

6. 进阶:把单次检测变成一张能交代的攻击态势图

6.1 做一个轻量级的检测回放脚本

单次 POST 只能验证一条记录,但答辩和实际使用需要看到整体效果。我会额外写一个回放脚本,读取测试集的一条条记录,把预测结果按攻击类型和时间窗口聚合,生成 JSON 给前端画趋势图:

# replay.py import json, joblib import pandas as pd from collections import Counter model = joblib.load('ids_model.joblib') pre = joblib.load('preprocessor.joblib') df = pd.read_csv('corrected.csv') result = [] for _, row in df.head(5000).iterrows(): x = pre.transform([row[:41].tolist()])[0] result.append(int(model.predict([x])[0])) # 按窗口聚合出攻击类型占比 print(Counter(result))

这个脚本的价值不在效果,而在于把「检测系统」从单接口演示延伸成批量回放工具,之后想画准确率曲线、误报随时间变化都从这个结果文件出发。

6.2 阈值自整定:让概率输出可调

同样一条记录,把判断阈值从 0.7 调到 0.3,结果会完全不同。接口里我还保留了一个 threshold 参数,前端滑块改阈值,后端只把概率数组返回,由前端决定是否判定为攻击。这是模型 demo 里最容易被忽略的一个交互点,加了它,演示时就能现场展示「阈值调低 → 检出率上升、误报也上升」的关系,比空口讲概念有用得多。

从那以后我每次做这类课设,都会强制自己走一遍「先看标签分布 → 再定评估口径 → 最后写接口」的顺序。数据没看清楚之前,模型调得再花哨都是白搭。希望帮到你。

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

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

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

立即咨询