ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于机器学习的网络入侵检测系统设计:从流量分析到NSL-KDD实践

基于机器学习的网络入侵检测系统设计:从流量分析到NSL-KDD实践

简介:一套基于网络的入侵检测系统毕业设计完整资料,包含可运行源码、配套数据集与详细文档,面向计算机、软件工程、网络空间安全、电子信息等专业的在校学生与老师,可用于毕业设计、课程设计、期末大作业或项目初期演示,也可作为学习入侵检测原理与C语言网络编程的进阶素材。压缩包共39个文件,涵盖C语言源码、PDF/HTML技术文档、libpcap/snort/libnids库压缩包、OpenDocument演示文稿等,整体约16.64MB,目录按主程序、参考库、测试目录和说明文档安排,检索方便。资源内容结合了网络抓包、协议分析、入侵检测规则设计等关键环节,详细文档包含Snort源码分析、libpcap入门教程及协同开发指南,可引导读者从搭建实验环境到完成功能扩展。目前已有258人学习下载,项目经导师认可且答辩评审达95分,代码经过本地编译运行验证,适合直接使用或二次开发。

1. 基于网络的入侵检测系统:流量背后的黑匣子是怎样被撬开的

网络攻防的焦点几乎都集中在流量上——端口扫描、暴力破解、漏洞探测都会在网络中留下指纹。基于网络的入侵检测系统(Network-based Intrusion Detection System,NIDS)就是在旁路上监听这些流量、把异常行为挑出来的机制。对做毕业设计的学生来说,这个方向最大的吸引力在于它同时具备工程的完整性和科研的延展性:最朴素的实现是抓包加规则匹配,往深处可以嵌入机器学习模型,并配套数据集、源码和文档形成一条能互相佐证的交付链。它适合两类人:一类是已经能写 Python、想找一个老师一眼能看出工作量的课题;另一类是想要一套从采集到评估完整闭环的基线系统。这个方案的价值不在堆了多少模型,而在于数据、模型、系统三个层面能否真正闭环。

2. 系统架构与检测原理:先搞清楚流量怎么进来,再谈模型

2.1 NIDS 与 HIDS 的分工:为什么要在网络侧做检测

NIDS 的部署位置在网络的旁路而非主路径上。最常见的接入方式是交换机端口镜像(SPAN),把被监控链路的流量复制一份到指定端口;流量到达服务器网卡后,网卡需要工作在混杂模式才能收下所有目标 MAC 地址的数据包。相比基于主机的入侵检测系统(HIDS),NIDS 不登录业务主机、不安装 agent、不改动生产配置,对被测对象的侵入性为零。代价是缺少主机上下文——你只看得到包和流的表象,不知道是哪个用户在跑什么脚本,也不知道系统日志里是否已有异常记录。所以在真实环境里两者通常是互补关系:NIDS 管边界和横向移动的探测,HIDS 管单机内的纵深与文件完整性。

加密流量的普及让这个分工又发生了微妙变化。TLS 占主流后,深度包检测能读到的明文载荷越来越少,传统的签名匹配开始把重心转向传输层特征的组合,而同一条攻击行为很多时候要依赖流级特征才能显现:连接持续时间、单流包数、上行与下行的字节比例、单位时间窗口内的新建连接数。这恰恰是公开数据集里按流(connection/flow)组织的统计特征,例如 NSL-KDD 的 41 维特征中的 count、srv_count、serror_rate 等字段。也就是说,用公开数据集训练的模型,和从 pcap 里手工提取的流特征逻辑上是一致的,这个一致性在毕设实现里非常重要,后面第 4 章会专门说到怎么确保特征对齐。

关于架构,我一般建议毕设采用规则加机器学习的混合式:规则层用 Suricata 或 Snort 匹配已知攻击签名,负责“快而确定”的命中;机器学习层用流特征做异常评分,负责“慢而模糊”的兜底。两层结果汇入统一决策模块,输出的告警同时带上规则命中和模型置信度。这种写法在论文里能呈现出“多级检测引擎”的系统设计思路,比只堆一个模型要更有说服力,同时实现难度并不大:规则层只需要调用 Suricata 的命令行输出 JSON,模型层用 sklearn 即可。

