☰
基于CNN+LSTM的网络流量检测实战:从数据预处理到模型训练
2026/9/28 8:05:16 网站建设 项目流程

简介:这是一套基于CNN与LSTM融合模型的网络流量检测系统Python源码,面向高校学生、毕业设计者及需要完成课程设计的开发者。源码由5个Python脚本和1份Markdown说明文档组成,压缩包仅6KB,结构紧凑,涵盖数据预处理、模型构建、训练与测试等关键环节;5个py文件分别承担数据加载、预处理、模型定义和主程序入口等职责,md文档用于说明项目设计与使用方式。作者标注为评审98分的高分课设,代码本地编译可运行,适合学习CNN与LSTM在流量时序数据分类中的协同用法。目前已有132人浏览学习。通过源码可掌握从数据加载到模型评估的完整流程,文档助读设计思路与参数设置;项目难度适中,适合作为深度学习课设、期末大作业或毕业设计的参考实现,也能帮助快速搭建流量检测原型。

1. 网络流量检测为什么值得用CNN+LSTM做高分课设:一个能跑、能写、能讲清的点

如果你正在为课设选题发愁,网络流量检测是一个几乎不会出错的赛道:数据公开、问题定义清楚、能出图能出表,而且用 CNN+LSTM python 源码落地时,模型效果一眼就能看出来,答辩时也讲得明白。直观理解是:CNN 擅长抓流量特征里的局部模式,比如某个时间窗口内包长的突变、标志位的组合规律;LSTM 擅长抓时序依赖,比如攻击行为往往是一段持续变化的轨迹而不是一个孤立的包。两者串联起来,正好覆盖了网络流量检测里最核心的“特征怎么提”和“时序怎么建模”两个问题。这套方案适合有 Python 和 PyTorch 基础、想在课设里同时展示深度学习和工程能力的同学,也适合想快速验证一个检测系统原型的从业者。

2. 把流量变成CNN和LSTM能吃的数据:从pcap到滑动窗口张量

2.1 数据集怎么选:UNSW-NB15和CICIDS2017的取舍

做网络流量检测课设,数据集的第一个坎不是模型,而是选哪份数据。常见做法是用 UNSW-NB15 或 CICIDS2017,前者单条记录已经是特征向量,适合快速起步;后者提供原始 pcap,能自己走一遍完整的特征提取流程,答辩时加分。如果只是跑通 CNN+LSTM 并拿到不错的准确率,我一般建议先用 UNSW-NB15:单条 CSV 读进来就是 49 维特征,省去从 pcap 解析报文的大量精力。如果你想展示“从原始流量到检测结果”的全链路,再上 CICIDS2017,但必须接受它的文件体积和类不平衡问题。课设场景下时间是硬约束,别在数据准备阶段耗尽精力。

选数据时还注意一点:不同数据集的攻击类型定义差异很大。UNSW-NB15 把流量分为 Normal、Fuzzers、Analysis、Backdoors、DoS、Exploits、Generic、Reconnaissance、Shellcode 和 Worms 十类,你自己做二分类或选其中 3 到 5 类做多分类都可以。CICIDS2017 有 14 种攻击,类别多且样本不均衡,直接用全套会让 LSTM 的训练异常漫长。折中的做法是挑两到三类样本量较大的攻击,做三分类,既能讲清问题又不至于跑一晚上。

2.2 特征提取:怎么把一条流量会话变成一个固定维度的向量

网络流量检测的核心问题,是原始报文无法直接扔给 CNN 和 LSTM。常见做法是先把 pcap 按五元组(源IP、目的IP、源端口、目的端口、协议)聚合成流,再对每条流提取统计特征:包总数、字节总数、平均包长、包长标准差、持续时间、SYN/FIN/ACK 标志位数量、TCP 窗口大小均值等。这些特征组成一个向量,代表一条流。如果你想更进一步,把时序信息保留下来,不要只取均值,而是按时间窗口把流切成片段,每个片段内计算上述统计量,最后得到形状为(时间步数,特征维度)的矩阵。这一步完成之后,数据就从“网络报文”变成了“序列数据”,LSTM 才能派上用场。

用 UNSW-NB15 时,数据集自带的 49 维特征里已经包含大量统计量,比如 sttl(源端到目的端的生存时间)、smean(源端发送的包平均大小)、dload(目的端负载字节速率)。你不需要全部用上,但也不能随便砍。我一般会保留协议相关字段、包长相关字段、标志位字段,去掉 IP 地址、时间戳等与分类无关的字段,最后剩下的维度大概在 35 到 42 之间。如果你用的是 pcap 原始数据,特征工程要自己写,工作量会显著增加,课设阶段不建议从零开始。

2.3 构造样本:时间步长、重叠窗口和数据切分

CNN 和 LSTM 吃的是张量,输入形状一般是(batch_size, seq_len, feature_dim)。这里的 seq_len 是关键参数,它决定了模型每次看到多长时间的流量行为。设太小,模型看不到攻击的完整过程,容易被短时噪声干扰;设太大,训练时间长,而且很多正常流根本不够长。经验值是在 10 到 30 之间,我一般先按 seq_len=16 起步,把每条长流切成多个重叠窗口,重叠率取 50%,这样既增加了样本量,又保留了相邻窗口之间的过渡信息。切完之后,窗口的标签沿用所在会话的标签,这是后续最容易出错的地方,后面避坑章会专门展开。

数据切分时要注意:不能按窗口随机划分训练集和测试集,而要按会话划分。原因很直白,同一个会话切出的几十个窗口高度相似,如果它们被分到两个集合里,模型相当于见过测试答案,评估结果会虚高。正确做法是先按会话 ID 分组,再按会话级别做分层切分,一般 8:2 比较常见。顺序上,先把所有会话 shuffle,再取前 80% 的会话窗口做训练、后 20% 做测试。

2.4 预处理代码:归一化、重采样与label对齐

数据预处理的代码不复杂,但顺序有讲究。直接看下面这段,这是从原始 CSV 构造训练张量的最小实现:

import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler, LabelEncoder df = pd.read_csv('UNSW_NB15_training-set.csv', low_memory=False) # 1. 去掉与分类无关的字段:IP、时间戳、ID 这类 drop_cols = ['id', 'srcip', 'dstip', 'srcport', 'dstport', 'stime', 'ltime'] feat_cols = [c for c in df.columns if c not in drop_cols + ['attack_cat', 'label']] # 2. 数值列做标准化:注意用 fit 的统计量,测试集只 transform scaler = StandardScaler() X_norm = scaler.fit_transform(df[feat_cols].fillna(0).values) # 3. 标签编码:attack_cat 是多分类标签,label 是二分类标签 encoder = LabelEncoder() y = encoder.fit_transform(df['attack_cat'].fillna('Normal').values)

逻辑说明:先剔除地址、端口这类不携带攻击语义的字段;再做标准化,StandardScaler 会把每个特征变成均值为 0、方差为 1 的分布,这对 CNN 和 LSTM 的训练稳定性很重要;最后用 LabelEncoder 把字符串类别转成整数索引。这里特别提醒,fillna(0) 不是万能药,如果缺失比例超过 5%,应该回头检查特征是否可用,直接填 0 会让模型学到“缺失=某种行为”的假规律,反而不利于泛化。

接下来是滑窗切分和会话级数据划分:

def build_windows(X, y, seq_len=16, stride=8): X_windows, y_windows = [], [] # 假设数据已经按会话分组,这里以会话为单位切分 for sess_id in np.unique(session_ids): idx = np.where(session_ids == sess_id)[0] for start in range(0, max(len(idx) - seq_len + 1, 1), stride): end = start + seq_len seg = X[idx[start:end]] # 长度不足 seq_len 的片段做 padding,这里用 0 填充 if len(seg) < seq_len: pad = np.zeros((seq_len - len(seg), seg.shape[1])) seg = np.vstack([seg, pad]) X_windows.append(seg) y_windows.append(y[idx[start]]) # 标签取片段首帧 return np.array(X_windows), np.array(y_windows)

参数说明:seq_len 是每个窗口的时间步数,stride 是滑动步长。stride=seq_len 时窗口之间没有重叠,stride=seq_len//2 时重叠一半。重叠率越高,样本量越大,但相邻样本越相似,训练时要注意适当增大 dropout 来削弱过拟合。标签取片段首帧的标签,前提是这一整个片段都属于同一个会话;如果片段跨越了两个会话,那就是数据构造错误,这种样本会让模型学到大量噪声,需要直接丢弃。构建完的 X_windows 形状是(样本数,16,特征维度),正好是 LSTM 和 Conv1D 的输入格式。

