Cálculo rápido de Exp: ¿es posible mejorar la precisión sin perder demasiado rendimiento?
Estoy probando la función rápida Exp(x) que se describió anteriormente en esta respuesta a una pregunta SO sobre cómo mejorar la velocidad de cálculo en C#:
public static double Exp(double x)
{
var tmp = (long)(1512775 * x + 1072632447);
return BitConverter.Int64BitsToDouble(tmp << 32);
}
La expresión utiliza algunos "trucos" de punto flotante IEEE y está destinada principalmente a su uso en conjuntos neuronales. La función es aproximadamente 5 veces más rápida que la Math.Exp(x)
función normal.
Desafortunadamente, la precisión numérica es solo -4% - +2% en relación con la Math.Exp(x)
función normal; lo ideal sería tener una precisión dentro del rango de al menos el subporcentaje.
He trazado el cociente entre las funciones Exp aproximada y regular y, como se puede ver en el gráfico, la diferencia relativa parece repetirse con una frecuencia prácticamente constante.
¿Es posible aprovechar esta regularidad para mejorar aún más la precisión de la función "fast exp" sin reducir sustancialmente la velocidad de cálculo, o la sobrecarga computacional de una mejora de la precisión superaría la ganancia computacional de la expresión original?
(Como nota al margen, también probé uno de los enfoques alternativos propuestos en la misma pregunta SO, pero este enfoque no parece ser computacionalmente eficiente en C#, al menos no para el caso general).
ACTUALIZACIÓN 14 DE MAYO
A pedido de @Adriano, ahora he realizado una prueba comparativa muy simple. He realizado 10 millones de cálculos utilizando cada una de las funciones exp alternativas para valores de coma flotante en el rango [-100, 100]. Dado que el rango de valores que me interesa abarca de -20 a 0, también he enumerado explícitamente el valor de la función en x = -5. Aquí están los resultados:
Math.Exp: 62.525 ms, exp(-5) = 0.00673794699908547
Empty function: 13.769 ms
ExpNeural: 14.867 ms, exp(-5) = 0.00675211846828461
ExpSeries8: 15.121 ms, exp(-5) = 0.00641270968867667
ExpSeries16: 32.046 ms, exp(-5) = 0.00673666189488182
exp1: 15.062 ms, exp(-5) = -12.3333325982094
exp2: 15.090 ms, exp(-5) = 13.708332516253
exp3: 16.251 ms, exp(-5) = -12.3333325982094
exp4: 17.924 ms, exp(-5) = 728.368055056781
exp5: 20.972 ms, exp(-5) = -6.13293614238501
exp6: 24.212 ms, exp(-5) = 3.55518353166184
exp7: 29.092 ms, exp(-5) = -1.8271053775984
exp7 +/-: 38.482 ms, exp(-5) = 0.00695945286970704
ExpNeural es equivalente a la función Exp especificada al principio de este texto. ExpSeries8 es la formulación que originalmente afirmé que no era muy eficiente en .NET; al implementarlo exactamente como Neil, en realidad fue muy rápido. ExpSeries16 es la fórmula análoga pero con 16 multiplicaciones en lugar de 8. exp1 a exp7 son las diferentes funciones de la respuesta de Adriano a continuación. La variante final de exp7 es una variante en la que se marca el signo de x ; si es negativo, la función regresa 1/exp(-x)
.
Desafortunadamente, ninguna de las funciones expN enumeradas por Adriano es suficiente en el rango de valores negativos más amplio que estoy considerando. El enfoque de expansión en serie de Neil Coffey parece ser más adecuado en "mi" rango de valores, aunque diverge demasiado rápidamente con x negativos más grandes , especialmente cuando se utilizan "sólo" 8 multiplicaciones.
Las aproximaciones de la serie de Taylor (como las expX()
funciones en la respuesta de Adriano ) son más precisas cerca de cero y pueden tener errores enormes en -20 o incluso -5. Si la entrada tiene un rango conocido, como -20 a 0 como la pregunta original, puede usar una pequeña tabla de consulta y una multiplicación adicional para mejorar considerablemente la precisión.
El truco consiste en reconocer que exp() se puede separar en partes enteras y fraccionarias. Por ejemplo:
exp(-2.345) = exp(-2.0) * exp(-0.345)
La parte fraccionaria siempre estará entre -1 y 1, por lo que una aproximación en serie de Taylor será bastante precisa. La parte entera tiene sólo 21 valores posibles para exp(-20) a exp(0), por lo que se pueden almacenar en una pequeña tabla de consulta.
Pruebe las siguientes alternativas ( exp1
es más rápido, exp7
es más preciso).
Código
public static double exp1(double x) {
return (6+x*(6+x*(3+x)))*0.16666666f;
}
public static double exp2(double x) {
return (24+x*(24+x*(12+x*(4+x))))*0.041666666f;
}
public static double exp3(double x) {
return (120+x*(120+x*(60+x*(20+x*(5+x)))))*0.0083333333f;
}
public static double exp4(double x) {
return 720+x*(720+x*(360+x*(120+x*(30+x*(6+x))))))*0.0013888888f;
}
public static double exp5(double x) {
return (5040+x*(5040+x*(2520+x*(840+x*(210+x*(42+x*(7+x)))))))*0.00019841269f;
}
public static double exp6(double x) {
return (40320+x*(40320+x*(20160+x*(6720+x*(1680+x*(336+x*(56+x*(8+x))))))))*2.4801587301e-5;
}
public static double exp7(double x) {
return (362880+x*(362880+x*(181440+x*(60480+x*(15120+x*(3024+x*(504+x*(72+x*(9+x)))))))))*2.75573192e-6;
}
Precisión
Error de función en [-1...1] Error en [3.14...3.14] exp1 0,05 1,8% 8,8742 38,40% exp2 0,01 0,36% 4,8237 20,80% exp3 0,0016152 0,59% 2,28 9,80% exp4 0,0002263 0,0083% 0,9488 4,10% exp5 0,0000279 0,001% 0,3516 1,50% exp6 0,0000031 0,00011% 0,1172 0,50% exp7 0,0000003 0,000011% 0,0355 0,15%
Créditos
Estas implementaciones de exp()
han sido calculadas por "scoofy" usando la serie Taylor de una tanh()
implementación de "fuzzpilz" (quienquiera que sean, acabo de tener estas referencias en mi código).