Dies war eine Frage, die mir kürzlich bei meinem Interview gestellt wurde und die ich wissen möchte (ich erinnere mich nicht wirklich an die Theorie der numerischen Analyse, also helfen Sie mir bitte :)
Wenn wir eine Funktion haben, die Gleitkommazahlen akkumuliert:
std::accumulate(v.begin(), v.end(), 0.0);
v
ist ein std::vector<float>
zum Beispiel.
Wäre es besser, diese Zahlen zu sortieren, bevor Sie sie akkumulieren?
Welche Reihenfolge würde die genaueste Antwort geben?
Ich vermute , dass die Sortieren Sie die Zahlen in aufsteigender Reihenfolge tatsächlich die numerischen Fehler machen würde weniger , aber leider kann ich es selbst nicht beweisen.
PS Mir ist klar, dass dies wahrscheinlich nichts mit realer Programmierung zu tun hat, nur neugierig zu sein.
c++
floating-point
precision
Yippie-Ki-Yay
quelle
quelle
Antworten:
Ihr Instinkt ist im Grunde richtig, das Sortieren in aufsteigender Reihenfolge (der Größe) verbessert normalerweise die Dinge etwas. Stellen Sie sich den Fall vor, in dem wir Floats mit einfacher Genauigkeit (32 Bit) hinzufügen und 1 Milliarde Werte gleich 1 / (1 Milliarde) und einen Wert gleich 1 sind. Wenn die 1 an erster Stelle steht, kommt die Summe auf 1, da 1 + (1/1 Milliarde) aufgrund von Genauigkeitsverlust 1 ist. Jede Addition hat keinerlei Einfluss auf die Gesamtsumme.
Wenn die kleinen Werte an erster Stelle stehen, summieren sie sich zumindest zu etwas, obwohl ich selbst dann 2 ^ 30 davon habe, während ich nach ungefähr 2 ^ 25 wieder in der Situation bin, in der jeder einzelne die Summe nicht beeinflusst nicht mehr. Also werde ich noch mehr Tricks brauchen.
Das ist ein Extremfall, aber im Allgemeinen ist das Hinzufügen von zwei Werten ähnlicher Größe genauer als das Hinzufügen von zwei Werten sehr unterschiedlicher Größen, da Sie auf diese Weise weniger Genauigkeitsbits in dem kleineren Wert "verwerfen". Indem Sie die Zahlen sortieren, gruppieren Sie Werte ähnlicher Größe, und indem Sie sie in aufsteigender Reihenfolge hinzufügen, geben Sie den kleinen Werten eine "Chance", kumulativ die Größe der größeren Zahlen zu erreichen.
Wenn es sich jedoch um negative Zahlen handelt, ist es einfach, diesen Ansatz zu "überlisten". Betrachten Sie drei Werte, um zu summieren
{1, -1, 1 billionth}
. Die arithmetisch korrekte Summe ist1 billionth
, aber wenn meine erste Addition den winzigen Wert beinhaltet, ist meine endgültige Summe 0. Von den 6 möglichen Ordnungen sind nur 2 "korrekt" -{1, -1, 1 billionth}
und{-1, 1, 1 billionth}
. Alle 6 Ordnungen liefern Ergebnisse, die auf der Skala des größten Größenwerts in der Eingabe (0,0000001% out) genau sind, aber für 4 von ihnen ist das Ergebnis auf der Skala der wahren Lösung (100% out) ungenau. Das spezielle Problem, das Sie lösen, zeigt Ihnen, ob das erstere gut genug ist oder nicht.Tatsächlich können Sie viel mehr Streiche spielen, als sie nur in sortierter Reihenfolge hinzuzufügen. Wenn Sie viele sehr kleine Werte, eine mittlere Anzahl mittlerer Werte und eine kleine Anzahl großer Werte haben, ist es möglicherweise am genauesten, zuerst alle kleinen Werte zu addieren, dann die mittleren Werte separat zu addieren und diese beiden Summen zu addieren zusammen dann die großen hinzufügen. Es ist überhaupt nicht trivial, die genaueste Kombination von Gleitkomma-Additionen zu finden, aber um mit wirklich schlimmen Fällen fertig zu werden, können Sie eine ganze Reihe laufender Summen in verschiedenen Größen beibehalten und jeden neuen Wert zu der Summe hinzufügen, die seiner Größe am besten entspricht. und wenn eine laufende Summe für ihre Größe zu groß wird, addieren Sie sie zur nächsten Summe und starten Sie eine neue. Auf den logischen Punkt gebracht, entspricht dieser Prozess der Ausführung der Summe in einem Typ mit beliebiger Genauigkeit (also Sie ' d mach das). Angesichts der vereinfachten Wahl, in aufsteigender oder absteigender Größenordnung zu addieren, ist aufsteigend die bessere Wahl.
Es hat eine gewisse Beziehung zur realen Programmierung, da es einige Fälle gibt, in denen Ihre Berechnung sehr schlecht laufen kann, wenn Sie versehentlich einen "schweren" Schwanz abhacken, der aus einer großen Anzahl von Werten besteht, von denen jeder zu klein ist, um ihn einzeln zu beeinflussen die Summe, oder wenn Sie zu viel Präzision von vielen kleinen Werten wegwerfen, die einzeln nur die letzten Bits der Summe beeinflussen. In Fällen, in denen der Schwanz sowieso vernachlässigbar ist, ist es Ihnen wahrscheinlich egal. Zum Beispiel, wenn Sie zunächst nur eine kleine Anzahl von Werten addieren und nur einige signifikante Zahlen der Summe verwenden.
quelle
Es gibt auch einen Algorithmus für diese Art von Akkumulationsoperation namens Kahan Summation , den Sie wahrscheinlich kennen sollten.
Laut Wikipedia
quelle
sum
undc
die Größe unterscheiden. Es kann trivial auf N Variablen erweitert werden.-ffast-math
GCC).-ffast-math
. Was ich aus dieser Diskussion und diesem Link gelernt habe , ist, dass Sie, wenn Sie sich für die numerische Genauigkeit interessieren, die Verwendung wahrscheinlich vermeiden sollten, dies-ffast-math
aber in vielen Anwendungen, in denen Sie möglicherweise CPU-gebunden sind, sich aber nicht für präzise numerische Berechnungen interessieren (z. B. Spielprogrammierung) )-ffast-math
ist vernünftig zu bedienen. Daher möchte ich meinen stark formulierten "verbotenen" Kommentar ändern.sum, c, t, y
hilft dabei. Sie müssen auchsum -= c
vorher hinzufügenreturn sum
.Ich habe das extreme Beispiel in der Antwort von Steve Jessop ausprobiert.
Ich habe folgendes Ergebnis erhalten:
Der Fehler in der ersten Zeile ist in der zweiten mehr als zehnmal größer.
Wenn ich das
double
sfloat
im obigen Code in s ändere , erhalte ich:Keine der Antworten liegt in der Nähe von 2,0 (aber die zweite ist etwas näher).
Verwendung der Kahan-Summation (mit
double
s) wie von Daniel Pryden beschrieben:Ich bekomme genau 2.0:
Und selbst wenn ich das
double
sfloat
im obigen Code in s ändere , erhalte ich:Es scheint, dass Kahan der richtige Weg ist!
quelle
double
das nicht schlecht leidet Präzisionsverlust beim Addieren einer Milliarde Milliardstel, da es 52 signifikante Bits hat, während IEEEfloat
nur 24 hat und würde.c
, um Werte zu enthalten, die viel größer als der nächste Summand sind. Dies bedeutet, dass der Summand viel, viel kleiner als die Hauptsumme ist, so dass es sehr viele von ihnen geben muss, um viel zu ergeben. Besonders mitdouble
Arithmetik.Es gibt eine Klasse von Algorithmen, die genau dieses Problem lösen, ohne dass die Daten sortiert oder anderweitig neu angeordnet werden müssen .
Mit anderen Worten kann die Summierung in einem Durchgang über die Daten erfolgen. Dies macht solche Algorithmen auch in Situationen anwendbar, in denen der Datensatz nicht im Voraus bekannt ist, z. B. wenn die Daten in Echtzeit eintreffen und die laufende Summe beibehalten werden muss.
Hier ist die Zusammenfassung eines kürzlich erschienenen Papiers:
Quelle: Algorithmus 908: Exakte Online-Summierung von Gleitkomma-Streams .
quelle
Aufbauend auf Steves Antwort, die Zahlen zuerst in aufsteigender Reihenfolge zu sortieren, möchte ich zwei weitere Ideen vorstellen:
Entscheiden Sie sich für den Exponentenunterschied zweier Zahlen, über dem Sie möglicherweise entscheiden, dass Sie zu viel Präzision verlieren würden.
Addieren Sie dann die Zahlen der Reihe nach, bis der Exponent des Akkumulators für die nächste Nummer zu groß ist. Stellen Sie den Akkumulator dann in eine temporäre Warteschlange und starten Sie den Akkumulator mit der nächsten Nummer. Fahren Sie fort, bis Sie die ursprüngliche Liste erschöpft haben.
Sie wiederholen den Vorgang mit der temporären Warteschlange (nachdem Sie sie sortiert haben) und mit einem möglicherweise größeren Exponentenunterschied.
Ich denke, das wird ziemlich langsam sein, wenn Sie ständig Exponenten berechnen müssen.
Ich hatte einen schnellen Versuch mit einem Programm und das Ergebnis war 1.99903
quelle
Ich denke, Sie können es besser machen, als die Zahlen zu sortieren, bevor Sie sie akkumulieren, denn während des Akkumulationsprozesses wird der Akkumulator immer größer. Wenn Sie eine große Anzahl ähnlicher Zahlen haben, verlieren Sie schnell an Präzision. Folgendes würde ich stattdessen vorschlagen:
Natürlich ist dieser Algorithmus mit einer Prioritätswarteschlange anstelle einer Liste am effizientesten. C ++ - Code:
Treiber:
Die Zahlen in der Warteschlange sind negativ, weil sie
top
die größte Zahl ergeben, aber wir wollen die kleinste . Ich hätte der Warteschlange mehr Vorlagenargumente zur Verfügung stellen können, aber dieser Ansatz scheint einfacher zu sein.quelle
Dies beantwortet Ihre Frage nicht ganz, aber es ist klug, die Summe zweimal auszuführen, einmal im Rundungsmodus "Aufrunden" und einmal mit " ". Vergleichen Sie die beiden Antworten, und Sie wissen / wie / ungenau Ihre Ergebnisse sind und ob Sie daher eine klügere Summierungsstrategie verwenden müssen. Leider machen die meisten Sprachen das Ändern des Gleitkomma-Rundungsmodus nicht so einfach, wie es sein sollte, da die Leute nicht wissen, dass es tatsächlich für alltägliche Berechnungen nützlich ist.
Werfen Sie einen Blick auf die Intervallarithmetik, bei der Sie alle Berechnungen auf diese Weise durchführen und dabei die höchsten und niedrigsten Werte beibehalten. Dies führt zu interessanten Ergebnissen und Optimierungen.
quelle
Die einfachste Sorte , die die Genauigkeit verbessert, besteht darin, nach dem aufsteigenden Absolutwert zu sortieren. Auf diese Weise können sich die kleinsten Größenwerte ansammeln oder aufheben, bevor sie mit größeren Größenwerten interagieren, die einen Genauigkeitsverlust auslösen würden.
Das heißt, Sie können es besser machen, indem Sie mehrere nicht überlappende Teilsummen verfolgen. Hier ist ein Artikel, der die Technik beschreibt und einen Genauigkeitsnachweis vorlegt: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
Dieser Algorithmus und andere Ansätze zur exakten Gleitkommasummierung werden in einfachem Python unter folgender Adresse implementiert: http://code.activestate.com/recipes/393090/ Mindestens zwei davon können trivial in C ++ konvertiert werden.
quelle
Für IEEE 754-Nummern mit einfacher oder doppelter Genauigkeit oder bekannten Formatnummern besteht eine andere Alternative darin, ein Array von Zahlen (vom Aufrufer übergeben oder in einer Klasse für C ++) zu verwenden, die vom Exponenten indiziert werden. Beim Hinzufügen von Zahlen zum Array werden nur Zahlen mit demselben Exponenten hinzugefügt (bis ein leerer Steckplatz gefunden und die Zahl gespeichert ist). Wenn eine Summe angefordert wird, wird das Array vom kleinsten zum größten summiert, um das Abschneiden zu minimieren. Beispiel mit einfacher Genauigkeit:
Beispiel mit doppelter Genauigkeit:
quelle
Ihre Schwimmer sollten mit doppelter Genauigkeit hinzugefügt werden. Das gibt Ihnen mehr Präzision als jede andere Technik. Für ein bisschen mehr Präzision und deutlich mehr Geschwindigkeit können Sie beispielsweise vier Summen erstellen und am Ende addieren.
Wenn Sie Zahlen mit doppelter Genauigkeit hinzufügen, verwenden Sie long double für die Summe. Dies wirkt sich jedoch nur positiv auf Implementierungen aus, bei denen long double tatsächlich eine höhere Genauigkeit als double aufweist (normalerweise x86, PowerPC, abhängig von den Compilereinstellungen).
quelle
In Bezug auf die Sortierung scheint es mir, dass, wenn Sie eine Stornierung erwarten, die Zahlen in absteigender Größenordnung und nicht in aufsteigender Reihenfolge hinzugefügt werden sollten . Zum Beispiel:
((-1 + 1) + 1e-20) ergibt 1e-20
aber
((1e-20 + 1) - 1) ergibt 0
In der ersten Gleichung werden zwei große Zahlen aufgehoben, während in der zweiten der 1e-20-Term verloren geht, wenn er zu 1 addiert wird, da die Genauigkeit nicht ausreicht, um ihn beizubehalten.
Außerdem ist die paarweise Summierung ziemlich anständig, um viele Zahlen zu summieren.
quelle