BM25,全称是 Best Matching 25,是一种用于信息检索的排序函数,用来估算一个文档与一个查询的相关性得分。它是在20世纪70年代到90年代由一批信息检索研究者(如Stephen E. Robertson和Karen Spärck Jones)开发的概率检索模型系列中的集大成者,被认为是传统词袋模型下最有效、最先进的检索算法之一。
BM25并非一个单一的公式,而是一个算法家族,但其中最经典、最常用的形式是 BM25 Okapi。
1. 核心思想
BM25的核心思想基于以下三个直觉:
-
词频(TF)的饱和性:一个词在文档中出现的次数越多,该文档与查询的相关性可能就越高。但是,这种增长不是线性的。出现10次比出现1次的相关性高很多,但出现100次并不比出现50次的相关性高一倍。BM25通过一个可调节的参数
k1来控制词频的饱和速度。 -
文档长度归一化:长文档自然更容易包含更多的查询词,但这并不意味着它就更相关。BM25引入了文档长度与平均文档长度的比值,通过参数
b来惩罚长文档或偏袒短文档。 -
逆文档频率(IDF):如果一个查询词在所有文档中都出现(如“的”、“是”),那么它对区分相关文档的贡献就很小。反之,一个稀有词(如“区块链”)如果能匹配上,则具有很强的区分能力。IDF就是对这种区分能力的量化。
2. 公式解析
对于一个查询 Q,包含多个词项 ,BM25计算文档
D 的得分公式为:
让我们来分解这个公式的各个部分:
-
Score(D, Q):文档
D对于查询Q的最终相关性得分。 -
Σ:对查询
Q中的每一个词项进行计算,并将结果求和。
-
IDF(qi): 词项
的逆文档频率。
-
常见计算公式为:
-
N:文档集合中的文档总数。 -
n(qi):包含词项qi的文档数量。 -
log:自然对数。 -
作用:惩罚常见词,提升稀有词的权重。
n(qi)越小,IDF值越大。
-
-
TF(qi, D):词项
qi在文档D中的词频。-
即
qi在文档D中出现的次数。
-
-
|D|:文档
D的长度(通常用词条数表示)。 -
avgdl:文档集合中所有文档的平均长度。
-
k1:一个可调参数,控制词频(TF)的饱和度。
-
k1是一个正数,通常取值在 1.2 到 2.0 之间。 -
k1 = 0时,完全忽略词频。 -
k1值越大,词频的贡献随着次数增加而线性增长的时间越长(饱和度来得越慢)。
-
-
b:一个可调参数,控制文档长度归一化的程度。
-
b取值在 0 到 1 之间。 -
b = 0时,完全不做文档长度归一化。 -
b = 1时,进行完全的文档长度归一化。
-
公式分母部分 是整个算法的精髓,它同时考虑了词频饱和与文档长度归一化。
第二部分:具体计算例子
现在让我们通过一个具体的场景来计算BM25得分。
场景设定
-
文档集合:我们有3个文档。
-
D1: "苹果是一种美味的水果" (长度 = 5)
-
D2: "我喜欢吃苹果和香蕉" (长度 = 6)
-
D3: "苹果公司发布了最新的智能手机产品" (长度 = 7)
-
-
平均文档长度 (avgdl):
-
avgdl = (5 + 6 + 7) / 3 = 6
-
-
文档总数 (N):
3 -
查询 (Q): "苹果"
-
参数设置:我们使用常见的默认值
k1 = 1.5,b = 0.75。
计算步骤
我们的任务是计算D1, D2, D3对于查询“苹果”的BM25得分。
第一步:计算词项“苹果”的IDF
-
n(苹果):包含“苹果”的文档数量。所有三个文档都包含“苹果”,所以n(苹果) = 3。 -
代入IDF公式:
-
IDF(苹果) = log(1 + (3 - 3 + 0.5) / (3 + 0.5)) -
= log(1 + (0.5) / (3.5)) -
= log(1 + 0.1429) -
= log(1.1429) ≈ 0.1335(这里使用自然对数)
-
第二步:为每个文档计算TF部分的分值
公式为: ( TF * (k1 + 1) ) / ( TF + k1 * (1 - b + b * (|D|/avgdl)) )
-
文档 D1 (|D|=5):
-
TF(苹果, D1) = 1(“苹果”出现1次) -
分母中的长度因子:
1 - b + b * (|D|/avgdl) = 1 - 0.75 + 0.75 * (5/6) = 0.25 + 0.75 * 0.833 = 0.25 + 0.625 = 0.875 -
TF部分分值 =
(1 * (1.5 + 1)) / (1 + 1.5 * 0.875) = (2.5) / (1 + 1.3125) = 2.5 / 2.3125 ≈ 1.0811
-
-
文档 D2 (|D|=6):
-
TF(苹果, D2) = 1 -
长度因子:
1 - 0.75 + 0.75 * (6/6) = 0.25 + 0.75 * 1 = 1.0 -
TF部分分值 =
(1 * 2.5) / (1 + 1.5 * 1.0) = 2.5 / (1 + 1.5) = 2.5 / 2.5 = 1.0
-
-
文档 D3 (|D|=7):
-
TF(苹果, D3) = 1 -
长度因子:
1 - 0.75 + 0.75 * (7/6) = 0.25 + 0.75 * 1.1667 ≈ 0.25 + 0.875 = 1.125 -
TF部分分值 =
(1 * 2.5) / (1 + 1.5 * 1.125) = 2.5 / (1 + 1.6875) = 2.5 / 2.6875 ≈ 0.9302
-
第三步:计算每个文档的最终BM25得分
Score(D, Q) = IDF(qi) * TF部分分值
-
Score(D1) = 0.1335 * 1.0811 ≈ 0.144
-
Score(D2) = 0.1335 * 1.0 ≈ 0.134
-
Score(D3) = 0.1335 * 0.9302 ≈ 0.124
结果分析
根据BM25得分,对于查询“苹果”,相关性排序为:
D1 (0.144) > D2 (0.134) > D3 (0.124)
-
为什么D1排名最高? 因为D1是最短的文档,并且包含了“苹果”。在BM25看来,在较短的文档中匹配到查询词,其“信息密度”更高,因此更相关。
-
为什么D3排名最低? 因为D3是最长的文档,受到了文档长度归一化的惩罚。BM25认为,在长文档中匹配到一个词,其重要性可能不如在短文档中匹配到。
代码实现
import math
def calculate_bm25(documents, query, k1=1.5, b=0.75):
"""
计算BM25得分
参数:
documents: 文档列表,每个文档是一个字符串
query: 查询字符串
k1, b: BM25参数
返回:
包含每个文档得分的字典
"""
# 预处理:中文分词(这里简单按空格分割,实际应用中需要使用中文分词工具)
doc_terms = [doc.split() for doc in documents]
query_terms = query.split()
# 基本统计信息
N = len(documents) # 文档总数
doc_lengths = [len(terms) for terms in doc_terms] # 每个文档的长度
avgdl = sum(doc_lengths) / N # 平均文档长度
print(f"文档总数 N = {N}")
print(f"文档长度: {doc_lengths}")
print(f"平均文档长度 avgdl = {avgdl:.2f}")
print(f"参数: k1 = {k1}, b = {b}")
print("-" * 50)
# 计算每个查询词的IDF
idf_scores = {}
for term in query_terms:
# 计算包含该词的文档数量
n_qi = sum(1 for doc_term in doc_terms if term in doc_term)
# 计算IDF
idf = math.log(1 + (N - n_qi + 0.5) / (n_qi + 0.5))
idf_scores[term] = idf
print(f"词项 '{term}' 的统计:")
print(f" 包含该词的文档数 n({term}) = {n_qi}")
print(f" IDF({term}) = log(1 + ({N} - {n_qi} + 0.5) / ({n_qi} + 0.5)) = {idf:.4f}")
print("-" * 50)
# 计算每个文档的BM25得分
scores = {}
for i, (doc, terms, doc_len) in enumerate(zip(documents, doc_terms, doc_lengths), 1):
doc_score = 0
print(f"计算文档 D{i} 的得分:")
print(f" 文档内容: '{doc}'")
print(f" 文档长度 |D{i}| = {doc_len}")
for term in query_terms:
if term in terms:
# 计算词频
tf = terms.count(term)
# 计算长度归一化因子
length_normalization = 1 - b + b * (doc_len / avgdl)
# 计算TF部分
tf_numerator = tf * (k1 + 1)
tf_denominator = tf + k1 * length_normalization
tf_component = tf_numerator / tf_denominator
# 该词项的贡献
term_contribution = idf_scores[term] * tf_component
doc_score += term_contribution
print(f" 词项 '{term}':")
print(f" TF = {tf}")
print(f" 长度归一化因子 = 1 - {b} + {b} * ({doc_len}/{avgdl:.2f}) = {length_normalization:.4f}")
print(f" TF部分 = ({tf} * ({k1}+1)) / ({tf} + {k1} * {length_normalization:.4f}) = {tf_component:.4f}")
print(f" 贡献值 = {idf_scores[term]:.4f} * {tf_component:.4f} = {term_contribution:.4f}")
else:
print(f" 词项 '{term}': 未出现在文档中,贡献为0")
scores[f"D{i}"] = doc_score
print(f" 文档 D{i} 总得分: {doc_score:.4f}")
print()
return scores
def main():
# 根据例子设置数据
documents = [
"苹果 是一种 美味 的 水果", # D1
"我 喜欢 吃 苹果 和 香蕉", # D2
"苹果 公司 发布了 最新 的 智能手机 产品" # D3
]
query = "苹果"
print("BM25算法计算示例")
print("=" * 50)
print("文档集合:")
for i, doc in enumerate(documents, 1):
print(f"D{i}: '{doc}'")
print(f"查询: '{query}'")
print("=" * 50)
# 计算BM25得分
scores = calculate_bm25(documents, query)
print("=" * 50)
print("最终结果:")
for doc, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
print(f"{doc}: {score:.4f}")
print("\n排序结果:", " > ".join([f"{doc}({score:.3f})" for doc, score in
sorted(scores.items(), key=lambda x: x[1], reverse=True)]))
if __name__ == "__main__":
main()
输出:
BM25算法计算示例
==================================================
文档集合:
D1: '苹果 是一种 美味 的 水果'
D2: '我 喜欢 吃 苹果 和 香蕉'
D3: '苹果 公司 发布了 最新 的 智能手机 产品'
查询: '苹果'
==================================================
文档总数 N = 3
文档长度: [5, 6, 7]
平均文档长度 avgdl = 6.00
参数: k1 = 1.5, b = 0.75
--------------------------------------------------
词项 '苹果' 的统计:
包含该词的文档数 n(苹果) = 3
IDF(苹果) = log(1 + (3 - 3 + 0.5) / (3 + 0.5)) = 0.1335
--------------------------------------------------
计算文档 D1 的得分:
文档内容: '苹果 是一种 美味 的 水果'
文档长度 |D1| = 5
词项 '苹果':
TF = 1
长度归一化因子 = 1 - 0.75 + 0.75 * (5/6.00) = 0.8750
TF部分 = (1 * (1.5+1)) / (1 + 1.5 * 0.8750) = 1.0811
贡献值 = 0.1335 * 1.0811 = 0.1444
文档 D1 总得分: 0.1444
计算文档 D2 的得分:
文档内容: '我 喜欢 吃 苹果 和 香蕉'
文档长度 |D2| = 6
词项 '苹果':
TF = 1
长度归一化因子 = 1 - 0.75 + 0.75 * (6/6.00) = 1.0000
TF部分 = (1 * (1.5+1)) / (1 + 1.5 * 1.0000) = 1.0000
贡献值 = 0.1335 * 1.0000 = 0.1335
文档 D2 总得分: 0.1335
计算文档 D3 的得分:
文档内容: '苹果 公司 发布了 最新 的 智能手机 产品'
文档长度 |D3| = 7
词项 '苹果':
TF = 1
长度归一化因子 = 1 - 0.75 + 0.75 * (7/6.00) = 1.1250
TF部分 = (1 * (1.5+1)) / (1 + 1.5 * 1.1250) = 0.9302
贡献值 = 0.1335 * 0.9302 = 0.1242
文档 D3 总得分: 0.1242
==================================================
最终结果:
D1: 0.1444
D2: 0.1335
D3: 0.1242
排序结果: D1(0.144) > D2(0.134) > D3(0.124)
这个例子展示了BM25如何巧妙地平衡词频、文档频率和文档长度这三个关键因素,从而产生一个更符合信息检索直觉的相关性排序。
总结
BM25是一个强大而高效的检索算法,它比传统的TF-IDF模型更加精细和健壮。它的主要优点在于:
-
非线性TF处理:避免了高频词的过度主导。
-
智能长度归一化:有效解决了长文档在统计上的优势。
-
坚实的理论基础:源于概率检索模型。
因此,BM25被广泛应用于各种搜索引擎和检索系统中,包括Elasticsearch和Lucene等知名开源项目,至今仍是许多实际应用的基准检索算法。
2812

被折叠的 条评论
为什么被折叠?



