深入解析HNSW:Faiss中的层次化可导航小世界图

image.png

层次化可导航小世界(HNSW)图是向量相似性搜索中表现最佳的索引之一。HNSW 技术以其超级快速的搜索速度和出色的召回率,在近似最近邻(ANN)搜索中表现卓越。尽管 HNSW 是近似最近邻搜索中强大且受欢迎的算法,但理解其工作原理并不容易。

本文旨在揭开 HNSW 的神秘面纱,并以易于理解的方式解释这种智能算法。在文章的最后,将探讨如何使用 Faiss 实现 HNSW,并讨论哪些参数设置可以实现所需的性能。

HNSW的基础

我们可以将ANN算法分为三个不同的类别;树、哈希和图。HNSW属于图类别。更具体地说,它是一个基于接近度的图,其中两个顶点根据它们的接近度(更接近的顶点被连接)连接——通常在欧几里得距离中定义。

从“接近度”图到“层次可导航的小世界”图的复杂度有显著的飞跃,将描述两种对HNSW贡献最大的基本技术:概率跳表和可导航的小世界图。

概率跳表

概率跳表由William Pugh在1990年引入,它结合了排序数组的快速搜索能力和链表的便捷插入操作。

跳表通过构建多个层的链表来工作。在最高层,链接能够跳过许多中间节点。在较低层,链接的“跳跃”数量逐渐减少。

要在跳表中进行搜索,从最高层开始,沿着边缘向右移动。如果发现当前节点的“键”大于目标键,表示已经超出目标,于是向下移动到下一层继续搜索。

image.png

HNSW继承了相同的分层格式,最高层有更长的边(用于快速搜索)和较低层有更短的边(用于准确搜索)。

可导航的小世界图Navigable Small World Graphs

可导航小世界图(Navigable Small World Graphs,简称NSW)是一种用于向量搜索的高效数据结构,其概念最早在2011至2014年间的学术论文中被提出。这种图经过巧妙地设计,结合了长程和短程链接的特性,使得搜索过程的时间复杂度显著降低。

在NSW图中,每个节点(或称为顶点)都与若干其他节点相连,这些相连的节点被称为“朋友”。每个节点维护着一个朋友列表,共同构成了整个图的结构。

进行NSW图搜索时,搜索过程遵循以下步骤:

  1. 从预定义的起点出发:选择一个起点,该点与多个相邻节点相连。
  2. 局部邻近性识别:在这些相邻节点中,识别出与查询向量最为接近的一个节点。
  3. 逐步逼近目标:移动到该节点,并重复上述过程,逐步缩小搜索范围,直至找到最接近查询向量的节点。

image.png

在可导航小世界图(Navigable Small World Graphs,简称NSW)中,搜索过程通过一种称为贪婪路由的方法实现,这种方法通过逐步优化来逼近目标顶点。具体步骤如下:

  1. 贪婪路由搜索:从任意顶点开始,识别朋友列表中与查询向量最近的相邻顶点,然后转移到该顶点。这个过程重复进行,直到找到一个局部最小值,即当前顶点比之前访问的任何顶点都更接近查询向量,此时停止搜索。
  2. 局部最小值作为停止条件:当搜索达到一个局部最小值时,认为已经找到了足够接近查询向量的顶点,从而结束搜索过程。
  3. 网络的可导航性定义:NSW图被定义为能够在多项式或对数时间复杂度内,通过贪婪路由有效搜索的网络结构。
  4. 贪婪路由的效率问题:在大型网络(顶点数量在1到10K以上)中,如果图的结构不可导航,贪婪路由的效率可能会显著下降。
  5. 路由的两个阶段
    • 缩小阶段:在搜索初期,优先通过度数较低的顶点进行路由,这有助于快速缩小搜索范围。
    • 放大阶段:随着搜索的深入,逐渐转向度数较高的顶点进行路由,这有助于在局部区域内进行更细致的搜索。

image.png

高度顶点有许多链接,而低度顶点链接非常少

