资讯动态

基于CNN+LSTM的网络流量检测系统:从特征工程到模型调优的完整实战

发布时间:2026/10/1 1:08:37 来源:尧图企业网站定制
简介这是一份面向计算机相关专业学生的课程设计与期末大作业参考项目主题为基于CNN与LSTM融合模型的网络流量检测系统适合正在准备高分课设、需要项目实战练习的学习者。资源以Python源码形式提供完整覆盖数据加载、预处理、模型定义、训练测试与主流程入口等环节可帮助读者理解卷积神经网络提取流量空间特征、长短期记忆网络建模时序依赖的联合思路。压缩包共6个文件以5个py脚本和1个txt说明文档为主整体约6KB体量轻便便于快速阅读与二次修改。目前已有207人学习下载具备一定参考热度。读者可据此获得一套结构清晰的赛题实现方案掌握流量特征处理、模型搭建与训练评估的完整链路并借助说明文档梳理项目模块关系与运行逻辑为课设答辩或后续深度学习实践提供可复用的代码基础。1. 从一份课设压缩包说起CNNLSTM 做流量检测到底在解决什么拿到「基于CNNLSTM的网络流量检测系统python源码」这个题目的人多半是两种情况一是课设/毕设卡在选题想找一个既有深度学习含量、又能跑出漂亮指标的方向二是已经动手了但卡在特征怎么喂、模型怎么搭、指标怎么解释。网络流量检测这件事的本质是把连续的网络行为切成一帧一帧的样本判断它是正常还是异常而 CNNLSTM 这套组合拳解决的正是「单帧看不出来、要看一段时间的节奏」这类问题。单看一条流量记录端口、包长、协议这些字段谁都能读但端口扫描、DDoS、慢速爆破这类攻击的特征藏在时间维度里——短时间内大量半连接、固定间隔的心跳式请求、包长序列的周期性抖动。CNN 擅长从局部窗口里抽模式LSTM 擅长记住长距离的先后依赖两者串起来等于既看「这一小段长什么样」又看「这一小段在整条序列里处于什么位置」。这套思路在公开数据集上做二分类正常流量和攻击流量的区分度通常能到 95% 以上做多分类区分具体攻击类型会掉一些但仍是课设里拿得出手的结果。这篇文章面向的是要真正把这份源码跑起来、改得动、讲得清的人。我会按「数据怎么来 → 特征怎么造 → 模型怎么搭 → 怎么训怎么调 → 哪里最容易翻车」的顺序讲中间给可直接抄的代码和参数。你不需要有 GPU 集群一台带独显的笔记本或者实验室的入门卡就够CPU 也能跑只是慢。2. 数据与特征工程把 pcap 变成 CNNLSTM 能吃的张量2.1 公开数据集怎么选CICIDS 和 NSL-KDD 差在哪做流量检测第一步不是写模型是找数据。课设里最常用的两个来源是 NSL-KDD 和 CICIDS2017。NSL-KDD 是 KDD99 的清洗版每条记录是已经提取好的 41 维统计特征连接时长、协议类型、字节数、错误率等没有原始包好处是干净、小、跑得快坏处是太老很多现代攻击形态它没有而且特征已经被人嚼过一遍你很难讲出「我做了什么特征工程」。CICIDS2017 更贴近真实它同时提供 pcap 和用 CICFlowMeter 提取的 CSV每条流有 80 多列包含前向/后向包长统计、流间隔、标志位计数等。做 CNNLSTM 我一般推荐 CICIDS2017因为它的 CSV 天然带时间戳可以按时间排序后切成序列正好喂给 LSTM。NSL-KDD 没有可靠的时间顺序硬做序列是自欺欺人。选数据时注意一点CICIDS2017 的类别极度不平衡正常流量占八成以上DDoS、Bot 这类可能只有几百条。直接训练模型会偏向多数类后面第 5 章会讲怎么处理。2.2 从原始流到定长序列滑窗切片的三个参数CNNLSTM 的输入是一个三维张量(样本数, 时间步, 特征数)。CICIDS 的 CSV 是「一行一条流」要变成序列得先按时间排序再用滑动窗口把连续的 N 条流捆成一个样本。这里有三个参数必须自己定参数含义常见取值影响window_size一个样本包含多少条连续流10 / 20 / 50太小抓不到节奏太大显存吃紧且稀释局部模式stride窗口每次滑动几条1 / window_size/2步长小样本多但重叠高步长大样本少stepLSTM 展开的时间步等于 window_size一般与窗口一致也可下采样标签怎么定如果窗口内只要有一条攻击流就标为攻击召回高但误报多如果要求窗口内全是攻击才标攻击精确高但漏报多。课设里我一般用「窗口内攻击占比超过 50% 标攻击」折中且好解释。2.3 特征归一化与序列构造的可复现代码下面这段是把 CICIDS CSV 切成序列张量的核心逻辑直接可跑import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def build_sequences(csv_path, window_size20, stride10, attack_ratio0.5): df pd.read_csv(csv_path) # 去掉无穷值和缺失CICIDS 里 Flow Bytes/s 经常出现 inf df.replace([np.inf, -np.inf], np.nan, inplaceTrue) df.dropna(inplaceTrue) # 按时间戳排序保证序列的先后关系真实 df[Timestamp] pd.to_datetime(df[Timestamp], format%d/%m/%Y %H:%M:%S, errorscoerce) df df.sort_values(Timestamp).reset_index(dropTrue) label_col Label feature_cols [c for c in df.columns if c not in [Timestamp, label_col, Flow ID, Source IP, Destination IP]] X_raw df[feature_cols].values.astype(np.float32) y_raw (df[label_col].str.strip() ! BENIGN).astype(np.int32).values # 归一化必须在切窗之前做否则每个窗口的尺度不一致 scaler MinMaxScaler() X_scaled scaler.fit_transform(X_raw) X_seq, y_seq [], [] for start in range(0, len(X_scaled) - window_size 1, stride): end start window_size window_x X_scaled[start:end] window_y y_raw[start:end] X_seq.append(window_x) y_seq.append(1 if window_y.mean() attack_ratio else 0) return np.array(X_seq), np.array(y_seq), scaler, feature_cols逻辑说明先清洗 inf 和 NaNCICIDS 的速率类字段除以零会产生无穷值不处理会让 loss 直接变 NaN。时间戳排序是序列建模的前提很多人跳过这步结果 LSTM 学的是乱序。归一化放在切窗之前保证所有窗口共享同一套缩放参数如果每个窗口单独归一化模型会学到窗口内的相对关系而非全局尺度泛化会崩。参数说明window_size20是课设里的稳妥起点20 条流大约覆盖几十秒到几分钟的真实行为足够体现扫描或爆破的节奏。stride10让相邻窗口重叠一半既扩增样本又保留连续性。attack_ratio0.5是标签判定阈值想提高召回就降到 0.3想提高精确就升到 0.7这个值直接决定你论文里那张混淆矩阵长什么样。提示切完序列后先打印X_seq.shape正常应该是(样本数, 20, 特征数)。如果特征数超过 80建议先做一次方差过滤或相关性剔除否则 LSTM 第一层参数量会大得没必要。3. CNNLSTM 模型搭建从 Conv1d 到 LSTM 的维度对齐3.1 为什么是 CNN 在前、LSTM 在后顺序不能反。CNN 的作用是在时间轴上做局部卷积把相邻几条流的联合模式压成更高层的特征相当于给 LSTM 喂「已经提炼过的片段」。如果 LSTM 在前它要先在原始特征上记长依赖再让 CNN 去卷等于让 CNN 处理已经被 LSTM 混过的表示局部模式反而模糊了。常见做法是Conv1d → 池化 → LSTM → 全连接这也是这份课设源码里最可能采用的骨架。Conv1d 的卷积核在时间维滑动kernel_size3表示每次看连续 3 条流out_channels是提取多少种局部模式。池化用MaxPool1d(2)把时间步减半既降参又保留最强响应。LSTM 接在池化后的序列上hidden_size决定记忆容量最后取最后一个时间步的输出送全连接分类。3.2 PyTorch 实现与维度变化逐层核对import torch import torch.nn as nn class CNNLSTM(nn.Module): def __init__(self, n_features, n_classes2, conv_channels64, kernel_size3, hidden_size128, num_layers2, dropout0.3): super().__init__() # 输入 (batch, n_features, seq_len)Conv1d 要求通道在前 self.conv nn.Sequential( nn.Conv1d(n_features, conv_channels, kernel_size, paddingkernel_size // 2), nn.BatchNorm1d(conv_channels), nn.ReLU(), nn.MaxPool1d(2) ) self.lstm nn.LSTM( input_sizeconv_channels, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.classifier nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, n_classes) ) def forward(self, x): # x: (batch, seq_len, n_features) - 转成 (batch, n_features, seq_len) x x.permute(0, 2, 1) x self.conv(x) # (batch, conv_channels, seq_len//2) x x.permute(0, 2, 1) # (batch, seq_len//2, conv_channels) out, (h_n, c_n) self.lstm(x) last out[:, -1, :] # 取最后时间步 return self.classifier(last)逻辑说明permute是这份代码最容易出错的地方。Conv1d 要求输入是(batch, channels, length)而我们的序列张量是(batch, length, features)所以进卷积前转一次出卷积后再转回来给 LSTM。LSTM 设了batch_firstTrue输入输出都是(batch, seq, hidden)取out[:, -1, :]拿最后一步的隐状态做分类。参数说明conv_channels64是特征数的量级参考特征 80 左右时 64 够用特征少可以降到 32。kernel_size3覆盖 3 条流的局部窗口想抓更长的局部模式可以升到 5但要注意padding同步改成kernel_size//2保持长度。hidden_size128是课设的甜点值升到 256 指标提升有限但训练时间翻倍。num_layers2时 dropout 才生效单层 LSTM 加 dropout 是无效的这点很多人不知道。3.3 训练循环与类别不平衡的加权损失CICIDS 正常流量占大头直接交叉熵会让模型学会「全猜正常」也有高准确率。用pos_weight给攻击类加权from torch.utils.data import DataLoader, TensorDataset def train(model, X_train, y_train, epochs30, batch_size64, lr1e-3, devicecuda): model.to(device) loader DataLoader(TensorDataset(torch.tensor(X_train), torch.tensor(y_train)), batch_sizebatch_size, shuffleTrue) # 攻击类权重 正常样本数 / 攻击样本数 n_pos y_train.sum() n_neg len(y_train) - n_pos pos_weight torch.tensor([n_neg / max(n_pos, 1)], dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weighttorch.tensor([1.0, n_neg / max(n_pos, 1)], dtypetorch.float32).to(device)) optimizer torch.optim.Adam(model.parameters(), lrlr) for epoch in range(epochs): model.train() total_loss 0 for xb, yb in loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() logits model(xb) loss criterion(logits, yb) loss.backward() # 梯度裁剪LSTM 容易梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss {total_loss/len(loader):.4f})逻辑说明CrossEntropyLoss的weight参数按类别给权重攻击类权重设为正常/攻击的比例让模型不敢忽视少数类。clip_grad_norm_是 LSTM 训练的后悔药梯度超过 5 就裁掉能避免 loss 突然变 NaN。参数说明batch_size64在显存 6G 以上都稳显存小降到 32。lr1e-3是 Adam 的常规起点loss 震荡就降到 5e-4。epochs30配合早停验证集 loss 连续 5 轮不降就停别硬训到过拟合。4. 训练完怎么验证指标、混淆矩阵和推理脚本4.1 别只看准确率看召回和 F1流量检测里准确率是最会骗人的指标。正常流量占 80%模型全猜正常就有 80% 准确率但攻击一个没抓到。真正要看的是攻击类的召回率Recall和 F1。混淆矩阵里假阴性攻击被判正常是安全场景最不能接受的假阳性正常被判攻击只是多几条告警。课设答辩时老师最爱问「你这个模型漏报多少」提前把召回算清楚。from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, X_test, y_test, devicecuda): model.eval() with torch.no_grad(): logits model(torch.tensor(X_test).to(device)) preds logits.argmax(dim1).cpu().numpy() print(confusion_matrix(y_test, preds)) print(classification_report(y_test, preds, target_names[BENIGN, ATTACK]))逻辑说明model.eval()关掉 dropout 和 BN 的训练行为torch.no_grad()省显存。classification_report直接给出每类的 precision、recall、f1比手算省事。4.2 单条流量序列的推理封装课设演示时经常要现场喂一条数据看输出封装一个推理函数def predict_one(model, sequence, scaler, devicecuda): # sequence: (window_size, n_features) 的原始未归一化数据 model.eval() seq_scaled scaler.transform(sequence) tensor torch.tensor(seq_scaled, dtypetorch.float32).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1)[0] label ATTACK if prob[1] 0.5 else BENIGN return label, prob.cpu().numpy()逻辑说明推理时必须用训练时保存的同一个scaler重新 fit 会导致尺度不一致预测全乱。unsqueeze(0)补上 batch 维度。返回概率而不是硬标签方便你调阈值。参数说明阈值 0.5 可以调安全场景想少漏报就降到 0.3代价是误报增多。这个阈值和 2.3 节的attack_ratio是两回事前者是推理判定后者是训练标签生成。5. 避坑与排查这份源码跑不起来时先查这五处5.1 现象loss 一开始就是 NaN原因CICIDS 的Flow Bytes/s、Flow Packets/s存在除以零产生的 inf归一化后仍是 inf进网络直接污染梯度。解决在读 CSV 后立刻replace([np.inf, -np.inf], np.nan)再dropna别等到归一化之后才处理。5.2 现象训练准确率 99%测试准确率 60%原因切窗时用了stride1且没按时间划分训练测试集相邻窗口高度重叠训练集和测试集泄漏。解决按时间前 70% 做训练、后 30% 做测试或者切窗后按时间顺序划分绝不用随机划分。这是课设里最隐蔽的翻车点。5.3 现象LSTM 层报维度错误原因Conv1d 输出是(batch, channels, length)直接喂给batch_firstTrue的 LSTM 会把它当成(batch, seq, feature)维度对不上。解决卷积后加一次permute(0, 2, 1)把通道维换到最后一维。打印每层输出 shape 是最快的定位方式。5.4 现象显存不够batch 降到 8 还 OOM原因window_size或特征数太大LSTM 的参数量随hidden_size和时间步平方级增长。解决先把window_size从 50 降到 20再做特征选择把 80 维降到 30 维以内最后才考虑降hidden_size。顺序别反先动数据再动模型。5.5 现象多分类时某些攻击类 F1 为 0原因该类样本只有几十条模型根本没学到。解决要么合并相似类别把各种 DDoS 合成一类要么用重采样给少数类扩增要么在损失里给该类更高权重。课设做二分类最稳多分类要预留调参时间。6. 让指标再上一个台阶阈值搜索与特征消融的实操技巧模型搭完、能跑通之后真正拉开课设分数的是最后这 10% 的调优。我一般先做阈值搜索把推理阈值从 0.1 到 0.9 扫一遍画一条召回-精确曲线挑 F1 最高的点。很多人默认 0.5其实在类别不平衡时最优阈值经常在 0.3 附近。代码很短import numpy as np from sklearn.metrics import f1_score def search_threshold(model, X_val, y_val, devicecuda): model.eval() with torch.no_grad(): probs torch.softmax(model(torch.tensor(X_val).to(device)), dim1)[:, 1].cpu().numpy() best_t, best_f1 0.5, 0 for t in np.arange(0.1, 0.9, 0.05): preds (probs t).astype(int) f1 f1_score(y_val, preds) if f1 best_f1: best_f1, best_t f1, t return best_t, best_f1逻辑说明拿验证集不是测试集扫阈值避免在测试集上过拟合。返回的best_t写进推理脚本best_f1就是你论文里能写的最优结果。第二个技巧是特征消融。CICIDS 的 80 多列里很多是冗余的比如前向包长均值、最大值、最小值高度相关。我一般按方差和与标签的相关性排序取前 30 维重训一次对比指标。如果 30 维和 80 维差不多就用 30 维训练快一倍答辩时还能讲「我做了特征选择」。这一步用sklearn.feature_selection.SelectKBest几行就能做但一定要在切窗之前对原始流做选择别对序列做。最后一个习惯每次改完参数把window_size、stride、hidden_size、lr、最优阈值和对应 F1 记到一张表里。我踩过的最大坑就是调了十几轮之后忘了哪组参数最好只能重跑。这份源码值不值得深挖取决于你愿不愿意在跑通之后继续做这两步调优——只跑默认参数它就是个及格课设把阈值和特征消融做完它才是一个你能讲清楚每个决策理由的项目。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