English Русский Español Deutsch 日本語
preview
Superando Desafios de Integração com ONNX

Superando Desafios de Integração com ONNX

MetaTrader 5Sistemas de negociação | 16 setembro 2024, 10:20
25 0
Omega J Msigwa
Omega J Msigwa

Introdução

ONNX (Open Neural Network Exchange) está revolucionando a forma como criamos programas sofisticados de IA baseados em MQL5. Essa nova tecnologia para o MetaTrader 5 é o futuro do aprendizado de máquina, pois demonstra um grande potencial como nenhuma outra para esse propósito. No entanto, o ONNX traz alguns desafios que podem causar dor de cabeça se você não souber como resolvê-los.

Se você implementar uma técnica de IA simples como uma rede neural feedforward, talvez não ache o processo de implantação tão problemático, mas, como a maioria dos projetos da vida real é muito mais complexa, pode ser necessário realizar várias tarefas, como extrair dados de séries temporais, pré-processar e transformar grandes volumes de dados para reduzir suas dimensões. Sem contar quando você tem que usar vários modelos em um grande projeto. Nessas situações, implantar modelos ONNX pode se tornar complicado.

ONNX é uma ferramenta autossuficiente que permite armazenar apenas um modelo de IA. Ela não inclui todos os elementos necessários para executar os modelos treinados no outro lado, e cabe a você descobrir como vai implantar seus modelos ONNX finais. Neste artigo, discutiremos três desafios: escalar e normalizar os dados, introduzir a redução de dimensão no modelo e superar o desafio de implantar modelos ONNX para previsões de séries temporais.

modelos onnx mql5

Este artigo assume que você tem um entendimento básico de aprendizado de máquina e teoria de IA, e que já tentou usar modelos ONNX no MQL5 uma ou duas vezes.


Superando os Desafios do Pré-processamento de Dados

No contexto de aprendizado de máquina, o processamento de dados refere-se ao processo de transformar os valores das características em seu conjunto de dados para um intervalo específico. Essa transformação visa alcançar uma representação mais consistente dos dados para seu modelo de aprendizado de máquina. O processo de escalonamento é muito crucial por várias razões:

Melhora o Desempenho do Modelo de Aprendizado de Máquina: Muitos algoritmos de aprendizado de máquina, especialmente os baseados em distância, como K-Nearest Neighbors (KNN) e Support Vector Machines (SVMs), dependem do cálculo de distâncias entre os pontos de dados. Se as características tiverem escalas muito diferentes (por exemplo, uma característica em milhares e outra em décimos), as características com escalas maiores dominarão os cálculos de distância, levando a um desempenho subótimo. Escalar coloca todas as características em um intervalo semelhante, permitindo que o modelo se concentre nas reais relações entre os pontos de dados.

Convergência Mais Rápida no Treinamento: Algoritmos de otimização baseados em descida de gradiente, comumente usados em redes neurais e outros modelos, tomam medidas em direção à solução ótima com base nos gradientes da função de perda. Quando as características têm escalas diferentes, os gradientes também podem ter magnitudes muito diferentes, dificultando que o otimizador encontre o mínimo de forma eficiente. A escalonamento ajuda os gradientes a terem uma faixa mais consistente, levando a uma convergência mais rápida.

Garante a Estabilidade das Operações Numéricas: Alguns algoritmos de aprendizado de máquina envolvem cálculos que podem se tornar instáveis com características de escalas significativamente diferentes. O escalonamento ajuda a prevenir esses problemas numéricos e garante que o modelo realize os cálculos com precisão.


Técnicas Comuns de Escalonamento:

  • Normalização (Escalonamento Min-Max): Essa técnica escala as características para um intervalo específico (geralmente de 0 a 1 ou de -1 a 1).
  • Padronização (normalização Z-score): Essa técnica centraliza os dados subtraindo a média de cada característica e, em seguida, escalonando-os ao dividir pelo desvio padrão.

Por mais crucial que seja esse processo de normalização, poucas fontes online explicam a maneira correta de realizá-lo. A mesma técnica de escalonamento e seus parâmetros usados para os dados de treinamento devem ser aplicados aos dados de teste e durante a implantação do modelo.

