English Русский 中文 Español 日本語 Português
preview
Neuronale Netze leicht gemacht (Teil 27): Tiefes Q-Learning (DQN)

Neuronale Netze leicht gemacht (Teil 27): Tiefes Q-Learning (DQN)

MetaTrader 5Handelssysteme | 14 November 2022, 10:38
294 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Inhalt

Einführung

Im vorigen Artikel haben wir begonnen, Methoden des Reinforcement Learning zu erforschen und unser erstes trainierbares Modell mit Cross-Entropie zu erstellen. In diesem Artikel befassen wir uns weiter mit Methoden des Verstärkungslernens. Wir werden mit der Methode des Deep Q-Learning fortfahren. Mithilfe von Deep Q-Learning gelang es dem DeepMind-Team 2013, ein Modell zu entwickeln, das sieben Atari-Computerspiele erfolgreich spielen kann. Bemerkenswert ist, dass sie für alle 7 Spiele das gleiche Modell trainierten, ohne die Architektur oder die Hyperparameter zu ändern. Nach den Trainingsergebnissen konnte das Modell in 6 der analysierten Spiele die zuvor erzielten Ergebnisse verbessern. Außerdem hat das Modell in drei Spielen besser abgeschnitten als ein Mensch. Die Veröffentlichung dieser Arbeit leitete eine neue Phase in der Entwicklung von Methoden des Reinforcement Learning ein. Sehen wir uns diese Methode an und versuchen wir, sie zur Lösung von Problemen im Zusammenhang mit dem Handel einzusetzen.


1. Das Konzept der Q-Funktion

Lassen Sie uns zunächst auf das im letzten Artikel behandelte Thema zurückkommen. Beim Verstärkungslernen bauen wir den Prozess der Interaktion zwischen einem Agenten und seiner Umgebung auf. Der Agent analysiert den aktuellen Zustand der Umgebung und führt eine Aktion durch, die den Zustand der Umgebung verändert. Als Reaktion auf die Aktion gibt die Umwelt dem Agenten Belohnungen zurück. Der Agent weiß nicht, wie sich die Belohnungen zusammensetzen. Das Ziel des Agenten ist es, die höchstmögliche Gesamtbelohnung für die zu analysierende Sitzung zu erhalten.

Achten Sie darauf, dass der Agent die Belohnung für die Aktion nicht erhält. Er erhält die Belohnung für den Übergang von einem Zustand in einen anderen. Gleichzeitig garantiert die Durchführung einer bestimmten Handlung in einer ähnlichen Situation nicht den Übergang in denselben Zustand. Das Ausführen einer Handlung führt nur mit einer gewissen Wahrscheinlichkeit zum Übergang in den erwarteten Zustand. Die Wahrscheinlichkeiten und Abhängigkeiten von Zuständen, Aktionen und Übergängen sind dem Agenten unbekannt. Der Agent muss sie im Prozess der Interaktion mit der Umwelt erlernen. 

Das Verstärkungslernen basiert auf der Annahme, dass es eine Beziehung zwischen dem aktuellen Zustand, der durchgeführten Aktion und der Belohnung gibt. Mathematisch gesehen, gibt es eine Funktion Q die in Abhängigkeit vom Zustand s und Aktionen a die Belohnung r. Sie wird bezeichnet als Q(s|a). Diese Funktion wird als die Handlungsnutzenfunktion.

Der Agent kennt diese Funktion nicht. Aber wenn es sie gibt, dann können wir uns dieser Funktion im Prozess der Interaktion mit der Umwelt durch unendlich viele Wiederholungen der Handlungen annähern.

Unter realen Bedingungen ist es nicht möglich, Zustände und Aktionen unendlich oft zu wiederholen. Aber mit genügend Wiederholungen können wir die Funktion mit einem akzeptablen Fehler annähern. Die Form des Q-Funktionsausdrucks kann unterschiedlich sein. Im vorigen Artikel haben wir bei der Bestimmung des Nutzens jeder Aktion eine Tabelle mit den Abhängigkeiten zwischen Zustand, Aktion und durchschnittlicher Belohnung erstellt. Es gibt andere Formen des Ausdrucks der Q-Funktion, die durchaus akzeptabel sind oder sogar bessere Ergebnisse liefern können. Dies können Entscheidungsbäume, neuronale Netze usw. sein.

Bitte beachten Sie, dass die vom Agenten approximierte Q-Funktion die Belohnungen nicht vorhersagt. Es liefert nur die erwarteten Erträge, die auf den bisherigen Erfahrungen des Agenten mit der Interaktion mit der Umwelt basieren.


2. Tiefes Q-Lernen

Sie haben wahrscheinlich schon erraten, dass beim Tiefem Q-Lernen (Deep Q-Learning) ein neuronales Netz zur Annäherung an eine Q-Funktion verwendet wird. Was ist der Vorteil eines solchen Ansatzes? Erinnern Sie sich an die Implementierung der tabellarischen Methode der Kreuzentropie im letzten Artikel. Ich habe betont, dass die Umsetzung einer tabellarischen Methode von einer endlichen Anzahl möglicher Zustände und Aktionen ausgeht. Wir haben also die Zahl der möglichen Zustände durch Clustering der Ausgangsdaten begrenzt. Aber ist es so gut? Führt das Clustering immer zu besseren Ergebnissen? Bei der Verwendung eines neuronalen Netzes ist die Zahl der möglichen Zustände nicht begrenzt. Ich halte dies für einen großen Vorteil bei der Lösung von Problemen im Zusammenhang mit dem Handel.

