数据应用当中,最近邻查询是非常重要的功能。不论是信息检索,推荐系统,还是数据库查询,最近邻查询(Nearst Neighbor Search)可谓无处不在。它要实现的是帮助我们找到数据中和查询最接近的一个或多个数据条目(前者叫NN search, 后者也叫kNN),其实本质上是一样的,我在这篇博客中讲的Kd-tree主要就是针对这种最近邻搜索问题。

1. 基本原理

其实,这种问题本来是很容易解决的,只要设计好了数据相似度的度量方法(有关相似度量的方法详细可参考我之前的博客:数据相似性的度量方法总结)计算所有数据与查询的距离,比较大小即可。但是随着数据量的增大以及数据维度的提高,这种方法就很难在现实中应用了,因为效率会非常低。解决此类问题的思路基本分为两类:
(1)通过构建索引,快速排除与查询相关度不大的数据;
(2)通过降维的方法,对数据条目先降维,再查询;
前者主要是为了解决数据量过大的问题,比较常见的有我们熟知的二叉搜索树,Merkel tree,B-tree,quad-tree等;后者主要是为了解决维度过大的问题,比较常见的方法有我在上一篇博客中讲的LSH:LSH(Locality Sensitive Hashing)原理与实现

而我们今天要说的Kd-tree就是一种对多维欧式空间分割,从而构建的索引,属于上面的第一类。

Kd-tree全称叫做:k dimension tree,这是一种对于多维欧式空间分割构造的的二叉树,其性质非常类似于二叉搜索树。我们先回顾一下二叉搜索树,它是一种具有如下特征的二叉树:
(1)若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左、右子树也分别为二叉搜索树;
这个概念是数据结构基础的东西,应该非常熟悉了,不再赘述,下面给出一棵普通的二叉搜索树的图:

如果我们把二叉搜索树所对应的数据集看做一个一维空间(因为这个数据集的每一个数据条目都是由一个单一的数值构成的),那么实际上二叉搜索树的分割依据就是数值的大小,这样的划分,帮助我们以平均O(lg(n))的时间复杂度搜索数据。

自然而然,我们会祥这样一个问题,能不能在多维欧式空间中,构建一棵类似原理的二叉搜索树?这也就是我们今天说的Kd-tree.

2. kd-tree的构建

先抛开搜索算法怎样设计这件事不管,我们单纯地关心怎样对多维欧式空间划分。一维空间简单,因为每个数据条目只有一个数值,我们直接比较数值大小,就能对这些数据条目划分,可是在多维空间就存在一个关键问题:每个数据条目由多个数值组成,我们怎么比较?

Kd-tree的原理是这样的:我们不比较全部的k维数据,而是选择其中某一个维度比较,根据这个维度进行空间划分。那接下来,我们需要做的是两件事:

  • 判断出在哪一个维度比较,也就是说,我们所要切割的面在哪一个维度上。当然这种切割需要遵循一个基本要求,那就是尽量通过这个维度的切割,使得数据集均分(为二);
  • 判断以哪个数据条目分依据划分。上面我们说,要使得数据集均分为二,那当然要选择一个合适的数据项,充当这个划分的“点”。

总结一下,就是要选择一个数据项,以这个数据项的某个维度的值为标准,同一维度的值大于这个值的数据项,划分为一部分,小于的划分为另一部分。根据这种划分来构建二叉树,就如同二叉搜索树那样。

现在,针对上面的两件事,我们需要做如下两个工作:
1. 确定划分维度:这里维度的确定需要注意的是尽量要使得这个维度上所有数据项数值的分布尽可能地有大方差,也就是说,数据在这个维度上尽可能分散。这就好比是我们切东西,如果你切的是一根黄瓜,当让横着切要比竖着切更容易。所以我们应该先对所有维度的数值计算方差,选择方差最大的那个维度;
2. 选择充当切割标准的数据项:那么只需要求得这个维度上所有数值的中位数即可;

至此,可以设计出kd-tree的构建算法了:

  • 对于一个由n维数据构成的数据集,我们首先寻找方差最大的那个维度,设这个维度是 d d,然后找出在维度dd上所有数据项的中位数 m m,按mm划分数据集,一分为二,记这两个数据子集为 Dl,Dr D_l, D_r。建立树节点,存储这次划分的情况(记录划分的维度 d d以及中位数mm);
  • 对 Dl,Dr D_l, D_r重复进行以上的划分,并且将新生成的树节点设置为上一次划分的左右孩子;
  • 递归地进行以上两步,直到不能再划分为止(所谓不能划分是说当前节点中包含的数据项的数量小于了我们事先规定的阈值,不失一般性,我在此篇博客中默认这个阈值是2,也就是说所有叶子节点包含的数据项不会多于2条),不能再划分时,将对应的数据保存至最后的节点中,这些最后的节点也就是叶子节点。

