1. 程式人生 > >kd 樹演算法之思路篇

kd 樹演算法之思路篇

【量化課堂】kd 樹演算法之思路篇

JoinQuant量化課堂

釋出於 2016-09-01

13581

17

35

導語:kd 樹是一種二叉樹資料結構,可以用來進行高效的 kNN 計算。kd 樹演算法偏於複雜,本篇將先介紹以二叉樹的形式來記錄和索引空間的思路,以便讀者更輕鬆地理解 kd 樹。

作者:肖睿
編輯:巨集觀經濟算命師

本文由JoinQuant量化課堂退出,本文的難度屬於進階(上),深度為level-1。

閱讀本文之前請掌握 kNN(level-1)的知識。

前言

kd 樹(k-dimensional tree)是一個包含空間資訊的二項樹資料結構,它是用來計算 kNN 的一個非常常用的工具。如果特徵的維度是 DD,樣本的數量是 NN,那麼一般來講 kd 樹演算法的複雜度是 O(DlogN)O(Dlog⁡N),相比於窮算的 O(DN)O(DN) 省去了非常多的計算量。

因為 kd 樹的概念和演算法較為複雜,固將本教程分為“思路篇”和“詳細篇”。兩篇的內容在一定程度上是重疊的,但是本篇注重於講解 kd 樹背後的思想和直覺,告訴讀者一顆二項樹是如何承載空間概念的,我們又該如何從樹中讀取這些資訊;而之後的詳細篇則詳細講解 kd 樹的定義,如何構造它並且如何計算 kNN。出於教學起見,本文講的例子和演算法與嚴格的 kd 樹有一些差異。有演算法經驗或者想嘗試挑戰的讀者可以直接跳過本篇去讀詳細篇

關於在學習程式設計和演算法時有沒有必要自己製作輪子的問題,一直存在著很多的爭議。作者認為,做不做輪子暫且不論,但是有必要去了解輪子是怎麼做出來的。Python 的 scikit-learn 機器學習包提供了蠻算、kd 樹和 ball 樹三種 kNN 演算法,學完本篇的讀者若無興趣自撰演算法,可以非常輕鬆地使用該包,詳細可見 

scikit-learn 之 kNN 分類

直覺

給定一堆已有的樣本資料,和一個被詢問的資料點(紅色五角星),我們如何找到離五角星最近的15個點?
1.png

先忽略在程式設計上的實現,想一想一個人如何主觀地執行。嗯,他一定會把在五角附近的一些點中,分別計算每一個的距離,然後選最近的15個。這樣可能只需要進行二三十次距離計算,而不是300次。
2.png

如圖,只對紫圈裡的點進行計算。

啊哈!問題來了。我們講到的“附近”已經包含了距離的概念,如果不經過計算我們怎麼知道哪個點是在五角星的“附近”?為什麼我們一下就認出了“附近”而計算機做不到?那是因為我們在觀看這張圖片時,得到的輸入是已經帶有距離概念的影像,然而計算機在進行計算時得到的則是沒有距離概念的座標資料。如果要讓一個人人為地從300組座標裡選出最近的15個,而不給他影象,那麼他也省不了功夫,必須要把300個全部計算一遍才行。

這樣來說,我們要做的就是在乾巴巴的座標資料上進行加工,將空間分割成小塊,並以合理地方法將資訊進行儲存,這樣方便我們讀取“附近”的點。

切割

這隻危險的兔子,它又回來了!它今天上了四個紋身,愛心、月牙、星星和眼淚,下面是它的照片。
3.jpg

我們來回答一個簡單的問題:在這幅照片上,距離愛心最近的紋身是什麼?記得上一篇文章中,我們選用的特徵是每一隻兔子的身高和體重;這次就不一樣了,在這個問題中,每個紋身的特徵是照片平面上的橫軸和豎軸的座標。

對於這個問題,如果進行蠻算的辦法我們需要計算 3 次距離(分別和月亮、眼淚和星星算一次)。下面我們要做的是把整個空間按照左右和上下進行等分,並且把分割後的小空間以二叉樹形式進行記錄,這樣可以很快地讀取鄰近的點而省去計算量。

好,我們先豎向沿中間把這個兔子切成兩半
4.jpg

再沿橫向從中間切成四份
5.jpg

再沿著豎向平分八份
6.jpg

最後再沿橫向切一次。這次有些區域是完全空白的,我們就把它捨棄不要了,得到 14 份:
7.jpg

我們再按照上下左右的關係把切開的圖片做成一個二叉樹,樹的每一個節點是一幅圖,它的兩個枝是這幅圖平分出來的子圖。
8.jpg

可以看出這個樹狀結構包含了很多區域性性的資訊,因為它的每一個節點都是按照上下或者左右進行平分的,因此如果兩個點在樹中的距離較近,那麼它們的實際距離就是比較近的。

