English Deutsch
preview
MQL5における数値予測を強化するアンサンブル法

MQL5における数値予測を強化するアンサンブル法

MetaTrader 5統計と分析 | 14 4月 2025, 08:21
180 0
Francis Dube
Francis Dube

はじめに

機械学習では、性能に差のある複数の予測モデルが生成されることがよくあります。一般的には、それらのモデルを評価したうえで、実運用において最も高いパフォーマンスを示すモデルを選択するのが一般的です。しかし本記事では、一見すると劣っているように見えるモデルであっても、それらの出力を組み合わせることで、全体としての予測性能を向上させる可能性について検討します。予測の統合に用いるさまざまな手法を取り上げ、それらをMQL5のみでどのように実装できるかを示します。最後に、これらの手法を比較し、異なるシナリオにおける適用の適否について議論します。

モデルの予測を組み合わせるという考え方を形式的に定義するために、いくつかの重要な記法を導入しましょう。K個のデータ点からなる学習用データセットを考えます。各データ点はペア(xi,yi)として表され、ここでxi​は予測子ベクトル、 yi​は対応するスカラー応答変数です 。予測をおこなうことができるN個の訓練済みモデルがあると仮定します。ある入力ベクトルxに対して、モデルnが生成する予測をf_n​(x)とします。私たちの目標は、これらN個の個別予測をうまく統合し、単一のモデルよりも高精度な予測を実現するコンセンサス関数f(x)を構築することです。

コンセンサス関数

このコンセンサス関数は、アンサンブルあるいはメタモデルとも呼ばれ、構成する各モデル単体よりも優れた性能を発揮する可能性があります。本記事では、効果的なアンサンブルモデルを構築するためのさまざまな手法について掘り下げ、MQL5における実装例とその実用的なパフォーマンスについて評価します。


平均予測に基づくアンサンブル

数値予測を組み合わせる最も基本的な手法のひとつが、単純な平均化です。複数の予測結果の平均を計算することで、単一のモデルに依存するよりも、より正確かつ堅牢な推定値が得られる場合が多くあります。この手法は計算効率に優れ、実装も容易であるため、幅広い応用において実用的な選択肢となります。算術平均のシンプルさこそが、この手法の最大の強みです。複数のパラメータ推定を必要とする複雑なアンサンブル手法とは異なり、平均化は本質的に過剰適合に強い耐性を持ちます。過剰適合とは、モデルが訓練データの固有の特徴に過度に適合してしまい、未知のデータへの汎化能力が低下する現象です。

単純平均では、パラメータ推定を一切おこなわないため、この問題を回避でき、ノイズの多いデータセットや小規模なデータセットにおいても安定したパフォーマンスを発揮します。一方で、後述するように他のアンサンブル手法では、パラメータの調整や最適化が必要となることが多く、それによって過剰適合のリスクが生じる可能性があります。このように、単純平均は高度なアンサンブル技術ほどの洗練性には欠けるかもしれませんが、その信頼性と手軽さから、アンサンブル学習における基本かつ不可欠な手法のひとつと言えるでしょう。

平均化による予測関数の理論的な基盤には、コーシー=シュワルツの不等式に根ざした基本的な数学的原理があります。この不等式は、「N個の数値の和の二乗は、その二乗の和にNを掛けた値以下である」と述べています。

コーシー導出不等式

ここで、従属変数yを予測するために使用される予測子ベクトルxを考えます。コーシー=シュワルツの不等式におけるaを、モデルによって生じる予測誤差、すなわち「aₙ = fₙ(x) - y」と置き換えます。ここで「fₙ(x)」はモデル n による予測値、y は実際の値です。さらに、予測値の平均を f(x) と定義したうえで、この式の左辺の加数を分解します。続いて、Nを因数として取り出し、式の右端の項をコーシー=シュワルツ不等式の左辺に移項します。最後に、両辺をN²で割ることで、アンサンブル手法としての平均化を理論的に支える基本方程式を導き出すことができます。

平均アンサンブル

上記の式の右辺にある総和は、各モデルが出す予測誤差(二乗誤差)を表しています。これらの二乗誤差を合計し、構成モデルの数で割ることで、各モデルの平均二乗誤差(MSE)が得られます。一方、式の左辺は、個々の予測値の平均(すなわちコンセンサスモデル)に対する二乗誤差を示しています。

