内存优化
内存占用太高,如何压缩存储?
我们前面介绍的索引类型,如 IndexFlatL2 和 IndexIVFFlat,都会完整地存储全部向量。当数据规模非常大时,这会带来极高的内存消耗。为了解决这个问题,Faiss 提供了一些索引变体,它们会用有损压缩方法(基于“乘积量化器”,Product Quantizer)对向量进行压缩存储,从而大幅减少内存消耗。
向量依然存储在分区(Voronoi cells)中,但每个向量被压缩为可自定义字节数 ( m )(此时维度 ( d ) 必须是 ( m ) 的整数倍)。
这种压缩方法依赖于乘积量化器 Product Quantizer(PQ)。可以简单理解为对每个向量切成多个子向量、分别进行量化,作为进一步的压缩编码机制。
由于向量是经过压缩存储的,Faiss 检索时返回的距离值不再是精确距离,而是近似距离。
Python 示例
nlist = 100
m = 8 # 子量化器(subquantizer)数量
k = 4
quantizer = faiss.IndexFlatL2(d) # 用于IVF分区的量化器类型不变
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, 8)
# 8 表示每个子向量编码为8位(即1字节)
index.train(xb) # 先训练索引
index.add(xb) # 添加向量
D, I = index.search(xb[:5], k) # 基本检查
print(I)
print(D)
index.nprobe = 10 # 搜索比较更多分区,与前文实验对应
D, I = index.search(xq, k) # 检索
print(I[-5:])
C++ 示例
int nlist = 100;
int k = 4;
int m = 8; // 子量化器数量
faiss::IndexFlatL2 quantizer(d); // IVF分区用量化器
faiss::IndexIVFPQ index(&quantizer, d, nlist, m, 8);
// 8 表示每个子向量编码8位
index.train(nb, xb); // 训练
index.add(nb, xb); // 添加向量
{ // 测试
...
index.search(5, xb, k, D, I);
printf("I=\n");
...
printf("D=\n");
...
}
{ // 检索xq
...
index.nprobe = 10;
index.search(nq, xq, k, D, I);
printf("I=\n");
...
}
检索结果解析
输出结果如下:
[[ 0 608 220 228] [ 1 1063 277 617] [ 2 46 114 304] [ 3 791 527 316] [ 4 159 288 393]]
[[ 1.40704751 6.19361687 6.34912491 6.35771513] [ 1.49901485 5.66632462 5.94188499 6.29570007] [ 1.63260388 6.04126883 6.18447495 6.26815748] [ 1.5356375 6.33165455 6.64519501 6.86594009] [ 1.46203303 6.5022912 6.62621975 6.63154221]] 可以看到,最相近的向量检索结果是正确的(编号与自身一致),但自身的距离并不是 0,只是比其它邻居的距离要小。这正是“有损压缩”的结果。
这里,我们将 64 维单精度浮点数组(每个都是32位)压缩到了 8 字节,压缩比例约为 32 倍。
实际查询场景下,结果类似:
[[ 9432 9649 9900 10287]
[10229 10403 9829 9740]
[10847 10824 9787 10089]
[11268 10935 10260 10571]
[ 9582 10304 9616 9850]]
可以与 IVFFlat 的结果进行对比。在这种构造的数据下,大部分结果并不完全准确,但编号普遍在1万左右,即还在空间的正确区域内。
对于真实的自然数据,实际检索效果通常会比演示数据更好。因为自然数据存在一定规律性,更易聚类与降维,且同义数据(语义最近邻)通常彼此间更接近。
注意事项
- 均匀分布(uniform data)的数据集很难通过聚类或降维手段获得效果提升,这会影响压缩索引效果;
- 在自然数据(如文本、图片向量)的场景下,语义最近邻之间的距离通常比无关数据要近得多。
简化索引构建
手动逐步构建索引参数较为繁琐。Faiss 提供 index factory(索引工厂)方法,可通过一行字符串参数快速组合配置所需索引。例如,上述的 IVFPQ 索引可以直接如下创建:
index = faiss.index_factory(d, "IVF100,PQ8")
faiss::Index* index = faiss::index_factory(d, "IVF100,PQ8");
如果把 PQ8 替换成 Flat,即构建一个未压缩的 IndexFlat 索引。工厂方法在配合预处理流程(如主成分分析,PCA)时尤为方便。例如先用PCA将向量降至32维再构建索引:
"PCA32,IVF100,Flat" 上述字符串可灵活拓展不同的处理与索引方式。
延伸阅读
请继续阅读后续章节,了解更多索引类型、GPU 版 Faiss、代码结构等详细资料。