English Русский Español Deutsch Português
preview
取引におけるニューラルネットワーク:多変量時系列のデュアルクラスタリング(DUET)

取引におけるニューラルネットワーク:多変量時系列のデュアルクラスタリング(DUET)

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

はじめに

多変量時系列とは、各時点において複数の相互に関連した変数を含むデータ列であり、複雑なシステムの挙動を表現します。この種のデータは、経済分析やリスク管理など、複数の要因を考慮した予測が求められる分野で広く利用されています。単変量時系列とは異なり、変数間の相関関係を扱えるため、より精度の高いモデル構築が可能になります。

金融市場では、多変量時系列解析は価格予測、ボラティリティ推定、トレンド検出、さらには取引戦略の構築などに応用されます。たとえば株価予測では、出来高、金利、マクロ経済指標、ニュースなどが同時に考慮されます。これらの要素は相互に影響し合っており、個別に扱うだけでは捉えられない構造が存在します。

多変量時系列の処理における主要な課題は、時間方向の依存関係とチャネル間の関係性の両方を適切に捉えることにあります。実際のデータは非定常性を持つため、この問題はさらに複雑になります。特に経済危機のような局面では、資産間の相関構造が大きく変化し、従来のモデルでは対応が難しくなります。

既存の手法は大きく3つに分類できます。第一は各チャネルを独立に解析する方法ですが、変数間の関係を無視してしまいます。第二はすべてのチャネルを統合する方法で、冗長な情報が増え、精度低下につながる場合があります。第三は変数クラスタリングですが、モデルの柔軟性が制限されます。

これらの問題に対処するため、論文「DUET:Dual Clustering Enhanced Multivariate Time Series Forecasting」ではDUET手法が提案されています。この手法は、時間クラスタリングとチャネルクラスタリングの2つを統合した構造を持ちます。時間クラスタリング(TCM)は、類似した特徴を持つ時系列の区間をグループ化し、時間的なパターンの変化に対応できるようにします。金融市場では、市場の異なる局面を識別する役割を果たします。一方、チャネルクラスタリング(CCM)は重要な変数を選択し、ノイズを抑えながら予測性能を高めます。資産間の安定した関係を抽出する点で、分散投資の設計にも有効です。

これらの結果はFusion Module (FM)によって統合され、時間的パターンとチャネル間依存関係が整合的に扱われます。この統合表現により、金融市場のような複雑なシステムに対して、より精度の高い予測が可能になります。このフレームワークの著者らが実施した実験により、DUETでは、DUETは既存手法を上回る性能を示し、時間的な多様性とチャネル間関係の変動に柔軟に対応できることが確認されています。



DUETアルゴリズム

DUETフレームワークのアーキテクチャは、多変量時系列の予測に対して、時間方向とチャネル方向の両面から入力データをクラスタリングする革新的なアプローチを採用しています。この設計により、モデルの性能が向上すると同時に、結果の解釈もしやすくなります。このアプローチは、複雑なデータ構造を複数のブロックに分解し、それぞれを個別に分析した後、全体として統合的に理解する熟練したアナリストの作業に似ています。DUETフレームワークは、以下の主要モジュールで構成され、それぞれがデータ分析において特定の役割を担います。

  1. インスタンス正規化
  2. 時間クラスタリングモジュール (Temporal Clustering, TCM)
  3. チャネルクラスタリングモジュール(CCM)
  4. フュージョンモジュール(FM)
  5. 予測モジュール

入力データの正規化は、外れ値の影響を抑え、急激な変動を平滑化します。これにより、学習データとテストデータの分布差に対するロバスト性が向上します。特に金融データでは、高頻度ノイズが有意なトレンドを覆い隠す場合があるため、この処理は特に重要です。また、異なるソースから得られた時系列間の統計特性を整合させ、異常値の影響を軽減します。