2.2 五级数据链路:从网卡到告警的模块划分

动手写代码之前,先把 NIDS 的数据流按模块拆开。我习惯拆成五级:采集层、预处理层、检测层、决策层、输出层。采集层负责抓包或读 pcap 文件;预处理层把原始数据包聚合成流,再按滑动窗口统计出特征向量;检测层拿特征向量做规则匹配或模型推理;决策层把多重信号换算成统一的威胁分数,并根据阈值判定告警等级;输出层负责写告警日志、更新可视化页面或触发回调。每一层之间只通过简单的数据结构对接——例如采集层产出 list[pkt],预处理层产出 list[feature_vector],检测层产出 list[(label, score)]——这样答辩时被问到“怎么加一种新攻击类型”就能答得很清楚:要么在规则库加签名,要么在训练标签里加一个类别并重训模型,其余模块不动。

流(flow)是这一整套设计的核心抽象。流通常由五元组(源 IP、目标 IP、源端口、目标端口、协议)聚合而成,也可以只使用前三元组做粗粒度聚合。流的统计窗口一般取 2 秒或 5 秒,窗口内所有数据包累积出总字节数、包数量、SYN/ACK 标志位计数、时长等数值。NSL-KDD 这类 CSV 数据集里每一行就是一条流记录的统计结果,所以如果你直接把 CSV 字段当输入,那么从 pcap 到模型之间必须有一层“把原始包转成流特征”的桥接代码。不少翻车案例就是在这一步偷偷漏掉了一个字段,导致特征维度对不上、模型直接报错。

预处理层的另一个任务是处理数值范围不一致的问题。直接取出的特征里既有字节数这种大数值,也有各种比例值这种 0 到 1 的小数,如果不做标准化,树模型之外的算法都会被大数值特征带偏。后面统一用 StandardScaler 处理,但要注意只能对训练集 fit、再 transform 测试集,这个细节在第 5 章的避坑里继续展开。

2.3 检测引擎选型:规则、树模型还是深度学习

检测引擎的选择取决于你的数据和演示目标。规则引擎的优点是快、可控、可解释,缺点是每条规则都是人肉维护的签名,面对变种攻击几乎没有泛化能力。如果毕设以规则为主,老师在测试时会随手填几个不在规则库里的载荷,当场打穿。所以主检测引擎建议放在机器学习上,规则作为旁路过滤。

NIDS 的分析对象是网络通信协议栈里的 TCP/IP 流量,越靠近传输层越难被加密干扰,这也是检测特征集中在 TCP 标志位和连接状态上的原因。模型具体选什么,看数据集的体量:NSL-KDD 训练集约 12.6 万条、特征 41 维,属于典型的小型表格数据集,随机森林或梯度提升树(XGBoost/LightGBM)就能拿到非常好的指标,训练只要几秒到几十秒,迭代快、可解释性好;如果选了 CICIDS2017 这种百万级且有 pcap 真实流量的数据集,可以尝试用小型 CNN 或时序模型,但需要 GPU 训练,且类别不平衡问题会指数级放大。我通常会先用随机森林跑基线,再根据基线不达标的类别决定要不要上深度学习。答辩被问“为什么不用深度学习”时,最有力的话术是:当前表格特征场景下,随机森林在精度上不输甚至优于深度模型,同时训练成本低、特征重要性可解释,后续演进方向才是深度学习。

3. 数据集选择与预处理:先定下喂什么料,模型才有谱

3.1 公开流量数据集横向对比:NSL-KDD、UNSW-NB15、CICIDS2017

刚才反复提到 NSL-KDD,但毕业设计选题前应该先把候选数据集横向对比一遍。我按“最容易复现”到“最接近真实”的顺序列了四个常用集:

