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


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

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

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

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

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

12 68

12 ответов:

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

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

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

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

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

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

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

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|. При построении графика в 2D он дает следующий профиль, где зеленый означает равенство x и y. (Я взял epsilon 0.5 для иллюстрации).

enter image description here

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

второй элемент абсолютное режим, когда мы просто сравниваем их разницу с фиксированным числом. Он дает следующий профиль (снова с epsilon 0,5 и a 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|, который представлен заштрихованным квадратом ниже. Это гарантирует, что обе зоны соединяются изящно.

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 находится в порядке неточности ваших вычислений - и редко это так низко, как FLT_MIN.

этот является причиной для введения

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

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

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

#define EPSILON 0.00000001

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

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

адаптация к PHP от Michael Borgwardt & bosonix's answer:

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

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

double eps = 0.000000001; //for instance

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

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

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

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

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

изменить: изменение от математики.Макс (a, b) к математике.Макс (Математика.АБС (а), математика.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);
}

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

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

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

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

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

для сравнения:

поплавок (-6.93889390391 E-18)

поплавок (6.93889390391 E-18)

float (0)

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

фактическая разница настолько мизерна, что на самом деле это не имеет значения для проведения расчетов, но это имеет значение при сравнении их.

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

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

float (0)

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

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

EDIT:

Я только что столкнулся с математическими функциями BC (binary calculation) 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 = string(4)" 0.00"

$float2 - $float3 = string(4)" 0.00"

$float1 - $float3 = string(4)"0.00"