MUVERA 多向量检索:固定维度编码(FDE)Python 实现全解析
在大规模检索系统中,文档常常以多条向量表示以提升准确性,但这也带来了检索变慢的困境。MUVERA(Multi-Vector Retrieval via Fixed Dimensional Encodings)提出了“固定维度编码”(Fixed-Dimensional Encoding,简称 FDE)方法,将多向量集合压缩为单个高维向量,同时保留原始集的相似度特征。本文将用通俗且实用的语言,带你从原理、实现到使用,全面了解 FDE 的 Python 实现。
目录
-
-
-
2.1 空间分区(SimHash + Gray Code) -
2.2 向量聚合:求和与平均 -
2.3 重复与串联:强化局部结构
-
-
-
-
-
5.1 Gray Code 相关函数 -
5.2 随机矩阵生成 -
5.3 分区索引计算
-
-
-
-
-
-
-
-
-
为啥要用 FDE?多向量检索的难题
想象一下,你有 10 亿篇文档,每篇都用 100 条向量来表示内容——这是 ColBERT 风格的多向量方案,能够捕捉文档细节、提升检索准确度;但如果对每条都做点积搜索,开销瞬间爆棚。
传统方案:
-
文档=单向量 → 检索飞快,但粗糙容易漏召。
现代方案:
-
文档=数百向量 → 精准却拖沓。
固定维度编码(FDE) 的核心思想:
“把多条向量装进一个大包里,完成后再比包的相似度,就能近似原来多向量的 Chamfer 相似度。”
换句话说,我们用一次点积运算代替遍历数百条向量,大幅加速检索而精度损失可控。
FDE 原理:魔法为何可行?
FDE 的魔法主要在三步:空间分区、向量聚合、重复串联。下面用简单比喻解释。
2.1 空间分区(SimHash + Gray Code)
-
随机高斯投影(SimHash)
用一个高斯随机矩阵把原始向量映射到“±1”签名:类似把向量投影到多条随机超平面,正负两侧分别标记为 1/-1。
-
Gray Code 编码
把多个 ±1 串成二进制再转 Gray Code,得到一个整数,代表“这个向量落在空间的哪个块里”。-
Gray Code 的优点:相邻分区只有一位不同,能更好地保留向量连续性。
-
-
分区数量
– 用num_simhash_projections
个投影,就能划分出2^k
个分区(例如 6 个投影 → 64 个块)。
2.2 向量聚合:求和与平均
-
Query(查询):分区内向量求和 -
Document(文档):分区内向量求平均
为什么不同?
查询通常少量向量,加和足够;文档向量量大,用平均避免某个块异常值过强影响。
2.3 重复与串联:强化局部结构
一次分区难免有误,把上面流程 重复 r 次(使用不同随机种子),再把每次结果串起来,得到最终 FDE 向量。
-
维度 = r × num_partitions × projection_dim
-
例如:20 次 × 64 块 × 128 投影维度 = 163840
环境准备与安装
在 macOS、Linux、Windows 下,只要安装 Python3 和 NumPy,就能无缝运行本项目。
# 克隆代码
git clone https://github.com/your-repo/fde_generator_python.git
cd fde_generator_python
# 建议新建虚拟环境
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
.\.venv\Scripts\activate # Windows
# 安装依赖
pip install -r requirements.txt
requirements.txt
numpy>=1.24 tqdm # 可选,用于进度条显示
确认安装成功:
python -c "import numpy; print(numpy.__version__)"
配置类详解
from enum import Enum
from dataclasses import dataclass
from typing import Optional
class EncodingType(Enum):
DEFAULT_SUM = 0 # 查询:各分区向量求和
AVERAGE = 1 # 文档:各分区向量求平均
class ProjectionType(Enum):
DEFAULT_IDENTITY = 0 # 不降维
AMS_SKETCH = 1 # AMS Sketch 方法降维
@dataclass
class FixedDimensionalEncodingConfig:
dimension: int = 128 # 原始向量维度
num_repetitions: int = 10 # 重复次数
num_simhash_projections: int = 6 # SimHash 投影数
seed: int = 42 # 随机种子
encoding_type: EncodingType = EncodingType.DEFAULT_SUM
projection_type: ProjectionType = ProjectionType.DEFAULT_IDENTITY
projection_dimension: Optional[int] = None
fill_empty_partitions: bool = False
final_projection_dimension: Optional[int] = None
参数 | 含义 |
---|---|
dimension | 原始每条向量的维度 |
num_repetitions | 随机分区与聚合重复的次数 |
num_simhash_projections | SimHash 投影位数,决定分区个数 = 2^k |
seed | 随机数种子,确保可复现 |
encoding_type | 编码类型:查询求和 / 文档平均 |
projection_type | 降维方式:不降维 / AMS Sketch |
projection_dimension | 中间投影降维后的维度 |
fill_empty_partitions | 是否对空分区填充零 |
final_projection_dimension | 最终 FDE 向量降维后的维度 |
内部辅助函数逐一拆解
5.1 Gray Code 相关函数
def _append_to_gray_code(gray_code: int, bit: bool) -> int:
return (gray_code << 1) + (int(bit) ^ (gray_code & 1))
-
作用:在已有 Gray Code 上追加一位,保持 Gray Code 邻接特性。
def _gray_code_to_binary(num: int) -> int:
mask = num >> 1
while mask:
num ^= mask
mask >>= 1
return num
-
作用:将 Gray Code 转回普通二进制,可用于校验或其他需要。
以上算法与原生 C++ 中的
internal::AppendToGrayCode
、internal::GrayCodeToBinary
保持一致,只是 Python 用更易读的循环替代位运算技巧。
5.2 随机矩阵生成
def _simhash_matrix_from_seed(dimension: int, num_projections: int, seed: int):
rng = np.random.default_rng(seed)
# 返回 dimension × num_projections 的高斯随机矩阵
return rng.normal(loc=0.0, scale=1.0, size=(dimension, num_projections))
-
用途:生成 SimHash 投影用的随机矩阵。
def _ams_projection_matrix_from_seed(dimension: int, projection_dim: int, seed: int):
# 构造稀疏随机矩阵,每行仅一个非零值
# 与 C++ 的 AMS Sketch 方法一一对应
-
用途:AMS Sketch 降维时使用,稀疏、高效。
5.3 分区索引计算
def _simhash_partition_index_gray(sketch_vector: np.ndarray) -> int:
partition_index = 0
for val in sketch_vector:
partition_index = _append_to_gray_code(partition_index, val > 0)
return partition_index
-
逻辑:对每个 SimHash 投影结果(正/负),按 Gray Code 拼接,得到分区编号。
核心算法流程一步步走
以下伪代码概览了 _generate_fde_internal()
的关键逻辑:
def _generate_fde_internal(point_cloud, config):
# 1. 校验输入:shape、dtype 是否符合预期
# 2. 计算分区数 = 2^config.num_simhash_projections
# 3. 若 projection_type != IDENTITY,则生成投影矩阵
# 4. 创建输出数组:shape = (num_repetitions, num_partitions, proj_dim)
for rep in range(config.num_repetitions):
# 4.1 生成 SimHash 矩阵 seed = config.seed + rep
# 4.2 对点云做矩阵乘 + 符号判断 → sketch_vectors
# 4.3 计算每条向量分区编号 → indices
# 4.4 若 config.fill_empty_partitions,先初始化全部为零
# 4.5 将各向量根据 indices:累加或平均写入对应分区
# 4.6 若 projection_type 是 AMS_SKETCH,应用中间降维
# 5. 将各 rep 结果沿最后一维拼接,得最终 FDE
# 6. 若 final_projection_dimension,做一次 Count Sketch 降维
return fde_vector
-
时间复杂度:O(n × d × r × k),其中 n=向量数,d=原始维度,r=重复次数,k=SimHash 投影数 -
输出维度: r × 2^k × proj_dim
对外 API:一行代码生成 FDE
def generate_query_fde(point_cloud: np.ndarray, config: FixedDimensionalEncodingConfig):
config.encoding_type = EncodingType.DEFAULT_SUM
return _generate_fde_internal(point_cloud, config)
def generate_document_fde(point_cloud: np.ndarray, config: FixedDimensionalEncodingConfig):
config.encoding_type = EncodingType.AVERAGE
return _generate_fde_internal(point_cloud, config)
def generate_fde(point_cloud: np.ndarray, config: FixedDimensionalEncodingConfig):
# 根据 config.encoding_type 路由到上面两个函数
这样,你只需传入形状为 (num_vectors, dimension)
的 numpy 数组和配置对象,即可获得 FDE 向量。
C++ → Python 对照表
C++ 实现 | Python 实现函数 | 说明 |
---|---|---|
GenerateFixedDimensionalEncoding() |
generate_fde() |
顶层路由函数 |
GenerateQueryFixedDimensionalEncoding() |
generate_query_fde() |
查询编码 |
GenerateDocumentFixedDimensionalEncoding() |
generate_document_fde() |
文档编码 |
internal::SimHashPartitionIndex() |
_simhash_partition_index_gray() |
分区索引 |
internal::ApplyCountSketchToVector() |
(内部)Count Sketch 降维 | 最终降维 |
使用示例
9.1 基础用法
import numpy as np
from fde_generator import (
FixedDimensionalEncodingConfig,
generate_query_fde,
generate_document_fde
)
config = FixedDimensionalEncodingConfig(
dimension=128,
num_repetitions=10,
num_simhash_projections=6,
seed=42
)
# 模拟查询与文档向量
query_vectors = np.random.randn(32, 128).astype(np.float32)
doc_vectors = np.random.randn(80, 128).astype(np.float32)
# 生成 FDE
q_fde = generate_query_fde(query_vectors, config)
d_fde = generate_document_fde(doc_vectors, config)
# 计算相似度(近似 Chamfer 相似度)
score = np.dot(q_fde, d_fde)
print("相似度分数:", score)
9.2 带降维配置
from fde_generator import ProjectionType, replace
# 内部 AMS Sketch 降维到 16 维
config1 = replace(
config,
projection_type=ProjectionType.AMS_SKETCH,
projection_dimension=16
)
# 最终 Count Sketch 降维到 1024 维
config2 = replace(
config,
final_projection_dimension=1024
)
性能特征速览
-
索引时间:O(D) 级别增加,测试中 4600 文档 × 20 次重复 ≈ 33ms/文档 -
查询时间:单次点积,<0.2ms -
Recall@10:与原生 ColBERT 相比略有下降(0.70 → 0.64),但速度提升数十倍
常见问答(FAQ)
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "什么是固定维度编码(FDE)?",
"acceptedAnswer": {
"@type": "Answer",
"text": "FDE 是一种将多条向量集合压缩为单个向量的技术,通过随机分区和聚合,保留原集合的相似度特征。"
}
},
{
"@type": "Question",
"name": "如何选择 num_simhash_projections?",
"acceptedAnswer": {
"@type": "Answer",
"text": "该参数 k 决定分区数 = 2^k。k 越大,分区越细,但计算开销也随之增加。常用值在 6~8 之间。"
}
},
{
"@type": "Question",
"name": "为何查询用求和而文档用平均?",
"acceptedAnswer": {
"@type": "Answer",
"text": "查询向量数量少,用求和即可;文档向量多,用平均可避免某些分区因向量过多而主导编码。"
}
},
{
"@type": "Question",
"name": "如何控制输出向量维度?",
"acceptedAnswer": {
"@type": "Answer",
"text": "可以通过设置 projection_dimension(中间降维)和 final_projection_dimension(最终降维)来压缩输出维度。"
}
}
]
}
HowTo 指南:快速上手 FDE
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "快速生成 Fixed-Dimensional Encoding",
"step": [
{
"@type": "HowToStep",
"name": "安装依赖",
"text": "pip install -r requirements.txt"
},
{
"@type": "HowToStep",
"name": "创建配置对象",
"text": "config = FixedDimensionalEncodingConfig(...)"
},
{
"@type": "HowToStep",
"name": "准备向量数据",
"text": "np.random.randn(num_vectors, dimension)"
},
{
"@type": "HowToStep",
"name": "调用生成函数",
"text": "fde = generate_query_fde(vectors, config)"
}
],
"tool": [
{
"@type": "HowToTool",
"name": "Python3"
},
{
"@type": "HowToTool",
"name": "NumPy"
}
]
}
参考链接
-
原文论文:MUVERA: Multi-Vector Retrieval via Fixed Dimensional Encodings -
C++ 实现仓库:Google Graph Mining -
技术博客:Google Research 博文