English Русский 中文 Español 日本語 Português
preview
Neuronale Netze leicht gemacht (Teil 71): Zielkonditionierte prädiktive Kodierung (Goal-Conditioned Predictive Coding, GCPC)

Neuronale Netze leicht gemacht (Teil 71): Zielkonditionierte prädiktive Kodierung (Goal-Conditioned Predictive Coding, GCPC)

MetaTrader 5Handelssysteme | 12 Juni 2024, 13:46
125 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Simuliertes Lernen (Behavioural Cloning (BC)) ist ein vielversprechender Ansatz für die Lösung verschiedener Offline-Verstärkungslernprobleme. Anstatt den Wert von Zuständen und Aktionen zu bewerten, trainiert BC direkt die Verhaltenspolitik des Agenten, indem es Abhängigkeiten zwischen dem gesetzten Ziel, dem analysierten Umgebungszustand und der Aktion des Agenten herstellt. Dies wird durch überwachte Lernmethoden auf zuvor gesammelten Offline-Trajektorien erreicht. Die bekannte Decision-Transformer-Methode und ihre abgeleiteten Algorithmen haben die Wirksamkeit der Sequenzmodellierung für das Offline-Verstärkungslernen bewiesen.

Bei der Verwendung der oben genannten Algorithmen haben wir zuvor mit verschiedenen Optionen für die Festlegung von Zielen experimentiert, um die von uns benötigten Agentenaktionen anzuregen. Wie das Modell jedoch die zuvor durchlaufene Trajektorie lernt, blieb außerhalb unserer Aufmerksamkeit. Es stellt sich nun die Frage, ob es sinnvoll ist, die Trajektorie als Ganzes zu untersuchen. Diese Frage wurde von den Autoren des Artikels „Goal-Conditioned Predictive Coding for Offline Reinforcement Learning“ behandelt. In ihrem Artikel gehen sie mehreren Schlüsselfragen nach:

  1. Sind Offline-Trajektorien für die Sequenzmodellierung nützlich oder liefern sie einfach mehr Daten für überwachtes Policy Learning?

  2. Welches wäre das effektivste Lernziel für die Darstellung der Trajektorie zur Unterstützung des politischen Lernens? Sollten Sequenzmodelle so trainiert werden, dass sie historische Erfahrungen, zukünftige Dynamiken oder beides kodieren?

  3. Da dasselbe Sequenzmodell sowohl für das Lernen der Trajektorienrepräsentation als auch für das Lernen von Strategien verwendet werden kann, sollten wir dieselben Lernziele haben oder nicht?

In dem Artikel werden die Ergebnisse von Experimenten in 3 künstlichen Umgebungen vorgestellt, aus denen die Autoren die folgenden Schlussfolgerungen ziehen können:

  • Die Modellierung von Abläufen kann, wenn sie richtig konzipiert ist, die Entscheidungsfindung wirksam unterstützen, wenn die daraus resultierende Darstellung der Trajektorie als Input für das Lernen von Strategien verwendet wird.

  • Es besteht eine Diskrepanz zwischen dem Lernziel der optimalen Trajektoriendarstellung und dem Lernziel der Strategie.

Basierend auf diesen Beobachtungen haben die Autoren der Arbeit ein zweistufiges Verfahren entwickelt, das die Trajektorieninformationen in kompakte, komprimierte Darstellungen komprimiert, indem es ein Sequenzmodellierungstraining verwendet. Die komprimierte Darstellung wird dann verwendet, um das Agentenverhalten mit Hilfe eines einfachen MLP-Modells (Multilayer Perceptron) zu trainieren. Die von ihnen vorgeschlagene Methode des Goal-Conditioned Predictive Coding (GCPC) ist die effektivste Methode zum Erlernen der Trajektoriendarstellung. Es bietet eine konkurrenzfähige Leistung in allen ihren Benchmark-Tests. Die Autoren heben besonders die Wirksamkeit bei der Lösung von Aufgaben mit langem Zeithorizont hervor. Die starke empirische Leistung von GCPC beruht auf der latenten Darstellung vergangener und vorhergesagter Zustände. In diesem Fall erfolgt die Zustandsvorhersage mit Blick auf die gesetzten Ziele, die eine entscheidende Richtschnur für die Entscheidungsfindung darstellen.

1. Algorithmus für zielgerichtete prädiktive Kodierung (Goal-Conditioned Predictive Coding)

Die Autoren der GCPC-Methode verwenden die Sequenzmodellierung für Offline-Verstärkungslernen. Um das Problem des Offline-Verstärkungslernens zu lösen, verwenden sie bedingtes, gefiltertes oder gewichtetes Imitationslernen. Es wird davon ausgegangen, dass es einen vorab gesammelten Satz von Trainingsdaten gibt. Aber die zur Datenerhebung angewandten Maßnahmen sind möglicherweise nicht bekannt. Die Trainingsdaten enthalten eine Reihe von Trajektorien. Jede Trajektorie wird als eine Menge von Zuständen und Aktionen (St, At) dargestellt. Eine Trajektorie kann optional eine Belohnung Rt, obtained at the time step t erhalten.

Da die Trajektorien mit unbekannten Strategien gesammelt werden, sind sie möglicherweise nicht optimal oder verfügen nicht über ein ausreichendes Maß an Fachwissen. Wir haben bereits erörtert, dass die richtige Verwendung von Offline-Trajektorien, die suboptimale Daten enthalten, zu effektiveren Verhaltensstrategien führen kann. Denn suboptimale Trajektorien können Teiltrajektorien enthalten, die nützliche „Fähigkeiten“ zeigen, die zur Lösung bestimmter Aufgaben kombiniert werden können.

Die Autoren der Methode sind der Meinung, dass die Verhaltenspolitik des Agenten in der Lage sein sollte, jede Form von Informationen über den Zustand oder die Trajektorie als Eingabe zu akzeptieren und die nächste Aktion vorherzusagen:

  • Wenn nur der aktuell beobachtete Zustand St und das Ziel G verwendet werden, ignoriert die Agentenpolitik die historischen Beobachtungen. 
  • Wenn die Agentenpolitik ein Sequenzmodell ist, kann sie die gesamte beobachtete Trajektorie zur Vorhersage der nächsten Aktion verwenden .

Zur Optimierung des Agentenverhaltens wird in der Regel eine Maximum-Likelihood-Zielfunktion verwendet.

Die Modellierung von Abläufen kann für die Entscheidungsfindung aus zwei Blickwinkeln genutzt werden: für die Darstellung von Lerntrajektorien und für Lernverhaltensstrategien. Die erste Richtung zielt darauf ab, nützliche Repräsentationen von rohen Eingabe-Trajektorien in Form einer kondensierten latenten Repräsentation oder der vortrainierten Netzwerkgewichte zu erhalten. Die zweite Richtung zielt darauf ab, die Beobachtung und das Ziel in eine optimale Handlung zur Erfüllung der Aufgabe umzuwandeln.

Das Lernen der Trajektorfunktion und der Politikfunktion kann mit Hilfe von Transformer-Modellen durchgeführt werden. Die Autoren der GCPC-Methode schlagen vor, dass es für die Trajektorienfunktion nützlich sein kann, die Originaldaten mit Hilfe von Sequenzmodellierungstechniken in eine komprimierte Darstellung zu bringen. Es ist auch wünschenswert, das Lernen der Trajektorienrepräsentation vom Erlernen der Politik zu entkoppeln. Die Entkopplung bietet nicht nur Flexibilität bei der Wahl der Ziele des Repräsentationslernens, sondern ermöglicht es uns auch, die Auswirkungen der Sequenzmodellierung auf das Lernen der Trajektorienrepräsentation und das Lernen von Strategien unabhängig voneinander zu untersuchen. Daher verwendet GCPC eine zweistufige Struktur mit TrajNet (Trajektorienmodell) und PolicyNet (Politikmodell). Um TrajNet zu trainieren, werden unüberwachte Lernmethoden wie maskierte Autoencoder oder Next-Token-Prediction für die Sequenzmodellierung verwendet. PolicyNet zielt darauf ab, mit Hilfe einer überwachten Lernzielfunktion aus gesammelten Offline-Trajektorien effektive Strategien abzuleiten.