時間クラスタリングモジュール (TCM)は、時間的依存関係を解析し、系列をクラスタに分割します。この処理は、金融アナリストがボラティリティや流動性、過去の特性に基づいて資産を分類する手法に近いものです。TCMの中核には、複数の並列エンコーダから構成されるアーキテクチャ(Mixture of Experts — MoE)があり、事前のクラスタリング結果に応じて各時系列セグメントに最適なエンコーダを動的に選択します。データのグループごとに異なる処理が適用されるため、時間構造の表現精度が高まります。MoE機構はエンコーダの切り替えを柔軟におこない、高頻度市場データを含む多様な時系列に対応します。

エンコーダは時系列を潜在的な特徴として表現し、それを長期トレンドと短期トレンドに分解します。この分解により、将来の価格変動の予測に有効な潜在パターンの抽出が可能になります。

チャネルクラスタリングモジュール(CCM)は、信号の周波数特性に基づいてチャネルをクラスタリングします。チャネル間の相関を評価し、重要な依存関係を明らかにするとともに、冗長または影響の小さい成分を除去します。この処理は、金融アナリストがマクロ経済指標やテクニカル指標の中から有用なものを選択し、ランダムな市場変動を排除する作業に対応します。

チャネルの周波数特性における振幅ベクトル間の距離を解析することで、相関のある信号を識別し、ノイズ成分を取り除くことが可能になります。金融市場では、資産間の隠れた関係性の発見が裁定機会の発見やシステミックリスクの把握に直結するため、この特性は重要です。

融合モジュール(FM)は、マスク付きアテンション機構を用いて時間的表現とチャネル表現を統合します。このプロセスは、複数の市場要因を統合的に分析する場面に類似しています。FMは重要なクラスタを選択し、不要な信号を抑えることで予測精度を向上させます。マスク付きアテンションにより、各データ要素の重要度が動的に調整され、処理の柔軟性が向上します。資産間の依存構造がマクロ経済イベントによって変化する金融環境では、この柔軟性が特に重要です。

最終段階では、予測モジュールが統合された特徴量を用いて将来の時系列値を予測します。この処理は、過去データに基づいて将来の価格変動を推定する投資家の意思決定に対応します。予測モジュールには、複雑な非線形関係を捉え、構造変化にも対応可能なニューラルネットワークが用いられます。出力結果は逆正規化を経て、元のスケールで解釈可能な形に変換されます。

マスク付きアテンション、周波数領域解析、潜在表現のクラスタリングといった手法を組み合わせることで、DUETは高い予測精度と解釈性を両立します。複雑な時系列シーケンスに潜む隠れたパターンの発見を可能にし、従来の手法では十分な効果が得られないことが多い取引戦略の最適化に、これらの知見を適用できるようにします。従来手法が手動調整や専門的な介入を必要とするのに対し、DUETはデータの構造を自動的に捉え、リアルタイムに適応します。この特性は、高頻度時系列の分析や急速に変化する市場環境において特に有効です。

DUETフレームワークのオリジナルの可視化を以下に示します。



MQL5を使用した実装

DUETフレームワークの理論的側面を詳細に検討したので、実装フェーズに移ります。ここでは、提案されたアプローチをMQL5上で実装していきます。

DUETのモジュール構造は段階的な実装に適しており、各機能ブロックを独立した要素として扱うことができます。このように分割することで、デバッグやテスト、後の最適化が容易になります。まずは時間クラスタリングモジュールから実装をおこないます。

時間クラスタリングモジュール


先に述べた通り、時間クラスタリングモジュールには複数のエンコーダが並列で含まれます。実装では、最も単純な構成として、2つの全結合層を直列に接続し、その間に活性化関数を挟む形のエンコーダを使用します。ただし各エンコーダは独立した時系列セグメントを処理し、それぞれ固有の学習パラメータを持ちます。そのための構造として畳み込み層を使用します。入力シーケンス全体をレイヤに渡し、解析ウィンドウサイズを定義し、ストライドをセグメントサイズに設定します。これにより、畳み込み層のパラメータはそのままエンコーダの全結合層パラメータとして扱うことができます。結果として、全シーケンスセグメントを並列に処理できます。さらに並列エンコーダ数を増やしたい場合は、畳み込み層のフィルタ数を増やすだけで対応できます。

