ColPali 文档相似度搜索
这篇文章的灵感来源于 Merve Noyan 关于无 OCR、基于视觉的文档 AI 的帖子这里和这里,文章使用了最新的 ColPali 模型。我想写一个更详细的版本,重点探讨使用视觉语言模型进行文档 AI 的另一个角度:基于相似度的检索。
给定一个文档页面,您可以使用 ColPali 找到所有与该示例页面相似的文档页面。这种类型的搜索为您进入大型、未标记(或错误标记)的文档档案提供了一个入口点。例如,基于相似度的检索可能是“整理”遗留企业文档存储的初始步骤,以便对部分业务文档进行标记,以便后续进行分类或信息提取任务的微调。
下面列出的 Colab 笔记本演示了如何在小型合成业务文档数据集上实现此功能。
多模态检索
文档检索旨在解决大海捞针的问题:您拥有一个庞大的文档语料库,可能包含数百万甚至更多文档,其中一些包含许多页面,您希望找到与您的查询最相关的文档,甚至是页面。
在网络上,这就是搜索引擎所做的事情。搜索引擎擅长搜索文本 HTML 文档,以及图像、视频和音频内容。
同样,业务文档不仅包含文本,还包含重要的视觉和布局线索。考虑一个文档,其首页顶部有粗体大标题、粗体章节标题、图像、图表和表格。成功的文档检索必须将这些视觉模态以及文档文本考虑在内。
文档检索最初侧重于文档的文本内容:文档文本传统上通过 OCR 提取,然后将该文本索引用于词汇搜索。
基于 OCR 的文本提取,然后进行布局和边界框分析,仍然是重要的文档 AI 模型(例如 LayoutLM)的核心。例如,LayoutLMv3编码文档文本,包括文本标记序列的顺序、标记或行段的 OCR 边界框坐标以及文档本身。这在关键文档 AI 任务中产生了最先进的结果,但前提是第一步,即 OCR 文本提取,运行良好。
但它通常无法正常工作。
根据我最近的经验,这个 OCR 瓶颈导致在真实世界生产文档档案上的命名实体识别 (NER) 任务中,性能下降了近 50%。
这说明了一个问题:许多文档 AI 模型都在干净的学术数据集上进行评估。但真实世界的数据是混乱的,在整洁的学术基准上表现良好的相同模型,在混乱、不规则的真实世界文档数据上可能表现不佳。
用于文档 AI 的 VLM
最近,视觉语言模型 (VLM) 擅长捕获图像中的视觉和文本上下文,其中也包括文档图像。事实上,VLM,例如 LlaVA-NeXT、PaliGemma、Idefics3、Qwen2-VL,开箱即用地展示了强大的文档 AI 能力,例如零样本信息提取,而无需 OCR。Donut 和 Idefics3 专门在大型文档语料库上进行了预训练,使其成为进一步微调的理想基础。
尽管这些基于 VLM 的无 OCR 模型在文档问答、信息提取或分类方面表现出色,但它们的主要重点并非可扩展的文档检索。
一款新模型 ColPali,代表了文档检索的质的飞跃。ColPali 将 ColBERT 首创的多向量检索技术与视觉语言模型 PaliGemma 融合,并将这种强大的融合应用于多模态文档检索。ColPali 作者的博客文章详细介绍了该模型。
嵌入选择
在纯文本模态中,基于神经网络的检索已取代词汇搜索:给定文档的合适表示(例如嵌入模型潜在空间中的高维向量)和查询的相似表示,您可以通过计算查询向量与文档向量的相似度来生成与查询匹配的文档列表。
如果您想将该技术扩展到文档中的视觉模态,一种合理的方法是使用 VLM 中的文档表示,例如模型最后一个隐藏状态的输出张量,作为文档嵌入进行检索。其想法是,该最后一个隐藏状态向量包含 VLM 对文档的丰富编码,从而使其成为向量相似度搜索中使用的良好候选。
然而,事实证明,使用单个向量表示文档并非最有效的检索选项。ColBERT(2020)的作者发现,使用*一袋向量*而不是单个向量,不仅提高了检索精度,还降低了检索延迟和计算成本。
多向量与后期交互
在 ColBERT 中,每个向量代表潜在向量空间中*文档标记*的丰富嵌入,例如 BERT 的嵌入。文档由一袋标记向量而不是单个向量表示。这使得标记嵌入能够捕获文档中标记的丰富上下文。捕获每个文档标记丰富上下文的能力是 ColBERT 中的*Co*。
ColBERT 文档嵌入是在离线索引步骤中创建的。在检索时,ColBERT 风格的检索器将查询编码为潜在嵌入空间中的一袋标记向量。然后,检索器计算文档和查询标记之间的相似度。
对于每个查询标记/文档标记对,只考虑具有最大相似度的匹配,即 MaxSim。然后将最高相似度求和以获得整个文档的查询/文档匹配分数。这种方法允许 ColBERT 执行廉价的向量比较和加法来识别与查询匹配的文档。
这也意味着文档和查询只在检索时交互,ColBERT 作者称之为*后期交互*。“late”是 ColBERT 中的“l”。
您可以在下面找到 MaxSim 和后期交互的分步演练。
相比之下,“早期交互”是指文档和查询从模型架构的开始就配对并一起处理。虽然这允许模型捕获查询和文档之间丰富的交互,但这种语义丰富性带来了更高的计算成本。ColBERT 论文详细描述了这一点。
正如名称中的 BERT 部分所示,ColBERT 侧重于文本文档检索。在此方面,它为检索精度和性能设定了新标准。如 ColBERT2 论文所述,多向量和后期交互范式也证明了其高度可扩展性。
ColPali 的主要贡献是将多向量和后期交互式搜索扩展到视觉语言模型,这里是 PaliGemma。该技术是可推广的,可应用于其他 VLM;例如,已经有 ColIdefics。
文档相似度
您通常会使用 ColPali 检索与文本查询匹配的文档,就像在 RAG 管道中一样。但是,我想通过展示查询不仅可以是文本,还可以是另一个格式丰富的文档来突出 ColPali 的多功能性。给定一个文档语料库 C 和一个示例文档 D,
从语料库 C 中选择所有与示例文档 D 相似的文档。
我发现基于相似度的文档检索在浏览大型、未标记的语料库(例如企业文档档案)时特别有用。
基于相似度的检索可以是“驯服”企业档案的第一步,用于下游文档 AI 任务,例如文档分类和关键信息提取。相似度搜索还可以为您提供发现错误标记或错误分类文档的工具。
为了了解 ColPali 是如何工作的,我整理了一个 Colab 笔记本。

