Wie soll ich einen Gleitkomma-Vergleich durchführen?

82

Ich schreibe gerade einen Code, in dem ich Folgendes habe:

Und dann muss ich an anderen Orten möglicherweise Gleichstellung tun:

Kurz gesagt, ich habe viel Gleitkomma-Mathematik im Gange und ich muss verschiedene Vergleiche für Bedingungen durchführen. Ich kann es nicht in ganzzahlige Mathematik umwandeln, weil so etwas in diesem Zusammenhang bedeutungslos ist.

Ich habe zuvor gelesen, dass Gleitkomma-Vergleiche unzuverlässig sein können, da solche Dinge passieren können:

Kurz gesagt, ich möchte wissen: Wie kann ich Gleitkommazahlen (kleiner als, größer als Gleichheit) zuverlässig vergleichen?

Der von mir verwendete Zahlenbereich reicht ungefähr von 10E-14 bis 10E6, daher muss ich sowohl mit kleinen als auch mit großen Zahlen arbeiten.

Ich habe dies als sprachunabhängig markiert, weil ich daran interessiert bin, wie ich dies erreichen kann, unabhängig davon, welche Sprache ich verwende.

Mike Bailey
quelle
Es gibt keine Möglichkeit, dies zuverlässig zu tun, wenn Gleitkommazahlen verwendet werden. Es wird immer Zahlen geben, die für den Computer gleich sind, obwohl dies in Wirklichkeit nicht der Fall ist (z. B. 1E + 100, 1E + 100 + 1), und Sie haben normalerweise auch Berechnungsergebnisse, die für den Computer nicht gleich sind, obwohl dies in Wirklichkeit der Fall ist (siehe einer der Kommentare zu Nelhages Antwort). Sie müssen wählen, welche der beiden Sie weniger wünschen.
Toochin
Wenn Sie sich beispielsweise nur mit rationalen Zahlen befassen, können Sie eine rationale Zahlenarithmetik implementieren, die auf ganzzahligen Zahlen basiert, und dann werden zwei Zahlen als gleich angesehen, wenn eine der beiden Zahlen auf die andere abgebrochen werden kann.
Toochin
Derzeit arbeite ich an einer Simulation. Der Ort, an dem ich normalerweise diese Vergleiche durchführe, hängt mit variablen Zeitschritten zusammen (zum Lösen einer Ode). Es gibt einige Fälle, in denen ich überprüfen muss, ob der angegebene Zeitschritt für ein Objekt gleich, kleiner oder größer als der Zeitschritt eines anderen Objekts ist.
Mike Bailey
Warum keine Arrays verwenden? stackoverflow.com/questions/28318610/…
Adrian P.

Antworten:

69

Das Vergleichen für größer / kleiner ist kein wirkliches Problem, es sei denn, Sie arbeiten direkt am Rand der Schwimm- / Doppelgenauigkeitsgrenze.

Für einen "Fuzzy Equals" -Vergleich ist dies (Java-Code, sollte leicht anzupassen sein) das, was ich mir nach viel Arbeit und unter Berücksichtigung vieler Kritik für The Floating-Point Guide ausgedacht habe:

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

Es kommt mit einer Testsuite. Sie sollten jede Lösung, die dies nicht tut, sofort verwerfen, da sie in einigen Randfällen wie einem Wert 0, zwei sehr kleinen Werten gegenüber Null oder Unendlichkeiten praktisch garantiert fehlschlägt.

Eine Alternative (siehe Link oben für weitere Details) besteht darin, die Bitmuster der Floats in eine Ganzzahl umzuwandeln und alles innerhalb eines festen Ganzzahlabstands zu akzeptieren.

In jedem Fall gibt es wahrscheinlich keine Lösung, die für alle Anwendungen perfekt ist. Idealerweise entwickeln / passen Sie Ihre eigene mit einer Testsuite an, die Ihre tatsächlichen Anwendungsfälle abdeckt.

