鸢尾花数据集与KNN分类:从标准化到交叉验证的完整实战解析

发布时间:2026/10/13 3:39:33
鸢尾花数据集与KNN分类:从标准化到交叉验证的完整实战解析 学机器学习的人几乎都会在某个阶段遇到这个经典组合鸢尾花数据集配上KNN算法。数据量小、特征清晰、类别明确KNN的直觉又好懂所以在很多教程里它都是“第一个分类项目”。但我也见过不少朋友在跑通示例之后其实是带着疑问离开的为什么要做标准化K值为什么不取1测试集准确率到底该看哪个数字以及如果我想拿去写报告或做演示该怎么把“模型训练”这个过程讲得完整、可信?这篇博文就把这条线完整走一遍。我会从数据本身出发把KNN的原理拆成三个真正需要做决定的点然后用完整的Python代码实现标准化、训练、评估和调参最后分享一些实际运行中容易踩但教程很少提的坑。不管你是刚入门、准备面试还是想把第一个项目沉淀成一篇博客这篇内容都可以直接参考。1. 鸢尾花数据集的四维特征与三类样本为什么它是最适合入门的分类实验1.1 数据长什么样150条记录四个测量值三个品种鸢尾花数据集来自上世纪30年代的一篇经典统计学论文后来被反复用于教学演示。数据本身不复杂总共150条样本每个品种恰好50条每个样本包含四个连续型数值特征没有缺失值也不需要做文本处理。四个特征分别是花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。以我实际读入后看到的边界值为例特征最小范围最大范围分布特点花萼长度4.37.9中等离散三类重叠明显花萼宽度2.04.4数值区间窄区分能力一般花瓣长度1.06.9区分三类最有效的特征之一花瓣宽度0.12.5区间最小但边界分明目标标签是三个品种山鸢尾、变色鸢尾、维吉尼亚鸢尾。也就是说我们要做的事情是给出一组新的四维测量数据判断它属于哪一种花。1.2 为什么说这个数据集“听话”新手可能会觉得数据集小、结果好是不是因为KNN算法“太强了”其实不是。鸢尾花数据集真正适合入门是因为它具备几个难得的条件。第一类别完全均衡。每类50条训练时不会出现“多数类压倒少数类”的问题。你不需要从一开始就纠结类别权重、上采样、下采样这些进阶操作可以专心理解算法本身。第二特征全是连续数值天生和距离计算匹配。KNN的核心是“算距离”而距离计算需要数值型特征。如果换成性别、颜色这种离散标签KNN还要先编码处理起来就不会这么顺手。第三三个类别的分布不是完全混在一起的。山鸢尾和另外两类之间有一个非常清晰的间隔几乎线性可分变色鸢尾和维吉尼亚鸢尾有重叠但用花瓣长度和花瓣宽度两个特征也能画出比较明显的分界。这种“部分好分、部分有难度”的格局非常适合观察算法在不同位置的表现——你能清楚看到什么样本被分对了什么样本被搞混了。1.3 量纲差异容易被忽略但必须处理的隐患这四个特征虽然都叫“长度/宽度”单位也相同但数值范围并不一样。如果你在sklearn里直接打印一下数据的describe会发现花萼宽度的标准差很小而花瓣长度和花萼长度的波动明显更大。KNN在计算欧氏距离时距离的平方项里数值更大的特征天然会“抢走”更多贡献。打个比方这就好比你想用“身高”和“体重”两个指标判断一个人常不常运动身高的单位是米体重的单位如果是千克两者数值差距一大最终算出来的距离几乎全由体重决定身高这个信息就浪费了。鸢尾花虽然单位一致但范围差异同样会扭曲距离的贡献比例。所以后面有一件必须做的事叫标准化这一步不是可选项而是KNN能够稳定发挥的前提。很多新手在这一步省掉得过且过最后发现结果也确实还行——那是因为鸢尾花本身结构太清晰掩盖了量纲问题。等你换到真实项目里同样的偷懒就会让准确率明显下滑。2. KNN背后真正需要想清楚的三个参数K值、距离度量和投票规则很多教程把KNN说成“物以类聚近朱者赤”这句话没错但也正是因为太顺口让很多人忽略了KNN其实是需要做三个明确技术决策的算法。这三个决策直接决定了模型的长相。2.1 第一个决策K值选多大KNN的全称是K-Nearest Neighbors核心思路一个未知样本的类别由离它最近的K个已知样本投票决定。K的取值直接影响决策边界。如果K1模型只看最近的一个邻居。后果是决策边界非常“碎”一个异常点就能在它周边划出一小片区域。训练集上可能接近满分但遇到新数据就容易被噪声带着跑这是典型的过拟合。如果K特别大比如K140几乎把全部150条样本都卷进来投票那最终结果就变成一个“全局多数类预测”——无论新样本长什么样大概率都归为样本最多的那类。决策边界退化成一条几乎平的划分线这是欠拟合。实际中K取多少没有一个万能答案。常见做法是先设一个相对中性的初值比如5跑通基线之后再用交叉验证去扫描。鸢尾花这类小数据集我测下来的经验是K在5到11之间通常表现不错但别照搬换一个数据集就要重新扫。2.2 第二个决策距离度量怎么选KNN所谓的“最近邻居”必须有一个具体的距离定义。最常用的是欧氏距离也就是初中几何里的直线距离。在二维平面上它很好理解但在四维特征空间里它一样生效——把每个特征差距平方后相加再开根号。曼哈顿距离也是可选方案它计算的是各个维度差的绝对值之和在特征维度较高时曼哈顿距离有时比欧氏距离更稳健。选哪个的关键在于你的特征空间里直线距离是否有意义。鸢尾花的四个特征都是连续测量值特征之间没有明显的相关性结构用欧氏距离是合理的。如果你想省事也可以把两种距离都放进网格搜索里让数据替你做决定。2.3 第三个决策投票规则要不要加权最朴素的投票是每个邻居一票不管远近票数相同。还有一种做法是距离越近的样本权重越大这叫距离加权投票。加权的好处是当一个近邻和一群比较远的邻居意见冲突时近的那个声音更大。在鸢尾花数据上加权投票的影响没有想象中那么大因为样本密度比较均匀距离差异不像真实业务数据那么悬殊。但如果你以后处理流失用户预测、欺诈检测这类类别混杂严重的数据距离加权往往能带来肉眼可见的提升。2.4 为什么说KNN是“懒惰学习”KNN属于“懒惰学习”的代表算法。所谓懒惰不是说它计算偷懒而是它把工作全部推迟到了预测阶段。训练时它只做一件事把训练数据“背下来”不学任何权重也不拟合任何函数。真正开始干活是在你给它一个测试样本时它才当场计算这个样本和所有训练样本的距离再找出最近的K个邻居。这个特性可以类比成一个考试前完全不复习、但带着满是笔记的书进考场的人他不需要考前花时间背诵但每次答题都需要现场翻书找答案。如果他翻书速度够快也能考出好成绩可是只要书的页数变多每次翻书的时间就会直线上升。这也是KNN在后期的真实痛点——训练几乎零成本预测成本跟样本量成正比。3. 模型训练的完整路径从读取数据到读懂混淆矩阵概念聊清楚了该动手了。下面这条路径是我在实际项目中一直在用的步骤顺序每一步都有存在的理由不是单纯拼代码。3.1 第一步读取数据先做快速检查用sklearn自带的鸢尾花数据代码非常少。拿到数据后我的习惯是马上打印几样东西数据维度、标签分布、特征有没有空值。from sklearn.datasets import load_iris iris load_iris() X iris.data # 150行4列的二维数组 y iris.target # 150个0/1/2标签 print(X.shape) # (150, 4) print(y.shape) # (150,) print(pd.Series(y).value_counts()) # 0 50 # 1 50 # 2 50不要觉得这几行多余。我见过不止一次有人从外部CSV读数据没检查就直接丢进模型跑出来的准确率异常高自己还很得意结果发现是标签列被当成了特征。先看一眼数据形状和分布能把这类低级失误挡在门外。3.2 第二步划分训练集和测试集而不是自己手动切数据划分对KNN尤其重要因为KNN在预测时要和所有训练样本做距离计算如果训练集里类别分布失衡整个模型会偏向多数类。sklearn的train_test_split支持stratify参数可以按类别比例分层抽样。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) print(X_train.shape) # (120, 4) print(X_test.shape) # (30, 4)test_size取0.2也就是120条训练、30条测试。鸢尾花总共就150条测试集再大就会挤压训练数据太小则评估波动大0.2在同类小数据集里是比较常见的折中。stratifyy保证三类的训练测试划分比例接近1:1:1不会出现某类全落到测试集的情况。random_state固定为42意思是每次运行时随机划分结果完全一致。这一步的价值是复现性。你今天跑出准确率0.9667明天再跑还是0.9667写报告、改代码、对比实验都有据可查。不设这个参数的话每次结果都不一样你根本分不清模型改进是因为参数调优还是因为切分运气好。3.3 第三步标准化必须在划分之后操作这一步是KNN实践里最容易做错、但也最适合讲清原理的一步。我先给出正确的代码再解释为什么顺序这么重要。from sklearn.preprocessing import StandardScaler scaler StandardScaler() scaler.fit(X_train) # 只在训练集上计算均值和标准差 X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test)标准化的本质是对每个特征做一次“平移和缩放”减去均值再除以标准差让每个特征的分布调整到均值接近0、标准差接近1。这样四个特征在距离计算中才能“平等对话”。顺序上只能用X_train去fit也就是让训练集的均值和标准差决定缩放尺度然后再用这个已经拟合好的scaler去transform测试集。一定不要对全部数据直接fit更不要把测试集也拿进来fit两次。原因在于如果scaler利用到了测试集的信息相当于你在预测之前就偷偷看了答案评估出来的准确率会偏高这叫数据泄漏。虽然鸢尾花数据泄漏的影响看起来不大但同样的错误换到真实数据集上水分可以大到让你误以为模型已经达到部署水平。3.4 第四步初始化模型、训练、预测与评估KNN在sklearn里对应的是KNeighborsClassifier。我们先用默认的n_neighbors5跑一个基线。from sklearn.neighbors import KNeighborsClassifier knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train_scaled, y_train) y_pred knn.predict(X_test_scaled)fit在这里并没有做真正意义上的“训练”它做的事情是把标准化后的训练集存入模型内部。所以你说KNN“训练快”就是因为这个fit过程几乎是瞬时完成的。真正的计算发生在predict的时候。接下来用三个指标去看结果准确率、分类报告、混淆矩阵。我会同时打印出来因为每个指标只能反映一个侧面。from sklearn.metrics import accuracy_score, classification_report, confusion_matrix acc accuracy_score(y_test, y_pred) print(f准确率: {acc:.4f}) # 准确率: 0.9667 print(classification_report(y_test, y_pred, target_namesiris.target_names)) # precision recall f1-score support # setosa 1.00 1.00 1.00 10 # versicolor 1.00 0.90 0.95 10 # virginica 0.90 1.00 0.95 10 print(confusion_matrix(y_test, y_pred)) # [[10 0 0] # [ 0 9 1] # [ 0 0 10]]上面是以random_state42为前提下我实测得到的典型结果。30个测试样本里29个预测正确1个错误。准确率0.9667。3.5 第五步从混淆矩阵里读出真实信息混淆矩阵是一个3x3的方阵行代表真实类别列代表预测类别。对角线上的数字是预测正确的数量非对角线则是“把某类认成了另一类”。上面这份矩阵里第一类setosa全对第二类versicolor有1个被错认成第三类virginica第三类也全对。这个结果和鸢尾花数据的先验分布是一致的——变色鸢尾和维吉尼亚鸢尾本来就比较接近存在少量错分非常正常。如果只看准确率0.9667你只能说“不错”但配合混淆矩阵你能知道错在哪里、为什么错。这种判断力在真实项目中比准确率本身更值钱因为现实业务里的数据不会像鸢尾花这样干净。4. 调参不是玄学K值扫描、交叉验证与网格搜索的实测对比基线模型已经拿下了0.9667的准确率但别急着满意。我通常还会做一轮调参目的不是追求“更高的数字”而是确认当前参数在测试集上不是运气好同时观察模型的稳定性。这轮调参包含三个阶段手动扫K值、交叉验证、网格搜索。4.1 手动扫描K值画一条准确率曲线我习惯用一个循环把K从1扫描到25每一轮都用相同的训练集和测试集重新训练并记录准确率。这样做能看到整体的趋势而不是只盯着一个K下结论。k_range range(1, 26) acc_list [] for k in k_range: model KNeighborsClassifier(n_neighborsk) model.fit(X_train_scaled, y_train) acc_list.append(accuracy_score(y_test, model.predict(X_test_scaled))) # 输出几个关键点 for i, acc in enumerate(acc_list, start1): if i in [1, 3, 5, 7, 9, 11, 15, 20]: print(fK{i}: accuracy{acc:.4f})在我固定的随机种子下输出大致是K1: accuracy0.9333 K3: accuracy0.9333 K5: accuracy0.9667 K7: accuracy0.9667 K9: accuracy0.9667 K11: accuracy0.9667 K15: accuracy0.9333 K20: accuracy0.9333注意K1时的准确率0.9333低于K5的0.9667。这其实就是过拟合的征兆K1把训练数据背得太过细致个别训练样本周围的小块区域被划成了它自己的类别测试样本落进去就容易被误判。K5到K11之间准确率稳定在高位说明这个区间内模型性能比较稳健。K超过15之后投票开始引入太多“远方邻居”边界被磨平准确率回落。4.2 交叉验证避免“一次切分定终身”上一步的所有数字都是建立在train_test_split那一次切分上。万一那次切分恰好把难样本都集中到测试集或者把代表性样本都留在训练集结果就会偏保守或偏乐观。为了降低这种随机性标准做法是交叉验证。我常用的方式是5折交叉验证把训练数据均匀分成5份每次拿4份训练、1份验证轮流做5次最后取平均。sklearn的cross_val_score几行就能搞定。from sklearn.model_selection import cross_val_score model KNeighborsClassifier(n_neighbors5) scores cross_val_score(model, X_train_scaled, y_train, cv5) print(scores) # [0.95833333 0.95833333 1. 0.95833333 0.95833333] print(scores.mean()) # 0.96675折平均准确率同样是0.9667和之前测试集上的数字很接近。这说明我们看到的0.9667不是一个偶然模型在这个任务上就是稳定在这个水平。交叉验证的结果比单次切分出的测试集数字更适合写进报告里因为它反映了模型在多种数据划分下的综合表现。4.3 网格搜索K、权重和距离度量一起搜我最后会把三个关键参数放进GridSearchCV里让搜索过程替我做组合筛选。这里不推荐把范围拉太大比如K从1扫描到50甚至100在小数据集上既没必要也容易搜索出一个“只对训练集有效”的幸运组合。from sklearn.model_selection import GridSearchCV param_grid { n_neighbors: [3, 5, 7, 9, 11], weights: [uniform, distance], p: [1, 2] } grid GridSearchCV( KNeighborsClassifier(), param_grid, cv5 ) grid.fit(X_train_scaled, y_train) print(grid.best_params_) # {n_neighbors: 5, p: 2, weights: uniform} print(grid.best_score_) # 0.975 print(grid.score(X_test_scaled, y_test)) # 0.9667这里有一个很多人忽略的细节grid.best_score_是交叉验证分数而不是测试集分数。在报告里要分清这两个概念。交叉验证分数0.975说明的是——在所有参数组合里K5、欧氏距离、等权投票这一组在“训练集内部的交叉验证”中表现最好。而用它再预测真正没见过的测试集得到0.9667。这两个数字之间的小差距才是模型真实泛化能力的体现。网格搜索的结果有点意料之外又情理之中默认的K5、欧氏距离、等权投票本来就是这批组合里最优的。这说明鸢尾花数据集对KNN非常友好默认参数已经足够。但我仍然建议走这一遍因为它让你确认了“不是随便选的参数”也让你在汇报时能拿出搜索证据。4.4 我其实不建议在小数据集上追求极致得分调参阶段很容易让人产生一种冲动把所有参数都扫一遍挑出交叉验证分数最高的组合觉得那才是“最优模型”。在鸢尾花这样的小数据集上我反而建议适可而止。原因很简单样本量只有150条30条测试样本里其实只有1到2个样本在决定准确率的浮动空间。把准确率从0.9667调到1.0很可能只是多猜对了那一个边缘样本并不代表模型变强了。与其追求分数上的小步提升不如把精力放在确认数据处理过程正确、结果稳定、边界情况可解释这三个要点上。5. 训练过程中最容易踩的五个坑标准化泄漏、随机种子和看不见的数据顺序KNN和鸢尾花的组合虽然简单但真要在自己的环境里复现并写出一份可信的笔记坑还不少。下面五个是我自己踩过、也在帮别人看代码时反复见到的值得一条条说清楚。5.1 标准化时把测试集的信息“泄漏”进训练过程这是最常见的错误也是最隐蔽的。错误代码长这样scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.fit_transform(X_test) # 错第二行又对测试集单独fit了一次。虽然测试集本身的均值和标准差都被“告诉”了模型看似没什么大不了但这个动作意味着测试集的分布信息参与到了特征缩放里。在评估模型时测试集必须完全是未知数据哪怕只泄露了一点统计信息准确率都会掺水。正确做法是上一节里写的那样对所有新数据一律只调用transform不再fit。5.2 不固定随机种子结果每次都不一样train_test_split默认会打乱数据顺序所以如果不设random_state每次运行得到的训练集和测试集都不同模型分数也忽上忽下。我在帮别人复现代码时经常看到对方跑来问“为什么我跑两次准确率从0.9变成了0.8”答案几乎都是随机种子没固定。写代码时养成习惯划分数据、调参、初始化模型这三处统一用一个固定值比如42。这样做不是为了装专业是为了让你自己在迭代代码时有可对比的基准。5.3 无视数据顺序拿未shuffle的数据直接切分如果你不是用train_test_split而是手动切数组前80%做训练、后20%做测试就要特别小心。原数据集里的样本是按类别连续排列的前50行是setosa中间50行是versicolor最后50行是virginica。如果拿前80%训练、后20%测试测试集里根本不会有前两类模型等于没有见过它们——训练时会因为缺少某类样本而学不好测试时又出现“未训练过的类别”这种意外会让准确率一团糟。train_test_split默认shuffleTrue已经把这个问题挡掉了但我还是会在代码里显式写出来让读代码的人一眼看懂。5.4 只看准确率不拆分错误类型准确率0.9667听起来很美但换一个场景如果三类样本比例严重失衡比如99个A类、1个B类模型全都预测成A类准确率也有0.99看起来比我们现在的0.9667还高实际却毫无用处。虽然鸢尾花数据是平衡的但提前养成看分类报告和混淆矩阵的习惯等遇到不平衡数据时就能直接上手。5.5 KNN训练快、预测慢这个特性要在设计时就想好训练阶段几乎不耗时是因为KNN只是把数据存下来了。可一旦训练样本量变大每次预测都要遍历全部样本计算距离。假设你有100万条训练数据新样本进来就要算100万次距离预测延迟会变得不可接受。我见过一个明显案例有同事把KNN用在实时推荐接口上样本量几十万单次预测耗时要几十毫秒一旦流量上来服务就承受不住。这不是KNN不好而是选型时没考虑预测阶段的计算复杂度。如果只是学习入门这个问题远但如果你计划把KNN部署到生产环境必须提前想清楚数据规模和响应时间要求。比较务实的策略是用KD树或球树来加速近邻搜索sklearn里直接设置algorithmkd_tree就能启用但这属于进阶话题就不在这篇里展开了。最后聊一点我个人的实操习惯模型训练这块我每次都会按固定的顺序跑一遍读数据、看分布、分层划分、标准化、训练基线、看混淆矩阵、扫K值、交叉验证、网格搜索。看起来步骤多其实大部分代码都是重复的真正需要动脑的地方只有两个——标准化顺序是否正确以及调参结果是否经得起交叉验证的考验。还有个细节可能对你有用。我做完网格搜索后会顺手把最优参数重新在完整训练集上fit一次然后再对测试集做一次预测。这一步看起来和GridSearchCV内部做的重复但能让最终的模型对象干干净净地保存下来方便后续直接调用也方便在你自己的代码里复现结果。如果你现在正准备把这个项目整理成一篇博客或实验报告我的建议是不要把重点放在“我准确率到多少”上而是把数据分布、为什么选KNN、标准化怎么做的、K值扫描曲线的趋势、混淆矩阵错在哪里这几块写清楚。一个能解释清楚自己每一步选择的项目往往会比一个纯堆高分数的项目更耐看也更值得参考。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询