Der erste naheliegende Ansatz besteht darin, die Tabelle aus dem vorherigen Artikel durch ein neuronales Netz zu ersetzen. Aber so einfach ist es leider nicht. In der Praxis erwies sich der Ansatz als nicht so gut, wie er schien. Um den Ansatz umzusetzen, müssen wir einige Heuristiken hinzufügen.

Betrachten wir zunächst das Ziel der Agentenausbildung. Im Allgemeinen besteht das Ziel darin, den Gesamtgewinn zu maximieren. Sehen Sie sich die folgende Abbildung an. Der Agent muss sich von der Zelle Start zur Zelle Finish bewegen. Der Agent erhält die Belohnung einmal, wenn er die Zelle Finish (Ende) erreicht. In allen anderen Zuständen ist die Belohnung gleich Null.

Diskontfaktor

Die Abbildung zeigt zwei Wege. Für uns ist es offensichtlich, dass der orangefarbene Weg kürzer und vorteilhafter ist. Aber in Bezug auf die Maximierung der Belohnung sind sie gleichwertig.

Auch beim Handel ist es besser, die Erträge sofort zu erhalten, als jetzt zu investieren und die Erträge erst in ferner Zukunft zu erhalten. Dabei wird der Wert des Geldes berücksichtigt: der Zinssatz, die Inflation und eine Reihe anderer Variablen. Das tun wir auch hier. Um das Problem zu lösen, führen wir einen Diskontfaktor ɣ ein, der den Wert zukünftiger Belohnungen verringert.

Kumulative Belohnungen

Der Diskontfaktor ɣ kann im Bereich von 0 bis 1 liegen. Wenn der Diskontfaktor 1 ist, findet keine Reduzierung statt. Und mit einem Diskontfaktor von 0 werden zukünftige Gewinne ignoriert. In der Praxis wird der Diskontfaktor nahe bei 1 angesetzt.

Aber es gibt noch ein weiteres Problem. Was in der Theorie gut aussieht, funktioniert in der Praxis nicht immer. Wir können künftige Belohnungen leicht berechnen, wenn wir eine vollständige Übergangs- und Belohnungskarte (transition and reward map) haben. Unter ihnen können wir den besten Weg wählen, der uns am Ende den größten Nutzen bringt. Bei der Lösung praktischer Probleme wissen wir jedoch nicht, was der nächste Zustand sein wird, nachdem eine bestimmte Aktion ausgeführt wurde. Wir kennen auch nicht die Belohnungen. All dies gilt bereits für den unmittelbar folgenden Schritt. Noch gravierender ist es, wenn wir über den gesamten Weg bis zum Ende der Sitzung sprechen. Wir können nicht in die Zukunft sehen. Um die nächste Belohnung zu erhalten, muss der Agent eine Aktion durchführen. Erst nach dem Übergang in einen neuen Zustand gibt die Umwelt die Belohnungen zurück. Außerdem haben wir keinen Weg zurück. Wir können nicht zum vorherigen Zustand zurückkehren und eine andere Aktion durchführen, um später die beste zu wählen.

Daher werden wir Methoden der dynamischen Programmierung anwenden. Insbesondere die Bellman-Optimierungsmethode. Sie besagt, dass für die Wahl der optimalen Strategie bei jedem Schritt die optimale Aktion gewählt werden muss. Das heißt, wenn wir bei jedem Schritt die Aktion mit der maximalen Belohnung auswählen, erhalten wir die maximale kumulative Belohnung für die Sitzung. Die mathematische Formel für die Aktualisierung der Aktionsnutzenfunktion wird im Folgenden dargestellt.

Bellman-Optimierung

Sehen Sie sich die Formel an. Erinnert Sie das nicht an die Formel für die Aktualisierung der Gewichte beim stochastischen Gradientenabstieg? Um den Wert der Handlungsnutzenfunktion zu aktualisieren, benötigen wir den vorherigen Wert der Funktion plus eine gewisse Abweichung, multipliziert mit dem Lernfaktor.

In der dargestellten Funktion ist auch zu erkennen, dass zur Bestimmung des Wertes der Funktion zum Zeitpunkt t den Wert der Handlungsnutzenfunktion im nächsten Zeitschritt zum Zeitpunkt t+1. Mit anderen Worten, wenn wir uns im Zustand st befinden, führen wir die Aktion at aus und, nachdem wir in den Zustand st+1 übergegangen sind, erhalten wir die Belohnung rt+1. Um den Wert der Nutzenfunktion der Aktion zu aktualisieren, müssen wir das Maximum der Nutzenfunktion der Aktion zu den Belohnungen im nächsten Schritt hinzufügen. Das heißt, wir addieren die maximale erwartete Belohnung, die wir im nächsten Schritt erhalten können. Natürlich kann unser Agent nicht in die Zukunft blicken und die zukünftige Belohnung bestimmen. Aber er kann seine Näherungsfunktion nutzen: Im Zustand st+1 kann er den Wert der Funktion für alle möglichen Aktionen aus dem gegebenen Zustand berechnen und den höchsten der erhaltenen Werte nehmen. Während des Lernprozesses werden seine Werte zunächst weit von der Wahrheit entfernt sein. Aber es ist besser als nichts. Je mehr der Agent lernt, desto geringer wird der Vorhersagefehler.


