Calcul robuste de la moyenne de deux nombres en virgule flottante?


15

Laissez x, ydeux nombres à virgule flottante. Quelle est la bonne façon de calculer leur moyenne?

La façon naïve (x+y)/2peut entraîner des refoulements quand xet ysont trop grandes. Je pense que c'est 0.5 * x + 0.5 * ypeut-être mieux, mais cela implique deux multiplications (ce qui est peut-être inefficace), et je ne sais pas si c'est assez bon. Y a-t-il une meilleure façon?

Une autre idée avec laquelle je joue est de savoir (y/2)(1 + x/y)si x<=y. Mais encore une fois, je ne sais pas comment analyser cela et prouver qu'il répond à mes exigences.

De plus, j'ai besoin d'une garantie que la moyenne calculée sera >= min(x,y)et <= max(x,y). Comme indiqué dans la réponse de Don Hatch , une meilleure façon de poser cette question est peut-être la suivante: quelle est l'implémentation de la moyenne de deux nombres qui donne toujours le résultat le plus précis possible? Autrement dit, si xet ysont des nombres à virgule flottante, comment calculer le nombre à virgule flottante le plus proche de (x+y)/2? Dans ce cas, la moyenne calculée est automatiquement >= min(x,y)et <= max(x,y). Voir la réponse de Don Hatch pour plus de détails.

Remarque: Ma priorité est une précision robuste. L'efficacité est consommable. Cependant, s'il existe de nombreux algorithmes robustes et précis, je choisirais le plus efficace.


(+1) Question intéressante, étonnamment non triviale.
Kirill

1
Dans le passé, les valeurs à virgule flottante étaient calculées et conservées sous une forme plus précise pour les résultats intermédiaires. Si a + b (double 64 bits) produit un résultat intermédiaire de 80 bits et que c'est ce qui est divisé par 2, vous n'avez pas à vous soucier du débordement. La perte de précision est moins évidente.
JDługosz

La solution à cela semble relativement simple ( j'ai ajouté une réponse ). Le fait est que je suis un programmeur et non un expert en informatique, alors qu'est-ce qui me manque qui rend cette question tellement plus difficile?
IQAndreas

Ne vous inquiétez pas du coût des multiplications et des divisions par deux; votre compilateur les optimisera pour vous.
Federico Poloni

Réponses:


18

Je pense que la précision et la stabilité des algorithmes numériques de Higham explique comment analyser ces types de problèmes. Voir le chapitre 2, en particulier l'exercice 2.8.

Dans cette réponse, je voudrais souligner quelque chose qui n'est pas vraiment abordé dans le livre de Higham (il ne semble pas être très largement connu, d'ailleurs). Si vous souhaitez prouver les propriétés d'algorithmes numériques simples comme ceux-ci, vous pouvez utiliser la puissance des solveurs SMT modernes ( Satisfiability Modulo Theories ), tels que z3 , en utilisant un package tel que sbv dans Haskell. C'est un peu plus facile que d'utiliser du crayon et du papier.

Supposons que l'on me donne , et j'aimerais savoir si z = ( x + y ) / 2 satisfait x z y . Le code Haskell suivant0Xyz=(X+y)/2xzy

import Data.SBV

test1 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test1 fun =
  do [x, y] <- sFloats ["x", "y"]
     constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
     constrain $ 0 .<= x &&& x .<= y
     let z = fun x y
     return $ x .<= z &&& z .<= y

test2 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test2 fun =
  do [x, y] <- sFloats ["x", "y"]
     constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
     constrain $ x .<= y
     let z = fun x y
     return $ x .<= z &&& z .<= y

me permettra de le faire automatiquement . Voici test1 funla proposition que pour tous les flotteurs finis x , y avec 0 x y .xfun(X,y)yX,y0Xy

λ> prove $ test1 (\x y -> (x + y) / 2)
Falsifiable. Counter-example:
  x = 2.3089316e36 :: Float
  y = 3.379786e38 :: Float

Il déborde. Supposons que je prenne maintenant votre autre formule: z=x/2+y/2

λ> prove $ test1 (\x y -> x/2 + y/2)
Falsifiable. Counter-example:
  x = 2.3509886e-38 :: Float
  y = 2.3509886e-38 :: Float

Ne fonctionne pas (en raison d'un débordement progressif: , ce qui pourrait ne pas être intuitif en raison de l'arithmétique de base 2).(x/2)×2x

