English Русский
preview
Previsão da distribuição condicional com uma MLP

Previsão da distribuição condicional com uma MLP

MetaTrader 5Indicadores |
32 9
Evgeniy Chernish
Evgeniy Chernish

Introdução

Diante da alta volatilidade e da incerteza dos mercados financeiros, como o Forex, prever com precisão os movimentos dos preços continua sendo uma das principais tarefas de traders e analistas. As abordagens tradicionais de regressão, baseadas em funções de perda convencionais, como o erro quadrático médio (MSE) ou a soma dos quadrados dos erros (SSE), muitas vezes não são suficientemente eficazes. Essas funções pressupõem homoscedasticidade, isto é, variância constante da variável-alvo, uma condição que raramente corresponde à dinâmica real das séries temporais financeiras. Para modelar esses dados de forma adequada, é necessário adotar uma abordagem que considere a heteroscedasticidade, ou seja, a variância dos incrementos de preço, que varia em função dos atributos de entrada.

Neste artigo, apresentamos uma abordagem probabilística para a regressão usando um perceptron multicamadas (MLP), capaz de prever a distribuição gaussiana condicional dos incrementos de preço. Para isso, implementaremos uma função de perda personalizada baseada na log-verossimilhança negativa gaussiana (GaussianNLLLoss).

Para implementar essa abordagem, foi criada a classe MLPRegressor, integrada à biblioteca ALGLIB, que fornece uma implementação do algoritmo de otimização L-BFGS. O artigo descreve em detalhes os principais métodos da classe: propagação para frente, propagação reversa e integração com o L-BFGS. Também é apresentado um indicador que demonstra a aplicação do modelo para prever os incrementos de preço de pares de moedas e estimar a incerteza associada a essas previsões. Como resultado, nosso modelo oferece ao trader não apenas uma previsão pontual, mas toda a distribuição condicional da variável-alvo.



Função de perda MSE e sua relação com a verossimilhança gaussiana de variância constante

Primeiro, vamos relembrar o que é uma função de perda e por que ela desempenha um papel fundamental nas tarefas de machine learning. A função de perda é uma medida que quantifica o grau de desvio entre as previsões do modelo e os dados observados. Uma das funções de perda mais utilizadas em tarefas de regressão é o erro quadrático médio (Mean Squared Error, MSE), calculado da seguinte forma:

MSE

onde:

  • t: valor observado da variável-alvo (target), 
  • y: valor previsto pelo modelo, 
  • n: número de observações.

O erro quadrático médio (MSE), amplamente utilizado em tarefas de regressão, pressupõe que a variável-alvo siga uma distribuição normal com variância constante σ²:

t ~ N(y, σ²)

Nesse caso, a função de verossimilhança de uma amostra com n observações é expressa pelo produto das densidades da distribuição normal:

LLF homoscedástica

Para facilitar a otimização, normalmente minimiza-se a log-verossimilhança negativa:

NLL_homoscedastic

Como a variância σ² é constante, o segundo termo da fórmula não depende dos parâmetros do modelo e não afeta a otimização. Portanto, minimizar a função de log-verossimilhança negativa equivale a minimizar o MSE. Isso mostra que o uso do MSE como função de perda pressupõe implicitamente que os incrementos de preço seguem uma distribuição normal com variância constante, independente dos atributos de entrada.

No entanto, no contexto de dados financeiros, como os incrementos de preço dos pares de moedas do Forex, essa hipótese raramente corresponde à realidade. Nessas condições, o uso do MSE como função de perda representa uma simplificação excessiva e pode resultar em modelos pouco precisos, incapazes de considerar as variações da volatilidade. Para superar essa limitação, criaremos uma função de perda personalizada baseada na log-verossimilhança negativa gaussiana, que modela a variância condicional em função dos atributos de entrada.



Verossimilhança gaussiana com variância não constante: GaussianNLLLoss

Nesse caso, pressupomos que a variável-alvo t siga uma distribuição normal com variância condicional σᵢ², que depende dos dados de entrada xᵢ.

Modelo heteroscedástico

A log-verossimilhança negativa, obtida pela média dos termos correspondentes às n observações, é então expressa da seguinte forma:

NLL heteroscedástica

Essa função de perda é composta por dois termos. O primeiro corresponde, essencialmente, a um erro quadrático médio ponderado. O peso de cada termo é inversamente proporcional à variância prevista. Isso significa que, quanto maior for a incerteza prevista (σ²), menor será a influência do erro correspondente sobre a função de perda total.

O segundo termo penaliza o aumento indiscriminado da variância prevista, impedindo que ele simplesmente aumente a variância σᵢ² para reduzir o primeiro termo. Dessa forma, o modelo é incentivado a prever uma variância compatível com a incerteza real dos dados.

Assim, a função GaussianNLLLoss permite prever simultaneamente duas grandezas: a esperança matemática condicional μᵢ e a variância σᵢ². Isso equivale a especificar integralmente uma distribuição normal para cada ponto de dados.

