English Русский Español Deutsch Português
preview
ニューラルネットワークが簡単に(第41回):階層モデル

ニューラルネットワークが簡単に(第41回):階層モデル

MetaTrader 5エキスパートアドバイザー | 21 11月 2023, 09:19
226 0
Dmitriy Gizlyk
Dmitriy Gizlyk

はじめに

この記事では、取引における階層強化学習の応用を探ります。このアプローチを使って、さまざまなレベルで最適な意思決定をおこない、さまざまな市場環境に適応できる階層型取引モデルを構築することを提案します。

この記事では、取引のエントリポイントやエグジットポイントの決定など、さまざまなレベルの意思決定を含む階層モデルのアーキテクチャについて考えます。また、大域レベルの強化学習と局所レベルの強化学習を組み合わせた階層的モデル学習法も紹介します。

階層学習を利用することで、複雑な意思決定構造をモデル化し、異なるレベルの知識を効果的に利用することが可能になります。これはモデルの汎化能力を高め、市場環境の変化への適応性を高めるのに役立ちます。


1.階層モデルの利点

近年、取引の分野で階層モデルの利用が注目され、研究が進んでいます。階層学習は、複雑な階層的意思決定構造をモデル化するための強力な手法です。取引において、これはいくつかの大きな利点をもたらス可能性があります。

第一の利点は、異なる市場環境に適応できる階層モデルの能力です。このモデルでは、政治的イベントや経済指標など、より高いレベルのマクロ経済要因を分析すると同時に、テクニカル分析や資産固有の情報など、より低いレベルのミクロ経済要因を考慮することができます。これにより、モデルはより多くの情報に基づいた決定を下し、さまざまな市場状況に適応することができます。

2つ目の利点は、利用可能な情報をより効率的に活用できることです。階層モデルでは、さまざまな階層の情報をモデル化し、利用することができます。ハイレベルの戦略は大まかなトレンドや傾向を考慮することができ、ローレベルの戦略はより正確で急速に変化するデータを考慮することができます。これにより、このモデルは市場の全体像を把握し、より多くの情報に基づいた意思決定をおこなうことができます。

階層モデルの3つ目の利点は、計算資源を効率的に配分できることです。ハイレベルの戦略はより大きな時間スケールで訓練することができ、ローレベルの戦略は小さな時間スケールで急激に変化するデータにより敏感に反応することができます。これにより、コンピューティングリソースを効率的に使用し、モデル訓練プロセスを高速化することができます。

第四の利点は、戦略の安定性と移植性の向上に関するものです。階層モデルは、抽象的な概念や依存関係を異なる階層レベルでモデル化することができるため、汎化能力が高くなります。これにより、さまざまな条件下で成功裏に適用でき、異なる市場や資産に移転できる持続可能な戦略を構築することができます。

階層モデルを使う5つ目の利点は、複雑な取引問題をより単純なサブタスクに分割できることです。これにより、課題の複雑さが軽減され、学習プロセスが単純化されます。各階層は、取引のエントリポイントやエグジットポイントの決定、リスク管理、ポートフォリオ配分など、取引の特定の側面に責任を持つことができます。これにより、より効率的なモデルの訓練が容易になり、その判断の質が向上します。

最後に、階層モデルの使用は、結果と決定の解釈可能性の向上に寄与します。モデルには明示的な階層構造があるため、各階層でどのような要因や変数が意思決定に影響を与えるかを理解しやすくなります。これにより、トレーダーや研究者は戦略の理由と結果をよりよく理解し、必要な調整をおこなうことができます。

このように、取引問題において階層モデルを使用することで、市場の状況への適応性、情報の効率的な利用、計算資源の配分、戦略の安定性と移植性、複雑な問題をサブ問題に分割すること、結果の解釈可能性の向上など、多くの利点が得られます。これらの利点により、階層モデルは成功する取引戦略を開発するための強力なツールとなります。 

取引で階層モデルを使用するには、訓練に特別なアプローチが必要です。単一レベルモデルで使用される従来の訓練方法は、その複雑な構造とレベル間の関係から、階層モデルには必ずしも適していません。

階層学習の使用は、そのようなモデルを訓練するための具体的なアプローチのひとつです。この場合、モデルは異なる階層レベルで段階的に学習され、低階層から始まり、順次高階層へと移行します。モデルは各レベルで学習する際、前のレベルで学習した情報を使用するため、より抽象的な依存関係や上位階層の概念を捉えることができます。

さらに、強化学習と教師あり学習を組み合わせることも重要です。この場合、モデルは強化タスク中に受け取った報酬と、各階層で提供される訓練例に基づいて訓練されます。このアプローチにより、モデルは他のエージェントの経験から学び、より高い階層で既に獲得した知識を利用することができます。

