Neuronale Netze leicht gemacht (Teil 23): Aufbau eines Tools für Transfer Learning

Dmitriy Gizlyk | 20 Oktober, 2022

Inhalt


Einführung

Wir tauchen weiter ein in die Welt der künstlichen Intelligenz. Heute lade ich Sie ein, sich mit der Technologie des Transfer Learning vertraut zu machen. Wir haben diese Technologie bereits in verschiedenen Artikeln erwähnt, sie aber noch nie eingesetzt. Inzwischen ist dies ein leistungsfähiges Werkzeug, das die Effizienz der Entwicklung neuronaler Netze erhöht und die Kosten für deren Training reduziert.


1. Der Zweck von Transfer Learning

Was ist Transfer Learning und warum brauchen wir es? Transfer Learning ist eine Methode des maschinellen Lernens, bei der das Wissen eines Modells, das zur Lösung eines Problems trainiert wurde, als Grundlage für die Lösung neuer Probleme wiederverwendet wird. Um neue Probleme zu lösen, wird das Modell natürlich vorher zusätzlich auf neue Daten trainiert. Im Allgemeinen kann mit einem richtig ausgewählten Spendermodell ein zusätzliches Training viel schneller und mit besseren Ergebnissen durchgeführt werden als das Training eines ähnlichen Modells von Grund auf.

Es ist möglich, das gesamte Spendermodell oder einen Teil davon zu verwenden.

Ähnlich verhält es sich mit dieser Technologie, wenn wir die Ergebnisse der Clusterbildung und Datenkomprimierung zur Vorverarbeitung der Quelldaten für das neuronale Netz verwenden. In diesem Fall haben wir das gesamte vortrainierte Modell verwendet. Beim Aufbau eines Modells zur Lösung neuer Probleme haben wir jedoch kein zusätzliches Training des Gebermodells durchgeführt. Wir haben es nur zur Vorverarbeitung der „rohen“ Quelldaten verwendet und mit diesen Daten ein neues Modell trainiert.

Als wir mit der Untersuchung von Autoencodern begannen, sprachen wir auch über die Möglichkeit, Transfer Learning nach dem Modelltraining einzusetzen. Aber in diesem Fall können wir den kompletten Autoencoder nicht vollständig als Spendermodell verwenden, da wir ihn darauf trainiert haben, die Originaldaten zu komprimieren und sie dann aus der komprimierten Darstellung wiederherzustellen. Es ist daher nicht sinnvoll, den gesamten Autoencoder als Spendermodell zu verwenden. Für die Vorverarbeitung der Daten wäre es viel effizienter, nur den Encoder zu verwenden. In diesem Fall wird das Gesamtmodell kleiner und die Effizienz der weiteren Schichten höher, da weniger trainierbare Gewichte benötigt werden, um die gleiche Menge an Informationen zu verarbeiten.

Die Anwendung von Transfer Learning ist jedoch nicht auf unüberwachte Lernergebnisse beschränkt. Denken Sie daran, wie oft Sie Ihr Modell neu trainieren mussten, wenn Sie auch nur eine neuronale Schicht hinzufügen oder entfernen wollten. In diesem Fall könnte ein Teil der neuronalen Schichten wiederverwendet werden.

Es gibt noch einen weiteren Anwendungsbereich für diese Technologie. Aufgrund des Fading-Gradienten-Problems ist es nahezu unmöglich, ein Deep Model vollständig zu trainieren. Die Verwendung von Transfer Learning ermöglicht es, neuronale Schichten blockweise zu trainieren und die Größe des Modells schrittweise zu erhöhen.

Natürlich gibt es noch viele andere Einsatzmöglichkeiten für diese Technologie, die Sie erkunden können. Lassen Sie uns nun über ein Instrument nachdenken, das seine Verwendung ermöglichen würde.


2. Erstellen des Werkzeugs

Legen wir zunächst den Zweck des Tools fest, das wir erstellen wollen, aber speichern wir erst einmal unsere trainierten Modelle. Sie werden alle in einer einzigen Binärdatei gespeichert. Jedes Modellobjekt hat seine eigene strenge Datenaufzeichnungsstruktur. Daher wird es schwierig sein, einen Teil der Daten im Editor einfach aus der Datei zu entfernen. Wir müssen also das gesamte trainierte Modell aus der Datei laden, die erforderlichen Manipulationen vornehmen und das neue Modell in einer neuen Datei speichern oder die vorherige Datei überschreiben. Eine neue Datei ist vorzuziehen, da das Spendermodell weiter verwendet werden kann, um die Probleme zu lösen, für die es trainiert wurde.

Außerdem funktionieren unsere neuronalen Netze nur mit den Daten gut, auf denen sie trainiert wurden. Das Ergebnis kann bei völlig neuen Daten unvorhersehbar sein. Dies gilt auch für einzelne neuronale Schichten. Daher können wir beim Transfer-Lernen nur aufeinanderfolgende neuronale Schichten verwenden, beginnend mit der Eingabedatenschicht. Sie können einen Block nicht aus der Mitte oder dem Ende des Modells herausziehen. Das heißt, wir können das gesamte Spendermodell oder mehrere seiner ersten Schichten verwenden. Dann fügen wir mehrere verschiedene neuronale Schichten hinzu und speichern das neue Modell.

Gleichzeitig müssen wir die volle Funktionsweise des neuen Modells sowohl im Trainingsmodus als auch im Betrieb sicherstellen. Natürlich muss das Modell zunächst trainiert werden.

Bitte beachten Sie die folgenden Hinweise. Die neuronalen Schichten des Spendermodells behalten ihre Gewichte bei. Außerdem behalten sie ihr gesamtes Wissen, das sie in der Phase vor dem Modelltraining erworben haben. Die neuen neuronalen Schichten erhalten zufällige Gewichte, genau wie bei der Initialisierung des Modells. Wenn wir mit dem Training eines neuen Modells beginnen, wie wir es zuvor getan haben, dann werden wir mit dem Training neuer Schichten auch die zuvor trainierten neuronalen Schichten aus dem Gleichgewicht bringen. Daher müssen wir zunächst das Training der neuronalen Schichten des Spendermodells blockieren. Auf diese Weise stellen wir sicher, dass nur neue Schichten trainiert werden.


2.1 Gestaltung

Wir brauchen nicht nur ein Softwareprodukt, das das ursprüngliche Spendermodell übernimmt. Wir müssen sie irgendwie verarbeiten und in einer neuen Datei speichern. Die Anzahl der kopierten Schichten sowie die Modellarchitektur sind immer individuell. Deshalb brauchen wir ein Werkzeug, mit dem der Nutzer schnell und bequem jedes Modell einzeln konfigurieren kann. D.h. wir brauchen ein Werkzeug mit einer komfortablen Nutzeroberfläche. Also, wir beginnen mit dem UI-Design.

Ich sehe also drei klare Blöcke. Im ersten Block werden wir mit dem Spendermodell arbeiten. Hier brauchen wir die Möglichkeit, eine Datei mit einem trainierten Modell auszuwählen. Nach dem Laden eines Modells aus einer Datei muss das Werkzeug eine Beschreibung der Architektur des geladenen Modells liefern. Der Grund dafür ist, dass der Nutzer wissen sollte, welches Modell geladen ist und welche neuronalen Schichten es kopiert. Wir informieren das Tool auch über die Anzahl der kopierten Ebenen. Wie bereits erwähnt, kopieren wir die neuronalen Schichten nacheinander, beginnend mit der Schicht der Quelldaten.

Im zweiten Block werden die neuronalen Schichten hinzugefügt. Hier werden wir Felder für die Eingabe von Informationen über die zu erstellende neuronale Schicht erstellen. Wie beim Programmcode werden wir nacheinander jede neuronale Schicht einzeln beschreiben und sie zur Architektur des neuen Modells hinzufügen.

