MUVERA 多向量检索:固定维度编码(FDE)Python 实现全解析

在大规模检索系统中,文档常常以多条向量表示以提升准确性,但这也带来了检索变慢的困境。MUVERA(Multi-Vector Retrieval via Fixed Dimensional Encodings)提出了“固定维度编码”(Fixed-Dimensional Encoding,简称 FDE)方法,将多向量集合压缩为单个高维向量,同时保留原始集的相似度特征。本文将用通俗且实用的语言,带你从原理、实现到使用,全面了解 FDE 的 Python 实现。


目录

  1. 为啥要用 FDE?多向量检索的难题

  2. FDE 原理:魔法为何可行?

  3. 环境准备与安装

  4. 配置类详解

  5. 内部辅助函数逐一拆解

  6. 核心算法流程一步步走

  7. 对外 API:一行代码生成 FDE

  8. C++ → Python 对照表

  9. 使用示例

  10. 性能特征速览

  11. 常见问答(FAQ)

  12. HowTo 指南:快速上手 FDE

  13. 参考链接


为啥要用 FDE?多向量检索的难题

想象一下,你有 10 亿篇文档,每篇都用 100 条向量来表示内容——这是 ColBERT 风格的多向量方案,能够捕捉文档细节、提升检索准确度;但如果对每条都做点积搜索,开销瞬间爆棚。

传统方案:

  • 文档=单向量 → 检索飞快,但粗糙容易漏召。

现代方案:

  • 文档=数百向量 → 精准却拖沓。

固定维度编码(FDE) 的核心思想:

“把多条向量装进一个大包里,完成后再比包的相似度,就能近似原来多向量的 Chamfer 相似度。”

换句话说,我们用一次点积运算代替遍历数百条向量,大幅加速检索而精度损失可控。


FDE 原理:魔法为何可行?

FDE 的魔法主要在三步:空间分区、向量聚合、重复串联。下面用简单比喻解释。

2.1 空间分区(SimHash + Gray Code)

  1. 随机高斯投影(SimHash)
    用一个高斯随机矩阵把原始向量映射到“±1”签名:

    类似把向量投影到多条随机超平面,正负两侧分别标记为 1/-1。

  2. Gray Code 编码
    把多个 ±1 串成二进制再转 Gray Code,得到一个整数,代表“这个向量落在空间的哪个块里”。

    • Gray Code 的优点:相邻分区只有一位不同,能更好地保留向量连续性。
  3. 分区数量
    – 用 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::AppendToGrayCodeinternal::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 博文