embedding向量索引之---PQ(Product Quantization,乘积量化)

222 阅读9分钟

​假设场景:​

  • ​任务:​​ 在图片数据库中快速找到和“小猫”图片最相似的图片。
  • ​技术:​​ 使用深度学习模型将每张图片转换为一个 ​​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]
  • ​约束:​x 必须能被 D 整除。128/8=16 成立。如果 D=128, x=5(不能被整除),则无法均匀切分,程序会报错。


📚 ​​步骤2:每段独立量化(构建码本)​

  • ​目的:​​ 对每一段 16 维的子空间,定义一个简化的“字典”(码本),用来近似表示该子空间内任何可能的向量。

  • ​过程 (训练阶段):​

    1. 收集大量代表性图片向量(如100万个)。

    2. ​独立地对每一段进行 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以简化说明。
  • ​结果:​​ 我们有了 ​​8个独立的码本​​,每个码本包含 ​​256 个16维向量(码字)​​。

  • ​可视化:​​ 想象有8个不同的字典(Dictionaries),字典1专用于描述图片的第1-16个特征组合,字典2用于描述17-32个特征组合,...,字典8用于描述113-128个特征组合。


📦 ​​步骤3:数据库向量编码(预处理)​

  • ​目的:​​ 使用码本将数据库中的每个完整128维向量压缩表示。

  • ​过程:​

    • 对数据库中的每一张图片的128维向量:

      1. x=8 切分成8段(每段16维)。

      2. ​对每一段​​,在它对应的码本中找到与其​​最相似(距离最近)的码字​​,并​​记录其索引(0-255)​​。

        • 例如:

          • 第一段 [v1, v2, ..., v16] 在 ​​第1段码本​​ 中找到最近码字索引 = 42。
          • 第二段 [v17, v18, ..., v32] 在 ​​第2段码本​​ 中找到最近码字索引 = 173。
          • ...
          • 第八段 [v113, v114, ..., v128] 在 ​​第8段码本​​ 中找到最近码字索引 = 8。
      3. ​组合这些索引:​​ 该图片向量的 ​​PQ 编码​​ 就是 [42, 173, ..., 8](共8个整数)。

  • ​存储:​

    • ​原始方式:​​ 存储128个浮点数(单精度)≈ 128 * 4 bytes = ​​512 bytes/向量​​。
    • ​PQ8 方式:​​ 存储8个整数(每个整数1字节)≈ ​​8 bytes/向量​​。
  • ​内存节省:​​ (512 - 8) / 512 ≈ ​​98.4% 的压缩率!​​ 这使得1亿张图片的向量可以轻松装入单台服务器的内存。


🔍 ​​步骤4:查询过程(快速检索)​

  • ​目的:​​ 给定一个查询向量(代表“小猫”的128维向量),快速在数据库中找出最相似的图片。

  • ​过程:​

    1. ​切分查询向量:​​ 将查询向量也切成8段(每段16维)。

    2. ​预计算距离表(关键加速!):​对于每一段

      • 计算该段查询子向量与本段码本中 ​​所有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)]
    3. ​查找和累加距离:​​ 对于数据库中每一个向量(它已被表示为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方式)。
    4. ​排序和筛选:​​ 对所有数据库向量计算完这个近似距离后,找出距离最小的前 K 个(TopK),它们就是最相似的候选结果。

  • ​速度优势:​

    • ​暴力搜索:​​ 计算1个向量距离需要 128次 (维度) 浮点运算(乘加)。

    • ​PQ8搜索:​

      • ​预计算开销:​​ 建8张距离表,每表计算256次距离,每次距离计算涉及16维。运算量 = 8 tables * 256 distance_calculations * 16 dimensions = 32768 次操作。这是​​一次性开销​​,与数据库大小无关。
      • ​单个向量开销:​​ 每个向量只需要 ​​8次查表​​ 和 ​​7次加法​​(加法可忽略)。主要就是8次查表(内存访问),几乎不涉及复杂计算。
    • ​加速比:​​ 对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亿 种表示。PQ8256^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 级别)、且召回率相对较好。


⚠️ ​​关键点回顾​

  1. ​切分 (x):​​ 把高维向量切成 x 段。x 必须整除向量维度。

  2. ​独立量化:​​ 对每一段子空间单独构建一个小的码本(通常包含256个中心)。

  3. ​编码 (存储):​​ 数据库向量表示为它的每一段在各自码本中最接近中心的​​索引序列​​。索引序列长度等于 x。内存占用 = x 字节。

  4. ​查询加速:​​ 基于预计算的​​查表 (Lookup Table)​​ 来计算近似距离,避免了原始的高维向量距离计算。查表次数 = x/向量

  5. x 的权衡:​

    • x↑ (更多段):​​ 召回率↑ (通常),速度↓,训练开销↑,距离表内存↑。
    • x↓ (更少段):​​ 速度↑,内存↓,训练开销↓,召回率↓。

通过乘积量化,我们巧妙地将高维向量空间分解为多个低维子空间的笛卡尔积,利用“分而治之”的思想和查表机制,在保证不错召回率的前提下,实现了内存占用的巨大压缩和搜索速度的显著提升!🎉