Jak mam zrobić porównanie zmiennoprzecinkowe?

85

Obecnie piszę kod, w którym mam coś w stylu:

A potem w innych miejscach być może będę musiał zrobić równość:

Krótko mówiąc, mam dużo matematyki zmiennoprzecinkowej i muszę robić różne porównania warunków. Nie mogę przekształcić tego w matematykę liczb całkowitych, ponieważ w tym kontekście taka rzecz jest bez znaczenia.

Czytałem wcześniej, że porównania zmiennoprzecinkowe mogą być niewiarygodne, ponieważ możesz mieć takie rzeczy:

Krótko mówiąc, chciałbym wiedzieć: jak mogę wiarygodnie porównywać liczby zmiennoprzecinkowe (mniejsze niż, większe niż, równość)?

Zakres liczb, których używam, wynosi z grubsza od 10E-14 do 10E6, więc muszę pracować zarówno z małymi, jak i dużymi liczbami.

Oznaczyłem to jako język agnostyk, ponieważ interesuje mnie, jak mogę to osiągnąć bez względu na język, którego używam.

Mike Bailey
źródło
Nie ma sposobu, aby to zrobić niezawodnie, używając liczb zmiennoprzecinkowych. Zawsze będą liczby, które dla komputera są równe, chociaż w rzeczywistości nie są (powiedzmy 1E + 100, 1E + 100 + 1), a także zazwyczaj otrzymasz wyniki obliczeń, które dla komputera nie są równe, chociaż w rzeczywistości są (patrz jeden z komentarzy do odpowiedzi Nelhage). Będziesz musiał wybrać, którego z dwóch pragniesz mniej.
toochin
Z drugiej strony, jeśli, powiedzmy, zajmujesz się tylko liczbami wymiernymi, możesz zaimplementować arytmetykę liczb wymiernych opartą na liczbach całkowitych, a wtedy dwie liczby są uważane za równe, jeśli jedną z dwóch liczb można anulować do drugiej.
toochin
Cóż, obecnie pracuję nad symulacją. Miejsce, w którym zwykle robię te porównania, jest związane ze zmiennymi krokami czasowymi (do rozwiązania jakiejś ody). Jest kilka przypadków, w których muszę sprawdzić, czy dany krok czasu dla jednego obiektu jest równy, mniejszy lub większy niż krok czasu innego obiektu.
Mike Bailey
Dlaczego nie używać tablic? stackoverflow.com/questions/28318610/…
Adrian P.

Odpowiedzi:

69

Porównywanie dla większego / mniejszego nie jest tak naprawdę problemem, chyba że pracujesz tuż przy granicy pływaka / podwójnej precyzji.

Aby porównać „rozmyte równa się”, to (kod Java powinien być łatwy do dostosowania) jest tym, co wymyśliłem dla Przewodnika zmiennoprzecinkowego po wielu pracach i biorąc pod uwagę wiele krytyki:

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

Jest dostarczany z zestawem testowym. Powinieneś natychmiast odrzucić każde rozwiązanie, które tego nie robi, ponieważ praktycznie gwarantuje się niepowodzenie w niektórych skrajnych przypadkach, takich jak jedna wartość 0, dwie bardzo małe wartości przeciwne do zera lub nieskończoność.

Alternatywą (patrz link powyżej, aby uzyskać więcej informacji) jest konwersja wzorców bitowych elementów zmiennoprzecinkowych na liczbę całkowitą i akceptowanie wszystkiego w ustalonej odległości całkowitej.

W każdym razie prawdopodobnie nie ma rozwiązania, które byłoby idealne do wszystkich zastosowań. Idealnie byłoby, gdybyś opracował / dostosował własny zestaw testów obejmujący rzeczywiste przypadki użycia.

