Morris的博客

唯上智与下愚不移

统计学习笔记:K近邻(KNN)原理及C源码实现

在学习李航的《统计学习方法》这本书时,遇到过不少困惑,在网上查文章向解决这些困惑,但有些东西比较模糊,或者是这里复制那里复制,而且千篇一律的是搜索最近邻,并且给的例子数据似乎都是相同的。至于搜索K个近邻,李航的《统计学习方法》也没有详细说明,网上也没看到的材料,所以决定写下这篇学习笔记,希望能给学习的人解惑,如果能解决你心中的困惑那说明这个笔记价值就有了,以下内容是对李航的《统计学习方法》中K近邻算法这一张的概括,同时也有一些自己的理解,及在实现算法过程中的问题,文末我GitHub的源码地址,你可以下载运行,算法实现K近邻,当K=1时为最近邻,结合李航的《统计学习方法》效果更好。

k-近邻法

k-近邻法(K-nearest neighbor,K-NN)是一种基本分类和回归方法,这里只介绍用于分类的k-最近邻法。k-近邻法的输入为实例的特征向量,输出为实例所属的类别,训练数据集可以包含多个类别的实例。k-近邻法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其k个最近邻(距离最近)中属于同一类个数最多的类别,将新的实例归属到该类别当中。k-近邻法没有一个显示的训练过程。

一、k-近邻法三要素

当训练数据集、距离的计算方法、k值及分类决策规则确定后,对任何一个新的输入实例,模型输出该实例所属的类别是唯一的。
1. k值的选择k值的选择会对k近邻法的结果产生重大的影响,k表示选取最近的k个近邻。
1)k值较小
    ①近似误差小:只有与输入实例较近的训练实例才会对预测结果起作用。
    ②估计误差大:预测结果会对近邻的实例非常敏感,如果近邻的点恰巧是噪声 预测就会出错。
    ③模型复杂
    ④泛化能力差
(2)k值较大
    ①近似误差大
    ②估计误差小
    ③模型简单
    ④泛化能力较好
(3)k取样本容量N
    ①无论输入实例是什么,都将简单的将输入实例预测为训练实例中占最多的类 别的那一类。
    ②忽略了大量有用的信息。
    在应用中,k值一般选取一个较小的,通常采用交叉验证法来选取最优k值。

2. 距离的计算方法
特征空间中两个实例的距离是两个实例点相似程度的反映,k近邻模型的特征向量一般是n维的,使用欧式距离,但也可以是其他距离,如更一般的Lp距离或Minkowski距离。

(1)欧式距离

(2)曼哈顿距离

(3)距离

所选用的距离表达式不同,近邻点也是不同的。

2. 分类决策(对新实例的分类规则)

①多数表决:

即由新输入实例的k个近邻训练实例中属于同一类个数占最多的那个类别 表示新输入实例的类别。

②误分类率:

设损失函数为0-1损失函数,对于给定的实例,其k个最近邻训练实例构成集合,如果涵盖的区域的类别是,那么误分类率就为

其中:

表示输入实例的目标输出;

表示模型的实际输出;

表示符号函数(括号中的表达式成立取1,否则取0);

要使误分类率最小化,就要使最大化,所以多数表决规则等价于经验风险最小化,通过调整k值来实现。

二、k近邻算法的实现

实现k近邻法时,主要考虑的问题是如何对训练数据集进行快速k近邻搜索。这点在特征空间维数大及训练数据集容量大时尤为必要。

k近邻法最简单的实现方法是线性扫描。采用这种方法要计算输入实力与每个训练实例间的距离,当训练集很大时,计算非常耗时,这种方法是不可行的。

为了提高k近邻法搜索的效率,可以考虑使用特殊的数据结构存储训练数据,以减少计算距离的次数。这里介绍kd树(kd tree)法,设特征向量维数为k,训练数据集容量为Nkd树适用于N>>2k的情况

注:在看到李航《统计学习方法》关于生成k-d树的方法时,心中有几点困惑,在这里将我的困惑说明,如果你也有一样的困惑,希望能给你帮助
(1)偶数个数据的切分点怎么选择?
(2)节点数据域存储什么?
(3)怎么判断数据落在切分超平面上?
(4)小于切分点数据归为左子树,大于切分点数据归为右子树,那么等于的该如何? 例如:
4 5 6 6 6 6 7 8
            关于上述问题本人在算法时得到解决


1. 构造kd
(1)根据训练样本集对特征向量的分量的当前所有可能取值按升序进行排序,其中l = 1,2,3,…,k(2)构建根节点,此时深度j = 0;

(3)在当前训练子集所有可能取值中选中位数作为当前切分点,其中l = ( j % k ) + 1,k表示特征向量的维数,%为取余操作:

①切分点选择优化
切分点的选择即(3)中l的取值构建开始前,对比数据点在各维度的分布情况,数据点在某一维度坐标值的方差越大分布越分散,方差越小分布越集中。从方差大的维度开始切分可以取得很好的切分效果及平衡性。

