ARTICLE DETAIL

资讯详情

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

Python实战:CNN、LSTM与SAE三模型融合的网络流量分类

Python实战:CNN、LSTM与SAE三模型融合的网络流量分类 简介这份资源面向深度学习入门者与网络流量分析方向的学习者提供基于CNN、LSTM和SAE三种模型的流量数据分类完整实现方案可用于课程设计、毕业设计或算法对比实验。压缩包共13个文件约456KB包含5个Python源码文件与4个编译缓存文件另有训练集与测试集的JSON数据文件、配置JSON及说明文档覆盖数据加载、模型定义、训练流程等核心模块目录按功能拆分清晰便于逐层阅读与二次修改。资源已有672人学习下载说明其在同类教程中具备一定参考价值。读者可借此掌握卷积神经网络、长短期记忆网络与稀疏自编码器在流量分类任务上的建模思路对比不同结构的特征提取效果并直接复用数据预处理与训练脚本快速搭建自己的实验基线减少从零编写代码的时间成本。1. 流量分类为什么值得用 CNN、LSTM 和 SAE 三件套一起做拿到一份网络流量数据第一反应往往是先看端口号、协议字段然后套个随机森林或者 XGBoost 跑一版 baseline。这套做法在流量类别少、特征干净的时候确实够用但只要流量一加密、一混淆端口和载荷特征就集体失效准确率掉得比想象中快。这时候大家会转向深度学习而 CNN、LSTM、SAE 这三个模型几乎是绕不开的组合CNN 抓局部字节模式LSTM 抓流内时序依赖SAE 做无监督特征压缩。它们各自解决流量分类里一个具体痛点拼在一起才构成一条完整的特征提取链路。这篇要讲的就是怎么用 Python 把这三条链路跑通从数据读取、特征组织、模型搭建到训练评估每一步都给可复现的代码和参数说明。适合已经会写 Python、装过 PyTorch 或 TensorFlow、但还没系统做过流量分类的工程师也适合做过图像分类想迁移到流量场景的人。读完你应该能自己搭一套能跑出结果的分类流程并且知道哪一步最容易翻车。2. 流量数据怎么组织成 CNN、LSTM、SAE 能吃的形状2.1 先搞清楚流量数据的三种表示形式流量分类的输入形态直接决定后面用哪个模型。常见做法是把一条流表示成三种形式之一字节序列、统计特征向量、包长与到达时间序列。CNN 吃的是前两种里带空间结构的那种LSTM 吃的是第三种这种带顺序的SAE 则对统计特征向量做降维和去噪。字节序列的做法是取每条流的前 N 个字节比如 784 或 1024转成 0 到 255 的整数再归一化到 0 到 1。这个表示保留了载荷里的局部模式CNN 的卷积核能在上面滑动抓出类似协议头、固定字段这种局部特征。缺点是加密流量里字节接近随机CNN 抓不到稳定模式所以通常只取前若干字节不指望覆盖整条流。统计特征向量是另一条路每条流提取几十到几百维特征比如包数量、平均包长、上下行字节比、流持续时间、包到达间隔的均值方差等。这类特征维度高、冗余大直接丢给分类器容易过拟合SAE 就是用来做无监督压缩的。LSTM 则更适合包长序列或到达时间序列因为这类数据天然有序前后包之间存在依赖关系。我一般会同时准备两套输入一套字节序列给 CNN一套统计特征给 SAE如果数据里包序列信息完整再加一套包长序列给 LSTM。三套输入可以分别训练也可以后期做模型融合。2.2 用 Python 把原始流量转成模型输入假设原始数据是 CSV每行一条流包含标签列和若干统计特征列另外有一列存字节序列的十六进制字符串。下面这段代码做三件事读数据、把字节序列转成定长数组、把统计特征标准化。import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler # 读取流量数据label 列是类别byte_seq 列是十六进制字节串 df pd.read_csv(traffic.csv) # 字节序列转定长数组取前 784 字节不足补 0 def hex_to_bytes(hex_str, max_len784): raw bytes.fromhex(hex_str) if isinstance(hex_str, str) else b arr np.frombuffer(raw[:max_len], dtypenp.uint8) if len(arr) max_len: arr np.pad(arr, (0, max_len - len(arr)), constant_values0) return arr.astype(np.float32) / 255.0 byte_input np.stack(df[byte_seq].apply(hex_to_bytes).values) # 统计特征标准化SAE 和 LSTM 都用这套 stat_cols [c for c in df.columns if c.startswith(stat_)] scaler StandardScaler() stat_input scaler.fit_transform(df[stat_cols].values).astype(np.float32) # 标签转整数 labels pd.factorize(df[label])[0].astype(np.int64) np.save(byte_input.npy, byte_input) np.save(stat_input.npy, stat_input) np.save(labels.npy, labels)这段代码里max_len784是个可调参数取 784 是因为它接近 28x28方便 CNN 直接 reshape 成二维。如果你数据里流长度普遍偏短可以降到 256 或 512减少无效补零。StandardScaler对统计特征做零均值单位方差这一步对 SAE 很关键因为 SAE 的重构损失对特征尺度敏感不标准化的话大方差特征会主导训练。字节归一化用除以 255把取值压到 0 到 1配合 Sigmoid 或 ReLU 都稳定。注意不要用 MinMaxScaler 对字节做逐列缩放字节的列没有物理意义逐列缩放会破坏字节之间的相对关系。2.3 数据集划分与类别不平衡处理流量数据里类别不平衡是常态某些攻击流量样本极少。划分的时候不能简单随机切要按类别分层保证训练集和验证集里每个类别都有。用train_test_split的stratify参数即可。from sklearn.model_selection import train_test_split idx np.arange(len(labels)) train_idx, val_idx train_test_split( idx, test_size0.2, random_state42, stratifylabels ) X_byte_train, X_byte_val byte_input[train_idx], byte_input[val_idx] X_stat_train, X_stat_val stat_input[train_idx], stat_input[val_idx] y_train, y_val labels[train_idx], labels[val_idx]如果某个类别样本数少于 100训练时要在损失函数里加类别权重或者用重采样。我一般用CrossEntropyLoss的weight参数权重取类别频率的倒数这样少数类样本的损失会被放大模型不会直接忽略它们。不要一上来就上 SMOTE 生成字节序列字节序列插值出来的样本没有物理意义反而引入噪声。3. CNN 分支用一维卷积抓流量字节的局部模式3.1 为什么流量分类里一维卷积比二维更常用图像分类习惯用二维卷积因为图像有空间邻接关系。流量字节序列是一维的相邻字节之间有顺序依赖但没有上下左右这种二维结构。所以流量分类里主流做法是一维卷积卷积核在字节序列上滑动每次覆盖连续 K 个字节提取局部模式。一维卷积的另一个好处是参数量小。同样感受野下一维卷积核参数是二维的 1/K训练更快过拟合风险更低。如果你非要把字节 reshape 成 28x28 用二维卷积也不是不行但那样等于人为把字节序列切断成二维网格相邻行的字节在原始序列里可能相隔很远卷积核跨行滑动时抓到的模式没有实际意义。我一般用三层一维卷积每层后面接 BatchNorm 和 ReLU再跟一个最大池化。卷积核数量从 64 开始逐层翻倍到 256核大小取 3 或 5。池化窗口取 2每池化一次序列长度减半。3.2 搭一个能跑通的 CNN 分类器下面用 PyTorch 搭 CNN 分支输入是 784 维字节序列输出是类别数。import torch import torch.nn as nn class CNNClassifier(nn.Module): def __init__(self, num_classes, input_len784): super().__init__() self.features nn.Sequential( nn.Conv1d(1, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size3, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(128, 256, kernel_size3, padding1), nn.BatchNorm1d(256), nn.ReLU(), nn.AdaptiveMaxPool1d(1) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(256, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): # x 形状 (batch, 784)转成 (batch, 1, 784) x x.unsqueeze(1) x self.features(x) return self.classifier(x)padding2配合kernel_size5保证卷积后序列长度不变池化再减半。AdaptiveMaxPool1d(1)把每个通道压成一个值这样不管输入长度多少全连接层输入维度固定是 256。Dropout(0.3)放在全连接前防止过拟合。如果你的类别数少于 10最后一层 Linear 输出维度就是类别数。训练循环里用 Adam学习率 1e-3batch size 取 128。损失函数用带类别权重的交叉熵。每个 epoch 后在验证集上算准确率和 F1F1 比准确率更能反映少数类表现。from torch.utils.data import TensorDataset, DataLoader train_ds TensorDataset(torch.from_numpy(X_byte_train), torch.from_numpy(y_train)) train_loader DataLoader(train_ds, batch_size128, shuffleTrue) model CNNClassifier(num_classeslen(np.unique(labels))) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(20): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step()学习率 1e-3 是 Adam 的常用起点如果 loss 震荡就降到 3e-4。batch size 128 在显存够的情况下可以加到 256训练更稳。epoch 数看验证集 F1 什么时候不再涨一般 15 到 30 之间。3.3 CNN 分支的参数怎么调卷积核数量从 64 起步如果数据量超过 10 万条可以加到 128 起步。核大小 3 和 5 都试一下5 的感受野更大适合抓长一点的模式但参数量也大。池化窗口固定 2不要用 3因为 784 不是 3 的整数倍池化后长度不整齐。Dropout 比例在 0.2 到 0.5 之间调数据量小就取大一点。BatchNorm 放在卷积和 ReLU 之间不要放在 ReLU 后面这是常见顺序错误。如果训练 loss 下降但验证 loss 上升先加 Dropout再加 L2 正则还不行就减卷积层数。4. LSTM 分支用包长序列抓流内时序依赖4.1 流量里的时序信息到底指什么一条流不是单个包而是一串包按时间先后到达。每个包有长度、方向、到达间隔。这些量按时间排成序列前后之间存在依赖比如 TCP 握手阶段包长偏小且方向交替数据传输阶段包长偏大且方向集中。LSTM 的门控结构能记住这种长距离依赖比 CNN 只看局部窗口更适合抓流级别的行为模式。常见做法是取每条流的前 50 到 200 个包每个包用两个特征表示包长带符号正表示上行负表示下行和到达间隔。这样一条流就是一个形状为 (seq_len, 2) 的序列。seq_len 取 100 是折中太短丢信息太长训练慢且梯度容易消失。如果你的数据里没有包级信息只有统计特征那 LSTM 分支可以跳过直接用 SAE 加全连接。不要硬把统计特征当序列喂给 LSTM统计特征没有顺序LSTM 学不到东西。4.2 搭 LSTM 分类器并处理变长序列下面用 PyTorch 搭 LSTM 分支输入是 (batch, seq_len, 2)输出类别。class LSTMClassifier(nn.Module): def __init__(self, num_classes, input_dim2, hidden_dim128, num_layers2): super().__init__() self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropout0.3 ) self.classifier nn.Sequential( nn.Linear(hidden_dim * 2, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): # x 形状 (batch, seq_len, 2) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出双向所以拼接了正向和反向 last out[:, -1, :] return self.classifier(last)bidirectionalTrue让 LSTM 同时从前往后和从后往前读序列最后时间步的输出拼接了两个方向的信息维度是hidden_dim * 2。num_layers2加dropout0.3是常见组合层数再多容易过拟合。batch_firstTrue让输入形状是 (batch, seq_len, feature)符合直觉。如果序列是变长的用pack_padded_sequence处理避免 padding 的零影响 LSTM 状态。具体做法是先按序列长度降序排序再 pack过完 LSTM 再 unpack。这一步容易写错我一般先跑通定长版本确认模型能学之后再上变长。训练参数上LSTM 学习率要比 CNN 小取 5e-4 或 1e-3。梯度裁剪设 1.0防止梯度爆炸。batch size 取 64因为 LSTM 显存占用比 CNN 大。4.3 LSTM 分支的常见参数与边界hidden_dim 取 128 是起点数据量大可以加到 256。num_layers 取 2超过 3 层收益很小且训练慢。dropout 在 LSTM 内部和全连接前都加比例 0.3 左右。序列长度 seq_len 取 100如果流里包数普遍少于 50就取实际最大包数。包长特征要做归一化除以最大包长让取值落在 -1 到 1 之间。到达间隔取对数后再归一化因为间隔分布长尾严重直接归一化会被极值主导。LSTM 训练慢是常态同样数据量下比 CNN 慢三到五倍。如果时间紧可以先跑 CNN 和 SAELSTM 作为后期提升手段。不要指望 LSTM 单独就能比 CNN 高很多流量分类里时序信息是补充不是主力。5. SAE 分支用无监督重构做特征压缩和去噪5.1 SAE 在流量分类里到底解决什么问题SAE 是堆叠自编码器本质是多个自编码器逐层堆叠每层先无监督预训练再整体微调。在流量分类里SAE 的作用不是直接分类而是把高维统计特征压成低维稠密表示同时去掉冗余和噪声。统计特征动辄上百维很多特征之间高度相关比如上行字节数和下行字节数、包总数和各类包计数。直接丢给全连接分类器参数量大且容易过拟合。SAE 先把这些特征压到 32 或 64 维再送分类器参数量降一个量级泛化更好。SAE 的另一个好处是可以用无标签数据预训练。流量数据里带标签的少无标签的多SAE 能在无标签数据上学到特征表示再用少量标签微调分类头。这个半监督思路在流量分类里很实用。5.2 搭 SAE 并做逐层预训练下面用 PyTorch 搭一个三层 SAE输入维度假设是 128逐层压到 64、32再对称解压回 128。class SAE(nn.Module): def __init__(self, input_dim128, hidden_dims(64, 32)): super().__init__() # 编码器 self.encoder nn.Sequential( nn.Linear(input_dim, hidden_dims[0]), nn.ReLU(), nn.Linear(hidden_dims[0], hidden_dims[1]), nn.ReLU() ) # 解码器对称结构 self.decoder nn.Sequential( nn.Linear(hidden_dims[1], hidden_dims[0]), nn.ReLU(), nn.Linear(hidden_dims[0], input_dim) ) def forward(self, x): z self.encoder(x) return self.decoder(z), z逐层预训练的做法是先训一个 128 到 64 的自编码器只保留编码部分再把 64 维输出作为下一个 64 到 32 自编码器的输入训完保留编码部分最后把两层编码拼起来接分类头用带标签数据微调。# 逐层预训练第一层 ae1 nn.Sequential(nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 128)) opt1 torch.optim.Adam(ae1.parameters(), lr1e-3) for epoch in range(30): for xb, _ in unlabeled_loader: opt1.zero_grad() loss nn.functional.mse_loss(ae1(xb), xb) loss.backward() opt1.step() # 用第一层编码输出作为第二层输入 with torch.no_grad(): h1 ae1[0](torch.from_numpy(X_stat_train)).numpy()预训练学习率 1e-3epoch 30 左右重构损失降到稳定即可。预训练完把编码器权重固定接一个 Linear 分类头再用标签数据微调微调学习率降到 1e-4避免破坏预训练学到的表示。5.3 SAE 的瓶颈维度和正则怎么选瓶颈维度取 32 或 64取决于原始特征维度和类别数。原始 128 维压到 32 是四倍压缩一般够用。如果类别多且类间差异细压到 64 保留更多信息。SAE 容易过拟合因为重构任务本身可能记住训练样本。加 L2 正则或对输入加噪声做去噪自编码器。去噪做法是输入随机置零 10% 到 20% 的特征让模型从残缺输入重构完整输入学到的表示更鲁棒。预训练数据可以用全部数据包括无标签的微调只用训练集。验证集不要参与预训练否则等于泄露。如果无标签数据量很大预训练 epoch 可以加到 50但要注意重构损失不再降就停继续训只会过拟合。6. 三个分支怎么合、怎么评、怎么排查翻车6.1 融合策略拼接、加权还是投票三个分支训完之后融合方式有三种。最简单是特征拼接把 CNN 的全连接前输出、LSTM 的最后时间步输出、SAE 的瓶颈表示拼成一个向量接一个全连接分类器端到端微调。这种方式让模型自己学各分支权重效果通常最好但需要重新训练融合层。第二种是加权投票三个分支各自输出类别概率按验证集 F1 给权重加权平均。这种方式不用重训适合分支已经训好不想动的情况。权重可以按 F1 归一化比如 CNN 0.5、LSTM 0.3、SAE 0.2。第三种是堆叠把三个分支的输出概率作为新特征训一个逻辑回归做最终分类。这种方式在小数据上稳但多一层容易过拟合。我一般先试特征拼接如果显存不够或训练不稳定退回加权投票。拼接时注意各分支输出维度差异大先各自过一层 Linear 压到同一维度再拼避免某一分支维度主导。6.2 评估指标不能只看准确率流量分类里准确率会骗人。如果 90% 样本是正常流量模型全预测正常也能拿 90% 准确率。必须看每个类别的精确率、召回率和 F1尤其是少数类。from sklearn.metrics import classification_report, confusion_matrix model.eval() with torch.no_grad(): logits model(torch.from_numpy(X_byte_val)) preds logits.argmax(dim1).numpy() print(classification_report(y_val, preds, digits4)) print(confusion_matrix(y_val, preds))classification_report给出每个类别的 precision、recall、f1-score。重点看少数类的 recall如果某个攻击类 recall 低于 0.5说明模型基本没学到它。混淆矩阵能看出哪些类别之间容易混比如两种攻击流量特征接近模型分不开。宏平均 F1 比微平均 F1 更能反映类别不平衡下的表现报告里看 macro avg 那一行。如果宏平均和微平均差距大说明少数类拖后腿要回去加类别权重或补样本。6.3 避坑与常见问题排查现象一训练 loss 正常下降验证 loss 从第一个 epoch 就很高。原因通常是数据泄露或预处理不一致。检查标准化是不是在划分训练验证之前做的如果是验证集信息泄露进了训练。解决方法是先划分再 fit scaler只用训练集 fit验证集 transform。现象二CNN 分支准确率卡在随机水平不涨。原因多半是字节序列全被补零或归一化错误。检查hex_to_bytes里bytes.fromhex是否抛异常被吞掉导致大部分样本变成全零。解决方法是加异常计数打印有多少条流解析失败失败的流要么丢弃要么单独处理。现象三LSTM 训练几个 epoch 后 loss 变 NaN。原因是梯度爆炸序列长且学习率大时常见。解决方法是加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)并把学习率降到 5e-4。如果还 NaN检查输入里有没有 inf 或 nan标准化时除零会产生 nan。现象四SAE 预训练重构损失降不下去。原因是瓶颈维度太小或输入特征尺度差异大。先把瓶颈从 32 加到 64 试如果还不行检查标准化是否对每个特征都做了有没有常数列导致方差为零。常数列直接删掉它不携带信息还干扰训练。现象五融合后效果比单分支还差。原因是分支输出尺度不一致拼接后某一分支主导。解决方法是拼接前对每个分支输出做 BatchNorm 或 L2 归一化让各分支输出尺度接近。另外检查融合层学习率端到端微调时融合层学习率要小取 1e-4避免随机初始化的融合层把预训练分支带偏。7. 把三分支融合跑稳之后我习惯再做一步消融验证三分支融合能跑出结果之后别急着调参冲更高分先做一轮消融确认每个分支到底贡献了多少。做法很简单分别只用 CNN、只用 LSTM、只用 SAE、CNNLSTM、CNNSAE、LSTMSAE、三分支全上七种组合各训一次记录宏平均 F1。这一步能告诉你哪个分支是主力哪个分支在拖后腿。我自己的经验是字节序列干净、类别区分明显的数据集上CNN 单独就能拿到接近融合的效果LSTM 和 SAE 提升有限。但在加密流量或类别细分的场景里SAE 的降维去噪往往比 LSTM 贡献更大因为统计特征里的冗余被压掉之后分类边界更清晰。LSTM 的贡献最不稳定包序列信息完整时能提两三个点信息缺失时基本没用。消融结果用表格记下来比脑子记靠谱。组合宏平均 F1少数类召回CNN0.820.61LSTM0.740.48SAE0.780.55CNNLSTM0.850.66CNNSAE0.870.70LSTMSAE0.810.59三分支融合0.890.73这张表是示意实际数字看你数据。重点看两列宏平均 F1 反映整体少数类召回反映模型有没有忽略难类。如果三分支融合比 CNNSAE 只高一点点而 LSTM 训练成本是 CNN 的三倍那 LSTM 就可以砍掉把时间花在数据清洗和特征工程上。消融之后如果还想再提优先动数据而不是动模型。流量分类里特征质量比模型结构重要。检查统计特征里有没有漏掉的关键量比如 TCP 窗口大小、TLS 握手版本、DNS 查询类型。这些领域特征加进去比换模型结构提升大。最后说个习惯每次实验把配置文件、随机种子、数据划分索引都存下来。流量分类的随机性不小同一个模型跑两次差一两个点是常事。没有记录的话过两天就分不清是模型改了还是随机波动。我吃过这个亏现在每个实验目录里必放一个config.json和split_idx.npy后悔药提前备好。希望帮到你。本文还有配套的精品资源点击获取
返回列表