English Русский 中文 Español 日本語 Português
preview
ONNX meistern: Der Game-Changer für MQL5-Händler

ONNX meistern: Der Game-Changer für MQL5-Händler

MetaTrader 5Handelssysteme | 31 Januar 2024, 09:29
369 0
Omega J Msigwa
Omega J Msigwa

„Die Möglichkeit, KI-Modelle im ONNX-Format zu exportieren und zu importieren, rationalisiert den Entwicklungsprozess und spart Zeit und Ressourcen bei der Integration von KI in verschiedene Sprachökosysteme.“


Einführung

Es ist unbestreitbar, dass wir uns im Zeitalter der KI und des maschinellen Lernens befinden. Jeden Tag gibt es eine neue KI-basierte Technologie, die in den Bereichen Finanzen, Kunst und Spiele, Bildung und vielen anderen Lebensbereichen eingesetzt wird.

Wenn wir Händler lernen, die Macht der künstlichen Intelligenz zu nutzen, könnte uns das einen Vorteil gegenüber dem Markt verschaffen, da wir Muster und Beziehungen erkennen können, die wir mit dem menschlichen Auge nicht sehen können.

Auch wenn KI cool und magisch erscheint, stecken hinter den Modellen komplexe mathematische Operationen, die einen enormen Arbeitsaufwand und ein hohes Maß an Genauigkeit und Konzentration erfordern, wenn man diese maschinellen Lernmodelle von Grund auf neu implementieren will, was dank open-source.

Heutzutage muss man nicht einmal mehr ein Mathe- und Programmiergenie sein, um KI-Modelle zu erstellen und zu implementieren. Man braucht nur ein Grundverständnis für eine bestimmte Programmiersprache oder die Tools, die man für sein Projekt verwenden möchte, und einen PC. In einigen Fällen muss man nicht einmal einen PC besitzen, denn dank Diensten wie Google Colab kann man mit Python kostenlos KI-Modelle programmieren, erstellen und ausführen.

So einfach es ist, Modelle des maschinellen Lernens mit Python und anderen beliebten und ausgereiften Programmiersprachen zu implementieren, so einfach ist es nicht, um ehrlich zu sein, dies in MQL5 zu tun. Wenn Sie das Rad nicht neu erfinden wollen, indem Sie Machine-Learning-Modelle in MQL5 von Grund auf neu erstellen, was wir in dieser Artikelserie tun, würde ich dringend empfehlen, ONNX zu verwenden, um in Python erstellte AI-Modelle in MQL5 zu integrieren. ONNX wird jetzt in MQL5 unterstützt. Ich bin so begeistert, ich glaube, das sollten Sie auch sein.

  onnx-mql5

Ein grundlegendes Verständnis von künstlicher Intelligenz und maschinellem Lernen ist erforderlich, um den Inhalt dieses Artikels zu verstehen, bitte lesen Sie diesen Eintrag und diesen Artikel 


Was ist ONNX?

ONNX steht für Open Neural Network Exchange und ist ein Open-Source-Format zur Darstellung von Modellen für maschinelles Lernen und Deep Learning. Damit können Sie Modelle, die in einem Deep-Learning-Framework trainiert wurden, in ein gemeinsames Format konvertieren, das in anderen Frameworks verwendet werden kann, was die Arbeit mit Modellen auf verschiedenen Plattformen und mit verschiedenen Tools erleichtert.

Das bedeutet, dass Sie Modelle für maschinelles Lernen mit jeder Sprache, die dies unterstützt, außer MQL5, erstellen und dann das Modell in das ONNX-Format konvertieren können. Dieses ONNX-Modell kann dann in Ihrem MQL5-Programm verwendet werden.

In diesem Beitrag werde ich Python zum Aufbau von Machine Learning verwenden, da ich damit vertraut bin. Mir wurde gesagt, dass man auch andere Sprachen verwenden kann, ich bin mir aber nicht sicher. Übrigens, die gesamte Dokumentation von ONNX scheint auf Python zu basieren. Ich glaube, dass ONNX im Moment für Python gemacht ist.


Grundlegende Konzepte im ONNX:

Bevor Sie sich mit ONNX beschäftigen, sollten Sie mit einigen Schlüsselbegriffen vertraut sein:

  • ONNX Modell: Ein ONNX-Modell ist die Umsetzung eines maschinellen Lernmodells. Es besteht aus einem Berechnungsgraphen, in dem Knoten Operationen (z. B. Faltung, Addition) und Kanten den Datenfluss zwischen den Operationen darstellen.
  • Nodes: Node (Knoten) in einem ONNX-Diagramm stellen Operationen oder Funktionen dar, die auf Eingabedaten angewendet werden. Diese Knoten können Operationen wie Faltung, Addition oder nutzerdefinierte Operationen sein.
  • Tensoren: Tensoren sind mehrdimensionale Arrays, die den Datenfluss zwischen den Knoten im Berechnungsgraphen darstellen. Sie können Eingaben, Ausgaben oder Zwischendaten sein.
  • Operatoren: Operatoren sind Funktionen, die in ONNX auf Tensoren angewendet werden. Jeder Operator steht für eine bestimmte Operation, z. B. Matrixmultiplikation oder elementweise Addition.

Erstellung von Modellen in Python und Bereitstellung in MQL5 mit ONNX

