入侵检测实战:贝叶斯、KNN与神经网络三模型融合调参指南
2026/9/24 18:16:32 网站建设 项目流程

简介:这份资源面向网络安全初学者与机器学习实践者,提供一套基于Python的入侵检测完整实现方案,围绕贝叶斯分类、神经网络与K近邻三种算法展开对比实验。包内共52个文件,以23张png结果图、9个html可视化页面、6个py源码、4个pkl与2个pth模型文件为主,另含csv数据集、md说明及js脚本,压缩包约3.39MB。项目采用约8万条KDD Cup记录,按8:2划分训练与测试集,涵盖数据预处理、模型训练、性能评估等环节,并输出混淆矩阵、ROC曲线、P-R曲线及各分类正确率等图表,便于直观比较三种算法在二分类与多分类任务上的表现。已有272人学习下载,适合希望理解入侵检测流程、复现经典算法对比实验或作为课程设计参考的读者,可从中获取可运行的代码结构、训练好的模型文件与完整的评估结果,快速搭建自己的检测实验环境。

1. 入侵检测三件套:贝叶斯、神经网络、KNN 到底该在什么场景下用

很多人做入侵检测的第一反应是「上个深度学习」,但真到落地阶段,数据量、标注成本、推理延迟三座大山压下来,往往还是朴素贝叶斯和 KNN 先扛住了线上流量。这个标题里的三个算法——贝叶斯、神经网络、KNN——不是让你三选一,而是让你按流量特征和攻击类型分工:贝叶斯处理高维稀疏的协议字段,KNN 兜住小样本的异常聚类,神经网络啃那些非线性组合的变种攻击。NSL-KDD、CIC-IDS 这类数据集上,三者单独跑都不差,但组合起来误报率能压下去一截。这篇笔记面向已经会用 Python 做数据处理、想把这套方案跑通并调参的从业者,从环境配置一路讲到三模型融合的推理脚本,中间该踩的坑一个不落。

2. 数据准备与特征工程:从原始流量到模型能吃的矩阵

2.1 入侵检测数据集长什么样,先搞清楚字段含义

入侵检测的公开数据集常见的有 NSL-KDD、CIC-IDS2017、UNSW-NB15。以 NSL-KDD 为例,每条记录 41 个特征加 1 个标签,特征分四类:TCP 连接基本特征(duration、protocol_type、service、flag)、流量统计特征(src_bytes、dst_bytes、count、srv_count)、基于时间的统计特征(serror_rate、rerror_rate、same_srv_rate)、基于主机的统计特征(dst_host_count、dst_host_srv_count)。标签分正常和四类攻击:DoS、Probe、R2L、U2R。

拿到数据第一步不是急着喂模型,而是看分布。protocol_typeserviceflag是字符串,必须做编码;num_outbound_cmds这类字段全零,直接删;difficulty这种 NSL-KDD 自带的难度分不能当特征用,否则标签泄漏。

import pandas as pd import numpy as np # 列名按 NSL-KDD 官方定义 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) # 丢掉难度分,避免标签泄漏 train.drop('difficulty', axis=1, inplace=True) test.drop('difficulty', axis=1, inplace=True) # 把具体攻击类型归成五大类 attack_map = { 'normal':'normal', 'back':'DoS','land':'DoS','neptune':'DoS','pod':'DoS','smurf':'DoS', 'teardrop':'DoS','mailbomb':'DoS','apache2':'DoS','processtable':'DoS', 'udpstorm':'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', 'ps':'U2R','sqlattack':'U2R','xterm':'U2R' } train['label'] = train['label'].map(attack_map) test['label'] = test['label'].map(attack_map)

这段代码做了三件事:按官方列名读入、删掉difficulty防止标签泄漏、把 39 种细粒度攻击归成 5 大类。归类的意义在于,R2L 和 U2R 样本极少,不归并的话模型根本学不动。参数上,names=col_names必须和文件列数严格对齐,少一列会直接报错;attack_map里没覆盖到的攻击名会变成 NaN,读完后要检查train['label'].isna().sum()

2.2 独热编码与数值归一化:三个算法对输入的要求不一样

朴素贝叶斯对离散特征天然友好,KNN 和神经网络则对数值尺度敏感。所以特征工程要分两路走:一路给贝叶斯用离散化后的特征,一路给 KNN 和神经网络用归一化后的数值矩阵。

