KNN算法和KD树

KNN算法和KD树

KNN算法的思路非常简单,对于新的样本,找出距其最近的k个样本,再根据这k个样本的类别,通过多数投票的方式预测新样本的类别。k近邻算法没有学习或训练过程。但k近邻算法仍有很多值得关注的地方,比如超参数k值的选择、距离的度量方式、决策规则以及快速检索k近邻的算法(kd树等)。

1. KNN算法的三要素

KNN算法的流程非常简单,确定一个KNN算法,明确下来三个基本要素即可。即k值的选择、距离的度量方式和决策规则。

1.1 k值的选择

在KNN算法中k值即表示对于一个新的样本,从训练集中选择和新样本距离最近的k个样本,用这k个样本决定新样本的类别。k值作为一个超参数,很慢明确给出一个合适的值,k取的过大或过小都会导致算法误差增大。一般可以采用留取验证集的方式确定k值的大小。即选择使得验证集准确率最高的k值。

1.2 距离度量方式

计算样本之间的距离时,有不同的距离计算方式,常用是欧式距离,也可以采用更一般的 L p 距离。

L p 距离: L p ( x i , x j ) = ( k n | x i k x j k | p ) 1 p

欧式距离是 L p 距离的特殊情况,即 p = 2 时:

欧式距离: L 2 ( x i , x j ) = k n | x i k x j k | 2

另外,还有曼哈顿距离(也称之为街区距离),也是 L p 距离的特殊情况,即 p = 1 时的情况。

曼哈顿距离: L 1 ( x i , x j ) = k n | x i k x j k |

p =

L p ( x i , x j ) = ( k n | x i k x j k | ) 1 = max k | x i k x j k |

选择不同的距离度量方式,最终结果也是不一样的,一般情况下都是选择欧式距离。

1.3 决策规则

决策规则就是指如何利用训练集中离新样本最近的k个样本决定。默认一般都是利用多数投票规则(即选择k个样本所属类别最多的类),因此很容易忽略决策规则的重要性。如果你愿意,你也可以选择其它的决策规则。

1.4 KNN算法求解

通过前面的解释,将KNN算法用代码解释如下:

问题描述

训练数据集(n个样本): D = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x n , y n ) ) }
新的样本: x

需要明确新样本 x 的类别

train_data # 训练样本
train_label #训练样本的标签类别,标签类别和样本一一对应,即trian_data[i]的标签为train_label[i]
distances = []
for i in range(0, len(train_data)):
    distances.append(distance(x,train_data[i])) #计算样本x和每个训练样本的距离
nn = np.argsort(distances) #对distances进行排序,并返回排序后的索引值
y = decision_rule(nn[0:k-1]) #用k近邻和响应的决策规则确定样本x的类别

根据上面的算法解释,如果训练样本数量很少时,是可行的,线性扫描一次计算消耗的计算量不算太大。但如果训练样本的数据量很大时呢?每个新样本,都需要和所有训练样本计算距离,计算距离后还需要排序,无论是时间复杂度还是空间复杂度都是很高,样本数量很大时,不能够接受这么高的复杂度。那么有没有更加高效的方式呢?这里我有一些个人的见解,比如最小堆(减少排序时间和排序所需要的空间)、训练数据排序等。

最小堆

在前面的算法流程中,保存了样本 x 和所有训练样本的距离,然后再排序,再取前 k 个训练样本做决策。但其实可以维护一个大小为 k 的最小堆。对于每次距离的计算,只需要更新该最小堆就好了。即可已节约内存空间(不需要保存所有的距离),也可以减少计算量。

对训练数据排序

最初步的想法是,对先对训练数据做排序,对于排好序的训练数据,每次检索时,就可以用二分查找法,就可以找到最近邻的样本,而k近邻的,就在最近邻的附近。但其实仔细一想,这是没法操作的,依据什么来排序呢?难道根据训练数据到新样本的距离排序?Are you kidding me? 这不就是最原始的算法吗?naive了。

那么实际情况下,应该怎么做呢?常见的是KD树。

2. KD树

前面提了一些我自己的想法改进检索策略的思路,其实KNN算法有更加详细好用的搜索算法,即kd树,kd树表示k-dimensional tree。需要注意的是,kd树中的k和knn中的k含义完全不一样,kd树中的k如英文名中的含义,表示k维。

kd树的想法是先用训练数据构建一颗查找树,我前面对数据进行排序的想法其实也是想干这个,但是我自己想不到这么深。构建了树之后,再对树进行搜索,可以大大节约搜索的时间。用kd树解决KNN问题的主要两点就是:1)构建kd树;2)如何利用kd树搜索。

2.1 构建kd树

kd树的构建比较简单,从1~k维(样本 x i 的维度为k)依次将数据集划分为左右两个节点,不断的递归进行划分,直到所有训练样本都被划分了为止。

构建kd树的流程
  1. 选择 x ( 1 ) 作为初始的划分坐标轴,计算所有训练样本在坐标轴 x ( 1 ) 下的中位点(奇数个样本,则对应中间位置的数,偶数个样本,则对应中间两个数的平均值),通过该中位点,可以将所有训练样本划分为两部分,该中位点作为根节点,而这两部分则作为左右子节点的划分数据样本
  2. 对于深度为 j 的节点,选择 j ( m o d ) k + 1 (根节点的深度为0)作为划分的坐标轴,同样以中位点将数据划分为两部分。
  3. 以此往复,直到没有样本可以划分了为止。

kd树的会保证每个样本都会占一个节点,即使两个点 x 1 ( 3 , 5 ) , x 2 ( 3 , 10 ) 会被同一个切分点划分?

2.2在kd树上搜索

kd树的构建是比较简单的,关键在于如何在kd树上搜索,找出k近邻的样本和最近邻的样本。

kd树搜索k近邻
  1. 对于新样本 x ,首先找到样本对应的叶节点(即使在kd树上搜索的过程,找了完全匹配的点,也要递归到叶节点)。若样本 x 当前维的坐标小于结点的坐标,则移动到左子结点,否则移动到右子结点;
  2. 将叶结点作为最近邻;
  3. 从叶结点开始回退,对回退路上的结点,判断结点和样本 x 的距离(如果还未凑齐k个近邻点,则加入;如果凑齐了,但比k近邻的点更近,那么更新k近邻结果)。
    另外对于每个回退结点,还需要判断是否要进入该结点的另外一个子结点搜索,如何判断呢?在k近邻的k个点中,必然存在一个离样本点 x 最远的点,那么以它们之间的距离作为判断准则,如果回退结点的切分坐标轴到样本 x 的距离大于这个最远距离,那么就没有必要再探索了。
  4. 一直回退到根节点为止。

猜你喜欢

转载自blog.csdn.net/b876144622/article/details/80298928