//+------------------------------------------------------------------+
//| GaussianNLLLoss (Negative Log-Likelihood)                        |
//+------------------------------------------------------------------+
double GaussianNLLLoss(const matrix &output, const matrix &target, int sample)
  {
   if(output.Rows() != 2 || output.Cols() != sample || target.Rows() != sample || target.Cols() != 1)
     {
      Print("GaussianNLLLoss: Invalid matrix dimensions");
      return DBL_MAX;
     }

   vector mu = output.Row(0);
   vector sigma2 = output.Row(1); // after softplus

// Limit the dispersion from below by eps value
   for(int i = 0; i < sample; i++)
     {
      sigma2[i] = MathMax(sigma2[i], 1e-6); // max(sigma_i^2, eps)
     }

//  target - mu
   vector diff = target.Col(0) - mu;
//  (t - mu)^2 / (2 * sigma^2)
   vector term1 = diff * diff / (2.0 * sigma2);
//  0.5 * log(2 * PI * sigma^2)
   vector term2 = 0.5 * (MathLog(2.0 * M_PI * sigma2));
// Sum both terms and average
   double sum = (term1 + term2).Sum();
   return sum / sample;
  }



Conceito de verossimilhança

A verossimilhança é um conceito fundamental da estatística, utilizado tanto na abordagem frequentista quanto na bayesiana. Ela caracteriza a plausibilidade dos dados observados para determinados parâmetros do modelo. No contexto de machine learning, a verossimilhança permite avaliar quão bem o modelo descreve os dados.

Como exemplo, considere um conjunto de dados que, por hipótese, segue uma distribuição normal com média μ e variância σ². A função de verossimilhança indica o quanto é plausível que os dados observados tenham sido gerados por uma distribuição normal com valores específicos desses parâmetros. Em outras palavras, ela ajuda a responder à seguinte pergunta: "Até que ponto os parâmetros escolhidos são compatíveis com os dados observados?".

Para ilustrar o princípio da máxima verossimilhança, podemos executar um script que gera o gráfico da log-verossimilhança de uma amostra extraída da distribuição normal N(0,1) em função da média. Mantendo a variância fixa em σ² = 1, percorremos os valores de μ no intervalo [−3,3] e calculamos a verossimilhança com a função MathProbabilityDensityNormal. O gráfico resultante mostra que a verossimilhança atinge seu valor máximo em μ ≈ 0, que corresponde à média teórica da distribuição que gerou a amostra. Todos os demais valores de μ apresentam menor verossimilhança, o que indica uma menor compatibilidade com os dados.

#include <Math\Stat\Normal.mqh>
#include <Graphics\Graphic.mqh>

//+----------------------------------------------------+
//| Log-Likelihood                             |
//+----------------------------------------------------+
double LogLikelihood(const double &t[], double mu, double sigma)
{
   int n = ArraySize(t);
   double result[];
   ArrayResize(result, n);
   // Calculate the logarithm of the probability density
   MathProbabilityDensityNormal(t, mu, sigma, true, result);

   // Sum ln(p(x_i)) to get the log-likelihood
   double ll = 0.0;
   for(int i = 0; i < n; i++)
      ll += result[i]; 
  
   return ll;
} 

//+--------------------------------------------+
//| Plot Log-Likelihood                        |
//+--------------------------------------------+
void PlotLL(const double &mu_values[], const double &ll_values[], int sec)
{
   ChartSetInteger(0, CHART_SHOW, false);
   CGraphic graphic;
   ulong width = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   ulong height = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   graphic.Create(0, "LL_Graphic", 0, 0, 0, int(width), int(height));
   graphic.CurveAdd(mu_values,ll_values, ColorToARGB(clrBlue, 255), CURVE_LINES, "Log-Likelihood");
   graphic.XAxis().Name("Mu");
   graphic.YAxis().Name("Log-Likelihood");
   graphic.BackgroundMain("Log-Likelihood");
   graphic.XAxis().NameSize(18);
   graphic.YAxis().NameSize(18);
   graphic.BackgroundMainColor(ColorToARGB(clrBlack, 255));
   graphic.BackgroundMainSize(24);
   graphic.CurvePlotAll();
   graphic.Update();
   Sleep(sec * 1000);
   ChartSetInteger(0, CHART_SHOW, true);
   graphic.Destroy();
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   // Generate a synthetic sample
   MathSrand(55); 
   int n = 100; // Number of observations
   double true_mu = 0.0; // True mean
   double true_sigma = 1.0; // Fixed standard deviation
   double t[]; // Sample
   ArrayResize(t, n);
   MathRandomNormal(true_mu, true_sigma, n, t);

   // Range for mu
   double mu_min = -3.0;
   double mu_max = 3.0;
   double mu_step = 0.1;
   int steps = (int)((mu_max - mu_min) / mu_step) + 1;

   double mu_values[];
   double ll_values[];
   ArrayResize(mu_values, steps);
   ArrayResize(ll_values, steps);

   // Calculate log-likelihood for each mu
   double max_ll = -DBL_MAX;
   double best_mu = 0.0;
   for(int i = 0; i < steps; i++)
   {
      mu_values[i] = mu_min + i * mu_step;
      ll_values[i] = LogLikelihood(t, mu_values[i], true_sigma);
      if(ll_values[i] > max_ll)
      {
         max_ll = ll_values[i];
         best_mu = mu_values[i];
      }
   }

   PlotLL(mu_values, ll_values, 10); 

   // Display results
   Print("True mean: mu = ", true_mu);
   Print("Found mean: mu = ", best_mu);
   Print("Maximum log likelihood value: ", max_ll);
}

