KNN算法/HNSW算法


高维稀疏数据进行快速相似查找,可以采用learning to hash,但高维稠密数据查找则采用annoy

learning to hash 参考:

https://blog.csdn.net/hero_fantao/article/details/70245284
《海量数据相似查找系列1 -- Minhashing & LSH & Simhash 技术汇总》

Annoy以及其他KNN算法可以看下面:

参考这篇文章:

https://zhuanlan.zhihu.com/p/152522906

《K近邻算法哪家强?KDTree、Annoy、HNSW原理和使用方法介绍》

关于KNN算法,一个核心问题是:如何快速从数据集中找到和目标样本最接近的K个样本?

本文将从这个角度切入,介绍常用的K近邻算法的实现方法。具体将从原理、使用方法、时间开销和准确率对比等方面进行分析和实验。

KNN算法的三要素:距离度量、k值的选择和分类决策规则。

其中机器学习领域常用的距离度量方法,有欧式距离、余弦距离、曼哈顿距离、dot内积等

主流的近邻算法都支持上述不同的距离度量。其中n维特征空间的a、b向量的欧式距离 [公式] 体现数值上的绝对差异,而余弦距离基于余弦相似度(两个向量间夹角的余弦值),体现方向上的相对差异。如果对向量做归一化处理,二者的结果基本是等价的。

首先最直观的想法(暴力法),是线性扫描法。将待预测样本和候选样本逐一比对,最终挑选出距离最接近的k个样本即可,时间复杂度O(n)。

常用的存储结构可以分为树和图两大类。树结构的代表是KDTree,以及改进版BallTree和Annoy等;基于图结构的搜索算法有HNSW等。

KDTree

KD树的构造:

一颗构建好的KD-Tree中,任何一个非叶节点都有一个特征空间切割维度,在这个维度上,所有左子树的节点都小于它,所有右子树的节点都大于等于它;

构建的步骤如下:

  1. 计算数据集在特征空间每个维度上的方差,将方差最大的维度作为当前的分割维度;
  2. 将数据集在分割维度上排序后,找到中位数,假设在排序的数据集中位置为m;
  3. 构建节点,保存中位数和当前分割维度;
  4. 将数据集中在当前维度上小于中位数(从0到m-1)作为左子树的数据集;大于等于中位数(从m+1到最后)的数据作为右子树的数据集;重复上面的步骤,继续构建左右子树;直到整个KD-Tree构建完成;

KD树的查找:

从root节点开始,DFS搜索直到叶子节点,同时在stack中顺序存储已经访问的节点。
如果搜索到叶子节点,当前的叶子节点被设为最近邻节点。
然后通过stack回溯:
如果当前点的距离比最近邻点距离近,更新最近邻节点.
然后检查以最近距离为半径的圆是否和父节点的超平面相交.
如果相交,则必须到父节点的另外一侧,用同样的DFS搜索法,开始检查最近邻节点。
如果不相交,则继续往上回溯,而父节点的另一侧子节点都被淘汰,不再考虑的范围中.
当搜索回到root节点时,搜索完成,得到最近邻节点。

kd树在维数小于20时效率最高,一般适用于训练实例数远大于空间维数时的k近邻搜索;当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线形扫描。

Annoy

annoy全称“Approximate Nearest Neighbors Oh Yeah”

是一种适合实际应用的快速相似查找算法。Annoy 同样通过建立一个二叉树来使得每个点查找时间复杂度是O(log n),和kd树不同的是,annoy没有对k维特征进行切分。

annoy的每一次空间划分,可以看作聚类数为2的KMeans过程。收敛后在产生的两个聚类中心连线之间建立一条垂线(图中的黑线),把数据空间划分为两部分。

 

直到每个子空间最多只剩下K个数据节点,划分结束。

 

最终生成的二叉树具有如下类似结构,二叉树底层是叶子节点记录原始数据节点,其他中间节点记录的是分割超平面的信息。

在 ANN 领域,最常见的两个问题是:

  1. 如果我们想要 Top K 的点,但是该区域的点集数量不足 K,该怎么办?
  2. 如果真实的 Top K 中部分点不在这个区域,该怎么办?

作者用了两个技巧来解决这个问题:

    1. 使用优先队列(priority queue):将多棵树放入优先队列,逐一处理;并且通过阈值设定的方式,如果查询的点与二叉树中某个节点比较相似,那么就同时走两个分支,而不是只走一个分支;
    2. 使用森林(forest of trees):构建多棵树,采用多个树同时搜索的方式,得到候选集 Top M(M > K),然后对这 M 个候选集计算其相似度或者距离,最终进行排序就可以得到近似 Top K 的结果。

最后提一点,annoy接口中一般需要调整的参数有两个:查找返回的topk近邻和树的个数。一般树越多,精准率越高但是对内存的开销也越大,需要权衡取舍(tradeoff)。

HNSW

和前几种算法不同,HNSW(Hierarchcal Navigable Small World graphs)是基于图存储的数据结构。

假设我们现在有13个2维数据向量,我们把这些向量放在了一个平面直角坐标系内,隐去坐标系刻度,它们的位置关系如上图所示。
朴素查找法:不少人脑子里都冒出过这样的朴素想法,把某些点和点之间连上线,构成一个查找图,存储备用;当我想查找与粉色点最近的一点时,我从任意一个黑色点出发,计算它和粉色点的距离,与这个任意黑色点有连接关系的点我们称之为“友点”(直译),然后我要计算这个黑色点的所有“友点”与粉色点的距离,从所有“友点”中选出与粉色点最近的一个点,把这个点作为下一个进入点,继续按照上面的步骤查找下去。如果当前黑色点对粉色点的距离比所有“友点”都近,终止查找,这个黑色点就是我们要找的离粉色点最近的点。

HNSW算法就是对上述朴素思想的改进和优化。为了达到快速搜索的目标,hnsw算法在构建图时还至少要满足如下要求:1)图中每个点都有“友点”;2)相近的点都互为“友点”;3)图中所有连线的数量最少;4)配有高速公路机制的构图法。

 HNSW低配版NSW论文中配了这样一张图,短黑线是近邻点连线,长红线是“高速公路机制”,如此可以大幅减少平均搜索的路径长度。

 在NSW基础之上,HNSW加入了跳表结构做了进一步优化。最底层是所有数据点,每一个点都有50%概率进入上一层的有序链表。这样可以保证表层是“高速通道”,底层是精细查找。通过层状结构,将边按特征半径进行分层,使每个顶点在所有层中平均度数变为常数,从而将NSW的计算复杂度由多重对数复杂度降到了对数复杂度。

小结

本文介绍了几种常用的k近邻查找算法,kdtree是KNN的一种基本实现算法;考虑到并发、延时等要素,annoy、hnsw是可以在实际业务中落地的算法,其中bert/sentence-bert+hnsw的组合会有不错的召回效果。