Pregunta

He estado perfilando una aplicación todo el día y, habiendo optimizado un par de bits de código, me quedo con esto en mi lista de tareas pendientes. Es la función de activación de una red neuronal, que se llama más de 100 millones de veces. Según dotTrace, representa aproximadamente el 60% del tiempo total de la función.

¿Cómo optimizarías esto?

public static float Sigmoid(double value) {
    return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
¿Fue útil?

Solución

Prueba:

public static float Sigmoid(double value) {
    return 1.0f / (1.0f + (float) Math.Exp(-value));
}

EDITAR: hice un punto de referencia rápido. En mi máquina, el código anterior es aproximadamente un 43% más rápido que su método, y este código matemáticamente equivalente es el más rápido (46% más rápido que el original):

public static float Sigmoid(double value) {
    float k = Math.Exp(value);
    return k / (1.0f + k);
}

EDIT 2: no estoy seguro de la cantidad de sobrecarga que tienen las funciones de C #, pero si #include <math.h> en su código fuente, debería poder usar esto, que usa un flotante-exp función. Puede ser un poco más rápido.

public static float Sigmoid(double value) {
    float k = expf((float) value);
    return k / (1.0f + k);
}

Además, si está haciendo millones de llamadas, la sobrecarga de llamadas a funciones podría ser un problema. Intente hacer una función en línea y vea si eso es de alguna ayuda.

Otros consejos

Si es para una función de activación, ¿importa mucho si el cálculo de e ^ x es completamente exacto?

Por ejemplo, si usa la aproximación (1 + x / 256) ^ 256, en mi prueba Pentium en Java (supongo que C # compila esencialmente las mismas instrucciones del procesador), esto es aproximadamente 7-8 veces más rápido que e ^ x (Math.exp ()), y tiene una precisión de 2 decimales hasta aproximadamente x de +/- 1.5, y dentro del orden correcto de magnitud en todo el rango que usted indicó. (Obviamente, para subir al 256, en realidad cuadras el número 8 veces, ¡no uses Math.Pow para esto!) En Java:

double eapprox = (1d + x / 256d);
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;

Siga duplicando o reduciendo a la mitad 256 (y agregando / eliminando una multiplicación) dependiendo de qué tan precisa desee que sea la aproximación. Incluso con n = 4, todavía da aproximadamente 1,5 lugares decimales de precisión para valores de x entre -0,5 y 0,5 (y parece ser 15 veces más rápido que Math.exp ()).

P.S. Olvidé mencionar que, obviamente, no deberías realmente dividir por 256: multiplicar por un 1/256 constante. El compilador JIT de Java hace esta optimización automáticamente (al menos, Hotspot lo hace), y estaba asumiendo que C # también debe hacerlo.

Eche un vistazo a esta publicación . tiene una aproximación para e ^ x escrita en Java, este debería ser el código C # para ello (no probado):

public static double Exp(double val) {  
    long tmp = (long) (1512775 * val + 1072632447);  
    return BitConverter.Int64BitsToDouble(tmp << 32);  
}

En mis puntos de referencia, esto es más de 5 veces más rápido que Math.exp () (en Java). La aproximación se basa en el papel & Quot; Una aproximación rápida y compacta de la función exponencial " que fue desarrollado exactamente para ser utilizado en redes neuronales. Básicamente es lo mismo que una tabla de búsqueda de 2048 entradas y aproximación lineal entre las entradas, pero todo esto con trucos de coma flotante IEEE.

EDITAR: Según Salsa especial esto es ~ 3.25x más rápido que el Implementación de CLR. Gracias!

  1. Recuerde que cualquier cambio en esta función de activación conlleva un comportamiento diferente . Esto incluso incluye cambiar a flotante (y así reducir la precisión) o usar sustitutos de activación. Solo experimentar con su caso de uso mostrará la forma correcta.
  2. Además de las optimizaciones de código simples, también recomendaría considerar la paralelización de los cálculos (es decir: aprovechar múltiples núcleos de su máquina o incluso máquinas en las nubes de Windows Azure) y mejorar la algoritmos de entrenamiento.

ACTUALIZACIÓN: Publicar en tablas de búsqueda para funciones de activación ANN

ACTUALIZACIÓN2: eliminé el punto en LUT ya que los confundí con el hash completo. Gracias a Henrik Gustafsson por devolverme a la pista. Por lo tanto, la memoria no es un problema, aunque el espacio de búsqueda todavía se confunde con los extremos locales un poco.

Con 100 millones de llamadas, comenzaría a preguntarme si la sobrecarga del generador de perfiles no está sesgando sus resultados. Reemplace el cálculo con un no-op y vea si se informa que todavía consume el 60% del tiempo de ejecución ...

O mejor aún, cree algunos datos de prueba y use un cronómetro para perfilar un millón de llamadas.

Si puede interoperar con C ++, podría considerar almacenar todos los valores en una matriz y recorrerlos usando SSE de esta manera:

void sigmoid_sse(float *a_Values, float *a_Output, size_t a_Size){
    __m128* l_Output = (__m128*)a_Output;
    __m128* l_Start  = (__m128*)a_Values;
    __m128* l_End    = (__m128*)(a_Values + a_Size);

    const __m128 l_One        = _mm_set_ps1(1.f);
    const __m128 l_Half       = _mm_set_ps1(1.f / 2.f);
    const __m128 l_OneOver6   = _mm_set_ps1(1.f / 6.f);
    const __m128 l_OneOver24  = _mm_set_ps1(1.f / 24.f);
    const __m128 l_OneOver120 = _mm_set_ps1(1.f / 120.f);
    const __m128 l_OneOver720 = _mm_set_ps1(1.f / 720.f);
    const __m128 l_MinOne     = _mm_set_ps1(-1.f);

    for(__m128 *i = l_Start; i < l_End; i++){
        // 1.0 / (1.0 + Math.Pow(Math.E, -value))
        // 1.0 / (1.0 + Math.Exp(-value))

        // value = *i so we need -value
        __m128 value = _mm_mul_ps(l_MinOne, *i);

        // exp expressed as inifite series 1 + x + (x ^ 2 / 2!) + (x ^ 3 / 3!) ...
        __m128 x = value;

        // result in l_Exp
        __m128 l_Exp = l_One; // = 1

        l_Exp = _mm_add_ps(l_Exp, x); // += x

        x = _mm_mul_ps(x, x); // = x ^ 2
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_Half, x)); // += (x ^ 2 * (1 / 2))

        x = _mm_mul_ps(value, x); // = x ^ 3
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver6, x)); // += (x ^ 3 * (1 / 6))

        x = _mm_mul_ps(value, x); // = x ^ 4
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver24, x)); // += (x ^ 4 * (1 / 24))

