Neuronale Netze in der Praxis: Übung macht den Meister
Einführung
Hallo und herzlich willkommen zu einem weiteren Artikel über neuronale Netze.
Im vorherigen Artikel, Neuronale Netze in der Praxis: Das erste Neuron“ haben wir unser erstes Neuron erstellt. Programmieren ist jedoch nicht so einfach. Viele Leute erstellen einfach, oder kopieren eher, etwas Code und fangen an, ihn zu verwenden. Das ist definitiv kein Problem. Es ist keineswegs etwas daran falsch; ganz im Gegenteil. Es ist eine ziemlich vernünftige Praxis, vorausgesetzt natürlich, dass wir den Code studieren und versuchen, ihn zu verbessern, damit er unseren Anforderungen entspricht. Problematisch wird es, wenn man etwas verwendet, ohne zu verstehen, wie es funktioniert. Oder, noch schlimmer, sich über etwas zu beschweren oder, noch schlimmer, über etwas zu reden, von dem man absolut nichts versteht.
Obwohl der Code, den wir im vorherigen Artikel untersucht haben, funktionsfähig ist, sollte er unter keinen Umständen in komplexeren Anwendungen verwendet werden. Dies ist notwendig, damit alles zeitnah oder zumindest innerhalb bestimmter Erwartungen bezüglich der Ergebnisse erledigt wird.
Damit Sie, liebe Leser, verstehen können, wovon ich spreche, lassen Sie uns sehen, wie die Dinge aussehen und welche Probleme auftreten können, wenn wir ein paar Änderungen vornehmen. Aber um es klarer zu machen, lassen Sie uns dies als ein neues Thema betrachten.
Unnötige Verlängerung der Ausführungszeit
Ausgezeichnet, unser einfaches und einzigartiges Neuron ist im folgenden Code zu sehen.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define macroRandom (rand() / (double)SHORT_MAX) 05. #define macroSigmoid(a) (1.0 / (1 + MathExp(-a))) 06. //+------------------------------------------------------------------+ 07. double Train[][3] { 08. {0, 0, 0}, 09. {0, 1, 0}, 10. {1, 0, 0}, 11. {1, 1, 1}, 12. }; 13. //+------------------------------------------------------------------+ 14. const uint nTrain = Train.Size() / 3; 15. const double eps = 1e-3; 16. //+------------------------------------------------------------------+ 17. double Cost(const double w0, const double w1, const double b) 18. { 19. double err; 20. 21. err = 0; 22. for (uint c = 0; c < nTrain; c++) 23. err += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 24. 25. return err / nTrain; 26. } 27. //+------------------------------------------------------------------+ 28. void OnStart() 29. { 30. double w0, w1, err, ew0, ew1, eb, bias; 31. ulong count, it0, it1; 32. 33. Print("The Neuron - Tutor ..."); 34. MathSrand(512); 35. w0 = (double)macroRandom; 36. w1 = (double)macroRandom; 37. bias = (double)macroRandom; 38. 39. it0 = GetTickCount(); 40. 41. for (count = 0; (count < ULONG_MAX) && ((err = Cost(w0, w1, bias)) > eps); count++) 42. { 43. ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; 44. ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; 45. eb = (Cost(w0, w1, bias + eps) - err) / eps; 46. w0 -= (ew0 * eps); 47. w1 -= (ew1 * eps); 48. bias -= (eb * eps); 49. } 50. 51. it1 = GetTickCount(); 52. Print("Time: ", (it1 - it0) / 1000.0, " seconds."); 53. PrintFormat("%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f", count, w0, ew0, w1, ew1, bias, eb, err); 54. Print("w0 = ", w0, " || w1 = ", w1, " || Bias = ", bias); 55. Print("Error Weight 0: ", ew0); 56. Print("Error Weight 1: ", ew1); 57. Print("Error Bias: ", eb); 58. Print("Error: ", err); 59. 60. Print("Testing the neuron..."); 61. for (uchar p0 = 0; p0 < 2; p0++) 62. for (uchar p1 = 0; p1 < 2; p1++) 63. PrintFormat("%d AND %d IS %f", p0, p1, macroSigmoid((p0 * w0) + (p1 * w1) + bias)); 64. 65. Print("************************************"); 66. } 67. //+------------------------------------------------------------------+
Wir haben einige Änderungen am Code vorgenommen, verglichen mit dem, was im vorherigen Artikel veröffentlicht wurde. Der Grund dafür ist ganz einfach. Wir möchten zeigen, wie eine einfache Änderung die Gesamtausführungszeit ebenso wie die erwarteten Ergebnisse spürbar beeinflussen kann. Wenn der obige Code in MetaTrader 5 ausgeführt wird, ergibt sich eine Ausgabe, die in etwa wie folgt aussieht.

