English Русский 中文 Español 日本語 Português
preview
Neuronale Netze leicht gemacht (Teil 25): Praxis des Transfer-Learnings

Neuronale Netze leicht gemacht (Teil 25): Praxis des Transfer-Learnings

MetaTrader 5Integration | 9 November 2022, 11:03
174 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Inhalt


Einführung

Wir untersuchen weiterhin die Technologie des Transfer-Learnings. In den beiden vorangegangenen Artikeln haben wir ein Tool zum Erstellen und Bearbeiten von neuronalen Netzmodellen entwickelt. Mit diesem Tool können wir einen Teil des vortrainierten Modells auf ein neues Modell übertragen und es mit neuen Entscheidungsschichten ergänzen. Dieser Ansatz könnte uns dabei helfen, das auf diese Weise erstellte Modell schneller für die Lösung neuer Probleme zu trainieren. In diesem Artikel werden wir die Vorteile dieses Ansatzes in der Praxis bewerten. Wir werden auch die Nutzerfreundlichkeit unseres Tools überprüfen.


1. Allgemeine Fragen zur Testvorbereitung

In diesem Artikel wollen wir die Vorteile des Einsatzes der Technologie des Transfer-Learnings bewerten. Am besten vergleicht man den Lernprozess von zwei Modellen zur Lösung eines Problems. Zu diesem Zweck werden wir ein „reines“ Modell verwenden, das durch Zufallsgewichte initiiert wird. Das zweite Modell wird mit der Technologie des Transfer-Learnings erstellt.

Wir können die Suche nach Fraktalen als Problem verwenden, so wie wir es beim Testen aller bisherigen Modelle in überwachten Lernmethoden getan haben. Aber was werden wir als Spendermodell für Transfer-Learning verwenden? Kommen wir zurück zu den Autoencodern. Wir haben sie als Spender für Transfer-Learning eingesetzt. Bei der Untersuchung von Autoencodern haben wir zwei Modelle von Variations-Autoencodern erstellt und trainiert. Im ersten Modell wurde der Encoder mit vollständig verbundenen neuronalen Schichten aufgebaut. Im zweiten Fall haben wir einen Encoder verwendet, der auf rekurrenten LSTM-Blöcken basiert. Dieses Mal können wir beide Modelle als Spender verwenden. So können wir auch die Effizienz dieser beiden Ansätze testen.

Damit haben wir die erste grundlegende Entscheidung bei der Vorbereitung des bevorstehenden Tests getroffen: Als Spendermodelle werden wir Variierte Autoencoder verwenden, die wir beim Studium der entsprechenden Themen trainiert haben.

Die zweite konzeptionelle Frage ist, wie wir die Modelle testen werden. Wir müssen möglichst gleiche Bedingungen für alle Modelle schaffen. Nur dann können wir den Einfluss anderer Faktoren ausschließen und nur den Einfluss der Merkmale der Modellgestaltung bewerten.

Der entscheidende Punkt sind die „Gestaltungsmerkmale“. Wie bewerten wir den Nutzen von Transfer-Learning in grundsätzlich verschiedenen Modellen? In der Tat ist die Situation nicht eindeutig. Erinnern wir uns daran, was der Autoencoder lernt. Die Architektur des Modells ist so angelegt, dass wir erwarten, dass wir am Ausgang des Modells Ausgangsdaten erhalten. Der Encoder komprimiert die Originaldaten auf einen „Flaschenhals“ des latenten Zustands, und der Decoder stellt die Daten dann wieder her. Das heißt, wir komprimieren einfach die Originaldaten. In einem solchen Fall kann man davon ausgehen, dass die Architekturen der Modelle identisch sind, wenn die Architektur des Modells nach dem ausgeliehenen Encoder-Block der Architektur des Referenzmodells entspricht.

Andererseits führt der Kodierer neben der Datenkomprimierung eine Vorverarbeitung der Daten durch. Sie hebt einige Merkmale hervor und lässt andere weg. Bei dieser Auslegung müssen wir, um die Architektur zweier Modelle anzugleichen, eine exakte Kopie des Modells erstellen, die jedoch bereits mit Zufallsgewichten initialisiert ist.

Da dies immer noch unklar ist, werden wir beide Ansätze zur Lösung des Problems testen.

Die nächste Frage betrifft das Prüfwerkzeug. Bisher haben wir für jedes Modell einen eigenen Expert Advisor (EA) zum Testen erstellt, weil wir das Modell jedes Mal im Initialisierungsblock des EA beschrieben und erstellt haben. Jetzt ist die Situation anders. Wir haben ein universelles Werkzeug zur Erstellung von Modellen geschaffen. Mit ihm können wir verschiedene Modellarchitekturen erstellen und in einer Datei speichern. Anschließend können wir das erstellte Modell in einen beliebigen EA hochladen, um es zu trainieren oder es zu verwenden.

Daher können wir jetzt einen EA erstellen, in dem wir alle Modelle trainieren. Wir werden also möglichst gleiche Bedingungen schaffen, um die Modelle zu testen.

