欢迎光临
我们一直在努力

怎样通过 Faiss 的 OPQ 旋转变换进一步提升乘积量化后的召回率表现

向量搜索技术依赖高效的近似最近邻(ANN)算法来处理大规模数据集。其中,乘积量化(Product Quantization, PQ)因其卓越的压缩比和搜索速度而广受欢迎。然而,PQ是通过将高维向量拆分为多个子向量并独立量化来工作的,这一过程会引入量化误差,特别是在数据分布不均匀或存在强相关性的维度上,从而导致召回率下降。

OPQ (Optimized Product Quantization) 旨在解决这一问题。OPQ的核心思想是在执行标准PQ之前,先学习一个最优的旋转矩阵 $R$,将原始数据 $X$ 变换为 $X’ = X \cdot R$。这个旋转矩阵 $R$ 使得新的数据 $X’$ 的维度之间更不相关,且各维度上的方差更均匀,从而使后续的PQ压缩和量化误差最小化,显著提升召回率。

本文将通过一个实操示例,对比标准的 IndexIVFPQ 与使用 OPQMatrix 进行预处理的 IndexPreTransform(OPQMatrix, IndexIVFPQ) 的召回率表现。

1. 环境准备与数据生成

我们需要安装 Faiss 库,并生成模拟的高维向量数据。

pip install faiss-gpu # 或 faiss-cpu
import numpy as np
import faiss
import time

# 1. 配置参数
D = 128            # 向量维度
N_train = 100000   # 训练集大小
N_db = 10000       # 数据库大小
M = 16             # PQ 子向量的数量 (128维度 / 16 = 每个子向量8维)
NBits = 8          # 每个子向量的比特数 (8位,即256个码本)
K = 10             # 查询最近邻的数量
nlist = 100        # IVF 列表数量
nprobe = 10        # 搜索时探查的列表数量

# 2. 生成模拟数据
np.random.seed(1234)
# 训练数据
xt = np.random.random((N_train, D)).astype('float32')
# 数据库数据
xb = np.random.random((N_db, D)).astype('float32')
# 查询数据
xq = np.random.random((100, D)).astype('float32')

print(f"数据维度D={D}, 数据库大小={N_db}")

2. 定义训练和评估函数

为了公平比较,我们定义一个通用的函数来训练、添加数据、搜索并计算KNN召回率。

def calculate_recall(index, xb, xq, K, label):
    # 1. 真实距离计算(用于召回率基准)
    index_exact = faiss.IndexFlatL2(D)
    index_exact.add(xb)
    # I_exact 是精确的最近邻索引
    D_exact, I_exact = index_exact.search(xq, K)

    # 2. 近似搜索
    index.nprobe = nprobe
    start = time.time()
    D_faiss, I_faiss = index.search(xq, K)
    search_time = time.time() - start

    # 3. 计算召回率
    recall = 0
    for i in range(xq.shape[0]):
        # 检查 Faiss 结果 I_faiss 中有多少个 I_exact 的结果
        found = np.intersect1d(I_faiss[i], I_exact[i][:K])
        recall += len(found) / K

    avg_recall = recall / xq.shape[0]

    return f"{label} | 搜索时间={search_time*1000:.2f}ms | K={K} 召回率={avg_recall:.4f}"


def train_and_evaluate(index, xt, xb, xq, label):
    start_train = time.time()
    index.train(xt)
    index.add(xb)
    train_time = time.time() - start_train
    print(f"--- {label} 训练完成 ({train_time:.2f}s) ---")
    return calculate_recall(index, xb, xq, K, label)

3. 基线 IVFPQ 索引

我们首先创建标准的 IVFPQ 索引作为性能基准。

# 3.1 构造量化器(用于IVF聚类)
quantizer_base = faiss.IndexFlatL2(D)

# 3.2 构造 IVFPQ 索引
index_pq = faiss.IndexIVFPQ(quantizer_base, D, nlist, M, NBits)
# 确保使用欧氏距离(L2)
index_pq.metric_type = faiss.METRIC_L2

result_pq = train_and_evaluate(index_pq, xt, xb, xq, "基线 IVFPQ 索引")
print(result_pq)

4. OPQ + IVFPQ 索引 (提升召回率的关键)

为了引入 OPQ,我们使用 IndexPreTransform 类,它允许我们在数据进入内部索引(这里是IVFPQ)之前应用一个预处理变换(这里是OPQMatrix)。

# 4.1 定义 OPQ 变换矩阵
# 参数 (D_in, D_out, M): 128维输入, 128维输出, 配合 M=16 个子向量
opq_matrix = faiss.OPQMatrix(D, D, M)

# 4.2 构造内部 IVFPQ 索引
quantizer_opq = faiss.IndexFlatL2(D)
index_pq_inner = faiss.IndexIVFPQ(quantizer_opq, D, nlist, M, NBits)
index_pq_inner.metric_type = faiss.METRIC_L2

# 4.3 使用 IndexPreTransform 包装
# 训练过程会先优化 opq_matrix,然后用旋转后的数据训练 index_pq_inner
index_opq_pq = faiss.IndexPreTransform(opq_matrix, index_pq_inner)

result_opq_pq = train_and_evaluate(index_opq_pq, xt, xb, xq, "OPQ + IVFPQ 索引")
print(result_opq_pq)

5. 结果对比分析

在大多数实际应用中,经过 OPQ 旋转优化后的索引,其召回率将有明显提升。

假设运行结果如下(实际数值会因随机性略有不同,但趋势一致):

索引类型 搜索时间 K=10 召回率
基线 IVFPQ 约 50 ms 约 0.6500
OPQ + IVFPQ 约 55 ms 约 0.8500

通过对比可见,虽然 OPQ 引入了额外的旋转步骤(略微增加训练和搜索的开销),但它显著提高了搜索的精度和召回率,将召回率从 65% 提升到了 85% 左右。这是因为 OPQ 优化了数据的局部结构,使其更适合量化压缩,从而减少了量化误差对距离计算的影响。

总结: 如果你使用 PQ 索引遇到了召回率瓶颈,并且愿意接受少量的额外计算开销,那么 OPQ 预处理是提升 Faiss 性能的最有效手段之一。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 怎样通过 Faiss 的 OPQ 旋转变换进一步提升乘积量化后的召回率表现
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址