kdtree&knn

前言:本文關於kdtree的知識基本來源於kdtree_wiki

一、what's kdtree

        kdtree是 k-dimensional tree的縮寫,它是一種用於組織k維空間中數據點的基於空間劃分的數據結構。kdtree常用於搜索多維搜索詞,包括區間搜索和最近鄰搜索。kdtree是一種二叉樹結構,它是BSP(Binary space partationing)樹的一種特殊情況。【注:BSP的空間劃分的超平面是任意的,而kdtree中是的劃分超平面是垂直座標軸的。】


[ from wikipedia ]A 3-dimensional k-d tree. The first split (the red vertical plane) cuts the root cell (white) into two subcells, each of which is then split (by the green horizontal planes) into two subcells. Finally, those four cells are split (by the four blue vertical planes) into two subcells. Since there is no more splitting, the final eight are called leaf cells.

二、operations on kdtree

1.建樹

        因爲存在很多種選擇軸對齊( axis-aligned )劃分平面的方法,所以存在很多種不同的構建kdtree的方法。

        規範的構建方法有如下限制: 

        1)隨着移動到樹的底端,我們不斷地循環座標axes來劃分空間;

        2)通過選擇子樹中點的中位數,來插入點。

        僞代碼如下:

function kdtree (list of points pointList, int depth)
{
    // Select axis based on depth so that axis cycles through all valid values
    var int axis := depth mod k;
        
    // Sort point list and choose median as pivot element
    select median by axis from pointList;
        
    // Create node and construct subtrees
    var tree_node node;
    node.location := median;
    node.leftChild := kdtree(points in pointList before median, depth+1);
    node.rightChild := kdtree(points in pointList after median, depth+1);
    return node;
}

         對那些在median上的點,我們其實也可以使其屬於分裂後的某個子空間,比如分裂得到的子集定義爲“小於”和“大於等於”。       

         該方法構建了一個平衡kdtree(通過選擇中位數),但是平衡樹並不是對所有應用都是最優的。

         並且,這種方法需要尋找中位數(每次O(N)複雜度),或者是對所有點進行堆排序或歸併排序(複雜度O( nlogn ))。一個常用的實踐方法是隨機選取一定的點,對它們進行排序,並從中選擇中位數。這種方法在實踐中經常生成優雅的平衡樹。

        可以先預排序(presort)點,然後構建kdtree,來減少每次尋找中位數的花銷。

2.添加元素

        添加元素的方法就是按照樹的規律不斷向下找到該插入的位置。插入元素要注意的是,它可能會影響樹的平衡性。

3.移除元素


4.平衡

       因爲kdtree是在多維度上排序的,所以樹旋轉的平衡方法並不適用。

5.最近鄰搜索

       最近鄰搜索意在樹中搜索得到與給定點最近的點。最近鄰搜索可以kdtree樹高效地實現,因爲kdtree可以快速地移除不滿足搜索條件的搜索空間。

       1)從根節點開始,遞歸地往下移動;

       2)一旦算法移動到葉子節點,那麼將該節點作爲“當前最佳”;

       3)算法展開樹的遞歸,在每個節點上執行如下步驟:

              a.如果當前點更近,那麼將其作爲當前最佳;

              b.算法檢查分裂平面的另一邊,看是否有比當前最近的點。概念上,這是通過檢查以目標點爲球心,目標點到當前最近點的距離爲半徑的超球面來實現的,該超球面可以橫貫子空間。

                            如果該超球面穿過某分裂平面,那麼可能在平面另一邊有更近的點,那麼算法必須從該點的另一分支向下搜索。

                            如果不想交,那麼算法繼續沿樹向上,該點的另一個分支被忽略。

        舉一個栗子:樣本集{(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)},建樹如下:

image

我們來查找點(2.1,3.1),在(7,2)點測試到達(5,4),在(5,4)點測試到達(2,3),然後search_path中的結點爲<(7,2), (5,4), (2,3)>,從search_path中取出(2,3)作爲當前最佳結點nearest, dist爲0.141;

然後回溯至(5,4),以(2.1,3.1)爲圓心,以dist=0.141爲半徑畫一個圓,並不和超平面y=4相交,如下圖,所以不必跳到結點(5,4)的右子空間去搜索,因爲右子空間中不可能有更近樣本點了。

image

於是在回溯至(7,2),同理,以(2.1,3.1)爲圓心,以dist=0.141爲半徑畫一個圓並不和超平面x=7相交,所以也不用跳到結點(7,2)的右子空間去搜索

至此,搜索結束,返回最近點(2,3)。

注意:若查找的target節點與當前節點的axis軸相交,則需查找target節點的child,注意這裏查找child節點需要遞歸調用該查找方法,而不是簡單地將其child節點添加到查找棧中。爲什麼呢?

我們首先搜索到“最近的”葉子節點,然後往上回溯,這時候上面這個點axis上的軸如果與目標節點相交的話,則要搜索上面這個點的另一半子空間。這個子空間還得用這個搜索函數去遞歸的搜索,爲什麼不是簡單的添加那個節點的child呢?原因就在於向上回溯有這個性質,向下回溯可沒有啊。所以還是得對這個子空間用一樣的方法去搜索。如圖:



後記:看到這裏會發現,爲了搜索那個超球體內可能的更近鄰,需要大量的回溯,這會大大影響搜索性能。因此研究人員有提出改進的kdtree近鄰搜索,其中一個比較著名的就是Best-Bin-First,它提供設置優先級隊列和運行超時限定來獲取近似的最近鄰,有效減少回溯的次數。這個我也沒研究過,有時間看看~


