基于CNN的网络入侵检测:数据处理与评估指标比准确率更重要 简介面向计算机专业毕业设计及课程设计需求基于CNN卷积神经网络的网络入侵检测Python源码与全量数据包可供学习者直接参考。项目经导师指导并认可评审98分覆盖数据预处理、模型定义、训练、预测及结果可视化完整流程可有效解决网络入侵检测场景中的特征提取与分类问题。压缩包内含33个文件以4个Python脚本如PreHandle.py、Train.py、Predict.py、CNNMould.py为核心搭配12个csv数据文件、训练好的best_model.pth权重、准确率与精确度图像、项目配置XML及README文档整体约21.58MB结构清晰便于对照学习。已有767人学习下载适合正在完成大作业、期末项目或需要实战练习的计算机专业学生。通过该资源可掌握CNN在入侵检测中的应用思路获得可直接运行与二次开发的工程代码同时结合NSL-KDD数据集理解数据预处理与模型调优的完整环节。1. 基于CNN卷积神经网络的网络入侵检测为什么先把准确率忘掉如果你从某个“Python毕业设计基于CNN卷积神经网络的网络入侵检测源码全部数据”压缩包解压第一次运行就看到准确率99.2%先别急着截图保存这个数字在真实的网络入侵检测场景里几乎必然是假象。这个方向真正考验人的地方不在卷积网络本身而在三个环节数据预处理是否公平、极度不平衡的类别分布是否被处理、评估指标是否经受得住反问。这里我按从数据到模型的顺序把NSL-KDD这一路走通代码和参数都会拆开讲并把最容易翻车的位置提前指出来。适合正在做Python毕业设计、想用CNN做网络入侵检测、且需要一份可复现源码结构的同学。先说结论网络入侵检测数据集里正常流量占一半以上U2R这类高危攻击类别可能只有几十条样本。模型只要全部预测成正常流量准确率就能轻松过半如果预处理再出点数据泄露测试集准确率很容易被推高到99%。所以整篇文章围绕一句话展开你怎么把数据处理公平、把模型评估说明白而不是盯着单点准确率自嗨。2. 数据预处理是第一道分水岭NSL-KDD 的41维特征如何编码、标准化与切分2.1 数据集选型NSL-KDD、KDD99 与 CICIDS2017 怎么挑网络入侵检测领域能直接下载使用的公开数据集不少但彼此差异很大。KDD99是1999年的原始版本包含大约490万条连接记录里面存在大量重复某些攻击类型在训练集和测试集里重复出现模型很容易“背诵”而不是“学习”。NSL-KDD是2009年Tavallaee等人对KDD99去重之后重新划分的版本训练集125973条、测试集22544条测试集里基本没有与训练集重复的样本作为论文数据来源更干净。CICIDS2017更接近真实流量特征维度有80多个数据文件本身就有几十GB前期清洗成本高不适合作为毕业设计的第一版方案。UNSW-NB15也不错49个特征、覆盖9类攻击但同样比NSL-KDD重。数据集规模特征数毕业设计起步建议KDD99约490万条41不推荐重复样本太多指标虚高NSL-KDD训练125973 / 测试2254441首选论文和复现资料最全CICIDS2017约285万条几十GB约80可作为进阶方向清洗成本高UNSW-NB15约250万条49适合想换数据集的场景略重我一般建议先把NSL-KDD整条流水线跑通再谈是否迁移到UNSW-NB15或自己抓的流量上。因为后面所有讨论都围绕41维特征展开换数据集时只需要改读文件和标签映射模型主体不用动。2.2 数值列、字符列与标签列一套能直接跑的 Python 预处理代码NSL-KDD每条记录的原始形态是41个逗号分隔字段加一个攻击类型标签。41维里大部分是数值列比如src_bytes、count、serror_rate另有3个字符列protocol_type、service、flag。标签有几十种攻击名需要先合并成5大类normal、dos、probe、r2l、u2r。预处理代码我一般这样写import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, StandardScaler FEAT_COLS [ 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 ] attack_map { normal: normal, neptune: dos, back: dos, land: dos, pod: dos, smurf: dos, teardrop: dos, apache2: dos, mailbomb: dos, processtable: dos, udpstorm: dos, ipsweep: probe, nmap: probe, portsweep: probe, satan: probe, mscan: probe, saint: probe, guess_passwd: r2l, ftp_write: r2l, imap: r2l, multihop: r2l, named: r2l, phf: r2l, sendmail: r2l, snmpgetattack: r2l, snmpguess: r2l, spy: r2l, warezclient: r2l, warezmaster: r2l, buffer_overflow: u2r, loadmodule: u2r, perl: u2r, rootkit: u2r } def load_nsl(path): df pd.read_csv(path, headerNone, namesFEAT_COLS [attack_type, difficulty]) df[label] df[attack_type].map(attack_map) df df.drop(columns[attack_type, difficulty]) return df这段代码做的事情很直接先把CSV读成DataFrame给41列特征加上名字再把攻击名映射到5个大类最后丢掉attack_type和difficulty两列。映射表必须把所有训练集里出现的攻击名都覆盖否则map之后会出现NaN模型输入直接出问题。如果你拿到的是原始KDD99文件攻击名会更多映射表需要按实际数据补齐。接下来是编码和标准化。字符列转数字数值列做方差缩放这里有一个关键点所有编码器只能用训练集去fit测试集只能transform。def preprocess_train_test(train_path, test_path): train_df load_nsl(train_path) test_df load_nsl(test_path) cat_cols [protocol_type, service, flag] num_cols [c for c in FEAT_COLS if c not in cat_cols] # 字符列训练集 fit测试集只 transform encoders {} for col in cat_cols: le LabelEncoder() # 两个集合一起 fit防止 test 出现训练集没见过的取值 le.fit(pd.concat([train_df[col], test_df[col]], axis0)) train_df[col] le.transform(train_df[col]) test_df[col] le.transform(test_df[col]) encoders[col] le # 数值列同样只统计训练集的均值/方差 scaler StandardScaler() train_num scaler.fit_transform(train_df[num_cols]) test_num scaler.transform(test_df[num_cols]) X_train np.hstack([train_num, train_df[cat_cols].values], dtypenp.float32) X_test np.hstack([test_num, test_df[cat_cols].values], dtypenp.float32) y_train train_df[label].values y_test test_df[label].values return X_train, X_test, y_train, y_test这里有两个常见的实现细节。第一字符编码我把训练集和测试集拼在一起fit是为了防止测试集里有训练集没出现过的service取值。更严格的写法是只对训练集fit测试集里遇到未知值时报错但对于毕业设计合并fit更省事也能保证维度一致。第二StandardScaler必须用训练集统计量不能在全量数据上fit否则就构成数据泄露后面第5章会专门展开。预处理完成后X_train的形状是(样本数, 41)y_train仍然是字符串标签。下一步要么把它转成数字标签要么在模型里用LabelEncoder做映射。为了避免后续多次转换建议在预处理函数里同时返回LabelEncoder。2.3 类别分布统计与训练/测试划分先看清楚再谈模型NSL-KDD训练集里normal大约6.7万条、dos大约4.6万条、probe大约1.2万条而r2l只有995条u2r只有52条。这个分布决定了你的模型会严重偏向多数组。拿到数据第一件事就是打印类别分布from collections import Counter def print_dist(y, nametrain): cnt Counter(y) total len(y) for k, v in sorted(cnt.items()): print(f{name} {k}: {v} ({v/total:.2%}))u2r占比不到0.05%如果你什么都不做模型学不到这个类别。训练和测试划分也要保持类别比例from sklearn.model_selection import train_test_split X_tr, X_va, y_tr, y_va train_test_split( X_train, y_train, test_size0.2, random_state42, stratifyy_train )stratifyy_train会让验证集的类别比例和训练集一致避免随机切分时把u2r样本全分到验证集里。切完之后再把标签转成数字后面模型才能吃进去。到这里数据侧的准备工作就完成了接下来才轮到模型设计。3. 一维 CNN 结构设计从输入张量到卷积核、池化与类别权重的落地选择3.1 为什么这个场景用 Conv1D 而不是把特征拼成图片很多人拿到41维特征第一反应是reshape成7×6的“图片”扔给二维CNN。这个做法其实站不住脚二维CNN依赖空间局部性而NSL-KDD的特征顺序本身没有空间语义把duration放在protocol_type旁边和放在count旁边对模型来说是无意义的。强行排成图片卷积核跨的是“特征的任意排列”模型学到的是偶然结构既难解释也不稳定。一维卷积把每条连接看作长度41的序列卷积核沿特征维度滑动天然捕捉相邻特征之间的依赖比如count和srv_count的差异、serror_rate和rerror_rate的协同。对网络流量这种“一组统计量同时出现”的数据一维CNN的归纳偏置比二维CNN更合适。另外41维长度很短一维CNN参数量小、训练快笔记本CPU也能跑出能用的结果如果换成LSTM训练速度会明显变慢对毕业设计来说性价比不高。3.2 搭一个能训练起来的 CNN结构、初始化与关键参数我常用的结构是三段卷积加两个全连接层整体非常朴素但稳定from tensorflow.keras.models import Sequential from tensorflow.keras.layers import ( Conv1D, MaxPooling1D, Flatten, Dense, Dropout, BatchNormalization, Input ) def build_cnn(input_dim41, num_classes5): model Sequential([ Input(shape(input_dim, 1)), Conv1D(filters64, kernel_size5, activationrelu, paddingsame), BatchNormalization(), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size3, activationrelu, paddingsame), BatchNormalization(), MaxPooling1D(pool_size2), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(num_classes, activationsoftmax) ]) return model第一层Input(shape(41, 1))意味着每条样本是长度41、通道数为1的序列。Conv1D第一层用64个卷积核、核大小5表示每次看相邻5个特征的局部窗口paddingsame保证输出长度不萎缩。BatchNormalization放在卷积层后面缓解中间层激活值分布偏移两段池化把长度从41压到10最后展平接全连接层。参数选择有讲究。kernel_size不建议超过7因为特征一共41维核太大会把局部细节一把抓光。filters从64涨到128即可再往256以上加训练时间线性上涨但准确率提升通常小于1个百分点。Dense(128)和Dropout(0.5)是防过拟合的标配组合如果训练loss一直在降但验证loss不动可以把Dropout提到0.6或者减少Dense神经元数到64。注意源码包里的模型经常堆得很深但在这个任务上深度不是关键数据分布才是。结构越简单越好复现、越好写论文。3.3 类别不平衡处理class_weight、过采样与损失函数的取舍直接拿原始类别分布去训练模型会把样本全预测成normal和dosr2l与u2r的召回率直接为0。最常见的处理是用compute_class_weight给每个类别加权from sklearn.utils.class_weight import compute_class_weight classes np.unique(y_train) weights compute_class_weight(balanced, classesclasses, yy_train) class_weight dict(zip(classes, weights)) print(class_weight)balanced的算法是样本总数除以类别数再除以该类别样本数。对于u2r权重会算到400多。这个数值直接传给Keras大概率训练到第三个epoch损失就开始飙升接着变成NaN因为交叉熵梯度被放大得太凶。所以我不建议直接无脑用balanced结果。先打印出来再手动压一下上限# 把权重上限压到 30防止收敛发散 class_weight {k: min(v, 30) for k, v in class_weight.items()}另外可以配合SMOTE过采样把r2l和u2r在训练集里合成到500条以上。SMOTE在数值特征上效果稳定因为数据已经完成了标准化近邻距离是有意义的。先过采样、再训练比单纯调大权重更容易收敛。如果你嫌调权重太玄学就先用SMOTE把少数类扩到至少500条然后再用balanced权重一般可以得到明显改善。4. 训练、保存与评估用混淆矩阵和每类召回率代替单点准确率4.1 训练循环、早停与模型保存一次能完整跑完的最小流程数据准备好了模型搭好了接下来是最小可运行流程。标签要先转成one-hot输入要补一个通道维度然后编译、训练from tensorflow.keras.utils import to_categorical from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint # 标签编码字符串 - 数字 - one-hot label_encoder LabelEncoder() label_encoder.fit(np.concatenate([y_train, y_test])) y_train_enc label_encoder.transform(y_tr) y_valid_enc label_encoder.transform(y_va) y_test_enc label_encoder.transform(y_test) y_train_cat to_categorical(y_train_enc, num_classes5) y_valid_cat to_categorical(y_valid_enc, num_classes5) # 输入补通道维度(n, 41) - (n, 41, 1) X_tr_cnn X_tr[..., np.newaxis].astype(np.float32) X_va_cnn X_va[..., np.newaxis].astype(np.float32) X_test_cnn X_test[..., np.newaxis].astype(np.float32) model build_cnn(input_dimX_train.shape[1], num_classes5) model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] ) callbacks [ EarlyStopping(monitorval_loss, patience8, restore_best_weightsTrue), ModelCheckpoint(best_cnn.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X_tr_cnn, y_train_cat, validation_data(X_va_cnn, y_valid_cat), epochs50, batch_size256, class_weightclass_weight, callbackscallbacks )这里几个要点。早停监控的是val_loss而不是val_accuracy因为准确率在类别不平衡时太容易虚高loss更敏感patience8意思是验证集loss连续8轮不创新低就停。restore_best_weightsTrue会把模型权重回滚到验证集loss最低的那一轮避免最后几个过拟合epoch覆盖好结果。ModelCheckpoint只保存验证集最优模型防止训练中断丢了进度。batch_size256是速度和稳定性的折中。样本量12万多256的batch能跑得快梯度的方差也足够小如果你发现loss震荡得厉害降到128或者128以下。4.2 混淆矩阵与分类报告模型到底漏了哪类攻击训练完成后第一件事不是打印准确率而是生成分类报告和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred model.predict(X_test_cnn) y_pred_class np.argmax(y_pred, axis1) CLASS_NAMES [normal, dos, probe, r2l, u2r] print(classification_report(y_test_enc, y_pred_class, target_namesCLASS_NAMES))在NSL-KDD测试集上一维CNN的典型表现是normal和dos的召回率能到0.9上下probe大概0.7到0.8但r2l和u2r的召回率经常在0到0.2之间。这个结果并不算失败它就是你应该写进论文里的baseline。下一步基于这个结果去改进比如加上SMOTE、调整结构、融合LSTM每改一步只看r2l和u2r的召回有没有真实提升。混淆矩阵图也要生成import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(y_test_enc, y_pred_class) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, xticklabelsCLASS_NAMES, yticklabelsCLASS_NAMES) plt.xlabel(Predicted) plt.ylabel(True) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)heatmap里对角线越亮越好u2r那一行如果不是0说明你的不平衡处理有效果。答辩时把这张图往论文目录页一放比任何文字都直接。4.3 训练曲线怎么看过拟合信号在验证集上的表现model.fit返回的history对象记录了每个epoch的训练和验证指标通常要画两张曲线图loss曲线和accuracy曲线。plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.savefig(loss_curve.png, dpi150)如果训练loss一路下降、验证loss始终不动说明模型过拟合优先检查数据泄露和类别权重。如果两条线同步下降然后验证loss在第30轮回升是典型过拟合早停会把它挡在第30轮左右这是正常信号。答辩时能解释清楚“为什么在第N轮停止”比只贴一个最终准确率好得多。5. 网络入侵检测项目排查最容易翻车的 5 个现场问题5.1 数据泄露标准化时把测试集统计量也卷了进去现象测试集准确率高达99%分类报告漂亮得不像话。原因很多源码在预处理时把训练集和测试集拼到一起再对整个DataFrame做StandardScaler().fit_transform()或者对全量数据先fit再切分。这样测试集的均值和方差参与了标准化模型在训练阶段就“见过”测试集的整体统计信息属于典型数据泄露。解决StandardScaler只对训练集fit测试集只能transform。验证集同理。这是整个实验公平性的地基没有讨价还价空间。5.2 class_weight 设太大训练直接 NaN 或发散现象前三个epoch的loss从2.0涨到几十第四个epoch直接变成nan。原因compute_class_weight(balanced)给u2r算出400以上的权重交叉熵梯度被放大优化过程直接崩了。解决打印出权重值手动把上限压到30以内或者放弃权重、改用SMOTE先把少数类样本合成到500条以上。实在要调就从小权重开始往上试记录每次的验证集u2r召回。5.3 input_shape 对不上训练时报错预测时也报错现象训练时报Negative dimension size caused by subtracting 5 from 1或者加载模型预测时报expected input to have 3 dimensions, but got array with shape (41,)。原因预处理得到的是(n, 41)二维数组而Conv1D期望(n, 41, 1)。部分源码里只在训练时手动reshape预测时却忘了补通道维度。解决统一在预处理最后加X X[..., np.newaxis].astype(np.float32)并且一次性写到数据保存和模型加载两个地方。5.4 用了 KDD99 原始数据而不是 NSL-KDD指标虚高现象准确率95%以上r2l和u2r召回率也很高但换了NSL-KDD后直接掉到80%以下。原因KDD99测试集与训练集存在大量重复记录模型记住了样本而非学习到模式导致评估结果失真。解决统一用NSL-KDD的KDDTrain.txt和KDDTest.txt。论文实验数据来源写清楚答辩前主动说明使用的是去重版本。5.5 只打印 accuracy 就下结论答辩被问住现象论文里写“模型准确率达到92%”老师问“U2R的召回率多少”当场答不上来。原因单点准确率在极度不平衡的数据上没有区分度全部预测成normal就有53%左右的准确率根本不能说明模型学会了识别攻击。解决在论文结果里附上classification_report、混淆矩阵、每类召回率和精确率表并明确给出u2r和r2l的单独数值哪怕数字不漂亮也要如实写然后再写改进实验。6. 复现与验证的最小路径把整套源码压缩成一个 quick_check 脚本6.1 quick_check五轮训练验证数据、模型与预测链路正式训练前建议先跑一个只训练5轮的“探路脚本”目的是验证数据读取、标签映射、维度变换、模型构造整条链路没有断点。命令我一般这样组织python preprocess.py \ --train KDDTrain.txt \ --test KDDTest.txt \ --out ./processed python quick_check.py --epochs 5 --batch_size 64preprocess.py负责把原始文本变成X_train.npy、X_test.npy、y_train.npy、y_test.npy。quick_check.py内部加载预处理产物一次性完成编码、补维度、建模型、训练5轮、输出分类报告。这个脚本只要能在两分钟内跑完说明链路通了后面再挂完整训练。注意看三个位置终端是否有数据形状输出比如Train shape: (100778, 41, 1)训练时第一轮loss是否比第二轮明显偏高分类报告是否把5个类全部列出来。如果分类报告只有3个类说明标签映射表漏了攻击名需要回头检查attack_map。6.2 面对 CNN 黑匣子答辩时怎么组织证据很多同学做到最后发现自己根本解释不了CNN内部学到了什么这很正常卷积网络本来就是黑匣子。不用强行做Grad-CAMNSL-KDD特征本身就是统计量没有图片那样的空间语义可视化解释的价值有限。更实际的做法是准备两类证据一是错误样本分析从测试集里挑出被预测错的样本按真实攻击类型排序对比特征值的差异说明哪些特征让模型混淆二是每类别的精确率、召回率、F1表格以及ROC曲线或AUC值这些是评审老师最容易接受的客观结果。我自己最开始做这套方案时恨不得把模型堆到几十层把准确率刷到99%结果一次内部评审被问到“u2r召回率只有0.2%你打算怎么解释”当场没法回答。从那之后我养成一个习惯每次跑完实验第一眼一定看混淆矩阵第二眼看分类报告然后才是总准确率。方向对不对看这两个就够。希望帮到你。本文还有配套的精品资源点击获取