对于向量的相似检索,除了检索算法本身以外,如何优化存储空间也是我们必须要关注的一个技术问题。以 1024 维的向量为例,因为每个向量维度值是一个浮点数(浮点数就是小数,一个浮点数有 4 个字节),所以一个向量就有 4K 个字节。如果是上亿级别的数据,光是存储向量就需要几百 G 的内存,这会导致向量检索难以在内存中完成检索。
因此,为了能更好地将向量加载到内存中,我们需要压缩向量的表示。比如说,我们可以用聚类中心的向量代替聚类中的每个向量。这样,一个类内的点都可以用这个类的 ID 来代替和存储,我们也就节省了存储每个向量的空间开销。那计算查询向量和原始样本向量距离的过程,也就可以改为计算查询向量和对应聚类中心向量的距离了。
想要压缩向量,我们往往会使用 向量量化(Vector Quantization)技术。其中,我们最常用的是 乘积量化(Product Quantization)技术。
乍一看,你会觉得乘积量化是个非常晦涩难懂的概念,但它其实并没有那么复杂。接下来,我就把它拆分成乘积和量化这两个概念,来为你详细解释一下。
量化指的就是将一个空间划分为多个区域,然后为每个区域编码标识。比如说,一个二维空间 可以被划为两块,那我们只需要 1 个比特位就能分别为这两个区域编码了,它们的空间编码分别是 0 和 1。那对二维空间中的任意一个点来说,它要么属于区域 0,要么属于区域 1。
这样,我们就可以用 1 个比特位的 0 或 1 编码,来代替任意一个点的二维空间坐标 了 。假设 x 和 y 是两个浮点数,各 4 个字节,那它们一共是 8 个字节。如果我们将 8 个字节的坐标用 1 个比特位来表示,就能达到压缩存储空间的目的了。前面我们说的用聚类 ID 代替具体的向量来进行压缩,也是同样的原理。
而 乘积指的是高维空间可以看作是由多个低维空间相乘得到的。我们还是以二维空间 为例,它就是由两个一维空间和相乘得到。类似的还有,三维空间 是由一个二维空间 和一个一维空间相乘得到,依此类推。
那将高维空间分解成多个低维空间的乘积有什么好处呢?它能降低数据的存储量。比如说,二维空间是由一维的 x 轴和 y 轴相乘得到。x 轴上有 4 个点 x1 到 x4,y 轴上有 4 个点 y1 到 y4,这四个点的交叉乘积,会在二维空间形成 16 个点。但是,如果我们仅存储一维空间中,x 轴和 y 轴的各 4 个点,一共只需要存储 8 个一维的点,这会比存储 16 个二维的点更节省空间。
总结来说,对向量进行乘积量化,其实就是将向量的高维空间看成是多个子空间的乘积,然后针对每个子空间,再用聚类技术分成多个区域。最后,给每个区域生成一个唯一编码,也就是聚类 ID。
好了,乘积量化压缩向量的原理我们已经知道了。接下来,我们就通过一个例子来说说,乘积量化压缩样本向量的具体操作过程。
如果我们的样本向量都是 1024 维的浮点数向量,那我们可以将它分为 4 段,这样每一段就都是一个 256 维的浮点向量。然后,在每一段的 256 维的空间里,我们用聚类算法将这 256 维空间再划分为 256 个聚类。接着,我们可以用 1 至 256 作为 ID,来为这 256 个聚类中心编号。这样,我们就得到了 256 4 共 1024 个聚类中心,每个聚类中心都是一个 256 维的浮点数向量(256 4 字节 = 1024 字节)。最后,我们将这 1024 个聚类中心向量都存储下来。
这样,对于这个空间中的每个向量,我们就不需要再精确记录它在每一维上的权重了。我们只需要将每个向量都分为四段,让 每段子向量都根据聚类算法找到所属的聚类,然后用它所属聚类的 ID 来表示这段子向量 就可以了。
因为聚类 ID 是从 1 到 256 的,所以我们只需要 8 个比特位就可以表示这个聚类 ID 了。由于完整的样本向量有四段,因此我们用 4 个聚类 ID 就可以表示一个完整的样本向量了,也就一共只需要 32 个比特位。因此,一个 1024 维的原始浮点数向量(共 1024 * 4 字节)使用乘积量化压缩后,存储空间变为了 32 个比特位,空间使用只有原来的 1/1024。存储空间被大幅降低之后,所有的样本向量就有可能都被加载到内存中了。