Soluciones para errores de redondeo de coma flotante

18

Al crear una aplicación que se ocupa de muchos cálculos matemáticos, me he encontrado con el problema de que ciertos números causan errores de redondeo.

Si bien entiendo que el punto flotante no es exacto , el problema es ¿cómo trato con los números exactos para asegurarme de que cuando se realicen los cálculos en ellos, el redondeo del punto flotante no cause ningún problema?

JNL
fuente
2
¿Hay un problema específico que enfrenta? Hay muchas formas de hacer pruebas, de acuerdo con algún problema. Las preguntas que pueden tener múltiples respuestas no son adecuadas para el formato de preguntas y respuestas. Sería mejor si pudiera definir el problema que tiene de una manera que podría tener una respuesta correcta en lugar de lanzar una red de ideas y recomendaciones.
Estoy creando una aplicación de software con muchas clasificaciones matemáticas. Entiendo que las pruebas NUNIT o JUNIT serían buenas, pero me encantaría tener una idea sobre cómo abordar los problemas con los cálculos matemáticos.
JNL
1
¿Puedes dar un ejemplo de un cálculo que estarías probando? Por lo general, uno no sería una unidad de prueba de matemática en bruto (a menos que esté probando sus propios tipos numéricos), pero probar algo así distanceTraveled(startVel, duration, acceleration)sería probado.
Un ejemplo será tratar con puntos decimales. Por ejemplo, supongamos que estamos construyendo un muro con configuraciones especiales para dist x-0 a x = 14.589 y luego algunos arreglos desde x = 14.589 a x = final del muro. La distancia .589 cuando se convierte en binario no es la misma ... Especialmente si agregamos algunas distancias ... como 14.589 + 0.25 no será igual a 14.84 en binario ... ¿Espero que no sea confuso?
JNL
1
@MichaelT gracias por editar la Pregunta. Ayudó mucho Como soy nuevo en esto, no soy demasiado bueno para enmarcar las preguntas. :) ... Pero estará bien pronto.
JNL

Respuestas:

22

Existen tres enfoques fundamentales para crear tipos numéricos alternativos que estén libres de redondeo de punto flotante. El tema común con estos es que usan matemáticas enteras en su lugar de varias maneras.

Racionales

Representa el número como una parte entera y número racional con un numerador y un denominador. El número 15.589se representaría como w: 15; n: 589; d:1000.

Cuando se agrega a 0.25 (que es w: 0; n: 1; d: 4), esto implica calcular el MCM y luego sumar los dos números. Esto funciona bien para muchas situaciones, aunque puede resultar en números muy grandes cuando se trabaja con muchos números racionales que son relativamente primos entre sí.

Punto fijo

Tienes toda la parte y la parte decimal. Todos los números están redondeados (existe esa palabra, pero ya sabes dónde está) con esa precisión. Por ejemplo, podría tener un punto fijo con 3 puntos decimales. 15.589+ se 0.250convierte en suma 589 + 250 % 1000para la parte decimal (y luego cualquier transferencia a la parte completa). Esto funciona muy bien con las bases de datos existentes. Como se mencionó, hay redondeo, pero usted sabe dónde está y puede especificarlo de manera que sea más preciso de lo necesario (solo está midiendo a 3 puntos decimales, por lo que debe fijarlo en 4).

Punto fijo flotante

Almacenar un valor y la precisión. 15.589se almacena como 15589para el valor y 3para la precisión, mientras que 0.25se almacena como 25y 2. Esto puede manejar precisión arbitraria. Creo que esto es lo que usa la parte interna de los usos BigDecimal de Java (no lo he mirado recientemente). En algún momento, querrá recuperarlo de este formato y mostrarlo, y eso puede implicar redondeo (nuevamente, usted controla dónde está).


Una vez que determine la opción para la representación, puede encontrar bibliotecas de terceros existentes que usen esto o escribir la suya propia. Al escribir el suyo, asegúrese de probarlo en la unidad y asegúrese de estar haciendo los cálculos correctamente.


fuente
2
Es un buen comienzo, pero, por supuesto, no resuelve completamente el problema de redondeo. Los números irracionales como π, e y √2 no tienen una representación estrictamente numérica; debe representarlos simbólicamente si desea una representación exacta, o evaluarlos lo más tarde posible si solo desea minimizar el error de redondeo.
Caleb
@Caleb para irracionales uno tendría que evaluarlos más allá de donde cualquier redondeo podría causar problemas. Por ejemplo, 22/7 tiene una precisión de 0.1% de pi, 355/113 tiene una precisión de 10 ^ -8. Si solo está trabajando con números con 3 decimales, tener 3.141592653 debería evitar cualquier error de redondeo en 3 decimales.
@MichaelT: para agregar números racionales no es necesario encontrar el LCM y es más rápido no hacerlo (y más rápido cancelar "ceros LSB" después, y solo simplificar completamente cuando sea absolutamente necesario). Para los números racionales en general, generalmente es solo "numerador / denominador" solo, o "numerador / denominador << exponente" (y no "parte entera + numerador / denominador"). Además, su "punto fijo flotante" es una representación de punto flotante, y se describiría mejor como "punto flotante de tamaño arbitrario" (para distinguirlo del "punto flotante de tamaño fijo").
Brendan
parte de su terminología es un poco dudosa - el punto fijo flotante no tiene sentido - Creo que está tratando de decir decimal flotante.
jk.
10

