从判断列表到训练好的 Learning to Rank( LTR )模型

作者:来自 Elastic Jeffrey Rengifo

学习如何将判断列表转换为 Learning To Rank( LTR )的训练数据,设计有效特征,并解读模型学到了什么。

Elasticsearch 提供了许多新功能,帮助你为你的使用场景构建最佳搜索解决方案。通过我们关于构建现代 Search AI 体验的实操 webinar,学习如何将这些功能付诸实践。你还可以立即开始免费的 cloud 试用,或在本地机器上试用 Elastic。


在 "使用判断列表评估搜索查询相关性" 一文中,我们构建了判断列表,并使用 _rank_eval API 来衡量搜索质量。尽管这种方法为我们提供了一种客观评估变更的方式,但提升相关性仍然需要手动进行查询调优。

如果判断列表回答的是 "How good is my ranking? ",那么 Learning To Rank( LTR )回答的是 "How do I systematically make it better?"

在本文中,我们迈出下一步:使用这些判断列表,通过 XGBoostElandElasticsearch 训练一个 LTR 模型。我们将重点理解这一过程,而不是具体实现细节。完整代码请参考配套 notebook

什么是 LTR?

LTR 使用机器学习( ML )为你的搜索引擎构建排序函数。与手动调优查询权重不同,你只需提供正确排序的示例(你的判断列表),让模型学习什么使文档相关。在 Elasticsearch 中,LTR 作为第二阶段重排器,在从 Elasticsearch 检索文档之后工作:

  • 第一阶段 :标准查询( BM25、向量或混合)快速检索候选文档。
  • 第二阶段:LTR 模型使用其学习到的多个信号对顶部结果进行重新排序。

更深入的介绍,请参见在 Elasticsearch 中引入 Learning To Rank( LTR )。

从判断列表到模型的过程

判断列表告诉我们,对于给定查询,哪些文档应该排名更高。但模型无法直接从文档 ID 中学习。它需要数值信号来解释为什么某些文档具有潜在相关性。

该过程如下:

  • 从判断开始。查询-文档对带有相关性评分,例如你定义 doc1 是 "DiCaprio performance" 搜索词的良好匹配。
  • 提取特征 。对于每个查询-文档对,计算数值信号,其中一些仅与文档本身相关(例如流行度),另一些则与查询和文档的交互相关(例如 BM25 分数)。
  • 训练模型。模型学习哪些特征模式可以预测高评分。
  • 部署。将训练好的模型部署到你的 Elasticsearch 集群。
  • 查询。使用该模型对搜索结果进行重新排序。

关键洞察是,特征必须能够捕捉你的判断所衡量的内容。如果你的判断列表偏好热门惊悚电影,但你的特征只包含文本匹配分数,那么模型就无法学习什么使这些文档相关。

什么是特征?

特征是描述查询-文档对的数值。在 Elasticsearch 中,我们使用返回分数的查询来定义特征。主要有三种类型:

  • 查询-文档特征用于衡量查询与文档的匹配程度。Eland 提供了 QueryFeatureExtractor 工具来定义这些特征,它会为每个查询-文档对计算 BM25 相关性分数:

    QueryFeatureExtractor(
    feature_name="title_bm25",
    query={"match": {"title": "{{query}}"}}
    )

这会为每个文档提取相对于查询的 title 字段 BM25 分数。

  • 文档特征 是与查询无关的文档属性。你可以使用 script_scorefunction_score 来提取这些特征:

    QueryFeatureExtractor(
    feature_name="popularity",
    query={
    "script_score": {
    "query": {"exists": {"field": "popularity"}},
    "script": {"source": "return doc['popularity'].value;"}
    }
    }
    )

  • 查询特征描述查询本身,例如术语数量。这类特征较少见,但可以帮助模型处理不同类型的查询。

设计你的特征集

选择特征不是随机的。每个特征都应捕捉可能解释用户为何偏好某些文档的信号。让我们看看 LTR notebook 中的特征,并理解其设计逻辑:

Feature Type Purpose
title_bm25 Query-document 标题匹配是强相关信号。例如,标题为 Star Wars 的电影在查询 "star wars" 时应排名靠前。
actors_bm25 Query-document 一些用户按演员姓名搜索。如果搜索 "leonardo dicaprio movies",应返回主演 Leonardo DiCaprio 的影片。
title_all_terms_bm25 Query-document 标题匹配的更严格版本,要求所有查询词都必须出现。它有助于区分完全匹配与部分匹配。
actors_all_terms_bm25 Query-document 与上述严格匹配逻辑相同,但专门应用于演员。
popularity Document 当相关性相近时,用户通常更偏好知名电影。热门 Star Wars 电影应排在低预算、标题包含 "Star Wars" 的模仿片之上。

