English Русский 中文 Español Português
preview
Neuronale Netze im Trading: Anomalieerkennung im Frequenzbereich (letzter Teil)

Neuronale Netze im Trading: Anomalieerkennung im Frequenzbereich (letzter Teil)

MetaTrader 5Handelssysteme |
31 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Im vorherigen Artikel haben wir das innovative Framework CATCH zur Anomalieerkennung in multivariaten Zeitreihen vorgestellt. Die von den Autoren vorgeschlagene Frequenzbereichsanalyse ermöglicht nicht nur die Erkennung von punktuellen Anomalien wie Ausreißern und abrupten Änderungen, sondern auch die Identifizierung komplexerer verborgener Muster, die sich herkömmlichen Ansätzen oft entziehen. Das Framework CATCH verwendet die Fourier-Transformation, um Daten aus dem Zeitbereich in ihre spektrale Darstellung zu überführen, was neue Möglichkeiten für eine detaillierte Analyse der Merkmale der untersuchten Sequenz eröffnet.

Einer der Hauptvorteile von CATCH ist die Verwendung von Frequenz-Patching. Anstatt das gesamte Spektrum als Ganzes zu analysieren, unterteilt das Modell es in separate Fragmente, die bestimmten Frequenzbereichen entsprechen. Dieser Ansatz ermöglicht es, sowohl globale Trends als auch lokale Merkmale zu untersuchen, die mit hochfrequenten Komponenten verbunden sind. Dadurch können verschiedene Arten von Anomalien mit hoher Präzision identifiziert und klassifiziert werden, unabhängig davon, ob es sich um kurzfristige Ausschläge oder komplexere Abweichungen auf Teilsequenzebene handelt. Eine solche spektrale Unterteilung ermöglicht eine wesentlich detailliertere Untersuchung der Zeitreihenstruktur.

Eine wichtige Komponente des Frameworks ist das adaptive Modul, das für die Analyse der Beziehungen zwischen verschiedenen Datenkanälen verantwortlich ist. Diese Komponente, die einen maskierten Attention-Mechanismus implementiert, ermöglicht es dem System, sich auf die signifikantesten Korrelationen zu konzentrieren und gleichzeitig Rauschen sowie irrelevante Informationen herauszufiltern. Dieser Mechanismus verbessert die Rekonstruktionsqualität des normalen Verhaltens und erhöht die Robustheit des Modells in einem sich verändernden Marktumfeld erheblich. Im Gegensatz zu herkömmlichen Methoden, die Kanäle unabhängig voneinander verarbeiten, berücksichtigt CATCH deren komplexe gegenseitige Abhängigkeiten. Dies ist besonders wichtig bei der Arbeit mit Finanzdaten, bei denen Bewegungen in einem Markt andere Segmente beeinflussen können.

In der letzten Phase des Frameworks CATCH wird die ursprüngliche Zeitreihe rekonstruiert. Nach einer detaillierten Analyse im Frequenzbereich und der Identifizierung potenzieller Anomalien führt das System eine inverse Transformation durch, wodurch die Daten in ihre herkömmliche Darstellung im Zeitbereich zurückgeführt werden. Die Differenz zwischen den ursprünglichen Werten und der rekonstruierten Zeitreihe dient als zuverlässiger Indikator für Abweichungen und ermöglicht eine rechtzeitige Reaktion auf Veränderungen der Marktdynamik.

Nachfolgend ist die Visualisierung des Frameworks CATCH durch die Autoren dargestellt.

Im praktischen Teil des letzten Artikels haben wir begonnen, unsere eigene Interpretation der vorgeschlagenen Ansätze mit MQL5 zu implementieren. Insbesondere haben wir eine Faltungsschicht für die Verarbeitung komplexwertiger Daten entwickelt. Wir haben auch die Implementierung des Forward- und des Backward-Pass des komplexwertigen maskierten Attention-Moduls innerhalb eines OpenCL-Kontexts untersucht. Heute setzen wir diese Arbeit fort.


Objekt für komplexwertige maskierte Attention-Mechanismen

Im letzten Artikel haben wir die Kernel MaskAttentionComplex und MaskAttentionGradientsComplex untersucht, die die Forward- und Backward-Pass-Algorithmen des maskierten Attention-Mechanismus im komplexen Bereich implementieren. Heute setzen wir diese Arbeit fort. Wir implementieren das maskierte Attention-Modul auf der Seite der Hauptanwendung. Um dies zu erreichen, erstellen wir ein neues Objekt CNeuronComplexMVMHMaskAttention mit der folgenden Struktur:

class CNeuronComplexMVMHMaskAttention :  public CNeuronBaseOCL
  {
protected:
   uint                       iWindow;
   uint                       iWindowKey;
   uint                       iHeads;
   uint                       iUnits;
   uint                       iVariables;
   //---
   CNeuronComplexConvOCL      cQKV;
   CNeuronBaseOCL             cQ;
   CNeuronBaseOCL             cKV;
   CNeuronConvOCL             cMask;
   CNeuronBaseOCL             cMHAttentionOut;
   CNeuronComplexConvOCL      cPooling;
   CNeuronBaseOCL             cResidual;
   CNeuronComplexConvOCL      cFeedForward[2];
   //---
   virtual bool               AttentionOut(void);
   virtual bool               AttentionInsideGradients(void);
   virtual bool               SumAndNormilize(CBufferFloat *tensor1, CBufferFloat *tensor2, CBufferFloat *out,
                                              int dimension, bool normilize = true, int shift_in1 = 0,
                                              int shift_in2 = 0, int shift_out = 0, float multiplyer = 0.5f) override;
   //---
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               calcInputGradients(CNeuronBaseOCL *prevLayer) override;

public:
                     CNeuronComplexMVMHMaskAttention(void) {};
                    ~CNeuronComplexMVMHMaskAttention(void) {};
   //---
   virtual bool               Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint window, uint window_key, uint heads, 
                                   uint units_count, uint variables, 
                                   ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int                Type(void)   const   {  return defNeuronComplexMVMHMaskAttention;   }
   //---
   virtual bool               Save(int const file_handle) override;
   virtual bool               Load(int const file_handle) override;
   //---
   virtual bool               WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void               SetOpenCL(COpenCLMy *obj) override;
  };

Wie man sehen kann, ist die Struktur dieses neuen Objekts für Attention-Module weitgehend standardisiert, wobei mehrere Variationen bereits in unserer Bibliothek implementiert wurden. Es gibt jedoch einen wichtigen Unterschied im Zusammenhang mit der Verwendung von komplexwertigen Daten. Das Objekt ist so konzipiert, dass es Eingabedaten empfängt, die bereits in eine komplexe Form konvertiert wurden, wodurch wir die Transformationsphase der Daten überspringen können. Um die Repräsentationen von Query, Key und Value zu generieren, verwenden wir die zuvor entwickelte Faltungsschicht für die komplexwertige Verarbeitung. Lassen Sie uns die Implementierung Schritt für Schritt untersuchen.

Alle internen Objekte werden als Klassenmitglieder deklariert, sodass wir den Konstruktor und Destruktor der Klasse leer lassen können. Die Initialisierung dieser deklarierten und geerbten Objekte wird in der Methode Init durchgeführt. Diese Methode erhält eine Reihe von Konstanten, die die Architektur des zu erstellenden Objekts eindeutig definieren. Ihre Struktur ist für solche Objekte ziemlich typisch.

bool CNeuronComplexMVMHMaskAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                           uint window, uint window_key, uint heads,
                                           uint units_count, uint variables,
                                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
