qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

两个预测峰都合理,平均值为什么落在低密度区?

用固定双峰高斯混合,区分平方误差最优、密度高低与混合负对数似然,再检查稳定计算和总方差。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先固定分布,再讨论什么叫预测得好
  4. 均值仍然让期望平方误差最小
  5. 混合密度先加起来,再取负对数
  6. 总方差既有每个峰内部的宽度,也有峰之间的距离
  7. 密度已经舍入成零,对数密度仍可能算得出来
  8. 运行脚本,并核对两条独立参考路径
  9. 把结果交给决策之前,保留你正在优化的目标
  10. 记住这些要点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
混合密度
把多个分布在同一位置的密度,按各自权重加起来。
全方差分解
总波动由每组内部的波动,以及各组均值之间的差异共同组成。
打开完整术语小抄 ↗

假设一个量通常出现在 −2 或 2 附近,两边一样常见,每边的标准差都是 0.5。把两个中心平均得到 0,但 0 恰好在两个峰之间,附近的密度很低。于是“取平均更稳妥”与“选择更常出现的值”给出不同答案。

这里没有哪种目标自动胜出。我们固定一个可完全写出的分布,分别计算期望平方误差、位置上的密度和负对数似然。每一个数字都由随文标准库程序产生;本篇不训练混合密度网络。

把例子带到自己的电脑上

下载完整练习包

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

下载 ZIP · 5 个文件
包里有什么
  • mixture_mean.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

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

python --version
python -B mixture_mean.py

解压到独立练习文件夹,在含 mixture_mean.py 的目录打开终端运行。已运行固定一维高斯混合的密度、MSE、NLL、总方差与边界验证,并以 Decimal 和数值积分核对;未训练 MDN、运行 EM 或评估真实数据。

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

平均值一定在高密度的位置吗?

两组结果分别集中在 −2 和 2 附近,各组标准差都是 0.5。调节两组的占比,再移动一个共用预测值 q,分别看密度和期望平方误差。

-4-2024概率密度数值 / 预测 q

蓝线:混合密度;橙色虚线:平均值;绿点:当前预测。纵轴随占比自动缩放,密度高度不是单个点的概率。

当前平均值 μ = 0.0000。在平均值处的密度是 0.0003。

预测 q = 0.0000:密度为 0.0003,负对数密度为 8.2258。

期望平方误差 = 方差 + (q − μ)² = 4.2500。取 q = μ 时达到最小值 4.2500。

总方差 4.2500 = 组内方差 0.2500 + 两组中心差异带来的方差 4.0000。

密度高的位置和平方误差最小的常数预测回答不同问题。这里使用固定参数分布的解析计算,没有抽样训练,也没有训练混合密度网络;正文的独立程序还检查稳定对数计算。

先固定分布,再讨论什么叫预测得好

用 φ(y;μ,σ) 表示均值为 μ、标准差为 σ 的一维高斯密度。这里 p(y)=0.5φ(y;−2,0.5)+0.5φ(y;2,0.5)。可以把它理解为先等概率选择左、右分量,再从选中的高斯分布取一个数。权重必须非负且加起来为 1;标准差必须为正。

这与“先把两个均值平均,再只保留一个高斯”不同。混合密度保留两边各自聚集的形状;一个均值和一个方差通常不能还原这种形状。Bishop 的混合密度网络把混合参数交给神经网络随输入产生,而本文直接给定参数,只研究分布计算。

程序算出 p(0)=0.000267660,p(2)=0.398942280。这里的 2 是右侧分量中心,不宣称它就是混合分布精确求解后的众数。密度更高表示相同且足够窄的邻域内更容易积累概率;连续变量精确等于某一点的概率仍为零,不能把密度直接读成百分比。

均值仍然让期望平方误差最小

若最终必须输出一个固定数 a,并以 (Y−a)² 计损失,那么 E[(Y−a)²]=Var(Y)+(a−E[Y])²。第一项不随 a 改变,第二项在 a=E[Y] 时最小。因此“均值落在低密度区”不会推翻它对平方误差的最优性。

本例 E[Y]=0,Var(Y)=4.25。输出 0 的期望平方误差是 4.25;输出右分量中心 2 的期望平方误差是 4.25+2²=8.25。选 2 虽然靠近一个高密度区域,却把另一半分布留在更远处,平方惩罚会把这部分代价放大。

“期望”是在本例设定的整个分布上取平均,不是凭一条观测评估。若业务在意落入某个可行区域、容许多个备选答案,或使用不同损失,就应把那项目标写出来。不能仅凭这里的密度差异宣布均值预测一定错误,也不能凭均方误差最优宣布它适合所有决策。

