BM42:混合搜索的新基准 - Qdrant

在过去的 40 年里,BM25 一直是搜索引擎的标准。它是一种简单但功能强大的算法,已被许多搜索引擎使用,包括 Google、Bing 和 Yahoo。

虽然看起来向量搜索的出现会削弱其影响力,但效果并不明显。目前最先进的检索方法试图将 BM25 与嵌入结合到混合搜索系统中。

然而,自从引入 RAG 以来,文本检索的使用情况发生了显著变化。BM25 所基于的许多假设不再有效。

例如,传统网络搜索和现代 RAG 系统之间文档和查询的典型长度存在很大差异。

在本文中,我们将回顾 BM25 为何如此受关注,以及为什么其他替代方法难以取代它。最后,我们将讨论 BM42,这是词汇搜索演进的下一步。

为何 BM25 能持续这么长时间?

要了解原因,我们需要分析其组成部分。

著名的BM25公式定义为:
score ⁡ ( D , Q ) = ∑ i = 1 N IDF ⁡ ( q i ) × f ( q i , D ) ⋅ ( k 1 + 1 ) f ( q i , D ) + k 1 ⋅ ( 1 − b + b ⋅ ∣ D ∣  avgdl  ) \operatorname{score}(D, Q)=\sum_{i=1}^N \operatorname{IDF}\left(q_i\right) \times \frac{f\left(q_i, D\right) \cdot\left(k_1+1\right)}{f\left(q_i, D\right)+k_1 \cdot\left(1-b+b \cdot \frac{|D|}{\text { avgdl }}\right)} score(D,Q)=i=1NIDF(qi)×f(qi,D)+k1(1b+b avgdl D)f(qi,D)(k1+1)

让我们简化这一点以便更好地理解。

  • 这score(D,Q)-- 意味着我们计算每对文档的分数并查询。

  • ∑ i = 1 N \sum_{i=1}^N i=1N意味着每个查询中的术语将作为总和的一部分计入最终得分。

  • IDF ⁡ ( q i ) \operatorname{IDF}\left(q_i\right) IDF(qi) 是逆文档频率。术语越罕见,越大,对得分的贡献就越大。简化的公式如下:
    IDF ⁡ ( q i ) =  Number of documents   Number of documents with  q i \operatorname{IDF}\left(q_i\right)=\frac{\text { Number of documents }}{\text { Number of documents with } q_i} IDF(qi)= Number of documents with qi Number of documents 

可以说 是IDFBM25 公式中最重要的部分。 IDF选择查询中相对于特定文档集合最重要的术语。 因此直观地,我们可以将 解释为语料库中的术语IDF重要性。

这解释了为什么 BM25 如此擅长处理查询,而密集嵌入认为这些查询超出了域范围。

公式的最后一个部分可以直观地解释为文档中的术语重要性。这可能看起来有点复杂,所以让我们分解一下。
 Term importance in document  ( q i ) = f ( q i , D ) ⋅ ( k 1 + 1 ) f ( q i , D ) + k 1 ⋅ ( 1 − b + b ⋅ ∣ D ∣  avgdl  ) \text { Term importance in document }\left(q_i\right)=\frac{f\left(q_i, D\right) \cdot\left(k_1+1\right)}{f\left(q_i, D\right)+k_1 \cdot\left(1-b+b \cdot \frac{|D|}{\text { avgdl }}\right)}  Term importance in document (qi)=f(qi,D)+k1(1b+b avgdl D)f(qi,D)(k1+1)

  • f ( q i , D ) f\left(q_i, D\right) f(qi,D) 是该词的频率在文档中换句话说,该术语的次数出现在文档中。
  • k 1 k1 k1 b 1 b1 b1是 BM25 公式的超参数。在大多数实现中,它们是设置为的常量和这些常数定义了公式中词频和文档长度的相对含义。
  • ∣ D ∣  avgdl  \frac{|D|}{\text { avgdl }}  avgdl D 是文档的相对长度与语料库中的平均文档长度相比。这部分的直觉如下:如果在较小的文档中找到标记,则该标记对该文档而言就更有可能是重要的。
文档中的 BM25 术语重要性是否适用于 RAG?

我们可以看出,_文档中的术语重要性_很大程度上取决于文档中的统计数据。此外,如果文档足够长,统计数据会很好用。因此,它适合搜索网页、书籍、文章等。

但是,它是否也适用于现代搜索应用程序(例如 RAG)?让我们拭目以待。

