English Русский 中文 Español Português
preview
取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(最終回)

取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(最終回)

MetaTrader 5トレーディングシステム |
11 1
Dmitriy Gizlyk
Dmitriy Gizlyk

はじめに

前回の記事では、金融市場の特性に適応するよう設計された時系列予測モデルであるACEFormerフレームワークについて紹介しました。そこでは、Probabilistic Attention(確率的注意)機構の基本原理、OpenCLカーネルレベルでの実装アルゴリズム、そして予測精度を維持しながら計算効率を向上させるための手法について検討しました。しかし、その議論では、もう1つ重要な側面が扱われていませんでした。それは、これらの計算メカニズムをメインプログラムへどのように統合するか、そして計算モジュールと取引アルゴリズム間でどのように信頼性の高い通信を確立するかという点です。

本記事では、ACEFormerフレームワークの著者らが提案した手法の実装をさらに進めます。主な焦点は、プログラム側における対応アルゴリズムの構築です。ただし、技術的な実装へ進む前に、ACEFormerの基本概念と主要な利点について簡単に振り返ります。このフレームワークの中心には、金融時系列データからノイズを除去するために設計されたACEEMD (Alias Complete Ensemble Empirical Mode Decomposition with Adaptive Noise)アルゴリズムがあります。ACEEMDは、平滑化の際に重要な情報を失うことなく、境界効果を効果的に抑制し、チャート上の主要な転換点を維持します。特に注目されるのは、第1固有モード関数(IMF)です。この成分を明示的に除去することで、有用なシグナルを過度に抑制することなく、高周波ノイズを低減できます。

この段階の出力は、時系列データの適応的なモード表現となります。この表現は、Probabilistic Attentionを備えたTransformerアーキテクチャ上に構築された蒸留モジュールへの入力として使用されます。この手法により、市場ノイズの除去だけでなく、モデルが過去データ内の本当に情報価値の高い領域へ集中できるようになります。その結果、高いボラティリティや強い確率性が存在する市場環境においても、予測精度の向上が期待できます。

処理後の表現は、さらに通常のSelf-Attentionブロックへ入力されます。これにより、モデルは元の系列全体の文脈情報を取得し、異なる時間スケール間での一貫性を向上させます。局所的な特徴を抽出するProbabilistic Attentionと、系列全体を理解するグローバルSelf-Attentionを組み合わせることで、モデルは急激な市場反転と継続的なトレンドの両方へ同時に対応できます。

アーキテクチャの最終段階には予測Headが配置され、高次元の特徴表現を具体的な数値予測へ変換します。

ACEFormerは、経験的モード分解の頑健性と、深層Attention機構の柔軟性という2つの手法の利点を組み合わせています。同時に、このモデルはユーザーターミナル上でリアルタイム運用できる程度にコンパクトであり、過剰な計算リソースを必要としません。モジュール構造と並列処理への対応により、ACEFormerは短期的なモメンタム戦略から中期的なポジション取引システムまで、幅広い取引戦略へ容易に適応できます。

著者らが作成したACEFormerフレームワークの可視化を以下に示します。



Probabilistic Attentionモジュールの構築

OpenCLプログラム側での確率的注意機構の実装が完了したので、次の段階に進みます。それは、メインプログラム側で対応する高レベルモジュールを開発することです。そのため、Probabilistic Attentionアルゴリズム全体をカプセル化する専用クラスCNeuronMHProbAttentionを作成します。

この新しいクラスはCResidualConvを継承します。この親クラスには、残差接続によって結合された2つの連続する畳み込み層から構成されるアーキテクチャがすでに実装されています。これにより、親クラスが提供する順伝播機構の仕組みを変更することなく、Probabilistic Attentionロジックの実装だけに集中できます。その結果、高いモジュール性を維持しながら、他のモデルコンポーネントとのスムーズな統合が可能になります。

新しいクラスの構造を以下に示します。

class CNeuronMHProbAttention :  public CResidualConv
  {
protected:
   uint                       iWindow;
   uint                       iWindowKey;
   uint                       iHeads;
   uint                       iUnits;
   uint                       iTopKQuerys;
   uint                       iRandomKeys;
   int                        ibScore;
   //---
   CNeuronConvOCL             cQKV;
   CNeuronBaseOCL             cQ;
   CNeuronBaseOCL             cKV;
   CNeuronBaseOCL             cRandomK;
   CNeuronBaseOCL             cMHAttentionOut;
   CNeuronConvOCL             cPooling;
   CNeuronTransposeOCL        cTranspose[2];
   CNeuronConvOCL             cScaling;
   //---
   virtual bool               RandomKeys(CBufferFloat* indexes, int random, int units, int heads);
   virtual bool               QueryImportance(void);
   virtual bool               TopKIndexes(void);
   //---
   virtual bool               AttentionOut(void);
   virtual bool               AttentionInsideGradients(void);
   //---
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               calcInputGradients(CNeuronBaseOCL *prevLayer) override;

public:
                              CNeuronMHProbAttention(void) {};
                             ~CNeuronMHProbAttention(void) {};
   //---
   virtual bool               Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                                   uint window, uint window_key, uint heads, uint units_count,
                                   ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int                Type(void)   const   {  return defNeuronMHProbAttention;   }
   //---
   virtual bool               Save(int const file_handle) override;
   virtual bool               Load(int const file_handle) override;
   //---
   virtual bool               WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void               SetOpenCL(COpenCLMy *obj) override;
  };