Der dritte Block zeigt die ganzheitliche Architektur des erstellten Modells an und bietet die Möglichkeit, eine Datei zum Speichern des Modells anzugeben. Im Folgenden wird ein Beispiel für die Gestaltung des Tools vorgestellt.

Tool design

Sowohl das Design des Tools als auch seine Implementierung werden nur zu Demonstrationszwecken vorgestellt. Sie können sie jederzeit ändern, um sie an Ihre Bedürfnisse anzupassen.


2.2 Nutzeroberfläche 

Nun können wir mit der Umsetzung des Entwurfs beginnen. Zu diesem Zweck erstellen wir eine neue Klasse CNetCreatorPanel, die von der Basisklasse CAppDialog der Dialoganwendung erbt.

Jedes Steuerelement im Panel wird als separates Objekt erstellt. Daher werden wir eine ganze Reihe von Objekten in unserer neuen Klasse deklarieren. Der Einfachheit halber werden wir sie in Blöcke unterteilen.

Der erste Block enthält Objekte, die sich auf die Visualisierung des vortrainierten Modells beziehen:

class CNetCreatorPanel : protected CAppDialog
  {
protected:
   //--- pre-trained model
   CEdit             m_edPTModel;
   CEdit             m_edPTModelLayers;
   CSpinEdit         m_spPTModelLayers;
   CListView         m_lstPTModel;
   CNetModify        m_Model;   
   CArrayObj*        m_arPTModelDescription;

Außerdem werden wir hier die Objekte deklarieren, die mit dem vorab trainierten Modell arbeiten sollen:

Achten Sie auf die folgenden beiden Momente. Alle Objekte werden als statisch deklariert, mit Ausnahme des dynamischen Arrays der Modellarchitekturbeschreibung. Die Verwendung von statischen Objekten ermöglicht die Übertragung von Speicheroperationen an das System. Dies liegt daran, dass statische Objekte zusammen mit dem Objekt, in dem sie enthalten sind, erstellt und gelöscht werden und keine zusätzliche Arbeit für den Programmierer erfordern. Auf diese Weise ist es jedoch nur möglich, Objekte in der Struktur unserer Klasse zu erstellen. Die Beschreibung der Architektur wird aus dem vortrainierten Modell gewonnen. Daher wurde dieses Objekt durch einen dynamischen Zeiger deklariert.

Und der zweite Aspekt. Um ein vorab trainiertes Modellobjekt zu deklarieren, haben wir die Klasse CNetModify verwendet. Zuvor haben wir jedoch die Klasse CNet für neuronale Netzmodelle erstellt. Das liegt daran, dass wir zusätzliche Funktionen von unserem neuronalen Netz benötigen. Um sie zu implementieren, erstellen wir eine neue Klasse CNetModify, die von der Klasse CNet abgeleitet ist. Aber wir werden auf diesen Teil zurückkommen, wenn wir die Funktionsweise des Tools beschreiben.

Der nächste Block enthält Objekte zur Beschreibung der neu zu erstellenden neuronalen Schicht. Die Objekte entsprechen den Elementen der Klasse CLayerDescription, die die Architektur der neuronalen Schicht beschreibt. Aus diesem Grund werden wir nicht auf jedes einzelne Element im Detail eingehen. Erwähnenswert ist jedoch die Einrichtung von zwei Schaltflächen zum Hinzufügen einer neuen neuronalen Ebene und zum Löschen einer bereits erstellten Ebene. Nur hinzugefügte neuronale Schichten können gelöscht werden. Um die Anzahl der kopierten neuronalen Schichten zu kontrollieren, werden die Elemente des vorherigen Blocks verwendet.

   //--- add layers
   CComboBox         m_cbNewNeuronType;
   CEdit             m_edCount;
   CEdit             m_edWindow;
   CEdit             m_edWindowOut;
   CEdit             m_edStep;
   CEdit             m_edLayers;
   CEdit             m_edBatch;
   CEdit             m_edProbability;
   CComboBox         m_cbActivation;
   CComboBox         m_cbOptimization;
   CButton           m_btAddLayer;
   CButton           m_btDeleteLayer;

Der letzte Block von Objekten des neuen Modells enthält nur 3 Elemente. Dabei handelt es sich um ein Objekt zur Anzeige der allgemeinen Architektur des Modells, eine Schaltfläche zum Speichern des neuen Modells und ein dynamisches Array, das die Architektur der hinzugefügten neuronalen Schichten beschreibt. In diesem Fall haben wir ein statisches Objekt des dynamischen Arrays erstellt, das die Architektur der hinzuzufügenden neuronalen Schichten beschreibt m_arAddLayers. Die Architektur der neuronalen Schichten wird innerhalb des Tools erstellt. Dieses Objekt kann auch als statisches Objekt erstellt werden.

   //--- new model
   CListView         m_lstNewModel;
   CButton           m_btSave;
   CArrayObj         m_arAddLayers;

Wir werden eine einfache Liste der öffentlichen Methoden der Klasse verwenden. Dazu gehören ein Klassenkonstruktor und -destruktor, eine Methode zur Erstellung eines Panels und eine Ereignisbehandlung.

Drei Methoden der übergeordneten Klasse wurden außer Kraft gesetzt. Dies hätte durch eine öffentliche Vererbung vermieden werden können.

public:
                     CNetCreatorPanel();
                    ~CNetCreatorPanel();
   //--- main application dialog creation and destroy
   virtual bool      Create(const long chart, const string name, const int subwin, const int x1, const int y1);
   //--- chart event handler
   virtual bool      OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
 
   virtual void      Destroy(const int reason = REASON_PROGRAM) override { CAppDialog::Destroy(reason); }
   bool              Run(void) { return CAppDialog::Run();}
   void              ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
     {               CAppDialog::ChartEvent(id, lparam, dparam, sparam); }
  };

Da wir statische Objekte verwenden, sind der Konstruktor und der Destruktor unserer Klasse praktisch leer.

Der Hauptteil der Arbeit im Zusammenhang mit der Erstellung und Anordnung der Oberflächenelemente ist in der Dialogfenstererstellungsmethode Create implementiert. Bevor wir jedoch zur Beschreibung der Methode übergehen, sollten wir ein wenig Vorarbeit leisten.

Zunächst müssen wir die Anzahl der Konstanten definieren, die uns helfen, den internen Raum der Schnittstelle richtig zu organisieren. Die vollständige Liste ist in der Anlage enthalten.

Zu beachten ist auch, dass unsere Schnittstelle neben den Eingabeelementen auch eine Reihe von Textkennzeichnungen enthält. Aber wir haben keine Objekte für sie deklariert. Dies geschieht absichtlich, um die Struktur unserer Klasse zu vereinfachen. Wir brauchen sie nur für die Visualisierung, sie werden also nicht für die Erstellung der Funktionalität unseres Tools verwendet. Allerdings müssen wir diese Objekte erstellen. Das Verfahren zur Erstellung solcher Objekte wird mit Ausnahme einiger Daten wiederholt. Dies kann auch den Objekttext und seinen Standort umfassen. Um unseren Code zu strukturieren, werden wir eine separate CreateLabel-Methode für die Erstellung solcher Kennzeichnungen erstellen.

In den Methodenparametern werden der Objektbezeichner, der Text des Kennzeichens und seine Koordinaten auf dem Panel übergeben.

Im Hauptteil der Methode wird zunächst ein neues Etikettenobjekt erstellt und das Ergebnis der Operation überprüft. Dann erstellen wir ein Objekt im Chart, übergeben ihm den erforderlichen Inhalt und fügen den Zeiger des erstellten Objekts zu einem dynamischen Array mit der Kollektion der Schnittstellenobjekte hinzu.

