[论文笔记]BM25S:Python打造超越RANK-BM25的实现

news2024/11/15 11:41:41

引言

今天带来一篇BM25变种的论文笔记,不要低估BM25,在RAG中检索中通常都会引入BM25检索,然后配合嵌入模型进行混合检索。

BM25S: Orders of magnitude faster lexical search via eager sparse scoring,题目翻译过来是: 通过快速稀疏评分实现数量级速度提升的词汇检索。

BM25S是一种高效的基于Python的BM25实现,仅依赖于Numpy和Scipy。与最流行的基于Python的框架(Rank-BM25)相比,BM25S在索引期间通过即时(eager)计算BM25分数并将其存储到稀疏矩阵中,实现了高达500倍的速度提升。

202408080001

代码开源在:https://github.com/xhluca/bm25s

1. 背景

稀疏词汇搜索算法(Sparse lexical search algorithms),比如BM25,仍然得到了广泛地应用,因为不需要训练,可应用于多种语言,并且速度较快。尤其是基于Lucene的实现,通常比现有的基于Python的实现(如Rank-BM25)更快。

本篇工作通过引入两项改进,能带来比现有基于Python实现显著的速度提升:在索引语料库时即时计算所有可能分配给任何未来查询标记的分数,并将这些计算存储在稀疏矩阵中,以便实现更快速的切片和求和。稀疏矩阵的思想在BM25-PT中被探索,它使用PyTorch预计算BM25分数,并通过稀疏矩阵乘法将其与查询的词袋编码相乘。

而本篇工作的BM25S不依赖于Pytorch,使用Scipy的稀疏矩阵实现,BM25-PT通过词袋与文档矩阵相乘,而BM25S则切片相关索引并在标记维度上进行求和,消除了矩阵乘法的需求。实现时BM25S还引入了一种简单快速的基于Python的分词器。

2. 实现

BM25的计算 我们使用Lucene提出的评分方法,对于一个给定查询 Q Q Q(分词为 q 1 , ⋯   , q ∣ Q ∣ ) q_1,\cdots,q_{|Q|}) q1,,qQ)和来自集合 C C C中的文档 D D D,计算下面的分数:
B ( Q , D ) = ∑ i = 1 ∣ Q ∣ S ( q i , D ) = ∑ i = 1 ∣ Q ∣ IDF ( q i , C ) TF ( q i , D ) D \begin{aligned} B(Q,D) &= \sum_{i=1}^{|Q|} S(q_i,D) \\ &= \sum_{i=1}^{|Q|} \text{IDF}(q_i,C) \frac{\text{TF}(q_i,D)}{\mathcal D} \end{aligned} B(Q,D)=i=1QS(qi,D)=i=1QIDF(qi,C)DTF(qi,D)
其中 D = TF ( t , D ) + k 1 ( 1 − b + b ∣ D ∣ L avg ) \mathcal D = \text{TF}(t,D) + k_1\left(1 - b + b \frac{|D|}{L_{\text{avg}}} \right) D=TF(t,D)+k1(1b+bLavgD) L avg L_{\text{avg}} Lavg是语料库 C C C中的平均文档长度(通过标记数计算), TF ( q i , D ) \text{TF}(q_i,D) TF(qi,D)是标记 q i q_i qi在文档 D D D中的词频, IDF \text{IDF} IDF是逆文档频率,计算为:
IDF ( q i , C ) = ln ⁡ ( ∣ C ∣ − DF ( q i , C ) + 0.5 DF ( q i , C ) + 0.5 + 1 ) \text{IDF}(q_i,C) = \ln \left(\frac{|C| - \text{DF}(q_i,C) + 0.5}{\text{DF}(q_i,C) + 0.5} + 1 \right) IDF(qi,C)=ln(DF(qi,C)+0.5CDF(qi,C)+0.5+1)
其中文档频率 DF ( q i , C ) \text{DF}(q_i,C) DF(qi,C) C C C中包含 q i q_i qi的文档数。尽管 B ( Q , D ) B(Q,D) B(Q,D)依赖于查询(query),而查询仅在检索期间提供,下面会展示如何重构等式,以便在索引时即时计算 TF \text{TF} TF IDF \text{IDF} IDF

