【机器学习(二)】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树,也举例介绍了算法的步骤和计算,最后使用代码(机器学习库和自己代码)实现案例。

猜你喜欢

转载自blog.csdn.net/qq_39709813/article/details/106679097