web-dev-qa-db-fra.com

Comment dois-je faire une comparaison en virgule flottante?

J'écris actuellement un code dans lequel j'ai quelque chose comme:

double a = SomeCalculation1();
double b = SomeCalculation2();

if (a < b)
    DoSomething2();
else if (a > b)
    DoSomething3();

Et puis, dans d'autres endroits, je devrais peut-être faire l'égalité:

double a = SomeCalculation3();
double b = SomeCalculation4();

if (a == 0.0)
   DoSomethingUseful(1 / a);
if (b == 0.0)
   return 0; // or something else here

En bref, j'ai beaucoup de calculs en virgule flottante et je dois faire diverses comparaisons des conditions. Je ne peux pas le convertir en maths entier car une telle chose n'a pas de sens dans ce contexte.

J'ai déjà lu que les comparaisons en virgule flottante peuvent ne pas être fiables, car vous pouvez avoir des problèmes comme celui-ci:

double a = 1.0 / 3.0;
double b = a + a + a;
if ((3 * a) != b)
    Console.WriteLine("Oh no!");

En bref, je voudrais savoir: Comment puis-je comparer de manière fiable des nombres en virgule flottante (inférieurs à, supérieurs à, égaux)?

La plage de nombres que j'utilise varie approximativement de 10E-14 à 10E6. Je dois donc travailler avec des nombres petits et grands.

Je l'ai étiquetée comme étant agnostique, car je m'intéresse à la façon dont je peux y parvenir, quelle que soit la langue utilisée.

69
Mike Bailey

La comparaison entre grand et petit n'est pas vraiment un problème, à moins que vous ne travailliez exactement à la limite de la limite de flottement/double précision.

Pour une comparaison "fuzzy equals", ceci (le code Java devrait être facile à adapter) est ce que j'ai proposé pour Le Guide des points flottants après beaucoup de travail et en tenant compte de nombreuses critiques:

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

Il vient avec une suite de tests. Vous devez immédiatement rejeter toute solution qui ne le ferait pas, car dans certains cas Edge, son échec est pratiquement garanti, par exemple si vous avez une valeur 0, deux valeurs très petites opposées à zéro ou des infinis.

Une alternative (voir le lien ci-dessus pour plus de détails) consiste à convertir les modèles de bits des flotteurs en nombres entiers et à accepter tout ce qui se trouve dans une distance de nombre entier fixe.

Dans tous les cas, il n’ya probablement pas de solution parfaite pour toutes les applications. Idéalement, vous développeriez/adapteriez le vôtre avec une suite de tests couvrant vos cas d'utilisation réels.

60
Michael Borgwardt

J'ai eu le problème de comparer les nombres à virgule flottante A < B et A > B Voici ce qui semble fonctionner:

if(A - B < Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is less than B");
}

if (A - B > Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is greater than B");
}

Les fabs - valeur absolue - s’occupe de leur égalité.

12
tech_loafer

Nous devons choisir un niveau de tolérance pour comparer les nombres flottants. Par exemple,

final float TOLERANCE = 0.00001;
if (Math.abs(f1 - f2) < TOLERANCE)
    Console.WriteLine("Oh yes!");

Une note. Votre exemple est plutôt amusant.

double a = 1.0 / 3.0;
double b = a + a + a;
if (a != b)
    Console.WriteLine("Oh no!");

Quelques maths ici

a = 1/3
b = 1/3 + 1/3 + 1/3 = 1.

1/3 != 1

Oh oui..

Tu veux dire

if (b != 1)
    Console.WriteLine("Oh no!")
11
nni6

TL; DR

  • Utilisez la fonction suivante au lieu de la solution actuellement acceptée pour éviter certains résultats indésirables dans certains cas limites, tout en étant potentiellement plus efficace.
  • Connaissez l'imprécision attendue de vos chiffres et intégrez-les en conséquence dans la fonction de comparaison.
bool nearly_equal(
  float a, float b,
  float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
  // those defaults are arbitrary and could be removed
{
  assert(std::numeric_limits<float>::epsilon() <= epsilon);
  assert(epsilon < 1.f);

  if (a == b) return true;

  auto diff = std::abs(a-b);
  auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
  return diff < std::max(relth, epsilon * norm);
}

Des graphiques, s'il vous plaît?

Lors de la comparaison de nombres en virgule flottante, il existe deux "modes".

Le premier est le mode relatif, où la différence entre x et y est considérée par rapport à leur amplitude |x| + |y|. Lorsque tracé en 2D, il donne le profil suivant, où le vert signifie l'égalité de x et y. (J'ai pris une epsilon de 0,5 à des fins d'illustration).

 enter image description here

Le mode relatif correspond à ce qui est utilisé pour les valeurs de points flottants "normales" ou "suffisamment grandes". (Plus sur cela plus tard).

Le second est un mode absolu, lorsque nous comparons simplement leur différence à un nombre fixe. Il donne le profil suivant (encore une fois avec une epsilon de 0,5 et une relth de 1 pour illustration).

 enter image description here

Ce mode de comparaison absolu est utilisé pour les "minuscules" valeurs en virgule flottante.

Maintenant, la question est de savoir comment assembler ces deux modèles de réponse.

Dans la réponse de Michael Borgwardt, le changement est basé sur la valeur de diff, qui devrait être inférieure à relth (Float.MIN_NORMAL dans sa réponse). Cette zone de commutation est hachurée dans le graphique ci-dessous. 

 enter image description here

Comme relth * epsilon est plus petit que relth, les patchs verts ne collent pas, ce qui confère à la solution une mauvaise propriété: nous pouvons trouver des triplets de nombres tels que x < y_1 < y_2 et pourtant x == y2 mais x != y1.

 enter image description here

Prenons cet exemple frappant:

x  = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32

Nous avons x < y1 < y2, et en fait y2 - x est plus de 2000 fois plus grand que y1 - x. Et pourtant, avec la solution actuelle,

nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True

En revanche, dans la solution proposée ci-dessus, la zone de commutation est basée sur la valeur de |x| + |y|, qui est représentée par le carré hachuré ci-dessous. Cela garantit que les deux zones se connectent harmonieusement.

 enter image description here

De plus, le code ci-dessus n'a pas de branche, ce qui pourrait être plus efficace. Considérez que des opérations telles que max et abs, qui a priori a besoin d'être ramifiées, ont souvent des instructions de montage dédiées. Pour cette raison, je pense que cette approche est supérieure à une autre solution qui consisterait à corriger la variable nearlyEqual de Michael en modifiant le commutateur de diff < relth à diff < eps * relth, ce qui produirait essentiellement le même motif de réponse.

Où passer de la comparaison relative à la comparaison absolue?

Le basculement entre ces modes est effectué autour de relth, qui est pris comme FLT_MIN dans la réponse acceptée. Ce choix signifie que la représentation de float32 est ce qui limite la précision de nos nombres à virgule flottante.

Cela n'a pas toujours de sens. Par exemple, si les chiffres que vous comparez sont les résultats d'une soustraction, il est peut-être plus judicieux d'utiliser quelque chose qui se situe dans la plage de FLT_EPSILON. S'ils sont des racines carrées de nombres soustraits, l'imprécision numérique pourrait être encore plus grande.

Il est plutôt évident de comparer un nombre à virgule flottante avec 0. Ici, toute comparaison relative échouera, car |x - 0| / (|x| + 0) = 1. La comparaison doit donc passer en mode absolu lorsque x est dans l’ordre de l’imprécision de votre calcul - et est rarement aussi bas que FLT_MIN.

C’est la raison de l’introduction du paramètre relth ci-dessus.

De plus, en ne multipliant pas relth par epsilon, l'interprétation de ce paramètre est simple et correspond au niveau de précision numérique que nous attendons de ces nombres.

Grondement mathématique

(gardé ici surtout pour mon propre plaisir)

Plus généralement, je suppose qu'un opérateur de comparaison de virgule flottante bien conçu, =~, devrait avoir certaines propriétés de base.

Les éléments suivants sont plutôt évidents:

  • égalité personnelle: a =~ a
  • symmetry: a =~ b implique b =~ a
  • invariance par opposition: a =~ b implique -a =~ -b

(Nous n'avons pas a =~ b et b =~ c implique a =~ c, =~ n'est pas une relation d'équivalence).

J'ajouterais les propriétés suivantes plus spécifiques aux comparaisons en virgule flottante

  • si a < b < c, alors a =~ c implique a =~ b (les valeurs les plus proches doivent également être égales)
  • si a, b, m >= 0 alors a =~ b implique a + m =~ b + m (des valeurs plus grandes avec la même différence devraient également être égales)
  • si 0 <= λ < 1 alors a =~ b implique λa =~ λb (peut-être moins évident pour argumenter pour).

Ces propriétés imposent déjà de fortes contraintes sur les éventuelles fonctions de quasi égalité. La fonction proposée ci-dessus les vérifie. Peut-être une ou plusieurs propriétés par ailleurs évidentes sont manquantes.

Quand on pense à =~ comme une famille de relations d’égalité =~[Ɛ,t] paramétrée par Ɛ et relth, on peut aussi ajouter

  • si Ɛ1 < Ɛ2 alors a =~[Ɛ1,t] b implique a =~[Ɛ2,t] b (une égalité pour une tolérance donnée implique une égalité avec une tolérance supérieure)
  • si t1 < t2 alors a =~[Ɛ,t1] b implique a =~[Ɛ,t2] b (l'égalité pour une imprécision donnée implique une égalité pour une imprécision plus élevée)

La solution proposée les vérifie également.

8
P-Gn

Idée que j'avais pour la comparaison en virgule flottante dans Swift

infix operator ~= {}

func ~= (a: Float, b: Float) -> Bool {
    return fabsf(a - b) < Float(FLT_EPSILON)
}

func ~= (a: CGFloat, b: CGFloat) -> Bool {
    return fabs(a - b) < CGFloat(FLT_EPSILON)
}

func ~= (a: Double, b: Double) -> Bool {
    return fabs(a - b) < Double(FLT_EPSILON)
}
3
Andy Poes

Adaptation à PHP de Michael Borgwardt & bosonix's answer:

class Comparison
{
    const MIN_NORMAL = 1.17549435E-38;  //from Java Specs

    // from http://floating-point-gui.de/errors/comparison/
    public function nearlyEqual($a, $b, $epsilon = 0.000001)
    {
        $absA = abs($a);
        $absB = abs($b);
        $diff = abs($a - $b);

        if ($a == $b) {
            return true;
        } else {
            if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) {
                return $diff < ($epsilon * self::MIN_NORMAL);
            } else {
                return $diff / ($absA + $absB) < $epsilon;
            }
        }
    }
}
1
Dennis

Vous devriez vous demander pourquoi vous comparez les chiffres. Si vous connaissez le but de la comparaison, vous devez également connaître l'exactitude requise de vos chiffres. C'est différent dans chaque situation et chaque contexte d'application. Mais dans presque tous les cas pratiques, il existe une précision requise absolue. Ce n’est que très rarement qu’une précision relative est applicable. 

Pour donner un exemple: si votre objectif est de tracer un graphique à l'écran, vous voudrez probablement que les valeurs en virgule flottante soient comparées égales si elles correspondent au même pixel à l'écran. Si la taille de votre écran est de 1 000 pixels et que vos chiffres sont compris entre 1 et 6, vous voudrez probablement que 100 se compare à 200. 

Compte tenu de la précision absolue requise, l’algorithme devient:

public static ComparisonResult compare(float a, float b, float accuracy) 
{
    if (isnan(a) || isnan(b))   // if NaN needs to be supported
        return UNORDERED;    
    if (a == b)                 // short-cut and takes care of infinities
        return EQUAL;           
    if (abs(a-b) < accuracy)    // comparison wrt. the accuracy
        return EQUAL;
    if (a < b)                  // larger / smaller
        return SMALLER;
    else
        return LARGER;
}
0
fishinear

Le conseil standard consiste à utiliser une petite valeur "epsilon" (choisie en fonction de votre application, probablement), et à considérer les flottants situés dans epsilon les mêmes. par exemple. quelque chose comme

#define EPSILON 0.00000001

if ((a - b) < EPSILON && (b - a) < EPSILON) {
  printf("a and b are about equal\n");
}

Une réponse plus complète est compliquée, car l’erreur en virgule flottante est extrêmement subtile et déroutante. Si vous vous souciez vraiment de l'égalité dans un sens précis, vous cherchez probablement une solution qui n'implique pas de virgule flottante.

0
nelhage

J'ai essayé d'écrire une fonction d'égalité avec les commentaires ci-dessus à l'esprit. Voici ce que je suis venu avec:

Modifier: passez de Math.Max ​​(a, b) à Math.Max ​​(Math.Abs ​​(a), Math.Abs ​​(b))

static bool fpEqual(double a, double b)
{
    double diff = Math.Abs(a - b);
    double epsilon = Math.Max(Math.Abs(a), Math.Abs(b)) * Double.Epsilon;
    return (diff < epsilon);
}

Pensées? Je dois encore travailler plus et moins bien.

0
Mike Bailey