Faiss是一个向量检索库。它的GitHub上描述是这样的
Faiss is a library for efficient similarity search and clustering of dense vectors. It contains algorithms that search in sets of vectors of any size, up to ones that possibly do not fit in RAM. It also contains supporting code for evaluation and parameter tuning. Faiss is written in C++ with complete wrappers for Python/numpy. Some of the most useful algorithms are implemented on the GPU. It is developed primarily at Meta’s Fundamental AI Research group.
通过以上描述,可以发现有如下关键点:
本质上是解决基于向量相似度的knn问题
简单来说就是构建索引、添加数据、查询数据。具体步骤为:
根据特点,索引类别主要分为几类。完整版参见文档:
以下为几个常见方法简介
# test_datasets.py
# 数据集
import numpy as np
import time
print("generate data ..")
a = time.time()
d = 64 # dimension
nb = 1000000 # database size
nq = 10000 # nb of queries
np.random.seed(1234) # make reproducible
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.
print("generate data done, time cost:{}".format(time.time() - a))
基于L2距离的暴力全量搜索,速度较慢
这里多介绍一下L1距离和L2距离
L1距离:即曼哈顿距离。每一维度坐标相减后的绝对值求和。公式为
d 1 ( v 1 , v 2 ) = ∑ i = 1 n ∣ x i − y i ∣ d_1(v_1,v_2)= \sum_{i=1}^n|x_i-y_i| d1(v1,v2)=i=1∑n∣xi−yi∣
L2距离:即欧氏距离。
d 2 ( v 1 , v 2 ) = ∑ i = 1 n ( x i − y i ) 2 d_2(v_1,v_2)=\sqrt{\sum_{i=1}^{n}{\left( x_{i}-y_{i} \right)^{2}}} d2(v1,v2)=i=1∑n(xi−yi)2
代码如下
from test_dataset import *
import faiss
import numpy as np
import time
print("start to build index..")
a = time.time()
index = faiss.IndexFlatL2(d) # build the index
print("build index done, time cost:{}".format(time.time() - a ))
print("is trained: {}".format(index.is_trained)) # IndexFlatL2不需要训练过程
print("start to add query..")
a = time.time()
index.add(xb) # add vectors to the index
print("add data done, time cost:{}".format(time.time() - a ))
print(index.ntotal)
print("start to search..")
k = 4 # we want to see 4 nearest neighbors
D, I = index.search(xq, k) # search
print("search done, time cost:{}".format(time.time() - a ))
print(I[:5]) # neighbors of the 5 first queries
print(D[:5]) # distance of the return results
结果
generate data ..
generate data done, time cost:1.2140648365020752
start to build index..
build index done, time cost:1.621246337890625e-05
is trained: True
start to add query..
add data done, time cost:0.19340801239013672
1000000
start to search..
search done, time cost:8.8518226146698
[[ 774 175 917 413]
[ 98 66 357 596]
[ 504 263 740 738]
[ 756 1102 454 353]
[ 352 381 545 264]]
[[6.8088226 6.8297997 6.8576965 6.8842087]
[7.1985703 7.3808327 7.4176826 7.491226 ]
[7.2444 7.460396 7.4658546 7.6690903]
[6.293293 6.414982 6.531719 6.7257767]
[7.6404266 7.77285 7.8547783 8.039719 ]]
先聚类再索引。索引初始化后先在数据集上执行train方法(本质是在对数据集做kmeans聚类),再add数据。查询时先查询与各簇中心距离,找到最近的nprobe个簇后在相应的簇中遍历向量,找到最终结果。
from test_dataset import *
import faiss
"""
先聚类再搜索,可以加快检索速度
先将xb中的数据进行聚类(聚类的数目是超参),nlist: 聚类的数目
nprobe: 在多少个聚类中进行搜索,默认为1, nprobe越大,结果越精确,但是速度越慢
"""
nlist = 100 #聚类的数目
k = 4
a = time.time()
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFFlat(quantizer, d, nlist)
print("build index done, time cost:{}".format(time.time() - a ))
a = time.time()
index.train(xb) # IndexIVFFlat是需要训练的,这边是在聚类
print("train index done, time cost:{}".format(time.time() - a ))
print("is trained: {}".format(index.is_trained))
a = time.time()
index.add(xb) # add may be a bit slower as well
print("add data done, time cost:{}".format(time.time() - a ))
a = time.time()
D, I = index.search(xq, k) # actual search
print("nprobe=1 search done, time cost:{}".format(time.time() - a ))
print(I[-5:]) # neighbors of the 5 last queries
index.nprobe = 10 # default nprobe is 1, try a few more
a = time.time()
D, I = index.search(xq, k)
print("nprobe=10 search done, time cost:{}".format(time.time() - a ))
print(I[-5:]) # neighbors of the 5 last queries
结果
generate data ..
generate data done, time cost:1.2480273246765137
build index done, time cost:5.1021575927734375e-05
train index done, time cost:0.19619107246398926
is trained: True
add data done, time cost:0.30707430839538574
nprobe=1 search done, time cost:0.18898820877075195
[[10179 10714 9864 8813]
[ 9762 10013 9720 10405]
[ 9229 9510 10039 10602]
[10584 9725 9787 9816]
[ 9464 9635 10199 9180]]
nprobe=10 search done, time cost:1.5632450580596924
[[10179 10714 9864 8813]
[ 9762 10013 9720 10405]
[ 9229 9510 10039 10602]
[10584 9725 9787 9816]
[ 9464 9635 10199 9180]]
可以看出建立索引后搜索速度显著提升,从1个类中搜索仅耗时不到0.19s,从10个类中搜索耗时1.56s,远小于暴力搜索。
基于Product Quantizer的压缩算法将向量压缩并存储。此方法可以在存储时压缩向量的大小,故搜索的时候也属于近似搜索。优点是可以减少存储空间。
from test_dataset import *
import faiss
"""
基于乘积量化(product quantizers)对存储向量进行压缩,节省存储空间
m:乘积量化中,将原来的向量维度平均分成多少份,d必须为m的整数倍
bits: 每个子向量用多少个bits表示
和之前输出的最优结果略有不同,牺牲精度,但是节约空间
"""
nlist = 100
m = 8 # number of subquantizers
bits = 8
k = 4
a = time.time()
quantizer = faiss.IndexFlatL2(d) # this remains the same
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, bits) # 8 specifies that each sub-vector is encoded as 8 bits
print("build index done, time cost:{}".format(time.time() - a ))
a = time.time()
index.train(xb)
print("train index done, time cost:{}".format(time.time() - a ))
print("is trained: {}".format(index.is_trained)) # IndexFlatL2不需要训练过程
a = time.time()
index.add(xb)
print("add data done, time cost:{}".format(time.time() - a ))
index.nprobe = 10 # make comparable with experiment above
a = time.time()
D, I = index.search(xq, k) # search
print("nprobe=10 search done, time cost:{}".format(time.time() - a ))
print(I[:5])
结果
generate data ..
generate data done, time cost:1.3283205032348633
build index done, time cost:0.005830287933349609
train index done, time cost:3.3282480239868164
is trained: True
add data done, time cost:0.8176021575927734
nprobe=10 search done, time cost:0.596820592880249
[[ 175 365 774 66]
[ 56 375 3 286]
[ 740 738 394 519]
[ 353 454 1350 233]
[ 87 262 47 642]]
Faiss提供了多种索引,涵盖了各种侧重点,在准确度、内存容量、数据集大小等方面可以综合考虑并选择适合自己业务的index。官方对此提供了较为详细的教程,参见Guidelines-to-choose-an-index
Faiss本身集成了一个rpc库(实际为创建socket连接进行通信,实现参见代码),可以支持分布式检索,代码参加官方demo。其原理为创建多个线程(线程数与远程服务器数相同)分别创建连接请求远程计算服务,并收集计算结果。
由于我手头只有两台机器,所以参考官方demo进行了简单修改,分别将计算分布在两台机器上和一台机器上进行对比测试
# 分布式版本
dis_machine_ports = [
('172.16.160.21', 12010),
('localhost', 12011),
]
# 单机版本
single_machine_ports = [
('localhost', 12014),
]
两台机器配置均为32核64G,测试结果如下
running:distribution
index size: 1000000
recall@1: 0.990
time cost: 3.421029567718506
running:single
index size: 1000000
recall@1: 0.989
time cost: 5.748946666717529
可以看到分布式明显快于单机版,且召回率略有提升。
运行过程中发现分布式版本的两台机器CPU利用率不足1000%,而单机版的CPU使用超过2000%,推测在单机版CPU已成为瓶颈。
在同一台机器上起了两个端口进行对比测试:
# 单机多实例版本
dis_machine_ports = [
('localhost', 12010),
('localhost', 12011),
]
# 单机版本
single_machine_ports = [
('localhost', 12014),
]
测试结果如下
running:distribution
index size: 1000000
recall@1: 0.992
time cost: 5.340034246444702
running:single
index size: 1000000
recall@1: 0.989
time cost: 4.62516450881958
单机版快于分布式版,证明确实存在CPU瓶颈问题。
faiss是一款非常优秀的向量检索库,具有出色的性能和丰富的向量索引、量化、检索方法可供选择,可以高效地解决向量的knn(ann)问题。但由于其缺少完整的整套解决方案(如横向扩容、高可用、监控等),还远不能称为系统,更不能直接用于大规模线上服务。如需线上使用,需在方案上整体设计,将faiss作为计算节点,兼具高可用性、可运维性、易扩展性、可观测性,打造可靠系统。
Faiss的github
Getting-started
常见问题总结
【关于 Faiss 】 那些你不知道的事
相似向量检索库-Faiss-简介及原理
Faiss-facebook开源框架入门教程