
简介这是一套以KDD数据集为实验基础、融合深度学习和机器学习技术的入侵检测系统IDS项目资料面向网络安全方向学习者与算法研究者可用于理解网络流量数据的异常检测与恶意行为识别流程。压缩包共包含90个文件其中Python脚本26个覆盖数据预处理、模型训练、测试与准确率计算CSV文件3个存放训练与测试数据集txt文本10个记录各算法的预测标签和概率结果另有XML工程配置、PDF学术论文及备份文件整体体积约18.48MB。资料实现并对比了决策树、随机森林、SVM、朴素贝叶斯等经典机器学习算法以及多个深度网络系列模型的训练与预测配套多个脚本、测试脚本和预测结果文件可直接用于复现实验、调整参数和评估AUC等指标。适合希望掌握入侵检测建模全流程、并进行算法对比与实践改进的中高级学习者。已有132人学习下载。1. 基于深度学习和机器学习的入侵检测系统先回答四个问题再动手把“基于深度学习和机器学习的入侵检测系统”做成能交付的检测服务工程师要回答的第一个问题不是“用 CNN 还是 XGBoost”而是“到底检测什么”。流量、主机日志、告警事件三种输入的特征与处理管线完全不同二分类只看正常与攻击多分类要识别 DoS、扫描、提权等具体攻击类型评估口径也会从准确率换成召回率与误报率。我的惯用路线是先用传统机器学习模型在公开数据集上打一个可解释的基线再用深度学习模型在大流量、高维度特征场景里做提升最后落到实时检测的工程改造上。这篇笔记按同样的顺序展开覆盖数据集选型、特征工程、模型训练、翻车排查和上线压误报适合刚接手机器学习检测类项目、或者已经在尝试深度学习模型却卡在效果验证阶段的人。2. 数据集与特征工程NSL-KDD 与 UNSW-NB15 的选型以及 41 维特征怎么落表数据集选型决定了后面所有工作的可信度。选一个过于简单的数据集模型再花哨也说服不了别人选一个过于复杂的数据集早期调参会把时间消耗在类别不平衡上。常见做法是先拿经典数据集跑通全流程再换现代数据集验证泛化。2.1 NSL-KDD 与 UNSW-NB15 数据集怎么选我见过不少团队直接挑最新的数据集理由是“老数据集不能反映现在的攻击”。这句话方向对但忽略了工程目标你要的是可复现的基线而不是一开始就陷入数据清洗。下表是我常用的选型口径。数据集规模量级覆盖攻击类型特点适合用途NSL-KDD训练约 12.5 万条4 大类约 39 种经典标注干净研究基准多快速验证算法、调参对照UNSW-NB15约 25 万条9 大类更接近现代流量带时间戳工程预研、时序特征验证CICIDS2017数百万条12 大类以上样本大、场景全、类别极不平衡上线前压力测试、容量评估NSL-KDD 虽然老但它的价值在于“已知的底线”网上有大量公开的准确率和 F1 参考值模型跑出来偏离太远说明代码有问题而不是数据集有问题。UNSW-NB15 带时间戳这对要做序列模型的团队很重要因为可以按时间划分训练集与测试集而不是随机划分。CICIDS2017 的体量适合验证深度学习模型的容量上限但它的类别不平衡程度会干扰早期判断一般放最后用。2.2 数据加载与预处理数值标准化、类别 one-hot、标签分层NSL-KDD 的原始文件是 KDDTrain.txt 和 KDDTest.txt没有表头每行 41 个特征加一个攻击标签。加载时先补列名再拆分类别特征与数值特征这是机器学习应用流程里最容易被跳过的一步。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler 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 ] def load_nslkdd(path): df pd.read_csv(path, headerNone, namesCOLUMNS [label]) df[label_binary] (df[label] ! normal).astype(int) return df train_df load_nslkdd(KDDTrain.txt) test_df load_nslkdd(KDDTest.txt)这里的 label_binary 先把问题简化成二分类目的是先跑通全流程。项目进入多分类阶段时再把它换成攻击类型编码比如把 neptune、smurf 归为 DoS把 ipsweep、portsweep 归为 Probe逻辑不变。二分类能更快暴露特征处理和评估链路的问题。接下来是特征处理的核心代码。数值特征需要标准化但标准化参数只能从训练集学测试集只做 transform类别特征做 one-hot 时测试集里可能出现训练集没有的取值必须用 reindex 对齐。cat_features [protocol_type, service, flag] num_features [c for c in COLUMNS if c not in cat_features] def preprocess(train_df, test_df, num_features, cat_features): # 数值特征只在训练集上 fit测试集只 transform防止数据泄漏 scaler StandardScaler() X_num_train scaler.fit_transform(train_df[num_features]) X_num_test scaler.transform(test_df[num_features]) # 类别特征按训练集列生成 one-hot测试集里没见过的值置 0 train_cat pd.get_dummies(train_df[cat_features], dummy_naFalse) test_cat pd.get_dummies(test_df[cat_features], dummy_naFalse) test_cat test_cat.reindex(columnstrain_cat.columns, fill_value0) X_train pd.concat([pd.DataFrame(X_num_train, columnsnum_features), train_cat], axis1) X_test pd.concat([pd.DataFrame(X_num_test, columnsnum_features), test_cat], axis1) return X_train, X_test, scaler X_train, X_test, scaler preprocess(train_df, test_df, num_features, cat_features) y_train, y_test train_df[label_binary], test_df[label_binary]StandardScaler 对每个数值特征减均值、除方差对线性模型和深度学习模型都是必需品树模型不依赖缩放但保留统一的预处理流程可以让特征在不同模型间做横向对比。one-hot 之后 service 字段会展开成几十列总特征维度一般会到 120 维左右这会影响后面的模型输入形状提前心里有数。python 机器学习常用包无非 pandas、scikit-learn 这一套这里已经覆盖了主流程。2.3 特征选择哪些流量特征对攻击识别最有用特征选择不是必须做但对模型上线有实际帮助。NSL-KDD 里有些特征在训练集上分布很“干净”比如 num_outbound_cmds 几乎全为 0有些特征如 src_bytes、count、dst_host_srv_count 对攻击识别贡献很大。用随机森林的特征重要性和互信息两个维度交叉看能避免单一方法偏差。from sklearn.ensemble import RandomForestClassifier from sklearn.feature_selection import mutual_info_classif rf_tmp RandomForestClassifier(n_estimators100, max_depth10, random_state42, n_jobs-1) rf_tmp.fit(X_train, y_train) imps pd.Series(rf_tmp.feature_importances_, indexX_train.columns) imps imps.sort_values(ascendingFalse) print(imps.head(10)) mi pd.Series(mutual_info_classif(X_train, y_train), indexX_train.columns) mi mi.sort_values(ascendingFalse) print(mi.head(10))随机森林特征重要性偏向取值多的特征互信息对非线性关系更敏感两列排名交叉看比只看一列可靠。如果某个特征在两个列表里都靠前比如 src_bytes、count说明它对攻击模式区分度强如果只在其中一个靠前就要谨慎判断。实际工程里还有一个更重要的原则训练时用的特征必须在实时流量里能拿到。src_bytes、dst_bytes、count 这类在流量侧好提取但有些主机侧特征在上线时拿不到就该提前删掉而不是等部署时发现特征缺失。3. 传统机器学习基线用随机森林给入侵检测定及格线很多人拿到入侵检测项目第一反应是上深度学习这个顺序容易让问题复杂化。深度学习模型 cn 识别恶意软件的论文很多但复现时效果不稳定。我的习惯是先跑一个随机森林用五折交叉验证拿到一个可信的数字这个数字就是后面所有工作的及格线。3.1 为什么先跑传统机器学习模型而不是直接上深度学习传统机器学习模型的训练速度快、参数少、可解释性强。NSL-KDD 这种规模的数据随机森林几十秒就能训完还能直接输出特征重要性帮你在做深度学习前就发现特征问题。另一个原因是评估成本。深度学习模型训练一次可能几十分钟调参周期长随机森林可以快速试错不同特征组合等特征稳定了再把这些特征交给深度学习模型能少走很多弯路。机器学习算法这么多选型时判断标准不是“越新越好”而是“在固定评估口径下谁更稳”。3.2 随机森林基线训练代码与参数说明先划分训练集和验证集注意用 stratify 做分层抽样保证验证集里的攻击类别比例和训练集一致。然后做五折交叉验证评估指标用 F1而不是准确率。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score from sklearn.metrics import classification_report X_train_s, X_val_s, y_train_s, y_val_s train_test_split( X_train, y_train, test_size0.2, stratifyy_train, random_state42 ) model RandomForestClassifier( n_estimators300, max_depth20, min_samples_leaf1, class_weightbalanced_subsample, n_jobs-1, random_state42 ) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(model, X_train_s, y_train_s, cvcv, scoringf1_macro) print(F1 macro: %.3f ± %.3f % (scores.mean(), scores.std())) model.fit(X_train_s, y_train_s) print(classification_report(y_val_s, model.predict(X_val_s), zero_division0))参数说明n_estimators 设为 300在 NSL-KDD 这个量级下已经足够继续增大到 500 以上收益递减只会拖慢训练max_depth 限制在 20 是为了防止树过度记忆训练集的噪声class_weight 用 balanced_subsample让每棵树的自助采样按类别比例加权这对正常流量占多数、攻击流量占少数的场景是刚需n_jobs-1 让所有 CPU 核一起跑。交叉验证的 scoring 用 f1_macro因为 accuracy 会被多数类拉高看 F1 才能发现少数类攻击是否被模型忽略。3.3 把 XGBoost 当作第二对照组差异往往出在特征而非模型随机森林跑通之后再加一个 XGBoost 作为对照组用于判断“还能不能涨点”的瓶颈在模型侧还是特征侧。机器学习项目里最常见的情况是换更强的模型效果提升不超过 1%但换了一组特征后 F1 直接涨 5%。所以第二模型不是为了炫技是为了定位问题。from xgboost import XGBClassifier pos sum(y_train_s) neg len(y_train_s) - pos xgb XGBClassifier( n_estimators200, max_depth6, learning_rate0.1, scale_pos_weightneg / pos, eval_metricmlogloss, use_label_encoderFalse, random_state42 ) xgb.fit(X_train_s, y_train_s) print(classification_report(y_val_s, xgb.predict(X_val_s), zero_division0))scale_pos_weight 是二分类时处理不平衡的关键参数值取负样本数除以正样本数。多分类时没有这个参数改用 sample_weight 传入每个样本的权重或者直接依赖 class_weight 思路。如果随机森林和 XGBoost 的验证结果差不多说明特征已经接近当前的表达上限接下来做深度学习时应该期待的是特征自动组合带来的收益而不是奇迹。4. 深度学习流量检测一维 CNN 与 LSTM 的结构选择、训练参数和评估口径深度学习在入侵检测里的价值在于自动学习特征组合尤其是流量统计特征之间的非线性关系。但输入形状的设计直接决定模型能不能收敛这里有一个常见误区需要先讲清楚。4.1 什么时候用 CNN、什么时候用 LSTM很多人看到深度学习模型 CNN 识别恶意软件的论文第一反应是把每一条流量记录 reshape 成二维矩阵当图片送进卷积网络。这个做法我不推荐因为流量特征是一维向量强行变成二维图片会破坏特征之间的位置关系卷积核扫到的“相邻像素”在语义上毫无关联。我一般这样选型特征组合关系重要、样本本身没有明显时序性时用一维 CNN流量流本身有先后顺序、攻击行为体现在时间窗口内的行为变化时用 LSTM两者常常一起用先让 CNN 提取局部组合再让 LSTM 捕捉上下文。输入形状可以保持一维序列而不是图片。4.2 训练一个 CNN-LSTM 混合检测模型从输入形状到评估指标构建序列输入时有一个关键决策按记录顺序做滑动窗口。对 NSL-KDD 这种没有真实时间戳的数据窗口内相邻记录是一种近似上下文做研究可以上线前一定要换带时间戳的数据集验证。这里先把窗口函数写好。def make_windows(X, y, window4): Xs, ys [], [] for i in range(window, len(X)): Xs.append(X[i - window:i]) ys.append(y[i]) return np.array(Xs), np.array(ys) WINDOW 4 X_train_w, y_train_w make_windows(X_train.values, y_train.values, WINDOW) X_test_w, y_test_w make_windows(X_test.values, y_test.values, WINDOW) print(X_train_w.shape, X_test_w.shape)窗口长度设 4因为后面的 MaxPooling1D(pool_size2) 要求输入长度至少为 2窗口太小池化层会直接报错。窗口的另一个作用是把单条记录的独立判断变成“前后几条记录联合判断”这对检测慢速扫描、渐进式渗透这类攻击更有效。模型结构使用一维 CNN 加 LSTM。输入形状是 (batch, WINDOW, FEATURE_DIM)卷积核在窗口维度上滑动也就是在看相邻几条记录的组合而不是在特征维度上乱扫。import tensorflow as tf from tensorflow.keras.layers import ( Input, Conv1D, MaxPooling1D, LSTM, Dense, Dropout, BatchNormalization ) FEATURE_DIM X_train_w.shape[2] N_CLASSES len(np.unique(y_train_w)) model tf.keras.Sequential([ Input(shape(WINDOW, FEATURE_DIM)), Conv1D(filters64, kernel_size3, activationrelu, paddingsame), BatchNormalization(), MaxPooling1D(pool_size2), Conv1D(filters32, kernel_size3, activationrelu, paddingsame), LSTM(64, return_sequencesFalse), Dropout(0.5), Dense(32, activationrelu), Dense(N_CLASSES, activationsoftmax) ]) model.summary()这里 Conv1D 的卷积轴是窗口维度kernel_size3 表示每次看 3 条连续记录paddingsame 保证输出长度不变再接池化压缩长度。BatchNormalization 放在卷积后能稳定深层网络训练减少对权重初始化的敏感度。LSTM 的 return_sequencesFalse 表示只输出最后一个时间步的隐状态再接全连接层做分类。Dropout 放在 LSTM 之后默认 0.5防止全连接层过拟合。训练参数上我的默认配置是 Adam 学习率 1e-3配合早停和学习率衰减。多分类场景直接使用 sparse_categorical_crossentropy不需要对标签做 one-hot。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau from sklearn.utils.class_weight import compute_class_weight class_weight compute_class_weight( class_weightbalanced, classesnp.unique(y_train_w), yy_train_w ) class_weight dict(zip(np.unique(y_train_w), class_weight)) model.compile( optimizerAdam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) callbacks [ EarlyStopping(monitorval_loss, patience8, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-6) ] history model.fit( X_train_w, y_train_w, validation_data(X_test_w, y_test_w), epochs50, batch_size64, class_weightclass_weight, callbackscallbacks, verbose1 )class_weight 传入 fit 后训练时每个 batch 的损失会按类别比例加权这个操作比在数据层面过采样更稳不改变样本的原始分布。这个配置在有攻击类型的多分类任务里一样适用先算所有类别的权重再传给 fit而不是手动调某一个类别的系数。训练结束后用验证集评估注意看每个类别的召回率。from sklearn.metrics import classification_report, confusion_matrix y_pred model.predict(X_test_w).argmax(axis1) print(classification_report(y_test_w, y_pred, zero_division0)) print(confusion_matrix(y_test_w, y_pred))评估时只盯 accuracy 是典型的坑。正常流量占比高模型全判 normal 也能有很高的准确率但 U2R、R2L 这类样本少的攻击类别可能完全没被识别。分类报告里的 per-class recall 和 confusion matrix 才能反映真实检测能力。4.3 训练参数怎么设批大小、学习率、早停与类别权重深度学习环境配置完成后真正影响结果的是下面几个参数。我按经验给出一个可直接起步的配置表。参数建议值说明batch_size32 ~ 64太大收敛慢且验证指标波动大learning_rate1e-3 起步配 ReduceLROnPlateau下降因子 0.5EarlyStopping patience8防止在验证集上过拟合ReduceLROnPlateau patience33 个 epoch 不下降就降学习率min_lr1e-6学习率下限避免衰减到无效class_weightbalanced不平衡场景必备epochs50 上限实际 epoch 由早停决定如果 loss 一直震荡不下降先检查是不是数据没打乱。滑动窗口生成的数据有顺序相关性相邻样本几乎一样模型会在局部模式上过拟合。解决办法是在 fit 里保证 shuffleTrue或者在生成窗口前把样本随机打乱一次再切窗口但要做真实时序验证时打乱前必须先按时间分好训练集和测试集不能把未来信息混进训练集。5. 入侵检测系统常见翻车点排查标签泄漏、类别不平衡与误报失控深度学习模型的坑比传统模型更隐蔽因为损失曲线还在下降但实际效果可能就是不行。我整理了四个高频问题每条都按现象、原因、解决三步写可以对照排查。5.1 标签泄漏训练表现好得离谱的元凶现象是模型在验证集上的 F1 接近 0.99你认为已经达成目标结果换一批真实流量一测检测率立刻崩盘。原因大概率是标签泄漏。常见手法包括把攻击标签字段直接留在特征矩阵里用包含未来信息的统计量做特征比如一条连接结束后才有的“连接状态”特征以及在 NSL-KDD 这类数据集里某些特征本身就是攻击行为的直接结果比如 num_outbound_cmds。解决方法是上线前做一次特征自检按“这个特征在流量到达的那一刻是否可知”过滤。写个小脚本把所有列名扫一遍把可疑的命名先筛出来。leak_keywords [label, attack, class, outbound_cmds] leak_cols [c for c in X_train.columns if any(k in c for k in leak_keywords)] print(可疑特征:, leak_cols)真正的泄漏往往是语义层面的脚本只能帮你找出明显的命名问题。更可靠的标准是把每条样本想象成“t 时刻我手里有什么”凡是 t 时刻之后才产生的信息一律不进特征。5.2 类别不平衡Accuracy 虚高与召回率崩塌现象是训练日志里 accuracy 有 99%但打开 classification_report少数类召回率是 0比如 U2R、R2L 一条都没识别出来。原因是正常流量占比太高模型把样本全判成 normal准确率依然很高少数类被完全淹没。这是入侵检测里最常见的问题也是深度学习和传统模型共有的问题。解决办法分三层先做分层采样保证训练集和验证集的类别比例一致再给模型加类别权重让少数类的损失放大最后看混淆矩阵确认每个攻击类别都被召回。不要一上来就上 SMOTE 过采样SMOTE 在类别特征多、特征重叠严重的数据集上会制造噪声样本反而干扰训练。先调权重性价比最高。5.3 换数据集就崩特征处理不一致让模型白训练现象是在 NSL-KDD 上效果很好的模型直接放到 UNSW-NB15 或者真实流量上F1 掉了一半甚至模型报特征维度不匹配。原因往往是预处理不一致。两个数据集的特征列顺序不同、类别取值集合不同或者上线时用了新的流量重新计算标准化的均值和方差导致输入分布漂移。解决方法不是重新训练而是把预处理过程固化成可保存的流水线。训练阶段把 scaler 存下来上线预测时加载同一个 scaler。import joblib joblib.dump(scaler, scaler.joblib) # 上线代码里只做 transform不要再 fit scaler joblib.load(scaler.joblib) X_live_scaled scaler.transform(X_live)还要把特征列名列表存成 json预测前先校验当前输入列顺序和历史训练列顺序一致。顺序对不上时模型不会报错但结果全是乱的这一步很多人忽略。5.4 误报与实时性上线前的最后两个大坑误报的典型现象是模型在离线测试时 F1 不错上线后每天触发上千条告警运维看不过来最后直接把检测服务关停。原因在于离线测试时攻击样本占比很高模型阈值是在这个先验下调的真实流量里攻击极少正常流量的微小波动就会跨过阈值。解决思路有两个方向一是把判定阈值往“高置信”方向移用验证集算 ROC 曲线选一个误报率可接受的点二是加滑动窗口投票连续 N 条样本里有 M 条被判为攻击才告警把单点噪声过滤掉。实时性问题的典型现象是模型推理很快但特征提取慢。流量流要等整个会话结束才能算统计特征那检测必然滞后。解决方法是做增量聚合每来一个包就更新五元组对应的计数器窗口超时后输出一条特征记录而不是等流结束再统计。这两件事放到下一章一起讲实现。6. 进阶把检测模型接到实时流量上用滑动窗口投票压误报离线模型再准接不上实时流量就是白做。常见做法是抓包、按五元组聚合特征、推送到模型、输出告警这个闭环里最容易被低估的是特征聚合方式和误报过滤策略。6.1 流量捕捉与特征聚合的最小闭环用 scapy 做原型验证足够生产环境可以换成 gopacket 或 DPDK但逻辑一致。核心是按五元组维护每个流量流的统计状态。from collections import defaultdict import time class FlowStats: def __init__(self, timeout60): self.packets 0 self.bytes 0 self.start_ts time.time() self.timeout timeout flows defaultdict(FlowStats) def flow_key(pkt): ip pkt.getlayer(IP) tcp pkt.getlayer(TCP) if ip is None or tcp is None: return None return (ip.src, ip.dst, ip.protocol, tcp.sport, tcp.dport) def on_packet(pkt): key flow_key(pkt) if key is None: return flows[key].packets 1 flows[key].bytes len(pkt)这里的 FlowStats 是增量聚合每个包到达时只更新计数不用等流结束。timeout 字段用来兜底长时间不活跃的流要强制输出特征否则内存会被半开连接拖垮。特征函数的输出顺序必须和训练时的 X_train 列顺序完全一致建议直接把训练阶段的特征提取函数原样复用不要在线重写一份。6.2 滑动窗口投票与离线回放验证滑动窗口投票是把单条预测变成序列决策的简单有效手段。def sliding_vote(preds, window5, threshold3): if len(preds) window: return False return sum(preds[-window:]) thresholdwindow5、threshold3 的含义是连续 5 个样本里至少 3 个被判为攻击才触发告警。这个参数会过滤掉偶发误报也会让真正攻击的告警延迟几个样本所以要在误报和延迟之间做权衡而不是照抄别人参数。上线前的验证建议是离线回放 pcap 文件用抓包文件逐包喂给特征提取和模型记录每个告警时间戳统计从攻击开始到首个告警的延迟。这个数字比准确率更贴近业务价值也是我判断模型能不能真正接流的最后依据。我现在接手这类项目惯例是先跑随机森林定一个可解释基线再考虑 CNN-LSTM最后永远把滑动窗口和日志回溯补上顺序反了会在线上栽跟头。希望帮到你。本文还有配套的精品资源点击获取