In der ersten Phase des Trainings der Trajektorienrepräsentation wird die maskierte automatische Kodierung verwendet. TrajNet erhält die Trajektorie und gegebenenfalls das Ziel G und lernt, τ aus einer maskierten Ansicht derselben Trajektorie wiederherzustellen. Optional erzeugt TrajNet auch eine komprimierte Darstellung der Trajektorie B, die von PolicyNet für das anschließende Policy-Training verwendet werden kann. In ihrem Beitrag schlagen die Autoren der GCPC-Methode vor, eine maskierte Darstellung der durchlaufenen Trajektorie als Eingabe für das Autoencoder-Modell zu verwenden. Am Ausgang des Decoders bemühen sie sich um eine unmaskierte Darstellung der durchlaufenen Trajektorie und der nachfolgenden Zustände.

In der zweiten Phase wird TrajNet auf die unmaskierte beobachtete Trajektorie angewendet, um eine verdichtete Darstellung der Trajektorie B zu erhalten. PolicyNet prognostiziert dann die Aktion A anhand der beobachteten Trajektorie (oder des aktuellen Zustands der Umgebung), des Ziels G und der kondensierten Trajektoriendarstellung B.

Der vorgeschlagene Rahmen bietet eine einheitliche Sichtweise für den Vergleich verschiedener Konzepte zur Umsetzung von Repräsentationslernen und Policy Learning. Viele bestehende Methoden können als Spezialfälle der vorgeschlagenen Struktur betrachtet werden. Bei der DT-Implementierung beispielsweise wird die Funktion zur Darstellung der Trajektorie als Identitätsabbildungsfunktion der Eingabetrajektorie festgelegt, und die Strategie wird so trainiert, dass sie autoregressiv Aktionen erzeugt.

Im Folgenden wird die Methode der Visualisierung der Autoren vorgestellt.

2. Implementierung mit MQL5

Wir haben uns mit den theoretischen Aspekten der Methode des Zielkonditionierte prädiktive Kodierung beschäftigt. Als Nächstes wollen wir uns mit der Implementierung von MQL5 befassen. Hier sollten Sie vor allem auf die unterschiedliche Anzahl von Modellen achten, die in verschiedenen Phasen des Trainings und des Betriebs des Modells verwendet werden.

2.1 Modellarchitektur

In der ersten Phase schlagen die Autoren der Methode vor, ein Modell zur Darstellung der Trajektorie zu trainieren. Die Modellarchitektur verwendet einen Transformator. Um ihn zu trainieren, müssen wir einen Autoencoder erstellen. In der zweiten Stufe wird nur der trainierte Encoder verwendet. Um nicht einen unnötigen Decoder in die zweite Stufe des Trainings zu „schleppen“, werden wir den Autoencoder in 2 Modelle aufteilen: Kodierer und Dekodierer. Die Architektur der Modelle wird in der Methode CreateTrajNetDescriptions dargestellt. In den Parametern erhält die Methode Zeiger auf 2 dynamische Arrays, die die Architektur der angegebenen Modelle angeben.

Im Hauptteil der Methode werden die empfangenen Zeiger überprüft und gegebenenfalls neue dynamische Array-Objekte erstellt.

bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *decoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!decoder)
     {
      decoder = new CArrayObj();
      if(!decoder)
         return false;
     }

Beschreiben wir zunächst die Architektur des Encoders. In das Modell fließen nur Daten über historische Kursbewegungen und analysierte Indikatoren ein.  

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Bitte beachten Sie, dass wir im Gegensatz zu den zuvor besprochenen Modellen in diesem Stadium weder Daten über den Kontostatus noch Informationen über die zuvor vom Agenten durchgeführten Aktionen verwenden. Es wird die Auffassung vertreten, dass sich in einigen Fällen Informationen über frühere Maßnahmen negativ auswirken können. Daher haben die Autoren der GCPC-Methode sie aus den Quelldaten ausgeschlossen. Informationen über den Kontostand haben keinen Einfluss auf den Zustand der Umgebung. Daher ist sie für die Vorhersage späterer Umweltzustände nicht wichtig.

Wir geben immer die unverarbeiteten Quelldaten in das Modell ein. Daher verwenden wir in der nächsten Ebene die Stapelnormalisierung, um die Quelldaten in eine vergleichbare Form zu bringen.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Nach der Vorverarbeitung der Daten müssen wir eine zufällige Datenmaskierung implementieren, die durch den GCPC-Algorithmus bereitgestellt wird. Um diese Funktionsweise zu implementieren, werden wir die DropOut-Schicht verwenden.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDropoutOCL;
   descr.count = prev_count;
   descr.probability = 0.8f;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Bitte beachten Sie, dass es in der allgemeinen Praxis nicht empfehlenswert ist, die Batch-Normalisierungsschicht und DropOut zusammen in einem Modell zu verwenden. Dies ist darauf zurückzuführen, dass der Ausschluss einiger Informationen und deren Ersetzung durch Nullwerte die ursprüngliche Datenverteilung verzerrt und sich negativ auf die Funktionsweise der Stapelnormalisierungsschicht auswirkt. Aus diesem Grund werden die Daten zunächst normalisiert und erst dann maskiert. Auf diese Weise arbeitet die Batch-Normalisierungsschicht mit dem vollständigen Datensatz und minimiert die Auswirkungen der DropOut-Schicht auf ihren Betrieb. Gleichzeitig implementieren wir eine Maskierungsfunktion, um unser Modell so zu trainieren, dass es fehlende Daten wiederherstellt und Ausreißer ignoriert, die in einer stochastischen Umgebung vorkommen.

Als Nächstes kommt im Modell unseres Encoders ein Faltungsblock, um die Dimension der Daten zu reduzieren und stabile Muster zu erkennen.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_wout = descr.window_out = BarDescr / 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Das Ergebnis der oben beschriebenen Verarbeitung der Quelldaten wird in einen Block voll vernetzter Schichten eingespeist, der es uns ermöglicht, eine Einbettung des Ausgangszustands zu erhalten.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Zusätzlich zu den historischen Daten schlagen die Autoren der GCPC-Methode vor, den Encoder mit Target Embedding und Slot Tokens (Ergebnisse früherer Encoder-Durchläufe) zu füttern. Unser globales Ziel, den größtmöglichen Gewinn zu erzielen, hat keine Auswirkungen auf die Umwelt und wir lassen sie außen vor. Stattdessen fügen wir die Ergebnisse des letzten Durchgangs unseres Encoders dem Modell mithilfe einer Verkettungsschicht hinzu.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 2 * EmbeddingSize;
   descr.window = prev_count;
   descr.step = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die weitere Datenverarbeitung erfolgt mit GPT-Modellen. Zur Umsetzung erstellen wir zunächst einen Datenstapel mit einer Einbettungsschicht.

//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronEmbeddingOCL;
   prev_count = descr.count = GPTBars;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      ArrayCopy(descr.windows, temp);
     }
   prev_wout = descr.window_out = EmbeddingSize;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Es folgt der Aufmerksamkeitsblock. Zuvor haben wir mit der DropOut-Schicht bereits einen datenarmen Prozess erstellt, sodass ich in diesem Modell die dünn besetzte Aufmerksamkeitsschicht nicht verwendet habe

//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_count * 2;
   descr.window = prev_wout;
   descr.step = 4;
   descr.window_out = 16;
   descr.layers = 4;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Am Ausgang des Encoders reduzieren wir die Datendimension mit einer vollständig verbundenen Schicht und normalisieren die Daten mit der SoftMax-Funktion.

//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = 1;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Wir geben eine komprimierte Darstellung der Trajektorie in den Decoder-Eingang ein.