Si los valores de punto flotante tienen problemas de redondeo, y no desea tener problemas de redondeo, lógicamente se deduce que el único curso de acción es no usar valores de punto flotante.

Ahora la pregunta es, "¿cómo hago matemáticas que involucran valores no enteros sin variables de punto flotante?" La respuesta es con tipos de datos de precisión arbitraria . Los cálculos son más lentos porque tienen que implementarse en software en lugar de en hardware, pero son precisos. No dijo qué idioma está utilizando, por lo que no puedo recomendar un paquete, pero hay bibliotecas de precisión arbitrarias disponibles para los lenguajes de programación más populares.

Mason Wheeler
fuente
Estoy usando VC ++ en este momento ... Pero agradecería más información sobre otros lenguajes de programación también.
JNL
Incluso sin valores de coma flotante, todavía se encontrará con problemas redondos.
Chad
2
@Chad True, pero el objetivo no es eliminar los problemas de redondeo (que siempre existirán, porque en cualquier base que use hay algunos números que no tienen una representación exacta y no tiene memoria y potencia de procesamiento infinitas), es reduzca al punto que no tiene efecto en el cálculo que está tratando de hacer.
Iker
@Iker Tienes razón. Aunque usted, ni la persona que hace la pregunta, han especificado exactamente qué cálculos están tratando de lograr y la precisión que desean. Necesita responder esa pregunta primero antes de lanzar el arma a la teoría de números. Solo decir lot of mathematical calculationsno es útil ni las respuestas dadas. En la gran mayoría de los casos (si no se trata de divisas), la flotación debería ser suficiente.
Chad
@Chad ese es un punto justo, ciertamente no hay suficientes datos del OP para decir cuál es exactamente el nivel de precisión que necesitan.
Iker
7

