K 近鄰算法
聲明:
1,本篇為個人對《2012.李航.統計學習方法.pdf》的學習總結,不得用作商用,歡迎轉載,但請註明出處(即:本帖地址)。
2,因為本人在學習初始時有非常多數學知識都已忘記,所以為了弄懂當中的內容查閱了非常多資料。所以裏面應該會有引用其它帖子的小部分內容,假設原作者看到能夠私信我,我會將您的帖子的地址付到以下。
3。假設有內容錯誤或不準確歡迎大家指正。
4。假設能幫到你。那真是太好了。
描寫敘述
給定一個訓練數據集,對新的輸入實例。在訓練數據集中找到與該實例最鄰近的K個實例,若這K個實例的多數屬於某個類。就把輸入實例分入這個類。
K值得選擇
K值得選擇會對K近鄰算法的結果產生重大影響:
若K值較小:
預測結果會對近鄰實例點十分敏感,若近鄰點恰巧為噪聲,那預測就會出錯。
若K值較大:
長處是可降低預計誤差,但近似誤差會增大,由於與輸入實例較遠(不相似的)訓練實例會起作用。
若K = N:
那不管輸入什麽。都將預為測訓練實例中最多的那個類,不可取。
在應用中,K一般取一個較小的值,通常採用交叉驗證法
KD樹
Kd樹是存儲K維空間數據的樹結構。
PS:這裏的K和K近鄰算法的K意義不同。
1,KD樹構造方法的描寫敘述:
a, 構造根節點,使根節點相應用於K維空間中包括全部實例點的超矩形區域。
b, 通過以下的遞歸方法,不斷切分K維空間,生成子節點:
B.1,在超矩形區域上選擇一個坐標軸和該坐標上中位數作為該坐標軸的切分點。
B.2,以經過該點且垂直於該坐標軸做一個超平面。該超平面將當前的超矩形區域切分成左右兩個子區域(此時:2個子區域相應2個子節點,其父節點就是剛才的切分點)
c,該過程直到子區域內無實例時終止(終止時的節點為子節點)
在上述過程中將實例集合保存在對應的節點上。
2。樣例:構造KD樹
描寫敘述
輸入:
K維空間數據集T = {x1,x2, …, xn},當中,xi = (xi(1), xi(2),…, xi(k)),i = 1, 2, …, n
輸出:
KD樹
解:
1。 構造根節點(根節點相應於包括T的K維空間的超矩形區域)
選擇x(1)為坐標軸。以T中全部實例的x(1)坐標的中位數為切分點,這樣。經過該切分點且垂直與x(1)的超平面就將超矩形區域切分成2個子區域。
而該切分點就是根節點。
2, 反復例如以下步驟直到兩個子區域無實例時停止:
對深度為J的節點選擇x(l)為切分的坐標軸,l = j(modk) + 1,以該節點區域中全部實例的x(l)坐標的中位數為切分點。將該節點相應的超平面切分成兩個子區域。
而該切分點就是節點。
樣例
題目:構造T={(2, 3),(5, 4), (9, 6), (4, 7), (8, 1), (7, 2)}的平衡KD樹.
解:
1。 在X軸上選X坐標是全部點中位數的點:(7,2)
用經過(7, 2)且垂直於X軸的超平面(圖中的①)將矩形區域切分成兩部分,這時(7, 2)即為根節點
2。 由於上一步是在X軸上選中位數,所以(7,2)把T分成了兩部分:
左節點:(2, 3) (4, 7) (5, 4)
右節點:(8, 1) (9, 6)
而上一步既然在X軸上選中位數。那這一步就以Y軸為標準在剛才的節點上選中位數。
於是左節點選取(5, 4)為中位數。那麽就做經過(5,4)且垂直於Y軸的超平面②。
同理右節點選取(9, 6)為中位數,然後就做經過(9,6)且垂直於Y軸的超平面②。
這時(5, 4)和(9, 6)就是(7, 2)的左右節點:
(7,2)
/ \
(5, 4) (9, 6)
3, 同理,循環回X軸,以X軸為準為(5, 4)和(9, 6)的左右節點選取中位數。並將其作為(5, 4)和(9, 6)的子節點
4。 循環上面的步驟,直至無實例。(當然。對於本例在第三步時就有結果了)
5, 最後KD樹例如以下:
(7,2)
/ \
(5, 4) (9, 6)
/ \ /
(2, 3)(4, 7)(8, 1)
使用KD樹—用KD樹做近鄰搜索
描寫敘述
輸入:
已知的KD樹。目標點X
輸出:
X的近期鄰
解:
1, 在KD樹中找出包括X的葉子節點
從根節點出發,遞歸向下訪問KD樹,若X小於節點坐標,則移動到左子節點。反之移動到右子節點,直到節點為葉子節點。
2。 以此葉子節點為“當前近期鄰點”。
3, 遞歸向上回退,在每一個節點上做下面操作:
A) 若該節點比剛才的“當前近期鄰點”距離X更近,則更新此節點為“當前近期鄰點”
B) “當前近期鄰點”一定存在於該節點的一個子節點的相應區域。
於是檢查該子節點的父節點的另外一個節點相應的區域,看是否有更近的點。
即:
檢查父節點的還有一個節點相應的區域中是否與“以X為球心,以X與‘當前近期鄰點’的距離為半徑的超球體”相交。
C) 若相交:
可能在還有一個子節點相應的區域內存在距離X更近的點,於是移動到還有一個子節點,並遞歸的進行近期鄰查找。
若不想交:
向上回退。
D) 當回退到根節點時,查找結束。最後的“當前近期鄰點”就是X的近期鄰點。
樣例:
對以下的KD樹,求S點的近期鄰
解:
1。 找到包括S的葉子節點D,以D作為“當前近期鄰點”。
以S為圓心,S到D的距離為半徑畫圓。
2。 返回父節點B,在B的還有一個子節點F的區域內做近期鄰查找,而F的區域與圓不想交。所以不可能有近期鄰點。
3, 繼續返回上一級父節點A,在A的還有一子節點C的區域內查找,發現該區域與圓相交。
4。 在該區域內遍歷點,發現E點在圓內(比S到D更近)。
5。 更新E為“當前近期鄰點”。
6。 反復上述過程直至返回根節點。
7, 終於得到點E為近期鄰點。
時間復雜度
若實例點隨機分布,則KD樹搜索的時間復雜度為O(logN)。N為訓練實例數。
K近鄰算法更適用於
KD樹更適用於訓練實例數遠大於空間維度的K近鄰搜索。
若空間維數接近訓練實例數時,它的效率會迅速下降,差點兒接近線性掃描。
代碼演示樣例:
<pre name="code" class="python">#-*-coding:utf-8-*- # LANG=en_US.UTF-8 # k 近鄰算法 # 文件名稱:k_nearest_neighbour.py import sys import math list_T = [ ( 2, 3 ), ( 5, 4 ), ( 9, 6 ), ( 4, 7 ), ( 8, 1 ), ( 7, 2 ), ] # 二叉樹結點 class BinaryTreeNode( object ): def __init__( self, data=None, left=None, right=None, father=None ): self.data = data self.left = left self.right = left self.father = father # 二叉樹遍歷 class BTree(object): def __init__(self,root=0): self.root = root # 中序遍歷 def inOrder(self,treenode): if treenode is None: return self.inOrder(treenode.left) print treenode.data self.inOrder(treenode.right) # 高速排序算法 # 1,取當前元素集的第一個元素為 key。i = 0,j = len(當前元素集) # 2,j-- 直到找到小於 key 的元素,然後 L[i] 與 L[j] 交換 # 3,i++ 直到找到大於 key 的元素,然後 L[i] 與 L[j] 交換 # 4,當 i == j 時停止 # 5。L[i] = key # 此時當前元素集被第 i 個元素分成了左右兩部分,左邊的都比 key 小。右邊的都比 key 大 # 6。對左右兩部分反復上面 5 步直到再無切割 def quick_sort( T, left, right, rank): tmp_i = left tmp_j = right if left >= right: return key = T[left][rank]; key_item = T[left] while tmp_i != tmp_j: while tmp_i < tmp_j and T[tmp_j][rank] > key: tmp_j -= 1 T[tmp_i] = T[tmp_j] while tmp_i < tmp_j and T[tmp_i][rank] < key: tmp_i += 1 T[tmp_j] = T[tmp_i] T[tmp_i] = key_item quick_sort( T, left, tmp_i-1, rank ) quick_sort( T, tmp_i+1, right, rank ) return T # 制作 kd 樹 # 原隊列: [(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)] # 1,以x軸為基準排列 : [(2, 3), (4, 7), (5, 4), (7, 2), (8, 1), (9, 6)] # 2,取中間的數為根,這時會產生左節點集和右節點集,即:經過(7, 2)的垂直於 x 軸的超平面 1 將整個矩形區域分成了左右兩部分: # (7, 2) # / # / # / # [(2, 3), (4, 7), (5, 4)] [(8, 1), (9, 6)] # 3,對左右子樹集以 y 軸為基準排列: # [(2, 3), (5, 4), (4, 7)] [(8, 1), (9, 6)] # 4,取中間的數為父節點,這時會產生左節點集和右節點集,即:經過 (5, 4) 和 (9, 6) 的垂直於超平面 2 (或者說 y 軸)的超平面將上面的兩個左右區域又分成了兩部分 # (5, 4) (9, 6) # / \ / # / \ / # (2, 3) (4, 7) (8, 1) # 循環上面 4 步,直到沒有結點。# 終於 kd 樹例如以下圖所看到的: # (7, 2) # / # / # (5, 4) (9, 6) # / \ / # / \ / # (2, 3) (4, 7) (8, 1) def make_kd_tree( T ): # 獲取中間的數 def get_middle_item( _input ): middle_item_num = len( _input ) / 2 middle_item = _input[middle_item_num] return middle_item, middle_item_num # kd 樹的叠代函數 # 參數:二叉樹的結點,上一步的結點集。當前叠代時結點集的最小序號,最大序號,秩 def iter_for_kd_tree( root, tmp_T, left, right, rank ): # 依據 left 和 right 截取 tmp_T,tmp_T 為上一步的結點集,如: # 在第一次叠代後,若當前循環的是左結點集,那 left = 0,right = middle_item_num # 於是本次就是在 [(2, 3), (5, 4), (4, 7)] 這個結點集中選擇中位點並繼續了。
tmp_T = tmp_T[left: right] # 若當前的結點集中已無節點,就返回 None if len(tmp_T) == 0: return # 若當前結點中僅僅有一個元素,那就創建並返回用該元素創建的二叉樹結點 if len(tmp_T) == 1: return BinaryTreeNode( tmp_T[0] ) # 對當前的結點集以當前的秩為基準進行排列 quick_sort( tmp_T, 0, len(tmp_T)-1, rank ) # 更新秩,為下次排列做準a rank = (rank + 1) % len(T[0]) # 獲取當前結點集的中間元素和中間元素的坐標(該坐標用於將當前結點集分離成兩部分) middle_item, middle_item_num = get_middle_item( tmp_T ) # 使用該中間元素創建一個二叉樹結點 root = BinaryTreeNode( middle_item ) # 將 "root 的左子結點。當前的結點集,左邊結點集的最小坐標。左邊結點集的最大坐標,秩" 傳入本函數進行叠代 # 返回的結點保存到 root 的左子結點 root.left = iter_for_kd_tree( root.left, tmp_T, 0, middle_item_num, rank ) # root 的左子結點的父結點指向 root 自己 if root.left != None: root.left.father = root # 同上。保存到 root 的右子結點 root.right = iter_for_kd_tree( root.right, tmp_T, middle_item_num+1, len(tmp_T), rank ) if root.right != None: root.right.father = root # 返回根 return root rank = 0 # 第一次在 x 軸上找中位點 return iter_for_kd_tree( BinaryTreeNode(), T, 0, len(T), rank ) # 使用 kd 樹,進行 k 近鄰算法 def use_kd_tree( T, root, target ): # 得到兩點間的距離 def get_distance( x, y ): distance = (x[0] - y[0]) * (x[0] - y[0]) + (x[1] - y[1]) * (x[1] - y[1]) return math.sqrt( distance ) # 中序遍歷 kd 樹。得到包括 target 的葉子結點 def inOrder( node, rank ): # 假設該結點沒有左子結點和右子結點,那該結點就是葉子結點了 if not node.left and not node.right: return node # 保存當前的秩 tmp_rank = rank # 更新秩 rank = (rank + 1) % len(T[0]) # 從根結點出發,假設目標點在當前秩的坐標 < node 在當前秩的坐標 if target[tmp_rank] <= node.data[tmp_rank]: # 移動到左子結點 node = inOrder( node.left, rank ) else: # 反之移動到右子結點 node = inOrder( node.right, rank ) return node # 得到近期的點 def find_close_node( node, target, close_node ): # 遍歷到根結點就 ok 了 if not node.father: return # 計算當前近期鄰點距離 min_distance = get_distance( node.data, target ) # 計算 target 距離當前最鄰近點的父節點的距離 new_distance = get_distance( node.father.data, target ) # 假設距離父節點更近 if min_distance >= new_distance: min_distance = new_distance # 將父節點保存成“當前近期鄰點” close_node = node.father.data # 推斷父節點的另外一個結點距離 target 是否更近,記得話將其保存成“當前近期鄰點” if node.father.left != node: new_distance = get_distance( node.father.left.data, target ) if min_distance >= new_distance: close_node = node.father.left.data else: new_distance = get_distance( node.father.right.data, target ) if min_distance >= new_distance: close_node = node.father.right.data find_close_node( node.father, target, close_node ) return close_node rank = 0 node = inOrder( root, rank ) close_node = node.data find_close_node( node, target, close_node ) return close_node root = make_kd_tree( list_T ) target = (4, 3) print use_kd_tree( list_T, root, target ) #bt = BTree( root ) #bt.inOrder( bt.root )
K 近鄰算法