1. 程式人生 > >統計學習方法 k 近鄰演算法(附簡單模型程式碼)

統計學習方法 k 近鄰演算法(附簡單模型程式碼)

1. k 近鄰演算法

k近鄰法(k-nearest neighbor, k-NN) 是一種基本分類與迴歸方法。  k近鄰法的輸入為例項的特徵向量, 對應於特徵空間的點輸出為例項的類別, 可以取多類。 k近鄰法假設給定一個訓練資料集, 其中的例項類別已定。 分類時, 對新的例項, 根據其k個最近鄰的訓練例項的類別, 通過多數表決等方式進行預測。因此, k近鄰法不具有顯式的學習過程。 k近鄰法實際上利用訓練資料集對特徵向量空間進行劃分, 並作為其分類的“模型”。 k值的選擇、 距離度量及分類決策規則是k近鄰法的三個基本要素。 k近鄰法1968年由Cover和Hart提出。
演算法(k近鄰法)
輸入: 訓練資料集


其中, xi∊x⊆Rn為例項的特徵向量, yi∊ ={c1, c2,…,cK}為例項的類別, i=1,2,…,N; 例項特徵向量x;
輸出: 例項x所屬的類y。
(1) 根據給定的距離度量, 在訓練集T中找出與x最鄰近的k個點, 涵蓋這k個點的x的鄰域記作Nk(x);
(2) 在Nk(x)中根據分類決策規則(如多數表決) 決定x的類別y:


上式中, I為指示函式, 即當yi=cj時I為1, 否則I為0。
k近鄰法的特殊情況是k=1的情形, 稱為最近鄰演算法。 對於輸入的例項點(特徵向量) x, 最近鄰法將訓練資料集中與x最鄰近點的類作為x的類。k近鄰法沒有顯式的學習過程。


2. k 近鄰模型

k近鄰法使用的模型實際上對應於對特徵空間的劃分。 模型由三個基本要素——距離度量、 k值的選擇和分類決策規則決定。

特徵空間中兩個例項點的距離是兩個例項點相似程度的反映。 k近鄰模型的特徵空間一般是n維實數向量空間Rn。 使用的距離是歐氏距離, 但也可以是其他距離, 如更一般的Lp距離(Lp distance) 或Minkowski距離(Minkowski distance) 。設特徵空間x是n維實數向量空間Rn,xi,xj的Lp距離定義為:這裡p≥1。 當p=2時, 稱為歐氏距離(Euclidean distance), 即
當p=1時, 稱為曼哈頓距離(Manhattan distance) , 即

當p= 時, 它是各個座標距離的最大值, 即

圖3.2給出了二維空間中p取不同值時, 與原點的Lp距離為1(Lp=1) 的點的圖形。

k值的選擇會對k近鄰法的結果產生重大影響。
如果選擇較小的k值, 就相當於用較小的鄰域中的訓練例項進行預測, “學習”的近似誤差(approximation error) 會減小, 只有與輸入例項較近的(相似的) 訓練例項才會對預測結果起作用。 但缺點是“學習”的估計誤差(estimation error) 會增大預測結果會對近鄰的例項點非常敏感。 如果鄰近的例項點恰巧是噪聲, 預測就會出錯。 換句話說, k值的減小就意味著整體模型變得複雜, 容易發生過擬合
如果選擇較大的k值, 就相當於用較大鄰域中的訓練例項進行預測。 其優點是可以減少學習的估計誤差。 但缺點是學習的近似誤差會增大。 這時與輸入例項較遠的(不相似的) 訓練例項也會對預測起作用, 使預測發生錯誤。 k值的增大就意味著整體的模型變得簡單
k近鄰法中的分類決策規則往往是多數表決, 即由輸入例項的k個鄰近的訓練例項中的多數類決定輸入例項的類。
多數表決規則(majority voting rule) 有如下解釋: 如果分類的損失函式為0-1損失函式, 分類函式為那麼誤分類的概率是。對給定的例項x∊x, 其最近鄰的k個訓練例項點構成集合Nk(x)。 如果涵蓋Nk(x)的區域的類別是cj, 那麼誤分類率是要使誤分類率最小即經驗風險最小, 就要使 最大, 所以多數表決規則等價於經驗風險最小化。


