CNN网络入侵检测:从NSL-KDD数据预处理到模型部署全流程
发布时间:2026/9/28 19:30:44来源:尧图网络
简介面向计算机相关专业毕业设计及网络安全入门实践这是一套基于CNN卷积神经网络的网络入侵检测完整项目。内含Python源码、KDD Cup数据集、说明文档与工程配置文件共16个文件压缩包大小17.51MB以py脚本、gz数据、xml/iml工程配置及md文档为主源码经本地调试可运行可支撑从数据预处理、模型训练到检测验证的全流程学习。项目评审得分98分整体难度适中适合准备高分毕业设计或希望用真实入侵检测场景练习深度学习建模的学生。目前已有120人学习下载读者可通过源码结构、数据处理脚本和实验文档快速掌握CNN在网络入侵检测中的应用思路是一份可参照、可扩展的完整实践方案。1. 网络入侵检测里的CNN为什么一张灰度图能识别恶意流量做网络入侵检测方向的人十有八九都是从 NSL-KDD 这个数据集开始的每一条网络连接被整理成 41 个特征标签是 normal 加四类攻击族。很多 python 源码工程能直接跑出 90% 以上的准确率可等你把混淆矩阵打出来一看U2R 这类样本少又重要的攻击几乎全被漏掉模型等于把一个烂结果包装成了“高分”。CNN 卷积神经网络在这类任务里真正有用的地方不是它比 SVM、XGBoost 更“高级”而是它能把一条连接记录重新组织成一张小灰度图用卷积核自动学习特征之间的局部相关模式省掉大量手工特征工程的精力也更容易在论文里做出变体和对比实验。这篇笔记会按一套常见的高分毕业设计路径展开把数据预处理、模型搭建、训练参数、评估指标、五个必踩坑和部署骨架一次讲透。适合谁想跑通并改造这个方向做项目复现的人尤其是 python 刚起步但愿意看代码的读者。2. 数据准备把NSL-KDD的每一条记录变成CNN的输入2.1 为什么是CNNSVM调参烦RNN又太重先说选型。SVM、随机森林在这个任务上也能做但它们对特征工程的依赖很重41 个特征里离散列怎么编码、数值列怎么归一化、要不要做特征选择每一步都直接影响最终分数。CNN 的优势在于它把“特征之间的局部相关性”当成可学习的模式——卷积核扫过一个 3x3 的小窗口就能捕捉相邻特征之间的组合关系不需要你手工定义“logged_in 和 count 同时出现代表什么”。对比 RNN/LSTMNSL-KDD 里的一条连接记录并不是严格的时间序列它只是把这次连接的属性按表格排开。RNN 结构专门为有前后依赖的序列设计硬套在这个任务上参数量更大、训练更慢收益却不明显。这也是近些年不少入侵检测论文选用 CNN 而不是 RNN 的原因结构简单、训练稳定、在表格型特征上足够用。边界也要说清楚把一个一维特征向量改成灰度图本质上是你手动规定了哪些特征做邻居这个排列顺序会影响卷积核看到的局部关系。所以特征列顺序必须在训练和部署阶段完全一致这一点后面避坑章还会再讲。2.2 NSL-KDD的读取与标签映射攻击子类、攻击族和二分类NSL-KDD 文件没有表头第一步是先把 41 个标准特征名写死第 42 列是标签。很多源码工程在这里直接pd.read_csv(KDDTrain.txt)不带列名后面做切片时全靠行列坐标一旦数据集换掉就会出问题。import numpy as np import pandas as pd # NSL-KDD 文件的 41 个标准特征名第 42 列是标签 COLUMNS [ 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 ] def load_kdd(path): # headerNone 是关键NSL-KDD 没有表头 return pd.read_csv(path, headerNone, namesCOLUMNS)KDDCup99 系数据集里具体攻击名有几十种但论文里几乎都归成四族dos、probe、r2l、u2r。源码里通常维护一张映射表把子类聚到族再把族映射成整数这样五分类和二分类都能共用一套逻辑。# 攻击子类 - 攻击族 ATTACK_GROUP { 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, warezclient: r2l, warezmaster: r2l, spy: r2l, buffer_overflow: u2r, loadmodule: u2r, perl: u2r, rootkit: u2r, } def map_label(raw_label, modefive_class): 原始攻击名 - 整数标签。 modefive_class 按正常/四族分成 5 类 modebinary 只区分正常和异常 if raw_label normal: return 0 group ATTACK_GROUP.get(raw_label) if group is None: # 测试集里会出现训练集没见过的变种比如 apache2、saint # 一般按 KDDCup99 的官方攻击族说明补映射不要一律丢进 u2r raise ValueError(f未知攻击类型: {raw_label}请补全 ATTACK_GROUP) if mode binary: return 1 # 非 normal 都是异常 order [normal, dos, probe, r2l, u2r] return order.index(group) # normal0, dos1, probe2, r2l3, u2r4这里有两个容易被忽略的点。第一map_label里如果遇到未知攻击类型就直接抛异常而不是默认归到 u2r否则测试集里没见过的子类会污染 U2R 这个本就很稀少的类让混淆矩阵变得不可信。第二modebinary时返回 1 而不是按 group 区分这样五分类实验和二分类实验能用同一份数据加载代码只是标签列不同。2.3 归一化和灰度图转换scaler只能fit训练集41维补到49维拿到 DataFrame 之后标准做法是先对三列离散特征做整数编码再做 MinMaxScaler 归一化最后把 41 维补成 49 维、重排成 7x7 的灰度图。这里最危险的一步是 scaler 的 fit 范围。from sklearn.preprocessing import MinMaxScaler def preprocess(df, x_scalerNone, modefive_class, img_size7): 把 NSL-KDD DataFrame 转成 CNN 输入。 x_scaler 传 None 时表示正在处理训练集需要 fit 处理验证集/测试集时传入训练集得到的 scaler只 transform。 # 前 3 列是离散特征protocol_type / service / flag做整数编码 for c in [1, 2, 3]: df.iloc[:, c] df.iloc[:, c].astype(category).cat.codes x df.iloc[:, :41].astype(float32).values if x_scaler is None: x_scaler MinMaxScaler() x x_scaler.fit_transform(x) # 只在训练集上 fit else: x x_scaler.transform(x) # 测试集沿用训练集的统计量 # 41 维 - 49 维 - (7, 7, 1)通道数 1 表示灰度图 n_feat img_size * img_size pad_len n_feat - x.shape[1] # 第一个 (0, 0) 表示样本维不补第二个 (0, pad_len) 表示尾部补 0 x_pad np.pad(x, ((0, 0), (0, pad_len)), modeconstant, constant_values0) x_img x_pad.reshape(-1, img_size, img_size, 1) y df[label].map(lambda s: map_label(str(s), modemode)).astype(int64).values return x_img, y, x_scaler代码里的两个决策点。第一为什么要补到 49 维而不是直接 reshape 成 41 维因为 7x7 是离 41 最近的方形尺寸41 没法拆成两个整数相乘强行 reshape 会直接报错。尾部补 0 不会引入新信息只是把输入形状对齐。第二为什么 scaler 只能 fit 训练集MinMaxScaler 计算的是每个特征的最小值和最大值如果训练集和测试集一起参与 fit模型在训练阶段就偷看了测试集的数值分布这属于数据泄露。线上部署时新来的流量也只能用训练阶段保存的 scaler 做 transform不存在重新 fit 一说。提示如果后续要换独热编码处理离散列特征维度会从 41 变成 120 多pad 长度也要跟着变灰度图的尺寸就不是 7x7 了。毕业设计里想快速跑通整数编码是最省事的方案。3. 模型构建与训练轻量CNN的搭建与参数顺序3.1 Conv1D还是Conv2D网络入侵检测场景里的两个选项选卷积方向之前先看清自己的输入形态。如果坚持 41 维一维特征Conv1D 更自然如果已经按上一章转成 7x7 灰度图Conv2D 才是匹配项。两者在 NSL-KDD 这类小数据集上都能出效果差别集中在解释成本和落地姿势。方案输入形状卷积方式论文可视化的方便程度主要代价Conv1D(41, 1)沿特征列一维滑动一般特征图只能画成折线或柱状不需要补零但“图像”这个说法站不住Conv2D(7, 7, 1)在二维邻域上滑动高能直接展示灰度图和卷积后的特征图特征排列顺序会影响模型列顺序必须固定我一般建议毕业设计用 Conv2D。原因很实际实验室和答辩现场更认可“把流量转成图像再用 CNN 识别”这条故事线且 7x7 输入非常轻CPU 也能在几分钟内跑完一个 epoch。Conv1D 更适合那些强调“不做冗余转换、直接在原始特征上建模”的创新点如果你后面想加注意力机制或对比实验Conv1D 也可以作为 baseline 之一。3.2 把模型写出来双层卷积、BatchNorm和Dropout7x7 的图很小网络不用堆太深。常见的稳定结构是两组“卷积BN池化”再接全连接和 Dropout。import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_shape(7, 7, 1), num_classes5): model models.Sequential([ layers.Input(shapeinput_shape), # 第一组卷积7x7 图上提取局部模式paddingsame 保持尺寸 layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 7x7 - 3x3 # 第二组卷积通道翻倍空间尺寸继续缩小 layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), # 3x3 - 1x1 layers.Flatten(), layers.Dropout(0.3), layers.Dense(64, activationrelu), layers.Dense(num_classes, activationsoftmax), ]) return model model build_cnn(num_classes5) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, # 标签是整数不需要做独热编码 metrics[accuracy], )逐层看输出形状输入是 (7, 7, 1)第一层 Conv2D 用 32 个 3x3 卷积核paddingsame后还是 (7, 7, 32)MaxPooling2D((2, 2)) 后变成 (3, 3, 32)。第二组同理池化后变成 (1, 1, 64)再展平就是 64 维向量。这个规模全网络参数量不到一万训练非常快。卷积核用 3x3 而不是 5x5是刻意选的7x7 的图如果用 5x5一次卷积就覆盖了整张图的 51% 面积局部特征提取的意义就弱了。paddingsame是为了让第一次池化能拿到 3x3 的中间特征如果直接不填充7x7 卷积一次变 5x5再池化变 2x2第二层卷积核会有点施展不开。3.3 训练参数怎么设学习率、早停、batch size、样本权重模型定义好之后训练参数这一块是翻车高发区。先看一组我常用的配置from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint EPOCHS 30 BATCH_SIZE 32 # 早停是后悔药val_loss 连续 8 轮不降就停下来 early_stop EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue, # 恢复验证集上最好的那组权重 ) checkpoint ModelCheckpoint( best_cnn.keras, monitorval_loss, save_best_onlyTrue, ) # 示意值正常样本最多dos 次之r2l/u2r 很少 # 正式跑之前用 sklearn.utils.class_weight.compute_class_weight 计算 class_weight {0: 1.0, 1: 1.0, 2: 1.5, 3: 6.0, 4: 8.0} history model.fit( x_train_img, y_train, validation_data(x_val_img, y_val), batch_sizeBATCH_SIZE, epochsEPOCHS, callbacks[early_stop, checkpoint], class_weightclass_weight, verbose1, )几个关键点。第一learning_rate1e-3是 Adam 的常见起点不建议一上来就用 1e-2小数据集上很容易震荡不收敛。第二EarlyStopping的restore_best_weightsTrue很重要否则回调停了之后拿到的可能是最后几个 epoch 的权重而不是验证集上最好的那批。第三class_weight针对的是类别不平衡r2l 和 u2r 样本本来就少不给权重的话模型会倾向于把所有样本都判成多数类。需要注意权重不能给得过高否则正常流量会被大面积误报成攻击。3.4 环境对齐再开跑python版本、依赖和GPU注意点跑这份源码前先把环境对齐能省一天。TensorFlow 2.x 在 python 3.8 到 3.11 之间基本都能正常工作过新的 python 3.12 以上某些 TF 版本安装时会没有预编译包。依赖按需装就行pip install tensorflow pandas scikit-learn如果只想在 CPU 上跑不要为了提速去折腾 GPU 版7x7 的灰度图输入量很小CPU 完全扛得住。GPU 版最容易翻车的地方是 CUDA 和 cuDNN 版本不匹配报错往往是一大段“Could not load dynamic library”。这种问题查起来很费时间建议直接在纯 CPU 环境跑通再去考虑 GPU 加速。IDE 方面VSCode 里给项目单独建一个虚拟环境解释器选对之后F5 就能跑不要用全局环境不然以后装包会把系统环境搞乱。4. 评估与判定准确率之外还有四个必须看的数字4.1 混淆矩阵别让90%的准确率骗了你NSL-KDD 的测试集和训练集分布不一样测试集里 r2l、u2r 的比例更高还有很多训练集没见过的攻击变种。如果只盯着 accuracy很容易得出“模型很好”的错误结论。指标关注点翻车迹象precision判为攻击的样本里有多少真的攻击误报多时 precision 会掉recall真实攻击里有多少被模型捞出来漏报多时 recall 会掉F1-scoreprecision 和 recall 的调和平均少数类 F1 接近 0 说明该类完全没学会混淆矩阵每一类被误判成了哪一类少数类被全部预测成 normal 是最常见的坏形态入侵检测场景里漏报的代价比误报高。一个只有 100 次攻击流量、模型全部放行的“高 accuracy 模型”在实际部署中没有价值。所以评估代码必须输出 confusion matrix 和 classification_report而不是只打印一个分数。4.2 一段能直接跑的评估代码把训练好的模型加载回来在测试集上做预测输出多维度的评估结果。from sklearn.metrics import confusion_matrix, classification_report y_pred model.predict(x_test_img, batch_size256) y_pred_class np.argmax(y_pred, axis1) print(confusion_matrix(y_test, y_pred_class)) print(classification_report( y_test, y_pred_class, target_names[normal, dos, probe, r2l, u2r], digits3, ))读报告时先看每个类别的 support 数量再看 r2l 和 u2r 的 recall。如果这两个类别的 recall 是 0.000说明模型根本没学会识别它们如果 precision 高但 recall 低说明模型只在极少情况下敢判成攻击大部分都被放过了。这两个类别的 F1 才是整个模型真正水平的体现而不是那个光鲜的 accuracy。评估还要区分验证集指标和测试集指标。验证集指标用来调参测试集指标用来汇报。按标题里的“全部数据”来组织项目时建议用 KDDTrain.txt 训练并从中切验证集用 KDDTest.txt 做最终测试不要拿测试集反复调参。4.3 五分类还是二分类论文目标先想清楚这是一个在写代码之前就该定下来的问题。二分类只区分正常和异常模型好训、指标好看、答辩时讲解简单但创新点会显得单薄。五分类把异常细分成 dos、probe、r2l、u2r和“网络入侵检测”这个标题的贴合度更高也让混淆矩阵有了更多分析空间但 r2l 和 u2r 的类别不平衡问题会直接摆到明面上。我的建议是主实验做五分类额外跑一组二分类作为对比实验。五分类用来展示模型对攻击类型的识别能力二分类用同一套网络把输出层改成 2 类证明模型在简化任务下能达到更高的检测率。两个实验共用前面的预处理代码只是在调用map_label时切换mode参数。这样论文里既有对比、又有层次且不需要重新发明一套代码。5. 避坑记录网络入侵检测CNN项目最常踩的五条弯路这一章五条弯路我基本都自己翻过车按“现象 → 原因 → 解决”写在这里。先看这些比急着调模型结构划算。5.1 数据泄露scaler把测试集一起fit了现象训练准确率 95% 以上验证集也接近换到 KDDTest 上直接掉到 75% 左右代码逻辑又看不出明显错误。原因预处理时图省事把训练集和测试集拼在一起scaler.fit()再切回去。MinMaxScaler 从测试集里偷看了每个特征的最大值和最小值相当于考试前看了答案。解决严格按“先切集合、再 fit 训练集、transform 验证集和测试集”的顺序走。即 2.3 节代码里x_scalerNone只在训练集上出现之后所有 transform 都传入这个已拟合的 scaler。排查时只需检查预处理代码里有没有对全量数据调用过一次fit_transform。5.2 类别不平衡少数类被normal吞掉现象整体 accuracy 有 90%打印 confusion_matrix 却发现 r2l、u2r 两行几乎全是 0所有这类样本都被预测成了 normal。原因交叉熵损失被多数类主导。模型发现只要全预测成 normal损失就已经很小没必要去学少数类的特征。解决给少数类加大权重也就是 3.3 节里的class_weight。正式项目里不要手写{0: 1.0, 1: 1.0, ...}用sklearn.utils.class_weight.compute_class_weight(balanced, classesnp.unique(y_train), y_train)自动算然后在model.fit()里传入。加了权重之后 r2l、u2r 的 recall 会明显上升但 normal 的误报也会增加这个 trade-off 要在报告里明确写出来。5.3 换一个数据源就崩特征列顺序被当成了记忆现象同一套模型在标准 NSL-KDD 上表现正常把数据换成从别的渠道导出的 CSV字段名看着都对准确率却暴跌。原因CNN 卷积核是按位置学习特征的。你把src_bytes从第 4 列挪到第 20 列模型不知道“第 4 列”已经变成了别的含义它只是在用训练时的位置记忆做判断。灰度图方案会放大这个问题因为相邻特征的关系被重新排列了。解决把 41 个特征名固定成一个配置文件训练、验证、测试、部署全部引用同一份。我一般会把COLUMNS[:-1]写进feature_columns.json数据加载时按这个文件的顺序取列而不是依赖 CSV 本身的顺序。5.4 同一个验证集被反复调优测试集一测就现原形现象花了两周在验证集上调参验证集指标一路涨到 96%最后用 KDDTest 一测只有 80%人直接懵了。原因验证集被用得太狠了。每一次“看到验证集分数不好就改参数”的动作都让模型选择偏向验证集验证集相当于变成了第二个训练集最终测到测试集时才发现泛化没提升。解决坚持三分法——训练集调权重、验证集做早停、测试集只许测一次。测试集应该是你代码里最后一个碰到的数据而且每改一次大结构就要意识到需要对整套流程重新评估一次。这不是玄学是模型选择的基本纪律。5.5 随机种子没固定同一份python源码每次跑结果不一样现象同一天跑两遍训练脚本accuracy 差了 1.5 个百分点评审质疑结果不可复现。原因网络初始化、数据 shuffle、Dropout 都带随机性。只要一个地方没固定种子两次训练就不会完全一致。解决在训练脚本最开头固定三个层级python 自带的 random、numpy、TensorFlow 各自的随机种子import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)如果项目对确定性要求更高可以在 Linux 环境里设置环境变量TF_DETERMINISTIC_OPS1但注意它依赖硬件和 TensorFlow 版本不是所有平台都生效。在论文里描述实验条件时写明“训练早期融合了随机初始化复现时固定随机种子”就够了。6. 把模型接到真实流量上从离线训练到在线检测的落地骨架6.1 单条推理一条新连接怎么变成7x7灰度图离线训练结束后最常被问的问题是“模型怎么用起来”。真实场景里流量是一条一条进来的不可能等你攒一批再去预测所以要写一个单条推理函数。def predict_one(feature_vec, x_scaler, model, img_size7): feature_vec 必须是 41 维且与训练时的列顺序一致 x np.asarray(feature_vec, dtypefloat32).reshape(1, -1) x x_scaler.transform(x) pad_len img_size * img_size - x.shape[1] x np.pad(x, ((0, 0), (0, pad_len)), modeconstant) x x.reshape(1, img_size, img_size, 1) prob model.predict(x, verbose0)[0] return int(np.argmax(prob)), float(prob.max())这里最要紧的是x_scaler和列顺序都必须和训练时完全相同少一个环节出来的结果就是废的。这个函数可以直接放在线上服务里每条连接特征到达时调用一次。6.2 滑窗判决和模型导出把单条误报滤掉一层单条预测的误报率在真实流量里会很难看一个常见的做法是加一个滑窗连续若干条连接里攻击占比超过阈值才告警from collections import deque window deque(maxlen10) # 最近 10 条连接的判定结果 alert_th 0.6 # 异常占比超过 60% 才触发 for feature in traffic_stream: pred, conf predict_one(feature, scaler, model) window.append(0 if pred 0 else 1) if sum(window) / len(window) alert_th: print(alert: 疑似入侵流量请人工复核) window.clear()模型部分还可以用tf.lite.TFLiteConverter.from_keras_model(model)转成cnn_nids.tflite方便在边缘设备上加载推理速度比 Keras 模型快不少。这是我做项目的收尾习惯跑分再高最后都会接一段滑窗判决因为单条误报才是落地的最大坎。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网