引言
极度快速的近似最近邻搜索算法(EFANNA)是NSG的作者之前的一篇论文,这篇论文主要介绍用更快的方法建立KNN图并且建立一个高性能的KNN图索引。这种方法建KNN图时采用类似于Wei等人提出的方案(地址),首先初始化一个KNN图,然后再使用NN-descent的方法精细化KNN图。该论文提出的方法改进了初始化KNN图的方式,使用更快的方法来初始化KNN图。在基于近似KNN图搜索时,大多方法都是随机选择入口点,该论文使用树结构而不是随机的方式来选择入口点。
简介
基于图的方法的两个问题
- 贪婪过程往往会收敛到局部最优
- 建立 $kNN$ 图非常耗时
尝试解决第一个问题的一个方法是不随机选择入口点,而是基于辅助结构(如哈希方法,树方法)为查询点提供更好的初始化入口点。
尝试解决第二个问题的很多方法是要创建近似的 $kNN$ 图,大都采用了 divide-and-conquer
策略(分治策略),这种策略主要分三步:
- 划分整个数据集为多个小的子集
- 在子集中暴力搜索获得许多重叠子图
- 归并子图,用NN-expansion的方法细化图
该论文提出的EFANNA方法的索引包括两部分:
- 多重随机分层结构
- 近似 $kNN$ 图
在离线阶段,首先,EFANNA通过快速分层的方式多次划分数据集为许多子集;接着,自底向上创建近似 $kNN$ 图,在此过程中,EFANNA利用这些结构定位可能的最近邻居,并利用这些候选来更新图,最后采用改进了类似于NN-Descent的方法来精细化索引。
在在线阶段,首先在分层结构中搜索获取查询点的候选邻居(确定入口点),然后使用贪婪方法(NN-expansion)在近似 $kNN$ 图上执行查询。
EFANNA索引构建之随机截断KD树构建算法
EFANNA索引的第一大部分为多个随机分层的结构,作者采用了随机截断的KD树(randomized truncated KD-tree)来实现这一结构,其实还有很多其它方法。作者在给定的数据集 $D$ 上建立了多个随机截断KD树。
主要思想
首先给要建立的随机截断KD树的叶子结点所能包含的数据点的个数限定一个上界 $K$ ,当输入的数据点的个数小于 $K$ 时直接返回,否则的话就进行划分,划分时随机选择一个维度,计算数据集在该维的平均值,然后根据平均值将数据集划分为两个子集,接着递归地对划分的两个子集建立随机截断KD树,直到数据集中点的个数小于 $K$。重复对数据集 $D$ 执行上述过程多次建立多个随机截断KD树。
伪代码分析
1 | //输入: |
EFANNA索引构建之近似kNN图构建算法
EFANNA索引的另一大部分就是近似kNN图了,作者采用了类似近似最近邻搜索的方法来建立近似kNN图,这个过程包括两个阶段:
- kNN图初始化
- 对初始kNN图进行细化
下面分别进行概括。
kNN图初始化之分层分治算法
作者采用分治的策略来快速获得一个初始化的kNN图。得到一个初始化的kNN图无非就是给基数据集中的每个点初始化邻居,这个过程其实就是在已经建好的几个随机截断KD树上为数据点找邻居,在这里作者采用的方法不过是“分层分治”的策略。
图1 分层分治算法图解
主要思想
对数据集中的每一个数据点,我们都要给它们初始化邻居,对某个数据点初始化邻居时,我们要在已经建立的所有树上以该数据点为查询点进行分层分治查找,也就是说在每棵树上都要查找一遍。下面只详细分析在一颗树上的分层分治过程,多棵树只是重复的过程。
为了便于分析,下面以图1的示例来说明。现在要给数据点 $q$ 找邻居(在树中找一些离它较近的数据点),从树根开始执行深度优先搜索进行查找,一直找到标号为8
的叶子结点,显然 $q$ 也处于这个结点中(整棵树涵盖了所有的数据点),这个叶子结点中的数据点加入到数据点 $q$ 的邻居候选集中,此时我们处理的是最底层(深度为3)。标号为1
、2
和4
的结点(非叶结点)是搜索的过程中遍历到的结点。接着该处理倒数第2层(深度为2)了,我们只处理该层中遍历到的结点,也就是结点4
了,其它的标号为5
、6
和7
的结点都没有遍历到。结点4
的孩子结点分别为结点8
和9
,因为结点8
已经遍历过了,此时我们只处理没有遍历过的孩子结点,因此,我们只处理结点9
,对以结点9
为根的子树进行深度优先搜索直到某个叶子结点,因为结点9
本身就是叶子结点,因此返回的叶子结点也就是它了,把该叶子结点中的数据点加入到数据点 $q$ 的邻居候选集中。接着处理第1层,同理选中结点2
,然后同理选中它的孩子结点5
,对以结点5
为根的子树执行深度优先搜索,将返回叶子结点10
(上面的局限图可以看出结点10
离 $q$ 更近),同样把该叶子结点中的数据点加入到数据点 $q$ 的邻居候选集中……
上面的那个过程可以一种处理到第0层,即根结点。但是,实际应用时这个过程是很耗时的,因为我们要对多个树重复上述过程,因此,都要处理到根结点是没必要的。在此,可以把能处理到的最小层设定为一个参数Dep
,Dep
具体值的设定可根据精度和速度的权衡来安排。
明白了上述过程,我们也就很容易明白作者给该算法命名为分层分治的原因了。
伪代码分析
1 | //输入: |
初始kNN图的精致化算法
这个过程就是对得到的初始化kNN图进行细化,使其更接近精确的kNN图,从而成为一个高质量的近似kNN图。这里有两种方法,分别为NN-expansion和NN-descent(两者区别详见),实践说明在构建kNN图方面NN-descent更有效,一句话概括它的思想就是各邻居之间更可能彼此互为邻居。
主要思想
精致化算法是在初始kNN图的基础上进行的,精致化之后将得到结果图G。一开始先将结果图初始化为预先建好的初始kNN图,对数据集中的每一个点,它在G中会有一定量的邻居(此时就是初始kNN图中它的邻居),将它的这些邻居之间互相添加为各自的邻居(添加到G中),添加后将新添加的邻居标记为new,对数据集中的所有点都执行上述过程后,对每个点保留其最近的一定量个邻居(预先设定的上界),这便是第一次迭代的过程,也是相对简单的一次。
下面来看第二次迭代。对数据集中的每一个点,它它在G中会有一定量的邻居(此时的邻居有新添加标记为new的,也有初始kNN图中没标记为new的记其标记为old),将它的这些标记为new的邻居(遍历过之后取消new标记记其标记为old)之间互相添加为各自的邻居(同样添加到G中),添加后将新添加的邻居标记为new,不仅如此,对每个标记为new的邻居,还要将所有标记为old的邻居添加到它的邻居中,同时也添加反向边(标记为new的点也要添加到标记为old的点的邻居中),对数据集中的所有点都执行上述过程后,对每个点保留其最近的一定量个邻居(预先设定的上界)。
接下来的各次迭代就和第二次类似了,可以预先设置一个合适的迭代次数。
伪代码分析
1 | //输入: |
在EFANNA索引上进行近似最近邻搜索
EFANNA的搜索算法伪代码
1 | //输入: |
注:作者的实验表明,迭代次数 $I=4$ 就足够了。
参考文献
Fu C , Cai D . EFANNA : An Extremely Fast Approximate Nearest Neighbor Search Algorithm Based on kNN Graph[J]. 2016.