简介基于Python机器学习的网络入侵检测系统源码包面向高校学生与机器学习初学者适用于网络安全课程设计、期末大作业及毕业设计等场景解决入侵检测任务中模型搭建困难、代码不完整、复现难度大等问题。项目已经导师指导并获97分高分核心功能完善下载后无需修改即可直接运行满足课设与答辩的交付要求。压缩包共16个文件大小约17.52MB其中4个Python脚本负责模型训练、检测与评估XML文件为工程配置MD说明文档描述项目设计与使用方式GZ格式存放KDD Cup数据集另有TensorFlow事件日志与备份文件数据、配置、代码分层存放结构清晰。针对KDD Cup数据集中类别多、特征量大的特点源码给出了具体的预处理与特征选取策略训练阶段可输出事件日志用于观察损失变化方便调参和复现。已有946人学习/下载认可度较高。除可直接运行的完整系统外还能从中学到网络流量特征处理、CNN模型构建、训练参数调优及结果分析的完整链路对完成同类入侵检测课题有直接参考价值。1. 基于Python机器学习的网络入侵检测系统期末大作业里最“稳”的安全方向如果你正为期末大作业选题发愁网络入侵检测系统NIDS几乎是机器学习方向里最好落地、最能讲出东西的题目之一。它解决的问题很直白从网络流量或连接记录里判断哪些是正常访问、哪些是攻击行为本质是一个二分类正常/攻击或多分类正常/DoS/Probe/R2L/U2R任务。用Python跑通这份源码你只需要一件事——把历史流量数据喂给机器学习模型让模型学会区分“好人”和“坏人”然后对新来的流量做预测。这个方案不需要真实攻击环境用公开数据集就能完成训练和验证适合期末答辩、课程设计也适合安全方向零基础入门的人练手。我下面按我实际做过的完整路径来讲从数据集选择、特征预处理、模型训练到集成成能演示的系统以及那些期末周最容易让你翻车的坑。2. 网络入侵检测系统先选对数据集NSL-KDD 与特征预处理2.1 为什么 NSL-KDD 是这个题目的默认选择做入侵检测的机器学习项目数据源基本绕不开两个公开数据集KDD Cup 1999 和它的改进版 NSL-KDD。KDD99 名气大但它有个致命问题训练集里大量重复记录模型很容易把重复样本背下来准确率虚高到 99% 以上答辩时老师一问真实场景就露馅。NSL-KDD 是 KDD99 的去重和平衡版本训练集和测试集的记录不重复每类样本数量也更合理是目前课程设计和论文里最常见的基准数据集。NSL-KDD 的每条记录有 41 个特征加 1 个标签。41 个特征分成三组第一组是 TCP 连接基本特征比如 duration、protocol_type、service、src_bytes、dst_bytes第二组是内容特征比如登录失败的次数、是否 root 登录第三组是流量统计特征比如过去 2 秒内相同目标主机的连接数、相同服务的连接数。这些特征里有连续数值字节数、时长也有离散字符协议类型、服务类型所以数据预处理的核心工作就是把字符特征转数值把标签映射成模型能理解的数字再统一量纲。我一般会建一个这样的目录结构来组织这个项目nids_ml/ ├── data/ │ ├── KDDTrain.txt │ ├── KDDTest.txt │ └── KDDTest-21.txt ├── preprocess.py # 特征处理 ├── train.py # 模型训练与评估 ├── detect.py # 命令行检测 ├── app.py # Web 检测界面可选 └── models/ # 训练好的模型文件答辩时老师通常会问“为什么选这个数据集”你只需要讲清楚两点NSL-KDD 解决了 KDD99 的重复记录和不平衡问题它是目前学术界和工业界验证入侵检测算法最通用的基准。这就够了。2.2 加载数据与特征数值化写清楚每一行的转换逻辑KDDTrain.txt 是带表头的 CSV 风格文件但列名需要自己定义。下面是加载数据并完成特征数值化的标准代码我做了注释说明import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, StandardScaler # NSL-KDD 的 41 个特征名顺序不能乱 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_col label train_df pd.read_csv(data/KDDTrain.txt, namescol_names [label_col]) test_df pd.read_csv(data/KDDTest.txt, namescol_names [label_col]) # 标签列原始标签是 normal. 或具体的攻击名做编码前先统一成可读形式 train_df[label] train_df[label].str.strip(.) test_df[label] test_df[label].str.strip(.) # 字符特征列protocol_type/service/flag 这三列需要转为数值 categorical_cols [protocol_type, service, flag] for col in categorical_cols: # 用 fit 在训练集上建立映射transform 同时应用到训练集和测试集 le LabelEncoder() train_df[col] le.fit_transform(train_df[col]) test_df[col] le.transform(test_df[col])这里有两个关键点。第一LabelEncoder 必须在训练集上fit再把同一个编码器应用到测试集不能在测试集上重新fit。如果训练集里没有出现测试集的某个 service 值transform会直接报错这一点后面避坑章节还会单独讲。第二str.strip(.)是为了把normal.和neptune.这类带句点的标签清理干净不然做标签映射时会出现匹配不上的问题。2.3 标签映射二分类还是多分类入侵检测的标签处理有两种路线二分类只判断是否异常和多分类具体识别攻击类型。期末大作业我建议两条路都做答辩时更出彩。二分类的映射逻辑是把normal映射为 0其余全部映射为 1。多分类则是把正常和每类攻击分别编码NSL-KDD 中共有 4 大类攻击DoS拒绝服务、Probe端口扫描探测、R2L远程到本地越权、U2R用户到 root 提权。多分类的做法是建立攻击名到攻击大类的映射表而不是直接对几十个细分的攻击名做编码否则类别太多、样本不均衡训练效果会很差# 攻击名 - 攻击大类NSL-KDD 标准映射 attack_map { normal: normal, back: dos, land: dos, neptune: dos, pod: dos, smurf: dos, teardrop: dos, apache2: dos, udpstorm: dos, processtable: dos, worm: dos, satan: probe, ipsweep: probe, nmap: probe, portsweep: probe, mscan: probe, saint: probe, guess_passwd: r2l, ftp_write: 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, sqlattack: u2r, xterm: u2r, ps: u2r } def map_label(label): return attack_map.get(label, unknown) train_df[label_binary] (train_df[label] ! normal).astype(int) train_df[label_multi] train_df[label].map(map_label)这段代码的逻辑不复杂但映射表是抄作业时最容易抄错的地方。unknown兜底很重要因为 NSL-KDD 测试集里有一部分攻击名是训练集没有的测试时遇到新攻击名至少不会直接崩溃。二分类用的(train_df[label] ! normal).astype(int)是一个很简洁的布尔转整数操作答辩时可以提一句。2.4 标准化、切分训练集和测试集顺序错了就是数据泄露数据标准化用StandardScaler让每个特征的均值为 0、方差为 1。这里有一个新手最容易犯的错对整个数据集先标准化再切分导致测试集的信息混入标准化的均值和方差里这属于数据泄露。正确做法是先切分再在训练集上fit然后用同一个 scaler 转换测试集feature_cols col_names # 41 个原始特征 X_train train_df[feature_cols].values.astype(float) y_train_bin train_df[label_binary].values y_train_mul train_df[label_multi].values X_test test_df[feature_cols].values.astype(float) y_test_bin (test_df[label] ! normal).astype(int).values y_test_mul test_df[label].map(map_label).values # 先切分这里直接用原始训练/测试集再 fit scaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) from sklearn.model_selection import train_test_split # 在训练集内部再切出 10% 作为验证集 X_tr, X_val, y_tr, y_val train_test_split( X_train_scaled, y_train_bin, test_size0.1, random_state42, stratifyy_train_bin )stratifyy_train_bin表示按标签比例分层抽样保证验证集和训练集的攻击样本比例一致。NSL-KDD 里正常和攻击样本比例原本就比较平衡加分层是习惯问题不加也不会差太多但答辩被问到“你怎么保证验证集分布一致”时这个参数就是你的加分项。3. 训练一个能交差的入侵检测模型算法选型与核心参数3.1 算法怎么选期末大作业不是算法竞赛很多人在这个环节会纠结是不是要用深度学习是不是要上 LSTM我的看法是期末大作业的评分重点是“你能不能把机器学习流程讲完整”不是“你用的模型多新”。在 NSL-KDD 这种表格数据上深度学习不一定打得过传统模型而且训练时间更长、调参更玄学。我推荐在下面几个模型里做对比兼顾效果和答辩可讲性模型优点缺点期末作业适配度决策树训练极快、完全可解释、效果不差容易过拟合、深度大时规则复杂高适合讲原理随机森林准确率高、抗过拟合、对异常值不敏感模型体积大、预测稍慢高主力模型KNN逻辑简单、无需训练预测慢、维度高时效果退化中适合做对比逻辑回归训练快、概率输出易解释线性假设、对非线性特征效果差中作为基线SVMRBF核小样本效果好、边界清晰数据量大时训练慢低NSL-KDD 上用会慢我一般会选随机森林作为主模型决策树作为对比模型KNN 作为第三个参考。答辩时老师问“为什么选随机森林”你可以回答它通过 Bagging 采样和多棵树的投票降低方差对入侵检测里特征维度高、存在噪声和异常值的场景有天然优势同时它不需要复杂的特征缩放但我们已经做了标准化不影响。这个回答比“因为它准确率高”要有说服力得多。3.2 完整训练脚本从数据到模型保存下面是我常用的训练脚本骨架包含训练、评估、模型持久化三步模型用joblib保存后面检测系统直接加载使用import joblib from sklearn.ensemble import RandomForestClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report, confusion_matrix, accuracy_score # 随机森林上一步切好的 X_tr, y_tr二分类标签 rf RandomForestClassifier( n_estimators100, # 树的数量100 棵是性能和时间的平衡点 max_depth20, # 限制单棵树深度防止过拟合 min_samples_split5, # 内部节点再划分所需最小样本数 min_samples_leaf2, # 叶节点最少样本数配合上一项用 random_state42, n_jobs-1 # 用所有 CPU 核训练加速 ) rf.fit(X_tr, y_tr) # 验证集评估 y_pred_val rf.predict(X_val) print(验证集准确率: {:.4f}.format(accuracy_score(y_val, y_pred_val))) print(classification_report(y_val, y_pred_val, target_names[normal, attack])) # 在真正的测试集上做最终评估KDDTest训练集从未见过 y_pred_test rf.predict(X_test_scaled) print(测试集准确率: {:.4f}.format(accuracy_score(y_test_bin, y_pred_test))) print(confusion_matrix(y_test_bin, y_pred_test)) # 保存模型和训练时用到的 scaler、编码器检测阶段全部要复用 joblib.dump(rf, models/rf_binary.joblib) joblib.dump(scaler, models/scaler.joblib)参数说明要细讲。n_estimators100不是越大越好树多了训练时间和模型体积线性增长100 棵在几万条数据上几十秒就能跑完而 300 棵的效果提升通常不到 1%不值当。max_depth20是对单棵树的深度限制无限深度的决策树纯属背题限制深度是随机森林“抗过拟合”的关键来源。min_samples_split5表示一个节点至少有 5 个样本才允许继续分裂min_samples_leaf2表示叶子节点至少要有 2 个样本这两个参数一起控制树的生长粒度值越小树越贴近训练集值越大越粗糙。n_jobs-1是 sklearn 的并行参数-1 表示使用所有 CPU 核心。3.3 评估指标准确率不是唯一标准入侵检测场景里有个特殊现象如果测试集里攻击样本占 40%一个“永远预测正常”的模型准确率也有 60%。所以答辩时老师一定会追问评估指标。你至少要能讲清楚这四个准确率Accuracy是整体预测正确的比例但它会被多数类主导精确率Precision是你预测为攻击的样本里真正是攻击的比例精确率低说明误报多召回率Recall是所有真实攻击里被找出来的比例召回率低说明漏报多F1-score 是精确率和召回率的调和平均类别不均衡时比准确率可靠。在入侵检测里漏报的代价往往比误报高——放走一次攻击可能整个内网被打穿而误报只是让运维多看一条告警。所以我一般要求模型对攻击类别的召回率达到 0.95 以上否则就是不合格。classification_report输出的每一行都要能看懂你的期末报告里用表格呈现这些指标比贴一段输出更专业类别精确率召回率F1-score样本数normal正常0.980.950.969711attack攻击0.930.980.95128333.4 多分类训练一个模型识别攻击类型二分类能拿基础分多分类是加分项。多分类的做法和二分类几乎一样只是标签从 0/1 变成 normal/dos/probe/r2l/u2r 五个类别from sklearn.ensemble import RandomForestClassifier rf_multi RandomForestClassifier( n_estimators120, max_depth25, min_samples_split5, min_samples_leaf2, class_weightbalanced, # 自动对少数类加权缓解 R2L/U2R 样本太少的问题 random_state42, n_jobs-1 ) rf_multi.fit(X_train_scaled, y_train_mul) y_pred_multi rf_multi.predict(X_test_scaled) print(classification_report(y_test_mul, y_pred_multi))class_weightbalanced会根据各类别样本数量自动调整权重样本量少的 R2L 和 U2R 会获得更高的误分类代价从而让模型更重视它们。但说实话NSL-KDD 里 R2L 和 U2R 的样本数量太少U2R 训练集只有几十条任何模型在这两个类别上效果都不好这不完全是你的问题。答辩时主动说“U2R 因为样本量太少效果不理想这是数据集本身的不平衡问题”比你等老师问出来再解释要好得多。4. 入侵检测系统的避坑手册5个常见翻车点与排查记录4.1 坑一标准化做早了数据泄露导致测试集分数虚高现象训练集准确率 0.99验证集 0.99测试集也 0.99但你把模型用到自己抓的流量上效果稀碎。原因你在切分训练集和测试集之前就做了标准化scaler.fit是在全量数据上做的测试集的均值和方差已经混进了标准化参数。模型训练时“见过”测试集分布评估分数虚高。这是一种非常典型的数据泄露。解决把fit_transform放到train_test_split之后测试集只用transform。检查方法很简单——打印出scaler.mean_然后对照只用训练集计算出的均值如果两者不同说明泄露了。4.2 坑二检测阶段特征顺序错位预测结果全乱现象训练时模型好好的检测脚本接手后对一条明显是攻击的记录预测成了 normal或者直接报维度错误。原因检测阶段重新构造特征时特征的排列顺序和训练时不一致。比如训练时第 4 列是flag已经编码成数值检测时你手写的特征列表把src_bytes放到了第 4 列。模型不关心列名只认位置顺序一错全是黑匣子。解决在训练阶段把特征列顺序存下来检测阶段严格按这个顺序构造特征。保存模型时顺手把feature_cols也joblib.dump一份joblib.dump({model: rf, scaler: scaler, features: feature_cols}, models/rf_binary_full.joblib)检测时先加载这个字典拿features列表去对齐输入数据。这个习惯能救你一命。4.3 坑三LabelEncoder 在测试集上重新 fit遇到新类别直接崩现象测试集加载后做特征编码时报错y contains previously unseen labels或者编码结果训练和测试对不上。原因test_df[col] le.fit_transform(test_df[col])在测试集上新做了一个编码器测试集里出现了训练集没有的 service比如http_8001新编码器会把它编成新数字整个特征空间错位。解决训练阶段的编码器用le.transform处理测试集不要在测试集上再fit。如果测试集里真有训练集没见过的类别用handle_unknownignore或手动把未知值填充为 -1# 检测阶段未知类别兜底处理 def safe_transform(le, values): known set(le.classes_) return [le.transform([v])[0] if v in known else -1 for v in values]4.4 坑四混淆矩阵和分类报告对不上标签映射错位现象混淆矩阵里 normal 类别下出现了大量数值但分类报告里 normal 的召回率却很低两个指标对不上。原因做二分类时标签映射不一致——训练时把normal映射成 0检测脚本把attack当作 0正负标签完全反了。混淆矩阵的四个格子位置是固定的左上 TP、右上 FN、左下 FP、右下 TN但传入confusion_matrix(y_true, y_pred)时labels参数如果不显式指定会按两个数组里出现的最小值排序。你的y_true里是 0/1y_pred如果全是 1矩阵形状都会变。解决调用confusion_matrix时显式传labels[0, 1]确保两个维度顺序固定。同时打印一份交叉表import pandas as pd result pd.DataFrame({真实标签: y_test_bin, 预测标签: y_pred_test}) print(pd.crosstab(result[真实标签], result[预测标签]))用交叉表核对比直接看混淆矩阵直观得多。4.5 坑五测试数据不是给的 KDDTest而是从训练集里抽的现象测试集准确率比训练集还高或者验证集上表现优秀、测试集上暴跌。原因你为了“省事”直接从 KDDTrain.txt 里用了train_test_split切出 20% 当测试集而没有使用 NSL-KDD 官方提供的KDDTest.txt。KDDTest 和 KDDTrain 有分布差异里面包含训练集没见过的攻击变种这才是真正的考验。只在自己的切分里自嗨答辩现场演示时用官方测试集分数断崖式下跌。解决训练用 KDDTrain评估用 KDDTest这是这个项目的标准流程。KDDTest-21.txt是更难的子集过滤掉了最容易判别的记录可以拿来作为“困难模式”加分项。5. 把模型包成能演示的系统命令行检测与 Web 展示的落地写法5.1 文件清单与系统流程模型训练完之后要把它变成能演示的系统。期末大作业的系统一般有三种形态命令行工具、Web 界面、离线批量检测脚本。我推荐做前两个命令行工具用于答辩时快速验证单条记录Web 界面用于打开网页点两下演示效果屏幕前展示比敲命令更有视觉冲击力。系统整体流程是加载保存的模型和参数 → 接收输入流量记录 → 预处理字符编码 特征对齐 标准化 → 模型预测 → 输出结果和概率。这个阶段的项目结构继续沿用前面的目录确保models/里有训练阶段保存的完整模型包nids_ml/ ├── models/ │ └── rf_binary_full.joblib # 包含 model/scaler/features ├── detect.py ├── app.py ├── requirements.txt └── README.md5.2 命令行检测脚本单条记录和批量文件都能跑detect.py的职责是读取特征输入调用模型预测。它的核心难点不在预测而在把输入转换成和训练时完全一致的特征向量。我推荐把它写成支持两种输入模式单条记录用命令行参数传入批量记录用 CSV 文件传入import sys import numpy as np import pandas as pd import joblib def load_model_package(pathmodels/rf_binary_full.joblib): pkg joblib.load(path) return pkg[model], pkg[scaler], pkg[features] def preprocess_input(raw_record, feature_cols): 把一条原始记录转成 41 维特征向量顺序必须以 feature_cols 为准 if len(raw_record) ! len(feature_cols): raise ValueError(f特征数量不对期望 {len(feature_cols)}实际 {len(raw_record)}) # 这里假设 raw_record 已经是数值型如果是原始 KDD 记录需要先做字符编码 vec np.array([float(x) for x in raw_record]).reshape(1, -1) return vec def detect_single(raw_record, model, scaler, feature_cols): vec preprocess_input(raw_record, feature_cols) scaled scaler.transform(vec) # 注意是 transform不是 fit_transform prob model.predict_proba(scaled)[0] pred model.predict(scaled)[0] return pred, prob if __name__ __main__: model, scaler, feature_cols load_model_package() # 单条python detect.py --record 0 tcp http SF 181 5450 0 ...41个值 if --record in sys.argv: idx sys.argv.index(--record) raw sys.argv[idx 1].split(,) pred, prob detect_single(raw, model, scaler, feature_cols) label 攻击 if pred 1 else 正常 print(f检测结果: {label}攻击概率: {prob[1]:.4f})这段代码的逻辑说明predict_proba返回的是每个类别的概率二分类时prob[0]是正常概率、prob[1]是攻击概率。答辩时输出攻击概率比只输出 0/1 更有说服力因为老师会问“模型凭什么判断这是攻击”概率就是你给出置信度依据。5.3 用 Flask 包一层 Web 界面简单的就是最好的如果答辩环境允许演示Flask 写一个最小可用 Web 界面是最稳妥的方案。不要追求复杂的前端框架一个 HTML 表单足够from flask import Flask, request, render_template_string import joblib import numpy as np app Flask(__name__) model, scaler, feature_cols load_model_package() HTML_PAGE !DOCTYPE html html body h2网络入侵检测系统/h2 form methodpost textarea namefeatures rows4 cols100 placeholder输入 41 个特征值用逗号分隔/textareabr button typesubmit检测/button /form p检测结果: {{ result }}/p /body /html app.route(/, methods[GET, POST]) def index(): result 等待输入... if request.method POST: raw_text request.form[features].strip() raw [float(x) for x in raw_text.split(,)] vec np.array(raw).reshape(1, -1) scaled scaler.transform(vec) prob model.predict_proba(scaled)[0] pred model.predict(scaled)[0] result 攻击 (概率: {:.4f}).format(prob[1]) if pred 1 \ else 正常 (置信度: {:.4f}).format(prob[0]) return render_template_string(HTML_PAGE, resultresult) if __name__ __main__: app.run(host127.0.0.1, port5000, debugFalse)这里有个安全细节feature_cols有 41 个特征如果用户在文本框里只输入 20 个值raw长度不对会直接崩。上线演示前一定要加长度校验并返回友好提示不然临场翻车很尴尬。Web 界面只是壳核心逻辑和命令行完全一致复用load_model_package()保证两处行为一致。5.4 进阶用 Scapy 抓真实流量做实时检测可选加分项NSL-KDD 的特征是历史连接记录不是原始数据包。如果你想演示“实时检测”常见做法是用 Scapy 监听网卡数据包解析出五元组源 IP、目的 IP、源端口、目的端口、协议和包长、标志位等信息再映射到 41 个特征。这个方案能做但有两个坑一是真实流量的特征分布和 NSL-KDD 完全不同模型准确率会明显下降二是很多特征如dst_host_srv_count需要统计窗口内的历史连接单包无法计算。我的建议是如果时间紧实时检测作为“概念演示”在答辩 PPT 里带过就行不要真做否则你会陷入特征对齐的泥潭。6. 比同学多拿分的进阶操作模型对比实验与期末报告可视化到了这个阶段系统已经能跑、能演示、能检测了。如果你想在答辩里拉开差距不要只交“一个模型跑通了”。我强烈建议做一组模型对比实验把结果直接做成图表放进期末报告。这个操作的成本很低但在老师眼里代表你真的理解了机器学习评估方法论而不仅仅是一个“调包侠”。多模型对比的脚本很简单。把第 3 章的随机森林换成几个模型循环训练记录每个模型在测试集上的准确率、精确率、召回率、F1 和训练时间from sklearn.ensemble import RandomForestClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, f1_score, recall_score import time models { 决策树: DecisionTreeClassifier(max_depth15, random_state42), 随机森林: RandomForestClassifier(n_estimators100, max_depth20, random_state42, n_jobs-1), KNN: KNeighborsClassifier(n_neighbors5), 逻辑回归: LogisticRegression(max_iter500, random_state42) } results [] for name, model in models.items(): t0 time.time() model.fit(X_train_scaled, y_train_bin) elapsed time.time() - t0 y_pred model.predict(X_test_scaled) results.append({ 模型: name, 准确率: accuracy_score(y_test_bin, y_pred), 召回率: recall_score(y_test_bin, y_pred), F1: f1_score(y_test_bin, y_pred), 训练时间(s): round(elapsed, 2) }) df_results pd.DataFrame(results) print(df_results.to_string(indexFalse))然后画一张柱状图把四个模型的准确率和召回率并列展示import matplotlib.pyplot as plt x df_results[模型] acc df_results[准确率] rec df_results[召回率] plt.figure(figsize(8, 5)) plt.bar(x, acc, width0.4, label准确率, alpha0.8) plt.bar(x, rec, width0.4, label召回率, alpha0.8, alignedge) plt.ylabel(分数) plt.ylim(0.5, 1.0) plt.legend() plt.title(不同模型在NSL-KDD测试集上的表现对比) plt.savefig(results/model_comparison.png, dpi150)这张图放进期末报告的价值远大于它 10 行代码的成本。答辩时你指着图说“逻辑回归的召回率明显低于随机森林说明线性模型无法捕捉入侵流量的非线性特征KNN 的准确率尚可但训练时间随样本量线性增长不适合大规模流量检测。”这就不是报参数而是在做分析。做对比实验时有一个血泪经验不同模型的默认参数直接跑就行没必要每个都调参。决策树用max_depth15随机森林用默认n_estimators100KNN 用n_neighbors5逻辑回归把max_iter调大避免不收敛。这个对比的目的是给大家看“不同算法家族的差异”不是看调参技巧——课程设计阶段没人指望你做超参数搜索。最后把模型持久化时我习惯把实验记录写进模型文件名里比如rf_binary_acc095.joblib这样模型多了之后不会忘记哪个是最终版本。期末周熬夜改方案是常态这个习惯能让你少改一次 bug。希望这篇文章能帮你把期末大作业做得又快又稳答辩顺利。本文还有配套的精品资源点击获取