LLF

Fig. 1. Gráfico da função de log-verossimilhança para a média de N(0,1)


Implementação do modelo MLP para regressão com a função de perda GaussianNLLLoss

Para implementar a abordagem proposta de previsão da distribuição gaussiana condicional, utilizaremos um modelo de perceptron multicamadas, representado pela classe MLPRegressor. A arquitetura da rede é bastante simples: o modelo possui apenas uma camada oculta e exatamente duas saídas. A primeira corresponde à esperança matemática condicional dos incrementos de preço, enquanto a segunda representa a variância condicional. 

Para modelar corretamente esses parâmetros, utilizamos funções de ativação fixas na camada de saída: ativação linear para a média e a função Softplus para a variância, garantindo que a variância prevista seja positiva. A função de ativação da camada oculta pode ser escolhida livremente pelo usuário. A lista completa das funções de ativação disponíveis pode ser consultada na documentação do MQL5, na seção "Machine Learning / Activation". 

Dois métodos desempenham um papel fundamental no funcionamento do modelo: a propagação para frente (FeedForward) e a propagação reversa (Backprop). O primeiro calcula as previsões μᵢ e σᵢ² com base nos dados de entrada. O segundo calcula os gradientes da função de perda GaussianNLLLoss, permitindo que o algoritmo L-BFGS atualize os parâmetros do modelo.



Propagação para frente (FeedForward)

O método FeedForward implementa a propagação do sinal pela rede neural e calcula as previsões com base nos dados de entrada:

//+------------------------------------------------------------------+
//| Feed forward                                                     |
//+------------------------------------------------------------------+
bool MLPRegressor::FeedForward(const matrix &data)
  {
   ones_ = matrix::Ones(1, data.Rows());
   n1 = weights1.MatMul(data.Transpose()) + bias1.MatMul(ones_);
   n1.Activation(act1, ac_func);
   n2 = weights2.MatMul(act1) + bias2.MatMul(ones_);
   act2.Init(n2.Rows(), n2.Cols());
   act2.Row(n2.Row(0), 0); // First output: linear activation (copy directly)
// Second output: softplus
   vector z = n2.Row(1);
   vector sigma2;
   z.Activation(sigma2, AF_SOFTPLUS);
   act2.Row(sigma2, 1);

   return true;
  }

Na última camada, a rede produz dois valores: 

  • μᵢ: média obtida diretamente por meio de uma ativação linear, 
  • σᵢ²: variância obtida pela aplicação da função Softplus à segunda saída da rede para que seu valor permaneça sempre positivo. 



Propagação reversa (Backprop)

O método Backprop implementa a retropropagação do erro e calcula os gradientes da função de perda em relação a todos os parâmetros do modelo, ou seja, pesos e vieses. Esses gradientes são necessários para treinar o modelo com o algoritmo L-BFGS:

