kd树

K近邻算法 KD树   http://blog.csdn.net/v_JULY_v/article/details/8203674

第一部分、K近邻算法

1.1、什么是K近邻算法

 在使用k近邻法进行分类时,对新的实例,根据其k个最近邻的训练实例的类别,通过多数表决的方式进行预测。由于k近邻模型的特征空间一般是n维实数向量,所以距离的计算通常采用的是欧式距离。关键的是k值的选取,如果k值太小就意味着整体模型变得复杂,容易发生过拟合,即如果邻近的实例点恰巧是噪声,预测就会出错,极端的情况是k=1,称为最近邻算法,对于待预测点x,与x最近的点决定了x的类别。k值得增大意味着整体的模型变得简单,极端的情况是k=N,那么无论输入实例是什么,都简单地预测它属于训练集中最多的类,这样的模型过于简单。经验是,k值一般去一个比较小的值,通常采取交叉验证的方法来选取最优的k值。

      

1.2、近邻的距离度量表示法

  因为特征空间中两个实例点的距离可以反应出两个实例点之间的相似性程度。K近邻模型的特征空间一般是n维实数向量空间,使用的距离可以使欧式距离,也是可以是其它距离,既然扯到了距离,下面就来具体阐述下都有哪些距离度量的表示法,权当扩展。

  • 1. 欧氏距离,最常见的两点之间或多点之间的距离表示法,又称之为欧几里得度量,它定义于欧几里得空间中,如点 x = (x1,...,xn) 和 y = (y1,...,yn) 之间的距离为:

(1)二维平面上两点a(x1,y1)与b(x2,y2)间的欧氏距离

(2)三维空间两点a(x1,y1,z1)与b(x2,y2,z2)间的欧氏距离:

(3)两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的欧氏距离:

  也可以用表示成向量运算的形式:

  • 2. 曼哈顿距离,我们可以定义曼哈顿距离的正式意义为L1-距离或城市区块距离,也就是在欧几里得空间的固定直角坐标系上两点所形成的线段对轴产生的投影的距离总和。例如在平面上,坐标(x1, y1)的点P1与坐标(x2, y2)的点P2的曼哈顿距离为:,要注意的是,曼哈顿距离依赖座标系统的转度,而非系统在座标轴上的平移或映射。 

     通俗来讲,想象你在曼哈顿要从一个十字路口开车到另外一个十字路口,驾驶距离是两点间的直线距离吗?显然不是,除非你能穿越大楼。而实际驾驶距离就是这个“曼哈顿距离”,此即曼哈顿距离名称的来源, 同时,曼哈顿距离也称为城市街区距离(City Block distance)。

(1)二维平面两点a(x1,y1)与b(x2,y2)间的曼哈顿距离 

(2)两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的曼哈顿距离 

                          

实现k近邻法时,主要考虑的问题是如何对训练数据进行快速k近邻搜索,这点在特征空间的维数大以及训练数据容量大时尤其重要。k近邻法的最简单实现是线性扫描,这时要计算输入实例与每一个训练实例的距离,当训练集很大时,计算非常耗时,这种方法是不可行的。为了提高k近邻搜索的效率,可以考虑使用特殊的结构存储训练数据,以减少计算距离的次数。具体方法有很多,这里介绍kd树方法。


第二部分、K近邻算法的实现:KD树


2.1、什么是KD树

    Kd-树是K-dimension tree的缩写,是对数据点在k维空间(如二维(x,y),三维(x,y,z),k维(x1,y,z..))中划分的一种数据结构,主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。本质上说,Kd-树就是一种平衡二叉树。

    首先必须搞清楚的是,k-d树是一种空间划分树,说白了,就是把整个空间划分为特定的几个部分,然后在特定空间的部分内进行相关搜索操作。想像一个三维(多维有点为难你的想象力了)空间,kd树按照一定的划分规则把这个三维空间划分了多个空间,如下图所示:

2.2、KD树的构建

    kd树构建的伪代码如下图所示:

Kd-树是一个二叉树,每个节点表示的是一个空间范围。下表表示的是Kd-树中每个节点中主要包含的数据结构。

Range域表示的是节点包含的空间范围。

Node-data域就是数据集中的某一个n维数据点。分割超面是通过数据点Node-Data并垂直于轴split的平面,分割超面将整个空间分割成两个子空间。

令split域的值为i,如果空间Range中某个数据点的第i维数据小于Node-Data[i],那么,它就属于该节点空间的左子空间,否则就属于右子空间。

Left,Right域分别表示由左子空间和右子空间空的数据点构成的Kd-树。

                             表1 k-d树中每个节点的数据类型
