当向量数量达到百万级,检索系统首先遇到的困难往往是内存。一个 128 维
float32 向量需要 512 字节;一百万个向量,仅原始数值就约占 512
MB,还没有计算 ID、索引和元数据。
乘积量化(Product
Quantization,PQ)用多个小码本近似表示高维向量,在存储成本、距离计算成本与检索精度之间做取舍。
从一个大问题拆成多个小问题
假设向量维度 D=128,将它切成 M=8 段,每段 16
维。针对训练样本的每一段,分别训练一个含 K=256 个聚类中心的 K-means
模型。
原向量:x = [x₁, x₂, …, x₈]
第 i 段的码本:Cᵢ = {cᵢ,₀, cᵢ,₁, …, cᵢ,₂₅₅}
编码:aᵢ = argminⱼ ‖xᵢ − cᵢ,ⱼ‖²
最终编码:[a₁, a₂, …, a₈]
每个编号只需要 8 bit,因此一个向量的编码是 8 字节,相比原来的 512
字节,编码部分缩小到 1/64。这里不包括共享码本、ID 和索引结构的开销。
注意:K-means 是在训练向量集合上进行的,不是拿一个向量的 8
个片段互相聚类。每个子空间有自己的码本,整套码本可以被所有数据库向量复用。
为什么叫“乘积”量化
8 个子空间各有 256 种选择,组合起来可以表示 256⁸
种重构结果。整体码本相当于各子码本的笛卡尔积,而实际只需存储 8
组中心。
以 float32 为例,共享码本数值占用约为:
M × K × (D/M) × 4 = 8 × 256 × 16 × 4 = 131072 字节
这种组合方式避免了显式学习和存储一个极大的完整码本,但也引入了分块假设与量化误差。
压缩以后怎样检索
常见方法是不压缩查询向量 q,只压缩数据库向量
x。这称为非对称距离计算(ADC)。
对一个查询,先计算每个子向量到对应码本所有中心的距离,生成查找表:
T[i, j] = ‖qᵢ − cᵢ,ⱼ‖²
近似距离 d(q, x) = Σᵢ T[i, aᵢ]
查找表针对当前查询构建一次。之后,每个候选向量只需要 8
次查表与累加,不必重新计算 128
个维度上的距离。上式对应平方欧氏距离;不能不加区分地把它当成所有相似度的计算公式。
PQ 的收益和代价
| 选择 | 可能收益 | 需要付出的代价 |
|---|---|---|
| 增大 M,固定每段编码位数 | 通常减小重构误差 | 编码变长,查表次数增加 |
| 增大每段中心数 K | 量化划分更细 | 训练、码本和查询表成本提高 |
| 使用更短的编码 | 降低内存与带宽压力 | 近邻排序可能受量化误差影响 |
| 结合候选筛选索引 | 减少需要扫描的编码数量 | 候选筛选本身也可能漏召回 |
PQ 负责压缩表示,并不自动消除全库扫描。IVF-PQ
等方案会先筛选候选区域,再在区域内计算压缩距离。即使把向量“解码”回聚类中心,也只是恢复近似值,无法还原原始浮点数。
实际选择参数时,应在相同数据集、查询集和硬件上同时观察
Recall@k、延迟与内存,不能仅凭压缩比判断方案更好。
自测:为什么数据库中有一百万个向量,不需要保存一百万套码本?因为码本按子空间训练并共享,每个向量只保存中心编号。