k近邻法的实现


前言

一、什么是k近邻算法?

k近邻法是一种基本分类与回归方法。给定一个训练数据集,其中的实例类别已定,对新的输入实例,在训练数据集中找到与该实例最近邻的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。
KNN使用的模型实际上对应于特征空间的划分,没有显式的训练过程。

二、KNN三要素

  • 距离度量
    特征空间中两个实例点的距离是两个实例点相似程度的反映。设输入实例 x ∈ R n x\in \mathbb{R}^n xRn x i x_i xi x j x_j xj L p L_p Lp距离定义为:
    L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i l − x j l ∣ p ) ( 1 p ) L_p(x_i,x_j)=(\sum_{l=1}^n|x_i^{l}-x_j^{l}|^p)^{(\frac{1}{p})} Lp(xi,xj)=(l=1nxilxjlp)(p1)
    当p为1时成为曼哈顿距离,当p为2是为欧氏距离,当p为正无穷大时为各个坐标轴距离的最大值。不同的p值可能导致最近邻点的选取不同。
  • K 值
    较小的的k值,就相当于用较小的领域中的训练实例进行预测,近似误差会减小,只有与输入实例较近的训练实例才会对预测结果起作用。但是误差估计会增大,预测结果会对近邻的实例点非常敏感。k值的减小意味着模型变得复杂,容易发生过拟合。
    较大的k值回减小学习的估计误差,但是学习的近似误差会变大。这时与输入实例较远的训练实例也会对最终判断起影响。k值的增大意味着模型变简单。
    在应用中,k值一般取一个比较小的数,通常采取交叉验证法来选取最优的k值。
  • 分类决策规则
    一般采用多数表决规则。

三、kd树

kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的二叉树结构,表示对l维空间的一个划分。构造kd树相当于不断地用垂直于坐标轴的超平面将l维空间切分,构成一系列的l维超矩形区域。kd树的每个结点对应于一个k维超矩形区域。

构造平衡kd树的算法

输入:k维空间数据集 T = { x 1 , x 2 , . . , x N } T=\{x_1,x_2,..,x_N\} T={x1,x2,..,xN},其中 x i = ( x i ( 0 ) , x i ( 1 ) , . . . , x i ( l − 1 ) ) x_i=(x_i^{(0)},x_i^{(1)},...,x_i^{(l-1)}) xi=(xi(0),xi(1),...,xi(l1)) l l l维向量。
输出:kd树。

1.开始:构造根结点,根结点对应于包含T的l维空间的超矩形区域。
选择 x ( 0 ) x^{(0)} x(0)为坐标轴,以T中所有实例在该坐标轴上的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并于坐标轴 x ( 0 ) x^{(0)} x(0)垂直的超平面实现。由根结点生成深度为1的左、右子结点,分别对应坐标轴 x ( 0 ) x^{(0)} x(0)小于和大于切分点的子区域,将落在切分超平面上的实例点保存在根结点

2.对深度为j的结点,选择 x j   m o d   l x^{j\bmod l} xjmodl为切分坐标轴(也可选择方差最大的坐标轴),重复1所述的切分过程。

3.直到两个子区域没有实例存在时停止,从而形成kd树的区域划分。

kd树的最近邻搜索

输入:kd树,目标点x;

输出:x的最近邻;

  1. 在kd树中从根结点出发,递归向下查找包含目标点x的叶结点;
  2. 以此叶结点为当前最近点;
  3. 递归向上回退,在每个结点进行以下操作:
    • 如果该结点保存到实例点比当前最近点距离目标点更近,则以该实例点位当前最近点。
    • 检查该结点的另一子结点是否与以目标点为球心,以目标点与当前最近点间的距离为半径的超球体相交。如果相交,则移动到另一子结点进行递归搜索,,否则向上回退。
    • 当回退到根结点时,搜索结束,当前最近点即为x的最近邻点。

四、代码实现

代码如下(示例):

"""
k近邻法的实现:kd树
"""

import numpy as np
from sklearn.datasets import load_digits
import heapq