Jetzt müssen wir uns für die Testumgebung entscheiden. Das heißt, anhand welcher Daten wir die Modelle testen werden. Die Antwort liegt auf der Hand: Zum Trainieren der Modelle werden wir eine ähnliche Umgebung verwenden wie zum Trainieren von Autoencodern. Neuronale Netze reagieren sehr empfindlich auf die Quelldaten und können nur mit den Daten korrekt arbeiten, mit denen sie trainiert wurden. Daher müssen wir, um die Technologie des Transfer-Learnings zu nutzen, die Quelldaten verwenden, die der Trainingsstichprobe des Spendermodells ähnlich sind.

Nachdem wir nun alle wichtigen Fragen geklärt haben, können wir uns an die Vorbereitung der Tests machen.


2. Erstellen des Expert Advisors für die Tests

Die vorbereitenden Arbeiten beginnen mit der Erstellung eines EA zum Testen der Modelle. Zu diesem Zweck erstellen wir eine EA-Vorlage „check_net.mq5“. Nehmen Sie zunächst Bibliotheken in die Vorlage auf:

  • NeuroNet.mqh — unsere Bibliothek zur Erstellung neuronaler Netze
  • SymbolInfo.mqh — Standardbibliothek für den Zugriff auf Handelssymboldaten
  • Oscilators.mqh — Standardbibliothek für die Arbeit mit Oszillatoren
Außerdem deklarieren wir hier eine Enumeration für die bequeme Arbeit mit Signalen.

//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+
#include "..\..\NeuroNet_DNG\NeuroNet.mqh"
#include <Trade\SymbolInfo.mqh>
#include <Indicators\Oscilators.mqh>
//---
enum ENUM_SIGNAL
  {
   Sell = -1,
   Undefine = 0,
   Buy = 1
  };

Der nächste Schritt besteht darin, die globalen Variablen des EA zu deklarieren. Geben Sie hier die Modelldatei, den Arbeitszeitraum und den Trainingszeitraum des Modells an. Außerdem werden wir alle Parameter der verwendeten Indikatoren anzeigen. Die Indikatorparameter werden in Gruppen aufgeteilt, um das EA-Menü übersichtlicher zu gestalten.

//+------------------------------------------------------------------+
//|   input parameters                                               |
//+------------------------------------------------------------------+
input int                  StudyPeriod =  2;            //Study period, years
input string               FileName = "EURUSD_i_PERIOD_H1_test_rnn";
ENUM_TIMEFRAMES            TimeFrame   =  PERIOD_CURRENT;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

Als Nächstes deklarieren wir die Instanzen der verwendeten Objekte. Die Verwendung von dynamischen Objekten wurde nach Möglichkeit vermieden. Dadurch wird der Code ein wenig vereinfacht, da unnötige Vorgänge im Zusammenhang mit der Erstellung von Objekten und der Überprüfung ihrer Relevanz entfernt werden. Die Benennung der Objekte stimmt mit dem Inhalt der Objekte überein. Dadurch wird die Verwechslung von Variablen minimiert und die Lesbarkeit des Codes verbessert.

CSymbolInfo          Symb;
CNet                 Net;
CBufferFloat        *TempData;
CiRSI                RSI;
CiCCI                CCI;
CiATR                ATR;
CiMACD               MACD;
CBufferFloat         Fractals;

Deklarieren wir auch die globalen Variablen des EA. Im Folgenden werde ich die Funktionsweise der einzelnen Programme beschreiben. Wir werden ihren Zweck bei der Analyse der Algorithmen der EA-Funktionen sehen.

uint                 HistoryBars =  40;            //Depth of history
MqlRates             Rates[];
float                dError;
float                dUndefine;
float                dForecast;
float                dPrevSignal;
datetime             dtStudied;
bool                 bEventStudy;

Sie sehen hier eine Variable für die Menge der Quelldaten in Balken, die zuvor in den externen Parametern des EA angegeben wurde. Die Ausblendung dieses Parameters und seine Verwendung als globale Variable ist eine Zwangsmaßnahme. Zuvor haben wir die Modellarchitektur in der EA-Initialisierungsfunktion beschrieben. Dieser Parameter gehörte also zu den Hyperparametern des Modells, die der Nutzer zu Beginn der EA angegeben hat. In diesem Artikel werden wir bereits erstellte Modelle verwenden. Der Parameter für die Tiefe der analysierten Geschichte muss mit dem geladenen Modell übereinstimmen. Da der Nutzer jedoch ein Modell „blind“ verwenden kann, ohne diesen Parameter zu kennen, besteht die Gefahr, dass der angegebene Parameter und das geladene Modell nicht übereinstimmen. Um dieses Risiko auszuschließen, habe ich beschlossen, den Parameter entsprechend der Größe der Quelldatenschicht des geladenen Modells neu zu berechnen.

Kommen wir nun zu den Algorithmen der EA-Funktionen. Wir beginnen mit der EA-Initialisierungsmethode — OnInit. Im Hauptteil der Methode laden wir zunächst das Modell aus der in den EA-Parametern angegebenen Datei. Zwei Momente unterscheiden sich hier von den gleichen Vorgängen in den zuvor betrachteten EAs.

Erstens: Da wir keine dynamischen Zeiger verwenden, müssen wir keine neue Instanz des Modellobjekts erstellen. Aus demselben Grund brauchen wir die Gültigkeit des Zeigers nicht zu prüfen.

