浮点数、舍入误差与机器精度
层级:A|建议先修:01-01、01-08
数学中的实数可以无限精确,计算机却只能用有限位存储数值。机器学习中许多“奇怪 bug”并非公式错误,而是浮点表示、舍入和误差累积造成的。
1. 浮点表示
浮点数类似科学计数法:
其中 是符号, 是有效数字, 是基数, 是指数。现代硬件通常遵循 IEEE 754,以二进制为基数。
常见格式:
- float32:1 位符号、8 位指数、23 位显式尾数;约 7 位十进制有效数字;
- float64:1 位符号、11 位指数、52 位显式尾数;约 16 位十进制有效数字;
- float16、bfloat16:训练加速常用,有效精度或范围更低。
2. 为什么 0.1 不能精确表示
十进制的 在二进制中是无限循环小数,就像 在十进制中无限循环。因此存储的是最接近的可表示浮点数。
这会导致程序中
0.1 + 0.2 == 0.3
可能为假。问题不是语言算错,而是两边经过不同舍入路径后得到相邻但不完全相同的浮点值。
3. 规格化数与次正规数
规格化数把有效数字最高位固定为非零,从而充分使用尾数位。接近 0 时,IEEE 754 使用次正规数逐渐降低精度,让数值平滑下溢到 0,而不是突然出现一个巨大间隙。
此外还有特殊值:
- :如有限非零数除以 0 或上溢;
- NaN:未定义结果,如 、 的实数计算;
- 带符号的 。
4. 舍入模型
一次基本运算的常用模型是
其中 是单位舍入误差, 表示加减乘除之一。这个相对误差模型需要结果处于正常范围等条件。
5. 机器精度
机器 epsilon 常定义为使
在该浮点格式下成立的最小正数。二进制、向最近值舍入时:
- float32 的 epsilon 约为 ;
- float64 的 epsilon 约为 。
有些数值分析教材把单位舍入误差 定义为 epsilon 的一半,阅读时要核对约定。
6. 浮点运算不满足结合律
实数中
浮点中却可能不成立。例如 :
- ;
- 中 的 1 可能被舍去,结果为 0。
因此并行归约改变求和顺序时,末位结果可能不同。这不一定是并发错误。
7. 灾难性消去
两个非常接近的大数相减时,高位有效数字相互抵消,只留下原本受舍入误差影响较大的低位,称为灾难性消去。
例如小 时直接计算
会损失精度。乘以共轭式可改写为
数值更稳定。
8. 求和误差
依次累加大量数值会不断舍入。改善方法包括:
- 从绝对值小的数开始相加;
- pairwise summation:像平衡树一样两两求和;
- Kahan 补偿求和:额外记录丢失的低位;
- 使用更高精度累加器。
深度学习的混合精度训练通常让矩阵乘法使用低精度,但关键累加和主权重保留更高精度。
9. 比较浮点数
不要只用绝对误差,也不要机械写 abs(a-b)<epsilon。较稳妥的判断是
其中绝对容差处理接近 0 的数,相对容差处理不同尺度。容差应根据算法误差和业务量纲确定,而非只取机器 epsilon。
10. 前向误差与后向误差
- 前向误差:计算结果与真实答案有多远;
- 后向误差:计算结果是否等于某个轻微扰动输入的精确答案。
后向稳定算法保证自己像是精确解决了一个与原问题非常接近的问题。最终前向误差还会被问题本身的条件数放大。
11. 易错点
- 打印小数位数有限,不表示内部值精确。
- float64 只是更精确,不是没有舍入误差。
- NaN 与任何数比较通常都为假,甚至
NaN == NaN也为假;应使用专门检测函数。 - 低精度训练的稳定性依赖缩放、累加格式和硬件实现,不能只改数据类型。
常见问答
Q1:金额计算可以用 float 吗?
需要精确十进制规则时通常应使用定点整数或十进制定点类型;二进制浮点不适合要求分币严格相等的账务。
Q2:为什么 GPU 重跑同一训练结果末位不同?
并行执行顺序、原子操作和非确定性内核会改变舍入路径,误差再经迭代放大。可开启确定性选项,但可能牺牲性能且仍受跨硬件差异影响。
Q3:机器 epsilon 能直接作为所有比较容差吗?
不能。多步计算的误差远大于单次舍入,且量纲与输入尺度不同;容差必须结合问题分析。
Q4:小数显示 0.30000000000000004 是否说明结果不可用?
不一定。这通常是正常的二进制舍入。关键是误差是否在任务可接受范围内。
练习
- 解释十进制 0.1 为什么不能在有限二进制位中精确表示。
- 给出一个浮点加法不满足结合律的例子。
- 将 在小 时改写为更稳定的形式。
- 为什么比较接近 0 的数时必须保留绝对容差?
答案与提示
- 它的最简分母含因子 5,而有限二进制小数只能精确表示分母为 2 的幂的有理数。
- 可用正文的 示例。
- 利用恒等式 。
- 相对误差以数值大小作分母,接近 0 时会失去意义或要求过严。