> For the complete documentation index, see [llms.txt](https://inwt233.gitbook.io/ai-learning/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://inwt233.gitbook.io/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-02/day-013.md).

# Day 013：BM25 为什么优于简单 TF-IDF

{% hint style="info" %}
**Day 011 的 TF-IDF 留下两个缺陷：词频线性增长，堆砌关键词就能刷分；文档长度只能靠余弦的范数粗略补偿。BM25 用两个参数分别修复这两个问题——饱和与长度归一化到底怎么工作的？为什么偏偏是这样两个参数？**
{% endhint %}

## 学习目标

完成本节后，你应该能够：

1. 写出 BM25 的完整打分公式，并解释 IDF、饱和、长度归一化三个组成部分；
2. 用极限值和半饱和点解释 $$k\_1$$ 对词频增长曲线的控制；
3. 用 $$b=0$$ 与 $$b=1$$ 两个边界解释长度归一化的行为；
4. 说出 BM25 的 IDF 变体与 Day 011 三种变体的差别；
5. 在同一语料上对比 BM25 与 TF-IDF 余弦的排名，并解释差异来自哪里；
6. 用真实输出说明堆词攻击被抑制的程度以及残余风险；
7. 说明参数 $$k\_1$$、$$b$$ 靠评价集选择，工业实现（Lucene/Elasticsearch）的默认值是什么；
8. 识别把 $$k\_1$$ 当 top-k、把分数当概率、以为 BM25 解决词汇鸿沟等误区。

{% hint style="info" %}
本节直接使用 Day 011 的四文档语料与查询，数字可以逐个对照；打分合法性的讨论见 Day 012。明天 Day 014 会把今天的打分函数组装成一个可交互的小型文档搜索器。
{% endhint %}

## 1. Day 011 留下的两个缺陷

**缺陷一：原始 TF 线性增长。** 第 11 天的实测显示，一个词重复 8 次，TF-IDF 权重精确变成 8 倍。第 3 节给过对数饱和 $$1+\ln(\mathrm{tf})$$ 的补丁，但那个"1"和底数都是经验选择，没有参数可调。

**缺陷二：长度处理粗糙。** 余弦相似度除以向量范数，只是间接补偿长度。Day 011 的反例表明：加不加 IDF、用哪个 IDF 变体，长短文档的排名都可能翻转——范数同时混合了"文档多长"和"文档里词多稀有"两种信息，无法分别控制。

BM25（Best Match 25，Okapi 检索系统在 TREC 评测中的第 25 号方案）对这两个问题各给出一个**显式参数**：$$k\_1$$ 管饱和，$$b$$ 管长度。理解 BM25，就是理解这两个参数各自封住了什么。

## 2. 完整公式与数据流

BM25 对查询 $$q$$ 与文档 $$d$$ 的相关度分数定义为：

$$
\operatorname{BM25}(q,d)
\=\sum\_{t\in q}\operatorname{idf}(t)\cdot
\frac{\operatorname{tf}(t,d)\cdot(k\_1+1)}
{\operatorname{tf}(t,d)+k\_1\left(1-b+b\cdot\dfrac{|d|}{\operatorname{avgdl}}\right)}
$$

其中：

* $$\operatorname{tf}(t,d)$$：词 $$t$$ 在文档 $$d$$ 中的出现次数（Day 011 的 TF）；
* $$|d|$$：文档长度（Token 数）；
* $$\operatorname{avgdl}$$：语料平均文档长度；
* $$k\_1\ge0$$：饱和参数，常见默认 1.2；
* $$b\in\[0,1]$$：长度归一化强度，常见默认 0.75；
* IDF 使用变体（Lucene/Elasticsearch 形式，+1 保证恒正）：

$$
\operatorname{idf}\_{\text{BM25}}(t)
\=\ln\left(\frac{N-\operatorname{df}(t)+0.5}{\operatorname{df}(t)+0.5}+1\right)
$$

Day 011 曾展示不带 +1 的概率式变体在 $$\operatorname{df}$$ 大时为负；Lucene 的 +1 让权重恒正且单调递减，是工程折中。

整条打分数据流：

```
查询 q 与文档 d
  → 对 q 中每个词 t：
      ① 查语料统计得 df(t) → idf(t)
      ② 在 d 中数 tf(t, d)
      ③ 取 |d| 与 avgdl 之比 → 长度折扣因子
      ④ 饱和项：tf 越大增长越慢，上限受 k1 控制
      ⑤ idf × 饱和项
  → 对所有查询词求和 → 排序
```

与 TF-IDF 的余弦打分相比，结构上的关键差别：**不把文档变成向量、不算夹角**，直接对命中的查询词逐个打分求和——这正是倒排索引（Day 078）擅长的计算形态。

## 3. 饱和：k1 怎么封顶

固定 $$|d|=\operatorname{avgdl}$$（长度项为 1），饱和项简化为：

$$
\frac{\operatorname{tf}\cdot(k\_1+1)}{\operatorname{tf}+k\_1}
$$

两个精确性质：

* **上限**：$$\operatorname{tf}\to\infty$$ 时趋近 $$k\_1+1$$。一个词无论重复多少次，对分数的贡献封顶在 $$\operatorname{idf}\cdot(k\_1+1)$$；
* **半饱和点**：$$\operatorname{tf}=k\_1$$ 时恰好取得上限的一半 $$(k\_1+1)/2$$。$$k\_1$$ 因此可以读作"收益减半点"：$$k\_1=1.2$$ 表示词频到 1.2 次后，再翻倍只带来不到一半的额外收益。

实测（$$k\_1=1.2$$，$$|d|=\operatorname{avgdl}$$）：

| tf | TF-IDF（线性） | BM25 饱和项 |
| -: | ---------: | -------: |
|  1 |          1 | 1.000000 |
|  2 |          2 | 1.375000 |
|  4 |          4 | 1.692308 |
|  8 |          8 | 1.913043 |
| 16 |         16 | 2.046512 |
| 32 |         32 | 2.120482 |

线性 TF-IDF 与重复次数同倍增长；BM25 在 8 次时已用掉上限 2.2 的 87%，32 次时 96%——**第 2 次出现之后的每一次，信息增量都急剧衰减**。

$$k\_1$$ 的两个极端边界：

* $$k\_1=0$$：饱和项退化为 1（只要命中），打分变成**二值命中乘 IDF**——完全不看词频；
* $$k\_1$$ 很大（如 100）：饱和项趋近 $$\operatorname{tf}$$，退回**线性 TF-IDF**。

实测（$$|d|=\operatorname{avgdl}$$）：

|  k1 |     tf=1 |     tf=8 |     tf=32 |
| --: | -------: | -------: | --------: |
|   0 | 1.000000 | 1.000000 |  1.000000 |
| 1.2 | 1.000000 | 1.913043 |  2.120482 |
| 100 | 1.000000 | 7.481481 | 24.484848 |

所以 BM25 不是"抛弃 TF-IDF"，而是把 TF-IDF 与二值命中放在同一个可调连续谱上，$$k\_1$$ 就是旋钮。

## 4. 长度归一化：b 怎么比较长短文档

分母里的长度项：

$$
k\_1\left(1-b+b\cdot\frac{|d|}{\operatorname{avgdl}}\right)
$$

把文档长度与语料平均长度之比 $$|d|/\operatorname{avgdl}$$ 折算进折扣。两个边界：

* $$b=0$$：长度项消失，长短文档同权；
* $$b=1$$：折扣完全正比于相对长度——文档比平均长一倍，分母的 $$k\_1$$ 部分就大一倍。

$$b$$ 在两者之间滑动，默认 0.75 表示"归一化，但留四分之一的缓冲"——直觉是长文档确实更难与查询"对准"，但也可能只是内容更全面，不该罚满。

实测（Day 011 语料，$$|d\_0|=6>|d\_2|=4$$，$$\operatorname{avgdl}=4.75$$，查询"自然语言 处理"）：

|    b |  doc0 得分 |  doc2 得分 |
| ---: | -------: | -------: |
|  0.0 | 1.386294 | 1.386294 |
| 0.75 | 1.251557 | 1.482023 |
|  1.0 | 1.212283 | 1.516940 |

$$b=0$$ 时两篇文档词频相同，恰好并列；长度归一化一旦介入，较短的 doc2 反超，且 $$b$$ 越大优势越明显。**并列被打破完全来自长度项**，这让"为什么短文档排前面"第一次变得可直接归因。

与余弦的对比值得写明：余弦除以**向量范数**，混合了长度和内容稀有度两种效应（Day 011 的排名反转正源于此）；BM25 除以**物理长度相对平均值之比**，两种效应解耦，各有开关。这是"BM25 优于简单 TF-IDF"的核心之一：**不是玄学地更好，而是控制变量更干净**。

## 5. Python 实现：同语料实测

```python
from collections import Counter
from math import log

corpus = [
    ["自然语言", "处理", "是", "人工智能", "的", "分支"],
    ["计算机", "视觉", "是", "人工智能", "的", "分支"],
    ["自然语言", "处理", "研究", "文本"],
    ["人工智能", "改变", "世界"],
]

N = len(corpus)
doc_lens = [len(doc) for doc in corpus]
avgdl = sum(doc_lens) / N
doc_freq = Counter(token for doc in corpus for token in set(doc))


def idf_bm25(token):
    # Lucene/Elasticsearch 使用的变体：+1 保证权重恒正。
    return log((N - doc_freq[token] + 0.5) / (doc_freq[token] + 0.5) + 1)


def bm25_score(query, doc, k1=1.2, b=0.75):
    counts = Counter(doc)
    score = 0.0
    for token in query:
        tf = counts.get(token, 0)
        if tf == 0:
            continue  # 未命中的查询词贡献为 0，稀疏跳过。
        length_ratio = len(doc) / avgdl
        saturation = tf * (k1 + 1) / (tf + k1 * (1 - b + b * length_ratio))
        score += idf_bm25(token) * saturation
    return score


print(f"N = {N}，avgdl = {avgdl}，文档长度 = {doc_lens}")
print(f"{'词':<6}{'df':>3}{'idf_bm25':>10}")
for token in sorted(doc_freq):
    print(f"{token:<6}{doc_freq[token]:>3}{idf_bm25(token):>10.6f}")

for query in (["自然语言", "处理"], ["人工智能"]):
    print(f"\nquery = {' '.join(query)}（k1=1.2, b=0.75）")
    scored = sorted(
        ((bm25_score(query, doc), i) for i, doc in enumerate(corpus)),
        reverse=True,
    )
    for score, i in scored:
        print(f"  BM25(doc{i}) = {score:.6f}")
```

输出：

```
N = 4，avgdl = 4.75，文档长度 = [6, 6, 4, 3]
词      df  idf_bm25
世界      1  1.203973
人工智能    3  0.356675
分支      2  0.693147
处理      2  0.693147
改变      1  1.203973
文本      1  1.203973
是       2  0.693147
的       2  0.693147
研究      1  1.203973
自然语言    2  0.693147
视觉      1  1.203973
计算机      1  1.203973

query = 自然语言 处理（k1=1.2, b=0.75）
  BM25(doc2) = 1.482023
  BM25(doc0) = 1.251557
  BM25(doc3) = 0.000000
  BM25(doc1) = 0.000000

query = 人工智能（k1=1.2, b=0.75）
  BM25(doc3) = 0.419972
  BM25(doc1) = 0.322009
  BM25(doc0) = 0.322009
  BM25(doc2) = 0.000000
```

### 5.1 排名解读：与 Day 011 的两次对照

* 查询"自然语言 处理"：BM25 给 doc2（1.482023）> doc0（1.251557）。Day 011 的经典 TF-IDF 余弦结论**相反**（doc0 0.621835 > doc2 0.447214），而 sklearn 平滑变体与 BM25 同序（doc2 0.619130 > doc0 0.594678）。doc0 与 doc2 命中相同，差异全部来自对文档其余部分的处理：余弦按范数罚 doc2 的稀有词，BM25 按长度罚 doc0 的多出来的两个 Token。**两种长度观不同，结论可以相反**——这不是谁对谁错，而是必须声明的建模选择。
* 查询"人工智能"：BM25 给 doc3（0.419972）> doc1 = doc0（0.322009）。三篇命中相同，长度项直接按 $$|d|$$ 排：3 < 6 = 6。Day 011 余弦则是 doc0 > doc3 > doc1（按范数间接比较）。BM25 的归因更直白：同样命中一次，越短的文档越"专指"这次命中。

### 5.2 b 与 k1 的消融实测

```python
print("b 消融（query = 自然语言 处理）：")
for b in (0.0, 0.75, 1.0):
    s0 = bm25_score(["自然语言", "处理"], corpus[0], b=b)
    s2 = bm25_score(["自然语言", "处理"], corpus[2], b=b)
    print(f"  b={b:<4} doc0={s0:.6f}  doc2={s2:.6f}")

print("\nk1 消融（|d| = avgdl）：")
print(f"{'k1':>6}{'tf=1':>10}{'tf=8':>10}{'tf=32':>10}")
for k1 in (0.0, 1.2, 100.0):
    row = [tf * (k1 + 1) / (tf + k1) if k1 > 0 else float(tf > 0)
           for tf in (1, 8, 32)]
    print(f"{k1:>6}{row[0]:>10.6f}{row[1]:>10.6f}{row[2]:>10.6f}")
```

输出：

```
b 消融（query = 自然语言 处理）：
  b=0.0  doc0=1.386294  doc2=1.386294
  b=0.75 doc0=1.251557  doc2=1.482023
  b=1.0  doc0=1.212283  doc2=1.516940

k1 消融（|d| = avgdl）：
    k1      tf=1      tf=8     tf=32
   0.0  1.000000  1.000000  1.000000
   1.2  1.000000  1.913043  2.120482
 100.0  1.000000  7.481481 24.484848
```

与第 3、4 节的推导逐项吻合：$$b$$ 只在长短文档之间搬动分数，$$k\_1$$ 只改变词频的收益曲线。

## 6. 堆词攻击：修复了多少

构造一篇把"自然语言"重复 8 次的堆词文档，与含该词一次的正常文档对比：

```python
spam_doc = ["自然语言"] * 8
normal_doc = corpus[2]  # 含一次“自然语言”的正常文档
print("堆词攻击（query = 自然语言）：")
print(f"  正常 doc2   tf=1, |d|=4  → BM25 = {bm25_score(['自然语言'], normal_doc):.6f}")
print(f"  堆词 doc    tf=8, |d|=8  → BM25 = {bm25_score(['自然语言'], spam_doc):.6f}")
```

输出：

```
堆词攻击（query = 自然语言）：
  正常 doc2   tf=1, |d|=4  → BM25 = 0.741012
  堆词 doc    tf=8, |d|=8  → BM25 = 1.242833
```

8 倍的堆砌只换来 1.242833/0.741012 ≈ 1.68 倍的分数，而且这 1.68 倍里还被长度项扣掉一截（堆词文档长度 8 超过平均值 4.75）。同样场景下线性 TF-IDF 会精确给出 8 倍——这就是第 1 节缺陷一的定量修复。

但要诚实地写明边界：**BM25 是抑制而非根除**。堆词文档仍然排到了正常文档前面。真实系统的完整防御还包括评价集监控（Day 092）、异常词频检测与内容质量信号——参数设计只负责把攻击的"性价比"从 8 倍压到 1.68 倍。

## 7. 公式从哪里来：概率检索框架一瞥

BM25 不是拍脑袋的启发式，它来自 Robertson 等人的概率检索框架：把"文档与查询相关"看作事件，用词频的**双泊松模型**（2-Poisson model）近似"有用词"与"背景词"的混合分布，从相关概率的排序推导出上述打分形式。推导细节超出本节范围，但两个结论要记住：

* 输出叫**检索状态值**（Retrieval Status Value，RSV），是排序用的序数，**不是概率、不是归一化相关度**——不能说"这篇文档 40% 相关"（打分的概率解释对照 Day 012）；
* $$k\_1$$、$$b$$ 没有理论最优值。1.2 与 0.75 来自 TREC 评测语料上的大量实证，Elasticsearch 自 5.0 起把 BM25 作为默认相似度，默认值同样是这一对。**换语料、换字段、换查询分布，最优参数可能不同，应以自己的评价集为准**（Day 014 将实践）。

## 8. 边界与实现差异

* **字段特异性**：$$|d|$$ 与 $$\operatorname{avgdl}$$ 按字段统计。标题字段短，正文字段长，各自有自己的 avgdl。Elasticsearch 为此在每个字段上存储压缩的长度 norms；对多字段（标题+正文+标签）分别打分再融合的扩展叫 BM25F。
* **未见查询词**：$$\operatorname{df}=0$$ 时，+1 变体给出 $$\ln((N+0.5)/0.5+1)$$——一个恒正的大权重，数学上有定义，但工程上通常直接丢弃词表外查询词（Day 011 的 OOV 讨论仍适用）。
* **长度单位**：$$|d|$$ 按 Token 数计。中文按词切分与按字切分、英文去不去停用词，都会同时改变 tf、$$|d|$$ 和 avgdl，参数不可跨分词方案照搬。
* **跨索引不可比**：IDF 依赖 $$N$$ 与 df，avgdl 依赖语料。同一查询在两个索引上的 BM25 分数没有可比性；分数只用于**同一查询内**的排序。
* **norms 精度**：Lucene 存储文档长度时做了有损压缩（单字节编码），极端长文档的长度因子有量化误差——读引擎源码或做精确复现时要知道这层近似。

## 9. 工程权衡与失效模式

* **参数调优**：$$k\_1$$、$$b$$ 用网格搜索在评价集（带相关性标注）上选，指标用 Recall\@K、MRR、NDCG（Day 092 展开）。无标注时不要盲调。
* **效率**：BM25 逐词打分、只碰命中文档，天然适配倒排索引：从词定位文档、沿途取 tf 与 norms。Day 078 讲索引结构，Day 014 先用朴素扫描完成实验。
* **词汇鸿沟仍在**：BM25 的所有改进都发生在"词已匹配"之后；查询"自然语言"依旧匹配不到只写"NLP"的文档。语义泛化要靠稠密检索（Day 080），两者互补，现代系统的主流是混合检索（Day 083）。
* **失效模式**：极短查询（单常见词）区分度低；极长文档在 $$b$$ 大时被过度惩罚；字段混杂（正文里嵌导航文字）会污染 avgdl——这些都要靠切片分析发现（Day 096 的错误归因）。

## 10. 常见误区

### 误区一：k1 是"取前 k 个"

$$k\_1$$ 是词频饱和速率（收益减半点），与 top-k 检索、返回条数毫无关系。

### 误区二：BM25 分数是相关度概率

它是排序序数（RSV），无界、不归一化、不可跨查询比较。"分数 8 比 4 相关两倍"是错误表述。

### 误区三：BM25 解决了词汇鸿沟

它只在词命中之后重新分配权重。同义词、缩写、错别字仍是词袋的固有盲区（Day 011）。

### 误区四：默认参数 1.2/0.75 万能

它们是 TREC 语料的实证产物。短文本场景（问答、标题检索）常用更小的 $$b$$；字段长度分布特殊时应重新调参并留证据。

### 误区五：所有 BM25 实现公式一致

IDF 变体（是否 +1）、norms 压缩、是否按字段统计都可能不同。对照两个引擎的分数前先对齐公式（Day 011 的教训同样适用）。

### 误区六：长文档一定吃亏

$$b<1$$ 时惩罚有缓冲；且长文档覆盖词更多，命中查询多词的机会更大。长度效应的正负取决于查询与文档的匹配结构，不绝对。

## 练习与面试准备

### 30 秒口述题

请在 30 秒内回答：

> BM25 相比 TF-IDF 改进了什么？k1 和 b 分别控制什么？

合格答案应包含：TF 线性改为带上限的饱和增长（$$k\_1$$ 控制饱和速度，上限 $$k\_1+1$$ 倍 IDF）；长度从余弦范数的间接补偿改为显式的相对长度归一化（$$b$$ 控制强度）；IDF 换用恒正变体；参数靠评价集选择。

### 基础题 1：手算饱和项

$$k\_1=1.2$$、$$b=0.75$$、$$|d|=\operatorname{avgdl}$$。计算 $$\operatorname{tf}=1.2$$ 与 $$\operatorname{tf}=3$$ 的饱和项，并说明前者为什么是"半饱和点"。

<details>

<summary>参考答案</summary>

长度项为 1，饱和项 $$\operatorname{tf}\cdot2.2/(\operatorname{tf}+1.2)$$。

* $$\operatorname{tf}=1.2$$：$$1.2\times2.2/2.4=1.1$$，恰为上限 2.2 的一半；
* $$\operatorname{tf}=3$$：$$3\times2.2/4.2\approx1.571429$$。

$$\operatorname{tf}=k\_1$$ 时分子分母的 $$\operatorname{tf}$$ 与 $$k\_1$$ 相等，饱和项恒等于 $$(k\_1+1)/2$$，与 $$k\_1$$ 取值无关。

</details>

### 基础题 2：两个边界退化

分别写出 $$k\_1=0$$ 与 $$b=0$$ 时 BM25 单词项的简化形式，并各举一个适用场景。

<details>

<summary>参考答案</summary>

$$k\_1=0$$：饱和项为 1，退化为二值命中乘 IDF——适合词频无意义或不可信的场景（如标签、作者字段）。$$b=0$$：长度项为 1，长短文档同权——适合长度均匀（如标题检索）或希望长文档保留覆盖优势的场景。

</details>

### 进阶题 3：解释排名翻转

Day 011 经典 TF-IDF 余弦给 doc0 > doc2，本节 BM25 给 doc2 > doc0（查询"自然语言 处理"）。两篇文档命中相同，请解释翻转的全部来源。

<details>

<summary>参考答案</summary>

TF-IDF 余弦的范数由全部非零项构成：doc2 的"研究、文本"（高 IDF）抬大其范数，被稀释到 doc0 之后。BM25 不看未命中词：IDF 只乘在命中项上，doc0 与 doc2 的命中贡献相同，唯一差别是长度项——doc0（6 Token）比 doc2（4 Token）长，被 $$b$$ 惩罚后落到后面。一个是"内容方向的范数"，一个是"物理长度的比率"。

</details>

### 进阶题 4：代码审查

下面的实现有什么问题？

```python
def bm25(query, doc, k1=1.2, b=0.75, avgdl=100):
    score = 0.0
    for t in query:
        tf = doc.count(t)
        score += math.log(N / df[t]) * tf * (k1 + 1) / (tf + k1 * b * len(doc))
```

<details>

<summary>参考答案</summary>

* `avgdl=100` 是硬编码，应从语料统计；
* 长度项写成 $$k\_1\cdot b\cdot|d|$$，漏掉 $$1-b$$ 缓冲与除以 avgdl——$$b=0$$ 时长度项消失是对的，但 $$b>0$$ 时罚得过重且量纲随 $$|d|$$ 绝对值爆炸；
* 用经典 $$\ln(N/\mathrm{df})$$，$$\mathrm{df}=N$$ 的常见词权重为 0，命中词可能整体消失；
* `df[t]` 未处理 $$\mathrm{df}=0$$ 的词表外查询词（KeyError）；
* `doc.count(t)` 在长文档上重复扫描，应先建 Counter。

</details>

### 面试追问 5：为什么 Elasticsearch 默认 1.2/0.75，你什么时候会调

默认值是 TREC 时代实证结论的延续，通用场景稳健。会调的场景：短文本字段（标题、问答对）降低 $$b$$（如 0.3–0.5）；词频信息噪声大的字段（UGC、弹幕）增大 $$k\_1$$ 让饱和更强甚至接近二值；调整必须以带标注的评价集和网格搜索为证据，不做无依据的"手感调参"。

### 面试追问 6：BM25 与稠密检索怎么选

BM25 胜在精确词匹配（专有名词、编号、代码）、零训练成本、可解释、延迟稳定；稠密检索胜在语义泛化（同义、改写、跨语言），但需要训练与向量库、对领域偏移敏感。生产主流是混合：BM25 保底召回 + 向量扩展语义，再经融合排序（Day 083 展开）。

### 项目答辩题 7：线上 BM25 排序质量差，如何系统性调参归因

答题要点：

* 先建带相关性标注的评价集，固定 Recall\@10/MRR 基线（Day 092）；
* 对 $$k\_1\in\[0,3]$$、$$b\in\[0,1]$$ 网格搜索，报告最优组合与敏感性曲线，而不是单点；
* 切片分析：质量差集中在长文档查询？短查询？特定字段？分片归因决定改参数还是改字段建模（BM25F）；
* 对照缺陷假设做消融：只调 $$b$$、只调 $$k\_1$$ 的边际收益各多少；
* 最后回归测试防止调参破坏其他场景（Day 098 的思路）。

## 延伸阅读

1. [Robertson & Zaragoza：The Probabilistic Relevance Framework: BM25 and Beyond](https://doi.org/10.1561/1500000019)\
   BM25 权威综述：概率推导、双泊松模型、参数实证与 BM25F 扩展的第一手来源。
2. [Manning, Raghavan & Schütze：Probabilistic Information Retrieval](https://nlp.stanford.edu/IR-book/html/htmledition/probabilistic-information-retrieval-1.html)\
   教科书第 11 章，从二值独立模型到 BM25 的推导路径，与本节公式互为印证。
3. [Elasticsearch：Similarity module](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html)\
   工业实现的精确约定：BM25 为默认相似度、k1=1.2、b=0.75、norms 的单字节压缩。
4. [rank\_bm25（GitHub）](https://github.com/dorianbrown/rank_bm25)\
   轻量 Python 实现，含多种 IDF 变体，适合与手写实现互相验证（Day 014 会用到）。

## 一页回顾

```
BM25(q,d) = Σ_t idf(t) · tf·(k1+1) / (tf + k1·(1-b+b·|d|/avgdl))

三块改进（对照 Day 011 TF-IDF）：
① 饱和：tf 线性 → 上限 idf·(k1+1) 的次线性
   tf = k1 时恰好半饱和
   k1=0 → 二值命中；k1→∞ → 退回线性 TF-IDF
② 长度：余弦的范数归一 → 显式相对长度 |d|/avgdl
   b=0 不归一；b=1 全归一；默认 0.75 留缓冲
③ IDF：换恒正变体 ln((N-df+0.5)/(df+0.5)+1)
   （Lucene 形式，避免概率式变体的负值）

本节实测（Day 011 语料，k1=1.2, b=0.75）：
query=自然语言 处理：doc2 1.482023 > doc0 1.251557
  与经典 TF-IDF 余弦排名相反，与平滑变体同序
  翻转来源：余弦罚 doc2 的稀有词（范数），BM25 罚 doc0 的长度
query=人工智能：doc3 0.419972 > doc0 = doc1 0.322009
  命中相同时，短文档更专指
堆词攻击：8 倍堆砌只换来约 1.68 倍分数（线性 TF-IDF 为 8 倍）
  抑制而非根除，仍需评价集监控

出身：概率检索框架（双泊松模型）
分数是 RSV 排序序数：不是概率、不跨查询/索引比较

工程：
参数 1.2/0.75 是 TREC 实证默认，调参靠带标注评价集
逐词打分天然适配倒排索引（Day 078）
字段长度按字段统计（BM25F）
词汇鸿沟仍在 → 混合检索（Day 083）
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://inwt233.gitbook.io/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-02/day-013.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