Um erfolgreich ein maschinelles Lernmodell in Python zu erstellen, setzen Sie dieses Modell in Ihrem EA, Indikator oder Skript in MQL5 ein; Es erfordert mehr als nur Python-Code für das Modell. Im Folgenden finden Sie die entscheidenden Schritte, die Sie befolgen müssen, damit Sie am Ende nicht nur ein ONNX-Modell haben, sondern ein Modell, das die gewünschten genauen Vorhersagen liefert;

  1. Datenerhebung
  2. Normalisierung der Daten auf der MQL5-Seite 
  3. Erstellung der Modelle in Python
  4. Das erstellte ONNX-Modell in MQL5 erhalten
  5. Ausführung des Modells in Echtzeit


01: Datenerhebung

Ich glaube, dass es am besten ist, alle Daten innerhalb Ihres MQL5-Programms zu sammeln, um mit der Art und Weise übereinzustimmen, wie wir die Trainingsdaten und die Daten sammeln, die während des Live-Handels oder der Ausführung des Modells in Echtzeit verwendet werden Beachten Sie, dass die Datenerfassung je nach Art des Problems, das Sie zu lösen versuchen, variieren kann. Wir werden die Marktinformationen OHLC(Open, High, Low, Close) als unseren primären Datensatz verwenden, wobei Open, High und Low als unabhängige Variablen und der Close-Preis als Zielvariable verwendet werden.

Innerhalb von ONNX Daten abrufen.mq5

matrixf GetTrainData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   ulong last_col;
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_LOW, start, total);
    CLOSE.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, start, total);
    
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
    
    
    csv_name_ = Symbol()+"."+EnumToString(Period())+"."+string(total_bars);
    
       
      x_vars = "OPEN,HIGH,LOW";
      
       
       return_matrix.Resize(total, 4); //if we are collecting the train data collect the target variable also
       
       last_col = return_matrix.Cols()-1; //Column located at the last index is the last column
       
       return_matrix.Col(CLOSE, last_col); //put the close price information in the last column of a matrix
       
       
       csv_name_ +=".targ=CLOSE";
       
       csv_header = x_vars + ",CLOSE";
         
       if (!WriteCsv("ONNX Datafolder\\"+csv_name_+".csv", return_matrix, csv_header))
         Print("Failed to Write to a csv file");
       else
         Print("Data saved to a csv file successfully");
     
    
   return return_matrix;
 } 

Beim überwachten maschinellen Lernen muss die Zielvariable spezifiziert und dem Modell übergeben werden, damit es daraus lernen und die Muster zwischen der Zielvariable und den übrigen Variablen verstehen kann. In unserem Fall versucht das Modell zu verstehen, wie diese Indikatorwerte zu einer Hausse oder Baisse führen. 

Bei der Bereitstellung des Modells müssen wir die Daten auf dieselbe Weise erfassen, nur dass wir diesmal ohne die Zielvariable erfassen, denn das ist etwas, das unser trainiertes Modell herausfinden soll. Sozusagen zur Vorhersage.

Aus diesem Grund gibt es eine andere Funktion namens GetLiveData, mit der neue Daten für kontinuierliche Vorhersagen auf dem Markt geladen werden.

Innerhalb von ONNX mt5.mq5

matrixf GetLiveData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_LOW, start, total);
        
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
          
      
   return return_matrix;
 }

Erfassen der Trainingsdaten

    matrixf dataset = GetTrainData(start_bar, total_bars);
    
    Print("Train data\n",dataset);

Ausgaben:

DK      0       23:10:54.837    ONNX get data (EURUSD,H1)       Train data
PR      0       23:10:54.837    ONNX get data (EURUSD,H1)       [[1.4243405,1.4130603,1.4215617,1.11194]
HF      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3976599,1.3894916,1.4053394,1.11189]
RK      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.402994,1.3919021,1.397626,1.11123]
PM      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3848507,1.3761013,1.3718294,1.11022]
FF      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3597701,1.3447646,1.3545419,1.1097701]
CH      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3461626,1.3522644,1.3433729,1.1106]
NL      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3683074,1.3525325,1.3582669,1.10996]

Abrufen der Live-Daten

Abrufen der aktuellen Bar Informationen OHL.

matrixf live_data = GetLiveData(0,1);
   
Print("Live data\n",live_data);

Ausgaben:

MN      0       23:15:47.167    ONNX mt5 (EURUSD,H1)    Live data
KS      0       23:15:47.167    ONNX mt5 (EURUSD,H1)    [[-0.21183228,-0.23540309,-0.20334835]]

Die Art und Weise, wie Live-Daten geladen werden, kann bei der Aufbereitung der Daten für die Modelle der Zeitreihen wie RNN, GRU und LSTM leicht variieren.

Beachten Sie, dass ich matrixf verwendet habe, was für matrix float steht, anstatt nur matrix!!, um die Datentypkompatibilität zwischen MQL5 und Python zu gewährleisten: Stellen Sie sicher, dass der Datentyp der Eingabedaten mit dem Datentyp kompatibel ist, den das ONNX-Modell erwartet. Wenn Ihr ONNX-Modell eine float32-Eingabe erwartet, stellen Sie sicher, dass Ihre Eingabedaten ebenfalls vom Typ float32 sind. ONNX ist kompatibel mit float32 und float64, ein falscher Ansatz kann zu Fehlern führen, die wie folgt aussehen;
error 2023.09.18 18:03:53.212   ONNX: invalid parameter size, expected 1044480 bytes instead of 32640


02: Daten-Normalisierung auf der MQL5-Seite

Die Normalisierung von Daten gehört zu den wichtigsten Dingen, die für einen Datensatz, der von einem maschinellen Lernmodell verwendet werden soll, richtig gemacht werden müssen.

