Optimisation des mathématiques en C#

j'ai profilé une application toute la journée et, après avoir optimisé quelques bits de code, je me retrouve avec ça sur ma liste de choses à faire. C'est la fonction d'activation d'un réseau neuronal, qui est appelé plus de 100 millions de fois. Selon dotTrace, il s'élève à environ 60% du temps de fonction global.

comment optimiser ceci?

public static float Sigmoid(double value) {
    return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
53
demandé sur Salvador Dali 2009-01-05 03:57:53

24 réponses

, Essayez:

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

EDIT: j'ai fait un rapide test. Sur ma machine, le code ci-dessus est environ 43% plus rapide que votre méthode, et ce code mathématiquement équivalent est le plus petit peu plus rapide (46% plus rapide que l'original):

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

EDIT 2: utilise une fonction float-exp. Il pourrait être un peu plus rapide.

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

aussi, si vous faites des millions d'appels, la fonction d'appel peut être un problème. Essayez de faire une fonction en ligne et voir si cela aide.

53
répondu Sophie Alpert 2009-01-05 01:29:15

si c'est pour une fonction d'activation, est-ce si important que le calcul de E^x soit complètement précis?

par exemple, si vous utilisez l'approximation (1+x/256)^256, sur mon test Pentium en Java (je suppose que C# compile essentiellement les mêmes instructions de processeur) c'est environ 7-8 fois plus rapide que e^x (Math.exp ()), et est précis à 2 décimales jusqu'à environ x de + / -1.5, et dans l'ordre de grandeur correct à travers la gamme vous énoncer. (Évidemment, pour augmenter le nombre à 256, Vous avez en fait carré le nombre 8 fois -- n'utilisez pas les mathématiques.Pow!) En Java:

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

doublez ou divisez par deux 256 (et ajoutez/supprimez une multiplication) en fonction de la précision que vous voulez que l'approximation soit. Même avec n=4, il donne encore environ 1.5 décimales de précision pour des valeurs de x entre -0.5 et 0.5 (et apparaît un bon 15 fois plus rapide que les mathématiques.exp()).

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

29
répondu Neil Coffey 2009-01-07 21:16:26

regarder ce post . il a une approximation pour E^x écrit en Java, ceci devrait être le code C Pour it (non testé):

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

dans Mes benchmarks, c'est plus que 5 fois plus rapide que les maths.exp () (en Java). L'approximation est basée sur le papier " une Approximation rapide et compacte de la fonction exponentielle " qui a été développé exactement pour être utilisé dans les réseaux neuronaux. Il est fondamentalement la même chose qu'une table de recherche de 2048 entrées et l'approximation linéaire entre les entrées, mais tout cela avec IEEE floating point trucs.

EDIT: Selon Sauce Spéciale c'est ~3.25 x plus rapide que le CLR mise en œuvre. Merci!

21
répondu martinus 2017-06-07 05:49:46
  1. rappelez-vous que toute modification de cette fonction d'activation vient au prix d'un comportement différent . Cela inclut même le passage au flotteur (et donc l'abaissement 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 optimisations de code simples, je recommanderais également de considérer parallélisation des calculs (i.e.: tirer parti de plusieurs noyaux de votre machine ou même des machines aux nuages D'Azur de Windows) et l'amélioration des algorithmes de formation.

mise à JOUR: Post sur les tables de recherche pour l'ANN fonctions d'activation

UPDATE2: j'ai supprimé le point sur les LUTs car je les ai confondus avec le hachage complet. Merci à Henrik Gustafsson pour m'avoir mis de retour sur la piste. Si la mémoire n'est pas un problème, bien que l'espace de recherche est encore foiré avec les extrema locaux un peu.

14
répondu Rinat Abdullin 2017-05-23 11:55:10

à 100 millions d'appels, je commencerais à me demander si profiler overhead ne modifie pas vos résultats. Remplacer le calcul par un no-op et voir s'il est encore déclaré consommer 60% du temps d'exécution...

ou mieux encore, créer des données de test et utiliser un chronomètre pour profiler un million d'appels.

8
répondu Shog9 2009-01-05 01:05:56

si vous êtes capable d'interop avec C++, vous pouvez envisager de stocker toutes les valeurs dans un tableau et de les passer 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 en utilisant _aligned_malloc(some_size * sizeof(float), 16) parce que 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 environ une demi-seconde. Toutefois, l'allocation de cette quantité de mémoire à la fois vous coûtera près de deux tiers d'un gigaoctet donc je suggère de traiter plus mais des tableaux plus petits à la fois. Vous pourriez même envisager d'utiliser une double approche de buffering avec des éléments de 100K ou plus.

en outre, si le nombre d'éléments commence à augmenter considérablement, vous pourriez choisir de traiter ces choses sur le GPU (il suffit de créer une texture float4 1D et exécuter un shader de fragment très trivial).

8
répondu Jasper Bekkers 2009-01-05 12:34:40

FWIW, voici mon c # benchmarks pour les réponses déjà publiées. (Vide est une fonction qui renvoie la valeur 0, pour mesurer l'appel de la fonction de surcharge)

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));
}
7
répondu Jimmy 2009-01-06 19:49:26

