равенство с плавающей точкой в ​​Python и вообще - PullRequest
16 голосов
/ 16 июня 2010

У меня есть фрагмент кода, который ведет себя по-разному, в зависимости от того, просматриваю ли я словарь для получения коэффициентов преобразования или использую ли я их напрямую.

Следующий фрагмент кода напечатает 1.0 == 1.0 -> False

Но если вы замените factors[units_from] на 10.0 и factors[units_to ] на 1.0 / 2.54, то будет напечатано 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, мы видим это.

Я знаю, что числа с плавающей запятой изменят значения, переходящие из регистра ЦП в кэш и обратно.Я знаю, что сравнение двух одинаковых переменных вернет false, если одна из них была выгружена, а другая осталась в регистре.

Вопросы

  • Каков наилучший способ избежать подобных проблем? ... В Python или вообще.
  • Я что-то делаю не так?

Примечание на стороне

Это, очевидно, часть урезанного примера, но я пытаюсь сделать это с классами длины, объема и т. Д., Которые можно сравнивать с другими объектами того же класса, но с разными единицами.

Риторические вопросы

  • Если это потенциально опасная проблема, поскольку она заставляет программы вести себя недетерминистски, должны ли компиляторы предупреждать или сообщать об ошибке, когда они обнаруживают, что выВы проверяете равенство с плавающей точкой
  • Должны ли компиляторы поддерживать возможность замены всех проверок на равенство с плавающей точкой с помощью функции «достаточно близко»?
  • У компиляторов уже dо, и я просто не могу найти информацию.

Ответы [ 8 ]

7 голосов
/ 16 июня 2010

Как было показано, сравнение двух чисел с плавающей запятой (или двойных и т. Д.) Может быть проблематичным.Как правило, вместо сравнения на точное равенство их следует проверять на наличие ошибок.Если они находятся в пределах ошибки, они считаются равными.

Это гораздо легче сказать, чем сделать.Природа с плавающей запятой делает фиксированную ошибку связанной бесполезной.Небольшая граница ошибки (например, 2 * float_epsilon) хорошо работает, когда значения близки к 0,0, но не срабатывает, если значение близко к 1000. Граница ошибки для значений, больших 1 000 000,0, будет слишком слабой для значений, близких к 0,0.

Лучшее решение - знать область вашей математики и выбирать ошибочную оценку, привязанную к конкретному случаю.

Если это нецелесообразно или вы ленитесь, Единицы вLast Place (ULPs) - очень новое и надежное решение.Полная информация довольно сложна, вы можете прочитать больше здесь .

Основная идея такова: число с плавающей запятой состоит из двух частей: мантиссы и экспоненты.Обычно ошибки округления изменяют мантиссу только на несколько шагов .Когда значение около 0,0, эти шаги в точности равны float_epsilon.Когда значение с плавающей запятой ближе к 1 000 000, количество шагов будет почти равно 1.

Google test использует ULP для сравнения чисел с плавающей запятой .Они выбрали значение по умолчанию 4 ULP для двух чисел с плавающей запятой, которые будут сравниваться равными.Вы также можете использовать их код в качестве ссылки для создания своего собственного компаратора типа float ULP.

6 голосов
/ 16 июня 2010

Разница в том, что если вы замените 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, которому должны соответствовать реализации (а не утверждать, что они всегда do ).

Не думаю, что должна быть автоматическая достаточно близкая замена. Часто это эффективный способ решения проблемы, но программист должен решить, использовать ли его и как и как.

Наконец, есть, конечно, варианты для арифметики произвольной точности, включая python-gmp и десятичное . Подумайте, действительно ли вам нужно , потому что они оказывают существенное влияние на производительность.

Нет проблем с перемещением между обычными регистрами и кешем. Вы можете подумать о 80-битной расширенной точности x86 .

4 голосов
/ 17 июня 2010

Спасибо за ваши ответы.Большинство из них были очень хорошими и содержали хорошие ссылки, поэтому я просто скажу это и отвечу на свой вопрос.

Каспин разместил эту ссылку .

Он также упомянул, что Google Testsиспользовал сравнение ULP, и когда я посмотрел на код Google, я увидел, что они упомянули ту же самую точную ссылку на cygnus-software.

В итоге я реализовал некоторые алгоритмы на C как расширение Python, а затем обнаружил, чтоЯ мог бы сделать это и на чистом 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 equivilent 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.
'''
4 голосов
/ 16 июня 2010

Позвольте мне сначала ответить, сказав, что вы должны прочитать классику Дэвида Голдберга Что должен знать каждый учёный-компьютерщик об арифметике с плавающей точкой .

Как уже говорили некоторые другие комментаторы, несоответствие, которое вы замечаете, связано с моделью с плавающей запятой и не имеет ничего общего с регистрами, кешем или памятью.

Согласно модели с плавающей точкой, 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)

Чтобы ответить на ваши вопросы:

  • Это - это трудная проблема, но явно детерминированная, ничего таинственного там нет.
  • Вы не можете автоматически заменить равенство сравнением достаточно близко . Последнее требует, чтобы вы указали допуск, который зависит от имеющейся проблемы, то есть от того, какой точности вы ожидаете от своих результатов. Есть также множество ситуаций, когда вам действительно нужно равенство, а не сравнение достаточно близко .
2 голосов
/ 16 мая 2012

если я запускаю это

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 голос
/ 16 июня 2010

Как лучше всего избежать проблем? как это? ... В Python или вообще.

Какая проблема? Вы работаете с физическими измерениями. Если у вас нет действительно сложного оборудования, погрешность ваших измерений будет на несколько порядков выше, чем у эпсилона с плавающей точкой. Так зачем писать код, который зависит от числа, которое должно быть точным до 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) состоит в том, чтобы каждый float имел одинаковый хэш-код. И это было бы несовместимо с целыми числами.

0 голосов
/ 16 июля 2012

Также интересно то, сколько существует плавающих точек между равными числами, когда одна из них записывается в виде строки и считывается обратно.

Это, возможно, ошибка Python.Это число было записано всего двенадцатью цифрами.Два уникально идентифицируют 64-битный двойник (тип с плавающей точкой Python), вам нужно семнадцать цифр мантиссы.Если Python напечатает свои числа с точностью до 17 цифр, то вы гарантированно получите точно такое же значение.

Проблема точности обсуждается по адресу: http://randomascii.wordpress.com/2012/03/08/float-precisionfrom-zero-to-100-digits-2/

Основное внимание уделяется32-разрядное число с плавающей запятой (для уникальной идентификации каждого номера требуется девять цифр мантиссы), но в нем кратко упоминается двойная цифра и тот факт, что для нее требуется 17 цифр мантиссы.

0 голосов
/ 16 июня 2010

Для общего сравнения поплавков сравните абсолютное значение разности поплавков с выбранной дельтой, которое достаточно мало, чтобы соответствовать вашим потребностям.

Риторические вопросы

  • Это ** опасная проблема **, поскольку она может скрывать ошибки или генерировать бесконечные циклы, если такое сравнение используется в качестве критерия остановки.
  • Современные компиляторы C / C ++ предупреждают о сравнении чисел с плавающей точкой
  • Все известные мне статические программы проверки кода будут выводить ошибки для языков, которые я использую

Я полагаю, что это то же самое для Python, так как дельта, используемая для сравнения, может варьироваться, она должна зависеть от реализатора, чтобы выбрать его. Это означает, что ни одно хорошее преобразование по умолчанию не может быть предоставлено полностью автоматически.

...