Beachten Sie, dass die Normalisierungstechnik, die zur Vorbereitung der Trainingsdaten verwendet wird, dieselbe sein muss, die auch zur Vorbereitung der Test- und Live-Daten verwendet wird. Dies bedeutet, dass, wenn die verwendete Technik MinMaxScaler, die Minimal- und Maximalwerte, die die grundlegenden Variablen in der MinMaxScaler-Gleichung sind, die bei der Vorbereitung der Zugdaten verwendet wurden und die dazu verwendet werden, die neuen Daten, die vom Modell an anderer Stelle verarbeitet werden sollen, weiter zu normalisieren. Um diese Konsistenz zu erreichen, müssen wir die Variablen für jedes Normalisierungsverfahren in einer csv-Datei speichern:

Die Datennormalisierung gilt nur für unabhängige Variablen, es spielt keine Rolle, welche Art von Problem Sie zu lösen versuchen, Sie müssen die Zielvariable nicht normalisieren.

Wir werden die Klasse Preprocessing von hier verwenden.

Innerhalb des ONNX-Skripts get data.mq5

 //--- Saving the normalization prameters
 
 switch(NORM)
   {
    case  NORM_MEAN_NORM:
      
       //--- saving the mean
       
       norm_params.Assign(norm_x.mean_norm_scaler.mean);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the min
       
       norm_params.Assign(norm_x.mean_norm_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.mean_norm_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.max.csv",norm_params,x_vars);
           
      break;
      
    case NORM_MIN_MAX_SCALER:
       
       //--- saving the min
       
       norm_params.Assign(norm_x.min_max_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.min_max_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.max.csv",norm_params,x_vars);
       
       
       break;
       
    case NORM_STANDARDIZATION:

       //--- saving the mean
       
       norm_params.Assign(norm_x.standardization_scaler.mean);             
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the std
       
       norm_params.Assign(norm_x.standardization_scaler.std);
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.std.csv",norm_params,x_vars);
       
       break;
   } 

Ausgaben:

Normalisierungsparameter

Wenn der „Standardization Scaler“ in den csv-Dateien verwendet wurde, sahen die Parameter wie folgt aus;


Beachten Sie, dass die Normalisierung auch in die Funktion GetData integriert ist. Da die Normalisierung so wichtig ist, muss jede Datenmatrix, die von den beiden für die Datenerfassung zuständigen Funktionen zurückgegeben wird, eine Matrix mit normalisierten Preiswerten sein.

Innerhalb des ONNX-Skripts get data.mq5

matrixf GetTrainData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   ulong last_col;
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_LOW, start, total);
    CLOSE.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, start, total);
    
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
    
    matrixf norm_params = {};
    
    csv_name_ = Symbol()+"."+EnumToString(Period())+"."+string(total_bars);
    
       
      x_vars = "OPEN,HIGH,LOW";
      
       while (CheckPointer(norm_x) != POINTER_INVALID)
         delete (norm_x);
         
       norm_x = new CPreprocessing<vectorf, matrixf>(return_matrix, NORM);
    
       
 
 //--- Saving the normalization prameters
 
 switch(NORM)
   {
    case  NORM_MEAN_NORM:
      
       //--- saving the mean
       
       norm_params.Assign(norm_x.mean_norm_scaler.mean);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the min
       
       norm_params.Assign(norm_x.mean_norm_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.mean_norm_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.max.csv",norm_params,x_vars);
           
      break;
      
    case NORM_MIN_MAX_SCALER:
       
       //--- saving the min
       
       norm_params.Assign(norm_x.min_max_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.min_max_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.max.csv",norm_params,x_vars);
       
       
       break;
       
    case NORM_STANDARDIZATION:

       //--- saving the mean
       
       norm_params.Assign(norm_x.standardization_scaler.mean);             
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the std
       
       norm_params.Assign(norm_x.standardization_scaler.std);
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.std.csv",norm_params,x_vars);
       
       break;
   }
 
       return_matrix.Resize(total, 4); //if we are collecting the train data collect the target variable also
       
       last_col = return_matrix.Cols()-1; //Column located at the last index is the last column
       
       return_matrix.Col(CLOSE, last_col); //put the close price information in the last column of a matrix
       
       
       csv_name_ +=".targ=CLOSE";
       
       csv_header = x_vars + ",CLOSE";
         
       if (!WriteCsv("ONNX Datafolder\\"+csv_name_+".csv", return_matrix, csv_header))
         Print("Failed to Write to a csv file");
       else
         Print("Data saved to a csv file successfully");
     
    
   return return_matrix;
 } 

 Schließlich wurden die Daten in einer CSV-Datei gespeichert, sodass sie mit Python-Code gemeinsam genutzt werden konnten.


03: Erstellung der Modelle in Python

Ich werde ein mehrschichtiges Perceptron-Netz aufbauen, aber Sie können auch ein beliebiges Modell Ihrer Wahl erstellen. Das ist nicht auf diese spezielle Art von Modell beschränkt. Beginnen Sie mit der Installation von Python in Ihrem System, falls Sie dies noch nicht getan haben. Danach installieren Sie virtualenv, indem Sie die folgenden Befehle über Windows CMD (nicht zu verwechseln mit Powershell) ausführen!

$ pip3 install virtualenv

Nach diesem Lauf,

$ virtualenv venv

Dadurch wird eine virtuelle Python-Umgebung für Ihren Windows-Rechner erstellt. Ich glaube, die meisten von uns arbeiten mit Windows, für Mac-Nutzer und Linux-Anwender kann der Prozess etwas anders aussehen.

$ venv\Scripts\activate

Danach installieren Sie alle in diesem Tutorial verwendeten Abhängigkeiten durch:

$ pip install -r requirements.txt #Diese txt-Datei befindet sich unter https://github.com/MegaJoctan/ONNX-MQL5/blob/main/requirements.txt

Es ist immer wichtig, das Projekt zu isolieren, indem man eine virtuelle Umgebung erstellt, um Konflikte zwischen den Modulen und Python-Versionen zu vermeiden und die gemeinsame Nutzung des Projekts zu erleichtern.

Importieren und Initialisieren von MT5

import MetaTrader5 as mt5

if not mt5.initialize(): #This will open MT5 app in your pc

   print("initialize() failed, error code =",mt5.last_error())

   quit()

# program logic and ML code will be here



mt5.shutdown() #This closes the program

# Getting the data we stored in the Files path on Metaeditor

data_path = terminal_info.data_path

dataset_path = data_path + "\\MQL5\\Files\\ONNX Datafolder"

Wir müssen prüfen, ob der Pfad existiert. Wenn er nicht existiert, bedeutet das, dass wir die Daten auf der MT5-Seite nicht gesammelt haben.

import os if not os.path.exists(dataset_path):

print("Dataset folder doesn't exist | Be sure you are referring to the correct path and the data is collected from MT5 side of things")
quit()

Aufbau eines mehrschichtigen neuronalen Perzeptron-Netzwerks (MLP)

Wir werden einen MLP NN in eine Klasse verpacken, um unseren Code in lesbare Abschnitte zu unterteilen.

01: Initialisierung der Klasse

Die Daten werden gesammelt und in Trainings- und Teststichproben aufgeteilt, wobei wichtige Variablen als für die gesamte Klasse verfügbar erklärt werden,

class NeuralNetworkClass():
    def __init__(self, csv_name, target_column, batch_size=32):

    # Loading the dataset and storing to a variable Array        
        self.data = pd.read_csv(dataset_path+"\\"+csv_name)

        if self.data.empty:
            print(f"No such dataset or Empty dataset csv = {csv_name}")
            quit() # quit the program
        

        print(self.data.head()) # Print 5 first rows of a given data

        self.target_column = target_column
        # spliting the data into training and testing samples

        X = self.data.drop(columns=self.target_column).to_numpy() # droping the targeted column, the rest is x variables
        Y = self.data[self.target_column].to_numpy() # We convert data arrays to numpy arrays compartible with sklearn and tensorflow
                
        
        self.train_x, self.test_x, self.train_y, self.test_y = train_test_split(X, Y, test_size=0.3, random_state=42) # splitting the data into training and testing samples 
        
        print(f"train x shape {self.train_x.shape}\ntest x shape {self.test_x.shape}")
                
        self.input_size = self.train_x.shape[-1] # obtaining the number of columns in x variable as our inputs
        
        self.output_size = 1 # We are solving for a regression problem we need to have a single output neuron
        
        self.batch_size = batch_size
        
        self.model = None # Object to store the model
        
        self.plots_directory = "Plots"
        self.models_directory = "Models"
                

Ausgaben:

pd-Kopf

02: Aufbau des neuronalen Netzmodells

Unser einschichtiges neuronales Netz wird mit einer bestimmten Anzahl von Neuronen definiert.

    def BuildNeuralNetwork(self, activation_function='relu', neurons = 10):

        # Create a Feedforward Neural Network model
        self.model = keras.Sequential([
            keras.layers.Input(shape=(self.input_size,)),  # Input layer
            keras.layers.Dense(units=neurons, activation=activation_function, activity_regularizer=l2(0.01), kernel_initializer="he_uniform"),  # Hidden layer with an activation function
            keras.layers.Dense(units=self.output_size, activation='linear', activity_regularizer=l2(0.01), kernel_initializer="he_uniform")  
        ])

        # Print a summary of the model's architecture.
        self.model.summary()

Ausgaben:

Zusammenfassung des sequenziellen Modells

03: Training und Test des neuronalen Netzmodells

    def train_network(self, epochs=100, learning_rate=0.001, loss='mean_squared_error'):

        early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) # Early stoppage mechanism | stop training when there is no major change in loss in the last to epochs, defined by the variable patience

        adam = optimizers.Adam(learning_rate=learning_rate) # Adam optimizer >> https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/
    
        # Compile the model: Specify the loss function, optimizer, and evaluation metrics.
        self.model.compile(loss=loss, optimizer=adam, metrics=['mae'])    

        # One hot encode the validation and train target variables
         
        validation_y = self.test_y
        y = self.train_y

        history = self.model.fit(self.train_x, y, epochs=epochs, batch_size=self.batch_size, validation_data=(self.test_x, validation_y), callbacks=[early_stopping], verbose=2)
        
        if not os.path.exists(self.plots_directory): #create plots path if it doesn't exist for saving the train-test plots
            os.makedirs(self.plots_directory)
        
        # save the loss and validation loss plot
        
        plt.figure(figsize=(12, 6))
        plt.plot(history.history['loss'], label='Training Loss')
        plt.plot(history.history['val_loss'], label='Validation Loss')
        plt.xlabel('Epochs')
        plt.ylabel('Loss')
        plt.legend()
        title = 'Training and Validation Loss Curves'
        plt.title(title)
        plt.savefig(fname=f"{self.plots_directory}\\"+title)

        
        # use the trained model to make predictions on the trained data 
        
        pred = self.model.predict(self.train_x)

        acc = metrics.r2_score(self.train_y, pred)

        # Plot actual & pred
        count = [i*0.1 for i in range(len(self.train_y))]

        title = f'MLP {self.target_column} - Train'
        
        # Saving the plot containing information about predictions and actual values
        
        plt.figure(figsize=(7, 5))
        plt.plot(count, self.train_y, label = "Actual")
        plt.plot(count, pred,  label = "forecast")
        plt.xlabel('Actuals')
        plt.ylabel('Preds')
        plt.title(title+f" | Train acc={acc}")
        plt.legend()
        plt.savefig(fname=f"{self.plots_directory}\\"+title)    

        self.model.save(f"Models\\lstm-pat.{self.target_column}.h5") #saving the model in h5 format, this will help us to easily convert this model to onnx later


    def test_network(self):
        # Plot actual & pred
        
        count = [i*0.1 for i in range(len(self.test_y))]

        title = f'MLP {self.target_column} - Test'
        

        pred = self.model.predict(self.test_x)

        acc = metrics.r2_score(self.test_y, pred)

        
        # Saving the plot containing information about predictions and actual values
        
        plt.figure(figsize=(7, 5))
        plt.plot(count, self.test_y, label = "Actual")
        plt.plot(count, pred,  label = "forecast")
        plt.xlabel('Actuals')
        plt.ylabel('Preds')
        plt.title(title+f" | Train acc={acc}")
        plt.legend()
        plt.savefig(fname=f"{self.plots_directory}\\"+title)    
        
        if not os.path.exists(self.plots_directory): #create plots path if it doesn't exist for saving the train-test plots
            os.makedirs(self.plots_directory)
        
        plt.savefig(fname=f"{self.plots_directory}\\"+title)    
        
        return acc

