Neuronale Netze leicht gemacht (Teil 24): Verbesserung des Instruments für Transfer Learning

Dmitriy Gizlyk | 4 November, 2022

Inhalt

Einführung

Im vorigen Artikel dieser Reihe haben wir ein Tool entwickelt, das die Vorteile der Technologie des Transfer Learnings nutzt. Das Ergebnis dieser Arbeit ist ein Werkzeug, das die Bearbeitung von bereits trainierten Modellen ermöglicht. Mit diesem Tool können wir eine beliebige Anzahl von neuronalen Schichten aus einem vortrainierten Modell übernehmen. Natürlich gibt es einschränkende Bedingungen. Wir nehmen nur aufeinanderfolgende Schichten, beginnend mit der ersten Datenschicht. Der Grund für diesen Ansatz liegt in der Natur der neuronalen Netze. Sie funktionieren nur dann gut, wenn die Ausgangsdaten denen ähnlich sind, die beim Training des Modells verwendet wurden.

Darüber hinaus ermöglicht das erstellte Tool nicht nur die Bearbeitung trainierter Modelle. Es können auch völlig neue erstellt werden. Dadurch kann die Beschreibung der Modellarchitektur im Programmcode vermieden werden. Wir müssen nur ein Modell mit Hilfe des Tools beschreiben. Dann werden wir das Modell verfolgen und verwenden, indem wir das erstellte neuronale Netz aus einer Datei hochladen. So kann mit verschiedenen Architekturen experimentiert werden, ohne den Programmcode zu ändern. Dies erfordert nicht einmal die Neukompilierung des Programms. Sie müssen lediglich die Modelldatei ändern.

Eine solche vorteilhaftes Hilfsmittel sollte auch so nutzerfreundlich wie möglich sein. Daher werden wir in diesem Artikel versuchen, seine Nutzerfreundlichkeit zu verbessern.


1. Anzeige der vollständigen Informationen über die neuronale Schicht

Wir beginnen damit, die Nutzerfreundlichkeit des Tools zu verbessern, indem wir die Menge an Informationen über jede neuronale Schicht erhöhen. Wie Sie sich erinnern, haben wir im letzten Artikel alle möglichen Informationen über die Architektur der einzelnen neuronalen Schichten des trainierten Modells gesammelt. Das Tool zeigte dem Nutzer jedoch nur den Typ der neuronalen Schicht und die Anzahl der Ausgangsneuronen an. Das ist in Ordnung, wenn wir mit einem Modell arbeiten und uns dessen Architektur merken. Aber wenn Sie mit einer großen Anzahl von Modellen experimentieren wollen, reicht diese Menge an Informationen natürlich nicht aus.

Andererseits benötigen mehr Informationen auch mehr Platz auf der Informationstafel. Wahrscheinlich wäre es nicht gut, dem Modellinformationsfenster einen horizontalen Bildlauf hinzuzufügen. Deshalb habe ich beschlossen, Informationen über jede neuronale Schicht in mehreren Zeilen anzuzeigen. Die ausgegebenen Informationen müssen leicht zu lesen sein. Er sollte nicht wie ein riesiger, schwer verständlicher Textblock aussehen. Um einen Text in Blöcke zu unterteilen, fügen wir visuelle Trennlinien zwischen den Beschreibungen zweier aufeinander folgender neuronaler Schichten ein.

Die Entscheidung, den Text in mehrere Zeilen aufzuteilen, scheint eine einfache Lösung zu sein, aber der Umsetzungsprozess erforderte auch nicht standardisierte Ansätze. Der Punkt ist, dass wir die Listenklasse CListView verwenden, um Informationen über die Architektur des Modells anzuzeigen. Jede Zeile steht dabei für ein einzelnes Element der Liste. Außerdem gibt es keine Möglichkeit, ein Element in mehreren Zeilen darzustellen und mehrere Elemente zu einer Einheit zusammenzufassen. Das Hinzufügen einer solchen Funktionalität erfordert Änderungen am Algorithmus und an der Klassenarchitektur. In der Praxis wird dies zur Schaffung einer neuen Klasse von Kontrollobjekten führen. In diesem Fall könnte man von der Klasse CListView erben oder ein völlig neues Element erstellen. Aber das erfordert zu viel Aufwand, den ich nicht geplant hatte.

Daher beschloss ich, eine bereits vorhandene Klasse zu verwenden, allerdings mit ein paar Änderungen, ohne den Code der Klasse zu verändern. Wie bereits erwähnt, werden wir Trennlinien verwenden, um den Text visuell in Blöcke für einzelne neuronale Schichten zu unterteilen. Die Trennzeichen teilen den gesamten Text mit der Beschreibung der Modellarchitektur in einzelne neuronale Schichtblöcke auf. Wir werden die Informationen für jede neuronale Schicht auch visuell gruppieren.

Neben der visuellen Gruppierung brauchen wir aber auch auf der Programmebene ein Verständnis dafür, zu welcher neuronalen Schicht ein Listenelement gehört. Im vorigen Artikel haben wir die Anzahl der kopierten neuronalen Schichten geändert, indem wir eine einzelne neuronale Schicht des trainierten Modells mit der Maus ausgewählt und die ausgewählte Schicht aus der Liste der dem neuen Modell hinzugefügten neuronalen Schichten gelöscht haben. In beiden Fällen brauchen wir ein klares Verständnis der Entsprechung zwischen dem ausgewählten Element und der spezifischen neuronalen Schicht.

Beim Hinzufügen jedes Elements zur Liste haben wir den Text und einen numerischen Wert angegeben. Normalerweise wird ein numerischer Wert zur schnellen Identifizierung eines ausgewählten Elements verwendet. Zuvor haben wir für jedes Element einen eigenen Wert angegeben. Es ist aber auch möglich, einen Wert für mehrere Elemente zu verwenden. Natürlich ist es bei diesem Ansatz schwierig, jedes Element der Liste zu identifizieren. Das tun wir aber im Moment nicht. Wir müssen nur eine Gruppe von Elementen identifizieren. Mit dieser Fähigkeit können wir also nicht nur ein einzelnes Element, sondern eine ganze Gruppe von Elementen identifizieren.

