Как мне проводить сравнение с плавающей запятой?

85

В настоящее время я пишу код, в котором есть что-то вроде:

А потом в других местах мне может понадобиться выполнить равенство:

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

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

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

Диапазон чисел, который я использую, составляет примерно от 10E-14 до 10E6, поэтому мне нужно работать как с маленькими, так и с большими числами.

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

Майк Бейли
источник
Невозможно сделать это надежно при использовании чисел с плавающей запятой. Всегда будут числа, которые для компьютера равны, хотя на самом деле это не так (скажем, 1E + 100, 1E + 100 + 1), и у вас также обычно будут результаты вычислений, которые для компьютера не равны, хотя на самом деле таковы (см. один из комментариев к ответу Нелхаге). Вам нужно будет выбрать, какой из двух вы хотите меньше.
toochin 06
С другой стороны, если вы, скажем, имеете дело только с рациональными числами, вы можете реализовать некоторую арифметику рациональных чисел, основанную на целых числах, и тогда два числа будут считаться равными, если одно из двух чисел может быть сокращено до другого.
toochin 06
Что ж, сейчас я работаю над симуляцией. Место, где я обычно делаю эти сравнения, связано с переменными временными шагами (для решения какой-то оды). Есть несколько случаев, когда мне нужно проверить, равен ли данный временной шаг для одного объекта, меньше или больше, чем временной шаг другого объекта.
Майк Бейли,
Почему не использовать массивы? stackoverflow.com/questions/28318610/…
Адриан П.

Ответы:

69

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

Для сравнения «нечеткое равенство» это (Java-код, который должен быть легко адаптируемым) - это то, что я придумал для The Floating-Point Guide после большой работы и с учетом множества критических замечаний:

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

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

Альтернативой (см. Ссылку выше для получения дополнительной информации) является преобразование битовых шаблонов чисел с плавающей запятой в целые числа и принятие всего в пределах фиксированного целочисленного расстояния.

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

Майкл Боргвардт
источник
1
@toochin: зависит от того, насколько велика погрешность, которую вы хотите учесть, но это становится наиболее очевидной проблемой, когда вы рассматриваете денормализованное число, наиболее близкое к нулю, положительное и отрицательное - кроме нуля, они ближе друг к другу, чем любые другие два значения, но многие наивные реализации, основанные на относительной ошибке, будут считать их слишком далекими друг от друга.
Майкл Боргвардт
2
Хм. У вас есть тест else if (a * b == 0), но тогда ваш комментарий в той же строке есть a or b or both are zero. Но разве это не две разные вещи? Например, если a == 1e-162и b == 2e-162тогда условие a * b == 0будет истинным.
Марк Дикинсон
1
@toochin: в основном потому, что код должен быть легко переносимым на другие языки, которые могут не иметь этой функциональности (он был добавлен в Java только в версии 1.5).
Майкл Боргвардт
1
Если эта функция используется очень часто (например, каждый кадр видеоигры), я бы переписал ее в сборке с эпической оптимизацией.
1
Отличное руководство и отличный ответ, особенно учитывая abs(a-b)<epsответы здесь. Два вопроса: (1) Не лучше ли заменить все <s на <=s, что позволит проводить сравнения «ноль-eps», эквивалентные точным сравнениям? (2) Не лучше ли использовать diff < epsilon * (absA + absB);вместо diff / (absA + absB) < epsilon;(последняя строка) -?
Franz D.
41

TL; DR

  • Используйте следующую функцию вместо принятого в настоящее время решения, чтобы избежать некоторых нежелательных результатов в определенных предельных случаях, но при этом потенциально более эффективно.
  • Знайте ожидаемую неточность ваших чисел и соответствующим образом скормите их в функции сравнения.

Графика, пожалуйста?

При сравнении чисел с плавающей запятой есть два «режима».

Первый - относительный режим, в котором разница между xи yсчитается относительно их амплитуды |x| + |y|. При построении 2D-графика получается следующий профиль, где зеленый цвет означает равенство xи y. (Я взял epsilon0,5 для иллюстрации).

введите описание изображения здесь

Относительный режим - это то, что используется для "нормальных" или "достаточно больших" значений с плавающей запятой. (Подробнее об этом позже).

Второй - абсолютный режим, когда мы просто сравниваем их разницу с фиксированным числом. Это дает следующий профиль (снова с epsilon0,5 и relth1 для иллюстрации).

введите описание изображения здесь

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

Теперь вопрос в том, как нам соединить эти два образца ответа.

В ответе Майкла Боргвардта переключатель основан на значении diff, которое должно быть ниже relth( Float.MIN_NORMALв его ответе). Эта зона переключения показана штриховкой на графике ниже.

