English Русский 中文 Español Deutsch Português
preview
取引におけるニューラルネットワーク:構造成分の分離(最終回)

取引におけるニューラルネットワーク:構造成分の分離(最終回)

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

はじめに

この記事では、SCNNフレームワークの著者らが提案したアプローチを、独自の解釈に基づいてMQL5で実装する最終段階に進みます。SCNN(季節性畳み込みニューラルネットワーク)は、明確な構造を持つ時系列データの分析に特化したアーキテクチャです。 その主な目的は、生データを長期トレンド、季節成分、短期成分という主要な要素に分解するとともに、変数間の空間的な関係性も考慮することです。 この分解は予測精度を高めるだけでなく、アルゴリズム取引では得難い重要な特性である、モデルの解釈可能性も確保します。

SCNNは、周期別正規化や季節変換といった古典的な解析手法を基盤としながら、Attention機構、パラメトリックな特徴量射影、畳み込みによる結果の集約など、現代的な情報処理手法を組み合わせています。 著者らによるSCNNフレームワークの構成図を以下に示します。

これまでの記事では、モデルを構成するすべての内部構成要素について、その構造と役割を体系的に検討し、専用クラスとして実装してきました。また、最終的なアーキテクチャを構成する各要素を、一つひとつ慎重に構築してきました。 実装作業は容易ではありませんでしたが、その努力に見合う成果が得られました。現在では、システム内でそれぞれ明確に定義された役割を担う、実用的なモジュール群が揃っています。

次のステップは、これらのモジュールを統合し、一貫性のある単一の構造を構築することです。 この段階で、SCNNのアーキテクチャ全体の枠組みが形成され、入力データから最終的な予測結果に至るまで、モデル内部の情報の流れが具体的に定まります。 つまり、準備段階から実際の動作段階へと移行するのです。各構成要素が機能し始め、相互に連携することで、単一の分析機構が完成します。

技術的な組み立てが完了したところで、いよいよ最も注目すべき段階、すなわち過去の市場データを用いたモデルの検証に進みます。 これにより、実装が正しくおこなわれているかどうかだけでなく、さまざまな市場環境において、この手法がどの程度頑健であるかも評価できます。SCNNは、単なる新しいニューラルネットワークアーキテクチャではありません。 計算の精度と、意思決定プロセスの透明性との両立を目指す試みでもあります。



SCNN Encoder

前回の記事では、CNeuronSCNNEncoderオブジェクトに実装されたSCNN Encoderのアーキテクチャについての検討を締めくくり、その内部構成要素すべての初期化手順を詳しく説明しました。 正規化、転置、空間適応のいずれを担うモジュールも、動作に必要な準備と設定を終え、入力データを受け取って処理できる状態になっています。 オブジェクトの構造を以下に示します。

class CNeuronSCNNEncoder   :  public CNeuronTransposeOCL
  {
protected:
   CNeuronPeriodNorm       cLongNorm;
   CNeuronTransposeVRCOCL  cSeasonTransp;
   CNeuronPeriodNorm       cSeasonNorm;
   CNeuronTransposeVRCOCL  cUnSeasonTransp;
   CNeuronPeriodNorm       cShortNorm;
   CNeuronAdaptSpatialNorm cAdaptSpatNorm;
   CNeuronBaseOCL          cConcatenated;
   CNeuronSwiGLUOCL        cProjection;
   CNeuronTransposeOCL     cTranspose;
   CNeuronConvOCL          caFusion[2];
   CNeuronBaseOCL          cFusionOut;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL)  override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL)  override;

public:
                     CNeuronSCNNEncoder(void) {};
                    ~CNeuronSCNNEncoder(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint variables, uint forecast,
                          uint season_period, uint short_period,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronSCNNEncoder; }
   virtual void      TrainMode(bool flag) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { }
  };

今回は、前回からの作業を引き継ぎ、重要な構成要素である順伝播アルゴリズムの実装に進みます。 順伝播は、データがモデル内の各層を順次通過し、変換・集約され、最終的な出力表現が生成されるまでの処理を制御します。

feedForwardメソッドに実装された順伝播処理を通じて、システム全体が連携して動作する仕組みを確認できます。

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

入力データは、まず長期成分の正規化ブロックを通過します。 ここでは、長期間にわたって蓄積されたバイアスやスケールの差が取り除かれ、その後の分析に向けてデータの基盤が整えられます。

続いて、処理済みのデータは、季節成分の抽出ブロックへと順次送られます。 前回の記事で説明したように、ここでは季節周期に対応する所定のステップ幅で転置処理をおこないます。 これにより、時系列の各要素が周期内の位相ごとにグループ化され、安定した季節パターンをより効果的に識別できるようになります。 こうして得られたデータ構造に周期別の正規化を適用することで、季節変動の特徴がより明確になります。 最後に逆転置処理をおこない、後続の処理に適したデータ形式へと戻します。

   if(!cSeasonTransp.FeedForward(cLongNorm.AsObject()))
      return false;
   if(!cSeasonNorm.FeedForward(cSeasonTransp.AsObject()))
      return false;
   if(!cUnSeasonTransp.FeedForward(cSeasonNorm.AsObject()))
      return false;

季節成分の処理に続いて、短期成分の抽出段階に移ります。 ここでは、局所的かつ急速な変動を抽出します。こうした変動には、市場ダイナミクスの直近の変化を捉えるうえで重要な情報が含まれている可能性があります。

   if(!cShortNorm.FeedForward(cUnSeasonTransp.AsObject()))
      return false;

前処理の最終段階では、空間正規化を適用します。 この処理は、長期成分、季節成分、短期成分という異なる成分から抽出された情報を整合させるうえで、重要な役割を果たします。 空間正規化によって、入力変数ごとのスケールや相対的な寄与度を調整し、特徴量間のダイナミクスの違いによって生じる歪みを取り除くことができます。 この処理の重要な特徴は、変数間の空間的依存関係を考慮し、Attention機構を利用している点です。これにより、表現力が高く、より多くの情報を含む特徴表現が得られます。

   if(!cAdaptSpatNorm.FeedForward(cShortNorm.AsObject()))
      return false;

次の処理段階に進む前に、正規化済みデータや算出された統計パラメータを含む、すべてのモジュールの出力を統合し、一貫性のある単一のテンソルにまとめる必要があります。 これは、モデル内部で後続の情報処理の整合性を確保するうえで、極めて重要なステップです。

ここで強調しておきたいのは、正規化後も時系列データ自体の次元数は維持される一方、平均値や標準偏差といった統計パラメータについては、意図的に時系列全体の長さまで拡張していないという点です。 これは、メモリ使用量を抑えるための設計上の判断です。同じ値を系列全体にわたって繰り返しても、新たな情報は得られないからです。 ただし、この最適化によって、時系列データと対応する統計量との間に次元数の不一致が生じます。

