K近邻算法原理及实现(Python)

来源:互联网 发布:淘宝围巾手机拍摄技巧 编辑:程序博客网 时间:2024/06/06 02:40

本文主要来自李航老师的《统计学习方法》。

算法

k近邻是一种常用的分类与回归算法,其原理比较简单:


  • 输入:训练集T={(x1,y1),(x2,y2),,(xn,yn)}; 待分类样本x;设定好的最近邻个数k
  • 输出:x的类别标签
  • 算法:

    1. 搜索训练集训练集T,根据给定的距离度量(如欧式距离),找出与x距离最近的k个点,并把涵盖这些点的领域记为Nk(x)
    2. 根据决策规则(如多数表决)得到x的类别y
      y=argmaxcjxiNk(x)I(yi=cj), i=1,2,,N; j=1,2,,M

    其中MNk(x)包含点的类别数, I是指示函数,即yi=cj时,I为1,否则为0。


原理

在k近邻算法中,当训练集、最近邻值k、距离度量、决策规则等确定下来时,整个算法实际上是利用训练集把特征空间划分成一个个子空间,训练集中的每个样本占据一部分空间。对最近邻而言,当测试样本落在某个训练样本的领域内,就把测试样本标记为这一类。
knn划分空间

度量

接近程度的度量常用的是欧式距离,也有Manhatan距离,Minkowski距离等。不同的距离度量得到的结果很可能不一样。而选择合适的距离对计算效率也很重要,如在kd树搜索过程中,把距离从欧式距离i(xiyi)2换成i(xiyi)2,, 在某一维的距离从xiyi换成(xiyi)2,就能节省计算量。

k值选择

偏差与方差

统计学习方法中参数选择一般是要在偏差(Bias)与方差(Variance)之间取得一个平衡(Tradeoff)。

  • 偏差:模型输出值与真实值之间的差异。偏差越高,则数据越容易欠拟合(Underfitting),未能充分利用数据中的有效信息。
  • 方差:对数据微小改变的敏感程度。假如有一组同一类的样本,并且这些样本的特征之间只有微小差异,用训练好的模型进行预测并求得方差。理想情况下,我们应该得到的方差为0,因为我们预料我们的模型能很好处理这些微小的变化;但现实中存在很多噪声(即存在不同类别的样本,其特征向量差异很小),即使是特征差异很小的同一类样本也可能达到不同类别的结果。而方差实际上就是衡量对噪声的敏感程度。方差越高,越容易过拟合(Overfiiting),对噪声越敏感。

如何选择

同样,对knn而言,k值的选择也要在偏差与方差之间取得平衡。若k取很小,例如k=1,则分类结果容易因为噪声点的干扰而出现错误,此时方差较大;若k取很大,例如k=N(N为训练集的样本数),则对所有测试样本而言,结果都一样,是分类的结果都是样本最多的类别,这样稳定是稳定了,但预测结果与真实值相差太远,是偏差过大。这样k即不能取太大也不能取太小,怎么办?通常的做法是,利用交叉验证(Cross Validation)评估一系列不同的k值,选取结果最好的k值作为训练参数。

kd树

考虑这样的问题, 给定一个数据集D和某个点x,找出与x距离最近的k个点。这是一类很常见的问题,最简单的方法是暴力搜索,直接线性搜索所有数据,直到找到这k个点为止。对少量数据而言,这种方法还不错,但对大量、高纬度的数据集来说,这种方法的效率就差了。我们知道,排序好的数据查找起来效率很高,所以,可以利用某种规则把已有的数据“排列”起来,再按某种特定的搜索算法,以达到高效搜索的目的。“kd树”这是这样一种方法。

