开始前,会这些就够了
- 知道均值、方差和平方误差
- 能读加权求和、指数与自然对数
- 先读 预测没变,只把米换成厘米,损失为什么涨了 4.605?:按该文准备相关概念。
假设一个量通常出现在 −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,分别看密度和期望平方误差。
蓝线:混合密度;橙色虚线:平均值;绿点:当前预测。纵轴随占比自动缩放,密度高度不是单个点的概率。
当前平均值 μ = 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 与方差输出也应逐项一致。没有安装统计框架、拟合数据或评估真实预测效果。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""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,哪些最优输出和数值会变?
- 先手算总体均值、分量内与分量间方差。
- 比较输出新均值、−2 和 2 时的期望平方误差与密度。
- 在 y=2 和 y=30 比较混合 NLL 与分量 NLL 加权平均。
- 把新参数加入 Decimal 与数值积分参考,保存新的
report.json。
做到这些,就算完成
- 权重仍非负且和为 1,报告单位与标准差约定。
- 独立积分与方差分解在你设定的容差内相符。
- 能说明“密度更高”和“MSE 更小”分别优化什么。
- 保留全部失败检查,不能只改 expected 输出使程序通过。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Bishop:Mixture Density Networks,印刷第 1、6 页 (新标签页)↗
条件均值的局限、混合密度定义和高斯分量
- scikit-learn:Gaussian mixture models (新标签页)↗
高斯混合模型定义、协方差选项和有限数据下的奇异性限制
- PyTorch v2.9.1:MixtureSameFamily 实现 (新标签页)↗
混合均值、全方差分解与 logsumexp 对数密度