☰
Python+CNN网络入侵检测实战:从数据预处理到模型调优
2026/9/28 1:48:11 网站建设 项目流程

简介:这是一份面向计算机相关专业学生与项目实战学习者的网络入侵检测毕业设计资料,以Python结合CNN卷积神经网络实现入侵流量识别,适合作为毕设参考、课程设计或期末大作业的完整方案。压缩包共33个文件,约21.59MB,包含4个py源码文件、12个csv数据集文件、7个xml配置、2个txt说明、1个md项目文档以及pth模型权重、png与jpg结果图等,覆盖数据预处理、模型训练、预测推理与精度评估全流程。资源围绕NSL-KDD数据集展开,提供minMax归一化、准确率与精确率可视化图表,并附有训练脚本、预测脚本与CNN模型定义,便于读者理解从数据清洗到模型落地的完整链路。目前已有191人学习下载,适合希望快速掌握深度学习在网络安全的实战应用、需要可运行代码与项目说明支撑的中高级学习者参考。

1. 从一份 Python+CNN 入侵检测源码说起:它到底能跑出什么结果

很多人第一次拿到「基于python+CNN的网络入侵检测算法源码+项目说明」这类压缩包,第一反应是解压、装依赖、python main.py,然后盯着终端等一个准确率数字跳出来。但真正决定这套东西能不能用的,不是那行准确率,而是你有没有搞清楚它把网络流量变成了什么形状的张量、CNN 在这个任务里到底卷的是什么。网络入侵检测本质是一个流量分类问题:把 NSL-KDD、CIC-IDS2017 这类数据集里的每条连接记录,判定为 normal 还是某类攻击(DoS、Probe、R2L、U2R)。CNN 原本是为图像设计的,用在结构化流量特征上,靠的是把一维特征向量 reshape 成二维矩阵,让卷积核去捕捉特征之间的局部相关性。这套源码适合两类人:一是想入门深度学习安全方向的学生和转岗工程师,二是需要快速搭一个可复现 baseline 的安全从业者。它不解决生产级流量采集和实时推理,但能让你把「数据预处理 → CNN 建模 → 训练评估」这条链路完整走一遍,理解每一步的参数含义。下面我按自己复现这类项目的顺序,把关键环节拆开讲。

2. 数据先过关:KDD 类流量数据的清洗与张量重塑

2.1 为什么原始 CSV 不能直接喂给 CNN

NSL-KDD 的训练集有 125973 条记录、43 个特征,其中 3 个是符号型(protocol_type、service、flag),其余是数值型。直接把字符串丢进模型必然报错,而简单用 LabelEncoder 把 service 的 70 个取值映射成 0~69,会引入一个虚假的序关系——模型会以为 service=69 比 service=3「更大」,这对分类是有害的。常见做法是对符号特征做 one-hot,对数值特征做标准化。这里有个容易忽略的点:KDD 的数值特征量纲差异极大,src_bytes能到上亿,num_failed_logins只有 0~5,不标准化的话卷积核的梯度会被大数值特征主导,训练直接发散。

我一般会先做一次特征分布检查,再决定标准化方式。下面这段是数据加载和预处理的骨架:

import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer # NSL-KDD 列名(官方 kddcup.names 顺序) 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","difficulty"] train = pd.read_csv("KDDTrain+.txt", names=col_names) test = pd.read_csv("KDDTest+.txt", names=col_names) # 把 40 多种细粒度攻击标签归并成 5 大类,这是 KDD 任务的标准做法 attack_map = { "normal": "normal", "back":"dos","land":"dos","neptune":"dos","pod":"dos","smurf":"dos","teardrop":"dos", "ipsweep":"probe","nmap":"probe","portsweep":"probe","satan":"probe", "ftp_write":"r2l","guess_passwd":"r2l","imap":"r2l","multihop":"r2l","phf":"r2l", "spy":"r2l","warezclient":"r2l","warezmaster":"r2l", "buffer_overflow":"u2r","loadmodule":"u2r","perl":"u2r","rootkit":"u2r" } train["label"] = train["label"].map(attack_map) test["label"] = test["label"].map(attack_map) cat_cols = ["protocol_type", "service", "flag"] num_cols = [c for c in col_names if c not in cat_cols + ["label", "difficulty"]] pre = ColumnTransformer([ ("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), cat_cols), ("num", StandardScaler(), num_cols) ]) X_train = pre.fit_transform(train[cat_cols + num_cols]) X_test = pre.transform(test[cat_cols + num_cols])

这段代码的逻辑是:先按官方列名读入,再把攻击标签归并成 5 类,然后用 ColumnTransformer 对符号列做 one-hot、对数值列做标准化。注意handle_unknown="ignore"这个参数,测试集里会出现训练集没见过的 service 取值,不加这个参数会直接抛异常。sparse_output=False是为了后面 reshape 方便,否则得到的是稀疏矩阵。

2.2 把一维特征 reshape 成 CNN 能吃的二维图

