ARTICLE DETAIL

资讯详情

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

基于CNN、LSTM与SAE的深度学习网络流量分类实战

基于CNN、LSTM与SAE的深度学习网络流量分类实战 简介这份资源面向深度学习入门者与网络流量分析方向的学习者提供基于CNN、LSTM和SAE三种模型的流量数据分类完整实现方案帮助读者理解不同网络结构在流量特征提取与分类任务上的差异与适用场景。压缩包共13个文件约456KB以Python源码为主包含5个py脚本与4个pyc编译文件分别承担模型定义、数据加载、训练流程与主程序入口等职责另附训练集与测试集的json数据文件、配置json及说明文档便于直接复现实验。目前已有672人学习下载。资源目录按模型、数据、训练模块清晰划分读者可据此对比卷积神经网络、长短期记忆网络与稀疏自编码器在流量分类中的建模思路掌握数据预处理、模型搭建、训练评估的完整链路并在此基础上替换自有数据集进行迁移实验适合作为课程设计、毕业设计或相关课题的参考实现。1. 流量分类为什么值得用 CNN、LSTM 和 SAE 三件套一起上网络流量分类这件事做过的人都知道它有多玄学。同一条 TCP 流抓包时间差一点、采样率变一下、加密层多包一层特征分布就完全变了。传统做法靠端口号、靠 DPI 正则、靠统计特征喂给随机森林或 SVM端口混淆和加密流量一上来就集体翻车。这几年大家转向深度学习CNN 抓局部字节模式、LSTM 抓包序列的时序依赖、SAE稀疏自编码器做无监督特征压缩三条路各有各的擅长区间。把这三者组合起来做流量数据分类是当前 Python 深度学习落地里性价比很高的一条路线数据可以自己抓模型用 PyTorch 就能搭单卡就能跑。这篇笔记面向的是想动手复现一套完整流量分类 pipeline 的工程师从数据预处理、三路模型搭建、训练调参到避坑排查全部落到可抄的代码和参数上新手能跟着跑通熟手能直接看到边界在哪。2. 数据准备与特征工程从 pcap 到模型能吃的张量流量分类的成败七成在数据。模型再花哨喂进去的张量不对loss 照样不降。这一章把从原始流量到模型输入的完整链路拆开讲包括抓包、切流、特征提取和三种模型各自需要的输入格式。2.1 流量数据的采集与流切分常见做法是用抓包工具拿到 pcap 文件然后按五元组源 IP、源端口、目的 IP、目的端口、协议把连续报文切成一条条流。每条流取前 N 个包每个包取前 M 个字节组成一个 N×M 的字节矩阵。这个矩阵就是 CNN 和 SAE 的原始输入而 LSTM 则在这个矩阵基础上按包序列展开。import numpy as np from scapy.all import rdpcap, IP, TCP, UDP def split_flows(pcap_path, max_packets20, max_bytes64): 把 pcap 按五元组切成流每条流取前 max_packets 个包 每个包取前 max_bytes 字节不足补零。 packets rdpcap(pcap_path) flows {} for pkt in packets: if IP not in pkt: continue proto TCP if TCP in pkt else (UDP if UDP in pkt else OTHER) if proto TCP: key (pkt[IP].src, pkt[TCP].sport, pkt[IP].dst, pkt[TCP].dport, proto) elif proto UDP: key (pkt[IP].src, pkt[UDP].sport, pkt[IP].dst, pkt[UDP].dport, proto) else: continue raw bytes(pkt)[:max_bytes] flows.setdefault(key, []).append(raw) X np.zeros((len(flows), max_packets, max_bytes), dtypenp.uint8) for i, (key, pkts) in enumerate(flows.items()): for j, raw in enumerate(pkts[:max_packets]): X[i, j, :len(raw)] np.frombuffer(raw, dtypenp.uint8) return X这段代码的逻辑很直白遍历每个包按五元组归入对应的流字典然后统一裁剪成固定形状的张量。max_packets20和max_bytes64是两个关键参数——包数太少会丢掉流的节奏感太多则引入大量冗余和噪声字节数取 64 是因为大多数协议头部信息集中在前 64 字节再往后多是载荷对分类贡献递减。实际调参时如果目标是识别应用类型20×64 够用如果要区分细粒度协议可以加到 30×128但显存和训练时间会明显上升。2.2 三种模型对输入格式的不同要求CNN、LSTM、SAE 虽然吃的是同一批流数据但对张量形状的偏好完全不同这一步搞错就是最常见的翻车点。模型输入形状含义关键预处理CNN(batch, 1, N, M)单通道图像N 包 × M 字节归一化到 [0,1]加通道维LSTM(batch, N, M)序列长度 N每步特征 M按包序列排列特征维归一化SAE(batch, N*M)展平后的向量展平 标准化常做稀疏约束CNN 把字节矩阵当图像看卷积核在字节维度上滑动能捕捉到类似协议头固定字段的局部模式。LSTM 把每个包当作时间步学习包与包之间的先后依赖比如握手阶段的包序。SAE 则是先把整条流展平成一个长向量通过编码器压到低维再解码重构用重构误差和稀疏惩罚逼着网络学到最紧凑的表示。import torch from torch.utils.data import TensorDataset, DataLoader def build_loaders(X, y, batch_size64): X X.astype(np.float32) / 255.0 # 归一化 X_cnn torch.tensor(X).unsqueeze(1) # (B,1,N,M) X_lstm torch.tensor(X) # (B,N,M) X_sae torch.tensor(X.reshape(len(X), -1)) # (B,N*M) y torch.tensor(y, dtypetorch.long) loaders {} for name, feat in [(cnn, X_cnn), (lstm, X_lstm), (sae, X_sae)]: ds TensorDataset(feat, y) loaders[name] DataLoader(ds, batch_sizebatch_size, shuffleTrue) return loaders这里batch_size64是通用起点显存吃紧就降到 32。归一化用除以 255 是最省事的做法如果字节分布极度不均可以换成按通道做 z-score 标准化。注意 SAE 的展平维度是 N×M20×64 就是 1280 维输入层别写错否则第一个全连接层直接报维度不匹配。3. 三路模型搭建CNN、LSTM、SAE 各自怎么落地数据管道通了之后进入模型搭建。这一章把三个模型分别写清楚包括结构设计理由、代码实现和关键参数最后给出一个融合三路特征的组合方案。3.1 CNN 分支用一维卷积抓字节局部模式流量字节矩阵在字节这个维度上有很强的局部相关性比如 IP 头的前 20 字节结构固定TCP 头的标志位集中在特定偏移。用二维卷积把包维和字节维一起卷或者用一维卷积只在字节维滑动都是常见做法。我一般用二维卷积卷积核设成 (3,3)让它同时看到相邻几个包和相邻几个字节。import torch.nn as nn class CNNBranch(nn.Module): def __init__(self, num_classes10): super().__init__() self.conv nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # N,M 各减半 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.AdaptiveAvgPool2d((4, 4)) # 固定输出尺寸 ) self.fc nn.Linear(64 * 4 * 4, 128) def forward(self, x): x self.conv(x) x x.flatten(1) return self.fc(x)Conv2d(1, 32, ...)里第一个 1 是输入通道因为流量矩阵是单通道。两层卷积后接AdaptiveAvgPool2d((4,4))是为了不管输入包数字节数怎么变输出尺寸都固定后面全连接层不用改。BatchNorm2d在流量数据上很关键因为不同流的字节分布差异大不加 BN 训练很容易震荡。输出 128 维特征方便后面和 LSTM、SAE 分支拼接。3.2 LSTM 分支把包序列当时间步来建模LSTM 处理的是包与包之间的顺序关系。输入形状 (B, N, M)N 是包序列长度M 是每包字节特征。这里有个细节直接把 64 字节喂进 LSTM每步特征维是 64序列长度 20模型能学但效率一般。更常见的做法是先用一个线性层把每包 64 字节压到 32 维再送进 LSTM。class LSTMBranch(nn.Module): def __init__(self, num_classes10, hidden64): super().__init__() self.feat_proj nn.Linear(64, 32) # 每包字节降维 self.lstm nn.LSTM(input_size32, hidden_sizehidden, num_layers2, batch_firstTrue, bidirectionalTrue, dropout0.3) self.fc nn.Linear(hidden * 2, 128) # 双向所以乘2 def forward(self, x): # x: (B, N, M) x torch.relu(self.feat_proj(x)) # (B, N, 32) out, (h, c) self.lstm(x) last out[:, -1, :] # 取最后时间步 return self.fc(last)num_layers2加bidirectionalTrue是流量分类里比较稳的配置双向能同时看到包序列的前后文。dropout0.3防过拟合流量数据集通常类别不均过拟合很常见。取out[:, -1, :]是拿最后一个时间步的隐状态做分类也可以用平均池化把所有时间步平均后者对长序列更稳。hidden 设 64 是起点数据量大可以加到 128。3.3 SAE 分支稀疏自编码器做无监督特征压缩SAE 的价值在于无监督预训练。流量数据标注成本高先用大量无标注流训练 SAE 学一个紧凑表示再拿编码器输出接分类头小样本下效果往往比直接监督训练好。稀疏约束通过 KL 散度或 L1 惩罚实现逼着大部分隐单元在大多数时候保持静默。class SAEBranch(nn.Module): def __init__(self, input_dim1280, hidden256, sparse_dim64): super().__init__() self.encoder nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, sparse_dim), nn.ReLU() ) self.decoder nn.Sequential( nn.Linear(sparse_dim, hidden), nn.ReLU(), nn.Linear(hidden, input_dim) ) def forward(self, x): z self.encoder(x) recon self.decoder(z) return z, recon def sparse_loss(self, z, rho0.05, beta1.0): rho_hat z.mean(dim0) kl rho * torch.log(rho / (rho_hat 1e-8)) \ (1 - rho) * torch.log((1 - rho) / (1 - rho_hat 1e-8)) return beta * kl.sum()input_dim1280对应 20×64 展平。sparse_dim64是压缩后的表示维度从 1280 压到 64压缩比 20:1这个比例在流量数据上比较合适再低会丢信息。rho0.05是目标稀疏度意思是希望每个隐单元平均只有 5% 的样本被激活。beta1.0控制稀疏惩罚权重太大模型学不动太小稀疏约束形同虚设一般从 0.5 到 2.0 之间调。3.4 三路特征融合与分类头三个分支各自输出 128 维特征拼接成 384 维接两层全连接做最终分类。融合方式有早期融合拼特征和晚期融合拼 logits流量分类里早期融合更常见因为三路特征互补性强。class FusionNet(nn.Module): def __init__(self, num_classes10): super().__init__() self.cnn CNNBranch() self.lstm LSTMBranch() self.sae SAEBranch() self.classifier nn.Sequential( nn.Linear(128 * 3, 128), nn.ReLU(), nn.Dropout(0.4), nn.Linear(128, num_classes) ) def forward(self, x_cnn, x_lstm, x_sae): f1 self.cnn(x_cnn) f2 self.lstm(x_lstm) z, recon self.sae(x_sae) f3 z feat torch.cat([f1, f2, f3], dim1) return self.classifier(feat), recon训练时损失是分类交叉熵加上 SAE 的重构误差和稀疏惩罚。Dropout(0.4)在融合层前因为 384 维拼接特征冗余度高不 dropout 很容易过拟合。三路分支可以联合训练也可以先单独预训练 SAE 再冻结编码器后者在小数据集上更稳。4. 训练、调参与评估让三路模型真正跑起来模型搭好只是开始训练过程里的学习率、类别不平衡、评估指标选择每一项都能让结果差出一大截。这一章讲训练循环怎么写、关键超参怎么设、评估怎么做才不被虚高指标骗。4.1 训练循环与损失函数组合总损失由三部分组成分类交叉熵、SAE 重构 MSE、稀疏 KL 惩罚。三者权重需要平衡否则 SAE 分支要么学不到东西要么主导整个梯度。def train_one_epoch(model, loaders, optimizer, device, lam_recon0.5, lam_sparse0.1): model.train() total_loss 0 for (x_cnn, y), (x_lstm, _), (x_sae, _) in zip( loaders[cnn], loaders[lstm], loaders[sae]): x_cnn, x_lstm, x_sae, y (x_cnn.to(device), x_lstm.to(device), x_sae.to(device), y.to(device)) optimizer.zero_grad() logits, recon model(x_cnn, x_lstm, x_sae) cls_loss nn.functional.cross_entropy(logits, y) recon_loss nn.functional.mse_loss(recon, x_sae) z model.sae.encoder(x_sae) sp_loss model.sae.sparse_loss(z) loss cls_loss lam_recon * recon_loss lam_sparse * sp_loss loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loaders[cnn])lam_recon0.5和lam_sparse0.1是起点。重构权重太大会让 SAE 只顾重建不管分类太小则编码器学不到有意义的表示。稀疏权重一般设得比重构小一个量级因为 KL 惩罚数值上容易偏大。优化器用 Adam学习率 1e-3配合ReduceLROnPlateau在验证 loss 不降时减半。4.2 类别不平衡与评估指标选择流量数据天然类别不均某些协议样本几千条某些只有几十条。直接用准确率评估会被多数类带偏看起来 95% 其实少数类全错。正确做法是用宏平均 F1 和每类召回率。from sklearn.metrics import classification_report, f1_score def evaluate(model, loader, device): model.eval() preds, labels [], [] with torch.no_grad(): for batch in loader: x_cnn, y batch[0].to(device), batch[1].to(device) x_lstm batch[0].squeeze(1).to(device) x_sae batch[0].flatten(1).to(device) logits, _ model(x_cnn, x_lstm, x_sae) preds.extend(logits.argmax(1).cpu().numpy()) labels.extend(y.cpu().numpy()) print(classification_report(labels, preds, digits4)) return f1_score(labels, preds, averagemacro)类别不平衡的缓解手段有两个一是损失里加weightclass_weights权重按类别频率倒数算二是用 WeightedRandomSampler 让每个 batch 里少数类被过采样。我一般两个一起上先 sampler 再 loss weight效果比单用一种稳。4.3 超参搜索的实用范围流量分类这套模型超参不用网格搜到底几个关键参数定好范围就够。参数推荐范围说明学习率1e-4 ~ 3e-3Adam 下 1e-3 最稳batch_size32 ~ 128显存允许优先 64max_packets10 ~ 3020 是通用起点max_bytes32 ~ 12864 覆盖多数协议头SAE sparse_dim32 ~ 12864 压缩比适中dropout0.2 ~ 0.5融合层前 0.4调参顺序建议先固定其他调学习率和 batch_size 让 loss 正常下降再调 max_packets 和 max_bytes 看验证 F1 是否提升最后微调 SAE 的稀疏维度和权重。每次只动一个变量否则出了问题根本不知道是谁的锅。5. 避坑与排查流量分类里最容易翻车的五个地方这套 pipeline 我前后搭过几轮踩的坑基本集中在数据对齐、维度、数值稳定和评估这几个环节。下面五条按现象 → 原因 → 解决写都是血泪经验。5.1 训练 loss 不降反升现象第一个 epoch loss 就飙到 nan 或者持续上升。原因通常是学习率太大或者 SAE 的稀疏 KL 里出现了 log(0)。解决学习率降到 1e-4 试KL 计算里rho_hat加1e-8防除零同时检查输入归一化有没有漏做。字节数据不归一化直接进网络梯度量级会失控。5.2 验证集准确率虚高但实际预测全是一类现象验证准确率 90%但打印混淆矩阵发现所有样本都预测成多数类。原因类别极度不平衡 用准确率做早停指标。解决早停指标换成宏平均 F1损失加类别权重训练集用 WeightedRandomSampler 重采样。这一步不做模型就是个只会猜多数类的黑匣子。5.3 三个 DataLoader 顺序对不上现象训练时 loss 忽高忽低完全没有收敛趋势。原因CNN、LSTM、SAE 三个 loader 各自 shuffle同一个 batch 里三路输入对应的根本不是同一条流。解决要么用一个 Dataset 同时返回三种形状要么固定 shuffle 种子并保证三个 loader 的 shuffle 顺序一致。最稳的做法是自定义 Dataset__getitem__里返回 (x_cnn, x_lstm, x_sae, y) 四元组。5.4 SAE 重构 loss 一直很高现象分类 loss 正常降但重构 loss 卡在某个值下不去。原因稀疏惩罚权重lam_sparse设太大编码器被压得只剩少数单元激活重构能力被牺牲。解决把lam_sparse从 0.1 降到 0.01 试或者先只用重构 loss 预训练 SAE 几个 epoch再联合训练。稀疏度和重构质量本身是 trade-off别指望两个都拉满。5.5 推理时维度对不上现象训练能跑加载模型做单条推理时报 shape mismatch。原因训练时 batch 维度是隐式的单条推理少了一个 batch 维或者 SAE 输入忘了 flatten。解决推理前统一x x.unsqueeze(0)补 batch 维SAE 输入显式x.reshape(1, -1)。这个坑不致命但很烦建议在模型 forward 里加一句维度断言早发现早改。6. 把三路模型压进生产量化、导出与增量更新训练完的模型要真用起来还得过部署这一关。流量分类模型通常要求低延迟单条流推理最好在毫秒级这就涉及模型量化和导出。PyTorch 的动态量化对 LSTM 和全连接层效果明显模型体积能压到原来的四分之一推理速度提升两三倍精度损失通常在一个点以内。import torch.quantization as tq model.eval() quantized tq.quantize_dynamic( model, {nn.LSTM, nn.Linear}, dtypetorch.qint8 ) torch.save(quantized.state_dict(), flow_cls_quantized.pt)quantize_dynamic只量化 LSTM 和 Linear卷积层保持浮点因为流量数据上卷积层对量化更敏感。导出后建议用一批真实流量做回归测试对比量化前后的宏 F1掉超过两个点就考虑只量化 LSTM 分支。增量更新是另一个生产必备能力。新协议、新应用不断出现模型不能每次重训。我的习惯是冻结 CNN 和 LSTM 分支只微调 SAE 编码器和最后的分类头学习率设成原来的十分之一用新样本跑几个 epoch。这样既保留旧类知识又能快速适配新类。验证时重点看旧类召回有没有掉掉了就说明灾难性遗忘开始了得把旧样本混进新数据一起微调。最后说个我自己的教训别一上来就追求三路融合的极致精度先把单路 CNN 跑通、评估指标搭好、数据管道验证无误再逐步加 LSTM 和 SAE。我最早就是三路一起上结果 loss 不降排查了两天才发现是三个 loader 顺序没对齐这种低级问题。把复杂度一步步加上去每一步都可验证比一口气堆完再 debug 快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表