即时索引评分 现在考虑词表 V V V中的所有标记,记作 t ∈ V t \in V tV。我们可以重构 S ( t , D ) S(t,D) S(t,D)为:
S ( t , D ) = TF ( t , D ) ⋅ IDF ( t , C ) 1 D S(t,D) = \text{TF}(t,D) \cdot \text{IDF}(t,C) \frac{1}{\mathcal D} S(t,D)=TF(t,D)IDF(t,C)D1
t t t是文档 D D D中不存在的标记时, TF ( t , D ) = 0 \text{TF}(t, D) = 0 TF(t,D)=0,这也导致 S ( t , D ) = 0 S(t, D) = 0 S(t,D)=0。这意味着,对于词表 V V V中的大多数标记,我们可以简单地将相关性评分设置为0,仅计算实际出现在文档 D D D中的 t t t的值。这个计算可以在索引过程中完成,从而避免在查询时计算 S ( q i , D ) S(q_i, D) S(qi,D)

分配查询评分

考虑形状为 ∣ V ∣ × ∣ C ∣ |V|×|C| V×C的稀疏矩阵,可以使用查询标记选择相关的行,从而得到一个形状为 ∣ Q ∣ × ∣ C ∣ |Q|×|C| Q×C的矩阵,然后可以在列维度上进行求和,最终得到一个单一的 ∣ C ∣ |C| C维向量(表示每个文档对查询的评分)。

高效矩阵稀疏

采用压缩稀疏列(Compressed Sparse Column, CSC)格式实现稀疏矩阵(使用scipy.sparse.csc_matrix),这在坐标格式与CSC格式之间提供了高效的转换。由于我们在列维度上进行切片和求和,这一实现是稀疏矩阵实现中的最佳选择。在实践中,我们直接使用Numpy数组来复制稀疏操作。

分词

为了拆分文本,使用与Scikit-Learn为其自己的标记器所用的相同正则表达式模式,模式为r"(?u)\b\w\w+\b"。该模式便捷地解析UTF-8中的单词(涵盖多种语言),其中\b​处理单词边界。然后,如果需要词干提取,我们可以对词汇表中的所有单词进行词干提取,以便查找集合中每个单词的词干版本。最后,我们构建一个字典,将每个唯一的(词干)单词映射到一个整数索引,以便将标记转换为相应的索引,从而显著减少内存使用并使其能够用于切片Scipy矩阵和Numpy数组。

Top-k 选择

在计算完集合中所有文档的评分后,我们可以通过选择前 k 个最相关的元素来完成搜索过程。一种简单的方法是对评分向量进行排序,并选择最后 k 个元素;而作者采用的是对数组的划分,仅选择最后 k 个文档(无序)。使用像 Quickselect这样的算法,可以在平均时间复杂度为 O ( n ) O(n) O(n)的情况下完成这一操作,其中 n 是集合中的文档数量,而排序需要 O ( n log ⁡ n ) O(n \log n) O(nlogn)。如果用户希望按顺序接收前 k 个结果,排序划分后的文档将额外消耗 O ( k l o g k ) O(k log k) O(klogk) 的时间复杂度,这在假设 k ≪ n k ≪ n kn 的情况下是可以忽略不计的。

在实践中,BM25S 允许使用两种实现:一种基于 numpy,利用 np.argpartition,另一种基于 jax,依赖于 XLA 的 top-k 实现。Numpy 的 argpartition 使用了内省选择算法,该算法修改了 quickselect 算法,以确保最坏情况下的性能保持在 O ( n ) O(n) O(n) 内。实际上观察到 JAX 的实现上表现更佳。

**多线程 **

通过池化执行器实现可选的多线程功能,以在检索过程中进一步加速。

BM25 的替代实现