それでも、分析対象となる単変量系列の本数については、共通の構造が維持されています。これが重要なポイントです。 結合処理では、この共通構造を利用します。 すべての演算を各単変量系列のセグメントごとに順次実行することで、最終的な結合処理の正確性を確保し、データ構造の歪みを防ぎます。

結合処理の最初の段階では、元の入力データと長期成分の正規化モジュールの出力を結合し、最初のブロックを形成します。 これにより、元の時系列が持つ文脈を保持しながら、正規化によって抽出された長期トレンドの情報を付加できます。 さらに、算出された統計パラメータである平均値と標準偏差も追加します。これらの値は、データのスケールと変動性を示す指標として機能します。

   uint windows[3] = {NeuronOCL.Neurons() / iWindow,
                      cLongNorm.GetPeriod()*cLongNorm.GetUnits(),
                      2 * cLongNorm.GetUnits()
                     };
   if(!Concat(NeuronOCL.getOutput(), cLongNorm.getOutput(), cLongNorm.GetMeanSTDevs().getOutput(),
              cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow))
      return false;

次の段階では、季節成分から抽出された情報を追加し、テンソルを拡張します。

   windows[0] = windows[0] + windows[1] + windows[2];
   windows[1] = cSeasonNorm.GetPeriod() * cSeasonNorm.GetUnits();
   windows[2] = 2 * cSeasonNorm.GetUnits();
   if(!cConcatenated.SwapOutputs() ||
      !Concat(cConcatenated.getPrevOutput(), cSeasonNorm.getOutput(),
              cSeasonNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(),
              windows[0], windows[1], windows[2], iWindow))
      return false;

季節成分の情報に続いて、短期成分と空間成分のデータを順番にテンソルへ追加します。 この段階でも、単変量系列ごとに結合するという方針を維持し、新たな特徴量を順次追加することでテンソルを拡張していきます。

   windows[0] = windows[0] + windows[1] + windows[2];
   windows[1] = cShortNorm.GetPeriod() * cShortNorm.GetUnits();
   windows[2] = 2 * cShortNorm.GetUnits();
   if(!cConcatenated.SwapOutputs() ||
      !Concat(cConcatenated.getPrevOutput(), cShortNorm.getOutput(),
              cShortNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(),
              windows[0], windows[1], windows[2], iWindow))
      return false;
   windows[0] = windows[0] + windows[1] + windows[2];
   windows[1] = cAdaptSpatNorm.GetUnits();
   windows[2] = 2 * cAdaptSpatNorm.GetUnits();
   if(!cConcatenated.SwapOutputs() ||
      !Concat(cConcatenated.getPrevOutput(), cAdaptSpatNorm.getOutput(),
              cAdaptSpatNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(),
              windows[0], windows[1], windows[2], iWindow))
      return false;

短期成分は、局所的な変動や急激な値動き、微細なパターンといった、直近の市場動向を反映する情報をモデルに提供します。これらは、予測が市場の変化に敏感に反応できるようにするうえで、特に重要です。 一方、空間正規化は、すべての変数にわたってデータのばらつきを整え、過度な変動を抑えることで、前処理の最終段階を担います。

こうして、各成分が一貫した形式で表現された、意味のあるバランスの取れた時系列表現が得られます。 この統合テンソルは射影層に渡されます。射影層の役割は、予測期間を考慮しながら、入力データの重み付け表現を生成することです。 この段階で、データは必要な出力次元数に合わせて変換され、後続の分析タスクに適した形式に整えられます。 ここで、過去の情報を十分に活用しつつ、今後予想される変化の構造も考慮できる、有意義な予測を生成するための基盤が形成されます。

   if(!cProjection.FeedForward(cConcatenated.AsObject()))
      return false;

前段階で得られた各単変量系列の表現は、Fusionモジュールで統合され、マルチモーダル時系列の構造へと再編成されます。 ここでは、単変量系列を基準とした表現から、時間ステップを基準とした表現へと切り替えます。具体的には、テンソルを転置し、その後の処理を時間軸に沿っておこないます。

この段階では、2つの畳み込み層を連続して適用します。 1つ目の層では、活性化関数TANHを使用します。これにより、各特徴量について豊かな非線形表現を学習できます。これは、本質的には、特徴信号を正規化した形で表現する処理です。 2つ目の層では、SIGMOIDを使用し、ゲートとして機能させます。この層は、時間的な文脈を踏まえて各特徴量の重要度を決定し、得られた値に滑らかな重み付けをおこないます。

最終的な出力は、2つの層の出力を要素ごとに乗算することで得られます。これにより、ノイズを効果的に抑えながら、時系列の中で特に重要な特徴を際立たせることができます。しかも、それらの特徴は、予測タスクに適合するよう整列された表現空間上で抽出されます。

   if(!cTranspose.FeedForward(cProjection.AsObject()))
      return false;
   for(uint i = 0; i < caFusion.Size(); i++)
      if(!caFusion[i].FeedForward(cTranspose.AsObject()))
         return false;
   if(!ElementMult(caFusion[0].getOutput(), caFusion[1].getOutput(), cFusionOut.getOutput()))
      return false;
//---
   return CNeuronTransposeOCL::feedForward(cFusionOut.AsObject());
  }

順伝播の最終段階では、逆転置処理によってデータを時間方向の表現から元の単変量系列の構造へと戻します。 これにより、Encoderの入力形式と出力形式の整合性が保たれます。

このように、feedForwardメソッドは、各構成要素が明確に定義された役割を果たすよう、綿密に設計・構成されています。 正規化と分解から、射影、そして複数の成分の集約に至るまで、一連の処理はすべて、時系列データの豊かで構造化された、情報量の多い表現を生成することを目的としています。

順伝播アルゴリズムの検討が終わったので、次は同じく重要な誤差逆伝播に進みましょう。 モデルが実際に学習を開始するのは、この段階です。出力側で計算された勾配が、一連の処理を逆方向にたどって順次伝播し、各モジュールの内部パラメータを調整できるようになります。 calcInputGradientsメソッドは、この処理を実装し、順伝播のアーキテクチャとの整合性を保ちながら、各処理を正しい順序で実行します。

bool CNeuronSCNNEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronTransposeOCL::calcInputGradients(cFusionOut.AsObject()))
      return false;
   if(!ElementMultGrad(caFusion[0].getOutput(), caFusion[0].getGradient(),
                       caFusion[1].getOutput(), caFusion[1].getGradient(),
                       cFusionOut.getGradient(), caFusion[0].Activation(), caFusion[1].Activation()))
      return false;
   if(!cTranspose.CalcHiddenGradients(caFusion[0].AsObject()))
      return false;

