Retrieval指的是通过用户问题(可能经过处理)从已经构建好的向量数据库中查询到相关文档的过程(下图红框的位置)。
今天我们要讲的是BM25算法。
1. TF-IDF算法
TF-IDF算法是BM25算法的基础,所以我们先讲TF-IDF算法。
关键词检索,就是先把查询和文档进行分词,然后在找到查询中的词出现最多的文档,认为它最相关。
这是一种直觉上的简单理解,今天就让我们深入理解具体的过程。
让我们继续从图书馆管理员的例子开始讲起。
假设你是一位图书管理员,有人来问你:“请帮我找找关于火星探测的书。”
第一步:最基础的直觉(TF - 词频)
你的第一个念头是:一本书如果提到了“火星”和“探测”这两个词,那它就可能相关。如果一本书提到“火星”10次,另一本只提到1次,那么提到10次的这本书很可能更相关。
这就是最核心的概念:词频(Term Frequency, TF) 。一个词在文章里出现的次数越多,文章和这个词的相关性就越高。
第二步:引入常识判断(IDF - 逆文档频率)
查询是“请帮我找找关于火星探测的书。”,但是我们只会关注“火星”和“探测”这两个词,而几乎忽略“请”,“的”,“关于”等等这种几乎每本书里都有的常用词。
为什么呢?因为“火星”这个词很稀有,只有少数书籍会讨论它,那么它能够传递的信息量就很多。而“的”这个字太普遍了,它不提供任何具体信息。
这就是第二个核心概念:逆文档频率(Inverse Document Frequency, IDF) 。一个词在所有书籍中出现的次数越少,它就越稀有、越珍贵,一旦它出现在某本书里,它的“权重”或“信息量”就应该越高。
TF 和 IDF 结合起来,就是信息检索领域的经典模型TF-IDF。它告诉我们:要找那些高频出现了稀有词汇的文档。
2. BM25算法
BM25是基于TF-IDF发展而来的,它进一步修正了TF-IDF算法中的漏洞,使得其成为最实用的关键词检索算法。
修正一:词频的“收益递减”效应
回到第一个例子。一本书提到“火星”10次,比提到1次要相关得多。但是,一本书提到“火星”1000次,真的比提到500次要相关一倍吗?
你的直觉会告诉你:不是的。
- 从0次到1次,相关性是质的飞跃。
- 从1次到10次,相关性有显著提升。
- 从500次到1000次,相关性可能几乎没有变化。书的内容已经完全是关于火星的了,再增加词频已经不能提供更多“它与火星相关”的信息了。
BM25的核心洞察之一就是:词频对相关性的贡献不是线性的,它有一个上限,会逐渐饱和。所以,在BM25算法中,当一个词的频率不断增加时,它对总分数的贡献增长会越来越慢,而且趋于一个极限。
修正二:文档长度的“公平性”考量
一本1000页的巨著和一本10页的小册子,如果同样出现了10次“火星”,那么显然10页小册子与火星更加相关。
这是因为如果一篇文档特别长,它天然就有更高的概率会包含查询词,即使它不是专门讲这个主题的。我们应该对这种“长度优势”进行惩罚。反之,如果一篇文档特别短,它能在有限的篇幅里包含查询词,说明这个词对它来说可能更重要。我们应该给予奖励。
所以,BM25引入了文档长度归一化。它会计算所有文档的平均长度,然后将当前文档的长度与这个平均值进行比较。
如果文档比平均长度长很多,它的分数就会被适度拉低。如果文档比平均长度短,它的分数就会被适度提高。
3. 公式和代码
公式
BM25为一份文档D和一个查询Q 计算相关性分数,其公式如下:
看起来复杂的公式,其实就是对原始词频做了修正,让它在增长时受到参数k的限制,并根据参数b受文档长度影响。
示例代码
#请首先安装相关库:
# pip install rank_bm25 jieba
import jieba
from rank_bm25 import BM25Okapi
# 1. 定义我们的文档库 (Corpus)
corpus = [
"阿波罗11号是第一次载人登月任务。",
"火星探测车“好奇号”成功登陆火星。",
"载人航天飞行是人类探索宇宙的重要一步。",
"对火星的详细探测揭示了其地质历史。",
"月球是地球唯一的天然卫星,也是人类第一个登陆过的地外天体。"
]
# 2. 对文档进行分词 (Tokenization)
# BM25 基于词进行计算,中文需要先分词。
# 这里我们使用 jieba 库。
tokenized_corpus = [list(jieba.cut(doc)) for doc in corpus]
print("分词后的文档库:", tokenized_corpus)
# 3. 初始化 BM25 模型
bm25 = BM25Okapi(tokenized_corpus)
# 4. 定义一个查询并分词
query = "载人火星探测任务"
tokenized_query = list(jieba.cut(query))
print("\n分词后的查询:", tokenized_query)
# 5. 使用BM25计算相关性分数,并获取最相关的文档
# get_scores 会返回每个文档相对于查询的分数
doc_scores = bm25.get_scores(tokenized_query)
print(f"\n各文档得分: {doc_scores}")
# get_top_n 会直接返回排序后的前N个文档
top_n = bm25.get_top_n(tokenized_query, corpus, n=3)
print("\n--- 检索结果 ---")
print(f"查询: '{query}'")
print("\n最相关的 Top 3 文档:")
for i, doc in enumerate(top_n):
print(f"{i+1}. {doc}")
代码运行效果如下
4. 与其他RAG优化算法结合
RAG的每个方法之间并不是孤立的,而是可以相互结合的来实现1+1 > 2的效果。
下面是Langchain知识库的真实例子
原始的问题是:给我介绍langchain的azure
查询的文档集合如下,可以很明显看到相关的文档应该是如图所示的三个文档
但是如果直接使用BM25算法,却会引入一个错误的查询结果
这时候,我们可以首先调用HyDE算法,生成一个假设性的回答,然后使用这个假设性的回答来进行检索
生成的假设性回答:LangChain 是一个用于构建基于语言模型的应用程序的框架,特别是在处理自然语言处理(NLP)任务时。它提供了一系列工具和组件,帮助开发者更高效地构建、管理和部署与语言模型相关的应用。LangChain 的 Azure 集成使得开发者能够利用 Microsoft Azure 的强大云计算能力和服务,来增强其语言模型应用的性能和可扩展性。在 Azure 上使用 LangChain,开发者可以利用 Azure OpenAI 服务,这是一项提供访问 OpenAI 的语言模型(如 GPT-3 和 GPT-4)的服务。通过 Azure OpenAI,用户可以轻松地将强大的语言生成能力集成到他们的应用中。LangChain 提供了与 Azure OpenAI 的无缝集成,使得调用和管理这些模型变得更加简单。LangChain 的 Azure 集成包括以下几个关键组件:Azure OpenAI API:开发者可以通过 LangChain 直接调用 Azure OpenAI API,进行文本生成、问答、对话等任务。LangChain 提供了封装好的接口,简化了 API 的调用过程。文档检索:LangChain 支持与 Azure 存储服务(如 Azure Blob Storage 和 Azure Cosmos DB)的集成,允许用户存储和检索文档。这使得用户可以构建基于文档的问答系统,利用 Azure 的存储能力来管理大量数据。链式调用:LangChain 的设计理念是将多个处理步骤串联起来,形成一个“链”。在 Azure 环境中,用户可以创建复杂的工作流,将 Azure 的计算资源与 LangChain 的处理能力结合起来,实现更复杂的 NLP 任务。环境管理:LangChain 允许用户在 Azure 上轻松管理和配置环境,包括选择合适的计算资源、设置环境变量等。这使得在 Azure 上部署和运行 LangChain 应用变得更加灵活和高效。可扩展性:利用 Azure 的云基础设施,LangChain 应用可以根据需求进行横向扩展,处理更大规模的请求和数据。这对于需要高可用性和高性能的应用尤为重要。安全性和合规性:Azure 提供了多层次的安全措施,确保数据的安全性和隐私保护。LangChain 在 Azure 上运行时,可以利用这些安全特性,确保用户数据的安全。总之,LangChain 的 Azure 集成为开发者提供了一个强大的平台,能够快速构建和部署基于语言模型的应用。通过利用 Azure 的计算和存储能力,开发者可以创建高效、可扩展且安全的 NLP 解决方案,满足各种业务需求。
可以看到,这时候可以检索得到正确的文档,而且相关文档的BM25得分远高于不相关文档。