//---
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, 2 * window * units_count * variables,
                                                                      optimization_type, batch))
      return false;

Zu Beginn der Methode rufen wir die entsprechende Methode der Basisklasse auf, die bereits die Initialisierungslogik für geerbte Objekte und Schnittstellen enthält. Anschließend speichern wir die vom externen Programm empfangenen Modellarchitekturparameter in internen Variablen.

   iWindow = window;
   iWindowKey = MathMax(window_key, 1);
   iUnits = units_count;
   iHeads = MathMax(heads, 1);
   iVariables = variables;

Als Nächstes fahren wir mit der Initialisierung der neu deklarierten Objekte fort. Die erste zu initialisierende Komponente ist das Maskengenerierungsmodul.

Es ist erwähnenswert, dass die Autoren des Frameworks ein Maskengenerierungsmodul vorschlagen, das auf einer trainierbaren Projektion der Eingabedaten basiert. Zusätzlich wird ein Multi-Head-Attention-Mechanismus verwendet, wobei jeder Attention-Head seine eigene, eindeutige Maske nutzt.

Ein weiterer wichtiger Aspekt ist, dass die Attention nicht über das gesamte Frequenzspektrum angewendet wird. Das Framework CATCH wendet Attention nur innerhalb einzelner Patches an, die demselben Frequenzsegment entsprechen, aber zu unterschiedlichen univariaten Sequenzen gehören.

Natürlich muss die Ausgabe des Maskengenerierungsmoduls eine probabilistische Darstellung des Einflusses einzelner Kanäle in Form von reellwertigen Koeffizienten sein.

Um diese Anforderungen zu erfüllen, verwendet unsere Implementierung eine standardmäßige Faltungsschicht mit einer verdoppelten Kernelgröße (um komplexwertige Fenster zu berücksichtigen) und einer Anzahl von Filtern, die der Dimensionalität des Maskierungsvektors für ein einzelnes Sequenzelement entspricht.

   uint index = 0;
   if(!cMask.Init(0, index, OpenCL, 2 * iWindow, 2 * iWindow, iVariables * iHeads,
                                         iUnits, iVariables, optimization, iBatch))
      return false;
   cMask.SetActivationFunction(SIGMOID);
   CBufferFloat *temp = cMask.GetWeightsConv();
   if(!temp ||
      !temp.Fill(0))
      return false;

Um sicherzustellen, dass die Ausgabewerte innerhalb des erforderlichen Bereichs bleiben, verwenden wir eine Sigmoid-Aktivierungsfunktion.

Beachten Sie, dass die trainierbare Parametermatrix bei der Initialisierung mit Nullen gefüllt wird. Dies ermöglicht es allen Elementen, mit einem gleichen Einfluss-Maskenwert von 0,5 zu beginnen. Während des Trainings werden die Gewichte angepasst, wodurch die Abhängigkeitsmaske zwischen den Kanälen schrittweise modifiziert wird.

Als Nächstes initialisieren wir das Modul, das für die Generierung der Repräsentationen Query, Key und Value verantwortlich ist. Hier verwenden wir eine komplexwertige Faltungsschicht, da sowohl die Eingabe als auch die Ausgabe des Moduls komplexwertig sein sollen.

   index++;
   if(!cQKV.Init(0, index, OpenCL, iWindow, iWindow, 3 * iWindowKey * iHeads,
                                    iUnits, iVariables, optimization, iBatch))
      return false;
   cQKV.SetActivationFunction(None);

Wir teilen dann den kombinierten Tensor, der alle drei Repräsentationen enthält, in zwei Teile auf und extrahieren die Query-Entität in eine separate Matrix. Um diesen Vorgang zu unterstützen, initialisieren wir zwei zusätzliche Objekte. Anschließend wird die Methode beendet und gibt ein boolesches Ergebnis zurück, das angibt, ob alle Operationen erfolgreich ausgeführt wurden.

   index++;
   if(!cQ.Init(0, index, OpenCL, 2 * iWindowKey * iHeads * iVariables * iUnits, optimization, iBatch))
      return false;
   cQ.SetActivationFunction(None);
   index++;
   if(!cKV.Init(0, index, OpenCL, 2 * cQ.Neurons(), optimization, iBatch))
      return false;
   cKV.SetActivationFunction(None);

In derselben Phase initialisieren wir das Objekt, das für die Speicherung der Multi-Head-Attention-Ergebnisse verantwortlich ist.

   index++;
   if(!cMHAttentionOut.Init(0, index, OpenCL, cQ.Neurons(), optimization, iBatch))
      return false;
   cMHAttentionOut.SetActivationFunction(None);

Wir fügen dann eine komplexwertige Faltungsschicht zur Reduzierung der Dimensionalität der Attention-Ausgaben hinzu.

   index++;
   if(!cPooling.Init(0, index, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, 
                              iWindow, iUnits, iVariables, optimization, iBatch))
      return false;
   cPooling.SetActivationFunction(None);

Ähnlich wie bei der klassischen Transformer-Architektur fügen wir eine Schicht hinzu, um Residualverbindungen zu erhalten.

   index++;
   if(!cResidual.Init(0, index, OpenCL, cPooling.Neurons(), optimization, iBatch))
      return false;
   cResidual.SetActivationFunction(None);

Darauf folgen zwei komplexwertige Faltungsschichten des Moduls FeedForward.

   index++;
   if(!cFeedForward[0].Init(0, index, OpenCL, iWindow, iWindow, 4 * iWindow,
                                  iUnits, iVariables, optimization, iBatch))
      return false;
   cFeedForward[0].SetActivationFunction(LReLU);
   index++;
   if(!cFeedForward[1].Init(0, index, OpenCL, 4 * iWindow, 4 * iWindow, iWindow,
                                       iUnits, iVariables, optimization, iBatch))
      return false;
   cFeedForward[1].SetActivationFunction(None);
   SetActivationFunction(None);
//---
   return true;
  }

Abschließend schließen wir die Objektinitialisierung ab und geben ein boolesches Ergebnis zurück, das dem aufrufenden Programm den Erfolg des Vorgangs anzeigt.

Der nächste Schritt besteht darin, den Forward-Pass-Algorithmus innerhalb der Methode feedForward zu erstellen.

bool CNeuronComplexMVMHMaskAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

Die Methode empfängt einen Zeiger auf das Eingabedatenobjekt, dessen Gültigkeit sofort überprüft wird.

Um die Trainingsstabilität zu verbessern, werden die Eingabedaten normalisiert.

   if(!NeuronOCL.SwapOutputs())
      return false;
   if(!SumAndNormilize(NeuronOCL.getPrevOutput(), NeuronOCL.getPrevOutput(), NeuronOCL.getOutput(),
                       iWindow, true, 0, 0, 0, 1))
      return false;

Es ist zu beachten, dass die Eingabedaten aus komplexwertigen Größen bestehen. Daher wurden die Summations- und Normalisierungsmethoden neu definiert, um komplexwertige Operationen zu unterstützen. Die vollständige Implementierung finden Sie im beigefügten Quellcode.

Nach der Normalisierung werden die Eingabedaten verwendet, um die Maskierungstensoren sowie die Entitäten Query, Key und Value zu generieren.

   if(!cMask.FeedForward(NeuronOCL))
      return false;
   if(!cQKV.FeedForward(NeuronOCL))
      return false;

