k近鄰法(KNN)

1.概念與目的

k近鄰法(k-Nearest Neighbor,簡稱kNN)學習是一種常用的監督式學習方法。

給定測試樣本,基於某種距離度量找出訓練集中與其最靠近的k個訓練樣本,然後基於這k個“鄰居”的信息來進行預測。通常,在分類任務中可使用“投票法”,即選擇這k個樣本中出現最多的類別標記作爲預測結果;在迴歸任務中可以使用“平均法”,即將這k個樣本的實值輸出標記的平均值作爲預測結果;還可以基於距離遠近進行加權平均或加權投票,距離越近的樣本權重越大。

**k近鄰有個明顯的不同之處:**它似乎沒有顯示的訓練過程。事實上,它是“懶惰學習”(lazy learning)的著名代表,此類學習技術在訓練階段僅僅是把樣本保存起來,訓練時間開銷爲0,待收到測試樣本後再進行處理,這種稱爲“急切學習”。

****KNN的三要素:****k值的選擇,距離度量及分類決策規則.當k=1時稱爲最近鄰算法.主要核心思想是“物以類聚”,看訓練集中離該輸入最近的實例多數屬於什麼類別。
在這裏插入圖片描述

2.模型

當訓練集,距離度量,k值以及分類決策規則確定後,特徵空間已經根據這些要素被劃分爲一些子空間,且子空間裏每個點所屬的類也已被確定.

3.策略

(1)距離:
特徵空間中兩個實例點的距離是相似程度的反映,k近鄰算法一般使用歐氏距離,也可以使用更一般的Lp距離或Minkowski距離.設特徵空間X是n維實數向量空間Rn,x(i),x(j)∈X,x=(x0,x1,x2,…,xn)T,x(i),x(j)的Lp距離定義爲:
在這裏插入圖片描述
這裏p⩾1。當p=2時,稱爲歐式距離(Euclidean distance),即
在這裏插入圖片描述
當p=1時,稱爲曼哈頓距離(Manhanttan distance),即
在這裏插入圖片描述
當p=∞時,它是各個座標距離的最大值,即:

在這裏插入圖片描述
(2)k值:
k值較小時,整體模型變得複雜,容易發生過擬合.k值較大時,整體模型變得簡單.在應用中k一般取較小的值,通過交叉驗證法選取最優的k.
(3)分類決策規則
k近鄰中的分類決策規則往往是多數表決,多數表決規則等價於經驗風險最小化.

4.算法

目標:根據給定的距離度量,在訓練集中找出與x最鄰近的k個點,根據分類規則決定x的類別ykd樹算法
(1)描述:
kd樹就是一種對k維空間中的實例點進行存儲以便對其進行快速檢索的樹形數據結構,它是一個二叉樹,表示對k維空間的一個劃分.構造kd樹相當於不斷用垂直於座標軸的超平面將k維空間劃分,構造一列的k維超矩形區域,而kd樹的每一個節點對應於一個k維超矩形區域。kd樹更適用於訓練實例數遠大於空間維數時的k近鄰搜索.
(2)構造平衡kd樹算法:
可以通過如下遞歸實現:在超矩形區域上選擇一個座標軸和此座標軸上的一個切分點,確定一個超平面,該超平面將當前超矩形區域切分爲兩個子區域.在子區域上重複切分直到子區域內沒有實例時終止.通常依次選擇座標軸和選定座標軸上的中位數點爲切分點,這樣可以得到平衡kd樹.
(3)kd樹的最近鄰搜索:
從根節點出發,若目標點x當前維的座標小於切分點的座標則移動到左子結點,否則移動到右子結點,直到子結點爲葉結點爲止.以此葉結點爲"當前最近點",遞歸地向上回退,在每個結點:(a)如果該結點比當前最近點距離目標點更近,則以該結點爲"當前最近點"(b)"當前最近點"一定存在於該結點一個子結點對應的區域,檢查該結點的另一子結點對應的區域是否與以目標點爲球心,以目標點與"當前最近點"間的距離爲半徑的超球體相交.如果相交,移動到另一個子結點,如果不相交,向上回退.持續這個過程直到回退到根結點,最後的"當前最近點"即爲最近鄰點.