混合密度先加起来,再取负对数

对一个观测 y,混合 NLL 是 −ln[Σᵢ wᵢφᵢ(y)]。另一个看起来相似的写法是 Σᵢwᵢ[−lnφᵢ(y)]:先对每个分量取负对数,再加权。对数是非线性函数,两种顺序不能交换。

在 y=2 处,正确混合 NLL 为 0.918939;分量 NLL 的加权平均为 16.225791。右分量能够很好解释这个观测,但左分量离得很远。后一个表达式仍把左分量的大损失计入平均,所以它回答的是另一个问题。

由于 −ln 是凸函数,权重非负且和为 1 时,有 −ln(Σᵢwᵢpᵢ)≤Σᵢwᵢ(−ln pᵢ)。这是两个目标的数学关系;不代表本例运行过某种训练并观察到了模型塌缩。代码保留高斯密度的常数项,避免把不同约定的裸损失放在一起比较。

总方差既有每个峰内部的宽度,也有峰之间的距离

把所选分量记为 Z,总方差可以拆成 E[Var(Y|Z)]+Var(E[Y|Z])。第一项先看每个分量内部有多分散,再按权重平均;第二项看各分量均值离总体均值有多远。本文把它们称为“分量内方差”和“分量间方差”。

本例分量内方差是 0.5×0.5²+0.5×0.5²=0.25。两边均值距离总体均值都是 2,分量间方差是 0.5×(−2)²+0.5×2²=4。总方差因此是 4.25。只把两个标准差或两个方差平均,会丢掉两个峰分开的事实。

程序还计算一个不对称例子:权重 [0.2,0.3,0.5],均值 [−1,0.5,3],标准差 [0.25,1,0.75]。它的均值为 1.45、总方差为 3.26625。除了代入分解公式,还对单独写出的密度做数值积分,避免仅靠左右对称让错误的权重计算也碰巧得到零均值。

密度已经舍入成零,对数密度仍可能算得出来

在 y=30 这样很远的位置,普通浮点数先计算两个 exp 再相加,会把密度舍入为 0。对这个 0 再取对数就失去意义;但混合 NLL 实际仍是有限的 1568.918939。远离数据不等于密度在数学上严格为零。

令 aᵢ=ln(wᵢ)+lnφᵢ(y),m=maxᵢaᵢ。使用 logsumexp(a)=m+ln[Σᵢexp(aᵢ−m)],至少有一项指数是 exp(0)=1,不会让所有分量同时因为指数下溢而消失。零权重直接跳过,避免求 ln(0);正权重和标准差则明确验证。

稳定写法也有适用范围。程序拒绝非有限输入、非法权重和非正标准差;权重和按 1e−12 的绝对容差检查,不偷偷重新归一化。若标准化残差大到超出浮点表示范围,也会拒绝。logsumexp 解决的是这一阶段的指数缩放问题,不能保证任意巨大参数都可计算。

运行脚本,并核对两条独立参考路径

保存为 mixture_mean.py,在同一目录执行 python -B mixture_mean.py。脚本只依赖 Python 标准库,已在 Python 3.14.0 实际运行;语法按 Python 3.10 检查,但未启动 Python 3.10 解释器。运行后生成 report.json,包含参数、检查名称、数值与执行范围。

对 NLL,参考实现用 Decimal 的 70 位精度直接计算各密度、求和再取对数,不调用被检查的 logsumexp。对均值、方差与平方误差,参考路径对另一份密度表达式做 Simpson 数值积分。对称分布积分范围为 [−8,8],不对称分布为 [−10,12];这仍是有限区间、有限步长的数值核验,不能当成无限精度证明。

本次共通过 36 项检查,包括上述独立参考、零权重退化、单分量退化与非法输入。最后一行应显示 checks=36 passed;前面的密度、MSE、NLL 与方差输出也应逐项一致。没有安装统计框架、拟合数据或评估真实预测效果。

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

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

小实验 · python
"""Fixed Gaussian mixtures: density, squared error, likelihood and variance.

Python 3.10+; standard library only. No model training or real dataset.
"""
import json
import math
import platform
from decimal import Decimal, localcontext
from pathlib import Path


