基于 ColPali/ColQwen 的 Qdrant 多向量文档检索

scaling-pdf-retrieval-qdrant

时间:30分钟难度:中等输出: GitHubOpen In Colab

高效的 PDF 文档检索是(智能体)检索增强生成 (RAG) 及许多其他基于搜索的应用中的常见需求。与此同时,在不遇到额外挑战的情况下实现 PDF 文档检索绝非易事。

许多传统的 PDF 检索方案依赖光学字符识别 (OCR),并结合特定用例的启发式算法来处理视觉复杂的元素,如表格、图像和图表。这些算法往往不具备通用性(即便是同一领域内),其定制化的解析和分块策略不仅耗费人力,而且容易出错,难以扩展。

视觉大语言模型 (VLLMs) 的最新进展,例如 ColPali 及其继任者 ColQwen,开启了 PDF 检索的变革。这些多模态模型直接将 PDF 页面作为输入,无需预处理。任何可以转换为图像的内容(可以将 PDF 视为文档页面的截图)都可以被这些模型有效处理。VLLMs 使用起来简单得多,并在 视觉文档检索 (ViDoRe) 基准测试 等 PDF 检索基准中达到了最先进的性能。

VLLMs 如何进行 PDF 检索

ColPaliColQwen 这样的 VLLM 会为每个 PDF 页面生成多向量表示;这些表示会被存储并索引到向量数据库中。在检索过程中,模型会动态地为(文本)用户查询创建多向量表示,并通过 后期交互机制 (late-interaction mechanism) 实现精确检索(PDF 页面与查询之间的匹配)。

扩展 VLLMs 的挑战

VLLMs 产生的繁重多向量表示使得大规模 PDF 检索在计算上非常密集。如果未经过优化,这些模型在处理大规模 PDF 检索任务时效率极低。

扩展背后的数学原理

ColPali 每个 PDF 页面生成超过 1,000 个向量,而其继任者 ColQwen 生成的略少——最多 768 个向量,并根据图像大小动态调整。通常情况下,ColQwen 每页产生 约 700 个向量

为了理解其影响,考虑构建一个 HNSW 索引 的过程,这是向量数据库中常见的索引算法。让我们粗略估计一下将新 PDF 页面插入索引所需进行的比较次数。

  • 每页向量数: 约 700 (ColQwen) 或 约 1,000 (ColPali)
  • ef_construct 100(默认值)

向量比较次数的下界估计为

$$ 700 \times 700 \times 100 = 49 \ \text{百万次} $$

现在想象一下,为 20,000 个页面构建索引需要多长时间!

对于 ColPali,这个数字还要翻倍。结果就是极长的索引构建时间

我们的解决方案

我们建议在第一阶段检索时减少 PDF 页面表示中的向量数量。在通过减少向量数量进行第一阶段检索后,我们提议使用原始的未压缩表示对检索到的子集进行重排序 (rerank)

减少向量可以通过对 VLLM 生成的多向量输出应用平均池化 (mean pooling) 操作来实现。平均池化对选定子组内的所有向量值取平均,将多个向量浓缩为单个代表性向量。如果操作得当,它可以在保留原始页面重要信息的同时,显著减少向量数量。

VLLMs 生成对应于代表 PDF 页面不同部分的补丁(patch)的向量。这些补丁可以按 PDF 页面的列和行进行分组。

例如

  • ColPali 将 PDF 页面划分为 1,024 个补丁
  • 通过该补丁矩阵的行(或列)进行平均池化,可以将页面表示减少到仅 32 个向量

ColPali patching of a PDF page

我们使用 ColPali 模型测试了这种方法,通过 PDF 页面行对多向量进行平均池化。结果显示

  • 索引时间缩短了一个数量级
  • 检索质量与原始模型相当

有关此实验的详细信息,请参阅我们的 GitHub 存储库ColPali 优化博客文章“PDF 检索规模化”网络研讨会

本教程的目标

在本教程中,我们将演示一种使用 QdrantColPali & ColQwen2 VLLMs 进行规模化 PDF 检索的方法。强烈建议采用这种方法,以避免索引时间过长和检索速度缓慢等常见陷阱。

在接下来的章节中,我们将演示一种源自我们成功实验的优化检索算法

第一阶段检索:使用平均池化向量

  • 仅使用平均池化向量构建 HNSW 索引。
  • 将它们用于第一阶段检索。

