web-dev-qa-db-fra.com

Optimisation mathématique en C #

J'ai profilé une application toute la journée et, après avoir optimisé quelques morceaux de code, je reste avec cela sur ma liste de tâches. C'est la fonction d'activation d'un réseau de neurones appelé plus de 100 millions de fois. Selon dotTrace, il représente environ 60% du temps de fonctionnement total.

Comment optimiseriez-vous cela?

public static float Sigmoid(double value) {
    return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
53
hb.

Essayer:

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

EDIT: J'ai fait un repère rapide. Sur ma machine, le code ci-dessus est environ 43% plus rapide que votre méthode, et ce code équivalent du point de vue mathématique est le bit le plus adolescent (46% plus rapide que l'original):

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

EDIT 2: Je ne suis pas sûr de la charge supplémentaire des fonctions C #, mais si vous #include <math.h> dans votre code source, vous devriez pouvoir l'utiliser, qui utilise une fonction float-exp. Cela pourrait être un peu plus rapide.

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

De plus, si vous passez des millions d’appels, le temps système d’appel de fonction peut poser problème. Essayez de créer une fonction en ligne et voyez si cela vous aide.

53
Sophie Alpert

Si c'est pour une fonction d'activation, cela a-t-il une importance capitale si le calcul de e ^ x est complètement exact?

Par exemple, si vous utilisez l'approximation (1 + x/256) ^ 256, lors de mes tests Pentium en Java (je suppose que C # est essentiellement compilé selon les mêmes instructions de processeur), cela est environ 7-8 fois plus rapide que e ^ x (Math.exp ()), et est précis à 2 décimales près, jusqu'à environ x de +/- 1,5, et dans le bon ordre de grandeur dans la plage que vous avez indiquée. (Évidemment, pour passer à 256, vous quadrillez le nombre 8 fois - n'utilisez pas Math.Pow pour cela!) En Java:

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

Continuez à doubler ou réduire de moitié 256 (et à ajouter/supprimer une multiplication) en fonction de la précision de l'approximation. Même avec n = 4, il donne toujours environ 1,5 décimale de précision pour les valeurs de x comprises entre -0,5 et 0,5 (et apparaît 15 fois plus vite que Math.exp ()).

P.S. J'ai oublié de mentionner - vous ne devriez évidemment pas vraiment diviser par 256: multiplier par une constante de 1/256. Le compilateur JIT de Java effectue cette optimisation automatiquement (du moins Hotspot), et je supposais que C # devait le faire aussi.

30
Neil Coffey

Regardez ce post . il a une approximation pour e ^ x écrit en Java, il devrait s'agir du code C # correspondant (non testé):

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

Dans mes tests, c'est plus que 5 fois plus rapide que Math.exp () (en Java). L'approximation est basée sur le document " Une approximation rapide et compacte de la fonction exponentielle " qui a été développé avec précision pour être utilisé dans les réseaux de neurones. C'est fondamentalement la même chose qu'une table de correspondance de 2048 entrées et une approximation linéaire entre les entrées, mais tout cela avec des astuces à virgule flottante IEEE.

EDIT: Selon Sauce spéciale c'est ~ 3,25x plus rapide que l'implémentation CLR. Merci!

21
martinus
  1. Rappelez-vous que tout changement dans cette fonction d'activation entraîne un comportement différent . Cela inclut même la commutation sur float (et donc la diminution de la précision) ou l'utilisation de substituts d'activation. Seulement expérimenter avec votre cas d'utilisation montrera le bon chemin.
  2. En plus des simples optimisations de code, je vous recommanderais également d’envisager la parallélisation des calculs (c’est-à-dire: exploiter plusieurs cœurs de votre machine ou même des machines sur les nuages ​​Windows Azure) et améliorer les algorithmes de formation.

UPDATE: Publication dans les tables de consultation pour les fonctions d'activation ANN

UPDATE2: J'ai supprimé le point sur les LUT puisque je les ai confondues avec le hachage complet. Merci à Henrik Gustafsson de m'avoir remis sur la piste. La mémoire n’est donc pas un problème, même si l’espace de recherche est encore un peu perturbé par les extrema locaux.

14
Rinat Abdullin

À 100 millions d'appels, je me demandais si les frais généraux du profileur ne faussaient pas vos résultats. Remplacez le calcul par un no-op et voyez s'il est encore consomme 60% du temps d'exécution ... 

Ou mieux encore, créez des données de test et utilisez un chronomètre pour profiler environ un million d'appels. 

8
Shog9

Si vous êtes capable d'interopérer avec C++, vous pouvez envisager de stocker toutes les valeurs d'un tableau et de les parcourir en boucle en utilisant SSE comme ceci:

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));
    }
}

