Skip to content

FAISS向量数据库

接触到大模型的RAG功能后,我才逐渐了解向量数据库。其中,FAISS是最古老、基础的向量数据库,有必要分析一下。

RAG和向量数据库

RAG(Retrieval-Augmented Generation,检索增强生成),结合信息检索和大模型生成的一种技术架构,通过为大模型提供知识源,来提升大模型准确度、减少大模型幻觉、提升大模型推理速度,以及让大模型能够进行“本地化”地回答。

RAG的主要流程就是“检索 - 增强 - 生成”:

  • 检索:将输入的查询转化为向量,利用向量数据库找出最接近的上下文数据
  • 增强:将检索结果与原始查询拼接,增强了大模型对于原始查询中关键词的理解
  • 生成:大模型基于原始查询和检索到的上下文数据,更快速地生成更准确、更本地化的回答。

举个例子:

类型数据
输入我的手机经常卡死,怎么解决?
检索R搜索到结果:
我的手机很容易卡死,怎么办?回答:清理手机垃圾。
我的手机很卡,如何解决?回答:清理微信缓存的图片数据。
增强A我的手机经常卡死,怎么解决?参考资料:
“我的手机很容易卡死,怎么办?回答:清理手机垃圾。”
“我的手机很卡,如何解决?回答:清理微信缓存的图片数据。”
生成G可以尝试一下清理手机垃圾,尤其是微信缓存的图片数据

这个例子中,向量数据库查询到了近似数据,大模型利用上下文得到了很多关联信息,然后用自然语言输出最终答案。而本地化体现在,如果这里提供的数据是“苹果手机知识库”,那么大模型就会回答苹果手机如何处理;如果是“华为手机数据库”,则是华为手机的解决方法。

RAG这套架构在2023年开始流行,解决了很多实际问题:

  • 大模型幻觉:当时大模型的参数量不高,幻觉问题很严重,通过提示具体上下文,能够很好地限制幻觉
  • 大模型可泛化:大模型可以使用泛化的模型,不需要费时费力自己训练一个专用的大模型
  • 知识垂直、安全、可增量更新:知识库的知识非常垂直,而且本地保存非常安全,随时可以增量更新

其中,向量数据库充当了非常重要的角色,它的查询速度和信息关联程度,很大程度上决定了整个RAG架构的速度和回答质量。

向量数据库的挑战

我们知道向量是很容易从数学上比较近似程度的,因此人们把文字、图片、视频、音频、甚至整个二进制文件这种非结构化的多模态数据,将其转换为向量存入数据库中,就能够很好地比较两份数据的近似程度了。

这种转换有两部分挑战:

  • 向量化:多模态非结构化数据,提取主要特征。这块以后有时间单独分析。
  • 检索和存储向量:这就是向量数据库的主要能力

围绕检索和存储向量,向量数据库的主要挑战就是:

  • 存储海量数据,传统索引查询高维向量数据太慢,需要更好的索引结构
  • 查询最近邻时,需要平衡或取舍准确率、速度、吞吐量

利用大模型学习FAISS主要架构

AI发展这几年,所有组件都在卷向量数据库的能力,甚至ES、Redis、Postgre这些传统数据库也都开始支持向量存储。

FAISS(Facebook AI Similarity Search)则是2015年Meta开发的一个古老的C++编写的向量数据库,用于早年间机器学习的一些应用场景,比如:

  • 推荐系统:淘宝“每日推荐”、“找相似”
  • 数据挖掘:计算与标签的匹配度
  • ……

最近FAISS能又火起来,主要是它提供了Python接口、搭建简易、支持CPU和GPU两种模式,非常利于RAG的入门和简单实现。

  • 提供python接口:意味着和LangChain这种早期RAG的具体实现组件能够很容易地结合
  • 搭建简易:完全不用单独部署一个服务,直接用磁盘和内存
  • 支持CPU和GPU:GPU资源稀缺,如果不在乎性能(入门和简单需求通常不在乎),只需要CPU就能够轻易上手

