qinghuan
← 回到知识探索

AI 直觉 / 动手实验

平均概率都一样,为什么一组模型比另一组更有分歧?

比较先平均概率再算熵,与先算成员熵再平均;用加权 KL 和高精度联合概率表核对差值。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先看两张平均后完全一样的表
  4. 熵在平均之前算,还是之后算?
  5. 差值可以量出这组概率表里的分歧
  6. 分歧为零,仍然可以一起答错
  7. 权重、零概率与成员数量都属于定义
  8. 运行八组案例,再看两条独立参考
  9. 把这个算例放回论文时,保留它的前提
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
  • 知道概率之和为 1,能读加权平均
  • 先读 熵、交叉熵与 KL
  • Python 3.10+;实验只用标准库
遇到不熟悉的词,先看这 2 个解释
预测熵
先把成员的类别概率按权重平均,再计算平均分布有多分散。
分歧互信息
在指定混合模型中,知道选中哪个成员能增加多少关于类别的信息。
打开完整术语小抄 ↗

两组分类器最后都给出“类别 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 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

平均概率相同,成员是在犹豫,还是互相反对?

正在准备实验控件…

平均预测第一类概率
0.500000
平均预测的熵 H(平均概率)
1.000000
成员熵的加权平均
0.080793
两者差值:成员分歧
0.919207

成员熵分别为 0.080793、0.080793。用另一条公式计算加权 KL,得到 0.919207 bit,与差值核对。0 log 0 按极限记为 0,不额外加平滑项。

平均预测把两份意见混合了。只看平均概率,会漏掉各成员的分歧。

两个手工给定的二分类概率分布,单位为 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 保存全部输入和参考值,这些检查不构成真实集成质量评估。

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

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

小实验 · python
"""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,也等于成员编号与类别的互信息。
  • 分歧小不等于预测正确;任意模型集合不自动是贝叶斯后验。

别把例子用过头

  • 只保存平均概率,却声称知道成员之间是否一致。
  • 忽略成员权重或交换计算顺序。
  • 把概率集中、低分歧与真实正确率混为一谈。
  • 把手工模型集合的熵差直接命名为现实中的某一种不确定性来源。

自己试一小步

能否构造两组三分类成员,让平均预测相同,但分歧明显不同?

  1. 自己选择非负、归一的成员权重和两组类别概率,运行前写出预期关系。
  2. 分别保存预测熵、成员平均熵、加权 KL 和 Decimal 联合表参考。
  3. 加入一个零权重成员或交换成员顺序,说明哪些结果应保持不变。
  4. 另行指定一组标签,检查分歧与正确性之间哪些推断不能成立。

做到这些,就算完成

  • 所有成员使用相同类别顺序,对数单位一致。
  • 三条计算路径在声明容差内一致,没有靠改 expected 通过。
  • 结论仅覆盖自己指定的概率模型,没有声称真实训练或后验推断完成。

查原始资料

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

  • Houlsby 等:Bayesian Active Learning for Classification and Preference Learning (新标签页)↗作者原论文 / arXiv · 英文

    在明确贝叶斯后验前提下,将信息增益改写为预测熵减条件熵期望,并解释模型间分歧。

    2026-10-23 已核读 PDF 可提取正文的第 2 节、公式 (1)(2) 及随后分歧解释,位于 PDF 第 2–3 页;未全读 17 页,未实现高斯过程或复现主动学习实验。

  • Lakshminarayanan 等:Deep Ensembles (新标签页)↗作者原论文 / NeurIPS 2017 · 英文

    分类集成按成员概率平均形成混合;具体集成方法与贝叶斯后验平均的前提需要区分。

    2026-10-23 已读摘要、Introduction 的 BMA 与 ensemble 区分、PDF 第 5 页 Algorithm 1 后的预测组合公式,以及第 9 页 Discussion;未全读或复现论文训练。