English Español Deutsch 日本語
preview
Решение проблем интеграции ONNX

Решение проблем интеграции ONNX

MetaTrader 5Торговые системы | 2 сентября 2024, 15:31
115 2
Omega J Msigwa
Omega J Msigwa

Введение

ONNX (Open Neural Network Exchange) совершил революцию в разработке сложных MQL5-программ на основе искусственного интеллекта (ИИ). Эта новая технология MetaTrader 5 — шаг к машинному обучению, поскольку она, как никакая другая, обещает широкие возможности для решения задач. Однако применение ONNX сопряжено с рядом проблем.

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

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

модели onnx mql5

Предполагается, что у вас есть базовые знания о машинном обучении и теории ИИ и что вы применяли модели ONNX в MQL5 как минимум один или два раза.


Предварительная обработка данных

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

Он улучшает производительность моделей машинного обучения: Многие алгоритмы машинного обучения, особенно основанные на расстоянии, такие как метод K-ближайших соседей (KNN) и метод опорных векторов (SVM), полагаются на вычисление расстояний между точками данных. Если масштабы признаков существенно различаются (например, один признак составляет тысячи, а другой — десятые доли), то признаки с большими масштабами будут доминировать при расчетах расстояний, что приведет к неоптимальной производительности. Масштабирование помещает все признаки в одинаковый диапазон, позволяя модели сосредоточиться на фактических отношениях между точками данных.

Более быстрая сходимость обучения: Алгоритмы оптимизации на основе градиентного спуска, обычно используемые в нейронных сетях и других моделях, предпринимают шаги к оптимальному решению на основе градиентов функции потерь. Если признаки имеют разные масштабы, то же самое происходит и с градиентами, усложняя оптмизиатору задачу по поиску минимума. Масштабирование помогает градиентам иметь более постоянный диапазон, что приводит к более быстрой сходимости.

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


Распространенные методы масштабирования:

  • Нормализация (масштабирование по минимуму-максимуму): Метод масштабирует признаки до определенного диапазона (часто от 0 до 1 или от -1 до 1).
  • Стандартизация (нормализация Z-оценки): этот метод центрирует данные, вычитая среднее значение из каждого признака, а затем масштабирует их, деля на стандартное отклонение.

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

Используя ту же аналогию с масштабированием, представьте, что в ваших обучающих данных есть признак, представляющий "доход". В процессе обучения масштабировщик узнает минимальные и максимальные значения дохода (или среднее значение и стандартное отклонение для стандартизации). Если вы используете другой масштабировщик для тестовых данных, он может столкнуться со значениями дохода, выходящими за пределы диапазона, который он видел во время обучения. Это может привести к неожиданному масштабированию и возникновению несоответствий между данными обучения и тестирования.

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

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

Вы можете использовать методы масштабирования из модуля Python Scikit-learn.preprocessing. Всё будет хорошо, если вы создаете модель ONNX и развертываете ее на том же языке Python.
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import numpy as np

# Example data
data = np.array([[1000, 2], [500, 1], [2000, 4], [800, 3]])

# Create a MinMaxScaler object
scaler_minmax = MinMaxScaler()

# Fit the scaler on the training data (learn min/max values)
scaler_minmax.fit(data)

# Transform the data using the fitted scaler
data_scaled_minmax = scaler_minmax.transform(data)

print("Original data:\n", data)
print("\nMin Max Scaled data:\n", data_scaled_minmax)

Однако все становится сложнее, когда вы хотите использовать обученную модель на языке MQL5. Несмотря на то, что масштабировщик можно сохранить разными способами в Python, его будет сложно извлечь в MetaEditor, поскольку в Python есть свои способы хранения объектов. Лучшим решением будет выполнить предварительную обработку данных в MQL5, сохранить масштабировщик, а также сохранить масштабированные данные в CSV-файле, который мы будем читать с помощью кода Python.

Ниже приведена дорожная карта предварительной обработки данных:

  1. Соберите рыночные данные и масштабируйте их
  2. Сохранить масштабировщик 
  3. Сохраните масштабированные данные в CSV-файл


01: Сбор и масштабирование рыночных данных

Мы собираем данные о ценах открытия, максимума, минимума и закрытия для 1000 баров с дневного таймфрейма, а затем создаем задачу распознавания паттернов, назначая бычий паттерн всякий раз, когда цена закрытия выше цены открытия, и медвежий сигнал в противном случае. Обучая модель ИИ LSTM по этому шаблону, мы пытаемся заставить ее понять, что влияет на эти шаблоны, чтобы после качественного обучения она могла выдавать нам торговые сигналы.

