大模型时代向量量化技术深度解析:从PQ、AQ到RVQ的实战选型
最近和几位负责大模型RAG系统落地的工程师聊天,大家不约而同地提到了同一个痛点:Embedding向量实在太占地方了。当我们把百万甚至千万级的文档切片转换成768维或1536维的向量后,存储成本直线上升,检索速度也开始变得捉襟见肘。这让我想起了几年前处理图像检索时遇到的类似困境,而当时破局的关键技术之一,正是向量量化。
向量量化本质上是一种有损压缩技术,但它聪明的地方在于,压缩的同时最大程度保留了向量间的相对距离关系——这对相似性搜索来说至关重要。想象一下,你不需要记住一幅画上每个像素的精确颜色,只需要记住它属于"莫奈风格"还是"梵高风格",就能在画廊里快速找到相似的作品。向量量化做的就是这个工作,只不过它处理的是高维数学空间中的"风格"。
在大模型应用井喷的今天,高效的向量检索已经成为AI系统的核心基础设施。无论是构建企业知识库、实现个性化推荐,还是开发智能对话助手,背后都离不开对海量向量数据的快速处理。今天我们就深入探讨三种主流的向量量化技术:Product Quantization(乘积量化)、Additive Quantization(加法量化)和Residual Vector Quantization(残差向量量化)。我不会只给你理论公式,而是结合实际的代码片段、性能测试数据和真实场景中的取舍经验,帮你建立清晰的选型框架。
1. 向量量化的核心逻辑:为什么我们需要压缩向量?
在深入具体算法之前,有必要先厘清一个基本问题:为什么非要对向量进行量化?直接存储原始浮点数向量不行吗?
让我们看一组真实的数据。假设你正在构建一个基于GPT-4的智能客服系统,需要检索企业内部的十万份技术文档。使用text-embedding-ada-002模型,每个文档切片会生成一个1536维的浮点数向量。如果使用标准的32位浮点数(float32)存储:
# 计算存储需求
num_vectors = 100_000 # 10万个向量
dimensions = 1536 # 每个向量的维度
bytes_per_float = 4 # float32占4字节
raw_storage = num_vectors * dimensions * bytes_per_float
print(f"原始向量存储需求: {raw_storage / (1024**3):.2f} GB")
运行这段代码,你会得到约5.86GB的存储需求。这还只是十万个文档——如果是百万级文档,存储需求将接近60GB。更关键的是,在进行相似性搜索时,系统需要计算查询向量与数据库中每个向量的距离(通常是余弦相似度或欧氏距离),计算复杂度为O(N×d),其中N是向量数量,d是维度。
注意:这里还没有考虑内存开销。在实际的向量数据库中,为了加速检索,通常需要将索引加载到内存中。60GB的数据意味着你需要一台配置极高的服务器,成本相当可观。
向量量化通过两个核心机制解决这个问题:
- 降低存储开销:将高精度浮点数向量转换为低比特表示(如8位整数),甚至用代码索引代替向量本身
- 加速距离计算:通过预计算的距离表或量化编码的快速运算,减少实时计算量
但量化不是免费的午餐。它引入了一个关键权衡:精度损失。量化后的向量是原始向量的近似表示,这意味着检索结果可能不是绝对精确的最近邻,而是近似最近邻(Approximate Nearest Neighbor, ANN)。在实际应用中,只要这种近似在可接受范围内,用少量精度换取巨大的性能和成本提升是完全值得的。
1.1 量化技术的演进脉络
为了更好地理解PQ、AQ、RVQ这三种技术的设计哲学,我们可以先看看它们的历史脉络:
| 技术 | 提出时间 | 核心创新 | 适用场景 |
|---|---|---|---|
| 传统K-means量化 | 1980年代 | 用聚类中心代表整个数据集 | 低维数据、码本较小 |
| Product Quantization (PQ) | 2010年 | 将高维空间分解为子空间的笛卡尔积 | 高维向量、内存受限 |
| Additive Quantization (AQ) | 2014年 | 用多个码本向量的和近似原始向量 | 需要高精度的检索任务 |
| Residual Vector Quantization (RVQ) | 2021年(现代形式) | 多阶段残差逼近,逐步细化 | 音频/视频编码、多粒度检索 |
这个演进过程反映了研究者们在不同约束条件下的思考:从"如何简单压缩"到"如何在压缩的同时保持精度",再到"如何实现多粒度的精度控制"。
2. Product Quantization:高维向量压缩的经典解法
我第一次在生产环境中使用PQ是在2018年,当时我们需要为一个电商平台的图像搜索系统构建索引。数据库里有超过500万张商品图片,每张图片通过ResNet提取出2048维的特征向量。原始存储需要近40TB空间,而我们的预算只允许使用10TB的SSD阵列。PQ成了我们的救命稻草。
2.1 PQ的工作原理:分而治之的艺术
PQ的核心思想非常直观:将高维向量拆分成多个低维子向量,分别对每个子空间进行量化。这就像是你不需要记住一整本字典,只需要记住每个字母开头的单词列表,需要时再组合起来。
让我们通过一个具体的例子来理解这个过程。假设我们有一个128维的向量,计划使用m=8个子向量,每个子向量16维:
import numpy as np
from sklearn.cluster import KMeans
def product_quantization_train(vectors, m=8, k=256):
"""
训练PQ量化器
参数:
vectors: 训练向量集, shape=(n_samples, d)
m: 子向量数量
k: 每个子空间的聚类中心数(必须是2的幂,便于编码)
返回:
codebooks: 码本列表,每个元素是一个聚类中心数组
sub_dim: 每个子向量的维度
"""
n_samples, d = vectors.shape
sub_dim = d // m
# 确保维度能被m整除
assert d % m == 0, f"维度{d}不能被{m}整除"
codebooks = []
# 对每个子空间独立训练量化器
for i in range(m):
# 提取第i个子空间的所有向量
sub_vectors = vectors[:, i*sub_dim:(i+1)*sub_dim]
# 使用K-means聚类
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(sub_vectors)
# 存储聚类中心(码本)
codebooks.append(kmeans.cluster_centers_)
return codebooks, sub_dim
# 示例:生成随机向量并训练PQ
np.random.seed(42)
n_samples = 10000
d = 128
vectors = np.random.randn(n_samples, d).astype(np.float32)
codebooks, sub_dim = product_quantization_train(vectors, m=8, k=256)
print(f"训练完成,每个码本包含{len(codebooks[0])}个聚类中心")
print(f"每个聚类中心维度: {codebooks[0].shape[1]}")
训练完成后,每个子空间都有一个包含256个聚类中心的码本。当需要量化一个新向量时:
- 将向量分割成8个16维的子向量
- 对每个子向量,在对应的码本中找到最近的聚类中心
- 用聚类中心的索引(0-255)代替原始子向量
这样,原本需要128个float32(512字节)的向量,现在只需要8个uint8(8字节)——压缩比达到64:1!
2.2 PQ的距离计算:非对称距离的妙用
PQ最巧妙的设计在于它的距离计算方式。传统方法需要先还原量化向量,再计算距离,但PQ使用了非对称距离计算(Asymmetric Distance Computation, ADC),直接通过预计算的距离表加速。
def build_distance_table(query_vector, codebooks):
"""
为查询向量构建距离表
参数:
query_vector: 查询向量, shape=(d,)
codebooks: PQ训练得到的码本列表
返回:
distance_table: 距离表,shape=(m, k)
"""
m = len(codebooks)
sub_dim = codebooks[0].shape[1]
k = codebooks[0].shape[0]
distance_table = np.zeros((m, k), dtype=np.float32)
# 分割查询向量
for i in range(m):
sub_query = query_vector[i*sub_dim:(i+1)*sub_dim]
# 计算查询子向量与当前子空间所有聚类中心的距离
for j in range(k):
# 欧氏距离平方(避免开方,节省计算)
diff = sub_query - codebooks[i][j]
distance_table[i, j] = np.dot(diff, diff)
return distance_table
def pq_search(query_vector, pq_codes, distance_table):
"""
使用PQ进行近似最近邻搜索
参数:
query_vector: 查询向量
pq_codes: 数据库中所有向量的PQ编码,shape=(n_vectors, m)
distance_table: 预计算的距离表
返回:
distances: 查询向量与每个数据库向量的距离
"""
n_vectors = pq_codes.shape[0]
m = pq_codes.shape[1]
distances = np.zeros(n_vectors, dtype=np.float32)
# 对每个数据库向量,通过查表求和得到距离
for i in range(n_vectors):
dist =

与选型指南&spm=1001.2101.3001.5002&articleId=153400301&d=1&t=3&u=1b28c24e569248a3ab1d84575e42064b)
5445

被折叠的 条评论
为什么被折叠?



