简介:这份资源围绕鸢尾花数据集的KNN分类实验展开,面向正在学习机器学习基础、需要完成课程作业或入门实战的读者。包内共1个文件,为Python脚本,压缩包约5KB,轻量易读,可直接运行并对照修改。内容覆盖从数据获取到模型评估的完整链路:先用箱式图观察样本分布,再以两种方式做特征预处理,并按80%与20%随机划分训练集和测试集;随后基于5折交叉验证在K=3至9之间选择近邻数,以总体预测错误率为指标绘制K值与错误率的关系曲线;最后在测试集上评价模型,生成混淆矩阵,并计算各类别的查准率、查全率、F1分值以及宏平均指标。已有328人学习,适合希望理解KNN调参流程、掌握分类评估指标计算方式的初学者,也可作为实验报告与代码复现的参考模板。
1. iris_KNN.rar 里到底装了什么:从一份压缩包拆出可复现的 KNN 分类流程
你拿到一个叫iris_KNN.rar的压缩包,解压后大概率看到的是 iris 数据、一份 KNN 脚本、以及按 k 折切分训练/测试集的代码。它解决的不是“KNN 是什么”这种教科书问题,而是“我手上这份 iris 数据,怎么用 KNN 跑出一个能解释、能复现、能改参数的分类结果”。适合两类人:刚接触 knn 算法案例、需要照着交作业或做实验的新手;以及想把 iris 数据集当成基线,快速验证特征工程或距离度量改动的熟手。核心链路只有四步:读数据、切 k 折、训练 KNN、在测试集/预测集上出指标。后面每一章都围绕这条链路展开,把参数、坑和验证方法讲透。
2. iris 数据集与 KNN 的选型理由:为什么它俩总被绑在一起
2.1 iris 数据集的四个特征与三个类别
iris 数据集一共 150 条样本,三个类别各 50 条:setosa、versicolor、virginica。每条样本四个特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位都是厘米。这个数据集的特殊性在于,setosa 和另外两类在花瓣长度上几乎线性可分,而 versicolor 和 virginica 在二维平面上有重叠。KNN 作为基于距离的算法,恰好能把这个重叠区域的决策边界暴露出来——你调 k 值、换距离度量,指标会明显抖动,这正是它适合做教学基线的理由。
很多人第一次跑 iris 会得到 0.95 以上的准确率,然后以为模型没问题。但如果你只做一次随机切分,这个数字没有统计意义。常见做法是固定随机种子,或者直接上 k 折交叉验证。iris_KNN.rar这个命名里带k fold,说明作者至少意识到了单次切分的不可靠。我一般会先把数据加载进来,确认没有缺失值和异常编码,再做后续切分。
from sklearn.datasets import load_iris import numpy as np iris = load_iris() X, y = iris.data, iris.target print("样本数:", X.shape[0], "特征数:", X.shape[1]) print("类别分布:", np.bincount(y)) print("特征范围:", X.min(axis=0), X.max(axis=0))这段代码做三件事:确认样本量和特征维度、检查类别是否均衡、看特征量纲是否接近。iris 四个特征都在厘米级别,量纲一致,所以不做标准化也能跑。但如果你后面换成其他数据集,量纲差异会直接毁掉欧氏距离的公平性,这是 KNN 最容易被忽略的前提。
2.2 KNN 在 iris 上的决策逻辑与 k 值影响
KNN 的预测过程很直白:对新样本计算它到所有训练样本的距离,取最近的 k 个,按多数投票决定类别。k=1 时,决策边界极度贴合训练点,训练集准确率 100%,但测试集容易受噪声影响;k 增大,边界变平滑,偏差上升、方差下降。iris 只有 150 条,k 取太大比如 50,会把整个类别的先验比例带进来,导致少数类被淹没。
我一般先在 1 到 20 之间扫一遍 k,看交叉验证均值和标准差的变化曲线。如果 k=3 到 k=7 之间指标平稳,就选中间值;如果某个 k 突然掉下去,通常是某个折里的局部样本分布被过度放大。iris_KNN.rar里如果只写了一个固定 k,建议你至少补一个循环验证,否则换一批测试集结论就可能翻车。
from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import cross_val_score for k in range(1, 21): knn = KNeighborsClassifier(n_neighbors=k) scores = cross_val_score(knn, X, y, cv=5, scoring='accuracy') print(f"k={k:2d} 均值={scores.mean():.4f} 标准差={scores.std():.4f}")cross_val_score的cv=5表示 5 折交叉验证,scoring='accuracy'是分类准确率。输出里重点看均值和标准差的比值:均值高且标准差小,说明这个 k 稳定;均值高但标准差大,说明结果依赖切分方式,需要固定随机种子或增加折数。这段代码不涉及训练集/测试集的手动切分,适合先摸清 k 的大致范围。
3. 把 iris 切成 k 折:训练集、测试集与预测集的边界
3.1 k 折交叉验证的切分逻辑与代码实现
k 折交叉验证把数据分成 k 份,每次取其中一份做验证,其余 k-1 份做训练,循环 k 次后取平均。它的价值在于:每个样本都当过一次验证数据,指标不再依赖某一次随机切分。iris_KNN.rar标题里的k fold和测试集预测集其实指向两个不同阶段:交叉验证用于选参数,最终评估需要留出一个从未参与训练的测试集。
我通常的做法是先用分层 k 折选 k 值和距离度量,再单独留 20% 做最终测试。分层的意思是每个折里三类样本比例保持一致,避免某一折全是 setosa。下面这段代码同时展示分层切分和交叉验证的衔接方式。
from sklearn.model_selection import StratifiedKFold, train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report # 先留出最终测试集,stratify 保证类别比例 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 在训练集内部做分层 k 折 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) knn = KNeighborsClassifier(n_neighbors=5) fold_scores = [] for train_idx, val_idx in skf.split(X_train, y_train): knn.fit(X_train[train_idx], y_train[train_idx]) pred = knn.predict(X_train[val_idx]) fold_scores.append(accuracy_score(y_train[val_idx], pred)) print("各折准确率:", [f"{s:.4f}" for s in fold_scores]) print("平均准确率:", np.mean(fold_scores))test_size=0.2表示测试集占 20%,random_state=42保证每次切分结果一致,stratify=y是分层的关键。StratifiedKFold的shuffle=True会在切分前打乱顺序,避免原始数据按类别排序导致折内分布偏移。循环里每次重新fit,确保验证折没有参与训练。最终测试集只在最后用一次,不能拿它反复调参,否则测试集就变成了验证集。
3.2 测试集预测与预测集输出的区别
测试集是有标签的,用来算准确率、召回率、混淆矩阵;预测集通常指没有标签、只输出预测类别的数据。iris_KNN.rar标题里把“测试集预测集”并列,说明作者可能既做了评估,也做了新样本预测。这两步的代码结构不同:评估需要y_true和y_pred对比,预测只需要predict或predict_proba输出结果。
我一般会在最终评估后,把模型保存下来,再对无标签数据做批量预测。如果预测集的特征顺序和训练集不一致,KNN 会直接给出错误结果,而且不会报错。这是血泪经验:特征列顺序错位是静默失败,指标看起来正常,实际全错。
# 最终评估 knn_final = KNeighborsClassifier(n_neighbors=5) knn_final.fit(X_train, y_train) y_pred = knn_final.predict(X_test) print("测试集准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=iris.target_names)) # 模拟无标签预测集,必须保持特征顺序一致 X_new = X_test[:5] y_new = knn_final.predict(X_new) print("预测集输出:", iris.target_names[y_new])classification_report会输出每个类别的精确率、召回率和 F1,比单一准确率更能暴露类别不平衡问题。X_new这里直接取测试集前五条做演示,实际使用时要用X_new.columns或特征名列表和训练集对齐。如果预测集有缺失值,KNN 无法处理,需要先填充或删除。
4. 避坑与排查:iris KNN 跑不通时先看这五条
4.1 现象:准确率异常高或异常低
原因:异常高通常是数据泄露,比如测试集参与了训练,或者用全部数据做了交叉验证后再报告测试集准确率;异常低通常是特征顺序错位、标签编码错误,或者 k 值取到了类别数量的整数倍导致投票平局。解决:检查fit和predict的数据来源,确认测试集从未进入训练流程;打印X_train.shape和X_test.shape,确认特征列顺序一致;k 值避免取 3 的倍数(iris 三类),平局时 sklearn 会选索引较小的类别。
4.2 现象:交叉验证标准差很大
原因:折数太少,或者没有分层,某一折里某个类别样本极少。iris 只有 150 条,5 折时每折 30 条,分层后每类 10 条,已经比较紧。如果shuffle=False且原始数据按类别排序,第一折可能全是 setosa。解决:用StratifiedKFold并开启shuffle=True,固定random_state;如果标准差仍然大,增加到 10 折,但注意每折验证样本变少,指标波动本身会变大。
4.3 现象:预测集输出全是同一类
原因:预测集特征量纲和训练集不一致,比如训练集是厘米,预测集是毫米;或者预测集特征列顺序被打乱;或者 k 值过大,多数投票被先验类别主导。解决:打印训练集和预测集的前几行做对比;用sklearn.preprocessing.StandardScaler在训练集上拟合后分别转换训练集和预测集;k 值从 3 到 7 重新扫一遍。
4.4 现象:换距离度量后结果剧烈变化
原因:iris 特征量纲一致,欧氏距离和曼哈顿距离差异不大。如果换成马氏距离或余弦距离,需要检查数据是否适合。余弦距离关注方向而非绝对距离,iris 的四个特征都是正数,余弦距离会压缩差异。解决:默认用欧氏距离;如果要做距离度量对比,固定 k 值和切分方式,只改metric参数,观察交叉验证均值变化。
4.5 现象:模型保存后重新加载预测结果不一致
原因:保存模型时没有保存训练数据的特征顺序和类别编码映射;或者用了pickle但 sklearn 版本不一致。解决:保存模型时同时保存iris.target_names和特征名列表;加载后先用训练集的一条样本验证预测结果是否和保存前一致;跨版本部署时固定 sklearn 版本,或者用joblib替代pickle。
5. 把 KNN 从 iris 推到可用:距离权重、概率输出与阈值调整
5.1 距离加权投票与概率校准
默认 KNN 是等权投票,近邻不管多近都算一票。weights='distance'让距离越近的邻居权重越大,通常能提升边界样本的预测稳定性。在 iris 上,versicolor 和 virginica 的重叠区域会受益。但距离加权对异常值更敏感,如果某个训练点标错,它的权重会被放大。我一般先跑等权,再跑距离加权,对比交叉验证均值和标准差,如果距离加权没有明显提升,就保持等权,少一个参数少一个坑。
from sklearn.neighbors import KNeighborsClassifier for weights in ['uniform', 'distance']: knn = KNeighborsClassifier(n_neighbors=5, weights=weights) scores = cross_val_score(knn, X, y, cv=5, scoring='accuracy') print(f"weights={weights:8s} 均值={scores.mean():.4f} 标准差={scores.std():.4f}")weights='uniform'是等权,weights='distance'是距离倒数加权。输出里如果两者均值差在 0.01 以内,选等权;如果距离加权均值高且标准差没有变大,可以选距离加权。predict_proba在距离加权下输出的概率更平滑,适合需要置信度的场景。
5.2 用预测概率做阈值调整
predict_proba返回每个类别的概率,默认取最大概率的类别。如果业务上对某一类的召回率要求更高,可以手动调阈值。比如把 virginica 的预测阈值从 0.5 降到 0.4,让更多样本被判为 virginica,提高召回但降低精确率。iris 三类均衡,阈值调整空间不大,但这个技巧在真实数据里很实用。
knn = KNeighborsClassifier(n_neighbors=5, weights='distance') knn.fit(X_train, y_train) proba = knn.predict_proba(X_test) # 默认取最大概率类别 y_pred_default = knn.classes_[np.argmax(proba, axis=1)] # 手动调整:如果 virginica 概率超过 0.4 就判为 virginica y_pred_adjusted = y_pred_default.copy() virginica_idx = list(knn.classes_).index(2) mask = proba[:, virginica_idx] > 0.4 y_pred_adjusted[mask] = 2 print("默认准确率:", accuracy_score(y_test, y_pred_default)) print("调整后准确率:", accuracy_score(y_test, y_pred_adjusted))knn.classes_是类别编码顺序,np.argmax取每行最大概率的索引。调整阈值后准确率可能下降,但 virginica 的召回率会上升。实际项目里要根据业务代价决定阈值,不能只看准确率。这段代码在 iris 上只是演示,真实数据里需要画精确率-召回率曲线来选阈值。
5.3 一个我常用来验证模型是否真的学会了的技巧
把训练集和测试集的准确率放在一起看。如果训练集 100%、测试集 95%,说明 k 太小,模型记住了训练点;如果训练集 96%、测试集 94%,说明模型泛化正常;如果训练集 90%、测试集 89%,说明 k 太大或特征区分度不够。iris 上 k=1 时训练集必然 100%,所以不要用训练集准确率来选 k。我习惯在交叉验证循环里同时记录训练折和验证折的准确率,差值超过 5 个百分点就警惕过拟合。
最后一句话是我自己的习惯:每次跑完 KNN,先把random_state、k 值、weights、折数四个参数写进实验记录,再去看指标。少了任何一个,下周复现时就会多花半小时找原因。希望帮到你。
本文还有配套的精品资源,点击获取