Michael Borgwardt
quelle
1
@toochin: hängt davon ab, wie groß die Fehlerquote ist, die Sie berücksichtigen möchten, aber es wird am offensichtlichsten zu einem Problem, wenn Sie die denormalisierte Zahl betrachten, die Null am nächsten kommt, positiv und negativ - abgesehen von Null liegen diese näher beieinander als alle anderen beiden Werte, aber viele naive Implementierungen, die auf relativen Fehlern basieren, werden sie als zu weit voneinander entfernt betrachten.
Michael Borgwardt
2
Hmm. Sie haben einen Test else if (a * b == 0), aber dann ist Ihr Kommentar in der gleichen Zeile a or b or both are zero. Aber sind das nicht zwei verschiedene Dinge? ZB wenn a == 1e-162und b == 2e-162dann wird die Bedingung a * b == 0wahr sein.
Mark Dickinson
1
@toochin: hauptsächlich, weil der Code leicht in andere Sprachen portierbar sein soll, die diese Funktionalität möglicherweise nicht haben (er wurde auch nur in 1.5 zu Java hinzugefügt).
Michael Borgwardt
1
Wenn diese Funktion sehr häufig verwendet wird (z. B. jedes Bild eines Videospiels), würde ich sie in Assembler mit epischen Optimierungen neu schreiben.
1
Toller Leitfaden und tolle Antwort, besonders wenn man die abs(a-b)<epsAntworten hier berücksichtigt . Zwei Fragen: (1) Wäre es nicht besser, alle <s in <=s zu ändern und so "Null-Eps" -Vergleiche zuzulassen, die exakten Vergleichen entsprechen? (2) Wäre es nicht besser, diff < epsilon * (absA + absB);anstelle von diff / (absA + absB) < epsilon;(letzte Zeile) - zu verwenden?
Franz D.
41

TL; DR

  • Verwenden Sie die folgende Funktion anstelle der derzeit akzeptierten Lösung, um in bestimmten Grenzfällen unerwünschte Ergebnisse zu vermeiden und gleichzeitig die Effizienz zu steigern.
  • Kennen Sie die erwartete Ungenauigkeit Ihrer Zahlen und geben Sie sie in der Vergleichsfunktion entsprechend ein.

Grafik bitte?

Beim Vergleich von Gleitkommazahlen gibt es zwei "Modi".

Der erste ist der relative Modus, bei dem der Unterschied zwischen xund yrelativ zu ihrer Amplitude betrachtet wird |x| + |y|. Beim Zeichnen in 2D wird das folgende Profil angezeigt, wobei Grün Gleichheit von xund bedeutet y. (Ich habe epsilonzu Illustrationszwecken eine von 0,5 genommen).

Geben Sie hier die Bildbeschreibung ein

Der relative Modus wird für "normale" oder "ausreichend große" Gleitkommawerte verwendet. (Dazu später mehr).

Der zweite ist ein absoluter Modus, wenn wir einfach ihre Differenz mit einer festen Zahl vergleichen. Es gibt das folgende Profil (wieder mit einem epsilonvon 0,5 und einem relthvon 1 zur Veranschaulichung).

Geben Sie hier die Bildbeschreibung ein

Dieser absolute Vergleichsmodus wird für "winzige" Gleitkommawerte verwendet.

Die Frage ist nun, wie wir diese beiden Antwortmuster zusammenfügen.

In Michael Borgwardts Antwort basiert der Schalter auf dem Wert von diff, der unten liegen sollte relth( Float.MIN_NORMALin seiner Antwort). Diese Schaltzone ist in der folgenden Grafik schraffiert dargestellt.

Geben Sie hier die Bildbeschreibung ein

Weil relth * epsilonkleiner ist, dass relthdie grünen Flecken nicht zusammenkleben, was wiederum der Lösung eine schlechte Eigenschaft gibt: Wir können Drillinge von Zahlen finden, die x < y_1 < y_2und doch x == y2aber x != y1.

Geben Sie hier die Bildbeschreibung ein

Nehmen Sie dieses bemerkenswerte Beispiel:

Wir haben x < y1 < y2und ist in der Tat y2 - xmehr als 2000-mal größer als y1 - x. Und doch mit der aktuellen Lösung,

Im Gegensatz dazu basiert in der oben vorgeschlagenen Lösung die Schaltzone auf dem Wert von |x| + |y|, der durch das schraffierte Quadrat unten dargestellt wird. Es stellt sicher, dass beide Zonen ordnungsgemäß verbunden sind.

Geben Sie hier die Bildbeschreibung ein

Der obige Code hat auch keine Verzweigung, was effizienter sein könnte. Bedenken Sie, dass Vorgänge wie maxund abs, die a priori verzweigt werden müssen, häufig dedizierte Montageanweisungen haben. Aus diesem Grund denke ich, dass dieser Ansatz einer anderen Lösung überlegen ist, die darin besteht, Michaels nearlyEqualdurch Ändern des Schalters von diff < relthauf zu reparieren diff < eps * relth, was dann im Wesentlichen das gleiche Antwortmuster erzeugen würde.

Wo kann man zwischen relativem und absolutem Vergleich wechseln?

Der Wechsel zwischen diesen Modi reltherfolgt wie FLT_MINin der akzeptierten Antwort. Diese Wahl bedeutet, dass die Darstellung von float32die Genauigkeit unserer Gleitkommazahlen einschränkt.