Usando a mesma analogia do escalonador: Imagine que você tenha uma característica representando "renda" nos seus dados de treinamento. O escalonador aprende os valores mínimo e máximo de renda (ou a média e o desvio padrão para padronização) durante o treinamento. Se você usar um escalonador diferente nos dados de teste, ele pode encontrar valores de renda fora da faixa vista durante o treinamento. Isso pode levar a um escalonamento inesperado e introduzir inconsistências entre os dados de treinamento e teste.

Usando a mesma analogia de parâmetros para o escalonador: Imagine uma régua usada para medir altura. Se você usar uma régua diferente marcada com unidades diferentes (polegadas vs. centímetros) para treinamento e teste, suas medições não seriam comparáveis. Da mesma forma, usar escalonadores diferentes nos dados de treinamento e teste interrompe a referência que o modelo aprendeu durante o treinamento.

Em essência, usar o mesmo escalonador garante que o modelo veja os dados de forma consistente tanto no treinamento quanto no teste, levando a resultados mais confiáveis e interpretáveis.

Você pode usar as técnicas de escalonamento do módulo Scikit-learn.preprocessing do Python. Tudo ficará bem, desde que você esteja construindo o modelo ONNX e o implantando na mesma linguagem 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)

No entanto, as coisas se tornam desafiadoras quando você quer usar o modelo treinado na linguagem MQL5. Apesar de existirem várias maneiras de salvar o escalonador em Python, será um desafio extraí-lo no MetaEditor, já que o Python tem suas maneiras próprias de armazenar objetos e facilitar o processo, ao contrário de outras linguagens de programação. A melhor coisa a fazer seria pré-processar os dados no MQL5, salvar o escalonador e salvar os dados escalonados em um arquivo CSV que iremos ler usando o código Python.

Abaixo está o roteiro para o pré-processamento dos dados:

  1. Coletar os dados do mercado & escalá-los
  2. Salvar o escalonador 
  3. Salvar os dados escalonados em um arquivo CSV


01: Coletando dados do mercado & escalando-os

Vamos coletar as taxas de Abertura, Máxima, Mínima e Fechamento de 1000 barras de um gráfico diário, e então criar um problema de reconhecimento de padrões atribuindo o padrão de alta sempre que o preço fechar acima da abertura e um sinal de baixa caso contrário. Ao treinar o modelo de IA LSTM nesse padrão, estamos tentando fazer com que ele entenda o que contribui para esses padrões para que, uma vez bem treinado, ele possa nos fornecer o sinal de negociação.

Dentro do script de coleta de dados ONNX:

Vamos começar incluindo as bibliotecas necessárias:

#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

Em seguida, precisamos coletar as informações de preços.

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
     }

//---
  }

Lembre-se! O escalonamento é para as variáveis independentes, por isso separamos a matriz de dados em matriz X e vetor Y, respectivamente, para obter a matriz X, que podemos escalonar nas colunas.

   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: Salvando o Escalonador

Como mencionado anteriormente, precisamos salvar o escalonador para uso futuro.

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

Após a execução deste trecho de código, será criada uma pasta com arquivos binários. Esses dois arquivos contêm os parâmetros do escalonador de padronização. Veremos mais tarde como usar esses parâmetros para carregar a instância do escalonador salvo.

eurusd scaler


03: Salvando os dados escalonados em um arquivo CSV

Por último, mas não menos importante, precisamos salvar os dados escalonados em um arquivo CSV que possamos usar posteriormente no 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:



Superando Desafios de Dados de Séries Temporais

Há alguns estudos sugerindo que modelos de aprendizado profundo para séries temporais, como GRU, LSTM e RNN, são melhores para fazer previsões no mercado de ações em comparação com outros modelos, devido à sua capacidade de entender padrões ao longo de um certo período de tempo. A maioria dos traders algorítmicos na comunidade de ciência de dados está sintonizada com esses modelos, inclusive eu.

Acontece que pode ser necessário escrever algumas linhas adicionais de código para preparar os dados adequados para previsões de séries temporais usando esses modelos.

Se você já trabalhou com modelos de séries temporais, provavelmente já viu uma função ou código semelhante a este:

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)

