《機器學習實戰》學習筆記——第2章 KNN


一. 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維中,枚舉計算,時間復雜度:O[D N^2]
  • 隨着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非常近,不用確切的計算出他們的距離
  • 計算復雜度: O[D N \log(N)]
  • 對參數空間沿着數據軸(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定義的超球內。通過使用三角不等式來減少鄰居搜索的候選點數量的

|x+y| \leq |x| + |y|

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 O[D N]
  • Ball tree query time grows as approximately O[D \log(N)]
  • KD tree query time changes with D in a way that is difficult to precisely characterise. For small D (less than 20 or so) the cost is approximately O[D\log(N)], and the KD tree query can be very efficient. For larger D, the cost increases to nearly O[DN], 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。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM