
简介一套基于卷积神经网络的网络入侵检测系统完整实现面向网络空间安全、人工智能相关专业的学生及毕业设计开发者解决网络流量分类与异常检测问题。系统利用CNN提取流量数据局部特征可区分正常与恶意流量覆盖入侵行为、恶意软件活动及DoS攻击等场景并包含数据预处理、模型设计、训练、评估与优化全流程。压缩包共16个文件以Python源码为主4个py配备KDDCUP数据集的10%与全量数据gz格式、训练日志事件文件、README说明及IDE配置参考包体约17.52MB。读者可直接运行main.py复现实验也可对照源码梳理CNN在入侵检测中的落地思路或基于现有结构替换数据集进行改进。资源已有350人学习浏览适合需要完整可运行方案、数据支撑和调参参考的入门及进阶学习者。1. 把 KDD99 喂给 CNN这套入侵检测源码到底怎么落地很多人一听到“网络入侵检测”就觉得要上抓包、要搞特征工程、要写一堆规则直到看到这份基于卷积神经网络的检测系统才发现原来把网络流量当成“信号序列”直接喂给 CNN也能把 KDD99 数据集上的准确率干到 99.5%。这套 python 源码包的核心思路很简单把每个网络连接转换成一条数值特征序列再让一维卷积神经网络自动提取局部模式区分正常流量和攻击行为它不依赖人工写攻击特征而是让模型自己去学“什么样的流量看起来不对劲”。这套资源适合两类人一类是做毕业设计需要一套能跑通、能出数据、能讲清楚原理的深度学习实战项目另一类是刚接触用深度学习做安全方向的工程师想找一个经典数据集和参考实现搞清楚数据怎么清洗、标签怎么编码、CNN 输入 shape 怎么设计。压缩包里的文件很全有 KDD99 的原始数据有训练用的 main_cnn.py还有处理数据的 handle2.py外加 TensorBoard 的日志目录基本不用自己再去找数据集解压就能开搞。2. 数据解剖KDD99 的 41 列特征和五类攻击标签2.1 每一列特征到底代表什么KDD99 是入侵检测领域绕不开的基准数据集它来自模拟军事网络环境采集的 TCP 连接记录每条记录对应一次完整的连接会话包含 41 个特征值。这套源码用的是 kddcup.data_10_percent.gz也就是 10% 的抽样版本大概有 49 万条连接记录跑起来内存压力不大普通笔记本 GPU 或纯 CPU 都能训。能直接送到 CNN 里的值是数值型特征。比如duration表示连接持续时间秒src_bytes和dst_bytes是源到目的、目的到源的字节数count是过去两秒内与当前连接相同目标的连接数srv_count是过去两秒内相同服务的连接数。这些数值里藏着攻击行为的关键线索短时间大量连接、传输字节数异常偏大或偏小、连接失败率突增都是入侵的典型信号。但 KDD99 里也有非数值特征最典型的是protocol_typetcp/udp/icmp、servicehttp/ftp/telnet 等、flag连接状态标志比如 SF 表示正常建立并关闭。这三个字段必须做编码不能直接把字符串喂给 TensorFlow否则模型直接把 categorical 字符串当成数值比较大小训练出来的模型一点意义都没有。在这个源码里handle2.py 就是专门干这个事的。2.2 攻击类型从 22 种细分到四类大类KDD99 训练集的攻击标签一共有 22 种但可以粗分为四大类加上 normal 就是五分类。源码在训练时常见的做法是把细分类映射到大类比如smurf、neptune归为 DOS拒绝服务攻击ipsweep、portsweep、satan归为 Probe探测与扫描guess_passwd、warezmaster归为 R2L远程到本地的未授权访问buffer_overflow、rootkit、loadmodule归为 U2R提权攻击。这样分的好处是样本数量均衡一些否则像 U2R 这类样本极少模型会学得很吃力。所以在做标签编码时我一般建议按大类做一个 label_map映射表大概长这样attack_map { normal: normal, neptune: dos, smurf: dos, back: dos, teardrop: dos, pod: dos, land: dos, ipsweep: probe, nmap: probe, portsweep: probe, satan: probe, guess_passwd: r2l, warezmaster: r2l, imap: r2l, ftp_write: r2l, multihop: r2l, phf: r2l, buffer_overflow: u2r, rootkit: u2r, loadmodule: u2r, perl: u2r }这一段代码看起来简单但决定了整个分类任务的走向。如果你直接用 22 个细分标签做分类DOS 大类里某些子类样本量差距可能有几百倍模型容易偏向样本多的类且评估指标会很丑。归并成五类之后类别间样本量差距仍然有但模型在损失函数上不会那么容易被少数类带偏。2.3 为什么用一维 CNN 而不是全连接网络网络流量特征在预处理后本质上是一条定长向量把它看成一维信号、用卷积核滑动提取局部模式理论上是说得通的。比如count、srv_count、serror_rate这几个特征在位置上相邻卷积核能捕捉它们之间的局部关联比如“短期内大量失败连接”这样一个模式可以跨特征被识别出来而不是像全连接网络那样把每个特征当成独立输入重新去学习特征组合。这个源码的输入 shape 是 41 维特征向量reshape 成[batch, 41, 1]之后做 Conv1D。1×3 的卷积核扫过相邻特征等于是每次看三列特征组成的局部模式这种设计比全连接 Dense 层参数少很多训练速度快而且在小数据集上不容易把噪声特征也学进去。虽说 KDD99 本身是 1999 年的数据拿到今天做真实网络检测意义有限但拿来练手 CNN 处理一维序列、跑通整套工程流程依然是最省事的路径。3. 数据预处理从原始文本到模型能吃的 NumPy 张量3.1 解析原始 gz 文件标称特征和数值特征分离KDD99 原始数据是文本格式逗号分隔最后一位是标签。拿到手之后不能直接pd.read_csv就完事因为协议类型、服务名、flag 标志这三列是字符串需要先单独映射成整数再拼回特征矩阵。源码里的 handle2.py 解决的就是这个问题它的核心逻辑是先读 Gzip 文件逐行切分把符号特征做编号比如tcp0, udp1, icmp2。下面是我常写的解析逻辑和源码实现基本一致import gzip import numpy as np import pandas as pd def load_kdd99(path): # 读 gz 压缩的原始数据 with gzip.open(path, rt) as f: df pd.read_csv(f, headerNone) # 第 1 列 protocol_type第 2 列 service第 3 列 flag # 这三列是标称特征手动转成整数编号 df[1] df[1].map({tcp: 0, udp: 1, icmp: 2}).fillna(0) df[2] df[2].astype(category).cat.codes df[3] df[3].astype(category).cat.codes # 第 41 列是标签0 起始其余 1~40 是特征 labels df[41].values features df.iloc[:, :41].values.astype(np.float32) return features, labels注意df.iloc[:, :41]这里取的是前 41 列但第 41 列是标签所以实际上取了 0 到 40 列作为特征labels 单独取第 41 列。KDD99 原始字段索引是 0 到 42最后一个是标签中间 41 个是特征取列的时候差一个索引就容易错位。fillna(0)是为了兜底如果协议类型里出现tcp之外的值比如icmp能匹配到但万一原始数据里有大小写差异或多余空格映射不到的值会变成 NaN直接填充成 0 保底不让它把整列数据搞崩。astype(category).cat.codes是把服务名和 flag 这种变量自动编码成整数编码顺序按出现频率或字母序不固定但稳定。3.2 数值特征的归一化防止量纲把 CNN 带偏KDD99 的数值特征里src_bytes可能大到几百万而serror_rate是 0 到 1 之间的小数如果不做归一化卷积核的梯度会被大数值的特征主导模型训练会变得极其不稳定。这里的常见做法是用 Min-Max 标准化把每个特征缩放到 [0, 1] 区间或者用 StandardScaler 做 Z-score 标准化看训练效果选。我在这套源码里更推荐 Min-Max因为 KDD99 的特征分布大多数不服从正态分布Z-score 对偏态分布不友好。实现上有个细节归一化的均值和最小值/最大值要只在训练集上拟合然后用同一套参数去转换验证集和测试集不能拿整份数据一起 fit否则会有数据泄漏测试集的信息提前进入训练过程评估指标虚高。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # X_train 是切分后的训练集特征 scaler.fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test) # 把特征矩阵 reshape 成 [样本数, 41, 1]适配 Conv1D 输入 X_train_cnn X_train_scaled.reshape(-1, 41, 1) X_test_cnn X_test_scaled.reshape(-1, 41, 1)这一段是模型是否能收敛的关键。reshape(-1, 41, 1)的含义是不管多少条样本每条样本都是一个 41 步长的单通道序列1 相当于图像的通道数。很多新手直接拿[batch, 41]喂给 Keras 的 Conv1D会报维度错误因为 Conv1D 在 Keras 里的输入形状是(batch, steps, channels)中间必须插一个维度。3.3 标签的 one-hot 编码交叉熵损失的标配输出格式这个源码做的是五分类任务normal、dos、probe、r2l、u2r类别是离散的、没有天然顺序不能直接当回归做。输出层如果用 Dense(5, activationsoftmax)配合categorical_crossentropy损失标签就必须是 one-hot 形式也就是每个标签变成一个长度为 5 的向量对应位置是 1其余是 0。to_categorical是 Keras 提供的工具函数一行就能搞定。但注意它要求类别索引从 0 开始连续编号当你做完 label_map 映射后要先用np.unique检查一下标签值是不是 0 到 4如果有类别是字符串或者编号不连续也会出问题。from tensorflow.keras.utils import to_categorical num_classes 5 y_train_onehot to_categorical(y_train_mapped, num_classesnum_classes) y_test_onehot to_categorical(y_test_mapped, num_classesnum_classes)参数num_classes必须和模型输出层的神经元个数一致否在训练时会报形状不匹配的错。如果输出层是 Dense(4) 而标签是五类最后一步在计算损失时维度对不上整个 fit 直接中断。4. CNN 模型设计一维卷积怎么从 41 维向量里抓攻击特征4.1 网络结构卷积层 池化层 全连接层这套源码的模型结构是典型的 1D-CNN 分类器核心由两个 Conv1D 层、一个最大池化层和两层全连接组成输出五分类。具体结构可以概括成输入层41×1→ Conv1D(64, kernel3, ReLU) → Conv1D(128, kernel3, ReLU) → MaxPooling1D → Flatten → Dense(128, ReLU) → Dropout(0.5) → Dense(5, Softmax)。为什么第一层卷积核要选 64 个、卷积核大小选 3因为 KDD99 的特征维度只有 41卷积核尺寸太大比如 7 或 11一次滑窗就把小半条序列盖住了提取的特征太粗糙尺寸为 3 是最常见的起步选择能捕捉相邻三列特征的局部关联同时也方便堆多层网络。第一层 64 个卷积核负责学低层模式比如“流量比例突然变大”第二层 128 个卷积核在低层模式基础上组合出更高层的语义比如“多个异常特征同时出现”。池化层用 MaxPooling1D(pool_size2)作用是把卷积层的输出缩小一半保留最显著的特征值降低计算量同时也减少过拟合。Flatten 层把二维特征图拉平成一维向量并接 Dense 层做非线性组合。4.2 Keras 实现每一层参数的含义from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout def build_cnn(input_shape(41, 1), num_classes5): model Sequential() model.add(Conv1D(filters64, kernel_size3, activationrelu, input_shapeinput_shape, paddingsame)) model.add(Conv1D(filters128, kernel_size3, activationrelu, paddingsame)) model.add(MaxPooling1D(pool_size2)) model.add(Flatten()) model.add(Dense(units128, activationrelu)) model.add(Dropout(rate0.5)) model.add(Dense(unitsnum_classes, activationsoftmax)) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) return model model build_cnn() model.summary()paddingsame的作用是让卷积操作不改变序列长度输入 41 维卷积后输出还是 41 维配合步长 1。如果不做 padding每过一层卷积序列长度就会减 2三层下来只剩 35 维也不是不行但会丢失边缘特征对 KDD99 这种每个特征都很关键的数据来说same padding 更稳。Dropout(0.5) 是防过拟合的核心训练时随机掐掉一半的神经元连接测试时全部保留这能有效对抗 KDD99 训练集和测试集样本量不均匀的问题。4.3 为什么不用 LSTM序列长度只有 41 步优先考虑效率和并发有人会问网络入侵检测是和时间序列相关的任务为什么不用 LSTM 或者 GRU这个问法本身有道理但对 KDD99 这个场景行不通。KDD99 的每条样本是独立的连接记录虽然 41 维特征里有过去的统计信息但它本身不是一个时间序列输入——每行与每行之间没有时序依赖。CNN 在这里的优势是训练速度快Conv1D 可以并行计算每个 batch 的不同样本互不干扰而 LSTM 必须一步步串行处理训练时间会拉长很多。其实真正适合 LSTM 的入侵检测场景是把多条连续连接记录按时间窗口拼接比如一次攻击会话包含哪几条连接LSTM 在窗口内部做序列建模。但这份源码没走这个路线它的特点是轻量、可解释性好、训练稳定作为毕业设计和入门项目非常合适。如果你想深挖可以在后续换成一个拼接窗口的版本把 5 条相邻连接记录叠在一起变成一个 205 维的输入但现在先把当前这套跑通再说。4.4 超参数调整从哪里开始改怎么判断方向自己动手调参时第一个要改的不是网络层数而是 batch size 和初始学习率。KDD99 这个数据集规模不大常见做法是 batch size 取 128 或 256。如果训练时 loss 振荡很厉害不收敛优先把 Adam 学习率从默认的 0.001 往下降到 0.0001这是最常见的稳定化手段。如果训练集准确率高但验证集准确率上不去典型症状是过拟合这时候加 Dropout 的比例从 0.3 升到 0.6或者把第一层卷积的 filters 从 64 降到 32。如果两边准确率都上不去梯度消失了先把网络改浅去掉第二个 Conv1D 层试试。每改一次参数记录一组 train/val 准确率你会发现 KDD99 这套数据对网络深度的容忍度很高加一层不会爆减一层也不会崩这是它适合做实验的原因。5. 模型训练与评估99.5% 的正确率从哪里来哪里可能有水分5.1 早停与模型保存不能让模型在验证集上慢慢变差训练 CNN 模型最怕两种情况一种是不收敛另一种是收敛过头导致过拟合。第一种调学习率第二种靠 EarlyStopping。它的原理很简单监控验证集损失如果连续若干个 epoch 不下降就停止训练并把最佳权重还原回来避免后面越训越差。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks [ EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_accuracy, save_best_onlyTrue, verbose1) ] history model.fit( X_train_cnn, y_train_onehot, batch_size256, epochs50, validation_split0.2, callbackscallbacks, verbose1 )重要的是patience参数设为 5 表示验证损失连续 5 个 epoch 都没降到历史最低就提前终止。KDD99 训练时模型经常在前 10 个 epoch 冲到 98% 以上但后面提升很慢如果你不看验证集只死训 50 个 epoch训练集能到 100%验证集可能反而掉到 96%这就是过拟合EarlyStopping 就是防止这个过程发生。ModelCheckpoint只保存验证集准确率最高的那个模型后面做测试集评估都用它别拿最后一个 epoch 的权重直接测。5.2 不要只看准确率KDD99 的类别不平衡陷阱这个源码标题写 99.5% 正确率说实话在 KDD99 上做到这个数并不稀奇因为预处理做得好、特征区分度本来就很强。但这不代表模型在所有攻击类型上都表现优秀U2R 和 R2L 这两类样本极少准确率很高会被 DOS 类“平均”上去——如果 DOS 类样本占 70% 以上模型只要学会了识别 DOS整体准确率就会显得很漂亮但对 R2L 的识别准确率可能不到 50%。所以评估时必须打印每一类的 precision、recall、F1 分数不能只看一条 accuracy。在 Keras 训练完之后用 scikit-learn 的 classification_report 是最省事的做法注意 predictions 要先做argmax转回类别索引再和真实标签对比。from sklearn.metrics import classification_report, confusion_matrix y_pred model.predict(X_test_cnn) y_pred_classes np.argmax(y_pred, axis1) y_true_classes np.argmax(y_test_onehot, axis1) print(classification_report(y_true_classes, y_pred_classes, target_names[normal, dos, probe, r2l, u2r]))这里一个关键点是axis1因为每行是一个样本的五个类别概率argmax在行方向求最大值索引才是该样本的预测类别。有些新手写成np.argmax(y_pred)默认是拉平整个数组找全局最大值结果得到一个标量后面的对比全乱套。如果需要同时保存混淆矩阵可以直接把confusion_matrix的返回值写进 CSV 里方便论文画图和答辩展示。TensorBoard 日志目录里的events.out.tfevents.*文件就是这个源码训练时自动生成的如果你重新跑训练可以用tensorboard --logdirmulti_logs查看训练曲线。5.3 训练中的避坑记录现象 1训练开始后 loss 一直是 NaN原因特征数据里有 NaN 值没处理干净或者学习率过大导致梯度爆炸。KDD99 原始数据极少出现缺失值但预处理时字符串映射如果没兜底会出现 NaN 悄悄混进数值矩阵。解决在归一化前对特征矩阵执行np.isnan(features).any()检查如果确认无 NaN 就把 Adam 学习率降到 0.0001重新训练。现象 2验证集准确率高但用测试集一测准确率差很远原因KDD99 的 10% 训练集和 corrected 测试集的攻击分布不完全一样比如测试集包含训练集没有的攻击子类型模型没见过这类型就有可能在测试集上失分。解决不要拿 10% 训练集的一部分当测试集来评估尽量从完整 kddcup.data.gz 里切分或者把测试评估标准定为“对已知攻击的识别率”并在结论里写明。现象 3加载kddcup.data_10_percent.gz时 pandas 直接报解析错误原因原始 txt 文件列数不固定某些行的字段数量和其他行不一致通常是末尾标签前的逗号多出来。解决在pd.read_csv里加error_bad_linesFalse老版本 pandas或on_bad_linesskippandas 2.0把这行坏数据跳过。我在实际处理 KDD99 时经常遇到这种情况基本每一条坏的记录都出现在数据文件的中后段。5.4 完整训练脚本的结构建议源码里有main_cnn.py和cnn_main.py两份入口文件两者很可能只是命名差异内容本质类似。我的习惯是训练、预测、评估拆分成三个函数主函数里按顺序调用避免全部挤在同一个文件里改参数时无从下手。推荐一个极简的脚本框架def main(): X_train, y_train load_kdd99(kddcup.data_10_percent.gz) X_train_cnn, y_train_onehot preprocess_data(X_train, y_train) model build_cnn(input_shape(41, 1), num_classes5) train_model(model, X_train_cnn, y_train_onehot) test_model(best_model.h5, X_test_cnn, y_test_onehot) if __name__ __main__: main()这套结构的好处是每块逻辑解耦改数据路径不用动模型换模型不用碰数据调参数只动一处适合反复实验。6. 从训练到上线用一维 CNN 做实时单条流量预测训练完模型之后真正的考验是拿一条“新”流量记录做推理。如果把这条记录再拆开分析你会发现整个过程和训练时一样的坑比如特征顺序不能乱、标称特征要按训练时的映射表编码、数值特征要按同一套 Min-Max 参数缩放任何一个步骤不一致预测结果就可能完全不可信。这里有个最容易被忽视的问题MinMaxScaler 在训练时保存的data_min_和data_max_数组必须随着模型一起落盘推理时重新加载后先做一遍 transform再 reshape。如果一个新样本的src_bytes超过训练集的 max 值MinMax 会得到 1 以上的值这不影响模型推理但说明这个样本和训练集的分布已经有偏差了预测结果的置信度要打折。另外要注意 KDD99 的标称特征编码表如果训练时用.cat.codes自动编码推理时用同样的操作会遇到“新类别没有编码对应关系”的情况。解决办法是训练完成后把映射关系直接存成 Python 字典或 JSON。如果新流量里出现训练集没见过的 service比如http_8080直接填一个默认编号 0不要让它报 KeyError这是实战里很常见的翻车点。import json import numpy as np from tensorflow.keras.models import load_model def predict_one(raw_record): # raw_record 是逗号分隔的字符串长度 42 字段 parts raw_record.strip().split(,) if len(parts) ! 42: raise ValueError(f字段数不对: {len(parts)}) # 标称特征映射和训练时保持一致 protocol_map {tcp: 0, udp: 1, icmp: 2} service_map json.load(open(service_map.json)) flag_map json.load(open(flag_map.json)) features [] features.append(protocol_map.get(parts[1], 0)) features.append(service_map.get(parts[2], 0)) features.append(flag_map.get(parts[3], 0)) for i in list(range(4, 41)) [0]: features.append(float(parts[i])) features np.array(features, dtypenp.float32).reshape(1, 41, 1) scaler joblib.load(scaler.pkl) features_norm scaler.transform(features.reshape(1, 41)) features_final features_norm.reshape(1, 41, 1) model load_model(best_model.h5) prob model.predict(features_final, verbose0)[0] classes [normal, dos, probe, r2l, u2r] return classes[np.argmax(prob)], float(np.max(prob))这段代码里最容易漏掉的是parts[0]也就是duration特征。上面for i in list(range(4, 41)) [0]的意思是先取第 4 到第 40 个特征再把第 0 个拼回来这个顺序看起来别扭但如果你的特征工程最终排好的列顺序就是这样映射的推理时就必须按同一套顺序装配。如果你拿这份源码做实时监控建议把上面的推理函数套在一个死循环或消息队列消费者里数据源来自 Zeek 或 Suricata 输出的连接日志转成 KDD99 格式后逐条送入模型。这套方案对单条推理的延迟基本在 10 毫秒以内如果瓶颈在数据预处理可以考虑只取关键特征列比如官方给的 13 个最重要特征来缩短reshape前的转换时间。从误报角度看CNN 在 KDD99 上的误报率很低但换到真实网络环境时会有分布偏移表现可能打折。我每次跑完这套脚本都会用训练时预留的验证集样本和完全随机的向量各测一条随机向量的预测概率如果超过 0.9我会立刻检查归一化参数是不是加载错误了判断不准的时候看分布的置信度差异而完全不参考模型给的准确率数字。希望这套方法能帮你在自己的数据集上少踩几个坑快速跑通 CNN 做入侵检测的完整链路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。