Neuronale Netze im Trading: LSTM-Optimierung zur Prognose multivariater Zeitreihen (DA-CG-LSTM)
Einführung
Finanzmärkte sind mehr als nur Zahlen auf Bildschirmen. Sie sind ein dynamisches Umfeld, in dem jeder Tick, jede Kerze und jede Änderung des Handelsvolumens menschliche Emotionen, Erwartungen, Ängste und Hoffnungen widerspiegelt. Diesen Rhythmus zu verstehen und zu lernen, wohin sich Preise entwickeln werden, ist eine Herausforderung, die Händler ständig zu lösen versuchen.
Der Fokus liegt auf der multivariaten Zeitreihe – der klassischen Darstellung von Marktdaten: Kurse von Vermögenswerten im Zeitverlauf, Handelsvolumina, technische Indikatoren und Nachrichtenströme. All dies sind Datenformen, die analysiert, modelliert und letztendlich für Prognosen verwendet werden können.
Bis vor Kurzem verließ sich die Branche hauptsächlich auf bewährte klassische Methoden wie ARIMA, SARIMA und verwandte Modelle. Diese Modelle sind praktisch, interpretierbar und erfordern keine enormen Rechenressourcen. Sie funktionierten bei der Bewältigung von Saisonalität und linearen Abhängigkeiten recht gut, insbesondere unter stabilen Marktbedingungen. Finanzmärkte sind jedoch nicht stationär. Nachrichten beeinflussen Erwartungen, die Anlegerstimmung kann sich innerhalb von Sekunden ändern, algorithmischer Handel erzeugt Resonanzeffekte, und all dies führt zu komplexen, nichtlinearen und oft chaotischen Zusammenhängen. Traditionelle Modelle können die allgemeine Richtung anzeigen, erfassen jedoch nicht die feineren Details.
Das Aufkommen des Deep Learning hat diese Landschaft grundlegend verändert. Rekurrente neuronale Netze (RNNs) ermöglichten es Modellen, historische Dynamiken zu berücksichtigen, waren jedoch weit von einer universellen Lösung entfernt. Ihre Haupteinschränkung ist das sogenannte „Kurzzeitgedächtnis“-Problem. Mit anderen Worten: Solche Modelle können nur einen begrenzten zeitlichen Kontext verarbeiten. Wenn Eingabesequenzen länger werden, verlieren sie schnell wichtige Informationen aus den früheren Teilen einer Zeitreihe.
Um dieses Problem anzugehen, wurden verbesserte Architekturen wie LSTM (Long Short-Term Memory) und GRU (Gated Recurrent Unit) entwickelt. Diese Modelle verliehen Gedächtnis neuronalen Netzen, d. h., die Fähigkeit, wichtige Informationen über längere Zeiträume zu speichern.
Diese Architekturen haben jedoch auch ihre eigenen Einschränkungen. Trotz ihrer Fähigkeit, längerfristige Abhängigkeiten zu erfassen, bleiben sie sehr empfindlich gegenüber der Qualität der Eingabedaten. Insbesondere haben sie Schwierigkeiten in Situationen, in denen kurzfristige Marktschwankungen eine entscheidende Rolle spielen. Solche kurzfristigen Bewegungen werden oft nicht im langfristigen Gedächtnis des Modells gespeichert, insbesondere wenn sie nicht von signifikanten Änderungen im langfristigen Kontext begleitet werden.
Um diese Mängel zu beheben, wurde der Aufmerksamkeitsmechanismus eingeführt. Er ermöglichte es Modellen, sich selektiv auf die relevantesten Segmente einer Zeitreihe zu konzentrieren, unabhängig von ihrem Abstand zum aktuellen Zeitschritt. Im Gegensatz zu LSTM erfordern Aufmerksamkeitsmechanismen keine sequentielle Verarbeitung jedes zeitlichen Schrittes und können sich sofort auf die informativsten Elemente konzentrieren. Dies verbesserte die Fähigkeit neuronaler Netze, langfristige Abhängigkeiten zu erfassen, erheblich, insbesondere bei komplexen multivariaten Zeitreihen.
Dieser Ansatz hat jedoch auch einen Nachteil: Solche Modelle sind zwar hervorragend darin, langfristige Abhängigkeiten zu erfassen, neigen aber dazu, kurzfristige Signale zu übersehen, die von entscheidender Bedeutung sein können. Finanzmärkte dulden keine Verzögerungen – eine einzige Schlagzeile kann Kurse in die Höhe schnellen oder abstürzen lassen. Wenn ein Modell nicht auf solche Ereignisse reagiert, verpasst es die Gelegenheit, seine Position rechtzeitig anzupassen.
Die Autoren des Beitrags „A Dual-Staged Attention Based Conversion-Gated Long Short Term Memory for Multivariable Time Series Prediction“ versuchten, das Beste aus beiden Ansätzen zu kombinieren, und schlugen das neue Framework Dual-Staged Attention Conversion-Gated LSTM (DA-CG-LSTM) vor. Das Modell verwendet einen dualen Aufmerksamkeitsmechanismus. In der ersten Stufe bewertet es die Bedeutung sowohl von Merkmalen als auch von Zeitintervallen; nach der Verarbeitung der Daten durch einen spezialisierten CG-LSTM-Block untersucht es dann erneut zeitliche Abhängigkeiten, verstärkt signifikante Signale und unterdrückt weniger relevante.
Ein zusätzlicher Vorteil des Modells liegt in seinen sorgfältig konzipierten Aktivierungsfunktionen, die sowohl die langfristige Informationsspeicherung als auch die Reaktionsfähigkeit auf kurzfristige Marktschwankungen verbessern.
Der DA-CG-LSTM-Algorithmus
Das Framework DA-CG-LSTM wurde entwickelt, um aussagekräftige Muster aus multivariaten Zeitreihen zu extrahieren. Seine Architektur kombiniert zwei Aufmerksamkeitsschichten mit einem modifizierten rekurrenten Block, der als Conversion-Gated LSTM bekannt ist. Dies macht das Modell besonders effektiv für die Vorhersage dynamischer multivariater Prozesse.
Der Prozess beginnt damit, dass dem Modell eine multivariate Zeitsequenz zugeführt wird. Diese Sequenz wird durch die Matrix X = [x1, x2, ..., xT] ∈ RT*n dargestellt, wobei jede Zeile xt ein Vektor von n Merkmalen zum Zeitpunkt t ist. Das Modell verarbeitet diese Informationen jedoch nicht blind. Es lernt zunächst zu identifizieren, welche Merkmale und welche Zeitschritte tatsächlich wichtig sind. So beginnt die erste Stufe: Aufmerksamkeit auf Eingabemerkmale.
Zunächst analysiert das Modell die Merkmale innerhalb jedes einzelnen Zeitschritts. Es berechnet den Wichtigkeitswert jedes Merkmals xkt zum Zeitschritt t gemäß der folgenden Formel:
![]()
Hier bezeichnen We und be die trainierbaren Gewichte und Bias-Werte einer linearen Schicht, die auf jeden Eingabemerkmalsvektor xt angewendet wird. Diese Schicht transformiert die Rohdaten in eine informativere latente Repräsentation, die für die Relevanzschätzung geeignet ist. Zusätzlich dient der trainierbare Vektor ve als interpretierbare Aufmerksamkeitsmaske, die zur Berechnung eines skalaren Wichtigkeitswerts verwendet wird. Dieses Design ermöglicht es dem Modell, Merkmale flexibel entsprechend ihrem Beitrag zu bewerten, was besonders beim Umgang mit verrauschten, hochdimensionalen Zeitreihen wertvoll ist.
Die resultierenden Wichtigkeitskoeffizienten werden mithilfe der Funktion SoftMax normalisiert, wodurch die Menge der Werte in eine Wahrscheinlichkeitsverteilung über die Merkmale umgewandelt wird:

Diese Koeffizienten werden dann verwendet, um den ursprünglichen Eingabevektor x ̃t anzupassen, wobei jedes Merkmal entsprechend seiner geschätzten Wichtigkeit skaliert wird.
![]()
Aber das ist noch nicht alles. Im nächsten Schritt bewertet das Modell die Wichtigkeit jeder zeitlichen Position und bestimmt, welche Zeitpunkte während der Verarbeitung hervorgehoben werden sollten. Eine ähnliche Struktur wird angewendet:

Die resultierende Sequenz ist eine zeitgewichtete Repräsentation:
![]()
Somit hat das Modell bereits vor Beginn der rekurrenten Verarbeitung seine Aufmerksamkeit auf die Informationen konzentriert, die es als am relevantesten erachtet.
Die Sequenz wird dann in den modifizierten rekurrenten Block, CG-LSTM, geleitet. Hier findet die zweite Stufe des Prozesses statt. Innerhalb der standardmäßigen LSTM-Architektur haben die Autoren die Aktivierungsfunktionen für die Eingabe- und Vergessens-Gates neu gestaltet. Dies geschah, um die Empfindlichkeit des Modells gegenüber kurzfristigen Spitzen zu erhöhen, die mit kurzfristigen Marktveränderungen einhergehen, und gleichzeitig seine Fähigkeit zu stärken, sich an langfristige Informationen zu erinnern.
In einem herkömmlichen LSTM-Block verlässt sich das Eingabe-Gate auf die Sigmoid-Aktivierungsfunktion, um zu bestimmen, welche Informationen beibehalten werden sollen. Sigmoid-Funktionen neigen jedoch zur Sättigung: Wenn Eingangswerte sehr klein oder sehr groß werden, nähern sich ihre Ableitungen null an, was die Lerneffizienz verringert. Die Autoren des Frameworks DA-CG-LSTM schlugen vor, die Sigmoid-Funktion mit der hyperbolischen Tangens-Funktion zu kombinieren, ausgedrückt durch die folgende Formel:
![]()
Dieses Design hilft, eine Sättigung während der frühen Trainingsphasen zu verhindern und gleichzeitig die Empfindlichkeit gegenüber subtilen, aber bedeutsamen Schwankungen zu bewahren. Bei Finanzzeitreihen ist diese Fähigkeit besonders wichtig. Zum Beispiel kann ein plötzlicher Anstieg des Handelsvolumens oder ein Anstieg der Volatilität auf einen Regimewechsel am Markt hindeuten, und die rechtzeitige Erkennung solcher Muster bietet einen erheblichen Prognosevorteil.
Das Vergessens-Gate im CG-LSTM wurde ebenfalls modifiziert. Im Gegensatz zur standardmäßigen LSTM-Architektur verwendet sie eine Funktion, die die Sigmoid-Funktion mit einem transformierten hyperbolischen Tangens-Ausdruck kombiniert:

Diese Formulierung besitzt eine charakteristische Eigenschaft: Ihre Ableitung reicht von 0 bis etwa 2,89, was einen Datendispersionseffekt erzeugt. In der Praxis ermöglicht dies dem Modell, irrelevante oder veraltete Informationen aggressiver zu verwerfen und sich dadurch auf aktuelle Entwicklungen zu konzentrieren. Diese Eigenschaft ist besonders wertvoll auf Finanzmärkten, wo historische Ereignisse schnell an Relevanz verlieren und der Erfolg oft davon abhängt, auf aktuelle Signale zu reagieren.
Die Ausgabe des CG-LSTM-Blocks ist die Sequenz (h1, h2, ..., hT), wobei jedes ht sowohl kurzfristige als auch langfristige Informationen enthält. Das bloße Beibehalten von Informationen reicht jedoch nicht aus – sie müssen auch korrekt abgerufen werden. Dies ist der Zweck der zweiten Aufmerksamkeitsschicht: zeitbezogene Aufmerksamkeit.
In dieser Phase überprüft das Modell effektiv seinen eigenen Speicher. Es berechnet die Wichtigkeit jedes verborgenen Zustands hj relativ zum aktuellen Zeitschritt. Die resultierenden Werte werden mithilfe von SoftMax normalisiert und ein Kontextvektor konstruiert – eine destillierte Essenz der gesamten zeitlichen Historie –, die anschließend von einem zweiten Block CG-LSTM verarbeitet wird.
Unter Verwendung des aktuellen verborgenen Zustands ht zusammen mit dem angereicherten Kontext ct erstellt das Modell seine endgültige Prognose:
![]()
wobei f typischerweise eine voll verbundene Schicht oder eine andere Ausgabekomponente der Architektur ist.
Die Architektur DA-CG-LSTM ist nicht nur intelligent; sie ist sinnvoll. Das Modell speichert nicht alles. Stattdessen trifft es fundierte Entscheidungen. Es reagiert nicht auf jede Instanz von Rauschen, sondern lernt, vorübergehende Schwankungen von aussagekräftigen Mustern zu unterscheiden. Diese Unterscheidung ist bei der Finanzprognose besonders wichtig. Das System kann Signale identifizieren, die denen aus der Vergangenheit ähneln, deren Bedeutung bewerten und seine Vorhersagen anpassen.
In diesem Sinne fungiert DA-CG-LSTM als dynamisches, adaptives System, das durch strukturierte Informationsanalyse lernt, sich weiterentwickelt und Schlussfolgerungen zieht. Seine Stärke liegt nicht nur in seinen mathematischen Formulierungen, sondern auch in seiner konzeptionellen Klarheit:
Aufmerksamkeit + Gedächtnis + Interpretation = aussagekräftige Prognosen
Nachfolgend ist die Visualisierung des Frameworks DA-CG-LSTM der Autoren dargestellt.

Implementierung in MQL5
Nachdem wir die theoretischen Aspekte des Frameworks DA-CG-LSTM untersucht haben, gehen wir nun zum praktischen Teil dieser Arbeit über, in dem wir unsere eigene Implementierung des vorgeschlagenen Ansatzes unter Verwendung von MQL5 vorstellen werden.
Wir beginnen mit der Konstruktion eines modifizierten CG-LSTM-Blocks, der nachfolgend im Diagramm des Autors dargestellt ist.

