it-swarm.com.de

Mathematische Optimierung in C #

Ich habe den ganzen Tag über eine Anwendung profiliert, und nachdem ich ein paar Bits Code optimiert habe, verbleibe ich auf meiner ToDo-Liste. Es ist die Aktivierungsfunktion für ein neuronales Netzwerk, das über 100 Millionen Mal aufgerufen wird. Sie beträgt laut dotTrace etwa 60% der Gesamtfunktionszeit.

Wie würdest du das optimieren?

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

Versuchen:

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

EDIT: Ich habe einen schnellen Benchmark gemacht. Auf meinem Rechner ist der obige Code etwa 43% schneller als Ihre Methode, und dieser mathematisch äquivalente Code ist das kleinste Bit (46% schneller als das Original):

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

EDIT 2: Ich bin mir nicht sicher, wie viel Overhead C # -Funktionen haben, aber wenn Sie #include <math.h> in Ihrem Quellcode verwenden, sollten Sie diese Funktion verwenden können, die eine Float-Exp-Funktion verwendet. Es könnte etwas schneller sein.

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

Auch wenn Sie Millionen Anrufe tätigen, kann der Funktionsaufruf-Overhead ein Problem sein. Versuchen Sie, eine Inline-Funktion zu erstellen, und prüfen Sie, ob dies eine Hilfe ist.

53
Sophie Alpert

Wenn es sich um eine Aktivierungsfunktion handelt, spielt es eine große Rolle, wenn die Berechnung von e ^ x völlig genau ist?