3. k 近鄰法的實現: kd 樹

kd樹是一種對k維空間中的例項點進行儲存以便對其進行快速檢索的樹形資料結構。kd樹是二叉樹, 表示對k維空間的一個劃分(partition) 。 構造kd樹相當於不斷地用垂直於座標軸的超平面將k維空間切分, 構成一系列的k維超矩形區域。 kd樹的每個結點對應於一個k維超矩形區域。
構造平衡kd樹
輸入: k維空間資料集T={x1, x2,…,xN},
其中 , i=1,2,…,N;
輸出: kd樹。
(1) 開始: 構造根結點, 根結點對應於包含T的k維空間的超矩形區域。
選擇x(1)為座標軸, 以T中所有例項的x(1)座標的中位數為切分點, 將根結點對應的超矩形區域切分為兩個子區域。 切分由通過切分點並與座標軸x(1)垂直的超平面實現。由根結點生成深度為1的左、 右子結點: 左子結點對應座標x(1)小於切分點的子區域,右子結點對應於座標x(1)大於切分點的子區域。將落在切分超平面上的例項點儲存在根結點。
(2) 重複: 對深度為j的結點, 選擇x(l)為切分的座標軸, l=j(modk)+1, 以該結點的區域中所有例項的x(l)座標的中位數為切分點, 將該結點對應的超矩形區域切分為兩個子區域。 切分由通過切分點並與座標軸x(l)垂直的超平面實現。由該結點生成深度為j+1的左、 右子結點: 左子結點對應座標x(l)小於切分點的子區域, 右子結點對應座標x(l)大於切分點的子區域。將落在切分超平面上的例項點儲存在該結點。
(3) 直到兩個子區域沒有例項存在時停止。 從而形成kd樹的區域劃分。
程式碼:

# kd-tree每個結點中主要包含的資料結構如下 
class KdNode(object):
    def __init__(self, dom_elt, split, left, right):
        self.dom_elt = dom_elt  # k維向量節點(k維空間中的一個樣本點)
        self.split = split      # 整數(進行分割維度的序號)
        self.left = left        # 該結點分割超平面左子空間構成的kd-tree
        self.right = right      # 該結點分割超平面右子空間構成的kd-tree
 
 
class KdTree(object):
    def __init__(self, data):
        k = len(data[0])  # 資料維度
        
        def CreateNode(split, data_set): # 按第split維劃分資料集exset建立KdNode
            if not data_set:    # 資料集為空
                return None
            # key引數的值為一個函式,此函式只有一個引數且返回一個值用來進行比較
            # operator模組提供的itemgetter函式用於獲取物件的哪些維的資料,引數為需要獲取的資料在物件中的序號
            #data_set.sort(key=itemgetter(split)) # 按要進行分割的那一維資料排序
            data_set.sort(key=lambda x: x[split])
            split_pos = len(data_set) // 2      # //為Python中的整數除法
            median = data_set[split_pos]        # 中位數分割點             
            split_next = (split + 1) % k        # cycle coordinates
            
            # 遞迴的建立kd樹
            return KdNode(median, split, 
                          CreateNode(split_next, data_set[:split_pos]),     # 建立左子樹
                          CreateNode(split_next, data_set[split_pos + 1:])) # 建立右子樹
                                
        self.root = CreateNode(0, data)         # 從第0維分量開始構建kd樹,返回根節點


# KDTree的前序遍歷
def preorder(root):  
    print (root.dom_elt)  
    if root.left:      # 節點不為空
        preorder(root.left)  
    if root.right:  
        preorder(root.right)    

