Question

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

Comment optimiseriez-vous cela ?

public static float Sigmoid(double value) {
    return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
Était-ce utile?

La solution

Essayez:

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

MODIFIER: j'ai effectué une analyse 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);
}

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

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

De même, 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.

Autres conseils

Si c’est pour une fonction d’activation, cela at-il une importance capitale si le calcul de e ^ x est parfaitement précis?

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), il 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 diviser par deux 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 devez évidemment pas vraiment diviser par 256: multiplier par une constante sur 1/256. Le compilateur JIT de Java effectue cette optimisation automatiquement (du moins Hotspot), et je supposais que C # devait le faire aussi.

Consultez ce message . 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 de performance, cela est plus de 5 fois plus rapide que Math.exp () (en Java). L’approximation est basée sur le document & Quot; Une approximation rapide et compacte de la fonction exponentielle " qui a été développé exactement 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.

MODIFIER: Selon sauce spéciale , elle est environ 3,25 fois plus rapide que le Mise en œuvre du CLR. Merci!

  1. Rappelez-vous, que tout changement dans cette fonction d'activation se fait au prix d'un comportement différent.Cela inclut même le passage au flotteur (et donc une diminution de la précision) ou l'utilisation de substituts d'activation.Seule l’expérimentation de votre cas d’utilisation montrera la bonne voie.
  2. En plus des simples optimisations de code, je recommanderais également de considérer parallélisation des calculs (c'est à dire.:pour exploiter plusieurs cœurs de votre machine ou même des machines dans les cloud Windows Azure) et améliorer les algorithmes de formation.

MISE À JOUR: Publier sur les tables de recherche pour les fonctions d'activation ANN

MISE À JOUR2 : J'ai supprimé le point sur les LUT car je les ai confondus avec le hachage complet.Merci allez à Henrik Gustafsson pour m'avoir remis sur la bonne voie.La mémoire n'est donc pas un problème, même si l'espace de recherche est encore un peu gâché par les extrema locaux.

À 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 si il est toujours signalé qu'il utilise 60% du temps d'exécution ...

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

Si vous êtes en mesure d'interopérer avec C ++, vous pouvez envisager de stocker toutes les valeurs d'un tableau et de les survoler à l'aide de SSE, comme suit:

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 allez utiliser doivent être alloués avec _aligned_malloc (some_size * sizeof (float), 16) car SSE nécessite une mémoire alignée sur une limite.

En utilisant SSE, je peux calculer le résultat pour la totalité des 100 millions d'éléments en environ une demi-seconde. 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 une approche à 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).

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)

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

De mémoire, cet article explique de manière approximative l'exponentielle en abusant de virgule flottante (cliquez sur le lien en haut à droite pour PDF), mais je ne sais pas si cela vous sera d'une grande utilité dans .NET.

En outre, un autre point: 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 Backprop efficace de LeCun et al et utiliser quelque chose centré sur le zéro (en fait, lisez tout ce papier, il est extrêmement utile).

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

Si nous réimplémentions l'extrait d'analyse comparative LUT (j'utilise une version légèrement modifiée) F #, puis le code résultant:

  • exécute la référence sigmoid1 en 588,8ms au lieu de 3899,2ms
  • exécute le point de repère sigmoid2 (LUT) en 156,6 ms au lieu de 411,4 ms

Plus de détails peuvent être trouvés dans le article de blog . Voici l'extrait FIC de 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: l'analyse comparative mise à jour afin 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 à partir 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

Remarque: Ceci est un suivi de la publication .

Modifier: Mettez à jour pour calculer la même chose que this et < a href = "https://stackoverflow.com/questions/412019/code-optimizations#416180"> this , en s'inspirant de this .

Maintenant, regarde ce que tu m'as fait faire! Vous m'avez 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, un peu plus d'un facteur 10 6 plus rapidement. 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 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 ceci:

  • 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 moyenne, 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 code C:)
  • Rinat Abdullin est tout à fait exact que l'aliasing et la perte de précision peuvent causer des problèmes, mais depuis pas vu les variables pour cela, je ne peux que vous conseiller d'essayer cela. 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());
    }
}

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

  • Les valeurs de " valeur " typiquement petit -10 < = valeur < = 10?

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

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

Si tel est le cas, vous pouvez probablement tirer parti de la Mémoization (probablement pas, mais ne fait pas mal pour vérifier ....)

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

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

Soprano a eu quelques belles optimisations 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 constatez que celle-ci utilise trop de mémoire, vous pouvez toujours consulter la valeur de votre paramètre pour chaque appel suivant en utilisant une technique de mise en cache.

Par exemple, essayez de mettre en cache la dernière valeur et le 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.

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

Ceci 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 cela 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 serait différent.

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

Je me rends compte que cette question a été posée 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 normale, 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): 2 656ms
  • 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.

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 de normaliser 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).

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

Etant donné que vous ne recherchez qu'un résultat à simple précision, pourquoi demander à la fonction Math.Exp de calculer un double? Tout calculateur d’exposants qui utilise une somme itérative (voir l’expansion de la fonction e x ) prendra plus de temps pour plus de précision, à chaque fois. Et le double est le double du 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 (float) de soprano en passant à expf, à moins que C # ne fasse pas de conversion implicite double-float.

Sinon, utilisez simplement un langage réel , tel que le Fortran ...

Il y a beaucoup de bonnes réponses ici. Je suggérerais de l'exécuter via cette technique , juste pour vous assurer que

  • 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émorisation)

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

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

Je pense qu'une solution de table de consultation 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 contient 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. En le faisant exécuter, le code produit des résultats légèrement différents et beaucoup plus intéressants (sur mon chemin lent, 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

profil

TODO:

Il y a des choses à améliorer et des moyens de supprimer 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 le dit Rex, l’interpolation pourrait vous apporter un peu plus de précision tout en étant plutôt bon marché en termes de performances.

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

x / (1 + abs(x)) & # 8211; remplacement rapide pour TAHN

Et pareillement:

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

Comparer parcelles avec sigmoïde réelle

En effectuant une recherche Google, j'ai trouvé une autre implémentation de la fonction Sigmoid.

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

Cela correspond-il à vos besoins? Est-ce plus rapide?

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

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 devez obtenir le dernier gain de performances, 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 votre 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.

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.

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, pas seulement exp. Le calcul de Sigmoid de cette façon est environ 5 fois plus rapide qu'avec exponentiel. Cette méthode vous permet de ne pas vous approximer. Par conséquent, 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 l’étape suivante de l’amélioration des performances, mais en ce qui concerne le calcul brut, l’utilisation de Math.Tanh est plus rapide que Math.Exp.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top