English Русский Español Deutsch 日本語 Português
preview
在 MQL5 中提升数值预测的集成方法

在 MQL5 中提升数值预测的集成方法

MetaTrader 5统计分析 |
308 1
Francis Dube
Francis Dube

引言

机器学习通常会产生多个质量各异的预测模型。从业者通常会评估这些模型,并选择表现最佳的那个用于实际应用。然而,本文探讨了一种替代方法:通过组合那些看似次优模型的输出来重新利用它们,以期提升潜在的整体预测性能。我们将研究多种组合预测的技术,并展示它们在纯 MQL5 中的实现。最后,我们将对这些方法进行比较,并讨论它们在不同场景下的适用性。

为了将组合模型预测的概念形式化,我们首先引入一些关键符号。考虑一个包含 K 个数据点的训练集,每个数据点表示为一对 (xi,yi),其中 xi​ 是预测因子向量,而 yi​ 是我们旨在预测的对应标量响应变量。假设我们有 N 个训练好的模型,每个模型都能进行预测。当给定一个预测因子 x 时,模型 n 会生成一个预测值,记为 f_n​(x)。我们的目标是构建一个共识函数 f(x),它能有效地组合这 N 个独立的预测,从而产生一个比任何单一模型都更准确的总体预测。

共识函数

这个共识函数,通常被称为集成或元模型,有潜力超越其构成模型。在本探索中,我们将深入研究构建有效集成模型的多种技术,并评估它们在 MQL5 中的实际实现与性能。


基于平均预测的集成

组合数值预测的最简单技术之一是简单平均。通过计算多个预测值的均值,我们通常可以获得比依赖任何单一模型更准确、更稳健的估计。这种方法在计算上高效且易于实现,使其成为广泛应用的实用选择。算术平均的简单性是其最大的优势。与需要估计多个参数的更复杂集成方法不同,平均法本质上能抵抗过拟合。过拟合是指模型与训练数据的特定特征过于紧密地绑定,这会损害其泛化到未见数据的能力。

通过完全避免参数估计,简单平均绕过了这个问题,确保了即使在嘈杂或小规模数据集上也能有一致的性能。相比之下,我们稍后将探讨的其他集成技术,通常涉及参数调优和优化,这可能会引入一定程度的过拟合风险。因此,尽管平均法可能缺乏高级集成方法的复杂性,但其可靠性和易用性使其成为集成学习工具箱中不可或缺的工具。

一个植根于柯西-施瓦茨不等式的基本数学原理,为定义平均预测集成的函数提供了理论基础。该不等式指出:N 个数的和的平方,总是小于或等于它们平方和的 N 倍。

柯西推导不等式

现在,考虑一个用于预测因变量 y 的预测向量 x。将不等式中的 a 替换为模型从 x 预测 y 时所产生的误差,那么 a_n = f_n(x) - y。如果通过假设 f(x) 为各个预测的平均值来拆分该方程左侧的求和项。提取公因子 N,并将方程最右边的项代入柯西推导不等式的左侧,然后两边同时除以 N²,我们便得到了一个作为集成方法基础的基本方程:

平均集成

上述方程右侧的求和项代表了各个模型的平方误差。将这些平方误差相加,再除以组件模型的数量,便得到了各个模型的均方误差。与此同时,方程的左侧代表了共识模型的平方误差,该模型是由各个预测的均值推导而来的。

从数学上讲,这个不等式断言:对于任何一组预测器和目标,平均预测的平方误差永远不会超过各个独立预测的均方误差。只有当所有独立模型的预测误差都完全相同时,等号才成立。

当然,这种优势并非没有代价。平均化的效果在很大程度上取决于组件模型的性质。如果所有模型都具有相似的预测能力,那么对它们的预测进行平均通常是一种合理且有效的方法。但是,当组件模型的预测能力差异很大时,问题就可能出现。在这种情况下,平均化可能会削弱强模型的贡献,同时过分强调弱模型的影响,从而可能降低集成模型的整体预测性能。

实现集成平均化的代码被封装在 ensemble.mqh 文件中定义的 CAvg 类中。这个类,以及所有其他实现了集成方法的类,都依赖于用户提供一个预训练好的模型集合。这些模型必须遵守 IModel 接口,该接口定义如下:

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

IModel接口定义了两个方法:

  • train(): 此方法包含了用于训练模型的逻辑。
  • forecast(): 此方法定义了基于新的输入数据进行预测的操作。
//+------------------------------------------------------------------+
//| 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;
  }

CAvg 类包含一个 predict() 方法,该方法通过一个输入数据向量和一组预训练的组件模型来调用。此方法返回一个代表共识预测的标量值。对于 CAvg 类而言,其共识预测是通过计算所提供模型数组的预测结果的平均值来得出的。通过遵循这种设计,CAvg 类确保了其灵活性和模块化,允许用户将各种类型的模型无缝地集成到他们的集成方法中。


预测模型的无约束线性组合

当面对一组预测质量差异巨大的模型时,可以采用的一种集成方法是简单线性回归。其核心思想是,将共识预测计算为组件模型预测值的加权和,并包含一个常数项以考虑任何可能存在的偏差。

基于线性回归的集成

该集成方法在 CLinReg 类中实现。其构造函数、析构函数和 predict() 方法与前面描述的 CAvg 类中的同名方法具有相同的签名。

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

然而,CLinReg 类引入了一个 fit() 方法,该方法指定了训练共识模型所需的操作。

fit() 方法接收以下输入:

  • 预测变量矩阵。
  • 目标向量。
  • 一个组件模型数组。

在 fit() 内部,使用 OLS 类的一个实例来表示共识回归模型。矩阵变量 independent 被用作设计矩阵,它由各个组件模型预测的平方误差构建而成,并增加了一个常数项(一列 1)。当调用 CLinReg 的 predict() 方法时,它会返回将组件模型的预测误差作为输入,代入共识回归模型计算得出的结果。

将模型组合为组件预测的加权和,在特定且罕见的情况下效果很好。然而,在现实世界的应用中,这种方法常常被忽视,主要有两个原因:

  • 过拟合风险:共识模型中的权重是必须优化的参数。如果集成中包含许多组件模型,优化过程可能会导致显著的过拟合,从而降低模型对未见数据的泛化能力。
  • 多重共线性:如果两个或多个模型产生相似的预测,多重共线性可能会导致权重估计的不稳定。出现这个问题的原因是,性能相似的模型的权重之和可能是一个常数,而这些模型仅在训练期间遇到的案例中表现相似。

然而,这一假设在现实场景中常常不成立。当出现样本外案例时,先前产生相似预测的模型可能会有不同的反应,这可能导致共识模型产生极端且不可靠的结果。


有偏模型的约束线性组合

使用简单回归作为组合多个预测模型的基础,有时会导致一个具有极端权重的不稳定模型。这个问题通常出现在回归系数具有相反符号时,这是为了平衡数值以使数据能够良好拟合所必需的。例如,一对相关模型中的一个系数,只有在其对应系数被驱动为一个很小的负值时,才可能被驱动为一个很大的正值。为了防止这些极端值的出现,我们可以对回归系数进行约束,以避免极端的负值。这种方法也减少了优化过程中的自由度,使模型更稳定,更不容易过拟合。

这种集成方法在 Cbiased 类中实现。它包含了在其他集成实现中常见的 fit() 和 predict() 方法。

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

然而,Cbiased 的关键区别在于其权重的优化方式。

权重的优化是使用鲍威尔(Powell)函数最小化方法来完成的。这就是为什么 Cbiased 类是 PowellsMethod 类的后代。准则函数在 func() 方法中实现,该方法遍历训练数据,使用提供的权重累加平方误差。对于数据集中的每个样本:

  • 组件模型的预测会根据当前权重和一个常数项进行加权。
  • 预测值与目标值之间的平方差会被求和。

准则函数的最后会检查是否有任何试验权重为负数。如果存在负权重,则会应用一个惩罚项。该函数返回总误差加上由负权重产生的任何惩罚。这种集成方法最适用于已知某些组件模型存在偏置的情况。此处的“偏置”指的是模型所产生的预测值与其对应的目标值相比,持续性地偏高或偏低,通常表现出一种显著的趋势。通过约束权重,Cbiased 有效地减少了偏置模型的影响,从而得出一个更平衡、更准确的集成预测。在下一节中,我们将介绍一种适用于表现出很少或没有偏置的模型集的方法,其重点在于聚合性能相当的模型的预测。


无偏模型的约束组合

