Подтвердить что ты не робот

С плавающей точкой в ​​Python и в целом

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

Следующий фрагмент кода напечатает 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 реализовано в C. Мы видим это.

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

Вопросы

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

Боковое примечание

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

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

  • Если это потенциально опасная проблема, поскольку она заставляет программы работать в неопределенном вопросе, должны ли компиляторы предупреждать или ошибочно, когда обнаруживают, что вы проверяете равенство поплавков.
  • Если компиляторы поддерживают возможность замены всех проверок равенства float с помощью функции "достаточно близко"?
  • Компиляторы уже делают это, и я просто не могу найти информацию.
4b9b3361

Ответ 1

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

Caspin разместил ссылку .

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

Я запустил реализацию некоторых из алгоритмов в 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.
'''

Ответ 2

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

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

Лучшим решением является знание области вашей математики и выбор подходящей ошибки в каждом конкретном случае.

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

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

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

Ответ 3

Разница в том, что если вы замените 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

Обратите внимание, что нет никакого детерминированного или undefined поведения. Существует стандарт IEEE-754, реализация которого должна соответствовать (не претендовать на то, что они всегда делают).

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

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

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

Ответ 4

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

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

В соответствии с моделью с плавающей запятой 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)

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

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

Ответ 5

Каков наилучший способ избежать проблем? как это?... В 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 иметь одинаковый хэш-код. И это будет несовместимо с ints.

Ответ 6

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

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

он печатает "не равно", что неверно, но как

x-0.9

дает ошибку float как -1.11022302e-16, я просто делаю что-то вроде этого:

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

иначе вы можете конвертировать оба в строки, я думаю:

if (str(x) == str(0.9)): print "equal (strings)"

Ответ 7

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

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

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

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

Ответ 8

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

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

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

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