//+--------------------------------------------------------+
//| Backprop                                               |
//+--------------------------------------------------------+
bool MLPRegressor::Backprop(const matrix &data, const matrix &target)
  {
   const double eps = 1e-6;     // Parameter for numerical stability of optimization
   vector mu = act2.Row(0);     // mu_i (network output for the mean)
   vector sigma2 = act2.Row(1); // sigma_i^2 (network output for variance)
// Limit sigma2 from below by the value of eps
   for(int i = 0; i < Sample_; i++)
     {
      sigma2[i] = MathMax(sigma2[i], eps); // max(sigma_i^2, eps)
     }
   vector t = target.Col(0);    // t_i (target variable)
   vector diff = t - mu;        // t_i - mu_i

// 1. Gradients of the loss function at the network outputs (mu_i, sigma_i^2)
   matrix DerivLoss_wrt_Output(layer2, Sample_);
   DerivLoss_wrt_Output.Row(-1*diff / sigma2 /Sample_, 0);  // dL/d(mu_i) = -(t_i - mu_i) / sigma^2_i
   vector term = 0.5 / sigma2 - 0.5 * (diff * diff / (sigma2 * sigma2)); // dL/d(sigma_i^2)
   DerivLoss_wrt_Output.Row(term/Sample_, 1); // dL/d(sigma_i^2) = (1/(2*sigma_i^2) - (t_i - mu_i)^2 / (2*sigma_i^2)^2)

// 2. Derivatives of output layer activations
   matrix deriv_act2(layer2, Sample_);
   deriv_act2.Row(vector::Ones(Sample_), 0);
   vector z = n2.Row(1);
   vector sigmoid_z;
   z.Derivative(sigmoid_z, AF_SOFTPLUS);
   deriv_act2.Row(sigmoid_z, 1);

// 3. Output layer error D2
   matrix D2 = deriv_act2 * DerivLoss_wrt_Output; // [layer2, Sample_]

// 4. Derivatives of hidden layer activations
   matrix deriv_act1;
   n1.Derivative(deriv_act1, ac_func); // [layer1, Sample_]

// 5. Hidden layer error D1
   matrix D1 = weights2.Transpose().MatMul(D2); // [layer1, Sample_]
   D1 = D1 * deriv_act1;

// 6. Calculate gradients
   matrix ones = matrix::Ones(Sample_, 1);
   gW1 = D1.MatMul(data);             // Gradients for weights1
   gb1 = D1.MatMul(ones);             // Gradients for bias1
   gW2 = D2.MatMul(act1.Transpose()); // Gradients for weights2
   gb2 = D2.MatMul(ones);             // Gradients for bias2

   return true;
  }

O método Backprop começa calculando os gradientes da função de perda em relação às saídas da rede: a média μᵢ e a variância σᵢ². Infelizmente, não podemos utilizar a função LossGradient já disponível na seção de machine learning, pois ela foi desenvolvida para as funções de perda predefinidas. Portanto, precisamos calcular esses gradientes diretamente com base nas fórmulas a seguir.

O gradiente da função de perda em relação à saída correspondente à média é calculado da seguinte forma:

Grad_L_u

O gradiente da função de perda em relação à saída correspondente à variância é:

Grad_L_sigma2

O método Backprop foi adaptado à função de perda GaussianNLLLoss, mas sua estrutura é universal. Para utilizar outra função de perda personalizada, a principal etapa consiste em calcular os gradientes em relação às saídas da rede. As demais etapas, como o cálculo das derivadas das funções de ativação e a propagação do erro pelas camadas anteriores, seguem as regras convencionais da propagação reversa.



Método Fit: treinamento do modelo com L-BFGS

O método Fit é responsável pelo treinamento da rede neural, integrando a propagação para frente, a propagação reversa e a otimização dos parâmetros por meio do algoritmo L-BFGS.

//+-------------------------------------------------+
//| Fit method using L-BFGS                         |
//+-------------------------------------------------+
bool MLPRegressor::Fit(const matrix &data, const matrix &target)
  {
   Sample_ = (int)data.Rows();
   Features = (int)data.Cols();
   ones_ = matrix::Ones(1, Sample_);

   ArrayResize(target_Plot, Sample_);
   for(int i = 0; i < Sample_; i++)
      target_Plot[i] = target[i, 0];

   if(!CreateNet())
      return false;

// Create an object for optimization
   MLPOptimizationObjective objective(GetPointer(this), data, target);
   MLPReportCallback frep(GetPointer(this)); // Create an object to track the loss function
   CObject obj;

// Prepare parameters
   CRowDouble params;
   objective.PackParameters(params);
   double theta[];
   ArrayResize(theta, params.Size());
   for(int i = 0; i < params.Size(); i++)
      theta[i] = params[i];

// Set up optimization parameters
   double epsg = 0.0001;     // Gradient accuracy
   double epsf = 0.00001;     // Precision by function value
   double epsw = 0.0000;   // Accuracy by parameters
   int maxits = Epochs;       // Maximum number of iterations

// Initialize and start L-BFGS
   CMinLBFGSStateShell state;
   CMinLBFGSReportShell rep;
   CAlglib::MinLBFGSCreate(params.Size(), theta, state);
   CAlglib::MinLBFGSSetCond(state, epsg, epsf, epsw, maxits);
   CAlglib::MinLBFGSSetXRep(state, true); // Enable optimization progress reports
   CAlglib::MinLBFGSOptimize(state, objective, frep, true, obj);
   CAlglib::MinLBFGSResults(state, theta, rep);

//----------TerminationType field contains completion code, which can be:
//  -8    internal integrity control detected  infinite  or  NAN  values  in
//        function/gradient. Abnormal termination signalled.
//   1    relative function improvement is no more than EpsF.
//   2    relative step is no more than EpsX.
//   4    gradient norm is no more than EpsG
//   5    MaxIts steps was taken
//   7    stopping conditions are too stringent,
//        further improvement is impossible,
//        X contains best point found so far.
//   8    terminated    by  user  who  called  minlbfgsrequesttermination().
//        X contains point which was   "current accepted"  when  termination
//        request was submitted.
//--------------------------------------------------------------------------
// Get the final array of parameters
   for(int i = 0; i < (int)params.Size(); i++)
     {
      params.Set(i, theta[i]);
     }
   objective.UnpackParameters(params);

// Final forward pass with optimized parameters
   if(!FeedForward(data))
      return false;

// Save all network outputs in one-dimensional form for later use in the PlotGraphic method
// for visualization. The array contains the parameters of the normal distribution for each observation.
   ArrayResize(NetOutput, Sample_ * layer2);
   for(int i = 0; i < Sample_; i++)
      for(int j = 0; j < layer2; j++)
         NetOutput[i * layer2 + j] = act2.Transpose()[i, j];

   PrintFormat("L-BFGS Optimization completed. Iterations: %d, Termination type: %d, Final loss: %.5f",
               rep.GetIterationsCount(), rep.GetTerminationType(), objective.GetLoss());

   for(int i = 0; i < LossCount; i++)
      PrintFormat("Iteration %d, Loss: %.5f", i, LossPlot[i]);

   return true;
  }

