English Русский 中文 Deutsch 日本語
preview
Métodos de ensamble para mejorar predicciones numéricas en MQL5

Métodos de ensamble para mejorar predicciones numéricas en MQL5

MetaTrader 5Estadística y análisis |
27 1
Francis Dube
Francis Dube

Introducción

El aprendizaje automático a menudo produce múltiples modelos predictivos de calidad variable. Los profesionales normalmente evalúan estos modelos y seleccionan el de mejor rendimiento para aplicaciones del mundo real. Sin embargo, este artículo explora un enfoque alternativo: reutilizar modelos aparentemente inferiores combinando sus resultados para mejorar potencialmente el desempeño predictivo general. Examinaremos varias técnicas para combinar predicciones y demostraremos su implementación en MQL5 puro. Finalmente, compararemos estos métodos y discutiremos su idoneidad para diferentes escenarios.

Para formalizar el concepto de combinar predicciones de modelos, introduzcamos algunas notaciones clave. Considere un conjunto de entrenamiento que consta de K puntos de datos, cada uno representado como un par (xi,yi), donde xi​ es un vector predictor y yi​ es la variable de respuesta escalar correspondiente que pretendemos predecir. Supongamos que tenemos N modelos entrenados, cada uno capaz de hacer predicciones. Cuando se le presenta un predictor x, el modelo n genera una predicción denotada como f_n​(x). Nuestro objetivo es construir una función de consenso f(x) que combine eficazmente estas N predicciones individuales, produciendo una predicción general más precisa que cualquier modelo individual.

Función de consenso

Esta función de consenso, a menudo denominada conjunto o metamodelo, tiene el potencial de superar a sus modelos constituyentes. En esta exploración, profundizaremos en diversas técnicas para construir modelos de conjunto efectivos y evaluaremos su implementación práctica y desempeño en MQL5.


Conjuntos basados en predicciones promediadas

Una de las técnicas más simples para combinar predicciones numéricas es el promedio simple. Al calcular la media de múltiples predicciones, a menudo podemos lograr una estimación más precisa y sólida que confiando en un solo modelo. Este enfoque es computacionalmente eficiente y fácil de implementar, lo que lo convierte en una opción práctica para una amplia gama de aplicaciones. La simplicidad de la media aritmética es su mayor fortaleza. A diferencia de los métodos de conjunto más complejos que requieren la estimación de múltiples parámetros, el promedio es inherentemente resistente al sobreajuste. El sobreajuste se produce cuando un modelo se vincula demasiado a las características específicas de los datos de entrenamiento, lo que compromete su capacidad de generalizarse a datos no vistos.

Al evitar por completo la estimación de parámetros, el promedio simple evita este problema y garantiza un rendimiento consistente incluso en conjuntos de datos pequeños o ruidosos. Por el contrario, otras técnicas de conjunto, como exploraremos más adelante, a menudo implican ajuste y optimización de parámetros, lo que puede introducir un grado de susceptibilidad al sobreajuste. Por lo tanto, si bien el promedio puede carecer de la sofisticación de los métodos de conjunto avanzados, su confiabilidad y facilidad de uso lo convierten en una herramienta esencial en el conjunto de herramientas de aprendizaje de conjunto.

Un principio matemático fundamental, basado en la desigualdad de Cauchy-Schwarz, proporciona la base teórica para la función que define un conjunto de predicciones promediadas. Esta desigualdad establece que el cuadrado de la suma de N números siempre es menor o igual a N veces la suma de sus cuadrados.

Desigualdad derivada de Cauchy

Ahora, considere un vector de predictores x utilizado para predecir una variable dependiente y. Sustituyendo a en la desigualdad con los errores cometidos por un modelo que predice y a partir de x, entonces a_n = f_n(x) - y. Si los sumandos del lado izquierdo de esta ecuación se dividen asumiendo que f(x) es el promedio de las predicciones. Factorizando N y reemplazando el término más a la derecha de la ecuación en el lado izquierdo de la desigualdad derivada de Cauchy y posteriormente dividiendo ambos lados por N^2, llegamos a la ecuación fundamental que sustenta el promedio como método de conjunto:

Conjunto promedio

Las sumas en el lado derecho de la ecuación anterior representan los errores al cuadrado de los modelos individuales. Sumando estos errores al cuadrado y dividiéndolos por el número de modelos componentes se obtiene el error cuadrático medio (Mean Squared Error, MSE) de los modelos individuales. Mientras tanto, el lado izquierdo de la ecuación representa el error al cuadrado del modelo de consenso, que se deriva de la media de las predicciones individuales.

Matemáticamente, esta desigualdad postula que, para cualquier conjunto de predictores y objetivos, el error al cuadrado de la predicción media nunca superará el error al cuadrado medio de las predicciones individuales. La igualdad sólo se logra cuando los errores de predicción de todos los modelos individuales son idénticos.

Por supuesto, este beneficio no está exento de limitaciones. La efectividad del promedio depende significativamente de la naturaleza de los modelos componentes. Si todos los modelos tienen un poder predictivo similar, promediar sus predicciones suele ser un enfoque razonable y efectivo. Sin embargo, pueden surgir problemas cuando el poder predictivo de los modelos de componentes varía ampliamente. En tales casos, el promedio puede diluir las contribuciones de los modelos más fuertes y sobreenfatizar los más débiles, lo que potencialmente reduce el desempeño predictivo general del conjunto.

El código que implementa el promedio de conjuntos está encapsulado en la clase CAvg, definida en ensemble.mqh. Esta clase, junto con todas las demás clases que implementan métodos de conjunto, depende de que el usuario proporcione una colección de modelos previamente entrenados. Estos modelos deben adherirse a la interfaz IModel, que se define de la siguiente manera:

//+------------------------------------------------------------------+
//| IModel interface defining methods for manipulation of learning   |
//| algorithms                                                       |
//+------------------------------------------------------------------+
interface IModel
  {
//train a model
   bool train(matrix &predictors,matrix&targets);
//make a prediction with a trained model
   double forecast(vector &predictors);
  };

La interfaz IModel especifica dos métodos:

  • train(): Este método contiene la lógica para entrenar un modelo.
  • forecast(): Este método define las operaciones para realizar predicciones basadas en nuevos datos de entrada.
