机器学习驱动的网络入侵检测:从特征工程到随机森林实战
2026/9/15 3:53:45 网站建设 项目流程

简介:面向计算机相关专业毕业生和项目实战初学者,这份基于机器学习的网络入侵检测项目包含完整可运行的Python源码及配套文档,适合用于毕业设计、课程设计或期末大作业,个人经导师指导并通过的高分设计课题。资源包共12个文件,以7个Python脚本为主体,覆盖数据读入、字符串数值转换、Min-Max归一化、特征选择、CNN模型训练与结果平衡处理等完整流程;另有2个TXT说明、Word论文、Markdown笔记和PPT报告,分别用于运行指引、实验记录和答辩展示,总大小仅3.37MB。项目评审得分99分,代码结构清晰,模块间分工明确,对新手友好,无需复杂环境即可运行;目前已有84人学习下载,可帮助快速理解机器学习在入侵检测中的应用,并支持根据实际数据集调整特征与参数,完成高质量课题交付。

1. 机器学习要解决的网络入侵检测问题,从来不是算法而是特征

无论标题里挂的是 python 源码、文档说明还是论文资料,落到工程上要回答的问题只有一句:如何让程序从网络流量里自动判断“这是正常访问还是攻击行为”。传统入侵检测系统靠经验规则匹配 payload 特征,规则没覆盖到的攻击在业务眼里就是正常流量;而当误报率被调低时,漏报又随之上升。把网络流量转成结构化特征,再用机器学习分类器去拟合这些特征和攻击标签之间的映射,就是这条技术路线的基本盘。

这条路有两个前提值得先说清楚:一是特征质量决定算法上限,随机森林再强也喂不饱一堆冗余字段;二是评估必须围绕少数类样本展开,因为正常流量在多数数据集里远多于攻击样本。李宏毅和吴恩达的课程能讲清楚损失函数和梯度,但真实的应用流程里特征构造与部署验证才占大头。下文从数据集选型、特征构造、Python 实现到调参与落地一次性讲完,适合正在做课程设计、论文实验或安全团队内部 PoC 的工程师。

2. 数据集选型和网络流量特征工程:决定检测上限的 80% 工作

2.1 KDD 系数据集为什么还在被用,选哪个版本更省事

KDD Cup 99 是这个领域被引用最多的旧基准,但它存在大量重复记录,训练集和测试集分布也不一致,直接拿它验证很容易得到虚高的准确率。NSL-KDD 是它的清洗版,去掉了冗余记录,并保证训练集、测试集样本可比较,适合快速验证整条 pipeline。UNSW-NB15 更贴近现代攻击,流特征更多,适合写论文时做横向对比。CICIDS 2017 有真实流量捕获,标签更细,缺点是文件体积大,预处理耗时明显。

数据集特征数攻击类别适合场景注意点
NSL-KDD414 大类 39 小类快速实验、课程设计攻击流量构造偏老
UNSW-NB15499 类论文实验、算法对比类别特征多,需要归一化
CICIDS 201780+14 类在线检测、业务 PoC体量大,训练耗时高

我常见的做法是:课程设计默认 NSL-KDD,因为公开资料多、列名清楚,遇到问题容易查;论文实验优先 UNSW-NB15,因为攻击类型更现代,审稿人接受度更高。特征数也不是越多越好,CICIDS 2017 的 80 多列里包含大量统计量相似字段,上来全灌进模型只会拖慢训练,后面还要靠特征重要性反推剔除。选定数据集后,下一个问题才是关键:原始 pcap 文件怎么变成模型能吃的特征矩阵。

2.2 从原始 pcap 到特征矩阵:协议解析与流聚合

机器学习模型读不了二进制报文,只能吃特征表。所谓特征工程,就是把一段时间内属于同一个五元组(源 IP、目的 IP、源端口、目的端口、协议)的报文聚合起来,统计出“这条流”的行为画像。常见特征包括流持续时间、前向包数与字节数、后向包数与字节数、TCP 标志位计数、包长均值与标准差、PSH/URG 标志数量等。流聚合窗口的设置直接影响模型效果:窗口太短抓不到慢速扫描,窗口太长又让实时性变差。

下面是用 Scapy 从 pcap 里提取 TCP 标志位的一个最小函数,实际特征提取会以这个函数为内核,四周再套窗口统计逻辑:

from scapy.all import rdpcap, TCP, IP def extract_flags(pkt): if TCP in pkt: flags = pkt[TCP].flags return { "syn": 1 if flags & 0x02 else 0, "ack": 1 if flags & 0x10 else 0, "psh": 1 if flags & 0x08 else 0, "urg": 1 if flags & 0x20 else 0, } return None