from sklearn.preprocessing import LabelEncoder, MinMaxScaler, OneHotEncoder from sklearn.compose import ColumnTransformer cat_cols = ['protocol_type','service','flag'] num_cols = [c for c in train.columns if c not in cat_cols + ['label']] # 方案 A:给朴素贝叶斯用,类别特征做 Label Encoding le = LabelEncoder() train_le = train.copy() test_le = test.copy() for col in cat_cols: # 用训练集拟合,测试集只 transform,防止数据泄漏 train_le[col] = le.fit_transform(train_le[col]) test_le[col] = le.transform(test_le[col]) # 方案 B:给 KNN 和神经网络用,类别特征做 One-Hot,数值特征做 MinMax preprocessor = ColumnTransformer( transformers=[ ('cat', OneHotEncoder(handle_unknown='ignore'), cat_cols), ('num', MinMaxScaler(), num_cols) ]) X_train = preprocessor.fit_transform(train.drop('label', axis=1)) X_test = preprocessor.transform(test.drop('label', axis=1))

这里有个血泪经验:LabelEncoder在测试集出现训练集没见过的类别时会直接抛异常,所以生产环境更推荐OneHotEncoder(handle_unknown='ignore')MinMaxScaler把数值压到 [0,1],是因为 KNN 靠欧氏距离算相似度,src_bytes动辄上百万,不归一化的话距离全被它主导。神经网络虽然理论上能自己学尺度,但输入归一化后收敛快得多,学习率也好调。

提示:fit_transform只能在训练集上调用,测试集一律用transform。这个顺序搞反了,离线指标会虚高,上线就翻车。

3. 三个模型分别怎么搭:朴素贝叶斯、KNN、神经网络的代码与参数

3.1 朴素贝叶斯:高斯、多项式、伯努利到底选哪个

朴素贝叶斯在入侵检测里的优势是训练快、对小样本稳、可解释性强。sklearn 提供三种:GaussianNB假设特征服从正态分布,适合连续数值;MultinomialNB适合计数型特征;BernoulliNB适合二值特征。入侵检测的数值特征经过归一化后不是严格正态,但GaussianNB实测在 NSL-KDD 上准确率能到 76% 左右,作为基线足够。

from sklearn.naive_bayes import GaussianNB, BernoulliNB from sklearn.metrics import classification_report, accuracy_score # 高斯朴素贝叶斯,适合连续特征 gnb = GaussianNB() gnb.fit(X_train.toarray() if hasattr(X_train, 'toarray') else X_train, train['label']) y_pred_gnb = gnb.predict(X_test.toarray() if hasattr(X_test, 'toarray') else X_test) print("GaussianNB 准确率:", accuracy_score(test['label'], y_pred_gnb)) print(classification_report(test['label'], y_pred_gnb, zero_division=0))

GaussianNB只有一个真正要调的参数var_smoothing,默认 1e-9,作用是给方差加一点平滑,防止某个特征方差为零导致概率计算除零。如果发现某个类别预测概率全是 0 或 1,把var_smoothing调到 1e-8 或 1e-7 试试。BernoulliNBbinarize参数可以把数值特征二值化,适合做「某特征是否超过阈值」这类判断,但会丢信息,一般不作为首选。

3.2 KNN:k 值、距离度量、加权方式三个参数怎么定

KNN 在入侵检测里常被低估。它的逻辑是「异常流量在特征空间里离正常流量远」,对未知攻击有一定泛化能力。缺点是推理时要和所有训练样本算距离,数据量大时慢。NSL-KDD 训练集 12 万条,直接跑 KNN 预测测试集要几分钟,可以接受;但如果是百万级流量日志,得先做降采样或用 KD-Tree。

from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import cross_val_score # 先在小样本上扫 k 值,别一上来就全量跑 sample_idx = np.random.choice(len(X_train), 20000, replace=False) X_sample = X_train[sample_idx] y_sample = train['label'].values[sample_idx] for k in [3, 5, 7, 9, 11]: knn = KNeighborsClassifier( n_neighbors=k, metric='euclidean', # 欧氏距离,数值特征归一化后用这个 weights='distance' # 距离越近权重越大,缓解类别不平衡 ) scores = cross_val_score(knn, X_sample, y_sample, cv=3, scoring='f1_macro') print(f"k={k}, F1-macro={scores.mean():.4f}")

三个参数逐个说:n_neighbors太小对噪声敏感,太大把异常点也划进正常类,5 到 9 之间通常最稳;metriceuclidean是因为特征已归一化,如果特征稀疏且高维,可以试cosineweights='distance'让近邻投票权重反比于距离,在 R2L、U2R 这种小类上比均匀投票好。注意cross_val_scorescoringf1_macro而不是accuracy,因为类别极不平衡,准确率高不代表小类能识别出来。

3.3 神经网络:用 MLP 还是自己搭前馈网络

入侵检测的神经网络不需要上 CNN 或 Transformer,特征矩阵是扁平向量,没有空间结构。sklearn 的MLPClassifier够用,想更灵活就用 PyTorch 搭三层前馈网络。先看 sklearn 版本:

from sklearn.neural_network import MLPClassifier mlp = MLPClassifier( hidden_layer_sizes=(128, 64), # 两层隐藏层,逐层收窄 activation='relu', solver='adam', alpha=1e-4, # L2 正则,防过拟合 batch_size=256, learning_rate_init=1e-3, max_iter=200, early_stopping=True, # 留 10% 做验证,验证分不涨就停 validation_fraction=0.1, random_state=42 ) mlp.fit(X_train, train['label']) y_pred_mlp = mlp.predict(X_test) print("MLP 准确率:", accuracy_score(test['label'], y_pred_mlp))

hidden_layer_sizes=(128, 64)是常见起点,输入维度经 One-Hot 后大概 120 维,第一层 128 够用,第二层 64 做压缩。alpha控制正则强度,过拟合时调大,欠拟合时调小。early_stopping=True很关键,入侵检测数据噪声大,不早停容易在训练集上刷到 99% 但测试集崩掉。batch_size256 是内存和收敛速度的折中,显存够可以上 512。

如果要用 PyTorch 自己搭,结构就是Linear -> ReLU -> Dropout -> Linear -> ReLU -> Linear,输出层节点数等于类别数,损失用CrossEntropyLoss,优化器用 Adam。Dropout 率设 0.3 左右,再高会欠拟合。

4. 三模型融合与推理脚本:投票、堆叠还是按攻击类型分工

4.1 硬投票与软投票:什么时候 1+1+1 大于 3

三个模型单独跑,准确率大概在 76% 到 80% 之间。融合的收益来自它们犯错的地方不一样:贝叶斯对数值边界不敏感,KNN 对局部密度敏感,神经网络对非线性组合敏感。最简单的是VotingClassifier硬投票,每个模型投一票,票多者胜。

from sklearn.ensemble import VotingClassifier voting = VotingClassifier( estimators=[ ('gnb', GaussianNB()), ('knn', KNeighborsClassifier(n_neighbors=7, weights='distance')), ('mlp', MLPClassifier(hidden_layer_sizes=(128,64), max_iter=200, early_stopping=True, random_state=42)) ], voting='soft' # 用预测概率加权,比硬投票稳 ) voting.fit(X_train, train['label']) y_pred_vote = voting.predict(X_test) print("软投票准确率:", accuracy_score(test['label'], y_pred_vote))

voting='soft'要求每个模型都能输出predict_proba,这三个都支持。软投票把三个模型的概率向量平均后取最大,比硬投票更平滑。如果某个模型明显弱,可以在weights参数里给它低权重,比如weights=[1, 2, 2]

4.2 按攻击类型分工:贝叶斯管 DoS,KNN 管 Probe,神经网络管 R2L/U2R

软投票是通用做法,但入侵检测有个更细的思路:不同攻击类型在特征空间里的分布不一样,让擅长的模型管擅长的类。DoS 流量特征明显(src_bytes大、count高),贝叶斯够用;Probe 是扫描行为,在特征空间里成簇,KNN 的局部判断准;R2L 和 U2R 样本少且非线性强,神经网络更能挖出组合模式。

# 先让三个模型各自输出概率 proba_gnb = gnb.predict_proba(X_test) proba_knn = knn.predict_proba(X_test) proba_mlp = mlp.predict_proba(X_test) # 按类别分配权重:DoS 信贝叶斯,Probe 信 KNN,R2L/U2R 信神经网络 class_names = gnb.classes_ weights = { 'DoS': [0.5, 0.2, 0.3], 'Probe': [0.2, 0.5, 0.3], 'R2L': [0.2, 0.2, 0.6], 'U2R': [0.2, 0.2, 0.6], 'normal': [0.34, 0.33, 0.33] } final_proba = np.zeros_like(proba_gnb) for i, cls in enumerate(class_names): w = weights.get(cls, [0.34, 0.33, 0.33]) final_proba[:, i] = (w[0]*proba_gnb[:, i] + w[1]*proba_knn[:, i] + w[2]*proba_mlp[:, i]) y_pred_final = class_names[np.argmax(final_proba, axis=1)] print("分工融合准确率:", accuracy_score(test['label'], y_pred_final))

这段代码的核心是weights字典,每个类别给三个模型分配不同权重。权重不是拍脑袋定的,是在验证集上按类别 F1 反推出来的:哪个模型在该类上 F1 高,权重就大。final_proba按列加权求和后取 argmax。注意gnb.classes_的顺序要和proba_gnb的列顺序一致,三个模型的classes_必须相同,否则概率对不上。

注意:分工融合的权重需要在验证集上重新标定,直接拿测试集调权重等于作弊,上线后指标会掉。

5. 避坑与排查:入侵检测三模型落地时最容易翻车的五个地方

5.1 现象:测试集准确率 99%,上线后误报一堆

原因:特征工程在训练集和测试集上用了不同的fit,或者归一化参数只用了训练集但测试集分布偏移。更隐蔽的是 NSL-KDD 的KDDTest+里有很多训练集没出现过的攻击类型,模型没见过,只能瞎猜。

解决:所有预处理步骤包进Pipeline,确保fit只在训练集调用。上线前用时间切分而不是随机切分验证,模拟真实流量分布变化。

5.2 现象:KNN 预测一条要好几秒,QPS 上不去

原因:KNN 推理时要算测试样本到所有训练样本的距离,12 万条训练数据,每条预测做 12 万次距离计算。

解决:训练集降采样到 2 万条以内,或者用KDTree/BallTree加速。sklearn 的KNeighborsClassifieralgorithm='auto'时会自动选,但数据量大时手动设algorithm='kd_tree'更稳。再不行就换近似最近邻库,但那是另一个话题了。

5.3 现象:神经网络训练 loss 一直不降,或者降了但验证集不降

原因:学习率太大导致震荡,或者alpha正则太强导致欠拟合。另一个常见原因是输入特征没归一化,src_bytes这种大数值直接把梯度带飞。

解决:先确认X_train已经过MinMaxScaler。学习率从 1e-3 开始,不降就试 1e-4。alpha从 1e-4 开始,过拟合时往 1e-3 调。early_stopping=True打开,n_iter_no_change设 10,连续 10 轮验证分不涨就停。

5.4 现象:朴素贝叶斯对某个类别的预测概率全是 0

原因:GaussianNB在某个特征上方差为零,概率密度计算除零,或者某个类别在训练集里样本太少,先验概率极低。

解决:调大var_smoothing到 1e-8 或 1e-7。检查训练集里每个类别的样本数,R2L 和 U2R 如果只有几十条,考虑过采样或直接归并到相近类别。

5.5 现象:三个模型单独跑都还行,一投票反而变差

原因:某个模型在某个类别上特别弱,软投票时它的错误概率把正确概率拉下去了。或者三个模型高度相关,投票没有多样性收益。

解决:先看三个模型的混淆矩阵,找出各自最差的类别。如果某个模型在 R2L 上 F1 只有 0.1,把它在 R2L 上的权重降到 0.1 以下。如果三个模型都是 MLP 变体,换一个贝叶斯进去增加多样性。

6. 进阶技巧:用贝叶斯优化给三模型调参,以及一个验证融合是否真有效的土办法

调参这件事,网格搜索太慢,随机搜索靠运气,贝叶斯优化是性价比最高的。optuna是常用库,定义一个目标函数,让它自己去找 KNN 的 k、MLP 的隐藏层大小和学习率、投票权重。

import optuna from sklearn.model_selection import cross_val_score def objective(trial): k = trial.suggest_int('k', 3, 15) hidden = trial.suggest_categorical('hidden', [(64,), (128,), (128, 64), (256, 128)]) lr = trial.suggest_float('lr', 1e-4, 1e-2, log=True) alpha = trial.suggest_float('alpha', 1e-5, 1e-2, log=True) knn = KNeighborsClassifier(n_neighbors=k, weights='distance') mlp = MLPClassifier(hidden_layer_sizes=hidden, learning_rate_init=lr, alpha=alpha, max_iter=200, early_stopping=True, random_state=42) voting = VotingClassifier( estimators=[('gnb', GaussianNB()), ('knn', knn), ('mlp', mlp)], voting='soft' ) scores = cross_val_score(voting, X_train, train['label'], cv=3, scoring='f1_macro', n_jobs=-1) return scores.mean() study = optuna.create_study(direction='maximize') study.optimize(objective, n_trials=30) print("最佳参数:", study.best_params) print("最佳 F1-macro:", study.best_value)

optunasuggest_intsuggest_categoricalsuggest_float分别对应整数、枚举、浮点参数。log=True让学习率和正则系数在对数尺度上采样,因为这两个参数跨几个数量级。n_trials=30是起步,想更准就 100 次,但每次要跑 3 折交叉验证,时间成本要算进去。

最后说一个验证融合是否真有效的土办法:把三个模型和融合模型的预测结果按类别拆开,看融合模型在哪些类别上比最好的单模型还高。如果融合模型在所有类别上都不如某个单模型,那融合就是白搭,不如直接用那个单模型。我一般会打印一张表,行是类别,列是四个模型的 F1,一眼就能看出融合到底贡献在哪。这个习惯帮我省过好几次「为了融合而融合」的功夫。希望帮到你。

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

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

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

立即咨询