数据集数据形态规模攻击类别适合场景最大的坑
KDD Cup 99CSV训练约 490 万条4 大攻击类+正常教学基线重复记录多、特征偏老、存在泄漏
NSL-KDDCSV训练 125973 条、测试 22544 条4 大攻击类+正常毕设主流R2L/U2R 严重不平衡
UNSW-NB15CSV训练 175341 条、测试 82332 条9 大攻击类+正常分类更细特征维度多、仍需清洗
CICIDS2017CSV+pcap约 280 万条14 类攻击+正常想跑 pcap 演示文件太大、类别极不平衡

先说 NSL-KDD 为什么是“省心之选”:它是 KDD Cup 99 的修正版,去掉了大量重复样本,并重新切分了训练测试集,避免了很多“训练集里出现过测试样本”的泄漏问题。41 维特征分为基础连接特征、内容特征、基于时间的流量统计特征三类,标签既可以做二分类(normal/attack),也可以做五分类(normal/DoS/Probe/R2L/U2R),非常适合用来展示完整的预处理和建模流程。UNSW-NB15 的优点是覆盖 9 大类攻击,数据比 NSL-KDD 更新,如果论文里想把“攻击类别识别”作为亮点,可以用它。CICIDS2017 是真实网络环境下采集的,同时提供 csv 和 pcap,是最接近生产环境的选择,但 280 万条记录和按日期分片的结构处理起来比较费时。

我的建议很直接:如果目标是“把系统跑通、把文档写好”,选 NSL-KDD 作为训练数据;如果额外想演示实时抓包,可以再从 CICIDS2017 中抽取一小段 pcap(例如包含攻击的那段)做回放验证,不需要把整个数据集都灌进引擎。

3.2 读取 NSL-KDD 的最小管线:列名、编码与归一化

NSL-KDD 的文本文件没有表头,每一行 42 列,最后一列是标签。我一般在第一个代码模块里定义一个 41 长的特征名列表,然后直接读取:

import pandas as pd from sklearn.preprocessing import LabelEncoder, StandardScaler # 41 个特征按 NSL-KDD 官方文档的顺序排列 cols = ['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'] df_train = pd.read_csv('KDDTrain+.txt', names=cols, header=None) df_test = pd.read_csv('KDDTest+.txt', names=cols, header=None)

读取这一步没有太多玄学,真正容易翻车的是类别特征编码。列里的 protocol_type、service、flag 都是字符串,必须先编码成整数。常见的错误写法是对训练集单独 fit、然后对测试集 transform,一旦测试集出现训练集没见过的 service 值就会直接崩溃。稳妥做法是先把两集的取值合并,再统一 fit:

# 类别特征:先合并取值再编码,避免测试集出现未知类别 cate_cols = ['protocol_type', 'service', 'flag'] for c in cate_cols: le = LabelEncoder() all_vals = pd.concat([df_train[c], df_test[c]]).astype(str).unique() le.fit(all_vals) df_train[c] = le.transform(df_train[c].astype(str)) df_test[c] = le.transform(df_test[c].astype(str))

这里的 fit 参数是训练和测试所有字符串的并集,因此两边的 LabelEncoder 映射表完全一致。如果只对训练集 fit,理论上优雅,但实际拿到的数据文件很可能并非类别完全对齐,并集编码是最少踩坑的方式。

接下来是特征的标准化:

num_cols = [c for c in cols[:-1] if c not in cate_cols] # 严格只对训练集拟合 scaler,再转换测试集 scaler = StandardScaler() X_train_num = scaler.fit_transform(df_train[num_cols]) X_test_num = scaler.transform(df_test[num_cols]) X_train = pd.concat( [pd.DataFrame(X_train_num, columns=num_cols), df_train[cate_cols].reset_index(drop=True)], axis=1) y_train = df_train['label'].copy()

标准化顺序不能颠倒:如果先把全部数据拼在一起再 fit StandardScaler,测试集的均值方差就泄漏到了训练过程里,交叉验证分数会虚高,这是第 5 章第一个坑。