#ifdef MORE_ACCURATE

        x = _mm_mul_ps(value, x); // = x ^ 5
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver120, x)); // += (x ^ 5 * (1 / 120))

        x = _mm_mul_ps(value, x); // = x ^ 6
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver720, x)); // += (x ^ 6 * (1 / 720))

#endif

        // we've calculated exp of -i
        // now we only need to do the '1.0 / (1.0 + ...' part
        *l_Output++ = _mm_rcp_ps(_mm_add_ps(l_One,  l_Exp));
    }
}

Sin embargo, recuerde que las matrices que usará deben asignarse usando _aligned_malloc (some_size * sizeof (float), 16) porque SSE requiere memoria alineada con un límite.

Utilizando SSE, puedo calcular el resultado para los 100 millones de elementos en aproximadamente medio segundo. Sin embargo, asignar esa cantidad de memoria a la vez le costará casi dos tercios de un gigabyte, por lo que sugeriría procesar más, pero matrices más pequeñas a la vez. Es posible que incluso desee considerar el uso de un enfoque de doble buffer con elementos de 100K o más.

Además, si el número de elementos comienza a crecer considerablemente, es posible que desees elegir procesar estas cosas en la GPU (solo crea una textura 1D float4 y ejecuta un sombreador de fragmentos muy trivial).

FWIW, aquí están mis puntos de referencia de C # para las respuestas ya publicadas. (Vacío es una función que solo devuelve 0, para medir la sobrecarga de la llamada de función)