このようにして並列エンコーダの構造は定義されます。ただしDUETフレームワークでは、すべてのエンコーダを使用するのではなく、より重要なもののみを選択する設計になっています。ここでは時系列が潜在的に正規分布に従うという前提があります。正規分布は平均と分散で特徴付けられます。そのため、最も確率の高いk個の潜在分布を選択するためにNoisy Gating手法を使用します。これは次のように表現されます。

ここで正規分布ノイズ(ε)を加えることで学習が安定し、Softplus関数により分散が常に正の値になるようにしています。

その後、SoftMax関数を用いて上位k個の潜在分布を選択し、それぞれの重みを計算します。この結果、同じk個の最も確率の高い潜在分布に属する時系列は、共通のエンコーダグループで処理されることになります。最終的に、このマスクをエンコーダ出力に掛け合わせることで重み付き結果が得られ、不要なフィルタの影響は除外されます。

アーキテクチャが決まったため、実装に移ります。まず、最も関連性の高いk個のエンコーダを選択する処理から作成します。各セグメントの分布パラメータは畳み込み層で生成します。一方でk個の選択処理はOpenCL側で実装します。そのためTopKgatesカーネルを作成します。

__kernel void TopKgates(__global const float *inputs,
                        __global const float *noises,
                        __global float *gates,
                        const uint k)
  {
   size_t idx = get_local_id(0);
   size_t var = get_global_id(1);
   size_t window = get_local_size(0);
   size_t vars = get_global_size(1);

カーネルの引数は入力データ、ノイズ、出力バッファ、そして選択数kです。

カーネル内ではまずスレッド位置を取得します。ここでは2次元のワークスペースを使用しており、第1次元はローカルグループとして扱います。この次元はセグメント単位であり、エンコーダ数に対応しています。

次に、ローカルデータバッファ内のオフセットを決定します。

   const int shift_logit = var * 2 * window + idx;
   const int shift_std = shift_logit + window;
   const int shift_gate = var * window + idx;

そして、対応する入力データを読み込みます。

   float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE);
   float noise = IsNaNOrInf(noises[shift_gate], 0);
   if(noise != 0)
     {
      noise *= Activation(inputs[shift_std], 3);
      logit += IsNaNOrInf(noise, 0);
     }

ノイズ値が0でない場合、分散とノイズに応じてlogit変数の値を調整します。

次に、ワークグループ内でk個の最大logitを求めます。このために、ワークグループ内のスレッド間でデータをやり取りするための手段としてローカルメモリ上に配列を作成し、さらに補助的なローカル変数を宣言します。

   __local float temp[LOCAL_ARRAY_SIZE];
//---
   const uint ls = min((uint)window, (uint)LOCAL_ARRAY_SIZE);
   uint bigger = 0;
   float max_logit = logit;

次に、ローカル配列のサイズと同じステップでワークグループの要素を反復処理するループを定義します。

//--- Top K
#pragma unroll
   for(int i = 0; i < window; i += ls)
     {
      if(idx >= i && idx < (i + ls))
         temp[idx % ls] = logit;
      barrier(CLK_LOCAL_MEM_FENCE);

ループ内では、現在のウィンドウの要素がその値をローカル配列に格納し、その後、ワークグループ内のスレッドを強制的に同期します。

その後、ネストされたループを作成します。このループの反復処理の中で、各スレッドはローカル配列内の要素のうち、現在のスレッドのlogit値よりも大きい要素がいくつ存在するかを計算します。

      for(int i1 = 0; (i1 < min((int)ls,(int)(window-i)) && bigger <= k); i1++)
        {
         if(temp[i1] > logit)
            bigger++;
         if(temp[i1] > max_logit)
            max_logit = temp[i1];
        }
      barrier(CLK_LOCAL_MEM_FENCE);
     }

