向量嵌入通常表示为高维浮点向量(例如 float32),它们包含丰富的语义信息。然而,存储和查询数百万或数十亿此类密集向量带来了很大的挑战。内存占用会非常大,并且在查询过程中计算高维浮点向量之间的距离计算量大,这直接影响延迟和吞吐量。为解决这些挑战,采用量化技术。在这种情况下,量化是指通过降低精度表示向量分量,从而减少每个向量所需内存的方法。这种压缩不仅节省内存,还能加速距离计算,尽管这可能以牺牲部分查询结果精度为代价(召回率可能降低)。我们将介绍两种主要的量化技术类别:标量量化和乘积量化。标量量化 (SQ)标量量化可能是最直接的方法。它独立地处理向量的每个维度(标量分量)。其核心思路是将每个维度中的原始浮点值映射到较小的值集中的一个值,该值通常用更少的比特表示。SQ 的常见形式包括:浮点数精度降低: 将 float32 向量转换为 float16(半精度)。这会立即将内存占用减半,并且可以加速支持 float16 操作的硬件上的计算,同时对许多应用的精度影响相对较小。整数量化: 将某个范围内的浮点值映射到整数,最常见的是 8 位整数 (int8)。对于每个维度,确定数据集中所有可能值的范围,并将此范围划分为 $2^8 = 256$ 个区间。然后,每个原始浮点值被映射到其所属区间的整数 ID。这将存储空间减少到每个维度 1 字节。距离计算有时可以使用专门为整数操作设计的 CPU 指令 (SIMD) 进行加速。二值量化: 一种极端形式,其中每个维度被减少到单个比特(0 或 1),通常基于原始值是正数还是负数,或者高于/低于某个阈值。这提供了每个维度的最大压缩,但通常会带来较大的信息损失。距离计算通常依赖汉明距离。SQ 的工作原理 (int8 示例):假设你的向量的某个维度在数据集中包含从 -10.0 到 +10.0 的值。确定范围: 找到此维度的最小值 (-10.0) 和最大值 (+10.0)。定义区间: 将范围 [-10.0, 10.0] 划分为 256 个等距区间。映射值: 对于此维度值为 3.5 的向量,确定它属于哪个区间并分配相应的 int8 值(例如,区间 172 可能表示从 3.4 到 3.55 的值)。存储: 存储 int8 值 (172) 而不是 float32 值 (3.5)。SQ 的优点:简单且易于实现。减少内存使用(例如,float32 到 int8 可实现 4 倍减少)。可以加速距离计算(特别是使用 SIMD 的 int8 或使用汉明距离的二值量化)。SQ 的缺点:压缩比受限于维度数量(例如,int8 总是每个维度使用 1 字节,无论原始精度如何)。信息损失在每个维度独立发生,这有时可能不成比例地影响方差大或重要的维度。对于非常高的压缩目标,SQ 可能会导致显著的精度下降。乘积量化 (PQ)乘积量化提供了一种更精巧的方法,通常能实现比 SQ 高得多的压缩比,同时力求在相同压缩程度下保留更多向量信息。PQ 不像 SQ 那样独立量化每个维度,而是处理向量的子段。PQ 的工作原理:子向量划分: 将每个高维向量 $v \in \mathbb{R}^D$ 划分为 $M$ 个维度为 $D/M$ 的独立子向量。(为简化说明,假设 $D$ 可被 $M$ 整除)。 $$ v = [v_1 | v_2 | \dots | v_M], \quad v_m \in \mathbb{R}^{D/M} $$码本生成(训练): 对于每个子向量位置 $m$(从 1 到 $M$),对整个数据集中所有第 $m$ 个子向量的集合运行聚类算法(通常是 k-means)。这会生成 $M$ 个独立的“码本”。每个码本 $C_m$ 包含 $k$ 个中心点(也称为码字),其中每个中心点 $c_{m,j} \in \mathbb{R}^{D/M}$ 代表在位置 $m$ 处找到的典型子向量。通常, $k$ 设置为 256,这样每个中心点索引可以存储在 8 比特(1 字节)中。向量编码: 为了量化原始向量 $v$,考虑其每个子向量 $v_m$。对于每个 $v_m$,找到其在相应码本 $C_m$ 中最近的中心点 $c_{m,j}$。原始向量 $v$ 然后通过最能近似其子向量的 $M$ 个中心点索引 (ID) 序列来表示。 $$ PQ(v) = [id(v_1), id(v_2), \dots, id(v_M)] $$ $$ id(v_m) = \underset{j \in {1, \dots, k}}{\operatorname{argmin}} | v_m - c_{m,j} |_2^2. $$存储: 我们存储 $M$ 个索引,而不是存储原始的 $D$ 维 float32 向量($D \times 32$ 比特)。如果 $k=256$,每个索引需要 $\log_2(256) = 8$ 比特。总存储空间为 $M \times 8$ 比特(或 $M$ 字节),与原始维度 $D$ 无关。这实现了大幅压缩,特别是当 $D$ 很大时。例如,一个 768 维的 float32 向量占用 3072 字节。使用 $M=96$(且 $k=256$)的 PQ 可将其压缩到仅 96 字节。距离计算(非对称距离计算 - ADC):完全量化向量之间的精确距离很少直接计算。相反,当使用查询向量 $q$ 进行查询时(通常保留其原始浮点格式),我们使用非对称距离计算(ADC)。将查询 $q$ 划分为 $M$ 个子向量 $[q_1 | q_2 | \dots | q_M]$。对于由 PQ 码 $[id_1, id_2, \dots, id_M]$ 表示的数据库向量,它与 $q$ 的近似欧氏距离平方可以通过求和查询子向量与从码本中查到的对应中心点之间的距离来计算: $$ d(q, PQ(v))^2 \approx \sum_{m=1}^{M} | q_m - c_{m, id_m} |2^2 $$ $c{m, id_m}$ 是与第 $m$ 个码本中存储的索引 $id_m$ 对应的中心点。为进一步加速此过程,每个查询子向量 $q_m$ 与第 $m$ 个码本 ($C_m$) 中所有 $k$ 个中心点之间的距离可以在查询到来时预先计算。最终的距离求和就变成对这些预计算距离表的一系列查找操作。PQ 的优点:实现非常高的压缩比,这基本上独立于原始向量维度 $D$。在相同压缩水平下,通常比 SQ 更好地保持查询精度,因为它优化了子向量空间中的量化误差。使用 ADC 和预计算表进行距离计算可以非常快速。PQ 的缺点:比 SQ 更难理解和实现。需要有代表性的训练数据集,通过 k-means 构建码本,这计算成本可能较高。$M$(子向量数量)和 $k$(每个码本的中心点数量)的选择需要在压缩率、精度以及码本/距离表的内存使用之间进行权衡。引入量化误差,影响查询精度;其质量很大程度上取决于训练数据和聚类过程。SQ 与 PQ 对比标量量化和乘积量化的选择取决于应用的具体要求,特别是所需的内存占用、查询速度和可接受的精度损失之间的平衡。特性标量量化(例如 int8)乘积量化(例如 M 个子段,k=256)机制独立量化每个维度使用码本量化子向量压缩中等(例如 float32 到 int8 为 4 倍)高(例如 D=768, M=96 为 32 倍)内存$D \times (\text{每维度比特数})$$M \times \log_2(k)$ 比特(加码本)复杂度较低较高(需要训练)训练数据非严格要求(但有助于确定范围)k-means 聚类所需精度损失大幅压缩时可能较高相同压缩比下通常较低距离计算简单(可能通过 SIMD 加速)ADC(通过预计算表快速实现)典型用途中等压缩,简单性高压缩,大数据集{ "data": [ { "x": ["Float32", "Float16 (SQ)", "Int8 (SQ)", "PQ (M=96, k=256)"], "y": [3072, 1536, 768, 96], "type": "bar", "marker": { "color": ["#4263eb", "#74c0fc", "#15aabf", "#12b886"] } } ], "layout": { "title": "每个向量的内存占用(768 维)", "yaxis": { "title": "每个向量的字节数" }, "xaxis": { "title": "表示方式" }, "margin": { "l": 60, "r": 20, "t": 40, "b": 80 } } }不同表示方式下,每个 768 维向量的估计内存使用量。与 SQ 方法相比,PQ 实现了更高的压缩。SQ 和 PQ 都是管理大规模向量查询资源需求的核心工具。SQ 提供简洁性,而 PQ 为实现更大程度的压缩提供了途径,使其成为许多处理数十亿向量的生产系统的核心技术。理解两者的机制和权衡对优化向量查询流程非常必要。在此基础上,下一节将介绍优化乘积量化 (OPQ),这是一种旨在进一步提升 PQ 效果的改进方法。