2.1. Erfahrungswiedergabe

Der stochastische Gradientenabstieg ist gut, weil er die Aktualisierung der Funktionswerte auf der Grundlage der Werte einer kleinen Stichprobe aus der Grundgesamtheit ermöglicht. Sie ermöglicht es unserem Agenten, die Werte der Aktionsnutzenfunktion bei jedem Schritt der Sitzung zu aktualisieren. Beim überwachten Lernen haben wir jedoch eine Trainingsstichprobe verwendet, bei der die Zustände unabhängig voneinander sind. Um diese Eigenschaft zu stärken, haben wir die Population jedes Mal neu gemischt, bevor wir einen neuen Trainingsdatensatz ausgewählt haben.

Beim überwachten Lernen bewegt sich unser Agent jedoch in der Zeit durch die Umgebung, führt eine Aktion aus und betritt jedes Mal einen neuen Zustand, der eng mit dem vorherigen zusammenhängt. Sehen Sie sich um. Ob Sie gehen oder sitzen und eine Tätigkeit ausführen, die Umgebung um Sie herum ändert sich nicht wesentlich. Ihr Handeln verändert nur einen kleinen Teil davon, auf den dieses Handeln gerichtet ist. Ebenso werden sich die Zustände der untersuchten Umgebung nicht wesentlich ändern, wenn der Agent Aktionen ausführt. Das bedeutet, dass die aufeinanderfolgenden Zustände stark miteinander verflochten sein werden. Unser Agent wird die Autokorrelation solcher Zustände beobachten.

Die Schwierigkeit besteht darin, dass selbst die Verwendung eines kleinen Trainingskoeffizienten den Agenten nicht daran hindert, seine Handlungsnutzenfunktion an den aktuellen Zustand anzupassen und dabei die Erinnerung an frühere Erfahrungen zu opfern.

Beim überwachten Lernen ermöglicht es die Verwendung unabhängiger Zustände nach einer großen Anzahl von Iterationen, die Gewichtungswerte des Modells zu mitteln. Im Falle des Verstärkungslernens wird das Modell, wenn wir es mit verbundenen und praktisch unveränderten Zuständen trainieren, auf den aktuellen Zustand umtrainiert.

Wie in jeder Zeitreihe nimmt das Verhältnis zwischen den Zuständen mit zunehmender Zeit ab. Um dieses Problem zu lösen, müssen wir beim Training unseres Agentenmodells Zustände verwenden, die über die Zeitachse verstreut sind. Dies lässt sich leicht bewerkstelligen, wenn wir über historische Daten verfügen. Aber wenn er sich in der Umgebung bewegt, hat unser Agent kein solches Gedächtnis. Er sieht nur den aktuellen Zustand und kann nicht von einem Zustand zum anderen springen.

Warum organisieren wir also nicht ein Gedächtnis für den Agenten? Um den Wert der Aktionsnutzenfunktion zu aktualisieren, benötigen wir den folgenden Datensatz:

Zustand -> Aktion -> Belohnung -> Zustand

Wir sollten dafür sorgen, dass der Agent während seiner Bewegung in der Umgebung den erforderlichen Datensatz in einem Puffer speichert. Die Puffergröße ist ein Hyperparameter und wird durch die Modellarchitektur bestimmt. Wenn der Puffer voll ist, verdrängen neu ankommende Daten die älteren. Um das Modell zu trainieren, verwenden wir nicht den aktuellen Zustand, sondern zufällig ausgewählte Daten aus dem Speicher des Agenten. Auf diese Weise minimieren wir die Beziehung zwischen einzelnen Zuständen und erhöhen die Fähigkeit des Modells, analysierte Daten zu verallgemeinern.


2.2. Zielnetz verwenden

Ein weiterer Punkt, auf den man beim Lernen der Handlungsnutzenfunktion achten sollte, ist der Maximalwert dieser Funktion beim nächsten Schritt maxQ(st+1|at+1). Bitte beachten Sie, dass es sich hierbei um einen „Wert aus der Zukunft“ handelt. Wir nehmen also einen prognostizierten Wert auf der Grundlage der approximierten Handlungsnutzenfunktion. Da wir uns aber zum Zeitpunkt t befinden, können wir den Zustandswert zum Zeitpunkt t+1 nicht ändern. Jedes Mal, wenn wir den Funktionswert aktualisieren, aktualisieren wir auch die Gewichte des Modells und ändern damit den nächsten vorhergesagten Wert.

Außerdem trainieren wir unseren Agenten, um die maximale Belohnung zu erhalten. Bei jeder Iteration der Modellaktualisierung maximieren wir also den erwarteten Wert. Durch die Verwendung des vorhergesagten Wertes wird der aktualisierte Wert rekursiv maximiert. Auf diese Weise maximieren wir die Werte unserer Handlungsnutzenfunktion in einer Progression. Dies führt zu einer Überschätzung unserer Funktionswerte und zu einer Fehlerzunahme bei der Vorhersage des Handlungsnutzens. Das ist nicht sehr gut. Daher brauchen wir einen stationären Mechanismus zur Bewertung des Nutzens einer künftigen Maßnahme.