逆伝播は、モデルの最上位層から始まります。ここでは、後続の層から受け取った勾配(出力層の場合は、損失関数から得られた勾配)を入力として使用します。 最初に処理するのは、Fusionモジュールで使用されている畳み込みブロックです。 順伝播では、2つの畳み込みチャネルを並列に使用したことを思い出してください。一方は活性化関数TANHを用いて非線形な特徴表現を生成し、もう一方はSIGMOIDを用いてゲートとして機能し、各特徴量の重要度を決定します。 最終的に、両チャネルの出力を要素ごとに乗算して、最終的な表現を生成しました。

逆伝播では、この乗算処理に特別な対応が必要です。 ElementMultGradメソッドは、それぞれの活性化関数の特性を考慮しながら、勾配を2つの分岐に正しく振り分けます。これは計算精度を確保するうえで極めて重要です。 この段階での計算結果が、畳み込み層の重みをどのように更新するかを決定するためです。

次に、勾配をTransposeモジュールへ伝播させます。前述のとおり、順伝播では、このモジュールによってテンソルの軸を入れ替え、単変量系列を基準とした表現から時間ステップを基準とした表現へと変換しました。 Fusionモジュールの2つの畳み込み層には、この形式でデータが入力されていました。そのため、逆伝播では、両方の分岐から戻ってくる勾配を適切に統合する必要があります。

まず、一方の畳み込み層から誤差勾配を伝播させます。 このとき、テンソル全体のデータをコピーするのではなく、データバッファへのポインタを差し替えます。これにより、メモリ領域を効率的に切り替え、余分なリソース消費を抑えながら、計算結果を格納できます。 続いて、ポインタを差し替えた状態で、もう一方の畳み込み層の逆伝播処理を実行します。

   CBufferFloat* temp = cTranspose.getGradient();
   if(!cTranspose.SetGradient(cTranspose.getPrevOutput(), false) ||
      !cTranspose.CalcHiddenGradients(caFusion[1].AsObject()) ||
      !SumAndNormilize(temp, cTranspose.getGradient(), temp, cTranspose.GetCount(),
                                                              false, 0, 0, 0, 1) ||
      !cTranspose.SetGradient(temp, false))
      return false;

その後、2つの勾配の流れを要素ごとの加算によって統合します。これにより、両方の分岐が最終的な誤差に及ぼす影響が反映されます。 最後に、バッファへのポインタを元の状態に戻し、以降の勾配伝播に備えてデータ構造の整合性を確保します。 この方法により、学習中のモデルの一貫性を維持しながら、メモリを効率的に管理できます。

次に、誤差勾配をProjectionブロックへ渡します。このブロックは、畳み込みモジュールに渡す前に情報の重み付けをおこなう役割を担います。 そこから得られた勾配は、Concatenatedブロックへと伝播します。ここから、技術的に最も複雑な処理の一つである、逆方向の分離(逆結合)処理が始まります。

   if(!cProjection.CalcHiddenGradients(cTranspose.AsObject()))
      return false;
   if(!cConcatenated.CalcHiddenGradients(cProjection.AsObject()))
      return false;

順伝播では、長期成分、季節成分、短期成分、空間成分の各正規化モジュールから得られたデータを、順次結合していったことを思い出してください。 さらに、正規化されたデータだけでなく、平均値や標準偏差といった統計パラメータも、結合後のテンソルに含めました。

ここでは、その逆の処理をおこないます。つまり、結合されたテンソルを、元の構成要素ごとに分離して取り出します。 このために、各構成要素のウィンドウサイズを、順伝播時のデータ配置に厳密に合わせて算出しながら、順次分離するDeConcat処理を使用します。 この段階では、特に高い精度が求められます。ウィンドウサイズにわずかな誤りがあっても、データの位置ずれや情報の欠落につながる可能性があるためです。

   uint windows[3] = {0};
   windows[1] = cAdaptSpatNorm.GetUnits();
   windows[2] = 2 * cAdaptSpatNorm.GetUnits();
   windows[0] = cConcatenated.Neurons() / iWindow - windows[1] - windows[2];
   if(!DeConcat(cConcatenated.getPrevOutput(), cAdaptSpatNorm.getGradient(),
                cAdaptSpatNorm.GetMeanSTDevs().getGradient(),
                cConcatenated.getGradient(), windows[0], windows[1], windows[2], iWindow))
      return false;
   windows[1] = cShortNorm.GetPeriod() * cShortNorm.GetUnits();
   windows[2] = 2 * cShortNorm.GetUnits();
   windows[0] = windows[0] - windows[1] - windows[2];
   if(!DeConcat(cConcatenated.getGradient(), cShortNorm.getPrevOutput(),
                cShortNorm.GetMeanSTDevs().getGradient(),
                cConcatenated.getPrevOutput(), windows[0], windows[1], windows[2], iWindow))
      return false;
   windows[1] = cSeasonNorm.GetPeriod() * cSeasonNorm.GetUnits();
   windows[2] = 2 * cSeasonNorm.GetUnits();
   windows[0] = windows[0] - windows[1] - windows[2];
   if(!DeConcat(cConcatenated.getPrevOutput(), cSeasonNorm.getPrevOutput(),
                cSeasonNorm.GetMeanSTDevs().getGradient(),
                cConcatenated.getGradient(), windows[0], windows[1], windows[2], iWindow))
      return false;
   windows[1] = cLongNorm.GetPeriod() * cLongNorm.GetUnits();
   windows[2] = 2 * cLongNorm.GetUnits();
   windows[0] = windows[0] - windows[1] - windows[2];
   if(!DeConcat(NeuronOCL.getPrevOutput(), cLongNorm.getPrevOutput(),
                cLongNorm.GetMeanSTDevs().getGradient(),
                cConcatenated.getPrevOutput(), windows[0], windows[1], windows[2], iWindow))
      return false;

ここで、重要な技術的ポイントを確認しておきましょう。 空間正規化を除くすべての正規化モジュールは、データ準備の処理チェーンにおける最終段階ではありません。 それぞれの出力は、後続の処理段階への入力として使用されます。 つまり、逆伝播の際、各モジュールが受け取る誤差勾配は、単一の経路から得られるものではなく、複数の経路から伝わってきます。 後続のすべての処理の寄与を正しく反映するには、より単純なアーキテクチャで一般的におこなわれるように、勾配を単純に上書きするわけにはいきません。 そこで、バックアップ用のバッファを使用します。各モジュールは、新たな勾配を受け取る前に、現在保持している勾配をこのバッファに保存します。

