检索调参与LTR工作流.md 23.4 KB

检索调参与 LTR 工作流

1. 目标

这份文档记录本仓库当前可复用的调参与分析流程,适用于:

  • 搜索相关性调优
  • rerank fusion 参数实验
  • bad case 定位
  • 后续 Learning to Rank(LTR)特征设计

2. 先看什么指标

当前主指标不是单一指标,而是一组主 scorecard:

  • NDCG@20
  • NDCG@50
  • ERR@10
  • Strong_Precision@10
  • Strong_Precision@20
  • Useful_Precision@50
  • Avg_Grade@10
  • Gain_Recall@20

实验排序时使用:

  • Primary_Metric_Score

说明:

  • Primary_Metric_Score 是上述 8 个指标的均值。
  • 其中 Avg_Grade@10 先除以 3 再参与平均。
  • 这个分数适合做实验排序,但真正决定是否采纳配置,仍然要看关键 query 和各主指标分布。

3. 调参顺序

推荐按以下顺序做,不要一上来只盯最终 rerank 分数。

  1. 确认标注覆盖

    • 先看 query 是否已有离线标签。
    • 没有标签的 query,不要拿它做正式 batch 结论。
    • 例如这次 patterned bodysuit 不在 canonical queries.txt 内,默认没有离线标签,只能做 live diagnosis。
  2. 判断问题属于哪一层

    • missing_relevant 很多:优先怀疑召回不够。
    • 召回到了但没进前 20:优先看 coarse_rank / rerank_window
    • 进了 rerank 仍排不好:再看 fusion 或 reranker 行为。
  3. 先改覆盖,再改融合

    • 如果高相关商品连候选都进不来,只改 rerank fusion 没有意义。
    • 典型动作:
      • 增大 knn_text_k / knn_text_num_candidates
      • 增大 knn_image_k / knn_image_num_candidates
      • 增大 coarse_rank.output_window
      • 增大 rerank.rerank_window
  4. 最后才微调 fusion

    • 适合调整:
      • es_bias, es_exponent
      • rerank_bias, rerank_exponent
      • text_bias, text_exponent
      • knn_image_weight, knn_bias, knn_exponent

4. 看日志的方法

debug=true 时,优先读这三块:

4.1 retrieval_plan

确认本次 query 实际用了哪套 KNN 计划:

  • text KNN 的 k
  • text KNN 的 num_candidates
  • image KNN 的 k
  • image KNN 的 num_candidates
  • 是否走了 long-query 分支

4.2 ltr_summary

看最终 top-N 里信号结构是否异常:

  • translation_match_docs
  • text_knn_docs
  • image_knn_docs
  • text_fallback_to_es_docs
  • 各类 score 平均值

经验:

  • translation_match_docs 很高且 source_score 很低,通常说明结果主要依赖翻译召回。
  • image_knn_docs 很高但精度下降,通常说明 image KNN 权重过强或候选过宽。
  • text_fallback_to_es_docs 很高,说明 named query 子分信息不稳定,后续要重点检查 query plan。

4.3 ranking_funnel.*.ltr_features

每个 doc 在各阶段都有稳定特征块,核心字段:

  • es_score
  • text_score
  • knn_score
  • rerank_score
  • source_score
  • translation_score
  • text_knn_score
  • image_knn_score
  • has_translation_match
  • has_text_knn
  • has_image_knn

最常见的判断模式:

  • source_score > 0translation_score = 0 说明主路是原文 lexical。
  • source_score = 0translation_score > 0 说明主路基本完全靠翻译。
  • rerank_score 很强但仍然上不来 说明 text_score/knn_score 或上游窗口裁剪在压制。
  • coarse_rank 排名很差、rerank_rank 没机会出现 说明问题在 coarse 阶段,不在 rerank 阶段。

5. 这次几个 key query 的经验

sock boots

问题模式:

  • lexical 会把 sockboots 分开吃,导致袜子、boot socks、cowboy boots 大量混入。
  • 高相关 boots 里不少根本没进最终候选。