//--- Decoder
   decoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die Ausgangsdaten des Decoders wurden aus dem Vorgängermodell übernommen und haben bereits eine vergleichbare Form. Das bedeutet, dass wir in diesem Fall die Schicht der Stapelnormalisierung nicht benötigen. Die daraus resultierenden Daten werden in der vollständig verknüpften Schicht verarbeitet.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (HistoryBars + PrecoderBars) * EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Dann verarbeiten wir sie in der Aufmerksamkeitsebene.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHAttentionOCL;
   prev_count = descr.count = prev_count / EmbeddingSize;
   prev_wout = descr.window = EmbeddingSize;
   descr.step = 4;
   descr.window_out = 16;
   descr.layers = 2;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die Architektur unseres Decoders ist so aufgebaut, dass wir am Ausgang des Aufmerksamkeitsblocks für jede Kerze eine Einbettung der analysierten und vorhergesagten Umweltzustände haben. Hier müssen wir den Zweck der Daten verstehen. Betrachten wir das Folgende.

Warum analysieren wir die Indikatoren? Trendindikatoren zeigen uns die Richtung des Trends an. Oszillatoren sind so konzipiert, dass sie überkaufte und überverkaufte Zonen anzeigen und damit auf mögliche Marktumkehrpunkte hinweisen. All dies ist zum gegenwärtigen Zeitpunkt wertvoll. Wäre eine solche Vorhersage mit einer gewissen Tiefe wertvoll? Meiner persönlichen Meinung nach liegt der Vorhersagewert des Indikators unter Berücksichtigung des Datenvorhersagefehlers nahe bei Null. Letztendlich erhalten wir Gewinn und Verlust aus den Preisänderungen des Instruments und nicht aus den Indikatorwerten. Daher werden wir am Ausgang des Decoders Daten zur Preisentwicklung vorhersagen.

Erinnern wir uns daran, welche Informationen über Kursbewegungen wir im Erfahrungswiedergabepuffer speichern. Die Informationen umfassen 3 Abweichungen:

  • Körper der Kerze Schlusskurs - Eröffnungskurs
  • Hoch - Eröffnungskurs
  • Tief - Eröffnungskurs

Wir werden also diese Werte vorhersagen. Zur unabhängigen Wiederherstellung von Werten aus Kerzen-Einbettungen wird die Schicht des Modellensembles verwendet.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 3;
   descr.window = prev_wout;
   descr.step = prev_count;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

Damit ist die Beschreibung der Autoencoder-Architektur für die erste Stufe des Trainings der Trajektoriendarstellung TrajNet abgeschlossen. Bevor ich mich jedoch mit dem Training von Expert Advisor befasse, schlage ich vor, die Arbeit an der Beschreibung der Architektur der Modelle abzuschließen. Werfen wir einen Blick auf die Architektur der PolicyNet-Modelle für die zweite Stufe des Trainings. Die Architektur wird in der Methode CreateDescriptions bereitgestellt.

Entgegen den Erwartungen werden wir in der zweiten Phase nicht nur ein Modell der Verhaltenspolitik des Akteurs trainieren, sondern drei Modelle.

Das erste ist ein kleines Modell des aktuellen Statusgebers. Verwechseln Sie ihn nicht mit dem Autoencoder Encoder, der in der ersten Stufe trainiert wurde. Dieses Modell kombiniert eine komprimierte Darstellung der Trajektorie aus dem Autoencoder Encoder mit Informationen über den Zustand des Kontos in einer einzigen Darstellung.

Das zweite ist das Modell der Akteurspolitik, das wir oben besprochen haben.

Und das dritte ist ein Modell der Zielsetzung, das auf der Analyse der kondensierten Trajektoriendarstellung basiert.

Wie üblich übergeben wir in den Methodenparametern Zeiger auf dynamische Arrays, die die Modellarchitektur beschreiben. Im Hauptteil der Methode wird die Relevanz der empfangenen Zeiger überprüft und gegebenenfalls werden neue Instanzen dynamischer Array-Objekte erstellt.

bool CreateDescriptions(CArrayObj *actor, CArrayObj *goal, CArrayObj *encoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!goal)
     {
      goal = new CArrayObj();
      if(!goal)
         return false;
     }
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }

Wie bereits erwähnt, geben wir eine komprimierte Darstellung der Trajektorie in den Encoder ein.

//--- State Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die empfangenen Daten werden in der Verkettungsschicht mit Informationen über den Kontostand kombiniert.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

An diesem Punkt werden die dem Encoder zugewiesenen Aufgaben als abgeschlossen betrachtet, und wir gehen zur Architektur des Akteurs (Actor) über, der die Ergebnisse der Arbeit des vorherigen Modells als Input erhält.

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Wir kombinieren die empfangenen Daten mit dem gesetzten Ziel.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = NRewards;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Verarbeitung mit den vollständig verbundenen Schichten.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Am Ausgang des Akteurs fügen wir der Politik seines Verhaltens Stochastizität hinzu.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Und nicht zuletzt das Modell der Zielerreichung. Ich denke, es ist kein Geheimnis, dass die Fähigkeit, Gewinne zu erzielen, stark von verschiedenen Aspekten des Umweltzustands abhängt. Daher habe ich aufgrund der bisherigen Erfahrungen beschlossen, ein separates Modell für die Generierung von Zielen in Abhängigkeit vom Zustand der Umgebung einzuführen.

Wir werden eine komprimierte Darstellung der beobachteten Trajektorie in die Modelleingabe einspeisen. Wir sprechen hier von Trajektorien, ohne den Kontostand zu berücksichtigen. Unsere Belohnungsfunktion ist so aufgebaut, dass sie mit relativen Werten arbeitet, ohne an eine bestimmte Einlagengröße gebunden zu sein. Daher gehen wir bei der Festlegung von Zielen nur von einer Analyse des Umfelds aus, ohne den Stand des Kontos zu berücksichtigen.

//--- Goal
   goal.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }

Die empfangenen Daten werden von 2 vollständig verbundenen Schichten analysiert.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {

      delete descr;
      return false;
     }

Bei der Modellausgabe verwenden wir eine vollständig parametrisierte Quantilfunktion. Der Vorteil dieser Lösung besteht darin, dass sie das wahrscheinlichste Ergebnis liefert und nicht den Durchschnittswert, der für eine vollständig verbundene Schicht typisch ist. Die Unterschiede in den Ergebnissen sind am deutlichsten bei Verteilungen mit 2 oder mehr Scheitelpunkten.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFQF;
   descr.count = NRewards;
   descr.window_out = 32;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

2.2 Modell der Interaktion mit der Umwelt

Wir setzen die Implementierung der Methode der zielgerichteten prädiktiven Kodierung fort. Nach der Beschreibung der Modellarchitekturen gehen wir zur Implementierung der Algorithmen über. Zunächst wird ein Expert Advisor implementiert, der mit der Umgebung interagiert und Daten für die Trainingsstichprobe sammelt. Die Autoren der Methode haben sich nicht auf die Methode zur Erhebung von Trainingsdaten konzentriert. Der Trainingsdatensatz kann auf jede verfügbare Art und Weise gesammelt werden, auch mit den bereits erwähnten Algorithmen: ExORL und Real-ORL. Es ist lediglich erforderlich, die Formate der Datenerfassung und -darstellung aufeinander abzustimmen. Zur Optimierung der vortrainierten Modelle benötigen wir jedoch einen EA, der bei der Interaktion mit der Umwelt die erlernte Verhaltenspolitik anwendet und die Ergebnisse der Interaktion in einer Trajektorie speichert. Wir implementieren diese Funktionalität in der EA ..\Experts\GCPC\Research.mq5. Die Grundprinzipien der Konstruktion des EA-Algorithmus entsprechen denen früherer Arbeiten. Allerdings hinterlässt die Anzahl der Modelle ihre Spuren. Lassen Sie uns speziell auf einige der Methoden des EA eingehen.

In diesem Expert Advisor werden wir 4 Modelle verwenden.

CNet                 Encoder;
CNet                 StateEncoder;
CNet                 Actor;
CNet                 Goal;