3.3 类别分布、不平衡处理与验证集划分

读取完数据先做一次“摸底”非常值得。看标签分布:

print(df_train['label'].value_counts())

NSL-KDD 训练集的五分类分布大致是:normal 67343 条、DoS 45927 条、Probe 11656 条、R2L 995 条、U2R 52 条。可以看到 R2L 和 U2R 是极端少数类。如果直接做五分类,模型几乎学不到 U2R 的特征,预测结果会一边倒地偏向多数类。学术上讲这个叫类别不平衡,处理方式基本有三条路:一是用 class_weight='balanced' 在训练时给少数类更高的权重;二是用 SMOTE 过采样少数类,但要注意必须在切分训练/验证集之后再合成,否则同样会有泄漏问题;三是改成二分类目标,把四类攻击都并成 attack 一类,这样多数类和少数类合并后相对均衡,精度会好看不少,但论文里“攻击类型细分”的亮点就弱了。

注意:LabelEncoder 用并集 fit 的方式牺牲了一点“严格性”,换来部署到未知数据时的鲁棒性。如果你换用 CICIDS2017 这类更大规模的数据,建议直接用 sklearn 的 ColumnTransformer 统一管理数值列和类别列的编码。

我通常的做法是:主实验做二分类,先保证系统可用性;辅助实验做五分类,并用类别权重和特征重要性来展示少数类处理的思路。答辩老师更看重的是你知道问题在哪、并且给出了合理的应对,而不是模型分数本身。划分验证集时记得用 StratifiedKFold 分层采样,保证每一折的类别比例接近原始分布,这个在最终验证章节会结合代码再讲一遍。

4. 核心源码模块实现:抓包、流特征、模型与告警的落地代码

4.1 用 Scapy 实时抓包并维护流统计

实时抓包是 NIDS 演示的门面,但如果一次性把几百个包都收进来再处理,内存和延迟都不可控。常见做法是用 Scapy 的 sniff 函数加上 per-packet 回调,在回调里做轻量统计:

from scapy.all import sniff, IP, TCP, UDP import time flow_stats = {} def packet_handler(pkt): # 只保留 IP 层及以上的包,ICMP 没有端口字段,单独处理 if not pkt.haslayer(IP): return ip = pkt[IP] # 取五元组作为流标识 if pkt.haslayer(TCP): sport, dport = pkt[TCP].sport, pkt[TCP].dport proto = 6 elif pkt.haslayer(UDP): sport, dport = pkt[UDP].sport, pkt[UDP].dport proto = 17 else: sport, dport = 0, 0 proto = ip.proto key = (ip.src, ip.dst, proto, sport, dport) stats = flow_stats.setdefault( key, {'start': time.time(), 'bytes': 0, 'pkts': 0, 'syn': 0}) stats['bytes'] += len(pkt) stats['pkts'] += 1 if pkt.haslayer(TCP) and pkt[TCP].flags & 0x02: # SYN 标志位 stats['syn'] += 1 # 每 200 个包输出一次当前流特征,演示用 if stats['pkts'] % 200 == 0: elapsed = time.time() - stats['start'] rate = stats['bytes'] / elapsed if elapsed > 0 else 0 print(f'flow={key} bytes={stats["bytes"]} pps={stats["pkts"]/elapsed:.1f} ' f'byte_rate={rate:.1f}B/s syn={stats["syn"]}') sniff(iface='eth0', prn=packet_handler, count=2000)

这个代码段的逻辑是:每收到一个 IP 包,按五元组找到对应的流记录,更新字节数、包数和 SYN 计数,然后周期性地把统计结果打印出来。需要说明的是,这里只做了最简统计,实际上要建模还需要在流结束时输出完整特征向量,Scapy 的 stop_filter 参数和超时机制可以用来控制流窗口的结束。参数方面,count=2000 是抓够 2000 个包就自动停止,适合演示;iface 要替换成你的实际网卡名,Windows 上常用 Npcap 接口名,Linux 上例如 eth0。