Empty Function:       79ms   0
Original:             1576ms 0.7202294
Simplified: (soprano) 681ms  0.7202294
Approximate: (Neil)   441ms  0.7198783
Bit Manip: (martinus) 836ms  0.72318
Taylor: (Rex Logan)   261ms  0.7202305
Lookup: (Henrik)      182ms  0.7204863
public static object[] Time(Func<double, float> f) {
    var testvalue = 0.9456;
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 1e7; i++)
        f(testvalue);
    return new object[] { sw.ElapsedMilliseconds, f(testvalue) };
}
public static void Main(string[] args) {
    Console.WriteLine("Empty:       {0,10}ms {1}", Time(Empty));
    Console.WriteLine("Original:    {0,10}ms {1}", Time(Original));
    Console.WriteLine("Simplified:  {0,10}ms {1}", Time(Simplified));
    Console.WriteLine("Approximate: {0,10}ms {1}", Time(ExpApproximation));
    Console.WriteLine("Bit Manip:   {0,10}ms {1}", Time(BitBashing));
    Console.WriteLine("Taylor:      {0,10}ms {1}", Time(TaylorExpansion));
    Console.WriteLine("Lookup:      {0,10}ms {1}", Time(LUT));
}

Fuera de mi cabeza, este documento explica una forma de aproximación el exponencial al abusar del punto flotante , (haga clic en el enlace en la parte superior derecha para PDF) pero no sé si le será de mucha utilidad en .NET.

Además, otro punto: con el propósito de entrenar redes grandes rápidamente, el sigmoide logístico que estás usando es bastante terrible. Consulte la sección 4.4 de Efficient Backprop de LeCun et al y use algo centrado en cero (en realidad, lea todo el documento, es inmensamente útil).

F # tiene mejor rendimiento que C # en algoritmos matemáticos .NET. Por lo tanto, reescribir la red neuronal en F # podría mejorar el rendimiento general.

Si volvemos a implementar fragmento de referencia de LUT (he estado usando una versión ligeramente modificada) en F #, luego el código resultante:

  • ejecuta el benchmark sigmoid1 en 588.8ms en lugar de 3899,2ms
  • ejecuta el benchmark sigmoid2 (LUT) en 156.6ms en lugar de 411.4 ms

Se pueden encontrar más detalles en el publicación de blog . Aquí está el FIC del fragmento de F #:

#light

let Scale = 320.0f;
let Resolution = 2047;

let Min = -single(Resolution)/Scale;
let Max = single(Resolution)/Scale;

let range step a b =
  let count = int((b-a)/step);
  seq { for i in 0 .. count -> single(i)*step + a };

let lut = [| 
  for x in 0 .. Resolution ->
    single(1.0/(1.0 +  exp(-double(x)/double(Scale))))
  |]

let sigmoid1 value = 1.0f/(1.0f + exp(-value));

let sigmoid2 v = 
  if (v <= Min) then 0.0f;
  elif (v>= Max) then 1.0f;
  else
    let f = v * Scale;
    if (v>0.0f) then lut.[int (f + 0.5f)]
    else 1.0f - lut.[int(0.5f - f)];

let getError f = 
  let test = range 0.00001f -10.0f 10.0f;
  let errors = seq { 
    for v in test -> 
      abs(sigmoid1(single(v)) - f(single(v)))
  }
  Seq.max errors;

open System.Diagnostics;

let test f = 
  let sw = Stopwatch.StartNew(); 
  let mutable m = 0.0f;
  let result = 
    for t in 1 .. 10 do
      for x in 1 .. 1000000 do
        m <- f(single(x)/100000.0f-5.0f);
  sw.Elapsed.TotalMilliseconds;

printf "Max deviation is %f\n" (getError sigmoid2)
printf "10^7 iterations using sigmoid1: %f ms\n" (test sigmoid1)
printf "10^7 iterations using sigmoid2: %f ms\n" (test sigmoid2)

let c = System.Console.ReadKey(true);

