k 近邻算法
我们来系统性地讲解 k 近邻算法(k-Nearest Neighbors, KNN)。我会从直观理解、核心原理、关键细节、代码示例到优缺点,层层递进。
一、直观理解:近朱者赤,近墨者黑
KNN 可能是机器学习中最简单、最直观的算法。它的核心思想只有一句话:
要判断一个新样本的类别或数值,就去看它在训练数据中最相似的 k 个邻居是谁,然后让这些邻居投票决定。
- 分类问题:少数服从多数
- 回归问题:取邻居的平均值
它不需要显式的训练过程,属于惰性学习算法——所有计算都推迟到预测时进行。
二、算法原理
以分类问题为例:
- 选择 k 值:确定要考虑的邻居数量
- 计算距离:计算新样本与所有训练样本的距离
- 找邻居:选出距离最近的 k 个样本
- 投票决策:统计这 k 个邻居的类别,把新样本归为出现次数最多的类别
- (回归)求均值:回归问题则计算 k 个邻居目标值的平均数
常用距离度量
- 欧氏距离(最常用):适用于连续特征\[d = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2} \]
- 曼哈顿距离:适用于高维或网格状数据\[d = \sum_{i=1}^{n}|x_i - y_i| \]
- 闵可夫斯基距离:欧氏和曼哈顿的泛化形式
- 余弦相似度:常用于文本、高维稀疏数据
三、关键参数与核心问题
1. k 值的选择——算法成败的关键
- k 太小(如 k=1):对噪声异常敏感,容易过拟合,决策边界崎岖
- k 太大(如 k=N):模型过于平滑,失去对细节的捕捉能力,容易欠拟合
- 经验做法:通常选奇数(避免投票平局),通过交叉验证在 [1, 30] 范围内寻找最优值
2. 距离的尺度陷阱
如果特征量纲不一致(如“收入”用元、“年龄”用岁),量级大的特征会主导距离计算。
必须做的事:标准化(Z-score)或归一化(Min-Max),让所有特征在相同尺度上。
3. 决策规则的变体
- 距离加权投票:让离得近的邻居权重更大(如权重 = 1/distance),这是最常见的改进
- 解决平局:减小 k 值、增加 k 值或使用距离加权
4. 计算复杂度
- 预测时间复杂度:O(N × D),N 为训练样本数,D 为特征维度
- 这是 KNN 的主要瓶颈,面对大规模数据需要 KD-Tree、Ball-Tree 等索引结构加速
四、Python 实现(从零手写 + sklearn 实战)
手写 KNN 分类器
import numpy as np
from collections import Counter
class KNNClassifier:
def __init__(self, k=3, distance='euclidean'):
self.k = k
self.distance = distance
def fit(self, X, y):
self.X_train = X
self.y_train = y
def predict(self, X):
return np.array([self._predict_one(x) for x in X])
def _predict_one(self, x):
# 计算所有距离
if self.distance == 'euclidean':
distances = np.sqrt(np.sum((self.X_train - x) ** 2, axis=1))
elif self.distance == 'manhattan':
distances = np.sum(np.abs(self.X_train - x), axis=1)
# 获取 k 个最近邻居的索引
k_indices = np.argsort(distances)[:self.k]
k_labels = self.y_train[k_indices]
# 多数投票
most_common = Counter(k_labels).most_common(1)
return most_common[0][0]
sklearn 实战(带数据预处理)
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.datasets import load_iris
# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 关键步骤:标准化!
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 寻找最优 k 值
best_k, best_score = 0, 0
for k in range(1, 31):
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X_train_scaled, y_train, cv=5)
if scores.mean() > best_score:
best_k, best_score = k, scores.mean()
print(f"最优 k 值: {best_k}, 交叉验证准确率: {best_score:.3f}")
# 训练最终模型
final_knn = KNeighborsClassifier(n_neighbors=best_k, weights='distance')
final_knn.fit(X_train_scaled, y_train)
print(f"测试集准确率: {final_knn.score(X_test_scaled, y_test):.3f}")
五、优缺点与应用场景
| 优点 | 缺点 |
|---|---|
| 简单直观,无需训练 | 预测慢,计算量大 |
| 没有假设数据分布 | 需存储全部训练数据 |
| 多分类问题天然支持 | 对高维数据效果差(维度灾难) |
| 新数据加入容易 | 对不平衡数据敏感 |
| 只有一个超参数 k | 缺失值处理麻烦 |
典型应用场景
- 推荐系统:找相似用户/物品
- 小规模图像分类:手写数字识别(MNIST 入门)
- 异常检测:远离邻居的点可能是异常
- 缺失值填补:用近邻的均值或众数填补
- 快速基线模型:评估问题难度
六、进阶优化方向
- 加速技术:KD-Tree、Ball-Tree、LSH(局部敏感哈希)—— sklearn 中设置
algorithm='ball_tree'即可 - 降维配合:先用 PCA 等降维,缓解维度灾难
- 度量学习:学习最优的距离度量矩阵(如 NCA、LMNN)
- 大数据变体:使用近似最近邻算法,牺牲少量精度换取巨大速度提升
总结
KNN 是“理解成本极低,但用好需要细致处理”的典型算法。关键三件事:标准化特征、精心选择 k 值、根据问题选择合适的距离度量。它常作为复杂问题的第一个基线模型,帮你快速建立对问题的认知。
本文来自博客园,作者:AceFenix,转载请注明原文链接:https://www.cnblogs.com/ukzq/p/20202991

浙公网安备 33010602011771号