同時に、ローカルグループ内の最大値を探索します。

ネストしたループのすべての反復が完了した後、再度ワークグループ内のスレッドを同期し、その後にのみ外側ループの次の反復へ進みます。

この処理から分かるように、logit値が大きいk個のスレッドのみが、「自身より大きい要素数が閾値を超えない」という条件を満たします。これらの値は結果バッファに格納されます。

   if(bigger <= k)
      gates[shift_gate] = logit - max_logit;
   else
      gates[shift_gate] = MIN_VALUE;
  }

それ以外のケースでは、最小値を表す定数が結果バッファに書き込まれます。その後にSoftMax関数が適用される際、この値はゼロの影響係数となります。

上記のカーネルは、各ケースにおいてk個の最も関連性の高いエンコーダを選択するためのフォワードパス処理を構成しています。ただし、より適応的なモデルを構築するためには、エンコーダ選択の学習プロセスも必要になります。もちろん、このカーネル自体には学習可能なパラメータは含まれていません。しかし、これらのパラメータはカーネルへの入力データ生成には使用されています。そのため、誤差勾配を入力データレベルへ逆伝播させる必要があります。この処理はTopKgatesGradカーネルとして実装されます。このカーネルのパラメータ構造には、対応する誤差勾配を保持するバッファへのポインタが追加されます。

__kernel void TopKgatesGrad(__global const float *inputs,
                            __global float *grad_inputs,
                            __global const float *noises,
                            __global const float *gates,
                            __global float *grad_gates)
  {
   size_t idx = get_global_id(0);
   size_t var = get_global_id(1);
   size_t window = get_global_size(0);
   size_t vars = get_global_size(1);

カーネル本体では、まず二次元のタスク空間における現在の実行スレッドを特定します。このタスク空間の構造はフォワードパスのカーネルから引き継がれていますが、本ケースではスレッドはワークグループとしてはグループ化されていません。

次に、フォワードパスのアルゴリズムと同様に、グローバルデータバッファにおけるオフセットを決定します。

   const int shift_logit = var * 2 * window + idx;
   const int shift_std = shift_logit + window;
   const int shift_gate = var * window + idx;

最初のステップは、現在のスレッドに対応するフィードフォワードパスの結果を読み込むことです。

   const float gate = IsNaNOrInf(gates[shift_gate], MIN_VALUE);
   if(gate <= MIN_VALUE)
     {
      grad_inputs[shift_logit] = 0;
      grad_inputs[shift_std] = 0;
      return;
     }

容易に推測できるように、取得された値が最小値を表す定数と等しい場合、その時点で入力データの勾配を格納するバッファにゼロ値を書き込むことができます。この値は、そのエンコーダが以降の処理から除外されていることを意味します。

それ以外の場合は、出力レベルにおける誤差勾配の値を読み込み、それを直ちに入力データの勾配バッファの対応する要素へ伝播させます(ここではlogitに対応する誤差となります)。

   float grad = IsNaNOrInf(grad_gates[shift_gate], 0);
   grad_inputs[shift_logit] = grad;

誤差勾配は、当然ながらノイズのレベルへは伝播されません。ただし、分散レベルにおける誤差値は別途求める必要があります。フォワードパスの処理では、分散はノイズと乗算されていました。そのため、次のステップとしてノイズ値を取得します。

   float noise = IsNaNOrInf(noises[shift_gate], 0);
   if(noise == 0)
     {
      grad_inputs[shift_std] = 0;
      return;
     }

当然ながら、ノイズが0の場合、分散はフォワードパスの処理に関与しません。そのため、そのような場合には追加の処理をおこなわず、ゼロの勾配をそのまま格納します。

最後に、それ以外のケースでは、誤差勾配の値をノイズ係数および活性化関数の微分によって補正します。

   grad *= noise;
   grad_inputs[shift_std] = Deactivation(grad, Activation(inputs[shift_std], 3), 3);
  }