階層モデルの訓練で重要なのは、状況の変化に適応する能力でもあります。モデルは柔軟で、新しい市場環境やデータの変化に素早く適応できるものでなければなりません。この目的のために、モデルの定期的な正則化と新しいデータに基づくパラメータの更新を含む動的学習を使用することができます。

取引における階層モデルの学習アルゴリズムの顕著な例のひとつに、Scheduled Auxiliary Control (SAC-X)があります。

Scheduled Auxiliary Control(SAC-X)アルゴリズムは、階層構造を用いて意思決定をおこなう強化学習手法です。これは、報酬が疎な問題を解くための新しいアプローチです。それは4つの主要原則に基づいています。

  1. 各状態-動作ペアには、(通常は疎な)外部報酬と(通常は疎な)内部補助報酬からなる報酬ベクトルが付随します。
  2. 各報酬エントリには、対応する累積報酬を最大化するように学習するインテントと呼ばれる方策が割り当てられます。
  3. 外部タスクエージェントのパフォーマンスを向上させる目的で、個々のインテントを選択して実行する高レベルのスケジューラーがあります。
  4. 学習は方策の外側(方策の実行とは非同期)でおこなわれ、情報の有効活用のために、意図の間で経験が交換されます。

SAC-Xアルゴリズムは、これらの原理を利用して、スパース報酬問題を効率的に解きます。報酬ベクトルは、タスクの異なる側面から学習することを可能にし、複数の意図を作り出し、それぞれが自身の報酬を最大化します。Plannerは、外部目標を達成するために最適な戦略を選択することで、意図の実行を管理します。学習は政治の外でおこなわれ、異なる意図からの経験を効果的な学習に生かすことができます。

このアプローチにより、エージェントは外部報酬と内部報酬から学習することで、疎な報酬問題を効率的に解くことができます。Plannerを使うことで、行動の調整が可能になります。また、意図する者同士が経験を交換することで、情報の効率的な利用を促進し、エージェントの全体的なパフォーマンスを向上させます。

SAC-Xは、報酬がまばらな環境において、より効率的で柔軟なエージェント訓練を可能にします。SAC-Xの主な特徴は、内部補助報酬の使用です。これは、スパース性の問題を克服し、低報酬タスクの学習を促進するのに役立ちます。

SAC-Xの学習プロセスでは、各インテントは対応する補助報酬を最大化する独自の方策を持っています。スケジューラーは、任意の時間にどの意図を選択し、実行するかを決定します。これにより、エージェントはタスクのさまざまな側面から学習し、利用可能な情報を効果的に利用して最適な結果を得ることができます。

SAC-Xの主な利点のひとつは、さまざまな外部アプリケーションに対応できることです。このアルゴリズムは、異なるターゲット機能で動作し、異なる環境やタスクに適応するように設定することができます。このおかげで、SAC-Xは幅広い分野で使用することができます。

加えて、非同期で意図的に経験を交換することで、情報の効率的な利用が促進されます。エージェントは成功した意図から学び、獲得した知識を使ってパフォーマンスを向上させることができます。これによりエージェントは、複雑な問題を解決するための最適な戦略を素早く効率的に見つけることができます。

全体として、Scheduled Auxiliary Control (SAC-X)アルゴリズムは、疎な報酬環境においてエージェントを訓練するための革新的なアプローチです。外部と内部の補助報酬、スケジューラー、非同期学習を組み合わせることで、エージェントの高いパフォーマンスと適応性を実現しています。SAC-Xは、複雑な問題を解くための新しい機能を提供し、スパース報酬が課題となる様々なアプリケーションに適用できます。

SAC-Xのアルゴリズムは次のように説明できます。

  1. 初期化:各インテントの方策とそれに対応する報酬ベクトルを初期化します。インテントを選択して実行するスケジューラーも初期化されます。
  2. 訓練サイクル
    1. 経験収集:エージェントは環境と相互作用し、選択されたインテントに基づいて行動を実行します。状態、行動、外部からの報酬、内部からの補助的報酬という形で経験を収集します。
    2. インテントの更新:各インテントに対して、対応する方策は収集された経験を用いて更新されます。方策は、この意図に割り当てられた累積補助報酬が最大になるように調整されます。
    3. プランニング:スケジューラーは、現在の状態と以前に実行された意図に基づいて、次のステップでどの意図を実行するかを選択します。スケジューラーの目的は、外部タスクに対するエージェントの全体的なパフォーマンスを向上させることです。
    4. 非同期学習:方策とスケジューラーの更新は非同期におこなわれるため、エージェントは他のインテントから受け取った情報と経験を効果的に活用することができます。
  3. 終了:アルゴリズムは、一定の性能や反復回数に達するなど、指定された停止基準に達するまで学習ループを続けます。

