KNN鸢尾花分类实战:从数据加载到参数调优的完整拆解
发布时间:2026/10/14 0:04:44 锦皓数字建站

1. 为什么鸢尾花数据集是KNN入门的黄金标尺鸢尾花数据集在机器学习圈里的地位大概相当于编程语言里的Hello World。很多人第一次接触分类算法拿到的就是这个数据集。但有意思的是我见过不少朋友跑完一遍代码、看到准确率数字就关掉笔记本然后跟别人说KNN我学过了。等到真正拿到一份业务数据面对缺失值、量纲差异、类别不平衡的时候整个人就懵了。问题出在哪儿出在把鸢尾花数据集当成了一个跑通就行的练习题而没有把它当成一个拆解算法每个环节的实验台。KNN算法看起来简单到不能再简单——找最近的K个邻居投票决定类别。但这里面藏着几个非常关键的工程问题K怎么选距离怎么算特征要不要归一化训练集和测试集怎么分这些问题在鸢尾花数据集上都能得到直观的验证而且因为数据量小、维度低你可以快速做几十组对比实验把每个参数的影响都摸清楚。这篇文章就是围绕用KNN做鸢尾花分类这件事把整个流程从数据加载到模型评估到参数调优完整地拆一遍。我会重点讲清楚每一步背后的逻辑以及在实际操作中容易踩的坑。适合刚入门机器学习的同学也适合已经跑过一遍但没深究细节的朋友。你不需要有很深的数学基础但需要有一点Python编程经验知道列表、循环、函数这些基本概念就够了。2. 鸢尾花数据集的结构与KNN的匹配逻辑2.1 四个特征、三个类别、一百五十个样本鸢尾花数据集的全称是Iris Dataset最早由统计学家R.A. Fisher在1936年整理发布。数据集里包含三种鸢尾花山鸢尾Setosa、变色鸢尾Versicolor、维吉尼亚鸢尾Virginica。每个样本有四个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。每种花各有50个样本总共150条记录。这个数据集之所以适合KNN入门有几个原因。第一特征维度低只有4维你可以用散点图矩阵直观地看到类别之间的分布关系。第二类别数量少只有3类而且其中山鸢尾和另外两种在特征空间里几乎是线性可分的——你甚至不需要跑算法光看花瓣长度的分布就能把山鸢尾挑出来。第三样本量小150条数据在普通笔记本上跑几百次交叉验证也就几秒钟的事非常适合做参数对比实验。但这里有个容易被忽略的点变色鸢尾和维吉尼亚鸢尾在特征空间里有重叠区域。如果你只用花瓣长度和花瓣宽度两个特征这两个类别的边界会非常模糊。这就是为什么在实际操作中特征选择本身就是一个值得研究的环节。KNN算法对特征的选择非常敏感因为它是基于距离度量的——你放进去一个跟类别无关的特征相当于在距离计算里加了一个噪声维度会直接拉低分类效果。2.2 KNN的懒惰学习到底懒在哪里KNN属于懒惰学习Lazy Learning算法。这个懒不是说它效果差而是说它在训练阶段几乎什么都不做——它只是把训练样本原封不动地存下来。真正的计算发生在预测阶段来一个新样本算法计算它到所有训练样本的距离找出最近的K个然后看这K个邻居里哪个类别最多就把新样本判为那个类别。这种机制带来一个很直观的好处模型的可解释性强。你拿到一个预测结果可以回溯到具体是哪几个邻居投了票甚至可以把这几个邻居的样本调出来看看。这在一些需要解释决策依据的场景里非常有价值。但代价也很明显预测阶段的计算量大。如果训练集有几十万条数据每预测一个样本都要算几十万次距离这在实时性要求高的场景里是不可接受的。所以在实际工程中KNN通常用在训练集不大、但对可解释性有要求的场景。比如推荐系统里的相似用户匹配、异常检测里的离群点识别或者作为更复杂模型的基准线。鸢尾花数据集只有150条计算量完全不是问题正好可以让你把精力集中在算法逻辑本身。2.3 距离度量欧氏距离不是唯一选择KNN的核心是找最近的邻居那最近怎么定义最常用的是欧氏距离也就是两点之间的直线距离。对于两个n维向量x和y欧氏距离的计算公式是各维度差值的平方和再开根号。但在实际应用中欧氏距离并不总是最优选择。举个例子如果你的特征里有年龄和年收入这两个维度年龄的范围是0到100年收入的范围是0到1000000。直接算欧氏距离的话年收入的差异会完全主导距离计算年龄的影响几乎可以忽略。这就是为什么在做KNN之前通常需要对特征做标准化处理。除了欧氏距离还有曼哈顿距离各维度差值绝对值之和、切比雪夫距离各维度差值绝对值的最大值、余弦相似度等。曼哈顿距离在高维空间里有时比欧氏距离更稳健因为它不会因为某一个维度的巨大差异而被过度放大。余弦相似度则更关注向量的方向而不是绝对距离在文本分类里用得比较多。在鸢尾花数据集上因为四个特征都是厘米单位量纲一致所以直接用欧氏距离问题不大。但如果你想养成好习惯建议还是先做标准化然后用欧氏距离和曼哈顿距离各跑一遍看看效果差异。这个对比实验能帮你建立对距离度量的直观感受。3. 从零实现KNN不调库的完整代码拆解3.1 数据加载与探索性分析虽然scikit-learn自带鸢尾花数据集但我建议你第一次实现的时候先从CSV文件读起。这样做的好处是你需要自己处理列名、类别标签编码、数据类型转换这些问题更接近真实项目的数据处理流程。假设你已经把数据存成了一个CSV文件每行是一条样本前四列是特征最后一列是类别标签。用pandas读取之后第一件事是检查数据的基本情况有没有缺失值、每个特征的分布范围、类别是否平衡。import pandas as pd import numpy as np # 读取数据 df pd.read_csv(iris.csv) print(df.head()) print(df.describe()) print(df[species].value_counts())describe()会给你每个特征的均值、标准差、最小值、最大值和四分位数。这一步很关键因为你能直观看到四个特征的量纲是否一致。在鸢尾花数据集里花萼长度的范围大约是4.3到7.9花瓣长度是1.0到6.9量纲确实一致但分布范围有差异。花瓣长度的标准差比花萼宽度大不少这意味着花瓣长度在距离计算中的贡献会更大。接下来做散点图矩阵看看特征之间的两两关系。如果你用matplotlib或者seaborn可以很快画出来。重点观察的是哪些特征组合能把三个类别分开通常花瓣长度和花瓣宽度的组合效果最好山鸢尾在这两个特征上和其他两类完全分离。3.2 手写距离计算与投票逻辑不调库实现KNN核心就是两个函数一个算距离一个做投票。先看距离计算。def euclidean_distance(x1, x2): return np.sqrt(np.sum((x1 - x2) ** 2))这个函数接受两个numpy数组返回它们的欧氏距离。注意这里用了numpy的向量化操作比用for循环逐维度计算快得多。如果你处理的是大规模数据这个差异会非常明显。然后是预测函数。对于一个新的测试样本计算它到所有训练样本的距离按距离排序取前K个然后看这K个里哪个类别最多。def predict(X_train, y_train, x_test, k): distances [euclidean_distance(x_test, x_train) for x_train in X_train] k_indices np.argsort(distances)[:k] k_labels [y_train[i] for i in k_indices] # 投票 from collections import Counter most_common Counter(k_labels).most_common(1) return most_common[0][0]这里有几个细节值得注意。np.argsort返回的是排序后的索引不是排序后的值这个很容易搞混。Counter的most_common(1)返回一个列表里面是元组所以要取[0][0]才能拿到类别标签。还有一个隐藏问题如果K是偶数而且两个类别的票数相同怎么办Counter的most_common在这种情况下会返回先出现的那个类别这其实是有偏的。更合理的做法是设定一个规则比如距离更近的那个邻居的类别优先或者干脆把K设为奇数。在实际操作中我通常直接把K设为奇数省去这个麻烦。3.3 训练集与测试集的划分策略在鸢尾花数据集上常见的划分比例是70%训练、30%测试或者80%训练、20%测试。但这里有个关键点划分必须是随机的而且要保证每个类别的样本在训练集和测试集里的比例大致相当。这就是分层抽样Stratified Sampling的概念。为什么要分层假设你随机划分恰好测试集里山鸢尾特别多而训练集里山鸢尾特别少那模型对山鸢尾的学习就不充分测试结果会偏低。虽然鸢尾花数据集只有150条随机划分出现极端情况的概率不大但养成分层抽样的习惯对处理真实数据非常重要。from sklearn.model_selection import train_test_split X df.iloc[:, :4].values y df[species].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy )stratifyy就是告诉函数按照y的类别比例来做分层。random_state42是为了让结果可复现——你每次运行都得到同样的划分方便对比不同参数的效果。3.4 准确率计算与混淆矩阵解读预测完之后最直接的评估指标是准确率预测正确的样本数除以总样本数。但准确率有个问题如果类别不平衡准确率会误导你。比如99%的样本都是A类你全预测成A类准确率也有99%但模型其实什么都没学到。鸢尾花数据集三个类别各50条是平衡的所以准确率是可靠的。但为了更全面地评估建议同时看混淆矩阵。混淆矩阵是一个3x3的表格行是真实类别列是预测类别对角线上的数字是正确分类的数量非对角线是错误分类的数量。from sklearn.metrics import confusion_matrix, classification_report y_pred [predict(X_train, y_train, x, k3) for x in X_test] print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))classification_report会给你每个类别的精确率、召回率和F1分数。在鸢尾花数据集上你通常会看到山鸢尾的F1是1.0另外两种在0.9到0.95之间。这个差异本身就说明了类别之间的可分性不同。4. K值选择与特征标准化两个决定成败的细节4.1 K值从1到20的实测曲线K值是KNN里最重要的超参数。K太小模型对噪声敏感容易过拟合K太大模型过于平滑容易欠拟合。在鸢尾花数据集上我建议你做一个完整的K值扫描实验K从1到20每个K值跑10次不同的随机划分取平均准确率。k_values range(1, 21) accuracies [] for k in k_values: acc_list [] for seed in range(10): X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_stateseed, stratifyy ) y_pred [predict(X_train, y_train, x, kk) for x in X_test] acc np.mean(y_pred y_test) acc_list.append(acc) accuracies.append(np.mean(acc_list))跑完之后画一条曲线你会看到准确率在K1时可能就不低但随着K增大先上升后下降在K3到K7之间达到峰值。这个峰值的位置和数据的噪声水平有关。鸢尾花数据集比较干净所以K3或K5通常就很好。但这里有个经验不要只看平均准确率还要看标准差。如果某个K值的平均准确率很高但标准差也很大说明这个K值对数据划分很敏感泛化能力可能不稳定。我通常选平均准确率高且标准差小的K值。4.2 标准化前后的效果对比前面提到过KNN对特征量纲敏感。鸢尾花数据集的四个特征虽然都是厘米但分布范围不同。花瓣长度的标准差大约是1.76花萼宽度大约是0.44差了4倍。这意味着在欧氏距离计算中花瓣长度的贡献远大于花萼宽度。标准化有两种常见方式Min-Max归一化和Z-Score标准化。Min-Max把特征缩放到0到1之间Z-Score把特征变成均值为0、标准差为1的分布。对于KNNZ-Score更常用因为它保留了特征的分布形状对异常值也更稳健。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X)注意这里有个坑fit_transform只能在训练集上做测试集必须用训练集的均值和标准差来transform。如果你对整个数据集做fit_transform测试集的统计信息就泄露到了训练过程中这叫数据泄露会导致评估结果偏乐观。在鸢尾花数据集上标准化前后的准确率差异可能只有1到2个百分点因为特征量纲本来就接近。但如果你换成真实数据比如同时有年龄和收入两个特征不做标准化的KNN基本不可用。所以这个习惯必须从一开始就养成。4.3 距离度量的选择实验除了欧氏距离你可以试试曼哈顿距离。曼哈顿距离的计算是各维度差值绝对值之和对异常值更不敏感。def manhattan_distance(x1, x2): return np.sum(np.abs(x1 - x2))在鸢尾花数据集上两种距离的准确率差异通常很小可能就零点几个百分点。但在高维数据上曼哈顿距离有时会明显优于欧氏距离。原因是高维空间里欧氏距离的最近邻和最远邻的差异会变得很小这叫维度灾难。曼哈顿距离因为不做平方对每个维度的差异保留得更完整所以在高维场景下更稳健。你可以做一个简单的对比实验分别用两种距离跑K3的KNN看准确率和混淆矩阵的差异。这个实验花不了几分钟但能帮你建立对距离度量的直观认知。5. 交叉验证与超参数调优的实操路径5.1 为什么单次划分不可靠用train_test_split做一次划分得到一个准确率这个结果有很大的随机性。换一个random_state准确率可能波动好几个百分点。在鸢尾花数据集上因为样本量小这种波动更明显。所以单次划分的结果只能作为参考不能作为最终结论。更可靠的做法是K折交叉验证。把数据集分成K份通常是5份或10份每次用其中一份做测试其余做训练重复K次取平均准确率。这样每个样本都有机会被用作测试评估结果更稳定。from sklearn.model_selection import cross_val_score from sklearn.neighbors import KNeighborsClassifier knn KNeighborsClassifier(n_neighbors3) scores cross_val_score(knn, X_scaled, y, cv5) print(f平均准确率: {scores.mean():.4f}, 标准差: {scores.std():.4f})5折交叉验证在鸢尾花数据集上跑一次大概几秒钟你可以放心地做多组参数对比。5.2 网格搜索找最优参数组合KNN的主要超参数有两个K值和距离度量。你可以用网格搜索Grid Search来系统地遍历这些组合找到最优的。from sklearn.model_selection import GridSearchCV param_grid { n_neighbors: range(1, 21), metric: [euclidean, manhattan] } grid GridSearchCV(KNeighborsClassifier(), param_grid, cv5, scoringaccuracy) grid.fit(X_scaled, y) print(f最优参数: {grid.best_params_}) print(f最优准确率: {grid.best_score_:.4f})网格搜索会遍历所有参数组合对每个组合做5折交叉验证最后返回平均准确率最高的组合。在鸢尾花数据集上最优的K值通常在3到7之间距离度量选欧氏或曼哈顿差异不大。但这里有个经验网格搜索的结果依赖于你设定的参数范围。如果你把K的范围设成1到5那最优值肯定在这个区间里但可能真正的全局最优在7。所以参数范围要设得足够宽先粗调再细调。5.3 学习曲线与过拟合判断学习曲线是判断模型是否过拟合或欠拟合的好工具。它画的是训练集准确率和验证集准确率随训练样本量变化的曲线。from sklearn.model_selection import learning_curve import matplotlib.pyplot as plt train_sizes, train_scores, val_scores learning_curve( KNeighborsClassifier(n_neighbors3), X_scaled, y, cv5, train_sizesnp.linspace(0.1, 1.0, 10) ) plt.plot(train_sizes, train_scores.mean(axis1), label训练集) plt.plot(train_sizes, val_scores.mean(axis1), label验证集) plt.xlabel(训练样本量) plt.ylabel(准确率) plt.legend() plt.show()如果训练集准确率很高但验证集准确率很低说明过拟合。如果两者都很低说明欠拟合。在鸢尾花数据集上K3的KNN通常训练集和验证集准确率都很高差距很小说明模型复杂度合适。但如果把K设成1训练集准确率会是100%验证集可能只有95%左右这就是过拟合的典型表现。K1的模型把每个训练样本都当成一个独立的邻居对噪声极其敏感。6. 实际项目中KNN的局限与应对策略6.1 计算复杂度与降维思路KNN最大的工程问题是预测阶段的计算量。训练集有N个样本预测一个样本需要算N次距离。如果N是百万级单次预测就要算百万次距离这在实时系统里是不可接受的。应对策略有几个方向。第一是降维用PCA或者特征选择把维度从几十维降到几维距离计算会快很多。第二是近似最近邻搜索用KD树或者Ball Tree这样的数据结构来加速。scikit-learn的KNeighborsClassifier默认会根据数据自动选择算法你可以通过algorithm参数手动指定。knn KNeighborsClassifier(n_neighbors3, algorithmkd_tree)KD树在低维数据上效果很好但在高维数据上会退化到暴力搜索。一般来说维度超过20维KD树的优势就不明显了。6.2 类别不平衡与距离加权如果数据集的类别不平衡比如A类有1000个样本B类只有10个KNN的投票会被A类主导。因为K个邻居里大概率大部分都是A类B类的样本即使离得很近也可能因为数量少而投不过。应对方法是给距离加权。不是简单数票数而是按距离的倒数加权距离越近的邻居权重越大。knn KNeighborsClassifier(n_neighbors5, weightsdistance)weightsdistance会让距离近的邻居有更大的投票权。在类别不平衡的场景下这个设置通常能明显提升少数类的召回率。6.3 特征选择不是越多越好KNN对无关特征非常敏感。你放进去一个跟类别无关的特征相当于在距离计算里加了一个随机噪声维度会拉低所有样本之间的区分度。在鸢尾花数据集上四个特征都有用所以问题不大。但在真实数据里特征选择是KNN调优的重要环节。一个简单的做法是计算每个特征和类别标签的相关性把相关性低的特征去掉。或者用递归特征消除RFE来系统地筛选特征。在鸢尾花数据集上你可以试试只用花瓣长度和花瓣宽度两个特征看看准确率会不会下降。如果下降不明显说明花萼特征确实贡献不大。7. 我在KNN实操中踩过的几个坑第一个坑是忘了做标准化。有一次处理一份包含年龄和收入的数据直接跑KNN准确率只有60%多。后来发现收入的数值范围是年龄的几千倍距离计算完全被收入主导。做了Z-Score标准化之后准确率直接跳到85%以上。这个教训让我养成了拿到数据先看量纲的习惯。第二个坑是K值设成了偶数。有一次K4结果两个类别的票数2比2Counter返回了先出现的那个类别导致预测结果有系统性偏差。后来我把K固定设为奇数或者用weightsdistance来打破平局。第三个坑是在整个数据集上做了标准化。当时图省事用fit_transform处理了全部数据然后才划分训练集和测试集。结果测试集的均值和标准差信息泄露到了训练过程中评估结果偏乐观。正确的做法是先划分再在训练集上fit然后transform测试集。第四个坑是忽略了类别分布。有一次处理一份不平衡数据直接用KNN多数类的召回率很高但少数类几乎全错。后来加了weightsdistance少数类的召回率从不到10%提升到了60%多。这些坑在鸢尾花数据集上可能不会全部遇到因为数据太干净了。但如果你把KNN用到真实项目里这些坑一个都跑不掉。所以我的建议是在鸢尾花数据集上把每个环节都拆开做一遍实验把参数的影响摸清楚然后再去处理真实数据。这样你踩坑的时候至少知道问题出在哪儿。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。