CNeuronMHProbAttentionの構造体は、Probabilistic Attention機構の処理手順をそのまま反映しています。ランダムなKeyの生成とQueryの重要度推定から始まり、勾配のバックプロパゲーションに至るまで、各処理段階が順を追って実装されています。各機能は独立したモジュールとして実装されているため、責務が明確に分離されるとともに、各処理を細かくカスタマイズできる柔軟な設計となっています。

アーキテクチャ上の特徴として、CNeuronMHProbAttentionに含まれる内部オブジェクトはすべて動的確保を伴わない固定メンバーとして宣言されています。この設計により、メモリ管理が簡潔になるだけでなく、クラスのインスタンス生成時に動的にリソースを割り当てる必要がなくなります。その結果、コンストラクタとデストラクタはいずれも空の実装で済み、オブジェクトの生成および破棄の信頼性と予測可能性が向上しています。

内部オブジェクトおよび各種変数の初期化は、すべてInitメソッドに集約されています。このメソッドは実質的にクラスのコンストラクタとして機能し、各コンポーネントの組み立て、パラメータの設定、および必要なリソースの確保を担当します。

bool CNeuronMHProbAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                  uint window, uint window_key, uint heads, uint units_count,
                                  ENUM_OPTIMIZATION optimization_type, uint batch
                                 )
  {
   if(!CResidualConv::Init(numOutputs, myIndex, open_cl, window, window, units_count, optimization_type, batch))
      return false;

新しいクラスは、残差接続を備えた畳み込みアーキテクチャを実装するCResidualConvを継承しているため、初期化処理はまず親クラスの対応するメソッドを呼び出すことから始まります。これにより、畳み込み層、残差接続、および各種バッファの初期化を含む基盤となるインフラストラクチャが直ちに構築されます。その結果、これらの機能を改めて実装する必要がなくなり、独自のAttentionロジックの実装に専念できます。

続いて、Attention機構の動作を規定する主要なパラメータを初期化します。

iWindow = window;
iWindowKey = MathMax(5, window_key);
iHeads = MathMax(1, heads);
iUnits = units_count;
iTopKQuerys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));
iRandomKeys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));

おなじみのパラメータに加えて、Probabilistic Attentionに固有の2つの新しい変数が導入されます。

  • iTopKQuerys:後続の処理のために選択される、最も情報量の多いQueryの数
  • iRandomKeysQuery選択処理の際にランダムにサンプリングされるKeyの数

スケーラビリティを確保するため、これら2つのパラメータの値はいずれも入力系列全体の長さの対数関数として計算されます。

オブジェクトのパラメータが保存されると、フレームワークはAttention機構を実装するすべての内部コンポーネントの構築を順次開始します。各オブジェクトは厳密に定められた順序で個別に初期化されます。最初に初期化されるコンポーネントはcQKVであり、これはQ (Query)、K (Key)、V (Value)の3つを同時に生成する畳み込み層です。この層は、各Attention Headに対して高密度な特徴表現を生成するとともに、それらをTANH活性化関数を用いてエンコードします。

int index = 0;
if(!cQKV.Init(0, index, OpenCL, iWindow, iWindow, 3 * iWindowKey * iHeads, iUnits, optimization, iBatch))
   return false;
cQKV.SetActivationFunction(TANH);

以前に実装したProbabilistic Attentionのカーネルでは、Queryテンソル専用のバッファを使用していることを思い出してください。そのため、次の段階では、これらの要素を分離するための2つの追加オブジェクトを作成します。

index++;
if(!cQ.Init(0, index, OpenCL, cQKV.Neurons() / 3, optimization, iBatch))
   return false;
index++;
if(!cKV.Init(0, index, OpenCL, 2 * cQ.Neurons(), optimization, iBatch))
   return false;

続いて初期化されるオブジェクトは、ランダムに選択されたKeyのインデックスを格納します。

index++;
if(!cRandomK.Init(0, index, OpenCL, iHeads * MathMax(iRandomKeys, iTopKQuerys), optimization, iBatch))
   return false;

この層のサイズは、サンプリングされたKeyの数と、最も重要なQueryの数という2つの値のうち、大きい方によって決定されることに注意してください。その目的は、計算時に必要となる可能性のあるすべてのインデックスを格納できる十分なメモリ容量を確保することです。ランダムKeys用のiRandomKeysQuery用のiTopKQuerysのどちらが大きくなるかは事前には分からないため、両者の最大値に基づいてメモリを確保します。その後、この値にAttention Head数(iHeads)を乗じます。各Headは独立して動作するため、それぞれ専用のインデックス集合を必要とするためです。

