KNN算法与鸢尾花分类:机器学习入门完整实战教程

📅 2026/8/27 22:27:29
KNN算法与鸢尾花分类:机器学习入门完整实战教程
简介机器学习入门常让人感到无从下手而掌握经典分类算法是建立数据思维的第一步。K近邻KNN作为最直观的监督学习算法之一基于距离度量与多数投票原理无需复杂数学推导即可理解模型决策过程。在实践环节数据预处理中的特征标准化尤为关键它能消除量纲差异对距离计算的影响避免某些特征主导分类结果。通过训练集与测试集划分、交叉验证选择超参数K、混淆矩阵与分类报告评估模型我们可以完整经历从数据探索到模型调优的工程流程。这类项目广泛应用于模式识别、推荐系统等场景也是学习Python数据科学生态如scikit-learn、pandas的高效路径。本文以鸢尾花数据集为载体从零手写KNN分类器到调用成熟库实现帮助初学者真正理解算法本质与机器学习项目落地的完整环节。 前阵子有朋友问我想入门机器学习但完全不知道从哪下手。我的回答始终是同一个先把基于KNN算法的鸢尾花分类这个项目完整跑通一遍。原因很简单这是机器学习领域公认的“Hello World”代码量小、数据集干净、算法原理直观但麻雀虽小五脏俱全——数据加载、数据探索、预处理、模型训练、效果评估、超参数调优一套完整的机器学习流程全都能过一遍。而且用Python实现KNN分类器几十行代码就能完成对新手极其友好。这篇文章我会从项目背景讲起把KNN算法原理掰开揉碎然后给出完整可复现的代码和实操步骤最后把我自己踩过的一些坑也一并整理出来。无论你是刚装好Python还不太会用Jupyter的纯小白还是已经写过一些脚本但没接触过机器学习的开发者这篇文章的目标都只有一个让你能看懂、能跑通、能理解背后的原理而不是只会复制粘贴。1. 项目整体设计为什么是KNN和鸢尾花的组合1.1 鸢尾花数据集一个恰到好处的入门样本鸢尾花数据集Iris Dataset是机器学习历史上最经典的数据集之一1936年由统计学家R.A. Fisher引入距今将近90年依然活跃在各类教材和入门教程中。数据集本身非常小总共只有150条样本每条样本包含4个特征花萼长度sepal length、花萼宽度sepal width、花瓣长度petal length、花瓣宽度petal width单位是厘米。而预测目标是判定这朵花属于3个品种中的哪一个山鸢尾Iris Setosa、变色鸢尾Iris Versicolour、维吉尼亚鸢尾Iris Virginica每个品种恰好50条样本。选这个数据集入门有几个实实在在的好处。第一数据量小训练和预测几乎是瞬间完成的不需要GPU不是显卡随便一台日常办公电脑就能跑。第二特征只有4个做可视化时我们可以用二维、三维散点图直接观察数据分布不需要做复杂的降维。第三这个数据集非常“干净”没有缺失值、没有异常值、类别分布完全均匀这就意味着你不需要在数据清洗上花太多时间可以把所有注意力集中在理解分类算法本身。第四也是最关键的这个数据集天然存在一个有趣的规律山鸢尾这个品种通过花瓣长度和花瓣宽度这两个特征就能和另外两个品种完全区分开而变色鸢尾和维吉尼亚鸢尾之间有部分特征重叠不是完全线性可分的。也就是说即使是最简单的模型也能拿到一个不错的准确率但想把准确率做到极致又需要认真调整参数非常适合拿来体会算法的差异。1.2 KNN算法底层逻辑懒人算法里的聪明策略KNN全称K-Nearest Neighbors中文叫K近邻算法。它的核心思想用一句话就能概括物以类聚人以群分。想判断一个新样本属于哪个类别就找训练集里离它最近的K个样本看看这K个“邻居”大多数属于哪个类别就把它也判成那个类别。这里说的“近”需要有一个明确的数学定义。我们默认使用欧氏距离对于一个有4个特征的样本两个样本之间的距离就是4维空间里的直线距离计算公式为d sqrt((x1-y1)^2 (x2-y2)^2 (x3-y3)^2 (x4-y4)^2)扩展到n维空间就是 d sqrt(Σ(xi-yi)^2)。这个公式本质上就是初中学的勾股定理在更高维空间的推广。如果把特征理解为坐标轴上的位置两个样本就相当于空间里的两个点KNN做的其实就是计算点与点之间的几何距离。K是算法里唯一的超参数它的取值直接决定了分类效果。K等于1时算法退化成最近邻分类新样本只参考离它最近的那一个样本的类别这很容易受到个别异常点的干扰K值增大参与投票的邻居变多算法的鲁棒性会提升但如果K值太大会把距离很远、其实不属于同一类群的样本也拉进来投票反而拉低准确率。后面我会专门讲怎么选K值。KNN还有一个重要特点它属于“懒惰学习”或者叫“基于实例的学习”。所谓懒惰并不是说它真的偷懒而是指它在训练阶段几乎不做任何事只是把训练数据原封不动地存下来不做模型拟合、不学参数。真正的计算都发生在预测阶段——每来一个新样本都要现场计算它和所有训练样本的距离然后排序取前K个。这就意味着它的预测时间成本和数据量成正比训练集越大、特征越多单次预测就越慢。反观逻辑回归、决策树这类“急切学习”算法训练阶段耗时较长但训练完成后预测非常快。这算是KNN在工业落地时最典型的短板。1.3 这个组合能帮你建立完整的机器学习思维框架很多人学机器学习容易犯一个错误一上来就研究神经网络的复杂结构结果梯度消失、过拟合这些概念把自己绕晕了最后写不出一个完整项目。KNN之所以成为学术界公认的入门首选是因为它在所有基础算法中概念壁垒最低完全不需要求导、不需要梯度下降、不需要理解损失函数只要懂距离和投票就能看懂全部原理。但重要的是KNN项目所承载的完整流程思维是可以复用到任何后续机器学习项目中的。你在跑鸢尾花项目时会遇到以下所有环节用pandas读数据、用matplotlib做可视化、用train_test_split划分训练集测试集、用StandardScaler做特征标准化、用分类报告评估效果、用交叉验证调超参数。这些流程在不同项目中几乎都是不变的。这也是我强烈建议你手动用Python实现一遍KNN而不仅仅调用sklearn的原因——手写一遍能让你彻底看清算法内部发生了什么。2. 环境准备与数据探索2.1 跑通这个项目需要准备什么如果你是零基础尽量别在自己电脑上折腾裸环境装Python的时候配环境变量、装库的时候处理依赖冲突这些都对新手不友好。我建议直接安装Anaconda它是一个Python发行版自带Python解释器、Jupyter Notebook、Spyder以及pandas、numpy、matplotlib、scikit-learn这些数据科学库。用一个Anaconda替代你手动配置一大堆东西能省下半天折腾时间。具体安装步骤很简单去Anaconda官网下载对应你操作系统的安装包一般选Python 3.x版本。Windows用户安装时注意勾选“Add Anaconda to my PATH environment variable”Mac和Linux用户在终端操作。安装完成后打开Anaconda自带的Jupyter Notebook新建一个Python 3的Notebook直接开始写代码。检查一下关键库是否齐全。在Notebook里执行以下命令import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, confusion_matrix print(环境就绪)正常情况下不会有报错。如果有报错提示找不到某个库用conda install 库名或者pip install 库名补装就行。有一点需要提醒在Jupyter里安装库要在Notebook的代码单元格前面加一个感叹号执行系统命令例如!pip install seaborn2.2 数据读入与结构体检sklearn自带了鸢尾花数据集所以不需要去UCI官网单独下载数据文件直接一行代码就能加载from sklearn.datasets import load_iris iris load_iris()但这里有个小坑load_iris()返回的是一个Bunch对象它同时包含数据、标签和特征名称格式不是DataFrame新手直接打印会发现看不懂。推荐转换成DataFrame再操作import pandas as pd df pd.DataFrame(iris.data, columnsiris.feature_names) df[target] iris.target df[target_name] df[target].map({0: setosa, 1: versicolor, 2: virginica}) print(df.head())这样一个150行、6列的DataFrame就出来了。先跑一下df.info()和df.describe()看看数据的情况。df.info()会告诉你这个数据集没有缺失值df.describe()会给出每个特征的均值、标准差、最小值、最大值比如花瓣长度的范围是1.0到6.9厘米而花萼宽度的范围是2.0到4.4厘米分布范围不一样这为我们后面做特征标准化埋下了伏笔。2.3 可视化在训练之前先“看”懂数据机器学习不是上来就训练模型。我在实际处理任何数据之前都会先做可视化用眼睛去理解数据结构。这一步看起来“很慢”实际上非常高效。先用seaborn画一个特征两两对比的散点图矩阵import seaborn as sns sns.pairplot(df, huetarget_name, diag_kindkde) plt.show()注意观察这个图山鸢尾setosa在花瓣长度和花瓣宽度的二维平面上和另外两个品种是完全分开的它独自占据一片区域而变色鸢尾和维吉尼亚鸢尾有部分区域重叠。这个观察会直接影响你对算法效果的预判即便只用花瓣特征山鸢尾也几乎不可能被错分但变色鸢尾和维吉尼亚鸢尾之间一定会存在一些较难划分的边界样本。还可以画一个箱线图来观察特征在不同类别间的分布差异plt.figure(figsize(10, 6)) for i, feature in enumerate(iris.feature_names): plt.subplot(2, 2, i1) sns.boxplot(xtarget_name, yfeature, datadf) plt.tight_layout() plt.show()箱线图能很直观地看出不同类别在某个特征上的区分度。比如花瓣长度这个特征三个类别的箱体位置明显不同说明这个特征对分类很有效而花萼宽度这个特征三个类别的箱体有很大的重叠区域说明单靠它区分效果会很差。3. 核心代码实现从手写到调库3.1 手写一个KNN分类器彻底理解算法本质为了让你彻底弄懂KNN到底在做什么我建议先不要直接用sklearn而是用numpy手写一个KNN分类器。代码量不大逻辑非常清晰核心就四步计算距离、排序、取前K个、投票。import numpy as np class KNN: def __init__(self, k3): self.k k def fit(self, X, y): # KNN的训练没有任何学习过程就是记住所有训练样本 self.X_train X self.y_train y def predict(self, X_test): predictions [] for x in X_test: # 第一步计算当前测试样本到所有训练样本的欧氏距离 distances np.sqrt(np.sum((self.X_train - x) ** 2, axis1)) # 第二步按距离从小到大排序返回索引 sorted_idx np.argsort(distances) # 第三步取最近的前k个索引 k_idx sorted_idx[:self.k] # 第四步对这k个标签投票取出现次数最多的作为预测结果 k_labels self.y_train[k_idx] label np.bincount(k_labels).argmax() predictions.append(label) return np.array(predictions) # 使用方法 X iris.data y iris.target knn KNN(k3) knn.fit(X, y) pred knn.predict(X) print(训练集准确率:, np.mean(pred y))要注意的是这段代码里有一个numpy高级索引的细节self.X_train是一个150行4列的二维数组x是一个长度为4的一维数组。self.X_train - x会触发numpy的广播机制让每个训练样本都减去x得到一个150行4列的新数组。然后np.sum(..., axis1)对每一行求和np.sqrt开根号之后得到一个长度为150的一维向量里面每一项就是当前测试样本到第i个训练样本的距离。这一段代码是整个KNN实现的核心建议手写一遍。3.2 用sklearn三分钟实现同一件事手写一遍理解原理之后实际项目中用现成库就方便多了。scikit-learn提供了完整的KNN实现from sklearn.neighbors import KNeighborsClassifier knn_sk KNeighborsClassifier(n_neighbors3) knn_sk.fit(X_train, y_train) y_pred knn_sk.predict(X_test)就这两行和手写版本的逻辑完全一致。KNeighborsClassifier的默认距离度量就是欧氏距离默认权重是均匀投票也就是说每个邻居对最终投票贡献相同。如果你想最近的点权重更大可以传入weightsdistance让距离越近的邻居投出的票权重越高这在很多场景下都能带来小幅的准确率提升。3.3 标准化一个不做就会吃大亏的预处理步骤我第一次跑鸢尾花分类时偷懒没做标准化结果准确率低了两三个百分点。当时想不明白为什么后来仔细一算才知道原因KNN算法依赖距离计算而距离计算对特征的数值范围极其敏感。回想一下欧氏距离公式里(x1-y1)^2和(x2-y2)^2是直接相加的。花瓣长度的波动范围是1.0到6.9花萼宽度的波动范围是2.0到4.4。在计算距离时花瓣长度贡献的差值平方远远大于花萼宽度相当于模型自动“偏心”了——它会更看重花瓣长度这类数值大的特征而忽略花萼宽度这种数值小的特征。但事实上单独看花萼宽度虽然区分度不强但它在特定组合下依然有帮助。解决办法就是对特征做标准化让每个特征都变成均值为0、标准差为1的分布公式是z (x - mean) / std。sklearn里的StandardScaler可以很轻松地完成这件事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训练集和测试集。我把标准化的效果做了个对比。同一组数据、同样的K值不标准化时准确率大概在92%左右标准化后能到96%以上。尤其对于鸢尾花数据集这种特征范围差异明显的场景标准化的收益非常显著。完整的代码块如下这是本项目最核心的训练流程# 1. 加载数据 from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score iris load_iris() X, y iris.data, iris.target # 2. 划分训练集和测试集stratify保证类别分布一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 3. 标准化 scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) # 4. 训练KNN模型 knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train, y_train) # 5. 预测并评估 y_pred knn.predict(X_test) print(测试集准确率:, accuracy_score(y_test, y_pred))关于train_test_split的random_state参数我多说一句。固定random_state42可以保证每次运行得到相同的训练集和测试集划分这让模型结果可复现。如果你不设置这个参数每次跑代码的划分都会不同模型准确率也会小幅波动新手容易误以为算法不稳定。而stratifyy参数的作用是让训练集和测试集中三类鸢尾花的比例尽量接近原始数据集。在150个样本里有5个样本被误分的概率都很大如果随机划分凑巧把某类样本全部留在训练集里测试集就会遇到“没见过的类别”那准确率就很难看了。4. 模型评估、调参与踩坑记录4.1 评估不能只看准确率加混淆矩阵和分类报告很多新手跑完模型只看一个准确率数字这远远不够。准确率在分类问题里是最直观的指标但在数据类别不均衡时会被严重误导。鸢尾花数据集恰好是均衡的每个类别50条所以准确率尚且可以用如果我换成99%是A类、1%是B类的数据模型直接“全猜A”就能拿到99%准确率但这个模型毫无意义。更好的做法是看混淆矩阵confusion matrix它能告诉你模型在哪个类别上容易出错from sklearn.metrics import confusion_matrix, classification_report print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesiris.target_names))把测试集有45条样本的情况下跑出来的混淆矩阵拿来看你会发现一种典型模式对角线上的数字大说明每个类别都被正确分类如果对角线以外的数字集中出现在某个位置比如把virginica误判为versicolor解释为这两个品种本身在特征空间里就存在重叠区域。可视化混淆矩阵也有助于解释import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(y_test, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsiris.target_names, yticklabelsiris.target_names) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.show()classification_report会给出每个类别的精确率precision、召回率recall和F1分数这些都是分类任务里的核心指标。从实操角度看我建议在任何一个分类项目里都养成看这三样东西的习惯准确率看整体、混淆矩阵看错在哪、分类报告看每个类别的精细表现。4.2 K值到底怎么选从K1试到K15看曲线找拐点K是KNN里最核心的超参数选K的过程也是一场“试错实验”。K太小时模型对训练数据过度敏感容易过拟合K太大时模型过于“从众”容易把细微差异忽略掉造成欠拟合。一般经验是选择奇数避免投票出现平局——尤其在二分类任务中偶数K值容易平票让算法不知道该选哪一个。实操方法很简单把K从1到15依次循环每个K值都做一次交叉验证画一条折线图看准确率的变化from sklearn.model_selection import cross_val_score import matplotlib.pyplot as plt import numpy as np k_range range(1, 16) k_scores [] for k in k_range: knn KNeighborsClassifier(n_neighborsk) scores cross_val_score(knn, X_train, y_train, cv5) k_scores.append(scores.mean()) plt.plot(k_range, k_scores) plt.xlabel(K值) plt.ylabel(交叉验证平均准确率) plt.title(K值对模型效果的影响) plt.show() best_k k_range[np.argmax(k_scores)] print(交叉验证最好的K值:, best_k)我实际跑出来的结果在K5到K9之间比较平稳准确率都在96%左右。选择K值时除非某一点明显优于其他值很多否则我会选一个中间的“安全值”而不是单纯选准确率最高的那个。因为交叉验证本身也有随机性两个K值之间的准确率差距在1%以内属于正常波动没必要为了这1%选择更复杂的模型。4.3 新手最容易踩的5个坑我把朋友们在跑这个项目时问得最多的问题整理成了一张表希望对你有用。问题原因解决办法不标准化直接训练准确率低特征量纲不同距离被数值大的特征主导先fit_transform训练集再transform测试集先标准化后划分数据集数据泄露测试集信息被提前用到先train_test_split再fit_transformrandom_state不固定每次结果不一样随机划分导致训练集测试集不同设置random_state42之类的固定值K设为偶数二分类平票投票结果可能打平用奇数K值直接使用训练集评估模型模型已经见过了训练数据评估结果虚高必须用没见过的测试集评估上述问题里我特别想展开讲数据泄露这个概念。很多人不理解“标准化一下测试集怎么了”但本质上测试集代表的是未来可能出现的新数据你在处理真实业务时根本不可能提前知道新数据的均值和标准差。如果标准化时用了测试集的信息相当于考试前偷偷看了答案训练时模型已经“偷窥”到测试集的分布规律最终评估结果自然会被人为抬高。这是学术研究里很忌讳的操作也是工程线上评估系统时容易疏忽的地方。手工实现KNN时还有一个经典坑如果你的训练集样本数不足K值代码会报IndexError。例如假设某一步把数据集切得非常小只有3条样本却把K设为5那“取前K个最近样本”这一步就会越界。实际业务中也偶尔会遇到某些类别样本本身就少于K的情况这时要么降低K值要么对少数类做重采样。这类问题在数据量小的时候尤其隐蔽。4.4 距离度量选型欧氏距离不是唯一选择KNN虽然默认使用欧氏距离但你完全可以换其他距离度量。sklearn的KNeighborsClassifier有个p参数p2就是欧氏距离p1就是曼哈顿距离# 曼哈顿距离 knn_manhattan KNeighborsClassifier(n_neighbors5, p1) # 闵可夫斯基距离p3或更高 knn_minkowski KNeighborsClassifier(n_neighbors5, p3)我在鸢尾花数据集上对比过欧氏距离和曼哈顿距离准确率差异很小因为特征维度只有4且没有极端离群点。但在高维稀疏特征场景中比如文本分类曼哈顿距离往往比欧氏距离更稳定因为欧洲距离的平方项会把维度之间的差异放大很容易受到个别特征剧烈变化的影响。这个知识点在后续做真实项目时会很有用。5. 项目扩展思路往后还能怎么玩KNN鸢尾花项目跑通之后千万不要停下脚步。这个项目最大的价值是提供了一个稳定的“骨架”你在它上面换数据、换算法、加功能就是在不断拓宽自己的能力。第一个扩展方向是尝试其他分类算法用同一份鸢尾花数据跑决策树、逻辑回归、支持向量机对比它们的准确率和分类报告。你会发现不同算法在同一份数据上表现各有千秋这能帮你建立“模型选择”的直觉。第二个方向是尝试多分类评估的深入分析。鸢尾花是三分类问题当你去看classification_report时会发现每个类别的精确率和召回率不完全一样。这是有现实意义的在医疗诊断场景中我们宁愿多一些“误报”也要降低漏诊率也就是更重视召回率在垃圾邮件过滤场景中我们宁愿误删一两个正常邮件也不愿意放过垃圾邮件。理解了这些你就学会了如何针对业务场景选择评估指标。第三个方向是换一个更大的真实数据集。sklearn里还有手写数字数据集digits、乳腺癌数据集breast_cancer直接用同样的代码框架就能跑。去UCI机器学习库找一个自己感兴趣的领域的数据集比如葡萄酒质量、成人收入、心脏病预测把数据处理流程完整走一遍这才是真正从教程走向实战的转折点。6. 写在最后的几点大实话如果你完整跑完了这个项目回头你会发现KNN分类本身并不难难的是理解数据、处理数据和评估模型这套方法论。我在带新人时经常说机器学习入门不是先学会各种花哨的算法而是先熟练掌握一套严谨的处理流程加载数据、查看结构、可视化、划分训练集测试集、预处理、建模、评估、调参、复盘。这套流程在鸢尾花数据集上练过一遍后今后做任何项目都不慌。我在实际跑这个项目的过程中还有一些经验想要分享。第一尽量在Notebook里分段执行代码每写一段就跑一段遇到报错能立刻定位比整个文件写完一次性跑通要省力得多。第二任何时候看到报错别慌把错误信息复制到搜索框里绝大多数常见错误都能找到解决方案我自己早期学的时候有一半时间都在查报错。第三改参数时要有意识保留之前的实验记录哪怕只是简单地在Notebook标题里写清楚“k5准确率96%”都能让你在调试时对当前选择有全局把握。如果你把这个项目跑下来不妨试着回答自己这几个问题为什么标准化能提升准确率为什么K值不是越大越好为什么训练集上的准确率不能作为模型上线依据如果能不看资料、用自己的话解释清楚那这个项目你就算真正吃透了。接下来就带着这套思维框架继续往前走。祝你在机器学习的路上少踩坑、多收获。本文还有配套的精品资源点击获取