Zweitens, wenn das Modell nicht aus der Datei gelesen werden konnte, wird der Nutzer informiert und die Funktion mit dem Ergebnis INIT_PARAMETERS_INCORRECT beendet. Außerdem beendet sich der EA. Wie bereits erwähnt, erstellen wir einen EA, der mit mehreren zuvor erstellten Modellen arbeitet. Es gibt also kein Standardmodell. Wenn es kein Modell gibt, gibt es auch nichts zu trainieren. Eine weitere EA-Operation macht also keinen Sinn. Wir informieren daher den Nutzer und beenden den EA.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!Net.Load(FileName + ".nnw", dError, dUndefine, dForecast, dtStudied, false))
     {
      printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError());
      return INIT_PARAMETERS_INCORRECT;
     }

Nach dem erfolgreichen Laden des Modells berechnen wir die Größe der analysierten Verlaufstiefe und speichern den resultierenden Wert in der Variable HistoryBars. Außerdem überprüfen wir die Größe der Ergebnisebene. Es sollte 3 Neuronen enthalten, entsprechend der Anzahl der möglichen Ergebnisse des Modells.

   if(!Net.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   Net.getResults(TempData);
   if(TempData.Total() != 3)
      return INIT_PARAMETERS_INCORRECT;

Wenn alle Prüfungen erfolgreich verlaufen sind, fahren wir mit der Initialisierung von Objekten für die Arbeit mit Indikatoren fort.

   if(!Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();

   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      return INIT_FAILED;

   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      return INIT_FAILED;

   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      return INIT_FAILED;

   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      return INIT_FAILED;

Vergessen wir nicht, die Ausführung von Vorgängen zu kontrollieren.

Sobald alle Objekte initialisiert sind, erzeugen wir ein nutzerdefiniertes Ereignis, mit dem wir die Kontrolle an die Modellschulungsmethode übertragen. Das Ergebnis der Erzeugung eines nutzerdefinierten Ereignisses weisen wir der Variable bEventStudy zu, die als Flag für den Start des Modelltrainings fungiert.

Der Vorgang der nutzerdefinierten Ereignisgenerierung ermöglicht die Vervollständigung der EA-Initialisierungsmethode. Parallel dazu können wir den Modellbildungsprozess analysieren, ohne auf den neuen Tick zu warten. Damit machen wir den Beginn des Modelllernprozesses unabhängig von der Marktvolatilität.

   bEventStudy = EventChartCustom(ChartID(), 1, (long)MathMax(0, MathMin(iTime(Symb.Name(), PERIOD_CURRENT,
                                  (int)(100 * Net.recentAverageSmoothingFactor * (dForecast >= 70 ? 1 : 10))), dtStudied)),

                                  0, "Init");
//---
   return(INIT_SUCCEEDED);
  }

In der Deinitialisierungsmethode des EA löschen wir das einzige dynamische Objekt, das im EA verwendet wird. Das liegt daran, dass wir die Verwendung anderer dynamischer Objekte ausgeschlossen haben.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(TempData) != POINTER_INVALID)
      delete TempData;
  }

Alle Chartereignisse werden in der Funktion OnChartEvent verarbeitet, einschließlich unseres nutzerdefinierten Ereignisses. In dieser Funktion warten wir also auf das Eintreten eines Nutzerereignisses, das durch seine ID identifiziert werden kann. Nutzerdefinierte Ereignis-IDs beginnen mit 1000. Bei der Erstellung eines nutzerdefinierten Ereignisses haben wir diesem eine ID von 1 gegeben. In dieser Funktion sollten wir also ein Ereignis mit der Kennung 1001 erhalten. Wenn ein solches Ereignis eintritt, nennen wir das Verfahren zum Trainieren des Modells — Train.

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == 1001)
      Train(lparam);
  }

Schauen wir uns die Organisation des Algorithmus — Train für das Training des Modells — genauer an, der wahrscheinlich wichtigsten Funktion unseres EAs. In den Parametern erhält diese Funktion als einzigen Wert das Datum des Beginns des Ausbildungszeitraums. Wir überprüfen zunächst, ob dieses Datum nicht außerhalb des vom Nutzer in den externen Parametern des EA angegebenen Ausbildungszeitraums liegt. Wenn das empfangene Datum nicht mit dem vom Nutzer angegebenen Zeitraum übereinstimmt, verschieben wir das Datum auf den Beginn des angegebenen Ausbildungszeitraums.

void Train(datetime StartTrainBar = 0)
  {
   int count = 0;
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);
   dtStudied = MathMax(StartTrainBar, st_time);
   ulong last_tick = 0;

Als Nächstes bereiten wir die lokalen Variablen vor.

   double prev_er = DBL_MAX;
   datetime bar_time = 0;
   bool stop = IsStopped();

Dann laden wir die historischen Daten. Hier werden die Kurse zusammen mit den Indikatordaten geladen. Es ist wichtig, die Indikatorpuffer und die geladenen Kurse synchron zu halten. Daher laden wir zunächst die Kurse für den angegebenen Zeitraum herunter, bestimmen die Anzahl der geladenen Balken und laden denselben Zeitraum für alle verwendeten Indikatoren.

   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      ExpertRemove();
      return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
      ExpertRemove();
      return;
     }
   RSI.Refresh(OBJ_ALL_PERIODS);
   CCI.Refresh(OBJ_ALL_PERIODS);
   ATR.Refresh(OBJ_ALL_PERIODS);
   MACD.Refresh(OBJ_ALL_PERIODS);

