ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

一维CNN在KDD99入侵检测中的实践:从预处理到99.5%准确率

一维CNN在KDD99入侵检测中的实践:从预处理到99.5%准确率 简介一套基于卷积神经网络的网络入侵检测项目使用经典 KDD Cup 数据集完整覆盖从网络流量数据预处理、CNN 模型搭建、训练分类到异常检测评估的整个流程。项目面向 Python 学习者、毕业设计学生及网络安全入门研究者可用于区分正常流量与恶意流量实测准确率约 99.5%。包内共 16 个文件含 4 个 Python 源码文件、2 个 gz 格式原始数据包、3 个 TensorFlow 事件日志文件以及配置类 xml、工程文件 iml 和 README 说明文档压缩包约 17.52MB已有 350 人学习下载。代码按数据加载、模型构建、训练评估等模块组织附带全部训练数据与可直接运行的源码README 给出运行说明训练脚本包含数据标准化、特征提取、模型评估和优化接口方便复现实验并在此基础上调整网络结构、超参数可用于课程设计、毕业设计或入侵检测方向入门参考。1. 从99.5%准确率说起CNN与KDD99为什么能配对说一个去年熬夜调项目时的直观感受KDD99上面跑全连接网络调了两周也卡在97%换成只有几层的一维卷积一个晚上就到了99.5%。这不是玄学而是把连接记录当成一条“带位置信息的序列”去看待卷积核天然擅长抓一段特征之间的局部关系。这个项目包正是这么做的源码里main.py、cnn_main.py两条入口配上kddcup.data_10_percent.gz数据跑通后能区分正常流量和DoS、Probe等异常流量。适合两类人拿它做课程设计或毕业设计、需要讲清原理又能复现的学生以及想看看CNN怎么处理表格型流量数据的工程师。下文从数据清洗开始把从原始数据到99.5%准确率的每个环节拆开讲。2. KDD99数据预处理从原始连接记录到CNN可用的张量2.1 数据格式与标签分布KDD Cup 1999 是入侵检测领域最常被搬运的数据集项目里的kddcup.data_10_percent.gz是 10% 抽样版本约 49 万条连接记录每条记录一行由 41 个特征和 1 个标签组成。这 41 个特征不是随意排列的前 9 个描述连接基本属性比如 duration、protocol_type、service、flag后面是内容特征和基于统计的流量特征。CNN 会把整行特征当作一个长度 41 的向量所以第一个坑就是不能改变特征顺序。标签不是简单的正常/异常。原始数据里有 23 种攻击类型加上 normal常见做法是先聚成四类DoS、Probe、R2L、U2R再把四类攻击统一映射成 1 表示异常normal 映射成 0。这样做的好处是模型只需要输出一个 sigmoid 概率直接把问题转成二分类。如果你要打印分类报告保留四类标签会更有说服力但保存模型时还是建议用二分类后续接入监控更容易写阈值。特征分组列名示例处理方式类别型特征protocol_type, service, flag独热编码连续数值特征duration, src_bytes, count, srv_count 等标准化攻击标签normal / 攻击类型二值化 数值化2.2 数据读取、标准化与独热编码我一般习惯把数据读取和预处理拆成独立的脚本项目里handle2.py正承担类似角色。先用 pandas 读取 gz 压缩文件手动给一份列名保证后续特征顺序固定。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler 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 ] # 共 41 个特征 df pd.read_csv(kddcup.data_10_percent.gz, headerNone, namescols [label]) df df.dropna() # KDD99理论上没有缺失但保留这一步防御意外namescols [label]是必须的否则 pandas 会把第一行当成表头。dropna 在原始数据上几乎不会删除样本但能避免训练时遇到 NaN 直接报错。缺失值在 KDD99 里基本不存在我不建议做填充入侵检测特征一旦被填充边界样本的判别信号就被抹掉了。接着处理三种类别特征。service 有约 70 个类别如果用 LabelEncoder 变成 0 到 69CNN 会误以为 protocol_type 的数值有大小关系tcp2 比 udp1 大两倍这种假设在流量语义里完全不成立所以走独热编码更稳妥。y_raw df[label].map(lambda x: 0 if x normal else 1) X df.drop(columns[label]) cat_cols [protocol_type, service, flag] X pd.get_dummies(X, columnscat_cols) scaler StandardScaler() X_scaled scaler.fit_transform(X) print(训练集大小: %d, 输入维度: %d % (X_scaled.shape[0], X_scaled.shape[1]))pd.get_dummies会自动在指定列上展开得到的特征维度在 120 左右具体数量取决于 service 的类别个数。StandardScaler 把所有数值特征拉到均值为 0、方差为 1这样卷积核在初始化时不会因为 duration 有几十万的大数值而偏向某一个方向。注意这里为了演示先 fit 了 scaler真实项目里应该先切分训练集和测试集再用训练集 fit、测试集只 transform否则测试集的信息会泄露进训练过程。2.3 切分与封装from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_scaled, y_raw, test_size0.2, random_state42, stratifyy_raw) X_train X_train[..., np.newaxis] X_test X_test[..., np.newaxis] print(X_train shape:, X_train.shape)stratifyy_raw是处理类别不平衡的第一步它保证训练集和测试集中正常/异常比例一致评估时不会因为抽样偏差出现虚假高分。最后一行把特征维度扩展成三维因为 Conv1D 的输入要求是(样本数, 特征长度, 通道数)这里通道数取 1。如果不做这一步模型定义阶段就会直接报错。提示独热编码后特征数量会变化所有后续代码里引用的 input_dim 都应以X_train.shape[1]为准不要写死成 41换数据子集时维度可能改变。3. 一维CNN入侵检测模型设计卷积核到底在提取什么3.1 为什么用一维卷积而不是全连接很多第一次看到这个项目的人会问流量特征又不是图像凭什么用 CNN 能把准确率从 97% 拉到 99.5%答案在于特征之间的局部相关性。例如serror_rate和srv_serror_rate同时为高往往意味着 SYN 错误比例异常same_srv_rate与dst_host_same_srv_rate的组合能反映端口扫描行为。这类“相邻几个特征联合判断”的模式全连接网络也能学但要堆大量参数一维卷积用一个小窗口滑过特征向量天然就能提取局部联合模式。关键在于特征顺序是有意义的。KDD99 特征并非随机排列前 9 个是连接基础属性后面是内容属性和基于时间窗口的统计特征同一个语义族群挨在一起。把特征列打乱卷积核学到的东西就变成随机拼凑。我拆过项目源码mian_cnn.py里没有打乱特征列这就是它能在几个 epoch 内快速收敛的原因之一。与全连接相比卷积还有参数共享优势。一个卷积核在 120 维的特征向量上滑动同一组权重被复用到不同位置参数量只有全连接的几十分之一。在只有几万条样本的训练子集上模型不容易过拟合这也是项目同时给出kddcup.data.gz完整数据和 10% 抽样数据两者都能训练出可用模型的原因。3.2 网络结构卷积层、汇聚层与全连接层这个模型可以理解为 LeNet5 卷积神经网络结构图的一维变体把二维卷积核换成一维。我按cnn_main.py常见写法整理了一份可直接用的结构各层输出形状如下层名称输出形状参数量作用Input(120,1)(None, 120, 1)0输入特征向量Conv1D(64, 3)(None, 118, 64)256提取相邻 3 个特征的局部组合MaxPooling1D(2)(None, 59, 64)0汇聚层降维并保留最强信号Conv1D(128, 3)(None, 57, 128)24832更抽象的高层模式GlobalMaxPooling1D(None, 128)0全局最大汇聚替代 FlattenDropout(0.5)(None, 128)0抑制过拟合Dense(64)(None, 64)8320分类决策Dense(1, sigmoid)(None, 1)65输出异常概率这里重点说一下卷积神经网络的汇聚层也就是常说的池化层。MaxPooling1D 的作用是降低特征维度同时让卷积对小的位置偏移不敏感。比如某个攻击模式的尖峰出现在第 40 位还是第 42 位经过宽度为 2 的最大池化后最大响应可能落在同一个区间模型就更容易学到“这里有个异常特征组合”而不是死记具体位置。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, GlobalMaxPooling1D, Dense, Dropout, BatchNormalization def build_cnn(input_dim): model Sequential() model.add(Conv1D(filters64, kernel_size3, activationrelu, input_shape(input_dim, 1))) model.add(BatchNormalization()) model.add(MaxPooling1D(pool_size2)) model.add(Conv1D(filters128, kernel_size3, activationrelu)) model.add(BatchNormalization()) model.add(GlobalMaxPooling1D()) model.add(Dropout(0.5)) model.add(Dense(64, activationrelu)) model.add(Dropout(0.3)) model.add(Dense(1, activationsigmoid)) return model model build_cnn(X_train.shape[1]) model.summary()BatchNormalization 不影响卷积核提取特征但能显著加速收敛。在入侵检测这种类别比例悬殊的任务上它可以让模型在训练初期不因为输入分布抖动而卡在局部最优。注意第一层的input_shape不能写成(input_dim,)Conv1D 要求第三个维度是通道数这也是从 2D 转 1D 时最常见的报错来源。3.3 损失函数与评估指标的匹配二分类用 binary_crossentropy 没有疑问真正要小心的是准确率这个指标。KDD99 里正常样本占比约 20%异常样本约 80%如果模型把全部样本都预测成异常准确率也有 80% 以上。因此模型编译时需要额外关注 AUCmodel.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy, AUC] )AUC 不受分类阈值影响能直接反映模型区分正负样本的能力。而准确率只告诉你总体判断对的比例。要复现项目宣称的 99.5%应该同时看分类报告里的召回率和 F1而不是只看 accuracy 一行。如果发现 accuracy 高但 recall 不足说明正常样本被放过太多这个模型在真实网络环境里会漏报。4. 训练与调优复现99.5%准确率的关键步骤4.1 训练主流程与TensorBoard监控项目压缩包里有一个events.out.tfevents.1482980284.zjx-24000635文件这是 TensorBoard 的事件日志说明原作者用 tf.keras 训练时记录了运行序列。复现时不要直接拿这份日志当自己的可视化结果机器环境不同数值会有差异但可以用它验证 TensorBoard 配置是否正确。训练代码的核心部分如下from tensorflow.keras.callbacks import TensorBoard, EarlyStopping, ReduceLROnPlateau from sklearn.utils.class_weight import compute_class_weight import time log_dir logs/ time.strftime(%m-%d_%H-%M) callbacks [ TensorBoard(log_dirlog_dir, histogram_freq1), EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience2, min_lr1e-6) ] class_weights compute_class_weight( class_weightbalanced, classesnp.array([0, 1]), yy_train) class_weight_dict {i: cw for i, cw in enumerate(class_weights)} model.fit(X_train, y_train, epochs30, batch_size128, validation_split0.2, callbackscallbacks, class_weightclass_weight_dict)histogram_freq1会记录每一层的权重分布方便观察梯度是否消失。EarlyStopping 监听val_loss而不是val_accuracy因为 loss 更容易反映模型置信度的变化准确率在小波动时可能长时间不变。compute_class_weight用来计算正常类和异常类的补偿权重异常类样本多权重就小正常类样本少权重反而大这样模型就不会一直偏向预测成多数类。4.2 训练中的坑过拟合、学习率与类别不平衡我复现这个项目时踩过几个比较典型的坑整理成一张表现象原因解决手段训练 loss 降到 0.01验证 loss 回升模型记住了攻击样本的噪声增大 Dropout、EarlyStopping、加 L2 正则验证 loss 抖动很大学习率太高ReduceLROnPlateau 联动调低准确率 99.8% 但召回率 80%类别不平衡导致模型偏向多数类加权损失、上采样少数类样本验证时精度高部署时崩特征列顺序不一致保存 feature_columns.txt 并按顺序构造输入还有两个细节值得单独说明。第一如果忘记标准化src_bytes这种最大值可以到几十万的数值特征会主导梯度更新卷积核几乎学不到其他特征的信息。第二kernel_size 不是越大越好120 维的输入用 kernel_size7 会把序列末尾的特征提前卷没了我建议用 3 或 5配合 MaxPooling1D 逐步扩大感受野效果更稳。4.3 模型保存与结果复现训练完成后模型和相关配置需要一起保存model.save(ids_cnn.h5) import joblib joblib.dump(scaler, scaler.pkl) with open(feature_columns.txt, w) as f: f.write(\n.join(X.columns))对应特征列名非常重要。部署时需要按同样的顺序构造特征向量少一列、换一列都会让预测结果完全错乱。评估阶段用 scikit-learn 直接出报告from sklearn.metrics import classification_report y_pred (model.predict(X_test) 0.5).astype(int) print(classification_report(y_test, y_pred, target_names[normal, attack]))在二分类任务上合理的结果应该是 normal 和 attack 的 precision、recall 都在 0.98 以上整体 accuracy 才能到 99.5%。注意99.5% 是二分类下的数字如果按五分类normal DoS Probe R2L U2R去训练准确率会掉到 97% 左右。这不是模型退步而是任务难度增加。复现时先确认标签映射是二分类还是多分类别拿多分类的结果对比二分类的指标。5. 部署视角把CNN入侵检测模型接进实时监控的三种做法5.1 离线批处理方式最稳妥的落地方式是离线分析把抓到的 pcap 文件拆成连接记录用 TShark 或 Scapy 按 KDD99 的特征定义提取出 41 维特征然后送入保存好的模型批量打分。这种方式适合安全团队做攻击溯源不要求秒级反馈特征提取出错还可以回放重算。5.2 实时检测滑动窗口 模型预测实时监控要解决的是“流还没结束如何判断这台机器是否在攻击”。常见做法是把 TCP 连接的前 N 个报文聚合成半连接特征用滑动窗口周期性更新特征向量。预测时只需要加载模型和配置from tensorflow.keras.models import load_model import joblib import pandas as pd model load_model(ids_cnn.h5) scaler joblib.load(scaler.pkl) columns open(feature_columns.txt).read().splitlines() def predict_one(raw_feature: dict) - float: df pd.DataFrame([raw_feature])[columns] x scaler.transform(df.fillna(0)) x x.reshape(1, -1, 1) prob model.predict(x, verbose0)[0][0] return prob # 调用示例 prob predict_one(sample_feature) if prob 0.5: print(异常连接触发告警)raw_feature是外部实时组装的特征字典必须包含columns列表里的所有键顺序也要一模一样。fillna(0)给缺失特征一个中性值但在严格监控场景下我建议缺失就直接丢弃这条样本因为入侵检测里“缺特征”本身可能就是一种躲避行为。5.3 一个容易被忽视的细节特征顺序固化项目自带源码里main.py和cnn_main.py的预测入口可能不一样但无论哪个入口最终都要保证训练时的特征顺序被固化。我在生产环境见过多次高准确率模型上线后崩掉的情况根因就是部署脚本里用字典推导式构造特征Python 的字典虽然保序但同事改了一个字段名顺序就全乱了。最好把训练时保存的顺序文件定义为常量FEATURE_ORDER tuple(open(feature_columns.txt).read().splitlines())然后把预测函数的columns直接赋值为这个元组不允许运行时再动态排序。这个细节比调模型参数更值得优先处理因为特征顺序错了模型再强也输出不了正确结果。本文还有配套的精品资源点击获取
返回列表