Kd-樹概念
Kd-樹 其實是K-dimension tree的縮寫,是對數據點在k維空間中划分的一種數據結構。其實,Kd-樹是一種平衡二叉樹。
舉一示例:
假設有六個二維數據點 = {(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},數據點位於二維空間中。為了能有效的找到最近鄰,Kd-樹采用分而治之的思想,即將整個空間划分為幾個小部分。六個二維數據點生成的Kd-樹的圖為:
2D 對應的kd的平面划分 3D 對應的kd的平面划分
k-d樹算法可以分為兩大部分
一部分是有關k-d樹本身這種數據結構建立的算法,
另一部分是在建立的k-d樹上如何進行最鄰近查找的算法。
Kd-樹的構建
Kd-樹是一個二叉樹,每個節點表示的是一個空間范圍。下表表示的是Kd-樹中每個節點中主要包含的數據結構。
Range域表示的是節點包含的空間范圍。
Node-data域就是數據集中的某一個n維數據點。分割超面是通過數據點Node-Data並垂直於軸split的平面,分割超面將整個空間分割成兩個子空間。
令split域的值為i,如果空間Range中某個數據點的第i維數據小於Node-Data[i],那么,它就屬於該節點空間的左子空間,否則就屬於右子空間。
Left,Right域分別表示由左子空間和右子空間空的數據點構成的Kd-樹。
表1 k-d樹中每個節點的數據類型
note:splits記錄着裂變維度
構建KD樹
算法:構建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}dataright = {d屬於Data-set' && d[split] > Node-data[split]} Right_Range = {Range && dataright} |
4.left = 由(dataleft,Left_Range)建立的k-d tree,即遞歸調用createKDTree(dataleft,Left_ Range)。並設置left的parent域為Kd; right = 由(dataright,Right_Range)建立的k-d tree,即調用createKDTree(dataleft,Left_ Range)。並設置right的parent域為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 為左子空間,包含節點(2,3),(5,4),(4,7),另一部分為右子空間。包含節點(9,6),(8,1)
這個構建過程是一個遞歸過程。重復上述過程,直至只包含一個節點。
k-d樹的構建是一個遞歸的過程。然后對左子空間和右子空間內的數據重復根節點的過程就可以得到下一級子節點(5,4)和(9,6)(也就是左右子空間的'根'節點),
同時將空間和數據集進一步細分。如此反復直到空間中只包含一個數據點,如圖1所示。最后生成的k-d樹如圖2所示。
k-d樹上的最鄰近查找算法
KD樹的查找算法:
在k-d樹中進行數據的查找也是特征匹配的重要環節,其目的是檢索在k-d樹中與查詢點距離最近的數據點。
這里先以一個簡單的實例來描述最鄰近查找的基本思路。
例一:查詢的點(2.1,3.1)(較簡單)。
1、如圖3所示,星號表示要查詢的點(2.1,3.1)。通過二叉搜索,順着搜索路徑很快就能找到最鄰近的近似點,也就是葉子節點(2,3)。
2、而找到的葉子節點並不一定就是最鄰近的,最鄰近肯定距離查詢點更近,應該位於以查詢點為圓心且通過葉子節點的圓域內。
3、為了找到真正的最近鄰,還需要進行'回溯'操作:
算法沿搜索路徑反向查找是否有距離查詢點更近的數據點。
此例中先從(7,2)點開始進行二叉查找,然后到達(5,4),最后到達(2,3),此時搜索路徑中的節點為<(7,2),(5,4),(2,3)>,
首先以(2,3)作為當前最近鄰點,計算其到查詢點(2.1,3.1)的距離為0.1414,
然后回溯到其父節點(5,4),並判斷在該父節點的其他子節點空間中是否有距離查詢點更近的數據點。以(2.1,3.1)為圓心,以0.1414為半徑畫圓,如圖3所示。發現該圓並不和超平面y = 4交割,因此不用進入(5,4)節點右子空間中去搜索。
4、最后,再回溯到(7,2),以(2.1,3.1)為圓心,以0.1414為半徑的圓更不會與x = 7超平面交割,因此不用進入(7,2)右子空間進行查找。至此,搜索路徑中的節點已經全部回溯完,結束整個搜索,返回最近鄰點(2,3),最近距離為0.1414。
例一:查詢的點(2,4.5)。
1、同樣先進行二叉查找,先從(7,2)查找到(5,4)節點,在進行查找時是由y = 4為分割超平面的,由於查找點為y值為4.5,因此進入右子空間查找到(4,7),形成搜索路徑<(7,2),(5,4),(4,7)>,
2、取(4,7)為當前最近鄰點,計算其與目標查找點的距離為3.202。然后回溯到(5,4),計算其與查找點之間的距離為3.041。
((4,7)與目標查找點的距離為3.202,而(5,4)與查找點之間的距離為3.041,所以(5,4)為查詢點的最近點;)
3、以(2,4.5)為圓心,以3.041為半徑作圓,如圖4所示。可見該圓和y = 4超平面交割,所以需要進入(5,4)左子空間進行查找。此時需將(2,3)節點加入搜索路徑中得<(7,2),(2,3)>。
4、回溯至(2,3)葉子節點,(2,3)距離(2,4.5)比(5,4)要近,所以最近鄰點更新為(2,3),最近距離更新為1.5。
5、回溯至(7,2),以(2,4.5)為圓心1.5為半徑作圓,並不和x = 7分割超平面交割,如圖5所示。
至此,搜索路徑回溯完。返回最近鄰點(2,3),最近距離1.5。
標准k-d樹查詢算法
算法:k-d樹最鄰近查找 |
輸入:Kd, //k-d tree類型 target //查詢數據點 |
輸出:nearest, //最鄰近數據點 dist //最鄰近數據點和查詢點間的距離 |
1. If Kd為NULL,則設dist為infinite並返回 |
2. //進行二叉查找,生成搜索路徑 Kd_point = &Kd; //Kd-point中保存k-d tree根節點地址 nearest = Kd_point -> Node-data; //初始化最近鄰點 while(Kd_point) push(Kd_point)到search_path中; //search_path是一個堆棧結構,存儲着搜索路徑節點指針 /*** If Dist(nearest,target) > Dist(Kd_point -> Node-data,target) nearest = Kd_point -> Node-data; //更新最近鄰點 Max_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; nearest = search_path中最后一個葉子節點; //注意:二叉搜索時不比計算選擇搜索路徑中的最鄰近點,這部分已被注釋 Max_dist = Dist(nearest,target); //直接取最后葉子節點作為回溯前的初始最近鄰點 |
3. //回溯查找 while(search_path != NULL) back_point = 從search_path取出一個節點指針; //從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; //如果target位於左子空間,就應進入右子空間 else Kd_point = back_point -> left; //如果target位於右子空間,就應進入左子空間 將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); //更新最近鄰點與查詢點間的距離 |
上述兩次實例表明,當查詢點的鄰域與分割超平面兩側空間交割時,需要查找另一側子空間,導致檢索過程復雜,效率下降。研究表明N個節點的K維k-d樹搜索過程時間復雜度為:tworst=O(kN1-1/k)。
總結:
從root節點開始,DFS搜索直到葉子節點,同時在stack中順序存儲已經訪問的節點。
如果搜索到葉子節點,當前的葉子節點被設為最近鄰節點。
然后通過stack回溯:
如果當前點的距離比最近鄰點距離近,更新最近鄰節點.
然后檢查以最近距離為半徑的圓是否和父節點的超平面相交.
如果相交,則必須到父節點的另外一側,用同樣的DFS搜索法,開始檢查最近鄰節點。
如果不相交,則繼續往上回溯,而父節點的另一側子節點都被淘汰,不再考慮的范圍中.
當搜索回到root節點時,搜索完成,得到最近鄰節點。