Ausgaben:

Epoch 1/50
219/219 - 2s - loss: 1.2771 - mae: 0.3826 - val_loss: 0.1153 - val_mae: 0.0309 - 2s/epoch - 8ms/step
Epoch 2/50
219/219 - 1s - loss: 0.0836 - mae: 0.0305 - val_loss: 0.0582 - val_mae: 0.0291 - 504ms/epoch - 2ms/step
Epoch 3/50
219/219 - 1s - loss: 0.0433 - mae: 0.0283 - val_loss: 0.0323 - val_mae: 0.0284 - 515ms/epoch - 2ms/step
Epoch 4/50
219/219 - 0s - loss: 0.0262 - mae: 0.0272 - val_loss: 0.0218 - val_mae: 0.0270 - 482ms/epoch - 2ms/step
Epoch 5/50
...
...
Epoch 48/50
219/219 - 0s - loss: 0.0112 - mae: 0.0106 - val_loss: 0.0112 - val_mae: 0.0121 - 490ms/epoch - 2ms/step
Epoch 49/50
219/219 - 0s - loss: 0.0112 - mae: 0.0106 - val_loss: 0.0112 - val_mae: 0.0109 - 486ms/epoch - 2ms/step
Epoch 50/50
219/219 - 1s - loss: 0.0112 - mae: 0.0106 - val_loss: 0.0112 - val_mae: 0.0097 - 501ms/epoch - 2ms/step
219/219 [==============================] - 0s 2ms/step
C:\Users\Omega Joctan\OneDrive\Documents\onnx article\ONNX python\venv\Lib\site-packages\keras\src\engine\training.py:3079: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
  saving_api.save_model(
94/94 [==============================] - 0s 2ms/step
Test accuracy = 0.9336617822086006

Zuggenauigkeit

Das Modell des neuronalen Netzes hatte eine Genauigkeit von 93 % beim Training und etwa 95 % beim Testen. Das könnte „overfitted“ sein, aber wir werden trotzdem damit fortfahren.

04: Speichern des ONNX-Modells.

Es ist im Allgemeinen eine gute Praxis, ein Modell zu speichern, sobald das Training erfolgreich abgeschlossen ist und Sie mit der Leistung des Modells sowohl beim Training als auch bei der Out-of-Sample-Validierung zufrieden sind. Wir müssen ONNX-Laufzeitcode hinzufügen, um das Modell während der Funktion train_network in unserer Klasse zu speichern. Als erstes müssen wir zwei Bibliotheken installieren: onnx und tf2onnx.

def train_network(self, epochs=100, learning_rate=0.001, loss='mean_squared_error'):
# at the end of this function
# ....

    self.model.save(f"Models\\MLP.REG.{self.target_column}.{self.data.shape[0]}.h5") #saving the model in h5 format, this will help us to easily convert this model to onnx later
    self.saveONNXModel()


def saveONNXModel(self, folder="ONNX Models"):
    
    path = data_path + "\\MQL5\\Files\\" + folder 
    
    if not os.path.exists(path): # create this path if it doesn't exist
        os.makedirs(path)
    
    onnx_model_name = f"MLP.REG.{self.target_column}.{self.data.shape[0]}.onnx"
    path +=  "\\" + onnx_model_name
    
    
    loaded_keras_model = load_model(f"Models\\MLP.REG.{self.target_column}.{self.data.shape[0]}.h5") 
    
    onnx_model, _ = tf2onnx.convert.from_keras(loaded_keras_model, output_path=path)

    onnx.save(onnx_model, path )
    
    print(f'Saved model to {path}')

Ausgaben:

onnx gespeichert

Sie haben vielleicht bemerkt, dass ich das ONNX-Modell unter dem übergeordneten Verzeichnis Files gespeichert habe. Warum dieses Verzeichnis? Der Grund dafür ist, dass es einfacher ist, die ONNX-Datei als Ressource in unser MQL5-Programm einzubinden, z. B. in einen Expert Advisor oder einen Indikator.


04: Das erstellte ONNX-Modell in MQL5 erhalten

#resource "\\Files\\ONNX Models\\MLP.REG.CLOSE.10000.onnx" as uchar RNNModel[]

Dies importiert das ONNX-Modell und speichert es im uchar-Array RNNModel.

Als Nächstes müssen wir das ONNX-Handle als globale Variable definieren und das Handle innerhalb der OnInit-Funktion erstellen.

Innerhalb von ONNX mt5.mq5 EA

long mlp_onnxhandle;

#include <MALE5\preprocessing.mqh>
CPreprocessing<vectorf, matrixf> *norm_x;

int inputs[], outputs[];

vectorf OPEN,
       HIGH, 
       LOW;
       
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
  if (!LoadNormParams()) //Load the normalization parameters we saved once
    {
      Print("Normalization parameters csv files couldn't be found \nEnsure you are collecting data and Normalizing them using [ONNX get data.ex5] Script \nTrain the Python model again if necessary");
      return INIT_FAILED;
    }
   
//--- ONNX SETTINGS
 
  mlp_onnxhandle = OnnxCreateFromBuffer(RNNModel, MQLInfoInteger(MQL_DEBUG) ? ONNX_DEBUG_LOGS : ONNX_DEFAULT); //creating onnx handle buffer | rUN DEGUG MODE during debug mode
  
  if (mlp_onnxhandle == INVALID_HANDLE)
    {
       Print("OnnxCreateFromBuffer Error = ",GetLastError());
       return INIT_FAILED;
    }

//--- since not all sizes defined in the input tensor we must set them explicitly
//--- first index - batch size, second index - series size, third index - number of series (only Close)
   
   OnnxTypeInfo type_info; //Getting onnx information for Reference In case you forgot what the loaded ONNX is all about

   long input_count=OnnxGetInputCount(mlp_onnxhandle);
   Print("model has ",input_count," input(s)");
   for(long i=0; i<input_count; i++)
     {
      string input_name=OnnxGetInputName(mlp_onnxhandle,i);
      Print(i," input name is ",input_name);
      if(OnnxGetInputTypeInfo(mlp_onnxhandle,i,type_info))
        {
          PrintTypeInfo(i,"input",type_info);
          ArrayCopy(inputs, type_info.tensor.dimensions);
        }
     }

   long output_count=OnnxGetOutputCount(mlp_onnxhandle);
   Print("model has ",output_count," output(s)");
   for(long i=0; i<output_count; i++)
     {
      string output_name=OnnxGetOutputName(mlp_onnxhandle,i);
      Print(i," output name is ",output_name);
      if(OnnxGetOutputTypeInfo(mlp_onnxhandle,i,type_info))
       {
         PrintTypeInfo(i,"output",type_info);
         ArrayCopy(outputs, type_info.tensor.dimensions);
       }
     }
   
//---

   if (MQLInfoInteger(MQL_DEBUG))
    {
      Print("Inputs & Outputs");
      ArrayPrint(inputs);
      ArrayPrint(outputs);
    }
   
   const long input_shape[] = {batch_size, 3};
   
   if (!OnnxSetInputShape(mlp_onnxhandle, 0, input_shape)) //Giving the Onnx handle the input shape
     {
       printf("Failed to set the input shape Err=%d",GetLastError());
       return INIT_FAILED;
     }
   
   const long output_shape[] = {batch_size, 1};
   
   if (!OnnxSetOutputShape(mlp_onnxhandle, 0, output_shape)) //giving the onnx handle the output shape
     {
       printf("Failed to set the input shape Err=%d",GetLastError());
       return INIT_FAILED;
     }
   
//---

   return(INIT_SUCCEEDED);
  }