Wir könnten dieses Problem angehen, indem wir ein zusätzliches Modell zur Vorhersage des Nutzens einer zukünftigen Aktion erstellen. Dieser Ansatz würde jedoch zusätzliche Kosten für das Training des zweiten Modells erfordern. Dies möchten wir vermeiden. Andererseits trainieren wir bereits ein Modell, das diese Funktion ausführt. Aber nachdem die Gewichte geändert wurden, sollte das Modell die Werte der Funktion wie vor der Aktualisierung zurückgeben. Dieses umstrittene Problem kann durch Kopieren des Modells gelöst werden. Wir erstellen einfach zwei Instanzen desselben Aktionsnutzenfunktionsmodells. Eine Instanz wird trainiert, die andere wird zur Vorhersage des Nutzens der zukünftigen Handlung verwendet.

Ist das Modell der Handlungsnutzenfunktion erst einmal festgelegt, wird es für den Lernprozess bald irrelevant. Dies kann die Weiterbildung ineffizient machen. Um den Einfluss dieses Faktors zu eliminieren, müssen wir das Modell der Vorhersagewerte im Lernprozess aktualisieren. Die zweite Instanz wird nicht parallel trainiert. Stattdessen kopieren wir die Gewichte aus der trainierten Instanz des Aktionsnutzenfunktionsmodells mit einer gewissen Periodizität in die zweite Instanz. Indem wir also nur ein Modell trainieren, erhalten wir ganz aktuell 2 Instanzen des Handlungsnutzenfunktionsmodells und vermeiden die rekursive Überschätzung der Vorhersagewerte. 

Fassen wir das Ganze zusammen.

  1. Um den Agenten zu trainieren, verwenden wir ein neuronales Netz.
  2. Das neuronale Netz wird darauf trainiert, den Erwartungswert der Q-Funktion des Handlungsnutzens vorherzusagen.
  3. Um die Korrelation zwischen benachbarten Zuständen zu minimieren, verwendet der Lernprozess einen Speicherpuffer, aus dem die Zustände nach dem Zufallsprinzip extrahiert werden.
  4. Um den zukünftigen Wert der Q-Funktion vorherzusagen, gibt es das zweite Zielnetzmodell, das eine „eingefrorene“ Kopie des trainierten Modells ist.
  5. Das Zielnetz wird durch periodisches Kopieren von Gewichtsmatrizen aus dem trainierten Modell aktualisiert.

Lassen Sie uns nun die Umsetzung der beschriebenen Ansätze betrachten.


3. Implementierung mittels MQL5

Um den tiefen Q-Learning-Algorithmus mit MQL5 zu implementieren, wird die EA-Datei „Q-learning.mq5“ verwendet. Den vollständigen Code des Expert Advisors finden Sie im Anhang. Wir konzentrieren uns hier nur auf die Implementierung der tiefen Q-Learning-Methode.

Bevor wir mit der Implementierung fortfahren, sollten wir entscheiden, wie die Ausgangsdaten und das Belohnungssystem aussehen sollen. Die Ausgangsdaten sind dieselben, die wir für frühere Experimente verwendet haben. Wie sieht es also mit dem Belohnungssystem aus? Das Fraktal-Prognoseproblem, das wir zuvor betrachtet haben, ist eher künstlich. Natürlich könnten wir ein Modell erstellen, um die maximal mögliche Anzahl von Fraktalen zu bestimmen. Aber unser Hauptziel ist es, den maximalen Gewinn aus den Handelsgeschäften zu erzielen.

In diesem Zusammenhang ist es durchaus sinnvoll, die Größe der nächsten Kerze als Belohnungsgröße zu verwenden. Natürlich muss das Vorzeichen der Belohnung mit der durchgeführten Operation übereinstimmen. In einem vereinfachten Modell haben wir zwei Handelsoperationen: Kauf und Verkauf. Wir können auch nicht in der richtigen Position sein.

Hier werden wir das Modell nicht durch die Bestimmung des Positionsvolumens, der Positionserhöhung oder der teilweisen Schließung verkomplizieren. Gehen wir davon aus, dass der Vermittler in einer Position mit einem festen Los sein kann. Außerdem kann der Makler alle Positionen schließen und sich vom Markt fernhalten.

Was die Belohnungspolitik anbelangt, so müssen wir uns darüber im Klaren sein, dass das Ausbildungsergebnis weitgehend von einem gut vorbereiteten Belohnungssystem abhängt. In der Praxis des Verstärkungslernens gibt es eine Fülle von Beispielen, bei denen eine falsch gewählte Belohnungspolitik zu unerwarteten Ergebnissen führte. Das Modell kann lernen, falsche Schlüsse zu ziehen. Sie kann auch in dem Versuch stecken bleiben, die maximale Belohnung zu erhalten, ohne das gewünschte Ergebnis zu erzielen. Zum Beispiel können wir das Modell für das Öffnen und Schließen von Positionen belohnen. Übersteigt diese Belohnung jedoch den aus dem Handel erzielten Gewinn, kann das Modell lernen, Positionen einfach zu öffnen und zu schließen. So wird das Modell die Gewinne maximieren, während wir die Verluste maximieren werden.

Wenn wir andererseits das Modell für das Eröffnen und Schließen einer Position bestrafen, ähnlich der Provision für eine Operation, kann das Modell einfach lernen, sich vom Markt fernzuhalten. Kein Gewinn, aber auch kein Verlust.

In Anbetracht all dieser Überlegungen habe ich beschlossen, ein Modell mit drei möglichen Aktionen zu erstellen: Kaufen, verkaufen, aus dem Markt.