Sobald die Trainingsstichprobe geladen ist, werden nach jeder Trainingsepoche die letzten 300 Elemente aus der Gesamtzahl der Trainingsstichprobenelemente zur Validierung herangezogen. Danach sollten wir ein System von Lernprozessschleifen erstellen. Die äußere Schleife zählt die Trainingsepochen und kontrolliert, ob das Modelltraining fortgesetzt werden soll. Aktualisieren wir die Flags im Schleifenkörper:

  • prev_er — Modellfehler der vorherigen Epoche
  • stop — Erzeugung eines Ereignisses der Programmbeendigung durch den Nutzer

   MqlDateTime sTime;
   int total = (int)(bars - MathMax(HistoryBars, 0) - 300);
   do
     {
      prev_er = dError;
      stop = IsStopped();

In einer verschachtelten Schleife iterieren wir über die Elemente der Trainingsstichprobe und übertragen diese nacheinander in das neuronale Netz. Da wir rekurrente Modelle verwenden werden, die auf die Reihenfolge der Eingabedaten reagieren, müssen wir die Auswahl eines zufälligen nächsten Elements der Reihenfolge vermeiden. Stattdessen werden wir die historische Abfolge der Elemente verwenden.

Wir prüfen sofort, ob die Daten des aktuellen Elements ausreichen, um das Muster zu erstellen. Wenn die Daten nicht ausreichen, gehen wir zum nächsten Element über.

      for(int it = total; it > 1 && !stop; t--)
        {
         TempData.Clear();
         int i = it + 299;
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;

Wenn genügend Daten vorhanden sind, bilden wir ein Muster, das in das Modell einfließt. Wir kontrollieren auch die Verfügbarkeit von Daten in Indikatorpuffern. Wenn die Indikatorwerte nicht definiert sind, fahren wir mit dem nächsten Element fort.

         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!TempData.Add((float)Rates[bar_t].close - open) || !TempData.Add((float)Rates[bar_t].high - open) ||
               !TempData.Add((float)Rates[bar_t].low - open) || !TempData.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
               !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
               break;
           }
         if(TempData.Total() < (int)HistoryBars * 12)
            continue;

Nachdem ein Muster erfolgreich gebildet wurde, rufen wir die Methode feedForward des Modells auf. Wir fordern sofort das Ergebnis des Vorwärtsdurchlaufs an.

         Net.feedForward(TempData, 12, true);
         Net.getResults(TempData);

Wir wenden die Funktion SortMax auf die Modellergebnisse an, um die erhaltenen Werte in Wahrscheinlichkeiten umzuwandeln. 

         float sum = 0;
         for(int res = 0; res < 3; res++)
           {
            float temp = exp(TempData.At(res));
            sum += temp;
            TempData.Update(res, temp);
           }
         for(int res = 0; (res < 3 && sum > 0); res++)
            TempData.Update(res, TempData.At(res) / sum);
         //---
         switch(TempData.Maximum(0, 3))
           {
            case 1:
               dPrevSignal = (TempData[1] != TempData[2] ? TempData[1] : 0);
               break;
            case 2:
               dPrevSignal = -TempData[2];
               break;
            default:
               dPrevSignal = 0;
               break;
           }

Anschließend zeigen wir Informationen über den Lernprozess auf dem Chart an.

         if((GetTickCount64() - last_tick) >= 250)
           {
            string s = StringFormat("Study -> Era %d -> %.2f -> Undefine %.2f%% foracast %.2f%%\n %d of %d -> %.2f%% \n
                                     Error %.2f\n%s -> %.2f ->> Buy %.5f - Sell %.5f - Undef %.5f", count, dError, 
                                     dUndefine, dForecast, total - it - 1, total, 
                                     (double)(total - it - 1.0) / (total) * 100, Net.getRecentAverageError(),
                                      EnumToString(DoubleToSignal(dPrevSignal)), dPrevSignal, TempData[1], TempData[2], TempData[0]);
            Comment(s);
            last_tick = GetTickCount64();
           }

Nach dem Vorwärtsdurchlauf beim Modelltraining folgt die Backpropagation. Zunächst erstellen wir die Zielwerte und übergeben sie der Backpropagation-Methode ein. Außerdem werden wir sofort die Statistik des Lernprozesses berechnen.

         stop = IsStopped();
         if(!stop)
           {
            TempData.Clear();
            bool sell = (Rates[i - 1].high <= Rates[i].high && Rates[i + 1].high < Rates[i].high);
            bool buy = (Rates[i - 1].low >= Rates[i].low && Rates[i + 1].low > Rates[i].low);
            TempData.Add(!(buy || sell));
            TempData.Add(buy);
            TempData.Add(sell);
            Net.backProp(TempData);
            ENUM_SIGNAL signal = DoubleToSignal(dPrevSignal);
            if(signal != Undefine)
              {
               if((signal == Sell && sell) || (signal == Buy && buy))
                  dForecast += (100 - dForecast) / Net.recentAverageSmoothingFactor;
               else
                  dForecast -= dForecast / Net.recentAverageSmoothingFactor;
               dUndefine -= dUndefine / Net.recentAverageSmoothingFactor;
              }
            else
              {
               if(!(buy || sell))
                  dUndefine += (100 - dUndefine) / Net.recentAverageSmoothingFactor;
              }
           }
        }

