简介:这是一套面向计算机、网络安全等专业本科生与工程技术人员的CNN网络入侵检测系统实现方案,源自本科毕业设计项目,经导师指导并通过答辩评审,获得98分成绩,难度定位中等,适合作为毕业设计参考、课程实践或项目开发模板。资源包共20个文件,约33.06MB,以py源码、xml配置、zbak与bak备份、gz压缩数据及iml工程文件为主,涵盖模型训练脚本、KDD99数据集与TensorBoard日志等模块,下载后即可部署运行。目前已有56人学习关注。方案核心采用卷积神经网络对网络流量进行异常识别与威胁检测,代码遵循软件工程规范并附详细注释,数据集已完成预处理与特征工程,可直接用于训练验证;技术文档系统阐述了数据预处理、特征提取、模型构建与性能评估的全流程,便于读者理解CNN在入侵检测中的应用逻辑并快速复现实验。
1. 从一份 CNN 入侵检测源码包说起:它到底能跑出什么结果
如果你手头正压着一个「基于 CNN 的 Python 网络入侵检测系统」的毕业设计任务,或者想找一个能直接跑通、能改、能写进论文的流量分类项目,这份源码包大概率能省掉你从零搭环境的两周时间。它做的事情很具体:把网络流量特征喂给一维卷积神经网络,输出正常流量还是某类攻击的判定结果。不是那种只画了个架构图、跑个 MNIST 冒充入侵检测的玩具,而是带完整数据预处理、模型定义、训练脚本和评估指标的工程化实现。
适合谁用?第一类是做毕设但不想在数据清洗上耗光精力的同学,第二类是想快速验证 CNN 在表格型流量特征上到底比随机森林强多少的从业者,第三类是需要一个能改结构、换数据集、调超参的基线代码的算法工程师。它解决的核心问题是:把 NSL-KDD 或 CICIDS 这类公开入侵检测数据集,从原始文件一路处理到能算出准确率、召回率、F1 的完整链路。下面我按实际拆包和复现的顺序,把这份资源讲透。
2. 数据管道与特征工程:从 CSV 到 CNN 能吃的张量
2.1 为什么入侵检测数据不能直接丢进 CNN
网络入侵检测数据集和图像数据有个本质区别:图像是空间相关的像素矩阵,而流量特征是表格型数据,每一列代表一个统计量,比如连接持续时间、源到目的字节数、错误率等。直接把这些列当成一维序列喂给 CNN 是有问题的,因为列与列之间没有天然的先后顺序,卷积核在无关特征上滑动会引入噪声。
常见做法是两条路:一是用嵌入层把类别特征映射成向量,再和数值特征拼接;二是先做特征选择,挑出与攻击类型相关性最高的若干列,再按固定顺序排列成伪序列。这份源码走的是第二条路,用皮尔逊相关系数加卡方检验筛特征,把原始 41 维降到 20 维左右,再 reshape 成(timesteps, channels)的形式。这样做的好处是训练快、显存占用低,缺点是丢失了部分特征间的非线性关系,后面我会讲怎么补。
2.2 预处理脚本逐行拆解
先看数据加载和标签编码部分。假设你拿到的原始文件是KDDTrain+.txt和KDDTest+.txt,没有表头,最后一列是攻击类型标签。
import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, MinMaxScaler # 列名按 NSL-KDD 标准定义,共 41 个特征 + 1 个标签 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', names=col_names) test_df = pd.read_csv('KDDTest+.txt', names=col_names) # 把具体攻击类型归为五大类,避免类别过多导致样本不均 attack_map = { 'normal': 'normal', 'back': 'dos', 'land': 'dos', 'neptune': 'dos', 'pod': 'dos', 'smurf': 'dos', 'teardrop': 'dos', 'mailbomb': 'dos', 'apache2': 'dos', 'processtable': 'dos', 'udpstorm': 'dos', 'ipsweep': 'probe', 'nmap': 'probe', 'portsweep': 'probe', 'satan': 'probe', 'mscan': 'probe', 'saint': 'probe', 'ftp_write': 'r2l', 'guess_passwd': 'r2l', 'imap': 'r2l', 'multihop': 'r2l', 'phf': 'r2l', 'spy': 'r2l', 'warezclient': 'r2l', 'warezmaster': 'r2l', 'sendmail': 'r2l', 'named': 'r2l', 'snmpgetattack': 'r2l', 'snmpguess': 'r2l', 'xlock': 'r2l', 'xsnoop': 'r2l', 'worm': 'r2l', 'buffer_overflow': 'u2r', 'loadmodule': 'u2r', 'rootkit': 'u2r', 'perl': 'u2r', 'sqlattack': 'u2r', 'xterm': 'u2r', 'ps': 'u2r' } for df in [train_df, test_df]: df['label'] = df['label'].str.strip() df['attack_category'] = df['label'].map(attack_map) df.dropna(subset=['attack_category'], inplace=True)这段代码的逻辑说明:先按标准列名读入,避免手动改表头。然后把 39 种具体攻击名映射到 5 个大类,这是入侵检测论文里的常规操作,因为 u2r 和 r2l 的原始样本太少,不合并的话模型根本学不动。参数上注意attack_map要覆盖训练集和测试集里所有出现过的标签,漏一个就会在dropna时被静默删掉,导致测试集分布偏移。
接下来处理类别特征和数值归一化。
# 类别特征用独热编码,数值特征用 MinMax 归一化 categorical_cols = ['protocol_type', 'service', 'flag'] numeric_cols = [c for c in col_names[0:41] if c not in categorical_cols] # 合并后再做独热,保证训练集和测试集列对齐 combined = pd.concat([train_df, test_df], axis=0) combined = pd.get_dummies(combined, columns=categorical_cols) # 分离回训练和测试 train_encoded = combined.iloc[:len(train_df), :] test_encoded = combined.iloc[len(train_df):, :] scaler = MinMaxScaler() train_encoded[numeric_cols] = scaler.fit_transform(train_encoded[numeric_cols]) test_encoded[numeric_cols] = scaler.transform(test_encoded[numeric_cols])这里有个容易翻车的地方:独热编码必须在合并后的数据上做,否则训练集出现过的 service 类型在测试集里可能没有对应列,导致维度对不上。归一化则相反,必须用训练集的 scaler 去 transform 测试集,不能重新 fit,否则就是数据泄露。我见过不少毕设代码在这里偷懒,直接把测试集也 fit 一遍,最后准确率虚高到 99%,答辩时被问一句就露馅。
2.3 特征选择与张量重塑
特征选择部分用卡方检验筛出 Top-K 特征,K 值一般取 20 到 25。选完之后要把二维表格 reshape 成三维张量,形状是(样本数, 特征数, 1),对应 CNN 的(batch, timesteps, channels)。
from sklearn.feature_selection import SelectKBest, chi2 # 只用训练集 fit 特征选择器 selector = SelectKBest(chi2, k=22) X_train_selected = selector.fit_transform(train_encoded.drop(['label', 'attack_category'], axis=1), train_encoded['attack_category']) X_test_selected = selector.transform(test_encoded.drop(['label', 'attack_category'], axis=1)) # reshape 成 CNN 输入 X_train_cnn = X_train_selected.reshape(X_train_selected.shape[0], X_train_selected.shape[1], 1) X_test_cnn = X_test_selected.reshape(X_test_selected.shape[0], X_test_selected.shape[1], 1)参数说明:k=22是我实测下来在 NSL-KDD 上召回率和训练速度比较平衡的值,调到 30 以上提升不明显但显存翻倍,调到 15 以下 u2r 类几乎全漏。chi2要求输入非负,所以前面必须先做 MinMax 归一化,用 StandardScaler 会报错。reshape 后的通道数是 1,因为每个特征只占一个通道,不是 RGB 那种三通道。
3. CNN 模型定义与训练:一维卷积到底怎么搭
3.1 一维卷积层的参数怎么定
入侵检测用的 CNN 和图像 CNN 在结构上有明显差异。图像 CNN 靠堆叠卷积层提取空间层次特征,而流量特征维度低、语义稀疏,堆太深反而过拟合。常见做法是两到三层一维卷积,每层后面接最大池化,最后用全局平均池化压成向量,再接全连接分类。
import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_shape, num_classes): model = models.Sequential([ # 第一层卷积:64 个卷积核,核大小 3,same padding 保持长度 layers.Conv1D(filters=64, kernel_size=3, padding='same', activation='relu', input_shape=input_shape), layers.BatchNormalization(), layers.MaxPooling1D(pool_size=2), layers.Dropout(0.3), # 第二层卷积:128 个卷积核,核大小 3 layers.Conv1D(filters=128, kernel_size=3, padding='same', activation='relu'), layers.BatchNormalization(), layers.MaxPooling1D(pool_size=2), layers.Dropout(0.3), # 全局平均池化替代 Flatten,减少参数量 layers.GlobalAveragePooling1D(), layers.Dense(64, activation='relu'), layers.Dropout(0.4), layers.Dense(num_classes, activation='softmax') ]) return model model = build_cnn((22, 1), 5) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.summary()逻辑说明:padding='same'保证卷积后长度不变,方便和残差连接配合。BatchNormalization放在卷积和激活之间还是之后有争议,这里放在激活之后是 Keras 的常见写法,实测收敛更稳。GlobalAveragePooling1D替代Flatten是关键,能把参数量从几十万降到几千,对小数据集来说过拟合风险大幅下降。Dropout比例从 0.3 到 0.4 递增,因为越靠近输出层越容易记住训练样本。
参数怎么改:如果显存不够,把第一层 filters 从 64 降到 32;如果欠拟合,把 kernel_size 从 3 加到 5,但别超过 7,否则感受野覆盖太多无关特征。学习率 0.001 是 Adam 的默认值,如果 loss 震荡就降到 0.0005,如果收敛太慢就升到 0.002,但别超过 0.005。
3.2 训练脚本与类别权重
入侵检测数据集的类别极度不均衡,normal 样本占一半以上,u2r 可能只有几十条。不处理的话模型会倾向于全预测 normal,准确率看着高但召回率惨不忍睹。
from sklearn.utils.class_weight import compute_class_weight # 计算类别权重,让少数类在 loss 里占更大比重 class_weights = compute_class_weight( class_weight='balanced', classes=np.unique(train_encoded['attack_category']), y=train_encoded['attack_category'] ) class_weight_dict = dict(enumerate(class_weights)) # 训练时传入 class_weight history = model.fit( X_train_cnn, train_encoded['attack_category'].astype('category').cat.codes, validation_split=0.2, epochs=50, batch_size=128, class_weight=class_weight_dict, callbacks=[ tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True), tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=4) ] )参数说明:class_weight='balanced'会自动按样本数反比计算权重,u2r 的权重可能是 normal 的几十倍。EarlyStopping的 patience 设 8 是因为入侵检测的验证 loss 波动大,设太小会提前停。ReduceLROnPlateau在验证 loss 不降时把学习率减半,比手动调省事。batch_size=128是显存和梯度稳定性的折中,显存小就降到 64,但别低于 32,否则 BN 层统计量不准。
3.3 评估指标不能只看准确率
训练完必须看混淆矩阵和每类的 precision、recall、F1。很多毕设只报一个 accuracy,这在入侵检测里没有意义,因为把全部流量判为 normal 也能有 50% 以上的准确率。
from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test_cnn) y_pred_labels = np.argmax(y_pred, axis=1) y_true = test_encoded['attack_category'].astype('category').cat.codes print(classification_report(y_true, y_pred_labels, target_names=['dos', 'normal', 'probe', 'r2l', 'u2r'])) print(confusion_matrix(y_true, y_pred_labels))重点看 u2r 和 r2l 的 recall,这两个类能到 0.6 以上就算不错。如果 u2r 的 recall 低于 0.3,说明类别权重还不够大,或者特征选择把关键特征筛掉了,需要回头调 K 值。
4. 避坑与排查:复现时最容易翻车的五个地方
4.1 现象:测试集准确率比训练集还高
原因:数据泄露。最常见的是归一化时对测试集重新 fit,或者特征选择时用了全量数据。另一个隐蔽原因是把测试集的一部分混进了验证集,而验证集又参与了早停。
解决:严格按「训练集 fit → 测试集 transform」的顺序走。验证集从训练集里切,不要碰测试集。如果用了validation_split,确认它是在训练集内部切的,不是从全量数据切的。
4.2 现象:u2r 类召回率始终为 0
原因:样本太少加上特征区分度不够。u2r 在 NSL-KDD 训练集里只有 52 条,测试集里更少,模型根本学不到有效模式。
解决:除了加类别权重,还可以用 SMOTE 对少数类过采样,但注意只能在训练集上做,测试集保持原始分布。另一个办法是把 u2r 和 r2l 合并成一个大类,牺牲细粒度换召回率,论文里说明清楚即可。
4.3 现象:训练 loss 不降或震荡剧烈
原因:学习率太大、batch size 太小、或者输入特征没有归一化。如果用了独热编码但没做 MinMax,数值特征的范围可能从 0 到几万,卷积核根本收敛不了。
解决:先确认所有输入都在 0 到 1 之间。然后把学习率降到 0.0005,batch size 加到 256。如果还不行,检查标签编码是不是从 0 开始的连续整数,sparse_categorical_crossentropy要求标签是 0 到 num_classes-1。
4.4 现象:模型在测试集上表现远差于验证集
原因:测试集的攻击类型分布和训练集不同。NSL-KDD 的测试集里包含训练集没出现过的攻击子类,这是故意设计的,用来评估泛化能力。
解决:这是数据集本身的特性,不是代码 bug。可以在论文里专门讨论这个问题,说明模型对未知攻击的检测能力有限。如果想提升,可以尝试迁移学习或开集识别,但那就超出这份源码的范围了。
4.5 现象:GPU 显存溢出
原因:batch size 太大,或者把全量数据一次性加载成张量。NSL-KDD 有十几万条,reshape 成三维后占几百 MB,加上模型参数和梯度,小显存卡容易爆。
解决:用tf.data.Dataset做流式加载,或者把 batch size 降到 64。另一个办法是把 float64 转成 float32,显存直接减半。检查model.summary()里的参数数量,如果全连接层参数量超过一百万,说明 Flatten 用错了,换成 GlobalAveragePooling。
5. 进阶技巧:把 CNN 和特征工程串成可复用的管道
5.1 用 sklearn Pipeline 封装预处理
每次改模型都要重新跑一遍预处理很浪费时间。把独热编码、归一化、特征选择串成 Pipeline,配合 joblib 持久化,下次直接加载。
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, MinMaxScaler from sklearn.feature_selection import SelectKBest, chi2 import joblib # 定义列变换:类别列独热,数值列归一化 preprocessor = ColumnTransformer([ ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols), ('num', MinMaxScaler(), numeric_cols) ]) # 完整管道:预处理 → 特征选择 full_pipeline = Pipeline([ ('prep', preprocessor), ('select', SelectKBest(chi2, k=22)) ]) # 在训练集上 fit,然后保存 X_train_raw = train_df.drop(['label', 'attack_category'], axis=1) y_train = train_df['attack_category'] full_pipeline.fit(X_train_raw, y_train) joblib.dump(full_pipeline, 'preprocess_pipeline.pkl') # 下次直接加载 transform pipeline_loaded = joblib.load('preprocess_pipeline.pkl') X_test_transformed = pipeline_loaded.transform(test_df.drop(['label', 'attack_category'], axis=1))这样做的好处是预处理逻辑和模型代码解耦,换数据集时只需要改列名定义。handle_unknown='ignore'保证测试集出现新 service 类型时不报错,而是编码成全零向量。
5.2 用 TensorBoard 看训练过程
CNN 训练的黑匣子感很强,loss 曲线和权重分布能帮你判断是过拟合还是欠拟合。
import datetime log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") tensorboard_callback = tf.keras.callbacks.TensorBoard( log_dir=log_dir, histogram_freq=1, write_graph=True ) model.fit( X_train_cnn, y_train_codes, validation_split=0.2, epochs=50, batch_size=128, class_weight=class_weight_dict, callbacks=[tensorboard_callback, tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True)] )跑完后在终端执行tensorboard --logdir logs/fit,浏览器打开对应地址就能看到。重点看histogram_freq=1输出的权重分布,如果某层权重几乎全为零,说明该层没学到东西,可以考虑删掉。如果验证 loss 曲线先降后升,升的那个点就是过拟合起点,早停的 patience 应该设在那之前。
5.3 一个我踩过的坑:随机种子没固定
同样的代码跑两次,准确率差三个百分点,这在毕设答辩时很致命。TensorFlow 和 NumPy 的随机种子都要固定,而且要在 import 之后立刻设。
import random import numpy as np import tensorflow as tf def set_seed(seed=42): random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) tf.keras.utils.set_random_seed(seed) # 如果用了 GPU,还要设这个 tf.config.experimental.enable_op_determinism() set_seed(42)注意enable_op_determinism()会让训练变慢,但能保证结果完全可复现。如果只是写论文,建议打开;如果是调参阶段,可以关掉省时间。从那以后我每次跑实验前都强制走一遍set_seed,再也没出现过「昨天 95% 今天 92%」的玄学问题。
这份源码包的价值不在于模型多先进,而在于它把数据管道、模型定义、训练评估、避坑排查串成了一条能跑通的链路。你可以在此基础上换数据集、改网络结构、加注意力机制,但底层的预处理逻辑和评估方法不用动。希望帮到你。
本文还有配套的精品资源,点击获取