kd tree最近邻搜索算法深度解析

李航统计学习方法(第二版)(六):k近邻算法实现(kd tree方法)中,对kd树进行了介绍,包括,kd树的简介、kd树的建立以及kd树的搜索。在看到李航老师书中对kd树搜索算法的描述后,其中对于递归回退的操作描述不是很理解,觉得太抽象了(很可能是我自己的理解能力的问题)。对比多个源码(各个源码都还有我觉得有问题的地方,有时间我会在各博主博客下面留言,主要有:1.c++实现kdTree创建以及最近邻点查找,2. KDTree的C++实现),最后靠蛮力弄明白kdtree最近邻搜索的算法,特此记录一下。

文章目录

一、kd树最近邻搜索算法

李航老师《统计学习方法(第二版)》中的kdtree最近邻搜索算法引出如下:
在这里插入图片描述
在这里插入图片描述

其中,算法最核心的部分是(3.a)与(3.b)这两句。我比较难以想象的是(3.b)中:

如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;

如何移动到另一个子结点?然后又如何递归地进行最近邻搜索?

网上找了很多博客,大部分都是学舌似的话,根本没有提供比书中这段话更多的信息。很多博客中给的例子,点到为止,一个简单的例子根本不会触发什么[移动到另一个子结点。接着,递归地进行最近邻搜索]好么!

于是寄希望于看别人实现的源码来弄清楚整个算法,因为源码必定是细节的。CSDN博主nnzzll的实现就很简洁清晰,github|nnzzll/NaiveKDTree。我借着他的代码一步一步在脑中虚拟的运行着一棵kdtree,终于知道[移动到另一个子结点。接着,递归地进行最近邻搜索]的意思了。虽然,nnzzll的代码也存在问题,我在他那篇博客下也留言了,希望之后与之有更多的交流。

不废话,下面开始用图与例子对这个过程进行说明。

二、一个二维kdtree的例子

假定我们有一个二维数据集 [ ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) ] [(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)] [(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)], 我们按照如下所示的kdtree构建算法,可以得到一棵kdtree。(注:如何构建kdtree不是本文的重点,所以不多赘述
在这里插入图片描述
由二维数据集 [ ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) ] [(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)] [(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)]构建的kdtree如下图:
在这里插入图片描述

该kdtree表示成空间形式如下图所示:
在这里插入图片描述

三、kdtree最近邻搜索步骤图示

假定给定目标数据点 ( 6 , 6 ) (6, 6) (6,6),我们基于上面构建的kdtree树,来说明如何搜索得到离 ( 6 , 6 ) (6, 6) (6,6)最近的数据点。
在这里插入图片描述

step 1:李航书中最近邻搜索算法第一步:

在这里插入图片描述
按以上步骤,从kdtree根节点 ( 7 , 2 ) (7,2) (7,2)开始向下搜索,路径为 ( 7 , 2 ) , ( 5 , 4 ) , ( 4 , 7 ) (7,2),(5,4),(4,7) (7,2),(5,4),(4,7),其中, ( 4 , 7 ) (4,7) (4,7)为叶子节点。我们将该步骤中经过的节点压入待比较节点队列(该队列为先进后出队列)。下图左边显示的是经过step1后的待比较节点队列,根节点 ( 7 , 2 ) (7,2) (7,2)在栈底部,叶子节点 ( 4 , 7 ) (4,7) (4,7)在栈顶部;下图右边显示的经过step1经过的节点(红色显示)。
在这里插入图片描述

step 2:初始化当前最近距离与最近节点

李航书中最近邻搜索算法第二步:以此叶结点为“当前最近点”
在这里插入图片描述
以step 1中的,也即待比较节点队列的栈顶节点 ( 4 , 7 ) (4,7) (4,7)来初始化与目标点 ( 6 , 6 ) (6,6) (6,6)当前最近节点当前最近距离

通过计算 ( 6 , 6 ) (6, 6) (6,6) ( 4 , 7 ) (4,7) (4,7)的距离,可得当前最近距离 d m i n = 2.23 d_{min}=2.23 dmin=2.23当前最近节点 ( 4 , 7 ) (4,7) (4,7)

在这里插入图片描述

step 3:持续地从待比较节点队列中压出栈顶节点,执行李航老师书上的第三步中的(a)与(b):

在这里插入图片描述

