ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

并行卷积神经网络实现网络攻击检测:KDD Cup99复现与避坑指南

并行卷积神经网络实现网络攻击检测:KDD Cup99复现与避坑指南 简介面向网络安全与深度学习交叉领域的研究文献系统阐述基于并行卷积神经网络的网络攻击检测方法。文中针对传统检测方法特征提取简单、误报率偏高的痛点设计了双CNN并行特征提取结构并结合全连接层与Softmax分类器实现攻击类型识别同时给出基于KDD Cup99数据集的训练与测试流程适合高校师生、安全研究人员及算法工程师作为课题参考或算法设计模板。资源共包含1个PDF文件大小3.12MB内容完整、图表清晰便于离线阅读与标注。已有570人学习查看对想快速了解深度学习在网络攻防中落地方案的读者是一份高性价比的参考文献。1. 并行卷积神经网络做攻击检测这篇论文的思路为什么值得拆网络攻击检测这个方向传统机器学习方法贝叶斯、SVM、逻辑回归在KDD Cup99这种数据集上的困境很一致特征靠人工设计41维属性里真正有判别力的维度不好找误报率压不下去。这篇2018年的论文给出一个反直觉的思路——不做特征筛选直接用两个不同输入尺寸的卷积神经网络并行提取特征CNN1吃160×96的大图CNN2吃64×64的小图最后拼到一个全连接层做五分类。在KDD Cup99上的结果是Normal类检测准确率98.8%、误报率0.6%。这篇博文把这篇论文里的并行CNN结构、数据转换步骤、训练要点和复现时容易踩的坑全部拆开讲清楚适合正在做网络入侵检测课题、想用深度学习方法替换传统特征工程的从业者和学生。2. 为什么是并行CNN两个输入尺寸背后的特征提取逻辑2.1 CNN1与CNN2的分工大图看全局关联小图看局部模式论文的核心动机是单条CNN在KDD Cup99这种混合了时序特征、内容特征、主机统计特征的数据上往往顾此失彼。KDD Cup99的五类标签——Normal、Dos、Probe、R2L、U2R——各自依赖的判别特征分布层次完全不同单一尺度的卷积核很难同时覆盖这些模式的尺度差异。Dos攻击的特征集中在连接频率和流量统计上属于全局性的大尺度模式Probe扫描的特征集中在端口访问模式上属于中等尺度的序列模式R2L和U2R则往往藏在内容载荷的局部异常里属于小尺度的局部模式。如果用一条CNN处理所有尺度卷积核设大了抓不到局部细节设小了又看不全流量统计特征。论文的并行设计思路就是让CNN1和CNN2各管一个尺度CNN1输入160×96的宽幅图用4层卷积去捕捉长程关联和统计特征CNN2输入64×64的小图用2层卷积去捕捉局部突变模式最后把两个分支的特征拼起来。这个设计在工程上有一个很直观的优势并行分支互不干扰梯度在各自的网络内回传只有当两个分支的特征在全连接层汇合时才发生交互。相比单条大网络的隐式特征融合这种显式的双分支结构至少让特征来源变得可解释——你至少知道哪类特征的贡献来自哪个网络。论文的第4.2节实验结果也印证了这一点Normal和Dos这两个数量占优的类别准确率都在96%以上它们恰恰是对应CNN1擅长的大尺度统计特征而R2L和U2R这两个小样本类别相比对比方法也有提升这就是CNN2的局部特征在起作用。2.2 卷积核、池化步长与感受野论文参数表的读法CNN1的完整参数链是论文里信息量最大的部分也是复现时最需要核对的地方。论文正文的原始描述可以梳理成下面这张表网络层序模板配置步长输出尺寸CNN1输入层——160×96CNN1C1卷积层20个14×6卷积核174×46CNN1P2池化层20个2×2模板237×23CNN1C3卷积层100个8×4卷积核130×20CNN1P4池化层100个2×2模板130×20CNN1C5卷积层200个2×2卷积核112×8CNN1P6池化层200个2×2模板26×4CNN1C7卷积层300个6×4卷积核11×300CNN1F8全连接层1×50—1×50CNN2输入层——64×64CNN2C1卷积层50个12×12卷积核414×14CNN2P2池化层50个2×2模板27×7CNN2C3卷积层100个7×7卷积核11×100CNN2F4全连接层1×50—1×50看这张表的时候有几个细节值得注意。第一CNN1的P4池化层步长是1不是常见的2这意味着这一层没有做降采样只做了局部区域内的特征压缩。第二C7卷积层用了300个6×4的卷积核输出是1×300这一步实际上是把一个小的特征图整体压成了特征向量相当于在网络的末尾做了一次全局映射。CNN2的表我做了修正论文正文里写C1输出还是64×64但从数学上算输入64×6412×12卷积核步长4输出应该是(64−12)/4114×14。后面的池化层输出7×7也反推了上一层是14×14。所以这里的64×64是论文笔误原意应该是14×14。复现时不要按正文的64×64搭网络直接按14×14设计。论文里给出的卷积运算公式其实就是在说一件事每个输出特征图的值等于上一层特征图与卷积核做卷积运算后加上偏置再过激活函数。深度网络本质上就是把这个操作堆叠多次层数越深越后面的卷积核看到的特征图越小但感受野反而越大。CNN1从160×96一路压到6×4最后那个300×6×4的卷积层实际感受野覆盖了整张输入图这就是论文说的保留对于目标决策重要的特征去掉不重要和冗余的特征。直觉上大输入配深网络是在做多层次的全局抽象小输入配浅网络是在抓局部强信号。两个分支合并后全连接层拿到的是两种互补的中间表达。池化层的选型上论文提到最大化池化和平均池化两种方法。对于网络攻击检测这个场景我的经验是最大化池化优先——攻击流量里的异常模式往往表现为某个特征维度上的尖峰比如短时间内大量SYN包、某个端口被频繁探测max池化能把这些尖峰保留下来平均池化反而会把它们抹平。复现时可以两个都试但默认从max开始和论文的并行CNN结构最搭。3. 数据落地把KDD Cup99的41维特征变成卷积网络的图像输入3.1 KDD Cup99的五类标签与41维特征结构KDD Cup99是林肯实验室采集的网络连接记录集每条连接记录包含41个固定特征属性和1个类标记标签。论文里明确说明41个属性中只有9个离散型属性其余都是连续型。这41个属性的分组方式非常重要直接影响后面特征转图像时的排列顺序。前9个属性是网络连接基本特征包括协议类型、服务类型、连接时长、源字节数、目标字节数等第10到22个是网络连接内容特征包括登录失败次数、su命令尝试次数、文件创建操作次数等这一类特征的设计初衷是捕获R2L和U2R这类隐藏在内容载荷里的攻击行为第23到31个是基于时间的流量统计特征比如过去两秒内与当前连接相同主机的连接数、相同服务的连接数、SYN错误占比等最后11个是主机网络流量统计特征按目标主机维度做统计。五类标签分别是Normal、Dos、Probe、R2L、U2R这就是后面Softmax输出层神经元个数的依据。3.2 属性映射与数据归一化的具体做法论文原文提到属性映射、数据归一化和数据转换三个预处理动作但没有展开细节。按这个场景下最常用的做法我拆成三步说明。属性映射针对9个离散特征比如协议类型有tcp、udp、icmp三种取值。常见做法是做整数编码或one-hot编码。考虑到后面要转成图像我建议直接用整数映射加归一化而不是做one-hot——41维特征如果其中9个都展开成one-hot维度直接膨胀到50以上特征转图像时的形状会变复杂。整数映射在这个数据集上足够用卷积神经网络有能力学出离散取值的数值关系。归一化是关键的一步。41维特征里既有几十毫秒级的时长值也有0/1的离散标记还有几百字节的流量计数值量纲差异极大。不归一化的话卷积核对不同尺度的输入会非常敏感训练初期loss会震得厉害。常见的做法是min-max归一化到[0,1]或者z-score标准化。对于网络攻击检测这个场景我一般用min-max因为后面的灰度图就是0到1的像素值可以直接复用归一化结果。import numpy as np def preprocess_kdd_record(record, discrete_cols, continuous_cols): KDD Cup99单条记录的预处理属性映射 min-max归一化 record np.asarray(record, dtypenp.float32) processed record.copy() # 离散属性整数映射后压缩到[0,1]避免数值量级过大 for col in discrete_cols: processed[col] processed[col] / (processed[col].max() 1e-8) # 连续属性min-max归一化到[0,1] for col in continuous_cols: vmin record[col].min() vmax record[col].max() if vmax - vmin 1e-8: processed[col] (record[col] - vmin) / (vmax - vmin) else: processed[col] 0.0 return processed.astype(np.float32)这段代码里的discrete_cols传离散特征的列索引列表continuous_cols传连续特征的列索引列表。处理顺序有讲究先做离散映射再做连续归一化防止离散特征的取值被连续特征的min-max挤压到看不出差别。注意那个1e-8的防零除数防御的是某条记录某个特征全为常量的情况。3.3 特征转图像尺寸对齐和插值方式的坑论文没有公开特征转图像的具体代码只说通过数据转换变成目标数据集。复现时必须自己补这一步。41维特征转图像的常见做法是reshape成6×7或7×6的单通道灰度图不足41的位置补零。比如7×6就有42个像素刚好能放下41维特征加一个空位。这里就遇到一个矛盾6×7的特征图和论文里CNN1的160×96输入、CNN2的64×64输入完全对不上。所以实操上需要做一次图像插值放大。这步很关键我一般用最近邻插值而不是双线性插值下面这段代码展示完整的流程import cv2 def kdd_to_cnn_input(feature_vector, target_h, target_w, height7, width6): 41维特征 - 7x6灰度图 - 最近邻放大到CNN输入尺寸 # 41维特征补零到42维reshape成7x6的单通道图 padded np.zeros(height * width) padded[:len(feature_vector)] feature_vector img padded.reshape(height, width).astype(np.float32) # 最近邻插值放大避免伪造过渡像素 resized cv2.resize(img, (target_w, target_h), interpolationcv2.INTER_NEAREST) return resized这里用INTER_NEAREST而不是INTER_LINEAR原因在于双线性插值会在0值填充区域和有效特征区域之间生成平滑过渡的灰度这些过渡值在原始数据里不存在是插值算法人为伪造的卷积核会把这些虚假模式当成特征学进去导致模型在训练集上表现好、在真实流量上翻车。最近邻插值虽然会形成明显的像素块边界但至少不会伪造数据。当然这样放大后的图像在结构上是马赛克式的所以卷积核大小和步长的设计也要跟着适配——这也是为什么论文里CNN1的第一个卷积核是14×6它的尺寸接近放大后特征块的尺寸能完整覆盖一个原始特征对应的区块。3.4 训练集和测试集的划分逻辑论文的实验数据量值得注意。训练集里Normal有6200条、Dos有18000条、Probe有2400条、R2L有1450条、U2R只有56条测试集对应是1800、15800、1700、1050、37。这个划分比例不是随机抽样是从KDD Cup99原始500万条记录里按类别挑出来的子集意图是控制训练代价同时保证五类都有样本。我复现时的建议是严格按这个比例抽样。U2R在测试集里只有37条这意味着U2R的准确率和误报率波动会很大——37条样本里错3条准确率就掉了8个百分点。所以拿到论文里U2R 95.2%的准确率时心里要清楚这个数字的置信区间很宽。如果要复现实验做对比保持和论文相同的抽样比例才能保证结果口径一致如果要追求模型质量那U2R的样本量就要靠SMOTE过采样或者数据增强来补。4. 模型搭建与训练TensorFlow下的并行CNN实现拆解4.1 并行网络结构的代码骨架论文明确说实现语言是Python、框架是TensorFlow。下面这段代码是按论文结构还原的并行CNN模型定义两个分支分别建图最后用tf.concat合并特征import tensorflow as tf def cnn1_branch(input_x): CNN1分支4个卷积层 3个池化层输入160x96 conv1 tf.layers.conv2d( input_x, filters20, kernel_size(14, 6), strides1, paddingVALID, activationtf.nn.relu) pool1 tf.layers.max_pooling2d(conv1, pool_size(2, 2), strides2) conv2 tf.layers.conv2d( pool1, filters100, kernel_size(8, 4), strides1, paddingVALID, activationtf.nn.relu) pool2 tf.layers.max_pooling2d(conv2, pool_size(2, 2), strides1) conv3 tf.layers.conv2d( pool2, filters200, kernel_size(2, 2), strides1, paddingVALID, activationtf.nn.relu) pool3 tf.layers.max_pooling2d(conv3, pool_size(2, 2), strides2) conv4 tf.layers.conv2d( pool3, filters300, kernel_size(6, 4), strides1, paddingVALID, activationtf.nn.relu) flat tf.layers.flatten(conv4) fc tf.layers.dense(flat, units50, activationtf.nn.relu) return fc def cnn2_branch(input_x): CNN2分支2个卷积层 1个池化层输入64x64 conv1 tf.layers.conv2d( input_x, filters50, kernel_size(12, 12), strides4, paddingVALID, activationtf.nn.relu) pool1 tf.layers.max_pooling2d(conv1, pool_size(2, 2), strides2) conv2 tf.layers.conv2d( pool1, filters100, kernel_size(7, 7), strides1, paddingVALID, activationtf.nn.relu) flat tf.layers.flatten(conv2) fc tf.layers.dense(flat, units50, activationtf.nn.relu) return fc def parallel_cnn(x1, x2, num_classes5): 并行CNN双分支特征拼接 - 全连接20 - Softmax feat1 cnn1_branch(x1) feat2 cnn2_branch(x2) concat_feat tf.concat([feat1, feat2], axis1) # 拼接成100维 fc_merge tf.layers.dense(concat_feat, units20, activationtf.nn.relu) logits tf.layers.dense(fc_merge, unitsnum_classes) return logits代码里的几个细节说一下。CNN1分支中pool2的strides设了1对应论文中P4池化层不降采样的设计等传到conv4时特征图空间尺寸是12×12左右6×4的卷积核作用后剩余的空间维度小于1tf.layers.flatten会把剩余维度和通道数一起拉平。但这里有个实际问题如果中间尺寸计算稍有偏差dense层输入维度就会对不上报类似Shape must be rank 2的错误。你要把每层conv的输出shape打出来逐个核验。另外我用了tf.layers.dense而不是手动建权重矩阵目的是让代码更贴近论文整体结构少写点样板代码。两个分支各自的输出都固定为50维这是后续concat的前提条件——如果其中一个分支的输出维度不是你预期的50后面的拼接就会出维度错误。4.2 训练流程误差阈值、反向传播与测试集验证论文训练过程的逻辑链条很清晰原始数据集经过属性映射、数据归一化和数据转换变成目标数据集划分训练集和测试集反向传播调整网络参数当训练误差小于一定阈值时用测试集得到检测结果。翻译成实际操作我一般会写成下面这种循环。注意要设置两个循环出口一个是loss降到阈值以下提前停止另一个是epoch数达到上限强制停止。X1_train, X2_train prepare_branch_inputs(train_images) # 分别转成160x96和64x64 loss tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits( labelsy_train, logitslogits)) optimizer tf.train.AdamOptimizer(learning_rate0.001).minimize(loss) # 训练循环 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for epoch in range(200): _, batch_loss sess.run( [optimizer, loss], feed_dict{x1: X1_train, x2: X2_train, y: y_train}) if batch_loss 0.05: print(loss threshold reached at epoch, epoch) break if epoch % 10 0: print(epoch%d, loss%.4f % (epoch, batch_loss))训练参数的说明。优化器选了Adam而不是SGD因为并行网络两个分支的梯度量纲差异大Adam的自适应学习率能缓解这个问题。learning_rate0.001是常见起步值如果loss震荡可以降到0.0005。0.05的误差阈值是论文里没有写明、但符合它描述的小于一定阈值的惯例值。这里有一个经验上的提醒训练误差小于阈值只能说明拟合到位不能说明泛化到位。我在实际训练时不会只靠loss阈值判断收敛还会同时算验证集上的准确率——验证集的loss回升而训练loss继续下降那就是过拟合信号该早停而不是继续跑。4.3 输出层设计Softmax分类与五类攻击映射论文的输出层是Softmax分类数目由攻击类别确定。KDD Cup99用五类的话Softmax输出就是5个概率值取最大概率的类别作为预测结果。这里有个容易搞错的细节论文实验部分对U2R的准确率和误报率单独列出了数据说明它是按五分类做的不是按正常/异常做二分类。复现时标签编码直接用整数0到4。# 五类标签映射0Normal, 1Dos, 2Probe, 3R2L, 4U2R label_map {normal: 0, dos: 1, probe: 2, r2l: 3, u2r: 4} def encode_label(label_str): 把KDD Cup99原始标签字符串映射为整数类别 label_str label_str.strip().lower() # KDD Cup99里Dos有多种子类型归一化到主类 if label_str normal: return 0 if label_str.startswith(smurf) or label_str.startswith(neptune) or \ label_str.startswith(back) or label_str.startswith(teardrop) or \ label_str.startswith(pod) or label_str.startswith(land): return 1 if label_str.startswith(satan) or label_str.startswith(ipsweep) or \ label_str.startswith(nmap) or label_str.startswith(portsweep): return 2 if label_str.startswith(guess_passwd) or label_str.startswith(ftp_write) or \ label_str.startswith(imap) or label_str.startswith(phf) or \ label_str.startswith(multihop) or label_str.startswith(warezmaster): return 3 if label_str.startswith(buffer_overflow) or label_str.startswith(loadmodule) or \ label_str.startswith(perl) or label_str.startswith(rootkit): return 4 return -1这段代码解决的是复现KDD Cup99时很隐蔽的坑数据集里的入侵标签不是五类而是几十种子类型比如Dos攻击里有smurf、neptune、back、teardrop等子类型R2L里有guess_passwd、ftp_write等。如果直接把原始字符串标签当多分类目标类别数会膨胀到几十个和论文的Softmax输出层完全对不上。通过这个映射把所有子类型归并到论文的五大类模型的输出层才能对应上。漏掉这一步的话训练时很可能在dense输出层的维度上报错。5. 复现避坑KDD Cup99与并行CNN的常见问题排查5.1 现象模型在Normal和Dos两类上准确率很高R2L和U2R几乎全判错原因训练集里U2R只有56条、R2L只有1450条而Normal有6200条、Dos有18000条类别极不平衡。Softmax交叉熵损失对小样本类别的梯度贡献微乎其微网络会把所有不确定样本倾向判成大类。解决两层处理。第一层是数据层面用SMOTE对R2L和U2R做过采样或者对每条U2R样本做特征噪声增强让小样本在训练中见过更多变体。第二层是训练层面在损失函数里给每类加权重权重按类别样本数的倒数归一化。我一般把U2R的权重设到Normal的200倍以上不然56条样本根本不起作用。5.2 现象特征图resize到64×64和160×96后训练速度慢到没法看原因论文原始数据集500万条记录如果你从完整KDD Cup99读数据不做抽样直接喂给并行CNN两个分支输入加起来是160×96加64×64像素batch size稍微大一点显存和训练时间直接爆炸。解决严格按照论文表1和表2的样本量做子集抽样Dos保持18000条、Normal 6200条。如果想让统计结果更稳按比例放大各数据量但不要单独扩U2R——论文U2R只有56条你强行补到几千条验证集口径和论文对不上结果数字就失去了可比性。5.3 现象两个分支的loss下降速度明显不一致合并后总loss震荡原因CNN1有4个卷积层、CNN2只有2个CNN1的梯度路径更长两个分支的收敛速度天然不同步。合并全连接层同时收到一个已收敛分支和一个未收敛分支的特征时梯度方向互相拉扯训练过程会变得很不稳定。解决常见做法是给两个分支设不同的学习率或者让CNN2先用少量epoch预训练。我实际操作时倾向于加一个branch_gate前10个epoch冻结CNN1只训CNN2让浅层分支先把局部特征学好再放开CNN1做联合微调。这样总loss下降会比直接联合训练稳定很多。5.4 现象复现出来的误报率比论文高很多尤其Probe类原因论文里Probe类误报率3.8%很多人复现出来会在6%以上。误报率偏高的根源通常是特征转图像的方式不同——论文里41维特征映射到图像的具体排列顺序没有公开。如果用不同的排列顺序卷积核学到的空间关系完全是另一套东西特征表达就和论文设计意图偏离了。解决不要试图还原论文的原始排列那是黑匣子。把特征转图像这一步骤做成可配置的做几组排列对比实验在验证集上选误报率最低的一组固定下来。我的习惯是先按特征类型分块排列基本特征一块、内容特征一块、统计特征一块再做一次随机排列作对照——如果两组结果差异在1%以内说明图像空间排列对这个数据集的判别力影响不大你的实现就是稳定的。5.5 现象论文里多个尺寸参数对不上按原参数搭建直接报维度错误原因前面2.2节分析过这篇论文几个中间输出尺寸存在笔误比如CNN2第一卷积层输入64×64配12×12卷积核步长4输出尺寸不可能是64×64。这些数字在复现时如果逐一对照模型根本建不起来。解决以第2章的参数表为准用paddingVALID按次序推导每层输出尺寸在每层卷积后打印实际shape验证。遇到dense层输入维度报错时先确认前一层的shape再改units。把论文里的尺寸笔误当成印刷问题处理就好不要当成自己的代码问题反复排查。6. 从论文到工程验证效果的两个可执行技巧6.1 用混淆矩阵看真实误报分布论文里只给了每类的准确率和误报率但这两组数字是割裂的——准确率高低只能说明该类判对多少误报率说明正常样本被误判的比例两者结合你看不到哪些类别之间互相混淆。复现之后我建议额外加一步在测试集上跑一遍预测输出一个5×5的混淆矩阵。from sklearn.metrics import confusion_matrix import numpy as np # y_true是真实标签y_pred是模型预测标签 cm confusion_matrix(y_true, y_pred, labels[0, 1, 2, 3, 4]) np.set_printoptions(precision2, suppressTrue) print(混淆矩阵行真实列预测) print(cm)这一步的价值在于定位隐蔽问题。如果Normal被误判的样本大量流向Dos说明网络把高频连接类特征和正常流量分不开如果R2L的样本被大量判为Probe说明两个分支合并后的特征表达过度依赖统计特征而忽略了CNN2提取的内容特征。看到混淆矩阵之后我通常会回头调整两个分支的卷积核配比比如增加CNN2在内容特征上的卷积核数量这比盲目调学习率有效得多。6.2 从KDD Cup99迁移到真实流量的增量思路KDD Cup99是1999年前后的数据集和现在企业网络的真实流量形态差异很大。论文做的是离线训练和测试如果要在自己的网络环境里用这套并行CNN思路我更推荐把它当成一个特征提取器而非最终模型先在KDD Cup99上把并行CNN训练好固定前几层卷积核的权重然后把自己采集的流量特征作为新数据只微调最后的全连接层和Softmax层。这种迁移方式保留了卷积层从大规模攻击流量里学到的通用模式同时让输出层适配你网络环境里的真实类别分布。做迁移时有一个很现实的难点真实网络流量的标签比KDD Cup99难获取得多没有现成的五类标签体系。常见做法是先和现有IDS规则的结果对齐作为弱标签再人工抽检一批流量修正标签错误。等积累了足够多的干净样本后再引入在线更新机制——持续用新流量微调模型否则随着流量分布漂移模型会逐渐钝化。从那以后我每次复现类似论文都会先检查数据集的口径和模型参数的对齐度再决定是搭新模型还是走迁移微调这套流程帮我省掉了很多盲目改网络结构的无效时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表