当前位置: 首页 > news >正文

20240316-1-向量化搜索

向量化搜索

在高维空间内快速搜索最近邻(Approximate Nearest Neighbor)。召回中,Embedding向量的搜索。

FAISS、kd-tree、局部敏感哈希、【Amnoy、HNSW】

FAISS

faiss是Facebook的AI团队开源的一套用于做聚类或者相似性搜索的软件库,底层是用C++实现。Faiss因为超级优越的性能,被广泛应用于推荐相关的业务当中。

faiss工具包一般使用在推荐系统中的向量召回部分。在做向量召回的时候要么是u2u,u2i或者i2i,这里的u和i指的是user和item。我们知道在实际的场景中user和item的数量都是海量的,最容易想到的基于向量相似度的召回就是使用两层循环遍历user列表或者item列表计算两个向量的相似度,但是这样做在面对海量数据是不切实际的,faiss就是用来加速计算某个查询向量最相似的topk个索引向量。

faiss查询的原理:

faiss使用了PCA和PQ(Product quantization乘积量化)两种技术进行向量压缩和编码,当然还使用了其他的技术进行优化,但是PCA和PQ是其中最核心部分。

主要流程

  • 构建索引index
  • 根据不同索引的特性,对索引进行训练(train
  • add 添加xb数据到索引
  • 针对xq进行搜索search操作

Example

1、数据集

d = 64                           # dimension
nb = 100000                      # 完整数据集
nq = 10000                       # 查询数据
np.random.seed(1234)             
xb = np.random.random((nb, d)).astype('float32')
xb[:, 0] += np.arange(nb) / 1000.
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq) / 1000.

2、构建索引

Faiss围绕Index对象构建。它封装了数据库向量集,并可选地对其进行预处理以提高搜索效率。索引的类型很多,我们将使用最简单的索引,它们仅对它们执行暴力L2距离搜索:IndexFlatL2

d在我们的例子中,所有索引都需要知道何时建立索引,即它们所操作的向量的维数

index = faiss.IndexFlatL2(d)   # build the index

3、对索引进行训练

然后,大多数索引还需要训练阶段,以分析向量的分布。对于IndexFlatL2,我们可以跳过此操作。

4、添加数据到索引

构建和训练索引后,可以对索引执行两项操作:addsearch

将元素添加到索引,我们称之为addxb。我们还可以显示索引的两个状态变量:is_trained,指示是否需要训练的布尔值,以及ntotal索引向量的数量。

一些索引还可以存储与每个向量相对应的整数ID(但不能存储IndexFlatL2)。如果未提供ID,则add只需将向量序号用作ID,即。第一个向量为0,第二个为1,依此类推。

index.add(xb)                  # add vectors to the index

5、对查询数据进行搜索操作

可以对索引执行的基本搜索操作是k-最近邻搜索,即。对于每个查询向量,k在数据库中找到其最近的邻居。

该操作的结果可以方便地存储在一个大小为nq-by-的整数矩阵中k,其中第i行包含查询向量i的邻居ID(按距离递增排序)。除此矩阵外,该search操作还返回一个具有相应平方距离的nqk浮点矩阵。

k = 4                          # we want to see 4 nearest neighbors
D, I = index.search(xb[:5], k) # sanity check, 首先搜索一些数据库向量,以确保最近的邻居确实是向量本身
print(I)
print(D)
D, I = index.search(xq, k)     # actual search
print(I[:5])                   # neighbors of the 5 first queries
print(I[-5:])                  # neighbors of the 5 last queries

索引方式

Faiss中的稠密向量各种索引都是基于 Index实现的,主要的索引方法包括: IndexFlatL2IndexFlatIPIndexHNSWFlatIndexIVFFlatIndexLSHIndexScalarQuantizerIndexPQIndexIVFScalarQuantizerIndexIVFPQIndexIVFPQR等,每个方法的具体介绍。

IndexFlatL2

  • 基于L2距离的暴力全量搜索,速度较慢,不需要训练过程。

IndexIVFFlat

  • 先聚类再搜索,可以加快检索速度;
  • 先将xb中的数据进行聚类(聚类的数目是超参),nlist: 聚类的数目
  • nprobe: 在多少个聚类中进行搜索,默认为1, nprobe越大,结果越精确,但是速度越慢
def IndexIVFFlat(nlist):quantizer = faiss.IndexFlatL2(d)index = faiss.IndexIVFFlat(quantizer, d, nlist)print(index.is_trained)index.train(xb)print(index.is_trained)index.add(xb)return index

IndexIFVPQ

  • 基于乘积量化(product quantizers)对存储向量进行压缩,节省存储空间
  • m:乘积量化中,将原来的向量维度平均分成多少份,d必须为m的整数倍
  • bits: 每个子向量用多少个bits表示
