English Deutsch 日本語
preview
Superar los retos de integración de ONNX

Superar los retos de integración de ONNX

MetaTrader 5Sistemas comerciales | 20 agosto 2024, 10:26
29 0
Omega J Msigwa
Omega J Msigwa

Introducción

ONNX (Open Neural Network Exchange) revoluciona la forma de hacer sofisticados programas MQL5 basados en IA. Esta nueva tecnología para MetaTrader 5 es el camino a seguir para el aprendizaje automático, ya que muestra un montón de promesas como ningún otro para su propósito, sin embargo, ONNX viene con un par de desafíos que pueden darle dolores de cabeza si no tienes ni idea de cómo resolverlos en absoluto.

Si despliega una técnica de IA sencilla, como una red neuronal de avance, es posible que el proceso de despliegue no le resulte tan problemático, pero dado que la mayoría de los proyectos de la vida real son mucho más complejos, es posible que tenga que hacer muchas cosas, como extraer datos de series temporales, preprocesar y transformar los macrodatos para reducir sus dimensiones, por no hablar de cuando tiene que utilizar varios modelos en un gran proyecto de despliegue de modelos de ONNX; en situaciones como esta, el despliegue de ONNX puede resultar complicado.

ONNX es una herramienta autosuficiente que viene con la capacidad de almacenar sólo un modelo de IA. No viene con todo lo necesario en la caja para ejecutar los modelos entrenados en el otro extremo, depende de ti averiguar cómo vas a desplegar tus modelos ONNX finales. En este artículo, discutiremos los tres retos que son escalar y normalizar los datos, introducir la reducción de dimensión al modelo y, superar el reto de desplegar modelos ONNX para predicciones de series temporales.

Modelos ONNX MQL5mql5

Este artículo asume que usted tiene una comprensión básica del aprendizaje automático y la teoría de la IA, y que al menos ha intentado utilizar modelos ONNX en MQL5 una o dos veces.


Superar los retos del preprocesamiento de datos

En el contexto del aprendizaje automático, el procesamiento de datos se refiere al proceso de transformar los valores de las características de su conjunto de datos a un rango específico. El objetivo de esta transformación es lograr una representación más coherente de los datos para su modelo de aprendizaje automático. El proceso de escalado es muy importante por varias razones;

Mejora el rendimiento de los modelos de aprendizaje automático: Muchos algoritmos de aprendizaje automático, especialmente los basados en distancias como K-Nearest Neighbors (KNN) y Support Vector Machines (SVM), se basan en el cálculo de distancias entre puntos de datos. Si las características tienen escalas muy diferentes (por ejemplo, una característica en miles, otra en décimas), las características con escalas más grandes dominarán los cálculos de distancia, lo que conducirá a un rendimiento subóptimo. El escalado sitúa todas las características en un rango similar, lo que permite al modelo centrarse en las relaciones reales entre los puntos de datos.

Convergencia de entrenamiento más rápida: Los algoritmos de optimización basados en el descenso de gradiente, utilizados habitualmente en redes neuronales y otros modelos, dan pasos hacia la solución óptima basándose en los gradientes de la función de pérdida. Cuando las características tienen diferentes escalas, los gradientes también pueden tener magnitudes muy diferentes, lo que hace más difícil para el optimizador encontrar el mínimo de manera eficiente. El escalado ayuda a que los gradientes tengan un rango más consistente, lo que conduce a una convergencia más rápida.

Asegura la estabilidad de las operaciones numéricas: Algunos algoritmos de aprendizaje automático implican cálculos que pueden volverse inestables con características de escalas significativamente diferentes. El escalado ayuda a evitar estos problemas numéricos y garantiza que el modelo realice los cálculos con precisión.


Técnicas comunes de escalado:

  • Normalización (escala mín-máx): Esta técnica escala las características a un rango específico (a menudo de 0 a 1 o de -1 a 1).
  • Normalización (normalización de la puntuación Z): Esta técnica centra los datos restando la media de cada característica y luego los escala dividiéndolos por la desviación estándar.

Por crucial que sea este proceso de normalización, no hay muchas fuentes en línea que expliquen la forma correcta de hacerlo. La misma técnica de escalado y sus parámetros utilizados para los datos de entrenamiento deben aplicarse a los datos de prueba y al desplegar el modelo.

