¿Cómo debo hacer una comparación de punto flotante?

Resuelto Mike Bailey asked hace 13 años • 13 respuestas

Actualmente estoy escribiendo un código donde tengo algo como:

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

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

Y luego, en otros lugares, es posible que necesite hacer igualdad:

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

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

En resumen, tengo muchos cálculos de punto flotante y necesito hacer varias comparaciones de condiciones. No puedo convertirlo a matemáticas de números enteros porque tal cosa no tiene sentido en este contexto.

He leído antes que las comparaciones de punto flotante pueden no ser confiables, ya que pueden suceder cosas como esta:

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

En resumen, me gustaría saber: ¿Cómo puedo comparar de manera confiable números de coma flotante (menor que, mayor que, igualdad)?

El rango de números que estoy usando es aproximadamente de 10E-14 a 10E6, por lo que necesito trabajar tanto con números pequeños como con números grandes.

He etiquetado esto como independiente del idioma porque estoy interesado en cómo puedo lograrlo sin importar el idioma que esté usando.

Mike Bailey avatar Feb 07 '11 02:02 Mike Bailey
Aceptado

TL;DR

  • Utilice la siguiente función en lugar de la solución actualmente aceptada para evitar algunos resultados no deseados en ciertos casos límite, siendo potencialmente más eficiente.
  • Conozca la imprecisión esperada que tiene en sus números e introdúzcalos en consecuencia en la función de comparación.
bool nearly_equal(
  float a, float b,
  float epsilon = 128 * FLT_EPSILON, float abs_th = 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());
  // or even faster: std::min(std::abs(a + b), std::numeric_limits<float>::max());
  // keeping this commented out until I update figures below
  return diff < std::max(abs_th, epsilon * norm);
}

¿Gráficos, por favor?

Al comparar números de coma flotante, existen dos "modos".

El primero es el modo relativo , donde la diferencia entre xy yse considera relativa a su amplitud |x| + |y|. Cuando se traza en 2D, se obtiene el siguiente perfil, donde el verde significa igualdad de xy y. (Tomé un epsilonvalor de 0,5 con fines ilustrativos).

ingrese la descripción de la imagen aquí

El modo relativo es el que se utiliza para valores de coma flotante "normales" o "suficientemente grandes". (Más sobre esto más adelante).

El segundo es un modo absoluto , cuando simplemente comparamos su diferencia con un número fijo. Da el siguiente perfil (nuevamente con un epsilonvalor de 0,5 y un abs_thvalor de 1 a modo ilustrativo).

ingrese la descripción de la imagen aquí

Este modo absoluto de comparación es el que se utiliza para valores de punto flotante "pequeños".

Ahora la pregunta es, ¿cómo unimos esos dos patrones de respuesta?

En la respuesta de Michael Borgwardt, el cambio se basa en el valor de diff, que debería estar por debajo abs_th( Float.MIN_NORMALen su respuesta). Esta zona de cambio se muestra sombreada en el siguiente gráfico.

ingrese la descripción de la imagen aquí

Como abs_th * epsilones más pequeño que abs_th, los parches verdes no se pegan, lo que a su vez le da a la solución una mala propiedad: podemos encontrar tripletas de números tales que x < y_1 < y_2y aún x == y2pero x != y1.

ingrese la descripción de la imagen aquí

Tomemos este sorprendente ejemplo:

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

Tenemos x < y1 < y2, y de hecho y2 - xes más de 2000 veces más grande que y1 - x. Y, sin embargo, con la solución actual,

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

Por el contrario, en la solución propuesta anteriormente, la zona de cambio se basa en el valor de |x| + |y|, que está representado por el cuadrado sombreado a continuación. Garantiza que ambas zonas se conecten con gracia.

ingrese la descripción de la imagen aquí

Además, el código anterior no tiene ramificaciones, lo que podría ser más eficiente. Tenga en cuenta que operaciones como maxy abs, que a priori necesitan ramificaciones, a menudo tienen instrucciones de ensamblaje dedicadas. Por esta razón, creo que este enfoque es superior a otra solución que sería arreglar el problema de Michael nearlyEqualcambiando el interruptor de diff < abs_tha diff < eps * abs_th, lo que produciría esencialmente el mismo patrón de respuesta.

¿Dónde cambiar entre comparación relativa y absoluta?

El cambio entre esos modos se realiza alrededor de abs_th, que se toma como FLT_MINen la respuesta aceptada. Esta elección significa que la representación de float32es lo que limita la precisión de nuestros números de coma flotante.

Esto no siempre tiene sentido. Por ejemplo, si los números que comparas son el resultado de una resta, quizás algo en el rango de FLT_EPSILONtenga más sentido. Si son raíces cuadradas de números restados, la imprecisión numérica podría ser aún mayor.