SAC-Xアルゴリズムでは、エージェントは学習のために外部と内部の補助報酬を効果的に使用し、外部タスクで最適な結果を達成するために最良の意図を選択することができます。これにより、報酬のスパース性問題を克服し、低報酬環境におけるエージェントのパフォーマンスが向上します。


2.MQL5を使用した実装

Scheduled Auxiliary Control (SAC-X)アルゴリズムは、エージェントの非同期訓練を提供し、異なるエージェント間で経験の自由な交換が可能です。前回の記事と同様に、学習プロセス全体を2段階に分けて説明します。

  • 経験の収集
  • 方策の訓練(エージェントの行動戦略)

経験値を集めるために、まず2つの構造体を作ります。最初の構造体SStateに、システムの個別の状態の記述を設定します。浮動小数点値を格納するための静的配列が1つだけ含まれます。

struct SState
  {
   float             state[HistoryBars * 12 + 9];
   //---
                     SState(void);
   //---
   bool              Save(int file_handle);
   bool              Load(int file_handle);
   //--- overloading
   void              operator=(const SState &obj)   { ArrayCopy(state, obj.state); }
  };

使いやすくするために、構造体の中にファイルの保存と読み込みを扱うメソッドを作成します。メソッドのコードは非常にシンプルです。添付ファイルをご覧ください。

2つ目のSTrajectory構造体には、エピソードの1パス中にエージェントが蓄積した経験に関するすべての情報が含まれます。その中に3つの静的配列があります。

  • States:状態の配列。上記の構造体の配列で、エージェントが訪れたすべての状態が記録されます。
  • Actions:エージェントの行動の配列
  • 報酬:外部環境から受け取る報酬の数々

さらに、3つの変数を追加します。

  • Total:訪問した状態の数
  • DiscountFactor:割引係数
  • CumCounted:累積報酬が割引率を考慮して再計算されることを示すフラグ

struct STrajectory
  {
   SState            States[Buffer_Size];
   int               Actions[Buffer_Size];
   float             Revards[Buffer_Size];
   int               Total;
   float             DiscountFactor;
   bool              CumCounted;
   //---
                     STrajectory(void);
   //---
   bool              Add(SState &state, int action, float reward);
   void              CumRevards(void);
   //---
   bool              Save(int file_handle);
   bool              Load(int file_handle);
  };

独立した状態を記述する上記の構造とは異なり、この構造のコンストラクタを作成します。配列と変数に初期値を入れて初期化します。

STrajectory::STrajectory(void)  :   Total(0),
                                    DiscountFactor(0.99f),
                                    CumCounted(false)
  {
   ArrayInitialize(Actions, -1);
   ArrayInitialize(Revards, 0);
  }

コンストラクタでは、訪問した状態の総数を0と定義していることにご注目ください。累積報酬CumCountedを計算するフラグをfalseに設定します。データをファイルに保存する前に、累積報酬を直接計算します。モデルを訓練する際には、これらの値が必要になります。

Addメソッドを使って、状態-行動-報酬セットをデータベースに追加します。

bool STrajectory::Add(SState &state, int action, float reward)
  {
   if(Total + 1 >= ArraySize(Actions))
      return false;
   States[Total] = state;
   Actions[Total] = action;
   if(Total > 0)
      Revards[Total - 1] = reward;
   Total++;
//---
   return true;
  }

報酬は、前の状態でエージェントが選択した行動を実行するときに、前の状態から現在の状態に遷移するために受け取るので、前の状態の報酬を保存することに注意してください。こうして、行動と報酬の因果関係を尊重します。

CumRevardsの累積報酬の計算方法はいたってシンプルですが、実行された計算フラグCumCountedの監視には注意を払う必要があります。これはとても重要なことです。この制御により、累積報酬の計算が繰り返され、訓練セットのデータが根本的に歪められることを防ぎ、その結果、モデル全体の訓練が歪められることを防ぐことができます。

void STrajectory::CumRevards(void)
  {
   if(CumCounted)
      return;
//---
   for(int i = Buffer_Size - 2; i >= 0; i--)
      Revards[i] += Revards[i + 1] * DiscountFactor;
   CumCounted = true;
  }

添付ファイルで扱い方をよく理解しておくことをお勧めします。では、当面の「主力」であるEAの話に移りましょう。

Research.mq5ファイルに、経験を収集するための最初のEAを作成します。ここでは、ストラテジーテスターの最適化モードでEAを起動し、過去データの訓練エピソードでエージェントを数回通過させ、経験を並行して収集する予定です。これは、前回の記事のフェーズ1で使ったアプローチとまったく同じです。Fasa1.mql5 EAと同様に、OnTester、OnTesterInit、OnTesterPass、OnTesterDeinitメソッドを使用して、様々なパスから情報を収集し、単一の経験蓄積バッファに保存します。ここでは、指定されたEAのように乱数生成器ではなく、モデルを使って行動を選択します。

