KDD-CUP99入侵检测实战:朴素贝叶斯、KNN与神经网络对比及避坑指南 简介这份资源围绕KDD-CUP99数据集用KNN、高斯贝叶斯、BP神经网络以及决策树四种方法实现入侵检测适合想入门机器学习分类实战的小白或进阶学习者也可作为毕设、课程设计、大作业或工程实训的参考方案。压缩包共51个文件约3.41MB包含6个Python源码文件、4个pkl与2个pth模型文件、1个csv数据集以及23张png结果图和9个html可视化页面覆盖训练、评估与结果展示的完整流程。作者从原始数据中抽取约8万条数据包测试集占20%交叉验证折数设为5并分别给出二分类与五分类下的准确率、混淆矩阵、ROC与P-R曲线等分析KNN还对比了K3至15的多种取值。目前已有93人学习读者可借此理解不同分类器在入侵检测任务上的表现差异掌握数据预处理、模型训练与性能评价的完整思路。1. 入侵检测这件事为什么我劝你先别急着上深度学习入侵检测Intrusion Detection这个词听起来很安全圈但落到工程上它本质上就是一个二分类或多分类问题给你一条网络连接记录判断它是正常流量还是某种攻击。KDD-CUP99 是这个领域最经典的公开数据集1999 年构建至今仍是很多课程设计和入门项目的首选。但我在实际带人做这个方向时发现一个反直觉的现象很多人一上来就堆神经网络结果被 KNN 和朴素贝叶斯按在地上摩擦。这不是说神经网络不行而是 KDD-CUP99 这个数据集有它的特殊性——特征维度高、类别极不平衡、大量符号型特征需要编码。如果你不先把数据摸清楚直接套一个全连接网络大概率得到的是一个在正常流量上准确率 99%、在 U2R 和 R2L 攻击上召回率接近 0 的“废模型”。这篇文章我会把贝叶斯、KNN、神经网络三条路线在同一个数据集上的完整落地流程拆开讲包括数据预处理、特征工程、模型调参、评估指标选择以及我在实际跑的时候踩过的那些坑。适合已经了解机器学习基础、想把这个经典数据集真正跑出可用结果的人。2. 先把 KDD-CUP99 读明白41 维特征里藏着什么2.1 数据集结构与三个容易忽略的细节KDD-CUP99 的每条记录包含 41 个特征外加一个标签。这 41 维可以分成四组TCP 连接的基本特征如 duration、protocol_type、service、flag、连接的内容特征如 hot、num_failed_logins、logged_in、基于时间的流量统计特征如 count、srv_count、serror_rate、基于主机的流量统计特征如 dst_host_count、dst_host_srv_count。标签分为五大类Normal、DoS、Probe、R2L、U2R。三个容易忽略的细节第一protocol_type、service、flag 是字符串类型必须编码第二标签在原始数据中是以攻击名称细分的比如 neptune、smurf 都属于 DoS你需要自己做映射第三训练集和测试集的攻击类型分布不一样测试集里有一些训练集没出现过的攻击子类这是 KDD-CUP99 故意设计的用来测试模型的泛化能力。我一般会先用 pandas 做一次快速探查确认列名、缺失值和标签分布。原始数据没有表头需要手动指定列名。import pandas as pd import numpy as np # KDD-CUP99 的 41 个特征列名 标签列 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, label ] train pd.read_csv(KDDTrain.txt, namescol_names) test pd.read_csv(KDDTest.txt, namescol_names) print(train.shape, test.shape) print(train[label].value_counts().head(15))这段代码做了三件事加载训练集和测试集、指定列名、查看标签分布。注意num_outbound_cmds这一列在 KDD-CUP99 中全部为 0可以直接删掉它不携带任何信息。另外is_host_login和is_guest_login也几乎全是 0但保留它们不会有害。2.2 标签映射与攻击大类合并原始标签有 23 种左右直接做多分类会导致某些类别样本极少模型根本学不动。常见做法是合并成五大类或者进一步合并成二分类Normal vs Attack。我建议先做五分类再根据业务需求决定是否降为二分类。# 攻击类型到大类的映射 attack_map { normal: Normal, # DoS back: DoS, land: DoS, neptune: DoS, pod: DoS, smurf: DoS, teardrop: DoS, apache2: DoS, udpstorm: DoS, processtable: DoS, worm: DoS, # Probe satan: Probe, ipsweep: Probe, nmap: Probe, portsweep: Probe, mscan: Probe, saint: Probe, # R2L guess_passwd: R2L, ftp_write: R2L, imap: R2L, phf: R2L, multihop: R2L, warezmaster: R2L, warezclient: R2L, spy: R2L, xlock: R2L, xsnoop: R2L, snmpguess: R2L, snmpgetattack: R2L, httptunnel: R2L, sendmail: R2L, named: R2L, # U2R buffer_overflow: U2R, loadmodule: U2R, rootkit: U2R, perl: U2R, sqlattack: U2R, xterm: U2R, ps: U2R } train[attack_type] train[label].map(attack_map) test[attack_type] test[label].map(attack_map) # 检查是否有未映射的标签 print(train[attack_type].isna().sum(), test[attack_type].isna().sum()) print(train[attack_type].value_counts())映射完成后一定要检查isna()的数量如果有未映射的标签说明你的字典漏了某些攻击名称。训练集中 R2L 和 U2R 的样本量非常少U2R 可能只有几十条这是后面模型评估时必须注意的问题。2.3 符号特征编码与数值特征标准化protocol_type、service、flag三列是字符串需要编码。常见做法有两种LabelEncoder 和 OneHotEncoder。service有 70 多个取值OneHot 会产生大量稀疏列我一般对protocol_type和flag用 OneHot对service用 LabelEncoder 或者按频率分组后再 OneHot。from sklearn.preprocessing import LabelEncoder, StandardScaler # 对 protocol_type 和 flag 做 OneHot train pd.get_dummies(train, columns[protocol_type, flag]) test pd.get_dummies(test, columns[protocol_type, flag]) # 对齐训练集和测试集的列 train, test train.align(test, joinleft, axis1, fill_value0) # 对 service 做 LabelEncoder le LabelEncoder() train[service] le.fit_transform(train[service]) # 测试集可能出现训练集没有的 service用 transform 会报错需要处理 test[service] test[service].map( lambda x: le.transform([x])[0] if x in le.classes_ else -1 ) # 数值特征标准化 num_cols train.select_dtypes(include[np.number]).columns.drop([label, attack_type], errorsignore) scaler StandardScaler() train[num_cols] scaler.fit_transform(train[num_cols]) test[num_cols] scaler.transform(test[num_cols])这里有个坑pd.get_dummies之后训练集和测试集的列可能不一致必须用align对齐。另外测试集里可能出现训练集没见过的service取值直接用transform会抛异常需要手动处理成 -1 或单独归为一类。标准化时注意label和attack_type列不能参与否则会报类型错误。3. 三条路线怎么选朴素贝叶斯、KNN、神经网络的真实对比3.1 朴素贝叶斯的假设与在 KDD-CUP99 上的表现朴素贝叶斯的核心假设是特征条件独立这在 KDD-CUP99 上显然不成立——src_bytes和dst_bytes明显相关count和srv_count也高度相关。但有趣的是即使假设不成立朴素贝叶斯在文本分类和高维稀疏数据上仍然表现不错。在 KDD-CUP99 上高斯朴素贝叶斯假设连续特征服从正态分布的训练速度极快适合做 baseline。from sklearn.naive_bayes import GaussianNB from sklearn.metrics import classification_report, confusion_matrix gnb GaussianNB() gnb.fit(train[num_cols], train[attack_type]) y_pred_nb gnb.predict(test[num_cols]) print(classification_report(test[attack_type], y_pred_nb)) print(confusion_matrix(test[attack_type], y_pred_nb))高斯朴素贝叶斯不需要调参直接 fit 就行。但你会看到它在 U2R 和 R2L 上的召回率非常低因为这两类样本太少先验概率被 Normal 和 DoS 压制。如果业务上更关心攻击召回率可以通过class_prior参数手动设置先验或者对少数类做上采样。3.2 KNN 的距离度量与 K 值选择KNN 在 KDD-CUP99 上的表现往往出人意料地好尤其是经过标准化之后。它的核心问题是K 值怎么选和距离度量怎么定。默认的欧氏距离在高维空间会失效维度灾难但 KDD-CUP99 经过特征选择后通常保留 15-20 维欧氏距离还能用。我一般会用一个简单的循环扫一遍 K 值。from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import f1_score best_k, best_f1 1, 0 for k in [1, 3, 5, 7, 9, 11, 15, 21]: knn KNeighborsClassifier(n_neighborsk, n_jobs-1) knn.fit(train[num_cols], train[attack_type]) pred knn.predict(test[num_cols]) f1 f1_score(test[attack_type], pred, averagemacro) print(fK{k}, macro-F1{f1:.4f}) if f1 best_f1: best_k, best_f1 k, f1 print(fBest K: {best_k})注意这里用的是macro-F1而不是 accuracy。因为 KDD-CUP99 类别极不平衡accuracy 会被 Normal 和 DoS 主导macro-F1 能更好反映少数类的表现。KNN 的另一个坑是预测速度慢测试集有 2 万多条K1 时每条都要算距离实际部署时需要考虑用 KD-Tree 或 Ball-Tree 加速或者做原型选择prototype selection压缩训练集。3.3 神经网络不是越深越好而是特征工程先做好神经网络在 KDD-CUP99 上要跑出好结果关键不在网络结构而在输入特征的质量。我见过太多人直接把 41 维原始特征丢进一个 3 层 MLP结果 U2R 召回率为 0。正确的做法是先做特征选择用随机森林或卡方检验筛掉冗余特征再对少数类做 SMOTE 过采样最后用一个浅层网络2-3 层就够了。import torch import torch.nn as nn from sklearn.model_selection import train_test_split from imblearn.over_sampling import SMOTE # 特征选择用随机森林的 feature_importances_ 筛前 20 维 from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier(n_estimators100, n_jobs-1, random_state42) rf.fit(train[num_cols], train[attack_type]) importances pd.Series(rf.feature_importances_, indexnum_cols) top_cols importances.nlargest(20).index.tolist() # 对训练集做 SMOTE 过采样 smote SMOTE(random_state42) X_res, y_res smote.fit_resample(train[top_cols], train[attack_type]) # 标签编码 from sklearn.preprocessing import LabelEncoder le_nn LabelEncoder() y_res_enc le_nn.fit_transform(y_res) y_test_enc le_nn.transform(test[attack_type]) # 转 tensor X_tensor torch.FloatTensor(X_res.values) y_tensor torch.LongTensor(y_res_enc) X_test_tensor torch.FloatTensor(test[top_cols].values) # 定义浅层 MLP class IDSModel(nn.Module): def __init__(self, input_dim, num_classes): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, num_classes) ) def forward(self, x): return self.net(x) model IDSModel(len(top_cols), len(le_nn.classes_)) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() # 训练循环 for epoch in range(50): model.train() optimizer.zero_grad() out model(X_tensor) loss criterion(out, y_tensor) loss.backward() optimizer.step() if (epoch 1) % 10 0: print(fEpoch {epoch1}, Loss: {loss.item():.4f})这段代码的关键点先做特征选择再过采样顺序不能反否则 SMOTE 会在冗余特征上生成噪声SMOTE 只在训练集上做测试集保持原始分布否则评估结果会虚高网络层数控制在 3 层以内KDD-CUP99 的样本量训练集约 12 万条撑不起太深的网络反而容易过拟合。4. 避坑指南我在 KDD-CUP99 上翻过的五次车4.1 坑一用 accuracy 评估模型看起来 99% 其实没用现象模型在测试集上 accuracy 达到 99.2%但实际部署后发现 U2R 攻击一条都检测不出来。原因KDD-CUP99 测试集中 Normal 和 DoS 占比超过 95%U2R 只有几十条。模型只要把所有样本都预测成 Normal 或 DoSaccuracy 就能很高。这是典型的类别不平衡陷阱。解决改用 macro-F1 或 per-class recall 作为主要评估指标。如果业务上对漏报攻击的容忍度低还要看 U2R 和 R2L 的召回率。我一般会同时打印 classification_report 和混淆矩阵确认每个类的表现。4.2 坑二测试集出现训练集没有的 service 取值编码直接报错现象用 LabelEncoder 对service编码后在测试集上transform抛出ValueError: y contains previously unseen labels。原因KDD-CUP99 的测试集故意包含了一些训练集没有的攻击类型和 service 取值这是为了测试泛化能力。LabelEncoder 的transform方法遇到未知类别会直接报错。解决用map加条件判断处理未知类别归为 -1 或单独一类。或者改用 OneHotEncoder 的handle_unknownignore参数。我一般会在编码前先检查测试集的service取值集合是否被训练集覆盖。4.3 坑三SMOTE 过采样后再标准化数据分布被破坏现象先做 SMOTE 再标准化模型在验证集上的表现波动很大每次运行结果差异明显。原因SMOTE 是基于距离生成新样本的如果特征没有标准化量纲大的特征会主导距离计算生成的样本质量很差。而且 SMOTE 后再标准化新样本的统计特性会和原始数据不一致。解决严格按「先标准化 → 再 SMOTE → 再训练」的顺序。标准化时用训练集的均值和方差测试集用同样的参数 transform。SMOTE 只在训练集上做且要在标准化之后。4.4 坑四KNN 的 K 值设为 1训练集噪声直接导致误报现象KNN 在 K1 时训练集准确率 100%但测试集上误报率很高正常流量被大量判为攻击。原因K1 时模型完全拟合训练集包括噪声和异常点。KDD-CUP99 的训练集本身就有标注错误和重复样本K1 会把这些错误直接带到预测中。解决K 值至少设为 3一般 5-7 比较合适。同时可以配合距离加权weightsdistance让近邻的投票权重更高。如果训练集很大考虑先做原型选择压缩训练集去掉冗余样本。4.5 坑五神经网络学习率设太大loss 震荡不收敛现象训练时 loss 在前几个 epoch 下降然后开始剧烈震荡甚至变成 NaN。原因Adam 的默认学习率 1e-3 对某些初始化比较敏感尤其是输入特征没有做标准化或者存在极端值时梯度爆炸会导致 loss 发散。解决先把学习率降到 1e-4 试试同时加梯度裁剪torch.nn.utils.clip_grad_norm_。另外检查输入特征是否有 inf 或 nan标准化之前先做np.nan_to_num处理。如果还是震荡换用 SGD momentum 往往更稳定。5. 把模型推到可用阈值调优与集成技巧前面三条路线各自跑通之后真正决定能不能落地的是后处理。我一般会做两件事一是对神经网络和 KNN 的输出概率做阈值调优二是把三个模型的预测结果做投票集成。先看阈值调优。以神经网络为例模型输出的是每个类的 softmax 概率默认取 argmax。但如果你更关心 U2R 的召回率可以手动降低 U2R 的判定阈值。model.eval() with torch.no_grad(): logits model(X_test_tensor) probs torch.softmax(logits, dim1).numpy() # 默认 argmax pred_default probs.argmax(axis1) # 手动调阈值U2R 的索引假设是 3降低它的判定门槛 u2r_idx list(le_nn.classes_).index(U2R) adjusted probs.copy() adjusted[:, u2r_idx] * 1.5 # 放大 U2R 概率 pred_adjusted adjusted.argmax(axis1) from sklearn.metrics import recall_score print(Default U2R recall:, recall_score(y_test_enc, pred_default, labels[u2r_idx], averagemacro)) print(Adjusted U2R recall:, recall_score(y_test_enc, pred_adjusted, labels[u2r_idx], averagemacro))这个放大系数需要根据验证集上的 precision-recall 曲线来定一般从 1.2 开始试观察 U2R 召回率提升和整体误报率之间的权衡。如果业务上误报成本很高就不要动阈值宁可漏报。再说集成。三个模型的预测结果可以做硬投票或软投票。硬投票是每个模型投一票取多数软投票是平均概率后取 argmax。我一般用软投票因为概率包含了更多信息。from sklearn.ensemble import VotingClassifier # 重新用 sklearn 接口包装三个模型 gnb_sk GaussianNB() knn_sk KNeighborsClassifier(n_neighbors5, n_jobs-1) # 神经网络用 sklearn 的 MLPClassifier 替代方便集成 from sklearn.neural_network import MLPClassifier mlp_sk MLPClassifier(hidden_layer_sizes(64, 32), max_iter200, random_state42) voting VotingClassifier( estimators[(nb, gnb_sk), (knn, knn_sk), (mlp, mlp_sk)], votingsoft ) voting.fit(train[top_cols], train[attack_type]) y_pred_vote voting.predict(test[top_cols]) print(classification_report(test[attack_type], y_pred_vote))软投票要求每个模型都能输出概率GaussianNB、KNN、MLPClassifier 都支持predict_proba。集成的效果通常比单模型好 1-3 个百分点尤其是在少数类上。但要注意如果某个模型在某个类上表现特别差软投票会被它拖累这时候可以考虑加权投票给表现好的模型更高权重。最后一个技巧保存预处理管道。实际部署时你不能每次预测都重新 fit 标准化器和编码器必须把scaler、le、top_cols一起保存下来。import joblib joblib.dump({ scaler: scaler, label_encoder: le, top_cols: top_cols, model: voting }, ids_pipeline.pkl) # 加载时 pipeline joblib.load(ids_pipeline.pkl)这个习惯是我踩了无数次坑之后养成的——训练时用一套预处理部署时忘了同步结果线上预测结果和离线评估完全对不上。把预处理和模型打包成一个文件能省掉很多排查时间。希望帮到你。本文还有配套的精品资源点击获取