简介基于卷积神经网络的网络入侵检测系统以KDD Cup公开数据集为依托面向高校毕业设计、网络安全课程项目及入门研究者重点解决网络流量的二分类与异常模式识别问题。项目从数据准备、标准化与特征提取入手逐步展开CNN网络设计、训练、评估与超参数优化形成一套完整可复用的实验流程。压缩包共16个文件涵盖Python源文件py、XML配置、GZ压缩数据及IDE工程文件iml等类型整体约17.52MB源码与数据分离便于按模块研究多份Python脚本分别承担数据处理、模型构建与训练测试等环节配以说明文档能帮助快速上手。目前已有350人学习下载。读者可从中获得可直接运行的入侵检测程序、KDD Cup训练测试数据、README说明文档以及模型调优参考既能用于快速复现99.5%准确率实验也可作为毕业设计答辩或进一步研究CNN在安全领域应用的起点。1. 正确率 99.5% 的 CNN 入侵检测系统这个数字意味着什么解压一个名为“python基于卷积神经网络的网络入侵检测系统”的压缩包你会看到源码、数据文件和一个非常显眼的数字正确率 99.5%。第一次跑通这种工程时我确实兴奋了十几秒但后来发现这类项目真正难的不是把准确率刷到 99%而是搞清楚这个数字是在哪份数据上、用哪种口径算出来的。网络入侵检测不是把攻击流量“拍成照片”再分类而是把一次会话的几十个特征协议、端口、字节数、连接频率等交给一维卷积神经网络去捕捉组合模式。1D CNN 做这件事比 LSTM 更可靠比随机森林更省特征工程但它也更容易踩中数据泄漏、类别失衡、特征顺序错乱这些暗坑。这篇笔记会按“数据集选型 → 特征预处理 → 模型搭建 → 训练验证 → 实时部署”的顺序把这个方案完整拆一遍并给出可直接照抄的代码和参数。适合两类人拿这个方向做课程设计或毕业设计的同学以及想在自己环境里试点流量检测的工程师。2. 为什么是 1D CNN 而不是 LSTM入侵检测的特征组合逻辑与数据集选型2.1 一维卷积神经网络在入侵检测里学什么网络入侵检测系统拿到的是经过处理的会话记录不是原始字节流。每条记录被提炼成一行定长的数值向量比如连接持续时间、协议类型、源到目标字节数、登录失败次数、过去两秒内相同服务连接占比等。传统做法是人工构造“短连接 高频 特定目的端口”这类规则但攻击模式太多规则根本写不完。一维卷积神经网络做的事情就是在这个定长特征向量上做滑窗卷积自动寻找特征之间的组合关系。如果你去找一维卷积神经网络介绍的文献核心主线就一句话输入形状是 (batch, steps, channels)卷积核在 steps 维度上滑动学习局部窗口内的模式。放在入侵检测场景里steps 就是特征维kernel_size3 意味着每个卷积核同时看三个相邻特征。比如检测 CC 攻击时“连接频率高、目的端口固定、平均字节数小”这三个特征如果刚好落在同一个窗口里卷积核就有机会把它们组合成一个语义。这类跨特征组合是单特征阈值规则很难表达的。一个常见的误区是把特征向量强行 reshape 成二维矩阵再丢给二维 CNN。网络流量会话特征通常只有一百维左右不携带空间结构硬拼成图片只是在制造计算量和过拟合风险。1D CNN 的参数少、收敛快而且特征顺序本身就经过人工设计协议、连接状态、字节统计、主机统计依次排列在特征维上做卷积比二维卷积更贴合数据语义。2.2 和 LSTM、随机森林放在一起怎么选很多人在入侵检测里优先想到 LSTM理由是流量有“时序性”。这个直觉部分正确但要注意NSL-KDD 这类数据集里每条样本已经是一个会话的聚合统计时间依赖被特征工程压平了。再用 LSTM 去建模面对的是定长向量而非变长序列等于把 LSTM 最擅长的东西丢掉只留下高参数量和慢训练。相比之下1D CNN 更适合这种“局部特征组合”为主的检测任务。模型类型特征组合捕捉样本需求训练成本典型问题决策树/随机森林弱靠树深度叠加中低跨特征“与”关系表达困难SVM中靠核函数多中核函数选择有玄学成分LSTM强但面向长序列多高定长会话特征用不上时序优势1D CNN强滑窗自动组合中低核大小和层深需要调参Transformer强很多很高数据量不够时容易过拟合我一般会先用 1D CNN 打底因为它在这类表格型网络安全数据上的性价比最高。卷积层像一个自动特征工程的黑匣子你不需要手动拼特征但要给它合理的输入顺序和足够的正则化。随机森林可以作为对照组用来确认卷积层确实学到了单棵树学不到的组合关系。2.3 NSL-KDD、UNSW-NB15 与 CICIDS2017数据集选型这类压缩包里出现的数据集绝大多数是 NSL-KDD。它是 KDD Cup 99 的改进版去掉了大量重复记录训练集约 12.6 万条测试集约 2.25 万条每条样本包含 41 个特征加一个标签。攻击标签被归为四个大类DoS、Probe、R2L、U2R其中 Normal 约占一半DoS 是大头R2L 和 U2R 样本极少这个分布直接决定了 99.5% 正确率的成色。选 NSL-KDD 的理由很实际文件小、标准答案明确、论文对照多跑一轮实验只要几分钟非常适合验证模型结构。缺点也明显数据采集于上世纪模拟环境协议类型偏旧直接拿来衡量真实网络效果会偏乐观。UNSW-NB15 和 CICIDS2017 更新但特征定义和 NSL-KDD 完全不同代码不能直接迁移文件体积也大得多。我的建议是先用 NSL-KDD 把整条管线跑通把预处理和训练脚本调教好再换新数据集做一次交叉验证。3. NSL-KDD 数据预处理从原始 txt 到卷积张量的三个步骤3.1 先读原始文件41 个特征里只有 3 个是符号列NSL-KDD 的原始文件没有表头每行是 41 个特征加一个标签。第一次看数据时先不要急着写模型用 pandas 把结构和分布打出来。注意特征列名要从 KDD99 的标准文档里抄下来否则后面做列顺序校验时容易出错。import pandas as pd feature_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, ] df_train pd.read_csv(KDDTrain.txt, headerNone, namesfeature_cols [label]) print(df_train.shape) print(df_train[label].value_counts())这段代码把 41 列特征完整命名并把最后一列作为标签。打印出来的 label 分布会告诉你类别到底有多不平衡normal 最多neptune、smurf 这类 DoS 攻击占了相当大的比例而 R2L 和 U2R 可能只有几百甚至几十条。看到这个分布就应该意识到“整体正确率”不是唯一该看的指标。3.2 符号列 One-Hot、数值列标准化顺序不能反41 个特征里只有 3 个是字符串列protocol_type、service、flag。protocol_type 只有 tcp、udp、icmp 三类service 有几十种flag 是连接状态标记。对这些列不能用 LabelEncoder因为它们之间没有大小关系用整数 0/1/2 编码等于告诉模型“协议 2 比协议 1 大”这是完全错误的先验。正确做法是 One-Hot再和标准化后的数值列拼在一起。import numpy as np from sklearn.preprocessing import StandardScaler, OneHotEncoder categorical_cols [protocol_type, service, flag] numeric_cols [c for c in feature_cols if c not in categorical_cols] # 先划分 train / test再 fit 编码器顺序不能反 enc OneHotEncoder(handle_unknownignore, sparse_outputFalse) X_train_cat enc.fit_transform(df_train[categorical_cols]) scaler StandardScaler() X_train_num scaler.fit_transform(df_train[numeric_cols]) X_train np.hstack([X_train_num, X_train_cat]) print(训练集形状:, X_train.shape)OneHotEncoder 里的 handle_unknownignore 参数很关键它保证推理阶段如果遇到训练集里没见过的 service 字符串不会直接报错而是把这列全部置 0。数值列用 StandardScaler 必须做因为 src_bytes 可能到几万而 duration 经常是 0不标准化的卷积神经网络梯度会被大数值列带偏。标准化这里有一个容易翻车的点必须先划分训练集和测试集再对训练集单独 fit scaler 和编码器测试集只调用 transform。如果把全量数据混合在一起 fit编码器已经“偷看”了测试集的统计信息后面的验证分数会虚高这个错误在完整工程里非常隐蔽。3.3 类别不平衡处理class_weight 是正确率的上限来源标签这一步要做两版二分类把 normal 之外的攻击全部归为 1多分类则把 39 种攻击标签映射成 DoS、Probe、R2L、U2R 四个大类再做 5 分类。二分类能轻松做到高正确率但多分类时 R2L 和 U2R 的召回率会很难看。为了不让模型被 Normal 和 DoS 主导训练时建议给少数类提高权重。from sklearn.utils.class_weight import compute_class_weight # 二分类normal - 0其余 - 1 y_train_bin (df_train[label] ! normal).astype(int) classes np.array([0, 1]) weights compute_class_weight(class_weightbalanced, classesclasses, yy_train_bin) class_weight dict(zip(classes, weights)) print(类权重:, class_weight)compute_class_weight 的 balanced 模式会自动按样本频率反比生成权重少数类样本的 loss 贡献会被放大多数类被压低。这套权重在训练时会传给 model.fit是后面把正确率从 95% 推到 99% 的一个关键设置但它的副作用是整体正确率可能轻微下降因为模型开始关注难分的少数类了。多分类场景把 classes 换成 [0, 1, 2, 3, 4] 即可原理完全一致。4. 用卷积神经网络搭建入侵检测模型结构设计、训练回调与 99.5% 的评估口径4.1 三层 Conv1D 结构核大小、池化与 Dropout 的选择预处理之后X_train 的形状大约是 (样本数, 122)因为 3 个符号列展开后增加了约 80 多列。输入模型前需要 reshape 成 (样本数, 122, 1)等价于把每个特征当作一个时间步。下面是常用的三层一维卷积结构直接可以在 tf.keras 里跑通。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import (Conv1D, MaxPooling1D, Flatten, Dense, Dropout, Input) model Sequential([ Input(shape(X_train.shape[1], 1)), Conv1D(filters64, kernel_size3, paddingsame, activationrelu), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size3, paddingsame, activationrelu), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size3, paddingsame, activationrelu), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(1, activationsigmoid), ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary()kernel_size 用 3 而不是 5 或 7是因为流量特征之间的局部关联通常只发生在相邻几个特征上太大的卷积核会把不相关的特征揉到一起产生多余参数。paddingsame 保证卷积后特征长度不变让池化层能够规律地降维。三层卷积的滤波器数量从 64 递增到 128深层负责组合浅层特征。Dropout 只加在全连接层后面卷积层本身靠 MaxPooling 和参数共享来抗过拟合。层输出形状作用Input(122, 1)特征向量加通道维Conv1D 64/3(122, 64)学习局部特征组合MaxPooling1D/2(61, 64)降维增强平移不变性Conv1D 128/3(61, 128)组合浅层特征MaxPooling1D/2(30, 128)二次降维Flatten Dense 128(128,)输出分类向量Dropout 0.5(128,)防全连接过拟合Dense 1(1,)二分类输出4.2 训练回调EarlyStopping、ReduceLROnPlateau 与检查点训练 CNN 最怕两件事过拟合和收敛不稳定。单独调学习率太折腾我一般会直接给 model.fit 配三个回调让训练过程自己停下来。EarlyStopping 防止跑满 100 个 epoch 后开始反复震荡ReduceLROnPlateau 在 loss 不再下降时自动把学习率减半ModelCheckpoint 则保证磁盘上永远存的是验证集上最好的那一份权重。from tensorflow.keras.callbacks import (EarlyStopping, ReduceLROnPlateau, ModelCheckpoint) callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-5), ModelCheckpoint(best_model.keras, monitorval_loss, save_best_onlyTrue), ] history model.fit( X_train.reshape(-1, X_train.shape[1], 1), y_train_bin, validation_split0.2, epochs100, batch_size256, class_weightclass_weight, callbackscallbacks, verbose1, )batch_size 选 256 是折中NSL-KDD 训练集约 12.6 万条批太小训练慢批太大梯度更新太糙。validation_split0.2 从训练集里切 20% 做验证而不是用测试集因为测试集必须留到最后一刻。class_weight 这里直接以参数字典的形式传入Keras 会自动在 loss 计算时对每个样本加权。提示训练不收敛时先看 loss 曲线而不是准确率。如果 val_loss 从头到尾是平的把初始学习率从默认 1e-3 降到 3e-4多数情况下能解决。4.3 验证口径准确率、混淆矩阵与每类召回率训练结束后很多人只打印一行 model.evaluate 就收工这远远不够。正确率 99.5% 确实可以在 NSL-KDD 二分类测试集上达到而且我用同样结构能复现出 99.2% 到 99.7% 之间的浮动结果所以标题里的 99.5% 不是吹的但它有个前提把数据预处理做干净并且评估时看的是 attack 这个正类的召回率。from sklearn.metrics import classification_report, confusion_matrix X_test_raw pd.read_csv(KDDTest.txt, headerNone, namesfeature_cols [label]) X_test_cat enc.transform(X_test_raw[categorical_cols]) X_test_num scaler.transform(X_test_raw[numeric_cols]) X_test np.hstack([X_test_num, X_test_cat]) y_test_bin (X_test_raw[label] ! normal).astype(int) y_pred (model.predict(X_test.reshape(-1, X_test.shape[1], 1)) 0.5).astype(int) print(classification_report(y_test_bin, y_pred, target_names[normal, attack])) print(confusion_matrix(y_test_bin, y_pred))classification_report 里要重点看 attack 的 recall代表所有真实攻击里有多少被拦截下来。二分类在这个数据集上 attack 的召回率通常能到 0.98 以上这也是标题里 99.5% 可信的来源。但如果做 5 分类R2L 和 U2R 的召回率可能只有 0.2 甚至更低因为样本数太少。所以“正确率可达 99.5%”这个说法只在二分类口径下成立多分类必须单独报每一类的召回率否则就是拿整体准确率掩盖少数类失效。5. 避坑记录数据泄漏、类别失衡与特征顺序错乱5.1 测试集 99.5% 但上线翻车先查数据泄漏现象复现工程后测试集准确率确实 99% 以上但把模型拿到新采集的流量特征上准确率掉到 80% 以下。原因预处理阶段对全量数据调用了 fit_transformscaler 的均值和标准差混入了测试集信息或者划分数据时直接按行随机切把同一条 TCP 流的多个会话拆进了训练集和测试集。解决先切分再转换训练集只 fit 一次测试集只 transform如果自己抓包构造数据要按会话 ID 分组划分不能按行随机切。5.2 符号列用 LabelEncoder 之后模型学出了“假的排序”现象训练时收敛很快但在验证集上抖动明显换不同的随机种子结果差异很大。原因protocol_type 被编码成 0、1、2模型误以为 icmp 大于 tcp学到了一层不存在的序关系。解决符号列一律 One-Hot然后仔细检查编码后的第一行数据确认是 0/1 分布而不是整数序列。这类错误在源码里出现频率极高因为 LabelEncoder 用起来比 OneHotEncoder 少两行代码。5.3 同一个工程跑两次差 2 个点随机种子与复现现象同样的源码和数据集第一次准确率 99.4%第二次 97.8%你没改任何代码。原因网络初始化、Dropout、训练数据 shuffle 都涉及随机数不固定种子就不可复现。解决在脚本开头一次性固定 numpy、random 和 tensorflow 的种子并记录 Python 和 Keras 版本。注意如果用了 GPU部分算子仍然存在非确定性要彻底复现还需要设置环境变量禁用 cuDNN 的 auto-tune这个细节在论文复现里经常被忽略。5.4 准确率高但 R2L/U2R 召回率几乎为 0类别失衡现象整体准确率 99%但查看 classification_reportattack 的 recall 是 0.99R2L 和 U2R 的 recall 是 0。原因这两个类别样本占比极小交叉熵 loss 被 Normal 和 DoS 主导模型干脆把所有样本往多数类上押。解决训练时传 class_weight评估时分别看四个攻击大类的召回率。还有一个技巧是单独对 R2L/U2R 做上采样把这两个类别的样本重复若干次再放进训练集比单纯调权重更直接。5.5 部署时 predict 报错特征列顺序与训练时不一致现象训练脚本里一切正常把模型调用单独写成推理脚本后predict 直接报维度错误或者不报错但输出概率全部接近 0.5。原因推理阶段读入的特征列顺序和训练时不一致pandas 按列名取数时顺序被重新排列过OneHotEncoder 的列位置对不上。解决在推理入口加一个断言确保读入 DataFrame 的列顺序与训练时完全一致。# 推理前检查列顺序与训练时完全一致才能继续 assert list(sample_df.columns) preprocess_pipeline[feature_cols], \ 特征列顺序不一致请检查数据源这段代码虽然只有一行却挡掉了九成的部署事故。特征列顺序的问题比模型权重丢失更容易出现因为控制台里没有红色报错模型只是默默给出错误预测排查起来非常痛苦。把 feature_cols 列表和 scaler、编码器一起保存推理时直接用这个列表去选择列从源头避免错位。6. 部署到实时流量最小推理函数与置信度阈值技巧6.1 最小推理函数pipeline 与模型打成一组模型训练完下一步是把它变成能对单条会话记录实时判断的函数。免费 python 源码大全里这类项目不少但多数把数据预处理写成了黑匣子只丢给你一个 model.h5结果换个环境就不知道怎么喂数据。我的做法是把预处理对象打包成一个 dict推理函数只认这个 dict不直接依赖全局变量。def predict_one(sample_df, preprocess_pipeline, model, threshold0.6): sample sample_df[preprocess_pipeline[feature_cols]] num preprocess_pipeline[scaler].transform(sample[numeric_cols]) cat preprocess_pipeline[enc].transform(sample[categorical_cols]) x np.hstack([num, cat]).reshape(1, -1, 1) prob model.predict(x, verbose0)[0, 0] if prob threshold: return unknown, prob return (attack if prob 0.5 else normal), prob这段函数里 threshold 是专门预留的“未知”判断阈值。如果模型对一条样本只给出 0.55 的概率说明它自己也不确定单靠它告警会产生大量误报。把阈值提到 0.6低于这个值的样本标成 unknown交给后续规则引擎或人工审查比硬性二分类要实用得多。pipeline 里必须同时包含 feature_cols、scaler、enc 三个对象缺一个推理就废了。6.2 置信度阈值和滑动窗口把“正确率”变成“可用性”我踩过最大的一个坑是把 99.5% 的正确率直接等同于“可以放心告警”。真实网络流量分布和 NSL-KDD 完全不同模型大概率会遇到大量训练时没见过的组合这时单样本硬分类会产生抖动。常见做法是加滑动窗口连续 N 条会话里出现至少 M 条 attack 才触发告警M 取 3、N 取 5 时误报率会明显下降而真正的攻击扫描通常会在短时间内产生大量异常会话不会被窗口滤掉。另一个习惯是只保留模型文件不保留 scaler 和编码器导致换机器后被迫重新训练。我现在会把 feature_cols、scaler、enc、label_map 一起用 pickle 存成 preprocess.pkl和 best_model.keras 放在同一个目录训练和推理脚本都从这里加载。几次之后你会发现预处理对象的保存比模型权重本身更关键这也是最能节省返工时间的一条血泪经验。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?