上述内容描述了如何为 BM25 的一种变体(Lucene)实现 BM25S。然而,可以很容易地将 BM25S 方法扩展到许多 BM25 的变体。

2.1 基于未出现调整扩展稀疏性

对于 BM25L、BM25+,当 TF ( t , D ) = 0 \text{TF}(t,D)=0 TF(t,D)=0时, S ( t , D ) S(t, D) S(t,D) 的值不会为零。将这个值记作标量 S θ ( t ) S^θ(t) Sθ(t),表示当 t t t 不出现在文档 D D D 中时的得分。

显然,构建一个 ∣ V ∣ × ∣ C ∣ |V| × |C| V×C 的稠密矩阵会消耗过多内存。相反,我们仍然可以通过从得分矩阵中每个标记 t t t和文档 D D D 中减去 S θ ( t ) S^θ(t) Sθ(t) 来实现稀疏性(因为词汇中的大多数标记 t t t不会出现在任何给定文档 D D D 中,它们在得分矩阵中的值将为 0)。然后,在检索过程中,我们只需计算每个查询 q i ∈ Q q_i ∈ Q qiQ S θ ( q i ) S^θ(q_i) Sθ(qi),并对其进行求和,以获取一个可以加到最终得分上的标量。更正式地,对于一个空文档 ∅ ∅ ,定义 S θ ( t ) = S ( t , ∅ ) S^θ(t) = S(t, ∅) Sθ(t)=S(t,) 为标记 t t t 的非出现得分。然后,差分 S ∆ ( t , D ) S^∆(t, D) S(t,D)​ 定义为:
S ∆ ( t , D ) = S ( t , D ) − S θ ( t ) S^∆(t, D) = S(t,D) - S^\theta(t) S(t,D)=S(t,D)Sθ(t)
因此,我们重构BM25(B)评分为:
B ( Q , D ) = ∑ i = 1 ∣ Q ∣ S ( q i , D ) = ∑ i = 1 ∣ Q ∣ ( S ( q i , D ) − S θ ( q i ) + S θ ( q i ) ) = ∑ i = 1 ∣ Q ∣ ( S Δ ( q i , D ) + S θ ( q i ) ) = ∑ i = 1 ∣ Q ∣ S Δ ( q i , D ) + ∑ i = 1 ∣ Q ∣ S θ ( q i ) \begin{aligned} B(Q,D) &= \sum_{i=1}^{|Q|} S(q_i,D) \\ &= \sum_{i=1}^{|Q|} \left( S(q_i,D) - S^\theta(q_i) + S^\theta(q_i) \right) \\ &= \sum_{i=1}^{|Q|} \left( S^\Delta(q_i,D) +S^\theta (q_i) \right) \\ &= \sum_{i=1}^{|Q|} S^\Delta(q_i,D) +\sum_{i=1}^{|Q|} S^\theta (q_i) \end{aligned} B(Q,D)=i=1QS(qi,D)=i=1Q(S(qi,D)Sθ(qi)+Sθ(qi))=i=1Q(SΔ(qi,D)+Sθ(qi))=i=1QSΔ(qi,D)+i=1QSθ(qi)
其中$ \sum_{i=1}^{|Q|} S^\Delta(q_i,D)$ 可以使用差分稀疏得分矩阵高效计算在 scipy 中实现。此外, ∑ i = 1 ∣ Q ∣ S θ ( q i ) \sum_{i=1}^{|Q|} S^\theta (q_i) i=1QSθ(qi)​只需要对查询 Q 计算一次,随后可以应用于每个检索到的文档以获得确切的得分。

3. 基准测试

吞吐量

image-20240808104152082

为了进行基准测试,使用 BEIR 基准中公开可用的数据集。表 1 中的结果显示,BM25S 的速度显著快于 Rank-BM25。

分词的影响

image-20240808104339994