用kd樹的最近鄰搜尋
輸入: 已構造的kd樹; 目標點x;
輸出: x的最近鄰。
(1) 在kd樹中找出包含目標點x的葉結點: 從根結點出發, 遞迴地向下訪問kd樹。 若目標點x當前維的座標小於切分點的座標, 則移動到左子結點, 否則移動到右子結點。 直到子結點為葉結點為止。
(2) 以此葉結點為“當前最近點”。
(3) 遞迴地向上回退, 在每個結點進行以下操作:
(a) 如果該結點儲存的例項點比當前最近點距離目標點更近, 則以該例項點為“當前最近點”。
(b) 當前最近點一定存在於該結點一個子結點對應的區域。 檢查該子結點的父結點的另一子結點對應的區域是否有更近的點。 具體地, 檢查另一子結點對應的區域是否與以目標點為球心、 以目標點與“當前最近點”間的距離為半徑的超球體相交。
如果相交, 可能在另一個子結點對應的區域記憶體在距目標點更近的點, 移動到另一個子結點。 接著, 遞迴地進行最近鄰搜尋;
如果不相交, 向上回退。
(4) 當回退到根結點時, 搜尋結束。 最後的“當前最近點”即為x的最近鄰點。
如果例項點是隨機分佈的, kd樹搜尋的平均計算複雜度是O(logN), 這裡N是訓練實
例數。 kd樹更適用於訓練例項數遠大於空間維數時的k近鄰搜尋。 當空間維數接近訓練實
例數時, 它的效率會迅速下降, 幾乎接近線性掃描。
程式碼:

# 對構建好的kd樹進行搜尋,尋找與目標點最近的樣本點:
from math import sqrt
from collections import namedtuple

# 定義一個namedtuple,分別存放最近座標點、最近距離和訪問過的節點數
result = namedtuple("Result_tuple", "nearest_point  nearest_dist  nodes_visited")
  
def find_nearest(tree, point):
    k = len(point) # 資料維度
    def travel(kd_node, target, max_dist):
        if kd_node is None:     
            return result([0] * k, float("inf"), 0) # python中用float("inf")和float("-inf")表示正負無窮
 
        nodes_visited = 1
        
        s = kd_node.split        # 進行分割的維度
        pivot = kd_node.dom_elt  # 進行分割的“軸”
        
        if target[s] <= pivot[s]:           # 如果目標點第s維小於分割軸的對應值(目標離左子樹更近)
            nearer_node  = kd_node.left     # 下一個訪問節點為左子樹根節點
            further_node = kd_node.right    # 同時記錄下右子樹
        else:                               # 目標離右子樹更近
            nearer_node  = kd_node.right    # 下一個訪問節點為右子樹根節點
            further_node = kd_node.left
 
        temp1 = travel(nearer_node, target, max_dist)  # 進行遍歷找到包含目標點的區域
        
        nearest = temp1.nearest_point       # 以此葉結點作為“當前最近點”
        dist = temp1.nearest_dist           # 更新最近距離
        
        nodes_visited += temp1.nodes_visited  
 
        if dist < max_dist:     
            max_dist = dist    # 最近點將在以目標點為球心,max_dist為半徑的超球體內
            
        temp_dist = abs(pivot[s] - target[s])    # 第s維上目標點與分割超平面的距離
        if  max_dist < temp_dist:                # 判斷超球體是否與超平面相交
            return result(nearest, dist, nodes_visited) # 不相交則可以直接返回,不用繼續判斷
            
        #----------------------------------------------------------------------  
        # 計算目標點與分割點的歐氏距離  
        temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(pivot, target)))     
        
        if temp_dist < dist:         # 如果“更近”
            nearest = pivot          # 更新最近點
            dist = temp_dist         # 更新最近距離
            max_dist = dist          # 更新超球體半徑
        
        # 檢查另一個子結點對應的區域是否有更近的點
        temp2 = travel(further_node, target, max_dist) 
        
        nodes_visited += temp2.nodes_visited
        if temp2.nearest_dist < dist:        # 如果另一個子結點記憶體在更近距離
            nearest = temp2.nearest_point    # 更新最近點
            dist = temp2.nearest_dist        # 更新最近距離
 
        return result(nearest, dist, nodes_visited)
 
    return travel(tree.root, point, float("inf"))  # 從根節點開始遞迴

p = inf為閔式距離minkowski_distance

例3.2 給定一個二維空間的資料集:構造一個平衡kd樹。

data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd = KdTree(data)
preorder(kd.root)

[7, 2]
[5, 4]
[2, 3]
[4, 7]
[9, 6]
[8, 1]

找出距離點[3,4.5]最近的點:

ret = find_nearest(kd, [3,4.5])
print (ret)

Result_tuple(nearest_point=[2, 3], nearest_dist=1.8027756377319946, nodes_visited=4)

本程式碼同樣適用三維,使用方式一樣。

以上內容均出自李航老師的《統計學習方法》。