def validate(weights, means, sigmas):
    if not weights or not (len(weights) == len(means) == len(sigmas)):
        raise ValueError("nonempty component arrays must have equal lengths")
    if not all(math.isfinite(x) for row in (weights, means, sigmas) for x in row):
        raise ValueError("parameters must be finite")
    if any(w < 0 for w in weights) or not math.isclose(
        math.fsum(weights), 1.0, rel_tol=0.0, abs_tol=1e-12
    ):
        raise ValueError("weights must be nonnegative and sum to one")
    if any(s <= 0 for s in sigmas):
        raise ValueError("standard deviations must be positive")


def log_normal(x, mean, sigma):
    residual = (x - mean) / sigma
    return -0.5 * math.log(2.0 * math.pi) - math.log(sigma) - 0.5 * residual * residual


def logsumexp(values):
    top = max(values)
    return top + math.log(math.fsum(math.exp(v - top) for v in values))


def log_mixture(x, weights, means, sigmas):
    validate(weights, means, sigmas)
    if not math.isfinite(x):
        raise ValueError("observation must be finite")
    terms = [math.log(w) + log_normal(x, m, s)
             for w, m, s in zip(weights, means, sigmas) if w > 0]
    if not all(math.isfinite(t) for t in terms):
        raise ValueError("standardized residual exceeds this float implementation")
    return logsumexp(terms)


def moments(weights, means, sigmas):
    validate(weights, means, sigmas)
    mean = math.fsum(w * m for w, m in zip(weights, means))
    within = math.fsum(w * s * s for w, s in zip(weights, sigmas))
    between = math.fsum(w * (m - mean) ** 2 for w, m in zip(weights, means))
    return mean, within, between


def decimal_nll(x, weights, means, sigmas):
    """Independent reference: directly sum high-precision densities, then log.

    This intentionally does not call log_normal or logsumexp.
    """
    with localcontext() as ctx:
        ctx.prec = 70
        pi = Decimal("3.1415926535897932384626433832795028841971693993751058209749445923")
        xd = Decimal(str(x))
        total = Decimal(0)
        for w, m, s in zip(weights, means, sigmas):
            wd, md, sd = map(lambda v: Decimal(str(v)), (w, m, s))
            exponent = -((xd - md) ** 2) / (2 * sd * sd)
            total += wd * exponent.exp() / ((2 * pi).sqrt() * sd)
        return float(-total.ln())


def simpson(fn, lo, hi, intervals=16000):
    """Independent numerical integration over a specified finite interval."""
    if intervals <= 0 or intervals % 2:
        raise ValueError("Simpson integration needs a positive even count")
    step = (hi - lo) / intervals
    total = math.fsum((4 if i % 2 else 2) * fn(lo + i * step)
                      for i in range(1, intervals))
    return step * (fn(lo) + fn(hi) + total) / 3


