摘要:前面讲的线性回归和逻辑回归都需要"训练过程"——从数据中学习参数。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 的最大克星 |
| 特征缩放 | 所有距离算法必须做——不做等于白做 |
核心三句话:
- KNN 是最简单的机器学习算法——没用任何数学优化,只需要"找邻居、投票"
- 特征缩放是 KNN 的生死线——不做标准化,KNN 一定出问题
- KNN 在低维小数据上表现出色,在高维大数据上表现糟糕——用它之前先检查数据维度
下一篇文章:支持向量机(SVM)——它用"最大间隔"的思想解决了高维分类问题,是逻辑回归之后最重要的线性分类器。
:最简单的“懒惰“学习器&spm=1001.2101.3001.5002&articleId=161977057&d=1&t=3&u=1eacb9d32e134eddaf545bd88ee5748b)
1097

被折叠的 条评论
为什么被折叠?