外部EAのパラメータは、変更されることなく以前のものからコピーされます。これらのパラメータには、作業時間枠と使用する指標のパラメータを示します。

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_H1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price
input int                  Agent=1;

ストラテジーオプティマイザを起動するAgentパラメータを追加します。これは EA コードでは使用されず、ストラテジーテスターオプティマイザーでエージェントの数を調整するためにのみ必要です。

グローバル変数エリアでは、システムの現在の状態を記録するために、SState構造体の要素を1つ宣言します。1つの軌道構造体STrajectoryで現在のエージェントの経験を保存します。1つの要素から軌跡の静的配列を宣言し、フレーム間で経験を転送するために使用します。

SState               sState;
STrajectory          Base;
STrajectory          Buffer[];
STrajectory          Frame[1];
CNet                 Actor;
CFQF                 Schedule;
int                  Models = 1;

ここでは、2つのニューラルネットワークモデルを作成するための変数も示します。Agentとスケジューラーです。1つのエージェントモデルの中で複数のエージェントを使用します。この問題については、モデルのアーキテクチャーを説明するときに詳しく述べることにします。

EAの初期化方法に目新しいものはありません。指標オブジェクトと取引クラスを初期化し、事前に訓練されたモデルをアップロードします。もしそのようなモデルがなければ、ランダムなパラメータで新しいモデルを作ります。このメソッドの完全なコードは添付ファイルにあります。

ここでは、モデルのアーキテクチャを記述するCreateDescriptionsメソッドについて触れたいと思います。Actor-Critic法を用いて意図エージェントを訓練します。そこで、3つのモデルについて説明を作成します。

  • エージェント(Actor)
  • Critic
  • スケジューラー(階層の最上位モデル)

3モデル用のアーキテクチャ記述を作成する際に、2モデル用のグローバル変数が宣言されていても心配しないでください。実際、データ収集の段階でモデルを訓練することはありません。したがって、Critic機能は使用されません。そのため、そのモデルは作りません。 

同時に、比較可能なモデルを作るために、モデルのアーキテクチャを宣言する方法を共通化しました。これはデータ収集段階とモデル訓練段階の両方で使用されます。

メソッドのパラメータには、作成されたモデルのアーキテクチャを転送するための3つのオブジェクトへのポインタを受け取ります。メソッド本体では、受け取ったポインターの妥当性をチェックし、必要であれば新しいオブジェクトを作成します。

bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *scheduler)
  {
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
//---
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }
//---
   if(!scheduler)
     {
      scheduler = new CArrayObj();
      if(!scheduler)
         return false;
     }

まず、Actor(エージェント)アーキテクチャの説明を作成します。いつものように、最初に完全連結層を使い、次にデータ正規化層を使います。

//--- Actor
   actor.Clear();
   CLayerDescription *descr;
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (int)(HistoryBars * 12 + 9);
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

次に、もう1つの完全連結層を追加しました。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 300;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

次に、畳み込み層はデータ内の特定のパターンを識別しようとします。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 100;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

その結果を完全連結層で処理します。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

その後ろに別の畳み込み層を配置します。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 50;
   descr.window = 2;
   descr.step = 2;
   descr.window_out = 4;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

その結果、このような「レイヤーケーキ」は、データ次元を100要素に縮小することになります。このアーキテクチャはデータの前処理をおこないます。

次に、いくつかのインテントエージェントを作る必要があります。複数のモデルを作成するのを避けるため、これまでの経験を生かし、多モデル完全連結層のCNeuronMultiModelクラスを適用します。まず、十分な大きさの完全連結層を作ります。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 1000;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

次に、それぞれ10個のモデルからなる2つの隠れ多モデル完全連結層を作成します。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 200;
   descr.window = 100;
   descr.step = 10;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 50;
   descr.window = 200;
   descr.step = 10;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

モデリングの最終段階では、独自の特徴を持つ結果出力層を作成します。Actorの出力では、行動の確率的分布を受け取るはずです。方策勾配法について考えたとき、結果の単一ベクトルに対してSoftMax関数で出力を正規化することで同様の問題に対処しました。ここでは、10個のモデルの結果を正規化する必要があります。

完全連結マルチモデル層を使うことで、10個のモデルすべての結果が1つの行列に格納されます。CNeuronSoftMaxOCL層を使用して、データを正規化することができます。層を初期化する際、10行からなる行列を正規化する必要があることを示します。

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 4;
   descr.window = 50;
   descr.step = 10;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = 4;
   descr.step = 10;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

1つのデータ前処理ユニットと、それに続く10個の並列Actor(インテントエージェント)を持つモデルを開発しました。各Actorは、出力における行動の確率的分布を持ちます。

同様に、Criticモデルは10のCriticから出力されます。しかし、Criticの出力では、各行動のvalue関数の値を受け取ることが期待されます。したがって、CriticモデルではSoftMax層は使用しません。

