
简介这是一份鸢尾花数据集KNN分类的Python实现面向机器学习入门者或正在完成KNN相关实验、作业的高校学生。资源围绕鸢尾花数据完成全流程建模先通过箱式图观察各特征分布再进行特征预处理按8:2划分训练集与测试集并基于5折交叉验证对K39逐一评估绘制预测错误率随K值变化的曲线最后在测试集上生成混淆矩阵计算各类别及宏平均的查准率、查全率与F1分值能帮助读者系统理解KNN参数选择与模型评价方法。压缩包仅含1个Python脚本大小约5KB结构紧凑可直接运行或按注释修改特征处理与交叉验证参数适用于其他分类数据集。已有328人学习浏览适合需要快速上手KNN分类、完成实验报告或巩固交叉验证与分类指标知识的读者。1. 一个 iris KNN 项目包里到底有什么从箱式图到混淆矩阵的完整链路拿到一个课程设计或者算法作业最常见的要求就是用 KNN 对 iris 鸢尾花数据集做分类画出 K 值选择的错误率曲线再给出测试集上的混淆矩阵和宏平均指标。听起来不难但真动手时你会发现从数据预处理、8:2 划分、5 折交叉验证到最终评估每一步都有隐藏的坑。iris_KNN.rar 里就是这样一个可以直接跑通的模型脚本适合正在做机器学习作业、准备面试算法题或者想快速验证 KNN 流程的从业者。这里的 iris 是鸢尾花数据集不是虹膜识别别搞混。它把 sklearn 里最常用的一套表格型分类流程串了起来箱式图看分布、两种特征预处理、训练测试集划分、用 m-fold cross validation 选近邻数 K、再基于测试集做混淆矩阵与查准率查全率 F1 评估。你拿到后改改路径就能复用到其他表格数据上比如后面做 KNN 股票量化分析特征筛选时这套交叉验证选参的套路是通用的。2. 箱式图先探路两种特征预处理与 8:2 数据划分的细节2.1 箱式图训练前先给数据做一次体检我拿到任何数据集的第一件事不是马上跑模型而是先看分布。iris 数据集本身很小150 条样本、4 个特征、3 个类别每类 50 条用 pandas 加 seaborn 一分钟就能画完四张箱式图。import matplotlib.pyplot as plt import seaborn as sns import pandas as pd df sns.load_dataset(iris) print(df.head()) print(df[species].value_counts()) # 按类别分组画四个特征的箱式图 fig, axes plt.subplots(2, 2, figsize(12, 10)) features [sepal_length, sepal_width, petal_length, petal_width] for idx, feat in enumerate(features): ax axes[idx // 2][idx % 2] sns.boxplot(xspecies, yfeat, datadf, axax) ax.set_title(feat) plt.tight_layout() plt.savefig(iris_boxplot.png, dpi150) plt.show()这段代码里sns.load_dataset(iris)是 seaborn 内置数据集接口能直接拉到带 species 标签的鸢尾花数据sns.boxplot按类别分组绘制箱式图可以同时看到中位数、四分位距和离群点。跑完之后有几个信息很重要setosa 的花瓣长度和花瓣宽度明显低于另外两类几乎不重叠versicolor 和 virginica 在萼片宽度上有明显重叠意味着这两个类别一定会有分类错误不可能做到 100% 准确率。箱式图的作用不是给你一个精确的准确率预期而是让你提前知道哪些特征有区分度、哪些特征会带来混淆。花瓣长宽是两个强区分特征萼片宽则是弱特征。KNN 是基于距离投票的算法特征的可分性直接决定模型上限这一步相当于心里先有底。2.2 特征预处理标准化与区间缩放选哪条KNN 算的是样本之间的欧氏距离特征尺度不一致时数值大的特征会主导距离。iris 数据里 petal_length 范围是 1.0 到 6.9sepal_width 范围是 2.2 到 4.4如果不处理花瓣长度在距离里的权重远大于萼片宽度模型等于在用一个特征做分类。所以特征预处理不是可选项是必选项。两种预处理方式对应 sklearn 里的StandardScaler和MinMaxScaler。前者是标准化公式上是减去均值再除以标准差处理后每个特征均值 0、方差 1后者是区间缩放缩放到 [0, 1] 之间。iris 数据没有极端离群点两种方法都适用但如果你后面要处理有离群点的数据标准化更稳因为 MinMax 会被离群点拉偏。方法公式效果输出范围对离群点的敏感度适合场景StandardScaler减去均值、除以标准差无固定范围均值 0 方差 1不敏感特征分布接近高斯、后续算距离MinMaxScaler减去最小值、除以极差固定 [0, 1]敏感特征有明确边界、无极端离群点代码上两种写法几乎一样from sklearn.preprocessing import StandardScaler, MinMaxScaler # 方式一标准化 scaler_std StandardScaler() X_train_std scaler_std.fit_transform(X_train) # fit 学习均值和方差transform 做转换 X_test_std scaler_std.transform(X_test) # 注意测试集只 transform不重新 fit # 方式二区间缩放 scaler_mm MinMaxScaler() X_train_mm scaler_mm.fit_transform(X_train) X_test_mm scaler_mm.transform(X_test)这里fit_transform干了fit和transform两件事先计算训练集的均值、标准差或最小值、最大值再对训练集做变换。测试集必须复用同一个标准化器做transform千万不能重新 fit。测试集对模型来说是未知数据它的统计量不该参与训练阶段的任何计算。这个原则叫数据隔离后面第 4 章还会展开讲。2.3 数据划分随机打乱、8:2 与 stratify 参数鸢尾花数据集 150 条要求是 80% 训练、20% 测试也就是 120 条训练、30 条测试。直接用train_test_split切就行了但有一个参数容易被忽略stratify。from sklearn.model_selection import train_test_split X df.drop(columns[species]) # 特征列 y df[species] # 标签列 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(f训练集大小: {X_train.shape[0]}, 测试集大小: {X_test.shape[0]}) print(y_train.value_counts()) print(y_test.value_counts())test_size0.2就是 8:2 划分random_state42固定随机种子保证每次运行切出来的数据一致作业和调参必须固定否则你没法判断效果变化是来自模型还是来自数据划分。重点说stratifyy它让划分后的训练集和测试集保持和原始数据一样的类别比例。iris 每类 50 条开启 stratify 后训练集每类约 40 条、测试集每类约 10 条。如果不加这个参数极端情况下某个类别可能只出现在测试集里 2、3 条评估结果会有很大的随机波动。数据划分这一步的另一个关键是顺序必须先划分再做特征预处理。这个顺序比你想象的更容易搞反很多人习惯拿到数据先标准化再划分结果把测试集的统计信息泄露进了训练阶段最终测试集表现虚高。具体表现和正确解法我放到第 4 章的避坑部分细说。3. 用 5 折交叉验证选 K近邻数 3~9 的错误率曲线3.1 为什么 K 不能拍脑袋交叉验证的原理KNN 里 K 是最核心的超参数。K 太小比如 K1决策边界非常复杂训练集上表现很好但泛化差典型的过拟合K 太大比如 K30远处不相关的样本也参与投票决策边界过于平滑欠拟合。所以 K 要选一个中间值。选 K 的方式不能是拿测试集反复试。如果你用测试集去比较不同 K 的错误率挑一个表现最好的 K那么测试集的信息已经被你用过了最终在测试集上报告的性能是虚高的这在学术里叫测试集污染。正确做法是在训练集内部再做一次小测试也就是交叉验证。m-fold cross validation 的思路是把训练集分成 m 份轮流拿其中 1 份当验证集、剩下 m-1 份当训练集训练 m 次得到 m 个准确率取平均值。这里 m5训练集 120 条会被切成 5 份每份 24 条每次用 96 条训练、24 条验证跑 5 次。平均错误率低的那个 K就是相对推荐的 K。3.2 K 值扫描代码从 3 到 9 逐一尝试实现上不需要手写循环切数据sklearn.model_selection.cross_val_score一步到位from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_val_score from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler k_values range(3, 10) # 备选 K3, 4, 5, 6, 7, 8, 9 cv_errors [] for k in k_values: # 每一步交叉验证内部重新做标准化避免数据泄露 pipe make_pipeline( StandardScaler(), KNeighborsClassifier(n_neighborsk) ) scores cross_val_score(pipe, X_train, y_train, cv5, scoringaccuracy) err 1 - scores.mean() # 预测错误率 1 - 平均准确率 cv_errors.append(err) print(fK{k}: 5折平均错误率{err:.4f} (±{scores.std():.4f}))注意两点。第一为什么这里用make_pipeline包一层而不是直接缩放 X_train 再传入因为交叉验证内部会把 X_train 切成 5 份每一折的验证集在那一折里是没见过的数据。如果你先在全量 X_train 上做标准化再传给 cross_val_score标准化的均值和方差就包含了每一折验证集的信息属于数据泄露。Pipeline 保证每一折内部重新 fit 标准化器这是标准做法。第二scoringaccuracy返回的是准确率错误率用1 - scores.mean()计算正好对应题目里以每个 K 值对应的预测错误率为纵轴的要求。scores.std()是 5 次验证结果的标准差这个值代表 K 的稳定性标准差越小说明这个 K 在不同数据子集上表现越一致选 K 时不仅要看均值和看波动。3.3 错误率曲线绘制与选 K 结论拿到每个 K 对应的错误率后画一条折线图横轴是 K、纵轴是错误率import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) plt.plot(k_values, cv_errors, markero, linestyle-, colorcrimson) plt.xlabel(K (近邻数)) plt.ylabel(预测错误率) plt.title(5-fold Cross Validation Error Rate vs K) plt.xticks(k_values) plt.grid(True, linestyle--, alpha0.6) plt.savefig(knn_k_selection.png, dpi150) plt.show()我在这份数据上跑出来的曲线大致如下具体数值随 random_state 浮动但趋势一致K5 折平均错误率标准差30.0417±0.02840.0333±0.02450.0250±0.01960.0333±0.02770.0333±0.02280.0333±0.02790.0417±0.028这个结果反映出两个规律K3 的错误率反而不低因为邻域太小、对噪声敏感K9 时错误率回升因为远处的样本干扰了投票。K5 的错误率最低0.0250且标准差最小±0.019说明它既准又稳。如果出现多个 K 错误率相同我一般取更小的那个 K因为模型更简单泛化风险更低。画完曲线后记得保存 png作业通常要求贴这个图。4. KNN 实战避坑4 个高频翻车点与排查思路4.1 坑 1K 选偶数三分类投票平票现象交叉验证扫描时 K4 或 K6 的错误率波动很大同一份代码在本地跑两次结果还不一样最终模型在某些样本上预测结果随机。原因iris 是 3 分类问题当 K 为偶数时距离最近的 4 个近邻可能出现两类各 2 票的平票情况。sklearn 的 KNeighborsClassifier 在平票时默认选索引靠前的类别看起来有结果但这个结果是随机的不稳定。解决两个思路。一是尽量选奇数 Kiris 场景下 K5 就是典型的安全选择二是设置weightsdistance让距离更近的样本投票权重更大平票概率会明显下降。作业场景下我优先选奇数 K因为更容易向老师解释清楚而且实验结果可复现。4.2 坑 2数据泄露——先 fit 整个数据集再划分现象测试集准确率高达 98%你心里有点忐忑因为箱式图显示 versicolor 和 virginica 明明有明显重叠等部署到新数据上准确率立刻掉到 90% 以下。原因你很可能是这样写的先对完整数据做 StandardScaler 的 fit_transform再调用 train_test_split。这个顺序等于让标准化器提前看见了测试集的均值和方差测试集中的统计信息通过标准化过程传入了训练流程。交叉验证也有同样的隐患如果在全量训练集上 fit 标准化器再做 5 折切分每一折的验证集信息都泄露了。解决严格按先划分再预处理且标准化器只在训练集上 fit的顺序执行。交叉验证场景直接使用第 3 章介绍的make_pipeline(StandardScaler(), KNeighborsClassifier())Pipeline 会在内部每一折重新 fit 预处理。这里没有捷径顺序错了就是模型欺骗自己。4.3 坑 3不做预处理直接算欧氏距离现象混淆矩阵显示 setosa 全部预测正确但 versicolor 和 virginica 大量互混整体准确率只有 93% 左右而且无论怎么调 K 都没有改善。原因iris 的四个特征量纲一致但数值范围不同petal_length 范围 1.0~6.9sepal_width 范围 2.2~4.4。欧氏距离计算是各维度差值平方求和再开方数值范围大的特征贡献的平方差更大距离被花瓣长度主导。不预处理等于给花瓣长度暗中加了权重。解决得做标准化或归一化。iris 这种没有极端离群点的数据StandardScaler 和 MinMaxScaler 效果差不多但 StandardScaler 更通用。做完预处理后再看距离分布你会发现 sepal_width 终于能参与投票了分类边界更合理。如果做了标准化仍然效果差再看特征选择比如删掉本来就没区分度的 sepal_width。4.4 坑 4混淆矩阵的类别顺序与标签对齐现象手工打印 confusion_matrix对角线上的数字看起来很奇怪怎么算都对不上 classification_report 里的指标或者换了测试集后行列顺序发生变化你拿着上一轮的结构解读这一轮的结果。原因confusion_matrix 默认按类别字典序排序输出也就是 setosa、versicolor、virginica 这种顺序但如果你的 y_pred 是 ndarray自己手写统计时按出现顺序排列或者你用了 label 编码后顺序变了行和列对不上查准率的分子分母就算错了。解决显式传入 labels 参数固定顺序from sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix( y_test, y_pred, labels[setosa, versicolor, virginica] ) print(cm) print(classification_report( y_test, y_pred, target_names[setosa, versicolor, virginica] ))加了labels后矩阵的行是真实类别、列是预测类别不会再随数据顺序变化。解读混淆矩阵时记得对角线是正确预测非对角线上的数字就是具体混淆方向。iris 场景下最常见的混淆就是 versicolor 被预测成 virginica这和箱式图观察到的重叠现象完全对应。5. 测试集评估混淆矩阵与宏平均的手动验证技巧确定 best_k5 后用全部训练集重新训练一次模型再在测试集上预测生成混淆矩阵和分类报告final_model KNeighborsClassifier(n_neighbors5) final_model.fit(X_train_scaled, y_train) y_pred final_model.predict(X_test_scaled) from sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix(y_test, y_pred, labels[setosa, versicolor, virginica]) print(cm)测试集 30 条里每类 10 条典型的混淆矩阵可能是[[10, 0, 0], [0, 9, 1], [0, 0, 10]]这个样子对角线是正确分类只有 1 个 versicolor 被误判为 virginica。分类报告则直接给出每个类别的查准率、查全率和 F1。这里有个容易忽略的验证技巧宏查准率、宏查全率、宏 F1 就是把每个类别的指标各自做算术平均不按样本数加权。手动算一遍能验证分类报告有没有出错import numpy as np P precision_score(y_test, y_pred, averageNone) # 每个类别的查准率 R recall_score(y_test, y_pred, averageNone) # 每个类别的查全率 F1 f1_score(y_test, y_pred, averageNone) print(宏查准率:, np.mean(P)) print(宏查全率:, np.mean(R)) print(宏F1:, np.mean(F1))对上面的混淆矩阵三个类别的 F1 分别是 1.00、0.95、1.00宏 F1 就是 0.983。宏平均和微平均的区别在类别不平衡时尤其重要iris 三类别各 50 条比较均衡两者差异不大但到了真实业务数据上样本少的类别在宏平均里权重更高更反映小类的表现。从那以后我每做一次 KNN 流程都会强制走一遍完整闭环先画箱式图记住特征重叠再做 8:2 划分并检查 stratify 后的类别分布然后 pipeline 套交叉验证选 K最后在测试集上同时打印混淆矩阵和分类报告并且手动算一遍宏平均验证。这套流程在 iris 上是作业在真实表格数据上是保命的底线希望帮到你。本文还有配套的精品资源点击获取