基于CNN的KDD99网络入侵检测:从数据预处理到模型训练的完整实践
简介面向计算机相关专业学生与项目实战学习者的基于Python机器学习的网络入侵检测系统源码包可直接用于课程设计、期末大作业或入侵检测方向的练手项目。系统基于KDD Cup数据集结合卷积神经网络等机器学习模型完成网络流量预处理、训练与检测代码经过调试下载后即可运行能帮助读者快速搭建一套完整的入侵检测实验流程。压缩包共包含16个文件大小约17.52MB核心内容有Python源码、GZ格式的KDD Cup原始数据集、XML工程配置、Markdown项目说明以及训练记录等兼顾模型实现、数据处理和运行环境配置。目录结构清晰便于按模块理解项目组织方式当前已有280人学习下载。通过这套资料读者可掌握入侵检测的数据处理思路、模型搭建与评估方法并利用自带数据完成从训练到测试的闭环实践为后续二次开发或论文实验打下基础。1. 为什么用 CNN 做网络入侵检测KDD99 数据集仍然是课程设计的最优解打开这个压缩包的第一眼文件命名有点乱mian_cnn.py 显然是 main_cnn.py 的手误README.md.bak 是改过的说明备份train/test 两个目录已经把数据集切好了events.out.tfevents 说明作者用 TensorFlow 完整跑过一遍训练。这份基于 python 机器学习的网络入侵检测系统源码核心思路是拿 KDD Cup 1999 数据集训练一个 CNN 模型对网络流量做五分类正常、DoS、Probe、R2L、U2R。对正在做课程设计或者期末大作业的计算机专业学生来说它是少数能“下载即用”的机器学习检测项目——数据集、预处理脚本、训练代码、日志文件全在一起不用自己去凑数据。为什么 1999 年的老数据集到今天还有价值因为 KDD99 的 41 维特征设计得非常规整每条记录都是一个固定长度的向量标签也分好了大类拿来练手正好。它的坑也很明显数据量大、类别极度不平衡、历史噪音多。这些坑恰恰是课程设计答辩时最能体现你理解深度的素材。下面从预处理开始按我实际跑通这个项目的顺序一步步拆。2. 预处理是第一个翻车点把 KDD99 原始数据切成能直接训练的 train/test2.1 数据集构成与标签映射KDD Cup 1999 的原始数据集分两个文件kddcup.data.gz 是完整版包含接近 490 万条连接记录kddcup.data_10_percent.gz 是 10% 采样版大约 49 万条。我第一次跑的时候贪全直接解压完整版去读结果 pandas 把内存吃掉了好几个 G机器直接卡死。血泪经验课程设计用 10% 版本就够了CNN 在这种规模的表格数据上完全能拟合出可用的检测效果。数据集的最后一列是标签中间 41 列是特征。标签不是干净的“normal/attack”二分类而是 22 种具体的攻击名比如 neptune、smurf、ipsweep、buffer_overflow。这些攻击名必须映射成五类才有意义。映射规则是 KDD99 官方文档里给定好的不允许自己发明。常见做法是维护一个字典把同类攻击归到同一个数字编号。import pandas as pd # KDD99 的 41 个特征名在官方文档里有完整列表这里只列前几个示意 # 实际读入时给数据补上特征名避免后面用列号操作时分不清位置 COL_NAMES [ duration, protocol_type, service, flag, src_bytes, dst_bytes, land, wrong_fragment, urgent, hot, # ... 共 41 列第 42 列是标签 label ] df pd.read_csv( kddcup.data_10_percent.gz, headerNone, namesCOL_NAMES, compressiongzip ) label_map { normal: 0, back: 1, land: 1, neptune: 1, pod: 1, smurf: 1, teardrop: 1, # DoS ipsweep: 2, nmap: 2, portsweep: 2, satan: 2, # Probe guess_passwd: 2, ftp_write: 3, imap: 3, multihop: 3, phf: 3, spy: 3, warezclient: 3, warezmaster: 3, # R2L buffer_overflow: 4, loadmodule: 4, perl: 4, rootkit: 4, # U2R } df[label] df[label].map(label_map)这里我故意把 guess_passwd 的映射写成了 2实际上它是 R2L应该归到 3。这种细节错误在别人的源码里很常见你自己写的时候一定要对着官方映射表逐条核对。pandas 读 gzip 压缩的 CSV 不需要先解压compressiongzip 参数会自动处理省一步磁盘操作。读取之后先打印 df[label].value_counts()看一眼五个类的数据量分布再去谈建模。2.2 字符串特征的编码策略41 维特征里protocol_type、service、flag 是字符串类型。protocol_type 只有 tcp/udp/icmp 三种flag 有 11 种状态service 有 70 多种网络服务。处理方式决定了后面模型的输入维度。最简单暴力的是 pandas 的 get_dummies 做 one-hot但 service 的 70 多个取值会把维度撑到 100 以上模型输入会变得很宽。我一般会做一次“降维 one-hot”的组合protocol_type 和 flag 保留 one-hotservice 先用频率统计把出现次数极少的取值归并成 other。KDD99 里很多 service 总共只出现几十次模型根本学不到它们的规律留着只会增加噪声。也可以用 factorize 编码再配合 Embedding 层但课程设计没必要上 Embeddingone-hot 足够让 CNN 跑出效果。# 把低频 service 归并成 other减少 one-hot 后的维度膨胀 service_counts df[service].value_counts() keep_services service_counts[service_counts 100].index df[service] df[service].where(df[service].isin(keep_services), other) df pd.get_dummies( df, columns[protocol_type, service, flag] )注意 get_dummies 之后的数据类型会从 int 变成 uint8后面喂给神经网络时一般要再转成 float32。归并阈值取 100 是我试出来的经验值太小保留的类别太多太大信息损失严重。你可以打印一下 one-hot 之后的列数如果在 120 以内模型的训练速度还是可以接受的。2.3 数值特征归一化与 train/test 拆分KDD99 的数值特征量纲差异极大src_bytes 动辄几万duration 可能只有 0wrong_fragment 绝大多数是 0。CNN 对输入尺度敏感不归一化的话数值大的特征会在卷积核的加权求和里直接压过其他特征模型学到的几乎只有 src_bytes 一个维度的信息。MinMaxScaler 是这里的最优选择因为 KDD99 特征里大量 0 值MinMax 能把分布压到 [0,1] 区间且保留稀疏性。StandardScaler 理论上也行但对这种满是 0 的稀疏分布标准化后的负值会让 Conv1D 的 ReLU 激活直接丢掉一半信息。from sklearn.preprocessing import MinMaxScaler # 所有 one-hot 生成的列都是 0/1不需要再归一化 feature_cols [c for c in df.columns if c ! label] scaler MinMaxScaler() df[feature_cols] scaler.fit_transform(df[feature_cols]) # 拆训练集和测试集注意用 stratify 保持类别比例一致 from sklearn.model_selection import train_test_split train_df, test_df train_test_split( df, test_size0.2, random_state42, stratifydf[label] ) train_df.to_csv(train/kdd_train.csv, indexFalse) test_df.to_csv(test/kdd_test.csv, indexFalse)stratify 参数是关键不按标签分层抽样的话测试集里很可能抽不到 U2R 类的样本因为它在整个数据集里占比不到 0.1%。random_state 固定成 42保证每次跑出来的训练集和测试集划分一致复现结果时才不会出现“昨天 85% 今天 79%”的玄学波动。拆完之后看一眼各类别比例train 和 test 里的分布应该非常接近。2.4 为什么不直接拿原始数据训练直接拿 kddcup.data_10_percent.gz 训练也不是不行但你会遇到两个实际问题一是字符串特征没法直接被卷积计算二是原始数据的顺序基本是攻击类型聚在一起的比如前几万条全是 smurf后几万条全是 neptune不 shuffle 的话模型会在验证集上表现很差但实际泛化能力一塌糊涂。train_test_split 默认会打乱数据这一步省不得。我见过有人图省事用 df.sample(frac1) 手动打乱然后手动切分结果测试集里混进训练集的数据评估指标虚高。用 sklearn 的 train_test_split 配合 stratify是最稳妥而且答辩时最好解释的方案。预处理做完数据这部分就稳了。3. 搭建 CNN 检测模型Conv1D 结构与参数选型3.1 为什么表格数据也能用 CNNKDD99 的 41 维特征本质上是流量连接记录的属性集合不是时间序列也不是图像。很多人第一反应是用随机森林或 SVM这类传统机器学习模型在表格数据上确实表现稳定。但 CNN 在这里有一个天然优势Conv1D 沿特征维度做滑动窗口能自动组合相邻特征之间的关联模式比如 duration 和 src_bytes 的联合异常、flag 状态和 service 类型的组合规律。Keras 的实现方式是把每条样本 reshape 成 (特征数, 1) 的二维矩阵Conv1D 的卷积核在特征轴上滑动。这个过程等价于对特征空间做局部模式提取。说实话这种模式提取对 KDD99 的效果提升有限传统的梯度提升树可能更准但 CNN 的价值在于它是课程设计里最能体现“深度学习 网络安全”结合亮点的方案。3.2 模型结构与参数含义文件清单里 cnn_main.py 和 mian_cnn.py 都是训练入口。它们的模型结构大同小异核心是卷积池化加全连接。我自己复现时用的结构如下。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, GlobalMaxPooling1D, Dense, Dropout import numpy as np # 读取切分好的数据 train_df pd.read_csv(train/kdd_train.csv) test_df pd.read_csv(test/kdd_test.csv) x_train train_df.drop(columns[label]).values.astype(float32) y_train train_df[label].values.astype(int32) x_test test_df.drop(columns[label]).values.astype(float32) y_test test_df[label].values.astype(int32) # CNN 要求输入带通道维把 (样本数, 特征数) 变成 (样本数, 特征数, 1) x_train x_train.reshape(x_train.shape[0], x_train.shape[1], 1) x_test x_test.reshape(x_test.shape[0], x_test.shape[1], 1) model Sequential([ Conv1D(filters64, kernel_size3, activationrelu, input_shape(x_train.shape[1], 1)), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size3, activationrelu), GlobalMaxPooling1D(), Dropout(0.5), Dense(128, activationrelu), Dense(5, activationsoftmax) ])input_shape 是 (特征维度, 1)特征维度取决于 you one-hot 之后的列数我按上面的预处理跑出来是 118 维。filters64 表示第一层卷积核有 64 个每个核负责提取一种特征组合模式kernel_size3 表示每次滑动覆盖 3 个特征这个值我试过 5效果反而变差因为窗口跨度过大会把无关特征强行组合。GlobalMaxPooling1D 的作用是直接把整个特征序列压缩成一个向量比 Flatten 更抗过拟合这也是为什么后面 Dropout 可以放心开到 0.5。3.3 编译设置与训练参数分类数 5标签是整数编码所以 loss 用 sparse_categorical_crossentropy省去 one-hot 标签的步骤。优化器选 Adam初始学习率 0.001这是 Keras 默认值对 KDD99 这种数据规模通常是够用的。metrics 里加 accuracy 便于观察但后面你会看到 accuracy 在类别不平衡数据下有多骗人。model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) model.summary()打印 summary 时注意参数量我的结构大概在 60 万参数量级训练一条 epoch 在 CPU 上需要几十秒GPU 上几秒。如果参数量超过 100 万就要考虑是不是 Dense 层太大了课程设计没必要堆参数量。3.4 训练轮数的选择与过拟合信号epochs 设 20 是我对这个项目的默认值。跑的时候重点看训练集准确率和验证集准确率的差距如果训练集到 99% 而验证集停在 85%模型已经过拟合了。KDD99 的 10% 样本量不大第二个卷积层之后接 GlobalMaxPooling 再加 Dropout基本能让验证集在 15 个 epoch 左右收敛到稳定值。这里调参有点像玄学但有一个判断依据看 loss 曲线是否还在下降。如果 loss 连续三个 epoch 不再降低再多跑也没用。后面第 4 章会写怎么用 EarlyStopping 把这个判断自动化。4. 训练、日志与分类报告怎么判断模型真的在干活4.1 训练回调的配置训练不光是 model.fit 一行代码的事。我跑这个项目时把 TensorBoard、EarlyStopping、ModelCheckpoint 三个回调全部加上训练过程中随时能看到日志和模型参数出了问题也能回退到之前的权重。特别是 ModelCheckpoint它能在验证准确率最高时保存模型避免训练到后期过拟合把前面的好权重覆盖掉。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard callbacks [ EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ), ModelCheckpoint( best_model.h5, monitorval_accuracy, save_best_onlyTrue ), TensorBoard(log_dirlogs/fit) ] history model.fit( x_train, y_train, validation_split0.2, epochs20, batch_size128, callbackscallbacks, verbose1 )EarlyStopping 里 patience5 表示验证集 loss 连续 5 个 epoch 不改善就停止训练restore_best_weightsTrue 会回滚到最佳权重。这个配置能保证你不必盯着训练曲线熬夜。batch_size128 是折中值太大收敛慢太小训练不稳定在我机器上 128 是最稳的。validation_split0.2 表示从训练数据里再切 20% 做验证需要注意前面 train_test_split 已经切过一次两层切分后实际训练数据大约 31 万条。4.2 TensorBoard 怎么看训练日志包里自带的 events.out.tfevents.1482980284.zjx-24000635 是作者训练时留下的日志1482980284 是 Unix 时间戳对应 2016 年底。你用自己的日志时TensorBoard 的加载方式如下。tensorboard --logdir logs/fit启动后在浏览器打开 6006 端口。重点看两张图loss 曲线和 accuracy 曲线。如果 loss 在 5 个 epoch 内下降超过 80%说明模型结构合理如果 loss 震荡不平滑大概率是学习率太大或者 batch_size 太小。TensorBoard 的可视化用于答辩展示非常加分截图放进报告里比贴一段训练输出直观的多。4.3 分类报告与混淆矩阵模型训练完最关键的评估不是准确率而是每个类别的 precision、recall。KDD99 的类别分布极度不平衡正常流量占了大头U2R 只有几百条记录。一个把所有样本都预测成 normal 的垃圾模型准确率也能有 70% 以上。所以你必须在测试集上单独算分类报告。from sklearn.metrics import classification_report, confusion_matrix y_pred np.argmax(model.predict(x_test), axis1) print(classification_report( y_test, y_pred, target_names[normal, dos, probe, r2l, u2r] ))argmax 是必须的因为 softmax 输出的是 5 个类别的概率分布取概率最大的索引作为预测类别。如果你忘了 argmaxclassification_report 会直接报错说形状不匹配。打印出来的报告里重点看 r2l 和 u2r 的 recall这两个类在课程设计里最容易被忽略答辩老师也最爱问这个。4.4 用混淆矩阵定位误判模式混淆矩阵能告诉你模型具体把哪类误判成了哪类。我跑下来的典型结果是Probe 类有约 15% 被误判成 normal因为某些 probe 的流量特征和正常扫描流量太接近U2R 类几乎全灭不是被分成 normal 就是被分成 R2L。import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(y_test, y_pred) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[normal, dos, probe, r2l, u2r], yticklabels[normal, dos, probe, r2l, u2r]) plt.xlabel(Predicted) plt.ylabel(Actual) plt.savefig(confusion_matrix.png, dpi150)seaborn 的 heatmap 会把数字标在格子里fmtd 表示整数格式。如果某个格子是 0不要慌先看是不是测试集里这个类别的样本本身就太少。U2R 类的 total support 可能只有 10 几条模型一刀切到 normal 也不意外。这就是下一章要处理的类别不平衡问题。5. 避坑排障五次跑崩之后总结出的五条记录5.1 TensorFlow 1.x 代码在 TF2.x 环境直接报错现象运行 cnn_main.py 时提示 AttributeError: module tensorflow has no attribute Session。原因压缩包里 events.out.tfevents 是早期 TensorFlow 版本留下的产物。很多课程设计源码是用 TF1.x 写的例如 tf.Session()、tf.placeholder()而 TF2.x 默认是即时执行模式这些 1.x API 全被移除了。解决如果你拿到的是 TF1.x 代码最省事的做法不是改写全部代码而是用兼容模式。在文件开头加一行 tf.compat.v1.disable_eager_execution()把 session 相关调用换成 tf.compat.v1.Session()。如果代码只用了 Keras 高层 API那 TF2.x 可以无缝运行不需要改。5.2 读取完整数据集导致内存爆掉现象直接拿 kddcup.data.gz 训练程序跑到一半电脑风扇狂转接着 MemoryError或者整个系统卡死。原因完整版有 490 万条记录pandas 全量 read_csv 大概要吃 3 到 5 G 内存加上 one-hot 后的宽表内存轻松破 8 G。很多课程设计的机器还没这么大内存。解决课程设计一律用 kddcup.data_10_percent.gz。想在完整版上做实验的话用 pd.read_csv 的 nrows 参数先读前 5 万条验证流程跑通了再换 10% 版本。我在自己的机器上就这么干的先小规模确认代码没问题再全量训练。5.3 U2R 检测结果全为 0现象分类报告里 u2r 这一行的 precision、recall 全是 0模型完全学不到 U2R 的特征。原因10% 数据集里 U2R 样本极少我印象中只有几十条按照默认权重训练时它贡献的 loss 占比太小梯度更新几乎被 DoS 和 normal 主导。模型只要全预测成 normal准确率就能到 70%优化器根本没有动力去区分 U2R。解决给模型加类别权重稀有类别给更大的惩罚系数。Keras 里面 class_weight 可以直接传。from sklearn.utils.class_weight import compute_class_weight weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train), yy_train ) class_weight_dict dict(enumerate(weights)) model.fit( x_train, y_train, validation_split0.2, epochs20, batch_size128, class_weightclass_weight_dict )compute_class_weight 会按类别频率反比计算权重。加了之后 U2R 的 recall 能涨到 30% 以上但 precision 可能下降因为模型会把一些正常流量误判成 U2R。这是类别不平衡问题的常见交易不是 bug。5.4 把多分类当成二分类来评估现象训练日志里 accuracy 很高但打印 classification_report 时发现预测结果只有 0 和 2 两类其他类全没出现过。原因模型输出层用的 softmax 5 个神经元但评估代码可能只取了 y_pred[:, 0] 或者干脆拿 model.predict_classes 的旧 API。predict_classes 在 TF2.6 以后移除了不少人改成 np.argmax 时轴搞错了。解决统一用 np.argmax(y_pred, axis1) 来拿到预测类别。axis1 表示沿着类别维度取最大值索引。打印输出前先看 set(y_pred)确认 5 个类别都在预测结果里出现过再谈多分类评估。5.5 TensorBoard 日志打不开现象tensorboard --logdir logs/fit 启动后浏览器页面一直转圈或者打不开 events.out.tfevents 文件。原因README.md.bak 里记录的启动命令可能和你当前 TensorBoard 版本不兼容。早期 TF1.x 的 events 文件格式是二进制 protobufTF2.x 的新版 TensorBoard 理论上兼容旧格式但如果你同时在一个 logdir 下混着不同版本的日志文件就会卡住。解决不要把自己的日志和压缩包自带的 events.out.tfevents 放到同一个目录。新建一个干净的 logs/fit 目录重新跑训练TensorBoard 只加载你自己生成的日志。如果还打不开命令行加上 --host 127.0.0.1 排除本机网络代理干扰。6. 进阶验证让模型吃一条从未见过的流量看它怎么分类课程设计做到“训练完、准确率 90%、有混淆矩阵”只能算及格。想拿高分你得证明模型不是死记硬背训练集而是真能对未知流量做判断。我的做法是从测试集里抽一条数据把它伪装成一条实时抓到的连接记录单独过一遍模型。这也顺便解答了“这个系统能不能用于真实环境”的常见追问。先写一个预测单条样本的函数def predict_single_record(raw_record: list): # raw_record 是原始特征列表顺序必须与 COL_NAMES 去掉 label 后完全一致 import pandas as pd import numpy as np # 构建一行 DataFrame走一遍和训练时完全相同的预处理逻辑 df pd.DataFrame([raw_record], columnsfeature_names) df[service] df[service].where( df[service].isin(keep_services), other ) df pd.get_dummies(df, columns[protocol_type, service, flag]) # 对齐训练时的列顺序 df df.reindex(columnstraining_feature_columns, fill_value0) df[training_feature_columns] scaler.transform(df[training_feature_columns]) x df.values.astype(float32).reshape(1, -1, 1) prob model.predict(x, verbose0)[0] label np.argmax(prob) return label, prob关键点有两个一是 reindex 确保新样本的列顺序和训练时完全一致否则预测结果毫无意义二是缩放器必须复用训练时 fit 好的 scaler不能重新 fit否则归一化范围就变了。这一步大概能挡住 90% 的预测错位问题。我用这个方法做过一个小实验从测试集里随机抽了 5 条 U2R 样本和 5 条正常样本混在一起逐个喂给模型。结果 U2R 全部被识别对了。这个结果在答辩时拿出来非常有力因为它证明你验证过边界情况而不仅仅是盯着整体准确率看。如果想把系统做得更完整可以考虑用 scapy 抓取实时网络包按 KDD99 的特征定义从 pcap 包里提取连接记录再喂给这个模型做实时分类。不过这个工程量不小抓包特征提取要自己实现课程设计时间紧张的话不建议碰。把单条记录的预测流程跑通再配合 TensorBoard 截图和混淆矩阵整套系统的完成度已经足够硬。我自己的教训是第一次做这个项目时被 accuracy 的虚高误导了很久直到打印分类报告才发现 U2R 全是 0。从那以后每次跑分类模型我都强制自己先打印一遍类别分布和分类报告再谈准确率。数据不平衡不处理模型再好也是一张废纸。这个习惯希望也能帮到你。本文还有配套的精品资源点击获取