CNN+LSTM在线流量分类实战:从pcap预处理到时空神经网络落地
简介基于CNNLSTM时空神经网络的在线流量分类项目面向需要完成课程设计、期末大作业或网络流量分析实战的高校学生与研究人员。系统利用CNN提取流量空间特征、LSTM捕捉时序特征可在实时场景下区分正常业务、恶意软件与网络攻击流量并提供时序可视化测试集准确率93.5%。压缩包内共24个文件包含6个Python源码cnn、rnn、clstm分类器及数据处理与训练脚本、文本词表与参数文件、CSV数据集、模型检查点、PDF报告及DOCX使用手册等整体约23.7MB可下载后直接运行。已有192人学习。该素材涵盖完整可复现的模型训练链路、官方Spirent pcap数据解析方式、预测结果示例及项目文档既适合理解CNN与LSTM组合建模思路也可直接作为高分开题与答辩材料参考。1. 在线流量分类为什么绕不开 CNNLSTM把报文变成时序图像的思路转变做过在线流量分类的人应该都有同感传统特征工程版本最磨人的不是模型而是特征。包长均值、方差、到达时间间隔、标志位统计……每加一个特征就要重跑一遍全量数据换一个数据集又要从头调。这个基于 CNNLSTM 时空神经网络的在线流量分类项目走的是另一条路把原始流量表示直接喂给网络用卷积层抓局部空间模式用 LSTM 抓时序依赖端到端输出分类结果。它解决两个痛点——不再手工设计特征模型自己学表示同时保留时间维度的动态信息对加密流量和应用识别这类任务都够用。适合做网络安全课设、毕设或者想把深度学习落到实际流量识别场景的开发者。整套资源含源码、使用文档和 PDF 报告拿到手就能复现。2. 数据准备与预处理从 pcap 到能喂给网络的张量2.1 选数据集与格式约定公开流数据集的差异和取舍流量分类的数据集选型直接决定后面模型能学到什么。常见做法是先跑通 ISCX VPN-nonVPN再扩展到 CICIDS2017 或 UNSW-NB15。这三个数据集在形式上差异很大ISCX VPN-nonVPN 是加密隧道流量的二分类/多分类标注样本规模适中适合验证模型结构CICIDS2017 覆盖的攻击类型多但类别极不平衡对加权损失的要求高UNSW-NB15 直接提供统计特征而非原始 pcap省了预处理但对时序建模不友好——因为时间顺序已经在特征化过程中丢掉了。我一般建议课程设计和毕设优先用 pcap 原始报文数据集。原因很简单CNNLSTM 的优势就建立在原始序列上如果你用别人已经算好的统计特征时空神经网络的两个核心组件都发挥不出来退化成普通全连接网络那还不如直接用 XGBoost。选了原始报文数据集之后接下来要处理的问题就变成怎么把一段 pcap 切成干净、不重叠、标注可靠的样本。这份源码包里默认的数据预处理管线是标准的 pcap → 流 → 会话 → 序列样本的四步流程。流按五元组聚合会话则把双向流合并并按起始时间对齐。对齐的目的是保证同一个交互过程的正反两个方向的包落在同一个样本里LSTM 才能看到请求和响应之间的时序关系。2.2 流切分与滑动窗口五元组聚流、会话方向对齐、时间窗采样流切分的代码是整个预处理的第一道工序。下面是基于 dpkt 的解析脚本按五元组聚合 pcap 中的包只保留 TCP 和 UDP 流量。import dpkt from collections import defaultdict flows defaultdict(list) def split_flow(pcap_path): with open(pcap_path, rb) as f: for ts, buf in dpkt.pcap.Reader(f): eth dpkt.ethernet.Ethernet(buf) if not isinstance(eth.data, dpkt.ip.IP): continue ip eth.data proto ip.p src, dst ip.src, ip.dst if proto 6: # TCP tcp ip.data key (src, dst, tcp.sport, tcp.dport, proto) elif proto 17: # UDP udp ip.data key (src, dst, udp.sport, udp.dport, proto) else: continue flows[key].append({ts: ts, payload_len: len(ip.data)}) return flows这段代码的核心逻辑是按五元组(src, dst, sport, dport, proto)聚合同一个 TCP 或 UDP 连接的所有包只保留时间戳和有效载荷长度。payload_len 是后续 CNN 输入的关键特征因为应用层协议不同载荷长度分布差异非常明显——比如视频流的大包密集、DNS 的小包短促。聚合完成后每个 key 对应的列表就是一个原始的双向流。但双向流还不能直接进网络长度差异太大。一个交互式 SSH 流可能几千个包一个 DNS 查询只有两三个包。所以需要滑动窗口把流切成固定长度的序列样本。def build_sequences(flow, window_size64, step32): seqs [] for i in range(0, len(flow) - window_size 1, step): win flow[i:i window_size] seqs.append(win) return seqswindow_size 选 64、step 选 32 是经验值。64 个包能覆盖绝大多数应用的完整交互阶段比如 TLS 握手的 ClientHello 到 Finished大约 30 到 50 个包64 的窗口刚好能把这个阶段完整包住。step32 让相邻样本有 50% 重叠能增强训练样本量。如果你发现某个类别样本太少可以先把 step 降到 16 而不是直接复制样本——复制样本在数据增强上是无效的LSTM 学到的还是同样的时序模式只是增大了计算量。2.3 特征标准化与数据集划分时间顺序不能打乱滑动窗口切出来的样本是 64×1 的载荷长度序列但在线上环境里单纯靠载荷长度分类不够稳。代码包里通常还会叠加每个包的到达时间间隔、TCP 标志位组合、报文方向等作为辅助特征。拼起来之后每个时间步就是一个向量整个样本形状是 (64, feature_dim)。标准化这一步有个隐蔽的坑均值和标准差必须只用训练集计算然后应用到验证集和测试集。如果全量数据一起算验证集的信息已经渗透进训练过程评估指标会虚高。from sklearn.preprocessing import StandardScaler scaler StandardScaler() train_flat train_data.reshape(-1, train_data.shape[-1]) scaler.fit(train_flat) train_norm scaler.transform(train_flat).reshape(train_data.shape) val_norm scaler.transform(val_data.reshape(-1, val_data.shape[-1])).reshape(val_data.shape)注意这里 fit 只发生在 train_flat 上val 和 test 直接 transform。更严格的做法是连归一化参数都存下来线上推理时加载同一个 scaler。最后是数据集划分——按时间切分不要随机打乱。随机打乱会把同一个流前半段分到训练集、后半段分到验证集LSTM 等于提前看到了答案验证分数基本失真。按时间切分的意思是训练集取前 70% 时间段的流验证集取中间 15%测试集取最后 15%。3. 模型结构与参数选型CNN 提空间特征、LSTM 抓时序依赖的配合逻辑3.1 为什么先用 CNN 再做 LSTM空间局部特征到时序依赖的两级抽象流量序列有两个显著特点。第一存在大量局部模式TLS 握手阶段的 ClientHello 和 ServerHello 载荷大小相对固定视频流的连续大包、DNS 响应的连续小包都是可以在几十个时间步内观察到的局部结构。这类模式适合用一维卷积来提取卷积核滑动时只要看到这个局部形状就能激活对应特征而且平移不变——不管这个模式出现在流的开头还是中段卷积都能捕捉到。第二流量行为有全局时序依赖很多应用不是单靠某一小段的模式就能判别的要看整个交互过程中状态的演化。比如某个流量前面是正常的 HTTP 请求后面突然变成大量小包高频发送这种状态转移只有 LSTM 这类带记忆的单元能建模。CNN 的感受野有限堆再多卷积层也只能扩大局部视野没法真正记住几十个时间步之前的上下文。所以结构上的配合是CNN 先把每个局部窗口压缩成高维语义特征LSTM 在这个特征序列上建模长程依赖最后接全连接层输出类别概率。这就是这个项目里时空神经网络的含义——空间上由 CNN 负责时间上由 LSTM 负责。3.2 网络各层参数与形状变化序列样本怎么流过 Conv1d 和 LSTM以下是核心模型定义输入是形状为 (batch, seq_len, feature_dim) 的序列样本。import torch.nn as nn class FlowClassifier(nn.Module): def __init__(self, feature_dim12, seq_len64, hidden128, num_classes7): super().__init__() self.conv1 nn.Conv1d(feature_dim, 64, kernel_size3, padding1) self.conv2 nn.Conv1d(64, 128, kernel_size3, padding1) self.pool nn.MaxPool1d(2) self.lstm nn.LSTM(128, hidden, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden * 2, num_classes) def forward(self, x): x x.transpose(1, 2) # (B, T, F) - (B, F, T) x torch.relu(self.conv1(x)) x self.pool(x) x torch.relu(self.conv2(x)) x x.transpose(1, 2) # (B, F, T) - (B, T, F) out, _ self.lstm(x) # (B, T, 2*hidden) out out[:, -1, :] # 取最后一个时间步 out self.dropout(out) return self.fc(out)要注意的是 Conv1d 的输入布局。PyTorch 的 Conv1d 期望 (batch, channels, length)所以先 transpose 把特征维度换到通道位置。两层卷积的卷积核都是 3padding1 保证时序长度不缩减。MaxPool1d(2) 把 64 的时间步压成 32这一步很关键等于告诉 LSTM只需要看压缩后的关键片段不需要逐包记忆。LSTM 设置 bidirectionalTrue 是因为流量行为没有方向限制前向看请求到响应的推进后向看响应往回对请求的呼应。取最后一个时间步的输出向量再经全连接完成分类。参数配置建议参考下表。这些数值是这一场景下比较稳的基线配置不是最优解但按这个跑基本不会翻车。参数取值说明conv1 卷积核/通道kernel3, 64抓局部包模式conv2 卷积核/通道kernel3, 128扩大感受野MaxPool1d2时间维度压缩一半LSTM 隐藏层128, 双向共 256 维输出Dropout0.3防过拟合类别数7按实际数据集调整3.3 损失函数与训练配置加权交叉熵、AdamW、早停与学习率分类任务的损失函数用交叉熵但流量数据集普遍存在类别不平衡直接 softmax 交叉熵会让模型偏向样本量大的类。处理方法不是过采样而是计算每个类别的权重样本少的类别给更高的惩罚系数。import torch from torch.nn import CrossEntropyLoss class_counts torch.tensor([12000, 3500, 800, 200, 50]) # 按真实分布填 weights 1.0 / class_counts.float() weights weights / weights.sum() * len(class_counts) criterion CrossEntropyLoss(weightweights)权重计算的核心思想是让稀有类的每个样本在反向传播时贡献更大的梯度强制模型不能无视它们。训练配置上我一般用 AdamW学习率 1e-3配合 CosineAnnealingLR 衰减到 1e-5。早停 patience 设 5 轮监控验证集 F1 而不是 loss——loss 下降但 F1 不动说明模型在过拟合多数类。梯度裁剪 clip_grad_norm(1.0) 必须加尤其当序列长度大于 64 时LSTM 的梯度很容易爆炸。4. 在线流量分类的落地从离线训练到实时推理的工程细节4.1 离线与在线推理的差异流长度不定和类别漂移问题离线训练时样本是完整的、定长的。但线上流量是源源不断进来的你永远不知道当前这个流会持续多久也等不到它结束再分类——在线分类的实时性要求必须在流的前几十个包就给出判断。这就是离线评估和在线部署最大的差距离线指标再高如果推理逻辑处理不了不定长输入上线就是一个摆设。另一个容易被低估的问题是类别漂移。离线训练数据收集的是某一时段的流量分布上线后新协议、新应用版本出现某些类别的特征分布悄然变化。这个项目源码里有周期性评估脚本建议保留下来每周用最近三天的流量做一次快速验证看各类别的查准率是否掉出阈值。4.2 模型导出与增量预测TorchScript 导出和窗口缓冲分类器在线推理不能每次调用走完整的 Python 预处理链。常见做法是把训练好的模型用 TorchScript 或 ONNX 导出脱离原始模型定义直接运行省掉 PyTorch 的动态图解析开销。model.eval() scripted torch.jit.script(model) scripted.save(flow_classifier.pt)TorchScript 导出后模型变成一个自包含的计算图线上服务只需要加载这一个文件。但模型只解决给定固定长度输入、输出概率的问题在线流的不定长问题要靠序列缓冲器解决。class OnlineClassifier: def __init__(self, model_path, window_size64): self.model torch.jit.load(model_path) self.window_size window_size self.buffer [] def update(self, packet_vector): self.buffer.append(packet_vector) if len(self.buffer) self.window_size: x torch.tensor( self.buffer[-self.window_size:] ).unsqueeze(0) with torch.no_grad(): prob torch.softmax(self.model(x), dim1) return prob.squeeze(0).tolist() return None这个类的逻辑是每个新包到达就把特征向量追加进 buffer窗口满 64 时才输出一次预测并且只取 buffer 末尾的 64 个样本保证预测始终基于最新流量。每次预测完不清理 buffer而是继续追加——这样流中途的状态转移能被模型观察到。线上环境一般还会在 prob 上叠加置信度阈值比如 max(prob) 0.6 就标记为待定而不是强行给出类别避免在流开头就误判。4.3 评估流程混淆矩阵关注对角线之外的错误离线评估阶段除了总的准确率我至少会看三样东西混淆矩阵、逐类别的查准率/查全率/F1、按时间切分的分段指标。混淆矩阵能暴露哪两个类在互相混淆比如如果 P2P 流量经常被分到视频流看矩阵就知道是特征重叠还是标注问题。逐类别指标则用来发现稀有类是否被完全忽略——准确率 90% 时稀有类可能几乎全错。5. 避坑记录流量分类项目最容易翻车的五个环节5.1 数据侧的两个坑时间泄露与类别不平衡被无视第一个坑是时间泄露导致的虚假高分。现象是验证集 F1 能到 0.95部署上线后直接掉到 0.6。原因几乎都是随机打乱数据集时同一个会话的前半段和后半段分别落进训练集和测试集LSTM 在训练时已经见过测试样本的上文。解决方法是严格按时间边界切分流级别的切分保证同一条流的所有窗口样本只属于一个集合更严格的还会按源 IP 主机划分防止同一台主机的多个流串集。我从那以后每次做流量分类数据集划分代码第一行就是按时间排序第二行才是 split。第二个坑是类别不平衡被无视。现象是损失函数在下降但混淆矩阵里稀有类的召回率是 0。原因不是模型问题是 loss 里每个样本贡献相等多数类把梯度主导了。解决方法是给稀有类加权交叉熵或者用 Focal Loss 自动降权容易样本还有一个笨但有效的办法是稀有类对应的窗口 step 调小一点让它在数据里出现得更频繁但注意这只增加了样本量没有引入新的多样性。5.2 模型与训练侧的三个坑梯度异常、重叠评估、特征不一致第三个坑是 LSTM 梯度爆炸。现象是训练到第 20 轮左右 loss 突然变成 NaN。原因是序列长度较长时LSTM 时间步反传的梯度连乘后爆炸。解决方法是梯度裁剪 norm1.0同时检查数值稳定性如果 LSTM 层数超过两层优先减到两层深层堆叠在小数据集上收益极低还容易炸。第四个坑是评估用的滑动窗口重叠过高。现象是验证 F1 高于测试 F1而且越高越可疑。原因是为了增加训练样本step 设成 16窗口之间 75% 重叠相邻训练样本和验证样本几乎一样模型相当于见过测试数据的近似副本。解决方法是训练用重叠窗口评估时用 stepwindow_size 的不重叠窗口评估结果才接近真实分布。第五个坑是离线特征和在线特征不一致。现象是离线各类别指标都完美上线后模型对陌生流乱分。原因是离线用了整个完整流的统计特征比如流总字节数、流持续时间这些值在线推理时永远拿不到——你看到的是还没结束的流。解决方法是特征设计阶段就只保留前缀可得的特征当前窗口已观测到的包长均值、间隔均值、标志位计数而不是全流统计。项目源码里的特征配置文件就分成了 offline_only 和 online_safe 两组线上推理只用后者这是最值得参考的设计之一。6. 进阶验证用混淆矩阵和细粒度指标确认模型真的可用而不是看起来可用一个流量分类模型如果只在准确率上好看基本等于没做验证。我拿这个项目复现时会把测试集评估脚本拆成两部分整体指标和分段时间指标。整体指标用 classification_report 一次性输出每个类别的 precision、recall、F1分段指标把测试集按小时切片逐段跑观察指标随时间的波动。波动太大说明模型依赖了某段时间特有的模式而不是真正学到了流量的通用行为。from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred [], [] model.eval() with torch.no_grad(): for xb, yb in test_loader: pred model(xb).argmax(dim1) y_true.extend(yb.tolist()) y_pred.extend(pred.tolist()) print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, digits3))看混淆矩阵时注意一件事对角线之外最高的那个数字才是模型真实能力的边界。如果视频流有 15% 被分到 P2P不要急着调模型先回数据里看这两类流在负载长度分布上有多少重合——很多时候模型没错是标注和数据本身重叠太严重。按时间切片验证时如果某个时间段的所有类别指标整体下掉优先怀疑该时段出现了训练集没覆盖的新协议变种而不是模型退化。最后分享一个血泪经验。我早期做流量分类时评估集全部随机打乱拿到 0.93 的准确率上线第一天就被真实流量的长尾分布教做人。从那以后我每次跑这个项目都强制走一遍时间切分验证并且离线只统计前缀特征、评估只用不重叠窗口。这套流程虽然看起来多花几分钟但能拦住九成以上的虚假验证。如果你正准备做流量分类方向的高分项目这套源码包里的预处理管线、模型权重配置和 PDF 报告正好覆盖了从数据到在线推理的完整链路省掉的正是那些只有实际动手才会踩到的弯路。希望帮到你。本文还有配套的精品资源点击获取