Achten Sie auf einige der im obigen Bild gezeigten Daten. Das Offensichtlichste, und dasjenige, das wir hervorheben möchten, ist natürlich die Ausführungszeit. Diese Laufzeit spiegelt nur den Teil der Anpassung der Parameter des Neurons wider. Sie können dies sehen, wenn Sie sich den Code ansehen. Beachten Sie, dass wir in Zeile 39 den genauen Moment unmittelbar vor Beginn des Neuronen-Trainingsprozesses erfassen. In Zeile 51 erfassen wir erneut einen entsprechenden Zeitpunkt, diesmal nach Abschluss des Trainings. Die Differenz zwischen diesen beiden Werten ergibt die Ausführungszeit des Trainings. Bitte beachten Sie, dass wir in Zeile 52 eine kleine Berechnung durchführen. Da die erfasste Zeit in Millisekunden gemessen wird, ergibt die Division der Differenz zwischen den Zeitwerten durch eintausend eine ungefähre Zeit in Sekunden. Dies ist genau der Zeitraum, der im obigen Bild hervorgehoben ist.
Lassen Sie uns nun sehen, was wir machen werden. In Zeile 15 desselben Codes setzen wir die Fehlerschwelle auf 0.001. Aber was wird in Bezug auf die Ausführungszeit für denselben Code passieren, wenn wir den Fehler auf 0.0001 oder 1e-4 ändern?
Gut, probieren Sie dies auf Ihrem eigenen Computer aus und sehen Sie, was passiert. Auf jeden Fall werden Sie etwas sehr Ähnliches wie das Folgende erhalten:

Jetzt aufgepasst: Allein die Änderung des Fehlers von 1e-3 auf 1e-4 lässt die Ausführungszeit von weniger als 1 Sekunde auf fast 80 Sekunden ansteigen. Genau das sehen Sie gerade. Eine einfache zehnfache Änderung der Genauigkeit führte dazu, dass die Ausführungszeit um mehr als das 80-fache anstieg.
Genau solche Momente sind es, die letztendlich die Träume vieler Anfänger zerstören, insbesondere derjenigen, die keine Ahnung haben, dass Programmieren keineswegs die einfachste Aufgabe ist. Wenn wir sagen, dass jedes Programm oft darauf ausgelegt ist, eine bestimmte Aufgabe zu erfüllen, sind viele Menschen überrascht, besonders wenn jemand sagt, dass ein neuronales Netzwerk alles sehr schnell erledigen kann, obwohl dies in Wirklichkeit nicht ganz stimmt.
Ich möchte Sie, liebe Leser, auf diese Tatsache aufmerksam machen. Bevor Sie dazu übergehen, einem Netzwerk neue Neuronen hinzuzufügen, ist es äußerst wichtig zu verstehen, wie das Netzwerk aufgebaut sein wird und zu welchem Zweck. Um dies zu demonstrieren, nehmen wir genau dasselbe einfache Neuron und ersetzen es durch etwas leicht anderes, das jedoch immer noch dazu bestimmt ist, dasselbe Problem wie im Code zu lösen. Mit anderen Worten: Wir werden das Neuron ein wenig spezialisierter machen – nicht im Sinne der Anpassung an die Daten, sondern im Sinne der Anzahl der Eingänge, die es verarbeiten soll. Um diese Themen zu trennen, lassen Sie uns dies in einem separaten Abschnitt besprechen.
Anpassung des Codes zur Erzielung besserer Ergebnisse
Denken wir nun über Folgendes nach: Welchen Sinn hat es, ein einzelnes Neuron 80-mal länger arbeiten zu lassen, nur um die Genauigkeit um das Zehnfache zu verbessern? Solche Dinge ergeben keinen Sinn. Wenn wir jedoch im Voraus wissen, dass das Neuron während seiner gesamten Lebensdauer mit zwei Eingängen und einem Ausgang arbeiten wird, können wir seinen Code verbessern. Dies beschleunigt das Training und macht das Neuron effizienter beim Erlernen dieser spezifischen Aufgabe.
Diese Änderungen vorzunehmen, mag sehr schwierig erscheinen, als ob nur jemand mit umfassendem Wissen eine solche Leistung vollbringen könnte. Das stimmt jedoch nicht ganz. Alles, was Sie verstehen müssen, liebe Leser, ist, wie das Programm funktioniert und was sein Zweck ist. Wenn Sie dies wissen und verstehen, wie das Programm in einer bestimmten Sprache geschrieben wurde, dann werden Sie in der Lage sein, wenn auch mit etwas Zeitaufwand, eine wesentlich bessere Lösung zu implementieren.
Um dies zu demonstrieren, werden wir den Code aus dem vorherigen Thema modifizieren und dabei den ursprünglichen Code beibehalten. Unser Ziel ist es zu verstehen, ob wir uns in die richtige Richtung bewegen oder nicht. Ein guter Programmierer versucht niemals, unbekannten Code zu ändern. Zuerst versuchen sie, den Code zu verstehen, herauszufinden, wie er funktioniert, und dann versuchen sie, einige Verbesserungen vorzunehmen. Vor diesem Hintergrund sehen Sie unten den neuen Code.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define macroRandom (rand() / (double)SHORT_MAX) 05. #define macroSigmoid(a) (1.0 / (1 + MathExp(-a))) 06. //+------------------------------------------------------------------+ 07. #define def_Fast 08. //+------------------------------------------------------------------+ 09. double Train[][3] { 10. {0, 0, 0}, 11. {0, 1, 0}, 12. {1, 0, 0}, 13. {1, 1, 1}, 14. }; 15. //+------------------------------------------------------------------+ 16. const uint nTrain = Train.Size() / 3; 17. const double eps = 1e-4; 18. //+------------------------------------------------------------------+ 19. double Cost(const double w0, const double w1, const double b) 20. { 21. double err; 22. 23. err = 0; 24. for (uint c = 0; c < nTrain; c++) 25. err += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 26. 27. return err / nTrain; 28. } 29. //+------------------------------------------------------------------+ 30. double Cost_2(double &w0, double &w1, double &b) 31. { 32. double err, ew0, ew1, eb; 33. 34. err = ew0 = ew1 = eb = 0; 35. for (uint c = 0; c < nTrain; c++) 36. { 37. err += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 38. ew0 += MathPow((macroSigmoid((Train[c][0] * (w0 + eps)) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 39. ew1 += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * (w1 + eps)) + b) - Train[c][2]), 2); 40. eb += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + (b + eps)) - Train[c][2]), 2); 41. } 42. 43. w0 -= (((ew0 - err)/ eps) * eps); 44. w1 -= (((ew1 - err)/ eps) * eps); 45. b -= (((eb - err)/ eps) * eps); 46. 47. return err / nTrain; 48. } 49. //+------------------------------------------------------------------+ 50. void OnStart() 51. { 52. double w0, w1, err, ew0, ew1, eb, bias; 53. ulong count, it0, it1; 54. 55. Print("The Neuron - Tutor..."); 56. MathSrand(512); 57. w0 = (double)macroRandom; 58. w1 = (double)macroRandom; 59. bias = (double)macroRandom; 60. 61. it0 = GetTickCount(); 62. #ifdef def_Fast 63. for (count = 0; (count < ULONG_MAX) && ((err = Cost_2(w0, w1, bias)) > eps); count++); 64. #else 65. for (count = 0; (count < ULONG_MAX) && ((err = Cost(w0, w1, bias)) > eps); count++) 66. { 67. ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; 68. ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; 69. eb = (Cost(w0, w1, bias + eps) - err) / eps; 70. w0 -= (ew0 * eps); 71. w1 -= (ew1 * eps); 72. bias -= (eb * eps); 73. } 74. #endif 75. it1 = GetTickCount(); 76. Print("Time: ", (it1 - it0) / 1000.0, " seconds."); 77. PrintFormat("%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f", count, w0, ew0, w1, ew1, bias, eb, err); 78. Print("w0 = ", w0, " || w1 = ", w1, " || Bias = ", bias); 79. Print("Error Weight 0: ", ew0); 80. Print("Error Weight 1: ", ew1); 81. Print("Error Bias: ", eb); 82. Print("Error: ", err); 83. 84. Print("Testing the neuron..."); 85. for (uchar p0 = 0; p0 < 2; p0++) 86. for (uchar p1 = 0; p1 < 2; p1++) 87. PrintFormat("%d AND %d IS %f", p0, p1, macroSigmoid((p0 * w0) + (p1 * w1) + bias)); 88. 89. Print("************************************"); 90. } 91. //+------------------------------------------------------------------+
Machen Sie sich keine Sorgen darüber, diesen Code zu schreiben. Sie können dies tun, wenn Sie möchten, um besser zu verstehen, wie man in MQL5 programmiert, denn wenn wir Code schreiben, verstehen wir besser, wie er erstellt wird. Darüber hinaus haben wir auch verschiedene weitere Aspekte der Syntax unserer Programmiersprache behandelt. Aber Sie können im Anhang darauf zugreifen. Es liegt also an Ihnen, wie Sie dieses Wissen nutzen.
Lassen Sie uns nun sehen, was wir in Bezug auf die Ausführungszeit erhalten. Dies ist auf dem nachstehenden Bild zu sehen.