Wir haben ein neues Objekt mit einem Zeiger in einer privaten Variablen erstellt. Überprüfen wir während der Ausführung der Methodenoperationen das Ergebnis jeder Operation und löschen im Falle eines Fehlers das erstellte Objekt. Aber nach dem Beenden der Methode hinterlassen wir keinen Zeiger auf das erstellte Objekt in unserer Klasse, um es beim Beenden des Programms wieder zu entfernen. Das liegt daran, dass wir einen Zeiger auf das erstellte Objekt an die Kollektion von Dialogfeldobjekten übergeben haben, deren volle Funktionsweise bereits in der übergeordneten Klasse implementiert ist. Die Funktionen beinhaltet das Löschen aller Objekte der Kollektion, wenn das Programm geschlossen wird. Wir können also vorerst den Zeiger auf die Kollektion übergeben und sie vergessen.

bool CNetCreatorPanel::CreateLabel(const int id, const string text, const int x1, const int y1, const int x2, const int y2)
  {
   CLabel *tmp_label = new CLabel();
   if(!tmp_label)
      return false;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return false;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return false;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return false;
     }
//---
   return true;
  }

In ähnlicher Weise werden wir eine Methode zur Erstellung von Eingabeobjekten erstellen. Aber anstatt neue Objekte zu erstellen, verwenden wir die zuvor in der Klasse erstellten. Die entsprechenden Zeiger werden in Methodenparametern übergeben.

bool CNetCreatorPanel::CreateEdit(const int id,
                                  CEdit& object,
                                  const int x1,
                                  const int y1,
                                  const int x2,
                                  const int y2,
                                  bool read_only)
  {
   if(!object.Create(m_chart_id, StringFormat("%s%d", EDIT_NAME, id), m_subwin, x1, y1, x2, y2))
      return false;
   if(!object.TextAlign(ALIGN_RIGHT))
      return false;
   if(!object.ReadOnly(read_only))
      return false;
   if(!Add(object))
      return false;
//---
   return true;
  }

Darüber hinaus verwenden wir Aufzählungen und Konstanten, um die Architektur der erstellten neuronalen Schichten zu beschreiben. Um die Eingabe falscher Werte durch die Nutzer in solche Elemente zu vermeiden, werden spezielle Steuerelemente erstellt. Der Nutzer kann nur ein Element aus der Vorschlagsliste auswählen. Wir brauchen mehrere solcher Elemente. Beginnen wir damit, ein Element zu erstellen, das den Typ der neuronalen Schicht angibt. Diese Funktionsweise wird in der Methode CreateComboBoxType implementiert. Da diese Methode dazu dient, ein bestimmtes Element zu erstellen, brauchen wir keinen Zeiger auf ein Objekt in den Parametern zu übergeben. Hier müssen wir nur die Koordinaten des zu erstellenden Elements angeben.

Im Hauptteil der Methode wird ein Element im Chart an den angegebenen Koordinaten erstellt und das Ergebnis überprüft.

Als Nächstes müssen wir das Element mit einer Textbeschreibung und der numerischen ID füllen. Wir können den Identifikator des neuronalen Schichttyps als ID verwenden. Aber wir haben keine Textbeschreibung. Um einen numerischen Identifikator in eine Textbeschreibung zu übersetzen, wird daher eine eigene Methode LayerTypeToString erstellt. Der Algorithmus ist recht einfach. Sie können ihn im Anhang einsehen. Hier wird diese Methode nur für jeden Typ von neuronaler Schicht aufgerufen.

Am Ende der Methode fügen wir den Objektzeiger in die Kollektion unserer Schnittstellenobjekte ein.

Beachten Sie, dass wir sowohl dynamische als auch statische Objekte in die Kollektion aufnehmen. Der Grund dafür ist, dass die Funktionsweise der Kollektion viel umfassender ist als die Kontrolle über das Entfernen von Objekten nach Programmende. Gleichzeitig sind die Kollektionselemente an der Bestimmung der Koordinaten von Objekten auf dem Chart und an der Verarbeitung von Ereignissen beteiligt. Der allgemeine Zweck der genannten Kollektion besteht darin, dass alle Objekte als ein einziger Gesamtorganismus funktionieren.

bool CNetCreatorPanel::CreateComboBoxType(const int x1, const int y1, const int x2, const int y2)
  {
   if(!m_cbNewNeuronType.Create(m_chart_id, "cbNewNeuronType", m_subwin, x1, y1, x2, y2))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBaseOCL), defNeuronBaseOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronConvOCL), defNeuronConvOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronProofOCL), defNeuronProofOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronLSTMOCL), defNeuronLSTMOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronAttentionOCL), defNeuronAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMHAttentionOCL), defNeuronMHAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMLMHAttentionOCL), defNeuronMLMHAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronDropoutOCL), defNeuronDropoutOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBatchNormOCL), defNeuronBatchNormOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronVAEOCL), defNeuronVAEOCL))
      return false;
   if(!Add(m_cbNewNeuronType))
      return false;
//---
   return true;
  }

Erstellen wir auf ähnliche Weise Objekte für Enumeration der Aktivierungsfunktionen und Methoden zur Parameteroptimierung. Um die Enumeration in eine Textform umzuwandeln, wird die Standardfunktion EnumToString verwendet. Daher können wir der Liste in einer Schleife Elemente hinzufügen. Der vollständige Code der Methoden ist im Anhang zu finden.

Damit sind die vorbereitenden Arbeiten abgeschlossen, und wir können mit dem Erstellen der Nutzeroberfläche fortfahren. Diese Funktionsweise wird in der Methode Create ausgeführt. In den Parametern erhalten wir nur die Koordinaten der Position der oberen rechten Ecke der Tafel auf der Karte. Um Objekte zu erstellen, benötigen wir jedoch auch die Abmessungen unseres Panels. Um eine bequeme Bedienung und spätere Änderungen (falls erforderlich) zu ermöglichen, habe ich die Abmessungen der Platte durch vordefinierte Konstanten festgelegt. Das Panel wird mit einer ähnlichen Methode der übergeordneten Klasse erstellt. Sie ist die erste, die im Methodenkörper aufgerufen wird.

bool CNetCreatorPanel::Create(const long chart, const string name, const int subwin, const int x1, const int y1)
  {
   if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT))
      return false;

Als Nächstes fügen wir dem erstellten Panel Oberflächenobjekte hinzu. Die Objekte werden der Reihe nach hinzugefügt, beginnend in der oberen linken Ecke. Die Koordinaten jedes neuen Objekts werden mit den Koordinaten des vorherigen Objekts verknüpft. Auf diese Weise können wir die Objekte in einer gleichmäßigen Struktur aufbauen.

Nach der obigen Logik beginnen wir mit der Erstellung von vortrainierten Arbeitsgruppenobjekten für das Modell. Das erste ist ein Gruppenetikett. Um es zu erstellen, bestimmen wir die Koordinaten des Etiketts und rufen die zuvor erstellte Methode CreateLabel auf. Der Kennzeichentext und die Koordinaten werden an diese Methode übergeben. Vergessen wir nicht, eine eindeutige Kennzeichen-ID hinzuzufügen.

   int lx1 = INDENT_LEFT;
   int ly1 = INDENT_TOP;
   int lx2 = lx1 + LIST_WIDTH;
   int ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(0, "PreTrained model", lx1, ly1, lx2, ly2))
      return false;

Als Nächstes erstellen wir ein Eingabefeld, das zur Auswahl des Namens der Datei mit einem vorab trainierten Modell verwendet wird. Dazu verschieben wir die Koordinaten des erstellten Objekts vertikal und lassen die horizontalen Koordinaten unverändert. Somit befinden sich 2 Objekte genau unter einander.

