机器学习算法之K近邻算法(K Nearest Neighbors, KNN)

KNN算法的原理

概念

K近邻算法(K Nearest Neighbour) 是常见的用于监督学习的算法。它的原理比较简单:

给定测试样本,基于某种距离找到训练集中与其最近的 K 个训练样本,然后基于这 K 个邻居的信息来预测。

KNN既可以用于分类问题,也可以用于回归问题:

分类 :投票法,选择K个邻居中出现次数最多的类别作为预测结果。

回归:可使用平均法,即将K个邻居的标记的平均值作为预测输出。也可以使用基于距离的加权平均,比如把距离的倒数作为权重进行加权运算。

距离

KNN用的是欧氏距离,设有两个 n n n 维的样本 x = ( x 1 , x 2 , . . , x n ) , y = ( y 1 , y 2 , . . . , y n ) \bm x=(x_1,x_2, .., x_n), \bm y=(y_1, y_2, ..., y_n) x=(x1,x2,..,xn),y=(y1,y2,...,yn), 它们之间的欧式距离表示为:
d = ∑ i = 1 n ( x i − y i ) 2 d=\sqrt{\sum_{i=1}^{n}{(x_i-y_i)^2}} d=i=1n(xiyi)2

K值的选择

  1. 如果选择较小的K值,就相当于用较小的领域中的训练实例进行预测,“学习”近似误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是“学习”的估计误差会增大,换句话说,K值的减小就意味着整体模型变得复杂,容易发生过拟合;
  2. 如果选择较大的K值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单。
  3. K=N,则完全不足取,因为此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的累,模型过于简单,忽略了训练实例中大量有用信息。

在实际应用中,K值一般取一个比较小的数值,例如采用交叉验证法(简单来说,就是一部分样本做训练集,一部分做测试集)设定不同的K来做多次测试,从而来选择最优的K值。

如何求最近的K个结点的距离

线性扫描

通常可以计算所有训练样本到测试样本的距离,把训练样本按到测试样本的距离从宏观小到大排序,选择前 K K K 个即可。

大顶堆优化

但是这样的话排序的时间复杂度比较大,可以用一个 大顶堆 来优化,先压入 K K K 个元素进入堆中,接下来每计算一个新的训练样本到测试样本的距离,就把该距离和 堆顶元素 相比较,若小于 堆顶元素 就把 堆顶元素 对应的样本弹出,把新的这个训练样本压入堆。这样就可以减少排序的复杂度。

kd tree 优化

即使使用 大顶堆 来优化,还是需要计算每个训练样本和测试样本的距离,当样本数量巨大时,计算次数将会十分巨大,时间复杂度也很大。kd tree 就是一种对训练样本进行重新存储进而减少距离计算次数的二叉树。通常对于训练样本数为 m, 维度为 d 的训练集, kd tree 适用于 m ≫ 2 d m \gg 2^d m2d 的情况。

kd tree的原理

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

kd tree的建立

常规的kd tree的构建过程为:循环依序取数据点的各维度来作为切分维度,取数据点在该维度的中值作为切分超平面,将中值左侧的数据点挂在其左子树,将中值右侧的数据点挂在其右子树。递归处理其子树,直至所有数据点挂载完毕。

通常轮次选择坐标轴对空间进行切分,选择训练样本在当前坐标轴上的中位数(median)作为切分点,这样得到的树是平衡的,但是,平衡的kd树未必是效率最优的kd树

假设在2维空间中有这么一些训练样本:

D = { ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) } D=\{(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)\} D={(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}

以下两图来自于维基百科 k-d tree。我们的样本数据有两个维度,交替使用这两个维度来划分空间。先基于第一个维度,也就是横坐标 x , 所有样本的横坐标集合是 { 2 , 5 , 9 , 4 , 8 , 7 } \{2,5,9,4,8,7\} {259487},显然 它们的中位数是7,以 ( 7 , 2 ) (7, 2) (7,2) 作为根结点,沿垂直当前维度也就是 x x x 轴方向过 ( 7 , 2 ) (7, 2) (7,2) 作一条垂直于当前维度的超平面,由于是二维空间,超平面退化为一条直线。超平面以左的点,也就是数据点当前维度的元素小于7的作为 ( 7 , 2 ) (7, 2) (7,2) 的左子树,超平面以右的点作为它的右子树。这是kd树的第一层。

然后基于第二个维度,也就是 y 轴,分布对 ( 7 , 2 ) (7, 2) (7,2) 的左子树 { ( 2 , 3 ) , ( 5 , 4 ) , ( 4 , 7 ) } \{(2, 3), (5, 4), (4, 7)\} {(2,3),(5,4),(4,7)} 和右子树 { ( 9 , 6 ) , ( 8 , 1 ) } \{(9, 6), (8, 1)\} {(9,6),(8,1)} 做类似的划分。对于左子树,各样本点纵坐标的中位数是 4,因此 ( 5 , 4 ) (5, 4) (5,4) 是中间点,作为 ( 7 , 2 ) (7, 2) (7,2) 的左子树的根结点。同理, ( 9 , 6 ) (9, 6) (9,6) 是右子树的根结点。过 ( 5 , 4 ) (5, 4) (5,4) ( 9 , 6 ) (9, 6) (9,6) 分别作垂直于当前维度y轴的直线,直到与其它超平面相交为止。

最后,对剩下的三个叶子结点在 x 轴维度上做超平面。至此完成对kd树的建立。
kd tree1
kd tree2

kd tree的最近邻搜索

假设我们现在有一个k维空间中的目标点 p 和一棵 kd树,可以按照如下方法寻找 kd树 中距离 p 最近的点。

  1. 在kd树中找到包含目标的叶子结点。方法是,从根结点出发,若 p 的当前维坐标小于根结点的对应坐标,则去左结点查找,否则去右结点,直到抵达叶子结点为止。此时说明 p 位于该叶子结点的超空间内。以这个叶子结点为当前最近点
  2. 递归向上回退访问,在每个结点进行以下操作之一:
    a. 如果当前结点距离 p 更近,则更新为当前最近点。然后接着向上回退。
    b. 否则, 当前最近点一定位于当前结点的某个子结点所在超空间,检查当前结点的另一个子结点所在空间是否有距离 p 更近的结点。具体操作就是,以目标点p当前最近点的距离为半径作一个超球面,看超球面是否与经过当前结点的超平面相交,若相交,说明该区域可能存在距离更近的结点,则先要递归访问当前结点的另一个子结点之后才能继续向上回退。若不相交,则接着向上回退。
  3. 当回退到根结点时,搜索结束,最后的当前最近点即为 p 的最近点。

伪代码如下:

struct Node{
	int dim; // 当前维度
	vector<int> vec;  // 样本点数据
	Node* left = nullptr;  // 左孩子结点
	Node* right = nullptr;  // 右孩子结点
	Node* father = nullptr;  // 父亲结点
}
void search(Node* root, Node* aim, Node* &bestNeighbor, double& minDistance){
 	/*
 	root: 当前访问的树的根结点
 	aim: 目标结点
 	bestNeighbor: 离目标结点最近的结点
 	minDistance: bestNeighbor离目标结点的距离
	*/
	Node* now = root;  // now表示当前结点 
	// 找到 aim所在超矩形区域的叶子结点
	while(now->left != nullptr || now->right != nullptr){
		if(root->vec[root->dim] < aim->vec[root->dim]){
			now = root->right;
		}else{
			now = root->left;
		}
	}
	minDistance = calcDistance(now, aim);
	bestNeighbor = now;
	// 从叶子结点开始向上回退
	while(now != root){
		now = now->father;
		double tempDistance =  calcDistance(now, aim);
		if(tempDistance > minDistance){
			// 更新当前最近点
			bestNeighbor = now;
		}else{
			double radius = calcDistance(bestNeighbor, aim);
			double superSurfaceToAim = calcDistToSuperSurface(now, aim);
			// 超球面和过当前点的超平面相交
			if(radius > superSurfaceToAim){
				if(now->vec[now->dim] > aim->vec[now->dim]){
					// 如果本来在左子树, 那么递归访问右子树
					search(now->right, aim, bestNeighbor,minDistance);
				}else{
					// 如果本来在右子树, 那么递归访问左子树
					search(now->left, aim, bestNeighbor,minDistance);
				}
				
			}
			
		}
		
	} // end while
	// 判断 root是不是最近的结点
	double distance = calcDistance(now, aim);
	if(distance < minDistance){
		minDistance = distance;
		bestNeighbor = root;
	}
}

我做了个动态示意图, 下图中 S S S 是我们要找的点, A A Akd树的根结点,黄色圆圈表示该结点是当前结点, 橘黄色的字母表示该结点是当前最近点, 蓝色的边是超平面,红色的圈是超球面
寻找最近邻的过程是这样的,先找到以A 为根结点的树中所属超矩形空间包含S的叶子结点 D。从 D 开始回退,回退到 B 时发现,此时当前最近点D, 超球面当前点 B 的父亲结点 A 所在超平面相交。于是在以 A 的另一个孩子结点 C 为根结点的子kd树中递归寻找最近邻点。在 C 为根结点的子kd树中,先找到 叶子结点 E, 然后开始回退访问 C, 此时当前最近点仍然是 E, 这段子递归结束。最后回退 A, 此时当前最近点仍然是 E。最后我们寻找的距离 S 最近的点就是 E
kd树最近邻搜寻示意图

KNN算法的优缺点

优点

  1. 简单易用
  2. 没有显式的训练过程,在训练过程中仅仅是把训练样本保存起来,训练时间开销为0,是懒惰学习(lazy learning) 的著名代表 。
  3. 预测效果好
  4. 对异常值不敏感

缺点

  1. 对内存要求较高,因为该算法存储了所有训练数据
  2. 预测阶段可能很慢
  3. 对不相关的功能和数据规模敏感

Reference

《机器学习》,周志华著
《统计机器学习》,李航著
CSDN-zhiyong_will: 数据结构和算法——kd树
磊磊落落的博客《k-d tree算法原理及实现》
维基百科 k-d tree

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值