KNN算法以及鸢尾花案例
KNN 是一个适合机器学习入门的算法:遇到一个未知样本,就在已知样本中找出与它最相似的几个,再根据这些邻居的答案做判断。
本文以鸢尾花分类为例,记录从加载数据、划分数据集,到标准化、训练、预测和评估的完整过程,并解释 fit()、predict() 和 score() 分别做了什么。
一、KNN 算法是什么?
KNN 全称是 K-Nearest Neighbors,中文叫 K 近邻算法。K 表示参考的邻居数量。
本文讨论的是 KNN 分类:给定一条待预测样本,查找训练集中距离它最近的 K 条样本,根据这些邻居的类别投票。
例如 K=3,最近的三个邻居分别属于:
1 | 邻居 A → 类别 1 |
采用每个邻居一票的规则时,类别 1 获得两票,因此预测结果是类别 1。
KNN 也可以用于回归,通常根据邻居的目标值求平均或加权平均;本文先关注分类。
1. 到底按什么排序?
刚接触 KNN 时,一个容易出现的误解是:“是不是把每个特征分别升序排列?”
实际上,比较的是每条训练样本与待预测样本之间的距离,各个特征列不会被单独排序。 每条样本的全部特征和标签始终对应在一起。
为了方便理解,假设每个样本只有两个特征,待预测样本是 Q=(5, 3):
| 训练样本 | 特征 | 真实类别 | 到 Q 的欧氏距离 |
|---|---|---|---|
| A | (6, 3) | 0 | 1 |
| B | (5, 5) | 0 | 2 |
| C | (8, 7) | 1 | 5 |
按照距离由近到远,顺序是 A、B、C。若取这三个邻居,类别 0 获得两票,预测为类别 0。
换一条待预测样本,计算出来的距离和邻居顺序也可能改变。实际实现只需要找到最近的 K 个邻居,不一定把全部训练样本完整排序。
2. 距离如何计算?
欧氏距离就是“两点间直线距离”向多个特征的推广。两个特征时:
1 | d = √[(x₁ - q₁)² + (x₂ - q₂)²] |
例如 C=(8, 7) 与 Q=(5, 3):
1 | d = √[(8 - 5)² + (7 - 3)²] |
鸢尾花有四个特征,因此将四个对应特征的差平方相加,再开根号:
1 | d = √[(x₁-q₁)² + (x₂-q₂)² + (x₃-q₃)² + (x₄-q₄)²] |
本文采用 KNeighborsClassifier(n_neighbors=3) 的默认距离配置,即欧氏距离,并使用默认的等权投票。参数说明
3. K 的大小有什么影响?
- K 较小时,预测更容易受到个别样本和噪声影响,可能过拟合。
- K 较大时,会参考更大范围的样本,可能忽略局部差异,导致欠拟合。
K=3 是本案例的练习设置,不代表所有任务中都最合适。正式选择 K 时,应使用验证集或交叉验证,测试集保留给最后评估。
二、认识鸢尾花数据集
鸢尾花数据集包含 150 条样本,每条样本有 4 个特征,目标是预测其所属的三个类别之一。
| 特征名 | 含义 |
|---|---|
| sepal length (cm) | 花萼长度 |
| sepal width (cm) | 花萼宽度 |
| petal length (cm) | 花瓣长度 |
| petal width (cm) | 花瓣宽度 |
标签与类别名对应如下:
| 标签 | 类别名 |
|---|---|
| 0 | setosa |
| 1 | versicolor |
| 2 | virginica |
这些数字是类别编号,没有“类别 2 比类别 1 更大”的含义。
load_iris() 加载的是完整数据集,随后才划分训练集和测试集。不要把加载完整数据集的注释写成“加载测试集”。
三、为什么 KNN 需要关注标准化?
距离公式会把各个特征的差值平方后相加。如果某一列数值变化很大,它就容易主导距离,其他特征的影响可能被掩盖。
即使各列单位都是厘米,各列的变化范围也可能不同。因此,标准化是在调整距离计算中各个特征的尺度,不只是让代码流程更完整;是否有助于具体任务,仍需要验证。
标准化按列进行:
1 | 标准化后的值 = (当前值 - 训练集该列均值) / 训练集该列标准差 |
代码是:
1 | transfer = StandardScaler() |
这里两个方法的区别是:
| 方法 | 做什么 |
|---|---|
fit_transform(x_train) |
计算并保存训练集每列的均值和标准差,然后转换训练集 |
transform(x_test) |
使用之前保存的训练集统计量转换测试集 |
测试集和未来的新数据,都必须使用训练集确定的同一套标准化规则。 如果在测试集上再次 fit_transform(),就会重新计算统计量,既使用了测试集信息,也会让训练和预测的尺度不一致。
标准化并不会把任意分布变成正态分布,也不是只适用于大数据集。对于训练集中非恒定的列,默认标准化后均值约为 0、方差约为 1;测试集的均值和方差不必恰好是 0 和 1。StandardScaler 文档
四、KNN 的 fit() 算是在训练吗?
下面这一行完成了 scikit-learn 接口意义上的模型拟合:
1 | estimator.fit(x_train, y_train) |
但不同模型的 fit() 做的事并不相同。
| 模型 | 拟合阶段的主要工作 |
|---|---|
| 线性回归、逻辑回归 | 根据训练数据学习参数,使相应目标函数尽量小 |
| KNN | 保存训练样本和标签,并根据搜索方式准备邻居查找结构 |
KNN 不需要像逻辑回归那样,通过梯度下降反复调整一组 w、b。它将针对具体新样本的邻居查找留到了预测阶段,这也是它被称为“惰性学习”的原因。
不过,不能说 fit() 完全没有计算:它仍然要检查数据,并可能建立搜索结构。
1 | fit():准备好参考样本与答案 |
五、完整案例代码
在当前 Python 环境中安装依赖:
1 | python -m pip install scikit-learn pandas seaborn matplotlib |
下面保留四个演示函数:加载数据、绘图、划分数据、完成分类流程。运行时默认执行第四个函数;需要查看图表时,取消对应函数调用前的注释即可。
1 | from sklearn.datasets import load_iris |
这里把原来绘图中的 fit_reg=True 改成了 False:目的是观察散点,不额外拟合回归直线。该图只展示两个特征,后面的 KNN 仍然使用全部四个特征。
六、逐步理解关键代码
1. 划分数据后的四个变量
1 | x_train, x_test, y_train, y_test = train_test_split( |
| 变量 | 含义 | 本例形状 |
|---|---|---|
x_train |
训练集特征 | (120, 4) |
x_test |
测试集特征 | (30, 4) |
y_train |
训练集真实标签 | (120,) |
y_test |
测试集真实标签 | (30,) |
150 是样本数量,4 才是特征数量。x_train[0] 的正确答案是 y_train[0],特征与标签会对应划分。
random_state=23 用来固定随机划分,方便复现实验。23 本身没有特殊含义,也不应该通过反复换随机种子来挑选最好看的测试分数。
本例保留原来的划分设置。分类任务也常添加 stratify=iris_data.target,使各类别在训练集和测试集中的比例更接近原数据;添加后具体划分和结果可能变化。
2. 为什么新样本使用两层方括号?
1 | my_data = [[7.8, 2.1, 3.9, 1.6]] |
模型接收的是二维特征表:每一行是一条样本,每一列是一个特征。因此一朵花也需要写成“一行、四列”,不能省掉外层方括号。
新样本不知道真实类别,也不需要先提供标签。模型正是要根据这四个特征预测类别。
3. predict_proba() 的概率从哪里来?
在本例的等权投票规则下,类别概率估计来自最近 K 个邻居中各类别的占比。
例如最近三个邻居的标签为 [1, 1, 2],那么类别 0、1、2 的概率估计依次是:
1 | [0/3, 2/3, 1/3] |
这只是投票原理的示例,不是对代码中新样本运行结果的预告。概率列的类别顺序应查看 estimator.classes_。
即使三个邻居全部投给同一类,输出概率为 1,也只表示这些邻居一致,不代表现实中的分类绝对不会出错。
4. 两种模型评估方式有什么区别?
第一种,让模型自己预测再评分:
1 | estimator.score(x_test, y_test) |
第二种,直接比较已经得到的预测标签与真实标签:
1 | y_pred = estimator.predict(x_test) |
对于这里的 KNN 分类器,同一份数据、同一个模型下,两者得到相同的准确率:
1 | 准确率 = 预测正确的样本数 / 测试样本总数 |
假设 30 条测试样本预测对 28 条,准确率就是 28/30,约为 93.33%。这是计算示例,实际分数以程序输出为准。
score() 会重新执行预测,不会自动使用之前的 y_pred。因此已有预测结果时,使用 accuracy_score() 可以省去一次重复预测。其他模型的 score() 不一定代表准确率,例如回归模型通常返回 R²。
5. 两个 fit() 不要混淆
代码中有两类对象,它们都使用了拟合接口:
| 对象 | 拟合时确定什么 |
|---|---|
transfer:标准化器 |
训练数据每列的均值和缩放尺度 |
estimator:KNN 分类器 |
保存训练样本、标签,准备查找邻居 |
前者负责把数据转换到一致的尺度,后者负责分类。预测新样本时,两个对象都需要保留并配套使用。
七、这个案例之后可以练习什么?
先尝试不看代码,独立写出下面这条流程:
1 | 加载数据 → 划分训练集和测试集 → 仅用训练集拟合标准化器 |
接下来可以尝试打印新样本的邻居距离与标签,验证“近邻投票”到底在参考哪些样本:
1 | # 放在 demo04_iris_evaluate_test() 中,创建 my_data_scaled 之后 |
这里的位置指向传入 fit() 的训练集行号,不是原始完整数据集的行号。距离是在标准化后的四维特征空间中计算的。
KNN 的优点是直观、容易实现;局限是需要保留训练数据,数据规模变大时预测可能变慢,而且容易受特征尺度、无关特征和高维数据影响。
学完这个案例,应当能够解释:模型参考什么数据、为什么要标准化、预测时如何找邻居,以及准确率是怎样算出来的。能把这些问题讲清楚,比仅仅记住 fit() 和 predict() 两个函数名更有用。