Wir teilen dann den kombinierten Tensor der generierten Entitäten in zwei separate Tensoren auf.

   if(!NeuronOCL.SwapOutputs())
      return false;
   if(!DeConcat(cQ.getOutput(), cKV.getOutput(), cQKV.getOutput(), 2 * iWindowKey * iHeads,
                                              4 * iWindowKey * iHeads, iUnits * iVariables))
      return false;

Alle vorbereiteten Daten werden anschließend an den Masked-Attention-Forward-Pass-Kernel übergeben. Dieser Vorgang wird innerhalb einer dedizierten Methode namens AttentionOut ausgeführt.

   if(!AttentionOut())
      return false;

Wir reduzieren dann die Dimensionalität der resultierenden Multi-Head-Attention-Ausgabe und addieren die Ausgabe zum ursprünglichen Eingabedatensatz, um Residualverbindungen herzustellen.

   if(!cPooling.FeedForward(cMHAttentionOut.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getOutput(), cPooling.getOutput(), cResidual.getOutput(), iWindow, true, 0, 0, 0, 1))
      return false;

Die Daten werden anschließend durch die zwei Faltungsschichten des Moduls FeedForward geleitet.

   if(!cFeedForward[0].FeedForward(cResidual.AsObject()))
      return false;
   if(!cFeedForward[1].FeedForward(cFeedForward[0].AsObject()))
      return false;