Ausgaben:

PR      0       18:57:10.265    ONNX mt5 (EURUSD,H1)    ONNX: Creating and using per session threadpools since use_per_session_threads_ is true
CN      0       18:57:10.265    ONNX mt5 (EURUSD,H1)    ONNX: Dynamic block base set to 0
EE      0       18:57:10.266    ONNX mt5 (EURUSD,H1)    ONNX: Initializing session.
IM      0       18:57:10.266    ONNX mt5 (EURUSD,H1)    ONNX: Adding default CPU execution provider.
JN      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Use DeviceBasedPartition as default
QK      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Saving initialized tensors.
GR      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Done saving initialized tensors
RI      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Session successfully initialized.
JF      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    model has 1 input(s)
QR      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    0 input name is input_1
NF      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       type ONNX_TYPE_TENSOR
PM      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       data type ONNX_TYPE_TENSOR
HI      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape [-1, 3]
FS      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       0 input shape must be defined explicitly before model inference
NE      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape of input data can be reduced to [3] if undefined dimension set to 1
GD      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    model has 1 output(s)
GQ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    0 output name is dense_1
LJ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       type ONNX_TYPE_TENSOR
NQ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       data type ONNX_TYPE_TENSOR
LF      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape [-1, 1]
KQ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       0 output shape must be defined explicitly before model inference
CO      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape of output data can be reduced to [1] if undefined dimension set to 1
GR      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    Inputs & Outputs
IE      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    -1  3
CK      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    -1  1