Внутри скрипта сбора данных ONNX:

Начнем с включения необходимых нам библиотек:

#include <MALE5\preprocessing.mqh> //This library contains the normalization techniques for machine learning
#include <MALE5\MatrixExtend.mqh>

StandardizationScaler scaler; //We want to use z-normalization/standardization technique for this project

Затем нам необходимо собрать информацию о ценах.

input int data_size = 10000; //number of bars to collect for our dataset

MqlRates rates[];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- vector.CopyRates is lacking we are going to copy rates in normal way

   ArraySetAsSeries(rates, true);    
   if (CopyRates(Symbol(), PERIOD_D1, 1, data_size, rates)<-1)
     {
       printf("Failed to collect data Err=%d",GetLastError());
       return;
     }
   

   matrix OHLC(data_size, 4);
   for (int i=0; i<data_size; i++) //Get OHLC values and save them to a matrix
     {
       OHLC[i][0] = rates[i].open;
       OHLC[i][1] = rates[i].high;
       OHLC[i][2] = rates[i].low;
       
        if (rates[i].close>rates[i].open)
          OHLC[i][3] = 1; //Buy signal
        else if (rates[i].close<rates[i].open)
          OHLC[i][3] = 0; //sell signal
     }

//---
  }

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

   matrix x;
   vector y;
   MatrixExtend::XandYSplitMatrices(OHLC, x, y);  //WE split the data into x and y | The last column in the matrix will be assigned to the y vector 
     
//--- Standardize the data
   
   x = scaler.fit_transform(x);   


02: Сохранение масштабировщика

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

   if (!scaler.save(Symbol()+"-SCALER"))
      return;

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

масштабировщик eurusd


03: Сохранение масштабированных данных в CSV-файл

И последнее, но не менее важное: нам необходимо сохранить масштабированные данные в CSV-файле, который мы позже сможем использовать в коде Python.
   OHLC = MatrixExtend::concatenate(x, y); //We apped the y column to the scaled x matrix, this is the opposite of XandYsplitMatrices function
   if (!MatrixExtend::WriteCsv(Symbol()+"-OHLSignal.csv",OHLC,"open,high,low,signal",false,8))       
    {
     DebugBreak();
     return;
    }

Результат:



Работа с данными временных рядов

Некоторые исследования показывают, что модели глубокого обучения временных рядов, такие как GRU, LSTM и RNN, лучше подходят для прогнозирования на фондовом рынке по сравнению с другими моделями, благодаря их способности понимать закономерности за определенный период времени. Большинство алгоритмических трейдеров, занимающихся анализом данных (включая и меня), предпочитают именно эти модели.

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

Если вы когда-либо работали с моделями временных рядов, вы, вероятно, видели похожую функцию или код:

def get_sequential_data(data, time_step):
    if dataset.empty is True:
      print("Failed to create sequences from an empty dataset")
      return

    Y = data.iloc[:, -1].to_numpy() # get the last column from the dataset and assign it to y numpy 1D array
    X = data.iloc[:, :-1].to_numpy() # Get all the columns from data array except the last column, assign them to x numpy 2D array

    X_reshaped = []
    Y_reshaped = []

    for i in range(len(Y) - time_step + 1):
        X_reshaped.append(X[i:i + time_step])
        Y_reshaped.append(Y[i + time_step - 1])

    return np.array(X_reshaped), np.array(Y_reshaped)

Эта функция очень важна для моделей временных рядов, таких как LSTM. Она выполняет подготовку данных путем:

  • Разбиения данных на последовательности фиксированного размера (time_step).
  • Разделения характеристик (предыдущей информации) от целей (прогнозируемого значения).
  • Преобразования данных в формат, подходящий для моделей LSTM.

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

В то время как LSTM могут обрабатывать последовательности, данные в реальном времени представляют собой непрерывный поток информации. Вам по-прежнему необходимо определить временное окно прошлых данных, которое модель будет учитывать при составлении прогнозов. Это делает данную функцию необходимой не только для обучения и тестирования, но и для прогнозирования в реальном времени. Нам не понадобятся массивы y, но нам понадобится код для изменения формы массива x. Мы собираемся делать прогнозы в реальном времени в MetaTrader 5. Поэтому нам нужно создать аналогичную функцию в MQL5. 