Michael Borgwardt
źródło
1
@toochin: zależy od tego, jak duży margines błędu chcesz dopuścić, ale staje się to najbardziej oczywistym problemem, gdy weźmie się pod uwagę zdenormalizowaną liczbę najbliższą zeru, dodatnią i ujemną - poza zerem są one bliżej siebie niż dwie pozostałe wartości, ale wiele naiwnych implementacji opartych na względnym błędzie uzna je za zbyt daleko od siebie.
Michael Borgwardt,
2
Hmm. Masz test else if (a * b == 0), ale twój komentarz w tej samej linii jest a or b or both are zero. Ale czy to nie są dwie różne rzeczy? Np. Jeśli a == 1e-162i b == 2e-162wtedy warunek a * b == 0będzie prawdziwy.
Mark Dickinson,
1
@toochin: głównie dlatego, że kod ma być łatwy do przenoszenia na inne języki, które mogą nie mieć takiej funkcjonalności (został dodany do Javy tylko w 1.5).
Michael Borgwardt
1
Jeśli ta funkcja jest używana bardzo często (na przykład każda klatka gry wideo), przepisałbym ją w asemblerze z epickimi optymalizacjami.
1
Świetny przewodnik i świetna odpowiedź, szczególnie biorąc pod uwagę abs(a-b)<epsodpowiedzi tutaj. Dwa pytania: (1) Czy nie byłoby lepiej zmienić wszystkie <s na <=s, umożliwiając w ten sposób porównania „zero-eps”, równoważne porównaniom dokładnym? (2) Czy nie byłoby lepiej użyć diff < epsilon * (absA + absB);zamiast diff / (absA + absB) < epsilon;(ostatnia linia) -?
Franz D.
41

TL; DR

  • Użyj poniższej funkcji zamiast obecnie przyjętego rozwiązania, aby uniknąć niektórych niepożądanych wyników w pewnych przypadkach granicznych, a jednocześnie być potencjalnie bardziej wydajnym.
  • Poznaj oczekiwaną niedokładność swoich liczb i odpowiednio je podawaj w funkcji porównania.

Grafika, proszę?

Przy porównywaniu liczb zmiennoprzecinkowych istnieją dwa „tryby”.

Pierwszym z nich jest tryb względny , w którym różnica między xi yjest rozpatrywana w stosunku do ich amplitudy |x| + |y|. Przy rysowaniu w 2D daje następujący profil, gdzie kolor zielony oznacza równość xi y. (Wziąłem epsilon0,5 dla celów ilustracyjnych).

wprowadź opis obrazu tutaj

Tryb względny jest używany dla „normalnych” lub „dostatecznie dużych” wartości zmiennoprzecinkowych. (Więcej o tym później).

Drugi to tryb absolutny , kiedy po prostu porównujemy ich różnicę do ustalonej liczby. Daje następujący profil (ponownie z epsilon0,5 i relth1 dla ilustracji).

wprowadź opis obrazu tutaj

Ten absolutny tryb porównania jest używany dla „małych” wartości zmiennoprzecinkowych.

Teraz pytanie brzmi, jak połączyć te dwa wzory odpowiedzi.

W odpowiedzi Michaela Borgwardta przełącznik opiera się na wartości diff, która powinna być poniżej relth( Float.MIN_NORMALw jego odpowiedzi). Ta strefa przełączania jest pokazana jako zakreskowana na poniższym wykresie.

wprowadź opis obrazu tutaj

Ponieważ relth * epsilonjest mniejszy relth, zielone plamy nie sklejają się, co z kolei nadaje rozwiązaniu złą właściwość: możemy znaleźć trojaczki liczb takie, x < y_1 < y_2a jednak x == y2ale x != y1.

wprowadź opis obrazu tutaj

Weźmy ten uderzający przykład:

Mamy x < y1 < y2i faktycznie y2 - xjest ponad 2000 razy większa niż y1 - x. A jednak przy obecnym rozwiązaniu

Z kolei w rozwiązaniu zaproponowanym powyżej strefa przełączania bazuje na wartości |x| + |y|, która jest reprezentowana przez zakreskowany kwadrat poniżej. Zapewnia, że ​​obie strefy łączą się wdzięcznie.

wprowadź opis obrazu tutaj

Ponadto powyższy kod nie ma rozgałęzień, co mogłoby być bardziej wydajne. Weź pod uwagę, że operacje takie jak maxi abs, które a priori wymagają rozgałęzienia, często mają dedykowane instrukcje asemblacji. Z tego powodu uważam, że to podejście jest lepsze od innego rozwiązania, które polegałoby na naprawie Michaela nearlyEqualpoprzez zmianę przełącznika z diff < relthna diff < eps * relth, co spowodowałoby zasadniczo ten sam wzorzec odpowiedzi.

Gdzie przełączać się między porównaniem względnym i bezwzględnym?

Przełączanie między tymi trybami odbywa się wokół relth, co jest przyjmowane tak, jak FLT_MINw zaakceptowanej odpowiedzi. Ten wybór oznacza, że ​​reprezentacja float32ogranicza precyzję naszych liczb zmiennoprzecinkowych.

