• 《统计学习方法》 第三章 K近邻法


    K K K近邻法

    k k k近邻法是基本且简单的分类与回归方法

    k k k近邻法的基本做法是:

    1. 对给定的训练实例点和输入实例点,首先确定输入实例点的 k k k个最近邻训练实例点
    2. 利用这 k k k个训练实例点的类的多数来预测输入实例点的类

    k k k近邻模型是对应于基于训练数据集对特征空间的一个划分

    k k k近邻法中,当训练集、距离度量、 k k k值及分类决策规则确定后,其结果唯一确定

    k k k近邻法三要素:距离度量、 k k k值的选择和分类决策规则

    常用的距离度量是欧氏距离及更一般的pL距离

    k k k值小时, k k k近邻模型更复杂; k k k值大时, k k k近邻模型更简单。

    k k k值的选择反映了对近似误差与估计误差之间的权衡,通常由交叉验证选择最优的 k k k

    常用的分类决策规则是多数表决,对应于经验风险最小化

    k k k近邻法的实现需要考虑如何快速搜索k个最近邻点

    kd树是一种便于对k维空间中的数据进行快速检索的数据结构

    kd树是二叉树,表示对 k k k维空间的一个划分,其每个结点对应于 k k k维空间划分中的一个超矩形区域

    利用kd树可以省去对大部分数据点的搜索, 从而减少搜索的计算量


    K K K近邻法公式

    设特征空间 x x x n n n维实数向量空间

    x i , x j ∈ X x_{i}, x_{j} \in \mathcal{X} xi,xjX

    x i = ( x i ( 1 ) , x i ( 2 ) , ⋯   , x i ( n ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),,xi(n))T

    x j = ( x j ( 1 ) , x j ( 2 ) , ⋯   , x j ( n ) ) T x_{j}=\left(x_{j}^{(1)}, x_{j}^{(2)}, \cdots, x_{j}^{(n)}\right)^{\mathrm{T}} xj=(xj(1),xj(2),,xj(n))T

    x i x_i xi, x j x_j xj L p L_p Lp距离定义为

    L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( i ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(i)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(i=1nxi(i)xj(l)p)p1

    其中

    • p = 1 p= 1 p=1 曼哈顿距离
    • p = 2 p= 2 p=2 欧氏距离
    • p = ∞ p= \infty p= 切比雪夫距离

    K K K近邻法代码实现

    import math
    from itertools import combinations
    
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    遍历所有数据点,找出 n n n个距离最近的点的分类情况,少数服从多数

    
    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
    
    # data
    iris = load_iris()
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['label'] = iris.target
    df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
    
    plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
    plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    data = np.array(df.iloc[:100, [0, 1, -1]])
    X, y = data[:,:-1], data[:,-1]
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    
    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):
            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.items(), key=lambda x: x[1])[-1][0]
            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)
    plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
    plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
    plt.plot(test_point[0], test_point[1], 'bo', label='test_point')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    在这里插入图片描述


    k d kd kd

    kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构

    kd树是二叉树,表示对 k k k维空间的一个划分(partition)

    构造kd树相当于不断地用垂直于坐标轴的超平面将 k k k维空间切分,构成一系列的k维超矩形区域。

    kd树的每个结点对应于一个 k k k维超矩形区域。

    构造kd树的方法如下

    • 构造根结点,使根结点对应于 k k k维空间中包含所有实例点的超矩形区域
    • 通过下面的递归方法,不断地对 k k k维空间进行切分,生成子结点
    • 在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面
    • 这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域 (子结点)
    • 这时,实例被分到两个子区域
    • 这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)
    • 在此过程中,将实例保存在相应的结点上

    通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数 (median)为切分点

    这样得到的 k d kd kd树是平衡的。注意,平衡的 k d kd kd树搜索时的效率未必是最优的。


    构造平衡 k d kd kd树算法

    输入:

    k k k维空间数据集 T = { x 1 , x 2 , … , x N } T=\{x_1,x_2,…,x_N\} T{x1x2,,xN}

    其中 x i = ( x i ( 1 ) , x i ( 2 ) , ⋯   , x i ( k ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(k)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),,xi(k))T i = 1 , 2 , … , N i=1,2,…,N i1,2,,N

    输出: kd


    开始:

    构造根结点,根结点对应于包含 T T T k k k维空间的超矩形区域

    选择 x ( 1 ) x^{(1)} x(1)为坐标轴,以T中所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域

    切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现

    由根结点生成深度为1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域

    右子结点对应于坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域

    将落在切分超平面上的实例点保存在根结点

    重复:

    对深度为 j j j的结点,选择 x ( 1 ) x^{(1)} x(1)为切分的坐标轴, l = j ( m o d k ) + 1 l=j(modk)+1 lj(modk)+1

    以该结点的区域中所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域

    切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现

    由该结点生成深度为 j + 1 j+1 j+1的左、右子结点:

    左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域

    将落在切分超平面上的实例点保存在该结点

    停止:

    直到两个子区域没有实例存在时停止。从而形成 k d kd 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    # 对构建好的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"))  # 从根节点开始递归
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
  • 相关阅读:
    Go-Excelize API源码阅读(十三)—— GetSheetVisible、SetSheetFormatPr
    学习教授LLM逻辑推理11.19
    2.1.3 运算放大器的参数以及选型、静态、交流技术指标
    反向传播——机器学习
    Nodejs+vue体育用品商城商品购物推荐系统_t81xg
    2022过半,Node你会用了吗
    Docker in docker 实现
    seatunnel web ui 构建时报错
    gcc编译webrtc x64
    w10系统 如何使用 C++、cmake、opencv、
  • 原文地址:https://blog.csdn.net/qq_38973721/article/details/128002583