Utilizando la misma analogía del escalador: Imagine que tiene una característica que representa «ingresos» en sus datos de entrenamiento. El escalador aprende los valores mínimo y máximo de los ingresos (o la media y la desviación estándar para la normalización) durante el entrenamiento. Si utiliza un escalador diferente en los datos de prueba, puede que encuentre valores de ingresos fuera del rango que vio durante el entrenamiento. Esto puede dar lugar a un escalado inesperado e introducir incoherencias entre los datos de entrenamiento y los de prueba.

Utilizando los mismos parámetros para la analogía del escalímetro: Imagina una regla utilizada para medir la altura. Si utilizas una regla diferente marcada con unidades distintas (pulgadas frente a centímetros) para entrenar y probar, tus medidas no serían comparables. Del mismo modo, el uso de diferentes escaladores en los datos de entrenamiento y de prueba altera el marco de referencia que el modelo aprendió durante el entrenamiento.

En esencia, utilizar el mismo escalador garantiza que el modelo vea los datos de forma coherente tanto durante el entrenamiento como durante las pruebas, lo que conduce a resultados más fiables e interpretables.

Puedes utilizar las técnicas de escalado del módulo de Python Scikit-learn.preprocessing. Todo irá bien siempre y cuando estés construyendo el modelo ONNX y desplegándolo en el mismo lenguaje 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)

Sin embargo, las cosas se complican cuando se quiere utilizar el modelo entrenado en el lenguaje MQL5. A pesar de que hay varias formas de guardar el escalador en Python será un reto extraerlo en MetaEditor ya que Python tiene sus formas extravagantes de almacenar objetos y hacer el proceso más fácil de lo que es en otros lenguajes de programación. Lo mejor sería pre-procesar los datos en MQL5, guardar el escalador, y guardar los datos escalados en un archivo CSV que vamos a leer usando código Python.

A continuación se presenta la hoja de ruta para el tratamiento previo de los datos:

  1. Recoger los datos del mercado y escalarlos
  2. Guardar el escalador 
  3. Guardar los datos escalados en un archivo CSV


01: Recoger datos del mercado y ampliarlos

Vamos a recopilar las tasas de Apertura, Máximo, Mínimo y Cierre para 1000 barras de un marco temporal diario y luego crearemos un problema de reconocimiento de patrones asignando el patrón alcista siempre que el precio haya cerrado por encima de donde abrió y una señal bajista en caso contrario. Al entrenar el modelo de IA LSTM en este patrón, estamos intentando que comprenda qué contribuye a estos patrones para que, una vez que esté bien entrenado, sea capaz de proporcionarnos la señal de trading.

Dentro del script de recogida de datos de ONNX:

Empezaremos por incluir las librerías que necesitamos:

#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

A continuación, tenemos que recopilar la información sobre los precios.

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
     }

//---
  }

¡Recuerda! El escalado es para las variables independientes, por eso dividimos la matriz de datos en la matriz y el vector `x` e `y` respectivamente para obtener la matriz `x` con la que podemos escalar las columnas.

   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: Guardar el escalador

Como hemos dicho antes, tenemos que guardar el escalador para utilizarlo más adelante.

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

Después de ejecutar este fragmento de código, se creará una carpeta con archivos binarios. Estos dos archivos contienen los parámetros para el escalador de estandarización, más adelante veremos cómo utilizar estos parámetros para cargar la instancia de escalador guardada.

Escalador EURUSD


03: Guardar los datos escalados en un archivo CSV

Por último, pero no por ello menos importante, necesitamos guardar los datos escalados en un archivo CSV que podamos utilizar posteriormente en código 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;
    }

Resultado:



Superar los retos de los datos de series temporales

Hay algunos estudios que sugieren que los modelos de aprendizaje profundo de series temporales como GRU, LSTM y RNN son mejores para hacer predicciones en el mercado de valores en comparación con otros modelos, debido a su capacidad para comprender patrones durante un cierto período de tiempo.

Resulta que hay algunas líneas adicionales de código que puede ser necesario escribir para preparar los datos para que sean adecuados para las predicciones de series temporales utilizando estos modelos.

Si ha trabajado alguna vez con modelos de series temporales probablemente haya visto una función o código similar a éste:

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)

Esta función es muy importante para los modelos de series temporales como el LSTM, ya que realiza la preparación de los datos:

  • Dividir los datos en secuencias de un tamaño fijo (time_step).
  • Separar las características (información pasada) de los objetivos (valor previsto).
  • Transformación de los datos en un formato adecuado para los modelos LSTM.