第二阶段重排序:使用原始模型的多向量

  • 使用来自 ColPali 或 ColQwen2 的原始多向量对第一阶段检索到的结果进行重排序

设置

安装并导入所需的库

# pip install colpali_engine>=0.3.1
from colpali_engine.models import ColPali, ColPaliProcessor
# pip install qdrant-client>=1.12.0
from qdrant_client import QdrantClient, models

为了运行这些实验,我们使用了 Qdrant 集群。如果您刚开始,可以设置一个免费层级集群进行测试和探索。请遵循文档 “如何创建免费层级 Qdrant 集群” 中的说明。

client = QdrantClient(
    url=<YOUR CLUSTER URL>,
    api_key=<YOUR API KEY>
)

下载 ColPali 模型及其输入处理器。请确保选择适合您环境的后端。

colpali_model = ColPali.from_pretrained(
        "vidore/colpali-v1.3",
        torch_dtype=torch.bfloat16,
        device_map="mps",  # Use "cuda:0" for GPU, "cpu" for CPU, or "mps" for Apple Silicon
    ).eval()

colpali_processor = ColPaliProcessor.from_pretrained("vidore/colpali-v1.3")
对于 ColQwen 模型
from colpali_engine.models import ColQwen2, ColQwen2Processor

colqwen_model = ColQwen2.from_pretrained(
        "vidore/colqwen2-v0.1",
        torch_dtype=torch.bfloat16,
        device_map="mps", # Use "cuda:0" for GPU, "cpu" for CPU, or "mps" for Apple Silicon
    ).eval()

colqwen_processor = ColQwen2Processor.from_pretrained("vidore/colqwen2-v0.1")

创建 Qdrant 集合

现在,我们可以在 Qdrant 中创建一个集合,用于存储由 ColPaliColQwen 生成的 PDF 页面多向量表示。

集合将包含 PDF 页面的按行和列平均池化的表示,以及原始的多向量表示。

client.create_collection(
    collection_name=collection_name,
    vectors_config={
        "original": 
            models.VectorParams( #switch off HNSW
                    size=128,
                    distance=models.Distance.COSINE,
                    multivector_config=models.MultiVectorConfig(
                        comparator=models.MultiVectorComparator.MAX_SIM
                    ),
                    hnsw_config=models.HnswConfigDiff(
                        m=0 #switching off HNSW
                    )
            ),
        "mean_pooling_columns": models.VectorParams(
                size=128,
                distance=models.Distance.COSINE,
                multivector_config=models.MultiVectorConfig(
                    comparator=models.MultiVectorComparator.MAX_SIM
                )
            ),
        "mean_pooling_rows": models.VectorParams(
                size=128,
                distance=models.Distance.COSINE,
                multivector_config=models.MultiVectorConfig(
                    comparator=models.MultiVectorComparator.MAX_SIM
                )
            )
    }
)

选择数据集

在本教程中,我们将使用 Daniel van Strien 提供的 UFO 数据集。它可以在 Hugging Face 上找到;您可以直接从那里下载。

from datasets import load_dataset
ufo_dataset = "davanstrien/ufo-ColPali"
dataset = load_dataset(ufo_dataset, split="train")

嵌入与平均池化

我们将使用一个函数,以批处理方式生成每个 PDF 页面(即图像)的多向量表示及其平均池化版本。为了完全理解,考虑 ColPaliColQwen 的以下细节非常重要:

ColPali: 理论上,ColPali 设计为每页 PDF 生成 1,024 个向量,但实际上它产生 1,030 个向量。这种差异是由于 ColPali 的预处理器在每个输入文本前附加了 <bos>Describe the image.,这段额外的文本生成了额外的 6 个多向量。

ColQwen: ColQwen 根据 PDF 页面的大小动态确定其“行和列”中的补丁数量。因此,多向量的数量可能会因输入而异。ColQwen 的预处理器在输入前添加 <|im_start|>user<|vision_start|>,并在末尾添加 <|vision_end|>Describe the image.<|im_end|><|endoftext|>

例如,ColQwen 的多向量输出就是这样形成的。

that’s how ColQwen multivector output is formed

get_patches 函数用于获取 ColPali/ColQwen2 模型将 PDF 页面划分成的 x_patches(行)和 y_patches(列)的数量。对于 ColPali,这些数字始终为 32x32;ColQwen 则会根据 PDF 页面大小动态定义它们。