Vorgefertigte Modelle werden mit der Initialisierungsmethode OnInit EA geladen. Den vollständigen Code der Methode finden Sie im Anhang. Ich werde hier nur die Änderungen erwähnen.

Zuerst laden wir das AutoEncoder-Encoder-Modell. Im Falle eines Ladefehlers wird ein neues Modell mit Zufallsparametern initialisiert.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
........
........
//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateTrajNetDescriptions(encoder, decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      delete encoder;
      delete decoder;
      //---
     }

Dann laden wir die 3 verbleibenden Modelle. Falls erforderlich, werden sie auch mit zufälligen Parametern initialisiert.

   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      delete actor;
      delete goal;
      delete encoder;
      //---
     }

Wir übertragen alle Modelle in einen einzigen OpenCL-Kontext.

   StateEncoder.SetOpenCL(Actor.GetOpenCL());
   Encoder.SetOpenCL(Actor.GetOpenCL());
   Goal.SetOpenCL(Actor.GetOpenCL());

Wir müssen sicherstellen, dass der Trainingsmodus für das Encoder-Modell ausgeschaltet ist.

   Encoder.TrainMode(false);

Bitte beachten Sie, dass wir zwar nicht vorhaben, Rückwärts-Durchgangsmethoden (backpropagation) in diesem EA zu verwenden, aber wir nutzen die DropOut-Schicht im Encoder. Daher müssen wir den Trainingsmodus ändern, um die Maskierung unter Betriebsbedingungen des Modells zu deaktivieren.

Als Nächstes überprüfen wir die Konsistenz der Architektur der geladenen Modelle.

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size (%d <> %d)", EmbeddingSize, 
                                                                                                  Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                        (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
//---
   return(INIT_SUCCEEDED);
  }

Die Interaktion mit der Umgebung ist in der Methode OnTick implementiert. Zu Beginn der Methode wird geprüft, ob ein neues Bar Opening Event eingetreten ist, und es werden gegebenenfalls historische Daten geladen. Die empfangenen Informationen werden in Datenpuffer übertragen. Diese Vorgänge wurden unverändert aus früheren Implementierungen übernommen, sodass wir nicht näher auf sie eingehen werden. Betrachten wir nur die Reihenfolge der Methodenaufrufe für den Vorwärts-Durchgang (feed-forward pass) des Modells. Wie vom GCPC-Algorithmus vorgesehen, rufen wir zunächst die Vorwärts-Durchgangsmethoden des Encoders auf.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
........
........
//---
   if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(Encoder)) ||

Beachten Sie, dass das Modell sich selbst immer wieder als Datenquelle für den zweiten Informationsfluss verwendet.

Als Nächstes rufen wir die Vorwärts-Durchgangsmethoden des State Encoders und des Goal Models auf. Beide Modelle verwenden eine komprimierte Darstellung der beobachteten Trajektorie als Eingabedaten.

      !StateEncoder.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat *)GetPointer(bAccount)) ||
      !Goal.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat *)NULL) ||

Die Ergebnisse dieser Modelle fließen in den Input des Politikmodells des Akteurs ein, um eine nachfolgende Aktion zu generieren.

      !Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CNet *)GetPointer(Goal)))
      return;

Wir sollten nicht vergessen, die Ergebnisse der Operationen zu überprüfen.

Anschließend werden die Ergebnisse des Akteursmodells dekodiert und Aktionen in der Umgebung durchgeführt, gefolgt von der Speicherung der gewonnenen Erfahrungen in einer Trajektorie. Der Algorithmus für diese Operationen wird unverändert übernommen. Den vollständigen Code des EA für die Interaktion mit der Umgebung finden Sie im Anhang.

2.3 Training der Trajektorienfunktion

Nach dem Sammeln des Trainingsdatensatzes gehen wir dazu über, Modell-Trainings-EAs zu erstellen. Nach dem GCPC-Algorithmus wird in einem ersten Schritt das Trajektorienfunktionsmodell von TrajNet trainiert. Wir implementieren diese Funktionalität in den EA ...\Experts\GCPC\StudyEncoder.mq5.

Wie im theoretischen Teil dieses Artikels beschrieben, trainieren wir in der ersten Stufe ein maskiertes Autoencoder-Modell, das in unserem Fall aus zwei Modellen besteht: Kodierer und Dekodierer.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int                  Iterations     = 1e4;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
STrajectory          Buffer[];
CNet                 Encoder;
CNet                 Decoder;

Bitte beachten Sie den folgenden Moment. Der Kodierer verwendet rekursiv seine eigenen Ergebnisse aus dem vorherigen Durchgang als Ausgangsdaten für den zweiten Informationsfluss. Für einen Vorwärts-Durchgang können wir einfach einen Zeiger auf das Modell selbst verwenden. Für einen Rückwärts-Durchgang ist dieser Ansatz jedoch nicht akzeptabel. Denn der Ergebnispuffer des Modells enthält die Daten des letzten Durchgangs, nicht die des vorherigen. Dies ist für unser Modelltraining nicht akzeptabel. Daher benötigen wir einen zusätzlichen Datenpuffer, um die Ergebnisse des vorherigen Durchgangs zu speichern.

CBufferFloat         LastEncoder;

Bei der EA-Initialisierungsmethode laden wir zunächst den Trainingsdatensatz und überprüfen die Ergebnisse der Operationen. Wenn es keine Daten für das Training der Modelle gibt, sind alle nachfolgenden Operationen sinnlos.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Nach dem erfolgreichen Laden des Trainingsdatensatzes versuchen wir, die vortrainierten Modelle zu öffnen. Wenn ein Fehler auftritt, werden neue Modelle mit zufälligen Parametern initialisiert.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new models");
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateTrajNetDescriptions(encoder, decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder) || !Decoder.Create(decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      delete encoder;
      delete decoder;
      //---
     }

 Wir platzieren beide Modelle in einem einzigen OpenCL-Kontext.

   OpenCL = Encoder.GetOpenCL();
   Decoder.SetOpenCL(OpenCL);

Wir prüfen die Kompatibilität der Modellarchitekturen.

   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size count (%d <> %d)", EmbeddingSize,
                                                                                                 Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                       (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("Input size of Decoder doesn't match Encoder output (%d <> %d)", Result.Total(), EmbeddingSize);
      return INIT_FAILED;
     }

Nach erfolgreichem Bestehen des Prüfblocks initialisieren wir die Hilfspuffer im gleichen OpenCL-Kontext.

   if(!LastEncoder.BufferInit(EmbeddingSize,0) ||
      !Gradient.BufferInit(EmbeddingSize,0) ||
      !LastEncoder.BufferCreate(OpenCL) ||
      !Gradient.BufferCreate(OpenCL))
     {
      PrintFormat("Error of create buffers: %d", GetLastError());
      return INIT_FAILED;
     }

Am Ende der EA-Initialisierungsmethode erzeugen wir ein nutzerdefiniertes Ereignis für den Beginn des Lernprozesses.

   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

Der eigentliche Trainingsprozess wird mit der Methode Train durchgeführt. In dieser Methode kombinieren wir traditionell den Algorithmus des Goal-Conditioned Predictive Coding mit unseren Entwicklungen aus früheren Artikeln. Zu Beginn der Methode erstellen wir einen Vektor der Wahrscheinlichkeiten für die Verwendung von Trajektorien zum Trainieren von Modellen.

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

Es ist jedoch zu beachten, dass das Abwägen von Trajektorien in diesem Fall keine praktischen Auswirkungen hat. Beim Training des Autoencoders verwenden wir nur historische Daten der Preisbewegung und der analysierten Indikatoren. Alle unsere Trajektorien werden in einem historischen Intervall eines Instruments erfasst. Für unseren Autoencoder enthalten daher alle Trajektorien identische Daten. Dennoch werde ich diese Funktion für die Zukunft beibehalten, um die Möglichkeit zu schaffen, Modelle auf Trajektorien mit verschiedenen Zeitintervallen und Instrumenten zu trainieren.