Damit ist die verschachtelte Schleife über die Trainingsprobenelemente innerhalb einer Epoche des Modelltrainings abgeschlossen. Danach werden wir eine Validierung durchführen, um das Verhalten des Modells bei Daten zu bewerten, die nicht in der Trainingsstichprobe enthalten sind. Dazu führen wir eine ähnliche Schleife über die letzten 300 Elemente aus, jedoch mit einem Vorwärtsdurchlauf. Während der Validierung ist es nicht erforderlich, den Backpropagation-Durchgang durchzuführen und die Gewichtsmatrix zu aktualisieren.

      count++;
      for(int i = 0; i < 300; i++)
        {
         TempData.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;
         //---
         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!TempData.Add((float)Rates[bar_t].close - open) || !TempData.Add((float)Rates[bar_t].high - open) ||
               !TempData.Add((float)Rates[bar_t].low - open) || !TempData.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
               !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
               break;
           }
         if(TempData.Total() < (int)HistoryBars * 12)
            continue;
         Net.feedForward(TempData, 12, true);
         Net.getResults(TempData);
         //---
         float sum = 0;
         for(int res = 0; res < 3; res++)
           {
            float temp = exp(TempData.At(res));
            sum += temp;
            TempData.Update(res, temp);
           }
         for(int res = 0; (res < 3 && sum > 0); res++)
            TempData.Update(res, TempData.At(res) / sum);
         //---
         switch(TempData.Maximum(0, 3))
           {
            case 1:
               dPrevSignal = (TempData[1] != TempData[2] ? TempData[1] : 0);
               break;
            case 2:
               dPrevSignal = (TempData[1] != TempData[2] ? -TempData[2] : 0);
               break;
            default:
               dPrevSignal = 0;
               break;
           }

Nach der Validierung des Vorwärtsdurchlaufs geben wir die Signale des Modells auf dem Chart aus, um eine visuelle Bewertung seiner Leistung zu ermöglichen.

         if(DoubleToSignal(dPrevSignal) == Undefine)
            DeleteObject(Rates[i].time);
         else
            DrawObject(Rates[i].time, dPrevSignal, Rates[i].high, Rates[i].low);
        }

Am Ende jeder Epoche wird der aktuelle Zustand des Modells gespeichert. Hier fügen wir auch den aktuellen Modellfehler in die Datei ein, um die Dynamik des Lernprozesses zu steuern.

      if(!stop)
        {
         dError = Net.getRecentAverageError();
         Net.Save(FileName + ".nnw", dError, dUndefine, dForecast, Rates[0].time, false);
         printf("Era %d -> error %.2f %% forecast %.2f", count, dError, dForecast);
         int h = FileOpen(FileName + ".csv", FILE_READ | FILE_WRITE | FILE_CSV);
         if(h != INVALID_HANDLE)
           {
            FileSeek(h, 0, SEEK_END);
            FileWrite(h, eta, count, dError, dUndefine, dForecast);
            FileFlush(h);
            FileClose(h);
           }
        }
     }
   while(!(dError < 0.01 && (prev_er - dError) < 0.01) && !stop);

Als Nächstes müssen wir die Veränderung des Modellfehlers im Vergleich zur letzten Trainingsepoche bewerten und entscheiden, ob das Training fortgesetzt werden soll. Wenn wir uns entscheiden, das Training fortzusetzen, werden die Iterationen der Schleife für die neue Lernepoche wiederholt.

Nach Abschluss des Modelltrainings löschen wir den Kommentar im Chart und initialisieren den EA-Abschluss. Inzwischen hat der EA die Aufgabe des Modelltrainings abgeschlossen und es besteht keine Notwendigkeit mehr, das Modell weiter im Speicher zu halten.

   Comment("");
   ExpertRemove();
  }

Die Hilfsfunktionen für die Anzeige und Löschen von Kennzeichnungen auf dem Chart sind genau die, die wir in den zuvor betrachteten EAs verwendet haben, daher werde ich ihre Algorithmen hier nicht wiederholen. Den vollständigen Code aller EA-Funktionen finden Sie im Anhang.


3. Erstellen von Modellen für Tests

Nachdem wir nun das Modellprüfungswerkzeug erstellt haben, müssen wir die Basis für die Prüfung vorbereiten. Das heißt, wir müssen die Modelle erstellen, die trainiert werden sollen. Hier ist keine Programmierung erforderlich, da wir die erforderliche Kodierung bereits in den beiden vorangegangenen Artikeln vorgenommen haben. Jetzt werden wir die Ergebnisse nutzen und mit unserem Werkzeug Modelle erstellen.

Wir führen also den zuvor erstellten NetCreator EA aus. Wir öffnen darin das vortrainierte Autoencoder-Modell mit dem rekurrenten Encoder auf der Basis von LSTM-Blöcken. Zuvor haben wir sie in der Datei „EURUSD_i_PERIOD_H1_rnn_vae.nnw“ gespeichert. Wir werden nur den Encoder dieses Modells verwenden. Wir suchen im linken Block des vortrainierten Modells die latente Zustandsschicht des Variations-Autoencoders (VAE). In meinem Fall ist es der achte. Ich werde also nur die ersten sieben neuronalen Schichten des Spendermodells kopieren.

