qinghuan
← 回到知识探索

AI 直觉 / 动手实验

把采样权重归一后,为什么平均结果反而偏了?

穷举两次抽样的四条路径,区分普通重要性估计与自归一估计,再逐项算偏差、方差和均方误差。

展开本文目录
  1. 下载完整练习包
  2. 目标分布与实际抽样分布先分清
  3. 四条抽样路径各有自己的发生概率
  4. 先按路径概率求平均,偏差才有明确含义
  5. 把方差和均方误差一起算,避免只看有无偏差
  6. 没有覆盖的状态,权重无法凭空补回来
  7. 有限样本有偏,不等于越抽越多也回不去
  8. 把六十六条路径交给第二份计算核验
  9. 报告结果时,把公式、抽样机制和证据边界写出来
  10. 记住这些要点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
  • 会用概率加权求平均
  • 理解独立有放回抽样
  • 能运行Python标准库与分数计算

你想知道目标人群的平均值,却只能从另一种分布抽样。给稀少出现的对象更大权重,可以纠正抽样机会的差异。但再把这些权重除以它们自己的总和,虽然结果像一个规范的加权平均,统计性质也会随之改变。

我们只有A和B两个状态,每次只抽两次,因此全部情况只有四种。逐条算完就能看到:普通估计的平均值正确,自归一估计有偏;与此同时,后者在这个例子的均方误差更小。有偏与不可靠不是同一个结论。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • importance_ratio.py:完整示例
  • README.md:环境、范围与独立练习
  • observed.stdout.txt:制作时的实际输出
  • verification.json:源码与产物验证记录
  • SHA256SUMS.txt:文件内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B importance_ratio.py

解压到独立练习文件夹,在含源文件的目录打开终端运行。程序会在当前目录写入教学报告;先预测结果,再查看当次输出。有限两状态分布的精确枚举,采样概率与权重均已知;不运行随机模拟,不估计真实日志中的密度比。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

目标分布与实际抽样分布先分清

目标P让A和B各占一半,函数值f(A)=0、f(B)=1,所以想求的目标均值是1/2。实际使用的抽样分布Q却让A占3/4、B占1/4。每次独立、有放回地从Q抽取,不能把观察到的B比例直接当作目标答案。

权重定义为w(x)=P(x)/Q(x)。因此A的权重是2/3,B的权重是二;B在抽样中少见,出现后贡献被放大。这里两个分布都精确已知且已经归一,没有学习密度比、估计行为概率或处理日志缺失。

普通估计把每次的w乘f相加,再除以样本数n;自归一估计使用同一分子,但分母改成这批样本的权重总和。一个分母固定,一个会随抽到的结果变化,后面所有差异都从这里开始。

四条抽样路径各有自己的发生概率

n等于二时,有序路径为AA、AB、BA、BB,概率依次是9/16、3/16、3/16、1/16。AB与BA虽然给出相同估计,却是两条不同路径;保留它们有助于检查独立抽样的乘法和总概率是否为一。

四条路径的加权函数和为0、2、2、4,权重总和为4/3、8/3、8/3、4。普通估计除以二,结果为0、1、1、2;自归一估计除以各自权重总和,结果为0、3/4、3/4、1。

普通估计在BB时得到二,超过函数值零到一的范围,这并不是程序溢出。它估计的是期望,其单次输出不必处在原值范围内。自归一结果是非负权重的加权平均,所以在分母为正时始终位于已观察函数值的范围内。

先按路径概率求平均,偏差才有明确含义

对普通估计加权:零乘9/16,加上一乘6/16,再加二乘1/16,得到1/2。自归一的平均则是3/4乘6/16,再加一乘1/16,得到11/32。相对目标1/2,它的偏差为负5/32。

不能把四个输出简单相加除以四,因为四条路径不等概率。也不能先求分子的平均,再除以分母的平均:比值的平均一般不等于两个平均的比值。分母与分子来自同一批抽样,会一起变化,并非一个可以随手换成常数的计数。

无偏说的是在固定n下,反复重做整个抽样实验后的理论平均等于目标;它没有保证某一次就接近1/2。这里直接穷举出完整分布,所以11/32是精确期望,不是随机种子或抽样次数不足造成的观察偏移。

把方差和均方误差一起算,避免只看有无偏差

方差度量估计值围绕自身平均的波动,均方误差度量它离目标均值的平方距离。主例普通估计无偏,方差与均方误差都为3/8;自归一方差为159/1024,均方误差为23/128。此处自归一的总平方误差更小。

用恒等式核对:均方误差等于方差加偏差的平方。自归一的偏差平方为25/1024,与159/1024相加得184/1024,也就是23/128。比较时不能只挑无偏这个标签,也不能只挑更小的波动而忽略整体偏移。

这一胜负依赖当前P、Q、函数和样本数。它没有证明自归一在所有问题中都更准确;换一种函数或分布后,方差与偏差可能重新分配。程序保留两个估计的完整结果,方便逐例比较,避免把某个方法的名称当作质量保证。

没有覆盖的状态,权重无法凭空补回来