Als Nächstes initialisieren wir lokale Variablen und Vektoren. Betrachten wir nun den Vektor der Standardabweichungen. Seine Größe ist gleich dem Vektor der Decoderergebnisse. Auf die Grundsätze ihrer Verwendung wird später noch eingegangen.

   vector<float> result, target;
   matrix<float> targets;
   STD = vector<float>::Zeros((HistoryBars + PrecoderBars) * 3);
   int std_count = 0;
   uint ticks = GetTickCount();

Nach den Vorbereitungsarbeiten führen wir ein System von Modellschulungszyklen ein. Der Encoder verwendet einen GPT-Block mit einem Stapel latenter Zustände, der auf die Reihenfolge der Quelldaten reagiert. Daher werden wir beim Trainieren von Modellen ganze Stapel von aufeinanderfolgenden Zuständen aus jeder gesampelten Trajektorie verwenden.

Im Körper der äußeren Schleife wird unter Berücksichtigung der zuvor generierten Wahrscheinlichkeiten eine Trajektorie ausgewählt und ein Anfangszustand zufällig bestimmt.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int batch = GPTBars + 50;
      int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 3 - PrecoderBars - batch));
      if(state <= 0)
        {
         iter--;
         continue;
        }

Dann werden die Modellstapel und der Puffer mit den vorherigen Encoder-Ergebnissen gelöscht.

      Encoder.Clear();
      Decoder.Clear();
      LastEncoder.BufferInit(EmbeddingSize,0);

Jetzt ist alles bereit, um die verschachtelte Lernschleife auf der ausgewählten Trajektorie zu beginnen.

      int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(int i = state; i < end; i++)
        {
         State.AssignArray(Buffer[tr].States[i].state);

Im Hauptteil der Schleife füllen wir den anfänglichen Datenpuffer aus dem Trainingsdatensatz und rufen nacheinander die Vorwärts-Durchgangsmethoden unserer Modelle auf. Zuerst der Encoder.

         if(!LastEncoder.BufferWrite() || !Encoder.feedForward((CBufferFloat*)GetPointer(State), 1, false, 
                                                               (CBufferFloat*)GetPointer(LastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Dann den Decoder.

         if(!Decoder.feedForward(GetPointer(Encoder), -1, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Nach erfolgreichem Abschluss der Vorwärts-Durchgänge unserer Modelle müssen wir einen Rückwärts-Durchgang durchführen und die Modellparameter anpassen. Zunächst müssen wir jedoch die Zielwerte für die Decoderergebnisse vorbereiten. Wie Sie sich erinnern, wollen wir am Ausgang des Decoders die rekonstruierten Werte und Ergebnisse der Preisänderungsvorhersagen für mehrere Kerzen erhalten, die in den ersten drei Elementen des Arrays angegeben sind, das den Zustand jedes Kerzenständers beschreibt. Um diese Daten zu erhalten, erstellen wir eine Matrix, in der wir in jeder Zeile Beschreibungen des Umweltzustands im gewünschten Zeitbereich speichern. Und dann nehmen wir nur die ersten 3 Spalten der resultierenden Matrix. Dies werden unsere Zielwerte sein.

         target.Assign(Buffer[tr].States[i].state);
         ulong size = target.Size();
         targets = matrix<float>::Zeros(1, size);
         targets.Row(target, 0);
         if(size > BarDescr)
            targets.Reshape(size / BarDescr, BarDescr);
         ulong shift = targets.Rows();
         targets.Resize(shift + PrecoderBars, 3);
         for(int t = 0; t < PrecoderBars; t++)
           {
            target.Assign(Buffer[tr].States[i + t].state);
            if(size > BarDescr)
              {
               matrix<float> temp(1, size);
               temp.Row(target, 0);
               temp.Reshape(size / BarDescr, BarDescr);
               temp.Resize(size / BarDescr, 3);
               target = temp.Row(temp.Rows() - 1);
              }
            targets.Row(target, shift + t);
           }
         targets.Reshape(1, targets.Rows()*targets.Cols());
         target = targets.Row(0);

Angeregt durch die Ergebnisse des vorangegangenen Artikels, in dem die Verwendung von Operatoren in geschlossener Form beschrieben wurde, beschloss ich, den Lernprozess leicht zu ändern und mehr Gewicht auf große Abweichungen zu legen. Daher ignoriere ich geringfügige Abweichungen einfach und betrachte sie als Prognosefehler. Daher berechne ich in dieser Phase die gleitende Standardabweichung der Modellergebnisse von den Zielwerten.

         Decoder.getResults(result);
         vector<float> error = target - result;
         std_count = MathMin(std_count, 999);
         STD = MathSqrt((MathPow(STD, 2) * std_count + MathPow(error, 2)) / (std_count + 1));
         std_count++;

Dabei ist zu beachten, dass wir die Abweichung für jeden Parameter einzeln kontrollieren.

Anschließend wird geprüft, ob der aktuelle Vorhersagefehler einen Schwellenwert überschreitet. Der Rückwärts-Durchgang wird nur durchgeführt, wenn der Vorhersagefehler über dem Schwellenwert liegt für mindestens einen Parameter.

         vector<float> check = MathAbs(error) - STD * STD_Multiplier;
         if(check.Max() > 0)
           {
            //---
            Result.AssignArray(CAGrad(error) + result);
            if(!Decoder.backProp(Result, (CNet *)NULL) ||
               !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient)))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               break;
              }
           }

Bitte beachten Sie, dass dieser Ansatz mehrere Nuancen aufweist. Der durchschnittliche Fehler des Modells wird nur bei der Durchführung eines Rückwärts-Durchgangs berechnet. In diesem Fall wirkt sich der aktuelle Fehler also nur dann auf den durchschnittlichen Fehler aus, wenn der Schwellenwert überschritten wird. Folglich haben die kleinen Fehler, die wir ignorieren, keinen Einfluss auf den Wert des durchschnittlichen Fehlers des Modells. Dies führt zu einer Überbewertung dieser Metrik. Dies ist unkritisch, da der Wert rein informativ ist.

Die „Kehrseite der Medaille“ ist, dass wir durch die Konzentration auf signifikante Abweichungen dem Modell helfen, die Hauptfaktoren zu identifizieren, die bestimmte Leistungswerte beeinflussen. Die Verwendung der gleitenden Standardabweichung als Richtwert für den Schwellenwert ermöglicht es uns, die Schwelle des zulässigen Fehlers während des Lernprozesses zu verringern. Dies ermöglicht eine genauere Abstimmung des Modells.

Am Ende der Iterationen der Schleife speichern wir die Ergebnisse des Encoders in einem Hilfspuffer und informieren den Nutzer über den Fortschritt des Modelltrainings.

         Encoder.getResults(result);
         LastEncoder.AssignArray(result);
         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations);
            string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Decoder", percent, Decoder.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

Nach Abschluss aller Iterationen des Trainingsschleifensystems löschen wir das Kommentarfeld auf dem Chart, zeigen Informationen über die Trainingsergebnisse im Protokoll an und fahren den EA herunter.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError());
   ExpertRemove();
//---
  }

Vergessen wir nicht, die trainierten Modelle zu speichern und den Speicher in der EA-Deinitialisierungsmethode zu löschen.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
     {
      Encoder.Save(FileName + "Enc.nnw", 0, 0, 0, TimeCurrent(), true);
      Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), 0, 0, TimeCurrent(), true);
     }
   delete Result;
   delete OpenCL;
  }

2.4 Training der Politik

Der nächste Schritt ist das Trainieren des Agentenverhaltens, das in der EA ...\Experts\GCPC\Study.mq5 implementiert ist. Hier werden wir ein Zustands-Encoder-Modell trainieren, das im Wesentlichen ein integraler Bestandteil unseres Agentenmodells ist. Wir werden auch das Zielsetzungsmodell trainieren.

