☰
KNN算法与鸢尾花分类:机器学习入门完整实战教程
2026/9/26 5:03:10 网站建设 项目流程

简介:机器学习入门常让人感到无从下手,而掌握经典分类算法是建立数据思维的第一步。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替代你手动配置一大堆东西,能省下半天折腾时间。

具体安装步骤很简单:

  1. 去Anaconda官网下载对应你操作系统的安装包,一般选Python 3.x版本。Windows用户安装时注意勾选“Add Anaconda to my PATH environment variable”,Mac和Linux用户在终端操作。
  2. 安装完成后,打开Anaconda自带的Jupyter Notebook,新建一个Python 3的Notebook,直接开始写代码。
  3. 检查一下关键库是否齐全。在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 seaborn

2.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, columns=iris.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, hue='target_name', diag_kind='kde') plt.show()

注意观察这个图:山鸢尾(setosa)在花瓣长度和花瓣宽度的二维平面上,和另外两个品种是完全分开的,它独自占据一片区域,而变色鸢尾和维吉尼亚鸢尾有部分区域重叠。这个观察会直接影响你对算法效果的预判:即便只用花瓣特征,山鸢尾也几乎不可能被错分;但变色鸢尾和维吉尼亚鸢尾之间,一定会存在一些较难划分的边界样本。

还可以画一个箱线图来观察特征在不同类别间的分布差异:

plt.figure(figsize=(10, 6)) for i, feature in enumerate(iris.feature_names): plt.subplot(2, 2, i+1) sns.boxplot(x='target_name', y=feature, data=df) plt.tight_layout() plt.show()

箱线图能很直观地看出不同类别在某个特征上的区分度。比如花瓣长度这个特征,三个类别的箱体位置明显不同,说明这个特征对分类很有效;而花萼宽度这个特征,三个类别的箱体有很大的重叠区域,说明单靠它区分效果会很差。

3. 核心代码实现:从手写到调库

3.1 手写一个KNN分类器:彻底理解算法本质

为了让你彻底弄懂KNN到底在做什么,我建议先不要直接用sklearn,而是用numpy手写一个KNN分类器。代码量不大,逻辑非常清晰,核心就四步:计算距离、排序、取前K个、投票。

import numpy as np class KNN: def __init__(self, k=3): 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, axis=1)) # 第二步:按距离从小到大排序,返回索引 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(k=3) 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(..., axis=1)对每一行求和,np.sqrt开根号之后得到一个长度为150的一维向量,里面每一项就是当前测试样本到第i个训练样本的距离。这一段代码是整个KNN实现的核心,建议手写一遍。

3.2 用sklearn三分钟实现同一件事

手写一遍理解原理之后,实际项目中用现成库就方便多了。scikit-learn提供了完整的KNN实现:

from sklearn.neighbors import KNeighborsClassifier knn_sk = KNeighborsClassifier(n_neighbors=3) knn_sk.fit(X_train, y_train) y_pred = knn_sk.predict(X_test)

就这两行,和手写版本的逻辑完全一致。KNeighborsClassifier的默认距离度量就是欧氏距离,默认权重是均匀投票,也就是说每个邻居对最终投票贡献相同。如果你想最近的点权重更大,可以传入weights='distance',让距离越近的邻居投出的票权重越高,这在很多场景下都能带来小幅的准确率提升。

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_size=0.3, random_state=42, stratify=y ) # 3. 标准化 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 4. 训练KNN模型 knn = KNeighborsClassifier(n_neighbors=5) 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_state=42可以保证每次运行得到相同的训练集和测试集划分,这让模型结果可复现。如果你不设置这个参数,每次跑代码的划分都会不同,模型准确率也会小幅波动,新手容易误以为算法不稳定。

而stratify=y参数的作用是让训练集和测试集中三类鸢尾花的比例尽量接近原始数据集。在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_names=iris.target_names))

把测试集有45条样本的情况下跑出来的混淆矩阵拿来看,你会发现一种典型模式:对角线上的数字大,说明每个类别都被正确分类;如果对角线以外的数字集中出现在某个位置,比如把virginica误判为versicolor,解释为这两个品种本身在特征空间里就存在重叠区域。可视化混淆矩阵也有助于解释:

import matplotlib.pyplot as plt import seaborn as sns cm = confusion_matrix(y_test, y_pred) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=iris.target_names, yticklabels=iris.target_names) plt.xlabel('预测类别') plt.ylabel('真实类别') plt.show()

classification_report会给出每个类别的精确率(precision)、召回率(recall)和F1分数,这些都是分类任务里的核心指标。从实操角度看,我建议在任何一个分类项目里都养成看这三样东西的习惯:准确率看整体、混淆矩阵看错在哪、分类报告看每个类别的精细表现。

4.2 K值到底怎么选:从K=1试到K=15,看曲线找拐点

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_neighbors=k) scores = cross_val_score(knn, X_train, y_train, cv=5) 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)

我实际跑出来的结果在K=5到K=9之间比较平稳,准确率都在96%左右。选择K值时,除非某一点明显优于其他值很多,否则我会选一个中间的“安全值”,而不是单纯选准确率最高的那个。因为交叉验证本身也有随机性,两个K值之间的准确率差距在1%以内属于正常波动,没必要为了这1%选择更复杂的模型。

4.3 新手最容易踩的5个坑