调参启示:

  • 单纯增大 rerank 权重不够。
  • 先扩大候选覆盖更有效,尤其是 image/text KNN 和 rerank window。
  • image KNN 对这类 footwear query 有价值,但不能放得过猛,否则会拉入太多“像鞋但不是 sock boots”的商品。

minimalist top

问题模式:

  • 很多结果的 source_score=0,主要靠 translation 命中。
  • query 意图偏抽象,容易把“简约风”“纯色上衣”“性感短上衣”等都混进来。

调参启示:

  • 这类 query 对翻译和 broad semantic 非常敏感。
  • text_exponent 降太多会丢掉本来还不错的 lexical 排序稳定性。
  • 更稳妥的方式是保留 baseline fusion,只做小范围偏置微调。

tassel maxi skirt

问题模式:

  • 结果里会出现“maxi skirt”“tassel dress”“sequin tassel skirt”混排。
  • 放大 image KNN / KNN exponent 后,容易把视觉上像裙装或带流苏元素的商品抬太高。

调参启示:

  • 这类 query 说明 image KNN 很容易帮助,也很容易过拟合。
  • 一旦 knn_image_weightknn_exponent 同时上调过猛,整体长尾 query 容易掉分。

patterned bodysuit

问题模式:

  • 默认没有离线标签,不能直接纳入 batch 指标结论。
  • live 结果能用于观察“patterned / lace / mesh / floral”这些属性如何被 text 与 rerank 解释。

调参启示:

  • 先补标签,再讨论是否加入长期评估集。

6. 这次实验的结论

本轮实验里,已验证的现象是:

  • 扩大 KNN 候选与 rerank 窗口,能改善 sock boots 一类“高相关没召回”的问题。
  • 但如果同时明显放大 knn_image_weightknn_exponent 并显著降低 text_exponent,会让 tassel maxi skirt 一类 query 变差,并拖累整体 batch 分数。
  • 两轮全量 batch 里,激进配置都没有超过已验证 baseline。

因此当前建议是:

  • 先把 日志、特征、主指标体系 升级好。
  • 在后续真正推进配置采纳前,继续做“只扩候选、不大改 fusion”的实验。
  • 如果未来做 LTR,优先把当前 ltr_features 直接沉淀成训练样本特征,而不是继续纯手调 fusion。

7. 推荐的下次工作

  1. patterned bodysuit 建离线标签集。
  2. 单独验证“baseline fusion + 更大 KNN/窗口”的 batch 效果。
  3. debug_info.per_result[].ranking_funnel.*.ltr_features 导出训练样本。
  4. 先做轻量 LambdaMART / GBDT 排序实验,再决定是否继续手工调乘法融合。

8. LTR 项目视角下的数据闭环

如果把后续工作从“手工调 fusion”升级为“正式 LTR 项目”,建议把整个流程理解成一条固定的数据闭环,而不是一次性的离线实验。

核心链路如下:

  1. 在线检索服务产生日志特征

    • searcher.pydebug_info 中输出:
      • retrieval_plan
      • ltr_summary
      • per_result
      • ranking_funnel
    • rerank_client.py_build_ltr_feature_block 中把各阶段稳定特征整理成统一结构。
  2. 评估框架持久化离线标签

    • 标注缓存保存在 artifacts/search_evaluation/search_eval.sqlite3
    • 关键表是 relevance_labels
    • 标签由 scripts/evaluation/eval_framework/constants.py 定义,为 4 档:
      • Fully Relevant
      • Mostly Relevant
      • Weakly Relevant
      • Irrelevant
  3. 离线脚本把“日志特征 + 离线标签”拼成训练样本

    • 当前实验脚本为 scripts/evaluation/offline_ltr_fit.py
    • 日志源默认是 logs/backend_verbose.log
    • 训练粒度是 query x doc
    • 每个 query 默认取日志中 top-100 结果
  4. 用 group-aware 排序目标做离线拟合

    • 当前先用简单版 RankNet Pairwise Loss
    • 训练函数用 FM(Factorization Machine)
    • 验证方式包含:
      • query-group K-fold cross validation
      • 单独留出 10 个 query 作为 holdout test
  5. 根据离线结果判断是否能进入正式线上化

    • 不是只看训练集和全量拟合表现
    • 最关键是看:
      • cross-validation 是否稳定优于 baseline
      • holdout test 是否仍优于 baseline
      • feature importance 是否符合业务直觉
      • 是否出现明显的“记住当前样本窗口”的过拟合信号