Essayez maintenant :z=x+(yx)/2

λ> prove $ test1 (\x y -> x + (y-x)/2)
Q.E.D.

Travaux! Le Q.E.D.est une preuve que la test1propriété est valable pour tous les flottants tels que définis ci-dessus.

Qu'en est-il de la même chose, mais limité à (au lieu de 0 x y )?xy0xy

λ> prove $ test2 (\x y -> x + (y-x)/2)
Falsifiable. Counter-example:
  x = -3.1300826e34 :: Float
  y = 3.402721e38 :: Float

D'accord, donc si déborde, qu'en est-il de z = x + ( y / 2 - x / 2 ) ?yxz=x+(y/2x/2)

λ> prove $ test2 (\x y -> x + (y/2 - x/2))
Q.E.D.

Il semble donc que parmi les formules que j'ai essayées ici, semble fonctionner (avec une preuve aussi). L'approche du solveur SMT me semble un moyen beaucoup plus rapide de répondre aux soupçons sur les formules simples à virgule flottante que de passer par l'analyse d'erreurs à virgule flottante avec un crayon et du papier.x+(y/2X/2)

Enfin, l'objectif de précision et de stabilité est souvent en contradiction avec l'objectif de performance. Pour les performances, je ne vois pas vraiment comment vous pouvez faire mieux que , d'autant plus que le compilateur fera toujours le gros du travail pour traduire cela en instructions machine pour vous.(X+y)/2

XX+(y/2-X/2)ySFloatSDouble

-ffast-math(X+y)/2

PPPS Je me suis un peu emporté en ne regardant que les expressions algébriques simples sans conditions. La formule de Don Hatch est strictement meilleure.


2
Attendez; avez-vous prétendu que si x <= y (indépendamment du fait que x> = 0 ou non) alors x + (y / 2-x / 2) soit une bonne façon de procéder? Il me semble que cela ne peut pas être correct, car cela donne la mauvaise réponse dans le cas suivant lorsque la réponse est exactement représentable: x = -1, y = 1 + 2 ^ -52 (le plus petit nombre représentable supérieur à 1), auquel cas la réponse est 2 ^ -53. Confirmation en python: >>> x = -1.; y = 1.+2.**-52; print `2**-53`, `(x+y)/2.`, `x+(y/2.-x/2.)`
Don Hatch

2
X(X+y)/2yX,y(X+y)/2(X+y)/2

8

Tout d'abord, observez que si vous avez une méthode qui donne une réponse la plus précise dans tous les cas, elle satisfera à votre condition requise. (Notez que je dis une réponse la plus exacte plutôt que la réponse la plus précise, car il peut y avoir deux gagnants.) Preuve: si, au contraire, vous avez une réponse aussi précise que possible qui ne remplit pas la condition requise, que signifie soit answer<min(x,y)<=max(x,y)(dans ce cas, min(x,y)c'est une meilleure réponse, une contradiction), soit min(x,y)<=max(x,y)<answer(dans ce cas, max(x,y)c'est une meilleure réponse, une contradiction).

Je pense donc que cela signifie que votre question se résume à trouver la réponse la plus précise possible. En supposant l'arithmétique IEEE754 tout au long, je propose ce qui suit:

if max(abs(x),abs(y)) >= 1.:
    return x/2. + y/2.
else:
    return (x+y)/2.

Mon argument selon lequel cela donne une réponse plus précise est une analyse de cas quelque peu fastidieuse. Voici:

  • Cas max(abs(x),abs(y)) >= 1.:

    • Le sous-cas ni x ni y n'est dénormalisé: dans ce cas, la réponse calculée x/2.+y/2.manipule les mêmes mantisses et donne donc exactement la même réponse que le calcul de (x+y)/2donnerait si nous supposions des exposants étendus pour empêcher le débordement. Cette réponse peut dépendre du mode d'arrondi mais dans tous les cas, elle est garantie par IEEE754 pour être la meilleure réponse possible (du fait que le calcul x+yest garanti comme étant la meilleure approximation de x + y mathématique, et la division par 2 est exacte dans ce cas). Cas).
    • Le sous-cas x est dénormalisé (et ainsi abs(y)>=1):

      answer = x/2. + y/2. = y/2. since abs(x/2.) is so tiny compared to abs(y/2.) = the exact mathematical value of y/2 = a best possible answer.

    • La sous-case y est dénormalisée (et ainsi abs(x)>=1): analogue.

  • Cas max(abs(x),abs(y)) < 1.:
    • Sous-cas, le calcul x+yest soit non dénormalisé soit dénormalisé et "pair": bien que le calcul x+yne soit pas exact, il est garanti par IEEE754 comme étant la meilleure approximation possible de la mathématique x + y. Dans ce cas, la division suivante par 2 dans l'expression (x+y)/2.est exacte, donc la réponse calculée (x+y)/2.est la meilleure approximation possible de la mathématique (x + y) / 2.
    • Sous-cas, le calcul x+yest dénormalisé et "impair": dans ce cas, exactement l'un des x, y doit également être dénormalisé et "impair", ce qui signifie que l'autre de x, y est dénormalisé avec le signe opposé, et donc le calcul x+yest exactement le mathématique x + y, et donc le calcul (x+y)/2.est garanti par IEEE754 comme étant la meilleure approximation possible du mathématique (x + y) / 2.

Je me rends compte que lorsque j'ai dit "dénormalisé", je voulais vraiment dire autre chose - c'est-à-dire des nombres aussi proches les uns des autres que les nombres, c'est-à-dire la plage de nombres qui est à peu près deux fois plus grande que la plage de nombres dénormalisés, c'est-à-dire les 8 premiers ticks ou plus dans le diagramme à en.wikipedia.org/wiki/Denormal_number . Le fait est que ceux "impairs" sont les seuls nombres pour lesquels les diviser par deux n'est pas exact. Je dois reformuler cette partie de la réponse pour que ce soit clair.
Don Hatch

Fl(op(X,y))=op(X,y)(1+δ)|δ|uX/2+y/2(X+y)/2sont toujours correctement arrondis, sans débordement / sous-dépassement, il ne reste plus rien à afficher, ce qui est facile.
Kirill

@Kirill, je suis un peu perdu ... d'où viens-tu? De plus, je ne pense pas qu'il soit tout à fait vrai que "les divisions par 2 sont exactes pour les nombres non dénormaux" ... c'est la même chose que j'ai trébuché, et il semble être un peu gênant d'essayer de faire les choses correctement. L'énoncé précis est quelque chose de plus comme "x / 2 est exact tant que abs (x) est au moins deux fois le plus grand nombre subnormal" ... argh, maladroit!
Don Hatch

3

Pour les formats binaires à virgule flottante IEEE-754, illustrés par le binary64calcul (double précision), S. Boldo a formellement prouvé que l'algorithme simple présenté ci-dessous fournit la moyenne correctement arrondie.

Sylvie Boldo, "Vérification formelle des programmes calculant la moyenne à virgule flottante." Dans International Conference on Formal Engineering Methods , pp. 17-32. Springer, Cham, 2015. ( projet en ligne )

(X+y)/2X/2+y/2binary64C[2-967,2970]C afin de fournir les meilleures performances pour un cas d'utilisation particulier.

Cela donne l'exemple de ISO-C99code suivant:

double average (double x, double y) 
{
    const double C = 1; /* 0x1p-967 <= C <= 0x1p970 */
    return (C <= fabs (x)) ? (x / 2 + y / 2) : ((x + y) / 2);
}

Dans des travaux de suivi récents, S. Boldo et ses co-auteurs ont montré comment obtenir les meilleurs résultats possibles pour les formats décimaux à virgule flottante IEEE-754 en utilisant des opérations de multiplication-addition (FMA) fusionnées et une précision bien connue. bloc de construction doublant (TwoSum):

Sylvie Boldo, Florian Faissole et Vincent Tourneur, «Un algorithme formellement prouvé pour calculer la moyenne correcte des nombres décimaux à virgule flottante». In 25th IEEE Symposium on Computer Arithmetic (ARITH 25) , juin 2018, p. 69-75. ( projet en ligne )


2