进一步通过比较 BM25S Lucene( k 1 = 1.5 , b = 0.75 k_1 = 1.5,b = 0.75 k1=1.5,b=0.75​)的表现,检查分词对每个模型的影响,分别为 (1) 不进行词干提取,(2) 不去除停用词,(3) 两者都不去除,以及 (4) 都去除。总体而言,添加词干提取器平均上会提高得分,而停用词的影响较小。然而,在个别情况下,停用词可能会产生更大的影响。

比较模型变体

image-20240808104436723

在表 3 中,比较了多个实现变体。大多数实现的平均得分在 39.7 到 40 之间,只有 Elastic 实现了稍高的得分。这种差异可以归因于分词方案的不同。

4. 结论

作者提供了一种新颖的计算 BM25 分数的方法,称为 BM25S,该方法不仅提供开箱即用的快速分词和高效的前 K 个选择,还最小化了依赖关系,使其能够直接在 Python 中使用。通过最小化依赖关系,BM25S 成为在存储可能受到限制的场景(例如边缘部署)中的良好选择。

实战

首先安装相相关库:

pip install bm25s

该项目默认只支持英文系列的语言,但我们可以简单修改,加入结巴分词让它支持中文:

from bm25s.tokenization import Tokenized
import jieba
from typing import List, Union
from tqdm.auto import tqdm


def tokenize(
    texts,
    return_ids: bool = True,
    show_progress: bool = True,
    leave: bool = False,
) -> Union[List[List[str]], Tokenized]:
    if isinstance(texts, str):
        texts = [texts]

    corpus_ids = []
    token_to_index = {}

    for text in tqdm(
        texts, desc="Split strings", leave=leave, disable=not show_progress
    ):

        splitted = jieba.lcut(text)
        doc_ids = []

        for token in splitted:
            if token not in token_to_index:
                token_to_index[token] = len(token_to_index)

            token_id = token_to_index[token]
            doc_ids.append(token_id)

        corpus_ids.append(doc_ids)

    # Create a list of unique tokens that we will use to create the vocabulary
    unique_tokens = list(token_to_index.keys())

    vocab_dict = token_to_index

    # Return the tokenized IDs and the vocab dictionary or the tokenized strings
    if return_ids:
        return Tokenized(ids=corpus_ids, vocab=vocab_dict)

    else:
        # We need a reverse dictionary to convert the token IDs back to tokens
        reverse_dict = unique_tokens
        # We convert the token IDs back to tokens in-place
        for i, token_ids in enumerate(
            tqdm(
                corpus_ids,
                desc="Reconstructing token strings",
                leave=leave,
                disable=not show_progress,
            )
        ):
            corpus_ids[i] = [reverse_dict[token_id] for token_id in token_ids]

        return corpus_ids

然后通过bm25s.tokenize = tokenize替换默认,下面参考官网给出一个简单的例子:

import bm25s

bm25s.tokenize = tokenize

# Create your corpus here
corpus = [
    "今天天气晴朗,我的心情美美哒",
    "小明和小红一起上学",
    "我们来试一试吧",
    "我们一起学猫叫",
    "我和Faker五五开",
    "明天预计下雨,不能出去玩了",
]

# corpus = load_corpus("data")

# Tokenize the corpus and index it
corpus_tokens = bm25s.tokenize(corpus)
print(corpus_tokens)

retriever = bm25s.BM25(corpus=corpus)
retriever.index(corpus_tokens)

query = "明天天气怎么样"
query_tokens = bm25s.tokenize(query)
docs, scores = retriever.retrieve(query_tokens, k=3)
print(f"Best result (score: {scores[0, 0]:.2f}): {docs[0, 0]}")
print(docs, scores)

# Happy with your index? Save it for later...
retriever.save("bm25s_index_animals")

# ...and load it when needed
ret_loaded = bm25s.BM25.load("bm25s_index_animals", load_corpus=True)
Tokenized(ids=[[0, 1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15], [12, 10, 16, 17], [3, 18, 19, 20], [21, 22, 23, 2, 24, 25, 26]], vocab={'今天': 0, '天气晴朗': 1, ',': 2, '我': 3, '的': 4, '心情': 5, '美美': 6, '哒': 7, '小明': 8, '和小红': 9, '一起': 10, '上学': 11, '我们': 12, '来': 13, '试一试': 14, '吧': 15, '学': 16, '猫叫': 17, '和': 18, 'Faker': 19, '五五开': 20, '明天': 21, '预计': 22, '下雨': 23, '不能': 24, '出去玩': 25, '了': 26})

