K 近邻(KNN):最简单的“懒惰“学习器

摘要:前面讲的线性回归和逻辑回归都需要"训练过程"——从数据中学习参数。KNN 完全不同:它什么都不学,只是把训练数据记住。预测时,它找到离新样本最近的 K 个"邻居",让它们投票决定答案。这种"懒惰学习"的方式让 KNN 简单直观,同时也暴露了它的致命弱点。这篇文章讲清楚 KNN 的原理、距离度量、K 值选择、以及它的适用场景。


一、KNN 的核心思想:近朱者赤

一个直觉

想象你要判断一个人是否经常运动。你不知道他的任何信息,但你知道他三个最铁的朋友:

朋友 A:每周健身 4 次  → 经常运动
朋友 B:每周健身 3 次  → 经常运动
朋友 C:每周健身 1 次  → 不常运动

三票中两票是"经常运动" → 预测他经常运动

这就是 KNN 的全部思想:物以类聚,人以群分。一个人的类别,可以由他最近的 K 个邻居的类别决定。

算法流程

输入:一个待分类的新样本
步骤:
  1. 计算它到训练集中所有样本的距离
  2. 找出距离最近的 K 个样本(邻居)
  3. 让 K 个邻居投票(分类:少数服从多数 / 回归:取平均值)
  4. 把投票结果作为预测类别

输出:预测的类别
KNN 直观示意(二分类,K=5):

特征₂
  │   ○ ○
  │       ○          ○ = 类别 A
  │     ★ ○ ○        × = 类别 B
  │   × × ★          ★ = 新样本
  │ × ×   ×
  │
  └──────────────────→ 特征₁

距离★最近的 5 个邻居:
  3 个 ○ (类别 A)
  2 个 × (类别 B)
  
预测结果:类别 A

KNN 的"懒惰"体现在哪?

其他算法(以线性回归为例):
  训练阶段:学习参数 w 和 b    ← 花时间学
  预测阶段:用 y = wx + b 计算  ← 瞬间完成

KNN:
  训练阶段:什么都不做,保存数据  ← 0 时间
  预测阶段:遍历所有数据找最近邻  ← 花时间算

这就是"懒惰学习"(Lazy Learning)——训练几乎不需要时间,但预测需要。

二、距离度量:如何定义"近"?

KNN 的核心依赖"距离"的定义。不同的距离度量会导致不同的邻居选择。

常用距离度量

import numpy as np
from scipy.spatial.distance import euclidean, manhattan, cosine

# 两个样本
a = np.array([2, 3])
b = np.array([5, 7])

# 1. 欧氏距离(Euclidean)—— 最常用
# 相当于用尺子量两点之间的直线距离
eu_dist = euclidean(a, b)  # = sqrt((5-2)² + (7-3)²) = 5.0

# 2. 曼哈顿距离(Manhattan)—— 城市街区的行走距离
# 只能沿坐标轴方向走
man_dist = manhattan(a, b)  # = |5-2| + |7-3| = 7.0

# 3. 余弦相似度(Cosine)—— 关注方向而不是长度
# 常用于文本(词频向量)
cos_sim = cosine(a, b)  # 越小越相似

什么时候该用哪种?

距离度量适用场景特点
欧氏距离连续数值特征(默认选择)直观,对尺度敏感
曼哈顿距离高维数据、稀疏特征比欧氏距离在高维中更稳定
余弦相似度文本、用户画像关注方向(模式)而非大小
闵可夫斯基距离需要调节参数 p欧氏(p=2)和曼哈顿(p=1)的推广

距离度量的维度灾难问题

KNN 在高维空间中有一个致命问题:随着维度增加,所有点之间的距离趋于相等。

1 维:点均匀分布在 [0,1] 线上
  → 最近邻距离 ≈ 0.01,最远邻距离 ≈ 0.99
  → 距离有意义 ✅

10 维:点均匀分布在 [0,1]¹⁰ 超立方体中
  → 最近邻距离 ≈ 0.5,最远邻距离 ≈ 1.0
  → 距离区分度急剧下降 ⚠️

100 维:
  → 几乎所有点之间的距离都差不多
  → "最近"的邻居和"最远"的邻居几乎一样远 ❌
  → KNN 基本失效

这就是维度灾难(Curse of Dimensionality)——KNN 是受维度灾难影响最严重的算法之一。


三、K 值的选择:Bias-Variance 权衡

