15. 浮点运算:问题和限制

浮点数在计算机硬件中表示为基2(二进制)分数。例如,小数部分:

0.125

值为1/10+2/100+5/1000,二进制分数也一样:

0.001

值为0/2+0/4+1/8。这两个分数的值相同,唯一的实际区别是第一个分数是以10为底的分数表示法写的,第二个分数是以2为底的。

不幸的是,大多数十进制分数不能精确地表示为二进制分数。其结果是,通常情况下,您输入的十进制浮点数仅与实际存储在机器中的二进制浮点数近似。

这个问题一开始在基10中更容易理解。考虑分数1/3。您可以将其近似为以10为基数的分数:

0.3

或者,更好地,:

0.33

或者,更好地,:

0.333

等等。不管你愿意写下多少个数字,结果永远不会精确到1/3,但会越来越接近1/3。

同样,无论您愿意使用多少以2为基数的数字,十进制值0.1都不能精确表示为以2为基数的分数。在基2中,1/10是无限重复分数:

0.0001100110011001100110011001100110011001100110011...

停在任意有限位数处,就得到一个近似值。在今天的大多数机器上,浮点数是用一个带分子的二进制分数来近似的,它使用前53位,从最重要的位开始,分母是2的幂。在1/10的情况下,二进制分数是 3602879701896397 / 2 ** 55 接近但不完全等于1/10的真值。

由于值的显示方式,许多用户不知道近似值。python只将十进制近似值打印到机器存储的二进制近似值的真十进制值。在大多数机器上,如果python要打印存储为0.1的二进制近似值的真十进制值,则必须显示:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这比大多数人认为有用的数字还要多,所以python通过显示一个舍入值来保持数字的可管理性:

>>> 1 / 10
0.1

请记住,即使打印的结果看起来是1/10的精确值,实际存储的值也是最近的可表示二进制分数。

有趣的是,有许多不同的十进制数字共享相同的最接近的近似二进制分数。例如,数字 0.10.100000000000000010.1000000000000000055511151231257827021181583404541015625 都近似于 3602879701896397 / 2 ** 55 . 由于所有这些十进制值共享相同的近似值,因此可以在保留不变量的同时显示其中任何一个值。 eval(repr(x)) == x .

历史上,python提示和内置 repr() 函数将选择17位有效数字, 0.10000000000000001 . 从python 3.1开始,python(在大多数系统上)现在可以选择其中最短的,并且只需显示 0.1 .

注意,这是二进制浮点的本质:这不是Python中的bug,也不是代码中的bug。在所有支持硬件浮点运算的语言中,您都会看到同样的情况(尽管有些语言可能不会 显示 默认情况下的差异,或在所有输出模式下的差异)。

为了获得更好的输出,您可能希望使用字符串格式来生成有限数量的有效数字:

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'

>>> repr(math.pi)
'3.141592653589793'

重要的是要意识到这是一种真实的幻觉:你只是把 显示 真正的机器值。

一种错觉可能产生另一种错觉。例如,由于0.1不完全是1/10,所以0.1的三个值相加可能不会得到0.3,或者:

>>> .1 + .1 + .1 == .3
False

此外,由于0.1不能再接近1/10的精确值,0.3也不能再接近3/10的精确值,因此使用 round() 函数无法帮助::

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

虽然数字不能接近其预期的精确值,但是 round() 函数可用于后舍入,以便具有不精确值的结果相互比较:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True

二进制浮点运算有许多这样的惊喜。“0.1”的问题将在下面的“表示错误”部分中详细解释。见 The Perils of Floating Point 更完整地描述其他常见的惊喜。

正如结尾所说,“没有简单的答案。”不过,不要过分警惕浮点!python float操作中的错误是从浮点硬件继承而来的,在大多数机器上,顺序不超过2中的1部分 * * 每次操作53次。对于大多数任务来说,这已经足够了,但是您需要记住,这不是十进制算术,并且每个浮点运算都会遇到新的舍入错误。