Esta preparación de los datos ayuda a proporcionar al modelo LSTM la información más relevante de forma estructurada, lo que permite un entrenamiento más rápido, una mejor gestión de la memoria y, potencialmente, una mayor precisión en las predicciones.

Mientras que las LSTM pueden manejar secuencias, los datos en tiempo real introducen un flujo continuo de información. Aún es necesario definir una ventana temporal de datos pasados para que el modelo los tenga en cuenta a la hora de hacer predicciones. Esto hace que esta función sea necesaria no sólo en el entrenamiento y las pruebas, sino también para las predicciones en tiempo real. No necesitaremos las matrices `y`, pero sí el código para remodelar la matriz `x`. Vamos a hacer predicciones en tiempo real en MetaTrader 5, ¿verdad? Necesitamos hacer una función similar a esta en MQL5. 

Antes comprobemos las dimensiones de las matrices Numpy `x` e `y` devueltas por la función `get_sequential_data` cuando el valor del paso de tiempo era 7.

X_reshaped, Y_reshaped = get_sequential_data(dataset, step_size)

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

Resultados:

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

La matriz `x` devuelta es un array 3D, en otras palabras, un tensor, mientras que los datos `y` devueltos son una matriz 1D, es decir, un vector. Debemos tener esto en cuenta al crear una función similar en MQL5.

Ahora vamos a hacer una clase simple con el nombre 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 
  };

Las dos funciones con nombres similares extract_timeseries_data hacen un trabajo similar excepto que una no devuelve el vector y, se utilizará para predicciones en tiempo real.

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

Ahora dentro de un asesor experto llamado `ONNX challenges EA` vamos a tratar de utilizar estas funciones para extraer los datos de series de tiempo:

#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());
 }

Resultados:

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

Genial, obtuvimos las mismas dimensiones que en el código de Python.

El propósito de ONNX es conseguir que un modelo de aprendizaje automático construido en un lenguaje funcione bien si no igual en el otro lenguaje, esto quiere decir que si construyo un modelo en Python y lo ejecuto allí la exactitud y precisión que me proporcione allí debería ser casi la misma que me proporcionará en otro lenguaje, en este caso, el lenguaje MQL5 cuando se utilicen los mismos datos sin conversión.

Si este es el caso, antes de utilizar el modelo ONNX en MQL5 es necesario comprobar si se ha hecho todo bien probando el modelo con los mismos datos en ambas plataformas para ver si proporciona la misma precisión. Pongamos a prueba este modelo.

Hice el modelo LSTM con 10 neuronas en la capa de entrada y la única capa oculta en la red, asigné el Optimizador Adam al progreso de aprendizaje.

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

Resultados:

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