K 是 KNN 最重要的超参数。K 值的选择直接影响模型性能。

K 值大小的影响

K=1(过拟合):
  只考虑最近的一个邻居
  → 决策边界极其复杂
  → 对噪声极其敏感
  → 训练准确率 100%,测试准确率低

K 值适中(比如 K=5~15):
  综合考虑多个邻居
  → 决策边界平滑
  → 对噪声不敏感
  → 泛化能力最好

K=训练集大小(欠拟合):
  考虑所有样本
  → 决策边界是一条直线(永远预测多数类)
  → 完全失去了"局部"信息

图形化理解

        K=1                     K=5                    K=N
    ┌──────┐               ┌──────┐               ┌──────┐
    │╱╲    │               │      │               │      │
    │  ╲╱  │               │  ╱╲  │               │      │
    │╱    ╲│               │╱  ╲ ╲│               │      │
    │  ╲╱  │               │    ╲ │               │      │
    │╱    ╲│               │╱    ╲│               │      │
    └──────┘               └──────┘               └──────┘
  边界复杂,过拟合        边界平滑,泛化好        边界太简单,欠拟合

如何选择 K?

from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
import numpy as np

# 用交叉验证选择最优 K
k_range = range(1, 31)
k_scores = []

for k in k_range:
    knn = KNeighborsClassifier(n_neighbors=k)
    scores = cross_val_score(knn, X_train, y_train, cv=5, scoring='accuracy')
    k_scores.append(scores.mean())

best_k = k_range[np.argmax(k_scores)]
print(f"最优 K 值: {best_k}, 交叉验证准确率: {max(k_scores):.3f}")

经验法则

  • K 通常取奇数(避免平票)
  • K 一般取 3 到 15 之间
  • 数据量大时 K 可以大一些,数据量小时 K 要小一些
  • 永远不要用 K=1 作为生产模型(除非你有非常特殊的理由)

四、特征缩放:KNN 的生死线

为什么 KNN 必须做特征缩放?

KNN 依赖距离,而距离计算对特征尺度极度敏感。

假设用两个特征预测房价:面积(m²)和卧室数

样本 A:面积=100, 卧室=3
样本 B:面积=101, 卧室=1

欧氏距离:
  √((100-101)² + (3-1)²) = √(1 + 4) = √5 ≈ 2.24

看起来是两个特征都在起作用,但仔细看:
  面积差 = 1(相对于 100 来说是 1% 的差异)
  卧室差 = 2(相对于 3 来说是 67% 的差异)

如果面积用"平方英尺"(1m² ≈ 10.76 ft²):
  样本 A:面积=1076, 卧室=3
  样本 B:面积=1087, 卧室=1
  
  距离 = √((1076-1087)² + (3-1)²) = √(121 + 4) = √125 ≈ 11.2
  面积差异占据了主导地位(121/125 = 97%)!

只是换了单位,KNN 的结果就完全不同!

解决方案:标准化

from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline

# 正确的做法:标准化 + KNN 放在一起
# 标准化让所有特征的均值为 0,标准差为 1
knn_pipeline = make_pipeline(
    StandardScaler(),           # 先标准化
    KNeighborsClassifier(n_neighbors=5)  # 再 KNN
)

knn_pipeline.fit(X_train, y_train)
y_pred = knn_pipeline.predict(X_test)

记住这条规则任何基于距离的算法(KNN、SVM、K-Means)都必须做特征缩放。 不做的话,KNN 的结果几乎肯定是错的。


五、KNN 用于回归

KNN 不仅做分类,也可以做回归——只需把"投票"换成"取平均"。

from sklearn.neighbors import KNeighborsRegressor

