Як слід робити порівняння з плаваючою точкою?


85

На даний момент я пишу код, де у мене є щось на зразок:

І тоді в інших місцях мені може знадобитися зробити рівність:

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

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

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

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

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


Неможливо зробити це надійно, використовуючи числа з плаваючою комою. Завжди будуть числа, які для комп’ютера рівні, хоча насправді не є (скажімо, 1E + 100, 1E + 100 + 1), і у вас також зазвичай будуть результати розрахунків, які для комп’ютера не рівні, хоча насправді є (див. один із коментарів до відповіді nelhage). Вам доведеться вибрати, кого з двох ви бажаєте менше.
toochin

З іншого боку, якщо ви, скажімо, маєте справу лише з раціональними числами, ви можете реалізувати деяку арифметику раціонального числа на основі цілих чисел, і тоді два числа вважаються рівними, якщо одне з двох чисел можна скасувати до іншого.
toochin

Ну, наразі я працюю із симуляцією. Місце, яке я зазвичай роблю для порівняння, пов’язане із змінними кроками часу (для вирішення деяких од). Є кілька випадків, коли мені потрібно перевірити, чи заданий часовий крок для одного об’єкта дорівнює, менший або більший, ніж крок часу іншого об’єкта.
Mike Bailey


Чому б не використовувати масиви? stackoverflow.com/questions/28318610/…
Адріан П.

Відповіді:


69

Порівняння великих / менших насправді не є проблемою, якщо ви не працюєте прямо на межі поплавкової / подвійної точності.

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

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

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


1
@toochin: залежить від того, наскільки велику похибку ви хочете допустити, але це стає найбільш очевидною проблемою, якщо врахувати денормалізоване число, найближче до нуля, позитивне та негативне - крім нуля, вони ближче, ніж будь-які інші два значення, проте багато наївних реалізацій, заснованих на відносній помилці, вважатимуть їх занадто далекими.
Michael Borgwardt

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).
Michael Borgwardt

1
Якщо ця функція використовується дуже багато (наприклад, кожен кадр відеоігри), я б переписав її у збірці з епічними оптимізаціями.

1
Чудовий путівник та чудова відповідь, особливо враховуючи abs(a-b)<epsвідповіді тут. Два запитання: (1) Чи не було б краще змінити всі <s на <=s, дозволяючи, таким чином, порівняння "нульових значень", еквівалентні точним порівнянням? (2) Чи не краще було б використовувати diff < epsilon * (absA + absB);замість diff / (absA + absB) < epsilon;(останній рядок) -?
Франц Д.

41

TL; DR

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

Графіка, будь ласка?

При порівнянні чисел з плаваючою комою існує два "режими".

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

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

Відносний режим - це те, що використовується для "нормальних" або "досить великих" значень з плаваючою комою. (Докладніше про це пізніше).

Другий - це абсолютний режим, коли ми просто порівнюємо їх різницю з фіксованим числом. Він дає наступний профіль (знову ж epsilonіз 0,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(рівність для даної неточності передбачає рівність при вищій точності)

Запропоноване рішення також підтверджує їх.


1
Це чудова відповідь!
davidhigh

1
Питання про реалізацію c ++: чи може (std::abs(a) + std::abs(b))колись бути більше ніж std::numeric_limits<float>::max()?
anneb

1
@anneb Так, це може бути + INF.
Paul Groke

16

У мене була проблема порівняння чисел із плаваючою комою, A < Bі A > B ось що, здається, працює:

Fabs - абсолютна величина - подбає про те, якщо вони по суті рівні.


1
Не потрібно використовувати fabsвзагалі, якщо ви робите перший тестif (A - B < -Epsilon)
fishinear

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

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

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

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


Що робити, якщо він працює з дійсно малими числами з плаваючою комою, як 2.3E-15?
toochin

1
Я працюю з діапазоном приблизно [10E-14, 10E6], не зовсім машинного епсилону, але дуже близького до нього.
Mike Bailey

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

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

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, не вдається для нескінченностей.
Michael Borgwardt

1
Випадок нескінченності не стосується моєї конкретної програми, але належним чином зазначений.
Mike Bailey

-1

Потрібно взяти до уваги, що помилка усічення є відносною. Дві цифри приблизно рівні, якщо їх різниця приблизно така ж велика, як їх ульп (одиниця останнього місця).

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


-1

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.