def IndexIVFPQ(nlist, m, bits):quantizer = faiss.IndexFlatL2(d)index = faiss.IndexIVFPQ(quantizer, d, nlist, m, bits)index.train(xb)index.add(xb)return index

kd树

kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。kd树是二叉树,表示对k维空间的一个划分(partition)。构造kd树相当于不断地用垂直于坐标轴的超平面将k维空间切分,构成一系列的k维超矩形区域。kd树的每个结点对应于一个k维超矩形区域。

kd树的结构

kd树是一个二叉树结构,它的每一个节点记载了【特征坐标,切分轴,指向左枝的指针,指向右枝的指针】。

其中,特征坐标是线性空间 R n \mathbf{R}^n Rn的一个点 ( x 1 , . . . , x n ) (x_1,...,x_n) (x1,...,xn)

切分轴由一个整数 r r r表示,这里 1 ≤ r ≤ n 1\leq r\leq n 1rn,是我们在 n n n 维空间中沿第 n n n维进行一次分割。

节点的左枝和右枝分别都是 kd 树,并且满足:如果 y 是左枝的一个特征坐标,那么 y r ≤ x r y_r \leq x_r yrxr并且如果 z 是右枝的一个特征坐标,那么$x_r \leq z_r $。

kd树的构造

通过数据集来构造kd树存储空间,在推荐系统中即用物品Embedding池进行构建。

  • 输入:k维空间数据集 T = { x 1 , x 2 , ⋯ , x N } T=\left\{x_{1}, x_{2}, \cdots, x_{N}\right\} T={x1,x2,,xN},其中 x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( k ) ) T , i = 1 , 2 , . . . , N x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(k)}\right)^{\mathrm{T}},i=1,2,...,N xi=(xi(1),xi(2),,xi(k))T,i=1,2,...,N

  • 输出:kd树;

  • (1)开始:构造根结点,根结点对应于包含 T T T k k k维空间的超矩形区域。

    选择 x ( 1 ) x^{(1)} x(1)为坐标轴,以 T T T中所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点【若超平面上没有切分点,可以适当移动位置,使得超平面上有点】,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。

    由根结点生成深度为1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应于坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。
    落在切分超平面上的实例点保存在根结点

  • (2)重复:对深度为 j j j的结点,选择 x ( l ) x^{(l)} x(l)为切分的坐标轴, l = j ( m o d k ) + 1 l=j(\bmod k)+1 l=j(modk)+1,以该结点的区域中所有实例的 x ( l ) x^{(l)} x(l)坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( l ) x^{(l)} x(l)垂直的超平面实现。
    由该结点生成深度为 j + 1 j+1 j+1的左、右子结点:左子结点对应坐标 x ( l ) x^{(l)} x(l)小于切分点的子区域,右子结点对应坐标 x ( l ) x^{(l)} x(l)大于切分点的子区域
    将落在切分超平面上的实例点保存在该结点。

  • (3)直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

最后每一部分都只剩一个点,将他们记在最底部的节点中。因为不再有未被记录的点,所以不再进行切分。

img

img

搜索kd树

在推荐系统中,即通过用户的Embedding向量来查找与其近邻的 K K K个物品Embedding向量。

  • 输入:已构造的kd树;目标点 x x x

  • 输出: x x x k k k近邻;

  • 设有一个$ k$个空位的列表,用于保存已搜寻到的最近点。

  • (1)在kd树中找出包含目标点 x x x的叶结点:从根结点出发,递归地向下访问树。若目标点 x x x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点,直到子结点为叶结点为止;

  • (2)如果**“当前k近邻点集”元素数量小于 k k k或者叶节点距离小于“当前k近邻点集”中最远点距离**,那么将叶节点插入“当前k近邻点集”;

  • (3)递归地向上回退,在每个结点进行以下操作:

    • 如果“当前k近邻点集”元素数量小于k或者当前节点距离小于“当前k近邻点集”中最远点距离,那么将该节点插入“当前k近邻点集”。
    • 检查该子结点的父结点的另一子结点对应的区域是否与以目标点为球心、以目标点与于“当前k近邻点集”中最远点间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点,接着,递归地进行最近邻搜索;如果不相交,向上回退;
  • 当回退到根结点时,搜索结束,最后的“当前k近邻点集”即为 x x x的k近邻点。

kd树的平均计算复杂度是 l o g ( N ) log(N) log(N)

参考资料:kd 树算法之详细篇

局部敏感哈希

局部敏感哈希的基本思想:

让相邻对的点落入同一个“桶”,这样在进行最近邻搜索时,仅需要在一个桶内,或相邻的几个桶内的元素中进行搜索即可。如果保持每个桶中的元素个数在一个常数附近,就可以把最近邻搜索的时间复杂度降低到常数级别。