knn_reg = KNeighborsRegressor(n_neighbors=5)
knn_reg.fit(X_train, y_train)
y_pred = knn_reg.predict(X_test)  # 预测值是 K 个邻居的平均值
任务类型预测方式
分类K 个邻居中最多类别的作为预测
回归K 个邻居的平均值作为预测
带权重的版本距离越近权重越大(weights='distance'

加权版本通常效果更好:

# 距离加权:更近的邻居有更大的投票/平均权重
knn_weighted = KNeighborsClassifier(
    n_neighbors=5,
    weights='distance'  # 按距离的倒数加权
)

六、KNN 的完整实战:乳腺癌分类

import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report, ConfusionMatrixDisplay
from sklearn.pipeline import make_pipeline
import matplotlib.pyplot as plt

# ===== 1. 加载数据 =====
data = load_breast_cancer()
X, y = data.data, data.target
feature_names = data.feature_names
print(f"样本数: {X.shape[0]}, 特征数: {X.shape[1]}")
# 样本数: 569, 特征数: 30

# ===== 2. 划分 =====
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# ===== 3. 用交叉验证找最优 K =====
k_range = range(1, 26)
cv_scores = []

for k in k_range:
    pipeline = make_pipeline(
        StandardScaler(),
        KNeighborsClassifier(n_neighbors=k)
    )
    scores = cross_val_score(pipeline, X_train, y_train, cv=5)
    cv_scores.append(scores.mean())

best_k = k_range[np.argmax(cv_scores)]
print(f"最优 K = {best_k}(交叉验证准确率: {max(cv_scores):.3f})")

# ===== 4. 用最优 K 训练最终模型 =====
final_model = make_pipeline(
    StandardScaler(),
    KNeighborsClassifier(n_neighbors=best_k)
)
final_model.fit(X_train, y_train)

# ===== 5. 评估 =====
y_pred = final_model.predict(X_test)
print(f"\n测试集准确率: {final_model.score(X_test, y_test):.3f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred))

# ===== 6. 查看 K 值 vs 准确率的曲线 =====
plt.figure(figsize=(10, 5))
plt.plot(k_range, cv_scores, 'bo-')
plt.axvline(x=best_k, color='r', linestyle='--', label=f'最优 K={best_k}')
plt.xlabel('K 值')
plt.ylabel('交叉验证准确率')
plt.title('K 值选择')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

输出示例

样本数: 569, 特征数: 30
最优 K = 7(交叉验证准确率: 0.971)

测试集准确率: 0.974

分类报告:
               precision  recall  f1-score
   malignant      0.98     0.95     0.96
      benign      0.97     0.99     0.98

七、KNN 与其他算法对比

KNN vs 逻辑回归

对比KNN逻辑回归
决策边界非线性(由数据密度决定)线性
训练时间几乎不需要 ❌ 预测慢需要训练 ✅ 预测快
可解释性❌ 无法解释"为什么"✅ 权重可直接解读
特征缩放必须做建议做(使用正则化时)
高维数据❌ 维度灾难,效果差✅ 支持高维(加正则化)
小数据集✅ 效果好⚠️ 可能欠拟合

KNN 的优缺点总结

优点缺点
✅ 原理简单直观,无需训练❌ 预测慢(需要遍历所有训练数据)
✅ 天然支持多分类❌ 高维数据失效(维度灾难)
✅ 没有训练过程,适合持续新增数据❌ 对特征尺度敏感
✅ 决策边界可以非常复杂❌ 无法给出特征重要性
✅ 同时支持分类和回归❌ 需要存储所有训练数据

KNN 的工程优化

当训练数据很大时(>10万),原始的 KNN 会非常慢。常用优化:

# 使用 KD-Tree 或 Ball-Tree 加速搜索
# 把时间复杂度从 O(N) 降到 O(log N)

fast_knn = KNeighborsClassifier(
    n_neighbors=5,
    algorithm='kd_tree',   # 或 'ball_tree'
    leaf_size=30,          # 叶子节点大小
)

# 当数据维度高时,'brute'(暴力搜索)反而更快
# sklearn 会自动选择最优算法:
auto_knn = KNeighborsClassifier(algorithm='auto')

八、总结

概念一句话理解
KNN找最近的 K 个邻居投票决定分类——"近朱者赤"
懒惰学习不训练只记忆,预测时才计算——训练快预测慢
距离度量如何定义"近"——欧氏距离最常用,但需根据场景选择
K 值邻居数量——太小过拟合,太大欠拟合
维度灾难高维空间中所有点一样远——KNN 的最大克星
特征缩放所有距离算法必须做——不做等于白做

核心三句话

  1. KNN 是最简单的机器学习算法——没用任何数学优化,只需要"找邻居、投票"
  2. 特征缩放是 KNN 的生死线——不做标准化,KNN 一定出问题
  3. KNN 在低维小数据上表现出色,在高维大数据上表现糟糕——用它之前先检查数据维度

下一篇文章:支持向量机(SVM)——它用"最大间隔"的思想解决了高维分类问题,是逻辑回归之后最重要的线性分类器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值