英文教程:Faiss-Facebook
Faiss是一个用于对向量进行高效相似向量搜索(Similarity Search)
和聚类的库。它同时支持验证和调参。它是C++写的,同时支持Python调用,一些有用的算法也实现了GPU版本。
给定一个d维的向量集合x_i,Faiss在内存上建立一个数据结构。当这个结构被建立之后,给定一个新的d维向量x时,这个结构可以高效地返回与x最相近的Topk个向量。这个结构就叫做索引(index)
。这个对象有一个add
方法来添加x_i向量,计算topk个最相近向量的操作就叫做搜索(search)
。
1中提到的问题,其实就是为1个或者多个向量找它的k个最近邻的向量,可以认为是一个最近邻问题
。第一反应的解决办法可能是计算x和向量集合中所有向量的欧式距离(或者cos距离,点积等),然后从小到大排序,这看上去似乎并不需要很复杂的算法,但在现实场景中往往面临海量的数据,当数据量在上亿级别的时候,这种暴力搜索的方法就并不可取了。所以在进行大规模向量检索时,通常采用的是近似最近邻算法(Approximate Nearest Neighbor,ANN)
。
和暴力解法不同的是,ANN
算法不需要和集合中的所有向量计算距离,而是有选择地和部分向量计算距离,这样得到的答案往往不是最精确的,但是在效率上却大大提高了。所以向量检索其实是一个精度和效率的权衡问题。
ANN算法主要分为基于空间划分的方法
和基于图
的方法。
基于划分的方法
的主要思路是将向量划分到不同的空间中(可以通过聚类等方法),在检索时先确定最相近的一个或多个空间,然后只在这些空间中搜索最近的向量。主要有以下几种:
基于图的方法
的主要思路是邻居的邻居也可能是邻居,这样把最近邻的查找转化为图的遍历,由于其连通性,可以针对性的考察部分向量而不是按区域来考察,因此可以大幅降低向量的考察范围。典型的如HNSW方法。
上面简单介绍了向量检索的背景,后面会基本按照Tutorial的顺序来介绍Faiss的使用。
假设Faiss已经安装好了,且后面的代码演示主要以Python为主,C++版本可以去官方github上查看。
1.构造数据
Faiss处理固定维度d维的向量集合,通常从10维到100维均可。这些集合可以被存储在矩阵中。我们假设按行存储,向量i的第j维元素被存储在矩阵的第i行第j列。Faiss使用的是32位的浮点数矩阵。
我们需要两个矩阵:
xb
用于构建数据库,包含我们要被构建索引的,即将被检索的所有向量。它的维度是nb * d。
xq
是我们需要寻找其最近邻的查询向量,它的维度是nq * d。如果只有单一query,nq=1。
在下面的例子中,我们将要使用的向量是64维的服从均匀分布的向量,仅仅是为了实验,我们给第一维做了一些变换。
In Python
import numpy as np
d = 64 # 定义向量维度
nb = 100000 # 数据库大小
nq = 10000 # query数量
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.
在python中,矩阵通常被表示成numpy
数组的形式(array
)。数据类型dtype
一定要等于float32
。
2. 构建索引并向其中添加向量
Faiss通常构建一个index
对象,它封装了数据库的向量集合,并且有选择地对它们进行一些预处理,使得检索更加高效。Faiss有很多不同类型的索引(index),我们首先使用最简单的版本,暴力搜索——欧式距离(L2 distance
)的方法:IndexFlatL2
。
所有的index在它们构建的时候都需要知道它们要操作的向量维度,在我们的例子里就是d。然后,许多index需要一个训练(training
)阶段,来分析向量的分布。对于IndexFlatL2
索引来说,我们可以跳过这个步骤。
当索引被构建和训练之后,就可以在index上执行两个操作:add
和search
。
向index
中添加元素,我们称作add
。我们也可以查看index的两个状态变量:is_trained
,一个boolean型的变量指示是否需要训练;ntotal
,表示被构建索引的向量的个数。
一些index可以给每个向量存储它们对应的int型ID
(但不是IndexFlatL2)。如果没有提供ID,add只是用向量原始的作为id,比如:第1个向量的id是0,第2个向量的id是1,等等。
In Python
import faiss # 使Faiss可调用
index = faiss.IndexFlatL2(d) # 构建索引
print(index.is_trained) # 打印index是否训练了
index.add(xb) # 向index中添加向量
print(index.ntotal) # 打印被构建索引的向量数量
Results
True # index已经是训练好的了
100000 # 被存储在index中的向量
3. 检索(Search)
在索引上可执行的基础search
操作是k近邻搜索,也就是对于每一个query向量,在数据库中寻找它最相近的 k 个邻居。
这个操作的结果被方便地存储在一个nq * k大小的整型矩阵
中,其中第 i 行包含第 i 个query向量的邻居的IDs
(根据距离递增排序好的)。除了这个矩阵,search
操作还返回了一个nq * k大小的浮点型矩阵
,对应着平方距离。
为了合理性检查,我们可以search
一些数据库中的向量,来确保它们的最近邻就是它们自己本身。
In Python
k = 4 # 我们想查看4个最近的邻居
D, I = index.search(xb[:5], k) # 合理性检查
print(I)
print(D)
D, I = index.search(xq, k) # 真实的search
print(I[:5]) # 打印前5个query的邻居
print(I[-5:]) # 打印后5个query的邻居
Results
上述合理性检查的代码结果如下:
[[ 0 393 363 78]
[ 1 555 277 364]
[ 2 304 101 13]
[ 3 173 18 182]
[ 4 288 370 531]]
[[ 0. 7.17517328 7.2076292 7.25116253]
[ 0. 6.32356453 6.6845808 6.79994535]
[ 0. 5.79640865 6.39173603 7.28151226]
[ 0. 7.27790546 7.52798653 7.66284657]
[ 0. 6.76380348 7.29512024 7.36881447]]
即,每个query最近的邻居就是它们本身的向量id,对应的距离是0。并且在一行中,距离是增加的。
真实的search
结果如下:
[[ 381 207 210 477]
[ 526 911 142 72]
[ 838 527 1290 425]
[ 196 184 164 359]
[ 526 377 120 425]]
[[ 9900 10500 9309 9831]
[11055 10895 10812 11321]
[11353 11103 10164 9787]
[10571 10664 10632 9638]
[ 9628 9554 10036 9582]]
由于向量的第1维被添加了随机值,所以数据集在d维空间的第1维被扰乱。因此前面一些向量的邻居集中在数据集的开头,在~10000周围的向量的邻居在集中在数据集的10000左右的位置。
执行这次search
操作在一台2016年的机器上大约耗时3.3秒。
(未完待续…)