当已知一组组件模型的预测中没有显著的偏置时,在共识模型中就没有必要包含常数项。移除常数项有助于降低模型对数据过拟合的倾向。此外,正如在先前方法中讨论过的,这种方法还确保了模型的权重永远不会为负。不仅如此,还对权重施加了一个额外的约束:它们的总和必须为一。这个约束提供了两个关键好处:

  • 确保共识模型无偏:只要组件模型是合理无偏的,要求权重总和为一就能确保共识模型也保持无偏。
  • 在预测之间进行插值:总和为一的约束保证了共识预测是在各组件模型预测之间的一种插值。这确保了最终预测不会与单个预测产生巨大偏差,从而防止了因极端权重而可能出现的极端结果。

以下方程对此进行了说明:

约束线性模型

实现此集成方法的代码与之前的实现大体相同。与 CUnbiased 类的主要区别在于被最小化的函数。

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

此函数融合了前面讨论的额外约束条件,具体而言就是权重的非负性以及权重之和必须为一的要求。


预测模型的方差加权组合

另一种组合组件模型预测结果的方法,是基于由每个模型预测准确度所决定的最优权重。该技术涉及为预测误差较大的模型分配较小的权重,而为预测误差较小的模型分配较大的权重。当组件模型的质量存在显著差异时,此方法尤其有效。然而,如果模型之间高度相关,该技术可能不是理想选择,此时应考虑采用其他的集成方法。

根据模型质量进行加权的思想,其根源在于以下理论:如果模型是无偏的且不相关,那么分配与模型误差成反比的权重,将能使期望的平方误差最小化。

方差加权集成

为实现这一目标,每个模型的相对权重是基于其误差的倒数来计算的,然后对这些权重进行缩放,以确保它们总和为一。

权重方程

方差加权模型的集成是在 CWeighted 类中实现的。在 fit() 方法中,对于每个训练样本:

  • 会计算出每个组件模型的预测值。
  • 并且会累加每个预测值的平方误差。

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

一旦对所有训练样本都完成此操作,每个模型的总误差就会被用来计算其权重。然后,这些权重会被求和并进行缩放,以确保总权重之和为一。这种方法确保了误差较小的模型在最终的集成预测中拥有更大的影响力,这有可能提高预测的准确性,尤其是在各模型表现出不同预测精度水平的场景下。


基于广义回归神经网络的插值组合

到目前为止,我们讨论的集成方法在共识模型使用干净数据进行训练时效果良好。然而,当训练数据存在噪声时,模型可能会泛化能力不佳。为了解决这个问题,一种有效的回归方法是广义回归神经网络。与传统回归相比,GRNN(广义回归神经网络)的突出优势在于其不易发生过拟合。这是因为与传统回归技术相比,GRNN 的参数对模型的影响相对较小。虽然这种泛化能力的提升是以牺牲一定的准确性为代价的,但 GRNN 能够对复杂的非线性关系进行建模,当数据呈现出此类特征时,它提供了一个有用的工具。

GRNN 产生的预测值,是对训练数据中目标值的一种插值。该插值由一个权重决定,这个权重定义了一个样本外案例与已知的样本内案例之间的差异程度。样本越相似,被赋予的相对权重就越高。尽管 GRNN 可以被描述为一种平滑操作,因为它将未知样本插值到已知样本空间中,但其理论基础是植根于统计学的。

当给定一个数据集——该数据集包含来自组件模型的预测及其相应的目标值时——GRNN 的共识预测就是最小期望平方误差,该误差由条件期望给出。

条件期望

由于训练数据的联合密度通常是未知的,我们无法直接使用条件期望的公式。相反,我们依赖于从训练数据中得出的联合密度估计值,这就引出了如下所示的 GRNN 形式。

GRNN 公式

在展示基于 GRNN 的集成方法的代码之前,我们必须首先讨论 GRNN 的实现。GRNN 的代码在 grnn.mqh 文件中定义,该文件包含了 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;
  };

用于回归任务的 GRNN 实现包含几个关键组件。构造函数会初始化一些参数,例如内部和外部迭代的次数,以及 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;
  }

fit 方法会存储训练数据,包括输入预测变量和目标值,并初始化 sigma 权重。然后,它通过使用模拟退火方法迭代优化 sigma 权重来训练 GRNN 模型。在训练过程中,sigma 权重会受到扰动,并计算出扰动后权重的交叉验证误差。是否接受该扰动,取决于误差值和一个温度参数,而温度参数会逐渐降低,以使搜索过程更加聚焦。

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