Der Nutzer kann den Dateinamen nicht manuell angeben. Stattdessen wird der Nutzer aufgefordert, eine Datei aus den vorhandenen Dateien auszuwählen. Auf die Funktionsweise dieser Aktion werden wir etwas später zurückkommen. Das Feld Dateiname ist vorerst schreibgeschützt. Das Objekt wird durch den Aufruf der zuvor erstellten Methode CreateEdit erstellt. Fügen wir nach der Erstellung des Feldes eine Informationsmeldung hinzu.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateEdit(0, m_edPTModel, lx1, ly1, lx2, ly2, true))
      return false;
   if(!m_edPTModel.Text("Select file"))
      return false;

Darunter geben wir die Anzahl der neuronalen Felder des trainierten Modells an. Erstellen wir dazu ein Textkennzeichen und ein Eingabefeld (in diesem Fall die Ausgabe) für die Anzahl der neuronalen Schichten. Auch dieses Feld ist schreibgeschützt.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(1, "Layers Total", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateEdit(1, m_edPTModelLayers, lx2 - EDIT_WIDTH, ly1, lx2, ly2, true))
      return false;
   if(!m_edPTModelLayers.Text("0"))
      return false;

Erstellen wir auf ähnliche Weise ein Kennzeichen und Felder zur Eingabe der Anzahl der zu kopierenden neuronalen Schichten. Wir müssen hier einen Mechanismus implementieren, der den Nutzer bei der Auswahl der Anzahl der neuronalen Schichten einschränkt. Sie darf nicht kleiner als 0 oder größer als die Gesamtzahl der neuronalen Schichten des Modells sein. Dies lässt sich leicht mit einer Instanz der Klasse CSpinEdit bewerkstelligen. Mit dieser Klasse können wir einen Bereich gültiger Werte angeben. Der Rest ist bereits in der Klasse implementiert.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(2, "Transfer Layers", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!m_spPTModelLayers.Create(m_chart_id, "spPTMCopyLayers", m_subwin, lx2 - 100, ly1, lx2, ly2))
      return false;
   m_spPTModelLayers.MinValue(0);
   m_spPTModelLayers.MaxValue(0);
   m_spPTModelLayers.Value(0);
   if(!Add(m_spPTModelLayers))
      return false;

Als Nächstes sollten wir nur ein Fenster mit der Beschreibung der vortrainierten Modellarchitektur anzeigen. Vergessen wir nicht, dass wir zuvor die Koordinaten der erstellten Objekte immer um eine Ebene nach unten verschoben haben. In diesem Fall haben wir nur den oberen Rand des vorherigen Objekts nach unten verschoben. Der untere Rand des Objekts wird mit einem Einzug von der Höhe unseres Fensters festgelegt. Auf diese Weise wird das Objekt auf die Größe des Fensters gestreckt und erhält eine glatte Kante am unteren Rand der erstellten Schnittstelle.

   lx1 = INDENT_LEFT;
   lx2 = lx1 + LIST_WIDTH;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ClientAreaHeight() - INDENT_BOTTOM;
   if(!m_lstPTModel.Create(m_chart_id, "lstPTModel", m_subwin, lx1, ly1, lx2, ly2))
      return false;
   if(!m_lstPTModel.VScrolled(true))
      return false;
   if(!Add(m_lstPTModel))
      return false;

Damit sind die Operationen mit dem vortrainierten Modellblock abgeschlossen, und es wird mit dem zweiten Block von Objekten fortgefahren, um die Architektur der hinzugefügten neuronalen Schicht zu beschreiben. Die Blockobjekte werden ebenfalls von oben nach unten erstellt. Bei der Festlegung der Koordinaten für das neue Objekt werden die Koordinaten horizontal verschoben und der obere Rand auf Höhe des Einzugs vom oberen Rand des Fensters festgelegt.

   lx1 = lx2 + CONTROLS_GAP_X;
   lx2 = lx1 + ADDS_WIDTH;
   ly1 = INDENT_TOP;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(3, "Add layer", lx1, ly1, lx2, ly2))
      return false;

Erstellen wir unten, im Abstand des Einzugs, ein Kombinationsfeld, um den Typ der zu erstellenden neuronalen Schicht auszuwählen. Dies geschieht mit Hilfe der zuvor erstellten Methode. Die Breite dieses Objekts ist gleich der Breite des gesamten Blocks.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateComboBoxType(lx1, ly1, lx2, ly2))
      return false;

Es folgen die Elemente, die die Architekturen der erstellten neuronalen Schicht beschreiben. Für jedes Element aus der Beschreibungsklasse für die Architektur der neuronalen Schicht CLayerDescription werden wir 2 Objekte erstellen: eine Textbeschriftung mit dem Namen des Elements und ein Werteingabefeld. Um die Elemente auf der Nutzeroberfläche in einer strikten Reihenfolge zu positionieren, werden wir die Textkennzeichnungen links und die Eingabefelder rechts vom Block ausrichten. Die Größe der Eingabefelder ist für alle gleich. Auf diese Weise wird eine Art von Tabelle erstellt.

Ich werde jetzt nicht den gleichen Code für alle 9 Elemente bereitstellen. Im Folgenden finden wir ein Codebeispiel für das Erstellen von 2 Zeilen aus unserer Tabelle. Der vollständige Code befindet sich im Anhang.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(4, "Neurons", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateEdit(2, m_edCount, lx2 - EDIT_WIDTH, ly1, lx2, ly2, false))
      return false;
   if(!m_edCount.Text((string)DEFAULT_NEURONS))
      return false;

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(5, "Activation", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateComboBoxActivation(lx2 - EDIT_WIDTH, ly1, lx2, ly2))
      return false;

Nachdem wir Elemente zur Beschreibung der Architektur der hinzugefügten neuronalen Schicht erstellt haben, fügen wir 2 Schaltflächen hinzu: zum Hinzufügen und Entfernen einer neuronalen Schicht. Ordnen wir die Schaltflächen in einer Reihe an, wobei wir die Breite des Blocks zwischen den Schaltflächen halbieren.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + BUTTON_HEIGHT;
   if(!m_btAddLayer.Create(m_chart_id, "btAddLayer", m_subwin, lx1, ly1, lx1 + ADDS_WIDTH / 2, ly2))
      return false;
   if(!m_btAddLayer.Text("ADD LAYER"))
      return false;
   m_btAddLayer.Locking(false);
   if(!Add(m_btAddLayer))
      return false;
//---
   if(!m_btDeleteLayer.Create(m_chart_id, "btDeleteLayer", m_subwin, lx2 - ADDS_WIDTH / 2, ly1, lx2, ly2))
      return false;
   if(!m_btDeleteLayer.Text("DELETE"))
      return false;
   m_btDeleteLayer.Locking(false);
   if(!Add(m_btDeleteLayer))
      return false;

Gehen wir nun zum dritten und letzten Block über, der die vollständige Architektur des zu erstellenden Modells beschreibt. Hier finden wir alle oben verwendeten Methoden.

Nachdem alle Elemente erstellt wurden, beenden wir die Methode mit „true“. Der vollständige Code aller Methoden und Klassen ist in der Anlage unten zu finden.

Damit ist die Anordnung der Elemente unserer Schnittstelle abgeschlossen. Sie kann nun zum Expert Advisor hinzugefügt werden. Aber in dieser Form wird es nur ein schönes Bild auf der Symboltafel sein. Als Nächstes müssen wir die erforderlichen Funktionen in das Formular implementieren.


2.3 Implementierung der Funktionalität des Tools