Wiederum fügen wir Residualverbindungen hinzu. Wir speichern die endgültige Ausgabe im geerbten Puffer, der für den Datenaustausch mit anderen neuronalen Schichten des Modells verwendet wird.

   if(!SumAndNormilize(cResidual.getOutput(), cFeedForward[1].getOutput(), getOutput(), iWindow, true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

Die Methode endet mit der Rückgabe des logischen Ergebnisses der Operation an das aufrufende Programm.

Ein paar Worte sollten zur Methode AttentionOut gesagt werden, die für das Einreihen der Ausführung des Multi-Head-Masked-Attention-Kernels MaskAttentionComplex verantwortlich ist. In unseren jüngsten Arbeiten haben wir uns kaum mit den technischen Details der Einreichung von OpenCL-Kerneln in die Ausführungswarteschlange befasst. Dies ist verständlich. Das Verfahren zum Einstellen von Kernels ist ziemlich standardisiert. Und es besteht keine Notwendigkeit, dieselben Vorgänge wiederholt zu beschreiben. In diesem Fall gibt es jedoch eine wichtige Nuance im Zusammenhang mit dem Framework CATCH.

Wie oben erwähnt, schlagen die Autoren des Frameworks CATCH vor, die maskierte Attention strikt zwischen Elementen innerhalb desselben Frequenzbereichs über verschiedene univariate Sequenzen hinweg durchzuführen. Einfach ausgedrückt nehmen wir identische Frequenz-Patches aus verschiedenen univariaten Sequenzen und analysieren deren gegenseitige Abhängigkeiten. Dies ermöglicht es uns, Beziehungen innerhalb verschiedener univariater Sequenzen über Frequenzbänder hinweg unabhängig zu identifizieren – wobei langfristige Trends und kurzfristige Dynamiken separat erfasst werden. Diese Eigenschaft wurde jedoch bei der Entwicklung eines vereinheitlichten maskierten Attention-Kernels nicht explizit berücksichtigt.

Dennoch können wir das erforderliche Verhalten organisieren, indem wir den Aufgabenraum steuern, der für die Kernel-Ausführungswarteschlange verwendet wird. Lassen Sie uns zunächst die Dimensionalität der vom Objekt empfangenen Eingabedaten untersuchen. Wir arbeiten mit segmentierten Daten aus einer multivariaten Sequenz, die als dreidimensionaler Tensor dargestellt werden können: {Variable, Segment, Dimension}. Wenn wir Entitäten für die Multi-Head-Attention konstruieren, kann zudem jede Ausgabe als vierdimensionaler Tensor dargestellt werden: {Variable, Segment, Head, Dimension}.

Es ist wichtig zu beachten, dass unsere Multi-Head-Attention-Implementierung jeden Attention-Head unabhängig verarbeitet. Wenn wir daher die Segment- und Head-Dimension zusammenführen und jedes Segment als unabhängigen Attention-Head behandeln, erhalten wir effektiv eine Analyse der Abhängigkeiten zwischen identischen Segmenten über verschiedene univariate Sequenzen hinweg. Dies ist genau das Verhalten, das für die korrekte Implementierung des Frameworks CATCH erforderlich ist.

Die Methode AttentionOut nimmt keine Parameter entgegen. Im Kontrollblock der Methode überprüfen wir lediglich die Gültigkeit des Zeigers auf das Kontextverwaltungsobjekt von OpenCL.

bool CNeuronComplexMVMHMaskAttention::AttentionOut(void)
  {
   if(!OpenCL)
      return false;

Als Nächstes definieren wir Arrays, die den Ausführungsraum des Kernels beschreiben. Wie bereits besprochen, wird die Sequenzdimension auf die Anzahl der univariaten Sequenzen im Datensatz festgelegt. Für die Anzahl der Köpfe verwenden wir das Produkt aus der konfigurierten Anzahl der Attention-Heads und der Anzahl der Segmente in jeder Sequenz.

   uint global_work_offset[3] = {0};
   uint global_work_size[3] = {iVariables/*Q units*/, iVariables/*K units*/, iHeads * iUnits/*Heads*/};
   uint local_work_size[3] = {1, iVariables, 1};

Wir gruppieren dann die Ausführungs-Threads in Arbeitsgruppen entlang der zweiten Dimension des Aufgabenraums.

Danach übergeben wir Zeiger auf die erforderlichen Datenpuffer als Kernel-Argumente.

   ResetLastError();
   int kernel = def_k_MaskAttentionComplex;
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_maskattcom_q, cQ.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_maskattcom_kv, cKV.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_maskattcom_scores, cMask.getPrevOutIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_maskattcom_out, cMHAttentionOut.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_maskattcom_masks, cMask.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

An dieser Stelle nutzen wir die Tatsache aus, dass die Attention-Koeffizientenmatrix dieselbe Dimensionalität aufweist wie die Kanalmaskierungsmatrix. Dies ermöglicht es uns, die Zuweisung eines zusätzlichen Puffers für die temporäre Speicherung von Attention-Koeffizienten zu vermeiden. Stattdessen wird ein ungenutzter Puffer aus dem Maskengenerierungsmodul für diesen Zweck wiederverwendet.

Wir übergeben dann die verbleibenden notwendigen Parameter an den Kernel.

   if(!OpenCL.SetArgument(kernel, def_k_maskattcom_dimension, (int)iWindowKey))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(kernel, def_k_maskattcom_heads_kv, (int)(iHeads * iUnits)))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

Es ist zudem erwähnenswert, dass wir sowohl für die Key- als auch für die Value-Repräsentationen die Anzahl der Attention-Heads erneut als Produkt aus der benutzerdefinierten Head-Anzahl und der Sequenzlänge definieren.

Schließlich stellen wir den Kernel zur Ausführung in die Warteschlange.

   if(!OpenCL.Execute(kernel, 3, global_work_offset, global_work_size, local_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

Die Methode endet mit der Rückgabe eines logischen Ergebnisses an das aufrufende Programm.

Nach Abschluss der Forward-Pass-Algorithmen fahren wir mit der Konstruktion der Backpropagation-Prozeduren fort. Der erste Schritt besteht darin, die Methode zu implementieren, die für die Verteilung der Fehlergradienten auf alle internen Komponenten gemäß ihrem Beitrag zum endgültigen Modellausgang verantwortlich ist: calcInputGradients.

bool CNeuronComplexMVMHMaskAttention::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

Die Methode erhält das gleiche Eingangsdatenobjekt, das während des Forward-Pass verwendet wurde. Diesmal wird es jedoch verwendet, um die entsprechenden Fehlergradienten zu propagieren. Wir überprüfen sofort die Gültigkeit des Zeigers.

In diesem Stadium enthält der externe Schnittstellenpuffer des Objekts den Fehlergradienten, der von der nachfolgenden Schicht empfangen wurde. Diese Werte wurden noch nicht durch die Ableitung der Aktivierungsfunktion angepasst. Dies ist beabsichtigt: Die Aktivierungsfunktion wurde bei der Initialisierung bewusst weggelassen. Dies geschieht, um Flexibilität bei der Auswahl der Aktivierungsfunktionen für interne Objekte zu bieten. Wir wenden nun die Ableitung der Aktivierungsfunktion der letzten Faltungsschicht im FeedForward-Modul an und propagieren die korrigierten Gradienten entsprechend. 

   if(!DeActivation(cFeedForward[1].getOutput(), cFeedForward[1].getGradient(), Gradient, cFeedForward[1].Activation()))
      return false;

Anschließend propagieren wir die Gradienten rückwärts durch die Schichten des Moduls bis zur Ebene der Residualverbindung, die dem Attention-Block folgt.

   if(!cFeedForward[0].calcHiddenGradients(cFeedForward[1].AsObject()))
      return false;
   if(!cResidual.calcHiddenGradients(cFeedForward[0].AsObject()))
      return false;

Für das Residualverbindungsobjekt nehmen wir ebenfalls das Fehlen einer Aktivierungsfunktion an. Wir wiederholen daher das Gradientenkorrekturverfahren unter Verwendung der Ableitung der Aktivierungsfunktion, die auf die Attention-Ausgangsskalierungsschicht angewendet wurde.

   if(!DeActivation(cPooling.getOutput(), cPooling.getGradient(), cResidual.getGradient(), cPooling.Activation()) ||
      !DeActivation(cPooling.getOutput(), cPooling.getPrevOutput(), Gradient, cPooling.Activation()) ||
      !SumAndNormilize(cPooling.getGradient(), cPooling.getPrevOutput(), cPooling.getGradient(), iWindow,
                                                                                       false, 0, 0, 0, 1))
      return false;

Es ist jedoch wichtig zu betonen, dass das Residualverbindungsobjekt derzeit nur den Gradienten enthält, der durch den Pfad FeedForward fließt. Wir müssen auch den Gradienten berücksichtigen, der durch den Residual-Attention-Pfad fließt. Daher korrigieren wir erneut den im externen Schnittstellenpuffer gespeicherten Gradienten unter Verwendung der Ableitung der Aktivierungsfunktion der Attention-Ausgangsprojektionsschicht.

Es ist zu beachten, dass die Wiederverwendung des externen Schnittstellenpuffers keine doppelte Transformation der Daten impliziert. Im ersten Fall wurden die korrigierten Werte im Puffer der finalen Schicht FeedForward gespeichert, während der Schnittstellenpuffer selbst die ursprünglichen Gradientenwerte beibehielt. Im aktuellen Schritt werden die Berechnungen in den Puffer der Projektionsschicht geschrieben, ohne die ursprünglichen Daten zu verändern, die später bei der Propagierung der Gradienten durch die residuale Verbindung zurück zur Eingangsebene wiederverwendet werden.

Nach diesen Korrekturen summieren wir die Beiträge aus beiden Informationsflüssen. Der Gradient wird dann auf die Attention-Heads verteilt.

   if(!cMHAttentionOut.calcHiddenGradients(cPooling.AsObject()))
      return false;
   if(!AttentionInsideGradients())
      return false;

Wir rufen dann die Methode AttentionInsideGradients auf, die für die Propagierung der Gradienten durch den Attention-Mechanismus verantwortlich ist. Wir werden hier nicht näher auf die Details dieser Methode eingehen. Wir überlassen dies dem Selbststudium. Die vollständige Umsetzung ist in der Anlage enthalten. Es sollte jedoch betont werden, dass die Konfiguration des Aufgabenraums und die Parameter der Kernel-Planung mit denen übereinstimmen müssen, die im Feed-Forward-Durchlauf verwendet wurden.

In der nächsten Phase führen wir alle Gradientenbeiträge der einzelnen Attention-Komponenten zu einem einzigen Tensor zusammen.

   if(!Concat(cQ.getGradient(), cKV.getGradient(), cQKV.getGradient(), 2 * iWindowKey * iHeads,
                                                  4 * iWindowKey * iHeads, iUnits * iVariables))
      return false;

Anschließend propagieren wir den Fehler bis zur Eingangsebene und wenden dabei die Ableitung der entsprechenden Aktivierungsfunktion an.

   if(!DeActivation(cQKV.getOutput(), cQKV.getPrevOutput(), cQKV.getGradient(), cQKV.Activation()) ||
      !prevLayer.calcHiddenGradients(cQKV.AsObject()))
      return false;

Dies stellt jedoch nur einen Informationsfluss dar. Wir beziehen auch den Gradientenbeitrag aus dem Pfad der residualen Verbindung ein und wenden dabei die Ableitung der Aktivierungsfunktion der Eingangsdatenverarbeitungsschicht an.

In diesem Schritt verwenden wir Daten sowohl aus dem Modul FeedForward als auch aus dem externen Schnittstellenpuffer.

   if(!DeActivation(prevLayer.getOutput(),cResidual.getPrevOutput(),cResidual.getGradient(),prevLayer.Activation()) ||
      !SumAndNormilize(prevLayer.getGradient(), cResidual.getPrevOutput(), cResidual.getPrevOutput(),
                       iWindow, false, 0, 0, 0, 1))
      return false;
   if(!DeActivation(prevLayer.getOutput(), cResidual.getGradient(), Gradient, prevLayer.Activation()) ||
      !SumAndNormilize(cResidual.getGradient(), cResidual.getPrevOutput(), cResidual.getPrevOutput(),
                       iWindow, false, 0, 0, 0, 1))
      return false;

Es ist wichtig, sich daran zu erinnern, dass die Eingangsdaten auch zur Erzeugung der Maskierungsmatrix verwendet wurden. Daher beziehen wir zusätzlich den mit diesem Berechnungspfad verbundenen Gradientenbeitrag ein.

   if(!DeActivation(cMask.getOutput(), cMask.getGradient(), cMask.getGradient(), cMask.Activation()) ||
      !prevLayer.calcHiddenGradients(cMask.AsObject()) ||
      !SumAndNormilize(prevLayer.getGradient(), cResidual.getPrevOutput(), prevLayer.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

An diesem Punkt haben wir das vollständige Gradientensignal über alle Informationsflüsse hinweg auf der Eingangsebene akkumuliert und können die Methode abschließen, indem wir einen booleschen Status an das aufrufende Programm zurückgeben.

Die Methode zur Parameteroptimierung bleibt dem Selbststudium überlassen. Ihre Logik ist relativ einfach und besteht hauptsächlich aus Aufrufen der entsprechenden Methoden interner Untermodule. Die vollständige Umsetzung ist in der Anlage enthalten, zusammen mit dem vollständigen Code des Objekts und all seinen Methoden. Wir gehen nun zur nächsten Phase unserer Arbeit über.


Zusammenbau des Frameworks CATCH

Man kann sagen, dass bis zu diesem Punkt eine beträchtliche Menge an Vorbereitungsarbeit abgeschlossen wurde, während der einzelne Objekte entwickelt wurden. Nun fahren wir damit fort, sie in eine einheitliche Struktur des Frameworks CATCH zu integrieren. Die Ausführungslogik des Frameworks ist innerhalb des Objekts CNeuronCATCH implementiert, dessen Struktur unten gezeigt wird.

class CNeuronCATCH   :  public CNeuronTransposeOCL
  {
protected:
   CNeuronTransposeOCL        cTranspose;
   CNeuronBaseOCL             caFreqIn[2];
   CNeuronBaseOCL             cFreqConcat;
   CNeuronComplexConvOCL      caProjection[2];
   CNeuronComplexMVMHMaskAttention  caChannelFusion[2];
   CNeuronComplexConvOCL      caLinearHead[2];
   CNeuronBaseOCL             caFreqOut[2];
   //---
   virtual bool               FFT(CBufferFloat *inp_re, CBufferFloat *inp_im,
                                  CBufferFloat *out_re, CBufferFloat *out_im,
                                  uint variables, bool reverse = false);
   //---
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               calcInputGradients(CNeuronBaseOCL *prevLayer) override;


public:
                     CNeuronCATCH(void) {};
                    ~CNeuronCATCH(void) {};
   //---
   virtual bool               Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint time_step, uint variables,
                                   uint window, uint step,
                                   uint window_key, uint heads,
                                   ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int                Type(void) override const   {  return defNeuronCATCH;   }
   //---
   virtual bool               Save(int const file_handle) override;
   virtual bool               Load(int const file_handle) override;
   //---
   virtual bool               WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void               SetOpenCL(COpenCLMy *obj) override;
  };

In der vorgestellten Struktur beobachten wir eine relativ große Anzahl interner Komponenten, von denen jede für einen spezifischen Teil des komplexen Verarbeitungsalgorithmus des Frameworks verantwortlich ist. Ihre Rollen werden schrittweise während der Implementierung der Klassenmethoden diskutiert. Vorerst ist es erwähnenswert, dass alle Komponenten statisch deklariert sind, was es sowohl dem Konstruktor als auch dem Destruktor ermöglicht, leer zu bleiben. Die Initialisierung von Objekten wird in der Methode Init durchgeführt.

bool CNeuronCATCH::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                        uint time_step, uint variables,
                        uint window, uint step, uint window_key, uint heads,
                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronTransposeOCL::Init(numOutputs, myIndex, open_cl, variables, time_step, optimization_type, batch))
      return false;

Die Methode erhält eine Reihe von Konstanten, die die Architektur des initialisierten Objekts eindeutig definieren. Einige dieser Parameter definieren die Dimensionalität der Eingabedaten. Andere beschreiben die Struktur der internen Informationsflüsse. Zum Beispiel definieren time_step und variables die Anzahl der Zeitschritte und univariaten Sequenzen in der multivariaten Zeitreihe. Die Parameter window und step definieren die Strategie zur Segmentierung des Frequenzspektrums. Unterdessen repräsentieren window_key und heads die Kernparameter des Attention-Mechanismus.

Im Methodenrumpf rufen wir zuerst die entsprechende Methode der Basisklasse auf. In diesem Fall ist das Elternobjekt ein Datentranspositionsobjekt, und diese Wahl ist beabsichtigt. Die Eingabe wird als multivariate Zeitreihe erwartet, die als Matrix dargestellt ist, wobei jede Zeile dem Zustandsvektor der Umgebung zu einem gegebenen Zeitschritt entspricht. Das Framework CATCH arbeitet jedoch mit der Frequenzbereichsdarstellung einzelner univariater Sequenzen. Der Einfachheit halber transponieren wir zuerst den Eingabetensor und stellen ihn später in seiner ursprünglichen Darstellung wieder her. Letztere Operation wird von der Basisklasse durchgeführt, was sich in den an sie übergebenen Parametern widerspiegelt.

Um die Zeitreihe in den Frequenzbereich zu transformieren, verwenden wir die Schnelle Fourier-Transformation. Dieser Algorithmus erfordert jedoch, dass die Sequenzlänge eine Zweierpotenz ist. Um diese Einschränkung zu erfüllen, können wir jede Sequenz durch Auffüllen mit Nullen erweitern, was das Transformationsergebnis nicht beeinflusst. Der nächste Schritt besteht darin, die nächstgelegene gültige Sequenzlänge zu bestimmen.

//--- Calculate FFT size
   int power = int(MathLog(time_step) / M_LN2);
   if(power <= 0)
      return false;
   if(MathPow(2, power) != time_step)
      power++;
   uint FreqUinits = uint(MathPow(2, power));

Wir berechnen dann die Anzahl der Segmente gemäß den angegebenen Segmentierungsparametern.

   if(window <= 0 || step <= 0)
      return false;
   int Segments = int((FreqUinits - int(window) + step - 1) / step);
   if(Segments <= 0)
      return false;

An diesem Punkt ist die Vorbereitungsphase abgeschlossen, und wir fahren mit der Initialisierung der internen Komponenten fort. Die erste ist die Transpositionsschicht für die Eingabedaten. Ihre Initialisierungsparameter sind unkompliziert und erfordern keine besondere Erklärung.

   uint index = 0;
   if(!cTranspose.Init(0, index, OpenCL, time_step, variables, optimization, iBatch))
      return false;

Als Nächstes erstellen wir zwei Datenpuffer, um die Real- und Imaginärkomponenten des Frequenzspektrums nach der Fourier-Zerlegung zu speichern.

   for(uint i = 0; i < caFreqIn.Size(); i++)
     {
      index++;
      if(!caFreqIn[i].Init(0, index, OpenCL, FreqUinits * variables, optimization, iBatch))
         return false;
      caFreqIn[i].SetActivationFunction(None);
     }

Wir fügen dann eine Konkatenationsschicht für die Frequenzbereichsdarstellung hinzu.

   index++;
   if(!cFreqConcat.Init(0, index, OpenCL, 2 * FreqUinits * variables, optimization, iBatch))
      return false;
   cFreqConcat.SetActivationFunction(None);

Für die spektrale Segmentierung verwenden wir eine komplexwertige Faltungsschicht, die mit den entsprechenden Parametern konfiguriert ist.

   index++;
   if(!caProjection[0].Init(0, index, OpenCL, window, step, 2 * window, Segments, variables, optimization, iBatch))
      return false;
   caProjection[0].SetActivationFunction(LReLU);

Eine zweite Faltungsschicht vervollständigt die Embedding-Konstruktion für Spektral-Patches.

   index++;
   if(!caProjection[1].Init(0, index, OpenCL,2*window,2*window,window_key, Segments, variables, optimization, iBatch))
      return false;
   caProjection[1].SetActivationFunction(TANH);

Die Analyse der Abhängigkeiten zwischen den Frequenzspektren einzelner univariater Sequenzen erfolgt mithilfe von zwei aufeinanderfolgenden Attention-Modulen. Das Erste arbeitet direkt auf den erzeugten Patch-Embeddings.

   index++;
   if(!caChannelFusion[0].Init(0, index, OpenCL,window_key,window_key,heads,Segments, variables, optimization, iBatch))
      return false;

Die Architektur der zweiten maskierten Inter-Channel-Attention-Schicht hängt von der Anzahl der Segmente ab. Wenn die Anzahl der Segmente durch zwei teilbar ist, wird eine paarweise Zusammenführung der Segmente durchgeführt, um Abhängigkeiten höherer Ordnung zu erfassen.

   index++;
   if(Segments % 2 == 0)
     {
      if(!caChannelFusion[1].Init(0, index, OpenCL, 2 * window_key, window_key, heads, Segments / 2,
                                                                   variables, optimization, iBatch))
         return false;
     }
   else
      if(!caChannelFusion[1].Init(0, index, OpenCL, window_key, window_key, heads, Segments, variables,
                                                                                 optimization, iBatch))
         return false;

Andernfalls bleibt die Architektur identisch mit der vorherigen Attention-Schicht.

Die Dimensionalität des von den Attention-Modulen erzeugten Tensors wird dann mithilfe von zwei aufeinanderfolgenden Projektionsfaltungsschichten wieder auf die Länge des ursprünglichen Frequenzspektrums reduziert.

   index++;
   if(!caLinearHead[0].Init(0, index, OpenCL, window_key, window_key, window, Segments, variables,
                                                                            optimization, iBatch))
      return false;
   caLinearHead[0].SetActivationFunction(LReLU);
   index++;
   if(!caLinearHead[1].Init(0, index, OpenCL, window * Segments, window * Segments, FreqUinits, 
                                                               variables, 1, optimization, iBatch))
      return false;
   caLinearHead[



1 ].SetActivationFunction(None);

Die erste Schicht passt die Segmentdimensionalität an, während die zweite die Länge der univariaten Sequenzen korrigiert.

Wir führen außerdem zwei zusätzliche Komponenten ein, die für die Trennung der Real- und Imaginärteile des Frequenzspektrums vor der Anwendung der inversen Fourier-Transformation verantwortlich sind.

   for(uint i = 0; i < caFreqOut.Size(); i++)
     {
      index++;
      if(!caFreqOut[i].Init(0, index, OpenCL, FreqUinits * variables, optimization, iBatch))
         return false;
      caFreqOut[i].SetActivationFunction(None);
     }
//---
   return true;
  }

Zu diesem Zeitpunkt sind alle internen Komponenten initialisiert, und die Methode endet mit der Rückgabe eines booleschen Wertes, der den erfolgreichen Abschluss anzeigt.

Wir fahren nun mit der Implementierung des Forward-Pass in der Methode feedForward fort. Wie bereits erwähnt, erhält die Methode einen Zeiger auf das Eingabedatenobjekt, das den multivariaten Zeitreihen-Tensor enthält.

bool CNeuronCATCH::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cTranspose.FeedForward(NeuronOCL))
      return false;

Die Eingabedaten werden zunächst transponiert, um die Verarbeitung univariater Sequenzen zu erleichtern. Anschließend werden sie mittels der Fast-Fourier-Transformation in den Frequenzbereich transformiert.

   if(!FFT(cTranspose.getOutput(), NULL, caFreqIn[0].getOutput(), caFreqIn[1].getOutput(), iCount, false))
      return false;

Die resultierende Frequenzdarstellung wird zu einem einheitlichen Tensor verkettet.

   if(!Concat(caFreqIn[0].getOutput(), caFreqIn[1].getOutput(), cFreqConcat.getOutput(), 1, 1, caFreqIn[0].Neurons()))
      return false;

Als Nächstes werden spektrales Patching und Embedding unter Verwendung von zwei Projektions-Faltungsschichten durchgeführt.

   CNeuronBaseOCL *neuron = cFreqConcat.AsObject();
   for(uint i = 0; i < caProjection.Size(); i++)
     {
      if(!caProjection[i].FeedForward(neuron))
         return false;
      neuron = caProjection[i].AsObject();
     }

Als Nächstes analysieren wir die gegenseitigen Abhängigkeiten mithilfe der Inter-Channel-Masked-Attention-Module.

   for(uint i = 0; i < caChannelFusion.Size(); i++)
     {
      if(!caChannelFusion[i].FeedForward(neuron))
         return false;
      neuron = caChannelFusion[i].AsObject();
     }

Die Ausgabe wird dann zurück auf die Dimensionalität des ursprünglichen Frequenzspektrums projiziert.

   for(uint i = 0; i < caLinearHead.Size(); i++)
     {
      if(!caLinearHead[i].FeedForward(neuron))
         return false;
      neuron = caLinearHead[i].AsObject();
     }

Wir teilen dann die Frequenzdarstellung in reelle und imaginäre Komponenten auf.

   if(!DeConcat(caFreqOut[0].getOutput(), caFreqOut[1].getOutput(), neuron.getOutput(), 1, 1, 
                                                                     caFreqOut[0].Neurons()))
      return false;
   if(!FFT(caFreqOut[0].getOutput(), caFreqOut[1].getOutput(), caFreqOut[0].getPrevOutput(),
                                                caFreqOut[1].getPrevOutput(), iCount, true))
      return false;

Danach wird die inverse Fourier-Transformation angewendet, welche die Daten aus dem Frequenzbereich zurück in die Zeitbereichs-Sequenzdarstellung überführt. Es ist jedoch wichtig zu beachten, dass die inverse Fast-Fourier-Transformation eine Sequenz erzeugt, deren Länge eine Zweierpotenz ist. Dies kann von der ursprünglichen Sequenzlänge abweichen; daher werden die überschüssigen Werte durch Tensor-Dekonkatenierung verworfen.

   if(!DeConcat(caFreqOut[0].getOutput(), caFreqOut[1].getOutput(), caFreqOut[0].getPrevOutput(), iWindow,
                                                       caFreqOut[0].Neurons() / iCount - iWindow, iCount))
      return false;

Residuelle Verbindungen aus den transponierten Eingabedaten werden dann zu den resultierenden Werten addiert, gefolgt von einer Normalisierung. Abschließend werden die Daten wieder in die ursprüngliche Zeitreihendarstellung transformiert.

   if(!SumAndNormilize(caFreqOut[0].getOutput(),cTranspose.getOutput(),caFreqOut[0].getOutput(),iWindow,true,0,0,0,1))
     return false;
//---
   return CNeuronTransposeOCL::feedForward(caFreqOut[0].AsObject());
  }

Dies schließt die Feed-Forward-Berechnung ab, und die Methode gibt ein boolesches Ergebnis an das aufrufende Programm zurück.

Die nächste Entwicklungsstufe umfasst die Implementierung der Backward-Pass-Prozeduren für das neue Objekt. Besondere Aufmerksamkeit muss der Methode gewidmet werden, die für die Verteilung der Fehlergradienten über alle Komponenten gemäß ihrem Beitrag zum Endergebnis verantwortlich ist: calcInputGradients.

bool CNeuronCATCH::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

Die Methode erhält einen Zeiger auf das Eingabedatenobjekt, das sofort validiert wird. Die Notwendigkeit einer solchen Validierung wurde bereits mehrfach zuvor erörtert.

Als Nächstes wird der von der nachfolgenden Schicht empfangene Gradient in die univariate Sequenzdarstellung transponiert.

   if(!CNeuronTransposeOCL::calcInputGradients(caFreqOut[1].AsObject()))
      return false;
   if(!SumAndNormilize(caFreqOut[1].getGradient(),caFreqOut[1].getGradient(),cTranspose.getPrevOutput(),
                                                                              iWindow,false,0,0,0,0.5f))
     return false;

Die resultierenden Werte werden sofort in den verfügbaren Puffer des Eingabetranspositionsobjekts kopiert, was den Fluss der residualen Verbindung darstellt.

Es ist wichtig, daran zu erinnern, dass die Ausgabe der inversen Fourier-Transformation die erwartete Sequenzlänge überschreiten kann. Während des Forward-Pass haben wir die überschüssigen Werte verworfen. Es sollte hier angemerkt werden, dass wir bei der Durchführung des Forward-Pass die analysierte Zeitreihe mit Nullwerten auf die erforderliche Größe aufgefüllt haben. Daher erwarten wir, im verworfenen Teil der Ergebnisse der inversen Fourier-Transformation ähnliche Nullwerte zu erhalten. Für ein korrektes Trainingsverhalten wird daher der Gradient, der diesem verworfenen Bereich entspricht, auf die zuvor erhaltenen Werte mit umgekehrtem Vorzeichen gesetzt.

   if((caFreqOut[0].Neurons() - iWindow) > 0)
      if(!SumAndNormilize(caFreqOut[1].getOutput(), caFreqOut[1].getOutput(), caFreqOut[1].getOutput(),
                                                                             1, false, 0, 0, 0, -0.5f))
         return false;

Die Gradienten aus beiden Blöcken werden dann zu einem einzigen Tensor zusammengefügt.

   if(!Concat(caFreqOut[1].getGradient(), caFreqOut[1].getOutput(), caFreqOut[0].getGradient(), iWindow,
                                                              caFreqOut[0].Neurons() - iWindow, iCount))
      return false;

An diesem Punkt erhalten wir den Gradienten des Realteils des rekonstruierten Signals. Der Imaginärteil muss jedoch ebenfalls berücksichtigt werden. Die inverse Fourier-Transformation rekonstruiert eine reellwertige Zeitreihe aus ihrer Frequenzdarstellung. Die Zeitreihe selbst wird durch reelle Werte dargestellt, deren Imaginärteil null ist. Daher folgt der Ansatz zur Definition des Fehlergradienten des Imaginärteils derselben Logik wie die verworfene Realkomponente, d. h., wir ändern das Vorzeichen der zuvor erhaltenen Ergebnisse.

   if(!SumAndNormilize(caFreqOut[1].getPrevOutput(), caFreqOut[1].getPrevOutput(), caFreqOut[1].getGradient(),
                                                                                    1, false, 0, 0, 0, -0.5f))
      return false;

Die Gradienten werden dann mittels der schnellen Fourier-Transformation in den Frequenzbereich transformiert.

   if(!FFT(caFreqOut[0].getGradient(), caFreqOut[1].getGradient(), caFreqOut[0].getOutput(), caFreqOut[1].getOutput(),
                                                                                                       iCount, false))
      return false;

Die resultierenden Werte werden zu einem einheitlichen Tensor zusammengefügt, der sowohl die Real- als auch die Imaginärkomponenten des komplexen Gradienten darstellt.

   if(!Concat(caFreqOut[0].getOutput(), caFreqOut[1].getOutput(), caLinearHead[1].getGradient(), 1, 1,
                                                                              caFreqOut[0].Neurons()))
      return false;

Als Nächstes wird der Gradient sequenziell durch alle internen Objekte propagiert. Zuerst durch die finalen Projektionsschichten der Attention-Module.

   if(!caLinearHead[0].calcHiddenGradients(caLinearHead[1].AsObject()))
      return false;

Dann durch die Attention-Module selbst, bis hinunter zu den einbettenden Faltungsschichten.

   CObject *neuron = caLinearHead[0].AsObject();
   for(int i = int(caChannelFusion.Size()) - 1; i >= 0; i--)
     {
      if(!caChannelFusion[i].calcHiddenGradients(neuron))
         return false;
      neuron = caChannelFusion[i].AsObject();
     }

Dieser Prozess setzt sich fort, bis die zusammengefügte Frequenzbereichsdarstellung der Eingabedaten erreicht ist.

   for(int i = int(caProjection.Size()) - 1; i >= 0; i--)
     {
      if(!caProjection[i].calcHiddenGradients(neuron))
         return false;
      neuron = caProjection[i].AsObject();
     }
//---
   if(!cFreqConcat.calcHiddenGradients(neuron))
      return false;

In diesem Stadium wird das Ergebnis in Real- und Imaginärkomponenten aufgeteilt.

   if(!DeConcat(caFreqIn[0].getGradient(), caFreqIn[1].getGradient(), cFreqConcat.getGradient(), 1, 1,
                                                                               caFreqIn[0].Neurons()))
      return false;

Die inverse Fourier-Transformation wird dann angewendet, um den Gradienten zurück in die Zeitbereichsdarstellung zu propagieren.

   if(!FFT(caFreqIn[0].getGradient(), caFreqIn[1].getGradient(), caFreqIn[0].getPrevOutput(),
                                                  caFreqIn[1].getPrevOutput(), iCount, false))
      return false;

Nur der Teil, der den gültigen Eingabedaten entspricht, wird beibehalten.

   if(!DeConcat(cTranspose.getGradient(), caFreqIn[0].getGradient(), caFreqIn[0].getPrevOutput(), iWindow,
                                                        caFreqIn[0].Neurons() / iCount - iWindow, iCount))
      return false;
//---
   if(!SumAndNormilize(cTranspose.getGradient(),cTranspose.getPrevOutput(),cTranspose.getGradient(),iWindow,
                                                                                          false,0,0,0,1.0f))
     return false;

Dieses Ergebnis wird dann zu den zuvor gespeicherten Residual-Verbindungsgradienten addiert.

Schließlich werden die Gradienten zurück in die ursprüngliche Eingaberepräsentation transponiert und gegebenenfalls durch die Ableitung der Aktivierungsfunktion der Eingangsschicht angepasst.

   if(!prevLayer.calcHiddenGradients(cTranspose.AsObject()))
      return false;
   if(prevLayer.Activation() != None)
     {
      if(!DeActivation(prevLayer.getOutput(), prevLayer.getGradient(), prevLayer.getGradient(), prevLayer.Activation()))
         return false;
     }
//---
   return true;
  }

Wir geben das logische Ergebnis der durchgeführten Operationen an das aufrufende Programm zurück und schließen die Ausführung der Methode ab.

Damit schließen wir die Vorstellung unserer Implementierung der im Framework CATCH vorgeschlagenen Ansätze ab. Der vollständige Quellcode aller beschriebenen Komponenten und Methoden ist in den Anhängen enthalten.


Modellarchitektur

Nachdem wir die Implementierungsalgorithmen unseres vorgeschlagenen Ansatzes untersucht haben, diskutieren wir nun kurz die Architektur der trainierbaren Modelle. Wie in der vorherigen Arbeit trainieren wir drei Modelle: einen Umgebungszustands-Encoder, einen Actor und ein Vorhersagemodell für die Wahrscheinlichkeit der nächsten Richtungsbewegung. Das Framework CATCH ist in den Encoder integriert, der für die Repräsentation des beobachteten Zustands der Umgebung verantwortlich ist. Da das gesamte Framework in einem einzigen Objekt gekapselt ist, ist die resultierende Modellarchitektur visuell relativ einfach.

bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&actor, CArrayObj *&probability)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!probability)
     {
      probability = new CArrayObj();
      if(!probability)
         return false;
     }
