English 中文 Español Deutsch 日本語 Português
preview
Освоение ONNX: Переломный момент для MQL5-трейдеров

Освоение ONNX: Переломный момент для MQL5-трейдеров

MetaTrader 5Торговые системы | 21 февраля 2024, 12:23
741 0
Omega J Msigwa
Omega J Msigwa

"Возможность экспортировать и импортировать модели ИИ в формате ONNX упрощает процесс разработки, экономя время и ресурсы при интеграции ИИ в экосистемы, написанные на разных языках".


Введение

Нельзя отрицать, что мы живем в эпоху искусственного интеллекта (ИИ) и машинного обучения: каждый день появляются новые технологии на основе ИИ, применяемые в финансах, искусстве, играх, образовании и многих других аспектах жизни.

Для нас, трейдеров, ИИ полезен тем, что позволяет обнаруживать закономерности и взаимосвязи, которые мы не можем увидеть невооруженным глазом.

Несмотря на кажущуюся мощь и волшебство, за моделями ИИ скрываются сложные математические операции, требующие огромного объема работы и высокой степени точности для их правильного понимания и реализации. К счастью, открытый исходный код существенно упрощает задачу, устраняя необходимость реализовывать модель машинного обучения с нуля.

Сейчас не нужно быть гением математики и программирования, чтобы создавать и реализовывать модели ИИ. Необходимо лишь базовое понимание определенного языка программирования или инструментов, которые вы хотите использовать в вашем проекте. Иногда даже необязательно иметь компьютер. Благодаря таким сервисам как Google Colab, вы можете бесплатно кодировать, создавать и запускать модели ИИ с помощью Python.

Реализовать модели машинного обучения с использованием Python и других популярных и развитых языков программирования относительно просто, чего нельзя сказать об MQL5. Если только вы не хотите изобрести велосипед, создавая модели машинного обучения в MQL5 с нуля, что мы и делаем в этой серии статей, я бы настоятельно советовал использовать ONNX для интеграции моделей ИИ, построенных с помощью Python. К счастью, ONNX теперь поддерживается в MQL5.

  onnx-mql5

Для понимания статьи необходимо базовое представление об ИИ и машинном обучении. Информацию можно найти здесь и здесь


Что такое ONNX?

ONNX (Open Neural Network Exchange) - открытая библиотека для представления моделей машинного и глубокого обучения. Она позволяет конвертировать модели, обученные в одном фреймворке глубокого обучения, в общий формат, который можно использовать в других фреймворках, что упрощает работу с моделями на разных платформах и инструментах.

Это означает, что вы можете создавать модели машинного обучения, используя любой язык, который его поддерживает, кроме MQL5, затем конвертировать эту модель в формат ONNX и использовать внутри вашей MQL5-программы.

В этой статье я буду использовать Python для создания машинного обучения, поскольку я с ним знаком. Насколько я понял, можно использовать и другие языки, хотя я не уверен в этом. Кстати, похоже, что вся документация по ONNX основана на Python. Я считаю, что на данный момент ONNX создан для Python. Это имеет смысл, потому что я не думаю, что существует какой-либо другой язык, кроме Python, с расширенными библиотеками и инструментами на основе ИИ.


Основные понятия в ONNX

Прежде чем погрузиться в ONNX, вы должны быть знакомы с некоторыми ключевыми понятиями:

  • Модель ONNX: Модель ONNX — это представление модели машинного обучения. Она состоит из графа вычислений, где узлы представляют операции (например, свертку, сложение), а ребра представляют поток данных между операциями.
  • Узлы: Узлы графа ONNX представляют операции или функции, применяемые к входным данным. Этими узлами могут быть такие операции, как свертка, сложение или пользовательские операции.
  • Тензоры: Тензоры — это многомерные массивы, которые представляют данные, передаваемые между узлами вычислительного графа. Они могут быть входными, выходными или промежуточными данными.
  • Операторы: Операторы — это функции, применяемые к тензорам в ONNX. Каждый оператор представляет собой определенную операцию, например умножение матрицы или поэлементное сложение.

Создание моделей на Python и развертывание на MQL5 с использованием ONNX

Чтобы успешно построить модель машинного обучения на Python, разверните эту модель внутри своего советника, индикатора или скрипта на MQL5. Для модели требуется нечто большее, чем просто код Python. Ниже приведены важные шаги, которые необходимо выполнить, чтобы в итоге вы получили не только модель ONNX, но и модель, которая действительно дает нужные вам точные прогнозы;

  1. Сбор данных
  2. Нормализация данных на стороне MQL5 
  3. Построение моделей на Python
  4. Получение встроенной модели ONNX в MQL5
  5. Запуск модели в реальном времени