Cependant, rappelez-vous que les tableaux que vous utiliserez doivent être alloués avec _aligned_malloc (taille_complète * (size), 16), car SSE nécessite une mémoire alignée sur une limite.

En utilisant SSE, je peux calculer le résultat pour les 100 millions d'éléments en une demi-seconde environ. Cependant, l'allocation de cette quantité de mémoire à la fois vous coûtera près de deux tiers de giga-octets, donc je suggérerais de traiter davantage de tableaux, mais de plus en plus petits à la fois. Vous voudrez peut-être même envisager d'utiliser une approche de double tampon avec 100 000 éléments ou plus.

En outre, si le nombre d'éléments commence à augmenter considérablement, vous pouvez choisir de traiter ces éléments sur le GPU (créez simplement une texture 1D float4 et exécutez un fragment shader très trivial).

8
Jasper Bekkers

FWIW, voici mes repères C # pour les réponses déjà postées. (Vide est une fonction qui ne retourne que 0 pour mesurer le temps système de l'appel de fonction) 

 Fonction vide: 79ms 0 
 Original: 1576ms 0.7202294 
 Simplifié: (soprano) 681ms 0.7202294 
 Approximatif: (Neil) 441ms 0.7198783 
 Bit Manip : (martinus) 836ms 0.72318 
 Taylor: (Rex Logan) 261ms 0.7202305 
 Recherche: (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));
}
7
Jimmy

F # a de meilleures performances que C # dans les algorithmes mathématiques .NET. La réécriture du réseau neuronal en F # pourrait donc améliorer les performances globales.

Si nous réimplémentons extrait de référence LUT } _ (j'ai utilisé une version légèrement modifiée) en F #, le code résultant: 

  • exécute le repère sigmoid1 dans 588.8ms au lieu de 3899,2ms
  • exécute le benchmark sigmoid2 (LUT) dans 156.6ms au lieu de 411.4 ms

Plus de détails peuvent être trouvés dans le blog post . Voici l'extrait FIC JIC:

#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);

Et la sortie (publication de la compilation avec CTP F # 1.9.6.2 sans débogueur):

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

UPDATE: analyse comparative mise à jour permettant d'utiliser 10 ^ 7 itérations pour obtenir des résultats comparables à C

UPDATE2: voici les résultats de performance de la implémentation C de la même machine à comparer:

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

De mémoire, cet article explique une façon de approximer l’exponentielle en abusant de la virgule flottante , (cliquez sur le lien en haut à droite pour PDF), mais je ne sais pas si elle sera d’une grande utilité. à vous dans .NET.

Autre point également: dans le but de former rapidement de grands réseaux, le sigmoïde logistique que vous utilisez est assez terrible. Voir la section 4.4 de Efficient Backprop de LeCun et al et utilisez quelque chose de zéro-centré (en fait, lisez tout cet article, il est extrêmement utile).

5
dwf

Note: Ceci est un suivi de this post.

Edit: Update pour calculer la même chose que this et this , en s’inspirant de this .

Maintenant regarde ce que tu m'as fait faire! Tu m'as fait installer 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 ne vaut plus la peine, le monde avance :)

Donc, juste un facteur dix 6 plus vite. Quelqu'un possédant une fenêtre Windows peut étudier l'utilisation de la mémoire et les performances à l'aide de MS-stuff :)

L'utilisation de tables de conversion (LUT) pour les fonctions d'activation n'est pas si rare, surtout lorsqu'elle est implémentée dans le matériel. Il existe de nombreuses variantes du concept qui ont fait leurs preuves si vous souhaitez inclure ces types de tableaux. Cependant, comme cela a déjà été souligné, le pseudonyme peut s'avérer être un problème, mais il existe également des solutions. Quelques lectures supplémentaires:

Quelques pièges avec ça:

  • L'erreur augmente lorsque vous atteignez l'extérieur de la table (mais converge vers 0 aux extrêmes); pour x environ + -7,0. Cela est dû au facteur de mise à l'échelle choisi. Des valeurs plus élevées de SCALE donnent des erreurs plus élevées dans la plage médiane, mais plus petites sur les bords.
  • C’est généralement un test très stupide, et je ne connais pas le C #, c’est une simple conversion de mon C-code :)
  • Rinat Abdullin est tout à fait exact que le repliement de spectre et la perte de précision peuvent poser problème, mais comme je n’ai pas vu les variables pour cela, je ne peux que vous conseiller d’essayer. En fait, je suis d'accord avec tout ce qu'il dit, à l'exception de la question des tables de consultation.

Pardonnez le codage copier-coller ...

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());
    }
}
5
Henrik Gustafsson