现在可以给出kd-tree的实现代码。当然,首先需要设计几个函数,供算法调用,限于篇幅,这里只是给出功能说明:

类或函数 作用
class-KdTreeNode kd-tree节点,包含以下6个Attributes
Attribute1-data 树节点属性,代表这个节点的数据项,其实是一个列表,如果不是叶子节点,则为空
Attribute2-split 树节点属性,代表构建树时,对这个节点进行分割所依据的数据维度
Attribute3-median 树节点属性,代表构建树时,所有上面split维度上数据的中位数
Attribute4-left 树节点属性,代表左孩子
Attribute5-right 树节点属性,代表右孩子
Attribute6-parent 树节点属性,代表父亲节点,作用是在后面的搜索算法中用
Attribute7-visited 树节点属性,代表此节点是否被算法回溯遍历,作用是在后面的搜索算法中用
func-getSplit 函数,得到所有维度中方差最大那个维度的序号
func-getMedian 函数,得到要分割的维度的中位数

按照上面这样设计,就可以实现kd-tree的构建了。我们这里使用numpy库,假设现在已经将所有的数据项读入为一个ndarray型的数据矩阵datamatrixdatamatrix的每一行代表了一个数据项。那么构建树算法的实现代码可以如下所示:

import numpy as np# 树节点类和其相关方法如下
class KdTreeNode(object):def __init__(self, dataMatrix):self.data = dataMatrixself.left, self.right = None, Noneself.parent = Noneself.split = self.getSplit()self.median = self.getMedian()self.visited = Falsedef getSplit(self):# 取方差最大的维度作为分割维度,代码略def getMedian(self):# 得到这个分割维度上所有数值的中位数,代码略# 构建kd-tree的函数,helper为其辅助函数,起到递归的作用
def buildKdTree(dataMatrix):root = KdTreeNode(dataMatrix)# there is only one data item in dataMatrixif root.data.shape[0] <= 1:return roothelper(root)return rootdef helper(root):if root is None or len(root.data) <= 2:return# distribute data into left and rightleftData, rightData = [], []# generate left and right childfor row in list(root.data):if row[root.split] <= root.median:leftData.append(row)else:rightData.append(row)left = KdTreeNode(np.array(leftData))left.parent = rootright = KdTreeNode(np.array(rightData))right.parent = rootroot.data = Noneroot.left = leftroot.right = righthelper(root.left)helper(root.right)

我在这里,借用博客Kd-Tree算法原理和开源实现代码中的测试样例:数据集合(2,3), (5,4), (9,6), (4,7), (8,1), (7,2),按照以上算法原理设计的kd-tree以及划分情况如以下两张图所示:我在这里直接借用了上面这个链接中博客的图,这位博主的文章思路写的非常清晰。


图中,非叶节点的二元组中,第一个元素表示分割维度(split值),第二个维度表示,取得的中位数(median值)

3. 搜索算法

构建好kd-tree后,就可以执行搜索算法了。其实,这也是信息检索最常见的模式,先构建索引,然后依照索引执行搜索算法。当然几乎所有的搜索算法都与其索引是配套的,也就是说,即便是同样的数据,索引不同,其搜索算法就不同,而各有各的技巧。这也是信息检索技术最大的魅力之一。

闲话少说,看搜索算法。基本思路可分为如下3步:

  1. 依照非叶节点中存储的分割维度以及中位数信息,自根节点始,从上向下搜索,直到到达叶子。遍历的原则当然是比较分割维度上,查询值与中位数的大小,设查询为Q,当前遍历到的节点为u,则若Q[u.split] > u.median,继续遍历u的右子树,反之,遍历左子树。
  2. 遍历到叶子之后,计算叶子节点中与查询Q距离最小的数据项与查询的距离,记为minDis;其后执行“回溯”操作,回溯至当前节点的父节点,判断以Q为球心,以minDis为半径的超球面是否与这个父节点的另一个分支所代表的区域有交集(其实,这里的区域就是一个超矩形,它包含了所有这个节点代表的数据项)。如果没有,继续向上一层回溯;如果有,则按照1步继续执行,探底到叶子节点后,如果此时Q与这个叶子节点中的数据项有更小的距离,则更新minDis
  3. 持续进行以上两步,直到回溯至根节点,且根节点的两个分支都被“探测”过为止。