Es bastante obvio cuando se compara un punto flotante con 0. En este caso, cualquier comparación relativa fracasará, porque |x - 0| / (|x| + 0) = 1. Por lo tanto, la comparación debe cambiar al modo absoluto cuando xsea del orden de la imprecisión de su cálculo, y rara vez es tan baja como FLT_MIN.

Este es el motivo de la introducción del abs_thparámetro anterior.

Además, al no multiplicar abs_thpor epsilon, la interpretación de este parámetro es sencilla y corresponde al nivel de precisión numérica que esperamos de esos números.

Retumbar matemático

(mantenido aquí principalmente para mi propio placer)

De manera más general, supongo que un operador de comparación de punto flotante que se comporta bien =~debería tener algunas propiedades básicas.

Los siguientes son bastante obvios:

  • autoigualdad:a =~ a
  • simetría: a =~ bimplicab =~ a
  • invariancia por oposición: a =~ bimplica-a =~ -b

(No tenemos a =~ be b =~ cimplica a =~ c, =~no es una relación de equivalencia).

Agregaría las siguientes propiedades que son más específicas para las comparaciones de punto flotante

  • si a < b < c, entonces a =~ cimplica a =~ b(los valores más cercanos también deberían ser iguales)
  • si a, b, m >= 0entonces a =~ bimplica a + m =~ b + m(los valores más grandes con la misma diferencia también deberían ser iguales)
  • si 0 <= λ < 1entonces a =~ bimplica λa =~ λb(quizás menos obvio para argumentar a favor).

Esas propiedades ya imponen fuertes restricciones sobre posibles funciones cercanas a la igualdad. La función propuesta anteriormente los verifica. Quizás falten una o varias propiedades que de otro modo serían obvias.

Cuando se piensa en =~una relación de familia de igualdad =~[Ɛ,t]parametrizada por Ɛy abs_th, también se podría agregar

  • si Ɛ1 < Ɛ2entonces a =~[Ɛ1,t] bimplica a =~[Ɛ2,t] b(la igualdad para una tolerancia dada implica igualdad en una tolerancia mayor)
  • si t1 < t2entonces a =~[Ɛ,t1] bimplica a =~[Ɛ,t2] b(la igualdad para una imprecisión dada implica igualdad en una imprecisión mayor)

La solución propuesta también los verifica.

P-Gn avatar Sep 01 '2015 14:09 P-Gn

Comparar para mayor/menor no es realmente un problema a menos que esté trabajando justo en el borde del límite de flotación/doble precisión.

Para una comparación de "iguales difusos", esto (código Java, debería ser fácil de adaptar) es lo que se me ocurrió para La guía de punto flotante después de mucho trabajo y teniendo en cuenta muchas críticas:

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;
    }
}

Viene con un conjunto de pruebas. Debe descartar inmediatamente cualquier solución que no lo haga, porque está prácticamente garantizado que fallará en algunos casos extremos, como tener un valor 0, dos valores muy pequeños opuestos a cero o infinitos.

Una alternativa (consulte el enlace anterior para obtener más detalles) es convertir los patrones de bits de los flotantes a números enteros y aceptar todo dentro de una distancia entera fija.

En cualquier caso, probablemente no exista una solución que sea perfecta para todas las aplicaciones. Lo ideal sería desarrollar/adaptar el suyo propio con un conjunto de pruebas que cubra sus casos de uso reales.

Michael Borgwardt avatar Feb 06 '2011 20:02 Michael Borgwardt

Tenemos que elegir un nivel de tolerancia para comparar números flotantes. Por ejemplo,

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

Una nota. Tu ejemplo es bastante divertido.

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

Algunas matemáticas aquí

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

1/3 != 1

Oh sí..

Quieres decir

if (b != 1)
    Console.WriteLine("Oh no!")
nni6 avatar Sep 27 '2013 12:09 nni6

Idea que tuve para la comparación de punto flotante en 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)
}
Andy Poes avatar Jun 24 '2015 01:06 Andy Poes

Adaptación a PHP de la respuesta de Michael Borgwardt y bosonix:

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;
            }
        }
    }
}
 avatar Jan 16 '2015 17:01

Deberías preguntarte por qué estás comparando los números. Si conoce el propósito de la comparación, también debe conocer la precisión requerida de sus números. Eso es diferente en cada situación y cada contexto de aplicación. Pero en casi todos los casos prácticos se requiere una precisión absoluta . Es muy raro que se pueda aplicar una precisión relativa.

Para dar un ejemplo: si su objetivo es dibujar un gráfico en la pantalla, entonces probablemente desee que los valores de punto flotante se comparen iguales si se asignan al mismo píxel en la pantalla. Si el tamaño de su pantalla es de 1000 píxeles y sus números están en el rango de 1e6, entonces probablemente querrá comparar 100 con 200.

Dada la precisión absoluta requerida, entonces el algoritmo se convierte en:

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;
}
fishinear avatar Aug 20 '2018 13:08 fishinear