このように、正規化処理のチェーンを逆方向にたどる際には、専用のバッファを用いて勾配を蓄積する仕組みを採用しています。 これにより、モデルの各構成要素の影響を正確に復元し、情報の欠落を防ぐことができます。

分離処理が正常に完了すると、各正規化モジュールには、対応する誤差勾配が渡されます。まず短期成分を扱うcShortNorm、次に季節成分を扱うcSeasonNorm、最後に長期成分を扱うcLongNormの順です。それぞれの勾配には、順伝播時のデータ処理チェーンが出力に及ぼした影響が反映されています。 しかし、すでに述べたように、これらの正規化モジュールはデータの前処理だけに使用されたわけではありません。 そのため、単一の経路に沿って勾配を逆方向に伝えるだけでは不十分です。

実際の処理は、次のように実装されます。 正規化モジュールが、その出力データを使用するオブジェクトから誤差勾配を受け取ったら、その勾配を、分離処理の段階ですでに得られている値に加算します。 このように、各段階で複数の経路から得られた情報を慎重に集約します。 そして、この加算処理が完了して初めて、統合された勾配を処理チェーンの前段の層へと伝播させます。

   if(!cShortNorm.CalcHiddenGradients(cAdaptSpatNorm.AsObject()) ||
      !SumAndNormilize(cShortNorm.getGradient(), cShortNorm.getPrevOutput(),
                       cShortNorm.getGradient(), cShortNorm.GetPeriod(), false, 0, 0, 0, 1))
      return false;
   if(!cUnSeasonTransp.CalcHiddenGradients(cShortNorm.AsObject()))
      return false;
   if(!cSeasonNorm.CalcHiddenGradients(cUnSeasonTransp.AsObject()) ||
      !SumAndNormilize(cSeasonNorm.getGradient(), cSeasonNorm.getPrevOutput(),
                       cSeasonNorm.getGradient(), cSeasonNorm.GetPeriod(), false, 0, 0, 0, 1))
      return false;
   if(!cSeasonTransp.CalcHiddenGradients(cSeasonNorm.AsObject()))
      return false;
   if(!cLongNorm.CalcHiddenGradients(cSeasonTransp.AsObject()) ||
      !SumAndNormilize(cLongNorm.getGradient(), cLongNorm.getPrevOutput(),
                       cLongNorm.getGradient(), cLongNorm.GetPeriod(), false, 0, 0, 0, 1))
      return false;

この方法により、順伝播で形成された情報のつながりが、誤差逆伝播の過程で失われたり、歪められたりすることを防ぎます。 勾配の伝播を厳密かつ慎重におこなうことは、実際の市場環境において、SCNN フレームワークの安定性と高い解釈可能性を支える重要な要因の一つです。

逆伝播は、入力側のNeuronOCLオブジェクトで勾配を計算することで完了します。 この段階は極めて重要です。複雑な変換と正規化の処理チェーンを通過してきた、すべての情報の流れが、ここで一つに集約されるためです。

   if(!NeuronOCL.CalcHiddenGradients(cLongNorm.AsObject()) ||
      !SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(),
                       NeuronOCL.getGradient(), cLongNorm.GetPeriod(), false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

ここで重要なのは、2つの主要な勾配の流れが合流するという点です。一方は複雑な正規化処理の系統から、もう一方は予測に必要な特徴空間を形成する射影層から伝わってきます。 これらを適切に統合することで、勾配の流れが途切れることなく維持され、学習中にモデルの重みが正しく更新されます。

このように、calcInputGradientsメソッドは、単に処理チェーンを逆方向にたどるだけの技術的な処理ではありません。モジュール化され、構造的に整理された、精緻な誤差逆伝播を実装しています。 各処理は順伝播で実行された処理と正確に対応しており、これによってSCNN Encoderのアーキテクチャ全体の一貫性と対称性が保たれ、動作を理解しやすくなっています。

EncoderであるCNeuronSCNNEncoderの学習可能なパラメータの最適化は、対応する内部モジュールに処理を委譲する形で実装されています。各モジュールは独自の重みを保持し、それぞれの更新ロジックを実装しています。 updateInputWeightsメソッドは、これらのモジュールに順次処理を委譲することで、学習プロセスを一元的に管理しながら、モジュールごとの独立性も維持します。

bool CNeuronSCNNEncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cAdaptSpatNorm.UpdateInputWeights(cShortNorm.AsObject()))
      return false;
   if(!cProjection.UpdateInputWeights(cConcatenated.AsObject()))
      return false;
   for(uint i = 0; i < caFusion.Size(); i++)
      if(!caFusion[i].UpdateInputWeights(cTranspose.AsObject()))
         return false;
//---
   return true;
  }

モジュール化された構造のおかげで、重み更新アルゴリズムはシンプルで論理的なものに保たれており、不要な依存関係によってコードが複雑になることもありません。

主要なメソッドの実装をすべて含むCNeuronSCNNEncoderクラスの完全なソースコードは、添付ファイルに含まれています。 構造が明確で、各処理が詳細にモジュール化されているため、このコードは今後の実験や改良を進めるうえでの基盤として活用できます。



最上位モジュール

SCNNフレームワークの原著者らは、複数のEncoderを積み重ねて構成する階層型アーキテクチャを提案しています。 このアプローチは、モデルの表現力を高めることで、予測精度の向上を図るものです。 このような連鎖構造では、後続の各Encoderは独立して動作するのではなく、直前のEncoderの出力を入力データとして利用します。 また、残差接続によって、分析にさらなる文脈情報が加わります。 その結果、モデルはより広い文脈を考慮し、より深い分析をおこなうとともに、時系列データにおける局所的な依存関係と長期的な依存関係の両方を捉えることができます。

しかし、こうした柔軟性には代償も伴います。 ある層から次の層へデータを渡す際、入力と出力のテンソルサイズが異なるという、具体的な問題が生じます。 各SCNN Encoderの出力は、互換性のある形式の単変量系列ですが、指定された予測ホライズンの分だけ系列長が延長されています。

ここで、難しい課題に直面します。 一方では、次の層に渡すデータから予測値を除外しなければなりません。そうしなければ、未来の情報が次の層に漏洩することになり、これは許容できません。 他方で、予測値を単純に破棄することもできません。最終的に、すべての層の出力を加算する際に必要となるためです。 つまり、データの時間的な構造を崩すことなく、モデルがすでに観測した情報と予測した情報の両方を考慮できるようにする必要があります。