得られた値はグローバルデータバッファに書き込まれ、その後カーネルの実行は終了します。

上記で説明した両カーネルの完全なソースコードは、記事の付録に含まれています。

次の段階では、この処理をメインプログラム側で統合していきます。まず、CNeuronTopKGatesオブジェクトを作成し、その中でk個の最も関連性の高いエンコーダを選択するアルゴリズムを実装します。新しいオブジェクトの構造を以下に示します。

class CNeuronTopKGates  :  public CNeuronSoftMaxOCL
  {
protected:
   int               iK;
   CBufferFloat      cbNoise;
   CNeuronConvOCL    cProjection;
   CNeuronBaseOCL    cGates;
   //---
   virtual bool      TopKgates(void);
   virtual bool      TopKgatesGradient(void);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronTopKGates(void) {};
                    ~CNeuronTopKGates(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint units_count, uint gates, uint top_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronTopKGates; }
   //---
   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;
   //---
   virtual uint      GetGates(void) const { return cProjection.GetFilters() / 2; }
   virtual uint      GetUnits(void) const { return cProjection.GetUnits(); }
  };

提示された構造では、通常のオーバーライドされた仮想メソッド群に加えて、TopKgatesおよびTopKgatesGradientが追加されています。これらは、先にOpenCLプログラム側で作成したカーネルに対するラッパーメソッドです。その実装は既に説明済みのアルゴリズムに従っているため、ここでは詳細には触れません。

内部オブジェクトは少数であり、すべて静的に宣言されています。そのため、クラスのコンストラクタおよびデストラクタは空のままにすることができます。宣言されたオブジェクトおよび継承されたオブジェクトはInitメソッド内で初期化されます。このメソッドのパラメータには、生成されるオブジェクトのアーキテクチャを一意に定義するための定数が渡されます。

bool CNeuronTopKGates::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                            uint window, uint units_count, uint gates, uint top_k, 
                            ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronSoftMaxOCL::Init(numOutputs, myIndex, open_cl, gates * units_count,
                                                        optimization_type, batch))
      return false;
   SetHeads(units_count);

初期化メソッドの処理は、まず親クラスの同名メソッドの呼び出しから開始されます。ここでは、必要最小限のチェック処理と、継承されたオブジェクトの初期化が既に実装されています。

なお本ケースでは、親クラスとしてSoftMax関数オブジェクトを使用しています。これにより、k個の最も関連性の高いエンコーダ選択結果を確率表現へ変換する処理を、追加の内部オブジェクトを作成することなく実現できます。親クラスの機能をそのまま利用すれば十分です。

親クラスのメソッド処理が正常に完了した後、新たに宣言されたオブジェクトの初期化処理に進みます。ここではまず、解析対象セグメントの分布パラメータを射影する畳み込み層を初期化します。

   if(!cProjection.Init(0, 0, OpenCL, window, window, 2 * gates, units_count, 1, optimization, iBatch))
      return false;
   cProjection.SetActivationFunction(None);

この層の出力では、モデル内の各エンコーダに対応する平均値と分散を取得することを想定しています。そのため、畳み込み層のフィルタ数は指定されたエンコーダ数の2倍に設定されます。

またここでは、ノイズを生成するためのデータバッファも追加します。

   if(!cbNoise.BufferInit(Neurons(), 0) ||
      !cbNoise.BufferCreate(OpenCL))
      return false;

最後に、先ほど作成したTopKgatesのフォワードカーネルによって生成される結果を格納するための全結合層を初期化します。

   if(!cGates.Init(0, 1, OpenCL, Neurons(), optimization, iBatch))
      return false;
   cGates.SetActivationFunction(None);
//---
   return true;
  }

その後、処理の論理結果を呼び出し元プログラムへ返し、メソッドを終了します。