Wenn Sie beispielsweise die Annäherung (1 + x/256) ^ 256 verwenden, ist dies bei meinem Pentium-Test in Java (ich gehe davon aus, dass C # im Wesentlichen die gleichen Prozessoranweisungen kompiliert) etwa 7-8 mal schneller als e ^ x (Math.exp ()) und ist auf 2 Dezimalstellen genau (bis zu x von +/- 1,5) und innerhalb des von Ihnen angegebenen Bereichs in der richtigen Größenordnung. (Um die 256 zu erhöhen, müssen Sie die Zahl tatsächlich achtmal quadrieren - verwenden Sie dafür nicht Math.Pow!) In Java:

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

Verdoppeln oder halbieren Sie 256 (und addieren/entfernen Sie eine Multiplikation), je nachdem, wie genau die Approximation sein soll. Selbst bei n = 4 ergibt sich für Werte von x zwischen -0,5 und 0,5 immer noch eine Genauigkeit von 1,5 Dezimalstellen (und erscheint gut 15-mal schneller als Math.exp ()).

P.S. Ich habe vergessen zu erwähnen - Sie sollten offensichtlich nicht ja wirklich durch 256 dividieren: mit einer Konstante 1/256 multiplizieren. Der JIT-Compiler von Java führt diese Optimierung automatisch durch (zumindest Hotspot), und ich nahm an, dass C # dies auch tun muss.

30
Neil Coffey

Schau mal auf diesen Beitrag . es hat eine Annäherung für in Java geschriebene Texte, dies sollte der C # -Code dafür sein (ungeprüft):

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

In meinen Benchmarks ist dies mehr als 5-mal schneller als Math.exp () (in Java). Die Näherung basiert auf dem Papier " Eine schnelle, kompakte Näherung der Exponentialfunktion ", das genau für die Verwendung in neuronalen Netzen entwickelt wurde. Es ist im Grunde dasselbe wie eine Nachschlagetabelle von 2048 Einträgen und einer linearen Annäherung zwischen den Einträgen, aber dies alles mit IEEE-Fließkomma-Tricks.

EDIT: Laut Special Sauce ist ~ 3.25x schneller als die CLR-Implementierung. Vielen Dank!

21
martinus
  1. Denken Sie daran, dass Änderungen dieser Aktivierungsfunktion auf Kosten unterschiedlichen Verhaltens gehen . Dies beinhaltet sogar das Umschalten auf Float (und damit die Verringerung der Genauigkeit) oder die Verwendung von Aktivierungsersatzmitteln. Nur wenn Sie mit Ihrem Anwendungsfall experimentieren, wird der richtige Weg gezeigt.
  2. Zusätzlich zu den einfachen Code-Optimierungen würde ich auch empfehlen, Parallelisierung der Berechnungen (d. H., Um mehrere Kerne Ihrer Maschine oder sogar Maschinen in den Windows Azure Clouds zu nutzen) und die Trainingsalgorithmen zu verbessern.

UPDATE: Posten in Nachschlagetabellen für ANN-Aktivierungsfunktionen

UPDATE2: Ich habe den Punkt auf LUTs entfernt, da ich diese mit dem vollständigen Hashing verwechselt habe. Danke an Henrik Gustafsson , der mich wieder auf die Strecke gebracht hat. Der Speicher ist also kein Problem, obwohl der Suchraum immer noch mit den lokalen Extremen durcheinander geraten ist.

14
Rinat Abdullin

Bei 100 Millionen Anrufen frage ich mich, ob der Profiler-Overhead Ihre Ergebnisse nicht verzerrt. Ersetzen Sie die Berechnung durch ein No-Op und prüfen Sie, ob noch 60% der Ausführungszeit verbraucht werden ... 

Oder noch besser: Erstellen Sie einige Testdaten und verwenden Sie einen Stoppuhr-Timer, um etwa eine Million Anrufe anzuzeigen. 

8
Shog9

Wenn Sie in der Lage sind, mit C++ zusammenzuarbeiten, können Sie alle Werte in einem Array speichern und mit SSE wie folgt überlaufen:

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

Denken Sie jedoch daran, dass die von Ihnen verwendeten Arrays mit _aligned_malloc (some_size * sizeof (float), 16) zugewiesen werden sollten, da für SSE Speicher an einer Grenze ausgerichtet ist.

Mit SSE kann ich das Ergebnis für alle 100 Millionen Elemente in etwa einer halben Sekunde berechnen. Wenn Sie jedoch so viel Speicher auf einmal zuweisen, werden Sie fast zwei Drittel eines Gigabytes kosten. Ich würde also die Verarbeitung von mehr, aber kleineren Arrays auf einmal vorschlagen. Möglicherweise möchten Sie sogar die Verwendung eines doppelten Pufferungsansatzes mit 100 K-Elementen oder mehr in Betracht ziehen.

Wenn die Anzahl der Elemente beträchtlich ansteigt, möchten Sie diese Dinge möglicherweise auch auf der GPU verarbeiten (erstellen Sie einfach eine 1D-Float4-Textur und führen Sie einen sehr trivialen Fragment-Shader aus).

8
Jasper Bekkers

FWIW, hier sind meine C # Benchmarks für die bereits veröffentlichten Antworten. (Leer ist eine Funktion, die nur 0 zurückgibt, um den Funktionsaufruf-Overhead zu messen.) 

 Leere Funktion: 79ms 0 
 Original: 1576ms 0.7202294 
 Vereinfacht: (Sopran) 681ms 0.7202294 
 Ungefähr: (Neil) 441ms 0.7198783 
 Bit-Manip : (martinus) 836ms 0,72318 
 Taylor: (Rex Logan) 261ms 0.7202305 
 Nachschlag: (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 # hat in .NET-Mathematikalgorithmen eine bessere Leistung als C #. Das Umschreiben eines neuronalen Netzwerks in F # kann die Gesamtleistung verbessern.

Wenn wir LUT-Benchmarking-Snippet (ich habe eine etwas optimierte Version verwendet) in F # erneut implementieren, dann lautet der resultierende Code: 

  • führt den sigmoid1-Benchmark in 588,8 ms statt 3899,2 ms aus
  • führt sigmoid2 (LUT) Benchmark in 156.6ms statt 411.4 ms aus

Weitere Details finden Sie im Blogbeitrag . Hier ist der F # -Schnipsel-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);

Und die Ausgabe (Kompilierung gegen F # 1.9.6.2 CTP ohne Debugger freigeben):

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

UPDATE: aktualisiertes Benchmarking zur Verwendung von 10 ^ 7 Iterationen, um Ergebnisse mit C vergleichbar zu machen

UPDATE2: Hier sind die Leistungsergebnisse der C-Implementierung von derselben Maschine zum Vergleich:

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

Ganz oben auf dem Kopf, In diesem Artikel wird erläutert, wie Sie das Exponential durch Missbrauch von Gleitkommazahlen (approximieren) (= Link oben rechts für PDF) anschlagen, aber ich weiß nicht, ob dies der Fall sein wird viel Nutzen für Sie in .NET.

Ein weiterer Punkt: Um große Netzwerke schnell zu trainieren, ist das logistische Sigmoid, das Sie verwenden, ziemlich schrecklich. Siehe Abschnitt 4.4 von Efficient Backprop von LeCun et al. und verwende etwas mit Nullpunkt (tatsächlich, lesen Sie das ganze Papier, es ist äußerst nützlich).

5
dwf

Hinweis: Dies ist ein Follow-up zu this post.

Bearbeiten: Aktualisieren, um dasselbe zu berechnen wie this und this , wobei einige Anregungen von this verwendet werden.

Nun schau, was du mich dazu gebracht hast! Du hast mich dazu gebracht, Mono zu installieren!

$ 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 ist die Mühe kaum mehr wert, die Welt bewegt sich vorwärts :)

Also nur über einen Faktor 10 6 schneller. Jemand mit einer Windows-Box kann mit MS-Material die Speicherauslastung und -leistung untersuchen :)

Die Verwendung von LUTs für Aktivierungsfunktionen ist nicht so ungewöhnlich, insbesondere wenn sie in Hardware implementiert sind. Es gibt viele bewährte Varianten des Konzepts, wenn Sie diese Tabellenarten verwenden möchten. Wie bereits erwähnt, kann sich Aliasing jedoch als Problem herausstellen, aber es gibt auch Möglichkeiten, dies zu umgehen. Einige weitere Lektüre:

Einige gotchas mit diesem:

  • Der Fehler wird erhöht, wenn Sie außerhalb des Tisches greifen (konvergiert jedoch an den Extremen auf 0). für x ungefähr + -7,0. Dies ist auf den gewählten Skalierungsfaktor zurückzuführen. Größere SCALE-Werte führen zu höheren Fehlern im mittleren Bereich, jedoch an den Kanten zu kleineren Fehlern.
  • Dies ist im Allgemeinen ein sehr dummer Test, und ich kenne C # nicht. Es ist nur eine einfache Konvertierung meines C-Codes :)
  • Rinat Abdullin ist sehr richtig, dass Aliasing und Präzisionsverlust Probleme verursachen könnten, aber da ich die Variablen dafür nicht gesehen habe, kann ich Ihnen nur raten, dies zu versuchen. Tatsächlich stimme ich mit allem überein, was er sagt, außer der Frage der Nachschlagetabellen.

Verzeihen Sie die Copy-Paste-Codierung ...

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

Sopran hatte einige nette Optimierungen für Ihren Anruf:

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

Wenn Sie eine Nachschlagetabelle ausprobieren und feststellen, dass sie zu viel Speicher benötigt, können Sie immer den Wert Ihres Parameters für jeden nachfolgenden Aufruf überprüfen und eine Zwischenspeicherungstechnik verwenden. 

Versuchen Sie beispielsweise, den letzten Wert und das Ergebnis zwischenzuspeichern. Wenn der nächste Anruf denselben Wert wie der vorherige Anruf hat, müssen Sie ihn nicht berechnen, da Sie das letzte Ergebnis zwischengespeichert haben. Wenn der aktuelle Anruf mit dem vorherigen Anruf identisch war (1 von 100), könnten Sie sich möglicherweise 1 Million Berechnungen sparen.

Oder Sie stellen möglicherweise fest, dass der Werteparameter innerhalb von 10 aufeinanderfolgenden Aufrufen im Durchschnitt 2 Mal gleich ist. Sie können also versuchen, die letzten 10 Werte/Antworten im Cache zu speichern.

4
Jeremy

Erster Gedanke: Wie wäre es mit einigen Statistiken über die Wertevariable?

  • Sind die Werte von "value" typischerweise klein -10 <= value <= 10? 

Wenn nicht, können Sie möglicherweise einen Schub erhalten, indem Sie auf Werte außerhalb der Grenzen testen 

if(value < -10)  return 0;
if(value > 10)  return 1;
  • Werden die Werte oft wiederholt?

Wenn ja, können Sie wahrscheinlich von Memoization profitieren (wahrscheinlich nicht, aber es schadet nicht, es zu überprüfen ...)

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

Wenn keine dieser Methoden angewendet werden kann, können Sie, wie einige andere vorgeschlagen haben, die Genauigkeit Ihres Sigmoid herabsetzen ...

4
Stobor

Idee: Vielleicht können Sie eine (große) Nachschlagetabelle mit den vorberechneten Werten erstellen?

2
Vilx-

Dies ist ein wenig abwegiges Thema, aber aus Neugierde habe ich dieselbe Implementierung vorgenommen wie die in C , C # und F # in Java. Ich lass das einfach hier, falls jemand anderes neugierig ist.

Ergebnis:

$ 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

Ich nehme an, die Verbesserung gegenüber C # liegt in meinem Fall daran, dass Java besser als Mono für OS X optimiert ist. Bei einer ähnlichen MS .NET-Implementierung (im Gegensatz zu Java 6, wenn jemand Vergleichszahlen buchen möchte), sind die Ergebnisse wahrscheinlich unterschiedlich .

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

Mir ist klar, dass diese Frage seit einem Jahr nicht mehr auftaucht, aber ich bin darauf gestoßen, weil ich über F # und C bezüglich C # gesprochen habe. Ich spielte mit einigen Samples von anderen Respondern und stellte fest, dass die Delegierten anscheinend schneller als ein normaler Methodenaufruf ausgeführt werden, aber es gibt keinen offensichtlichen Leistungsvorteil für F # gegenüber C # .

  • C: 166 ms
  • C # (Delegat): 275 ms 
  • C # (Methode): 431 ms
  • C # (Methode, Float Counter): 2.656 ms
  • F #: 404 ms

Das C # mit einem Float-Counter war ein direkter Port des C-Codes. Es ist viel schneller, ein int in der for-Schleife zu verwenden.

2
Brian Reiter

(Aktualisiert mit Leistungsmessungen) (Erneut aktualisiert mit realen Ergebnissen :)

Ich denke, eine Lookup-Table-Lösung würde Sie sehr weit bringen, wenn es um Leistung geht, zu einem vernachlässigbaren Speicher- und Präzisionsaufwand.

Der folgende Ausschnitt ist eine Beispielimplementierung in C (ich spreche nicht fließend genug, um ihn trocken zu codieren). Es läuft und läuft gut genug, aber ich bin sicher, dass darin ein Fehler ist :)

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

