转自 http://blog.csdn.net/likika2012/article/details/39619687html
前两日,在微博上说:“到今天为止,我至少亏欠了3篇文章待写:一、KD树;二、神经网络;三、编程艺术第28章。你看到,blog内的文章与你于别处所见的任何都不一样。因而,等啊等,等一台电脑,只好等待..”。得益于田,借了我一台电脑(借他电脑的时候,我连表示感谢,他说“能找到工做全靠你的博客,这点儿小忙还说,不地道”,有的时候,稍许感觉到受人信任也是一种压力,愿我不辜负你们对个人信任),因而今天开始Top 10 Algorithms in Data Mining系列第三篇文章,即本文「从K近邻算法谈到KD树、SIFT+BBF算法」的创做。node
一我的坚持本身的兴趣是比较难的,由于太多的人太容易为外界所动了,而尤为当你没法从中获得多少实际性的回报时,所幸,我能一直坚持下来。毕达哥拉斯学派有句名言:“万物皆数”,最近读完「微积分概念发展史」后也感觉到了这一点。同时,从算法到数据挖掘、机器学习,再到数学,其中每个领域任何一个细节都值得探索终生,或许,这就是“终生为学”的意思。mysql
本文各部份内容分布以下:算法
- 第一部分讲K近邻算法,其中重点阐述了相关的距离度量表示法,
- 第二部分着重讲K近邻算法的实现--KD树,和KD树的插入,删除,最近邻查找等操做,及KD树的一系列相关改进(包括BBF,M树等);
- 第三部分讲KD树的应用:SIFT+kd_BBF搜索算法。
同时,你将看到,K近邻算法同本系列的前两篇文章所讲的决策树分类贝叶斯分类,及支持向量机SVM同样,也是用于解决分类问题的算法,sql

而本数据挖掘十大算法系列也会按照分类,聚类,关联分析,预测回归等问题依次展开阐述。数据库
OK,行文仓促,本文如有任何漏洞,问题或者错误,欢迎朋友们随时不吝指正,各位的批评也是我继续写下去的动力之一。感谢。编程
第一部分、K近邻算法
1.一、什么是K近邻算法
何谓K近邻算法,即K-Nearest Neighbor algorithm,简称KNN算法,单从名字来猜测,能够简单粗暴的认为是:K个最近的邻居,当K=1时,算法便成了最近邻算法,即寻找最近的那个邻居。为什么要找邻居?打个比方来讲,假设你来到一个陌生的村庄,如今你要找到与你有着类似特征的人群融入他们,所谓入伙。网络
用官方的话来讲,所谓K近邻算法,便是给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的K个实例(也就是上面所说的K个邻居),这K个实例的多数属于某个类,就把该输入实例分类到这个类中。根据这个说法,我们来看下引自维基百科上的一幅图:数据结构