Obwohl es funktionell möglich ist, den Prozess des Trainings der Agenten-Verhaltenspolitik und des Zielsetzungsmodells in zwei separate Programme aufzuteilen, habe ich mich entschieden, sie in einem EA zu kombinieren. Wie aus dem Implementierungsalgorithmus hervorgeht, sind diese beiden Prozesse eng miteinander verflochten und verwenden eine große Menge an gemeinsamen Daten. In diesem Fall wäre es kaum effizient, das Modelltraining in 2 parallele Prozesse mit einem großen Anteil an doppelten Operationen aufzuteilen.

Dieser EA verwendet, ähnlich wie der EA für die Interaktion mit der Umwelt, 4 Modelle, von denen 3 in ihm trainiert werden.

CNet                 Actor;
CNet                 StateEncoder;
CNet                 Encoder;
CNet                 Goal;

In der EA-Initialisierungsmethode OnInit laden wir, wie in dem oben beschriebenen EA, den Trainingsdatensatz.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Als Nächstes laden wir die Modelle. Zunächst versuchen wir, einen vortrainierten Encoder zu öffnen. Sie muss in der ersten Stufe des Algorithmus für die zielgerichtete prädiktive Codierung trainiert werden. Wenn dieses Modell nicht verfügbar ist, können wir nicht zur nächsten Stufe übergehen.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Cann't load Encoder model");
      return INIT_FAILED;
     }

Nach dem erfolgreichen Lesen des Encoder-Modells versuchen wir, die übrigen Modelle zu öffnen. Sie alle sind in diesem EA geschult. Wenn also ein Fehler auftritt, erstellen wir neue Modelle und initialisieren sie mit zufälligen Parametern.

   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      delete actor;
      delete goal;
      delete encoder;
      //---
     }

Anschließend verschieben wir alle Modelle in einen einzigen OpenCL-Kontext. Wir setzen auch den Trainingsmodus des Encoders auf false, um die Maskierung der Quelldaten zu deaktivieren.

Der nächste Schritt besteht darin, die Kompatibilität der Architekturen aller geladenen Modelle zu prüfen, um mögliche Fehler bei der Übertragung von Daten zwischen den Modellen zu vermeiden.

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size (%d <> %d)", EmbeddingSize, Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                               (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   StateEncoder.GetLayerOutput(0, Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("Input size of State Encoder doesn't match Bottleneck (%d <> %d)", Result.Total(), EmbeddingSize);
      return INIT_FAILED;
     }
//---
   StateEncoder.getResults(Result);
   int latent_state = Result.Total();
   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Actor doesn't match output State Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }
//---
   Goal.GetLayerOutput(0, Result);
   latent_state = Result.Total();
   Encoder.getResults(Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Goal doesn't match output Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }
//---
   Goal.getResults(Result);
   if(Result.Total() != NRewards)
     {
      PrintFormat("The scope of Goal doesn't match rewards count (%d <> %d)", Result.Total(), NRewards);
      return INIT_FAILED;
     }

Nach erfolgreicher Übergabe aller erforderlichen Steuerelemente erstellen wir Hilfspuffer im OpenCL-Kontext.

   if(!bLastEncoder.BufferInit(EmbeddingSize, 0) ||
      !bGradient.BufferInit(MathMax(EmbeddingSize, AccountDescr), 0) ||
      !bLastEncoder.BufferCreate(OpenCL) ||
      !bGradient.BufferCreate(OpenCL))
     {
      PrintFormat("Error of create buffers: %d", GetLastError());
      return INIT_FAILED;
     }

Für den Beginn des Lernprozesses erzeugen wir ein nutzerdefiniertes Ereignis.

   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

Bei der EA-Deinitialisierungsmethode speichern wir die trainierten Modelle und entfernen die verwendeten dynamischen Objekte.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
     {
      Actor.Save(FileName + "Act.nnw", 0, 0, 0, TimeCurrent(), true);
      StateEncoder.Save(FileName + "StEnc.nnw", 0, 0, 0, TimeCurrent(), true);
      Goal.Save(FileName + "Goal.nnw", 0, 0, 0, TimeCurrent(), true);
     }
   delete Result;
   delete OpenCL;
  }

Der Prozess des Trainings von Modellen ist in der Train-Methode implementiert. Im Hauptteil der Methode wird zunächst ein Puffer mit Wahrscheinlichkeiten für die Auswahl der Trajektorien zum Trainieren der Modelle erstellt. Wir gewichten alle Trajektorien in der Trainingsmenge nach ihrer Rentabilität. Bei den profitabelsten Durchgängen ist die Wahrscheinlichkeit größer, dass sie am Lernprozess teilnehmen.

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

Dann initialisieren wir die lokalen Variablen. Hier sehen wir zwei Vektoren von Standardabweichungen, die wir für Politikmodelle und Zielsetzungen verwenden werden.

   vector<float> result, target;
   matrix<float> targets;
   STD_Actor = vector<float>::Zeros(NActions);
   STD_Goal = vector<float>::Zeros(NRewards);
   int std_count = 0;
   bool Stop = false;
//---
   uint ticks = GetTickCount();

Obwohl keines der trainierten Modelle rekurrente Blöcke und Stapel in ihrer Architektur hat, erstellen wir dennoch ein Schleifensystem, um die Modelle zu trainieren. Denn die Ausgangsdaten für die trainierten Modelle werden vom Encoder erzeugt, der mit der GPT-Architektur arbeitet.

Im Körper der äußeren Schleife werden die Trajektorie und der Anfangszustand abgetastet.

   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int batch = GPTBars + 50;
      int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - PrecoderBars - batch));
      if(state <= 0)
        {
         iter--;
         continue;
        }

Wir löschen den Verarbeitungsstapel des Encoders und den Puffer mit den letzten Ergebnissen.

      Encoder.Clear();
      bLastEncoder.BufferInit(EmbeddingSize, 0);

Beachten Sie, dass wir einen Puffer verwenden, um den letzten Zustand des Encoders aufzuzeichnen, obwohl wir für dieses Modell keine Rückwärts-Durchgänge durchführen werden. Für Vorwärts-Durchgänge könnten wir einen Zeiger auf das Modell verwenden, wie es in der Umweltinteraktion EA implementiert wurde. Wenn wir jedoch zu einer neuen Trajektorie übergehen, müssen wir nicht nur den Stapel der latenten Zustände, sondern auch den Ergebnispuffer des Modells zurücksetzen. Dies ist mit einem zusätzlichen Puffer einfacher zu bewerkstelligen.

Im Hauptteil der verschachtelten Schleife werden die analysierten Zustandsdaten aus dem Trainingsdatensatz geladen und mit Hilfe des Encoder-Modells eine komprimierte Darstellung davon erzeugt.

      int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(int i = state; i < end; i++)
        {
         bState.AssignArray(Buffer[tr].States[i].state);
         //---
         if(!bLastEncoder.BufferWrite() ||
            !Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bLastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Als Nächstes füllen wir den Puffer für die Beschreibung des Kontostandes, der mit Zeitstempel-Harmonien ergänzt wird.

         float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
         bAccount.Clear();
         bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance);
         bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
         bAccount.Add(Buffer[tr].States[i].account[2]);
         bAccount.Add(Buffer[tr].States[i].account[3]);
         bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance);
         double time = (double)Buffer[tr].States[i].account[7];
         double x = time / (double)(D'2024.01.01' - D'2023.01.01');
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_MN1);
         bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_W1);
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_D1);
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         if(bAccount.GetIndex() >= 0)
            bAccount.BufferWrite();