Перед этим давайте проверим размеры x и y массивов Numpy, возвращаемых функцией get_sequential_data при значении временного шага, равным 7.

X_reshaped, Y_reshaped = get_sequential_data(dataset, step_size)

print(f"x_shape{X_reshaped.shape} y_shape{Y_reshaped.shape}")

Результаты:

x_shape(9994, 7, 3) y_shape(9994,)

Возвращаемый массив x представляет собой трехмерный массив (тензор), в то время как возвращаемые данные y представляют собой одномерную матрицу (вектор). Нам необходимо учитывать это при создании аналогичной функции в MQL5.

Теперь давайте создадим простой класс с именем CTSDataProcessor:

class CTSDataProcessor 
  {
CTensors *tensor_memory[]; //Tensor objects may be hard to track in memory once we return them from a function, this keeps track of them
bool xandysplit;

public:
                     CTSDataProcessor (void);
                    ~CTSDataProcessor (void);
                    
                     CTensors *extract_timeseries_data(const matrix<double> &x, const int time_step); //for real time predictions
                     CTensors *extract_timeseries_data(const matrix<double> &MATRIX, vector &y, const int time_step); //for training and testing purposes 
  };

Две функции с похожими именам extract_timeseries_data выполняют аналогичную работу, за исключением того, что одна из них не возвращают вектор y. Он будет использоваться для прогнозов в реальном времени.