Bisherige Ergebnisse waren darauf zurückzuführen, dass der Optimierer seine Arbeit erledigte und die Berechnungen wegfiel. Wenn Sie den Code tatsächlich ausführen, erhalten Sie etwas andere und viel interessantere Ergebnisse (auf meinem Weg langsam 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


MACHEN:

Es gibt Dinge zu verbessern und Möglichkeiten, Schwächen zu beseitigen. wie zu tun ist, ist dem Leser als Übung überlassen :)

  • Stimmen Sie den Bereich der Funktion ab, um den Sprung zu vermeiden, an dem die Tabelle beginnt und endet.
  • Fügen Sie eine leichte Rauschfunktion hinzu, um die Aliasing-Artefakte auszublenden.
  • Wie Rex gesagt hat, könnte die Interpolation Sie zwar ein wenig präziser machen, ist aber in Bezug auf die Leistung recht günstig.
1

Sie können auch mit alternativen Aktivierungsfunktionen experimentieren, die günstiger zu bewerten sind. Zum Beispiel:

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

(das könnte man als faktorisieren

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

für eine Vervielfachung weniger). Diese Funktion hat eine ungerade Symmetrie und ihre Ableitung ist trivial. Die Verwendung für ein neuronales Netzwerk erfordert die Normalisierung der Summe der Eingaben durch Division durch die Gesamtzahl der Eingaben (Begrenzung der Domäne auf [-1..1], was auch Bereich ist).

