简介:该项目是一套基于Python与CNN网络入侵检测算法源码,面向网络安全、机器学习方向的课程设计、期末大作业及毕业设计场景。项目以NSL-KDD等数据集为基础,涵盖数据预处理、CNN模型构建、训练、预测与评估的完整流程,适合具备一定Python基础并希望深入理解深度学习安全应用的读者学习借鉴。压缩包共33个文件,除py源码外,还有csv数据文件、xml工程配置、pth模型权重、md项目说明、txt说明及训练过程可视化图片等,整体约21.58MB。已有631人学习/下载。源码中的Train.py、Predict.py、CNNMould.py等模块分工明确,可直接运行,帮助读者快速复现实验并进一步调参和扩展,尤其适合作为课程项目、毕设或论文研究的参考资料。
1. 一套 CNN 入侵检测源码到底在解决什么问题
你大概率是冲着“python + CNN + 网络入侵检测”这几个词来的:手里拿到一个 zip,里面有源码和项目说明,但不确定这东西能不能跑、跑通了又能不能用在真实流量上。这套方案的核心,是把网络流量里每条连接记录的特征(协议类型、端口、数据包长度、标志位这类字段)重组成矩阵或序列,交给 CNN 卷积神经网络自动提取局部特征,最后做二分类(正常/攻击)或多分类(DoS、Probe、R2L、U2R 等)。它替代了传统的“人工设计特征 + 机器学习分类器”的老路线,主要价值是把特征工程这份苦差事部分交给卷积层去完成。适合三类人:做安全方向毕设和课题复现的学生、想快速验证深度学习入侵检测可行性的工程师、以及刚入门想拿一份 python 源码当教程跟着改的研究者。它不是部署级产品,但作为第一个能跑的模型基线,足够撑起后续优化。
2. 把网络流量喂给 CNN 之前:特征工程与数据集准备
2.1 为什么是 CNN 而不是随机森林或循环神经网络
网络入侵检测的传统做法是梯度提升树或随机森林加手工特征,效果不差,但特征工程占掉一大半精力。CNN 卷积神经网络的优势在于它能自动学习局部特征组合:比如某条连接里 duration 短、src_bytes 异常大、同时 service 是特殊端口,这种“短连接 + 大流量 + 非常见服务”的组合模式,卷积核在滑动过程中会自动提取出来,不需要你显式写规则。
用 CNN 处理表格型流量特征,一般有两条路线:一是套用图像里的二维卷积,把每条连接记录的特征向量 padding 成固定宽高的矩阵,当作灰度图;二是直接用一维卷积 Conv1D,把每条记录看作一个长度为特征数的序列。对于 NSL-KDD、CICIDS2017 这类结构化数据集,我建议先走 Conv1D,原因很简单:特征维度本身是顺序无关的,Conv2D 硬把它们排成矩阵会引入不存在的空间相邻关系,收敛慢且解释性差。Conv1D 的卷积核只在一维方向上滑动,等价于做特征组合筛选,参数更少,小样本下不容易过拟合。
2.2 数据列名与预处理:数值编码、归一化、样本平衡
拿到源码包第一步不是跑模型,是先确认数据长什么样。常见的 NSL-KDD 每条记录有 41 个特征加 1 个标签列,前 4 个特征是离散或类别型(protocol_type、service、flag),中间 32 个是数值型,后 5 个也是数值型。CICIDS2017 更麻烦,列名是英文长描述,还有大量 NaN 和无穷大。无论哪个数据集,CNN 吃的只能是定长数值矩阵,所以预处理是整套源码里最容易翻车、也最值得花时间的地方。
# preprocessing.py # 0. 先读入原始数据,KDD99 系列常见是 42 列,最后一列是标签 import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder raw_df = pd.read_csv("data/train.csv", header=None) # 给列起名,列名数量以你手里的数据为准,别生搬硬套 raw_df.columns = [ "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" ] # 1. 类别特征做 one-hot,数值特征归一化 categorical_cols = ["protocol_type", "service", "flag"] numeric_cols = [c for c in raw_df.columns if c not in categorical_cols + ["label"]] enc = OneHotEncoder(handle_unknown="ignore", sparse_output=False) cat_features = enc.fit_transform(raw_df[categorical_cols]) scaler = StandardScaler() num_features = scaler.fit_transform(raw_df[numeric_cols].replace([np.inf, -np.inf], np.nan).fillna(0)) # 2. 拼回特征矩阵 X = np.hstack([cat_features, num_features]) # 3. 标签编码成整数 label_encoder = LabelEncoder() y = label_encoder.fit_transform(raw_df["label"]) print("特征矩阵形状:", X.shape, "标签类别:", label_encoder.classes_) np.save("data/X.npy", X) np.save("data/y.npy", y)这里的核心点有三个。第一,OneHotEncoder 必须加handle_unknown="ignore",否则训练集里没出现过的 service 值在预测时会让程序直接抛异常;第二,replace([np.inf, -np.inf], np.nan).fillna(0)是处理 CICIDS 这类含脏数据集的常规手段,CNN 对 NaN 极其敏感,一个 NaN 就能让 loss 变成 NaN;第三,sparse_output=False是为了拿到普通 ndarray,方便后面直接 hstack。如果类别特征特别多导致维度膨胀,也可以只对 protocol_type 和 flag 做 one-hot,service 用 hash 编码,但那是后期优化,首次跑通不建议改。
2.3 样本不平衡:先看分布再谈训练
入侵检测数据集几乎都是不平衡的,KDD99 里 Normal 占比能到一半以上,而 U2R、R2L 这类攻击样本往往只有几百条。直接训练出来的模型会无脑倾向多数类。我一般的做法是:训练前打印np.bincount(y),如果某个类别占比小于 1%,先用 class_weight 兜底,不要急着上 SMOTE。
from sklearn.utils.class_weight import compute_class_weight classes = np.unique(y) weights = compute_class_weight(class_weight="balanced", classes=classes, y=y) class_weight_dict = dict(zip(classes, weights)) print("各类别权重:", class_weight_dict)注意,class_weight 只在训练时生效,评估指标必须看每个类别单独的 precision 和 recall,而不是全局 accuracy。这个细节后面第 5 章还会重点讲,因为它是整个复现过程里最隐蔽的翻车点。
3. 用 Keras 搭一个可复现的 CNN 入侵检测模型:核心代码与参数调法
3.1 Conv1D 模型结构:过滤器数量、卷积核大小与池化
预处理做完,特征矩阵已经是定长的数值向量。现在搭 CNN 卷积神经网络,我用 TensorFlow 的 Keras 接口,结构尽量精简:两层 Conv1D + 两层 MaxPooling1D + Flatten + 全连接 + Dropout。不要一上来就堆 ResNet 那种重型结构,入侵检测的数据量撑不起深网络,效果反而更差。
# model.py from tensorflow.keras.models import Sequential from tensorflow.keras.layers import ( Conv1D, MaxPooling1D, Flatten, Dense, Dropout, BatchNormalization ) from tensorflow.keras.optimizers import Adam def build_cnn(input_dim, num_classes): model = Sequential([ # 第一层卷积:64 个过滤器,卷积核大小为 3 Conv1D(filters=64, kernel_size=3, activation="relu", input_shape=(input_dim, 1)), BatchNormalization(), MaxPooling1D(pool_size=2), # 第二层卷积:128 个过滤器,继续提局部特征 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(learning_rate=0.001, clipnorm=1.0), loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) return model参数说明:input_shape=(input_dim, 1)表示把每条记录看成长度为 input_dim、通道数为 1 的序列,这是 Conv1D 的标准输入格式,和图像里 HWC 布局类似,通道数相当于灰度图的 1。kernel_size=3的意思是卷积核每次滑动覆盖 3 个相邻特征,这个值对表格型数据是比较稳妥的起点;改成 5 能覆盖更宽的特征组合,但容易把不相关特征也揉在一起。filters=64/128是卷积核数量,数据量大可以往上加,但 NSL-KDD 这种万级样本,再深收益很小。BatchNormalization放在卷积和激活之后,能明显缓解梯度消失,还能允许你用稍大的学习率。clipnorm=1.0是做梯度裁剪,防止某条异常样本把权重一步打飞导致 loss 变成 NaN——这个是提前给你打预防针。
3.2 训练策略:早停、模型保存与批次大小选择
模型构建好了,接下来是训练脚本。这个环节一定要加 EarlyStopping 和 ModelCheckpoint,原因很实际:深度学习 cnn 训练入侵检测模型时,验证集 loss 通常在 10 个 epoch 左右开始回升,你不加早停,最后保存的往往是一个过拟合版本。
# train.py import numpy as np from sklearn.model_selection import train_test_split from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint from model import build_cnn X = np.load("data/X.npy") y = np.load("data/y.npy") # 分层切分,保证每个类别在训练集和验证集里都有 X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) # Conv1D 需要把最后一个维度显式补出来 X_train = X_train[..., np.newaxis] X_val = X_val[..., np.newaxis] model = build_cnn(input_dim=X_train.shape[1], num_classes=len(np.unique(y))) callbacks = [ EarlyStopping(monitor="val_loss", patience=5, 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=30, batch_size=64, callbacks=callbacks, class_weight=class_weight_dict, verbose=1 )三个关键参数要讲透。batch_size=64是入侵检测数据集的常见选择,样本量不大,256 会导致收敛慢且容易震荡,32 虽然更稳但训练时间长,64 是平衡点。patience=5表示验证集 loss 连续 5 个 epoch 不下降就停止,这是给模型一定容错空间,避免因为单次波动提前收工。restore_best_weights=True会让模型回滚到验证集最优的权重,这个必须开,否则 EarlyStopping 触发时保存的是停止那一刻的权重,指标反而变差。class_weight 参数直接喂给 fit,Keras 会在每个 batch 计算 loss 时自动放大少数类的权重,比手工过采样省事得多。
3.3 评估指标:别只盯着 accuracy 看
训练完第一件事不是看 accuracy,是算混淆矩阵和分类的 precision、recall、F1。入侵检测场景里,漏报攻击的代价远高于误报正常流量,所以 recall 才是第一指标。
# evaluate.py from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred_proba = model.predict(X_val) y_pred = np.argmax(y_pred_proba, axis=1) print(classification_report(y_val, y_pred, target_names=label_encoder.classes_)) print(confusion_matrix(y_val, y_pred)) # 单独看攻击类别的召回率是否偏低 report = classification_report(y_val, y_pred, output_dict=True) for cls in label_encoder.classes_: if cls != "normal": print(f"{cls} recall = {report[cls]['recall']:.4f}")这里的逻辑是:分类报告会按类别输出,你要重点盯攻击类别的 recall,尤其是样本数极少的 R2L、U2R。如果 accuracy 有 95% 但 R2L 的 recall 是 0,说明模型直接把这个类全判错了,class_weight 或者数据增强还没吃透。
4. 拿到源码包后怎么跑通与验证:项目结构、训练顺序与指标判读
4.1 解压之后先看这三个文件
不要急着跑 train.py。任何一份成体系的 python 源码包,拿到手先找三个文件:README 或项目说明、requirements.txt、以及一个数据目录说明。我见过太多人解压后直接python train.py,然后被 ModuleNotFoundError 和维度不匹配轮番折磨。合理顺序是:
# 1. 创建虚拟环境,避免污染系统 python python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate # 2. 安装依赖 pip install -r requirements.txt # 3. 先看项目说明,通常会有运行顺序和预期输出标准源码包的项目结构一般长这样:data/放原始 CSV 和预处理后的 npy 文件,preprocessing.py负责数据清洗和特征构建,model.py放网络结构,train.py是训练入口,evaluate.py或predict.py做测试和推理,requirements.txt锁依赖版本。如果你手里的包结构不一样也没关系,按“数据处理 → 模型定义 → 训练 → 评估”这条主链去对应就行。
这里要强调一点:requirements.txt 里的版本号是作者在自己机器上验证过的,不要盲目升级。比如 tensorflow 2.x 的 API 在 2.13 之后有些行为变化,如果你直接装最新版,源码里写的Adam(lr=...)这种旧式用法就会报错。稳妥做法是完全按 requirements 装,跑通后再考虑升级。
4.2 训练预期:loss 曲线怎么看才算正常
训练开始后,你要关注两个东西:训练 loss 是否在稳步下降,以及验证 loss 和训练 loss 的差距是否快速拉大。如果前 5 个 epoch 训练 loss 下降但验证 loss 纹丝不动,说明模型容量不够或特征预处理有问题;如果两者都在降但差距超过 30%,说明过拟合正在发生,靠早停能救一部分,但更根本的办法是增大 Dropout 或减小网络宽度。
监控曲线时还要看 batch 粒度还是 epoch 粒度。Keras 默认 verbose=1 会输出每个 epoch 末的指标,但我们更该关心的是验证集指标在最后几个 epoch 的表现。如果 best_model.h5 的保存条件是 val_accuracy,而早停条件是 val_loss,两者选中的模型可能不是同一个权重,这是设计上的小坑。我一般把 ModelCheckpoint 的 monitor 也改成 val_loss,让“最优”的定义保持一致,避免最后拿到的模型和训练过程中表现最好的模型错位。
4.3 验证集结果和公开基线对标:用什么口径比
模型训练完,你拿到的准确率、召回率需要有个参照物。常见做法是看这套源码在数据集上的分类报告和论文里的基线对比,但要注意口径差异:一是部分作者用全部数据训练再在测试集评估,那属于数据泄露,指标不可信;二是有的源码只做二分类(normal/attack),有的是五分类,五分类的准确率天然低于二分类,不能直接比。我一般先把二分类结果跑出来作为上界,再去展开多分类。如果多分类里每个攻击类别的 recall 都过 80%,这个模型的实用性就算过关了。
5. 入侵检测模型复现避坑:五个最容易翻车的地方与排查路径
5.1 现象:loss 变成 NaN,训练直接中断
原因很简单:输入数据里残留了 NaN 或无穷大,或者学习率偏大导致梯度爆炸。入侵检测数据集尤其是 CICIDS2017,原始 CSV 里有大量空值和 Infinity,预处理只要漏掉一个,卷积层算出来的中间值就会溢出。
解决:在预处理脚本里统一做np.nan_to_num(X),并用replace([np.inf, -np.inf], np.nan).fillna(0)做清洗。同时在优化器上加clipnorm=1.0做梯度裁剪,双保险。如果训练中依然偶发 NaN,还要检查是否出现了除零操作,比如自定义 loss 或指标函数里分母是 precision + recall 时没有加 epsilon。
5.2 现象:准确率 95%,但每个攻击类别 recall 全是 0
这是最典型的不平衡数据翻车现场。模型学到的最优策略是什么都不判,全输出 Normal,因为 Normal 占绝大多数,光靠猜就有 90% 以上准确率。训练过程看起来一切正常,loss 在降,accuracy 在涨,但混淆矩阵一出来就露馅。
解决:训练前用compute_class_weight给少数类加权,如果效果不够,再对少数类做 SMOTE 过采样。但 SMOTE 要做好只作用在训练集上,别把验证集也生成一遍,否则验证指标虚高得离谱。调权重时注意一个细节:class_weight 只在 Keras 内部计算 loss 时生效,最终评估还是要回到原始分布的数据上,否则你算出来的 precision 没有参考意义。
5.3 现象:训练时维度正常,预测时却报维度不匹配
原因基本锁定在 one-hot 编码上。训练集的service列里有 66 个类别,编码后多了 66 列;预测时的数据里如果出现新 service 值,handle_unknown="ignore"能保证不报错,但列数对不上就会崩。更隐蔽的是,如果你用 pandas 的get_dummies而不是 sklearn 的 OneHotEncoder,训练和预测的列顺序可能因为数据内容不同而错位。
解决:统一用 sklearn 的 OneHotEncoder,训练完成后用joblib.dump(enc, "artifacts/encoder.pkl")保存编码器和 scaler,预测时重新加载。这个编码器要跟着模型一起走,否则换一台机器推理就废了。
# predict.py import joblib import numpy as np encoder = joblib.load("artifacts/encoder.pkl") scaler = joblib.load("artifacts/scaler.pkl") new_sample = pd.DataFrame([["tcp", "http", "SF", 181, 5450]], columns=["protocol_type", "service", "flag", "src_bytes", "dst_bytes"]) cat = encoder.transform(new_sample[categorical_cols]) num = scaler.transform(new_sample[numeric_cols].fillna(0)) X_new = np.hstack([cat, num]).reshape(1, -1, 1) prob = model.predict(X_new) print("预测类别:", label_encoder.inverse_transform([np.argmax(prob)])[0])5.4 现象:训练时 shuffle 后效果虚高,部署后直线下降
很多源码在train_test_split之后还会再做一次np.random.shuffle,这在小数据集上是常规操作。但对于按时间采集的入侵检测数据,shuffle 会把时间相关性打散,模型学到的是“整个时间窗口的全局统计特征”,而不是“当前流量切片”的特征。真实部署时流量是流式到达的,这种模型往往第一天就失灵。
解决:如果你手里的源码是拿时间序列数据集做的,改成按时间戳切分,train 取前 80%,test 取后 20%,并且训练时不要 shuffle。如果不确定数据是否按时间排序,打开原始 CSV 看看时间戳列,有的话务必按时间切。
5.5 现象:训练到一半内存爆掉,卡死不动
入侵检测特征经过 one-hot 后维度可能到 120 以上,如果把每条记录继续 pad 成 8x8 的矩阵,内存开销还会翻几倍。Keras 默认把整个数据集加载进内存做 batch 切分,NSL-KDD 几万条没问题,但 CICIDS2017 几百万条直接吃满 32G 内存。
解决:改用tf.data.Dataset.from_tensor_slices配合batch()和prefetch(),让数据按 batch 流式读取。另一个办法是训练前检查X.shape,如果特征维度超过 200,考虑删掉冗余的 one-hot 列或改用 Embedding 层做离散特征编码,别硬扛。
6. 从分类准确率到实战可用:阈值校准与小流量切片
模型训练完只完成了 70% 的工作,剩下 30% 是把“模型能分类”变成“系统能决策”。对于一个二分类甚至多分类模型,默认的 argmax 阈值 0.5 并不适合入侵检测——攻击样本少且难以识别,直接套 0.5 往往让 recall 偏低。正确的做法是画 precision-recall 曲线,按业务偏好选阈值。
# threshold_tuning.py from sklearn.metrics import precision_recall_curve import numpy as np # 假设二分类,取正类的预测概率 probs = model.predict(X_val)[:, 1] precision, recall, thresholds = precision_recall_curve(y_val, probs) # 怎在漏报和误报之间做取舍,这里给一个 F1 最大化的示范 f1_scores = 2 * precision * recall / (precision + recall + 1e-9) best_idx = np.argmax(f1_scores) best_threshold = thresholds[best_idx] print("最佳阈值:", best_threshold, "对应 F1:", f1_scores[best_idx])调完阈值,再谈实战落地。源码里的模型是按“单条连接记录”做分类的,但真实网络流量是连续数据流,没有天然的“一条记录”。常见做法是用 Scapy 抓包,按固定时间窗口或固定包数切出流量切片,再把这些切片转成特征表喂给模型。窗口大小一般取 2 到 5 秒,太短特征统计不稳定,太长会被攻击流量平均掉,检测时效性也差。这一步做好后,模型才能从“离线复现”走向“实时检测原型”。
最后说一个我的血泪经验:第一次复现这类项目时,我盯着 accuracy 调了一周,直到把混淆矩阵打印出来才意识到模型在偷懒。从那以后,我的习惯是先跑通最小二分类、再扩多分类,每改一个参数都重新算一次每个类别的 recall。调阈值和剪枝这类优化手段是后半场的玄学,但数据清洗和指标口径才是决定模型能不能用的基本功。这套流程走下来,你手里的这份 python 源码至少能变成一个你真正理解的检测基线,而不是一个跑完就丢的黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取