如上图所示,有两类不一样的样本数据,分别用蓝色的小正方形和红色的小三角形表示,而图正中间的那个绿色的圆所标示的数据则是待分类的数据。也就是说,如今,咱们不知道中间那个绿色的数据是从属于哪一类(蓝色小正方形or红色小三角形),下面,咱们就要解决这个问题:给这个绿色的圆分类。
咱们常说,物以类聚,人以群分,判别一我的是一个什么样品质特征的人,经常能够从他/她身边的朋友入手,所谓观其友,而识其人。咱们不是要判别上图中那个绿色的圆是属于哪一类数据么,好说,从它的邻居下手。但一次性看多少个邻居呢?从上图中,你还能看到:
app
- 若是K=3,绿色圆点的最近的3个邻居是2个红色小三角形和1个蓝色小正方形,少数从属于多数,基于统计的方法,断定绿色的这个待分类点属于红色的三角形一类。
- 若是K=5,绿色圆点的最近的5个邻居是2个红色三角形和3个蓝色的正方形,仍是少数从属于多数,基于统计的方法,断定绿色的这个待分类点属于蓝色的正方形一类。
于此咱们看到,当没法断定当前待分类点是从属于已知分类中的哪一类时,咱们能够依据统计学的理论看它所处的位置特征,衡量它周围邻居的权重,而把它归为(或分配)到权重更大的那一类。这就是K近邻算法的核心思想。
1.二、近邻的距离度量表示法
上文第一节,咱们看到,K近邻算法的核心在于找到实例点的邻居,这个时候,问题就接踵而至了,如何找到邻居,邻居的断定标准是什么,用什么来度量。这一系列问题即是下面要讲的距离度量表示法。但有的读者可能就有疑问了,我是要找邻居,找类似性,怎么又跟距离扯上关系了?
这是由于特征空间中两个实例点的距离能够反应出两个实例点之间的类似性程度。K近邻模型的特征空间通常是n维实数向量空间,使用的距离可使欧式距离,也是能够是其它距离,既然扯到了距离,下面就来具体阐述下都有哪些距离度量的表示法,权当扩展。
第二部分、K近邻算法的实现:KD树
2.0、背景
以前blog内曾经介绍过SIFT特征匹配算法,特征点匹配和数据库查、图像检索本质上是同一个问题,均可以归结为一个经过距离函数在高维矢量之间进行类似性检索的问题,如何快速而准确地找到查询点的近邻,很多人提出了不少高维空间索引结构和近似查询的算法。
通常说来,索引结构中类似性查询有两种基本的方式:
- 一种是范围查询,范围查询时给定查询点和查询距离阈值,从数据集中查找全部与查询点距离小于阈值的数据
- 另外一种是K近邻查询,就是给定查询点及正整数K,从数据集中找到距离查询点最近的K个数据,当K=1时,它就是最近邻查询。
一样,针对特征点匹配也有两种方法:
- 最容易的办法就是线性扫描,也就是咱们常说的穷举搜索,依次计算样本集E中每一个样本到输入实例点的距离,而后抽取出计算出来的最小距离的点即为最近邻点。此种办法简单直白,但当样本集或训练集很大时,它的缺点就立马暴露出来了,举个例子,在物体识别的问题中,可能有数千个甚至数万个SIFT特征点,而去一一计算这成千上万的特征点与输入实例点的距离,明显是不足取的。
- 另一种,就是构建数据索引,由于实际数据通常都会呈现簇状的聚类形态,所以咱们想到创建数据索引,而后再进行快速匹配。索引树是一种树结构索引方法,其基本思想是对搜索空间进行层次划分。根据划分的空间是否有混叠能够分为Clipping和Overlapping两种。前者划分空间没有重叠,其表明就是k-d树;后者划分空间相互有交叠,其表明为R树。
而关于R树本blog内以前已有介绍(同时,关于基于R树的最近邻查找,还能够看下这篇文章:http://blog.sina.com.cn/s/blog_72e1c7550101dsc3.html),本文着重介绍k-d树。
1975年,来自斯坦福大学的Jon Louis Bentley在ACM杂志上发表的一篇论文:Multidimensional Binary Search Trees Used for Associative Searching 中正式提出和阐述的了以下图形式的把空间划分为多个部分的k-d树。

2.一、什么是KD树
Kd-树是K-dimension tree的缩写,是对数据点在k维空间(如二维(x,y),三维(x,y,z),k维(x1,y,z..))中划分的一种数据结构,主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。本质上说,Kd-树就是一种平衡二叉树。
首先必须搞清楚的是,k-d树是一种空间划分树,说白了,就是把整个空间划分为特定的几个部分,而后在特定空间的部份内进行相关搜索操做。想像一个三维(多维有点为难你的想象力了)空间,kd树按照必定的划分规则把这个三维空间划分了多个空间,以下图所示:

2.二、KD树的构建
kd树构建的伪代码以下图所示:

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

6个二维数据点{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}构建kd树的具体步骤为:
- 肯定:split域=x。具体是:6个数据点在x,y维度上的数据方差分别为39,28.63,因此在x轴上方差更大,故split域值为x;
- 肯定:Node-data = (7,2)。具体是:根据x维上的值将数据排序,6个数据的中值(所谓中值,即中间大小的值)为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)则为根结点的左右子结点,而(2,3),(4,7)则为(5,4)的左右孩子(经过两条细红斜线相连),最后,(8,1)为(9,6)的左孩子(经过细红斜线相连)。如此,便造成了下面这样一棵k-d树:
k-d树的数据结构

