第 38 课:K 近邻算法(KNN)
1. 学习目标
通过本节课的学习,你将能够:
- 理解 K 近邻算法的核心思想和“近朱者赤”的决策逻辑。
- 掌握 KNN 分类算法的工作流程和关键参数(K值、距离度量)。
- 使用 Python 的
scikit-learn库构建、训练和评估一个 KNN 分类模型。 - 应用 特征缩放(标准化)来优化 KNN 模型的性能。
- 评估 不同 K 值对模型效果的影响,并选择合适的 K。
2. 核心概念
什么是 KNN? K 近邻算法(K-Nearest Neighbors)是一种简单、直观的监督学习算法。它既可以用于分类,也可以用于回归。今天我们聚焦于分类任务。
它的核心思想非常朴素,就像一句古话:“物以类聚,人以群分”。想象一下,在一个房间里,你不知道自己属于哪个群体,但你知道你身边最常接触的 K 个朋友分别属于哪些群体。那么,你最有可能属于人数最多的那个群体。
算法步骤(分类):
- 存储:算法在训练阶段不会构建明确的模型,只是简单地“记住”所有训练数据(样本及其对应的标签)。这是它被称为“实例-based”或“懒惰学习”算法的原因。
- 计算:当需要预测一个新样本的类别时,算法会计算该样本与所有训练样本之间的“距离”。
- 找邻居:找出距离新样本最近的 K 个训练样本(即“K 个邻居”)。
- 投票:查看这 K 个邻居的类别标签。新样本的类别被预测为这 K 个邻居中出现次数最多的类别(即“多数表决”)。
关键参数与决策:
- K 值:邻居的数量。这是一个需要预先设定的超参数。
- K 值太小(如 K=1),模型容易受到噪声点的影响,导致“过拟合”。
- K 值太大(如 K 接近所有样本数),模型会变得非常简单,可能忽略数据中的模式,导致“欠拟合”。
- 通常通过交叉验证来寻找一个合适的 K 值(奇数通常更好,以避免平票)。
- 距离度量:如何衡量样本之间的“相似度”或“远近”?最常用的是欧氏距离(二维/三维空间中的直线距离)。对于特征,也常用曼哈顿距离、闵可夫斯基距离等。
- 特征缩放:由于 KNN 完全依赖于距离计算,如果不同特征的量纲(单位)和范围差异巨大,那么“距离”将主要被数值范围大的特征主导。因此,在使用 KNN 之前,对数据进行标准化(如使用
StandardScaler)是至关重要的一步。
3. 代码示例
我们将使用 scikit-learn 中的鸢尾花(Iris)数据集演示一个完整的 KNN 分类流程。
# 导入必要的库
import numpy as np
import matplotlib.pyplot as plt
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, classification_report, confusion_matrix
# 1. 加载数据
iris = load_iris()
X = iris.data # 特征矩阵 (150个样本,4个特征)
y = iris.target # 目标向量 (类别标签: 0, 1, 2)
# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
# stratify=y 确保训练集和测试集中各类别的比例与原始数据一致
# 3. 特征缩放(关键步骤!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 在训练集上计算均值和方差,并转换
X_test_scaled = scaler.transform(X_test) # 使用训练集的参数转换测试集
# 4. 创建并训练 KNN 模型
# 选择 K=5 作为起点(一个经验上的小奇数)
knn = KNeighborsClassifier(n_neighbors=5, metric='euclidean')
knn.fit(X_train_scaled, y_train)
# 5. 在测试集上进行预测
y_pred = knn.predict(X_test_scaled)
# 6. 评估模型
print("测试集准确率:", accuracy_score(y_test, y_pred))
print("\n分类报告:\n", classification_report(y_test, y_pred, target_names=iris.target_names))
print("混淆矩阵:\n", confusion_matrix(y_test, y_pred))
# 7. (可选)可视化不同 K 值对准确率的影响
k_range = range(1, 31)
train_scores = []
test_scores = []
for k in k_range:
knn_temp = KNeighborsClassifier(n_neighbors=k, metric='euclidean')
knn_temp.fit(X_train_scaled, y_train)
train_scores.append(knn_temp.score(X_train_scaled, y_train))
test_scores.append(knn_temp.score(X_test_scaled, y_test))
plt.figure(figsize=(10, 6))
plt.plot(k_range, train_scores, label='Training Accuracy')
plt.plot(k_range, test_scores, label='Testing Accuracy')
plt.xlabel('Number of Neighbors (K)')
plt.ylabel('Accuracy')
plt.title('KNN Varying number of neighbors')
plt.legend()
plt.grid(True)
plt.show()
4. 实践练习
练习 1(基础):手写数字识别入门
使用 scikit-learn 自带的 load_digits 数据集(8x8 像素的手写数字图像,共10类)。
- 加载数据,划分训练集/测试集。
- 对特征进行标准化。
- 使用
KNeighborsClassifier(默认 K=5) 进行训练和预测。 - 输出模型在测试集上的准确率。
练习 2(进阶):参数调优
继续使用 load_digits 数据集。
- 编写一个循环,测试 K 从 1 到 20(奇数)时,模型在测试集上的准确率。
- 找出并打印出使得测试集准确率最高的 K 值及其对应的准确率。
- 绘制 K 值与准确率的关系图。
练习 3(应用):特征工程的影响
- 重复练习 1 的步骤,但跳过特征标准化步骤。
- 对比使用标准化和不使用标准化两种情况下,模型的测试集准确率。
- 思考并简述:为什么标准化对 KNN 特别重要?
5. 常见错误
- 忘记特征缩放:这是 KNN 最常见的错误。如果不缩放,模型会严重偏向数值范围大的特征,导致性能不佳。
- K 值选择不当:
- 直接使用默认的 K=5 而不进行任何验证。
- 选择偶数的 K 值,在二分类问题中可能导致平票(虽然
scikit-learn有自己的平票处理策略,但最好是避免)。
- 数据泄露:在进行特征缩放(如标准化)时,错误地在整个数据集(包含测试集)上计算均值和方差。正确做法是:
fit仅在训练集上进行,然后用相同的参数去transform测试集。 - 忽略计算成本:KNN 的预测阶段需要计算与所有训练样本的距离,当训练集非常大时,预测会非常慢。这是它“懒惰”学习的一个代价。
- 维度灾难:当特征维度非常高时,基于距离的度量会变得不那么可靠(所有点之间的距离可能变得相似),KNN 的性能会下降。此时可能需要进行降维处理。
6. 小结
- KNN 核心:通过查找新样本在特征空间中最近的 K 个邻居,根据邻居的类别进行多数表决来预测。
- 关键三要素:
- K 值:控制模型复杂度,需通过交叉验证选择。
- 距离度量:通常使用欧氏距离。
- 特征缩放:必须进行,以消除特征量纲的影响。
- 实现流程:加载数据 -> 划分数据集 -> 特征缩放 -> 创建模型 -> 训练(只是存储数据) -> 预测 -> 评估。
- 优点:简单直观,易于理解和实现;无需训练过程(“懒惰”);对异常值不敏感(除非 K 值很小)。
- 缺点:预测计算量大;存储开销大;在高维空间中性能下降;对数据不平衡敏感(如果某个类别的样本特别多,它更容易成为“邻居”)。
- 下一步:下一课我们将学习另一种强大的分类算法——决策树,它通过一系列问题来对样本进行划分,思路与 KNN 截然不同。
练习编辑器
rust
Loading...