from tqdm import tqdm


class Node(object):
    '''
    树节点
    '''
    def __init__(self, data, left, right,  dim):
        self.data = data
        self.left = left
        self.right = right
        self.dim = dim  # 该节点划分数据的维度


class KDTree(object):
    '''
    KDTree的构造和搜索
    '''
    def __init__(self, datas):
        self.root = self.build_tree(datas, 0, datas.shape[1]-1)

    def build_tree(self, datas, cur_dim, max_dim):
        '''
        构建kd树
        :param datas: 特征数据
        :param cur_dim: 当前节点的划分维度
        :param max_dim: 最大划分维度
        :return:
        '''
        if len(datas) == 0:
            return
        datas = datas[np.argsort(datas[:, cur_dim])]  # 按照当前维度的特征对实例进行排序
        mid = datas.shape[0] // 2
        return Node(datas[mid], self.build_tree(datas[:mid], (cur_dim+1) % max_dim, max_dim),
                    self.build_tree(datas[mid+1:], (cur_dim+1) % max_dim, max_dim), cur_dim)

    def search(self, x, k, lp_dis):
        '''
        在kd树中查找与实例x距离最近的k个实例点
        '''
        top_k = [(-np.inf, None)] * k

        def visit(node):
            if node is None:
                return
            dis_with_axis = x[node.dim] - node.data[node.dim]  # 待查点与切分轴的距离
            visit(node.left if dis_with_axis < 0 else node.right)

            dis_with_node = lp_dis(x.reshape((-1,)), node.data.reshape((-1,))[:-1])
            heapq.heappushpop(top_k, (-dis_with_node, node.data[-1]))
            # 如果当前已知的最近的k个点中最远的点距离查询点的距离(-top_k[0][0])大于查询点到当前节点分割超平面的距离(abs(
            # dis_with_axis)),那么我们可以断定,在当前节点的另一个子树中可能存在距离查询点更近的点
            if -top_k[0][0] > abs(dis_with_axis):
                visit(node.right if dis_with_axis < 0 else node.right)

        visit(self.root)

        top_k = [int(x[1]) for x in heapq.nlargest(k, top_k)]

        return top_k


class KNN(object):
    '''
    Knn算法的具体实现
    '''
    def __init__(self, k, train_x, train_y, p=2):
        self.train_x = train_x
        self.train_y = train_y
        self.k = k
        self.p = p
        self.kdtree = None

    def lp_distance(self, x1, x2):
        '''
        L_p距离
        '''
        distance = np.sum(np.abs(x1 - x2)**self.p, -1)**(1/self.p)
        return distance

    def vote(self, top_k):
        '''
        多数表决
        '''
        count = {}
        max_freq, predict_y = 0, 0
        for key in top_k:
            count[key] = count.get(key, 0) + 1

            if count[key] > max_freq:
                max_freq = count[key]
                predict_y = key

        return predict_y

    def kdtree_search(self, test_x):
        '''
        kdtree搜索
        '''
        if self.kdtree is None:
            self.kdtree = KDTree(np.concatenate([self.train_x, self.train_y.reshape((-1, 1))], -1))

        predict_ys = []
        for x in tqdm(test_x):
            top_k = self.kdtree.search(x, self.k, self.lp_distance)
            predict_y = self.vote(top_k)
            predict_ys.append(predict_y)

        predict_ys = np.array(predict_ys)
        return predict_ys


if __name__ == '__main__':
    digits = load_digits()
    features = digits.data
    target = digits.target

    shuffle_indices = np.random.permutation(features.shape[0])
    features = features[shuffle_indices]
    target = target[shuffle_indices]

    train_count = int(len(features)*0.8)
    train_x, test_x = features[:train_count], features[train_count:]
    train_y, test_y = target[:train_count], target[train_count:]

    k = 5
    p = 2
    knn = KNN(k, train_x, train_y, p)

    predict_y = knn.kdtree_search(test_x)

    accuracy = (predict_y == test_y).sum() / test_y.shape[0]

    print('Accuracy:%.4f' % accuracy)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值