逻辑说明:TCP标志位以位掩码方式存储在flags字段里,0x02 是 SYN,0x10 是 ACK,0x08 是 PSH,0x20 是 URG。这段代码把每个报文转成一个含 4 个布尔值的字典,再用外层defaultdict按五元组累加,就能形成“这条流里出现过多少次 SYN、多少比例 ACK”这类特征。参数上唯一要留意的是 pcap 文件过大时rdpcap会一次性读入内存,改迭代式PcapReader更稳妥。

真实项目里更省事的路径是用pyshark直接解析 Wireshark 的tshark字段,或者用nfstream这类流处理库自动产出特征。后者内置了按五元组分流、时间窗口聚合和 40 多个内置统计特征的能力,适合快速验证。需要说明的是,这些库产出的特征列名与 NSL-KDD 并不一致,换数据集时特征拼接代码几乎要重写,所以小规模实验不建议一上来就依赖专用流特征库。

2.3 预处理三件套:空值、编码、标准化

拿到原始 CSV 特征表后,必须先做三件事:处理无穷值和缺失值,把协议类型、服务类型、标志位等离散字段转成数值,最后对数值字段标准化。很多人在这一步直接调StandardScaler作用于全表,却忘了离散字段编码后会被当成连续值参与缩放,导致模型学到错误的大小关系。

import numpy as np import pandas as pd from sklearn.preprocessing import LabelEncoder, StandardScaler df = pd.read_csv("NSL_KDD_Train.csv") df.replace([np.inf, -np.inf], np.nan, inplace=True) df.dropna(inplace=True) for col in ["protocol_type", "service", "flag"]: df[col] = LabelEncoder().fit_transform(df[col]) num_cols = df.select_dtypes(include=["float64", "int64"]).columns num_cols = [c for c in num_cols if c not in ["label", "difficulty"]] scaler = StandardScaler() df.loc[:, num_cols] = scaler.fit_transform(df[num_cols])

逻辑说明:dropna在样本量充足时最简单直接,但如果缺失比例高,建议改用中位数填充,否则会损失大量攻击样本。LabelEncoder适用于协议类型这种无序离散值,但要注意它会给类别强加整数排序;更稳妥的做法是OneHotEncoder,只是特征维度会明显增加。num_cols里剔除labeldifficulty是防止把标签列也做标准化,这一类低级错误在初学者代码里出现频率很高。标准化参数scaler在训练集上拟合后,验证集和测试集只调用transform,不能让测试集信息混入训练过程。

3. 用 Python 在本地跑通基于机器学习的入侵检测最小示例

3.1 环境准备与依赖版本

写 Python 代码之前,先确认本机环境是 Python 3.8 以上版本,并安装以下依赖:pandasnumpy负责数据操作,scikit-learn提供分类器与评估指标,imbalanced-learn处理类别不平衡,scapy用于 pcap 解析,joblib做模型持久化。一般我会单独创建一个虚拟环境,避免与系统 Python 包冲突。

python -m venv nids_env source nids_env/bin/activate pip install pandas numpy scikit-learn imbalanced-learn scapy joblib

参数说明:scikit-learn版本建议不低于 1.2,新版对交叉验证接口和评估指标有优化。imbalanced-learnscikit-learn存在版本联动,升级其中一个时最好同步升级另一个,否则SMOTE可能报接口不匹配。如果你在 Windows 环境安装scapy遇到编译问题,可以直接安装pyshark配合 Wireshark 的tshark引擎,两者选一即可,功能上都能完成报文解析。

3.2 构建训练与验证数据的规范切分

很多人拿到数据直接train_test_split(X, y, test_size=0.2, random_state=42),这条命令在普通分类问题上没错,但网络流量数据有时间相关性,同一时间段内的攻击往往是同一工具、同一变体的爆发。如果随机打乱,模型会在训练阶段看到未来流量,验证集指标虚高,上线后立刻被打回原形。

from sklearn.model_selection import train_test_split X = df.drop(columns=["label"]) y = df["label"] X_train, X_valid, y_train, y_valid = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )

逻辑说明:这里用stratify=y保证切分后正常与攻击样本比例一致,尤其适合标签分布本就不平衡的数据集。如果原始数据里有明确的时间戳字段,我会先按时间排序,再取前 70% 做训练、后 30% 做验证,而不是随机切分。很多公开数据集没有时间戳,退而求其次只能随机切分,但论文里要注明这一限制。参数random_state固定随机种子,保证多次运行结果可复现。

3.3 随机森林与逻辑回归的基线对比

这个场景我习惯用随机森林作为主力模型,因为网络流量特征里既有数值字段也有离散字段,随机森林对两者混合的容错度高,不需要做复杂的特征缩放;同时它自带特征重要性排序,便于后续做特征筛选。逻辑回归作为对照模型,用于判断非线性模型是否带来了显著提升。

