在处理数百万甚至数十亿规模的向量数据时,内存消耗往往是最大的瓶颈之一。Faiss 提供了多种索引结构来应对这一挑战,其中,标量量化(Scalar Quantization, SQ)是一种非常高效且易于实现的方法,尤其是 8 位标量量化(SQ8),它能将原本 32 位浮点数(4 字节)表示的向量压缩为 8 位整数(1 字节),理论上实现了 4 倍的内存压缩。
本文将详细介绍如何在 Faiss 中使用 IndexScalarQuantizer 并选择 SQ8 编码,实现高实操性的内存优化。
什么是标量量化 (SQ8)?
标量量化通过独立地对向量的每个维度进行量化。对于 SQ8 而言,Faiss 会分析训练数据集中每个维度的最小值和最大值,并将该范围线性地映射到 0 到 255 的 8 位整数空间。查询时,Faiss 会反量化(dequantize)查询向量或数据向量,或直接在量化空间内计算距离(具体取决于实现和参数)。
环境准备
确保你安装了 Faiss 库和 NumPy:
pip install faiss-cpu numpy
1. 核心实操:创建和使用 SQ8 索引
我们将创建一个 128 维,包含 10 万个向量的虚拟数据集,并对比标准 IndexFlatL2 和 IndexScalarQuantizer (SQ8) 的使用方法。
import faiss
import numpy as np
import time
import os
# 向量维度
D = 128
# 向量数量
N = 100000
# 查询数量
K = 5
# 1. 准备数据
np.random.seed(42)
xb = np.random.random((N, D)).astype('float32')
xq = np.random.random((10, D)).astype('float32')
# --- 索引定义与训练 ---
# 2. 传统 IndexFlatL2 (高内存消耗,高精度基线)
print("初始化 IndexFlatL2...")
index_flat = faiss.IndexFlatL2(D)
index_flat.add(xb)
# 3. IndexScalarQuantizer (SQ8) 索引 (4倍内存压缩)
# faiss.ScalarQuantizer.SQ8 指定使用 8 位标量量化
print("初始化 IndexScalarQuantizer (SQ8)...")
index_sq8 = faiss.IndexScalarQuantizer(D, faiss.ScalarQuantizer.SQ8)
# 4. 训练:SQ 索引需要训练来确定每个维度的缩放因子和偏移量
# 训练集通常只需要原始数据集的一小部分
N_train = 50000
print(f"开始训练 SQ8 索引 (使用 {N_train} 个向量)...")
t0 = time.time()
index_sq8.train(xb[:N_train])
print(f"训练完成,耗时: {time.time() - t0:.2f}s")
# 5. 添加数据
index_sq8.add(xb)
print(f"数据添加完成。IndexFlatL2 包含 {index_flat.ntotal} 个向量,IndexSQ8 包含 {index_sq8.ntotal} 个向量。\n")
# --- 6. 搜索与对比 ---
# IndexFlatL2 搜索 (基线)
t0 = time.time()
D_flat, I_flat = index_flat.search(xq, K)
t_flat = time.time() - t0
# IndexSQ8 搜索
t0 = time.time()
D_sq8, I_sq8 = index_sq8.search(xq, K)
t_sq8 = time.time() - t0
print(f"Flat Index 搜索时间: {t_flat:.4f}s")
print(f"SQ8 Index 搜索时间: {t_sq8:.4f}s\n")
# --- 7. 评估精度 (Top-1 召回率) ---
# 检查 SQ8 搜索结果的 Top-1 结果是否与 Flat 索引一致
recall_at_1 = np.sum(I_flat[:, 0] == I_sq8[:, 0]) / len(xq)
print(f"Top-1 召回率 (SQ8 vs Flat): {recall_at_1 * 100:.2f}%")
# --- 8. 内存对比 (通过保存文件大小直观对比) ---
# 保存索引文件
faiss.write_index(index_flat, "flat_index.faiss")
faiss.write_index(index_sq8, "sq8_index.faiss")
size_flat = os.path.getsize("flat_index.faiss")
size_sq8 = os.path.getsize("sq8_index.faiss")
print(f"\n内存对比 (文件大小):\n")
print(f"Flat Index 大小: {size_flat / (1024*1024):.2f} MB")
print(f"SQ8 Index 大小: {size_sq8 / (1024*1024):.2f} MB")
print(f"压缩比约为: {size_flat / size_sq8:.2f}X")
# 清理文件
os.remove("flat_index.faiss")
os.remove("sq8_index.faiss")
结果分析与结论
- 内存节省: 运行上述代码可以看到,flat_index.faiss 的大小约为 50MB,而 sq8_index.faiss 的大小约为 13MB 左右。这完美地展现了理论上的 4 倍内存压缩效果。
- 精度保持: 尽管进行了大幅度量化,但由于标量量化在每个维度上都独立进行了精确的线性映射,对于大多数相似度计算场景,SQ8 索引的 Top-K 召回率通常能保持在 90% 以上,甚至接近 100%,展现了优秀的精度保持能力。
- 适用场景: 当你的向量维度不是特别高 (例如 D<=1024) 且对内存要求严格时,IndexScalarQuantizer (SQ8) 是一个理想的选择。它比乘积量化 (PQ) 更容易训练,且在精度损失上更小。
汤不热吧