ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+CNN网络入侵检测实战:从NSL-KDD到模型部署

Python+CNN网络入侵检测实战:从NSL-KDD到模型部署 简介这份资源面向计算机相关专业的毕业设计、课程设计及期末大作业需求者提供一套基于Python与CNN卷积神经网络的网络入侵检测完整项目。项目经导师指导并获评审99分认可代码完整可运行适合零基础学习者上手实践。压缩包共33个文件约21.58MB包含4个Python脚本训练、预测、数据预处理与模型定义、12个CSV数据集文件、7个XML配置、1个pth模型权重及png/jpg结果图等覆盖从数据加载到模型训练评估的全流程。资源基于NSL-KDD数据集配套README说明文档目录结构清晰便于按模块查阅。已有115人学习下载。读者可获得可直接运行的源码、完整数据集、训练好的模型文件及准确率与精确率可视化结果快速复现实验并理解CNN在入侵检测中的应用为毕业设计或项目实战提供可靠参考。1. 从一次校园网告警说起PythonCNN 做网络入侵检测到底在做什么凌晨两点校园网出口流量告警一台内网主机在十分钟内向 300 多个不同 IP 发起了 SSH 连接尝试。值班的同学第一反应是是不是有人在扫段但翻日志翻了半小时也没定位到具体是哪条流量触发的。这类场景就是网络入侵检测Network Intrusion DetectionNID最典型的用武之地不是等主机被拿下再去查而是在流量层面就把异常行为识别出来。传统做法靠 Snort、Suricata 这类规则引擎写规则、匹配特征命中就报警问题是规则是死的攻击者稍微改一下端口、改一下载荷编码规则就失效了。这几年把 CNN 卷积神经网络搬进入侵检测核心动机就是让模型自己从流量特征里学模式而不是靠人一条条写规则。这篇笔记围绕基于 PythonCNN 网络实现入侵检测这条线展开从数据集怎么选、特征怎么处理、CNN 结构怎么搭、训练怎么调参一直讲到部署时怎么把模型接进实际流量管道。适合正在做计算机毕业设计、想找一个能跑通又有技术含量的题目的同学也适合已经会 Python 但没碰过深度学习的运维或安全从业者。读完你应该能自己复现一套完整的训练推理流程并且知道哪些参数不能乱动、哪些坑一定会踩。2. 数据集与特征工程NSL-KDD 和 CIC-IDS 怎么选、怎么洗2.1 为什么不能直接拿原始 pcap 喂给 CNN很多人第一反应是我有 pcap 包直接丢给模型不就行了。这条路在入侵检测里基本走不通原因有三个。第一pcap 是变长字节流一次会话可能几十字节也可能几兆CNN 要求定长输入你得先切窗、对齐、填充处理不好就把关键信息截掉了。第二原始字节里大量是协议头、校验和、时间戳这类噪声模型很容易学到端口号22 就是 SSH 攻击这种伪相关。第三标注成本极高你很难给每一个包打上正常/攻击的标签。所以业界主流做法是先做流特征提取把一次会话五元组相同的双向流压缩成几十维的统计特征再喂给模型。这也是 NSL-KDD、CIC-IDS2017 这些数据集存在的原因——它们已经把 pcap 处理成了结构化特征表。常见的数据集选择数据集特征维度样本量级适用场景KDD Cup 9941约 500 万教学入门但太老重复样本多NSL-KDD41约 15 万去掉了 KDD99 的冗余毕业设计首选CIC-IDS201778约 280 万更接近现代流量含 DDoS、暴力破解等UNSW-NB1549约 250 万攻击类型更细适合做多分类毕业设计如果只求跑通NSL-KDD 足够如果想体现贴近真实场景用 CIC-IDS2017 更有说服力但清洗工作量会翻倍。2.2 用 pandas 把 NSL-KDD 洗成 CNN 能吃的格式NSL-KDD 的原始文件是KDDTrain.txt和KDDTest.txt逗号分隔最后一列是标签倒数第二列是 difficulty level这个要丢掉。下面是我一般用的清洗脚本import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler, LabelEncoder # NSL-KDD 的 41 个特征列名官方文档里有这里直接写死 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_df pd.read_csv(KDDTrain.txt, namescol_names) test_df pd.read_csv(KDDTest.txt, namescol_names) # 丢掉 difficulty 列它对检测没有意义 train_df.drop(difficulty, axis1, inplaceTrue) test_df.drop(difficulty, axis1, inplaceTrue) # 把 39 种具体攻击归并成 5 大类降低分类难度 attack_map { normal:normal, back:dos,land:dos,neptune:dos,pod:dos,smurf:dos, teardrop:dos,apache2:dos,udpstorm:dos,processtable:dos,worm: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,httptunnel:r2l, buffer_overflow:u2r,loadmodule:u2r,perl:u2r,rootkit:u2r, httptunnel:u2r,ps:u2r,sqlattack:u2r,xterm:u2r } train_df[label] train_df[label].map(attack_map) test_df[label] test_df[label].map(attack_map) # 类别型特征做 one-hot数值型做归一化 cat_cols [protocol_type,service,flag] train_df pd.get_dummies(train_df, columnscat_cols) test_df pd.get_dummies(test_df, columnscat_cols) # 对齐列防止 train/test 的 one-hot 列不一致 test_df test_df.reindex(columnstrain_df.columns, fill_value0) # 数值列归一化到 [0,1]CNN 对输入尺度很敏感 num_cols [c for c in train_df.columns if c ! label] scaler MinMaxScaler() train_df[num_cols] scaler.fit_transform(train_df[num_cols]) test_df[num_cols] scaler.transform(test_df[num_cols]) # 标签编码 le LabelEncoder() train_df[label] le.fit_transform(train_df[label]) test_df[label] le.transform(test_df[label]) print(训练集形状:, train_df.shape) print(测试集形状:, test_df.shape) print(类别映射:, dict(zip(le.classes_, le.transform(le.classes_))))这段代码有几个关键点必须说清楚。attack_map把 39 种细粒度攻击归成 5 类是因为原始标签里很多类样本只有几十条直接做多分类会导致严重不均衡模型根本学不动。归成 DoS、Probe、R2L、U2R、Normal 五类后每类样本量至少上千训练才稳。reindex那一步是血泪经验one-hot 之后 train 和 test 的列数经常不一致比如 test 里出现了 train 没见过的 service 值不手动对齐后面喂进模型直接报维度错误。归一化必须用fit_transform在训练集上拟合、transform在测试集上应用不能两边各 fit 一次否则测试集的分布信息泄漏进训练评估结果虚高。2.3 把表格数据 reshape 成 CNN 需要的图像形状CNN 天生是处理二维/三维数据的一维特征向量直接喂进去要么用 1D-CNN要么 reshape 成伪图像。我一般用 1D-CNN因为入侵检测的特征之间没有空间邻接关系强行排成方阵反而引入噪声。但如果你想让代码看起来更像 CNN也可以 reshape 成(n, 8, 8)这种前提是特征维度刚好能开方或者补零到平方数。from sklearn.model_selection import train_test_split X train_df.drop(label, axis1).values.astype(np.float32) y train_df[label].values # 1D-CNN 输入形状: (样本数, 特征数, 通道数) X X.reshape(X.shape[0], X.shape[1], 1) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(训练:, X_train.shape, 验证:, X_val.shape)stratifyy保证切分后各类比例一致不然某一类可能全被切到验证集训练集里就没了。reshape加的那个通道维度是给 Conv1D 用的如果后面用 Conv2D就得改成(n, h, w, 1)。3. 用 Keras 搭一个能跑通的 CNN结构、参数和训练曲线3.1 网络结构怎么定三层卷积够不够入侵检测的特征维度通常只有几十到一百多比图像小得多所以网络不需要很深。我试过 ResNet、Inception 这些结构在这个任务上反而不如三层卷积原因是参数太多、过拟合严重。一个稳定的基线结构是这样import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_dim, num_classes): model models.Sequential([ # 第一层卷积提取局部特征组合 layers.Conv1D(filters64, kernel_size3, paddingsame, activationrelu, input_shape(input_dim, 1)), layers.BatchNormalization(), layers.MaxPooling1D(pool_size2), # 第二层感受野扩大捕捉跨特征模式 layers.Conv1D(filters128, kernel_size3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling1D(pool_size2), # 第三层高层抽象 layers.Conv1D(filters128, kernel_size3, paddingsame, activationrelu), layers.GlobalAveragePooling1D(), # 全连接分类头 layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model model build_cnn(X_train.shape[1], len(le.classes_)) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) model.summary()参数说明filters64/128是经验值特征维度小于 50 时用 32/64 就够再大就是浪费算力。kernel_size3是 1D 卷积的常用选择等于同时看相邻 3 个特征如果你的特征做过相关性排序可以试 5。paddingsame保证卷积后长度不变配合 MaxPooling 逐步降维。GlobalAveragePooling1D替代 Flatten能大幅减少全连接层参数降低过拟合。Dropout(0.5)放在全连接前是标配如果验证集 loss 一直比训练集高很多可以加到 0.6。BatchNormalization放在卷积后、激活前还是激活后Keras 里习惯放激活后实测差别不大但一定要有否则学习率稍大就梯度爆炸。3.2 训练时的回调配置和曲线判读callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ), tf.keras.callbacks.ModelCheckpoint( best_cnn_ids.h5, monitorval_accuracy, save_best_onlyTrue, verbose1 ) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs50, batch_size128, callbackscallbacks, verbose1 )EarlyStopping的patience5意思是验证 loss 连续 5 轮不下降就停restore_best_weightsTrue保证最后留下的是最优轮次的权重而不是最后一轮的。ReduceLROnPlateau在 loss 卡住时把学习率砍半是突破平台期的常用手段。训练完看曲线如果训练 accuracy 到 99% 但验证只有 85%说明过拟合加 Dropout 或减层如果两条曲线都上不去说明欠拟合加层或加 filter如果验证 loss 震荡剧烈把 batch_size 调大或学习率调小。3.3 评估不能只看 accuracy混淆矩阵和各类召回率入侵检测里 accuracy 是最容易骗人的指标。NSL-KDD 里 normal 样本占一半以上模型全预测 normal 也能有 50% 的 accuracy但 U2R 这类攻击一条都抓不到。必须看每一类的召回率from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt y_pred model.predict(X_val).argmax(axis1) print(classification_report(y_val, y_pred, target_namesle.classes_)) cm confusion_matrix(y_val, y_pred) plt.figure(figsize(8,6)) sns.heatmap(cm, annotTrue, fmtd, xticklabelsle.classes_, yticklabelsle.classes_) plt.xlabel(Predicted); plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)重点看 U2R 和 R2L 的召回率这两类样本少、特征隐蔽召回率低于 60% 是常态。如果毕业设计答辩被问到为什么 U2R 效果差标准回答是样本不均衡 特征区分度低可以提一句用 Focal Loss 或 SMOTE 过采样作为改进方向。4. 避坑与排查训练不收敛、指标虚高、部署翻车的真实记录4.1 现象训练 loss 一直是 nan原因输入特征里有 inf 或极大值。NSL-KDD 的src_bytes、dst_bytes原始值能到 10^9如果归一化之前没检查MinMaxScaler 算出来还是极大值进网络就溢出。解决归一化前先df.replace([np.inf, -np.inf], np.nan).dropna()或者用np.log1p对长尾特征做对数变换再归一化。4.2 现象验证集 accuracy 99%换一批真实流量全错原因训练集和测试集来自同一分布但真实流量分布不同。NSL-KDD 是 1998 年的数据里面的 service 类型和现在的 HTTP/2、QUIC 完全对不上one-hot 之后全是零向量。解决做跨数据集验证用 NSL-KDD 训练、CIC-IDS2017 测试如果掉到 60% 以下说明模型没学到通用特征。改进方向是去掉强依赖具体协议的特征如 service 的 one-hot改用统计特征。4.3 现象模型文件 200MB部署到树莓派跑不动原因全连接层参数太多。128 维特征接 128 个神经元参数量是 128×12816384加上前面的卷积层模型轻松上百万参数。解决用GlobalAveragePooling1D替代Flatten把 Dense 层从 128 降到 64或者训练完后做量化tf.lite.TFLiteConverter转成 tflite体积能压到 1/4推理速度提升 3 倍以上。4.4 现象推理时每条流量要 50ms吞吐上不去原因Keras 默认按 batch 推理单条输入时框架开销占大头。解决攒够 64 条再批量推理或者用 ONNX Runtime 替换 TensorFlow 后端单条延迟能降到 5ms 以内。如果做在线检测建议用消息队列缓冲不要来一条算一条。4.5 现象同一份代码别人跑 accuracy 98%我跑只有 80%原因随机种子没固定。Keras 的权重初始化、Dropout、数据切分都带随机性。解决在代码开头加import os, random os.environ[PYTHONHASHSEED] 42 random.seed(42) np.random.seed(42) tf.random.set_seed(42)注意 GPU 上的某些算子仍然有非确定性完全复现需要设tf.config.experimental.enable_op_determinism()但会牺牲速度。5. 从训练脚本到可用工具模型导出、批量推理和阈值调优5.1 把 h5 模型转成推理友好的格式训练完的best_cnn_ids.h5只适合继续训练部署时建议转成 SavedModel 或 ONNX# 方式一SavedModelTensorFlow Serving 直接加载 model.save(ids_saved_model) # 方式二ONNX跨框架推理 import tf2onnx spec (tf.TensorSpec((None, X_train.shape[1], 1), tf.float32, nameinput),) tf2onnx.convert.from_keras(model, input_signaturespec, opset13, output_pathids_model.onnx)ONNX 的好处是推理时不需要装完整的 TensorFlow用onnxruntime只要几十 MB适合塞进 Docker 镜像或者边缘设备。5.2 批量推理脚本从 CSV 到告警import onnxruntime as ort import pandas as pd import numpy as np sess ort.InferenceSession(ids_model.onnx) input_name sess.get_inputs()[0].name def predict_batch(csv_path, scaler, le): df pd.read_csv(csv_path) # 注意这里必须复用训练时的 scaler 和 one-hot 列顺序 df pd.get_dummies(df, columns[protocol_type,service,flag]) df df.reindex(columnstrain_columns, fill_value0) X scaler.transform(df.values).astype(np.float32) X X.reshape(X.shape[0], X.shape[1], 1) logits sess.run(None, {input_name: X})[0] probs np.exp(logits) / np.exp(logits).sum(axis1, keepdimsTrue) preds probs.argmax(axis1) confs probs.max(axis1) # 置信度低于阈值的不报减少误报 alerts [(i, le.classes_[p], c) for i,(p,c) in enumerate(zip(preds,confs)) if c 0.85 and p ! 0] return alerts这里的关键是train_columns必须保存下来推理时严格按训练时的列顺序对齐少一列多一列都会导致结果错乱。置信度阈值 0.85 是我在 CIC-IDS2017 上试出来的平衡点低于这个值误报明显增多高于 0.95 又会漏掉一些低置信度的真实攻击。5.3 阈值怎么调用 PR 曲线而不是 ROC入侵检测是典型的不均衡分类ROC 曲线会被大量真负例拉高看起来 AUC 0.99 很漂亮实际召回率可能只有 70%。应该看 Precision-Recall 曲线from sklearn.metrics import precision_recall_curve, average_precision_score # 以二分类为例正常 vs 攻击 y_bin (y_val ! 0).astype(int) p_attack 1 - model.predict(X_val)[:, 0] # 攻击类概率 precision, recall, thresholds precision_recall_curve(y_bin, p_attack) ap average_precision_score(y_bin, p_attack) print(fAverage Precision: {ap:.4f}) # 找 F1 最大的阈值 f1 2 * precision * recall / (precision recall 1e-8) best_thr thresholds[f1.argmax()] print(f最佳阈值: {best_thr:.4f}, F1: {f1.max():.4f})实际部署时安全团队通常更在意召回率宁可误报也别漏报所以阈值可以比 F1 最优点再低 0.05 左右。这个取舍没有标准答案取决于你的告警处理能力——如果一天只能处理 100 条告警阈值就得调高如果有 SOC 团队 7×24 值班可以放宽。5.4 一个我踩过的坑别用测试集调阈值刚做这个方向时我直接在测试集上找最佳阈值然后报告F1 0.96。答辩时老师问了一句你这个阈值是在哪部分数据上选的当场卡住。正确做法是把训练集再切出一部分做验证集在验证集上选阈值测试集只用来做最终评估全程不参与任何调参。这个习惯后来帮我省了很多麻烦也让我在写论文时能理直气壮地说测试集从未参与模型选择。如果你正在做毕业设计建议把这条写进实验设置章节答辩时是个加分项。希望帮到你。本文还有配套的精品资源点击获取
返回列表