12.2 浮点数与舍入:有限比特怎样近似实数
地心引擎再次送来那笔微小数值误差,数学瞭望塔这次从有限表示和舍入规则解释它。
数学里的实数连续而无限,计算机的存储有限。浮点数用固定数量的比特挑选一组可表示值,其他实数只能舍入到附近。
本课目标
- 理解符号、有效数和指数的作用;
- 解释
0.1 + 0.2的结果而不把它叫作语言 bug; - 正确处理 NaN、无穷大、上溢和下溢;
- 根据问题选择容差比较、十进制定点或更高精度。
1. 浮点表示像科学计数法
二进制浮点数可概括为:
$$ (-1)^s\times significand\times 2^{exponent}. $$
IEEE 754 常见的 binary32 使用 1 位符号、8 位指数域和 23 位尾数字段;正规数还有一个隐含的前导 1,因此有效精度通常说是 24 个二进制位。binary64 使用 1 位符号、11 位指数域和 52 位尾数字段,有 53 位有效精度。
指数扩大可表示范围,有效数决定相对精度。可表示数在实数轴上并不等距:数量级越大,相邻浮点数之间的绝对间隔通常越大。
除正规数外,标准还包含:
+0与-0;- 次正规数,用较低精度逐渐接近零;
+∞与-∞;- NaN,表示某些无效或未定义结果。
这些特殊值会参与后续运算,系统必须决定检测、传播还是拒绝。
2. 为什么十进制 0.1 不能精确表示
有限二进制小数只能精确表示分母约分后为 $2^k$ 的有理数。十进制 0.1=1/10 的分母含因子 5,在二进制中会无限循环,只能舍入成附近的浮点数。
from decimal import Decimal
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(Decimal.from_float(0.1))
print(Decimal("0.1") + Decimal("0.2"))Decimal.from_float(0.1) 展示 binary64 值的精确十进制形式;Decimal("0.1") 则从十进制文本构造,不先经过二进制浮点舍入。
这是有限二进制表示的必然结果,不是 Python 独有现象,也不表示 IEEE 754 算错了。十进制也有同类限制,例如有限位十进制无法精确表示 $1/3$。
3. 每一步运算都可能舍入
通常的浮点运算模型是:先得到精确数学结果,再按当前舍入规则映射到可表示值。常见默认模式是“舍入到最近值,正中间时取偶数最低位”,它避免长期产生单向偏差。
误差类型包括:
- 表示误差:输入本身不可精确表示;
- 舍入误差:运算结果落在两个可表示值之间;
- 上溢:绝对值超过有限范围,可能得到无穷大;
- 下溢:结果过小,进入次正规范围或舍入为零。
机器 epsilon 描述 1 附近可分辨的相对间隔,不是适用于所有数量级、所有算法的通用比较阈值。ULP(末位单位)会随数值大小变化。
4. 相等比较要看问题语义
浮点数不是永远不能用 ==。若比较的是同一份标识性计算结果、整数可精确范围内的值或协议定义的特殊常量,精确比较可能合理。对经过独立数值路径得到的近似实数,通常使用绝对容差与相对容差组合:
$$ |a-b|\le \max(\text{abs_tol},\text{rel_tol}\cdot\max(|a|,|b|)). $$
import math
result = 0.1 + 0.2
print(math.isclose(result, 0.3, rel_tol=1e-12, abs_tol=1e-15))绝对容差负责零附近,相对容差负责不同数量级。阈值应来自测量精度、算法误差和业务容忍度,不能机械复制 1e-9。
NaN 与任何值比较都不相等,包括它自己。检测 NaN 应使用 math.isnan,不要写 x == float("nan")。
5. 金额、计数和科学计算的选择不同
- 计数:优先整数,注意范围与溢出;
- 货币:常用最小货币单位的整数,或规则明确的十进制定点/十进制类型;
- 科学计算:binary64 常是合理起点,但误差预算决定是否需要更高精度;
- 机器学习:低精度可换吞吐和显存,但常需损失缩放、混合精度和稳定算子;
- 协议与存储:必须明确格式、舍入、特殊值和跨语言兼容性。
Decimal 不是自动“完全精确”。它仍有上下文精度和舍入规则,对无限十进制结果也必须近似;它解决的是十进制语义和可控舍入,而不是消除有限表示。
6. 运算顺序会改变结果
浮点加法通常不满足结合律:
$$ (a+b)+c\ne a+(b+c). $$
a = 1e16
b = -1e16
c = 1.0
print((a + b) + c) # 1.0
print(a + (b + c)) # 0.0(常见 binary64 结果)在第二种顺序中,b + c 的小量可能在舍入时丢失。并行归约改变求和顺序,所以浮点程序即使输入相同,也可能因线程划分不同产生末位差异。需要位级可复现时,必须额外约束算法和执行环境。
常见误区
- 双精度有 52 位十进制小数:52 是尾数字段的二进制位数,不是十进制小数位数。
- epsilon 是全局最小正数:机器 epsilon、最小正规数和最小次正规数是不同概念。
- Decimal 消除所有误差:它也受有限精度与舍入上下文控制。
- 浮点异常一定抛异常:许多环境会产生 NaN 或无穷大并继续计算。
练习
- 找出哪些简单分数可被有限二进制小数精确表示,并说明规律。
- 用
math.nextafter观察 1、1e10和1e-10附近的浮点间隔。 - 为一个金额结算模块比较“分为单位的整数”和
Decimal两种设计。 - 构造一个 NaN 进入排序、聚合或序列化流程的测试,记录运行时行为。
小结
浮点数以有限比特换取巨大的动态范围,代价是离散表示、舍入和特殊值。不必害怕小数;应让数值类型、比较规则和误差容忍度与问题语义一致。
下一课继续追问:同样使用 binary64,为什么一种公式稳定,另一种数学上等价的公式却会溢出或丢掉全部有效数字?