その結果、単一のオブジェクトcRandomKが、ランダムにサンプリングされたKeyのインデックスと、最も情報量の多いQueriesの両方を格納するために使用されます。これにより、モジュール内部の構造が簡潔になり、オブジェクト数が削減されるとともに、メモリ管理も容易になります。

次のコンポーネントは、最重要Queryに対するMultihead Attentionの出力を保持します。

index++;
if(!cMHAttentionOut.Init(0, index, OpenCL, iTopKQuerys * iHeads * iWindowKey, optimization, iBatch))
   return false;

続いて、すべてのAttention Headの出力を統合して単一の表現へまとめる適応的集約層が初期化されます。

index++;
if(!cPooling.Init(0, index, OpenCL, iHeads * iWindowKey, iHeads * iWindowKey, iWindow, iTopKQuerys,
                                                                              optimization, iBatch))
   return false;
cPooling.SetActivationFunction(TANH);

この時点で、アルゴリズムはすでにProbabilistic Attention機構の出力を生成しています。しかし、ここで1つ重要なアーキテクチャ上の点を強調しておく必要があります。これらの出力は、選択された上位KのQueryのみに対応しています。そのため、得られるテンソルの時間方向の次元は、元の入力系列よりも大幅に小さくなります。

このことはアーキテクチャ上の不整合を生みます。Probabilistic Attentionブロックは圧縮された表現を生成する一方で、通常のSelf-Attentionアーキテクチャは元の系列長を保持します。この次元的一貫性を維持することは、学習の安定化と効果的な勾配伝播を実現する残差接続を正しく適用するために不可欠です。

この不一致を解消するため、次の戦略を採用します。

  • まず、Attention出力行列を転置し、そのレイアウトを特徴次元に沿った畳み込みに適した形へ変換します。

index++;
if(!cTranspose[0].Init(0, index, OpenCL, iTopKQuerys, iWindow, optimization, iBatch))
   return false;

  • 次に、畳み込み層cScalingを適用して、出力を元の系列長まで復元します。その結果、テンソルは元の入力と同じ次元数を取り戻します。特に、このアップスケーリングは各特徴ごとに独立して行われるため、グローバルなAttention構造を考慮しながら各時刻の表現を再構成できます。
index++;
if(!cScaling.Init(0, index, OpenCL, iTopKQuerys, iTopKQuerys, iUnits, iWindow, optimization, iBatch))
   return false;
cScaling.SetActivationFunction(None);
  • 最後に、2回目の転置操作を適用することで、データを元のレイアウトへ戻します。
if(!cTranspose[1].Init(0, index, OpenCL, iWindow, iUnits, optimization, iBatch))
   return false;

この一連の処理により、Probabilistic Attention機構と従来のSelf-Attentionアーキテクチャの要件との間で最大限の互換性が確保されます。これにより、モデルは構造的な整合性を維持するとともに、安定した学習に必要な計算効率と柔軟性も維持します。

また、逆伝播時に必要となる中間的なAttentionスコアを格納するibScoreバッファにも特別な注意が払われています。このバッファは、OpenCLコンテキスト内でのみ確保されます。

   ibScore = OpenCL.AddBuffer(sizeof(float) * iTopKQuerys * iUnits * iHeads, CL_MEM_READ_WRITE);
   if(ibScore < 0)
      return false;
//---
   return true;
  }

すべての内部オブジェクトの初期化が完了すると、このメソッドは論理的な成功ステータスを返して終了します。

次の主要な段階は、フィードフォワードアルゴリズムの実装です。ただし、メインのデータフローを処理する前に、少量の準備作業が必要となります。

本連載を通じて開発してきたほとんどのモジュールと同様に、いくつかのメソッドは単にOpenCLカーネルの実行をキューに追加する役割を担っています。これらの基盤となるアルゴリズムについては、すでに以前の記事で説明しているため、ここで再度取り上げる価値はほとんどありません。その代わりに、ここではRandomKeysメソッドに実装されているKeyサンプリングアルゴリズムに焦点を当てます。

Probabilistic Attention機構の目的は、最も有益なQueryを特定する際に調査する必要があるKeyの数を削減することです。これにより計算コストを低減すると同時に、確率的な要素を導入することで過学習を抑制し、モデルの汎化能力を向上させる効果が期待されます。

RandomKeysメソッドは、サンプリングされたKeyのインデックスを格納するためのindexesバッファへのポインタを受け取ります。パラメータrandomunitsheadsは、それぞれランダムサンプル数、利用可能なKeyの総数、およびAttention Head数を指定します。

