K近鄰算法:原理、kd樹構造和查找、案例分析、代碼(機器學習庫、自己實現)

目錄

簡述

原理

三要素一:距離度量

三要素二:k值選擇

三要素三:分類決策規則

算法流程:

k近鄰實現:kd樹

構造kd樹

kd樹查找

案例說明

構造kd樹:

kd樹搜索:

結果輸出

案例代碼一

KNN類實現

kd樹構造類:

kd樹的最近鄰搜索

案例代碼二


簡述

k近鄰算法是一種分類和迴歸的算法,可以簡單認爲:

輸入:訓練的特徵向量和標籤訓練(類)

輸出:任意給一個輸入,得到這個輸入的標籤(類)

因此k近鄰有三要素:距離度量、k值選擇和分類決策。

原理

三要素確定好後,輸入一個特徵向量$x=(x^1,x^2,...,x^n)$,尋找x最近的k個訓練特徵向量,觀察最近的k個特徵向量的標籤(類),然後選擇標籤最多的那個標籤作爲x的標籤,輸出結果。

三要素一:距離度量

常用的距離度量方式就是歐式距離,但也包含其他距離度量方式,根據最近的k的距離,就可以選擇最近的k訓練特徵向量。

輸入特徵向量:$x=(x^1,x^2,...,x^n)$

L_p距離:L_p(x_1,x_2)=(\sum |x^l_i-x^l_j|^p)^{1/p}

歐式距離:L_p(x_1,x_2)=(\sum |x^l_i-x^l_j|^2)^{1/2}

曼哈頓距離:L_p(x_1,x_2)=\sum |x^l_i-x^l_j|

L_\infty距離:L_p(x_1,x_2)=\max |x^l_i-x^l_j|

具體選擇什麼樣的距離方式,可以根據自己實際的需求來來定,也可以嘗試幾種距離,選擇一個效果好的距離的距離。

三要素二:k值選擇

選擇的k值小:近似誤差低,估計誤差高;噪聲敏感;模型複雜,容易過擬合。

選擇的k值大:近似誤差高,估計誤差低;模型變得簡單。

一般k取一個比較小的數值,例如k=20,;通常採用交叉驗證法來選取k值。

三要素三:分類決策規則

k近鄰法常用的分類決策是多數表決,即k近鄰中哪個標籤(類)多,就選那個。分類函數爲:f:R^n\rightarrow {c_1,c_2,...,c_K},那麼誤分類的概率爲:p(y\ne \hat y) = 1-p(y=\hat y)

 

算法流程:

輸入:需要訓練的特徵向量及其標籤(類)

輸出:給一個特徵向量$x=(x^1,x^2,...,x^n)$,輸出它的標籤(類)

  • 確定三要素
  • 利用距離度量,選出最近的k個訓練特徵向量;
  • 在k近鄰中統計每種標籤有多少個實例
  • 選擇實例最多的標籤作爲x的標籤輸出。

 

k近鄰實現:kd樹

當我們在計算距離的時候,傳統的方法是將x與訓練集的所有實例挨個計算,這樣屬於線性掃描的方式。當訓練集很大的時候,計算非常耗時(訓練集小的時候可以這麼做),因此使用構造kd樹來加快k近鄰的搜索效率。

構造kd樹

輸入k爲空間數據集T={x_1,x_2,...,x_n},其中x_i=x^1_i,x^2_i,...,x^k_i

構造根結點,以x^1爲座標軸,選擇中位數作爲切分點,把訓練特徵分成兩部分,小於中位數的劃分在左側區域,大於中位數的劃分在右側區域;

重複這種操作,選擇x^l爲座標軸(l=j mod k +1,j是樹的深度);

直到沒有特徵向量位置,就構造完成了kd樹。

可以理解爲,使用超平面一次對訓練集進行劃分,直到把所有的訓練實例都劃分在不同區域內。

kd樹查找

已經構造好kd樹,輸入一個特徵x,查找和x的k近鄰。

從根結點出發,遞歸向下訪問kd樹的每個結點,若x的當前維小於當前結點值,則移動到左子節點,否則移動到右子節點。直到找到葉子結點爲止。

以葉子結點爲最近點

遞歸向上回退。如果當前結點的距離比葉子結點更近,則以當前結點爲最近點;檢查最近點的兄弟結點區域是否有更近點,遞歸使用最近鄰搜索算法。

直到回退到根結點,搜索結束,得到k個最近鄰。

 

案例說明

給定一個二維空間數據集:T =(2,3)^T,(5,4)^T,(9,6)^T,(4,7)^T(,8,1)^T,(7,2)^T,類分別是0,0,1,0,1,1。求給一個特徵爲(5,8)的類別。

解答:

因爲數據較少,所以可以直接使用線性搜索,爲了同時演示kd樹,因此,使用構造kd樹的方式來解答。

先確定三要素:距離度量-------->歐式距離,K值爲3,分類決策------->投票原則

構造kd樹:

先以x^1爲基準,對訓練特徵排序(2,3),(4,7),(5,4,),(7,2),(8,1),(9,6),得到中位數7,以(7,2)爲根結點,把(2,3),(4,7),(5,4)劃分到左子區域,(8,1),(9,6)劃分到右子區域。

在(7,2)爲根結點的左子區域內,以x^2維爲基礎,對(2,3),(4,7),(5,4)進行排序------->(2,3),(5,4),(4,7),找到中位數4,(5,4)作爲節點,把(2,3)劃分到左子區域,(4,7)劃分到右子區域。

在(7,2)爲根結點的右子區域內,以x^2維爲基礎,對(8,1),(9,6)進行排序-------->(8,1),(9,6)找到中位數6,把(8,1)劃分到左子區域。

