开始前,会这些就够了
- 会用概率加权求平均
- 理解独立有放回抽样
- 能运行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等论文说明提议分布与目标差异的重要性。本文只验证上述有限分布账本,没有复现离线策略学习、神经网络训练或论文性能结果。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""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的实验代替一致性证明。
自己试一小步
你能在三个状态上,独立比较两种估计的完整误差分布吗?
- 自行设定三个状态的目标分布、非均衡提议分布和函数值,确认所需覆盖与分母条件。
- 对自选小样本数穷举有序抽样路径,导出概率、权重、分子、分母和两种估计。
- 独立计算两种估计的期望、偏差、方差和均方误差,解释本组比较支持什么结论。
- 再设计一个接近漏覆盖与一个真正漏覆盖的对照,分别记录有限预算影响与输入处理。
做到这些,就算完成
- 使用自选三状态数据,不直接复用正文两状态结果。
- 每条路径按自身概率汇总,并核验总概率和误差分解。
- 将观察到的有限样本结果与需要额外条件的极限结论分开。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- 10-708 Lecture 13:Importance Sampling (新标签页)↗
普通与自归一估计公式、支持覆盖及有限样本偏差。
- The Self-Normalized Estimator for Counterfactual Learning (新标签页)↗
第5节比值估计、值域、有偏但一致,以及独立同分布与覆盖条件。
- Importance Sampling: Intrinsic Dimension and Computational Cost (新标签页)↗
目标相对提议分布的覆盖要求,以及权重二阶矩与计算成本的联系。