这条闭环的意义是:之后每次改特征、改候选、改标签、改 loss,都能复用同一条训练与验证路径,而不是重新临时拼脚本。

9. 当前日志结构如何支撑 LTR

这次日志增强的价值,不是“方便人工 debug”这么简单,而是已经具备了进入 LTR 训练样本层的必要条件。

9.1 query 级信息

主要来自 debug_info.query_analysisdebug_info.retrieval_plan

  • 原始 query / rewrite query
  • 检测语言
  • translation 结果
  • query tokens
  • text KNN 是否开启
  • text KNN 的 k
  • text KNN 的 num_candidates
  • 是否走了 long-query 计划
  • image KNN 是否开启
  • image KNN 的 k
  • image KNN 的 num_candidates

这类信息本质上是 query context feature。即使某个 doc 的 ltr_features 不变,不同 query plan 下它的含义也会不同。

9.2 doc 级稳定特征

主要来自 per_result[].ltr_featuresranking_funnel.*.ltr_features

当前最核心的数值/布尔特征包括:

  • es_score
  • text_score
  • knn_score
  • rerank_score
  • fine_score
  • source_score
  • translation_score
  • text_primary_score
  • text_support_score
  • text_knn_score
  • image_knn_score
  • knn_primary_score
  • knn_support_score
  • style_boost
  • stage_score
  • has_text_match
  • has_translation_match
  • has_text_knn
  • has_image_knn
  • text_score_fallback_to_es
  • has_style_boost

这些特征的优点是:

  • 来源清晰
  • 各阶段定义稳定
  • 可以直接落盘
  • 后续加新特征时不需要破坏旧特征含义

9.3 funnel 级特征

ranking_funnel 很重要,因为它不仅告诉我们“最终分数是什么”,还告诉我们“这个 doc 是怎么一路走到最终排序的”。

例如当前可以直接抽出的 funnel 信息包括:

  • initial_rank
  • coarse_rank
  • rerank_rank
  • final_rank
  • es_score_normalized
  • coarse_score
  • fused_score
  • coarse_stage_score
  • rerank_stage_score

这类特征在离线拟合时非常强,但也最容易带来过拟合。原因是它们会携带一部分“现有排序器已经做过的决定”。

因此正式项目里要把 funnel 特征分成两类看:

  • 可长期保留的过程特征
    • 例如 coarse_rankinitial_rank
  • 容易泄漏当前排序器决策的特征
    • 例如 final_rank
    • 或者与当前 fusion 公式几乎等价的派生项

当前离线实验里,我们保留了大部分 funnel 特征,是为了先快速判断“是否存在可学信号”;正式收敛时需要再做一轮特征裁剪。

10. 样本定义:LTR 训练样本到底是什么

LTR 项目里最容易混淆的点之一,是“训练样本”到底按什么定义。

10.1 基础样本单元

基础单元不是 pair,而是:

  • 一条 query
  • 在该 query 下的一个 doc
  • 这条 doc 的 feature vector
  • 这条 doc 的 relevance label

也就是一个标准的 query-doc 样本。

10.2 为什么最终训练用的是 pair

当前采用的是 RankNet Pairwise Loss,所以优化时不是直接回归 label,而是把同一 query 下 label 不同的两个 doc 组成一个 pair。

构造规则很简单:

  • 同一 query 内
  • label_i != label_j
  • grade_i > grade_j
  • 就构造一个正负有序 pair:(doc_i, doc_j)

在当前 4 档标注下,grade 映射为:

  • Fully Relevant -> 3
  • Mostly Relevant -> 2
  • Weakly Relevant -> 1
  • Irrelevant -> 0

因此:

  • Fully Relevant 会压过其它三档
  • Mostly Relevant 会压过 Weakly RelevantIrrelevant
  • Weakly Relevant 会压过 Irrelevant

这样做的好处是:

  • 不需要先定义复杂的 gain 权重
  • 逻辑与人工排序直觉一致
  • 很适合作为第一版 baseline

