这是系列文章的第三篇文章。之前的文章是:
-
Elasticsearch:实用 BM25 - 第 1 部分:分片如何影响 Elasticsearch 中的相关性评分
-
Elasticsearch:实用 BM25 - 第 2 部分:BM25 算法及其变量
选择 b 和 k1
值得注意的是,当你的用户不能快速找到文档时,选择 b 和 k1 通常不是第一件事。 b = 0.75 和 k1 = 1.2 的默认值适用于大多数语料库,因此你可能对默认值没有意见。 更有可能的是,你想从以下内容开始:
- 为 bool 查询中的精确短语匹配(phrase matches)之类的事物提升或添加常量分数
- 利用同义词(synonyms)来匹配用户可能感兴趣的其他术语
- 添加模糊性(fuziness)、预输入(typeahead)、语音匹配(phonetic matching)、词干提取(stemming)和其他文本/分析组件,以帮助解决拼写错误、语言差异等问题。
- 添加或使用函数分数(function score)来衰减旧文档或地理上远离最终用户的文档的分数
Elasticsearch 如此强大的部分原因在于你可以使用这些原始的工具来创建非常强大的搜索体验。 但是假设你已经完成了所有其他工作,并且想查看 b 和 k1 的最后一英里……你如何选择它们?
已经相当深入地研究了所有数据/查询没有 “最佳” b 和 k1 值。 确实更改 b 和 k1 参数的用户通常通过评估每个增量来逐步执行此操作。 Elasticsearch 中的 Rank Eval API 可以帮助评估阶段。
在试验 b 和 k1 时,你应该首先考虑它们的边界。 我还建议你查看过去的实验,让你对你可能感兴趣的实验类型有一个粗略的了解 —— 尤其是如果你是第一次接触这种实验:
- b 需要在 0 和 1 之间。许多实验以 0.1 左右的增量测试值,大多数实验似乎表明最佳 b 在 0.3-0.9 的范围内(Lipani、Lupu、Hanbury、Aizawa(2015 年); Taylor、Zaragoza、Craswell、Robertson、Burges (2006);Trotman、Puurula、Burgess (2014);等)
- k1 通常在 0 到 3 的范围内进行评估,但没有什么可以阻止它变得更高。 许多实验都集中在 0.1 到 0.2 的增量上,大多数实验似乎表明最佳 k1 在 0.5-2.0 的范围内
对于 k1,你应该问,“我们什么时候认为一个词项可能饱和?” 对于像书籍这样的非常长的文档 —— 尤其是虚构或多主题的书籍 —— 很可能在一部作品中多次出现很多不同的术语,即使这些术语与整个作品的相关性并不高。 例如,“eye” 或 “eyes” 在一本虚构的书中可能出现数百次,即使 “eyes” 不是该书的主要主题之一。 然而,一本提到 “eyes” 一千次的书可能与眼睛有更多关系。 在这种情况下,你可能不希望术语很快饱和,因此有人建议,当文本更长且更多样化时,k1 通常应该趋向于更大的数字。 对于相反的情况,建议将 k1 设置在较低的一侧。 短新闻文章集出现几十次 “eyes” 而不与眼睛作为主题高度相关的可能性很小。
对于 b,你应该问,“我们什么时候认为文档可能很长,什么时候应该隐藏它与术语的相关性?” 高度具体的文件(如工程规范或专利)为了更具体地说明一个主题而显得冗长。 它们的长度不太可能对相关性产生不利影响,b 越小越好。 另一方面,广泛涉及多个不同主题的文档 —— 新闻文章(政治文章可能涉及经济、国际事务和某些公司)、用户评论等。 — 通常通过选择更大的 b 获益,这样与用户搜索不相关的主题(包括垃圾邮件等)将受到惩罚。
这些是一般的起点,但最终你应该测试您设置的任何参数。 这也展示了相关性如何真正紧密地绑定到同一索引中的相似文档(相似的语言、相似的一般结构等)。
Explain API
既然你了解了 BM25 算法的工作原理以及参数的工作原理,我想简要介绍一下 Elasticsearch 工具箱中最方便的工具之一,为你提供更多信息以回答不可避免地出现的 “为什么” 问题。 如果您曾经不得不回答 “为什么文档 x 的排名高于文档 y” 这个问题,Explain API 可以为你提供显着帮助。 让我们看一下 people 索引中的文档 4,这次是一个包含两个术语的查询:
GET /people/_explain/4
{
"query": {
"match": {
"title": "shane connelly"
}
}
}
这将返回有关如何针对此查询对文档 4 进行评分的大量信息:
{
"_index": "people3",
"_type": "_doc",
"_id": "4",
"matched": true,
"explanation": {
"value": 0.71437943,
"description": "sum of:",
"details": [
{
"value": 0.102611035,
"description": "weight(title:shane in 3) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.102611035,
"description": "score(doc=3,freq=1.0 = termFreq=1.0\n), product of:",
"details": [
{
"value": 0.074107975,
"description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
"details": [
{
"value": 6,
"description": "docFreq",
"details": []
},
{
"value": 6,
"description": "docCount",
"details": []
}
]
},
{
"value": 1.3846153,
"description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
"details": [
{
"value": 1,
"description": "termFreq=1.0",
"details": []
},
{
"value": 5,
"description": "parameter k1",
"details": []
},
{
"value": 1,
"description": "parameter b",
"details": []
},
{
"value": 3,
"description": "avgFieldLength",
"details": []
},
{
"value": 2,
"description": "fieldLength",
"details": []
}
]
}
]
}
]
},
{
"value": 0.61176836,
"description": "weight(title:connelly in 3) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.61176836,
"description": "score(doc=3,freq=1.0 = termFreq=1.0\n), product of:",
"details": [
{
"value": 0.44183275,
"description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
"details": [
{
"value": 4,
"description": "docFreq",
"details": []
},
{
"value": 6,
"description": "docCount",
"details": []
}
]
},
{
"value": 1.3846153,
"description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
"details": [
{
"value": 1,
"description": "termFreq=1.0",
"details": []
},
{
"value": 5,
"description": "parameter k1",
"details": []
},
{
"value": 1,
"description": "parameter b",
"details": []
},
{
"value": 3,
"description": "avgFieldLength",
"details": []
},
{
"value": 2,
"description": "fieldLength",
"details": []
}
]
}
]
}
]
}
]
}
}
我们可以看到 k1 和 b 的单独值,还有 fieldLength 和 avgFieldLength 等每个 term 得分的组成部分! 因此,根据我们的最终得分 0.71437943,我们可以看到 0.102611035 来自 “shane”,0.61176836 来自 “connelly”。 Connelly 在我们的语料库中是一个罕见得多的术语,因此它具有更高的 IDF,这似乎是得分的主要影响因素。 但我们也可以看到文档的长度(2 个词项对比平均 3 个词项)也提高了分数的 “tfNorm” 部分。 如果我们觉得这不公平,我们可能会降低 b 的值来进行补偿。 当然,对 b 或 k1 的任何更改不仅会影响此处给定的一个查询,因此如果你最终更改了这些,请确保在许多查询和许多文档中重新测试。
请注意,Explain API 是一种调试工具,并被视为调试工具。 就像在正常情况下你不会在调试模式下运行生产应用程序一样,在正常情况下,你应该在 Elasticsearch 的生产部署中关闭对 _explain 的调用。
最后的话
BM25 不是镇上唯一的评分算法! 有经典的 TF/IDF,与随机性的分歧,还有很多很多 —— 更不用说基于超链接的修饰符,比如 pagerank —— 而且你通常还可以将其中的许多组合在一起! 此外,多年来出现了核心 BM25 算法的各种排列。 例如,已经有一些学术努力通过其中一些 BM25 排列自动选择/建议/解释 k1 和 b 的最佳值。 事实上,有一些理由/证据相信至少 k1 会在逐项的基础上得到最佳优化(Lv,ChengXiang(2011))。 有了这个,很自然地会问 “为什么是 BM25?” 或者 “为什么 BM25 具有这些特定的 k1 = 1.2 和 b = 0.75 值?”
简短的回答是,在算法或选择 k1 或 b 值时似乎没有任何灵丹妙药,但 k1 = 1.2 和 b = 0.75 的 BM25 似乎在大多数情况下都能给出非常好的结果。 在“BM25 的改进和检查的语言模型”(Trotman、Puurula、Burgess (2014))中,Trotman 等人。 搜索 b = 0-1 和 k1 = 0-3,并应用了许多不同的相关算法,包括尝试自动调整 BM25 参数的算法。 我认为他们在结论中说得最好:
“这项调查检查了 9 个排名函数、2 个相关反馈方法、5 个词干提取算法和 2 个停用词列表。 它表明停用词无效,词干提取有效,相关反馈有效,并且不停止、词干提取和反馈的组合通常会导致普通排名函数的改进。 然而,没有明确的证据表明排名函数中的任何一个在系统上优于其他函数。”
所以,当我们开始这个博客时,我们应该结束:你的大部分调优工作可能最好花在使用富有表现力的 Elasticsearch 查询语言、索引/语言控制以及整合用户反馈上,所有这些都可以通过 Elasticsearch API 完成。 对于那些做了所有这些然后想深入研究的人,可以考虑改变 k1 和 b 参数。 对于那些想要走得更远的人,Elasticsearch 支持可插入的相似性算法,包括附带许多更常见的算法。 但无论你做什么,请务必测试你的更改!