域名
数据类型
描述
Node-data
数据矢量
数据集中某个数据点,是n维矢量(这里也就是k维)
Range
空间矢量
该节点所代表的空间范围
split
整数
垂直于分割超平面的方向轴序号
Left
k-d树
由位于该节点分割超平面左子空间内所有数据点所构成的k-d树
Right
k-d树
由位于该节点分割超平面右子空间内所有数据点所构成的k-d树
parent
k-d树
父节点
从上面对k-d树节点的数据类型的描述可以看出构建k-d树是一个逐级展开的递归过程。下面是给出的是构建k-d树的伪码。

 

构建k-d树的算法实现
算法:构建k-d树(createKDTree)

输入:数据点集Data-set 和 其所在的空间Range 
输出:Kd,类型为k-d tree 


1、If Data-set为空,则返回空的k-d tree 


2、调用节点生成程序:
(1)确定split域:对于所有描述子数据(特征矢量),统计它们在每个维上的数据方差。以SURF特征为例,描述子为64维,可计算64个方差。挑选出最大值,对应的维就是split域的值。数据方差大表明沿该坐标轴方向上的数据分散得比较开,在这个方向上进行数据分割有较好的分辨率;
(2)确定Node-data域:数据点集Data-set按其第split域的值排序。位于正中间的那个数据点被选为Node-data。此时新的Data-set' = Data-set\Node-data(除去其中Node-data这一点)。 


3、dataleft = {d属于Data-set' && d[split] ≤ Node-data[split]}   Left_Range = {Range && dataleft}  

 把剩下的点分离,左边的是split坐标轴上的值比 Node-data[split]小的点

 dataright = {d属于Data-set' && d[split] > Node-data[split]}      Right_Range = {Range && dataright}  

 右边的是split坐标轴上的值比 Node-data[split]大的点


4.、left = 由(dataleft,Left_Range)建立的k-d tree,即递归调用createKDTree(dataleft,Left_Range)。并设置left的parent域为Kd;
right = 由(dataright,Right_Range)建立的k-d tree,即调用createKDTree(dataright,Right_Range)。并设置right的parent域为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树的具体步骤为:

  1. 确定:split域=x。具体是:6个数据点在x,y维度上的数据方差分别为39,28.63,所以在x轴上方差更大,故split域值为x;
  2. 确定:Node-data = (7,2)。具体是:根据x维上的值将数据排序,6个数据的中值(所谓中值,即中间大小的值)为7,所以Node-data域位数据点(7,2)。这样,该节点的分割超平面就是通过(7,2)并垂直于:split=x轴的直线x=7;
  3. 确定:左子空间和右子空间。具体是:分割超平面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库)

[cpp]  view plain copy
  1. /** a node in a k-d tree */  
  2. struct kd_node  
  3. {  
  4.     int ki;                      /**< partition key index *///关键点直方图方差最大向量系列位置  
  5.     double kv;                   /**< partition key value *///直方图方差最大向量系列中最中间模值  
  6.     int leaf;                    /**< 1 if node is a leaf, 0 otherwise */  
  7.     struct feature* features;    /**< features at this node */  
  8.     int n;                       /**< number of features */  
  9.     struct kd_node* kd_left;     /**< left child */  
  10.     struct kd_node* kd_right;    /**< right child */  
  11. };  

    也就是说,如之前所述,kd树中,kd代表k-dimension,每个节点即为一个k维的点。每个非叶节点可以想象为一个分割超平面,用垂直于坐标轴的超平面将空间分为两个部分,这样递归的从根节点不停的划分,直到没有实例为止。经典的构造k-d tree的规则如下:

  1. 随着树的深度增加,循环的选取坐标轴,作为分割超平面的法向量。对于3-d tree来说,根节点选取x轴,根节点的孩子选取y轴,根节点的孙子选取z轴,根节点的曾孙子选取x轴,这样循环下去。
  2. 每次均为所有对应实例的中位数的实例作为切分点,切分点作为父节点,左右两侧为划分的作为左右两子树。

    对于n个实例的k维数据来说,建立kd-tree的时间复杂度为O(k*n*logn)。

    以下是构建k-d树的代码:

[cpp]  view plain copy
  1. struct kd_node* kdtree_build( struct feature* features, int n )  
  2. {  
  3.     struct kd_node* kd_root;  
  4.   
  5.     if( ! features  ||  n <= 0 )  
  6.     {  
  7.         fprintf( stderr, "Warning: kdtree_build(): no features, %s, line %d\n",  
  8.             __FILE__, __LINE__ );  
  9.         return NULL;  
  10.     }  
  11.   
  12.     //初始化   
  13.     kd_root = kd_node_init( features, n );  //n--number of features,initinalize root of tree.   
  14.     expand_kd_node_subtree( kd_root );  //kd tree expand   
  15.   
  16.     return kd_root;  
  17. }  

    上面的涉及初始化操作的两个函数kd_node_init,及expand_kd_node_subtree代码分别如下所示

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static struct kd_node* kd_node_init( struct feature* features, int n )  
  2. {                                     //n--number of features  
  3.     struct kd_node* kd_node;  
  4.   
  5.     kd_node = (struct kd_node*)(malloc( sizeofstruct kd_node ) ));  
  6.     memset( kd_node, 0, sizeofstruct kd_node ) ); //0填充  
  7.     kd_node->ki = -1; //???????  
  8.     kd_node->features = features;  
  9.     kd_node->n = n;  
  10.   
  11.     return kd_node;  
  12. }  
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static void expand_kd_node_subtree( struct kd_node* kd_node )  
  2. {  
  3.     /* base case: leaf node */  
  4.     if( kd_node->n == 1  ||  kd_node->n == 0 )  
  5.     {   //叶节点               //伪叶节点  
  6.         kd_node->leaf = 1;  
  7.         return;  
  8.     }  
  9.   
  10.     assign_part_key( kd_node ); //get ki,kv  
  11.     partition_features( kd_node ); //creat left and right children,特征点ki位置左树比右树模值小,kv作为分界模值  
  12.                                  //kd_node中关键点已经排序,在ki维度上 比kd_node值小的 大的分开 并给kd_left kd_right赋值 为各自集合中的中位数
  13.     if( kd_node->kd_left )  
  14.         expand_kd_node_subtree( kd_node->kd_left );  
  15.     if( kd_node->kd_right )  
  16.         expand_kd_node_subtree( kd_node->kd_right );  
  17. }  

    构建完kd树之后,如今进行最近邻搜索呢?从下面的动态gif图中,你是否能看出些许端倪呢?


    k-d树算法可以分为两大部分,除了上部分有关k-d树本身这种数据结构建立的算法,另一部分是在建立的k-d树上各种诸如插入,删除,查找(最邻近查找)等操作涉及的算法。下面,咱们依次来看kd树的插入、删除、查找操作。