搜索过程的有效性依赖于精心设计的停止条件和路由策略,以下是对NSW图搜索策略的优化要点:

  1. 精确的停止条件:搜索停止的条件是当在当前顶点的“朋友”列表中找不到更接近查询向量的顶点时。这种情况更可能在“缩放”阶段发生,因为在这一阶段,由于顶点的连接数较少,搜索可能过早地结束。
  2. 避免过早停止:为了减少过早停止的风险并提高搜索的召回率(即确保找到尽可能多的相关顶点),可以考虑增加顶点的平均连接度。然而,这同时会增加网络的复杂性,并可能延长搜索时间。
  3. 召回率与搜索速度的平衡:在提高召回率和保持搜索速度之间需要找到一个平衡点。这涉及到对顶点的平均度数进行优化,以确保搜索既全面又高效。
  4. 改进的搜索起点:另一种策略是从连接度较高的顶点开始搜索,即首先进入“放大”阶段。这种方法在处理低维数据时已被证明可以提高NSW图的性能。

创建HNSW

分层导航小世界图(Hierarchical Navigable Small World Graphs,简称HNSW)是可导航小世界图(NSW)的高级演变,它引入了概率跳表结构中的概率多层次概念。

HNSW通过向NSW添加层次化结构,创建一个在不同层级间具有不同链接长度的图。这种结构在最高层拥有最长的链接,在最低层则拥有最短的链接。

image.png

分层图的HNSW,最高层作为入口点,仅包含最长的链接,有助于快速跨越大范围的空间。随着向下层级的移动,链接逐渐变短且数量增多,这有助于在局部区域内进行更精细的搜索

搜索开始于最高层,利用最长的链接快速定位到可能的候选顶点。这些顶点往往是高度顶点,它们跨越多个层具有链接,这为搜索提供了一个自然的“放大”阶段。

通过贪婪路由策略,遍历每一层的链接,逐步向最近的顶点移动,直至达到局部最小值。与NSW不同,在达到局部最小值后,搜索不会停止,而是转移到当前顶点在下一层的对应点,并在那里重新开始搜索。这个过程在每一层重复进行,直到达到最底层(层0)并找到局部最小值为止。

image.png

通过 HNSW 图的多层结构的搜索过程

图构建

在图构建过程中,向量是逐个插入的,层数由参数L表示。给定层的向量插入概率由一个概率函数给出,该函数由“层乘数” m L m_L mL规范化,其中 m L = 0 m_L = ~0 mL= 0表示向量仅插入层0。

image.png

概率函数对每个层(除了层0)重复,向量被添加到其插入层以及其下的每个层

HNSW的创造者发现,当最小化跨层共享邻居的重叠时,就能获得最佳性能。减少 m L m_L mL可以有助于最小化重叠(将更多向量推到层0),但这会增加搜索过程中的平均遍历次数。因此,使用一个平衡两者的 m L m_L mL值,这个最优值的近似规则是 1 / l n ( M ) 1/ln(M) 1/ln(M)

图构建从顶部层开始,进入图后,算法贪婪地遍历边,找到插入向量q的ef最近邻居——此时 e f = 1 ef = 1 ef=1
找到局部最小值后,它移动到下一层,这个过程重复直到达到选择的插入层,这里开始构建的第二阶段。
ef值增加到efConstruction(设置的一个参数),将返回更多的最近邻居。在第二阶段,这些最近邻居是候选链接到新插入元素q以及下一层的入口点。
从这些候选者中选择M个邻居作为链接——最直接的选取标准是选择最接近的向量。
经过多次迭代后,在添加链接时还有两个参数需要考虑。 M m a x M_{max} Mmax定义了顶点可以拥有的最大链接数,以及 M m a x 0 M_{max0} Mmax0定义同样但适用于层0的顶点。

image.png

分配给每个顶点的链接数量以及M、 M m a x M_{max} Mmax M m a x 0 M_{max0} Mmax0的效果

插入的停止条件是在层0达到局部最小值。

HNSW的实现

使用Facebook AI的相似性搜索库Faiss,可以高效地实现并测试HNSW(分层导航小世界图)的不同构建和搜索参数,进而评估这些参数对索引性能的影响。
初始化HNSW索引

通过以下Python代码初始化HNSW索引:

# 初始化HNSW参数
d = 128  # 向量维度
M = 32  # 每个顶点的邻居数量index = faiss.IndexHNSWFlat(d, M)
print(index.hnsw)

在上述代码中,设置了M参数,它定义了在插入操作中每个顶点将添加的邻居数量。然而,尚未指定M_maxM_max0参数。

在Faiss库中,M_maxM_max0这两个参数在索引初始化时通过set_default_probas方法自动配置。默认情况下,M_max被设置为M的值,而M_max0则设置为M*2

构建索引

在开始使用index.add(xb)添加数据构建索引之前,注意到HNSW索引初始时没有设置层级:

# HNSW索引初始时没有层级
index.hnsw.max_level  # -1# 层级(或层次)也是空的
levels = faiss.vector_to_array(index.hnsw.levels)
np.bincount(levels)  # array([], dtype=int64)

一旦添加数据构建索引,max_level和层级信息将自动设置:

index.add(xb)# 添加数据后,层级将自动设置
index.hnsw.max_level  # 4# 层级(或层次)现在已填充
levels = faiss.vector_to_array(index.hnsw.levels)
np.bincount(levels)  # array([0, 968746, 30276,  951, 26, 1], dtype=int64)

此时,可以看到图的层级从0到4,正如max_level所描述的那样。levels数组展示每个层上的顶点分布情况。此外,还可以识别出哪个向量是作为图的入口点:

index.hnsw.entry_point  # 118295

以上是对Faiss风格的HNSW图的高层次概览。在进行索引性能测试之前,深入了解Faiss如何构建这一结构至关重要。

图结构

在初始化HNSW索引时,指定向量的维度d和每个顶点的邻居数M,这些参数用于调用set_default_probas方法,进而确定每个层级的插入概率。以下是Python中实现这一逻辑的示例:

import numpy as npdef set_default_probas(M: int, m_L: float):nn = 0  # 初始化最近邻居计数为0cum_nneighbor_per_level = []level = 0  # 从层级0开始assign_probas = []while True:# 计算当前层的概率proba = np.exp(-level / m_L) * (1 - np.exp(-1 / m_L))# 当概率低于阈值时,停止创建更多层if proba < 1e-9: breakassign_probas.append(proba)# 除层级0外,每层的邻居数为M;层级0为M*2nn += M*2 if level == 0 else Mcum_nneighbor_per_level.append(nn)level += 1return assign_probas, cum_nneighbor_per_level

此函数构建了两个列表:

  • assign_probas,表示在特定层级插入的概率
  • cum_nneighbor_per_level,表示在不同层级顶点累积的最近邻居总数
assign_probas, cum_nneighbor_per_level = set_default_probas(32, 1/np.log(32))
assign_probas, cum_nneighbor_per_level
([0.96875,0.030273437499999986,0.0009460449218749991,2.956390380859371e-05,9.23871994018553e-07,2.887099981307982e-08],[64, 96, 128, 160, 192, 224])

输出示例显示了层级0的插入概率远高于其他层级,意味着更高层级更为稀疏,这有助于减少搜索过程中陷入局部最小值的风险,并确保搜索从长距离遍历开始。
接下来,assign_probas向量被用于random_level函数,该函数为每个顶点分配一个插入层级:

def random_level(assign_probas: list, rng):f = rng.uniform()  # 从随机数生成器获取随机浮点数for level, proba in enumerate(assign_probas):if f < proba:  # 如果随机数小于层级概率return level  # 则在此层级插入f -= proba  # 否则减去概率值,尝试下一层return len(assign_probas) - 1  # 极低概率下返回最高层级

对于每个层,检查f是否小于assign_probas中为该层分配的概率——如果是,这就是插入层。
如果f太高,从f中减去assign_probas的值,并再次尝试下一个层。这种逻辑的结果是,向量最有可能在层0插入。如果不符合概率条件,将在最高层插入向量,返回len(assign_probas) - 1。如果比较Python实现和Faiss的结果,可以看到非常相似的结果:

chosen_levels = []
rng = np.random.default_rng(12345)
for _ in range(1_000_000):chosen_levels.append(random_level(assign_probas, rng))
np.bincount(chosen_levels)  # array([968821, 30170, 985, 23, 1], dtype=int64)