//+------------------------------------------------------------------+
//| Compute the simple average of the predictions                    |
//+------------------------------------------------------------------+
class CAvg
  {

public:
                     CAvg(void) ;
                    ~CAvg(void) ;
   double            predict(vector &inputs, IModel* &models[]) ;

  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CAvg::CAvg(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CAvg::~CAvg(void)
  {
  }
//+------------------------------------------------------------------+
//|  Make a prediction by using consensus from multiple models       |
//+------------------------------------------------------------------+
double CAvg::predict(vector &inputs, IModel* &models[])
  {
   double output = 0.0 ;

   for(uint imodel=0 ; imodel<models.Size() ; imodel++)
     {
      output +=models[imodel].forecast(inputs) ;
     }

   output /= double(models.Size()) ;
   return output;
  }

La clase CAvg incluye un método predict(), que se invoca con un vector de datos de entrada y una matriz de modelos de componentes preentrenados. Este método devuelve un valor escalar que representa la predicción de consenso. En el caso de la clase CAvg, la predicción de consenso se calcula como la predicción media derivada de la matriz de modelos proporcionada. Al adherirse a este diseño, la clase CAvg garantiza flexibilidad y modularidad, permitiendo a los usuarios integrar sin problemas varios tipos de modelos en sus métodos de conjunto.


Combinaciones lineales sin restricciones de modelos predictivos

Cuando nos enfrentamos a un conjunto de modelos con cualidades predictivas muy variables, un método de conjunto que se puede adoptar es la regresión lineal simple. La idea es calcular la predicción de consenso como una suma ponderada de las predicciones del modelo de componentes, incluido un término constante para tener en cuenta cualquier sesgo.

Conjunto basado en regresión lineal

Este método de conjunto se implementa en la clase CLinReg. Los métodos constructor, destructor y predict() comparten las mismas firmas que las de la clase CAvg descrita anteriormente.

//+------------------------------------------------------------------+
//| Compute the linear regression of the predictions                 |
//+------------------------------------------------------------------+
class CLinReg
  {

public:

                     CLinReg(void) ;
                    ~CLinReg() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);

   double            predict(vector &inputs, IModel* &models[]) ;

private:
   OLS *m_linreg ;   // The linear regression object
  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLinReg::CLinReg(void)
  {
   m_linreg = new OLS();
  }
//+------------------------------------------------------------------+
//| Fit the consensus model from saved models                        |
//+------------------------------------------------------------------+
bool CLinReg::fit(matrix &train_vars,vector &train_targets,IModel* &models[])
  {

   matrix independent(train_vars.Rows(),models.Size()+1);

   for(ulong i=0 ; i<independent.Rows() ; i++)     // Build the design matrix
     {
      independent[i][models.Size()] = 1.0;
      vector ins = train_vars.Row(i);
      for(uint imodel=0 ; imodel<models.Size() ; imodel++)
         independent[i][imodel] = models[imodel].forecast(ins) ;

     }
   return m_linreg.Fit(train_targets,independent);
  }
//+------------------------------------------------------------------+
//|  Destructor                                                      |
//+------------------------------------------------------------------+
CLinReg::~CLinReg(void)
  {
   if(CheckPointer(m_linreg)==POINTER_DYNAMIC)
      delete m_linreg ;
  }
//+------------------------------------------------------------------+
//| Predict                                                          |
//+------------------------------------------------------------------+
double CLinReg::predict(vector &inputs, IModel* &models[])
  {
   vector args = vector::Zeros(models.Size());

   for(uint i = 0; i<models.Size(); i++)
      args[i] = models[i].forecast(inputs);

   return m_linreg.Predict(args);
  }

Sin embargo, la clase CLinReg introduce un método fit(), que especifica las operaciones para entrenar el modelo de consenso.

El método fit() toma como entrada:

  • Matriz de predictores.
  • Objetivos vectoriales.
  • Una variedad de modelos de componentes.

Dentro de fit(), se utiliza una instancia de la clase OLS para representar el modelo de regresión de consenso. La variable matricial independiente sirve como matriz de diseño, construida a partir de los errores al cuadrado de las predicciones realizadas por los modelos de componentes individuales, aumentados por un término constante (una columna de unos). Cuando se invoca el método predict() de CLinReg, devuelve el resultado de utilizar los errores de predicción de los modelos de componentes como entradas para el modelo de regresión de consenso.

La combinación de modelos como una suma ponderada de predicciones de componentes funciona bien en escenarios específicos y poco frecuentes. Sin embargo, este enfoque a menudo se pasa por alto en aplicaciones del mundo real por dos razones principales:

  • Riesgo de sobreajuste: Los pesos en el modelo de consenso son parámetros que deben optimizarse. Si el conjunto incluye muchos modelos de componentes, el proceso de optimización puede generar un sobreajuste significativo, reduciendo la capacidad del modelo para generalizarse a datos no vistos.
  • Colinealidad: Si dos o más modelos producen predicciones similares, la colinealidad puede causar inestabilidad en las estimaciones de peso. Este problema surge porque los pesos de los modelos con un rendimiento similar pueden sumar una constante y esos modelos se comportan de manera similar solo para los casos encontrados durante el entrenamiento.

Sin embargo, esta suposición a menudo no se cumple en situaciones del mundo real. Cuando ocurre un caso fuera de la muestra, los modelos que previamente generaron predicciones similares pueden reaccionar de manera diferente, lo que potencialmente puede llevar al modelo de consenso a producir resultados extremos y poco confiables.


Combinaciones lineales restringidas de modelos sesgados

El uso de una regresión simple como base para combinar múltiples modelos predictivos a veces puede conducir a un modelo inestable con pesos extremos. Este problema suele surgir cuando los coeficientes de regresión tienen signos opuestos, lo cual es necesario para equilibrar los valores para que los datos se ajusten bien. Por ejemplo, un coeficiente de un par de modelos correlacionados solo puede llevarse a un valor positivo grande si su contraparte se lleva a un valor negativo pequeño. Para evitar estos valores extremos, podemos restringir los coeficientes de regresión para evitar valores negativos extremos. Este enfoque también reduce el número de grados de libertad en el proceso de optimización, lo que hace que el modelo sea más estable y menos propenso al sobreajuste.

Este método de conjunto se implementa en la clase Cbiased. Incluye los ahora familiares métodos fit() y predict() que se encuentran en otras implementaciones de conjuntos.

//+------------------------------------------------------------------+
//|Compute the optimal linear combination of the predictions         |
//|subject to the constraints that the weights are all nonnegative.  |
//|A constant term is also included.                                 |
//|This is appropriate for biased predictors                         |
//+------------------------------------------------------------------+
class Cbiased:public PowellsMethod
  {

public:

                     Cbiased(void) ;
                    ~Cbiased() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   vector m_coefs ;    // Computed coefficients here
   int biased_ncases ;
   int biased_nvars ;
   matrix biased_x ;
   vector biased_y ;
   virtual double    func(vector &p,int n=0);
  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
Cbiased::Cbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
Cbiased::~Cbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Function to be optimized                                         |
//+------------------------------------------------------------------+
double Cbiased::func(vector &p,int n = 0)
  {

   double  err, pred,diff, penalty ;
// Compute criterion
   err = 0.0 ;
   for(int i=0 ; i<biased_ncases ; i++)
     {
      pred = p[p.Size()-1] ;                    // Will cumulate prediction
      for(int j=0 ; j<biased_nvars ; j++)       // For all model outputs
         pred += biased_x[i][j] * p[j] ;        // Weight them per call
      diff = pred - biased_y[i] ;               // Predicted minus true
      err += diff * diff ;                      // Cumulate squared error
     }

   penalty = 0.0 ;
   for(int j=0 ; j<biased_nvars ; j++)
     {
      if(p[j] < 0.0)
         penalty -= 1.e30 * p[j] ;
     }

   return err + penalty ;
  }
//+------------------------------------------------------------------+
//| Fit the consensus model                                          |
//+------------------------------------------------------------------+
bool Cbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[])
  {
   biased_ncases = int(train_vars.Rows());
   biased_nvars = int(models.Size());
   biased_x = matrix::Zeros(biased_ncases,biased_nvars);

   biased_y = train_targets;

   m_coefs = vector::Zeros(biased_nvars+1);

   for(int i = 0; i<biased_ncases; i++)
     {
      vector ins = train_vars.Row(i);
      for(int j = 0; j<biased_nvars; j++)
         biased_x[i][j] = models[j].forecast(ins);
     }

   m_coefs.Fill(1.0/double(biased_nvars));
   m_coefs[m_coefs.Size()-1] = 0.0;

   int iters = Optimize(m_coefs,int(m_coefs.Size()));

   double sum = m_coefs.Sum();

   m_coefs/=sum;

   return true;
  }
//+------------------------------------------------------------------+
//| Make prediction with consensus model                             |
//+------------------------------------------------------------------+
double Cbiased::predict(vector &inputs,IModel* &models[])
  {
   double output=0.0;
   for(uint imodel=0 ; imodel<models.Size() ; imodel++)
     {
      output += m_coefs[imodel] * models[imodel].forecast(inputs);
     }
   return output;
  }

Sin embargo, la diferencia clave en Cbiased radica en cómo se optimizan los pesos.

La optimización de pesos se realiza utilizando el método de Powell para la minimización de funciones. Es por esto que la clase Cbiased es un descendiente de la clase PowellsMethod. La función de criterio se implementa en el método func(), que itera a través de los datos de entrenamiento, acumulando errores al cuadrado utilizando los pesos proporcionados. Para cada muestra del conjunto de datos:

  • Las predicciones de los modelos de componentes se ponderan de acuerdo con los pesos actuales y un término constante.
  • Se suman las diferencias al cuadrado entre las predicciones y los valores objetivo.

La función de criterio finaliza verificando si alguno de los pesos de prueba es negativo. Si algún peso es negativo se aplica una penalización. La función devuelve el error total más cualquier penalización de los pesos negativos. Este tipo de método de conjunto es más apropiado cuando se sabe que algunos modelos de componentes están sesgados. En este contexto, el sesgo se refiere a modelos que producen consistentemente predicciones que son demasiado altas o demasiado bajas en comparación con sus valores objetivo correspondientes y que a menudo muestran una tendencia notable. Al restringir los pesos, Cbiased reduce eficazmente la influencia de los modelos sesgados, lo que conduce a una predicción de conjunto más equilibrada y precisa. En la siguiente sección, presentaremos un método adecuado para conjuntos de modelos que presentan poco o ningún sesgo, donde el enfoque está en agregar predicciones de modelos con un rendimiento comparable.


Combinaciones restringidas de modelos imparciales

Cuando se sabe que un conjunto de modelos de componentes no tiene sesgo significativo en sus predicciones, no es necesario incluir un término constante en el modelo de consenso. Eliminar el término constante ayuda a reducir la propensión del modelo a sobreajustar los datos. Además, este enfoque garantiza que los pesos de los modelos nunca sean negativos, como se discutió en métodos anteriores. Además, se impone una restricción adicional a los pesos: deben sumar uno. Esta restricción proporciona dos beneficios clave:

  • Garantizar un modelo de consenso imparcial: siempre que los modelos de componentes sean razonablemente imparciales, exigir que los pesos sumen uno garantiza que el modelo de consenso también siga siendo imparcial.
  • Interpolación entre predicciones: La restricción de suma a uno garantiza que la predicción de consenso sea una interpolación entre las predicciones de los modelos componentes. Esto garantiza que la predicción final no se desvíe drásticamente de las predicciones individuales, evitando resultados extremos que podrían surgir de pesos extremos.

Esto se ilustra con la siguiente ecuación:

Modelo lineal restringido

El código que implementa este método de conjunto es en gran medida idéntico a las implementaciones anteriores. La principal diferencia con la clase CUnbiased radica en que la función se minimiza.

//+------------------------------------------------------------------+
//|Compute the optimal linear combination of the predictions         |
//|subject to the constraints that the weights are all nonnegative   |
//|and they sum to one.  This is appropriate for unbiased predictors.|
//+------------------------------------------------------------------+
class CUnbiased:public PowellsMethod
  {

public:

                     CUnbiased(void) ;
                    ~CUnbiased() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   vector m_coefs ;    // Computed coefficients here
   int unbiased_ncases ;
   int unbiased_nvars ;
   matrix unbiased_x ;
   vector unbiased_y ;
   virtual double    func(vector &p,int n=0);
  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CUnbiased::CUnbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CUnbiased::~CUnbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Function to be optimized                                         |
//+------------------------------------------------------------------+
double CUnbiased::func(vector &p,int n = 0)
  {

   double sum, err, pred,diff, penalty ;

// Normalize weights to sum to one
   sum = p.Sum() ;

   if(sum < 1.e-60)    // Should almost never happen
      sum = 1.e-60 ;   // But be prepared to avoid division by zero

   vector   unbiased_work = p / sum ;

// Compute criterion
   err = 0.0 ;
   for(int i=0 ; i<unbiased_ncases ; i++)
     {
      pred = 0.0 ;                                       // Will cumulate prediction
      for(int j=0 ; j<unbiased_nvars ; j++)              // For all model outputs
         pred += unbiased_x[i][j] * unbiased_work[j] ;   // Weight them per call
      diff = pred - unbiased_y[i] ;                      // Predicted minus true
      err += diff * diff ;                               // Cumulate squared error
     }

   penalty = 0.0 ;
   for(int j=0 ; j<unbiased_nvars ; j++)
     {
      if(p[j] < 0.0)
         penalty -= 1.e30 * p[j] ;
     }

   return err + penalty ;
  }
//+------------------------------------------------------------------+
//| Fit the consensus model                                          |
//+------------------------------------------------------------------+
bool CUnbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[])
  {
   unbiased_ncases = int(train_vars.Rows());
   unbiased_nvars = int(models.Size());
   unbiased_x = matrix::Zeros(unbiased_ncases,unbiased_nvars);

   unbiased_y = train_targets;

   m_coefs = vector::Zeros(unbiased_nvars);

   for(int i = 0; i<unbiased_ncases; i++)
     {
      vector ins = train_vars.Row(i);
      for(int j = 0; j<unbiased_nvars; j++)
         unbiased_x[i][j] = models[j].forecast(ins);
     }

   m_coefs.Fill(1.0/double(unbiased_nvars));

   int iters = Optimize(m_coefs);

   double sum = m_coefs.Sum();

   m_coefs/=sum;

   return true;
  }
//+------------------------------------------------------------------+
//| Make prediction with consensus model                             |
//+------------------------------------------------------------------+
double CUnbiased::predict(vector &inputs,IModel* &models[])
  {
   double output=0.0;
   for(uint imodel=0 ; imodel<models.Size() ; imodel++)
     {
      output += m_coefs[imodel] * models[imodel].forecast(inputs);
     }
   return output;
  }

Esta función incorpora las restricciones adicionales discutidas anteriormente, específicamente la no negatividad de los pesos y el requisito de que sumen uno.


Combinaciones de modelos predictivos ponderadas por la varianza

Otro método para combinar predicciones de modelos de componentes se basa en pesos óptimos que están determinados por la precisión de predicción de cada modelo. Esta técnica implica asignar pesos más pequeños a los modelos con errores de predicción mayores y pesos más grandes a los modelos con errores de predicción menores. Este método es particularmente efectivo cuando hay una variabilidad significativa en la calidad de los modelos de componentes. Sin embargo, si los modelos están altamente correlacionados, esta técnica puede no ser ideal y debería considerarse otro método de conjunto.

La idea detrás de la ponderación según la calidad del modelo se basa en la teoría de que si los modelos son imparciales y no están correlacionados, asignar pesos inversamente proporcionales a los errores de los modelos minimizará el error cuadrático esperado.

Conjunto ponderado por varianza

Para lograr esto, se calcula el peso relativo de cada modelo en función del inverso de su error y luego se escalan los pesos para garantizar que sumen uno.

Ecuación de peso

El conjunto de modelos ponderados por varianza se implementa en la clase CWeighted. En el método fit(), para cada muestra de entrenamiento:

  • Se calcula la predicción de cada modelo de componente.
  • Y el error al cuadrado de cada predicción se acumula.

//+------------------------------------------------------------------+
//| Compute the variance-weighted average of the predictions         |
//+------------------------------------------------------------------+
class CWeighted
  {
public:

                     CWeighted(void) ;
                    ~CWeighted() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   vector m_coefs ;    // Computed coefficients here

  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWeighted::CWeighted(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWeighted::~CWeighted(void)
  {
  }
//+------------------------------------------------------------------+
//| Fit a consensus model                                            |
//+------------------------------------------------------------------+
bool CWeighted::fit(matrix &train_vars,vector &train_targets,IModel* &models[])
  {
   m_coefs = vector::Zeros(models.Size());

   m_coefs.Fill(1.e-60);
   double diff = 0.0;
   for(ulong i = 0; i<train_vars.Rows(); i++)
     {
      vector ins = train_vars.Row(i);
      for(ulong j = 0; j<m_coefs.Size(); j++)
        {
         diff = models[j].forecast(ins) - train_targets[i];
         m_coefs[j] += (diff*diff);
        }
     }

   m_coefs=1.0/m_coefs;

   m_coefs/=m_coefs.Sum();

   return true;
  }
//+------------------------------------------------------------------+
//| Make a prediction with the consensus model                       |
//+------------------------------------------------------------------+
double CWeighted::predict(vector &inputs,IModel* &models[])
  {
   double output = 0.0;

   for(uint i = 0; i<models.Size(); i++)
      output+=m_coefs[i]*models[i].forecast(inputs);

   return output;
  }

Una vez hecho esto para todas las muestras de entrenamiento, se utiliza el error total de cada modelo para calcular su peso. Luego, estos pesos se suman y se escalan para garantizar que el peso total sume uno. Este enfoque garantiza que los modelos con menores errores tengan más influencia en la predicción final del conjunto, lo que potencialmente puede mejorar la predicción, especialmente en escenarios donde los modelos exhiben distintos grados de precisión predictiva.


Combinaciones interpoladas basadas en redes neuronales de regresión general

Los métodos de conjunto que hemos analizado hasta ahora funcionan bien cuando el modelo de consenso se entrena con datos limpios. Sin embargo, cuando los datos de entrenamiento son ruidosos, el modelo puede sufrir una generalización deficiente. Para abordar esta cuestión, un método de regresión eficaz es la red neuronal de regresión general (General Regression Neural Network, GRNN). La ventaja destacada de GRNN sobre la regresión tradicional es su menor susceptibilidad al sobreajuste. Esto se debe a que los parámetros de GRNN tienen un impacto relativamente menor en el modelo en comparación con las técnicas de regresión tradicionales. Si bien esta ganancia en generalización se produce a costa de cierta precisión, las GRNN pueden modelar relaciones complejas y no lineales, lo que proporciona una herramienta útil cuando los datos exhiben tales características.

Las GRNN producen predicciones que son interpolaciones entre los valores objetivo en los datos de entrenamiento. La interpolación está determinada por un peso que define cómo un caso fuera de la muestra difiere de los casos conocidos dentro de la muestra. Cuanto más similares sean las muestras, mayor será el peso relativo asignado. Aunque una GRNN puede describirse como una operación de suavizado debido a su interpolación de muestras desconocidas en el espacio muestral conocido, sus fundamentos teóricos se basan en la estadística.

Cuando se presenta un conjunto de datos (que incluye predicciones de modelos de componentes y sus objetivos correspondientes), la predicción de consenso de una GRNN es el error cuadrático mínimo esperado, que se da por la expectativa condicional.

Expectativa condicional

Dado que la densidad conjunta de los datos de entrenamiento normalmente es desconocida, no podemos utilizar directamente la fórmula para la expectativa condicional. En lugar de ello, nos basamos en estimaciones de las densidades conjuntas derivadas de los datos de entrenamiento, lo que conduce a la forma de GRNN que se muestra a continuación.

Fórmula GRNN

Antes de presentar el código para el método de conjunto basado en GRNN, primero debemos analizar la implementación de GRNN. El código para GRNN se define en grnn.mqh, que contiene la definición de la clase CGrnn.

//+------------------------------------------------------------------+
//| General regression neural network                                |
//+------------------------------------------------------------------+
class CGrnn
  {
public:
                     CGrnn(void);
                     CGrnn(int num_outer, int num_inner, double start_std);
                    ~CGrnn(void);
   bool              fit(matrix &predictors,matrix &targets);
   vector            predict(vector &predictors);
   //double            get_mse(void);
private:
   bool              train(void);
   double            execute(void);
   ulong             m_inputs,m_outputs;
   int               m_inner,m_outer;
   double            m_start_std;
   ulong             m_rows,m_cols;
   bool              m_trained;
   vector            m_sigma;
   matrix            m_targets,m_preds;
  };

La implementación de GRNN para tareas de regresión involucra varios componentes clave. El constructor inicializa parámetros como el número de iteraciones internas y externas y la desviación estándar inicial para los pesos sigma.

//+------------------------------------------------------------------+
//| Default constructor                                              |
//+------------------------------------------------------------------+
CGrnn::CGrnn(void)
  {
   m_inner = 100;
   m_outer = 10;
   m_start_std = 3.0;
  }
//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CGrnn::CGrnn(int num_outer,int num_inner,double start_std)
  {
   m_inner = num_inner;
   m_outer = num_outer;
   m_start_std = start_std;
  }

El método de ajuste almacena los datos de entrenamiento, incluidos los predictores de entrada y los valores objetivo, e inicializa los pesos sigma. Luego entrena el modelo GRNN optimizando iterativamente los pesos sigma utilizando un enfoque de recocido simulado. Durante el entrenamiento, se perturban los pesos sigma y se calcula el error de validación cruzada para los pesos perturbados. La perturbación se acepta o rechaza en función del error y de un parámetro de temperatura, y la temperatura se reduce gradualmente para centrar la búsqueda.

//+------------------------------------------------------------------+
//| Fit data to a model                                              |
//+------------------------------------------------------------------+
bool CGrnn::fit(matrix &predictors,matrix &targets)
  {
   m_targets = targets;
   m_preds = predictors;
   m_trained = false;
   m_rows = m_preds.Rows();
   m_cols = m_preds.Cols();
   m_sigma = vector::Zeros(m_preds.Cols());

   if(m_targets.Rows() != m_preds.Rows())
     {
      Print(__FUNCTION__, " invalid inputs ");
      return false;
     }

   m_trained = train();

   return m_trained;
  }

El método de predicción calcula la distancia entre el vector de entrada y cada punto de datos de entrenamiento, pondera los puntos de datos de entrenamiento en función de su distancia desde la entrada y calcula la salida prevista como un promedio ponderado de los valores objetivo de los puntos de datos de entrenamiento. Los pesos sigma determinan la influencia de cada punto de datos de entrenamiento en la predicción.

//+------------------------------------------------------------------+
//| Make a prediction with a trained model                           |
//+------------------------------------------------------------------+
vector CGrnn::predict(vector &predictors)
  {
   if(!m_trained)
     {
      Print(__FUNCTION__, " no trained model available for predictions ");
      return vector::Zeros(1);
     }

   if(predictors.Size() != m_cols)
     {
      Print(__FUNCTION__, " invalid inputs ");
      return vector::Zeros(1);
     }

   vector output  = vector::Zeros(m_targets.Cols());
   double diff,dist,psum=0.0;

   for(ulong i = 0; i<m_rows; i++)
     {
      dist  = 0.0;
      for(ulong j = 0; j<m_cols; j++)
        {
         diff  = predictors[j]  - m_preds[i][j];
         diff/= m_sigma[j];
         dist += (diff*diff);
        }
      dist  = exp(-dist);
      if(dist< EPS1)
         dist = EPS1;
      for(ulong k = 0; k<m_targets.Cols(); k++)
         output[k] += dist * m_targets[i][k];
      psum += dist;
     }
   output/=psum;
   return output;
  }

La validación cruzada se utiliza para evaluar el rendimiento del modelo y optimizar los pesos sigma, mientras que el recocido simulado sirve como un algoritmo de optimización metaheurística para encontrar los pesos sigma óptimos. En última instancia, GRNN realiza una interpolación basada en kernel, donde la predicción es una interpolación ponderada entre los puntos de datos de entrenamiento.

El conjunto basado en GRNN se implementa como la clase CGenReg.

//+------------------------------------------------------------------+
//| Compute the General Regression of the predictions                |
//+------------------------------------------------------------------+
class CGenReg
  {

public:

                     CGenReg(void) ;
                    ~CGenReg(void) ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   CGrnn *grnn ;       // The GRNN object
   vector m_work ;     // Work vector nmodels long
   vector            m_targs;
   matrix            m_vars;
  } ;

La clase CGenReg utiliza un objeto CGrnn para modelar relaciones complejas entre las predicciones de modelos individuales y los valores objetivo reales. En el método de ajuste, primero se almacenan los datos de entrenamiento, incluidos los valores objetivo (train_targets) y las variables de entrada (train_vars). Luego, reúne las predicciones individuales de cada modelo y crea una matriz (preds) donde cada fila representa una muestra de entrenamiento y cada columna contiene la predicción del modelo correspondiente en el conjunto. El objeto CGrnn se entrena utilizando la matriz de predicciones individuales (preds) como entrada y los valores objetivo reales (targ) como salida.

//+------------------------------------------------------------------+
//| Fit consensus model                                              |
//+------------------------------------------------------------------+
bool CGenReg::fit(matrix & train_vars, vector &train_targets,IModel* &models[])
  {
   m_targs = train_targets;
   m_vars = train_vars;

   m_work = vector::Zeros(models.Size());

   matrix targ = matrix::Zeros(train_targets.Size(),1);

   if(!targ.Col(train_targets,0))
     {
      Print(__FUNCSIG__, " error adding column ", GetLastError());
      return false;
     }

   matrix preds(m_vars.Rows(),models.Size());
   for(ulong i = 0; i<m_vars.Rows(); i++)
     {
      vector ins = m_vars.Row(i);
      for(uint j = 0; j< models.Size(); j++)
        {
         preds[i][j] = models[j].forecast(ins);
        }
     }

   return grnn.fit(preds,targ);
  }

En el método de predicción, la clase recopila predicciones de cada modelo para una nueva entrada (inputs) y las almacena en un vector de trabajo (m_work). Luego, el CGrnn entrenado se utiliza para predecir el resultado final en función de estas predicciones individuales. El método devuelve el primer elemento del vector de salida previsto como predicción final.

//+------------------------------------------------------------------+
//| Make a prediction                                                |
//+------------------------------------------------------------------+
double CGenReg::predict(vector &inputs,IModel* &models[])
  {
   vector output;
   for(uint i = 0; i<models.Size(); i++)
      m_work[i] = models[i].forecast(inputs);
   output = grnn.predict(m_work);
   return output[0];
  }


Conclusión: Comparación de métodos de conjunto

Se han presentado varios métodos de conjunto y se han analizado brevemente sus fortalezas y debilidades inherentes. Para concluir este artículo, investigaremos cómo se comparan estos métodos cuando se aplican a datos reales. Esta comparación se implementa como un script de MetaTrader 5 llamado Ensemble_Demo.mq5.

El script genera varios grupos sintéticos de conjuntos de datos. El primer grupo consta de conjuntos de datos utilizados para entrenar modelos de referencia. Los modelos entrenados con dichos datos se distinguen como buenos modelos y los datos en sí se consideran limpios. Se genera un segundo grupo de conjuntos de datos para entrenar modelos malos, que se consideran inferiores a los modelos buenos entrenados con datos limpios.

El último grupo de conjuntos de datos se utiliza para entrenar modelos que se consideran sesgados. Estos modelos están sesgados en comparación con los buenos modelos mencionados anteriormente. Se combina un conjunto de datos parcial de cada grupo para simular datos ruidosos.

El script permite a los usuarios especificar cuántos modelos buenos, malos y sesgados se entrenan. El usuario también tiene control sobre la cantidad de muestras que componen los datos de entrenamiento, lo que permite evaluar cómo el tamaño de la muestra afecta el rendimiento de los métodos de conjunto. Por último, los usuarios pueden elegir entrenar un modelo de conjunto utilizando datos limpios configurando el parámetro TrainCombinedModelsOnCleanData como verdadero, o entrenarlo utilizando datos ruidosos configurandolo como falso.

Los modelos son redes neuronales de propagación hacia adelante implementadas por la clase FFNN en mlffnn.mqh.

//+------------------------------------------------------------------+
//| Class for a basic feed-forward neural network                    |
//+------------------------------------------------------------------+
class FFNN
  {
protected:

   bool              m_trained;             // flag noting if neural net successfully trained
   matrix            m_weights[];           // layer weights
   matrix            m_outputs[];           // hidden layer outputs
   matrix            m_result;              // training result
   uint              m_epochs;              // number of epochs
   ulong             m_num_inputs;          // number of input variables for nn
   ulong             m_layers;              // number of layers of neural net
   ulong             m_hidden_layers;       // number of hidden layers
   ulong             m_hidden_layer_size[]; // node config for layers
   double            m_learn_rate;          // learning rate
   ENUM_ACTIVATION_FUNCTION m_act_fn;       // activation function
   //+------------------------------------------------------------------+
   //| Initialize the neural network structure                          |
   //+------------------------------------------------------------------+

   virtual bool      create(void)
     {
      if(m_layers - m_hidden_layers != 1)
        {
         Print(__FUNCTION__,"  Network structure misconfiguration ");
         return false;
        }

      for(ulong i = 0; i<m_layers; i++)
        {
         if(i==0)
           {
            if(!m_weights[i].Init(m_num_inputs+1,m_hidden_layer_size[i]))
              {
               Print(__FUNCTION__," ",__LINE__," ", GetLastError());
               return false;
              }
           }
         else
            if(i == m_layers-1)
              {
               if(!m_weights[i].Init(m_hidden_layer_size[i-1]+1,1))
                 {
                  Print(__FUNCTION__," ",__LINE__," ", GetLastError());
                  return false;
                 }
              }
            else
              {
               if(!m_weights[i].Init(m_hidden_layer_size[i-1]+1,m_hidden_layer_size[i]))
                 {
                  Print(__FUNCTION__," ",__LINE__," ", GetLastError());
                  return false;
                 }
              }
        }

      return true;
     }
   //+------------------------------------------------------------------+
   //| Calculate output  from all layers                                |
   //+------------------------------------------------------------------+

   virtual matrix    calculate(matrix &data)
     {
      if(data.Cols() != m_weights[0].Rows()-1)
        {
         Print(__FUNCTION__," input data not compatible with network structure ");
         return matrix::Zeros(0,0);
        }

      matrix temp = data;

      for(ulong i = 0; i<m_hidden_layers; i++)
        {
         if(!temp.Resize(temp.Rows(), m_weights[i].Rows()) ||
            !temp.Col(vector::Ones(temp.Rows()), m_weights[i].Rows() - 1))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            matrix::Zeros(0,0);
           }

         m_outputs[i]=temp.MatMul(m_weights[i]);

         if(!m_outputs[i].Activation(temp, m_act_fn))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return matrix::Zeros(0,0);
           }

        }

      if(!temp.Resize(temp.Rows(), m_weights[m_hidden_layers].Rows()) ||
         !temp.Col(vector::Ones(temp.Rows()), m_weights[m_hidden_layers].Rows() - 1))
        {
         Print(__FUNCTION__," ",__LINE__," ", GetLastError());
         return matrix::Zeros(0,0);
        }

      return temp.MatMul(m_weights[m_hidden_layers]);
     }
   //+------------------------------------------------------------------+
   //|  Backpropagation method                                          |
   //+------------------------------------------------------------------+

   virtual bool      backprop(matrix &data, matrix& targets, matrix &result)
     {
      if(targets.Rows() != result.Rows() ||
         targets.Cols() != result.Cols())
        {
         Print(__FUNCTION__," invalid function parameters ");
         return false;
        }
      matrix loss = (targets - result) * 2;
      matrix gradient = loss.MatMul(m_weights[m_hidden_layers].Transpose());
      matrix temp;

      for(long i = long(m_hidden_layers-1); i>-1; i--)
        {
         if(!m_outputs[i].Activation(temp, m_act_fn))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         if(!temp.Resize(temp.Rows(), m_weights[i+1].Rows()) ||
            !temp.Col(vector::Ones(temp.Rows()), m_weights[i+1].Rows() - 1))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         m_weights[i+1] = m_weights[i+1] + temp.Transpose().MatMul(loss) * m_learn_rate;

         if(!m_outputs[i].Derivative(temp, m_act_fn))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         if(!gradient.Resize(gradient.Rows(), gradient.Cols() - 1))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         loss = gradient * temp;

         gradient = (i>0)?loss.MatMul(m_weights[i].Transpose()):gradient;
        }

      temp = data;
      if(!temp.Resize(temp.Rows(), m_weights[0].Rows()) ||
         !temp.Col(vector::Ones(temp.Rows()), m_weights[0].Rows() - 1))
        {
         Print(__FUNCTION__," ",__LINE__," ", GetLastError());
         return false;
        }

      m_weights[0] = m_weights[0] + temp.Transpose().MatMul(loss) * m_learn_rate;

      return true;
     }

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+

                     FFNN(ulong &layersizes[], ulong num_layers = 3)
     {
      m_trained = false;
      m_layers = num_layers;
      m_hidden_layers = m_layers - 1;

      ArrayCopy(m_hidden_layer_size,layersizes,0,0,int(m_hidden_layers));

      ArrayResize(m_weights,int(m_layers));

      ArrayResize(m_outputs,int(m_hidden_layers));
     }
   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+

                    ~FFNN(void)
     {
     }
   //+------------------------------------------------------------------+
   //| Neural net training method                                       |
   //+------------------------------------------------------------------+

   bool              fit(matrix &data, matrix &targets,double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs)
     {
      m_learn_rate = learning_rate;
      m_act_fn = act_fn;
      m_epochs = num_epochs;
      m_num_inputs = data.Cols();
      m_trained = false;

      if(!create())
         return false;

      for(uint ep = 0; ep < m_epochs; ep++)
        {
         m_result = calculate(data);

         if(!backprop(data, targets,m_result))
            return m_trained;
        }

      m_trained = true;
      return m_trained;
     }
   //+------------------------------------------------------------------+
   //| Predict method                                                   |
   //+------------------------------------------------------------------+

   matrix            predict(matrix &data)
     {
      if(m_trained)
         return calculate(data);
      else
         return matrix::Zeros(0,0);
     }

  };
//+------------------------------------------------------------------+

La clase FFNN define un perceptrón multicapa (Multi-Layer Perceptron, MLP), un tipo de red neuronal artificial utilizada para tareas de aprendizaje supervisado. Contiene varias propiedades como:

  • m_trained, un indicador booleano que indica si la red ha sido entrenada exitosamente;
  • m_weights, una matriz de matrices que almacenan los pesos entre cada capa;
  • m_outputs, una matriz de matrices que contienen las salidas de cada capa oculta;
  • m_result, una matriz que contiene la salida final de la red después del entrenamiento;
  • m_epochs, el número de épocas de entrenamiento (iteraciones);
  • m_num_inputs, el número de variables de entrada para la red;
  • m_layers, el número total de capas en la red, incluidas las capas de entrada y salida;
  • m_hidden_layers, el número de capas ocultas en la red;
  • m_hidden_layer_size, una matriz que define el número de nodos en cada capa oculta;
  • m_learn_rate, la tasa de aprendizaje utilizada para las actualizaciones de peso durante el entrenamiento;
  • m_act_fn, la función de activación utilizada en las capas ocultas.

La clase incluye métodos privados y públicos. Métodos privados como:

  • create, que inicializa la estructura de la red asignando memoria para las matrices de peso y las salidas de la capa oculta en función de la configuración especificada;
  • calculate, que propaga los datos de entrada a través de la red, aplicando pesos y funciones de activación para calcular la salida;
  • backprop, que implementa el algoritmo de retropropagación, ajustando los pesos en función del error entre las salidas previstas y reales.

Los métodos públicos incluyen:

  • FFNN (constructor), que inicializa la red con el número especificado de capas y tamaños de capas ocultas;
  • ~FFNN (destructor), que libera los recursos asignados para la red;
  • fit, que entrena la red en un conjunto de datos determinado, ajustando los pesos a través de retropropagación durante el número especificado de épocas;
  • predict, que utiliza la red entrenada para generar predicciones para nuevos datos de entrada, realizando de manera efectiva una propagación hacia adelante.

En el script, la clase CMlfn implementa la interfaz IModel basada en una instancia de FFNN. A continuación se muestra una breve exposición de cómo ejecutar el script en diferentes configuraciones.

//+------------------------------------------------------------------+
//|                                                Ensemble_Demo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<mlffnn.mqh>
#include<ensemble.mqh>
#include<np.mqh>
//--- input parameters
input int      NumGoodModels=3;
input int      NumBiasedModels=7;
input int      NumBadModels=5;
input int      NumSamples=20;
input int      NumAttempts=1;
input double   VarParam=3.0;//variance parameter
input bool     TrainCombinedModelsOnCleanData = true;
//+------------------------------------------------------------------+
//| Clean up dynamic array pointers                                  |
//+------------------------------------------------------------------+
void cleanup(IModel* &array[])
  {
   for(uint i = 0; i<array.Size(); i++)
      if(CheckPointer(array[i])==POINTER_DYNAMIC)
         delete array[i];
  }
//+------------------------------------------------------------------+
//| IModel implementation of Multilayered iterative algo of GMDH     |
//+------------------------------------------------------------------+
class CMlfn:public IModel
  {
private:
   FFNN              *m_mlfn;
   double             m_learningrate;
   ENUM_ACTIVATION_FUNCTION    m_actfun;
   uint               m_epochs;
   ulong              m_layer[3];

public:
                     CMlfn();
                    ~CMlfn(void);
   void              setParams(double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs);
   bool              train(matrix &predictors,matrix&targets);
   double            forecast(vector &predictors);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CMlfn::CMlfn(void)
  {
   m_learningrate=0.01;
   m_actfun=AF_SOFTMAX;
   m_epochs= 100;
   m_layer[0] = 2;
   m_layer[1] = 2;
   m_layer[2] = 1;
   m_mlfn = new FFNN(m_layer);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CMlfn::~CMlfn(void)
  {
   if(CheckPointer(m_mlfn) == POINTER_DYNAMIC)
      delete m_mlfn;
  }
//+------------------------------------------------------------------+
//| Set other hyperparameters of the model                           |
//+------------------------------------------------------------------+
void CMlfn::setParams(double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs)
  {
   m_learningrate=learning_rate;
   m_actfun=act_fn;
   m_epochs= num_epochs;
  }
//+------------------------------------------------------------------+
//| Fit a model to the data                                          |
//+------------------------------------------------------------------+
bool CMlfn::train(matrix &predictors,matrix &targets)
  {
   return m_mlfn.fit(predictors,targets,m_learningrate,m_actfun,m_epochs);
  }
//+------------------------------------------------------------------+
//| Make a prediction with the trained model                         |
//+------------------------------------------------------------------+
double CMlfn::forecast(vector &predictors)
  {
   matrix preds(1,predictors.Size());

   if(!preds.Row(predictors,0))
     {
      Print(__FUNCTION__, " error inserting row ", GetLastError());
      return EMPTY_VALUE;
     }
   matrix out = m_mlfn.predict(preds);
   return out[0][0];
  }

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   if(NumSamples<1 || NumAttempts<1 || VarParam<0.0 || NumBadModels<1 || NumGoodModels<1 || NumBiasedModels<1)
     {
      Print(" Invalid User inputs ");
      return;
     }

   int ndone, divisor;
   double diff, std, temp;

   double computed_err_average ;
   double computed_err_unconstrained ;
   double computed_err_unbiased ;
   double computed_err_biased ;
   double computed_err_weighted ;
   double computed_err_bagged ;
   double computed_err_genreg ;

   CAvg average;
   CLinReg unconstrained;
   CUnbiased unbiased;
   Cbiased biased;
   CWeighted weighted;
   CGenReg genreg;

   vector computed_err_raw = vector::Zeros(NumBadModels+NumGoodModels+NumBiasedModels);

   std  =  sqrt(VarParam);

   divisor = 1;

   IModel* puremodels[];
   matrix xgood[],xbad[],xbiased[],test[10];

   if(ArrayResize(puremodels,NumBadModels+NumGoodModels+NumBiasedModels)<0 ||
      ArrayResize(xgood,NumBadModels+NumGoodModels+NumBiasedModels)<0 ||
      ArrayResize(xbad,NumBadModels+NumGoodModels+NumBiasedModels)<0 ||
      ArrayResize(xbiased,NumBadModels+NumGoodModels+NumBiasedModels)<0)
     {
      Print(" failed puremodels array resize ", GetLastError());
      return;
     }

   for(uint i = 0; i<puremodels.Size(); i++)
      puremodels[i] = new CMlfn();

   for(uint i = 0; i<xgood.Size(); i++)
      xgood[i] = matrix::Zeros(NumSamples,3);

   for(uint i = 0; i<xbad.Size(); i++)
      xbad[i] = matrix::Zeros(NumSamples,3);

   for(uint i = 0; i<xbiased.Size(); i++)
      xbiased[i] = matrix::Zeros(NumSamples,3);

   for(uint i = 0; i<test.Size(); i++)
      test[i] = matrix::Zeros(NumSamples,3);

   computed_err_average = 0.0 ;
   computed_err_unconstrained = 0.0 ;
   computed_err_unbiased = 0.0 ;
   computed_err_biased = 0.0 ;
   computed_err_weighted = 0.0 ;
   computed_err_bagged = 0.0 ;
   computed_err_genreg = 0.0 ;

   vector t,v;
   matrix d;

   ndone  = 1;
   for(uint i = 0; i<xgood.Size(); i++)
     {
      xgood[i].Random(0.0,1.0);
      if(!xgood[i].Col(sin(xgood[i].Col(0)) - pow(xgood[i].Col(1),2.0) + std*xgood[i].Col(2),2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }
   matrix xb(xgood[0].Rows(),1);
   for(uint i = 0; i<xbad.Size(); i++)
     {
      xbad[i] = xgood[0];
      xb.Random(0.0,1.0);
      if(!xbad[i].Col(xb.Col(0),2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }
   for(uint i = 0; i<xbiased.Size(); i++)
     {
      xbiased[i] = xgood[0];
      if(!xbiased[i].Col(xgood[0].Col(2)+1.0,2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }
   for(uint i = 0; i<test.Size(); i++)
     {
      test[i].Random(0.0,1.0);
      if(!test[i].Col(sin(test[i].Col(0)) - pow(test[i].Col(1),2.0) + std * test[i].Col(2),2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }

   for(uint imodel=0; imodel<puremodels.Size(); imodel++)
     {
      if(imodel < xgood.Size())
        {
         t=xgood[imodel].Col(2);
         d=np::sliceMatrixCols(xgood[imodel],0,2);
        }
      else
         if(imodel >= xgood.Size() && imodel<(xgood.Size()+xbiased.Size()))
           {
            t=xbiased[imodel-xgood.Size()].Col(2);
            d=np::sliceMatrixCols(xbiased[imodel-xgood.Size()],0,2);
           }
         else
           {
            t=xbad[imodel - (xgood.Size()+xbiased.Size())].Col(2);
            d=np::sliceMatrixCols(xbad[imodel - (xgood.Size()+xbiased.Size())],0,2);
           }

      matrix tt(t.Size(),1);

      if(!tt.Col(t,0) || !puremodels[imodel].train(d,tt))
        {
         Print(" failed column insertion ", GetLastError());
         cleanup(puremodels);
         return;
        }

      temp  = 0.0;

      for(uint i = 0; i<test.Size(); i++)
        {
         for(int j = 0; j<NumSamples; j++)
           {
            t  = test[i].Row(j);
            v  = np::sliceVector(t,0,2);
            diff = puremodels[imodel].forecast(v) - t[2];
            temp += diff*diff;
           }
        }
      computed_err_raw[imodel] += temp/double(test.Size()*NumSamples);
     }
//average
   matrix tdata;
   if(TrainCombinedModelsOnCleanData)
      tdata = xgood[0];
   else
     {
      tdata = matrix::Zeros(NumSamples*3,3);
      if(!np::matrixCopyRows(tdata,xgood[0],0,NumSamples) ||
         !np::matrixCopyRows(tdata,xbad[0],NumSamples,NumSamples*2) ||
         !np::matrixCopyRows(tdata,xbiased[0],NumSamples*2))
        {
         Print(" failed to create noisy dataset");
         cleanup(puremodels);
         return;
        }
     }
   temp = 0.0;
   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = average.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_average += temp/double(test.Size()*NumSamples);
//unconstrained
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!unconstrained.fit(d,t,puremodels))
     {
      Print(" failed to fit unconstrained model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = unconstrained.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_unconstrained += temp/double(test.Size()*NumSamples);
//unbiased
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!unbiased.fit(d,t,puremodels))
     {
      Print(" failed to fit unbiased model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = unbiased.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_unbiased += temp/double(test.Size()*NumSamples);
//biased
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!biased.fit(d,t,puremodels))
     {
      Print(" failed to fit biased model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = biased.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_biased += temp/double(test.Size()*NumSamples);
//weighted
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!weighted.fit(d,t,puremodels))
     {
      Print(" failed to fit weighted model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = weighted.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_weighted += temp/double(test.Size()*NumSamples);
//gendreg
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!genreg.fit(d,t,puremodels))
     {
      Print(" failed to fit generalized regression model ");
      cleanup(puremodels);
     }
   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = genreg.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_genreg += temp/double(test.Size()*NumSamples);

   temp = 0.0;
   PrintFormat("\n\n\nRandom DataSet%5d    Raw errors:", ndone);
   for(uint imodel  = 0; imodel<puremodels.Size() ; imodel++)
     {
      PrintFormat("  %.8lf", computed_err_raw[imodel] / ndone) ;
      temp += computed_err_raw[imodel] / ndone ;
     }
   PrintFormat("\n       Mean raw error = %8.8lf", temp / double(puremodels.Size())) ;

   PrintFormat("\n        Average error = %8.8lf", computed_err_average / ndone) ;
   PrintFormat("\n  Unconstrained error = %8.8lf", computed_err_unconstrained / ndone) ;
   PrintFormat("\n       Unbiased error = %8.8lf", computed_err_unbiased / ndone) ;
   PrintFormat("\n         Biased error = %8.8lf", computed_err_biased / ndone) ;
   PrintFormat("\n       Weighted error = %8.8lf", computed_err_weighted / ndone) ;
   PrintFormat("\n         GenReg error = %8.8lf", computed_err_genreg / ndone) ;

   cleanup(puremodels);
  }
//+------------------------------------------------------------------+

Al ejecutar el script con los valores predeterminados se obtiene el siguiente resultado.

MR      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)       Random DataSet    1    Raw errors:
KI      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.38602529
HP      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.36430552
CK      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.36703202
OS      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.51205057
EJ      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.57791798
HE      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.66825953
FL      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.65051234
QD      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.57403745
EO      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.71593174
PF      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.62444495
NQ      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.77552594
KI      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.75079339
MP      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.78851743
CK      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)         0.52343272
OR      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)         0.70166082
EK      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
RE      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)              Mean raw error = 0.59869651
QL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
DE      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)               Average error = 0.55224337
ML      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
QF      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)         Unconstrained error = 10.21673109
KL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
RI      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)              Unbiased error = 0.55224337
GL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
PH      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)                Biased error = 0.48431477
CL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
HH      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)              Weighted error = 0.51507522
OM      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
LK      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)                GenReg error = 0.33761372
KM      0       15:57:11.108    Ensemble_Demo (BTCUSD,D1)       
GG      0       15:57:11.108    Ensemble_Demo (BTCUSD,D1)       
CQ      0       15:57:11.108    Ensemble_Demo (BTCUSD,D1)       

Si todos los parámetros del script se mantienen iguales que en la ejecución anterior, excepto que esta vez optamos por entrenar el modelo de consenso con datos ruidosos. Observamos el siguiente resultado:

NL      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)       Random DataSet    1    Raw errors:
OS      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.72840629
GJ      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.63345953
PE      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.68442450
JL      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.91936106
OD      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.75230667
LO      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.88366446
PF      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.78226316
CQ      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.87140196
II      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.58672356
KP      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         1.09990815
MK      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.92548778
OR      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         1.03795716
GJ      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         0.80684429
GE      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         1.24041209
GL      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         0.92169606
NF      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
CS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)              Mean raw error = 0.85828778
RF      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
DS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)               Average error = 0.83433599
FF      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
FP      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         Unconstrained error = 23416285121251567120416768.00000000
DS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
JR      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)              Unbiased error = 0.83433599
HS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
PP      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)                Biased error = 0.74321307
LD      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
GQ      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)              Weighted error = 0.83213118
PD      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
FR      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)                GenReg error = 0.78697882

