
简介这是一份基于CNN卷积神经网络的网络入侵检测Python毕业设计项目专为计算机相关专业学生完成课程设计、期末大作业或毕业设计而准备也适合希望掌握入侵检测实战的入门学习者。项目以NSL-KDD数据集为依托完整覆盖数据预处理、模型搭建、训练评估与推理预测全流程包含源码、全部数据、训练好的模型权重以及精度曲线、混淆矩阵等可视化结果预处理阶段涉及标准化与编码转换模型部分包含卷积、池化、全连接层及评估指标便于读者快速复现实验、理解算法细节并在此基础上扩展研究。压缩包共33个文件以Python脚本、CSV数据文件、XML工程配置、PNG/JPG图表和Markdown说明为主整体约21.58MB目录结构清晰训练与预测入口明确。截至当前已有767人学习下载项目为经导师认可的高分毕业设计完成度与实用性均较高适合作为项目实战参考和论文写作支撑。1. 用CNN做网络入侵检测这件事到底卡在哪毕设里最常见的翻车现场不是模型不收敛而是你把一个分类器从头搓到尾最后发现评委一句“这和传统机器学习有什么区别”就把你问住。用 CNN 做网络入侵检测核心卖点是把它当成一维序列让卷积核自动学特征组合不再依赖手工构造特征。这个方向适合两类人一是还没定题的网络安全或计算机方向毕业生想找数据好拿、效果能写进论文又不烂大街的题目二是已经在用 SVM、随机森林做流量分类、想看看深度学习方法实际能差多少的从业者。下面直接按数据准备、模型搭建、训练评估、避坑和部署演示的顺序把整个流程拆开讲。2. 把NSL-KDD变成能喂给CNN的张量数据准备全流程2.1 数据集选型为什么我推荐用 NSL-KDD入侵检测方向的公开数据集不少但真正适合在一两个月内跑完的NSL-KDD 是首选。KDD99 原始版本有四百万到五百万条记录量大但冗余严重重复记录会让训练集和测试集很像最后准确率虚高写论文时解释起来很别扭。CICIDS2017 更接近真实网络特征有 80 多个但类别极不平衡很多攻击类型只有几千条预处理成本高如果毕业设计时间紧很容易卡在数据清洗上。对比之下NSL-KDD 是 KDD99 的清理版去掉了重复记录训练集约十二点六万条测试集约两万两千条训练集和测试集的分布有差异更能反映模型的泛化能力。攻击类型也覆盖了四类DoS、Probe、R2L、U2R加上 Normal一共五类做多分类和二分类都合适。文件本身是 CSV 格式但没有表头最后一列是标签倒数第二列是难度等级读进来时要把列名手动加上。2.2 41 维特征里哪些是数值、哪些是枚举NSL-KDD 每一条连接记录有 41 个特征可以分成四组基本特征duration、protocol_type、service、flag、src_bytes、dst_bytes 等前 9 个内容特征hot、num_failed_logins、logged_in、num_compromised 等13 个主要描述与连接内容相关的行为基于时间的流量统计特征count、srv_count、serror_rate、srv_serror_rate 等9 个基于主机的流量统计特征dst_host_count、dst_host_srv_count、dst_host_same_src_port_rate 等10 个。字符串类型的特征只有三个protocol_type、service、flag。protocol_type 一般是 TCP、UDP、ICMPservice 是目标服务类型像 http、ftp、ssh 这些flag 是连接状态标记比如 SF 表示正常结束REJ 表示拒绝。其余 38 个特征都是数值型。CNN 处理这些数据时不需要像传统方法那样做太多特征筛选但类别型特征必须编码成数字这一点是预处理环节最容易漏掉的。2.3 读取、编码与标准化一套能直接跑的代码用 Pandas 读入时先把 41 个特征名定义好然后把三个字符串列单独处理。下面的代码是我平时跑这套流程的固定写法直接复制改路径就能用。import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, StandardScaler FEATURES [ 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, class ] train_df pd.read_csv(KDDTrain.txt, headerNone, namesFEATURES) test_df pd.read_csv(KDDTest.txt, headerNone, namesFEATURES) cat_cols [protocol_type, service, flag] # 先统一去掉可能存在的空格再转成 category for df in [train_df, test_df]: for c in cat_cols: df[c] df[c].str.strip().astype(category) # 用训练集的类别集合约束测试集防止出现训练时没见过的类别 for c in cat_cols: known_cats list(train_df[c].cat.categories) train_df[c] train_df[c].cat.set_categories(known_cats) test_df[c] test_df[c].cat.set_categories(known_cats) le LabelEncoder() train_df[c] le.fit_transform(train_df[c]) test_df[c] le.transform(test_df[c])这里有个关键点LabelEncoder 一定要在训练集上 fit再去 transform 测试集。如果直接在测试集上重新 fit类别编号可能和训练集不一致比如 TCP 在训练集里是 0在测试集里变成了 2CNN 会把同一个协议类型当成完全不同的含义。用 set_categories 把测试集的 category 集合强制改成训练集的集合就是为了保证两边编码一致。特征准备好之后把类别列和标签分开然后做标准化。label_col class X_train train_df.drop(columns[label_col]).values.astype(np.float32) X_test test_df.drop(columns[label_col]).values.astype(np.float32) # 标准化只在训练集上 fit再应用到测试集 scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) # 加一个通道维度给 Conv1D 用 X_train X_train.reshape(-1, 41, 1) X_test X_test.reshape(-1, 41, 1) np.save(X_train.npy, X_train) np.save(X_test.npy, X_test)标准化是另一个容易踩雷的地方。StandardScaler 必须在训练集上 fit再用同一个scaler去 transform 测试集。有人图省事把两个数据集拼在一起再 fit这会把测试集的均值方差信息泄漏给模型最后得到的评估结果会偏乐观。reshape 成 (-1, 41, 1) 是因为 Conv1D 需要的输入形状是 (样本数, 特征长度, 通道数)这里每个样本是一个长度 41 的向量单通道。2.4 标签映射二分类还是五分类NSL-KDD 的标签有 normal 和几十种攻击名称不能直接丢给模型做多分类否则类数太多、部分类别样本极少模型根本学不动。常见做法是按攻击家族映射成五类或者干脆做二分类。attack2class { normal: normal, back: dos, land: dos, neptune: dos, pod: dos, smurf: dos, teardrop: dos, apache2: dos, udpstorm: dos, processtable: dos, worm: dos, satan: probe, ipsweep: probe, nmap: probe, portsweep: probe, mscan: probe, saint: probe, guess_passwd: r2l, warezmaster: r2l, warezclient: r2l, imap: r2l, ftp_write: r2l, multihop: r2l, phf: r2l, spy: r2l, xlock: r2l, snmpguess: r2l, snmpgetattack: r2l, httptunnel: r2l, named: r2l, sendmail: r2l, xterm: r2l, buffer_overflow: u2r, loadmodule: u2r, perl: u2r, rootkit: u2r, ps: u2r, sqlattack: u2r, xsnoop: u2r, ntinfoscan: u2r } def to_five_class(s): return attack2class.get(s.strip(), unknown) train_df[label5] train_df[class].apply(to_five_class) test_df[label5] test_df[class].apply(to_five_class) class_names [normal, dos, probe, r2l, u2r] label2id {c: i for i, c in enumerate(class_names)} y_train train_df[label5].map(label2id).values y_test test_df[label5].map(label2id).values如果做二分类把 normal 记为 0其他所有攻击记为 1 就行最后一层用 sigmoid损失换成 binary_crossentropy。五分类能写进论文里的信息量更多推荐优先做五分类。这里还留了一个隐患R2L 和 U2R 的样本非常少后面训练时要用 class_weight 或者换损失函数来平衡否则模型会把这两类几乎全部错判成 normal。3. 搭一个能跑通的1D-CNN网络结构设计与参数选择3.1 为什么网络流量用一维卷积而不是 LSTM 或 SVM很多人听到 CNN 第一反应是图像识别其实网络流量数据是一个 41 维的特征向量天然适合一维卷积。Conv1D 在维度方向上滑动窗口相当于每次看相邻的几个特征捕捉局部组合关系。比如 dst_host_same_srv_rate 和 dst_host_srv_diff_host_rate 这类统计特征位置相邻时卷积核能学到“同一个服务下不同主机占比”和“同一个源端口访问不同主机占比”之间的关系这些组合对判断扫描行为很有用。和 LSTM 对比LSTM 擅长处理长序列前后依赖但网络流特征不是严格的时间序列硬套 LSTM 训练慢、参数多在 NSL-KDD 这种 41 维短特征上优势不明显反而容易过拟合。和 SVM 对比SVM 需要做特征选择和核函数调参CNN 把这些工作变成了卷积核自动学习代价是需要更多数据来支撑训练。换句话说用 CNN 做入侵检测的价值不是“一定比 SVM 准”而是省去了大量特征工程在数据量充足的攻击类别上表现更稳定。3.2 一个两层 Conv1D 的基础网络结构毕设场景不建议把网络搭得太大两层 Conv1D 加一层全连接已经能在这个数据集上得到不错的结果。第一层卷积提取基础特征组合第二层卷积在第一层输出基础上继续抽象池化层降低特征维度Dropout 防止过拟合。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, BatchNormalization, Dropout, Flatten, Dense def build_cnn(input_dim41, num_classes5): model Sequential(namecnn_network_intrusion) model.add(Conv1D( filters64, kernel_size3, paddingsame, activationrelu, input_shape(input_dim, 1) )) model.add(BatchNormalization()) model.add(MaxPooling1D(pool_size2)) model.add(Conv1D( filters128, kernel_size3, paddingsame, activationrelu )) model.add(BatchNormalization()) model.add(MaxPooling1D(pool_size2)) model.add(Flatten()) model.add(Dropout(0.5)) model.add(Dense(128, activationrelu)) model.add(Dense(num_classes, activationsoftmax)) return model model build_cnn(input_dim41, num_classes5) model.summary()第一层卷积的输入是 (41, 1)41 是特征长度1 是通道数。paddingsame 让卷积后长度不变方便后续池化。filters64 表示第一层有 64 个卷积核每个卷积核相当于在学一种特征组合模式。kernel_size3 是窗口大小表示每次看相邻 3 个特征。第二层 filters 从 64 加到 128通道数翻倍因为经过池化后特征图变小需要更多卷积核来提取不同角度的特征。3.3 几个必调参数的经验值kernel_size 是最先要试的参数。3 和 5 是常用的两个值41 维特征长度不算长kernel_size 设 3 计算量小设 5 能覆盖更宽的局部范围但也不是越大越好。我试过 kernel_size7训练波动变大测试集准确率反而掉了 1 到 2 个百分点因为卷积窗口过宽会把不相关的特征强行组合在一起引入噪声。pool_size 用 2和 kernel_size3 搭配每经过一次池化特征长度减半。两次池化后41 维输入变成 10 维左右最后接全连接层时参数数量不会爆炸。Dropout 放在全连接层之前而不是卷积层之间0.5 这个值在入侵检测这种中等规模数据上表现稳定。BatchNormalization 放在每次卷积之后、激活之前作用是让每层输入分布稳定学习率可以放心用 0.001。model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] )这里用 sparse_categorical_crossentropy 是因为标签是整数编号而不是 one-hot 向量可以直接省掉 to_categorical 这一步。如果做二分类把最后一层激活函数换成 sigmoidloss 换成 binary_crossentropy。4. 训练与评估别让准确率骗了你4.1 训练配置早停、学习率衰减、模型保存训练 CNN 做入侵检测时我一般不会硬跑满固定 epoch 数。更可靠的做法是配好早停和学习率衰减让模型在验证集不再变好的时候自动停下来同时把最好的权重保存下来。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks [ EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue ), ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ), ModelCheckpoint( best_model.h5, monitorval_accuracy, save_best_onlyTrue ) ] history model.fit( X_train, y_train, batch_size64, epochs60, validation_data(X_test, y_test), callbackscallbacks )EarlyStopping 的 patience8 表示如果连续 8 个 epoch 验证集损失没有下降就停止训练并恢复到验证损失最小的权重。ReduceLROnPlateau 在验证损失连续 3 个 epoch 不降时把学习率减半这样训练后期可以用更细的步长逼近最优解。ModelCheckpoint 只保存验证准确率最高的那次权重防止最后几个 epoch 过拟合把好的权重覆盖掉。batch_size64 是默认的起步值显存不够就降到 32。epochs 设 60 只是上限实际早停一般会在 20 到 30 个 epoch 触发因为 NSL-KDD 数据量不大CNN 收敛很快。4.2 评估模型看混淆矩阵而不是只看准确率NSL-KDD 的测试集里 normal 和 dos 占了大头r2l 和 u2r 样本很少。如果一个模型把所有样本都判成 normal准确率也能到 50% 以上这个数字没有任何说服力。所以评估时必须输出每个类别的精确率、召回率和 F1 值再看混淆矩阵。from sklearn.metrics import confusion_matrix, classification_report import numpy as np y_pred np.argmax(model.predict(X_test, batch_size64), axis-1) print(classification_report( y_test, y_pred, target_namesclass_names, digits4 )) print(confusion_matrix(y_test, y_pred))classification_report 会给每个类别单独的 precision、recall、f1-score。对于入侵检测recall 比 precision 更关键漏掉一次攻击的代价比误报一次正常流量更大。所以调参的目标应该是让 dos、probe、r2l、u2r 四类的 recall 尽量高而不是只盯着总准确率。混淆矩阵能直观看出哪两类容易被混淆常见结果是 r2l 大量被错判成 normalu2r 几乎全军覆没。4.3 类别不平衡class_weight 与 focal loss如果跑完发现 r2l 和 u2r 的召回率是 0说明模型彻底放弃了这两个类。常规解决方法是给少数类更高的权重让模型把少数类判错时付出更大的损失。from sklearn.utils.class_weight import compute_class_weight class_weight compute_class_weight( class_weightbalanced, classesnp.array([0, 1, 2, 3, 4]), yy_train ) class_weight_dict {i: w for i, w in enumerate(class_weight)} model.fit( X_train, y_train, batch_size64, epochs60, validation_data(X_test, y_test), callbackscallbacks, class_weightclass_weight_dict )compute_class_weight 会按样本数量的反比自动算出权重。比如 u2r 训练集里只有几十条权重可能到几十normal 有几万条权重不到 1。加 class_weight 之后r2l 和 u2r 的召回率会明显提升但也要注意副作用少数类权重过大可能会把一些 normal 样本误判成攻击precision 会下降。如果加了 class_weight 还不行可以试 focal loss它对难分类样本的关注更强。不过对于毕设来说class_weight 已经足够说明你处理过类别不平衡问题写进论文里也是一个完整的实验对比。5. 避坑指南五个让入侵检测模型翻车的隐蔽问题5.1 数据泄漏准确率虚高的头号原因现象训练时准确率一路涨到 97% 以上测试集准确率也很高换一份真实数据推理时效果惨不忍睹。原因标准化时把训练集和测试集拼在一起调用了 fit_transform测试集的均值和方差已经泄漏到了标准化参数里。更隐蔽的是有人用 TSNE 或者 PCA 对整个数据集做降维后再划分训练测试集同样属于数据泄漏。解决标准化、降维、特征选择这类操作必须在训练集上完成测试集只能用训练好的参数做 transform。这类问题不会报错只能靠代码习惯约束建议把 scaler 和模型一起保存线上推理时直接加载。5.2 测试集被反复用来调参现象模型在测试集上的准确率一直在涨但发到外面跑结果大跌。原因每次调参都看测试集结果测试集的信息已经间接进入了调参决策相当于把测试集当成了验证集用最终模型在真正的未知数据上过拟合。解决严格划分训练集、验证集、测试集。用 KDDTrain.txt 训练从中切一块做验证集调参KDDTest.txt 只在最终评估时碰一次。跑出来的结论更经得起答辩追问。5.3 特征顺序不对推理时直接报维度错误现象模型训练时好好的但换一个数据集或者拿真实流量特征来推理程序报 shape mismatch或者不报错但预测结果完全乱掉。原因训练时用的 41 列顺序是自定义的而新数据集的列顺序不一致Pandas 按列名读取后没有对齐数值错位。解决把特征列名顺序保存成 JSON 文件加载新数据时按同一份顺序重新索引。feature_order train_df.drop(columns[class]).columns.tolist() with open(feature_order.json, w) as f: json.dump(feature_order, f)推理时先读 feature_order再按这个顺序选取数据列能省掉大量排查时间。5.4 卷积核越大越好现象把 kernel_size 从 3 改成 7验证准确率不仅没涨反而掉了 2 个点训练时间变长。原因41 维特征本身长度很短kernel_size7 相当于一次覆盖近五分之一的特征卷积核在强耦合不相关的特征组合学到的模式泛化能力差。解决kernel_size 优先试 3 和 5在入侵检测这种短特征数据上不建议超过 7。需要更大感受野时通过加深网络而不是单纯扩大卷积核来实现。5.5 95% 准确率的陷阱全判 normal 也能有高分现象训练时准确率很高但分类报告显示 r2l、u2r 的 precision 和 recall 都是 0模型其实什么都没学会。原因类别不平衡少数类在损失函数里占比太小模型发现全预测成 normal 就能拿到很低的总损失。解决不要只打印 accuracy必须输出每类的 classification_report 和混淆矩阵。训练时加上 class_weight并在论文里记录加入前后 r2l、u2r 的 F1 变化。6. 把模型装进演示流程在线推理与毕设演示加分项6.1 单条样本的推理函数训练完模型后需要写一个能接收单条特征并返回预测结果的推理函数。线上数据往往是一条一条过来的不像测试集那样一次性预测。import json import numpy as np def predict_one(feature_vec, scaler, model, class_names): vec scaler.transform([feature_vec]) vec vec.reshape(1, -1, 1) proba model.predict(vec, verbose0)[0] idx int(np.argmax(proba)) return class_names[idx], float(proba[idx])推理时最关键的环节是先加载训练时保存的 scaler 和 feature_order把输入数据按相同顺序排列成 41 维向量。缺失的特征字段要补默认值多出来的字段要丢弃这些逻辑写成一个 load_and_preprocess 函数比在推理脚本里手动写特征顺序可靠得多。6.2 给毕设答辩做一个看得见的演示很多同学做了半天模型答辩时只能打开 Jupyter Notebook 跑代码效果很干。常见做法是写一个离线模拟在线检测的脚本用一个 CSV 文件模拟实时到达的连接记录控制循环频率每条记录预测完成后打印出类别和置信度并弹出一个简单的小窗口展示当前流量的风险等级。真实抓包转特征在毕设里不建议做因为从原始网络流量还原连接记录需要处理协议解析、会话重组、超时判断工作量接近重新做一个数据集时间不可控。演示环节用 CSV 轮询完全够用重点在于展示模型对不同攻击类型的判断能力和置信度展示。完成这套流程后我最大的体会是 CNN 在这里不是万能药它解决的是特征自动学习的问题但类别不平衡和数据泄漏仍然要靠训练策略来控制。把 NSL-KDD 跑通只是第一步后面换数据集、加特征、调网络结构时前面的每一条经验都会反复用到希望帮到你。本文还有配套的精品资源点击获取