image.png

在Faiss实现(左)和Python实现(右)中,顶点在各个层的分布。

Faiss实现确保总是有至少一个顶点在最高层,以作为图的入口点。

HNSW性能

在深入了解了HNSW(分层导航小世界图)的理论基础和Faiss库的实现细节后,现在转向评估不同参数对HNSW索引性能的具体影响。将重点分析召回率、搜索时间、构建时间以及内存使用情况。

将调整以下三个关键参数:MefSearchefConstruction,并在Sift1M数据集上测试它们的影响。

  • M 控制每个节点的最大连接数量,影响图的密度和搜索精度。

  • efSearch 控制查询过程中候选列表的大小,影响查询时间和精度。

  • efConstruction 控制索引构建过程中候选列表的大小,影响索引构建时间和质量。

初始化索引

index = faiss.IndexHNSWFlat(d, M)

设置额外参数

index.hnsw.efConstruction = efConstruction
index.add(xb)  # 构建索引
index.hnsw.efSearch = efSearch
# 执行搜索
index.search(xq[:1000], k=1)

注意,efConstruction必须在构建索引前设置,而efSearch可以在任何时间调整。
召回率与参数的关系

通过调整参数,可以显著影响召回率(recall@1):

image.png

各种MefConstructionefSearch参数的recall@1性能

MefSearch值对召回率有显著正面影响,而合理的efConstruction值对于优化召回率同样重要。增加efConstruction可以在较低的MefSearch值下实现更高的召回率。

搜索时间与参数的权衡

尽管提高参数值可以提升召回率,但也显著增加搜索时间:

image.png

在搜索1000个查询时,各种MefConstructionefSearch参数的搜索时间(以微秒为单位),y轴使用了对数刻度

搜索时间可以从80%召回率的1毫秒变化到100%召回率的50毫秒,具体取决于参数的选择。如果对召回率的要求不是特别高,搜索时间可以降至0.1毫秒。

对于少量查询,efConstruction对搜索时间的影响不大。但当查询数量增加时,即使是小的efConstruction值变化也可能导致搜索时间的显著增加。
如果查询任务主要是低频的,增加efConstruction参数可以提高召回率,而对搜索时间的影响很小,特别是在使用较低的M值时。

image.png

当只搜索一个查询时,efConstruction和搜索时间。当使用较低的M值时,对于不同的efConstruction值,搜索时间几乎保持不变

内存使用情况

最后,HNSW索引的内存使用情况也是一个重要考量:

image.png

使用Sift1M数据集增加M值时的内存使用情况。efSearchfConstruction对内存使用没有影响

efSearchefConstruction不影响内存使用,而M的值对内存使用有直接影响。即使是较小的M值,索引的大小也可能迅速增加,这可能导致较高的基础设施成本。即使M的值只有2,索引大小已经超过0.5GB,当M为512时,接近5GB。因此,需要权衡高内存使用和由此产生的不可避免的高基础设施成本。

改善内存使用和搜索速度

虽然HNSW索引在内存利用率方面不是最高效的,但如果内存优化是关键需求,可以通过一些策略来改善这一状况。以下是几种提升HNSW性能的方法:

  • 使用乘积量化(PQ)压缩:乘积量化(PQ)是一种向量压缩技术,可以在保持相对较高召回率的同时减少内存占用。通过应用PQ,可以在牺牲一定召回率和增加搜索时间的代价下,显著降低内存使用。
  • 加速搜索的策略:若目标是提升搜索速度,可以考虑在HNSW索引中集成倒排文件(IVF)组件。IVF通过聚类技术减少搜索空间,从而加快搜索速度。
  • 混合使用索引技术:混合使用IVF和PQ等技术可以提供更多的灵活性和性能优化空间。