CTSDataProcessor ::CTSDataProcessor (void)
 {
   xandysplit = true; //by default obtain the y vector also
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CTensors *CTSDataProcessor ::extract_timeseries_data(const matrix<double> &x,const int time_step)
 {
  CTensors *timeseries_tensor;
  timeseries_tensor = new CTensors(0);
  ArrayResize(tensor_memory, 1);
  tensor_memory[0] = timeseries_tensor;
  
  xandysplit = false; //In this function we do not obtain the y vector
  
  vector y;
  timeseries_tensor = extract_timeseries_data(x, y, time_step);
  
  xandysplit = true; //restore the original condition
   
  return timeseries_tensor;
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CTensors *CTSDataProcessor ::extract_timeseries_data(const matrix &MATRIX, vector &y,const int time_step)
 {
  CTensors *timeseries_tensor;
  timeseries_tensor = new CTensors(0);
  ArrayResize(tensor_memory, 1);
  tensor_memory[0] = timeseries_tensor;
  
  matrix<double> time_series_data = {};
  matrix x = {}; //store the x variables converted to timeseries
  vector y_original = {};
  y.Init(0);
  
  if (xandysplit) //if we are required to obtain the y vector also split the given matrix into x and y
     if (!MatrixExtend::XandYSplitMatrices(MATRIX, x, y_original))
       {
         printf("%s failed to split the x and y matrices in order to make a tensor",__FUNCTION__);
         return timeseries_tensor;
       }
  
  x = xandysplit ? x : MATRIX; 
  
  for (ulong sample=0; sample<x.Rows(); sample++) //Go throught all the samples
    {
      matrix<double> time_series_matrix = {};
      vector<double> timeseries_y(1);
      
      for (ulong time_step_index=0; time_step_index<(ulong)time_step; time_step_index++)
        {
            if (sample + time_step_index >= x.Rows())
                break;
             
             time_series_matrix = MatrixExtend::concatenate(time_series_matrix, x.Row(sample+time_step_index), 0);
             
             if (xandysplit)
               timeseries_y[0] = y_original[sample+time_step_index]; //The last value in the column is assumed to be a y value so it gets added to the y vector
        }
      
      if (time_series_matrix.Rows()<(ulong)time_step)
        continue;
        
        timeseries_tensor.Append(time_series_matrix);
         
        if (xandysplit)
         y = MatrixExtend::concatenate(y, timeseries_y);
    }
   
   return timeseries_tensor;
 }

Теперь внутри советника ONNX попробуем использовать эти функции для извлечения данных временного ряда:

#include <Timeseries Deep Learning\tsdataprocessor.mqh>

input int time_step_ = 7;
//it is very important the time step value matches the one used during training in  a python script


CTSDataProcessor ts_dataprocessor;
CTensors *ts_data_tensor;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
   if (!onnx.Init(lstm_model))
     return INIT_FAILED;
     
   string headers;  
   matrix data = MatrixExtend::ReadCsv("EURUSD-OHLSignal.csv",headers); //let us open the same data so that we don't get confused along the way
   
   matrix x;
   vector y;
      
   ts_data_tensor = ts_dataprocessor.extract_timeseries_data(data, y, time_step_);
   
   printf("x_shape %s y_shape (%d,)",ts_data_tensor.shape(),y.Size());
 }

Результаты:

GD      0       07:21:14.710    ONNX challenges EA (EURUSD,H1)  Warning: CTensors::shape assumes all matrices in the tensor have the same size
IG      0       07:21:14.710    ONNX challenges EA (EURUSD,H1)  x_shape (9994, 7, 3) y_shape (9994,)

Отлично! Мы получили те же размеры, что и в коде Python.

Цель ONNX — заставить модель машинного обучения, созданную на одном языке, работать так же хорошо на другом. Это означает, что если я создам модель на Python и запущу ее там, то точность и достоверность, которые она мне предоставит, должны быть почти такими же, как и на другом языке (в данном случае - MQL5), когда те же данные использовались без преобразования.

Перед использованием модели ONNX в MQL5 необходимо проверить, всё ли вы сделали правильно, протестировав модель на одних и тех же данных на обеих платформах, чтобы увидеть, обеспечивает ли она одинаковую точность. Давайте протестируем модель.

Я создал модель LSTM с 10 нейронами во входном слое и одним скрытым слоем в сети и назначил для хода обучения оптимизатор Adam.

from keras.optimizers import Adam
from keras.callbacks import EarlyStopping

learning_rate = 1e-3
patience = 5 #if this number of epochs validation loss is unchanged stop the process


model = Sequential()

model.add(LSTM(units=10, input_shape=(step_size, dataset.shape[1]-1))) #Input layer
model.add(Dense(units=10, activation='relu', kernel_initializer='he_uniform'))
model.add(Dropout(0.3))
model.add(Dense(units=len(classes_in_data), activation = 'softmax')) #last layer outputs = classes in data

model.compile(optimizer=Adam(learning_rate=learning_rate), loss="binary_crossentropy", metrics=['accuracy'])

Результаты:

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 lstm (LSTM)                 (None, 10)                560       
                                                                 
 dense (Dense)               (None, 10)                110       
                                                                 
 dropout (Dropout)           (None, 10)                0         
                                                                 
 dense_1 (Dense)             (None, 2)                 22        
                                                                 
=================================================================
Total params: 692 (2.70 KB)
Trainable params: 692 (2.70 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

Я обучил модель в течение 100 эпох, установив patience на 5 эпох и batch_size = 64.

from keras.utils import to_categorical

y_train = to_categorical(y_train, num_classes=len(classes_in_data)) #ONE-HOT encoding
y_test = to_categorical(y_test, num_classes=len(classes_in_data)) #ONE-HOT encoding

early_stopping = EarlyStopping(monitor='val_loss', patience = patience, restore_best_weights=True)

history = model.fit(x_train, y_train, epochs = 100 , validation_data = (x_test,y_test), callbacks=[early_stopping], batch_size=64, verbose=2)

Модель LSTM сошлась на 77-й эпохе со значением loss = 0,3000 и оценкой точности 0,8876.

Epoch 75/100
110/110 - 1s - loss: 0.3076 - accuracy: 0.8856 - val_loss: 0.2702 - val_accuracy: 0.8983 - 628ms/epoch - 6ms/step
Epoch 76/100
110/110 - 1s - loss: 0.2968 - accuracy: 0.8856 - val_loss: 0.2611 - val_accuracy: 0.9060 - 651ms/epoch - 6ms/step
Epoch 77/100
110/110 - 1s - loss: 0.3000 - accuracy: 0.8876 - val_loss: 0.2634 - val_accuracy: 0.9063 - 714ms/epoch - 6ms/step

loss vs iterations graph


Наконец, я протестировал модель на всем наборе данных:

X_reshaped, Y_reshaped = get_sequential_data(dataset, step_size)

predictions = model.predict(X_reshaped)

predictions = classes_in_data[np.argmax(predictions, axis=1)]  # Find class with highest probability | converting predicted probabilities to classes

from sklearn.metrics import accuracy_score

print("LSTM model accuracy: ", accuracy_score(Y_reshaped, predictions))

Результат:

313/313 [==============================] - 2s 3ms/step

LSTM model accuracy:  0.9179507704622774

Нам нужно ожидать это значение точности или что-то близкое к нему, когда мы используем эту модель LSTM, которая

была сохранена в ONNX в MQL5. inp_model_name была model.eurusd.D1.onnx.

output_path = inp_model_name
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"saved model to {output_path}")

Давайте включим эту модель в наш советник.

#include <Timeseries Deep Learning\onnx.mqh>
#include <Timeseries Deep Learning\tsdataprocessor.mqh>
#include <MALE5\metrics.mqh>

#resource "\\Files\\model.eurusd.D1.onnx" as uchar lstm_model[]

input int time_step_ = 7;
//it is very important the time step value matches the one used during training in  a python script

CONNX onnx;
CTSDataProcessor ts_dataprocessor;
CTensors *ts_data_tensor;

Внутри библиотеки onnx.mqh нет ничего, кроме класса ONNX, который инициализирует модель ONNX и имеет функции для выполнения прогнозов.

class CONNX
  {
protected:

   bool initialized;
   long onnx_handle;
   void PrintTypeInfo(const long num,const string layer,const OnnxTypeInfo& type_info);
   long inputs[], outputs[];
   
   void replace(long &arr[]) { for (uint i=0; i<arr.Size(); i++) if (arr[i] <= -1) arr[i] = UNDEFINED_REPLACE; }
   string ConvertTime(double seconds);
   
public:
                     CONNX(void);
                    ~CONNX(void);
                     
                     bool Init(const uchar &onnx_buff[], ulong flags=ONNX_DEFAULT); //Initislized ONNX model from a resource uchar array with default flag
                     bool Init(string onnx_filename, uint flags=ONNX_DEFAULT); //Initializes the ONNX model from a .onnx filename given

                     virtual int predict_bin(const matrix &x, const vector &classes_in_data); //Returns the predictions for the current given matrix, this function is for real-time prediction
                     virtual vector predict_bin(CTensors &timeseries_tensor, const vector &classes_in_data); //gives out the vector for all the predictions | useful function for testing only
                     virtual vector predict_proba(const matrix &x); //Gives out the predictions for the current given matrix | this function is for realtime predictions
  };

Наконец, я запустил загруженную модель LSTM внутри советника ONNX challenges:

int OnInit()
  {
   if (!onnx.Init(lstm_model))
     return INIT_FAILED;
     
   string headers;  
   matrix data = MatrixExtend::ReadCsv("EURUSD-OHLSignal.csv",headers); //let us open the same data so that we don't get confused along the way
   
   matrix x;
   vector y;
      
   ts_data_tensor = ts_dataprocessor.extract_timeseries_data(data, y, time_step_);
      
   vector classes_in_data = MatrixExtend::Unique(y); //Get the classes in the data
      
   vector preds = onnx.predict_bin(ts_data_tensor, classes_in_data);
   
   Print("LSTM Model Accuracy: ",Metrics::accuracy_score(y, preds));
   
//---
   return(INIT_SUCCEEDED);
  }

Результат:

2024.04.14 07:44:16.667 ONNX challenges EA (EURUSD,H1)  LSTM Model Accuracy: 0.9179507704622774

Отлично! Мы получили то же значение точности, что и в коде Python с точностью до значащих цифр. Это говорит нам о том, что мы всё сделали правильно.

Теперь давайте воспользуемся этой моделью для составления прогнозов в реальном времени, прежде чем двигаться дальше:

Внутри ONNX challenges REALTIME EA;

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

#resource "\\Files\\model.eurusd.D1.onnx" as uchar lstm_model[]
#resource "\\Files\\EURUSD-SCALER\\mean.bin" as double standardization_scaler_mean[];
#resource "\\Files\\EURUSD-SCALER\\std.bin" as double standardization_scaler_std[];

Сразу после загрузки модели ONNX в качестве ресурса нам необходимо включить сохраненные нами двоичные файлы mean и std.

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

#include <Timeseries Deep Learning\onnx.mqh>
#include <Timeseries Deep Learning\tsdataprocessor.mqh>
#include <MALE5\preprocessing.mqh>

#resource "\\Files\\model.eurusd.D1.onnx" as uchar lstm_model[]
#resource "\\Files\\EURUSD-SCALER\\mean.bin" as double standardization_scaler_mean[];
#resource "\\Files\\EURUSD-SCALER\\std.bin" as double standardization_scaler_std[];

input int time_step_ = 7;
//it is very important the time step value matches the one used during training in  a python script

CONNX onnx;
StandardizationScaler *scaler;
CTSDataProcessor ts_dataprocessor;
CTensors *ts_data_tensor;

MqlRates rates[];
vector classes_ = {0,1};
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
   if (!onnx.Init(lstm_model))
     return INIT_FAILED;
   
   scaler = new StandardizationScaler(standardization_scaler_mean, standardization_scaler_std); //laoding the saved scaler
   
//---
   return(INIT_SUCCEEDED);
  }

