1. 我们为什么需要"权重"?
想象你有一个工具目录,每个工具都有一个描述文本。用户输入查询"搜索网页",你希望找到最相关的工具。
最简单的想法:统计查询词在工具描述中出现的次数,出现次数越多越相关。这就是"词频(TF)"。
但仅仅看词频会有一个问题:有些词非常常见,比如"一个"、"的"、"工具"、"使用"等。它们可能在几乎所有文档中都出现很多次。如果只用词频,这些常见词会主导排序,导致结果被噪声淹没。例如查询"使用工具",所有工具的描述里都有"使用"和"工具",那么所有工具得分都很高,无法区分哪个更相关。
所以我们需要一种方法,降低那些到处都出现的词的重要性,提高那些只在少数文档中出现的词的重要性。这就是 IDF 的核心思想。
2. IDF:逆文档频率
2.1 什么是"文档频率(DF)"?
文档频率(Document Frequency, DF)指的是:包含某个词的文档数量。
例如有 10 个工具描述:
-
其中 9 个包含"工具"
-
其中 2 个包含"搜索"
-
其中 1 个包含"天气"
那么:
-
DF("工具") = 9
-
DF("搜索") = 2
-
DF("天气") = 1
2.2 什么是"逆文档频率(IDF)"?
"逆"就是取倒数(或类似操作)。一个词的 DF 越高,说明它越常见,区分能力越弱;DF 越低,说明它越稀有,区分能力越强。
因此 IDF 大致可以定义为:

-
如果 DF = 总文档数(即词出现在所有文档中),则 IDF = log(1) = 0,表示该词没有任何区分能力。
-
如果 DF 很小(比如只有 1 个文档包含),则 IDF 会很大,表示该词能有效筛选出相关文档。
2.3 IDF 的作用:给每个查询词分配一个"重要性权重"
当用户查询"搜索网页"时,我们会对查询中的每个词分别计算 IDF:
-
"搜索"可能出现在一些工具中,IDF 中等;
-
"网页"可能只出现在少数工具中,IDF 较大。
于是,如果一个工具同时包含"搜索"和"网页",它的得分会比较高,因为"网页"这个词贡献了更大的权重,帮助它从其他工具中脱颖而出。
总结:IDF 衡量一个词在语料库中的稀有程度,稀有词的匹配更有价值。
3. 词频(TF)与词频归一化
现在我们知道,查询词的重要性(IDF)不同。但光有 IDF 还不够------我们还需要考虑查询词在具体文档中出现了多少次,即词频(TF)。
3.1 为什么需要词频归一化?
直觉上,一个文档中某个词出现次数越多,它应该与该查询更相关。但这里面有两个问题:
-
饱和效应:如果一个词出现了 100 次,并不代表它比出现 50 次的相关性强一倍。比如"搜索"出现 5 次可能已经足够表明主题,出现 20 次也不会增加太多信息量。所以我们需要对词频进行"饱和"处理,避免词频无限线性增长导致过度影响。
-
文档长度差异:有些工具描述很长(比如包含大量参数说明),有些很短。长文档天然会包含更多的词,即使不相关,某个查询词也可能偶然出现多次。如果不考虑长度,长文档可能会获得不公平的高分。所以我们需要根据文档长度进行归一化。
BM25 的归一化公式就是同时解决这两个问题的经典方案。
3.2 BM25 中的词频归一化公式
在 BM25 中,词频的贡献不是简单的 tf,而是:

其中:
-
tf:查询词在该文档中的出现次数。 -
dl:文档长度(即该文档的总词数)。 -
avgdl:语料库中所有文档的平均长度。 -
k1和b:可调参数,通常k1 = 1.5,b = 0.75。
让我们拆解这个公式的含义:
3.2.1 参数 k1:控制词频饱和程度
当 tf 很小时(例如 1 或 2),这个分数接近 tf;当 tf 很大时,分数会趋近于 k1 + 1,而不是无限增长。因此 k1 决定了词频增长的"上限"。
例如 k1 = 1.5,那么最大贡献约为 2.5。即使 tf 是 100,贡献也几乎不会超过 2.5。这就避免了高频词碾压一切。
3.2.2 参数 b 和长度归一化
分母中的 k1 * (1 - b + b * dl / avgdl) 部分,会根据文档长度调整惩罚力度:
-
如果文档长度等于平均长度(
dl = avgdl),则(1 - b + b * 1) = 1,没有额外惩罚。 -
如果文档比平均长度长(
dl > avgdl),分母变大,TF 贡献被压低,避免长文档占便宜。 -
如果文档比平均长度短(
dl < avgdl),分母变小,TF 贡献相对提升,补偿短文档的劣势。
b 控制长度归一化的强度:b=0 表示完全不考虑长度,b=1 表示完全按长度比例缩放。通常 b=0.75 是经验值。
4. 完整的 BM25 得分
最终,一个查询 Q 对文档 d 的 BM25 得分是:

也就是说,对查询中的每个词:
-
计算该词的 IDF(衡量词的重要性)。
-
计算该词在当前文档中的 TF 贡献(经过饱和和长度归一化)。
-
两者相乘,再对查询中所有词求和。
5. 一个完整例子
假设我们有一个工具目录,包含 3 个工具:
| 工具名 | 描述文本(简化) |
|---|---|
| tool_a | "search web pages" |
| tool_b | "search images" |
| tool_c | "get weather forecast" |
平均文档长度:假设每个描述都是 3 个词,所以 avgdl = 3。
用户查询:"search web"
分词:"search", "web"
第一步:计算文档频率(DF)
-
"search" 出现在 tool_a, tool_b → DF=2
-
"web" 出现在 tool_a → DF=1
第二步:计算 IDF
-
总文档数 N = 3
-
IDF("search") = ln(1 + (3 - 2 + 0.5)/(2 + 0.5)) = ln(1 + 1.5/2.5) = ln(1.6) ≈ 0.47
-
IDF("web") = ln(1 + (3 - 1 + 0.5)/(1 + 0.5)) = ln(1 + 2.5/1.5) = ln(2.666) ≈ 0.98
第三步:对每个工具计算得分
对 tool_a(文档 tokens: "search", "web", "pages",长度 dl=3):
-
"search" 的 tf=1, df=2, idf=0.47
-
TF贡献 = 1*(1.5+1)/(1 + 1.5* (1-0.75+0.753/3)) = 2.5/(1 + 1.5(1)) = 2.5/2.5 = 1.0
-
贡献 = 0.47 * 1.0 = 0.47
-
-
"web" 的 tf=1, df=1, idf=0.98
-
TF贡献 = 1*(2.5)/(1 + 1.5*(1)) = 2.5/2.5 = 1.0
-
贡献 = 0.98 * 1.0 = 0.98
-
-
总分 = 0.47 + 0.98 = 1.45
对 tool_b("search", "images",dl=2,注意 dl < avgdl=3):
-
"search" 的 tf=1, idf=0.47
-
长度归一化:分母 = 1 + 1.5*(1 - 0.75 + 0.75* 2/3) = 1 + 1.5*(0.25 + 0.5) = 1 + 1.5*0.75 = 1 + 1.125 = 2.125
-
TF贡献 = 1*2.5 / 2.125 ≈ 1.176
-
贡献 = 0.47 * 1.176 ≈ 0.553
-
-
"web" 不在文档中,贡献=0
-
总分 ≈ 0.553
对 tool_c("get", "weather", "forecast",dl=3):
- "search" 和 "web" 都不出现,总分 0。
最终排序:tool_a(1.45)> tool_b(0.553)> tool_c(0)。显然 tool_a 最相关。
6. 为什么这很有效?
-
IDF 让稀有词(如"web")比常见词(如"search")更重要。
-
词频归一化 避免高频词过度贡献,同时补偿短文档。
-
整个公式简单、高效,且在实践中表现优异,是工业界常用的基线。