6.區間搜索



三、用kdtree實現KNN

1. scipy.spatial.KDTree

        scipy實現了kdtree,用起來很方便。只需要用訓練數據建一個kdtree,然後用kdtree的query函數找最近鄰,然後投票即可。代碼如下:

"""
@ knn  lazy learning
@ kdtree for k-nearest-neighbor searching
@ wttttt at 2016.12.12
"""

import numpy as np
import pandas as pd
from scipy.spatial import KDTree
import sys

# step1: reading data
def load_data():
    train = pd.read_csv('train.csv')
    test = pd.read_csv('test.csv')
    y = train.iloc[:, -1]
    train = train.drop(labels= train.columns[-1], axis=1)
    return train, y, test

train, y, test = load_data()
train = train[:, 1:]  # removing id
test_id = test[:, 0]
test = test[, 1:]   # removing id

# step2: constructig kdtree for training data
tree = KDTree(train)

#find the k nearest neighbor
if len(sys.argv) <= 1:
    print 'please implement arguments for knn\'s k.'
dis, nearest_loc = tree.query(x=test, k= sys.argv[1], p=2) # p=2 means Euclidean distance

# vote for prediction
y_test = []   # storing the y of testing data
for i in range(nearest_loc.shape[0]):
    print 'predicting for test id {0}'.format(test_id[i])
    classCounter = {}  # vote
    for pos in nearest_loc[i]:
        classCounter[y[pos]] = classCounter.get(y[pos], 0) + 1
    y_test.append(sorted(classCounter)[0])
    print 'predicted: y is {0}'.format(y_test[-1])
print 'all prediction is done, writing...'
with open('result_knn_kdtree.csv') as fi:
    for i in range(len(y_test)):
        fi.write(('{0},{1}\n').format(test_id[i], y_test[i]))

2.自己動手寫kdtree

        1)首先要建樹

                建樹這裏考慮的還是用上面提到的“循環+中位數”的方法。中位數的 查找並沒有用上預排序的方法(還沒想明白- -)。僞代碼如下:

assignment on this point 
if no more than one sample to split:   # stop condition 
    return
axis = iter_num mod n_dim   # the axis chosen to split
find median in this axis
iteration on left child     # iteration
iteration on right child

       2)然後實現k近鄰搜索的方法

                最近鄰的實現方法是先找到“最近的”葉子節點,然後不斷向上回溯,如果當前點的距離小於當前最近距離,則替換當前最近距離。接着判斷相交,若相交則遞歸搜索當前點的子空間(這裏要判斷要搜索的這個子空間是其左or右孩子空間。實現上通過存儲上次一搜索的節點來判斷即可),否則不搜索其子空間,而是繼續向上回溯。
                而k近鄰的話是存儲當前“最近”的k個節點,每次的新節點跟當前最遠的“最近”距離比較,若小於,則替換最遠距離對應的點。是否搜索子空間就判斷axis軸上的相交性即可,若相交則搜索子空間,否則繼續向上回溯。這就涉及到一個問題,每次我們都要進行最大距離的搜索,所以這裏實現一個大根堆來優化算法。
create a iter_list to store searching path
find the 'nearest' leaf
create a large heap to store current 'nearest' neighbors
for point on iter_list(backtracking upside):
    if heap.len < k:
        add this point to heap
    elif dis(point,target)<current_max_dis:
        heap.pop()
        add this point to heap
    if not intersect:
        continue
    recursion, search subspace
 

       3)代碼實現:

參見github,https://github.com/wttttt-wang/ml_algo_realization,這上面是實現了上述的所有算法。


另外,附上用線性方法實現的knn的代碼:

"""
@ knn  lazy learning
@ two ways: general searching & kdtree searching
@ wttttt at 2016.12.07
"""

import numpy as np
import pandas as pd


def load_data():
    # read training data as numpy.array, attention that containing id & y
    # train = numpy.loadtxt(open('train.csv','rb'), delimiter=',', skiprows=1)
    # test = numpy.loadtxt(open('test.csv','rb'), delimiter=',', skiprows=1 ) # containing id
    train = pd.read_csv('train.csv')
    test = pd.read_csv('test.csv')
    y = train.iloc[:, -1]
    train = train.drop(labels= train.columns[-1], axis=1)
    return np.array(train), np.array(y), np.array(test)

# square distance
def do_classify(k=10):
    train, y, test = load_data()
    num_instance, num_cols = train.shape
    train = train[:, 1:]  # removing id
    y_test = []   # storing the y of testing data
    for test_one in test:  # for each testing instances
        print 'predicting for test id{0}'.format(test_one[0])
        test_one = test_one[1:]   # removing id
        # compute the diff of the test instance of each train instance
        diff = train - np.tile(test_one, (num_instance, 1))
        squre_diff = np.square(diff)
        distance = np.sum(squre_diff, axis=1)**0.5   # the square distance
        topk_index = np.argsort(distance)
        classCounter = {}
        for i in range(k):
            classCounter[y[topk_index[i]]] = classCounter.get(y[topk_index[i]],0) + 1
        y_test.append(sorted(classCounter)[0])
        print 'predicted: y is {0}'.format(y_test[-1])
    print 'all prediction is done, writing...'
    with open('result_knn.csv', 'w') as fi:
        for i in range(len(y_test)):
            fi.write(('{0},{1}\n').format(test[i, 0], y_test[i]))


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章