なお本ケースでは、Top-Kエンコーダの確率分布を保持するためのオブジェクトは新たに作成しません。絶対的なlogit 値を確率空間へ変換する処理は親クラスの機能を利用するためです。そのため、この処理を支援するために必要なオブジェクトはすべて親クラス内で既に作成され、初期化されています。

次のステップでは、k 個の最も関連性の高いエンコーダを選択するフォワード処理をCNeuronTopKGates::feedForwardとして構築します。通常通り、このメソッドの引数には入力データオブジェクトへのポインタが含まれており、これは直ちに解析対象セグメントの分布統計特性を生成するオブジェクトの同名メソッドへ渡されます。

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

次に重要な点として、DUETフレームワークの著者らは、logit 値へのノイズ付加は学習時のみにおこなうことを提案しています。そのため、まずモデルの動作モードを確認し、必要に応じてノイズを生成します。

   if(bTrain)
     {
      double random[];
      if(!Math::MathRandomNormal(0, 1, Neurons(), random))
         return false;
      if(!cbNoise.AssignArray(random))
         return false;
      if(!cbNoise.BufferWrite())
         return false;
     }
   else
      if(!cbNoise.Fill(0))
         return false;

それ以外の場合は、ノイズバッファをゼロで埋めます。

その後、k個の最も関連性の高いエンコーダを選択するためのラッパーメソッドを呼び出します。

   if(!TopKgates())
      return false;
//---
   return CNeuronSoftMaxOCL::feedForward(cGates.AsObject());
  }

得られた結果は親クラスの同名メソッドへ渡され、そこで絶対値が確率空間へと変換されます。

実行された処理の論理結果を呼び出し元プログラムに返し、メソッドの実行を完了します。

ご覧の通り、フォワードパスの処理は線形的な構造になっています。これに対応してバックプロパゲーション側の処理も同様に線形構造となっているため、詳細な説明については各自の確認に委ねる形とします。本オブジェクトおよび全メソッドの完全なソースコードは記事の付録に含まれています。

この時点で、k個の最も関連性の高いエンコーダを選択するアルゴリズムは、メインプログラム側とOpenCL コンテキスト側の両方で実装が完了しています。これにより、Mixture of Experts (MoE)アーキテクチャの構築に進むことができます。このアーキテクチャはCNeuronMoEオブジェクト内に実装します。新しいオブジェクトの構造を以下に示します。

class CNeuronMoE  :  public CNeuronBaseOCL
  {
protected:
   CNeuronTopKGates     cGates;
   CLayer               cExperts;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronMoE(void) {};
                    ~CNeuronMoE(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_out, uint units_count,
                          uint experts, uint top_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronMoE; }
   //---
   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;
   //---
   virtual void      TrainMode(bool flag)
     {  bTrain = flag;  cGates.TrainMode(bTrain); }
  };

提示された構造では、内部オブジェクトは2つのみ存在します。そのうち1つは、すでに作成済みのk個の最も関連性の高いエンコーダを選択するためのオブジェクトです。もう1つは、エンコーダオブジェクトへのポインタを格納するための動的配列です。これら2つのオブジェクトはいずれも静的に宣言されているため、コンストラクタおよびデストラクタは空のままにすることができます。これらのオブジェクトはすべてInitメソッド内で初期化されます。

初期化メソッドのパラメータには、生成されるオブジェクトのアーキテクチャを一意に記述するための定数が渡されます。同時に、出力データの次元数を変更できるような柔軟性も許容されています。

bool CNeuronMoE::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                      uint window, uint window_out, uint units_count,
                      uint experts, uint top_k,
                      ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_count, optimization_type, batch))
      return false;

アルゴリズムはまず、親クラスの同名メソッドの呼び出しから開始されます。ここでは、継承されたオブジェクトの初期化および入力データの検証がすでに実装されています。