Das Werkzeug bietet drei Möglichkeiten, die gewünschte Anzahl von Ebenen für das Kopieren auszuwählen. Sie können die Schaltflächen im Bereich „Transfer Layers“ oder die Pfeiltasten ↑ und ↓ verwenden. Alternativ können Sie auch einfach auf die Beschreibung des zuletzt kopierten Modells in der Beschreibung des Spenders klicken.

Gleichzeitig mit der Änderung der Anzahl der kopierten Ebenen ändert sich auch die Beschreibung des erstellten Modells im rechten Block des Werkzeugs. Ich denke, das ist praktisch und informativ. Sie können sofort sehen, wie sich Ihre Aktionen auf die Architektur des zu erstellenden Modells auswirken.

Als Nächstes müssen wir das neue Modell mit mehreren neuronalen Entscheidungsschichten für eine bestimmte Lernaufgabe ergänzen. Ich habe versucht, diesen Teil nicht zu verkomplizieren, da der Hauptzweck dieser Tests darin besteht, die Wirksamkeit der Ansätze zu bewerten. Ich habe zwei vollständig verknüpfte Schichten mit 500 Elementen und einem hyperbolischen Tangens als Aktivierungsfunktion hinzugefügt.

Das Hinzufügen neuer neuronaler Schichten erwies sich als eine recht einfache Aufgabe. Wir wählen zunächst den Typ der neuronalen Schicht aus. Eine vollständig verknüpfte neuronale Schicht entspricht „Dense“. Wir geben die Anzahl der Neuronen in der Schicht an, die Aktivierungsfunktion und die Methode zur Aktualisierung der Parameter. Wenn ein anderer Typ von neuronaler Schicht ausgewählt wird, müssen die entsprechenden Felder ausgefüllt werden. Nachdem Sie alle erforderlichen Daten eingegeben haben, klicken Sie auf Weiter.

Ein weiterer Vorteil ist, dass Sie die Daten nicht erneut eingeben müssen, wenn Sie mehrere identische neuronale Schichten hinzufügen möchten. Klicken Sie einfach noch einmal auf ADD LAYER. So habe ich das gemacht. Um die zweite Ebene hinzuzufügen, habe ich keine Daten eingegeben, sondern einfach auf die Schaltfläche zum Hinzufügen einer neuen Ebene geklickt.

Die Ergebnisschicht ist ebenfalls vollständig vernetzt und enthält drei Elemente, die den Anforderungen des oben erstellten EA entsprechen. Sigmoid wird als Aktivierungsfunktion für die Ergebnisschicht verwendet.

Unsere früheren neuronalen Schichten waren ebenfalls vollständig verbunden. Wir können also nur die Anzahl der Neuronen und die Aktivierungsfunktion ändern. Dann fügen wir die Ebene zu unserem Modell hinzu.

Speichern Sie nun das neue Modell in einer Datei. Drücken Sie dazu die Schaltfläche MODELL SPEICHERN und geben Sie den Dateinamen des neuen Modells EURUSD_i_PERIOD_H1_test_rnn.nnw an. Beachten Sie, dass Sie den Dateinamen ohne die Erweiterung angeben können. Die richtige Erweiterung wird automatisch hinzugefügt.

Der gesamte Prozess der Modellerstellung wird in der folgenden Grafik veranschaulicht.

Verwendung des Werkzeugs zur Modellerstellung

Das erste Modell ist fertig. Fahren wir nun mit der Erstellung des zweiten Modells fort. Als Spender für das zweite Modell laden wir den Variierten Autoencoder mit einem vollständig verbundenen Encoder aus der Datei EURUSD_i_PERIOD_H1_vae.nnw. Hier kommt eine weitere Überraschung. Nach dem Laden des neuen Spendermodells haben wir die hinzugefügten neuronalen Schichten nicht entfernt. Sie wurden also automatisch dem geladenen Modell hinzugefügt. Wir müssen nur die Anzahl der neuronalen Schichten auswählen, die vom Spendermodell in das neue Modell kopiert werden sollen. Unser neues Modell ist also fertig.

Auf der Grundlage des letzten Autoencoder-Modells habe ich nicht nur ein, sondern zwei Modelle erstellt. Das erste Modell ist analog zum ersten Modell. Ich habe den Encoder des Spendermodells verwendet und die zuvor erstellten drei Ebenen hinzugefügt. Für das zweite Modell habe ich nur die Quelldatenschicht und die Batch-Normalisierungsschicht aus dem Spendermodell übernommen. Dann fügte ich die gleichen drei vollständig verknüpften neuronalen Schichten zu ihnen hinzu. Das letzte Modell wird als Leitfaden für die Ausbildung des neuen Modells dienen. Ich beschloss, dass die vortrainierte Normalisierung eines Batch-Jobs für die Aufbereitung der rohen Eingabedaten verwendet werden sollte. Dies dürfte die Konvergenz des neuen Modells erhöhen. Außerdem verzichten wir auf die Datenkompression. Wir können davon ausgehen, dass das letzte Modell vollständig mit Zufallsgewichten gefüllt ist.

Wie bereits erwähnt, gibt es verschiedene Möglichkeiten, die Auswirkungen der Architektur eines vortrainierten Modells zu bewerten. Deshalb habe ich ein weiteres Modell zum Testen erstellt. Ich habe die Architekturen des neu erstellten Modells unter Verwendung des Autoencoders mit LSTM-Blöcken verwendet und sie vollständig in das neue Modell übernommen. Aber dieses Mal habe ich den Encoder nicht von dem Spendermodell kopiert. So erhielt ich eine völlig identische Modellarchitektur, die jedoch mit zufälligen Gewichten initialisiert wurde.