3.1: 取出栈顶节点为 ( 4 , 7 ) (4,7) (4,7),并执行下列操作:

在这里插入图片描述

--------------------kd树搜索算法(3.a)--------------------------

执行算法(3.a):如果该节点保存的实例点比当前最近点距离目标点更近,则以该实例点为当前最近节点

计算目标点 ( 6 , 6 ) (6,6) (6,6) ( 4 , 7 ) (4,7) (4,7)的欧式距离为2.23,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

执行算法(3.b):检测该节点的兄弟节点(更一般的说法是兄弟子kdtree)(该节点父节点的另一个子树)有没有可能包含最近节点。具体地,检查兄弟结点对应的区域是否以目标点为球心、以目标点与当前最近节点间的距离为半径的超球体相交。如果相交,可能在另一个子节点对应的区域内存在距目标点更近的点,移动到另一个子节点。接着,递归地进行最近邻搜索

我们得出如下图的圆与父节点 ( 5 , 4 ) (5,4) (5,4)用来划分左子树与右子树的维度为 y y y,也即 y = 4 y=4 y=4有相交,说明节点 ( 5 , 4 ) (5,4) (5,4)的另一子树区域(左子树)可能存在离目标点 ( 6 , 6 ) (6, 6) (6,6)更近的节点。当前访问节点为 ( 4 , 7 ) (4,7) (4,7),在其父节点的右边,所以递归地对其父节点 ( 5 , 4 ) (5,4) (5,4)的左子树区域进行最近邻搜索。
在这里插入图片描述

移动到另一个子节点。接着,递归地进行最近邻搜索

经过kd树搜索算法(3.b),我们知道节点 ( 5 , 4 ) (5,4) (5,4)左子树可能存在最近节点,因此要对该子树进行最近邻搜索。在程序实现上有两种方式:

1)将整个最近邻搜索写成可递归的函数findNearestPoint(kdtree, pt);
2) 用堆栈队列实现。

用第一种方式,我们只需要在findNearestPoint(kdtree, pt)中从叶子节点循着父节点往上,一直到根节点进行访问,中途若出现某个访问节点的兄弟子树可能存在最近节点(根据kd树搜索算法(3.b)可判断),则用兄弟子kdtree(假定为brother_kdtree)调用findNearestPoint(brother_kdtree, pt),构成一个嵌套调用。代码比较美观。

用第二种方式的优点是比较直观。用一个先入后出的队列来存储待比较节点队列,在原理上是和第一种嵌套调用方式是一样的。本文为了更直观,所以采用第二种方式。

按照step 1中的,最近邻搜索算法中如下所示的第一步,对子树进行遍历,至到叶子节点。
在这里插入图片描述
由于,节点 ( 4 , 7 ) (4, 7) (4,7)的兄弟子树只有一个节点 ( 2 , 3 ) (2,3) (2,3),它的根节点也是叶子节点。因此,访问节点路径中只有一个节点 ( 2 , 3 ) (2,3) (2,3),将其加入待比较节点队列

在这里插入图片描述

3.2: 取出栈顶节点为 ( 2 , 3 ) (2,3) (2,3),并执行下列操作:

在这里插入图片描述

--------------------kd树搜索算法(3.a)--------------------------

计算目标点 ( 6 , 6 ) (6,6) (6,6) ( 2 , 3 ) (2,3) (2,3)的欧式距离为5.0,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

由于节点 ( 2 , 3 ) (2,3) (2,3)的兄弟节点 ( 4 , 7 ) (4,7) (4,7)(或者说是以 ( 4 , 7 ) (4,7) (4,7)为根结点的子kd树)已经被访问,所以跳过该步骤。

3.3: 取出栈顶节点为 ( 5 , 4 ) (5,4) (5,4),并执行下列操作:

在这里插入图片描述

--------------------kd树搜索算法(3.a)--------------------------

计算目标点 ( 6 , 6 ) (6,6) (6,6) ( 5 , 4 ) (5,4) (5,4)的欧式距离为2.23,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点 ( 5 , 4 ) (5,4) (5,4)的父节点 ( 7 , 2 ) (7,2) (7,2)的右子树(以节点 ( 9 , 6 ) (9,6) (9,6)为根节点)是其兄弟子树,如下图可知,以 ( 6 , 6 ) (6,6) (6,6)为圆心,以2.23为半径的圆与 x = 7 x=7 x=7 ( 7 , 2 ) (7,2) (7,2)的划分维度为 x x x)相交,可知节点 ( 5 , 4 ) (5,4) (5,4)的兄弟子树(以节点 ( 9 , 6 ) (9,6) (9,6)为根节点)可能存在最近节点。

