Blame view

docs/检索调参与LTR工作流.md 23.4 KB
465f90e1   tangwang   添加LTR数据收集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
  # 检索调参与 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 > 0` 且 `translation_score = 0`
    说明主路是原文 lexical。
  - `source_score = 0` 且 `translation_score > 0`
    说明主路基本完全靠翻译。
  - `rerank_score` 很强但仍然上不来
    说明 `text_score/knn_score` 或上游窗口裁剪在压制。
  - `coarse_rank` 排名很差、`rerank_rank` 没机会出现
    说明问题在 coarse 阶段,不在 rerank 阶段。
  
  ## 5. 这次几个 key query 的经验
  
  ### `sock boots`
  
  问题模式:
  
  - lexical 会把 `sock` 和 `boots` 分开吃,导致袜子、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_weight` 与 `knn_exponent` 同时上调过猛,整体长尾 query 容易掉分。
  
  ### `patterned bodysuit`
  
  问题模式:
  
  - 默认没有离线标签,不能直接纳入 batch 指标结论。
  - live 结果能用于观察“patterned / lace / mesh / floral”这些属性如何被 text 与 rerank 解释。
  
  调参启示:
  
  - 先补标签,再讨论是否加入长期评估集。
  
  ## 6. 这次实验的结论
  
  本轮实验里,**已验证**的现象是:
  
  - 扩大 KNN 候选与 rerank 窗口,能改善 `sock boots` 一类“高相关没召回”的问题。
  - 但如果同时明显放大 `knn_image_weight`、`knn_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.py` 在 `debug_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 档:
       - `Exact Match`
       - `High Relevant`
       - `Low 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_analysis` 与 `debug_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_features` 和 `ranking_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_rank`、`initial_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 映射为:
  
  - `Exact Match -> 3`
  - `High Relevant -> 2`
  - `Low Relevant -> 1`
  - `Irrelevant -> 0`
  
  因此:
  
  - `Exact Match` 会压过其它三档
  - `High Relevant` 会压过 `Low Relevant` 和 `Irrelevant`
  - `Low 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 标注存在噪声**
     - 特别是 `Exact` 和 `High` 的边界可能不够稳定
  
  因此正式项目中,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 很适合回答:
  
  - `translation` 和 `text_score` 是否存在稳定交互
  - `rerank_score` 与 `knn_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
  -`5400` 个 `query-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-level`、`doc-level`、`funnel-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_rank`、`fused_score`、`stage_score` 的项。
  3. 做 feature family ablation,确认“原始语义特征”本身是否足够支撑泛化。
  4. 给更多 query 补齐高质量离线标签,尤其是当前容易失真的风格类和属性组合类 query。
  5. 在 FM baseline 收敛后,再进入 LambdaMART / GBDT 路线。
  
  一句话总结当前阶段:
  
  - **日志体系已经足够支持 LTR 样本化**
  - **离线标签体系已经足够支持第一版监督训练**
  - **FM + RankNet 已经证明“有信号可学”**
  - **但 holdout 结果说明“离正式可上线还有一段特征治理与泛化治理工作要做”**