one-hot 之后特征维度会从 41 涨到 120 左右,具体取决于 service 的取值数量。CNN 需要 4 维输入(batch, height, width, channels),所以要把这个一维向量折成二维。常见做法是取最接近的平方数,比如 121 就补零到 144,reshape 成 12×12×1。补零还是截断要看维度,宁可补零也别截断,截断会丢特征。

import numpy as np def to_image(X, side=12): n, d = X.shape target = side * side if d < target: # 右侧补零到 target 维 X = np.pad(X, ((0,0),(0, target - d)), mode="constant") else: X = X[:, :target] return X.reshape(n, side, side, 1).astype("float32") X_train_img = to_image(X_train) X_test_img = to_image(X_test) print(X_train_img.shape) # (125973, 12, 12, 1)

参数side=12不是随便定的:特征维度 120 左右,11×11=121 最接近,但 11 是奇数,卷积加池化后尺寸计算容易出小数,用 12 更省心。如果你换 CIC-IDS2017 数据集,特征维度是 78,那 side 取 9(81)更合适。这个 reshape 是整套方案里最「玄学」的一步——它没有严格的数学依据,本质是给卷积核制造一个可滑动的局部结构,让相邻特征之间产生交互。实测下来,reshape 方式对最终准确率的影响在 1~2 个百分点,不用过度纠结,但补零位置要固定,训练和推理必须一致。

3. CNN 模型怎么搭:卷积核、池化与分类头的参数取舍

3.1 一个够用的两层卷积结构

入侵检测数据不是自然图像,没有纹理和边缘,所以不需要 ResNet 那种深层结构。两层卷积加池化,接全连接分类头,在 KDD 上就能到 99% 左右的准确率。层数再深反而容易过拟合,因为样本的有效信息量有限。

import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_shape=(12,12,1), n_classes=5): model = models.Sequential([ layers.Conv2D(32, (3,3), padding="same", activation="relu", input_shape=input_shape), layers.BatchNormalization(), layers.MaxPooling2D((2,2)), layers.Conv2D(64, (3,3), padding="same", activation="relu"), layers.BatchNormalization(), layers.MaxPooling2D((2,2)), layers.Flatten(), layers.Dense(128, activation="relu"), layers.Dropout(0.5), layers.Dense(n_classes, activation="softmax") ]) model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss="sparse_categorical_crossentropy", metrics=["accuracy"]) return model model = build_cnn() model.summary()

逐层说下参数:第一层 32 个 3×3 卷积核,padding="same"保证输出还是 12×12,BatchNormalization 放在卷积和激活之后能加速收敛、缓解梯度问题。MaxPooling 2×2 把尺寸降到 6×6。第二层 64 个卷积核再降到 3×3。Flatten 后是 64×3×3=576 维,接 128 维全连接,Dropout 0.5 是防过拟合的关键——KDD 训练集里 normal 样本占比高,不加 Dropout 模型会偏向多数类。输出层 5 个神经元对应 5 类,用 softmax。

3.2 训练参数与类别不平衡的处理

KDD 的类别分布极度不均:normal 约 53%,DoS 约 37%,Probe 约 9%,R2L 约 0.8%,U2R 只有 0.04%。如果直接训练,U2R 几乎永远预测不对。两种处理方式:一是用class_weight给少数类加权,二是对多数类下采样。我一般先用 class_weight,因为它不损失数据。

from sklearn.utils.class_weight import compute_class_weight import numpy as np y_train = train["label"].map({"normal":0,"dos":1,"probe":2,"r2l":3,"u2r":4}).values classes = np.unique(y_train) weights = compute_class_weight("balanced", classes=classes, y=y_train) class_weight = dict(zip(classes, weights)) print(class_weight) # u2r 的权重会非常大 history = model.fit( X_train_img, y_train, validation_split=0.2, epochs=30, batch_size=256, class_weight=class_weight, callbacks=[ tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True), tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=3) ] )

compute_class_weight("balanced")会按n_samples / (n_classes * n_samples_per_class)计算权重,U2R 的权重可能到几百。这里有个坑:权重过大会让模型对少数类过拟合,验证集上 U2R 的召回率上去了但精确率暴跌。我的经验是把权重开方压一下,比如weights ** 0.5,在召回和精确之间取平衡。batch_size=256是显存和收敛速度的折中,太小训练慢,太大梯度估计不准。EarlyStopping 的 patience=5 配合 ReduceLROnPlateau 的 patience=3,基本能避免手动调 epoch。

3.3 评估指标不能只看 accuracy

在类别极不平衡的数据上,accuracy 是个骗人的指标。全预测成 normal 也能有 53% 的准确率。必须看每个类的 precision、recall、f1,以及混淆矩阵。

from sklearn.metrics import classification_report, confusion_matrix y_pred = np.argmax(model.predict(X_test_img), axis=1) y_test = test["label"].map({"normal":0,"dos":1,"probe":2,"r2l":3,"u2r":4}).values print(classification_report(y_test, y_pred, target_names=["normal","dos","probe","r2l","u2r"], digits=4)) print(confusion_matrix(y_test, y_pred))

