20240316-1-向量化搜索

news2024/9/22 21:23:00

向量化搜索

在高维空间内快速搜索最近邻(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近邻个。

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

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

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

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

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

相关文章

【JSON2WEB】10 基于 Amis 做个登录页面login.html

【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSON2WEB前端框架搭建 【J…

linux系统----------MySQL索引浅探索

目录 一、数据库索引介绍 二、索引的作用 索引的副作用 (缺点) 三、创建索引的原则依据 四、索引的分类和创建 4.1普通索引 4.1.1直接创建索引 4.1.2修改表方式创建 4.1.3创建表的时候指定索引 4.2唯一索引 4.2.1直接创建唯一索引 4.2.2修改表方式创建 4.2.3创建表…

Linux——du, df命令查看磁盘空间使用情况

一、实现原理: df 命令的全称是Disk Free ,显而易见它是统计磁盘中空闲的空间,也即空闲的磁盘块数。它是通过文件系统磁盘块分配图进行计算出的。 du 命令的全称是 Disk Used ,统计磁盘有已经使用的空间。它是直接统计各文件各目…

Qt QGraphicsView移动、缩放

原链接 首先需要明白,view在整个视图框架中的角色是用于显示scene的,所以决定了如何展示scene,包括scale()函数,用于放大缩小所展示的scene;centerOn()函数,决定scene的中心在何方。所有的操作&#xff0c…

移动app测试的好处简析,有必要选择第三方软件测试机构吗?

移动app测试是指对移动应用程序进行全面、系统和深入的检查和验证,以确保其功能、性能和稳定性达到预期要求。在移动应用市场日益竞争激烈的今天,进行移动app测试是至关重要的。 一、移动app测试的好处:   1、具有确保应用质量的作用。通过…

集简云新增“AI图像生成与识别”功能:实现智能图像识别与理解场景

自OpenAI发布GPT-4V以来,也掀起了各大企业对于多模态大模型的研究热潮。和以往的生图模型相比,多模态模型已突破文本限制,图像理解和识别能力尤为突出。 本周,集简云上线AI图像识别与问答功能,集成OpenAI和Anthropic两…

复制浏览器请求到Postman

目录 1.复制链接 2.导入到Postman 1.复制链接 F12打开开发者模式 2.导入到Postman 如上图所示,参数及cookie等信息都被导入进来。

VUE自己项目做的时候遇到的疑惑问题

晚上还在疑惑为什么下面还有一个一模一样的 早上起来,神清气爽,想了一下。原来是我用了两个路由出口

C语言复杂度(个人笔记)

时间复杂度主要衡量一个算法的运行快慢. 空间复杂度主要衡量一个算法运行所需要的额外空间. 时间复杂度 算法中的基本操作的执行次数,为算法的时间复杂度. 只需要大概执行次数,我们使用大O的渐进表示法。(看谁对数学表达式的影响最大) 空间复杂度 是…

学习C++是否有必要学习Boost库?

C作为一门强大且灵活的编程语言,在软件开发领域有着广泛的应用。而在C的学习过程中,Boost库是一个经常被提及的重要资源。那么,对于C的学习者而言,是否有必要投入精力去学习Boost库呢?本文将就此问题展开详尽讨论。 一…

LVGL:拓展部件——键盘 lv_keyboard

一、概述 此控件特点: 特殊Button矩阵:lv_keyboard 本质上是一个经过定制的按钮矩阵控件。每个按钮都可以独立触发事件或响应。预定义的键映射:lv_keyboard 自带了一套预设的按键布局和对应的字符映射表,开发者可以根据需要选择…

js实现hash路由原理

一、简单的上下布局&#xff0c;点击左侧导航&#xff0c;中间内容跟对变化&#xff0c;主要技术使用js检测路由的onhashchange事件 效果图 二、话不多说&#xff0c;直接上代码 <!DOCTYPE html> <html lang"zh"><head><meta charset"…

[Java、Android面试]_11_线程的启动方式和区别

文章目录 1. 继承Thread类2. 实现Runnable接口3. 实现Callable接口4. 使用Executor框架4. 四者的区别 本人今年参加了很多面试&#xff0c;也有幸拿到了一些大厂的offer&#xff0c;整理了众多面试资料&#xff0c;后续还会分享众多面试资料。 整理成了面试系列&#xff0c;由于…

Qt教程 — 3.5 深入了解Qt 控件:Display Widgets部件(1)

目录 1 Display Widgets简介 2 如何使用Display Widgets部件 2.1 QLabel组件-显示图像或文本 2.2 QCalendarWidget组件-日历简单的使用 2.3 QLCDNumber组件-控件作时钟的显示 2.4 QProgressBar组件-模拟手机电池充电 2.5 QFrame组件-绘制水平/垂直线 Display Widgets将分…

【机器学习】基于树种算法优化的BP神经网络分类预测(TSA-BP)

目录 1.原理与思路2.设计与实现3.结果预测4.代码获取 1.原理与思路 【智能算法应用】智能算法优化BP神经网络思路【智能算法】树种优化算法&#xff08;TSA&#xff09;原理及实现 2.设计与实现 数据集&#xff1a; 多输入多输出&#xff1a;样本特征24&#xff0c;标签类别…

【vue-小知识】var、let 和 const之间的区别

文章目录 结论1、重复定义变量名var&#xff1a;允许重复定义变量名let和const&#xff1a;不可以重复定义变量名 2、修改值var&#xff1a;允许修改值let&#xff1a;允许修改值const&#xff1a;不允许修改值&#xff0c;会报错 3、变量提升var : 支持变量提升let和const&…

[ C++ ] STL---stack与queue

目录 stack简介 stack的常用接口 queue简介 queue的常用接口 stack的模拟实现 queue的模拟实现 stack简介 1. stack是具有后进先出操作的一种容器适配器&#xff0c;其只能从容器的一端进行元素的插入与删除操作&#xff1b; 2. stack是作为容器适配器被实现的&#xff0…

使用Barrier对齐ConstraintLayout几个控件的最高的一个

前提就是想让一个控件X&#xff0c;对齐A&#xff0c;B&#xff0c;C等控件最高的位置&#xff0c;直接看图。 看&#xff0c;由于name的一行&#xff0c;或者2行&#xff0c;会导致email行的高度&#xff0c;可能比image块高&#xff0c;也可能比image快矮。 那么&#xff…

【STC8A8K64D4开发板】第2-17讲:PCA实现数模转换(DAC)

第2-17讲&#xff1a;PCA实现数模转换&#xff08;DAC&#xff09; 学习目的了解DAC数模转换原理及RC积分电路原理。掌握STC8A8K64D4系列单片机实现DAC功能的硬件和软件设计。 DAC简介 DAC (全称是Digital to Analog Convertor)数模转换器是一种将数字信号转换为模拟信号&a…

vue3+threejs新手从零开发卡牌游戏(七):创建卡组

在开始前先优化下之前的代码&#xff1a; 在之前hand/p1.vue中为了定位 utils文件夹下新建common.ts&#xff0c;将一些公用方法提取出来放在这里&#xff1a; 在game/Cards.ts中&#xff0c;我们调整下卡牌的厚度&#xff0c;由原来的0.02改为0.005&#xff0c;原因是之前的…