假设场景:
- 任务: 在图片数据库中快速找到和“小猫”图片最相似的图片。
- 技术: 使用深度学习模型将每张图片转换为一个 128维 的向量表示(嵌入向量)。向量中的每个数值代表图片的某种视觉特征(如颜色、纹理、形状等)。
- 问题: 数据库有1亿张图片。如果采用暴力搜索(即计算查询向量与所有1亿个128维向量的距离),速度太慢(几秒甚至分钟级),内存占用也极大(存储128维浮点数向量需要大量内存)。
- 解决方案: 使用 PQx(例如
PQ8)。
🧩 步骤1:PQx 向量切分 (x=8 的例子)
-
原始向量维度 (D): 128
-
设定参数 x: 8
-
切分: 将每个128维向量平均切成 8段。
-
每段向量长度为
D/x = 128/8 = 16维。 -
例如:向量
[v1, v2, ..., v128]被切分成:- 第1段:
[v1, v2, ..., v16] - 第2段:
[v17, v18, ..., v32] - ...
- 第8段:
[v113, v114, ..., v128]
- 第1段:
-
-
约束:
x必须能被D整除。128/8=16成立。如果D=128,x=5(不能被整除),则无法均匀切分,程序会报错。
📚 步骤2:每段独立量化(构建码本)
-
目的: 对每一段 16 维的子空间,定义一个简化的“字典”(码本),用来近似表示该子空间内任何可能的向量。
-
过程 (训练阶段):
-
收集大量代表性图片向量(如100万个)。
-
独立地对每一段进行 K-Means 聚类:
- 只关注所有向量的 第一段(16维)。进行 K-Means 聚类(例如
k=256),得到256个 代表向量(码字)。这些码字是第一段子空间的最佳近似点。形成一个第1段码本。 - 同理,独立地对所有向量的 第二段(16维) 进行 K-Means 聚类(
k=256),得到第2段码本。 - 重复此过程,直到为全部8段各自建立独立的码本(每个码本包含256个16维的码字)。
k=256意味着每段可以用一个0-255的整数(8位)索引来表示。k是另一个可调参数,这里固定为256以简化说明。
- 只关注所有向量的 第一段(16维)。进行 K-Means 聚类(例如
-
-
结果: 我们有了 8个独立的码本,每个码本包含 256 个16维向量(码字)。
-
可视化: 想象有8个不同的字典(Dictionaries),字典1专用于描述图片的第1-16个特征组合,字典2用于描述17-32个特征组合,...,字典8用于描述113-128个特征组合。
📦 步骤3:数据库向量编码(预处理)
-
目的: 使用码本将数据库中的每个完整128维向量压缩表示。
-
过程:
-
对数据库中的每一张图片的128维向量:
-
按
x=8切分成8段(每段16维)。 -
对每一段,在它对应的码本中找到与其最相似(距离最近)的码字,并记录其索引(0-255)。
-
例如:
- 第一段
[v1, v2, ..., v16]在 第1段码本 中找到最近码字索引 = 42。 - 第二段
[v17, v18, ..., v32]在 第2段码本 中找到最近码字索引 = 173。 - ...
- 第八段
[v113, v114, ..., v128]在 第8段码本 中找到最近码字索引 = 8。
- 第一段
-
-
组合这些索引: 该图片向量的 PQ 编码 就是
[42, 173, ..., 8](共8个整数)。
-
-
-
存储:
- 原始方式: 存储128个浮点数(单精度)≈ 128 * 4 bytes = 512 bytes/向量。
- PQ8 方式: 存储8个整数(每个整数1字节)≈ 8 bytes/向量。
-
内存节省: (512 - 8) / 512 ≈ 98.4% 的压缩率! 这使得1亿张图片的向量可以轻松装入单台服务器的内存。
🔍 步骤4:查询过程(快速检索)
-
目的: 给定一个查询向量(代表“小猫”的128维向量),快速在数据库中找出最相似的图片。
-
过程:
-
切分查询向量: 将查询向量也切成8段(每段16维)。
-
预计算距离表(关键加速!): 对于每一段:
-
计算该段查询子向量与本段码本中 所有256个码字 的距离(16维距离计算)。
-
将计算出的256个距离值(浮点数)存储在一个 查询表(Lookup Table) 中。
-
例如:
- 查询表1(对应第1段): 查询子段1 vs 码本1所有256个码字的距离 =
[dist(1,0), dist(1,1), ..., dist(1,255)] - 查询表2(对应第2段): 查询子段2 vs 码本2所有256个码字的距离 =
[dist(2,0), dist(2,1), ..., dist(2,255)] - ...
- 查询表8(对应第8段): 查询子段8 vs 码本8所有256个码字的距离 =
[dist(8,0), dist(8,1), ..., dist(8,255)]
- 查询表1(对应第1段): 查询子段1 vs 码本1所有256个码字的距离 =
-
-
查找和累加距离: 对于数据库中每一个向量(它已被表示为8个索引,如
[idx1, idx2, ..., idx8]):- 查表: 对于它的第1个索引
idx1,从 查询表1 中取出对应的距离dist(1, idx1)。 - 查表: 对于它的第2个索引
idx2,从 查询表2 中取出对应的距离dist(2, idx2)。 - ...
- 查表: 对于它的第8个索引
idx8,从 查询表8 中取出对应的距离dist(8, idx8)。 - 累加: 将这8个距离值
dist(1, idx1) + dist(2, idx2) + ... + dist(8, idx8)相加。这个和就是查询向量与该数据库向量的近似距离(采用非对称距离计算ADC方式)。
- 查表: 对于它的第1个索引
-
排序和筛选: 对所有数据库向量计算完这个近似距离后,找出距离最小的前
K个(TopK),它们就是最相似的候选结果。
-
-
速度优势:
-
暴力搜索: 计算1个向量距离需要
128次(维度) 浮点运算(乘加)。 -
PQ8搜索:
- 预计算开销: 建8张距离表,每表计算256次距离,每次距离计算涉及16维。运算量 =
8 tables * 256 distance_calculations * 16 dimensions = 32768 次操作。这是一次性开销,与数据库大小无关。 - 单个向量开销: 每个向量只需要 8次查表 和 7次加法(加法可忽略)。主要就是8次查表(内存访问),几乎不涉及复杂计算。
- 预计算开销: 建8张距离表,每表计算256次距离,每次距离计算涉及16维。运算量 =
-
加速比: 对1亿个向量:
- 暴力:
128 * 1e9=1280 亿次浮点运算。 - PQ8:
32768(建表) +1e9 * 8次查表≈8亿次查表+ 建表。查表开销远小于高维浮点运算,整体加速10倍~100倍以上,达到毫秒级响应。
- 暴力:
-
🔧 参数 x 的影响权衡
-
x增大(例如从PQ4->PQ8->PQ16):-
优点 (召回率↑ / 精度↑):
- 每段维度更小 (
d = D/x),单个码本建模更容易(但可能略粗糙)。 - 关键优势: 组合码本数量呈指数级增长!(256^x)
PQ4的总组合数是256^4 ≈ 42亿种表示。PQ8是256^8 ≈ 18.4 * 10^18种表示。表示能力更强,能更精细地刻画原始向量,近似误差更小,召回率通常更高(找到真正近邻的概率增大)。
- 每段维度更小 (
-
缺点 (耗时↑ / 内存↑):
- 距离表变大: 需要预计算和存储
x个距离表(每个表256个距离值)。内存占用与x线性增长(仍很小)。 - 查表次数增加: 计算每个向量的距离需要
x次查表和累加。计算量与x线性增长。x越大,遍历整个数据库的速度越慢。 - 训练开销增大: 需要训练更多的码本。
- 距离表变大: 需要预计算和存储
-
-
x减小(例如从PQ16->PQ8->PQ4):-
优点 (耗时↓ / 内存↓):
- 距离表更少更小。
- 每个向量查表次数更少(
x次),搜索速度更快。 - 训练开销更小。
-
缺点 (召回率↓ / 精度↓):
- 每段维度更大 (
d = D/x),码本建模更复杂(但可能略精确)。 - 关键劣势:组合码本数量急剧下降(256^x)。表示能力减弱,近似误差更大,召回率通常更低(更容易错过真正的近邻)。
- 每段维度更大 (
-
📌 总结与示例选择
-
目的: 在内存紧张 (
RAM < 1GB) 且需要极快响应 (<10ms) 的手机端图片搜索 App 中使用 PQ。 -
向量: 128 维。
-
抉择:
-
PQ4(x=4, 每段32维): 速度最快 (每向量只需4次查表),内存占用最小 (编码4字节)。但召回率较低。 -
PQ8(x=8, 每段16维): 速度较快 (8次查表/向量),内存占用小 (8字节/向量),召回率显著优于PQ4(因为有天文数字的组合)。通常推荐作为平衡点,兼顾速度、内存和召回率。 -
PQ16(x=16, 每段8维): 速度较慢 (16次查表/向量,接近暴力搜索速度的1/2~1/4)。内存稍大 (16字节/向量)。召回率最高。
-
-
最终选择:
PQ8。满足小内存 (1亿向量占用约 0.8GB 内存)、快速度 (1亿向量检索在数 ms - 数十 ms 级别)、且召回率相对较好。
⚠️ 关键点回顾
-
切分 (
x): 把高维向量切成x段。x必须整除向量维度。 -
独立量化: 对每一段子空间单独构建一个小的码本(通常包含256个中心)。
-
编码 (存储): 数据库向量表示为它的每一段在各自码本中最接近中心的索引序列。索引序列长度等于
x。内存占用 =x字节。 -
查询加速: 基于预计算的查表 (Lookup Table) 来计算近似距离,避免了原始的高维向量距离计算。查表次数 =
x/向量。 -
x的权衡:-
x↑(更多段): 召回率↑ (通常),速度↓,训练开销↑,距离表内存↑。 -
x↓(更少段): 速度↑,内存↓,训练开销↓,召回率↓。
-
通过乘积量化,我们巧妙地将高维向量空间分解为多个低维子空间的笛卡尔积,利用“分而治之”的思想和查表机制,在保证不错召回率的前提下,实现了内存占用的巨大压缩和搜索速度的显著提升!🎉