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)));
}
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.
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.
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!
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.
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.
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).
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));
}
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:
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
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).
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:
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());
}
}
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.
Erster Gedanke: Wie wäre es mit einigen Statistiken über die Wertevariable?
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;
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 ...
Idee: Vielleicht können Sie eine (große) Nachschlagetabelle mit den vorberechneten Werten erstellen?
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());
}
}
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 # .
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.
(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
MACHEN:
Es gibt Dinge zu verbessern und Möglichkeiten, Schwächen zu beseitigen. wie zu tun ist, ist dem Leser als Übung überlassen :)
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).
Hier gibt es viele gute Antworten. Ich würde vorschlagen, es durch diese Technik auszuführen, nur um sicher zu gehen
BTW die Funktion, die Sie haben, ist die inverse Logit-Funktion,
oder die Umkehrung der Log-Odds-Ratio-Funktion log(f/(1-f))
.
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
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) 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.
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.
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
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.