数学的に言えば、この不等式は、どのような予測子と目的変数の組み合わせにおいても、平均予測による二乗誤差は、個別モデルの平均二乗誤差を超えることはないと述べています。等号が成り立つのは、すべてのモデルの予測誤差が完全に同一である場合に限られます。

もっとも、このような利点も制約なしに享受できるわけではありません。平均化の効果は、使用されるモデル群の性質に大きく左右されます。もしすべてのモデルが同等の予測力を持っているのであれば、それらの出力を平均化することは、合理的でかつ効果的な手法となります。しかし、モデル間で予測性能に大きな差がある場合には注意が必要です。そうしたケースでは、単純な平均化によって優れたモデルの貢献が薄れ、性能の劣るモデルが過度に影響力を持ってしまう可能性があり、アンサンブル全体の精度が低下するおそれがあります。

この平均化アンサンブルの実装コードは、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インターフェイスは2つのメソッドを指定します。

  • 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()メソッドが呼び出されると、各モデルの予測誤差を入力としてコンセンサス回帰モデルを通じて算出された結果が返されます。

モデルをコンポーネント予測の加重平均として組み合わせる手法は、特定のまれなケースでは有効に機能します。しかし、以下の2つの主な理由から、現実のアプリケーションにおいてはあまり採用されない傾向があります。

  • 過剰適合のリスク:コンセンサスモデルにおける重みは最適化対象のパラメータです。アンサンブル内に多くのモデルが含まれている場合、この最適化により深刻な過剰適合が生じることがあり、未知のデータへの汎化性能が著しく低下する恐れがあります。
  • 共線性:複数のモデルが類似した予測を行う場合、共線性により重みの推定が不安定になることがあります。これは、性能が似通ったモデル群の重みが定数として相殺しあう結果、学習データに対しては似たような動きを見せても、新たなデータでは全く異なる反応を示す可能性があるためです。

この仮定は、実際の運用環境ではしばしば破綻します。サンプル外(未知)データに直面した場合、過去に似た予測をしていたモデルが異なる挙動を示すことで、コンセンサスモデルが極端かつ信頼性の低い予測を行うリスクが生じます。


バイアス付きモデルの制約付き線形結合

単純回帰をベースに複数の予測モデルを組み合わせる場合、極端な重みを持つ不安定なモデルになることがあります。この問題は、回帰係数に符号の逆転が生じるときによく見られます。これは、モデルの予測値が適切にデータに適合するよう、値のバランスを取る必要があるためです。たとえば、相関関係にある2つのモデルにおいて、一方の回帰係数を大きな正の値に設定するには、もう一方の係数を非常に小さな負の値に設定せざるを得ないケースがあります。こうした極端な係数の出現を防ぐためには、回帰係数に制約を設けて、過度な負の値が発生しないようにするのが効果的です。このアプローチにより、最適化における自由度が減少し、モデルの安定性が向上すると同時に、過剰適合のリスクも低減されます。

このアンサンブル手法は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クラスの最大の特徴は、重みの最適化方法にあります。

重みの最適化には、関数最小化のためのパウエル法が使用されます。そのため、CbiasedクラスはPowellsMethodクラスを継承しています。基準関数はfunc()メソッドとして実装されており、訓練データを反復処理しながら、指定された重みに基づいて二乗誤差を蓄積します。データセット内の各サンプルに対して、以下の処理がおこなわれます。

  • コンポーネントモデルからの予測値に、現在の重みと定数項を使って加重平均を計算する
  • この予測値と実際の目標値との誤差の二乗を計算し、累積する

処理の最後に、基準関数は現在の試行重みの中に負の値が含まれているかどうかを確認します。もし負の重みが存在すれば、ペナルティが課されます。この関数は、合計エラーと負の重みによるペナルティを返します。このタイプのアンサンブル法は、一部のコンポーネントモデルにバイアスがあることが知られている場合に最も適しています。ここでの「バイアス」とは、モデルがターゲット値と比較して一貫して高すぎたり低すぎたりする予測を行い、しばしば顕著な傾向を示すことを指します。Cbiasedは重みを制限することによって、バイアスのあるモデルの影響を効果的に抑え、よりバランスの取れた正確なアンサンブル予測を実現します。次のセクションでは、バイアスがほとんどまたは全くないモデルセットに適した方法を紹介します。この方法では、性能が類似したモデルからの予測を集約することに焦点を当てます。


