k 近邻法

作者: 千与千与 | 来源:发表于2019-01-24 11:44 被阅读1次

    k 近邻法

    • k 近邻算法
    • k 近邻模型
    • k 近邻法的实现:kd 树
    • 搜索 kd 树

    k 近邻模型实现

    • k 近邻模型实现
    • kd 树搜索单个近邻点
    • kd 树搜索 k 个近邻点

    k 近邻法(k-nearest neighbor,k-NN)是一种基本分类与回归方法。k 近邻法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其 k 个最近邻的训练实例的类别,通过多数表决等方式进行预测。因此,k近邻法不具有显式的学习过程。k 近邻法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。k 值的选择、距离度量及分类决策规则是 k 近邻法的三个基本要素。

    k 近邻算法

    1. 给定数据集 T=\{(x_1,y_1), (x_2,y_2),...,(x_N,y_N)\},其中, x_i \in X \subseteq R^n 为实例的特征向量, y_i \in Y = \{c_1, c_2, ..., c_K\} 为实例的类别, i=1,2,...,N;实例的特征向量 x
      1>> 根据给定的距离度量,在训练集 T 中找出与 x 最邻近的 k 个点,涵盖这 k 个点的 x 的邻域记作 N_k(x)
      2>> 在 N_k(x) 中根据分类决策规则(如多数表决)决定 x 的类别 y
      y = arg\ max_{c_j} \sum_{x_i \in N_i(x)}I(y_i=c_j),\ \ \ \ \ i=1,2,..,N; j=1,2,...,K
      式中I 为指示函数。

    2. k 近邻法的特殊情况是 k=1 的情形,称为最近邻算法。对于输入的实例点(特征向量)x,最近邻法将训练数据集中与 x 最邻近点的类作为 x 的类。

    k 近邻模型

    1. 模型由三个基本要素——距离度量、k 值的选择和分类决策规则决定。

    2. 特征空间中,对每个训练实例点 x_i,距离该点比其他点更近的所有点组成一个区域,叫作单元(cell)。
    1. 特征空间中两个实例点的距离是两个实例点相似程度的反映。(欧氏距离、L_p距离、Minkowski 距离)

    2. 设特征空间 Xn 维实数向量空间 R^nx_i,x_j \in Xx_i=(x_i^{(1)}, x_i^{(2)},...,x_i^{{n}})^Tx_j=(x_j^{(1)}, x_j^{(2)},...,x_j^{{n}})^Tx_i,x_jL_p 距离定义为
      L_p(x_i,x_j) = (\sum_{I=1}^n \mid x_i^{(I)} - x_j^{(I)} \mid ^p)^{\frac{1}{p}}
      这里 p \ge 1。当 p=2 时, 称为欧氏距离,即
      L_2(x_i,x_j) = (\sum_{I=1}^n \mid x_i^{(I)} - x_j^{(I)} \mid ^2)^{\frac{1}{2}}
      p=1 时, 称为哈曼顿距离,即
      L_1(x_i,x_j) = \sum_{I=1}^n \mid x_i^{(I)} - x_j^{(I)} \mid
      p=\infty,它是各个坐标距离的最大值,即
      L_\infty(x_i,x_j) = max \mid x_i^{(I)} - x_j^{(I)} \mid

    3. k 值的选择会对 k 近邻法的结果产生重大影响。如果选择较小的 k 值,就相当于用较小的邻域中的训练实例进行预测,“学习”的近似误差(approximation error)会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。换句话说,k 值的减小就意味着整体模型变得复杂,容易发生过拟合。
      如果选择较大的 k 值,就相当于用较大邻域中的训练实例进行预测。其优点是可以减少学习的估计误差。但缺点是学习的近似误差会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误。k 值的增大就意味着整体的模型变得简单。
      如果 k=N,那么无论输入实例是什么,都将简单地预测它属于在训练实例中最多的类。这时,模型过于简单,完全忽略训练实例中的大量有用信息,是不可取的。
      在应用中,k 值一般取一个比较小的数值。通常采用交叉验证法来选取最优的 k 值。

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

    k 近邻法的实现:kd 树

    1. 实现 k 近邻法时,主要考虑的问题是如何对训练数据进行快速 k 近邻搜索。k 近邻法最简单的实现方法是线性扫描(linear scan)。这时要计算输入实例与每一个训练实例的距离。当训练集很大时,计算非常耗时,这种方法是不可行的。

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

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

    4. 通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数(median)为切分点,这样得到的 kd 树是平衡的。注意,平衡的 kd 树搜索时的效率未必是最优的。

    5. 构造平衡 kd 树:
      给定 k 维空间数据集 T=\{x_1,x_2,...,x_N\},其中 x_i=(x_i^{(1)}, x_i^{(2)},...,x_i^{(k)})^Ti=1,2,...,N
      1>> 开始:构造根结点,根结点对应于包含 T 的 k 维空间的超矩形区域。选择 x^{(1)} 为坐标轴,以 T 中所有实例的 x^{(1)} 坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x^{(1)} 垂直的超平面实现。
      由根结点生成深度为 1 的左、右子结点:左子结点对应坐标 x^{(1)} 小于切分点的子区域,右子结点对应于坐标 x^{(1)} 大于切分点的子区域。
      将落在切分超平面上的实例点保存在根结点。
      2>> 重复:对深度为 j 的结点,选择 x^{(p)} 为切分的坐标轴,p=j(mod\ k)+1,以该结点的区域中所有实例的 x^{(p)} 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x^{(p)} 垂直的超平面实现。
      由该结点生成深度为 j+1 的左、右子结点:左子结点对应坐标 x^{(p)} 小于切分点的子区域,右子结点对应坐标 x^{(p)} 大于切分点的子区域。
      将落在切分超平面上的实例点保存在该结点。
      3>> 直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

    6. kd 树示例:
      给定一个二维空间的数据集:T=\{(2,3)^T,(5,4)^T,(9,6)^T,(4,7)^T,(8,1)^T,(7,2)^T\}
      根结点对应包含数据集 T 的矩形,选择 x^{(1)} 轴,6 个数据点的 x^{(1)} 坐标的中位数是 7,以平面 x^{(1)}=7 将空间分为左、右两个子矩形(子结点);接着,左矩形以 x^{(2)}=4 分为两个子矩形,右矩形以 x^{(2)}=6 分为两个子矩形,如此递归,得到如下特征空间划分图及 kd 树图。

    搜索 kd 树

    1. 在 kd 树中找出包含目标点 x 的叶结点:从根结点出发,递归地向下访问 kd 树。若目标点 x 当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶结点为止。

    2. 以此叶结点为“当前最近点”。

    3. 递归地向上回退,在每个结点进行以下操作:
      1>> 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。
      2>> 当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;如果不相交,向上回退。

    4. 当回退到根结点时,搜索结束。最后的“当前最近点”即为 x 的最近邻点。

    5. kd 树更适用于训练实例数远大于空间维数时的 k 近邻搜索。当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线性扫描。

    6. kd 树搜索示例:
      给定一个如下图所示的 kd 树,根结点为 A,其子结点为 B,C 等。树上共存储 7 个实例点;另有一个输入目标实例点 S,求 S 的最近邻。
      解:首先在 kd 树中找到包含点 S 的叶结点 D(图中的右下区域),以点 D 作为近似最近邻。真正最近邻一定在以点 S 为中心通过点 D 的圆的内部。然后返回结点 D 的父结点 B,在结点 B 的另一子结点 F 的区域内搜索最近邻。结点 F 的区域与圆不相交,不可能有最近邻点。继续返回上一级父结点 A,在结点 A 的另一子结点 C 的区域内搜索最近邻。结点 C 的区域与圆相交;该区域在圆内的实例点有点 E,点 E 比点 D 更近,成为新的最近邻近似。最后得到点 E 是点 S 的最近邻。

    k 近邻模型实现

    k 近邻模型实现

    import math
    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
    
    
    class Knn(object):
        def __init__(self, x_train, y_train, k=3, p=2):
            self.k = k
            self.p = p
            self.x_train = x_train
            self.y_train = y_train
        
        # 计算 Lp 距离
        def lp(self, x, y, p=2):
            if len(x) == len(y) and len(x) > 1:
                _sum = sum([math.pow(abs(x[i] - y[i]), p) for i in range(len(x))])
                return math.pow(_sum, 1/p)
            else:
                return 0
        
        # 预测
        def predict(self, x):
            # 计算 x 到其他节点的距离
            dists = [(np.linalg.norm(x - self.x_train[i], ord=self.p), self.y_train[i])
                     for i in range(len(x_train))]
            # 排序筛选出最近的 k 个节点
            nodes = sorted(dists, key=lambda x: x[0])[:self.k]
            # 对最近的 k 个节点的所属类别进行计数
            counter = Counter([node[-1] for node in nodes])
            # 返回计数最多的一个类别
            return counter.most_common(1)[0][0]
        
        # 计算模型的准确率
        def score(self, x_test, y_test):
            rights = 0
            for x, y in zip(x_test, y_test):
                label = self.predict(x)
                if label == y:
                    rights += 1
            return rights / len(x_test)
    
    
    if __name__ == '__main__':
        # 获取鸢尾花数据集
        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']
        # 生成训练样本,只取 sepal length,sepal width 作为样本特征
        train = np.array(df.iloc[:100, [0, 1, -1]])
        x, y = train[:, :-1], train[:, -1]
        # 划分训练集与测试集
        x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
        # 生成 knn 模型,并使用测试集进行验证
        knn = Knn(x_train, y_train)
        print('测试集验证模型准确率为:%s' % knn.score(x_test, y_test))
        # 对点 [6.0, 3.0] 进行分类
        print('点 [6.0, 3.0] 分类结果为: %s' % knn.predict([6.0, 3.0]))
        # 绘图
        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(6.0, 3.0, 'bo', label='[6.0, 3.0]')
        plt.xlabel('sepal length')
        plt.ylabel('sepal width')
        plt.legend()
    
    运行结果

    也可以使用 sklearn 实现 knn 模型

    from sklearn.neighbors import KNeighborsClassifier
    # KNeighborsClassifier 参数
    # n_neighbors:临近点个数
    # p:距离度量
    # algorithm:近邻算法,可选{'auto', 'ball_tree', 'kd_tree', 'brute'}
    # weights:近邻的权重
    knn_sk = KNeighborsClassifier()
    knn_sk.fit(x_train, y_train)
    knn_sk.score(x_test, y_test)
    

    kd 树搜索单个近邻点

    from math import sqrt
    from collections import namedtuple
    
    
    class KdNode(object):
        # 节点数据结构
        def __init__(self, node, split, left, right):
            self.node = node # k 维向量节点
            self.split = split # 分割维度序号
            self.left = left # 左树
            self.right = right # 右树
    
    
    class KdTree(object):
        def __init__(self, data):
            self.k = len(data[0]) # 数据维度
            self.root = self.create_kdnode(0, data) # 生成 kd 树
            
        def create_kdnode(self, split, data):
            if not data: return None
            data.sort(key=lambda x: x[split])
            split_index = len(data) // 2
            median = data[split_index] # 中位数分割点 
            split_next = (split + 1) % self.k
            return KdNode(
                median,
                split,
                self.create_kdnode(split_next, data[:split_index]),
                self.create_kdnode(split_next, data[split_index + 1:])
            )
        
        # 对于构建好的 kd 树 tree,寻找离 node 最近的节点
        def nearest(self, node):
            k = len(node)
            nearest_tuple = namedtuple("nearest_tuple", "nearest_node nearest_dist visited")
            
            def travel(kd_node, target, nearest_dist):
                # python中用float("inf")和float("-inf")表示正负无穷
                if kd_node is None: return nearest_tuple([0] * k, float("inf"), 0)
                
                visited = 1
                # 分割维度
                split = kd_node.split
                # 分割节点
                node = kd_node.node
    
                # 如果目标点第 split 维小于分割节点的对应值,则目标离左子树更近,否则离右子树更近
                if target[split] <= node[split]:
                    nearer_node = kd_node.left
                    further_node = kd_node.right
                else:
                    nearer_node = kd_node.right
                    further_node = kd_node.left
    
                # 进行遍历找到包含目标点的区域
                nearest_tuple_1 = travel(nearer_node, target, nearest_dist)
                nearest = nearest_tuple_1.nearest_node
                dist = nearest_tuple_1.nearest_dist
                visited += nearest_tuple_1.visited
                # 更新最近距离
                if dist < nearest_dist:
                    nearest_dist = dist
                # 第 split 维上目标点与分割超平面的距离
                temp_dist = abs(node[split] - target[split])
                # 判断超球体是否与超平面相交
                if  temp_dist > nearest_dist:
                    # 相交则可以直接返回,不用继续判断
                    return nearest_tuple(nearest, dist, visited)
                
                # 计算目标点与分割点的欧氏距离
                temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(node, target)))
                # 如果更近,更新最近距离
                if temp_dist < nearest_dist: 
                    nearest = node
                    nearest_dist = temp_dist
    
                # 检查另一个子结点对应的区域是否有更近的点
                nearest_tuple_2 = travel(further_node, target, nearest_dist) 
                visited += nearest_tuple_2.visited
                if nearest_tuple_2.nearest_dist < nearest_dist:  
                    nearest = nearest_tuple_2.nearest_node
                    nearest_dist = nearest_tuple_2.nearest_dist
                return nearest_tuple(nearest, nearest_dist, visited)
            # 从根节点开始递归
            return travel(self.root, node, float("inf"))
    
    
    if __name__ == '__main__':
        data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
        kd = KdTree(data)
        print(kd.nearest([3, 5]))
    

    运行结果

    nearest_tuple(nearest_node=[4, 7], nearest_dist=2.23606797749979, visited=4)
    

    kd 树搜索 k 个近邻点

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    from math import sqrt
    from sklearn.datasets import load_iris
    from sklearn.model_selection import train_test_split
    from collections import Counter
    
    
    class KdNode(object):
        def __init__(self, data, depth=0, lchild=None, rchild=None):
            self.data = data
            self.depth = depth
            self.lchild = lchild
            self.rchild = rchild
    
    
    class KdTree(object):
        def __init__(self):
            self.n = 0
            self.tree = None
            self.nearest = None
    
        def create(self, data_set, depth=0):
            if len(data_set) > 0:
                m, n = np.shape(data_set)
                self.n, axis, mid = n - 1, depth % (n - 1), int(m / 2)
                data_set_sorted = sorted(data_set, key=lambda x: x[axis])
                node = KdNode(data_set_sorted[mid], depth)
                if depth == 0: self.tree = node
                node.lchild = self.create(data_set_sorted[:mid], depth+1)
                node.rchild = self.create(data_set_sorted[mid+1:], depth+1)
                return node
            return None
    
        # 搜索 kdtree 的前 k 个最近点
        def search(self, x, k=1):
            nearest = []
            for i in range(k):
                nearest.append([-1, None])
            # 初始化 n 个点,nearest 是按照距离递减的方式
            self.nearest = np.array(nearest)
    
            def travel(node):
                if node is not None:
                    # 当前点的维度 axis
                    axis = node.depth % self.n
                    # x 点和当前点在 axis 维度上的差
                    daxis = x[axis] - node.data[axis]
                    # 如果小于进左子树,大于进右子树
                    if daxis < 0:
                        travel(node.lchild)
                    else:
                        travel(node.rchild)
    
                    # 计算 x 点到当前点的距离 dist
                    dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(x, node.data)))
                    for i, d in enumerate(self.nearest):
                        # 如果有比现在最近的 n 个点更近的点,更新最近的点
                        if d[0] < 0 or dist < d[0]:
                            # 插入第 i 个位置的点
                            self.nearest = np.insert(self.nearest, i, [dist, node], axis=0)
                            # 删除最后一个多出来的点
                            self.nearest = self.nearest[:-1]
                            break
                    
                    # 统计距离为 -1 的个数 n
                    n = list(self.nearest[:, 0]).count(-1)
                    # self.nearest[-n-1, 0] 是当前 nearest 中已经有的最近点中,距离最大的点
                    # self.nearest[-n-1, 0] > abs(daxis) 代表以 x 点为圆心,self.nearest[-n-1, 0]为半径的圆
                    # 与 axis 相交,说明在左右子树里面可能有比 self.nearest[-n-1, 0] 更近的点
                    if self.nearest[-n-1, 0] > abs(daxis):
                        if daxis < 0:
                            travel(node.rchild)
                        else:
                            travel(node.lchild)
    
            travel(self.tree)
            # nodes 就是最近 k 个点
            nodes = self.nearest[:, 1]
            counter = Counter([node.data[-1] for node in nodes])
            return self.nearest, counter.most_common(1)[0][0]
    
    
    if __name__ == '__main__':
        # 获取鸢尾花数据集
    
        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']
        # 生成训练样本,只取 sepal length,sepal width 作为样本特征
        data = np.array(df.iloc[:100, [0, 1, -1]])
        # 划分训练集与测试集
        train, test = train_test_split(data, test_size=0.1)
        x0 = np.array([x0 for i, x0 in enumerate(train) if train[i][-1] == 0])
        x1 = np.array([x1 for i, x1 in enumerate(train) if train[i][-1] == 1])
            
        # 生成 knn 模型,并使用测试集进行验证
        kdt = KdTree()
        kdt.create(train)
    
        score = 0
        for x in test:
            # 绘制训练点
            plt.scatter(x0[:, 0], x0[:, 1], c='pink', label='[0]')
            plt.scatter(x1[:, 0], x1[:, 1], c='orange', label='[1]')
            plt.xlabel('sepal length')
            plt.ylabel('sepal width')
             # 绘制测试点
            plt.scatter(x[0], x[1], c='red', marker='x')
            # 设置临近点的个数
            nearest, belong = kdt.search(x[:-1], 5)
            if belong == x[-1]: score += 1
            print("test:", x, "predict:", belong)
            print("nearest:", nearest)
            for near in nearest:
                # k 个近邻点
                plt.scatter(near[1].data[0], near[1].data[1], c='green', marker='+')
            plt.legend()
            plt.show()
    
        score /= len(test)
        print("score:", score)
    
    运行结果(部分截图)

    相关文章

      网友评论

        本文标题:k 近邻法

        本文链接:https://www.haomeiwen.com/subject/cijyjqtx.html