Dies kann jedoch aufgrund von Unterlauf oder Überlauf im Aufstau durchaus scheitern product (Hinweis: long double vermeidet dieses Problem nicht wirklich). Die nächste Option besteht also darin, die Logarithmen zusammenzufassen:
Das funktioniert, ruft aber an std::log() für jedes Element, das potenziell langsam ist. Kann ich das vermeiden? Zum Beispiel durch Nachverfolgen (des Äquivalents von) des Exponenten und der Mantisse des Akkumulierten product separat?
Gibt es einen maximalen Bereich, in dem sich der Wert von Zahlen befindet?
– Abhishek Bansal
14. November 2013 um 14:45 Uhr
@Walter: hast du versucht, long double zu verwenden
– ahmedsafan86
14. November 2013 um 14:48 Uhr
@Ahmedsafan hilft nicht, wenn es zu viele Zahlen gibt: Betrachten Sie Zahlen im Bereich von 0,01 bis 1 und 10 ^ 6 davon …
– Walther
14. November 2013 um 14:51 Uhr
Haben Sie überprüft, um zu sehen, wie langsam log() eigentlich ist, im Vergleich zu den Alternativen? Mich würde ein echter Leistungsvergleich interessieren…
– kommender Sturm
14. November 2013 um 19:52 Uhr
Der Logarithmus lässt sich sehr effizient weiter umsetzen doubles. Vielleicht möchten Sie hier wirklich eine Schießerei machen, das Ergebnis wäre ziemlich interessant 🙂
– Filmer
14. November 2013 um 20:02 Uhr
sbabbi
Die Lösung “Split Exponent und Mantisse”:
double geometric_mean(std::vector<double> const & data)
{
double m = 1.0;
long long ex = 0;
double invN = 1.0 / data.size();
for (double x : data)
{
int i;
double f1 = std::frexp(x,&i);
m*=f1;
ex+=i;
}
return std::pow( std::numeric_limits<double>::radix,ex * invN) * std::pow(m,invN);
}
Wenn Sie das befürchten ex könnte überlaufen, können Sie es als Double anstelle von a definieren long longund multipliziere mit invN bei jedem Schritt, aber Sie könnten mit diesem Ansatz viel Präzision verlieren.
BEARBEITEN Für große Eingaben können wir die Berechnung in mehrere Buckets aufteilen:
double geometric_mean(std::vector<double> const & data)
{
long long ex = 0;
auto do_bucket = [&data,&ex](int first,int last) -> double
{
double ans = 1.0;
for ( ;first != last;++first)
{
int i;
ans *= std::frexp(data[first],&i);
ex+=i;
}
return ans;
};
const int bucket_size = -std::log2( std::numeric_limits<double>::min() );
std::size_t buckets = data.size() / bucket_size;
double invN = 1.0 / data.size();
double m = 1.0;
for (std::size_t i = 0;i < buckets;++i)
m *= std::pow( do_bucket(i * bucket_size,(i+1) * bucket_size),invN );
m*= std::pow( do_bucket( buckets * bucket_size, data.size() ),invN );
return std::pow( std::numeric_limits<double>::radix,ex * invN ) * m;
}
@Walter Ich habe nur ein paar Tests gemacht, ich habe einige offensichtliche Grenzfälle, die fehlschlagen können, nicht ausprobiert. Zum Beispiel m kann unterlaufen, wenn Sie mehr als ~1022 Nummern haben. Auch, auf den zweiten Gedanken, ex wird nicht realistisch überlaufen (benötigen etwa 10 ^ 16 Eingänge, um eine Chance zum Überlaufen zu haben).
– Sbabbi
14. November 2013 um 16:50 Uhr
@stabbi Du darfst zwar immer noch unterlaufen, zumal seit jeher f1<1. Also, bei näherem Nachdenken hätte ich das nicht akzeptieren sollen ... Vielleicht können Sie dieses Problem beheben. In der Praxis habe ich viel mehr als 1022 Nummern ...
– Walther
14. November 2013 um 18:33 Uhr
@Walter behoben und mit Eingabegrößen von bis zu 10 ^ 8 getestet.
– Sbabbi
14. November 2013 um 18:58 Uhr
du machst immer noch a std::pow pro Eimer, was meiner Meinung nach langsamer ist als std::logda muss also Verbesserungspotential vorhanden sein …
– Walther
14. November 2013 um 19:01 Uhr
@Walter Sie können dies wie in Ihrer Lösung auf “adaptive” Weise tun. Bedenken Sie auch, dass ein Bucket ziemlich groß ist (~1000 Elemente). Darüber hinaus kann diese Lösung einfach parallelisiert werden (entweder mit openmp oder durch Ersetzen der ersten for-Schleife durch a parallel_for), müssen Sie nur schützen ex mit std::atomic.
– Sbabbi
14. November 2013 um 19:06 Uhr
Walter
Ich glaube, ich habe einen Weg gefunden, es zu tun, es kombiniert die beiden Routinen in der Frage, ähnlich wie Peters Idee. Hier ist ein Beispielcode.
Die schlechte Nachricht ist: Das kommt mit einem Ast. Die gute Nachricht: Der Verzweigungsprädiktor wird dies wahrscheinlich fast immer richtig machen (die Verzweigung sollte nur selten ausgelöst werden).
Die Verzweigung könnte mit Peters Idee einer konstanten Anzahl von Termen im Produkt vermieden werden. Das Problem dabei ist, dass abhängig von den Werten immer noch innerhalb weniger Terme ein Überlauf/Unterlauf auftreten kann.
Dies funktioniert möglicherweise immer noch nicht, wenn einer der Werte sehr groß oder klein ist (>1e244 oder <1e-244).
– Jeffrey Sax
14. November 2013 um 14:57 Uhr
Sicher, aber die Herausforderung besteht darin, auch unter schwierigen Umständen das bestmögliche Ergebnis zu erzielen.
– Jeffrey Sax
14. November 2013 um 21:03 Uhr
Sie können dies möglicherweise beschleunigen, indem Sie Zahlen wie in Ihrer ursprünglichen Lösung multiplizieren und nur jede bestimmte Anzahl von Multiplikationen in Logarithmen umwandeln (abhängig von der Größe Ihrer Anfangszahlen).
Ein anderer Ansatz, der eine bessere Genauigkeit und Leistung als das Logarithmusverfahren erbringen würde, bestünde darin, außerhalb des Bereichs liegende Exponenten durch einen festen Betrag zu kompensieren, wobei ein exakter Logarithmus des gelöschten Überschusses aufrechterhalten würde. So:
const int EXP = 64; // maximal/minimal exponent
const double BIG = pow(2, EXP); // overflow threshold
const double SMALL = pow(2, -EXP); // underflow threshold
double product = 1;
int excess = 0; // number of times BIG has been divided out of product
for(int i=0; i<n; i++)
{
product *= A[i];
while(product > BIG)
{
product *= SMALL;
excess++;
}
while(product < SMALL)
{
product *= BIG;
excess--;
}
}
double mean = pow(product, 1.0/n) * pow(BIG, double(excess)/n);
Alle Multiplikationen mit BIG und SMALL sind genau, und es gibt keine Anrufe zu log (eine transzendente und daher besonders ungenaue Funktion).
Es gibt eine einfache Idee, um die Berechnung zu reduzieren und auch einen Überlauf zu verhindern. Sie können Zahlen gruppieren, sagen wir mindestens zwei gleichzeitig, und ihr Protokoll berechnen und dann ihre Summe auswerten.
Ja, das ist im Grunde die gleiche Idee wie Peters, aber mit nur 2 Nummern.
– Walther
14. November 2013 um 14:49 Uhr
Alexandre C.
Das Summieren von Protokollen zur stabilen Berechnung von Produkten ist vollkommen in Ordnung und ziemlich effizient (wenn dies nicht ausreicht: Es gibt Möglichkeiten, vektorisierte Logarithmen mit ein paar SSE-Operationen zu erhalten – es gibt auch die Vektoroperationen von Intel MKL).
Um einen Überlauf zu vermeiden, besteht eine übliche Technik darin, jede Zahl vorher durch den maximalen oder minimalen Betragseintrag zu dividieren (oder logarithmische Differenzen zu logmax oder logmin zu summieren). Sie können auch Buckets verwenden, wenn die Zahlen stark variieren (z. B. das Protokoll von kleinen Zahlen und großen Zahlen separat summieren). Beachten Sie, dass normalerweise nichts davon benötigt wird, außer für sehr große Mengen, da das Protokoll von a double ist nie riesig (zwischen sagen wir -700 und 700).
Außerdem müssen Sie die Schilder separat im Auge behalten.
Rechnen log x behält in der Regel die gleiche Anzahl signifikanter Stellen wie xausser wenn x liegt in der Nähe 1: Sie verwenden möchten std::log1p wenn du rechnen musst prod(1 + x_n) mit klein x_n.
Schließlich, wenn Sie beim Summieren Rundungsfehler haben, können Sie verwenden Kahan-Summe oder Varianten.
Ja, das ist im Grunde die gleiche Idee wie Peters, aber mit nur 2 Nummern.
– Walther
14. November 2013 um 14:49 Uhr
ZachB
Anstatt den sehr teuren Logarithmus zu verwenden, können Sie die Ergebnisse direkt mit Zweierpotenzen skalieren.
Kommt mir bekannt vor. 😉 Beachten Sie, dass der letzte Logarithmus nicht erforderlich ist, Sie machen ihn einfach danach rückgängig.
– Sneftel
14. November 2013 um 15:10 Uhr
Ja, sehr ähnlich. Beachten Sie jedoch 2 Dinge: Erstens, pow Implizit verwendet einen Aufruf an log du sparst also nichts. Zweitens, wenn einer der Werte sehr groß oder sehr klein ist (>DBL_MAX/BIG oder
– Jeffrey Sax
14. November 2013 um 15:15 Uhr
13734400cookie-checkEffiziente Methode zur Berechnung des geometrischen Mittels vieler Zahlenyes
Gibt es einen maximalen Bereich, in dem sich der Wert von Zahlen befindet?
– Abhishek Bansal
14. November 2013 um 14:45 Uhr
@Walter: hast du versucht, long double zu verwenden
– ahmedsafan86
14. November 2013 um 14:48 Uhr
@Ahmedsafan hilft nicht, wenn es zu viele Zahlen gibt: Betrachten Sie Zahlen im Bereich von 0,01 bis 1 und 10 ^ 6 davon …
– Walther
14. November 2013 um 14:51 Uhr
Haben Sie überprüft, um zu sehen, wie langsam
log()
eigentlich ist, im Vergleich zu den Alternativen? Mich würde ein echter Leistungsvergleich interessieren…– kommender Sturm
14. November 2013 um 19:52 Uhr
Der Logarithmus lässt sich sehr effizient weiter umsetzen
double
s. Vielleicht möchten Sie hier wirklich eine Schießerei machen, das Ergebnis wäre ziemlich interessant 🙂– Filmer
14. November 2013 um 20:02 Uhr