English Русский Español 日本語 Português
preview
Neuronale Netze im Handel: Kontrollierte Segmentierung

Neuronale Netze im Handel: Kontrollierte Segmentierung

MetaTrader 5Handelssysteme |
53 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Einführung

Die Aufgabe der geführten Segmentierung erfordert die Isolierung eines bestimmten Bereichs innerhalb einer Punktwolke auf der Grundlage einer natürlichsprachlichen Beschreibung des Zielobjekts. Um diese Aufgabe zu lösen, führt das Modell eine detaillierte Analyse komplexer, feinkörniger semantischer Abhängigkeiten durch und generiert eine punktweise Maske des Zielobjekts. Der Artikel „RefMask3D: Language-Guided Transformer for 3D Referring Segmentation“ stellt einen effizienten und umfassenden Rahmen vor, der sprachliche Informationen umfassend nutzt. Die vorgeschlagene Methode RefMask3D verbessert die multimodalen Interaktions- und Verständnismöglichkeiten.

Die Autoren schlagen die Verwendung einer frühen Phase der Merkmalskodierung vor, um einen reichhaltigen multimodalen Kontext zu extrahieren. Zu diesem Zweck führen sie das Modul „Geometry-Enhanced Group-Word Attention“ (Geometrie-erweiterte Wortgruppen-Aufmerksamkeit) ein, das in jeder Phase der Merkmalskodierung eine kreuz-modale Aufmerksamkeit zwischen der natürlichsprachlichen Objektbeschreibung und lokalen Punktgruppen (Subwolken) ermöglicht. Diese Integration reduziert nicht nur das Rauschen, das typischerweise mit direkten Punkt-Wort-Korrelationen verbunden ist und durch die spärliche und unregelmäßige Beschaffenheit von Punktwolken verursacht wird, sondern nutzt auch intrinsische geometrische Beziehungen und feine strukturelle Details innerhalb der Wolke. Dadurch wird die Fähigkeit des Modells, sowohl mit linguistischen als auch mit geometrischen Daten umzugehen, erheblich verbessert.

Zusätzlich bauen die Autoren ein lernbares „Hintergrund“-Token ein, das verhindert, dass irrelevante linguistische Merkmale mit lokalen Gruppenmerkmalen verwoben werden. Dieser Mechanismus stellt sicher, dass Darstellungen auf Punktebene mit relevanten semantischen linguistischen Informationen angereichert werden, wobei eine kontinuierliche und kontextbewusste Anpassung an den entsprechenden Sprachkontext für jede Gruppe oder jedes Objekt innerhalb der Punktwolke erfolgt.

Durch die Kombination von Computer Vision und der Verarbeitung natürlicher Sprache haben die Autoren eine effektive Strategie zur Identifizierung von Zielobjekten im Decoder entwickelt, die als Linguistic Primitives Construction (LPC) bezeichnet wird. Diese Strategie beinhaltet die Initialisierung einer Reihe verschiedener Primitive, die jeweils bestimmte semantische Attribute wie Form, Farbe, Größe, Beziehungen, Standort usw. darstellen sollen. Durch die Interaktion mit relevanten linguistischen Informationen erhalten diese Primitive entsprechende Attribute.

Durch die Verwendung semantisch angereicherter Primitive innerhalb des Decoders wird der Fokus des Modells auf die verschiedenen Semantiken der Punktwolke verstärkt, wodurch die Fähigkeit zur genauen Lokalisierung und Identifizierung des Zielobjekts erheblich verbessert wird.

Um ganzheitliche Informationen zu sammeln und Objekteinbettungen zu erzeugen, führt das RefMask3D Framework das Object Cluster Module (OCM) ein. Linguistische Primitive werden verwendet, um bestimmte Teile der Punktwolke hervorzuheben, die mit ihren semantischen Attributen korrelieren. Das eigentliche Ziel ist jedoch die Identifizierung des Zielobjekts auf der Grundlage der bereitgestellten Beschreibung. Dies erfordert ein umfassendes Verständnis der Sprache. Dies wird durch die Integration des Objektcluster-Moduls erreicht. Innerhalb dieses Moduls werden zunächst die Beziehungen zwischen den sprachlichen Primitiven analysiert, um gemeinsame Merkmale und Unterschiede in ihren Kernbereichen zu ermitteln. Anhand dieser Informationen werden Abfragen in natürlicher Sprache initiiert. Auf diese Weise können wir diese gemeinsamen Merkmale erfassen, die die endgültige Einbettung bilden, die für die Identifizierung des Zielobjekts unerlässlich ist.

Das vorgeschlagene Objektcluster-Modul spielt eine entscheidende Rolle dabei, dass das Modell ein tieferes, ganzheitlicheres Verständnis sowohl von sprachlichen als auch von visuellen Informationen erreicht.


1. Der Algorithmus RefMask3D

RefMask3D erzeugt eine punktweise Maske eines Zielobjekts, indem es die ursprüngliche Punktwolkenszene zusammen mit der textuellen Beschreibung der gewünschten Attribute analysiert. Die zu analysierende Szene besteht aus N Punkten, von denen jede 3D-Koordinate P und einen zusätzlichen Merkmalsvektor F enthält, der Attribute wie Farbe, Form und andere Eigenschaften beschreibt.

Zunächst wird ein Textkodierer eingesetzt, um aus der Textbeschreibung Einbettungen Ft zu erzeugen. Punktweise Merkmale werden dann mit Hilfe eines Punkt-Encoders extrahiert, der tiefe Wechselwirkungen zwischen der beobachteten geometrischen Form und der Texteingabe über das Modul Geometry-Enhanced Group-Word Attention herstellt. Der Punkt-Encoder fungiert als Backbone, ähnlich wie ein 3D-U-Netz.