x_patches, y_patches = model_processor.get_n_patches(
    image_size, 
    patch_size=model.patch_size
)
对于 ColQwen 模型
model_processor.get_n_patches(
    image_size, 
    patch_size=model.patch_size,
    spatial_merge_size=model.spatial_merge_size
)

我们选择保留前缀和后缀多向量。我们的池化操作根据模型确定的行数和列数(ColPali 为固定的 32x32,ColQwen 为动态的 XxY)压缩表示图像标记的多向量。该函数会将模型产生的额外多向量保留并整合回池化后的表示中。

ColPali 模型的简化版池化代码

(请参阅 教程笔记本 中的完整版本——同样适用于 ColQwen


processed_images = model_processor.process_images(image_batch) 
# Image embeddings of shape (batch_size, 1030, 128)
image_embeddings = model(**processed_images)

# (1030, 128)
image_embedding = image_embeddings[0] # take the first element of the batch

# Now we need to identify vectors that correspond to the image tokens
# It can be done by selecting tokens corresponding to special `image_token_id`

# (1030, ) - boolean mask (for the first element in the batch), True for image tokens 
mask = processed_images.input_ids[0] == model_processor.image_token_id

# For convenience, we now select only image tokens 
#   and reshape them to (x_patches, y_patches, dim)

# (x_patches, y_patches, 128)
image_patch_embeddings = image_embedding[mask].view(x_patches, y_patches, model.dim)

# Now we can apply mean pooling by rows and columns

# (x_patches, 128)
pooled_by_rows = image_patch_embeddings.mean(dim=0)

# (y_patches, 128)
pooled_by_columns = image_patch_embeddings.mean(dim=1)

# [Optionally] we can also concatenate special tokens to the pooled representations, 
# For ColPali, it's only postfix

# (x_patches + 6, 128)
pooled_by_rows = torch.cat([pooled_by_rows, image_embedding[~mask]])

# (y_patches + 6, 128)
pooled_by_columns = torch.cat([pooled_by_columns, image_embedding[~mask]])

上传至 Qdrant

上传过程很简单;唯一需要注意的是 ColPali 和 ColQwen2 模型的计算成本。在资源受限的环境中,建议使用较小的批量大小进行嵌入和平均池化。

完整版的上传代码可在 教程笔记本 中获得。

查询 PDF

在对 PDF 文档进行索引后,我们就可以使用两阶段检索方法来查询它们了。

query = "Lee Harvey Oswald's involvement in the JFK assassination"
processed_queries = model_processor.process_queries([query]).to(model.device)

# Resulting query embedding is a tensor of shape (22, 128)
query_embedding = model(**processed_queries)[0]

现在让我们设计一个函数,使用 VLLMs 生成的多向量进行两阶段检索。

  • 第一步: 使用压缩的多向量表示和 HNSW 索引预取结果。
  • 第二步: 使用原始的多向量表示对预取的结果进行重排序。

让我们使用组合的平均池化表示查询我们的集合,作为检索的第一阶段。

# Final amount of results to return
search_limit = 10
# Amount of results to prefetch for reranking
prefetch_limit = 100

response = client.query_points(
    collection_name=collection_name,
    query=query_embedding,
    prefetch=[
        models.Prefetch(
            query=query_embedding,
            limit=prefetch_limit,
            using="mean_pooling_columns"
        ),
        models.Prefetch(
            query=query_embedding,
            limit=prefetch_limit,
            using="mean_pooling_rows"
        ),
    ],
    limit=search_limit,
    with_payload=True,
    with_vector=False,
    using="original"
)

并检查对查询 “Lee Harvey Oswald 在肯尼迪遇刺案中的参与” 的首要检索结果。

dataset[response.points[0].payload['index']]['image']

Results, ColPali

结论

在本教程中,我们演示了一种优化方法,即使用 Qdrant 进行大规模 PDF 检索,并处理像 ColPaliColQwen2 这样产生繁重多向量表示的 VLLM。

若无此类优化,检索系统的性能会严重下降,无论是在索引时间还是查询延迟方面,尤其是随着数据集规模的增长。

我们强烈建议在您的工作流程中实施此方法,以确保高效且可扩展的 PDF 检索。忽视对检索过程的优化可能会导致无法接受的性能低下,从而阻碍系统的可用性。

立即开始扩展您的 PDF 检索吧!

此页面有用吗?

感谢您的反馈!🙏

很抱歉听到这个。😔 您可以在 GitHub 上 编辑 此页面,或 创建 一个 GitHub issue。