注意这里的策略:

  • 同一概念的多重信号。我们同时有 title_bm25(宽松)和 title_all_terms_bm25(严格)。宽松版本对至少有一个查询词匹配标题的文档打分,严格版本要求所有词都必须出现。对于短查询,宽松匹配可能就足够;而对于较长、具体的查询,严格匹配可能更重要。模型可以学习何时依赖哪种匹配。
  • 文本特征加上质量特征。仅靠文本匹配可能返回包含正确词语但无关的文档。popularity 特征允许模型在文本分数相近时提升知名、高质量内容。
  • 覆盖不同查询类型。有些查询针对标题(如 "star wars"),有些针对演员(如 "dicaprio movies")。为两者提供特征意味着模型可以处理多样化搜索。

在设计自己的特征时,问自己:"一个人会用哪些信号来判断文档是否相关?" 这些就是你的候选特征。

构建训练数据集

一旦定义了特征,我们就为判断列表中的每个查询-文档对提取它们。结果是一个训练数据集,每行包含:

  • 查询标识符
  • 文档标识符
  • 相关性评分(来自判断列表)
  • 所有特征值

下面是一个简化示例:

`query_id` `query` `doc_id` `grade`
qid:1 star wars 11 4
qid:1 star wars 12180 3
qid:1 star wars 278427 1
qid:2 tom hanks movies 857 4
qid:2 tom hanks movies 13 3

需要注意的几点:

  • NaN 值是正常的。当查询未匹配某个字段时,该特征不会返回分数。电影 Star Wars 的 title_bm25 很高,但 actors_bm25 为空,因为查询 "star wars" 并未匹配任何演员姓名。
  • 训练期间查询会被分组。query_id 列告诉模型哪些文档需要相互比较。对于 "star wars",它会学习文档 11(评分 4)应排在文档 278427(评分 1)之前。

但重点是:模型不会记住这些具体查询。相反,它学习一般模式,例如 "title_bm25 高且 popularity 高的文档往往评分高"。当遇到新查询时,模型会应用这些学习到的模式来排序结果。

  • 特征必须能够解释评分差异。看看 qid:1:评分 4 的文档比评分 1 的文档具有更高的 title_bm25 和更高的 popularity。这些模式就是模型学习的内容。

训练 LTR 模型

在准备好训练数据集后,我们使用带有 ranking 目标的 XGBoost 模型进行训练。模型构建决策树以学习模式,例如:

  • "如果 title_bm25 > 10 且 popularity > 50,则预测高相关性。"

  • "如果 title_bm25 缺失但 actors_bm25 > 12,仍然预测中等相关性。"

实际训练过程如下:

复制代码
from xgboost import XGBRanker
from sklearn.model_selection import GroupShuffleSplit

# Create the ranker model:
ranker = XGBRanker(
    objective="rank:ndcg",
    eval_metric=["ndcg@10"],
    early_stopping_rounds=20,
)

# Shaping training and eval data in the expected format.
X = judgments_with_features[ltr_config.feature_names]
y = judgments_with_features["grade"]
groups = judgments_with_features["query_id"]

# Split the dataset in two parts respectively used for training and evaluation of the model.
group_preserving_splitter = GroupShuffleSplit(n_splits=1, train_size=0.7).split(
    X, y, groups
)
train_idx, eval_idx = next(group_preserving_splitter)

train_features, eval_features = X.loc[train_idx], X.loc[eval_idx]
train_target, eval_target = y.loc[train_idx], y.loc[eval_idx]
train_query_groups, eval_query_groups = groups.loc[train_idx], groups.loc[eval_idx]

# Training the model
ranker.fit(
    X=train_features,
    y=train_target,
    group=train_query_groups.value_counts().sort_index().values,
    eval_set=[(eval_features, eval_target)],
    eval_group=[eval_query_groups.value_counts().sort_index().values],
    verbose=True,
)

在训练过程中,模型尝试这些规则的不同组合,并衡量生成的排序与判断评分的匹配程度。它使用一个称为归一化折扣累积增益(Normalized Discounted Cumulative Gain, NDCG)的指标进行评分。NDCG 达到 1.0 表示模型的排序完全匹配你的判断。分数较低意味着一些相关文档排在了它们本应出现的位置之下。

训练过程中还使用了一种称为早停(early stopping)的技术。如果模型的分数在若干轮中不再提升,训练会自动停止。这可以防止模型过度记忆训练数据,从而影响其对新查询的泛化能力。

配套 notebook 包含完整的训练代码。

理解你的 LTR 模型学到了什么

训练完成后,XGBoost 可以显示模型最依赖的特征。你可以使用 XGBoost 内置可视化生成特征重要性图:

复制代码
from xgboost import plot_importance

plot_importance(ranker, importance_type="weight")

参数 importance_type="weight" 显示每个特征在决策树分裂中被使用的频率。下面是生成的图表:

F 分数统计每个特征在模型所有决策树中用于分裂决策的次数。值越高,说明模型越依赖该特征。

在此示例中:

  • popularity (2178):最重要的特征。模型经常使用 popularity 来区分相关与非相关文档。
  • title_bm25 (1642):第二重要特征。标题匹配在电影搜索中非常关键。
  • actors_bm25 (565):中等重要。对于包含演员的查询很有用。
  • title_all_terms_bm25 (211):偶尔有用。严格匹配对某些查询有帮助。
  • actors_all_terms_bm25 (63):很少使用。模型发现此特征预测能力较低。