Essa função é muito importante para os modelos de séries temporais, como o LSTM, pois prepara os dados ao:

  • Dividir os dados em sequências de um tamanho fixo (time_step).
  • Separar características (informação passada) dos alvos (valor previsto).
  • Reformatar os dados em um formato adequado para modelos LSTM.

Essa preparação de dados ajuda a fornecer ao modelo LSTM as informações mais relevantes de maneira estruturada, resultando em um treinamento mais rápido, melhor gerenciamento de memória e, potencialmente, maior precisão nas previsões.

Embora os LSTMs possam lidar com sequências, dados em tempo real introduzem um fluxo contínuo de informações. Ainda é necessário definir uma janela de tempo de dados passados para o modelo considerar ao fazer previsões. Isso torna essa função necessária não apenas para o treinamento e testes, mas também para previsões em tempo real. Não precisaremos dos vetores Y, mas precisamos ter o código para reformatar a matriz X. Faremos previsões em tempo real no MetaTrader 5, certo? Precisamos fazer uma função semelhante a esta em MQL5. 

Antes disso, vamos verificar as dimensões dos arrays X e Y retornados pela função get_sequential_data quando o valor de time_step era 7.

X_reshaped, Y_reshaped = get_sequential_data(dataset, step_size)

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

Saídas:

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

O array X retornado é um array 3D, ou seja, um Tensor, enquanto os dados Y retornados são uma matriz 1D, ou seja, um vetor. Precisamos considerar isso ao fazer uma função semelhante em MQL5.

Agora vamos criar uma classe simples chamada 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 
  };

As duas funções com nomes semelhantes extract_timeseries_data realizam trabalhos semelhantes, exceto que uma delas não retorna o vetor Y, pois será usada para previsões em tempo 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;
 }

Agora, dentro de um Expert Advisor chamado ONNX challenges EA, vamos tentar usar essas funções para extrair os dados de séries temporais:

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

Saídas:

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

Ótimo, obtivemos as mesmas dimensões do código Python.

O propósito do ONNX é fazer com que um modelo de aprendizado de máquina construído em uma linguagem funcione bem, se não da mesma forma, em outra linguagem. Isso significa que, se eu construir um modelo em Python e executá-lo lá, a precisão e exatidão que ele me proporciona devem ser quase as mesmas que serão fornecidas em outra linguagem, neste caso, a linguagem MQL5, quando os mesmos dados forem usados sem conversão.

Se este for o caso, antes de usar o modelo ONNX no MQL5, você deve verificar se tudo está correto testando o modelo com os mesmos dados em ambas as plataformas para ver se ele fornece a mesma precisão. Vamos testar esse modelo.

Eu criei o modelo LSTM com 10 neurônios na camada de entrada e uma única camada oculta na rede, atribuí o otimizador Adam ao progresso de aprendizado.

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

Saídas:

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

Treinei o modelo por 100 épocas com paciência definida para 5 épocas e 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)

O modelo LSTM convergiu na 77ª época com uma perda de 0.3000 e uma precisão 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 perda versus iterações


Por fim, testei o modelo em todo o conjunto de dados;

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

Abaixo está o resultado:

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

LSTM model accuracy:  0.9179507704622774

Precisamos esperar esse valor de precisão ou algo próximo disso ao usar este modelo LSTM que

foi salvo em ONNX no MQL5. inp_model_name foi 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}")

Vamos incluir este modelo dentro do nosso Expert Advisor.

#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 da biblioteca onnx.mqh, não há nada além de uma classe ONNX que inicializa o modelo ONNX e possui funções para fazer previsões.

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

Finalmente, eu executei um modelo LSTM carregado dentro do 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);
  }

Abaixo está o resultado:

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

Ótimo! Obtivemos o mesmo valor de precisão que obtivemos no código Python com precisão de casas decimais significativas. Isso nos diz que fizemos tudo da maneira correta.

Agora vamos usar este modelo para fazer previsões em tempo real antes de prosseguirmos:

Dentro do ONNX challenges REALTIME EA;

Como faremos previsões em conjuntos de dados em tempo real, diferente do que fizemos anteriormente, onde usamos o arquivo CSV contendo dados normalizados para teste, desta vez precisamos carregar o scaler salvo uma vez e aplicá-lo aos novos dados sempre antes de alimentar os dados para o nosso modelo LSTM no 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[];

