简介:这是一套以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, header=None, names=COLUMNS + ["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_na=False) test_cat = pd.get_dummies(test_df[cat_features], dummy_na=False) test_cat = test_cat.reindex(columns=train_cat.columns, fill_value=0) X_train = pd.concat([pd.DataFrame(X_num_train, columns=num_features), train_cat], axis=1) X_test = pd.concat([pd.DataFrame(X_num_test, columns=num_features), test_cat], axis=1) 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_estimators=100, max_depth=10, random_state=42, n_jobs=-1) rf_tmp.fit(X_train, y_train) imps = pd.Series(rf_tmp.feature_importances_, index=X_train.columns) imps = imps.sort_values(ascending=False) print(imps.head(10)) mi = pd.Series(mutual_info_classif(X_train, y_train), index=X_train.columns) mi = mi.sort_values(ascending=False) 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_size=0.2, stratify=y_train, random_state=42 ) model = RandomForestClassifier( n_estimators=300, max_depth=20, min_samples_leaf=1, class_weight="balanced_subsample", n_jobs=-1, random_state=42 ) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(model, X_train_s, y_train_s, cv=cv, scoring="f1_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_division=0))参数说明: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_estimators=200, max_depth=6, learning_rate=0.1, scale_pos_weight=neg / pos, eval_metric="mlogloss", use_label_encoder=False, random_state=42 ) xgb.fit(X_train_s, y_train_s) print(classification_report(y_val_s, xgb.predict(X_val_s), zero_division=0))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, window=4): 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_size=2) 要求输入长度至少为 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(filters=64, kernel_size=3, activation="relu", padding="same"), BatchNormalization(), MaxPooling1D(pool_size=2), Conv1D(filters=32, kernel_size=3, activation="relu", padding="same"), LSTM(64, return_sequences=False), Dropout(0.5), Dense(32, activation="relu"), Dense(N_CLASSES, activation="softmax") ]) model.summary()这里 Conv1D 的卷积轴是窗口维度,kernel_size=3 表示每次看 3 条连续记录,padding="same" 保证输出长度不变,再接池化压缩长度。BatchNormalization 放在卷积后,能稳定深层网络训练,减少对权重初始化的敏感度。LSTM 的 return_sequences=False 表示只输出最后一个时间步的隐状态,再接全连接层做分类。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_weight="balanced", classes=np.unique(y_train_w), y=y_train_w ) class_weight = dict(zip(np.unique(y_train_w), class_weight)) model.compile( optimizer=Adam(learning_rate=1e-3), loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) callbacks = [ EarlyStopping(monitor="val_loss", patience=8, restore_best_weights=True), ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=3, min_lr=1e-6) ] history = model.fit( X_train_w, y_train_w, validation_data=(X_test_w, y_test_w), epochs=50, batch_size=64, class_weight=class_weight, callbacks=callbacks, verbose=1 )class_weight 传入 fit 后,训练时每个 batch 的损失会按类别比例加权,这个操作比在数据层面过采样更稳,不改变样本的原始分布。这个配置在有攻击类型的多分类任务里一样适用:先算所有类别的权重,再传给 fit,而不是手动调某一个类别的系数。训练结束后用验证集评估,注意看每个类别的召回率。
from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test_w).argmax(axis=1) print(classification_report(y_test_w, y_pred, zero_division=0)) print(confusion_matrix(y_test_w, y_pred))评估时只盯 accuracy 是典型的坑。正常流量占比高,模型全判 normal 也能有很高的准确率,但 U2R、R2L 这类样本少的攻击类别可能完全没被识别。分类报告里的 per-class recall 和 confusion matrix 才能反映真实检测能力。
4.3 训练参数怎么设:批大小、学习率、早停与类别权重
深度学习环境配置完成后,真正影响结果的是下面几个参数。我按经验给出一个可直接起步的配置表。
| 参数 | 建议值 | 说明 |
|---|---|---|
| batch_size | 32 ~ 64 | 太大收敛慢且验证指标波动大 |
| learning_rate | 1e-3 起步 | 配 ReduceLROnPlateau,下降因子 0.5 |
| EarlyStopping patience | 8 | 防止在验证集上过拟合 |
| ReduceLROnPlateau patience | 3 | 3 个 epoch 不下降就降学习率 |
| min_lr | 1e-6 | 学习率下限,避免衰减到无效 |
| class_weight | balanced | 不平衡场景必备 |
| epochs | 50 上限 | 实际 epoch 由早停决定 |
如果 loss 一直震荡不下降,先检查是不是数据没打乱。滑动窗口生成的数据有顺序相关性,相邻样本几乎一样,模型会在局部模式上过拟合。解决办法是在 fit 里保证 shuffle=True,或者在生成窗口前把样本随机打乱一次再切窗口,但要做真实时序验证时,打乱前必须先按时间分好训练集和测试集,不能把未来信息混进训练集。
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, timeout=60): 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, window=5, threshold=3): if len(preds) < window: return False return sum(preds[-window:]) >= thresholdwindow=5、threshold=3 的含义是连续 5 个样本里至少 3 个被判为攻击才触发告警。这个参数会过滤掉偶发误报,也会让真正攻击的告警延迟几个样本,所以要在误报和延迟之间做权衡,而不是照抄别人参数。
上线前的验证建议是离线回放 pcap 文件:用抓包文件逐包喂给特征提取和模型,记录每个告警时间戳,统计从攻击开始到首个告警的延迟。这个数字比准确率更贴近业务价值,也是我判断模型能不能真正接流的最后依据。我现在接手这类项目,惯例是先跑随机森林定一个可解释基线,再考虑 CNN-LSTM,最后永远把滑动窗口和日志回溯补上,顺序反了会在线上栽跟头。希望帮到你。
本文还有配套的精品资源,点击获取