RAG 中文档的典型长度比网络搜索的文档长度短得多。事实上,即使我们处理的是网页和文章,我们也更愿意将它们分成块,以便 a) 密集模型可以处理它们,并且 b) 我们可以精确地找到与查询相关的文档的确切部分

因此,RAG 中的文档大小很小且固定。

这实际上使得 BM25 公式的文档部分中的词重要性变得毫无用处。文档中的词频始终为 0 或 1,文档的相对长度始终为 1。

因此,BM25 公式中唯一与 RAG 相关的部分是IDF。让我们看看如何利用它。

为什么 SPLADE 并不总是答案

在讨论我们的新方法之前,让我们先来研究一下 BM25 的当前最先进的替代方案 - SPLADE。

SPLADE 背后的想法很有趣——如果我们让一个智能的、端到端训练的模型为我们生成文本的词袋表示会怎么样?它会将所有权重分配给标记,因此我们无需担心统计数据和超参数。然后,文档将表示为稀疏嵌入,其中每个标记都表示为稀疏向量的一个元素。

而且它在学术基准测试中表现良好。许多论文报告称,SPLADE 在检索质量方面优于 BM25。然而,这种性能是有代价的。

  • 不合适的标记器:要将转换器整合到此任务中,SPLADE 模型需要使用标准转换器标记器。这些标记器不是为检索任务设计的。例如,如果单词不在(相当有限的)词汇表中,它将被拆分为子词或替换为标记[UNK]。这种行为对于语言建模很有效,但对于检索任务却完全是破坏性的。

  • 昂贵的标记扩展:为了补偿标记化问题,SPLADE 使用_标记扩展_技术。这意味着我们为查询中的每个标记生成一组类似的标记。这种方法存在一些问题:

    • 它在计算和内存上都很昂贵。我们需要为文档中的每个标记生成更多值,这会增加存储大小和检索时间。
    • 标记扩展何时停止并不总是很清楚。我们生成的标记越多,我们获得相关标记的可能性就越大。但同时,我们生成的标记越多,我们获得不相关结果的可能性就越大。
    • 标记扩展削弱了搜索的可解释性。我们无法说出文档中使用了哪些标记,哪些标记是由标记扩展生成的。
  • 领域和语言依赖性:SPLADE 模型是在特定语料库上训练的。这意味着它们并不总是可以推广到新的或罕见的领域。由于它们不使用语料库中的任何统计数据,因此它们无法在不进行微调的情况下适应新领域。

  • 推理时间:此外,目前可用的 SPLADE 模型相当大且速度很慢。它们通常需要 GPU 才能在合理的时间内进行推理。

在 Qdrant,我们承认上述问题并正在寻找解决方案。我们的想法是结合两全其美的优势——BM25 的简单性和可解释性以及 transformer 的智能性,同时避免 SPLADE 的缺陷。

这就是我们的想法。

两全其美的

如前所述,IDF是 BM25 公式中最重要的部分。事实上,它非常重要,因此我们决定将其计算构建到 Qdrant 引擎本身中。查看我们最新的发布说明。这种类型的分离允许对稀疏嵌入进行流式更新,同时保持IDF计算最新。

至于公式的第二部分,_文档中的重要性一词_需要重新考虑。

既然我们不能依赖文档内的统计数据,那么我们可以尝试使用文档的语义。而语义正是 Transformer 所擅长的。因此,我们只需要解决两个问题:

  • 如何从变压器中提取重要信息?
  • 如何避免标记化问题?

你只需要关注

Transformer 模型(即使是用于生成嵌入的模型)也会生成大量不同的输出。其中一些输出用于生成嵌入。

其他用于解决其他类型的任务,例如分类、文本生成等。

对我们来说,一个特别有趣的输出是注意力矩阵。

注意力矩阵

注意力矩阵

注意力矩阵是一个方阵,每行每列对应输入序列中的token,表示输入序列中每个token对彼此的重要性。

经典的 Transformer 模型经过训练可以预测上下文中的掩码标记,因此注意力权重定义了哪些上下文标记对掩码标记的影响最大。

除了常规的文本 token,Transformer 模型还有一个名为 的特殊 token [CLS]。这个 token 代表了分类任务中的整个序列,这正是我们所需要的。

通过查看[CLS]token 的注意力行,我们可以得到文档中每个 token 对整个文档的重要性。

sentences = "Hello, World - is the starting point in most programming languages"

features = transformer.tokenize(sentences)

# ...

attentions = transformer.auto_model(**features, output_attentions=True).attentions