ColBERT 风格的 MaxSim
ColBERT 风格的多向量检索与更传统的单向量检索之间的区别一开始可能难以理解。我发现,简化、分步的示例通常很有帮助,所以我手工演示了一个高度简化的 ColBERT 风格 MaxSim。希望其他人也能从中受益。
考虑以下两个文档,D1 和 D2,以及一个查询 Q,仅关注文档和查询文本。
|
|
文档 D1 |
这个苹果又甜又脆 |
文档 D2 |
这个香蕉又熟又黄 |
查询 Q |
甜苹果 |
为简单起见,假设 D1、D2 和 Q 的二维嵌入如下。(BERT 的嵌入是 768 维,并且一个词会分配多个标记。在 ColPali 中,这些将是 VLM 嵌入空间中的标记。)
在这个虚构的嵌入空间中,我将 [0.0,0.0] 分配给填充词。
文档嵌入在离线索引阶段创建,查询嵌入在查询时定义。嵌入包分别代表 D1、D2 和 Q。
文档 1
标记 |
嵌入 |
“这个” |
[0.0,0.0] |
“苹果” |
[0.9,0.1] |
“是” |
[0.0,0.0] |
“甜” |
[0.1,0.9] |
“和” |
[0.0,0.0] |
“脆” |
[0.7,0.7] |
文档 2
标记 |
嵌入 |
“这个” |
[0.0,0.0] |
“香蕉” |
[0.8,0.2] |
“是” |
[0.0,0.0] |
“熟” |
[0.2,0.8] |
“和” |
[0.0,0.0] |
“黄” |
[0.3,0.7] |
查询
标记 |
嵌入 |
“甜” |
[0.1,0.9] |
“苹果” |
[0.9,0.1] |
在查询时,ColBERT 风格的检索器计算第一个查询标记 **sweet** 与 **文档 1** 标记之间的相似度,以它们的点积表示(也可以使用其他相似度度量)。然后它选择最高的相似度分数,以表示标记之间的最大相似度。
标记 |
点积 |
sim(qsweet,d1the) |
[0.1,0.9]⋅[0.0,0.0]=0 |
sim(qsweet,d1apple) |
[0.1,0.9]⋅[0.9,0.1]=0.09+0.09=0.18 |
sim(qsweet,d1is) |
[0.1,0.9]⋅[0.0,0.0]=0 |
sim(qsweet,d1sweet) |
[0.1,0.9]⋅[0.1,0.9]=(0.1∗0.1)+(0.9∗0.9)=0.82 (最高) |
sim(qsweet,d1and) |
[0.1,0.9]⋅[0.0,0.0]=0 |
sim(qsweet,d1crisp) |
[0.1,0.9]⋅[0.7,0.7]=0.07+0.63=0.70 |
MaxSim(qsweet,D1)=max(0.0,0.18,0.0,0.82,0.0,0.70)=0.82 |
|
对下一个查询标记 **apple** 和 **文档 1** 标记重复此过程
标记 |
点积 |
sim(qapple,d1the) |
[0.9,0.1]⋅[0.0,0.0]=0.0 |
sim(qapple,d1apple) |
[0.9,0.1]⋅[0.9,0.1]=(0.9∗0.9)+(0.1∗0.1)=0.82 (最高) |
sim(qapple,d1is) |
[0.9,0.1]⋅[0.0,0.0]=0.0 |
sim(qapple,d1sweet) |
[0.9,0.1]⋅[0.1,0.9]=(0.9∗0.1)+(0.1∗0.9)=0.18 |
sim(qapple,d1and) |
[0.9,0.1]⋅[0.0,0.0]=0.0 |
sim(qapple,d1crisp) |
[0.9,0.1]⋅[0.7,0.7]=(0.9∗0.7)+(0.1∗0.7)=0.63+0.07=0.70 |
MaxSim(qapple,D1)=max(0.0,0.82,0.0,0.18,0.0,0.70)=0.82 |
|
最后,ColBERT 汇总 MaxSim 分数,得出 Q 和 D1 的分数:Score(Q,D1)=MaxSim(qsweet,D1)+MaxSim(qapple,D1)=0.82+0.82=1.64
这种匹配方式的一个关键优势是,它允许单个向量嵌入受益于像 BERT 这样的嵌入模型所提供的丰富语义。此外,由于文档的词元向量在查询之前已预先计算好,因此在查询时只需计算 Q 的嵌入,然后进行快速点积和求和运算即可。
为了对 D1 和 D2 相对于 Q 进行排名,我们对 D2 执行 MaxSim 操作,然后比较 D1 和 D2 的分数。
标记 |
点积 |
sim(qsweet,d2the) |
[0.1,0.9]⋅[0.0,0.0]=0 |
sim(qsweet,d2banana) |
[0.1,0.9]⋅[0.8,0.2]=0.08+0.18=0.26 |
sim(qsweet,d2is) |
[0.1,0.9]⋅[0.0,0.0]=0 |
sim(qsweet,d2ripe) |
[0.1,0.9]⋅[0.2,0.8]=0.02+0.72=0.74 (最高) |
sim(qsweet,d2and) |
[0.1,0.9]⋅[0.0,0.0]=0 |
sim(qsweet,d2yellow) |
[0.1,0.9]⋅[0.3,0.7]=0.03+0.63=0.66 |
MaxSim(qsweet,D2)=max0.0,0.26,0.0,0.74,0.0,0.66=0.74 |
|
标记 |
点积 |
sim(qapple,d2the) |
[0.9,0.1]⋅[0.0,0.0]=0 |
sim(qapple,d2banana) |
[0.9,0.1]⋅[0.8,0.2]=0.72+0.02=0.74 (最高) |
sim(qapple,d2is) |
[0.9,0.1]⋅[0.0,0.0]=0 |
sim(qapple,d2ripe) |
[0.9,0.1]⋅[0.2,0.8]=0.18+0.08=0.26 |
sim(qapple,d2and) |
[0.9,0.1]⋅[0.0,0.0]=0 |
sim(qapple,d2yellow) |
[0.9,0.1]⋅[0.3,0.7]=0.27+0.07=0.34 |
MaxSim(qapple,D2)=max0.0,0.74,0.0,0.26,0.0,0.34=0.74 |
|
Score(Q,D2)=MaxSim(qsweet,D2)+MaxSim(qapple,D2)=0.74+0.74=1.48 |
|
与 Q 的比较
文档 |
分数 |
文档 1 |
1.64 |
文档 2 |
1.48 |
因此,文档 1 与查询 Q 更相关。
主要启示是,向量包必须存储在文档索引中,并且简单的向量比较不足以用于这种类型的检索。