1
joel.neely

Hier gibt es viele gute Antworten. Ich würde vorschlagen, es durch diese Technik auszuführen, nur um sicher zu gehen

  • Du rufst es nicht mehr als nötig an.
    (Manchmal werden Funktionen mehr als nötig aufgerufen, nur weil sie so einfach aufgerufen werden können.)
  • Sie rufen es nicht wiederholt mit denselben Argumenten an
    (wo Sie Memo verwenden können)

BTW die Funktion, die Sie haben, ist die inverse Logit-Funktion,
oder die Umkehrung der Log-Odds-Ratio-Funktion log(f/(1-f)).

1
Mike Dunlavey

Es gibt viel schnellere Funktionen, die sehr ähnliche Dinge tun:

x / (1 + abs(x)) - schneller Ersatz für TAHN

Und ähnlich:

x / (2 + 2 * abs(x)) + 0.5 - schneller Ersatz für SIGMOID

Vergleiche Diagramme mit tatsächlichem Sigmoid

1
Lex

Eine milde Variante des Sopran-Themas:

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

Da Sie nur nach einem einzigen Präzisionsergebnis suchen, warum sollte die Math.Exp-Funktion eine doppelte Zahl berechnen? Jeder Exponentenrechner, der eine iterative Summation verwendet (siehe die Erweiterung des ex ) dauert immer länger für mehr Genauigkeit. Und doppelt ist doppelt so viel wie Single! Auf diese Weise konvertieren Sie zuerst in einzelne, dann mach dein exponentielles.

