K近邻算法详解

来源:互联网 发布:js时间戳转换工具 编辑:程序博客网 时间:2024/06/05 21:41

K近邻算法详解

近朱者赤,近墨者黑。

  • KNN简介
  • KNN原理
  • KNN的优缺点
  • KNN的性能问题
  • kd树

KNN简介

K最近邻(k-Nearest Neighbor,KNN),是一种常用于分类的算法,是有成熟理论支撑的、较为简单的经典机器学习算法之一。该方法的基本思路是:如果一个待分类样本在特征空间中的k个最相似(即特征空间中K近邻)的样本中的大多数属于某一个类别,则该样本也属于这个类别,即近朱者赤,近墨者黑。显然,对当前待分类样本的分类,需要大量已知分类的样本的支持,因此KNN是一种有监督学习算法。

KNN原理

先举一个简单的例子:

这里写图片描述

上图中,所有样本可以使用一个二维向量表征。图中,蓝色方形样本和红色三角形样本为已知分类样本。若使用KNN对图中绿色圆形未知分类样本进行分类,当K=3时,其三近邻中有2个红色三角形样本和1个蓝色方形样本,因此预测该待分类样本为红色三角形样本;当K=5时,其三近邻中有3个红色三角形样本和2个蓝色方形样本,因此预测该待分类样本为蓝色方形样本;

综上,KNN算法有以下几个要素:


  • 数据集
  • 样本的向量表示
  • 样本间距离的计算方法
  • K值的选取

数据集:即必须存在一个样本数据集,也称作训练集,样本数据集中每个样本是有标签的,即我们知道样本数据集中每一个样本的分类。当获取到一个没有标签的待分类样本时,将待分类样本与样本数据集中的每一个样本进行比较,找到与当前样本最为相近的K个样本,并获取这K歌样本的标签,最后,选择这k个样本标签中中出现次数最多的分类,作为待分类样本的分类。因此,K近邻算法可行的基础是,存在数据集,并且是有标签数据集!

样本的向量表示:即不管是当前已知的样本数据集,还是将来可能出现的待分类样本,都必须可以用向量的形式加以表征。样本的向量表现形式,构筑了问题的解空间,即囊括了样本所有可能出现的情况。向量的每一个维度,刻画样本的一个特征,必须是量化的,可比较的。

样本间距离的计算方法:既然要找到待分类样本在当前样本数据集中与自己距离最近的K个邻居,必然就要确定样本间的距离计算方法。样本间距离的计算方法的构建,与样本的向量表示方法有关,当建立样本的向量表示方法时,必须考虑其是否便于样本间距离的计算。因此,样本的向量表示与样本间距离的计算方法,两者相辅相成。常用的距离计算方法有:欧氏距离、余弦距离、汉明距离、曼哈顿距离等等。

K值的选取:K是一个自定义的常量,是KNN算法中一个非常重要的参数。K值的选取会影响待分类样本的分类结果,会影响算法的偏差方差

偏差:模型输出值与真实值之间的差异。偏差越高,则数据越容易欠拟合(Underfitting),未能充分利用数据中的有效信息。

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

K值较小:就相当于用较小的领域中的训练实例进行预测,“学习”近似误差会减小, K值的减小就意味着整体模型变得复杂,容易发生过拟合,即增大了方差;

K值较大:就相当于用较大领域中的训练实例进行预测,其优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单。k很大,那么可以减少干扰数据的影响,但是此时就导致了系统性偏差(K值太小会造成过度拟合),比如如果取k为总的训练数据数,那么每次投票肯定都是训练数据中多的类别胜利。显然训练数据的系统性偏差会影响结果。

通常情况下,我们需要对 k 经过多种尝试,来决定到底使用多大的 k 来作为最终参数。k通常会在3~10直接取值,或者是k等于训练数据的平方根。比如15个数据,可能会取k=4。

在实际应用中,一般采用交叉验证法(简单来说,就是一部分样本做训练集,一部分做测试集)来选择最优的K值。

KNN的优缺点

优点

① 简单,易于理解,易于实现,无需参数估计,无需训练;
② 对异常值不敏感(个别噪音数据对结果的影响不是很大);
③ 适合对稀有事件进行分类;
④ 适合于多分类问题(multi-modal,对象具有多个类别标签),KNN要比SVM表现要好;

缺点

① 对测试样本分类时的计算量大,内存开销大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本;
② 可解释性差,无法告诉你哪个变量更重要,无法给出决策树那样的规则;
③ K值的选择:最大的缺点是当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。可以采用权值的方法(和该样本距离小的邻居权值大)来改进;
④ KNN是一种消极学习方法、懒惰算法。

KNN性能问题

KNN的性能问题也是KNN的缺点之一。使用KNN,可以很容易的构造模型,但在对待分类样本进行分类时,为了获得K近邻,必须采用暴力搜索的方式,扫描全部训练样本并计算其与待分类样本之间的距离,系统开销很大。