3. CNN+LSTM检测模型的PyTorch实现:自己手写,不用现成库

3.1 网络结构怎么定:Conv1D提取局部模式,LSTM捕捉时序依赖

网络流量检测里,CNN+LSTM 的经典结构是:先让 Conv1D 在特征维度上做卷积,提取局部关联模式,再把卷积输出的序列交给 LSTM 做时序建模,最后经过全连接层输出分类概率。为什么不用纯 CNN?因为卷积核的视野是局部的,它擅长发现“包长在连续几个窗口里异常增长”这样的短模式,但难以直接建模“攻击行为会持续数秒乃至更长”的长期依赖。为什么不用纯 LSTM?因为 LSTM 对输入特征的原始尺度敏感,直接用高维原始特征训练不仅慢,而且容易忽略局部特征组合。两个模型串联是互补关系。

具体到结构参数,我一般这样设计:第一层 Conv1D 的核大小取 3,意味着每次看相邻 3 个时间步的局部模式;卷积核数量取 64,输出后接 BatchNorm 和 ReLU;然后是最大池化,把时间维压缩一半;再接一个双向 LSTM,hidden_size 取 64,num_layers 取 2,配合 dropout 层防止过拟合;最后把 LSTM 最后一个时间步的隐状态拉平,过一层全连接,直接映射到类别数。双向 LSTM 的意义在于,流量行为前后的依赖常常是双向的,比如某段突发流量之后紧接着的连接重置,这也是判定攻击的重要信号。

3.2 模型定义代码:一个可以直接跑通的PyTorch实现

下面给出一个完整可运行的模型定义,已经处理好了 Conv1D 和 LSTM 之间的维度衔接:

import torch import torch.nn as nn class CNNLSTMClassifier(nn.Module): def __init__(self, input_dim, num_classes, seq_len=16, conv_channels=64, lstm_hidden=64, num_layers=2, dropout=0.3): super().__init__() # 输入形状: (batch, seq_len, input_dim) # Conv1D 期望 (batch, channels, seq_len),所以先做维度置换 self.conv = nn.Sequential( nn.Conv1d(in_channels=input_dim, out_channels=conv_channels, kernel_size=3, padding=1), nn.BatchNorm1d(conv_channels), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2, stride=2) # 时间维减半 ) # 池化后序列长度约 seq_len // 2 lstm_input_size = conv_channels self.lstm = nn.LSTM( input_size=lstm_input_size, hidden_size=lstm_hidden, num_layers=num_layers, batch_first=True, bidirectional=True, dropout=dropout if num_layers > 1 else 0.0 ) # 双向 LSTM 的输出维度是 hidden_size * 2 self.classifier = nn.Sequential( nn.Dropout(dropout), nn.Linear(lstm_hidden * 2, 64), nn.ReLU(inplace=True), nn.Linear(64, num_classes) ) def forward(self, x): # x: (batch, seq_len, input_dim) -> (batch, input_dim, 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, _ = self.lstm(x) # out: (batch, seq_len//2, hidden*2) out = out[:, -1, :] # 取最后一个时间步 return self.classifier(out)

逻辑说明:permute 的两次调用是这类模型最容易写错的地方,Conv1D 默认把通道维放在第二维,而输入数据是(batch, seq_len, feature_dim),所以必须先用 permute 把特征维换到通道位置,卷积池化之后再换回来给 LSTM。最后取 LSTM 最后一个时间步的输出来做分类,虽然长度只有 seq_len//2,但由于 Conv1D 的 padding 为 1,信息没有丢失。代码里每个层的维度变化都写了注释,照着跑一般不会出维度报错。

参数说明:input_dim 就是前面预处理得到的特征维度,通常在 35 到 42 之间;num_classes 是分类数,二分类就是 2。conv_channels 控制卷积层抽取特征的宽度,取 64 在课设数据量下足够;lstm_hidden 取 64 兼顾表达能力和训练速度;num_layers 取 2 能让模型抓到多层时序抽象,但如果你的数据量少于几万条,3 层以上几乎必然过拟合。dropout 设 0.3 是经验值,如果你的训练准确率已经接近 100% 而验证集明显偏低,优先把它提到 0.5。

3.3 训练循环里容易被忽略的三个细节

