logo

KNN分类算法

作者
Modified on
Reading time
12 分钟阅读:..评论:..

KNN没有训练一个模型去学习输入到输出的映射,只是会把训练数据存储在kdtree的数据结构中,唯一训练的是kdtree中每个节点的split规则。而kdtree只是加速找NN的过程

KNN,K-Nearest Neighbor algorithm,所谓K近邻算法,即是给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的K个实例(也就是上面所说的K个邻居),这K个实例的多数属于某个类,就把该输入实例分类到这个类中

如上图所示,有两类不同的样本数据,分别用蓝色的小正方形和红色的小三角形表示,而图正中间的那个绿色的圆所标示的数据则是待分类的数据。 也就是说,现在,我们不知道中间那个绿色的数据是从属于哪一类(蓝色小正方形or红色小三角形),KNN就是解决这个问题的。

如果K=3,绿色圆点的最近的3个邻居是2个红色小三角形和1个蓝色小正方形,少数从属于多数,基于统计的方法,判定绿色的这个待分类点属于红色的三角形一类。 如果K=5,绿色圆点的最近的5个邻居是2个红色三角形和3个蓝色的正方形,还是少数从属于多数,基于统计的方法,判定绿色的这个待分类点属于蓝色的正方形一类。

于此我们看到,当无法判定当前待分类点是从属于已知分类中的哪一类时,我们可以依据统计学的理论看它所处的位置特征,衡量它周围邻居的权重,而把它归为(或分配)到权重更大的那一类。这就是K近邻算法的核心思想。 于是它的分类问题就变成了:如何衡量距离?如何选取k值?

距离度量

欧氏距离,最常见的两点之间或多点之间的距离表示法,又称之为欧几里得度量:

曼哈顿距离,我们可以定义曼哈顿距离的正式意义为L1-距离或城市区块距离。 例如在平面上,坐标(x1, y1)的点P1与坐标(x2, y2)的点P2的曼哈顿距离为:

,要注意的是,曼哈顿距离依赖坐标系统的转度而非系统在座标轴上的平移或映射。 两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的曼哈顿距离:

曼哈顿距离直接理解为城市地图距离就好了,在城市中不存在横跨的概念,只能够直线行驶

**切比雪夫距离,又称为棋盘距离,**若二个点p and q,其座标分别为pi及qi,则两者之间的切比雪夫距离定义如下:

在平面几何中,若二点p及q的直角坐标系坐标为(x1,y1)及(x2,y2),则切比雪夫距离为:
玩过国际象棋的朋友或许知道,国王走一步能够移动到相邻的8个方格中的任意一个。那么国王从格子(x1,y1)走到格子(x2,y2)最少需要多少步?你会发现最少步数总是max( | x2-x1 | , | y2-y1 | ) 步

注意,这里说的国际象棋是可以斜着走,比如下图:

**其他距离,**还没写完:链接

选取K值

如何选取合适的K值?

  • 如果选择较小的K值,就相当于用较小的领域中的训练实例进行预测,“学习”近似误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是“学习”的估计误差会增大,换句话说,K值的减小就意味着整体模型变得复杂,容易发生过拟合
  • 如果选择较大的K值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单。
  • K=N,则完全不足取,因为此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的累,模型过于简单,忽略了训练实例中大量有用信息

在实际应用中,K值一般取一个比较小的数值,例如采用交叉验证法(简单来说,就是一部分样本做训练集,一部分做测试集)来选择最优的K值

那么问题来了,我们上面说的是计算测试点和训练集中的每一个数据的距离,然后进行排序。数据量少的时候完全问题,可是当数据量大的时候,时间复杂度以及空间复杂度直线飙升

这时候,我们聪明的前辈就提出了kd树(k-dimensional tree)。 这是一颗什么样的树呢?** ** kd树可以帮助我们在很快地找到与测试点最邻近的K个训练点。不再需要计算测试点和训练集中的每一个数据的距离。 kd树是二叉树的一种,是对k维空间的一种分割,不断地用垂直于坐标轴的超平面将k维空间切分,形成k维超矩形区域,kd树的每一个结点对应于一个k维超矩形区域。

KD树

本质上还是二分查找

Kd-树是K-dimension tree的缩写,是对数据点在k维空间(如二维(x,y),三维(x,y,z),k维(x1,y,z..))中划分的一种数据结构,主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。本质上说,Kd-树就是一种平衡二叉树

首先必须搞清楚的是,k-d树是一种空间划分树,说白了,就是把整个空间划分为特定的几个部分,然后在特定空间的部分内进行相关搜索操作。想像一个三维(多维有点为难你的想象力了)空间,kd树按照一定的划分规则把这个三维空间划分了多个空间,如下图所示:

三维可能不太直观,以二维空间的样本为例:假设有6个二维数据点{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},数据点位于二维空间内,如下图所示。
为了能有效的找到最近邻,k-d树采用分而治之的思想,即将整个空间划分为几个小部分,首先,粗黑线将空间一分为二,然后在两个子空间中,细黑直线又将整个空间划分为四部分最后虚黑直线将这四部分进一步划分

