以失去保证以找到最近邻居为代价来加速该过程的典型方法是采用诸如k均值的分区技术。 相应的算法有时被称为 cell-probe 方法:
我们使用基于多探测的基于分区的方法(可以联想到best-bin KD-tree的一种变体)。
特征空间被划分为 ncells 个单元格。
由于散列函数(在k均值的情况下,对最靠近查询的质心的分配),数据库向量被分配给这些单元中的一个,并且存储在由ncells反向列表形成的反向文件结构中。
在查询时,会选择一组 nprobe 个的反向列表
将查询与分配给这些列表的每个数据库向量进行比较
这样做,只有一小部分数据库与查询进行比较:作为第一个近似值,这个比例是 nprobe / ncells,但请注意,这个近似值通常被低估,因为反向列表的长度不相等。 当未选择给定查询的最近邻居的单元格时,将显示失败案例。
在C++中,相应的索引是索引IndexIVFFlat。
构造函数将索引作为参数,用于对反转列表进行赋值。 在该索引中搜索查询,并且返回的向量id(s)是应该被访问的反向列表。
一般的,我们使用一个 Flat index 作为粗糙量化。 IndexIVF 的训练方法给 flat index 添加了质心。 nprobe 的值在搜索时设置(对调节速度-准确比很管用)。
注意: 根据经验,n 表示要被索引的点的数量, 一般确定合适质心数量的方法是在“分配向量到质心的开销(如果是纯粹的kmeans:ncentroids * d)” 和 “解析反转列表时执行的精确距离计算的数量(按照 kprobe / ncells * n * C 的顺序,其中常量 C 考虑了列表的不均匀分布, 以及当使用质心批处理时单个矢量比较更有效的事实,比如 C = 10)”之间找平衡。
这导致了许多质心的数量都遵循 ncentroids = C * sqrt(n)。
注意:在引擎盖下,IndexIVFKmeans 和 IndexIVFSphericalKmeans 不是对象,而是返回正确设置的 IndexIVFFlat 对象的函数。
警告:分区方法容易受到维数的诅咒。对于真正高维数据,实现良好的召回需要具有非常多的probe。
tar -xvf OpenBLAS-0.3.2.tar.gz
cd OpenBLAS-0.3.2
make
make PREFIX=/usr/local/ install
yum install lapack-devel.x86_64 -y
./configure -with-blas="-L/usr/local/lib -lopenblas -llapack -lgfortran -lpthread" --without-cuda
make && make install
faiss::MultiIndexQuantizer coarse_quantizer (d, nhash, nbits_subq);
faiss::IndexIVFFlat index (&coarse_quantizer, d, ncentroids, metric);
faiss::IndexIVFPQ index (&coarse_quantizer, d, ncentroids, bytes_per_code, 8);
std::vector trainvecs (nt * d);
index.train (nt, trainvecs.data());
std::vector database (nb * d);
std::vector ids (nb);
index.add (nb, database.data());
index.add_with_ids (end - begin, database.data() + d * begin, ids.data() + begin);
std::vector queries;
std::vector nns (k * nq);
std::vector dis (k * nq);
index.search (nq, queries.data(), k, dis.data(), nns.data());
faiss::write_index(&index, "/tmp/populated_index.faissindex");
faiss::Index * idx = faiss::read_index("/tmp/trained_index.faissindex");
当数据集分布在多个索引上时,可以通过它们调度查询,并将结果与IndexShards 结合使用。 如果索引分布在多个GPU上并且查询可以并行完成,这也很有用. 请参阅在 GpuClonerOptions中 将 shards 设置为 true 的 index_cpu_to_gpus 。
IndexFlatL2 不压缩向量,但不会在它们之上增加开销。 它不支持添加id(add_with_ids),只支持顺序添加,因此如果需要 add_with_ids,请使用“IDMap,Flat”。支持GPU。
HNSWx 非常快速和准确的索引。 x 的范围是[4, 64],它表示了每个向量的链接数量,越大越精确,但是会使用越多的内存。只支持顺序添加(不是add_with_ids),所以在这里再次使用 IDMap 作为前缀(如果需要)。 HNSW 不需要训练,也不支持从索引中删除矢量。不支持GPU。
“…,Flat” “…” 表示必须事先执行数据集的聚类(如下所示)。 在聚类之后,“Flat”只是将向量组织到不同桶中,因此它不会压缩它们,存储大小与原始数据集的大小相同。 速度和精度之间的权衡是通过 nprobe 参数设置的。支持GPU,聚类方法也需要支持GPU。
“PCARx,…,SQ8” 如果存储整个向量太昂贵,则执行两个操作:(1) 使用尺寸为x的PCA以减小尺寸
(2) 每个矢量分量的标量量化为1个字节。因此,总存储量是每个向量 x 个字节。不支持GPU。
“OPQx_y,…,PQx” Qx 代表了通过一个product quantizer压缩向量为 x 字节。 x 一般 <= 64,对于较大的值,SQ 通常是准确和快速的。OPQ 是向量的线性变换,使其更容易压缩。 y是一个维度: (1)y是x的倍数(必需)(2)y <= d,d为输入向量的维度(最好)
(3)y <= 4*x(最好)
当数据集的数量为 N 时,那么 x 应该处于 4 * sqrt(N) 和 16 * sqrt(N) 之间。 这只是用k-means聚类向量。 你需要 30 * x 到 256 * x 的矢量进行训练(越多越好)。支持 GPU。
IMI在训练向量上执行具有2^10个质心的 k-means,但它在向量的前半部分和后半部分独立地执行。 这将簇的数量增加到 2^(2 * 10)。您将需要大约64 * 2 ^ 10个向量进行训练。 不支持GPU。
与上面相同,将10替换为12。不支持GPU。
与上面相同,将10替换为14。不支持GPU。
默认情况下,Faiss 为添加到索引的向量分配顺序 id。 本页介绍如何将其更改为任意ID。
一些Index类实现了 add_with_ids 方法,除了向量之外,还可以提供64位向量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
IndexIVF 中的 IDs: IndexIVF 子类始终存储矢量ID。 因此,IndexIDMap 的附加表是浪费空间。 IndexIVF 本身提供 add_with_ids。
https://waltyou.github.io/Faiss-Introduce/
https://waltyou.github.io/Faiss-Indexs/#%E6%8C%91%E4%B8%80%E4%B8%AA%E5%90%88%E9%80%82%E7%9A%84-index
https://github.com/facebookresearch/faiss/wiki/Faiss-indexes