不偏モデルの制約付き組み合わせ

一連のコンポーネントモデルに予測に大きな偏りがないことがわかっている場合、コンセンサスモデルに定数項を含める必要はありません。定数項を削除することで、モデルがデータに過剰適合する傾向を減らすことができます。さらに、このアプローチでは、前述の方法で説明したように、モデルの重みが決して負の値にならないことが保証されます。さらに、重みには追加の制約が課せられます。それは、重みの合計が1である必要があるというものです。この制約には2つの重要な利点があります。

  • 偏りのないコンセンサスモデルの確保:コンポーネントモデルが適度に偏りがない限り、重みの合計が1になるように制約することで、コンセンサスモデルも偏りがなくなることが保証されます。
  • 予測間の補間:合計が1になる制約により、コンセンサス予測がコンポーネントモデルの予測間で補間されることが保証されます。これにより、最終的な予測が個々の予測から極端に逸脱することがなくなり、極端な重みが生じることによる異常な結果を防ぎます。

これは次の式で示されます。

制約付き線形モデル

このアンサンブルメソッドを実装するコードは、以前の実装とほぼ同じです。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;
  }

この関数には、前述した追加の制約が組み込まれています。具体的には、重みが負でないこと、および重みの合計が1になるという要件です。


予測モデルの分散重み付けによる組み合わせ

コンポーネントモデルからの予測を組み合わせる別の方法は、各モデルの予測精度に基づいて決定される最適な重みに基づいています。この手法では、予測誤差が大きいモデルに対しては小さい重みを割り当て、予測誤差が小さいモデルには大きい重みを割り当てます。この方法は、コンポーネントモデルの品質に大きなばらつきがある場合に特に効果的です。ただし、モデル同士が高い相関を持つ場合、この手法は理想的ではない可能性があり、別のアンサンブル手法を検討する必要があります。

モデルの品質に応じた重み付けの考え方は、モデルが偏りがなく相関がない場合に、モデルの誤差に反比例する重みを割り当てることで、予測される二乗誤差を最小化できるという理論に基づいています。

分散加重アンサンブル

これを実現するために、各モデルの相対的な重みはその誤差の逆数に基づいて計算され、重みの合計が1になるようにスケーリングされます。

重量方程式

分散重み付けモデルのアンサンブルは、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;
  }

すべての訓練サンプルについてこれが完了すると、各モデルの総誤差がその重みを計算するために使用されます。これらの重みは合計され、最終的な重みの合計が1になるようにスケーリングされます。このアプローチにより、誤差が小さいモデルに最終的なアンサンブル予測でより大きな影響が与えられ、特にモデルが異なる予測精度を示すシナリオでは予測が改善される可能性があります。


一般的な回帰ニューラルネットワークに基づく補間組み合わせ

これまで説明したアンサンブル手法は、コンセンサスモデルがクリーンなデータで訓練されている場合にうまく機能します。ただし、訓練データにノイズが多い場合、モデルの一般化が不十分になる可能性があります。この問題に対処するための効果的な回帰法の1つは、一般回帰ニューラルネットワーク(GRNN)です。従来の回帰法と比較した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実装には、いくつかの重要なコンポーネントが含まれます。コンストラクタは、内部および外部反復回数やシグマ重みの開始標準偏差などのパラメータを初期化します。

//+------------------------------------------------------------------+
//| 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メソッドは、入力予測値とターゲット値を含む訓練データを保存し、シグマ重みを初期化します。続いて、焼きなまし法を用いてシグマ重みを反復的に最適化することで、GRNNモデルの訓練をおこないます。訓練中は、シグマ重みに摂動(微小な変化)を加え、その摂動後の重みに対する交差検証誤差を計算します。摂動の採用可否は、誤差と温度パラメータに基づいて決定され、温度は徐々に下げられていき、探索の集中度を高めていきます。

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