构建kd树的具体步骤为:

  • 确定:split域=x。具体是:6个数据点在x,y维度上的数据方差分别为39,28.63,所以在x轴上方差更大,故split域值为x;
  • 确定:Node-data = (7,2)。具体是:根据x维上的值将数据排序,6个数据的中值(时间复杂度就在这,最快也是nlogn)为7,所以Node-data域位数据点(7,2)。这样,该节点的分割超平面就是通过(7,2)并垂直于:split=x轴的直线x=7;
  • 确定:左子空间和右子空间。具体是:分割超平面x=7将整个空间分为两部分:x<=7的部分为左子空间,包含3个节点={(2,3),(5,4),(4,7)};另一部分为右子空间,包含2个节点={(9,6),(8,1)};
  • 如上算法所述,kd树的构建是一个递归过程,我们对左子空间和右子空间内的数据重复根节点的过程就可以得到一级子节点(5,4)和(9,6),同时将空间和数据集进一步细分,如此往复直到空间中只包含一个数据点。

与此同时,经过对上面所示的空间划分之后,我们可以看出,点(7,2)可以为根结点,从根结点出发的两条红粗斜线指向的(5,4)和(9,6)则为根结点的左右子结点

注意!节点左右的子节点,并不一定是最近的节点!比如8-1明显比9-6离根节点7-2更近。因此才需要回溯计算距离

而(2,3),(4,7)则为(5,4)的左右孩子(通过两条细红斜线相连),最后,(8,1)为(9,6)的左孩子(通过细红斜线相连)。如此便形成了下面这样一棵k-d树

对于n个实例的k维数据来说,建立kd-tree的时间复杂度为O(knlogn)。(寻找中值)

**下面以查询一个点来演示其查找过程:**星号表示要查询的点查询点(2,4.5)。

通过二叉搜索,顺着搜索路径很快就能找到最邻近的近似点。而找到的叶子节点并不一定就是最邻近的,最邻近肯定距离查询点更近,应该位于以查询点为圆心且通过叶子节点的圆域内。为了找到真正的最近邻,还需要进行相关的‘回溯-比较’操作。也就是说,算法首先沿搜索路径反向查找是否有距离查询点更近的数据点

  • 二叉树搜索:先从(7,2)查找到(5,4)节点,在进行查找时是由y = 4为分割超平面的,由于查找点为y值为4.5,因此进入右子空间查找到(4,7),形成搜索路径<(7,2),(5,4),(4,7)>(4-7是经过x分割的),但(4,7)与目标查找点的距离为3.202,而(5,4)与查找点之间的距离为3.041,所以(5,4)为查询点的最近点;
  • 回溯查找:以(2,4.5)为圆心,以3.041为半径作圆,如下图所示。可见该圆和y = 4超平面交割,所以需要进入(5,4)左子空间进行查找,也就是将(2,3)节点加入搜索路径中得<(7,2),(2,3)>;于是接着搜索至(2,3)叶子节点,(2,3)距离(2,4.5)比(5,4)要近,所以最近邻点更新为(2,3),最近距离更新为1.5
  • 回溯查找至(5,4),直到最后回溯到根结点(7,2)的时候,以(2,4.5)为圆心1.5为半径作圆,并不和x = 7分割超平面交割,如下图所示。至此,搜索路径回溯完,返回最近邻点(2,3),最近距离1.5

回溯终止的条件即是,新画出的圆并不和上一级的分割超平面相交割

KD改进-BBF

加了一个优先队列,而不是原始的递归回溯

实例点是随机分布的,那么kd树搜索的平均计算复杂度是O(logN),这里的N是训练实例树。所以说,kd树更适用于训练实例数远大于空间维数时的k近邻搜索,当空间维数接近训练实例数时,它的效率会迅速下降,一降降到“解放前”:线性扫描的速度。

也正因为上述k最近邻搜索算法的第4个步骤中的所述:“回退到根结点时,搜索结束”,每个最近邻点的查询比较完成过程最终都要回退到根结点而结束,而导致了许多不必要回溯访问和比较到的结点,这些多余的损耗在高维度数据查找的时候,搜索效率将变得相当之地下,那有什么办法可以改进这个原始的kd树最近邻搜索算法呢?

从上述标准的kd树查询过程可以看出其搜索**过程中的“回溯”是由“查询路径”决定的,并没有考虑查询路径上一些数据点本身的一些性质。**一个简单的改进思路就是将“查询路径”上的结点进行排序,如按各自分割超平面(也称bin)与查询点的距离排序,也就是说,回溯检查总是从优先级最高(Best Bin)的树结点开始。

还是以上面的查询(2,4.5)为例,搜索的算法流程为

  • 将(7,2)压人优先队列中;提取优先队列中的(7,2),由于(2,4.5)位于(7,2)分割超平面的左侧,所以检索其左子结点(5,4)。
  • 同时,根据BBF机制”搜索左/右子树,就把对应这一层的兄弟结点即右/左结点存进队列”,将其(5,4)对应的兄弟结点即右子结点(9,6)压人优先队列中此时优先队列为{(9,6)},最佳点为(7,2);
  • 然后一直检索到叶子结点(4,7),此时优先队列为{(2,3),(9,6)},“最佳点”则为(5,4)
  • 提取优先级最高的结点(2,3),重复步骤2,直到优先队列为空。

简单实现-手写字体识别

代码链接

参考

https://github.com/NLP-LOVE/ML-NLP/tree/master/Machine%20Learning/9.%20KNN#12-%E8%BF%91%E9%82%BB%E7%9A%84%E8%B7%9D%E7%A6%BB%E5%BA%A6%E9%87%8F