Y la salida (compilación de lanzamiento contra F # 1.9.6.2 CTP sin depurador):

Max deviation is 0.001664
10^7 iterations using sigmoid1: 588.843700 ms
10^7 iterations using sigmoid2: 156.626700 ms

ACTUALIZACIÓN: actualización de la evaluación comparativa para utilizar 10 ^ 7 iteraciones para hacer que los resultados sean comparables con C

ACTUALIZACIÓN2: aquí están los resultados de rendimiento de la implementación de C la misma máquina para comparar:

Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms

Nota: Esto es un seguimiento de esta publicación.

Editar: Actualice para calcular lo mismo que esto y < a href = "https://stackoverflow.com/questions/412019/code-optimizations#416180"> esto , inspirándose en esto .

¡Ahora mira lo que me hiciste hacer! ¡Me hiciste instalar Mono!

$ gmcs -optimize test.cs && mono test.exe
Max deviation is 0.001663983
10^7 iterations using Sigmoid1() took 1646.613 ms
10^7 iterations using Sigmoid2() took 237.352 ms

C ya no vale la pena el esfuerzo, el mundo está avanzando :)

Entonces, un poco más de un factor 10 6 más rápido. Alguien con un cuadro de Windows puede investigar el uso de la memoria y el rendimiento utilizando MS-stuff :)

El uso de LUT para funciones de activación no es tan raro, especialmente cuando se implementa en hardware. Existen muchas variantes probadas del concepto si está dispuesto a incluir ese tipo de tablas. Sin embargo, como ya se ha señalado, el alias puede resultar un problema, pero también hay formas de evitarlo. Algunas lecturas adicionales:

Algunos problemas con esto:

  • El error aumenta cuando alcanza fuera de la tabla (pero converge a 0 en los extremos); para x aprox + -7.0. Esto se debe al factor de escala elegido. Los valores más grandes de ESCALA dan errores más altos en el rango medio, pero más pequeños en los bordes.
  • Esta es generalmente una prueba muy estúpida, y no sé C #, es solo una simple conversión de mi código C :)
  • Rinat Abdullin es muy correcto que el alias y la pérdida de precisión pueden causar problemas, pero desde que tengo No he visto las variables para eso solo puedo aconsejarle que pruebe esto. De hecho, estoy de acuerdo con todo lo que dice, excepto por el tema de las tablas de búsqueda.

Disculpe la codificación de copiar y pegar ...

using System;
using System.Diagnostics;

class LUTTest {
    private const float SCALE = 320.0f;
    private const int RESOLUTION = 2047;
    private const float MIN = -RESOLUTION / SCALE;
    private const float MAX = RESOLUTION / SCALE;

    private static readonly float[] lut = InitLUT();

    private static float[] InitLUT() {
      var lut = new float[RESOLUTION + 1];

      for (int i = 0; i < RESOLUTION + 1; i++) {
        lut[i] = (float)(1.0 / (1.0 + Math.Exp(-i / SCALE)));
      }
      return lut;
    }

    public static float Sigmoid1(double value) {
        return (float) (1.0 / (1.0 + Math.Exp(-value)));
    }

    public static float Sigmoid2(float value) {
      if (value <= MIN) return 0.0f;
      if (value >= MAX) return 1.0f;
      if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
      return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
    }

    public static float error(float v0, float v1) {
      return Math.Abs(v1 - v0);
    }

    public static float TestError() {
        float emax = 0.0f;
        for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
          float v0 = Sigmoid1(x);
          float v1 = Sigmoid2(x);
          float e = error(v0, v1);
          if (e > emax) emax = e;
        }
        return emax;
    }

    public static double TestPerformancePlain() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                Sigmoid1(x);
            }
        }
        sw.Stop();
        return sw.Elapsed.TotalMilliseconds;
    }    

    public static double TestPerformanceLUT() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                Sigmoid2(x);
            }
        }
        sw.Stop();
        return sw.Elapsed.TotalMilliseconds;
    }    

    static void Main() {
        Console.WriteLine("Max deviation is {0}", TestError());
        Console.WriteLine("10^7 iterations using Sigmoid1() took {0} ms", TestPerformancePlain());
        Console.WriteLine("10^7 iterations using Sigmoid2() took {0} ms", TestPerformanceLUT());
    }
}

Primer pensamiento: ¿Qué tal algunas estadísticas sobre la variable de valores?

  • ¿Son los valores de " value " típicamente pequeño -10 < = valor < = 10?

Si no, probablemente pueda obtener un impulso al probar valores fuera de límites