程序额外尝试Q(A)=1、Q(B)=0。目标仍给B一半概率,实际抽样却永远见不到它,P(B)/Q(B)也无法计算。因此输入检查直接拒绝这组分布,不能以“本次没抽到B”为由跳过,继续宣称估计了原目标。

即便Q(B)只是很小而非零,也可能需要很多样本。对照让Q(B)=1/100,抽两次都没见B的机会是9801/10000;在这些路径上,两种估计都为零。少数出现B的路径会带很大的普通权重,令普通估计方差达到99/8。

这区分了原则上无法覆盖与有限预算下很难覆盖。对特定函数,普通估计所需的覆盖条件有时能放宽到有贡献的部分;本文为了同时比较自归一分布估计,采用更清楚的充分条件:目标有正概率的状态,Q也必须有正概率。

有限样本有偏,不等于越抽越多也回不去

在本文有限状态、有限函数值、Q充分覆盖和独立同分布抽样的条件下,样本平均的wf逐渐趋向目标均值,而样本平均的w逐渐趋向一。自归一比值也就趋向目标均值,这叫一致性。它与n等于二时仍有偏差可以同时成立。

这段结论使用大数定律的条件与比值结构,不是仅凭n等于一、二、四的几张表证明。有限几个n的结果既不能保证偏差逐步单调下降,也不能确定真实问题需要多少样本;数据相关、权重估错或缺乏可积性时必须重新检查。

自归一还有一个直接用途:若只知道与P成正比的量cP,所有权重同乘未知正数c,比值中的c会约掉。代码以共同乘三作恒等检查;这个检查只验证常数抵消,不表示我们真的求解了某个未知归一常数的模型。

把六十六条路径交给第二份计算核验

importance_ratio.py比较均衡、主例和B极少三种Q,每种取n为一、二、四,共九组、六十六条有序路径。JSON保存输入与统计摘要,CSV逐行保存路径概率、每个权重、分子、分母和两种输出。所有计算使用标准库的精确分数。

每组都检查路径总概率、普通估计均值、比值范围,以及均方误差分解。另写的文件核验程序只读取这些输出,从路径和输入重新算出概率与估计,不导入原实验函数;完整嵌入正文的源码也会在单独目录再执行。

CMU课程讲义给出两套公式与覆盖要求,Cornell作者论文第5节讨论有界范围和一致性,Agapiou等论文说明提议分布与目标差异的重要性。本文只验证上述有限分布账本,没有复现离线策略学习、神经网络训练或论文性能结果。

