参考资料:
-
https://blog.csdn.net/u011233351/article/details/85116719
做向量近似搜索,HNSW索引是目前最快召回最准的。
朴素想法
如图,在二维平面(隐去xy坐标轴)上,有13个点(也就是二维向量)。
朴素查找法
- 动机: 把空间中的点连成图,在图上做查找
- 算法
- 把某些点与点之间连上线,构成一个查找图,存下来备用;
- 当我想查找与粉色点最近的一点时,我从任意一个黑点出发,计算当前节点与粉点的距离,并计算黑点的所有邻居节点与粉点的距离,如果邻居节点距离粉点的最小值小于当前节点与粉点的距离,当前节点移动到距离粉点最近的邻居节点,否则,停止搜索。
- 举例
- 从任意一个黑色点出发,比如选C点; 计算distance(C, 粉);
- 计算C的邻居节点(A, B, I)与粉色点的距离,distance(A, 粉), distance(B, 粉), distance(C, 粉);
- distance(B, 粉)最小,且小于 计算distance(C, 粉),所以,移动到B点;
- B的邻居节点(A, C, I, E, H),分别计算他们到粉点的距离;
- distance(E, 粉点) 最小, 且小于 计算distance(B, 粉),所以,移动到E点;
- E的邻居节点(J,B,D,G),分别计算他们到粉点的距离;
- distance(J, 粉点) 最小, 且大于 计算distance(E, 粉),所以停止搜索,返回E点;
- 缺点
- 图中的K点是无法被查询到的,因为K点没有友点,怎么办?。
- 如果我们要查找距离粉色点最近的两个点,而这两个近点之间如果没有连线,那么将大大影响效率(比如L和E点,如果L和E有连线,那么我们可以轻易用上述方法查出距离粉色点最近的两个点),怎么办?
- 最后一个大问题,D点真的需要这么多“友点”吗?谁是谁的友点应该怎么确定呢?需要对超级节点进行控制
- 改进
- 关于K点的问题,我们规定在构图时所有数据向量节点都必须有友点。
- 关于L和E的问题,我们规定在构图时所有距离相近(相似)到一定程度的向量必须互为友点。
- 关于D点问题,权衡构造这张图的时间复杂度,我们规定尽量减少每个节点的“友点”数量。
NSW算法 Navigable Small World graphs
NSW朴素构图算法
- 算法
向图中逐个插入点,插图一个全新点时,通过朴素想法中的朴素查找法(通过计算“友点”和待插入点的距离来判断下一个进入点是哪个点)查找到与这个全新点最近的m个点(m由用户设置),连接全新点到m个点的连线。完了。 -
举例
对7个二维点进行构图,用户设置m=3(每个点在插入时找3个紧邻友点)。
- 初始点是A点(随机出来的),A点插入图中只有它自己,所以无法挑选“友点”。
- B点,B点只有A点可选,所以连接BA,此为第1次构造。
- F点,F只有A和B可以选,所以连接FA,FB,此为第2此构造。
- C点,同样地,C点只有A,B,F可选,连接CA,CB,CF,此为第3次构造。
- E点,E点在A,B,F,C中只能选择3个点(m=3)作为“友点”,根据我们前面讲规则,要选最近的三个,怎么确定最近呢?朴素查找!从A,B,C,F任意一点出发,计算出发点与E的距离和出发点的所有“友点”和E的距离,选出最近的一点作为新的出发点,如果选出的点就是出发点本身,那么看我们的m等于几,如果不够数,就继续找第二近的点或者第三近的点,本着不找重复点的原则,直到找到3个近点为止。由此,我们找到了E的三个近点,连接EA,EC,EF,此为第四次构造。
- 第5次构造和第6次与E点的插入一模一样,都是在“现成”的图中查找到3个最近的节点作为“友点”,并做连接。
高速公路链接
图画完了,请关注E点和A点的连线,如果我再这个图的基础上再插入6个点,这6个点有3个和E很近,有3个和A很近,那么距离E最近的3个点中没有A,距离A最近的3个点中也没有E,但因为A和E是构图早期添加的点,A和E有了连线,我们管这种连线叫“高速公路”,在查找时可以提高查找效率(当进入点为E,待查找距离A很近时,我们可以通过AE连线从E直接到达A,而不是一小步一小步分多次跳转到A)。分析
首先,我们的构图算法是逐点随机插入的,这就意味着在图构建的早期,很有可能构建出“高速公路”。假设我们现在要构成10000个点组成的图,设置m=4(每个点至少有4个“友点”),这10000个点中有两个点,p和q,他们俩坐标完全一样。假设在插入过程中我们分别在第10次插入p,在第9999次插入q,请问p和q谁更容易具有“高速公路”?答:因为在第10次插入时,只见过前9个点,故只能在前9个点中选出距离最近的4个点(m=4)作为“友点”,而q的选择就多了,前9998个点都能选,所以q的“友点”更接近q,p的早期“友点”不一定接近p,所以p更容易具有“高速公路”。结论:一个点,越早插入就越容易形成与之相关的“高速公路”连接,越晚插入就越难形成与之相关的“高速公路”连接。所以这个算法设计的妙处是降低了构图算法时间复杂度的同时还带来了数量有限的“高速公路”,加速了查找。优化
在查找的过程中,为了提高效率,我们可以建立一个废弃列表,在一次查找任务中遍历过的点不再遍历。在一次查找中,已经计算过这个点的所有友点距离查找点的距离,并且已经知道正确的跳转方向了,这些结果是唯一的,没有必要再去做走这个路径,因为这个路径会带给我们同样的重复结果,没有意义。
在查找过程中,为了提高准确度,我们可以建立一个动态列表,把距离查找点最近的n个点存储在表中,并行地对这n个点进行同时计算“友点”和待查找点的距离,在这些“友点”中选择n个点与动态列中的n个点进行并集操作,在并集中选出n个最近的友点,更新动态列表。 本质是并行查找,动态更新。
-
NSW查找步骤
注意,插入过程之前会先进行查找,所以优化查找过程就是在优化插入过程。以下给出NSW查找步骤。设待查找q点的m个近邻点。随机选一个点作为初始进入点,建立空废弃表g和动态列表c,g是变长的列表,c是定长为s的列表(s>m),将初始点放入动态列表c(附上初始点和待查找q的距离信息),制作动态列表的影子列表c'。
对动态列表c中的所有点并行找出其“友点”,查看这些“友点”是否存储在废弃表g中,如果存在,则丢弃,如不存在,将这些 剩余“友点”记录在废弃列表g中(以免后续重复查找,走冤枉路)。
并行计算这些剩余“友点”距离待查找点q的距离,将这些点及其各自的距离信息放入c。
对动态列表c去重,然后按距离排序(升序),储存前s个点及其距离信息。
查看动态列表c和c'是否一样,如果一样,结束本次查找,返回动态列表中前m个结果。如果不一样,将c'的内容更新为c的 内容,执行第2步。
插入算法更简单了,插入算法就是先用查找算法查找到m个(用户设置)与待插入点最近的点,连接它们建边,完了。
HNSW算法是对NSW算法的优化
HNSW 是将调表的思想用于对NSW的优化,所以先介绍跳表。
跳表 Skip List数据结构 是对有序链表的改进
-
自己理解
通过对有序链表建层级索引(思想上有点像二分查找)来提升链表的查找、插入、删除、更新速度。属于空间换时间的思路
三个有序链表+分层连接指针构成的跳表
跳表的构建
三个字,抛硬币。
- 对于sorted_link链表中的每个节点进行抛硬币,如抛正,则该节点进入上一层有序链 表,每个sorted_link中的节点有50%的概率进入上一层有序链表。
- 将上一层有序链表中和sorted_link链表中相同的元素做一一对应的指针链接。
- 再从sorted_link上一层链表中再抛硬币,sorted_link上一层链表中的节点有50%的可能进入最表层,相当于sorted_link中的每个节点有25%的概率进入最表层。
- 以此类推。
这样就保证了表层是“高速通道”,底层是精细查找,这个思想被应用到了NSW算法中,变成了其升级版-----HNSW。
- 跳表的实现
后续补充。
主角:HNSW算法
- 思想
把NSW图存成看作是有序链表,然后构建成多层级的跳表。
第0层中,是数据集中的所有点,你需要设置一个常数ml,通过公式floor(-ln(uniform(0,1)) x ml)来计算每个点可以出现到第几层。这些就是对应跳表中的层级建索引。 公式中x是乘号,floor()的含义是向下取整,uniform(0,1)的含义是在均匀分布中随机取出一个值,ln()表示取对数。
import math
import random
ml = 5
math.floor(-math.log(random.uniform(0,1)) * ml)
- 查找过程
- 从表层(上图中编号为Layer=2)任意点开始查找,
- 选择进入点最邻近的一些友点,把它们存储在定长的动态列表中,别忘了把它们也同样在废弃表中存一份,以防后面走冤枉路。
- 一般地,在第x次查找时,先计算动态列表中所有点的友点距离待查找点(上图绿色点)的距离,在废弃列表中记录过的友点不要计算,计算完后更新废弃列表,不走冤枉路,再把这些计算完的友点存入动态列表,去重排序,保留前k个点,看看这k个点和更新前的k个点是不是一样的,如果不是一样的,继续查找,如果是一样的,返回前m个结果。
插入构图
先计算这个点可以深入到第几层,在每层的NSW图中查找t个最紧邻点,分别连接它们,对每层图都进行如此操作,描述完毕。参数设置
- 插入时的动态列表c的大小,它的大小直接影响了插入效率,和构图的质量,size越大,图的质量越高,构图和查找效率就越低。
- 一个节点至少有几个“友点”,“友点”越多,图的质量越高,查找效率越低。作者在论文中还提到了“max友点连接数”这个参数,设置一个节点至多有多少友点,来提高查找效率,但是设的太小又会影响图的质量,权衡着来。
- 上一段中的ml也是你来控制的,设置的小了,层数就少,内存消耗少,但严重影响效率,太大了会严重消耗内存和构图时间。在论文中,作者将查找状态下的动态列表长度和插入状态下的动态列表长度做了区分,你可以通过调整他们来实现“精构粗找”或者“精找粗构”。