Wir arbeiten weiter an der Entwicklung unseres Tools, und der nächste Schritt besteht darin, die Schnittstelle mit den erforderlichen Funktionen auszustatten. Bevor wir fortfahren, kommen wir noch einmal auf den gewünschten Algorithmus für unser Tool zurück.

  1. Zunächst müssen wir die Datei mit dem gespeicherten trainierten Modell öffnen. Dazu klickt der Nutzer auf das Objekt, um eine Datei auszuwählen. Es öffnet sich ein Dialogfeld, in dem der Nutzer eine vorhandene Datei mit der angegebenen Erweiterung auswählt.
  2. Nachdem der Nutzer eine Datei ausgewählt hat, sollte das Werkzeug das Modell aus der angegebenen Datei laden und Informationen über das geladene Modell anzeigen (Art und Anzahl der Neuronenschichten, Anzahl der Neuronen in jeder Schicht).
  3. Zusammen mit der Ausgabe von Informationen über das standardmäßig geladene Modell werden alle seine neuronalen Schichten in das neue Modell kopiert. Die Informationen über sie werden auch in den Beschreibungsblock des erstellten Modells kopiert.
  4. Der Nutzer sollte in der Lage sein, die Anzahl der kopierten neuronalen Schichten manuell zu ändern. Gleichzeitig mit der Änderung der Anzahl der kopierten neuronalen Schichten muss auch die Architektur des erstellten Modells geändert werden. Dies wird sich in dem Block widerspiegeln, der die Architektur des erstellten Modells beschreibt.
  5. Nach der Auswahl der Anzahl der kopierten neuronalen Schichten kann der Nutzer den Typ und die Architektur der neuen neuronalen Schicht manuell festlegen und sie durch Drücken der Schaltfläche „SCHICHT HINZUFÜGEN“ zu dem erstellten Modell hinzufügen.
  6. Wenn dem Modell versehentlich eine neuronale Schicht hinzugefügt wurde, kann der Nutzer eine solche neuronale Schicht im Block, der die Modellarchitektur beschreibt, auswählen und sie durch Drücken der Schaltfläche „DELETE“ löschen. Bitte beachten Sie, dass nur hinzugefügte neuronale Schichten gelöscht werden können. Um die Schichten des Spendermodells zu löschen, müssen wir die Anzahl der kopierten neuronalen Schichten mit dem Werkzeug ändern.
  7. Nachdem die Architektur des erstellten neuronalen Netzes erstellt wurde, drückt der Nutzer auf die Schaltfläche „SAVE MODEL“. Es öffnet sich ein Dialogfeld, in dem der Nutzer eine vorhandene Datei auswählen oder den Namen einer neuen Datei angeben kann.

Dies scheint mir ein logisches Szenario für die Arbeit mit dem Werkzeug zu sein. Es sind jedoch noch einige Anstrengungen erforderlich, um sie umzusetzen. Zunächst benötigen wir die Möglichkeit, Informationen über das gespeicherte Modell zu erhalten. Bisher haben wir dem Nutzer keine Informationen über das geladene Modell gegeben. Um diese Funktionalität zu implementieren, müssen wir Änderungen an der Klasse des neuronalen Netzes vornehmen. Da diese Funktionalität jedoch die Funktionsweise des Modells selbst nicht beeinträchtigt, fügen wir sie der neuen Klasse CNetModify hinzu, die ein direkter Nachfolger der zuvor erstellten Modellklasse CNet für neuronale Netze sein wird.

Wir werden keine neuen Objekte in der neuen Klasse erstellen. Daher bleiben der Konstruktor und der Destruktor der Klasse leer. Die Methode LayersTotal gibt die Anzahl der neuronalen Schichten im Modell zurück. Der Algorithmus ist nicht kompliziert, da er einfach die Größe des Arrays zurückgibt. Der vollständige Code befindet sich im Anhang.

class CNetModify :  public CNet
  {
public:
                     CNetModify(void) {};
                    ~CNetModify(void) {};
   //---
   uint              LayersTotal(void);
   CArrayObj*        GetLayersDiscriptions(void);
  };

Gehen wir kurz auf die Methode GetLayersDiscriptions ein, um Informationen über die verwendeten neuronalen Netze zu erhalten. Als Ergebnis der Ausführung dieser Methode sollten wir ein dynamisches Array mit der Beschreibung der Architektur des neuronalen Netzes erhalten, ähnlich der Modellbeschreibung, die in den Parametern der Modellkonstruktormethode übergeben wird. Die Komplexität der Organisation dieses Prozesses hängt mit der Tatsache zusammen, dass wir bisher keine Methoden zur Ermittlung von Hyperparametern neuronaler Schichten entwickelt haben. Daher müssen wir die entsprechende Methode zu den neuronalen Schichtklassen hinzufügen. Zunächst fügen wir der Basisklasse CNeuronBaseOCL für neuronale Schichten die Methode GetLayerInfo hinzu.

Die neue Methode enthält keine Parameter und gibt nach der Ausführung das Objekt CLayerDescription zur Beschreibung der neuronalen Schicht zurück. Im Hauptteil der Methode wird zunächst eine Instanz des Objekts zur Beschreibung der neuronalen Schicht erstellt. Dann füllen wir es mit den Hyperparametern der aktuellen neuronalen Schicht. Danach beenden wir die Methode und geben den erstellten Objektzeiger an das aufrufende Programm zurück.

CLayerDescription* CNeuronBaseOCL::GetLayerInfo(void)
  {
   CLayerDescription* result = new CLayerDescription();
   if(!result)
      return result;
//---
   result.type = Type();
   result.count = Output.Total();
   result.optimization = optimization;
   result.activation = activation;
   result.batch = (int)(optimization == LS ? iBatch : 1);
   result.layers = 1;
//---
   return result;
  }

Indem wir der Basisklasse der neuronalen Schicht eine Methode hinzugefügt haben, haben wir eine Methode zu allen ihren Nachkommen hinzugefügt. Alle neuronalen Schichten verfügen also über diese Methode. Jetzt können wir ähnliche Informationen von jeder neuronalen Schicht erhalten. Wenn diese Daten für Sie ausreichend sind, können Sie die Arbeit mit der neuronalen Schicht beenden und zur Modellinformationserfassungsmethode übergehen.

Wenn Sie jedoch spezifische Informationen für jede neuronale Schicht benötigen, müssen Sie diese Methode in allen neuronalen Schichten außer Kraft setzen. Es folgt ein Beispiel für die Überschreibung einer Methode in der Unterabtastungsebene, die es ermöglicht, Daten über die Größe des analysierten Fensters und seinen Bewegungsschritt zu erhalten. Im Methodenkörper rufen wir zunächst die Methode der übergeordneten Klasse auf, um die zugrunde liegenden Hyperparameter zu erhalten. Und dann ergänzen wir das resultierende Objekt zur Beschreibung der neuronalen Schicht mit spezifischen Parametern. Danach beenden wir die Methode, indem wir einen Zeiger auf das Objekt zur Beschreibung der neuronalen Schicht an das aufrufende Programm zurückgeben.

CLayerDescription* CNeuronProofOCL::GetLayerInfo(void)
  {
   CLayerDescription *result = CNeuronBaseOCL::GetLayerInfo();
   if(!result)
      return result;
   result.window = (int)iWindow;
   result.step = (int)iStep;
//---
   return result;
  }

Ähnliche Methoden für alle zuvor besprochenen Arten von neuronalen Schichten finden Sie in der Anlage unten.

Jetzt können wir Informationen über die Hyperparameter jeder neuronalen Schicht erhalten. Diese Informationen können in einer gemeinsamen Struktur zusammengefasst werden. Kehren wir zu unserer Methode CNetModify::GetLayersDiscriptions zurück und erstellen darin ein dynamisches Array, um Zeiger auf Beschreibungsobjekte für neuronale Schichten zu speichern.

Als Nächstes werden wir eine Schleife durch alle neuronalen Schichten erstellen. Im Schleifenkörper fordern wir von jeder neuronalen Schicht ein Architekturbeschreibungsobjekt an, indem wir die oben erstellte Methode aufrufen. Die erhaltenen Objekte werden dem dynamischen Array hinzugefügt.

Nach Ausführung aller Iterationen der Schleife haben wir ein dynamisches Array mit der Beschreibung der vollständig geladenen Modellarchitektur. Nach Beendigung der Methode wird sie an das aufrufende Programm zurückgegeben.