我把朋友们在跑这个项目时问得最多的问题整理成了一张表,希望对你有用。

问题原因解决办法
不标准化直接训练,准确率低特征量纲不同,距离被数值大的特征主导先fit_transform训练集,再transform测试集
先标准化后划分数据集数据泄露,测试集信息被提前用到先train_test_split,再fit_transform
random_state不固定,每次结果不一样随机划分导致训练集测试集不同设置random_state=42之类的固定值
K设为偶数,二分类平票投票结果可能打平用奇数K值
直接使用训练集评估模型模型已经见过了训练数据,评估结果虚高必须用没见过的测试集评估

上述问题里,我特别想展开讲数据泄露这个概念。很多人不理解“标准化一下测试集怎么了”,但本质上测试集代表的是未来可能出现的新数据,你在处理真实业务时,根本不可能提前知道新数据的均值和标准差。如果标准化时用了测试集的信息,相当于考试前偷偷看了答案,训练时模型已经“偷窥”到测试集的分布规律,最终评估结果自然会被人为抬高。这是学术研究里很忌讳的操作,也是工程线上评估系统时容易疏忽的地方。

手工实现KNN时还有一个经典坑:如果你的训练集样本数不足K值,代码会报IndexError。例如,假设某一步把数据集切得非常小,只有3条样本,却把K设为5,那“取前K个最近样本”这一步就会越界。实际业务中也偶尔会遇到某些类别样本本身就少于K的情况,这时要么降低K值,要么对少数类做重采样。这类问题在数据量小的时候尤其隐蔽。

4.4 距离度量选型:欧氏距离不是唯一选择

KNN虽然默认使用欧氏距离,但你完全可以换其他距离度量。sklearn的KNeighborsClassifier有个p参数,p=2就是欧氏距离,p=1就是曼哈顿距离:

# 曼哈顿距离 knn_manhattan = KNeighborsClassifier(n_neighbors=5, p=1) # 闵可夫斯基距离,p=3或更高 knn_minkowski = KNeighborsClassifier(n_neighbors=5, p=3)

我在鸢尾花数据集上对比过,欧氏距离和曼哈顿距离准确率差异很小,因为特征维度只有4,且没有极端离群点。但在高维稀疏特征场景中,比如文本分类,曼哈顿距离往往比欧氏距离更稳定,因为欧洲距离的平方项会把维度之间的差异放大,很容易受到个别特征剧烈变化的影响。这个知识点在后续做真实项目时会很有用。

5. 项目扩展思路:往后还能怎么玩

KNN鸢尾花项目跑通之后,千万不要停下脚步。这个项目最大的价值是提供了一个稳定的“骨架”,你在它上面换数据、换算法、加功能,就是在不断拓宽自己的能力。

第一个扩展方向是尝试其他分类算法,用同一份鸢尾花数据跑决策树、逻辑回归、支持向量机,对比它们的准确率和分类报告。你会发现不同算法在同一份数据上表现各有千秋,这能帮你建立“模型选择”的直觉。

第二个方向是尝试多分类评估的深入分析。鸢尾花是三分类问题,当你去看classification_report时,会发现每个类别的精确率和召回率不完全一样。这是有现实意义的:在医疗诊断场景中,我们宁愿多一些“误报”也要降低漏诊率,也就是更重视召回率;在垃圾邮件过滤场景中,我们宁愿误删一两个正常邮件也不愿意放过垃圾邮件。理解了这些,你就学会了如何针对业务场景选择评估指标。

第三个方向是换一个更大的真实数据集。sklearn里还有手写数字数据集digits、乳腺癌数据集breast_cancer,直接用同样的代码框架就能跑。去UCI机器学习库找一个自己感兴趣的领域的数据集,比如葡萄酒质量、成人收入、心脏病预测,把数据处理流程完整走一遍,这才是真正从教程走向实战的转折点。

6. 写在最后的几点大实话

如果你完整跑完了这个项目,回头你会发现,KNN分类本身并不难,难的是理解数据、处理数据和评估模型这套方法论。我在带新人时经常说,机器学习入门不是先学会各种花哨的算法,而是先熟练掌握一套严谨的处理流程:加载数据、查看结构、可视化、划分训练集测试集、预处理、建模、评估、调参、复盘。这套流程在鸢尾花数据集上练过一遍后,今后做任何项目都不慌。

我在实际跑这个项目的过程中还有一些经验想要分享。第一,尽量在Notebook里分段执行代码,每写一段就跑一段,遇到报错能立刻定位,比整个文件写完一次性跑通要省力得多。第二,任何时候看到报错别慌,把错误信息复制到搜索框里,绝大多数常见错误都能找到解决方案,我自己早期学的时候有一半时间都在查报错。第三,改参数时要有意识保留之前的实验记录,哪怕只是简单地在Notebook标题里写清楚“k=5准确率96%”,都能让你在调试时对当前选择有全局把握。

如果你把这个项目跑下来,不妨试着回答自己这几个问题:为什么标准化能提升准确率?为什么K值不是越大越好?为什么训练集上的准确率不能作为模型上线依据?如果能不看资料、用自己的话解释清楚,那这个项目你就算真正吃透了。接下来,就带着这套思维框架,继续往前走。祝你在机器学习的路上少踩坑、多收获。

本文还有配套的精品资源,点击获取

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

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

立即咨询