Der Agent sagt die Richtung der erwarteten Bewegung bei jeder neuen Kerze voraus und wählt eine Aktion, ohne die vorherigen zu berücksichtigen. Um das Modell zu vereinfachen, werden wir dem Agenten keine Informationen darüber geben, ob er sich in einer Position befindet oder in welche Richtung er geht. Dementsprechend verfolgt der Agent das Öffnen und Schließen von Positionen nicht. Für das Öffnen und Schließen von Positionen wird keine Belohnung gezahlt.

Um die Zeit zu minimieren, in der wir nicht auf dem Markt sind, werden wir das Fehlen einer Position bestrafen. Diese Strafe ist jedoch geringer als die Strafe für eine Verlustposition.

Hier ist also die Belohnungspolitik für Agenten:

  1. Eine gewinnbringende Position erhält eine Belohnung, die der Größe des Kerzenkörpers entspricht (analysieren Sie den Systemzustand bei jeder Kerze; wir halten eine Position von der Eröffnung bis zum Ende der Kerze).
  2. Der Zustand „aus dem Markt“ wird durch die Größe des Kerzenkörpers bestraft (die Größe des Kerzenkörpers mit einem negativen Vorzeichen, um entgangene Gewinne anzuzeigen).
  3. Eine Verlustposition wird durch die Größe des doppelten Kerzenkörpers (Verlust + verlorener Gewinn) bestraft.

Nachdem wir nun das Belohnungssystem definiert haben, können wir direkt mit der Implementierung der Methode fortfahren.

Wie bereits erwähnt, wird unser Modell zwei neuronale Netze verwenden. Zu diesem Zweck müssen wir zwei Objekte für die Arbeit mit neuronalen Netzen erstellen. Wir trainieren StudyNet, während TargetNet verwendet wird, um zukünftige Werte der Q-Funktion vorherzusagen.

CNet                StudyNet;
CNet                TargetNet;

Um die Arbeit der Deep-Q-Learning-Methode zu organisieren, benötigen wir auch neue externe Variablen, die die Hyperparameter für den Aufbau und das Training des Modells bestimmen.

  • Batch — die Größe des Aktualisierungspakets für den Gewichtungsfaktor;
  • UpdateTarget — die Anzahl der Aktualisierungen der Gewichtungsmatrix des trainierten Modells vor dem Kopieren in das „eingefrorene“ Modell, das zukünftige Q-Funktionswerte vorhersagt
  • Iterations — die Gesamtzahl der Iterationen der Aktualisierungen des trainierten Modells während des Trainings
  • DiscountFactor — Diskontfaktor für zukünftige Belohnungen
input int                  Batch =  100;
input int                  UpdateTarget = 20;
input int                  Iterations = 1000;
input double               DiscountFactor =   0.9;

Die Erstellung des neuronalen Netzmodells wird außerhalb dieses EA durchgeführt. Um sie zu erstellen, werden wir ein Tool aus den Artikeln zum Thema Transfer Learning verwenden. Dieser Ansatz ermöglicht es uns, Experimente mit verschiedenen Architekturen durchzuführen, ohne den EA zu verändern. Daher sollte in der EA-Initialisierungsmethode nur das Laden eines zuvor erstellten Modells implementiert werden.

//---
   float temp1, temp2;
   if(!StudyNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false) ||
      !TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
      return INIT_FAILED;

Bitte beachten Sie, dass wir zwei Instanzen desselben Modells verwenden und beide Modelle aus derselben Datei geladen werden.

Die Möglichkeit, verschiedene architektonische Lösungen zu verwenden, impliziert nicht nur die Verwendung verschiedener Architekturen von verborgenen Schichten und deren Größe, sondern auch die Möglichkeit, die Tiefe der analysierten Geschichte anzupassen. Früher haben wir das Modell im EA-Code erstellt, und die Tiefe der Geschichte wurde durch einen externen Parameter bestimmt. Jetzt können wir die Tiefe der analysierten Historie anhand der Größe der Quelldatenschicht bestimmen. Der EA bestimmt sie analytisch, basierend auf der Größe der Quelldatenschicht. Nur die Anzahl der Neuronen pro Kerze der analysierten Historie und die Größe der Ergebnisschicht bleiben unverändert. Denn diese Parameter stehen in einem strukturellen Zusammenhang mit den verwendeten Indikatoren und der Anzahl der vorhersehbaren Maßnahmen.

   if(!StudyNet.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   StudyNet.getResults(TempData);
   if(TempData.Total() != Actions)
      return INIT_PARAMETERS_INCORRECT;

Wir haben bisher noch nicht über die Größe der ursprünglichen Schicht im tiefen Q-Learning-Modell gesprochen. Wie bereits erwähnt, liefert die Q-Funktion die erwartete Belohnung in Abhängigkeit vom Zustand und der durchgeführten Aktion. Um die sinnvollste Aktion zu bestimmen, müssen wir den Funktionswert für alle möglichen Aktionen im aktuellen Zustand berechnen. Die Verwendung eines neuronalen Netzes ermöglicht die Erstellung einer Ergebnisschicht, bei der die Anzahl der Neuronen gleich der Anzahl aller möglichen Aktionen ist. In diesem Fall ist jedes Neuron der Ergebnisschicht für die Vorhersage des Nutzens einer bestimmten Handlung verantwortlich. Dadurch werden die Nutzenwerte aller Aktionen in einem Durchgang des neuronalen Netzes ermittelt. Dann müssen wir nur noch den Maximalwert wählen.

Der Rest der EA-Initialisierungsfunktion bleibt unverändert. Der gesamte EA-Code ist im Anhang zu finden.

Der Modelltrainingsvorgang wird in der Funktion Train. Zu Beginn des Funktionskörpers bestimmen wir die Größe der Trainingseinheit und laden Sie die historischen Daten. Dies ist vergleichbar mit den früher betrachteten Verfahren in überwachten und unüberwachten Lernalgorithmen.

void Train(void)
  {
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);
//---
   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      ExpertRemove();
      return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
      ExpertRemove();
      return;
     }
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();