Live-Daten abrufen

Wie bereits erwähnt, müssen die Live-Daten vom Markt bezogen und auf dieselbe Weise normalisiert werden, wie dies bei der Erfassung der Daten für die Schulung der Fall war.

Innerhalb von ONNX mt5.mq5 EA

matrixf GetLiveData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_LOW, start, total);
        
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
     
     if (!norm_x.Normalization(return_matrix))
        Print("Failed to Normalize");  
      
   return return_matrix;
 }

Damit die Instanz der Klasse norm_x funktioniert, wurde sie innerhalb der Funktion LoadNormParams() in OnInit deklariert. Diese Funktion lädt die gespeicherten Normalisierungsparameter aus einer entsprechenden CSV-Datei.

Innerhalb von ONNX mt5.mq5 EA

bool LoadNormParams()
 {
    vectorf min = {}, max ={}, mean={} , std = {};
    
    csv_name_ = Symbol()+"."+EnumToString(Period())+"."+string(total_bars);
    
    switch(NORM)
      {
       case  NORM_MEAN_NORM:
          
          mean = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv"); //--- Loading the mean
          min = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.min.csv"); //--- Loading the min 
          max = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.max.csv"); //--- Loading the max
          
          if (MQLInfoInteger(MQL_DEBUG))
              Print(EnumToString(NORM),"\nMean ",mean,"\nMin ",min,"\nMax ",max);
          
          norm_x = new CPreprocessing<vectorf,matrixf>(max, mean, min);
           
          if (mean.Sum()<=0 && min.Sum()<=0 && max.Sum() <=0)
              return false;  

         break;
         
       case NORM_MIN_MAX_SCALER:
          
          min = ReadCsvVector(normparams_folder+csv_name_+".min_max_scaler.min.csv"); //--- Loading the min
          max = ReadCsvVector(normparams_folder+csv_name_+".min_max_scaler.max.csv"); //--- Loading the max  
       
           
          if (MQLInfoInteger(MQL_DEBUG))
              Print(EnumToString(NORM),"\nMin ",min,"\nMax ",max);
              
          norm_x = new CPreprocessing<vectorf,matrixf>(max, min);
          
          
          if (min.Sum()<=0 && max.Sum() <=0)
            return false;
            
          break;
          
       case NORM_STANDARDIZATION:
          
          mean = ReadCsvVector(normparams_folder+csv_name_+".standardization_scaler.mean.csv"); //--- Loading the mean
          std = ReadCsvVector(normparams_folder+csv_name_+".standardization_scaler.std.csv"); //--- Loading the std
         
          if (MQLInfoInteger(MQL_DEBUG))
              Print(EnumToString(NORM),"\nMean ",mean,"\nStd ",std);
             
           norm_x = new CPreprocessing<vectorf,matrixf>(mean, std, NORM_STANDARDIZATION);
            
          if (mean.Sum()<=0 && std.Sum() <=0)
            return false;
            
          break;
      }
      
   return true;
 }


