ELAI S.r.l.

负方差:正确公式为什么会在计算机中出错

三个数的精确方差为 14/9,计算结果却是 −128:通过推导、有限精度分析和可复现算法比较解释原因。

负方差:正确公式为什么会在计算机中出错

不可能的数值揭示真实问题

AI 流水线在归一化数据或检查传感器之前,需要计算一个量的波动。观测值很大,彼此差异却很小,程序竟返回负方差。方差是平方的平均值,因此负号不可能描述预期的数学量。问题在数据、公式,还是算术运算的执行方式?本文用三个合成数值隔离第三种可能。

问题很明确:实数上等价的表达式,在有限精度下还能互换吗?我们将解释为何不能,比较中心化的两遍计算与增量更新,并区分计算误差和输入中已经丢失的信息。实验不测量生产线或 EL-AI 模型精度。单位是抽象的:若输入为伏特,方差单位就是伏特平方,但本文没有真实电压表数据。

选择算法前先定义偏差

算术平均数是数值的平衡点,偏差是观测值减去平均数。方差描述偏差平方的平均大小,平方能避免正负偏差抵消。n 表示观测数量,x_i 表示第 i 个值,mu 表示平均数。这里除以 n,即计算由三个观测组成的有限总体的总体方差。

mu = (1/n) sum_i x_i\nv = (1/n) sum_i (x_i - mu)^2\n = (1/n) sum_i x_i^2 - mu^2

展开每个平方可得到最后一个等式。−2 mu x_i 的总和为 −2 n mu²,而 mu² 出现 n 次,除以 n 后就得到平方的平均数减去平均数的平方。在精确算术中,结果相同且不可能为负。最后的表达式只需累计总和及平方和,看似方便,但代数上的方便不保证数值稳定。

n−1 修正回答的是另一个统计问题:在适当假设下,用样本估计更大总体的方差。它不能修复数值消减。对于三个值,更换分母只会把方差乘以 3/2,并不能恢复丢失的位数。NumPy 调用明确指定 ddof=0,确保各方法比较的是相同数学量。

不用计算机也能推导的核心案例

令 b=1,000,000,000,三个值为 b+1、b+2、b+4。平均数是 b+7/3,偏差为 −4/3、−1/3、5/3,平方和为 42/9,再除以三得到 14/9。共同平移不会改变到平均数的距离,因此很大的基线 b 消失了。精确方差约为 1.5555555556,既不是零,也不可能为负。

实际程序使用 Python 3.14.0、有效数位为 53 位的浮点数和顺序累加,原始公式却返回 −128。确定性搜索采用 10^k 基线,k 从 0 到 16,增量固定为 1、2、4。在这个顺序下,首个负值出现在 k=9。公开搜索规则可以避免把挑选的案例包装成错误发生率,并不声称所有三元组或所有计算机都会得到 −128。

数字在哪里消失

浮点数保留有限个有效位,而不是固定的小数位数。数值越大,相邻可表示数之间的间隔也越大。在 b 附近,math.ulp(b) 约为 1.19×10⁻⁷,因此整数增量仍然清晰可分。k=9 时的问题不是输入阶段已经丢失了 1、2、4,而是在后续对大数平方的运算中产生。

原始公式把两个接近 10¹⁸ 的量相减,希望得到约 1.56 的残差。乘法、求和及平均过程中的舍入误差,相对于这个残差可能很大。相减使这种不确定性暴露出来:共同的高位被消去,但已经不可得的低位不会重新出现。不能简单说任何相近数相减都不准确,关键在于操作数已有误差主导了我们想提取的小结果。

把求和换成 math.fsum 是有价值的检查,但不是万能修复。fsum 改善接收到的数值的累加,不能使已经舍入的 x*x 或平均数平方重新精确。本例中,fsum 版本同样返回 −128。用 max(v,0) 截断会得到零,虽然消除了不可能的负号,但正确方差仍为 14/9。截断可以是系统中的控制策略,却不能证明信息已经恢复。

先中心化,再平方

两遍法先估计平均数,再累计相对于它的偏差平方。被平方的量较小,不需要在最后把两个巨大项相减。代码对平均数及偏差求和都使用 fsum。若计算中心为 mu+e,在精确算术下,围绕该中心的平方距离平均值为 v+e²,因为真实偏差之和为零。这说明小的中心误差不会自动变成很大的方差误差,但其他运算仍会舍入。

(1/n) sum_i (x_i - (mu+e))^2 = v + e^2

对于不希望重新读取的数据流,通常称为 Welford 的更新只保留计数、当前平均数和中心平方和 M2。新值 x_k 到来时,delta 是它到旧平均数的距离,平均数移动 delta/k,再用 delta 乘以它到新平均数的距离来更新 M2。本次比较最终使用 M2/n,而不是 M2/(n−1)。

delta = x_k - mu_(k-1) mu_k = mu_(k-1) + delta/k M2_k = M2_(k-1) + delta * (x_k - mu_k)

