qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

预测没变,只把米换成厘米,损失为什么涨了 4.605?

用高斯密度、区间概率与完整负对数似然,拆开计量单位、方差输入和常数项三个容易混淆的尺度。

展开本文目录
  1. 下载完整练习包
  2. 负对数密度可以为负
  3. 单位变换会改变密度的数值
  4. 把密度、区间宽度和损失放在同一张账里
  5. 接口写 var,就传方差
  6. 残差越小,并不代表方差可以随意缩到零
  7. 把量纲与区间都打印出来
  8. 比较回归概率时,保留完整约定
  9. 记住这些要点
  10. 别把例子用过头
  11. 自己试一小步
  12. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
概率密度
描述单位长度附近能累积多少概率;要对一个区间累积,才能得到事件概率。
坐标换元的密度因子
同一段长度改用更细的数字单位表示后,每单位对应的概率密度要相应降低,才不会改变整段概率。
打开完整术语小抄 ↗

一个回归模型预测误差约为 0.1 米,标准差为 0.2 米。换成厘米后,误差变成 10,标准差变成 20。实际预测完全相同,但完整高斯负对数似然从 −0.565499 变为 4.039671。

这不是模型突然变差,也不必是程序出错。连续变量的密度数值依赖所用坐标单位;我们先算明白密度怎样变化,再用一个区间的概率确认,描述的随机事件并没有改变。

把例子带到自己的电脑上

下载完整练习包

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

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

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

python --version
python -B gaussian_nll_units.py

解压到独立练习文件夹,在含 gaussian_nll_units.py 的目录打开终端运行。固定一维高斯密度与单位变换算术;未训练回归器、未运行 PyTorch、未验证真实数据分布或校准。

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

负对数密度可以为负

对单个连续目标 y,模型给出均值 μ 和方差 v=σ²。完整高斯 NLL 为 0.5×[ln(2π)+ln(v)+(y−μ)²/v]。这里取的是观测位置的概率密度的负对数,不是“精确等于这个数”的概率的负对数。

当 μ=0、y=0.1、σ=0.2,方差为 0.04,密度约为 1.760327 每米。密度高于 1 没有违反概率上限;概率要对一个区间累积。对应 NLL 为 −0.565499,负数本身不是错误信号。

对于连续分布,单个精确数值的概率为零;密度描述单位长度附近积累概率的速率。离散类别的概率与连续数值的密度都能进入似然,但它们的数值边界与单位含义不同。

单位变换会改变密度的数值

设新数值 y′=a×y,a>0。为了让同一段物理区间的概率不变,新密度必须满足 p′(y′)=p(y)/a。因此 NLL′=NLL+ln(a)。这个除以 a 的因子来自坐标伸缩的 Jacobian,也就是变换如何拉长单位区间。

米变厘米时 a=100,所以单个一维观测的完整 NLL 增加 ln(100)=4.605170。均值与标准差都乘 100,方差必须乘 10000;只把方差乘 100 会把分布真的改错。

程序同时算区间 [−0.1,0.1] 米与 [−10,10] 厘米的概率,两边都为 0.382925。密度数值变小、NLL 数值变大,与区间概率保持不变完全相容。

同一数据、同一维度并统一单位后,所有模型加上相同常数,所以排序不会仅因这种单位变换改变。不同单位、维度、求和/平均方式或常数项混在一起时,不能直接横向比较一个裸露的 loss 数字。

把密度、区间宽度和损失放在同一张账里

仍在 y=0.1 米附近看一小段 [0.09,0.11] 米,它和 [9,11] 厘米是同一物理范围。区间宽度从 0.02 米变成 2 厘米,数字放大了 100 倍;相应的密度从约 1.760327 每米变成 0.017603 每厘米,数字缩小为百分之一。宽度和密度一增一减,概率才保持一致。

“中点密度×区间宽度”在有限区间里只是近似,因为区间内密度还会变化。本例近似值为 0.035206533,用高斯累积概率作差得到的区间概率为 0.035195533。把单位换成厘米后,累积概率差仍为 0.035195533。所以密度大于 1,不是在说这段范围的概率超过 100%。

再把 NLL 公式拆开看:米单位下,残差平方除以方差为 0.1²/0.04=0.25;厘米单位下为 10²/400=0.25,这一项没有变。常数 ln(2π) 也没有变。变化只来自 0.5×ln(v):它增加 0.5×ln(400/0.04)=ln(100)。这就是同一维度、同一物理分布只改单位时 NLL 增加 4.605170 的来源。