10.3 为什么不用 doc 级随机切分

排序任务必须按 query 分组切分,不能把同一个 query 下的 doc 同时放到 train 和 test。

否则模型只是在“见过这个 query 的局部排序关系”的前提下做插值,泛化评估会明显偏乐观。

所以当前脚本里:

  • cross-validation 用 GroupKFold
  • 额外再留出 10 个 query 做 holdout test

这是正式项目里必须坚持的评估原则。

11. label 获取与质量边界

11.1 label 来源

当前 label 来自评估框架的离线缓存:

  • DB 路径:artifacts/search_evaluation/search_eval.sqlite3
  • 表:relevance_labels

脚本会按:

  • tenant_id
  • query_text
  • spu_id

去查 label,并拼回日志中的 query-doc 样本。

11.2 label 语义

4 档标签不是“点击率标签”,而是人工/LLM 语义相关性标签。因此它更接近:

  • 检索相关性监督
  • 召回和排序质量监督

而不是:

  • 转化率预测
  • CTR / CVR 预估

所以这套 LTR 更适合作为 semantic relevance ranker,而不是最终商业目标排序器。

11.3 label 的边界与风险

这点需要在项目初期说清楚。

当前标签有几个天然限制:

  1. 标签覆盖的是固定 query 集

    • 未标注 query 不能直接拿来做正式结论
  2. 标签覆盖的是 query 对 doc 的语义相关性

    • 不含价格、库存、点击、转化等业务偏好
  3. 标签池来自当前检索系统可见候选

    • 如果上游召回漏掉了大量相关 doc,LTR 只能在“已见候选”上学习
  4. LLM 标注存在噪声

    • 特别是 ExactHigh 的边界可能不够稳定

因此正式项目中,label 质量治理要作为单独工作项推进,而不是把模型问题和标签问题混在一起。

12. 当前离线特征工程思路

12.1 为什么要做特征工程

虽然 ltr_features 已经比较完整,但直接喂原始特征给 FM 还不够。

原因是:

  • 很多数值特征分布偏斜
  • 不同特征的量纲差异很大
  • 排序规律往往不是线性的

所以当前离线实验里做了轻量级特征展开。

12.2 当前使用的特征展开

对大部分数值特征,会扩展出:

  • raw
  • log1p
  • sqrt
  • square
  • inv = 1 / (1 + x)

比如:

  • text_score__raw
  • text_score__log1p
  • text_score__sqrt
  • text_score__square
  • text_score__inv

这类展开的目的很简单:

  • 让模型能表达“分数高一点”和“分数非常高”不是同一个效应
  • 让模型能表达某些特征的边际收益递减
  • 让一些长尾数值特征更稳定

12.3 当前额外构造的组合特征

除了单特征变换外,还增加了少量人工组合特征:

  • translation_share = translation_score / text_score
  • source_share = source_score / text_score
  • image_knn_share = image_knn_score / knn_score
  • text_knn_share = text_knn_score / knn_score
  • rerank_x_text
  • rerank_x_knn
  • rerank_x_es
  • text_minus_es
  • knn_minus_text
  • coarse_minus_rerank

这些特征本质上在回答几类问题:

  • 当前 doc 更偏 lexical 还是 translation
  • 当前 doc 更偏 text knn 还是 image knn
  • rerank 分和上游分数是否协同
  • coarse 与 rerank 是否发生明显冲突

12.4 当前特征工程的结论

这套特征工程足够支持“第一版是否有信号”的判断,但还不够适合直接进入生产。

原因是目前的强特征中,已经出现了不少明显依赖现有排序器状态的项,例如:

  • initial_rank__log
  • initial_rank__inv
  • fused_score__*
  • stage_score__*
  • rerank_stage_score__*

这些特征有助于模型快速拟合当前数据,但可能会让模型过度学习“旧排序器的决策痕迹”。

正式项目里建议将特征分桶:

  • A 类:原始可解释特征
    • text / knn / rerank / translation / style
  • B 类:轻量派生特征
    • ratio / log / sqrt / square
  • C 类:过程排序特征
    • rank / stage score / fused score