if(value < -10)  return 0;
if(value > 10)  return 1;
  • ¿Se repiten los valores con frecuencia?

Si es así, probablemente pueda obtener algún beneficio de Memoization (probablemente no, pero no hace daño comprobar ...)

if(sigmoidCache.containsKey(value)) return sigmoidCache.get(value);

Si ninguno de estos puede aplicarse, entonces, como han sugerido algunos otros, tal vez pueda evitar reducir la precisión de su sigmoide ...

Soprano tuvo algunas buenas optimizaciones para tu llamada:

public static float Sigmoid(double value) 
{
    float k = Math.Exp(value);
    return k / (1.0f + k);
}

Si prueba una tabla de búsqueda y encuentra que usa demasiada memoria, siempre podría mirar el valor de su parámetro para cada llamada sucesiva y emplear alguna técnica de almacenamiento en caché.

Por ejemplo, intente almacenar en caché el último valor y resultado. Si la siguiente llamada tiene el mismo valor que la anterior, no necesita calcularla ya que habría almacenado en caché el último resultado. Si la llamada actual era la misma que la anterior, incluso 1 de cada 100 veces, podría ahorrarse 1 millón de cálculos.

O, puede encontrar que dentro de 10 llamadas sucesivas, el parámetro de valor es en promedio las mismas 2 veces, por lo que podría intentar almacenar en caché los últimos 10 valores / respuestas.

Idea: ¿Quizás pueda hacer una tabla de búsqueda (grande) con los valores precalculados?

Esto está un poco fuera de tema, pero solo por curiosidad, hice la misma implementación que en C , C # y F # en Java. Dejaré esto aquí en caso de que alguien más sienta curiosidad.

Resultado:

$ javac LUTTest.java && java LUTTest
Max deviation is 0.001664
10^7 iterations using sigmoid1() took 1398 ms
10^7 iterations using sigmoid2() took 177 ms

Supongo que la mejora sobre C # en mi caso se debe a que Java está mejor optimizada que Mono para OS X. En una implementación similar de MS .NET (frente a Java 6 si alguien quiere publicar números comparativos) supongo que los resultados sería diferente.

Código:

public class LUTTest {
    private static final float SCALE = 320.0f;
    private static final  int RESOLUTION = 2047;
    private static final  float MIN = -RESOLUTION / SCALE;
    private static final  float MAX = RESOLUTION / SCALE;

    private static final float[] lut = initLUT();

    private static float[] initLUT() {
        float[] lut = new float[RESOLUTION + 1];

        for (int i = 0; i < RESOLUTION + 1; i++) {
            lut[i] = (float)(1.0 / (1.0 + Math.exp(-i / SCALE)));
        }
        return lut;
    }

    public static float sigmoid1(double value) {
        return (float) (1.0 / (1.0 + Math.exp(-value)));
    }

    public static float sigmoid2(float value) {
        if (value <= MIN) return 0.0f;
        if (value >= MAX) return 1.0f;
        if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
        return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
    }

    public static float error(float v0, float v1) {
        return Math.abs(v1 - v0);
    }

    public static float testError() {
        float emax = 0.0f;
        for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
            float v0 = sigmoid1(x);
            float v1 = sigmoid2(x);
            float e = error(v0, v1);
            if (e > emax) emax = e;
        }
        return emax;
    }

    public static long sigmoid1Perf() {
        float y = 0.0f;
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                y = sigmoid1(x);
            }
        }
        long t1 = System.currentTimeMillis();
        System.out.printf("",y);
        return t1 - t0;
    }    

    public static long sigmoid2Perf() {
        float y = 0.0f;
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                y = sigmoid2(x);
            }
        }
        long t1 = System.currentTimeMillis();
        System.out.printf("",y);
        return t1 - t0;
    }    

    public static void main(String[] args) {

        System.out.printf("Max deviation is %f\n", testError());
        System.out.printf("10^7 iterations using sigmoid1() took %d ms\n", sigmoid1Perf());
        System.out.printf("10^7 iterations using sigmoid2() took %d ms\n", sigmoid2Perf());
    }
}