bool CNeuronMHProbAttention::RandomKeys(CBufferFloat *indexes, int random, int units, int heads)
  {
   if(!indexes || random > units ||
      indexes.Total() < (random * heads)
     )
      return false;

このメソッドは、まず入力パラメータの検証から開始します。バッファポインタがnullの場合、要求されたランダムサンプル数が利用可能なKeyの数を超えている場合、またはバッファサイズが不足している場合、メソッドは直ちにfalseを返します。

検証処理が正常に完了すると、random × headsの行列が生成されます。この行列では、各列が個々のAttention Headに対応し、各行にはサンプリングされたKeyの位置が格納されます。

matrix<float> ind = matrix<float>::Zeros(random, heads);

その後の処理には、2つのケースがあります。サンプリングが不要な場合(random == units)、行列には連続したインデックスが格納されます。つまり、利用可能なすべてのKeyが使用されます。

if(random == units)
  {
   for(int r = 0; r < random; r++)
     {
      for(int c = 0; c < heads; c++)
         ind[r, c] = (float)r;
     }
  }

選択するKeyの数(random)が利用可能な全要素数(units)より少ない場合、単純なランダムサンプリング戦略では、利用可能なKeyやデータ範囲を十分に代表するサンプルが得られない可能性があります。全範囲から単純にランダムなインデックスを選択すると、同じ値が重複して選ばれる、特定の局所領域にサンプルが集中する、あるいは逆に一部の領域がまったく選択されないといった問題が発生する可能性があります。その結果、モデルが入力シーケンス内の重要な部分を十分に観測できず、学習プロセスに悪影響を及ぼす可能性があります。

この問題を回避するために、本実装では均一層化抽出法を採用しています。まず、インデックス全体の範囲を同じ大きさの区間に分割します。

else
  {
   double step = double(units) / random;

結果は小数になる場合がありますが、これは問題ありません。これは、サンプル数がシーケンス長の正確な約数になることはほとんどないためです。

各層(範囲セグメント)について、その境界内から1つの値がランダムに選択されます。

 for(int r = 0; r < random; r++)
   {
    for(int c = 0; c < heads; c++)
       ind[r, c] = float(int((r + MathRand() / 32767.0) * step));
   }
}

その結果、各Attention Headはそれぞれ独立して生成されたインデックスセットを受け取ります。同時に、これらすべてのセットは、特定の領域に偏ったり過度に集中したりすることなく、シーケンス全体を均等にカバーします。

サンプリング処理が完了すると、サンプルはindexesバッファへ書き込まれます。その後、このバッファはOpenCLカーネル内で使用されます。

   if(!indexes.AssignArray(ind) ||
      !indexes.BufferWrite())
      return false;
//---
   return true;
  }

このアプローチには、いくつかの重要な利点があります。第一に、特徴空間全体を均一にカバーすることが保証されます。第二に、インデックスが有効な範囲外に出ることを防ぎながら、サンプルの重複が発生する可能性を最小限に抑えます。その結果、サンプリングされたサブセットは元のデータをより忠実に代表するものとなり、モデルに対して、より完全でバランスの取れた情報を提供できます。これは特に金融時系列データを扱う場合に重要です。金融市場では、低ボラティリティの期間も、急激な市場変動が発生する局面と同様に有益な情報を含んでいる可能性があるためです。

準備段階が完了したため、次に実装の中核部分であるfeedForwardメソッドに進みます。このメソッドでは、Probabilistic Attentionアルゴリズム全体の処理が実行されます。

bool CNeuronMHProbAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cQKV.FeedForward(NeuronOCL))
      return false;

最初の処理では、cQKV畳み込み層のFeedForwardメソッドが呼び出されます。その名前が示す通り、この層はQueryKeyValueの各表現を連結したテンソルを生成します。出力は単一のテンソルであり、その内部には3つの構成要素が順番に格納されています。

しかし、その後のアルゴリズム処理では、これらの表現を2つのテンソルに分離する必要があります。1つはQuery (Q)を格納するテンソル、もう1つはKeyValue (KV)を格納するテンソルです。この処理にはDeConcatメソッドを使用します。このメソッドは、結合されたQKVテンソルから対応する部分を抽出し、それぞれcQおよびcKVへ転送します。

if(!DeConcat(cQ.getOutput(), cKV.getOutput(), cQKV.getOutput(), iWindowKey * iHeads,
                                                    2 * iWindowKey * iHeads, iUnits))
   return false;

次に、前述したKeyサンプリングアルゴリズムがRandomKeysメソッドの呼び出しによって実行されます。

if(!RandomKeys(cRandomK.getOutput(), iRandomKeys, iUnits, iHeads))
   return false;

続く段階では、QueryImportanceおよびTopKIndexesメソッドが呼び出されます。これらのメソッドは、Queryの重要度を評価し、最も情報量の多いQueryを選択するためのカーネルをキューへ追加します。

if(!QueryImportance() || !TopKIndexes())
   return false;

アルゴリズムの中心部分はAttentionOutメソッドで実行されます。このメソッドでは、選択されたQueryのみに対してAttention計算を実行するAttentionカーネルがキューへ追加されます。

if(!AttentionOut())
   return false;

その後、すべてのAttention Headから得られた出力は、cPooling畳み込み層によって統合されます。この層ではTANH活性化関数も適用され、出力信号の表現力が向上します。

if(!cPooling.FeedForward(cMHAttentionOut.AsObject()))
   return false;

統合された出力は、続いて元の入力シーケンスと同じ次元へ戻すためにアップスケールされます。これにより、残差接続を正しく適用できるようになります。

