简介:毕业设计项目聚焦基于卷积神经网络的网络入侵检测,提供完整Python源码与全部实验数据,面向计算机相关专业正在完成课程设计、期末大作业或毕业设计的学生,也适合需要实战练习的初学者。项目基于NSL-KDD数据集,完整涵盖数据预处理、特征处理、模型搭建、训练评估与预测推理等环节,代码模块划分清晰,并附带训练好的模型权重及准确率、精确率等可视化图表,便于对照复现实验结论。资源压缩包共33个文件,核心为csv数据集文件与py源码文件,另包含xml工程配置、png/jpg结果图、pth模型权重、md说明文档等,整体约21.58MB,目录结构简洁,可快速定位数据、代码与结果。目前已有767人学习使用,系经导师认可、评审分98分的高分毕业设计,对理解入侵检测中的特征提取与卷积神经网络建模流程具有较高参考价值,也可直接迁移到类似分类项目。
1. 这个毕业设计到底在做什么:把网络流量当成"图像"来分类
先说一个反直觉的结论:网络入侵检测这个传统上属于机器学习的课题,用 CNN 卷积神经网络来做,不是噱头,而是确实能落地的一种方案。核心思路是把每一条网络连接记录转成固定长度的特征序列,再把这个序列当成"图像"让 CNN 去提取局部模式——CNN 擅长抓局部关联,而网络攻击流量往往就藏在某些特征字段的组合里。
这个标题的受众很明确:正在选毕业设计题目、或者已经选了题目但不知道怎么下手的计算机相关专业学生。你需要交出一套能跑、能出实验对比、能写进论文的东西。"python源码+全部数据"说明这个课题不要求你自己造数据集,重心在模型设计、训练调试和结果分析上。这篇笔记按我自己做这类课题的习惯,把从数据准备到模型验证的完整链路拆开讲,参数和坑都给到位,你可以直接照着复现。
2. 数据与特征工程:CNN 的输入不是"包",是"字段"
2.1 数据集选型:NSL-KDD 与 UNSW-NB15 怎么选
网络入侵检测最常用的公开数据集是 KDD Cup 1999 和它的改进版 NSL-KDD。KDD99 年代久、样本冗余严重,训练集和测试集有大量重复记录,导致模型在测试集上的分数虚高。NSL-KDD 剔除了冗余记录,训练集约 12.5 万条,测试集约 2.2 万条,类别覆盖 Dos、Probe、R2L、U2R 和 Normal 五类,做本科毕业设计足够用了。
如果你想让课题看起来更"现代",可以换 UNSW-NB15,它包含 9 类攻击,特征 49 维,流量更接近真实场景。但要注意它的某些攻击类别样本极少,训练时要处理不平衡问题,调参成本更高。我建议毕设默认走 NSL-KDD,答辩时提一句"可扩展换用 UNSW-NB15"就够了。
2.2 特征编码:数值型、类别型、符号型字段的处理
NSL-KDD 的每条记录有 41 个特征,其中 38 个是数值特征,3 个是符号类型:protocol_type、service、flag。读进来第一步是把符号字段编码成数值。直接用 LabelEncoder 简单省事,但要注意测试集要拿训练集拟合好的 encoder 去转换,不能重新 fit,否则测试集里的新类别会报错。
数值特征之间量纲差异很大,比如 src_bytes 可能到几十万,而 count 只有个位数。CNN 对输入尺度敏感,必须做标准化。用 sklearn 的 StandardScaler 拟合训练集,变换训练集和测试集。有一个细节:标准化要在数据划分之后做,不能先整体标准化再划分,否则测试集的信息通过均值和方差泄漏到了训练过程里,答辩时容易被问住。
2.3 序列化:把一维特征变成二维"伪图像"
CNN 处理一维向量可以直接用一维卷积(Conv1d),也可以把 41 维特征重排成类似 7×6 的二维矩阵然后用二维卷积。我的经验是:对于这种低纬度特征向量,Conv1d 更自然,卷积核沿特征维度滑动,不会破坏特征之间的相对位置语义。如果你想把图像处理的思路讲得更直白,也可以用 Conv2d 配合 1×1 卷积做特征融合。
下面是数据加载和预处理的完整代码,按 NSL-KDD 的 CSV 格式处理:
import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.model_selection import train_test_split # NSL-KDD 特征列名(按官方文档顺序) col_names = [ 'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', 'num_failed_logins', 'logged_in', 'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations', 'num_shells', 'num_access_files', 'num_outbound_cmds', 'is_host_login', 'is_guest_login', 'count', 'srv_count', 'serror_rate', 'srv_serror_rate', 'rerror_rate', 'srv_rerror_rate', 'same_srv_rate', 'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 'dst_host_srv_count', 'dst_host_same_srv_rate', 'dst_host_diff_srv_rate', 'dst_host_same_src_port_rate', 'dst_host_srv_diff_host_rate', 'dst_host_serror_rate', 'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 'dst_host_srv_rerror_rate', 'label' ] # 读取训练集和测试集(按你的实际文件路径修改) train_df = pd.read_csv('KDDTrain+.txt', header=None, names=col_names) test_df = pd.read_csv('KDDTest+.txt', header=None, names=col_names) # 将 label 二值化:Normal 为 0,攻击为 1 train_df['label'] = train_df['label'].apply(lambda x: 0 if x == 'normal' else 1) test_df['label'] = test_df['label'].apply(lambda x: 0 if x == 'normal' else 1) # 符号特征编码 cat_features = ['protocol_type', 'service', 'flag'] encoders = {} for col in cat_features: enc = LabelEncoder() enc.fit(pd.concat([train_df[col], test_df[col]]).astype(str).unique()) train_df[col] = enc.transform(train_df[col].astype(str)) test_df[col] = enc.transform(test_df[col].astype(str)) encoders[col] = enc # 分离特征与标签 X_train = train_df.drop('label', axis=1).values y_train = train_df['label'].values X_test = test_df.drop('label', axis=1).values y_test = test_df['label'].values # 标准化 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 转换成 PyTorch 张量格式 import torch X_train_t = torch.tensor(X_train, dtype=torch.float32).unsqueeze(1) # (N, 1, 41) X_test_t = torch.tensor(X_test, dtype=torch.float32).unsqueeze(1) y_train_t = torch.tensor(y_train, dtype=torch.long) y_test_t = torch.tensor(y_test, dtype=torch.long) print(f'训练集形状: {X_train_t.shape}, 测试集形状: {X_test_t.shape}') print(f'训练集攻击比例: {y_train.mean():.3f}')这里unsqueeze(1)是在第 1 维增加一个通道维度,PyTorch 的 Conv1d 要求输入形状是(batch, channels, length),41 个特征就是序列长度。print 出攻击比例是为了确认正负样本分布——NSL-KDD 训练集攻击占比约 53%,二分类下模型更容易学到有意义的判别边界。
2.4 类别不平衡:别让模型"偷懒"全预测成多数类
二分类问题里,如果直接拿原始数据训练,模型很容易倾向预测多数类。NSL-KDD 训练集二分类还算均衡,但 KDD99 或者 UNSW-NB15 就不是这样了。最简单的处理方式是在 DataLoader 里设置weights做加权采样,或者直接用CrossEntropyLoss的weight参数给少数类更高的惩罚权重。
一般做法是计算每个类别的样本数倒数作为权重,再归一化。比如多数类 10 万条,少数类 1 万条,少数类权重是多数类的 10 倍左右,这样模型在反向传播时少数类的梯度贡献更大。我习惯给正类的 weight 设为(1 - class_ratio) / class_ratio,效果直观可控。
3. CNN 模型设计:一维卷积怎么"卷"出一条流量的攻击特征
3.1 为什么选 CNN 而不是普通全连接网络
全连接网络把每个特征独立对待,学不到特征之间的局部组合关系。而 CNN 的本质是"局部感知 + 参数共享",卷积核在特征序列上滑动时,每一小段特征(比如src_bytes和dst_bytes的比值、count与serror_rate的组合)都被同一个卷积核扫描,相当于在提取局部模式。网络流量中很多攻击行为的特征确实体现在多个字段的组合上,这就是 CNN 在这个任务上合理的切入点。
相比 transformer 这类现代结构,CNN 参数少、训练快、在几千条样本上不容易过拟合,对本科毕设来说"够用且可控"。
3.2 网络结构:四层设计思路
我一般用这样一组结构:输入层 1×41 → 第一个卷积块(Conv1d + ReLU + BatchNorm + MaxPool)→ 第二个卷积块 → 全局平均池化 → Dropout → 全连接输出二分类。
卷积核大小和通道数按这个原则设:第一层用大一点的 KernelSize 覆盖更多特征区间,比如 kernel_size=5;第二层收窄到 kernel_size=3,提取更精细的模式。通道数从 64 起步翻倍到 128,不能太多,否则在小数据集上很容易过拟合。MaxPool 用 kernel_size=2,stride=2,把序列从 20 压缩到 10。
下面给出完整模型定义代码:
import torch.nn as nn class CNN1D(nn.Module): def __init__(self, input_dim=41, num_classes=2): super().__init__() self.conv_block1 = nn.Sequential( nn.Conv1d(1, 64, kernel_size=5, padding=2), # 保持长度不变 nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2, stride=2) # 长度 41 -> 20 ) self.conv_block2 = nn.Sequential( nn.Conv1d(64, 128, kernel_size=3, padding=1), nn.BatchNorm1d(128), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2, stride=2) # 长度 20 -> 10 ) self.global_pool = nn.AdaptiveAvgPool1d(1) # 长度 10 -> 1 self.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): x = self.conv_block1(x) x = self.conv_block2(x) x = self.global_pool(x).squeeze(-1) x = self.classifier(x) return x model = CNN1D() print(model)这里的padding=2和padding=1是为了让卷积操作不缩序列长度,让 MaxPool 来显式控制下采样。AdaptiveAvgPool1d(1)是全局平均池化,把最后一个卷积层的输出压缩成一个 128 维的向量,这一步好处是无论前面的特征图尺寸怎么变,全连接层的输入维度都固定。
3.3 残差连接要不要加:小网络别硬上
很多人一上来就想堆 ResNet 结构。对这个任务,我的观点很明确:如果网络总深度不超过 10 层,残差连接收益很小,反而增加参数量和调参难度。只有当你把网络加深到 5 个以上卷积块,或者数据集换到 UNSW-NB15 时,才考虑加残差。毕设场景先把基础结构跑出结果,再谈改进,别一开始就把自己困在"网络不收敛"的坑里。
3.4 激活函数与归一化:老话题但有讲究
ReLU 是默认选择,但在网络中部偶尔会出现 Dead ReLU 问题——某个神经元输出恒为 0,梯度永远为 0。可以换成 LeakyReLU 缓解,negative_slope 设 0.1 就够。BatchNorm 要用,它对这种特征范围差异大的输入作用明显,能让梯度传播稳定得多。有一点必须注意:BatchNorm 在训练和推理模式下的行为不同,PyTorch 中model.eval()会自动切换,测试前别忘了调用。
4. 训练与调参:让 loss 降得下去,让结果经得起问
4.1 训练流程设计与核心代码
训练流程按标准套路走:Adam 优化器,初始学习率 0.001,batch size 128,CrossEntropyLoss。训练轮次设 50 轮,但用早停机制,连续 7 轮验证集 loss 不降就停止,保存验证集 F1 最高的权重。每次训练都记录训练/验证的 loss 和精度曲线,做成图放在论文里,答辩时能讲故事。
import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset dataset = TensorDataset(X_train_t, y_train_t) loader = DataLoader(dataset, batch_size=128, shuffle=True) optimizer = optim.Adam(model.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss() # 可传 weight 参数处理不平衡 def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss, correct, total = 0, 0, 0 for xb, yb in loader: optimizer.zero_grad() out = model(xb) loss = criterion(out, yb) loss.backward() optimizer.step() total_loss += loss.item() * xb.size(0) pred = out.argmax(dim=1) correct += (pred == yb).sum().item() total += yb.size(0) return total_loss / total, correct / total model_cnn = CNN1D() optimizer = optim.Adam(model_cnn.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss() for epoch in range(30): train_loss, train_acc = train_one_epoch(model_cnn, loader, optimizer, criterion) print(f'Epoch {epoch+1:02d} | Loss {train_loss:.4f} | Acc {train_acc:.4f}')这段代码是核心骨架,实际项目中还需要加入验证集评估和早停逻辑。验证集可以在训练集里切出 10% 来做,如果不想切,也可以直接用官方测试集当验证,但这样的话早停的参考信号就偏乐观了——NSL-KDD 的测试集分布和训练集还是有一些差异的。
4.2 学习率、batch size 和 Dropout 的搭配关系
学习率设 0.001 配合 batch size 128 是稳定的起步组合。如果 loss 震荡严重,先把学习率降到 0.0005;如果训练集 loss 下降很慢,可能是 batch size 太大或学习率太小。Dropout 我设在 0.5,对全连接层有效,对卷积层建议不加或只加 0.1——卷积层本身参数少,BN 已经承担了正则化角色。
4.3 评估指标:准确率不是唯一标准
网络入侵检测是典型的类不平衡问题,准确率在攻击样本占比小时会严重失真。测试集上必须同时算 Precision、Recall、F1 和混淆矩阵。F1 是核心答辩指标,Recall 反映"漏报"的风险,Precision 反映"误报"的成本。入侵检测里漏报比误报严重得多,一条攻击流量被放过可能意味着整个系统被渗透,所以你答辩时要把 Recall 和 F1 放在准确率前面讲。
下面是测试评估代码,直接输出关键指标:
from sklearn.metrics import confusion_matrix, classification_report model_cnn.eval() with torch.no_grad(): pred_logits = model_cnn(X_test_t) y_pred = pred_logits.argmax(dim=1).numpy() print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=['Normal', 'Attack'], digits=4))在 NSL-KDD 测试集上,这个基础 CNN1D 的 F1 通常在 0.90 到 0.94 之间浮动。如果你跑出来低于 0.90,先查是不是数据预处理有问题,其次是训练轮次不够或者早停设得过严。如果高于 0.94,乐观点看,这个结构在公开数据集上已经够用了。
4.4 训练、验证、测试三者的边界别搞混
很多新手会把测试集反复拿来调参,这是数据泄漏。测试集只能碰一次,就是最终评估那次。验证集用来做早停和调超参数,应该从训练集中划分。如果数据总量小,用 5 折交叉验证也可以,但训练时间会乘以 5 倍。毕设场景直接固定切分更省力,别把自己的时间耗在交叉验证上。
5. 常见问题与避坑:能让你少熬五天的 5 条记录
5.1 准确率虚高但 F1 很低:你被类别不平衡骗了
现象:训练准确率很快就到 97%,但看混淆矩阵发现攻击类别的 Recall 只有 0.6。
原因:正负样本比例失衡时,模型学到的决策边界偏向了多数类,准确率被多数类贡献的样本数拉高了,少数类几乎全错。
解决:在CrossEntropyLoss里传入weight,值按torch.tensor([1.0, 3.0])或更高比例设,也可以用WeightedRandomSampler做样本重采样。注意验证时不要用训练时那套加权指标,评估用原样本分布上的 F1 和混淆矩阵,反映真实效果。
5.2 测试集效果暴跌:预处理信息泄漏了
现象:训练集 F1 0.96,测试集掉到 0.82。参数怎么调都回不来。
原因:这是最典型的翻车点。用的是全局归一化,比如拿全部数据fit了StandardScaler再划分训练/测试集,测试集的均值和方差参与了训练数据的归一化,模型隐式"看过"测试集了。
解决:划分数据后,只对训练集fit,再transform测试集。代码就是上面第 2 章的写法,顺序错了重写一次。
5.3 模型收敛慢或不收敛:数值范围完全没控制住
现象:loss 前几轮不下降,或者 loss 降到某个平台就死死卡住。
原因:最粗暴的可能是特征数值范围跨度太大,某些特征在 0 到 1 之间,某些在 1 到 10 万之间。CNN 卷积核的初始权重按小随机数初始化,大数值输入直接把梯度顶爆了。
解决:检查X_train是否做了标准化,看每个特征的均值是否接近 0、标准差是否接近 1。另一个隐藏问题是 LR 太高导致 loss 震荡不降,降到 0.0005 再试一轮。
5.4 训练到一半 loss 变成 NaN:踩到数值不稳定
现象:某个 epoch 之后 loss 输出的都是 nan,acc 也变成 0。
原因:常见于三个地方:学习率过大导致梯度爆炸;Adam默认参数在极端梯度下飞了;输入里有 NaN 值——CSV 文件里某些缺失字段被 pandas 读成 NaN,参与计算后污染了梯度。
解决:PR 文件里df.replace([np.inf, -np.inf], np.nan)然后dropna(),保证喂给网络的没有脏数据。再把学习率从 0.001 降到 0.0005 试跑。用torch.clamp也可以限制梯度值,但治标不治本。
5.5 测试时结果与训练差很远:Dropout 和 BN 的开关没切换
现象:训练集效果惊艳,测试集评估时结果却诡异得差,甚至出现全预测成一个类别的情况。
原因:写评估代码时忘了切model.eval()。Dropout在训练时随机失活,推理时保留全部神经元;BatchNorm在训练时用 batch 统计量,推理时用累计统计量。没切切换状态,测试时运行在训练模式,输出是随机的。
解决:推理和评估前先model.eval(),恢复训练前再model.train()。这个坑几乎每届都有人踩,不是玄学,是状态开关。
6. 进阶验证方法:让答辩从"我跑了个模型"变成"我验证了有效性"
训练完模型、拿到指标不是终点。答辩时老师大概率会问一个直击灵魂的问题:"你凭什么说 CNN 学到了攻击特征,而不是在背训练集?" 准备两手证据就能应对。
第一招是用 t-SNE 做特征可视化。把训练好的模型在测试集上跑一次,抽取最后一层卷积输出的 128 维特征向量,用 t-SNE 降到二维平面画散点图。如果 Normal 和 Attack 两个类别在投影上形成了清晰可分的簇,说明模型的表征空间确实把两类流量区分开了。t-SNE 的perplexity不要默认设 30,对这种二分类任务数据量又不大的情况,设 10 到 15 效果更干净。
第二招是做一个简单的对比实验,证明 CNN 比基线模型强。以随机森林或 MLP 为基线,用同样的特征工程跑一轮对比。MLP 用一个隐藏层 128 维的网络,随机森林用n_estimators=100,对比表里写清楚三个模型的 F1 和 Recall。CNN 只要能更高,你从"结构选择"角度做出的解释就有数据支撑了。如果差距不大,也别慌,答辩的时候直接说"CNN 的优势在于端到端特征提取,同等特征工程下表现稳定",再补一句可以联合 MLP 做集成。
最后补一个我自己养成的小习惯:把所有实验的种子固定好——torch.manual_seed(42)、np.random.seed(42),训练代码里固定随机种子,这样每次跑的结果可以被复现。我见过太多同学因为没固定种子,实验 A 和实验 B 的结果差异比模型结构差异还大,写论文的时候根本解释不了。毕设做完后,把随机种子和超参数表整理进附录,这是加分项,也是自己给自己留的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取