参考

  • HNSW教程
  • https://youtu.be/QvKMwLjdK-s
  • ANN Benchmarks
  • Skip lists: a probabilistic alternative to balanced trees
  • Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs
  • Approximate Nearest Neighbor Search Small World Approach
  • Scalable Distributed Algorithm for Approximate Nearest Neighbor Search Problem in High Dimensional General Metric Spaces
  • Approximate nearest neighbor algorithm based on navigable small world graphs
  • Navigability of complex networks
  • Growing homophilic networks are natural navigable small worlds
  • Faiss HNSW Implementation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/48250.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详解SVN与Git相比存在的不足

原文全文详见个人博客&#xff1a; 详解SVN与Git相比存在的不足截至目前&#xff0c;我们已既从整理梳理的SVN和Git在设计理念上的差异&#xff0c;也重点对二者的存储原理和分支管理理念的差异进行深入分析。这些差异也直接造成了SVN和Git在分支合并、冲突解决、历史记录管理…

山西大学—双一流大学,考数据结构+C语言。山西大学计算机考研考情分析!

山西大学&#xff08;Shanxi University&#xff09;&#xff0c;位于山西省太原市&#xff0c;是中国办学历史最悠久的高等学府之一&#xff0c;是国家“双一流”建设高校&#xff0c;教育部和山西省人民政府共同建设的“部省合建高校”&#xff0c;山西省重点建设大学&#x…

JVM监控及诊断工具-命令行篇-jstack命令介绍

加粗样式 JVM监控及诊断工具-命令行篇04-jstack&#xff1a;打印JVM中线程快照 一 基本情况二 基本语法 一 基本情况 jstack(JVM Stack Trace)&#xff1a; 用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。 线程快照就是当前虚拟机内指定进程的每一条线程正在执…

新手小白的pytorch学习第七弹------分类问题模型

目录 1. 准备分类数据1.1 输入和输出的形状 shape1.2 将数据转换为张量&#xff0c;同时将我们的数据集转换为训练集和测试集 2 创建模型方法一&#xff1a;自定义forward()方法二&#xff1a;nn.Sequential()方法三&#xff1a;自定义forward()nn.Sequential() 用 pytorch 使用…

基于A律压缩的PCM脉冲编码调制通信系统simulink建模与仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1A律压缩的原理 4.2 PCM编码过程 4.3 量化噪声与信噪比 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#…

python项目读取oracle数据库方法(cx_Oracle库实现)

目录 创建一个python项目&#xff0c;并配置运行环境 查看oracle对应数据库版本&#xff08;该标题下内容只是为了查看版本&#xff0c;不用在意&#xff09; 从oracle官网下载对应版本的oracle客户端 解压下载的压缩包&#xff0c;并获取依赖 将依赖文件导入python项目运…

82. UE5 RPG 实现角色升级系统(下)

书接上回&#xff0c;在上一篇博客里&#xff0c;我们实现了角色升级的基础的功能。给敌人增加的经验奖励配置&#xff0c;并且在敌人死亡时&#xff0c;能够将经验通过事件传递给击杀者&#xff0c;玩家定义了被动技能&#xff0c;在被动技能中接收传递的事件&#xff0c;通过…

iOS 开发包管理之CocoaPods

CocoaPods&#xff08;Objective-C 时期&#xff0c;支持Objective-C和swift&#xff09;&#xff0c;CocoaPods下载第三方库源代码后会将其编译成静态库.a 文件 或动态库框架.framework 文件 的形式&#xff0c;并将它们添加到项目中&#xff0c;建立依赖关系&#xff0c;这种…

Redis实现用户会话

1.分布式会话 (1)什么是会话 会话Session代表的是客户端与服务器的一次交互过程&#xff0c;这个过程可以是连续也可以是时断时续的。曾经的Servlet时代&#xff08;jsp&#xff09;&#xff0c;一旦用户与服务端交互&#xff0c;服务器tomcat就会为用户创建一个session&#…

开源PDF解析工具marker 和 MinerU的解析效果对比

RAG中的文档解析需求&#xff1a;需要的是文档的完整段落&#xff0c;标题&#xff0c;图片&#xff0c;表格。我们希望删除的是md格式&#xff0c;或者josn格式。 MinerU 和 maker恰好。都是能够满足此需求的开源工具。这篇文章分享一下对两者的对比。整理出来目前还存在的问题…