bool  AddItem( 
   const string  item,     // text 
   const long    value     // value 
   )

Tatsächlich bietet diese Lösung einen weiteren Vorteil. Die Klasse CListView verfügt über die Methode SelectByValue. Der Hauptzweck dieser Methode besteht darin, ein Element nach seinem numerischen Wert auszuwählen. Sein Algorithmus findet das erste Element mit dem angegebenen Zahlenwert unter allen Elementen der Liste und wählt es aus. Durch die Organisation der Behandlung des Änderungsereignisses der Listenauswahl können wir den Wert des vom Nutzer ausgewählten Elements lesen und die Klasse auffordern, das erste Element der Liste mit diesem Wert auszuwählen. Damit wird der Beginn der Gruppe visualisiert. Ich denke, das ist eine ziemlich praktische Funktion.

bool  SelectByValue( 
   const long  value     // value 
   )

Lassen Sie uns nun die Umsetzung der beschriebenen Ansätze betrachten. Zunächst müssen wir eine textuelle Darstellung der Beschreibung der neuronalen Schichtarchitektur implementieren, um sie auf dem Panel anzuzeigen. Zu diesem Zweck erstellen wir die Methode LayerDescriptionToString. Die Methode erhält als Parameter einen Zeiger auf das Objekt der Architekturbeschreibung der neuronalen Schicht und einen Zeiger auf das dynamische Array von Zeichenketten, in das die textuelle Beschreibung der neuronalen Schicht geschrieben wird. Jedes Element des Arrays ist eine eigene Zeile in der Liste der Modellarchitekturbeschreibungen. Im obigen Sinne ist jedes Element eine eigene Gruppe von Elementen in der Liste zur Beschreibung einer neuronalen Schicht. Durch die Verwendung eines dynamischen Arrays können wir Elementgruppen unterschiedlicher Größe organisieren, je nach der Notwendigkeit, eine bestimmte neuronale Schicht zu beschreiben.

Die Methode erhält die Anzahl der Elemente im Array.