Bien qu'il ne soit pas très efficace en termes de performances, il existe un moyen très simple de (1) s'assurer qu'aucun des nombres n'est supérieur à l'un xou à l' autre y(pas de débordements) et (2) garder le point flottant aussi "précis" que possible (et (3) , comme bonus supplémentaire, même si la soustraction est utilisée, aucune valeur ne sera jamais stockée sous forme de nombres négatifs.

float difference = max(x, y) - min(x, y);
return min(x, y) + (difference / 2.0);

En fait, si vous voulez vraiment rechercher la précision, vous n'avez même pas besoin d'effectuer la division sur place; il suffit de renvoyer les valeurs de min(x, y)et differenceque vous pouvez utiliser pour simplifier logiquement ou manipuler plus tard.


Ce que j'essaie de comprendre maintenant, c'est comment faire fonctionner cette même réponse avec plus de deux éléments , tout en gardant toutes les variables inférieures au plus grand des nombres, et en utilisant une seule opération de division pour préserver la précision.
IQAndreas

@becko Yup, vous feriez la division au moins deux fois. De plus, l'exemple que vous avez donné ferait mal répondre. Imaginez la moyenne de 2,4,9, ce n'est pas la même chose que la moyenne de 3,9.
IQAndreas

Vous avez raison, ma récursivité était mauvaise. Je ne sais pas comment le réparer maintenant, sans perdre en précision.
Becko

Pouvez-vous prouver que cela donne le résultat le plus précis possible? Autrement dit, si xet ysont à virgule flottante, votre calcul produit un virgule flottante le plus proche de (x+y)/2?
Becko

1
Ne débordera-t-il pas lorsque x, y sont les plus petits et les plus grands nombres exprimables?
Don Hatch

1

Convertissez en précision supérieure, ajoutez-y les valeurs et reconvertissez.

Il ne doit pas y avoir de débordement dans la précision supérieure et si les deux sont dans la plage de virgule flottante valide, le nombre calculé doit également être à l'intérieur.

Et cela devrait être entre les deux, le pire des cas seulement la moitié du plus grand nombre si la précision n'est pas suffisante.


C'est l'approche de la force brute. Cela fonctionne probablement, mais je cherchais une analyse qui ne nécessitait pas une précision intermédiaire supérieure. Pouvez-vous également estimer la précision intermédiaire nécessaire? Dans tous les cas, ne supprimez pas cette réponse (+1), je ne l'accepterai pas comme réponse.
Becko

1

Théoriquement, x/2peut être calculé en soustrayant 1 de la mantisse.

Cependant, l'implémentation d'opérations au niveau du bit comme celle-ci n'est pas nécessairement simple, surtout si vous ne connaissez pas le format de vos nombres à virgule flottante.

Si vous pouvez le faire, toute l'opération est réduite à 3 additions / soustractions, ce qui devrait être une amélioration significative.


0

Je pensais dans le même sens que @Roland Heath mais je ne peux pas encore commenter, voici mon point de vue:

x/2peut être calculé en soustrayant 1 de l' exposant (pas la mantisse, la soustraction de 1 de la mantisse soustrait 2^(value_of_exponent-length_of_mantissa)de la valeur globale).

Sans restriction du cas général, supposons x < y. (Si x > y, réétiquetez les variables. Si x = y, (x+y) / 2est trivial.)

  • Transformer (x+y) / 2en x/2 + y/2, qui peut être effectué par deux soustractions entières (par une de l'exposant)
    • Cependant, il y a une limite inférieure sur l'exposant en fonction de votre représentation. Si votre exposant est déjà minime avant de soustraire 1, cette méthode nécessitera une gestion de cas particulière. Un exposant minimal sur xrendra x/2plus petit que représentable (en supposant que la mantisse est représentée avec un interligne implicite 1).
    • Au lieu de soustraire 1 de l'exposant de x, déplacez xla mantisse de un vers la droite (et ajoutez le premier implicite, le cas échéant).
    • Soustrayez 1 de l'exposant de y, s'il n'est pas minimal. S'il est minimal (y est plus grand que x, à cause de la mantisse), déplacez la mantisse vers la droite de un (ajoutez le début implicite 1, le cas échéant).
    • Déplacer la nouvelle mantisse de xvers la droite en fonction de l'exposant de y.
    • Effectuez l'addition entière sur les mantisses, à moins que la mantisse de xn'ait été complètement déplacée. Si les deux exposants étaient minimes, les principaux déborderont, ce qui est correct, car ce débordement est censé redevenir implicite.
  • et un ajout de virgule flottante.
    • Je ne peux penser à aucun cas spécial ici; à l'exception de l'arrondi, qui s'applique également au décalage décrit ci-dessus.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.