Kd-Tree算法原理和开源实现代码
本文介绍一种用于高维空间中的高速近期邻和近似近期邻查找技术——Kd-Tree(Kd树)。
Kd-Tree,即K-dimensional
tree,是一种高维索引树形数据结构,经常使用于在大规模的高维数据空间进行近期邻查找(Nearest Neighbor)和近似近期邻查找(Approximate Nearest Neighbor),比如图像检索和识别中的高维图像特征向量的K近邻查找与匹配。本文首先介绍Kd-Tree的基本原理,然后对基于BBF的近似查找方法进行介绍。最后给出一些參考文献和开源实现代码。
一、Kd-tree
Kd-Tree。即K-dimensional
tree,是一棵二叉树,树中存储的是一些K维数据。在一个K维数据集合上构建一棵Kd-Tree代表了对该K维数据集合构成的K维空间的一个划分。即树中的每一个结点就相应了一个K维的超矩形区域(Hyperrectangle)。
在介绍Kd-tree的相关算法前。我们先回想一下二叉查找树(Binary
Search Tree)的相关概念和算法。
二叉查找树(Binary Search
Tree,BST)。是具有例如以下性质的二叉树(来自wiki):
1)若它的左子树不为空。则左子树上全部结点的值均小于它的根结点的值;
2)若它的右子树不为空,则右子树上全部结点的值均大于它的根结点的值;
3)它的左、右子树也分别为二叉排序树;
比如,图1中是一棵二叉查找树,其满足BST的性质。
图1 二叉查找树(来源:Wiki)
给定一个1维数据集合。如何构建一棵BST树呢?依据BST的性质就能够创建,即将数据点一个一个插入到BST树中,插入后的树仍然是BST树。即根结点的左子树中全部结点的值均小于根结点的值,而根结点的右子树中全部结点的值均大于根结点的值。
将一个1维数据集用一棵BST树存储后,当我们想要查询某个数据是否位于该数据集合中时。仅仅须要将查询数据与结点值进行比較然后选择相应的子树继续往下查找就可以,查找的平均时间复杂度为:O(logN),最坏的情况下是O(N)。
假设我们要处理的对象集合是一个K维空间中的数据集,那么是否也能够构建一棵类似于1维空间中的二叉查找树呢?答案是肯定的。仅仅只是推广到K维空间后,创建二叉树和查询二叉树的算法会有一些对应的变化(后面会介绍到两者的差别),这就是以下我们要介绍的Kd-tree算法。
如何构造一棵Kd-tree?
对于Kd-tree这样一棵二叉树,我们首先须要确定如何划分左子树和右子树。即一个K维数据是根据什么被划分到左子树或右子树的。
在构造1维BST树时,一个1维数据依据其与树的根结点和中间结点进行大小比較的结果来决定是划分到左子树还是右子树。同理。我们也能够依照这种方式,将一个K维数据与Kd-tree的根结点和中间结点进行比較。仅仅只是不是对K维数据进行总体的比較,而是选择某一个维度Di。然后比較两个K维数在该维度Di上的大小关系。即每次选择一个维度Di来对K维数据进行划分,相当于用一个垂直于该维度Di的超平面将K维数据空间一分为二。平面一边的全部K维数据在Di维度上的值小于平面还有一边的全部K维数据相应维度上的值。也就是说。我们每选择一个维度进行如上的划分,就会将K维数据空间划分为两个部分。假设我们继续分别对这两个子K维空间进行如上的划分。又会得到新的子空间,对新的子空间又继续划分,反复以上过程直到每一个子空间都不能再划分为止。以上就是构造Kd-Tree的过程,上述过程中涉及到两个重要的问题:1)每次对子空间的划分时。如何确定在哪个维度上进行划分;2)在某个维度上进行划分时,如何确保在这一维度上的划分得到的两个子集合的数量尽量相等。即左子树和右子树中的结点个数尽量相等。
问题1: 每次对子空间的划分时,如何确定在哪个维度上进行划分?
最简单的方法就是轮着来,即假设这次选择了在第i维上进行数据划分,那下一次就在第j(j≠i)维上进行划分,比如:j
= (i mod k) + 1。
想象一下我们切豆腐时,先是竖着切一刀,切成两半后。再横着来一刀,就得到了非常小的方块豆腐。
但是“轮着来”的方法能否够非常好地解决这个问题呢?再次想象一下,我们如今要切的是一根木条,依照“轮着来”的方法先是竖着切一刀,木条一分为二。干净利落,接下来就是再横着切一刀。这个时候就有点考验刀法了,假设木条的直径(横截面)较大,还能够下手,假设直径较小,就没法往下切了。
因此。假设K维数据的分布像上面的豆腐一样,“轮着来”的切分方法是能够奏效。但是假设K维度上数据的分布像木条一样。“轮着来”就不好用了。因此,还须要想想其它的切法。
假设一个K维数据集合的分布像木条一样。那就是说明这K维数据在木条较长方向代表的维度上。这些数据的分布散得比較开,数学上来说,就是这些数据在该维度上的方差(invariance)比較大。换句话说,正由于这些数据在该维度上分散的比較开,我们就更easy在这个维度上将它们划分开。因此,这就引出了我们选择维度的还有一种方法:最慷慨差法(max
invarince)。即每次我们选择维度进行划分时,都选择具有最慷慨差维度。
问题2:在某个维度上进行划分时。如何确保在这一维度上的划分得到的两个子集合的数量尽量相等。即左子树和右子树中的结点个数尽量相等?
如果当前我们依照最慷慨差法选择了在维度i上进行K维数据集S的划分,此时我们须要在维度i上将K维数据集合S划分为两个子集合A和B,子集合A中的数据在维度i上的值都小于子集合B中。首先考虑最简单的划分法,即选择第一个数作为比較对象(即划分轴。pivot),S中剩余的其它全部K维数据都跟该pivot在维度i上进行比較。如果小于pivot则划A集合。大于则划入B集合。
把A集合和B集合分别看做是左子树和右子树,那么我们在构造一个二叉树的时候,当然是希望它是一棵尽量平衡的树,即左右子树中的结点个数相差不大。
而A集合和B集合中数据的个数显然跟pivot值有关。由于它们是跟pivot比較后才被划分到对应的集合中去的。好了。如今的问题就是确定pivot了。给定一个数组。如何才干得到两个子数组,这两个数组包括的元素个数差点儿相同且当中一个子数组中的元素值都小于还有一个子数组呢?方法非常easy,找到数组中的中值(即中位数。median)。然后将数组中全部元素与中值进行比較,就能够得到上述两个子数组。相同,在维度i上进行划分时,pivot就选择该维度i上全部数据的中值。这样得到的两个子集合数据个数就基本相同了。
攻克了上面两个重要的问题后,就得到了Kd-Tree的构造算法了。
Kd-Tree的构建算法:
(1)在K维数据集合中选择具有最慷慨差的维度k,然后在该维度上选择中值m为pivot对该数据集合进行划分。得到两个子集合;同一时候创建一个树结点node,用于存储;
(2)对两个子集合反复(1)步骤的过程,直至全部子集合都不能再划分为止。假设某个子集合不能再划分时,则将该子集合中的数据保存到叶子结点(leaf
node)。
以上就是创建Kd-Tree的算法。
以下给出一个简单样例。
给定二维数据集合:(2,3), (5,4),
(9,6), (4,7), (8,1), (7,2),利用上述算法构建一棵Kd-tree。左图是Kd-tree相应二维数据集合的一个空间划分。右图是构建的一棵Kd-tree。
图2 构建的kd-tree
当中圆圈代表了中间结点(k,
m),而红色矩形代表了叶子结点。
Kd-Tree与一维二叉查找树之间的差别:
二叉查找树:数据存放在树中的每一个结点(根结点、中间结点、叶子结点)中;
Kd-Tree:数据仅仅存放在叶子结点,而根结点和中间结点存放一些空间划分信息(比如划分维度、划分值);
构建好一棵Kd-Tree后,以下给出利用Kd-Tree进行近期邻查找的算法:
(1)将查询数据Q从根结点開始,依照Q与各个结点的比較结果向下訪问Kd-Tree,直至达到叶子结点。
当中Q与结点的比較指的是将Q相应于结点中的k维度上的值与m进行比較,若Q(k)
< m,则訪问左子树。否则訪问右子树。达到叶子结点时,计算Q与叶子结点上保存的数据之间的距离。记录下最小距离相应的数据点。记为当前“近期邻点”Pcur和最小距离Dcur。
(2)进行回溯(Backtracking)操作,该操作是为了找到离Q更近的“近期邻点”。
即推断未被訪问过的分支里是否还有离Q更近的点。它们之间的距离小于Dcur。
假设Q与其父结点下的未被訪问过的分支之间的距离小于Dcur,则觉得该分支中存在离P更近的数据,进入该结点,进行(1)步骤一样的查找过程,假设找到更近的数据点,则更新为当前的“近期邻点”Pcur,并更新Dcur。
假设Q与其父结点下的未被訪问过的分支之间的距离大于Dcur,则说明该分支内不存在与Q更近的点。
回溯的推断过程是从下往上进行的,直到回溯到根结点时已经不存在与P更近的分支为止。
如何推断未被訪问过的树分支Branch里是否还有离Q更近的点?
从几何空间上来看,就是推断以Q为中心center和以Dcur为半径Radius的超球面(Hypersphere)与树分支Branch代表的超矩形(Hyperrectangle)之间是否相交。
在实现中,我们能够有两种方式来求Q与树分支Branch之间的距离。第一种是在构造树的过程中。就记录下每一个子树中包括的全部数据在该子树相应的维度k上的边界參数[min,
max];另外一种是在构造树的过程中,记录下每一个子树所在的切割维度k和切割值m,(k, m)。Q与子树的距离则为|Q(k) - m|。
以上就是Kd-tree的构造过程和基于Kd-Tree的近期邻查找过程。
以下用一个简单的样例来演示基于Kd-Tree的近期邻查找的过程。
数据点集合:(2,3), (4,7),
(5,4), (9,6), (8,1), (7,2) 。
已建好的Kd-Tree:
图3 构建的kd-tree
当中,左图中红色点表示数据集合中的全部点。
查询点: (8, 3) (在左图中用茶色菱形点表示)
第一次查询:
图4 第一次查询的kd-tree
当前近期邻点: (9, 6) , 近期邻距离:
sqrt(10)。
且在未被选择的树分支中存在于Q更近的点(如茶色圈圈内的两个红色点)
回溯:
图5 回溯kd-tree
当前近期邻点: (8, 1)和(7,
2) 。 近期邻距离: sqrt(2)
最后,查询点(8, 3)的近似近期邻点为(8,
1)和(7, 2) 。
二、Kd-tree
with BBF
上一节介绍的Kd-tree在维度较小时(比如:K≤30),算法的查找效率非常高,然而当Kd-tree用于对高维数据(比如:K≥100)进行索引和查找时,就面临着维数灾难(curse
of dimension)问题,查找效率会随着维度的添加而迅速下降。
通常。实际应用中,我们经常处理的数据都具有高维的特点,比如在图像检索和识别中。每张图像通经常使用一个几百维的向量来表示,每一个特征点的局部特征用一个高维向量来表征(比如:128维的SIFT特征)。因此,为了可以让Kd-tree满足对高维数据的索引,Jeffrey
S. Beis和David G. Lowe提出了一种改进算法——Kd-tree with BBF(Best Bin First),该算法可以实现近似K近邻的高速搜索。在保证一定查找精度的前提下使得查找速度较快。
在介绍BBF算法前。我们先来看一下原始Kd-tree是为什么在低维空间中有效而到了高维空间后查找效率就会下降。在原始kd-tree的近期邻查找算法中(第一节中介绍的算法)。为了可以找到查询点Q在数据集合中的近期邻点,有一个重要的操作步骤:回溯,该步骤是在未被訪问过的且与Q的超球面相交的子树分支中查找可能存在的近期邻点。
随着维度K的增大。与Q的超球面相交的超矩形(子树分支所在的区域)就会添加,这就意味着须要回溯推断的树分支就会很多其它,从而算法的查找效率便会下降非常大。
一个非常自然的思路是:既然kd-tree算法在高维空间中是因为过多的回溯次数导致算法查找效率下降的话。我们就能够限制查找时进行回溯的次数上限,从而避免查找效率下降。
这样做有两个问题须要解决:1)最大回溯次数怎么确定?2)如何保证在最大回溯次数内找到的近期邻比較接近真实近期邻。即查找精确度不能下降太大。
问题1):最大回溯次数怎么确定?
最大回溯次数一般人为设定,通常依据在数据集上的实验结果进行调整。
问题2):如何保证在最大回溯次数内找到的近期邻比較接近真实近期邻。即查找精确度不能下降太大?
限制回溯次数后。假设我们还是依照原来的回溯方法挨个地进行訪问的话。那非常显然最后的查找结果的精度就非常大程度上取决于数据的分布和回溯次数了。挨个訪问的方法的问题在于觉得每一个待回溯的树分支中存在近期邻的概率是一样的,所以它对全部的待回溯树分支一视同仁。实际上。在这些待回溯树分支中,有些树分支存在近期邻的可能性比其它树分支要高,由于树分支离Q点之间的距离或相交程度是不一样的,离Q更近的树分支存在Q的近期邻的可能性更高。因此,我们须要差别对待每一个待回溯的树分支。即採用某种优先级顺序来訪问这些待回溯树分支。使得在有限的回溯次数中找到Q的近期邻的可能性非常高。
我们要介绍的BBF算法正是基于这种解决思路。以下我们介绍BBF查找算法。
基于BBF的Kd-Tree近似近期邻查找算法
已知:
Q:查询数据;
KT:已建好的Kd-Tree;
1. 查找Q的当前近期邻点P
1)从KT的根结点開始,将Q与中间结点node(k,m)进行比較,依据比較结果选择某个树分支Branch(或称为Bin);并将未被选择的还有一个树分支(Unexplored
Branch)所在的树中位置和它跟Q之间的距离一起保存到一个优先级队列中Queue;
2)依照步骤1)的过程。对树分支Branch进行如上比較和选择,直至訪问到叶子结点。然后计算Q与叶子结点中保存的数据之间的距离,并记录下最小距离D以及相应的数据P。
注:
A、Q与中间结点node(k,m)的比較过程:假设Q(k)
> m则选择右子树,否则选择左子树。
B、优先级队列:依照距离从小到大的顺序排列。
C、叶子结点:每一个叶子结点中保存的数据的个数可能是一个或多个。
2. 基于BBF的回溯
已知:最大回溯次数BTmax
1)假设当前回溯的次数小于BTmax,且Queue不为空。则进行例如以下操作:
从Queue中取出最小距离相应的Branch,然后依照1.1步骤訪问该Branch直至达到叶子结点;计算Q与叶子结点中各个数据间距离,假设有比D更小的值,则将该值赋给D。该数据则被觉得是Q的当前近似近期邻点。
2)反复1)步骤,直到回溯次数大于BTmax或Queue为空时,查找结束,此时得到的数据P和距离D就是Q的近似近期邻点和它们之间的距离。
以下用一个简单的样例来演示基于Kd-Tree+BBF的近似近期邻查找的过程。
数据点集合:(2,3), (4,7),
(5,4), (9,6), (8,1), (7,2) 。
已建好的Kd-Tree:
图6 构建的kd-tree
基于BBF的查找的过程:
查询点Q:
(5.5, 5)
第一遍查询:
图7 第一次查询的kd-tree
当前近期邻点: (9, 6) , 近期邻距离:
sqrt(13.25)。
同一时候将未被选择的树分支的位置和与Q的距离记录到优先级队列中。
BBF回溯:
从优先级队列里选择距离Q近期的未被选择树分支进行回溯。
图8 利用BBF方法回溯kd-tree
当前近期邻点: (4, 7) 。 近期邻距离:
sqrt(6.25)
继续从优先级队列里选择距离Q近期的未被选择树分支进行回溯。
图9 利用BBF方法回溯kd-tree
当前近期邻点: (5, 4) , 近期邻距离:
sqrt(1.25)
最后,查询点(5.5,
5)的近似近期邻点为(5, 4) 。
三、參考文献
Paper
[1] Multidimensional
binary search trees used for associative searching
[2] Shape indexing
using approximate nearest-neighbour search in high-dimensional spaces
Tutorial
[1] An introductory
tutorial on kd trees
[2] Nearest-Neighbor
Methods in Learning and Vision: Theory and Practice
Website
[1] wiki: http://en.wikipedia.org/wiki/K-d_tree
Code
[1] OpenCV
FLANN
[2] VLFeat
[3] FLANN
[4] KD-Tree
Implementation in Java and C#
[5] C/C++
http://code.google.com/p/kdtree/
https://github.com/sdeming/kdtree
版权声明:本文博主原创文章。博客,未经同意不得转载。
Kd-Tree算法原理和开源实现代码的更多相关文章
- K-D TREE算法原理及实现
博客转载自:https://leileiluoluo.com/posts/kdtree-algorithm-and-implementation.html k-d tree即k-dimensional ...
- FP Tree算法原理总结(转载)
FP Tree算法原理总结 在Apriori算法原理总结中,我们对Apriori算法的原理做了总结.作为一个挖掘频繁项集的算法,Apriori算法需要多次扫描数据,I/O是很大的瓶颈.为了解决这个问题 ...
- 【数据结构与算法】k-d tree算法
k-d tree算法 k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构.主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索). 应用背景 SIFT算法中做特征点 ...
- k-d tree算法
k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构.主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索). 应用背景 SIFT算法中做特征点匹配的时候就会利用到k ...
- FP Tree算法原理总结
在Apriori算法原理总结中,我们对Apriori算法的原理做了总结.作为一个挖掘频繁项集的算法,Apriori算法需要多次扫描数据,I/O是很大的瓶颈.为了解决这个问题,FP Tree算法(也称F ...
- 多项式相乘快速算法原理及相应C代码实现---用到fft
最近认真研究了一下算法导论里面的多项式乘法的快速计算问题,主要是用到了FFT,自己也实现了一下,总结如下. 1.多项式乘法 两个多项式相乘即为多项式乘法,例如:3*x^7+4*x^5+1*x^2+5与 ...
- 最全排序算法原理解析、java代码实现以及总结归纳
算法分类 十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序. 线性时间非比较类排序:不通过 ...
- Kd Tree算法详解
kd树(k-dimensional树的简称),是一种分割k维数据空间的数据结构,主要应用于多维空间关键数据的近邻查找(Nearest Neighbor)和近似最近邻查找(Approximate Nea ...
- KD Tree算法
参考:http://blog.csdn.net/v_july_v/article/details/8203674 #!/user/bin/env python # -*- coding:utf8 -* ...
随机推荐
- Blend4精选案例图解教程(四):请给我路径指引
原文:Blend4精选案例图解教程(四):请给我路径指引 路径在界面设计中,可以起到很好的辅助作用,我常常使用它来对元素进行规则排列和非规则排列控制. 本次教程将演示,Blend中路径的常规用法. 1 ...
- iOS 真机调试(最具体的步骤来解决历史,hmt精心打造)
/*************************************************************1************************************* ...
- C#使用SqlBulkCopy将DataTable写入数据库的表中(表不存在则创建新表,数据存在则更新,不存在则插入)
原文:.net使用SqlBulkCopy导入数据(创建新表) .net2.0后ado.net提供了一个快速导入sqlserver的方法sqlbulkcopy.导入效率非常高. 包装了一个简单的sql ...
- android-将系统和应用程序级的屏幕亮度
/** * 获取当前屏幕亮度模式 * SCREEN_BRIGHTNESS_MODE_AUTOMATIC=1 为自己主动调节屏幕亮度 * SCREEN_BRIGHTNESS_MODE_MANUAL=0 ...
- $POST 、$HTTP_RAW_POST_DATA、php://input三者之间的差别
$POST .$HTTP_RAW_POST_DATA.php://input三者之间的差别 总是产生变量包括有原始的 POST 数据.否则,此变量仅在碰到未识别 MIME 类型的数据时产生.只是,訪问 ...
- 熟知CDN
一.概念 1.什么是CDN? CDN的全拼是(Content Delivery Network).即内容分发网络.其基本思想是尽可能避开互联网上有可能影响传输数据速度和稳定性瓶颈的环节,使内容传输的更 ...
- Windows Phone 的控件倾斜效果
原文:Windows Phone 的控件倾斜效果 Windows Phone 7的系统设置里,按钮都有一个点击倾斜的效果,但自己添加的控件就没有.但微软提供了这个效果的代码:TiltEffect MS ...
- iOS开发多线程篇—多线程简介
iOS开发多线程篇-多线程简介 一.进程和线程 1.什么是进程 进程是指在系统中正在执行的一个应用程序 每一个进程之间是独立的.每一个进程均执行在其专用且受保护的内存空间内 比方同一时候打开QQ.Xc ...
- Codeforces Round #270(利用prim算法)
D. Design Tutorial: Inverse the Problem time limit per test 2 seconds memory limit per test 256 mega ...
- linux下磁盘进行分区、文件系统创建、挂载和卸载(转)
任务的原因:由于,刚购买来的服务器需要将磁盘挂载到操作系统上,为了挂载磁盘首先要对磁盘进行分区,然后进行文件系统的创建,最后将磁盘挂载到操作系统上的某个目录. MBR(Master Boot Reco ...