Logo após carregar o modelo ONNX como um recurso, precisamos incluir os arquivos binários de média e desvio padrão que salvamos.

Desta vez, chamamos o scaler de padronização com um ponteiro, pois o instanciamos com os valores do scaler salvo.

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

Aqui está como normalizar cada novo dado 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, executei o EA no testador de estratégia sem erros, as previsões foram exibidas com sucesso no gráfico.

Sinais LSTM no gráfico


Superando o Desafio da Redução de Dimensão

Como dito anteriormente, na resolução de problemas da vida real usando modelos de aprendizado de máquina, é necessário mais do que apenas um código de modelo de IA para realizar a tarefa. Uma das ferramentas úteis que cientistas de dados costumam carregar em sua caixa de ferramentas são os algoritmos de redução de dimensão, como PCA, LDA, NMF, SVD Truncado e muitos outros. Apesar de terem suas desvantagens, os algoritmos de redução de dimensionalidade ainda têm benefícios, incluindo: 


Benefícios da Redução de Dimensionalidade:

Melhor Desempenho do Modelo: Dados de alta dimensionalidade podem levar à "maldição da dimensionalidade", onde os modelos têm dificuldade em aprender efetivamente devido ao vasto espaço de recursos. PCA reduz a complexidade e pode melhorar o desempenho de vários algoritmos de aprendizado de máquina, incluindo classificação, regressão e agrupamento.

Treinamento e Processamento Mais Rápidos: Treinar modelos de aprendizado de máquina em dados de alta dimensionalidade pode ser computacionalmente caro. PCA reduz o número de recursos, levando a tempos de treinamento mais rápidos e potencialmente menores requisitos de recursos computacionais.

Redução de Overfitting: Alta dimensionalidade pode aumentar o risco de overfitting, onde o modelo memoriza os dados de treinamento, mas falha em generalizar bem para dados não vistos. PCA ajuda a mitigar esse risco ao focar nos recursos mais informativos.

Assim como as técnicas de escalonamento, é interessante usar uma técnica de redução de dimensão como a Análise de Componentes Principais (PCA) oferecida pelo Scikit-Learn. No entanto, você terá dificuldade em encontrar maneiras de usar essa PCA no MQL5, onde a maior parte do trabalho é feita, incluindo a negociação com base em tudo o que você construiu. 

Dentro do script de coleta de dados ONNX temos que adicionar o PCA.

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

CPCA *pca;

Queremos adicionar a técnica PCA para normalizar as variáveis x antes que o processo de normalização ocorra.

   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

Isso criará uma subpasta na pasta MQL5\Files. Esta pasta consistirá em arquivos binários com informações para o PCA.

Arquivos PCA onnx

novo conjunto de dados CSV com PCA agora tem duas variáveis independentes, conforme instruído no construtor PCA para criar dois componentes a partir dos dados originais.

Conjunto de dados PCA

Para evitar confusão, podemos criar uma condição booleana para verificar se a condição para o PCA é permitida pelo usuário, já que salvar os dados do PCA em um arquivo CSV pode ser diferente. Além disso, podemos precisar alterar o nome do arquivo CSV e incluir PCA em seu nome para que possamos identificar a diferença entre os arquivos CSV de conjuntos de dados.

Dentro do script de coleta de dados 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;
  }

Também precisamos fazer mudanças semelhantes no EA principal chamado 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);

  }

Notou as mudanças? Existem dois modelos incluídos no Expert Advisor, um modelo LSTM foi treinado em um conjunto de dados regular e o outro, com a palavra PCA no nome, foi treinado nos dados aplicados com PCA. Como os dados processados com PCA podem ter dimensões diferentes em comparação com os dados que não passaram pelo PCA, que sempre terão dimensões semelhantes aos dados originais, essa diferença torna importante também ter escaladores diferentes para cada modelo.

Agora que criamos espaço para um novo modelo com PCA, vamos voltar ao nosso script Python e fazer algumas alterações. Apenas algumas mudanças a serem feitas, o nome do arquivo CSV e o nome final do arquivo ONNX:

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

Desta vez, o modelo convergiu na 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