Die Expf-Funktion sollte aber noch schneller sein. Ich sehe jedoch keine Notwendigkeit für einen Sopran (Float), der an Expf weitergegeben wird, es sei denn, C # führt keine implizite Float-Double-Konvertierung durch.

Ansonsten verwenden Sie einfach eine echt Sprache, wie FORTRAN ...

1
Phil H

1) Nennen Sie dies nur von einer Stelle aus? Wenn dies der Fall ist, können Sie eine geringfügige Leistung erzielen, indem Sie den Code aus dieser Funktion heraus verschieben und einfach an der Stelle platzieren, an der Sie normalerweise die Sigmoid-Funktion aufgerufen hätten. Ich mag diese Idee in Bezug auf die Lesbarkeit und Organisation von Code nicht, aber wenn Sie den letzten Leistungsgewinn erzielen müssen, kann dies hilfreich sein, da Funktionsaufrufe meiner Meinung nach ein Push/Pop von Registern auf dem Stack erfordern, was bei der Verwendung von Code war alles Inline.

2) Ich habe keine Ahnung, ob dies helfen könnte, aber versuchen Sie, Ihren Funktionsparameter zu einem Ref-Parameter zu machen. Sehen Sie, ob es schneller geht. Ich hätte vorgeschlagen, es const zu machen (was in c ++ eine Optimierung gewesen wäre), aber c # unterstützt keine const-Parameter.

0
Jeremy

Wenn Sie einen riesigen Geschwindigkeitsschub benötigen, können Sie die Funktion wahrscheinlich mit Hilfe der (ge) force parallelisieren. IOW, verwenden Sie DirectX, um die Grafikkarte für Sie zu steuern. Ich habe keine Ahnung, wie das geht, aber ich habe gesehen, dass Leute Grafikkarten für alle Arten von Berechnungen verwenden.

0
erikkallen

Bei einer Google-Suche habe ich eine alternative Implementierung der Sigmoid-Funktion gefunden.

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

Ist das für Ihre Bedürfnisse richtig? Ist es schneller

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

0
Haacked

Ich habe gesehen, dass viele Leute hier versuchen, die Näherung zu verwenden, um Sigmoid schneller zu machen. Es ist jedoch wichtig zu wissen, dass Sigmoid auch mit tanh ausgedrückt werden kann, nicht nur exp. Die Berechnung von Sigmoid ist auf diese Weise etwa fünfmal schneller als mit Exponentialfunktion, und mit dieser Methode können Sie nichts annähern Das ursprüngliche Verhalten von Sigmoid wird so beibehalten, wie es ist.

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

Parellization wäre natürlich der nächste Schritt zur Leistungsverbesserung, aber für die reine Berechnung ist Math.Tanh schneller als Math.Exp.

0
Dash