RPG素材Unity7月20闪促限时4折游戏开发资产兽人角色模型动画休闲放置模板物理交互流体水下焦散VR界面UI2D模板场景20240720

今天这个是RPG素材比较多&#xff0c;还有一些休闲放置模板、FPS场景素材、角色模型、动画、特效。 详细内容展示&#xff1a;www.bilibili.com/video/BV1Tx4y1s7vm 闪促限时4折&#xff1a;https://prf.hn/l/0eEOG1P 半价促销&#xff1a;https://prf.hn/l/RlDmDeQ 7月闪促…

谷粒商城实战-Vue学习过程中踩坑记录

一&#xff0c;自闭合的<script>标签 第一次使用Vue&#xff0c;按照步骤引入vue.js&#xff0c;创建div&#xff0c;创建Vue对象&#xff0c;但是未达预期效果。 插值表达式{{name}}没被替换为data对象中的属性值。 F12看了下网页源代码&#xff0c;发现创建Vue对象的…

OpenAI突发新模型GPT-4o mini,GPT-3.5退役!

OpenAI突发新模型&#xff0c;全面取代老去的GPT-3.5——GPT-4o mini&#xff01; 免费用户已可使用GPT-4o mini模型。 GPT-4o mini&#xff0c;能力接近原版GPT-4&#xff0c;价格却要便宜一个数量级&#xff1a; GPT-4o mini:每百万输入tokens&#xff0c;15美分&#xff0…

【Node.js基础03】利用http模块创建Web服务

一&#xff1a;使用步骤 1 加载http模块&#xff0c;并创建Web服务程序 2 利用Web服务程序监听request事件&#xff0c;设置响应头和响应体 3 配置端口号并启动Web服务 4 浏览器请求设置的端口号&#xff0c;进行Web服务程序测试 二&#xff1a;简单应用 const http requir…

基于多线程延迟排序的睡眠排序算法的创新与改进

基于多线程延迟排序的睡眠排序算法的创新与改进 摘要 本文在传统睡眠排序算法的基础上&#xff0c;提出了一种改进方案&#xff0c;旨在优化处理负数和大规模数据集的性能。通过引入线程池管理和数据分段排序技术&#xff0c;改进后的算法在处理大数据集和包含负数的数据集时…

计算机网络入门 -- TCP详解

计算机网络入门 – TCP详解 1.TCP协议 1.1 报文格式 1.32位序号&#xff1a;该条TCP数据携带的起始序号。 2.32位确认序号&#xff1a;期望对方发送数据从那个序号开始发送。 3.4位首部长度&#xff1a;最大为0xF(15)&#xff0c;指的是TCP头部长度。 首部长度 4 位首部长…

谷粒商城实战笔记-37-前端基础-Vue-基本语法插件安装

文章目录 一&#xff0c;v-model1&#xff0c;双向绑定2&#xff0c;vue的双向绑定2.1 html元素上使用指令v-model2.2 model中声明对应属性2.3&#xff0c;验证view绑定modelmodel绑定view 完整代码 二&#xff0c;v-on1&#xff0c;指令简介2&#xff0c;在button按钮中添加v-…

rimraf快速删除node_modules方法

项目中&#xff0c;有时候会遇到下载依赖报错&#xff0c;然后想要删除node_modules再重新下载&#xff0c;但是有时候直接用yarn 或者npm install仍热不行&#xff0c;我们可以尽量用yran&#xff0c;因为npm 可能会自动下一些给一些包升级了&#xff0c;此时因为前面已经下过…

JVM:GraalVM

文章目录 一、介绍1、什么是GraalVM&#xff1a;2、GraalVM版本 二、两种使用模式 一、介绍 1、什么是GraalVM&#xff1a; GraalVM是Oracle官方推出的一款高性能JDK&#xff0c;使用它享受比OpenJDK或者OracleJDK更好的性能。GraalVM的官网地址&#xff1a;https://www.graa…

泛型新理解

1.创建三个类&#xff0c;并写好对应关系 package com.jmj.gulimall.study;public class People { }package com.jmj.gulimall.study;public class Student extends People{ }package com.jmj.gulimall.study;public class Teacher extends People{ }2.解释一下这三个方法 pub…