5.kd樹的代碼實現

(1)首先在構造kd樹的時候需要尋找中位數,因此用快速排序來獲取一個list中的中位數。

import matplotlib.pyplot as plt
import numpy as np

class QuickSort(object):
    "Quick Sort to get medium number"

    def __init__(self, low, high, array):
        self._array = array
        self._low = low
        self._high = high
        self._medium = (low+high+1)//2 # python3中的整除

    def get_medium_num(self):
        return self.quick_sort_for_medium(self._low, self._high, 
                                          self._medium, self._array)

    def quick_sort_for_medium(self, low, high, medium, array): #用快速排序來獲取中位數
        if high == low:
            return array[low] # find medium
        if high > low:
            index, partition = self.sort_partition(low, high, array); 
            #print array[low:index], partition, array[index+1:high+1]
            if index == medium:
                return partition
            if index > medium:
                return self.quick_sort_for_medium(low, index-1, medium, array)
            else:
                return self.quick_sort_for_medium(index+1, high, medium, array)

    def quick_sort(self, low, high, array):  #正常的快排
        if high > low:
            index, partition = self.sort_partition(low, high, array); 
            #print array[low:index], partition, array[index+1:high+1]
            self.quick_sort(low, index-1, array)
            self.quick_sort(index+1, high, array)

    def sort_partition(self, low, high, array): # 用第一個數將數組裏面的數分成兩部分
        index_i = low
        index_j = high
        partition = array[low]
        while index_i < index_j:
            while (index_i < index_j) and (array[index_j] >= partition):
                index_j -= 1
            if index_i < index_j:
                array[index_i] = array[index_j]
                index_i += 1
            while (index_i < index_j) and (array[index_i] < partition):
                index_i += 1
            if index_i < index_j:
                array[index_j] = array[index_i]
                index_j -= 1
        array[index_i] = partition
        return index_i, partition

(2)構造kd樹

class KDTree(object):

    def __init__(self, input_x, input_y):
        self._input_x = np.array(input_x)
        self._input_y = np.array(input_y)
        (data_num, axes_num) = np.shape(self._input_x)
        self._data_num = data_num
        self._axes_num = axes_num
        self._nearest = None  #用來存儲最近的節點
        return

    def construct_kd_tree(self):
        return self._construct_kd_tree(0, 0, self._input_x)

    def _construct_kd_tree(self, depth, axes, data):
        if not data.any():
            return None
        axes_data = data[:, axes].copy()
        qs = QuickSort(0, axes_data.shape[0]-1, axes_data)
        medium = qs.get_medium_num() #找到軸的中位數

        data_list = []
        left_data = []
        right_data = []
        data_range = range(np.shape(data)[0])
        for i in data_range:   # 跟中位數相比較
            if data[i][axes] == medium:  #相等
                data_list.append(data[i])
            elif data[i][axes] < medium: 
                left_data.append(data[i])
            else:
                right_data.append(data[i])

        left_data = np.array(left_data)
        right_data = np.array(right_data)
        left = self._construct_kd_tree(depth+1, (axes+1)% self._axes_num, left_data)
        right = self._construct_kd_tree(depth+1, (axes+1)% self._axes_num, right_data)
        #[樹的深度,軸,中位數,該節點的數據,左子樹,右子樹]
        root = [depth, axes, medium, data_list, left, right] 
        return root

    def print_kd_tree(self, root): #打印kd樹
        if root:
            [depth, axes, medium, data_list, left, right] = root
            print('{} {}'.format('    '*depth, data_list[0]))
            if root[4]:
                self.print_kd_tree(root[4])
            if root[5]:
                self.print_kd_tree(root[5])


