在前面的文章中已经有说明,Faiss库的运行是基于索引的,这个索引与传统数据库中的Index不同,它是包含向量集,训练和查询方法等的类。
Method |
Class name |
index_factory |
Main parameters |
Bytes/vector |
Exhaustive |
Comments |
Exact Search for L2 |
IndexFlatL2 |
"Flat" |
d |
4*d |
yes |
brute-force |
Exact Search for Inner Product |
IndexFlatIP |
"Flat" |
d |
4*d |
yes |
also for cosine (normalize vectors beforehand) |
Hierarchical Navigable Small World graph exploration |
IndexHNSWFlat |
'HNSWx,Flat` |
d, M |
4*d + 8 * M |
no |
|
Inverted file with exact post-verification |
IndexIVFFlat |
"IVFx,Flat" |
quantizer, d, nlists, metric |
4*d |
no |
Take another index to assign vectors to inverted lists |
Locality-Sensitive Hashing (binary flat index) |
IndexLSH |
- |
d, nbits |
nbits/8 |
yes |
optimized by using random rotation instead of random projections |
Scalar quantizer (SQ) in flat mode |
IndexScalarQuantizer |
"SQ8" |
d |
d |
yes |
4 bit per component is also implemented, but the impact on accuracy may be inacceptable |
Product quantizer (PQ) in flat mode |
IndexPQ |
"PQx" |
d, M, nbits |
M (if nbits=8) |
yes |
|
IVF and scalar quantizer |
IndexIVFScalarQuantizer |
"IVFx,SQ4" "IVFx,SQ8" |
quantizer, d, nlists, qtype |
SQfp16: 2 * d, SQ8: d or SQ4: d/2 |
no |
there are 2 encodings: 4 bit per dimension and 8 bit per dimension |
IVFADC (coarse quantizer+PQ on residuals) |
IndexIVFPQ |
"IVFx,PQy" |
quantizer, d, nlists, M, nbits |
M+4 or M+8 |
no |
the memory cost depends on the data type used to represent ids (int or long), currently supports only nbits <= 8 |
IVFADC+R (same as IVFADC with re-ranking based on codes) |
IndexIVFPQR |
"IVFx,PQy+z" |
quantizer, d, nlists, M, nbits, M_refine, nbits_refine |
M+M_refine+4 or M+M_refine+8 |
no |
Flat Index (IndexFlat*)
Flat索引只是将向量简单的编码为固定大小的代码,并将它们存储在 ntotal * code_size的数组中,不对向量数据进行压缩或折叠等操作。
在搜索时,对所有的索引向量依次与查询向量比较。
Cell-probe methods (IndexIVF* indexes)
IVF会采用一些办法(最典型的如K-mean之类的分区技术)来提高搜索速度,但这会使得搜索的结果未必是最近邻的结果。这种方法有时候被称为cell-probe 方法。
Faiss使用基于分区的多探测方法:
采用这种方法只会对database的一小部分(nprobe / nlist)进行比较,但是因为划分簇时采用了倒序排列的方式,所以尽管比较很少但仍能得到近邻比较。
IndexHNSW及其变体
HNSW全称为Hierarchical Navigable Small World,这类索引会对索引数据生成图,在搜索时,会以尽快收敛到最近邻的方式浏览图。IndexHNSW以Flat Index的方式存储,以便快速访问database的向量。
IndexHNSW的参数:
IndexLSH
目前最流行的cell-probe 方法是逻辑敏感哈希算法(Locality Sensitive Hashing method),这类算法有两个弊端:
在Faiss库中,IndexLSH以Flat Index的二进制代码形式保存,database向量和查询向量以哈希的形式散列在内存中,比较时计算汉明距离。
IndexBinary是二进制索引,其表示的向量集中每个维度只有 0 和 1 两种数值,查询时计算待查向量与DataBase的汉明距离。
向量在内存中按字节存储,每个维度占用1bit,总的内存空间为(dimension / 8)bytes,所以这类向量只支持维度为8的整数倍的向量集,否则需要对向量进行扩展或压缩。
IndexBinaryFlat
穷举搜索,在查询时会计算索引集中所有向量,该索引针对256维向量进行了特殊优化。
示例(python)
import faiss
# 向量维度
d = 256
# 建立索引的向量,可视为DataBase
db = ...
# 需从Index中查询的目标向量
queries = ...
# 初始化Index
index = faiss.IndexBinaryFlat(d)
# 添加db到index中
index.add(db)
# 每个查询向量要检索的最近邻居数
k = ...;
# 查询索引, D是存放最近K个距离的容器, I是D中每个距离的向量在db中的下标
D, I = index.search(queries, k)
IndexBinaryIVF
该索引会对向量进行聚类来加快查询速度。聚类搜索需要先进行训练,相当于无监督学习。
示例(Python)
import faiss
# 向量维度
d = 256
# 建立索引的向量,可视为DataBase
db = ...
# 训练用的向量集
training = ...
# 需从Index中查询的目标向量
queries = ...
# 初始化量化器
quantizer = faiss.IndexBinaryFlat(d)
# 设置向量集中簇的个数
nlist = ...
# 初始化索引
index = faiss.IndexBinaryIVF(quantizer, d, nlist)
# 设置每次查询的簇的数量
index.nprobe = 4
# 训练
index.train(training)
# 将向量集添加进索引
index.add(db)
# 每个查询向量要检索的最近邻居数
k = ...
# 查询索引, D是存放最近K个距离的容器, I是D中每个距离的向量在db中的下标
D, I = index.search(queries, k)
训练过程主要是量化器学习对数据进行聚类,划分为nlist个簇。这里采用的聚类方法是按距离划分。
IndexBinaryIVF索引查询与IndexBinaryFlat的方式不同,前者是基于聚类的。
混合索引表示使用了上述多种索引方法以增强查询性能。
IndexPQ实例
m = 16 # number of subquantizers
n_bits = 8 # bits allocated per subquantizer
pq = faiss.IndexPQ (d, m, n_bits) # Create the index
pq.train (x_train) # Training
pq.add (x_base) # Populate the index
D, I = pq.search (x_query, k) # Perform a search
IndexIVFPQ实例
coarse_quantizer = faiss.IndexFlatL2 (d)
index = faiss.IndexIVFPQ (coarse_quantizer, d,
ncentroids, code_size, 8)
index.nprobe = 5
粗略PQ索引
乘积量化器(PQ)也可以作为初始索引,对应于多索引[The inverted multi-index, Babenko & Lempitsky, CVPR'12],对于具有m个段(每个段编码为c个质心)的PQ,反向列表的数量为c ^ m。因此,m = 2是唯一可行的选择。
在FAISS中,相应的粗略量化器索引是MultiIndexQuantizer。该索引没有添加向量。因此,必须在IndexIVF上设置特定标志(quantizer_trains_alone)。
nbits_mi = 12 # c
M_mi = 2 # m
coarse_quantizer_mi = faiss.MultiIndexQuantizer(d, M_mi, nbits_mi)
ncentroids_mi = 2 ** (M_mi * nbits_mi)
index = faiss.IndexIVFFlat(coarse_quantizer_mi, d, ncentroids_mi)
index.nprobe = 2048
index.quantizer_trains_alone = True
预过滤PQ索引
比较汉明距离比使用PQ算法快6倍,但是通过对量化质心进行适当的重新排序,PQ码之间的汉明距离将与真实距离相关。通过在汉明距离上应用阈值,可以避免最昂贵的PQ代码比较。
#
#For an IndexPQ:
#
index = faiss.IndexPQ (d, 16, 8)
# before training
index.do_polysemous_training = true
index.train (...)
# before searching
index.search_type = faiss.IndexPQ.ST_polysemous
index.polysemous_ht = 54 # the Hamming threshold
index.search (...)
#
#For an IndexIVFPQ:
#
index = faiss.IndexIVFPQ (coarse_quantizer, d, 16, 8)
# before training
index. do_polysemous_training = true
index.train (...)
# before searching
index.polysemous_ht = 54 # the Hamming threshold
index.search (...)
设置阈值的原则:
Faiss提供的索引多种多样,虽然它们都能完成既定的任务,但是对于不同的应用场景,表现的性能差异是非常大的。针对不同的应用场景,如何选择最好的索引,可以遵循下列因素:
搜索量
如果搜索量比较小(比如1000 - 10000),那么建立索引的时间在整个搜索过程中的占比会比较大,所以可以直接计算。
结果精确度
如果对结果要求绝对的准备,那么选择带有"Flat"关键字的索引。只有IndexFlatL2 或 IndexFlatIP能保证绝对的准确。它们为其他索引的结果提供了基准值。
内存敏感性
dataset大小
Faiss的前后处理主要进行重新映射向量ID,对数据进行转换,并使用更好的索引对搜索结果重新排序等操作。
默认情况下Faiss按顺序将向量添加到索引,并设置ID。一些Index类实现了add_with_ids方法,其中除了向量之外,还可以提供64位向量ID。在搜索时,该类将返回存储的ID,而不是初始向量。
映射ID可以使用IndexIDMap方法,该方法封装了另一个索引,并在添加和搜索时转换ID。它维护带有映射的表。
示例
index = faiss.IndexFlatL2(xb.shape[1])
ids = np.arange(xb.shape[0])
index.add_with_ids(xb, ids) # this will crash, because IndexFlatL2 does not support add_with_ids
index2 = faiss.IndexIDMap(index)
index2.add_with_ids(xb, ids) # works, the vectors are stored in the underlying index
由于输入数据量过大等原因,在对数据进行索引前,往往需要进行数据转换,如压缩,降维等。所有的数据预转换方法都继承自VectorTransform类。该类对维度为d_in的输入向量进行转换,输出维度为d_out的输出向量。
数据预转换方法如下表
Transformation |
Class name |
Comments |
随机转换 |
RandomRotationMatrix |
useful to re-balance components of a vector before indexing in an IndexPQ or IndexLSH |
remapping of dimensions |
RemapDimensionsTransform |
to reduce or increase the size of a vector because the index has a preferred dimension, or to apply a random permutation on dimensions. |
PCA |
PCAMatrix |
for dimensionality reduction |
OPQ rotation |
OPQMatrix |
OPQ applies a rotation to the input vectors to make them more amenable to PQ coding. See Optimized product quantization, Ge et al., CVPR'13 for more details. |
PCAMatrix : 使用PCA降维示例
将向量维度从2048D减到16字节
# the IndexIVFPQ will be in 256D not 2048
coarse_quantizer = faiss.IndexFlatL2 (256)
sub_index = faiss.IndexIVFPQ (coarse_quantizer, 256, ncoarse, 16, 8)
# PCA 2048->256
# also does a random rotation after the reduction (the 4th argument)
pca_matrix = faiss.PCAMatrix (2048, 256, 0, True)
#- the wrapping index
index = faiss.IndexPreTransform (pca_matrix, sub_index)
# will also train the PCA
index.train(...)
# PCA will be applied prior to addition
index.add(...)
RemapDimensionsTransform:增加维度
可以通过添加零值的方式给向量增加维度
# input is in dimension d, but we want a multiple of M
d2 = int((d + M - 1) / M) * M
remapper = faiss.RemapDimensionsTransform (d, d2, true)
# the index in d2 dimensions
index_pq = faiss.IndexPQ(d2, M, 8)
# the index that will be used for add and search
index = faiss.IndexPreTransform (remapper, index_pq)
IndexRefineFlat:重新排列搜索结果
查询向量时,使用实际距离计算对搜索结果重新排序可能会很有用。以下示例使用IndexPQ搜索索引,然后通过计算实际距离对第一个结果进行排名:
q = faiss.IndexPQ (d, M, nbits_per_index)
rq = faiss.IndexRefineFlat (q)
rq.train (xt)
rq.add (xb)
rq.k_factor = 4
D, I = rq:search (xq, 10)