Best result (score: 0.53): 明天预计下雨,不能出去玩了
[['明天预计下雨,不能出去玩了' '今天天气晴朗,我的心情美美哒' '小明和小红一起上学']] [[0.5313357 0.        0.       ]]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1992533.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sqlserver导出数据脚本

文章目录 sqlserver导出数据脚本任务-生成脚本 sqlserver导出数据脚本 任务-生成脚本

第二十天的学习(2024.8.8)Vue拓展

昨天的笔记中,我们进行的项目已经可以在网页上显示查询到数据库中的数据,今天的笔记中将会完成在网页上进行增删改查的操作 1.删除表中数据 现在网页上只能呈现出数据库中的数据,我们首先添加一个删除按钮,使其可以对数据库数据…

线上学习管理系统/在线学习系统/网上学习系统

摘 要 本毕业设计的内容是设计并且实现一个基于SSM框架的线上学习管理系统。它是在Windows下,以MYSQL为数据库开发平台,Tomcat网络信息服务作为应用服务器。线上学习管理系统的功能已基本实现,主要包括学生、教师、课程信息、课程资料、试题…

用Python轻松移除PDF中的注释

PDF文档因其跨平台的兼容性和格式稳定性而备受青睐。然而,随着文档在不同用户间的流转,累积的注释可能会变得杂乱无章,甚至包含敏感或过时的信息,这不仅影响了文档的清晰度和专业性,还可能引发隐私风险。因此&#xff…

本地Linux服务器创建我的世界MC私服并实现与好友异地远程联机游戏

文章目录 前言1. 安装JAVA2. MCSManager安装3.局域网访问MCSM4.创建我的世界服务器5.局域网联机测试6.安装cpolar内网穿透7. 配置公网访问地址8.远程联机测试9. 配置固定远程联机端口地址9.1 保留一个固定tcp地址9.2 配置固定公网TCP地址9.3 使用固定公网地址远程联机 前言 本…

01_Electron 跨平台桌面应用开发介绍

Electron 跨平台桌面应用开发介绍 一、Electron 的介绍二、关于 NW.js 和 Electron 介绍三、搭建 Electron 的环境1、准备工作:2、安装 electron 环境3、查看 electron 的版本,electron -v 一、Electron 的介绍 Electron 是由 Github 开发的一个跨平台的…

四宫格照片拼图怎么制作?5种方法制作很简单

一张创意满满的四宫格照片总能瞬间吸引眼球,无论是社交媒体分享还是日常记录,都能让你的作品脱颖而出。今天,给大家分享五种超实用的四宫格照片拼图制作方法,快来一起看看吧。 方法一:迅捷图片转换器 这不仅是一款强大…

数据结构基础入门

😀前言 本篇博文是关于数据结构基础入门,希望你能够喜欢 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨犀,希望我的文章可以帮助到大家,您的满意是我的动力&#x1f609…

有没有比较好的PDF编辑软件可以推荐一下?

推荐3款好用的PDF编辑器,简单好用,且基础功能免费,可以满足99%的工作、学习需求。 支持Windows版、Mac版 1、PDF编辑器 点击直达链接>>pdfbianji.55.la 这是一款功能丰富、编辑简单好用的PDF编辑器,目前仅支持Windows系统…

【C++】函数的定义

函数定义的格式 函数类型 函数名( 参数列表 ) { 函数体语句 return表达式 } 下面是一个实例&#xff0c;是用来写一个加法函数的 #include<iostream> using namespace std;//函数的定义 //语法&#xff1a; //返回值类型 函数名 &#xff08;参数列表&#xff09; {…

DIRB:一款强大的Web目录扫描工具使用指南

