BM25
简介
BM25算法,全称为Okapi BM25(BM,Best Matching),Okapi BM25 包括第一个使用它的系统的名称,即 1980 年代和 1990 年代在伦敦城市大学实施的 Okapi 信息检索系统。
BM25(Best Matching 25)是一种用于信息检索(Information Retrieval)和文本挖掘的算法,它被广泛应用于搜索引擎和相关领域。BM25 基于 TF-IDF(Term Frequency-Inverse Document Frequency)的思想,但对其进行了改进以考虑文档的长度等因素。
现代BM25算法是用来计算某一个目标文档(Document)相对于一个查询关键字(Query)的“相关性”(Relevance)的流程。通常情况下,BM25是“非监督学习”排序算法中的一个典型代表。
BM25 算法的实现通常用于排序文档,使得与查询更相关的文档排名更靠前。在信息检索领域,BM25 已经成为一个经典的算法。
基本思想
BM25 算法的基本思想:
-
TF-IDF 的改进: BM25 通过对文档中的每个词项引入饱和函数(saturation function)和文档长度因子,改进了 TF-IDF 的计算。让算法在衡量词与文档相关性时更加精准。
-
饱和函数: 在 BM25 中,对于词项的出现次数(TF),引入了一个饱和函数来调整其权重。这是为了防止某个词项在文档中出现次数过多导致权重过大。
在文档中,某些词可能出现次数过多,如果直接按照 TF-IDF 计算,这些词的权重会过大,可能会掩盖其他重要词的作用。BM25 算法引入饱和函数来调整词项出现次数(TF)的权重,有效避免了某个词项权重过高的问题,使得算法能更合理地评估每个词对文档相关性的贡献。
-
文档长度因子: BM25 考虑了文档的长度,引入了文档长度因子,使得文档长度对权重的影响不是线性的。这样可以更好地适应不同长度的文档。
不同文档长度差异很大,如果不考虑文档长度,短文档可能因为词频较低,在检索中处于劣势。BM25 引入的文档长度因子,使得文档长度对权重的影响不再是简单的线性关系。它会根据文档的平均长度,对不同长度文档中的词权重进行调整,让算法能更好地适应各种长度的文档,提高检索的公平性和准确性。
计算公式
BM25 的具体计算公式如下:
完整公式:
其中:
-
是查询中的词项数。
-
是查询中的第个词项,即对 query 进行特征提取分解,生成的若干特征项(词)。
-
是逆文档频率,计算方式通常是 ,其中 是文档总数, 是包含词项 的文档数。
-
是词项在文档 中的出现次数()级,即 。
-
是文档 的长度,即 。
-
是所有文档的平均长度, 即 。
-
和 是调整参数,分别控制词语频率饱和度和文档长度标准化的影响。通常设置为 和 。
根据实验,k 的值在 0.5 到 2 的范围内趋于最优,而 b 的值在 0.3 到 0.9 之间趋于最优 。
- 是一个正系数,用于控制词频的饱和度。较高的 k1 值意味着词频对评分的影响更大
- 超参数 起着调节特征词文本频率尺度的作用, 取 0 意味着算法退化为二元模型(不考虑词频),而 取较大的值则近似于只用原始的特征词频。
- 是用于控制文档长度对评分的影响的参数,取值在0到1之间。当 时,文档长度的影响最大;当 时,文档长度不影响评分。
- 超参数 一般称作文本长度的规范化,作用是调整文档长度对相关性影响的大小。 越大,文档长度的对相关性得分的影响越大,而文档的相对长度越长,则相关性得分会越小。
- 是一个正系数,用于控制词频的饱和度。较高的 k1 值意味着词频对评分的影响更大
关键组件
BM25 的关键组件
- **词语频率 (TF):**TF 是指特定词语在文档中出现的次数。但是,BM25 使用修改后的词语频率,该词语考虑了饱和度效应,以防止过分强调大量重复的词语。
- **逆向文档频率 (IDF):**IDF 衡量术语在整个语料库中的重要性。它为语料库中罕见的术语分配较高的权重,为常见的术语分配较低的权重。IDF 使用以下公式计算:,其中 是文档总数, 是包含该术语的文档数。
- 文档长度标准化 :BM25 合并了文档长度标准化,以解决文档长度对相关性评分的影响。较长的文档往往出现多个术语,从而导致潜在的偏差。文档长度规范化通过将术语频率除以文档的长度并应用规范化因子来抵消这种偏差。
- 查询词饱和度 :BM25 还包括词饱和函数,以减轻过高词频的影响。此功能减少了极高的词语频率对相关性评分的影响,因为非常高的频率通常对应于信息较少的词语。
BM25优缺点
优点:
- BM25 是一种广泛使用的排序算法,因为它在生成相关搜索结果方面简单而有效。
- 它同时考虑了词语频率和文档长度规范化,这有助于解决文档长度偏差的问题。
- 该算法可以有效地处理大型文档集合,使其可针对实际搜索方案进行扩展。
缺点:
- BM25 不考虑查询和文档的语义含义或上下文,这可能会导致某些类型的查询排名不理想。
- 它假定查询词之间的统计独立性,这在存在词语依赖关系的某些情况下可能并不成立。
- 该算法严重依赖词语频率和文档长度,可能会忽略其他重要因素,如文档结构和相关性反馈。
BM25变体
BM25 算法公式,通过使用不同的特征项的分析方法、特征项权重判定方法,以及特征项与文档的相关度计算方法,都留有较强的灵活性,自然会促使后续的研究者在此基础上,提出更具个性化的不同的搜索相关性得分算法。
| BM25 Variants | Scoring Function |
|---|---|
| Robertson et al. | |
| Lucene (default) | |
| Lucene (accurate) | |
| ATIRE | |
| BM25L | |
| BM25+ | |
| BM25-adpt | |
BM25L
BM25L(BM25 with Length Normalization):BM25L算法是在BM25算法的基础上,考虑了文档长度对得分的影响,通过引入文档长度规范化项来平衡不同长度的文档,目的是降低文档长度对相关性评分的影响,它可以通过对BM25公式中的长度归一化因子进行调整来实现,优化点改进在于更全面地考虑文档特征,以更准确地衡量文档与查询之间的相似度。
BM25+
BM25+是一种改进的BM25算法,加入了查询项权重的计算,以更好地处理查询中的重要词项,这个惩罚项用于调整较长的文档的相关性评分,以避免较长的文档在评分中占据过大的比重。优化点改进在于对查询项的权重进行动态调整,以提高信息检索的准确性和性能。
BM25T
BM25T(BM25 with Term Weights)是一种将词权重引入BM25的方法,通过考虑词频、逆文档频率以及文档长度等特征,以确定每个词项在文本中的重要性,它允许用户为查询中的每个词分配不同的权重,以更好地反映查询的重要性。优化点改进在于更精细地衡量词项的重要性,以提高信息检索的准确性。
BM25F
BM25F是一种将多个字段考虑在内的改进算法。在信息检索中,通常会有多个字段(如标题、正文、标签等)的相关性需要评分。BM25F通过对多个字段的评分进行加权求和,可以更好地考虑文档的不同部分对匹配得分的影响,从而得出最终的相关性评分。优化点改进在于更灵活地处理文档的不同部分,以提高信息检索的准确性。
将词频和词位置信息综合考虑的改进算法。其中, 表示词频, 函数表示词位置的影响, 表示词位置权重, 表示逆文档频率,通过 和 来调整词频和逆文档频率的权重,以提高对稀有词项的重视程度。这种算法可以根据词在文档中的位置给予不同的权重,进一步提高相关性评分的准确性。优化点改进在于更好地处理稀有词项,以适应大规模数据集的场景。
BM25-adpt
之前的 BM25 算法和相关改进,都忽略了对超参数 的考察。Lv & Zhai 在不同的 BM25 相关研究工作中,发现对实际应用而言,全局的 参数不及特征项相关的(term-specific) 参数使用起来高效。他们用随机理论中的信息增益和散度等概念,实现了 去“超参化” 的目标,即 跟随 不同而变化,可以直接计算获得,这个算法被称为 BM25-adpt。
实践
BM25代码
为了完全理解BM25算法的计算过程,因此,我们可以查看完整实现代码
其他变体的实现代码,可看:rank_bm25/rank_bm25.py at master · dorianbrown/rank_bm25
import math
import numpy as np
from multiprocessing import Pool, cpu_count
class BM25: # 定义BM25基类
def __init__(self, corpus, tokenizer=None): # 初始化方法,接收语料库和分词器参数
self.corpus_size = 0 # 初始化语料库大小为0
self.avgdl = 0 # 初始化平均文档长度为0
self.doc_freqs = [] # 初始化文档频率列表
self.idf = {} # 初始化逆文档频率字典
self.doc_len = [] # 初始化文档长度列表
self.tokenizer = tokenizer # 设置分词器
if tokenizer: # 如果提供了分词器
corpus = self._tokenize_corpus(corpus) # 使用分词器处理语料库
nd = self._initialize(corpus) # 初始化处理语料库,返回词频统计
self._calc_idf(nd) # 计算逆文档频率
def _initialize(self, corpus): # 初始化方法,处理语料库
nd = {} # 创建词->包含该词的文档数量的映射字典
num_doc = 0 # 初始化文档总词数
for document in corpus: # 遍历语料库中的每个文档
self.doc_len.append(len(document)) # 添加文档长度到文档长度列表
num_doc += len(document) # 累加文档词数到总词数
frequencies = {} # 创建词频字典
for word in document: # 遍历文档中的每个词
if word not in frequencies: # 如果词不在频率字典中
frequencies[word] = 0 # 初始化该词的频率为0
frequencies[word] += 1 # 该词的频率加1
self.doc_freqs.append(frequencies) # 将该文档的词频字典添加到文档频率列表
for word, freq in frequencies.items(): # 遍历词频字典中的每个词和频率
try:
nd[word] += 1 # 尝试增加该词的文档频率
except KeyError:
nd[word] = 1 # 如果词不存在,初始化为1
self.corpus_size += 1 # 语料库大小加1
self.avgdl = num_doc / self.corpus_size # 计算平均文档长度
return nd # 返回词的文档频率字典
def _tokenize_corpus(self, corpus): # 对语料库进行分词的方法
pool = Pool(cpu_count()) # 创建与CPU核心数量相同的进程池
tokenized_corpus = pool.map(self.tokenizer, corpus) # 使用进程池并行处理分词
return tokenized_corpus # 返回分词后的语料库
def _calc_idf(self, nd): # 计算逆文档频率的方法
raise NotImplementedError() # 抛出未实现错误,需要在子类中实现
def get_scores(self, query): # 获取查询得分的方法
raise NotImplementedError() # 抛出未实现错误,需要在子类中实现
def get_batch_scores(self, query, doc_ids): # 获取批量文档得分的方法
raise NotImplementedError() # 抛出未实现错误,需要在子类中实现
def get_top_n(self, query, documents, n=5): # 获取前N个最相关文档的方法
# 断言检查文档数量与索引语料库大小是否匹配
assert self.corpus_size == len(
documents), "The documents given don't match the index corpus!"
scores = self.get_scores(query) # 获取查询的得分
top_n = np.argsort(scores)[::-1][:n] # 获取得分最高的n个文档索引
return [documents[i] for i in top_n] # 返回得分最高的n个文档
class BM25Okapi(BM25): # 定义BM25Okapi类,继承自BM25
def __init__(self, corpus, tokenizer=None, k1=1.5, b=0.75, epsilon=0.25): # 初始化方法
self.k1 = k1 # 设置k1参数
self.b = b # 设置b参数
self.epsilon = epsilon # 设置epsilon参数
super().__init__(corpus, tokenizer) # 调用父类初始化方法
def _calc_idf(self, nd): # 实现计算逆文档频率的方法
"""
计算词语在文档和语料库中的频率。
该算法为idf值设置了一个下限,为eps * average_idf
"""
# 收集idf总和以计算epsilon值的平均idf
idf_sum = 0 # 初始化idf总和
# 收集具有负idf的词,为它们设置特殊的epsilon值
# 如果词出现在超过一半的文档中,idf可能为负
negative_idfs = [] # 初始化负idf词列表
for word, freq in nd.items(): # 遍历每个词及其文档频率
idf = math.log(self.corpus_size - freq + 0.5) - math.log(freq + 0.5) # 计算idf值
self.idf[word] = idf # 将idf值存入字典
idf_sum += idf # 累加idf值
if idf < 0: # 如果idf值为负
negative_idfs.append(word) # 将词添加到负idf列表
self.average_idf = idf_sum / len(self.idf) # 计算平均idf值
eps = self.epsilon * self.average_idf # 计算epsilon值
for word in negative_idfs: # 遍历所有负idf的词
self.idf[word] = eps # 将其idf值设为epsilon值
def get_scores(self, query): # 实现获取查询得分的方法
"""
ATIRE BM25变体使用了一个使用log(idf)分数的idf函数。为了防止负的idf分数,
该算法还为idf值添加了一个epsilon的下限。
更多信息请参见 [Trotman, A., X. Jia, M. Crane, Towards an Efficient and Effective Search Engine]
:param query: 查询
:return: 得分
"""
score = np.zeros(self.corpus_size) # 初始化得分数组为0
doc_len = np.array(self.doc_len) # 将文档长度列表转换为numpy数组
for q in query: # 遍历查询中的每个词
q_freq = np.array([(doc.get(q) or 0) for doc in self.doc_freqs]) # 获取每个文档中查询词的频率
score += (self.idf.get(q) or 0) * (q_freq * (self.k1 + 1) /
(q_freq + self.k1 * (
1 - self.b + self.b * doc_len / self.avgdl))) # 计算BM25得分并累加
return score # 返回得分数组
def get_batch_scores(self, query, doc_ids): # 实现获取批量文档得分的方法
"""
计算查询和所有文档子集之间的bm25得分
"""
assert all(di < len(self.doc_freqs) for di in doc_ids) # 断言检查所有文档ID是否有效
score = np.zeros(len(doc_ids)) # 初始化得分数组为0
doc_len = np.array(self.doc_len)[doc_ids] # 获取指定文档的长度
for q in query: # 遍历查询中的每个词
q_freq = np.array([(self.doc_freqs[di].get(q) or 0) for di in doc_ids]) # 获取每个指定文档中查询词的频率
score += (self.idf.get(q) or 0) * (q_freq * (self.k1 + 1) /
(q_freq + self.k1 * (
1 - self.b + self.b * doc_len / self.avgdl))) # 计算BM25得分并累加
return score.tolist() # 返回得分列表
相关实现依赖
日常使用时,可以下载相关依赖包来直接使用
rank_bm25
pip install rank_bm25
bm25s
它实现了 BM25 以在 Python 中进行快速词法搜索,与最流行的基于 Python 的库相比,速度提高了 500 倍
pip install bm25s
retriv
retriv 是一个用 Python 实现的用户友好且高效的搜索引擎 ,支持 Sparse(使用 BM25、TF-IDF 进行传统搜索)、Dense( 语义搜索 )和 Hybrid retrieval(稀疏和密集检索的混合)。它允许您在一行代码中构建搜索引擎
pip install retriv
中文 DEMO
在这个案例中,使用的依赖是rank_bm25相关的内容
def chinese_tokenizer(text):
"""
中文分词器,使用jieba进行分词
:param text: 中文文本字符串
:return: 分词后的列表
"""
# 使用jieba进行精确模式分词
words = jieba.cut(text)
# 过滤掉空白字符和单个字符(可选)
words = [word for word in words if len(word.strip()) > 1]
return words
# 完整案例实现
def bm25_chinese_demo():
# 1. 准备中文语料库
corpus = [
"自然语言处理是人工智能领域的一个重要分支",
"信息检索技术帮助我们从大量文档中找到相关内容",
"BM25算法是信息检索中常用的经典算法",
"中文分词是中文自然语言处理的基础步骤",
"搜索引擎使用各种算法来提高搜索结果的相关性",
"TF-IDF和BM25都是基于统计的检索模型",
"深度学习在自然语言处理中取得了显著进展",
"倒排索引是信息检索系统的核心技术之一",
"查询扩展可以提高信息检索的召回率",
"准确率和召回率是评价信息检索系统的重要指标"
]
# 2. 初始化BM25模型,使用中文分词器
tokenized_corpus = [chinese_tokenizer(doc) for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# 3. 准备查询
queries = [
"信息检索算法",
"中文分词技术",
"自然语言处理"
]
# 4. 对每个查询进行检索
for query in queries:
print(f"\n查询: '{query}'")
# 对查询进行分词
tokenized_query = chinese_tokenizer(query)
print(f"分词后的查询: {tokenized_query}")
# 获取相关性最高的前3个文档
top_docs = bm25.get_top_n(tokenized_query, corpus, n=3)
# 打印结果
print("最相关的文档:")
for i, doc in enumerate(top_docs, 1):
print(f"{i}. {doc}")
# 5. 额外:显示语料库统计信息
print("\n语料库统计信息:")
print(f"文档数量: {bm25.corpus_size}")
print(f"文档长度分布: {bm25.doc_len}")
print(f"文档词频: {bm25.nd}")
print(f"文档频率列表: {bm25.doc_freqs}")
print(bm25.idf)
print(f"平均文档长度: {bm25.avgdl:.2f} 词")
print(f"索引词汇量: {len(bm25.idf)} 个唯一词")
# 6. 显示部分重要词的IDF值
print("\n部分关键词的IDF值:")
important_words = ["信息检索", "算法", "自然语言处理", "中文分词", "BM25"]
for word in important_words:
if word in bm25.idf:
print(f"{word}: {bm25.idf[word]:.4f}")
结语
BM25算法作为信息检索领域的经典方法,凭借其对词频饱和、文档长度归一化的优化设计,至今仍是搜索引擎和文本相关性分析的核心工具之一。
技术的深度永远藏在理论与实践的交汇处,期待您在BM25的学习中持续挖掘价值!
RESOURCES
- mp.weixin.qq.com/s/J9VqAT9ox…
- link.springer.com/chapter/10.…
- www.cs.otago.ac.nz/homepages/a…
- medium.com/mlworks/why…
- pub.aimind.so/understandi…
- www.cnblogs.com/shengshengw…
- developer.aliyun.com/article/141…
- xie.infoq.cn/article/8b7…
BM25
简介
BM25算法,全称为Okapi BM25(BM,Best Matching),Okapi BM25 包括第一个使用它的系统的名称,即 1980 年代和 1990 年代在伦敦城市大学实施的 Okapi 信息检索系统。
BM25(Best Matching 25)是一种用于信息检索(Information Retrieval)和文本挖掘的算法,它被广泛应用于搜索引擎和相关领域。BM25 基于 TF-IDF(Term Frequency-Inverse Document Frequency)的思想,但对其进行了改进以考虑文档的长度等因素。
现代BM25算法是用来计算某一个目标文档(Document)相对于一个查询关键字(Query)的“相关性”(Relevance)的流程。通常情况下,BM25是“非监督学习”排序算法中的一个典型代表。
BM25 算法的实现通常用于排序文档,使得与查询更相关的文档排名更靠前。在信息检索领域,BM25 已经成为一个经典的算法。
基本思想
BM25 算法的基本思想:
-
TF-IDF 的改进: BM25 通过对文档中的每个词项引入饱和函数(saturation function)和文档长度因子,改进了 TF-IDF 的计算。让算法在衡量词与文档相关性时更加精准。
-
饱和函数: 在 BM25 中,对于词项的出现次数(TF),引入了一个饱和函数来调整其权重。这是为了防止某个词项在文档中出现次数过多导致权重过大。
在文档中,某些词可能出现次数过多,如果直接按照 TF-IDF 计算,这些词的权重会过大,可能会掩盖其他重要词的作用。BM25 算法引入饱和函数来调整词项出现次数(TF)的权重,有效避免了某个词项权重过高的问题,使得算法能更合理地评估每个词对文档相关性的贡献。
-
文档长度因子: BM25 考虑了文档的长度,引入了文档长度因子,使得文档长度对权重的影响不是线性的。这样可以更好地适应不同长度的文档。
不同文档长度差异很大,如果不考虑文档长度,短文档可能因为词频较低,在检索中处于劣势。BM25 引入的文档长度因子,使得文档长度对权重的影响不再是简单的线性关系。它会根据文档的平均长度,对不同长度文档中的词权重进行调整,让算法能更好地适应各种长度的文档,提高检索的公平性和准确性。
计算公式
BM25 的具体计算公式如下:
完整公式:
其中:
-
是查询中的词项数。
-
是查询中的第个词项,即对 query 进行特征提取分解,生成的若干特征项(词)。
-
是逆文档频率,计算方式通常是 ,其中 是文档总数, 是包含词项 的文档数。
-
是词项在文档 中的出现次数()级,即 。
-
是文档 的长度,即 。
-
是所有文档的平均长度, 即 。
-
和 是调整参数,分别控制词语频率饱和度和文档长度标准化的影响。通常设置为 和 。
根据实验,k 的值在 0.5 到 2 的范围内趋于最优,而 b 的值在 0.3 到 0.9 之间趋于最优 。
- 是一个正系数,用于控制词频的饱和度。较高的 k1 值意味着词频对评分的影响更大
- 超参数 起着调节特征词文本频率尺度的作用, 取 0 意味着算法退化为二元模型(不考虑词频),而 取较大的值则近似于只用原始的特征词频。
- 是用于控制文档长度对评分的影响的参数,取值在0到1之间。当 时,文档长度的影响最大;当 时,文档长度不影响评分。
- 超参数 一般称作文本长度的规范化,作用是调整文档长度对相关性影响的大小。 越大,文档长度的对相关性得分的影响越大,而文档的相对长度越长,则相关性得分会越小。
- 是一个正系数,用于控制词频的饱和度。较高的 k1 值意味着词频对评分的影响更大
关键组件
BM25 的关键组件
- 词语频率 (TF):TF 是指特定词语在文档中出现的次数。但是,BM25 使用修改后的词语频率,该词语考虑了饱和度效应,以防止过分强调大量重复的词语。
- 逆向文档频率 (IDF):IDF 衡量术语在整个语料库中的重要性。它为语料库中罕见的术语分配较高的权重,为常见的术语分配较低的权重。IDF 使用以下公式计算:,其中 是文档总数, 是包含该术语的文档数。
- 文档长度标准化 :BM25 合并了文档长度标准化,以解决文档长度对相关性评分的影响。较长的文档往往出现多个术语,从而导致潜在的偏差。文档长度规范化通过将术语频率除以文档的长度并应用规范化因子来抵消这种偏差。
- 查询词饱和度 :BM25 还包括词饱和函数,以减轻过高词频的影响。此功能减少了极高的词语频率对相关性评分的影响,因为非常高的频率通常对应于信息较少的词语。
BM25优缺点
优点:
- BM25 是一种广泛使用的排序算法,因为它在生成相关搜索结果方面简单而有效。
- 它同时考虑了词语频率和文档长度规范化,这有助于解决文档长度偏差的问题。
- 该算法可以有效地处理大型文档集合,使其可针对实际搜索方案进行扩展。
缺点:
- BM25 不考虑查询和文档的语义含义或上下文,这可能会导致某些类型的查询排名不理想。
- 它假定查询词之间的统计独立性,这在存在词语依赖关系的某些情况下可能并不成立。
- 该算法严重依赖词语频率和文档长度,可能会忽略其他重要因素,如文档结构和相关性反馈。
BM25变体
BM25 算法公式,通过使用不同的特征项的分析方法、特征项权重判定方法,以及特征项与文档的相关度计算方法,都留有较强的灵活性,自然会促使后续的研究者在此基础上,提出更具个性化的不同的搜索相关性得分算法。
| BM25 Variants | Scoring Function |
|---|---|
| Robertson et al. | |
| Lucene (default) | |
| Lucene (accurate) | |
| ATIRE | |
| BM25L | |
| BM25+ | |
| BM25-adpt | |
BM25L
BM25L(BM25 with Length Normalization):BM25L算法是在BM25算法的基础上,考虑了文档长度对得分的影响,通过引入文档长度规范化项来平衡不同长度的文档,目的是降低文档长度对相关性评分的影响,它可以通过对BM25公式中的长度归一化因子进行调整来实现,优化点改进在于更全面地考虑文档特征,以更准确地衡量文档与查询之间的相似度。
BM25+
BM25+是一种改进的BM25算法,加入了查询项权重的计算,以更好地处理查询中的重要词项,这个惩罚项用于调整较长的文档的相关性评分,以避免较长的文档在评分中占据过大的比重。优化点改进在于对查询项的权重进行动态调整,以提高信息检索的准确性和性能。
BM25T
BM25T(BM25 with Term Weights)是一种将词权重引入BM25的方法,通过考虑词频、逆文档频率以及文档长度等特征,以确定每个词项在文本中的重要性,它允许用户为查询中的每个词分配不同的权重,以更好地反映查询的重要性。优化点改进在于更精细地衡量词项的重要性,以提高信息检索的准确性。
BM25F
BM25F是一种将多个字段考虑在内的改进算法。在信息检索中,通常会有多个字段(如标题、正文、标签等)的相关性需要评分。BM25F通过对多个字段的评分进行加权求和,可以更好地考虑文档的不同部分对匹配得分的影响,从而得出最终的相关性评分。优化点改进在于更灵活地处理文档的不同部分,以提高信息检索的准确性。
将词频和词位置信息综合考虑的改进算法。其中, 表示词频, 函数表示词位置的影响, 表示词位置权重, 表示逆文档频率,通过 和 来调整词频和逆文档频率的权重,以提高对稀有词项的重视程度。这种算法可以根据词在文档中的位置给予不同的权重,进一步提高相关性评分的准确性。优化点改进在于更好地处理稀有词项,以适应大规模数据集的场景。
BM25-adpt
之前的 BM25 算法和相关改进,都忽略了对超参数 的考察。Lv & Zhai 在不同的 BM25 相关研究工作中,发现对实际应用而言,全局的 参数不及特征项相关的(term-specific) 参数使用起来高效。他们用随机理论中的信息增益和散度等概念,实现了 去“超参化” 的目标,即 跟随 不同而变化,可以直接计算获得,这个算法被称为 BM25-adpt。
实践
BM25代码
为了完全理解BM25算法的计算过程,因此,我们可以查看完整实现代码
其他变体的实现代码,可看:rank_bm25/rank_bm25.py at master · dorianbrown/rank_bm25
import math
import numpy as np
from multiprocessing import Pool, cpu_count
class BM25: # 定义BM25基类
def __init__(self, corpus, tokenizer=None): # 初始化方法,接收语料库和分词器参数
self.corpus_size = 0 # 初始化语料库大小为0
self.avgdl = 0 # 初始化平均文档长度为0
self.doc_freqs = [] # 初始化文档频率列表
self.idf = {} # 初始化逆文档频率字典
self.doc_len = [] # 初始化文档长度列表
self.tokenizer = tokenizer # 设置分词器
if tokenizer: # 如果提供了分词器
corpus = self._tokenize_corpus(corpus) # 使用分词器处理语料库
nd = self._initialize(corpus) # 初始化处理语料库,返回词频统计
self._calc_idf(nd) # 计算逆文档频率
def _initialize(self, corpus): # 初始化方法,处理语料库
nd = {} # 创建词->包含该词的文档数量的映射字典
num_doc = 0 # 初始化文档总词数
for document in corpus: # 遍历语料库中的每个文档
self.doc_len.append(len(document)) # 添加文档长度到文档长度列表
num_doc += len(document) # 累加文档词数到总词数
frequencies = {} # 创建词频字典
for word in document: # 遍历文档中的每个词
if word not in frequencies: # 如果词不在频率字典中
frequencies[word] = 0 # 初始化该词的频率为0
frequencies[word] += 1 # 该词的频率加1
self.doc_freqs.append(frequencies) # 将该文档的词频字典添加到文档频率列表
for word, freq in frequencies.items(): # 遍历词频字典中的每个词和频率
try:
nd[word] += 1 # 尝试增加该词的文档频率
except KeyError:
nd[word] = 1 # 如果词不存在,初始化为1
self.corpus_size += 1 # 语料库大小加1
self.avgdl = num_doc / self.corpus_size # 计算平均文档长度
return nd # 返回词的文档频率字典
def _tokenize_corpus(self, corpus): # 对语料库进行分词的方法
pool = Pool(cpu_count()) # 创建与CPU核心数量相同的进程池
tokenized_corpus = pool.map(self.tokenizer, corpus) # 使用进程池并行处理分词
return tokenized_corpus # 返回分词后的语料库
def _calc_idf(self, nd): # 计算逆文档频率的方法
raise NotImplementedError() # 抛出未实现错误,需要在子类中实现
def get_scores(self, query): # 获取查询得分的方法
raise NotImplementedError() # 抛出未实现错误,需要在子类中实现
def get_batch_scores(self, query, doc_ids): # 获取批量文档得分的方法
raise NotImplementedError() # 抛出未实现错误,需要在子类中实现
def get_top_n(self, query, documents, n=5): # 获取前N个最相关文档的方法
# 断言检查文档数量与索引语料库大小是否匹配
assert self.corpus_size == len(
documents), "The documents given don't match the index corpus!"
scores = self.get_scores(query) # 获取查询的得分
top_n = np.argsort(scores)[::-1][:n] # 获取得分最高的n个文档索引
return [documents[i] for i in top_n] # 返回得分最高的n个文档
class BM25Okapi(BM25): # 定义BM25Okapi类,继承自BM25
def __init__(self, corpus, tokenizer=None, k1=1.5, b=0.75, epsilon=0.25): # 初始化方法
self.k1 = k1 # 设置k1参数
self.b = b # 设置b参数
self.epsilon = epsilon # 设置epsilon参数
super().__init__(corpus, tokenizer) # 调用父类初始化方法
def _calc_idf(self, nd): # 实现计算逆文档频率的方法
"""
计算词语在文档和语料库中的频率。
该算法为idf值设置了一个下限,为eps * average_idf
"""
# 收集idf总和以计算epsilon值的平均idf
idf_sum = 0 # 初始化idf总和
# 收集具有负idf的词,为它们设置特殊的epsilon值
# 如果词出现在超过一半的文档中,idf可能为负
negative_idfs = [] # 初始化负idf词列表
for word, freq in nd.items(): # 遍历每个词及其文档频率
idf = math.log(self.corpus_size - freq + 0.5) - math.log(freq + 0.5) # 计算idf值
self.idf[word] = idf # 将idf值存入字典
idf_sum += idf # 累加idf值
if idf < 0: # 如果idf值为负
negative_idfs.append(word) # 将词添加到负idf列表
self.average_idf = idf_sum / len(self.idf) # 计算平均idf值
eps = self.epsilon * self.average_idf # 计算epsilon值
for word in negative_idfs: # 遍历所有负idf的词
self.idf[word] = eps # 将其idf值设为epsilon值
def get_scores(self, query): # 实现获取查询得分的方法
"""
ATIRE BM25变体使用了一个使用log(idf)分数的idf函数。为了防止负的idf分数,
该算法还为idf值添加了一个epsilon的下限。
更多信息请参见 [Trotman, A., X. Jia, M. Crane, Towards an Efficient and Effective Search Engine]
:param query: 查询
:return: 得分
"""
score = np.zeros(self.corpus_size) # 初始化得分数组为0
doc_len = np.array(self.doc_len) # 将文档长度列表转换为numpy数组
for q in query: # 遍历查询中的每个词
q_freq = np.array([(doc.get(q) or 0) for doc in self.doc_freqs]) # 获取每个文档中查询词的频率
score += (self.idf.get(q) or 0) * (q_freq * (self.k1 + 1) /
(q_freq + self.k1 * (
1 - self.b + self.b * doc_len / self.avgdl))) # 计算BM25得分并累加
return score # 返回得分数组
def get_batch_scores(self, query, doc_ids): # 实现获取批量文档得分的方法
"""
计算查询和所有文档子集之间的bm25得分
"""
assert all(di < len(self.doc_freqs) for di in doc_ids) # 断言检查所有文档ID是否有效
score = np.zeros(len(doc_ids)) # 初始化得分数组为0
doc_len = np.array(self.doc_len)[doc_ids] # 获取指定文档的长度
for q in query: # 遍历查询中的每个词
q_freq = np.array([(self.doc_freqs[di].get(q) or 0) for di in doc_ids]) # 获取每个指定文档中查询词的频率
score += (self.idf.get(q) or 0) * (q_freq * (self.k1 + 1) /
(q_freq + self.k1 * (
1 - self.b + self.b * doc_len / self.avgdl))) # 计算BM25得分并累加
return score.tolist() # 返回得分列表
相关实现依赖
日常使用时,可以下载相关依赖包来直接使用
rank_bm25
pip install rank_bm25
bm25s
它实现了 BM25 以在 Python 中进行快速词法搜索,与最流行的基于 Python 的库相比,速度提高了 500 倍
pip install bm25s
retriv
retriv 是一个用 Python 实现的用户友好且高效的搜索引擎 ,支持 Sparse(使用 BM25、TF-IDF 进行传统搜索)、Dense( 语义搜索 )和 Hybrid retrieval(稀疏和密集检索的混合)。它允许您在一行代码中构建搜索引擎
pip install retriv
中文 DEMO
在这个案例中,使用的依赖是rank_bm25相关的内容
def chinese_tokenizer(text):
"""
中文分词器,使用jieba进行分词
:param text: 中文文本字符串
:return: 分词后的列表
"""
# 使用jieba进行精确模式分词
words = jieba.cut(text)
# 过滤掉空白字符和单个字符(可选)
words = [word for word in words if len(word.strip()) > 1]
return words
# 完整案例实现
def bm25_chinese_demo():
# 1. 准备中文语料库
corpus = [
"自然语言处理是人工智能领域的一个重要分支",
"信息检索技术帮助我们从大量文档中找到相关内容",
"BM25算法是信息检索中常用的经典算法",
"中文分词是中文自然语言处理的基础步骤",
"搜索引擎使用各种算法来提高搜索结果的相关性",
"TF-IDF和BM25都是基于统计的检索模型",
"深度学习在自然语言处理中取得了显著进展",
"倒排索引是信息检索系统的核心技术之一",
"查询扩展可以提高信息检索的召回率",
"准确率和召回率是评价信息检索系统的重要指标"
]
# 2. 初始化BM25模型,使用中文分词器
tokenized_corpus = [chinese_tokenizer(doc) for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# 3. 准备查询
queries = [
"信息检索算法",
"中文分词技术",
"自然语言处理"
]
# 4. 对每个查询进行检索
for query in queries:
print(f"\n查询: '{query}'")
# 对查询进行分词
tokenized_query = chinese_tokenizer(query)
print(f"分词后的查询: {tokenized_query}")
# 获取相关性最高的前3个文档
top_docs = bm25.get_top_n(tokenized_query, corpus, n=3)
# 打印结果
print("最相关的文档:")
for i, doc in enumerate(top_docs, 1):
print(f"{i}. {doc}")
# 5. 额外:显示语料库统计信息
print("\n语料库统计信息:")
print(f"文档数量: {bm25.corpus_size}")
print(f"文档长度分布: {bm25.doc_len}")
print(f"文档词频: {bm25.nd}")
print(f"文档频率列表: {bm25.doc_freqs}")
print(bm25.idf)
print(f"平均文档长度: {bm25.avgdl:.2f} 词")
print(f"索引词汇量: {len(bm25.idf)} 个唯一词")
# 6. 显示部分重要词的IDF值
print("\n部分关键词的IDF值:")
important_words = ["信息检索", "算法", "自然语言处理", "中文分词", "BM25"]
for word in important_words:
if word in bm25.idf:
print(f"{word}: {bm25.idf[word]:.4f}")
结语
BM25算法作为信息检索领域的经典方法,凭借其对词频饱和、文档长度归一化的优化设计,至今仍是搜索引擎和文本相关性分析的核心工具之一。
技术的深度永远藏在理论与实践的交汇处,期待您在BM25的学习中持续挖掘价值!