введите описание изображения здесь

Поскольку relth * epsilonэто меньше relth, зеленые пятна не слипаются, что, в свою очередь, придает решению плохое свойство: мы можем найти тройки чисел такие, что x < y_1 < y_2и все же x == y2но x != y1.

введите описание изображения здесь

Вот яркий пример:

У нас есть x < y1 < y2, а на самом деле y2 - xболее чем в 2000 раз больше y1 - x. И все же с текущим решением,

Напротив, в предложенном выше решении зона переключения основана на значении |x| + |y|, которое представлено ниже заштрихованным квадратом. Это гарантирует изящное соединение обеих зон.

введите описание изображения здесь

Кроме того, в приведенном выше коде нет ветвления, что могло бы быть более эффективным. Учтите, что такие операции, как maxи abs, которые априори требуют ветвления, часто имеют специальные инструкции по сборке. По этой причине я думаю, что этот подход превосходит другое решение, которое заключалось бы в том, чтобы исправить ошибку Майкла, nearlyEqualизменив переключатель с diff < relthна diff < eps * relth, что затем дало бы по существу тот же шаблон ответа.

Где переключаться между относительным и абсолютным сравнением?

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

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

Это довольно очевидно, если учесть сравнение с плавающей запятой 0. Здесь любое относительное сравнение не удастся, потому что |x - 0| / (|x| + 0) = 1. Таким образом, сравнение должно переключаться в абсолютный режим, когда xэто порядка неточности ваших вычислений - и редко бывает так мало, как FLT_MIN.

Это причина введения указанного relthвыше параметра.

Кроме того, без умножения relthна epsilon, интерпретация этого параметра проста и соответствует уровню числовой точности, который мы ожидаем от этих чисел.

Математическое урчание

(хранится здесь в основном для собственного удовольствия)

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

Достаточно очевидны следующие утверждения:

  • самодостаточность: a =~ a
  • симметрия: a =~ bподразумеваетb =~ a
  • инвариантность по оппозиции: a =~ bподразумевает-a =~ -b

(У нас нет a =~ bи b =~ cподразумевается a =~ c, что =~это не отношение эквивалентности).

Я бы добавил следующие свойства, которые более специфичны для сравнений с плавающей запятой

  • если a < b < c, то a =~ cподразумевает a =~ b(более близкие значения также должны быть равны)
  • если a, b, m >= 0тогда a =~ bподразумевает a + m =~ b + m(большие значения с той же разницей также должны быть равны)
  • если 0 <= λ < 1тогда a =~ bподразумевает λa =~ λb(возможно, менее очевидный аргумент в пользу).

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

Если представить =~себе семью отношений равенства, =~[Ɛ,t]параметризованных с помощью Ɛи relth, можно также добавить

  • если Ɛ1 < Ɛ2то a =~[Ɛ1,t] bподразумевает a =~[Ɛ2,t] b(равенство для данного допуска подразумевает равенство при более высоком допуске)
  • если t1 < t2тогда a =~[Ɛ,t1] bподразумевает a =~[Ɛ,t2] b(равенство для данной неточности означает равенство с большей погрешностью)

Предлагаемое решение также подтверждает это.

P-Gn
источник
1
Это отличный ответ!
davidhigh
1
Вопрос реализации c ++: может (std::abs(a) + std::abs(b))быть больше чем std::numeric_limits<float>::max()?
аннеб
1
@anneb Да, это может быть + INF.
Пол Грок
16

У меня возникла проблема сравнения чисел с плавающей запятой, A < Bи A > B вот что, похоже, работает:

Fabs - абсолютная ценность - заботится о том, равны ли они по существу.

tech_loafer
источник
1
fabsif (A - B < -Epsilon)
Вообще
11

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

final float TOLERANCE = 0.00001;
if (Math.abs(f1 - f2) < TOLERANCE)
    Console.WriteLine("Oh yes!");

Одна запись. Ваш пример довольно забавен.

double a = 1.0 / 3.0;
double b = a + a + a;
if (a != b)
    Console.WriteLine("Oh no!");

Немного математики здесь

a = 1/3
b = 1/3 + 1/3 + 1/3 = 1.

1/3 != 1

О да..

Ты имеешь ввиду

if (b != 1)
    Console.WriteLine("Oh no!")
nni6
источник
3

Идея, которая у меня была для сравнения с плавающей запятой в быстром

infix operator ~= {}

func ~= (a: Float, b: Float) -> Bool {
    return fabsf(a - b) < Float(FLT_EPSILON)
}

