本文主要来讲如何解决K邻近问题以及它的KD树优化方法。K邻近问题的描述如下
给定n个高维数据样本(n≤100000),每个样本有m维,现在给定一个样本,问离它最近的k条样本是什么?
本质上就是解决高维数据的K邻近问题。在机器学习中,通过计算K邻近实现分类算法,这就是著名的KNN算法。
![c2ad72fcd16a9664920a7b2018070551.png](https://i-blog.csdnimg.cn/blog_migrate/018855cb9c819557ff7bfe529859c1a8.jpeg)
KNN算法原理与实现
KNN算法是K Nearest Neighbor的简写,它的原理也比较简单,核心思想就是:假设样本空间分为几个类别,给定一个待分类样本,计算这个样本距离它最近的K个样本,看这K个样本哪个类别最多就把这个样本划分为这个类别。为了详细描述KNN算法,用下图来说明
![709668d96559ac32a0c8124f3ccb7fef.png](https://i-blog.csdnimg.cn/blog_migrate/3f57cd8f9170c4be5a156b9064ddb2d7.jpeg)
在上图中,假如要对中间的红色圆进行分类,那么根据K的不同取值,分类结果可能也不一样
- 假如K=5,从图中可以看出,离圆最近的5个样本中方框最多,那么圆属于方框这个类。
- 假如K=10,从图中可以看出,离圆最近的10个样本中三角形最多,有6个,那么圆属于三角形这个类。
- 假如K=18,从图中可以看出,离圆最近的18个样本中方框最多,那么圆属于方框这个类。
这就是KNN分类算法的核心思想,在KNN算法中K这个参数的选取比较关键,为了保证没有平局的发生,当类别个数为奇数时,K取一个偶数,当类别个数为偶数时,K取一个奇数。
按照上述KNN的原理介绍,通过C++来实现。
首先看如何计算距离,计算距离的方式有很多,这里用最简单的欧式距离度量法。代码如下
double getDistance(double *x, double *y, int nFeatures) { double dist = 0; for (int i = 0; i < nFeatures - 1; i++) dist += (x[i] - y[i]) * (x[i] - y[i]); return sqrt(dist);}
在预测时,需要分如下几步实现
- 寻找离当前样本最近的K个样本,保存下来。
- 统计这K个样本分别在每个类别中的出现次数。
- 计算这K个样本中最多的类别是哪个。
为了处理方便,将样本的label数据也放入特征里面,最后一个特征实际上是样本的label。代码实现如下
![dbf185bf869f40bd564add79b98239c6.png](https://i-blog.csdnimg.cn/blog_migrate/6c5be9af547c03867e243ec351155931.jpeg)
从代码实现中可以看出,每预测一个新的样本时,都需要遍历所有的样本集合,效率非常低下。实际上,通过KD树可以对其进行优化,下面来讲一些KD树的实现原理。
![45daae532f5749ea9590c45585ec7e4e.png](https://i-blog.csdnimg.cn/blog_migrate/de87ce839c529b28dffc525132e10565.jpeg)
KD树原理介绍
KD树是K Dimensional的缩写,它是一种高维索引树型数据结构,主要用于大规模高维空间的K邻近计算查找,例如对KNN算法的优化,图像检索中高维图像特征向量的K邻近匹配等。
KD树本质上是一种高维二叉搜索树,对于二叉搜索树来说,它具有如下三种性质
- 若它的左子树不为空,那么左子树上所有节点的值均小于它的根节点的值。
- 若它的右子树不为空。那么右子树上所有节点的值均大于它的根节点的值。
- 它的左右子树也分别是一棵二叉搜索树。
普通的二叉搜索树的值是一维的,而KD树是值为多维的二叉搜索树。在进行划分时候,也是将K维的数据与根节点进行比较,只不过在比较时候是选择其中一个维度进行比较的。那么主要关注两个问题
- 每次划分时候,应该选择哪个维度比较合适?
- 在按某个维度划分数据时,如何保证左右子树两边节点个数尽量相等?
对于问题1,我们可以按照特征一个,个轮流进行划分,但假如出现这样一种情况,当我们切一个长方形,这个长方形的长度远远大于宽度,要想把它切成尽量相同的小块,很显然先按长度来切更合理。所以在KD树中,通常每次切分都按照跨度最大的那个属性来切分,如果跨度越大,说明比较分散,对应方差就越大,即每次切分按照方差最大的属性切分就可以了。
对于问题2,为了保证左右子树节点个数尽量相等,可以将每次划分选取的属性值对应的中位数作为左右子树划分的标准。
那么根据上面的基本描述,就能够建立一棵KD树了。
在查询时候,给定一个样本,来计算距离它最近的K个样本,通过一个栈来维护最近的K个样本。算法描述如下
- 从根节点开始,将要查询的样本与树的各个节点进行比较向下遍历,当达到叶子节点时,计算要查询的样本与这个叶子节点的所有样本的距离,记录最小距离对应的数据点,并将这个查找路径中的划分元素都存入栈中。
- 向上回溯,查找到父节点,若父节点与待检索样本之间的距离小于当前的最短距离,则替换当前的最短距离。
- 以待检索的样本为圆心,以步骤2找到的最短距离为半径画圆,若圆与父节点所在的平面相割,则需要将父节点的另一棵子树进栈,重新执行以上的出栈操作。
- 直到栈为空为止。
以上就是KD树的建树和查询操作原理。当维数比较大的时候,建树后的分支自然会增多,进而回溯的次数增加,算法效率会随之降低。在图像检索中,特征往往是高维的,很有必要对K-D树算法进行改进,BBF算法就是对KD树的改进。
另外推荐一个比较好的C++实现的KD树算法,https://github.com/sdeming/kdtree。
本文解决的是多维数据的K邻近问题,或者认为是多维数据的TOP K问题。而针对一维数据的TOP K问题,有BFPRT算法,它的最坏时间复杂度为O(n),这个算法后面有空了我再详细介绍。