Вот как мы нормализуем все новые входные данные:

void OnTick()
  { 
   if (CopyRates(Symbol(), PERIOD_D1, 1, time_step_, rates)<-1)
     {
       printf("Failed to collect data Err=%d",GetLastError());
       return;
     }
   
   matrix data(time_step_, 3);
   for (int i=0; i<time_step_; i++) //Get the independent values and save them to a matrix
     {
       data[i][0] = rates[i].open;
       data[i][1] = rates[i].high;
       data[i][2] = rates[i].low;
     }
   
   ts_data_tensor = ts_dataprocessor.extract_timeseries_data(data, time_step_);  //process the new data into timeseries 
   
   data = ts_data_tensor.Get(0); //This tensor contains only one matrix for the recent latest bars thats why we find it at the index 0
   data = scaler.transform(data); //Transform the new data 
   
   int signal = onnx.predict_bin(data, classes_);
   
   Comment("LSTM trade signal: ",signal);
  }

Наконец, я запустил советник на тестере стратегий без ошибок. Прогнозы успешно отображались на графике.

Сигналы LSTM на графике


Преодоление проблемы снижения размерности

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


Преимущества снижения размерности:

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

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

Уменьшение переобучения - высокая размерность может увеличить риск переобучения, когда модель запоминает обучающие данные, но не может эффективно обобщать их на невидимые данные. PCA помогает снизить этот риск, сосредоточившись на наиболее информативных признаках.