无需调用库也可以验证递推式。把前 k−1 个值的中心移动 delta/k,由于旧偏差和为零,它们的贡献增加 (k−1)delta²/k²。新值到新平均数的距离为 delta(k−1)/k,贡献为 delta²(k−1)²/k²。两项相加得到 delta²(k−1)/k,正好等于 delta(x_k−mu_k)。实数上的推导精确,并不意味着程序具有无限精度。

结果与精确参考的含义

方法方差,单位²
原始公式-128
原始公式 + fsum-128
两遍法1.5555555555555571
Welford1.555555522441864
NumPy 2.5.3 float641.5555555555555571
精确分数14/9

本例中,两遍法与 NumPy 一致,Welford 非常接近正确值但并非完全相同。这不是库之间的通用竞赛。NumPy 输入为 float64,ddof=0;其他方法是本实验的简短实现。Fraction.from_float 把实际浮点输入转换为精确分数,再无舍入地计算方差。k=9 时,整数输入可以精确表示,因此这个参考也等于原本想生成的数据的方差。

基线更大时,出现重要区别。在 10¹⁶ 附近,相邻浮点数间隔为 2,增量 1 不再总能按预期保留。转换之后,方差算法无法重建这个信息。因此图中的误差以实际表示输入的精确方差为参考,并不假设整个扫描中方差始终为 14/9。错误的参考会混淆输入表示误差和计算稳定性两个问题。

左:基线为 10^k、增量为 1、2、4 时,相对于实际表示输入的绝对误差。采用对称对数坐标,±10⁻¹⁴ 内为线性区以显示零。右:k=9 案例。曲线重合不代表普遍精确。
左:基线为 10^k、增量为 1、2、4 时,相对于实际表示输入的绝对误差。采用对称对数坐标,±10⁻¹⁴ 内为线性区以显示零。右:k=9 案例。曲线重合不代表普遍精确。

程序还检查核心三元组的全部六种排列。在本环境中,原始公式始终为 −128,而 Welford 大约在 1.5555555224 至 1.5555555820 之间变化。这提醒我们,即使更稳定的算法,在有限精度下也可能受顺序影响。基线扫描和排列检查是确定性测试,不是对任意序列稳定性的形式证明。JSON 包含完整结果,而不只挑选有利案例。

理解取舍后再选择方法

两遍法和 Welford 的操作次数都与 n 成正比。Welford 保留固定大小的状态;两遍法需要重新读取或保存数据。这是在描述算法,不是整个脚本的内存,因为脚本为了复现而保留输入和结果。Fraction 在小例子中提供精确参考,但不断增长的大整数算术不具有浮点运算的固定成本。实验没有进行时间或内存基准测试。

中心化也不能解决所有尺度问题:非有限输入、乘法溢出、极小偏差下溢或平均数超出可表示范围,都需要进一步处理。若计算前已经丢失信息,可能需要改变表示,或采集相对于较小参考值的差分。统计量用于模型归一化时,小方差常进入分母,因此接近零的处理策略也必须明确。不能由三个数推断预测质量得到提升。

公式是恒等式,算法是选择

答案是否定的:代数等价不意味着数值等价。本例的精确方差 14/9 在中心化计算中得以保留,增量法也保留得非常接近,而原始公式的大数相减淹没了它。负号是有价值的症状,不应盲目修正。把异常归咎于 AI 模型之前,应先明确要计算什么量、输入实际如何表示,以及执行了哪些算术步骤。

来源、代码与分析范围

已阅读 numpy.var 关于公式、ddof 和精度的技术说明,以及 Python 对 fsum、ulp 的定义。推导与合成三元组属于本文的教学分析,不是复现已发表基准。2026 年 10 月 21 日查阅 NumPy 2.5 及 Python 3.14 文档,实际执行环境为 Python 3.14.0、NumPy 2.5.3,结果中已记录。未在传感器、GPU 或嵌入式平台执行,也不声称经过人工审核。

NumPy 2.5 — numpy.var.

Python 3.14 — math.fsum, math.ulp.

from fractions import Fraction
xs=[1000000001.0,1000000002.0,1000000004.0]
total=squares=0.0
for x in xs:
    total+=x
    squares+=x*x
raw=squares/len(xs)-(total/len(xs))**2
ys=[Fraction.from_float(x) for x in xs]
mean=sum(ys)/len(ys)
reference=sum((x-mean)**2 for x in ys)/len(ys)
assert raw==-128.0 and reference==Fraction(14,9)
print(raw,reference)

代码、数据与说明 · JSON. 教学计算使用 Python 3.14.0,图使用 Matplotlib 3.11.2。分析由 AI 辅助,不声称经过同行评审或人工审核。原创 ImageGen 封面仅作示意,不记录 EL-AI 人员、场所或实际安装。来源查阅于 2026 年 10 月 21 日。