取引におけるニューラルネットワーク:予測符号化を備えたハイブリッド取引フレームワーク(最終回)
はじめに
前回の記事では、ハイブリッド取引システムStockFormerの理論的な側面を詳細に検討しました。StockFormerは、予測符号化と強化学習アルゴリズムを組み合わせて市場動向や金融資産の変動を予測するシステムです。StockFormerは複雑な金融市場の課題に対応するため、いくつかの重要な技術とアプローチを統合したハイブリッドフレームワークです。その中核的な特徴は、改良された三つのTransformerブランチを使用しており、それぞれが市場動態の異なる側面を捉える役割を担っています。第一のブランチは資産間の隠れた相互依存関係を抽出し、第二および第三のブランチは短期および長期の予測に焦点を当てることで、現在の市場動向と将来の市場動向の両方を考慮することを可能にしています。
これらのブランチの統合は、アテンション機構のカスケードによって実現されます。これにより、モデルはマルチヘッドブロックから学習する能力が強化され、データ中の潜在的なパターンの検出能力が向上します。その結果、システムは過去のデータに基づくトレンドの分析と予測だけでなく、さまざまな資産間の動的な関係性も考慮することができます。これは、急速に変化する市場環境に適応できる取引戦略の開発において特に重要です。
StockFormerフレームワークのオリジナルの可視化を以下に示します。

