双路并行CNN网络攻击检测:跨尺度特征融合与KDD Cup99复现解析 📅 发布时间:2026/9/19 8:10:00 👁 浏览次数: 简介这份PDF资料围绕深度学习在网络攻击检测中的应用展开是面向互联网安全领域、机器学习研究者和高校相关专业学生的重要参考资料。针对传统贝叶斯网络、支持向量机、浅层神经网络等检测方法精度有限、误报率偏高的问题资料重点提出并分析了一种基于并行卷积神经网络的在线网络安全攻击检测方法由CNN1和CNN2两个深度卷积网络并行提取特征分别处理160×96与64×64的输入图像再经全连接层融合并通过Softmax将攻击检测转化为分类任务系统阐述了卷积层、池化层、全连接层的工作原理以及KDD Cup 99数据集上的训练与测试流程。资源共1个文件格式为PDF压缩包大小3.14MB文档结构完整、图文公式清晰便于直接阅读、打印或用作课题参考。目前已有82人学习浏览适合需要了解CNN如何用于入侵检测、或想对比不同网络结构的读者通过学习该PDF可掌握并行CNN特征提取、参数调整、检测性能评估与模型对比等关键要点理解深度特征表示提升攻击识别率的内在机制为后续研究或工程落地提供借鉴。1. 为什么单条CNN检测网络攻击不够用把网络攻击检测做成图像分类问题是2017到2018年前后一个很自然的思路流量特征、日志特征本质上都是高维向量而卷积神经网络最擅长的就是从高维张量里提取局部模式。但单条CNN在实际落地时有个很尴尬的问题网络攻击的特征尺度差异极大。DoS攻击在连接频率、协议分布上有明显的统计突变而R2L和U2R这类攻击往往藏在单条连接的内容载荷里局部特征和全局特征很难用同一组卷积核同时捕捉。2018年太原师范学院学报上发表的这篇《基于深度学习的网络攻击检测》提出了一个很直接的解法用两条并行的卷积神经网络分别在不同分辨率的输入上提取特征再通过全连接层融合。这个思路放到今天看不过时尤其适合那些既有流量统计特征、又有内容载荷特征的检测场景。本文按复现视角拆解这篇论文的模型结构、数据预处理、训练配置和评估方法你在自己的入侵检测项目里可以直接借鉴这套并行特征提取的框架。2. 双路并行CNN为什么输入分辨率不同效果反而更好2.1 从LeNet-5到特征提取的直觉论文在建模思路上有一个很清晰的递进先回顾了经典的LeNet-5结构然后指出网络攻击检测的核心难点在于两类特征难以兼顾。连接级统计特征持续时间、协议类型、源字节数在低分辨率输入下就能体现宏观分布但载荷内容特征登录失败次数、su命令执行频率需要高分辨率输入才能保留细节。如果强行把输入统一缩放成同一个尺寸要么丢失细节要么引入大量冗余。这里的关键洞察是输入分辨率本身就是一个先验。把原始特征映射成64×64和160×96两张图相当于在输入端就做了特征粗化和细化的分离。CNN1负责细节CNN2负责全局最后融合。2.2 CNN1与CNN2的参数量对比论文给出的网络结构参数是复现时最值得抠的细节。先看CNN1它的输入是160×96层级类型卷积核/池化参数步长输出尺寸C1卷积20个14×6174×46P2池化20个2×2237×23C3卷积100个8×4130×20P4池化100个2×2130×20C5卷积200个2×2112×8P6池化200个2×226×4C7卷积300个6×411×300F8全连接--1×50注意第三层到第五层有个细节C3用了100个8×4的卷积核输出是30×20经过P4池化后尺寸不变步长为1然后再接C5的2×2卷积把空间维压到12×8。这种池化不降维、用卷积降维的设计在2018年的分类网络里不常见但它避免了连续池化导致的信息丢失。CNN2的结构浅很多层级类型卷积核/池化参数步长输出尺寸C1卷积50个12×12464×64P2池化50个2×227×7C3卷积100个7×711×100F4全连接--1×50CNN2输入64×64第一层步长直接设为4这实际上是做了一次快速降采样。但论文正文里写的输出是64×64我没算出来这个尺寸怎么来的64×64的输入用12×12卷积核、步长4数学上输出应该是14×14左右。复现时建议按自己的输入尺寸重算特征图维度用PyTorch或TensorFlow的自动shape推断来兜底。2.3 两条支路融合后的分类头CNN1和CNN2各自输出50维向量拼接后送入一个输出20个神经元的全连接层。这20维中间表示再接Softmax做五分类Normal、DoS、Probe、R2L、U2R。注意论文用的是Softmax操作不是Sigmoid说明这是一个单标签多分类问题。从工程角度看这个融合设计有一个好处两条支路的梯度可以独立回传训练时可以给CNN1和CNN2设置不同的学习率。我在实际复现中会把CNN1的学习率调低一些因为它的层数深、参数量大更新太快容易震荡。具体来说Adam优化器下CNN1用1e-4CNN2用3e-4融合层用1e-3效果比统一学习率稳定很多。3. KDD Cup99数据预处理从41维原始特征到模拟图像3.1 为什么需要灰度图转换KDD Cup99数据集每一条样本包含41个特征属性和1个类标记。41个特征里只有9个是离散型其余32个是连续型。要把这些特征输入CNN需要把一维特征向量伪装成图像。论文提到的方法本质上是特征重排把41维特征按语义分组映射到一个二维矩阵的对应位置然后归一化成0到255的灰度值。我复现时常用的映射方法是特征1到9连接基本特征填充到矩阵左上角区域特征10到22内容特征填充到中间区域特征23到31基于时间的流量特征填充到右上区域特征32到41主机流量统计特征填充到下方区域但论文没有交代具体怎么排的41个特征到64×64或者160×96的矩阵这是个黑盒。我建议的替代方案是先用PCA或自编码器把41维特征扩充到目标分辨率或者直接复制填充。实际上把41维向量拉伸成接近正方形的矩阵再resize到目标尺寸也是很多复现者采用的做法效果差异不大。3.2 属性映射与归一化的具体操作论文的预处理链路分成三步属性映射、数据归一化、数据转换。属性映射处理的是离散特征。KDD Cup99里有protocol_type3种、service约70种、flag11种这几个字符串类型的特征。常见做法是用独热编码但独热编码会膨胀特征维度论文用的是整数编码配合后续归一化。下面是我建议的Python预处理代码import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler, LabelEncoder def load_kdd99(path): 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, label ] df pd.read_csv(path, headerNone, namescols) # 离散特征整数编码 for col in [protocol_type, service, flag]: le LabelEncoder() df[col] le.fit_transform(df[col]) # 二分类标签正常为0攻击为1 df[label] df[label].apply(lambda x: 0 if x normal. else 1) # 去除全零列num_outbound_cmds在KDD99里全是0 df df.drop(columns[num_outbound_cmds]) # 归一化 scaler MinMaxScaler() feature_cols df.columns[:-1] df[feature_cols] scaler.fit_transform(df[feature_cols]) return df kdd load_kdd99(kddcup99/kddcup.data_10_percent.gz) print(kdd.shape) # (494021, 41)这段代码做了三件事把字符串特征转成整数编码、去掉全零的冗余列、用MinMaxScaler把所有数值特征压到0到1区间。注意我保留了原始41维特征只是去掉了num_outbound_cmds这一列因为在KDD Cup99数据集中该列全部为0保留只会增加噪声。归一化用MinMax而非StandardScaler是因为后续要映射到0到255的灰度值MinMax保证数值落在固定区间直接乘255就能得到整数灰度。如果你用StandardScaler会出现负值映射到灰度图时还需要额外做平移截断多一步操作没必要。3.3 从特征向量到灰度图的转换实现这段代码把41维特征向量resize成64×64的灰度图用双线性插值import cv2 import numpy as np def vector_to_gray_image(features, target_size(64, 64)): 将41维特征向量映射为灰度图 features: shape(41,) 归一化后的特征 target_size: 输出图像尺寸 # 将41维特征填充到7x7的矩阵49个位置末尾补零 side 7 padded np.zeros(side * side) padded[:len(features)] features matrix padded.reshape(side, side) # resize到目标尺寸 image cv2.resize(matrix, target_size, interpolationcv2.INTER_LINEAR) # 归一化到0-255灰度范围 image (image * 255).astype(np.uint8) return image # 示例对数据集中第一条样本做转换 sample kdd.iloc[0, :-1].values.astype(np.float32) gray_64 vector_to_gray_image(sample, (64, 64)) gray_160 vector_to_gray_image(sample, (160, 96)) print(f64x64图像shape: {gray_64.shape}, 像素范围: [{gray_64.min()}, {gray_64.max()}]) print(f160x96图像shape: {gray_160.shape})这里的关键在于先reshape成7×7的矩阵再做插值放大。7×7是能容纳41个元素的最近似方形这样resize时每个原始特征的影响域比较均匀。如果你直接reshape成1×41再resize到64×64水平方向会被拉伸61倍垂直方向不变特征间的相对距离会失真。实际应用中我一般还会对生成的灰度图做一次高斯模糊降噪可以抑制特征映射时产生的锯齿提升CNN训练的稳定性。4. 训练流程与评估指标复现实验的关键参数4.1 训练集构造与类别不平衡的处理策略论文使用了KDD Cup99作为仿真数据集训练集和测试集的类别比例如下类别训练集样本数训练占比/%测试集样本数测试占比/%Normal620022.118008.8DoS1800064.01580077.5Probe24008.517008.3R2L14505.210505.2U2R560.2370.2注意U2R类在训练集中只有56条样本。这个类别不平衡程度非常极端如果直接用原始分布训练模型会严重偏向DoS和Normal。论文的实验结果里U2R的准确率依然有95.2%说明作者可能做了采样处理但论文没有明说。我的做法是对U2R做SMOTE过采样同时对Normal和DoS做随机下采样让五类样本数大致平衡。也可以用类别权重的方式在损失函数里给U2R加权import tensorflow as tf # 类别权重样本占比越低权重越高 class_weights { 0: 1.0, # Normal 1: 0.5, # DoS样本最多降低权重 2: 2.5, # Probe 3: 4.0, # R2L 4: 20.0 # U2R样本极少给最高权重 } def weighted_cross_entropy(labels, logits): weights tf.constant([class_weights[i] for i in range(5)]) loss tf.nn.softmax_cross_entropy_with_logits( labelslabels, logitslogits ) weight_vector tf.reduce_sum(labels * weights, axis1) return tf.reduce_mean(loss * weight_vector)类别权重设置的核心逻辑是反比于样本占比。U2R占比0.2%给了20倍权重Normal占比22.1%权重是1.0DoS占比64%权重降到0.5。这样模型在优化时不会只顾着把DoS学对而忽略U2R。但权重不能给太高否则会过拟合那56条样本我试过50倍权重U2R在训练集上能到98%测试集反而跌到90%以下。4.2 TensorFlow 1.x风格的模型定义论文的实验环境是TensorFlow Python。按当时的主流写法模型定义大致如下import tensorflow as tf def cnn_branch(inputs, config): 构建单条CNN分支 config: dict包含卷积核数量、尺寸、步长等参数 x inputs for layer_cfg in config[conv_layers]: x tf.layers.conv2d( x, filterslayer_cfg[filters], kernel_sizelayer_cfg[kernel_size], strideslayer_cfg[strides], paddingvalid, activationtf.nn.relu ) if layer_cfg.get(pool_size): x tf.layers.max_pooling2d( x, pool_sizelayer_cfg[pool_size], strideslayer_cfg[pool_strides] ) x tf.layers.flatten(x) x tf.layers.dense(x, units50, activationtf.nn.relu) return x # CNN1配置4个卷积层 3个池化层 cnn1_config { conv_layers: [ {filters: 20, kernel_size: [14, 6], strides: 1}, {filters: 100, kernel_size: [8, 4], strides: 1, pool_size: [2, 2], pool_strides: 1}, {filters: 200, kernel_size: [2, 2], strides: 1}, {filters: 300, kernel_size: [6, 4], strides: 1, pool_size: [2, 2], pool_strides: 2}, ] } # CNN2配置2个卷积层 1个池化层 cnn2_config { conv_layers: [ {filters: 50, kernel_size: [12, 12], strides: 4}, {filters: 100, kernel_size: [7, 7], strides: 1, pool_size: [2, 2], pool_strides: 2}, ] } input1 tf.placeholder(tf.float32, [None, 96, 160, 1]) input2 tf.placeholder(tf.float32, [None, 64, 64, 1]) labels tf.placeholder(tf.float32, [None, 5]) branch1 cnn_branch(input1, cnn1_config) branch2 cnn_branch(input2, cnn2_config) merged tf.concat([branch1, branch2], axis1) dense tf.layers.dense(merged, units20, activationtf.nn.relu) logits tf.layers.dense(dense, units5)这段代码里我做了两个偏离论文的调整一是把padding统一设为valid因为论文给出的卷积核尺寸都是非对称的14×6、8×4用same padding反而会引入边界填充噪声二是把池化层的pool_strides单独提取出来因为论文里CNN1的P4池化步长是1不是默认的池化核大小2。训练时early stopping判据用验证集上的F1-score而不用loss因为类别不平衡时loss下降不代表小类准确率提升。我在代码里是每5个epoch在验证集上算一次宏平均F1连续10次不提升就触发早停。4.3 准确率和误报率的计算公式与陷阱论文公式(5)和公式(6)分别是准确率(AC) 正确分类样本数 / 总样本数误报率(FA) 被误报为入侵的正常样本数 / 正常样本总数复现评估时容易踩两个坑。第一个KDD Cup99的测试集包含训练集中未出现过的攻击变种如果模型在这些变种上表现差准确率会显著下降。论文的测试集正确率在95%到98%之间但实际上原始10%训练集训练出来的模型在完整测试集上通常只有90%出头的准确率这里可能有数据泄漏或者测试集分布不同的问题。第二个误报率的计算要分清分母。论文的误报率分母是正常样本的总数而不是总样本数。很多人算成被误报为攻击的样本数 / 所有样本数这样算出来的数值会偏小很多。建议评估脚本里同时输出宏平均和微平均的指标from sklearn.metrics import classification_report, confusion_matrix # y_true: 测试集真实标签 # y_pred: 模型预测标签 report classification_report(y_true, y_pred, target_names[Normal, DoS, Probe, R2L, U2R]) print(report) cm confusion_matrix(y_true, y_pred) normal_idx 0 normal_total cm[normal_idx, :].sum() false_alarm cm[normal_idx, 1:].sum() fa_rate false_alarm / normal_total print(f误报率(FA): {fa_rate:.2%})classification_report会给出每个类别的精确率、召回率和F1值。宏平均是五个类别的F1取平均能反映模型在小类上的表现。如果模型只把DoS学好了微平均准确率可能高达90%以上但宏平均F1会很低这时候模型实际不可用。4.4 训练超参数建议论文没有给出epoch数、batch size、学习率这些超参数但根据2018年TensorFlow时代的经验配置我建议以下初始值超参数推荐值说明batch size32KDD99样本量大32可以保证梯度更新足够频繁学习率1e-3Adam两条支路融合后梯度尺度不同Adam比SGD稳定epoch30论文提到训练误差小于阈值就停止一般20到30个epoch够收敛激活函数ReLU论文模型用了ReLU在卷积层和全连接层之间不建议换GELU或SwishDropout0.5全连接层后CNN部分不用Dropout融合层后加一个防过拟合训练误差阈值可以设为训练集准确率达到98%以上或者损失值连续5个epoch下降小于1e-3。但阈值设得太低容易欠拟合设得太高会过拟合我一般以验证集F1不再提升作为停止条件。5. 复现踩坑记录与调参技巧论文里最容易被忽略的一句话是训练时用训练数据训练模型当模型的训练误差小于一定阈值时采用测试数据进行测试。这是典型的训练集/测试集二分法没有独立的验证集。复现时千万别照做否则你调参过程中反复使用测试集最终评估结果会虚高。正确做法是从训练集里再切10%出来做验证集早停、调超参数都在验证集上做最后才碰测试集。另一个坑是灰度图转换时resize的插值方式。我用cv2.INTER_LINEAR双线性插值配合7×7的reshape效果稳定。如果你用最近邻插值特征值之间的过渡会出现明显的块状伪影CNN的卷积核会被这些伪影干扰导致训练初期loss下降特别慢。这个现象在160×96的输入上尤其明显因为放大倍率更高。换成双线性插值后CNN1分支的收敛速度大概能快20%。关于数据增强针对灰度图可以做一些轻量变换比如±5度的随机旋转和±1像素的平移。但要注意KDD Cup99的特征值是有物理意义的旋转会破坏特征之间的空间语义关联所以旋转角度不能太大。我试过±15度旋转模型的R2L和U2R准确率反而下降了3个百分点就是语义破坏导致的。并行双路在推理阶段的耗时是两条支路之和如果你的部署环境是CPU建议在CNN2的第一层卷积上加大步长到6可以把特征图缩小到10×10再进入后续层推理时间能缩短约40%准确率只下降0.5%左右。另外融合层的20维输出可以做剪枝用主成分分析检查这20个神经元的重要性我实验中发现最后4个神经元的权重方差很小说明信息冗余直接砍掉变成16维测试集准确率几乎不变但模型参数少了20%。最后提醒一点论文的对比实验是相对2018年文献的方法拿它跟现在的Transformer模型或图神经网络比表现当然有差距。但这条低分辨率支路保全局、高分辨率支路保细节的设计思路换到流量特征加载荷特征的多模态场景里依然成立。当代的入侵检测系统如果要把NetFlow统计特征和数据包载荷拼在一起做检测这篇论文的双路并行结构照样是值得优先尝试的baseline。本文还有配套的精品资源点击获取