展开收起代码 · 小实验python · 101 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Enumerate all ordered IID draws from Q; compare two expectation estimators."""
import csv
import json
from fractions import Fraction as F
from itertools import product
from pathlib import Path

ROOT = Path(__file__).resolve().parent
TARGET = {"A": F(1, 2), "B": F(1, 2)}
VALUES = {"A": F(0), "B": F(1)}
PROPOSALS = {"balanced": {"A": F(1, 2), "B": F(1, 2)},
             "unequal": {"A": F(3, 4), "B": F(1, 4)},
             "rare_B": {"A": F(99, 100), "B": F(1, 100)}}
SIZES = (1, 2, 4)


def check_distributions(target, proposal, values):
    if set(target) != set(proposal) or set(target) != set(values):
        raise ValueError("state keys must match")
    if any(p < 0 for p in target.values()) or any(q < 0 for q in proposal.values()):
        raise ValueError("negative probability")
    if sum(target.values()) != 1 or sum(proposal.values()) != 1:
        raise ValueError("probabilities must sum to one")
    if any(target[x] > 0 and proposal[x] == 0 for x in target):
        raise ValueError("proposal misses target support")


def enumerate_case(name, proposal, n):
    check_distributions(TARGET, proposal, VALUES)
    target_mean = sum(TARGET[x] * VALUES[x] for x in TARGET)
    states = [x for x in proposal if proposal[x] > 0]
    rows = []
    for draws in product(states, repeat=n):
        probability = F(1)
        numerator, denominator = F(0), F(0)
        weights = []
        for x in draws:
            probability *= proposal[x]
            weight = TARGET[x] / proposal[x]
            numerator += weight * VALUES[x]
            denominator += weight
            weights.append(weight)
        if denominator <= 0:
            raise ValueError("self-normalized denominator is zero")
        ordinary, normalized = numerator / n, numerator / denominator
        assert 0 <= normalized <= 1
        # A common unknown positive normalization constant cancels in the ratio.
        assert normalized == (3 * numerator) / (3 * denominator)
        rows.append({"proposal": name, "n": n, "draws": "".join(draws), "probability": str(probability),
                     "weights": ";".join(map(str, weights)), "weighted_sum": str(numerator),
                     "weight_sum": str(denominator), "ordinary_estimate": str(ordinary),
                     "normalized_estimate": str(normalized)})
    assert sum(F(row["probability"]) for row in rows) == 1
    summaries = {}
    for method, field in (("ordinary", "ordinary_estimate"), ("self_normalized", "normalized_estimate")):
        mean = sum(F(row["probability"]) * F(row[field]) for row in rows)
        variance = sum(F(row["probability"]) * (F(row[field]) - mean) ** 2 for row in rows)
        mse = sum(F(row["probability"]) * (F(row[field]) - target_mean) ** 2 for row in rows)
        bias = mean - target_mean
        assert mse == variance + bias ** 2
        summaries[method] = {"mean": str(mean), "bias": str(bias), "variance": str(variance), "mse": str(mse)}
    assert F(summaries["ordinary"]["mean"]) == target_mean
    return rows, {"proposal": name, "n": n, "outcomes": len(rows), "target_mean": str(target_mean),
                  "probability_no_B": str(proposal["A"] ** n), "estimators": summaries}


def main():
    rows, summaries = [], []
    for name, proposal in PROPOSALS.items():
        for n in SIZES:
            emitted, summary = enumerate_case(name, proposal, n)
            rows.extend(emitted)
            summaries.append(summary)
    main_case = next(s for s in summaries if s["proposal"] == "unequal" and s["n"] == 2)
    assert main_case["estimators"]["ordinary"] == {"mean": "1/2", "bias": "0", "variance": "3/8", "mse": "3/8"}
    assert main_case["estimators"]["self_normalized"] == {"mean": "11/32", "bias": "-5/32", "variance": "159/1024", "mse": "23/128"}
    bad = {"A": F(1), "B": F(0)}
    try:
        check_distributions(TARGET, bad, VALUES)
    except ValueError as error:
        rejected = str(error)
    else:
        raise AssertionError("uncovered target support accepted")
    inputs = {"target_P": {k: str(v) for k, v in TARGET.items()}, "function_f": {k: str(v) for k, v in VALUES.items()},
              "proposals_Q": {name: {k: str(v) for k, v in q.items()} for name, q in PROPOSALS.items()},
              "sample_sizes": list(SIZES), "sampling": "ordered independent draws with replacement from Q"}
    report = {"scope": "Exact finite distributions; enumeration of sampling outcomes, no random simulation or learned density ratios",
              "weights": "P(x)/Q(x), P and Q normalized and exactly known",
              "cases": summaries, "sequence_rows": len(rows), "rejected_support": rejected,
              "consistency_note": "Finite-sample bias is not inconsistency; finite positive full support and IID sampling satisfy the ratio law-of-large-numbers argument"}
    (ROOT / "importance-inputs.json").write_text(json.dumps(inputs, indent=2) + "\n", encoding="utf-8")
    (ROOT / "importance-report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
    with (ROOT / "importance-sequences.csv").open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)
    print(f"PASS importance_ratio: {len(summaries)} cases, {len(rows)} ordered outcomes, missing-support proposal rejected")


if __name__ == "__main__":
    main()

怎样运行:Python 3.10+标准库;保存为importance_ratio.py,在空练习目录运行 python importance_ratio.py。不要加-O,验收使用assert。 2026-10-16 已用 Python 3.14.0 执行这份代码并通过自带检查;下方是当次输出。有限两状态分布的精确枚举,采样概率与权重均已知;不运行随机模拟,不估计真实日志中的密度比。

运行后对照结果
PASS importance_ratio: 9 cases, 66 ordered outcomes, missing-support proposal rejected

报告结果时,把公式、抽样机制和证据边界写出来

一份能复查的报告至少需要目标P、抽样Q、函数f、样本数、是否独立有放回、权重来源及分母定义。只写“已加权校正”还不足以知道你算了哪一种估计,更无法判断覆盖条件是否满足。

本文最值得保留的是四条路径的分子与分母,而非一句“归一有偏”。把有限偏差、波动、总误差和大样本一致性分别讨论,才能解释一种估计为什么可能有偏却更适合当前误差目标,也能及时发现任何加权都补不回来的缺失状态。

记住这些要点

  • 普通估计除以样本数,自归一估计除以随机权重总和。
  • 比值的期望不能一般化成两个期望的比值。
  • 有限偏差、方差、均方误差与一致性是不同问题。
  • 目标有贡献而提议分布无法抽到的部分,不能靠归一补回。

别把例子用过头

  • 把不等概率的路径等权平均。
  • 从一次输出超过函数范围就判断普通估计出错。
  • 把当前算例较小的均方误差说成普遍保证。
  • 用几个有限n的实验代替一致性证明。

自己试一小步

你能在三个状态上,独立比较两种估计的完整误差分布吗?

  1. 自行设定三个状态的目标分布、非均衡提议分布和函数值,确认所需覆盖与分母条件。
  2. 对自选小样本数穷举有序抽样路径,导出概率、权重、分子、分母和两种估计。
  3. 独立计算两种估计的期望、偏差、方差和均方误差,解释本组比较支持什么结论。
  4. 再设计一个接近漏覆盖与一个真正漏覆盖的对照,分别记录有限预算影响与输入处理。

做到这些,就算完成

  • 使用自选三状态数据,不直接复用正文两状态结果。
  • 每条路径按自身概率汇总,并核验总概率和误差分解。
  • 将观察到的有限样本结果与需要额外条件的极限结论分开。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。