前回の記事の実践編では、Transformerモデルの標準的なアテンション機構を強化する基盤として機能するDiversified Multi-Head Attention (DMH-Attn)モジュールのアルゴリズムを実装しました。DMH-Attnは、金融時系列における多様なパターンや相互依存関係の検出効率を大幅に向上させます。これは、特にノイズが多く変動の激しいデータを扱う際に非常に価値があります。
本記事では、モデルの各部分のアーキテクチャと、それらが統一された状態空間を構築する際の相互作用のメカニズムに焦点を当てて作業を続けます。さらに、意思決定エージェントの取引方針の学習プロセスについても検討します。
予測符号化モデル
まず、予測符号化モデルから始めます。StockFormerフレームワークの著者たちは、3つの予測モデルの使用を提案しています。1つは、分析対象の金融資産の動態を記述するデータ内の依存関係を特定することを目的としています。残りの2つは、解析対象のマルチモーダル時系列の今後の動きを予測するように訓練されており、それぞれ異なる計画期間(プランニングホライゾン)を持っています。
3つのモデルはいずれも、改良されたDMH-Attnモジュールを用いたエンコーダ–デコーダTransformerアーキテクチャに基づいています。私たちの実装では、エンコーダと デコーダは別々のモデルとして構築します。
依存関係検索モデル
金融資産の時系列に対する依存関係探索モデルのアーキテクチャは、CreateRelationDescriptionsメソッドで定義されています。
bool CreateRelationDescriptions(CArrayObj *&encoder, CArrayObj *&decoder) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!decoder) { decoder = new CArrayObj(); if(!decoder) return false; }
このメソッドのパラメータには、2つの動的配列へのポインタが含まれており、エンコーダとデコーダのアーキテクチャ記述をそれぞれ渡す必要があります。メソッド内では、受け取ったポインタの有効性を確認し、必要に応じて動的配列オブジェクトの新しいインスタンスを作成します。
エンコーダの最初の層には、入力された生データのすべてのテンソルを受け取れる十分なサイズの全結合層を使用します。
なお、エンコーダは分析対象の履歴全体の深さにわたる過去データを受け取ることを思い出してください。
//--- Encoder encoder.Clear(); //--- if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
生データは取引ターミナルから取得されます。予想される通り、指標や複数の金融商品を含むマルチモーダル時系列データは、それぞれ異なる分布に属します。そのため、まず入力データに対してバッチ正規化層を用いた前処理をおこないます。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
StockFormerの著者たちは、依存関係探索モデルの学習時に入力データの最大50%をランダムにマスクすることを提案しています。モデルは、残りの情報に基づいてマスクされたデータを再構築する必要があります。エンコーダでは、このマスキング処理はDropout層によって実現されています。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronDropoutOCL; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; descr.probability = 0.5f; if(!encoder.Add(descr)) { delete descr; return false; }
これに続いて、学習可能な位置符号化層を追加します。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronLearnabledPE; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
エンコーダは、3つの入れ子になった層から構成されるDiversified Multi-Head Attentionモジュールで締めくくられます。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronDMHAttention; descr.window = BarDescr; descr.window_out = 32; descr.count = HistoryBars; descr.step = 4; //Heads descr.layers = 3; //Layers descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
依存関係探索モデルにおけるデコーダへの入力も同じマルチモーダル時系列データであり、同じマスキングと位置符号化が適用されます。そのため、エンコーダとデコーダのアーキテクチャの大部分は同一です。主な違いは、Diversified Multi-Head AttentionモジュールをクCross-Attentionモジュールに置き換える点であり、これによりデコーダとエンコーダのデータストリームを整列させます。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCrossDMHAttention; //--- Windows { int temp[] = {BarDescr, BarDescr}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.window_out = 32; //--- Units { int temp[] = {prev_count/descr.windows[0], HistoryBars}; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.step = 4; //Heads descr.layers = 3; //Layers descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { delete descr; return false; }
デコーダの出力は元の入力データと比較されるため、モデルの最後には逆正規化層を配置して終了します。
//--- layer 5 prev_count = descr.units[0] * descr.windows[0]; if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronRevInDenormOCL; descr.count = prev_count; descr.layers = 1; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { delete descr; return false; } //--- return true; }
予測モデル
2つの予測モデルは、計画期間が異なるにもかかわらず、同じアーキテクチャを共有しており、そのアーキテクチャはCreatePredictionDescriptionsメソッドで定義されています。注目すべき点として、エンコーダは依存関係探索モデルで先に解析された同じマルチモーダル時系列データを受け取るよう設計されています。そのため、Dropout層を除き、エンコーダのアーキテクチャは完全に再利用されます。予測モデルの学習時には入力マスキングが適用されないためです。
予測モデルのデコーダは、最後のバーの特徴ベクトルのみを入力として受け取り、その値は全結合層を通して処理されます。
//--- Decoder decoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { delete descr; return false; }
先に述べたモデルと同様に、その後にバッチ正規化層が続きます。これは、生データの初期前処理に使用します。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { delete descr; return false; }
本記事では、単一の金融商品の過去データを解析するためのモデル学習に焦点を当てます。この場合、入力データに単一バーの特徴量ベクトルしか存在しないため、位置符号化の効果は最小限になります。そのため、ここでは位置符号化を省略します。しかし、複数の金融商品を解析する場合には、入力データに位置符号化を追加することが推奨されます
次に、三層構成のDiversified Multi-Head Cross-Attentionモジュールが続きます。このモジュールは、対応するEncoderの出力を第2の情報源として使用します。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCrossDMHAttention; //--- Windows { int temp[] = {BarDescr, BarDescr}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.window_out = 32; //--- Units { int temp[] = {1, HistoryBars}; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.step = 4; //Heads descr.layers = 3; //Layers descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { delete descr; return false; }
モデルの出力には、活性化関数を持たない全結合射影層を追加します。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = BarDescr; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { delete descr; return false; } //--- return true; }
ここで2つの重要な点を強調する必要があります。まず第一に、従来のモデルが解析対象の時系列の継続部分の期待値を予測するのとは異なり、StockFormerフレームワークの著者たちは、指標の変化係数を予測することを提案しています。これは、計画期間に関係なく、デコーダの出力ベクトルのサイズが入力テンソルと一致することを意味します。このアプローチにより、デコーダの出力における逆正規化層を省略することが可能になります。さらに、この予測設定では、変化係数と生指標が異なる分布に属するため、逆正規化は不要となります。
第二に、デコーダの出力に全結合層を使用する点についてです。前述の通り、本記事では単一金融商品のマルチモーダル時系列を解析しています。そのため、解析対象のすべての単位系列は異なる程度で相関を示すと予想されます。したがって、それらの変化係数は整合させる必要があります。この場合には、全結合層を使用するのが適切です。ただし、複数の金融商品を並列で解析する場合には、各資産の変化係数を独立に予測できるように、全結合層を畳み込み層に置き換えることが望ましいです。
以上で、予測符号化モデルのアーキテクチャの解説は終了です。設計の詳細な説明は付録に記載されています。
予測符号化モデルの学習
StockFormerフレームワークにおいて、予測符号化モデルの学習は独立したステージとして実装されています。これまでに予測モデルのアーキテクチャを確認したので、ここからはそれらを学習させるためのエキスパートアドバイザー(EA)の構築に移ります。EAの基本的なメソッドの多くは、本連載のこれまでの記事で取り上げた類似プログラムから流用されています。したがって本記事では、主に学習アルゴリズムそのもの、すなわちTrainメソッド内で構成される直接的な学習手順に焦点を当てます。
まず、少し準備作業をおこないます。ここでは、経験再生バッファから学習に用いる軌跡を選択するための確率ベクトルを生成します。利益率が最大の軌跡に高い確率を割り当てることで、学習プロセスを利益性の高い実行例に偏らせ、ポジティブなサンプルを多く含むようにします。
void Train(void) { //--- vector<float> probability = GetProbTrajectories(Buffer, 0.9); //--- vector<float> result, target, state; matrix<float> predict; bool Stop = false; //--- uint ticks = GetTickCount();
この段階では、学習中に中間データを保持するために使用される必要なローカル変数も宣言します。準備が完了したら、学習反復ループを開始します。反復回数の総数は、EAの外部パラメータで定義されています。
for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { int tr = SampleTrajectory(probability); int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast)); if(i <= 0) { iter --; continue; } if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == 0 || !bState.AssignArray(state)) { iter --; continue; } if(!state.Assign(Buffer[tr].States[i + NForecast].state) || !state.Resize((NForecast + 1)*BarDescr) || MathAbs(state).Sum() == 0) { iter --; continue; }
ループ内では、経験再生バッファから1つの軌跡と、その初期環境状態をサンプリングします。次に、選択された状態に履歴データが存在するか、また指定された計画期間に対応する実データが存在することを確認します。これらのチェックを通過した場合、必要な解析深度における履歴値を適切なデータバッファに転送し、すべての予測モデルのフォワードパス(順伝播)を実行します。
//--- Feed Forward if(!RelateEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !RelateDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(RelateEncoder)) || !ShortEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !ShortDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(ShortEncoder)) || !LongEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !LongDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(LongEncoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
ここで重要な点を指摘しておきます。まず、アーキテクチャが同一であるにもかかわらず、各予測モデルはそれぞれ独自のエンコーダを持っています。これにより、学習可能なモデルの総数が増加し、その結果として学習および運用時の計算コストも高くなります。しかし、その一方で、各モデルが自らの特定のタスクに関連する依存関係をより効果的に捉えることが可能になります。
次に、デコーダのメインストリームにおける生入力テンソルの使用についても触れておきます。前述の通り、予測モデルのデコーダは最後のバーのみを入力として受け取ります。しかし、学習時には、すべての場合において解析の全深度にわたる履歴バッファが使用されます。これを明確にするために、リプレイバッファに保存された環境状態は行列として表すことができます。この行列では、行がバーを、列が特徴量(価格や指標など)を表します。最初の行には最後のバーのデータが含まれています。したがって、デコーダの入力サイズよりも大きなテンソルを渡す場合、モデルは単に入力層のサイズに一致する最初のセグメントのみを取得します。これにより、追加のバッファや不要なデータコピーを作成する必要がなくなり、まさに目的にかなった動作となります。
フォワードパスが正常に完了した後、ターゲット値を準備し、バックプロパゲーション(逆伝播)を実行します。依存関係探索モデルの場合、ターゲット値はマルチモーダル時系列データそのものです。そのため、即座にデコーダを通じてバックプロパゲーションを実行し、誤差勾配をエンコーダに伝達することができます。得られた勾配に基づいて、エンコーダのパラメータを適切に更新します。
//--- Relation if(!RelateDecoder.backProp(GetPointer(bState), (CNet *)GetPointer(RelateEncoder)) || !RelateEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
一方で、予測モデルの場合はターゲット値を定義する必要があります。前述のとおり、ここでのターゲットはパラメータの変化係数です。計画期間は、履歴データの解析深度よりも短いと仮定します。したがって、ターゲット値を計算するために、リプレイバッファから指定された期間分だけ先に記録された将来の環境状態を取得します。その後、このテンソルを行列に変換し、各行がバーに対応するようにします。
//--- Prediction if(!predict.Resize(1, state.Size()) || !predict.Row(state, 0) || !predict.Reshape(NForecast + 1, BarDescr) ) { iter --; continue; }
このような行列では、最初の行がより後のバーを表しているため、計画期間よりも1行多く取得します。この切り詰められた行列の最後の行は、現在解析中のバーに対応します。
ここで注意すべき点として、リプレイバッファには正規化されていないデータが格納されていることがあります。したがって、計算された変化係数を意味のある範囲に収めるために、将来値の行列における各パラメータの最大絶対値で正規化をおこないます。その結果、得られる係数は通常、{-2.0, 2.0}の範囲に収まります。
result = MathAbs(predict).Max(0);
短期予測モデルの場合、ターゲットは次のバーにおけるパラメータの変化係数となります。これは、予測行列の最後の2行の差を取り、それを最大値ベクトルで割ることで計算し、得られた結果を対応するバッファに格納します。
target = (predict.Row(NForecast - 1) - predict.Row(NForecast)) / result; if(!bShort.AssignArray(target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
長期予測モデルの場合は、すべてのバーにわたってパラメータの変化係数を合計し、割引係数を適用します。
for(int i = 0; i < NForecast - 1; i++) target += (predict.Row(i) - predict.Row(i + 1)) / result * MathPow(DiscFactor, NForecast - i - 1); if(!bLong.AssignArray(target)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
ターゲット値の全セットが定義されたら、予測誤差を最小化するように予測モデルのパラメータを更新します。具体的には、まず短期予測モデルのデコーダおよびエンコーダを通じてバックプロパゲーションを実行し、その後に長期予測モデルに対して同様の処理をおこないます。
//--- Short prediction if(!ShortDecoder.backProp(GetPointer(bShort), (CNet *)GetPointer(ShortEncoder)) || !ShortEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
//--- Long prediction if(!LongDecoder.backProp(GetPointer(bLong), (CNet *)GetPointer(LongEncoder)) || !LongEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
この段階で学習されたすべてのモデルの更新が完了した後、進捗状況をユーザーに通知するためにログを記録し、その後、次の学習反復へと進みます。
//--- if(GetTickCount() - ticks > 500) { double percent = double(iter) * 100.0 / (Iterations); string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Relate", percent, RelateDecoder.getRecentAverageError()); str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Short", percent, ShortDecoder.getRecentAverageError()); str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Long", percent, LongDecoder.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } }
すべての学習反復が完了したら、学習の進行状況を表示するために使用していたチャート上のコメント欄をクリアします。
Comment(""); //--- PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Relate", RelateDecoder.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Short", ShortDecoder.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Long", LongDecoder.getRecentAverageError()); ExpertRemove(); //--- }
結果を操作ログに出力し、EAの動作終了処理が開始されます。
予測モデル学習用EAの完全なソースコードは、添付ファイル(ファイル:"...\MQL5\Experts\StockFormer\Study1.mq5")にあります。
最後に、本記事で実施したモデル学習では、これまでの研究と同じ入力データ構造を使用したことを付記します。重要な点として、予測モデルの学習はエージェントの行動とは独立した環境状態のみに基づいて行われます。そのため、事前に収集されたデータセットを用いて学習を実行することが可能です。それでは、次の作業段階へと進みます。
方策の学習
予測モデルの学習が進む一方で、次の段階としてエージェントの行動方策の学習に移ります。
モデルアーキテクチャ
まず、この段階で使用されるモデルのアーキテクチャを準備します。アーキテクチャはCreateDescriptionsメソッドで定義されています。ここで重要な点は、StockFormerフレームワークにおいて、ActorとCriticの両方が予測モデルの出力を入力として受け取り、これらをアテンション機構のカスケードによって統合されたサブスペースにまとめることです。私たちのライブラリでは、2つのデータソースを持つモデルを構築できます。そのため、アテンションカスケードは2つの別々のモデルに分割します。1番目のモデルでは、2つの計画期間のデータを整列させます。著者らは、メインストリームには長期計画データを使用することを推奨しています。長期データはノイズに対して比較的影響を受けにくいためです。
2期間整列モデルのアーキテクチャはシンプルです。ここでは以下の2つの層を作成します。
- 全結合入力層
- 内部に3層を持つDiversified Cross-Attentionモジュール
//--- Long to Short predict long_short.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!long_short.Add(descr)) { delete descr; return false; } //--- Layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCrossDMHAttention; //--- Windows { int temp[] = {BarDescr, BarDescr}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.window_out = 32; //--- Units { int temp[] = {1, 1}; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.step = 4; //Heads descr.layers = 3; //Layers descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!long_short.Add(descr)) { delete descr; return false; }
ここでは正規化層は使用しません。というのも、モデルの入力は生データではなく、すでに学習済みの予測モデルの出力であるためです。
2期間整列の結果は、次に依存関係探索モデルのエンコーダから取得した現在の環境状態情報で補強されます。このエンコーダは入力データに適用されます。
依存関係探索モデルは、入力データのマスクされた部分を再構築するように学習されていることを思い出してください。この段階では、各単位時系列が他の一変量系列に基づいて形成された予測状態表現を持っていることが期待されます。したがって、エンコーダの出力は環境状態のノイズ除去済みテンソルとなります。これは、モデルの予測に合わない外れ値が、他の系列から得られた統計値によって補正されるためです。
環境状態情報で予測を補強するモデルのアーキテクチャは、2期間整列モデルに非常に近い構造を持ちます。唯一の違いは、第二データソースの系列長を変更している点です。
//--- Predict to Relate predict_relate.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!predict_relate.Add(descr)) { delete descr; return false; } //--- Layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCrossDMHAttention; //--- Windows { int temp[] = {BarDescr, BarDescr}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.window_out = 32; //--- Units { int temp[] = {1, HistoryBars}; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.step = 4; //Heads descr.layers = 3; //Layers descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!predict_relate.Add(descr)) { delete descr; return false; }
3つの予測モデルの出力を統合サブスペースにまとめるアテンションカスケードを構築した後、次にActorを構築します。Actorモデルの入力は、このアテンションカスケードの出力となります。
//--- Actor actor.Clear(); //--- Input Layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (BarDescr); descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
予測された期待値は口座状態情報と組み合わせます。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = AccountDescr; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
統合された情報は、意思決定ブロックに渡されます。このブロックは、多層パーセプトロン(MLP)として実装されており、出力ヘッドは確率的に構成されています。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 2 * NActions; descr.activation = None; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronVAEOCL; descr.count = NActions; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
モデルの出力では、各方向の取引パラメータが、シグモイド活性化関数を持つ畳み込み層によって調整されます。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = NActions / 3; descr.window = 3; descr.step = 3; descr.window_out = 3; descr.activation = SIGMOID; descr.optimization = ADAM; descr.probability = Rho; if(!actor.Add(descr)) { delete descr; return false; }
Criticも同様のアーキテクチャを持っていますが、口座状態の代わりにエージェントの行動を解析します。Criticの出力には確率的ヘッドは使用されません。すべてのモデルの完全なアーキテクチャは添付ファイルに記載されています。
方策学習手順
モデルアーキテクチャが定義された後は、学習アルゴリズムを整理します。第二段階では、リターンを最大化しつつリスクを最小化する最適なエージェント行動戦略を探索します。
前回と同様に、学習メソッドは準備作業から始まります。ここでは、経験再生バッファから軌跡を選択するための確率ベクトルを、その性能に基づいて生成し、ローカル変数を宣言します。
void Train(void) { //--- vector<float> probability = GetProbTrajectories(Buffer, 0.9); //--- vector<float> result, target, state; bool Stop = false; //--- uint ticks = GetTickCount();
次に、EAの外部パラメータによって設定された反復回数で学習ループに入ります。
for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { int tr = SampleTrajectory(probability); int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast)); if(i <= 0) { iter --; continue; } if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == 0 || !bState.AssignArray(state)) { iter --; continue; }
各反復内では、軌跡とその現在の状態をサンプリングします。必要なデータがすべて揃っているかどうかを必ず確認してください。
予測モデルとは異なり、方策学習では追加の入力データが必要です。環境状態の記述を抽出した後、リプレイバッファから該当する時点の口座残高および保有ポジションを収集します。
//--- Account bAccount.Clear(); float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0]; float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1]; bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance); bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity); bAccount.Add(Buffer[tr].States[i].account[2]); bAccount.Add(Buffer[tr].States[i].account[3]); bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance); //--- double time = (double)Buffer[tr].States[i].account[7]; double x = time / (double)(D'2024.01.01' - D'2023.01.01'); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_MN1); bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_W1); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_D1); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); if(!!bAccount.GetOpenCL()) { if(!bAccount.BufferWrite()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } }
解析対象の状態には、タイムスタンプも追加されます。
この情報を用いて、予測符号化モデルおよびアテンションカスケードを通したフォワードパスを実行し、予測出力を統合サブスペースに変換します。
//--- Generate Latent state if(!RelateEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !ShortEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !ShortDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(ShortEncoder)) || !LongEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL) || !LongDecoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(LongEncoder)) || !LongShort.feedForward(GetPointer(LongDecoder), -1, GetPointer(ShortDecoder), -1) || !PredictRelate.feedForward(GetPointer(LongShort), -1, GetPointer(RelateEncoder), -1) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
注意:この段階では、依存関係探索モデルのデコーダは実行されません。方策学習や実運用では使用されないためです。
次に、Criticを最適化して、エージェントの行動評価における誤差を最小化します。選択された状態から実際の行動をリプレイバッファから取得し、Criticに入力します。
//--- Critic target.Assign(Buffer[tr].States[i].action); target.Clip(0, 1); bActions.AssignArray(target); if(!!bActions.GetOpenCL()) if(!bActions.BufferWrite()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } Critic.TrainMode(true); if(!Critic.feedForward(GetPointer(PredictRelate), -1, (CBufferFloat*)GetPointer(bActions))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Criticのフォワードパスによって得られた行動の推定値は、初期段階ではランダム分布に近い値を示します。しかし、経験再生バッファには、軌跡収集中にエージェントが実際に取った行動に対して得られた報酬も保存されています。そのため、Criticは予測報酬と実際の報酬との誤差を最小化するように学習させることが可能です。
経験再生バッファから実際の報酬を抽出し、Criticのバックプロパゲーションを実行します。
result.Assign(Buffer[tr].States[i + 1].rewards); target.Assign(Buffer[tr].States[i + 2].rewards); result = result - target * DiscFactor; Result.AssignArray(result); if(!Critic.backProp(Result, (CBufferFloat *)GetPointer(bActions), (CBufferFloat *)GetPointer(bGradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
次に、Actorの行動方策の実際の学習に進みます。収集した入力データを用いて、Actorを通したフォワードパスを実行し、現在の方策に従った行動テンソルを生成します。
//--- Actor Policy if(!Actor.feedForward(GetPointer(PredictRelate), -1, (CBufferFloat*)GetPointer(bAccount))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
続いて、生成された行動をCriticを用いて評価します。
Critic.TrainMode(false); if(!Critic.feedForward(GetPointer(PredictRelate), -1, (CNet*)GetPointer(Actor), -1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
注意:Actorの方策を最適化する際には、Criticの学習モードは無効化されます。これにより、誤差勾配をActorに伝播させることが可能になりますが、Criticのパラメータは無関係なデータに基づいて更新されません。
Actorの方策学習は二段階でおこなわれます。第一段階では、経験再生バッファに記録された実際の行動の有効性を評価します。報酬が正の場合、予測された行動テンソルと実際の行動テンソルの誤差を最小化します。これにより、利益の出る方策を教師あり学習で訓練することができます。
if(result.Sum() >= 0) if(!Actor.backProp(GetPointer(bActions), (CBufferFloat*)GetPointer(bAccount), GetPointer(bGradient)) || !PredictRelate.backPropGradient(GetPointer(RelateEncoder), -1, -1, false) || !LongShort.backPropGradient(GetPointer(ShortDecoder), -1, -1, false) || !ShortDecoder.backPropGradient((CNet *)GetPointer(ShortEncoder), -1, -1, false) || !ShortEncoder.backPropGradient((CBufferFloat*)NULL) || !LongDecoder.backPropGradient((CNet *)GetPointer(LongEncoder), -1, -1, false) || !LongEncoder.backPropGradient((CBufferFloat*)NULL) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
重要な点として、この段階では誤差勾配が予測モデルにまで伝播されます。これにより、Actorの方策最適化タスクを支援するように予測モデルがファインチューニングされます。
Critic主導ステージ:Criticからの誤差勾配を伝播させることで、Actorの方策を最適化します。このステージでは、環境内での実際の行動結果に関係なくほうs開くを調整し、現在の方策に対するCriticの評価のみを基に学習します。そのため、行動評価は1%強化して扱います。
Critic.getResults(Result); for(int c = 0; c < Result.Total(); c++) { float value = Result.At(c); if(value >= 0) Result.Update(c, value * 1.01f); else Result.Update(c, value * 0.99f); }
次に、この調整された報酬をCriticにターゲットとして渡し、バックプロパゲーションを実行します。これにより、誤差勾配がActorに伝播され、Actorの出力における誤差勾配が生成されます。この勾配は、行動をより高い利益率に向かわせる方向へ導きます。
if(!Critic.backProp(Result, (CNet *)GetPointer(Actor), LatentLayer) || !Actor.backPropGradient((CBufferFloat*)GetPointer(bAccount), GetPointer(bGradient)) || !PredictRelate.backPropGradient(GetPointer(RelateEncoder), -1, -1, false) || !LongShort.backPropGradient(GetPointer(ShortDecoder), -1, -1, false) || !ShortDecoder.backPropGradient((CNet *)GetPointer(ShortEncoder), -1, -1, false) || !ShortEncoder.backPropGradient((CBufferFloat*)NULL) || !LongDecoder.backPropGradient((CNet *)GetPointer(LongEncoder), -1, -1, false) || !LongEncoder.backPropGradient((CBufferFloat*)NULL) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
得られた勾配は、第一段階の学習と同様に、すべての関連モデルに伝播されます。
その後、ユーザーに学習の進行状況を通知し、次の反復に進みます。
//--- if(GetTickCount() - ticks > 500) { double percent = double(iter) * 100.0 / (Iterations); string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent, Actor.getRecentAverageError()); str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Critic", percent, Critic.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } }
すべての学習反復が完了したら、チャート上のコメントをクリアし、ジャーナルに結果を記録し、プログラムの終了処理を開始します。これは第一段階の学習と同様です。
Comment(""); //--- PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic", Critic.getRecentAverageError()); ExpertRemove(); //--- }
ここで注意すべき点として、アルゴリズムの調整は、モデル学習用のEAだけでなく、環境との相互作用をおこなうEAにも影響を与えています。しかし、環境との相互作用アルゴリズムの調整は、主に上記で説明したActorのフィードフォワード処理を反映した内容となっており、その詳細なロジックは独立した学習課題として残されています。そのため、ここではこれらのアルゴリズムの詳細なロジックには触れません。それぞれの実装を独自に調査してみることをお勧めします。本記事で使用されたすべてのプログラムの完全なソースコードは、添付ファイルに含まれています。
テスト
私たちはMQL5を用いてStockFormerフレームワークの大規模な実装を完了し、作業の最終段階であるモデル学習と、実際の過去データに基づく性能評価に到達しました。
前述の通り、予測モデルの初期学習段階では、以前の研究で収集されたデータセットを使用しました。このデータセットは、EURUSDの2023年全期間のH1時間足データで構成されています。すべてのインジケーターのパラメータはデフォルト値のままとしています。
予測モデルの学習中は、エージェントの行動とは独立した環境状態を記述する過去データのみを使用します。これにより、学習データセットを更新することなくモデルの学習が可能となります。学習は、誤差が狭い範囲内で安定するまで継続されます。
第二の学習段階であるActorの行動方策の最適化は、反復的におこなわれ、学習データセットも現在の方策を反映するよう定期的に更新されます。
学習済みモデルの性能は、MetaTrader 5のストラテジーテスターを用いて、2024年1月の過去データで評価しました。この期間は、学習データセット期間の直後にあたります。結果は以下の通りです。

テスト期間中、モデルは合計15回の取引を実行し、そのうち10回が利益で終了しました。成功率は66%を超えます。非常に良好な結果です。特に注目すべき点は、平均的な利益取引が平均的な損失取引の4倍の大きさであることです。このため、残高チャートには明確な上昇傾向が示されています。
結論
これら2つの記事を通じて、StockFormerフレームワークを詳しく探究しました。StockFormerは、金融市場向けの取引戦略を学習させるための革新的なアプローチを提供します。StockFormerは予測符号化と強化学習を組み合わせており、複数資産間の動的依存関係を捉えつつ、短期および長期の挙動を予測できる柔軟な方策の構築を可能にします。
StockFormerにおける三分岐の予測符号化構造は、短期トレンド、長期変化、および資産間関係を反映する潜在表現の抽出を可能にします。これらの表現の統合は、マルチヘッドアテンションモジュールのカスケードを通じて実現され、取引判断の最適化のための統一状態空間を構築します。
実践面では、私たちはMQL5でフレームワークの主要コンポーネントを実装し、モデルを学習させ、実際の過去データでテストしました。実験結果は、提案されたアプローチの有効性を確認しています。しかしながら、これらのモデルを実運用に適用するには、より大規模な過去データセットでの学習および包括的な追加テストが必要です。
参照文献
記事で使用されているプログラム
| # | 名前 | 種類 | 詳細 |
|---|---|---|---|
| 1 | Research.mq5 | EA | サンプル収集用EA |
| 2 | ResearchRealORL.mq5 | EA | Real-ORL法を用いたサンプル収集用EA |
| 3 | Study1.mq5 | EA | 予測学習EA |
| 4 | Study2.mq5 | EA | 方策学習EA |
| 5 | Test.mq5 | EA | モデルテスト用EA |
| 6 | Trajectory.mqh | クラスライブラリ | システム状態とモデルアーキテクチャ記述構造 |
| 7 | NeuroNet.mqh | クラスライブラリ | ニューラルネットワークを作成するためのクラスのライブラリ |
| 8 | NeuroNet.cl | コードベース | OpenCLプログラムコードライブラリ |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/16713
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
市場シミュレーション(第2回):両建て注文(II)
ブラックホールアルゴリズム(BHA)
取引におけるニューラルネットワーク:ウェーブレット変換とマルチタスクアテンションを用いたモデル
PythonとMQL5で構築するマルチモジュール型取引ロボット(第1回):基本アーキテクチャと最初のモジュールの作成
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索