知っておくべきMQL5ウィザードのテクニック(第23回):CNN
はじめに
本連載では、MQL5ウィザードが提供する迅速なテストとプロトタイピング環境を利用して、トレーダーにとって有益な機械学習と統計のアイデアを見ていきます。ゴールは1つの記事で1つのアイデアを見ることに変わりはなく、この作品については当初、少なくとも2つの記事が必要だと考えていたが、どうやら1つの記事に絞ることができそうです。 畳み込みニューラルネットワーク(CNN)は、その名が示すように、カーネルによって多次元のデータを畳み込み処理します。
これらのカーネルはネットワークの重みを持ち、多次元の入力データと同様に、通常は行列形式です。後述するように、フィードフォワード中に入力データ行列を反復することで、各反復は実質的に入力データをサイクルします。この「サイクル」が「畳み込み」という名前の由来となっています。
そこでこの記事では、CNNに関与する主要なステップを紹介し、これらのステップを実装する簡単なMQL5クラスを構築し、このクラスをカスタムMQL5ウィザードシグナルクラスに統合し、最後にこのシグナルクラスから組み立てたEAでテスト実行をおこないます。
CNNは一般的に複雑なニューラルネットワークで、その主な用途は、前回GANで見たように、ビデオや画像処理です。しかし、本物の画像や画像中の被写体を偽物から識別するように訓練されたGANとは異なり、CNNは、入力データ(多くの場合画像ピクセル)をさまざまなサブグループに分割し、各サブグループが入力データの重要な特性や非常に重要な特性を捉えるようにするという点で、より分類器に近い働きをする傾向があります。これらの作成されたサブグループは、しばしば特徴量マップと呼ばれます。
これらの特徴量マップにたどり着くまでのステップは、パディング、フィーディングフォワード、活性化、プーリング、そして最後に、ネットワークが訓練中であれば、バックプロパゲーションです。これらの各ステップを、非常にシンプルな単層CNNで見てみましょう。単層とは、入力データがカーネルの単層を通して処理されることを意味します。CNNの場合は必ずしもそうとは限りません。CNNは多くの層にまたがることがあり、パディング、フォワードフィード、活性化、プーリングという上記の4つのステップが各層で繰り返されるからです。複数層の設定では、上位層から生成された各特徴量マップには、他の重要な構成要素があり、それらは新しい特徴量マップに分割されます。
パディング
これはCNNの始まりを示すもので、このステップを含むかどうかは任意です。パディングとは何をするかと言えば、その名前が示すように、入力データの端に沿ってデータの境界を追加するだけです。基本的に、入力データはパディングされます。入力データは通常1次元以上であり、実際には2次元であることが多いため、行列表現が適切であることが多いです。画像はXY平面上のピクセルでできているので、CNNによる分類は簡単です。
では、なぜパディングが必要なのでしょうか。その必要性は、フィードフォワードステップにおけるカーネルとの畳み込みの性質から生じます。カーネルも入力データと同様に行列形式です。これらはネットワークの重みを担っています。通常、1つの層は複数のカーネルを持ちます。各カーネルは特定の特徴量マップの出力を担当するからです。
カーネル内の重みを入力データに乗算するプロセスは、反復またはサイクル、つまり畳み込みと同義の方法で実行されます。この乗算の最終生成物は、常に入力データよりも次元が小さい特徴量マップ行列です。つまり、パディングのポイントは、ユーザーが特徴量マップを生の入力データと同じ寸法にしたい場合、入力データに余分なデータの境界を追加する必要があるということです。
これを理解するために、サイズ6×6の入力データ行列と、3×3の重みのカーネルを考えると、重みの直接乗算は上記のように4×4の行列を生成します。入力データサイズとカーネル行列サイズが与えられた場合の出力行列サイズの公式は以下の通りです。
ここで
- mは入力データ行列の次元
- nは重みカーネルの次元
- pはパディングサイズ
- sはステップ
したがって、特徴量マップの入力データ行列のサイズを維持する必要がある場合、入力行列とカーネル行列のサイズだけでなく、使用するストライドの量も考慮した量だけ、入力データ行列をパッドする必要があります。
パディングには主に3つの方法があります。1つ目はゼロパディングで、入力行列の境界に沿って0を追加し、必要な幅に合わせます。パディングの2つ目の形式はエッジパディングで、行列のエッジの数字が、新しい目標サイズに合わせて新しい境界に沿って繰り返されます。そして最後に、新しい拡大された境界上の数字が入力データ行列内から取得され、そのエッジに沿った数字がミラーラインとして機能するリフレクトパディングがあります。
<
パディングが完了したら、フィードフォワードのステップに進みます。このパディングは、前述のように、ユーザーがマッチングサイズの特徴量マップを必要としない場合は、すべてスキップすることができるという点でオプションです。例えば、CNNが多くの画像を調べ、その中から人の顔の写真を抽出する場合を考えてみましょう。
必然的に、各反復から得られる特徴量マップや出力画像は、入力画像よりもピクセル数が少なくなり、したがって寸法も小さくなるため、この場合、入力画像の初期パッドや拡大をおこなう意味がなくなる可能性があります。次のコードではパディングを実装しています。
//+------------------------------------------------------------------+ //| Pad | //+------------------------------------------------------------------+ void Ccnn::Pad() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); return; } if(padding != PADDING_NONE) { matrix _padded; _padded.Init(inputs.Rows() + 2, inputs.Cols() + 2); _padded.Fill(0.0); for(int i = 0; i < int(_padded.Cols()); i++) { for(int j = 0; j < int(_padded.Rows()); j++) { if(i == 0 || i == int(_padded.Cols()) - 1 || j == 0 || j == int(_padded.Rows()) - 1) { if(padding == PADDING_ZERO) { _padded[j][i] = 0.0; } else if(padding == PADDING_EDGE) { if(i == 0 && j == 0) { _padded[j][i] = inputs[0][0]; } else if(i == 0 && j == int(_padded.Rows()) - 1) { _padded[j][i] = inputs[inputs.Rows() - 1][0]; } else if(i == int(_padded.Cols()) - 1 && j == 0) { _padded[j][i] = inputs[0][inputs.Cols() - 1]; } else if(i == int(_padded.Cols()) - 1 && j == int(_padded.Rows()) - 1) { _padded[j][i] = inputs[inputs.Rows() - 1][inputs.Cols() - 1]; } else if(i == 0) { _padded[j][i] = inputs[j - 1][i]; } else if(j == 0) { _padded[j][i] = inputs[j][i - 1]; } else if(i == int(_padded.Cols()) - 1) { _padded[j][i] = inputs[j - 1][inputs.Cols() - 1]; } else if(j == int(_padded.Rows()) - 1) { _padded[j][i] = inputs[inputs.Rows() - 1][i - 1]; } } else if(padding == PADDING_REFLECT) { if(i == 0 && j == 0) { _padded[j][i] = inputs[1][1]; } else if(i == 0 && j == int(_padded.Rows()) - 1) { _padded[j][i] = inputs[inputs.Rows() - 2][1]; } else if(i == int(_padded.Cols()) - 1 && j == 0) { _padded[j][i] = inputs[1][inputs.Cols() - 2]; } else if(i == int(_padded.Cols()) - 1 && j == int(_padded.Rows()) - 1) { _padded[j][i] = inputs[inputs.Rows() - 2][inputs.Cols() - 2]; } else if(i == 0) { _padded[j][i] = inputs[j - 1][1]; } else if(j == 0) { _padded[j][i] = inputs[1][i - 1]; } else if(i == int(_padded.Cols()) - 1) { _padded[j][i] = inputs[j - 1][inputs.Cols() - 2]; } else if(j == int(_padded.Rows()) - 1) { _padded[j][i] = inputs[inputs.Rows() - 2][i - 1]; } } } else { _padded[j][i] = inputs[j - 1][i - 1]; } } } // Set(_padded, false); } }
私たちは画像科学者としてではなくトレーダーとしてで目的を持っているので、指標値の入力データ行列を持つことになります。これらの指標値は様々なオプションにカスタマイズすることができますが、ここでは様々な移動平均指標から近い価格ギャップを選択しました。
フィードフォワード(Convolve)
入力データが準備されると、重みの乗算が入力データを横断して層の各カーネルに対して実行され、特徴量マップが生成されます。より小さなサイズの行列を生成する重みの乗算に加えて、バイアスが各行列値に加算され、このバイアスは各重みと同様に各カーネルで一意です。
各カーネルは、入力データの主要な特徴や性質を抽出することに特化した重みとバイアスを持ちます。つまり、より多くの特徴を獲得しようとすればするほど、ネットワーク内でより多くのカーネルを使用することになります。フィードフォワードはConvolve関数によって実行されます。以下はそのコードです。
//+------------------------------------------------------------------+ //| Convolve through all kernels | //+------------------------------------------------------------------+ void Ccnn::Convolve() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); return; } // Loop through kernel at set padding_stride for (int f = 0; f < kernels; f++) { bool _stop = false; int _stride_row = 0, _stride_col = 0; output[f].Fill(0.0); for (int g = 0; g < int(output[f].Cols()); g++) { for (int h = 0; h < int(output[f].Rows()); h++) { for (int i = 0; i < int(kernel[f].weights.Cols()); i++) { for (int j = 0; j < int(kernel[f].weights.Rows()); j++) { output[f][h][g] += (kernel[f].weights[j][i] * inputs[_stride_row + j][_stride_col + i]); } } output[f][h][g] += kernel[f].bias; _stride_col += padding_stride; if(_stride_col + int(kernel[f].weights.Cols()) > int(inputs.Cols())) { _stride_col = 0; _stride_row += padding_stride; if(_stride_row + int(kernel[f].weights.Rows()) > int(inputs.Rows())) { _stride_col = 0; _stride_row = 0; } } } } } }
活性化
畳み込んだ後、生成された行列は、典型的な多層知覚と同じように活性化されます。画像処理において、活性化の最も一般的な目的は、より複雑な関係(例えば2次方程式)も捉えることができるように、非線形データをマッピングする能力をモデル内に導入することです。一般的な活性化アルゴリズムはReLU、漏洩ReLU、Sigmoid、Tanhです。
ReLUは、勾配消失問題をよりうまく処理できるため、一般的に使用される活性化アルゴリズムとしては間違いなくよりポピュラーなものですが、デッドニューロンの問題に直面します。これは、漏洩ReLUによって解決されます。デッドニューロンとは、入力の変化に関係なく、ネットワークの出力が一定の値に更新される状況を指します。これは、重みで初期化され、負の入力が与えられると、負の入力の変動に関係なく静的な出力が得られるネットワークでは大きな問題となります。これは訓練でも起こることで、必然的に重みがゆがむことになります。これは表現能力の損失であり、モデルはより複雑なパターンを表現できなくなります。逆伝播法では、ネットワークを通る勾配の流れは収束が遅くなるか、あるいは完全に停滞します。
そこで漏洩ReLUは、最適化可能な小さな正の値「アルファ」を負の入力に対する小さな勾配として割り当てることで、この問題を部分的に緩和します。また、逆伝播における勾配の流れがより滑らかになることで、一般的なReLUよりも安定した効率的な訓練過程が可能になります。
プーリング
畳み込みの出力である特徴量画像は、活性化された後、プーリングと呼ばれるプロセスでノイズのスクリーニングがおこなわれます。プーリングとは、特徴量マップの縦横の寸法を小さくする処理です。プーリングのポイントは、計算負荷を減らし、ネットワークが扱わなければならないパラメータの量を減らすことです。プーリングは、最小限のデータで各特徴量マップの主要な特性を検出できるため、翻訳不変性にも役立ちます。
プーリングには主に、最大プーリング、平均プーリング、グローバルプーリングの3種類があります。最大プーリングは、畳み込み点における各特徴量行列パッチの最大値を選択します。そして、選ばれた各ポイントは、プールされた行列となる新しい行列にまとめられます。その支持者は、過剰適合の可能性を減らしながら、プールされた特徴量マップの重要な特性のほとんどを保持できると主張しています。
平均プーリングは,畳み込み中の各パッチの平均値を計算し,最大プーリングと同様に,それをプール行列に返します。プール行列のサイズは、プーリングウィンドウのサイズと特徴量マップとの差だけでなく、プーリングストライドにも影響されます。プーリングストライドはしばしば1以上の値で使用され、必然的にプールされた行列は特徴量マップよりもかなり小さくなります。今回は、CNN入門編ということでシンプルにしたいので、プーリングのストライドを1にしています。平均プーリングの支持者は、平均プーリングは最大プーリングよりも微妙で攻撃的でないため、プーリング時に重要な特徴量を見落とす可能性が低いと主張します。
CNNでよく使われる3つ目のプーリングはグローバルプーリングです。このタイプのプーリングでは、畳み込みはおこなわれず、代わりに、特徴量マップの平均を取るか、その最大値を選択することによって、特徴量マップ全体が単一の値に縮小されます。これは多層CNNの最終層で適用されるプーリングの一種で、各カーネルに対して1つの値が目標とされます。
プーリングウィンドウサイズとプーリングストライドサイズはプーリングデータサイズの主要な決定要因です。ストライドが大きいほど、プールされたデータが小さくなる傾向がありますが、一方で、特徴マップサイズとプーリングウィンドウサイズは反比例します。プールされたデータサイズを小さくすることで、ネットワークの活性化とメモリ要件が大幅に削減されます。プーリングはMQL5で以下のように実装されています。
//+------------------------------------------------------------------+ //| Pool | //+------------------------------------------------------------------+ void Ccnn::Pool() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); return; } if(pooling != POOLING_NONE) { for(int f = 0; f < int(output.Size()); f++) { matrix _pooled; if(output[f].Cols() > 2 && output[f].Rows() > 2) { _pooled.Init(output[f].Rows() - 2, output[f].Cols() - 2); _pooled.Fill(0.0); for (int g = 0; g < int(_pooled.Cols()); g++) { for (int h = 0; h < int(_pooled.Rows()); h++) { if(pooling == POOLING_MAX) { _pooled[h][g] = DBL_MIN; } for (int i = 0; i < int(output[f].Cols()); i++) { for (int j = 0; j < int(output[f].Rows()); j++) { if(pooling == POOLING_MAX) { _pooled[h][g] = fmax(output[f][j][i], _pooled[h][g]); } else if(pooling == POOLING_AVERAGE) { _pooled[h][g] += output[f][j][i]; } } } if(pooling == POOLING_AVERAGE) { _pooled[h][g] /= double(output[f].Cols()) * double(output[f].Rows()); } } } output[f].Copy(_pooled); } } } }
バックプロパゲーション(Evolve)
バックプロパゲーションとは、他のニューラルネットワークと同様に、ネットワークの重みとバイアスを調整して「学習」させる段階です。これは訓練の過程でおこなわれ、訓練の頻度は採用するモデルによって決まります。トレーダーが使用する金融モデルの場合、例えば最新の企業収益ニュースに合わせてネットワークを四半期に一度訓練するようにプログラムできるモデルがある一方、主要な経済カレンダーのニュース発表後の日付に合わせて月に一度訓練するモデルもあります。ここで言いたいのは、正しいネットワークの重みとバイアスを持つことは重要だが、おそらくそれ以上に、重みとバイアスを訓練し更新するための明確なプリセット体制を持つことが重要だということです。
一度の訓練で、その後は訓練の必要性を心配することなく使用できるネットワークはあるのでしょうか。多くのシナリオではありえないが、可能性はあります。したがって、ニューラルネットワークを使用した取引をおこなうのであれば、ネットワークの訓練カレンダーを常に用意しておくことが賢明です。
つまり、誤差を計算し、この誤差の変化量を使用して勾配を計算し、その勾配を使用して重みとバイアスを更新するという3つのステップが、逆伝播の典型的なステップです。これらの3つのステップをすべて下記のEvolve関数で実行します。
//+------------------------------------------------------------------+ //| Evolve pass through the neural network to update kernel | //| and biases using gradient descent | //+------------------------------------------------------------------+ void Ccnn::Evolve(double LearningRate = 0.05) { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); return; } for(int f = 0; f < kernels; f++) { matrix _output_error = target[f] - output[f]; // Calculate output layer gradients matrix _output_gradients; _output_gradients.Init(output[f].Rows(),output[f].Cols()); for (int g = 0; g < int(output[f].Rows()); g++) { for (int h = 0; h < int(output[f].Cols()); h++) { _output_gradients[g][h] = LeakyReLUDerivative(output[f][g][h]) * _output_error[g][h]; } } // Update output layer kernel weights and biases int _stride_row = 0, _stride_col = 0; for (int g = 0; g < int(output[f].Cols()); g++) { for (int h = 0; h < int(output[f].Rows()); h++) { double _bias_sum = 0.0; for (int i = 0; i < int(kernel[f].weights.Cols()); i++) { for (int j = 0; j < int(kernel[f].weights.Rows()); j++) { kernel[f].weights[j][i] += (LearningRate * _output_gradients[_stride_row + j][_stride_col + i]); // output[f][_stride_row + j][_stride_col + i]); _bias_sum += _output_gradients[_stride_row + j][_stride_col + i]; } } kernel[f].bias += LearningRate * _bias_sum; _stride_col += padding_stride; if(_stride_col + int(kernel[f].weights.Cols()) > int(_output_gradients.Cols())) { _stride_col = 0; _stride_row += padding_stride; if(_stride_row + int(kernel[f].weights.Rows()) > int(_output_gradients.Rows())) { _stride_col = 0; _stride_row = 0; } } } } } }
最終的な出力は行列であり、このため、このため、誤差の変化量も行列形式で取り込まれることになります。この誤差の変化量が得られたら、最終層に到達する前に活性化されたので、その活性化されたプロダクトを調整する必要があります。そして、この活性化の調整は、誤差の差分と活性化関数の導関数を掛け合わせることによっておこなわれます。
また、出力誤差と出力勾配が行列形式であっても、このプロセスは各カーネルについて繰り返す必要があることに留意してください。そのため、インデクサーを整数fとし、最大サイズがカーネルカウントを超えることのない別の包括的なforループで、これらの各処理を包んでいます。この記事で紹介するテストCNNクラスの出力行列は3つです。CNNでは、様々な移動平均線との価格ギャップをインプットとして提供された証券について、強気、弱気、ウィップソーのマップを提供します。これらの価格差も行列形式です。
出力誤差と出力勾配値は行列形式であり、すでに上で強調した前のステップでプールされているため、それらのサイズはカーネル行列の計量サイズと一致しません。そのため、カーネルの重みを調整するために勾配をどのように使用するかを決定する際に、最初のうちは課題が生じます。ただし、このソリューションはフィードフォワードで適用した畳み込みアプローチに従うため非常に簡単です。フィードフォワードでは、入力データ行列(およびそのパディング)とは異なるサイズのカーネル重み行列がサイクルで乗算され、各ポイントでフォーカスされているウィンドウ上のすべてのカーネル積から単一の値が合計され、出力行列に配置されます。
このテストでは、フィードフォワードで使用されるストライドと一致させるため、ストライドは1だけとします。しかし、バイアスの更新は単一の値であるため、少々厄介です。にもかかわらず、解決策は常に行列の勾配を合計し、この合計に(学習率で調整した後の)古いバイアスを乗算することです。
シグナルクラスへの統合
カスタムシグナルの中でCNNクラスを使用するには、基本的に2つのことを定義しなければなりません。第一に、どのような形式の入力データを使用するか、第二に、出力行列に期待するデータの種類です。入力データは現在の終値と多数(デフォルトでは25)の移動平均価格値の間の価格差であるため、これら2つの質問に対する答えはすでに上で示唆されています。多数の移動平均は、固有の移動平均期間によって区別され、以下に強調するように、GetOutput関数を通して入力行列にこれらを入力します。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalCNN::GetOutput() { int _index = 5; matrix _inputs; vector _ma, _h, _l, _c; _inputs.Init(m_input_size, m_input_size); for(int g = 0; g < m_epochs; g++) { for(int h = m_train_set - 1; h >= 0; h--) { _inputs.Fill(0.0); _index = 0; for(int i = 0; i < m_input_size; i++) { for(int j = 0; j < m_input_size; j++) { if(_ma.CopyIndicatorBuffer(m_ma[_index].Handle(), 0, h, __KERNEL + 1)) { _inputs[i][j] = _c[0] - _ma[0]; _index++; } } } // ... } } ... ... }
そう簡単にはいかないのが、出力行列の目標データです。前述したように、強気か弱気かのマップを手に入れたいのです。わかりやすくするためにこの2つだけにしたのですが(市場が横ばいかどうかの指標は含めなかった)、これに対処するためにソースコードをご自由に修正してください。これをどのように測定しているかというと、各入力データポイントの価格変動後の動きを見ているのです。繰り返しますが、このデータポイントは、移動平均価格の配列に価格ギャップを閉じることを選択した指標の測定値を取りますが、これはお好みに応じて簡単にカスタマイズできます。
ここで、強気の指標として、単一の値ではなく行列で捉えたいと考えているのは、異なるスパンでの高値の変化です。同様に、データポイントを記録した後の最終的な弱気度を捉えるために、異なるスパンでの安値の変化を行列に記録します。これは以下のようなコードになります。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalCNN::GetOutput() { ... for(int g = 0; g < m_epochs; g++) { for(int h = m_train_set - 1; h >= 0; h--) { _inputs.Fill(0.0); _index = 0; ... // _h.CopyRates(m_symbol.Name(), m_period, 2, h, __KERNEL + 1); _l.CopyRates(m_symbol.Name(), m_period, 4, h, __KERNEL + 1); _c.CopyRates(m_symbol.Name(), m_period, 8, h, __KERNEL + 1); //Print(" inputs are: \n", _inputs); CNN.Set(_inputs); CNN.Pad(); //Print(" padded inputs are: \n", CNN.inputs); CNN.Convolve(); CNN.Activate(); CNN.Pool(); // targets as eventual price changes with each matrix a proxy for bullishness, bearishness, or whipsaw action // implying matrices for eventual: // high price changes // low price changes // close price changes, // respectively // // price changes in each column are over 1 bar, 2 bar and 3 bars respectively // & price changes in each row are over different weightings of the applied price with other applied prices // so high is: highs only(H); (Highs + Highs + Close)/3 (HHC); and (Highs + Close)/3 (HC) // while low is: lows only(L); (Lows + Lows + Close)/3 (LLC); and (Lows + Close)/3 (LC) // and close is: closes only(C); (Highs + Lows + Close + Close)/3 (HLCC); and (Highs + Lows + Close)/3 (HLC) // // assumptions here are: // large values in highs mean bullishness // large values in lows mean bearishness // and small magnitude in close imply a whipsaw market matrix _targets[]; ArrayResize(_targets, __KERNEL_SIZES.Size()); for(int i = 0; i < int(__KERNEL_SIZES.Size()); i++) { _targets[i].Init(__KERNEL_SIZES[i], __KERNEL_SIZES[i]); // for(int j = 0; j < __KERNEL_SIZES[i]; j++) { if(i == 0)// highs for 'bullishness' { _targets[i][j][0] = _h[j] - _h[j + 1]; _targets[i][j][1] = ((_h[j] + _h[j] + _c[j]) / 3.0) - ((_h[j + 1] + _h[j + 1] + _c[j + 1]) / 3.0); _targets[i][j][2] = ((_h[j] + _c[j]) / 2.0) - ((_h[j + 1] + _c[j + 1]) / 2.0); } else if(i == 1)// lows for 'bearishness' { _targets[i][j][0] = _l[j] - _l[j + 1]; _targets[i][j][1] = ((_l[j] + _l[j] + _c[j]) / 3.0) - ((_l[j + 1] + _l[j + 1] + _c[j + 1]) / 3.0); _targets[i][j][2] = ((_l[j] + _c[j]) / 2.0) - ((_l[j + 1] + _c[j + 1]) / 2.0); } else if(i == 2)// close for 'whipsaw' { _targets[i][j][0] = _c[j] - _c[j + 1]; _targets[i][j][1] = ((_h[j] + _l[j] + _c[j] + _c[j]) / 3.0) - ((_h[j + 1] + _l[j + 1] + _c[j + 1] + _c[j + 1]) / 3.0); _targets[i][j][2] = ((_h[j] + _l[j] + _c[j]) / 2.0) - ((_h[j + 1] + _l[j + 1] + _c[j + 1]) / 2.0); } } // //Print(" targets for: "+IntegerToString(i)+" are: \n", _targets[i]); } CNN.Get(_targets); CNN.Evolve(m_learning_rate); } } ... }
各データポイントの後に市場がどの程度平坦になるかを記録する3つ目の出力行列は、異なるスパンでの終値の変化の大きさに再び焦点を当てることで表現され、これらのスパンの様々な長さは、上記の強気と弱気の両方の測定に使用されるサイズに一致します。この目標データを新しいバーごとに取り込むことは、モデルが新しいバーごとに訓練していることを意味します。また、これは1つのアプローチに過ぎず、前述のように、この訓練を毎月または四半期ごとなど、より低い頻度で実行することもできます。
しかし、各訓練セッションの後、現在のデータポイントから強気と弱気の見通しを予測する必要があります。これを処理するコードの一部を以下に示します。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CSignalCNN::GetOutput() { ... ... _index = 0; _h.CopyRates(m_symbol.Name(), m_period, 2, 0, __KERNEL + 1); _l.CopyRates(m_symbol.Name(), m_period, 4, 0, __KERNEL + 1); _c.CopyRates(m_symbol.Name(), m_period, 8, 0, __KERNEL + 1); for(int i = 0; i < m_input_size; i++) { for(int j = 0; j < m_input_size; j++) { if(_ma.CopyIndicatorBuffer(m_ma[_index].Handle(), 0, 0, __KERNEL + 1)) { _inputs[i][j] = _c[__KERNEL] - _ma[__KERNEL]; _index++; } } } CNN.Set(_inputs); CNN.Pad(); CNN.Convolve(); CNN.Activate(); CNN.Pool(); double _long = 0.0, _short = 0.0; if(CNN.output[0].Median() > 0.0) { _long = fabs(CNN.output[0].Median()); } if(CNN.output[1].Median() < 0.0) { _short = fabs(CNN.output[1].Median()); } double _neutral = fabs(CNN.output[2].Median()); if(_long+_short+_neutral == 0.0) { return(0.0); } return((_long-_short)/(_long+_short+_neutral)); }
行列には多くのデータポイントがあるため、出力行列から弱気か強気かの感覚を得るために選択される最良のアプローチは、各行列のそれぞれの中央値を読み取ることです。つまり、強気行列では大きな正の値を、弱気行列では大きな負の値を求めたい。フラット市場行列では、この中央値の大きさを求め、これが小さいほど、市場はよりフラットになると予測されます。
つまり、GetOutput関数の結果は、0.5を下回れば弱気、0.5を上回れば強気の見通しを意味する浮動小数点値となります。日次時間枠でEURJPY銘柄に対して3 x 3のサイズの出力行列を生成するためにパディングも使用する、3 x 3カーネル3個を含む5 x 5入力行列の単層CNNを使用して実行されたテストから、プラスマイナス0.5値に非常に近い出力が得られました。つまり、この実装では、0.5を超えるものはロングコンディション関数で100の値が割り当てられ、0.5を下回るものはショートコンディション関数で100の値が割り当てられました。
ストラテジーテスターレポート
組み立てられたシグナルクラスは、こちらとこちらのガイドラインに従いながら、MQL5ウィザードを通じてエキスパートアドバイザー(EA)にまとめられました。日足時間枠で2023年のEURJPYをテストしたところ、以下のような結果が得られました。
これらの結果は、ネットワークの出力値が正規化されていないため、ロングコンディションとショートコンディションの結果が0または100になったものです。ネットワークの結果を正規化しようとすると、開閉閾値を微調整できるようになるため、より「敏感」な結果が得られるはずです。
結論
まとめとして、画像処理によく使われる機械学習アルゴリズムであるCNNを、トレーダーという切り口で見てきました。パディング、フィードフォワード、活性化、プーリングといった主要なステップを、独立したMQL5クラスファイルで調べ、コーディングしました。また、CNNのバックプロパゲーションを掘り下げることで、不等間隔の行列を対にする際に畳み込みが果たす役割に焦点を当てながら、訓練プロセスを見てきました。この記事では単層CNNを紹介しましたが、読者には、この単層クラスをTransformerに積み重ねるだけでなく、さまざまな入力データタイプやターゲット出力データセットを見ることでも探索できる未開拓の領域がたくさんあります。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/15101
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索