K近邻算法原理及实现(Python)
来源:互联网 发布:淘宝围巾手机拍摄技巧 编辑:程序博客网 时间:2024/06/06 02:40
本文主要来自李航老师的《统计学习方法》。
算法
k近邻是一种常用的分类与回归算法,其原理比较简单:
- 输入:训练集
T={(x1,y1),(x2,y2),…,(xn,yn)} ; 待分类样本x′ ;设定好的最近邻个数k - 输出:
x′ 的类别标签 算法:
- 搜索训练集训练集
T ,根据给定的距离度量(如欧式距离),找出与x′ 距离最近的k 个点,并把涵盖这些点的领域记为Nk(x′) - 根据决策规则(如多数表决)得到
x′ 的类别y′ :y′=argmaxcj∑xi∈Nk(x′)I(yi=cj), i=1,2,…,N; j=1,2,…,M
其中
M 为Nk(x′) 包含点的类别数,I 是指示函数,即yi=cj 时,I 为1,否则为0。- 搜索训练集训练集
原理
在k近邻算法中,当训练集、最近邻值k、距离度量、决策规则等确定下来时,整个算法实际上是利用训练集把特征空间划分成一个个子空间,训练集中的每个样本占据一部分空间。对最近邻而言,当测试样本落在某个训练样本的领域内,就把测试样本标记为这一类。
度量
接近程度的度量常用的是欧式距离,也有Manhatan距离,Minkowski距离等。不同的距离度量得到的结果很可能不一样。而选择合适的距离对计算效率也很重要,如在kd树搜索过程中,把距离从欧式距离
k值选择
偏差与方差
统计学习方法中参数选择一般是要在偏差(Bias)与方差(Variance)之间取得一个平衡(Tradeoff)。
- 偏差:模型输出值与真实值之间的差异。偏差越高,则数据越容易欠拟合(Underfitting),未能充分利用数据中的有效信息。
- 方差:对数据微小改变的敏感程度。假如有一组同一类的样本,并且这些样本的特征之间只有微小差异,用训练好的模型进行预测并求得方差。理想情况下,我们应该得到的方差为0,因为我们预料我们的模型能很好处理这些微小的变化;但现实中存在很多噪声(即存在不同类别的样本,其特征向量差异很小),即使是特征差异很小的同一类样本也可能达到不同类别的结果。而方差实际上就是衡量对噪声的敏感程度。方差越高,越容易过拟合(Overfiiting),对噪声越敏感。
如何选择
同样,对knn而言,k值的选择也要在偏差与方差之间取得平衡。若k取很小,例如k=1
,则分类结果容易因为噪声点的干扰而出现错误,此时方差较大;若k取很大,例如k=N
(N为训练集的样本数),则对所有测试样本而言,结果都一样,是分类的结果都是样本最多的类别,这样稳定是稳定了,但预测结果与真实值相差太远,是偏差过大。这样k即不能取太大也不能取太小,怎么办?通常的做法是,利用交叉验证(Cross Validation)评估一系列不同的k值,选取结果最好的k值作为训练参数。
kd树
考虑这样的问题, 给定一个数据集
所谓kd树(k-dimensional tree)即k维树,是一种不断利用数据某个维度划分空间的数据结构。下面结合代码(参考自维基百科)来说明如何构造kd树:
- 先写一个树节点的结构
Node
,包含左子树、右子树、节点的包含样本的特征向量、节点样本的类别标签、划分数据的某个特征维度。 - ·
kdtree
:输入训练数据集data
(data.shape=(样本数, 特征维数)
)及类别标签labels
,选择一个划分的维度(即第axis
维特征),按该维特征排序所有数据,选择位于中点的样本x¯ 保留到该节点。 - 递归调用`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树后,还需要通过特定算法搜索与
最近邻搜索
我误解了李航老师书上的说法,以为先实现要找到叶结点,再考虑实现回退搜索。但其实从根结点开始比较,不断递归下去,自然可以得到想要的结果。其中最重要的一点是knn避免搜索那些在某一维上的距离就大于最短距离的子树,从而缩小搜索空间。
- 调用函数
nn()
输入构造好的kd树tree
(即根结点)以及待分类样本test_point
。开始时,kdtree()
参数中的best_point
及best_label
设为None
,最短距离best_dist
设为无穷。 - 从根结点开始, 计算当前结点样本特征与
test_point
的距离,并与best_dist
比较,保留距离较小的样本信息。 - 利用当前结点的划分阈值
node.node_feature[node.axis]
来向下搜索,若测试样本当前维的值test_point[axis]
小于当前节点阈值,则搜索左子树,否则,搜索右子树。 - 按照第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
)。这样我们就可以舍弃一部分区域,从而缩小了搜索空间。 - 搜索完整棵树,我们就得到了测试样本的最近邻结点及其类别标签,则测试样本的类别就是最近邻点的类别。
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树搜索的平均时间复杂度为
参考文献
- 李航, 《统计学习方法》
- kd-tree Wikipedia
- Understanding the Bias-Variance Tradeoff
- K近邻算法原理及实现(Python)
- k近邻算法及python实现
- K近邻(knn)算法简介及用python实现
- k-近邻算法(Python实现)
- K-近邻算法(kNN)python实现
- K-近邻算法python实现
- Python实现k-近邻算法
- k-近邻算法-python实现
- 机器学习:K-近邻算法原理与Python代码实现
- k-近邻算法(k-NN)及其Python实现
- 机器学习算法-K最近邻从原理到实现(Python)
- 写程序学ML:K近邻(KNN)算法原理及实现(一)
- 写程序学ML:K近邻(KNN)算法原理及实现(二)
- k-近邻算法(Python)
- K-近邻算法的Python实现(一)
- k-近邻算法 python实现(学习笔记no.1)
- KNN(k-近邻)分类算法讲解与实现(python)
- K近邻分类算法实现 in Python
- 充实的一天
- 杭电acm--2081
- Set Up VTune Amplifier(windows) 2015 for Remote (linux)Analysis
- 软件开发常用英语词汇
- 一个简单的服务器客服端通信
- K近邻算法原理及实现(Python)
- Android注释技巧
- 1203 - Argus(优先队列)
- material design(一) 源码类型大放送
- Apache Awstats 安装配置系列 (3)之 perl 安装
- 苹果Ipad锁屏密码忘记之后,如何不会变成砖
- C单链表 测试通过 路过大神指点
- 零基础学python-18.5 函数的内建工具与函数的属性
- arm指令编码格式和语法格式及其寻址方式