Me doy cuenta de que ha pasado un año desde que surgió esta pregunta, pero la encontré debido a la discusión sobre el rendimiento de F # y C en relación con C #. Jugué con algunas de las muestras de otros respondedores y descubrí que los delegados parecen ejecutarse más rápido que una invocación de método regular pero no hay una ventaja aparente de rendimiento para F # sobre C # .

  • C: 166ms
  • C # (delegado): 275 ms
  • C # (método): 431 ms
  • C # (método, contador flotante): 2,656 ms
  • F #: 404ms

El C # con un contador flotante era un puerto directo del código C. Es mucho más rápido usar un int en el bucle for.

También podría considerar experimentar con funciones de activación alternativas que son más baratas de evaluar. Por ejemplo:

f(x) = (3x - x**3)/2

(que podría factorizarse como

f(x) = x*(3 - x*x)/2

para una multiplicación menos). Esta función tiene una simetría impar y su derivada es trivial. Usarlo para una red neuronal requiere normalizar la suma de entradas dividiendo por el número total de entradas (limitando el dominio a [-1..1], que también es rango).

Una ligera variación en el tema de Soprano:

public static float Sigmoid(double value) {
    float v = value;
    float k = Math.Exp(v);
    return k / (1.0f + k);
}

Dado que solo busca un resultado de precisión único, ¿por qué hacer que la función Math.Exp calcule un doble? Cualquier calculadora de exponente que utilice una suma iterativa (consulte la expansión de e x ) llevará más tiempo para mayor precisión, todas y cada una de las veces. ¡Y el doble es el doble del trabajo del sencillo! De esta manera, primero se convierte a soltero, luego hace su exponencial.

Pero la función expf debería ser aún más rápida. Sin embargo, no veo la necesidad de emitir soprano (float) pasando a expf, a menos que C # no haga una conversión doble flotante implícita.

De lo contrario, solo use un lenguaje real , como FORTRAN ...

Hay muchas buenas respuestas aquí. Sugeriría ejecutarlo a través de esta técnica , solo para asegurarse

  • No lo estás llamando más veces de lo necesario.
    (A veces, las funciones se llaman más de lo necesario, solo porque son muy fáciles de llamar).
  • No lo estás llamando repetidamente con los mismos argumentos
    (donde podría usar la memorización)

Por cierto, la función que tiene es la función de logit inversa,
o el inverso de la función log-odds-ratio log(f/(1-f)).

(actualizado con mediciones de rendimiento) (actualizado nuevamente con resultados reales :)

Creo que una solución de tabla de búsqueda lo llevaría muy lejos cuando se trata de rendimiento, a un costo insignificante de memoria y precisión.

El siguiente fragmento es un ejemplo de implementación en C (no hablo c # con suficiente fluidez para codificarlo en seco). Se ejecuta y funciona lo suficientemente bien, pero estoy seguro de que hay un error :)

#include <math.h>
#include <stdio.h>
#include <time.h>

#define SCALE 320.0f
#define RESOLUTION 2047
#define MIN -RESOLUTION / SCALE
#define MAX RESOLUTION / SCALE

static float sigmoid_lut[RESOLUTION + 1];

void init_sigmoid_lut(void) {
    int i;    
    for (i = 0; i < RESOLUTION + 1; i++) {
        sigmoid_lut[i] =  (1.0 / (1.0 + exp(-i / SCALE)));
    }
}

static float sigmoid1(const float value) {
    return (1.0f / (1.0f + expf(-value)));
}

static float sigmoid2(const float value) {
    if (value <= MIN) return 0.0f;
    if (value >= MAX) return 1.0f;
    if (value >= 0) return sigmoid_lut[(int)(value * SCALE + 0.5f)];
    return 1.0f-sigmoid_lut[(int)(-value * SCALE + 0.5f)];
}

float test_error() {
    float x;
    float emax = 0.0;

    for (x = -10.0f; x < 10.0f; x+=0.00001f) {
        float v0 = sigmoid1(x);
        float v1 = sigmoid2(x);
        float error = fabsf(v1 - v0);
        if (error > emax) { emax = error; }
    } 
    return emax;
}

int sigmoid1_perf() {
    clock_t t0, t1;
    int i;
    float x, y = 0.0f;

    t0 = clock();
    for (i = 0; i < 10; i++) {
        for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
            y = sigmoid1(x);
        }
    }
    t1 = clock();
    printf("", y); /* To avoid sigmoidX() calls being optimized away */
    return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}