01: Сбор данных

Сбор данных — это первое, что необходимо сделать прямо внутри вашей MQL5-программы. Я считаю, что лучше всего собирать все данные внутри вашей программы, чтобы они соответствовали тому, как мы собираем данные обучения и данные, которые используются во время реальной торговли или запуска модели в режиме реального времени. Имейте в виду, что сбор данных может варьироваться в зависимости от характера проблемы, которую вы пытаетесь решить. В этой статье мы будем пытаться решить проблему регрессии. Мы будем использовать данные OHLC (Open, High, Low, Close) в качестве нашего основного набора данных, где Open, High и Low будут использоваться как независимые переменные, а значения Close будут использоваться в качестве целевой переменной.

Inside ONNX 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);
    
    
    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;
 } 

Эта функция собирает независимые переменные OHL и целевую переменную CLOSE. При контролируемом машинном обучении целевую переменную необходимо указать и передать модели, чтобы она могла учиться на ней и понимать закономерности между целевой переменной и остальными переменными. В нашем случае модель попытается понять, как эти показания индикатора приводят к бычьему или медвежьему движению. 

При развертывании модели нам необходимо собирать данные таким же способом за исключением того, что на этот раз мы будем собирать данные без целевой переменной, потому что мы хотим, чтобы наша обученная модель это выяснила. По сути, речь идет о прогнозировании.

Вот почему существует другая функция GetLiveData для загрузки новых данных для быстрых прогнозов на рынке.

Inside 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;
 }

Сбор обучающих данных

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

Результаты

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]

Получение реальных данных

Получение текущей данных OHL по бару.

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

Результаты

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]]

Способ загрузки данных в реальном времени может немного отличаться при подготовке данных для модели прогнозирования временных рядов, таких как RNN, GRU и LSTM

Обратите внимание, что я использовалmatrixf, что означает матрицу с плавающей запятой, вместо обычной matrix!! Это необходимо для обеспечения совместимости типов данных между MQL5 и Python. Убедитесь, что тип входных данных совместим с типом данных, ожидаемым моделью ONNX. Если ваша ONNX-модель ожидает входные данные float32, убедитесь, что ваши входные данные также имеют тип float32. ONNX совместим с float32 и float64. Нарушение совместимости приводит к следующим ошибкам;
error 2023.09.18 18:03:53.212   ONNX: invalid parameter size, expected 1044480 bytes instead of 32640


02: Нормализация данных на стороне MQL5

Нормализация данных — одна из наиболее важных вещей, которые необходимо реализовать правильно для набора данных, который будет использоваться моделью машинного обучения.

Имейте в виду, что метод нормализации, используемый для подготовки обучающих данных, должен быть таким же, как и для подготовки тестовых и реальных данных. Это означает, что при использовании метода MinMaxScaler значения min и max, которые являются фундаментальными переменными в уравнении MinMaxScaler, использовавшемся при подготовке обучающих данных, должны использоваться для продолжения нормализации новых данных, которые будут обрабатываться моделью в другом месте. Чтобы добиться такой согласованности, нам необходимо сохранить переменные для каждого метода нормализации в файл csv:

Нормализация данных предназначена только для независимых переменных. Независимо от того, какую проблему вы пытаетесь решить, вам не нужно нормализовать целевую переменную.

Мы собираемся использовать класс Preprocessing, представленный здесь.

Inside ONNX get data.mq5 script

 //--- 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;
   } 

Результаты

параметры нормализации

Когда Standardization Scaler использовался внутри файлов csv, параметры выглядели следующим образом:


Обратите внимание, что нормализация также интегрирована внутри функции GetData. Поскольку нормализация очень важна, каждая матрица данных, возвращаемая обеими функциями, ответственными за сбор данных, должна быть матрицей с нормализованными значениями цен.

Inside ONNX get data.mq5 script

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;
 } 

 Наконец, данные были сохранены в формате CSV, чтобы их можно было использовать совместно с кодом Python.


03: Построение моделей на Python

Я буду строить нейронную сеть многослойного перцептрона, но вы можете построить любую модель по вашему выбору. Вы не ограничены этим конкретным типом модели. Установите Python, если у вас его еще нет. Затем установите virtualenv, выполнив следующие команды из Windows CMD (не путайте с Powershell!!)

$ pip3 install virtualenv

После запуска,

$ virtualenv venv