05: Ausführen des Modells in Echtzeit

Um das Modell in der Funktion OnTick zu verwenden, müssen wir nur die Funktion OnnxRun aufrufen und ihr das ONNX-Handle, einen Vektor oder eine Matrix von Float-Werten für die Eingaben und Vorhersagen übergeben - das war's.

Innerhalb von ONNX mt5.mq5 EA

void OnTick()
  {
//---
   matrixf input_data = GetLiveData(0,1);
   vectorf output_data(1); //It is very crucial to resize this vector or matrix
   
   
   if (!OnnxRun(mlp_onnxhandle, ONNX_NO_CONVERSION, input_data, output_data))
     {
       Print("Failed to Get the Predictions Err=",GetLastError());
       ExpertRemove();
       return;
     }
   
   Comment("inputs_data\n",input_data,"\npredictions\n",output_data);
}

Der Ausgangsdatenvektor oder die Float-Matrix muss in der Größe angepasst werden, um den Fehlercode 5805 zu vermeiden, der für ERR_ONNX_INVALID_PARAMETER steht. Da ich nur eine Ausgabe im neuronalen Netz habe, habe ich die Größe dieses Vektors auf 1 geändert. Wenn ich eine Matrix verwenden würde, müsste ich die Größe auf 1 Zeile und 1 Spalte ändern,

Ausgaben:

Chart

Großartig, alles funktioniert einwandfrei. Wir verwenden jetzt ein neuronales Netzwerkmodell, das mit Python in MetaTrader5 erstellt und trainiert wurde. Das Verfahren ist aber gar nicht so schwierig, Glückwunsch.