4. Testergebnisse.

Nachdem wir nun alle Modelle erstellt haben, die wir für unsere Tests benötigen, können wir sie trainieren.

Wir haben die Modelle mit Hilfe des überwachten Lernens trainiert und dabei die zuvor verwendeten Trainingsparameter beibehalten. Die Modelle wurden in einem Zeitintervall der letzten zwei Jahre trainiert, wobei EURUSD auf dem H1-Zeitrahmen verwendet wurde. Die Indikatoren wurden mit Standardparametern verwendet.

Im Interesse der Reinheit des Experiments wurden alle Modelle gleichzeitig an einem Terminal auf verschiedenen Karten trainiert.

Ich muss sagen, dass die gleichzeitige Ausbildung von mehreren Modellen nicht wünschenswert ist. Dadurch verringert sich die Lernrate jedes einzelnen von ihnen erheblich. OpenCL wird in den Modellen verwendet, um den Berechnungsprozess zu parallelisieren und die verfügbaren Ressourcen zu nutzen. Beim parallelen Training mehrerer Modelle werden die verfügbaren Ressourcen auf alle Modelle aufgeteilt. Jeder von ihnen hat also Zugang zu begrenzten Ressourcen. Dies verlängert die Lernzeit. Dies geschah jedoch absichtlich, um beim Training der Modelle ähnliche Bedingungen zu gewährleisten.

Test 1:

Für den ersten Test wurden zwei Modelle mit vortrainierten Kodierern und ein kleines voll vernetztes Modell mit einer ausgeliehenen Batch-Normalisierungsschicht und zwei voll verknüpften versteckten Schichten verwendet.

Die Ergebnisse der Modellversuche sind in der nachstehenden Grafik dargestellt.

Vergleich der Lerndynamik von Modellen

Wie in der Grafik zu sehen ist, zeigte das Modell mit einem vortrainierten rekurrenten Encoder die beste Leistung. Sein Fehler nahm praktisch ab den ersten Trainingsepochen deutlich schneller ab.

Das Modell mit einem vollständig vernetzten Encoder zeigte ebenfalls eine Fehlerreduzierung während des Lernprozesses, allerdings in einem langsameren Tempo.

Ein vollständig verknüpftes Modell mit zwei versteckten Schichten, das mit Zufallswerten initialisiert wird, sieht aus, als sei es überhaupt nicht trainiert worden. Nach dem vorliegenden Schaubild scheint der Fehler an Ort und Stelle zu verharren.

Fehlerdynamik bei vollständig verbundenen Modellen

Bei näherer Betrachtung lässt sich eine Tendenz zur Fehlerreduzierung feststellen. Allerdings erfolgt dieser Rückgang wesentlich langsamer. Offensichtlich ist ein solches Modell zu einfach, um solche Probleme zu lösen.

Daraus lässt sich schließen, dass die Leistung des Modells nach wie vor stark von der Verarbeitung der Ausgangsdaten durch einen vortrainierten Encoder beeinflusst wird. Die Architektur eines solchen Encoders hat einen erheblichen Einfluss auf die Funktionsweise des gesamten Modells.

Gesondert erwähnen möchte ich die Trainingsrate des Modells. Das einfachste Modell wies natürlich die kleinste Zeitdauer für das Durchlaufen einer Epoche auf. Die Lernrate eines Modells mit dem rekurrenten Encoder lag jedoch sehr nahe bei dieser Rate. Meiner Meinung nach wurde dies durch eine Reihe von Faktoren beeinflusst.

Zunächst einmal ermöglichte die Architektur des rekurrenten Modells die Verkleinerung des analysierten Datenfensters um das Vierfache. Daher wurde auch die Zahl der interneuronalen Verbindungen reduziert. Infolgedessen wurden die Kosten für ihre Verarbeitung gesenkt. Gleichzeitig bedeutet die rekurrente Architektur zusätzliche Ressourcenkosten für den Backpropagation-Durchgang, aber wir haben den Backpropagation-Durchgang für vortrainierte neuronale Schichten deaktiviert. Dadurch konnten die Kosten für das erneute Training der Modelle gesenkt werden.

Das Modell mit einem vollständig verknüpften Encoder zeigte langsamere Lernraten.

Test 2:

Im zweiten Test beschlossen wir, die architektonischen Unterschiede zwischen den Modellen zu minimieren und zwei rekurrente Modelle mit der gleichen Architektur zu trainieren. Ein Modell verwendet einen vortrainierten rekurrenten Encoder. Das zweite Modell wird vollständig mit Zufallsgewichten initialisiert. Die Modelle wurden mit denselben Parametern trainiert, die wir im ersten Test verwendet haben.

Die Testergebnisse sind in der nachstehenden Tabelle aufgeführt. Wie Sie sehen können, begann das vorab trainierte Modell mit einem kleineren Fehler. Doch schon bald holte das zweite Modell auf, und im weiteren Verlauf lagen ihre Werte recht nahe beieinander. Dies bestätigt die frühere Schlussfolgerung, dass die Encoderarchitektur einen erheblichen Einfluss auf die Leistung des gesamten Modells hat.