Как и в случае с методами масштабирования, было бы здорово использовать метод уменьшения размерности, например анализ главных компонентов (PCA), предлагаемый Scikit-Learn. Однако вам будет сложно найти способы использования PCA в MQL5, где выполняется большая часть работы, включая торговлю. 

Нам нужно добавить PCA в скрипт сбора данных ONNX.

#include <MALE5\Dimensionality Reduction\PCA.mqh>

CPCA *pca;

Добавим метод PCA для нормализации переменных x до нормализации.

   MatrixExtend::XandYSplitMatrices(OHLC, x, y);  //WE split the data into x and y | The last column in the matrix will be assigned to the y vector 

//--- Reduce data dimension

   pca = new CPCA(2); //reduce the data to have two columns
   x = pca.fit_transform(x);
   if (!pca.save(Symbol()+"-PCA"))
     return

Это создаст подпапку в папке MQL5\Files. Эта папка будет содержать двоичные файлы с информацией для PCA.

pca files onnx

Новый набор данных CSV с PCA теперь имеет две независимые переменные, как указано в конструкторе PCA, для создания двух компонентов из исходных данных.

набор данных pca

Чтобы избежать путаницы, мы можем создать логическое условие для проверки того, разрешено ли пользователем условие для PCA, поскольку сохранение данных PCA в CSV-файл может отличаться. Также нам может потребоваться изменить имя CSV-файла, включив в него PCA, чтобы мы могли определить разницу между CSV-файлами наборов данных.

Внутри скрипта сбора данных ONNX:

input bool use_pca = true;

MqlRates rates[];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- vector.CopyRates is lacking we are going to copy rates in normal way
... some code

//---
  
   matrix x;
   vector y;
   MatrixExtend::XandYSplitMatrices(OHLC, x, y);  //WE split the data into x and y | The last column in the matrix will be assigned to the y vector 

//--- Reduce data dimension
     
     if (use_pca)
      { 
         pca = new CPCA(2); //reduce the data to have two columns
         x = pca.fit_transform(x);
         if (!pca.save(Symbol()+"-PCA"))
           return;
      }
      
//--- Standardize the data  
     ...rest of the code
   
   if (CheckPointer(pca)!=POINTER_INVALID)
      delete pca;
  }

Нам также необходимо внести аналогичные изменения в основной советник ONNX challenges REALTIME.

//.... other imports 

#include <MALE5\Dimensionality Reduction\PCA.mqh>

CPCA *pca;

#resource "\\Files\\model.eurusd.D1.onnx" as uchar lstm_model_data[]
#resource "\\Files\\model.eurusd.D1.PCA.onnx" as uchar lstm_model_pca[]

