RAG 原理:PQ 乘积量化
当向量数量达到百万、千万甚至更大规模时,瓶颈不仅是“搜索范围太大”,还包括原始向量占用内存过高、距离计算成本过大。PQ(Product Quantization,乘积量化)的核心目标,就是用很短的离散编码近似表示高维向量。
一、为什么需要 PQ?
假设知识库中有NNN个ddd维向量,每个元素采用float32存储,则原始向量大约占用:
Memory=N×d×4 Bytes Memory = N \times d \times 4\ BytesMemory=N×d×4Bytes
例如,1 亿个 768 维向量仅保存原始数据就需要约:
100,000,000×768×4≈286 GiB 100,000,000 \times 768 \times 4 \approx 286\ GiB100,000,000×768×4≈286GiB
全量保存和逐维计算都会带来很高的内存、带宽与计算开销。PQ 通过“分段 + 聚类 + 编码”压缩向量,并利用查表完成近似距离计算。
PQ 不是传统意义上的降维:向量仍然对应原来的ddd维空间,只是每一段不再保存完整浮点数,而是保存最接近的聚类中心编号。
二、PQ 的核心思想
设原始向量x∈Rdx \in \mathbb{R}^{d}x∈Rd,将它平均切分为mmm个子向量:
x=[x(1),x(2),…,x(m)] x = [x^{(1)}, x^{(2)}, \ldots, x^{(m)}]x=[x(1),x(2),…,x(m)]
每个子向量的维度为:
dsub=dm d_{sub} = \frac{d}{m}dsub=md
因此必须满足:
d mod m=0 d \bmod m = 0dmodm=0
对第jjj个子空间单独执行 K-Means,训练出KKK个聚类中心,形成子码本:
Cj={cj,1,cj,2,…,cj,K} C_j = \{c_{j,1}, c_{j,2}, \ldots, c_{j,K}\}Cj={cj,1,cj,2,…,cj,K}
编码时,只需找到子向量最近的聚类中心,并保存它的编号:
ij=argmink∈[1,K]∥x(j)−cj,k∥22 i_j = \arg\min_{k \in [1,K]} \left\|x^{(j)} - c_{j,k}\right\|_2^2ij=argk∈[1,K]minx(j)−cj,k22
最终,一个高维向量被压缩为:
PQ(x)=[i1,i2,…,im] PQ(x) = [i_1, i_2, \ldots, i_m]PQ(x)=[i1,i2,…,im]
直观流程
原始 d 维向量 │ ├── 子向量 1 ──> 子码本 1 ──> 中心编号 i1 ├── 子向量 2 ──> 子码本 2 ──> 中心编号 i2 ├── ... └── 子向量 m ──> 子码本 m ──> 中心编号 im │ └── PQ Code = [i1, i2, ..., im]三、为什么叫“乘积量化”?
每个子空间都有一个包含KKK个中心的码本,mmm个子码本组合起来,可表达的中心组合数量为:
Km K^mKm
完整码本空间相当于多个子码本的笛卡尔积(Cartesian Product),因此称为Product Quantization。
这种方法避免了直接在高维空间训练一个巨大码本:高维问题被拆成多个低维聚类问题,训练和存储成本都更可控。
四、PQ 如何压缩内存?
通常令:
K=2nbits K = 2^{nbits}K=2nbits
当nbits = 8时,每个子空间有 256 个聚类中心,一个中心编号只需 1 字节。
若原始向量维度为 128,切成m = 8段:
| 表示方式 | 单向量大小 |
|---|---|
原始float32 | 128×4=512128 \times 4 = 512128×4=512字节 |
| PQ 编码 | 8×1=88 \times 1 = 88×1=8字节 |
忽略码本等额外开销时,压缩倍数约为:
5128=64 \frac{512}{8} = 648512=64
压缩率越高,量化误差通常越大。工程上需要结合召回率、内存和延迟共同调参。
五、PQ 如何进行距离计算?
PQ 不必先把数据库中的每个向量完整还原,再执行逐维距离计算。
对查询向量qqq同样切成mmm段,预先计算每个查询子向量到对应子码本所有中心的距离,得到m×Km \times Km×K的距离表:
查询子向量 q(1) ──> 到子码本 1 的 K 个中心距离 查询子向量 q(2) ──> 到子码本 2 的 K 个中心距离 ... 查询子向量 q(m) ──> 到子码本 m 的 K 个中心距离数据库向量只保存[i1, i2, ..., im],所以它与查询向量的近似距离可通过查表并求和得到:
d~(q,x)=∑j=1m∥q(j)−cj,ij∥22 \tilde{d}(q,x) = \sum_{j=1}^{m} \left\|q^{(j)} - c_{j,i_j}\right\|_2^2d~(q,x)=j=1∑mq(j)−cj,ij22
原本复杂的浮点距离计算,被转换为少量查表与加法操作。
ADC 与 SDC
| 方式 | 查询向量 | 数据库向量 | 特点 |
|---|---|---|---|
| ADC(非对称距离) | 保留原始向量 | 使用 PQ 编码 | 精度通常更高,ANN 检索中更常见 |
| SDC(对称距离) | 也进行量化 | 使用 PQ 编码 | 速度更快,但查询侧也引入量化误差 |
六、PQ 的完整构建与查询流程
1. 构建阶段
- 准备具有代表性的训练向量;
- 将每个向量切分为mmm个子向量;
- 在每个子空间中独立执行 K-Means;
- 得到mmm个子码本;
- 用最近中心的编号编码全部数据库向量。
2. 查询阶段
- 将查询向量按相同方式切成mmm段;
- 计算查询子向量到各子码本中心的距离表;
- 根据每个数据库向量保存的中心编号查表;
- 累加各子空间距离;
- 返回近似距离最小的 Top K 结果。
七、PQ 与 IVF-PQ
单独使用 PQ 解决的是“每个向量太大、距离计算太贵”的问题,但仍可能需要扫描大量 PQ 编码。
IVF-PQ 将两种优化组合起来:
IVF:先通过粗聚类定位少量候选簇 ↓ PQ:再用压缩编码快速估算候选向量距离 ↓ 可选:取较大的候选集,用原始向量或 Reranker 精排| 组件 | 主要作用 |
|---|---|
| IVF | 减少需要扫描的向量数量 |
| PQ | 压缩向量,并降低单次距离计算成本 |
| 精排 | 降低量化误差对最终 Top K 的影响 |
在 FAISS 的IndexIVFPQ中,通常会先计算向量与粗聚类中心之间的残差,再对残差进行 PQ 编码,从而提高近似精度。
八、FAISS:IndexPQ 示例
pipinstallfaiss-cpu numpyimportfaissimportnumpyasnp rng=np.random.default_rng(42)d=128m=8nbits=8database_size=100_000top_k=5database_vectors=rng.random((database_size,d),dtype=np.float32)query_vectors=rng.random((5,d),dtype=np.float32)# d 必须能被 m 整除assertd%m==0# 创建 PQ 索引:128 维向量分成 8 段,每段使用 8 bit 编码index=faiss.IndexPQ(d,m,nbits)# PQ 必须先训练各子空间码本index.train(database_vectors)index.add(database_vectors)distances,ids=index.search(query_vectors,top_k)print("近邻 ID:")print(ids)print("近似 L2 距离:")print(distances)# 查看独立 PQ 编码大小codes=index.sa_encode(database_vectors)print("原始向量字节数:",database_vectors.nbytes)print("PQ 编码字节数:",codes.nbytes)print("单向量编码字节数:",index.code_size)九、FAISS:IndexIVFPQ 示例
importfaissimportnumpyasnp rng=np.random.default_rng(42)d=128nlist=256m=8nbits=8top_k=5database_vectors=rng.random((100_000,d),dtype=np.float32)query_vectors=rng.random((5,d),dtype=np.float32)# IVF 使用的粗量化器quantizer=faiss.IndexFlatL2(d)index=faiss.IndexIVFPQ(quantizer,d,nlist,m,nbits,)# IVF 和 PQ 都需要训练index.train(database_vectors)index.add(database_vectors)# 查询时探测的粗聚类数量index.nprobe=16distances,ids=index.search(query_vectors,top_k)print(ids)print(distances)关键参数:
| 参数 | 含义 | 影响 |
|---|---|---|
m | 子空间数量 | 越大通常越精确,但编码更长、查表次数更多 |
nbits | 每段编码位数 | 越大码本越细,误差更小,但训练和码本成本更高 |
nlist | IVF 粗聚类数量 | 决定倒排列表的粒度 |
nprobe | 查询探测簇数量 | 越大召回率越高,但查询更慢 |
top_k | 最终返回数量 | 应结合业务召回和后续重排需求设置 |
十、误差来源与调优思路
PQ 用聚类中心近似真实子向量,因此必然存在量化误差:
Error=∥x−x^∥22 Error = \left\|x - \hat{x}\right\|_2^2Error=∥x−x^∥22
主要误差来源包括:
- 子空间划分后,不同分段之间的相关性被弱化;
- 每个子码本的聚类中心数量有限;
- 训练样本不能代表真实数据分布;
- 压缩率过高,导致多个差异较大的向量共享相同编码。
常见优化方法:
- 增大
m或nbits,用更长编码换取更低误差; - 使用足够大且有代表性的训练集;
- 使用 OPQ 先旋转向量空间,让各子空间的信息分布更均衡;
- 使用 IVF-PQ 先缩小候选集,再执行 PQ 距离计算;
- 先召回更多候选,再使用原始向量或重排序模型精排。
十一、如何评估 PQ?
不要只比较压缩倍数,还应同时观察:
- Recall@K:与精确 Flat 搜索结果的重合比例;
- 查询延迟与 QPS:是否达到线上性能目标;
- 内存占用:PQ 编码、码本与索引的总成本;
- 索引训练时间:训练集增大后会明显上升;
- 重建误差:原向量与量化重建向量之间的距离。
推荐使用精确索引生成基准答案,再对不同的m、nbits、nlist、nprobe组合进行对比。
十二、小结
PQ 的本质可以概括为四步:
- 分段:把高维向量切成多个低维子向量;
- 聚类:为每个子空间训练独立码本;
- 编码:用聚类中心编号代替浮点子向量;
- 查表:用预计算距离表快速估算向量距离。
它以可控的精度损失换取显著的内存压缩和检索加速。实际 RAG 系统中,PQ 常与 IVF 组合成 IVF-PQ,并配合候选扩召回和精排,在召回率、延迟、内存与成本之间取得平衡。