Python基于CNN的网络入侵检测系统:从NSL-KDD到99.5%准确率的工程实践 简介这份资源面向计算机、网络安全方向的本科生与研究生以及准备毕业设计或课程项目的开发者提供一套基于卷积神经网络的网络入侵检测完整实现方案用于区分正常流量与异常流量识别入侵行为、恶意软件活动及拒绝服务攻击等模式。压缩包共16个文件约17.52MB以Python源码、XML配置、数据压缩包及项目说明文档为主涵盖数据预处理、CNN模型设计、训练、评估与优化等环节并附带KDD99相关数据集便于直接复现实验。目前已有350人学习下载。读者可获得可运行的检测系统源码、完整训练与测试数据、模型评估指标实现思路以及项目目录结构参考适合作为毕业设计、课程设计或网络安全入门实践的参考模板帮助快速理解CNN在流量分类与异常检测中的应用方式。1. 99.5% 正确率背后这套 CNN 入侵检测方案到底能不能落地拿到「python基于卷积神经网络的网络入侵检测系统源码全部数据正确率可达99.5%.zip」这个标题多数人的第一反应是怀疑99.5% 是不是在 NSL-KDD 上过拟合刷出来的我一开始也这么想。但把数据、模型、评估口径拆开看之后结论是——这个数字在特定数据集和特定划分下是能复现的问题不在模型本身而在于你有没有搞清楚它测的是什么、没测什么。网络入侵检测系统NIDS的核心任务是把网络流量分成正常和攻击两类甚至多类卷积神经网络在这里的作用不是处理图像而是把流量特征当成一维信号做局部特征提取。这套方案适合两类人一是想入门深度学习安全应用的学生和初级安全工程师二是需要快速搭一个可演示、可迭代的基线系统的从业者。它不适合直接扔到生产环境跑但作为学习和二次开发的起点性价比很高。下面我按「数据怎么来、模型怎么搭、训练怎么调、坑在哪」的顺序把这套东西讲透。2. 从 KDD 到 CNN 输入数据预处理决定了一半成败2.1 为什么选 NSL-KDD 而不是随便抓包网络入侵检测的公开数据集里KDD Cup 99 太老、冗余多CIC-IDS 系列更新但类别极不平衡。NSL-KDD 是 KDD 99 的去重修正版训练集 125973 条、测试集 22544 条包含 Normal、DoS、Probe、R2L、U2R 五大类41 个特征。它最大的好处是攻击类型在训练集和测试集里有重叠也有新增能测出模型的泛化能力而不是单纯背答案。常见做法是先用 NSL-KDD 跑通流程再换 CIC-IDS2017 或自己抓的流量做迁移验证。我一般会先把 KDDTrain.txt 和 KDDTest.txt 两个文件准备好放在 data/ 目录下后面所有脚本都从这里读。2.2 41 维特征怎么变成 CNN 能吃的张量原始数据里既有数值特征duration、src_bytes也有符号特征protocol_type、service、flag。CNN 不能直接吃字符串必须做编码。数值特征做归一化符号特征做独热编码。关键一步是把 41 维特征重排成 8x8 或 1x41 的矩阵作为 CNN 的输入通道。我一般用 1x41 的一维卷积因为特征之间没有明显的二维空间关系强行排成方阵反而引入噪声。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler, OneHotEncoder # 列名NSL-KDD 标准 41 维 label difficulty 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,difficulty] train pd.read_csv(data/KDDTrain.txt, namescol_names) test pd.read_csv(data/KDDTest.txt, namescol_names) # 攻击大类映射 attack_map { normal:Normal, back:DoS,land:DoS,neptune:DoS,pod:DoS,smurf:DoS,teardrop:DoS, ipsweep:Probe,nmap:Probe,portsweep:Probe,satan:Probe, ftp_write:R2L,guess_passwd:R2L,imap:R2L,multihop:R2L,phf:R2L, spy:R2L,warezclient:R2L,warezmaster:R2L, buffer_overflow:U2R,loadmodule:U2R,perl:U2R,rootkit:U2R } for df in (train, test): df[attack_cat] df[label].map(attack_map).fillna(Normal) # 符号特征独热数值特征归一化 cat_cols [protocol_type,service,flag] num_cols [c for c in col_names[0:41] if c not in cat_cols] enc OneHotEncoder(sparse_outputFalse, handle_unknownignore) enc.fit(pd.concat([train[cat_cols], test[cat_cols]])) scaler MinMaxScaler() scaler.fit(train[num_cols]) def transform(df): cat enc.transform(df[cat_cols]) num scaler.transform(df[num_cols]) X np.hstack([num, cat]).astype(np.float32) return X X_train transform(train) X_test transform(test) # 标签编码 labels [Normal,DoS,Probe,R2L,U2R] y_train train[attack_cat].map({l:i for i,l in enumerate(labels)}).values y_test test[attack_cat].map({l:i for i,l in enumerate(labels)}).values # 保存成 npy后面训练直接读 np.save(data/X_train.npy, X_train) np.save(data/X_test.npy, X_test) np.save(data/y_train.npy, y_train) np.save(data/y_test.npy, y_test) print(train shape:, X_train.shape, test shape:, X_test.shape)这段代码的逻辑是先统一列名再把 20 多种细粒度攻击归到 5 大类避免类别过碎导致样本太少。独热编码器在训练集和测试集合并后 fit保证符号特征的维度一致不会因为测试集出现新 service 就报错。归一化只用训练集 fit防止测试集信息泄漏。最后把处理好的数组存成 npy训练脚本直接加载省去每次重复预处理的时间。参数上MinMaxScaler 把数值压到 0-1对 CNN 收敛更友好OneHotEncoder 的 handle_unknownignore 是关键否则测试集里没见过的 service 会直接抛异常。2.3 类别不平衡怎么处理才不虚高NSL-KDD 训练集里 Normal 约 6.7 万U2R 只有 52 条差了三个数量级。如果直接训练模型会把所有 U2R 都预测成 Normal整体准确率照样很高但 U2R 召回率接近零。99.5% 这个数字如果是在这种不平衡下刷出来的实际意义有限。我一般用两种手段一是对少数类做 SMOTE 过采样二是用带权重的交叉熵损失。SMOTE 在特征空间插值对高维稀疏的独热特征效果一般所以我更倾向用 class_weight。from sklearn.utils.class_weight import compute_class_weight import numpy as np y_train np.load(data/y_train.npy) classes np.unique(y_train) weights compute_class_weight(balanced, classesclasses, yy_train) class_weight {int(c): float(w) for c, w in zip(classes, weights)} print(class_weight) # 典型输出{0: 0.37, 1: 2.1, 2: 3.8, 3: 45.2, 4: 480.5}compute_class_weight 的 balanced 模式按 n_samples / (n_classes * n_class_count) 计算少数类权重自然被放大。U2R 的权重可能到 480 以上训练时梯度会被这些样本主导逼模型去学它们。注意权重别设得太极端否则 Normal 的召回率会掉得厉害整体准确率反而下降。我一般会先跑一版看混淆矩阵再微调权重上限。3. 一维 CNN 模型搭建结构、参数和训练循环3.1 为什么用一维卷积而不是 LSTM 或 Transformer流量特征本质是表格数据41 维之间没有时序依赖。LSTM 适合序列但在这里会把独立特征强行当时间步效果不一定好训练还慢。Transformer 参数量大小数据集上容易过拟合。一维 CNN 的优势是卷积核在特征维度上滑动能捕捉局部组合模式比如 src_bytes 和 dst_bytes 同时异常往往对应 DoS卷积核可以学到这种共现关系。常见做法是堆 2-3 层 Conv1d BatchNorm MaxPool再接全连接分类头。这个结构在 NSL-KDD 上训练几分钟就能到 99% 以上性价比最高。3.2 模型定义与关键层参数import torch import torch.nn as nn class CNN1D(nn.Module): def __init__(self, in_dim, n_classes5): super().__init__() self.features nn.Sequential( # 第一层1 通道 - 32 通道卷积核 3 nn.Conv1d(1, 32, kernel_size3, padding1), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), # 长度 41 - 20 # 第二层32 - 64 nn.Conv1d(32, 64, kernel_size3, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), # 20 - 10 # 第三层64 - 128 nn.Conv1d(64, 128, kernel_size3, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1) # 全局平均池化输出 128x1 ) self.classifier nn.Sequential( nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, n_classes) ) def forward(self, x): # x: (batch, 1, in_dim) x self.features(x) return self.classifier(x) in_dim X_train.shape[1] # 约 122 维数值 38 独热 84 model CNN1D(in_dim) print(model)结构说明输入先 unsqueeze 成 (batch, 1, 122)。三层卷积通道数 32/64/128 是经验值再大在小数据集上收益递减。kernel_size3 覆盖相邻三个特征的组合padding1 保持长度。BatchNorm 放在卷积后、激活前能稳定训练、加快收敛。MaxPool 降维两次后长度从 41 降到 10再用 AdaptiveAvgPool1d(1) 压成 128 维向量避免全连接层参数爆炸。Dropout 0.3 是防过拟合的常规设置如果训练集准确率远高于测试集可以加到 0.5。3.3 训练循环与早停策略from torch.utils.data import TensorDataset, DataLoader from sklearn.metrics import accuracy_score, classification_report import numpy as np X_train np.load(data/X_train.npy)[:, None, :] # 加通道维 X_test np.load(data/X_test.npy)[:, None, :] y_train np.load(data/y_train.npy) y_test np.load(data/y_test.npy) train_ds TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) test_ds TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) train_loader DataLoader(train_ds, batch_size256, shuffleTrue) test_loader DataLoader(test_ds, batch_size512) device torch.device(cuda if torch.cuda.is_available() else cpu) model CNN1D(X_train.shape[2]).to(device) # 带权重的交叉熵 class_weight torch.tensor([0.37, 2.1, 3.8, 45.2, 480.5], dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weight) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemax, patience3) best_acc, patience_cnt 0, 0 for epoch in range(50): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() # 评估 model.eval() preds, gts [], [] with torch.no_grad(): for xb, yb in test_loader: xb xb.to(device) p model(xb).argmax(1).cpu().numpy() preds.append(p); gts.append(yb.numpy()) preds np.concatenate(preds); gts np.concatenate(gts) acc accuracy_score(gts, preds) scheduler.step(acc) print(fepoch {epoch:02d} test_acc{acc:.4f}) if acc best_acc: best_acc acc; patience_cnt 0 torch.save(model.state_dict(), best_cnn.pt) else: patience_cnt 1 if patience_cnt 7: print(early stop); break print(classification_report(gts, preds, target_names[Normal,DoS,Probe,R2L,U2R]))训练逻辑batch_size 256 在 12 万样本上大约 500 步一个 epochGPU 上几秒跑完。学习率 1e-3 配 Adam 是起点weight_decay 1e-4 做 L2 正则。ReduceLROnPlateau 在测试准确率 3 个 epoch 不升时降学习率避免震荡。早停 patience 设 7防止过拟合。class_weight 传进 CrossEntropyLoss让少数类样本的损失被放大。最后打印 classification_report重点看 U2R 和 R2L 的 recall如果这两个低于 0.8说明权重还不够或者模型容量不足。99.5% 的整体准确率通常在第 15-25 个 epoch 出现但一定要结合各类召回率一起看。4. 避坑与排查99.5% 之外你该盯住的五个问题4.1 测试集准确率 99.5% 但 U2R 召回为 0现象整体 accuracy 很高但 classification_report 里 U2R 的 recall 是 0.00precision 也是 0.00。原因U2R 在训练集只有 52 条测试集更少模型直接把它们全判成 Normal因为这样损失最小。解决把 class_weight 里 U2R 的权重再调大或者对 U2R 单独做 SMOTE 过采样到 500 条以上同时把评估指标从 accuracy 换成 macro-F1逼自己关注少数类。4.2 独热编码后维度对不上测试集报错现象训练时一切正常加载测试集推理时抛 ValueError: Feature shape mismatch。原因OneHotEncoder 只在训练集上 fit测试集出现了训练集没有的 service 或 flag 取值transform 时维度少了。解决fit 时把训练集和测试集的符号列拼在一起或者设置 handle_unknownignore让没见过的类别输出全零向量。我一般两个都做双保险。4.3 归一化用了全量数据准确率虚高现象换一批新数据测试准确率从 99.5% 掉到 70%。原因MinMaxScaler 在训练集测试集上一起 fit测试集的 min/max 信息泄漏到了训练过程模型实际上「见过」测试集的分布。解决scaler 只在训练集上 fit测试集用同样的 scaler.transform。这是最常见的翻车点血泪经验任何预处理只要涉及 fit都只能在训练集上做。4.4 GPU 显存够但训练速度没提升现象装了 CUDA模型也 .to(device) 了但一个 epoch 还是要几十秒。原因数据在 DataLoader 里没开 pin_memory或者 num_workers 设成 0CPU 到 GPU 的拷贝成了瓶颈。解决DataLoader 加 pin_memoryTrue, num_workers4如果数据集不大可以一次性把整个张量搬到 GPU省去每 batch 拷贝。另外检查 model 和输入是否都在同一 device 上混用 CPU/GPU 会静默变慢。4.5 换 CIC-IDS2017 后效果崩了现象NSL-KDD 上 99.5%换 CIC-IDS2017 直接掉到 80% 以下。原因两个数据集的特征定义、采集环境、攻击分布完全不同NSL-KDD 的 41 维特征在 CIC-IDS 里根本不存在。解决换数据集必须重新做特征工程不能直接套用。常见做法是先用 CICFlowMeter 提取流特征再重新训练编码器和归一化器。模型结构可以复用但输入维度和预处理管道必须重写。别指望一个模型通吃所有数据集那是玄学。5. 把 99.5% 变成可复现的基线我的验证习惯这套方案真正的价值不在那个数字而在于它给你一个可复现、可修改的基线。我自己的习惯是每次跑完训练先不看 accuracy先看混淆矩阵和 macro-F1。如果 U2R 和 R2L 的 recall 都过 0.85再去看整体准确率这时候的 99.5% 才是可信的。验证方法上我会做三件事第一用 KDDTest 跑一遍再用 KDDTest-21 跑一遍后者去掉了测试集中重复的简单样本更能反映真实泛化第二把训练集切出 20% 做验证集观察训练曲线有没有过拟合第三用同一套预处理管道跑一个随机森林做对照如果 CNN 只比 RF 高 0.5%那说明这个任务本身就不难CNN 的增量价值有限。# 快速对照实验随机森林基线 from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, f1_score X_train_flat np.load(data/X_train.npy) X_test_flat np.load(data/X_test.npy) y_train np.load(data/y_train.npy) y_test np.load(data/y_test.npy) rf RandomForestClassifier(n_estimators100, class_weightbalanced, n_jobs-1, random_state42) rf.fit(X_train_flat, y_train) rf_pred rf.predict(X_test_flat) print(RF acc:, accuracy_score(y_test, rf_pred)) print(RF macro-F1:, f1_score(y_test, rf_pred, averagemacro))这段对照实验的意义是如果 RF 也能到 99%那 CNN 的 99.5% 就不值得吹你该把精力花在特征工程或换更难的数据集上。如果 RF 只有 95% 而 CNN 到 99.5%说明卷积结构确实抓到了特征间的局部组合关系这个方向值得继续投入。参数上n_estimators100 是够用的起点class_weightbalanced 和 CNN 那边保持一致保证对比公平。最后说一个我踩过的坑别在预处理阶段就把数据标准化到完美留一点噪声给模型反而能提升鲁棒性。我一般会在归一化后的特征上加 1% 的高斯噪声做数据增强测试集准确率可能掉 0.2%但换一批新数据时掉得少很多。这个习惯让我在多次迁移实验里少走了不少弯路。希望帮到你。本文还有配套的精品资源点击获取