O treinamento segue uma sequência de etapas. Primeiro, o método CreateNet é chamado para inicializar as matrizes de pesos (weights1, weights2) e os vetores de vieses (bias1, bias2) com valores aleatórios, preparando o modelo para o treinamento. Em seguida, é criado um objeto da classe MLPOptimizationObjective, que conecta o modelo ao otimizador L-BFGS e fornece a função de perda e seus gradientes.

Os parâmetros do modelo são convertidos em um array unidimensional params pelo método PackParameters, de modo a atender ao formato exigido pelo L-BFGS. Para monitorar a convergência, é criado um objeto da classe MLPReportCallback, que registra no array LossPlot os valores da função de perda após cada iteração da otimização.

Em seguida, o algoritmo L-BFGS é configurado com os seguintes parâmetros de otimização:

  • epsg: tolerância da norma do gradiente, que define o critério de parada quando os gradientes se tornam suficientemente pequenos,
  • epsf: tolerância definida com base no valor da função de perda,
  • epsw: tolerância da variação dos parâmetros,
  • maxits: número máximo de iterações, correspondente ao número de épocas definido pelo usuário.

A otimização é iniciada com o vetor inicial de parâmetros por meio das funções MinLBFGSCreate e MinLBFGSOptimize. A chamada de MinLBFGSSetXRep(state, true) habilita as chamadas ao método virtual Rep da classe MLPReportCallback, permitindo salvar o histórico dos valores da função de perda. Ao término da otimização, os parâmetros finais são extraídos para o array theta pelo método MinLBFGSResults e desempacotados de volta nas matrizes de pesos e nos vetores de vieses. Depois disso, é realizada uma propagação para frente final com o método FeedForward para obter as previsões do modelo. Os resultados são armazenados no array NetOutput para análise ou visualização posterior.



Por que escolher o L-BFGS

Para treinar o modelo MLPRegressor, optamos pelo algoritmo L-BFGS porque ele é compatível com funções de perda personalizadas, ao contrário do algoritmo de Levenberg-Marquardt, que se limita a funções quadráticas, como o MSE.

Métodos de gradiente estocástico, como o Adam, são a melhor opção e, muitas vezes, a única alternativa viável para treinar redes neurais com muitas camadas ocultas e grandes volumes de dados. No entanto, na análise de cotações, normalmente são utilizados conjuntos de dados relativamente pequenos, pois a influência dos eventos passados decai rapidamente. Nesse contexto, esses métodos apresentam uma convergência muito mais lenta do que métodos de segunda ordem, como o L-BFGS.



Classes de integração com o otimizador L-BFGS

Para conectar nossa classe ao otimizador L-BFGS, foram implementadas duas classes auxiliares: MLPOptimizationObjective e MLPReportCallback. Por meio delas, fornecemos ao otimizador a função de perda e seus gradientes, além de acompanhar o progresso da otimização. 

Classe MLPOptimizationObjective

A classe MLPOptimizationObjective conecta a rede neural MLPRegressor à implementação do algoritmo de otimização L-BFGS fornecida pela biblioteca ALGLIB. Como herda de CNDimensional_Grad, ela sobrescreve a função virtual Grad para fornecer ao L-BFGS o valor da função de perda GaussianNLLLoss e os gradientes necessários ao treinamento.

