简介深度学习驱动的加密流量识别模型源码包融合LeNet、AlexNet与全局平均池化GAP结构面向网络安全研究人员、AI爱好者及流量分析开发者可直接用于加密流量分类、网络异常检测等场景模型设计兼顾准确率与效率。压缩包共29个文件其中20个Python源文件涵盖模型构建、数据预处理、训练验证、指标计算与实用工具等模块其余为配置文档、版本控制忽略文件、许可证及说明文本整体仅73KB轻量易部署。目前已有638人学习下载适合希望以深度学习提升网络安全管理能力的中高级开发者。项目代码按AlexNet、GAP等子网络清晰组织便于对比不同结构的融合效果同时附有数据划分与评估脚本读者可在此基础上扩展特征工程、调整网络参数或将其迁移至自己的流量识别项目中是理解深度网络在安全领域落地的实用参考资料。1. 加密流量识别为什么回头看 LeNet、AlexNet 和 GAP抓回来的流量里七成是 TLS深度包检测一看到加密载荷基本瞎火。这时候最务实的做法不是直接上大模型而是把流字节按顺序排成一张灰度图让 CNN 去学结构。这类方案里LeNet 负责轻量低层特征AlexNet 提供 ReLU、Dropout 这些训练稳定性组件全局平均池化GAP把全连接层压缩成一层线性输出三样拼起来就是一套可以离线训练、边缘推理的加密流量识别模型源码骨架。本文把这套源码拆开讲数据怎么变成图、模型怎么拼、参数怎么调、哪些坑必须躲。适合正在做流量分类或用深度学习做协议识别的工程师也适合拿公开加密流量数据集复现对比实验的入门者。2. 让原始流量变成模型能吃的“图”预处理 3 个关键决定直接把 pcap 喂给 CNN 是不可能的。绝大多数加密流量识别项目的第一步都是把网络流变成二维矩阵也就是灰度图。这里有两个容易搞反的点第一一张图只应该装一条双向流第二图的大小不是越大越好。2.1 流切分一张图只装一条双向流常见做法是用五元组分桶源 IP、源端口、目的 IP、目的端口、协议号。收到一个包先算五元组再往对应的 flow 里追加 payload。这里有个容易被忽略的细节五元组有方向性客户端发起连接和服务器返回响应的方向不同但同一个握手过程是双向的。TLS 握手里客户端随机数、服务器证书、密钥交换数据分散在两个方向所以分桶之后不能只保留“客户端到服务器”那半边必须把双向包按时间戳排序后拼成一条字节流。我习惯在每条流里给每个包前面加一个方向标记字节0 表示上行1 表示下行。这样模型至少有机会学到“哪个方向先出现什么字段”。如果你只是做粗粒度的应用类型分类不加方向也能跑但如果要判断恶意加密流量方向标记往往比多一层卷积更有用。流切分的另一个关键是超时。同一个五元组如果间隔超过 30 秒通常会重新建连或复用连接这时候还硬拼在同一条流里一张图就会混入两条不同会话的内容特征被平均掉。所以我一般用“五元组 最后包时间戳”作为分桶状态超过超时阈值就开新 flow。2.2 图像尺寸64×64 是比 32×32 和 224×224 更稳的折中加密流量转图像时输入尺寸直接决定模型能看到多少信息。LeNet 原始设计是 32×32AlexNet 用 224×224但流量图不是 ImageNet不是越大越准。一张 64×64 的灰度图等于 4096 字节。如果每个包截取 128 字节那么 32 个包正好填满。64×64 既能覆盖大多数 TLS 握手的头部区域又不会让 padding 比例高到离谱。32×32 只有 1024 字节通常只够看到 ClientHello 和 ServerHello信息太少224×224 需要 50176 字节很多短流根本填不满大批量补零反而制造虚假特征。输入尺寸字节量模型结构实际建议32×321024很轻只适合快速基线64×644096LeNetAlexNet 混合最推荐96×969216较深 AlexNet 风格数据量大时再试224×22450176重模型大多数场景没必要我一般建议先固定单包截断 128 字节、固定 32 个包生成 64×64 图。这个配置在抓包、内存开销和模型输入之间比较平衡。2.3 预处理代码从 pcap 到灰度图的 20 行核心逻辑下面这段代码把 Scapy 读到的 pcap 转成训练用的张量。生产环境可以用 dpkt 或 C 解析器提速但思路完全一致。import numpy as np import torch from scapy.all import rdpcap def get_l4_payload(pkt): # 只取 TCP/UDP 层之后的载荷不含以太网和 IP 头 if pkt.haslayer(TCP): return bytes(pkt[TCP].payload) if pkt.haslayer(UDP): return bytes(pkt[UDP].payload) return b def canonical_flow_key(pkt): # 五个元组统一排序保证双向包进同一个桶 ip pkt[IP] if pkt.haslayer(IP) else None if pkt.haslayer(TCP): l4, proto pkt[TCP], TCP elif pkt.haslayer(UDP): l4, proto pkt[UDP], UDP else: return None src (ip.src, l4.sport) dst (ip.dst, l4.dport) key (src[0], src[1], dst[0], dst[1], proto) if src dst: return key return (dst[0], dst[1], src[0], src[1], proto) def flow_to_tensor(pkts, width64, height64, max_packets32, per_packet128): raw bytearray() for pkt in pkts[:max_packets]: raw get_l4_payload(pkt)[:per_packet] if len(raw) width * height: break # 不足部分补零保证形状固定 if len(raw) width * height: raw b\x00 * (width * height - len(raw)) arr np.frombuffer(raw, dtypenp.uint8).reshape(height, width) return torch.from_numpy(arr.astype(np.float32) / 255.0).unsqueeze(0)代码逻辑是先按五元组分桶分桶内的包按时间排序再逐包截取 L4 层载荷前 4096 字节作为灰度图的像素值。reshape(height, width)必须保证字节数正好等于 64×64所以先用len(raw) width * height截断再用补零兜底。返回结果直接是[1, 64, 64]张量不落盘 PNG训练时不用反复做 IO。这里最需要警惕的是补零。补零区域在训练数据里几乎恒定模型很容易把它当背景特征学进去。后面避坑章节会专门讲这个问题。3. 融合 LeNet、AlexNet、GAP 的模型源码结构设计与参数量模型部分是整套源码的核心。题目里说的 LeNet、AlexNet、GAP不是把三个网络并联而是把结构单元按阶段组合起来。先讲清楚为什么这样拼再贴完整源码。3.1 GAP 的作用参数砍半特征更稳传统 LeNet 和 AlexNet 的末尾是展平加全连接。特征图如果是 8×8×128展平后是 8192 维再接一层全连接到分类数参数量立刻上来。加密流量数据集往往只有几千条流这种参数量很容易过拟合。GAP 的做法是每一张特征图直接做全局平均池化变成 1 个标量。128 张特征图池化后就是 128 维向量再送进分类器。这样分类器的权重从 8192×num_classes 变成 128×num_classes参数量下降一个数量级。另一个附带好处是GAP 之后每个类别的分类权重可以直接和特征图相乘生成类似热力图的解释结果。这一点最后一章会用到。3.2 LeNet 和 AlexNet 的结构单元怎么融合LeNet 给我留下最深印象的是 5×5 卷积核和逐级下采样。前两阶段用 5×5 卷积在输入尺寸还比较大的时候捕获局部结构AlexNet 的价值在于 ReLU、Dropout 和多个 3×3 卷积堆叠。融合不是简单抄名字而是按阶段取长补短先复刻 LeNet 的低层结构1 通道输入Conv1 用 5×5配合 BatchNorm 和 MaxPool再复刻 AlexNet 的高层结构后面叠两层 3×3 卷积加深抽象特征最后接 MaxPool 把特征图压到 8×8。在分类头之前插入 GAP。整体输入是 64×64三层池化后特征图尺寸正好是 8×8信息量足够又不至于太大。很多同学会直接调torchvision.models.alexnet(pretrainedTrue)我一般不建议。ImageNet 预训练权重期望 3 通道自然图像而流量图是单通道字节矩阵数值分布完全不一样迁移过来的收益远不如随机初始化加好好调参。3.3 源码实现与参数核对下面是一份可以直接跑的 PyTorch 模型源码。import torch import torch.nn as nn class EncryptedFlowNet(nn.Module): def __init__(self, num_classes12, dropout0.3): super().__init__() self.features nn.Sequential( # LeNet 风格5x5 卷积逐级下采样 nn.Conv2d(1, 32, kernel_size5, padding2), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size5, padding2), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # AlexNet 风格3x3 卷积堆叠加深特征 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) # GAP每张特征图收缩到 1 个标量 self.gap nn.AdaptiveAvgPool2d((1, 1)) self.dropout nn.Dropout(pdropout) self.fc nn.Linear(128, num_classes) def forward(self, x, need_feature_mapFalse): fmaps self.features(x) # [B, 128, 8, 8] pooled self.gap(fmaps) # [B, 128, 1, 1] flat torch.flatten(pooled, 1) # [B, 128] logits self.fc(self.dropout(flat)) if need_feature_map: return logits, fmaps return logits代码里need_feature_mapTrue是为了让 CAM 热力图拿到池化前的特征图。如果不需要可解释性直接返回 logits 即可。各层输出尺寸和参数量如下表模块输出尺寸说明Conv1 BN ReLU MaxPool32×32×325×5 卷积学习局部字节组合Conv2 BN ReLU MaxPool16×16×645×5 卷积加深通道Conv3 Conv4 BN ReLU16×16×1283×3 卷积堆叠AlexNet 风格MaxPool8×8×128下采样到 8×8GAP128每张特征图取均值Dropout FCnum_classes128 维到分类数总参数量大约在 0.3M 左右比 AlexNet 原始版本小一个量级。对加密流量这种小样本任务这个容量是合理的。如果数据量特别大可以再加一组 3×3 卷积如果数据量只有几百条流建议把第一层通道降到 16进一步压缩模型。4. 训练与验证小样本加密流量分类最容易翻车的 3 个点模型结构定下来之后训练策略直接决定能不能复现出一个能用的结果。加密流量分类的样本量通常不大类别分布也通常不均匀这一章讲三个最关键的调试点。4.1 类别权重先解决不平衡再谈准确率公开加密流量数据集里视频、网页、文件传输这些类别的样本数量往往差很多。如果不做任何处理模型会把多数类学得很好少数类全被牺牲。最简单的方法是给 CrossEntropyLoss 传入类别权重。import torch import torch.nn as nn counts torch.bincount(labels) # 每一类样本数 weights 1.0 / counts.float() weights weights / weights.mean() # 归一化到均值 1 附近 weights weights.clamp(max10.0) # 防止少数类权重过大 criterion nn.CrossEntropyLoss(weightweights.to(device))这段代码的含义很直接样本越少的类别损失权重越高。归一化到均值 1 附近是为了让整体损失量级和普通交叉熵接近不至于因为权重过大把梯度带偏。clamp是防止极端不平衡下某个类别的权重变成几十导致训练震荡。如果你同时做应用类型分类和恶意流量检测两个任务的 loss 直接相加很容易被大 loss 任务主导。常见做法是先用loss loss_cls 0.5 * loss_mal起步再根据各自收敛速度调整比例。多任务里调整多个 loss 之间的比例是个单独话题但不要一上来就上复杂的不确定性加权先用手调系数跑两版再说。4.2 学习率、L2 正则、batch size 的默认值加密流量识别不是大模型预训练学习率一般不用太大。我常用的默认值是参数默认值理由学习率1e-4小样本下 1e-3 容易过拟合batch size32太小 BN 不稳定太大收敛慢weight_decay1e-4等效 L2 正则压住多余参数优化器Adam对流数据这类非平滑梯度比较省心早停耐心值8防止在小数据集上硬跑满 epochweight_decay在 PyTorch 里直接写在优化器里就是 L2 正则。这里多说一句BatchNorm 层有自己的缩放参数L2 作用在卷积核上比作用在 BN 缩放系数上更安全所以一般不对全模型统一加太大 weight_decay1e-4 是个稳妥起点。4.3 训练、早停与宏平均 F1 的验证骨架只盯准确率在小样本分类里是典型翻车姿势。比如某个类占 90%模型全猜这个类准确率也有 90%但实际上一点用都没有。我一般用宏平均 F1 作为主指标再输出混淆矩阵看每个类的表现。from sklearn.metrics import f1_score def evaluate(model, loader): model.eval() preds, trues [], [] with torch.no_grad(): for x, y in loader: logits model(x) preds.extend(logits.argmax(1).cpu().tolist()) trues.extend(y.cpu().tolist()) return f1_score(trues, preds, averagemacro) model EncryptedFlowNet(num_classesnum_classes) optimizer torch.optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.5, patience3 ) best_f1, patience 0.0, 0 for epoch in range(50): model.train() for x, y in train_loader: optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() val_f1 evaluate(model, val_loader) scheduler.step(val_f1) if val_f1 best_f1: best_f1 val_f1 patience 0 torch.save(model.state_dict(), best_encflow.pt) else: patience 1 if patience 8: break训练循环里做了三件重要的事梯度裁剪、ReduceLROnPlateau、早停。梯度裁剪控制在 5.0可以避免个别异常样本把参数更新推飞学习率随验证 F1 停滞减半早停兜底防止过拟合。验证 F1 上升时才保存模型这是最朴素也最有效的后悔药。5. 加密流量识别实战避坑5 个值得写进笔记的问题这一章是血泪经验。很多加密流量识别模型训练时指标不错一换环境就崩问题往往不在模型结构而在数据处理和验证方式。5.1 按包切分数据集造成的信息泄漏现象训练准确率和验证准确率都接近 95%但拿到另一天抓的流量一试掉到 60%。原因数据按包而不是按流划分。同一条 TLS 流的握手包既出现在训练集又出现在验证集模型等于提前看到了同一个会话的上下文验证指标虚高。解决先按五元组分桶再按流 ID 划分训练集和验证集保证同一条流的所有包只进一个集合。flow_ids list(flows.keys()) train_ids set(np.random.choice( flow_ids, sizeint(len(flow_ids) * 0.8), replaceFalse )) val_ids set(flow_ids) - train_ids这个泄漏问题在公开数据集上尤其隐蔽因为很多数据集直接给的是包不给流标签。拿到数据第一件事应该是重建会话而不是直接拆包。5.2 一条流被拆成多张图或者多张图合成一张现象模型对某些类别的分类极不稳定同一个应用前后两次预测结果不一致。原因五元组在长连接或连接复用场景下没有边界。有人把 30 分钟的 TCP 长连接全部塞进一张图也有人因为中间某个包超过超时阈值就错误地拆成了多条流。解决定义空闲超时。一般 TLS 会话空闲超过 30 秒就认为连接已失效后续同五元组流量应该开新 flow。同时采样时固定包数只取每条流的前 32 个包这样既覆盖握手阶段又避免长连接把图片撑爆。5.3 加了 GAP 后收敛变慢不一定是结构的问题现象同样的骨干网络把全连接换成 GAP 后loss 下降变慢甚至卡在某个平台期不动。原因GAP 把所有空间位置的激活值做平均梯度回传时被均摊稀释尤其是最后一层卷积没有接 BatchNorm 的时候特征分布不稳定池化后信号被磨平。解决在最后一组卷积后加 BatchNorm 和 ReLU确保 GAP 输入的特征分布在一个稳定的尺度上。还有一个细节是初始化用 Kaiming 初始化卷积层避免某些通道一开始就是全 0 激活导致 GAP 输出为 0梯度直接消失。这个坑也说明一个道理手写网络不能当黑匣子每换一层结构都要看中间特征图的均值和方差。5.4 零填充成了“假特征”跨集泛化直接崩现象训练集和验证集都是同一个数据集里切出来的F1 不错换到另一个时间窗口重新抓的流量指标掉了 10 个点以上。原因短流填不满 4096 字节右侧和下侧大片全零区域。模型学到的不完全是流量特征而是“哪里是全零”这种背景特征。换一批流量后不同样本的 padding 区域形状变了模型自然失灵。解决先看训练集里每条流平均有效字节数。如果很多样本有效字节低于 204864×64 可能偏大缩到 48×48 或 32×32 更稳妥如果数据充足直接丢弃 padding 占比过高的样本让模型专注在真实字节区域上。5.5 训练数据时间太旧模型学到的是旧版本 TLS 的皮现象用半年前抓的 TLS 1.2 流量训练线上环境已经切换到 TLS 1.3准确率明显下滑。原因TLS 版本升级后握手包结构变化很大。模型如果过度依赖旧版本里的某个固定字段比如记录头类型、版本号、随机数长度新版本一替换就失效。解决训练数据里混合不同 TLS 版本如果只能拿到单一版本尽量把特征重心从“固定字段精确值”挪到“长度分布和时序模式”上这类特征跨版本更稳定。做恶意加密流量检测时也不建议只依赖握手头几个字节还要结合流长度、包间隔这些统计特征。6. 用 GAP 自带的可解释性做热力图验证模型到底在看什么最后一章给一个验证技巧用 GAP 做类别热力图把模型的黑匣子打开看一眼。这个技巧在流量分类里非常实用因为模型学到的特征到底合不合理不能只看指标。6.1 生成类别热力图10 行代码因为分类头是fcGAP 之后的每一维对应一个特征图通道的平均值。想反推“模型关注哪个区域”只需要把类别权重和池化前的特征图相乘。import torch.nn.functional as F import numpy as np def show_cam(model, x, class_idx0): model.eval() logits, fmaps model(x.unsqueeze(0), need_feature_mapTrue) fmap fmaps[0] # [128, 8, 8] weight model.fc.weight[class_idx] # [128] cam torch.einsum(c,hw-hw, weight, fmap.flatten(1)) cam cam.reshape(fmap.shape[1], fmap.shape[2]) cam F.relu(cam) # 只保留正向激活 cam F.interpolate( cam.unsqueeze(0).unsqueeze(0), size(64, 64), modebilinear, align_cornersFalse ) return cam.squeeze().detach().cpu().numpy()这段代码的关键在torch.einsum(c,hw-hw)把每个通道的权重乘到对应通道的特征图上再按通道累加。由于 GAP 的存在这个权重意义很直接就是“这个通道对某个类别的影响力”。F.relu 是为了忽略负激活因为负激活通常代表抑制区域热力图上只看正向贡献。6.2 三种典型的热力图结论怎么判断模型是否可用拿到热力图后不要急着调参先判断模型在看什么。第一种热力区集中在图像左上角或头部区域这是比较理想的情况说明模型学到的是 TLS 握手前段的协议字段。第二种热力点分散在整张图但没有明显规律说明模型可能在依靠长度、密文分布这类统计特征这种情况下需要额外检查跨数据集泛化能力。第三种热力区集中在补零区域这种模型不能上线因为它把 padding 当特征了回到避坑章节的 5.4 继续处理。我现在每次训练完都会先抽 20 个验证样本把热力图打出来用眼睛扫一遍再做结论。这个习惯帮我挡掉至少两次“测试集准确率 92% 但线上完全不可用”的翻车。说白了GAP 给的可解释性就是后悔药不看白不看。希望帮到你。本文还有配套的精品资源点击获取