このアルゴリズムにおけるスケジューラーモデルは、1つのレベルを持つ古典的なモデルです。しかし、このアルゴリズムの文脈では、スケジューラーはエージェントの行動を選択するのではなく、現在の状況でその方針に従うように、プールから特定のActorを選択します。スケジューラーには、システムの現在の状態を評価し、適切なインテン トエージェントを選択する機能があります。また、エージェントの状態を照会して判断を下すこともできます。

この実装では、分析されたシステムの状態のベクトルと、Actorのプールからの結果のベクトルを連結したものをスケジューラーに提供することが提案されています。これによりPlannerは、システム状態情報とActorの結果評価を使用して、適切なインテントActorを選択することができます。

スケジューラーモデルの説明では、適切なサイズのソースデータ層を示します。

//--- Scheduler
   scheduler.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (int)(HistoryBars * 12 + 9+40);
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

続いて、元のデータを正規化する層が続きます。

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

ソースデータを処理するために、前述と同様のモジュラーアプローチが使用されます。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 300;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 100;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 50;
   descr.window = 2;
   descr.step = 2;
   descr.window_out = 4;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

決定ブロックでは2つの隠れ層を持つパーセプトロンを使用します。これは多層ニューラルネットワークであり、抽象化された複数の層と高レベルの特徴を用いて入力データを処理分析することができます。隠れ層を2つ使うことで、モデルの表現力が高まり、入力データと出力決定の間の複雑な依存関係を捉えることができます。

このパーセプトロンの出力に、完全にパラメータ化された分位関数を適用します。分位関数は、入力データに基づいて対象変数の条件付分布をモデル化することを可能にします。単一の値を予測するのではなく、ターゲット変数の値がある範囲内に入る確率についての情報を提供します。

決定ブロックの結果層のサイズは、エージェントプールのサイズに対応しています。つまり、結果ベクトルの各要素は、プール内の対応するエージェントの確率またはスコアを表しています。これにより、スコアと確率に基づいて最適なエージェントやエージェントの組み合わせを選択することができます。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFQF;
   descr.count = 10;
   descr.window_out = 32;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

作成されたモデルアーキテクチャは、システムの現状を評価し、最適な決断を下すための幅広い可能性を提供します。多層ニューラルネットワークを使用することで、モデルは入力データの様々な側面を分析し、効果的な戦略と意思決定に関連付けることができるハイレベルの特徴を抽出することができます。

これにより、モデルは限られたデータや疎な報酬で効率的に問題を解決し、変化する条件やシナリオに適応することができます。

OnTickメソッドは、さらに言及に値します。最初に、新しいローソク足が開いているかどうかを確認し、システムの現在の状態に関するパラメータを収集します。このプロセスは、複数の記事でEAに対して変更を加えずに繰り返されますが、これについては詳しく説明しません。次に、2つのモデルを直接通過させ、その結果に基づいてエージェントの行動を選択します。

まず、インテントエージェントのプールをフォワードパスします。

   State1.AssignArray(sState.state);
   if(!Actor.feedForward(GetPointer(State1), 12, true))
      return;

エージェントのダイレクトパスによって得られた結果は、システム状態の現在の記述と連結され、評価のためにスケジューラーの入力に送られます。

   Actor.getResults(Result);
   State1.AddArray(Result);
   if(!Schedule.feedForward(GetPointer(State1),12,true))
      return;

両モデルをフォワードパスした後、サンプリングを使用して、その分布に基づいて特定のインテントエージェントを選択します。次に、選択されたエージェントから、その確率分布から特定の行動をサンプリングします。

   int act = GetAction(Result, Schedule.getSample(), Models);

重要なのは、訓練なしで、すべてのパスで一定のパラメータを持つモデルを使用することです。したがって、エージェントと行動を貪欲に選択すると、高い確率で各パスで同じ軌道を繰り返すことになります。分布からランダムな値をサンプリングすることで、環境を探索し、パスごとに異なる軌道を得ることができます。同時に、分布による制約があることで、一定の方向で調査を進めることができます。

関数の最後に、選択されたエージェントの行動を実行し、後の訓練のためにデータを保存します。

   switch(act)
     {
      case 0:
         if(!Trade.Buy(Symb.LotsMin(), Symb.Name()))
            act = 3;
         break;
      case 1:
         if(!Trade.Sell(Symb.LotsMin(), Symb.Name()))
            act = 3;
         break;
      case 2:
         for(int i = PositionsTotal() - 1; i >= 0; i--)
            if(PositionGetSymbol(i) == Symb.Name())
               if(!Trade.PositionClose(PositionGetInteger(POSITION_IDENTIFIER)))
                 {
                  act = 3;
                  break;
                 }
         break;
     }