Это создаст виртуальную среду Python для вашего компьютера с Windows. Этот процесс может немного отличаться для пользователей Mac и Linux. После этого запустите виртуальную среду, выполнив эту команду:

$ venv\Scripts\activate

После этого установите все зависимости, используемые в этом руководстве, запустив

$ pip install -r requirements.txt #This txt file is found at https://github.com/MegaJoctan/ONNX-MQL5/blob/main/requirements.txt

Всегда важно изолировать проект, создавая виртуальную среду, чтобы избежать конфликтов между модулями и версиями Python и упростить совместное использование проекта.

Импорт и инициализация 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"

Нам нужно проверить, существует ли путь. Если он не существует, это означает, что мы не собрали данные со стороны MetaTrader 5.

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()

Построение многоуровневой нейронной сети перцептрона (MLP)

Мы собираемся обернуть NN MLP внутри класса, чтобы превратить наш код в читаемые разделы;

01: Инициализация класса

Данные собираются и разбиваются на обучающую и тестовую выборки, при этом важные переменные объявляются доступными для использования всем классом,

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"
                

Результаты

pd head

02: Построение нейронной сети

Наша однослойная нейронная сеть определяется заданным количеством нейронов.

    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()

Результаты

последовательное описание модели

03: Обучение и тестирование модели нейронной сети

    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

Результаты

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

точность обучения

Модель нейронной сети имела точность 93% во время обучения и около 95% во время тестирования. Есть риск подгонки, но мы всё равно продолжим работать с ней.

04: Сохранение модели ONNX

Рекомендуется сохранять модель после успешного завершения обучения и после того, как вы убедились в производительности модели как при обучении, так и при проверке вне выборки. Нам необходимо добавить код времени выполнения ONNX, чтобы сохранить модель во время функции train_network в нашем классе. Прежде всего нам нужно установить две библиотеки - onnx и 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}')

Результаты

onnx сохранена

Возможно, вы заметили, что я решил сохранить модель ONNX в родительском каталоге Files. Почему там? Это связано с тем, что файл ONNX проще включить в нашу MQL5-программу в качестве ресурса, например советника или индикатора.


04: Получение встроенной модели ONNX в MQL5

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

Команда импортирует модель ONNX и сохраняет ее внутри массива RNNModel uchar.

Следующее, что нам нужно сделать, это определить хэндл ONNX в качестве глобальной переменной и создать хэндл внутри функции OnInit.

Inside 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);
  }

Результаты

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

Получение реальных данных

Как я уже говорил ранее, живые данные должны быть получены с рынка и нормализованы так же, как они были нормализованы при сборе данных для обучения.

Inside 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;
 }

Рабочий экземпляр класса norm_x был объявлен внутри функции LoadNormParams() в OnInit. Функция загружает сохраненные параметры нормализации из соответствующего CSV-файла.

Inside 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: Запуск модели в режиме реального времени

Чтобы использовать модель внутри функции OnTick, необходимо вызвать функцию OnnxRun и передать ей хэндл ONNX, вектор или матрицу значений float как для входных данных, так и для прогнозов.

Inside 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);
}

Вектор выходных данных или матрицы float необходимо изменить, чтобы избежать ошибки 5805 (ERR_ONNX_INVALID_PARAMETER). Поскольку у меня есть только один выход в нейронной сети, я изменил размер этого вектора, чтобы он имел размер 1 Если бы я тогда использовал матрицу, я должен был изменить ее размер до 1 строки и 1 столбца.

Результаты

график

Всё работает нормально. Сейчас мы используем модель нейронной сети, созданную и обученную с использованием Python внутри MetaTrader5. Процедура не так сложна.