この問題を解決するために、最上位オブジェクトであるCNeuronSCNNを作成します。 これは、Encoder全体のスタック構造を内包する、汎用的な制御要素です。 その役割は、単に各Encoderを順番に呼び出すことではありません。層間でデータを受け渡し、各層のサイズを整合させ、予測値を蓄積し、学習と誤差逆伝播を管理することで、アーキテクチャ全体が正しく機能するようにします。 このオブジェクトは、モデルの計算ロジックとアーキテクチャ全体の整合性を結び付ける役割を担います。

新しいオブジェクトの構造を以下に示します。

class CNeuronSCNN    :  public   CNeuronBaseOCL
  {
protected:
   CLayer            cLayers;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL)  override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL)  override;

public:
                     CNeuronSCNN(void) {};
                    ~CNeuronSCNN(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint variables, uint forecast,
                          uint season_period, uint short_period, uint layers,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronSCNN; }
   virtual void      TrainMode(bool flag) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { }
  };

CNeuronSCNNの内部には、CLayer型のオブジェクトが配置されています。このオブジェクトは、基本的に、内部に配置されるすべてのEncoderを格納するコンテナとして機能します。 このクラスにはCLayerオブジェクトが1つだけ組み込まれているため、コンストラクタとデストラクタを空のままにし、メモリ管理を実行環境に委ねることができます。

システム全体を正常に動作させるには、各構成要素を適切に組み合わせてアーキテクチャを構築する必要があります。 この処理を担うのがInitメソッドです。ここで、CNeuronSCNNオブジェクトの動的な構造全体を生成し、必要な設定をおこないます。

bool CNeuronSCNN::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                       uint units_count, uint variables, uint forecast,
                       uint season_period, uint short_period, uint layers,
                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, (units_count + forecast)*variables,
                                                                   optimization_type, batch))
      return false;
   SetActivationFunction(None);

まず、親クラスに実装された同名のメソッドを通じて基底部分を初期化し、ニューラルネットワーク層の基本インターフェースを生成します。 次に、活性化関数を無効にします。この階層では活性化関数は必要ありません。必要な非線形変換とデータの前処理は、各Encoder自体がすべて担うためです。

その後、コンテナであるcLayersをクリアし、必要なオブジェクトを格納できるように準備します。

   cLayers.Clear();
   cLayers.SetOpenCL(OpenCL);
   CNeuronSCNNEncoder* encoder = NULL;
   CNeuronBaseOCL* residual = NULL;
   for(uint l = 0; l < layers; l++)
     {
      encoder = new CNeuronSCNNEncoder();
      if(!encoder)
         return false;
      if(!encoder.Init(0, l, OpenCL, units_count, variables, forecast, season_period,
                                                  short_period, optimization, iBatch) ||
         !cLayers.Add(encoder))
         return false;
      encoder.SetActivationFunction(None);
      if((l + 1) == layers)
         break;

layersパラメータで指定された層数に従ってループ処理をおこない、CNeuronSCNNEncoder型のEncoderを順番に生成していきます。 各層について、まずEncoderオブジェクトを作成し、入力次元数、変数の数、予測期間の長さ、季節周期をパラメータとして初期化します。 初期化が正常に完了したら、そのオブジェクトを層のコンテナに追加します。

ただし、処理はこれだけでは終わりません。 層間の残差接続を維持するため、各Encoderの間に追加の層を挿入します。 これらの中間層は、いわばサイズ調整用のバッファとして機能します。直前のEncoderの出力を受け取り、予測値を除外することで、各階層間のデータの流れが途切れないようにします。 ここでも活性化関数は無効にします。これらの中間層は計算処理を担うのではなく、補助的な役割を果たすためです。

      residual = new CNeuronBaseOCL();
      if(!residual)
         return false;
      if(!residual.Init(0, l, OpenCL, units_count * variables, optimization, iBatch) ||
         !cLayers.Add(residual))
         return false;
      residual.SetActivationFunction(None);
     }
   if(!SetGradient(encoder.getGradient(), true))
      return false;
//---
   return true;
  }

スタックの最後に位置するEncoderには、その後に補助層を配置しない点も重要です。 このEncoderの勾配は、最上位層全体の主要な勾配として使用されるため、データを追加でコピーする必要がありません。

このように、Initメソッドは単なる初期化処理から、いわばオブジェクトビルダーへと発展しています。層間のサイズ調整、予測期間、勾配の伝播管理など、細部にまで配慮しながら、Encoderを積み重ねた構造全体を動的に組み立てます。 このように適切に設計されたアーキテクチャにより、モデルを効果的に学習させるだけでなく、短期的な価格予測から季節トレンドの長期分析まで、実際の課題に応じて拡張することも可能になります。

初期化が完了し、多層構造のSCNNモデルが構築されて動作可能な状態になると、次の重要な段階である順伝播処理に進みます。 ここから、モデルは本来の役割を果たし始めます。すなわち、生データを処理して予測を生成し、各階層で情報を蓄積していきます。

bool CNeuronSCNN::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
   if(!Output.Fill(0))
      return false;
   CNeuronBaseOCL* inputs = NeuronOCL;
   CNeuronSCNNEncoder* current = NULL;
   CNeuronBaseOCL* residual = NULL;
   int layers = cLayers.Total();

まず、受け取った入力データのポインタが有効であることを確認します。 データが存在しない場合は、処理を終了します。 次に、前回の反復処理で生じた不要なデータが残らないよう、結果バッファをクリアします。 その後、inputs変数を外部データソースへのポインタで初期化します。これは、Encoderへの入力として渡されるデータです。 これらの準備が完了すると、すべての層を順番に処理するループが始まります。

