开始前,会这些就够了
- 知道概率之和为 1,能读加权平均
- 先读 熵、交叉熵与 KL
- Python 3.10+;实验只用标准库
两组分类器最后都给出“类别 A 与 B 各占一半”。第一组里,一个成员把 A 的概率报成 99%,另一个把 B 报成 99%;第二组里的两个成员都报各一半。平均结果相同,成员之间发生的事却不同。
本篇固定几组小概率表,分别量出平均预测的分散程度、成员各自的分散程度,以及二者差值。它不训练模型,也不把手工指定的模型集合当成真实贝叶斯后验。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。手工指定有限类别概率与权重;核验预测熵、成员平均熵、加权 KL 和 Decimal 联合表互信息;未训练、推断后验或评估真实模型。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
平均概率相同,成员是在犹豫,还是互相反对?
正在准备实验控件…
两个手工给定的二分类概率分布,单位为 bit;成员编号按所选权重抽取,再按该成员分布抽取类别。差值表示这份有限联合分布中的成员分歧,不证明真实模型正确、校准或拥有贝叶斯后验;没有训练或调用模型。
先看两张平均后完全一样的表
第一组的两个成员分别输出 [0.99,0.01] 和 [0.01,0.99],权重都为 0.5。它们各自很偏向一个类别,但偏向方向相反。第二组的两个成员都输出 [0.5,0.5],彼此完全一致,只是每个都没有偏向哪一类。
两组平均概率都是 [0.5,0.5]。如果只保存这个平均向量,之后无法从中恢复成员是否互相反对;信息在合并时已经丢失。这是一个明确的信息丢失例子,不是在比较两组真实模型谁更可靠。
本篇把熵的对数底固定为 2,单位是 bit。二分类均匀分布的熵为 1,因此两组的预测熵都是 1。若只看这一个数,它们仍然无法区分。
熵在平均之前算,还是之后算?
把第 m 个成员的类别概率记为 p_m,权重记为 w_m;权重非负且和为 1。平均预测是 q_k=Σ_m w_m p_mk。预测熵计算 H(q),而成员平均熵计算 Σ_m w_m H(p_m)。两者分别先做不同的运算。
熵 H(p)=−Σ_k p_k log₂(p_k) 是非线性函数,不能把平均号随意移到它里面。第一组每个成员的熵约为 0.080793,平均后仍是这个数;第二组每个成员的熵都是 1,平均后也是 1。
于是相同的预测熵 1,可以拆出不同的成员平均熵。这里“成员自身很确定”只描述它输出的概率有多集中,不意味着它知道真实答案;下面会单独给出全部成员一起猜错的对照。
差值可以量出这组概率表里的分歧
定义差值 D=H(q)−Σ_m w_m H(p_m)。第一组得到约 0.919207 bit,第二组得到 0。将熵展开并合并相同类别项,可以得到 D=Σ_m w_m KL(p_m∥q)。它是在这组明确指定的概率表中,成员偏离共同平均分布的加权程度。
还可以定义一个完全写得出的随机实验:先按 w 选择成员编号 M,再按那个成员的 p_m 选择类别 Y。联合概率就是 P(M=m,Y=k)=w_m p_mk。在这个有限模型里,上述差值也等于互信息 I(M;Y)。知道成员编号能改变多少关于类别的概率信息,正是这里的含义。
这是对指定联合分布的恒等关系。M 是这个实验里的成员编号,不自动等于真实模型参数的后验随机变量。脚本分别计算熵差、加权 KL 和联合表互信息,让三条计算路径互相核对。
分歧为零,仍然可以一起答错
第三组两个成员都给出 [0.99,0.01]。它们的平均预测熵约为 0.080793,成员平均熵也一样,差值为 0。现在另行指定这个演示输入的标签为类别 1,而两者选中的最大概率类别都是类别 0:它们一致,但都不符合这个标签。
因此,低分歧既可能表示一致的信息,也可能表示共同的错误;单看这份概率表不能判断是哪一种。预测熵低也只是概率集中,不是正确率证明。现实中的校准与准确率需要独立、带可靠标签的数据来检查。
同样,高分歧并不自动证明“这里缺少知识,补一个标签一定有用”。要把差值解释成特定来源的不确定性,必须说明模型集合、权重和数据生成假设。任意挑两份意见不同的模型,不能直接完成这种归因。
权重、零概率与成员数量都属于定义
若两个成员分别给 [1,0] 和 [0,1],但权重改为 [0.8,0.2],平均预测变成 [0.8,0.2],成员熵仍全为 0,差值约为 0.721928 bit。不能只数有几个成员反对,就忽略它们在混合中占多少权重。
只有一个有权重的成员时,差值应为 0;权重为零的成员不贡献联合概率;所有成员都不给某个类别概率时,该类别也不参与对数计算。代码显式跳过零质量项,没有偷偷加 epsilon 改写分布。
对于这份有限联合分布,互信息不会超过成员编号的熵,也不会超过类别数的 log₂。程序检查这些上界,但保留浮点容差,不靠大范围截成零来掩盖错误。若正的权重乘概率已经在本实现中下溢为零,程序明确拒绝这类超出浮点计算范围的输入。
运行八组案例,再看两条独立参考
保存并运行 demo.py,无需安装第三方包。2026-10-23 已在 Windows、Python 3.14.0 实测,79 项检查通过。代码按 Python 3.10 语法解析过,但没有另行启动 Python 3.10 解释器。
主计算使用标准浮点数。第一条参考直接求成员到混合的加权 KL;第二条参考以 Decimal 的 70 位精度构造联合表,先独立求行、列边缘概率,再按互信息定义求和,不调用主程序的 entropy 或 summarize。
八组案例覆盖相同均值不同分歧、一致但指定错误、非均匀权重、三分类、单成员、零权重和结构性零概率;另拒绝空输入、不同类别数量、非归一概率、非有限值、布尔值与不可表示的小联合质量。report.json 保存全部输入和参考值,这些检查不构成真实集成质量评估。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Finite categorical mixtures; no model training or posterior inference."""
from copy import deepcopy
from decimal import Decimal, localcontext
import json
import math
from pathlib import Path
TOL = 1e-12
checks = []
def check(name, condition):
if not condition:
raise AssertionError(name)
checks.append(name)
def near(left, right):
return math.isclose(left, right, rel_tol=0.0, abs_tol=TOL)
def distribution(values):
if not isinstance(values, (list, tuple)) or not values:
raise ValueError("nonempty probability sequence required")
if any(isinstance(x, bool) or not isinstance(x, (int, float))
or not math.isfinite(x) or not 0 <= x <= 1 for x in values):
raise ValueError("finite probabilities in [0,1] required")
if not near(math.fsum(values), 1.0):
raise ValueError("probabilities must sum to one")
def entropy(probabilities):
distribution(probabilities)
return math.fsum(-p * math.log2(p) for p in probabilities if p > 0)
def summarize(weights, members):
distribution(weights)
if not isinstance(members, (list, tuple)) or len(weights) != len(members):
raise ValueError("one distribution per weight required")
for row in members:
distribution(row)
if len({len(row) for row in members}) != 1:
raise ValueError("all members need the same category order and count")
for weight, row in zip(weights, members):
if any(weight > 0 and p > 0 and weight * p == 0 for p in row):
raise ValueError("positive joint mass underflows this float implementation")
mean = [math.fsum(w * row[k] for w, row in zip(weights, members))
for k in range(len(members[0]))]
member_entropies = [entropy(row) for row in members]
expected = math.fsum(w * h for w, h in zip(weights, member_entropies))
predictive = entropy(mean)
# A separate expression: average divergence from each member to the mixture.
weighted_kl = math.fsum(
w * math.fsum(p * (math.log2(p) - math.log2(q))
for p, q in zip(row, mean) if p > 0)
for w, row in zip(weights, members) if w > 0
)
return {"mean": mean, "predictive_entropy_bits": predictive,
"member_entropy_bits": member_entropies, "expected_entropy_bits": expected,
"gap_bits": predictive - expected, "weighted_kl_bits": weighted_kl}
def decimal_joint_information(weights, members):
"""Independent reference: I(M;Y) from the high-precision joint table."""
with localcontext() as context:
context.prec = 70
mass = [[Decimal(str(w)) * Decimal(str(p)) for p in row]
for w, row in zip(weights, members)]
model_marginal = [sum(row, Decimal(0)) for row in mass]
label_marginal = [sum((row[k] for row in mass), Decimal(0))
for k in range(len(members[0]))]
information = Decimal(0)
for m, row in enumerate(mass):
for k, joint in enumerate(row):
if joint:
independent = model_marginal[m] * label_marginal[k]
information += joint * (joint / independent).ln()
return float(information / Decimal(2).ln())
CASES = [
("confident_disagreement", [0.5, 0.5], [[0.99, 0.01], [0.01, 0.99]]),
("shared_uncertainty", [0.5, 0.5], [[0.5, 0.5], [0.5, 0.5]]),
("unanimous_confident", [0.5, 0.5], [[0.99, 0.01], [0.99, 0.01]]),
("weighted_disagreement", [0.8, 0.2], [[1.0, 0.0], [0.0, 1.0]]),
("three_categories", [0.2, 0.3, 0.5], [[0.8, 0.1, 0.1], [0.1, 0.7, 0.2], [0.2, 0.2, 0.6]]),
("single_member", [1.0], [[0.2, 0.3, 0.5]]),
("zero_weight", [1.0, 0.0], [[0.2, 0.8], [1.0, 0.0]]),
("structural_zero", [0.5, 0.5], [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]),
]
def main():
report = []
by_name = {}
for name, weights, members in CASES:
before = deepcopy((weights, members))
result = summarize(weights, members)
reference = decimal_joint_information(weights, members)
check(name + " mixture mass", near(math.fsum(result["mean"]), 1.0))
check(name + " entropy gap equals weighted KL", near(result["gap_bits"], result["weighted_kl_bits"]))
check(name + " independent Decimal joint table", near(result["gap_bits"], reference))
check(name + " nonnegative up to tolerance", result["gap_bits"] >= -TOL)
check(name + " bounded by model-index entropy", result["gap_bits"] <= entropy(weights) + TOL)
check(name + " bounded by category count", result["gap_bits"] <= math.log2(len(members[0])) + TOL)
check(name + " input unchanged", (weights, members) == before)
row = {"case": name, "weights": weights, "members": members, **result,
"decimal_joint_information_bits": reference}
report.append(row)
by_name[name] = row
print(f"{name}: predictive={result['predictive_entropy_bits']:.6f}; "
f"expected={result['expected_entropy_bits']:.6f}; gap={result['gap_bits']:.6f}")
left = by_name["confident_disagreement"]
right = by_name["shared_uncertainty"]
check("same predictive distribution", left["mean"] == right["mean"] == [0.5, 0.5])
check("same predictive entropy", near(left["predictive_entropy_bits"], 1.0)
and near(right["predictive_entropy_bits"], 1.0))
check("different member uncertainty", left["expected_entropy_bits"] < right["expected_entropy_bits"])
check("different disagreement", left["gap_bits"] > right["gap_bits"])
check("identical uncertain members have zero gap", near(right["gap_bits"], 0.0))
check("unequal one-hot weights give H(weights)", near(by_name["weighted_disagreement"]["gap_bits"], entropy([0.8, 0.2])))
check("single member has zero gap", near(by_name["single_member"]["gap_bits"], 0.0))
check("zero-weight member does not contribute", near(by_name["zero_weight"]["gap_bits"], 0.0))
check("unused category needs no epsilon", near(by_name["structural_zero"]["gap_bits"], 1.0))
unanimous = by_name["unanimous_confident"]
hypothetical_label = 1
predictions = [max(range(len(row)), key=row.__getitem__) for row in unanimous["members"]]
check("low disagreement can accompany a specified error", predictions == [0, 0]
and hypothetical_label == 1 and near(unanimous["gap_bits"], 0.0))
print("specified_counterexample: predictions=[0,0]; label=1; zero_gap_does_not_prove_correctness")
invalid = [
([], []), ([0.4, 0.4], [[1, 0], [0, 1]]), ([-0.1, 1.1], [[1, 0], [0, 1]]),
([1], []), ([1], [[0.2, 0.2]]), ([1], [[-0.1, 1.1]]),
([0.5, 0.5], [[1, 0], [0.2, 0.3, 0.5]]), ([1], [[float('nan'), 1]]),
([1], [[float('inf'), 0]]), ([True], [[1, 0]]), ([1], [[True, False]]),
([1], [["0.5", "0.5"]]), ([1e-300, 1.0], [[1e-300, 1.0], [0.5, 0.5]]),
]
for index, args in enumerate(invalid):
try:
summarize(*args)
except ValueError:
check("reject invalid or unrepresentable input " + str(index), True)
else:
raise AssertionError("invalid input accepted: " + str(index))
print(f"PASS checks={len(checks)}; reference=weighted_KL+Decimal_joint_table; unit=bits")
Path('report.json').write_text(json.dumps({
"scope": "specified finite categorical mixtures; no training, posterior inference, or real-data evaluation",
"unit": "bits", "tolerance": TOL, "cases": report,
"specified_error": {"predictions": predictions, "hypothetical_label": hypothetical_label},
"checks": len(checks), "passed": checks,
}, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
if __name__ == '__main__':
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。手工指定有限类别概率与权重;核验预测熵、成员平均熵、加权 KL 和 Decimal 联合表互信息;未训练、推断后验或评估真实模型。
运行后对照结果
confident_disagreement: predictive=1.000000; expected=0.080793; gap=0.919207
shared_uncertainty: predictive=1.000000; expected=1.000000; gap=0.000000
unanimous_confident: predictive=0.080793; expected=0.080793; gap=0.000000
weighted_disagreement: predictive=0.721928; expected=0.000000; gap=0.721928
three_categories: predictive=1.576179; expected=1.216895; gap=0.359284
single_member: predictive=1.485475; expected=1.485475; gap=0.000000
zero_weight: predictive=0.721928; expected=0.721928; gap=0.000000
structural_zero: predictive=1.000000; expected=0.000000; gap=1.000000
specified_counterexample: predictions=[0,0]; label=1; zero_gap_does_not_prove_correctness
PASS checks=79; reference=weighted_KL+Decimal_joint_table; unit=bits
把这个算例放回论文时,保留它的前提
BALD 论文在贝叶斯框架下,从参数后验与未知标签的互信息出发,推导出预测熵减条件熵期望的目标。那里对参数取平均的权重来自指定后验;本文则人为给定有限成员和权重,只核对同一信息恒等关系。
Deep Ensembles 论文把分类预测按成员概率平均,研究实际训练集成的不确定性表现。它提供了方法与实证背景,不能把本文几组手写概率表称为已经训练了深度集成,也不能把普通集成自动说成精确后验采样。
在实际项目里,先记录成员怎样得到、权重怎样确定、类别顺序是否一致,再决定计算哪个量。若要讨论认知不确定性与数据噪声,还需明确建模假设和独立验证;本篇只把“平均概率一样”背后可能存在的不同成员结构展示出来。
记住这三点
H(平均概率)与平均H(成员概率)是不同运算,平均结果相同也可有不同分歧。- 对明确的有限混合,熵差等于成员到平均分布的加权 KL,也等于成员编号与类别的互信息。
- 分歧小不等于预测正确;任意模型集合不自动是贝叶斯后验。
别把例子用过头
- 只保存平均概率,却声称知道成员之间是否一致。
- 忽略成员权重或交换计算顺序。
- 把概率集中、低分歧与真实正确率混为一谈。
- 把手工模型集合的熵差直接命名为现实中的某一种不确定性来源。
自己试一小步
能否构造两组三分类成员,让平均预测相同,但分歧明显不同?
- 自己选择非负、归一的成员权重和两组类别概率,运行前写出预期关系。
- 分别保存预测熵、成员平均熵、加权 KL 和 Decimal 联合表参考。
- 加入一个零权重成员或交换成员顺序,说明哪些结果应保持不变。
- 另行指定一组标签,检查分歧与正确性之间哪些推断不能成立。
做到这些,就算完成
- 所有成员使用相同类别顺序,对数单位一致。
- 三条计算路径在声明容差内一致,没有靠改 expected 通过。
- 结论仅覆盖自己指定的概率模型,没有声称真实训练或后验推断完成。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Houlsby 等:Bayesian Active Learning for Classification and Preference Learning (新标签页)↗
在明确贝叶斯后验前提下,将信息增益改写为预测熵减条件熵期望,并解释模型间分歧。
- Lakshminarayanan 等:Deep Ensembles (新标签页)↗
分类集成按成员概率平均形成混合;具体集成方法与贝叶斯后验平均的前提需要区分。