sur le dessus de ma tête, ce papier explique une façon d'approcher l'exponentielle en abusant de la pointe flottante , (cliquez sur le lien En haut à droite pour PDF) mais je ne sais pas si elle sera d'une grande utilité pour vous dans .NET.

aussi, un autre point: dans le but de former rapidement de grands réseaux, le sigmoïde logistique que vous utilisez est assez terrible. Voir rubrique 4.4 de efficacité de Backprop par LeCun et al et utilisation quelque chose de zéro-centré (en fait, lire ce papier entier, il est immensément utile).

5
répondu dwf 2009-01-05 11:30:09

F# A de meilleures performances que C# dans les algorithmes mathématiques. donc réécrire le réseau neuronal en F# pourrait améliorer la performance globale.

Si nous re-mettre en œuvre LUT benchmarking "extrait de 151990920" (j'ai été en utilisant un peu tordu version) en F#, le code résultant:

  • exécute sigmoid1 benchmark dans 588.8 ms au lieu de 3899.2 ms
  • exécute le benchmark sigmoid2 (LUT) dans 156.6 ms au lieu de 411.4 ms

plus de détails peuvent être trouvés dans le post de blog . Voici le F # snippet 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 (Version compilation contre F# 1.9.6.2 CTP sans debugger):

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

mise à JOUR: mise à jour de l'analyse comparative de l'utilisation de 10^7 itérations à faire résultats comparables à c

UPDATE2: voici les résultats de performance de la c mise en œuvre de la même machine à comparer avec:

Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms
5
répondu Rinat Abdullin 2017-05-23 12:34:51

Note: C'est une suite à ce après.

Edit: mise à Jour pour calculer la même chose que ce et ce , en prenant un peu d'inspiration de ce .

regardez ce que vous m'avez 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 est à peine la peine de l'effort plus, le monde va de l'avant :)

donc, un peu plus d'un facteur 10 6 plus rapide. Quelqu'un avec une boîte de windows arrive à enquêter sur l'utilisation de la mémoire et de la performance à l'aide de MS-stuff :)

L'utilisation de luth pour des fonctions d'activation n'est pas si rare, surtout lorsqu'elle est mise en œuvre dans le matériel. Il existe de nombreuses variantes bien éprouvées du concept là-bas si vous êtes prêt à inclure ces types de tableaux. Cependant, comme l'ont déjà souligné, l'aliasing est peut-être un problème, mais il ya des façons de contourner cela. Quelques lectures complémentaires:

Certains pièges avec ceci:

  • l'erreur augmente lorsque vous sortez de la table (mais converge à 0 aux extrêmes); pour x env +-7.0. Cela est dû au facteur d'échelle. De plus grandes valeurs D'échelle donnent des erreurs plus élevées dans la gamme moyenne, mais plus petites aux bords.
  • c'est généralement un test très stupide, et je ne sais pas C#, c'est juste une simple conversion de mon C-code :)
  • Rinat Abdullin est tout à fait correct que la perte d'Alias et de précision pourrait causer des problèmes, mais comme je n'ai pas vu les variables pour cela, je ne peux que vous conseiller pour essayer cela. En fait, je suis d'accord avec tout ce qu'il dit à l'exception de la question des tables de recherche.

Pardonnez le copier-coller de codage...

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
répondu Henrik Gustafsson 2017-05-23 11:55:10

première réflexion: Que Diriez-vous de quelques statistiques sur la variable des valeurs?

  • est-ce que les valeurs de" valeur " sont typiquement petites -10 <= valeur <= 10?

si non, 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 souvent répétées?

si oui, vous pouvez probablement obtenir un avantage de Memoization (probablement pas, mais ça ne fait pas de mal de vérifier....)

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

si ni l'un ni l'autre ne peut être appliqué, alors comme d'autres l'ont suggéré, peut-être que vous pouvez vous en tirer en abaissant la précision de votre sigmoïde...

4
répondu Stobor 2009-01-05 02:05:03

Soprano a eu de 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 recherche et qu'elle utilise trop de mémoire, vous pouvez toujours regarder la valeur de votre paramètre pour chaque appel successif et employer une technique de mise en cache.

par exemple, essayez la mise en cache de la dernière valeur et du résultat. Si le prochain appel a la même valeur que le précédent, vous n'avez pas besoin de le calculer car vous avez mis en cache le dernier résultat. Si l' appel actuel était le même que l'appel précédent même 1 sur un 100 fois, vous pourriez potentiellement économiser vous-même 1 million de calculs.

ou, vous pouvez trouver que dans 10 appels successifs, le paramètre valeur est en moyenne la même 2 fois, donc vous pouvez essayer de cacher les 10 dernières valeurs/réponses.

4
répondu Jeremy 2009-01-05 03:35:54

idée: peut-être Pouvez-vous faire une (grande) table de recherche avec les valeurs pré-calculées?

2
répondu Vilx- 2009-01-05 01:00:07

c'est légèrement hors sujet, mais juste 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 similaire DE MS .NET (vs. Java 6 si quelqu'un veut publier les 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
répondu Henrik Gustafsson 2017-05-23 10:30:10

je me rends compte qu'il y a un an que cette question a surgi, mais j'ai couru à travers elle en raison de la discussion de f# et c performance relative à C#. J'ai joué avec certains des échantillons provenant d'autres répondants et j'ai découvert que les délégués semblent exécuter plus rapidement qu'une invocation de méthode régulière mais il n'y a pas d'avantage de rendement apparent à F# sur C# .

  • C: 166ms
  • c# (délégué): 275ms
  • C# (méthode): 431ms
  • C# (méthode, float compteur): 2,656 ms
  • F#: 404ms

le C# avec un compteur de flotteurs était un port droit du code C. Il est beaucoup plus rapide d'utiliser un int dans la boucle for.

2
répondu Brian Reiter 2010-12-31 00:15:51

vous pourriez également envisager d'expérimenter d'autres fonctions d'activation qui sont moins chères à évaluer. Par exemple:

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