To nie zawsze ma sens. Na przykład, jeśli porównywane liczby są wynikiem odejmowania, być może coś z zakresu FLT_EPSILONma większy sens. Jeśli są to pierwiastki kwadratowe z odejmowanych liczb, niedokładność liczbowa może być jeszcze większa.

Jest to raczej oczywiste, gdy rozważasz porównanie liczb zmiennoprzecinkowych z 0. Tutaj każde względne porównanie zakończy się niepowodzeniem, ponieważ |x - 0| / (|x| + 0) = 1. Zatem porównanie musi zostać przełączone do trybu bezwzględnego, gdy xjest na poziomie dokładności obliczeń - i rzadko jest tak niskie, jak FLT_MIN.

To jest powód wprowadzenia relthpowyższego parametru.

Ponadto, nie mnożąc relthprzez epsilon, interpretacja tego parametru jest prosta i odpowiada poziomowi dokładności numerycznej, którego oczekujemy od tych liczb.

Matematyczne dudnienie

(trzymane tutaj głównie dla własnej przyjemności)

Bardziej ogólnie zakładam, że dobrze zachowujący się operator porównania zmiennoprzecinkowego =~powinien mieć kilka podstawowych właściwości.

Oto raczej oczywiste:

  • równość siebie: a =~ a
  • symetria: a =~ bimplikujeb =~ a
  • niezmienność przez opozycję: a =~ bimplikuje-a =~ -b

(Nie mamy a =~ bi b =~ csugeruje a =~ c, że =~nie jest to relacja równoważności).

Dodałbym następujące właściwości, które są bardziej specyficzne dla porównań zmiennoprzecinkowych

  • jeśli a < b < c, to a =~ cimplikuje a =~ b(bliższe wartości również powinny być równe)
  • jeśli a, b, m >= 0to a =~ boznacza a + m =~ b + m(większe wartości z tą samą różnicą również powinny być równe)
  • jeśli 0 <= λ < 1to a =~ boznacza λa =~ λb(być może mniej oczywiste do argumentowania za).

Te właściwości już dają silne ograniczenia dla możliwych funkcji bliskich równości. Weryfikuje je funkcja zaproponowana powyżej. Być może brakuje jednej lub kilku innych oczywistych właściwości.

Kiedy myślimy o =~rodzinie relacji równości =~[Ɛ,t]sparametryzowanej przez Ɛi relth, można również dodać

  • jeśli Ɛ1 < Ɛ2to a =~[Ɛ1,t] bimplikuje a =~[Ɛ2,t] b(równość dla danej tolerancji oznacza równość przy wyższej tolerancji)
  • jeśli t1 < t2to a =~[Ɛ,t1] bimplikuje a =~[Ɛ,t2] b(równość dla danej niedokładności implikuje równość przy większej nieprecyzyjności)

Zaproponowane rozwiązanie również je weryfikuje.

P-Gn
źródło
1
To świetna odpowiedź!
davidhigh
1
pytanie implementacji c ++: czy (std::abs(a) + std::abs(b))kiedykolwiek może być większe niż std::numeric_limits<float>::max()?
anneb
1
@anneb Tak, może to być + INF.
Paul Groke
16

Miałem problem z porównaniem liczb zmiennoprzecinkowych A < Bi A > B oto, co wydaje się działać:

Fabryki - wartość bezwzględna - dba o to, czy są zasadniczo równe.

tech_loafer
źródło
1
fabsW ogóle nie musisz go używać , jeśli zrobisz pierwszy testif (A - B < -Epsilon)
fishinear
11

Musimy wybrać poziom tolerancji, aby porównać liczby zmiennoprzecinkowe. Na przykład,

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

Jedna uwaga. Twój przykład jest dość zabawny.

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

Tutaj trochę matematyki

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

1/3 != 1

O tak..

Czy masz na myśli

if (b != 1)
    Console.WriteLine("Oh no!")
nni6
źródło
3

Pomysł, który miałem do szybkiego porównania zmiennoprzecinkowego

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)
}
Andy Poes
źródło
1

Adaptacja do PHP na podstawie odpowiedzi Michaela Borgwardta i bosonixa:

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;
            }
        }
    }
}
Dennis
źródło
1

Powinieneś zadać sobie pytanie, dlaczego porównujesz liczby. Jeśli znasz cel porównania, powinieneś także znać wymaganą dokładność swoich liczb. To jest inne w każdej sytuacji i w każdym kontekście aplikacji. Ale w prawie wszystkich praktycznych przypadkach wymagana jest absolutna dokładność. Rzadko kiedy ma zastosowanie względna dokładność.