Der Linguistic Primitives Constructor erzeugt eine Reihe von Primitiven 𝒪′ zur Darstellung verschiedener semantischer Attribute unter Verwendung informativer linguistischer Hinweise. Dadurch wird die Fähigkeit des Modells verbessert, das Zielobjekt genau zu lokalisieren und zu identifizieren, indem es auf spezifische semantische Signale achtet.

Die linguistischen Primitive 𝒪′, multiskalige Punktmerkmale {𝑭1′,𝑭2′,𝑭3′,𝑭4′} und Sprachmerkmale 𝑭t dienen als Eingabe für einen vierschichtigen kreuz-modalen Decoder, der auf der Architektur des Transformers basiert.

Angereicherte linguistische Primitive und Objektabfragen 𝒪c werden dann an das Object Cluster Module (OCM) weitergeleitet, um die Beziehungen zwischen den Primitiven zu analysieren, ihr semantisches Verständnis zu vereinheitlichen und gemeinsame Merkmale zu extrahieren.

Ein Modul zur Modalitätsfusion wird auf dem Grundgerüst des Bild- und Sprachmodells eingesetzt. Die Autoren integrieren die multimodale Fusion in den Punktcodierer. Eine frühzeitige Zusammenführung von modalübergreifenden Merkmalen verbessert die Effizienz des Zusammenführungsprozesses. Der Mechanismus Geometry-Enhanced Group-Word Attention verarbeitet auf innovative Weise lokale Punktgruppen (Teilwolken) mit geometrisch benachbarten Punkten. Dieser Ansatz reduziert das Rauschen von direkten Punkt-Wort-Korrelationen und nutzt inhärente geometrische Beziehungen innerhalb der Punktwolke, wodurch die Fähigkeit des Modells verbessert wird, linguistische Informationen mit der 3D-Struktur zu verschmelzen.

Die reinen kreuz-modale Aufmerksamkeitsmechanismen haben oft Probleme, wenn einem Punkt keine entsprechenden beschreibenden Wörter zugeordnet sind. Um dieses Problem zu lösen, führen die Autoren lernfähige Hintergrund-Token ein. Diese Strategie ermöglicht es Punkten ohne Bezug zu Textdaten, sich auf eine gemeinsame Hintergrund-Token-Einbettung zu konzentrieren, wodurch Verzerrungen durch nicht verwandte Textassoziationen minimiert werden.

Durch die Einbeziehung von Punkten, die sprachlich nicht übereinstimmen, in das Hintergrundobjekt-Cluster wird der Einfluss irrelevanter Elemente weiter verringert. Dies führt zu Punktmerkmalen, die mit linguistisch begründeten Attributen verfeinert sind, sich auf lokale Zentren konzentrieren und von nicht verwandten Wörtern unbeeinflusst sind. Die Hintergrundeinbettung ist ein trainierbarer Parameter, der die Gesamtverteilung des Datensatzes erfasst und die ursprünglichen Eingabedaten effektiv darstellt. Sie wird ausschließlich in der Phase der Aufmerksamkeitsberechnung verwendet. Durch diesen Mechanismus erreicht das Modell präzisere kreuz-modale Interaktionen, die nicht durch irrelevante Sprachhinweise beeinflusst werden.

Die meisten bestehenden Methoden stützen sich in der Regel auf Zentren, die direkt aus der Punktwolke abgetastet werden. Eine entscheidende Einschränkung dieses Ansatzes ist jedoch die Vernachlässigung des sprachlichen Kontexts, der für eine genaue Segmentierung unerlässlich ist. Wenn nur die am weitesten entfernten Punkte abgetastet werden, weichen die Vorhersagen oft von den tatsächlichen Zielen ab, insbesondere in unübersichtlichen Szenen, was die Konvergenz behindert und zu verpassten Erkennungen führt. Dies ist besonders problematisch, wenn die ausgewählten Punkte das Objekt ungenau darstellen oder an ein einziges beschreibendes Wort gebunden sind. Um dieses Problem zu lösen, schlagen die Autoren die Konstruktion von linguistischen Primitiven vor, die semantische Inhalte beinhalten und es dem Modell ermöglichen, verschiedene semantische Attribute zu lernen, die mit zielrelevanten Objekten verbunden sind.

Diese Primitive werden durch Stichproben aus verschiedenen Gaußschen Verteilungen initialisiert. Jede Verteilung steht für eine bestimmte semantische Eigenschaft. Die Primitive sind so konzipiert, dass sie Attribute wie Form, Farbe, Größe, Material, Beziehungen und Ort kodieren. Jedes Primitiv aggregiert bestimmte sprachliche Merkmale und extrahiert die entsprechenden Informationen. Die linguistischen Primitive dienen dazu, semantische Muster auszudrücken. Wenn sie den Transformer-Decoder durchlaufen, kann dieser ein breites Spektrum an sprachlichen Hinweisen extrahieren und so die Objektidentifizierung in späteren Phasen verbessern.

Jedes linguistische Primitiv konzentriert sich auf bestimmte semantische Muster innerhalb der gegebenen Punktwolke, die mit den jeweiligen linguistischen Attributen korrelieren. Das eigentliche Ziel bleibt jedoch die Identifizierung eines eindeutigen Zielobjekts auf der Grundlage der bereitgestellten Textbeschreibung. Dies erfordert ein umfassendes Verständnis und eine semantische Interpretation der Objektbeschreibung. Um dies zu erreichen, setzen die Autoren das Object Cluster Module ein, das die Beziehungen zwischen sprachlichen Primitiven analysiert und gemeinsame und abweichende Merkmale in ihren Schlüsselregionen identifiziert. Dies fördert ein tieferes Verständnis der beschriebenen Objekte. Ein Mechanismus der Selbstaufmerksamkeit (Self-Attention) wird verwendet, um gemeinsame Merkmale aus den sprachlichen Primitiven zu extrahieren. Bei der Dekodierung werden Objektabfragen als Queries eingeführt, während die durch linguistische Primitive angereicherten gemeinsamen Merkmale als Key-Value (Schlüssel-Werte) dienen. Diese Konfiguration ermöglicht es dem Decoder, sprachliche Erkenntnisse aus den Primitiven in Objektabfragen zusammenzuführen, wodurch Abfragen, die mit dem Zielobjekt verbunden sind, effektiv identifiziert und in 𝒪c′ und eine präzise Identifizierung zu erreichen.