Das macht nicht immer Sinn. Wenn die Zahlen, die Sie vergleichen, beispielsweise das Ergebnis einer Subtraktion sind, ist möglicherweise etwas im Bereich von FLT_EPSILONsinnvoller. Wenn es sich um Quadratwurzeln subtrahierter Zahlen handelt, kann die numerische Ungenauigkeit sogar noch höher sein.

Es ist ziemlich offensichtlich, wenn Sie einen Gleitkomma mit vergleichen 0. Hier wird jeder relative Vergleich scheitern, weil |x - 0| / (|x| + 0) = 1. Der Vergleich muss also in den absoluten Modus wechseln, wenn er xin der Größenordnung der Ungenauigkeit Ihrer Berechnung liegt - und selten ist er so niedrig wie FLT_MIN.

Dies ist der Grund für die Einführung des relthobigen Parameters.

Auch durch die nicht multipliziert relthmit epsilonder Interpretation dieses Parameters ist einfach und entsprechen dem Niveau der numerischen Präzision , dass wir auf diese Zahlen erwarten.

Mathematisches Grollen

(hier meistens zu meinem eigenen Vergnügen gehalten)

Generell gehe ich davon aus, dass ein gut erzogener Gleitkomma-Vergleichsoperator =~einige grundlegende Eigenschaften haben sollte.

Folgendes ist ziemlich offensichtlich:

  • Selbstgleichheit: a =~ a
  • Symmetrie: a =~ bimpliziertb =~ a
  • Invarianz durch Opposition: a =~ bimpliziert-a =~ -b

(Wir haben a =~ bund b =~ cimplizieren a =~ c, =~ist keine Äquivalenzbeziehung).

Ich würde die folgenden Eigenschaften hinzufügen, die spezifischer für Gleitkomma-Vergleiche sind

  • wenn a < b < c, dann a =~ cimpliziert a =~ b(engere Werte sollten auch gleich sein)
  • wenn a, b, m >= 0dann a =~ bimpliziert a + m =~ b + m(größere Werte mit der gleichen Differenz sollten auch gleich sein)
  • wenn 0 <= λ < 1dann a =~ bimpliziert λa =~ λb(vielleicht weniger offensichtlich zu argumentieren).

Diese Eigenschaften schränken mögliche Gleichheitsfunktionen bereits stark ein. Die oben vorgeschlagene Funktion überprüft sie. Möglicherweise fehlen eine oder mehrere ansonsten offensichtliche Eigenschaften.

Wenn man sich =~eine Familie von Gleichheitsbeziehungen =~[Ɛ,t]vorstellt, die durch Ɛund parametrisiert sind relth, könnte man auch hinzufügen

  • wenn Ɛ1 < Ɛ2dann a =~[Ɛ1,t] bimpliziert a =~[Ɛ2,t] b(Gleichheit für eine gegebene Toleranz impliziert Gleichheit bei einer höheren Toleranz)
  • wenn t1 < t2dann a =~[Ɛ,t1] bimpliziert a =~[Ɛ,t2] b(Gleichheit für eine gegebene Ungenauigkeit impliziert Gleichheit bei einer höheren Ungenauigkeit)

Die vorgeschlagene Lösung überprüft auch diese.

P-Gn
quelle
1
Das ist eine gute Antwort!
Davidhigh
1
c ++ Implementierungsfrage: Kann (std::abs(a) + std::abs(b))jemals größer sein als std::numeric_limits<float>::max()?
Anneb
1
@anneb Ja, es kann + INF sein.
Paul Groke
16

Ich hatte das Problem, Gleitkommazahlen zu vergleichen A < Bund A > B Folgendes scheint zu funktionieren:

Die Fabs - absoluter Wert - kümmern sich darum, ob sie im Wesentlichen gleich sind.

tech_loafer
quelle
1
Keine Notwendigkeit, fabsüberhaupt zu verwenden, wenn Sie den ersten Test machenif (A - B < -Epsilon)
fishinear
11

Wir müssen eine Toleranzstufe wählen, um Float-Zahlen zu vergleichen. Zum Beispiel,

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

Eine Note. Dein Beispiel ist ziemlich lustig.

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

Einige Mathe hier

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

1/3 != 1

Oh ja..

Meinst du

if (b != 1)
    Console.WriteLine("Oh no!")
nni6
quelle
3

Idee, die ich für den Gleitkomma-Vergleich schnell hatte

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
quelle
1

Anpassung an PHP aus der Antwort von Michael Borgwardt & 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;
            }
        }
    }
}
Dennis
quelle
1

Sie sollten sich fragen, warum Sie die Zahlen vergleichen. Wenn Sie den Zweck des Vergleichs kennen, sollten Sie auch die erforderliche Genauigkeit Ihrer Zahlen kennen. Das ist in jeder Situation und in jedem Anwendungskontext unterschiedlich. In so ziemlich allen praktischen Fällen ist jedoch eine absolute Genauigkeit erforderlich . Es ist nur sehr selten, dass eine relative Genauigkeit anwendbar ist.