该图表有助于你迭代特征集。如果某个预期重要的特征显示几乎为零的重要性,需要调查原因。可能是特征提取未按预期工作,或者该信号在你的判断数据中实际上不能预测相关性。

部署和使用 LTR 模型

训练完成后,使用 Eland 将模型上传到 Elasticsearch:

复制代码
MLModel.import_ltr_model(
    es_client=es_client,
    model=ranker,
    model_id="ltr-model-xgboost",
    ltr_model_config=ltr_config,
    es_if_exists="replace",
)

上传后,该模型可以作为重评分重检器(rescorer retriever)使用,并可与其他重检索器(retrievers)器结合,用于多阶段搜索管道:

复制代码
GET movies/_search
{
  "retriever": {
    "rescorer": {
      "rescore": {
        "window_size": 50,
        "learning_to_rank": {
          "model_id": "ltr-model-xgboost",
          "params": {
            "query": "star wars"
          }
        }
      },
      "retriever": {
        "standard": {
          "query": {
            "multi_match": {
              "fields": ["title", "overview", "actors", "director", "tags", "characters"],
              "query": "star wars"
            }
          }
        }
      }
    }
  }
}

响应(简化版):

复制代码
 "hits": {
    "total": {
      "value": 852,
      "relation": "eq"
    },
    "max_score": 25.165691,
    "hits": [
      {
        "_index": "movies",
        "_id": "11",
        "_score": 25.165691,
        "_source": {
          "title": "Star Wars"
        }
      },
      {
        "_index": "movies",
        "_id": "12180",
        "_score": 25.092865,
        "_source": {
          "title": "Star Wars: The Clone Wars"
        }
      },
      {
        "_index": "movies",
        "_id": "181812",
        "_score": 23.456198,
        "_source": {
          "title": "Star Wars: The Rise of Skywalker"
        }
      },
      {
        "_index": "movies",
        "_id": "140607",
        "_score": 23.320757,
        "_source": {
          "title": "Star Wars: The Force Awakens"
        }
      },
...

第一阶段查询使用 BM25 检索候选文档。然后 LTR 模型使用它学到的所有特征对前 50 个结果进行重新排序。

在此示例中,仅使用 multi_match 查询可能会在前几个位置返回一些相关性较低的结果,而 LTR 帮助修正了这些问题:

复制代码
{
  "hits": [
    {
      "_index": "movies",
      "_id": "11",
      "_score": 10.971989,
      "_source": {
        "title": "Star Wars"
      }
    },
    {
      "_index": "movies",
      "_id": "12180",
      "_score": 9.923633,
      "_source": {
        "title": "Star Wars: The Clone Wars"
      }
    },
    {
      "_index": "movies",
      "_id": "1022100",
      "_score": 8.9880295,
      "_source": {
        "title": "Andor: A Disney+ Day Special Look"
      }
    },
    {
      "_index": "movies",
      "_id": "278427",
      "_score": 8.845748,
      "_source": {
        "title": "Family Guy Presents: It's a Trap!"
      }
    },
    ...
  ]
}

结论

从判断列表到可用的 LTR 模型的路径包括三个关键步骤:设计捕捉相关性信号的特征、构建将这些特征与判断评分配对的训练数据集,以及训练能够学习模式的模型。

我们之前的文章成为这个过程的起点。你的评分定义了 "相关" 的含义以及如何衡量,而你的特征为模型提供了预测所需的信号。

有关完整实现及包含 9,750 部电影和 384,755 条判断记录的数据集,请参见 LTR notebook。对于高级用例,如个性化搜索,请参见使用 LTR 的个性化搜索

原文:https://www.elastic.co/search-labs/blog/learning-to-rank-models-judgment-lists

相关推荐
xiami_world2 小时前
AI生成PPT工具技术横评:Agent专家模式如何重构PPT生成工作流(6款工具实测)
人工智能·经验分享·ai·信息可视化·powerpoint
云雾J视界2 小时前
2026年AI Agent框架选型指南:OpenClaw vs LangChain vs AutoGen 深度对比
大数据·人工智能·langchain·agent·open claw
纪伊路上盛名在2 小时前
PPT汇报中方法学、框架流程图的 文生图方案1
人工智能·文生图·流程图·科研·agent
程序大视界2 小时前
用Claude Code分析Claude Code源码
人工智能·claude code
盘古信息IMS2 小时前
IMS六代精研!盘古信息擘画“中国离散制造MES + AI数智平台”新蓝图
人工智能·制造
在荒野的梦想2 小时前
LangChain4j 集成若依单体应用 | 5 大 AI 功能实战:多轮对话、流式输出、RAG 知识库
java·人工智能
学电子她就能回来吗2 小时前
liunx嵌入式基础:socket通信
linux·运维·服务器·人工智能·单片机·嵌入式硬件·学习
吴佳浩 Alben2 小时前
Claude Code 源码泄露事件深度剖析
人工智能·arcgis·语言模型·自然语言处理·npm·node.js
禁默2 小时前
自动化智能体生成+外接MCP,我用 ModelEngine Nexent 5分钟手搓了一个小红书爆款收割机
运维·人工智能·自动化