La aritmética de coma flotante suele ser bastante precisa (15 dígitos decimales para a double) y bastante flexible. Los problemas surgen cuando haces matemáticas que reducen significativamente la cantidad de dígitos de precisión. Aquí hay unos ejemplos:

  • Cancelación en la resta: 1234567890.12345 - 1234567890.12300el resultado 0.0045tiene solo dos dígitos decimales de precisión. Esto golpea siempre que restas dos números de magnitud similar.

  • Ingestión de precisión: se 1234567890.12345 + 0.123456789012345evalúa 1234567890.24691, se pierden los últimos diez dígitos del segundo operando.

  • Multiplicaciones: si multiplica dos números de 15 dígitos, el resultado tiene 30 dígitos que deben almacenarse. Pero no puede almacenarlos, por lo que se pierden los últimos 15 bits. Esto es especialmente molesto cuando se combina con un sqrt()(como en sqrt(x*x + y*y): El resultado solo tendrá 7,5 dígitos de precisión.

Estas son las principales trampas que debe tener en cuenta. Y una vez que los conozca, puede intentar formular sus matemáticas de una manera que las evite. Por ejemplo, si necesita incrementar un valor una y otra vez en un bucle, evite hacer esto:

for(double f = f0; f < f1; f += df) {

Después de algunas iteraciones, el más grande fse tragará parte de la precisión de df. Peor aún, los errores se sumarán, lo que conducirá a la situación contraintuitiva de que un tamaño más pequeño dfpuede conducir a peores resultados generales. Mejor escribe esto:

for(int i = 0; i < (f1 - f0)/df; i++) {
    double f = f0 + i*df;

Como está combinando los incrementos en una sola multiplicación, el resultado fserá preciso a 15 dígitos decimales.

Este es solo un ejemplo, hay otras formas de evitar la pérdida de precisión debido a otras razones. Pero ya ayuda mucho pensar en la magnitud de los valores involucrados e imaginar qué sucedería si hiciera sus cálculos con lápiz y papel, redondeando a un número fijo de dígitos después de cada paso.

cmaster - restablecer monica
fuente
2

Cómo asegurarse de que no tiene problemas: aprenda sobre problemas aritméticos de punto flotante, o contrate a alguien que lo tenga, o use algo de sentido común.

El primer problema es la precisión. En muchos idiomas tiene "flotante" y "doble" (doble posición para "doble precisión"), y en muchos casos "flotante" le da una precisión de aproximadamente 7 dígitos, mientras que el doble le da 15. El sentido común es que si tiene un situación en la que la precisión podría ser un problema, 15 dígitos es muchísimo mejor que 7 dígitos. En muchas situaciones ligeramente problemáticas, usar "doble" significa que te saldrás con la tuya, y "flotar" significa que no. Digamos que la capitalización de mercado de una empresa es de 700 mil millones de dólares. Representa esto en flotante, y el bit más bajo es $ 65536. Represente usando doble, y el bit más bajo es de aproximadamente 0.012 centavos. Entonces, a menos que realmente sepas lo que estás haciendo, usarás doble, no flotante.

El segundo problema es más una cuestión de principios. Si hace dos cálculos diferentes que deberían dar el mismo resultado, a menudo no lo hacen debido a errores de redondeo. Dos resultados que deberían ser iguales serán "casi iguales". Si dos resultados están juntos, los valores reales pueden ser iguales. O tal vez no lo sean. Debe tener eso en cuenta y debe escribir y usar funciones que digan "x es definitivamente mayor que y" o "x es definitivamente menor que y" o "x e y podrían ser iguales".

Este problema empeora mucho si usa el redondeo, por ejemplo, "redondear x al entero más cercano". Si multiplica 120 * 0.05, el resultado debería ser 6, pero lo que obtendrá es "algún número muy cercano a 6". Si luego "redondea al número entero más cercano", ese "número muy cercano a 6" podría ser "ligeramente menor que 6" y redondearse a 5. Y tenga en cuenta que no importa la precisión que tenga. No importa qué tan cerca de 6 esté su resultado, siempre que sea menor que 6.

Y tercero, algunos problemas son difíciles . Eso significa que no hay una regla rápida y fácil. Si su compilador admite "doble largo" con más precisión, puede usar "doble largo" y ver si hace la diferencia. Si no hay diferencia, entonces estás bien o tienes un problema realmente complicado. Si hace el tipo de diferencia que esperarías (como un cambio en el 12º decimal), entonces probablemente estés bien. Si realmente cambia sus resultados, entonces tiene un problema. Pedir ayuda.

gnasher729
fuente
1
No hay nada de "sentido común" sobre las matemáticas de coma flotante.
cuál es
Obtenga más información al respecto.
gnasher729
0

La mayoría de las personas comete el error cuando ven el doble y gritan BigDecimal, cuando en realidad acaban de trasladar el problema a otra parte. Doble da bit de signo: 1 bit, ancho de exponente: 11 bits. Precisión significativa: 53 bits (52 almacenados explícitamente). Debido a la naturaleza del doble, cuanto mayor sea el número entero, perderá una precisión relativa. Para calcular la precisión relativa que usamos aquí está abajo.

Precisión relativa del doble en el cálculo usamos el siguiente foluma 2 ^ E <= abs (X) <2 ^ (E + 1)

epsilon = 2 ^ (E-10)% Para un flotante de 16 bits (media precisión)

 Accuracy Power | Accuracy -/+| Maximum Power | Max Interger Value
 2^-1           | 0.5         | 2^51          | 2.2518E+15
 2^-5           | 0.03125     | 2^47          | 1.40737E+14
 2^-10          | 0.000976563 | 2^42          | 4.39805E+12
 2^-15          | 3.05176E-05 | 2^37          | 1.37439E+11
 2^-20          | 9.53674E-07 | 2^32          | 4294967296
 2^-25          | 2.98023E-08 | 2^27          | 134217728
 2^-30          | 9.31323E-10 | 2^22          | 4194304
 2^-35          | 2.91038E-11 | 2^17          | 131072
 2^-40          | 9.09495E-13 | 2^12          | 4096
 2^-45          | 2.84217E-14 | 2^7           | 128
 2^-50          | 8.88178E-16 | 2^2           | 4

En otras palabras, si desea una precisión de +/- 0.5 (o 2 ^ -1), el tamaño máximo que puede ser el número es 2 ^ 52. Más grande que esto y la distancia entre los números de coma flotante es mayor que 0.5.

Si desea una precisión de +/- 0.0005 (aproximadamente 2 ^ -11), el tamaño máximo que puede ser el número es 2 ^ 42. Más grande que esto y la distancia entre los números de coma flotante es mayor que 0.0005.

Realmente no puedo dar una mejor respuesta que esta. El usuario necesitará determinar qué precisión quiere al realizar el cálculo necesario y su valor unitario (metros, pies, pulgadas, mm, cm). Para la gran mayoría de los casos, flotar será suficiente para simulaciones simples dependiendo de la escala del mundo que pretendes simular.

Aunque es algo que decir, si solo pretende simular un mundo de 100 metros por 100 metros, tendrá un lugar del orden de precisión cercano a 2 ^ -45. Esto ni siquiera explica cómo la FPU moderna dentro de las CPU realizará cálculos fuera del tamaño de tipo nativo y solo después de que se complete el cálculo se redondeará (dependiendo del modo de redondeo de FPU) al tamaño de tipo nativo.

Chad
fuente