机器学习之K近邻法

K近邻法

KNN算法流程

K近邻法思想非常简单:给定测试样本?基于某种距离度量找出训练集中与其最靠近的k 个训练样本,然后基于这k 个" 邻居"的信息来进行预测. 通常, 在分类任务中可使用"投票法" 即选择这k 个样本中出现最多的类别标记作为预
测结果;在回归任务中时使用"平均法" ,即将这k 个样本的实值输出标记的平均值作为预测结果;还可基于距离远近进行加权平均或加权投票,距离越近的样本权重越大

这里直接给出其算法流程:
在这里插入图片描述

KNN没有显式的训练过程,它是"懒惰学习" (lazy learning) 的著名代表,此类学习技术在训练阶段仅仅是把样本保存起来,训练时间开销为零,待收到测试样本后再进行处理;相应的,那些在训练阶段就对样本进行学习处理的方
法,称为"急切学习" (eager learning)

KD-tree

KD-tree介绍

KNN最简单的实现方法是线性扫描,这时需要计算输入实例与每一个训练实例的距离,当训练集很大或者问题是高维问题时,计算非常耗时。为了提高KNN搜索的效率,可以考虑用特殊的结构存储训练数据,以减少计算距离的次数,具体方法很多,下面介绍其中的KD-tree方法。KD-tree是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构,KD-tree是二叉树,表示对k维空间的一个划分。构造KD-tree相当于不断地用垂直于坐标轴的超平面将k维空间划分,构成一系列的k维超矩形区域,KD-tree的每一个结点对应于一个k维超矩形区域

KD-tree可以看作是二叉搜索树(BST)在高维的推广,1D-tree其实就是BST

KD-tree的构造流程

KD-tree的构造流程如下:

在这里插入图片描述

上图展示的流程是李航《统计学习方法》中给出的,其中选择切分的坐标轴是按顺序依次循环的方式选取的,只是一种简单的执行方式,但不一定能得到一个好的平衡KD-tree(类似平衡二叉搜索树的平衡的含义:尽可能让每个结点的左子树和右子树划分得到的数据量相当,这样就能使得树有较低的深度,从而使得搜索k个距离最近的点时能有较低的复杂度),所以实际往往选择切分点时并不是按顺序循环,而是每次选方差最大的维度作为切分维度(这样能够较好的从切分点处分开,且分开后两边的数据量相当,还能保证自下而上搜索k个距离最近的点时,越到上方越不可能再合并考查另一端结点,见下面的KD-tree搜索算法),可以更好地二分,保持最大程度的区分度,达到平衡KD-tree,好处是最后找k近邻时复杂度更低

KD-tree搜索算法

构建好了KD-tree,就可以在KD-tree上执行搜索了,这里我们的目的是找一个测试实例在原始训练数据集中的k个距离最近的点,下面给出找最邻近的算法,找k邻接的算法是类似的(可以用一个最大堆来实现,最大堆里面先一直添加数据集中的点到该测试实例的距离,直到达到k个,此时最大堆的堆顶元素就是距离最大的第k个元素,以后新来的点到测试实例的距离若小于堆顶元素,则将最大堆的堆顶元素弹出,加入新的点到测试实例的距离,并调整元素位置以维持最大堆的条件,如果新来的点到测试实例的距离大于等于堆顶元素,则不需要执行操作,同样一直遍历到根节点结束)

在这里插入图片描述

KD-tree构造和搜索算法的Python实现

import heapq

class KdTreeNode:
    def __init__(self, data=None, split=0, median=None, left=None, right=None, parent=None):
        self.data = data
        self.split = split
        self.median = median
        self.left = left
        self.right = right
        self.parent = parent

class KdTree:
    def __init__(self, all_data, dimension, min_split=1):
        self.min_split = min_split
        self.dimension = dimension
        self.size = len(all_data)
        self.root = self.__build_kdtree(all_data)

    def __build_kdtree(self, all_data):
        if len(all_data) == 0: return None
        if len(all_data) <= self.min_split: return self.KdTreeNode(all_data, 0, all_data[0], None, None)
        split = self.__get_max_variance_dimension(all_data)
        left_data, median, right_data = self.__split(all_data, split)
        node = self.KdTreeNode([], split, median,
                               self.__build_kdtree(left_data),
                               self.__build_kdtree(right_data))
        if node.left is not None: node.left.parent = node
        if node.right is not None: node.right.parent = node
        return node

    def __get_max_variance_dimension(self, all_data):
        num_of_data = len(all_data)

        means = [0.0] * self.dimension
        variances = [0.0] * self.dimension

        for data in all_data:
            for i in range(self.dimension):
                means[i] += data[i]

        means = [mean / num_of_data for mean in means]

        for data in all_data:
            for i in range(self.dimension):
                variances[i] += (data[i] - means[i]) ** 2

        return max((variance, i) for i, variance in enumerate(variances))[1]

    def __split(self, all_data, dim):
        length = len(all_data)

        def min_k(data, begin, end, k):
            # partition
            pivot = data[begin][:]
            left = begin
            right = end
            while left < right:
                while left < right and data[right][dim] >= pivot[dim]:
                    right -= 1
                data[left] = data[right]
                while left < right and data[left][dim] <= pivot[dim]:
                    left += 1
                data[right] = data[left]
            data[left] = pivot

            if left - begin + 1 == k:
                return
            elif left - begin + 1 < k:
                return min_k(data, left + 1, end, k - (left - begin + 1))
            else:
                return min_k(data, begin, left - 1, k)

        mid = round(length / 2)
        min_k(all_data, 0, length - 1, mid)
        return all_data[0:mid], all_data[mid - 1], all_data[mid:]

    def data_distance(self, data_1, data_2):
        dis = 0.0
        for i in range(self.dimension):
            dis += (data_1[i] - data_2[i]) ** 2
        return dis ** (1 / 2)
    
    def search_k_nearest(self, data, k=1):
        result = []
        min_radius = float('inf')

        if self.root is None:
            return result

        def helper(root):
            nonlocal result
            nonlocal min_radius
            if self.size == 0:
                return
            # leaf node
            if root.left is None and root.right is None:
                for src_data in root.data:
                    dis = self.data_distance(data, src_data)
                    if dis <= min_radius:
                        if len(result) == k:
                            heapq.heappop(result)
                        heapq.heappush(result, (-dis, src_data))
                        if len(result) == k:
                            min_radius = -result[0][0]  # update threshold
            else:
                if data[root.split] <= root.median[root.split]:
                    helper(root.left)
                    if data[root.split] - root.median[root.split] >= -min_radius:
                        helper(root.right)  # cycle cross rectangle
                else:
                    helper(root.right)
                    if data[root.split] - root.median[root.split] <= min_radius:
                        helper(root.left)

        helper(self.root)
        result.sort(key=(lambda x: -x[0]))
        result = [(data, -dis) for dis, data in result]
        return result
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值