モデルのアーキテクチャでは、各層が交互に配置されるよう設計されています。偶数番目の位置には SCNN Encoderが、奇数番目の位置には基本的なニューラルオブジェクトとして実装された残差接続用バッファが配置されます。 そのため、ループのステップ幅は2に設定されています。これにより、1回のループ処理でEncoderと、それに対応する残差接続ブロックの両方を処理することができます。

   for(int l = 0; l < layers; l += 2)
     {
      current = cLayers[l];
      if(!current ||
         !current.FeedForward(inputs) ||
         !SumAndNormilize(Output, current.getOutput(), Output, current.GetCount(), false, 0, 0, 0, 1))
         return false;
      if((l + 1) == layers)
         break;

各ループの反復処理では、まず現在のEncoder を取得し、そのFeedForwardメソッドを呼び出します。次に、そのモジュールの出力を予測結果に加算します。 このように、順伝播の段階から、各層が生成した予測値の累積が始まります。

ただし、これが最終層でない場合は、さらに処理が続きます。 次に、奇数番目に配置された残差接続用バッファを取得します。 このバッファの役割は、予測値の追加によって生じた余分なデータを取り除き、次のEncoderに渡す入力データの次元を調整することです。 この処理にはDeConcatメソッドを使用します。このメソッドは、予測値の部分を予測値を含まない履歴データから分離し、次のEncoderが必要とする履歴データのみの形式に戻します。 その後、新たに得られた入力データを前段の入力データに加算し、残差接続の効果を実現したうえで、正規化します。 これにより、各層は独立して動作するのではなく、前段の層の処理結果を踏まえながら、より豊かな時系列表現を受け取って処理を進めます。

      uint variables = current.GetWindow();
      uint dimension = inputs.Neurons() / variables;
      uint forecast = current.GetCount() - dimension;
      residual = cLayers[l + 1];
      if(!residual)
         return false;
      if(!DeConcat(residual.getOutput(), current.getPrevOutput(), current.getOutput(),
                   dimension, forecast, variables) ||
         !SumAndNormilize(residual.getOutput(), inputs.getOutput(), residual.getOutput(),
                          dimension, true, 0, 0, 0, 1))
         return false;
      inputs = residual;
     }
//---
   return true;
  }

分離処理と正規化を経て得られたデータは、次のEncoderに渡されます。この処理は、すべての階層を順番に通過するまで繰り返されます。 最終的に得られるのは、ブロック内のすべてのEncoderが連携して生成した、統合された予測結果です。

このように、feedForwardメソッドは、各段階で情報を充実させ、歪みを取り除き、バランスの取れた予測を生成するための、綿密に構成された処理です。 これは、スタック型アーキテクチャの基本理念を具体化したものです。すなわち、過去の文脈を失うことなく、各階層の寄与を考慮しながら、分析を段階的に深めていきます。

順伝播が完了したら、次は同じく重要な誤差逆伝播の段階に進みます。 ここでは、出力層から入力データ側へと勾配を逆方向に伝播させ、モデルのパラメータを最適化できるようにします。 この処理は、calcInputGradientsメソッドに実装されています。

bool CNeuronSCNN::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
   if(!PrevOutput.Fill(0))
      return false;
//---
   CNeuronBaseOCL* inputs = NULL;
   CNeuronSCNNEncoder* current = cLayers[-1];
   CNeuronBaseOCL* residual = NULL;
   int layers = cLayers.Total() - 2;

処理は、基本的な確認から始まります。まず、外部のNeuronOCLオブジェクトへのポインタが有効であることを確認し、補助バッファであるPrevOutputをクリアします。 次に、学習処理の対象となる層の数を特定します。 ここで重要なのは、層を数える順序が逆になるという点です。逆伝播では、モデルの出力側から入力側へと処理を進める必要があるため、最後のEncoderから開始します。

ステップ幅を「-1」に設定したループで、残差接続用オブジェクトを含むすべての層を逆順に処理します。各層の種類に応じて、適切な処理ロジックが実行されます。

   for(int l = layers; l >= 0; l--)
      switch(cLayers[l].Type())
        {
         case defNeuronBaseOCL:
            inputs = cLayers[l];
            if(!inputs ||
               !inputs.CalcHiddenGradients(current))
               return false;
            if(!!residual)
               if(!SumAndNormilize(inputs.getGradient(), residual.getGradient(), inputs.getGradient(),
                                   current.GetWindow(), false, 0, 0, 0, 1))
                  return false;
            residual = inputs;
            break;

現在のオブジェクトが基底クラスであるCNeuronBaseOCLの場合、2つの処理をおこないます。 まず、現在のEncoderに基づいて中間層の勾配を計算します。次に、前段に残差接続ブロック(residual変数)が存在する場合は、その勾配を現在の勾配に加算し、次の層へ渡す値を調整します。 これにより、層間の情報の流れが途切れないようにし、アーキテクチャの残差構造に起因する情報の損失を防ぎます。

一方、現在の層がSCNN Encoderの場合、処理はより複雑になります。 まず、residualがすでに定義されていることを確認します。 定義されていなければ、そのまま次の処理へ進みます。 定義されている場合は、2層前に位置するオブジェクト、すなわち、順伝播時に現在のEncoderの入力にデータを供給したオブジェクトを取得します。 これにより、データ構造を正確に再構築できます。

 case defNeuronSCNNEncoder:
    current = cLayers[l];
    if(!residual)
       break;
    inputs = cLayers[l + 2];
    if(!inputs)
       return false;
    if(!Concat(residual.getGradient(), PrevOutput, current.getGradient(),
               residual.Neurons() / current.GetWindow(),
               current.GetCount() - residual.Neurons() / current.GetWindow(),
               current.GetWindow()) ||
       !SumAndNormilize(current.getGradient(), inputs.getGradient(), current.getGradient(),
                        current.GetWindow(), false, 0, 0, 0, 1))
       return false;
    break;
 default:
    return false;
    break;
}

次に、Concat関数を使用して、勾配の形状を元に戻します。残差層の現在の勾配に、そのPrevOutputのうち予測値に相当する部分をゼロで埋めた値を連結します。 これは、順伝播時にデータが過去のデータ部分と予測値部分に分割されていたためであり、誤差勾配においても、この構造を正確に再現する必要があるからです。 続いて、これらの勾配を後続のEncoderからの勾配と合算します。ここが重要なポイントです。情報は2つの経路を通じて伝播する可能性があるため、それぞれの経路から得られる勾配を1つに統合する必要があります。

すべての層を通過した後、最後の処理として、最上位レベルに勾配を渡します。 ここでは、NeuronOCLが下位のSCNN Encoderから勾配を受け取り、残差層が存在する場合は、その勾配を最後に合算します。

   if(!NeuronOCL.CalcHiddenGradients(current))
      return false;
   if(!!residual)
      if(!SumAndNormilize(NeuronOCL.getGradient(), residual.getGradient(), NeuronOCL.getGradient(),
                          current.GetWindow(), false, 0, 0, 0, 1))
         return false;
//---
   return true;
  }

このように、calcInputGradientsメソッドは、スタック型アーキテクチャの中核となる考え方を体現しています。各層は、自身の誤差だけでなく、モデル全体の性能にどのような影響を与えるかに基づいても、パラメータを調整できます。 これにより、時系列データの特性に応じたきめ細かな調整が可能になり、データの特徴に対するモデルの感度も高まります。

スタック型アーキテクチャのすべての層で勾配の計算が完了したら、学習の最終段階である重みの更新に進みます。 ここでは、これまでの処理で得られた情報を実際のパラメータ調整に反映させます。つまり、累積された誤差情報を、モデルの学習可能なパラメータの更新量へと変換します。