int sigmoid2_perf() {
    clock_t t0, t1;
    int i;
    float x, y = 0.0f;
    t0 = clock();
    for (i = 0; i < 10; i++) {
        for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
            y = sigmoid2(x);
        }
    }
    t1 = clock();
    printf("", y); /* To avoid sigmoidX() calls being optimized away */
    return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}

int main(void) {
    init_sigmoid_lut();
    printf("Max deviation is %0.6f\n", test_error());
    printf("10^7 iterations using sigmoid1: %d ms\n", sigmoid1_perf());
    printf("10^7 iterations using sigmoid2: %d ms\n", sigmoid2_perf());

    return 0;
}

Los resultados anteriores se debieron a que el optimizador hizo su trabajo y optimizó los cálculos. Hacer que realmente ejecute el código produce resultados ligeramente diferentes y mucho más interesantes (en mi camino, lento MB Air):

$ gcc -O2 test.c -o test && ./test
Max deviation is 0.001664
10^7 iterations using sigmoid1: 571 ms
10^7 iterations using sigmoid2: 113 ms

profile


TODO:

Hay cosas que mejorar y formas de eliminar las debilidades; cómo hacerlo se deja como ejercicio para el lector :)

  • Ajuste el rango de la función para evitar el salto donde la tabla comienza y termina.
  • Agregue una función de ruido leve para ocultar los artefactos de aliasing.
  • Como dijo Rex, la interpolación podría llevarlo a un poco más de precisión a la vez que es más barato en cuanto a rendimiento.

Hay funciones mucho más rápidas que hacen cosas muy similares:

x / (1 + abs(x)) & # 8211; reemplazo rápido para TAHN

Y de manera similar:

x / (2 + 2 * abs(x)) + 0.5 - reemplazo rápido de SIGMOID

Compare las parcelas con sigmoide real

Haciendo una búsqueda en Google, encontré una implementación alternativa de la función Sigmoid.

public double Sigmoid(double x)
{
   return 2 / (1 + Math.Exp(-2 * x)) - 1;
}

¿Es correcto para sus necesidades? ¿Es más rápido?

http://dynamicnotions.blogspot.com/2008 /09/sigmoid-function-in-c.html

1) ¿Llamas a esto desde un solo lugar? Si es así, puede obtener una pequeña cantidad de rendimiento moviendo el código fuera de esa función y colocándolo justo donde normalmente habría llamado la función Sigmoide. No me gusta esta idea en términos de legibilidad y organización del código, pero cuando necesita obtener el último aumento de rendimiento, esto podría ayudar porque creo que las llamadas a funciones requieren un empuje / pop de registros en la pila, lo que podría evitarse si su el código estaba todo en línea.

2) No tengo idea si esto podría ayudar, pero intente hacer que su parámetro de función sea un parámetro de referencia. A ver si es más rápido. Hubiera sugerido que sea const (que habría sido una optimización si fuera en c ++) pero c # no admite parámetros const.

Si necesita un aumento de velocidad gigante, probablemente podría considerar paralelizar la función utilizando la fuerza (ge). IOW, use DirectX para controlar que la tarjeta gráfica lo haga por usted. No tengo idea de cómo hacer esto, pero he visto a personas usar tarjetas gráficas para todo tipo de cálculos.

He visto que mucha gente por aquí está tratando de usar la aproximación para hacer que Sigmoid sea más rápido. Sin embargo, es importante saber que Sigmoid también se puede expresar usando tanh, no solo exp. Calcular Sigmoid de esta manera es alrededor de 5 veces más rápido que con exponencial, y al usar este método no estás aproximando nada, por lo tanto, el comportamiento original de Sigmoid se mantiene tal cual.

    public static double Sigmoid(double value)
    {
        return 0.5d + 0.5d * Math.Tanh(value/2);
    }

Por supuesto, la parellización sería el siguiente paso para mejorar el rendimiento, pero en lo que respecta al cálculo bruto, usar Math.Tanh es más rápido que Math.Exp.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top