简介:这是一份基于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、ICMP;service 是目标服务类型,像 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", header=None, names=FEATURES) test_df = pd.read_csv("KDDTest+.txt", header=None, names=FEATURES) 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,在测试集里变成了 2,CNN 会把同一个协议类型当成完全不同的含义。用 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_dim=41, num_classes=5): model = Sequential(name="cnn_network_intrusion") model.add(Conv1D( filters=64, kernel_size=3, padding="same", activation="relu", input_shape=(input_dim, 1) )) model.add(BatchNormalization()) model.add(MaxPooling1D(pool_size=2)) model.add(Conv1D( filters=128, kernel_size=3, padding="same", activation="relu" )) model.add(BatchNormalization()) model.add(MaxPooling1D(pool_size=2)) model.add(Flatten()) model.add(Dropout(0.5)) model.add(Dense(128, activation="relu")) model.add(Dense(num_classes, activation="softmax")) return model model = build_cnn(input_dim=41, num_classes=5) model.summary()第一层卷积的输入是 (41, 1),41 是特征长度,1 是通道数。padding="same" 让卷积后长度不变,方便后续池化。filters=64 表示第一层有 64 个卷积核,每个卷积核相当于在学一种特征组合模式。kernel_size=3 是窗口大小,表示每次看相邻 3 个特征。第二层 filters 从 64 加到 128,通道数翻倍,因为经过池化后特征图变小,需要更多卷积核来提取不同角度的特征。
3.3 几个必调参数的经验值
kernel_size 是最先要试的参数。3 和 5 是常用的两个值,41 维特征长度不算长,kernel_size 设 3 计算量小,设 5 能覆盖更宽的局部范围,但也不是越大越好。我试过 kernel_size=7,训练波动变大,测试集准确率反而掉了 1 到 2 个百分点,因为卷积窗口过宽会把不相关的特征强行组合在一起,引入噪声。
pool_size 用 2,和 kernel_size=3 搭配,每经过一次池化,特征长度减半。两次池化后,41 维输入变成 10 维左右,最后接全连接层时参数数量不会爆炸。Dropout 放在全连接层之前,而不是卷积层之间,0.5 这个值在入侵检测这种中等规模数据上表现稳定。BatchNormalization 放在每次卷积之后、激活之前,作用是让每层输入分布稳定,学习率可以放心用 0.001。
model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] )这里用 sparse_categorical_crossentropy 是因为标签是整数编号而不是 one-hot 向量,可以直接省掉 to_categorical 这一步。如果做二分类,把最后一层激活函数换成 sigmoid,loss 换成 binary_crossentropy。
4. 训练与评估:别让准确率骗了你
4.1 训练配置:早停、学习率衰减、模型保存
训练 CNN 做入侵检测时,我一般不会硬跑满固定 epoch 数。更可靠的做法是配好早停和学习率衰减,让模型在验证集不再变好的时候自动停下来,同时把最好的权重保存下来。
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks = [ EarlyStopping( monitor="val_loss", patience=8, restore_best_weights=True ), ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=3, min_lr=1e-6 ), ModelCheckpoint( "best_model.h5", monitor="val_accuracy", save_best_only=True ) ] history = model.fit( X_train, y_train, batch_size=64, epochs=60, validation_data=(X_test, y_test), callbacks=callbacks )EarlyStopping 的 patience=8 表示如果连续 8 个 epoch 验证集损失没有下降,就停止训练并恢复到验证损失最小的权重。ReduceLROnPlateau 在验证损失连续 3 个 epoch 不降时,把学习率减半,这样训练后期可以用更细的步长逼近最优解。ModelCheckpoint 只保存验证准确率最高的那次权重,防止最后几个 epoch 过拟合把好的权重覆盖掉。
batch_size=64 是默认的起步值,显存不够就降到 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_size=64), axis=-1) print(classification_report( y_test, y_pred, target_names=class_names, digits=4 )) print(confusion_matrix(y_test, y_pred))classification_report 会给每个类别单独的 precision、recall、f1-score。对于入侵检测,recall 比 precision 更关键:漏掉一次攻击的代价比误报一次正常流量更大。所以调参的目标应该是让 dos、probe、r2l、u2r 四类的 recall 尽量高,而不是只盯着总准确率。混淆矩阵能直观看出哪两类容易被混淆,常见结果是 r2l 大量被错判成 normal,u2r 几乎全军覆没。
4.3 类别不平衡:class_weight 与 focal loss
如果跑完发现 r2l 和 u2r 的召回率是 0,说明模型彻底放弃了这两个类。常规解决方法是给少数类更高的权重,让模型把少数类判错时付出更大的损失。
from sklearn.utils.class_weight import compute_class_weight class_weight = compute_class_weight( class_weight="balanced", classes=np.array([0, 1, 2, 3, 4]), y=y_train ) class_weight_dict = {i: w for i, w in enumerate(class_weight)} model.fit( X_train, y_train, batch_size=64, epochs=60, validation_data=(X_test, y_test), callbacks=callbacks, class_weight=class_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_size=7 相当于一次覆盖近五分之一的特征,卷积核在强耦合不相关的特征组合,学到的模式泛化能力差。
解决: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, verbose=0)[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 跑通只是第一步,后面换数据集、加特征、调网络结构时,前面的每一条经验都会反复用到,希望帮到你。
本文还有配套的精品资源,点击获取