predict 方法会计算输入向量与每个训练数据点之间的距离,根据训练数据点与输入的距离为其赋予权重,然后将预测输出计算为这些训练数据点目标值的加权平均值。sigma 权重决定了每个训练数据点对预测的影响程度。

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

交叉验证用于评估模型的性能并优化 sigma 权重,而模拟退火则作为一种元启发式优化算法,用于寻找最优的 sigma 权重。最终,GRNN 执行的是基于核的插值,其预测是训练数据点之间的一种加权插值。

CGenReg类实现了基于GRNN的集成方法。

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

CGenReg 类利用一个 CGrnn 对象来建模单个模型的预测值与实际目标值之间的复杂关系。在 fit 方法中,它首先存储训练数据,包括目标值 (train_targets) 和输入变量 (train_vars)。然后,它收集来自每个模型的单独预测,创建一个矩阵 (preds),其中每一行代表一个训练样本,每一列保存来自模型集中相应模型的预测值。CGrnn 对象使用这个由单个预测值组成的矩阵 (preds) 作为输入,并使用实际的目标值 (targ) 作为输出来进行训练。

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

在 predict 方法中,该类会为一个新的输入 (inputs) 收集每个模型的预测值,并将它们存储在一个工作向量 (m_work) 中。然后,使用训练好的 CGrnn 根据这些单独的预测值来预测最终的输出。该方法返回预测输出向量的第一个元素作为最终预测结果。

//+------------------------------------------------------------------+
//| 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];
  }


结论:比较集成方法

本文介绍了多种集成方法,并简要讨论了它们固有的优缺点。作为本文的总结,我们将研究这些方法在应用于实际数据时表现如何。该比较通过一个名为 Ensemble_Demo.mq5 的 MetaTrader 5 脚本来实现。

该脚本会生成几组合成数据集。第一组包含用于训练基准模型的数据集。使用此类数据训练的模型被定义为“好模型”,而数据本身则被认为是“干净”的。生成第二组数据集是为了训练“坏模型”,这些模型被认为不如在干净数据上训练的“好模型”。

最后一组数据集用于训练被认为是“有偏”的模型。这些模型是相对于前面提到的“好模型”而言存在偏差的。将每组中的部分数据集组合起来,以模拟带噪声的数据。

该脚本允许用户指定要训练多少个“好”模型、“坏”模型和“有偏”模型。用户还可以控制构成训练数据的样本数量,从而能够评估样本量如何影响集成方法的性能。最后,用户可以通过将参数 TrainCombinedModelsOnCleanData 设置为 true,来选择使用干净数据训练集成模型;或者将其设置为 false,以使用带噪声的数据进行训练。

这些模型是由 mlffnn.mqh 文件中的 FFNN 类实现的前馈神经网络。

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

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

FFNN 类定义了一个多层感知器,这是一种用于监督学习任务的人工神经网络。它包含若干属性,例如:

  • m_trained,一个布尔标志,用于指示网络是否已成功训练;
  • m_weights,一个矩阵数组,用于存储每一层之间的权重;
  • m_outputs,一个矩阵数组,用于保存每个隐藏层的输出;
  • m_result,一个矩阵,用于保存训练后网络的最终输出;
  • m_epochs,训练周期(迭代)的次数;
  • m_num_inputs,网络的输入变量数量;
  • m_layers,网络中的总层数,包括输入层和输出层;
  • m_hidden_layers,网络中隐藏层的数量;
  • m_hidden_layer_size,一个数组,用于定义每个隐藏层中的节点数量;
  • m_learn_rate,训练期间用于权重更新的学习率;
  • m_act_fn,隐藏层中使用的激活函数。

该类同时包含私有方法和公共方法。私有方法例如:

  • create,该方法根据指定的配置,通过为权重矩阵和隐藏层输出分配内存来初始化网络结构;
  • calculate,该方法将输入数据在网络中传播,通过应用权重和激活函数来计算输出;
  • backprop,该方法实现了反向传播算法,根据预测输出与实际输出之间的误差来调整权重。

公共方法包括:

  • FFNN(构造函数),它使用指定的层数和隐藏层大小来初始化网络;
  • ~FFNN(析构函数),它释放为网络分配的资源;
  • fit,它使用给定的数据集对网络进行训练,在指定的周期数内通过反向传播来调整权重;
  • predict,它使用训练好的网络为新的输入数据生成预测,有效地执行前向传播。

