简介:这套Python实现的基于CNN网络入侵检测项目源码与文档说明,适合深度学习入门、网络安全方向课设或期末大作业的本科生及自学者。项目难度适中,代码经过本地编译验证,完整覆盖NSL-KDD数据集预处理、CNN模型定义、训练与预测流程,并附有README说明与实操文档,可帮助理解入侵检测任务的建模思路与代码组织方式。压缩包共33个文件,约21.58MB,主要包含4个Python源码文件、12个cvs数据文件、7个xml工程配置文件、1个pth模型权重文件,以及准确率曲线图、混淆矩阵图表等辅助材料;源码按数据预处理、模型构建、训练、预测模块拆分,注释清晰、易读易改。数据预处理脚本和模型训练代码均可直接运行,支持自行调节学习率、卷积核等参数,复用于其他二分类或多分类任务,也可作为期末答辩时的完整演示项目。目前已有355人学习下载,适合希望快速参考完整实现或作为高分期末作业模板的读者。
1. 用 Python 和 CNN 做网络入侵检测:这门课设为什么值得认真做
网络入侵检测系统(NIDS)一直是网络安全领域的热门方向,而用 Python 实现基于 CNN(卷积神经网络)的入侵检测模型,正是许多高校网络工程、信息安全专业期末大作业的高频选题。这类项目源码加上文档说明被打包成 zip 发放,本质上是在考察学生能否把深度学习理论落地到真实流量数据上,而不是停留在调库层面。打开这个压缩包,你大概率会看到 KDD Cup 1999 或 NSL-KDD 数据集的预处理脚本、CNN 模型定义文件、训练与评估代码,以及一份说明文档。
这个方向能解决的问题很明确:传统入侵检测系统依赖专家规则和特征匹配,对未知攻击的泛化能力差;而 CNN 能自动从流量特征中提取空间相关性,把分类任务交给网络自己学习。适合的人群包括正在做课程设计的学生、想入门深度学习在安全领域应用的开发者,以及需要快速搭建一个可演示 IDS 原型的工程师。接下来的内容,我会从数据集处理讲到模型调参,再讲踩过的坑,尽量让你拿到源码后能直接跑通、能讲清楚、能应付答辩。
2. 入侵检测与 CNN 的契合点:为什么流量数据适合用卷积网络
2.1 从特征工程到自动特征提取的转变
传统入侵检测的做法是先定义大量规则,比如某个端口短时间内连接次数超过阈值就判定为扫描攻击。这种方式依赖人工经验,规则覆盖不全时漏报率很高。后来发展到机器学习方法,用 SVM、随机森林等算法对特征向量分类,但特征仍需要人工挑选。CNN 的思路完全不同:它把原始特征当作一种"空间结构"来处理,通过卷积核在特征维度上滑动,自动学习局部组合特征。
网络流量数据经过预处理后,每条记录通常是几十个数值特征(协议类型、源字节数、目的字节数、连接状态等)。这些特征看似是平铺的一维向量,但相邻特征之间存在某种关联——比如同一个 TCP 连接中的包长、标志位、持续时长在空间上相邻,这种局部模式恰好是 CNN 擅长捕捉的。把一维特征向量 reshape 成合适的形状喂给卷积层,卷积核就能提取出特征之间的短程依赖关系,这比全连接网络直接学习所有特征组合要高效得多。
2.2 CNN 处理一维序列输入的常见结构
对于 NIDS 这种输入是一维特征向量的任务,常见做法是使用一维卷积(Conv1D),而不是图像领域常用的 Conv2D。一个典型的网络结构包括:输入层接受 shape 为 (seq_len, feature_dim) 或 (seq_len,) 的数据,经过几个 Conv1D 层提取特征,每层后接 BatchNormalization 和 MaxPooling1D 做归一化和降采样,最后接全局池化或展平层,再通过全连接层输出分类概率。
代码骨架大概是这样的:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout, BatchNormalization def build_cnn_model(input_shape, num_classes): model = Sequential([ Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=input_shape), BatchNormalization(), MaxPooling1D(pool_size=2), Conv1D(filters=128, kernel_size=3, activation='relu'), BatchNormalization(), MaxPooling1D(pool_size=2), Flatten(), Dense(128, activation='relu'), Dropout(0.5), Dense(num_classes, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) return model这段代码里,input_shape 需要根据特征维度确定。如果每条样本有 41 个特征(NSL-KDD 标准),可以 reshape 成 (41, 1),即把每个特征当作一个时间步,Conv1D 在特征序列上滑动。filters 表示卷积核数量,决定提取多少种局部模式;kernel_size 表示卷积核覆盖的相邻特征个数,一般取 3 或 5,太小感受野不足,太大容易过拟合。BatchNormalization 加速收敛并稳定训练,Dropout 防止过拟合,这些做课设答辩时都是加分点。
2.3 二分类还是多分类:取决于数据集标签定义
KDD Cup 1999 和 NSL-KDD 的标签分为正常(Normal)和四类攻击:DoS、Probe、R2L、U2R。做课设时先确认文档里要求的是二分类(正常 vs 异常)还是五分类(正常 + 四类攻击)。二分类简单,准确率容易做高,但答辩时老师常问"各类攻击的检出率分别是多少",这时只看总体准确率是不够的;五分类任务中 R2L 和 U2R 样本量极少,是典型的类别不平衡问题,需要额外处理。
选择哪种分类粒度,直接决定模型输出层神经元数量和损失函数。多分类用 softmax 和 categorical_crossentropy,二分类用 sigmoid 和 binary_crossentropy。建议文档说明里明确写清楚实验设定,把混淆矩阵和每类别的精确率、召回率都打出来,这比只给一个准确率数字有说服力得多。
3. 数据预处理与特征编码:决定模型上限的关键一步
3.1 NSL-KDD 数据集的加载与清洗
NSL-KDD 是 KDD Cup 1999 的改进版本,去掉了冗余记录,训练集和测试集的分布更合理。拿到源码包后,第一步通常是加载 CSV 格式的 KDDTrain.csv 和 KDDTest.csv。每条记录有 41 个特征和 1 个标签,特征分为三类:连续数值型(如 duration、src_bytes)、离散符号型(如 protocol_type、service、flag)、以及二进制型。
加载代码通常长这样:
import pandas as pd column_names = [ 'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', # ... 省略部分特征名 '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.csv', names=column_names) test_df = pd.read_csv('KDDTest.csv', names=column_names) print(f"训练集形状: {train_df.shape}, 测试集形状: {test_df.shape}") print(train_df['label'].value_counts())这里需要注意,KDD 数据集的原始 CSV 没有表头,必须手动指定列名。如果列名数量和 CSV 实际列数不匹配,pandas 会报错,常见的翻车点在最后有一列difficulty_level(NSL-KDD 特有),读数据时要么把这一列也加进列名,要么用usecols忽略它。建议先打印train_df.shape确认列数,不要想当然。
3.2 符号特征编码:LabelEncoder 还是 OneHotEncoder
protocol_type、service、flag 这三个特征是离散字符串,不能直接喂给 CNN。常见的处理方式有两种:如果每个符号特征的取值种类少(如 protocol_type 只有 3 种取值),用 OneHot 编码展开成多列;如果取值很多(service 有 70 种左右),用 LabelEncoder 转成整数再归一化。但这里有个坑:LabelEncoder 给类别赋予的整数大小是随机的,比如 tcp=0、udp=1、icmp=2,这个大小关系没有实际意义,CNN 的卷积核会把它当作真实数值关系来学习,效果往往不如 OneHot。
我一般会这样做:
from sklearn.preprocessing import LabelEncoder # 方法一:对取值少的特征做 OneHot train_df = pd.get_dummies(train_df, columns=['protocol_type', 'flag']) # 方法二:对取值多的特征做 LabelEncoder 并归一化 le = LabelEncoder() for col in ['service']: train_df[col] = le.fit_transform(train_df[col]) test_df[col] = le.transform(test_df[col])但这里有个严重的问题:训练集和测试集的类别取值可能不一致。比如训练集里 service 有 70 种,测试集里可能出现训练集没见过的 service 值。如果直接用训练集拟合的 LabelEncoder 去 transform 测试集,sklearn 会直接抛异常。解决方法是先合并两个数据集做编码,或者对测试集中出现的新类别统一映射到一个"未知"类,再在文档里说明这种处理的理由。这个细节在答辩时很加分,因为它体现了对真实场景中分布漂移问题的理解。
3.3 数值特征归一化与训练集测试集泄漏
41 个特征里数值型特征的量纲差异巨大:src_bytes 可能是 0 到几百万,而 land 只有 0 或 1。如果不归一化,卷积层的梯度更新会被大数值特征主导,模型很难收敛。标准做法是用 StandardScaler 或 MinMaxScaler,但必须只在训练集上拟合 scaler,然后同时 transform 训练集和测试集。如果先对全量数据做归一化再划分训练集测试集,会造成数据泄漏,测试集的信息被模型间接看到了,评估结果虚高。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() train_numeric_cols = train_df.select_dtypes(include=['float64', 'int64']).columns train_df[train_numeric_cols] = scaler.fit_transform(train_df[train_numeric_cols]) test_df[train_numeric_cols] = scaler.transform(test_df[train_numeric_cols])这段代码里,scaler 只在 train_df 上 fit,test_df 只做 transform。注意select_dtypes会把标签列也选进去,如果标签是字符串没问题,如果是整数编码就需要先剔除 label 列。处理完特征后,把标签单独拆出来做 one-hot 编码,用于多分类模型训练。最后要把数据 reshape 成 CNN 输入格式,通常是(样本数, 特征维度, 1)。
4. 模型训练与评估:把准确率做上去的完整流程
4.1 数据集划分:不要只留一个测试集
拿到源码后常见的做法是直接把 KDDTrain 当训练集、KDDTest 当测试集,然后开始训练。但这样做有一个问题:你无法在训练过程中验证模型效果,超参数调优没有依据。更好的流程是从训练集里再切出 20% 作为验证集,训练时监控验证集准确率和损失,防止过拟合。
from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split( X_train_raw, y_train_raw, test_size=0.2, random_state=42, stratify=y_train_raw ) X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], 1) X_val = X_val.reshape(X_val.shape[0], X_val.shape[1], 1)stratify=y_train_raw是按类别比例分层抽样,保证验证集里各个攻击类型的占比和训练集一致。这对 NSL-KDD 尤其重要,因为 R2L 和 U2R 样本极少,如果不分层抽样,验证集里可能一个 U2R 样本都分不到,验证指标完全失真。
4.2 训练循环与 EarlyStopping
CNN 在 NSL-KDD 上训练比较容易过拟合,因为训练集规模只有几万条且类别不平衡严重。常见做法是设置早停,当验证集损失连续多个 epoch 不再下降时停止训练,并用回调保存最佳模型。
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ModelCheckpoint('best_model.h5', monitor='val_accuracy', save_best_only=True) ] history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=256, callbacks=callbacks, verbose=1 )patience=10表示连续 10 个 epoch 验证损失没改善就停。restore_best_weights=True会把模型权重回滚到验证集表现最好的那一轮,而不是停在最后一个 epoch。batch_size 取 256 在大多数 GPU 或 CPU 上都能接受,太大容易收敛慢,太小则梯度噪声大、训练振荡。如果训练时发现验证损失震荡剧烈,优先减小学习率或增大 batch_size。
4.3 评估指标:准确率之外必须看混淆矩阵
课设报告里如果只写"准确率 99.2%",答辩老师大概率会追问:对 U2R 攻击的检出率是多少?NSL-KDD 测试集中 U2R 样本极少,很多模型对其检出率为 0,但总体准确率依然很高,这就是准确率指标的欺骗性。必须输出混淆矩阵、各类别的 precision、recall、F1-score。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred = model.predict(X_test) y_pred_class = np.argmax(y_pred, axis=1) y_true_class = np.argmax(y_test, axis=1) print(classification_report(y_true_class, y_pred_class, target_names=label_names)) print(confusion_matrix(y_true_class, y_pred_class))看报告时重点关注 R2L 和 U2R 两行的 recall。如果这两行数字很低,说明模型基本没学会少数类攻击模式,这时候需要回头处理类别不平衡。一个简单的改进是class_weight,让少数类在 loss 计算中拥有更高权重。另一个是过采样少数类样本,但要注意别在测试集上做任何采样。
4.4 类别不平衡的两种处理方案对比
处理 NSL-KDD 类别不平衡,课设中最常用两种做法:class_weight 和少数类过采样。class_weight 不改数据,只在训练时加大少数类的 loss 占比,实现简单。
from sklearn.utils.class_weight import compute_class_weight class_weights = compute_class_weight( 'balanced', classes=np.unique(y_train_raw), y=y_train_raw ) class_weight_dict = dict(enumerate(class_weights))过采样则用 imbalanced-learn 库的 SMOTE 生成少数类合成样本,但 SMOTE 是基于特征空间插值的,对表格型数据效果尚可,对类别型特征多的数据可能生成矛盾的样本。做课设时用 class_weight 更安全,答辩时讲清楚原理就够;如果追求更好的少数类检出率,再尝试 SMOTE 并对比两组实验结果。
5. 常见坑与避坑指南:课设翻车现场复盘
5.1 特征维度不匹配导致模型无法编译
现象:输入模型时报错Input 0 of layer conv1d is incompatible,提示期望三维输入但实际给了两维。原因:Conv1D 要求输入是(batch_size, steps, channels),而很多人在预处理后直接拿二维数组(样本数, 特征数)去喂模型,忘记 reshape。解决:在 fit 之前执行X_train = X_train.reshape(-1, n_features, 1),第三维设为 1 表示单通道。这个错误在第一次跑代码时几乎必现,源码包里的脚本如果没处理,手动补上即可。
5.2 训练集和测试集类别不一致导致 transform 崩溃
现象:执行测试集 LabelEncoder transform 时报unknown class错误。原因:测试集中某个 service 协议在训练集中从未出现,编码器不认识。解决:合并训练集测试集后统一 fit,或对测试集中的新类别做"未知类"映射。课设报告里如果提到这个处理,说明考虑了真实流量的开放性问题,是加分项。另一个相关坑是 OneHot 编码后训练集和测试集列数不同,原因同样是新类别产生了新列,用pd.concat后统一编码能根治。
5.3 训练准确率不升反降或直接 NaN
现象:loss 在几个 epoch 后变成 NaN,或准确率一直徘徊在基线水平。原因:学习率过大导致梯度爆炸,或数值特征没有归一化,或激活函数选择不当。解决:先把学习率降到 0.0001 试试,确认归一化已执行,检查数据里是否有无穷大或空值。加 BatchNormalization 层能缓解梯度问题。另外要注意,NSL-KDD 中有少量特征的取值极端分布(如 src_bytes 的右偏),只用 StandardScaler 有时不如取 log 变换稳定,这属于进阶调参技巧。
5.4 准确率高但攻击检出率极低
现象:总准确率 99%,但 U2R 和 R2L 的 recall 接近于 0。原因:类别极度不平衡,模型把所有样本都预测为多数类就是最优策略。解决:使用 class_weight 加重少数类权重,或在训练时对少数类做重复采样。评估时不要只看准确率,输出 macro-F1 或加权 F1,这几个指标对不平衡更敏感。如果答辩时老师问"为什么 U2R 检不出",回答思路是:U2R 样本只有几十条,特征空间被正常流量淹没,模型没有足够的判别信息,这是数据层面的问题,不是模型结构的问题。
5.5 结果无法复现
现象:同一个脚本多次运行,准确率波动明显。原因:没有固定随机种子,模型初始化随机,数据划分随机。解决:在代码开头固定 numpy、tensorflow 和 Python 内置 random 的种子。
import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)课设报告里截图的结果必须可复现,不然答辩时老师让你现场跑一遍,结果和报告不一样就很尴尬。固定种子后,模型训练速度和结果基本一致,这是所有深度学习项目中必须养成的习惯。
6. 进阶玩法与验证技巧:让课设从"能跑"变成"能讲"
6.1 试试用训练好的模型做离线流量检测演示
模型训练完,可以写一个小函数,接收一条 CSV 格式的流量记录,输出预测类型,做成命令行工具。
def predict_single_record(model, record_df, scaler, encoder): record = record_df.copy() for col in ['protocol_type', 'flag']: record = pd.get_dummies(record, columns=[col]) record = record.reindex(columns=feature_columns, fill_value=0) record[numeric_cols] = scaler.transform(record[numeric_cols]) record_array = record.values.reshape(1, -1, 1) pred = model.predict(record_array) return label_names[np.argmax(pred)]这段代码里reindex是核心:单挑记录做 OneHot 后列数肯定和训练时不一样,用训练时的完整特征列列表对齐,缺失列填 0。这样模型才能接受输入。演示时拿测试集里几条已知类型的数据跑一遍,输出预测结果,比光讲一堆训练曲线更有说服力。
6.2 画训练曲线时注意原始记录而不是平滑曲线
课设报告里通常会放训练准确率和损失曲线图。画图时用 matplotlib 记录 history 中的原始数据就好,关键是把训练集和验证集两条线放一起对比,观察过拟合发生的 epoch 位置。如果只画训练集曲线,没法展示早停策略的效果。
import matplotlib.pyplot as plt plt.plot(history.history['accuracy'], label='train_acc') plt.plot(history.history['val_accuracy'], label='val_acc') plt.xlabel('Epoch') plt.ylabel('Accuracy') plt.legend() plt.savefig('training_history.png', dpi=150)图片保存分辨率要够,不要截窗口模糊图。文档里左图右图对应说明,比如"第 20 轮后验证集准确率开始下降,说明过拟合出现,触发早停回滚到第 18 轮权重"。
6.3 把你的模型和传统机器学习做横向对比
答辩时老师最常问的问题之一:为什么用 CNN?CNN 比随机森林好在哪?建议在文档里补一个对比实验:跑一个随机森林或逻辑回归作为基线,比较二者在 NSL-KDD 测试集上的准确率和 F1。CNN 在这种表格数据上优势并不明显,但特征相关性提取能力比手工特征略强,特别是在原始特征不做太多人工构造的情况下。如果不做对比实验,老师容易怀疑"为什么不用随机森林"。
实现方式很简单:用 sklearn 的 RandomForestClassifier 跑同样的数据,然后对比两到三个指标。对比结果不一定要 CNN 全面碾压,哪怕 CNN 在 U2R 上多召回几个样本,或者总体 F1 高一点,都有交代。如果随机森林反而更好,那也可以说明"传统方法在小样本攻击上仍有优势,未来可以考虑 CNN + 树模型的集成方案",这比硬吹 CNN 合理得多。
6.4 文档说明怎么写才打高分
我见过很多课设源码里带的文档说明,长篇大论贴代码却唯独不写实验结果分析。高分文档至少要有这几块:问题定义和数据集描述、数据预处理流程图、模型结构图和每层参数表、超参数设置表、训练曲线图、混淆矩阵与各类别指标表、对比实验表,以及"遇到的三个问题和解决办法"章节。每个表格配一句解释,比如"从表 4 可以看出,U2R 的召回率仅为 0.12,主要原因是训练集中该类别只有 52 条样本"。这种文档老师没理由扣分。
整个项目做下来,我最深的感受是:CNN 在网络入侵检测上的表现很大程度上取决于数据预处理而不是网络深度。我自己的课设就是从乱吞 Shape 错误开始,一步步把代码跑通,又因为类别不平衡反复调整,最后才把改进过程写成文档。这条路你大概率也要走一遍,希望这些踩坑经验能帮你少走几步弯路。希望帮到你。
本文还有配套的精品资源,点击获取