次に、最も関連性の高いエンコーダを選択するためのオブジェクトを初期化します。

   int index = 0;
   if(!cGates.Init(0, index, OpenCL, window, units_count, experts, top_k, optimization, iBatch))
      return false;

その後、エンコーダオブジェクトの直接的な初期化に進みます。まず、作成されたオブジェクトへのポインタを一時的に格納するための動的配列とローカル変数を準備します。

   cExperts.Clear();
   cExperts.SetOpenCL(OpenCL);
   CNeuronConvOCL *conv = NULL;
   CNeuronTransposeRCDOCL *transp = NULL;

最初に作成するコンポーネントは畳み込み層であり、これはエンコーダの最初の層として機能します。このオブジェクトへの入力は、すべてのエンコーダで共通の入力データテンソルとなります。この層のフィルタ数は、1つのエンコーダの出力テンソルサイズと、モデル内のエンコーダ総数との積に等しくなります。この構成により、すべてのエンコーダの計算を並列に実行することができます。

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window, window, window_out * experts, units_count, 1, optimization, iBatch) ||
      !cExperts.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(SoftPlus);

エンコーダ層間に非線形性を導入するため、活性化関数としてSoftPlusを使用します。

次に、エンコーダの第2層を追加する必要があります。ご理解の通り、各エンコーダはそれぞれ固有のパラメータセットを受け取る必要がありますが、これは畳み込み層を用いることで実現できます。パラメータとして、独立した解析シーケンス数を表す値にエンコーダ数を指定するだけで対応可能です。ただし注意すべき点として、第1層の出力は{Units, Encoders, Dimension }という3次元テンソルになっています。これは、先ほど実装した畳み込み層の動作アルゴリズムとは一致しません。

そのため正しい処理をおこなうには、最初の2次元を入れ替える必要があります。この処理はデータ転置層によって実現します。

   transp = new CNeuronTransposeRCDOCL();
   index++;
   if(!transp ||
      !transp.Init(0, index, OpenCL, units_count, experts, window_out, optimization, iBatch) ||
      !cExperts.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());

その後、独立したエンコーダの第2層として機能する畳み込み層を初期化することができます。

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window_out, window_out, window_out, units_count, experts, optimization, iBatch) ||
      !cExperts.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(None);

最後に、逆方向のデータ転置層を追加します。

   transp = new CNeuronTransposeRCDOCL();
   index++;
   if(!transp ||
      !transp.Init(0, index, OpenCL, experts, units_count, window_out, optimization, iBatch) ||
      !cExperts.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
//---
   return true;
  }

この時点で、内部オブジェクトの初期化アルゴリズムは完了します。次に、操作の論理結果を呼び出し元に返して、メソッドの実行を完了します。

初期化フェーズの完了後、CNeuronMoE::feedForwardメソッド内でフォワードパスのアルゴリズムを実装していきます。

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

メソッドのパラメータには入力データオブジェクトへのポインタが含まれており、これは直ちにエンコーダ選択オブジェクトの対応するメソッドへ渡されます。

次にエンコーダの処理に移ります。ここでエンコーダは、メソッドのパラメータとして受け取った同一の入力データオブジェクトを使用します。まず、このポインタをローカル変数に格納します。

   CNeuronBaseOCL *prev = NeuronOCL;
   int total = cExperts.Total();
   for(int i = 0; i < total; i++)
     {
      CNeuronBaseOCL *neuron = cExperts[i];
      if(!neuron ||
         !neuron.FeedForward(prev))
         return false;
      prev = neuron;
     }

その後、ループを構成し、エンコーダ層を順次反復しながら、それぞれのフォワードメソッドを呼び出します。