Es ist wichtig anzumerken, dass der Eingabetensor, ähnlich wie bei einem herkömmlichen LSTM-Block, mit dem im vorherigen Zeitschritt erzeugten verborgenen Zustand verkettet wird. Der resultierende Tensor wird dann verwendet, um vier Entitäten zu konstruieren: drei Gates und eine neue Kontextrepräsentation. Jede dieser Entitäten wird durch eine separate lineare Schicht erzeugt. Ein nichtlineares Verhalten wird jedoch durch die Anwendung unterschiedlicher Aktivierungsfunktionen auf die Ausgaben dieser Schichten eingeführt.
Die Verwendung von vier verschiedenen Aktivierungsfunktionen für die einzelnen Entitäten würde natürlicherweise vier voll verbundene Schichten erfordern, die nacheinander ausgeführt werden. Dies ist eindeutig keine optimale Lösung. Während unserer gesamten Arbeit streben wir danach, die operative Parallelität zu maximieren, da dies sowohl das Modelltraining als auch die Entscheidungsfindung unter realen Bedingungen beschleunigt.
Um dies zu erreichen, haben wir bei der Implementierung des klassischen LSTM-Blocks den gesamten Feed-Forward-Durchlauf innerhalb eines einzigen Kernels organisiert. Die Werte jeder Entität wurden gleichzeitig in parallelen Arbeitsgruppen-Threads berechnet, wobei Zwischenwerte über den lokalen Speicher ausgetauscht wurden. Dieser Ansatz erwies sich als äußerst effizient. Wenn wir jedoch die komplexeren Aktivierungsfunktionssequenzen verwenden, die von den Autoren des Frameworks DA-CG-LSTM vorgeschlagen wurden, benötigen wir zusätzliche Datenpuffer zum Speichern von Zwischenwerten, was den Algorithmus erheblich verkompliziert.
Daher haben wir uns bei dieser Implementierung für einen alternativen Ansatz entschieden. Der Feed-Forward-Durchlauf ist in zwei Stufen unterteilt. In der ersten Stufe werden alle vier Entitäten generiert, ohne Aktivierungsfunktionen anzuwenden. Dies kann entweder mit einer vollständig verbundenen Schicht oder einer Faltungsschicht erreicht werden, deren Ausgabetensorgröße proportional zur Anzahl der generierten Entitäten ist. Bei der Arbeit mit mehrdimensionalen Eingabedaten ist eine Faltungsschicht vorzuziehen, da sie unabhängige Operationen für einzelne univariate Sequenzen ermöglicht.
Die zweite Stufe führt die eigentlichen Berechnungen von CG-LSTM durch, indem die erforderlichen Aktivierungsfunktionen angewendet und die internen Prozesse des Blocks ausgeführt werden.
Natürlich beginnen wir mit der Implementierung der zweiten Stufe im Kontext von OpenCL.
Ändern des OpenCL-Programms
Zuerst implementieren wir den Feed-Forward-Durchlauf unserer Blockversion CG-LSTM innerhalb des CSLSTM_FeedForward-Kernels. Dieser Kernel empfängt Zeiger auf nur drei Datenpuffer.
Einer dieser Puffer enthält die Eingabedaten (verkettet), in denen die Werte der vier Komponenten gespeichert sind, bevor Aktivierungsfunktionen angewendet werden. Um die mit dem globalen Speicherzugriff verbundene Latenz zu minimieren und die Leseleistung zu verbessern, werden die Werte unter Verwendung des Vektortyps float4 gespeichert, wodurch vier aufeinanderfolgende Elemente mit einem einzigen Speicherzugriff abgerufen werden können. Diese Organisation nutzt die Speicherbandbreite effizienter und beschleunigt Berechnungen, insbesondere bei der Verarbeitung großer Mengen von Eingabedaten.
Die verbleibenden zwei Puffer werden zum Speichern der Kontext- und Ausgabewerte verwendet.
__kernel void CSLSTM_FeedForward(__global const float4* __attribute__((aligned(16))) concatenated, __global float *memory, __global float *output) { uint id = (uint)get_global_id(0); uint total = (uint)get_global_size(0); // hidden size uint idv = (uint)get_global_id(1); uint total_v = (uint)get_global_size(1); // variables
Dieser Kernel arbeitet innerhalb eines zweidimensionalen Ausführungsraums, ohne Arbeitsgruppen zu erstellen. Die erste Dimension entspricht der Größe des verborgenen Zustands der Zelle, während die zweite Dimension die Anzahl der univariaten Sequenzen in den Eingabedaten darstellt. Im Kernelkörper identifizieren wir den Thread in allen Dimensionen des Aufgabenraums.
Unter Verwendung dieser Informationen bestimmen wir die geeigneten Offsets in den Datenpuffern und laden sofort den entsprechenden Block der Eingabedaten in eine lokale Variable.
uint shift = id + total * idv;
float4 concat = concatenated[shift];
Als Nächstes wenden wir die erforderlichen Aktivierungsfunktionen auf alle Entitäten an.
float fg = 1 - Activation(1 - 1 / pow(Activation(concat.s0, ActFunc_SIGMOID), 2), ActFunc_TANH); float ig = Activation(Activation(concat.s1, ActFunc_SIGMOID), ActFunc_TANH); float nc = Activation(concat.s2, ActFunc_TANH); float og = Activation(concat.s3, ActFunc_SIGMOID);
Anschließend aktualisieren wir sowohl die Kontextwerte als auch den verborgenen Zustand.
float mem = IsNaNOrInf(memory[shift] * fg + ig * nc, 0); float out = IsNaNOrInf(og * Activation(mem, ActFunc_TANH), 0);
Die resultierenden Werte werden in den entsprechenden Elementen der globalen Datenpuffer gespeichert, wonach die Kernel-Ausführung abgeschlossen ist.
memory[shift] = mem;
output[shift] = out;
} Der resultierende Kernel-Code ist kompakt und leicht zu lesen, was größtenteils der Verwendung einer Hilfsmethode zu verdanken ist, die für die Auswahl der Aktivierungsfunktionen verantwortlich ist. Dieser Ansatz vereinfacht nicht nur die Logik des Kernels selbst, sondern macht den Code auch modularer und erweiterbarer. Darüber hinaus verbessert die Einbindung eines Mechanismus zur Validierung berechneter Werte die Zuverlässigkeit der gesamten Berechnungspipeline.
Nach Abschluss des Feed-Forward-Kernels fahren wir mit der Implementierung der Backpropagation-Operationen fort. Es ist leicht zu erkennen, dass während des Feed-Forward-Durchlaufs keine trainierbaren Parameter verwendet werden. Alle trainierbaren Parameter befinden sich in der neuronalen Schicht, die während der ersten Stufe verwendet wird. Daher erfordert die Implementierung der Backpropagation lediglich, dass wir die Fehlergradienten korrekt auf die beteiligten Komponenten verteilen. Diese Operationen werden innerhalb des CSLSTM_CalcHiddenGradient Kernels ausgeführt.
Die Parameter des Gradientenverteilungs-Kernels werden um die entsprechenden Datenpuffer ergänzt, wobei der ursprüngliche Ausführungsraum beibehalten wird.
__kernel void CSLSTM_CalcHiddenGradient(__global const float4* __attribute__((aligned(16))) concatenated, __global float4* __attribute__((aligned(16))) grad_concat, __global const float* memory, __global const float* grad_output ) { uint id = get_global_id(0); uint total = get_global_size(0); uint idv = get_global_id(1); uint shift = id + total * idv;
Im Kernelkörper identifizieren wir den aktuellen Thread über alle Dimensionen hinweg und berechnen sofort die entsprechenden Offsets in den Datenpuffern.
Es ist zu beachten, dass die Berechnung der Ableitungen der komplexen Aktivierungsfunktionen, die vom DA-CG-LSTM Framework vorgeschlagen wurden, mehrere Zwischenwerte erfordert, die während des Feed-Forward-Durchlaufs absichtlich nicht gespeichert wurden. Das Speichern dieser Werte hätte eindeutig erhebliche Speicherressourcen erfordert. Darüber hinaus ist der Zugriff auf globale Datenpuffer ein kostspieliger Vorgang. Da jedoch der Großteil der Matrixberechnungen an die interne neuronale Schicht der ersten Stufe delegiert wurde, können wir die erforderlichen Werte schnell aus den Pre-Aktivierungswerten der Entitäten berechnen.
Um dies zu erreichen, lesen wir die Pre-Aktivierungsdaten in lokale Variablen ein und berechnen die Funktionen neu, wobei wir Zwischenergebnisse lokal speichern.
float4 concat = concatenated[shift]; // Pre-activation values for all 4 gates // --- Forward reconstruction of gates --- float fg_s = Activation(concat.s0, ActFunc_SIGMOID); float fg = 1.0f - Activation(1.0f - 1.0f / pow(fg_s, 2), ActFunc_TANH); // Forget gate (ft) float ig_s = Activation(concat.s1, ActFunc_SIGMOID); float ig = Activation(ig_s, ActFunc_TANH); // Input gate (it) float nc = Activation(concat.s2, ActFunc_TANH); // New content (ct~) float og = Activation(concat.s3, ActFunc_SIGMOID); // Output gate (ot) float mem = memory[shift]; // New memory state (ct) float mem_t = Activation(mem, ActFunc_TANH); // tanh(ct)
In dieser Phase rekonstruieren wir auch den Speicherzustand aus dem vorherigen Zeitschritt.
// --- Reconstruct previous memory state (t-1) --- float prev_mem = IsNaNOrInf((mem - ig * nc) / fg, 0);
Nach Abschluss der vorbereitenden Berechnungen gehen wir direkt zu den Gradientenverteilungsoperationen über. Zuerst laden wir den Ausgabegradienten aus dem globalen Puffer in eine lokale Variable.
// --- Gradients computation --- float out_g = grad_output[shift];
Als Nächstes verteilen wir diesen Gradienten zwischen den Ausgangs-Gates und dem Kontextspeicher unter Verwendung der Ableitungen der entsprechenden Aktivierungsfunktionen.
float og_g = Deactivation(out_g * mem_t, og, ActFunc_SIGMOID); float mem_g = Deactivation(out_g * og, mem_t, ActFunc_TANH);
Dann propagieren wir den Gradienten des Kontextspeichers zwischen der neuen Kontextprojektion und den Eingangs-Gates.
float nc_g = Deactivation(mem_g * ig, nc, ActFunc_TANH); float ig_g = Deactivation(Deactivation(mem_g * nc, ig, ActFunc_TANH), ig_s, ActFunc_SIGMOID);
Besondere Aufmerksamkeit sollte den Stufen gewidmet werden, die die Ableitungen der jeweiligen Aktivierungsfunktionen beinhalten, welche sequenziell angewendet werden, während die Gradienten der Eingangs-Gates aktualisiert werden.
Schließlich propagieren wir den Fehlergradienten bis zu den Werten des Forget-Gates vor der Aktivierung. Wie bereits erwähnt, schlugen die Autoren des DA-CG-LSTM-Frameworks eine relativ komplexe Aktivierungsfunktion für diese Komponente vor. Folglich muss die Gradienten-Propagierung in mehreren Stufen durchgeführt werden.
Zuerst bestimmen wir den Fehler des Forget-Gates basierend auf dem Gradienten des Kontextspeichers und dessen vorherigem Wert.
// ∂L/∂fg = ∂L/∂ct * mem_(t-1) float fg_g = mem_g * prev_mem;
Der resultierende Wert wird dann unter Verwendung der Ableitung der hyperbolischen Tangensfunktion und der Ableitung des komplexen internen Ausdrucks angepasst.
// Derivative of the complex forget gate: // f(z) = 1 - tanh(1 - 1 / σ(z)^2) float fg_s_g = 2 / pow(fg_s, 3) * Deactivation(-fg_g, fg, ActFunc_TANH); fg_g = Deactivation(fg_s_g, fg_s, ActFunc_SIGMOID);
Abschließend wenden wir die Sigmoid-Ableitung an.
Die resultierenden Werte werden an den entsprechenden Stellen im globalen Datenpuffer gespeichert.
// --- Write back gradients ---
grad_concat[shift] = (float4)(fg_g, ig_g, nc_g, og_g);
}
Damit ist unsere Implementierungs-Arbeit auf Seiten von OpenCL abgeschlossen. Der vollständige Quellcode ist im Anhang enthalten.
Erstellen des CG-LSTM-Objekts
Als Nächstes gehen wir zum Hauptprogramm über. Hier erstellen wir ein neues Objekt, CNeuronCGLSTMOCL, das für die Verwaltung des Betriebs unseres CG-LSTM-Blocks verantwortlich ist. Die Struktur des neuen Objekts wird im Folgenden dargestellt.
class CNeuronCGLSTMOCL : public CNeuronBaseOCL { protected: CNeuronBaseOCL cConcatenateInputs; CNeuronConvOCL cProjection; //--- virtual bool CSLSTM_feedForward(void); virtual bool CSLSTM_CalcHiddenGradient(void); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronCGLSTMOCL(void) {}; ~CNeuronCGLSTMOCL(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, ENUM_OPTIMIZATION optimization_type, uint batch) override; virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual int Type(void) override const { return defNeuronCGLSTMOCL; } virtual bool Clear(void) override; virtual CBufferFloat *getLSTMWeights(void) { return cProjection.GetWeightsConv(); } virtual void SetOpenCL(COpenCLMy *obj) override; virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
In der neuen Klassenstruktur sehen wir einen vertrauten Satz überschriebener Methoden und zwei interne Objekte, deren Funktionalität klarer wird, sobald wir die Klassenmethoden implementieren. Alle internen Objekte werden direkt als Klassenmitglieder deklariert, sodass wir den Konstruktor und Destruktor der Klasse leer lassen können. Die Initialisierung der deklarierten und geerbten Objekte wird innerhalb der Methode Init durchgeführt.
Die Initialisierungsmethode erhält eine Reihe von Konstanten, die die Architektur des zu erstellenden Objekts eindeutig definieren.
bool CNeuronCGLSTMOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, count * variables, optimization_type, batch)) return false; SetActivationFunction(None);
Ein Teil dieser Werte wird unmittelbar an die gleichnamige Methode der Elternklasse weitergegeben, die bereits die Parametervalidierung und die Initialisierung der geerbten Objekte übernimmt.
Beachten Sie, dass wir das Fehlen einer Aktivierungsfunktion für das neu erstellte Objekt explizit angeben.
Nachdem die Initialisierung der Elternklasse erfolgreich abgeschlossen ist, fahren wir mit der Vorbereitung der deklarierten Objekte für den Betrieb fort. In diesem Fall gibt es nur zwei. Die erste ist eine voll verbundene Schicht (Fully Connected Layer), die verwendet wird, um den verketteten Tensor zu speichern, der aus den Eingabedaten und dem während früherer Feed-Forward-Durchläufe erzeugten verborgenen Zustand besteht.
if(!cConcatenateInputs.Init(0, 0, OpenCL, (count + window) * variables, optimization, iBatch)) return false; cConcatenateInputs.SetActivationFunction(None);
Auch hier geben wir das Fehlen der Aktivierungsfunktion explizit an.
Das zweite Objekt ist eine Faltungsschicht, die dafür verantwortlich ist, den verketteten Tensor auf vier Entitäten zu projizieren.
if(!cProjection.Init(0, 1, OpenCL, count + window, count + window, count * 4, 1, variables, optimization, iBatch)) return false; cProjection.SetActivationFunction(None);
Wie bereits früher erörtert, arbeitet auch diese Schicht ohne Aktivierungsfunktion, was wir explizit angeben.
Besondere Aufmerksamkeit sollte der Initialisierung der Faltungsschicht gewidmet werden. Während ihrer Erstellung setzen wir die Sequenzlänge explizit auf eins. Auf den ersten Blick mag dies einschränkend erscheinen; diese Designentscheidung spiegelt jedoch eine bewusste architektonische Entscheidung wider. Gleichzeitig geben wir die Anzahl der univariaten (unabhängigen) Sequenzen an, die parallel verarbeitet werden.
Dieser Ansatz ermöglicht es jeder univariaten Sequenz, ihren eigenen unabhängigen Satz trainierbarer Parameter beizubehalten – ihre eigene Gewichtsmatrix. Infolgedessen kann jede Sequenz unabhängig analysiert und trainiert werden. Jede Sequenz lernt in ihrem eigenen Kontext und reagiert auf spezifische Muster und Gesetzmäßigkeiten, ohne Parameter mit anderen Sequenzen zu teilen. Folglich erhalten wir eine ausdrucksstärkere, anpassungsfähigere und strukturell flexiblere Darstellung der Eingabedaten, insbesondere bei Aufgaben, bei denen verschiedene zeitliche Teilsequenzen unterschiedliche semantische Funktionen erfüllen oder individuelle Marktfaktoren repräsentieren.
Eine solche Parameterisolierung spielt auch während des Trainings eine wichtige Rolle. Erstens reduziert sie die gegenseitige Beeinflussung zwischen den Kanälen und mildert dadurch das Overfitting auf dominante Muster ab. Zweitens kann sich jede einzelne univariate Sequenz auf ihre eigenen Datenmerkmale konzentrieren. Diese Art des differenzierten Lernens macht das Modell nicht nur genauer für spezifische Aufgaben, sondern auch deutlich robuster gegenüber sich ändernden Marktbedingungen.
Darüber hinaus fördern unabhängig trainierte univariate Filter eine bessere Generalisierung. Das Modell neigt weniger dazu, Daten auswendig zu lernen, und extrahiert eher allgemeine Muster. Dies ist besonders wichtig für Finanzzeitreihen, bei denen historische Daten einzigartige, nicht repräsentative Ereignisse enthalten können. Durch die Zerlegung des Lernprozesses in mehrere isolierte Zweige wird das Modell in die Lage versetzt, typische Marktsignale auch in bisher unbekannten Situationen zu erkennen.
Schließlich ist eines der bestimmenden Merkmale rekurrenter Modelle ihre Abhängigkeit von Daten aus dem vorherigen Feed-Forward-Durchlauf. Daher löschen wir alle Datenpuffer, bevor wir den Initialisierungsprozess abschließen.
if(!Clear()) return false; //--- return true; }
Die nächste Stufe unserer Arbeit ist die Implementierung der Forward-Pass-Operationen, die wir in der Methode feedForward umsetzen. Dieser Teil ist relativ einfach. Die Methode erhält einen Zeiger auf das Eingabedatenobjekt, das sofort validiert wird.
bool CNeuronCGLSTMOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
Als Nächstes extrahieren wir die Dimensionen sowohl des Eingabetensors als auch des verborgenen Zustands aus den Parametern der Projektionsschicht.
int hidden = (int)cProjection.GetFilters() / 4; int inputs = (int)cProjection.GetWindow() - hidden; int variables = (int)cProjection.GetVariables();
Anschließend verketten wir die Eingabedaten mit den Ergebnissen des vorherigen Feed-Forward-Durchlaufs für jede univariate Sequenz.
if(!Concat(NeuronOCL.getOutput(), getOutput(), cConcatenateInputs.getOutput(), inputs, hidden, variables)) return false;
Die resultierenden Werte werden in vier Entitäten projiziert.
if(!cProjection.FeedForward(cConcatenateInputs.AsObject())) return false;
An dieser Stelle bleibt nur noch der Aufruf der Wrapper-Methode, die den zuvor erstellten Feed-Forward-Kernel CSLSTM_feedForward zur Ausführung in die Warteschlange einreiht.
return CSLSTM_feedForward();
}
Die Methoden zum Platzieren von Kernels in der Ausführungswarteschlange folgen der gleichen Architektur, die in früheren Artikeln besprochen wurde, daher werden wir ihren internen Algorithmus hier nicht im Detail untersuchen.
Nachdem die Feed-Forward-Implementierung abgeschlossen ist, gehen wir zu den Backpropagation-Verfahren über. Wie Sie wissen, besteht dieser Prozess aus zwei Stufen: der Verteilung der Fehlergradienten und der Optimierung der trainierbaren Parameter.
In diesem Fall existieren die trainierbaren Parameter nur innerhalb der Projektionsschicht der verketteten Daten. Daher reduziert sich die Parameteroptimierung auf den Aufruf der entsprechenden Optimierungsmethode der Projektionsschicht.
bool CNeuronCGLSTMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { return cProjection.UpdateInputWeights(cConcatenateInputs.AsObject()); }
Der Algorithmus zur Verteilung der Fehlergradienten auf die beteiligten Komponenten, der in calcInputGradients implementiert ist, ist etwas komplexer. Die Methode erhält einen Zeiger auf das Eingabedatenobjekt. Es handelt sich um dasselbe Objekt, das auch beim Forward-Pass verwendet wird. Diesmal müssen wir jedoch den Fehlergradienten entsprechend dem Beitrag der Eingabedaten zum Endergebnis des Modells in dieses propagieren.
bool CNeuronCGLSTMOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
Im Hauptteil der Methode wird sofort die Gültigkeit des empfangenen Zeigers überprüft. Die Notwendigkeit dieses Kontrollpunkts wurde in früheren Artikeln wiederholt diskutiert.
Als Nächstes bestimmen wir, ähnlich wie bei der Feed-Forward-Methode, die Dimensionen der Eingabedaten und des verborgenen Zustands.
int hidden = (int)cProjection.GetFilters() / 4; int inputs = (int)cProjection.GetWindow() - hidden; int variables = (int)cProjection.GetVariables();
Wir beginnen damit, den Fehlergradienten auf die Entitäten zu verteilen, indem wir die Wrapper-Methode aufrufen, die den entsprechenden Kernel in die Ausführungswarteschlange einreiht.
if(!CSLSTM_CalcHiddenGradient()) return false;
Dann propagieren wir den Gradienten bis hinunter zum verketteten Eingabetensor.
if(!cConcatenateInputs.calcHiddenGradients(cProjection.AsObject())) return false;
Unter Verwendung der umgekehrten Verkettung extrahieren wir den Fehlergradienten, der den ursprünglichen Eingabedaten entspricht.
if(!DeConcat(NeuronOCL.getGradient(), getPrevOutput(), cConcatenateInputs.getGradient(), inputs, hidden, variables)) return false;
Es ist erwähnenswert, dass wir während der Initialisierung der internen Objekte die Aktivierungsfunktionen absichtlich deaktiviert haben. Dies schließt jedoch die Verwendung von Aktivierungsfunktionen innerhalb der Eingabedaten selbst nicht aus. Daher prüfen wir, ob eine Aktivierungsfunktion mit den Eingabedaten verknüpft ist, und passen bei Bedarf die resultierenden Gradienten mithilfe der Ableitungen der entsprechenden Aktivierungsfunktionen an.
if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(), NeuronOCL.Activation())) return false; //--- return true; }
Die Methode schließt mit der Rückgabe des logischen Ergebnisses der Operation an den Aufrufer ab.
Damit ist unsere Untersuchung der Algorithmen abgeschlossen, die zur Konstruktion der Methoden der neuen Klasse CNeuronCGLSTMOCL verwendet werden. Der vollständige Quellcode für diese Klasse und alle ihre Methoden ist in den Anhängen verfügbar.
Allmählich, fast ohne es selbst zu bemerken, haben wir die praktischen Grenzen des aktuellen Artikels erreicht. Die logische Vervollständigung dieser Forschung erfordert jedoch eine Fortsetzung. Wir machen hier erst einmal eine Pause. Fortsetzung folgt – und der nächste Teil wird nicht weniger umfangreich sein.
Schlussfolgerung
In diesem Artikel haben wir die theoretischen Grundlagen des Frameworks DA-CG-LSTM untersucht. Im Gegensatz zu herkömmlichen Modellen umfasst seine Architektur mehrere innovative Mechanismen, darunter CG-LSTM und einen Dual-Attention-Mechanismus, die beide eine tiefere und genauere Extraktion von Abhängigkeiten aus Daten ermöglichen. Diese Komponenten ermöglichen es dem Modell, komplexe zeitliche Beziehungen effektiv zu verarbeiten und gleichzeitig sowohl langfristige als auch kurzfristige Muster zu erfassen.
Im praktischen Teil haben wir unsere eigene Implementierung des CG-LSTM-Blocks unter Verwendung von MQL5 vorgestellt. Diese Arbeit ist jedoch noch nicht abgeschlossen, und wir werden sie im nächsten Artikel fortsetzen, um das Projekt zu einem logischen Abschluss zu bringen.
Referenzen
- A Dual-Staged Attention Based Conversion-Gated Long Short Term Memory for Multivariable Time Series Prediction
- Andere Artikel dieser Serie
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 Offline-Modelltraining |
| 4 | StudyOnline.mq5 | Expert Advisor | Expert Advisor für das Online-Modelltraining |
| 4 | Test.mq5 | Expert Advisor | Expert Advisor für das Modelltesten |
| 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/17901
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Marktsimulation (Teil 24): Positionsansicht (II)
Von der Grundstufe zur Mittelstufe: Objekte (III)
Von der Grundstufe zur Mittelstufe: Objekte (IV)
Neuronale Netze im Trading: Actor–Director–Critic (Letzter Teil)
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
Und wie lauten die genauen Modellparameter (Größe der Eingabedaten, Anzahl der Neuronen, Optimierer)?
Guten Tag. Wo kann man die Bibliotheken „NeuroNet.mqh“, „NeuroNet.cl“ und „Trajectory.mqh“ herunterladen?
Und wie lauten die genauen Modellparameter (Größe der Eingabedaten, Anzahl der Neuronen, Optimierer)?
Guten Tag, Vladimir.
Alle NeuroNet.*-Bibliotheken befinden sich im Anhang „MQL5\Experts\NeuroNet_DNG\NeuroNet.*“, Trajectory.mqh hingegen in „MQL5\Experts\DACGLSTM\Trajectory.mqh“.
Eine detaillierte Beschreibung der trainierbaren Modelle wird im nächsten Artikel vorgestellt.