int CNetCreatorPanel::LayerDescriptionToString(const CLayerDescription *layer, string& result[])
  {
   if(!layer)
      return -1;

Im Hauptteil der Methode überprüfen wir zunächst die Gültigkeit des empfangenen Zeigers auf die Beschreibung der Architektur der neuronalen Schicht.

Als Nächstes werden wir eine lokale Variable vorbereiten und das resultierende dynamische Array löschen.

   string temp;
   ArrayFree(result);

Als Nächstes erstellen wir eine Textbeschreibung der neuronalen Schicht in Abhängigkeit von ihrem Typ. Wir werden nicht sofort mit dem dynamischen Array von Zeichenketten arbeiten. Stattdessen wird die gesamte Beschreibung in eine einzige Zeichenkette geschrieben. Wir fügen jedoch an der Stelle, an der die Zeichenkette aufgeteilt werden soll, ein Trennzeichen ein. In diesem Beispiel habe ich einen Backslash „\“ verwendet. Ich habe die Funktion StringFormat verwendet, um den Text mit diesem Markup richtig zusammenzusetzen. Die Funktion erzeugt mit minimalem Aufwand formatierten Text.

Nachdem wir eine formatierte String-Beschreibung der Architektur der neuronalen Schicht erstellt haben, werden wir die Funktion StringSplit verwenden und unseren Text in Zeilen aufteilen. Diese Funktion unterteilt den Text in Zeilen entsprechend den Trennelementen, die im vorherigen Schritt sorgfältig in den Text eingefügt wurden. Der Vorteil dieser Funktion liegt auch darin, dass sie die Größe des dynamischen Arrays auf die erforderliche Größe erhöht. DAHER brauchen wir diesen Teil nicht zu kontrollieren.

   switch(layer.type)
     {
      case defNeuronBaseOCL:
         temp = StringFormat("Dense (outputs %d, \activation %s, \optimization %s)", 
                layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronConvOCL:
         temp = StringFormat("Convolution (outputs %d, \window %d, step %d, window out %d, \activation %s, \optimization %s)",
                layer.count * layer.window_out, layer.window, layer.step, layer.window_out, EnumToString(layer.activation),

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronProofOCL:
         temp = StringFormat("Proof (outputs %d, \window %d, step %d, \optimization %s)",
                layer.count, layer.window, layer.step, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronAttentionOCL:
         temp = StringFormat("Self Attention (outputs %d, \units %s, window %d, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronMHAttentionOCL:
         temp = StringFormat("Multi-Head Attention (outputs %d, \units %s, window %d, heads %s, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.step, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronMLMHAttentionOCL:
         temp = StringFormat("Multi-Layer MH Attention (outputs %d, \units %s, window %d, key size %d, \heads %s, layers %d,
                              \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.window_out, layer.step, layer.layers,

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronDropoutOCL:
         temp = StringFormat("Dropout (outputs %d, \probability %d, \optimization %s)",
                layer.count, layer.probability, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronBatchNormOCL:
         temp = StringFormat("Batchnorm (outputs %d, \batch size %d, \optimization %s)",
                layer.count, layer.batch, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronVAEOCL:
         temp = StringFormat("VAE (outputs %d)", layer.count);
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronLSTMOCL:
         temp = StringFormat("LSTM (outputs %d, \optimization %s)", layer.count, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      default:	
         temp = StringFormat("Unknown type %#x (outputs %d, \activation %s, \optimization %s)",
                layer.type, layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
     }

Nachdem Sie Beschreibungen für alle bekannten neuronalen Schichten erstellt haben, vergessen wir nicht, eine Standardbeschreibung für unbekannte Typen hinzuzufügen. So können wir den Nutzer über die Erkennung einer unbekannten neuronalen Schicht informieren und ihn vor einer unbeabsichtigten Verletzung der Modellintegrität schützen.

Am Ende der Methode geben wir die Größe des Arrays der Ergebnisse an den Aufrufer zurück.

//---
   return ArraySize(result);
  }

Als Nächstes wenden wir uns der Methode LoadModel zu, die wir bereits im vorherigen Artikel besprochen haben. Wir werden nicht die gesamte Methode ändern, sondern nur den Hauptteil der Schleife, die der Liste Elemente hinzufügt. Wie zuvor holen wir uns im Schleifenkörper zunächst einen Zeiger auf das nächste Schichtbeschreibungsobjekt aus dem dynamischen Array. Wir überprüfen sofort die Gültigkeit des empfangenen Zeigers.

   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;

Dann bereiten wir ein dynamisches Array von Strings vor und rufen die oben beschriebene Methode LayerDescriptionToString auf, um eine Textbeschreibung der neuronalen Schicht zu erzeugen. Nach Abschluss der Methode erhalten wir ein Array mit Textbeschreibungen und der Anzahl der darin enthaltenen Elemente. Wenn ein Fehler auftritt, gibt die Methode ein leeres Array und -1 anstelle der Array-Größe zurück. Wir informieren den Nutzer über den Fehler und beenden die Methode.

      string items[];
      int total_items = LayerDescriptionToString(temp, items);
      if(total_items < 0)
        {
         printf("%s %d Error at layer %d: %d", __FUNCSIG__, __LINE__, i, GetLastError());
         return false;
        }

Wenn der Beschreibungstext erfolgreich erstellt wurde, fügen wir zunächst das Blocktrennungselement hinzu. Dann geben wir in einer verschachtelten Schleife den gesamten Inhalt des Textfeldes aus, das die neuronale Schicht beschreibt.

      if(!m_lstPTModel.AddItem(StringFormat("____ Layer %d ____", i + 1), i + 1))
         return false;
      for(int it = 0; it < total_items; it++)
         if(!m_lstPTModel.AddItem(items[it], i + 1))
            return false;
     }

Achten Sie darauf, dass wir bei der Angabe der Gruppen-ID 1 zur Ordnungszahl der neuronalen Schicht im dynamischen Array mit der Modellbeschreibung hinzufügen. Dies ist erforderlich, da die Indizierung im Array mit 0 beginnt. Wenn wir 0 als numerischen Identifikator angeben, wird er von der Klasse CListView automatisch durch die Gesamtzahl der Elemente in der Liste ersetzt. Wir möchten nicht, dass wir einen Zufallswert anstelle einer Gruppen-ID erhalten.

Der übrige Code der LoadModel-Methode hat sich nicht geändert. Der gesamte EA-Code ist im Anhang zu finden. Außerdem enthält der Anhang die Codes aller im Programm verwendeten Methoden und Klassen. Insbesondere können Sie ähnliche Ergänzungen an der Methode zur Anzeige der Beschreibung des neuen Modells ChangeNumberOfLayers sehen.

Bitte beachten Sie, dass in der Methode ChangeNumberOfLayers die Informationen über das Modell aus zwei dynamischen Arrays gesammelt werden, die Beschreibungen der Modellarchitektur enthalten. Der erste Teil beschreibt die Architektur des Spendermodells. Daraus entnehmen wir die Beschreibung der kopierten neuronalen Schicht. Das zweite Array enthält eine Beschreibung der neuronalen Netze, die wir hinzufügen.

Nachdem wir die Beschreibungen der Modellarchitektur ausgegeben haben, gehen wir zu den Methoden über, die Ereignisse von Änderungen in den erstellten Listenzuständen verarbeiten.

ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)

Wenn der Nutzer eine beliebige Zeile in der Liste auswählt, wird die Auswahl wie oben beschrieben auf die erste Zeile des angegebenen Blocks verschoben. Dazu holen wir uns einfach die Gruppen-ID des vom Nutzer ausgewählten Elements und weisen das Programm an, das erste Element mit der angegebenen ID auszuwählen. Dieser Vorgang wird durch die Methode SelectByValue implementiert.

bool CNetCreatorPanel::OnChangeListNewModel(void)
  {
   long value = m_lstNewModel.Value();
//---
   return m_lstNewModel.SelectByValue(value);
  }

Dadurch werden die angezeigten Informationen über die Modellarchitektur erweitert. Die Informationsmenge ist minimal ausreichend und spezifisch für einen neuronalen Schichttyp. Der Nutzer sieht also nur die relevanten Informationen über eine bestimmte neuronale Schicht. Außerdem wird das Fenster nicht mit zusätzlichen Informationen überfrachtet.


2. Aktivierung verwendeter & Deaktivierung nicht verwendeter Eingabefelder

Die nächste Änderung betrifft die Dateneingabefelder. So seltsam es klingen mag, aber sie bieten ein ziemlich großes Feld für die Phantasie. Das erste, was Ihnen wahrscheinlich ins Auge fällt, ist die Menge der eingegebenen Informationen. Das Panel bietet Eingabefelder für alle Elemente der Klasse CLayerDescription, die die Architektur der neuronalen Schicht beschreiben. Ich sage nicht, dass es schlecht ist. Der Nutzer kann alle angegebenen Daten sehen und sie in beliebiger Reihenfolge und bei Bedarf ändern, bevor er eine Ebene hinzufügt. Wir wissen jedoch, dass nicht alle diese Felder für alle neuronalen Schichten relevant sind.

Für eine vollständig verknüpfte neuronale Schicht genügt es zum Beispiel, nur drei Parameter anzugeben: die Anzahl der Neuronen, die Aktivierungsfunktion und die Methode zur Parameteroptimierung. Die übrigen Parameter sind dafür irrelevant. Bei der neuronalen Faltungsschicht müssen wir die Größe des Eingabedatenfensters und dessen Schritt angeben. Die Anzahl der Ausgabeelemente hängt von der Größe des Quelldatenpuffers und den beiden angegebenen Parametern ab.

Im rekurrenten LSTM-Block sind die Aktivierungsfunktionen durch die Blockarchitektur definiert und müssen daher nicht spezifiziert werden. 

Nun, der Nutzer könnte alle diese Funktionen kennen. Ein gut durchdachtes Werkzeug sollte den Nutzer jedoch vor möglichen „mechanischen“ Fehlern warnen. Es gibt zwei Möglichkeiten der Vorbeugung. Wir können irrelevante Elemente aus dem Panel entfernen oder sie einfach uneditierbar machen.

Jede Option hat ihre Vor- und Nachteile. Zu den Vorteilen der ersten Option gehört, dass die Anzahl der Eingabefelder auf dem Panel reduziert wird. So kann das Panel kompakter sein. Der Nachteil ist eine komplexere Implementierung. Da wir die Elemente auf der Tafel jedes Mal neu anordnen müssen. Gleichzeitig kann die ständige Neuanordnung von Objekten den Nutzer verwirren und zu Fehlern führen.

Meiner Meinung nach ist diese Methode gerechtfertigt, wenn Sie eine große Menge an Daten eingeben müssen. Durch das Entfernen unnötiger Objekte wird das Panel kompakter und übersichtlicher.

Die zweite Option ist akzeptabel, wenn wir eine kleine Anzahl von Elementen haben. Wir können ganz einfach alle Elemente auf der Tafel auf einmal anordnen. Außerdem verwirren wir den Nutzer nicht, indem wir ihn unnötig auf dem Bedienfeld herumführen. Der Nutzer kann sich seinen Standort visuell merken, was die Gesamtleistung verbessert.

Wir haben bereits alle Eingabefelder auf dem Schnittstellenpanel platziert. Daher halte ich die zweite Umsetzungsoption für akzeptabel.

Wir haben bereits eine architektonische Lösung. Aber wir werden noch ein wenig weiter gehen. Das Panel enthält Felder mit Dropdown-Listen und direkte Eingabefelder. Im Dropdown-Feld kann nur eine der verfügbaren Optionen ausgewählt werden. In Werteingabefeldern kann der Nutzer jedoch physisch jeden beliebigen Text eingeben.

Wir erwarten jedoch, dass wir dort einen ganzzahligen Wert erhalten. Logischerweise sollten wir eine Überprüfung der eingegebenen Informationen hinzufügen, bevor wir sie an das Objekt weitergeben, das die Architektur der erstellten neuronalen Schicht beschreibt. Um die Richtigkeit der Informationen mit dem Nutzer zu teilen, werden die eingegebenen Informationen sofort nach der Eingabe des Textes validiert. Nach der Validierung ersetzen wir die vom Nutzer in das Feld eingegebenen Informationen durch die vom Tool akzeptierten Informationen. So kann der Nutzer den Unterschied zwischen den eingegebenen und den gelesenen Informationen erkennen. Falls erforderlich, kann der Nutzer die Daten weiter korrigieren.

Und noch eine Sache. Bei der Beschreibung der Architektur der neuronalen Schicht in der Klasse CLayerDescription haben wir Elemente mit doppeltem Verwendungszweck. Zum Beispiel, step spezifiziert für die Faltungsschicht und die Subsample-Schicht einen Schritt des Quelldatenfensters an. Derselbe Parameter wird jedoch zur Angabe der Anzahl der Aufmerksamkeitsköpfe bei der Beschreibung der neuronalen Schichten der Aufmerksamkeit (attention) verwendet.

Der Parameter window_out gibt die Anzahl der Filter in der Faltungsschicht und die Größe der internen Schlüsselschicht im Aufmerksamkeitsblock an.

Um die Schnittstelle nutzerfreundlicher zu gestalten, ist es besser, die Textbeschriftungen bei der Auswahl des entsprechenden Typs der neuronalen Schicht zu ändern.

Der Nutzer wird nicht mit dem Problem der Neuanordnung im Schnittstellenfenster konfrontiert. Das Feld selbst ändert sich nicht. Nur die Informationen daneben ändern sich. Wenn der Nutzer die neuen Daten nicht beachtet und automatisch Informationen in das entsprechende Feld einträgt, führt dies nicht zu Fehlern in der Organisation des Modells. Die Daten werden in jedem Fall in das gewünschte Element der Beschreibung der Schichtenarchitektur gesendet.

Um die oben genannten Lösungen umzusetzen, müssen wir einen Schritt zurücktreten und einige vorbereitende Arbeiten durchführen.

Zunächst einmal haben wir bei der Erstellung von Textbeschriftungen auf der Nutzeroberfläche keine Zeiger auf die entsprechenden Objekte gespeichert. Wenn wir nun den Text einiger dieser Objekte ändern wollen, müssen wir sie im allgemeinen Array der Objekte suchen. Um dies zu vermeiden, kehren wir zur Methode CreateLabel zur Erstellung von Textetiketten zurück. Nach Abschluss der Methodenoperationen geben wir anstelle eines logischen Ergebnisses einen Zeiger auf das erstellte Objekt zurück.

CLabel* 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 NULL;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return NULL;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return NULL;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return NULL;
     }
//---
   return tmp_label;
  }

Natürlich werden wir nicht Zeiger auf alle Kennzeichen speichern. Wir werden nur zwei Objekte speichern. Zu diesem Zweck werden zwei zusätzliche Variablen deklariert. Obwohl wir dynamische Zeiger auf Objekte verwenden, werden wir sie nicht in den Destruktor unserer Werkzeugklasse aufnehmen. Diese Objekte werden trotzdem aus der Liste aller Werkzeugobjekte gelöscht. Gleichzeitig erhalten wir aber auch direkten Zugang zu den von uns benötigten Objekten.

   CLabel*           m_lbWindowOut;
   CLabel*           m_lbStepHeads;

Wir werden Zeiger auf neue Variablen in die Create-Methode unserer Klasse schreiben. Die Methode erfordert kleine Änderungen, die im Folgenden dargestellt werden. Der Rest des Methodencodes bleibt unverändert. Der vollständige Code der Methoden ist im Anhang zu finden.

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;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbStepHeads = CreateLabel(8, "Step", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbStepHeads)
      return false;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbWindowOut = CreateLabel(9, "Window Out", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbWindowOut)
      return false;
//---
...............
...............
//---
   return true;
  }

Der nächste Schritt in unseren Vorbereitungen besteht darin, eine Methode zum Ändern des Status des Eingabefeldes zu entwickeln. Die Standardklasse CEdit verfügt bereits über die Struktur ReadOnly zur Änderung des Objektstatus. Diese Methode ermöglicht jedoch keine Visualisierung des Status. Sie sperrt nur die Möglichkeit, Daten einzugeben. Wir brauchen jedoch eine visuelle Trennung der für die Eingabe verfügbaren und nicht verfügbaren Objekte. Wir werden nichts Neues erfinden. Wir wollen die Objekte mit einer Hintergrundfarbe hervorheben. Bearbeitbare Felder haben einen weißen Hintergrund, und nicht bearbeitbare Felder haben eine Hintergrundfarbe, die der Farbe des Bereichs entspricht.

Diese Funktionsweise wird in der Methode EditReedOnly implementiert. Wir übergeben im Methodenparameter einen Zeiger auf das Objekt und ein neues Statusflag. Im Methodenrumpf übergeben wir das empfangene Flag an die Methode ReadOnly des Eingabeobjekts und setzen den Hintergrund des Objekts entsprechend dem angegebenen Flag.

bool CNetCreatorPanel::EditReedOnly(CEdit& object, const bool flag)
  {
   if(!object.ReadOnly(flag))
      return false;
   if(!object.ColorBackground(flag ? CONTROLS_DIALOG_COLOR_CLIENT_BG : CONTROLS_EDIT_COLOR_BG))
      return false;
//---
   return true;
  }

Achten wir nun auf die Aktivierungsfunktionen. Oder besser gesagt, in die Dropdown-Liste der verfügbaren Aktivierungsfunktionen. Nicht alle Arten von neuronalen Schichten erfordern Dropdown-Listen. Einige Architekturen bieten einen vordefinierten Aktivierungsfunktionstyp, der nicht durch die Liste geändert werden kann. Ein Beispiel hierfür ist der LSTM-Block, die Schicht der Unterproben und die Aufmerksamkeitsblöcke. Die Klasse CComboBox bietet jedoch keine Methode, die die Funktionalität der Klasse in irgendeiner Weise blockieren würde. Daher werden wir einen Workaround verwenden und die Liste der verfügbaren Aktivierungsfunktionen von Fall zu Fall ändern. Wir werden separate Methoden für das Auffüllen der Liste der verfügbaren Aktivierungsfunktionen erstellen.

In der Tat gibt es nur zwei solcher Methoden. Eine davon ist allgemein und gibt die Aktivierungsfunktionen an — ActivationListMain. Die zweite ist leer — ActivationListEmpty, die nur eine Auswahlmöglichkeit „None“ hat.

Um den Algorithmus der Methodenkonstruktion zu verstehen, betrachten wir den Code der Methode ActivationListMain. Wir löschen zu Beginn der Methode die bestehende Liste der Elemente der verfügbaren Aktivierungsfunktionen. Dann füllen wir die Liste in einer Schleife mit der Methode ItemAdd und der Funktion EnumToString.

Dabei ist zu beachten, dass die Kodierung der Elemente in der Enumeration der Aktivierungsfunktionen mit -1 für Keine beginnt. Die nächste Funktion — der hyperbolische Tangens TANH — hat den Index 0. Dies ist aus dem Grund nicht gut, der oben bei der Beschreibung des Auffüllens der Liste der Beschreibungen genannt wurde. Denn die Dropdown-Liste ist die Klasse CListView. Um den Nullwert des Listenbezeichners auszuschließen, fügen wir daher einfach eine kleine Konstante an den Enumerationsbezeichner an.

Nachdem wir die Liste der verfügbaren Aktivierungsfunktionen ausgefüllt haben, setzen wir den Standardwert und beenden die Methode.

bool CNetCreatorPanel::ActivationListMain(void)
  {
   if(!m_cbActivation.ItemsClear())
      return false;
   for(int i = -1; i < 3; i++)
      if(!m_cbActivation.ItemAdd(EnumToString((ENUM_ACTIVATION)i), i + 2))
         return false;
   if(!m_cbActivation.SelectByValue((int)DEFAULT_ACTIVATION + 2))
      return false;
//---
   return true;
  }

Eine weitere Methode, die wir brauchen, wird uns helfen, die Arbeit des Nutzers ein wenig zu automatisieren. Wie bereits erwähnt, ist bei Faltungsmodellen oder Aufmerksamkeitsblöcken die Anzahl der Elemente am Ausgang des Modells abhängig von der Größe des Fensters der analysierten Ausgangsdaten und dessen Bewegungsschritt. Um mögliche Fehler auszuschließen und die manuelle Arbeit des Nutzers zu reduzieren, habe ich beschlossen, das Eingabefeld für die Anzahl der Blöcke zu schließen und es mit einer separaten SetCounts-Methode zu füllen.

In den Parametern dieser Methode übergeben wir den Typ der erstellten neuronalen Schicht. Die Methode gibt das boolesche Ergebnis der Operation zurück.

bool CNetCreatorPanel::SetCounts(const uint position, const uint type)
  {
   const uint position = m_arAddLayers.Total();

Im Hauptteil der Methode wird zunächst die Anzahl der Elemente in der Ausgabe der vorherigen Schicht bestimmt. Bitte beachten Sie, dass die vorherige Schicht in einem von zwei dynamischen Arrays sein kann: Beschreibungen der Architektur des Spendermodells oder Beschreibungen der Architektur für das Hinzufügen neuer neuronaler Schichten. Wir können leicht bestimmen, woher wir die letzte neuronale Schicht nehmen. Eine neuronale Schicht wird immer am Ende der Liste hinzugefügt. Daher wird nur dann eine Schicht aus dem Spendermodell übernommen, wenn das Feld der neuen neuronalen Schichten leer ist. Nach dieser Logik überprüfen wir die Größe des dynamischen Arrays der neuen neuronalen Schichten. Fordern Sie aus dem entsprechenden Array je nach Größe einen Zeiger auf die vorherige neuronale Schicht an.

   CLayerDescription *prev;
   if(position <= 0)
     {
      if(!m_arPTModelDescription || m_spPTModelLayers.Value() <= 0)
         return false;
      prev = m_arPTModelDescription.At(m_spPTModelLayers.Value() - 1);
      if(!prev)
         return false;
     }
   else
     {
      if(m_arAddLayers.Total() < (int)position)
         return false;
      prev = m_arAddLayers.At(position - 1);
     }
   if(!prev)
      return false;

Als Nächstes zählen wir die Anzahl der Elemente im Ergebnispuffer der vorangegangenen Ebene entsprechend ihrem Typ. Wenn die Puffergröße nicht größer als 0 ist, wird die Methode mit false beendet.

   int outputs = prev.count;
   switch(prev.type)
     {
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         outputs *= prev.window;
         break;
      case defNeuronConvOCL:
         outputs *= prev.window_out;
         break;
     }
//---
   if(outputs <= 0)
      return false;

Lesen wir dann aus der Schnittstelle die Werte für die Größe des analysierten Ausgangsdatenfensters und dessen Schritt. Wir bereiten auch eine Variable vor, um das Ergebnis der Berechnung aufzuzeichnen.

   int counts = 0;
   int window = (int)StringToInteger(m_edWindow.Text());
   int step = (int)StringToInteger(m_edStep.Text());

Die Anzahl der Elemente wird in Abhängigkeit von der Art der zu erstellenden neuronalen Schicht berechnet. Um die Anzahl der Elemente der Faltungsschicht und der Untersample-Schicht zu berechnen, benötigen wir die Größe des analysierten Eingangsdatenfensters und dessen Schritt.

   switch(type)
     {
      case defNeuronConvOCL:
      case defNeuronProofOCL:
         if(step <= 0)
            break;
         counts = (outputs - window - 1 + 2 * step) / step;
         break;

Bei der Verwendung von Aufmerksamkeitsblöcken ist die Schrittweite gleich der Fenstergröße. Reduzieren wir die Formel mit Hilfe mathematischer Regeln.

      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         if(window <= 0)
            break;
         counts = (outputs + window - 1) / window;
         break;

Bei Verwendung der latenten Schicht des Variations-Autoencoders ist die Schichtgröße genau zweimal kleiner als die vorherige.

      case defNeuronVAEOCL:
         counts = outputs / 2;
         break;

In allen anderen Fällen setzen wir die Größe der neuronalen Schicht gleich der Größe der vorherigen Schicht. Dies kann verwendet werden, wenn eine Batch-Normalisierung oder eine Dropout-Ebene deklariert wird.

      default:
         counts = outputs;
         break;
     }
//---
   return m_edCount.Text((string)counts);
  }

Übertragen wir den empfangenen Wert an das entsprechende Schnittstellenelement.

Jetzt haben wir genügend Mittel, um Schnittstellenänderungen je nach Art der zu erstellenden neuronalen Schicht zu organisieren. Sehen wir uns also an, wie wir das machen können. Diese Funktionalität ist in der Methode OnChangeNeuronType implementiert. Der Name wird so genannt, weil wir ihn jedes Mal aufrufen, wenn der Nutzer den Typ der neuronalen Schicht ändert.

Die angegebene Methode enthält keine Parameter und gibt das logische Ergebnis der Operation zurück. Im Hauptteil der Methode wird zunächst der Typ der vom Nutzer ausgewählten neuronalen Schicht festgelegt.

bool CNetCreatorPanel::OnChangeNeuronType(void)
  {
   long type = m_cbNewNeuronType.Value();

Außerdem verzweigt sich der Algorithmus je nach ausgewähltem neuronalen Schichttyp. Der Algorithmus für jede neuronale Schicht wird ähnlich sein. Aber fast jede neuronale Schicht hat ihre eigenen Nuancen. Bei einer vollständig verknüpften neuronalen Schicht belassen wir nur ein aktives Eingabefeld für die Anzahl der Neuronen und laden die vollständige Liste der möglichen Aktivierungsfunktionen.

   switch((int)type)
     {
      case defNeuronBaseOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListMain())
            return false;
         break;

Bei einer Faltungsschicht werden drei weitere Eingabefelder aktiv. Dazu gehören die Größe des analysierten Quelldatenfensters und dessen Schritt sowie die Größe des Ergebnisfensters (die Anzahl der Filter). Wir aktualisieren auch die Werte von zwei Textbeschriftungen und starten die Neuberechnung der Anzahl der Elemente in einer neuronalen Schicht in Abhängigkeit von der Größe des Quelldatenfensters und dem Schritt. Beachten Sie, dass wir die Anzahl der Elemente für einen Filter zählen. Das Ergebnis hängt also nicht von der Anzahl der verwendeten Filter ab.

      case defNeuronConvOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!m_lbWindowOut.Text("Window Out"))
            return false;
         if(!ActivationListMain())
            return false;
         if(!SetCounts(defNeuronConvOCL))
            return false;
         break;

Für die Unterprobenschicht geben wir die Anzahl der Filter und die Aktivierungsfunktion nicht an. In unserer Implementierung verwenden wir immer den Maximalwert als Aktivierungsfunktion für die Unterprobenschicht. Löschen wir daher die Liste der verfügbaren Aktivierungsfunktionen. Aber wie bei der Faltungsschicht beginnen wir mit der Berechnung der Anzahl der Elemente der erstellten Schicht.

      case defNeuronProofOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!SetCounts(defNeuronProofOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Bei der Deklaration des LSTM-Blocks wird die Liste der Aktivierungsfunktionen ebenfalls nicht verwendet, daher sollte sie gelöscht werden. Es ist nur ein Eingabefeld verfügbar — die Anzahl der Elemente in der neuronalen Schicht.

      case defNeuronLSTMOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Um die Dropout-Schicht zu initialisieren, müssen wir nur die Werte für die Dropout-Wahrscheinlichkeit der Neuronen angeben. Es wird keine Aktivierungsfunktion verwendet. Die Anzahl der Elemente ist gleich der Größe der vorherigen neuronalen Schicht.

      case defNeuronDropoutOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, false) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronDropoutOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Ein ähnlicher Ansatz gilt für die Ebene der Stapelnormalisierung. Hier geben wir jedoch die Losgröße an.

      case defNeuronBatchNormOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, false) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronBatchNormOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Je nach Aufmerksamkeitsmethode aktivieren wir die Eingabefelder für die Anzahl der Aufmerksamkeitsköpfe und neuronalen Schichten im Block. Die Textbeschriftungen für die entsprechenden Eingabefelder werden geändert.

      case defNeuronAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

      case defNeuronMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!SetCounts(defNeuronMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

      case defNeuronMLMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, false) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!m_lbWindowOut.Text("Keys size"))
            return false;
         if(!SetCounts(defNeuronMLMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Für die latente Schicht des Variations-Autoencoders müssen keine Daten eingegeben werden. Wir wählen nur den Ebenentyp aus und fügen ihn dem Modell hinzu.

      case defNeuronVAEOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         if(!SetCounts(defNeuronVAEOCL))
            return false;
         break;

Wenn der in den Parametern angegebene Typ der neuronalen Schicht nicht gefunden wird, wird die Methode mit „false“ abgeschlossen.

      default:
         return false;
         break;
     }
//---
   return true;
  }

Wenn alle Operationen der Methode erfolgreich abgeschlossen sind, wird die Methode mit einem positiven Ergebnis beendet.

Nun müssen wir den Start der beschriebenen Methode zum richtigen Zeitpunkt organisieren. Wir werden das Ereignis verwenden, das sich auf eine Änderung des Wertes des Elements für die Auswahl des Ebenentyps bezieht, und wir werden einen entsprechenden Ereignisbehandler hinzufügen.

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)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
EVENT_MAP_END(CAppDialog)

Durch die Implementierung der oben beschriebenen Methoden haben wir die Aktivierung und Deaktivierung von Eingabefeldern in Abhängigkeit vom Typ der ausgewählten neuronalen Schicht organisiert. Wir haben aber auch über die Kontrolle der Dateneingabe gesprochen.

In allen Eingabefeldern erwarten wir ganze Zahlen größer als Null. Die einzige Ausnahme ist der Wert der Wahrscheinlichkeit, dass ein Element ausfällt, in der Schicht Dropout. Dies kann ein realer Wert zwischen 0 und 1 sein. Daher benötigen wir zwei Methoden zur Validierung der eingegebenen Daten. Eine für die Wahrscheinlichkeit und eine für alle anderen Elemente.

Der Algorithmus beider Methoden ist recht einfach. Zunächst lesen wir den vom Nutzer eingegebenen Textwert, wandeln ihn in einen numerischen Wert um und prüfen, ob er im Bereich der gültigen Werte liegt. Geben Sie den empfangenen Wert in das entsprechende Fenster der Schnittstelle zurück. Der Nutzer braucht nur zu prüfen, ob die Daten richtig interpretiert wurden.

bool CNetCreatorPanel::OnEndEditProbability(void)
  {
   double value = StringToDouble(m_edProbability.Text());
   return m_edProbability.Text(DoubleToString(fmax(0, fmin(1, value)), 2));
  }

bool CNetCreatorPanel::OnEndEdit(CEdit& object)
  {
   long value = StringToInteger(object.Text());
   return object.Text((string)fmax(1, value));
  }

Beachten Sie, dass wir bei der Überprüfung der Korrektheit des Wahrscheinlichkeitswertes das Eingabefeld eindeutig identifizieren. Um jedoch ein Objekt in der zweiten Methode zu identifizieren, werden wir den entsprechenden Objektzeiger in den Methodenparametern übergeben. Hier liegt eine weitere Herausforderung. Die vorgeschlagenen Makros zur Ereignisbehandlung verfügen nicht über ein geeignetes Makro zur Übergabe eines Zeigers auf das aufrufende Objekt an die Ereignisbehandlungsmethode. Wir müssen also ein solches Makro hinzufügen.

#define ON_EVENT_CONTROL(event,control,handler)          if(id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) \
                                                              { handler(control); return(true); }

Unter den Eingabefeldern können die Größe des analysierten Quelldatenfensters und dessen Schrittweite angegeben werden. Diese Parameter beeinflussen die Anzahl der Elemente in der neuronalen Schicht. Wenn wir also ihre Werte ändern, müssen wir die Größe der erstellten neuronalen Schicht neu berechnen. Das von uns verwendete Modell der Ereignisbehandlung erlaubt jedoch nur einen Handler für jedes Ereignis. Gleichzeitig können wir einen Handler verwenden, um verschiedene Ereignisse zu bedienen. Lassen Sie uns daher eine weitere Methode erstellen, die zunächst die Werte in den Eingabefeldern für die Fenstergröße und deren Schritt überprüft. Und dann rufen wir die Methode zur Neuberechnung der Größe der neuronalen Schicht auf, wobei wir den gewählten Typ der neuronalen Schicht berücksichtigen.

bool CNetCreatorPanel::OnChangeWindowStep(void)
  {
   if(!OnEndEdit(m_edWindow) || !OnEndEdit(m_edStep))
      return false;
   return SetCounts((uint)m_cbNewNeuronType.Value());
  }

Jetzt müssen wir nur noch unsere Event-Handler-Map vervollständigen. So können Sie die richtige Ereignisbehandlung zur richtigen Zeit ausführen.

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)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
ON_EVENT(ON_END_EDIT, m_edWindow, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edStep, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edProbability, OnEndEditProbability)
ON_EVENT_CONTROL(ON_END_EDIT, m_edCount, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edWindowOut, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edLayers, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edBatch, OnEndEdit)
EVENT_MAP_END(CAppDialog)


3. Hinzufügen der Behandlung von Tastaturereignissen

Wir haben gute Arbeit geleistet, um unser Transfer Learning-Tool viel bequemer und nutzerfreundlicher zu gestalten. Aber all diese Verbesserungen konzentrierten sich auf die Nutzeroberfläche, um die Bedienung mit einer Maus oder einem Touchpad zu erleichtern. Wir haben jedoch keine Möglichkeit vorgesehen, die Tastatur für die Arbeit mit dem Tool zu verwenden. Es kann zum Beispiel praktisch sein, die Anzahl der zu kopierenden neuronalen Schichten mit Hilfe der Auf- und Abwärtspfeile zu ändern. Durch Drücken der Entf-Taste kann eine Methode zum Löschen der ausgewählten neuronalen Schicht aus dem zu erstellenden Modell aufgerufen werden.

Ich werde jetzt nicht näher auf dieses Thema eingehen. Ich zeige Ihnen, wie Sie mit nur wenigen Zeilen Code die Verarbeitung von Schlüsseln mit bestehenden Ereignisbehandlungen hinzufügen können.

Alle drei oben vorgeschlagenen Funktionen sind bereits in unserem Programmcode implementiert. Sie werden ausgeführt, wenn ein bestimmtes Ereignis eintritt. Um die ausgewählte neuronale Schicht zu löschen, gibt es eine separate Schaltfläche im Bedienfeld. Die Anzahl der zu kopierenden neuronalen Schichten wird mit den Schaltflächen des CSpinEdit-Objekts geändert.

Technisch gesehen ist das Drücken der Tastaturtasten dasselbe Ereignis wie das Drücken der Maustasten oder das Bewegen der Maus. Sie wird auch von der Funktion OnChartEvent verarbeitet. Also wird die ChartEvent-Methode der Klasse aufgerufen.

Wenn ein Tastendruck-Ereignis auftritt, erhalten wir die Ereignis-ID CHARTEVENT_KEYDOWN. In der Variablen lparam wird die ID der gedrückten Taste gespeichert.

Mit dieser Eigenschaft können wir mit der Tastatur spielen und die Identifikatoren aller Tasten ermitteln, die uns interessieren. Hier sind zum Beispiel die Codes der oben genannten Schlüssel.

#define KEY_UP                               38
#define KEY_DOWN                             40
#define KEY_DELETE                           46

Kehren wir nun zur Methode ChartEvent unserer Klasse zurück. Darin haben wir eine ähnliche Methode der übergeordneten Klasse aufgerufen. Nun müssen wir die Prüfung der Ereignis-ID und der Sichtbarkeit unseres Werkzeugs hinzufügen. Die Ereignisbehandlung wird nur ausgeführt, wenn die Schnittstelle des Werkzeugs sichtbar ist. Der Nutzer sollte in der Lage sein, die Vorgänge auf dem Bedienfeld zu sehen und den Prozess visuell zu kontrollieren.

Wenn die erste Stufe der Überprüfung bestanden ist, prüfen wir den Code der gedrückten Taste. Wenn es eine entsprechende Taste in der Liste gibt, erzeugen wir ein nutzerdefiniertes Ereignis, das einer ähnlichen Aktion auf dem Panel unserer Schnittstelle entspricht.

Wenn z.B. die Schaltfläche Delete (löschen) gedrückt wird, wird das Ereignis DELETE auf dem Schnittstellenpanel erzeugt. 

void CNetCreatorPanel::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CAppDialog::ChartEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_KEYDOWN && m_spPTModelLayers.IsVisible())
     {
      switch((int)lparam)
        {
         case KEY_UP:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 2, 0.0, m_spPTModelLayers.Name() + "Inc");
            break;
         case KEY_DOWN:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 3, 0.0, m_spPTModelLayers.Name() + "Dec");
            break;
         case KEY_DELETE:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_btDeleteLayer.Id(), 0.0, m_btDeleteLayer.Name());
            break;
        }
     }
  }

Danach verlassen wir die Methode. Als Nächstes lassen wir das Programm das erzeugte Ereignis mit Hilfe der bereits vorhandenen Ereignisbehandler und Methoden behandeln.

Natürlich ist dieser Ansatz nur möglich, wenn es im Programm entsprechende Handler gibt. Sie können jedoch neue Ereignisbehandler erstellen und eindeutige Ereignisse für sie erzeugen.


Schlussfolgerung

In diesem Artikel haben wir uns verschiedene Möglichkeiten zur Verbesserung der Nutzerfreundlichkeit der Nutzeroberfläche angesehen. Sie können die Qualität der Ansätze beurteilen, indem Sie das dem Artikel beigefügte Tool testen. Ich hoffe, Sie finden dieses Tool nützlich. Ich wäre Ihnen dankbar, wenn Sie Ihre Eindrücke und Wünsche zur Verbesserung des Tools in dem entsprechenden Forumsthread mitteilen würden.

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
  4. Neuronale Netze leicht gemacht (Teil 23): Aufbau eines Tools für Transfer Learning

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 Die Programmcode-Bibliothek OpenCL