Da wir zum Trainieren des Modells historische Daten verwenden, ist es nicht erforderlich, einen Speicherpuffer zu erstellen. Wir können einfach alle historischen Daten als einen einzigen Speicherpuffer verwenden. Wenn ein Modell jedoch in Echtzeit trainiert wird, müssen wir einen Speicherpuffer hinzufügen und diesen verwalten.

Als Nächstes bereiten wir die Hilfsvariablen vor:

  • total — Die Größe der Trainingsstichprobe,
  • use_target — das Verwendungsflag für Target Net zur Vorhersage zukünftiger Belohnungen

   int total = bars - (int)HistoryBars - 240;
   bool use_target = false;

Wir verwenden das Flag use_target, weil wir die Vorhersage zukünftiger Belohnungen bis zur ersten Aktualisierung des Target Net (Zielnetzmodell) deaktivieren müssen. Das ist eigentlich ein sehr subtiler Punkt. Im ersten Schritt wird das Modell mit Zufallsgewichten initialisiert. Daher werden alle vorhergesagten Werte zufällig sein. Höchstwahrscheinlich werden sie sehr weit von den wahren Werten entfernt sein. Die Verwendung solcher Zufallswerte kann den Modelllernprozess verzerren. In diesem Fall approximiert das Modell nicht die wahren Werte der Belohnungen, sondern den im Modell selbst enthaltenen Zufallswerten. Daher sollten wir dieses Rauschen vor der ersten Iteration der Aktualisierung des Zielnetzes Target Net beseitigen.

Als Nächstes implementieren wir ein System von Agententrainingsschleifen. Die äußere Schleife zählt die Gesamtzahl der Iterationen zur Aktualisierung der Gewichtsmatrix unseres Agenten.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter += UpdateTarget)
     {
      int i = 0;

In einer verschachtelten Schleife zählen wir die Größe der Gewichtungsaktualisierung und die Anzahl der Aktualisierungen vor der Aktualisierung von Target Net. Hier ist anzumerken, dass die Gewichte in unserem Modell bei jeder Iteration des Backpropagation-Durchgangs aktualisiert werden. Daher sieht die Verwendung des Aktualisierungsstapels nicht ganz korrekt aus, da er bei unserem Modell immer auf 1 gesetzt ist. Um jedoch die Anzahl der verarbeiteten Zustände zwischen den Aktualisierungen von Target Net auszugleichen, ist ihre Häufigkeit gleich dem Produkt aus der Paketgröße und der Anzahl der Aktualisierungen zwischen den Aktualisierungen.

Im Schleifenkörper wird der Zustand des Systems für die aktuelle Modelltrainingsiteration zufällig bestimmt. Wir löschen auch die Puffer, um zwei nachfolgende Zustände zu schreiben. Der erste Zustand wird für den Feedforward-Durchlauf des trainierten Modells verwendet. Die zweite wird für die Vorhersage von Q-Funktionswerten in Target Net verwendet.

      for(int batch = 0; batch < Batch * UpdateTarget; batch++)
        {
         i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
         State1.Clear();
         State2.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;

Dann füllen wir in einer verschachtelten Schleife die vorbereiteten Puffer mit historischen Daten. Um unnötige Operationen zu vermeiden, sollten wir vor dem Füllen des zweiten Zustandspuffers die Verwendung des Flags Target Net überprüfen. Der Puffer wird nur bei Bedarf gefüllt.

         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State1.Add((float)Rates[bar_t].close - open) || !State1.Add((float)Rates[bar_t].high - open) ||
               !State1.Add((float)Rates[bar_t].low - open) || !State1.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State1.Add(sTime.hour) || !State1.Add(sTime.day_of_week) || !State1.Add(sTime.mon) ||
               !State1.Add(rsi) || !State1.Add(cci) || !State1.Add(atr) || !State1.Add(macd) || !State1.Add(sign))
               break;
            if(!use_target)
               continue;
            //---
            bar_t --;
            open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            rsi = (float)RSI.Main(bar_t);
            cci = (float)CCI.Main(bar_t);
            atr = (float)ATR.Main(bar_t);
            macd = (float)MACD.Main(bar_t);
            sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State2.Add((float)Rates[bar_t].close - open) || !State2.Add((float)Rates[bar_t].high - open) ||
               !State2.Add((float)Rates[bar_t].low - open) || !State2.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State2.Add(sTime.hour) || !State2.Add(sTime.day_of_week) || !State2.Add(sTime.mon) ||
               !State2.Add(rsi) || !State2.Add(cci) || !State2.Add(atr) || !State2.Add(macd) || !State2.Add(sign))
               break;
           }