Oto przykład: jeśli Twoim celem jest narysowanie wykresu na ekranie, prawdopodobnie chcesz, aby wartości zmiennoprzecinkowe były równe, jeśli odwzorowują ten sam piksel na ekranie. Jeśli rozmiar twojego ekranu wynosi 1000 pikseli, a twoje liczby mieszczą się w zakresie 1e6, prawdopodobnie będziesz chciał porównać 100 równe 200.

Biorąc pod uwagę wymaganą absolutną dokładność, algorytm wygląda następująco:

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;
}
rybi
źródło
0

Standardową radą jest użycie małej wartości „epsilon” (wybranej prawdopodobnie w zależności od aplikacji) i uznanie, że liczby zmiennoprzecinkowe znajdujące się w obrębie epsilon są równe. np. coś takiego

Bardziej kompletna odpowiedź jest skomplikowana, ponieważ błąd zmiennoprzecinkowy jest niezwykle subtelny i mylący w rozumowaniu. Jeśli naprawdę zależy Ci na równości w jakimkolwiek konkretnym sensie, prawdopodobnie szukasz rozwiązania, które nie obejmuje liczb zmiennoprzecinkowych.

nelhage
źródło
A jeśli pracuje z naprawdę małymi liczbami zmiennoprzecinkowymi, takimi jak 2.3E-15?
Toochin
1
Pracuję z zakresem mniej więcej [10E-14, 10E6], niezupełnie maszynowym epsilonem, ale bardzo bliskim.
Mike Bailey
2
Praca z małymi liczbami nie stanowi problemu, jeśli pamiętasz, że musisz pracować z względnymi błędami. Jeśli nie dbasz o stosunkowo duże tolerancje błędów, powyższe byłoby OK, gdybyś zastąpił to stan czymś w rodzajuif ((a - b) < EPSILON/a && (b - a) < EPSILON/a)
toochin
2
Kod podany powyżej jest również problematyczny, gdy masz do czynienia z bardzo dużymi liczbami c, ponieważ gdy twoja liczba jest wystarczająco duża, EPSILON będzie mniejszy niż precyzja maszyny c. Załóżmy na przykład c = 1E+22; d=c/3; e=d+d+d;. Wtedy e-cmoże być znacznie większe niż 1.
toochin
1
Na przykład spróbuj 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 i c nie są równe według pnt i nelhage) lub double a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;(a i b równe według pnt i nelhage)
toochin
0

Próbowałem napisać funkcję równości mając na uwadze powyższe komentarze. Oto, co wymyśliłem:

Edycja: zmiana z Math.Max ​​(a, b) na Math.Max ​​(Math.Abs ​​(a), Math.Abs ​​(b))

Myśli? Nadal muszę wypracować większe niż i mniejsze niż również.

Mike Bailey
źródło
epsilonpowinno być Math.abs(Math.Max(a, b)) * Double.Epsilon;, lub zawsze będzie mniejsze niż diffdla wartości ujemnej ai b. I myślę, że twój epsilonjest za mały, funkcja może nie zwracać niczego innego niż ==operator. Większy niż jest a < b && !fpEqual(a,b).
Toochin
1
Niepowodzenie, gdy obie wartości są równe dokładnie zero, niepowodzenie w przypadku Double.Epsilon i -Double.Epsilon, niepowodzenie w przypadku nieskończoności.
Michael Borgwardt,
1
Przypadek nieskończoności nie jest problemem w moim konkretnym zastosowaniu, ale jest podwójnie zauważony.
Mike Bailey
-1

Musisz wziąć pod uwagę, że błąd obcięcia jest względny. Dwie liczby są mniej więcej równe, jeśli ich różnica jest mniej więcej tak duża, jak ich ulp (jednostka na ostatnim miejscu).

Jednakże, jeśli wykonujesz obliczenia zmiennoprzecinkowe, twój potencjał błędu rośnie z każdą operacją (szczególnie ostrożnie przy odejmowaniu!), Więc twoja tolerancja błędu musi odpowiednio wzrosnąć.

toochin
źródło
-1

Najlepszym sposobem porównania podwójnych liczb pod względem równości / nierówności jest wzięcie bezwzględnej wartości ich różnicy i porównanie jej z wystarczająco małą (w zależności od kontekstu) wartością.

pnt
źródło