网络的知识总是过时的,而读源代码不但耗时,还可能因为不熟悉语言特性(比如这里的C++)可能在一些细节上绕圈和错误解读。现在有Cursor、Trae这种AI IDE,很容易能够借助大模型来进行最新的源码阅读:

这是著名向量数据库FAISS,用C++编写。请你阅读源代码,作为FAISS专家的身份回答我的后续问题。

trae.png

FAISS架构

faiss.png

FAISS的核心组件是:索引和量化。

  • 索引:提供向量数据存储和搜索的整体架构和流程
  • 量化:具体的压缩、编解码、计算优化

索引层

核心索引类型,提供不同的性能权衡:

索引类型搜索速度内存占用精度适用场景
IndexFlat100%小规模精确搜索
IndexIVF大规模近似搜索
IndexHNSW高性能近似搜索
IndexPQ内存受限场景
IndexBinary极低二进制向量

IVF(Inverted File)倒排文件索引

IVF索引是一种基于聚类的索引结构的索引方法。核心思想是:先划分多个聚类桶,每个桶再放置一个向量列表。

  • 插入时,先找到聚类桶,再放置进列表
  • 查询时,只需要访问与当前向量最相关的几个聚类桶,检索效率很高。

倒排列表是在Lucene、ES时代就已经存在的产物,key是分词抓到的关键词,文档为value(实际可能是文档ID、词频、位置、偏移量等),可以通过关键词快速定位文档。类似地,对于向量,key就是聚类中心,value就是向量数据。

key(聚类中心ID)value(向量ID和数据)
0[(V1, <0.1, 0.3, ...>), (V5, <0.1, 0.7, ...>)]
1[(V2, <0.5, 0.7, ...>), (V4, <0.6, 0.9, ...>)]
2[(V3, <0.8, 0.1, ...>)]

HNSW(Hierarchical Navigable Small World)分层可导航小世界索引

HNSW 索引是一种基于分层图结构的索引方法。核心思想是:分层小世界图+贪心搜索。

NSW是HNSW的前身,主要是贪心搜索:

  • 每个节点与多个邻居相连
  • 检索时从随机节点出发,通过 “贪心搜索”(每次跳向距离查询向量更近的邻居)快速逼近目标 很明显的缺点就是,数据量很大时,图会巨幅膨胀,导致贪心搜索的跳数增加,延迟变高。

trae.png

HNSW基于NSW的改进,有点像redis里面的跳表的图版本:

  • 节点层数随机生成(比如90%的节点在底层,9%在中间层,1%在顶层),大部分节点在底层,小部分节点在顶层
  • 每层都是一个独立的NSW
HNSW插入流程

插入时:根据概率公式(概率和维护的最近邻数量M有关)生成随机层高L,从顶层开始一直向下贪心遍历,从L层开始直至底层每一层都要建立连接:

  • 从当前层的入口出发,贪心地找到局部最近邻
    • 如果是顶层,则随机选择入口
    • 如果是中间层,最近邻为入口
  • 如果低于等于L层,会为该节点在该层创建一个分身节点,仅维护M个最近邻,然后与这M个最近邻建立双向连接,避免中间层图的膨胀
  • 当遍历到底层时,进行更广泛的贪心搜索,维护2*M个最近邻,建立双向连接,保证底层的稠密
  • 各层建立连接的过程中,如果被连接的节点已经达到M、2*M的上限,则会触发他们更新最近邻
HNSW查询流程

查询时:从顶层开始一直向下贪心遍历到1层,然后在最底层(0层)执行精确搜索

  • 顶层到1层,贪心地维护最近邻候选集,然后以这些候选节点为种子继续下一层遍历,候选集数量会在贪心后保持不变
  • 进入到最底层,精确地维护最近邻候选集,这一层需要精确计算向量之间的距离,然后返回TopK