updateInputWeightsメソッドは、モデル内のすべての学習可能なブロックの重みを順次更新する役割を担います。 処理は、外部データソースであるNeuronOCLオブジェクトへのポインタで、変数inputsを初期化するところから始まります。

bool CNeuronSCNN::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL* inputs = NeuronOCL;
   CNeuronBaseOCL* current = NULL;
//---
   for(int l = 0; l < cLayers.Total(); l++)
     {
      current = cLayers[l];
      if(!current)
         return false;
      if(current.Type() == defNeuronSCNNEncoder)
         if(!current.UpdateInputWeights(inputs))
            return false;
      inputs = current;
     }
//---
   return true;
  }

次に、このメソッドはモデル内のすべての層を順に走査します。 ループの各反復では、層の種類を確認します。 ここで対象となるのは、学習可能なパラメータを持つSCNN Encoderのみです。 現在の層がこの型に該当する場合、UpdateInputWeightsメソッドを呼び出し、入力データを渡します。 これにより、計算済みの勾配に基づいて重みを適切に調整できます。 更新が正常に完了したら、inputs変数のポインタを更新後のものに差し替え、次の層への入力として使用します。 これにより、順伝播や誤差逆伝播と同様に、処理の論理的な連続性が保たれます。

updateInputWeightsメソッドによって学習の一連の処理が完結し、各Encoderはモデルの誤差に応じて適応しながら、全体の最適化に貢献します。 実装は、アーキテクチャのロジックに忠実でありながら、不要な複雑さを避け、簡潔かつ実用的にまとめられています。

CNeuronSCNNクラスおよびそのすべてのメソッドの完全なコードは、添付ファイルに記載されています。



モデルアーキテクチャ

SCNNフレームワークの構築に使用するオブジェクトの動作ロジックの説明を終えたところで、次はモデル自体のアーキテクチャを詳しく見ていきます。システムがデータからパターンをどれほど正確かつ頑健に抽出できるかは、このアーキテクチャによって決まるからです。 ここでは、CreateDescriptionsメソッドによって、ニューラルネットワークの処理パイプラインを構成するための設定を構築します。

このメソッドは、層の構成情報を作成して格納する役割を担います。これらの構成情報をもとに、後続の処理で計算モデルが構築されます。 簡単に言えば、ここでは入力データ層からエンコーダ、予測用の分岐に至るまで、これから構築するニューラルネットワークの各層を、レンガを積み上げるように順番に定義していきます。 処理の冒頭では、6つのコンテナを作成して初期化します。その内訳は、環境状態を処理するメインのエンコーダ用が1つ、異なる予測モデルのバリエーション用が3つ、そして強化学習タスクで使用するActor分岐用とCritic分岐用がそれぞれ1つです。

Environment-State Encoder(環境状態エンコーダ)は、まず基本的な全結合層から始まります。この層には、過去のバー情報から生成された入力データベクトルが渡されます。

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&forecast1,
                        CArrayObj *&forecast2,
                        CArrayObj *&forecast3,
                        CArrayObj *&actor,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!forecast1)
     {
      forecast1 = new CArrayObj();
      if(!forecast1)
         return false;
     }
   if(!forecast2)
     {
      forecast2 = new CArrayObj();
      if(!forecast2)
         return false;
     }
   if(!forecast3)
     {
      forecast3 = new CArrayObj();
      if(!forecast3)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }
//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   uint 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 = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- Layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatDiff;
   prev_count = descr.count = HistoryBars;
   descr.layers = BarDescr;
   descr.step = 1;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

続いて、一次差分特徴量を連結する層CNeuronConcatDiffが配置されます。この層は差分に基づく特徴量を生成し、ネットワークが局所的な変化をより的確に捉えられるようにします。

現代的なアーキテクチャ要素であるCMamba4CastEmbeding層は、特に注目に値します。 この層は、複数の時間窓(ここでは日次と月次)を考慮して潜在特徴量を抽出し、時間的な周期性を反映した埋め込み表現を生成します。 ここで、ニューラルネットワークは初めて、季節性や長期的なトレンドを考慮するようになります。