这里的 Jacobian 因子可以先读成“单位刻度被拉长多少倍,就把每单位的密度除以多少倍”。它是在保持区间概率,不是模型突然更不确定。这个一维正比例例子用一个倍数就能说明;涉及多维或非线性变换时,需要重新写出相应变换规则。

接口写 var,就传方差

PyTorch GaussianNLLLoss 的 var 表示正方差。本文 σ=0.2,因此正确输入为 0.04。误把标准差 0.2 塞进方差位置,相当于把分布改宽,完整 NLL 会变成 0.139220。它仍是个正常数字,形状检查通常发现不了这类语义错误。

该接口默认 full=False,会省略 0.5×ln(2π) 常数项。本例省略后是 −1.484438,比完整值少 0.918939。固定同一任务时这不改变参数梯度,但会影响你拿数字去和其他实现对照。本文只核对文档,未调用 PyTorch。

接口还可能用 eps 对方差做数值下限处理。我们的小程序选择直接拒绝非正方差,以便暴露输入错误;这不是完整模拟框架的稳定化或自动求导行为。对齐实现时,应连同 full、reduction、eps 和输入量纲一起核对。

残差越小,并不代表方差可以随意缩到零

只看一个非零残差 r,完整 NLL 作为方差的函数在 v=r² 处有极小值。脚本对本例 r=0.1,比较 v=0.01 与其一半、两倍,核对这个局部数学关系。它不是用一条样本可靠估计真实不确定性的统计方法。

如果预测恰好命中 y=μ,残差项为零,σ 从 1 缩到 0.1、0.01 时,NLL 分别为 0.918939、−1.383647、−3.686232,继续缩小还会继续下降。一个过度灵活、逐样本控制方差的模型可能因此产生不合理的极小方差。

真实训练需要结合参数化、方差约束、独立数据和校准检查判断,而不是把训练 NLL 越负越好当成充分结论。本例没有训练神经网络,也没有比较具体的正则化方法。

把量纲与区间都打印出来

保存为 gaussian_nll_units.py,运行 python -B gaussian_nll_units.py。程序打印米和厘米两套数值,核对 NLL 差、密度伸缩、区间概率、常数项以及非正方差边界,并写入 report.json。

区间概率使用 Python 标准库 math.erf 计算高斯累积概率的差,没有依赖统计库。输出明确写出密度是每米还是每厘米,避免看见 1.760327 就把它读成 176% 概率。

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

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

小实验 · python
"""Gaussian density and NLL under a declared change of measurement units."""
import json
import math
from pathlib import Path


def nll(y, mean, variance, full=True):
    if variance <= 0:
        raise ValueError('variance must be positive')
    value = 0.5 * (math.log(variance) + (y - mean) ** 2 / variance)
    return value + (0.5 * math.log(2 * math.pi) if full else 0.0)


def interval_probability(low, high, mean, sigma):
    if sigma <= 0 or high < low:
        raise ValueError('invalid interval or sigma')
    return 0.5 * (math.erf((high-mean)/(sigma*math.sqrt(2))) - math.erf((low-mean)/(sigma*math.sqrt(2))))