所有訓練實例結束,構造樹停止。

      

kd樹搜索:

給定輸輸入 x=(5,8),開始從kd樹中搜索先從根結點開始,5<7,因此搜索(7,2)的左側,8>4,搜索(5,4)的右側,找到的(4,7)爲葉子結點,因此把(4,7)作爲最近鄰,計算距離-------------->\sqrt{2}並保存。

由下向上,找到父結點(5,4),計算距離----------------->4並保存,比\sqrt{2}大,因此最近鄰不更新。

計算兄弟結點(2,3),計算距離------------------>\sqrt{34}並保存,比\sqrt{2}大,因此最近鄰不更新。

再向上回溯,找到(54)的父結點(7,2),計算距離-------------->2\sqrt{10}並保存,比\sqrt{2}、4、\sqrt{34}都大,且k=3已經滿足,因此停止搜索。找到x的最近鄰(4,7)、(5,4)、(2,3)。

結果輸出

查看最近鄰標籤(類)

(4,7)------------->0

(5,4)------------->0

(2,3)------------->0

得到標籤的投票,標籤0是3次,標籤1是0次,選擇0作爲x=(5,8)的標籤(類)輸出。

 

案例代碼一

最簡單的代碼實現就是使用機器學習庫

from sklearn.neighbors import KNeighborsClassifier

X_train = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
y_train = [0, 0, 1, 0, 1, 1]

x = [[5, 8]]

clf_sk = KNeighborsClassifier(n_neighbors=3,
                              algorithm='kd_tree', )
clf_sk.fit(X_train, y_train)
y = clf_sk.predict(x)
print(y)

如果不是用機器學習庫,那就得自己定義類和函數,下面貼出KNN的類實現、kd樹類實現及kd樹搜索,可以在此基礎上進行修改和二次開發。

 

KNN類實現

class KNN:
    def __init__(self, X_train, y_train, n_neighbors=3, p=2):
        """
        parameter: n_neighbors 臨近點個數
        parameter: p 距離度量
        """
        self.n = n_neighbors
        self.p = p
        self.X_train = X_train
        self.y_train = y_train

    def predict(self, X):
        # 取出n個點
        knn_list = []
        for i in range(self.n):
            dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
            knn_list.append((dist, self.y_train[i]))

        for i in range(self.n, len(self.X_train)):
            max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))
            dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
            if knn_list[max_index][0] > dist:
                knn_list[max_index] = (dist, self.y_train[i])

        # 統計
        knn = [k[-1] for k in knn_list]
        count_pairs = Counter(knn)
        max_count = sorted(count_pairs, key=lambda x: x)[-1]
        return max_count

    def score(self, X_test, y_test):
        right_count = 0
        n = 10
        for X, y in zip(X_test, y_test):
            label = self.predict(X)
            if label == y:
                right_count += 1
        return right_count / len(X_test)

使用方法:

訓練數據:clf=KNN(x_train,y_train)

測試數據:clf.score(x_test,y_test)

預測數據:clf.predict(test_point)

 

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=KdTree(data),data是二維

按順序顯示結點:preorder(kd.root),使用的前序遍歷的方法

 

kd樹的最近鄰搜索

# 定義一個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)

使用方法:在上面定義了kd=KdTree(data)

查找最近鄰:ret=find_nearest(kd,x)

 

案例代碼二

使用這些類和函數對案例進行測試

import math
from itertools import combinations
from math import sqrt
from collections import namedtuple
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

from collections import Counter


# 定義距離度量方式,p=1曼哈頓距離 p=2歐式距離
def L(x, y, p=2):
    # x1 = [1, 1], x2 = [5,1]
    if len(x) == len(y) and len(x) > 1:
        sum = 0
        for i in range(len(x)):
            sum += math.pow(abs(x[i] - y[i]), p)
        return math.pow(sum, 1 / p)
    else:
        return 0


class KNN:
    def __init__(self, X_train, y_train, n_neighbors=3, p=2):
        """
        parameter: n_neighbors 臨近點個數
        parameter: p 距離度量
        """
        self.n = n_neighbors
        self.p = p
        self.X_train = X_train
        self.y_train = y_train

    def predict(self, X):
        # 取出n個點
        knn_list = []
        for i in range(self.n):
            dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
            knn_list.append((dist, self.y_train[i]))

        for i in range(self.n, len(self.X_train)):
            max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))
            dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
            if knn_list[max_index][0] > dist:
                knn_list[max_index] = (dist, self.y_train[i])

        # 統計
        knn = [k[-1] for k in knn_list]
        count_pairs = Counter(knn)
        max_count = sorted(count_pairs, key=lambda x: x)[-1]
        return max_count

    def score(self, X_test, y_test):
        right_count = 0
        n = 10
        for X, y in zip(X_test, y_test):
            label = self.predict(X)
            if label == y:
                right_count += 1
        return right_count / len(X_test)


# 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)


# 定義一個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"))  # 從根節點開始遞歸


# 測試
# kd樹構建和查找  找到的是最近鄰 不是k鄰(是k=1)
data = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
x_train=np.array([[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]])
y_train = np.array([0, 0, 1, 0, 1, 1])
kd = KdTree(data)
preorder(kd.root)
ret = find_nearest(kd, [5, 8])
print(ret)
# KNN 測試
clf = KNN(data, y_train)
test_point = np.array([5, 8])
print(clf.predict(test_point))

這和我們上一個案例代碼一有點出入。

 

到此,本文介紹了k近鄰算法的功能——對輸入特徵向量進行分類,介紹了k近鄰的三要素、流程,介紹了提升查找速度的方法——構建kd樹,也舉例介紹了算法的步驟和計算,最後使用代碼(機器學習庫和自己代碼)實現案例。

 

 

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