def main():
    checks = []

    def check(name, condition):
        if not condition:
            raise AssertionError(name)
        checks.append(name)

    w, m, s = [0.5, 0.5], [-2.0, 2.0], [0.5, 0.5]
    mean, within, between = moments(w, m, s)
    variance = within + between
    density_mean = math.exp(log_mixture(mean, w, m, s))
    density_center = math.exp(log_mixture(2.0, w, m, s))
    mixture_nll = -log_mixture(2.0, w, m, s)
    component_nll = math.fsum(-wi * log_normal(2.0, mi, si)
                              for wi, mi, si in zip(w, m, s))
    check("density at the mean is lower than at a component center", density_mean < density_center)
    check("mixture NLL is below weighted component NLL here", mixture_nll < component_nll)
    check("within variance", math.isclose(within, 0.25))
    check("between variance", math.isclose(between, 4.0))

    # Integrate a separately written density formula, avoiding log_mixture.
    def density(x):
        return sum(wi * math.exp(-((x - mi) ** 2) / (2 * si * si))
                   / (si * math.sqrt(2 * math.pi)) for wi, mi, si in zip(w, m, s))

    mass = simpson(density, -8, 8)
    integrated_mean = simpson(lambda x: x * density(x), -8, 8)
    integrated_variance = simpson(lambda x: (x - mean) ** 2 * density(x), -8, 8)
    mse_at_mean = simpson(lambda x: (x - mean) ** 2 * density(x), -8, 8)
    mse_at_center = simpson(lambda x: (x - 2) ** 2 * density(x), -8, 8)
    check("quadrature mass", math.isclose(mass, 1.0, abs_tol=1e-10))
    check("quadrature mean", math.isclose(integrated_mean, mean, abs_tol=1e-10))
    check("quadrature total variance", math.isclose(integrated_variance, variance, abs_tol=1e-10))
    check("quadrature MSE at mean", math.isclose(mse_at_mean, 4.25, abs_tol=1e-10))
    check("quadrature MSE at component center", math.isclose(mse_at_center, 8.25, abs_tol=1e-10))

    # An asymmetric case prevents symmetry from hiding incorrect mixture weights.
    wa, ma, sa = [0.2, 0.3, 0.5], [-1.0, 0.5, 3.0], [0.25, 1.0, 0.75]
    amean, awithin, abetween = moments(wa, ma, sa)
    def asymmetric_density(x):
        return sum(wi * math.exp(-((x - mi) ** 2) / (2 * si * si))
                   / (si * math.sqrt(2 * math.pi)) for wi, mi, si in zip(wa, ma, sa))
    amass = simpson(asymmetric_density, -10, 12)
    amean_ref = simpson(lambda x: x * asymmetric_density(x), -10, 12)
    avar_ref = simpson(lambda x: (x - amean) ** 2 * asymmetric_density(x), -10, 12)
    check("asymmetric quadrature mass", math.isclose(amass, 1, abs_tol=1e-10))
    check("asymmetric quadrature mean", math.isclose(amean, amean_ref, abs_tol=1e-10))
    check("asymmetric quadrature variance", math.isclose(awithin + abetween, avar_ref, abs_tol=1e-10))
    check("asymmetric Decimal NLL", math.isclose(
        -log_mixture(1.7, wa, ma, sa), decimal_nll(1.7, wa, ma, sa), abs_tol=1e-10))

    references = [(x, -log_mixture(x, w, m, s), decimal_nll(x, w, m, s))
                  for x in [-8.0, -2.0, 0.0, 2.0, 8.0, 30.0]]
    for x, actual, reference in references:
        check("Decimal NLL x=" + str(x), math.isclose(actual, reference, rel_tol=0, abs_tol=1e-10))
    naive_far = density(30.0)
    stable_far_nll = -log_mixture(30.0, w, m, s)
    check("ordinary floating density underflows at x=30", naive_far == 0.0)
    check("log-domain NLL remains finite at x=30", math.isfinite(stable_far_nll))
    check("zero-weight component is skipped", math.isclose(
        log_mixture(1, [0, 1], [-99, 2], [1, 0.5]), log_normal(1, 2, 0.5)))
    check("single component reduces to Gaussian", math.isclose(
        log_mixture(0.3, [1], [0], [2]), log_normal(0.3, 0, 2)))
    invalid = [([], [], []), ([1], [0, 1], [1]), ([-0.1, 1.1], [0, 1], [1, 1]),
               ([0.4, 0.4], [0, 1], [1, 1]), ([0, 0], [0, 1], [1, 1]),
               ([1], [0], [0]), ([1], [0], [-1]), ([1], [math.inf], [1]),
               ([math.nan], [0], [1])]
    for i, params in enumerate(invalid):
        try:
            log_mixture(0, *params)
        except ValueError:
            check("reject invalid parameters " + str(i), True)
        else:
            check("reject invalid parameters " + str(i), False)
    for x in [math.nan, math.inf, -math.inf]:
        try:
            log_mixture(x, w, m, s)
        except ValueError:
            check("reject nonfinite observation " + str(x), True)
        else:
            check("reject nonfinite observation " + str(x), False)
    try:
        log_mixture(1e308, [1], [0], [1e-308])
    except ValueError:
        check("reject residual outside float range", True)
    else:
        check("reject residual outside float range", False)

    report = {
        "python_version": platform.python_version(), "checks": len(checks), "passed": checks,
        "mean": mean, "within_variance": within, "between_variance": between,
        "total_variance": variance, "density_at_mean": density_mean,
        "density_at_component_center": density_center, "mse_at_mean": mse_at_mean,
        "mse_at_component_center": mse_at_center, "mixture_nll_at_2": mixture_nll,
        "weighted_component_nll_at_2": component_nll, "naive_density_at_30": naive_far,
        "stable_nll_at_30": stable_far_nll, "decimal_references": references,
        "quadrature": {"range": [-8, 8], "intervals": 16000, "mass": mass},
        "asymmetric_reference": {"weights": wa, "means": ma, "sigmas": sa,
                                 "mean": amean, "variance": awithin + abetween,
                                 "integrated_mean": amean_ref, "integrated_variance": avar_ref},
        "scope": "Fixed one-dimensional Gaussian mixtures only; no training or real-data evaluation."
    }
    Path("report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
    print(f"mean={mean:.6f} density_at_mean={density_mean:.9f} density_at_center={density_center:.9f}")
    print(f"MSE_at_mean={mse_at_mean:.6f} MSE_at_center={mse_at_center:.6f}")
    print(f"mixture_NLL={mixture_nll:.6f} weighted_component_NLL={component_nll:.6f}")
    print(f"variance: within={within:.6f} between={between:.6f} total={variance:.6f}")
    print(f"x=30 naive_density={naive_far:.1f} stable_NLL={stable_far_nll:.6f}")
    print(f"asymmetric_reference: mean={amean:.6f} variance={awithin + abetween:.6f}")
    print(f"checks={len(checks)} passed; references=Decimal+quadrature; report.json written")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B mixture_mean.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。已运行固定一维高斯混合的密度、MSE、NLL、总方差与边界验证,并以 Decimal 和数值积分核对;未训练 MDN、运行 EM 或评估真实数据。

运行后对照结果
mean=0.000000 density_at_mean=0.000267660 density_at_center=0.398942280
MSE_at_mean=4.250000 MSE_at_center=8.250000
mixture_NLL=0.918939 weighted_component_NLL=16.225791
variance: within=0.250000 between=4.000000 total=4.250000
x=30 naive_density=0.0 stable_NLL=1568.918939
asymmetric_reference: mean=1.450000 variance=3.266250
checks=36 passed; references=Decimal+quadrature; report.json written

把结果交给决策之前,保留你正在优化的目标

一份可核对的结果至少应写明:分量权重、每个均值和标准差、选择的输出规则,以及比较的是密度、某段区间概率、期望平方误差还是负对数似然。若单位变化,还要沿前置文章的规则同时处理密度与方差。

本例足以说明“一个位置不常见”和“输出它的平均平方代价最小”可以同时成立。它不能证明一个真实任务需要多少分量,也不能证明高斯形状合理。真实训练还涉及参数估计、方差约束、数据划分和校准,需另做实验。

下方练习只给任务和验收条件,不提前给出改动后的数值答案。先手算你的预测,再修改参数、运行程序,并用独立积分与高精度参考解释两种结果是否一致。

记住这些要点

  • 密度高的位置与期望平方误差最小的输出可以不同。
  • 混合 NLL 必须先对密度加权求和,再取负对数。
  • 总方差包含分量内和分量间两部分。
  • logsumexp 改善数值稳定性,仍需要输入与范围检查。

别把例子用过头

  • 把密度当作某个精确数值出现的概率。
  • 把分量 NLL 平均误写成混合 NLL。
  • 平均分量方差后遗漏峰之间的离散程度。
  • 把固定参数验证说成完成了 MDN 训练或真实数据评估。

自己试一小步

把左、右分量权重改成 0.8 和 0.2,哪些最优输出和数值会变?

  1. 先手算总体均值、分量内与分量间方差。
  2. 比较输出新均值、−2 和 2 时的期望平方误差与密度。
  3. 在 y=2 和 y=30 比较混合 NLL 与分量 NLL 加权平均。
  4. 把新参数加入 Decimal 与数值积分参考,保存新的 report.json。

做到这些,就算完成

  • 权重仍非负且和为 1,报告单位与标准差约定。
  • 独立积分与方差分解在你设定的容差内相符。
  • 能说明“密度更高”和“MSE 更小”分别优化什么。
  • 保留全部失败检查,不能只改 expected 输出使程序通过。

查原始资料

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

  • Bishop:Mixture Density Networks,印刷第 1、6 页 (新标签页)↗Christopher M. Bishop / Microsoft Research 作者存档 · 英文

    条件均值的局限、混合密度定义和高斯分量

    2026-10-18 已下载并渲染 PDF,实际查看印刷第 1 页(PDF 第 2 页)的条件概率分解与条件均值局限,以及印刷第 6 页(PDF 第 7 页)第 3 节、公式 (22)(23)。定位时另看过印刷第 4、5 页,未完整核验其推导;未全读 26 页,也未复现论文训练实验。

  • scikit-learn:Gaussian mixture models (新标签页)↗scikit-learn 官方 · 英文

    高斯混合模型定义、协方差选项和有限数据下的奇异性限制

    2026-10-18 已打开并阅读 2.1 定义、2.1.1 Gaussian Mixture,以及 Singularities、Number of components 段。本文固定参数,不运行 EM、scikit-learn 或模型选择。

  • PyTorch v2.9.1:MixtureSameFamily 实现 (新标签页)↗PyTorch 官方仓库 · 英文

    混合均值、全方差分解与 logsumexp 对数密度

    2026-10-18 已阅读 mean、variance、log_prob 的完整实现,以及参数形状检查与 sample 段。本文用独立标准库程序验证对应数学量,未调用 PyTorch。