func ~= (a: CGFloat, b: CGFloat) -> Bool {
    return fabs(a - b) < CGFloat(FLT_EPSILON)
}

func ~= (a: Double, b: Double) -> Bool {
    return fabs(a - b) < Double(FLT_EPSILON)
}
Энди По
источник
1

Адаптация к PHP от Майкла Боргвардта и ответ bosonix:

class Comparison
{
    const MIN_NORMAL = 1.17549435E-38;  //from Java Specs

    // from http://floating-point-gui.de/errors/comparison/
    public function nearlyEqual($a, $b, $epsilon = 0.000001)
    {
        $absA = abs($a);
        $absB = abs($b);
        $diff = abs($a - $b);

        if ($a == $b) {
            return true;
        } else {
            if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) {
                return $diff < ($epsilon * self::MIN_NORMAL);
            } else {
                return $diff / ($absA + $absB) < $epsilon;
            }
        }
    }
}
Деннис
источник
1

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

Приведу пример: если ваша цель состоит в том, чтобы нарисовать график на экране, то вы, вероятно, захотите, чтобы значения с плавающей запятой сравнивались одинаково, если они отображаются в один и тот же пиксель на экране. Если размер вашего экрана составляет 1000 пикселей, а ваши числа находятся в диапазоне 1e6, то вы, вероятно, захотите, чтобы 100 для сравнения было равно 200.

При требуемой абсолютной точности алгоритм становится:

public static ComparisonResult compare(float a, float b, float accuracy) 
{
    if (isnan(a) || isnan(b))   // if NaN needs to be supported
        return UNORDERED;    
    if (a == b)                 // short-cut and takes care of infinities
        return EQUAL;           
    if (abs(a-b) < accuracy)    // comparison wrt. the accuracy
        return EQUAL;
    if (a < b)                  // larger / smaller
        return SMALLER;
    else
        return LARGER;
}
рыбоводный
источник
0

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

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

Nelhage
источник
Что, если он работает с очень маленькими числами с плавающей запятой, например 2.3E-15?
toochin 06
1
Я работаю с диапазоном примерно [10E-14, 10E6], не совсем машинный эпсилон, но очень близкий к нему.
Майк Бейли,
2
Работа с небольшими числами не проблема, если помнить, что приходится работать с относительными ошибками. Если вас не интересуют относительно большие допуски на ошибки, то приведенное выше было бы нормально, если бы вы заменили это условие чем-то вродеif ((a - b) < EPSILON/a && (b - a) < EPSILON/a)
toochin
2
Приведенный выше код также проблематичен, когда вы имеете дело с очень большими числами c, потому что, как только ваше число станет достаточно большим, EPSILON будет меньше, чем машинная точность c. Например, предположим c = 1E+22; d=c/3; e=d+d+d;. Тогда e-cвполне может быть значительно больше 1.
Toochin 06
1
Например, попробуйте double a = pow(8,20); double b = a/7; double c = b+b+b+b+b+b+b; std::cout<<std::scientific<<a-c;(a и c не равны согласно pnt и nelhage) или double a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;(a и b равны согласно pnt и nelhage)
toochin 06
0

Я попытался написать функцию равенства с учетом приведенных выше комментариев. Вот что я придумал:

Изменить: перейти с Math.Max ​​(a, b) на Math.Max ​​(Math.Abs ​​(a), Math.Abs ​​(b))

Мысли? Мне все еще нужно проработать больше и меньше.

Майк Бейли
источник
epsilonдолжно быть Math.abs(Math.Max(a, b)) * Double.Epsilon;, или всегда будет меньше, чем diffдля отрицательного aи b. И я думаю, что ваш epsilonслишком мал, функция может не возвращать ничего, отличного от ==оператора. Больше, чем есть a < b && !fpEqual(a,b).
toochin
1
Не удается, когда оба значения равны точно нулю, не удается для Double.Epsilon и -Double.Epsilon, не удается для бесконечностей.
Майкл Боргвардт
1
Случай бесконечностей не является предметом моего внимания в моем конкретном приложении, но он должным образом отмечен.
Майк Бейли
-1

При этом нужно учитывать, что ошибка усечения относительная. Два числа примерно равны, если их разница примерно равна их ulp (Unit на последнем месте).

Однако, если вы выполняете вычисления с плавающей запятой, ваш потенциал ошибки возрастает с каждой операцией (особенно осторожно с вычитаниями!), Поэтому ваша устойчивость к ошибкам должна соответственно увеличиваться.

Точин
источник
-1

Лучший способ сравнить двойники на предмет равенства / неравенства - это взять абсолютное значение их разности и сравнить его с достаточно небольшим (в зависимости от вашего контекста) значением.

pnt
источник