from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, roc_auc_score rf = RandomForestClassifier( n_estimators=200, max_depth=15, min_samples_leaf=2, n_jobs=-1, random_state=42, ) lr = LogisticRegression(max_iter=1000, C=1.0, random_state=42) for model in [rf, lr]: model.fit(X_train, y_train) pred = model.predict(X_valid) proba = model.predict_proba(X_valid)[:, 1] print(model.__class__.__name__) print(classification_report(y_valid, pred, target_names=["normal", "attack"])) print("AUC:", roc_auc_score(y_valid, proba))

逻辑说明:RandomForestClassifier里的n_estimators=200是树的数量,超过一定值后收益递减且耗时线性上升;max_depth=15限制单棵树深度,防止对少数攻击样本过拟合;min_samples_leaf=2要求叶子节点至少 2 个样本,对噪声数据有平滑作用;n_jobs=-1使用全部 CPU 核心加速训练。LogisticRegressionmax_iter=1000是因为流量特征经过标准化后仍可能有线性不可分情况,迭代上限太低会不收敛。评估时不要只看accuracy,正常流量占多数时 90% 准确率可能意味着攻击样本全部被漏,classification_report里的 recall 和 F1-score 才是关键。

随机森林在这类小规模公开数据上通常能到 95% 以上的准确率和 0.99 级别的 AUC,但先别急着高兴。公开数据集里攻击模式相对规整,特征提取和标签都是预先处理好的,真实流量里的新攻击、加密流量、内部横向移动,表现会明显下降。所以基线模型跑通之后,下一步要重点检查模型到底依赖了哪些特征,这一检查经常能暴露出数据泄漏问题。

3.4 从特征重要性反推模型是否学对了

import pandas as pd importance = pd.DataFrame({ "feature": X_train.columns, "importance": rf.feature_importances_, }).sort_values("importance", ascending=False) print(importance.head(10))

逻辑说明:feature_importances_是随机森林在所有树中按特征切分带来的不纯度降低量归一化后的结果,值越大说明模型越依赖该特征。输出前 10 个特征后,我会检查两个事情:一是serviceflag等离散特征占比是否过高,过高说明模型在记特定端口或状态码,换网络环境会失效;二是基础统计特征如包长均值、流持续时间是否合理靠前,这类特征对真实流量仍有泛化意义。如果发现某个特征在逻辑上不该有如此高的重要性,回头检查预处理或特征拼接,往往能找到标签字段混入特征的逻辑错误。

4. 随机森林在 nids 里的 3 个必调参数和类别不平衡对策

4.1 参数怎么设:先调结构,再调复杂度

随机森林在入侵检测场景里的参数优先级与普通表格数据不同。流量特征维度高、噪音大,调参重点是控制单棵树复杂度,而不是盲目增加树的数量。n_estimators超过 300 后训练时间翻倍但 AUC 提升通常不到 0.01,OOB 误差曲线基本走平;max_features默认为特征的平方根,在 80 维以上的特征表里可以尝试放宽到 0.3 到 0.5;max_depth不限制时容易记住少数攻击样本的奇怪模式,从 10 到 25 按步长 5 调整;min_samples_leaf对噪音敏感,调大到 5 到 10 能让线上误报率下降。

参数常见范围主要影响我的调参顺序
n_estimators100~500训练时间与稳定性先固定 200
max_depth10~30过拟合与泛化第二个调
min_samples_leaf1~10噪音敏感度第三个调
max_featuressqrt 或 0.3~0.5树间相关性最后调
class_weightbalanced 或自定义少数类召回率第一名

逻辑说明:调参顺序应当先解决“类别不平衡”,再调结构参数。因为class_weight改变的是模型优化目标,结构参数改变的是模型复杂度,顺序反过来容易陷入一个局部最优。class_weight="balanced"会自动按样本量反比调整权重,在不改采样方式的情况下让模型更关注攻击类。如果用了balanced后误报明显增加,可以手动设置{0: 1.0, 1: 10.0}这类比例,控制对攻击类的偏重程度。

4.2 类别不平衡下的评估指标和重采样

入侵检测最典型的坑是负样本远多于正样本,准确率指标毫无意义。假设数据里 99% 是正常流量,模型把所有样本都判成正常,准确率仍有 99%,但它一个攻击都抓不到。正确做法是看召回率、精确率和 PR-AUC。召回率回答“攻击里抓到了多少”,精确率回答“报出来的里面有多少是真攻击”,F1-score 是两者的调和平均。

