简介:面向Python机器学习课程设计与期末大作业场景,该压缩包提供了一套完整的网络入侵检测系统源码,依托KDD Cup 1999数据集完成训练与检测,适合需要快速交付高分项目的高校学生。资源共包含16个文件,以多个Python脚本(主程序、CNN模型、数据处理)、kddcup数据压缩包、项目配置与README文档为主,总大小约17.5MB,目录层次清晰,代码、数据与说明分离,方便对照阅读。该方案已获导师指导并通过,为97分高分期末大作业,涵盖数据预处理、模型构建、多日志分析等核心模块,下载后无需修改即可运行复现入侵检测流程,也可作为课程设计或期末大作业的完整工程基础。目前已有946人学习,适合希望借鉴完整项目结构、快速上手机器学习实战的初学者。
1. 基于 Python 机器学习的网络入侵检测系统源码:期末大作业为什么都选它?
学期末选大作业题目,时间紧、要求高、还要能现场答辩,基于 Python 机器学习的网络入侵检测系统源码是我见过性价比最高的一档选题。它做的事很直观:把一条条网络连接记录当作样本,训练分类模型,判断当前流量是正常还是攻击。
公开数据集、sklearn 一条流水线、加上混淆矩阵和特征重要性图,就足够支撑一次高分期末答辩。适合正愁大作业题目的计算机专业学生,也适合已经下载了同类源码、但卡在数据处理阶段的初学者。
真正劝退人的通常不是模型,而是数据预处理和测评方式:类别不平衡、字符串特征、时间泄漏,这三件事做不对,分数再好看也经不起答辩追问。这篇把“数据 → 建模 → 排错 → 验证”的路径完整走一遍。
2. 网络入侵检测建模前先过数据关:NSL-KDD 的四个预处理必踩点
先别急着 fit 模型。网络入侵检测方向的期末大作业里,代码翻车的概率远低于数据处理翻车的概率;数据没备好,后面所有模型结论都是空中楼阁。这一章先把数据来源、特征结构、标签映射和最容易出问题的字符串列讲清楚。
2.1 入侵检测用什么数据:NSL-KDD 为什么是期末作业默认答案
NSL-KDD 是 KDD Cup 99 的修正版,KDD Cup 99 又源自 DARPA 98 年的离线流量仿真。它去掉大量重复记录,按难度分层采样,让不同攻击类别的样本比例更接近真实场景,所以课堂、竞赛、期末大作业的资料基本都围绕它展开。每条样本由 41 个特征加一个攻击标签组成,标签分为五类:normal 加 DoS、Probe、R2L、U2R 四类攻击族;四类攻击又可以细分到几十个小类,比如 neptune、satan、warezclient。期末如果只求跑通,二分类足够;想冲高分,就做五分类,别把目标定复杂。
这 41 个特征不是平铺的,它本身就分四组,答辩时能说出这一层,说明你真读懂了数据。我一般按下面的口径记:
| 特征分组 | 关注信号 | 代表字段 |
|---|---|---|
| 基本连接特征 | TCP 会话本身的状态 | duration、protocol_type、service、flag、src_bytes、dst_bytes |
| 内容特征 | 载荷里与攻击相关的迹象 | hot、num_failed_logins、logged_in、num_shells |
| 两秒窗口流量特征 | 短时间内的连接密度与错误率 | count、srv_count、serror_rate、same_srv_rate |
| 主机窗口流量特征 | 过去 100 条连接的主机统计 | dst_host_count、dst_host_srv_count、dst_host_same_src_port_rate |
为什么不直接用 CICIDS2017 这类更现代的数据集?不是因为 NSL-KDD 更好,而是因为做期末大作业要的是“可复现”和“可解释”。CICIDS2017 光原始 pcap 就几十个 G,特征列八十几列,处理数据就得花两三天;NSL-KDD 一张 CSV 装完,五六分钟跑出结果,老师也熟悉它的攻击分类。
2.2 标签与类别不平衡:直接训练,少数攻击类会被淹掉
NSL-KDD 的类别分布并不是均衡的。整体看 normal 和 DoS 占了大头,Probe 有一些,R2L 和 U2R 少到“几乎是稀有事件”,尤其 U2R,在整个训练集中可能只有几百条。这带来一个具体后果:用原始比例训练,即便整体准确率到了 96%,模型也大概率把所有样本都判成 normal 或 DoS,U2R 这类真正体现入侵检测价值的攻击一条也抓不住。
期末需要两个策略。第一,标签做两层:一层二元标签 normal / attack,演示总体流程;一层五分类标签,展示对攻击族的区分能力。第二,模型侧用 class_weight 或过采样兜底,抵消多数类的压倒性优势。这两件事后面都会落到代码里,这里先记住结论:不要拿原始 label 直接往 sklearn 里塞。
2.3 读取 NSL-KDD 数据与标签映射:能直接复制的加载代码
import pandas as pd # NSL-KDD 的 CSV 没有表头,所以先把 41 列特征命名为 0~40,第 42 列是标签 train_df = pd.read_csv("KDDTrain+.csv", header=None) test_df = pd.read_csv("KDDTest+.csv", header=None) train_df.columns = list(range(41)) + ["label"] test_df.columns = list(range(41)) + ["label"] # 重命名几个常用列,方便后面按名字取特征 rename_map = {0: "duration", 1: "protocol_type", 2: "service", 3: "flag", 4: "src_bytes", 5: "dst_bytes", 6: "land", 7: "wrong_fragment", 8: "urgent"} train_df = train_df.rename(columns=rename_map) test_df = test_df.rename(columns=rename_map) # 二分类标签:normal 之外全部视为攻击 train_df["label_binary"] = train_df["label"].ne("normal").astype(int) test_df["label_binary"] = test_df["label"].ne("normal").astype(int) # 五分类标签:映射到四大攻击族,未枚举的小类兜底归到 R2L MULTI_CLASS_MAP = { "normal": 0, "neptune": 1, "smurf": 1, "pod": 1, "teardrop": 1, # DoS "nmap": 2, "satan": 2, "ipsweep": 2, "portsweep": 2, # Probe "warezclient": 3, "guess_passwd": 3, "imap": 3, "ftp_write": 3, # R2L "buffer_overflow": 4, "rootkit": 4, "loadmodule": 4, # U2R } train_df["label_multi"] = train_df["label"].map(MULTI_CLASS_MAP).fillna(3) test_df["label_multi"] = test_df["label"].map(MULTI_CLASS_MAP).fillna(3) print(train_df.shape, test_df.shape) print(train_df["label"].value_counts().head(10))header=None是关键,直接read_csv会把第一行当表头,而 NSL-KDD 原文件没有表头。重命名不是必须的,但“按列号写预处理”到后面自己看着都头大,所以先把常用列转成语义化名字。fillna(3)是为了把没有枚举到的小类归到 R2L 族,避免训练时遇到“未知标签”;这是工程取舍,答辩口径是“聚合大类,降低类别爆炸”。
2.4 字符串列比数值列更容易出问题:protocol_type、service、flag 怎么处理
41 个特征里最麻烦的是字符串列。protocol_type 取值基本是 tcp/udp/icmp 三个,flag 是连接状态码,不到十几类;service 有几十个取值,从 http、ftp 到 private、domain_u 都有。有些模板代码直接把这三列做 label encoding 丢进模型,随机森林确实能跑,但答辩一旦被问“为什么要这样编码”就露馅。
更要注意的是,对字符串列做 one-hot 后,service 的几十个取值会产生几十列稀疏特征。如果换逻辑回归训练,这些稀疏列在 StandardScaler 下容易失控;而树模型对稀疏特征容忍度高,所以常见的做法是:随机森林 + OneHotEncoder(handle_unknown="ignore"),把未知 service 当成空特征,模型不知道它是什么,但新流量出现新服务时至少不崩。这部分代码放在第 3 章 Pipeline 里讲,因为单独处理最容易踩“先 fit 再 transform 还是合并 fit”的坑。
3. 用 sklearn 把检测模型跑通:随机森林、Pipeline 与召回率评估
数据准备好之后,建模阶段的核心不是把准确率刷到 98%,而是让整条预处理 + 训练的链路可复现、可解释。这一章用 Pipeline 把字符串编码、数值标准化和随机森林串起来,再讲评估指标怎么看、特征重要性怎么用。
3.1 为什么主模型选随机森林:期末答辩最好讲、最好调
候选模型不少,但站在期末大作业的位置上,各自的毛病也很明显。朴素贝叶斯适合做对比实验,但特征独立假设在流量统计特征上基本不成立;逻辑回归解释性强,但面对 service one-hot 后的稀疏列,需要手动特征筛选和正则;SVM 在小样本上可以锐利,但这里训练集接近十万条,线性核勉强、RBF 核很慢。
随机森林恰好站在“效果、速度、可解释性”的平衡点上。它对数值尺度不敏感,对 one-hot 后的稀疏特征容忍度高,能输出特征重要性,而且需要调的核心参数就三四个,用 GridSearchCV 跑一轮也快。下面是期末场景里三个模型的可比口径:
| 模型 | 对稀疏 OneHot 的容忍度 | 答辩可解释性 | 训练耗时 |
|---|---|---|---|
| 朴素贝叶斯 | 低 | 一般 | 快 |
| 逻辑回归 | 低,需要筛选特征和正则 | 高 | 快 |
| 随机森林 | 高 | 高(特征重要性) | 中等 |
3.2 用 ColumnTransformer 把数值列和字符列一次处理完
很多模板代码先把预处理做成几个独立变量,训练集上手动 fit,测试集再手动 transform。这样容易出两类 bug:一是预处理参数泄漏到测试集,二是列顺序对不上导致维度不一致。用 ColumnTransformer 可以把标准化和 one-hot 绑进同一个转换器,放进 Pipeline 之后训练和预测都只调 fit/predict。
from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder cat_features = ["protocol_type", "service", "flag"] num_features = [c for c in train_df.columns if c not in cat_features + ["label", "label_binary", "label_multi"]] preprocessor = ColumnTransformer(transformers=[ ("num", StandardScaler(), num_features), ("cat", OneHotEncoder(handle_unknown="ignore"), cat_features), ])第一个转换器处理数值列,StandardScaler 会把连接时长、字节数从不同量纲拉回同一个尺度;第二个转换器把三个字符串列分别做 one-hot,service 几十个取值会变成几十列。handle_unknown="ignore"是给“测试集出现训练集没见过的 service”准备的,遇到未知取值时整行 one-hot 输出 0,模型按“什么都没发生”处理,而不会像 LabelEncoder 那样直接抛异常。
提示:把
handle_unknown去掉,测试集一旦出现陌生 service 就会当场报错。这次侥幸跑通不代表没有风险,换一个数据集版本就会炸。
3.3 训练与评估:先看召回率,再看准确率
from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report model = Pipeline(steps=[ ("prep", preprocessor), ("clf", RandomForestClassifier( n_estimators=100, max_depth=12, n_jobs=-1, class_weight="balanced", random_state=42)), ]) model.fit(train_df, train_df["label_binary"]) y_pred = model.predict(test_df) print(classification_report(test_df["label_binary"], y_pred, digits=3))n_estimators=100是效果与速度的常见折中,加到 300 收益有限;max_depth=12是为了防止随机森林把训练集噪声背下来,也让树更浅,答辩时更容易讲清楚。class_weight="balanced"针对第 2 章的类别不平衡,按类别频率反比放大少数类的惩罚权重,是期末最省事的处理办法。
评估阶段不要只盯 accuracy。这个数据上 accuracy 很容易到 90% 以上,但网络入侵检测系统真正关心的是“攻击来了有没有拦住”,也就是 attack 一类的 recall;同时也要看 precision,避免模型为了拦住攻击把所有流量都打成攻击,误报率爆掉。classification_report会按类别打印 precision、recall、f1,答辩时挑这一行来讲,比说“准确率 97%”有说服力得多。时间充裕的话,再用 GridSearchCV 把max_depth和n_estimators跑三五组候选,固定其他参数。
3.4 特征重要性:给期末报告补充一个可视化亮点
import pandas as pd rf = model.named_steps["clf"] cat_names = preprocessor.named_transformers_["cat"].get_feature_names_out(cat_features) feature_names = list(num_features) + list(cat_names) importance = pd.Series(rf.feature_importances_, index=feature_names) print(importance.sort_values(ascending=False).head(10))feature_importances_来自随机森林的杂质减少汇总,它告诉你哪些列在分裂时贡献大。一般排前面的会有 src_bytes、dst_bytes、duration、count、dst_host_count、service_http 这些。如果打印出来是数字列名,就到官方文档里对一下位序,期末不用全部重命名,把常出现的高权重列弄清楚就行。到这里可以顺手画一张水平条形图,把 Top10 特征放进去,PPT 里放一张,报告写一小节“会话字节数和主机统计特征对检测结果影响最大”,加分。
4. 常见问题与避坑:网络入侵检测大作业里五次让人翻车的报错
这一章是血泪经验汇总。下面五个问题我在不同版本的源码和数据上都遇过,现象、原因、解决写清楚,你照着排查能省下大量时间。
4.1 FileNotFoundError:解压后直接跑,最常见的第一道坎
现象:拿到源码解压,进入目录执行python main.py,立刻报FileNotFoundError: [Errno 2] No such file or directory: 'KDDTrain+.csv'。
原因:绝大多数模板代码用的是相对路径pd.read_csv("KDDTrain+.csv"),它依赖“当前工作目录”正好是数据文件所在目录。PyCharm 运行的工作目录可能是项目根目录,终端运行的工作目录是终端所在目录,数据文件如果放在data/子目录,路径又对不上。
解决:把路径锚定到脚本所在目录,而不是“猜用户在哪运行”。
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent DATA_DIR = BASE_DIR / "data" train_df = pd.read_csv(DATA_DIR / "KDDTrain+.csv", header=None)Path(__file__).resolve().parent拿到当前脚本的绝对目录,不管从哪个终端运行都不会错。如果是网盘中转的压缩包,解压时偶尔会出现文件损坏或者提示需要密码,这种情况下先重新下载,绝大多数是传输丢包,代码本身没问题。
4.2 整体准确率挺高,U2R 那一行的指标全是 0.00
现象:classification_report 打出来,整体 accuracy 96%,但往下滚,U2R 和 R2L 这两行的 precision、recall、f1 全是 0。
原因:这两类在训练集里样本太少,模型几乎没有见过正例。如果没有设置class_weight,分裂时只要把样本全部判为多数类,不纯度下降照样很低,于是 U2R 一个都抓不到。这是“准确率被多数类淹没”的典型表现。
解决:先加class_weight="balanced",让少数类的误分代价放大;如果还不够,把 U2R 单独挑出来做“正常 vs U2R”的二分类对比,再用 SMOTE 或简单的随机过采样把 U2R 复制几份再训练。期末不用追求完美,但要在报告里写清楚“少数类样本少,模型对它们的召回有限”,这句话比你藏着掖着等老师问更得体。
4.3 LabelEncoder 训练没报错,测试集爆出 previously unseen labels
现象:测试集执行le.transform(test_df["service"])时报ValueError: y contains previously unseen labels。
原因:字符串编码器在训练集上 fit 之后,测试集出现了一个训练集没见过的 service 取值,比如训练集里没有service_ftp_data而测试集里有,transform 找不到对应映射。
解决:分类特征上不要用 LabelEncoder,改用 OneHotEncoder 并设置handle_unknown="ignore",这是第 3 章 ColumnTransformer 里那个参数真正发挥作用的场景。如果一定要用 LabelEncoder,就先做集合差看看是哪几个值:
train_services = set(train_df["service"]) test_services = set(test_df["service"]) print("只出现在测试集的 service:", test_services - train_services)打印出来往往是几个冷门服务名。手工补映射不是不行,但很丑,而且破坏了“同一套代码换数据集还能跑”的复用性,所以推荐handle_unknown="ignore"。
4.4 随机划分测试集,分数虚高,换个时间段就露馅
现象:train_test_split(train_df, test_size=0.2, random_state=42)跑出来分类报告很好看,但把日期靠后的数据单独抽出来当测试集,性能明显下降。
原因:NSL-KDD 记录之间有时序相关性,紧挨着的连接往往来自同一台主机或同一种服务。随机划分会把相似样本拆散到训练集和测试集,相当于模型先见过答案再考试,这是一个典型的评估泄漏。
解决:期末演示至少改成按原文件顺序切分,模拟“用前面数据训练,预测后面数据”:
cut = int(len(df) * 0.8) train_part = df.iloc[:cut].copy() test_part = df.iloc[cut:].copy()这样只能减弱问题,不能完全消除,因为同一攻击家族本来就会在不同时段反复出现。但答辩时能说出“我没有随机划分,避免相邻连接泄漏到测试集”,这一句话就比只会跑模板的同学高不少。严格做法是分组交叉验证或按会话 ID 分组,NSL-KDD 没有现成会话 ID,所以多数人退而求其次用顺序切分。
4.5 特征重要性和网上文章对不上,代码没问题
现象:打印 Top10 特征,跟某博客的“前五个必是 srv_count、same_srv_rate、dst_host_count”完全不一致,于是怀疑自己代码写错了。
原因:特征重要性来自单棵树上杂质减少的累加,它受数据子采样、max_depth、随机种子、是否做了标准化和 one-hot 影响。另外网上很多文章用的是老版本 KDD99,不是 NSL-KDD,或者特征列拼接顺序不同,结果天然不可能完全一致。
解决:不要抄结论。固定random_state=42,同一个种子跑出来的结果可复现就行。如果要更稳,用 sklearn 的 permutation_importance 对测试集做排列重要性,它衡量的是“把这个特征打乱后模型性能掉多少”,比杂质减少更直白:
from sklearn.inspection import permutation_importance result = permutation_importance( model, test_df, test_df["label_binary"], n_repeats=5, random_state=42, n_jobs=-1, )这个计算量比训练还大,期末只对 Top10 特征做就可以,不必全量跑。报告里写“我同时用排列重要性做了验证”,比只说 feature_importances_ 稳得多。
5. 用你自己的流量做验证:别让模型只在 NSL-KDD 上好看
如果目标是高分,模型在标准数据集上跑通只是开始。老师最容易追问的一句话是:换一个环境还能检测吗?在期末答辩前,我建议做一个 10 分钟的自验证:用 Wireshark 的命令行版 tshark 抓一段自己浏览网页产生的流量,简单转成几条特征,喂给模型看概率。
拿到这一类源码,还要先确认它有没有“单条预测入口”。很多模板只有训练和评估,没有predict_one();答辩现场老师如果让你拿一条记录预测一下,临时拼代码会非常被动。建议先补一个函数:接收一条 dict 形式的记录,转成 DataFrame,与训练特征列对齐后调用predict_proba。这个函数本身二三十行,却是从“跑通源码”到“讲清源码”的分水岭。
import subprocess subprocess.run([ "tshark", "-i", "en0", "-a", "duration:30", "-T", "fields", "-E", "header=y", "-E", "separator=,", "-e", "frame.time_relative", "-e", "ip.proto", "-e", "tcp.len", "-e", "tcp.flags", ], stdout=open("capture.csv", "w"))这段命令抓 30 秒本机网卡流量,把相对时间、协议、TCP 长度、TCP 标志导出为 CSV。注意网卡名en0在 Windows 上通常是WLAN或“以太网”,先执行tshark -D查看可用网卡列表。抓包只需要本机权限,拿自己浏览器的流量验证就够了。
拿到 CSV 后,把抓到的协议、服务、flag、字节数映射到训练时的列名,缺失列用 0 填充,再调用:
record_df = pd.DataFrame([record]).reindex(columns=feature_names, fill_value=0) prob = model.predict_proba(record_df)[0]这里不要求完整复现 41 个特征,因为 tshark 字段到 NSL-KDD 特征之间存在一层映射逻辑。这一步的核心价值是看分布:如果模型把你正常上网的大多数流量判成攻击,说明特征分布和 NSL-KDD 差异太大,别急着否定模型,先看是不是 service 和 flag 字段的取值空间不匹配。
我当年就吃过亏:只在 NSL-KDD 上刷到 98%,答辩现场被问“能不能拿你电脑上的真实流量试一下”,临时跑出来一版误报率极高,只能硬着头皮解释特征分布漂移。后来再做类似项目,我都会把“真实流量验证”当成固定工序。期末大作业不需要做到生产级,但“标准数据 + 真实数据双验证”这件事,你做到一次,答辩的底气完全不一样。希望帮到你。
本文还有配套的精品资源,点击获取