量化层

量化层是FAISS的核心组件之一,主要负责向量压缩和距离计算优化。

向量压缩是指,将高维浮点向量压缩为低比特编码,大幅减少内存占用,支持十亿级向量存储。 距离计算优化是指,通过查表加速距离计算,避免直接计算高维向量距离,同时支持非对称距离计算(ADC)。

量化器压缩率精度速度适用场景
乘积量化器 ProductQuantizer
分段量化,平衡精度和压缩率
大规模相似搜索
标量量化器 ScalarQuantizer
每维度独立量化
最快简单量化需求
加性量化器 AdditiveQuantizer
多层量化叠加
高精度需求
残差量化器 ResidualQuantizer
残差量化
残差编码场景
RaBit量化器 RaBitQuantizer
快速二进制量化
极高超大规模数据

乘积量化器PQ

PQ是最经典和常用的量化器,它将高维向量空间划分为多个子空间,在每个子空间内进行独立的标量量化,然后将各个子空间的量化结果组合起来表示原始向量。

PQ量化过程

trae.png 假设向量库中的所有向量都是N=128维,我们指定参数M=4,意味着高维向量会被划分为4段,每一段是32维。然后对于每个分段进行聚类,聚合256(固定经验值)个簇心,每个向量对应M=4个簇心的ID,将浮点数的N维向量表示成了整型的簇心ID(0~255,uint8),实现了降维。

PQ计算距离过程

trae.png 将查询输入的向量进行同样的量化处理,将128维的向量分成4段32维子向量,然后每个段与对应段的全部256个簇心计算距离,得到Mx256的距离表。对于库中的每个向量,利用簇心ID对这个查询向量和簇心的距离表进行反查,将得到的距离加起来,便是与查询向量之间的距离。

PQ将float向量降维还有一个重要的能力:实现了归一化。归一化在机器学习里面的重要性不言而喻,可以参考之前写过的一篇文章《特征归一化特性及其数学原理推导》。这里通过将浮点数任意的数据限制到0~255,实际上就是实现了归一化。

此外可以预见,对于PQ的性能,N的维度越高,这种降维查询的提效就越大;但库中存储的数据越多,因为是反查,遍历查询的效率就会越低。因此量化和索引是结合起来使用的,例如IndexIVFPQ = IndexIVF + ProductQuantize,通过倒排索引来减少存储库中向量的个数,再利用PQ算法提供快速的距离计算,既避免了倒排索引计算距离时间复杂度高的弊端、又避免了PQ在数据量大时遍历数据太多的弊端。

实际运用

这里只描述一下过程。我们有个场景需要对“用户反馈数据”进行分类,比如情感分类、功能点分类。一开始想到的是传统的机器学习方法,比如“随机森林”、“神经网络”,但实际使用下来并不好,因为这类特定领域的反馈数据的数据量非常少,因此分类的准确率极低。

后面考虑使用大模型+RAG的模式,大模型输入了非常多的“常识”,足够对分类进行判别。同时给予少量的人工标注的“用户反馈数据”,来提升对特定领域的认知。

  • 大模型(LLM):大模型使用的是2张L20卡运行的“Qwen3-8B”,使用vllm搭建。8B的参数量已经能涌现足够的智能了。
  • 向量化(embedding):采用常规的文本向量化组件SentenceTransformer:all-MiniLM-L6-V2
  • 向量数据库(VectorDB):采用的是FAISS,因为容易上手。正在考虑迁移到Milvus,因为FAISS有个实际运用才会发现的弊端是——不方便在线增减数据。
  • RAG:采用的是LangChain搭建,比较方便。后面在考虑使用SpringAI,毕竟主语言是Java。

整体效果非常好,相比于传统机器学习方案有质的提升。

转载请注明出处https://bananaoven.com/articles/58733.html | 香蕉微波炉
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。