//---
   float reward = 0;
   if(Base.Total > 0)
      reward = ((sState.state[240] + sState.state[241]) - 
               (Base.States[Base.Total - 1].state[240] + Base.States[Base.Total - 1].state[241])) / 10;
   if(!Base.Add(sState, act, reward))
      ExpertRemove();
//---
  }

各パスの後、実行された行動、通過したシステム状態、受け取った報酬に関する情報は、その後のモデルの訓練のために1つのバッファに保存されます。これらの操作はOnTester、OnTesterInit、OnTesterPass、OnTesterDeinitメソッドで実行されます。Go-Exploreアルゴリズムについての記事で、その原理を詳しく説明しています。

EAの全コードとすべてのメソッドを添付ファイルでご覧ください。

EAを構築して経験を収集した後、ストラテジーテスターの最適化モードでEAを起動し、Study.mq5モデル訓練EAに取り掛かります。本EAの外部パラメータでは、学習反復回数のみを示します。

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int                  Iterations     = 100000;

グローバル変数のブロックでは、すでにActor、Critic、スケジューラーの3つのモデルを示しています。モデルのアーキテクチャは前述の通り。

STrajectory          Buffer[];
CNet                 Actor;
CNet                 Critic;
CFQF                 Scheduler;

OnInit関数では、まず前のEAが作成した訓練サンプルをロードします。

int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

事前に訓練されたモデルを読み込むか、新しいモデルを作成します。

//--- load models
   float temp;
   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) ||
      !Scheduler.Load(FileName + "Sch.nnw", dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *schedule = new CArrayObj();
      if(!CreateDescriptions(actor, critic, schedule))
        {
         delete actor;
         delete critic;
         delete schedule;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic.Create(critic) || !Scheduler.Create(schedule))
        {
         delete actor;
         delete critic;
         delete schedule;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      delete schedule;
     }
   Scheduler.getResults(SchedulerResult);
   Models = (int)SchedulerResult.Size();
   Actor.getResults(ActorResult);
   Scheduler.SetUpdateTarget(Iterations);
   if(ActorResult.Size() % Models != 0)
     {
      PrintFormat("The scope of the scheduler does not match the scope of the Agent (%d <> %d)", 
                                                                     Models, ActorResult.Size());
      return INIT_FAILED;
     }

訓練プロセス開始イベントを初期化します。

//---
   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

Trainメソッドでは、直接訓練のプロセスをアレンジします。訓練セットは複数のパスから構成され、現在の実装では、それらをすべて1つの共通のデータベースにまとめるのではなく、連続した軌跡構造で状態を保存することに注意することが重要です。つまり、システムの1つの状態をランダムに選択するには、まず配列から1つのパスを選択し、次にそのパスから状態を選択する必要があります。

厳密に言えば、パッセージや行動を意図の特定のエージェントに関連付けることはしません。その代わり、すべてのエージェントは、共通の例に基づいて訓練されます。このアプローチにより、交換可能で一貫性のあるエージェント方策を作成することができ、各エージェントは、システムのどの状態からでも、その状態に到達する前にどの方策が適用されたかに関係なく、方策を実行し続けることができます。

メソッドの始めに、少し準備作業をおこないます。例データベースのパス数を決定し、訓練プロセスの時間を制御するための目盛りカウンターの値を保存します。

void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();

準備作業をおこなった後、モデル訓練のサイクルを編成します。

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = (int)(((double)MathRand() / 32767.0) * (total_tr - 1));
      int i = 0;
      i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));

訓練ループの中で、まず前述のように、訓練ベースの例からパスを選択します。次に、選択されたパスからランダムに状態を選択します。この状態は、ActorモデルとCriticモデルのフォワードパスの入力として渡されます。

      State1.AssignArray(Buffer[tr].States[i].state);
      if(IsStopped())
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }
      if(!Actor.feedForward(GetPointer(State1), 12, true) ||
         !Critic.feedForward(GetPointer(State1), 12, true))
         return;

ダイレクトパスの結果を対応するベクトルにアップロードします。

      Actor.getResults(ActorResult);
      Critic.getResults(CriticResult);

Actorのフォワードパスの結果のベクトルは、システムの状態ベクトルと連結されます。この結合されたベクトルは、分析と評価のためにスケジューラーモデルの入力に供給されます。

      State1.AddArray(ActorResult);
      if(!Scheduler.feedForward(GetPointer(State1), 12, true))
         return;

スケジューラーのフォワードパスを実行した後、貪欲なインテントエージェント選択を適用します。

      Scheduler.getResults(SchedulerResult);
      int agent = Scheduler.getAction();
      if(agent < 0)
        {
         iter--;
         continue;
        }

訓練の初期には、サンプリングを使って可能な限り環境を探索することが重要です。しかし、スケジューラーが学習し、その戦略が向上するにつれて、貪欲なエージェント選択に移行します。これは、Plannerが経験を積み、システムの状態をより正確に推定できるようになり、目標を達成するために最適なエージェントを選択できるようになるためです。

