Cálculo rápido de Exp: ¿es posible mejorar la precisión sin perder demasiado rendimiento?

Resuelto Anders Gustafsson asked hace 12 años • 7 respuestas

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.

Cociente entre la función exp rápida y regular

¿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.

Anders Gustafsson avatar May 11 '12 20:05 Anders Gustafsson
Aceptado

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.

shoelzer avatar Jan 03 '2013 16:01 shoelzer

Pruebe las siguientes alternativas ( exp1es más rápido, exp7es 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).

Adriano Repetti avatar May 11 '2012 13:05 Adriano Repetti