Vergleich der Lerndynamik von rekurrenten Modellen

Achten Sie auf die Lernraten. Das vortrainierte Modell benötigte sechsmal weniger Zeit, um eine Epoche zu durchlaufen. Dies ist natürlich die reine Zeit, ohne Berücksichtigung des Autoencoder-Trainings.


Schlussfolgerung

Auf der Grundlage der obigen Arbeit können wir feststellen, dass die Verwendung der Technologie des Transfer-Learnings eine Reihe von Vorteilen bietet. Zunächst einmal funktioniert diese Technologie wirklich. Seine Anwendung ermöglicht die Wiederverwendung von zuvor trainierten Modellblöcken zur Lösung neuer Probleme. Die einzige Bedingung ist die Einheitlichkeit der Ausgangsdaten. Die Verwendung von vortrainierten Blöcken auf nicht korrekten Eingabedaten wird nicht funktionieren.

Durch den Einsatz von Technologie wird die Zeit für die Ausbildung neuer Modelle verkürzt. Bitte beachten Sie jedoch, dass wir die reine Testzeit gemessen haben, ohne das Vortraining des Autoencoders. Wenn wir die für das Training des Autoencoders aufgewendete Zeit addieren, wird die Zeit wahrscheinlich gleich sein. Oder, aufgrund einer komplexeren Architektur des Decoders, kann das Training des „reinen“ Modells sogar schneller sein. Daher kann der Einsatz von Transfer-Learning gerechtfertigt sein, wenn ein Block für die Lösung verschiedener Probleme verwendet werden soll. Es kann auch verwendet werden, wenn die Ausbildung des Modells als Ganzes aus irgendeinem Grund nicht möglich ist. So kann das Modell beispielsweise sehr komplex sein, und der Fehlergradient nimmt während des Lernprozesses ab und erreicht nicht alle Schichten.

Die Technik kann auch bei der Suche nach dem am besten geeigneten Modell angewandt werden, wenn wir das Modell auf der Suche nach dem optimalen Fehlerwert schrittweise verkomplizieren.


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
  5. Neuronale Netze leicht gemacht (Teil 24): Verbesserung des Instruments für Transfer-Learning

Programme, die im diesem Artikel verwendet werden

# Name Typ Beschreibung
1 check_net.mq5  EA EA für das zusätzliche Training von Modellen 
2 NetCreator.mq5 EA Tool für die Modellbildung
3 NetCreatotPanel.mqh Klassenbibliothek Klassenbibliothek zur Erstellung des Tools
4 NeuroNet.mqh Klassenbibliothek Eine Bibliothek von Klassen zur Erstellung eines neuronalen Netzes
5 NeuroNet.cl Code Base Die Bibliothek des Programmcodes von OpenCL


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

Beigefügte Dateien |
MQL5.zip (78.84 KB)
Neuronale Netze leicht gemacht (Teil 26): Reinforcement-Learning Neuronale Netze leicht gemacht (Teil 26): Reinforcement-Learning
Wir untersuchen weiterhin Methoden des Reinforcement-Learnings. Mit diesem Artikel beginnen wir ein weiteres großes Thema, das Reinforcement-Learning. Dieser Ansatz ermöglicht es den Modellen, bestimmte Strategien zur Lösung der Probleme zu entwickeln. Es ist zu erwarten, dass diese Eigenschaft des Reinforcement-Learnings (Lernen durch Verstärkung) neue Horizonte für die Entwicklung von Handelsstrategien eröffnen wird.
Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 28): Der Zukunft entgegen (III) Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 28): Der Zukunft entgegen (III)
Es gibt noch eine Aufgabe, der unser Auftragssystem nicht gewachsen ist, aber wir werden das ENDLICH verstehen. Der MetaTrader 5 bietet ein Ticketsystem, das die Erstellung und Korrektur von Auftragswerten ermöglicht. Die Idee ist, einen Expert Advisor zu haben, der das gleiche Ticketsystem schneller und effizienter machen würde.
DoEasy. Steuerung (Teil 17): Beschneiden unsichtbarer Objektteile, Hilfspfeiltasten WinForms-Objekte DoEasy. Steuerung (Teil 17): Beschneiden unsichtbarer Objektteile, Hilfspfeiltasten WinForms-Objekte
In diesem Artikel werde ich die Funktionalität zum Ausblenden von Objektabschnitten, die sich außerhalb ihrer Container befinden, erstellen. Außerdem werde ich zusätzliche Pfeiltastenobjekte erstellen, die als Teil anderer WinForms-Objekte verwendet werden können.
Neuronale Netze leicht gemacht (Teil 24): Verbesserung des Instruments für Transfer Learning Neuronale Netze leicht gemacht (Teil 24): Verbesserung des Instruments für Transfer Learning
Im vorigen Artikel haben wir ein Tool zum Erstellen und Bearbeiten der Architektur neuronaler Netze entwickelt. Heute werden wir die Arbeit an diesem Instrument fortsetzen. Wir werden versuchen, sie nutzerfreundlicher zu gestalten. Dies mag ein Schritt weg von unserem Thema sein. Aber ist es nicht so, dass ein gut organisierter Arbeitsplatz eine wichtige Rolle bei der Erreichung dieses Ziels spielt?