web-dev-qa-db-fra.com

Dans quel ordre faut-il ajouter des flotteurs pour obtenir le résultat le plus précis?

C'est une question qui m'a été posée lors de ma récente interview et je veux savoir (Je ne me souviens pas vraiment de la théorie de l'analyse numérique, alors aidez-moi s'il vous plaît:)

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

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

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

  • Serait-il préférable de trier ces nombres avant de les accumuler?

  • Quel ordre donnerait la réponse la plus précise?

Je soupçonne que tri les nombres dans l'ordre croissant feraient en fait l'erreur numérique moins, mais malheureusement je ne peux pas le prouver moi-même.

P.S. Je réalise que cela n'a probablement rien à voir avec la programmation du monde réel, juste être curieux.

104
Yippie-Ki-Yay

Votre instinct est fondamentalement juste, le tri par ordre croissant (de magnitude) améliore généralement quelque peu les choses. Considérons le cas où nous ajoutons des flottants simple précision (32 bits), et il y a 1 milliard de valeurs égales à 1/(1 milliard), et une valeur égale à 1. Si le 1 vient en premier, alors la somme viendra à 1, car 1 + (1/1 milliard) est 1 en raison d'une perte de précision. Chaque ajout n'a aucun effet sur le total.

Si les petites valeurs viennent en premier, elles résumeront au moins quelque chose, bien que même alors j'en ai 2 ^ 30, alors qu'après 2 ^ 25 environ, je suis de retour dans la situation où chacune individuellement n'affecte pas le total plus. Je vais donc encore avoir besoin de plus de trucs.

C'est un cas extrême, mais en général, l'ajout de deux valeurs d'amplitude similaire est plus précis que l'ajout de deux valeurs d'amplitudes très différentes, car vous "jetez" moins de bits de précision de la plus petite valeur de cette façon. En triant les nombres, vous regroupez des valeurs de magnitude similaire et en les ajoutant par ordre croissant, vous donnez aux petites valeurs une "chance" d'atteindre cumulativement la magnitude des plus grands nombres.

Pourtant, si des nombres négatifs sont impliqués, il est facile de "déjouer" cette approche. Considérez trois valeurs pour additionner, {1, -1, 1 billionth}. La somme arithmétiquement correcte est 1 billionth, mais si mon premier ajout implique une valeur minuscule, ma somme finale sera 0. Sur les 6 commandes possibles, seulement 2 sont "correctes" - {1, -1, 1 billionth} et {-1, 1, 1 billionth}. Les 6 ordres donnent des résultats précis à l'échelle de la plus grande valeur d'entrée (0,000000001% en sortie), mais pour 4 d'entre eux, le résultat est inexact à l'échelle de la vraie solution (100% en sortie). Le problème particulier que vous résolvez vous dira si le premier est assez bon ou non.

En fait, vous pouvez jouer beaucoup plus de tours que de simplement les ajouter dans un ordre trié. Si vous avez beaucoup de très petites valeurs, un nombre moyen de valeurs intermédiaires et un petit nombre de grandes valeurs, il peut être plus précis de commencer par additionner toutes les petites, puis de additionner séparément les moyennes, d'ajouter ces deux totaux ensemble puis ajoutez les gros. Il n'est pas du tout trivial de trouver la combinaison la plus précise d'ajouts à virgule flottante, mais pour faire face à de très mauvais cas, vous pouvez conserver toute une gamme de totaux cumulés à différentes magnitudes, ajouter chaque nouvelle valeur au total qui correspond le mieux à sa magnitude, et lorsqu'un total cumulé commence à devenir trop grand pour son ampleur, ajoutez-le au total suivant et commencez-en un nouveau. Pris à son extrême logique, ce processus équivaut à effectuer la somme dans un type de précision arbitraire (vous feriez donc cela). Mais étant donné le choix simpliste d'ajouter dans l'ordre de grandeur croissant ou décroissant, l'ascendant est le meilleur pari.

Cela a une certaine relation avec la programmation du monde réel, car il y a des cas où votre calcul peut très mal se passer si vous coupez accidentellement une queue "lourde" composée d'un grand nombre de valeurs dont chacune est trop petite pour être affectée individuellement la somme, ou si vous jetez trop de précision à partir d'un grand nombre de petites valeurs qui n'affectent individuellement que les derniers bits de la somme. Dans les cas où la queue est négligeable de toute façon, vous ne vous en souciez probablement pas. Par exemple, si vous additionnez seulement un petit nombre de valeurs en premier lieu et que vous n'utilisez que quelques chiffres significatifs de la somme.

108
Steve Jessop

Il existe également un algorithme conçu pour ce type d'opération d'accumulation, appelé Kahan Summation , que vous devriez probablement connaître.