Das vorgeschlagene Objektcluster-Modul ist zwar eine große Hilfe bei der Identifizierung von Zielobjekten, beseitigt aber nicht die Mehrdeutigkeiten, die bei der Inferenz in anderen Anwendungen auftreten können. Solche Zweideutigkeiten können zu falsch positiven Ergebnissen führen. Um dies abzumildern, setzen die Autoren von RefMask3D kontrastives Lernen ein, um das Ziel-Token von anderen zu unterscheiden. Dabei wird die Ähnlichkeit mit der richtigen Textreferenz maximiert und die Ähnlichkeit mit negativen (Nicht-Ziel-)Paaren minimiert.

Im Folgenden wird eine Visualisierung der Methode RefMask3D vorgestellt.



2. Die Implementation in MQL5

Nachdem wir uns mit den theoretischen Aspekten der Methode RefMask3D beschäftigt haben, kommen wir nun zum praktischen Teil unseres Artikels. In diesem Teil werden wir unsere Vision der vorgeschlagenen Ansätze mit MQL5 umsetzen.

In der obigen Beschreibung haben die Autoren der RefMask3D-Methode den komplexen Algorithmus in mehrere Funktionsblöcke unterteilt. Daher erscheint es logisch, unsere Implementierungen entsprechend in Form von entsprechenden Modulen aufzubauen.

2.1 Geometry-Enhanced Group-Word Attention


Wir beginnen mit der Konstruktion des Punkt-Encoders, der in der ursprünglichen Methode das Modul Geometry-Enhanced Group-Word Attention enthält. Wir werden dieses Modul in einer neuen Klasse namens CNeuronGEGWA implementieren. Wie im theoretischen Überblick über RefMask3D erwähnt, ist der Punktcodierer als U-Netz-ähnliches Backbone konzipiert. Dementsprechend haben wir ausgewählt CNeuronUShapeAttention als übergeordnete Klasse ausgewählt, die die für unser Objekt erforderliche Basisfunktionalität bereitstellt. Die Struktur der neuen Klasse ist unten dargestellt.

class CNeuronGEGWA   :  public CNeuronUShapeAttention
  {
protected:
   CNeuronBaseOCL    cResidual;
   CNeuronMLCrossAttentionMLKV   cCrossAttention;
   CBufferFloat      cTemp;
   bool              bAddNeckGradient;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer) override { return false; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                                        CBufferFloat *SecondGradient,
                                        ENUM_ACTIVATION SecondActivation = None) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;

public:
                     CNeuronGEGWA(void)   :  bAddNeckGradient(false) {};
                    ~CNeuronGEGWA(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint heads, uint units_count,
                          uint window_kv, uint heads_kv, uint units_count_kv,
                          uint layers, uint inside_bloks,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronGEGWA;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual CNeuronBaseOCL* GetInsideLayer(const int layer) const;
   virtual void      AddNeckGradient(const bool flag) {  bAddNeckGradient = flag; }
  }; 

Die meisten Variablen und Objekte, die es uns ermöglichen, das U-Net-Backbone zu organisieren, werden von der Elternklasse geerbt. Wir führen jedoch zusätzliche Komponenten für den Aufbau von Mechanismen der kreuz-modalen Aufmerksamkeit ein.

Alle Objekte werden als statisch deklariert, was es uns ermöglicht, den Konstruktor und den Destruktor der Klasse leer zu lassen. Die Initialisierung von geerbten und neu hinzugefügten Objekten wird in der Init-Methode durchgeführt. Wie Sie wissen, erhält diese Methode Parameter, die explizite Informationen über die erforderliche Architektur des zu erstellenden Objekts liefern.

bool CNeuronGEGWA::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                        uint window, uint window_key, uint heads, uint units_count,
                        uint window_kv, uint heads_kv, uint units_count_kv,
                        uint layers, uint inside_bloks,
                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      return false;

Im Hauptteil der Methode rufen wir zunächst die gleichnamige Methode aus der Basisklasse CNeuronBaseOCL auf, die als ultimativer Vorfahre aller unserer neuronalen Schichtobjekte dient.

Beachten Sie, dass wir in diesem Fall eine Methode aus der Basisklasse und nicht aus der unmittelbaren Elternklasse aufrufen. Dies ist auf bestimmte architektonische Merkmale zurückzuführen, die wir für den Aufbau des U-Net-Backbone verwenden. Insbesondere bei der Konstruktion des „neck“ (Hals) verwenden wir die rekursive Erstellung von Objekten. In diesem Stadium müssen wir Komponenten aus einer anderen Klasse verwenden.

Anschließend werden die Objekte der primären Aufmerksamkeit und der Skalierung initialisiert.

   if(!cAttention[0].Init(0, 0, OpenCL, window, window_key, heads, units_count, layers, optimization, iBatch))
      return false;
   if(!cMergeSplit[0].Init(0, 1, OpenCL, 2 * window, 2*window, window, (units_count + 1) / 2, optimization, iBatch))
      return false;

