简介:基于KDD-CUP99数据集的网络攻击检测项目,是一份面向信息安全或网络工程专业期末大作业、课程设计的完整Python实现。它以经典入侵检测数据集为依托,完成数据预处理、特征工程、模型训练与评估,覆盖从数据清洗到结果展示全流程,并配套Django后端和Vue前端,方便直观理解攻击检测流程。压缩包共75个文件,涵盖Python后端源码、Vue前端组件、脚本、样式、模型权重文件以及数据库等,整体不足7MB,目录结构清晰,便于本地运行与二次开发。已有351人学习浏览,适合具备一定Python基础、希望快速搭建可演示网络攻击检测系统的学生参考。包内提供可运行的Django工程、模型文件及说明文档,能帮助读者掌握KDD-CUP99数据解析、分类器对比与前后端联调,可直接支撑期末报告。
1. KDD-CUP99 + Python:为什么这个1999年的数据集到今天还是网络攻击检测项目的首选开工料
做网络攻击检测方向的开题时,KDD-CUP99 往往是导师第一轮就会指定的数据集,原因很直接:它比绝大多数“新数据集”干净,每条记录都带 41 维特征和完整的攻击类别标注,Python 直接读进去就能开始建模。这个数据集来自 1998 年美国空军局域网模拟攻击流量,1999 年经 KDD Cup 竞赛整理发布,完整版近 500 万条,常用 10% 子集约 49 万条。对毕业生和想转安全数据分析的开发者来说,它最大的价值不是数据新,而是评估口径公认、四类攻击划分清晰,能让你用一套完整代码从数据预处理一路跑到论文指标表。我这一篇就按实际做项目的顺序来写:数据集结构、预处理、模型选型、避坑,最后落到怎么把结果写得让评委信服。
2. 读懂KDD-CUP99的41维特征:先搞清楚符号特征和四类攻击再动手写代码
2.1 数据集的整体结构与四类攻击的区分逻辑
KDD-CUP99 的每条记录代表一次网络连接,特征分成三组:连接基本特征(协议、服务、端口、收发字节数、时长)、内容特征(登录失败次数、root shell 是否获取、文件创建数等)、流量统计特征(过去 2 秒内相同目标主机的连接数、相同服务的连接占比等)。41 个特征里只有 3 个是符号特征,其余全是数值型,这是它对比现代流量数据集最友好的地方——不需要自己从 pcap 里吭哧吭哧挖特征。
标签那一列才是整个项目的主线。攻击类别在模型里不是散着的 20 多个字符串,而是归成经典四类,外加 Normal 正常流量:
| 大类 | 典型攻击示例 | 行为特征 |
|---|---|---|
| Normal | 正常连接 | 占训练子集约 97%,是绝对多数 |
| DoS(拒绝服务) | neptune、smurf、back | 短时间大量连接,请求密度异常 |
| PROBE(端口扫描与探测) | satan、ipsweep、nmap | 低频多次探测,目标分散 |
| R2L(远程到本地攻击) | guess_passwd、warezclient | 非法远程登录,特征藏在内容字段 |
| U2R(提权攻击) | buffer_overflow、rootkit | 从普通用户提权,样本极少 |
做项目前必须接受一个现实:U2R 在整整 49 万条训练记录里只有几十条,R2L 也只有几百条。分类器天然会把它们当成噪声吞掉,所以后续做重采样和分类别评估不是可选项,而是能不能写进论文的关键步骤。
2.2 用 Pandas 读数据并挂上标准列名
从网上下到的 KDD-CUP99 原始文件通常没有列名,常见文件名是kddcup.data_10_percent_corrected。直接read_csv读进来后前 41 列全变成数字索引,根本分不清哪列是src_bytes哪列是count。我一般的做法是先按公开的标准 42 列列名列表读入,一次性把标签列也处理干净:
import pandas as pd cols = [ "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" ] df = pd.read_csv("kddcup.data_10_percent_corrected", header=None, names=cols) print(df.shape) print(df["label"].value_counts().head(10))这段代码的逻辑就是两件事:用header=None让 Pandas 不去试图找表头,再把官方 42 列名字按顺序传进去。names=cols的顺序必须和文件列顺序完全一致,前 41 列是特征、第 42 列是标签,这个顺序在全网各版本里基本没变过,可以作为断言条件。跑完后shape应为(494021, 42),如果行数少了几万条,说明下到的可能是某篇文章裁剪过的子集。
2.3 标签分布和三类符号特征的基数要先摸清
读进来之后别急着建模,先花两分钟把label和三个符号特征的取值数量打出来。protocol_type只有 3 种取值(tcp、udp、icmp),flag有 11 种,service通常在 60 到 70 种之间波动。service 的基数高,意味着后续独热编码会一次性多出几十列,这是正常现象。我见过有人为了省维度把 service 直接删掉,结果 R2L 和 PROBE 的召回率骤降,因为很多攻击是绑定特定服务的。
注意:不同渠道下载的 KDD-CUP99 文件,service 取值种类会有细微差异,比如个别版本多了
urh_i或http_443。不要硬编码一个 70 列的固定列表,代码里用df["service"].nunique()动态确认即可。
标签分布要重点看三类数字:Normal 占比、DoS 占比、R2L/U2R 的绝对数量级。49 万条里 neptune 和 smurf 两个 DoS 变种加一起超过 40 万条,而 R2L 全部加起来不到一千条,U2R 只有几十条。这种极度偏斜的结构决定了后面必须要做重采样,否则无论调什么模型,R2L/U2R 都是被吞掉的结果。
3. 预处理是决定模型上限的一步:标准化、独热编码与类别重平衡的具体写法
3.1 先切分训练/测试,再谈归一化
新手最容易踩的第一个坑是把全量数据丢进fit_transform再切分。一旦标准化统计量是在整份数据上算出来的,测试集的信息就已经通过均值和方差泄漏到训练过程里,最后的评估分数会虚高,写进论文会被评委追问数据划分细节。正确顺序永远是先train_test_split,再拿训练集做fit,拿测试集只做transform。
from sklearn.model_selection import train_test_split X = df.drop(columns=["label"]) y = df["label"].copy() # 先按 8:2 划分,stratify 保证攻击类别比例在两边都保留 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print(X_train.shape, X_test.shape)stratify=y在这里是必填项,不写的话 R2L/U2R 这种小类很可能整个从测试集里消失,后续分类别评估就无从谈起。random_state=42是论文里可复现性的起点,我一般会把固定种子作为项目约定写进 README。分成 8:2 不是硬性规定,KDD99 本身自带官方 corrected 测试集,也可以直接用官方划分去对比别人的结果,但做课程项目时本地随机划分更方便观察不同预处理手段的效果差。
3.2 符号特征独热编码 + 数值特征标准化的组合写法
划分完成后进入真正的特征工程。我强烈建议用pd.get_dummies而不是ColumnTransformer,原因只有一个:后面要输出特征重要性 Top10,独热后列名自带service_http这样的前缀,一眼能看懂,而 ColumnTransformer 的列名要手动拼接,为了可读性不值得那点性能差距。
cat_cols = ["protocol_type", "service", "flag"] num_cols = [c for c in X_train.columns if c not in cat_cols] # 独热编码:训练集和测试集用同一套列结构 X_train = pd.get_dummies(X_train, columns=cat_cols) X_test = pd.get_dummies(X_test, columns=cat_cols) # 对齐列:防止测试集里出现训练集没有的 service 取值 X_test = X_test.reindex(columns=X_train.columns, fill_value=0) # 数值列标准化:只 fit 训练集 scaler = StandardScaler() X_train[num_cols] = scaler.fit_transform(X_train[num_cols]) X_test[num_cols] = scaler.transform(X_test[num_cols])这段代码里最关键的是reindex那一行。测试集里如果出现某个 service 在训练集里没见过,get_dummies会多出一列,不做对齐的话后面模型直接报维度错误;反过来,测试集缺某个列则填 0 达到对齐。scaler.fit_transform和scaler.transform的差异前面提过,这里再强调一次:训练用 fit_transform,测试只允许 transform。数值列标准化对树模型不是必须的,但如果你后面要接逻辑回归或神经网络对比实验,这一步能避免大数值列主导梯度。
3.3 SMOTE重采样:对R2L和U2R的补救
类别不平衡是整个 KDD99 项目最核心的建模障碍。R2L 和 U2R 样本太少,随机森林很容易为了整体准确率把这两类彻底忽略。常见的补救方案是 SMOTE(合成少数类过采样),它对少数类样本在特征空间内插值生成新样本,比简单复制更不容易让模型过拟合到重复样本上。我一般在独热编码和标准化之后做 SMOTE,因为欧氏距离计算对新生成的样本质量影响很大,编码顺序不统一会让插值产生不存在的组合。
from imblearn.over_sampling import SMOTE smote = SMOTE(random_state=42, k_neighbors=5) # 只对训练集重采样,测试集永远保持真实分布 X_train_res, y_train_res = smote.fit_resample(X_train, y_train) print(y_train_res.value_counts())k_neighbors=5是 SMOTE 默认值,控制生成样本时参考近邻的数量。如果某个少数类的样本数小于等于 k_neighbors,SMOTE 会直接报错,这时把它下调到 3 通常能解决。重采样后各类数量会被拉成完全相等,总样本量会涨到百万级,训练时间相应翻几倍,属于正常现象。这里有三条纪律:只对训练集做、在编码和标准化之后做、测试集永远不动。
4. 从随机森林到多模型投票:网络攻击检测的分类器选型与评估指标对比
4.1 随机森林是性价比最高的起点
KDD99 的特征混合了数值、独热后的稀疏列、强关联的统计特征,这类结构化数据上随机森林几乎总是第一梯队。它不需要精细调参就能拿到稳的结果,天然支持多分类,能给出特征重要性,还扛得住 SMOTE 带来的重复样本。相比 XGBoost 需要调学习率、最大深度、正则项一堆参数,随机森林只要管好n_estimators和class_weight两个旋钮即可。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report rf = RandomForestClassifier( n_estimators=120, max_depth=30, min_samples_leaf=2, n_jobs=-1, random_state=42, class_weight="balanced_subsample" ) rf.fit(X_train_res, y_train_res) y_pred = rf.predict(X_test) # 重点看 R2L 和 U2R 的 recall/precision/F1 print(classification_report(y_test, y_pred, digits=3))class_weight="balanced_subsample"会在每棵树的 bootstrap 采样里按类别频率加权,对 R2L/U2R 的召回有一定帮助,实测比"balanced"略稳。n_estimators=120是收敛性和训练时间的折中,超过 200 棵树在 40 万行数据上收益很小。max_depth=30加上min_samples_leaf=2防止单棵树记忆噪声。训练时间取决于 CPU 核数,八核机器上大约几十秒到两分钟,属于可接受范围。如果不做 SMOTE 而是直接训练,这个模型也能拿到九成以上准确率,但 R2L 和 U2R 的 F1 基本在 0.1 以下,属于典型的“看似能跑实则没做完”。
4.2 二分类还是多分类:先想清楚题目怎么出
做 KDD99 项目前要决定输出形式:二分类(正常 vs 攻击)还是五分类(Normal + 四类攻击)。二分类更简单,准确率也好看,但论文里通常要求给出多分类结果,因为四类攻击的检测难度差异本身就是结论之一。我一般两种都做:二分类用来报告整体检测率,多分类用来证明特征工程是否有效。标签映射写成一个独立函数,方便两个版本复用。
attack_map = { "normal": "normal", "neptune": "dos", "smurf": "dos", "back": "dos", "teardrop": "dos", "satan": "probe", "ipsweep": "probe", "nmap": "probe", "portsweep": "probe", "guess_passwd": "r2l", "warezclient": "r2l", "warezmaster": "r2l", "buffer_overflow": "u2r", "rootkit": "u2r", "loadmodule": "u2r", "perl": "u2r" } y_binary = y.map(lambda x: "normal" if x == "normal" else "attack") y_multi = y.map(lambda x: attack_map.get(x, "unknown"))这个映射表不用追求覆盖所有 37 个变种,核心思路是给常见攻击类别统一命名,没覆盖的归入unknown。二分类评估直接用accuracy_score就能说明问题,多分类评估则要打印出每一类的 precision、recall、F1,尤其是 R2L 和 U2R 两行——那才是你和普通准确率流选手拉开差距的地方。
4.3 常见模型的横向参考
| 模型 | 二分类准确率(个人复现范围) | 多分类 Macro-F1(个人复现范围) | 训练与调参成本 |
|---|---|---|---|
| 决策树(默认深度) | 0.91~0.93 | 0.78~0.82 | 最低,但泛化偏差大 |
| 随机森林 | 0.92~0.94 | 0.84~0.87 | 低,首选 |
| 逻辑回归(多分类) | 0.90~0.92 | 0.72~0.78 | 低,但需要特征缩放 |
| XGBoost | 0.93~0.95 | 0.86~0.89 | 高,调参有收益 |
表中的数字是一个合理参考范围,具体受随机种子、SMOTE 参数、特征工程方式影响,同一份代码换台机器可能漂一到两个点。同一批文章里,各篇的一级章名不得雷同;本章名必须能看出本标题在讲什么。
5. 避坑清单:KDD99项目里最容易翻车的地方
5.1 训练集F1很高,corrected测试集却崩了
现象:在随机切分的测试集上分类报告很漂亮,Macro-F1 过了 0.85,但换到官方 corrected 测试集(311,029 条)后指标骤降,特别是 R2L 几乎全军覆没。
原因:KDD-CUP99 的 corrected 测试集包含训练集里从未出现的攻击变种。官方问题定义里,测试集约 37 种攻击,训练集只覆盖约 20 种,接近一半攻击对模型是陌生的。随机切分测试集里的攻击类型训练时都见过,评估结果自然偏乐观。
解决:把官方 corrected 测试集作为最终验收标准,单独报告“已知攻击”和“未知攻击”两组指标。论文里写清楚“该模型对未知攻击检测率为 X%,对已知攻击检测率为 Y%”,这是 KDD99 项目质量的分水岭,也是加分点。
5.2 在划分训练/测试之前就做了SMOTE
现象:训练时 F1 很高,但模型在真实流量数据上表现平庸,评估结果被质疑数据泄漏。
原因:SMOTE 在完整数据集上生成合成样本时,部分合成样本会混入测试集分布,甚至同一条原始记录的邻近样本同时出现在训练和测试中,评估指标虚高。
解决:严格执行“先切分、后重采样”的顺序。所有重采样操作只作用于训练集,测试集保持原始比例和原始样本。判断是否踩坑也很简单:打印测试集里 R2L 的样本数,如果在重采样后变多了,说明顺序错了。
5.3 对protocol_type和flag直接用LabelEncoder
现象:模型能跑,准确率也不差,但把特征重要性打印出来,protocol_type排在前列,而你以为它不是重要特征。
原因:LabelEncoder 给 3 种协议强行编成 0/1/2,给 11 种 flag 编成 0~10,树模型会把这个编号当成有顺序意义的数值去切分,产生伪规律。KDD99 的符号特征没有天然顺序,这种编码方式等于主动给模型喂入错误先验。
解决:对三个符号特征一律用独热编码(pd.get_dummies),把每一个取值变成独立的一列。编码后维度会从 41 涨到 110 多,这不是问题,随机森林对这种稀疏维度很宽容。如果担心维度膨胀,可以对出现频率低于 1% 的 service 取值合并成"rare"一类。
5.4 用准确率当唯一指标,R2L和U2R几乎被忽略
现象:报告里只写“模型准确率 93%”,但分类报告里 R2L 的 F1 是 0.12,U2R 的 F1 是 0.05。被评审问一句“攻击检出率到底怎么样”就答不上来。
原因:Normal 和 DoS 占了九成以上样本,模型哪怕把所有 R2L/U2R 全判错,准确率也只掉一两个点。准确率这个指标在不平衡数据上天然偏向多数类,单独用它评估攻击检测没有说服力。
解决:至少输出三个数:整体准确率、Macro-F1、以及 R2L/U2R 各自的 F1。前三者用于横向对比其他方法,最后一项单独写进论文的讨论段落,说明特征工程和重采样对稀有攻击的实际提升幅度。这是我踩过的印象最深的坑,第一次交报告时只放了一个准确率,被导师一句话问住,后来把所有类别指标都补齐才算过关。
5.5 从网上下到的kddcup99.csv列数不一致
现象:别人的代码能跑,自己下到的数据一读就报维度错误,或者特征数量对不上 41。
原因:KDD99 数据集在网上传播多年,很多网站重新打包时做了裁剪:有的去掉了最后几列主机流量统计特征,有的把训练集和测试集拼在一起再随机打乱,还有的加了表头导致 Pandas 首行被当成列名。
解决:读取后立刻做两个断言,不通过就换数据源:
assert df.shape[1] == 42, "文件列数不是42,请检查数据版本" df["label"] = df["label"].str.strip().str.lower()标签列做一次小写清洗同样重要,有些版本的攻击名是Neptune.带小数点、有的是neptune,不统一的话映射表会漏匹配,直接把大量样本归进unknown。
6. 不只跑通:用交叉验证、特征重要性与宏平均把结果写进论文
6.1 用StratifiedKFold验证模型稳定性
单次随机切分的结果只有一次抽样的运气成分。我做完主实验后,一般会补一组五折分层交叉验证,用f1_macro作为评分函数,看模型在不同折上是否稳定。KDD99 数据量大,全量五折训练时间会拉长,可以只在重采样后的训练集上跑,也可以直接省略 SMOTE、用原始数据跑,目的不是刷分,是证明结论不依赖某一次随机划分。
from sklearn.model_selection import StratifiedKFold, cross_val_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) cv_scores = cross_val_score(rf, X_train_res, y_train_res, cv=cv, scoring="f1_macro", n_jobs=-1) print(cv_scores)输出五个 F1 值后,写论文时直接给均值和标准差,比单次测试集数字更有说服力,也顺手把“模型稳定性”这个维度补上了。注意shuffle=True和固定random_state缺一不可,否则折间样本分配不可复现。
6.2 特征重要性Top10如何产出
随机森林的feature_importances_在独热编码后天然可读,这是它对比神经网络最大的优势。收敛前先看这几列通常很稳:count、srv_count、dst_host_srv_count、same_srv_rate、dst_host_same_srv_rate、serror_rate,清一色是流量统计特征,说明 KDD99 里攻击行为主要暴露在连接聚合统计上,而不是单条连接内容里。
importance = pd.Series(rf.feature_importances_, index=X_train_res.columns) top10 = importance.sort_values(ascending=False).head(10) print(top10)这个 Top10 表格可以直接放进论文的特征分析章节,配合一句“模型主要依赖连接频率类特征识别攻击行为”的结论,比贴一堆模型公式更落地。如果 Top10 里混进了某个protocol_type_icmp之类的独热列,也不用大惊小怪,说明该取值和某个攻击类别高度绑定,这本身就是可写的发现。
6.3 给评委看的评估口径
最后的评估表我习惯按“四类攻击 + Normal”五行呈现,每行给出 precision、recall、F1,最后一行加上 Macro-F1。这个格式在毕业论文里几乎是标配,也方便和任何一篇 KDD99 相关论文做数值对比。写结论时不要只报“准确率 93%”,把 R2L/U2R 的 F1 提升幅度单独讲一段,因为稀有攻击才是这类数据集的难点。我从第一次只报准确率被问住的教训里学到的经验是:安全方向的项目,重点不是做得有多准,而是把“漏了什么、为什么漏、改进后漏得少了多少”讲清楚。希望这些步骤能帮你把这个经典项目做得比大多数人深一层。
本文还有配套的精品资源,点击获取