针对上表给出的kd树的数据结构,转化成具体代码以下所示(注,本文如下代码分析基于Rob Hess维护的sift库):
- struct kd_node
- {
- int ki;
- double kv;
- int leaf;
- struct feature* features;
- int n;
- struct kd_node* kd_left;
- struct kd_node* kd_right;
- };
- struct kd_node
- {
- int ki;
- double kv;
- int leaf;
- struct feature* features;
- int n;
- struct kd_node* kd_left;
- struct kd_node* kd_right;
- };
也就是说,如以前所述,kd树中,kd表明k-dimension,每一个节点即为一个k维的点。每一个非叶节点能够想象为一个分割超平面,用垂直于坐标轴的超平面将空间分为两个部分,这样递归的从根节点不停的划分,直到没有实例为止。经典的构造k-d tree的规则以下:
- 随着树的深度增长,循环的选取坐标轴,做为分割超平面的法向量。对于3-d tree来讲,根节点选取x轴,根节点的孩子选取y轴,根节点的孙子选取z轴,根节点的曾孙子选取x轴,这样循环下去。
- 每次均为全部对应实例的中位数的实例做为切分点,切分点做为父节点,左右两侧为划分的做为左右两子树。
对于n个实例的k维数据来讲,创建kd-tree的时间复杂度为O(k*n*logn)。
如下是构建k-d树的代码:
- struct kd_node* kdtree_build( struct feature* features, int n )
- {
- struct kd_node* kd_root;
-
- if( ! features || n <= 0 )
- {
- fprintf( stderr, "Warning: kdtree_build(): no features, %s, line %d\n",
- __FILE__, __LINE__ );
- return NULL;
- }
-
-
- kd_root = kd_node_init( features, n );
- expand_kd_node_subtree( kd_root );
-
- return kd_root;
- }
- struct kd_node* kdtree_build( struct feature* features, int n )
- {
- struct kd_node* kd_root;
-
- if( ! features || n <= 0 )
- {
- fprintf( stderr, "Warning: kdtree_build(): no features, %s, line %d\n",
- __FILE__, __LINE__ );
- return NULL;
- }
-
-
- kd_root = kd_node_init( features, n );
- expand_kd_node_subtree( kd_root );
-
- return kd_root;
- }
上面的涉及初始化操做的两个函数kd_node_init,及expand_kd_node_subtree代码分别以下所示:
- static struct kd_node* kd_node_init( struct feature* features, int n )
- {
- struct kd_node* kd_node;
-
- kd_node = (struct kd_node*)(malloc( sizeof( struct kd_node ) ));
- memset( kd_node, 0, sizeof( struct kd_node ) );
- kd_node->ki = -1;
- kd_node->features = features;
- kd_node->n = n;
-
- return kd_node;
- }
- static struct kd_node* kd_node_init( struct feature* features, int n )
- {
- struct kd_node* kd_node;
-
- kd_node = (struct kd_node*)(malloc( sizeof( struct kd_node ) ));
- memset( kd_node, 0, sizeof( struct kd_node ) );
- kd_node->ki = -1;
- kd_node->features = features;
- kd_node->n = n;
-
- return kd_node;
- }
- static void expand_kd_node_subtree( struct kd_node* kd_node )
- {
-
- if( kd_node->n == 1 || kd_node->n == 0 )
- {
- kd_node->leaf = 1;
- return;
- }
-
- assign_part_key( kd_node );
- partition_features( kd_node );
-
- if( kd_node->kd_left )
- expand_kd_node_subtree( kd_node->kd_left );
- if( kd_node->kd_right )
- expand_kd_node_subtree( kd_node->kd_right );
- }
- static void expand_kd_node_subtree( struct kd_node* kd_node )
- {
-
- if( kd_node->n == 1 || kd_node->n == 0 )
- {
- kd_node->leaf = 1;
- return;
- }
-
- assign_part_key( kd_node );
- partition_features( kd_node );
-
- if( kd_node->kd_left )
- expand_kd_node_subtree( kd_node->kd_left );
- if( kd_node->kd_right )
- expand_kd_node_subtree( kd_node->kd_right );
- }
构建完kd树以后,现在进行最近邻搜索呢?从下面的动态gif图中,你是否能看出些许端倪呢?

