> 恒为常数的特征在归一化融合里等于不存在——不报错,还会连累依赖它的阈值
min-max 归一化把常数信号映成全零。一个本该有区分度、实际恒为常数的特征, 在加权融合里贡献恒为 0,权重写多大都没用,而且不会有任何报错。
结论先行¶
min-max 归一化把常数信号映成全零。一个本该有区分度、实际恒为常数的特征, 在加权融合里贡献恒为 0,权重写多大都没用,而且不会有任何报错。
def _normalize_signal(scores):
lo, hi = min(v for _, v in scores), max(v for _, v in scores)
if hi - lo <= 0:
return {i: 0.0 for i, _ in scores} # 静默变成死特征
...
融合之前给每个信号打一行 (min, max, 非零个数)。 常数信号要么去掉,要么修好。
为什么¶
一次实测。排序里有个信号叫"几个独立的检索策略同时找到了这篇论文",
权重 1.5,在自适应分支里还会提到 3.0——设计上是个强证据。
它恒为 1,因为去重合并的时候只合并了 sources 字段,忘了合并 found_by:
一篇被两个策略各自找到的论文,合并后策略归属只剩一边。
两处同时失效,这才是要点。 死特征的损失不止在排序里:
- 排序:归一化后全零,1.5 的权重白给,"多个策略同意"这条证据从没进过分数。
- 过滤:相关性地板上有一条保命条件是
len(found_by) >= 2——它永远不成立。 于是同时被两个策略找到的论文反而被删掉了,正是最该留下的那一类。
合并函数修好之后,30 篇结果里有 6 篇的 found_by 长度 ≥ 2。
普遍的形状:
去重/合并函数只合并作者当时想得起来的字段。 被漏掉的字段不报错,它承载的证据静默归零, 而下游可能有好几处正在依赖它——而且依赖点通常不在合并函数附近。
顺带一个容易搞混的点:常数信号的权重仍然留在分母里
(total = sum(weights)),所以它把所有论文的分数等比压低。
等比压低不改变排序,所以光看排序看不出来;
真正受害的是任何拿绝对分数比较的阈值。
怎么用¶
合并函数按字段清单写,不按记忆写。 对记录的每个字段明确写出合并语义:
| 语义 | 例子 |
|---|---|
| 取并集 | sources、found_by、标签 |
| 取逻辑或 | _pinned 这种指令标记 |
| 取最大 / 最新 | 引用数、抓取时间 |
| 胜者独占 | 题名、摘要、URL |
写不出语义的字段,说明你不知道它是干什么的——去查,别默认"胜者独占"。 "整条记录按非空字段多者胜"是最常见的错误默认值:它会把败者独有的证据全扔掉。
三条可以直接加的断言:
- 合并后的记录,证据类字段的基数不小于任一输入。
len(merged.found_by) >= max(len(a.found_by), len(b.found_by)) - 每个融合信号在一次真实运行里方差不为零,否则打警告。 这条要跑真数据,单元测试里的两三条夹具通常凑不出方差。
- 阈值型判断要在合并之后跑一遍反例:造一条"被两路各找到一次"的记录, 断言它活过了过滤器。
同一族的另一面:换一种聚合方式排序会反号,见 标量摘要必须报告它的聚合方式——换一种加权,排序可能反号;默认回退不出声,见 指标的默认回退是静默的——而且 p 值不是效应量。
出处¶
~/Tools/scholarSpyder/scout/core/dedup.py的合并分支、scout/core/ranking.py的_normalize_signal/SIGNAL_WEIGHTS/apply_relevance_floor(2026-09-07 修复)- 触发这次排查的事故:检索工具说"没有相关工作"之前,先让它找一篇你已经知道存在的论文