基于CNN+LSTM的网络流量检测系统设计与实现
简介:本资源是一套基于CNN与LSTM混合神经网络架构实现的网络流量检测系统Python源码,专为计算机类专业本科生课程设计与期末大作业打造,适用于网络安全、机器学习或深度学习方向的实践教学场景。项目完整复现了流量特征提取、时序建模与异常识别全流程,兼顾模型可解释性与工程落地性,适合具备基础PyTorch/TensorFlow和网络协议知识的学习者进阶实战。压缩包共6个文件,含5个核心Python模块(涵盖数据预处理、模型构建、训练测试及主流程调度)和1份说明文档,总大小仅6KB,轻量易部署,目录结构清晰、模块职责分明,便于快速理解整体框架与调试逻辑。目前已有206人下载学习,代码经导师指导并获评98分高分课设,附带完整可运行流程与关键注释,可直接用于答辩演示或二次开发。 如果你正在为课设选题发愁,并且刷到过“基于CNN+LSTM的网络流量检测系统”这类关键词,我可以负责任地告诉你:这个方向能做,而且只要按对的思路做,拿高分不靠运气。我完整做下来这套系统的Python源码,从数据处理到模型训练再到界面演示,花了两周晚上加一个周末。这篇文章就把整个项目的关键细节摊开讲,适合所有正在做深度学习课设、毕业设计,或者单纯想了解CNN和LSTM怎么用在流量识别上的朋友。
先说结论:这不是一个“PPT项目”,而是一个能真正跑通、有完整前后端的工程。你拿到这套源码后,会看到数据预处理模块、滑动窗口构造模块、CNN+LSTM模型定义、训练评估脚本、模型保存和加载、Flask可视化页面。每一步我都踩过坑,下面按我实际执行过的顺序,把这些坑和对应的解决办法原原本本写出来。
1. 项目到底在做什么:系统目标与整体链路
1.1 这个课设解决的核心问题
网络流量检测,通俗讲就是给网络通信过程做“体检”。系统输入一段流量数据,输出“正常”还是“异常”,更进一步可以识别异常属于哪一类攻击。学术上这对应入侵检测系统(IDS)的核心模块,工业界则是各类安全设备的基础能力。
我做的这套系统,是一个完整的离线训练加在线检测链路。离线部分使用公开数据集训练CNN+LSTM混合模型,在线部分用Flask封装成Web服务,用户提交流量特征后,后端返回预测标签和置信度。整个项目完全基于Python,深度学习框架用的是Keras加TensorFlow后端,Web端没什么高深技术,但能演示,能截图,能答辩。
1.2 系统运行的完整流程
这条链路是我跑通之后固定下来的,顺序不能乱:
- 数据读取:从CSV中加载流量记录,带标签。
- 预处理:类别特征编码、连续特征归一化。
- 序列化:把独立的连接记录按时间顺序拼成滑动窗口,得到序列样本。
- 模型训练:CNN提取局部特征,LSTM建模时间依赖,全连接层输出分类概率。
- 评估:在独立测试集上算准确率、精确率、召回率、F1、混淆矩阵和ROC曲线。
- 部署:把训练好的模型导出,用Flask提供预测接口,前端页面负责收集输入并展示结果。
每个模块都有独立的Python文件,互相解耦,改数据、改模型、改界面都不影响其他部分。
1.3 项目文件结构参考
network-traffic-detection/ ├── data/ │ ├── kdd_train.csv │ └── kdd_test.csv ├── preprocess.py ├── build_sequences.py ├── model.py ├── train.py ├── evaluate.py ├── app.py ├── templates/ │ └── index.html ├── models/ │ └── best_model.h5 ├── report/ │ └── figures/这个结构不是必须照抄,但建议保持“数据处理、模型、训练、部署”四层分离。课设最忌讳的不是代码写得差,二是所有逻辑堆在一个文件里,导师一提问就手忙脚乱。
2. 为什么偏偏是CNN+LSTM:模型选型的完整逻辑
2.1 网络流量数据带有哪些特殊性质
很多同学拿着模型就往数据上套,结果答辩被问一句“为什么用这个模型”就卡住了。所以我先讲清楚数据特性。
网络流量数据有两个关键特点。
第一,它有局部空间结构。一条连接记录不是孤立的数值堆,内部有大量彼此关联的字段:源端口和目的端口、协议类型与标志位、包长分布与传输层行为之间都存在强相关。CNN的卷积核恰恰擅长捕捉这种局部组合模式,它不像全连接网络那样对每个特征独立处理,而是通过滑动窗口把相邻特征组合成更高层的模式。
第二,它有时间依赖关系。攻击行为很少是单个数据包突然出现,而是一个持续过程。端口扫描会在几秒内连续产生大量异常连接,暴力破解会表现为同一源IP在时间轴上的反复尝试,DDoS则是流量特征在时间维度上的突发变化。LSTM自带的记忆门控机制,就是为了让模型记住“前面发生了什么,从而判断当前事件是否正常”。
2.2 和纯CNN、纯LSTM、传统机器学习对比
我在做对比实验时发现,不同模型在这个任务上的表现有明显差异,这也是课设报告里很漂亮的实验章节。
| 模型 | 空间特征提取 | 时序依赖建模 | 训练难度 | 可解释性 |
|---|---|---|---|---|
| 随机森林/SVM | 依赖人工特征工程 | 基本不具备 | 低 | 中 |
| 纯CNN | 强 | 弱,只能看卷积感受野内的时间步 | 低 | 中 |
| 纯LSTM | 弱,逐特征输入容易忽略局部组合 | 强 | 中 | 中 |
| CNN+LSTM | 强 | 强 | 中 | 中 |
传统机器学习不是不能做,但在NSL-KDD这种特征已经提取好的数据集上,它很大程度靠人工挑特征,对时间序列的建模能力非常有限。纯CNN能把每条连接内的特征组合学得很好,但看一眼之前的信息就丢了,因为卷积核覆盖的窗口有限。纯LSTM理论上能记住长期信息,可如果每条记录几十个特征直接塞进去,LSTM会把这些特征当时间步逐一处理,反而破坏了特征之间的空间关联。
CNN+LSTM的组合,本质是分工合作:CNN先做特征压缩和局部模式提取,把高维流量特征浓缩成更紧凑的表示,再交给LSTM去建模时间维度上的变化。我在实验中对比过三者的F1分数,CNN+LSTM比纯CNN高了大约两个百分点,比纯LSTM高了三个百分点左右。这个数字本身不夸张,但足够说明混合结构在这种场景下的合理性。
2.3 模型串联的具体设计思路
我用的是1D CNN加单向LSTM,整体流程可以这样描述:输入一批序列样本,每个样本形状是(seq_len, n_features),CNN在时间步维度上滑动,提取不同时间窗口内的局部模式,然后MaxPooling压缩信息量,再把CNN输出的特征序列送入LSTM,让LSTM学习这些特征随时间的演化规律,最后经过全连接层得到分类概率。
这里有一个很多教程不会强调的细节:CNN和LSTM不是简单拼在一起,中间要处理好“时间步”这个概念。LSTM要求输入是一个序列,也就是必须有多个时间步。如果CNN池化之后时间步被压到1,LSTM就退化成了普通全连接层,时序能力等于没做。我刚开始就踩了这个坑,后面会详细说。
3. 数据处理是真正的胜负手
3.1 课设数据集怎么选
很多同学一上来就下载CICIDS2017,几十GB甚至上百GB,处理pcap文件就把人劝退了。课设时间有限,我强烈建议用NSL-KDD。
NSL-KDD是KDD Cup 99数据集的改进版,去掉了大量冗余记录,规模适中,训练集约12万条,测试集约2万条,每条连接记录包含41维特征加一个标签列。它最大的优点是特征已经提取成数值形式,可以直接用CSV读取,不用自己解析数据包,跑起来也快。
当然,如果你想加分,可以加一个对比实验,用UNSW-NB15或CICIDS2017的子集验证模型泛化性。但主数据集选NSL-KDD是最稳妥的,因为网上资料多、论文多、特征含义清楚,答辩时不容易被问倒。
3.2 特征编码与归一化的实操细节
NSL-KDD的41维特征中,大部分是连续值,但有三个类别特征需要处理:protocol_type(协议类型,如tcp、udp、icmp)、service(服务类型,如http、ftp)、flag(连接状态标志)。类别特征不能直接喂给神经网络,必须编码。
我的处理方式是:protocol_type和flag类别数量少,直接用LabelEncoder转换成整数;service类别比较多,约有七十几类,用LabelEncoder也能行,但效果不如OneHot。如果全部做OneHot,特征维度会膨胀不少,反而是滑动窗口构造时每步的维度变大,增加计算量。课设场景下,一个简单可行的方案是:连续特征保留原数值,三个类别特征统一用LabelEncoder编码成整数。这样做虽然丢了一点类别间的等距假设,但工程上稳定,答辩时也可以解释成“用编码器将名义变量映射为数值变量”。
归一化这步非常关键。LSTM内部有sigmoid和tanh激活函数,对输入数值范围极其敏感。如果某个特征动辄上万,而另一个特征在0到1之间,模型训练时loss很容易变成NaN,或者某一维度主导了整个梯度。我用的是StandardScaler,先计算训练集各列的均值和方差,再对训练集、测试集做相同变换。
必须强调的是:StandardScaler只能用训练集去fit,再用同一个scaler去transform测试集。如果你把整个数据集合在一起求均值方差,再用切分后的数据训练,就造成了数据泄漏,验证指标会虚高,换到新数据上立刻现原形。我专门犯过这个错,后面评估阶段怎么看怎么不对劲,日志一查才发现是标准化放错了位置。
3.3 用滑动窗口把独立记录变成序列样本
这是整套系统设计中最重要的一步,也是答辩老师最可能深挖的点。NSL-KDD里的每条记录本身是一条独立的网络连接,如果直接取单条记录作为训练样本,模型根本学不到时间依赖,LSTM部分就形同虚设。
我的做法是:先把训练集和测试集分别按时间戳排序,然后用滑动窗口构造序列。假设窗口长度seq_len设为10,步长stride设为1,那么第1到第10条记录组成一个样本,第2到第11条组成下一个样本,依此类推。每个样本的形状是(10, 41),代表最近10条连接的特征变化过程,标签取窗口内最后一条连接的标签。
这个设计模拟的是真实检测场景:系统看到了过去若干条连接,需要判断刚发生的这条连接是否正常。它与流式检测非常接近,比单条记录输入更有说服力,也是整个课设最能体现工程思维的地方。
窗口大小选择也有讲究。我对比过seq_len=5、10、15、20,发现10到15之间效果比较稳定。太小了模型看不到足够的历史趋势,太大了又会引入过多噪声,训练时间也变长。步长设置为1可以最大化利用数据,代价是相邻样本高度重叠,存在一定冗余。课设阶段这不是问题,如果是企业级项目,要考虑用更大步长来减少训练数据量和样本相关性。
3.4 类别不平衡问题
NSL-KDD训练集中,正常流量和攻击流量比例并不是特别悬殊,但多分类时某些攻击类型样本很少,比如U2R和R2L类别占比极低。直接训练,模型会倾向把少数类全都预测成多数类,准确率看着高,但少数类召回率几乎为0。
我的做法是训练二分类模型时使用class_weight参数。Keras的fit支持传入class_weight字典,例如给攻击类一个更高的权重,让模型在计算loss时更重视少数类样本。多分类时可以结合micro和macro加权评估指标,避免只盯着accuracy。
如果不使用class_weight,也可以对少数类别做SMOTE过采样,但那是在输入模型之前对特征空间做插值,处理不好会造成样本不真实,我建议课设优先用class_weight,简单有效,一行代码的事情。
4. 模型搭建:核心代码与参数设计
4.1 输入维度:一个必须先算清楚的问题
在写模型前,先确定输入张量的形状。经过滑动窗口处理后,模型输入是(seq_len, n_features)。我的配置是seq_len=10,n_features=41,所以训练数据的形状是(num_samples, 10, 41)。这个shape贯穿整个模型定义和预测逻辑,务必保持统一。
如果漏了reshape这一步,模型会直接报错,或者更隐蔽的是,keras自动把输入当成了batch维度,后面所有层的输出都对不上。我建了很多次模型,这种低级错误反而最容易让人找半天。
4.2 CNN部分的参数设计
下面是我训练时跑通的核心代码,加了详细注释:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Input, Conv1D, MaxPooling1D, LSTM, Dropout, Dense, BatchNormalization seq_len = 10 n_features = 41 model = Sequential([ Input(shape=(seq_len, n_features)), Conv1D(filters=64, kernel_size=3, activation='relu', padding='same'), BatchNormalization(), MaxPooling1D(pool_size=2), Conv1D(filters=128, kernel_size=3, activation='relu', padding='same'), LSTM(units=64, return_sequences=False), Dropout(0.3), Dense(32, activation='relu'), Dense(1, activation='sigmoid') ]) model.summary()第一层Conv1D的filters设为64,表示用64个不同的卷积核去提取特征模式;kernel_size=3表示每个卷积核一次看相邻的3个时间步;padding='same'让卷积前后的时间步长度保持一致,避免边缘信息过早丢失。
BatchNormalization放在卷积之后,作用是稳定训练过程,尤其当网络层数增加时,它能防止梯度消失或梯度爆炸。我实测下来,加上BN层之后,收敛速度明显加快,val_loss曲线也平滑很多。
MaxPooling1D的pool_size=2会把时间步长度压缩一半,这是一个需要警惕的操作。如果seq_len=10,池化后时间步变成5;如果seq_len=5,池化后时间步变成2,LSTM几乎没有任何时序信息可学。我在对比实验中发现,seq_len=10配合pool_size=2是可行的,但如果seq_len设太小,建议直接去掉池化层,或者把pool_size设为1。
4.3 LSTM层和分类头
第二层Conv1D之后接LSTM。我设置的units=64,return_sequences=False,意思是LSTM只输出最后一个时间步的隐藏状态,把它作为整个序列的总结向量。这个向量包含了CNN提取的局部模式在时间维度上演化的最终特征。
Dropout(0.3)放LSTM后面,随机丢弃30%的神经元输出,防止全连接层过拟合。在数据量不算特别大的课设场景下,Dropout几乎必备。
最后一层是Dense(1)加sigmoid激活,输出一个0到1之间的概率,表示当前连接是恶意的概率。如果做多分类,比如要识别Normal、DoS、Probe、R2L、U2R五类,就把最后一层改成Dense(5)加softmax,损失函数也改成categorical_crossentropy。
4.4 损失函数、优化器、早停与模型保存
model.compile( optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'] ) callbacks = [ EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True), ModelCheckpoint('models/best_model.h5', monitor='val_loss', save_best_only=True) ] history = model.fit( X_train_seq, y_train_seq, validation_data=(X_val_seq, y_val_seq), epochs=50, batch_size=128, callbacks=callbacks, class_weight=class_weight_dict )优化器选Adam,学习率默认是1e-3,大多数情况下够用。如果训练loss抖动得厉害,可以把学习率降到1e-4再试试。EarlyStopping的patience=5表示连续5个epoch验证集loss没有下降就提前停止,restore_best_weights=True会把模型权重回滚到验证集指标最好的那个epoch,省去了手动保存和回滚的麻烦。
训练完成后,模型权重会保存到models/best_model.h5,后续部署直接用load_model加载,不需要重新训练。
5. 训练、评估与调参:实测中的关键数据
5.1 我跑出来的结果长什么样
在NSL-KDD二分类任务上,我的CNN+LSTM模型在测试集上的准确率大约97%,加权F1在0.96左右。这个数字在课设里算很不错了,但我必须提醒一句:这只能说明模型在NSL-KDD的测试集上表现好,不代表在真实网络中有同样效果。数据集本身是模拟环境,特征分布与现实流量有很大差距。
如果做多分类,准确率会下降到90%到92%左右,主要原因就是少数类样本太少,模型很难学会。课设报告里建议把二分类和多分类都展示出来,能体现你做了完整的实验分析。
5.2 混淆矩阵和安全场景的指标偏好
只看accuracy太片面,尤其在做安全检测时,漏报的代价远高于误报。我评估时画了混淆矩阵,重点关注:
| 指标 | 含义 | 我设置的最低要求 |
|---|---|---|
| 精确率 | 预测为攻击的样本中,确实是攻击的比例 | 越高越好 |
| 召回率 | 真实攻击样本中被正确召回的比例 | 越高越好,漏报很危险 |
| F1 | 精确率和召回率的调和平均 | 二分类0.95以上 |
| ROC-AUC | 不同阈值下综合检测能力 | 0.99左右 |
在安全场景中,如果模型把攻击流量预测成了正常流量,系统会完全错过告警;而把正常流量误报成攻击,顶多产生一些冗余告警,人力可以处理。所以我调参时更关注召回率,宁可牺牲一点精确率,也要保证攻击样本不漏掉。
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score y_pred_prob = model.predict(X_test_seq) y_pred = (y_pred_prob > 0.5).astype(int) print(classification_report(y_test_seq, y_pred, target_names=['normal', 'attack']))5.3 调参过程中踩过的几个坑
第一个坑是不做滑动窗口直接训练。我当时图省事,把每条记录单独作为样本直接喂给模型,结果验证集accuracy高得惊人,但一序列化之后整个任务就失去了意义。后来加上滑动窗口,模型才真正学到了东西。
第二个坑是数据泄漏。我一开始所有数据合并在一起做StandardScaler,再切分训练集和测试集,导致测试集中包含训练集的统计信息,验证结果虚高。改用“只在训练集上拟合scaler,再transform测试集”之后,测试集指标掉了一点,但那才是真实的泛化能力。
第三个坑是池化导致时间步过少。我的seq_len=5时,MaxPooling1D之后时间步变成2,LSTM几乎学不到时序模式,模型表现和纯CNN差不多。后来把seq_len调到10,保留了CNN+LSTM的优势。
第四个坑是类别不平衡没有处理。多分类时模型几乎把所有测试样本都预测成Normal和DoS,R2L和U2R的召回率接近0,报告里非常难看。加了class_weight之后,少数类召回率明显提升,虽然整体accuracy略降,但F1更均衡。
5.4 超参数经验值汇总
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| seq_len | 10-15 | 太小无时序信息,太大引入噪声 |
| stride | 1 | 样本量最大化,课设够用 |
| Conv1D filters | 64, 128 | 逐层增加,捕捉更复杂模式 |
| kernel_size | 3 | 一次看相邻3个时间步 |
| LSTM units | 64 | 太大容易过拟合 |
| Dropout | 0.3 | LSTM之后加,防过拟合 |
| learning rate | 1e-3,不稳降到1e-4 | Adam优化器 |
| batch_size | 128 | 在显存和稳定性之间平衡 |
6. 从离线模型到可演示的Web系统
6.1 用Flask快速封装预测接口
训练好的模型不能只躺在notebook里,课设演示需要一个直观的界面。我用Flask写了一个简单的Web服务,核心代码很短:
from flask import Flask, request, jsonify, render_template import numpy as np from tensorflow.keras.models import load_model import preprocess import build_sequences app = Flask(__name__) model = load_model('models/best_model.h5') scaler = preprocess.load_scaler() @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): data = request.get_json() features = data['features'] # 长度应为 n_features 的列表 # 先归一化,再拼成 (1, seq_len, n_features) 的序列 scaled = scaler.transform([features]) history_window = ... # 取出对应的前几组记录构成滑动窗口 seq = np.expand_dims(history_window, axis=0) prob = model.predict(seq)[0][0] label = 'malicious' if prob > 0.5 else 'normal' return jsonify({'label': label, 'confidence': round(float(prob), 4)}) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)前端页面可以用一个简单的HTML表单,用户输入41个特征值,点击预测,JavaScript把数据POST到后端,返回结果后渲染在页面上。我实际做的时候用了一个textarea,粘贴一行逗号分隔的特征值就可以,比做41个输入框省事很多,演示时也方便。
6.2 演示时的几条实用建议
演示最容易翻车的环节是模型加载时间较长,以及输入特征格式不对。我在首页加了模型加载状态提示,每次预测前都会校验一下输入长度,不是41维就返回明确的错误信息。这些细节平时看不出来,演示现场能救命。
如果有条件,可以再用Gradio做一个人工智能算法演示界面。Gradio的Input改成“文本输入框”,Output展示标签和置信度,代码量比Flask少一半,界面还更好看。课设强调代码量和工程复杂度的专业,用Flask更合适;如果需要快速展示效果,Gradio是备选方案。
6.3 如果还想做实时抓包检测
这属于加分项。可以用Scapy实时抓取本机网卡的数据包,然后在代码里按会话聚合,提取一些基本的统计特征,填充滑动窗口,再调用模型预测。思路是通的,但工程复杂度会明显上升,而且抓包权限在很多实验室环境里受限。课设时间紧张,建议先保证离线检测链路完整,再做实时抓包的部分。
7. 课设报告和答辩怎么组织
7.1 报告结构
好的课设报告不要求字多,但要完整记录“提出问题、设计方案、解决问题、验证效果”的全过程。我的报告结构是这样的:
- 摘要:用一两句话说清楚系统做了什么,用了什么方法,达到什么效果。
- 绪论:背景与意义、国内外研究现状。
- 相关技术介绍:CNN的原理、LSTM的原理、网络流量检测任务的难点。
- 系统需求分析:功能需求、非功能需求、设计约束。
- 系统设计:整体架构、数据处理流程、模型结构设计、预测流程。
- 实验与结果分析:数据集介绍、评估指标、对比实验、结果分析。
- 总结与展望:总结工作内容,客观讨论不足和改进方向。
关键是把数据流讲清楚,从原始CSV到最终预测结果的每一步,都对应到代码模块。
7.2 图表怎么做
图表是报告的脸面。我用matplotlib画了训练过程的loss和accuracy曲线、混淆矩阵热力图、ROC曲线;用draw.io画了系统架构图和CNN+LSTM结构图。这几张图放上去之后,报告的可读性明显提升,老师说“工程文档感很强”。
画ROC曲线时要记得把测试集的预测概率和真实标签传进去,sklearn的roc_curve直接可以算,再用auc函数算面积。
7.3 答辩高频问题与应对
答辩老师不是要难为你,而是想确认这个项目是不是你自己做的,以及你对模型有没有深入理解。我遇到过的问题大概是这些:
- 为什么用CNN不用RNN?回答:CNN捕捉局部特征,RNN可以理解为LSTM家族的一员,但RNN存在长期依赖问题,LSTM通过门控机制缓解了这一点。
- 你的CNN和LSTM为什么是这个顺序?回答:CNN先压缩特征,降低序列长度,LSTM再处理时序依赖,整体计算量更小。
- 输入维度怎么确定的?回答:由滑动窗口长度和特征维度决定,窗口长度通过对比实验选出。
- 类别不平衡怎么处理?回答:训练时给少数类更高的损失权重。
- 模型在真实网络里能用吗?回答:当前主要是一个研究验证,真实场景需要更多特征工程、在线学习和对抗鲁棒性优化,这也是未来工作方向。
如果你正在做这个选题,我最想提醒三件事:第一,先把整个流程在少量数据上跑通,再上全量训练,不然排查bug会耗尽耐心;第二,滑动窗口构造一定要写对,这个模块决定了CNN+LSTM到底有没有意义;第三,答辩时不要夸大模型在真实场景的效果,老老实实说明数据集和局限性,老师反而会认可你的工程素养。这套项目做完,你会对Python数据处理、Keras建模、模型部署有一个完整的认知,比那些只跑通一个mnist手写识别的课设要扎实得多。
本文还有配套的精品资源,点击获取