Die kondensierte Darstellung des analysierten Zustands der Umgebung wird mit einem Vektor kombiniert, der den Zustand des Kontos beschreibt.

         //--- State embedding
         if(!StateEncoder.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat*)GetPointer(bAccount)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Und jetzt, um einen Vorwärts-Durchgang des Akteurs auszuführen, müssen wir das Ziel angeben. Wie beim Decision Transformer werden auch in dieser Phase die tatsächlichen Ergebnisse der Interaktion mit der Umwelt als Ziele verwendet. Die tatsächlichen Aktionen des Agenten werden als Zielergebnisse der Politik verwendet. Auf diese Weise stellen wir Verbindungen zwischen Ziel und Handlung in einem bestimmten Umweltzustand her. Aber es gibt einen Punkt. Beim Training des Autoencoders haben wir uns bemüht, Prognosedaten für mehrere Kerzen im Voraus zu erhalten. Daher erwarten wir nun, dass wir in einer komprimierten Darstellung des aktuellen Zustands Vorhersageinformationen über mehrere nachfolgende Kerzen erhalten. Es ist logisch anzunehmen, dass die in dieser Phase durchgeführten Aktionen des Agenten so gestaltet sein sollten, dass ein Ergebnis in der vorhergesagten Zeitspanne erzielt wird. Wir könnten die Gesamtbelohnung für den Prognosezeitraum als Ziel für die getroffene Maßnahme nehmen. Warum aber sollte ein aktuell offenes Geschäft erst nach Ablauf des Prognosezeitraums abgeschlossen werden? Sie kann entweder früher oder später geschlossen werden. Für den „späteren“ Fall können wir nicht über die Prognosewerte hinausschauen. Daher können wir nur das Ergebnis am Ende des Prognosezeitraums nehmen. Ändert sich jedoch die Richtung der Preisbewegung innerhalb des Prognosezeitraums, sollte das Geschäft früher abgeschlossen werden. Unser potenzielles Ziel sollte daher der maximale Wert über den Prognosezeitraum sein, wobei der Abzinsungsfaktor zu berücksichtigen ist.

Das Problem ist, dass der Erfahrungswiederholungspuffer kumulative Belohnungen bis zum Ende der Episode speichert. Wir benötigen jedoch die Gesamtmenge der Belohnungen aus dem analysierten Zustand über den Prognosedatenhorizont. Daher stellen wir zunächst die Belohnung bei jedem Schritt wieder her, ohne den Abzinsungsfaktor zu berücksichtigen.

         targets = matrix<float>::Zeros(PrecoderBars, NRewards);
         result.Assign(Buffer[tr].States[i + 1].rewards);
         for(int t = 0; t < PrecoderBars; t++)
           {
            target = result;
            result.Assign(Buffer[tr].States[i + t + 2].rewards);
            target = target - result * DiscFactor;
            targets.Row(target, t);
           }

Dann addieren wir sie in umgekehrter Reihenfolge und berücksichtigen dabei den Abwertungsfaktor.

         for(int t = 1; t < PrecoderBars; t++)
           {
            target = targets.Row(t - 1) + targets.Row(t) * MathPow(DiscFactor, t);
            targets.Row(target, t);
           }

Aus der sich daraus ergebenden Matrix wählen wir die Zeile mit der höchsten Belohnung aus, die unser Ziel sein wird.

         result = targets.Sum(1);
         ulong row = result.ArgMax();
         target = targets.Row(row);
         bGoal.AssignArray(target);

Ich stimme der Beobachtung zu, dass der Gewinn (oder Verlust), der in späteren Zeitschritten erzielt wird, mit Geschäften in Verbindung gebracht werden kann, die der Agent früher oder später getätigt hat. Hier gibt es zwei Punkte.

Die Erwähnung von bereits getätigten Geschäften ist nicht ganz korrekt. Denn die Tatsache, dass der Agent sie offen gelassen hat, ist eine Handlung des gegenwärtigen Augenblicks. Ihr späteres Ergebnis ist daher eine Folge dieser Handlung.

Was die nachfolgenden Handlungen betrifft, so analysieren wir im Rahmen der Trajektorienanalyse nicht die einzelnen Handlungen, sondern die Verhaltenspolitik des Akteurs als Ganzes. Folglich wird das Ziel für die Politik für die absehbare Zukunft festgelegt und nicht für eine separate Aktion. Unter diesem Gesichtspunkt ist die Festlegung eines Höchstziels für den Prognosezeitraum durchaus sinnvoll.

Unter Berücksichtigung des vorbereiteten Ziels verfügen wir über genügend Daten, um einen Vorwärts-Durchgang des Akteurs durchzuführen.

         //--- Actor
         if(!Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CBufferFloat*)GetPointer(bGoal)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Als Nächstes müssen wir die Modellparameter anpassen, um den Fehler zwischen den vorhergesagten und den tatsächlich ausgeführten Handlungen im Prozess der Interaktion mit der Umwelt zu minimieren. Hier verwenden wir Methoden des überwachten Lernens, ergänzt durch die Berücksichtigung maximaler Abweichungen. Wie bei dem oben beschriebenen Algorithmus berechnen wir zunächst den gleitenden Standardabweichungsfehler der Prognosen für jeden Parameter.

         target.Assign(Buffer[tr].States[i].action);
         target.Clip(0, 1);
         Actor.getResults(result);
         vector<float> error = target - result;
         std_count = MathMin(std_count, 999);
         STD_Actor = MathSqrt((MathPow(STD_Actor, 2) * std_count + MathPow(error, 2)) / (std_count + 1));

Dann vergleichen wir den aktuellen Fehler mit dem Schwellenwert. Ein Rückwärts-Durchgang wird nur ausgeführt, wenn bei mindestens einem Parameter eine Abweichung über dem Schwellenwert vorliegt.

         check = MathAbs(error) - STD_Actor * STD_Multiplier;
         if(check.Max() > 0)
           {
            Result.AssignArray(CAGrad(error) + result);
            if(!Actor.backProp(Result, (CBufferFloat *)GetPointer(bGoal), (CBufferFloat *)GetPointer(bGradient)) ||
               !StateEncoder.backPropGradient(GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient)))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               break;
              }
           }

Nach der Aktualisierung der Akteur-Parameter wird das Zielsetzungsmodell trainiert. Im Gegensatz zum Akteurs verwendet er nur eine komprimierte Darstellung des analysierten Zustands, die er vom Encoder als Ausgangsdaten erhält. Außerdem müssen wir keine zusätzlichen Daten vorbereiten, bevor wir einen Vorwärts-Durchgang durchführen.

         //--- Goal
         if(!Goal.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

Als Zielwerte für das Modelltraining werden wir die Ziele verwenden, die oben für die Akteurspolitik festgelegt wurden. Allerdings mit einem kleinen Zusatz. In vielen Arbeiten wird empfohlen, bei der Bildung von Zielen für geschulte Maßnahmen einen Steigerungsfaktor für die tatsächlich erzielten Ergebnisse zu verwenden. Dies sollte die Verhaltenspolitik dazu anregen, optimalere Maßnahmen zu wählen. Wir werden das Zielsetzungsmodell sofort trainieren, um bessere Ergebnisse zu erzielen. Um dies zu erreichen, werden wir bei der Bildung eines Vektors von Zielwerten die tatsächlichen Leistungen um das Zweifache erhöhen. Bitte beachten Sie jedoch Folgendes. Wir können den Vektor der tatsächlichen Belohnungen nicht einfach mit 2 multiplizieren. Da es unter den erhaltenen Belohnungen auch negative Werte geben kann, wird die Multiplikation mit 2 die Erwartungen nur verschlechtern. Daher bestimmen wir zunächst das Vorzeichen der Belohnung.

         target=targets.Row(row);
         result = target / (MathAbs(target) + FLT_EPSILON);

Als Ergebnis dieser Operation wird ein Vektor erwartet, der „-1“ für negative Werte und „1“ für positive Werte enthält. Erhöht man den Vektor von „2“ auf die Potenz des resultierenden Vektors, erhält man „2“ für positive Werte und „½“ für negative Werte.

        result = MathPow(vector<float>::Full(NRewards, 2), result);

Nun können wir den Vektor der tatsächlichen Ergebnisse mit dem Vektor der oben ermittelten Koeffizienten multiplizieren, um den erwarteten Gewinn zu verdoppeln. Wir werden dies als Zielwerte für das Training unseres Zielsetzungsmodells verwenden.

         target = target * result;
         Goal.getResults(result);
         error = target - result;
         std_count = MathMin(std_count, 999);
         STD_Goal = MathSqrt((MathPow(STD_Goal, 2) * std_count + MathPow(error, 2)) / (std_count + 1));
         std_count++;
         check = MathAbs(error) - STD_Goal * STD_Multiplier;
         if(check.Max() > 0)
           {
            Result.AssignArray(CAGrad(error) + result);
            if(!Goal.backProp(Result, (CBufferFloat *)NULL, (CBufferFloat *)NULL))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               break;
              }
           }

