Faiss(4):索引(Index)

在前面的文章中已经有说明,Faiss库的运行是基于索引的,这个索引与传统数据库中的Index不同,它是包含向量集,训练和查询方法等的类。

1. 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

1.1 基本索引方法

Flat Index (IndexFlat*)

Flat索引只是将向量简单的编码为固定大小的代码,并将它们存储在 ntotal * code_size的数组中,不对向量数据进行压缩或折叠等操作。

在搜索时,对所有的索引向量依次与查询向量比较。

Cell-probe methods (IndexIVF* indexes)

IVF会采用一些办法(最典型的如K-mean之类的分区技术)来提高搜索速度,但这会使得搜索的结果未必是最近邻的结果。这种方法有时候被称为cell-probe 方法。

Faiss使用基于分区的多探测方法:

  • 特征空间被划分为nlist个单元;
  • 使用hash函数,将database向量分配给这些单元并存储在由nlist个倒序列表构成的倒序文件结构中。
  • 查询时选择nprobe个倒序列表;
  • 将查询向量与这nprobe个列表进行比较;

采用这种方法只会对database的一小部分(nprobe / nlist)进行比较,但是因为划分簇时采用了倒序排列的方式,所以尽管比较很少但仍能得到近邻比较。

IndexHNSW及其变体

HNSW全称为Hierarchical Navigable Small World,这类索引会对索引数据生成图,在搜索时,会以尽快收敛到最近邻的方式浏览图。IndexHNSW以Flat Index的方式存储,以便快速访问database的向量。

IndexHNSW的参数:

  • m:表示图中邻居的个数,m值越大,结果越精确,但占用内存更多;
  • efConstruction : add阶段的搜索深度;
  • efSearch : 查询阶段的搜索深度;

IndexLSH

目前最流行的cell-probe 方法是逻辑敏感哈希算法(Locality Sensitive Hashing method),这类算法有两个弊端:

  1. 需要大量哈希函数才能得到想要的结果,也就意味着需要占用大量内存;
  2. 哈希函数不适用于输入数据;

在Faiss库中,IndexLSH以Flat Index的二进制代码形式保存,database向量和查询向量以哈希的形式散列在内存中,比较时计算汉明距离。

1.2 二进制索引

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的方式不同,前者是基于聚类的。

1.3 混合索引

混合索引表示使用了上述多种索引方法以增强查询性能。

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 (...)

设置阈值的原则:

  • 阈值应在0和每个代码的位数之间(在这种情况下为128 = 16 * 8),并且代码遵循二项式分布;
  • 阈值设置应小于代码位数的1/2。

2. 如何选择索引

Faiss提供的索引多种多样,虽然它们都能完成既定的任务,但是对于不同的应用场景,表现的性能差异是非常大的。针对不同的应用场景,如何选择最好的索引,可以遵循下列因素:

搜索量

如果搜索量比较小(比如1000 - 10000),那么建立索引的时间在整个搜索过程中的占比会比较大,所以可以直接计算。

结果精确度

如果对结果要求绝对的准备,那么选择带有"Flat"关键字的索引。只有IndexFlatL2 或 IndexFlatIP能保证绝对的准确。它们为其他索引的结果提供了基准值。

内存敏感性

  • 如果对内存没有要求,选择"HNSWx",其中x(4 - 64)表示每个向量的link数,x值越大,结果越精确,占用内存也越大;
  • 要求不高,选择"... , Flat",其中"..."表示先对数据进行聚类。
  • 要求比较高,选择"PCARx,...,SQ8"
  • 对内存要求非常高,选择"OPQx_y,...,PQx"

dataset大小

  • 小于1M, 选择"...,IVFx,..."
  • 1M - 10M之间,选择 "...,IVF65536_HNSW32,..."
  • 10M - 100M:之间, 选择"...,IVF262144_HNSW32,..."
  • 100M - 1B:之间, 选择"...,IVF1048576_HNSW32,..."

3. 向量方法

Faiss的前后处理主要进行重新映射向量ID,对数据进行转换,并使用更好的索引对搜索结果重新排序等操作。

3.1 向量映射

默认情况下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

3.2 预转换数据

由于输入数据量过大等原因,在对数据进行索引前,往往需要进行数据转换,如压缩,降维等。所有的数据预转换方法都继承自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)

你可能感兴趣的:(Faiss,机器学习)