#resource "\\Files\\EURUSD-SCALER\\mean.bin" as double standardization_scaler_mean[];
#resource "\\Files\\EURUSD-SCALER\\std.bin" as double standardization_scaler_std[];

#resource "\\Files\\EURUSD-PCA-SCALER\\mean.bin" as double standardization_pca_scaler_mean[];
#resource "\\Files\\EURUSD-PCA-SCALER\\std.bin" as double standardization_pca_scaler_std[];

#resource "\\Files\\EURUSD-PCA\\components-matrix.bin" as double pca_comp_matrix[];
#resource "\\Files\\EURUSD-PCA\\mean.bin" as double pca_mean[];


input int time_step_ = 7;
input bool use_pca = true;

//it is very important the time step value matches the one used during training in  a python script

CONNX onnx;
StandardizationScaler *scaler;

// ......

MqlRates rates[];
vector classes_ = {0,1};
int prev_bars = 0;
MqlTick ticks;
double min_lot = 0;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
   if (use_pca)
    {
    if (!onnx.Init(lstm_model_pca))
      return INIT_FAILED;
    }
   else
     {
       if (!onnx.Init(lstm_model_data))
         return INIT_FAILED;
     }
   
   if (use_pca)   
    {
      scaler = new StandardizationScaler(standardization_pca_scaler_mean, standardization_pca_scaler_std); //loading the saved scaler applied to PCA data
      pca = new CPCA(pca_mean, pca_comp_matrix);
    }  
   else
      scaler = new StandardizationScaler(standardization_scaler_mean, standardization_scaler_std); //laoding the saved scaler
    
//---
   
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetDeviationInPoints(100);
   m_trade.SetTypeFillingBySymbol(Symbol());
   m_trade.SetMarginMode();
   
   min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
   
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   // ... collecting data code 
 ...
   
   ts_data_tensor = ts_dataprocessor.extract_timeseries_data(data, time_step_);  //process the new data into timeseries 
   
   data = ts_data_tensor.Get(0); //This tensor contains only one matrix for the recent latest bars thats why we find it at the index 0
   
   if (use_pca)
    data = pca.transform(data);
    
   data = scaler.transform(data); //Transform the new data 
   
   int signal = onnx.predict_bin(data, classes_);
   
   Comment("LSTM trade signal: ",signal);

  }

Заметили изменения? В состав советника входят две модели: одна (модель LSTM) была обучена на обычном наборе данных, а другая (со словом PCA в названии) была обучена на данных, примененных с помощью PCA, поскольку данные, переданные с помощью PCA, могут иметь другие измерения по сравнению с данными, которые не были переданы. Это различие делает важным наличие различных масштабирующих коэффициентов для каждой модели.

Теперь, когда мы освободили место для новой модели с PCA, давайте вернемся к нашему скрипту Python и внесем несколько изменений. Осталось внести несколько изменений - имя файла CSV и окончательное имя файла ONNX:

csv_file = "EURUSD-OHLSignalPCA.csv"
step_size = 7
inp_model_name = "model.eurusd.D1.PCA.onnx"

На этот раз модель сошлась на 17-й эпохе:

110/110 - 1s - loss: 0.6920 - accuracy: 0.5215 - val_loss: 0.6921 - val_accuracy: 0.5168 - 658ms/epoch - 6ms/step
Epoch 15/100
110/110 - 1s - loss: 0.6918 - accuracy: 0.5197 - val_loss: 0.6921 - val_accuracy: 0.5175 - 656ms/epoch - 6ms/step
Epoch 16/100
110/110 - 1s - loss: 0.6919 - accuracy: 0.5167 - val_loss: 0.6921 - val_accuracy: 0.5178 - 627ms/epoch - 6ms/step
Epoch 17/100
110/110 - 1s - loss: 0.6919 - accuracy: 0.5248 - val_loss: 0.6920 - val_accuracy: 0.5222 - 596ms/epoch - 5ms/step

Сходимость составила относительно типичные 52,48%, а не 89%, которые мы получили без PCA. Теперь давайте создадим простую стратегию, в которой мы сможем открывать сделки на основе полученных сигналов:

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

