
简介面向机器学习与网络安全的入门及进阶学习者这套基于KDD CUP99数据集的入侵检测实战资源整合了KNN、高斯贝叶斯、BP神经网络与决策树四种分类方法。项目从原始数据集中抽取约8万条样本统一完成训练与测试并分别输出二分类和五分类结果采用5折交叉验证评估可直接运行 classify.py 复现实验适合毕业设计、课程设计或工程实训参考。压缩包共51个文件、3.41MB主要包含Python源码、PNG结果图、HTML可视化报告、模型权重文件pkl/pth、CSV数据文件与Markdown说明文档既有完整可运行代码也有训练后的模型存档和混淆矩阵、ROC曲线等图表便于对照学习与二次开发。已有93人学习下载适合希望快速掌握KDD CUP99预处理、分类器对比及性能评估流程的读者。1. 从规则库漏报到三类ML模型入侵检测为什么绕不开贝叶斯、KNN和神经网络真实环境里安全运营最烦的不是攻击太隐蔽而是规则库越滚越大、误报越来越多新变种一到又得连夜加规则。做入侵检测之所以绕不开机器学习路线就是要让模型从流量或审计数据里自己总结“攻击长什么样”。KDD-CUP99是这个领域最经典的评测数据集贝叶斯、KNN、神经网络恰好代表了概率推断、近邻学习和端到端非线性拟合三条不同路径。这篇文章不堆算法推导按我实际跑通这条实验链路的路子来数据清洗、模型实现、参数调优、避坑记录最后给横向对比和集成验证。适合正在做安全方向课设、毕设或产品原型的工程师参考。2. KDD-CUP99 数据工程41维特征、标签编码与训练测试集划分2.1 字段结构与攻击类别映射KDD-CUP99 来自1999年前后的入侵检测评估项目数据是模拟局域网上抓取的一批网络连接记录每条连接被打上“正常”或具体攻击名。做实验时一般用它的 10% 训练子集完整版本数据量太大跑 KNN 这种惰性学习的模型会非常痛苦。每条记录是一行逗号分隔的字段前 41 列是特征最后一列是标签。特征分成四组第一组是连接基本属性比如 duration、protocol_type、service、flag、src_bytes、dst_bytes第二组是连接内容属性比如登录失败次数、root_shell、su_attempted 等第三组和第四组分别是基于时间和基于主机的统计特征比如 count、serror_rate、dst_host_srv_count。离散特征主要是 protocol_type、service、flag其余大多是数值型还有少量取值只有 0/1 的二元特征。这类混合结构决定了后面不能用同一套预处理方式离散特征要做编码长尾数值特征要变换连续特征要标准化。标签层先把攻击名归并成五类正常 normal、拒绝服务 DoS、探测 Probe、远程到本地 R2L、本地提权 U2R。这是 KDD-CUP99 评测的常规世界观所有模型最终都在这个五分类上比。读数据时留意一点原始标签列带点号后缀比如 “normal.” “neptune.”需要先清理。2.2 读取数据、映射标签与训练测试切分的代码常见做法是直接用 pandas 读文本自己补列名。把压缩包解压后的 kddcup.data_10_percent.txt 放到当前目录然后执行import pandas as pd import numpy as np from sklearn.model_selection import train_test_split col_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 ] df pd.read_csv(kddcup.data_10_percent.txt, headerNone, namescol_names [label]) df[label] df[label].str.replace(., , regexFalse) attack_map { normal: normal, neptune: dos, smurf: dos, back: dos, teardrop: dos, pod: dos, land: dos, satan: probe, portsweep: probe, ipsweep: probe, nmap: probe, guess_passwd: r2l, ftp_write: r2l, imap: r2l, phf: r2l, multihop: r2l, warezmaster: r2l, warezclient: r2l, spy: r2l, buffer_overflow: u2r, loadmodule: u2r, perl: u2r, rootkit: u2r } df[attack_type] df[label].map(attack_map) df df[df[attack_type].notna()] print(df[attack_type].value_counts())这里的 attack_map 把具体攻击名归并成五类。map 之后如果出现 NaN说明数据里存在本次映射没有覆盖的攻击名直接 dropna 过滤掉避免把未知类带进训练。value_counts 打印出来后你会发现类别分布极度不均衡normal、neptune、smurf 占了大头u2r 可能只有几十条。接下来做特征编码和切分。一个关键原则是先切分再做任何编码和标准化否则测试集信息会提前进入训练流程后面模型指标全是虚的。train_df, test_df train_test_split( df, test_size0.2, random_state42, stratifydf[attack_type] ) cat_cols [protocol_type, service, flag] num_cols [c for c in col_names if c not in cat_cols] X_train pd.get_dummies( train_df.drop(columns[label, attack_type]), columnscat_cols ) X_test pd.get_dummies( test_df.drop(columns[label, attack_type]), columnscat_cols ) X_test X_test.reindex(columnsX_train.columns, fill_value0) # 长尾特征做 log1p减小极端值对距离和梯度的影响 for col in [src_bytes, dst_bytes]: X_train[col] np.log1p(X_train[col]) X_test[col] np.log1p(X_test[col]) y_train train_df[attack_type] y_test test_df[attack_type] from sklearn.preprocessing import StandardScaler scaler StandardScaler().fit(X_train) X_train_s scaler.transform(X_train) X_test_s scaler.transform(X_test)get_dummies 先把三个离散列展开成多列service 这个字段取值非常多展开后特征维度会到三四百这是正常现象。测试集 reindex 对齐训练集列遇到没见过的 service 值补 0这是处理新类别最常见的做法。src_bytes、dst_bytes 偏态严重最大值能到几千万不变换的话 KNN 的距离计算会被这两列直接主导。StandardScaler 只 fit 训练集测试集用同一个 scaler 的 transform这是基线实验里必须守住的红线。到这里数据工程结束得到的 X_train_s 和 X_test_s 进入任何机器学习模型都不需要再做额外处理。接下来的 KNN、朴素贝叶斯、神经网络都吃这份数据。3. KNN 与朴素贝叶斯的最小可运行实现距离度量、k值和概率平滑把 KNN 和朴素贝叶斯放在一起讲是因为它们代表了同一种工程思维的两种极端KNN 不假设特征分布纯靠样本间距离说话朴素贝叶斯则做了很强的条件独立假设用先验和似然相乘算概率。两者都在几分钟内能跑出基线结果非常适合当作入侵检测实验的起点。3.1 KNN 的两个关键参数k 值与距离度量KNN 在训练阶段几乎不花时间它只是把样本存下来真正的计算全部发生在预测阶段。新来一条连接记录时模型在特征空间里找最近的 k 个邻居让邻居投票决定它是 normal、dos、probe、r2l 还是 u2r。这套逻辑的问题在于KDD-CUP99 的样本量在十万级特征维度几百维预测一条样本要跟所有训练样本算一遍距离代价不低。我一般会先调三个参数k 值、weights 模式、距离度量。k 太小容易把单个噪声点当成决定项k 太大又会让相距很远的样本也参与投票。weights 有两个常用选项uniform 是周围 k 个邻居一视同仁distance 是按距离倒数为近邻加权。类别不均衡时uniform 很容易被数量占优的 dos 邻居带偏distance 会让更近的少数类样本有更大话语权。metric 默认用欧氏距离特征经过标准化后基本够用曼哈顿距离在某些高维场景下更稳但实测差距不大。from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report knn KNeighborsClassifier( n_neighbors7, weightsdistance, metriceuclidean, algorithmbrute_force, n_jobs-1 ) knn.fit(X_train_s, y_train) # 测试集太大时 KNN predict 会很慢先抽 10000 条评估 X_test_eval X_test_s[:10000] y_test_eval y_test.iloc[:10000].values y_pred_knn knn.predict(X_test_eval) print(classification_report(y_test_eval, y_pred_knn, digits4))algorithm 参数值得单独说。sklearn 默认 auto内部会自动判断用 KD 树还是暴力计算。特征维度在几百维时 KD 树的加速效果会很差甚至自动退化成 brute_force所以我直接显式指定 brute_force配合 n_jobs-1 让多核并行算距离代码简单速度也可控。k 值我会按 1、3、5、7、9、15 扫一遍观察测试集上的宏平均 F1而不是只看 overall accuracy。k1 在训练集上几乎完美测试集上震荡明显k 在 5 到 15 之间的结果是相对稳的。调参时可以只抽三五千条样本快速试确定再全量预测。KNN 还有一个容易被忽略的点是特征维度的解释性。模型在几百维空间里找到的“近邻”很难直接回答“为什么这条是攻击”这对安全场景的告警解释是个短板。KNN 的优势是实现在所有基线模型里最简单且不需要任何分布假设适合先拿来做通道验证。3.2 朴素贝叶斯的条件独立假设与零概率平滑朴素贝叶斯对入侵检测的意义在于它给每条告警一个概率值而不只是分类结果。它的公式底子是贝叶斯定理P(攻击类型 | 特征) 等于先验 P(攻击类型) 乘上各个特征的条件概率 P(特征 | 攻击类型)再除以归一化常数。朴素两个字来自条件独立假设——它假定特征之间互不影响。真实网络流量里这个假设几乎不成立比如 serror_rate 和 count 明显相关所以理论上它不如神经网络能刻画复杂关系。但在入侵检测场景里它仍然值得跑因为训练快、预测快、概率输出天然适合做阈值告警。from sklearn.naive_bayes import GaussianNB gnb GaussianNB(var_smoothing1e-9) gnb.fit(X_train_s, y_train) # 预测时取出各类别概率方便后面调报警阈值 y_prob_gnb gnb.predict_proba(X_test_s) y_pred_gnb gnb.predict(X_test_s) print(gnb.classes_)GaussianNB 是 Sklearn 里处理连续特征的朴素贝叶斯实现它假设每个特征在每个类别下服从高斯分布用训练数据估计均值和方差。前面的 log1p 变换加标准化正好让 src_bytes、dst_bytes 这类长尾特征更接近高斯这是影响贝叶斯效果的关键一步。var_smoothing 是最值得调的参数它会在每个特征的方差估计上叠加一个极小值避免某些类别里某个特征完全不变导致方差为 0、概率直接爆炸。默认值通常够用但训练数据里某一类样本特别少时比如 u2r 只有几十条把 var_smoothing 调到 1e-6 能减少数值抖动。如果遇到概率输出出现极端 0 和 1 的情况优先检查这个参数。predict_proba 输出的概率可以当成置信度来用。实际做自适应入侵检测时我不会只用 argmax 取类别而是对每个类别设一个置信度门槛比如检测到 r2l 的概率超过 0.6 才告警低于门槛的样本继续观察。这个思路就是自适应入侵检测的朴素实现。想更进一步可以去看动态贝叶斯网络或稀疏贝叶斯学习前者把时间片之间的状态转移建模进去后者在高维特征里做自动选择但在这份数据集上作为基线朴素贝叶斯已经足够。4. 神经网络入侵检测前馈网络的结构设计、训练与过拟合排查4.1 输入输出设计和隐藏层规模怎么定标题里的神经网络最直接的落地实现是前馈神经网络也就是 BP 神经网络结构图里最常见的那种全连接结构。它不要求特征之间满足特定分布也不依赖样本间的距离假设而是通过多层非线性变换拟合决策边界。对 KDD-CUP99 这种单条连接记录的表格型数据前馈网络是比 CNN、LSTM、图神经网络更自然的起点。LSTM 和 RNN 需要输入具备时间序列结构一维卷积神经网络需要把特征组织成连续窗口而这些数据每行就是一条独立连接硬套序列模型反而会增加不必要的预处理复杂度。输入维度就是编码后的列数取决于 one-hot 后 service 展开了多少列通常在三百到四百之间。输出固定为 5 个类别最后一层用 softmax 得到概率分布。隐藏层我先给一个可复现的两层结构第一层 128 个神经元ReLU 激活接 BatchNormalization 和 Dropout第二层 64 个神经元同样接归一化和 Dropout最后接 5 节点的 softmax 输出层。神经元数量不需要一上来就给很大数据十万量级、输入几百维两层全连接足以学出有效的决策面。先跑小网络再根据训练曲线决定是否加宽加深这是我最常用的方式。如果之后想把方向往序列模型延伸可以考虑把每个源 IP 的连接记录按时间窗口重组形成会话序列再喂给 LSTM 或一维卷积神经网络。那就完全是另一套特征工程了KDD-CUP99 原始每条记录独立排列不补充会话聚合信息模型看不到时序上下文。4.2 用 Keras 搭建前馈网络模型代码与训练参数先把标签转成整数编码神经网络里 sparse_categorical_crossentropy 接收的是整数类编号不需要手动做 one-hot 编码。然后定义模型结构并训练from sklearn.preprocessing import LabelEncoder from tensorflow import keras from tensorflow.keras.layers import Dense, Dropout, BatchNormalization from tensorflow.keras.callbacks import EarlyStopping le LabelEncoder() y_train_enc le.fit_transform(y_train) y_test_enc le.transform(y_test) model keras.Sequential([ keras.Input(shape(X_train_s.shape[1],)), Dense(128, activationrelu), BatchNormalization(), Dropout(0.3), Dense(64, activationrelu), BatchNormalization(), Dropout(0.3), Dense(5, activationsoftmax) ]) model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) early_stop EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ) history model.fit( X_train_s, y_train_enc, validation_split0.15, batch_size512, epochs50, callbacks[early_stop], verbose1 )这个代码块里的几个参数需要解释。BatchNormalization 放在 Dropout 之前作用是把每层输出拉回到均值为 0、方差为 1 的分布里网络训练会更稳学习率可以给得稍微大一点。Dropout 0.3 表示每轮训练随机丢弃 30% 的神经元输出这是防止全连接网络过拟合最有效的单点操作。validation_split0.15 是从训练集尾部再切 15% 当验证集注意它是从切分完的 X_train_s 里继续切不碰测试集。EarlyStopping 盯的是 val_losspatience 设为 5意思是连续 5 轮验证损失不下降就停并且恢复成历史最优权重这是防止神经网络越练越偏的后悔药机制。训练结束后用测试集评估时要注意 label 的编码顺序。LabelEncoder 是按字母排序的sklearn 里 KNN 和贝叶斯的 classes_ 也是排序后的类别名三者顺序一致后面做模型集成时概率才能直接相加。4.3 训练曲线判读什么迹象说明网络在过拟合模型训练不是 fit 完就结束我每次都会把 history 里的训练损失和验证损失拉出来看。正常状态是两条曲线一起下降然后走平如果训练损失还在降验证损失却开始掉头向上这就是过拟合的经典信号。全连接网络参数量大KDD-CUP99 里 u2r、r2l 类别样本又少网络很容易把少数类样本直接记住而不是学出泛化模式。遇到这种情况我的调整顺序是先加大 Dropout 到 0.4 甚至 0.5再看要不要减小网络的宽度或层数最后才考虑降低学习率。不要一上来就加数据也不要盲目加层。另一个常用的观察指标是验证集上的宏平均 F1而不是 accuracy。因为整体准确率在类别不平衡的数据上非常容易虚高模型全预测成 normal 都能拿到九成以上只有分类别看 F1 才能找到模型真正弱在哪一类。神经网络在这种任务上往往能拿到比其他两个模型更高的整体 F1但它的缺点是训练过程有随机性不同随机种子结果波动明显调参也更依赖经验。遇到同样代码两次结果不同不用奇怪先固定随机种子再用多次训练取平均来评估。5. 避坑记录KDD-CUP99 实验里最容易翻车的五件事5.1 数据泄漏标准化拟合了全量数据比较常见的翻车是把缩放器和编码器放在切分之前对整个数据集 fit 了一遍。现象是训练集和测试集准确率都很高但把模型拿到新流量上一测效果马上掉下来。原因是 scaler 在 fit 全量数据时已经偷偷看到了测试集的统计信息测试集不再具有“未知数据”的意义。处理方式很简单先 train_test_split再用训练集 fit scaler后续所有测试集操作都只调 transform。特征编码同理get_dummies 不涉及统计量泄漏但列对齐必须发生在测试集上。5.2 样本不均衡U2R 样本太少导致准确率虚高KDD-CUP99 的类别分布天然是一边倒的neptune 和 smurf 这类 DoS 攻击占了极大比例u2r 样本少到可以忽略。现象是模型整体 accuracy 能到 99%但看 u2r 的召回率可能只有 0.1。原因是分类器学会了把绝大多数样本判成 normal 或 dos 来压低损失少数类根本得不到有效的学习信号。处理方式是先 stratify 切分保证训练和测试里类别比例一致然后评估时一定看按类别的召回率和宏平均 F1必要时候对 dos 大类做下采样或者复制少数类样本做上采样。神经网络训练时还可以给每个类别配 class_weight让少数类的损失权重更高。5.3 同源会话被随机切分验证集指标虚高现象是随机切分后模型在测试集上表现不错部署到真实网络里却大跌眼镜。原因是 KDD-CUP99 里同一个 IP 的连续连接记录在特征上高度相似随机切分会让这些相似样本一部分进训练集、一部分进测试集模型等于在“背答案”。KNN 和神经网络对这种泄漏尤其敏感。处理方式是按连接会话或源 IP 分组建模先按 src 字段聚合再把整个组划到训练或测试的同一侧。没有现成的 session id 时我一般会按源 IP 加时间窗口构造分组维度再基于组做切分。牺牲一点训练样本量换来的是接近真实分布的评估。5.4 离散特征直接喂给 KNNone-hot 距离被稀疏列主导现象是 KNN 在 one-hot 编码后的数据上预测很慢近邻解释起来也很怪。原因是 service 字段展开后产生了几十个稀疏列欧氏距离计算被这些 0/1 列主导真正的连续特征反而失声了。处理方式是混合特征时不要一刀切连续特征做标准化离散特征如果一定要 one-hot可以考虑对距离度量做折中或者把连续特征和离散特征拆开算加权距离。更省事的做法是只对 KNN 使用数值主导的特征子集把离散特征作为附加判断条件。这个坑在贝叶斯模型里影响相对小因为概率模型本身就是在条件概率上工作稀疏列不会通过距离传染。5.5 朴素贝叶斯遇上零概率训练集没见过的组合直接预测为 0现象是某条测试样本的特征组合在训练数据里没有出现过predict_proba 输出里某一类的概率直接变成 0。原因是朴素贝叶斯把所有特征的条件概率相乘任何一个条件概率为 0 都会让整个乘积归零。处理方式是给概率估计加平滑高斯朴素贝叶斯用 var_smoothing离散特征的变体则用拉普拉斯平滑参数 alpha。另一个更长远的处理是在特征工程阶段多做一步先观察每个特征在各类别下的分布形态对稀疏区块的类别做合并。6. 让模型投入验证混淆矩阵、交叉验证与三类模型的集成投票6.1 按攻击类别看混淆矩阵不只看准确率模型都跑完后我最先看的是测试集上的混淆矩阵不是 accuracy。用 sklearn 的 classification_report 打印每个类别的 precision、recall、f1-score重点盯三类probe 能不能从 normal 里分出来r2l 和 u2r 是不是几乎全军覆没。一般经验是 doS 和 probe 好分r2l、u2r 因为训练样本太少三个模型在这里都会明显退化。发现这一点不要太早下结论说模型不行先做少数类增强再重新对比这属于算法的正常规律。6.2 集成策略概率平均在 KDD-CUP99 上最稳三个模型决策边界完全不同集成通常会提升稳定性。我一般不用硬投票而是做概率平均把 KNN、朴素贝叶斯、神经网络三者的 predict_proba 结果按类别取平均再取 argmax。KNN 和贝叶斯互补性最强一个重局部距离一个重全局概率分布集成后往往比单个模型更稳。代码上很简单ensemble_prob ( knn.predict_proba(X_test_eval) gnb.predict_proba(X_test_s[:10000]) nn_prob[:10000] ) / 3 y_pred_ensemble np.argmax(ensemble_prob, axis1)这里有个前提三个模型输出的类别顺序必须一致。sklearn 的 classes_ 和 LabelEncoder 都是按字母排序所以 normal、dos、probe、r2l、u2r 的顺序是固定的。如果不放心在集成前打印一遍各模型的 classes_ 做核对。概率平均的另一个好处是能直接把这个平均概率当成告警分数接入到自适应入侵检测的阈值体系里。6.3 迁移到真实流量前先解决特征对齐从 KDD-CUP99 拿到的模型不能直接上生产核心问题是特征对齐。真实网络流量没有现成的 conn 记录表需要自己把 netflow 或全量抓包转成同样的 41 维统计特征尤其是 count、serror_rate 这类依赖时间窗口的统计量必须按连接会话重新聚合。我踩过一次坑直接拿历史训练好的模型跑真实流量特征对不上召回率低到没法看最后只能回炉重做特征工程。从那以后我每次搭入侵检测原型都先花时间写特征对齐脚本再谈模型调参。希望这个顺序能帮到你少走一段弯路。本文还有配套的精品资源点击获取