网安学习交流 DIRB是一款广泛使用的开源Web内容扫描工具&#xff0c;它专注于发现Web服务器上存在的目录和文件。对于安全研究员、渗透测试人员以及Web开发者来说&#xff0c;DIRB是一个不可或缺的工具&#xff0c;它能帮助他们识别潜在的入口点&#xff0c;从而进一步评估目标…

2023华为od机试C卷【符号运算/求分数计算结果】Python实现

思路: 先将中缀表达式改为后缀表达式,这样就不用考率需要使用括号来标识操作符的优先级。后缀表达式的计算按 操作符 从左到右出现的顺序依次执行(不考虑运算符之间的优先级),对于计算机而言是比较简单的结构。 然后实现后缀表达式的计算。需要注意的是:在处理后缀表达式…

谁才是制作中国式报表的最佳工具?赶紧看看这款“功能强大且免费”的报表工具!

确定制作中国式报表最佳工具的前提是&#xff1a;先搞懂到底什么是中国式报表&#xff1f; 一. 什么是中国式报表&#xff1f; 其实行业内一直没有对中国式报表做出明确的定义&#xff0c;但综合来看&#xff0c;典型的中国式报表具有以下四个显著特征&#xff1a; 1.报表格…

GIF压缩怎么压?整理了六个图片压缩方法(附步骤)

GIF压缩方法有哪些&#xff1f;电脑照片怎么压缩到200k&#xff1f; 最近很经常看到类似的问题&#xff0c;本文就分享几个简单的电脑文件压缩方法。对GIF图进行压缩是一个常见的需求&#xff0c;可以帮助减小文件大小&#xff0c;提高加载速度&#xff0c;同时保持图像质量。…

一文读懂 ESLint配置

你好,我是Qiuner. 为帮助别人少走弯路和记录自己编程学习过程而写博客 这是我的 github https://github.com/Qiuner ⭐️ ​ gitee https://gitee.com/Qiuner &#x1f339; 如果本篇文章帮到了你 不妨点个赞吧~ 我会很高兴的 &#x1f604; (^ ~ ^) 想看更多 那就点个关注吧 我…

2.2 QT 环境配置

2.2 QT环境配置 QT是一个1991年由QT Company开发的跨平台C图形用户界面应用程序开发框架。它既可以开发GUI程序&#xff0c;也可以用于开发非GUI程序&#xff0c;比如控制台工具和服务器。Qt是面向对象的框架&#xff0c;使用特殊的代码生成扩展&#xff08;称为元对象编译器&…

目录函数以及链接文件

一、对stat里面的用户名时间做处理的函数 1.1.getpwuid&#xff08;&#xff09; struct passwd *getpwuid(uid_t uid); 功能: 根据用户id到/etc/passwd文件下解析获得 结构体信息 参数: uid:用户id 返回值: 成功返回id对应用户的信息 失败返回NULL 1. 2.getgrgid&#xf…

第1章 第2节 数据的表示(软件评测师)

1.若某计算机采用8位整数补码表示数据&#xff0c;则运算&#xff08;1271&#xff09;将产生溢出 【解析】8位整数补码表示的整数范围&#xff1a;-128~127 2.采用&#xff08;补码&#xff09;表示带符号数据时&#xff0c;算术运算过程中符号位与数值位采用同样的运算规则…

java框架介绍

Java框架是Java开发中不可或缺的一部分&#xff0c;它们为开发者提供了预定义好的软件架构、类和接口&#xff0c;以及编程规范&#xff0c;从而简化了Java应用程序的开发过程。下面我将详细介绍Java框架的几个方面&#xff1a; 一、Java框架的定义 Java框架是一种为了解决特定…

bootstrap之表格

通过添加 .table-striped class&#xff0c;您将在 <tbody> 内的行上看到条纹 通过添加 .table-bordered class&#xff0c;您将看到每个元素周围都有边框&#xff0c;且占整个表格是圆角的 <!DOCTYPE html> <html><head><meta charset"utf…