weights = torch.mean(attentions[-1][0,:,0], axis=0)                       
#                ▲               ▲  ▲   ▲                                 
#                │               │  │   └─── [CLS] token is the first one
#                │               │  └─────── First item of the batch         
#                │               └────────── Last transformer layer       
#                └────────────────────────── Averate all 6 attention heads

for weight, token in zip(weights, tokens):
    print(f"{token}: {weight}")

# [CLS]       : 0.434 // Filter out the [CLS] token
# hello       : 0.039
# ,           : 0.039
# world       : 0.107 // <-- The most important token
# -           : 0.033
# is          : 0.024
# the         : 0.031
# starting    : 0.054
# point       : 0.028
# in          : 0.018
# most        : 0.016
# programming : 0.060 // <-- The third most important token
# languages   : 0.062 // <-- The second most important token
# [SEP]       : 0.047 // Filter out the [SEP] token

BM42 分数的最终公式如下:
score ⁡ ( D , Q ) = ∑ i = 1 N IDF ⁡ ( q i ) × Attention ⁡ ( C L S , q i ) \operatorname{score}(D, Q)=\sum_{i=1}^N \operatorname{IDF}\left(q_i\right) \times \operatorname{Attention}\left(\mathrm{CLS}, q_i\right) score(D,Q)=i=1NIDF(qi)×Attention(CLS,qi)
请注意,经典的 Transformer 有多个注意力头,因此我们可以为同一篇文档获得多个重要性向量。将它们组合起来的最简单方法就是简单地对它们取平均值。

这些平均注意力向量构成了我们正在寻找的重要性信息。最好的部分是,人们可以从任何 Transformer 模型中获取它们,而无需任何额外的训练。因此,只要有 Transformer 模型,BM42 就可以支持任何自然语言。

在我们的实现中,我们使用了该sentence-transformers/all-MiniLM-L6-v2模型,与 SPLADE 模型相比,该模型大大提高了推理速度。实际上,可以使用任何 Transformer 模型。它不需要任何额外的训练,并且可以轻松适应用作 BM42 后端。

WordPiece 重新标记

我们需要解决的最后一块拼图是标记化问题。为了获得注意力向量,我们需要使用原生的 Transformer 标记化。但这种标记化不适合检索任务。我们能做些什么呢?

其实我们想出的解决方案很简单,得到注意力向量后,再逆向进行 token 化的过程。

Transformers 使用WordPiece标记化。如果它看到不在词汇表中的单词,它会将其拆分为子词。

看起来是这样的:

"unbelievable" -> ["un", "##believ", "##able"]

什么可以将子词重新合并到单词中。幸运的是,子词标有前缀##,因此我们可以轻松检测到它们。由于注意力权重已归一化,我们可以简单地将子词的注意力权重相加,以获得单词的注意力权重。

之后,我们可以应用相同的传统 NLP 技术,例如

  • 删除停用词
  • 删除标点符号
  • 词形还原

通过这种方式,我们可以显著减少 token 的数量,从而最大限度地减少稀疏嵌入的内存占用。我们不会同时损害匹配(几乎)精确 token 的能力。

实例

特征 BM25 斯普拉德 BM42
可解释性 高✅ 好的🆗 高✅
文档推理速度 非常高✅ 慢🐌 高✅
查询推理速度 非常高✅ 慢🐌 非常高✅
内存占用 低✅ 高 ❌ 低✅
域内准确度 好的🆗 高✅ 高✅
域外准确度 好的🆗 低❌ 好的🆗
小文件准确度 低❌ 高✅ 高✅
大型文件准确度 高✅ 低❌ 好的🆗
未知令牌处理 是的✅ 糟糕❌ 是的✅
多语言支持 是的✅ 没有❌ 是的✅
最佳匹配 是的✅ 没有❌ 是的✅

从 Qdrant v1.10.0 开始,BM42 可以通过 FastEmbed 推理在 Qdrant 中使用。

让我们看看如何使用 BM42 和jina.ai密集嵌入设置混合搜索集合。

PUT collections/my-hybrid-collection
{
  "vectors": {
    "jina": {
      "size": 768,
      "distance": "Cosine"
    }
  },
  "sparse_vectors": {
    "bm42": {
      "modifier": "idf" // <--- This parameter enables the IDF calculation
    }
  }
}

搜索查询将检索具有密集和稀疏嵌入的文档,并使用倒数秩融合 (RRF) 算法合并分数。

from fastembed import SparseTextEmbedding, TextEmbedding