ループの全反復が完了すると、すべてのエンコーダからの出力セットが得られます。ここで、先に各入力データセグメントに対して最も関連性の高いエンコーダの確率マスクを取得していることを思い出してください。そのため、各セグメントの加重和を求めるには、そのセグメントに対応するエンコーダの関連確率の行ベクトルを、同セグメントに対応するエンコーダ出力行列に乗算するだけで済みます。

   if(!MatMul(cGates.getOutput(), prev.getOutput(), getOutput(),
              1, cGates.GetGates(), Neurons() / cGates.GetUnits(), cGates.GetUnits()))
      return false;
//---
   return true;
  }

得られた値は本オブジェクトの結果バッファに格納されます。メソッドは、呼び出し元のプログラムに論理結果を返すことによって終了します。

これで、エンコーダ集合オブジェクトのメソッドを構築するためのアルゴリズムの検討は一通り完了となります。オブジェクトのバックプロパゲーション処理については、各自の確認課題として扱うことを推奨します。いつもの通り、本オブジェクトおよび全メソッドの完全なソースコードは記事の付録に含まれています。

本日はかなりの量の作業を進めることができ、記事として扱う範囲もほぼここまでとなります。ただし、作業自体はまだ完了していません。ここで一度区切りを入れ、次回の記事ではDUETフレームワークの提案手法に対する私たちの実装を引き続き進めていきます。



結論

本日はDUETフレームワークについて検討しました。このフレームワークは、時系列クラスタリング(TCM)とチャネルクラスタリング(CCM)を組み合わせることで、多変量時系列の解析および予測精度を向上させる手法です。TCMは時間的変化へのモデル適応を担い、CCMは重要な変数の抽出とノイズの低減をおこないます。

本記事の実装パートでは、時間クラスタリングモジュール(TCM)の実装を示しました。次回の記事では、今回開始した実装をさらに継続していきます。フレームワークの著者らが提案したアプローチに対する独自の解釈を提示し、実際の過去データを用いたテストによって、その取り組みを最終的な形まで完成させる予定です。


参考文献リスト


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

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

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

添付されたファイル |
MQL5.zip (2538.92 KB)
取引におけるニューラルネットワーク:市場異常の適応型検出(DADA) 取引におけるニューラルネットワーク:市場異常の適応型検出(DADA)
時系列データにおける異常検知のための革新的手法であるDADAフレームワークについてご紹介します。本手法は、ランダムな変動と疑わしい逸脱を区別することを可能にします。従来の方法とは異なり、DADAは柔軟性を持っており、さまざまな種類のデータに適応します。固定された圧縮レベルを用いるのではなく、複数の選択肢の中から各ケースに最も適したものを選択する点が特徴です。
取引におけるニューラルネットワーク:カオス理論を時系列予測に統合する(最終回) 取引におけるニューラルネットワーク:カオス理論を時系列予測に統合する(最終回)
引き続き、Attraosフレームワークの著者らが提案した手法を取引モデルに統合します。このフレームワークは、時系列予測問題を多次元カオス動的システムの投影として解釈し、カオス理論の概念を用いて解決するものであることを改めてお伝えしておきます。
市場シミュレーション(第13回):ソケット(VII) 市場シミュレーション(第13回):ソケット(VII)
xlwingsなど、Excelへの直接的な読み書きを可能にするパッケージを用いて何かを開発する場合には、すべてのプログラム、関数、または手続きは実行され、その処理を完了すると同時に終了するという点に注意する必要があります。どれだけ工夫をしても、それらを継続的なループ処理として動作させ続けることはできません。
市場シミュレーション(第12回):ソケット(VI) 市場シミュレーション(第12回):ソケット(VI)
本記事では、Pythonコードを他のプログラム内で使用する際に発生する特定の問題や課題をどのように解決するかについて説明します。特に、ExcelとMetaTrader 5を併用する際に生じる一般的な問題を取り上げ、その具体例を示します。なお、この連携の実現にはPythonを使用します。ただし、この実装には小さな欠点があります。この問題は常に発生するわけではなく、特定の状況下でのみ起こります。そして、実際に発生した場合には、その原因を理解することが重要です。本日の記事では、この問題の解決方法について解説を開始します。