2.5、KD树的最近邻搜索算法

    现实生活中有许多问题需要在多维数据的快速分析和快速搜索,对于这个问题最常用的方法是所谓的kd树。在k-d树中进行数据的查找也是特征匹配的重要环节,其目的是检索在k-d树中与查询点距离最近的数据点。在一个N维的笛卡儿空间在两个点之间的距离是由下述公式确定:

2.5.1、k-d树查询算法的伪代码

    k-d树查询算法的伪代码如下所示:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. 算法:k-d树最邻近查找  
  2. 输入:Kd,    //k-d tree类型  
  3.      target  //查询数据点  
  4. 输出:nearest, //最邻近数据点  
  5.      dist      //最邻近数据点和查询点间的距离  
  6.   
  7. 1. If Kd为NULL,则设dist为infinite并返回  
  8. 2. //进行二叉查找,生成搜索路径  
  9.    Kd_point = &Kd;                   //Kd-point中保存k-d tree根节点地址  
  10.    nearest = Kd_point -> Node-data;  //初始化最近邻点  
  11.   
  12.    while(Kd_point)  
  13.      push(Kd_point)到search_path中; //search_path是一个堆栈结构,存储着搜索路径节点指针  
  14.   
  15.       If Dist(nearest,target) > Dist(Kd_point -> Node-data,target)  
  16.        nearest  = Kd_point -> Node-data;    //更新最近邻点  
  17.        Min_dist = Dist(Kd_point,target);  //更新最近邻点与查询点间的距离  ***/  
  18.      s = Kd_point -> split;                       //确定待分割的方向  
  19.   
  20.      If target[s] <= Kd_point -> Node-data[s]     //进行二叉查找  
  21.        Kd_point = Kd_point -> left;  
  22.      else  
  23.        Kd_point = Kd_point ->right;  
  24.    End while  
  25.   
  26. 3. //回溯查找  
  27.    while(search_path != NULL)  
  28.      back_point = 从search_path取出一个节点指针;   //从search_path堆栈弹栈  
  29.      s = back_point -> split;                      //确定分割方向  
  30.   
  31.      If Dist(target[s],back_point -> Node-data[s]) < Max_dist   //判断还需进入的子空间  
  32.        If target[s] <= back_point -> Node-data[s]  
  33.          Kd_point = back_point -> right;  //如果target位于左子空间,就应进入右子空间  
  34.        else  
  35.          Kd_point = back_point -> left;    //如果target位于右子空间,就应进入左子空间  
  36.        将Kd_point压入search_path堆栈;  
  37.   
  38.      If Dist(nearest,target) > Dist(Kd_Point -> Node-data,target)  
  39.        nearest  = Kd_point -> Node-data;                 //更新最近邻点  
  40.        Min_dist = Dist(Kd_point -> Node-data,target);  //更新最近邻点与查询点间的距离的  
  41.    End while   

    读者来信点评@yhxyhxyhx,在“将Kd_point压入search_path堆栈;”这行代码后,应该是调到步骤2再往下走二分搜索的逻辑一直到叶结点,我写了一个递归版本的二维kd tree的搜索函数你对比的看看:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void innerGetClosest(NODE* pNode, PT point, PT& res, int& nMinDis)  
  2. {  
  3.     if (NULL == pNode)  
  4.         return;  
  5.     int nCurDis = abs(point.x - pNode->pt.x) + abs(point.y - pNode->pt.y);  
  6.     if (nMinDis < 0 || nCurDis < nMinDis)  
  7.     {  
  8.         nMinDis = nCurDis;  
  9.         res = pNode->pt;  
  10.     }  
  11.     if (pNode->splitX && point.x <= pNode->pt.x || !pNode->splitX && point.y <= pNode->pt.y)  
  12.         innerGetClosest(pNode->pLft, point, res, nMinDis);  
  13.     else  
  14.         innerGetClosest(pNode->pRgt, point, res, nMinDis);  
  15.     int rang = pNode->splitX ? abs(point.x - pNode->pt.x) : abs(point.y - pNode->pt.y);  
  16.     if (rang > nMinDis)  
  17.         return;  
  18.     NODE* pGoInto = pNode->pLft;  
  19.     if (pNode->splitX && point.x > pNode->pt.x || !pNode->splitX && point.y > pNode->pt.y)  
  20.         pGoInto = pNode->pRgt;  
  21.     innerGetClosest(pGoInto, point, res, nMinDis);  
  22. }  

    下面,以两个简单的实例(例子来自图像局部不变特性特征与描述一书)来描述最邻近查找的基本思路。