但是这个里面有一个难点:如何判断以查询Q为球心,以当前的minDis为半径的超球面与树中,一个非叶节点所代表的超矩形是否相交?
一种简单的方法是在构建树的时候直接给每个节点赋值一个超矩形,这个超矩形以一个树节点属性的形式存在。一般情况下是给出超矩形的一个最大点和一个最小点。判断的方法只需要看如下的两个条件是否都成立即可:

  • Q[u.split] + minDis >= minPoint[u.split]
  • Q[u.split] - minDis >= maxPoint[u.split]

其中,u为查询当前遍历到的节点的父节点,minPoint与maxPoint为u所代表的超矩形的最大点和最小点(所谓最大最小点,那二维空间的矩形来说,就是他的右上角的点和左下角的点,分别拥有这个矩形范围内各个维度上的最大值和最小值)

原因很简单,因为以Q为球心,以当前这个矩形区域的一个点为球面上一点的一个超球面,一定是经过了当前这个叶子所代表的区域,但是同时它不可能完全覆盖他的兄弟节点代表的区域。这个道理听上去有点乱,看下面这个图就能明白:

图中,Q1,Q2,Q3是三个查询点,线段AB是这个矩形空间的分割情况。可见,上面的结论书成立的,同时,我们还可以得到一个观点:只要|Q[u.split] - u.median|<= minDis那么就是与其兄弟节点所代表的区域相交。其实这个道理也可以通过数学上的推导得到,如果不能理解的话一试便知。

说道这里,可以给出搜索算法的实现代码了:

import math# 计算两个多维向量的欧式距离
def dis(item, query):代码略# 回溯,找寻需要处理的下一个节点,下一节点应满足不曾被算法回溯遍历
def findNextNode(cur):代码略# 判断以查询为球心,以此时的最小距离minDis为半径的超球面是否与节点所代表的超矩形相交
def intersect(node, query, radius):代码略# 找到节点的兄弟节点
def getBrother(node):代码略def search(root, query, result, minDis):cur = root# the root is Noneif not cur:return result# find leafelif not cur.visited:while cur.left and cur.right:if query[cur.split] >= cur.median:cur = cur.rightelse:cur = cur.left# update the min dis if it is necessaryfor item in list(cur.data):tempDis = dis(item, query)if abs(tempDis - minDis) < 1e-9:result.append(list(item))elif tempDis < minDis:minDis = tempDisresult = [list(item)]# update the visitedcur.visited = True# process the next nodecur = findNextNode(cur)if intersect(cur, query, minDis):return search(cur, query, result, minDis)else:cur.visited = TruenextNode = findNextNode(cur)return search(nextNode, query, result, minDis)else:return result

依照算法的设计,我们以上面的kd-tree的图为例,可以看看搜索算法遍历的顺序:

  1. 查询点(8, 3)自根节点起,按照分割维度以及中位数向下遍历,找到叶子节点(9, 6),此时算得的最小距离为 10‾‾‾√ \sqrt{10};
  2. 回溯,找到下一个需要处理的节点,也就是(8,1), (7,2)这个点(此时以(8,3)为圆心,以 10‾‾‾√ \sqrt{10}为半径的圆与这个点所代表区域相交),数据项 (7,2)与查询(8, 3)的距离更近,为 2‾√ \sqrt{2},更新最小距离为 2‾√ \sqrt{2};
  3. 回溯,此时,非叶节点<2, 2>这个点所在的分支已经被访问过了,找到下一个需要处理的节点,<2, 4>这个点。不过计算距离发现,这个点所代表的区域并不与此时的圆相交,放弃对这一分支的搜索;
  4. 回溯至根节点,并且此时根节点的两个分支都被考虑了,搜索结束,返回最近邻(7, 2),最短距离是 2‾√ \sqrt{2}

以上就是全部kd-tree的原理以及对应搜索算法的实现。内容我大多参考了博客:Kd-Tree算法原理和开源实现代码。
限于篇幅,本篇博客并未给出全部的详细代码,若要参考,请查看我的github主页:KD-tree。

不足之处,还望指正。