kd树

kd树是为了降低KNN的时间复杂度,提升其性能的一种数据结构,是一种二叉树。
使用 kd树,可以对n维空间中的样本点进行存储以便对其进行快速搜索。构造kd树,相当于不断地用垂直于坐标轴的超平面将n维空间进行切分,构成一系列的n维超矩形区域。kd树的每一个节点对应于一个n维超矩形区域。下表给出k-d树节点数据结构。
kd树的数据结构

字段 数据结构 描述 Node—data 数据矢量 当前节点所代表的样本数据,即n为向量 Range 空间矢量 当前节点代表的空间范围 Split 整数 与当前超平面垂直的坐标轴序号 Left kd树 当前节点左子树 Right kd树 当前节点右子树 Parent kd树 父节点(只有一个节点的kd树)

kd树的构造
kd树构造过程的伪代码:

    算法:构建k-d树(createKDTree)      输入:数据点集Data-set和其所在的空间Range      输出:Kd,类型为k-d tree      1.If Data-set为空,则返回空的k-d tree      2.调用节点生成程序:        (1)确定split域:对于所有描述子数据(特征矢量),统计它们在每个维上的数据方差。假设每条数据记录为64维,可计算64个方差。挑选出最大值,对应的维就是split域的值。数据方差大表明沿该坐标轴方向上的数据分散得比较开,在这个方向上进行数据分割有较好的分辨率;        (2)确定Node-data域:数据点集Data-set按其第split域的值排序。位于正中间的那个数据点被选为Node-data。此时新的Data-set' = Data-set \ Node-data(除去其中Node-data这一点)。      3.dataleft = {d属于Data-set' && d[split] ≤ Node-data[split]}         Left_Range = {Range && dataleft}        dataright = {d属于Data-set' && d[split] > Node-data[split]}         Right_Range = {Range && dataright}      4.left = 由(dataleft,Left_Range)建立的k-d tree,即递归调用createKDTree(dataleft,Left_Range)。并设置left的parent域为Kd;         right = 由(dataright,Right_Range)建立的k-d tree,即调用createKDTree(dataleft,Left_Range)。并设置right的parent域为Kd。  

还是给出简单的例子理解kd树的构造过程
这里写图片描述
如上图所示,留个样本点构成平面内二维的样本空间;
首先,计算样本点在x维度和y维度的方差,发现样本点x维度的方差较大,因此,需要确定一条与x轴垂直的分割线(n维空间中为超平面)对样本空间进行划分;
将样本空间中的样本点按照x周取值排序,选择中间一点,即为kd树的当前节点;
通过改点,做x轴垂线(垂直超平面),该垂线(超平面)左侧的样本点空间,构成该节点的左子树,该垂线右侧的样本点,构成该节点的右子树。
通过递归的方式,对左子树空间和右子树空间进行划分;

基于kd树的近邻搜索

① 待分类样本test_point,初始化best_dist为无穷大;
② 首先从根节点开始搜索,确定当前节点弄得,计算当前节点与test_point之间的距离;
③ 若当前节点与test_point之间的距离小于best_dist,则将当前节点与test_point之间的距离赋值给best_dist;
④ 确定当前节点的划分维度,即split;
⑤ 利用当前结点的划分阈值node.node_data[node.split]来向下搜索,若测试样本当前维的值test_point[split]小于当前节点阈值,则搜索左子树,否则,搜索右子树。
⑥ 采用递归的方式继续对左子树(或右子树)进行搜索,获得best_dist;
⑦ 至此,我们仅搜索了左子树(或右子树),另外一个子树是否要进行搜索呢?答案是需要的,因为我们并不能确定那边没有距离更近的样本点。
⑧ 那这样不是和暴力搜索没有差别了?当然不是!
⑨ 假设测试样本当前维值为test_point[split]=4, 当前结点当前维的值为node.node_data[node.split]=15,所以,我们搜索的是左子树,假设我们搜索完左子树得到的最短距离为best_dist=3, 那我们可以计算当前结点与测试样本在当前维的距离err_dist=node.node_data[split] - test_point[split], 若其大于best_dist,则不需要搜索右子树了,因为根据kd树的结构,右子树所有结点在当前维axis的值肯定大于10,则其与测试样本的距离肯定大于best_dist(因为test_point[split]是小于10的,且某一维度的差值大于best_dist,则其常用距离,不论是欧式或马氏距离,肯定是大于best_dist)。这样我们就可以舍弃一部分区域,从而缩小了搜索空间。

上述方法,将最近邻搜索的时间复杂度从暴力搜索的O(n)降低至O(log(n))

基于kd树的近邻搜索
K近邻的搜索,其实是通过上述方法,构建长度为K的有界优先队列,保存和不断的更新当前搜索过程中与待分类样本点距离最近的K个样本点的即距离。

原创粉丝点击