在该脚本中,CMlfn 类基于一个 FFNN 的实例实现了 IModel 接口。接下来是对在不同配置下运行该脚本的简要说明。

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

以默认参数运行脚本得到如下输出。

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)       

如果所有脚本参数都保持与上次运行时一致,但这次我们选择在带噪声的数据上训练共识模型。我们观察到下述输出。

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

关键观察结果表明,集成方法通常优于单一模型,因为结合多个模型通常比依赖单一模型能产生更好的效果。然而,并不存在一种普遍最优的方法,因为每种方法都有其自身的优缺点,最佳选择取决于具体的数据集和待解决的问题。

无约束回归虽然可能很强大,但极易发生过拟合,尤其是在处理噪声较大或规模较小的数据集时。另一方面,GRNN通过有效地平滑数据,在处理小规模、高噪声数据集方面表现出色;但对于规模较大、质量较高的数据集,它可能会牺牲一部分拟合能力。

线性回归方法也可以是有效的,但过拟合同样是一个需要关注的问题,尤其是在数据集噪声较大或规模较小的情况下。简单平均和方差加权这两种方法通常具有稳健性,当数据集噪声较大,或者无法确定哪种方法为最优时,它们是不错的选择。总之,集成方法的选择应基于数据集的具体特性进行审慎考量。通常,尝试多种方法并在验证集上评估其性能,将有助于做出明智的决策。文中引用的所有代码均已附上。

文件名
说明
MQL5/include/mlffnn.mqh
定义了 FFNN 类,该类实现了一个基本的多层感知机。
MQL5/include/grnn.mqh
定义了 CGrnn 类,该类实现了一种使用模拟退火算法的广义回归神经网络。
MQL5/include/OLS.mqh
定义了 OLS 类,该类封装了普通最小二乘法回归。
MQL5/include/ensemble.mqh
包含了六种集成方法的定义,这些方法分别被实现为 CAvg、CLinReg、CBiased、CUnbiased、CWeighted 和 CGenReg 类。
MQL5/include/np.mqh
包含了矩阵和向量的各种实用函数。
MQL5/scripts/Ensemble_Demo.mq5
这是一个演示脚本,用于展示 ensemble.mqh 文件中定义的集成类的用法。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16630

附加的文件 |
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)
最近评论 | 前往讨论 (1)
Roman Shiredchenko
Roman Shiredchenko | 14 7月 2025 在 08:28

超级好文章 - 谢谢。主题很有意义 - 正在研究数据 - 将查看我的终端和研究.....

将在此给予反馈。

这些信息在我的审议队列中......。

价格行为分析工具包开发(第五部分):波动率导航智能交易系统(Volatility Navigator EA) 价格行为分析工具包开发(第五部分):波动率导航智能交易系统(Volatility Navigator EA)
判断市场方向或许相对简单,但把握入场时机却颇具挑战。作为“价格行为分析工具包开发”系列文章的一部分,我很高兴再为大家介绍一款能够提供入场点、止盈水平和止损设置位置的工具。为实现这一目标,我们采用了MQL5编程语言。让我们在本文中深入探讨每一步。
数据科学和机器学习(第 32 部分):保持您的 AI 模型更新,在线学习 数据科学和机器学习(第 32 部分):保持您的 AI 模型更新,在线学习
在瞬息万变的交易世界中,适应市场变化不仅是一种选择 — 而且是一种必要。每天都有新的形态和趋势出现,即使是最先进的机器学习模型,也难以面对不断变化的条件保持有效。在本文中,我们将探讨如何通过自动重训练,令您的模型保持相关性、及对新市场数据的响应能力。
您应当知道的 MQL5 向导技术(第 49 部分):搭配近端政策优化的强化学习 您应当知道的 MQL5 向导技术(第 49 部分):搭配近端政策优化的强化学习
近端政策优化是强化学习中的另一种算法,通常以网络形式以非常小的增量步幅更新政策,以便确保模型的稳定性。我们以向导汇编的智能系统来试验其作用,如同我们之前的文章一样。
基于通用 MLP 逼近器的EA 基于通用 MLP 逼近器的EA
本文介绍了一种在交易 EA 中使用神经网络的简单且易于实现的方法,该方法不需要深厚的机器学习知识。该方法免除了对目标函数进行归一化的步骤,同时克服了“权重爆炸”和“网络停滞”等问题,并提供了直观的训练过程和结果的可视化控制。