例のデータベースには、実行された行動とそれに対応する報酬に関する情報がすでに含まれているため、行動の選択に関する決定はおこないません。。このデータから各モデルの報酬ベクトルを生成し、各モデルに対して順次バックワードパスを実行します。まず、スケジューラーのバックワードパスを実行します。

      int actions = (int)(ActorResult.Size() / SchedulerResult.Size());
      float max_value = CriticResult[agent * actions];
      for(int j = 1; j < actions; j++)
         max_value = MathMax(max_value, CriticResult[agent * actions + j]);
      SchedulerResult[agent] = Buffer[tr].Revards[i];
      Result.AssignArray(SchedulerResult);
      //---
      if(!Scheduler.backProp(GetPointer(Result),0.0f,NULL))
         return;

そして、Criticのリバースパスメソッドを呼び出しますす。

      int agent_action = agent * actions + Buffer[tr].Actions[i];
      CriticResult[agent_action] = Buffer[tr].Revards[i];
      Result.AssignArray(CriticResult);
      //---
      if(!Critic.backProp(GetPointer(Result)))
         return;

続いて、意図のエージェントモデルです。

      ActorResult.Fill(0);
      ActorResult[agent_action] = Buffer[tr].Revards[i] - max_value;
      Result.AssignArray(ActorResult);
      //---
      if(!Actor.backProp(GetPointer(Result)))
         return;

ループの繰り返し終了時に、訓練時間を確認し、0.5秒ごとに訓練プロセスに関する情報をユーザーに表示します。

      if(GetTickCount() - ticks > 500)
        {
         string str = StringFormat("Actor %.2f%% -> Error %.8f\n", 
                                iter * 100.0 / (double)(Iterations), Actor.getRecentAverageError());
         str += StringFormat("Critic %.2f%% -> Error %.8f\n", 
                                iter * 100.0 / (double)(Iterations), Critic.getRecentAverageError());
         str += StringFormat("Scheduler %.2f%% -> Error %.8f\n", 
                                iter * 100.0 / (double)(Iterations), Scheduler.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

モデル訓練プロセスが完了したら、達成された結果を記録し、EAの終了を開始します。

   Comment("");
//---
   PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Critic.getRecentAverageError());
   PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Scheduler.getRecentAverageError());
   ExpertRemove();
//---
  }

EAのすべてのコードは添付ファイルにあります。このモデルのすべてのファイルは、SACディレクトリにアーカイブされています。

モデルの訓練プロセスは、最適化モードで例を収集し、リアルタイムチャート上で訓練プロセスを実行する反復で構成されます。訓練結果が期待にそぐわない場合は、例収集操作を再実行し、モデルを再訓練します。これらの操作は、学習目標を満たす最適な結果が得られるまで繰り返されます。

例題の収集とモデル訓練の反復は、学習プロセスの不可欠な部分です。それによってモデルを改良し、変化する状況に適応させ、最適な結果を達成するよう努力することができます。繰り返すたびに、新しいデータとモデルを改善する機会が得られ、より効果的に問題を解決し、目標を達成することができます。

学習プロセスは反復的であり、望ましい結果を得るまでに数回のサイクルを必要とすることに注意することが重要です。モデルの訓練は、常に改良と改善を必要とする複雑なプロセスだからです。反復的なアプローチをとり、目標を達成し最適な結果が得られるまで、収集と訓練の作業を繰り返すことを厭わないべきです。

例のデータベースが、その後に例題を収集するたびに常に更新されるように配置されたシステムは、私たちに大きな利点をもたらします。これにより、最も完全な例データベースを作成することができ、モデルの訓練と最適な決定を下す能力を大幅に向上させることができます。

しかし、例のデータベースのサイズを大きくすることは、それなりの結果をもたらすことを肝に銘じておく必要があります。第一に、大量のデータの処理と分析には時間がかかり、より多くのコンピューティングリソースを必要とする可能性があります。このため、モデル学習の反復時間が長くなる可能性があります。第二に、例ベースのサイズを大きくすると、モデルがより多くのデータを処理し、より多様なシナリオに適応する必要があるため、訓練の複雑さが増す可能性があります。


3.検証

2023年の最初の4か月間のEURUSD H1の過去のデータでモデルを訓練した結果、モデルは訓練セット上でもセット外でも利益を生み出すことが可能であることが示されました。各反復で8回から24回の最適化パスを含む、10回以上の例収集とモデルの訓練の繰り返しがおこなわれました。合計で200以上のパスが収集され、訓練プロセスには100,000から10,000,000回の反復が含まれました。

モデルの学習結果を確認するために、サンプリングの代わりに貪欲にエージェントと行動を選択するTest.mq5 EAを作成しました。これにより、モデルの動作をテストし、偶然の要因を排除することが可能になりました。