Auch hier nutzen wir die Idee, geschlossene Ausdrücke zu verwenden, um das Modell zu optimieren, wobei der Schwerpunkt auf maximalen Abweichungen liegt.

In dieser Phase haben wir die Parameter aller trainierten Modelle optimiert. Wir speichern die Ergebnisse des Encoders in den entsprechenden Puffer.

         Encoder.getResults(result);
         bLastEncoder.AssignArray(result);

Wir informieren den Nutzer über den Fortschritt des Lernprozesses und gehen zur nächsten Iteration des Schleifensystems über.

         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations);
            string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent, Actor.getRecentAverageError());
            str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Goal", percent, Goal.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

Nachdem alle Iterationen des Modelltrainings abgeschlossen sind, wird das Kommentarfeld in der Symboltabelle gelöscht. Ausdruck der Trainingsergebnisse in das Protokoll und Abschluss des EAs.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Goal", Goal.getRecentAverageError());
   ExpertRemove();
//---
  }

Damit ist die Beschreibung der vom Algorithmus verwendeten Programme abgeschlossen. Im Anhang finden Sie den vollständigen Code aller im Artikel verwendeten Programme. Dieser Anhang enthält auch den EA zum Testen trainierter Modelle, auf den wir jetzt nicht näher eingehen wollen.


3. Test

Wir haben viel Arbeit in die Implementierung der Goal-Conditioned Predictive Coding Methode mit MQL5 gesteckt. Der Umfang dieses Artikels bestätigt den Umfang der geleisteten Arbeit. Nun ist es an der Zeit, die Ergebnisse zu testen.

Wie üblich trainieren und testen wir die Modelle anhand historischer Daten für EURUSD, H1. Die Modelle werden mit historischen Daten für die ersten 7 Monate des Jahres 2023 trainiert. Um das trainierte Modell zu testen, verwenden wir historische Daten vom August 2023, der unmittelbar auf den historischen Trainingszeitraum folgt.

Das Training wurde iterativ durchgeführt. Zunächst wurde ein Trainingsdatensatz gesammelt, den wir in 2 Stufen erhoben haben. In der ersten Stufe haben wir, wie in der Real-ORL-Methode vorgeschlagen, Durchgänge auf der Grundlage von realen Signaldaten in der Trainingsmenge gespeichert. Dann wurde der Trainingsdatensatz mit Durchläufen unter Verwendung der EA ...\Experts\GCPC\Research.mq5 und zufälligen Richtlinien ergänzt.

Der Autoencoder wurde auf diesen Daten mit dem EA ...\Experts\GCPC\StudyEncoder.mq5 trainiert. Wie bereits erwähnt, sind für die Zwecke des Trainings dieses EA alle Durchgänge identisch. Die Modellschulung erfordert keine zusätzliche Aktualisierung des Schulungsdatensatzes. Deshalb trainieren wir einen maskierten Autoencoder, bis wir akzeptable Ergebnisse erhalten.

In der zweiten Phase trainieren wir die Verhaltenspolitik des Agenten und das Zielsetzungsmodell. Hier verwenden wir einen iterativen Ansatz, bei dem wir Modelle trainieren und dann die Trainingsdaten aktualisieren. Ich muss sagen, dass ich zu diesem Zeitpunkt überrascht war. Der Trainingsprozess erwies sich als recht stabil und mit einer guten Dynamik der Ergebnisse. Während des Trainingsprozesses wurde eine Strategie entwickelt, die in der Lage war, sowohl im Trainings- als auch im Testzeitraum Gewinne zu erzielen.


Schlussfolgerung

In diesem Artikel haben wir eine recht interessante Methode des Goal-Conditioned Predictive Coding kennengelernt. Sein Hauptbeitrag ist die Aufteilung des Modelltrainings in zwei Teilprozesse: Trajektorienlernen und separates Politiklernen. Beim Lernen einer Trajektorie liegt das Augenmerk auf der Möglichkeit, beobachtete Trends auf zukünftige Zustände zu projizieren, was im Allgemeinen den Informationsgehalt der Daten erhöht, die dem Agenten zur Entscheidungsfindung übermittelt werden.

Im praktischen Teil dieses Artikels haben wir unsere Vision der vorgeschlagenen Methode mit MQL5 umgesetzt und in der Praxis die Wirksamkeit des vorgeschlagenen Ansatzes bestätigt.

Ich möchte jedoch noch einmal darauf hinweisen, dass alle in diesem Artikel vorgestellten Programme nur zu Demonstrationszwecken der Technologie dienen. Sie sind nicht für den Einsatz auf realen Finanzmärkten geeignet.


Referenzen


Programme, die im diesem Artikel verwendet werden

# Name Typ Beschreibung
1 Research.mq5 Expert Advisor Beispielsammlung EA
2 ResearchRealORL.mq5
Expert Advisor
EA zum Sammeln von Beispielen mit der Real-ORL-Methode
3 Study.mq5  Expert Advisor Trainings-EA der Politik
4 StudyEncoder.mq5 Expert Advisor
Trainings-EA des Autoencoders
5 Test.mq5 Expert Advisor Trainings-EA für das Model
6 Trajectory.mqh Klassenbibliothek Struktur der Systemzustandsbeschreibung
7 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
8 NeuroNet.cl Code Base OpenCL-Programmcode-Bibliothek

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

Beigefügte Dateien |
MQL5.zip (757.07 KB)
Algorithmen zur Optimierung mit Populationen: Binärer genetischer Algorithmus (BGA). Teil I Algorithmen zur Optimierung mit Populationen: Binärer genetischer Algorithmus (BGA). Teil I
In diesem Artikel werden wir verschiedene Methoden untersuchen, die in binären genetischen und anderen Populationsalgorithmen verwendet werden. Wir werden uns die Hauptkomponenten des Algorithmus, wie Selektion, Crossover und Mutation, und ihre Auswirkungen auf die Optimierung ansehen. Darüber hinaus werden wir Methoden der Datendarstellung und ihre Auswirkungen auf die Optimierungsergebnisse untersuchen.
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 1): Zusammenarbeit von mehreren Handelsstrategien Entwicklung eines Expertenberaters für mehrere Währungen (Teil 1): Zusammenarbeit von mehreren Handelsstrategien
Es gibt eine ganze Reihe von verschiedenen Handelsstrategien. Daher kann es sinnvoll sein, mehrere Strategien parallel anzuwenden, um Risiken zu diversifizieren und die Stabilität der Handelsergebnisse zu erhöhen. Wenn jedoch jede Strategie als separater Expert Advisor (EA) implementiert wird, wird die Verwaltung ihrer Arbeit auf einem Handelskonto sehr viel schwieriger. Um dieses Problem zu lösen, wäre es sinnvoll, den Betrieb verschiedener Handelsstrategien innerhalb eines einzigen EA zu implementieren.
Algorithmen zur Optimierung mit Populationen: Binärer genetischer Algorithmus (BGA). Teil II Algorithmen zur Optimierung mit Populationen: Binärer genetischer Algorithmus (BGA). Teil II
In diesem Artikel befassen wir uns mit dem binären genetischen Algorithmus (BGA), der die natürlichen Prozesse modelliert, die im genetischen Material von Lebewesen in der Natur ablaufen.
Kausalschluss in den Problemen bei Zeitreihenklassifizierungen Kausalschluss in den Problemen bei Zeitreihenklassifizierungen
In diesem Artikel werden wir uns mit der Theorie des Kausalschlusses unter Verwendung von maschinellem Lernen sowie mit der Implementierung des nutzerdefinierten Ansatzes in Python befassen. Kausalschlüsse und kausales Denken haben ihre Wurzeln in der Philosophie und Psychologie und spielen eine wichtige Rolle für unser Verständnis der Realität.