一、KNN 到底是什么?
1.1 从一个生活中的例子开始
假设你刚转学到一个新班级,想猜一位同学更喜欢打篮球还是踢足球。
你暂时不了解他,但注意到几位与他兴趣、性格和日常习惯比较相似的同学:
- 与他最相似的同学 A 喜欢篮球;
- 第二相似的同学 B 喜欢篮球;
- 第三相似的同学 C 喜欢足球。
如果让你根据这三位同学的情况作一个初步猜测,你大概率会猜“篮球”。理由是:三个相似的人中,有两个人喜欢篮球。
KNN 的思想就这么朴素:一个新样本通常可以参考它附近、与它最相似的几个已有样本来判断。
这有点像“物以类聚,人以群分”,但要注意,这只是帮助理解的直觉,并不保证所有问题中“离得近的样本”一定同类。KNN 的效果取决于数据是否适合用距离表示相似性。
1.2 KNN 名字中的 K、N、NN 是什么意思?
KNN 的英文名称是 K-Nearest Neighbors,中文叫 K 近邻法。
- K:选择多少个最近的邻居。例如 K=3,就参考最近的三个样本。
- Nearest:最近的。这里的“近”通常由特征之间的数学距离决定。
- Neighbors:邻居。指已有数据中,与待预测样本最接近的样本。
如果训练数据中一共有 N 个样本,那么 N 表示训练样本数;它不是算法名称中一个需要单独调节的参数。
例如,一共有 1000 条历史记录。我们预测一条新记录时,如果选择 K=5,便从这 1000 条历史记录里找出最接近它的 5 条,再根据这 5 条记录判断。
1.3 KNN 是一种监督学习算法
先区分两个词:
- 特征(feature):描述样本的属性,比如身高、体重、年龄、考试分数。
- 标签(label):我们希望预测的结果,比如“猫/狗”“是否合格”“喜欢篮球/足球”。
在监督学习中,训练数据同时包含特征和正确标签。例如:
| 样本 | 身高(cm) | 体重(kg) | 标签 |
|---|---|---|---|
| A | 170 | 60 | 篮球 |
| B | 175 | 65 | 篮球 |
| C | 160 | 55 | 足球 |
| D | 178 | 68 | 篮球 |
现在来了一个身高 173 cm、体重 63 kg 的新同学。我们知道他的两个特征,却不知道他的标签。KNN 就根据历史样本与他的距离,推测他更可能属于哪一类。
这里必须提醒一句:身高、体重未必真正决定运动偏好,这只是方便演示的例子。真实问题中的特征需要结合实际业务选择。
1.4 KNN 的完整预测流程
KNN 用于分类时,基本过程分为五步:
- 准备训练数据:保存已有样本的特征和标签。
- 计算距离:计算待预测样本与各个训练样本之间的距离。
- 选出最近的 K 个:按距离排序,挑出距离最小的 K 个训练样本。
- 让邻居投票:统计这 K 个样本分别属于哪些类别。
- 输出预测类别:通常选择票数最多的类别。
用一句话记忆:
计算距离 → 找邻居 → 数票数 → 定类别。
1.5 一个可以手算的分类例子
假设我们用两个数值特征描述一些动物。现在已有五个带标签的样本,要判断新样本 Q 属于“猫”还是“狗”。
新样本坐标为:
\[Q=(3,3)\]已知训练数据如下:
| 训练样本 | 坐标 | 类别 | 到 Q 的欧氏距离 |
|---|---|---|---|
| A | (1,1) | 猫 | $\sqrt{8}\approx2.83$ |
| B | (2,2) | 猫 | $\sqrt{2}\approx1.41$ |
| C | (4,3) | 狗 | $1$ |
| D | (3,4) | 猫 | $1$ |
| E | (5,5) | 狗 | $\sqrt{8}\approx2.83$ |
假设 K=3,只需要看距离最近的三个样本:
- C:狗,距离 1;
- D:猫,距离 1;
- B:猫,距离约 1.41。
投票结果:猫 2 票,狗 1 票。
因此,KNN 把 Q 预测为 猫。
你可以看到,算法并没有事先推导出“猫的特征必须满足什么公式”。它只是直接参考训练集中与 Q 最接近的实例。
1.6 特殊情况:K=1
如果 K=1,那么只参考离新样本最近的一个训练样本。
例如上面的例子中,C 和 D 都距离 Q 为 1,出现最近邻并列;实际程序必须设定一个明确的并列处理规则。若最近的样本唯一,并且它是一只猫,那么新样本就会被预测为猫。
这种只参考一个最近邻居的方法,也叫 最近邻算法(1-NN)。
它非常直接,但也更容易被单个错误样本影响。关于这一点,我们在选择 K 值时再详细讨论。
二、距离度量:计算机怎样知道“谁更像谁”?
KNN 的第一件大事就是定义“近”。在现实生活中,我们通常说两个人“性格很像”“兴趣很像”,但计算机不能直接理解这些表达。
如果一个样本可以用一组数值表示,计算机便能够通过数学公式计算距离。距离越小,通常代表样本越相似。
2.1 将样本表示成坐标
例如:
- 学生 A:身高 170 cm,体重 60 kg;
- 学生 B:身高 175 cm,体重 65 kg。
可以写成:
\[A=(170,60),\qquad B=(175,65)\]它们是二维向量。所谓“二维”,就是每个样本有两个数值特征。
如果加入年龄,样本就变成三维;如果有 100 个特征,就相当于处在一个 100 维的特征空间里。我们可能画不出 100 维空间,但依旧能用公式计算距离。
2.2 曼哈顿距离(Manhattan Distance,$L_1$)
想象你在一个道路横平竖直的城市中走路。你不能直接穿过建筑物,只能沿街道先横向走,再纵向走。
曼哈顿距离就是:各个坐标相差多少,全部取绝对值后加起来。
二维公式为:
\[d_1(A,B)=|x_A-x_B|+|y_A-y_B|\]设:
\[A=(1,2),\qquad B=(4,6)\]横向差 3,纵向差 4,所以:
\[d_1(A,B)=|4-1|+|6-2|=3+4=\boxed{7}\]在更多维度的情况下,只要把每一维的差值绝对值加起来即可。
2.3 欧氏距离(Euclidean Distance,$L_2$)
欧氏距离就是我们最熟悉的两点之间的直线距离。
想象在操场上从 A 点直接走向 B 点,不必沿着横竖的街道绕路。它就是勾股定理的应用。
二维公式:
\[d_2(A,B)=\sqrt{(x_A-x_B)^2+(y_A-y_B)^2}\]仍然取:
\[A=(1,2),\qquad B=(4,6)\]那么:
\[d_2(A,B)=\sqrt{(4-1)^2+(6-2)^2}\] \[=\sqrt{3^2+4^2}=\sqrt{9+16}=\boxed{5}\]欧氏距离是 KNN 中非常常见的选择。不过“常见”不意味着对任何数据都最合适。
2.4 切比雪夫距离(Chebyshev Distance,$L_\infty$)
切比雪夫距离比较特别:它只关心各个坐标差距中最大的那个。
二维公式:
\[d_\infty(A,B)=\max\{|x_A-x_B|,|y_A-y_B|\}\]仍然是上面的两个点:
- 横向相差 3;
- 纵向相差 4。
最大差距是 4,因此:
\[d_\infty(A,B)=\max(3,4)=\boxed{4}\]你可以想象一个棋盘上的国王:如果一次能向任意相邻方向(包括斜向)走一步,从一个位置到另一个位置,最少需要的步数就与切比雪夫距离对应。
2.5 三种距离的对比
对于同样的点 $A=(1,2)$、$B=(4,6)$:
| 距离 | 通俗理解 | 计算方式 | 结果 |
|---|---|---|---|
| 曼哈顿距离 $L_1$ | 沿横竖街道走 | 各维绝对差值之和 | 7 |
| 欧氏距离 $L_2$ | 两点直线距离 | 各维差值平方和开方 | 5 |
| 切比雪夫距离 $L_\infty$ | 只看最大坐标差 | 各维绝对差值的最大值 | 4 |
这三种结果不同,并不是谁算错了,而是因为“距离”有不同的定义。
2.6 同一组数据,换一种距离,最近邻可能会改变
看下面这个例子。已有三个点:
\[x_1=(1,1),\quad x_2=(5,1),\quad x_3=(4,4)\]我们站在 $x_1$,想比较 $x_2$ 与 $x_3$ 哪一个更近。
先算曼哈顿距离:
\[d_1(x_1,x_2)=|5-1|+|1-1|=4\] \[d_1(x_1,x_3)=|4-1|+|4-1|=6\]所以在曼哈顿距离下,$x_2$ 更近。
再算欧氏距离:
\[d_2(x_1,x_2)=\sqrt{4^2+0^2}=4\] \[d_2(x_1,x_3)=\sqrt{3^2+3^2}=\sqrt{18}\approx4.24\]欧氏距离下,仍然是 $x_2$ 更近。
最后算切比雪夫距离:
\[d_\infty(x_1,x_2)=\max(4,0)=4\] \[d_\infty(x_1,x_3)=\max(3,3)=3\]这次变成了 $x_3$ 更近。
汇总如下:
| 距离类型 | $x_1$ 到 $x_2$ | $x_1$ 到 $x_3$ | 最近邻 |
|---|---|---|---|
| 曼哈顿 | 4 | 6 | $x_2$ |
| 欧氏 | 4 | $\sqrt{18}\approx4.24$ | $x_2$ |
| 切比雪夫 | 4 | 3 | $x_3$ |
重要结论:距离度量的选择会改变邻居的排列顺序,因而可能改变最终分类结果。
2.7 把三种距离统一起来:闵可夫斯基距离
它们其实可以用一个更一般的公式表示:
\[d_p(x,y)=\left(\sum_{j=1}^{m}|x_j-y_j|^p\right)^{1/p},\qquad p\ge1\]其中 $m$ 是特征维数:
- $p=1$ 时,得到曼哈顿距离;
- $p=2$ 时,得到欧氏距离;
- 当 $p$ 越来越大并趋向无穷时,得到切比雪夫距离。
这个家族叫闵可夫斯基距离(Minkowski Distance)。
入门阶段不用刻意背复杂的推导,记住:曼哈顿是求和,欧氏是平方和开方,切比雪夫是取最大值。
三、使用距离之前,为什么必须处理特征?
现在出现一个实际问题:并不是所有数值都适合直接拿来计算距离。
假设用两个特征衡量学生:
- 身高:单位是厘米,数值大约 150~190;
- 月零花钱:单位是元,数值可能从几十到几千。
如果我们直接使用欧氏距离,零花钱的差异可能远远盖过身高的差异。即便我们原本认为两个特征同样重要,实际计算却让数值范围大的那一项拥有了更大影响力。
3.1 数值尺度问题
考虑两个数据记录:
\[x=(\text{true},0.2,254)\] \[y=(\text{false},0.1,194)\]它们分别包含一个布尔属性、一个小数属性和一个较大数值的属性。
如果把布尔值简单编码成 true=1、false=0,那么三项差距分别是:
- 第一项:1;
- 第二项:0.1;
- 第三项:60。
直接算欧氏距离,第三项的平方是 $60^2=3600$,而第二项的平方只有 $0.1^2=0.01$。第三项几乎控制了整个距离。
这不一定合理。数值大只说明量纲或取值范围大,不等于这个特征对分类一定更重要。
3.2 归一化:把不同特征放在相近范围
一个常见方法是最小—最大归一化:
\[x'=\frac{x-x_{\min}}{x_{\max}-x_{\min}}\]如果一项特征在训练数据中的最小值是 100,最大值是 200,那么原来的 150 就变成:
\[x'=\frac{150-100}{200-100}=0.5\]这样通常可以把训练数据中该特征的数值映射到 0~1 的范围。
但注意:如果后续出现超过原训练范围的新值,归一化结果也可能小于 0 或大于 1;这不代表计算错误。
3.3 标准化:以均值为中心调整尺度
另一种常见方法是 Z-score 标准化:
\[z=\frac{x-\mu}{\sigma}\]其中:
- $\mu$ 是该特征在训练数据中的均值;
- $\sigma$ 是标准差。
标准化后的特征通常具有“均值接近 0、标准差接近 1”的性质,能够降低不同特征量纲带来的不合理影响。
归一化与标准化没有绝对的优劣,需要结合数据分布和验证结果选择。对于严重偏斜或异常值很多的特征,有时还需要其他处理方法。
很重要的实操原则:只能用训练集计算归一化或标准化所需的统计量,再把同一套变换应用到验证集、测试集和未来的新数据。 如果提前查看了测试集的统计信息,就可能造成“数据泄漏”,让测试结果看起来比实际效果更好。
3.4 不是数值的特征该怎么办?
有些特征是:
- 是否有某种属性:是/否;
- 喜欢的颜色:红、蓝、绿;
- 季节:春、夏、秋、冬。
这些数据不能想当然地当成普通连续数值。例如把颜色编码为红=1、蓝=2、绿=3,会让“红与绿的数值距离”为 2,但这种大小关系通常并没有实际意义。
常见的办法包括:
- 对真正只有两种状态的特征使用 0/1 编码;
- 对无大小顺序的类别使用独热编码(one-hot encoding);
- 对混合类型的数据设计合适的距离函数。
实际使用时,也需要处理缺失值,因为普通的距离公式不能直接处理“某个坐标没有数值”的情况。
3.5 不相关的特征会误导 KNN
假设我们想根据健康数据判断某个样本的风险类别,记录了:
- 体温;
- 血压;
- 心率;
- 鞋码。
前三项可能与目标有比较直接的关系,鞋码却可能几乎没有帮助。
如果把所有特征一股脑地放进距离公式,鞋码仍会影响“谁离谁最近”。加入大量无关特征后,本来很相似的样本反而可能被距离计算分开。
因此,特征越多不一定越好;与任务有关、能够代表真实相似性的特征更重要。
四、K 值怎么选?小 K 和大 K 为什么都可能出问题?
理解了距离,接下来就需要决定:到底找几个邻居?
这个数量就是 K,也是 KNN 最重要的超参数之一。
“超参数”可以理解成:算法运行前需要人为设定或通过验证方法选择的配置,而不是模型从数据中自动学出的普通系数。
4.1 K 太小:容易被个别样本带偏
假设某个区域里实际上有 9 只猫、1 只狗,而那只狗刚好位于待预测样本的最近位置。
如果 K=1,算法只看这一只狗,就会把新样本判为狗。
但如果最近的 5 个邻居中有 4 只猫、1 只狗,K=5 就会预测猫。
这说明:K 过小时,算法非常在意个别点,对噪声、误标数据和异常值比较敏感。
比如一只猫被错误标记为狗,如果新样本恰好靠近这个错误标签,1-NN 就可能立刻受到影响。
在机器学习中,这种对训练数据的局部细节和偶然波动过分敏感的现象,通常与过拟合(overfitting)相关。
通俗地说:就像复习考试时,只背几道碰巧遇到的题,结果换一道题便不会了。
4.2 K 太大:容易忽略局部差别
再想象另一种情况:K=100。
这意味着不管新样本处于什么位置,都要参考最近的 100 个训练样本。它们中可能已经有很多离新样本很远、并不真正相似。
极端情况下,如果 K 等于整个训练集的样本数 N,那么每次预测都会参考全部训练数据。使用普通多数投票时,只要全局类别分布不变,模型通常会对所有新样本预测训练集中数量最多的那个类别。
这样就很难根据局部位置做出精细判断了。
这与机器学习中的欠拟合(underfitting)有关:模型太粗糙,没能抓住真正存在的类别结构。
通俗地说:只记住“大家一般是什么情况”,却忽略每个人的具体差异。
4.3 小 K 与大 K 的对比
| 对比角度 | K 较小 | K 较大 |
|---|---|---|
| 参考范围 | 非常局部 | 更宽的邻域 |
| 对细节的反应 | 很敏感 | 相对平缓 |
| 对噪声的反应 | 通常更敏感 | 通常更稳定 |
| 可能的问题 | 过拟合 | 欠拟合 |
| 分类边界 | 更曲折、复杂 | 往往更平滑 |
从“偏差—方差”的角度,也可以把它理解成:小 K 往往方差较高,大 K 往往偏差较高。这是一种常见趋势,而不是对每个数据集都绝对成立的定律。
4.4 K 取奇数就一定不会平票吗?
一个常见建议是:做二分类时可以尝试奇数 K,例如 3、5、7,这能避免在普通多数投票中出现两类票数完全相同的情况。
但要注意两个限制:
- 这只对二分类的普通投票有这个效果;如果有三种或更多类别,K 是奇数也可能平票。
- 即使没有投票平局,也可能出现多个训练样本到新样本的距离相同,需要定义邻居选取的并列处理规则。
因此,奇数 K 是实用的小技巧,并不是普遍的数学保证。
4.5 怎样选一个比较合适的 K?
最可靠的思路之一是:在训练数据上进行交叉验证。
例如你可以尝试:
\[K\in\{1,3,5,7,9,11\}\]然后比较它们在未参与对应训练的验证数据上的表现,选择效果较好的 K。
把这个过程比作考试:
- 训练集相当于平时学习使用的教材和练习;
- 验证集相当于模拟考试,用来挑选学习方法;
- 测试集相当于最终考试,用来评估最终方案。
只看训练集准确率是不够的。K=1 在训练样本上通常表现得非常好,但不代表面对新样本时也会最好。
4.6 什么是 K 折交叉验证?
假设有 100 条可用于训练和调参的数据,我们决定做 5 折交叉验证。
简单理解为:
- 把数据分成大致等量的 5 份;
- 第 1 轮,用其中 4 份作为训练数据,剩下 1 份用于验证;
- 第 2 轮,换另一份做验证;
- 一共做 5 轮,让每份数据都轮流承担一次验证任务;
- 计算每个候选 K 在这 5 轮中的平均表现。
例如得到如下假设结果:
| K 值 | 五折平均验证准确率(示例) |
|---|---|
| 1 | 84% |
| 3 | 89% |
| 5 | 92% |
| 7 | 90% |
| 9 | 87% |
在这组示例结果中,我们倾向于先选择 K=5,再使用独立测试集进行最终评价。
如果类别比例不平衡,通常还要考虑分层抽样,让各折中的类别比例尽量合理;评价指标也不能只看准确率。
4.7 选择 K 时需要同时考虑哪些因素?
K 并不是孤立存在的。
- 如果距离度量改变了,合适的 K 可能改变。
- 如果做了标准化,邻居排列可能改变,合适的 K 也可能改变。
- 如果样本很多,K 可尝试的范围通常与小数据集不同。
- 如果任务更在意少数类的识别能力,最优 K 未必是“准确率最高的 K”。
因此,一个可靠的实践方式是把预处理、距离度量和 K 值当作一套完整方案共同验证。
五、分类决策规则:找到邻居之后,怎样做决定?
找到了最近的 K 个邻居,下一步就是把邻居的标签转化为预测结果。
最常见的是多数表决,也可以使用距离加权投票。
5.1 多数表决:谁票数最多,就选谁
假设 K=7,最近的 7 个邻居分别属于:
- 猫:4 个;
- 狗:2 个;
- 兔子:1 个。
投票结果是猫 4 票、狗 2 票、兔子 1 票,所以预测类别为猫。
对于普通分类 KNN,可以写成:
\[\hat y(x)=\arg\max_c\sum_{i\in N_K(x)}\mathbf1(y_i=c)\]这里:
- $x$ 是待预测的新样本;
- $N_K(x)$ 是距离它最近的 K 个训练样本的索引集合;
- $y_i$ 是第 $i$ 个邻居的已知类别;
- $\mathbf1(y_i=c)$ 表示“如果第 $i$ 个邻居属于类别 c,就记 1 票,否则记 0 票”;
- $\arg\max$ 表示找出票数最多的类别。
不用害怕公式:它只是把“数一数哪个类别出现最多”写成了数学语言。
5.2 多数表决为什么与“经验风险最小化”有关?
这个词听起来很抽象,可以把它拆开:
- 经验:我们手头已经看到的样本;
- 风险:预测错的代价;
- 最小化:选一个让错误代价尽量小的结果。
假设最近的 7 个邻居中有 4 只猫、2 只狗、1 只兔子。
如果我们把新样本判断成:
- 猫,那么邻居中有 3 个标签不一致;
- 狗,那么邻居中有 5 个标签不一致;
- 兔子,那么邻居中有 6 个标签不一致。
显然,选择“猫”能让这个局部邻域中标签不一致的数量最少。
如果用最简单的 0-1 损失衡量错误:预测正确损失为 0,预测错误损失为 1,那么预测类别 c 的局部平均损失为:
\[R(c\mid x)=\frac1K\sum_{i\in N_K(x)}\mathbf1(y_i\ne c)\]选择让这个值最小的类别,恰好等价于普通多数表决。
所以,多数表决可以理解为:在最近的 K 个邻居中,让 0-1 分类错误尽可能少。
请注意,这里指的是“局部邻域内”的经验风险,不等于它能保证在未来所有新数据上的错误率都最低。
5.3 平票了怎么办?
例如 K=4:
- 猫 2 票;
- 狗 2 票。
这时普通多数表决无法唯一确定类别。常用的处理办法包括:
- 采用固定的类别优先级或稳定的实现规则;
- 参考这些邻居的距离,让更近的样本拥有更高权重;
- 重新考虑 K 的设置。
真实程序一定要有明确的平票处理逻辑,这样同样的数据才能得到可复现的结果。
5.4 距离加权投票:更近的邻居说话更有分量
普通投票默认每个邻居都是 1 票。可是如果最近的邻居只有 0.01 的距离,最远的第 K 个邻居有 100 的距离,让它们投同样重的一票是否合理?
一种改进办法是让距离越近的邻居权重越大。例如可以设:
\[w_i=\frac{1}{d(x,x_i)+\varepsilon}\]其中 $\varepsilon>0$ 是一个很小的数,用来避免距离为 0 时出现除以 0 的问题。
然后把每一类得到的权重相加,选择总权重最大的类别。
举个例子,K=3:
| 邻居 | 类别 | 距离 | 示例权重(约) |
|---|---|---|---|
| A | 猫 | 0.2 | 5.0 |
| B | 狗 | 1.0 | 1.0 |
| C | 狗 | 1.2 | 0.83 |
普通投票:狗 2 票、猫 1 票,预测狗。
距离加权投票:猫约 5.0 分、狗约 1.83 分,预测猫。
这说明:加权投票可能改变分类结果。
但距离加权不一定总比普通投票好。特别是最近邻本身存在噪声或错误标签时,给它太大的权重也可能放大问题。最好仍然通过验证决定是否使用。
六、KNN 的分类边界和“懒惰学习”
6.1 分类边界是什么?
想象平面上有许多红点和蓝点。我们在平面上随便找一个新位置,KNN 都会根据邻近的红、蓝样本给它一个类别。
当我们把所有预测为红色的区域涂红,把所有预测为蓝色的区域涂蓝,两种颜色交界的线就是分类决策边界。
K 小的时候,预测容易随着单个邻居的变化而变化,因此边界可能非常曲折。
K 大的时候,需要多个邻居共同改变投票结果,边界通常更加平滑。
这是理解“过拟合与欠拟合”的另一种方式:
- 边界过于曲折,可能把训练数据中的噪声当成真实规律;
- 边界过于平滑,可能漏掉真实存在的复杂类别结构。
6.2 为什么 KNN 被称为“懒惰学习”?
许多机器学习方法会在训练阶段学习出参数,例如一条分类直线的系数。
KNN 通常不需要先学习这样的一组全局分类系数。它主要把训练样本保存起来,等新样本到来时,再计算距离、寻找邻居、作出预测。
所以它被称为懒惰学习(lazy learning)。
这个名字并不是说算法不好,而是说它把大量工作放在了预测阶段。
因此,KNN 有一个很鲜明的特点:
训练过程相对简单,但预测可能比较费时间。
当然,如果事先建立了 kd 树等索引结构,那么准备阶段依然需要一定工作量。
七、KNN 为什么会慢?怎样引出 kd 树?
假设你保存了 100 万个训练样本,现在要预测一个新样本的类别。
最直接的方法是:
- 计算新样本到 100 万个训练样本的距离;
- 找出最近的 K 个;
- 进行投票。
如果新样本只有一条,或许还能接受。但如果每天要预测几十万条新数据,重复做距离计算可能造成很大的时间开销。
这时我们自然会想:
能不能提前把训练样本整理得更有条理,让寻找邻居时不必每次都检查全部样本?
kd 树就是用来组织空间中的点,并加速邻居搜索的一种数据结构。
7.1 用“找便利店”理解 kd 树
假设你在一座大城市里寻找离自己最近的便利店。
办法一:查看全市每家便利店,分别算距离,再比较。
办法二:先把城市分成东区和西区,然后把每个区域继续划成南区和北区,再划成更小的区域。查找时优先搜索自己所在区域附近的便利店,并在必要时检查邻近区域。
第二种办法与 kd 树的思想很像:不断划分空间,并利用划分信息排除不可能包含更近邻居的区域。
7.2 注意区分 KNN 的 K 和 kd 树的 k
这两个字母很容易混淆:
- KNN 的 K:参考多少个邻居;
- kd 树的 k:数据所在空间的特征维数。比如二维点构成的是 2d 树的典型应用场景。
它们表示完全不同的概念。一个有 100 个特征的数据集可以使用 kd 树组织点,同时预测时仍然选择 K=5 个最近邻。
八、kd 树怎样构造?一步一步手工建树
下面用六个二维点构造一棵 kd 树:
\[(2,3),\ (5,4),\ (9,6),\ (4,7),\ (8,1),\ (7,2)\]为了便于理解,我们采用一个常见的建树规则:
- 这一层选一个坐标轴;
- 按该坐标轴的数值排序,选择中位数附近的一个样本作为当前结点;
- 比它小的点放到一边,大的点放到另一边;
- 下一层切换坐标轴,递归处理左右两边。
由于偶数个点时存在两种中位数候选,而且相等坐标的处理方式可以不同,同一组点不一定只有一种合法 kd 树。我们只需要明确规则、前后一致即可。
8.1 第一步:按照 x 坐标进行划分
先按 x 值排序:
| 顺序 | 点 | x 坐标 |
|---|---|---|
| 1 | (2,3) | 2 |
| 2 | (4,7) | 4 |
| 3 | (5,4) | 5 |
| 4 | (7,2) | 7 |
| 5 | (8,1) | 8 |
| 6 | (9,6) | 9 |
选取靠近中位数的 $(7,2)$ 作为根结点。
它相当于画了一条竖直分割线:
\[x=7\]于是:
- 左侧点:$(2,3),(4,7),(5,4)$;
- 右侧点:$(8,1),(9,6)$。
根结点本身保存样本 $(7,2)$。
8.2 第二步:切换到 y 坐标
在左侧区域,点的 y 坐标分别为:
\[3,\ 7,\ 4\]排序得到:
\[3,\ 4,\ 7\]选中位点 $(5,4)$ 作为左子树的根。它用水平线 $y=4$ 把左区域继续分成上下两部分:
- 较小的 y 一侧:$(2,3)$;
- 较大的 y 一侧:$(4,7)$。
在根结点右侧区域,两个点是 $(8,1)$ 和 $(9,6)$。这一层同样按 y 坐标划分。这里选择 $(9,6)$ 作为右子树的根,$(8,1)$ 进入它的一侧子树。
8.3 第三步:继续划分,直到安排完所有点
最后形成一棵可能的 kd 树:
flowchart TD
A["(7,2) 按 x 划分"]
B["(5,4) 按 y 划分"]
C["(9,6) 按 y 划分"]
D["(2,3)"]
E["(4,7)"]
F["(8,1)"]
A -->|x 较小| B
A -->|x 较大| C
B -->|y 较小| D
B -->|y 较大| E
C -->|y 较小| F
注意,它是一棵空间划分树,不是按照类别标签建立的分类决策树。
kd 树结点通常保存样本点,并记录该层使用的划分维度。KNN 可以利用它更快地寻找最近邻,但分类标签最终仍然由邻居投票决定。
8.4 kd 树为什么要交替选择坐标轴?
如果在二维空间里永远只按 x 划分,空间就会不断被竖线切开,可能无法有效利用 y 方向的信息。
所以常见做法是按维度循环:
\[x\rightarrow y\rightarrow x\rightarrow y\rightarrow\cdots\]如果是三维空间,则可以轮流使用:
\[x\rightarrow y\rightarrow z\rightarrow x\rightarrow\cdots\]也有其他建树策略,比如根据当前数据分布选择变化较大的维度。无论哪一种,目的都是让空间划分便于后续搜索。
九、kd 树怎样搜索最近邻?为什么不能只向下走?
建树只是第一步。真正难理解的是:给定一个新点,如何用树找到它的最近邻?
先记住两个动作:
向下查找,向上回溯。
9.1 第一阶段:从根结点向下查找
假设要查询的点是:
\[Q=(5,3)\]我们使用上一节建好的树。
从根结点 $(7,2)$ 出发:
- 根结点按 x 划分。因为 $Q$ 的 x=5 小于 7,所以先走左子树。
- 到达 $(5,4)$,这一层按 y 划分。因为 $Q$ 的 y=3 小于 4,所以继续走它的较小 y 一侧。
- 到达叶结点 $(2,3)$。
这一步的目的,是按照空间划分迅速找到一个与 Q 可能接近的区域。
9.2 第二阶段:记录当前最近点
先算 Q 到叶结点 $(2,3)$ 的欧氏距离:
\[d(Q,(2,3))=\sqrt{(5-2)^2+(3-3)^2}=3\]于是暂时记录:
- 当前候选最近邻:$(2,3)$;
- 当前最短距离:3。
但它只是暂时的答案。
9.3 第三阶段:向上回溯,检查父结点
沿原路径返回到父结点 $(5,4)$,计算:
\[d(Q,(5,4))=\sqrt{(5-5)^2+(3-4)^2}=1\]距离 1 明显小于之前的距离 3,所以更新:
- 当前候选最近邻:$(5,4)$;
- 当前最短距离:1。
这里已经能看出:第一次向下走到的叶结点,不一定是真正最近的点。 如果只走到叶子就停止,会得出错误结果。
9.4 为什么还要考虑另一侧子树?
返回父结点时,除了比较父结点本身,还要思考:被我们暂时没走的另一侧区域,会不会藏着更近的点?
例如某条分割线是 $y=4$,而查询点的 y=3。查询点到这条分割线的最短距离是:
\[|3-4|=1\]如果目前找到的最近邻距离也是 1,那么另一侧空间在几何上仍有可能出现距离不大于 1 的点,是否需要访问取决于实现的并列规则与搜索目标。
更加一般地说,采用欧氏距离时:
- 如果查询点到分割线的距离大于当前最短距离,另一侧不可能存在更近的点,可以剪枝跳过;
- 如果查询点到分割线的距离小于当前最短距离,另一侧有可能存在更近的点,需要继续搜索;
- 如果刚好相等,通常需要按照是否保留等距离候选的要求谨慎处理。
这里所谓剪枝,就是“通过数学判断,提前排除不值得搜索的一整块区域”。
9.5 用“圆是否碰到分割线”理解回溯
想象我们以查询点 Q 为圆心,以当前最短距离 r 为半径画一个圆。
- 圆完全没有碰到某个被跳过的区域,那么该区域不可能有距离小于 r 的点,可跳过。
- 圆与另一个区域相交,那么那个区域可能有更近的点,需要继续检查。
二维时我们可以画圆;三维时对应一个球;更高维时则是超球。直观原理是一样的。
实际的 kd 树实现通常利用分割平面或子树包围盒的距离来判断是否剪枝。上面的“圆碰不碰线”是适用于理解二维欧氏距离搜索的直观模型。
9.6 搜索整个过程总结
一次最近邻搜索可以理解为:
- 从根结点出发,根据查询点与划分坐标的关系,优先沿一条路径向下;
- 到达叶结点后,记录当前最好的候选点;
- 逐层回溯,比较路径上的结点是否更近;
- 判断另一侧子树是否可能包含更近的点;
- 可能的话进入该子树,不可能的话剪枝;
- 最终得到最近邻。
如果不是找 1 个最近邻,而是找 K 个最近邻,思路仍然类似,只是要维护当前最好的 K 个候选,并利用其中距离最远的候选来决定哪些区域可以剪枝。
十、kd 树到底能快多少?有什么限制?
10.1 暴力搜索与 kd 树搜索
暴力搜索(brute force)通常需要对每个训练样本计算距离。设训练样本数为 N、每个样本有 d 个数值特征,那么对一个查询点计算全部距离,数量级通常是:
\[O(Nd)\]对于固定且较低的维度,常被简化描述为 $O(N)$。
kd 树搜索有机会避开大量不相关区域。在分布较友好、树较平衡、维度较低等条件下,单次近邻查询常能显著快于完整扫描,理想或典型情况下可接近 $O(\log N)$ 的搜索表现。
不过,这不是对所有数据的保证。
10.2 为什么最坏情况下仍可能很慢?
如果数据分布不利、查询点附近有大量候选区域,或者剪枝条件经常无法成立,搜索就可能访问很多结点。
最坏情况下,kd 树也可能需要检查几乎所有训练样本,搜索复杂度退化到与线性扫描同量级。
也就是说:kd 树的作用是尽可能减少候选,而不是保证永远只看很少的数据点。
10.3 维度一高,kd 树可能不再占优势
二维空间可以轻松理解“附近的一个小圆”。但如果数据有几百个甚至几千个特征,“附近”就变得难以有效划分。
高维空间中经常出现维度灾难(curse of dimensionality):
- 样本在高维空间中可能显得更加稀疏;
- 最近和较远样本之间的距离差距可能变得不明显;
- 很多子区域都无法被轻易排除;
- kd 树的剪枝效果下降。
因此,当特征维度非常高时,直接扫描、其他索引结构或近似最近邻搜索方案反而可能更合适。
10.4 kd 树与 KNN 的关系
这两个概念要分清:
| 概念 | 主要回答的问题 | 作用 |
|---|---|---|
| KNN | 新样本属于哪一类? | 根据邻居标签进行预测 |
| kd 树 | 怎样更快找到附近的样本? | 组织空间、加速搜索 |
kd 树不是另一种投票分类规则,而是一种帮助 KNN 查找邻居的数据结构。
即使不使用 kd 树,KNN 仍然成立;只不过可能需要直接计算所有训练样本的距离。
十一、KNN 在实际使用中经常遇到哪些问题?
11.1 错误标签与异常值
想象训练集中有一只猫,误被标记成“狗”。如果新样本恰好靠近它,而 K 又取得很小,KNN 很可能被这个错误样本带偏。
针对这种情况,可以考虑:
- 检查并更正明显错误的标签;
- 寻找合理的 K,减少单点噪声影响;
- 在验证中比较普通投票与距离加权投票;
- 对明显异常的样本进行谨慎分析,而不是不加判断地删除。
特别要注意:某个样本“看起来奇怪”,并不等于它一定是错误数据。真实世界中的少数类别和罕见情况,本来就可能很少见。
11.2 两类交界的样本很难判断
在猫和狗两类样本交错的边界区域,可能出现:
- 最近的 3 个邻居是 2 猫、1 狗;
- 最近的 5 个邻居是 2 猫、3 狗。
这时 K=3 与 K=5 可能给出不同的答案。
这是因为边界附近本来就比较难分类。不同 K 使用不同的邻域范围,因此对“局部大多数”的判断可能不同。
不能简单认为只要增加 K 就一定解决了问题。有时数据本身的类别定义就存在重叠,任何分类器都会遇到不确定性。
11.3 类别数量严重不平衡
假设训练集中有:
- 990 个正常样本;
- 10 个异常样本。
如果我们总预测“正常”,整体准确率就高达 99%,但异常样本一个都没识别出来。
对于 KNN,类别比例失衡还可能让少数类样本在多数投票中处于不利位置。
所以此时不能只看准确率,可以根据任务关注:
- 精确率(precision):预测为异常的样本中,有多少真的异常;
- 召回率(recall):真正异常的样本中,有多少被找到;
- F1 分数:综合精确率与召回率的一个指标。
如果少数类尤其重要,可以考虑合理的重采样、权重设计或其他分类方法,并通过正确的验证流程评价。
11.4 重复样本与冗余数据
如果训练集中保存了大量完全重复或极其相似的样本,会增加存储与搜索负担。
适当清理冗余数据可能提高效率,但有一个容易忽视的问题:重复样本也会影响投票的票数。
例如某个位置有 20 条相同类别的记录,删除其中 19 条以后,附近的类别比例可能发生明显变化。
因此,去重并不总是中性的预处理步骤。它可能改变 KNN 的预测,需要检查重复记录是否代表真实频率、采集重复还是数据错误。
11.5 特征数量太多
特征过多可能带来两个问题:
- 不相关特征影响距离,让“真正相似”的样本不再显得近;
- 高维距离搜索效率下降,kd 树可能难以有效剪枝。
可以尝试特征选择、降维或构造更有代表性的特征,再用验证结果判断是否改善。
11.6 数据是否真的适合用“距离”描述?
这一点常被初学者忽略。
KNN 的全部预测建立在一个假设上:在选定的特征与距离定义下,近邻确实更有可能拥有相同标签。
如果这个前提不成立,KNN 就很难表现好。
例如,把两个用户的账号编号当成连续数值来算欧氏距离,通常没有意义。编号 10001 的用户并不必然比编号 90001 的用户更像编号 10000 的用户。
因此,KNN 的难点并不只是会不会代入公式,更在于能否建立有意义的特征表达和距离度量。
十二、KNN 不只能做分类,也能做回归
到目前为止,我们一直讨论“猫还是狗”这种分类问题。
但 KNN 还可以做回归(regression):预测一个连续数值。
12.1 分类与回归的区别
- 分类:预测离散类别,例如“合格/不合格”“猫/狗/兔子”。
- 回归:预测连续数值,例如房价、温度、某个商品的需求量。
KNN 用于分类时,让邻居投票;用于回归时,通常求邻居目标数值的平均值。
12.2 KNN 回归的一个例子
假设你想估计一套房子的价格,找到与它面积、位置等特征最接近的 3 套已成交房屋:
| 邻居房屋 | 成交价 |
|---|---|
| A | 100 万元 |
| B | 110 万元 |
| C | 120 万元 |
如果采用普通 KNN 回归,预测值就是:
\[\hat y=\frac{100+110+120}{3}=110\text{ 万元}\]如果使用距离加权平均,则可以让更相似的房屋影响更大。
KNN 回归并不改变“找最近的 K 个邻居”的基本思想,只是把“多数表决”换成了“数值聚合”。
十三、把 KNN 真正用起来:一套完整流程
前面讲了很多概念。现在把它们连成一条实际工作流程。
假设我们要根据一批历史数据预测新样本的类别。
第一步:明确预测目标
先问清楚:
- 我们预测的是类别,还是具体数值?
- 哪些特征可以在预测时获得?
- 预测错误会带来什么后果?
- 是否特别需要识别少数类?
这些问题会影响特征、距离、投票规则和评价指标。
第二步:准备数据并划分训练集、验证集和测试集
通常需要把数据分成不同用途:
- 训练数据:供 KNN 存储并用于查找邻居;
- 验证数据:选择 K、距离度量及预处理策略;
- 测试数据:尽量只在方案确定之后用于最终评价。
如果使用交叉验证,可以用训练部分进行多折验证。对于时间序列、同一用户的多条记录等特殊数据,还需要避免让高度关联的信息同时出现在训练和验证/测试中。
第三步:清洗并转换特征
包括:
- 检查错误值与缺失值;
- 处理类别特征;
- 对数值特征进行合适的标准化或归一化;
- 移除明显不应参与距离计算的标识符和无关字段。
记住:所有需要从数据学习的预处理参数,只能在对应训练折上拟合,再应用到该折的验证数据。
第四步:选择距离与 K
例如准备比较以下候选方案:
- 距离:欧氏距离、曼哈顿距离;
- K:1、3、5、7、9;
- 投票:普通投票、距离加权投票。
利用交叉验证比较它们的效果,选出表现较好的组合。
第五步:考虑搜索方式
如果数据规模不大,直接计算距离可能已经足够。
如果数据量很大、维度较低,可以考虑 kd 树等索引结构。
如果维度很高,则需要实际测量索引效果,不能默认 kd 树一定更快。
第六步:用确定好的方案预测新数据
以分类 KNN 为例,算法伪代码可以写成:
输入:
训练样本 X、训练标签 y
待预测样本 q
邻居数量 K
距离函数 distance
1. 用训练阶段确定的规则处理 q 的特征
2. 对每个训练样本 X[i]:
计算 d[i] = distance(q, X[i])
3. 选出距离最小的 K 个样本索引
4. 统计这些邻居的标签票数
(或按距离加权统计)
5. 按已定义的平票规则选出票数最多的类别
6. 返回该类别
这个伪代码描述的是最直观的暴力搜索版本。如果使用 kd 树,第 2~3 步会被更高效的索引搜索替代,但后面的分类逻辑仍然相同。
第七步:评价真实效果
至少需要关注两个方面:
预测效果:
- 分类:准确率、精确率、召回率、F1、混淆矩阵等;
- 回归:平均绝对误差、均方误差等。
运行成本:
- 保存训练数据需要多少内存;
- 每条新样本预测需要多长时间;
- 训练集变大以后是否仍然可用。
如果效果不理想,先检查距离是否有意义、特征是否合适、预处理是否正确,然后再考虑调整 K 或更换算法。
十四、KNN 的优点与缺点
14.1 优点
第一,原理直观。
找附近的人、数票、做判断,容易理解,也容易作为机器学习入门算法。
第二,传统 KNN 不需要拟合复杂的全局参数。
数据准备好以后,就可以利用邻居搜索进行预测。
第三,天然支持多类别分类。
类别不一定只有两种。只要能够统计邻居标签,就可以在多个类别之间做选择。
第四,能形成比较灵活的分类边界。
因为它参考局部样本,不要求分类边界一定是一条直线。
第五,同一种近邻思想既能用于分类,也能用于回归。
只需要调整邻居结果的聚合方式。
14.2 缺点
第一,数据多时预测成本可能很高。
尤其是直接扫描全部训练样本的做法。
第二,非常依赖特征与距离度量。
距离设计得不好,找到的邻居也可能没有真正的参考价值。
第三,可能受到噪声、异常值及标签错误的影响。
特别是 K 很小的时候。
第四,需要合理选择 K 与投票规则。
不同选择会影响分类边界和泛化能力。
第五,高维数据可能不适合直接使用传统距离。
维度灾难既影响邻居质量,也可能使搜索加速失效。
第六,训练数据需要保留,存储成本不可忽略。
训练样本越多,占用的存储与内存往往越多。
十五、把所有内容串起来
现在我们回到最开始的问题:一个陌生的新样本来了,KNN 要怎样处理?
其实只要回答几个核心问题:
问题一:谁和它最像?
要先把样本转成有意义的特征,再选择合适的距离度量。
这一步涉及:
- 欧氏距离;
- 曼哈顿距离;
- 切比雪夫距离;
- 归一化与标准化;
- 特征选择、编码和缺失值处理。
如果“像”定义得不合理,KNN 从一开始就找错了人。
问题二:应该参考几个人?
这就是 K 值选择。
- K 太小:容易受个别邻居影响,可能过拟合;
- K 太大:容易忽略局部结构,可能欠拟合;
- 实际选 K:用交叉验证评估不同候选值。
问题三:邻居们意见不统一,听谁的?
这就是分类决策规则。
- 普通多数表决:谁的票数最多就选谁;
- 距离加权表决:越近的邻居通常越有话语权;
- 平票时:使用明确、可复现的处理规则。
如果邻居数量很大、查找太慢,还可以额外问一个“工程问题”:
问题四:怎样更快找到这些邻居?
一种办法是用 kd 树整理数据:
- 按不同坐标维度反复划分空间;
- 查询时先向下找到候选;
- 再向上回溯,必要时搜索其他分支;
- 对不可能包含更近点的区域进行剪枝。
kd 树优化的是寻找邻居的过程,不改变 KNN “依邻居作判断”的核心思想。