k-d树算法能够分为两大部分,除了上部分有关k-d树自己这种数据结构创建的算法,另外一部分是在创建的k-d树上各类诸如插入,删除,查找(最邻近查找)等操做涉及的算法。下面,我们依次来看kd树的插入、删除、查找操做。
2.三、KD树的插入
元素插入到一个K-D树的方法和二叉检索树相似。本质上,在偶数层比较x坐标值,而在奇数层比较y坐标值。当咱们到达了树的底部,(也就是当一个空指针出现),咱们也就找到告终点将要插入的位置。生成的K-D树的形状依赖于结点插入时的顺序。给定N个点,其中一个结点插入和检索的平均代价是O(log2N)。
下面4副图(来源:中国地质大学电子课件)说明了插入顺序为(a) Chicago, (b) Mobile, (c) Toronto, and (d) Buffalo,创建空间K-D树的示例:




应该清楚,这里描述的插入过程当中,每一个结点将其所在的平面分割成两部分。因比,Chicago 将平面上全部结点分红两部分,一部分全部的结点x坐标值小于35,另外一部分结点的x坐标值大于或等于35。一样Mobile将全部x坐标值大于35的结点以分红两部分,一部分结点的Y坐标值是小于10,另外一部分结点的Y坐标值大于或等于10。后面的Toronto、Buffalo也按照一分为二的规则继续划分。
2.四、KD树的删除
KD树的删除能够用递归程序来实现。咱们假设但愿从K-D树中删除结点(a,b)。若是(a,b)的两个子树都为空,则用空树来代替(a,b)。不然,在(a,b)的子树中寻找一个合适的结点来代替它,譬如(c,d),则递归地从K-D树中删除(c,d)。一旦(c,d)已经被删除,则用(c,d)代替(a,b)。假设(a,b)是一个X识别器,那么,
它得替代节点要么是(a,b)左子树中的X坐标最大值的结点,要么是(a,b)右子树中x坐标最小值的结点。
也就是说,跟普通二叉树(包括以下图所示的
红黑树)结点的删除是一样的思想:用被删除节点A的左子树的最右节点或者A的右子树的最左节点做为替代A的节点(好比,下图红黑树中,若要删除根结点26,第一步即是用23或28取代根结点26)。
当(a,b)的右子树为空时,找到(a,b)左子树中具备x坐标最大的结点,譬如(c,d),将(a,b)的左子树放到(c,d)的右子树中,且在树中从它的上一层递归地应用删除过程(也就是(a,b)的左子树) 。
下面来举一个实际的例子(来源:中国地质大学电子课件,原课件错误已经在下文中订正),以下图所示,原始图像及对应的kd树,如今要删除图中的A结点,请看一系列删除步骤:
要删除上图中结点A,选择结点A的右子树中X坐标值最小的结点,这里是C,C成为根,以下图:
从C的右子树中找出一个结点代替先前C的位置,
这里是D,并将D的左子树转为它的右子树,D代替先前C的位置,以下图:
在D的新右子树中,找X坐标最小的结点,这里为H,H代替D的位置,
在D的右子树中找到一个Y坐标最小的值,这里是I,将I代替原先H的位置,从而A结点从图中顺利删除,以下图所示:
从一个K-D树中删除结点(a,b)的问题变成了在(a,b)的子树中寻找x坐标为最小的结点。不幸的是寻找最小x坐标值的结点比二叉检索树中解决相似的问题要复杂得多。特别是虽然最小x坐标值的结点必定在x识别器的左子树中,但它一样可在y识别器的两个子树中。所以关系到检索,且必须注意检索坐标,以使在每一个奇数层仅检索2个子树中的一个。
从K-D树中删除一个结点是代价很高的,很清楚删除子树的根受到子树中结点个数的限制。用TPL(T)表示树T总的路径长度。可看出树中子树大小的总和为TPL(T)+N。 以随机方式插入N个点造成树的TPL是O(N*log2N),这就意味着从一个随机造成的K-D树中删除一个随机选取的结点平均代价的上界是O(log2N) 。
2.五、KD树的最近邻搜索算法
现实生活中有许多问题须要在多维数据的快速分析和快速搜索,对于这个问题最经常使用的方法是所谓的kd树。在k-d树中进行数据的查找也是特征匹配的重要环节,其目的是检索在k-d树中与查询点距离最近的数据点。在一个N维的笛卡儿空间在两个点之间的距离是由下述公式肯定:

2.5.一、k-d树查询算法的伪代码
k-d树查询算法的伪代码以下所示:
- 算法:k-d树最邻近查找
- 输入:Kd,
- target
- 输出:nearest,
- dist
-
- 1. If Kd为NULL,则设dist为infinite并返回
- 2.
- Kd_point = &Kd;
- nearest = Kd_point -> Node-data;
-
- while(Kd_point)
- push(Kd_point)到search_path中;
-
- If Dist(nearest,target) > Dist(Kd_point -> Node-data,target)
- nearest = Kd_point -> Node-data;
- Min_dist = Dist(Kd_point,target);
- s = Kd_point -> split;
-
- If target[s] <= Kd_point -> Node-data[s]
- Kd_point = Kd_point -> left;
- else
- Kd_point = Kd_point ->right;
- End while
-
- 3.
- while(search_path != NULL)
- back_point = 从search_path取出一个节点指针;
- s = back_point -> split;
-
- If Dist(target[s],back_point -> Node-data[s]) < Max_dist
- If target[s] <= back_point -> Node-data[s]
- Kd_point = back_point -> right;
- else
- Kd_point = back_point -> left;
- 将Kd_point压入search_path堆栈;
-
- If Dist(nearest,target) > Dist(Kd_Point -> Node-data,target)
- nearest = Kd_point -> Node-data;
- Min_dist = Dist(Kd_point -> Node-data,target);
- End while
- 算法:k-d树最邻近查找
- 输入:Kd,
- target
- 输出:nearest,
- dist
-
- 1. If Kd为NULL,则设dist为infinite并返回
- 2.
- Kd_point = &Kd;
- nearest = Kd_point -> Node-data;
-
- while(Kd_point)
- push(Kd_point)到search_path中;
-
- If Dist(nearest,target) > Dist(Kd_point -> Node-data,target)
- nearest = Kd_point -> Node-data;
- Min_dist = Dist(Kd_point,target);
- s = Kd_point -> split;
-
- If target[s] <= Kd_point -> Node-data[s]
- Kd_point = Kd_point -> left;
- else
- Kd_point = Kd_point ->right;
- End while
-
- 3.
- while(search_path != NULL)
- back_point = 从search_path取出一个节点指针;
- s = back_point -> split;
-
- If Dist(target[s],back_point -> Node-data[s]) < Max_dist
- If target[s] <= back_point -> Node-data[s]
- Kd_point = back_point -> right;
- else
- Kd_point = back_point -> left;
- 将Kd_point压入search_path堆栈;
-
- If Dist(nearest,target) > Dist(Kd_Point -> Node-data,target)
- nearest = Kd_point -> Node-data;
- Min_dist = Dist(Kd_point -> Node-data,target);
- End while
读者来信点评@yhxyhxyhx,在“将Kd_point压入search_path堆栈;”这行代码后,应该是调到步骤2再往下走二分搜索的逻辑一直到叶结点,我写了一个递归版本的二维kd tree的搜索函数你对比的看看:
- void innerGetClosest(NODE* pNode, PT point, PT& res, int& nMinDis)
- {
- if (NULL == pNode)
- return;
- int nCurDis = abs(point.x - pNode->pt.x) + abs(point.y - pNode->pt.y);
- if (nMinDis < 0 || nCurDis < nMinDis)
- {
- nMinDis = nCurDis;
- res = pNode->pt;
- }
- if (pNode->splitX && point.x <= pNode->pt.x || !pNode->splitX && point.y <= pNode->pt.y)
- innerGetClosest(pNode->pLft, point, res, nMinDis);
- else
- innerGetClosest(pNode->pRgt, point, res, nMinDis);
- int rang = pNode->splitX ? abs(point.x - pNode->pt.x) : abs(point.y - pNode->pt.y);
- if (rang > nMinDis)
- return;
- NODE* pGoInto = pNode->pLft;
- if (pNode->splitX && point.x > pNode->pt.x || !pNode->splitX && point.y > pNode->pt.y)
- pGoInto = pNode->pRgt;
- innerGetClosest(pGoInto, point, res, nMinDis);
- }
- void innerGetClosest(NODE* pNode, PT point, PT& res, int& nMinDis)
- {
- if (NULL == pNode)
- return;
- int nCurDis = abs(point.x - pNode->pt.x) + abs(point.y - pNode->pt.y);
- if (nMinDis < 0 || nCurDis < nMinDis)
- {
- nMinDis = nCurDis;
- res = pNode->pt;
- }
- if (pNode->splitX && point.x <= pNode->pt.x || !pNode->splitX && point.y <= pNode->pt.y)
- innerGetClosest(pNode->pLft, point, res, nMinDis);
- else
- innerGetClosest(pNode->pRgt, point, res, nMinDis);
- int rang = pNode->splitX ? abs(point.x - pNode->pt.x) : abs(point.y - pNode->pt.y);
- if (rang > nMinDis)
- return;
- NODE* pGoInto = pNode->pLft;
- if (pNode->splitX && point.x > pNode->pt.x || !pNode->splitX && point.y > pNode->pt.y)
- pGoInto = pNode->pRgt;
- innerGetClosest(pGoInto, point, res, nMinDis);
- }
下面,以两个简单的实例(例子来自图像局部不变特性特征与描述一书)来描述最邻近查找的基本思路。
2.5.二、举例:查询点(2.1,3.1)
星号表示要查询的点(2.1,3.1)。经过二叉搜索,顺着搜索路径很快就能找到最邻近的近似点,也就是叶子节点(2,3)。而找到的叶子节点并不必定就是最邻近的,最邻近确定距离查询点更近,应该位于以查询点为圆心且经过叶子节点的圆域内。为了找到真正的最近邻,还须要进行相关的‘回溯'操做。也就是说,算法首先沿搜索路径反向查找是否有距离查询点更近的数据点。
以查询(2.1,3.1)为例:
- 二叉树搜索:先从(7,2)点开始进行二叉查找,而后到达(5,4),最后到达(2,3),此时搜索路径中的节点为<(7,2),(5,4),(2,3)>,首先以(2,3)做为当前最近邻点,计算其到查询点(2.1,3.1)的距离为0.1414,
- 回溯查找:在获得(2,3)为查询点的最近点以后,回溯到其父节点(5,4),并判断在该父节点的其余子节点空间中是否有距离查询点更近的数据点。以(2.1,3.1)为圆心,以0.1414为半径画圆,以下图所示。发现该圆并不和超平面y = 4交割,所以不用进入(5,4)节点右子空间中(图中灰色区域)去搜索;
- 最后,再回溯到(7,2),以(2.1,3.1)为圆心,以0.1414为半径的圆更不会与x = 7超平面交割,所以不用进入(7,2)右子空间进行查找。至此,搜索路径中的节点已经所有回溯完,结束整个搜索,返回最近邻点(2,3),最近距离为0.1414。

