乘积量化(PQ)是什么
PQ 把 d 维向量切成 m 段子向量,每段用一个独立码本量化:一段只保留它最接近的质心编号,一条向量最终用 m 个字节左右的编码代替 4·d 字节的浮点数组,内存降一个数量级。检索时按子空间查表累加近似距离,速度也更快。代价是距离只是近似值,压缩比越高误差越大,通常搭配 IVF 做粗排、再用原始向量对候选做重排来补偿精度。
基本信息
| 参数 m | 子空间个数,d 必须能被 m 整除 |
|---|---|
| 参数 nbits | 每段码本位数,常取 8(256 个质心) |
| 存储 | 每条向量约 m 字节,替代 4·d 字节 |
| 依赖训练 | 码本需要用样本训练,训练不足则精度损失明显 |
| 常见组合 | IVF 粗排 + PQ 压缩 + 原始向量重排 |
典型使用场景
- 内存受限的大规模索引:千万级向量无法全部以浮点驻留
- 离线近邻分析:去重、聚类、近似推荐的候选生成阶段
- 冷热分层:热数据保留原始向量,冷数据用 PQ 编码存放
- 实验对照:同一份数据比较 PQ 压缩前后的召回损失与内存占用
- 成本优化:按预算反推可接受的 m 与 nbits 组合
FAISS IVFPQ 示例
FAISS IVFPQ 示例
import faiss, numpy as np
d = 768
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(quantizer, d, 1024, 96, 8) # nlist=1024, m=96, nbits=8
index.train(np.random.rand(200000, d).astype('float32')) # 必须先训练
index.add(np.random.rand(2000000, d).astype('float32'))
index.nprobe = 32 # 查询时扫描的聚类数
D, I = index.search(np.random.rand(1, d).astype('float32'), 10)参数取舍
| 参数 | 增大 | 减小 |
|---|---|---|
| m | 精度更高,编码更长 | 内存更省,误差变大 |
| nbits | 码本更细,精度上升 | 码本变小,误差明显 |
| nprobe | 召回上升、延迟上升 | 更快但可能漏掉候选 |
| 训练样本量 | 码本更贴合数据分布 | 训练不足导致精度骤降 |
上线前的验证步骤
- 先量基线:用精确索引记录 Recall@10,作为压缩后的对照基准
- 逐档压缩:从低压缩比开始试,记录内存占用与召回损失的关系曲线
- 加重排补偿:对前若干候选取原始向量重算距离,观察召回是否回到可接受区间
- 固化配置:把 m、nbits、nprobe 写入部署配置并纳入回归评测
训练样本必须来自真实数据分布:拿随机向量或少量样本训练出来的码本,在真实查询上会表现出明显的召回下降,且这种下降很难从监控指标上看出来。