Nachdem wir die Puffer erfolgreich mit historischen Daten gefüllt haben, überprüfen wir deren Größe und führen einen Feedforward-Durchlauf beider Modelle durch. Vergessen wir nicht, die Ergebnisse der Ausführung der Operation zu überprüfen.

         if(IsStopped())
           {
            ExpertRemove();
            return;
           }
         if(State1.Total() < (int)HistoryBars * 12 ||
            (use_target && State2.Total() < (int)HistoryBars * 12))
            continue;
         if(!StudyNet.feedForward(GetPointer(State1), 12, true))
            return;
         if(use_target)
           {
            if(!TargetNet.feedForward(GetPointer(State2), 12, true))
               return;
            TargetNet.getResults(TempData);
           }

Nach einem erfolgreichen Feed-Forward-Durchgang erhalten wir eine Belohnung aus der Umgebung und bereiten einen Puffer von Zielen für den Backpropagation-Durchgang gemäß der oben definierten Belohnungspolitik vor.

Achten Sie bitte auf die folgenden beiden Momente. Zunächst prüfen wir die Verwendung des Flags für Target Net. Wir fügen einen prädiktiven Wert nur im Falle eines positiven Ergebnisses hinzu. Ist der Flag auf false gesetzt, sollten die Vorhersagewerte der Q-Funktion auf 0 gesetzt werden.

Der zweite Punkt ist die Abweichung von der Bellman-Gleichung. Wie Sie sich erinnern, verwendet die Bellman-Gleichung den maximalen Wert der zukünftigen Belohnung. Auf diese Weise wird das Modell so trainiert, dass es den maximalen Gewinn erzielt. Dieser Ansatz führt natürlich zu einer maximalen Rentabilität. Wenn die Kurscharts jedoch mit viel Rauschen gefüllt sind, führt dies zu einem Anstieg der Zahl der Abschlüsse beim Handel. Außerdem mindert das Rauschen die Qualität der Prognosen. Dies ist vergleichbar mit dem Versuch, jede neue Kerze vorherzusagen. Dies kann dazu führen, dass Positionen bei fast jeder neuen Kerze geöffnet und geschlossen werden, anstatt den Trend zu bestimmen und eine Position in Trendrichtung zu eröffnen.

Um den Einfluss des oben genannten Faktors zu eliminieren, habe ich beschlossen, von der Bellman-Gleichung abzuweichen. Um das Q-Funktionsmodell zu aktualisieren, werde ich unidirektionale Werte verwenden. Der Höchstbetrag wird nur für die Aktion „aus dem Markt“ verwendet.

         Rewards.Clear();
         double reward = Rates[i - 1 + 240].close - Rates[i - 1 + 240].open;
         if(reward >= 0)
           {
            if(!Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-2 * (use_target ? reward + DiscountFactor * TempData.At(1) : 0)))
               ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;
           }
         else
            if(!Rewards.Add((float)(2 * reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(1) : 0))) ||
               !Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;

Nachdem wir den Belohnungspuffer vorbereitet haben, führen wir den Backpropagation-Durchgang im trainierten Modell durch. Wir überprüfen auch hier das Ergebnis der Vorgangsausführung.

         if(!StudyNet.backProp(GetPointer(Rewards)))
            return;
        }

Damit sind die Operationen der verschachtelten Schleife abgeschlossen, die die Iterationen des Agententrainings zählt. Nach der Fertigstellung aktualisieren wir das Modell Target Net. Unsere Modelle haben keine Methoden zum Gewichtsaustausch. Ich habe beschlossen, nichts Neues zu erfinden. Stattdessen werden wir den bestehenden Mechanismus zum Speichern und Laden des Modells verwenden. In diesem Fall erhalten wir die exakte Kopie des Modells mit all seinen Inhalten.

Speichern wir also das trainierte Modell in einer Datei, und laden das gespeicherte Modell aus der Datei in TargetNet. Vergessen wir nicht, die Ergebnisse der Ausführung der Operation zu überprüfen.

      if(!StudyNet.Save(FileName + ".nnw", StudyNet.getRecentAverageError(), 0, 0, Rates[i].time, false))
         return;
      float temp1, temp2;
      if(!TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
         return;
      use_target = true;
      PrintFormat("Iteration %d, loss %.5f", iter, StudyNet.getRecentAverageError());
     }

Nachdem das TargetNet-Modell erfolgreich aktualisiert wurde, ändern wir sein Verwendungsflag, geben eine Informationsmeldung in das Protokoll ein und fahren mit der nächsten Iteration der äußeren Schleife fort.

Sobald der Trainingsprozess abgeschlossen ist, löschen wir die Kommentare und leiten die Schließung des Modelltrainings EA ein.

   Comment("");
//---
   ExpertRemove();
  }

Den vollständigen Code des Expert Advisors finden Sie im Anhang.


4. Tests

Die Methode wurde an den EURUSD-Daten mit dem H1-Zeitrahmen für die letzten 2 Jahre getestet. Bei allen früheren Versuchen wurden die gleichen Daten verwendet. Die Indikatoren wurden mit Standardparametern verwendet.