//--- 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;
     }

Wie üblich ist die erste Komponente eine vollständig verbundene Schicht, die zur Kodierung der Rohdaten verwendet wird, gefolgt von einer Batch-Normalisierungsschicht. Diese Stufe führt eine anfängliche Vorverarbeitung der multivariaten Zeitreihen durch und bringt die univariaten Sequenzen auf eine vergleichbare Skala.

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

Als Nächstes wenden wir unser Modul CATCH an. Hier verwenden wir eine Segmentierung des Frequenzspektrums in 8 Elemente mit einer Schrittweite von 1. Dies ermöglicht eine detailliertere Analyse der gegenseitigen Abhängigkeiten über das gesamte Frequenzspektrum der Eingabedaten hinweg. 

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCATCH;
   prev_count=descr.count = HistoryBars;
     {
      int temp[]={BarDescr,8,32,4};       // Variables, Frequency window, Key Size, Heads
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.step=1;
   int prev_out=descr.windows[0];
   descr.batch = 1e4;
   descr.optimization=ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die resultierenden Repräsentationen werden dann in einen engen normalisierten Bereich umskaliert.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   descr.window_out = BarDescr;
   descr.layers = 1;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Schließlich stellen wir die ursprüngliche Datenverteilung mithilfe einer inversen Normalisierungsschicht wieder her.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = HistoryBars * BarDescr;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Die Architekturen des Actors und des Wahrscheinlichkeitsprognosemodells für die Richtung der bevorstehenden Bewegung bleiben gegenüber der vorherigen Arbeit nahezu unverändert. Interessierte Leser können diese Details eigenständig nachvollziehen. Der vollständige Quellcode ist im Anhang enthalten. Dasselbe gilt für die Skripte zur Interaktion mit der Umgebung und zum Modelltraining, die unverändert übernommen wurden.


Tests

Es wurde ein erheblicher Arbeitsaufwand betrieben, um den Ansatz des Frameworks CATCH in MQL5 zu implementieren und in trainierbare Modelle zu integrieren. Nun ist es Zeit für die entscheidende Phase – die Bewertung der Wirksamkeit der vorgeschlagenen Lösung anhand realer historischer Daten. Dieser Schritt ermöglicht es uns, sowohl die Stärken als auch die Schwächen der Implementierung zu bewerten und ihr Potenzial für weitere Optimierungen zu bestimmen.

Für das Training wurde ein Datensatz unter Verwendung zufälliger Episoden erstellt, die im MetaTrader 5 Strategy Tester generiert wurden. Der Datensatz basiert auf historischen Daten von EURUSD des Zeitrahmens M1, die das gesamte Jahr 2024 abdecken.

Die Modellbewertung wurde anhand historischer Daten von Januar bis März 2025 durchgeführt. Alle experimentellen Parameter wurden unverändert gelassen, um die Objektivität der Ergebnisse zu gewährleisten und eine unvoreingenommene Bewertung der Strategieperformance zu ermöglichen. Dieses Setup garantiert, dass das Modell den Trainingsdatensatz nicht einfach nur auswendig lernt, sondern stattdessen seine Fähigkeit unter Beweis stellt, sich an neue Marktbedingungen anzupassen.

Die Testergebnisse werden im Folgenden vorgestellt.

Während des Testzeitraums führte das Modell 68 Trades aus, von denen 32 mit Gewinn geschlossen wurden, was einer Gewinnrate von etwas über 47% entspricht. Gleichzeitig war der durchschnittliche Gewinn pro Gewinntrade fast doppelt so hoch wie der durchschnittliche Verlust pro Verlusttrade. Als Ergebnis erzielte das Modell über den Testzeitraum eine Gesamtrentabilität mit einem Profit Factor von 1,72.


Schlussfolgerung

In diesen beiden Artikeln haben wir die theoretischen Grundlagen des Frameworks CATCH untersucht, einem innovativen Ansatz, der Fourier-Transformationsverfahren mit Frequenzbereichs-Patching-Mechanismen zur Anomalieerkennung in multivariaten Zeitreihen kombiniert. Sein Hauptvorteil liegt in seiner Fähigkeit, komplexe Muster aufzudecken, die bei einer reinen Zeitbereichsanalyse verborgen bleiben.

Die Darstellung von Daten im Frequenzbereich bietet tiefere Einblicke in die Marktdynamik. Der Frequenz-Patching-Mechanismus verleiht der Analyse Flexibilität und ermöglicht es dem Modell, sich an veränderte Bedingungen in der beobachteten Umgebung anzupassen. Im Gegensatz zu klassischen Methoden ist CATCH nicht auf die Erkennung von starken Preisspitzen oder Ausreißern beschränkt, sondern in der Lage, latente Abhängigkeiten innerhalb der Daten zu identifizieren.

Im praktischen Teil haben wir unsere eigene Version des vorgeschlagenen Ansatzes in MQL5 implementiert, das Modell trainiert und es an realen historischen Daten getestet. Die Ergebnisse deuten auf ein vielversprechendes Potenzial hin; dennoch bleiben einige Fragen bezüglich weiterer Optimierungen offen.


Referenzen


In diesem Artikel verwendete Programme

# Name Typ Beschreibung
1 Research.mq5 Expert Advisor Expert Advisor für die Datenerfassung
2 ResearchRealORL.mq5
Expert Advisor
Expert Advisor für die Datenerfassung mit der Methode Real-ORL
3 Study.mq5 Expert Advisor Expert Advisor für das Modelltraining
4 Test.mq5 Expert Advisor Expert Advisor für die Modelltests
5 Trajectory.mqh Klassenbibliothek Struktur der Beschreibung des Systemzustands und der Modellarchitektur
6 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
7 NeuroNet.cl Codebibliothek OpenCL-Programmcode

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

Beigefügte Dateien |
MQL5.zip (2596.27 KB)
Untersuchung von Regressionsmodellen für kausale Inferenz und algorithmischen Handel Untersuchung von Regressionsmodellen für kausale Inferenz und algorithmischen Handel
Der Artikel untersucht die Möglichkeit, Regressionsmodelle im algorithmischen Handel einzusetzen. Regressionsmodelle ermöglichen im Gegensatz zur binären Klassifizierung die Erstellung flexiblerer Handelsstrategien durch die Quantifizierung vorhergesagter Kursänderungen.
Analyse von Kurs-Zeit-Lücken in MQL5 (Teil II): Erstellung einer Heatmap der Liquiditätsverteilung über die Zeit Analyse von Kurs-Zeit-Lücken in MQL5 (Teil II): Erstellung einer Heatmap der Liquiditätsverteilung über die Zeit
Ein detaillierter Leitfaden zur Erstellung eines Heatmap-Indikators für MetaTrader 5, der die zeitliche Verteilung der Kurse visualisiert. Der Artikel erläutert die mathematischen Grundlagen der Zeitdichteanalyse, bei der jedes Kursniveau von Rot (minimale Verweildauer) bis Blau (maximale Verweildauer) eingefärbt wird.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Quantitative Strategien mit niedriger Handelsfrequenz in MetaTrader 5: (Teil 1) Einrichtung eines OLAP-geeigneten Datenspeichers Quantitative Strategien mit niedriger Handelsfrequenz in MetaTrader 5: (Teil 1) Einrichtung eines OLAP-geeigneten Datenspeichers
Der Artikel skizziert eine praktische Datenpipeline für die quantitative Analyse auf der Grundlage von Parquet-Speicherung, Hive-artigen Partitionen und DuckDB. Er beschreibt die Migration ausgewählter SQLite-Tabellen nach Parquet, die Strukturierung von Marktdaten nach Quelle, Symbol, Zeitrahmen und Datum sowie deren Abfrage mit SQL-Fensterfunktionen. Das Beispiel eines Goldenen Kreuzes veranschaulicht die symbolübergreifende Auswertung von künftigen Renditen. Begleitende Python-Skripte übernehmen den Datendownload, die Konvertierung und die Ausführung.