//測試代碼
input_x = [[2,3], [6,4], [9,6], [4,7], [8,1], [7,2]]
input_y = [1, 1, 1, 1, 1, 1]
kd = KDTree(input_x, input_y)
tree = kd.construct_kd_tree()
kd.print_kd_tree(tree)

#得到結果:
 [7 2]
     [6 4]
         [2 3]
         [4 7]
     [9 6]
         [8 1]

(3)搜索kd樹
在類中繼續添加如下函數,基本的思路是將路徑上的節點依次入棧,再逐個出棧。

def _get_distance(self, x1, x2): #計算兩個向量之間的距離
        x = x1-x2
        return np.sqrt(np.inner(x, x))

    def _search_leaf(self, stack, tree, target): #以tree爲根節點,一直搜索到葉節點,並添加到stack中
        travel_tree = tree
        while travel_tree:
            [depth, axes, medium, data_list, left, right] = travel_tree
            if target[axes] >= medium:
                next_node = right
                next_direction = 'right' # 記錄被訪問過的子樹的方向
            elif target[axes] < medium:
                next_node = left
                next_direction = 'left' # 記錄被訪問過的子樹的方向
            stack.append([travel_tree, next_direction]) #保存方向,用來記錄哪個子樹被訪問過
            travel_tree = next_node

    def _check_nearest(self, current, target): # 判斷當前節點跟目標的距離
        d = self._get_distance(current, target)
        if self._nearest:
            [node, distance] = self._nearest
            if d < distance:
                self._nearest = [current, d]
        else:
            self._nearest = [current, d]

    def search_kd_tree(self, tree, target): #搜索kd樹
        stack = []
        self._search_leaf(stack, tree, target) # 一直搜索到葉節點,並將路徑入棧
        self._nearest = []
        while stack:
            [[depth, axes, medium, data_list, left, right], next_direction] = stack.pop() #出棧
            [data] = data_list
            self._check_nearest(data, target) #檢查當前節點的距離

            if left is None and right is None: #如果當前節點爲葉節點,繼續下一個循環
                continue
            [node, distance] = self._nearest
            if abs(data[axes] - node[axes]) < distance: #<*> 當前節點的軸經過圓
                if next_direction == 'right': # 判斷哪個方向被訪問過,轉向相反方向
                    try_node = left
                else:
                    try_node = right
                self._search_leaf(stack, try_node, target) #往相反的方向搜索葉節點
        print(self._nearest)


//測試代碼
kd.search_kd_tree(tree, [7.1, 4.1])
> [array([6, 4]), 1.1045361017187258]

kd.search_kd_tree(tree, [9, 2])
> [array([8, 1]), 1.4142135623730951]

kd.search_kd_tree(tree, [6, 2])
> [array([7, 2]), 1.0]

(4)尋找k個最近節點
如果要尋找k個最近節點,則需要保存k個元素的數組,並在函數_check_nearest中與k個元素做比較,然後在標記<*>的地方跟k個元素的最大值比較。其他代碼略。

def _check_nearest(self, current, target, k):
        d = self._get_distance(current, target)
        #print current, d
        l = len(self._nearest)
        if l < k:
            self._nearest.append([current, d])
        else:
            farthest = self._get_farthest()[1]
            if farthest > d:
                # 將最遠的節點移除
                new_nearest = [i for i in self._nearest if i[1] [[array([7, 2]), 2.1023796041628633], [array([6, 4]), 1.1045361017187258]]

kd.search_kd_tree(tree, [9, 2], k=2)
> [[array([8, 1]), 1.4142135623730951], [array([7, 2]), 2.0]]

kd.search_kd_tree(tree, [6, 2], k=2)

> [[array([6, 4]), 2.0], [array([7, 2]), 1.0]]

參考博客解釋:
https://blog.csdn.net/baimafujinji/article/details/52928203

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