Vorteile der Verwendung von ONNX in MQL5

  1. Interoperabilität: ONNX bietet ein gemeinsames Format für die Darstellung von Deep-Learning-Modellen. Mit diesem Format können Modelle, die in einem Deep-Learning-Framework (wie oder trainiert wurden, in MQL5 verwendet werden, ohne dass eine umfangreiche Neuimplementierung des Modells erforderlich ist. Dadurch können wir viel Zeit sparen, da wir die Modelle nicht mehr von Grund auf neu kodieren müssen, damit sie in MQL5 funktionieren.
  2. Flexibilität: ONNX unterstützt eine breite Palette von Deep-Learning-Modelltypen, von traditionellen neuronalen Feedforward-Netzwerken bis hin zu komplexeren Modellen wie rekurrenten neuronalen Netzwerken (RNNs) und konvolutionären neuronalen Netzwerken (CNNs). Diese Flexibilität macht es für verschiedene Anwendungen geeignet.
  3. Effizienz: ONNX-Modelle können für den effizienten Einsatz auf verschiedenen Hardware- und Plattformsystemen optimiert werden. Das bedeutet, dass Sie Modelle auf Edge-Geräten, mobilen Geräten, Cloud-Servern und sogar speziellen Hardware-Beschleunigern einsetzen können.
  4. Unterstützung der Gemeinschaft: ONNX hat in der Gemeinschaft große Unterstützung gefunden. Die wichtigsten Deep-Learning-Frameworks wie TensorFlow, PyTorch und scikit-learn unterstützen den Export von Modellen im ONNX-Format, und verschiedene Runtime-Engines wie ONNX Runtime erleichtern die Bereitstellung von ONNX-Modellen.
  5. Breites Ökosystem: ONNX ist in verschiedene Softwarepakete integriert, und es gibt umfangreiche Tools für die Arbeit mit ONNX-Modellen. Mit diesen Werkzeugen können Sie Modelle im ONNX-Format konvertieren, optimieren und ausführen.
  6. Plattformübergreifende Kompatibilität: ONNX ist plattformübergreifend konzipiert, d. h. Modelle, die im ONNX-Format exportiert werden, können ohne Änderungen auf verschiedenen Betriebssystemen und Hardwarekomponenten ausgeführt werden. 
  7. Modell Evolution: ONNX unterstützt die Versionierung und Weiterentwicklung von Modellen. Sie können Ihre Modelle im Laufe der Zeit verbessern und erweitern, ohne die Kompatibilität mit früheren Versionen zu verlieren.
  8. Standardisierung: ONNX entwickelt sich zu einem De-facto-Standard für die Interoperabilität zwischen verschiedenen Deep-Learning-Frameworks und erleichtert der Community den Austausch von Modellen und Tools. 


Abschließende Überlegungen

ONNX ist besonders wertvoll in Szenarien, in denen Sie Modelle über verschiedene Frameworks hinweg nutzen, Modelle auf einer Vielzahl von Plattformen einsetzen oder mit anderen zusammenarbeiten müssen, die möglicherweise unterschiedliche Deep Learning-Tools verwenden. Es vereinfacht die Arbeit mit Deep-Learning-Modellen, und da das Ökosystem weiter wächst, werden die Vorteile von ONNX noch bedeutender. In diesem Artikel haben wir die 5 wichtigen Schritte gesehen, die man befolgen muss, um mit einem funktionierenden Modell zu beginnen, um es mal so auszudrücken, Sie können diesen Code erweitern, um ihn Ihren Bedürfnissen anzupassen. Damit das Programm auf dem Strategietester funktioniert, müssen die normalisierten CSV-Dateien innerhalb eines Testers gelesen werden, was ich in diesem Artikel nicht behandelt habe. 

Mit besten Grüßen

Datei Verwendung
 neuralnet.py  Die Hauptpython-Skriptdatei enthält die gesamte Implementierung des neuronalen Netzes in der Sprache Python
ONNX mt5.mq5  Ein Expert Advisor, der zeigt, wie man das ONNX-Modell in Handelssituationen einsetzt
 ONNX get data.mq5   Ein Skript zum Sammeln und Aufbereiten von Daten, die mit dem Python-Skript geteilt werden sollen


Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13394

Beigefügte Dateien |
MQL5-CODE.zip (9.14 KB)
Kategorientheorie in MQL5 (Teil 23): Ein anderer Blick auf den doppelten exponentiellen gleitenden Durchschnitt Kategorientheorie in MQL5 (Teil 23): Ein anderer Blick auf den doppelten exponentiellen gleitenden Durchschnitt
In diesem Artikel setzen wir unser Thema vom letzten Mal fort, indem wir uns mit alltäglichen Handelsindikatoren befassen, die wir in einem „neuen“ Licht betrachten. Wir befassen uns in diesem Beitrag mit der horizontalen Zusammensetzung natürlicher Transformationen, und der beste Indikator dafür, der das soeben behandelte Thema noch erweitert, ist der doppelte exponentielle gleitende Durchschnitt (DEMA).
ALGLIB Bibliothek für numerische Analysen in MQL5 ALGLIB Bibliothek für numerische Analysen in MQL5
Der Artikel wirft einen kurzen Blick auf die numerische Analysebibliothek ALGLIB 3.19, ihre Anwendungen und neue Algorithmen, die die Effizienz der Finanzdatenanalyse verbessern können.
Lernen Sie, wie man mit Datum und Uhrzeit in MQL5 umgeht Lernen Sie, wie man mit Datum und Uhrzeit in MQL5 umgeht
Ein neuer Artikel über ein neues wichtiges Thema, das sich mit Datum und Zeit beschäftigt. Als Händler oder Programmierer von Handelsinstrumenten ist es sehr wichtig zu verstehen, wie man mit diesen beiden Aspekten Datum und Zeit sehr gut und effektiv umgehen kann. Ich werde also einige wichtige Informationen darüber weitergeben, wie wir mit Datum und Zeit umgehen können, um effektive Handelsinstrumente reibungslos und einfach zu erstellen, ohne dass es zu Komplikationen kommt.
Datenkennzeichnung für die Zeitreihenanalyse (Teil 3):Beispiel für die Verwendung von Datenkennzeichnungen Datenkennzeichnung für die Zeitreihenanalyse (Teil 3):Beispiel für die Verwendung von Datenkennzeichnungen
In dieser Artikelserie werden verschiedene Methoden zur Kennzeichnung (labeling) von Zeitreihen vorgestellt, mit denen Daten erstellt werden können, die den meisten Modellen der künstlichen Intelligenz entsprechen. Eine gezielte und bedarfsgerechte Kennzeichnung von Daten kann dazu führen, dass das trainierte Modell der künstlichen Intelligenz besser mit dem erwarteten Design übereinstimmt, die Genauigkeit unseres Modells verbessert wird und das Modell sogar einen qualitativen Sprung machen kann!