„Was? Aber wie ist das möglich? Wie ist die Zeit von fast 80 Sekunden auf etwas mehr als 18 Sekunden gesunken? Das ist absolut unmöglich. Ah, ich weiß, Sie haben den Computer gewechselt und dafür einen anderen, leistungsstärkeren verwendet. Oder Sie haben mit ziemlicher Sicherheit irgendwie geschummelt, indem Sie einen weiteren Fehler in das System eingeführt oder etwas anderes geändert haben. Auf diese Weise dauert der erste Durchlauf länger, wie in der ersten Abbildung gezeigt, aber dann würden Sie durch einen Trick viel weniger Zeit aufwenden. Auf diese Weise würden Sie das Bild erhalten, das in der zweiten Abbildung zu sehen ist."
Nun, liebe Leser, vielleicht hätten wir das tun können. Aber nein, genau deshalb haben wir den Code im Anhang gelassen. Wenn Sie denselben Code auf Ihrem eigenen Computer ausprobieren, werden Sie verstehen können, was passiert ist. Aber bevor Sie das ausprobieren, sehen wir uns zunächst an, warum der Code, der zuvor fast 80 Sekunden dauerte, jetzt in etwas mehr als 18 Sekunden ausgeführt wird. Zunächst einmal möchte ich sagen, dass es hier keine Magie oder Ähnliches gibt. Es geht darum, zu verstehen, was das Neuron lernen muss.
Wenn Sie sich den Code ansehen, mag er sehr kompliziert erscheinen, besonders wenn Sie gerade erst mit dem Programmieren beginnen. Machen Sie sich jedoch keine Sorgen; es ist recht einfach. Ich habe dafür gesorgt, dass es so bleibt. Dies macht die Erklärung der Funktionsweise anschaulicher und praktischer.
Beginnen wir mit Zeile 07. Sie enthält eine Definition, und dies ist eine der wenigen Stellen, an denen Sie als Anfänger den Code ändern können, ohne zu riskieren, ihn instabil zu machen. Wenn Zeile 07 entfernt wird, bleibt der Code genau derselbe wie im vorherigen Abschnitt. Mit anderen Worten: langsam und ineffizient, aber dennoch universell. Wenn jedoch das Makro in Zeile 07 wie hier im Artikel definiert ist, wird die schnelle Version des Codes verwendet, und Sie werden das im Bild oben gezeigte Ergebnis sehen.
Jetzt möchte ich, dass Sie das Bild aus diesem Abschnitt mit den Bildern aus dem vorherigen vergleichen und dabei auf den Fehlerwert achten. Bitte beachten Sie, wie ähnlich sie sind. Dies gilt insbesondere für das letzte Bild aus dem vorherigen Thema und das hier Gezeigte. Mit anderen Worten: In beiden Fällen verwenden wir einen Fehler von 1e-4. „Warten Sie einen Moment. Wollen Sie damit sagen, dass die Ausführungszeit trotz der gleichen Genauigkeit stark gesunken ist?“ Ja, liebe Leser, das ist möglich. Genau das ist geschehen. „Aber warum ist das passiert? Könnten Sie mir das erklären?“
Gut, um das zu verstehen, müssen wir uns ein paar Punkte im Code ansehen. Lassen Sie uns den größten Teil des Codes überspringen und zur Prozedur OnStart übergehen, die sich in Zeile 50 befindet. Jetzt brauchen wir ein wenig Geduld, aber zu verstehen, was passiert, ist überhaupt nicht schwierig. Bitte beachten Sie, dass der größte Teil des Codes in dieser Prozedur bereits im vorherigen Abschnitt besprochen wurde. Diese identischen Teile machen das Neuron zu demselben, das wir zuvor gesehen haben. In Zeile 62 weisen wir den Compiler an, eine Prüfung durchzuführen, bevor die endgültige ausführbare Datei erstellt wird. Dieser Test prüft, ob die Definition in Zeile 07 vorhanden ist.
Wenn sie nicht vorhanden ist, ist der resultierende Code identisch mit dem, was im vorherigen Thema besprochen wurde. Wenn das Makro in Zeile 07 definiert ist, wird der Code aus dem vorherigen Thema durch den Code in Zeile 63 ersetzt. Achten Sie auf Folgendes. Wir ersetzen den Code zwischen den Zeilen 65 und 73 durch den Code aus Zeile 63. Alles andere bleibt unverändert und unberührt.
Aber wenn wir uns jetzt den Inhalt von Zeile 63 ansehen, können wir erkennen, dass es sich um eine for-Schleife handelt. Sie ist dem Code in Zeile 65 sehr ähnlich. Der Unterschied besteht natürlich darin, dass wir in Zeile 65 Cost aufrufen, während wir in Zeile 63 Cost_2 aufrufen. In jeder anderen Hinsicht ist der Code identisch. „Aber das erklärt nicht, warum es in einem Fall fast 80 Sekunden dauert, während es im anderen nur 18 Sekunden dauert. Solche Dinge ergeben absolut keinen Sinn.“ Aber genau da irren Sie sich, liebe Leser. Diese einfache Änderung kann den entscheidenden Unterschied ausmachen. Bitte beachten Sie, dass die Funktion Cost, obwohl wir uns nur Zeile 65 ansehen, tatsächlich viermal aufgerufen wird: in Zeile 65, Zeile 67, Zeile 68 und Zeile 69.
Für jeden dieser Aufrufe müssen die gesamte Tabelle, die Datenbank oder, in unserem Fall, das Trainings-Array analysiert werden. Denken Sie einen Moment darüber nach. Beachten Sie, dass unser Trainings-Array nur vier Datenzeilen enthält. Dies ist in Zeile 09 des Codes zu sehen. Nur vier Datenzeilen. Allerdings muss bei jedem Aufruf der Kostenfunktion in Zeile 19 die Schleife in Zeile 24 alles erneut lesen und berechnen. Dies gilt für alle Aufrufe in der OnStart-Prozedur.
Dieser Programmierstil vereinfacht die Aufgabe zwar erheblich, ist aber im Allgemeinen nicht sehr effizient. Derselbe Programmierstil macht das Neuron jedoch wesentlich universeller. Dies liegt daran, dass das Neuron so konzipiert werden kann, dass es beliebig viele Eingaben oder Ausgaben verarbeitet, ohne dass Änderungen an diesem Teil des Codes erforderlich sind. Genau hier wird es entscheidend, zu wissen, was wir tun, oder das erwartete bzw. beabsichtigte Ergebnis zu kennen. Viele würden diese Funktion aus Zeile 19 nehmen und in OpenCL-Code umwandeln oder sogar in Code, der auf parallelen Systemen ausgeführt werden könnte. In einigen Fällen können Maschinen, die parallele Anwendungen ausführen, recht teuer sein.
In anderen Fällen reicht sogar eine gute Grafikkarte aus. Dennoch würden wir lediglich eine Sache durch eine andere ersetzen, da der Code weiterhin ineffizient bliebe. Wir sagen das deshalb, weil wir bei jeder Berechnung der Kosten die gesamte für das Training verwendete Datenbank durchsuchen müssten. Stellen Sie sich eine Datenbank vor, die tausende Megabyte an Daten enthält.
Kehren wir nun zu Zeile 63 zurück. Auf den ersten Blick mag Zeile 63 nicht besonders erscheinen, aber sie ruft eine spezialisierte Funktion auf. Dies beschleunigt die Ausführung im Vergleich zur nicht spezialisierten Ausführung erheblich. Ein spezialisiertes Neuron macht die Programmierung jedoch etwas komplizierter, denn wenn wir etwas am Neuron ändern müssen, sei es die Anzahl der Eingänge oder die Anzahl der Ausgänge, werden wir den Code der Funktion Cost2 vollständig umschreiben müssen. Der Grund liegt genau in der Art und Weise, wie diese Funktion arbeitet. Lassen Sie uns sehen, wie das funktioniert, und zu Zeile 30 übergehen, wo Cost_2 implementiert ist. Aber um die Erklärung klarer zu machen, lassen Sie uns diese Funktion als neues Thema untersuchen.
Spezialisierung eines einzelnen Neurons
Lassen Sie uns nun für diejenigen, die alles in Ruhe studieren und im Wesentlichen verstehen wollen, wie es funktioniert, die Funktion Cost_2 in zwei Teile unterteilen. Dies wird es einfacher machen, zu erklären, warum sie schneller funktioniert als die Standardversion.
Sehr gut, der erste Teil befindet sich zwischen den Zeilen 35 und 41. Was macht der Code in diesen Zeilen? Es tut dasselbe, was die Zeilen 65, 67, 68 und 69 früher getan haben. Darüber hinaus führt es natürlich auch dieselbe Operation aus wie die for-Schleife in Zeile 24. Mit anderen Worten: Wir führen letztendlich Zeile 25 aus. Im Gegensatz zu dem, was zuvor getan wurde, erledigen wir jetzt jedoch alles gleichzeitig. Infolgedessen benötigen wir mehr Variablen als zuvor. Jede der in Zeile 32 deklarierten Variablen dient dazu, die Codeausführung zu beschleunigen. Daher berechnen wir in Zeile 37 den Gesamtfehler des Neurons. Dies wird den Prozess nicht beschleunigen, aber in den Zeilen 38, 39 und 40 berechnen wir alle anderen Fehler, die zuvor in der for-Schleife in Zeile 65 gemessen wurden.
Nun kommen wir zum schwierigen Teil der Angelegenheit. Zeile 40 ist Teil jedes Neurons. Mit anderen Worten, es ist universeller Code, genau wie Zeile 37. Aber die Zeilen 38 und 39 sind es nicht. Genau, sie spezialisieren das Neuron. Mit anderen Worten: Wenn wir dem Neuron mehr Eingaben hinzufügen müssen, werden wir mehr solcher Zeilen zum Code hinzufügen müssen, und ein für einen bestimmten Zweck erstelltes Neuron wird höchstwahrscheinlich für nichts anderes nützlich sein. Dieser Ansatz macht den Code jedoch viel schneller als eine allgemeinere Implementierung, die für die Aufgabe schlecht optimiert ist.
Der zweite Teil des Codes befindet sich in den Zeilen 43, 44 und 45. Dort korrigieren wir die Werte für den nächsten Aufruf der Kostenfunktion. Zuvor wurde dies in den Zeilen 70, 71 und 72 erledigt.
Allein durch diese kleinen Änderungen am Code konnte das Neuron viel schneller laufen, was es ihm ermöglichte, zu lernen oder, genauer gesagt, einen Satz von Werten zu erzeugen, der als gespeichertes Vorwissen genutzt werden kann. Und das ist die wahre Schönheit dieser Art von Programmierung. Es geht nicht um den Aufruf selbst als Test, sondern um die Freude an der Erkenntnis, dass alles viel effizienter erledigt werden kann. Bitte beachten Sie, dass es nicht einmal notwendig war, OpenCL zu verwenden oder parallelisierten Code zu schreiben. Dies ist weitaus fesselnder und angenehmer als all die Langeweile, die viele Menschen an den Tag legen, besonders wenn das Thema auf neuronale Netze kommt.
In Ordnung, dies war ein ziemlich einfaches Beispiel dafür, wie dies gemacht werden kann, aber wir können bei demselben Thema noch ein wenig weitergehen. Obwohl dieses einfache Neuron zu vielen Dingen fähig ist und seine einzige Einschränkung darin besteht, die Anzahl der Eingaben ständig erhöhen oder verringern zu müssen, während man natürlich immer versucht, dies so schnell wie möglich zu tun, ist es dennoch nicht „intelligent“ genug, um mit bestimmten Arten von Situationen umzugehen. Daher sind künstliche Intelligenz oder neuronale Netze nicht so einfach, wie viele Leute glauben. Ebenso kann man nicht davon ausgehen, dass ein Neuron in der Lage sein wird, jede Situation innerhalb eines bestimmten Szenarios zu bewältigen.
Um dies zu veranschaulichen, lassen Sie uns ein kleines Gedankenexperiment durchführen. Hier ist nichts Kompliziertes dabei; ich möchte Ihnen einfach etwas zeigen, das viele Leute übersehen, wenn sie neuronale Netze studieren. Viele Leute schweigen darüber oder wollen nicht darüber sprechen, aber wenn Sie es verstehen, werden Sie erkennen, dass kein Programm den Geist übertreffen kann, der es erschaffen hat.
Lassen Sie uns die für das Training verwendeten Daten betrachten. Es gibt Situationen, in denen ein Neuron die Daten nicht verarbeiten kann. Und das liegt nicht daran, dass die Daten extrem komplex sind oder etwas in der Art; im Grunde sind die Daten einfach. Aber ein einzelnes Neuron löst das Problem nicht, und ich wiederhole, es löst es NICHT, daher muss mehr als ein Neuron dafür verwendet werden. Daher werden wir kurz ein Thema umreißen, das später besprochen wird. In einigen spezifischen Fällen ist es jedoch akzeptabel, ein kleines neuronales Netzwerk unter Verwendung eines einzelnen Neurons zu modellieren. Es hängt alles von der Art der Aufgabe ab, für die wir eine Lösung suchen.
Daher kann in vielen Fällen, und tatsächlich in den meisten davon, ein Neuron gebaut oder, besser gesagt, programmiert werden, sodass es in Zukunft zur Implementierung eines neuronalen Netzwerks verwendet werden kann. Und ich spreche nicht von neuronalen Netzwerken wie Chatbots oder etwas Ähnlichem; ich spreche von etwas viel Einfacherem. Es gibt jedoch Fälle, in denen wir ein einzelnes Neuron dazu bringen können, eine Aufgabe zu lösen, die normalerweise ein kleines neuronales Netzwerk erfordern würde. Und wir müssen keine Big-Data-Szenarien mit enormen Datenmengen konstruieren, um eine Situation zu finden, in der dies geschieht. Dies kann mit demselben Code durchgeführt werden, der in diesem Artikel gezeigt wird.
„Wow, jetzt ist wirklich alles viel komplizierter geworden. Warum? Lassen Sie mich sehen, ob ich Sie richtig verstanden habe. Sagen Sie, dass dieses Neuron, mit dem wir arbeiten, etwas nicht verarbeiten kann, das in derselben Datenbank dargestellt werden kann, aber wenn wir wissen, wie wir damit umgehen müssen, können wir es dazu bringen, ein kleines neuronales Netzwerk zu imitieren und so eine Aufgabe zu lösen, die es alleine nicht lösen kann? Ist das richtig?“ Ja, das ist korrekt. Bitte beachten Sie, dass der von uns verwendete Trainingsdatensatz einer Wahrheitstabelle für Logikgatter ähnelt. Mit anderen Worten, wir können ihm Kombinationen aus Nullen und Einsen zuführen und das Neuron bitten, zu versuchen, eine Gleichung oder, genauer gesagt, eine lineare Funktion zu finden, die ein logisches Gatter repräsentieren könnte.
Dennoch gibt es zwei Fälle, obwohl wir sie als einen behandeln können. Dies liegt daran, dass sie zueinander invers sind. In diesen Fällen wird dieses einfache Neuron nicht in der Lage sein, die richtige Gleichung zu finden, aber wenn wir es zwingen, auf eine bestimmte Weise zu arbeiten, wird es möglich sein, die richtige Gleichung zu finden.
Um das, was ich erklären möchte, verständlicher zu machen, betrachten Sie das folgende Bild.