Selon Wikipedia,

L'algorithme de sommation de Kahan (également connu sous le nom de sommation compensée ) réduit considérablement la erreur numérique dans le total obtenu en ajoutant une séquence de nombres à virgule flottante de précision finie, par rapport à l'approche évidente. Cela se fait en conservant une compensation de fonctionnement séparée (une variable pour accumuler les petites erreurs).

En pseudocode, 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
87
Daniel Pryden

J'ai essayé l'exemple extrême dans 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 dans la première ligne est plus de dix fois plus grande dans la seconde.

Si je change les doubles en 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)

Aucune des deux réponses n'est même proche de 2.0 (mais la seconde est légèrement plus proche).

Utilisation de la somme de Kahan (avec doubles) comme 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 les doubles en floats dans le code ci-dessus, j'obtiens:

Kahan sum  = 2.000000000000000    (difference = 0.000000000000000)

Il semblerait que Kahan soit la voie à suivre!

34
Andrew Stein

Il existe une classe d'algorithmes qui résolvent ce problème exact, sans avoir besoin de trier ou de réorganiser les données .

En d'autres termes, la sommation peut être effectuée en un seul passage sur les données. Cela rend également ces algorithmes applicables dans des situations où l'ensemble de données n'est pas connu à l'avance, par exemple si les données arrivent en temps réel et que la somme cumulée doit être maintenue.

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

Nous présentons un nouvel algorithme en ligne pour la somme exacte d'un flux de nombres à virgule flottante. Par "en ligne", nous voulons dire que l'algorithme n'a besoin de voir qu'une seule entrée à la fois et peut prendre un flux d'entrée de longueur arbitraire de ces entrées tout en ne nécessitant qu'une mémoire constante. Par "exact", nous voulons dire que la somme du tableau interne de notre algorithme est exactement égale à la somme de toutes les entrées, et le résultat renvoyé est la somme correctement arrondie. La preuve d'exactitude est valable pour toutes les entrées (y compris les nombres non normalisés mais les débordements intermédiaires modulo), et est indépendante du nombre de sommations ou du numéro de condition de la somme. L'algorithme n'a besoin qu'asymptotiquement de 5 FLOP par sommet, et en raison du parallélisme au niveau de l'instruction, il ne fonctionne que 2 à 3 fois plus lentement que la boucle évidente de "sommation récursive ordinaire", rapide mais stupide, lorsque le nombre de sommets est supérieur à 10 000. . Ainsi, à notre connaissance, il est le plus rapide, le plus précis et le plus efficace en mémoire parmi les algorithmes connus. En effet, il est difficile de voir comment un algorithme plus rapide ou nécessitant beaucoup moins de FLOP pourrait exister sans améliorations matérielles. Une demande pour un grand nombre de sommations est fournie.

Source: algorithme 908: sommation exacte en ligne des flux à virgule flottante .

14
NPE

Cela ne répond pas tout à fait à votre question, mais une chose intelligente à faire est d'exécuter la somme deux fois, une fois avec mode d'arrondi "arrondir" et une fois avec "arrondir". Comparez les deux réponses, et vous savez/comment/inexactes vos résultats, et si vous devez donc utiliser une stratégie de sommation plus intelligente. Malheureusement, la plupart des langues ne rendent pas le changement du mode d'arrondi à virgule flottante aussi simple qu'il devrait l'être, car les gens ne savent pas qu'il est réellement utile dans les calculs quotidiens.

Jetez un oeil à arithmétique d'intervalle où vous faites tous les calculs comme celui-ci, en gardant les valeurs les plus élevées et les plus faibles au fur et à mesure. Cela conduit à des résultats et des optimisations intéressants.

2
rjmunro

S'appuyant sur la réponse de Steve de trier d'abord les nombres dans l'ordre croissant, je présenterais deux autres idées:

  1. Décidez de la différence d'exposant de deux nombres au-dessus desquels vous pourriez décider que vous perdriez trop de précision.

  2. Additionnez ensuite les nombres dans l'ordre jusqu'à ce que l'exposant de l'accumulateur soit trop grand pour le nombre suivant, puis placez l'accumulateur dans une file d'attente temporaire et démarrez l'accumulateur avec le nombre suivant. Continuez jusqu'à épuisement de la liste d'origine.

Vous répétez le processus avec la file d'attente temporaire (après l'avoir triée) et avec une différence d'exposant éventuellement plus grande.

Je pense que ce sera assez lent si vous devez calculer les exposants tout le temps.

J'ai eu un essai rapide avec un programme et le résultat a été 1,99903

2
quamrana

Je pense que vous pouvez faire mieux que de trier les chiffres avant de les accumuler, car pendant le processus d'accumulation, l'accumulateur grossit de plus en plus. Si vous avez un grand nombre de nombres similaires, vous commencerez rapidement à perdre de la précision. Voici ce que je suggérerais à la place:

while the list has multiple elements
    remove the two smallest elements from the list
    add them and put the result back in
the single element in the list is the result

Bien sûr, cet algorithme sera plus efficace avec une file d'attente prioritaire au lieu d'une liste. Code C++:

template <typename Queue>
void reduce(Queue& queue)
{
    typedef typename Queue::value_type vt;
    while (queue.size() > 1)
    {
        vt x = queue.top();
        queue.pop();
        vt y = queue.top();
        queue.pop();
        queue.Push(x + y);
    }
}

chauffeur:

#include <iterator>
#include <queue>

template <typename Iterator>
typename std::iterator_traits<Iterator>::value_type
reduce(Iterator begin, Iterator end)
{
    typedef typename std::iterator_traits<Iterator>::value_type vt;
    std::priority_queue<vt> positive_queue;
    positive_queue.Push(0);
    std::priority_queue<vt> negative_queue;
    negative_queue.Push(0);
    for (; begin != end; ++begin)
    {
        vt x = *begin;
        if (x < 0)
        {
            negative_queue.Push(x);
        }
        else
        {
            positive_queue.Push(-x);
        }
    }
    reduce(positive_queue);
    reduce(negative_queue);
    return negative_queue.top() - positive_queue.top();
}

Les nombres dans la file d'attente sont négatifs car top donne le nombre le plus grand, mais nous voulons le le plus petit. J'aurais pu fournir plus d'arguments de modèle à la file d'attente, mais cette approche semble plus simple.

2
fredoverflow

Pour les nombres à simple ou double précision IEEE 754 ou au format connu, une autre alternative consiste à utiliser un tableau de nombres (transmis par l'appelant ou dans une classe pour C++) indexé par l'exposant. Lors de l'ajout de nombres dans le tableau, seuls les nombres ayant le même exposant sont ajoutés (jusqu'à ce qu'un emplacement vide soit trouvé et le nombre stocké). Lorsqu'une somme est demandée, le tableau est sommé du plus petit au plus grand pour minimiser la troncature. Exemple de précision simple:

/* clear array */
void clearsum(float asum[256])
{
size_t i;
    for(i = 0; i < 256; i++)
        asum[i] = 0.f;
}

/* add a number into array */
void addtosum(float f, float asum[256])
{
size_t i;
    while(1){
        /* i = exponent of f */
        i = ((size_t)((*(unsigned int *)&f)>>23))&0xff;
        if(i == 0xff){          /* max exponent, could be overflow */
            asum[i] += f;
            return;
        }
        if(asum[i] == 0.f){     /* if empty slot store f */
            asum[i] = f;
            return;
        }
        f += asum[i];           /* else add slot to f, clear slot */
        asum[i] = 0.f;          /* and continue until empty slot */
    }
}

/* return sum from array */
float returnsum(float asum[256])
{
float sum = 0.f;
size_t i;
    for(i = 0; i < 256; i++)
        sum += asum[i];
    return sum;
}

exemple de double précision:

/* clear array */
void clearsum(double asum[2048])
{
size_t i;
    for(i = 0; i < 2048; i++)
        asum[i] = 0.;
}

/* add a number into array */
void addtosum(double d, double asum[2048])
{
size_t i;
    while(1){
        /* i = exponent of d */
        i = ((size_t)((*(unsigned long long *)&d)>>52))&0x7ff;
        if(i == 0x7ff){         /* max exponent, could be overflow */
            asum[i] += d;
            return;
        }
        if(asum[i] == 0.){      /* if empty slot store d */
            asum[i] = d;
            return;
        }
        d += asum[i];           /* else add slot to d, clear slot */
        asum[i] = 0.;           /* and continue until empty slot */
    }
}

/* return sum from array */
double returnsum(double asum[2048])
{
double sum = 0.;
size_t i;
    for(i = 0; i < 2048; i++)
        sum += asum[i];
    return sum;
}
0
rcgldr

Le tri le plus simple qui améliore la précision est de trier par la valeur absolue croissante. Cela permet aux plus petites valeurs de magnitude d'avoir une chance de s'accumuler ou d'annuler avant d'interagir avec des valeurs de magnitude plus grandes qui auraient déclenché une perte de précision.

Cela dit, vous pouvez faire mieux en suivant plusieurs sommes partielles qui ne se chevauchent pas. Voici un article décrivant la technique et présentant une preuve de précision: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps

Cet algorithme et d'autres approches pour la sommation exacte en virgule flottante sont implémentés en simple Python at: http://code.activestate.com/recipes/393090/ Au moins deux de ceux-ci peuvent être convertis trivialement en C++.

0