4.2 从 pcap 离线提取流特征:让训练集和实时数据对齐的关键

实时抓包只是演示用的,真正的模型训练依赖公开数据集。问题是公开数据集给的是 CSV 特征,而实时抓包产出的是原始包,这两者之间必须有一层转换。我一般会写一个从 pcap 文件提取流特征的函数,用回放历史 pcap 来验证特征对齐:

from scapy.all import rdpcap, IP, TCP, UDP import numpy as np import time def pcap_to_flow_features(pcap_path, window_sec=2.0): packets = rdpcap(pcap_path) flows = {} # 第一遍:按五元组聚合到流,记录窗口内统计量 for pkt in packets: if not pkt.haslayer(IP): continue ip = pkt[IP] if pkt.haslayer(TCP): sport, dport, proto = pkt[TCP].sport, pkt[TCP].dport, 6 elif pkt.haslayer(UDP): sport, dport, proto = pkt[UDP].sport, pkt[UDP].dport, 17 else: sport, dport, proto = 0, 0, ip.proto key = (ip.src, ip.dst, proto, sport, dport) f = flows.setdefault(key, {'bytes': 0, 'pkts': 0, 'start': pkt.time}) f['bytes'] += len(pkt) f['pkts'] += 1 # 第二遍:把流转换成特征向量 features = [] for key, f in flows.items(): feat = [ f['bytes'], # src_bytes 的近似 f['pkts'], # 包数 f['bytes'] / max(f['pkts'], 1), # 平均包长 0, # 占位:真实实现要补更多统计字段 ] features.append(feat) return np.array(features), flows

这个函数看起来简单,但角落很多:rdpcap 会把整个 pcap 读入内存,对大文件有压力,可以改用 PcapReader 逐包迭代;窗口统计的起止时间应该用包的时间戳 pkt.time 来计算,而不是系统当前时间,否则离线回放时窗口统计完全失效。真实工程里特征要从 CSV 训练集的特征名反向生成,保证列顺序一致,不能像上面这样随手写占位符——这里只是演示 pcap 转特征的最小框架。

注意:pcap 文件里每条包的时间戳是离线特征提取的唯一时钟源,start 与 end 字段都应取 pkt.time,用 time.time() 会让窗口统计完全失效。

4.3 用随机森林做二分类基线

数据准备好以后,模型部分其实很薄。用 scikit-learn 的随机森林完成二分类,并做了五折交叉验证:

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.metrics import classification_report # 二分类标签:把四类攻击合并为 attack y_train_bin = (df_train['label'] != 'normal').astype(int) y_test_bin = (df_test['label'] != 'normal').astype(int) clf = RandomForestClassifier( n_estimators=200, max_depth=30, min_samples_leaf=4, class_weight='balanced', n_jobs=-1, random_state=42, ) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(clf, X_train, y_train_bin, cv=skf, scoring='f1_macro') print(f'五折交叉验证 f1_macro: {scores.mean():.4f} ± {scores.std():.4f}') clf.fit(X_train, y_train_bin) y_pred = clf.predict(X_test) print(classification_report(y_test_bin, y_pred, target_names=['normal', 'attack']))

选随机森林而不是逻辑回归或 SVM,主要三个原因:它对特征量纲不敏感,数值特征和编码过的类别特征可以直接混合输入;能输出特征重要性,论文分析能直接用;内置多棵树的投票天然平滑了部分噪声。参数方面,n_estimators=200 在 12 万样本量级下已经能收敛,再多收益很小;max_depth=30 防止单棵树过深记住噪声;class_weight='balanced' 用来缓解多数类偏向。如果五折交叉验证的 F1 均值低于 0.9,先检查特征对齐而不是调参,这个顺序很重要。

4.4 告警输出模块与结果的结构化