Преимущества использования ONNX в MQL5

  1. Взаимодействие: ONNX предоставляет общий формат для представления моделей глубокого обучения. Этот формат позволяет моделям, обученным в одной среде глубокого обучения (такой как TensorFlow, PyTorch или scikit-learn), использоваться в MQL5 без необходимости обширной повторной реализации модели. Это может сэкономить много времени, поскольку нам больше не придется жестко кодировать модели с нуля, чтобы заставить их работать в MQL5.
  2. Гибкость: ONNX поддерживает широкий спектр типов моделей глубокого обучения: от традиционных нейронных сетей прямого распространения до более сложных моделей, таких как рекуррентные нейронные сети (RNN) и сверточные нейронные сети (CNN). Эта гибкость делает его пригодным для различных применений.
  3. Эффективность: модели ONNX можно оптимизировать для эффективного развертывания на различном оборудовании и платформах. Это означает, что вы можете развертывать модели на периферийных устройствах, мобильных устройствах, облачных серверах и даже в специализированных аппаратных ускорителях.
  4. Поддержка сообщества: ONNX располагает существенной поддержкой сообщества. Основные платформы глубокого обучения, такие как TensorFlow, PyTorch и scikit-learn, поддерживают экспорт моделей в формат ONNX, а различные механизмы выполнения, такие как ONNX Runtime, упрощают развертывание ONNX-моделей.
  5. Широкая экосистема: ONNX интегрирован в различные пакеты программного обеспечения, имеется обширный набор инструментов для работы с ONNX-моделями. С помощью этих инструментов вы можете конвертировать, оптимизировать и запускать модели в формате ONNX.
  6. Кроссплатформенная совместимость: Модели, экспортированные в формате ONNX, могут работать на различных операционных системах и оборудовании без изменений. 
  7. Эволюция модели: ONNX поддерживает управление версиями и эволюцию модели. Вы можете со временем улучшать и расширять свои модели, сохраняя при этом совместимость с предыдущими версиями.
  8. Стандартизация: ONNX становится фактическим стандартом взаимодействия между различными платформами глубокого обучения, что упрощает для сообщества обмен моделями и инструментами. 


Заключение

ONNX особенно ценен в сценариях, где вам необходимо использовать модели в разных платформах, развертывать модели на различных платформах или сотрудничать с другими людьми, которые могут использовать разные инструменты глубокого обучения. Он упрощает процесс работы с моделями глубокого обучения, а по мере дальнейшего роста экосистемы преимущества ONNX становятся еще более значимыми. В этой статье мы рассмотрели пять важных шагов, которые необходимо выполнить, чтобы начать работу с рабочей моделью. По меньшей мере вы можете расширить этот код в соответствии со своими потребностями. Кроме того, чтобы программа работала с тестером стратегий, CSV-файлы нормализации необходимо прочитать внутри тестера. В текущей статье я это не рассматривал. 

Спасибо за внимание!

Файл Применение
neuralnet.py  Основной файл Python-скрипта. Содержит всю реализацию нейронной сети на языке Python
ONNX mt5.mq5  Советник, показывающий, как использовать модель ONNX в торговле
ONNX get data.mq5  Скрипт для сбора и подготовки данных для совместного использования с Python-скриптом


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/13394

Прикрепленные файлы |
MQL5-CODE.zip (9.14 KB)
DoEasy. Элементы управления (Часть 33): вертикальный "ScrollBar" DoEasy. Элементы управления (Часть 33): вертикальный "ScrollBar"
В статье продолжим разработку графических элементов библиотеки DoEasy, и добавим вертикальную прокрутку элементов управления объекта-формы и некоторые полезные функции и методы, которые потребуются в дальнейшем.
Изучение MQL5 — от новичка до профи (Часть II): Базовые типы данных и использование переменных Изучение MQL5 — от новичка до профи (Часть II): Базовые типы данных и использование переменных
Продолжение серии для начинающих. Здесь мы рассмотрим, как создавать константы и переменные, записывать дату, цвета и другие полезные данные. Научимся создавать перечисления вроде дней недели или стилей линий (сплошная, пунктирная и т.д.). Переменные и выражения - это база программирования. Они обязательно есть в 99% программ, поэтому понимать их критически важно. И поэтому, если вы - новичок в программировании - прошу. Уровень знания программирования: очень базовый - в пределах моей предыдущей статьи (ссылка - в начале).
Добавляем пользовательскую LLM в торгового робота (Часть 1): Развертывание оборудования и среды Добавляем пользовательскую LLM в торгового робота (Часть 1): Развертывание оборудования и среды
Языковые модели (LLM) являются важной частью быстро развивающегося искусственного интеллекта, поэтому нам следует подумать о том, как интегрировать мощные LLM в нашу алгоритмическую торговлю. Большинству людей сложно настроить эти мощные модели в соответствии со своими потребностями, развернуть их локально, а затем применить к алгоритмической торговле. В этой серии статей будет рассмотрен пошаговый подход к достижению этой цели.
Нейросети — это просто (Часть 77): Кросс-ковариационный Трансформер (XCiT) Нейросети — это просто (Часть 77): Кросс-ковариационный Трансформер (XCiT)
В своих моделях мы часто используем различные алгоритмы внимание. И, наверное, чаще всего мы используем Трансформеры. Основным их недостатком является требование к ресурсам. В данной статье я хочу предложить Вам познакомиться с алгоритмом, который поможет снизить затраты на вычисления без потери качества.