Dieses Bild zeigt die wichtigsten logischen Gatter, die hier betrachtet werden. Der grüne Bereich enthält die Gattereingänge. Die Ausgänge sind in Rot- und Orangetönen dargestellt, und der Gattertyp ist oben in jedem Diagramm angegeben.
Lassen Sie uns nun sehen, was das bedeutet. Von links nach rechts sind die Gatter in der folgenden Reihenfolge angeordnet: AND, NAND, OR, NOR, XOR und NXOR.
Und welche Bedeutung hat diese Tabelle im Bild für uns, wenn unser Thema neuronale Netze ist? Um dies zu verstehen, lassen Sie uns noch einmal den Neuron-Code betrachten. Beachten Sie, dass wir in Zeile 09, wo wir die Trainingsregeln definieren, genau das Gleiche haben wie in der Tabelle, die im Bild gezeigt wird.
Nun kommt der interessante Teil: Die ersten beiden Werte sind die Eingabedaten. Der dritte Wert hingegen ist das Ergebnis. Vergleichen Sie das, was Sie im Code sehen, mit der Tabelle, die im Bild gezeigt wird. Was fällt Ihnen auf? Nun, wenn Sie etwas genauer hinsehen, werden Sie feststellen, dass wir im Trainingsprozess das AND-Gatter definieren.
Sie können experimentieren, indem Sie die Werte des dritten Arguments ändern, das beim Training verwendet wird. Sie werden sehen, dass Sie das Neuron dazu bringen können, eine Gleichung zu lernen oder zu generieren, die fast alle Gatter darstellen kann. Ich sage „fast“, weil das Neuron nicht in der Lage sein wird, zwei davon zu lernen. Diese Gatter sind genau XOR und sein inverses Gegenstück, NXOR.
„Wow! Aber warten Sie einen Moment. Warum wird das Neuron nicht in der Lage sein, zu lernen, die XOR- und NXOR-Gatter zu generieren? Machen Sie Witze? Alle Werte bestehen ausschließlich aus Nullen und Einsen. Das kann nur ein Witz oder ein Streich sein. Machen Sie sich lustig über mich?“
Ich würde sogar gerne scherzen und Sie aufziehen, aber tatsächlich ist das nicht der Fall. Dies ist eine Tatsache, und Sie können sie überprüfen. Wenn Sie versuchen, ein einzelnes Neuron nur mit Nullen und Einsen zu trainieren, wird das Neuron in einigen Fällen nicht in der Lage sein, mit der Situation umzugehen. Das liegt daran, dass die Lösung weitaus komplexer ist, als das, was mit einem einzelnen Neuron modelliert werden kann.
Es ist unwahrscheinlich, dass Sie jemals jemanden über solche Dinge sprechen hören oder dass sie überhaupt erwähnt werden. Dies geschieht, wenn das Thema, das wir untersuchen, in irgendeiner Weise mit künstlicher Intelligenz, neuronalen Netzen oder der Programmierung von Neuronen zusammenhängt. Die Leute denken nicht daran, alle alternativen Optionen auszuprobieren. Wenn etwas in einem bestimmten Fall funktioniert, reden sie sofort überall darüber. Aber wenn man sie nach spezifischen Situationen fragt, fangen sie an, um den heißen Brei herumzureden oder die Frage zu ignorieren.
Es gibt jedoch eine Möglichkeit, ein Neuron für solche Fälle zu trainieren, zum Beispiel wenn wir das XOR-Gatter-Problem oder dessen Umkehrung, das NXOR-Gatter, lösen müssen. Da dieses Thema im Rahmen dieses Artikels nur schwer zu erklären ist, werden wir in der nächsten Veröffentlichung untersuchen, wie dieses Problem gelöst werden kann. Aber bevor der nächste Artikel veröffentlicht wird, können Sie, meine lieben Leser, vielleicht selbst einen Weg finden, das Problem des XOR-Gatters und seiner Umkehrung, des NXOR-Gatters, zu lösen. Hinweis: Sie dürfen nicht mehr als ein Neuron verwenden; das wäre Schummelei. Die Regel lautet: Es darf nur ein Neuron zur Implementierung der Lösung verwendet werden.
Abschließende Überlegungen
In diesem Artikel haben wir gezeigt, wie eine kleine Änderung im Code, die auf die Spezialisierung des Neurons abzielt, die Trainingsphase deutlich beschleunigen kann. Denn sobald ein Neuron oder ein neuronales Netz, wie wir später sehen werden, trainiert wurde, arbeitet es anschließend wesentlich schneller. So sehr, dass man im Code, der sowohl im Artikel selbst als auch im Anhang präsentiert wird, sehen kann, dass die Generierung von Ergebnissen aus Eingabedaten sehr schnell erfolgt. Dies gilt natürlich unter der Voraussetzung, dass wir bereits wissen, welche Werte für die Berechnung verwendet werden sollen.
Hier haben wir auch ein bestehendes Problem erwähnt, über das fast niemand spricht oder das auch nur erwähnt wird. Gemeint ist, dass ein Neuron nicht in der Lage ist, alle möglichen Szenarien zu bewältigen, entgegen dem, was viele Leute zu glauben gewohnt sind. Eine Maschine ist nicht so perfekt und nicht in der Lage, ein Lebewesen zu übertreffen. Ein einzelnes biologisches Neuron ist in der Lage, jede einfache Aufgabe zu lösen. Dennoch ist ein programmiertes oder in Silizium implementiertes Neuron nicht zu einer solchen Leistung fähig.
Im nächsten Artikel werden wir uns also weiter mit diesem Thema befassen. Bis später und bis bald.
Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/13748
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Von der Grundstufe bis zur Mittelstufe: Objektereignisse (I)
Neuronale Netze im Trading: LSTM-Optimierung für die multivariate Zeitreihenprognose (letzter Teil)
Entwicklung eines Multi-Currency Expert Advisors (Teil 28): Hinzufügen eines Managers zum Schließen von Positionen
Von der Grundstufe zur Mittelstufe: Objekte (IV)
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.