第一阶段可以保留 A+B+C 做诊断; 第二阶段应重点验证 A+B 是否足够支撑泛化。

13. 模型与 loss:为什么先选 FM + RankNet

13.1 为什么先不用复杂模型

正式 LTR 项目可以有很多路线:

  • LambdaMART / GBDT
  • DNN
  • DSSM / cross encoder 后融合
  • listwise ranking

但在当前阶段,首要目标不是追求“最强模型”,而是先回答两个问题:

  1. 当前日志特征有没有可学习信号?
  2. 这些信号能否超越手工 fusion baseline?

所以第一版需要:

  • 足够简单
  • 可解释
  • 容易离线验证
  • 便于做 feature ablation

13.2 FM 的角色

Factorization Machine 的价值在于:

  • 一阶部分可以学习单特征权重
  • 二阶部分可以学习特征交互
  • 相比显式枚举所有交叉项,更节省参数

在当前场景里,FM 很适合回答:

  • translationtext_score 是否存在稳定交互
  • rerank_scoreknn_score 是否协同
  • initial_rank 与某些语义特征是否共同决定最终排序

13.3 当前 loss 选择

当前用的是简化版 RankNet Pairwise Loss:

  • 对每个有序 pair (i, j),希望 score_i > score_j
  • softplus(-(s_i - s_j)) 作为损失

也就是:

  • s_i - s_j 很大,loss 很小
  • s_i - s_j 不够大甚至反了,loss 会增大

当前不加 deltaNDCG 权重,目的是先把流程跑通,让误差来源更容易解释。

13.4 为什么暂时不加 LambdaRank 权重

因为当前阶段更重要的是“建立稳定 baseline”:

  • pair 采样是否正确
  • query-group split 是否正确
  • 特征是否有用
  • 模型是否明显过拟合

如果一开始就加入复杂的 listwise 权重,很容易把问题混在一起,导致难以定位:

  • 是 label 问题
  • 是 feature 问题
  • 是 weighting 问题
  • 还是 train/valid split 问题

因此现在的选择是合理的:先简单,后增强。

14. 当前离线实验结果与含义

14.1 数据规模

当前一次离线实验使用:

  • 54 个 query
  • 每个 query 100 个 doc
  • 5400query-doc 样本
  • 154592 个有效 pair

标签分布大致为:

  • grade 3: 1035
  • grade 2: 1633
  • grade 1: 1191
  • grade 0: 1541

这说明数据量对“验证可学性”已经够用,但对“高置信正式定型”还不算很大。

14.2 cross-validation 结果

当前 FM 模型在 query-group cross-validation 下:

  • Primary_Metric_Score = 0.654043

当前线上 fusion baseline:

  • Primary_Metric_Score = 0.641844

这说明:

  • 特征里确实有可学习信号
  • 模型在交叉验证中可以平均超过手工 fusion

14.3 holdout test 结果

为了进一步检查泛化,额外留出 10 个 query 作为 holdout test。

holdout 上:

  • FM:0.53056
  • 当前 fusion baseline:0.5674

这说明一个很关键的事实:

  • 当前模型存在明显泛化风险
  • 虽然 cross-validation 平均提升,但在这组留出 query 上没有赢 baseline

这不是坏消息,反而是好消息,因为它告诉我们:

  • 当前项目已经进入“可以认真做泛化治理”的阶段
  • 不是完全没信号,而是信号与过拟合同时存在

14.4 如何理解这个结果

更高层地说,这说明我们已经知道:

  1. LTR 是值得继续做的

    • 因为 CV 有提升
  2. 当前特征集合还不够干净

    • 因为 holdout 掉分
  3. 当前模型还不是可上线模型

    • 因为泛化未验证通过

所以正式项目下一步不是“直接部署”,而是“收敛特征与验证体系”。

15. FM 权重解释:如何读 feature importance

当前离线脚本已经输出两类权重文件:

  • feature_importance_linear.csv
  • feature_importance_interactions.csv

15.1 一阶权重

可以理解为单特征对排序分的直接影响。

当前绝对值较高的一阶特征包括:

  • text_knn_score__square
  • knn_primary_score__square
  • has_translation_match
  • knn_score__square
  • text_support_score__square

