简介这份资源是面向高校学生与深度学习初学者的网络流量检测课程设计完整方案基于 Python 实现采用 CNN 与 LSTM 组合模型完成流量特征提取与分类识别适合用作课程设计、毕业设计参考或网络安全方向的入门实战练习。压缩包共 6 个文件包含 5 个 py 源码与 1 个 md 说明文档整体约 5KB源码覆盖数据预处理、模型构建、训练与测试等核心环节文档则交代了项目背景、实现思路、代码结构与结果分析。目前已有 306 人学习下载说明该方案在同类课设中具有一定参考价值。读者可据此理清 CNNLSTM 混合模型在流量识别任务中的搭建流程理解数据加载与特征处理方式并借助文档快速定位各模块职责减少从零摸索的时间成本也便于在此基础上替换数据集或调整网络结构开展二次实验。1. 从一份课程设计说起CNNLSTM 做网络流量检测到底靠不靠谱很多做安全方向课程设计的人第一次拿到「网络流量检测」这个题目时第一反应是上规则匹配比如 Snort 那种特征串比对。但规则库更新永远慢半拍遇到变种流量就抓瞎。于是越来越多的人转向深度学习方案其中 CNNLSTM 混合模型是出现频率最高的组合之一。原因很直接网络流量本质上是带时序的序列数据每个流由一串带统计特征的数据包组成CNN 擅长从单条流里提取局部特征模式LSTM 擅长捕捉流与流之间的时序依赖两者串起来刚好覆盖「单流特征 跨流时序」两个维度。这套方案适合谁如果你正在做网络安全、入侵检测方向的课程设计或毕业设计手头有 CICIDS2017、NSL-KDD 这类公开数据集想跑通一个从数据预处理到模型训练再到检测输出的完整链路那 CNNLSTM 是一个性价比很高的选择。它不需要 GPU 集群一张消费级显卡甚至 CPU 都能跑起来代码量可控论文里也好写创新点。但要注意课程设计级别的方案和工业级部署之间差距很大后面会具体讲边界在哪。2. 数据从哪来、怎么洗流量特征工程的四个关键决策2.1 数据集选型CICIDS2017 和 NSL-KDD 的取舍做网络流量检测第一步不是写模型是找数据。目前公开数据集里用得最多的是 CICIDS2017 和 NSL-KDD。NSL-KDD 是老牌数据集样本量小、特征维度低41 维跑起来快适合快速验证模型结构是否 work。但它的问题是年代久远攻击类型和现代网络环境脱节严重拿它跑出来的准确率再高放到真实场景里也没有参考价值。CICIDS2017 是加拿大网络安全研究所发布的包含正常流量和多种攻击流量DDoS、PortScan、Web Attack、Brute Force 等特征维度 78 维样本量百万级。它的优势是流量采集时间近、攻击类型全、特征工程已经做得比较到位。缺点是类别极度不平衡正常流量占绝大多数少数攻击类样本可能只有几百条。我一般建议如果只是跑通流程用 NSL-KDD如果要写论文、做对比实验用 CICIDS2017但必须处理类别不平衡问题。提示CICIDS2017 原始数据是 pcap 格式需要用 CICFlowMeter 提取特征生成 CSV。网上能找到已经提取好的 CSV 版本省去这一步。2.2 特征清洗无穷值、缺失值和常数列怎么处理拿到 CSV 之后第一件事是检查数据质量。CICIDS2017 有几个经典坑某些特征列存在 Infinity 和 NaN 值原因是流量计算时出现了除以零的情况还有一些列在所有样本中取值相同属于无效特征。下面是我常用的清洗流程import pandas as pd import numpy as np def clean_data(df): # 1. 替换无穷值为 NaN df.replace([np.inf, -np.inf], np.nan, inplaceTrue) # 2. 删除包含 NaN 的行也可以选择填充但流量特征填充容易引入噪声 df.dropna(inplaceTrue) # 3. 删除常数列方差为 0 的特征对模型没有贡献 nunique df.nunique() constant_cols nunique[nunique 1].index.tolist() df.drop(columnsconstant_cols, inplaceTrue) # 4. 去除重复行 df.drop_duplicates(inplaceTrue) # 5. 重置索引 df.reset_index(dropTrue, inplaceTrue) print(f清洗后剩余特征数: {df.shape[1]}, 样本数: {df.shape[0]}) return df这段代码的逻辑很直白无穷值本质上是计算异常不能直接当有效值用常数列对分类没有信息增益留着只会增加计算量重复行会导致训练集和测试集泄露必须去掉。参数方面dropna的how默认是any也就是只要有一个特征缺失就删整行。如果你的数据缺失率很高可以考虑用中位数填充但流量特征里缺失往往意味着采集异常删掉更安全。2.3 标签编码与类别不平衡处理CICIDS2017 的标签列是字符串类型的攻击名称需要转成数值。同时正常流量和攻击流量的比例可能达到 10:1 甚至更高直接训练会导致模型偏向多数类。常见做法有两种欠采样从多数类中随机抽取与少数类相当的数量和过采样用 SMOTE 生成少数类合成样本。课程设计里我一般用欠采样因为实现简单、训练快。from sklearn.preprocessing import LabelEncoder from sklearn.utils import resample def balance_data(df, label_colLabel): # 标签编码 le LabelEncoder() df[label_encoded] le.fit_transform(df[label_col]) # 分离多数类和少数类 df_majority df[df[label_encoded] 0] # 假设 0 是 BENIGN df_minority df[df[label_encoded] ! 0] # 欠采样多数类降到少数类样本数的 3 倍不用 1:1保留一些真实分布信息 n_minority len(df_minority) df_majority_downsampled resample( df_majority, replaceFalse, n_samplesmin(len(df_majority), n_minority * 3), random_state42 ) df_balanced pd.concat([df_majority_downsampled, df_minority]) df_balanced df_balanced.sample(frac1, random_state42).reset_index(dropTrue) print(f平衡后标签分布:\n{df_balanced[label_encoded].value_counts()}) return df_balanced, le这里有个细节欠采样比例不要设成 1:1因为真实网络中正常流量本来就占多数完全均衡反而会让模型对正常流量的识别率下降。我一般设 3:1 到 5:1 之间具体看少数类样本量。random_state固定住是为了保证实验可复现这个在写论文时很重要。2.4 归一化与滑动窗口构造CNNLSTM 的输入需要是三维张量形状通常是(样本数, 时间步长, 特征数)。但 CICIDS2017 的每条记录是一个独立的流没有天然的时间步维度。常见做法是用滑动窗口把连续的 N 条流拼成一个序列模拟时序关系。虽然这种做法在严格意义上不是真正的时序建模但在课程设计层面足够用。from sklearn.preprocessing import MinMaxScaler def create_sequences(X, y, time_steps10): 用滑动窗口将二维数据转成三维序列 X: (样本数, 特征数) y: (样本数,) time_steps: 每个序列包含的流数量 Xs, ys [], [] for i in range(len(X) - time_steps): Xs.append(X[i:(i time_steps)]) ys.append(y[i time_steps]) # 用序列末尾的标签作为该序列的标签 return np.array(Xs), np.array(ys) # 归一化 scaler MinMaxScaler() feature_cols [c for c in df_balanced.columns if c not in [Label, label_encoded]] X_scaled scaler.fit_transform(df_balanced[feature_cols]) y df_balanced[label_encoded].values # 构造序列 X_seq, y_seq create_sequences(X_scaled, y, time_steps10) print(f序列形状: {X_seq.shape}) # (样本数, 10, 特征数)time_steps这个参数需要调。设太小比如 3LSTM 捕捉不到足够的时序依赖设太大比如 50训练慢且容易过拟合。我一般从 10 开始试根据验证集表现调整。归一化用 MinMaxScaler 而不是 StandardScaler是因为流量特征里有很多计数类特征分布不均匀MinMax 能把它们压到 [0,1] 区间对 CNN 的卷积操作更友好。3. CNNLSTM 模型怎么搭从 Keras 代码到参数调优3.1 模型结构设计为什么是 CNN 在前、LSTM 在后先讲清楚为什么这么串。CNN 的核心是卷积核在局部窗口上滑动提取的是局部特征。放在流量检测场景里一个卷积核可以捕捉相邻几个数据包之间的统计模式比如短时间内连续出现的大包、特定端口号的频繁出现等。LSTM 的核心是门控机制能记住长距离依赖。放在 CNN 后面它接收的是 CNN 提取的高层特征序列学习的是这些特征在时间维度上的演变规律。如果反过来LSTM 在前、CNN 在后LSTM 输出的序列再经过卷积效果通常不如前者。原因是 LSTM 的输出已经是高度抽象的时间特征再做卷积容易丢失时序信息。所以标准做法是Conv1D 提取局部模式MaxPooling 降维然后送入 LSTM 做时序建模最后全连接层分类。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense, Dropout, BatchNormalization from tensorflow.keras.optimizers import Adam def build_cnn_lstm(input_shape, num_classes): model Sequential([ # 第一层卷积64 个卷积核核大小 3激活函数 ReLU Conv1D(filters64, kernel_size3, activationrelu, paddingsame, input_shapeinput_shape), BatchNormalization(), MaxPooling1D(pool_size2), Dropout(0.3), # 第二层卷积128 个卷积核核大小 3 Conv1D(filters128, kernel_size3, activationrelu, paddingsame), BatchNormalization(), MaxPooling1D(pool_size2), Dropout(0.3), # LSTM 层128 个隐藏单元return_sequencesFalse 表示只取最后一个时间步的输出 LSTM(128, return_sequencesFalse), Dropout(0.3), # 全连接层 Dense(64, activationrelu), Dropout(0.3), # 输出层softmax 多分类 Dense(num_classes, activationsoftmax) ]) model.compile( optimizerAdam(learning_rate0.001), losssparse_categorical_crossentropy, metrics[accuracy] ) return model model build_cnn_lstm(input_shape(10, X_seq.shape[2]), num_classeslen(le.classes_)) model.summary()逐层解释一下参数选择。第一层卷积用 64 个核是因为输入特征维度通常几十到上百64 个核能覆盖足够多的局部模式。kernel_size3是经验值对应一次看 3 个相邻时间步。paddingsame保证输出长度和输入一致避免边缘信息丢失。BatchNormalization 放在卷积后面、激活前面这里 Keras 的写法是先激活再 BN实际效果差异不大作用是加速收敛、防止梯度消失。Dropout 设 0.3 是防止过拟合的常规操作如果训练集很大可以降到 0.2。LSTM 层用 128 个隐藏单元return_sequencesFalse表示只取最后一个时间步的输出送入全连接层。如果你的任务需要每个时间步都输出比如序列标注就设成 True。损失函数用sparse_categorical_crossentropy而不是categorical_crossentropy是因为标签是整数编码而不是 one-hot 编码这样省内存。3.2 训练配置batch size、epoch 和早停策略模型搭好之后训练配置直接影响最终效果。下面是我常用的训练代码from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau from sklearn.model_selection import train_test_split # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X_seq, y_seq, test_size0.2, random_state42, stratifyy_seq ) # 回调函数 early_stop EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue, verbose1 ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6, verbose1 ) # 训练 history model.fit( X_train, y_train, validation_split0.2, epochs50, batch_size64, callbacks[early_stop, reduce_lr], verbose1 )batch_size64是折中值。设太小比如 16训练不稳定loss 震荡大设太大比如 256梯度更新次数少收敛慢。epochs50配合 EarlyStopping实际训练轮数通常远小于 50。patience5表示验证集 loss 连续 5 轮不下降就停止训练并恢复最优权重。ReduceLROnPlateau是学习率衰减策略当验证集 loss 停滞时把学习率减半帮助模型跳出局部最优。stratifyy_seq这个参数很重要它保证训练集和测试集的类别比例一致。如果不加可能测试集里某个攻击类样本极少评估结果不可信。3.3 评估指标准确率之外必须看混淆矩阵训练完之后不要只看 accuracy。网络流量检测是典型的不平衡分类问题准确率高不代表模型好用。比如 99% 的样本是正常流量模型全预测为正常也能拿到 99% 准确率但攻击流量一个都没抓到。必须看混淆矩阵和每个类别的 precision、recall、f1-score。from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt import seaborn as sns # 预测 y_pred_prob model.predict(X_test) y_pred np.argmax(y_pred_prob, axis1) # 分类报告 print(classification_report(y_test, y_pred, target_namesle.classes_)) # 混淆矩阵 cm confusion_matrix(y_test, y_pred) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsle.classes_, yticklabelsle.classes_) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Confusion Matrix) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) plt.show()看混淆矩阵时重点关注两点一是正常流量被误判为攻击的比例假阳性这个太高会导致告警疲劳二是攻击流量被漏判的比例假阴性这个太高意味着安全防护形同虚设。课程设计里如果能把假阴性率压到 5% 以下就算不错了。4. 避坑与排查五个让模型翻车的经典问题4.1 训练 loss 不下降accuracy 卡在某个值不动现象训练开始后 loss 一直在 0.6 附近震荡accuracy 停在 50% 左右几十轮都不变。原因最常见的是学习率设太大梯度更新步长过大导致在最优解附近来回跳。其次是数据没有归一化特征值范围差异太大导致梯度方向被大值特征主导。解决先把学习率降到 0.0001 试试如果还不行检查归一化步骤是否漏掉了某些列。另外确认标签编码是否正确如果标签是字符串直接送进去模型会报错而不是静默失败但标签编码错位比如 0 和 1 反了会导致模型学到相反的模式。4.2 验证集准确率远高于测试集现象训练时验证集 accuracy 到 98%但拿测试集一跑只有 80% 出头。原因数据泄露。最常见的情况是归一化时用了全部数据 fit然后才划分训练测试集。这样测试集的统计信息已经泄露到了训练过程中。另一个原因是重复样本没有去干净同一个流既出现在训练集又出现在测试集。解决归一化必须在训练集上 fit然后 transform 测试集。去重要在划分之前做。如果用了滑动窗口构造序列注意窗口之间可能有重叠重叠部分会导致信息泄露可以改用不重叠的窗口或者按时间顺序划分。4.3 少数类攻击样本 recall 极低现象正常流量的 f1 是 0.99但某个攻击类别的 recall 只有 0.3 甚至更低。原因类别不平衡没有处理好或者欠采样比例太激进少数类样本被淹没。另一个可能是该攻击类别的特征和正常流量太相似模型区分不开。解决先检查该类别的样本量如果少于 500 条考虑用 SMOTE 过采样而不是欠采样。如果样本量够但 recall 还是低可以给损失函数加类别权重让模型对少数类的错误惩罚更大。Keras 里可以用class_weight参数from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( balanced, classesnp.unique(y_train), yy_train ) class_weight_dict dict(enumerate(class_weights)) model.fit( X_train, y_train, class_weightclass_weight_dict, ... )4.4 模型训练时间过长一个 epoch 要跑十几分钟现象数据集只有几十万条但每个 epoch 耗时超过 10 分钟50 个 epoch 跑完要一整天。原因可能是 batch_size 设太小导致迭代次数过多。也可能是 LSTM 层隐藏单元太多参数量爆炸。还有一种情况是数据没有转成 numpy 数组每次 batch 都在做 pandas 索引速度极慢。解决先把 batch_size 提到 128 或 256 试试。然后检查 LSTM 的 units 是不是设了 256 以上课程设计场景 64 到 128 足够了。最后确认输入数据是np.array类型而不是 DataFrame后者在 Keras 里会触发额外的类型转换开销。4.5 保存的模型加载后预测结果不一致现象训练完保存模型下次加载后拿同样的输入预测结果和保存前不一样。原因没有保存归一化器的参数。模型保存的是权重但输入数据的归一化是在模型外部做的。如果重新加载时用了不同的 scaler 或者没有做归一化预测结果自然不同。解决把 scaler 的均值和方差一起保存加载时先 transform 再送入模型。可以用 joblib 保存 scalerimport joblib # 保存 joblib.dump(scaler, scaler.pkl) model.save(cnn_lstm_model.h5) # 加载 scaler joblib.load(scaler.pkl) model load_model(cnn_lstm_model.h5) X_new_scaled scaler.transform(X_new)5. 从课程设计到可用原型三个让方案更值钱的进阶技巧5.1 用注意力机制替换 LSTM 的最后一层LSTM 的问题是长序列上容易遗忘早期信息而且训练慢。如果你的序列长度超过 20可以试试在 CNN 后面加一个注意力层让模型自己学习哪些时间步更重要。Keras 里可以用Attention层或者自己写一个简单的注意力池化from tensorflow.keras.layers import Layer import tensorflow.keras.backend as K class AttentionPooling(Layer): def __init__(self, **kwargs): super(AttentionPooling, self).__init__(**kwargs) def build(self, input_shape): self.W self.add_weight( nameattention_weight, shape(input_shape[-1], 1), initializerglorot_uniform, trainableTrue ) self.b self.add_weight( nameattention_bias, shape(input_shape[1], 1), initializerzeros, trainableTrue ) super(AttentionPooling, self).build(input_shape) def call(self, x): # x shape: (batch, time_steps, features) e K.tanh(K.dot(x, self.W) self.b) # (batch, time_steps, 1) a K.softmax(e, axis1) # 注意力权重 output x * a # 加权 return K.sum(output, axis1) # 池化 def compute_output_shape(self, input_shape): return (input_shape[0], input_shape[-1])把 LSTM 换成AttentionPooling之后训练速度通常能快 30% 以上而且在小样本上表现更稳定。代价是可解释性变差注意力权重虽然能可视化但不如 LSTM 的门控信号直观。5.2 用混淆矩阵反推特征工程方向模型跑完之后如果某个类别的 recall 特别低不要急着调模型先回去看数据。把该类别的样本单独拎出来和它被误判成的类别做特征对比。比如 PortScan 被误判成 Normal对比两者的特征均值往往能发现某个关键特征比如 Destination Port 的方差在两类之间差异不明显说明这个特征没有区分度需要构造新特征或者换特征。我一般会做一张特征重要性表用随机森林跑一遍特征重要性排序把排名后 20% 的特征删掉再训练往往能提升 1 到 2 个百分点。这个技巧在课程设计论文里也好写属于「基于特征选择的优化」这类创新点。5.3 用 TensorBoard 看训练过程别靠猜很多人训练模型就是盯着终端输出的 loss 和 accuracy看到不下降就手动停掉。其实 TensorBoard 能提供更多信息比如每层的权重分布、梯度大小、学习率变化。如果某一层的梯度一直是 0说明该层没有学到东西可能是初始化有问题或者被前面的层阻断了梯度。import tensorflow as tf log_dir logs/fit/ datetime.datetime.now().strftime(%Y%m%d-%H%M%S) tensorboard_callback tf.keras.callbacks.TensorBoard( log_dirlog_dir, histogram_freq1, write_graphTrue ) model.fit( X_train, y_train, validation_split0.2, epochs50, batch_size64, callbacks[early_stop, reduce_lr, tensorboard_callback] )跑完之后在终端执行tensorboard --logdir logs/fit浏览器打开就能看到可视化面板。我一般重点看三个图loss 曲线是否平滑下降、学习率是否在合理区间、每层的权重直方图是否在更新。如果权重直方图一直不动说明这层是死的直接删掉或者换初始化方式。最后说一个我自己的习惯每次跑完实验不管结果好坏都把超参数、数据集版本、随机种子记在一个表格里。课程设计往往要跑几十组对比实验没有记录的话过两天就忘了哪组参数对应哪个结果。这个习惯帮我省了很多后悔药希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?