Las observaciones clave de estos resultados son que los métodos de conjunto generalmente superan a los modelos individuales, ya que la combinación de múltiples modelos generalmente produce mejores resultados que confiar en uno solo. Sin embargo, no existe un método universal óptimo, ya que cada uno tiene sus fortalezas y debilidades, y la elección óptima depende del conjunto de datos específicos y del problema en cuestión.

La regresión sin restricciones, si bien es potencialmente poderosa, es muy susceptible al sobreajuste, particularmente con conjuntos de datos ruidosos o pequeños. Por otro lado, GRNN se destaca en el manejo de conjuntos de datos pequeños y ruidosos al suavizar eficazmente los datos, aunque puede sacrificar algo de potencia de ajuste para conjuntos de datos más grandes y limpios.

Los métodos de regresión lineal también pueden ser efectivos, pero el sobreajuste es un problema, especialmente con conjuntos de datos ruidosos o pequeños. El promedio simple y la ponderación de la varianza son generalmente robustos y pueden ser buenas opciones cuando el conjunto de datos es ruidoso o el método óptimo es incierto. En resumen, la elección del método de conjunto debe considerarse cuidadosamente en función de las características específicas del conjunto de datos, y a menudo es beneficioso experimentar con diferentes métodos y evaluar su desempeño en un conjunto de validación para tomar una decisión informada. Se adjunta todo el código referenciado en el texto.

