English Русский Español Deutsch 日本語
preview
Ciência de Dados e Aprendizado de Máquina (Parte 15): SVM — uma ferramenta útil no arsenal do trader

Ciência de Dados e Aprendizado de Máquina (Parte 15): SVM — uma ferramenta útil no arsenal do trader

MetaTrader 5Sistemas de negociação | 24 abril 2024, 13:05
109 0
Omega J Msigwa
Omega J Msigwa

Conteúdo:


    Introdução

    As máquinas de vetores de suporte (Support Vector Machine, SVM) são uma forma de aprendizado supervisionado usada em tarefas de classificação linear e não linear e regressão, e às vezes para detecção de valores atípicos.

    Ao contrário dos métodos de classificação bayesiana e regressão logística, que usam modelos matemáticos simples para classificação de informações, SVM utiliza funções matemáticas sofisticadas de treinamento voltadas para encontrar o hiperplano ótimo que separa os dados em um espaço N-dimensional.

    O algoritmo SVM é frequentemente usado para tarefas de classificação. Essas são as tarefas que abordaremos neste artigo.


    O que é um hiperplano?

    Um hiperplano é uma linha usada para separar pontos de dados de diferentes classes. 

    Hiperplano SVM (fonte da imagem: wikipedia.com)

    O hiperplano tem as seguintes propriedades:

    Dimensão. Em tarefas de classificação binária, o hiperplano é um subespaço (d-1)-dimensional, onde d é a dimensão do espaço de características. Por exemplo, em um espaço bidimensional de características, o hiperplano é uma linha unidimensional.

    Matematicamente, o hiperplano pode ser representado por uma equação linear do tipo:


    — vetor ortogonal ao hiperplano e que determina sua orientação.

    — vetor de características.

    b — termo escalar de deslocamento, que move o hiperplano a partir da origem.

    Divisão. O hiperplano divide o espaço de características em dois semiespaços:

    Área onde corresponde a uma classe.

    Área onde corresponde a outra classe.

    Margem. No SVM, o objetivo é encontrar um hiperplano que maximize o espaço entre o hiperplano e os pontos de dados mais próximos de qualquer classe. Esses pontos de dados mais próximos são chamados de "vetores de suporte". O SVM busca encontrar um hiperplano que forneça o maior espaço possível enquanto minimiza o erro de classificação.

    Classificação. O hiperplano ideal encontrado pode ser usado para classificar novos pontos de dados. Calculando , pode-se determinar de que lado do hiperplano o ponto de dados está, e assim atribuí-lo a uma das duas classes.

    O conceito de hiperplano é um elemento chave das máquinas de vetores de suporte, pois forma a base do classificador de margem máxima. O objetivo do método de vetores de suporte é encontrar o hiperplano que melhor separa os dados, mantendo ao mesmo tempo a maior margem possível entre as classes, o que, por sua vez, aumenta a generalização do modelo e sua resistência a dados não vistos.

    double CLinearSVM::hyperplane(vector &x)
     {
       return x.MatMul(W) - B;   
     }
    
    As equações e são equivalentes e descrevem o mesmo hiperplano. A escolha específica é uma questão de preferência pessoal, mas ambas as variantes são algebricamente equivalentes. Em meu código, não transpus a matriz x, pois decidi usar W como um vetor, então não havia necessidade.

    Como mencionado anteriormente, o termo de deslocamento, denotado por b, é um termo escalar, então tive que declarar uma variável dupla.

    class CLinearSVM
      {
       protected:
       
          CMatrixutils      matrix_utils;
          CMetrics          metrics;
          
          CPreprocessing<vector, matrix> *normalize_x;
          
          vector            W; //Weights vector 
          double            B; //bias term
          
          bool is_fitted_already;
          
          struct svm_config 
            {
              uint batch_size;
              double alpha;
              double lambda;
              uint epochs;
            };
    
       private:
          svm_config config;
       
    

    Aqui, usamos a classe CLinearSVM. Neste artigo, vamos considerar duas versões do método de vetores de suporte — linear e dual.


    Método linear de vetores de suporte

    O SVM linear é um tipo de vetor de suporte que utiliza um kernel linear, o que significa que ele usa uma fronteira de decisão linear para separar os pontos de dados. No SVM linear, você trabalha diretamente com o espaço de características, e o problema de otimização é frequentemente expresso em sua forma mais simples. O objetivo principal do SVM linear é encontrar o hiperplano linear que melhor separa os dados.

    Esse tipo é mais adequado para dados linearmente separáveis.


    Método dual de vetores de suporte

    A forma dual não é um tipo separado do método de vetores de suporte, mas sim uma representação do problema de otimização do SVM. A forma dual do SVM é uma reformulação matemática da tarefa de otimização original, que permite o uso de métodos de resolução mais eficientes. Os multiplicadores de Lagrange são introduzidos na fórmula para maximizar a função objetivo dual, o que é equivalente à tarefa principal. A solução do problema dual leva à identificação dos vetores de suporte, que são cruciais para a classificação.

    Esse tipo é melhor para dados que não são linearmente separáveis.

    Questão linear e não linear

    Além disso, é possível usar margens rígidas ou suaves para tomar decisões sobre o classificador SVM usando um hiperplano.


    Margem rígida

    Se os dados de treinamento são linearmente separáveis, é possível escolher dois hiperplanos paralelos que separam as duas classes de dados de modo que a distância entre eles seja a maior possível. A área limitada por esses dois hiperplanos é chamada de margem, e o hiperplano com a maior margem é o que fica no meio entre eles. Com um conjunto de dados normalizado ou padronizado, esses hiperplanos podem ser descritos pelas equações

    (tudo que está nessa fronteira ou acima pertence a uma classe com o rótulo 1)

    e

    (tudo que está nesta fronteira ou abaixo pertence a outra classe com o rótulo -1).

    A distância entre eles é igual a 2/||w||, e para maximizar a distância, ||w|| deve ser minimizado. Para evitar que qualquer ponto de dados fique dentro da margem, adicionamos a restrição: yi(wTXi -b) >= 1, onde yi = i-ésima linha do objetivo, e Xi = i-ésima linha em X.


    Margem suave

    Para usar o SVM em casos onde os dados não são linearmente separáveis, a função de perda de articulação foi introduzida.

    .

    Aqui, é o i-ésimo alvo (ou seja, neste caso 1 ou -1), e é o i-ésimo resultado.

    Se o ponto de dados tem classe = 1, então a perda será 0, caso contrário, será a distância entre a fronteira e o ponto de dados. Nosso objetivo é minimizar

    onde λ é um compromisso entre o tamanho da margem e xi estar no lado correto desta margem. Se o valor de λ for muito baixo, a equação se torna uma margem rígida.

    Usaremos margem rígida para a classe Linear SVM. Isso é possível graças à função de sinal, que retorna o sinal de um número real em notação matemática. Expressa como:


    int CLinearSVM::sign(double var)
     {   
       if (var == 0)
        return (0);
       else if (var < 0)
        return -1;
       else 
        return 1; 
     }
    


    Treinamento do modelo do método linear de vetores de suporte

    O treinamento do método SVM envolve a busca por um hiperplano ótimo que separe os dados enquanto maximiza o aumento da margem. A margem, ou o espaçamento, é a distância entre o hiperplano e os pontos de dados mais próximos de qualquer classe. O objetivo é encontrar um hiperplano que maximize a margem enquanto minimiza os erros de classificação.

    Atualização de pesos (w):

    a. Primeiro termo. O primeiro termo da função de perda corresponde à perda de articulação, que mede o erro de classificação. Para cada exemplo de treinamento i, calculamos a derivada da função de perda em relação aos pesos w:

    • Se , isso significa que o ponto de dados está corretamente classificado e fora da margem, a derivada é zero.
    • Se , isso indica que o ponto de dados está dentro da margem ou classificado incorretamente, a derivada é .

    b. Segundo termo:

    O segundo termo é a regularização. Isso fornece uma pequena margem e ajuda a prevenir o sobreajuste. A derivada deste termo em relação aos pesos w é 2λw, onde λ é o parâmetro de regularização.

    c. Combinamos as derivadas do primeiro e segundo termos,

    Atualizamos os pesos w:

    -Quando os pesos são atualizados assim: , e se , atualizamos assim: . Aqui α é o coeficiente de aprendizado.

    Atualização do ponto de interseção (b):

    a. Primeiro termo:

    A derivada da função de perda de articulação em relação ao ponto de interseção b é calculada de maneira semelhante aos pesos:

    • Se , a derivada é zero.
    • Se , a derivada é igual a .

    b. Segundo termo:

    O segundo termo não depende do ponto de interseção, então sua derivada em relação a b é zero. c. Atualizamos o ponto de interseção b:

    • Se , atualizamos assim:
    • Se , atualizamos assim:

    Variável de desvio (ξ):

    A variável de desvio (ξ) permite que alguns pontos de dados estejam dentro da margem, o que significa que eles estão mal classificados ou dentro dessa margem. A condição significa que a fronteira de decisão deve estar pelo menos unidades afastada do ponto de dados i.

    Assim, o processo de treinamento do SVM inclui a atualização de pesos e interseções com base na perda de articulação e no termo de regularização. O objetivo é encontrar o hiperplano ótimo que maximize a margem, considerando os possíveis erros de classificação dentro do limite permitidos pela variável de desvio. Esse processo geralmente é resolvido usando métodos de otimização. Neste processo, os vetores de suporte são definidos durante o treinamento. Eles têm como objetivo definir a fronteira de decisão.

    void CLinearSVM::fit(matrix &x, vector &y)
     {
       matrix X = x;
       vector Y = y;
      
       ulong rows = X.Rows(),
             cols = X.Cols();
       
       if (X.Rows() != Y.Size())
          {
             Print("Support vector machine Failed | FATAL | X m_rows not same as yvector size");
             return;
          }
       
       W.Resize(cols);
       B = 0;
        
       normalize_x = new CPreprocessing<vector, matrix>(X, NORM_STANDARDIZATION); //Normalizing independent variables
         
    //---
    
      if (rows < config.batch_size)
        {
          Print("The number of samples/rows in the dataset should be less than the batch size");
          return;
        }
       
        matrix temp_x;
        vector temp_y;
        matrix w, b;
        
        vector preds = {};
        vector loss(config.epochs);
        during_training = true;
    
        for (uint epoch=0; epoch<config.epochs; epoch++)
          {
            
             for (uint batch=0; batch<=(uint)MathFloor(rows/config.batch_size); batch+=config.batch_size)
               {              
                  temp_x = matrix_utils.Get(X, batch, (config.batch_size+batch)-1);
                  temp_y = matrix_utils.Get(Y, batch, (config.batch_size+batch)-1);
                  
                  #ifdef DEBUG_MODE:
                      Print("X\n",temp_x,"\ny\n",temp_y);
                  #endif 
                  
                   for (uint sample=0; sample<temp_x.Rows(); sample++)
                      {                                        
                         // yixiw-b≥1
                         
                          if (temp_y[sample] * hyperplane(temp_x.Row(sample))  >= 1) 
                            {
                              this.W -= config.alpha * (2 * config.lambda * this.W); // w = w + α* (2λw - yixi)
                            }
                          else
                             {
                               this.W -= config.alpha * (2 * config.lambda * this.W - ( temp_x.Row(sample) * temp_y[sample] )); // w = w + α* (2λw - yixi)
                               
                               this.B -= config.alpha * temp_y[sample]; // b = b - α* (yi)
                             }  
                      }
               }
            
            //--- Print the loss at the end of an epoch
           
             is_fitted_already = true;  
             
             preds = this.predict(X);
             
             loss[epoch] = preds.Loss(Y, LOSS_BCE);
            
             printf("---> epoch [%d/%d] Loss = %f Accuracy = %f",epoch+1,config.epochs,loss[epoch],metrics.confusion_matrix(Y, preds, false));
             
            #ifdef DEBUG_MODE:  
              Print("W\n",W," B = ",B);  
            #endif   
          }
        
        during_training = false;
        
        return;
     }
    


    Previsões obtidas do modelo de vetor de suporte linear

    Para obter previsões do nosso modelo, os dados são passados para a função sinal depois que o hiperplano for estabelecido.

    int CLinearSVM::predict(vector &x)
     { 
       if (!is_fitted_already)
         {
           Print("Err | The model is not trained, call the fit method to train the model before you can use it");
           return 1000;
         }
       
       vector temp_x = x;
       if (!during_training)
         normalize_x.Normalization(temp_x); //Normalize a new input data when we are not running the model in training 
         
       return sign(hyperplane(temp_x));
     }
    


    Treinamento e teste do modelo de vetor de suporte linear

    É necessário testar o modelo antes de implementá-lo para fazer quaisquer previsões significativas sobre os dados de mercado. Começamos com a inicialização de uma instância da classe Linear SVM.

    #include <MALE5\Support Vector Machine(SVM)\svm.mqh>
    CLinearSVM *svm;
    
    input uint bars = 1000;
    input uint epochs_ = 1000;
    input uint batch_size_ = 64;
    input double alpha__ =0.1;
    input double lambda_ = 0.01;
    
    bool train_once;
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //---          
        svm = new CLinearSVM(batch_size_, alpha__, epochs_, lambda_);
        train_once = false;
    //---
    
       return(INIT_SUCCEEDED);
      }
    

    Continuamos com a coleta de dados. Usaremos 4 variáveis independentes: RSI, HIGH BANDS BOLLINGER, LOW e MID.

    vec_.CopyIndicatorBuffer(rsi_handle, 0, 0, bars);
    dataset.Col(vec_, 0);
    vec_.CopyIndicatorBuffer(bb_handle, 0, 0, bars);
    dataset.Col(vec_, 1);
    vec_.CopyIndicatorBuffer(bb_handle, 1, 0, bars);
    dataset.Col(vec_, 2);
    vec_.CopyIndicatorBuffer(bb_handle, 2, 0, bars);
    dataset.Col(vec_, 3);
    
    open.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, 0, bars);
    close.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, 0, bars);
          
    for (ulong i=0; i<vec_.Size(); i++) //preparing the independent variable
       dataset[i][4] = close[i] > open[i] ? 1 : -1; // if price closed above its opening thats bullish else bearish
    

    Concluímos a coleta de dados, dividindo os dados em conjuntos de treinamento e teste.

    matrix_utils.TrainTestSplitMatrices(dataset,train_x,train_y,test_x,test_y,0.7,42); //split the data into training and testing samples


    Treinamento/ajuste do modelo

    svm.fit(train_x, train_y);

    Resultado

            0       15:15:42.394    svm test (EURUSD,H1)    ---> epoch [1/1000] Loss = 7.539322 Accuracy = 0.489000
    IK      0       15:15:42.395    svm test (EURUSD,H1)    ---> epoch [2/1000] Loss = 7.499849 Accuracy = 0.491000
    EG      0       15:15:42.395    svm test (EURUSD,H1)    ---> epoch [3/1000] Loss = 7.499849 Accuracy = 0.494000
    ....
    ....
    GG      0       15:15:42.537    svm test (EURUSD,H1)    ---> epoch [998/1000] Loss = 6.907756 Accuracy = 0.523000
    DS      0       15:15:42.537    svm test (EURUSD,H1)    ---> epoch [999/1000] Loss = 7.006438 Accuracy = 0.521000
    IM      0       15:15:42.537    svm test (EURUSD,H1)    ---> epoch [1000/1000] Loss = 6.769601 Accuracy = 0.516000
    

    Observamos a precisão do modelo tanto no treinamento quanto no teste.

    vector train_pred = svm.predict(train_x), 
        test_pred = svm.predict(test_x);
    
    printf("Train accuracy = %f",metrics.confusion_matrix(train_y, train_pred, true));
    printf("Test accuracy = %f ",metrics.confusion_matrix(test_y, test_pred, true));
    

    Resultado

    CH      0       15:15:42.538    svm test (EURUSD,H1)    Confusion Matrix
    IQ      0       15:15:42.538    svm test (EURUSD,H1)    [[171,175]
    HE      0       15:15:42.538    svm test (EURUSD,H1)     [164,190]]
    DQ      0       15:15:42.538    svm test (EURUSD,H1)    
    NO      0       15:15:42.538    svm test (EURUSD,H1)    Classification Report
    JD      0       15:15:42.538    svm test (EURUSD,H1)    
    LO      0       15:15:42.538    svm test (EURUSD,H1)    _    Precision  Recall  Specificity  F1 score  Support
    JQ      0       15:15:42.538    svm test (EURUSD,H1)    -1.0    0.51     0.49     0.54       0.50     346.0
    DH      0       15:15:42.538    svm test (EURUSD,H1)    1.0    0.52     0.54     0.49       0.53     354.0
    HL      0       15:15:42.538    svm test (EURUSD,H1)    
    FG      0       15:15:42.538    svm test (EURUSD,H1)    Accuracy                                   0.52
    PP      0       15:15:42.538    svm test (EURUSD,H1)    Average   0.52    0.52    0.52      0.52    700.0
    PS      0       15:15:42.538    svm test (EURUSD,H1)    W Avg     0.52    0.52    0.52      0.52    700.0
    FK      0       15:15:42.538    svm test (EURUSD,H1)    Train accuracy = 0.516000
    
    MS      0       15:15:42.538    svm test (EURUSD,H1)    Confusion Matrix
    LI      0       15:15:42.538    svm test (EURUSD,H1)    [[79,74]
    CM      0       15:15:42.538    svm test (EURUSD,H1)     [68,79]]
    FJ      0       15:15:42.538    svm test (EURUSD,H1)    
    HF      0       15:15:42.538    svm test (EURUSD,H1)    Classification Report
    DM      0       15:15:42.538    svm test (EURUSD,H1)    
    NH      0       15:15:42.538    svm test (EURUSD,H1)    _    Precision  Recall  Specificity  F1 score  Support
    NN      0       15:15:42.538    svm test (EURUSD,H1)    -1.0    0.54     0.52     0.54       0.53     153.0
    PQ      0       15:15:42.538    svm test (EURUSD,H1)    1.0    0.52     0.54     0.52       0.53     147.0
    JE      0       15:15:42.538    svm test (EURUSD,H1)    
    GP      0       15:15:42.538    svm test (EURUSD,H1)    Accuracy                                   0.53
    RI      0       15:15:42.538    svm test (EURUSD,H1)    Average   0.53    0.53    0.53      0.53    300.0
    JH      0       15:15:42.538    svm test (EURUSD,H1)    W Avg     0.53    0.53    0.53      0.53    300.0
    DO      0       15:15:42.538    svm test (EURUSD,H1)    Test accuracy = 0.527000 
    

    O modelo mostrou uma precisão de 53% em previsões fora da amostra. Alguns podem dizer que é um modelo ruim, mas eu diria que é regular. Esses resultados podem estar relacionados a muitos fatores, incluindo erros no modelo, má normalização, critérios de convergência e muitos outros. Você pode experimentar com os parâmetros e tentar melhorar o resultado. No entanto, é mais provável que os dados sejam muito complexos para um modelo linear. Estou quase certo disso, por isso vamos tentar o método dual SVM e ver se ele mostra melhores resultados.

    Estudaremos o método SVM duplo no ONXX Python. Não consegui obter um modelo em MQL5 que pudesse se aproximar do desempenho e precisão do modelo sklearn python. É por isso que continuaremos trabalhando no método dual SVM em Python. No entanto, incluí a biblioteca Dual SVM em MQL5 no arquivo principal svm.mqh — ele está anexado a este artigo e também disponível no meu GitHub, cujo link está no final deste artigo.

    Para executar o método dual SVM em Python, precisamos coletar dados e normalizá-los usando MQL5. Vamos precisar criar uma nova classe chamada CDualSVMONNX dentro do arquivo svm.mqh. Esta classe será responsável por trabalhar com o modelo ONNX obtido do Python.

    class CDualSVMONNX
      {
    private:
          CPreprocessing<vectorf, matrixf> *normalize_x;
          CMatrixutils matrix_utils;
          
          struct data_struct
           {
             ulong rows,
                   cols;
           } df;
          
    public:  
                         CDualSVMONNX(void);
                        ~CDualSVMONNX(void);
          
                         long onnx_handle;              
                         
                         void SendDataToONNX(matrixf &data, string csv_name = "DualSVMONNX-data.csv", string csv_header="");
                         bool LoadONNX(const uchar &onnx_buff[], ENUM_ONNX_FLAGS flags=ONNX_NO_CONVERSION);
                         int Predict(vectorf &inputs);
                         vector Predict(matrixf &inputs);
      };
    

    Aqui está uma visão geral da classe. 


    Coleta e normalização de dados

    Precisamos de dados para o nosso modelo aprender. Esses dados precisam ser limpos para que sejam adequados para o nosso modelo SVM:

    void CDualSVMONNX::SendDataToONNX(matrixf &data, string csv_name = "DualSVMONNX-data.csv", string csv_header="")
     {
        df.cols = data.Cols();
        df.rows = data.Rows();
        
        if (df.cols == 0 || df.rows == 0)
          {
             Print(__FUNCTION__," data matrix invalid size ");
             return;
          }
        
        matrixf split_x;
        vectorf  split_y;
        
        matrix_utils.XandYSplitMatrices(data, split_x, split_y); //since we are going to be normalizing the independent variable only we need to split the data into two
        
        normalize_x = new CPreprocessing<vectorf,matrixf>(split_x, NORM_MIN_MAX_SCALER); //Normalizing Independent variable only
        
        
        matrixf new_data = split_x;
        new_data.Resize(data.Rows(), data.Cols());
        new_data.Col(split_y, data.Cols()-1);
        
        if (csv_header == "")
          {
             for (ulong i=0; i<df.cols; i++)
               csv_header += "COLUMN "+string(i+1) + (i==df.cols-1 ? "" : ","); //do not put delimiter on the last column
          }
        
    //--- Save the Normalization parameters also
        
       matrixf params = {};
        
       string sep=",";
       ushort u_sep;
       string result[];
       
       u_sep=StringGetCharacter(sep,0); 
       int k=StringSplit(csv_header,u_sep,result); 
       
       ArrayRemove(result, k-1, 1); //remove the last column header since we do not have normalization parameters for the target variable  as it is not normalized
        
        normalize_x.min_max_scaler.min.Swap(params);
        matrix_utils.WriteCsv("min_max_scaler.min.csv",params,result,false,8);
        normalize_x.min_max_scaler.max.Swap(params);
        matrix_utils.WriteCsv("min_max_scaler.max.csv",params,result,false,8); 
        
    //--- 
        
        matrix_utils.WriteCsv(csv_name, new_data, csv_header, false, 8); //Save dataset to a csv file 
     }
    

    Como a coleta de dados para treinamento precisa ser feita uma vez, usaremos um script para isso.

    Script GetDataforONNX.mq5

    #include <MALE5\Support Vector Machine(SVM)\svm.mqh>
    
    CDualSVMONNX dual_svm;
    
    input uint bars = 1000;
    input uint epochs_ = 1000;
    input uint batch_size_ = 64;
    input double alpha__ =0.1;
    input double lambda_ = 0.01;
    
    input int rsi_period = 13;
    input int bb_period = 20;
    input double bb_deviation = 2.0;
    
    int rsi_handle, 
        bb_handle;
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
      
        rsi_handle = iRSI(Symbol(),PERIOD_CURRENT,rsi_period, PRICE_CLOSE);
        bb_handle = iBands(Symbol(), PERIOD_CURRENT, bb_period,0, bb_deviation, PRICE_CLOSE);
        
    //---
    
        matrixf data = GetTrainTestData<float>();
        dual_svm.SendDataToONNX(data,"DualSVMONNX-data.csv","rsi,bb-high,bb-low,bb-mid,target");
      }
    //+------------------------------------------------------------------+
    //|   Getting data for Training and Testing the model                |
    //+------------------------------------------------------------------+
    template <typename T>
    matrix<T> GetTrainTestData()
     {
       matrix<T> data(bars, 5);
       vector<T> v; //Temporary vector for storing Indicator buffers
        
       v.CopyIndicatorBuffer(rsi_handle, 0, 0, bars);
       data.Col(v, 0);
       v.CopyIndicatorBuffer(bb_handle, 0, 0, bars);
       data.Col(v, 1);
       v.CopyIndicatorBuffer(bb_handle, 1, 0, bars);
       data.Col(v, 2);
       v.CopyIndicatorBuffer(bb_handle, 2, 0, bars);
       data.Col(v, 3);
       
       vector open, close;
       open.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, 0, bars);
       close.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, 0, bars);
       
       for (ulong i=0; i<v.Size(); i++) //preparing the independent variable
         data[i][4] = close[i] > open[i] ? 1 : -1; // if price closed above its opening thats bullish else bearish
         
       return data;  
     }
    
    

    Resultado

    Um arquivo chamado DualSVMONNX-data.csv foi criado na pasta Files no diretório MQL5.

    Note o término da função SendDataToONNX .

    Também salvei os parâmetros de normalização.

    normalize_x.min_max_scaler.min.Swap(params);
    matrix_utils.WriteCsv("min_max_scaler.min.csv",params,result,false,8);
    normalize_x.min_max_scaler.max.Swap(params);
    matrix_utils.WriteCsv("min_max_scaler.max.csv",params,result,false,8); 
    

    Usaremos os mesmos parâmetros de normalização novamente para obter as melhores previsões do modelo. Portanto, salvar esses dados ajudará a rastrear os valores dos parâmetros de normalização. Os arquivos CSV estarão na mesma pasta que o conjunto de dados. Também salvaremos o modelo ONNX lá.


    Instância da classe DualSVMONNX. Inicialização da classe

    class DualSVMONNX:
        def __init__(self, dataset, c=1.0, kernel='rbf'):
            
            data = pd.read_csv(dataset) # reading a csv file 
            np.random.seed(42)
            
            self.X = data.drop(columns=['target']).astype(np.float32) # dropping the target column from independent variable 
            self.y = data["target"].astype(int) # storing the target variable in its own vector 
            
            self.X = self.X.to_numpy()
            self.y = self.y.to_numpy()            
    
            # Split the data into training and testing sets
            self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(self.X, self.y, test_size=0.2, random_state=42) 
    
            self.onnx_model_name = "DualSVMONNX" #our final onnx model file name for saving purposes really
            
            # Create a dual SVM model with a kernel
            
            self.svm_model = SVC(kernel=kernel, C=c)
    


    Treinamento do modelo Dual SVM em Python

        def fit(self):
            
            self.svm_model.fit(self.X_train, self.y_train) # fitting/training the model
            
            y_preds = self.svm_model.predict(self.X_train)
            
            print("accuracy = ",accuracy_score(self.y_train, y_preds))        
    

    Após o treinamento do modelo, vamos ver qual é a precisão após executar este trecho de código.

    Resultado


    Obtivemos uma precisão de 63%, o que indica que o modelo SVM para classificar este problema específico é, no melhor dos casos, mediano. No entanto, vamos realizar uma validação cruzada para entender se a precisão é a esperada:

            scores = cross_val_score(self.svm_model, self.X_train, self.y_train, cv=5)
            mean_cv_accuracy = np.mean(scores)
    
            print(f"\nscores {scores} mean_cv_accuracy {mean_cv_accuracy}")
    

    Resultado


    O que significa esse resultado de validação cruzada?

    Quando executamos o modelo com diferentes parâmetros, não há uma grande diferença nos resultados. Isso nos diz que nosso modelo está no caminho certo. A precisão média que conseguimos obter foi de 59,875, o que não está muito distante dos 63,3 que alcançamos.


    Transformação do modelo de vetor de suporte do sklearn para ONNX

        def saveONNX(self):                 
            
            initial_type = [('float_input', FloatTensorType(shape=[None, 4]))]  # None means we don't know the rows but we know the columns for sure, Remember !! we have 4 independent variables
            onnx_model = convert_sklearn(self.svm_model, initial_types=initial_type) # Convert the scikit-learn model to ONNX format
    
            onnx.save_model(onnx_model, dataset_path + f"\\{self.onnx_model_name}.onnx") #saving the onnx model
    

    O modelo foi salvo no diretório MQL5/Files.

    Abaixo está como o arquivo ONNX parece quando aberto no MetaEditor. Note a explicação do processo. Isso é importante.

    Arquivo ONNX por dentro

    Na seção de parâmetros de entrada, temos float_input — um parâmetro do tipo float. A seguir, 'tensor' significa que precisamos passar para a função OnnxRun uma matriz ou vetor, pois ambos são tensores. No final, está indicado (?, 4) — o tamanho das entradas, onde ? significa que o número de linhas é desconhecido; o número de colunas é 4. A seguir está a parte Outputs.

    Aqui temos dois nós — um fornece as etiquetas previstas -1 ou 1, que neste caso são do tipo INT64 ou INT em mql5.

    O segundo nó com probabilidades é um tensor de tipos float, contendo 2 colunas e um número desconhecido de linhas. Para extrair valores, pode-se usar uma matriz nx2 ou simplesmente um vetor de tamanho >= 2.

    Como há dois nós nos dados de saída, podemos extrair os resultados duas vezes:

       long outputs_0[] = {1};
       if (!OnnxSetOutputShape(onnx_handle, 0, outputs_0)) //giving the onnx handle first node output shape
         {
           Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError());
           return false;
         }
         
       long outputs_1[] = {1,2};
       if (!OnnxSetOutputShape(onnx_handle, 1, outputs_1)) //giving the onnx handle second node output shape
         {
           Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError());
           return false;
         }
    

    Por outro lado, podemos extrair um único nó de entrada.

       const long inputs[] = {1,4};
       
       if (!OnnxSetInputShape(onnx_handle, 0, inputs)) //Giving the Onnx handle the input shape
         {
           Print(__FUNCTION__," Failed to set the input shape Err=",GetLastError());
           return false;
         }   
    

    Este código ONNX foi obtido da função LoadONNX, mostrada abaixo:

    bool CDualSVMONNX::LoadONNX(const uchar &onnx_buff[], ENUM_ONNX_FLAGS flags=ONNX_NO_CONVERSION)
     {
       onnx_handle =  OnnxCreateFromBuffer(onnx_buff, flags); //creating onnx handle buffer 
       
       if (onnx_handle == INVALID_HANDLE)
        {
           Print(__FUNCTION__," OnnxCreateFromBuffer Error = ",GetLastError());
           return false;
        }
       
    //---
       
       const long inputs[] = {1,4};
       
       if (!OnnxSetInputShape(onnx_handle, 0, inputs)) //Giving the Onnx handle the input shape
         {
           Print(__FUNCTION__," Failed to set the input shape Err=",GetLastError());
           return false;
         }
       
       long outputs_0[] = {1};
       if (!OnnxSetOutputShape(onnx_handle, 0, outputs_0)) //giving the onnx handle first node output shape
         {
           Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError());
           return false;
         }
         
       long outputs_1[] = {1,2};
       if (!OnnxSetOutputShape(onnx_handle, 1, outputs_1)) //giving the onnx handle second node output shape
         {
           Print(__FUNCTION__," Failed to set the output shape Err=",GetLastError());
           return false;
         }
    
       return true;
     }
    

    Observe atentamente a função que, como você pode ter adivinhado, nos falta ao carregar os parâmetros de normalização. Esses parâmetros são muito importantes para padronizar novos dados de entrada para que correspondam às dimensões dos dados treinados com os quais o modelo já está familiarizado.

    É possível carregar os parâmetros de um arquivo CSV - isso funciona sem problemas em tempo real de negociação. No entanto, esse método pode ser complicado e nem sempre funcionar bem no testador de estratégias. Portanto, por enquanto, vamos copiar os parâmetros de normalização manualmente para o código do nosso EA. Finalmente, teremos os parâmetros de normalização dentro do nosso EA. Primeiro, alteremos a função LoadONNX para que ela aceite vetores de entrada max e min, que são usados no Min Max Scaler.

    bool CDualSVMONNX::LoadONNX(const uchar &onnx_buff[], ENUM_ONNX_FLAGS flags, vectorf &norm_max, vectorf &norm_min)

    Fim desta função.

    normalize_x = new CPreprocessing<vectorf,matrixf>(norm_max, norm_min); //Load min max scaler with parameters

    Copiando e colando parâmetros de normalização de arquivos CSV para EAs.


    Vamos treinar e tentar testar o modelo da mesma forma que fizemos com Python. Nosso objetivo é garantir que estamos no mesmo caminho em ambas as linguagens.

    Função OnInit no EA de teste test.mq5

    vector min_v = {14.32424641,1.04674852,1.04799891,1.04392886};
    vector max_v = {86.28263092,1.07385755,1.07907069,1.07267821};
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //---    
        rsi_handle = iRSI(Symbol(),PERIOD_CURRENT, rsi_period, PRICE_CLOSE);
        bb_handle = iBands(Symbol(), PERIOD_CURRENT, bb_period, 0 , bb_deviation, PRICE_CLOSE);
    
        
          vector y_train,
                 y_test;
            
        // float values
          
          matrixf datasetf = GetTrainTestData<float>();
          matrixf x_trainf,
                  x_testf;
          
          vectorf y_trainf,
                  y_testf;
                
    //---
          
          matrix_utils.TrainTestSplitMatrices(datasetf,x_trainf,y_trainf,x_testf,y_testf,0.8,42); //split the data into training and testing samples
          
          vectorf max_vf = {}, min_vf = {}; //convertin the parameters into float type
          max_vf.Assign(max_v); 
          min_vf.Assign(min_v);
          
          dual_svm.LoadONNX(SVMModel, ONNX_DEFAULT, max_vf, min_vf);
                
          y_train.Assign(y_trainf);
          y_test.Assign(y_testf);
          
          vector train_preds = dual_svm.Predict(x_trainf);
          vector test_preds = dual_svm.Predict(x_testf);
          
          Print("\n<<<<< Train Classification Report >>>>\n");
          metrics.confusion_matrix(y_train, train_preds);
          
          Print("\n<<<<< Test  Classification Report >>>>\n");
          metrics.confusion_matrix(y_test, test_preds);
    
    
       return(INIT_SUCCEEDED);
      }
    
    Resultado
    RP      0       17:08:53.068    svm test (EURUSD,H1)    <<<<< Train Classification Report >>>>
    HE      0       17:08:53.068    svm test (EURUSD,H1)    
    MR      0       17:08:53.068    svm test (EURUSD,H1)    Confusion Matrix
    IG      0       17:08:53.068    svm test (EURUSD,H1)    [[245,148]
    CO      0       17:08:53.068    svm test (EURUSD,H1)     [150,257]]
    NK      0       17:08:53.068    svm test (EURUSD,H1)    
    DE      0       17:08:53.068    svm test (EURUSD,H1)    Classification Report
    HO      0       17:08:53.068    svm test (EURUSD,H1)    
    FI      0       17:08:53.068    svm test (EURUSD,H1)    _    Precision  Recall  Specificity  F1 score  Support
    ON      0       17:08:53.068    svm test (EURUSD,H1)    1.0    0.62     0.62     0.63       0.62     393.0
    DP      0       17:08:53.068    svm test (EURUSD,H1)    -1.0    0.63     0.63     0.62       0.63     407.0
    JG      0       17:08:53.068    svm test (EURUSD,H1)    
    FR      0       17:08:53.068    svm test (EURUSD,H1)    Accuracy                                   0.63
    CK      0       17:08:53.068    svm test (EURUSD,H1)    Average   0.63    0.63    0.63      0.63    800.0
    KI      0       17:08:53.068    svm test (EURUSD,H1)    W Avg     0.63    0.63    0.63      0.63    800.0
    PP      0       17:08:53.068    svm test (EURUSD,H1)    
    DH      0       17:08:53.068    svm test (EURUSD,H1)    <<<<< Test  Classification Report >>>>
    PQ      0       17:08:53.068    svm test (EURUSD,H1)    
    EQ      0       17:08:53.068    svm test (EURUSD,H1)    Confusion Matrix
    HJ      0       17:08:53.068    svm test (EURUSD,H1)    [[61,31]
    MR      0       17:08:53.068    svm test (EURUSD,H1)     [40,68]]
    NH      0       17:08:53.068    svm test (EURUSD,H1)    
    DP      0       17:08:53.068    svm test (EURUSD,H1)    Classification Report
    HL      0       17:08:53.068    svm test (EURUSD,H1)    
    FF      0       17:08:53.068    svm test (EURUSD,H1)    _    Precision  Recall  Specificity  F1 score  Support
    GJ      0       17:08:53.068    svm test (EURUSD,H1)    -1.0    0.60     0.66     0.63       0.63     92.0
    PO      0       17:08:53.068    svm test (EURUSD,H1)    1.0    0.69     0.63     0.66       0.66     108.0
    DD      0       17:08:53.068    svm test (EURUSD,H1)    
    JO      0       17:08:53.068    svm test (EURUSD,H1)    Accuracy                                   0.65
    LH      0       17:08:53.068    svm test (EURUSD,H1)    Average   0.65    0.65    0.65      0.64    200.0
    CJ      0       17:08:53.068    svm test (EURUSD,H1)    W Avg     0.65    0.65    0.65      0.65    200.0
    

    Nós novamente obtivemos os mesmos 63% de precisão, que obtivemos com o script em Python. Não é maravilhoso?

    Aqui está como a função de previsão parece por dentro:

    int CDualSVMONNX::Predict(vectorf &inputs)
     {
        vectorf outputs(1); //label outputs
        vectorf x_output(2); //probabilities
        
        vectorf temp_inputs = inputs;
        
        normalize_x.Normalization(temp_inputs); //Normalize the input features
        
        if (!OnnxRun(onnx_handle, ONNX_DEFAULT, temp_inputs, outputs, x_output))
          {
             Print("Failed to get predictions from onnx Err=",GetLastError());
             return (int)outputs[0];
          }
          
       return (int)outputs[0];
     }
    

    Ela executa o arquivo ONNX para obter previsões e retorna um inteiro para o rótulo previsto.

    Depois, implementamos uma estratégia simples para testar ambos os modelos do método de vetores de suporte no testador de estratégias. A estratégia é simples: se a classe prevista pelo SVM == 1, abrimos uma operação de compra, caso contrário, se a classe prevista == -1, abrimos uma operação de venda.

    Resultados no testador de estratégias:

    Para o método linear de vetores de suporte

    Para a forma dual do método de vetores de suporte:

    Mantemos todos os mesmos dados de entrada, exceto svm_type.


    A forma dual do SVM não se saiu muito bem com os dados de entrada que funcionaram para o SVM linear. Talvez seja necessário mais otimização e investigação sobre por que o modelo ONNX não está convergindo, mas isso é assunto para outro artigo.


    Pensamentos finais

    Vantagens dos modelos de vetores de suporte SVM

    1. Funciona eficazmente em espaços multidimensionais, ou seja, o método de vetores de suporte é adequado para trabalhar com amostras de dados financeiros com múltiplas funções, indicadores de negociação e variáveis de mercado.
    2. Os vetores de suporte são menos propensos a sobreajuste, fornecendo uma solução mais generalizada que pode se adaptar melhor às condições de mercado invisíveis.
    3. Os modelos de SVM oferecem versatilidade graças às diferentes funções de kernel, permitindo que os traders experimentem diferentes estratégias e adaptem o modelo a padrões de mercado específicos.
    4. Os métodos SVM capturam bem as relações não lineares dentro dos dados, o que é crucial ao lidar com mercados financeiros complexos.

    Desvantagens

    1. SVM podem ser sensíveis a dados ruidosos, o que afeta seus resultados e os torna mais suscetíveis a comportamentos de mercado instáveis.
    2. O treinamento de modelos SVM pode ser custoso em termos de recursos computacionais, especialmente ao lidar com grandes conjuntos de dados, o que limita sua escalabilidade em certos cenários de negociação em tempo real.
    3. Vetores de suporte dependem significativamente do desenvolvimento de características, exigindo conhecimento na área de domínio para escolher os indicadores apropriados e o processamento eficaz dos dados.
    4. Como vimos, os modelos em SVM mostraram uma precisão média de até 63% na forma dual e 59% na linear. Embora esses modelos possam não superar alguns métodos avançados de aprendizado de máquina, eles ainda oferecem um ponto de partida razoável para traders de MQL5.

    Declínio da popularidade

    Apesar do sucesso no passado, nos últimos anos a popularidade do SVM diminuiu. Isso pode estar relacionado ao seguinte:

    1. O desenvolvimento de métodos de aprendizado profundo, especialmente redes neurais, suplantou algoritmos tradicionais de machine learning devido à sua capacidade de extrair automaticamente características hierárquicas.
    2. Conjuntos extensivos de dados financeiros tornaram-se cada vez mais acessíveis, tornando modelos de aprendizado profundo, que funcionam bem com grandes volumes de dados, mais atraentes.
    3. O surgimento de hardware poderoso e recursos computacionais distribuídos tornou mais viável o treinamento e a implantação de modelos complexos de aprendizado profundo.

    Em conclusão, embora os modelos SVM possam não ser a solução de ponta, seu uso em ambientes de negociação MQL5 é justificado. Sua simplicidade, confiabilidade e adaptabilidade os tornam uma ferramenta valiosa, especialmente para traders com dados limitados ou recursos computacionais. Vetores de suporte podem ser vistos como parte de um conjunto de ferramentas mais amplo, potencialmente complementando-os com novos enfoques sobre o aprendizado de máquina conforme a dinâmica do mercado evolui.

    Obrigado pela atenção.

    Arquivo Descrição | Uso
     dual_svm.py | script python  Implementação do Dual SVM em Python.
     GetDataforONNX.mq5 | script mql5  Usado para coletar, normalizar e armazenar dados em um arquivo csv localizado na pasta MQL5/Files.
     preprocessing.mqh | arquivo include mql5  Contém a classe e funções para normalização e padronização dos dados de entrada.
     matrix_utils.mqh | arquivo include mql5  Biblioteca com operações matriciais adicionais. 
     metrics.mqh | arquivo include mql5  Biblioteca que contém funções adicionais para análise de desempenho de modelos de aprendizado de máquina. 
     svm test.mq5 | EA  Expert Advisor para testar todo o código que está no artigo.  

    O código usado neste artigo também pode ser encontrado no meu repositório no GitHub.


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

    Arquivos anexados |
    code.zip (25.53 KB)
    Stop-loss e take-profit amigáveis ao trader Stop-loss e take-profit amigáveis ao trader
    Stop-loss e take-profit podem ter um impacto significativo nos resultados do trading. Neste artigo, vamos explorar algumas maneiras de encontrar os valores ótimos para ordens de stop.
    Redes neurais de maneira fácil (Parte 63): pré-treinamento do transformador de decisões não supervisionado (PDT) Redes neurais de maneira fácil (Parte 63): pré-treinamento do transformador de decisões não supervisionado (PDT)
    Continuamos nossa análise, desta vez, explorando a família de transformadores de decisão. Em trabalhos anteriores, já observamos que o treinamento do transformador subjacente à arquitetura desses métodos é bastante desafiador e requer uma grande quantidade de dados de treinamento rotulados. Neste artigo, consideramos um algoritmo para usar trajetórias não rotuladas com o objetivo de pré-treinar modelos.
    Redes neurais de maneira fácil (Parte 64): Método de clonagem de comportamento ponderada conservadora (CWBC) Redes neurais de maneira fácil (Parte 64): Método de clonagem de comportamento ponderada conservadora (CWBC)
    Pelo resultado dos testes realizados em artigos anteriores, concluímos que a qualidade da estratégia treinada depende muito da amostra de treinamento utilizada. Neste artigo, apresento a vocês um método simples e eficaz para selecionar trajetórias com o objetivo de treinar modelos.
    Criando um Expert Advisor simples multimoeda usando MQL5 (Parte 3): Prefixos/sufixos de símbolos e sessão de negociação Criando um Expert Advisor simples multimoeda usando MQL5 (Parte 3): Prefixos/sufixos de símbolos e sessão de negociação
    Recebi comentários de vários colegas traders sobre como usar o Expert Advisor multimoedas que estou analisando com corretoras que usam prefixos e/ou sufixos com nomes de símbolos, bem como sobre como implementar fusos horários de negociação ou sessões de negociação no Expert Advisor.