kd 樹演算法之詳細篇
【數學】kd 樹演算法之詳細篇
山巔一寺,山巔二寺
124 人讚了該文章
期末考完試啦,趁著過年有時間,搬磚搬磚!
導語:在上一篇《kd 樹演算法之思路篇》中,我們介紹瞭如何用二叉樹格式記錄空間內的距離,並以其為依據進行高效的索引。在本篇文章中,我們將詳細介紹 kd 樹的構造以及 kd 樹上的 kNN 演算法。
作者:肖睿
編輯:巨集觀經濟算命師
本文由JoinQuant量化課堂推出,本文的難度屬於進階(下),深度為 level-1
閱讀本文前請掌握 kNN(level-1)的知識。
kd 樹的結構
kd樹是一個二叉樹結構,它的每一個節點記載了【特徵座標,切分軸,指向左枝的指標,指向右枝的指標】。
其中,特徵座標是線性空間中的一個點。
切分軸由一個整數表示,這裡,是我們在 n 維空間中沿第 r 維進行一次分割。
節點的左枝和右枝分別都是 kd 樹,並且滿足:如果 y 是左枝的一個特徵座標,那麼並且如果 z 是右枝的一個特徵座標,那麼。
給定一個數據樣本集 和切分軸 r , 以下遞迴演算法將構建一個基於該資料集的 kd 樹,每一次迴圈制作一個節點:
−− 如果 ,記錄 S 中唯一的一個點為當前節點的特徵資料,並且不設左枝和右枝。(指集合 S 中元素的數量)
−− 如果 :
∙∙ 將 S 內所有點按照第 r 個座標的大小進行排序;
∙∙ 選出該排列後的中位元素(如果一共有偶數個元素,則選擇中位左邊或右邊的元素,左或右並無影響),作為當前節點的特徵座標,並且記錄切分軸 r;
∙∙ 將
∙∙ 當前節點的左枝設為以 為資料集並且 r 為切分軸製作出的 kd 樹;當前節點的右枝設為以 為資料集並且 r 為切分軸製作出的 kd 樹。再設 。(這裡,我們想輪流沿著每一個維度進行分割; 是因為一共有 n 個維度,在沿著最後一個維度進行分割之後再重新回到第一個維度。)
構造 kd 樹的例子
上面抽象的定義和演算法確實是很不好理解,舉一個例子會清楚很多。首先隨機在 中隨機生成 13 個點作為我們的資料集。起始的切分軸
首先先沿 x 座標進行切分,我們選出 x 座標的中位點,獲取最根部節點的座標
並且按照該點的x座標將空間進行切分,所有 x 座標小於 6.27 的資料用於構建左枝,x座標大於 6.27 的點用於構建右枝。
在下一步中 對應 y 軸,左右兩邊再按照 y 軸的排序進行切分,中位點記載於左右枝的節點。得到下面的樹,左邊的 x 是指這該層的節點都是沿 x 軸進行分割的。
空間的切分如下
下一步中 r≡1+1≡0 mod 2,對應 x 軸,所以下面再按照 x 座標進行排序和切分,有
最後每一部分都只剩一個點,將他們記在最底部的節點中。因為不再有未被記錄的點,所以不再進行切分。
就此完成了 kd 樹的構造。
kd 樹上的 kNN 演算法
給定一個構建於一個樣本集的 kd 樹,下面的演算法可以尋找距離某個點 p 最近的 k 個樣本。
零、設 L 為一個有 k 個空位的列表,用於儲存已搜尋到的最近點。
一、根據 p 的座標值和每個節點的切分向下搜尋(也就是說,如果樹的節點是照 進行切分,並且 p 的 r 座標小於 a,則向左枝進行搜尋;反之則走右枝)。
二、當達到一個底部節點時,將其標記為訪問過。如果 L 裡不足 k 個點,則將當前節點的特徵座標加入 L ;如果 L 不為空並且當前節點的特徵與 p 的距離小於 L 裡最長的距離,則用當前特徵替換掉 L 中離 p 最遠的點。
三、如果當前節點不是整棵樹最頂端節點,執行 (a);反之,輸出 L,演算法完成。
a. 向上爬一個節點。如果當前(向上爬之後的)節點未曾被訪問過,將其標記為被訪問過,然後執行 (1) 和 (2);如果當前節點被訪問過,再次執行 (a)。
1. 如果此時 L 裡不足 kk 個點,則將節點特徵加入 L;如果 L 中已滿 k 個點,且當前節點與 p 的距離小於 L 裡最長的距離,則用節點特徵替換掉 L 中離最遠的點。
2. 計算 p 和當前節點切分線的距離。如果該距離大於等於 L 中距離 p 最遠的距離並且 L 中已有 k 個點,則在切分線另一邊不會有更近的點,執行 (三);如果該距離小於 L 中最遠的距離或者 L 中不足 k 個點,則切分線另一邊可能有更近的點,因此在當前節點的另一個枝從 (一) 開始執行。
啊呃… 被這演算法噎住了,趕緊喝一口下面的例子
設我們想查詢的點為 p=(−1,−5),設距離函式是普通的 距離,我們想找距離問題點最近的 k=3 個點。如下:
首先執行 (一),我們按照切分找到最底部節點。首先,我們在頂部開始
和這個節點的 x 軸比較一下,
p 的 x 軸更小。因此我們向左枝進行搜尋:
這次對比 y 軸,
p 的 y 值更小,因此向左枝進行搜尋:
這個節點只有一個子枝,就不需要對比了。由此找到了最底部的節點 (−4.6,−10.55)。
在二維圖上是
此時我們執行 (二)。將當前結點標記為訪問過,並記錄下 L=[(−4.6,−10.55)]。啊,訪問過的節點就在二叉樹上顯示為被劃掉的好了。
然後執行 (三),嗯,不是最頂端節點。好,執行 (a),我爬。上面的是 (−6.88,−5.4)。
執行 (1),因為我們記錄下的點只有一個,小於 k=3,所以也將當前節點記錄下,有 L=[(−4.6,−10.55),(−6.88,−5.4)].再執行 (2),因為當前節點的左枝是空的,所以直接跳過,回到步驟 (三)。(三) 看了一眼,好,不是頂部,交給你了,(a)。於是乎 (a) 又往上爬了一節。
(1) 說,由於還是不夠三個點,於是將當前點也記錄下,有 L=[(−4.6,−10.55),(−6.88,−5.4),(1.24,−2.86)]。當然,當前結點變為被訪問過的。
(2) 又發現,當前節點有其他的分枝,並且經計算得出 p 點和 L 中的三個點的距離分別是 6.62,5.89,3.10,但是 p 和當前節點的分割線的距離只有 2.14,小於與 L 的最大距離:
因此,在分割線的另一端可能有更近的點。於是我們在當前結點的另一個分枝從頭執行 (一)。好,我們在紅線這裡:
要用 p 和這個節點比較 x 座標:
p 的 x 座標更大,因此探索右枝 (1.75,12.26),並且發現右枝已經是最底部節點,因此啟動 (二)。
經計算,(1.75,12.26) 與 p 的距離是 17.48,要大於 p 與 L 的距離,因此我們不將其放入記錄中。
然後 (三) 判斷出不是頂端節點,撥出 (a),爬。
(1) 出來一算,這個節點與 p 的距離是 4.91,要小於 p 與 L 的最大距離 6.62。
因此,我們用這個新的節點替代 L 中離 p 最遠的 (−4.6,−10.55)。
然後 (2) 又來了,我們比對 p 和當前節點的分割線的距離
這個距離小於 L 與 p 的最小距離,因此我們要到當前節點的另一個枝執行 (一)。當然,那個枝只有一個點,直接到 (二)。
計算距離發現這個點離 p 比 L 更遠,因此不進行替代。
(三) 發現不是頂點,所以撥出 (a)。我們向上爬,
這個是已經訪問過的了,所以再來(a),
好,(a)再爬,
啊!到頂點了。所以完了嗎?當然不,還沒輪到 (三) 呢。現在是 (1) 的回合。
我們進行計算比對發現頂端節點與p的距離比L還要更遠,因此不進行更新。
然後是 (2),計算 p 和分割線的距離發現也是更遠。
因此也不需要檢查另一個分枝。
然後執行 (三),判斷當前節點是頂點,因此計算完成!輸出距離 p 最近的三個樣本是 L=[(−6.88,−5.4),(1.24,−2.86),(−2.96,−2.5)]。
結語
kd 樹的 kNN 演算法節約了很大的計算量(雖然這點在少量資料上很難體現),但在理解上偏於複雜,希望本篇中的例項可以讓讀者清晰地理解這個演算法。喜歡動手的讀者可以嘗試自己用程式碼實現 kd 樹演算法,但也可以用現成的機器學習包 scikit-learn 來進行計算。量化課堂的下一篇文章就將講解如何用 scikit-learn 進行 kNN 分類。