105 votes

Dans quel ordre flotteurs doivent être ajoutés pour obtenir le résultat plus précis ?

C'était une question que j'avais demandé lors de ma récente interview, et je veux savoir (je n'ai pas vraiment souvenir de la théorie de l'analyse numérique, donc merci de m'aider :)

Si nous avons une fonction, qui accumule les nombres à virgule flottante:

std::accumulate(v.begin(), v.end(), 0.0);

v est std::vector<float>, par exemple.

  • Serait-il préférable de trier ces numéros avant de l'accumulation?

  • L'ordonnance qui donnerait le plus de réponse précise?

Je soupçonne que le tri des nombres dans l'ordre croissant serait effectivement faire l'erreur numérique moins, mais malheureusement, je ne peux pas le prouver à moi-même.

P. S. je me rends compte ce qui a probablement rien à voir avec le monde réel, la programmation, il suffit d'être curieux.

108voto

Steve Jessop Points 166970

Votre instinct est fondamentalement de droite, dans l'ordre croissant (de grandeur) améliore quelque peu les choses. Considérons le cas où nous sommes en ajoutant simple précision (32 bits) flotte, et il y a 1 milliards de dollars des valeurs égales à 1 / (1 milliard), et une valeur égale à 1. Si le 1 est premier, alors la somme viendra à 1, puisque 1 + (1 / 1 milliards de dollars) est de 1 à cause de la perte de précision. Chaque addition n'a aucun effet sur le total.

Si les petites valeurs de venir en premier, ils auront au moins la somme à quelque chose, mais même alors, j'ai 2^30 d'entre eux, alors qu'après 2^25 ou alors je suis de retour dans la situation où chacun, individuellement, n'est-ce pas touchant au total plus de. Donc, je vais encore avoir besoin de plus d'astuces.

C'est un cas extrême, mais en général, l'ajout de deux valeurs du même ordre de grandeur est plus précise que l'ajout de deux valeurs très différentes grandeurs, puisque vous "jeter" moins de bits de précision dans la plus petite valeur de cette façon. En triant les chiffres, vous les valeurs du groupe du même ordre de grandeur ensemble, et en les ajoutant dans l'ordre croissant vous donner les petites valeurs une "chance" de façon cumulative d'atteindre la grandeur du plus grand nombre.

Encore, si les nombres négatifs sont impliqués, il est facile de "berner" cette approche. Considérons trois valeurs à additionner, {1, -1, 1 billionth}. L'arithmétique correcte somme est 1 billionth, mais si mon premier ajout implique la minuscule de la valeur, puis mon somme finale sera de 0. Du 6 ordres possibles, seulement 2 sont "correctes" - {1, -1, 1 billionth} et {-1, 1, 1 billionth}. Tous les 6 ordres de donner des résultats précis à l'échelle de la plus grande amplitude de valeur dans l'entrée (0.0000001%), mais pour 4 d'entre elles, le résultat est inexacte à l'échelle de la solution réelle (100%). Le problème que vous résolvez vous dira si le premier est assez bon ou pas.

En fait, vous pouvez jouer beaucoup plus de trucs que simplement en ajoutant dans l'ordre de tri. Si vous avez beaucoup de très petites valeurs, un nombre moyen de médiocre, les valeurs, et un petit nombre de grandes valeurs, alors il peut être plus précis pour ajouter tous les petits, puis, séparément, le total de la médiocre, ajouter ces deux totaux ensemble, puis ajouter les grands. Il n'est pas trivial de trouver la plus précise de la combinaison de virgule flottante ajouts, mais de composer avec vraiment mauvais cas, vous pouvez conserver l'ensemble des totaux en cours d'exécution à différentes amplitudes, ajouter chaque nouvelle valeur pour le total qui correspond le mieux à son ampleur, et lors de l'exécution d'un total commence à être trop grand pour sa grandeur, l'ajouter dans la prochaine total et en commencer une nouvelle. Poussé à son extrême logique, ce processus est équivalent à l'exécution de la somme dans une précision arbitraire type (donc, si vous voulez le faire). Mais compte tenu de l'simpliste choix d'ajouter dans l'ordre croissant ou décroissant de grandeur, l'ascension, c'est le meilleur pari.

Il n'en relation avec le monde réel de la programmation, car il y a certains cas où votre calcul peut aller très mauvais si vous avez accidentellement couper un "lourd" queue composé d'un grand nombre de valeurs de chaque de ce qui est trop petit, individuellement, d'affecter le montant, ou si vous jetez trop de précision à partir d'un grand nombre de petites valeurs qui, individuellement, n'affectent que la dernière quelques bits de la somme. Dans les cas où la queue est négligeable, de toute façon vous n'avez probablement pas de soins. Par exemple, si vous êtes seulement l'addition d'un petit nombre de valeurs, en premier lieu, et vous êtes seulement à l'aide de quelques chiffres significatifs de la somme.

88voto

Daniel Pryden Points 22167

Il y a également un algorithme conçu pour ce genre d'accumulation opération, appelée Kahan Sommation, que vous devriez connaître.

Selon Wikipedia,

Le Kahan algorithme de sommation (également connu en tant que compensé la sommation) réduit considérablement l'erreur numérique dans le total obtenu par l'ajout d'une séquence finie de la précision des nombres à virgule flottante, par rapport à l'approche évidente. Ceci est fait en gardant un séparé de la rémunération (une variable à accumuler de petites erreurs).

En pseudo-code, l'algorithme est:

function kahanSum(input)
 var sum = input[1]
 var c = 0.0          //A running compensation for lost low-order bits.
 for i = 2 to input.length
  y = input[i] - c    //So far, so good: c is zero.
  t = sum + y         //Alas, sum is big, y small, so low-order digits of y are lost.
  c = (t - sum) - y   //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
  sum = t             //Algebraically, c should always be zero. Beware eagerly optimising compilers!
 next i               //Next time around, the lost low part will be added to y in a fresh attempt.
return sum

34voto

Andrew Stein Points 6344

J'ai essayé l'exemple extrême de la réponse fournie par Steve Jessop.

#include <iostream>
#include <iomanip>
#include <cmath>

int main()
{
    long billion = 1000000000;
    double big = 1.0;
    double small = 1e-9;
    double expected = 2.0;

    double sum = big;
    for (long i = 0; i < billion; ++i)
        sum += small;
    std::cout << std::scientific << std::setprecision(1) << big << " + " << billion << " * " << small << " = " <<
        std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    sum = 0;
    for (long i = 0; i < billion; ++i)
        sum += small;
    sum += big;
    std::cout  << std::scientific << std::setprecision(1) << billion << " * " << small << " + " << big << " = " <<
        std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    return 0;
}

J'ai obtenu le résultat suivant:

1.0e+00 + 1000000000 * 1.0e-09 = 2.000000082740371    (difference = 0.000000082740371)
1000000000 * 1.0e-09 + 1.0e+00 = 1.999999992539933    (difference = 0.000000007460067)

L'erreur à la première ligne, soit plus de dix fois plus grande dans le second.

Si je change l' doubles pour floats dans le code ci-dessus, j'obtiens:

1.0e+00 + 1000000000 * 1.0e-09 = 1.000000000000000    (difference = 1.000000000000000)
1000000000 * 1.0e-09 + 1.0e+00 = 1.031250000000000    (difference = 0.968750000000000)

Ni la réponse est la même pour les 2.0 (mais la deuxième est un peu plus proche).

À l'aide de la Kahan sommation (avec doubles) tel que décrit par Daniel Pryden:

#include <iostream>
#include <iomanip>
#include <cmath>

int main()
{
    long billion = 1000000000;
    double big = 1.0;
    double small = 1e-9;
    double expected = 2.0;

    double sum = big;
    double c = 0.0;
    for (long i = 0; i < billion; ++i) {
        double y = small - c;
        double t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }

    std::cout << "Kahan sum  = " << std::fixed << std::setprecision(15) << sum <<
        "    (difference = " << std::fabs(expected - sum) << ")" << std::endl;

    return 0;
}

J'obtiens exactement 2.0:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

Et même si je change l' doubles pour floats dans le code ci-dessus, j'obtiens:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

Il semblerait que Kahan est le chemin à parcourir!

14voto

NPE Points 169956

Il existe une classe d'algorithmes qui permettent de résoudre ce problème précis, sans la nécessité de trier ou de revoir l'ordre des données.

En d'autres termes, la somme peut être fait en une seule passe sur les données. Cela rend également ces algorithmes applicables dans des situations où le jeu de données n'est pas connue à l'avance, par exemple, si les données arrivent en temps réel et la somme doit être maintenu.

Voici le résumé d'un article récent:

Nous présentons un roman, un algorithme en ligne exacte de la somme d'un flux des nombres à virgule flottante. Par "en ligne", nous entendons que l'algorithme besoin de voir une seule entrée à la fois, et peut prendre un arbitraire la longueur du flux d'entrée de ces intrants tout en nécessitant une seule constante de la mémoire. Par "exact", nous entendons que la somme du tableau interne de notre l'algorithme est exactement égale à la somme de toutes les entrées, et le résultat retourné est bien arrondi de la somme. La preuve de la justesse est valable pour toutes les entrées (y compris nonnormalized mais des nombres modulo intermédiaire de débordement), et est indépendant du nombre de additionnées ou la condition numéro de la somme. L'algorithme asymptotiquement besoins seulement 5 FLOPs par terme, et grâce à l'instruction au niveau de parallélisme ne fonctionne que sur 2 ou 3 fois plus lent que ce qui est évident, rapide-mais-muets "ordinaire récursive de sommation" de la boucle lorsque le nombre de additionnées, est plus de 10 000. Ainsi, à notre connaissance, c'est la plus rapide, la plus exact, et la mémoire la plus efficace parmi les algorithmes connus. En effet, il est difficile de voir comment un algorithme plus rapide ou un exigeant significativement moins de FLOPs pourrait exister sans les améliorations de matériel. Une demande pour un grand nombre de additionnées, est fourni.

Source: Algorithme 908: en Ligne Exacte de la Somme de la virgule Flottante Flux.

2voto

quamrana Points 6411

La construction de Steve réponse de la première trier les nombres dans l'ordre croissant, je voudrais vous présenter deux autres idées:

  1. Décider sur la différence dans l'exposant de deux nombres au-dessus de laquelle vous pouvez décider que vous ne voudriez pas perdre trop de précision.

  2. Ensuite, additionnez les nombres dans l'ordre jusqu'à l'exposant de l'accumulateur est trop grand pour le prochain numéro, puis mettre la batterie sur une file d'attente temporaire et de commencer à l'accumulateur avec le numéro suivant. Continuer jusqu'à ce que vous avez épuisé la liste d'origine.

Vous répétez le processus avec la file d'attente temporaire (ayant trié) et avec une plus grande différence dans l'exposant.

Je pense que ce sera assez lent si vous avez de calculer les exposants de tous les temps.

J'ai eu un rapide aller avec un programme et le résultat a été 1.99903

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X