2.5.三、举例:查询点(2,4.5)
一个复杂点了例子如查找点为(2,4.5),具体步骤依次以下:
- 一样先进行二叉查找,先从(7,2)查找到(5,4)节点,在进行查找时是由y = 4为分割超平面的,因为查找点为y值为4.5,所以进入右子空间查找到(4,7),造成搜索路径<(7,2),(5,4),(4,7)>,但(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。

上述两次实例代表,当查询点的邻域与分割超平面两侧空间交割时,须要查找另外一侧子空间,致使检索过程复杂,效率降低。
通常来说,最临近搜索只须要检测几个叶子结点便可,以下图所示:
可是,若是当实例点的分布比较糟糕时,几乎要遍历全部的结点,以下所示:
研究代表N个节点的K维k-d树搜索过程时间复杂度为:tworst=O(kN1-1/k)。
同时,以上为了介绍方便,讨论的是二维或三维情形。但在实际的应用中,如SIFT特征矢量128维,SURF特征矢量64维,维度都比较大,直接利用k-d树快速检索(维数不超过20)的性能急剧降低,几乎接近贪婪线性扫描。假设数据集的维数为D,通常来讲要求数据的规模N知足N»2D,才能达到高效的搜索。因此这就引出了一系列对k-d树算法的改进:BBF算法,和一系列M树、VP树、MVP树等高维空间索引树(下文2.6节kd树近邻搜索算法的改进:BBF算法,与2.7节球树、M树、VP树、MVP树)。
2.六、kd树近邻搜索算法的改进:BBF算法
我们顺着上一节的思路,参考统计学习方法一书上的内容,再来总结下kd树的最近邻搜索算法:
输入:以构造的kd树,目标点x;
输出:x 的最近邻
算法步骤以下:
- 在kd树种找出包含目标点x的叶结点:从根结点出发,递归地向下搜索kd树。若目标点x当前维的坐标小于切分点的坐标,则移动到左子结点,不然移动到右子结点,直到子结点为叶结点为止。
- 以此叶结点为“当前最近点”。
- 递归的向上回溯,在每一个结点进行如下操做:
(a)若是该结点保存的实例点比当前最近点距离目标点更近,则更新“当前最近点”,也就是说以该实例点为“当前最近点”。
(b)当前最近点必定存在于该结点一个子结点对应的区域,检查子结点的父结点的另外一子结点对应的区域是否有更近的点。具体作法是,检查另外一子结点对应的区域是否以目标点位球心,以目标点与“当前最近点”间的距离为半径的圆或超球体相交:
若是相交,可能在另外一个子结点对应的区域内存在距目标点更近的点,移动到另外一个子结点,接着,继续递归地进行最近邻搜索;
若是不相交,向上回溯。
- 当回退到根结点时,搜索结束,最后的“当前最近点”即为x 的最近邻点。
若是实例点是随机分布的,那么kd树搜索的平均计算复杂度是O(NlogN),这里的N是训练实例树。因此说,kd树更适用于训练实例数远大于空间维数时的k近邻搜索,当空间维数接近训练实例数时,它的效率会迅速降低,一降降到“解放前”:线性扫描的速度。
也正由于上述k最近邻搜索算法的第4个步骤中的所述:“回退到根结点时,搜索结束”,每一个最近邻点的查询比较完成过程最终都要回退到根结点而结束,而致使了许多没必要要回溯访问和比较到的结点,这些多余的损耗在高维度数据查找的时候,搜索效率将变得至关之地下,那有什么办法能够改进这个原始的kd树最近邻搜索算法呢?
从上述标准的kd树查询过程能够看出其搜索过程当中的“回溯”是由“查询路径”决定的,并无考虑查询路径上一些数据点自己的一些性质。一个简单的改进思路就是将“查询路径”上的结点进行排序,如按各自分割超平面(也称bin)与查询点的距离排序,也就是说,回溯检查老是从优先级最高(Best Bin)的树结点开始。
针对此BBF机制,读者Feng&书童点评道:
- 在某一层,分割面是第ki维,分割值是kv,那么 abs(q[ki]-kv) 就是没有选择的那个分支的优先级,也就是计算的是那一维上的距离;
- 同时,从优先队列里面取节点只在某次搜索到叶节点后才发生,计算过距离的节点不会出如今队列的,好比1~10这10个节点,你第一次搜索到叶节点的路径是1-5-7,那么1,5,7是不会出如今优先队列的。换句话说,优先队列里面存的都是查询路径上节点对应的相反子节点,好比:搜索左子树,就把对应这一层的右节点存进队列。
如此,就引出了本节要讨论的kd树最近邻搜索算法的改进:BBF(Best-Bin-First)查询算法,它是由发明sift算法的David Lowe在1997的一篇文章中针对高维数据提出的一种近似算法,此算法能确保优先检索包含最近邻点可能性较高的空间,此外,BBF机制还设置了一个运行超时限定。采用了BBF查询机制后,kd树即可以有效的扩展到高维数据集上。
伪代码以下图所示(图取自图像局部不变特性特征与描述一书):

仍是以上面的查询(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,直到优先队列为空。
如你在下图所见到的那样(话说,用鼠标在图片上写字着实很差写):


2.七、球树、M树、VP树、MVP树
2.7.一、球树
我们来针对上文内容总结回顾下,针对下面这样一棵kd树:

现要找它的最近邻。
经过上文2.5节,总结来讲,咱们已经知道:
一、为了找到一个给定目标点的最近邻,须要从树的根结点开始向下沿树找出目标点所在的区域,以下图所示,给定目标点,用星号标示,咱们彷佛一眼看出,有一个点离目标点最近,由于它落在以目标点为圆心以较小长度为半径的虚线圆内,但为了肯定是否可能还村庄一个最近的近邻,咱们会先检查叶节点的同胞结点,然叶节点的同胞结点在图中所示的阴影部分,虚线圆并不与之相交,因此肯定同胞叶结点不可能包含更近的近邻。

二、因而咱们回溯到父节点,并检查父节点的同胞结点,父节点的同胞结点覆盖了图中全部横线X轴上的区域。由于虚线圆与右上方的矩形(KD树把二维平面划分红一个一个矩形)相交...
如上,咱们看到,KD树是可用于有效寻找最近邻的一个树结构,但这个树结构其实并不完美,当处理不均匀分布的数据集时便会呈现出一个基本冲突:既邀请树有完美的平衡结构,又要求待查找的区域近似方形,但无论是近似方形,仍是矩形,甚至正方形,都不是最好的使用形状,由于他们都有角。

什么意思呢?就是说,在上图中,若是黑色的实例点离目标点星点再远一点,那么势必那个虚线圆会如红线所示那样扩大,以至与左上方矩形的右下角相交,既然相交了,那么势必又必须检查这个左上方矩形,而实际上,最近的点离星点的距离很近,检查左上方矩形区域已经是多余。于此咱们看见,KD树把二维平面划分红一个一个矩形,但矩形区域的角倒是个难以处理的问题。
解决的方案就是使用以下图所示的球树:

先从球中选择一个离球的中心最远的点,而后选择第二个点离第一个点最远,将球中全部的点分配到离这两个聚类中心最近的一个上,而后计算每一个聚类的中心,以及聚类可以包含它全部数据点所需的最小半径。这种方法的优势是分裂一个包含n个殊绝点的球的成本只是随n呈线性增长。

使用球树找出给定目标点的最近邻方法是,首先自上而下贯穿整棵树找出包含目标点所在的叶子,并在这个球里找出与目标点最靠近的点,这将肯定出目标点距离它的最近邻点的一个上限值,而后跟KD树查找同样,检查同胞结点,若是目标点到同胞结点中心的距离超过同胞结点的半径与当前的上限值之和,那么同胞结点里不可能存在一个更近的点;不然的话,必须进一步检查位于同胞结点如下的子树。
以下图,目标点仍是用一个星表示,黑色点是当前已知的的目标点的最近邻,灰色球里的全部内容将被排除,由于灰色球的中心点离的太远,因此它不可能包含一个更近的点,像这样,递归的向树的根结点进行回溯处理,检查全部可能包含一个更近于当前上限值的点的球。

球树是自上而下的创建,和KD树同样,根本问题就是要找到一个好的方法将包含数据点集的球分裂成两个,在实践中,没必要等到叶子结点只有两个胡数据点时才中止,能够采用和KD树同样的方法,一旦结点上的数据点打到预先设置的最小数量时,即可提早中止建树过程。
也就是上面所述,先从球中选择一个离球的中心最远的点,而后选择第二个点离第一个点最远,将球中全部的点分配到离这两个聚类中心最近的一个上,而后计算每一个聚类的中心,以及聚类可以包含它全部数据点所需的最小半径。这种方法的优势是分裂一个包含n个殊绝点的球的成本只是随n呈线性增长(注:本小节内容主要来自参考条目19:数据挖掘实用机器学习技术,[新西兰]Ian H.Witten 著,第4章4.7节)。
2.7.二、VP树与MVP树简介
高维特征向量的距离索引问题是基于内容的图像检索的一项关键技术,目前常常采用的解决办法是首先对高维特征空间作降维处理,而后采用包括四叉树、kd树、R树族等在内的主流多维索引结构,这种方法的出发点是:目前的主流多维索引结构在处理维数较低的状况时具备比较好的效率,但对于维数很高的状况则显得力不从心(即所谓的维数危机) 。
实验结果代表当特征空间的维数超过20 的时候,效率明显下降,而可视化特征每每采用高维向量描述,通常状况下能够达到10^2的量级,甚至更高。在表示图像可视化特征的高维向量中各维信息的重要程度是不一样的,经过降维技术去除属于次要信息的特征向量以及相关性较强的特征向量,从而下降特征空间的维数,这种方法已经获得了一些实际应用。
然而这种方法存在不足之处采用降维技术可能会致使有效信息的损失,尤为不适合于处理特征空间中的特征向量相关性很小的状况。另外主流的多维索引结构大都针对欧氏空间,设计须要利用到欧氏空间的几何性质,而图像的类似性计算极可能不限于基于欧氏距离。这种状况下人们愈来愈关注基于距离的度量空间高维索引结构能够直接应用于高维向量类似性查询问题。