☰
鸢尾花数据集与KNN分类:从标准化到交叉验证的完整实战解析
2026/10/13 3:39:26 网站建设 项目流程

学机器学习的人,几乎都会在某个阶段遇到这个经典组合:鸢尾花数据集,配上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的取值直接影响决策边界。

如果K=1,模型只看最近的一个邻居。后果是决策边界非常“碎”,一个异常点就能在它周边划出一小片区域。训练集上可能接近满分,但遇到新数据就容易被噪声带着跑,这是典型的过拟合。

如果K特别大,比如K=140,几乎把全部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_size=0.2, stratify=y, random_state=42 ) print(X_train.shape) # (120, 4) print(X_test.shape) # (30, 4)

test_size取0.2,也就是120条训练、30条测试。鸢尾花总共就150条,测试集再大就会挤压训练数据,太小则评估波动大,0.2在同类小数据集里是比较常见的折中。stratify=y保证三类的训练测试划分比例接近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_neighbors=5跑一个基线。

from sklearn.neighbors import KNeighborsClassifier knn = KNeighborsClassifier(n_neighbors=5) 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_names=iris.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_state=42为前提下我实测得到的典型结果。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_neighbors=k) 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, start=1): if i in [1, 3, 5, 7, 9, 11, 15, 20]: print(f"K={i}: accuracy={acc:.4f}")

在我固定的随机种子下,输出大致是:

K=1: accuracy=0.9333 K=3: accuracy=0.9333 K=5: accuracy=0.9667 K=7: accuracy=0.9667 K=9: accuracy=0.9667 K=11: accuracy=0.9667 K=15: accuracy=0.9333 K=20: accuracy=0.9333

注意K=1时的准确率0.9333,低于K=5的0.9667。这其实就是过拟合的征兆:K=1把训练数据背得太过细致,个别训练样本周围的小块区域被划成了它自己的类别,测试样本落进去就容易被误判。K=5到K=11之间准确率稳定在高位,说明这个区间内模型性能比较稳健。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_neighbors=5) scores = cross_val_score(model, X_train_scaled, y_train, cv=5) print(scores) # [0.95833333 0.95833333 1. 0.95833333 0.95833333] print(scores.mean()) # 0.9667

5折平均准确率同样是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, cv=5 ) 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说明的是——在所有参数组合里,K=5、欧氏距离、等权投票这一组在“训练集内部的交叉验证”中表现最好。而用它再预测真正没见过的测试集,得到0.9667。这两个数字之间的小差距,才是模型真实泛化能力的体现。

网格搜索的结果有点意料之外又情理之中:默认的K=5、欧氏距离、等权投票,本来就是这批组合里最优的。这说明鸢尾花数据集对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默认shuffle=True已经把这个问题挡掉了,但我还是会在代码里显式写出来,让读代码的人一眼看懂。

5.4 只看准确率,不拆分错误类型

准确率0.9667听起来很美,但换一个场景:如果三类样本比例严重失衡,比如99个A类、1个B类,模型全都预测成A类,准确率也有0.99,看起来比我们现在的0.9667还高,实际却毫无用处。虽然鸢尾花数据是平衡的,但提前养成看分类报告和混淆矩阵的习惯,等遇到不平衡数据时就能直接上手。

5.5 KNN训练快、预测慢,这个特性要在设计时就想好

训练阶段几乎不耗时,是因为KNN只是把数据存下来了。可一旦训练样本量变大,每次预测都要遍历全部样本计算距离。假设你有100万条训练数据,新样本进来就要算100万次距离,预测延迟会变得不可接受。

我见过一个明显案例:有同事把KNN用在实时推荐接口上,样本量几十万,单次预测耗时要几十毫秒,一旦流量上来,服务就承受不住。这不是KNN不好,而是选型时没考虑预测阶段的计算复杂度。如果只是学习入门,这个问题远;但如果你计划把KNN部署到生产环境,必须提前想清楚数据规模和响应时间要求。比较务实的策略是用KD树或球树来加速近邻搜索,sklearn里直接设置algorithm='kd_tree'就能启用,但这属于进阶话题,就不在这篇里展开了。

最后聊一点我个人的实操习惯

模型训练这块,我每次都会按固定的顺序跑一遍:读数据、看分布、分层划分、标准化、训练基线、看混淆矩阵、扫K值、交叉验证、网格搜索。看起来步骤多,其实大部分代码都是重复的,真正需要动脑的地方只有两个——标准化顺序是否正确,以及调参结果是否经得起交叉验证的考验。

还有个细节可能对你有用。我做完网格搜索后,会顺手把最优参数重新在完整训练集上fit一次,然后再对测试集做一次预测。这一步看起来和GridSearchCV内部做的重复,但能让最终的模型对象干干净净地保存下来,方便后续直接调用,也方便在你自己的代码里复现结果。

如果你现在正准备把这个项目整理成一篇博客或实验报告,我的建议是不要把重点放在“我准确率到多少”上,而是把数据分布、为什么选KNN、标准化怎么做的、K值扫描曲线的趋势、混淆矩阵错在哪里这几块写清楚。一个能解释清楚自己每一步选择的项目,往往会比一个纯堆高分数的项目更耐看,也更值得参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询