Soprano a eu quelques optimisations intéressantes à votre appel:

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

Si vous essayez une table de correspondance et découvrez qu'elle utilise trop de mémoire, vous pouvez toujours consulter la valeur de votre paramètre pour chaque appel successif et utiliser une technique de mise en cache. 

Par exemple, essayez de mettre en cache la dernière valeur et le dernier résultat. Si l'appel suivant a la même valeur que le précédent, vous n'avez pas besoin de le calculer car vous auriez mis en cache le dernier résultat. Si l'appel en cours était identique à l'appel précédent, même 1 fois sur 100, vous pourriez économiser 1 million de calculs.

Ou, vous pouvez constater que, dans 10 appels successifs, le paramètre value est en moyenne identique 2 fois, vous pouvez donc essayer de mettre en cache les 10 dernières valeurs/réponses.

4
Jeremy

Première pensée: que diriez-vous de statistiques sur la variable de valeurs?

  • Les valeurs de "valeur" sont-elles typiquement petites -10 <= valeur <= 10? 

Sinon, vous pouvez probablement obtenir un coup de pouce en testant des valeurs hors limites. 

if(value < -10)  return 0;
if(value > 10)  return 1;
  • Les valeurs sont-elles répétées souvent?

Si oui, vous pouvez probablement tirer un bénéfice de Memoization (probablement pas, mais ça ne fait pas de mal de vérifier ....)

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

Si aucune de ces solutions ne peut être appliquée, alors, comme l'ont suggéré d'autres personnes, vous pouvez peut-être vous permettre d'abaisser la précision de votre sigmoïde ...

4
Stobor

Idée: Peut-être pouvez-vous créer une (grande) table de consultation avec les valeurs précalculées?

2
Vilx-

C’est un peu hors sujet, mais par curiosité, j’ai fait la même implémentation que celle de C , C # et F # en Java. Je vais laisser ça ici au cas où quelqu'un d'autre serait curieux.

Résultat:

$ 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

Je suppose que l'amélioration par rapport à C # dans mon cas est due au fait que Java est mieux optimisé que Mono pour OS X. Sur une implémentation MS .NET similaire (vs Java 6 si quelqu'un veut publier des chiffres comparatifs), je suppose que les résultats seraient différents. .

Code:

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());
    }
}
2
Henrik Gustafsson

Je me rends compte que cette question a surgi il y a un an, mais je l'ai rencontrée à cause de la discussion sur les performances de F # et de C par rapport à C #. J'ai joué avec certains exemples d'autres répondeurs et j'ai découvert que les délégués semblaient s'exécuter plus rapidement qu'une invocation de méthode classique, mais il n'y a aucun avantage apparent de performance pour F # par rapport à C # .

  • C: 166ms
  • C # (délégué): 275 ms 
  • C # (méthode): 431ms
  • C # (méthode, compteur de flottants): 2656ms
  • F #: 404ms

Le C # avec un compteur de float était un portage direct du code C. Il est beaucoup plus rapide d'utiliser un int dans la boucle for.

2
Brian Reiter

(Mis à jour avec des mesures de performance) (Mis à jour à nouveau avec des résultats réels :)

Je pense qu'une solution de table de correspondance vous mènerait très loin en termes de performances, avec un coût de mémoire et de précision négligeable.

L'extrait suivant est un exemple d'implémentation en C (je ne parle pas assez c # suffisamment pour coder à sec). Il fonctionne et fonctionne assez bien, mais je suis sûr qu'il y a un bug :)

#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;
}

Les résultats précédents étaient dus au travail effectué par l'optimiseur et à l'optimisation des calculs. Le faire exécuter le code donne des résultats légèrement différents et beaucoup plus intéressants (sur mon chemin, MB Air lent):

$ 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


FAIRE:

Il y a des choses à améliorer et des moyens d'éliminer les faiblesses; comment faire est laissé comme un exercice au lecteur :)

  • Réglez la plage de la fonction pour éviter le saut où la table commence et se termine.
  • Ajoutez une légère fonction de bruit pour masquer les artefacts de crénelage.
  • Comme l'a dit Rex, l'interpolation peut vous apporter un peu plus de précision tout en étant plutôt bon marché en termes de performances.
1
Henrik Gustafsson

Vous pouvez également envisager d’expérimenter d’autres fonctions d’activation moins coûteuses à évaluer. Par exemple:

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

(qui pourrait être considéré comme

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

pour une multiplication de moins). Cette fonction a une symétrie étrange et sa dérivée est triviale. Son utilisation pour un réseau de neurones nécessite la normalisation de la somme des entrées en divisant par le nombre total d'entrées (en limitant le domaine à [-1..1], qui est également compris dans la plage).

1
joel.neely

Il y a beaucoup de bonnes réponses ici. Je suggérerais de l'exécuter avec cette technique , juste pour être sûr

  • Vous ne l'appelez pas plus de fois que nécessaire.
    (Parfois, les fonctions sont appelées plus que nécessaire, simplement parce qu'elles sont si faciles à appeler.)
  • Vous ne l'appelez pas à plusieurs reprises avec les mêmes arguments
    (où vous pourriez utiliser la mémoisation)

BTW la fonction que vous avez est la fonction logit inverse,
ou l'inverse de la fonction log-odds-ratio log(f/(1-f)).

1
Mike Dunlavey

Il y a des fonctions beaucoup plus rapides qui font des choses très similaires:

x / (1 + abs(x)) - remplacement rapide de TAHN

Et pareillement:

x / (2 + 2 * abs(x)) + 0.5 - remplacement rapide de SIGMOID

Comparer les parcelles avec le sigmoïde réel

1
Lex

Une légère variation sur le thème de Soprano:

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

Puisque vous recherchez seulement un résultat en simple précision, pourquoi demander à la fonction Math.Exp de calculer un double? Tout calculateur d’exposants qui utilise une sommation itérative (voir l’extension dux ) prendra plus de temps pour plus de précision, à chaque fois. Et double est deux fois le travail de célibataire! De cette façon, vous convertissez d'abord en single, puis faites votre exponentielle.

Mais la fonction expf devrait être encore plus rapide. Cependant, je ne vois pas le besoin de la conversion de la soprano (float) en passant à expf, à moins que C # ne fasse pas de conversion implicite double float.

Sinon, utilisez simplement un réal langue, comme le Fortran ...

1
Phil H

1) Appelez-vous cela depuis un seul endroit? Si tel est le cas, vous pouvez gagner un peu en performances en déplaçant le code hors de cette fonction et en le plaçant là où vous auriez normalement appelé la fonction Sigmoid. Je n’aime pas cette idée en termes de lisibilité du code et d’organisation, mais lorsque vous avez besoin d’obtenir chaque gain de performance, cela peut aider car j’estime que les appels de fonction nécessitent un push/pop de registres sur la pile, ce qui pourrait être évité si le code était tout en ligne.

2) Je ne sais pas si cela pourrait aider mais essayez de transformer votre paramètre de fonction en paramètre de référence. Voir si c'est plus rapide. J'aurais suggéré de le rendre const (ce qui aurait été une optimisation s'il s'agissait de c ++), mais c # ne prend pas en charge les paramètres const.

0
Jeremy

Si vous avez besoin d'une accélération de vitesse géante, vous pourriez probablement envisager de paralléliser la fonction en utilisant la force (ge). IOW, utilisez DirectX pour contrôler la carte graphique et la faire pour vous. Je ne sais pas comment faire cela, mais j'ai déjà vu des gens utiliser des cartes graphiques pour effectuer toutes sortes de calculs.

0
erikkallen

En faisant une recherche sur Google, j'ai trouvé une implémentation alternative de la fonction Sigmoid.

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

Est-ce correct pour vos besoins? Est-ce plus rapide?

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

0
Haacked

J'ai vu que beaucoup de gens ici essayaient d'utiliser l'approximation pour rendre Sigmoid plus rapide. Cependant, il est important de savoir que Sigmoid peut également être exprimé à l'aide de tanh, et pas seulement exp. le comportement original de Sigmoid est conservé tel quel.

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

Bien sûr, la parellisation serait la prochaine étape pour améliorer les performances, mais en ce qui concerne le calcul brut, l’utilisation de Math.Tanh est plus rapide que Math.Exp.

0
Dash