//+------------------------------------------------------------------+
//| Class for the L-BFGS optimizer objective function                |
//+------------------------------------------------------------------+
class MLPOptimizationObjective : public CNDimensional_Grad
  {
private:
   MLPRegressor*     m_mlp;  // Pointer to the neural network object
   matrix            m_data;        // Input data
   matrix            m_target;      // Target values
   double            m_loss;        // Current losses

public:
                     MLPOptimizationObjective(MLPRegressor* mlpLBFGS, const matrix &data, const matrix &target)
      :              m_mlp(mlpLBFGS), m_data(data), m_target(target), m_loss(0.0) {}

   double            GetLoss() { return m_loss; }

   // Method for converting parameters to a one-dimensional array
   void              PackParameters(CRowDouble &params)
     {
      int total_params = (int)(m_mlp.GetNumNeuronsLayer1() * m_data.Cols() +     // weights1
                               m_mlp.GetNumNeuronsLayer1() +                     // bias1
                               m_mlp.GetNumNeuronsLayer2() * m_mlp.GetNumNeuronsLayer1() + // weights2
                               m_mlp.GetNumNeuronsLayer2()) ;                     // bias2
      params.Resize(total_params);

      int idx = 0;
      //  weights1
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer1(); i++)
         for(int j = 0; j < (int)m_data.Cols(); j++)
            params.Set(idx++, m_mlp.weights1[i,j]);

      //  bias1
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer1(); i++)
         params.Set(idx++, m_mlp.bias1[i,0]);

      //  weights2
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer2(); i++)
         for(int j = 0; j < m_mlp.GetNumNeuronsLayer1(); j++)
            params.Set(idx++,m_mlp.weights2[i,j]);

      //  bias2
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer2(); i++)
         params.Set(idx++,m_mlp.bias2[i,0]);
     }

   // Method for unpacking parameters from a one-dimensional array
   void              UnpackParameters(const CRowDouble &params)
     {
      int idx = 0;
      //  weights1
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer1(); i++)
         for(int j = 0; j <(int) m_data.Cols(); j++)
            m_mlp.weights1[i,j] = params[idx++];

      //  bias1
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer1(); i++)
         m_mlp.bias1[i,0] = params[idx++];

      //  weights2
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer2(); i++)
         for(int j = 0; j < m_mlp.GetNumNeuronsLayer1(); j++)
            m_mlp.weights2[i,j] = params[idx++];

      //  bias2
      for(int i = 0; i < m_mlp.GetNumNeuronsLayer2(); i++)
         m_mlp.bias2[i,0] = params[idx++];
     }

   virtual void      Grad(CRowDouble &params, double &func, CRowDouble &grad, CObject &obj) override
     {
      UnpackParameters(params);

      // Feed forward
      if(!m_mlp.FeedForward(m_data))
        {
         func = DBL_MAX;
         for(int i = 0; i < params.Size(); i++)
            grad.Set(i, DBL_MAX);
         return;
        }

      // Calculate the loss function
      func = GaussianNLLLoss(m_mlp.act2, m_target,(int) m_data.Rows());
      m_loss = func;

      // Perform backpropagation to obtain gradients
      if(!m_mlp.Backprop(m_data, m_target))
        {
         func = DBL_MAX;
         for(int i = 0; i < params.Size(); i++)
            grad.Set(i, DBL_MAX);
         return;
        }

      // Form a gradient vector
      int idx = 0;

      for(int i = 0; i < m_mlp.GetNumNeuronsLayer1(); i++)
         for(int j = 0; j <(int) m_data.Cols(); j++)
            grad.Set(idx++, m_mlp.gW1[i,j]);

      for(int i = 0; i < m_mlp.GetNumNeuronsLayer1(); i++)
         grad.Set(idx++, m_mlp.gb1[i,0]);

      for(int i = 0; i < m_mlp.GetNumNeuronsLayer2(); i++)
         for(int j = 0; j < m_mlp.GetNumNeuronsLayer1(); j++)
            grad.Set(idx++, m_mlp.gW2[i,j]);

      for(int i = 0; i < m_mlp.GetNumNeuronsLayer2(); i++)
         grad.Set(idx++, m_mlp.gb2[i,0]);
     }
  };

O L-BFGS opera com um array unidimensional de parâmetros, enquanto nossa rede armazena esses parâmetros em matrizes de pesos e vetores de vieses. Por isso, os parâmetros são vetorizados pela função PackParameters e posteriormente desempacotados de volta nas matrizes de pesos e nos vetores de vieses pela função UnpackParameters. 

Grad é o principal método chamado pelo L-BFGS a cada iteração. Ele pode ser executado várias vezes, às vezes dezenas ou centenas de vezes durante uma única iteração, para avaliar a função de perda e os gradientes de diferentes conjuntos de parâmetros. Portanto, não podemos registrar o valor da função de perda nesse método. Caso contrário, obteríamos uma grande quantidade de valores intermediários que não representam o progresso real do treinamento, mas apenas as avaliações realizadas pelo L-BFGS.

Para monitorar a convergência, utilizamos uma classe separada, MLPReportCallback, que registra o valor da função de perda somente após o término de cada iteração.

Classe MLPReportCallback

Como herda de CNDimensional_Rep da biblioteca ALGLIB, nossa classe sobrescreve a função virtual Rep para armazenar os valores da função de perda no array LossPlot, permitindo analisar posteriormente a convergência do modelo.