def main():
    y, mean, sigma = 0.1, 0.0, 0.2
    factor = 100.0
    meters = nll(y, mean, sigma**2)
    centimeters = nll(y*factor, mean*factor, (sigma*factor)**2)
    delta = centimeters - meters
    assert math.isclose(delta, math.log(factor), abs_tol=1e-12)
    density_m, density_cm = math.exp(-meters), math.exp(-centimeters)
    assert density_m > 1
    assert math.isclose(density_m/factor, density_cm, abs_tol=1e-12)
    prob_m = interval_probability(-0.1,0.1,mean,sigma)
    prob_cm = interval_probability(-10,10,mean*factor,sigma*factor)
    assert math.isclose(prob_m, prob_cm, abs_tol=1e-12)
    omitted = nll(y, mean, sigma**2, full=False)
    wrong_variance = nll(y, mean, sigma)
    print(f'meters: variance={sigma**2:.6f}, full NLL={meters:.6f}, density={density_m:.6f} per meter')
    print(f'centimeters: variance={(sigma*factor)**2:.6f}, full NLL={centimeters:.6f}, density={density_cm:.6f} per centimeter')
    print(f'NLL difference={delta:.6f}; log(100)={math.log(factor):.6f}')
    print(f'interval probability: [-0.1,0.1] m={prob_m:.6f}; [-10,10] cm={prob_cm:.6f}')
    print(f'constant omitted NLL={omitted:.6f}; difference={meters-omitted:.6f}')
    print(f'incorrectly passing sigma as variance: NLL={wrong_variance:.6f}')
    exact_fit = [{'sigma':s, 'nll':nll(0,0,s*s)} for s in [1.0,0.1,0.01]]
    print('exact-fit NLL as sigma shrinks: '+', '.join(f"{r['nll']:.6f}" for r in exact_fit))
    assert all(a['nll'] > b['nll'] for a,b in zip(exact_fit,exact_fit[1:]))
    residual = y - mean
    optimum = residual**2
    assert nll(y,mean,optimum) < nll(y,mean,optimum/2)
    assert nll(y,mean,optimum) < nll(y,mean,optimum*2)
    try:
        nll(y,mean,0)
    except ValueError:
        pass
    else:
        raise AssertionError('zero variance must fail')
    report = {'meters':{'nll':meters,'density':density_m},'centimeters':{'nll':centimeters,'density':density_cm},
        'nll_delta':delta,'interval_probability':prob_m,'constant_omitted':omitted,
        'sigma_as_variance':wrong_variance,'exact_fit':exact_fit,
        'scope':'Fixed univariate Gaussian arithmetic; no network fitting, PyTorch execution or empirical calibration.'}
    Path('report.json').write_text(json.dumps(report, indent=2, allow_nan=False)+'\n', encoding='utf-8')
    print('checks passed: Jacobian scaling, interval invariance, constant, variance boundary')


if __name__ == '__main__':
    main()

怎样运行:在独立练习文件夹中执行 python -B gaussian_nll_units.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。固定一维高斯密度与单位变换算术;未训练回归器、未运行 PyTorch、未验证真实数据分布或校准。

运行后对照结果
meters: variance=0.040000, full NLL=-0.565499, density=1.760327 per meter
centimeters: variance=400.000000, full NLL=4.039671, density=0.017603 per centimeter
NLL difference=4.605170; log(100)=4.605170
interval probability: [-0.1,0.1] m=0.382925; [-10,10] cm=0.382925
constant omitted NLL=-1.484438; difference=0.918939
incorrectly passing sigma as variance: NLL=0.139220
exact-fit NLL as sigma shrinks: 0.918939, -1.383647, -3.686232
checks passed: Jacobian scaling, interval invariance, constant, variance boundary

比较回归概率时,保留完整约定

一个可比较的记录至少包含目标单位、输出维度、标准差还是方差、是否保留常数、每样本还是每元素归约,以及测试数据。只看损失正负,很难区分模型变化、单位变化与实现约定变化。

本次验证限于一维高斯公式与固定数字,没有学习回归器、检查真实数据是否近似高斯,也没有完成经验覆盖率或校准评估。它能解释数字为何变化,不能证明高斯就是某项任务的合适分布。

记住这些要点

  • 连续密度可以高于 1,负对数密度可以小于 0。
  • 米改厘米使一维完整 NLL 增加 ln100,但同一物理区间的概率不变。
  • 标准差乘 a,方差必须乘 a²。
  • 比较 NLL 必须对齐单位、常数项、维度与归约方式。

别把例子用过头

  • 把密度值当成一个事件的概率。
  • 将 sigma 直接传入名为 var 的参数。
  • 不同实现一个保留常数、一个省略,却解释为模型质量差异。
  • 把一个样本的最优方差当作可靠的不确定性估计。

自己试一小步

若两个目标分别使用米和秒,整体 NLL 在更换其中一个单位时该怎样变化?

  1. 先假设两个目标独立,逐维计算完整 NLL 再求和。
  2. 只把长度换成厘米,时间单位保持秒。
  3. 分别打印逐维项、总和与按维平均值。
  4. 验证同一矩形区域的联合概率保持不变。

做到这些,就算完成

  • 明确独立假设,不把公式当成一般相关多元高斯。
  • 分别推导并核对总和与按两维平均的变化,说明两种归约为何不同。
  • 写明每个方差的单位。

查原始资料

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