消融、统计显著性与有效性威胁
正在读取学习记录…
消融是保留其余条件,移除一个组件,观察结果怎样变化。想判断检索重排有没有贡献,可以让同一批问题分别经过“有重排”和“直接取首条候选”,再按每个问题的正确答案评分。配对指的是两种版本处理的是同一道题,每题都有可对应的两份结果。
例如查询图书退货,候选第一条是食品政策、第二条是图书政策。有重排时按商品类型选图书,移除重排时选首条,答案从正确变成错误。这一题的差值是 1 - 0 = 1。
不过六道题中四道改善、一道不变、一道变差,平均看起来提升很大,也不能立刻推断所有用户都受益。效应表示观察到的差异大小,区间表达估计的不确定性,统计显著性检查观察结果在指定零假设下是否罕见。三者要一起看。
零假设是检验时先设定的参照条件。这里假设同一道题的两条路径在零假设下可以交换标签,交换后仍按同一统计量计算。alpha是预先约定的判定阈值,例子取0.05;p值低于它时按这项规则称为显著,p值本身不表示“组件没有作用”的概率。
同一道题运行两条实际选择路径
程序的重排按文档商品字段匹配查询商品;移除版本直接返回首条候选。正确答案只在评分时使用,选择器看不到 expected 标签。
q6专门保存一份错误元数据:正确政策的商品写成misc,另一份旧政策写成book。重排因此挑错,移除版碰巧挑对。这个例子让“组件可能帮忙也可能伤害”落实为具体程序行为。
保存完整程序并运行
六条固定资料题都已预置。每题得到full(保留重排)和removed(移除重排)两个0/1结果和差值。bootstrap指自助重采样:从已有配对差值中反复抽取,估计平均效应如何波动;有放回表示抽到的差值仍留在候选池里,可以再次被抽到。程序穷举六对结果的64种符号交换,并枚举配对差值的46656种有放回重采样,不使用随机种子或外部统计包。枚举上限为6对,便于直接运行。
在 PowerShell 创建并进入文件夹:
mkdir l7-ablation
cd l7-ablation把下面完整代码保存为该文件夹的 ablation_lab.py。数据已经写在程序中,直接保存即可运行。
"""对固定资料选择器做配对消融,枚举置换与 bootstrap 重采样。"""
from itertools import product
from math import ceil
from statistics import mean
# 候选顺序模拟召回结果;正确答案标签只供评分,不传给选择器。
CASES = [
{"id": "q1", "product": "book", "expected": 30,
"docs": [("food", 0), ("book", 30)]},
{"id": "q2", "product": "book", "expected": 7,
"docs": [("food", 0), ("book", 7)]},
{"id": "q3", "product": "hotel", "expected": 1,
"docs": [("food", 0), ("hotel", 1)]},
{"id": "q4", "product": "ticket", "expected": 2,
"docs": [("food", 0), ("ticket", 2)]},
{"id": "q5", "product": "book", "expected": 14,
"docs": [("book", 14), ("food", 0)]},
# 第六条的正确文档商品元数据误写,重排会选到另一份旧政策。
{"id": "q6", "product": "book", "expected": 30,
"docs": [("misc", 30), ("book", 7)]},
]
def choose(product_name, docs, rerank):
if rerank:
ranked = sorted(docs, key=lambda doc: doc[0] == product_name, reverse=True)
return ranked[0][1]
return docs[0][1]
def paired_scores(cases):
if len({c["id"] for c in cases}) != len(cases):
raise ValueError("duplicate paired sample")
rows = []
for case in cases:
full = int(choose(case["product"], case["docs"], True) == case["expected"])
removed = int(choose(case["product"], case["docs"], False) == case["expected"])
rows.append((case["id"], full, removed, full - removed))
return rows
def validate(differences):
if not 1 <= len(differences) <= 6:
raise ValueError("enumeration supports 1 to 6 pairs")
def permutation_p(differences):
validate(differences)
observed = abs(sum(differences))
# 每一对交换 full/removed 等价于让差值乘 +1 或 -1。
values = [abs(sum(sign * diff for sign, diff in zip(signs, differences)))
for signs in product((-1, 1), repeat=len(differences))]
return sum(value >= observed for value in values) / len(values)
def bootstrap_interval(differences):
validate(differences)
n = len(differences)
# 从完整配对差值有放回抽 n 次;枚举 n^n 种索引,保留配对。
estimates = sorted(mean(differences[i] for i in indices)
for indices in product(range(n), repeat=n))
def quantile(p):
return estimates[ceil(p * len(estimates)) - 1]
return quantile(.025), quantile(.975)
def main():
rows = paired_scores(CASES)
for identifier, full, removed, diff in rows:
print(f"{identifier} full={full} removed={removed} difference={diff:+d}")
differences = [row[3] for row in rows]
low, high = bootstrap_interval(differences)
p = permutation_p(differences)
print(f"paired_n={len(rows)} full_rate={mean(r[1] for r in rows):.4f} "
f"removed_rate={mean(r[2] for r in rows):.4f}")
print(f"mean_effect={mean(differences):.4f} bootstrap_95pct=[{low:.4f}, {high:.4f}]")
print(f"two_sided_exact_p={p:.4f} alpha=0.0500 significant={p < .05}")
if __name__ == "__main__":
main()保存后列出文件,确认能看到 ablation_lab.py,再运行:
ls
python -X utf8 ablation_lab.py程序输出:
q1 full=1 removed=0 difference=+1
q2 full=1 removed=0 difference=+1
q3 full=1 removed=0 difference=+1
q4 full=1 removed=0 difference=+1
q5 full=1 removed=1 difference=+0
q6 full=0 removed=1 difference=-1
paired_n=6 full_rate=0.8333 removed_rate=0.3333
mean_effect=0.5000 bootstrap_95pct=[-0.1667, 1.0000]
two_sided_exact_p=0.3750 alpha=0.0500 significant=False逐项读懂结果
q1–q4的full为1、removed为0,各贡献+1;q5两者都正确,差为0;q6为0与1,差为−1。full共5/6=0.8333,removed共2/6=0.3333。
平均效应 Δ = Σdᵢ / n:dᵢ 是第i题的full减removed,n 是6,故 (1+1+1+1+0−1)/6=0.5。正确率的绝对差为50个百分点,不能写成“提升50%”而忽略基准分母。这些结果来自固定资料选择器,没有调用模型。
95% bootstrap区间为 [-0.1667, 1.0000]。bootstrap(自助重采样)从六个完整配对差值中有放回取六个,再求均值;程序枚举 6⁶=46656 组索引,排序后取2.5%与97.5%的nearest-rank分位。它模拟若样本构成改变,估计效应可能怎样波动。区间很宽且包含0,六题难以给出稳定总体估计。
置换检验的零假设要求在每对内部,full与removed的分配可交换;等价于每个差值可乘+1或−1。双侧统计量用 |Σdᵢ|,观测值为3。64种符号组合中24种绝对和至少为3,因此 p=24/64=0.375。这里p值与alpha=0.05比较,significant=False。
零差值q5翻符号后仍为0,其组合重复但权重仍正确。p值衡量零假设下出现至少同样极端统计量的概率。判断两版本是否足够接近,还需预先定义等价范围并采用相应分析。六题是人为预置的机制样本,独立抽样与可交换性没有通过真实实验设计建立;数字帮助理解计算,推广结论需要实际采样与实验设计。
改一处,再看对应结果
把 CASES 中q6的第一条文档商品字段从 "misc" 改成 "book",保留它的30天政策值,运行 python -X utf8 ablation_lab.py。
观察q6:稳定排序保留两条book文档的原顺序,full也会取第一条30天政策,因此差值从−1变0。平均效应变为 4/6=0.6667,置换p值变0.125,仍高于0.05。这个变化直接修复元数据,六道样本仍全部保留。完成后恢复misc并运行测试。
修复后bootstrap区间会变成 [0.3333, 1.0000],而p值仍为0.125。重采样只能重新组合现在已有的0和1差值;六题下百分位区间的总体覆盖率可能较差。这个区间与精确置换检验并非彼此反演,判断显著性仍按预先选定的检验规则,后续用独立样本确认。
让消融真正回答组件贡献
两条路径要使用同一输入、候选、模型版本、Prompt、工具数据和评分规则,只改变被消融的组件。若移除重排时同时扩大检索数量,就难以把差异归因于重排。实际模型有随机波动时,应该预先规定重复次数与配对运行方式,并记录其成本和延迟。
配对也必须匹配分析单位。同一用户连续十道相似问题不能直接当十个独立用户;可以在用户层配对聚合或采用分组重采样。bootstrap区间是基于观测样本和重采样方法的估计,尤其在样本极小或分布偏斜时要谨慎解释。SciPy bootstrap 文档区分了重采样方式与区间方法。
有效性威胁是让“这项组件带来贡献”的结论站不稳的条件:样本只包含图书问题,未覆盖其他人群;标签引用旧政策;用同一测试集反复调重排;只报告改善题、漏掉失败题;一次尝试很多组件后只选p最小的一项。可以预先冻结样本与主要指标,保留全部配对结果,用独立保留集确认,并对多重比较作相应控制。
业务决策还需实际意义。例如正确率提高1个百分点,但延迟和费用翻倍,可能不满足业务目标。预先设定最小可接受效应与成本约束,能避免“p值好看就上线”的冲动。
保存并运行测试
7 项测试检查两条实际路径的配对、元数据反例、精确p值、双侧方向对称、恒定差值区间、区间跨0及重复样本/枚举上限。
把下面代码保存为同一文件夹的 test_ablation_lab.py,确认两个文件都在当前目录,运行:
ls
python -X utf8 -m unittest -v test_ablation_lab.py完整测试:test_ablation_lab.py
"""验证配对、差值方向、精确置换和配对重采样。"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import ablation_lab as lab
class AblationTests(unittest.TestCase):
def test_component_is_removed_on_same_six_cases(self):
rows = lab.paired_scores(lab.CASES)
self.assertEqual([r[0] for r in rows], [f"q{i}" for i in range(1, 7)])
self.assertEqual([r[3] for r in rows], [1, 1, 1, 1, 0, -1])
self.assertAlmostEqual(lab.mean(r[3] for r in rows), .5)
def test_metadata_error_can_hurt_full_system(self):
row = lab.paired_scores(lab.CASES)[-1]
self.assertEqual(row[1:], (0, 1, -1))
def test_exact_two_sided_probability(self):
self.assertEqual(lab.permutation_p([1, 1, 1, 1, 0, -1]), .375)
self.assertEqual(lab.permutation_p([0, 0]), 1)
def test_flipping_effect_direction_preserves_two_sided_p(self):
differences = [1, 1, 0, -1]
self.assertEqual(lab.permutation_p(differences),
lab.permutation_p([-x for x in differences]))
def test_identical_differences_have_point_interval(self):
self.assertEqual(lab.bootstrap_interval([1, 1, 1]), (1, 1))
def test_interval_contains_zero_for_fixture(self):
low, high = lab.bootstrap_interval([1, 1, 1, 1, 0, -1])
self.assertLessEqual(low, 0)
self.assertGreaterEqual(high, 0)
def test_duplicate_sample_and_large_enumeration_are_rejected(self):
with self.assertRaises(ValueError):
lab.paired_scores([lab.CASES[0]] * 2)
for differences in ([], [1] * 7):
with self.assertRaises(ValueError):
lab.permutation_p(differences)
if __name__ == "__main__":
unittest.main()继续查统计定义
读 SciPy permutation_test 的 samples 配对交换,对应每对交换标签的64种组合。再读 bootstrap 的 paired 参数与 percentile 方法,核对为何要整对重采样。
课后练习展开习题收起习题
如果full用图书题、removed用食品题,还能称为配对消融吗?如果平均提升很大但区间跨0,你会怎样安排下一批采样?
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 要判断检索模块是否带来收益,怎样安排比较
要判断检索模块是否带来收益,怎样安排比较?
对应知识点:L7-ablation
简答题 · 解释机制
效果大小、样本量和显著性各回答什么?
对应知识点:L7-ablation
代码题 · 核对给定数据
配对差值是[1,0,-1,1],用代码计算均值。
对应知识点:L7-ablation
场景题 · 分析具体变化
只看成功率而忽略费用和超时,会带来怎样的判断偏差?
对应知识点:L7-ablation
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。