首先需要明确一个概念,

在欧式空间中,将高维空间的点映射到低维空间,原本相近的点在低维空间中肯定依然相近,但原本远离的点则有一定概率变成相近的点。

所以利用低维空间可以保留高维空间相近距离关系的性质,就可以构造局部敏感哈希的桶。

对于Embedding向量,可以用内积操作构建局部敏感哈希桶。假设 v \mathbf{v} v是高维空间中的 k k k维Embedding向量, x \mathbf{x} x是随机生成的 k k k维向量。内积操作可以将 v \mathbf{v} v映射到1维空间,成为一个数值:
h ( v ) = v ⋅ x h(\mathbf{v})=\mathbf{v}\cdot \mathbf{x} h(v)=vx
因此,可以使用哈希函数 h ( v ) h(v) h(v)进行分桶:
h x , b ( v ) = ⌊ x x ⋅ v + b w ⌋ x h^{x,b}(\mathbf{v})=\lfloor x \frac{\mathbf{x}\cdot \mathbf{v}+ b}{w}\rfloor x hx,b(v)=xwxv+bx
其中 ⌊ ⌋ \lfloor \rfloor 是向下取整, w w w是分桶宽度, b b b是0到 w w w间的一个均匀分布随机变量,避免分桶边界固化。

如果仅采用一个哈希函数进行分桶,则必然存在相近点误判的情况。有效的解决方法是采用 m m m个哈希函数同时进行分桶。同时掉进 m m m个哈希函数的同一个桶的两点,是相似点的概率大大增加。找到相邻点集合后,取 K K K近邻个。

采用多个哈希函数进行分桶,存在一个待解决的问题,到底通过“与”还是“或”:

  • 与:候选集规模减小,计算量降低,但可能会漏掉一些近邻点;
  • 或:候选集中近邻点的召回率提高,但候选集的规模变大,计算开销变大;

以上是将欧式空间中内积操作的局部敏感哈希使用方法;还有余弦相似度、曼哈顿距离、切比雪夫距离、汉明距离等。

相关文章:

  • EMC Unity存储系统(包含VNXe)常用检查命令
  • 大模型: Function calling的作用
  • 整型溢出问题及解决之道
  • 【开源-土拨鼠充电系统】鸿蒙 HarmonyOS 4.0 App+微信小程序+云平台
  • 【MySQL】InnoDB引擎
  • HTML + CSS 核心知识点- 定位
  • 爬虫逆向sm3和sm4 加密 案例
  • C语言打印当前时间
  • Huggingface 笔记:大模型(Gemma2B,Gemma 7B)部署+基本使用
  • AI论文速读 | UniTS:构建统一的时间序列模型
  • Python中的环境管理与虚拟环境的使用【第148篇—虚拟环境】
  • 【gpt实践】比OpenAI 的 GPT-4 更好模型 Claude 3.0
  • 10分钟带你了解分布式系统的补偿机制
  • Android14 - AMS之Activity启动过程(1)
  • 大数据java工具
  • __proto__ 和 prototype的关系
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • 【刷算法】从上往下打印二叉树
  • C++11: atomic 头文件
  • GraphQL学习过程应该是这样的
  • JS笔记四:作用域、变量(函数)提升
  • Laravel核心解读--Facades
  • Linux gpio口使用方法
  • mysql innodb 索引使用指南
  • Next.js之基础概念(二)
  • React的组件模式
  • Shadow DOM 内部构造及如何构建独立组件
  • underscore源码剖析之整体架构
  • WebSocket使用
  • 包装类对象
  • 构造函数(constructor)与原型链(prototype)关系
  • 前嗅ForeSpider中数据浏览界面介绍
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 数据科学 第 3 章 11 字符串处理
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • #HarmonyOS:软件安装window和mac预览Hello World
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (补)B+树一些思想
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (九)信息融合方式简介
  • (转)jQuery 基础
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • ***原理与防范
  • **PyTorch月学习计划 - 第一周;第6-7天: 自动梯度(Autograd)**
  • .NET Core实战项目之CMS 第十二章 开发篇-Dapper封装CURD及仓储代码生成器实现
  • .NET 跨平台图形库 SkiaSharp 基础应用
  • .NET开源快速、强大、免费的电子表格组件
  • @modelattribute注解用postman测试怎么传参_接口测试之问题挖掘
  • @property python知乎_Python3基础之:property
  • [C++][基础]1_变量、常量和基本类型
  • [C语言]——函数递归
  • [docker] Docker容器服务更新与发现之consul
  • [HCIE] IPSec-VPN (手工模式)
  • [JDK工具-2] javap 类文件解析工具-帮助理解class文件,了解Java编译器机制
  • [JS7] 显示从0到99的100个数字