轉自 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的量級,甚至更高。在表示圖像可視化特徵的高維向量中各維信息的重要程度是不一樣的,經過降維技術去除屬於次要信息的特徵向量以及相關性較強的特徵向量,從而下降特徵空間的維數,這種方法已經獲得了一些實際應用。
然而這種方法存在不足之處採用降維技術可能會致使有效信息的損失,尤爲不適合於處理特徵空間中的特徵向量相關性很小的狀況。另外主流的多維索引結構大都針對歐氏空間,設計須要利用到歐氏空間的幾何性質,而圖像的類似性計算極可能不限於基於歐氏距離。這種狀況下人們愈來愈關注基於距離的度量空間高維索引結構能夠直接應用於高維向量類似性查詢問題。