1. 为什么矢量量化值得单独拿出来讲
聊数据压缩和高效检索,大部分人第一反应是哈夫曼编码、LZ77、或者近几年的深度学习Embedding。矢量量化(Vector Quantization,VQ)这个名字听着像教科书里的古董概念,但如果你真正在工业界做过大规模检索、做过模型压缩、或者碰过语音信号的编码传输,会发现它至今仍是压箱底的关键工具。
我最早接触VQ是在语音编码领域,那时候做的是LPC(线性预测编码)之后的残差量化,用的就是矢量量化。后来转去做向量检索,发现ANN(近似最近邻)索引里大量算法都在直接或间接用VQ的思想——比如PQ(乘积量化)、OPQ(优化乘积量化)、IVF+PQ这些经典组合。可以说,不懂VQ,就很难真正理解当前主流的高维向量检索方案为什么那样设计,遇到性能瓶颈时也无从下手。
这篇内容我不打算只贴公式,而是想从一个实践者的角度,把矢量量化背后的原理、经典算法流程、以及在数据压缩和信息检索里的真实位置讲清楚。适合三类人看:一是刚接触量化概念的学生,二是做检索系统优化的工程师,三是做模型或特征压缩的算法同学。
2. 从标量量化到矢量量化:核心问题到底是什么
2.1 标量量化为什么不够用
要理解矢量量化,先从它的小兄弟标量量化(Scalar Quantization,SQ)说起。
标量量化做的事情是:把一维连续值,比如一个浮点数0.7321,映射到有限个离散值上。比如我用8个等级去表示它,那就先画8个区间,落在哪个区间,就输出哪个区间的代表值。整个过程可以理解成"四舍五入"的泛化版本。
标量量化的问题在于:它一次只处理一个数,完全没有利用数据之间的相关性。举个例子,一段语音信号相邻帧之间往往高度相关,如果一帧一帧独立量化,每一帧都要花比特去描述,信息冗余非常大。同理,图像相邻像素、高维向量的各个维度之间也常常存在结构相关性,标量量化视而不见。
2.2 矢量量化的本质:一次量化一组数
矢量量化把输入从标量变成向量。给定一个k维向量 x = (x1, x2, ..., xk),我们不单独量化每个维度,而是把它作为一个整体,去码本(Codebook)里找一个最接近的码字(Codeword),然后用这个码字的索引来表示原始向量。
这个"整体映射"就是VQ和SQ最本质的区别。它有两个直接收益:
- 利用了向量内部各维度的联合分布。码本的每个码字本身就是一个完整的k维向量,它刻画的不是某一条坐标轴上的分布,而是k维空间里的典型模式。
- 用更少的比特表达同样的信息。假设码本大小是N,那么传输时只需要log2(N)个比特来传索引。原始向量如果是k个浮点数每个32比特,压缩比非常可观。
用大白话说:标量量化是"每个数单独报数",矢量量化是"整组数据对暗号"。暗号本(码本)里预先存好了常见的组合模式,通信双方只需要交换暗号编号就行。
2.3 率失真视角:为什么VQ能超过SQ的理论极限
理论上有句话叫"矢量量化永远不差于标量量化",这在率失真理论(Rate-Distortion Theory)里是有明确结论的。简单解释:在相同比特率下,矢量量化可以达到更低的失真;反过来,在相同失真要求下,VQ需要的比特率更低。
关键在于维度增益(Dimensionality Gain)。当量化维度从1变成k时,我们的自由度大大增加——码字的形状不再是坐标轴上的点,而是高维空间里可以任意分布的代表点。这就像用二维平面的网格去覆盖一个圆形区域,比用一维数轴上的刻度去近似它要灵活得多。
这个"维度增益"在高维空间里尤其明显。虽然理论上要充分发挥这种增益,码本要随着维度指数级增长,但实际中哪怕是低比特、小码本的VQ,也比同等条件下的标量量化效果好。这个现象在语音编码时代就被反复验证过。
3. 矢量量化的完整工作流程:从训练码本到量化检索
3.1 码本训练:经典LBG算法
矢量量化有两个阶段:训练阶段和量化阶段。训练阶段的核心任务是生成码本。
最经典的训练方法是LBG算法(Linde-Buzo-Gray),它是K-Means聚类的泛化版。基本流程如下:
- 收集一批训练向量,作为样本集。
- 初始化一个码本,可以随机选一个样本点作为唯一码字,也可以先用K-Means粗训练一轮。
- 把每个训练样本分配到距离最近的码字上,形成若干个簇。
- 重新计算每个簇的质心,作为新的码字。
- 如果码本大小还没达到目标,把当前码本里的码字进行分裂——通常是在码字上叠加一个微小扰动生成两个新码字。
- 重复步骤3到5,直到码本大小达标,且失真不再显著下降。
这个过程其实就是"先粗后细"的思路。从1个码字开始,每次翻倍扩展码本,每次扩展后都做一次优化调整,类似在粗糙的网格上逐步加密。实际使用中,如果训练数据量足够,直接跑K-Means初始化到最终码本大小也是常见做法。
3.2 量化过程:查表与最近邻搜索
训练好码本之后,量化阶段就很简单了:对于一个输入向量,在码本里找出距离最近的码字,记录下码字索引。这个"找最近码字"的过程本质上是最近邻搜索。
注意一个容易忽略的点:量化的计算开销主要在编码端。如果码本有N个码字,线性扫描的话每个向量要做N次距离计算。码本较大时这可能是瓶颈。所以工业实现里往往对码本建立索引结构,比如树形结构或倒排结构,来加速最近邻搜索。
解码端则非常轻量:拿到索引,直接查表取出码字向量。这个不对称性在很多场景里非常有用——比如服务器端做重编码、终端做轻量解码,或者反过来终端轻量编码、服务器统一处理。
3.3 失真度量:欧氏距离还是内积
量化过程中"距离最近"到底用什么距离,由实际场景决定。
最常见的度量是欧氏距离(L2距离)。它的特点是:在码本固定的情况下,把"找最近码字"和"计算量化误差"结合起来,优化目标非常直接——最小化重构误差的期望。
但在信息检索场景里,经常用内积(点积)相似度,比如计算cosine相似度做ranking。此时量化误差的定义会略有变化。一个标准做法是:先把所有向量做L2归一化,然后最大化内积就等价于最小化欧氏距离。这也是为什么很多检索系统在量化之前先做归一化预处理。
我自己的习惯是:如果后续要直接比较相似度分数,优先用内积一致性训练码本;如果只是做候选召回,然后有精排阶段,那用L2距离更省事。具体选哪种,取决于你做的是ANN索引还是模型特征压缩。
3.4 码本大小与比特数的权衡
码本大小N直接决定索引的比特数。比如N=256,索引是8比特;N=65536,索引是16比特。
对压缩任务来说,码本越大,量化失真越低,但码本本身也是一个存储开销;对检索任务来说,码本太大,编码阶段搜索变慢,码本训练需要的样本量也指数增长。有一个粗略经验:码本大小N至少要远小于训练样本数量,一般要求训练样本数N的几十倍以上,否则码本过拟合,泛化能力差。
4. 信息检索里的VQ:从PQ到IVF+PQ的主流组合
4.1 直接VQ做检索的局限性
既然VQ可以把高维向量压缩成短索引,那能不能直接把所有向量VQ成码字索引,然后通过索引匹配来做检索?
理论上可以,实际上几乎不用。原因有两个:
- 单码本VQ的表达能力有限。高维空间里数据分布非常复杂,用一个全局码本去描述所有向量,量化误差会很大,相似向量的码字可能完全不同,检索质量会很差。
- 索引匹配丢失了距离信息。不同码字之间的距离没有被编码,两个不同的码字可能非常接近,也可能非常远,但只靠索引是无法区分这些情况的。
所以VQ在检索里的真正角色,不是替代原始向量直接参与比较,而是作为"压缩表示"和"加速结构"存在。
4.2 乘积量化(PQ):把高维向量拆开再量化
乘积量化是目前ANN检索里最有影响力的VQ改进方案。它的核心思路是:把高维向量切成m个子向量,对每个子向量分别做VQ。
比如一个128维向量,切成4段,每段32维。对每段分别学习一个码本(比如每段256个码字),那么每个原始向量最终被表示成4个8比特的索引,总共32比特。这和直接VQ出一个32比特索引是完全不同的——PQ相当于把整个向量空间分解成了4个子空间的乘积,所以叫"乘积量化"。
PQ带来两个好处:
- 码本规模可控。如果128维直接做VQ要65536个码字,训练量巨大;PQ每段只有256个码字,训练简单很多。
- 距离计算可以查表加速。对于查询向量q,可以预先计算出它到每段码本里每个码字的距离,得到m个距离表。然后对于任何压缩后的向量,只需要查m次表相加,就能得到近似距离,避免了高维浮点运算。
这个查表加速的特性非常关键,它把原本需要高维距离计算的问题,变成了多次整数查表和加法,性能提升非常明显。
4.3 倒排+PQ:工业检索系统的标准范式
单独用PQ做全量扫描还是慢,因为要遍历所有向量。这时候引入倒排索引(IVF)。
IVF的思路是先对全量向量做粗聚类,比如聚成nlist个簇。每个向量归属于最近的簇,建立倒排列表。查询时,先在簇中心里找最近的若干个簇(nprobe个),然后只在这些簇内部用PQ做精确距离计算。
这个组合就是著名的IVF+PQ。它把"粗粒度路由"和"细粒度压缩"结合在一起:
- 粗粒度聚类(IVF)解决了"不用和全库比"的问题。
- PQ压缩解决了"内存放不下全量原始向量"的问题。
- PQ查表解决了"距离计算太慢"的问题。
实际工程里,IVF+PQ可以轻松把千万级别的向量库放入内存,并且单次查询控制在毫秒级,召回率通过调节nprobe灵活权衡。这也是Faiss这类库最经典的使用方式。
4.4 优化变体:OPQ与残差量化
为了进一步提升PQ的效果,学术界和工业界做了不少改进。
OPQ(Optimized Product Quantization)的思路是:在切分向量之前,先学一个正交旋转矩阵,对原始向量做旋转,使得各子向量段之间的方差更均衡。这样每段子空间的重要程度接近,量化误差更均匀。实话说,OPQ在大多数中等维度数据集上比普通PQ有稳定提升,Faiss里也有现成实现。
残差量化(Residual Quantization,RQ)则是另一种思路:先做一次粗量化,对残差再做一次量化,逐级细化。多级RQ有点像用多本码本逐层逼近原始向量,好处是可以用较小的码本获得较低的量化误差,缺点是查表计算比PQ复杂一些。
我的个人经验是:如果数据分布比较均匀,普通PQ或者OPQ就够了;如果数据方差很大、有显著的长尾分布,考虑用残差量化配合倒排索引,效果会更稳定。
5. 数据压缩场景里,VQ依然是不容忽视的方案
5.1 语音与图像编码中的应用
说回最传统的应用领域:语音和图像编码。
语音信号经过特征提取后得到的一组参数向量,天然适合VQ。早期低比特率语音编码器大量使用VQ来量化LPC系数、线谱对(LSF)等参数。因为LSF参数有明确的物理意义和分布规律,用VQ能高效利用这些先验知识。
图像压缩里的VQ则更直观:把图像分成小块,比如4x4的像素块,拉成一个16维向量,然后对整个图像块的集合做码本训练。解码时只需要把每个块替换成码字再拼起来。这种方案在低比特率压缩下有不错的表现,虽然在高比特率下不如JPEG之类的变换编码,但VQ的思路启发了很多后续方法。
5.2 模型压缩与Embedding压缩
这几年VQ在深度学习里重新火了一把,主要是两个方向。
一个是Embedding压缩。推荐系统和搜索系统里的大规模Embedding表动辄几百GB,如果不做压缩,内存根本扛不住。用VQ把Embedding向量量化到有限的码字集合里,共享码字,可以大幅降低存储。这里和PQ的思路一致:对Embedding分段做乘积量化,既能压缩又保持一定的表达能力。
另一个是VQ-VAE这类生成模型。VQ-VAE把编码器的输出离散化到码本上,让模型学习的是一个离散的隐空间。这个思想源头就是VQ。虽然生成模型的细节比较复杂,但如果你理解了VQ的基本流程——码本训练、最近邻匹配、索引表示——再看VQ-VAE的代码会轻松很多。
5.3 从压缩比和保真度看VQ的定位
用VQ做压缩时,有两个指标永远是核心:
- 压缩率:原始表示所需比特数 / 压缩后表示所需比特数。
- 保真度:用量化后的表示重建出来的结果和原始结果的接近程度。
VQ在低比特率区域(比如每个向量只给几个比特)特别有优势,因为它能用索引表达丰富的联合模式。但在高比特率区域,效率逐渐下降,因为码本大小需要指数增长才能继续降低失真。
所以我的建议是:如果压缩率需求在100倍以下,先考虑传统变换+熵编码方案;如果需求是几百上千倍,VQ体系几乎是绕不开的选择。在检索场景,VQ的"压缩"目的不是还原信号,而是让内存、带宽、计算量可控,所以VQ对保真度的要求可以比较宽松——只要保序性(quantization前的相似排序和量化后的相似排序大致一致)就行。
6. 动手实现一个迷你VQ:从零到可用的完整代码
6.1 环境准备与数据生成
为了直观理解VQ,我写一个最简单的Python实现,只依赖numpy。
先准备数据:生成一些符合高斯混合分布的二维点,这样数据有聚集性,量化效果可视化比较明显。
import numpy as np import matplotlib.pyplot as plt np.random.seed(42) # 生成三个高斯簇作为训练数据 def generate_data(n_samples=3000): centers = [[0, 0], [5, 5], [0, 5]] data = [] for center in centers: cluster = np.random.randn(n_samples // 3, 2) * 0.8 + center data.append(cluster) return np.vstack(data) train_data = generate_data(3000) test_data = generate_data(900)这样我们就有一组二维训练数据和一组测试数据。维度低,跑起来很快,也方便画图观察码本和量化结果。
6.2 实现LBG码本训练
我直接写一个基础的LBG实现。不用现成的K-Means,而是演示分裂式的码本生长过程,更能体现VQ训练思路。
def lbg_train(data, codebook_size, max_iter=100, tol=1e-5): # 初始:所有样本的质心作为第一个码字 codebook = np.mean(data, axis=0, keepdims=True) while len(codebook) < codebook_size: # 分裂:每个码字生成两个新码字(加微小扰动) new_codebook = [] for codeword in codebook: new_codebook.append(codeword + 0.01) new_codebook.append(codeword - 0.01) codebook = np.array(new_codebook) # 迭代优化 for _ in range(max_iter): # 分配:每个样本归到最近的码字 distances = np.linalg.norm(data[:, None, :] - codebook[None, :, :], axis=2) labels = np.argmin(distances, axis=1) # 更新:每个码字取簇内样本的均值 new_codebook = np.copy(codebook) for i in range(len(codebook)): if np.sum(labels == i) > 0: new_codebook[i] = np.mean(data[labels == i], axis=0) # 检查是否收敛 if np.linalg.norm(new_codebook - codebook) < tol: codebook = new_codebook break codebook = new_codebook # 如果码本尺寸超出目标,截断 return codebook[:codebook_size]这段代码分成两块:分裂过程和迭代优化。分裂用±0.01扰动生成两个新码字,等价于把原来一个粗粒度的代表点细化成两个。迭代优化就是K-Means的E步和M步交替:先分配样本,再更新质心。
运行一下:
codebook = lbg_train(train_data, codebook_size=16) print("码本形状:", codebook.shape)输出码本形状是 (16, 2),表示我们得到了16个二维码字。
这里我想强调一个工程细节:在真实场景里,LBG的分裂步长0.01不一定适用所有数据范围。如果数据方差很大,扰动太小,分裂后的码字几乎重合,收敛很慢;扰动太大,又可能跳过了最优区域。更稳妥的做法是:扰动步长取该码字对应簇内标准差的一部分。比如 std * 0.1 之类的比例。我这里为了代码简单用固定值,实际使用时建议自适应。
6.3 量化与重建的完整流程
有了码本,量化就是对每个输入向量做最近邻搜索。
def quantize(data, codebook): distances = np.linalg.norm(data[:, None, :] - codebook[None, :, :], axis=2) indexes = np.argmin(distances, axis=1) return indexes def dequantize(indexes, codebook): return codebook[indexes] train_idx = quantize(train_data, codebook) test_idx = quantize(test_data, codebook) reconstructed_test = dequantize(test_idx, codebook)量化之后的test_idx就是每个向量的码字索引。如果码本大小为16,那么每个向量只需要4比特来表示。原始数据如果每个维度是float32,那就是2×32=64比特,压缩比是16倍。
我们计算一下量化误差:
quantization_error = np.mean(np.linalg.norm(test_data - reconstructed_test, axis=1)) print("平均量化误差:", quantization_error)这个误差就代表了压缩后的信息损失。误差越小,重建越接近原始数据。
6.4 可视化码本与量化效果
画出训练数据、码本位置、量化后测试数据的对比。
plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.scatter(train_data[:, 0], train_data[:, 1], s=5, alpha=0.5, label="训练数据") plt.scatter(codebook[:, 0], codebook[:, 1], marker="x", s=100, color="red", label="码字") plt.legend() plt.title("码本分布") plt.subplot(1, 2, 2) plt.scatter(test_data[:, 0], test_data[:, 1], s=5, alpha=0.3, label="测试数据") plt.scatter(reconstructed_test[:, 0], reconstructed_test[:, 1], s=5, alpha=0.3, color="green", label="重建数据") plt.legend() plt.title("测试数据 vs 重建数据") plt.tight_layout() plt.show()运行后能看到:码字分布在数据稠密区域,重建之后的测试数据被"拉"到码字附近。数据点之间的相对结构大体保留,比如原来左上角的两个簇,重建后依然能看出两个大致的聚集区,只是细节被抹平了。
这个迷你实验虽然简单,但它完整演示了VQ的三个核心环节:
- 用训练数据学码本。
- 用最近邻匹配把向量转成索引。
- 用索引查表重建向量。
6.5 维度灾难对码本训练的影响
二维实验很直观,但我要提醒:到了高维,码本训练的难度会急剧上升。
一个128维向量空间,如果要均匀覆盖各种方向,需要的码字数量是天文数字。实际中码本只能覆盖数据分布的高概率区域,所以"码本到底学得好不好"严重依赖训练样本的代表性。样本不够全面,码本就偏向某个区域,量化误差在长尾数据上会飙升。
这也是为什么高维场景下,直接VQ远不如PQ、OPQ这些改进方案。维度越高,越要拆开处理,每个子空间的维度保持在8到32之间,码本训练才相对可控。
7. 聊聊工程实践里的坑与经验
7.1 度量不一致导致的效果暴跌
一个我反复踩过的坑:训练码本时用的距离度量和线上查询时的距离度量不一致。
比如训练PQ码本时用欧氏距离,但线上排序用的是内积相似度。理论上如果向量都做了L2归一化,两者是等价的;但如果某个环节忘了归一化,或者归一化在量化前和量化后不一致,效果就会有明显滑坡。排查这类问题时,我通常会先检查预处理管道,确认归一化在量化之前完成。
7.2 码本训练的随机性与稳定性
LBG或者K-Means类的训练都依赖初始码本,随机初始化不同,最终码本也可能不同,量化误差有波动。如果追求稳定,可以多跑几次取最优质心,或者用K-Means++这类的初始化策略。
另外,训练样本的数量要充足。一个经验法则:码本大小为N时,每个码字平均至少有几十个训练样本,否则簇内统计不稳定。比如N=256,最好有至少数万条训练数据。
7.3 检索场景中nprobe与性能的权衡
在IVF+PQ体系里,nprobe是最常调的参数。nprobe越大,查询扫描的倒排列表越多,召回率越高,但延迟也越高。
我的调参习惯是:先固定nprobe=1,测一次召回率作为底线,然后逐步增加,观察召回率曲线的上升趋势。当增加nprobe带来的召回提升开始变得很平缓时,就停下来。因为继续增加nprobe,延迟会线性增长而召回收益越来越小,性价比很低。
另外,nlist的设置也很重要。nlist太小,每个倒排列表太长,查询压力大;nlist太大,聚类过细,边界样本分错簇的概率上升。一个粗经验:nlist设置为向量总数开平方的量级,然后根据实际召回和延迟再微调。
7.4 VQ在内存带宽受限场景下的优势
现代检索系统的瓶颈往往不在CPU计算,而在内存带宽。全量扫描浮点向量意味着要搬移大量字节;而PQ压缩后,内存访问量大幅降低,带宽压力显著缓解。
举个例子,1000万条128维float32向量,原始存储约5.12GB。用PQ压缩成每向量32字节(比如16个码字索引,每个索引占2字节),总存储降到320MB。这不仅让数据能放进内存缓存,也让顺序扫描提速很多倍。
7.5 何时不该用VQ
VQ不是银弹。如果数据本身是稀疏的、高维且每个维度独立性很强,VQ的码本很难学到有效的联合模式,此时压缩率可能上不去,量化误差也大。
这种情况下,可以考虑稀疏编码、哈希或者更简单的降维+标量量化组合。判断标准很简单:可视化一下或算一下数据的有效维度(比如PCA累积方差占比),如果前几十个主成分只占很少方差,说明数据本质上是高维独立的,VQ收益会很小。
8. 从VQ到现代ANN检索:索引结构里的VQ影子
8.1 Faiss里的PQ实现逻辑
如果你用过Meta的Faiss,会发现PQ相关的IndexPQ、IndexIVFPQ是高频工具。理解VQ后,看Faiss源码或者调参时会更有底。
在Faiss中,IndexPQ训练的就是per-subspace的码本,参数是M(子向量数量)和nbits(每个子码本的比特数)。默认nbits=8,也就是每段码本256个码字。M越大,压缩粒度越细,但存储和计算开销也增加。M的选择需要根据向量维度、内存预算、召回需求综合考虑。
我的一个调参例子:一个768维的文本Embedding向量,内存预算紧张时,我用M=48、nbits=8,每向量压缩到48字节。召回率相比原始向量全精度检索损失大约1到2个点,但内存降了32倍,延迟也稳定在毫秒内。如果召回要求更高,可以降到M=96,代价是内存翻倍。
8.2 ScaNN、DiskANN等新方案里的量化思想
Google的ScaNN里有一个核心组件叫Score-aware quantization,本质上还是在做VQ,但优化的目标更聪明:它不单纯最小化重构误差,而是让量化后的打分和真实打分尽可能同序。这比"最小化距离误差"更进一步,直接面向检索任务的度量质量。思想源头依然是VQ,只是目标函数变了。
微软的DiskANN则把PQ用在了SSD存储场景。它把全量向量用PQ压缩后存放在磁盘,用SSD的顺序吞吐换内存容量。图索引负责导航,PQ负责距离估计,这也是VQ在现代大规模检索系统里的典型落地方式。
可以看到,近十年的高性能ANN方案几乎没有离开VQ这个底层的量化思想。理解VQ的基本原理,会为阅读这些系统的论文和源码提供极大的方便。
8.3 学习VQ对做工程的实际价值
有人可能会问:我不做底层索引,理解VQ原理有什么直接收益?
我的看法是:工程上很多问题本质是"表示问题"。一个特征存成float32还是uint8,对系统性能的影响可能天差地别;一个向量的相似度是欧氏还是内积,决定了后续所有优化手段的适配性。理解了VQ,你就理解了"如何用更紧凑的表示保留最关键的信息",这套思维在特征工程、模型量化、缓存设计里都能复用。
9. 实测:量化参数对检索质量的具体影响
9.1 实验设置
为了让你对VQ参数的影响有个直观概念,我跑了一组对比实验。用公开的SIFT1M数据集子集,取10万条128维向量作为底库,查询集取100条。对比不同M值(子向量数)和不同nbits(每段比特数)下的召回率(Recall@10)和内存占用。
9.2 实验结果
| M值 | nbits | 每向量字节 | 压缩比 | Recall@10 |
|---|---|---|---|---|
| 0(原始) | - | 512 | 1x | 100% |
| 16 | 8 | 16 | 32x | 92.3% |
| 32 | 8 | 32 | 16x | 96.8% |
| 48 | 8 | 48 | 10.7x | 98.1% |
| 64 | 8 | 64 | 8x | 99.0% |
| 16 | 4 | 8 | 64x | 85.6% |
| 32 | 4 | 16 | 32x | 91.2% |
可以看到,压缩比和召回率直接矛盾。M从16增加到64,内存翻了4倍,召回率从92.3%提升到99.0%。如果追求极致压缩,nbits从8降到4,内存再减半,但召回率跌得比较明显。
9.3 从结果能得出什么结论
一个容易被忽略的结论:M值本身不是越大越好。M太大意味着每段子向量维度太低,例如128维用M=64,每段只有2维,码本难以捕捉段内结构;M太小,每段维度太高,码本训练压力大。存在一个适中的区间。
另一个结论是:nbits的选择对精度影响非常大。nbits=4意味着每段只有16个码字,表达能力太弱,适合极端内存受限的场景;nbits=8是工业默认值,因为256个码字基本能覆盖每个子空间的常见模式。
实际项目中,我会先在较小样本上做M和nbits的粗搜索,确定一个基线,再根据线上延迟和内存监控微调。没必要一开始就追求全部参数最优。
10. 最后说点个人经验
VQ这门技术,初看是一堆聚类和距离计算的组合,实际用下来你会发现它的精髓在于"用一种有损的表示,换取系统整体性能的大幅提升"。什么时候可以用有损、损失做到多大、怎么用更紧凑的结构逼近无损效果,这些才是工程里真正有价值的问题。
如果让我给新手一个学习路径:先跑通本文的迷你实验,理解码本和索引的含义;然后试试在Faiss里用IndexIVFPQ过一遍检索流程;最后再读PQ的原始论文,你会发现公式回到代码里都一一对应。这条路走完,你对向量检索的压缩侧基本就通了。
最后分享一个小技巧:调试VQ相关系统时,一定要把"量化误差的可视化"和"召回率/精度曲线"同时打出来,别只盯指标。很多时候量化误差降下来了,但召回没变化,说明瓶颈在别处(比如粗聚类质量);而量化误差还在高位但召回已经不错,说明冗余度高、可以进一步压缩。两幅图对照着看,定位问题会快很多。