Neuronale Netze im Trading: Duales Clustering multivariater Zeitreihen (DUET)
Einführung
Multivariate Zeitreihen sind Datenfolgen, bei denen jeder Zeitstempel mehrere miteinander verknüpfte Variablen enthält, die komplexe Prozesse beschreiben. Sie werden häufig in der Wirtschaftsanalyse, im Risikomanagement und in anderen Bereichen eingesetzt, in denen Prognosen für multivariate Daten erforderlich sind. Im Gegensatz zu univariaten Zeitreihen ermöglichen multivariate Reihen die Berücksichtigung von Korrelationen zwischen Variablen und damit die Erstellung genauerer Prognosemodelle.
Auf den Finanzmärkten wird die Analyse multivariater Zeitreihen zur Vorhersage von Vermögenspreisen, zur Schätzung der Volatilität, zur Trenderkennung und zur Entwicklung von Handelsstrategien eingesetzt. Bei der Vorhersage von Aktienkursen werden zum Beispiel Faktoren wie Handelsvolumen, Zinssätze, makroökonomische Indikatoren und Nachrichten berücksichtigt. Alle diese Parameter sind miteinander verknüpft, und ihre gemeinsame Analyse ermöglicht es, Muster zu erkennen, die bei getrennter Betrachtung der einzelnen Variablen nicht erkennbar sind.
Eine zentrale Herausforderung bei der Verarbeitung multivariater Zeitreihen ist die Entwicklung von Methoden, die sowohl zeitliche als auch kanalübergreifende Abhängigkeiten erkennen können. In der Praxis ergeben sich jedoch aufgrund der Variabilität der Daten Schwierigkeiten. In Zeiten von Wirtschaftskrisen ändern sich die Korrelationsstrukturen zwischen den Vermögenswerten, was die Anwendung traditioneller Modelle erschwert.
Die vorhandenen Datenverarbeitungsmethoden lassen sich in drei Kategorien einteilen. Beim ersten Ansatz werden die einzelnen Kanäle unabhängig voneinander analysiert, wobei jedoch die Beziehungen zwischen den Variablen außer Acht gelassen werden. Der zweite Ansatz kombiniert alle Kanäle, was jedoch zu redundanten Informationen und geringerer Genauigkeit führen kann. Der dritte Ansatz ist das Clustering von Variablen, das jedoch die Flexibilität des Modells einschränkt.
Zur Lösung dieser Probleme haben die Autoren des Artikels „DUET: Dual Clustering Enhanced Multivariate Time Series Forecasting“ die Methode DUET vorgeschlagen, die zwei Arten von Clustering kombiniert: das temporale und kanalbasierte Clustering. Zeitliches Clustering (TCM) gruppiert Daten auf der Grundlage ähnlicher Merkmale und ermöglicht es den Modellen, sich an Veränderungen im Laufe der Zeit anzupassen. In der Finanzmarktanalyse können so verschiedene Phasen von Konjunkturzyklen berücksichtigt werden. Das Channel Clustering (CCM) identifiziert Schlüsselvariablen, entfernt Rauschen und verbessert die Vorhersagegenauigkeit. Dadurch lassen sich stabile Beziehungen zwischen Vermögenswerten erkennen, was für die Zusammenstellung diversifizierter Anlageportfolios besonders wichtig ist.
Anschließend werden die Ergebnisse durch das Fusion Module (FM) integriert, das Informationen über zeitliche Muster und kanalübergreifende Abhängigkeiten synchronisiert. Dieser Ansatz ermöglicht genauere Prognosen für komplexe Systeme wie die Finanzmärkte. Experimente, die von den Autoren des Frameworks durchgeführt wurden, haben gezeigt, dass DUET die bestehenden Methoden übertrifft und genauere Prognosen liefert. Es berücksichtigt heterogene zeitliche Muster und die Dynamik von kanalübergreifenden Beziehungen und passt sich an die Datenvariabilität an.
Der DUET-Algorithmus
Die Architektur des Frameworks DUET stellt einen innovativen Ansatz für die Vorhersage multivariater Zeitreihen dar, der eine doppelte Clusterung der Eingabedaten entlang zeitlicher und kanalbezogener Dimensionen verwendet. Dies verbessert die Leistung des Modells und macht seine Ergebnisse besser interpretierbar. Der Ansatz kann mit der Arbeit eines erfahrenen Analysten verglichen werden, der ein komplexes Datensystem in einzelne Blöcke aufteilt und diese zunächst einzeln und dann gemeinsam analysiert, um ein detaillierteres Verständnis zu erhalten. Das Framework von DUET umfasst mehrere Schlüsselmodule, die jeweils eine spezielle Rolle im Datenanalyseprozess spielen:
- Instanz-Normalisierung
- Temporal Clustering Module – TCM
- Channel Clustering Module – CCM
- Fusion Module – FM
- Prediction Module
Durch die Normalisierung der Eingabedaten werden Ausreißer entfernt und starke Schwankungen geglättet, wodurch das Modell robuster gegenüber Unterschieden zwischen Trainings- und Testdatensätzen wird. Dies ist besonders wichtig bei der Analyse von Finanzdaten, wo hochfrequentes Rauschen aussagekräftige Trends verschleiern kann. Die Normalisierung trägt auch dazu bei, die statistischen Merkmale einzelner Zeitsequenzen, die aus unterschiedlichen Quellen stammen, anzugleichen und den Einfluss anomaler Werte zu verringern.
Das Temporal Clustering Module (TCM) analysiert zeitliche Abhängigkeiten und gruppiert Sequenzen in Clustern, ähnlich wie Finanzanalysten Vermögenswerte nach ihrer Volatilität, Liquidität und ihren historischen Eigenschaften klassifizieren. Das Herzstück von TCM ist eine Architektur aus mehreren parallelen Encodern (Mixture of Experts – MoE), die dynamisch die am besten geeigneten Encoder für jedes analysierte Segment auswählt, abhängig von der vorherigen Clusterung der Zeitreihe. Dies gewährleistet eine genaue Darstellung der zeitlichen Abläufe, da verschiedene Datengruppen unterschiedliche Verarbeitungsmethoden erfordern können. Der MoE-Mechanismus schaltet adaptiv zwischen den Encodern um, sodass das Modell effizient mit Zeitreihen unterschiedlicher Art, einschließlich hochfrequenter Marktdaten, arbeiten kann.
Die Encoder analysieren Zeitreihen, die als verborgene Merkmale dargestellt werden, die dann in langfristige und kurzfristige Trends zerlegt werden. Auf diese Weise lassen sich verborgene Muster erkennen, die die Vorhersage künftiger Kursbewegungen auf den Finanzmärkten verbessern.
Das Channel Clustering Module (CCM) führt das Channel Clustering anhand der Frequenzmerkmale von Signalen durch. Dieses Modul bewertet die Korrelationen zwischen den Kanälen, identifiziert die wichtigsten Abhängigkeiten und schließt redundante oder unbedeutende Komponenten aus. Ähnlich wie ein Finanzanalyst, der aussagekräftige makroökonomische und technische Indikatoren auswählt und dabei zufällige Marktschwankungen herausfiltert, hilft CCM, die informativsten Signale zu isolieren.
Die Analyse der Abstände zwischen den Amplitudenvektoren der Frequenzcharakteristik der Kanäle ermöglicht es, korrelierte Signale zu identifizieren und Rauschphänomene zu eliminieren. Dies ist besonders auf den Finanzmärkten nützlich, wo verborgene Beziehungen zwischen Vermögenswerten genutzt werden können, um Arbitragestrategien zu entwickeln oder systematische Risiken zu erkennen.
Das Fusion Module (FM) integriert zeitliche und kanalbezogene Repräsentationen mithilfe eines Mechanismus der maskierten Aufmerksamkeit. Dieser Prozess ähnelt der Analyse komplexer Beziehungen zwischen verschiedenen Marktfaktoren, bei der ein Analyst Informationen aus verschiedenen Quellen zu einem ganzheitlichen Bild zusammenfasst. FM identifiziert die wichtigsten Cluster und filtert irrelevante Signale heraus, wodurch die Vorhersagegenauigkeit verbessert wird. Durch den Einsatz von maskierter Aufmerksamkeit kann die Bedeutung verschiedener Datenkomponenten dynamisch angepasst werden, was die Verarbeitung adaptiver macht. Dies ist von entscheidender Bedeutung für Finanzanwendungen, bei denen sich die Abhängigkeitsstruktur zwischen Vermögenswerten unter dem Einfluss makroökonomischer Ereignisse ändern kann.
In der letzten Phase verwendet das Prediction Module die aggregierten Merkmale, um zukünftige Werte der Zeitreihen vorherzusagen. Dieser Prozess kann mit der Arbeit eines professionellen Anlegers verglichen werden, der auf der Grundlage historischer Marktdaten fundierte Vorhersagen über künftige Kursänderungen trifft. Das Prediction Module verwendet neuronale Netzwerkmethoden, die in der Lage sind, komplexe nichtlineare Beziehungen zu erfassen und sich an mögliche strukturelle Veränderungen in den Daten anzupassen. Die endgültigen Prognosen werden einer inversen Normalisierung unterzogen, die eine Interpretation auf der Ebene der ursprünglichen Daten ermöglicht.
Durch die Anwendung fortschrittlicher maschineller Lerntechniken wie maskierte Aufmerksamkeitsmechanismen, Frequenzbereichsanalyse und Clustering latenter Repräsentationen bietet DUET eine hohe Vorhersagegenauigkeit und Interpretierbarkeit. Es hilft, verborgene Muster in komplexen zeitlichen Abläufen aufzudecken und diese Erkenntnisse zur Optimierung von Handelsstrategien zu nutzen, wo sich herkömmliche Ansätze oft als unzureichend effektiv erweisen. Im Vergleich zu herkömmlichen Methoden, die eine umfangreiche manuelle Abstimmung und das Eingreifen von Experten erfordern, erkennt DUET automatisch die strukturellen Merkmale der Daten und passt sich in Echtzeit an diese an. Dies macht es besonders nützlich für die Analyse hochfrequenter Zeitreihen und die Arbeit in sich schnell verändernden Marktumgebungen.
Nachfolgend ist die Originalvisualisierung des DUET-Frameworks dargestellt.

