如何使用乘积量化压缩向量?

简介: 乘积量化通过将高维向量划分为多个低维子空间,对每个子空间聚类并用聚类ID表示子向量,大幅压缩存储空间。例如,1024维向量可分段聚类,用32比特替代原始4KB空间,压缩率达1/1024,显著提升内存加载与检索效率。

对于向量的相似检索,除了检索算法本身以外,如何优化存储空间也是我们必须要关注的一个技术问题。以 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。存储空间被大幅降低之后,所有的样本向量就有可能都被加载到内存中了。

相关文章
|
API iOS开发
彻底搞懂同步与异步,阻塞/非阻塞
彻底搞懂同步与异步,阻塞/非阻塞
3628 0
|
7月前
|
存储 机器学习/深度学习 算法
|
9月前
|
数据管理 开发者 Python
揭秘Python的__init__.py:从入门到精通的包管理艺术
__init__.py是Python包管理中的核心文件,既是包的身份标识,也是模块化设计的关键。本文从其历史演进、核心功能(如初始化、模块曝光控制和延迟加载)、高级应用场景(如兼容性适配、类型提示和插件架构)到最佳实践与常见陷阱,全面解析了__init__.py的作用与使用技巧。通过合理设计,开发者可构建优雅高效的包结构,助力Python代码质量提升。
936 10
|
机器学习/深度学习 搜索推荐 算法
协同过滤算法
协同过滤算法
1340 0
|
11月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
624 6
|
SQL 关系型数据库 MySQL
Pandas获取SQL数据库read_sql()函数及参数一文详解+实例代码
Pandas获取SQL数据库read_sql()函数及参数一文详解+实例代码
8585 0
Pandas获取SQL数据库read_sql()函数及参数一文详解+实例代码
|
12月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
762 1
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
793 5
|
安全 Java Unix
log4cplus最新介绍、详细编译过程及使用(最全面)
log4cplus最新介绍、详细编译过程及使用(最全面)