//--- Layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   uint prev_out = descr.window_out = NSkills;
     {
      uint temp[] = {PeriodSeconds(PERIOD_D1), PeriodSeconds(PERIOD_MN1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- Layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = descr.count;

続いて、埋め込み表現を転置し、単変量系列の表現へと変換します。

そして、Environment-State Encoderの設計の総仕上げとなるのが、先に説明したモジュール、すなわちSCNN Encoderを積み重ねたスタック構造の組み込みです。 4段階の入れ子構造を持ち、季節性パターンと短期的なパターンを捉えられるように綿密に調整されたこのブロックこそが、知的な情報処理の中核を担います。

//--- Layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSCNN;
   descr.variables = prev_count;
   {
      uint temp[]={prev_out,NForecast,SeasonPeriod,ShortPeriod};
      if(ArrayCopy(descr.windows,temp)<(int)temp.Size())
        return false;
   }
   descr.count=descr.windows[0]+descr.windows[1];
   descr.layers=4;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   uint variables=descr.variables;
   uint count=descr.count;

ここでは、単なるデータ変換にとどまらない処理がおこなわれています。 このモジュールは、主要な分析処理を担います。特徴量を抽出するだけでなく、それらを多層の隠れ表現へと構造化し、それぞれの表現が、異なるスケールで観測されたパターンを一般化します。 その結果、出力として得られる予測ベクトルには、残差接続と、それ以前の各階層から引き継がれた記憶がすでに反映されています。

つまり、この構成要素において、生の入力データが市場状態の高水準な表現へと変換されます。 ここでは、抽象的な行動パターンが、予測モジュールやモデルの制御分岐で解釈できる数値表現へと変換されます。 そして、まさにこの段階で、データの準備と正規化から分析、解釈、意思決定へと処理が進み、アーキテクチャ全体が完成します。

ここで、重要な技術的ポイントを強調しておきます。 SCNN Encoderのスタックの出力として得られるのは、単変量系列の配列です。この形式は、後続の予測モデルで扱ううえで便利であり、理にかなっています。 また、指定された予測期間にわたる予測を生成するのにも適しています。 しかし、ActorとCriticの各モジュールがおこなう分析においては、この形式が必ずしも適切とは限りません。

その理由は、意思決定をおこなうエージェントには、個々の予測を把握するだけでなく、それらを一貫した時間的構造として理解することも求められるからです。つまり、システムの挙動における短期的な側面と長期的な側面の両方を包含する、一種のマルチモーダルな動態として捉える必要があります。 そのためには、データの構成方法を変える必要があります。時間ステップを順に並べると同時に、それらの相互関係を維持しなければなりません。

そこで、もう1つの転置層を追加します。 この処理により、テンソルは単変量予測の集合から、マルチモーダル系列の各時間ステップに対応する形式へと変換されます。 これにより、各時点で、異なるチャネルから集められたすべての予測変数を参照できるようになります。 その結果、ActorとCriticは、環境の時間的な変化を、個々に切り離された断片としてではなく、相互に関連する一体的な全体として捉えることができます。

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

このように、最後の変換層によってEncoderのアーキテクチャが完成し、予測機構と意思決定モジュールの間に適切なインターフェースを確保するうえで、重要な役割を果たします。

予測モデルと意思決定モデルのアーキテクチャは、以前のプロジェクトで使用したものをほぼそのまま引き継いでいます。 これらの構成要素はすでに有効性が実証されているため、本記事では詳しく取り上げません。 アーキテクチャ上の設計についてさらに理解を深めたい方は、添付資料に含まれる関連資料をご覧ください。

また、添付資料には、モデルの学習とテストに使用したプログラムのソースコードも含まれています。 これらの資料を利用することで、システム設計の内部ロジックを追跡できるだけでなく、実験の全工程を再現することも可能になります。



テスト

モデルの学習は、連続する2つの段階で構成されています。 第1段階はオフライン学習です。この段階では、2024年の1年間にわたる通貨ペアEURUSDの1時間足(H1)の過去データを用いて学習をおこないました。 この期間には、さまざまな市場シナリオが含まれています。 データに多様性があったことで、モデルは典型的な市場状況だけでなく、まれに発生する状況にも対応できるようになりました。

オフライン学習の完了後、第2段階として、実際の市場環境にできる限り近い条件下でオンライン追加学習をおこないました。 学習にはMetaTrader 5ストラテジーテスターを使用し、モデルは逐次入力されるローソク足データを段階的に分析しました。 これにより、ノイズや市場の歪みに対するモデルの耐性を検証するとともに、変化する市場環境への適応能力も確認できました。 このアプローチによって、モデルの頑健性が大幅に向上し、過学習のリスクが低減するとともに、汎化能力も改善されました。

最後に、学習には一切使用していない新しいデータ、すなわち2025年1月から3月までの相場データを用いてモデルをテストしました。 学習によって得られたパラメータと設定は、いずれも変更せずに保持しました。 したがって、得られた結果は、提案手法の予測精度と実運用上の信頼性を客観的に評価するためのものとなります。 テストの結果を以下に示します。

3か月間のテストにおいて、モデルは資金を100ドルから約430ドルに増加させました。1取引当たりの平均利益(13.17ドル)は平均損失(11.71ドル)を上回りました。 勝ちトレードの割合は約48%であり、プロフィットファクターは約1.04でした。この数値は、システムが利益側にわずかな優位性を持って動作していることを示しています。

一方、最大ドローダウンは82%を超え、特に大きな下落は3月11~20日頃に発生しました。 この期間は、学習データから最も時間的に離れた期間に当たります。 このことは、新たな市場環境に直面した際にモデルが十分に対応できず、予想外のボラティリティの急上昇に耐えられるだけの頑健性を備えていないことを示唆しています。

総じて、今回のテストでは、SCNNアーキテクチャが利益を生み出し、リスクとリターンのバランスを維持できる可能性が示されました。しかし、実用化に向けては、さらなるリスク管理の仕組みを導入するとともに、学習期間を延長する必要があります。 これにより、モデルは既知の市場環境で安定して動作するだけでなく、新たな予測困難な状況にも、より確実に適応できるようになると考えられます。



結論

本記事では、時系列予測を目的としたSCNNモデルの開発を完了し、実践的なテストをおこないました。 時系列分解の理論的な考え方の検討から、学習済みのEncoderスタックの構築、さらに提案手法のActor-Criticアーキテクチャへの統合に至るまで、多くの作業を進めてきました。

2025年1月から3月までの通貨ペアEURUSDのデータを用いたバックテストでは、モデルが利益を生み出し、バランスの取れた予測をおこなえることが確認されました。一方で、学習データに含まれていない急激な市場変動に対して脆弱であることも明らかになりました。 特に3月に発生した大幅なドローダウンは、モデルのさらなる最適化が必要であることを示しています。


参考文献


本記事で使用したプログラム

# 名前 種類 説明
1 Study.mq5 EA モデルのオフライン学習用EA
2 StudyOnline.mq5 EA モデルのオンライン学習用EA
3 Test.mq5 EA モデルテスト用EA
4 Trajectory.mqh クラスライブラリ システム状態とモデルアーキテクチャを記述するための構造体
5 NeuroNet.mqh クラスライブラリ ニューラルネットワークを構築するためのクラスライブラリ
6 NeuroNet.cl ライブラリ OpenCLプログラム用のコードライブラリ


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

添付されたファイル |
MQL5.zip (2956.64 KB)
決闘者アルゴリズム 決闘者アルゴリズム
もし、あなたの取引戦略が、実際の格闘家のように互いに学び合えるとしたら、どうでしょうか。 決闘者アルゴリズムは、取引システムのパラメータ同士が、文字どおり「最良」と呼ばれる権利を懸けて決闘する、新しい最適化手法です。
取引におけるニューラルネットワーク:構造成分の分離(Encoder) 取引におけるニューラルネットワーク:構造成分の分離(Encoder)
柔軟性と解釈可能性を兼ね備え、時系列データの構造成分を精密に特定できるSCNN(Structured Component Neural Network:構造成分ニューラルネットワーク)フレームワークの実装に向けた次のステップを解説します。 市場環境の変化に対するモデルの頑健性を支える、適応的正規化とAttention機構の仕組みについて詳しく解説します。
取引におけるニューラルネットワーク:Temporal Queryモデル(TQNet) 取引におけるニューラルネットワーク:Temporal Queryモデル(TQNet)
TQNetフレームワークは、モジュール性、柔軟性、高い処理性能を兼ね備え、金融時系列データのモデリングと予測に新たな可能性を切り開きます。 高度なパラメータ初期化手法も含め、大域的な相関関係を捉えるための複雑なメカニズムを実装する可能性について考察します。
取引におけるニューラルネットワーク:構造成分の分離(SCNN) 取引におけるニューラルネットワーク:構造成分の分離(SCNN)
データを長期成分、季節成分、短期成分、残差成分に明確に分解することで、時系列分析を新たな段階へと引き上げる革新的なSCNNフレームワークをご紹介します。 このアプローチでは、モデルが複雑かつ変化し続ける市場の動向に適応できるため、予測精度の大幅な向上が期待できます。