Zu Testzwecken wurde ein Faltungsmodell (convolutional model) mit der folgenden Architektur erstellt:

  1. Ursprüngliche Datenschicht, 240 Elemente (20 Kerzen, 12 Neuronen pro Beschreibung einer Kerze).
  2. Faltungsschicht, Eingangsdatenfenster 24 (2 Kerzen), Stufe 12 (1 Kerze), 6 Filter am Ausgang.
  3. Faltungsschicht, Eingangsdatenfenster 2, Stufe 1, 2 Filter.
  4. Faltungsschicht, Eingangsdatenfenster 3, Stufe 1, 2 Filter.
  5. Faltungsschicht, Eingangsdatenfenster 3, Stufe 1, 2 Filter.
  6. Vollständig verbundene neuronale Schicht mit 1000 Elementen.
  7. Vollständig verbundene neuronale Schicht mit 1000 Elementen.
  8. Vollständig verbundene Schicht aus 3 Elementen (Ergebnisschicht für 3 Aktionen).

Die Schichten von 2 bis 7 wurden durch die Funktion sigmoid aktiviert. Für die Ergebnisschicht wurde der hyperbolische Tangens als Aktivierungsfunktion verwendet.

Die folgende Abbildung zeigt das Diagramm der Fehlerdynamik. Wie Sie aus dem Diagramm ersehen können, hat sich der Fehler bei der Vorhersage der erwarteten Belohnung während des Lernprozesses schnell verringert. Nach 500 Iterationen lag er nahe bei 0. Der Modellbildungsprozess mit 1000 Iterationen endete mit einem Fehler von 0,00105.

DQN-Modellprüfungsdiagramm


Schlussfolgerung

In diesem Artikel haben wir uns weiter mit Methoden des Verstärkungslernens beschäftigt. Wir haben uns die Deep-Q-Learning-Methode angesehen, die 2013 vom DeepMind-Team eingeführt wurde. Die Veröffentlichung dieser Arbeit leitete eine neue Phase in der Entwicklung von Methoden des Reinforcement Learning ein. Diese Methode zeigte die Möglichkeit auf, Modelle zu trainieren, um Strategien zu entwickeln. Die Verwendung eines einzigen Modells ermöglicht es außerdem, es für die Lösung verschiedener Probleme zu trainieren, ohne strukturelle Änderungen an seiner Architektur oder seinen Hyperparametern vorzunehmen. Dies waren die ersten Experimente, bei denen der trainierte Algorithmus die EA-Ergebnisse übertraf.

Wir haben die Implementierung der Methode mit MQL5 gesehen. Die Ergebnisse der Modelltests zeigen, dass die Methode zur Erstellung funktionierender Handelsmodelle verwendet werden kann.


Liste der Referenzen

  1. Playing Atari with Deep Reinforcement Learning
  2. Neuronale Netze leicht gemacht (Teil 25): Praxis des Transfer-Learnings
  3. Neuronale Netze leicht gemacht (Teil 26): Reinforcement-Learning

Programme, die im diesem Artikel verwendet werden

# Name Typ Beschreibung
1 Q-learning.mq5 EA Expert Advisor zum Trainieren des Modells 
2 NeuroNet.mqh Klassenbibliothek Bibliothek zur Erstellung neuronaler Netzmodelle
3 NeuroNet.cl Code Base
OpenCL-Programmcode-Bibliothek zur Erstellung neuronaler Netzwerkmodelle


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/11369

Beigefügte Dateien |
MQL5.zip (66.7 KB)
DoEasy. Steuerung (Teil 19): Verschieben der Registerkarten in TabControl, Ereignisse im WinForms-Objekt DoEasy. Steuerung (Teil 19): Verschieben der Registerkarten in TabControl, Ereignisse im WinForms-Objekt
In diesem Artikel werde ich die Funktionsweise zum Verschieben (scrolling) von Registerkartenüberschriften in TabControl mithilfe von Scroll-Schaltflächen erstellen. Die Funktionalität ist dazu gedacht, Tabulator-Kopfzeilen in einer einzigen Zeile auf beiden Seiten des Steuerelements zu platzieren.
DoEasy. Steuerung (Teil 18): Funktionsweise für scrollende Registerkarten in TabControl DoEasy. Steuerung (Teil 18): Funktionsweise für scrollende Registerkarten in TabControl
In diesem Artikel werde ich die Schaltflächen der Kopfzeilen-Scroll-Steuerung im TabControl WinForms-Objekt platzieren, für den Fall, dass die Kopfzeile nicht in die Größe des Steuerelements passt. Außerdem werde ich die Verschiebung der Kopfleiste beim Klicken auf die abgeschnittene Registerkartenüberschrift implementieren.
Datenwissenschaft und maschinelles Lernen (Teil 07): Polynome Regression Datenwissenschaft und maschinelles Lernen (Teil 07): Polynome Regression
Im Gegensatz zur linearen Regression ist die polynome Regression ein flexibles Modell, das darauf abzielt, Aufgaben besser zu erfüllen, die das lineare Regressionsmodell nicht bewältigen kann. Lassen Sie uns herausfinden, wie man polynome Modelle in MQL5 erstellt und etwas Positives daraus macht.
DoEasy. Steuerung (Teil 17): Beschneiden unsichtbarer Objektteile, Hilfspfeiltasten WinForms-Objekte DoEasy. Steuerung (Teil 17): Beschneiden unsichtbarer Objektteile, Hilfspfeiltasten WinForms-Objekte
In diesem Artikel werde ich die Funktionalität zum Ausblenden von Objektabschnitten, die sich außerhalb ihrer Container befinden, erstellen. Außerdem werde ich zusätzliche Pfeiltastenobjekte erstellen, die als Teil anderer WinForms-Objekte verwendet werden können.