CArrayObj* CNetModify::GetLayersDiscriptions(void)
  {
   CArrayObj* result = new CArrayObj();
   for(uint i = 0; i < LayersTotal(); i++)
     {
      CLayer* layer = layers.At(i);
      if(!layer)
         break;
      CNeuronBaseOCL* neuron = layer.At(0);
      if(!neuron)
         break;
      if(!result.Add(neuron.GetLayerInfo()))
         break;
     }
//---
   return result;
  }

In diesem Stadium haben wir die Möglichkeit geschaffen, eine Beschreibung der Architektur eines zuvor erstellten Modells zu erhalten. Jetzt können wir mit der Implementierung einer Methode zum Laden eines vortrainierten Modells aus einer nutzerdefinierten Datei fortfahren. Um diese Funktionalität zu implementieren, müssen wir die Methode CNetCreatorPanel::LoadModel erstellen. Die Methode erhält als Parameter den Namen der Datei, die das Modell laden soll.

Im Hauptteil der Methode laden wir zunächst das Modell aus der angegebenen Datei. Beachten Sie, dass wir den Wert des Parameters nicht überprüfen, bevor wir die Load-Methode des Modells aufrufen. Dies liegt daran, dass alle Kontrollen in der Lademethode implementiert sind. Wir prüfen nur das Ergebnis der Operation. Im Falle eines Modellladefehlers wird die Fehlerinformation im geladenen Modellbeschreibungsblock ausgegeben.

bool CNetCreatorPanel::LoadModel(string file_name)
  {
   float error, undefine, forecast;
   datetime time;
   ResetLastError();
   if(!m_Model.Load(file_name, error, undefine, forecast, time, false))
     {
      m_lstPTModel.ItemsClear();
      m_lstPTModel.ItemAdd("Error of load model", 0);
      m_lstPTModel.ItemAdd(file_name, 1);
      int err = GetLastError();
      if(err == 0)
         m_lstPTModel.ItemAdd("The file is damaged");
      else
         m_lstPTModel.ItemAdd(StringFormat("error id: %d", GetLastError()), 2);
      m_edPTModel.Text("Select file");
      return false;
     }

Nach dem erfolgreichen Laden des Modells werden der Name der geladenen Datei und die Anzahl der neuronalen Schichten in den entsprechenden Elementen der Nutzeroberfläche angezeigt.

Löschen wir die Beschreibung des zuvor geladenen Modells, falls vorhanden, rufen dann die Methode zum Sammeln von Informationen über die Architektur des geladenen Modells auf.

   m_edPTModel.Text(file_name);
   m_edPTModelLayers.Text((string)m_Model.LayersTotal());
   if(!!m_arPTModelDescription)
      delete m_arPTModelDescription;
   m_arPTModelDescription = m_Model.GetLayersDiscriptions();

Nach dem Empfang von Informationen über das geladene Modell erstellen wir eine Schleife, in deren Körper wir die empfangenen Informationen im entsprechenden Block der Schnittstelle ausgeben.

   m_lstPTModel.ItemsClear();
   int total = m_arPTModelDescription.Total();
   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;
      //---
      string item = StringFormat("%s (units %d)", LayerTypeToString(temp.type), temp.count);
      if(!m_lstPTModel.AddItem(item, i))
         return false;
     }

Ändern wir am Ende der Methode den Wertebereich für die zulässige Anzahl der kopierten neuronalen Schichten auf die Gesamtgröße des geladenen Modells. Weisen wir das Werkzeug an, das gesamte geladene Modell zu kopieren. Dann verlassen wir die Methode.

   m_spPTModelLayers.MaxValue(total);
   m_spPTModelLayers.Value(total);
//---
   return true;
  }

Wie Sie sehen, erhält die obige Methode über die Parameter den Namen der Datei, aus der Daten geladen werden sollen, vom aufrufenden Programm. Wir müssen dem Nutzer die Möglichkeit geben, die Modelldatei auszuwählen.

Erstellen wir eine weitere OpenPreTrainedModel-Methode. Im Hauptteil dieser Methode rufen wir nur die Standardfunktion FileSelectDialog auf, die bereits die Schnittstelle des Dateidialogfelds implementiert. Beim Funktionsaufruf geben wir die erforderlichen Dateierweiterungen und das Kennzeichen FSD_FILE_MUST_EXIST an, das besagt, dass nur eine vorhandene Datei angegeben werden kann.

Mit bestimmten Flags ermöglicht diese Funktion die Auswahl mehrerer Dateien. Daher wird als Ergebnis der Ausführung von FileSelectDialog die Anzahl der ausgewählten Dateien zurückgegeben. Die Dateinamen sind in dem Array enthalten, auf das die Funktion als Parameter einen Zeiger erhält.

Wenn der Nutzer also eine Datei auswählt, wird ihr Name in den Parametern an die oben genannte Methode übergeben. Andernfalls wird eine Meldung generiert, in der der Nutzer aufgefordert wird, eine Datei zum Laden von Daten auszuwählen.

bool CNetCreatorPanel::OpenPreTrainedModel(void)
  {
   string filenames[];
   if(FileSelectDialog("Select a file to load data", NULL,
                       "Neuron Net (*.nnw)|*.nnw|All files (*.*)|*.*",
                       FSD_FILE_MUST_EXIST, filenames, NULL) > 0)
     {
      if(!LoadModel(filenames[0]))
         return false;
     }
   else
      m_edPTModel.Text("Files not selected");
//---
   return true;
  }

Wir kommen schrittweise voran und haben bereits eine Visualisierung der Schnittstelle erstellt. Wir haben auch eine Kette von Methoden zur Auswahl einer Datei und zum Laden eines vorab trainierten Modells entwickelt. Bislang sind diese beiden Programmblöcke jedoch nicht zu einem einzigen Bio-Programm zusammengefasst. Die Datenlademethode zeigt Informationen über das geladene Datenmodell auf dem Panel an. Aber im Moment ist es eine Einbahnstraße. Wir müssen den Rückweg angeben, auf dem das Programm Informationen über die Aktionen des Nutzers und die Reaktion des Nutzers auf Informationen erhält.

Verwenden wir dazu den Event-Handler. In den abgeleiteten Klassen von CAppDialog wird dieser Mechanismus durch Makro-Substitutionen implementiert. Zu diesem Zweck wird im Programmcode ein Makroblock angelegt, der mit dem Makro EVENT_MAP_BEGIN beginnt und mit dem Makro EVENT_MAP_END endet. Dazwischen gibt es eine Reihe von Makros, die verschiedenen Ereignissen entsprechen. In unserem Fall verwenden wir das Makro ON_EVENT, das die Ereignisverarbeitung durch einen numerischen Bezeichner impliziert. Um das Mausklick-Ereignis auf das Dateinamen-Objekt zu behandeln, geben wir im Makrokörper das ON_CLICK-Ereignis, den m_edPTModel-Objektzeiger und den Namen der Methode an, die aufgerufen werden soll, wenn das OpenPreTrainedModel-Ereignis eintritt. Wenn also die Maustaste auf dem Objekt m_edPTModel gedrückt wird, das dem Feld für die Eingabe des Dateinamens entspricht, ruft das Programm die Methode OpenPreTrainedModel auf und startet damit die Kette der Methoden zum Laden des vortrainierten Modells.

EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
EVENT_MAP_END(CAppDialog)

Beschreiben wir in der gleichen Weise auch andere Ereignisse und Methoden, die von ihnen aufgerufen werden:

Der gesamte Code aller Klassen und Methoden befindet sich in der Anlage. Betrachten wir die Methode, die das neue Modell löst, da ihre Implementierung ziemlich kompliziert ist und die Erstellung zusätzlicher Methoden in der Modellklasse CNetModify des neuronalen Netzes erfordert.

