数据应用当中,最近邻查询是非常重要的功能。不论是信息检索,推荐系统,还是数据库查询,最近邻查询(Nearst Neighbor Search)可谓无处不在。它要实现的是帮助我们找到数据中和查询最接近的一个或多个数据条目(前者叫NN search, 后者也叫kNN),其实本质上是一样的,我在这篇博客中讲的Kd-tree主要就是针对这种最近邻搜索问题。
1. 基本原理
其实,这种问题本来是很容易解决的,只要设计好了数据相似度的度量方法(有关相似度量的方法详细可参考我之前的博客:数据相似性的度量方法总结)计算所有数据与查询的距离,比较大小即可。但是随着数据量的增大以及数据维度的提高,这种方法就很难在现实中应用了,因为效率会非常低。解决此类问题的思路基本分为两类:
(1)通过构建索引,快速排除与查询相关度不大的数据;
(2)通过降维的方法,对数据条目先降维,再查询;
前者主要是为了解决数据量过大的问题,比较常见的有我们熟知的二叉搜索树,Merkel tree,B-tree,quad-tree等;后者主要是为了解决维度过大的问题,比较常见的方法有我在上一篇博客中讲的LSH:LSH(Locality Sensitive Hashing)原理与实现
而我们今天要说的Kd-tree就是一种对多维欧式空间分割,从而构建的索引,属于上面的第一类。
Kd-tree全称叫做:k dimension tree,这是一种对于多维欧式空间分割构造的的二叉树,其性质非常类似于二叉搜索树。我们先回顾一下二叉搜索树,它是一种具有如下特征的二叉树:
(1)若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左、右子树也分别为二叉搜索树;
这个概念是数据结构基础的东西,应该非常熟悉了,不再赘述,下面给出一棵普通的二叉搜索树的图:
如果我们把二叉搜索树所对应的数据集看做一个一维空间(因为这个数据集的每一个数据条目都是由一个单一的数值构成的),那么实际上二叉搜索树的分割依据就是数值的大小,这样的划分,帮助我们以平均O(lg(n))的时间复杂度搜索数据。
自然而然,我们会祥这样一个问题,能不能在多维欧式空间中,构建一棵类似原理的二叉搜索树?这也就是我们今天说的Kd-tree.
2. kd-tree的构建
先抛开搜索算法怎样设计这件事不管,我们单纯地关心怎样对多维欧式空间划分。一维空间简单,因为每个数据条目只有一个数值,我们直接比较数值大小,就能对这些数据条目划分,可是在多维空间就存在一个关键问题:每个数据条目由多个数值组成,我们怎么比较?
Kd-tree的原理是这样的:我们不比较全部的k维数据,而是选择其中某一个维度比较,根据这个维度进行空间划分。那接下来,我们需要做的是两件事:
- 判断出在哪一个维度比较,也就是说,我们所要切割的面在哪一个维度上。当然这种切割需要遵循一个基本要求,那就是尽量通过这个维度的切割,使得数据集均分(为二);
- 判断以哪个数据条目分依据划分。上面我们说,要使得数据集均分为二,那当然要选择一个合适的数据项,充当这个划分的“点”。
总结一下,就是要选择一个数据项,以这个数据项的某个维度的值为标准,同一维度的值大于这个值的数据项,划分为一部分,小于的划分为另一部分。根据这种划分来构建二叉树,就如同二叉搜索树那样。
现在,针对上面的两件事,我们需要做如下两个工作:
1. 确定划分维度:这里维度的确定需要注意的是尽量要使得这个维度上所有数据项数值的分布尽可能地有大方差,也就是说,数据在这个维度上尽可能分散。这就好比是我们切东西,如果你切的是一根黄瓜,当让横着切要比竖着切更容易。所以我们应该先对所有维度的数值计算方差,选择方差最大的那个维度;
2. 选择充当切割标准的数据项:那么只需要求得这个维度上所有数值的中位数即可;
至此,可以设计出kd-tree的构建算法了:
- 对于一个由n维数据构成的数据集,我们首先寻找方差最大的那个维度,设这个维度是 d ,然后找出在维度
d 上所有数据项的中位数 m ,按m 划分数据集,一分为二,记这两个数据子集为