所谓kd树(k-dimensional tree)即k维树,是一种不断利用数据某个维度划分空间的数据结构。下面结合代码(参考自维基百科)来说明如何构造kd树:

  1. 先写一个树节点的结构Node,包含左子树、右子树、节点的包含样本的特征向量、节点样本的类别标签、划分数据的某个特征维度。
  2. ·kdtree:输入训练数据集data(data.shape=(样本数, 特征维数))及类别标签labels,选择一个划分的维度(即第axis维特征),按该维特征排序所有数据,选择位于中点的样本x¯保留到该节点。
  3. 递归调用`kdtree,输入位于x¯左边的数据及类别标签构造左子树;输入位于x¯右边边的数据及类别标签构造右子树。
class Node(namedtuple('Node', 'left_child right_child node_feature node_label axis')):    def __repr__(self):        pformat(tuple(self))def kdtree(data, labels, depth=0):    assert(data.shape[0]==labels.shape[0])    k = data.shape[1]    axis = depth % k    if data.shape[0] < 1:        return None    elif data.shape[0] == 1:        return Node(left_child=None, right_child=None,                    node_feature=data[0], node_label=labels[0],                    axis=axis)    sorted_idx = data[:, axis].argsort()    sorted_data = data[sorted_idx]    sorted_labels = labels[sorted_idx]    median = data.shape[0]//2    return Node(left_child=kdtree(sorted_data[:median], sorted_labels[:median], depth+1),                right_child=kdtree(sorted_data[median+1:], sorted_labels[median+1:], depth+1),                node_feature=sorted_data[median], node_label=sorted_labels[median], axis=axis)def construct_kdtree(data, labels):    data = np.atleast_2d(data)    assert(data.shape[0] == labels.shape[0])    return kdtree(data, labels)

搜索kd树

构造完kd树后,还需要通过特定算法搜索与x距离最近的k个点。下面结合代码说明如何搜索kd树:

最近邻搜索

我误解了李航老师书上的说法,以为先实现要找到叶结点,再考虑实现回退搜索。但其实从根结点开始比较,不断递归下去,自然可以得到想要的结果。其中最重要的一点是knn避免搜索那些在某一维上的距离就大于最短距离的子树,从而缩小搜索空间。

  1. 调用函数nn()输入构造好的kd树tree(即根结点)以及待分类样本test_point。开始时,kdtree()参数中的best_pointbest_label设为None,最短距离best_dist设为无穷。
  2. 从根结点开始, 计算当前结点样本特征与test_point的距离,并与best_dist比较,保留距离较小的样本信息。
  3. 利用当前结点的划分阈值node.node_feature[node.axis]来向下搜索,若测试样本当前维的值test_point[axis]小于当前节点阈值,则搜索左子树,否则,搜索右子树。
  4. 按照第3步的方法,我们只搜索了特定的左(或右)子树,那另一边的子树要不要搜索呢?当然是需要的,因为我们并不能确定那边没有距离更近的样本点。那这样不是和暴力搜索没有差别了?当然不是,假设测试样本当前维值为test_point[axis]=5, 当前结点当前维的值为node_feature[axis]=10,所以,我们搜索的是左子树,假设我们搜索完左子树得到的最短距离为best_dist=3, 那我们可以计算当前结点与测试样本在当前维的距离err_dist=node.node_feature[axis] - test_point[axis], 若其大于best_dist,则不需要搜索右子树了,因为根据kd树的结构,右子树所有结点在当前维axis的值肯定大于10,则其与测试样本的距离肯定大于best_dist(因为test_point[axis]是小于10的,且某一维度的差值大于best_dist,则其常用距离,不论是欧式或马氏距离,肯定是大于best_dist)。这样我们就可以舍弃一部分区域,从而缩小了搜索空间。
  5. 搜索完整棵树,我们就得到了测试样本的最近邻结点及其类别标签,则测试样本的类别就是最近邻点的类别。
def get_distance(a, b):    return np.linalg.norm(a-b)def nn_search(test_point, node, best_point, best_dist, best_label):    if node is not None:        cur_dist = get_distance(test_point, node.node_feature)        if cur_dist < best_dist:            best_dist = cur_dist            best_point = node.node_feature            best_label = node.node_label        axis = node.axis        search_left = False        if test_point[axis] < node.node_feature[axis]:            search_left = True            best_point, best_dist, best_label = nn_search(test_point, node.left_child,                                                          best_point, best_dist, best_label)        else:            best_point, best_dist, best_label = nn_search(test_point, node.right_child,                                                          best_point, best_dist, best_label)        if np.abs(node.node_feature[axis] - test_point[axis]) < best_dist:            if search_left:                best_point, best_dist, best_label = nn_search(test_point, node.right_child,                                                  best_point, best_dist, best_label)            else:                best_point, best_dist, best_label = nn_search(test_point, node.left_child,                                                  best_point, best_dist, best_label)    return best_point, best_dist, best_labeldef nn(test_point, tree):    best_point , best_dist, best_label = nn_search(test_point, tree, None, np.inf, None)    return  best_label

k近邻搜索

k近邻搜索与最近邻搜索大致相同,只是需要一个 有界优先队列(Bounded Priority Queue, BPQ), BPQ是按某个权重保留最优的k个元素的一种数据结构。在搜索过程中,我们一直维持着最大长度为k的BPQ,BPQ保存找到的与测试样本距离最近的k个点;搜索结束后,BPQ中的样本点就是测试样本的k个最近邻点。实现与最近邻基本相同,唯一需要注意的是,BPQ不满或当前结点与测试样本在当前维的距离小于最好距离时需要搜索另一侧的子树。

class BPQ:    def __init__(self, length=5, hold_max=False):        self.data = []        self.length = length        self.hold_max = hold_max    def append(self, point, distance, label):        self.data.append((point, distance, label))        self.data.sort(key=itemgetter(1), reverse=self.hold_max)        self.data = self.data[:self.length]    def get_data(self):        return [item[0] for item in self.data]    def get_label(self):        labels = [item[2] for item in self.data]        uniques, counts = np.unique(labels, return_counts=True)        return uniques[np.argmax(counts)]    def get_threshold(self):        return np.inf if len(self.data) == 0 else self.data[-1][1]    def full(self):        return len(self.data) >= self.lengthdef knn_search(test_point, node, queue):    if node is not None:        cur_dist = get_distance(test_point, node.node_feature)        if cur_dist < queue.get_threshold():            queue.append(node.node_feature, cur_dist, node.node_label)        axis = node.axis        search_left = False        if test_point[axis] < node.node_feature[axis]:            search_left = True            queue = knn_search(test_point, node.left_child, queue)        else:            queue = knn_search(test_point, node.right_child, queue)        if not queue.full() or np.abs(node.node_feature[axis] - test_point[axis]) < queue.get_threshold():            if search_left:                queue = knn_search(test_point, node.right_child, queue)            else:                queue = knn_search(test_point, node.left_child, queue)    return queuedef knn(test_point, tree, k):    queue = BPQ(k)    queue = knn_search(test_point, tree, queue)    return queue.get_label()

搜索效率

kd树搜索的平均时间复杂度为logN.。所以,当样本较小时,kd树搜索与暴力搜索接近;但当数据量很大,kd树搜索能节省很多搜索时间。一般来说,k维的数据,数据集超过2k时,kd树能表现的比较好。

参考文献

  1. 李航, 《统计学习方法》
  2. kd-tree Wikipedia
  3. Understanding the Bias-Variance Tradeoff
0 0
原创粉丝点击