Convergiu com uma precisão razoável de 52,48%, algo que normalmente acontece, mas longe dos 89% que obtivemos sem PCA. Agora vamos criar uma estratégia simples onde podemos abrir negociações com base nos sinais fornecidos:

A lógica de negociação é simples. Verifique se não há posição aberta na direção e abra uma nessa direção, mantendo o controle da mudança de sinal. Se houver um novo sinal, feche uma posição desse tipo e a posição na direção oposta.

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

Eu executei testes no modelo de Preços de Abertura no período de 12 horas, já que o período diário gera muitos erros de Mercado fechado. Abaixo estão os resultados quando o modelo LSTM foi aplicado com PCA:

PCA lstm strategy tester

Sem PCA:

lstm no PCA tester



Considerações Finais

ONNX é uma ótima ferramenta, mas precisamos começar a pensar fora da caixa ao usá-la. Ao nos dar a capacidade de compartilhar código de aprendizado de máquina entre diferentes plataformas, ela nos poupa muito trabalho e dores de cabeça que podem ser causadas quando decidimos implementar esses sofisticados modelos de aprendizado profundo e IA na linguagem MQL5. No entanto, ainda é necessário fazer algum trabalho para acabar com um programa confiável e funcional.

Até mais.

Para mais informações sobre todos os arquivos incluídos nesta postagem e mais, confira este repositório GitHub.

Anexos: 

Arquivo Descrição|Uso
MatrixExtend.mqh
Contém funções adicionais para manipulação de matrizes.
metrics.mqh
Contém funções e código para medir o desempenho de modelos de aprendizado de máquina.
preprocessing.mqh
Biblioteca para pré-processamento de dados de entrada brutos para torná-los adequados para uso em modelos de aprendizado de máquina.
plots.mqh
Biblioteca para plotar vetores e matrizes.
Timeseries Deep Learning\onnx.mqh
Esta biblioteca consiste na classe ONNX, responsável por ler arquivos .onnx e usar os arquivos carregados para fazer previsões.
Tensors.mqh
Uma biblioteca contendo Tensores, objetos de matrizes algébricas 3D programados em linguagem MQL5 pura
Timeseries Deep Learning\tsdataprocessor.mqh
Uma biblioteca com uma classe contendo funções para converter dados brutos em dados adequados para previsões de séries temporais.
Dimensionality Reduction\base.mqh
Um arquivo contendo funções necessárias para tarefas de redução de dimensão. 
Dimensionality Reduction\PCA.mqh
Biblioteca de Análise de Componentes Principais (PCA).
Python\onnx_timeseries.ipynb  Um notebook Jupyter contendo todo o código Python usado nesta postagem. 
Python\requirements.txt  Um arquivo de texto com todas as dependências necessárias para o código Python rodar.


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/14703

Arquivos anexados |
Code_7_Files.zip (81.91 KB)
Redes neurais de maneira fácil (Parte 88): Codificador denso de séries temporais (TiDE) Redes neurais de maneira fácil (Parte 88): Codificador denso de séries temporais (TiDE)
O desejo de obter previsões mais precisas leva os pesquisadores a complicar os modelos de previsão. Isso, por sua vez, aumenta os custos de treinamento e manutenção do modelo. Mas será que isso sempre é justificado? Neste artigo, proponho que você conheça um algoritmo que utiliza a simplicidade e a velocidade dos modelos lineares, e demonstra resultados no nível dos melhores com uma arquitetura mais complexa.
Desenvolvendo um sistema de Replay (Parte 65): Dando play no serviço (VI) Desenvolvendo um sistema de Replay (Parte 65): Dando play no serviço (VI)
Aqui neste artigo mostrarei como faremos para conseguir implementar o avanço rápido, assim como também resolveremos o problema do indicador de mouse, quando este está sendo usando junto com a aplicação de replay / simulação. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Como adicionar Trailing Stop com o indicador Parabolic SAR Como adicionar Trailing Stop com o indicador Parabolic SAR
Ao criar uma estratégia de negociação, precisamos testar diversas opções de stops de proteção. Aqui, surge a ideia de ajustar dinamicamente o nível do Stop Loss acompanhando o movimento do preço. O melhor candidato para isso é o indicador Parabolic SAR, porque é difícil pensar em algo mais simples e claro.