Der Algorithmus dieser Methode kann bedingt in 3 Blöcke unterteilt werden:

Derzeit ist nur der letzte Punkt in unserer Klasse für neuronale Netze implementiert worden. Wir haben keine Methoden, um neuronale Schichten von einem anderen Modell zu kopieren oder neue neuronale Schichten zu einem bestehenden Modell hinzuzufügen.

Gehen wir von Punkt für Punkt. Zunächst werden wir einen Mechanismus zum Kopieren neuronaler Schichten schaffen. Wir wissen, dass die neuronale Schicht je nach ihrer Architektur eine unterschiedliche Anzahl von Objekten enthalten kann. Wir benötigen jedoch einen universellen Algorithmus, der das Kopieren aller Arten von neuronalen Schichten mit verschiedenen Methoden zur Parameteroptimierung ermöglicht. Beim Kopieren des trainierten Modells werden nicht nur die Architektur, sondern auch alle Gewichte übertragen. Hier ist die Frage: Warum müssen wir alle Elemente der einzelnen neuronalen Schichten kopieren? Warum können wir nicht einfach den Zeiger auf das erforderliche Objekt der neuronalen Schicht kopieren? Durch die Verwendung von Zeigern können wir von verschiedenen Teilen des Programmcodes aus auf dasselbe Objekt zugreifen. Wir werden also diese Eigenschaft nutzen. Erstellen wir zwei Methoden. Man gibt einen Zeiger auf das Objekt der neuronalen Schicht durch seine Nummer in der Modellstruktur zurück. Und die zweite fügt der Modellarchitektur einen Zeiger auf das Objekt der neuronalen Schicht hinzu.

CLayer* CNetModify::GetLayer(uint layer)
  {
   if(!layers || LayersTotal() <= layer)
      return NULL;
//---
   return layers.At(layer);
  }

bool CNetModify::AddLayer(CLayer *new_layer)
  {
   if(!new_layer)
      return false;
   if(!layers)
     {
      layers = new CArrayLayer();
      if(!layers)
         return false;
     }
//---
   return layers.Add(new_layer);
  }

Da wir einen Block von aufeinanderfolgenden neuronalen Schichten kopieren, können wir durch die Übertragung von Zeigern auf ein neues Modell unter Beibehaltung der Reihenfolge alle Beziehungen zwischen diesen neuronalen Schichten speichern.

Dies war der erste Punkt. Fahren wir fort. Der Konstruktor unseres Modells kann ein neues Modell entsprechend der Architekturbeschreibung erstellen. Als wir dem Modell neuronale Schichten hinzufügten, erstellten wir eine ähnliche Beschreibung der neuronalen Schichten. Es scheint, dass wir einfach neue Ebenen hinzufügen können, was das Modell bereits kann. Die Schwierigkeit besteht jedoch darin, dass es keine Brücke zwischen den kopierten und den neu geschaffenen neuronalen Schichten gibt.

Gemäß der Architektur unserer neuronalen Schichten sind die Gewichte einer neuronalen Schicht direkt mit den Elementen einer anderen neuronalen Schicht verbunden. Damit das Modell im Vorwärts- und Rückwärtsmodus funktioniert, müssen wir diese Verbindung herstellen. Wenn wir sich die Initialisierungsmethode der Basisklasse der neuronalen Schicht CNeuronBaseOCL ansehen, können wir unter den Parametern die Anzahl der Neuronen in der nachfolgenden neuronalen Schicht feststellen. Dieser Parameter bestimmt die Größe der zu erstellenden Gewichtsmatrix und der zugehörigen Puffer, die bei der Parameteroptimierung verwendet werden.

Zunächst fügen wir der Klasse die Methode CNeuronBaseOCLhinzu, die die Gewichtsmatrix entsprechend der angegebenen Anzahl der Neuronen in der nachfolgenden SchichtCNeuronBaseOCL::numOutputs anpasst.

In den Parametern der Methode geben wir die Anzahl der Neuronen in der nachfolgenden Schicht und die Methode der Parameteroptimierung an.

Im Hauptteil der Methode wird die in den Parametern angegebene Anzahl der Elemente in der nachfolgenden neuronalen Schicht überprüft und gegebenenfalls eine Gewichtsmatrix mit den entsprechenden Größen erstellt. Füllen wir ihn mit zufälligen Gewichten, da er sich auf die neu hinzugefügte neuronale Schicht bezieht. Für die gefüllte Matrix erstellen wir einen Puffer im OpenCL-Kontext und übergeben den Inhalt der Matrix an diesen.

Es ist notwendig, Daten an den OpenCL-Kontext zu übergeben, da unsere Klassenmethode versuchen wird, die Daten aus dem Kontext zu laden, bevor sie in der Datei gespeichert werden. Im Falle eines Fehlers wird die Modellspeicherung mit einem negativen Ergebnis abgebrochen. Natürlich könnten wir die Methoden unserer neuronalen Schichtklassen ändern. Aber ich denke, dass diese Arbeitskosten die Kosten für die Übertragung von Informationen in den OpenCL-Kontext und zurück übersteigen.