if(!cTranspose[0].FeedForward(cPooling.AsObject()))
   return false;
if(!cScaling.FeedForward(cTranspose[0].AsObject()))
   return false;
if(!cTranspose[1].FeedForward(cScaling.AsObject()))
   return false;

Attentionブロック内の最後の処理では、SumAndNormalizeメソッドが呼び出されます。このメソッドは、アップスケールされた表現と元の入力を結合し、残差接続の加算および正規化処理を実行します。これにより、勾配の安定した伝播が保証され、学習時の収束速度が向上します。

if(!SumAndNormilize(cTranspose[1].getOutput(), NeuronOCL.getOutput(), cTranspose[1].getOutput(),
                    iWindow, true, 0, 0, 0, 1))
   return false;

最後に、生成されたテンソルは親クラスの対応するメソッドへ渡されます。これにより、順伝播処理が完了し、次のニューラルネットワークモジュールで処理可能な出力テンソルが返されます。

 return CResidualConv::feedForward(cTranspose[1].AsObject());
}

この結果、Probabilistic Attentionメカニズム全体がCNeuronMHProbAttentionオブジェクト内にカプセル化されます。この設計により、高いモジュール性、再利用性、および容易なスケーラビリティを備えた実装が実現されています。

順伝播処理が完了すると、学習プロセスは最も重要な段階である、最適化誤差勾配の逆伝播によるモデルパラメータ更新の段階へ移行します。この段階では、各構成要素が全体の損失関数へ与える影響が計算され、その後ネットワークの重みが適切に更新されます。CNeuronMHProbAttentionクラスによって実装されたProbabilistic Attentionメカニズムの場合、逆伝播処理はさらに複雑になります。これは、勾配が選択されたQueryのサブセットを通してのみ伝播されるためです。そのため、この処理では計算ロジックに厳密に従った慎重な構成が必要になります。

bool CNeuronMHProbAttention::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

calcInputGradientsメソッドは、引数として受け取る前層へのポインタの基本的な検証から開始されます。前層への有効な参照が存在しない場合、誤差勾配をネットワーク内でさらに伝播させることはできません。

次のステップでは、重要な準備処理としてQueryの勾配バッファを明示的にクリアします。

if(!cQ.getGradient().Fill(0))
   return false;

この処理は必須です。なぜなら、Attentionアルゴリズムでは、順伝播時に選択された最も重要なQueryのサブセットのみが処理対象となるためです。勾配行列内の残りの要素には、以前の計算で残った古い値や未定義の値が含まれている可能性があります。これらの値はもはや有効ではなく、後続のパラメータ更新処理に悪影響を与える可能性があります。そのため、正しい勾配値の蓄積を開始する前に、行列全体をクリアします。

次に、親クラスの対応するメソッドが呼び出されます。この処理により、FeedForwardブロックを通じて誤差が伝播されます。つまり、逆伝播処理のこの部分は、すでに検証済みの既存ロジックへ委譲されます。

if(!CResidualConv::calcInputGradients(cTranspose[1].AsObject()))
   return false;

その後の処理では、Probabilistic Attentionメカニズム内部の各ブロックを通じて、誤差勾配が順番に伝播されます。これは順伝播処理の構造を完全に反転させたものです。まず、勾配はスケーリングブロックを通過します。

if(!cScaling.calcHiddenGradients(cTranspose[1].AsObject()))
   return false;
if(!cTranspose[0].calcHiddenGradients(cScaling.AsObject()))
   return false;

続いて、Multihead Attentionの集約層を通じて勾配が伝播されます。

if(!cPooling.calcHiddenGradients(cTranspose[0].AsObject()))
   return false;
if(!cMHAttentionOut.calcHiddenGradients(cPooling.AsObject()))
   return false;

これらの段階は、勾配を正確に再構築するために不可欠です。これは、順伝播処理ではデータの形状や構造に対して複数の変換が実行されているためです。逆伝播では、これらの処理を正しい順序で逆向きに実行する必要があります。

特に重要なのがAttentionInsideGradientsメソッドです。このメソッドは、Probabilistic Attentionメカニズム内部における勾配伝播を実行します。ここでは、選択されたQueryのサブセットを通じて誤差が慎重に伝播されます。これは逆伝播処理全体の中で最も重要な構成要素です。元の情報の一部のみが処理に利用される場合でも、重み更新が正確かつ数学的に一貫した状態で維持されることを保証します。

if(!AttentionInsideGradients())
   return false;

次の段階では、QueryKeyValueの各勾配を結合し、単一のQKVテンソルへ統合します。これにより、順伝播処理で生成された元の表現形式が復元されます。また、cQKVブロック内で活性化関数が適用されている場合、その関数の微分値を計算することで勾配が補正されます。これにより、活性化関数が伝播する信号へ与える影響が考慮され、処理全体の数学的な整合性が維持されます。

if(!Concat(cQ.getGradient(), cKV.getGradient(), cQKV.getGradient(), iWindowKey * iHeads,
                                                        2 * iWindowKey * iHeads, iUnits))
   return false;