模型定义好了,训练循环里最容易坑人的三个细节:设备放置、loss 计算和梯度裁剪。设备放置就是 model、输入、标签都要 .to(device),经常有人只把模型放到 GPU 而忘了输入张量,报错时一脸懵。loss 计算要注意标签的 dtype,CrossEntropyLoss 要求标签是 LongTensor,不能是 FloatTensor。梯度裁剪是针对 LSTM 的专项操作,RNN 系模型训练到后期容易梯度爆炸,表现为 loss 突然变成 NaN,解决办法是用 clip_grad_norm_ 把梯度模长限制在 5.0 以内。这三个细节不看一眼的话,第一个 epoch 就能让你怀疑人生。

除这三项之外,还有一个容易忽略但影响最终分数的点:LSTM 的权重初始化。PyTorch 默认初始化一般能跑,但如果发现训练初期 loss 下降极慢,可以手动对 LSTM 的权重做正交初始化,这在流数据这类序列建模里经常能带来更稳定的收敛。不过课设阶段不需要在这上面花太多时间,默认初始化加梯度裁剪通常已经够用。

3.4 评估指标为什么用F1而不是只用accuracy

网络流量数据集几乎都带类别不平衡,UNSW-NB15 里 Normal 占比超过半数,某些攻击类样本可能只有几百条。这时准确率是骗人的:模型全预测为 Normal 也能拿到 50% 以上的准确率,看起来很好看,实际上一个攻击都没检出。正确做法是看 Precision、Recall 和 F1-Score,尤其是每个类别的 F1 加权平均。Precision 衡量“检出的攻击里有多少是真的”,Recall 衡量“真实攻击里有多少被找到了”,F1 是两者的调和平均。答辩时如果能说清楚为什么不用准确率而用 F1,已经比大多数只会跑模型的同学高出一截。

评估代码一般这样写:

from sklearn.metrics import classification_report, f1_score model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for batch_x, batch_y in test_loader: batch_x = batch_x.to(device) logits = model(batch_x) preds = torch.argmax(logits, dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(batch_y.numpy()) print(classification_report(all_labels, all_preds, digits=4)) weighted_f1 = f1_score(all_labels, all_preds, average='weighted') print(f'Weighted F1: {weighted_f1:.4f}')

逻辑说明:模型处在 eval 模式时,Dropout 会被关闭,BatchNorm 会使用累积的全局统计量,这保证了推理结果的确定性。torch.no_grad 块避免计算梯度,大幅度降低显存占用。classification_report 会按类别输出精确率、召回率和 F1,以及整体准确率,一眼就能看出哪些攻击类被模型严重漏检。

4. 训练这个模型要调的关键参数:把课设跑出理想分数

4.1 learning rate怎么定:1e-3起步,配合ReduceLROnPlateau

Adam 优化器在大多数流量检测任务上比 SGD 收敛更快,初始学习率 1e-3 是一个稳妥起点。如果训练集样本量在 5 万条以下,1e-3 可能偏高,loss 会出现震荡;这时降到 3e-4 通常能解决。一个更省心的做法是配合 ReduceLROnPlateau,当验证集 F1 连续 3 个 epoch 不提升时,把学习率缩小到原来的 0.5 倍。这个策略的好处是不用手动盯着 loss 曲线反复重启训练,也不用在课设答辩前拿着学习率瞎试。需要注意,学习率调度要基于验证集指标而不是训练集 loss,否则模型已经过拟合了你还在降低学习率,只会浪费训练轮次。

4.2 序列长度与batch size:显存不够时的应对方案

seq_len 和 batch_size 是显存敏感参数。在 6GB 显存下,seq_len=16、hidden=64 的模型,batch_size 设 128 到 256 都算安全。如果跑的是双向 LSTM,显存占用大约是单向的 1.5 倍到 2 倍,batch_size 要相应缩减。显存不足的典型报错是 CUDA out of memory,课设阶段很多同学第一反应是换更大的 GPU,其实更快的办法是把 batch_size 减半,同时把 DataLoader 的 num_workers 调到 4,让 CPU 预加载数据。如果还是不够,可以把 LSTM 层数从 2 降到 1,精度损失通常在一个点以内。显存不是越满越好,训练时留出 20% 余量可以避免偶发的 OOM 中断。

4.3 正负样本不平衡:直接改loss不如用WeightedRandomSampler

流量检测的类不平衡问题很常见,比如 DoS 攻击样本可能是普通的 10 倍以上。处理不平衡有两个层面:一是修改损失函数,比如给少数类更高的权重;二是修改采样器,让每个 batch 里各类样本的比例接近。常见做法是 WeightedRandomSampler,它根据每个类别的样本数量倒数计算采样权重,让少数类在训练时被更多次抽到,直接效果是模型不再一边倒地偏向多数类。这里我建议优先用采样器而不是改 loss,因为改 loss 权重需要额外调权重大小,采样的意义更直观,也更稳定。

from torch.utils.data import DataLoader, WeightedRandomSampler import numpy as np # labels 是训练集的全部标签 unique, counts = np.unique(labels, return_counts=True) class_weights = 1.0 / counts sample_weights = class_weights[labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(labels), replacement=True) train_loader = DataLoader(train_dataset, batch_size=128, sampler=sampler, num_workers=4)

参数说明:class_weights 取每个类别样本数的倒数,样本少的类权重高。WeightedRandomSampler 的 replacement=True 表示允许重复采样,相当于对少数类做了带放回的复制;num_samples 设为原样本总数,那么每个 epoch 的训练步数和原来一致。这种做法的缺点是少数类在单个 epoch 内被反复看到,容易过拟合,所以 dropout 要适当提高,或者配合早停。评估时不要用采样后的分布,必须在原始测试集上计算,否则 F1 会虚高。

4.4 训练脚本:从数据加载到模型保存的完整流程

把前面的要素整合成一段可运行的训练主流程,注意每个关键位置的注释:

import torch import torch.nn as nn from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau model = CNNLSTMClassifier(input_dim=42, num_classes=10).to(device) criterion = nn.CrossEntropyLoss() optimizer = Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=3) best_f1 = 0.0 for epoch in range(30): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y = batch_x.to(device), batch_y.to(device) optimizer.zero_grad() logits = model(batch_x) loss = criterion(logits, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() # 验证并做学习率调度 val_f1 = evaluate(model, val_loader, device) scheduler.step(val_f1) print(f'Epoch {epoch+1:02d}, loss={loss.item():.4f}, val_f1={val_f1:.4f}') if val_f1 > best_f1: best_f1 = val_f1 torch.save(model.state_dict(), 'best_cnnlstm.pt') print('saved best model')

逻辑说明:这里每轮训练完都做一次验证,用验证集 F1 来驱动学习率衰减和模型保存。weight_decay=1e-4 是 L2 正则,能约束 LSTM 这类大参数模型;clip_grad_norm_ 放在 optimizer.step 之前,防止梯度爆炸。保存模型只存 state_dict 而不存整个模型对象,加载时先构建同样的模型结构再 load_state_dict,这是 PyTorch 的推荐做法,也方便在预测脚本里复用。30 个 epoch 对课设来说足够,配合早停不会浪费太多时间。

5. 流量检测系统避坑:5个让新手白折腾一晚的典型问题

5.1 训练精度极高但测试精度暴跌:标准化用了全局统计量

现象:训练集准确率接近 100%,验证集和测试集却只有 60% 上下,而且每次跑结果还不一样。

原因:对全部数据统一执行了 fit_transform,包括测试集。标准化时计算出的均值和方差已经“见过”测试集,相当于把测试集的分布信息偷给了训练过程。更隐蔽的是,测试期间的统计量会跟随全量数据,导致分布被重新调整,破坏了训练时学到的尺度关系。

解决:在训练集上单独 fit,再用同一个 scaler 去 transform 验证集和测试集。官方文档也是这个要求,初学者很少注意这一步。可以理解为:标准化器是模型的一部分,它只允许学习训练集的经验。

另外注意一点,标准化应该放在数据切分之后,如果用未切分数据直接 fit,即便分集后再 transform,均值方差里也已经混入了测试集的信息,同样的泄露问题,顺序反了依然会翻车。

5.2 LSTM训练几轮后loss变成NaN

现象:前几个 epoch 正常,第三个 epoch 开始 loss 突然变成 nan,或者直接爆到几百。

原因:最常见的是学习率过大导致梯度爆炸,其次是训练数据里存在极端值,比如某个特征的数值达到 1e8 量级。数据经过标准化后极端值问题会缓解,但 LSTM 的长链条反向传播会累积梯度,在不该出现的地方放大。

解决:先加梯度裁剪,clip 值设 5.0;如果还炸,把学习率从 1e-3 降到 1e-4。再检查数据,打印一下每个特征列的最大值和最小值,如果标准化后仍有超过 50 的绝对值,说明原始数据里有离群点,对它们做截断或删除。

5.3 识别结果偏向多数类:所有样本都被判成Normal

现象:测试集里攻击样本识别率极低,所有输出都是 Normal,整体准确率却不低。

原因:类别不平衡严重时,模型发现“全都预测为 Normal”就能拿到很高的优化收益,CrossEntropyLoss 默认情况下不会惩罚这种偷懒行为。这是流量检测里最常见的问题,尤其是用 CICIDS2017 这种极不平衡的数据集时。

解决:使用 WeightedRandomSampler 强制每个 batch 里有接近比例的类别,同时把评估指标从 accuracy 换成 weighted F1。改完采样器之后,模型会对少数类更敏感,但也要注意不要走向另一个极端——少数类精确率很高但召回率过低,这种情况可以通过调低少数类权重或增加 dropout 来缓解。

5.4 DataLoader返回的batch形状不对:把序列维度丢了

现象:报错说期望输入是 3 维,得到的却是 2 维;或者模型能跑,但训练 loss 完全不下降。

原因:Dataset 的getitem方法里如果返回的是一个形状为(seq_len, feature_dim)的 numpy 数组,DataLoader 会默认把它当作一个整体样本处理,但不小心 squeeze 掉了 seq_len 维。另一处常见错误是,构建窗口时如果只有单条样本,numpy 会自动丢弃第一维,导致 batch 维度消失。

解决:在 Dataset 的getitem里显式返回 torch.from_numpy(x).float(),并保证 x.shape 为(seq_len, feature_dim)。调试时可以打印一个 batch 的 shape 确认是(batch_size, seq_len, feature_dim)而不是(batch_size, feature_dim)。这个坑很小,但每届都有同学卡在维度报错上。

5.5 训练时间太长:LSTM层数太多或seq_len过大

现象:一个 epoch 要跑十几分钟,30 个 epoch 得等一个晚上,想调参都没机会。

原因:LSTM 的时间复杂度与序列长度成正比,双向 LSTM 又倍增计算量。seq_len=32 加上 num_layers=3 和双向,课设数据量达到几十万窗口时,训练时长会非常难看。

解决:先减少 num_layers 到 1 或 2,再检查 seq_len 是否真的需要这么长。一般来说 seq_len=16、stride=8 已经能在多数数据集上取得不错的效果;如果效果差异不大,说明任务不需要那么长的时序依赖。还可以用 torch.cuda.amp 的 GradScaler 做混合精度训练,只需要两三行改动,训练速度提升明显。

6. 验证闭环与课设文档写法:混淆矩阵、特征可视化和结果论证

模型训练完,下一步是把“它能用”变成“论文和答辩里能证明”,这一步值得单独打磨。先跑混淆矩阵,它能直观展示哪些类别之间容易互相混淆:比如 Fuzzers 被误判为 Generic,说明这两个类别的流量特征分布高度重叠。这种观察写进课设文档里,比“准确率 95%”有力得多,因为它体现的是对数据的理解,而不仅是调包能力。

可视化的第二个建议是 t-SNE 降维。取测试集一部分样本,用训练好的模型提取 LSTM 最后一层的输出特征,再用 t-SNE 降到二维并散点绘制,按真实类别着色。如果 CNN+LSTM 确实学到了好特征,你会看到同类样本聚成团、不同类之间有明显边界,这比任何指标都直观。如果聚类效果很差,说明模型学到的是浅层统计量,而不是真正的攻击模式,此时优先回头调数据,而不是继续堆模型复杂度。

文档说明书的核心结构一般包括:系统架构图、数据处理流程、模型结构说明、实验结果表和分析。实验结果表不要只贴一个总体准确率,按类别列出 Precision、Recall、F1,并挑两个表现最好和最差的类别做简要分析。这一部分能拉开差距:大多数课设只写“跑了模型得了多少分”,而你会解释为什么某类识别率低、哪些特征不够、后续可以怎么改进,这才是高分课设该有的样子。

整个项目做完我最大的体会是:CNN+LSTM 这套结构本身并不神秘,真正的难点在数据预处理和验证设计上。最开始我也踩过测试集泄露和类别不平衡两个大坑,修完之后 F1 从 0.6 升到 0.91,效果立竿见影。如果你正卡在某个环节,按文章顺序从前到后排查一遍,特别是标准化和采样器这两处,多半能救回来。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询