Python中和一般情况下的浮点数相等性

18

我有一段代码,根据我是通过字典获取转换因子还是直接使用它们来表现不同的行为。

以下代码将打印1.0 == 1.0 -> False

但如果你用10.0替换factors[units_from],用1.0 / 2.54替换factors[units_to],它将打印1.0 == 1.0 -> True

#!/usr/bin/env python

base = 'cm'
factors = {
    'cm'        : 1.0,
    'mm'        : 10.0,
    'm'         : 0.01,
    'km'        : 1.0e-5,
    'in'        : 1.0 / 2.54,
    'ft'        : 1.0 / 2.54 / 12.0,
    'yd'        : 1.0 / 2.54 / 12.0 / 3.0,
    'mile'      : 1.0 / 2.54 / 12.0 / 5280,
    'lightyear' : 1.0 / 2.54 / 12.0 / 5280 / 5.87849981e12,
}

# Convert 25.4 mm to inches
val = 25.4
units_from = 'mm'
units_to = 'in'

base_value = val / factors[units_from]
ret = base_value * factors[units_to  ]
print ret, '==', 1.0, '->', ret == 1.0

首先,让我说一下我对这里正在发生的事情非常确定。我以前在C中见过它,只是从未在Python中看到过,但由于Python是用C实现的,所以我们看到了它。

我知道浮点数在从CPU寄存器到缓存再返回时会改变值。我知道如果其中一个被分页而另一个保留在寄存器中,比较应该相等的两个变量将返回false。

问题

  • 避免这种问题的最佳方法是什么?... 在Python或一般情况下。
  • 我做错了什么吗?

附注

显然,这是一个简化的示例的一部分,但我试图做的是创建长度、体积等类,可以与同一类别但具有不同单位的其他对象进行比较。

反问

  • 如果这是一个潜在的危险问题,因为它使程序表现出不确定性,编译器是否应该在检测到您正在检查浮点数的相等性时发出警告或错误?
  • 编译器是否应支持选项,将所有浮点数相等性检查替换为“足够接近”的函数?
  • 编译器是否已经这样做了,而我只是找不到信息?

4
你确定你“知道”这个?我知道这是错误的。浮点数只是数据,当它们被移动时并不会损坏。从寄存器到缓存再返回寄存器时,浮点数的值可能会发生变化,但这种变化是由于舍入误差而非数据损坏造成的。如果两个变量应该相等,但其中一个被换出页面而另一个保留在寄存器中,则比较它们会返回false,这是由于它们被分配到了不同的内存位置。 - Seth Johnson
6
在x86处理器上,将80位浮点寄存器截断为64位在存储到内存时是非常普遍的。因此,当它们从寄存器中移出时就会损坏。@Seth Johnson - Mark Ransom
1
@Mark:但是寄存器不会进入缓存,即使它们进入了缓存,也不会因此而损坏。如果FPU寄存器随机更改,那么在x87中编程将是不可能的... - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
3
@Longpoke,我只是认为他把“缓存”这个词用作“内存”的宽泛术语。FPU寄存器不会改变,但编译器可以自由地随意移动数据,这对普通观察者来说看起来是随机的。 - Mark Ransom
8个回答

8
作为比较两个浮点数(或双精度数等)可能存在问题的事情已经被证明。通常情况下,应该检查它们是否在误差范围内,而不是进行精确相等性比较。如果它们在误差范围内,则被视为相等。这比实际操作要容易得多。浮点数的性质使得固定误差范围毫无用处。当值接近0.0时,小误差界限(如2*float_epsilon)效果很好,但如果值接近1000,则会失败。对于像1,000,000.0这样大的值的误差范围,对于接近0.0的值来说则过于宽松。最好的解决方案是了解您的数学领域并根据具体情况选择适当的误差界限。当这种方法不可行或者您懒惰时,“最后一位单位”(ULP)是一个非常新颖和强大的解决方案。完整细节相当复杂,您可以在此处阅读更多信息。
基本思路是这样的,浮点数由尾数和指数两部分组成。通常情况下,舍入误差只会使尾数改变几个步骤。当值接近0.0时,这些步骤恰好为float_epsilon。当浮点值接近1,000,000时,步骤将几乎与1一样大。 Google test 使用ULP来比较浮点数。他们选择了默认的4个ULP来比较两个浮点数是否相等。您也可以使用他们的代码作为参考来构建自己的ULP风格浮点比较器。

6
区别在于,如果你将factors[units_to ]替换为1.0 / 2.54,你就是在执行:
(base_value * 1.0) / 2.54

使用字典,你正在进行:

base_value * (1.0 / 2.54)