query_text = "best programming language for beginners?"

model_bm42 = SparseTextEmbedding(model_name="Qdrant/bm42-all-minilm-l6-v2-attentions")
model_jina = TextEmbedding(model_name="jinaai/jina-embeddings-v2-base-en")

sparse_embedding = list(embedding_model.query_embed(query_text))[0]
dense_embedding = list(model_jina.query_embed(query_text))[0]

client.query_points(
  collection_name="my-hybrid-collection",
  prefetch=[
      models.Prefetch(query=sparse_embedding.as_object(), using="bm42", limit=10),
      models.Prefetch(query=dense_embedding.tolist(),  using="jina", limit=10),
  ],
  query=models.FusionQuery(fusion=models.Fusion.RRF), # <--- Combine the scores
  limit=10
)

基准

为了进一步证明这一点,我们进行了一些基准测试,以突出 BM42 优于 BM25 的情况。请注意,我们无意进行详尽的评估,因为我们介绍的是一种新方法,而不是一种新模型。

对于我们的实验,我们选择了quora数据集,它代表了一个问题去重任务问答任务

该数据集的典型示例如下:

{"_id": "109", "text": "How GST affects the CAs and tax officers?"}
{"_id": "110", "text": "Why can't I do my homework?"}
{"_id": "111", "text": "How difficult is it get into RSI?"}

正如您所见,它的文本很短,没有太多可依赖的统计数据。

使用 BM42 编码后,每个文档的平均向量大小仅为5.6 个元素。

在 Qdrant 中,对于约 530k 个文档,datatype: uint8稀疏向量索引的总大小约为13Mb 。

BM25(坦蒂维) BM42
~精度为 10~* 0.45 0.49
召回率@10 0.71 0.85

* - 由于评估脚本中的错误,发布后对值进行了更正。

为了使我们的基准透明,我们发布了用于评估的脚本:请参阅github repo

请注意,BM25 和 BM42 在生产环境中单独使用效果都不是很好。最佳效果是通过混合方法结合稀疏和密集嵌入来实现的。在这种情况下,这两个模型是互补的。稀疏模型负责精确的标记匹配,而密集模型负责语义匹配。

一些更高级的模型可能会比sentence-transformers/all-MiniLM-L6-v2我们使用的默认模型表现更好。我们鼓励参与训练嵌入模型的开发人员加入一种提取注意力权重的方法,并为 BM42 后端做出贡献。

培养好奇心和实验精神

尽管 BM42 具有诸多优点,但它并非万能药。对于没有块的大型文档,BM25 可能仍然是更好的选择。

可能有更智能的方法从 transformer 中提取重要性信息。可能有更好的方法来权衡 IDF 与注意力得分。

Qdrant 并不专注于模型训练。我们的核心项目是搜索引擎本身。但是,我们明白,我们并不是在真空中运作。通过引入 BM42,我们正在加紧为我们的社区提供新颖的实验工具。

我们确实相信稀疏向量方法处于精确的抽象层次,能够产生强大而灵活的结果。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-12 17:32:10       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-12 17:32:10       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-12 17:32:10       58 阅读
  4. Python语言-面向对象

    2024-07-12 17:32:10       69 阅读

热门阅读

  1. CSS基础

    2024-07-12 17:32:10       22 阅读
  2. 动态路由的基本概念

    2024-07-12 17:32:10       21 阅读
  3. Linux系统基础命令有哪些

    2024-07-12 17:32:10       20 阅读
  4. 嵌入式Qt开发C++核心编程知识万字总结

    2024-07-12 17:32:10       28 阅读
  5. linux ssh 远程执行shell 获取返回值

    2024-07-12 17:32:10       22 阅读
  6. WSGI 服务器教程:`execute` 方法解析

    2024-07-12 17:32:10       22 阅读
  7. Scala学习笔记16: 注解

    2024-07-12 17:32:10       18 阅读
  8. vscode使用ssh连接远程服务器

    2024-07-12 17:32:10       15 阅读
  9. RabbitMQ 高级功能

    2024-07-12 17:32:10       20 阅读
  10. 【QT学习十五】 QT绘图

    2024-07-12 17:32:10       23 阅读
  11. 3213. 最小代价构造字符串

    2024-07-12 17:32:10       19 阅读
  12. 基于 SD 卡的 FatFs 文件系统

    2024-07-12 17:32:10       19 阅读
  13. 拥抱变革,AI工作新纪元

    2024-07-12 17:32:10       19 阅读