搜尋

接下來我們要通過這棵二叉樹找到離愛心最近的紋身。

首先從樹的最頂端開始,向下搜尋找到最底部包含愛心的節點。這個操作非常簡單,因為每一次分割要麼是沿著某縱線 x=ax=a 要麼是沿著橫線 y=ay=a,因此只需要判斷愛心的 xx 或 yy 軸座標是大於 aa 還是小於 aa,便知道是向左還是右邊選擇樹枝。
9.jpg

在找到了愛心之後,我們沿著相同的路徑向上攀爬。只爬了一節就發現了屁股上的兩個紋身
10.png

這裡看出,在8平分的情況下,愛心和月亮是在同一個區域的。在某種意義上來講它們是“近”的,但是我們還不能確定它們是最近的,因此還要繼續向上攀爬尋找。再繼續向上爬兩個節點,都沒有出現愛心和月亮以外的紋身。在下面這個節點中
11.png

我們發現愛心和月亮之間的距離(紅線)要小於愛心和分割線的距離(藍線),也就是說,不論分割線的右邊是什麼情況,那邊的紋身都不可能離愛心更近。因此可以判斷,離愛心最近的圖形是月亮。

這樣,我們只計算了一次愛心和月亮之間的距離和一次愛心和分割線之間的距離,而不是分別計算愛心和其他三個紋身的距離。並且,要知道,愛心和分割線之間距離的計算非常簡單,就是愛心的 xx 座標和分割線的 xx 座標的差(的絕對值),相比於計算兩點之間的距離

((x1−y1)2+(x2−y2)2)−−−−−−−−−−−−−−−−−−−−√((x1−y1)2+(x2−y2)2)


要省下很多計算量。

 

麻煩

啊,但也有可能這個搜尋最近點的過程沒那麼順利。在上面的計算中,在找到了離愛心比較近的月亮之後,我們發現愛心距離分割線的距離比較遠,因此確定月亮的確就是最近的。但是,在分割線的另一邊有一個更近的紋身,那麼情況就稍微複雜了。

就說這個兔子啊,又去加了兩個紋身,一片葉子和一個圓圈。
12.png

二叉樹分割上也相應地多出這兩個紋身。我們想找到離愛心最近的紋身,所以依舊向下搜尋先找到愛心。
13.jpg

我們找來一張紙,記下在已訪問節點中距離愛心最近的紋身和所對應的距離。現在這張紙還是空的。
14.png

向上爬了一節,發現那一節的另一個枝裡有月亮,於是跑下去檢視月亮的座標,計算愛心和月亮的距離,並在紙上記錄 (圖形=月亮,距離=d1)(圖形=月亮,距離=d1)。

再回到藍圈的節點向上爬,繼續向上爬。我們發現,d1d1(紅線)大於愛心和分割線的距離(藍線)。
new1.png

也就是說分割線的另一邊可能有更近的點,所以從另一個分枝開始向下搜,找到…
16.png

在另一個分枝中我們追溯到圓圈,並計算它與愛心的距離 d2d2,發現 d2>d1d2>d1,比月亮遠,所以丟棄不要。

再向上爬一個節,我們發現 d1d1(紅線)大於愛心和切分線之間的距離(藍線)
new2.png

因此,切分線的另一端可能有更近的紋身,因此我們從另一個樹枝向下搜尋…
18.png

找到了葉子。(所幸在這個分枝裡只搜尋到了葉子,如果有更多的圖形的話,還需要進行多層的遞迴。具體的過程會在後面的詳細篇中講解。)計算葉子和愛心之間的距離,得 d3d3,並發現 d3<d1d3<d1,比月亮更近,於是更新紙上的記錄為 (紋身=葉子,距離=d3)(紋身=葉子,距離=d3)。

再向上攀登一節,我們發現 d3d3 小於愛心和切分線的距離,因此另一邊的資料就不用考慮了。
new3.png

這次我們已經爬到了樹的最頂端,完成了搜尋,紙上記載的 (葉子,d3)(葉子,d3) 就是最近的紋身和對應的距離。
20.png

結語

在以上的演算法中,當我們已經找到了比切分線更近的點時,就不需要繼續搜尋切分線另一邊的點了,因為那些只會更遠。於是,通過把整個空間進行分割並以樹狀結構進行記錄,我們只需要在問題點附近的一些區域進行搜尋便可以找到最近的資料點,節省了大量的計算。

到此為止,本篇文章友好地介紹瞭如何使用二叉樹的形式記錄距離資訊並快速地進行搜尋,但文中所講的還不是 kd 樹。下一篇文章,kd 樹演算法之詳細篇,將系統性地介紹 kd 樹的定義和在 kd 樹上的 kNN 演算法。