PyTorch实现CNN-LSTM网络流量检测系统全解析

📅 发布时间:2026/8/31 19:38:40
PyTorch实现CNN-LSTM网络流量检测系统全解析
简介本资源是一套基于PyTorch实现的CNN-LSTM混合网络流量检测系统面向高校计算机科学、网络安全方向的本科生与研究生解决网络异常行为识别与分类建模问题适用于毕业设计、课程实验及科研入门实践。压缩包共10个文件5个核心Python源码、3个备份文件、1个Markdown说明文档及1个嵌套ZIP总大小仅11KB结构精简——含模型定义model.py、数据预处理data_preprocess.py、训练测试主流程train_and_test.py、主入口main.py及README.md等关键模块便于快速理解整体架构与代码逻辑。已有95人学习下载读者可直接复现95%准确率的流量分类效果获取完整端到端实现从KDDCUP抽样数据加载、时序特征卷积提取、LSTM时序建模到模型训练、评估与结果分析全流程代码并附有清晰注释与模块化设计参考。 网络流量检测这件事做的时间越长越觉得传统方法不够用。之前用规则和特征工程去识别恶意流量特征筛选、规则维护都相当费劲而且一遇到加密流量或者变种攻击就容易抓瞎。后来我逐渐把重心转到深度学习上用PyTorch实现了基于CNN-LSTM的网络流量检测系统整体效果比传统机器学习好了不少尤其是在时序特征和局部特征融合方面CNN和LSTM的组合确实能打。这篇文章就以这个项目为主线把环境搭建、数据预处理、模型结构、训练调优和性能分析这几块完整拆开讲给同样想用PyTorch做网络流量检测的朋友一条可以直接上手的路径。适合谁看呢如果你手头有流量数据集但不知道从哪下手或者已经跑通一个简单分类模型但精度上不去又或者想了解CNN-LSTM这种混合结构在网络安全场景里怎么落地那这篇文章应该能帮你省不少时间。默认你懂一点Python和机器学习基础但即使基础薄弱按步骤来也能跑通。1. 为什么选CNN-LSTM做网络流量检测1.1 网络流量检测的本质与难点网络流量检测核心其实就是流量分类或异常识别。常见的场景包括恶意流量检测、入侵检测、协议识别、加密流量分类等。传统做法靠人工设计特征比如流量持续时间、包长统计、协议标志位等等再喂给随机森林、XGBoost这类模型。这类方法的问题在于特征工程非常依赖经验而且面对隧道、加密、伪装流量的泛化能力很差。把流量数据当作时间序列来看一个网络会话就是一个包序列前后包之间有时间依赖包的大小、方向、到达时间间隔都在持续变化。这正好是LSTM擅长建模的方向。同时每个包本身的特征组合比如负载字节分布、TCP窗口大小、标志位组合之类又存在局部相关性这又适合CNN去抽取。所以很自然的想法就是把两者串起来先用CNN在局部窗口内提取特征再用LSTM去捕捉跨时间的依赖关系。1.2 为什么不用纯CNN或纯LSTM纯CNN擅长空间或局部模式但网络流量的时序长度变化很大单纯依赖固定卷积核很难捕捉长期依赖纯LSTM虽然能建模时序但输入通常是高维原始特征参数多、收敛慢而且容易忽略局部关键模式。CNN-LSTM组合的巧妙之处在于CNN先把高维流量特征压缩成有表达力的局部抽象特征LSTM再顺着时间维度去建模包与包之间的动态变化。从实际效果看这种结构在CICIDS2017、UNSW-NB15这类公开数据集上的分类表现通常能比单独用CNN或LSTM高几个百分点。另外PyTorch对这类组合模型的支持非常友好。它的nn.Conv1d、nn.LSTM的接口设计得很直觉可以方便地把两个模块拼装在一起而且能灵活控制维度变化。我用TensorFlow也写过类似结构但调试维度时还是更习惯PyTorch的动态图机制。这也是我选PyTorch的主要原因。2. 环境搭建与流量数据预处理2.1 PyTorch环境搭建的几条经验这个项目的开发环境我推荐用Anaconda来管理。先创建独立环境避免把系统Python搞乱。安装GPU版PyTorch时最关键的坑是CUDA版本匹配。你装PyTorch前先看一下自己的显卡驱动支持哪个CUDA版本用nvidia-smi查看。比如驱动支持CUDA 12.1那就可以装对应的PyTorch版本。安装命令一般从PyTorch官网获取最简单的形式是conda create -n flow python3.10 conda activate flow pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121之前有朋友直接pip install torch装CPU版跑了半天才发现慢得离谱。所以装完一定要检查一下CUDA是否可用import torch print(torch.__version__) print(torch.cuda.is_available())如果输出False多半是CUDA版本不匹配或者PyTorch装了CPU版。这里还有个容易被忽略的点PyTorch 2.6之后torch.load默认weights_onlyTrue如果你加载的是完整的训练检查点里面包含模型对象会报错。这就需要在保存和加载时显式处理。我一会儿在踩坑部分详细说。2.2 网络流量数据怎么表示成模型输入这是项目里最容易被低估的一步。模型再厉害输入数据不对全都白搭。网络流量数据通常以PCAP包的形式存在。要作为CNN-LSTM输入我一般会按“会话”或“流”为单位组织样本。一条流包含源IP、目的IP、源端口、目的端口、协议、开始时间、结束时间、包序列等多个字段。模型输入有几种常见处理方式第一种是直接用结构化流特征每条流提取N个统计特征组成一个一维特征向量。这种最简单适合用全连接网络或CNN处理但缺点是比较依赖特征是预先设计好的。第二种是把流的前若干包截取出来每个包提取关键字段比如包长、到达间隔、方向、TCP标志位等组成一个序列这样每个样本就是 (seq_len, feature_dim) 的二维矩阵正好能喂给CNN-LSTM。第三种更极端直接处理原始字节把每个包的前L个字节作为特征组成字节序列。第三种方式上限高但数据量需求和计算开销都大。这个项目我采用的是第二种方式。预处理流程大概是从PCAP或公开CSV数据集中解析出每条流定长截取或填充到固定长度seq_len20也就是说只取每条流的前20个包不足的补零每个包提取8个特征包括包长、时间间隔、方向、TCP窗口大小、标志位等对所有特征做归一化避免数值范围差异过大影响训练标签编码比如正常流量为0DDoS攻击为1端口扫描为2等把数据转成PyTorch的Dataset和DataLoader。这里有一个重点很多人在预处理阶段容易忽略“按流切分”和“保证训练测试集不重叠”的问题。如果一个IP会话同时出现在训练集和测试集模型等于见过答案测试分数虚高上线后效果必然拉胯。所以我会先按流的五元组去重再按流ID划分数据集而不是直接在包级别随机切。2.3 构建数据加载器数据加载器的代码虽然简单但要注意两个细节shuffle和worker数量。对时间序列数据训练集应该shuffle避免模型学到样本顺序测试集不要shuffle。num_workers在Linux上可以设大一点Windows上设2到4就好不然容易报错。另外如果每个样本的序列长度已经固定了那不用collate_fn做动态padding。如果长度不固定就需要自定义collate_fn这个后面再细说。下面是我常用的一个简单数据加载框架from torch.utils.data import Dataset, DataLoader class TrafficDataset(Dataset): def __init__(self, X, y): self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.long) def __len__(self): return len(self.y) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_dataset TrafficDataset(X_train, y_train) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4)3. CNN-LSTM模型的完整实现3.1 模型结构设计思路CNN-LSTM模型的结构我按照“输入序列 → 一维CNN特征提取 → LSTM时序建模 → 全连接分类”的顺序来搭。Conv1d处理的是维度为 (batch, channels, length) 的数据。对网络流量来说channels对应每个包的特征数length对应包序列长度。比如输入形状是(batch, seq_len20, input_size8)为了喂给Conv1d需要转成(batch, 8, 20)。CNN卷积核在序列长度维度上滑动等于同时观察相邻几个包的特征组合提取局部模式。然后经过ReLU激活和最大池化压缩序列长度。接着把CNN输出送入LSTM。LSTM期望输入是(seq_len, batch, input_size)或(batch, seq_len, input_size)设置batch_firstTrue。这里要注意从CNN出来的是(batch, channels, new_seq_len)需要转回(batch, new_seq_len, channels)。LSTM会逐步更新隐藏状态最后的隐藏状态h_n包含了整条序列的动态信息再接一个全连接层做分类。我用的是两层结构第一层Conv1d负责局部特征提取第二层LSTM负责时序特征捕捉。如果你的数据维度更大可以多加一层CNN或者LSTM但要注意过拟合问题。网络流量检测数据集往往不大模型过于复杂反而容易跑偏。3.2 核心代码实现与维度推导模型定义直接上代码关键地方都写了注释import torch import torch.nn as nn class CNNLSTM(nn.Module): def __init__(self, input_size, seq_len, num_classes): super(CNNLSTM, self).__init__() self.conv1 nn.Sequential( # 输入: (batch, input_size, seq_len) nn.Conv1d(in_channelsinput_size, out_channels64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool1d(kernel_size2, stride1) # 不改变序列长度过多只压缩一点 ) self.lstm nn.LSTM( input_size64, hidden_size128, num_layers2, batch_firstTrue, bidirectionalFalse, dropout0.3 ) self.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, num_classes) ) def forward(self, x): # x: (batch, seq_len, input_size) x x.permute(0, 2, 1) # - (batch, input_size, seq_len) x self.conv1(x) # - (batch, 64, seq_len) x x.permute(0, 2, 1) # - (batch, seq_len, 64) out, (h_n, c_n) self.lstm(x) # out: (batch, seq_len, hidden_size) # 取最后一层最后一个时间步的隐藏状态 last_hidden h_n[-1] # (batch, hidden_size) output self.fc(last_hidden) return output这段代码里最容易被绕晕的地方是维度。我第一次写也踩了坑。Conv1d的输入维度是(batch, channels, length)而LSTM如果设置了batch_firstTrue输入是(batch, seq_len, input_size)。所以卷积输出后要permute一下把序列长度换到中间维度。还有一个细节MaxPool1d(kernel_size2, stride1)不会改变长度太多但如果序列长度是20经过Conv1d后长度仍然是20因为padding1再池化后长度变成19LSTM处理19步没问题。如果长度特别短比如只有4池化核2会把长度压得很厉害可能只剩2信息就丢了。所以卷积核大小、池化核和序列长度这三个参数要放在一起调。3.3 处理变长序列的collate_fn上面代码默认所有样本序列长度都是20。但真实流量里有的流只有3个包有的流有几百个包。如果你不想截断可以用padding加mask。PyTorch中比较省事的方案是在Dataset里做截断或填充把长度统一。更优雅的方案是每个batch取该batch内的最大长度然后padding到一致配合pack_padded_sequence来让LSTM忽略padding部分。pack_padded_sequence的用法不算复杂但经常有人写错from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence def collate_fn(batch): sequences, labels zip(*batch) lengths torch.tensor([seq.shape[0] for seq in sequences]) padded torch.nn.utils.rnn.pad_sequence(sequences, batch_firstTrue) return padded, lengths, torch.tensor(labels)然后模型里先用CNN处理padded序列再根据lengths调整。不过如果你想快速跑通第一版我建议还是先用固定长度。等到后面优化时再上pack_padded_sequence否则处理不均匀长度会让排查问题变得复杂。4. 训练流程与性能分析4.1 损失函数与优化器选择网络流量检测本质是多分类问题。如果各类别样本均衡直接用nn.CrossEntropyLoss()。如果类别极不均衡比如正常流量占95%恶意流量只占5%可以给损失函数加权重或者用Focal Loss。最简单的方式weights torch.tensor([1.0, 5.0, 3.0]) # 根据类别样本比例倒数设置 criterion nn.CrossEntropyLoss(weightweights)优化器我习惯用AdamW设weight_decay1e-4。Adam虽然收敛快但泛化性偶尔不如SGD好在AdamW加了权重衰减修正在流量分类这种中等规模数据集上表现稳定。学习率初期可以设1e-3如果训练到一半loss不降了就降到1e-4。也可以直接用torch.optim.lr_scheduler.ReduceLROnPlateau自动降学习率。4.2 训练循环代码模板一个标准的训练循环大概长这样def train_epoch(model, loader, criterion, optimizer): model.train() total_loss, total_correct, total 0, 0, 0 for X, y in loader: X, y X.to(device), y.to(device) optimizer.zero_grad() logits model(X) loss criterion(logits, y) loss.backward() optimizer.step() total_loss loss.item() * len(y) total_correct (logits.argmax(1) y).sum().item() total len(y) return total_loss / total, total_correct / total def eval_epoch(model, loader, criterion): model.eval() with torch.no_grad(): total_loss, total_correct, total 0, 0, 0 for X, y in loader: X, y X.to(device), y.to(device) logits model(X) loss criterion(logits, y) total_loss loss.item() * len(y) total_correct (logits.argmax(1) y).sum().item() total len(y) return total_loss / total, total_correct / total训练的时候我建议每个epoch打印训练集和验证集的loss与准确率方便观察过拟合。如果验证loss先降后涨而训练loss还在降那就是过拟合信号需要增大dropout或减少模型层数。4.3 性能评估指标怎么选才靠谱很多人只报准确率这不够。网络流量检测里数据不均衡是常态准确率会被大类主导。比如正常流量占90%模型全预测正常也有90%准确率看着很高实际啥也没干。所以我至少会看四类指标精确率Precision、召回率Recall、F1-Score和混淆矩阵。对每个类别都算一遍重点关注恶意类别的F1。用sklearn直接算from sklearn.metrics import classification_report, confusion_matrix y_pred [] y_true [] model.eval() with torch.no_grad(): for X, y in test_loader: X X.to(device) logits model(X) y_pred.extend(logits.argmax(1).cpu().numpy()) y_true.extend(y.numpy()) print(classification_report(y_true, y_pred)) print(confusion_matrix(y_true, y_pred))如果某个恶意类别F1非常低可能原因有三类样本太少、特征表达不足、模型把该类别与其他混淆。这时需要回到数据层面做分析光调模型往往没用。4.4 我实验中观察到的性能对比这个项目里我拿公开数据集的一部分做测试大概是5万条样本、4分类正常、DDoS、端口扫描、暴力破解。输入序列长度取20每个包8个特征。基线是逻辑回归和随机森林深度模型分别是纯LSTM和CNN-LSTM。从实验结果看纯LSTM的准确率大约87%CNN-LSTM能到91%到92%。而在恶意类别上的F1提升更明显特别是DDoS类别CNN-LSTM比纯LSTM高5个百分点左右。这说明CNN先把每个包周围的局部模式抓出来后LSTM接收到的特征质量更高后续时序建模更有效。用表整理会看得更直观模型准确率宏平均F1DDoS F1逻辑回归81.2%0.760.72随机森林85.6%0.800.77纯LSTM87.3%0.820.82CNN-LSTM91.8%0.880.89这里需要说明具体数值取决于数据集划分和预处理方式不同数据子集结果会浮动。但趋势是稳定的CNN-LSTM比传统的LSTM和机器学习模型更好。5. 调参与优化经验5.1 关键超参数的影响这个项目里影响最大的几个超参数我按重要性排序序列长度seq_len太长会引入噪声和过多padding太短则丢失上下文。我试过10、20、40三种20的效果最好40虽然信息更多但训练明显变慢收益有限。卷积核大小kernel_size默认3适合观察相邻包的局部关系。如果调成5会覆盖更宽的包间局部模式。对流量特征来说3到5之间效果差距不大。LSTM隐藏层维度128到256之间比较合适。太小特征容量不够太大容易过拟合。学习率1e-3到2e-3起步配合ReduceLROnPlateau。学习率设太大loss会震荡不收敛设太小模型半天学不动。这些参数不要一起盲试。我的做法是先固定seq_len和batch_size手调学习率再调模型宽度最后再回去微调seq_len。这样每一步都有明确结论。5.2 过拟合的几个处理方法网络流量数据集规模普遍不大过拟合很常见。表现就是训练集acc接近100%测试集acc卡在某个值上不去。我常用几招加大Dropout尤其是LSTM输出到全连接之间的Dropout从0.3提到0.5给LSTM加dropout0.3但注意num_layers1时这个参数不生效数据增强。流量数据不太容易做经典图像增强但可以对序列做“时间抖动”随机把相邻包的时间间隔加一点小噪声或者随机mask掉少量包特征。这个在序列分类里是有用的早停。验证loss连续三个epoch不降就停止训练保存最优模型。5.3 处理类别不平衡如果你的数据集里正常流量远多于攻击流量模型会偏向多数类。除了给损失函数加权重还可以用torch.utils.data.WeightedRandomSampler对少数类过采样from torch.utils.data import WeightedRandomSampler class_weights 1.0 / torch.bincount(torch.tensor(y_train)) sample_weights class_weights[torch.tensor(y_train)] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size64, samplersampler)不过过采样要注意别把同一个流复制太多遍否则模型容易过拟合到少数样本的噪声上。6. 实操中踩过的坑与排查思路6.1 PyTorch加载模型时的weights_only坑PyTorch 2.6开始torch.load的weights_only默认值从False改成了True。如果你之前用的是老式保存方式torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch }, checkpoint.pth)加载时用老写法checkpoint torch.load(checkpoint.pth) # 2.6会报错就会因为weights_onlyTrue而报错。解决方法很简单加载时显式改成checkpoint torch.load(checkpoint.pth, weights_onlyFalse)或者保存时直接保存state_dict而不是整个字典里的对象。我一开始习惯保存整个优化器状态结果升级PyTorch后加载脚本挂了排查半天才反应过来。这块大家一定要小心。6.2 LSTM维度不匹配问题很多人第一次跑LSTM都会遇到维度错误。核心就是记住batch_firstTrue后输入是(batch, seq_len, input_size)输出是(batch, seq_len, hidden_size)。如果从CNN出来接LSTMCNN的输出要先permute不然LSTM会把channel当成seq_len去处理然后报错或者训练效果极差。调试维度问题的思路是在模型的forward里加几个print(x.shape)或者干脆把模型改成一行一列逐层打印。虽然PyTorch是动态图但调试器不支持在张量维度变化时自动提醒手动打印最有效。6.3 序列对齐与padding问题如果你的数据长度不一致用了padding但没处理padding部分LSTM会把这些填充的0也当成真实数据去建模等于模型学到一堆“空包”规律。解决方法是前面提到的pack_padded_sequence或者在预处理阶段就把长度统一为固定值并保证训练和测试一致。快速迭代时我建议先固定长度等稳定了再上pack。6.4 随机种子固定问题深度学习实验要可复现必须固定所有随机源。我在代码开头加import random, numpy as np, torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False如果不固定种子同一个模型跑两次结果可能差一两个百分点调参时根本没法判断是参数生效还是随机波动。固定种子之后性能对比才有意义。6.5 检查点保存与断点续训网络流量数据集虽然不大但模型训练也要时间。我习惯每个epoch结束都保存一次检查点保留验证集上效果最好的那份。保存用state_dict加载时严格匹配键名。如果是多GPU训练的模型加载到单GPU时记得先去掉module.前缀或者正确设置map_locationtorch.load(best_model.pth, map_locationcpu)6.6 可视化与异常样本分析调模型过程中我强烈建议把测试集的混淆矩阵和错误分类样本导出来重点看看哪些样本被分错。很多时候你会发现不是模型不够强而是标签标错了、特征提取逻辑有bug或者某个类别本身语义太模糊。比如端口扫描和网络扫描在很多数据集中天然重叠模型分错也正常。我自己在实际操作中的体会是CNN-LSTM这个框架并不复杂真正决定项目成败的往往是数据预处理规范和调试耐心。模型搭好后把时间花在分析错误样本和调特征上收益比反复改网络结构大得多。这套流程跑通之后再往更复杂的注意力机制、Transformer结构上迁移也顺理成章。本文还有配套的精品资源点击获取