void OnTick()
  {
//---
   
   if (!MQLInfoInteger(MQL_TESTER)) //if we are live trading consider new bar event
      if (!isnewBar(PERIOD_CURRENT))
        return;
      
//.... some code to collect data
... 
    
   data = scaler.transform(data); //Transform the new data 
   
   int signal = onnx.predict_bin(data, classes_);
   
   Comment("LSTM trade signal: ",signal);

//--- Open trades based on Signals
   
   SymbolInfoTick(Symbol(), ticks);
   if (signal==1) 
    {
      if (!PosExists(POSITION_TYPE_BUY))
        m_trade.Buy(min_lot,Symbol(), ticks.ask);
      else
       {
         PosClose(POSITION_TYPE_BUY); 
         PosClose(POSITION_TYPE_SELL); 
       } 
    }
   else
     {
      if (!PosExists(POSITION_TYPE_SELL))
        m_trade.Sell(min_lot,Symbol(), ticks.bid);
      else
       {
          PosClose(POSITION_TYPE_SELL); 
          PosClose(POSITION_TYPE_BUY); 
       }
     } 
  }

Я провел тесты по модели цен открытия на 12-часовом таймфрейме, поскольку дневной таймфрейм дает много ошибок закрытия рынка. Ниже приведены результаты при применении модели LSTM с PCA:

PCA lstm тестер стратегий

Без PCA:

lstm без PCA тестер



Заключительные мысли

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

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

Вложения: 

Файл Описание и использование
MatrixExtend.mqh
Дополнительные функции для работы с матрицами.
metrics.mqh
Функции и код для измерения производительности моделей машинного обучения.
preprocessing.mqh
Библиотека для предварительной обработки необработанных входных данных, чтобы сделать их пригодными для использования в моделях машинного обучения.
plots.mqh
Библиотека для построения векторов и матриц.
Timeseries Deep Learning\onnx.mqh
Библиотека состоит из класса ONNX, отвечающего за чтение файлов .onnx и использование загруженных файлов для создания прогнозов.
Tensors.mqh
Библиотека, содержащая тензоры, объекты алгебраических 3D-матриц на простом языке MQL5
Timeseries Deep Learning\tsdataprocessor.mqh
Библиотека с классом, содержащим функции для преобразования необработанных данных в данные, пригодные для прогнозирования временных рядов
Dimensionality Reduction\base.mqh
Файл, содержащий необходимые функции для задач по уменьшению размерности 
Dimensionality Reduction\PCA.mqh
Библиотека метода анализа главных компонентов(PCA)
Python\onnx_timeseries.ipynb  Python jupyter-notebook, содержащий весь код Python, использованный в этом посте 
Python\requirements.txt  Текстовый файл со всеми зависимостями Python, необходимыми для запуска кода Python.


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

Прикрепленные файлы |
Code_7_Files.zip (81.91 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
gino
gino | 23 июл. 2024 в 16:13

Здравствуйте,

Спасибо за эту очень информативную статью!

Однако у меня возникла проблема при воспроизведении ваших результатов.

Когда я выполняю скрипт 'ONNX collect data.mq5' (прикрепляю его к дневному графику EURUSD), я получаю следующую ошибку :

2024.07.23 15:58:35.344 ONNX collect data (EURUSD,D1) array out of range in 'ONNX collect data.mq5' (39,27)

Я что-то делаю не так?


С уважением,

Джино.

Omega J Msigwa
Omega J Msigwa | 23 июл. 2024 в 16:36
gino array out of range in 'ONNX collect data.mq5' (39,27)

Я что-то делаю не так?


С уважением,

Джино.

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

Возможности Мастера MQL5, которые вам нужно знать (Часть 16): Метод главных компонент с собственными векторами Возможности Мастера MQL5, которые вам нужно знать (Часть 16): Метод главных компонент с собственными векторами
В статье рассматривается метод главных компонент — метод снижения размерности при анализе данных, а также то, как его можно реализовать с использованием собственных значений и векторов. Как всегда, мы попытаемся разработать прототип класса сигналов советника, который можно будет использовать в Мастере MQL5.
Нейросети в трейдинге: Анализ облака точек (PointNet) Нейросети в трейдинге: Анализ облака точек (PointNet)
Прямой анализ облака точек позволяет избежать излишнего увеличения объема данных и повышает эффективность моделей в задачах классификации и сегментации. Подобные подходы демонстрируют высокую производительность и устойчивость к возмущениям в исходных данных.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Методы Уильяма Ганна (Часть III): Работает ли астрология? Методы Уильяма Ганна (Часть III): Работает ли астрология?
Влияет ли положение планет и звезд на финансовые рынки? Вооружимся статистикой и большими данными и отправимся в увлекательное путешествие в мир, где пересекаются звезды и биржевые графики.