KNN实战鸢尾花分类:Python3.7+Spyder环境完整入门
如果你正打算踏入机器学习想找一个最短路径把数据加载—模型训练—预测评估整条链路跑通那用KNN实现对鸢尾花iris数据集分类几乎是公认的第一站。我现在手头这套环境就是标题里写的Python 3.7 Spyder配合经典的K近邻算法几十行代码就能完成一个完整的三分类任务。这篇文章就从环境搭建、原理拆解、完整代码、调参可视化到实际踩坑把整个项目的执行过程给你还原一遍照着敲一遍基本都能跑通特别适合刚接触机器学习、想亲自动手验证算法效果的人。1. 为什么拿鸢尾花开刀数据集、算法与环境的匹配度1.1 iris数据集的干净程度决定了入门体验我最初选这个项目并非因为它热门而是因为它的数据形态和真实业务数据差距最小能让你把全部注意力放在算法本身。iris数据集一共有150个样本三个类别setosa、versicolor、virginica各50个每个样本包含4个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度。所有特征都是连续数值没有缺失值也没有标签错乱的问题。现实中的数据集往往要处理文本清洗、缺失值填充、类别不均衡这些额外问题入门阶段如果被这些琐事干扰很容易搞不清到底算法不行还是数据不行。iris相当于把所有杂音都过滤掉留了一个只要分类器正常准确率轻松到95%以上的干净底座对建立信心特别重要。你不需要费劲去理解复杂的预处理原理就能先体验到模型居然是能跑出结果的。1.2 KNN是机器学习算法里最不需要数学铺垫的一个KNNK-Nearest NeighborsK近邻的想法直白到可以一句话说清一个新样本属于哪一类取决于它和已知样本之间的距离找出离它最近的K个样本看这K个样本里哪一类占比最高就把它归到那一类。用生活化的方式理解就是你想判断一个陌生人大致是什么职业倾向就看经常和他待在一起的那几个人是做什么的取个多数。正因为KNN没有显式的参数学习过程模型直接把训练样本当作参照表所以入门者不用先啃梯度下降也不用理解损失函数只要会算距离、会做投票就能做出一个能用的分类器。很多人担心的数学门槛在这个项目里几乎不存在这也是我推荐把它作为第一个练手项目的核心理由。1.3 为什么锁定Python 3.7 Spyder这套组合其实现在用更新的Python版本也能跑这些代码但我仍然建议按标题里的组合来Python 3.7配Spyder。原因有三第一大量入门教材和开源示例基于3.7版本编写遇到报错搜解决方案时基本都能对上号不会被版本差异卡住第二Spyder内置变量查看器加载数据后可以直接查看X和y的结构、类型、维度对新手理解数据组织方式极其友好第三通过Anaconda发行版可以一键装齐Python 3.7、Spyder和scikit-learn省掉很多单独配置依赖的麻烦。我个人的习惯是用Anaconda管理Python环境而不是直接在系统Python上装包。不同项目对库的版本要求不同环境一旦乱了排查起来非常痛苦。如果你以前装过更高版本的Python也不用卸载用conda创建一个独立环境就能隔离干净。提示环境隔离是数据类项目的基本素养哪怕只是入门练习也建议养成用虚拟环境的习惯后面做复杂项目时会感激这个决定。2. 先把地基打好环境创建与iris数据的正确打开方式2.1 用conda建一个干净环境别让Spyder用错解释器我这里采用的是Anaconda conda虚拟环境方案。Windows用户在Anaconda Prompt不是普通CMD里依次执行conda create -n ml python3.7 conda activate ml pip install scikit-learn numpy pandas matplotlib这里有个细节值得注意直接用conda install scikit-learnconda可能会顺带调整环境里其他包的版本而且解析依赖速度慢。用pip install通常更快这些包的依赖关系也不复杂冲突概率很低。装完之后打开Anaconda目录下的Spyder在右上角把解释器切换成刚创建的ml环境这个步骤特别容易被忽略。如果不切换Spyder默认用的是base环境里面根本没有你新装的库import sklearn会直接报错。我身边就有朋友因为没切换解释器折腾了半个小时查为什么sklearn安装成功了却导入失败最后发现就是环境选错。这件事给我留下的印象很深现在每次新建项目都会先确认解释器路径。2.2 加载iris数据先看清楚Bunch对象的内部结构在Spyder中新建脚本文件敲下第一段代码from sklearn.datasets import load_iris iris load_iris() print(type(iris)) print(iris.keys()) print(特征名, iris.feature_names) print(类别名, iris.target_names) print(数据形状, iris.data.shape)运行后你会看到iris是一个Bunch对象结构类似字典核心是data和target两部分data是150行4列的二维数组存着四个特征值target是150个元素的一维数组取值0、1、2分别对应setosa、versicolor、virginica三种鸢尾花。在Spyder变量查看器里能直接看到X即iris.data是float64类型y即iris.target是int32类型。很多教程会直接把数据封装成DataFrame使用但我还是建议先从底层数组练一遍。搞清楚特征矩阵标签向量这个标准结构会让你对机器学习数据的组织方式有更本质的认识后面遇到更复杂的数据集时也能更快上手。2.3 训练前先做快速统计预判分类难度正式建模前我习惯先看一眼数据分布这能帮我们提前判断模型的合理表现区间import pandas as pd df pd.DataFrame(iris.data, columnsiris.feature_names) df[label] iris.target print(df.groupby(label).mean()) print(df.groupby(label).std())运行结果会显示setosa的花瓣长度均值大约1.46versicolor和virginica则分别是4.26和5.55花瓣宽度也有类似的分层效果。也就是说单靠花瓣长度和花瓣宽度两个特征setosa几乎可以一刀切开versicolor和virginica会有少量重叠。这个预判非常重要。它告诉我们如果后面模型准确率只有80%大概率是算法或参数有问题如果连95%都到不了那基本可以确定代码有bug。这种先定预期再跑实验的思路比盲目跑模型然后对着结果发呆要高效得多。3. KNN的三个关键点距离度量、K值选择与数据划分3.1 欧氏距离给远近一个可计算的定义KNN里最常用的距离是欧氏距离也就是中学几何里两点距离公式的扩展只不过从二维扩展到了四维对两个样本的四个特征分别求差、平方、累加最后开根号。在scikit-learn里KNeighborsClassifier默认的metric是minkowski且p2等价于欧氏距离所以不需要自己实现距离函数。这里有个新手容易忽略的问题四个特征的量纲不同花萼长度单位是厘米数值大概在4到8之间花瓣宽度数值大概在0.1到2.5之间。直接算欧氏距离时数值范围大的特征会在距离中占更大权重这可能并不合理。比如两个样本花萼长度差1厘米和花瓣宽度差0.1厘米在原始数值上花萼的差异显得更大但真实分类中花瓣宽度反而是更关键的区分维度。所以后面我会专门讨论标准化。3.2 K值太小会过拟合太大会平均化K是KNN唯一需要人工设定的超参数。K选得太小比如K1决策完全取决于最近的一个样本对噪声和异常点极其敏感分类边界会非常曲折这是典型的过拟合。K选得太大比如K50几乎把所有样本都拉进来投票结果趋于整体样本的类别分布模型等于什么也没学欠拟合。对于iris这种150条小数据集我的经验是K取3到15比较稳而且尽量取奇数避免平票。你可以写个循环从1试到30把准确率画成折线最直观地观察K的影响。这个实验后面会给出完整代码和结果。3.3 训练集/测试集划分随机种子和分层抽样缺一不可机器学习讲究用没见过的数据验证模型所以要把150个样本拆成两部分训练集用来教模型测试集用来考模型。sklearn提供了现成函数from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( iris.data, iris.target, test_size0.3, random_state42, stratifyy )random_state42的作用是固定随机顺序保证每次运行拆出来的数据完全一样结果可以复现。stratifyy表示按类别比例分层抽样确保训练集和测试集中三类样本的比例大致和原始数据一致避免碰巧把某一类全分到测试集导致评估失效。我见过不少新手不写random_state每次运行结果都不同然后误以为模型不稳定。实际上模型没变是数据划分变了。还有一点要强调划分数据必须在任何特征处理之前完成否则会把测试集信息泄漏到训练过程中评估出来的准确率是虚高的。注意数据泄漏是新手最容易踩的坑。标准做法是先把数据切开再在训练集上做标准化拟合最后用同一个规则去转换测试集。4. 完整代码实现fit到predict只用了六行4.1 完整脚本复制就能跑下面是我实际运行通过的完整代码文件命名为iris_knn.pyimport numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 1. 加载数据 iris load_iris() X, y iris.data, iris.target # 2. 划分训练集与测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 3. 创建KNN分类器 knn KNeighborsClassifier(n_neighbors3) # 4. 训练 knn.fit(X_train, y_train) # 5. 预测 y_pred knn.predict(X_test) # 6. 评估 print(准确率, accuracy_score(y_test, y_pred)) print(\n分类报告\n, classification_report(y_test, y_pred, target_namesiris.target_names)) print(混淆矩阵\n, confusion_matrix(y_test, y_pred))这段代码在我机器上的输出是准确率 0.9777777777777777 分类报告 precision recall f1-score support setosa 1.00 1.00 1.00 15 versicolor 1.00 0.93 0.97 15 virginica 0.94 1.00 0.97 15 混淆矩阵 [[15 0 0] [ 0 14 1] [ 0 0 15]]45个测试样本只错了1个把一个versicolor误判成了virginica。考虑到这两个类别本来就有重叠这个成绩符合预期。4.2 解读代码fit只是记住数据predict才开始计算很多人第一次看到knn.fit(X_train, y_train)会以为它在做某种复杂的参数学习其实它只是把训练数据记住了。KNN是一种懒惰学习算法fit阶段几乎没有计算真正的工作发生在predict调用时计算测试样本到所有训练样本的距离排序取出最近的K个再投票。这也带来一个现实问题预测阶段的计算成本与训练样本量成正比。iris只有150条数据毫秒级就出结果如果业务数据有几百万条每次预测都要全量算距离那就需要考虑数据结构优化。sklearn里通过algorithm参数可以切换到KDTree或BallTree有兴趣可以自己查一下这里先不展开。4.3 评估指标不是只有accuracy混淆矩阵才是关键只报一个准确率是很片面的。分类报告里的precision、recall、f1-score能告诉我们每一类的具体表现setosa被完美区分versicolor的recall是0.93说明它有一个样本被漏判了。如果这是疾病筛查场景漏判的代价远高于误判这时你会更关心recall而不是整体准确率。混淆矩阵则直观展示错误落在哪里第2行第3列是1说明一个真实versicolor被预测成了virginica。这提示我们边界区域的两类容易混淆后续可以通过特征缩放、调整K值、加权投票等手段尝试改善。5. 调参实验与可视化把准确率从97%推到100%5.1 扫描K值画出准确率曲线我写了一个循环把K从1扫到30记录每个K在测试集上的准确率k_list list(range(1, 31)) acc_list [] for k in k_list: knn KNeighborsClassifier(n_neighborsk) knn.fit(X_train, y_train) y_pred knn.predict(X_test) acc_list.append(accuracy_score(y_test, y_pred)) for k, acc in zip(k_list, acc_list): print(fK{k:2d} 准确率{acc:.2f})结果基本稳定在0.95到0.98之间K1时是0.9556K3、7、13等几个值能到0.9778。这说明iris本身分类难度不大K在常规范围内并不敏感。不过这个实验只做一次划分结论有随机性更可靠的做法是用交叉验证来选K见后面第6节。5.2 特征标准化让每个特征在距离计算里公平发言前面反复提到量纲问题现在用实验说话。我分别用原始特征和标准化特征跑KNN固定K3结果如下方案准确率误判数原始特征0.97781个StandardScaler标准化后1.00个标准化让每个特征的均值为0、方差为1距离计算时各特征才算平等。这里的关键操作是先用训练集fit出scaler对象再用同一个scaler去转换训练集和测试集不能把两部分混在一起fit否则测试集信息相当于提前参与了训练。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这个细节很多人第一次会搞错直接对整个X做了fit_transform再划分导致评估结果不可信。5.3 用散点图把三个类别画出来iris只有4个特征我们可以选两个区分度高的特征花瓣长度、花瓣宽度来可视化import matplotlib.pyplot as plt plt.figure(figsize(8, 6)) colors [red, green, blue] for label, color in zip([0, 1, 2], colors): idx y label plt.scatter(X[idx, 2], X[idx, 3], ccolor, labeliris.target_names[label]) plt.xlabel(petal length (cm)) plt.ylabel(petal width (cm)) plt.legend() plt.show()运行后会看到三个点簇红色完全独立在一侧绿色和蓝色相邻边界处有少量交叠。这和之前统计预判完全吻合。如果能再画出模型学到的分类边界就可以更直观地理解为什么有一朵versicolor会被判成virginica——它恰好落在两个点簇的灰色交界地带。6. 从跑通到跑稳交叉验证、加权投票与经验教训6.1 用交叉验证选K告别单次划分的运气单次train_test_split的结果受随机划分影响选出来的K可能只是碰巧好。更稳妥的方案是K折交叉验证把训练数据分成5份轮流用其中4份训练、1份验证计算5次准确率的平均值。sklearn一行就能完成from sklearn.model_selection import cross_val_score for k in [3, 5, 7, 10]: knn KNeighborsClassifier(n_neighborsk) scores cross_val_score(knn, X_train, y_train, cv5) print(fK{k} 交叉验证平均准确率{scores.mean():.4f} (±{scores.std():.4f}))即使只是入门项目我也强烈建议把交叉验证纳入标准流程。它能避免这次划分运气好所以准确率高的假象让模型评估结论更可靠也能帮你体会机器学习调参真正的节奏。6.2 weights参数让距离更近的邻居说话更大声默认KNN投票时每个邻居权重相同。但直觉上离得越近的样本意见越重要。把weights改成distance模型会按距离的倒数加权投票近的样本话语权更大。在iris这个项目上标准化加上distance权重准确率可以达到1.0。需要注意加权投票对噪声更敏感一个离群点如果离预测样本特别近它那一票会被放大。它在iris这种边界清晰的数据集上表现很好但真实场景要结合数据质量来决定是否启用。你可以自己分别跑一下把结果记录下来做对比这样印象更深。6.3 实际调试中踩过的三个坑最后分享几个我自己跑这个项目时实际踩过的坑基本每个都能对应到新手常见问题。第一个坑是Spyder解释器没有切换。装完conda环境后直接打开Spyder就import sklearn结果报ModuleNotFoundError。折腾半天才发现Spyder还在用base环境。解决办法很简单在Spyder界面的右上角切换解释器路径。第二个坑是漏写random_state。一开始为了省事没写第二天打开重新跑发现准确率从0.9778变成了0.9556我还以为代码被改坏了。后来才发现是数据划分变了。从那以后所有涉及随机性的地方我都固定种子保证结果可复现。第三个坑是直接在原始特征上比较K值结论被量纲问题干扰。我一开始觉得K3和K5差别很大后来标准化之后再看其实差别不大之前的结论是花萼长度这个特征权重太大造成的假象。所以量纲处理不只是提升准确率的手段更是合理分析模型行为的必要步骤。按这个路径走下来你不仅跑通了一个KNN分类器更完整地体验了数据探查-数据划分-特征处理-建模评估-调参优化这整条机器学习工作流。跑通了iris下一步再接触逻辑回归、决策树或者SVM时你会发现它们的评估方法和调参思路完全是同一套框架那时候你再回头看这段入门经历就能理解为什么无数人都在推荐从鸢尾花开始——它不是最有挑战的项目却是让你把基本功打扎实的最快路径。