AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

消融、统计显著性与有效性威胁

我的学习 →

正在读取学习记录…

消融是保留其余条件,移除一个组件,观察结果怎样变化。想判断检索重排有没有贡献,可以让同一批问题分别经过“有重排”和“直接取首条候选”,再按每个问题的正确答案评分。配对指的是两种版本处理的是同一道题,每题都有可对应的两份结果。

例如查询图书退货,候选第一条是食品政策、第二条是图书政策。有重排时按商品类型选图书,移除重排时选首条,答案从正确变成错误。这一题的差值是 1 - 0 = 1。

不过六道题中四道改善、一道不变、一道变差,平均看起来提升很大,也不能立刻推断所有用户都受益。效应表示观察到的差异大小,区间表达估计的不确定性,统计显著性检查观察结果在指定零假设下是否罕见。三者要一起看。

零假设是检验时先设定的参照条件。这里假设同一道题的两条路径在零假设下可以交换标签,交换后仍按同一统计量计算。alpha是预先约定的判定阈值,例子取0.05;p值低于它时按这项规则称为显著,p值本身不表示“组件没有作用”的概率。

同一道题运行两条实际选择路径

程序的重排按文档商品字段匹配查询商品;移除版本直接返回首条候选。正确答案只在评分时使用,选择器看不到 expected 标签。

q6专门保存一份错误元数据:正确政策的商品写成misc,另一份旧政策写成book。重排因此挑错,移除版碰巧挑对。这个例子让“组件可能帮忙也可能伤害”落实为具体程序行为。

同一批六题分别经过商品重排和直接取首条两条路径,每题产生配对差值,再计算效应、区间与置换p值
图1:评分标签仅用于两条路径输出后的评分;比较以同一道题的差值为单位。

保存完整程序并运行

六条固定资料题都已预置。每题得到full(保留重排)和removed(移除重排)两个0/1结果和差值。bootstrap指自助重采样:从已有配对差值中反复抽取,估计平均效应如何波动;有放回表示抽到的差值仍留在候选池里,可以再次被抽到。程序穷举六对结果的64种符号交换,并枚举配对差值的46656种有放回重采样,不使用随机种子或外部统计包。枚举上限为6对,便于直接运行。

在 PowerShell 创建并进入文件夹:

Bash
mkdir l7-ablation
cd l7-ablation

把下面完整代码保存为该文件夹的 ablation_lab.py。数据已经写在程序中,直接保存即可运行。

Python
"""对固定资料选择器做配对消融,枚举置换与 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,再运行:

Bash
ls
python -X utf8 ablation_lab.py

程序输出:

TXT
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,确认两个文件都在当前目录,运行:

Bash
ls
python -X utf8 -m unittest -v test_ablation_lab.py
完整测试:test_ablation_lab.py
Python
"""验证配对、差值方向、精确置换和配对重采样。"""

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,你会怎样安排下一批采样?

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 要判断检索模块是否带来收益,怎样安排比较

要判断检索模块是否带来收益,怎样安排比较?

对应知识点:L7-ablation

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

效果大小、样本量和显著性各回答什么?

对应知识点:L7-ablation

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

配对差值是[1,0,-1,1],用代码计算均值。

对应知识点:L7-ablation

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

只看成功率而忽略费用和超时,会带来怎样的判断偏差?

对应知识点:L7-ablation

图片查看器
100%

文章配图 · 按 Esc 关闭