在这里插入图片描述

移动到另一个子节点。接着,递归地进行最近邻搜索

节点 ( 5 , 4 ) (5, 4) (5,4)的兄弟子树是以节点 ( 9 , 6 ) (9, 6) (9,6)为根节点,并且根节点只有一个左叶子节点 ( 8 , 1 ) (8,1) (8,1)

以目标点 ( 6 , 6 ) (6,6) (6,6)为输入,对该子树进行正向搜索,找到叶子节点。由于目标点的 y = 6 y=6 y=6不小于用于划分的分界线 y = 6 y=6 y=6,按理应该归到节点 ( 9 , 6 ) (9,6) (9,6)的右叶子节点,可是 ( 9 , 6 ) (9, 6) (9,6)不存在右叶子节点。为了在代码上实现统一,遇到这种只有一个子节点的情况,不用按分界线来划分左和右,直接往下走到它的这个唯一子节点即可。
在这里插入图片描述

3.4: 取出栈顶节点为 ( 8 , 1 ) (8,1) (8,1),并执行下列操作:

在这里插入图片描述

--------------------kd树搜索算法(3.a)--------------------------

计算目标点 ( 6 , 6 ) (6,6) (6,6) ( 8 , 1 ) (8,1) (8,1)的欧式距离为5.3,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点 ( 8 , 1 ) (8,1) (8,1)无兄弟子树,故此步骤跳过。

3.5: 取出栈顶节点为 ( 9 , 6 ) (9,6) (9,6),并执行下列操作:

在这里插入图片描述

--------------------kd树搜索算法(3.a)--------------------------

计算目标点 ( 6 , 6 ) (6,6) (6,6) ( 9 , 6 ) (9,6) (9,6)的欧式距离为3.0,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点 ( 9 , 6 ) (9,6) (9,6)的兄弟子树已被访问,跳过此步骤。

3.5: 取出栈顶节点为 ( 7 , 2 ) (7,2) (7,2),并执行下列操作:

在这里插入图片描述

--------------------kd树搜索算法(3.a)--------------------------

计算目标点 ( 6 , 6 ) (6,6) (6,6) ( 7 , 2 ) (7,2) (7,2)的欧式距离为4.12,不比当前最近距离2.23要小,所以不会导致当前最近节点当前最近距离的更新。

--------------------kd树搜索算法(3.b)--------------------------

节点 ( 7 , 2 ) (7,2) (7,2)无兄弟子树,跳过此步骤。

3.6: 取出栈顶节点为NULL,也即待比较节点队列为空,结束搜索。

备注:结束搜索条件有两种:1)回退到根结点;2)栈顶节点为空。

第一种结束搜索条件是李航老师《统计学习方法(第二版)》书中的,这个结束条件需要在递归回退之前,也即李书中步骤(2)与步骤(3)之间,加一个判断根节点是否为当前最近节点的步骤,不然会漏掉可能是最近节点的根节点。此方式则将本文中的step 3.5与step 3.6合并为step 3.5:取出栈顶节点为根节点,结束搜索。

第二种则不用,更加直观,这也是本文采用的方式。

结尾

本文以二维数据集[ ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) ] 构建的kdtree为例,用(6,6)作为目标点,事实上证明 ( 6 , 6 ) (6,6) (6,6)作为目标点使这个例子非常复杂,最终把所有的节点都访问了一遍。本文也正是想采用这样一个复杂的例子,把这个kdtree最近邻搜索算法说透。

如果你愿意的话,用同样的例子去走一遍c++实现kdTree创建以及最近邻点查找KDTree的C++实现中给出的代码。你会发现,博主们手撸的代码能覆盖我们在网上看到的那些简单情况,但是对于本文中给出的情况却cover不了。这也说明,他们代码并没有完全复现算法的思想。

如果这篇博客有幸被两位博主看到,我先借这个机会向你们道声感谢。你们的博客的确给了我很多思考的方式,包括采用堆栈的方式来实现最近邻搜索。如对文中观点持不同意见,请狠狠踩我吧!欢迎一起交流,然后一起成长。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

windSeS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值