Darauf folgt der Algorithmus zur Erstellung des „neck“ (Hals). Die Art des „Hals“-Objekts hängt von seiner Größe ab. Im Allgemeinen erstellen wir ein Objekt, das dem aktuellen Objekt ähnelt. Wir verringern einfach die Größe des inneren „Halses“ um „1“.

   if(inside_bloks > 0)
     {
      CNeuronGEGWA *temp = new CNeuronGEGWA();
      if(!temp)
         return false;
      if(!temp.Init(0, 2, OpenCL, window, window_key, heads, (units_count + 1) / 2, window_kv, heads_kv, units_count_kv, layers, inside_bloks - 1, optimization, iBatch))
        {
         delete temp;
         return false;
        }
      cNeck = temp;
     }

Für die letzte Schicht verwenden wir einen Kreuzaufmerksamkeits-Block.

   else
     {
      CNeuronMLCrossAttentionMLKV *temp = new CNeuronMLCrossAttentionMLKV();
      if(!temp)
         return false;
      if(!temp.Init(0, 2, OpenCL, window, window_key, heads, window_kv, heads_kv, (units_count + 1) / 2, units_count_kv, layers, 1, optimization, iBatch))
        {
         delete temp;
         return false;
        }
      cNeck = temp;
     }

Anschließend initialisieren wir das Modul für Re-Attention und inverse Skalierung.

   if(!cAttention[1].Init(0, 3, OpenCL, window, window_key, heads, (units_count + 1) / 2, layers, optimization, iBatch))
      return false;
   if(!cMergeSplit[1].Init(0, 4, OpenCL, window, window, 2*window, (units_count + 1) / 2, optimization, iBatch))
      return false;

Danach fügen wir eine Verbindungsschicht der Residuen und ein multimodales Kreuzaufmerksamkeits-Modul hinzu.

   if(!cResidual.Init(0, 5, OpenCL, Neurons(), optimization, iBatch))
      return false;
   if(!cCrossAttention.Init(0, 6, OpenCL, window, window_key, heads, window_kv, heads_kv, units_count, units_count_kv, layers, 1, optimization, iBatch))
      return false;

Außerdem wird ein Hilfspuffer für die temporäre Datenspeicherung initialisiert.

   if(!cTemp.BufferInit(MathMax(cCrossAttention.GetSecondBufferSize(),
                                cAttention[0].Neurons()), 0) ||
      !cTemp.BufferCreate(OpenCL))
      return false;

Am Ende der Initialisierungsmethode ersetzen wir Zeiger auf Datenpuffer, um die Datenkopiervorgänge zu minimieren.

   if(Gradient != cCrossAttention.getGradient())
     {
      if(!SetGradient(cCrossAttention.getGradient(), true))
         return false;
     }
   if(cResidual.getGradient() != cMergeSplit[1].getGradient())
     {
      if(!cResidual.SetGradient(cMergeSplit[1].getGradient(), true))
         return false;
     }
   if(Output != cCrossAttention.getOutput())
     {
      if(!SetOutput(cCrossAttention.getOutput(), true))
         return false;
     }
//---
   return true;
  }

Wir geben dann einen booleschen Wert an das aufrufende Programm zurück, der das Ergebnis der Ausführung der Methodenoperationen angibt.

Nachdem die Initialisierung des neuen Objekts abgeschlossen ist, wird der Vorwärtsdurchlauf-Algorithmus in der Methode feedForward implementiert. Anders als die übergeordnete Klasse benötigt unser neues Objekt zwei Datenquellen. Daher wurde die geerbte Methode, die für die Arbeit mit einer einzigen Datenquelle konzipiert ist, mit einem negativen Teil überschrieben. Die neue Methode wurde dagegen von Grund auf neu entwickelt.

In den Methodenparametern erhalten wir Zeiger auf zwei Eingabedatenobjekte. In diesem Stadium werden jedoch bei beiden keine Validierungsprüfungen durchgeführt.

bool CNeuronGEGWA::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!cAttention[0].FeedForward(NeuronOCL))
      return false;

Zunächst übergeben wir den Zeiger auf eine der Datenquellen an die gleichnamige Methode in der primären Aufmerksamkeitsunterschicht. Die Zeigervalidierung wird bereits intern von dieser Methode durchgeführt. Wir müssen also nur das logische Ergebnis der Ausführung überprüfen. Dann skalieren wir die Ausgabe des Aufmerksamkeitsblocks.

   if(!cMergeSplit[0].FeedForward(cAttention[0].AsObject()))
      return false;

Wir übergeben die skalierten Daten und einen Zeiger auf das zweite Datenquellenobjekt an den „Hals“.

   if(!cNeck.FeedForward(cMergeSplit[0].AsObject(), SecondInput))
      return false;

Wir leiten das erhaltene Ergebnis durch den zweiten Aufmerksamkeitsblock und führen eine inverse Datenskalierung durch.

   if(!cAttention[1].FeedForward(cNeck))
      return false;
   if(!cMergeSplit[1].FeedForward(cAttention[1].AsObject()))
      return false;

Danach fügen wir die restlichen Verbindungen hinzu und führen eine kreuz-modale Abhängigkeitsanalyse durch.

   if(!SumAndNormilize(NeuronOCL.getOutput(), cMergeSplit[1].getOutput(), cResidual.getOutput(), 1, false))
      return false;
   if(!cCrossAttention.FeedForward(cResidual.AsObject(), SecondInput))
      return false;
//---
   return true;
  }

Bevor wir mit der Arbeit an den Methoden des Rückwärtsdurchlaufs beginnen, muss noch ein Punkt diskutiert werden. Schauen Sie sich den Auszug aus der Visualisierung der Methode RefMask3D unten an.