//+------------------------------------------------------------------+
//| Class for tracking the network training progress                 |
//+------------------------------------------------------------------+
class MLPReportCallback : public CNDimensional_Rep
  {
private:
   MLPRegressor      *mlp;

public:
                     MLPReportCallback(MLPRegressor *mlp_instance) : mlp(mlp_instance) {}
                    ~MLPReportCallback() {}

   virtual void      Rep(CRowDouble &arg, double func, CObject &obj) override
     {
      if(mlp != NULL)
        {
         ArrayResize(mlp.LossPlot, mlp.LossCount + 1, 10000);
         mlp.LossPlot[mlp.LossCount] = func;
         mlp.LossCount++;
        }
      else
        {
         Print("MLPReportCallback: Invalid pointer to MLPRegressor");
        }

     }
  };

O algoritmo L-BFGS chama o método Rep e passa a ele os parâmetros atuais do modelo (arg) e o valor da função de perda (func). É importante observar que Rep é chamado somente após a conclusão de uma iteração completa do L-BFGS, e não durante os cálculos intermediários realizados pelo método Grad da classe MLPOptimizationObjective. Assim, basta armazenar no array as informações necessárias, como o valor da função de perda.



Indicador para previsão da distribuição condicional

Para testar o modelo MLPRegressor, criaremos um indicador que prevê a esperança matemática condicional do incremento de preço e a variância condicional correspondente.

Parâmetros de entrada do indicador:

  • InpPeriodWindow: tamanho da janela usada para calcular os incrementos de preço,
  • InpSamples: número de exemplos de treinamento,
  • InpShift: número de barras usadas no teste,
  • InpLayer1: número de neurônios na camada oculta da rede,
  • InpEpochs: número máximo de épocas de treinamento do modelo.

Os conjuntos de treinamento e teste são gerados pelas funções CollectData e CollectTestData. O treinamento utiliza os preços históricos de fechamento. Os atributos são representados por uma matriz de dimensão nsamples×(window−1), em que cada linha contém os incrementos de preço das window−1 barras anteriores. A variável-alvo é um vetor de dimensão nsamples×1, contendo os incrementos de preço (close_i − close_i+1).

Os dados são normalizados com a média (train_data_mean) e o desvio-padrão (train_data_std) calculados no conjunto de treinamento. A normalização estabiliza o treinamento e melhora a convergência do modelo ao colocar os atributos de entrada e a variável-alvo em uma escala comum. A função CollectTestData forma o conjunto de teste correspondente às últimas InpShift barras e aplica os mesmos parâmetros de normalização.

Na função OnInit, é criada uma instância da classe MLPRegressor com a função de ativação especificada para a camada oculta. Após o treinamento, são exibidos o tempo gasto nessa etapa e o número total de parâmetros do modelo.

Na função OnCalculate, o método Predict gera a previsão. Os cálculos são executados somente quando surge uma nova barra.

Predict MLPRegressor

Fig. 2. Previsão da média para o par EURUSD e intervalos de confiança de 95%



Conclusão

Neste artigo, utilizamos um modelo de perceptron multicamadas que prevê os parâmetros de uma distribuição gaussiana condicional. Para isso, aplicamos uma função de perda personalizada baseada na log-verossimilhança negativa gaussiana. Essa função permite modelar de forma eficaz a heteroscedasticidade dos incrementos de preço, uma vantagem fundamental em relação às abordagens tradicionais de treinamento de redes neurais baseadas no erro quadrático médio (MSE), que pressupõe implicitamente uma variância constante da variável-alvo. 

Descrevemos em detalhes a arquitetura da classe MLPRegressor, incluindo os métodos de propagação para frente e propagação reversa, bem como sua integração com a implementação do algoritmo de otimização L-BFGS fornecida pela biblioteca ALGLIB por meio das classes MLPOptimizationObjective e MLPReportCallback. 

O indicador desenvolvido com base na classe MLPRegressor demonstra a aplicabilidade prática do modelo, permitindo que os traders obtenham, além da previsão da média, intervalos de previsão. Com isso, é possível tomar decisões mais fundamentadas, considerando não apenas os movimentos de preço esperados, mas também a incerteza associada a eles, algo especialmente valioso em mercados voláteis.

É importante destacar que o treinamento de redes neurais não deve se limitar a funções de perda convencionais, como o MSE. Experimentos com diferentes modelos probabilísticos e funções de verossimilhança abrem novas possibilidades para a análise de dados. Pesquisadores e profissionais podem adaptar os modelos de machine learning às características específicas dos mercados financeiros, contribuindo para previsões mais precisas e uma compreensão mais profunda da dinâmica do mercado.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/19683

