文章目录
-
- [1. KNN 的算法直觉](#1. KNN 的算法直觉)
-
- [KNN 分类的数学形式](#KNN 分类的数学形式)
- [2. K 值选择](#2. K 值选择)
-
- 偏差-方差权衡的直觉
- [交叉验证搜索最优 K](#交叉验证搜索最优 K)
- [3. 距离度量全景](#3. 距离度量全景)
- [4. 距离度量选择框架](#4. 距离度量选择框架)
- [5. 特征缩放](#5. 特征缩放)
-
- 量纲主导的案例
- [StandardScaler vs MinMaxScaler 的选择](#StandardScaler vs MinMaxScaler 的选择)
- [6. 维度灾难](#6. 维度灾难)
- [7. 近似最近邻加速](#7. 近似最近邻加速)
-
- KD-Tree:低维高效的空间分割
- [Ball Tree:中高维的改进](#Ball Tree:中高维的改进)
- [HNSW:当前最先进的 ANN 算法](#HNSW:当前最先进的 ANN 算法)
- 三种索引结构对比
- [8. 加权 KNN](#8. 加权 KNN)
-
- 反距离加权
- [加权 KNN 的边界情况](#加权 KNN 的边界情况)
- [9. 选型边界](#9. 选型边界)
-
- [KNN 的真实优势](#KNN 的真实优势)
- 选型决策表
- [10. 实战](#10. 实战)
-
- [不同 K 值对比](#不同 K 值对比)
- 不同距离度量对比
- 近邻搜索算法的速度对比
- 输出分类报告
- [小结:KNN 知识地图](#小结:KNN 知识地图)
KNN 没有模型训练、没有参数学习、没有分布假设------纯靠数据本身说话。这种"懒惰"的哲学让 KNN 成为理解机器学习决策机制最直接的窗口。但"最近邻"的定义完全取决于距离度量:选错了度量,KNN 的判断会比随机猜测更差。
本文的核心不是"KNN 原理很简单"------而是距离度量的选择、维度灾难的本质、以及如何在大规模数据上让 KNN 真正可用。
1. KNN 的算法直觉
KNN 的分类规则只有一句话:一个样本的类别由它最近的 K 个邻居的多数票决定。回归版本则用邻居的均值(或加权均值)。
"没有模型"是 KNN 的特征,也是它的代价:
训练阶段:仅存储训练数据,O(1) 时间
预测阶段:计算与所有训练样本的距离,O(n×d) 时间
存储占用:所有训练数据,O(n×d) 空间
对比参数化模型(如逻辑回归):
- 逻辑回归训练慢,预测快(只需矩阵乘法)
- KNN 训练瞬间,预测慢(需要遍历所有样本)
这就是"懒惰学习"(Lazy Learning)的本质:把所有计算推迟到预测时刻。
KNN 分类的数学形式
y ^ = arg max c ∑ i ∈ N K ( x ) 1 y i = c \hat{y} = \arg\max_c \sum_{i \in N_K(x)} \mathbf{1}y_i = c y^=argcmaxi∈NK(x)∑1yi=c
其中 N K ( x ) N_K(x) NK(x) 是 x x x 的 K 个最近邻的集合。
KNN 回归:
y ^ = 1 K ∑ i ∈ N K ( x ) y i \hat{y} = \frac{1}{K} \sum_{i \in N_K(x)} y_i y^=K1i∈NK(x)∑yi
python
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# 生成二分类数据
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 最基础的 KNN
knn = KNeighborsClassifier(n_neighbors=5, metric='euclidean')
knn.fit(X_train, y_train)
print(f"K=5, 欧氏距离, 准确率: {accuracy_score(y_test, knn.predict(X_test)):.4f}")
2. K 值选择
K 是 KNN 唯一的真正超参数(距离度量是另一个重要选择,但通常由数据类型决定而非调优)。
偏差-方差权衡的直觉
#mermaid-svg-ZdojFMTdCliQR8EV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZdojFMTdCliQR8EV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZdojFMTdCliQR8EV .error-icon{fill:#552222;}#mermaid-svg-ZdojFMTdCliQR8EV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZdojFMTdCliQR8EV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZdojFMTdCliQR8EV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZdojFMTdCliQR8EV .marker.cross{stroke:#333333;}#mermaid-svg-ZdojFMTdCliQR8EV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZdojFMTdCliQR8EV p{margin:0;}#mermaid-svg-ZdojFMTdCliQR8EV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZdojFMTdCliQR8EV .cluster-label text{fill:#333;}#mermaid-svg-ZdojFMTdCliQR8EV .cluster-label span{color:#333;}#mermaid-svg-ZdojFMTdCliQR8EV .cluster-label span p{background-color:transparent;}#mermaid-svg-ZdojFMTdCliQR8EV .label text,#mermaid-svg-ZdojFMTdCliQR8EV span{fill:#333;color:#333;}#mermaid-svg-ZdojFMTdCliQR8EV .node rect,#mermaid-svg-ZdojFMTdCliQR8EV .node circle,#mermaid-svg-ZdojFMTdCliQR8EV .node ellipse,#mermaid-svg-ZdojFMTdCliQR8EV .node polygon,#mermaid-svg-ZdojFMTdCliQR8EV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZdojFMTdCliQR8EV .rough-node .label text,#mermaid-svg-ZdojFMTdCliQR8EV .node .label text,#mermaid-svg-ZdojFMTdCliQR8EV .image-shape .label,#mermaid-svg-ZdojFMTdCliQR8EV .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZdojFMTdCliQR8EV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZdojFMTdCliQR8EV .rough-node .label,#mermaid-svg-ZdojFMTdCliQR8EV .node .label,#mermaid-svg-ZdojFMTdCliQR8EV .image-shape .label,#mermaid-svg-ZdojFMTdCliQR8EV .icon-shape .label{text-align:center;}#mermaid-svg-ZdojFMTdCliQR8EV .node.clickable{cursor:pointer;}#mermaid-svg-ZdojFMTdCliQR8EV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZdojFMTdCliQR8EV .arrowheadPath{fill:#333333;}#mermaid-svg-ZdojFMTdCliQR8EV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZdojFMTdCliQR8EV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZdojFMTdCliQR8EV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZdojFMTdCliQR8EV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZdojFMTdCliQR8EV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZdojFMTdCliQR8EV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZdojFMTdCliQR8EV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZdojFMTdCliQR8EV .cluster text{fill:#333;}#mermaid-svg-ZdojFMTdCliQR8EV .cluster span{color:#333;}#mermaid-svg-ZdojFMTdCliQR8EV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZdojFMTdCliQR8EV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZdojFMTdCliQR8EV rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZdojFMTdCliQR8EV .icon-shape,#mermaid-svg-ZdojFMTdCliQR8EV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZdojFMTdCliQR8EV .icon-shape p,#mermaid-svg-ZdojFMTdCliQR8EV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZdojFMTdCliQR8EV .icon-shape .label rect,#mermaid-svg-ZdojFMTdCliQR8EV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZdojFMTdCliQR8EV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZdojFMTdCliQR8EV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZdojFMTdCliQR8EV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} K=1
(最小邻域)
决策边界极度
不规则(过拟合)
高方差/低偏差
K=N
(全局多数票)
预测恒为
多数类(欠拟合)
低方差/高偏差
K=最优值
(交叉验证)
平衡偏差-方差
边界平滑但不过于简单
从数学上看:
- K=1:每个训练点都是决策边界的塑造者,噪声点对预测影响最大
- K→N:所有邻居权重相等,等同于全局先验(类频率)
- 最优 K:通常随 n 1 / 2 n^{1/2} n1/2 增长------对于 10000 个样本,K=100 左右是合理起点
交叉验证搜索最优 K
python
from sklearn.model_selection import cross_val_score
import matplotlib.pyplot as plt
k_range = range(1, 31)
cv_scores = []
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X_train, y_train, cv=5, scoring='accuracy')
cv_scores.append(scores.mean())
optimal_k = k_range[np.argmax(cv_scores)]
print(f"最优 K 值: {optimal_k}, CV 准确率: {max(cv_scores):.4f}")
# 绘制 K vs 准确率曲线
plt.figure(figsize=(8, 4))
plt.plot(k_range, cv_scores, marker='o', markersize=4)
plt.axvline(optimal_k, color='red', linestyle='--', label=f'最优 K={optimal_k}')
plt.xlabel('K 值')
plt.ylabel('5折交叉验证准确率')
plt.title('K 值与模型性能')
plt.legend()
plt.tight_layout()
plt.savefig('knn_k_selection.png', dpi=100)
3. 距离度量全景
这是 KNN 中最容易被忽视却最关键的决策。选错度量,模型等同于在错误的空间中寻找"邻居"。
六种核心距离度量
欧氏距离(L2)
d ( x , y ) = ∑ i = 1 p ( x i − y i ) 2 d(x, y) = \sqrt{\sum_{i=1}^{p} (x_i - y_i)^2} d(x,y)=i=1∑p(xi−yi)2
几何意义:两点之间的直线距离。对每个维度的差值平方,所以对大差值非常敏感。
适用:连续数值特征、各维度量纲相同(或已标准化)、低维空间。
曼哈顿距离(L1,城市街区距离)
d ( x , y ) = ∑ i = 1 p ∣ x i − y i ∣ d(x, y) = \sum_{i=1}^{p} |x_i - y_i| d(x,y)=i=1∑p∣xi−yi∣
几何意义:在网格城市中从 A 到 B 的最短路径(只能走横竖街道)。对异常值更鲁棒------异常值在 L1 下只是线性影响,在 L2 下是平方影响。
适用:存在少量大幅偏差、坐标系网格化场景(如物流配送)、高维稀疏特征。
切比雪夫距离(L∞)
d ( x , y ) = max i ∣ x i − y i ∣ d(x, y) = \max_i |x_i - y_i| d(x,y)=imax∣xi−yi∣
几何意义:所有维度差值中的最大值------"最坏情况"的距离。
适用:游戏中棋盘的步数计算(国王移动)、仓储物流中叉车的移动路径规划。
余弦相似度(方向度量)
cos ( θ ) = x ⋅ y ∥ x ∥ ⋅ ∥ y ∥ \cos(\theta) = \frac{x \cdot y}{\|x\| \cdot \|y\|} cos(θ)=∥x∥⋅∥y∥x⋅y,对应距离 d = 1 − cos ( θ ) d = 1 - \cos(\theta) d=1−cos(θ)
几何意义:两个向量之间的角度------只关心方向,不关心大小。文档 A 有 100 个词,文档 B 有 50 个词,但词频分布类似,余弦相似度接近 1。
适用:文本向量(TF-IDF/词袋)、用户行为向量(高维稀疏)、推荐系统的相似度计算。
马氏距离(考虑特征相关性)
d ( x , y ) = ( x − y ) T Σ − 1 ( x − y ) d(x, y) = \sqrt{(x-y)^T \Sigma^{-1} (x-y)} d(x,y)=(x−y)TΣ−1(x−y)
其中 Σ \Sigma Σ 是协方差矩阵。几何意义:在标准化坐标系中的欧氏距离------同时处理尺度和相关性问题。两个高度相关的特征在欧氏距离下会被双重计算,马氏距离通过协方差逆矩阵消除这种重复。
适用:特征之间高度相关、不希望预先做 PCA 降维但想消除冗余信息。
计算代价 :需要估计协方差矩阵 Σ \Sigma Σ( O ( p 2 n ) O(p^2 n) O(p2n)),高维下代价大。
汉明距离(分类/二进制特征)
d ( x , y ) = 1 p ∑ i = 1 p 1 x i ≠ y i d(x, y) = \frac{1}{p}\sum_{i=1}^{p} \mathbf{1}x_i \\neq y_i d(x,y)=p1i=1∑p1xi=yi
几何意义:两个等长字符串/向量中不同位置的比例。
适用:One-hot 编码后的分类特征、DNA 序列比对、二进制特征向量。
六种距离的几何直觉对比
以二维平面为例,从原点出发,到各距离度量下"距离为1"的等值线(单位球):
#mermaid-svg-ul1SqfcU4boOa5ou{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ul1SqfcU4boOa5ou .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ul1SqfcU4boOa5ou .error-icon{fill:#552222;}#mermaid-svg-ul1SqfcU4boOa5ou .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ul1SqfcU4boOa5ou .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ul1SqfcU4boOa5ou .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ul1SqfcU4boOa5ou .marker.cross{stroke:#333333;}#mermaid-svg-ul1SqfcU4boOa5ou svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ul1SqfcU4boOa5ou p{margin:0;}#mermaid-svg-ul1SqfcU4boOa5ou .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ul1SqfcU4boOa5ou .cluster-label text{fill:#333;}#mermaid-svg-ul1SqfcU4boOa5ou .cluster-label span{color:#333;}#mermaid-svg-ul1SqfcU4boOa5ou .cluster-label span p{background-color:transparent;}#mermaid-svg-ul1SqfcU4boOa5ou .label text,#mermaid-svg-ul1SqfcU4boOa5ou span{fill:#333;color:#333;}#mermaid-svg-ul1SqfcU4boOa5ou .node rect,#mermaid-svg-ul1SqfcU4boOa5ou .node circle,#mermaid-svg-ul1SqfcU4boOa5ou .node ellipse,#mermaid-svg-ul1SqfcU4boOa5ou .node polygon,#mermaid-svg-ul1SqfcU4boOa5ou .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ul1SqfcU4boOa5ou .rough-node .label text,#mermaid-svg-ul1SqfcU4boOa5ou .node .label text,#mermaid-svg-ul1SqfcU4boOa5ou .image-shape .label,#mermaid-svg-ul1SqfcU4boOa5ou .icon-shape .label{text-anchor:middle;}#mermaid-svg-ul1SqfcU4boOa5ou .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ul1SqfcU4boOa5ou .rough-node .label,#mermaid-svg-ul1SqfcU4boOa5ou .node .label,#mermaid-svg-ul1SqfcU4boOa5ou .image-shape .label,#mermaid-svg-ul1SqfcU4boOa5ou .icon-shape .label{text-align:center;}#mermaid-svg-ul1SqfcU4boOa5ou .node.clickable{cursor:pointer;}#mermaid-svg-ul1SqfcU4boOa5ou .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ul1SqfcU4boOa5ou .arrowheadPath{fill:#333333;}#mermaid-svg-ul1SqfcU4boOa5ou .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ul1SqfcU4boOa5ou .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ul1SqfcU4boOa5ou .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ul1SqfcU4boOa5ou .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ul1SqfcU4boOa5ou .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ul1SqfcU4boOa5ou .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ul1SqfcU4boOa5ou .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ul1SqfcU4boOa5ou .cluster text{fill:#333;}#mermaid-svg-ul1SqfcU4boOa5ou .cluster span{color:#333;}#mermaid-svg-ul1SqfcU4boOa5ou div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ul1SqfcU4boOa5ou .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ul1SqfcU4boOa5ou rect.text{fill:none;stroke-width:0;}#mermaid-svg-ul1SqfcU4boOa5ou .icon-shape,#mermaid-svg-ul1SqfcU4boOa5ou .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ul1SqfcU4boOa5ou .icon-shape p,#mermaid-svg-ul1SqfcU4boOa5ou .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ul1SqfcU4boOa5ou .icon-shape .label rect,#mermaid-svg-ul1SqfcU4boOa5ou .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ul1SqfcU4boOa5ou .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ul1SqfcU4boOa5ou .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ul1SqfcU4boOa5ou :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 六种距离度量
L∞ 切比雪夫
单位球=正方形
L2 欧氏
单位球=圆形
L1 曼哈顿
单位球=菱形旋转45°
余弦距离
单位球=圆弧-角度扇形
马氏距离
单位球=椭圆-由协方差决定
汉明距离
仅适用离散特征空间
对最大维度差敏感
对所有维度均等对待
对异常值更鲁棒
对向量长度不敏感
消除特征相关性影响
逐位比较不同位数
4. 距离度量选择框架
#mermaid-svg-q5fWlGTb8UgV4n9V{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q5fWlGTb8UgV4n9V .error-icon{fill:#552222;}#mermaid-svg-q5fWlGTb8UgV4n9V .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q5fWlGTb8UgV4n9V .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q5fWlGTb8UgV4n9V .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q5fWlGTb8UgV4n9V .marker.cross{stroke:#333333;}#mermaid-svg-q5fWlGTb8UgV4n9V svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q5fWlGTb8UgV4n9V p{margin:0;}#mermaid-svg-q5fWlGTb8UgV4n9V .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V .cluster-label text{fill:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V .cluster-label span{color:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V .cluster-label span p{background-color:transparent;}#mermaid-svg-q5fWlGTb8UgV4n9V .label text,#mermaid-svg-q5fWlGTb8UgV4n9V span{fill:#333;color:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V .node rect,#mermaid-svg-q5fWlGTb8UgV4n9V .node circle,#mermaid-svg-q5fWlGTb8UgV4n9V .node ellipse,#mermaid-svg-q5fWlGTb8UgV4n9V .node polygon,#mermaid-svg-q5fWlGTb8UgV4n9V .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q5fWlGTb8UgV4n9V .rough-node .label text,#mermaid-svg-q5fWlGTb8UgV4n9V .node .label text,#mermaid-svg-q5fWlGTb8UgV4n9V .image-shape .label,#mermaid-svg-q5fWlGTb8UgV4n9V .icon-shape .label{text-anchor:middle;}#mermaid-svg-q5fWlGTb8UgV4n9V .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q5fWlGTb8UgV4n9V .rough-node .label,#mermaid-svg-q5fWlGTb8UgV4n9V .node .label,#mermaid-svg-q5fWlGTb8UgV4n9V .image-shape .label,#mermaid-svg-q5fWlGTb8UgV4n9V .icon-shape .label{text-align:center;}#mermaid-svg-q5fWlGTb8UgV4n9V .node.clickable{cursor:pointer;}#mermaid-svg-q5fWlGTb8UgV4n9V .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q5fWlGTb8UgV4n9V .arrowheadPath{fill:#333333;}#mermaid-svg-q5fWlGTb8UgV4n9V .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q5fWlGTb8UgV4n9V .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q5fWlGTb8UgV4n9V .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q5fWlGTb8UgV4n9V .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q5fWlGTb8UgV4n9V .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q5fWlGTb8UgV4n9V .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q5fWlGTb8UgV4n9V .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q5fWlGTb8UgV4n9V .cluster text{fill:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V .cluster span{color:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q5fWlGTb8UgV4n9V .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q5fWlGTb8UgV4n9V rect.text{fill:none;stroke-width:0;}#mermaid-svg-q5fWlGTb8UgV4n9V .icon-shape,#mermaid-svg-q5fWlGTb8UgV4n9V .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q5fWlGTb8UgV4n9V .icon-shape p,#mermaid-svg-q5fWlGTb8UgV4n9V .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q5fWlGTb8UgV4n9V .icon-shape .label rect,#mermaid-svg-q5fWlGTb8UgV4n9V .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q5fWlGTb8UgV4n9V .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q5fWlGTb8UgV4n9V .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q5fWlGTb8UgV4n9V :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 纯分类/二进制特征
连续数值特征
低维 d<20
有
无
是
否
高维稀疏 d>100
方向文本/行为向量
大小都重要
混合特征
输入数据特征
特征类型?
汉明距离
Hamming
维度数量?
有无明显异常值?
曼哈顿距离
L1
特征是否高度相关?
马氏距离
协方差调整
欧氏距离
L2 + 标准化
关注方向
还是大小?
余弦相似度
Cosine
L1 曼哈顿距离
或归一化欧氏
Gower 距离
混合类型距离
同一组数据,不同度量的"邻居"差异
python
import numpy as np
from sklearn.neighbors import NearestNeighbors
# 模拟:2个特征,第2个特征范围远大于第1个(量纲不同)
np.random.seed(42)
X_demo = np.array([
[1.0, 100.0], # 样本 A(查询点)
[1.1, 101.0], # 邻居候选 1(各维度都接近)
[5.0, 100.5], # 邻居候选 2(第1维差异大,第2维接近)
[1.0, 200.0], # 邻居候选 3(第2维差异大)
])
query = X_demo[0].reshape(1, -1)
print("查询点:", query)
for metric in ['euclidean', 'manhattan', 'chebyshev']:
nn = NearestNeighbors(n_neighbors=2, metric=metric)
nn.fit(X_demo[1:])
distances, indices = nn.kneighbors(query)
print(f"\n{metric} 最近邻: 索引={indices[0]}, 距离={distances[0].round(3)}")
# 未标准化时,第2特征(量纲100)会主导欧氏距离的判断
5. 特征缩放
特征缩放不是可选步骤,对 KNN 而言它是前置必修。
量纲主导的案例
设有两个特征:年龄(0-80岁)和收入(0-100万)。未标准化时,欧氏距离中收入的量级是年龄的约 10000 倍------计算出的"距离"几乎等于收入差。KNN 等价于完全忽略年龄特征,只用收入分类。
python
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.pipeline import Pipeline
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=1000, n_features=8, random_state=42)
# 故意放大某些特征的量纲
X_scaled_manually = X.copy()
X_scaled_manually[:, 0] *= 1000 # 放大特征0的量纲
X_scaled_manually[:, 1] *= 100
# 未缩放 vs 标准化 vs MinMax 的对比
configs = {
'未缩放': KNeighborsClassifier(n_neighbors=5),
'StandardScaler': Pipeline([('scaler', StandardScaler()), ('knn', KNeighborsClassifier(5))]),
'MinMaxScaler': Pipeline([('scaler', MinMaxScaler()), ('knn', KNeighborsClassifier(5))]),
}
for name, model in configs.items():
scores = cross_val_score(model, X_scaled_manually, y, cv=5)
print(f"{name}: 均值={scores.mean():.4f}, 标准差={scores.std():.4f}")
# 预期:未缩放的精度明显低于缩放后
StandardScaler vs MinMaxScaler 的选择
| 缩放方法 | 公式 | 特点 | 推荐场景 |
|---|---|---|---|
| StandardScaler | ( x − μ ) / σ (x - \mu) / \sigma (x−μ)/σ | 均值0,标准差1;不限制范围 | 大多数场景,对异常值鲁棒 |
| MinMaxScaler | ( x − x m i n ) / ( x m a x − x m i n ) (x - x_{min}) / (x_{max} - x_{min}) (x−xmin)/(xmax−xmin) | 压缩到 0,1;受异常值影响 | 神经网络、图像像素、无异常值 |
| RobustScaler | ( x − Q 2 ) / ( Q 3 − Q 1 ) (x - Q_2) / (Q_3 - Q_1) (x−Q2)/(Q3−Q1) | 用四分位数;对异常值最鲁棒 | 数据有明显异常值 |
6. 维度灾难
维度灾难不是概念------可以用数字量化它对 KNN 的破坏程度。
高维下的近邻变得"不近"
在 d d d 维单位超立方体中,若要一个球形近邻区域覆盖比例 r r r 的数据,需要的近邻半径 ℓ \ell ℓ 满足:
ℓ = r 1 / d \ell = r^{1/d} ℓ=r1/d
python
import numpy as np
import matplotlib.pyplot as plt
dims = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
r = 0.01 # 想要覆盖 1% 的数据
needed_radius = [r ** (1.0 / d) for d in dims]
print("维度 | 覆盖1%数据需要的近邻半径")
for d, radius in zip(dims, needed_radius):
print(f"d={d:4d}: 半径 = {radius:.4f}")
输出(部分):
d= 1: 半径 = 0.0100
d= 10: 半径 = 0.6310
d= 100: 半径 = 0.9550
d=1000: 半径 = 0.9954
解读:在 100 维空间中,覆盖 1% 数据需要半径 0.955 的球------几乎等于整个空间。"近邻"在高维中不再是局部概念,所有点几乎等距。
高维下所有点趋向等距
python
import numpy as np
for d in [10, 50, 100, 500, 1000]:
# 随机生成100个点,计算所有两两距离
X = np.random.randn(100, d)
dists = []
for i in range(len(X)):
for j in range(i+1, len(X)):
dists.append(np.linalg.norm(X[i] - X[j]))
dists = np.array(dists)
# 相对极差 = (max - min) / min(越小代表越等距)
relative_range = (dists.max() - dists.min()) / dists.min()
print(f"d={d:4d}: 最近距离={dists.min():.2f}, 最远距离={dists.max():.2f}, 相对极差={relative_range:.4f}")
随着维度增加,相对极差趋向于零------最近邻和最远邻的距离比越来越接近 1,KNN 的"近邻"选择变得无意义。
维度灾难的实践含义
- 高维空间( d > 50 d > 50 d>50)中,欧氏距离几乎失效
- 需要先降维(PCA/t-SNE/UMAP)或换用余弦距离(对高维稀疏有效)
- 或者放弃 KNN,改用对高维更鲁棒的算法(线性 SVM、XGBoost)
7. 近似最近邻加速
暴力搜索(brute-force)的预测复杂度是 O ( n × d ) O(n \times d) O(n×d),百万级数据集上每次预测需要数秒------实际应用不可接受。
KD-Tree:低维高效的空间分割
KD-Tree 把空间递归地分割成超矩形区域。查询时,只需搜索可能包含最近邻的分支,剪枝掉明显更远的分支。
#mermaid-svg-tyuq1m4NEg9RiuIh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tyuq1m4NEg9RiuIh .error-icon{fill:#552222;}#mermaid-svg-tyuq1m4NEg9RiuIh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tyuq1m4NEg9RiuIh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tyuq1m4NEg9RiuIh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tyuq1m4NEg9RiuIh .marker.cross{stroke:#333333;}#mermaid-svg-tyuq1m4NEg9RiuIh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tyuq1m4NEg9RiuIh p{margin:0;}#mermaid-svg-tyuq1m4NEg9RiuIh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh .cluster-label text{fill:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh .cluster-label span{color:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh .cluster-label span p{background-color:transparent;}#mermaid-svg-tyuq1m4NEg9RiuIh .label text,#mermaid-svg-tyuq1m4NEg9RiuIh span{fill:#333;color:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh .node rect,#mermaid-svg-tyuq1m4NEg9RiuIh .node circle,#mermaid-svg-tyuq1m4NEg9RiuIh .node ellipse,#mermaid-svg-tyuq1m4NEg9RiuIh .node polygon,#mermaid-svg-tyuq1m4NEg9RiuIh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tyuq1m4NEg9RiuIh .rough-node .label text,#mermaid-svg-tyuq1m4NEg9RiuIh .node .label text,#mermaid-svg-tyuq1m4NEg9RiuIh .image-shape .label,#mermaid-svg-tyuq1m4NEg9RiuIh .icon-shape .label{text-anchor:middle;}#mermaid-svg-tyuq1m4NEg9RiuIh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tyuq1m4NEg9RiuIh .rough-node .label,#mermaid-svg-tyuq1m4NEg9RiuIh .node .label,#mermaid-svg-tyuq1m4NEg9RiuIh .image-shape .label,#mermaid-svg-tyuq1m4NEg9RiuIh .icon-shape .label{text-align:center;}#mermaid-svg-tyuq1m4NEg9RiuIh .node.clickable{cursor:pointer;}#mermaid-svg-tyuq1m4NEg9RiuIh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tyuq1m4NEg9RiuIh .arrowheadPath{fill:#333333;}#mermaid-svg-tyuq1m4NEg9RiuIh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tyuq1m4NEg9RiuIh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tyuq1m4NEg9RiuIh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tyuq1m4NEg9RiuIh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tyuq1m4NEg9RiuIh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tyuq1m4NEg9RiuIh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tyuq1m4NEg9RiuIh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tyuq1m4NEg9RiuIh .cluster text{fill:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh .cluster span{color:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tyuq1m4NEg9RiuIh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tyuq1m4NEg9RiuIh rect.text{fill:none;stroke-width:0;}#mermaid-svg-tyuq1m4NEg9RiuIh .icon-shape,#mermaid-svg-tyuq1m4NEg9RiuIh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tyuq1m4NEg9RiuIh .icon-shape p,#mermaid-svg-tyuq1m4NEg9RiuIh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tyuq1m4NEg9RiuIh .icon-shape .label rect,#mermaid-svg-tyuq1m4NEg9RiuIh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tyuq1m4NEg9RiuIh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tyuq1m4NEg9RiuIh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tyuq1m4NEg9RiuIh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 根节点
按第1维分割: x₁=5
左子树
x₁ < 5
右子树
x₁ ≥ 5
按第2维分割
x₂=3
按第2维分割
x₂=7
按第2维分割
x₂=4
按第2维分割
x₂=9
叶节点
点集合
叶节点
点集合
叶节点
点集合
叶节点
点集合
叶节点
点集合
- 构建复杂度: O ( n log n ) O(n \log n) O(nlogn)
- 查询复杂度: O ( log n ) O(\log n) O(logn)(低维时)
- 致命缺陷 :维度 d > 20 d > 20 d>20 后,剪枝失效,退化到接近暴力搜索
Ball Tree:中高维的改进
Ball Tree 用超球体代替超矩形。球体在高维下的剪枝比矩形更高效(相同体积下球体更紧凑)。
python
from sklearn.neighbors import KNeighborsClassifier
import time
import numpy as np
from sklearn.datasets import make_classification
# 不同 algorithm 参数的速度对比
X_large, y_large = make_classification(n_samples=50000, n_features=20, random_state=42)
X_train, X_test = X_large[:40000], X_large[40000:]
for algorithm in ['brute', 'kd_tree', 'ball_tree', 'auto']:
knn = KNeighborsClassifier(n_neighbors=5, algorithm=algorithm)
t0 = time.time()
knn.fit(X_train, y_large[:40000])
t1 = time.time()
knn.predict(X_test[:1000])
t2 = time.time()
print(f"{algorithm:10s}: 训练={t1-t0:.3f}s, 预测1000条={t2-t1:.3f}s")
HNSW:当前最先进的 ANN 算法
HNSW(Hierarchical Navigable Small World)是目前工业级向量检索的主流算法,被 Faiss、Hnswlib、Milvus 等向量数据库广泛使用。
核心思想:受"小世界网络"启发,构建多层图结构:
- 顶层:稀疏长距离边(快速定位大致区域)
- 底层:密集短距离边(精确局部搜索)
- 查询:从顶层贪婪下降,逐层缩小候选范围
#mermaid-svg-vM0kmsbWSy8RSQp3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vM0kmsbWSy8RSQp3 .error-icon{fill:#552222;}#mermaid-svg-vM0kmsbWSy8RSQp3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vM0kmsbWSy8RSQp3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .marker.cross{stroke:#333333;}#mermaid-svg-vM0kmsbWSy8RSQp3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vM0kmsbWSy8RSQp3 p{margin:0;}#mermaid-svg-vM0kmsbWSy8RSQp3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .cluster-label text{fill:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .cluster-label span{color:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .cluster-label span p{background-color:transparent;}#mermaid-svg-vM0kmsbWSy8RSQp3 .label text,#mermaid-svg-vM0kmsbWSy8RSQp3 span{fill:#333;color:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .node rect,#mermaid-svg-vM0kmsbWSy8RSQp3 .node circle,#mermaid-svg-vM0kmsbWSy8RSQp3 .node ellipse,#mermaid-svg-vM0kmsbWSy8RSQp3 .node polygon,#mermaid-svg-vM0kmsbWSy8RSQp3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .rough-node .label text,#mermaid-svg-vM0kmsbWSy8RSQp3 .node .label text,#mermaid-svg-vM0kmsbWSy8RSQp3 .image-shape .label,#mermaid-svg-vM0kmsbWSy8RSQp3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-vM0kmsbWSy8RSQp3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .rough-node .label,#mermaid-svg-vM0kmsbWSy8RSQp3 .node .label,#mermaid-svg-vM0kmsbWSy8RSQp3 .image-shape .label,#mermaid-svg-vM0kmsbWSy8RSQp3 .icon-shape .label{text-align:center;}#mermaid-svg-vM0kmsbWSy8RSQp3 .node.clickable{cursor:pointer;}#mermaid-svg-vM0kmsbWSy8RSQp3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .arrowheadPath{fill:#333333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vM0kmsbWSy8RSQp3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vM0kmsbWSy8RSQp3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vM0kmsbWSy8RSQp3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vM0kmsbWSy8RSQp3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .cluster text{fill:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 .cluster span{color:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vM0kmsbWSy8RSQp3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vM0kmsbWSy8RSQp3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-vM0kmsbWSy8RSQp3 .icon-shape,#mermaid-svg-vM0kmsbWSy8RSQp3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vM0kmsbWSy8RSQp3 .icon-shape p,#mermaid-svg-vM0kmsbWSy8RSQp3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vM0kmsbWSy8RSQp3 .icon-shape .label rect,#mermaid-svg-vM0kmsbWSy8RSQp3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vM0kmsbWSy8RSQp3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vM0kmsbWSy8RSQp3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vM0kmsbWSy8RSQp3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每层贪婪搜索
找到当前最近点
进入下一层
继续细化
查询点
第2层(稀疏)
快速跳转大区域
第1层(中等)
区域细化
第0层(密集)
精确局部搜索
找到K个近邻
python
# 使用 hnswlib 库(比 sklearn 快 10-100x)
# pip install hnswlib
import hnswlib
import numpy as np
# 构建 HNSW 索引
dim = 128 # 特征维度
n_elements = 100000
data = np.random.randn(n_elements, dim).astype(np.float32)
query = np.random.randn(1, dim).astype(np.float32)
# 初始化
p = hnswlib.Index(space='l2', dim=dim)
p.init_index(max_elements=n_elements, ef_construction=200, M=16)
p.add_items(data)
# 设置查询精度(ef越大越准确,越慢)
p.set_ef(50)
# 查询
labels, distances = p.knn_query(query, k=10)
print("HNSW 最近邻索引:", labels[0])
print("HNSW 距离:", distances[0])
三种索引结构对比
| 方法 | 适用维度 | 构建时间 | 查询时间 | 准确性 | 内存占用 |
|---|---|---|---|---|---|
| 暴力搜索 | 任意 | O ( 1 ) O(1) O(1) | O ( n d ) O(nd) O(nd) | 精确 | O ( n d ) O(nd) O(nd) |
| KD-Tree | d < 20 d < 20 d<20 | O ( n log n ) O(n\log n) O(nlogn) | O ( log n ) O(\log n) O(logn) | 精确 | O ( n d ) O(nd) O(nd) |
| Ball Tree | d < 40 d < 40 d<40 | O ( n log n ) O(n\log n) O(nlogn) | 中等 | 精确 | O ( n d ) O(nd) O(nd) |
| LSH | d > 100 d > 100 d>100(稀疏) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 近似 | 低 |
| HNSW | 任意(最优 d = 50 ∼ 1000 d=50\sim1000 d=50∼1000) | O ( n log n ) O(n\log n) O(nlogn) | O ( log n ) O(\log n) O(logn) | 近似高精度 | 较高 |
sklearn 的 algorithm='auto' 选择逻辑 :当 d < 20 d < 20 d<20 且 n n n 不太大时选 KD-Tree,否则选 Ball Tree;暴力搜索在 n n n 很小时自动使用。对于工业级大规模搜索,需要使用 Faiss 或 Hnswlib。
8. 加权 KNN
标准 KNN 给所有 K 个邻居相同权重------但距离更近的邻居理论上应该更"可信"。
反距离加权
y ^ = ∑ i ∈ N K ( x ) w i y i ∑ i ∈ N K ( x ) w i , w i = 1 d ( x , x i ) p \hat{y} = \frac{\sum_{i \in N_K(x)} w_i y_i}{\sum_{i \in N_K(x)} w_i}, \quad w_i = \frac{1}{d(x, x_i)^p} y^=∑i∈NK(x)wi∑i∈NK(x)wiyi,wi=d(x,xi)p1
距离越近,权重越大( p = 2 p=2 p=2 是最常用的平方反比加权)。
python
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
param_grid = {
'n_neighbors': [3, 5, 7, 10, 15, 20],
'weights': ['uniform', 'distance'], # 均匀 vs 距离加权
'metric': ['euclidean', 'manhattan']
}
grid_search = GridSearchCV(
KNeighborsClassifier(),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1
)
grid_search.fit(X_train, y_large[:40000])
print("最优参数:", grid_search.best_params_)
print("最优 CV 分数:", grid_search.best_score_)
加权 KNN 的边界情况
当某个训练点恰好等于查询点时(精确匹配),距离为 0,权重为无穷大------该点独自决定预测结果(退化到 K=1)。sklearn 通过将相同距离点都置为权重1 来处理这个边界情况。
9. 选型边界
KNN 并非"只是教学用的玩具"------在特定场景有其不可替代的价值。
KNN 的真实优势
- 零训练代价:数据不断增量更新时,无需重新训练(只需追加数据)
- 天然多分类:任意多个类别无需特殊处理(逻辑回归需要 OVR/multinomial)
- 局部适应性:决策边界自动适应局部密度变化
- 异常检测:K 近邻距离可直接用作异常分数(LOF、GLOSH 等方法的基础)
选型决策表
| 场景 | KNN 合适? | 推荐替代 | 原因 |
|---|---|---|---|
| 小数据集 n < 10000 n < 10000 n<10000, d < 20 d < 20 d<20 | ✅ | --- | 速度可接受,效果好 |
| 增量学习(数据实时流入) | ✅ | --- | 无需重训练 |
| 大数据集 n > 10 5 n > 10^5 n>105 | ❌ | XGBoost, 线性SVM | 预测延迟高 |
| 高维稀疏文本 d > 1000 d > 1000 d>1000 | ❌(欧氏)✅(余弦) | SVM线性核 | 欧氏失效,余弦有效 |
| 实时预测(< 10ms 延迟) | ❌(暴力)✅(HNSW) | 线性模型 | 延迟要求高 |
| 可解释性要求 | 部分 | 逻辑回归 | 邻居可展示,但决策边界复杂 |
| 类别严重不平衡 | ❌ | 过采样+其他算法 | 多数类邻居主导 |
10. 实战
用 MNIST 手写数字数据集的子集(减少计算量),比较不同 K 值、距离度量、和近邻搜索算法的效果与速度。
python
from sklearn.datasets import fetch_openml
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
import numpy as np
import time
# 加载 MNIST(取前5000个样本做演示)
mnist = fetch_openml('mnist_784', version=1, as_frame=False, parser='auto')
X_mnist, y_mnist = mnist.data[:5000], mnist.target[:5000]
# 数据分割
X_tr, X_te, y_tr, y_te = train_test_split(X_mnist, y_mnist, test_size=0.2, random_state=42)
print(f"训练集: {X_tr.shape}, 测试集: {X_te.shape}")
print(f"类别数: {np.unique(y_mnist).shape[0]}")
# 标准化(像素值0-255 → 均值0标准差1)
scaler = StandardScaler()
X_tr_sc = scaler.fit_transform(X_tr)
X_te_sc = scaler.transform(X_te)
不同 K 值对比
python
print("\n=== K 值 vs 精度 ===")
for k in [1, 3, 5, 7, 10, 15]:
knn = KNeighborsClassifier(n_neighbors=k, algorithm='ball_tree', metric='euclidean')
t0 = time.time()
knn.fit(X_tr_sc, y_tr)
y_pred = knn.predict(X_te_sc)
elapsed = time.time() - t0
print(f"K={k:2d}: 准确率={accuracy_score(y_te, y_pred):.4f}, 时间={elapsed:.2f}s")
不同距离度量对比
python
print("\n=== 距离度量 vs 精度 ===")
metrics = [
('euclidean', 'ball_tree'),
('manhattan', 'ball_tree'),
('chebyshev', 'ball_tree'),
('cosine', 'brute'), # 余弦只能 brute force
]
for metric, algorithm in metrics:
knn = KNeighborsClassifier(n_neighbors=5, metric=metric, algorithm=algorithm)
t0 = time.time()
knn.fit(X_tr_sc, y_tr)
y_pred = knn.predict(X_te_sc)
elapsed = time.time() - t0
print(f"{metric:12s}: 准确率={accuracy_score(y_te, y_pred):.4f}, 时间={elapsed:.2f}s")
典型输出:
euclidean : 准确率=0.9530, 时间=1.24s
manhattan : 准确率=0.9490, 时间=1.31s
chebyshev : 准确率=0.9120, 时间=1.18s
cosine : 准确率=0.9560, 时间=2.87s
发现:在图像像素特征上,余弦距离(只关注像素分布方向,忽略亮度差异)精度略高于欧氏距离;切比雪夫(只看最差维度)表现最弱。
近邻搜索算法的速度对比
python
print("\n=== 搜索算法 vs 速度 ===")
for algorithm in ['brute', 'kd_tree', 'ball_tree', 'auto']:
knn = KNeighborsClassifier(n_neighbors=5, algorithm=algorithm, metric='euclidean')
t0 = time.time()
knn.fit(X_tr_sc, y_tr)
knn.predict(X_te_sc)
print(f"{algorithm:10s}: {time.time()-t0:.3f}s")
输出分类报告
python
knn_best = KNeighborsClassifier(n_neighbors=5, algorithm='ball_tree', weights='distance', metric='euclidean')
knn_best.fit(X_tr_sc, y_tr)
y_pred_best = knn_best.predict(X_te_sc)
print("\n最优 KNN 分类报告(K=5, 距离加权, Ball Tree, 欧氏距离):")
print(classification_report(y_te, y_pred_best))
小结:KNN 知识地图
#mermaid-svg-bAyP3W2di5Rmmgnl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bAyP3W2di5Rmmgnl .error-icon{fill:#552222;}#mermaid-svg-bAyP3W2di5Rmmgnl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bAyP3W2di5Rmmgnl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bAyP3W2di5Rmmgnl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bAyP3W2di5Rmmgnl .marker.cross{stroke:#333333;}#mermaid-svg-bAyP3W2di5Rmmgnl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bAyP3W2di5Rmmgnl p{margin:0;}#mermaid-svg-bAyP3W2di5Rmmgnl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl .cluster-label text{fill:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl .cluster-label span{color:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl .cluster-label span p{background-color:transparent;}#mermaid-svg-bAyP3W2di5Rmmgnl .label text,#mermaid-svg-bAyP3W2di5Rmmgnl span{fill:#333;color:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl .node rect,#mermaid-svg-bAyP3W2di5Rmmgnl .node circle,#mermaid-svg-bAyP3W2di5Rmmgnl .node ellipse,#mermaid-svg-bAyP3W2di5Rmmgnl .node polygon,#mermaid-svg-bAyP3W2di5Rmmgnl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bAyP3W2di5Rmmgnl .rough-node .label text,#mermaid-svg-bAyP3W2di5Rmmgnl .node .label text,#mermaid-svg-bAyP3W2di5Rmmgnl .image-shape .label,#mermaid-svg-bAyP3W2di5Rmmgnl .icon-shape .label{text-anchor:middle;}#mermaid-svg-bAyP3W2di5Rmmgnl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bAyP3W2di5Rmmgnl .rough-node .label,#mermaid-svg-bAyP3W2di5Rmmgnl .node .label,#mermaid-svg-bAyP3W2di5Rmmgnl .image-shape .label,#mermaid-svg-bAyP3W2di5Rmmgnl .icon-shape .label{text-align:center;}#mermaid-svg-bAyP3W2di5Rmmgnl .node.clickable{cursor:pointer;}#mermaid-svg-bAyP3W2di5Rmmgnl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bAyP3W2di5Rmmgnl .arrowheadPath{fill:#333333;}#mermaid-svg-bAyP3W2di5Rmmgnl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bAyP3W2di5Rmmgnl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bAyP3W2di5Rmmgnl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bAyP3W2di5Rmmgnl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bAyP3W2di5Rmmgnl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bAyP3W2di5Rmmgnl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bAyP3W2di5Rmmgnl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bAyP3W2di5Rmmgnl .cluster text{fill:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl .cluster span{color:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bAyP3W2di5Rmmgnl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bAyP3W2di5Rmmgnl rect.text{fill:none;stroke-width:0;}#mermaid-svg-bAyP3W2di5Rmmgnl .icon-shape,#mermaid-svg-bAyP3W2di5Rmmgnl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bAyP3W2di5Rmmgnl .icon-shape p,#mermaid-svg-bAyP3W2di5Rmmgnl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bAyP3W2di5Rmmgnl .icon-shape .label rect,#mermaid-svg-bAyP3W2di5Rmmgnl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bAyP3W2di5Rmmgnl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bAyP3W2di5Rmmgnl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bAyP3W2di5Rmmgnl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} KNN 核心知识体系
算法基础
懒惰学习 / 无训练阶段
K 值选择
偏差-方差权衡 / CV 搜索
距离度量
6种度量 / 选型决策框架
特征缩放
标准化前置必修
维度灾难
高维下距离退化
ANN 加速
KD-Tree/Ball Tree/HNSW
加权 KNN
反距离加权
选型边界
小数据/增量学习/异常检测
欧氏: 低维连续
曼哈顿: 异常值鲁棒
余弦: 高维稀疏/文本
马氏: 相关特征
汉明: 分类特征
KD-Tree: d<20
Ball Tree: d<40
HNSW: 工业级d>50
距离度量的选择从"是否标准化"开始,到"数据维度"和"特征类型",每一步都有清晰的决策依据。近似最近邻加速让 KNN 从"只能处理小数据"扩展到工业级向量检索------现代推荐系统、人脸识别、文本语义搜索的召回层,本质上都是 KNN 在高维嵌入空间中的大规模工程实现。
如果这篇文章帮助厘清了距离度量和维度灾难的本质,欢迎点赞收藏支持,也欢迎关注账号持续跟进机器学习实战系列。在之前的文章中,机器学习项目方法论 讲解了算法选型的系统决策框架,数据预处理 和 线性模型精讲 分别覆盖了数据质量保障与经典线性算法,可作为本文的前置补充。有任何问题欢迎在评论区留言。