Nombre del archivo
Descripción
MQL5/include/mlffnn.mqh
Contiene la definición de la clase FFNN que implementa un peceptrón multicapa básico.
MQL5/include/grnn.mqh
Define la clase CGrnn que implementa una red neuronal de regresión generalizada que utilizó recocido simulado.
MQL5/include/OLS.mqh
Define la clase MCO que encapsula la regresión de mínimos cuadrados ordinarios
MQL5/include/ensemble.mqh
Contiene la definición de seis métodos de conjunto implementados como las clases CAvg, CLinReg, Cbiased, CUnbiased, CWeighted y CGenReg
MQL5/include/np.mqh
Contiene matriz de variación y funciones de utilidad vectorial.
MQL5/scripts/Ensemble_Demo.mq5
Este es un script que demuestra las clases de conjunto definidas en ensemble.mqh

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

Archivos adjuntos |
ensemble.mqh (50.08 KB)
grnn.mqh (6.6 KB)
mlffnn.mqh (8.39 KB)
np.mqh (74.16 KB)
OLS.mqh (13.34 KB)
Ensemble_Demo.mq5 (13.03 KB)
Roman Shiredchenko
Roman Shiredchenko | 14 jul 2025 en 08:28

super artículo - gracias. El tema es relevante -la investigación de los datos - se verá en mi terminal y la investigación.....