if(cQKV.Activation() != None)
   if(!DeActivation(cQKV.getOutput(), cQKV.getGradient(), cQKV.getGradient(), cQKV.Activation()))
      return false;

この段階で得られた勾配は、続いて前段のレイヤへ渡されます。

if(!prevLayer.calcHiddenGradients(cQKV.AsObject()))
   return false;

残る唯一の構成要素は、残差接続を通じて流れる誤差勾配です。まず、この勾配は元の入力レイヤに適用された活性化関数の微分値を用いて補正されます。その後、2つの分岐から得られた勾配が加算されます。

   if(prevLayer.Activation() != None)
      if(!DeActivation(prevLayer.getOutput(), cTranspose[1].getGradient(), cTranspose[1].getGradient(),
                                                                               prevLayer.Activation()))
         return false;
   if(!SumAndNormilize(cTranspose[1].getGradient(), prevLayer.getGradient(), prevLayer.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

この結果、calcInputGradientsメソッドは、Probabilistic Attentionモジュールにおける完全な逆伝播アルゴリズムを実装します。このメソッドでは、すべての勾配変換における精度と整合性が維持されます。そのため、情報の損失や学習信号の歪みが発生するリスクなく、Probabilistic Attentionメカニズムを複雑なニューラルネットワークアーキテクチャへ統合することが可能になります。

学習プロセスの最終段階では、モデルパラメータの更新が実行されます。この処理はupdateInputWeightsメソッドによって実装されています。すべての学習可能なパラメータは内部オブジェクト内に配置されているため、この実装では各構成要素に対応する更新メソッドを順番に呼び出すだけで処理が完了します。実装自体は単純であるため、本記事ではこのメソッドの詳細な説明は省略します。updateInputWeightsメソッドを含むProbabilistic Attentionモジュールの完全なソースコードは、添付資料に提供されています。


モデルアーキテクチャ

基本的な実装コンポーネントの説明の最後に、学習システム全体のアーキテクチャについて検討します。これまでのいくつかの研究と同様に、本システムではActor–Director–Criticパラダイムに基づいた階層型学習フレームワークを採用しています。このアプローチにより、環境処理、意思決定、行動評価の各役割を明確に分離できます。これは、特に複雑かつ非常に動的な金融市場を扱う場合に重要となります。

このフレームワークでは、4つのモデルが学習されます。最初のモデルであるEnvironment State Encoder(環境状態エンコーダ)は、中心的な役割を担います。このモデルは、市場状況を深く分析し、コンパクトでありながら高い情報量を持つ潜在表現を構築します。このモデルでは、ACEFormerフレームワークで提案された手法を利用し、指定された計画ホライズンにおける将来の環境状態を予測します。その結果、安定性が高く、動的な意味を持つ市場表現が生成され、その後エージェントの行動決定の基盤として利用されます。

2番目のモデルであるActorは、同時に2種類の情報源を利用します。1つ目は現在の口座状態および保有ポジション情報です。2つ目はエンコーダによって生成された潜在的な環境表現です。Actorは現在の市場状況に最も適した行動を選択するように学習されます。そして、その決定は後述する2つのモデルによって評価されます。

DirectorおよびCriticモデルは、Actorによって選択された行動を評価します。Directorは、厳密なバイナリフィルタリング機構を導入し、戦略的に不適切な意思決定を排除する役割を担います。一方、Criticはより柔軟な定量的評価を提供し、選択された行動の期待効用を推定します。これら2つのコンポーネントを組み合わせることで、エージェントは堅牢で適応性の高い取引戦略を獲得できます。

本記事では、環境状態エンコーダのアーキテクチャに焦点を当てます。これは、このコンポーネントがACEFormerフレームワークの主要な要素を取り入れているためです。残りのモデルのアーキテクチャについては、基本的に過去の公開記事で説明した原則に従っています。

通常通り、入力層には十分なサイズを持つ全結合層を使用します。

//--- Encoder
   encoder.Clear();
//--- Input layer
   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;
     }

ここで注目すべき点は、元のACEFormer実装とは異なり、予測対象となる要素に対してゼロ値を追加していないことです。

データ処理の最初の段階では、専用の特徴量前処理モジュールが使用されます。このモジュールでは、正規化、確率的ノイズ注入、および特徴量次元の畳み込み変換を組み合わせています。

処理は入力データの正規化から開始されます。これにより、個々の特徴量間に存在するスケールの違いが除去され、学習プロセスが安定化されます。さらに、モデルの汎化性能と局所的なデータ変動に対する頑健性を向上させるため、正規化処理中に制御された量のランダムノイズが導入されます。これは正則化メカニズムとして機能し、実際の市場環境に存在する変動をシミュレーションします。その結果、モデルは不安定な状況や未知のシナリオに対して、より適切に適応できるようになります。

正規化され、ノイズが付加されたデータは、その後、特徴量次元を後続のネットワークコンポーネントが必要とする形式へ変換する畳み込み層へ入力されます。この処理では、同時に冗長な情報が削減され、指定された時間ウィンドウ内における特徴量間の重要な空間的関係が抽出されます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_out = descr.window_out = NSkills;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

前処理の後、データは転置処理されます。これにより、時間方向の分析から、各特徴量を独立した単位として分析する形式へ変換されます。この処理によって、Attentionメカニズムは各特徴量内部に存在する有意な依存関係を識別できるようになります。その結果、特徴表現の品質が向上し、学習プロセスの効率も改善されます。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   prev_out = descr.count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

次に、蒸留ブロック(distillation block)へ進みます。エンコーダアーキテクチャにおいて、このブロックはACEFormerフレームワークの中心的な考え方を実装しています。それは、最も情報量の多い特徴を抽出し、有意な情報を集約することです。その中核となるのがProbabilistic Attentionモジュールです。このモジュールは、まず入力データを処理する際に、最も重要な特徴へ重点的に注目します。これにより、モデルは元の信号に含まれる情報量の高い成分へ集中することが可能となります。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Attentionモジュールの直後には、特徴量の時間次元を削減する畳み込み層が続く。この層の目的は、重要な特徴を保持しながら情報を圧縮することです。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2;
   int filt=descr.window_out = 5;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

続いて、集約モジュールが配置されます。このモジュールはmax-pooling層と同様の動作を行い、各ウィンドウ内で最も強い活性化値を選択し、主要な特徴を保持します。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

最後に、正規化層によって特徴量の分布が安定化され、学習プロセスの高速化が図られます。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

本実装では、3つの連続した蒸留ブロックを使用しています。各ブロックは、それぞれ独立して個々の特徴量時系列の次元を削減します。

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2 ;
   filt=descr.window_out = 3;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2 ;
   filt=descr.window_out = 3;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 14
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 15
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

したがって、蒸留ブロックは単に次元削減を行うだけではありません。元のデータに含まれる本質的な情報を抽出・集約し、ノイズに対して高い頑健性を持つ豊かな潜在表現を生成します。

その後、エンコーダアーキテクチャには2層構成のSelf-Attentionブロックが組み込まれています。このブロックの目的は、圧縮された潜在表現内に存在する特徴量間の依存関係を捉えることです。これにより、隠れた関係性を発見し、異なる特徴次元間で情報を同期させることが可能になります。

//--- layer 16
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDMHAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.layers = 2;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

その結果、モデルはデータを単なる独立した観測値の集合としてではなく、相互に作用する要素から構成される一貫したシステムとして認識するようになります。

個々の特徴量時系列を独立して予測するために、エンコーダでは2つの連続した畳み込み層を使用します。この設計により、一般化された潜在表現を各予測対象特徴量の数値予測値へ効率的に変換します。

最初の畳み込み層では、拡張された数のフィルタ(予測対象パラメータ数の4倍)を使用し、SoftPlus活性化関数を適用します。これにより、滑らかな正の値への変換が生成され、ノイズを抑制しながら学習の安定性が向上します。

//--- layer 17
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = 4 * NForecast;
   descr.layers = prev_count;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

2番目の層では、出力数を予測対象パラメータ数(NForecast)と正確に一致する数まで削減することでデコード処理を完了します。また、TANH活性化関数を適用します。これにより、予測値を制御された範囲内に収めることが可能になります。これは、正規化されたデータを扱う場合に特に重要です。

//--- layer 18
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = NForecast;
   descr.layers = prev_count;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

このアーキテクチャは、シンプルでありながら効果的です。個々の特徴量予測の独立性を維持しながら、正確な予測を生成するための十分な柔軟性を提供します。これは金融時系列をモデル化する際に重要な特性です。金融データでは、各変数がそれぞれ固有の情報を担う可能性があり、それぞれを個別に扱う能力が求められるためです。

エンコーダの最終段階では、予測値が元のデータ空間へ変換され、モデル出力を解釈可能な形式へ戻します。

最初の処理はテンソル転置です。これにより、時間次元が元の方向へ復元され、予測値が対応する各時間ステップと正しく整列されます。

//--- layer 19
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count=descr.window = prev_out;
   prev_out=descr.count;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

次に、畳み込み層によって特徴量の次元が元の入力データ形式へ再投影されます。本質的には、これは射影処理です。モデルは潜在表現を圧縮または拡張し、元のデータが形成された空間と互換性を持つ形式へ変換します。

//--- layer 20
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = BarDescr;
   descr.layers = 1;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

最後に、逆正規化処理によって、モデル内部で使用されている正規化表現から予測値を元の実世界の値へ変換します。これにより、ユーザーが解釈可能な形式となり、トレード判断に直接利用できる予測結果が得られます。

//--- layer 21
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count * prev_out;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

これらの処理を組み合わせることで、モデル内部の表現と実際の市場との間に存在する隔たりが解消され、抽象的な潜在予測が実際の行動につながる予測結果へ変換されます。

学習可能なすべてのモデルの完全なアーキテクチャは、添付資料に提供されています。



テスト

ACEFormerフレームワークをMQL5環境へ適応および実装するために、多くの作業をおこないました。現在では、このフレームワークの主要コンポーネントが学習可能なモデルのアーキテクチャへ統合されています。次の段階では、実際の過去データを用いて、構築されたモデルを評価します。

学習用データセットは、2024年全期間の1分足EURUSD価格データを対象として、MetaTrader 5ストラテジーテスター上でランダムなエージェント方策を実行することで生成しました。この方法により、モデルは幅広い市場状況に触れることができ、学習された行動の汎化性能が向上します。

学習は2段階で実施しました。第1段階では、データセットを更新せず、最適化誤差が安定するまでオフライン学習をおこないました。この処理には、チャート上でStudy.mq5エキスパートアドバイザー(EA)を実行しました。第2段階では、StudyOnline.mq5 EAを使用してストラテジーテスター上でオンライン学習を実施しました。これにより、実際のトレード環境に近い条件下でモデルを微調整することが可能になります。

結果を客観的に評価するため、学習済みモデルは2025年1月から3月までの期間を含む、アウトオブサンプルデータでテストしました。これにより、過学習の影響を排除し、得られた結果の実用的な価値を評価できます。

学習およびテストの両方の期間において、その他の環境パラメータおよびテクニカル指標は変更していません。そのため、観測された結果は、学習された戦略そのものの品質を反映しています。

テスト結果を以下に示します。

テスト期間全体では、モデルは利益を上げる結果となり、13件の取引を実行しました。そのうち、半数をわずかに超える取引が利益となりました。ただし、3か月間の評価期間で13件の取引という結果は、取引活動としては比較的少ないことに注意する必要があります。

その理由の1つとして、Probabilistic Attentionメカニズム自体の特性が考えられます。最も情報量の多い特徴量およびQueryのみを選択することで、モデルはより高い汎化性能を獲得します。一方で、弱い売買シグナルに対する感度が低下する可能性があります。

もう1つの要因として、学習データセットの代表性が限定的であった可能性があります。モデルは学習期間中に、十分に多様な市場状況を経験していなかったため、評価期間中に類似した状況へ遭遇した際、自信を持った行動を選択できなかった可能性があります。学習データの規模および多様性を拡大することで、エージェントの行動柔軟性をさらに向上できる可能性があります。


結論

本記事では、時系列データから有益な特徴を抽出し、コンパクトな潜在表現を構築するための効果的な手法であるACEFormerフレームワークについて検討しました。そのアーキテクチャは、Probabilistic Attention、特徴量蒸留、および深層シーケンス変換を組み合わせています。そのため、ノイズが多く、変動性の高い金融データの分析に特に適しています。

実践的な観点では、ACEFormerの主要コンポーネントをMQL5を用いて独自に実装し、それらを学習可能なActor–Director–Criticアーキテクチャへ統合しました。特に、フレームワークで提案された特徴処理手法を実装する環境状態エンコーダの設計に重点を置きました。

実験結果は有望なものでした。モデルは、学習に使用していないテスト期間においてプラスのリターンを生成し、提案したアーキテクチャの基本的な有効性を確認できました。一方で、実験では比較的低い取引頻度も確認されました。これは、Probabilistic Attentionの特性や、学習データセットの多様性不足に起因している可能性があります。これらの観察結果は、今後の研究に向けた明確な方向性を示しています。


参照文献


記事で使用されたプログラム

#名前種類詳細
1Research.mq5EAサンプル収集用EA
2ResearchRealORL.mq5
EA
Real-ORL法を用いたサンプル収集用EA
3Study.mq5EAオフラインモデル学習用EA
4StudyOnline.mq5
EA
オンラインモデル学習用EA
5Test.mq5EAモデルテスト用EA
6Trajectory.mqhクラスライブラリシステム状態とモデルアーキテクチャ記述構造
7NeuroNet.mqhクラスライブラリニューラルネットワークを作成するためのクラスのライブラリ
8NeuroNet.clコードライブラリOpenCLプログラムコード

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/18041

添付されたファイル |
MQL5.zip (2720.51 KB)
最後のコメント | ディスカッションに移動 (1)
Aleksander
Aleksander | 8 5月 2025 において 11:26
13件の取引は、1つの通貨ペアあたりの件数ですか?分析対象を10ペアにした場合、取引件数はどれくらいになりますか?
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(ACEFormer) 取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(ACEFormer)
ACEFormerアーキテクチャについて紹介します。これは、Probabilistic Attention(確率的注意)機構の有効性と、適応型時系列分解を組み合わせた最新のソリューションです。本記事は、金融市場において計算性能と予測精度のバランスを求める方にとって有用な内容となっています。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
取引におけるニューラルネットワーク:多変量時系列予測のためのLSTMの最適化(最終回) 取引におけるニューラルネットワーク:多変量時系列予測のためのLSTMの最適化(最終回)
時系列解析および予測のための革新的な手法を提供するDA-CG-LSTMフレームワークの実装を引き続き進めます。CG-LSTMとデュアルアテンション(dual attention)機構を組み合わせることで、データに含まれる長期的および短期的な依存関係をより高い精度で検出できるため、金融市場の分析に特に有効です。