Der springende Punkt dabei ist, dass im Decoder eine kreuz-modale Aufmerksamkeit zwischen den trainierbaren Primitiven und den Zwischenergebnissen unseres Punkt-Encoders stattfindet. Dieser scheinbar einfache Vorgang erfordert in Wirklichkeit einen entsprechenden Fehlergradientenfluss. Natürlich müssen wir die entsprechenden Schnittstellen implementieren, um dies zu unterstützen. Bei der Implementierung der Gradientenverteilung in unserem vereinheitlichten Block RefMask3D berechnen wir zunächst die Decoder-Gradienten und anschließend die Gradienten der Punkt-Encoder. Bei einem klassischen Gradienten-Backpropagation-Modell würde diese Sequenzierung jedoch zum Verlust der vom Decoder übermittelten Gradientendaten führen. Wir erkennen jedoch an, dass diese spezielle Verwendung des Blocks einen Sonderfall darstellt. Daher gibt es in der Methode calcInputGradients zwei Betriebsmodi: einen, bei dem die zuvor gespeicherten Gradienten gelöscht werden (Standardverhalten), und einen, bei dem sie erhalten bleiben (für Sonderfälle wie diesen). Um diese Funktionalität zu ermöglichen, haben wir eine interne Flag-Variable bAddNeckGradient und eine entsprechende Setter-Methode AddNeckGradient eingeführt.

virtual void      AddNeckGradient(const bool flag) {  bAddNeckGradient = flag; }

Aber kommen wir zurück zum Algorithmus des Rückwärtsdurchlaufs. In den Parametern der Methode calcInputGradients erhalten wir Zeiger auf 3 Objekte und eine Konstante der Aktivierungsfunktion der zweiten Datenquelle.

bool CNeuronGEGWA::calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                                      CBufferFloat *SecondGradient,
                                      ENUM_ACTIVATION SecondActivation = -1)
  {
   if(!prevLayer)
      return false;

Im Hauptteil der Methode wird nur die Relevanz des Zeigers auf die erste Datenquelle geprüft. Die übrigen Zeiger werden im Körper der Fehlergradientenverteilungsmethoden der internen Schichten überprüft.

Da wir die Substitution von Zeigern auf Datenpuffer implementiert haben, beginnt der Algorithmus zur Verteilung des Fehlergradienten in der inneren Schicht der kreuz-modalen Aufmerksamkeit.

   if(!cResidual.calcHiddenGradients(cCrossAttention.AsObject(), SecondInput, SecondGradient, SecondActivation))
      return false;

Danach führen wir eine Skalierung der Fehlergradienten durch.

   if(!cAttention[1].calcHiddenGradients(cMergeSplit[1].AsObject()))
      return false;

Dann organisieren wir die Verzweigung des Algorithmus in Abhängigkeit davon, ob wir den zuvor akkumulierten Fehlergradienten erhalten müssen. Wenn der Fehler erhalten bleiben soll, ersetzen wir den Fehler-Gradientenpuffer im „Hals“ durch einen ähnlichen Gradientenpuffer aus der ersten Datenskalierungsschicht. Dabei machen wir uns die folgende Eigenschaft zunutze: Die Größe der Ausgangstensoren der angegebenen Skalierungsschicht ist gleich der des „Halses“. Wir werden den Fehlergradienten später auf diese Ebene übertragen. Daher ist der Betrieb in diesem Fall sicher.

   if(bAddNeckGradient)
     {
      CBufferFloat *temp = cNeck.getGradient();
      if(!cNeck.SetGradient(cMergeSplit[0].getGradient(), false))
         return false;

Anschließend wird der Fehlergradient auf der Ebene des „Halses“ mit der klassischen Methode ermittelt. Wir summieren die Ergebnisse aus den beiden Informationsdämpfen und geben Zeiger auf die Objekte zurück.

      if(!cNeck.calcHiddenGradients(cAttention[1].AsObject()))
         return false;
      if(!SumAndNormilize(cNeck.getGradient(), temp, temp, 1, false, 0, 0, 0, 1))
         return false;
      if(!cNeck.SetGradient(temp, false))
         return false;
     }

Wenn der zuvor akkumulierte Fehlergradient nicht benötigt wird, erhalten wir den Fehlergradienten einfach mit Standardmethoden.

   else
      if(!cNeck.calcHiddenGradients(cAttention[1].AsObject()))
         return false;

Als Nächstes müssen wir den Fehlergradienten durch das Objekt „neck“ propagieren. Dieses Mal verwenden wir die klassische Methode. Hier erhalten wir den Fehlergradienten der zweiten Datenquelle in den temporären Datenspeicher. Später müssen wir die vom Modul für kreuz-modale Aufmerksamkeit erhaltenen Werte des aktuellen Objekts und des Halses addieren.

   if(!cMergeSplit[0].calcHiddenGradients(cNeck.AsObject(), SecondInput, GetPointer(cTemp), SecondActivation))
      return false;
   if(!SumAndNormilize(SecondGradient, GetPointer(cTemp), SecondGradient, 1, false, 0, 0, 0, 1))
      return false;

Anschließend propagieren wir den Fehlergradienten bis zur Ebene der ersten Quelle von Eingabedaten.

   if(!cAttention[0].calcHiddenGradients(cMergeSplit[0].AsObject()))
      return false;
   if(!prevLayer.calcHiddenGradients(cAttention[0].AsObject()))
      return false;

Wir propagieren den Gradienten des Restverbindungsfehlers durch die Ableitung der Aktivierungsfunktion und summieren die Informationen aus den beiden Strömen.

   if(!DeActivation(prevLayer.getOutput(), GetPointer(cTemp), cMergeSplit[1].getGradient(), prevLayer.Activation()))
      return false;
   if(!SumAndNormilize(prevLayer.getGradient(), GetPointer(cTemp), prevLayer.getGradient(), 1, false))
      return false;
//---
   return true;
  }

