Esta fue una pregunta que me hicieron en mi entrevista reciente y quiero saber (en realidad no recuerdo la teoría del análisis numérico, así que por favor ayúdenme :)
Si tenemos alguna función, que acumula números de punto flotante:
std::accumulate(v.begin(), v.end(), 0.0);
v
es un std::vector<float>
, por ejemplo.
¿Sería mejor ordenar estos números antes de acumularlos?
¿Qué orden daría la respuesta más precisa?
Sospecho que la clasificación de los números en orden ascendente en realidad lo haría el error numérico menos , pero por desgracia no puedo demostrar que yo mismo.
PD: Me doy cuenta de que esto probablemente no tiene nada que ver con la programación del mundo real, solo tengo curiosidad.
c++
floating-point
precision
Yippie-Ki-Yay
fuente
fuente
Respuestas:
Su instinto es básicamente correcto, ordenar en orden ascendente (de magnitud) generalmente mejora un poco las cosas. Considere el caso en el que estamos agregando flotadores de precisión simple (32 bits), y hay mil millones de valores iguales a 1 / (mil millones) y un valor igual a 1. Si el 1 es primero, entonces la suma vendrá a 1, ya que 1 + (1/1 billón) es 1 debido a la pérdida de precisión. Cada adición no tiene ningún efecto en el total.
Si los valores pequeños vienen primero, al menos sumarán algo, aunque incluso entonces tengo 2 ^ 30 de ellos, mientras que después de 2 ^ 25 más o menos estoy de vuelta en la situación en la que cada uno individualmente no afecta el total nunca más. Así que todavía voy a necesitar más trucos.
Ese es un caso extremo, pero en general, sumar dos valores de magnitud similar es más exacto que sumar dos valores de magnitudes muy diferentes, ya que "descarta" menos bits de precisión en el valor más pequeño de esa manera. Al ordenar los números, agrupa valores de magnitud similar y, al sumarlos en orden ascendente, le da a los valores pequeños una "posibilidad" de alcanzar acumulativamente la magnitud de los números más grandes.
Aún así, si se trata de números negativos, es fácil "burlar" este enfoque. Considere tres valores Resumiendo,
{1, -1, 1 billionth}
. La suma aritméticamente correcta es1 billionth
, pero si mi primera adición involucra el valor minúsculo, entonces mi suma final será 0. De los 6 órdenes posibles, solo 2 son "correctos" -{1, -1, 1 billionth}
y{-1, 1, 1 billionth}
. Los 6 órdenes dan resultados que son precisos en la escala del valor de mayor magnitud en la entrada (0,0000001% fuera), pero para 4 de ellos el resultado es inexacto en la escala de la solución verdadera (100% fuera). El problema particular que está resolviendo le dirá si el primero es lo suficientemente bueno o no.De hecho, puede jugar muchos más trucos que simplemente agregarlos en orden ordenado. Si tiene muchos valores muy pequeños, un número medio de valores medios y un número pequeño de valores grandes, entonces podría ser más exacto sumar primero todos los pequeños, luego sumar los medianos por separado, sumar esos dos totales juntos luego agregue los grandes. No es en absoluto trivial encontrar la combinación más precisa de adiciones de punto flotante, pero para hacer frente a casos realmente malos, puede mantener un conjunto completo de totales acumulados en diferentes magnitudes, agregar cada nuevo valor al total que mejor coincida con su magnitud, y cuando un total acumulado comience a ser demasiado grande para su magnitud, agréguelo al siguiente total y comience uno nuevo. Llevado a su extremo lógico, este proceso es equivalente a realizar la suma en un tipo de precisión arbitraria (por lo que ' haría eso). Pero dada la opción simplista de sumar en orden de magnitud ascendente o descendente, ascender es la mejor apuesta.
Tiene alguna relación con la programación del mundo real, ya que hay algunos casos en los que su cálculo puede salir muy mal si accidentalmente corta una cola "pesada" que consiste en una gran cantidad de valores, cada uno de los cuales es demasiado pequeño para afectar individualmente la suma, o si descarta demasiada precisión de una gran cantidad de valores pequeños que individualmente solo afectan a los últimos bits de la suma. En los casos en que la cola es insignificante de todos modos, probablemente no le importe. Por ejemplo, si solo está sumando una pequeña cantidad de valores en primer lugar y solo está usando algunas cifras significativas de la suma.
fuente
También hay un algoritmo diseñado para este tipo de operación de acumulación, llamado Kahan Summation , que probablemente debería conocer.
Según Wikipedia,
fuente
sum
yc
de diferente magnitud. Puede extenderse trivialmente a N variables.-ffast-math
de GCC).-ffast-math
. Lo que aprendí de esta discusión y este enlace es que si le importa la precisión numérica, probablemente debería evitar usarla,-ffast-math
pero eso en muchas aplicaciones donde puede estar limitado a la CPU pero no le importan los cálculos numéricos precisos (programación de juegos, por ejemplo ),-ffast-math
es de uso razonable. Por lo tanto, me gustaría enmendar mi comentario "prohibido" fuertemente redactado.sum, c, t, y
ayudará. También debe agregarsum -= c
antesreturn sum
.Probé el ejemplo extremo en la respuesta proporcionada por Steve Jessop.
Obtuve el siguiente resultado:
El error en la primera línea es más de diez veces mayor en la segunda.
Si cambio la
double
safloat
s en el código anterior, obtengo:Ninguna de las respuestas se acerca siquiera a 2.0 (pero la segunda está un poco más cerca).
Usando la suma de Kahan (con
double
s) como lo describe Daniel Pryden:Obtengo exactamente 2.0:
E incluso si cambio las
double
s porfloat
s en el código anterior, obtengo:¡Parece que Kahan es el camino a seguir!
fuente
double
no sufre nada malo pérdida de precisión al sumar mil millonésimas, ya que tiene 52 bits significativos, mientras que IEEEfloat
solo tiene 24 y tendría.c
para contener valores mucho más grandes que el siguiente sumando. Esto significa que la suma es mucho, mucho menor que la suma principal, por lo que tendrá que haber una gran cantidad de ellos para sumar mucho. Especialmente con ladouble
aritmética.Existe una clase de algoritmos que resuelven este problema exacto, sin la necesidad de ordenar o reordenar los datos .
En otras palabras, la suma se puede realizar en una pasada sobre los datos. Esto también hace que dichos algoritmos sean aplicables en situaciones en las que el conjunto de datos no se conoce de antemano, por ejemplo, si los datos llegan en tiempo real y es necesario mantener la suma acumulada.
Aquí está el resumen de un artículo reciente:
Fuente: Algoritmo 908: Suma exacta en línea de corrientes de punto flotante .
fuente
Sobre la base de la respuesta de Steve de ordenar primero los números en orden ascendente, presentaré dos ideas más:
Decida la diferencia en exponente de dos números por encima del cual podría decidir que perdería demasiada precisión.
Luego sume los números en orden hasta que el exponente del acumulador sea demasiado grande para el siguiente número, luego coloque el acumulador en una cola temporal y comience el acumulador con el siguiente número. Continúe hasta agotar la lista original.
Repite el proceso con la cola temporal (habiéndola ordenado) y con una diferencia posiblemente mayor en el exponente.
Creo que esto será bastante lento si tienes que calcular exponentes todo el tiempo.
Probé rápidamente un programa y el resultado fue 1.99903
fuente
Creo que puedes hacerlo mejor que ordenar los números antes de acumularlos, porque durante el proceso de acumulación, el acumulador se hace cada vez más grande. Si tiene una gran cantidad de números similares, comenzará a perder precisión rápidamente. Esto es lo que sugeriría en su lugar:
Por supuesto, este algoritmo será más eficiente con una cola de prioridad en lugar de una lista. Código C ++:
conductor:
Los números en la cola son negativos porque
top
produce el número más grande , pero queremos el más pequeño . Podría haber proporcionado más argumentos de plantilla a la cola, pero este enfoque parece más simple.fuente
Esto no responde del todo a su pregunta, pero una cosa inteligente que puede hacer es ejecutar la suma dos veces, una con el modo de redondeo "redondear hacia arriba" y una vez con "redondear hacia abajo". Compare las dos respuestas y sabrá / cómo / inexactos son sus resultados, y si por lo tanto necesita utilizar una estrategia de suma más inteligente. Desafortunadamente, la mayoría de los lenguajes no hacen que cambiar el modo de redondeo de coma flotante sea tan fácil como debería ser, porque la gente no sabe que es realmente útil en los cálculos diarios.
Eche un vistazo a la aritmética de intervalos, donde hace todas las matemáticas como esta, manteniendo los valores más altos y más bajos a medida que avanza. Conduce a algunas optimizaciones y resultados interesantes.
fuente
El más simple tipo que mejora la precisión es para ordenar por el valor absoluto ascendente. Eso permite que los valores de magnitud más pequeños tengan la oportunidad de acumularse o cancelarse antes de interactuar con valores de magnitud más grandes que provocarían una pérdida de precisión.
Dicho esto, puede hacerlo mejor si realiza un seguimiento de varias sumas parciales que no se superponen. Aquí hay un artículo que describe la técnica y presenta una prueba de precisión: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
Ese algoritmo y otros enfoques para la suma exacta de punto flotante se implementan en Python simple en: http://code.activestate.com/recipes/393090/ Al menos dos de ellos se pueden convertir trivialmente a C ++.
fuente
Para los números de formato conocido o de precisión simple o doble IEEE 754, otra alternativa es usar una matriz de números (pasados por el llamador o en una clase para C ++) indexados por el exponente. Al agregar números a la matriz, solo se agregan números con el mismo exponente (hasta que se encuentra un espacio vacío y se almacena el número). Cuando se solicita una suma, la matriz se suma de menor a mayor para minimizar el truncamiento. Ejemplo de precisión simple:
ejemplo de doble precisión:
fuente
Sus flotadores deben agregarse con doble precisión. Eso le dará más precisión adicional que cualquier otra técnica. Para un poco más de precisión y significativamente más velocidad, puede crear, digamos, cuatro sumas y sumarlas al final.
Si está agregando números de doble precisión, use long double para la suma; sin embargo, esto solo tendrá un efecto positivo en implementaciones donde long double en realidad tiene más precisión que double (típicamente x86, PowerPC dependiendo de la configuración del compilador).
fuente
Con respecto a la clasificación, me parece que si espera una cancelación, los números deben agregarse en orden descendente de magnitud, no ascendente. Por ejemplo:
((-1 + 1) + 1e-20) dará 1e-20
pero
((1e-20 + 1) - 1) dará 0
En la primera ecuación se anulan dos números grandes, mientras que en la segunda el término 1e-20 se pierde cuando se suma a 1, ya que no hay suficiente precisión para retenerlo.
Además, la suma por pares es bastante decente para sumar muchos números.
fuente