虽然病理病例确实存在,但如果您只是将最终结果的显示四舍五入到您期望的小数位数,那么对于大多数随意使用的浮点运算,您将看到您期望的结果。 str() 通常足够,为了更好地控制,请参见 str.format() 方法的格式说明符位于 格式字符串语法 .

对于需要精确十进制表示的用例,请尝试使用 decimal 该模块实现了适合会计应用和高精度应用的十进制算法。

另一种形式的精确算法由 fractions 基于有理数实现算法的模块(这样1/3这样的数字就可以精确表示)。

如果您是浮点运算的超级用户,那么您应该看看NumPy包和SciPy项目提供的用于数学和统计操作的许多其他包。看到了吗<https://scipy.org>.

python提供了一些工具,可以在您真正 do 想知道浮点数的确切值。这个 float.as_integer_ratio() 方法将浮点值表示为分数:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

因为比率是精确的,所以可以使用它无损地重新创建原始值:

>>> x == 3537115888337719 / 1125899906842624
True

这个 float.hex() 方法以十六进制(以16为基数)表示浮点,再次给出计算机存储的确切值:

>>> x.hex()
'0x1.921f9f01b866ep+1'

这种精确的十六进制表示可用于精确地重建浮点值:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

由于该表示是精确的,因此对于跨Python(平台无关性)的不同版本可靠地移植值和与支持相同格式的其他语言(如Java和C99)交换数据是有用的。

另一个有用的工具是 math.fsum() 有助于减少求和过程中精度损失的函数。它跟踪“丢失的数字”,因为值被添加到一个连续的总数中。这会对整体准确性产生影响,使误差不会累积到影响最终总误差的程度:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

15.1. 表示错误

本节详细解释了“0.1”示例,并展示了如何自己对这种情况执行精确的分析。假设基本熟悉二进制浮点表示。

Representation error 指的是有些(大多数,实际上)十进制分数不能精确地表示为二进制(基2)分数。这是Python(或Perl、C、C++、Java、FORTRAN和许多其他)经常不能显示您期望的精确十进制数的主要原因。

为什么会这样?1/10不完全可以表示为二进制分数。现在(2000年11月)几乎所有的机器都使用IEEE-754浮点运算,几乎所有的平台都将python浮点映射到了IEEE-754“双精度”。754双精度包含53位精度,因此在输入时,计算机会努力将0.1转换为形式中最接近的分数。 J/2**N 在哪里 j 是一个正好包含53位的整数。改写:

1 / 10 ~= J / (2**N)

身份:

J ~= 2**N / 10

并回忆起 J 正好有53位 >= 2**52 但是 < 2**53 )的最佳价值 N 56:

>>> 2**52 <=  2**56 // 10  < 2**53
True

也就是说,56是 N 那叶子 J 正好有53位。最大可能值 J 那么这个商是四舍五入的:

>>> q, r = divmod(2**56, 10)
>>> r
6

由于余数超过10的一半,因此通过四舍五入得到最佳近似值:

>>> q+1
7205759403792794

因此,754双精度中1/10的最佳近似值为:

7205759403792794 / 2 ** 56

将分子和分母除以2,则分数减小为:

3602879701896397 / 2 ** 55

注意,自从我们四舍五入后,这个值实际上比1/10大一点;如果我们没有四舍五入,商就会比1/10小一点。但在任何情况下都不能 确切地 1/10!

所以计算机永远不会“看到”1/10:它看到的是上面给出的精确分数,它能得到的最佳754双近似值:

>>> 0.1 * 2 ** 55
3602879701896397.0

如果我们把这个分数乘以10 * * 55,我们可以看到55位小数:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

这意味着存储在计算机中的确切数字等于十进制值0.10000000000000055511151231257827021181583404541015625。许多语言(包括旧版本的python)没有显示完整的十进制值,而是将结果四舍五入为17位有效数字:

>>> format(0.1, '.17f')
'0.10000000000000001'

这个 fractionsdecimal 模块使这些计算变得简单:

>>> from decimal import Decimal
>>> from fractions import Fraction

>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)

>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'