Entrené el modelo durante 100 épocas con la paciencia establecida en 5 épocas y un tamaño de lote de 64 (`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)

El modelo LSTM convergió en la 77ª época con una pérdida = 0,3000 y una puntuación de precisión de: 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

Gráfico de pérdidas frente a iteraciones


Por último, he probado el modelo con el conjunto de datos completo;

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

El resultado fue el siguiente:

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

LSTM model accuracy:  0.9179507704622774

Tenemos que esperar este valor de precisión o algo parecido cuando utilicemos este modelo LSTM que

fue guardado en ONNX en MQL5. inp_model_name fue 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}")

Incluyamos este modelo dentro de nuestro asesor experto.

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

Dentro de la librería onnx.mqh no hay nada más que una clase ONNX que inicializa el modelo ONNX y tiene funciones para hacer predicciones.

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

Por último, ejecuté un modelo LSTM cargado dentro de ONNX challenges EA:

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

El resultado fue el siguiente:

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

¡Estupendo! Obtenemos el mismo valor de exactitud que en el código Python con precisión de cifras significativas. Esto nos dice que hemos hecho todo de la manera correcta.

Ahora vamos a utilizar este modelo para hacer predicciones en tiempo real antes de continuar:

Inside ONNX challenges REALTIME EA;

Dado que vamos a realizar predicciones sobre conjuntos de datos en tiempo real, a diferencia de lo que hicimos anteriormente, cuando utilizamos el archivo CSV que contenía los datos normalizados para las pruebas, esta vez tenemos que cargar el escalador que guardamos una vez y aplicarlo a los nuevos datos cada vez antes de alimentar los datos a nuestro modelo LSTM en formato 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[];

Justo después de cargar el modelo ONNX como recurso, debemos incluir los archivos binarios `mean` y `std` que hemos guardado.

Esta vez llamamos al escalador de normalización con un puntero, ya que lo instanciaremos con los valores del escalador guardados.

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

Así es como se normaliza cada nuevo dato de entrada:

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

Finalmente, ejecuté el EA en el probador de estrategias sin errores, las predicciones se mostraban correctamente en el gráfico.

Señales LSTM en el gráfico


Superar el reto de la reducción de dimensiones

Una de las herramientas útiles que los científicos de datos suelen llevar en su caja de herramientas son los algoritmos de reducción de dimensión como el PCA, LDA, NMF, Truncated SVD y muchos más. A pesar de tener sus inconvenientes, los algoritmos de reducción de la dimensionalidad siguen teniendo sus ventajas, entre las que se incluyen: 


Ventajas de la reducción de la dimensionalidad:

Mejora del rendimiento de los modelos: Los datos de alta dimensionalidad pueden provocar la «maldición de la dimensionalidad», por la que los modelos tienen dificultades para aprender de forma eficaz debido al enorme espacio de características. El PCA reduce la complejidad y puede mejorar el rendimiento de varios algoritmos de aprendizaje automático, como la clasificación, la regresión y la agrupación.

Entrenamiento y procesamiento más rápidos: El entrenamiento de modelos de aprendizaje automático en datos de alta dimensión puede ser costoso desde el punto de vista informático. El PCA reduce el número de características, lo que acelera el tiempo de entrenamiento y reduce potencialmente las necesidades de recursos informáticos.

Reducción del sobreajuste: la alta dimensionalidad puede aumentar el riesgo de sobreajuste, en el que el modelo memoriza los datos de entrenamiento pero no consigue generalizar bien a los datos no vistos. El PCA ayuda a mitigar este riesgo centrándose en las características más informativas.

Al igual que las técnicas de escalado, es genial si se utiliza una técnica de reducción de dimensión como el Principal Component Analysis (PCA) ofrecido por Scikit-Learn sin embargo, usted tendrá un tiempo difícil encontrar la manera de utilizar este PCA en MQL5 donde la mayor parte del trabajo se hace incluyendo el comercio basado en todo lo que construyó. 

Dentro del script de recogida de datos ONNX tenemos que añadir el PCA.

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

CPCA *pca;

Queremos añadir la técnica PCA para normalizar las variables `x` antes de que tenga lugar el proceso de normalización.

   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

Esto creará una subcarpeta bajo la carpeta MQL5\Files, Esta carpeta constará de archivos binarios con información para el PCA.

Archivos PCA ONNX

El nuevo conjunto de datos CSV con PCA tiene ahora dos variables independientes como se indica en el constructor PCA para hacer dos componentes de los datos originales.

Conjunto de datos PCA

Para evitar confusiones podemos crear una condición booleana para comprobar si la condición para PCA está permitida por el usuario ya que guardar los datos PCA en un archivo CSV podría ser diferente también podríamos necesitar que se altere el nombre de un archivo CSV e incluir PCA en su nombre para poder identificar la diferencia entre los archivos CSV del conjunto de datos.

Dentro del script de recogida de datos 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;
  }

También tenemos que hacer cambios similares en el EA principal llamado 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);

  }

¿Nota los cambios? Hay dos modelos incluidos en el asesor experto, un modelo LSTM fue entrenado en un conjunto de datos regulares y el otro con la palabra PCA en su nombre fue entrenado en los datos aplicados con PCA, ya que los datos pasados bajo PCA pueden venir con diferentes dimensiones en comparación con los datos que no fueron pasados que siempre tendrán dimensiones similares a los datos originales, Esta diferencia hace que sea importante tener también diferentes escaladores para cada modelo.

Ahora que hemos dado espacio para un nuevo modelo con PCA para llenar, vamos a volver a nuestro script Python y hacer algunos cambios. Sólo hay que hacer unos pocos cambios: el nombre del archivo CSV y el nombre final del archivo ONNX:

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

Esta vez el modelo convergió en la 17ª época:

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

Convergió con una precisión justa del 52,48% algo que suele ocurrir, no cerca del 89% que conseguimos sin PCA. Ahora vamos a hacer una estrategia simple donde podemos abrir operaciones basadas en las señales dadas:

La lógica de la negociación es sencilla. Compruebe si no hay ninguna posición abierta en la dirección, y abra una en esa dirección sin perder de vista el cambio de señal. Si hay una nueva señal cierre una posición de su tipo y la posición en sentido contrario.

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

Realicé pruebas en el modelo de precios de apertura en el marco de tiempo de 12 horas, ya que el marco de tiempo diario genera muchos errores de mercado cerrado. A continuación se presentan los resultados cuando se aplicó el modelo LSTM con PCA:

Probador de estrategias PCA LSTM

Sin PCA:

LSTM sin PCA



Reflexiones finales

ONNX es una gran herramienta, pero necesitamos empezar a pensar fuera de la caja al usarla. Al brindarnos la capacidad de compartir código de aprendizaje automático entre diferentes plataformas, nos ahorra mucho trabajo y dolores de cabeza que pueden surgir cuando decides implementar estos sofisticados modelos de Deep Learning e Inteligencia Artificial en el lenguaje MQL5. Sin embargo, aún necesitas hacer tu parte del trabajo para terminar con un programa confiable y funcional.

Paz.

Para obtener más información sobre todos los archivos incluidos en esta publicación y más, consulta este repositorio de GitHub.

Archivos adjuntos: 

Archivo Descripción y uso
MatrixExtend.mqh
Dispone de funciones adicionales para la manipulación de matrices.
metrics.mqh
Contiene funciones y código para medir el rendimiento de los modelos ML.
preprocessing.mqh
La librería para el preprocesamiento de datos de entrada sin procesar para hacerlos aptos para el uso de modelos de aprendizaje automático.
plots.mqh
Librería para el trazado de vectores y matrices.
Timeseries Deep Learning\onnx.mqh
Esta librería consiste en la clase ONNX, encargada de leer los ficheros .onnx y utilizar los ficheros cargados para realizar predicciones
Tensors.mqh
Una librería que contiene tensores, objetos de matrices algebraicas 3D, programada en lenguaje MQL5 puro.
Timeseries Deep Learning\tsdataprocessor.mqh
Una librería con una clase que contiene funciones para convertir datos brutos en datos adecuados para predicciones de series temporales.
Dimensionality Reduction\base.mqh
Un archivo que contiene las funciones necesarias para las tareas de reducción de dimensiones. 
Dimensionality Reduction\PCA.mqh
Librería de análisis de componentes principales (PCA)
Python\onnx_timeseries.ipynb  Un `Jupyter Notebook` de Python que contiene todo el código de Python utilizado en esta publicación. 
Python\requirements.txt  Un archivo de texto con todas las dependencias de Python necesarias para que el código de Python funcione.


Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/14703

Archivos adjuntos |
Code_7_Files.zip (81.91 KB)
Algoritmos de optimización de la población: Resiliencia ante el estancamiento en los extremos locales (Parte I) Algoritmos de optimización de la población: Resiliencia ante el estancamiento en los extremos locales (Parte I)
El presente artículo presenta un experimento único cuyo objetivo es investigar el comportamiento de los algoritmos de optimización basados en poblaciones en el contexto de su capacidad para abandonar eficientemente los mínimos locales cuando la diversidad en la población es baja y alcanzar los máximos globales. Los trabajos en este campo nos permitirán comprender mejor qué algoritmos específicos pueden continuar con éxito la búsqueda a partir de las coordenadas fijadas por el usuario como punto de partida, y qué factores influyen en su éxito en este proceso.
Clase básica de algoritmos de población como base para una optimización eficaz Clase básica de algoritmos de población como base para una optimización eficaz
El presente material supone un intento único de investigación para combinar una variedad de algoritmos de población en una sola clase y simplificar la aplicación de técnicas de optimización. Este enfoque no solo descubre oportunidades para el desarrollo de nuevos algoritmos, incluidas variantes híbridas, sino que también crea un banco de pruebas básico y versátil. Este banco se convertirá así en una herramienta clave para seleccionar el algoritmo óptimo según un problema específico.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Formulación Genérica de Optimización (GOF, Generic Optimization Formulation) utilizando el `Criterio máximos del usuario` (Custom Max) con múltiples restricciones en el Probador de Estrategias Formulación Genérica de Optimización (GOF, Generic Optimization Formulation) utilizando el `Criterio máximos del usuario` (Custom Max) con múltiples restricciones en el Probador de Estrategias
En este artículo presentaremos una forma de implementar problemas de optimización con múltiples objetivos y restricciones al seleccionar «Custom Max» en la pestaña Setting del terminal MetaTrader 5. Como ejemplo, el problema de optimización podría ser: Maximizar el Factor de Beneficio, el Beneficio Neto y el Factor de Recuperación, de forma que la reducción sea inferior al 10%, el número de pérdidas consecutivas sea inferior a 5 y el número de operaciones por semana sea superior a 5.