bool CNeuronBaseOCL::numOutputs(const uint outputs, ENUM_OPTIMIZATION optimization_type)
  {
   if(outputs > 0)
     {
      if(CheckPointer(Weights) == POINTER_INVALID)
        {
         Weights = new CBufferFloat();
         if(CheckPointer(Weights) == POINTER_INVALID)
            return false;
        }
      Weights.BufferFree();
      Weights.Clear();
      int count = (int)((Output.Total() + 1) * outputs);
      if(!Weights.Reserve(count))
         return false;
      float k = (float)(1 / sqrt(Output.Total() + 1));
      for(int i = 0; i < count; i++)
        {
         if(!Weights.Add((2 * GenerateWeight()*k - k)*WeightsMultiplier))
            return false;
        }
      if(!Weights.BufferCreate(OpenCL))
         return false;

Nachdem wir die Gewichtsmatrix erstellt haben, erstellen wir die Datenpuffer, die bei der Gewichtsoptimierung verwendet werden.

Wenn keine Notwendigkeit für eine Matrix von Gewichten und entsprechenden Puffern besteht, sollten diese als unnötig entfernt werden. Dann verlassen wir die Methode.

Der vollständige Code der Methode ist in der Anlage unten zu finden.

Kehren wir nun zur Klasse CNetModify zurück, um eine Methode zum Hinzufügen neuronaler Schichten gemäß der gegebenen AddLayers-Beschreibung zu erstellen. In den Methodenparametern übergeben wir einen Zeiger auf das dynamische Array mit einer Beschreibung der Architektur der hinzuzufügenden neuronalen Schichten. Sofort prüfen wir im Hauptteil der Methode die empfangenen Daten. Der empfangene Zeiger muss gültig sein und eine Beschreibung von mindestens einer neuronalen Schicht enthalten.

bool CNetModify::AddLayers(CArrayObj *new_layers)
  {
   if(!new_layers || new_layers.Total() <= 0)
      return false;
//---
   if(!layers || LayersTotal() <= 0)
     {
      Create(new_layers);
      return true;
     }

Als Nächstes überprüfen wir die Anzahl der neuronalen Schichten, die im Modell vorhanden sind. Wenn keine vorhanden sind, rufen wir einfach den Konstruktor der übergeordneten Klasse auf. Es wird ein neues Modell mit der angegebenen Architektur erstellt.

Wenn wir einem bestehenden Modell neuronale Schichten hinzufügen wollen, müssen wir zunächst lokale Variablen deklarieren.

   CLayerDescription *desc = NULL, *next = NULL;
   CLayer *temp;
   int outputs;

Dann machen wir ein wenig Vorarbeit und rufen die oben erstellte Methode zum Verbinden zweier neuronaler Schichten auf.

   int shift = (int)LayersTotal() - 1;
   CLayer* last_layer = layers.At(shift);
   if(!last_layer)
      return false;
//---
   CNeuronBaseOCL* neuron = last_layer.At(0);
   if(!neuron)
      return false;
//---
   desc = neuron.GetLayerInfo();
   next = new_layers.At(0);
   outputs = (next == NULL || (next.type != defNeuron && next.type != defNeuronBaseOCL) ? 0 : next.count);
   if(!neuron.numOutputs(outputs, next.optimization))
      return false;
   delete desc;

Ähnlich wie beim Konstruktor der übergeordneten Klasse durchlaufen wir das dynamische Array der Modellarchitekturbeschreibung und fügen nacheinander alle neuronalen Schichten hinzu. Der Code dieses Blocks wiederholt vollständig den Code des Konstruktors der übergeordneten Klasse. Daher werde ich sie in diesem Artikel nicht wiederholen. Der vollständige Code aller Methoden und Klassen ist in der Anlage unten zu finden.

Kehren wir zur Klasse CNetCreatorPanel des Tools zurück und erstellen wir eine Methode für die Behandlung des Ereignisses zum Drücken der Schaltfläche zum Speichern des Modells, die die oben genannten Methoden zum Erstellen eines neuen Modells in einer einzigen Sequenz kombiniert.

Zu Beginn der Methode OnClickSaveButton wird der Nutzer aufgefordert, eine Datei zum Speichern des Modells anzugeben. Hierfür verwenden wir die bereits bekannte Funktion FileSelectDialog. Diesmal ändern wir das Flag, um anzuzeigen, dass eine Datei zum Schreiben erstellt wird. Außerdem geben wir den Standard-Dateinamen an.

bool CNetCreatorPanel::OnClickSaveButton(void)
  {
   string filenames[];
   if(FileSelectDialog("Select files to save", NULL,
                       "Neuron Net (*.nnw)|*.nnw|All files (*.*)|*.*",
                       FSD_WRITE_FILE, filenames, "NewModel.nnw") <= 0)
     {
      Print("File not selected");
      return false;
     }

Anschließend erstellen wir eine neue Instanz der Klasse des neuronalen Netzes und überprüfen das Ergebnis der Operation.

   string file_name = filenames[0];
   if(StringLen(file_name) - StringLen(EXTENSION) > StringFind(file_name, EXTENSION))
      file_name += EXTENSION;
   CNetModify* new_model = new CNetModify();
   if(!new_model)
      return false;

Nach erfolgreicher Erstellung eines neuen Modells implementieren wir eine Schleife, um die erforderliche Anzahl von neuronalen Schichten zu kopieren. Für alle kopierten neuronalen Schichten sollte das Lernflag auf false gesetzt werden. Daher deaktivieren wir den Prozess der Aktualisierung der Gewichte dieser Schichten im weiteren Verlauf des Trainings. Später können wir dieses Flag für alle neuronalen Schichten des Modells programmatisch ändern, indem wir buchstäblich eine einzige Methode aufrufen.

   int total = m_spPTModelLayers.Value();
   bool result = true;
   for(int i = 0; i < total && result; i++)
     {
      CLayer* temp = m_Model.GetLayer((uint)i);
      if(!temp)
        {
         result = false;
         break;
        }
      CNeuronBaseOCL* neuron = temp.At(0);
      neuron.TrainMode(false);
      if(!new_model.AddLayer(temp))
         result = false;
     }

Nachdem die Iterationen des Kopierens neuronaler Schichten abgeschlossen sind, rufen wir die obige Methode auf, um neuronale Schichten hinzuzufügen, wodurch die Erstellung eines neuen Modells abgeschlossen wird.

   new_model.SetOpenCL(m_Model.GetOpenCL());
   if(result && m_arAddLayers.Total() > 0)
      if(!new_model.AddLayers(GetPointer(m_arAddLayers)))
         result = false;

Danach müssen wir das erstellte Modell nur noch speichern.

   if(result && !new_model.Save(file_name, 1.0e37f, 100, 0, 0, false))
      result = false;
//---
   if(!!new_model)
      delete new_model;
   LoadModel(m_edPTModel.Text());
//---
   return result;
  }

Nach dem Speichern des Modells können wir es löschen, da das Training in einem anderen Programm durchgeführt werden wird.

Beachten Sie, dass beim Löschen des Modells auch die kopierten neuronalen Schichten gelöscht werden. Das liegt daran, dass wir keine Daten in das neue Modell kopiert haben, sondern nur Zeiger übergeben haben. Wenn wir also ein anderes Modell auf der Grundlage des bereits verwendeten erstellen wollen, müssen wir es neu laden. Um die unnötige Routine zu vermeiden, rufen wir die Methode zum Nachladen des Modells auf. Und erst danach verlassen wir die Methode.

Damit ist die Arbeit mit dem Skriptcode abgeschlossen. Als Nächstes steht die Prüfung an.


3. Tests

Um das erstellte Tool zu testen, erstellen wir den Expert Advisor NetCreator.mq5. Der EA-Code ist recht einfach und enthält nur die Verbindung der oben erstellten CNetCreatorPanel-Klasse. Die Integration der Klasse in den EA erfolgt derzeit an 3 Punkten. Initialisierung und Start des Modells mit der Funktion OnInit. Zerstören der Klasse in der Funktion OnDeinit. Übergabe von Ereignissen an die Klasse in der OnChartEvent-Methode. Der Code für alle Integrationspunkte ist unten angegeben.

#include "NetCreatorPanel.mqh"
CNetCreatorPanel Panel;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!Panel.Create(0, "NetCreator", 0, 50, 50))
      return INIT_FAILED;
   if(!Panel.Run())
      return INIT_FAILED;
//---
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
//---
   Panel.Destroy(reason);
  }

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
      Sleep(0);
   Panel.ChartEvent(id, lparam, dparam, sparam);
  }

Praktische Tests haben unsere Erwartung bestätigt, dass neuronale Schichten von einem Modell auf ein anderes übertragen werden können, wobei die Möglichkeit besteht, neue Schichten hinzuzufügen. Darüber hinaus können wir mit dem Tool ein völlig neues Modell erstellen. So können wir von der Beschreibung des erstellten Modells in den Programmcode ableiten.  


Schlussfolgerung

In diesem Artikel haben wir ein Werkzeug entwickelt, das die Übertragung eines Teils der neuronalen Schichten von einem Modell auf ein anderes ermöglicht. Es ermöglicht auch das Hinzufügen einer beliebigen Anzahl neuer Schichten mit beliebiger Architektur. Ich lade alle ein, mit ihren bereits trainierten Modellen zu experimentieren und zu sehen, wie sich eine Änderung der Architektur auf die Produktivität des Modells auswirken kann.

Sie können versuchen, verschiedene Architekturen in einem Modell zu kombinieren und eine Reihe weiterer Experimente durchführen, um die Architektur des Modells zu verändern. Wenn wir gleichzeitig die Architekturen der Ergebnis- und Quelldatenschichten beibehalten, können wir versuchen, eine völlig neue Modellarchitektur in einen bereits bestehenden Expert Advisor „einzubauen“. Dann trainieren wir das Modell und vergleichen den Einfluss der Architektur und den Fehler des Modells.


Liste der Referenzen

  1. Neuronale Netze leicht gemacht (Teil 20): Autoencoder
  2. Neuronale Netze leicht gemacht (Teil 21): Variierter Autoencoder (VAE)
  3. Neuronale Netze leicht gemacht (Teil 22): Unüberwachtes Lernen von rekurrenten Modellen

Programme, die im diesem Artikel verwendet werden

# Name Typ Beschreibung
1 NetCreator.mq5 EA   Tool für die Modellbildung
2 NetCreatotPanel.mqh Klassenbibliothek Klassenbibliothek zur Erstellung des Tools
3 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
4 NeuroNet.cl Code Base OpenCL-Programmcode-Bibliothek