from imblearn.over_sampling import SMOTE from sklearn.metrics import precision_recall_curve, auc, f1_score sm = SMOTE(random_state=42, sampling_strategy="auto") X_res, y_res = sm.fit_resample(X_train, y_train) rf.fit(X_res, y_res) pred = rf.predict(X_valid) print("F1-score:", f1_score(y_valid, pred))

逻辑说明:SMOTE在特征空间内对少数类样本做插值生成新样本,sampling_strategy="auto"表示把所有类别采样到和多数类相同数量。这里有两个重采样原则:只能在训练集上做,验证集必须保持原始分布,否则评估结果失真;如果特征维度很高,SMOTE 插值可能生成噪音样本,反而降低模型效果,此时优先调class_weight而非重采样。评估时还要同时打印 PR-AUC,它比 ROC-AUC 对少数类更敏感,公式为对 precision-recall 曲线求面积,分数越低说明模型在攻击样本上的排名越差。

4.3 训练集与验证集应该怎么切:避免时间泄漏

前面提过流量数据不是独立同分布,具体到切割方法上有两种坑。第一种是随机切分造成的时间泄漏,模型在训练时见过“来自同一时段的相似攻击”,验证时自然表现好。第二种是验证集内混入了同一条流的不同分片,比如把同一条 TCP 连接的前 100 个包分到训练集、后 100 个包分到验证集,这相当于同一物体的两张照片一张用来学一张用来考。

df.sort_values("timestamp", inplace=True) split_idx = int(len(df) * 0.8) train = df.iloc[:split_idx] valid = df.iloc[split_idx:]

逻辑说明:按时间顺序切分模拟真实上线场景,模型只用历史数据预测未来。timestamp字段在部分公开数据集中缺失,这时至少要做到按流 ID 分组后再切分,确保同一条流不会被拆散。做法是先groupby流 ID 聚合特征,再做时间切分。特征工程阶段也应该先分组再做切分,否则统计特征会通过同一流的信息提前泄漏。这个细节在论文实验里尤其重要,审稿人经常拿这一点判断工作是否严谨。

5. 把模型从 Jupyter 接到真实流量上的三个落地技巧

5.1 用 scapy 构造最小在线检测半成品

from collections import defaultdict, deque import joblib from scapy.all import sniff, IP, TCP model = joblib.load("nids_rf.joblib") scaler = joblib.load("scaler.joblib") windows = defaultdict(lambda: deque(maxlen=50)) def to_feature(key, packets): sizes = packets return [ len(sizes), sum(sizes), max(sizes), min(sizes), sum(1 for p in sizes if p > 1000), ] def detect(pkt): if IP in pkt: key = (pkt[IP].src, pkt[IP].dst, pkt[IP].proto) windows[key].append(pkt[IP].len) if len(windows[key]) == 50: feat = to_feature(key, windows[key]) feat_scaled = scaler.transform([feat]) prob = model.predict_proba(feat_scaled)[0, 1] if prob > 0.7: print("attack candidate:", key, "prob:", prob) sniff(prn=detect, store=False)

逻辑说明:sniffprn回调在每个包到达时触发,store=False避免内存中堆积原始报文。deque(maxlen=50)维护滑动窗口,只用最近 50 个包的特征做判断,to_feature把窗口内包长转为 5 维特征,实际生产要替换成与训练时完全一致的特征列顺序。这条代码只能说明思路,真实落地还要把抓包和推理分离到独立线程,否则慢速扫描攻击会让回调阻塞。

5.2 上线后怎么判断检测可信:阈值调整与误报观察

模型默认以 0.5 作为分类阈值,但入侵检测场景里攻击先验概率远低于 0.5,直接使用默认阈值会产生海量误报。我习惯在验证集上遍历阈值 0.3 到 0.9,取 F1-score 最大值对应阈值作为线上初始值,之后根据实际误报工单再调。阈值调高则精确率升、召回率降,适合安全运营人力紧张的团队;调低则反过来,适合追求不漏报的场景。

5.3 模型持久化不只 dump 模型,还要存特征列和预处理参数

import joblib joblib.dump(rf, "nids_rf.joblib") joblib.dump(scaler, "nids_scaler.joblib") joblib.dump(X_train.columns.tolist(), "nids_columns.json")

逻辑说明:joblib.dump保存的模型依赖 sklearn 版本,升级 sklearn 后joblib.load很可能报兼容错误,所以模型文件要与依赖版本清单一起归档。保存X_train.columns.tolist()是为了上线推理时强制按训练列顺序拼接特征,这个问题排查起来很隐蔽,特征列顺序错了预测结果可能整体翻转。推理端加载模型后先跑一条已知样本验证输出,再接入真实流量,这个自检步骤能省下后面大量排错时间。

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

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

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

立即咨询