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 的真实优势

  1. 零训练代价:数据不断增量更新时,无需重新训练(只需追加数据)
  2. 天然多分类:任意多个类别无需特殊处理(逻辑回归需要 OVR/multinomial)
  3. 局部适应性:决策边界自动适应局部密度变化
  4. 异常检测: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 在高维嵌入空间中的大规模工程实现。


如果这篇文章帮助厘清了距离度量和维度灾难的本质,欢迎点赞收藏支持,也欢迎关注账号持续跟进机器学习实战系列。在之前的文章中,机器学习项目方法论 讲解了算法选型的系统决策框架,数据预处理线性模型精讲 分别覆盖了数据质量保障与经典线性算法,可作为本文的前置补充。有任何问题欢迎在评论区留言。