予測メソッドは、入力ベクトルと各訓練データポイント間の距離を計算し、入力からの距離に基づいて訓練データポイントに重みを付け、訓練データポイントのターゲット値の加重平均として予測出力を計算します。シグマ重みは、各訓練データポイントが予測に与える影響を決定します。

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

交差検証はモデルの性能を評価し、シグマ重みを最適化するために使用されます。一方、焼きなまし法は最適なシグマ重みを見つけるためのメタヒューリスティックな最適化アルゴリズムとして機能します。最終的に、GRNNはカーネルベースの補間を実行し、予測は訓練データポイント間の重み付き補間として得られます。

GRNNに基づくアンサンブルは、クラス「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;
  } ;

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クラスに実装された 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クラスは、多層パーセプトロン(MLP)を定義しており、これは教師あり学習タスクで使用される人工ニューラルネットワークの一種です。このクラスには、以下のようなプロパティが含まれています。

  • 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:隠れ層で使用される活性化関数

クラスにはprivateメソッドとpublicメソッドの両方が含まれます。 privateメソッドには以下のようなものがあります。

  • create:指定された構成に基づいて、重み行列や隠れ層出力用のメモリを割り当て、ネットワーク構造を初期化します。
  • calculate:入力データをネットワークに伝播させ、重みと活性化関数を適用して出力を計算します。
  • backprop:バックプロパゲーションアルゴリズムを実装しており、予測出力と実際の出力との誤差に基づいて重みを調整します。

publicメソッドには以下のようなものがあります。

  • 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クラスとして実装された6つのアンサンブルメソッドの定義
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)
プライスアクション分析ツールキットの開発(第5回):Volatility Navigator EA プライスアクション分析ツールキットの開発(第5回):Volatility Navigator EA
市場の方向性を判断するのは簡単ですが、いつエントリーするかを知るのは難しい場合があります。連載「プライスアクション分析ツールキットの開発」の一環として、エントリーポイント、テイクプロフィットレベル、ストップロスの配置を提供する別のツールを紹介できることを嬉しく思います。これを実現するために、MQL5プログラミング言語を利用しました。この記事では、各ステップについて詳しく見ていきましょう。
古典的な戦略を再構築する(第12回):EURUSDブレイクアウト戦略 古典的な戦略を再構築する(第12回):EURUSDブレイクアウト戦略
MQL5で収益性の高いブレイクアウト取引戦略を構築する挑戦に、ぜひご参加ください。EURUSDペアを選択し、時間枠で価格ブレイクアウトを取引しましたが、私たちのシステムでは偽のブレイクアウトと真のトレンドの始まりを区別するのが難しかったです。そこで、損失を最小限に抑えながら利益を増やすことを目的としたフィルターをシステムに組み込みました。最終的にはシステムを収益性の高いものにし、誤ったブレイクアウトに対する耐性を高めることに成功しました。
MQL5で自己最適化エキスパートアドバイザーを構築する(第2回):USDJPYスキャルピング戦略 MQL5で自己最適化エキスパートアドバイザーを構築する(第2回):USDJPYスキャルピング戦略
今日は私たちと一緒にUSDJPYペアを中心とした取引戦略の構築に挑戦するしましょう。日足のローソク足パターンは、潜在的により強い動きがあるため、日足パターンで形成されるローソク足パターンを取引します。私たちの当初の戦略は利益を生み、これにより獲得した資本を保護するために、戦略を継続的に改良し、安全性をさらに高める努力を続けることができました。
出来高による取引の洞察:トレンドの確認 出来高による取引の洞察:トレンドの確認
強化型トレンド確認手法は、プライスアクション、出来高分析、そして機械学習を組み合わせることで、真の市場動向を見極めることを目的としています。この手法では、取引を検証するために、価格のブレイクアウトと平均比50%以上の出来高急増という2つの条件を満たす必要があります。さらに、追加の確認手段としてLSTMニューラルネットワークを活用します。システムはATR (Average True Range)に基づいたポジションサイズ設定と動的リスク管理を採用しており、誤ったシグナルを排除しつつ、多様な市場環境に柔軟に対応できる設計となっています。