经典机器学习模型(二)KNN模型

经典机器学习模型(二)KNN模型

1 KNN算法简介

1.1 初识KNN

KNN算法可以说是机器学习中最简单的是一种分类、回归方法,它甚至连一个显性的模型结构表达都没有。

假如给定了一个训练数据集 T T T,里面包含了 N N N个样本,每一个实例的标签都给定。当给定一个新的实例 x x x时,我们就可以在训练数据集 T T T里面寻找与 x x x最近的 k k k个实例,然后根据这 k k k个实例的标签来决定新实例的 x x x

  • 对于分类问题,采用多数表决。哪一种类别所占比例最大,那么新的实例点它就属于哪个类别。

  • 对于回归问题,可以取这些邻居的标签,计算平均值。

  • 如下图,k=3时,绿色圆点属于红色三角形类别,k=5时,绿色圆点属于蓝色正方形类别

  • 如果换一种距离定义方式,可能这里绿圆点所属的类别也会发生变化。所以,多少个邻居,怎么计算距离,如何通过邻居的情况反映目标点的信息,都是非常关键的问题。

在这里插入图片描述

1.2 KNN算法及其三要素

KNN算法如下:

在这里插入图片描述

距离度量、 k k k个邻居、和分类决策规则,这三个就是k近邻法的三要素。任意一者的变化,都可能导致实例 x x x所属的类别发生变化。

1.2.1 距离度量

特征空间中任意的 x i 和 x j 都是一个 n 维向量,下式中 x ( l ) 代表的是第 l 个特征, p 决定了不同的范数距离。 L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p , p > = 1 特征空间中任意的x_i和x_j都是一个n维向量,下式中x^{(l)}代表的是第l个特征,p决定了不同的范数距离。 \\ L_p(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^p)^{\frac{1}{p}},p>=1 特征空间中任意的xixj都是一个n维向量,下式中x(l)代表的是第l个特征,p决定了不同的范数距离。Lp(xi,xj)=(l=1nxi(l)xj(l)p)p1,p>=1

  • 当p=2的时候,是我们最熟悉的欧式距离

L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 = ( x i ( 1 ) − x j ( 1 ) ) 2 + ( x i ( 2 ) − x j ( 2 ) ) 2 + . . . + ( x i ( n ) − x j ( n ) ) 2 L_2(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^2)^{\frac{1}{2}} \\ =\sqrt{(x_i^{(1)}-x_j^{(1)})^2+(x_i^{(2)}-x_j^{(2)})^2+...+(x_i^{(n)}-x_j^{(n)})^2} \\ L2(xi,xj)=(l=1nxi(l)xj(l)2)21=(xi(1)xj(1))2+(xi(2)xj(2))2+...+(xi(n)xj(n))2

  • 当p=1的时候,称为曼哈顿距离,也就是计算所有特征下的绝对距离,再求和。

L 1 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ ) = ∣ x i ( 1 ) − x j ( 1 ) ∣ + ∣ x i ( 2 ) − x j ( 2 ) ∣ + . . . + ∣ x i ( n ) − x j ( n ) ∣ L_1(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|) \\ =|x_i^{(1)}-x_j^{(1)}|+|x_i^{(2)}-x_j^{(2)}|+...+|x_i^{(n)}-x_j^{(n)}| \\ L1(xi,xj)=(l=1nxi(l)xj(l))=xi(1)xj(1)+xi(2)xj(2)+...+xi(n)xj(n)

  • 当p为无穷的时候,称为切比雪夫距离,也就是找到所有特征下的绝对距离的最大值。

L ∞ ( x i , x j ) = max ⁡ l ∣ x i ( l ) − x j ( l ) ∣ L_\infty(x_i,x_j)=\max \limits_{l}|x_i^{(l)}-x_j^{(l)}| \\ L(xi,xj)=lmaxxi(l)xj(l)

举例如下:

x 0 = ( 0 , 0 ) T x_0=(0,0)^T x0=(0,0)T ,求出下图蓝色、红色和黄色分别代表的 L P L_P LP距离。

  • L 2 ( x i , x 0 ) = = ( x i ( 1 ) − 0 ) 2 + ( x i ( 2 ) − 0 ) 2 = ( x i ( 1 ) − 0 ) 2 + ( x i ( 2 ) − 0 ) 2 = 1 L_2(x_i,x_0)= =\sqrt{(x_i^{(1)}-0)^2+(x_i^{(2)}-0)^2} = (x_i^{(1)}-0)^2+(x_i^{(2)}-0)^2 = 1 L2(xi,x0)==(xi(1)0)2+(xi(2)0)2 =(xi(1)0)2+(xi(2)0)2=1,代表一个圆,因此红色线为L_2,即欧式距离。
  • L 1 ( x i , x 0 ) = = ∣ x i ( 1 ) − 0 ∣ + ∣ x i ( 2 ) − 0 ∣ = 1 L_1(x_i,x_0)= =|x_i^{(1)}-0|+|x_i^{(2)}-0|=1 L1(xi,x0)==xi(1)0∣+xi(2)0∣=1,而AB + BO=1,可知蓝色线为L_1,即曼哈顿距离。曼哈顿距离的命名来源于曼哈顿岛上的街道网格系统,因为只能沿着水平和垂直方向移动,所以通过网格线移动的最短路径就是曼哈顿距离。
  • 黄色线代表切比雪夫距离。
  • 需要注意的是不同的距离度量,所确定的最近邻点也是不同的

在这里插入图片描述

1.2.2 k值的选择

我们已经知道了,当 k k k值不同时,所属的分类也会不同。

  • 如果选择的 k k k值比较小,相当于就在一个比较小的领域里面对训练集内的实例进行预测,所以近似误差小,但是如果新增一个实例时,超过了范围则会导致估计误差增大,这就是只对训练集友好,对新的实例点不友好的情况——过拟合;
  • 与之相对, k k k值较大时,就会出现欠拟合;
  • k k k值可通过交叉验证选择, k k k值一般低于训练集样本量的平方根。

1.2.3 分类决策规则

k 近邻法中的分类决策规则往往是多数表决,即由输入实例的k 个邻近的训练实例中的多数类决定输入实例的类。

在这里插入图片描述

1.3 利用python简单实现KNN

import numpy as np
from collections import Counter

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):
        # 1、从训练集中取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]))
        # 2、遍历未取的训练集,如果所取的knn_list中最大距离 > 目前遍历点的距离,就用现在的点进行替换
        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])

        # 3、将取出的n个最近邻的标签值进行统计
        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
        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)


if __name__ == '__main__':
    # 求x的类别
    x = [1, 1]
    x1 = [15, 10]
    x2 = [4, 4]
    x3 = [10, 2]
    x4 = [2, 1]
    X_train = np.array([x1, x2, x3, x4])
    y_train = np.array([0, 1, 0, 1])
    knn = KNN(X_train=X_train, y_train=y_train, n_neighbors=3)
    print(knn.predict(np.array(x)))

2 K近邻法的实现:kd树

  • 我们现在已经知道了KNN算法,并且用python进行了简单实现。在上面的实现过程中,实现方法是线性扫描(linear scan),计算了输入实例与每1个训练实例的距离。当训练集很大时,计算非常耗时,这种方法是不可行的。

  • 当训练集很大时,我们可以用一个更快速的计算办法——kd树。

  • kd 从根本上来看,是一个二叉树结构,根据这个结构对 k k k维空间进行不断的划分,每一个节点就代表了 k k k维超矩形区域。既然是二叉树,可以想到在建树的时候需要用到递归法

2.1 利用递归法构造kd树

2.1.1 构造kd树的算法描述

在这里插入图片描述

2.1.2 举例构造kd树

为了更加理解上面算法,就用《统计学习方法》书中的例题,构建一棵二维的kd树,手动走下算法流程。