②中位数在数学中,中位数为一个有序序列的中间位置数(序列有奇数个元素)或者中间两个数的平均值(序列有偶数个元素),而在kd树中所选中位数必须包含在训练样本集合中,因此根据排序之后的序列的长度来确定中位数,设序列长度为L,则中位数i = L // 2,其中://表示整除。
例如:集合在x维从小到大排序为(2,3),(4,7),(5,4),(7,2),(8,1),(9,6);其中值为(7,2)。(注:2,4,5,7,8,9在数学中的中值为(5 + 7)/2=6,但因该算法的中值需在点集合之内,所以本文中值计算用的是L//2=3, points[3]=(7,2))。所以,k-d树生成时,中位数就是可能取值个数除以2

③中值选择优化
I、在算法开始前,对原始数据点在所有分量处进行一次排序,如步骤(1)所述,存储下来,然后在后续的中值选择中,无须每次都对其子集进行排序,提升了性能。
II、从原始数据点中随机选择固定数目的点,然后对其进行排序,每次从这些样本点中取中值,来作为分割超平面。该方式在实践中被证明可以取得很好性能及很好的平衡性。

     

(4)将当前所有与切分点相等的特征向量(落在切分超平面上的点)存储到  节点数据域,将当前选择的切分维度l保存到当前节点数据域;

注:在这一步时,李航的《统计学习方法书中》并未详细说明。对于落在切分超平面上的点其实是当前样本集合中所  有与切分点相等的点,所以一个节点存储的数据应该是多个的,但实现算法时发现也可以只存储切分点数据,这并不影响搜索结果,影响的是生成k-d树所需的内存、树的深度,以及最近邻搜索时的速度。

(5)构造左、右子节点,将当前训练数据集划分为两部分,其中左子节点对应小于切分点的训练子集,右子节点对应大于切分点的训练子集,kd树深度l + 1;

注:若(4)中采用的方式是存储多个数据,那么(5)的描述方式无误,但如果节点存储的是切分点数据,那么其实我觉得应该是要理解为“左子节点对应切分点左边的训练子集,右子节点对应右边的训练子集”。

(6)递归调用(3)~(5),直至所有训练子集为空;

 

2. 搜索kd

相比于生成k-d树的实现而言,搜索k-d树其实更为麻烦,主要是在回溯的过程中,每个节点只能遍历一次。以下是实现搜索时的几个问题:
(1)搜索叶节点时,若给定点在当前维度的分量与切分点的分量相等时应该 移动到哪个子节点?
(2)对于伪叶节点应该怎么处理?(一个子节点为空的节点)
(3)怎么判断与超球体相交,相切又该怎么处理?(4)k个近邻要怎么搜索?

以下是实现时总结的步骤:特征向量的第l个分量,l为生成k-d树节点切分点 的切分维度。

(1)从根节点开始,输入向量小于切分点时移动至左子节点,等于或大于时移动至右节点,过程中将搜索的路径记录(压栈)。

(2)判断当前节点是否的叶节点(不存在子节点的节点)或伪叶节点(只有一个子节点),若是则退出搜索。

(3)将距离初始值初始化为正无穷,从路径记录的最后一个记录(叶节点或伪叶节点)开始向根节点回溯搜索最近邻。

(4)在每个节点判断当前节点保存的数据在近邻列表中是否存在,若不存在,则计算当前叶节点存储数据与输入数据之间的距离,若距离小于当前最小距离则更新,并更新当前最近邻,否则两者都不更新。若存在则不计算。删除当前节点在搜索路径中的记录(出栈)。

(5)判断以当前最小距离为半径的超球体与节点所表示的超矩形区域是否相交。具体的,判断当前节点数据的与输入数据的差值的绝对值是否小于当前最小距离,若小于则相交,等于则相切,否则不相交。

(6)若相交,则在以当前节点的另一子节点为根的子树中一定存在比当前距离更近的点,判断是由左子节点还是右子节点移动到当前节点。具体的,对当前节点存储数据的与输入数据的进行比较,若输入小于节点则是由左 子节点移动到当前节点的,否则是右子节点移动节点移动至当前节点。判断将要移 动的子节点是否存在,若存在移动至该节点,以该节点为根节点搜索叶节点(搜索 路径叶需记录),在新的叶节点重复(4)~(7)。若不存在则移动至该节点的父 节点重复(4)~(7)。

(7)若不相交,移动至该节点的父节点重复(4)~(7),直到根节点的父节点(假想)退出搜索。

(8)重复(1)~(7)k次,则搜索到k个近邻。

 

注:每次搜索最近邻一定保证路径记录中每个节点只能出现一次,并且过程执行过(5)~(6)步的节点记录一定要删除,否则可能导致无限递归,原因在于:假设由左子节点移动至当前节点,判断在右子节点存在比当前最近邻更近的点,便会移动至右子节点进行距离和最近邻更新,当再次回溯到这个“当前”节点时,若再次进行判断可能会进入左子树从而无限递归。所以从右子树回溯回来时,应该直接移动至“当前”节点的父节点。
另外,在搜索到叶节点时不应该计算距离,应当把叶节点看成是无左右子节点的根节点进行回溯,否则在搜索k个近邻时,伪叶节点的另一个节点将搜索不到,从而导致搜索错误。

 

源码在我的GitHub上有,用C语言实现,备注很详细,运行通过。

网址:https://github.com/FAmorris/K-Nearest

装载请说明出处,请支持原创,在不收取他人费用下,可以随便传播学习,我们一起进步!

1. 参考李航《统计学习方法》

阅读更多
版权声明: https://blog.csdn.net/weixin_41592704/article/details/79977750
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

统计学习笔记:K近邻(KNN)原理及C源码实现

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