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

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

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

double a = SomeCalculation1();
double b = SomeCalculation2();

if (a < b)
    DoSomething2();
else if (a > b)
    DoSomething3();

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

double a = SomeCalculation3();
double b = SomeCalculation4();

if (a == 0.0)
   DoSomethingUseful(1 / a);
if (b == 0.0)
   return 0; // or something else here

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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, двумя очень маленькими значениями, противоположными нулю, или бесконечностями.

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

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

Ответ 2

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

if(A - B < Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is less than B");
}

if (A - B > Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is greater than B");
}

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

Ответ 3

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

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!")

Ответ 4

TL; DR

  • Используйте следующую функцию вместо принятого в настоящее время решения, чтобы избежать некоторых нежелательных результатов в определенных предельных случаях, будучи потенциально более эффективным.
  • Знайте ожидаемую неточность, которую вы имеете на своих числах и введите их соответственно в функции сравнения.
bool nearly_equal(
  float a, float b,
  float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
  // those defaults are arbitrary and could be removed
{
  assert(std::numeric_limits<float>::epsilon() <= epsilon);
  assert(epsilon < 1.f);

  if (a == b) return true;

  auto diff = std::abs(a-b);
  auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
  return diff < std::max(relth, epsilon * norm);
}

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

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

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

enter image description here

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

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

enter image description here

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

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

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

enter image description here

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

enter image description here

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

x  = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32

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

nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True

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

enter image description here

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

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

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

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

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

Это причина для введения параметра relth выше.

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

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

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

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

Следующее довольно очевидно:

  • само равенство: 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 (равенство для данной неточности подразумевает равенство при большей неточности)

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

Ответ 5

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

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

Ответ 6

Адаптация к PHP от Майкла Боргвардта и босоникс:

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

Ответ 7

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

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

static bool fpEqual(double a, double b)
{
    double diff = Math.Abs(a - b);
    double epsilon = Math.Max(Math.Abs(a), Math.Abs(b)) * Double.Epsilon;
    return (diff < epsilon);
}

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

Ответ 8

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

Чтобы привести пример: если ваша цель состоит в том, чтобы нарисовать график на экране, то вам, скорее всего, понадобятся значения с плавающей запятой, чтобы сравнить их, если они сопоставляются с одним и тем же пикселем на экране. Если размер вашего экрана составляет 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;
}

Ответ 9

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

#define EPSILON 0.00000001

if ((a - b) < EPSILON && (b - a) < EPSILON) {
  printf("a and b are about equal\n");
}

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

Ответ 10

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

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

Ответ 11

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

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

<?php

while(1) {
  $time = time();
  $millis = $time  * 1000;
  $hours = $millis / 3600000;
  $mins  = $hours  * 60; 
  //$secs  = (int)($mins * 60);
  $secs  = ($mins * 60);

  printf("gettype(time) == %s\n", gettype($time));
  printf("gettype(secs) == %s\n", gettype($secs));
  printf("$time == $secs\n", $time, $secs);
  printf("%d == %d\n", $time, $secs );
  printf("%f == %f\n", $time, $secs );
  printf("%.1f == %.1f\n", $time, $secs );
  printf("%.10f == %.10f\n", $time, $secs );
  printf("%.10F == %.10F\n", $time, $secs );
  printf("%b == %b\n", $time, $secs );

  assert($secs == $time);
  if($secs != $time) {
    break;
  }
}

Ответ 12

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

double eps = 0.000000001; //for instance

double a = someCalc1();
double b = someCalc2();

double diff = Math.abs(a - b);
if (diff < eps) {
    //equal
}

Ответ 13

Я недавно сталкивался с подобной проблемой и проводил некоторые тесты.

В некоторых случаях, если два поплавков имеют одинаковое строковое значение, они будут сравниваться одинаково при сравнении с float (i..e, (float)$float1 === (float)$float2), независимо от того, как они были получены. Однако в других случаях, даже если два поплавка имеют одинаковое строковое значение, они иногда возвращаются как сравнительно разные при сравнении с плавающей точкой, если они были получены по-разному.

См. приведенный ниже пример:

$float1 = 0.04 + 0.02;
$float2 = 0.04 + 0.01 + 0.01;
$float3 = 0.03 + 0.03;

echo 'Values:';
var_dump($float1); echo '<br>';
var_dump($float2); echo '<br>';
var_dump($float3); echo '<br><br>';

echo 'Comparisons:';
var_dump($float1 - $float2); echo '<br>';
var_dump($float2 - $float3); echo '<br>';
var_dump($float1 - $float3); echo '<br>';

Выполнить на PHP 5.3, вот результат:

значения:

float (0.06)

float (0.06)

float (0.06)

Сравнения:

float (-6.93889390391E-18)

float (6.93889390391E-18)

поплавок (0)

Итак, как вы можете видеть, $float2 не совпадает с $float1 и $float3 при сравнении с float. Единственное различие между ними заключается в том, как они были получены. Вы могли бы подумать, что имеет смысл предположить, что неважно, как было выведено float, только то, что его конечное значение, но из приведенного выше примера вы можете видеть, что это плохое предположение.

Фактическое различие настолько незначительно, что это не имеет большого значения для расчета, но при их сравнении важно.

Если вы хотите сравнить float как float уверенно, вот что я предлагаю перед любым сравнением:

$float1 = 0.04 + 0.02;
$float2 = 0.04 + 0.01 + 0.01;
$float3 = 0.03 + 0.03;

//Cast to string, then back to float
$float1 = (float)(string)$float1;
$float2 = (float)(string)$float2;
$float3 = (float)(string)$float3;

echo 'Values:';
var_dump($float1); echo '<br>';
var_dump($float2); echo '<br>';
var_dump($float3); echo '<br><br>';

echo 'Comparisons:';
var_dump($float1 - $float2); echo '<br>';
var_dump($float2 - $float3); echo '<br>';
var_dump($float1 - $float3); echo '<br>';

значения:

float (0.06)

float (0.06)

float (0.06)

Сравнения:

float (0)

float (0)

поплавок (0)

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

Надеюсь, что это поможет.

EDIT:

Я только что наткнулся на BC (двоичный расчет) Math Functions на php.net. Кажется, что они достигают того же, что и выше, однако они возвращают значения строк, поэтому, если вы хотите поплавки, убедитесь, что впоследствии вы возвращаетесь к поплавкам. Здесь документация: http://php.net/manual/en/ref.bc.php

См. ниже:

echo '$float1 - $float2 = '; var_dump(bcsub($float1,$float2,2)); echo '<br>';
echo '$float2 - $float3 = '; var_dump(bcsub($float2,$float3,2)); echo '<br>';
echo '$float1 - $float3 = '; var_dump(bcsub($float1,$float3,2)); echo '<br>';

Возврат:

$float1 - $float2 = строка (4) "0.00"

$float2 - $float3 = строка (4) "0.00"

$float1 - $float3 = строка (4) "0.00"