第十五课时之浮点数算法:争议和限制

14.浮点数算法:争议和限制

浮点数在计算机中表达为二进制(binary)小数。例如:十进制小数:

0.125

是 1/10 + 2/100 + 5/1000 的值,同样二进制小数:

0.001

是 0/2 + 0/4 + 1/8。这两个数值相同。唯一的实质区别是第一个写为十进制小数记法,第二个是二进制。

不幸的是,大多数十进制小数不能完全用二进制小数表示。结果是,一般情况下,你输入的十进制浮点数仅由实际存储在计算机中的近似的二进制浮点数表示。

这个问题更早的时候首先在十进制中发现。考虑小数形式的 1/3 ,你可以来个十进制的近似值

0.3

或者更进一步的,

0.33

  

诸如此类。如果你写多少位,这个结果永远不是精确的 1/3 ,但是可以无限接近 1/3 。

同样,无论在二进制中写多少位,十进制数 0.1 都不能精确表达为二进制小数。二进制来表达 1/10 是一个无限循环小数:

0.0001100110011001100110011001100110011001100110011...

在任何有限数量的位停下来,你得到的都是近似值。今天在大多数机器上,浮点数的近似使用的小数以最高的 53 位为分子,2 的幂为分母。至于 1/10 这种情况,其二进制小数是 3602879701896397 ** 55,它非常接近但不完全等于1/10真实的值。

由于显示方式的原因,许多使用者意识不到是近似值。Python 只打印机器中存储的二进制值的十进制近似值。在大多数机器上,如果 Python 要打印 0.1 存储的二进制的真正近似值,将会显示:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这么多位的数字对大多数人是没有用的,所以 Python 显示一个舍入的值

>>> 1 / 10
0.1

只要记住即使打印的结果看上去是精确的 1/10,真正存储的值是最近似的二进制小数。

有趣地是,存在许多不同的十进制数共享着相同的近似二进制小数。例如,数字 0.1 和 0.10000000000000001 以及 0.1000000000000000055511151231257827021181583404541015625 都是 3602879701896397 ** 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,3 个 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”的问题在下面”误差的表示”一节中有准确详细的解释。更完整的常见怪异现象请参见 浮点数的危险

最后我要说,“没有简单的答案”。也不要过分小心浮点数!Python 浮点数计算中的误差源之于浮点数硬件,大多数机器上每次计算误差不超过 2**53 分之一。对于大多数任务这已经足够了,但是你要在心中记住这不是十进制算法,每个浮点数计算可能会带来一个新的舍入错误。

虽然确实有问题存在,对于大多数平常的浮点数运算,你只要简单地将最终显示的结果舍入到你期望的十进制位数,你就会得到你期望的最终结果。str() 通常已经足够用了,对于更好的控制可以参阅 格式化字符串语法 中 str.format() 方法的格式说明符。

对于需要精确十进制表示的情况,可以尝试使用 decimal 模块,它实现的十进制运算适合会计方面的应用和高精度要求的应用。

fractions 模块支持另外一种形式的运算,它实现的运算基于有理数(因此像1/3这样的数字可以精确地表示)。

如果你是浮点数操作的重度使用者,你应该看一下由 SciPy 项目提供的 Numerical Python 包和其它用于数学和统计学的包。参看 <http://scipy.org>。

当你真的  想要知道浮点数精确值的时候,Python 提供这样的工具可以帮助你。float.as_integer_ratio() 方法以分数的形式表示一个浮点数的值:

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

因为比值是精确的,它可以用来无损地重新生成初始值:

>>> x == 3537115888337719 / 1125899906842624
True

float.hex() 方法以十六进制表示浮点数,给出的同样是计算机存储的精确值:

>>> 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

14.1. 表达错误

这一节详细说明 “0.1” 示例,教你怎样自己去精确的分析此类案例。假设这里你已经对浮点数表示有基本的了解。

Representation error 提及事实上有些(实际是大多数)十进制小数不能精确的表示为二进制小数。这是 Python (或 Perl,C,C++,Java,Fortran 以及其它很多)语言往往不能按你期待的样子显示十进制数值的根本原因。

这是为什么? 1/10 不能精确的表示为二进制小数。大多数今天的机器(2000年十一月)使用 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 是保持 J 精度的唯一 N 值。J 最好的近似值是整除的商:

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

因为余数大于 10 的一半,最好的近似是取上界:

>>> q+1
7205759403792794

因此在 754 双精度中 1/10 最好的近似值是是 2**56,或:

7205759403792794 / 2 ** 56

分子和分母都除以2将小数缩小到:

3602879701896397 / 2 ** 55

  

要注意因为我们向上舍入,它其实比 1/10 稍大一点点。如果我们没有向上舍入,它会比 1/10 稍小一点。但是没办法让它 恰好 是 1/10!

所以计算机永远也不 “知道” 1/10:它遇到上面这个小数,给出它所能得到的最佳的 754 双精度实数:

>>> .1 * 2**55
7205759403792794.0

fractions 和 decimal 模块使得这些计算很简单:

>>> 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'

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

 

  

  

posted @ 2018-02-09 22:42  LQ6H  阅读(443)  评论(0编辑  收藏  举报