K 近邻算法 KNN:基于距离的懒惰学习
K 近邻算法(K-Nearest Neighbors,KNN):基于距离的懒惰学习
K 近邻算法(KNN)是机器学习中最简单、最直观的算法之一。它既可以用于分类,也可以用于回归。之所以被称为“懒惰学习”,是因为它在训练阶段几乎不做任何计算,只是把数据“记住”,直到需要进行预测时才真正开始工作——通过寻找与新样本最相似的 K 个邻居来做决定。本教程将带你从零理解 KNN 的核心理念、关键参数、实际应用以及优缺点,并提供可运行的代码示例,帮助你快速上手。
什么是 KNN?核心思想与直观理解
KNN 的核心假设非常简单:“相似的事物往往具有相似的性质”。如果你不知道一个新数据点的类别,那么就看看离它最近的几个已知类别的点都是什么,然后用“少数服从多数”的原则来决定它的类别。
想象你在一张地图上,地图上分布着红色和蓝色的点,分别代表两类事物。现在出现了一个新的点(绿色),你不知道它是红还是蓝。你会怎么做?最自然的做法就是看看离它最近的几个邻居:如果大多数邻居是红色的,那就认为它也是红色;反之则是蓝色。这就是 KNN 的基本逻辑。
如何定义“近”?——距离度量
KNN 的效果高度依赖于“距离”的定义。常见的距离度量方式有:
- 欧氏距离:最直观的“直线距离”,适用于连续型特征。
公式:$d(\mathbf{p},\mathbf{q}) = \sqrt{\sum_{i=1}^{n} (p_i - q_i)^2}$ - 曼哈顿距离:城市街区距离,计算各坐标差绝对值之和,对异常值更鲁棒。
公式:$d(\mathbf{p},\mathbf{q}) = \sum_{i=1}^{n} |p_i - q_i|$ - 闵可夫斯基距离:前两者的泛化,当参数 $p=2$ 时为欧氏距离,$p=1$ 时为曼哈顿距离。
- 余弦相似度:测量向量方向上的相似性,常用于文本分析等高维稀疏数据。
选择距离度量时,通常默认使用欧氏距离,但需要记住:不同距离度量方式对特征的量纲非常敏感,因此在使用 KNN 之前,通常需要对特征进行标准化或归一化处理。
K 值的选择:算法的关键超参数
K 是 KNN 中最重要的参数,它决定了“多少个邻居参与投票”。K 值过小或过大都会带来问题:
- K 太小(例如 K=1):模型会变得非常敏感,容易受到噪声影响,导致过拟合。仅仅因为一个靠得很近的嘈杂点就完全改变预测结果。
- K 太大:模型会趋向于考虑较远的点,可能导致决策边界过于平滑,丧失捕捉局部模式的能力,造成欠拟合。极端情况下,K 等于训练集大小,则无论输入是什么,都只会预测出现次数最多的类别。
如何选择 K?
- 通常选择奇数 K(尤其在二分类中),以避免平局。
- 通过交叉验证来寻找最优 K 值。一般从较小的 K(如 1、3、5)开始尝试,观察验证集上的性能变化曲线,选择使误差最低的 K。
- 常见的经验法则是让 $K = \sqrt{N}$,其中 $N$ 是训练样本总数,但这只是一个起点,需要根据实际情况调整。
分类与回归:KNN 的双重身份
KNN 分类器
对于分类任务,KNN 找到离待预测点最近的 K 个点,然后进行“多数表决”。预测的类别就是这 K 个邻居中出现次数最多的那个类别。
如果数据集的类别分布不均衡,可以使用加权投票:根据邻居到待预测点的距离赋予不同权重(例如距离的倒数),让更近的邻居有更大的话语权。Scikit-learn 中通过 weights='distance' 参数即可实现。
KNN 回归器
在回归任务中,KNN 预测的目标是邻居目标值的(加权)平均值。同样可以引入基于距离的加权,距离越近的邻居对预测值贡献越大。
示例:预测房价时,找到与目标房屋特征最相似的 K 套已售房屋,将这些房屋成交价的平均值作为预测价格。如果使用距离加权,则离目标更近的房屋价格权重更高。
数据预处理:为什么特征缩放至关重要
由于 KNN 直接计算样本间的距离,不同特征如果量纲差异过大会导致距离计算被大数值特征主导。例如,在预测健康状况时,“年龄”特征的数值范围是 0-100,而“年收入”的范围可能是几万到几十万,收入对欧氏距离的影响将远大于年龄,即便年龄更重要。
因此,在应用 KNN 之前必须进行特征缩放。常用的方法有:
- 归一化(Min-Max Scaling):将特征缩放到 [0,1] 区间。
- 标准化(Standardization):将特征转换为均值为 0、标准差为 1 的分布,适合数据包含异常值的情况。
规则:首先对全体数据完成训练/测试划分,然后仅使用训练集的统计量去缩放训练集和测试集,以避免数据泄露。
实践:用 Python 实现 KNN
这里使用 Scikit-learn 库演示 KNN 分类器和回归器的基本用法。假设已有经过预处理的数据集 X_train, X_test, y_train, y_test。
KNN 分类器示例
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
# 假设 X, y 是已加载的特征和标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 特征标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 训练 KNN 分类器 (K=5,使用距离加权)
knn_clf = KNeighborsClassifier(n_neighbors=5, weights='distance')
knn_clf.fit(X_train_scaled, y_train) # 实际上只是“记住”数据
# 预测
y_pred = knn_clf.predict(X_test_scaled)
print(classification_report(y_test, y_pred))
KNN 回归器示例
from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import mean_absolute_error, r2_score
# 标准化同样重要
knn_reg = KNeighborsRegressor(n_neighbors=7, weights='uniform')
knn_reg.fit(X_train_scaled, y_train)
y_pred = knn_reg.predict(X_test_scaled)
print("MAE:", mean_absolute_error(y_test, y_pred))
print("R²:", r2_score(y_test, y_pred))
懒惰学习的含义与影响
KNN 被称为懒惰学习器,与其相对的则是急切学习器(如线性回归、决策树)。懒惰学习的特点:
- 训练阶段几乎没有计算开销,仅仅是存储数据。
- 所有的计算都推迟到预测阶段,对于每个新样本都需要在整个训练集中搜索最近邻。
- 因此,预测速度慢,并且随着训练集增大,预测成本和内存占用都会线性增长。
- 懒惰学习使得模型能够快速适应新数据(只需添加新的训练点),适合需要频繁更新的场景。
因为预测时要扫描全部训练数据,KNN 对高维数据或大规模数据集的表现会显著下降,这就是所谓的“维数灾难”。高维空间中距离变得不区分,所有点看起来都差不多远,KNN 的效果会大打折扣。
KNN 的适用场景与局限
适用场景
- 数据集较小或中等规模。
- 特征维度较低(通常建议几十维以内)。
- 数据呈现清晰的局部聚类结构。
- 需要快速构建基线模型或进行可解释性分析(可以解释“因为哪几个邻居”)。
- 推荐系统中,基于相似用户或相似物品的协同过滤。
局限性
- 对噪声和不相关特征敏感。
- 高维数据上表现差,需要降维或特征选择。
- 预测时间可能成为瓶颈,不适合低延迟要求的大规模在线服务。
- 没有明确的模型训练过程,无法提取规则或特征重要性。
- 不平衡数据集需要通过过采样/欠采样或距离加权来缓解。
实战技巧与调优
- 特征工程:去除无关特征、组合特征或使用主成分分析(PCA)降维,可以极大提升 KNN 的性能。
- 距离度量选择:对于文本或高维稀疏向量,使用余弦相似度往往更合适。
- 加速搜索:预测时采用如 KD-Tree 或 Ball Tree 等数据结构可以避免线性扫描(Scikit-learn 的 KNN 类默认在训练集小于一定规模时使用暴力搜索,否则自动选择树结构,可通过
algorithm参数调整)。 - 处理缺失值:KNN 要求完整的特征值,预测前必须填补或删除缺失值。
- 加权机制:在数据集包含噪声时,距离加权(
weights='distance')通常能提高鲁棒性,但也会增加对局部异常值的敏感度。
总结
KNN 是一种基于实例的学习方法,其核心在于“相似性”和“多数原则”。它的简单与直观使其成为入门机器学习的绝佳起点,同时也是快速建立性能基线的不错选择。但它的性能强依赖于良好的数据预处理、合适的 K 值以及距离度量的选择。理解了这些要素,你就能在实际问题中灵活驾驭 KNN,并把它作为你数据科学工具箱里的一个锋利武器。