k-d樹(k-dimensional樹的簡稱),是一種分割k維數據空間的數據結構。主要應用於多維空間關鍵數據的搜索(如:範圍搜索和最近鄰搜索)。css
應用背景html
SIFT算法中作特徵點匹配的時候就會利用到k-d樹。而特徵點匹配實際上就是一個經過距離函數在高維矢量之間進行類似性檢索的問題。針對如何快速而準確地找到查詢點的近鄰,如今提出了不少高維空間索引結構和近似查詢的算法,k-d樹就是其中一種。算法
索引結構中類似性查詢有兩種基本的方式:一種是範圍查詢(range searches),另外一種是K近鄰查詢(K-neighbor searches)。範圍查詢就是給定查詢點和查詢距離的閾值,從數據集中找出全部與查詢點距離小於閾值的數據;K近鄰查詢是給定查詢點及正整數K,從數據集中找到距離查詢點最近的K個數據,當K=1時,就是最近鄰查詢(nearest neighbor searches)。數據結構
特徵匹配算子大體能夠分爲兩類。一類是線性掃描法,即將數據集中的點與查詢點逐一進行距離比較,也就是窮舉,缺點很明顯,就是沒有利用數據集自己蘊含的任何結構信息,搜索效率較低,第二類是創建數據索引,而後再進行快速匹配。由於實際數據通常都會呈現出簇狀的聚類形態,經過設計有效的索引結構能夠大大加快檢索的速度。索引樹屬於第二類,其基本思想就是對搜索空間進行層次劃分。根據劃分的空間是否有混疊能夠分爲Clipping和Overlapping兩種。前者劃分空間沒有重疊,其表明就是k-d樹;後者劃分空間相互有交疊,其表明爲R樹。(這裏只介紹k-d樹)app
實例函數
先以一個簡單直觀的實例來介紹k-d樹算法。假設有6個二維數據點{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},數據點位於二維空間內(如圖1中黑點所示)。k-d樹算法就是要肯定圖1中這些分割空間的分割線(多維空間即爲分割平面,通常爲超平面)。下面就要經過一步步展現k-d樹是如何肯定這些分割線的。post
圖1 二維數據k-d樹空間劃分示意圖性能
k-d樹算法能夠分爲兩大部分,一部分是有關k-d樹自己這種數據結構創建的算法,另外一部分是在創建的k-d樹上如何進行最鄰近查找的算法。學習
k-d樹構建算法url
k-d樹是一個二叉樹,每一個節點表示一個空間範圍。表1給出的是k-d樹每一個節點中主要包含的數據結構。
表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樹是一個逐級展開的遞歸過程。表2給出的是構建k-d樹的僞碼。
表2 構建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}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。 |
以上述舉的實例來看,過程以下:
因爲此例簡單,數據維度只有2維,因此能夠簡單地給x,y兩個方向軸編號爲0,1,也即split={0,1}。
(1)肯定split域的首先該取的值。分別計算x,y方向上數據的方差得知x方向上的方差最大,因此split域值首先取0,也就是x軸方向;
(2)肯定Node-data的域值。根據x軸方向的值2,5,9,4,8,7排序選出中值爲7,因此Node-data = (7,2)。這樣,該節點的分割超平面就是經過(7,2)並垂直於split = 0(x軸)的直線x = 7;
(3)肯定左子空間和右子空間。分割超平面x = 7將整個空間分爲兩部分,如圖2所示。x < = 7的部分爲左子空間,包含3個節點{(2,3),(5,4),(4,7)};另外一部分爲右子空間,包含2個節點{(9,6),(8,1)}。
圖2 x=7將整個空間分爲兩部分
如算法所述,k-d樹的構建是一個遞歸的過程。而後對左子空間和右子空間內的數據重複根節點的過程就能夠獲得下一級子節點(5,4)和(9,6)(也就是左右子空間的'根'節點),同時將空間和數據集進一步細分。如此反覆直到空間中只包含一個數據點,如圖1所示。最後生成的k-d樹如圖3所示。
圖3 上述實例生成的k-d樹
注意:每一級節點旁邊的'x'和'y'表示以該節點分割左右子空間時split所取的值。
k-d樹上的最鄰近查找算法
在k-d樹中進行數據的查找也是特徵匹配的重要環節,其目的是檢索在k-d樹中與查詢點距離最近的數據點。這裏先以一個簡單的實例來描述最鄰近查找的基本思路。
星號表示要查詢的點(2.1,3.1)。經過二叉搜索,順着搜索路徑很快就能找到最鄰近的近似點,也就是葉子節點(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爲半徑畫圓,如圖4所示。發現該圓並不和超平面y = 4交割,所以不用進入(5,4)節點右子空間中去搜索。
圖4 查找(2.1,3.1)點的兩次回溯判斷
再回溯到(7,2),以(2.1,3.1)爲圓心,以0.1414爲半徑的圓更不會與x = 7超平面交割,所以不用進入(7,2)右子空間進行查找。至此,搜索路徑中的節點已經所有回溯完,結束整個搜索,返回最近鄰點(2,3),最近距離爲0.1414。
一個複雜點了例子如查找點爲(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。以(2,4.5)爲圓心,以3.041爲半徑做圓,如圖5所示。可見該圓和y = 4超平面交割,因此須要進入(5,4)左子空間進行查找。此時需將(2,3)節點加入搜索路徑中得<(7,2),(2,3)>。回溯至(2,3)葉子節點,(2,3)距離(2,4.5)比(5,4)要近,因此最近鄰點更新爲(2,3),最近距離更新爲1.5。回溯至(7,2),以(2,4.5)爲圓心1.5爲半徑做圓,並不和x = 7分割超平面交割,如圖6所示。至此,搜索路徑回溯完。返回最近鄰點(2,3),最近距離1.5。k-d樹查詢算法的僞代碼如表3所示。
圖5 查找(2,4.5)點的第一次回溯判斷
圖6 查找(2,4.5)點的第二次回溯判斷
表3 標準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)。
後記
以上爲了介紹方便,討論的是二維情形。像實際的應用中,如SIFT特徵矢量128維,SURF特徵矢量64維,維度都比較大,直接利用k-d樹快速檢索(維數不超過20)的性能急劇降低。假設數據集的維數爲D,通常來講要求數據的規模N知足N»2D,才能達到高效的搜索。因此這就引出了一系列對k-d樹算法的改進。有待進一步研究學習。
參考
1.《圖像局部不變特性特徵與描述》王永明 王貴錦 編著 國防工業出版社
2.http://underthehood.blog.51cto.com/2531780/687160
轉載請註明:http://www.cnblogs.com/eyeszjwang/articles/2429382.html