
简介该项目是一套基于深度学习的加密流量识别模型源码融合LeNet、AlexNet与全局平均池化结构面向网络安全研究人员、算法工程师及流量分析学习者解决传统规则方法在面对加密流量时分类准确率不足的问题。压缩包共29个文件约73KB其中20个Python源文件覆盖数据预处理、训练与测试入口、模型定义、评估指标及通用工具函数另含配置文件、说明文档、版本控制忽略项和LICENSE证书目录结构按功能模块划分便于查阅和二次开发。目前已有638人学习适合希望从代码层面理解深度学习流量识别完整流程的读者。借助该源码可对比不同网络结构的建模思路掌握LeNet、AlexNet与GAP组合应用的实现细节并复现实验用于课题研究或安全产品原型验证也可作为网络安全课程设计或毕业设计的起点。1. 加密流量识别为什么把流量“画成图”给 CNN 看传统 DPI 在加密流量面前近乎失灵TLS 握手完成后全是密文正则表达式和固定特征很快失效。一个更实用的思路是——把一条流的负载字节排成矩阵当作灰度图喂给卷积网络让模型自己去找“加密流量的视觉纹理”。LeNet 负责捕捉局部字节结构AlexNet 风格分支看更大感受野GAP 全局平均池化把特征压成向量并压住参数量这三个结构融合在一起就是本标题要讲的核心。这个方案能解决的是在缺少明文协议特征时仅凭负载本身判断“这是不是加密流量、属于哪类加密协议”。适合正在做流量分类建模、想避免自己从零搭 CNN 踩坑的算法工程师和数据工程师。后面我会按落地顺序讲清楚pcap 怎么变成 28×28 矩阵、模型怎么搭、训练参数怎么设、哪些坑会让结果看起来很美却不可用。2. 把 pcap 变成 28×28 的“流量图”切流、填充与标准化脚本无论标题里写的模型结构多复杂第一步永远是数据形态问题pcap 是一堆报文CNN 吃的是矩阵。常见做法是先把报文按五元组聚合成“流”再把每条流的 TCP 负载拼成字节数组截断或填充到固定长度最后 reshape 成单通道灰度图。LeNet 经典输入是 28×28所以这里也把长度定为 784 字节。2.1 为什么要按流切分而不是按包切分如果按单个包做样本模型看到的只是碎片学不到“一次完整交互”的上下文。加密协议的特征往往体现在流的长度分布和前若干包的序列关系上单个包的信息量不够。更关键的是按包切分会在训练集和验证集之间造成严重泄漏同一条流的一半包进了训练集另一半进了验证集模型等于见过答案再考试。所以第一步必须按流聚合。流的标准定义是五元组源 IP、源端口、目的 IP、目的端口、传输层协议。双向流量建议合并成一条会话做法是把两端地址按字典序排好避免 A→B 和 B→A 被当成两条不同的流。这对加密流量识别尤其重要因为 TLS 握手本身是双向的客户端问候和服务端证书交换必须放在同一个样本里。2.2 用 dpkt 把 pcap 切流并转成矩阵dpkt 是处理离线 pcap 最轻量的库之一适合做实验级预处理。下面这段是常用的切流脚本骨架直接把每个五元组或双向会话的 TCP 负载累积起来再转成固定尺寸矩阵。注意它只负责产出数组标签需要你按数据组织方式单独维护比如“目录名即类别”或单独 CSV 记录流 ID 与标签。import dpkt import numpy as np import socket from collections import defaultdict def session_key(ip, tcp): # 双向会话把两端按字典序排好A-B 和 B-A 合并成同一条流 a (socket.inet_ntoa(ip.src), tcp.sport) b (socket.inet_ntoa(ip.dst), tcp.dport) if a b: return (a[0], a[1], b[0], b[1], ip.p) return (b[0], b[1], a[0], a[1], ip.p) def pcap_to_flow_matrices(pcap_path, target_len784, min_len64): flows defaultdict(list) with open(pcap_path, rb) as f: cap dpkt.pcap.Reader(f) for ts, buf in cap: try: eth dpkt.ethernet.Ethernet(buf) if not isinstance(eth.data, dpkt.ip.IP): continue ip eth.data if isinstance(ip.data, dpkt.tcp.TCP): tcp ip.data key session_key(ip, tcp) flows[key].extend(tcp.data) # 只累积 TCP 净载荷 except Exception: continue X [] flow_ids [] for key, payload in flows.items(): payload bytes(payload) if len(payload) min_len: continue # 太短的流没有统计意义 arr np.frombuffer(payload[:target_len], dtypenp.uint8).astype(np.float32) if arr.size target_len: arr np.pad(arr, (0, target_len - arr.size)) # 不足位补零 X.append(arr) flow_ids.append(key) X np.array(X).reshape(-1, 1, 28, 28) / 255.0 return X, flow_ids这段代码里三个参数是实践出来的target_len784对应 28×28 平方展开跟后面 LeNet 分支的输入尺寸正好对齐min_len64用来滤掉纯 ACK、握手失败的短流否则零填充占比过大模型会去学“填充区是零”而不是学真实负载/ 255.0把字节值归一化到 0~1灰度图的标准做法。dpkt.pcap.Reader只支持经典 pcap 格式如果抓包文件是 pcapng需要先用tshark -r input.pcapng -F pcap -w output.pcap转一次格式这个细节经常让人在数据准备阶段卡住。2.3 标准化、最短长度过滤与样本划分的细节输入归一化用“除以 255”还是“z-score”对流量图来说我一般优先用除以 255。因为灰度值的取值范围天然固定不需要在训练集上估计全局均值和方差z-score 会让原本的零填充变成负常数模型更容易识别出“这些位置是被填充的”相当于白送了一个不该有的捷径。如果你发现除以 255 后收敛太慢可以再加 InstanceNorm而不是全局 z-score。样本划分是另一个决定模型真实水平的地方。必须用 GroupShuffleSplit 按流 ID 切而不是直接train_test_split。下面这段是标准写法from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, val_idx next(gss.split(X, labels, groupsflow_ids)) X_train, X_val X[train_idx], X[val_idx] y_train, y_val labels[train_idx], labels[val_idx]groups参数传的是每条样本所属的流 ID五元组元组即可保证同一条流的样本不会同时落到训练和验证两边。random_state42固定后后续所有消融实验都能对比。还有一个被问很多次的问题是“流量图能不能做数据增强”——不建议做随机裁剪和水平翻转。水平翻转等于把包方向反过来会破坏时序语义随机裁剪可能裁掉 TLS 握手明文段造成模型学到“域名片段”而不是加密行为。提示pcap 文件很大时 dpkt 逐包解析会非常慢建议先按时间窗口或 IP 段把文件切小再并行跑预处理。3. 融合 LeNet、AlexNet 与 GAP 的 EncryptNetPyTorch 实现与逐层说明数据准备好之后就是模型结构。标题里的“融合”我一般理解为双分支设计LeNet 风格分支用大卷积核抓局部细节AlexNet 风格分支用更深的 3×3 卷积栈扩大感受野两条分支末端分别接 GAP把特征图压成向量再拼接进分类器。GAP 在标题里不是独立网络而是替代 Flatten全连接的关键层。3.1 模型架构为什么是“两个分支 一个全局池化”LeNet 的经典结构是 5×5 卷积加 2×2 池化感受野小擅长捕捉负载里连续几个字节的局部模式比如 TLS record header 的长度字段、TCP 分段边界附近的重复结构。AlexNet 经典版本更重但完整复刻它动辄上亿参数在流量数据集上必然过拟合。这里借用的是它的设计思路用更小的 3×3 卷积核、堆更多层、通道数逐层翻倍让网络能组合出跨位置的抽象模式——对密文来说就是学“字节分布呈现某种统计纹理”。两条分支的输出如果直接 Flatten 再拼全连接参数量会非常大。举例28×28 输入经过两次池化后特征图是 7×7LeNet 分支 32 通道就有 1568 维AlexNet 风格分支 128 通道则有 6272 维拼接后近 8000 维接全连接层就是几十万参数。GAP 的作用是把每个特征图平均成一个值输出维度等于通道数32128160参数量直接降一个数量级对中小规模流量数据集非常友好。3.2 EncryptNet 完整代码与 forward 流程下面是一个可直接跑通的双分支模型全部代码用 PyTorch 的nn.Module写成没有依赖任何第三方模型库。输入是 1×28×28 的灰度图输出是类别 logits。结构上保留了 GAP 的全局平均池化分类器只留了一个小型 MLP。import torch import torch.nn as nn class EncryptNet(nn.Module): def __init__(self, num_classes2, dropout0.3): super().__init__() # LeNet 分支5x5 卷积抓局部字节模式 self.lenet nn.Sequential( nn.Conv2d(1, 16, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 14x14 nn.Conv2d(16, 32, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 7x7 ) # AlexNet 风格分支3x3 卷积堆叠通道翻倍 self.alexnet nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 14x14 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 7x7 ) # GAP把每个特征图压成 1 个值 self.gap_lenet nn.AdaptiveAvgPool2d(1) # 输出 32 维 self.gap_alexnet nn.AdaptiveAvgPool2d(1) # 输出 128 维 self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(32 128, 64), nn.ReLU(inplaceTrue), nn.Dropout(dropout * 0.5), nn.Linear(64, num_classes), ) def forward(self, x): # 两条分支各自提取特征GAP 后拼接 f_lenet self.gap_lenet(self.lenet(x)).flatten(1) f_alex self.gap_alexnet(self.alexnet(x)).flatten(1) fusion torch.cat([f_lenet, f_alex], dim1) return self.classifier(fusion)forward 的逻辑很直接同一个输入分别过 LeNet 分支和 AlexNet 分支各自做完卷积和池化后进 GAPflatten(1)把 1×1×C 变成 C 维向量拼接成一个 160 维的融合向量再进分类器。两个分支的权重完全独立也就是说同一个输入会被两种不同感受野视角审视最后投票决定类别。AdaptiveAvgPool2d(1)的好处是不用手动算特征图尺寸。即使把输入从 28×28 换成 32×32这个模型也不需要改任何一行GAP 会自适应地把特征图收成 1×1。3.3 关键参数解读padding、dropout、GAP 维度这里把最有调整价值的参数列成一张表方便你在自己数据上调模块参数取值作用LeNet 分支kernel_size5, padding2保证卷积后尺寸不变池化负责降分辨率AlexNet 分支kernel_size3, padding1更深的堆叠组合感受野更大GAPAdaptiveAvgPool2d(1)输出维度等于通道数替代 Flatten 降参分类器dropout0.3中小数据集上防过拟合的第一道防线输入归一化/255固定到 0~1避免 z-score 让填充区变成负常数想进一步收紧模型时优先调的是两处一是 dropout 从 0.3 往上加流量数据集往往只有几万到几十万条流模型很快会过拟合二是 LeNet 分支第一层卷积核从 5×5 换成 3×3如果发现局部模式不需要那么大感受野换小核能省参。反之如果数据量很充足把 AlexNet 分支再加一个 3×3 卷积层通道从 128 继续翻到 256分类能力会再强一截。3.4 为什么不用全连接层做融合这是被问过最多的问题之一直接在 GAP 前把两个分支的特征图拼起来再 Flatten 不行吗当然行但参数量完全不同。特征图拼接后是 7×7×(32128) 的体量Flatten 后将近 8000 维接任何全连接层都会让模型容量暴增而 GAP 之后只有 160 维两者的参数规模差了几十上百倍。流量数据集通常只有几万条样本用全连接融合几乎必然过拟合。另一个原因是 GAP 让每条分支输出的语义更稳定。每个通道对应一个“某种模式有没有出现”的响应图GAP 取平均后变成一个全局置信度。两个分支各自置信度的拼接比原始像素级特征图的拼接更抗噪也更接近标题里“融合 LeNet、AlexNet 及 GAP 结构”的初衷——结构融合而不是特征图拼接。4. 训练配置与评估指标学习率、批次、类不平衡和验证策略模型搭完后面才是真正决定成败的部分。加密流量识别的训练配置和普通图像分类不太一样样本之间方差大、类别通常不平衡、训练集里混着长短不一的流。这些因素会让默认配置直接翻车所以优化器、损失函数和评估方式都得单独设计。4.1 优化器选择与学习率AdamW、warmup、梯度裁剪我一般用 AdamW 而不是裸 Adam它对 weight_decay 的处理更正确训练更稳。学习率从 1e-3 起步配合 OneCycleLR 做 warmup前 30% 步数把学习率从很低抬到峰值后面再降。加密流量样本的负载矩阵稀疏且数值方差大梯度很容易在某几个离群样本上爆炸所以梯度裁剪基本是必开的。import torch.optim as optim total_steps len(train_loader) * epochs optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.OneCycleLR( optimizer, max_lr1e-3, total_stepstotal_steps, pct_start0.3 ) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) for epoch in range(epochs): for x_batch, y_batch in train_loader: optimizer.zero_grad() logits model(x_batch) loss criterion(logits, y_batch) loss.backward() clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step()这里pct_start0.3的意思是 30% 的训练步数用来 warmup对流量数据足够了。max_norm1.0是把梯度范数截断到 1防止某个极端样本把参数顶飞。如果你的 loss 曲线在中期突然跳到很大的值又回落首先怀疑的不是模型结构而是学习率或梯度问题。如果你的显卡显存只有 4~6GB优先把 batch 降到 32 或 16而不是把输入从 28×28 降到 16×16。分辨率下降丢的是字节级局部信息对 LeNet 分支打击很大。训练环境的细节同样影响结果PyTorch 的 GPU 版和 CPU 版混装torch.cuda.is_available()显示 False 却能 import torch这是最常见的环境坑在 Ubuntu 20.04 服务器上配置深度学习环境时先装驱动再装 CUDA toolkit然后用 conda 装匹配 cudatoolkit 的 PyTorch GPU 版顺序反了基本都会出问题。4.2 类不平衡怎么处理加权交叉熵与 Focal Loss真实加密流量数据里类别分布极不均匀TLS 流量可能占了一半以上而某些协议只占 1% 不到。直接用标准交叉熵训练模型会倾向把所有样本都预测成多数类。第一步先给 CrossEntropyLoss 加类别权重权重按w_c N / (num_classes * N_c)计算少数类拿到更大的权重。class_weight torch.tensor([w0, w1], devicedevice) criterion nn.CrossEntropyLoss(weightclass_weight)如果加权交叉熵在少数类上召回率还是起不来就换 Focal Loss。它的核心是让模型更关注难样本也就是那些被分错的少数类样本。标准实现如下import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, gamma2.0, alpha0.25): super().__init__() self.gamma gamma self.alpha alpha def forward(self, logits, targets): ce F.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce) if isinstance(self.alpha, torch.Tensor): alpha_t self.alpha[targets] else: alpha_t self.alpha return (alpha_t * (1 - pt) ** self.gamma * ce).mean()Focal Loss 里gamma2.0让容易分类样本的 loss 贡献指数级下降alpha用来平衡正负类占比。实战建议是从gamma0.5开始扫不要直接上 2太强的聚焦会让模型在小数据集上训练不稳定。顺序上先跑加权交叉熵拿基线确认多数类不再主导后再尝试 Focal Loss不要一上来就换。4.3 评估加密流量模型为什么不能只看 accuracy类别不平衡的数据集里accuracy 是最骗人的指标。多数类占 90% 时模型哪怕把所有样本都预测成多数类accuracy 也有 90%。真正要看的是一组组合指标precision、recall、F1、AUC 和混淆矩阵。加密流量识别场景通常更在意召回率——漏掉一个加密流量意味着把它误当成了普通流量放行风险远高于误报。from sklearn.metrics import classification_report, confusion_matrix from sklearn.metrics import roc_auc_score, roc_curve probs torch.softmax(logits, dim1)[:, 1].detach().cpu().numpy() fpr, tpr, thresholds roc_curve(labels, probs) auc roc_auc_score(labels, probs) # 找一个 FPR 0.01 的阈值加密识别优先压误放 valid_idx np.where(fpr 0.01)[0] best_t thresholds[valid_idx[np.argmax(tpr[valid_idx])]] preds (probs best_t).astype(int) print(AUC:, auc) print(classification_report(labels, preds, target_names[non_enc, enc]))这段代码里的best_t是按业务约束选出的阈值。默认 0.5 的阈值在类别不平衡时几乎不可用真正上线时应该根据你 accept 的误报率反推。比如你在 FPR0.01 的约束下取最大 TPR模型输出的概率阈值可能不是 0.5 而是 0.7 或 0.9直接用默认阈值会把很多低置信度样本错误放行。4.4 环境准备GPU 版的深度学习环境配置要点在讨论调参之前先确认环境是对的。PyTorch 装成 CPU 版是新手最常摔的跟头训练速度慢一个数量级还让人误以为是模型算力不够。装完环境后先跑三行代码验证import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))torch.cuda.is_available()输出 False第一嫌疑是装了 CPU 版第二嫌疑是 CUDA 版本和 PyTorch 编译版本不匹配。先卸载重装对应 CUDA 版本的 PyTorch GPU 版再验证不要一上来就换模型结构。这个步骤能帮你省掉后面一半的玄学调参。5. 加密流量模型避坑实录五个高频问题与排查路径这一章是从实际训练里被反复打磨出来的踩坑记录按“现象、原因、解决”展开。每一个坑都真实发生在流量类 CNN 模型上而且都不是改一行代码能解决的得从数据组织和评估方式上处理。5.1 验证集精度虚高同一条流的包泄漏到了两个集合现象训练集准确率 95%验证集也 95%模型一拿到新环境的数据直接掉到 70% 以下。原因切分数据集时偷懒用了随机划分同一条 TCP 流的不同包被分进训练和验证两边。模型在训练时已经见过这条流的“前半段”验证时拿“后半段”来考等于开卷考试。流量识别模型的样本单位必须是流不是包。解决用 2.3 里的 GroupShuffleSplit 按流 ID 切分确保任意一条流的所有包只出现在一个集合里。如果数据是按 pcap 文件组织的还要注意同一个主机可能出现在多个文件中需要按流去重后再划分。5.2 模型记住域名而不是加密行为SNI 脱敏现象模型在训练集和验证集上表现很好换到一批没见过的协议流量时全面翻车。把热力图画出来后发现模型关注的区域集中在 TLS ClientHello 的明文段。原因TLS 握手的第一个包是明文的里面携带 SNI域名。模型根本不学“加密流量的统计纹理”而是记住了域名集合。这对加密流量识别任务是致命的——实际场景里域名可以随便换模型立刻失效。解决预处理时对 TCP 流的前几百字节做脱敏抹掉或随机掩码 ClientHello 里的 SNI 字段。常见做法是直接把每条流的前若干个握手包跳过不让明文域名进入负载矩阵。5.3 短流零填充导致模型学偏长度过滤与 mask现象训练 loss 降得很快验证集 loss 也降但在真实环境中对“只有几个包的流”预测结果完全不靠谱。原因大量短流不足 784 字节预处理时用零补齐。模型发现右下角大面积像素恒为 0于是学会“看到一片零就判断类别”而不是看真实的负载模式。零填充区域变成了一个稳定的伪特征。解决过滤掉长度低于 min_len64 字节的流如果不想丢数据可以在 GAP 层引入 mask让平均池化时忽略填充区域。但工程上更省事的做法是先过滤保证进入模型的每条流都有足够内容。5.4 loss 振荡、显存不稳定lr、batch 和归一化现象训练到第 10 个 epochloss 突然从 0.3 跳到 5然后又降回去或者 GPU 在训练中途报 OOM换小 batch 后依然偶发。原因学习率设置过高负载矩阵数值分布方差大BatchNorm 在小 batch 下统计量不稳定个别超长流样本经过多层卷积后产生极大梯度。流量数据的分布比图像数据更野默认图像训练配置直接搬过来经常出问题。解决学习率从 1e-3 往下调训练层数深时直接降到 3e-4加 clip_grad_norm 把梯度约束到 1.0如果用了 Norm优先考虑 InstanceNorm 而不是 BatchNorm。OOM 时先减 batch减到 16 还不行再减输入尺寸。5.5 全预测成多数类类别权重与 Focal Loss 的实际配置现象训练完成后看混淆矩阵只有对角线上一格有数字所有样本都被预测成数量最多的那个类F1 接近 0。原因标准交叉熵在极度不平衡的数据上会把少数类当噪声忽略加上学习率太低时模型直接躺在了多数类的局部最优上。解决先用类别权重让模型起点不偏权重按N / (num_classes * N_c)算少数类召回率如果还是 0换 Focal Loss 并调 alpha 偏向少数类。gamma 不要一上来就设 2从 0.5 和 1 开始扫找到模型在验证集上 F1 最高的组合。6. 从指标到落地消融验证、GAP 可视化和上线前的快速检查训练收敛不是终点真正要回答的是“这个融合结构到底值不值得用”。我的习惯是固定随机种子做消融实验用 GAP 热力图检查模型在看哪里最后拿短流和未知协议做回归测试。这三件事比调参更能说明问题。6.1 用固定随机种子做三模型消融要回答“融合到底有没有用”至少要比三组单 LeNetGAP、单 AlexNet 风格GAP、融合版。如果不固定随机种子两次训练之间的 random shuffle、dropout 初始化都会造成几分的波动让对比结果变成玄学。先写一个统一的种子设置函数import random import numpy as np import 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注意最后两行经常被漏掉。cudnn.deterministic True让卷积运算使用确定性算法benchmark False关闭自动选择最优算法。否则即使设了随机种子CNN 在 GPU 上跑出的结果也可能有细微差异。做完消融后看的是 F1 和 AUC通常融合版在小数据上优势不一定大但到样本量上来后会更稳。6.2 看一眼 GAP 前的特征热力图GAP 的额外红利是可视化方便。分类器第一层线性层的权重可以对应回 GAP 前的每个通道加权求和后得到一个粗略的类别响应图。实现可以简化到只取一层特征图feat model.lenet[2](x) # 取 LeNet 分支池化后的特征图 weights model.classifier[3].weight[:, :32] cam (feat * weights[cls].view(-1, 1, 1)).mean(dim0).detach().cpu().numpy()热力图高亮的区域如果集中在握手明文段要警惕模型在学 SNI如果集中在密文区域说明模型确实在学习加密负载的统计分布。这个检查只要十分钟能避免上线后才发现模型学错了特征。6.3 上线的最后一步用短流和未知协议做回归测试训练时过滤了短流不代表线上不会遇到短流。我一般会把测试集按“前 8 包、前 32 包、前 128 包”切成三份分别看模型的预测和行为。如果模型对前 8 包的预测开始抖动说明它依赖了长流才能看到的负载模式那么在只能基于前几个包做决策的生产场景里就要谨慎使用。另外找一份训练时完全没出现过的协议流量看模型默认把它判到哪一类——这个结果能暴露模型是否过拟合到训练协议的名字空间上。我之前做过一个加密流量分类项目最开始只盯着 accuracy模型一度漂亮到 97%后来做了 SNI 脱敏和按流切分准确率掉到 78%但换新环境时反而稳了。从那时起我把这三件事列为固定动作按流划分、脱敏数量少的协议、卡 FPR 选阈值。它们比网络结构上多一层少一层重要得多。希望帮到你。本文还有配套的精品资源点击获取