BM25算法概述

BM25,全称是 Best Matching 25,是一种用于信息检索的排序函数,用来估算一个文档与一个查询的相关性得分。它是在20世纪70年代到90年代由一批信息检索研究者(如Stephen E. Robertson和Karen Spärck Jones)开发的概率检索模型系列中的集大成者,被认为是传统词袋模型下最有效、最先进的检索算法之一。

BM25并非一个单一的公式,而是一个算法家族,但其中最经典、最常用的形式是 BM25 Okapi

1. 核心思想

BM25的核心思想基于以下三个直觉:

  1. 词频(TF)的饱和性:一个词在文档中出现的次数越多,该文档与查询的相关性可能就越高。但是,这种增长不是线性的。出现10次比出现1次的相关性高很多,但出现100次并不比出现50次的相关性高一倍。BM25通过一个可调节的参数 k1 来控制词频的饱和速度。

  2. 文档长度归一化:长文档自然更容易包含更多的查询词,但这并不意味着它就更相关。BM25引入了文档长度与平均文档长度的比值,通过参数 b 来惩罚长文档或偏袒短文档。

  3. 逆文档频率(IDF):如果一个查询词在所有文档中都出现(如“的”、“是”),那么它对区分相关文档的贡献就很小。反之,一个稀有词(如“区块链”)如果能匹配上,则具有很强的区分能力。IDF就是对这种区分能力的量化。

2. 公式解析

对于一个查询 Q,包含多个词项 q_1, q_2, ..., q_n,BM25计算文档 D 的得分公式为:

\text{Score}(D, Q) = \sum_{q_i \in Q} \left[ \text{IDF}(q_i) \cdot \frac{\text{TF}(q_i, D) \cdot (k_1 + 1)}{\text{TF}(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)} \right]

让我们来分解这个公式的各个部分:

  • Score(D, Q):文档 D 对于查询 Q 的最终相关性得分。

  • Σ:对查询 Q 中的每一个词项 q_i 进行计算,并将结果求和。

  • IDF(qi): 词项 q_i的逆文档频率。

    • 常见计算公式为:\text{IDF}(q_i) = \log \left( 1 + \frac{N - n(q_i) + 0.5}{n(q_i) + 0.5} \right)

    • 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 时,进行完全的文档长度归一化。

公式分母部分  {\text{TF}(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)} 是整个算法的精髓,它同时考虑了词频饱和与文档长度归一化。

第二部分:具体计算例子

现在让我们通过一个具体的场景来计算BM25得分。

场景设定
  • 文档集合:我们有3个文档。

    • D1: "苹果是一种美味的水果" (长度 = 5)

    • D2: "我喜欢吃苹果和香蕉" (长度 = 6)

    • D3: "苹果公司发布了最新的智能手机产品" (长度 = 7)

  • 平均文档长度 (avgdl):

    • avgdl = (5 + 6 + 7) / 3 = 6

  • 文档总数 (N)3

  • 查询 (Q): "苹果"

  • 参数设置:我们使用常见的默认值 k1 = 1.5b = 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)) )

  1. 文档 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

  2. 文档 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

  3. 文档 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等知名开源项目,至今仍是许多实际应用的基准检索算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北京地铁1号线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值