直觉上,这说明模型目前非常关注:

  • KNN 主信号是否强
  • text 支撑项是否强
  • translation 是否参与命中

但其中 has_translation_match 权重很大且为负,也提示一个风险:

  • 模型可能在学习“当前数据中,依赖 translation 的结果往往不够好”
  • 这有可能是合理现象,也有可能只是当前样本分布偏差

15.2 二阶交互权重

FM 的核心价值在二阶交互。

当前绝对值较高的交互项包括:

  • text_score_fallback_to_es * initial_rank__log
  • text_support_score__log1p * initial_rank__log
  • text_knn_score__square * initial_rank__log
  • has_text_knn * initial_rank__log
  • translation_share * source_share

这里最值得注意的是:很多强交互都和 initial_rank__log 有关。

这通常意味着:

  • 模型高度依赖“原排序位置”作为先验
  • 它在做的事情更像“在现有排序上修修补补”
  • 而不是“真正只根据语义相关特征重建排序”

这就是当前 holdout 泛化不佳的重要嫌疑之一。

15.3 正式项目中如何用 importance

feature importance 不应该只拿来“看着开心”,而应该直接指导迭代:

  1. 如果重要特征都来自原始可解释信号

    • 说明特征方向健康
  2. 如果重要特征高度集中在 rank / stage / fused score

    • 说明模型可能在记忆旧排序器
  3. 如果高权重特征与业务直觉相反

    • 要排查:
      • label 噪声
      • query 覆盖偏差
      • 特征定义异常

16. 正式开展 LTR 项目的建议路线

16.1 第一阶段:把数据资产固定下来

目标是建立一个稳定、可反复复用的样本生产链路。

建议动作:

  1. 固定日志导出格式

    • 保证 query-leveldoc-levelfunnel-level 字段名稳定
  2. 固定样本导出脚本

    • 不只从 backend_verbose.log 临时读
    • 后续可直接产出训练 parquet / csv
  3. 固定 label join 规则

    • 严格按 tenant_id + query_text + spu_id
  4. 固定评估切分方式

    • 始终按 query 分组
    • 始终保留固定 holdout query 集

16.2 第二阶段:做 feature ablation

建议按特征族做消融:

  • 只用 A 类原始特征
  • A + B
  • A + B + C

重点回答:

  • 去掉 initial_rank 相关特征后,是否还能赢 baseline
  • 去掉 fused_score / stage_score 后,泛化是否更稳
  • translation 相关特征是否真的有效
  • image KNN 相关特征是否会引入 query 类型偏差

16.3 第三阶段:扩模型但不破坏验证纪律

只有当:

  • cross-validation 稳定提升
  • holdout test 也提升

再考虑进入下一步:

  • LambdaRank / LambdaMART
  • 更强的 GBDT 排序器
  • 加入 query-level 特征桶
  • 更细粒度 pair weighting

16.4 第四阶段:考虑线上集成方式

正式上线前,需要先决定 LTR 放在哪一层:

  • 替代当前 rerank fusion
  • 作为 coarse_rank 之后的再排序器
  • 作为最终 fused score 的一个额外因子

当前更推荐的顺序是:

  1. 先离线验证
  2. 再 shadow score
  3. 再小流量 AB

而不是直接替换线上排序。

17. 当前最推荐的下一个动作

如果要推动 LTR 项目正式开展,建议下一步按优先级做:

  1. 固定一份长期 holdout query 集,不随实验随机变化。
  2. 对当前特征做一次系统性裁剪,优先移除强依赖 initial_rankfused_scorestage_score 的项。
  3. 做 feature family ablation,确认“原始语义特征”本身是否足够支撑泛化。
  4. 给更多 query 补齐高质量离线标签,尤其是当前容易失真的风格类和属性组合类 query。
  5. 在 FM baseline 收敛后,再进入 LambdaMART / GBDT 路线。

一句话总结当前阶段:

  • 日志体系已经足够支持 LTR 样本化
  • 离线标签体系已经足够支持第一版监督训练
  • FM + RankNet 已经证明“有信号可学”
  • 但 holdout 结果说明“离正式可上线还有一段特征治理与泛化治理工作要做”