Die Methode updateInputWeights zum Aktualisieren der Modellparameter ist recht einfach. Wir rufen die entsprechenden Aktualisierungsmethoden der internen Schichten auf, die trainierbare Parameter enthalten. Daher möchte ich Sie ermutigen, deren Implementierungen unabhängig zu erkunden. Die vollständige Implementierung dieser Klasse und aller ihrer Methoden finden Sie im Anhang.

Ich möchte noch ein paar Worte über die Schaffung einer Schnittstelle für den Zugriff auf „Hals“-Objekte hinzufügen. Um diese Funktionalität zu implementieren, haben wir die Methode GetInsideLayer erstellt. In seinen Parametern übergeben wir den Index der gewünschten Ebene.

CNeuronBaseOCL* CNeuronGEGWA::GetInsideLayer(const int layer) const
  {
   if(layer < 0)
      return NULL;

Wird ein negativer Index ermittelt, bedeutet dies, dass ein Fehler aufgetreten ist. In diesem Fall gibt die Methode einen NULL-Zeiger zurück. Ein Wert von Null bedeutet, dass auf die aktuelle Ebene zugegriffen wird. Daher gibt die Methode einen Zeiger auf das Objekt „neck“ zurück.

   if(layer == 0)
      return cNeck;

Andernfalls muss der Hals ein Objekt der entsprechenden Klasse sein, und wir rufen diese Methode rekursiv mit dem um 1 verringerten Index der gewünschten Ebene auf.

   if(!cNeck || cNeck.Type() != Type())
      return NULL;
//---
   CNeuronGEGWA* temp = cNeck;
   return temp.GetInsideLayer(layer - 1);
  }

2.2 Konstruktion linguistischer Primitive


Im nächsten Schritt erstellen wir ein Objekt für das Modul Linguistic Primitives Construction in der Klasse CNeuronLPC. Die Originalvisualisierung dieser Methode wird im Folgenden vorgestellt.

Hier sind Ähnlichkeiten mit dem klassischen Cross-Attention-Block zu erkennen, der die Auswahl der entsprechenden Elternklasse vorschlägt. In diesem Fall verwenden wir die Objektklasse der Kreuzaufmerksamkeit CNeuronMLCrossAttentionMLKV. Die Struktur der neuen Klasse ist unten dargestellt.

class CNeuronLPC  :  public CNeuronMLCrossAttentionMLKV
  {
protected:
   CNeuronBaseOCL    cOne;
   CNeuronBaseOCL    cPrimitives;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) override { return feedForward(NeuronOCL); }
   //---
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override { return calcInputGradients(NeuronOCL); }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) override { return updateInputWeights(NeuronOCL); }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronLPC(void) {};
                    ~CNeuronLPC(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint heads_kv, uint units_count, uint units_count_kv, uint layers, uint layers_to_one_kv, ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override  const   {  return defNeuronLPC;   }
   //---
   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;
  };

Im vorigen Fall haben wir Quellen von Eingabedaten für die Vorwärts- und Rückwärts-Durchläufe hinzugefügt. In diesem Fall ist es jedoch genau andersherum. Obwohl das Modul der Kreuzaufmerksamkeit zwei Datenquellen erfordert, wird in dieser Implementierung nur eine verwendet. Der Grund dafür ist, dass die zweite Datenquelle (die trainierbaren Primitive) intern in diesem Objekt generiert wird.

Um diese trainierbaren Primitive zu erzeugen, definieren wir zwei interne, vollständig verbundene Schichtobjekte. Diese beiden Objekte sind als statisch deklariert, sodass wir den Konstruktor und den Destruktor der Klasse leer lassen können. Die Initialisierung dieser deklarierten und geerbten Objekte wird in der Methode Init durchgeführt.

bool CNeuronLPC::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                      uint window, uint window_key, uint heads, uint heads_kv, 
                      uint units_count, uint units_count_kv, uint layers, 
                      uint layers_to_one_kv, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronMLCrossAttentionMLKV::Init(numOutputs, myIndex, open_cl, window, window_key, 
                                         heads, window, heads_kv, units_count, units_count_kv, 
                                         layers, layers_to_one_kv, optimization_type, batch))
      return false;

In den Parametern dieser Methode erhalten wir Konstanten, mit denen wir die Architektur des zu erstellenden Objekts eindeutig bestimmen können. Im Methodenkörper rufen wir sofort die entsprechende Methode der Elternklasse auf, die die Kontrolle der empfangenen Parameter und die Initialisierung der geerbten Objekte implementiert.

Bitte beachten Sie, dass wir die Parameter der generierten Primitive als Informationen über die primäre Datenquelle verwenden.

Als Nächstes erzeugen wir eine einzelne, vollständig verbundene Schicht, die aus einem Element besteht.

   if(!cOne.Init(window * units_count, 0, OpenCL, 1, optimization, iBatch))
      return false;
   CBufferFloat *out = cOne.getOutput();
   if(!out.BufferInit(1, 1) || !out.BufferWrite())
      return false;

Dann initialisieren wir die Erzeugungsschicht mit einem Primitiv.

   if(!cPrimitives.Init(0, 1, OpenCL, window * units_count, optimization, iBatch))
      return false;
//---
   return true;
  }

Beachten Sie, dass wir in diesem Fall keine positionsbezogene Kodierungsschicht verwenden. Nach der ursprünglichen Logik sind einige Primitive für die Erfassung der Position des Objekts zuständig, während andere seine semantischen Attribute sammeln.