2.5.2、举例:查询点(2.1,3.1)

    星号表示要查询的点(2.1,3.1)。通过二叉搜索,顺着搜索路径很快就能找到最邻近的近似点,也就是叶子节点(2,3)。而找到的叶子节点并不一定就是最邻近的,最邻近肯定距离查询点更近,应该位于以查询点为圆心且通过叶子节点的圆域内。为了找到真正的最近邻,还需要进行相关的‘回溯'操作。也就是说,算法首先沿搜索路径反向查找是否有距离查询点更近的数据点。

    以查询(2.1,3.1)为例:

  1. 二叉树搜索:先从(7,2)点开始进行二叉查找,然后到达(5,4),最后到达(2,3),此时搜索路径中的节点为<(7,2),(5,4),(2,3)>,首先以(2,3)作为当前最近邻点,计算其到查询点(2.1,3.1)的距离为0.1414,
  2. 回溯查找:在得到(2,3)为查询点的最近点之后,回溯到其父节点(5,4),并判断在该父节点的其他子节点空间中是否有距离查询点更近的数据点。以(2.1,3.1)为圆心,以0.1414为半径画圆,如下图所示。发现该圆并不和超平面y = 4交割,因此不用进入(5,4)节点右子空间中(图中灰色区域)去搜索;
  3. 最后,再回溯到(7,2),以(2.1,3.1)为圆心,以0.1414为半径的圆更不会与x = 7超平面交割,因此不用进入(7,2)右子空间进行查找。至此,搜索路径中的节点已经全部回溯完,结束整个搜索,返回最近邻点(2,3),最近距离为0.1414。


2.5.3、举例:查询点(2,4.5)

    一个复杂点了例子如查找点为(2,4.5),具体步骤依次如下:

  1. 同样先进行二叉查找,先从(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. 以(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;
  3. 回溯查找至(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算法,与球树、M树、VP树、MVP树),参考http://blog.csdn.net/v_JULY_v/article/details/8203674

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用 JavaScript 编写的记忆游戏(附源代码)   项目:JavaScript 记忆游戏(附源代码) 记忆检查游戏是一个使用 HTML5、CSS 和 JavaScript 开发的简单项目。这个游戏是关于测试你的短期 记忆技能。玩这个游戏 时,一系列图像会出现在一个盒子形状的区域中 。玩家必须找到两个相同的图像并单击它们以使它们消失。 如何运行游戏? 记忆游戏项目仅包含 HTML、CSS 和 JavaScript。谈到此游戏的功能,用户必须单击两个相同的图像才能使它们消失。 击卡片或按下键盘键,通过 2 乘 2 旋转来重建鸟儿对,并发现隐藏在下面的图像! 如果翻开的牌面相同(一对),您就赢了,并且该对牌将从游戏中消失! 否则,卡片会自动翻面朝下,您需要重新尝试! 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox, 以获得更好、更优化的游戏体验。要玩游戏,首先,通过单击 memorygame-index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值