Ein Beispiel: Wenn Sie ein Diagramm auf dem Bildschirm zeichnen möchten, möchten Sie wahrscheinlich, dass Gleitkommawerte gleich verglichen werden, wenn sie demselben Pixel auf dem Bildschirm zugeordnet sind. Wenn die Größe Ihres Bildschirms 1000 Pixel beträgt und Ihre Zahlen im Bereich von 1e6 liegen, möchten Sie wahrscheinlich, dass 100 gleich 200 sind.

Bei der erforderlichen absoluten Genauigkeit wird der Algorithmus zu:

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;
}
fishinear
quelle
0

Die Standardempfehlung besteht darin, einen kleinen "Epsilon" -Wert zu verwenden (wahrscheinlich abhängig von Ihrer Anwendung ausgewählt) und Floats, die innerhalb von Epsilon voneinander liegen, als gleich zu betrachten. zB so etwas wie

Eine vollständigere Antwort ist kompliziert, da Gleitkommafehler äußerst subtil und verwirrend sind. Wenn Sie sich wirklich für Gleichheit im wahrsten Sinne des Wortes interessieren, suchen Sie wahrscheinlich nach einer Lösung, die kein Gleitkomma beinhaltet.

nelhage
quelle
Was ist, wenn er mit wirklich kleinen Gleitkommazahlen wie 2.3E-15 arbeitet?
Toochin
1
Ich arbeite mit einer Reihe von ungefähr [10E-14, 10E6], nicht ganz maschinell, aber sehr nahe daran.
Mike Bailey
2
Das Arbeiten mit kleinen Zahlen ist kein Problem, wenn Sie bedenken, dass Sie mit relativen Fehlern arbeiten müssen. Wenn Sie sich nicht für relativ große Fehlertoleranzen interessieren, wäre das Obige in Ordnung, wenn Sie die Bedingung durch etwas wieif ((a - b) < EPSILON/a && (b - a) < EPSILON/a)
toochin
2
Der oben angegebene Code ist auch problematisch, wenn Sie mit sehr großen Zahlen arbeiten c, denn sobald Ihre Zahl groß genug ist, ist der EPSILON kleiner als die Maschinengenauigkeit von c. Nehmen wir zum Beispiel an c = 1E+22; d=c/3; e=d+d+d;. Dann e-ckann durchaus erheblich größer sein als 1.
Toochin
1
Versuchen Sie zum Beispiel 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 und c nicht gleich nach pnt und nelhage) oder double a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;(a und b gleich nach pnt und nelhage)
toochin
0

Ich habe versucht, eine Gleichstellungsfunktion unter Berücksichtigung der obigen Kommentare zu schreiben. Folgendes habe ich mir ausgedacht:

Bearbeiten: Wechsel von Math.Max ​​(a, b) zu Math.Max ​​(Math.Abs ​​(a), Math.Abs ​​(b))

Gedanken? Ich muss immer noch ein größeres als und ein kleineres als auch herausfinden.

Mike Bailey
quelle
epsilonsollte sein Math.abs(Math.Max(a, b)) * Double.Epsilon;, oder es wird immer kleiner sein als difffür negative aund b. Und ich denke, Ihre epsilonist zu klein, die Funktion gibt möglicherweise nichts anderes als den ==Operator zurück. Größer als es ist a < b && !fpEqual(a,b).
Toochin
1
Schlägt fehl, wenn beide Werte genau Null sind, schlägt für Double.Epsilon und -Double.Epsilon fehl, schlägt für Unendlichkeiten fehl.
Michael Borgwardt
1
Der Fall der Unendlichkeit ist in meiner speziellen Anwendung kein Problem, wird aber gebührend zur Kenntnis genommen.
Mike Bailey
-1

Sie müssen berücksichtigen, dass der Kürzungsfehler relativ ist. Zwei Zahlen sind ungefähr gleich, wenn ihre Differenz ungefähr so ​​groß ist wie ihre ulp (Einheit an letzter Stelle).

Wenn Sie jedoch Gleitkommaberechnungen durchführen, steigt Ihr Fehlerpotential mit jeder Operation (insbesondere vorsichtig mit Subtraktionen!), Daher muss sich Ihre Fehlertoleranz entsprechend erhöhen.

Toochin
quelle
-1

Der beste Weg, um Doppelwerte auf Gleichheit / Ungleichheit zu vergleichen, besteht darin, den absoluten Wert ihrer Differenz mit einem ausreichend kleinen Wert (abhängig von Ihrem Kontext) zu vergleichen.

pnt
quelle