dará retroalimentación aquí.

Esta información está en mi cola para su consideración......

Integración de Discord con MetaTrader 5: Creación de un robot comercial con notificaciones en tiempo real Integración de Discord con MetaTrader 5: Creación de un robot comercial con notificaciones en tiempo real
En este artículo veremos cómo integrar MetaTrader 5 y el servidor Discord para recibir notificaciones de transacciones en tiempo real desde cualquier parte del mundo. Además, aprenderemos a configurar la plataforma y Discord para asegurarnos de que las alertas se envían a Discord, y hablaremos de los problemas de seguridad que surgen al utilizar WebRequest y webhooks para estos métodos de notificación.
Cambiamos a MQL5 Algo Forge (Parte 2): Trabajando con varios repositorios Cambiamos a MQL5 Algo Forge (Parte 2): Trabajando con varios repositorios
Hoy analizaremos uno de los posibles enfoques para organizar el almacenamiento del código fuente de un proyecto en un repositorio público. Utilizando la distribución en diferentes ramas, crearemos reglas claras y cómodas para el desarrollo del proyecto.
Desarrollamos un asesor experto para controlar los puntos de entrada en las operaciones swing Desarrollamos un asesor experto para controlar los puntos de entrada en las operaciones swing
A medida que el año se acerca a su fin, los tráders a largo plazo suelen hacer balance del año, analizando la historia, el comportamiento y las tendencias del mercado para evaluar el potencial de los movimientos futuros. En este artículo, analizaremos el desarrollo de un asesor experto para el seguimiento de operaciones a largo plazo utilizando MQL5. El objetivo será hacer frente a problemas como la pérdida de oportunidades comerciales debido al trading manual y a la falta de sistemas de supervisión automatizados. Como ejemplo de definición eficaz de una estrategia para nuestra solución y también para desarrollar la misma, utilizaremos uno de los pares comerciales más destacados.
Modelo de riesgo de cartera utilizando el criterio de Kelly y la simulación de Monte Carlo Modelo de riesgo de cartera utilizando el criterio de Kelly y la simulación de Monte Carlo
Durante décadas, los operadores han utilizado la fórmula del criterio de Kelly para determinar la proporción óptima de capital que se debe asignar a una inversión o apuesta con el fin de maximizar el crecimiento a largo plazo y minimizar el riesgo de ruina. Sin embargo, seguir ciegamente el criterio de Kelly utilizando el resultado de una sola prueba retrospectiva suele ser peligroso para los operadores individuales, ya que en el trading en vivo, la ventaja comercial disminuye con el tiempo y el rendimiento pasado no es un indicador de resultados futuros. En este artículo, presentaré un enfoque realista para aplicar el criterio de Kelly a la asignación de riesgos de uno o más EA en MetaTrader 5, incorporando los resultados de la simulación de Monte Carlo de Python.