Arquivos anexados |
MLP_LBFGS.mq5 (23.51 KB)
MLP_LBFGS.mqh (37.11 KB)
Últimos Comentários | Ir para discussão (9)
Evgeniy Chernish
Evgeniy Chernish | 25 set. 2025 em 14:29
Stanislav Korotky #:
Está tudo ótimo, exceto por um detalhe. Para o “indicador de previsão”, deveria ter sido ativado o parâmetro PLOT_SHIFT (com as linhas se estendendo para o futuro), mas, no momento, em vez da previsão, é exibido apenas o “dado histórico”. Ou talvez tenhamos interpretações diferentes do termo “previsão”.
Afinal, fazemos previsões apenas para um passo à frente. Não fazemos previsões para várias barras.

Portanto, o que o indicador exibe são todas as previsões para um passo à frente, com base no vetor de características construído com um deslocamento de uma barra para trás.

E, antes da exibição das previsões, ocorre uma amostragem para treinamento, com a qual treinamos a rede.
Stanislav Korotky
Stanislav Korotky | 25 set. 2025 em 14:58
Evgeniy Chernish #:
Afinal, a gente faz previsões apenas para o próximo passo. Não fazemos previsões para várias barras à frente.

Portanto, o que o indicador exibe são todas as previsões para um passo à frente, com base no vetor de características construído com um deslocamento de uma barra para trás.

E, antes da exibição das previsões, ocorre uma amostragem para treinamento, com a qual treinamos a rede.
Em outras palavras, mostramos em cada barra de teste onde está previsto o preço de fechamento e seu intervalo para essa barra que, por assim dizer, ainda não existe?
Evgeniy Chernish
Evgeniy Chernish | 25 set. 2025 em 15:17
Stanislav Korotky #:
Em outras palavras, estamos indicando em cada barra de teste onde está prevista a cotação de fechamento e seu intervalo para essa barra que, por assim dizer, ainda não existe?
Por que “ainda não existe”? A barra zero ainda está se formando, mas já recebemos a previsão para ela na abertura dessa barra, pois um novo vetor de características se formou. E essa previsão não muda, não é recalculada, pois depende dos preços anteriores, e não dos preços anteriores e do atual. Ou será que eu deixei passar alguma coisa e o código está prevendo o futuro?
Stanislav Korotky
Stanislav Korotky | 26 set. 2025 em 14:21
Evgeniy Chernish #:
Por que “inexistente”? A barra zero ainda está se formando, mas já recebemos a previsão para ela na abertura dessa barra, pois se formou um novo vetor de sinais. E essa previsão não muda, não é recalculada, pois depende dos preços anteriores, e não dos preços anteriores e do atual. Ou será que eu deixei passar alguma coisa e o código está prevendo o futuro?
Provavelmente não há previsão do futuro; eu me referia a outra coisa. Ao que tudo indica, o cálculo da previsão é feito logo no início da formação de uma nova barra, ou seja, na verdade, apenas quando se conhece um único preço de abertura dessa “barra”; por isso, eu a chamei, cito, de “praticamente inexistente”.
Evgeniy Chernish
Evgeniy Chernish | 26 set. 2025 em 15:58
Stanislav Korotky #:
Provavelmente não se trata de uma previsão do futuro; eu me referia a outra coisa. Ao que tudo indica, o cálculo da projeção é feito logo no início da formação de uma nova barra, ou seja, na verdade, somente quando se conhece um único preço de abertura dessa “barra”; por isso, eu a chamei, cito, de “praticamente inexistente”.
Sim, foi exatamente assim que foi concebido
Desenvolvendo um EA multimoeda (Parte 29): Aprimoramento do pipeline Desenvolvendo um EA multimoeda (Parte 29): Aprimoramento do pipeline
Vamos tornar mais prático o uso do pipeline de otimização automática, percorrendo todo o caminho desde a criação do projeto de otimização até o teste do EA final. Para facilitar a compreensão, reproduziremos passo a passo todo o processo de criação do EA final, fazendo pausas para aplicar os ajustes desejados.
Redes neurais em trading: dos Transformers aos neurônios de disparo (SpikingBrain) Redes neurais em trading: dos Transformers aos neurônios de disparo (SpikingBrain)
O framework SpikingBrain apresenta uma abordagem singular para o processamento de dados: seus neurônios reagem apenas a eventos relevantes, filtrando o ruído com eficiência. Sua arquitetura orientada a eventos reduz os custos computacionais sem perder informações essenciais sobre os movimentos do mercado. Os limiares adaptativos e a possibilidade de utilizar módulos pré-treinados conferem flexibilidade e escalabilidade ao modelo.
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.
Algoritmo do Bisão: Bison Algorithm (BIA) Algoritmo do Bisão: Bison Algorithm (BIA)
O novo método de otimização Bison Algorithm (BIA) combina duas estratégias inspiradas no comportamento dos bisões para resolver problemas contínuos com uma única função objetivo. O BIA se baseia em dois princípios fundamentais observados no comportamento dos bisões: a capacidade de deslocamento dinâmico e a estratégia defensiva.