重点看 U2R 和 R2L 两行。如果 U2R 的 recall 低于 0.5,说明权重还不够或者特征本身区分度不足。KDD 的 U2R 样本只有 52 条,模型很难学到模式,这时候要接受现实:这个数据集上 U2R 的检测本身就是难题,别为了刷指标去调测试集。

4. 避坑与排查:复现这套源码时最容易翻车的 5 个地方

4.1 现象:训练 loss 一直不降,accuracy 卡在 53%

原因:标签没做映射,模型把 40 多个原始攻击标签当成 40 多类,而输出层只有 5 个神经元,维度对不上或者标签编码错位。另一个可能是数值特征没标准化,大数值特征主导了梯度。

解决:先打印train["label"].value_counts()确认标签已经归并成 5 类,再检查X_train的均值和方差是否在 0 和 1 附近。如果均值是几百,说明 StandardScaler 没生效,检查 ColumnTransformer 的列名是否和 DataFrame 对得上。

4.2 现象:验证集准确率 99%,测试集只有 75%

原因:KDDTest+ 里包含训练集没出现过的攻击类型,这是 NSL-KDD 的设计意图——考模型的泛化能力。如果你在训练集上做了过采样或者用了测试集的信息做标准化,就会导致这个落差。

解决:标准化器只能在训练集上 fit,测试集用 transform。检查代码里有没有pre.fit_transform(test[...])这种写法,有的话改成pre.transform。另外别用测试集调超参数,那等于偷看答案。

4.3 现象:U2R 类的 recall 始终为 0

原因:U2R 样本太少,class_weight 虽然给了高权重,但 batch 里可能一个 U2R 都没有,梯度更新时这类样本被淹没。

解决:改用分层采样,保证每个 batch 里都有少数类样本。或者对 U2R 做 SMOTE 过采样,但要注意 SMOTE 只能在训练集上做,且合成样本要参与标准化。实测分层采样比 SMOTE 更稳,因为 SMOTE 在 one-hot 特征上合成容易产生无意义的组合。

4.4 现象:模型保存后再加载,预测结果全乱

原因:reshape 的 side 参数、one-hot 的列顺序、标准化的均值方差没有一起保存。推理时重新 fit 一遍预处理器,列顺序变了,特征就对不上了。

解决:把预处理器和模型一起序列化。用 joblib 存 ColumnTransformer,用model.save("ids_cnn.keras")存模型,推理时先 load 预处理器再 transform。别在推理脚本里重新写一遍预处理逻辑,那是 bug 的温床。

4.5 现象:GPU 显存够但训练速度极慢

原因:数据是 numpy 数组,每个 epoch 都在 CPU 和 GPU 之间拷贝,没用到 tf.data 的流水线。另外 batch_size 太小也会让 GPU 利用率上不去。

解决:用tf.data.Dataset.from_tensor_slices((X, y)).batch(256).prefetch(tf.data.AUTOTUNE)包一层,prefetch 能让数据准备和 GPU 计算重叠。如果数据能放进显存,加.cache()更好。这套模型很小,其实 CPU 也能跑,但用 tf.data 后 GPU 利用率能从 30% 提到 80% 以上。

5. 从源码到能用:把检测率再往上推的几个实操技巧

跑通源码只是起点,真正决定这套东西值不值得投入的,是你能不能把它推到接近可用的水平。第一个技巧是特征工程比换模型更有效。KDD 的 41 维特征里,src_bytes、dst_bytes、count、srv_count这几个的区分度最高,可以单独对它们做对数变换,把长尾分布压平,实测能让 Probe 类的 f1 提升 2~3 个点。第二个技巧是集成,CNN 提特征、随机森林做分类,把 CNN 倒数第二层的 128 维输出当特征喂给 RF,在 U2R 上比纯 CNN 稳,因为 RF 对少数类的处理更鲁棒。

第三个技巧是推理阶段的阈值调整。模型输出的是 softmax 概率,默认取 argmax,但安全场景下漏报比误报代价高。可以对少数类设一个更低的判定阈值,比如 U2R 概率超过 0.3 就报警,牺牲一点精确率换召回。这个阈值要在验证集上按业务需求调,没有通用值。

验证方法上,我习惯做一次「时间切分」测试:把数据按时间排序,前 70% 训练、后 30% 测试,而不是随机切分。随机切分会让相邻的相似样本同时出现在训练和测试集,准确率虚高。时间切分更接近真实部署场景,虽然指标会掉几个点,但那个数字才可信。

最后说个我自己的教训:早期复现这类项目时,我花了两周调网络结构,从两层卷积加到五层,准确率只动了 0.3 个点;后来花半天做特征分布分析和类别权重调整,U2R 的召回率从 0.1 提到 0.6。在入侵检测这个任务上,数据和标签的处理永远比模型深度重要。拿到一份源码,先别急着改网络,把数据管道和评估指标盯死,收益大得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询