检测到攻击时只打印一行文本对答辩来说不够直观。我会把告警输出成结构化日志,既方便后续接可视化面板,也方便统计误报率:

import json from datetime import datetime def emit_alert(feature_vec, prob, threshold=0.7): level = 'high' if prob >= threshold else 'medium' alert = { 'timestamp': datetime.now().isoformat(), 'attack_probability': round(float(prob), 4), 'level': level, 'feature': [round(v, 3) for v in feature_vec[:5]], # 截断展示 } with open('alerts.jsonl', 'a', encoding='utf-8') as f: f.write(json.dumps(alert, ensure_ascii=False) + '\n') return alert

这里把预测概率和阈值比较后生成高/中两个级别,告警落成 JSON Lines 格式。threshold 这个参数非常值得调:阈值拉高会降低误报但漏报增加,拉低则相反。在第 6 章的验证阶段,我会画 ROC 曲线来帮你选择一个合理阈值。

5. 避坑排查:训练集 99% 准确率,实时检测却翻车的原因

这一章列了五个我自己踩过或陪学生踩过的坑,每一条都按“现象→原因→解决”来梳理。

5.1 特征泄漏:为什么验证分数高得离谱

现象:交叉验证的 F1 接近 1.0,但换到测试集或真实流量后指标断崖式下跌。

原因:最常见的是预处理泄漏。训练集、测试集甚至全量数据一起 fit 了 StandardScaler,均值方差把测试集的信息带进了训练;另一个泄漏点是特征构造,比如用整段时间窗口内的统计值去构造每条样本的特征,这个“未来信息”在离线验证里看得到,但实时检测时不可能预知。NSL-KDD 本身已经修正了大部分重复泄漏,但你自己写的预处理代码同样可能制造泄漏。

解决:标准做法是 sklearn 的 Pipeline,它强制在每个交叉验证折内重复 fit/transform 流程;如果自己写循环,记住一个原则:任何统计类变换(标准化、PCA、SMOTE)都只能对训练折 fit,再用训练折的参数 transform 验证折。

5.2 少数类学不到:全被预测成 normal

现象:二分类没问题,五分类时 R2L 和 U2R 的召回率几乎为 0,模型输出全是 normal 和 DoS。

原因:R2L 只有约千条,U2R 只有几十条,面对几万条 normal,决策树在信息增益上几乎没有动力去切分这类极其稀疏的区域。

解决:先用 3.3 的 value_counts() 摸清底数;第二步在模型里设 class_weight='balanced_subsample' 或手动给类别加权;如果还不够,用 SMOTE 生成合成样本,但只能在划分训练/验证之后做。论文里不需要强求五分类全指标很高,诚实呈现少数类的难度并给出处理实验,本身就是一个加分项。

5.3 特征维数不一致:模型报错说 shape 对不上

现象:训练时一切正常,换到实时抓包或新 pcap 输入时模型端报 shape mismatch。

原因:训练用的是 NSL-KDD 的 41 维 CSV 特征,实时抓包模块只提取了 4~5 个自定义特征,两边的特征顺序和数量完全没对齐。

解决:把特征提取做成唯一的公共函数,训练前和推理前都调同一个入口。我通常把这个函数放在 features.py 里,里面定义 FEATURE_COLS 常量,它既用于给 CSV 列重命名,也用于给 pcap 提取结果排序。宁可多写几个兼容性断言(assert feature_vector.shape == (41,)),也不要到部署阶段才发现黑匣子。

5.4 Scapy 抓包丢包:流量一多,丢了快一半

现象:用 Scapy 在演示机抓包,流量稍大(比如几兆每秒)就开始丢包,统计的包数和实际不符。

原因:Scapy 是基于 Python 的包解析库,每个包都要经过 Python 层处理,性能远低于 tcpdump/Wireshark 的内核态抓包路径;当回调函数里同时做字典维护和打印输出时,处理一个包的时间可能超过包到达间隔。

解决:演示场景流量不大,可以降低处理逻辑,比如只在回调里做最小统计;更稳妥的方案是先用 tcpdump -i eth0 -w capture.pcap 把流量落盘,再用第 4.2 的离线解析函数做分析。如果有人问为什么不用更快的抓包工具,直接回答“实时抓包只做演示,核心检测链路在离线模块上验证”,这个边界在论文里是合理的。

5.5 答辩追问:为什么不用 CNN 或 Transformer

这个现象不是运行产生的,而是答辩时老师随口一问,学生当场卡壳。原因很简单:现在人工智能相关课程论文都在提深度学习,老师对“机器学习”方案会有天然的追问惯性。

解决:先摆数据事实:NSL-KDD 是 41 维表格特征,样本量 12 万级别,在这种规模下随机森林的精度与简单深度模型相当,但训练快、可解释性强;然后列举你的对比实验,比如随机森林 f1_macro 0.93,而两层 MLP 也就 0.91——当然这里的数值要换成你自己的实验结果。如果未来要接 CICIDS2017 的原始报文流,再引入时序网络才有价值。最后把话题引到特征重要性图上,展示你模型的可解释性,这块第 6 章会给出具体怎么画。

6. 验证与呈现技巧:混淆矩阵、五折交叉验证和特征重要性可视化

到了冲刺阶段,模型已经跑通,但怎么让答辩老师觉得“这个系统是严谨的”是关键。我的习惯是固定准备三样东西:混淆矩阵、ROC 曲线、特征重要性条形图。这三样加起来,既覆盖正确率也覆盖误报漏报的权衡,同时能说明模型在学什么。

6.1 混淆矩阵与 ROC 曲线的落地

混淆矩阵直接用 matplotlib 画热力图,代码很短:

from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt cm = confusion_matrix(y_test_bin, y_pred) plt.imshow(cm, cmap='Blues') plt.colorbar() plt.xlabel('Predicted') plt.ylabel('Actual') plt.xticks([0, 1], ['normal', 'attack']) plt.yticks([0, 1], ['normal', 'attack']) for i in range(2): for j in range(2): plt.text(j, i, cm[i, j], ha='center', va='center', color='red') plt.savefig('confusion_matrix.png', dpi=150)

ROC 曲线的价值在于告诉你阈值怎么选。用 predict_proba 得到攻击概率,画出 TPR 与 FPR 的关系,然后根据业务倾向选阈值:如果更怕漏报,选 TPR 高但 FPR 也偏高的左侧点;如果更怕误报,往右上走,对应更高的 threshold。这一步做下来,答辩当老师问“为什么阈值设 0.7”时,你能给出从曲线推导的过程,而不是拍脑袋。

6.2 特征重要性:用模型自己证明它学到了什么

特征重要性是另一个很容易打动人的可视化。把 clf.feature_importances_ 排序后取前十个特征画横向条形图:

import pandas as pd feat_imp = pd.Series(clf.feature_importances_, index=X_train.columns) feat_imp.nlargest(10).plot.barh() plt.xlabel('importance') plt.savefig('feature_importance.png', dpi=150)

通常你会看到 dst_host_srv_count、src_bytes、count 这类统计特征排在最前,这正是“网络流量统计特征比深层载荷更有效”的直接证据。答辩时把这张图放在系统设计之后作为模型分析页,老师基本不会再纠缠深度学习。

最后说个我自己的教训。第一次带毕设做 NIDS 时,我把大量时间花在调模型精度上,结果到最后才回头补数据清洗和评估,交叉验证与展示的数据几乎全重来。后来养成的习惯是:第一天先打通从 CSV 到随机森林的最小链路,哪怕准确率只有 85%,之后再迭代特征和模型都很快;特征对齐的断言、训练测试严格隔离这两条底线从一开始就写进代码,不改口。这个顺序帮我省了至少两周的翻车时间,也让你在答辩前留下更多余量。希望这个围绕入侵检测系统的“数据—系统—呈现”思路,能真正帮到你完成课题并拿到那个高分。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进