( qui pourrait être pris en compte 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. L'utiliser pour un réseau neuronal nécessite de normaliser la somme des entrées en divisant par le nombre total d'entrées (limitant le domaine à [-1..1], qui est aussi gamme.)

1
répondu joel.neely 2009-01-05 04:01:29

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 êtes seulement après un résultat de précision simple, Pourquoi faire le calcul.Fonction Exp calculer un double? Toute calculatrice exposant qui utilise une sommation itérative (voir l'expansion de l'e x ) prendra plus de temps pour plus de précision, à chaque fois. Et double est le double du travail de single! De cette façon, vous convertissez en single d'abord, puis faites votre exponentielle.

mais la fonction expf devrait être encore plus rapide. Je ne vois pas la nécessité de la distribution de soprano (float) en passant à expf cependant, à moins que C# ne fasse pas la conversion implicite float-double.

sinon, il suffit d'utiliser un vrai langue, comme FORTRAN...

1
répondu Phil H 2009-01-05 12:01:11

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

  • vous ne l'appelez pas plus de fois que vous n'en avez besoin.

    (Parfois les fonctions appelées plus que nécessaire, simplement parce qu'ils sont si faciles à appeler.)
  • vous ne l'appelez pas à plusieurs reprises avec les mêmes arguments

    (où vous pourriez utiliser memoization)

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
répondu Mike Dunlavey 2017-05-23 12:02:48

(mise à jour avec des mesures de rendement) (mise à jour avec des résultats réels:)

je pense qu'une solution de table de recherche vous mènerait très loin quand il s'agit de performance, à 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 c# assez couramment pour le Dry-code it). Il exécute 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;
}

précédent les résultats étaient dus au fait que l'optimiseur faisait son travail et optimisait les calculs. En le faisant exécuter le code, on obtient des résultats légèrement différents et beaucoup plus intéressants (sur mon chemin slow 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:

Il y a des choses à améliorer et les moyens d'éliminer les faiblesses; la façon de le faire est est laissé comme exercice au lecteur :)

  • accorder la portée de la fonction pour éviter le saut où la table commence et se termine.
  • ajouter une fonction de Bruit Léger pour cacher les artéfacts d'alias.
  • comme Rex l'a dit, l'interpolation pourrait vous apporter un peu plus de précision tout en étant assez bon marché performance-sage.
1
répondu Henrik Gustafsson 2015-01-16 12:28:53

il y a des fonctions beaucoup plus rapides qui font des choses très semblables:

x / (1 + abs(x)) – remplacement rapide pour TAHN

et de même:

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

comparez les tracés avec le sigmoïde réel

1
répondu Lex 2016-10-29 17:32:05

faisant une recherche 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-il plus rapide?

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

0
répondu Haacked 2009-01-05 01:18:10

1) l'appelez-vous d'un seul endroit? Si c'est le cas, vous pouvez gagner une petite quantité de performance en déplaçant le code hors de cette fonction et en le mettant juste 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 quand vous avez besoin d'obtenir chaque dernier gain de performance, cela pourrait aider parce que je pense que les appels de fonction nécessitent un push / pop de registres sur la pile, ce qui pourrait être évité si votre code était tout inline.

2) Je n'ai aucune idée si cela pourrait aider, mais essayez de faire de votre paramètre de fonction un paramètre ref. Voir si c'est plus rapide. J'aurais suggéré d'utiliser const (ce qui aurait été une optimisation si c'était en c++) mais c# ne supporte pas les paramètres const.

0
répondu Jeremy 2009-01-05 03:56:10

si vous avez besoin d'un boost de vitesse géant, vous pouvez probablement regarder dans la parallélisation de la fonction en utilisant la force (ge). Iow, utilisez DirectX pour contrôler la carte graphique pour le faire pour vous. Je ne sais pas comment faire, mais j'ai vu des gens utiliser des cartes graphiques pour toutes sortes de calculs.

0
répondu erikkallen 2009-01-05 17:06:52

j'ai vu que beaucoup de gens ici essayent d'utiliser l'approximation pour rendre Sigmoid plus rapide. Cependant, il est important de savoir que Sigmoid peut également être exprimé en utilisant tanh, pas seulement exp. Calculer Sigmoid de cette façon est environ 5 fois plus rapide qu'avec exponential, et en utilisant cette méthode vous ne vous rapprochez de rien, donc le comportement original de Sigmoid est maintenu 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élioration de la performance, mais en ce qui concerne le calcul brut, en utilisant les mathématiques.Tanh est plus rapide que les maths.Exp.

0
répondu Dash 2017-07-09 16:26:28