展示如何通过特定微调技术改进 embedding 模型性能。对构建 RAG 系统和向量搜索的工程师有实际参考价值。
Qdrant 1.10 推出了对多向量表示的支持,后期交互是这类模型的一个重要例子。本质上,文档和查询都由多个向量表示,识别最相关文档涉及基于相应查询和文档嵌入之间的相似性计算得分。如果你不熟悉这种范式,我们更新后的混合搜索文章解释了多向量表示如何增强检索质量。
图1:我们可以可视化相应文档-查询嵌入对之间的后期交互。
有许多专门的后期交互模型,如 ColBERT,但看起来常规密集嵌入模型也可以以这种方式有效利用。
在这项研究中,我们将证明传统上用于单向量表示的标准密集嵌入模型,可以通过使用输出 token 嵌入作为多向量表示,有效地适配后期交互场景。
通过使用 Qdrant 的多向量功能进行检索测试,我们将展示这些模型可以与专门的后期交互模型在检索性能上相当或更优,同时提供更低的复杂性和更高的效率。这项工作重新定义了密集模型在高级搜索管道中的潜力,为优化检索系统提供了新方法。
嵌入模型的内部工作原理可能会让某些人感到惊讶。该模型不直接处理输入文本;相反,它需要一个分词步骤来将文本转换为一系列 token 标识符。每个 token 标识符随后通过嵌入层,将其转换为密集向量。本质上,嵌入层充当查找表,将 token 标识符映射到密集向量。然后这些向量被输入到 transformer 模型中。
图2:分词步骤,它在向量添加到 transformer 模型之前进行。
输入 token 嵌入是与上下文无关的,在模型的训练过程中学习。这意味着每个 token 总是接收相同的嵌入,无论其在文本中的位置如何。在这个阶段,token 嵌入不知道它们出现的上下文。transformer 模型的作用是对这些嵌入进行语境化。
关于 attention 在 transformer 模型中的作用已有很多讨论,但本质上,这个机制负责捕捉跨 token 的关系。每个 transformer 模块将一系列 token 嵌入作为输入,并生成一系列输出 token 嵌入。两个序列长度相同,每个 token 嵌入在当前步骤中被其他 token 嵌入的信息充实。
图3:生成输出 token 嵌入序列的机制。
图4:嵌入模型执行的最后一步是汇聚输出 token 嵌入,生成输入文本的单个向量表示。
有几种汇聚策略,但无论模型使用哪一种,输出总是一个单向量表示,这不可避免地会丢失有关输入的某些信息。这就像有人给你详细的分步指示找到最近的杂货店,而不是简单地指向大致方向。虽然模糊的方向在某些情况下可能足够,但详细的指示更有可能导出所需的结果。
我们常常忽视输出 token 嵌入,但事实是——它们也作为输入文本的多向量表示。那么,为什么不探索在多向量检索模型中使用它们,类似于后期交互模型呢?
我们进行了多项实验,以确定输出 token 嵌入是否可以有效地替代传统后期交互模型。结果相当有希望。
这些实验的源代码是开源的,使用 beir-qdrant,它是 Qdrant 与 BeIR 库的集成。虽然这个包不是由 Qdrant 团队正式维护的,但对于那些有兴趣用各种 Qdrant 配置进行实验以查看它们如何影响检索质量的人来说可能会有用。所有实验都使用 Qdrant 的精确搜索模式进行,确保结果不受近似搜索的影响。
即使是简单的 all-MiniLM-L6-v2 模型也可以以后期交互模型的方式应用,对检索质量产生积极影响。然而,最佳结果是通过 BAAI/bge-small-en 模型实现的,它优于稀疏和后期交互模型。
重要的是要注意 ColBERT 没有在 BeIR 数据集上进行训练,使其性能完全超出领域。尽管如此,all-MiniLM-L6-v2 训练数据集也缺乏任何 BeIR 数据,但它仍然表现得非常好。
检索质量不言自明,但还有其他重要因素需要考虑。
我们测试的传统密集嵌入模型的复杂性低于后期交互或稀疏模型。参数较少,这些模型在推理过程中预期速度更快,维护成本更低。以下是实验中使用的模型的比较:
反对使用输出 token 嵌入的一个论点是与类 ColBERT 模型相比存储需求的增加。例如,all-MiniLM-L6-v2 模型产生 384 维的输出 token 嵌入,这是 ColBERT 类模型生成的 128 维嵌入的三倍。这种增加不仅导致更高的内存使用,还影响检索的计算成本,因为计算距离需要更多时间。通过向量压缩来缓解这个问题是很有意义的。
二进制量化通常对高维向量更有效,使得输出相对低维的 all-MiniLM-L6-v2 模型不太适合这种方法。然而,标量量化似乎是一个可行的替代方案。下表总结了量化对检索质量的影响。
重要的是要注意量化并不总是以相同水平保留检索质量,但在这种情况下,标量量化似乎对检索性能的影响最小。效果可以忽略不计,而内存节省是巨大的。
我们设法在仅使用四倍内存的情况下保持原始质量。此外,量化向量需要 384 字节,相比 ColBERT 的 512 字节。这导致内存使用量减少 25%,检索质量几乎不变。
如果你使用的是句子 transformer 模型之一,输出 token 嵌入默认被计算。虽然单向量表示在存储和计算方面更高效,但无需丢弃输出 token 嵌入。根据我们的实验,这些嵌入可以显著增强检索质量。你可以在 Qdrant 中同时存储单向量和输出 token 嵌入,使用单向量进行初始检索步骤,然后用输出 token 嵌入重新排序结果。
图5:一个仅依赖输出 token 嵌入进行重新排序的单模型管道。
为了演示这个概念,我们在 Qdrant 中实现了一个简单的重新排序管道。该管道使用密集嵌入模型进行初始过采样检索,然后仅依赖输出 token 嵌入进行重新排序步骤。
我们的测试侧重于对检索和重新排序都使用相同的模型。报告的指标是 NDCG@10。在所有测试中,我们应用了 5 倍的过采样因子,意味着检索步骤返回 50 个结果,然后在重新排序步骤中缩小到 10 个。以下是一些 BeIR 数据集的结果:
基准的源代码是公开的,你可以在 beir-qdrant 包的仓库中找到它。
总的来说,添加使用相同模型的重新排序步骤通常会改进检索质量。然而,各种后期交互模型的质量通常是基于它们在 BM25 用于初始检索时的重新排序性能报告的。该实验旨在演示如何有效地使用单个模型进行检索和重新排序,结果相当有希望。
现在,让我们探索如何使用 Qdrant 1.10 中引入的新 Query API 来实现这一点。
Qdrant 1.10 中新的 Query API 支持构建更复杂的检索管道。我们可以使用汇聚后创建的单向量进行初始检索步骤,然后使用输出 token 嵌入重新排序结果。
假设集合名为 my-collection 并配置为存储两个命名向量:dense-vector 和 output-token-embeddings,以下是如何在 Qdrant 中创建这样的集合:
from qdrant_client import QdrantClient, models
client = QdrantClient("http://localhost:6333")
client.create_collection(
collection_name="my-collection",
vectors_config={
"dense-vector": models.VectorParams(
size=384,
distance=models.Distance.COSINE,
),
"output-token-embeddings": models.VectorParams(
size=384,
distance=models.Distance.COSINE,
multivector_config=models.MultiVectorConfig(
comparator=models.MultiVectorComparator.MAX_SIM
),
),
}
)
两个向量大小相同,因为它们由同一个 all-MiniLM-L6-v2 模型产生。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
现在,我们可以创建一个重新排序管道,而不仅仅使用单向量的搜索 API。首先,我们使用密集向量检索 50 个结果,然后使用输出 token 嵌入重新排序它们以获得前 10 个结果。
query = "What else can be done with just all-MiniLM-L6-v2 model?"
client.query_points(
collection_name="my-collection",
prefetch=[
# Prefetch the dense embeddings of the top-50 documents
models.Prefetch(
query=model.encode(query).tolist(),
using="dense-vector",
limit=50,
)
],
# Rerank the top-50 documents retrieved by the dense embedding model
# and return just the top-10. Please note we call the same model, but
# we ask for the token embeddings by setting the output_value parameter.
query=model.encode(query, output_value="token_embeddings").tolist(),
using="output-token-embeddings",
limit=10,
)
在实际场景中,你可能会更进一步,首先计算 token 嵌入,然后进行汇聚以获得单向量表示。这种方法允许你在一次通过中完成所有操作。
在 Qdrant 中开始尝试构建复杂重新排序管道的最简单方法是使用 Qdrant Cloud 上的永久免费集群并阅读 Qdrant 的文档。
这些实验的源代码是开源的,使用 beir-qdrant,它是 Qdrant 与 BeIR 库的集成。
使用输出 token 嵌入在检索过程中的初步实验已经产生了有希望的结果。然而,我们计划进行进一步的基准测试以验证这些发现,并探索为初始检索纳入稀疏方法。此外,我们的目标是调查量化对多向量表示的影响及其对检索质量的影响。最后,我们将评估检索速度,这是许多应用的关键因素。