Kd-tree原理与实现相关推荐

  1. KD Tree原理讲解

    1.引子 在一张地图上,有600多个单位,每个单位之间都需要独立寻路,检测碰撞和寻找最近的敌方目标.当这一切需要在手机上流畅运行并尽可能快的在服务器进行模拟时,最简单的平方算法O(N^2)已经不能满足 ...

  2. Kd tree原理详解

    目录 一.kd-tree简介 二.kd-tree的实现过程 1. kd-tree的创建 2. 最近邻搜索 三.kd-tree代码实现 一.kd-tree简介  kd-tree(全称为k-dimensi ...

  3. KD Tree的原理及Python实现

    1. 原理篇 我们用大白话讲讲KD-Tree是怎么一回事. 1.1 线性查找 假设数组A为[0, 6, 3, 8, 7, 4, 11],有一个元素x,我们要找到数组A中距离x最近的元素,应该如何实现呢 ...

  4. k-d tree算法原理及实现

    k-d tree即k-dimensional tree,常用来作空间划分及近邻搜索,是二叉空间划分树的一个特例.通常,对于维度为k,数据点数为N的数据集,k-d tree适用于N≫2k的情形. 1)k ...

  5. PCL:k-d tree 1 讲解

    1.简介 kd-tree简称k维树,是一种空间划分的数据结构.常被用于高维空间中的搜索,比如范围搜索和最近邻搜索.kd-tree是二进制空间划分树的一种特殊情况.(在激光雷达SLAM中,一般使用的是三 ...

  6. 统计学习方法笔记(二)-kd树原理及python实现

    kd树 kd树简介 构造平衡kd树算法原理 kd树代码实现 案例地址 kd树简介 kdkdkd树是一种对kkk维空间中的实例点进行存储以便对其进行快速检索的树形数据结构. kdkdkd树构造方法: 构 ...

  7. k-d tree入门

    暑期集训开始了 本篇博客将会简单讲一下k-d tree的原理以及实现 首先大家要先了解一下二叉搜索树 二叉搜索树是一个很简单常见的数据结构,他符合以下两个特征 1.一个节点若有左子树,则左子树上的点全 ...

  8. K-D Tree 算法详解及Python实现

    K-D Tree 算法 k−d treek−dtree\mathrm{k-d\ tree}即k−dimensional treek−dimensionaltree\mathrm{k-dimension ...

  9. k-d tree 介绍

    作为存取高维数据的一种数据结构,k-d tree 在静态查询和插入方面的效率还是很高的.本文在这里对 k-d tree 的内容作一些介绍,可能也会结合自己使用 k-d tree 的一些体验作一些点评. ...

  10. PCL点云库调库学习系列——k-d tree与八叉树

    k-d tree与八叉树 1 k-d tree与八叉树 本文并不涉及具体原理的解释,文章着重在k-d树与八叉树在近邻搜索方面的API的使用 1.1 k-d tree k-d tree算法及原理: ht ...

最新文章

  1. orm连接mysql_Django项目如何使用ORM连接MySQL
  2. 英伟达开源行人生成/重识别代码
  3. java token生成和验证_SpringBoot集成JWT生成token及校验方法过程解析
  4. 8个适合用来练手的SpringBoot开源项目
  5. 怎样让你的安卓手机瞬间变Firefox os 畅玩firefox os 应用
  6. Android之jni解决JNIEnv跨线程问题
  7. mvvm 自动绑定_ZK的实际应用:MVVM –表单绑定
  8. 指定查询条件,查询对应的集合List(单表)
  9. 笨方法“学习python笔记之字符串
  10. 1.4 为什么深度学习会兴起?(Why is Deep Learning taking off?)
  11. DrawPrimitiveUP And DrawIndexedPrimitiveUP
  12. jtree和mysql_让JTree所有节点展开和关闭的类 - 我爱看火影的日志 - 网易博客
  13. 大数据方面核心技术有哪些?
  14. android button设置边框背景颜色
  15. 在.NET中进行AutoCAD二次开发(C#+ObjectArx) (二)
  16. linux open换行windows,python中遇到的Windows系统中换行符的一个坑
  17. vue webapp之music(六)利用axios与后端接口代理请求歌单推荐数据
  18. 计算机核心基础、计算机硬件组成原理、计算机硬件组成详解、硬盘接口及操作系统、计算机网络简述、OSI七层协议、子网划分、 VLAN简述
  19. 中国手机市场惊蛰降临:从Counterpoint数据看2020年的5G未来
  20. Windows 8安装软件时遇到运行时错误‘339’ MSCOMCTL.ocx或其附件之一未注册

热门文章

  1. DHCP租约过程,如何使用DHCP
  2. 我的平安健康生活---师旭平
  3. 做游戏与web的区别 - 服务器篇【1】
  4. 论文阅读-WARP: Word-level Adversarial ReProgramming
  5. 【数据结构】栈的实现(C语言)
  6. 【论文阅读】用于自动中文作文评分的多阶段预训练
  7. Qt入门-QLabel类
  8. Python将py文件转换为pyd文件
  9. 操作电脑时间长,眼睛疲劳怎么办?
  10. Gif表情包如何用视频制作?教你一键快速制作gif表情包