下のグラフは、訓練セット外でのモデルの結果を示しています。短期間で、このモデルはわずかな利益を上げることができました。プロフィットファクターは1.19、リカバリーファクターは0.46でした。

しかし、バランスグラフに不採算ゾーンが含まれていることは注目に値します。これは、モデルの訓練をさらに反復する必要があることを示している可能性があります。これは、利益を生み出す能力を向上させ、取引のリスクレベルを下げるのに役立ちます。

訓練結果 訓練結果


結論

金融市場のインテントエージェントモデルの学習において、Scheduled Auxiliary Control (SAC-X)法の効率性を強調することができます。SAC-Xは、古典的な強化学習アプローチを進化させたもので、金融データの特殊性と取引戦略の要件を考慮しています。

SAC-Xの大きな特徴のひとつは、複数のモデル(Actor、Critic、Planner)を使ってシステムの状態を評価し、意思決定をおこなうことです。これにより、取引の様々な側面を考慮し、より柔軟で適応力のあるエージェント方策を作成することができます。

SAC-Xのもう1つの重要な点は、システムの状態を分析し、最適なインテントエージェントを選択するためのスケジューラーの使用です。これにより、より効率的で正確な意思決定が可能になり、より安定した取引結果を得ることができます。

EURUSDの過去のデータでSAC-Xをテストしたところ、訓練セット上でもセット外でも利益を生み出す能力があることが示されました。しかし、バランスチャート上に不採算ゾーンが発見されたケースもあり、これはモデルの追加訓練の必要性を示している可能性があることに留意すべきです。

一般に、Scheduled Auxiliary Control (SAC-X)法は、金融業界におけるインテントエージェントモデルの訓練のための強力なツールです。市場データの詳細を考慮し、適応的で柔軟な取引戦略を作成することができ、安定した収益性の高い取引を達成する可能性を示します。SAC-Xのさらなる研究と改良は、より良い結果を導き、金融市場での応用を拡大することができます。


参考文献リスト

  • by Playing - Sparse Reward Taskをゼロから解く
  • ニューラルネットワークが簡単に(第29回):Advantage Actor-Criticアルゴリズム
  • ニューラルネットワークが簡単に(第35回):内因性好奇心モジュール
  • ニューラルネットワークが簡単に(第36回):関係強化学習
  • ニューラルネットワークが簡単に(第37回):疎な注意
  • ニューラルネットワークが簡単に(第38回):不一致による自己監視型探索
  • ニューラルネットワークが簡単に(第39回):Go-Explore、探検への異なるアプローチ
  • ニューラルネットワークが簡単に(第40回):大量のデータでGo-Exploreを使用する


  • 記事で使用されているプログラム

    # 名前 種類 詳細
    1 Research.mq5 EA 例収集EA
    2 Study.mql5 EA モデル訓練EA
    3 Test.mq5 EA モデルテストEA
    4 Trajectory.mqh クラスライブラリ システム状態記述構造
    5 FQF.mqh クラスライブラリ 完全にパラメータ化されたモデルの作業を整理するためのクラスライブラリ
    6 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
    7 NeuroNet.cl コードベース OpenCLプログラムコードライブラリ

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

    添付されたファイル |
    MQL5.zip (219.34 KB)
    ニューラルネットワークが簡単に (第42回):先延ばしのモデル、理由と解決策 ニューラルネットワークが簡単に (第42回):先延ばしのモデル、理由と解決策
    強化学習の文脈では、モデルの先延ばしにはいくつかの理由があります。この記事では、モデルの先延ばしの原因として考えられることと、それを克服するための方法について考察しています。
    ニューラルネットワークが簡単に(第40回):大量のデータでGo-Exploreを使用する ニューラルネットワークが簡単に(第40回):大量のデータでGo-Exploreを使用する
    この記事では、長い訓練期間に対するGo-Exploreアルゴリズムの使用について説明します。訓練時間が長くなるにつれて、ランダムな行動選択戦略が有益なパスにつながらない可能性があるためです。
    ニューラルネットワークが簡単に(第43回):報酬関数なしでスキルを習得する ニューラルネットワークが簡単に(第43回):報酬関数なしでスキルを習得する
    強化学習の問題は、報酬関数を定義する必要性にあります。それは複雑であったり、形式化するのが難しかったりします。この問題に対処するため、明確な報酬関数を持たずにスキルを学習する、活動ベースや環境ベースのアプローチが研究されています。
    リプレイシステムの開発 - 市場シミュレーション(第8回):指標のロック リプレイシステムの開発 - 市場シミュレーション(第8回):指標のロック
    この記事では、MQL5言語を使用しながら指標をロックする方法を見ていきます。非常に興味深く素晴らしい方法でそれをおこないます。