数据集为 T = ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) T={(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)} T=(2,3),(5,4),(9,6),(4,7),(8,1),(7,2),利用此训练数据集构造一棵kd树。

1、开始:构造根节点

因为该训练数据集的维度为2,那么任选一个特征即可。不妨选择 x ( 1 ) x^{(1)} x(1)为坐标轴(即第一个特征维度),将 x ( 1 ) x^{(1)} x(1)中的数据按照从小到大排序,分别是:2,4,5,7,8,9。

中位数不妨选7,即以(7,2)为根节点,切分整个区域,那么左区域的坐标小于7,右节点坐标大于7。

在这里插入图片描述

2、剩余特征的选取和切割

左边区域点为(5,4),(2,3),(4,7),我们先递归的创建左子树

此时,按照第2个特征进行排序,即3、4、7,中位数为4,切分坐标为(5, 4),将左区域再次划分为上、下两个区域。

在这里插入图片描述

继续递归,下面区域小于4,按照第1个特征进行排序,只有1个点了(2, 3),切割完无实例点结束。

上面区域大于4,仍旧按照第1个特征进行排序,也只有1个点了(4, 7),切割完无实例点结束,左子树构造完毕。

在这里插入图片描述

同理可以递归构造右子树,构造完如下。

在这里插入图片描述

2.1.3 构造kd树的python实现

# 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维划分数据集data_set创建KdNode
            if not data_set:  # 数据集为空
                return None
            # 按照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    # 下一个分割点

            print(f'median = {median}, split = {split + 1}')

            # 递归的创建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)


if __name__ == '__main__':
    data = [
        [2, 3],
        [5, 4],
        [9, 6],
        [4, 7],
        [8, 1],
        [7, 2]
    ]
    tree = KdTree(data)
    preorder(tree.root)
median = [7, 2], split = 1
median = [5, 4], split = 2
median = [2, 3], split = 1
median = [4, 7], split = 1
median = [9, 6], split = 2
median = [8, 1], split = 1
[7, 2]
[5, 4]
[2, 3]
[4, 7]
[9, 6]
[8, 1]

2.2 搜索kd树

2.2.1 最近邻搜索算法

在这里插入图片描述

2.2.2 举例理解最近邻搜索算法

在这里插入图片描述

还是上面的例题。我们目标点(2,4.5)的最近邻。

在这里插入图片描述

  • 1.寻找当前最近点:

    • 从根节点开始,(2, 4.5) 在根节点 (7, 2)的左子区域内,继续到(5, 4) 所确定的上子区域内,继续到 (4,7)的左子区域中,(4, 7)就是当前最近邻点。
  • 2.回溯:

    • 我们以 (2, 4.5) 为圆心,以 (2, 4.5) 到 (4,7)两点之间的距离为半径画一个圆,这个区域内有两个节点,分别是 (2, 3) 和 (5, 4),通过计算 (2, 4.5) 到这两点的距离,得出到 (2, 3)的距离最近,那么 (2, 3) 就是最近邻点。
    • 接着,再以 (2, 4.5) 为圆心以 (2, 4.5)到(2, 3) 两点之间的距离为半径画一个圆,此时圆里没有其他的节点,说明可以确认 (2, 3)就是(2, 4.5) 的最近邻点。

2.2.3 使用python完成最近邻搜索算法

注意,这里并没有实现k近邻的算法,k近邻可以使用sklearn中的API。

# 对构建好的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:
            # python中用float("inf")和float("-inf")表示正负无穷
            return result([0] * k, float("inf"), 0)



        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"))  # 从根节点开始递归


if __name__ == '__main__':
    data = [
        [2, 3],
        [5, 4],
        [9, 6],
        [4, 7],
        [8, 1],
        [7, 2]
    ]
    tree = KdTree(data)
    # preorder(tree.root)
    # print(find_nearest(tree, point=[2.1, 3.1]))
    print(find_nearest(tree, point=[2, 4.5]))
Result_tuple(nearest_point=[2, 3], nearest_dist=0.14142135623730964, nodes_visited=3)
  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值