一. KNN原理:
1. 有監督的學習
根據已知事例及其類標,對新的實例按照離他最近的K的鄰居中出現頻率最高的類別進行分類。偽代碼如下:
1)計算已知類別數據集中的點與當前點之間的距離
2)按照距離從小到大排序
3)選取與當前點距離最小的k個點
4)確定這k個點所在類別的出現頻率
5)返回這K個點出現頻率最高的類別作為當前點的預測分類
1 import numpy as np 2 3 # 讀取數據 4 def file2matrix(filename): 5 fr = open(filename) 6 arrayLines = fr.readlines() 7 numbersOfArray = len(arrayLines) 8 9 df = np.zeros((numbersOfArray, 3)) 10 classLabel = [] 11 index = 0 12 13 for line in arrayLines: 14 # 截取掉所有的回車字符 15 line = line.strip() 16 # 使用\t將上一步得到的整行數據切分成一個元素列表 17 listFromLine = line.split('\t') 18 df[index] = listFromLine[0:3] 19 classLabel.append(listFromLine[-1]) 20 index +=1 21 22 return df, classLabel 23 filename = '\datingTestSet.txt' 24 df, classLabel = file2matrix(filename)
2. KNN缺點:
(1)KNN算法是基於實例的學習,使用算法時我們必須有接近實際數據的訓練樣本數據。KNN必須保存全部數據集,如果訓練數據集的很大,必須使用大量的存儲空間。
(2)必須對數據集中的每個數據計算距離值,耗時非常大
(3)它無法給出任何數據的基礎結構信息, 因此我們無法知曉平均實例樣本和典型事例樣本具有什么特征
二. 使用sklearn.neighbors
1 from sklearn import neighbors 2 import numpy as np 3 4 x = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]]) 5 y = [1, 1] 6 nbr = neighbors.NearestNeighbors(n_neighbors=2, algorithm='auto').fit(x) 7 distance, indices = nbr.kneighbors(y)
結果:
最近鄰算法主要分為三種:
(1)Brute Force
- 對於N個實例在D維中,枚舉計算,時間復雜度:
- 隨着N增加,時間消耗相當大
(2)K-D tree
1. 什么叫做K_Dtree:
K:K鄰近查詢中的k;D:空間是D維空間(Demension)tree:二叉樹
2. 建樹過程:
K-D tree的建立就是分裂空間的過程
- 首先我們來定義樹節點的狀態:
分裂點(split_point)
分裂方式(split_method)
左兒子(left_son)
右兒子(right_son)
- 建樹依據:
先計算當前區間 [ L , R ] 中(這里的區間是點的序號區間,而不是我們實際上的坐標區間),每個點的坐標的每一維度上的方差,取方差最大的那一維,設為 d,作為我們的分裂方式(split_method ),把區間中的點按照在 d 上的大小,從小到大排序,取中間的點 sorted_mid 作為當前節點記錄的分裂點,然后,再以 [ L , sorted_mid-1 ] 為左子樹建樹 , 以 [sorted_mid+1 , R ] 為右子樹建樹,這樣,當前節點的所有狀態我們便確定下來了:
split_point= sorted_mid
split_method= d
left_son = [ L , sorted_mid-1 ]
right_son = [ sorted_mid+1 , R ]
- 舉例:
假設現在我們有平面上的點集 E ,其中有 5 個二維平面上的點 : (1,4)(5,8) (4,2) (7,9) (10,11)
首先,我們對整個區間 [1 , 15] 建樹:先計算區間中所有點在第一維(也就是 x 坐標)上的方差:
平均值 : ave_1 =5.4
方差 : varance_1 =9.04
再計算區間中所有點在第二維(也就是 y 坐標)上的方差:
平均值:ave_2 =6.8
方差:varance_2 =10.96
明顯看見,varance_2 > varance_1 ,那么我們在本次建樹中,分裂方式 :split_method =2 , 再將所有的點按照第2維的大小從小到大排序,得到了新的點的一個排列:
(4,2) (1,4)(5,8) (7,9) (10,11)
取中間的點作為分裂點 sorted_mid =(5,8)作為根節點,再把區間 [1 , 2] 建成左子樹 , [4 , 5] 建成右子樹,此時,直線 : y = 8 將平面分裂成了兩半,前面一半給左兒子,后面一半給了右兒子,如圖:
建左子樹 [1, 3] 的時候可以發現,這時候第一維的方差大 ,分裂方式就是1 ,把區間 [ 1, 2 ] 中的點按照 第一維 的大小,從小到大排序 ,取中間點(1,4) 根節點,再以區間 [ 2, 2] 建立右子樹 得到節點 (4,2)
建右子樹 [4 , 5] 的時候可以發現,這時還是第一維的方差大, 於是,我們便得到了這樣的一顆二叉樹 也就是 K-D tree,它把平面分成了如下的小平面,使得每個小平面中最多有一個點:
3. 查詢過程:
查詢,其實相當於我們要將一個點“添加”到已經建好的 K-D tree 中,但並不是真的添加進去,只是找到他應該處於的子空間即可,所以查詢就顯得簡單的。
每次在一個區間中查詢的時候,先看這個區間的分裂方式是什么,也就是說,先看這個區間是按照哪一維來分裂的,這樣如果這個點對應的那一維上面的值比根節點的小,就在根節點的左子樹上進行查詢操作,如果是大的話,就在右子樹上進查詢操作。
每次回溯到了根節點(也就是說,對他的一個子樹的查找已經完成了)的時候,判斷一下,以該點為圓心,目前找到的最小距離為半徑,看是否和分裂區間的那一維所構成的平面相交,要是相交的話,最近點可能還在另一個子樹上,所以還要再查詢另一個子樹,同時,還要看能否用根節點到該點的距離來更新我們的最近距離。為什么是這樣的,我們可以用一幅圖來說明:
在查詢到左兒子的時候,我們發現,現在最小的距離是 r = 10 ,當回溯到父親節點的時候,我們發現,以目標點(10,1)為圓心,現在的最小距離 r = 10 為半徑做圓,與分割平面 y = 8 相交,這時候,如果我們不在父親節點的右兒子進行一次查找的話,就會漏掉(10,9) 這個點,實際上,這個點才是距離目標點 (10,1) 最近的點
由於每次查詢的時候可能會把左右兩邊的子樹都查詢完,所以,查詢並不是簡單的 log(n) 的,最壞的時候能夠達到 sqrt(n)
4. 在對樣本實例進行KD-tree算法
KD(K-dimensional tree)樹形結構。KD tree是二叉樹
- 原理:如果我們知道A與B非常近,B與C非常近,我們就假定認為A與C非常近,不用確切的計算出他們的距離
- 計算復雜度:
- 對參數空間沿着數據軸(N)進行划分,KDtree 很高效,因為划分過程是在參數軸(N)上進行,而不用管D實例維數
- 但是只有當D很小(D<20)的時候,運算塊,當D大時,計算也會變慢
- 對於上面代碼,只需修改,algorithm='KDTree'
1 nbr = neighbors.NearestNeighbors(n_neighbors=2, algorithm='KDTree').fit(x)
(3)Ball tree
1. 原理:
為了改進KDtree的二叉樹樹形結構,並且沿着笛卡爾坐標進行划分的低效率,ball tree將在一系列嵌套的超球體上分割數據。也就是說:使用超球面而不是超矩形划分區域。雖然在構建數據結構的花費上大過於KDtree,但是在高維甚至很高維的數據上都表現的很高效。
球樹遞歸地將數據划分為由質心C和半徑r定義的節點,使得節點中的每個點都位於由r和C定義的超球內。通過使用三角不等式來減少鄰居搜索的候選點數量的
2. 划分
選擇一個距離當前圓心最遠的觀測點i1,和距離i1最遠的觀測點 i2,將圓中所有離這兩個點最近的觀測點都賦給這兩個簇的中心,然后計算每一個簇的中心點和包含所有其所屬觀測點的最小半徑。對包含n個觀測點的超圓進行分割,只需要線性的時間。
3. 查詢
使用ball tree時,先自上而下找到包含target的葉子結點(c, r),從此結點中找到離它最近的觀測點。這個距離就是最近鄰的距離的上界。檢查它的兄弟結點中是否包含比這個上界更小的觀測點。方法是:如果目標點距離兄弟結點的圓心的距離 > 兄弟節點所在的圓半徑 + 前面的上界的值,則這個兄弟結點不可能包含所要的觀測點。否則,檢查這個兄弟結點是否包含符合條件的觀測點。
如果:該點的最近距離上確界+兄弟節點的半徑 > 該點到兄弟節點圓心的距離,則表明構成了三角形,兩個圓必然相交
4. 在對樣本實例進行ball tree算法
- 對於上面代碼,只需修改,algorithm='ball_tree'
nbr = neighbors.NearestNeighbors(n_neighbors=2, algorithm='ball_tree').fit(x)
(4)選擇何種算法
1. 各算法時間復雜度:N:樣本數量,D:特征向量
- Brute force query time grows as
- Ball tree query time grows as approximately
- KD tree query time changes with
in a way that is difficult to precisely characterise. For small
(less than 20 or so) the cost is approximately
, and the KD tree query can be very efficient. For larger
, the cost increases to nearly
, and the overhead due to the tree structure can lead to queries which are slower than brute force.
對於小數據集(N小於30左右), log(N)可以與N比較,並且強力算法可以比基於樹的方法更有效。KDTree和BallTree都通過提供葉大小參數來解決這個問題:這控制了查詢切換到暴力的樣本數。 這允許兩個算法接近小於N的強力計算的效率。
2. 數據結構:
數據的內在維度或數據的稀疏性。 內在維度是指數據所在的D,其可以線性地或非線性地嵌入參數空間中。 稀疏性指的是數據填充參數空間的程度(這與“稀疏”矩陣中使用的概念區分開來。數據矩陣可以沒有零項,但是結構在這個項中仍然可以是“稀疏的” )。
- 強制查詢時間根據數據結構不變。
- ball tree和KD樹查詢時間會受到數據結構的很大影響。 一般來說,具有較小固有維度的稀疏數據導致更快的查詢時間。 由於KD樹內部表示與參數軸對齊,因此對於任意結構化數據,它通常不會顯示與ball tree相同的改進。
機器學習中使用的數據集往往是非常結構化的,非常適合基於樹的查詢。
3. 可以自動選擇:
nbr = neighbors.NearestNeighbors(n_neighbors=2, algorithm='auto').fit(x)
目前,如果k <N / 2並且'effective_metric_'在'kd_tree'的'VALID_METRICS'列表中,algorithm ='auto'選擇'kd_tree'。 如果k <N / 2並且'effective_metric_'不在'kd_tree'的'VALID_METRICS'列表中,它選擇'ball_tree'。 如果k> = N / 2,則選擇“brute”。 該選擇基於如下假設:查詢點的數量至少與訓練點的數量相同,並且該leaf_size接近其默認值30。