1. 程式人生 > >從K近鄰演算法、距離度量談到KD樹、SIFT+BBF演算法

從K近鄰演算法、距離度量談到KD樹、SIFT+BBF演算法

本文各部分內容分佈如下:

  1. 第一部分講K近鄰演算法,其中重點闡述了相關的距離度量表示法,
  2. 第二部分著重講K近鄰演算法的實現--KD樹,和KD樹的插入,刪除,最近鄰查詢等操作,及KD樹的一系列相關改進(包括BBF,M樹等);
  3. 第三部分講KD樹的應用:SIFT+kd_BBF搜尋演算法。

    同時,你將看到,K近鄰演算法同本系列的前兩篇文章所講的決策樹分類貝葉斯分類,及支援向量機SVM一樣,也是用於解決分類問題的演算法,

  

    而本資料探勘十大算法系列也會按照分類,聚類,關聯分析,預測迴歸等問題依次展開闡述。

    OK,行文倉促,本文若有任何漏洞,問題或者錯誤,歡迎朋友們隨時不吝指正,各位的批評也是我繼續寫下去的動力之一。感謝。

第一部分、K近鄰演算法

1.1、什麼是K近鄰演算法

    何謂K近鄰演算法,即K-Nearest Neighbor algorithm,簡稱KNN演算法,單從名字來猜想,可以簡單粗暴的認為是:K個最近的鄰居,當K=1時,演算法便成了最近鄰演算法,即尋找最近的那個鄰居。為何要找鄰居?打個比方來說,假設你來到一個陌生的村莊,現在你要找到與你有著相似特徵的人群融入他們,所謂入夥。

    用官方的話來說,所謂K近鄰演算法,即是給定一個訓練資料集,對新的輸入例項,在訓練資料集中找到與該例項最鄰近的K個例項(也就是上面所說的K個鄰居),這K個例項的多數屬於某個類,就把該輸入例項分類到這個類中。根據這個說法,咱們來看下引自維基百科上的一幅圖:

    如上圖所示,有兩類不同的樣本資料,分別用藍色的小正方形和紅色的小三角形表示,而圖正中間的那個綠色的圓所標示的資料則是待分類的資料。也就是說,現在,我們不知道中間那個綠色的資料是從屬於哪一類(藍色小正方形or紅色小三角形),下面,我們就要解決這個問題:給這個綠色的圓分類。
    我們常說,物以類聚,人以群分,判別一個人是一個什麼樣品質特徵的人,常常可以從他/她身邊的朋友入手,所謂觀其友,而識其人。我們不是要判別上圖中那個綠色的圓是屬於哪一類資料麼,好說,從它的鄰居下手。但一次性看多少個鄰居呢?從上圖中,你還能看到:

  • 如果K=3,綠色圓點的最近的3個鄰居是2個紅色小三角形和1個藍色小正方形,少數從屬於多數,基於統計的方法,判定綠色的這個待分類點屬於紅色的三角形一類。
  • 如果K=5,綠色圓點的最近的5個鄰居是2個紅色三角形和3個藍色的正方形,還是少數從屬於多數,基於統計的方法,判定綠色的這個待分類點屬於藍色的正方形一類。

    於此我們看到,當無法判定當前待分類點是從屬於已知分類中的哪一類時,我們可以依據統計學的理論看它所處的位置特徵,衡量它周圍鄰居的權重,而把它歸為(或分配)到權重更大的那一類。這就是K近鄰演算法的核心思想。

1.2、近鄰的距離度量表示法

    上文第一節,我們看到,K近鄰演算法的核心在於找到例項點的鄰居,這個時候,問題就接踵而至了,如何找到鄰居,鄰居的判定標準是什麼,用什麼來度量。這一系列問題便是下面要講的距離度量表示法。但有的讀者可能就有疑問了,我是要找鄰居,找相似性,怎麼又跟距離扯上關係了?

    這是因為特徵空間中兩個例項點的距離可以反應出兩個例項點之間的相似性程度。K近鄰模型的特徵空間一般是n維實數向量空間,使用的距離可以使歐式距離,也是可以是其它距離,既然扯到了距離,下面就來具體闡述下都有哪些距離度量的表示法,權當擴充套件。

第二部分、K近鄰演算法的實現:KD樹

2.0、背景

    之前blog內曾經介紹過SIFT特徵匹配演算法,特徵點匹配和資料庫查、影象檢索本質上是同一個問題,都可以歸結為一個通過距離函式在高維向量之間進行相似性檢索的問題,如何快速而準確地找到查詢點的近鄰,不少人提出了很多高維空間索引結構和近似查詢的演算法。

    一般說來,索引結構中相似性查詢有兩種基本的方式:

  1. 一種是範圍查詢,範圍查詢時給定查詢點和查詢距離閾值,從資料集中查詢所有與查詢點距離小於閾值的資料
  2. 另一種是K近鄰查詢,就是給定查詢點及正整數K,從資料集中找到距離查詢點最近的K個數據,當K=1時,它就是最近鄰查詢。

    同樣,針對特徵點匹配也有兩種方法:

  • 最容易的辦法就是線性掃描,也就是我們常說的窮舉搜尋,依次計算樣本集E中每個樣本到輸入例項點的距離,然後抽取出計算出來的最小距離的點即為最近鄰點。此種辦法簡單直白,但當樣本集或訓練集很大時,它的缺點就立馬暴露出來了,舉個例子,在物體識別的問題中,可能有數千個甚至數萬個SIFT特徵點,而去一一計算這成千上萬的特徵點與輸入例項點的距離,明顯是不足取的。
  • 另外一種,就是構建資料索引,因為實際資料一般都會呈現簇狀的聚類形態,因此我們想到建立資料索引,然後再進行快速匹配。索引樹是一種樹結構索引方法,其基本思想是對搜尋空間進行層次劃分。根據劃分的空間是否有混疊可以分為Clipping和Overlapping兩種。前者劃分空間沒有重疊,其代表就是k-d樹;後者劃分空間相互有交疊,其代表為R樹。

    1975年,來自斯坦福大學的Jon Louis Bentley在ACM雜誌上發表的一篇論文:Multidimensional Binary Search Trees Used for Associative Searching 中正式提出和闡述的瞭如下圖形式的把空間劃分為多個部分的k-d樹。

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樹構建的虛擬碼如下圖所示:

    再舉一個簡單直觀的例項來介紹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庫)

  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. };  
  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樹的程式碼:

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

    上面的涉及初始化操作的兩個函式kd_node_init,及expand_kd_node_subtree程式碼分別如下所示:

  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.     kd_node = (struct kd_node*)(malloc( sizeofstruct kd_node ) ));  
  5.     memset( kd_node, 0, sizeofstruct kd_node ) ); //0填充   
  6.     kd_node->ki = -1; //???????   
  7.     kd_node->features = features;  
  8.     kd_node->n = n;  
  9.     return kd_node;  
  10. }  
  1. staticstruct kd_node* kd_node_init( struct feature* features, int n )  
  2. {                                     //n--number of features
  3.     struct kd_node* kd_node;  
  4.     kd_node = (struct kd_node*)(malloc( sizeofstruct kd_node ) ));  
  5.     memset( kd_node, 0, sizeofstruct kd_node ) ); //0填充
  6.     kd_node->ki = -1;