
MQL5における数値予測を強化するアンサンブル法
はじめに
機械学習では、性能に差のある複数の予測モデルが生成されることがよくあります。一般的には、それらのモデルを評価したうえで、実運用において最も高いパフォーマンスを示すモデルを選択するのが一般的です。しかし本記事では、一見すると劣っているように見えるモデルであっても、それらの出力を組み合わせることで、全体としての予測性能を向上させる可能性について検討します。予測の統合に用いるさまざまな手法を取り上げ、それらを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.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





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索