知っておくべきMQL5ウィザードのテクニック(第18回):固有ベクトルによるニューラルアーキテクチャの探索
序文
MQL5ウィザードの実装に関する本連載の続きとして、ニューラルアーキテクチャ検索(英語)を取り上げ、特に、ネットワーク訓練を迅速におこなうこの過程において、固有ベクトルが果たす役割について考察します。ニューラルネットワークは、入力データ(x)に適用することで目標値(y)を提供する定型表現を考案する手助けをします。それは、ちょうど二次方程式が曲線を描くのと同じです。この点で、ニューラルネットワークがデータセットへの曲線の当てはめであることは間違いありません。ただし、xとyのデータポイントは多次元である可能性があり、実際しばしば多次元です。これが、ニューラルネットワークが(多次元データを扱うことができるために)非常に人気を集めている理由です。とはいえ、定型表現を導き出すという原理は残っています。これが、ニューラルネットワークはそれを導き出すための単なる手段であるが、唯一の手段ではない理由です。はじめに
この記事のケースのように、訓練データセットとその目標の関係を定義するためにニューラルネットワークを使用することを選択した場合、このネットワークはどのような設定を使用するのかという問題と戦わなければなりません。ネットワークにはいくつかの種類があり、これは適用可能な設計や設定もまた多くの種類があることを意味します。この記事では、多層パーセプトロンと呼ばれる非常に基本的なケースを考えます。このタイプでは、私たちがこだわる設定は隠れ層の数と各隠れ層のサイズだけです。
NASは通常、これら2つの設定やその他多くの設定を特定するのに役立ちます。例えば、単純なMLPであっても、どの種類の活性化を使用するか、どの初期重みを使用するか、そして初期バイアスは、すべてネットワークのパフォーマンスと精度に影響を与える要素です。しかし、探索空間は非常に広範であり、適度な大きさのデータセットでさえ順伝播と逆伝播に必要な計算リソースは法外であるため、ここでは省略します。
ただし、ここで採用されるNASのアプローチは、理想的な設定を特定するために、行列検索空間の固有値と固有ベクトルに関与するという点で、少し斬新です。従来、NASは、強化学習、進化的アルゴリズム、 ベイズ最適化(英語)、https://en.wikipedia.org/wiki/Random_無作為探索(英語)のいずれかによっておこなわれてきました。
これらの従来のアプローチはそれぞれ、選択した設定(別名アーキテクチャ)でネットワークを訓練し、比較のために目標からそれぞれのパフォーマンスをベンチマークすることで相互検証をおこないます。それらの違いは、いかに網羅的であるか、あるいは探索空間内で網羅的でなくとも効率的であるというアプローチです。強化学習は、その設定に基づいて探索空間内のネットワークを事前に評価するアルゴリズムに依存しており、選択のたびにこのアルゴリズムを改善し続けます。進化的アルゴリズムは、再度目標をベンチマークしてパフォーマンスを再度評価することによって、探索空間内のネットワークを交差させたり組み合わせたりして、必ずしも最初から探索空間内にあったわけではない新しいネットワークに到達します。無作為探索のようなベイズ最適化は、探索空間が配列形式で並び替えられているか、異なるネットワーク設定が探索空間内の座標として認識できるかに依存しています。例えば、探索空間が2次元で、隠れ層の標準的なサイズと隠れ層の数という2つの変数しかない場合、これらのオプションは、以下の画像のような形式で、対角線を昇順(または降順)にこの行列に広がります。
この配置では、どのネットワークのパフォーマンスも、その空間内の「座標」に固定されることになります。したがって、統計的な手法は、その後の選択のたびに使用され、最高のパフォーマンスをもたらすネットワークの選択に磨きをかけることになります。数日前に書かれた固有ベクトルとPCAに関するこちらの記事では、4時間足の時間枠を使用する際にEURUSDを取引するために、理想的な曜日と指標適用価格を選択するために行列探索空間を使用しました。これは、5週間の各日と、考慮された各適用価格における価格変動のクロス行列によるものです。
この記事でも同様の検索方法を考えます。ここで「解決」しようとしている問題はすべてのネットワークを網羅的に訓練することであるため、ベンチマークは、標準的な重みとバイアスで初期化されたネットワークにおける、目標値からのフォワードパススコアになります。データサンプルに対してフォワードランだけをおこない、各設定の平均スコアが行列内のベンチマークとなります。NASにおける固有ベクトルの役割
前述したように、NASに使用する固有行列は、簡潔にするために2次元とします。すべての隠れ層が同じサイズを持つ単純なMLPを考えた場合、私たちが答えたい唯一の2つの質問は、MLPがいくつの隠れ層を持つべきか、そしてそれぞれの隠れ層はどのくらいのサイズなのかということです。
これらの質問に対する可能な答えは、各層の大きさと数の組み合わせにおける各ネットワークのデフォルトパフォーマンスを記録した行列で簡単に示すことができます。行列表にあるように、ネットワークの設定はさまざまですが、入力層と出力層は標準的なものです。この記事では、サイズ4の入力層とサイズ1の出力層を使用します。ここでは、直近の4つの終値から次の終値を予想するという、通常のシナリオを考えています。テスト銘柄は4時間足で2022年のEURJPYです。つまり、2022年の終値は4時間足となります。このモデルを「訓練」する際におこなっているのは、すべてのネットワーク設定について、年間を通じて目標値からの平均偏差を記録することです。私たちの設定は、行列の行に沿って1つの隠れ層から最大10隠れ層まで、列は2から11までの隠れ層サイズを特徴とします。これらのテスト設定は任意です。本稿の最後に完全なソースコードが添付されているので、お好みに応じてカスタマイズしてください。
繰り返しになりますが、モデルの「訓練」では、標準的なデフォルトの重みとバイアスを使用して2022年の1年間にわたり、利用可能な各ネットワークを、各バー予測を実際の終値に対してベンチマークしながら、1回だけフォワードパスすることになります。この「訓練」では、バックプロパゲーションや典型的なネットワークの訓練はおこなわれません。
MLPの実装には、こちらの記事で紹介したネットワーククラスを利用しています。単純に、層の総数を定義するサイズの整数配列が必要で、各インデックスの整数値が層サイズを設定します。
この記事と連載がMQL5ウィザードにスポットを当てているにもかかわらず、上記の「訓練」は前回の固有ベクトルの記事の場合と同様にスクリプトでおこなわれ、ウィザードで組み立てられたEAでテストするためのシグナルクラスのインスタンスをコード化するために、その結果/推奨を使用します。組み立てられたEAのテストでは、各バーまたは各新しいデータポイントで通常のネットワーク訓練をおこないます。推薦されたネットワークのストラテジーテスターでの結果は、コントロールとして、最悪の推薦に対してベンチマークされ、固有ベクトルと固有値がNASにおいて有益かどうかを評価することができます。
固有ベクトルと固有値について前回説明したことをもう一度まとめると、次元削減によって、分析対象の行列から単一のベクトルが得られることがわかります。つまり、私たちの場合、行列で持っている各ネットワークの記録されたパフォーマンスは、ベクトルに縮小されます。前回の記事では、4時間足でEURJPYペアの1年間の変動幅の大部分を捉えた平日と適用価格を取得したいと考えました。これは、投影された行列内の固有ベクトルの最大値が目標と最も正の相関関係にあるため、それらに焦点を当てたことを意味します。
この場合、行列には目標値からの偏差が記録されていますが、これは行列にあるものが各ネットワークのエラーファクターであることを意味しています。テスト目的では、最も誤差の少ないネットワークを使用したいので、層数と各層のサイズによるネットワークの選択は、射影行列から検索された各固有ベクトルの最小値となります。前述したように、この前処理はすべてスクリプトで処理され、次の5つのセクションに分けられます。 a) ネットワークを初期化します。
//initialise networks ArrayResize(__M.row, __SIZE); for(int r = 0; r < __SIZE; r++) { for(int c = 0; c < __SIZE; c++) { ArrayResize(__M.row[r].col, __SIZE); ArrayResize(__M.row[r].col[c].settings, 2 + __LEAST_LAYERS + r); ArrayFill(__M.row[r].col[c].settings, 0, __LEAST_LAYERS + r + 2, __LEAST_SIZE + c); __M.row[r].col[c].settings[0] = __INPUTS; __M.row[r].col[c].settings[__LEAST_LAYERS + r + 1] = __OUTPUTS; __M.row[r].col[c].n = new Cnetwork(__M.row[r].col[c].settings, __initial_weight, __initial_bias); } }
b) ネットワークをベンチマークします。
//benchmark networks int _buffer_size = (52*PeriodSeconds(PERIOD_W1))/PeriodSeconds(Period()); PrintFormat(__FUNCSIG__ + " buffered: %i", _buffer_size); if(_buffer_size >= __INPUTS) { for(int i = _buffer_size - 1; i >= 0; i--) { for(int r = 0; r < __SIZE; r++) { for(int c = 0; c < __SIZE; c++) { vector _in,_out; vector _in_new,_out_new,_in_old,_out_old; _in_new.CopyRates(Symbol(), Period(), 8, i + 1, __INPUTS); _in_old.CopyRates(Symbol(), Period(), 8, i + 1 + 1, __INPUTS); _out_new.CopyRates(Symbol(), Period(), 8, i, __OUTPUTS); _out_old.CopyRates(Symbol(), Period(), 8, i + 1, __OUTPUTS); _in = Norm(_in_new, _in_old); _out = Norm(_out_new, _out_old); __M.row[r].col[c].n.Set(_in); __M.row[r].col[c].n.Forward(); __M.row[r].col[c].benchmark += fabs(__M.row[r].col[c].n.output[0]-_out[0]); } } } }
c) 分析行列にベンチマークをコピーします。
//copy benchmarks to analysis matrix matrix _m; _m.Init(__SIZE, __SIZE); _m.Fill(0.0); for(int r = 0; r < __SIZE; r++) { for(int c = 0; c < __SIZE; c++) { _m[r][c] = __M.row[r].col[c].benchmark; } }
d) 行列を正規化し、固有ベクトルと固有値を生成します。
//generating eigens PrintFormat(" for: %s, with: %s", Symbol(), EnumToString(Period())); matrix _z = ZNorm(_m); matrix _cov_col = _z.Cov(false); matrix _e_vectors; vector _e_values; _cov_col.Eig(_e_vectors, _e_values);
e)そして最後に固有ベクトルを解釈して、射影行列から理想と最悪のネットワークの層番号と層サイズを取り出します。
//interpreting the eigens from projection matrix _t = _e_vectors.Transpose(); matrix _p = _m * _t; vector _max_row = _p.Max(0); vector _max_col = _p.Max(1); string _layers[__SIZE]; for(int i=0;i<__SIZE;i++) { _layers[i] = IntegerToString(i + __LEAST_LAYERS)+" layer"; } double _nr_layers[]; _max_row.Swap(_nr_layers); //since network performance inversely relates to network deviation from target PrintFormat(" est. ideal nr. of layers is: %s", _layers[ArrayMinimum(_nr_layers)]); PrintFormat(" est. worst nr. of layers is: %s", _layers[ArrayMaximum(_nr_layers)]); string _sizes[__SIZE]; for(int i=0;i<__SIZE;i++) { _sizes[i] = "size "+IntegerToString(i + __LEAST_SIZE); } double _size_nr[]; _max_col.Swap(_size_nr); PrintFormat(" est. ideal size of layers is: %s", _sizes[ArrayMinimum(_size_nr)]); PrintFormat(" est. worst size of layers is: %s", _sizes[ArrayMaximum(_size_nr)]);100の検索スペースで上記のスクリプトを実行すると、数秒間実行されます。添付のスクリプトでは、スペースのサイズ属性をグローバル変数として設定し、ユーザーがそれを変更することで、より精度の高いものを作成できるようにしています。さらに、ネットワークインスタンスとそのベンチマークを扱うための構造体が必要でした。これはヘッダーで以下のように定義されています。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct Scol { int settings[]; Cnetwork *n; double benchmark; Scol() { ArrayFree(settings); benchmark = 0.0; } ~Scol(){ delete n; }; }; struct Srow { Scol col[]; Srow(){}; ~Srow(){}; }; struct Smatrix { Srow row[]; Smatrix(){}; ~Smatrix(){}; }; Smatrix __M; //matrix of networks
EAのテスト
理想的なネットワーク設定のスクリーニングに役立つ上記のスクリプトを実行すると、EURJPYに4時間足でアタッチした場合、次のようなログが得られます。
2024.05.03 18:22:39.336 nas_1_changes (EURJPY.ln,H4) void OnStart() buffered: 2184 2024.05.03 18:22:42.209 nas_1_changes (EURJPY.ln,H4) for: EURJPY.ln, with: PERIOD_H4 2024.05.03 18:22:42.209 nas_1_changes (EURJPY.ln,H4) est. ideal nr. of layers is: 6 layer 2024.05.03 18:22:42.209 nas_1_changes (EURJPY.ln,H4) est. worst nr. of layers is: 9 layer 2024.05.03 18:22:42.209 nas_1_changes (EURJPY.ln,H4) est. ideal size of layers is: size 2 2024.05.03 18:22:42.209 nas_1_changes (EURJPY.ln,H4) est. worst size of layers is: size 4推奨されるネットワーク設定は6層のネットワークで、それぞれのサイズは2です。余談ですが、行列のベンチマークに使用された目標データ(y値)は、0.0から1.0の範囲になるように正規化されています。0.5の数値は、結果としての価格変動が0であることを意味し、0.5未満の数値は結果としての価格下落、0.5を上回る数値は価格上昇を意味します。この正規化をおこなう関数のコードを以下に示します。
//+------------------------------------------------------------------+ //| Normalization (0.0 - 1.0, with 0.5 for 0 | //+------------------------------------------------------------------+ vector Norm(vector &A, vector &B) { vector _n; _n.Init(A.Size()); if(A.Size() > 0 && B.Size() > 0 && A.Size() == B.Size() && A.Min() > 0.0 && B.Min() > 0.0) { int _size = int(A.Size()); _n.Fill(0.5); for(int i = 0; i < _size; i++) { if(A[i] > B[i]) { _n[i] += (0.5*((A[i] - B[i])/A[i])); } else if(A[i] < B[i]) { _n[i] -= (0.5*((B[i] - A[i])/B[i])); } } } return(_n); }この正規化が必要だったのは、私たちが考えている小さなデータセットでは、出力として負と正の値を扱うことができる重みとバイアスを開発するためにネットワークを訓練するには、広範囲に及ぶ大きなデータセット、より複雑なネットワーク設定、そして確かに多くの計算リソースが必要になるからです。いずれのシナリオも本稿では検討しませんが、実現可能だと判断されれば、思い切って挑戦することもできます。つまり、正規化することで、控えめな訓練とデータセットで、ネットワークから高感度な結果を得ることができるのです。
推奨ネットワーク構成である6層、サイズ2でテストを実行すると、以下のようなレポートとエクイティカーブが得られます。
また、スクリプトのログ結果には、9層のネットワーク構成(サイズは4)が記録されていました。このネットワーク構成に対して、同一のEA入力設定でテストを実行すると、次のような結果が得られます。
衝撃的なことに、結果はほとんど同じでした。なぜでしょうか。これを説明できる理論的な理由はいくつかあります。
ニューラルネットワークは、冗長性 に悩まされることがあります。異なる設定(またはアーキテクチャ)は、異なる構造を持っているにもかかわらず、データ内の同じ基本的な関係を学習します。どちらのパスでも、ネットワークは訓練中だったので、重みとバイアスの両方が改善されていたことを思い出してください。つまり、分散が大きい固有ベクトルはより幅広い特徴量を捉え、分散が小さい固有ベクトルは細部に焦点を当てますが、どちらのネットワーク構成でも優れたパフォーマンスを発揮するための基本を学習することができます。
隠れ層の数とそのサイズは、この状況におけるネットワークのパフォーマンスを決定する重要な要因ですが、活性化関数の選択(ここではSoftplusを使用)や使用する学習率のように、他の支配的な要因が作用している可能性もあります。これらのそれぞれ、あるいはすべてがネットワークのパフォーマンスに不釣り合いな影響を与えた可能性があります。
もうひとつ考えられるのは、検索スペースの制限です。10種類の層サイズと10種類の隠し層オプションを検討し、すべて長方形の形に変形させました。このため、この特定のデータセットをマッピングする際に、可能なネットワークの組み合わせが本質的に制限される可能性があります。
結論
ニューラルネットワークの構成から選択できる範囲が限られている場合に、固有ベクトルと固有値を使用して非正統的な方法でNASを実行する方法を見てきました。この過程は、隠れ層の形(この記事では長方形のみを考慮)や活性化の種類を追加することによって、分析行列に含まれなかった他の要因を含めたり考慮したりするために、規模を拡大したり、おそらく拡張したりすることができます。後者は、この記事で取り上げたような行列に追加するのが最も簡単です。というのも、主な活性化は2、3種類しかないため、これは単に、列の数を3倍にし、固有ベクトル解析の前提条件を満たす正方行列が維持されるように行の数も増やすことを意味するからです。考慮すべき様々な形態が明確な型として列挙されていれば、隠れ層の形態の追加も同様におこなうことができます。 メモ:
添付ファイルは、こちらとこちらにあるEAウィザードガイドに従って使用することができます。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/14845
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索