Die Methode FeedForward in dieser Implementierung ist ebenfalls recht einfach. Sie nimmt als Parameter einen Zeiger auf das Eingabedatenobjekt entgegen, und der erste Schritt besteht darin, die Gültigkeit dieses Zeigers zu überprüfen. Ich sollte darauf hinweisen, dass dies bei neueren Methoden für Vorwärtsdurchläufe nicht mehr üblich ist.

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

Solche Prüfungen werden normalerweise intern von verschachtelten Komponenten durchgeführt. In diesem Fall werden jedoch die vom externen Programm empfangenen Daten als Kontext verwendet. Das bedeutet, dass wir beim Aufruf von Methoden interner Objekte auf verschachtelte Mitglieder des bereitgestellten Eingabeobjekts zugreifen müssen. Aus diesem Grund sind wir verpflichtet, die Gültigkeit des eingehenden Zeigers ausdrücklich zu prüfen.

Als Nächstes erzeugen wir den Merkmalstensor.

   if(bTrain && !cPrimitives.FeedForward(cOne.AsObject()))
      return false;

Um die Dauer des Entscheidungsprozesses zu verkürzen, wird dieser Vorgang nur während des Trainings durchgeführt. In der Errichtungsphase bleibt der primitive Tensor statisch und muss daher nicht bei jeder Iteration neu generiert werden.

Der Vorwärtsdurchlauf schließt mit einem Aufruf der Vorwärtsdurchlauf-Methode der übergeordneten Klasse ab. Dieser Methode übergeben wir den generierten Tensor von Primitiven als primäre Datenquelle und die Kontextinformationen aus dem externen Programm als sekundären Input.

   if(!CNeuronMLCrossAttentionMLKV::feedForward(cPrimitives.AsObject(), NeuronOCL.getOutput()))
      return false;
//---
   return true;
  }

Bei der Gradientenfortpflanzungsmethode calcInputGradients werden die Operationen des Vorwärtsdurchlauf-Algorithmus in umgekehrter Reihenfolge durchgeführt.

bool CNeuronLPC::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

Auch hier prüfen wir zunächst den empfangenen Zeiger auf das Quelldatenobjekt. Dann rufen wir die Methode der übergeordneten Klasse mit demselben Namen auf und verteilen den Fehlergradienten zwischen den Primitiven und dem ursprünglichen Kontext.

   if(!CNeuronMLCrossAttentionMLKV::calcInputGradients(cPrimitives.AsObject(), NeuronOCL.getOutput(),
                                                       NeuronOCL.getGradient(),
                                                       (ENUM_ACTIVATION)NeuronOCL.Activation()))
      return false;

Danach fügen wir den Gradienten des primitiven Diversifikationsfehlers hinzu.

   if(!DiversityLoss(cPrimitives.AsObject(), iUnits, iWindow, true))
      return false;
//---
   return true;
  }

Die Fortpflanzung des Fehlergradienten bis hinunter auf die Ebene einer einzelnen Schicht ist von geringem praktischem Wert, sodass wir diesen Vorgang weglassen. Der Algorithmus zur Aktualisierung der Parameter wird ebenfalls einer unabhängigen Untersuchung unterzogen. Den vollständigen Code dieser Klasse und alle ihre Methoden finden Sie im Anhang.

Die nächste Komponente im Ablauf von RefMask3D ist der reine Transformer-Decoder-Block, der den multimodalen Cross-Attention-Mechanismus zwischen der Punktwolke und den erlernbaren Primitiven implementiert. Diese Funktionalität kann mit den von uns entwickelten Tools abgedeckt werden. Wir werden also keinen neuen Block speziell für diesen Zweck erstellen.

Ein weiteres Modul, das wir implementieren müssen, ist das Objekt-Clustering-Modul. Der Algorithmus für dieses Modul wird in der Klasse CNeuronOCM implementiert. Dies ist ein ziemlich komplexes Modul. Es kombiniert zwei Blöcke der Selbstaufmerksamkeit: einen für die Primitive und einen für die semantischen Merkmale. Sie werden durch einen Querbegrenzungsblock ergänzt. Die Struktur der neuen Klasse ist unten dargestellt.

class CNeuronOCM  :  public CNeuronBaseOCL
  {
protected:
   uint              iPrimWindow;
   uint              iPrimUnits;
   uint              iPrimHeads;
   uint              iContWindow;
   uint              iContUnits;
   uint              iContHeads;
   uint              iWindowKey;
   //---
   CLayer            cQuery;
   CLayer            cKey;
   CLayer            cValue;
   CLayer            cMHAttentionOut;
   CLayer            cAttentionOut;
   CArrayInt         cScores;
   CLayer            cResidual;
   CLayer            cFeedForward;
   //---
   virtual bool      CreateBuffers(void);
   virtual bool      AttentionOut(CNeuronBaseOCL *q, CNeuronBaseOCL *k, CNeuronBaseOCL *v,
                                  const int scores, CNeuronBaseOCL *out,
                                  const int units,
                                  const int heads,
                                  const int units_kv,
                                  const int heads_kv,
                                  const int dimension);
   virtual bool      AttentionInsideGradients(CNeuronBaseOCL *q, CNeuronBaseOCL *k, CNeuronBaseOCL *v,
                                              const int scores, CNeuronBaseOCL *out,
                                              const int units, const int heads,
                                              const int units_kv, const int heads_kv,
                                              const int dimension);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; }

public:
                     CNeuronOCM(void) {};
                    ~CNeuronOCM(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint prim_window, uint window_key, uint prim_units, uint prim_heads,
                          uint cont_window, uint cont_units, uint cont_heads,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronOCM; }
   //---
   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 bool      feedForward(CNeuronBaseOCL *Primitives, CNeuronBaseOCL *Context);
   virtual bool      calcInputGradients(CNeuronBaseOCL *Primitives, CNeuronBaseOCL *Context);
   virtual bool      updateInputWeights(CNeuronBaseOCL *Primitives, CNeuronBaseOCL *Context);
  };