舍入的顺序很重要。如果您执行以下操作,则更容易看到:
>>> print (((25.4 / 10.0) * 1.0) / 2.54).__repr__()
1.0
>>> print ((25.4 / 10.0) * (1.0 / 2.54)).__repr__()
0.99999999999999989

请注意,没有非确定性或未定义的行为。有一个标准,IEEE-754,实现必须符合该标准(并不意味着他们总是这样做)。
我认为不应该有自动的足够接近的替代品。那通常是解决问题的有效方法,但应由程序员决定是否以及如何使用它。
最后,当然有任意精度算术的选项,包括python-gmpdecimal。请考虑是否真正需要这些,因为它们确实会对性能产生显著影响。
在常规寄存器和缓存之间移动没有问题。您可能在考虑x86的80位扩展精度

澄清并补充一下:
(9.7321.0)/2.54==9.732(1.0/2.54). ==> 错误
与缓存或寄存器无关。
- Andrew Jaffe
啊,好的,也许是这样。这是一个很好的观察,但并不是一个好的答案,因为问题仍然存在。从数学上讲,它们应该是相同的。我想知道如何避免这种情况发生在未来。 - eric.frederich
起初我并没有意识到我对它们进行了不同的分组,但这仍然是我的一个问题。我无法保证分组或其他变量是否经历了各种变化。再次强调,我的问题仍然存在。 - eric.frederich
谢谢,我会研究一下十进制并看看是否要使用它或者一个 close_enough 函数。 - eric.frederich

4

首先,我建议你阅读David Goldberg的经典著作《计算机科学家应该了解的浮点运算知识》

正如其他评论者所说,你注意到的差异本质上是由浮点模型引起的,与寄存器、缓存或内存无关。

根据浮点模型,2.54实际上被表示为

>>> 2859785763380265 * 2 ** -50
2.54

然而,这种表示并不精确:

>>> from fractions import Fraction
>>> float(Fraction(2859785763380265, 2 ** 50) - Fraction(254, 100))
3.552713678800501e-17

现在,您正在评估的表达式实际上是:
>>> 25.4 / 10 * (1/2.54)
0.99999999999999989

问题在于1/2.54:
>>> Fraction.from_float(1/2.54)
Fraction(1773070719437203, 4503599627370496)

但是你期望的是什么

>>> 1/Fraction.from_float(2.54)
Fraction(1125899906842624, 2859785763380265)

回答你的问题:

  • 这确实是一个困难的问题,但显然是确定性的,没有什么神秘的地方。
  • 你不能自动用“足够接近”的比较代替相等。后者需要指定容差,这取决于手头的问题,即你从结果中期望什么样的精度。还有很多情况下,你真正想要的是相等而不是“足够接近”的比较。

4

感谢您的回复。大多数都很好,提供了不错的链接,所以我就这样说吧,并回答自己的问题。

Caspin发布了这个链接

他还提到Google Tests使用了ULP比较,当我查看谷歌代码时,我看到他们提到了完全相同的链接到Cygnus软件。

我最终实现了一些算法作为Python扩展的C版本,然后后来发现我也可以用纯Python来做。代码如下所示。

最终,我可能只会把ULP差异添加到我的技巧中。

有趣的是看到在两个相等的数字之间有多少浮点数从未离开内存。我阅读的其中一篇文章或者 Google 代码说4是一个很好的数字…但是在这里,我能达到10。

>>> f1 = 25.4
>>> f2 = f1
>>>
>>> for i in xrange(1, 11):
...     f2 /= 10.0          # To cm
...     f2 *= (1.0 / 2.54)  # To in
...     f2 *= 25.4          # Back to mm
...     print 'after %2d loops there are %2d doubles between them' % (i, dulpdiff(f1, f2))
...
after  1 loops there are  1 doubles between them
after  2 loops there are  2 doubles between them
after  3 loops there are  3 doubles between them
after  4 loops there are  4 doubles between them
after  5 loops there are  6 doubles between them
after  6 loops there are  7 doubles between them
after  7 loops there are  8 doubles between them
after  8 loops there are 10 doubles between them
after  9 loops there are 10 doubles between them
after 10 loops there are 10 doubles between them

同样有趣的是,当其中一个数被写成字符串并读回来时,在相等的数字之间有多少浮点数。

>>> # 0 degrees Fahrenheit is -32 / 1.8 degrees Celsius
... f = -32 / 1.8
>>> s = str(f)
>>> s
'-17.7777777778'
>>> # Floats between them...
... fulpdiff(f, float(s))
0
>>> # Doubles between them...
... dulpdiff(f, float(s))
6255L

import struct
from functools import partial

# (c) 2010 Eric L. Frederich
#
# Python implementation of algorithms detailed here...
# From http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm

def c_mem_cast(x, f=None, t=None):
    '''
    Do a c-style memory cast

    In Python...

    x = 12.34
    y = c_mem_cast(x, 'd', 'l')

    ... should be equivalent to the following in c...

    double x = 12.34;
    long   y = *(long*)&x;
    '''
    return struct.unpack(t, struct.pack(f, x))[0]

dbl_to_lng = partial(c_mem_cast, f='d', t='l')
lng_to_dbl = partial(c_mem_cast, f='l', t='d')
flt_to_int = partial(c_mem_cast, f='f', t='i')
int_to_flt = partial(c_mem_cast, f='i', t='f')

def ulp_diff_maker(converter, negative_zero):
    '''
    Getting the ULP difference of floats and doubles is similar.
    Only difference if the offset and converter.
    '''
    def the_diff(a, b):

        # Make a integer lexicographically ordered as a twos-complement int
        ai = converter(a)
        if ai < 0:
            ai = negative_zero - ai

        # Make b integer lexicographically ordered as a twos-complement int
        bi = converter(b)
        if bi < 0:
            bi = negative_zero - bi

        return abs(ai - bi)

    return the_diff

# Double ULP difference
dulpdiff = ulp_diff_maker(dbl_to_lng, 0x8000000000000000)
# Float ULP difference
fulpdiff = ulp_diff_maker(flt_to_int, 0x80000000        )

# Default to double ULP difference
ulpdiff = dulpdiff
ulpdiff.__doc__ = '''
Get the number of doubles between two doubles.
'''

Python中的浮点数字符串不够准确,但repr是准确的。此外,您可以通过循环产生任意大的误差。不确定一个好的参考资料是什么,但可以查找“数值稳定性”。这将帮助您了解误差以及如何为浮点数编写更正确的算法。正如其他人所展示的,浮点数运算并不总是关联的,而普通代数则是,因此顺序变得更加重要。 - YOUR ARGUMENT IS VALID

2

如果我运行这个

x = 0.3+0.3+0.3
if (x != 0.9): print "not equal"
if (x == 0.9): print "equal"

它打印了“不相等”,这是错误的,但是由于

x-0.9

返回浮点误差为-1.11022302e-16。我只是像这样做:

if (x - 0.9 < 10**-8): print "equal (almost)"

否则,我想你可以将它们都转换为字符串:
if (str(x) == str(0.9)): print "equal (strings)"

1
什么是避免这种问题的最佳方法?...在Python或一般情况下。
什么问题?您正在使用物理测量。除非您有一些非常复杂的设备,否则您的测量误差将比浮点epsilon高几个数量级。因此,为什么要编写依赖于数字精确到16个有效数字的代码呢?
编译器是否应支持选项,以将所有浮点等式检查替换为“足够接近”的函数?
如果这样做,您将得到一些奇怪的结果:
>>> float.tolerance = 1e-8    # hypothetical "close enough" definition
>>> a = 1.23456789
>>> b = 1.23456790
>>> c = 1.23456791
>>> a == b
True
>>> b == c
True
>>> a == c
False

如果你觉得在字典中存储浮点数已经很困难了,那么试试使用非传递性==操作符!而且性能会非常糟糕,因为保证x == yhash(x) == hash(y)的唯一方法是让每个浮点数都有相同的哈希码。这将与整数不一致。


1
"float"对象没有"tolerance"属性。 - xApple
2
你不知道“假设”的意思吗? - dan04

0
为了比较浮点数,通常比较浮点数之差的绝对值与所选择的 Delta 相比较,Delta 的值需要足够小以满足你的需求。
反问句:
  • 这是一个危险的问题,如果使用此类比较作为停止条件,则可能隐藏错误或生成无限循环。
  • 现代 C/C++ 编译器会警告浮点数之间的相等比较。
  • 我知道的所有静态代码检查器都会输出我使用的语言的错误信息。
我认为 Python 也一样,因为用于比较的 Delta 可能会有所不同,所以必须由实施者来选择。这意味着不能完全自动地提供良好的默认转换。

0
有意思的是,当一个数以字符串形式写出并读回时,相等的两个数字之间可以有多少个浮点数。

这可能算是Python的一个bug。那个数只用了十二位小数来写出。要唯一地标识64位双精度(Python的浮点类型),你需要十七位小数。如果Python以17位小数的精度打印出数值,那么你将确保得到完全相同的值。

关于精度问题,可以参考以下链接: http://randomascii.wordpress.com/2012/03/08/float-precisionfrom-zero-to-100-digits-2/

该文章主要讨论32位浮点数(每个数字需要九位小数才能唯一标识),但它也简要提到了双精度和需要十七位小数来唯一标识的事实。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接