Implementierung mit MQL5
Nach einer detaillierten Untersuchung der theoretischen Aspekte des DUET-Frameworks gehen wir zum praktischen Teil unserer Arbeit über, in dem wir unsere eigene Interpretation der vorgeschlagenen Ansätze mit MQL5 umsetzen.
Die modulare Architektur von DUET ermöglicht eine schrittweise Entwicklung: Jeder Funktionsblock kann als unabhängiges Element des Systems betrachtet werden. Die Aufteilung der Architektur in autonome Module vereinfacht die Fehlersuche, das Testen und die anschließende Optimierung. Wir beginnen mit der Implementierung des Temporal Clustering Module.
Temporal Clustering Module
Wie bereits erwähnt, umfasst das Modul für das Temporal Clustering mehrere parallel arbeitende Encoder. Im Rahmen dieser Arbeit werden wir eine maximal einfache Encoder-Architektur konstruieren, die aus zwei sequentiellen, voll verknüpften Schichten besteht, zwischen denen eine Nichtlinearität mittels einer Aktivierungsfunktion eingeführt wird. Es ist jedoch zu beachten, dass jeder Encoder separate, unabhängige Segmente mit seinen eigenen trainierbaren Parametern verarbeitet. Um diese Verarbeitung zu organisieren, werden wir Faltungsschichten verwenden. Indem wir die gesamte Sequenz der Eingabedaten in die Schicht einspeisen, legen wir die Größe des Analysefensters fest und setzen die Schrittweite gleich der Segmentgröße. Infolgedessen fungieren die Parameter der Faltungsschicht als Parameter der vollverknüpften Schicht des Encoders, wodurch eine parallele Verarbeitung aller Sequenzsegmente gewährleistet wird. Um die Anzahl der parallel arbeitenden Encoder zu erhöhen, genügt es, die Anzahl der Filter in der Faltungsschicht proportional zu erhöhen.
Damit ist die Organisation des parallelen Encoder-Betriebs festgelegt. Es ist jedoch wichtig zu beachten, dass die Autoren des DUET-Frameworks vorschlagen, nur die relevantesten Encoder zu verwenden. Es wird angenommen, dass die Zeitreihen einer latenten Normalverteilung folgen. Eine Normalverteilung ist bekanntlich durch ihren Mittelwert und ihre Varianz gekennzeichnet. Um die k wahrscheinlichsten latenten Verteilungen auszuwählen, verwenden die Autoren die Noisy-Gating-Methode, die wie folgt dargestellt werden kann:
![]()
Das Hinzufügen von normalverteiltem Rauschen (ε) stabilisiert das Training, während die Softplus-Funktion sicherstellt, dass die Varianz positiv bleibt.
Als Nächstes wählen wir die wahrscheinlichsten k latenten Verteilungen aus und berechnen ihre Gewichte mithilfe der SoftMax-Funktion. Auf diese Weise werden Zeitreihen, die zu denselben k höchstwahrscheinlichen latenten Verteilungen gehören, von einer gemeinsamen Gruppe von Encodern verarbeitet. Die Multiplikation der resultierenden Maske mit den Ausgängen der Encoder ergibt ein gewichtetes Ergebnis und eliminiert den Einfluss irrelevanter Filter.
Nachdem wir die architektonische Lösung festgelegt haben, fahren wir mit der Implementierung fort. Zunächst implementieren wir den Algorithmus zur Auswahl der k wichtigsten Encoder. Die Parametrisierung der Verteilungsparameter für die einzelnen Segmente wird über eine Faltungsschicht organisiert. Der Algorithmus zur Auswahl der k relevantesten Encoder wird jedoch in OpenCL implementiert. Zu diesem Zweck erstellen wir den Kernel TopKgates.
__kernel void TopKgates(__global const float *inputs, __global const float *noises, __global float *gates, const uint k) { size_t idx = get_local_id(0); size_t var = get_global_id(1); size_t window = get_local_size(0); size_t vars = get_global_size(1);
Die Kernel-Parameter umfassen Zeiger auf drei Datenpuffer (Eingabedaten, Rauschen und Ergebnisse) und die Anzahl der auszuwählenden Elemente.
Im Kernelkörper identifizieren wir wie üblich zunächst den aktuellen Thread im Arbeitsraum. In diesem Fall wird ein zweidimensionaler Arbeitsraum verwendet, wobei die Gruppierung in lokale Gruppen entlang der ersten Dimension erfolgt. Diese Dimension fasst Threads zusammen, die sich auf dasselbe Segment beziehen, und entspricht der Anzahl der vom Modell verwendeten Encoder.
Als Nächstes wird der Offset innerhalb der lokalen Datenpuffer bestimmt.
const int shift_logit = var * 2 * window + idx; const int shift_std = shift_logit + window; const int shift_gate = var * window + idx;
Anschließend werden die entsprechenden Eingabedaten geladen.
float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE); float noise = IsNaNOrInf(noises[shift_gate], 0); if(noise != 0) { noise *= Activation(inputs[shift_std], 3); logit += IsNaNOrInf(noise, 0); }
Wenn der Wert des Rauschens ungleich 0 ist, wird der Wert der Variablen logit entsprechend der Varianz und dem Rauschen angepasst.
Als Nächstes müssen wir die k größten Werte von Logit innerhalb einer einzelnen Arbeitsgruppe ermitteln. Zu diesem Zweck erstellen wir ein Array im lokalen Speicher, das als Medium für den Datenaustausch zwischen den Threads in der Arbeitsgruppe dient, und wir deklarieren lokale Hilfsvariablen.
__local float temp[LOCAL_ARRAY_SIZE]; //--- const uint ls = min((uint)window, (uint)LOCAL_ARRAY_SIZE); uint bigger = 0; float max_logit = logit;
Dann definieren wir eine Schleife, die die Elemente der Arbeitsgruppe in einem Schritt durchläuft, der der Größe des lokalen Arrays entspricht.
//--- Top K #pragma unroll for(int i = 0; i < window; i += ls) { if(idx >= i && idx < (i + ls)) temp[idx % ls] = logit; barrier(CLK_LOCAL_MEM_FENCE);
Innerhalb der Schleife speichern die Elemente des aktuellen Fensters ihre Werte im lokalen Array, gefolgt von der obligatorischen Synchronisierung der Threads innerhalb der Arbeitsgruppe.
Danach erstellen wir eine verschachtelte Schleife. Im Verlauf seiner Iterationen berechnet jeder Thread, wie viele Elemente in dem lokalen Array größer sind als der logit-Wert des aktuellen Threads.
for(int i1 = 0; (i1 < min((int)ls,(int)(window-i)) && bigger <= k); i1++) { if(temp[i1] > logit) bigger++; if(temp[i1] > max_logit) max_logit = temp[i1]; } barrier(CLK_LOCAL_MEM_FENCE); }
Gleichzeitig suchen wir nach dem maximalen Wert innerhalb der lokalen Gruppe.
Nach Abschluss aller Iterationen der geschachtelten Schleife synchronisieren wir erneut die Threads der Arbeitsgruppe und fahren erst dann mit der nächsten Iteration der äußeren Schleife fort.
Es ist leicht zu erkennen, dass nur k Threads mit den größten logit-Werten den Schwellenwert für die Anzahl der größeren Elemente nicht überschreiten. Diese Werte werden im Ergebnispuffer gespeichert.
if(bigger <= k) gates[shift_gate] = logit - max_logit; else gates[shift_gate] = MIN_VALUE; }
In allen anderen Fällen wird eine Konstante, die den Mindestwert darstellt, in den Ergebnispuffer geschrieben. Bei der anschließenden Anwendung der SoftMax-Funktion führt dieser Wert zu einem Einflusskoeffizienten von Null.
Der oben beschriebene Kernel realisiert den Vorwärtsdurchlauf für die Auswahl der k relevantesten Encoder im jeweiligen Fall. Um jedoch ein wirklich adaptives Modell zu erstellen, müssen wir auch den Trainingsprozess für die Auswahl des Encoders organisieren. Der oben beschriebene Kernel enthält natürlich keine trainierbaren Parameter. Dennoch werden diese Parameter verwendet, um die vom Kernel verwendeten Eingabedaten zu erzeugen. Daher müssen wir den Fehlergradienten zurück auf die Ebene der Eingabedaten propagieren. Dieser Prozess ist im Kernel TopKgatesGrad implementiert. In seiner Parameterstruktur fügen wir Zeiger auf die Puffer hinzu, die die entsprechenden Fehlergradienten enthalten.
__kernel void TopKgatesGrad(__global const float *inputs, __global float *grad_inputs, __global const float *noises, __global const float *gates, __global float *grad_gates) { size_t idx = get_global_id(0); size_t var = get_global_id(1); size_t window = get_global_size(0); size_t vars = get_global_size(1);
Innerhalb des Kernelkörpers identifizieren wir den aktuellen Ausführungsthread im zweidimensionalen Arbeitsraum. Die Struktur des Arbeitsraums wird vom Kernel des Vorwärtsdurchlaufs übernommen, mit dem Unterschied, dass in diesem Fall die Threads nicht in Arbeitsgruppen gruppiert sind.
Als Nächstes bestimmen wir den Offset in den globalen Datenpuffern, analog zum Algorithmus des Vorwärtsdurchlaufs.
const int shift_logit = var * 2 * window + idx; const int shift_std = shift_logit + window; const int shift_gate = var * window + idx;
Der erste Schritt besteht darin, das dem aktuellen Thread entsprechende Ergebnis des Vorwärtsdurchlaufs zu laden.
const float gate = IsNaNOrInf(gates[shift_gate], MIN_VALUE); if(gate <= MIN_VALUE) { grad_inputs[shift_logit] = 0; grad_inputs[shift_std] = 0; return; }
Wie leicht zu erraten ist, können wir, wenn der erhaltene Wert gleich der Mindestkonstante ist, sofort Nullwerte in den Puffer schreiben, der die Gradienten der Eingabedaten enthält. Ein solcher Wert entspricht dem Ausschluss des Encoders von nachfolgenden Operationen.
Andernfalls laden wir den Wert des Fehlergradienten auf der Ausgangsebene und übertragen ihn sofort auf das entsprechende Element des Gradientenpuffers der Eingangsdaten (den logit-Fehler ).
float grad = IsNaNOrInf(grad_gates[shift_gate], 0); grad_inputs[shift_logit] = grad;
Der Fehlergradient wird natürlich nicht auf den Pegel für das Rauschen übertragen. Wir müssen jedoch noch den Fehlerwert auf der Varianzebene bestimmen. Während des Vorwärtsdurchlaufs wurde die Varianz mit dem Rauschen multipliziert. Daher besteht der nächste Schritt darin, den Wert für das Rauschen zu ermitteln.
float noise = IsNaNOrInf(noises[shift_gate], 0); if(noise == 0) { grad_inputs[shift_std] = 0; return; }
Wenn das Rauschen gleich 0 ist, nimmt die Varianz natürlich nicht an den Operationen des Vorwärtsdurchlaufs teil. Daher speichern wir in solchen Fällen einfach einen Nullgradienten, ohne weitere Operationen durchzuführen.
Im verbleibenden Fall schließlich passen wir den Wert des Fehlergradienten durch den Rauschkoeffizienten und die Ableitung der Aktivierungsfunktion an.
grad *= noise; grad_inputs[shift_std] = Deactivation(grad, Activation(inputs[shift_std], 3), 3); }
Der resultierende Wert wird in den globalen Datenpuffer geschrieben, woraufhin die Kernel-Ausführung abgeschlossen wird.
Der vollständige Quellcode für beide oben beschriebenen Kernel ist im Anhang des Artikels zu finden.
Der nächste Schritt unserer Arbeit besteht darin, diesen Prozess im Hauptprogramm zu organisieren. Zunächst erstellen wir das Objekt CNeuronTopKGates, in dem wir den Algorithmus zur Auswahl der k relevantesten Encoder implementieren. Die Struktur des neuen Objekts wird im Folgenden dargestellt.
class CNeuronTopKGates : public CNeuronSoftMaxOCL { protected: int iK; CBufferFloat cbNoise; CNeuronConvOCL cProjection; CNeuronBaseOCL cGates; //--- virtual bool TopKgates(void); virtual bool TopKgatesGradient(void); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronTopKGates(void) {}; ~CNeuronTopKGates(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint gates, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTopKGates; } //--- 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; //--- virtual uint GetGates(void) const { return cProjection.GetFilters() / 2; } virtual uint GetUnits(void) const { return cProjection.GetUnits(); } };
In der vorgestellten Struktur sehen wir, dass zusätzlich zu den üblichen überschriebenen virtuellen Methoden die Methoden TopKgates und TopKgatesGradient hinzugefügt wurden. Dies sind Wrapper-Methoden für die zuvor beschriebenen Kernel, die auf der Programmseite von OpenCL erstellt wurden. Die Umsetzung erfolgt nach einem Algorithmus, der Ihnen bereits bekannt ist, sodass wir hier nicht näher darauf eingehen werden.
Die internen Objekte sind statisch deklariert, was es uns ermöglicht, den Konstruktor und Destruktor der Klasse leer zu lassen. Die Initialisierung aller deklarierten und geerbten Objekte erfolgt in der Methode Init, deren Parameter Konstanten erhalten, die eine eindeutige Interpretation der Architektur des erzeugten Objekts ermöglichen.
bool CNeuronTopKGates::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint gates, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronSoftMaxOCL::Init(numOutputs, myIndex, open_cl, gates * units_count, optimization_type, batch)) return false; SetHeads(units_count);
Die Operationen der Initialisierungsmethode beginnen mit einem Aufruf der gleichnamigen Methode in der Elternklasse, in der die minimal erforderlichen Prüfungen und die Initialisierung der geerbten Objekte bereits organisiert sind.
Beachten Sie, dass wir in diesem Fall das Funktionsobjekt SoftMax als Elternklasse verwenden. So können wir die Ergebnisse der Auswahl der k relevantesten Encoder in eine probabilistische Darstellung umwandeln, ohne ein zusätzliches internes Objekt zu schaffen. Es reicht aus, die Funktionalität der übergeordneten Klasse zu nutzen.
Nach erfolgreicher Ausführung der Operationen der Methode der übergeordneten Klasse fahren wir mit der Konstruktion des Initialisierungsalgorithmus für die neu deklarierten Objekte fort. Hier initialisieren wir zunächst die Faltungsschicht, die die Verteilungsparameter der analysierten Segmente projiziert.
if(!cProjection.Init(0, 0, OpenCL, window, window, 2 * gates, units_count, 1, optimization, iBatch)) return false; cProjection.SetActivationFunction(None);
Am Ausgang dieser Schicht erwarten wir die Mittelwerte und Varianzen für jeden Encoder in unserem Modell. Folglich ist die Anzahl der Filter in der Faltungsschicht doppelt so hoch wie die angegebene Anzahl der Encoder.
Hier fügen wir auch einen Datenpuffer hinzu, in dem das Rauschen erzeugt wird.
if(!cbNoise.BufferInit(Neurons(), 0) || !cbNoise.BufferCreate(OpenCL)) return false;
Schließlich initialisieren wir eine vollständig verknüpfte Schicht, die die Ergebnisse des zuvor erstellten Kernels für den Vorwärtsdurchlauf TopKgates speichert.
if(!cGates.Init(0, 1, OpenCL, Neurons(), optimization, iBatch)) return false; cGates.SetActivationFunction(None); //--- return true; }
Danach geben wir das logische Ergebnis der Operationen an das aufrufende Programm zurück und beenden die Methode.
Beachten Sie, dass wir in diesem Fall keine Objekte zur Speicherung des Encoders der Wahrscheinlichkeitsverteilung Top-K erstellen. Wir beabsichtigen, die absoluten logit-Werte in den Wahrscheinlichkeitsraum zu konvertieren, indem wir die Funktionalität der übergeordneten Klasse nutzen. Daher sind alle Objekte, die zur Unterstützung dieses Prozesses erforderlich sind, bereits in der übergeordneten Klasse erstellt und initialisiert worden.
Der nächste Schritt ist die Konstruktion der Vorwärtsdurchlaufmethode zur Auswahl der k relevantesten Encoder, implementiert in CNeuronTopKGates::feedForward. Wie üblich enthalten die Parameter dieser Methode einen Zeiger auf das Objekt der Eingabedaten, der sofort an die gleichnamige Methode des Objekts weitergegeben wird, das für die Erzeugung der statistischen Merkmale der Verteilung der analysierten Segmente verantwortlich ist.
bool CNeuronTopKGates::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cProjection.FeedForward(NeuronOCL)) return false;
Als Nächstes ist es wichtig zu beachten, dass die Autoren des DUET-Frameworks vorschlagen, den logit-Werten nur während des Trainings Rauschen hinzuzufügen. Deshalb überprüfen wir die Funktionsweise des Modells und erzeugen gegebenenfalls ein Rauschen.
if(bTrain) { double random[]; if(!Math::MathRandomNormal(0, 1, Neurons(), random)) return false; if(!cbNoise.AssignArray(random)) return false; if(!cbNoise.BufferWrite()) return false; } else if(!cbNoise.Fill(0)) return false;
Andernfalls wird der Puffer für das Rauschen mit Nullen gefüllt.
Anschließend rufen wir die Wrapper-Methode auf, die für die Auswahl der k wichtigsten Encoder zuständig ist.
if(!TopKgates()) return false; //--- return CNeuronSoftMaxOCL::feedForward(cGates.AsObject()); }
Die erhaltenen Ergebnisse werden an die gleichnamige Methode in der übergeordneten Klasse übergeben, die die absoluten Werte in den Wahrscheinlichkeitsraum umrechnet.
Wir geben das logische Ergebnis der durchgeführten Operationen an das aufrufende Programm zurück und schließen die Ausführung der Methode ab.
Wie Sie vielleicht bemerkt haben, handelt es sich bei der Vorwärtsdurchlaufmethode um einen linearen Algorithmus. Dementsprechend folgen auch die Backpropagation-Verfahren einer linearen Struktur. Daher schlage ich vor, ihre eingehende Prüfung einer unabhängigen Studie zu überlassen. Den vollständigen Code dieses Objekts und alle seine Methoden finden Sie im Anhang zu diesem Artikel.
In diesem Stadium haben wir die Algorithmen für die Auswahl der k relevantesten Encoder implementiert, sowohl im Hauptprogramm als auch in OpenCL. Wir können nun mit dem Aufbau der Architektur Mixture of Experts (MoE) fortfahren, die wir im Objekt CNeuronMoE implementieren. Die Struktur des neuen Objekts wird im Folgenden dargestellt.
class CNeuronMoE : public CNeuronBaseOCL { protected: CNeuronTopKGates cGates; CLayer cExperts; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMoE(void) {}; ~CNeuronMoE(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint experts, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMoE; } //--- 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; //--- virtual void TrainMode(bool flag) { bTrain = flag; cGates.TrainMode(bTrain); } };
In der vorgestellten Struktur sehen wir nur zwei interne Objekte. Eines davon ist das zuvor erstellte Objekt, das für die Auswahl der k wichtigsten Encoder verantwortlich ist. Das zweite ist ein dynamisches Array zur Speicherung von Zeigern auf die Encoder-Objekte. Beide Objekte werden statisch deklariert, was es uns ermöglicht, den Konstruktor und den Destruktor leer zu lassen. Die gesamte Initialisierungsarbeit für diese Objekte wird in der Methode Init organisiert.
Die Parameter der Initialisierungsmethode übergeben Konstanten, die eine eindeutige Beschreibung der Architektur des erstellten Objekts liefern. Gleichzeitig wird die Möglichkeit eingeräumt, die Dimensionalität der Ausgabedaten zu ändern.
bool CNeuronMoE::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint experts, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_count, optimization_type, batch)) return false;
Der Algorithmus beginnt mit einem Aufruf der gleichnamigen Methode in der übergeordneten Klasse, in der die Initialisierung der geerbten Objekte und die Validierung der Eingabedaten bereits organisiert wurden.
Als Nächstes initialisieren wir das Objekt, das für die Auswahl der wichtigsten Encoder zuständig ist.
int index = 0; if(!cGates.Init(0, index, OpenCL, window, units_count, experts, top_k, optimization, iBatch)) return false;
Danach gehen wir zur direkten Initialisierung der Encoder-Objekte über. Zunächst bereiten wir ein dynamisches Array und lokale Variablen für die vorübergehende Speicherung von Zeigern auf die erstellten Objekte vor.
cExperts.Clear(); cExperts.SetOpenCL(OpenCL); CNeuronConvOCL *conv = NULL; CNeuronTransposeRCDOCL *transp = NULL;
Die erste erstellte Komponente ist eine Faltungsschicht, die als erste Schicht der Encoder dient. Die Eingabe für dieses Objekt ist der Eingangsdatentensor, der allen Encodern gemeinsam ist. Die Anzahl der Filter in dieser Schicht entspricht dem Produkt aus der Größe des Ergebnistensors eines Encoders und der Gesamtzahl der Encoder im Modell. Mit diesem Ansatz können wir die Werte für alle Encoder parallel berechnen.
index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, window, window, window_out * experts, units_count, 1, optimization, iBatch) || !cExperts.Add(conv)) { delete conv; return false; } conv.SetActivationFunction(SoftPlus);
Um Nichtlinearität zwischen den Encoderschichten einzuführen, verwenden wir SoftPlus als Aktivierungsfunktion.
Als Nächstes müssen wir die zweite Ebene der Encoder hinzufügen. Wie Sie verstehen, muss jeder Encoder seinen eigenen Parametersatz erhalten. Wir können dies umsetzen. Dies ist auch mit einer Faltungsschicht möglich. Wir geben einfach die Anzahl der Encoder in dem Parameter an, der die Anzahl der unabhängigen analysierten Sequenzen darstellt. Es ist jedoch zu beachten, dass der Output der ersten Schicht ein dreidimensionaler Tensor mit den Dimensionen { Units, Encoders, Dimension } ist. Dies entspricht jedoch nicht der Arbeitsweise der zuvor erstellten Faltungsschicht.
Um eine korrekte Verarbeitung zu gewährleisten, müssen wir die ersten beiden Dimensionen vertauschen. Diese Aufgabe wird von einer Transpositionsschicht der Daten übernommen.
transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(0, index, OpenCL, units_count, experts, window_out, optimization, iBatch) || !cExperts.Add(transp)) { delete transp; return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
Danach können wir die Faltungsschicht initialisieren, die als zweite Schicht unserer unabhängigen Encoder dienen wird.
index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, window_out, window_out, window_out, units_count, experts, optimization, iBatch) || !cExperts.Add(conv)) { delete conv; return false; } conv.SetActivationFunction(None);
Schließlich fügen wir eine Schicht für die inverse Datentransposition hinzu.
transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(0, index, OpenCL, experts, units_count, window_out, optimization, iBatch) || !cExperts.Add(transp)) { delete transp; return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); //--- return true; }
Zu diesem Zeitpunkt ist der Initialisierungsalgorithmus für die internen Objekte abgeschlossen. Dann geben wir das logische Ergebnis der Operation an den Aufrufer zurück und beenden die Ausführung der Methode.
Nach Abschluss der Initialisierungsphase implementieren wir den Vorwärtsdurchlaufalgorithmus innerhalb der Methode CNeuronMoE::feedForward.
bool CNeuronMoE::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cGates.FeedForward(NeuronOCL)) return false;
Zu den Methodenparametern gehört ein Zeiger auf das Eingabedatenobjekt, der unmittelbar an die entsprechende Methode des Encoder-Auswahlobjekts übergeben wird.
Als Nächstes arbeiten wir mit den Encodern. Beachten Sie, dass sie dasselbe Eingangsdatenobjekt verwenden, das sie in den Methodenparametern erhalten. Wir speichern diesen Zeiger zunächst in einer lokalen Variablen.
CNeuronBaseOCL *prev = NeuronOCL; int total = cExperts.Total(); for(int i = 0; i < total; i++) { CNeuronBaseOCL *neuron = cExperts[i]; if(!neuron || !neuron.FeedForward(prev)) return false; prev = neuron; }
Dann organisieren wir eine Schleife, die sequentiell über die Encoderschichten iteriert und deren Methoden des Vorwärtsdurchlaufs aufruft.
Nach Beendigung aller Iterationen der Schleife erhalten wir den vollständigen Satz von Ausgaben aller Encoder. Erinnern Sie sich daran, dass wir zuvor bereits die Wahrscheinlichkeitsmaske der relevantesten Encoder für jedes Segment der Eingabedaten erhalten haben. Um die gewichtete Summe für jedes Segment zu erhalten, multiplizieren wir daher einfach den Zeilenvektor der Encoder-Relevanzwahrscheinlichkeiten für das Segment mit der Matrix der Encoder-Ausgänge für dieses Segment.
if(!MatMul(cGates.getOutput(), prev.getOutput(), getOutput(), 1, cGates.GetGates(), Neurons() / cGates.GetUnits(), cGates.GetUnits())) return false; //--- return true; }
Die resultierenden Werte werden im Ergebnispuffer unseres Objekts gespeichert. Die Methode endet mit der Rückgabe eines logischen Ergebnisses an das aufrufende Programm.
An dieser Stelle schließen wir unsere Untersuchung der Algorithmen ab, die zur Konstruktion der Methoden des Encoder-Set-Objekts verwendet werden. Ich schlage vor, die Backpropagation-Methoden dieses Objekts einer eigenständigen Analyse zu überlassen. Wie immer ist der vollständige Quellcode dieses Objekts und aller seiner Methoden im Anhang des Artikels verfügbar.
Heute haben wir einen erheblichen Teil der Arbeit bewältigt und den in diesem Artikel vorgesehenen Umfang praktisch ausgeschöpft. Unsere Arbeit ist jedoch noch nicht beendet. Wir werden eine kurze Pause einlegen und unsere Interpretation der von den Autoren des DUET-Frameworks vorgeschlagenen Ansätze im nächsten Artikel fortsetzen.
Schlussfolgerung
Heute haben wir den DUET-Framework untersucht, der temporales Clustering (TCM) und Channel Clustering (CCM) für multivariate Zeitreihen kombiniert, um die Genauigkeit ihrer Analyse und Vorhersage zu verbessern. TCM passt die Modelle an zeitliche Veränderungen an, während CCM die Schlüsselvariablen identifiziert und das Rauschen reduziert.
Im praktischen Teil des Artikels haben wir eine Implementierung des Temporal Clustering Module (TCM) vorgestellt. Im nächsten Artikel werden wir die begonnene Umsetzung fortsetzen. Wir werden unsere eigene Interpretation der von den Autoren des Frameworks vorgeschlagenen Ansätze vorstellen und die Implementierung abschließen, indem wir das Modell an realen historischen Daten testen.
Liste der Referenzen
In diesem Artikel verwendete Programme
| # | Name | Typ | Beschreibung |
|---|---|---|---|
| 1 | Research.mq5 | Expert Advisor | Expert Advisor für die Probenahme |
| 2 | ResearchRealORL.mq5 | Expert Advisor | Expert Advisor für die Probenahme mit der Methode Real-ORL |
| 3 | Study.mq5 | Expert Advisor | Expert Advisor für das Training des Modells |
| 4 | Test.mq5 | Expert Advisor | Expert Advisor für 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 | Code-Bibliothek | OpenCL-Programmcode |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/17459
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.
Prognose von Renko-Bars mit CatBoost AI
Marktsimulation (Teil 16): Sockets (X)
Marktsimulation (Teil 17): Sockets (XI)
- 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.