Ich glaube, es ist klar, dass die Methoden innerhalb dieser Klasse ziemlich komplexe Algorithmen beinhalten. Jede von ihnen bedarf einer ausführlichen Erläuterung. Allerdings ist das Format dieses Artikels etwas eingeschränkt. Daher schlage ich vor, die Diskussion in einem Folgeartikel fortzusetzen, um einen gründlichen und qualitativ hochwertigen Überblick über die implementierten Algorithmen zu geben. In diesem Artikel werden auch die Testergebnisse der Modelle vorgestellt, die die vorgeschlagenen Ansätze auf realen Daten anwenden.


Schlussfolgerung

In diesem Artikel haben wir die Methode RefMask3D untersucht, die für die Analyse komplexer multimodaler Interaktionen und das Verständnis von Merkmalen entwickelt wurde. Die Methode birgt erhebliches Potenzial für eine Innovation im Bereich des Handels. Durch die Nutzung multidimensionaler Daten kann es sowohl aktuelle als auch historische Muster im Marktverhalten berücksichtigen. RefMask3D setzt eine Reihe von Mechanismen ein, um sich auf die wichtigsten Merkmale zu konzentrieren und gleichzeitig die Auswirkungen von Rauschen und irrelevanten Eingaben zu minimieren.

Im praktischen Teil begannen wir mit der Umsetzung der vorgeschlagenen Konzepte mit MQL5 und entwickelten die Objekte für zwei der vorgeschlagenen Module. Der Umfang der abgeschlossenen Arbeiten geht jedoch über das hinaus, was in einem einzelnen Artikel sinnvollerweise behandelt werden kann. Deshalb werden wir die begonnene Arbeit fortsetzen.

Referenzen

Programme, die im diesem Artikel verwendet werden

#NameTypBeschreibung
1Research.mq5Expert AdvisorEA zum Sammeln von Beispielen
2ResearchRealORL.mq5
Expert Advisor
EA zum Sammeln von Beispielen mit der Real-ORL-Methode
3Study.mq5Expert AdvisorModelltraining EA
4Test.mq5Expert AdvisorModelltraining EA
5Trajectory.mqhKlassenbibliothekStruktur der Systemzustandsbeschreibung
6NeuroNet.mqhKlassenbibliothekEine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
7NeuroNet.clBibliothekOpenCL-Programmcode-Bibliothek

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

Beigefügte Dateien |
MQL5.zip (1950.24 KB)
Von der Grundstufe bis zur Mittelstufe: Array (II) Von der Grundstufe bis zur Mittelstufe: Array (II)
In diesem Artikel werden wir uns ansehen, was ein dynamisches Array und ein statisches Array sind. Gibt es einen Unterschied zwischen der Verwendung des einen oder des anderen? Oder ist es doch dasselbe? Wann sollten Sie den einen und wann den anderen Typ verwenden? Und was ist mit konstanten Arrays? Wir werden versuchen zu verstehen, wofür sie gedacht sind, und die Risiken berücksichtigen, die entstehen, wenn nicht alle Werte im Array initialisiert werden.
MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 60): Inferenzlernen (Wasserstein-VAE) mit gleitendem Durchschnitt und stochastischen Oszillatormustern MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 60): Inferenzlernen (Wasserstein-VAE) mit gleitendem Durchschnitt und stochastischen Oszillatormustern
Wir schließen unsere Betrachtung der komplementären Paarung von MA und stochastischem Oszillator ab, indem wir untersuchen, welche Rolle das Inferenzlernen in einer Situation nach überwachtem Lernen und Verstärkungslernen spielen kann. Es gibt natürlich eine Vielzahl von Möglichkeiten, wie man in diesem Fall das Inferenzlernen angehen kann, unser Ansatz ist jedoch die Verwendung von Variationsautokodierern. Wir untersuchen dies in Python, bevor wir unser trainiertes Modell mit ONNX exportieren, um es in einem von einem Assistenten zusammengestellten Expert Advisor in MetaTrader zu verwenden.
Entwicklung eines Replay-Systems (Teil 70): Das richtige Bestimmen der Zeit (III) Entwicklung eines Replay-Systems (Teil 70): Das richtige Bestimmen der Zeit (III)
In diesem Artikel erfahren Sie, wie Sie die Funktion CustomBookAdd richtig und effektiv nutzen können. Trotz ihrer scheinbaren Einfachheit hat sie viele Nuancen. So können Sie dem Mauszeiger beispielsweise mitteilen, ob ein nutzerdefiniertes Symbol gerade versteigert oder gehandelt wird oder ob der Markt geschlossen ist. Der hier dargestellte Inhalt ist ausschließlich für Bildungszwecke bestimmt. Die Anwendung sollte unter keinen Umständen zu einem anderen Zweck als zum Erlernen und Beherrschen der vorgestellten Konzepte verwendet werden.
Aufbau eines nutzerdefinierten Systems zur Erkennung von Marktregimen in MQL5 (Teil 2): Expert Advisor Aufbau eines nutzerdefinierten Systems zur Erkennung von Marktregimen in MQL5 (Teil 2): Expert Advisor
Dieser Artikel beschreibt den Aufbau eines adaptiven Expert Advisors (MarketRegimeEA) unter Verwendung des Regime-Detektors aus Teil 1. Er wechselt automatisch die Handelsstrategien und Risikoparameter für steigende, volatile oder Seitwärtsmärkte. Praktische Optimierung, Handhabung von Übergängen und ein Indikator für mehrere Zeitrahmen sind enthalten.