English Русский 中文 Español Deutsch Português
preview
ニューラルネットワークが簡単に(第71回):目標条件付き予測符号化(GCPC)

ニューラルネットワークが簡単に(第71回):目標条件付き予測符号化(GCPC)

MetaTrader 5トレーディングシステム | 11 6月 2024, 09:33
196 0
Dmitriy Gizlyk
Dmitriy Gizlyk

はじめに

目標条件付き行動模倣(BC)は、様々なオフライン強化学習問題を解くための有望な方法です。BCは、状態や行動の価値を評価する代わりに、設定された目標、分析された環境状態、エージェントの行動の間に依存関係を構築しながら、エージェントの行動方策を直接訓練します。これは、事前に収集されたオフラインの軌跡に対する教師あり学習法を用いて達成されます。おなじみのDecision Transformer法とその派生アルゴリズムは、オフライン強化学習におけるシーケンスモデリングの有効性を実証してきました。

以前、上記のアルゴリズムを使用する際、必要なエージェントの行動を刺激するために、目標設定の様々なオプションを実験しました。しかし、過去に通過した軌跡をモデルがどのように学習するのかは、私たちの関心の外にありました。ここで、軌跡を全体として研究することの適用性について疑問が生じます。この疑問は、論文『Predictive Coding for Offline Reinforcement Learning』の著者によって解決されました。論文の中で、彼らはいくつかの重要な疑問を探っています。

  1. オフラインの軌跡はシーケンスモデリングに有用なのか、それとも単に教師あり方策の学習により多くのデータを提供するだけなのか

  2. 方策学習を支持するために、軌道表現の最も効果的な学習目標は何か。シーケンスモデルは、過去の経験、将来のダイナミクス、あるいはその両方を符号化するように訓練されるべきなのか

  3. 同じシーケンスモデルを軌跡表現学習と方策学習の両方に使用できるため、学習目標は同じであるべきか否か

本稿では、3つの人工環境での実験結果を紹介し、以下の結論を導き出しています。

  • シーケンスモデリングは、適切に設計されていれば、結果として得られる軌跡表現が方策学習の入力として使用される場合、意思決定を効果的に支援することができます。

  • 最適軌道表現学習目標と方策学習目標には食い違いがあります。

これらの観察に基づき、この論文の著者は、シーケンスモデリングの事前学習を使用して、軌跡情報をコンパクトな圧縮表現に圧縮する2段階のフレームワークを作成しました。圧縮された表現は、単純な多層パーセプトロン(MLP)ベースのモデルを使用してエージェントの行動方策を訓練するために使用されます。彼らが提案する目標条件付き予測符号化(Goal-Conditioned Predictive Coding:GCPC)法は、軌跡表現を学習するための最も効果的な目標です。すべてのベンチマークテストで競争力のあるパフォーマンスを発揮します。著者は特に、長期タスクを解決するための有効性に注目しています。GCPCの強力な経験的性能は、過去と予測状態の潜在的表現に由来します。この場合、状態予測は、意思決定のための決定的な指針となる、設定された目標に焦点を当てて実行されます。

1.目標条件付き予測符号化アルゴリズム

GCPC法の著者は、オフライン強化学習にシーケンスモデリングを用いています。オフライン強化学習の問題を解決するために、彼らは条件付き模倣学習、フィルタ模倣学習、重み付き模倣学習を用います。事前に収集された訓練データのセットがあると仮定します。しかし、データ収集に使用された方策は知られていないかもしれません。訓練データには軌道のセットが含まれます。各軌跡は、状態と行動(St, At)の集合として表現されます。軌跡は、オプションとして、時間ステップtで得られる報酬Rtを含むことができます。

軌跡は未知の方策で収集されるため、最適であるとは限らないし、十分な専門知識を持っているとは限りません。最適でないデータを含むオフラインの軌跡を適切に利用することで、より効果的な行動方策を導き出せることはすでに述べました。なぜなら、最適でない軌道には、与えられた課題を解決するために組み合わせることができる有用な「スキル」を示す部分軌道が含まれている可能性があるからです。

この手法の著者は、エージェントの行動方策は、状態や軌道に関するあらゆる情報を入力として受け入れ、次の行動を予測できるものでなければならないと考えています。

  • 現在観測されている状態Stと目標Gのみを使用する場合、エージェント方策は履歴観測を無視します。 
  • エージェント方策がシーケンスモデルである場合、次の行動Atを予測するために、観測された軌跡全体を採用することができます。

エージェントの行動方策を最適化するために、通常、最尤目的関数が使用されます。

シーケンスモデリングは、学習軌跡表現と学習行動方策という2つの観点から意思決定に用いることができます。1番目の方向性は、生の入力軌跡から、凝縮された潜在表現または事前に訓練されたネットワーク重みの形で有用な表現を得ようとするものです。2番目の方向性は、観察と目標を、タスクを完了するための最適な行動に変換しようとするものです。

軌跡関数と方策関数の学習は、Transformerモデルを使用して実装することができます。GCPC法の著者は、軌跡関数の場合、シーケンスモデリング技術を用いて元のデータを凝縮表現に圧縮することが有効であることを示唆しています。また、軌道表現学習を方策学習から切り離すことも望ましくなります。このように分離することで、表現学習の目標を柔軟に選択できるだけでなく、シーケンスモデリングが軌道表現学習と方策学習に与える影響を独立して研究することができます。そのためGCPCでは、TrajNet(軌跡モデル)とPolicyNet(方策モデル)の2段階構造を採用しています。TrajNetの学習には、マスクオートエンコーダやネクストトークン予測などの教師なし学習法をシーケンスのモデリングに使用します。PolicyNetは、収集されたオフラインの軌跡から教師あり学習の目的関数を使用して効果的な方策を導出することを目指しています。

軌跡表現訓練の第一段階では、マスクされた自動符号化を用います。TrajNetは軌跡と必要に応じて目標Gを受け取り、同じ軌跡のマスクされたビューからτを復元するように学習します。オプションとして、TrajNetは軌跡Bの凝縮された表現も生成し、これはその後の方策の訓練のためにPolicyNetが使用することができます。論文では、GCPC法の著者は、トラバースされた軌跡のマスクされた表現をオートエンコーダモデルへの入力として与えることを提案しています。デコーダの出力では、トラバースした軌跡とそれに続く状態のマスクされていない表現を得ようと努力します。

第2段階では、TrajNetがマスクされていない観測された軌跡に適用され、軌跡Bの凝縮された表現を得ます。そして、PolicyNetは、観測された軌跡(または環境の現在の状態)、目標G、凝縮された軌跡表現Bを与えて、行動Aを予測します。

提案されたフレームワークは、表現学習と方策学習を実装するための異なる設計を比較するための統一的な視点を提供します。既存の多くの手法は、提案された構造の特殊なケースと考えることができます。例えば、DTの実装では、軌跡表現関数は入力軌跡の同一性マッピング関数に設定され、方策は自己回帰的に行動を生成するように訓練されます。

著者の可視化法は以下で示されています。

2.MQL5を使用した実装

ここまで、目標条件付き予測符号化法の理論的側面について考えてきました。次に、MQL5を使用した実装について説明しましょう。ここでは主に、モデルの訓練と運用の段階ごとに使用されるモデルの数が異なることに注意を払う必要があります。

2.1 モデルのアーキテクチャ

第1段階では、この手法の著者は軌跡表現モデルを訓練することを提案しています。モデルアーキテクチャはTransformerを使用しています。これを訓練するには、オートエンコーダを構築する必要があります。第2段階では、訓練済みエンコーダのみを使用します。したがって、不要なデコーダを第2段階の訓練に「引きずり込まない」ために、オートエンコーダを2つのモデルに分割します。エンコーダとデコーダです。モデルのアーキテクチャはCreateTrajNetDescriptionsメソッドで示されます。パラメータとして、このメソッドは、指定されたモデルのアーキテクチャを示す2つの動的配列へのポインタを受け取ります。

メソッド本体では、受け取ったポインタを確認し、必要であれば新しい動的配列オブジェクトを作成します。

bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *decoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!decoder)
     {
      decoder = new CArrayObj();
      if(!decoder)
         return false;
     }

まず、エンコーダのアーキテクチャを説明しましょう。過去の値動きと分析指標データのみをモデルに投入します。  

//--- 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;
     }

先に説明したモデルとは異なり、この段階では、口座ステータスに関するデータも、エージェントが以前におこなった行動に関する情報も使用しません。過去の行動に関する情報がマイナスに働くケースもあるという意見もあります。そのため、GCPC法の著者はこれをソースデータから除外しました。口座の状態に関する情報は、環境の状態には影響しません。したがって、その後の環境状態の予測には重要ではありません。

常に未処理のソースデータをモデルに投入します。そこで次の層では、ソースデータを比較可能な形にするためにバッチ正規化をおこないます。

//--- 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(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

データを前処理した後、GCPCアルゴリズムが提供するランダムデータマスキングを実装する必要があります。この機能を実装するために、DropOut層を使用します。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDropoutOCL;
   descr.count = prev_count;
   descr.probability = 0.8f;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

一般的には、1つのモデルでバッチ正規化層とDropOutを一緒に使用することは推奨されません。これは、一部の情報を除外してゼロ値で置き換えると、元のデータ分布が歪み、バッチ正規化層の動作に悪影響を及ぼすためです。このため、まずデータを正規化し、それからマスクします。こうすることで、バッチ正規化層は完全なデータセットで動作し、DropOut層の動作への影響を最小限に抑えることができます。同時に、マスキング機能を実装し、欠損データを回復し、確率的環境に固有の外れ値を無視するようにモデルを訓練します。

エンコーダのモデルでは次に、データの次元を下げ、安定したパターンを識別するための畳み込みブロックが来ます。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_wout = descr.window_out = BarDescr / 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

上述のソースデータの処理結果は、全結合層のブロックに供給され、これにより初期状態の埋め込みを得ることができます。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

GCPC法の著者は、過去のデータに加えて、目標埋め込みとスロットトークン(以前のエンコーダのパス結果)をエンコーダに供給することを提案しています。最大限の利益を得るという私たちのグローバルな目標は、環境に影響を与えないので省いています。その代わりに、エンコーダの最後のパスの結果を、連結層を使用してモデルに追加します。

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 2 * EmbeddingSize;
   descr.window = prev_count;
   descr.step = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

さらなるデータ処理はGPTモデルを使用しておこなわれます。これを実現するために、まず埋め込み層を使用してデータスタックを作成します。

//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronEmbeddingOCL;
   prev_count = descr.count = GPTBars;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      ArrayCopy(descr.windows, temp);
     }
   prev_wout = descr.window_out = EmbeddingSize;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

その後に注目ブロックが続きます。前回、すでにDropOut層を使用してデータスパースプロセスを作成したので、このモデルではスパースアテンション層を使用しませんでした。 

//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_count * 2;
   descr.window = prev_wout;
   descr.step = 4;
   descr.window_out = 16;
   descr.layers = 4;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

エンコーダの出力では、全結合層でデータの次元を縮小し、SoftMax関数でデータを正規化します。

//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = 1;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

軌跡を凝縮したものをデコーダの入力に送り込みます。

//--- Decoder
   decoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

デコーダの初期データは前モデルから入手したもので、すでに同等の形になっています。つまり、この場合はバッチ正規化層は必要ありません。得られたデータは全結合層で処理されます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (HistoryBars + PrecoderBars) * EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

そしてアテンション層でそれを処理します。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHAttentionOCL;
   prev_count = descr.count = prev_count / EmbeddingSize;
   prev_wout = descr.window = EmbeddingSize;
   descr.step = 4;
   descr.window_out = 16;
   descr.layers = 2;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

私たちのデコーダのアーキテクチャは、アテンションブロックの出力に、分析され予測された環境状態の各ローソク足の埋め込みを持つように構築されています。ここで、データの目的を理解する必要があります。次のことを考えてみましょう。

なぜ指標を分析するのでしょうか。トレンド指標はトレンドの方向を示します。オシレーターは、買われ過ぎと売られ過ぎのゾーンを示し、それによって市場が反転する可能性のあるポイントを示すように設計されています。これらはすべて、現時点では貴重なものです。そのような予測は、ある程度の深みがあれば価値があるのでしょうか。個人的な意見としては、データの予測誤差を考慮すると、指標の予測値はゼロに近いと思います。最終的には、指標値からではなく、商品価格の変動から損益を受け取ります。そこで、デコーダの出力で値動きのデータを予測します。

再生バッファに保存されている値動きの情報を思い出してみましょう。この情報には3つの偏差値が含まれています。

  • ローソク足実体終値 - 始値
  • 高値 - 始値
  • 安値 - 始値

これらの値を予測します。ローソク足の埋め込みから独立して値を復元するために、モデルのアンサンブルの層を使用します。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 3;
   descr.window = prev_wout;
   descr.step = prev_count;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

以上で、軌跡提示TrajNetの学習の第一段階におけるオートエンコーダアーキテクチャの説明を終えますが、EAのモデル訓練に移る前に、モデルのアーキテクチャを説明する作業を完了させることを提案します。第2段階の方策学習モデルPolicyNetのアーキテクチャを見てみましょう。アーキテクチャはCreateDescriptionsメソッドで提供されます。

予想に反して、第2段階ではActor行動方策のモデルを1つではなく、3つ訓練することになります。

最初のものは、現在状態エンコーダの小さなモデルです。第1段階で学習したオートエンコーダエンコーダと混同しないようにしてください。このモデルは、オートエンコーダからの軌跡の凝縮された表現と、口座の状態に関する情報を単一の表現に統合します。

もう1つは、前述したActorの方策モデルです。

そして3つ目は、凝縮された軌跡表現の分析に基づく目標設定のモデルです。

いつものように、メソッドのパラメータには、モデルアーキテクチャを記述する動的配列へのポインタを渡します。メソッド本体では、受け取ったポインタの妥当性を確認し、必要であれば動的配列オブジェクトの新しいインスタンスを作成します。

bool CreateDescriptions(CArrayObj *actor, CArrayObj *goal, CArrayObj *encoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!goal)
     {
      goal = new CArrayObj();
      if(!goal)
         return false;
     }
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }

上述したように、軌跡の凝縮された表現をエンコーダに送り込みます。

//--- State Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

受信したデータは、連結層で口座状態に関する情報と結合されます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

この時点で、エンコーダに代入されたタスクは完了したとみなされ、前のモデルの作業結果を入力として受け取るActorのアーキテクチャに移ります。

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

受信したデータを設定した目標と組み合わせます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = NRewards;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

全結合層で処理します。

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

Actorの出力では、その行動方策に確率を加えます。

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

そして最後が目標生成モデルです。利益を生み出せるかどうかは、環境状態のさまざまな側面に強く依存していることは周知の事実だと思います。そこで、過去の経験から、環境の状態に応じて目標を生成するモデルを別に追加することにしました。

観測された軌跡を凝縮してモデル入力に与えます。ここで話しているのは、口座の状態を考慮せずに軌道についてです。私たちの報酬関数は、特定の預金額に縛られることなく、相対的な価値で運用されるように構築されています。したがって、目標を設定するためには、口座の状態を考慮することなく、環境の分析だけを進めます。

//--- Goal
   goal.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }

受信されたデータは、2つの全結合層によって分析されます。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!goal.Add(descr))
     {

      delete descr;
      return false;
     }

モデル出力では、完全にパラメータ化された分位関数を使用します。この解決法の利点は、全結合層にありがちな平均値ではなく、最も確率の高い結果を返すことです。結果の違いは、2つ以上の頂点を持つ分布で最も顕著です。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFQF;
   descr.count = NRewards;
   descr.window_out = 32;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!goal.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

2.2 環境との相互作用モデル

目標条件付き予測符号化法の実装を続けます。モデルアーキテクチャを説明した後、アルゴリズムの実装に移ります。まず、環境と対話し、訓練サンプルのデータを収集するためのEAを実装します。この手法の著者は、訓練データの収集手法には焦点を当てていません。実際、訓練データセットは、先に説明したEx-ORLアルゴリズムとReal-ORLアルゴリズムを含め、利用可能なあらゆる方法で収集することができます。必要なのは、データの記録形式とプレゼンテーション形式を一致させることだけです。ただし、事前に訓練されたモデルを最適化するためには、環境と相互作用する過程で、学習した行動方策を使用し、相互作用の結果を軌跡として保存するEAが必要です。この機能はEA「..\Experts\GCPC\Research.mq5」に実装されています。EAアルゴリズム構築の基本原理は、これまでの研究で用いられてきたものと一致します。しかし、モデル数の多さには定評があります。EAの手法のいくつかに焦点を当ててみましょう。

このEAでは、4つのモデルを使用します。

CNet                 Encoder;
CNet                 StateEncoder;
CNet                 Actor;
CNet                 Goal;

事前に訓練されたモデルは、OnInit EA初期化メソッドで読み込まれます。このメソッドの完全なコードは添付ファイルにあります。ここでは、その変更点についてのみ言及します。

まず、オートエンコーダモデルを読み込みます。読み込みエラーがあれば、ランダムなパラメータで新しいモデルを初期化します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
........
........
//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateTrajNetDescriptions(encoder, decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      delete encoder;
      delete decoder;
      //---
     }

そして、残りの3つのモデルを読み込みます。必要に応じて、ランダムなパラメータで初期化します。

   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      delete actor;
      delete goal;
      delete encoder;
      //---
     }

すべてのモデルを1つのOpenCLコンテキストに転送します。

   StateEncoder.SetOpenCL(Actor.GetOpenCL());
   Encoder.SetOpenCL(Actor.GetOpenCL());
   Goal.SetOpenCL(Actor.GetOpenCL());

エンコーダモデルの訓練モードは必ずオフにしてください。

   Encoder.TrainMode(false);

なお、このEAではバックプロパゲーションの手法を使用する予定はありませんが、エンコーダではDropOut層を使用しています。したがって、モデルの動作条件下でマスキングを無効にするように訓練モードを変更する必要があります。

次に、読み込まれたモデルのアーキテクチャの整合性を確認します。

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size (%d <> %d)", EmbeddingSize, 
                                                                                                  Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                        (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
//---
   return(INIT_SUCCEEDED);
  }

環境とのインタラクションはOnTickメソッドで実装されています。メソッドの冒頭で、新しいバー開始イベントの発生を確認し、必要なら履歴データを読み込みます。受信した情報はデータバッファに転送されます。これらの操作は、以前の実装から変更なくコピーされているので、ここでは触れません。フィードフォワードモデルパスのメソッド呼び出しのシーケンスだけを考えてみましょう。GCPCアルゴリズムが提供するように、まずエンコーダのフィードフォワードメソッドを呼び出します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
........
........
//---
   if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(Encoder)) ||

このモデルは、2つ目の情報の流れのデータソースとして、それ自体を再利用しています。

次に、状態エンコーダと目標モデルのフィードフォワードメソッドを呼び出します。どちらのモデルも、観測された軌跡の凝縮された表現を入力データとして使用します。

      !StateEncoder.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat *)GetPointer(bAccount)) ||
      !Goal.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat *)NULL) ||

これらのモデルの結果は、Actor方策モデルの入力に入力され、その後の行動を生成します。

      !Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CNet *)GetPointer(Goal)))
      return;

操作の結果を確認することも忘れてはなりません。

次に、Actorモデルの結果がデコードされ、環境内で行動が実行され、得られた経験が軌跡として保存されます。これらの操作のアルゴリズムはそのまま使用されます。環境と相互作用するためのEAの完全なコードは添付ファイルにあります。

2.3 軌跡関数の訓練

訓練データセットを収集した後、モデルの訓練EAの構築に移ります。GCPCアルゴリズムによれば、最初のステップはTrajNetの軌跡関数モデルを訓練することです。この機能をEA「 ...\Experts\GCPC\StudyEncoder.mq5」に実装しました。

この記事の理論的な部分で説明したように、最初の段階では、マスクされたオートエンコーダモデルを訓練します。これは2つのモデルを含みます。エンコーダとデコーダです。

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int                  Iterations     = 1e4;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
STrajectory          Buffer[];
CNet                 Encoder;
CNet                 Decoder;

次の瞬間にご注目ください。エンコーダは再帰的に、前のパスの結果を2つ目のフロー情報の初期データとして使用します。フィードフォワードパスには、単純にモデル自体へのポインタを使用します。ただし、バックプロパゲーションパスの場合、この方法は受け入れられません。なぜなら、モデルの結果バッファには、前のパスではなく、最後のパスのデータが含まれるからです。これは、私たちのモデル訓練過程には受け入れられません。したがって、前のパスの結果を保存するための追加のデータバッファが必要になります。

CBufferFloat         LastEncoder;

EA初期化メソッドでは、まず訓練データセットを読み込み、演算結果を確認します。モデルを訓練するためのデータがなければ、その後の操作はすべて無意味です。

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

訓練データセットを読み込んだたら、訓練済みのモデルを開いてみます。エラーが発生した場合は、ランダムなパラメータで新しいモデルを初期化します。

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new models");
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateTrajNetDescriptions(encoder, decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder) || !Decoder.Create(decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      delete encoder;
      delete decoder;
      //---
     }

 両方のモデルを1つのOpenCLコンテキストに配置します。

   OpenCL = Encoder.GetOpenCL();
   Decoder.SetOpenCL(OpenCL);

モデルアーキテクチャの互換性を確認します。

   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size count (%d <> %d)", EmbeddingSize,
                                                                                                 Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                       (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("Input size of Decoder doesn't match Encoder output (%d <> %d)", Result.Total(), EmbeddingSize);
      return INIT_FAILED;
     }

確認ブロックを通過できたら、同じOpenCLコンテキストで補助バッファを初期化します。

   if(!LastEncoder.BufferInit(EmbeddingSize,0) ||
      !Gradient.BufferInit(EmbeddingSize,0) ||
      !LastEncoder.BufferCreate(OpenCL) ||
      !Gradient.BufferCreate(OpenCL))
     {
      PrintFormat("Error of create buffers: %d", GetLastError());
      return INIT_FAILED;
     }

EA初期化メソッドの最後に、学習過程開始のためのカスタムイベントを生成します。

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

実際の訓練過程はTrainメソッドで実行されます。この手法では、従来、目標条件付き予測符号化アルゴリズムと以前の記事で開発したものを組み合わせています。手法の最初に、モデルの学習に軌跡を使用する確率のベクトルを作成します。

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

ただし、この場合、軌跡を計量することによる実用的な効果はありません。オートエンコーダを訓練する過程では、値動きの履歴データと分析された指標のみを使用します。軌跡はすべて、1つの計測器の1つの履歴区間で収集されています。したがって、私たちのオートエンコーダでは、すべての軌跡が同一のデータを含んでいます。とはいえ、さまざまな時間間隔や計測器の軌跡でモデルを訓練できるようにするため、この機能は将来に残しておこうと思います。

次に、ローカル変数とベクトルを初期化します。標準偏差のベクトルに注目してみましょう。そのサイズは、デコーダの結果のベクトルに等しくなります。その使用原理については、もう少し後で説明します。

   vector<float> result, target;
   matrix<float> targets;
   STD = vector<float>::Zeros((HistoryBars + PrecoderBars) * 3);
   int std_count = 0;
   uint ticks = GetTickCount();

準備作業の後、モデル訓練サイクルのシステムを導入します。エンコーダは、ソースデータのシーケンスに敏感な潜在状態のスタックを持つGPTブロックを使用します。したがって、モデルを学習する際には、サンプリングされた各軌跡から連続した状態のバッチ全体を使用します。

外側ループの本体では、事前に生成した確率を考慮して、1つの軌道をサンプリングし、その軌道上の初期状態をランダムに選択します。

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int batch = GPTBars + 50;
      int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 3 - PrecoderBars - batch));
      if(state <= 0)
        {
         iter--;
         continue;
        }

次に、モデルスタックと以前のエンコーダ結果のバッファをクリアします。

      Encoder.Clear();
      Decoder.Clear();
      LastEncoder.BufferInit(EmbeddingSize,0);

これで、選択された軌道のネストされた学習ループを開始する準備がすべて整いました。

      int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(int i = state; i < end; i++)
        {
         State.AssignArray(Buffer[tr].States[i].state);

ループの本体では、訓練データセットから初期データバッファを満たし、モデルのフィードフォワードパスメソッドを順次呼び出します。まずエンコーダです。

         if(!LastEncoder.BufferWrite() || !Encoder.feedForward((CBufferFloat*)GetPointer(State), 1, false, 
                                                               (CBufferFloat*)GetPointer(LastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

次にデコーダです。

         if(!Decoder.feedForward(GetPointer(Encoder), -1, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

モデルのフィードフォワードパスが成功したら、バックプロパゲーションパスを実行し、モデルのパラメータを調整する必要があります。しかし、その前にデコーダの結果の目標値を用意する必要があります。覚えているように、デコーダの出力では、複数のローソク足の再構築された値と価格変動予測の結果を受け取る予定であり、それは各ローソク足の状態を記述する配列の最初の3つの要素で示されます。このデータを得るために、行列を作成し、その各行に、目的の時間範囲における環境状態の記述を格納します。そして、出来上がった行列の最初の3列だけを取り出します。これが目標値となります。

         target.Assign(Buffer[tr].States[i].state);
         ulong size = target.Size();
         targets = matrix<float>::Zeros(1, size);
         targets.Row(target, 0);
         if(size > BarDescr)
            targets.Reshape(size / BarDescr, BarDescr);
         ulong shift = targets.Rows();
         targets.Resize(shift + PrecoderBars, 3);
         for(int t = 0; t < PrecoderBars; t++)
           {
            target.Assign(Buffer[tr].States[i + t].state);
            if(size > BarDescr)
              {
               matrix<float> temp(1, size);
               temp.Row(target, 0);
               temp.Reshape(size / BarDescr, BarDescr);
               temp.Resize(size / BarDescr, 3);
               target = temp.Row(temp.Rows() - 1);
              }
            targets.Row(target, shift + t);
           }
         targets.Reshape(1, targets.Rows()*targets.Cols());
         target = targets.Row(0);

閉形式演算子の使用について説明した前回の記事の結果に触発され、私は学習過程を少し変え、大きな偏差に重きを置くことにしました。だから、ちょっとしたズレは予測誤差だと思って無視します。そこでこの段階で、目標値からのモデル結果の移動標準偏差を計算します。

         Decoder.getResults(result);
         vector<float> error = target - result;
         std_count = MathMin(std_count, 999);
         STD = MathSqrt((MathPow(STD, 2) * std_count + MathPow(error, 2)) / (std_count + 1));
         std_count++;

ここで注意しなければならないのは、各パラメータの偏差を個別に制御していることです。

次に、現在の予測誤差が閾値を超えるかどうかを確認します。バックプロパゲーションパスは、少なくとも1つのパラメータ閾値以上の予測誤差がある場合にのみ実行されます。

         vector<float> check = MathAbs(error) - STD * STD_Multiplier;
         if(check.Max() > 0)
           {
            //---
            Result.AssignArray(CAGrad(error) + result);
            if(!Decoder.backProp(Result, (CNet *)NULL) ||
               !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient)))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               break;
              }
           }

この方法にはいくつかのニュアンスがあります。モデルの平均誤差は、バックプロパゲーションパスを実行するときにのみ計算されます。したがって、この場合、現在の誤差が平均誤差に影響するのは、閾値を超えたときだけです。その結果、私たちが無視する小さな誤差は、モデルの平均誤差の値に影響しません。したがって、この指標は過大評価となります。値は純粋に情報提供のみを目的としているため、これは重要なことではありません。

「コインの裏側」とは、有意な乖離のみに注目することで、特定のパフォーマンス値に影響を与える主なドライバーをモデルが特定できるようになるということです。閾値のガイドラインとして移動標準偏差を使用することで、学習過程で許容される誤差の閾値を小さくすることができます。これにより、モデルの微調整が可能になります。

ループの反復が終わったら、エンコーダの結果を補助バッファに保存し、モデル訓練過程の進捗状況をユーザーに通知します。

         Encoder.getResults(result);
         LastEncoder.AssignArray(result);
         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations);
            string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Decoder", percent, Decoder.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

訓練ループのすべての反復が完了したら、チャートのコメント欄を消去し、訓練結果に関する情報をログに表示し、EAのシャットダウンを開始します。

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

EAの初期化解除メソッドでは、訓練済みモデルの保存とメモリのクリアを忘れないようにしてください。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
     {
      Encoder.Save(FileName + "Enc.nnw", 0, 0, 0, TimeCurrent(), true);
      Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), 0, 0, TimeCurrent(), true);
     }
   delete Result;
   delete OpenCL;
  }

2.4 方策訓練

次のステップは、EA「 ...\Experts\GCPC\Study.mq5」で実装されているエージェントの行動方策の訓練です。ここでは、状態エンコーダモデルを訓練します。これは、本質的にエージェントモデルの不可欠な部分です。目標設定モデルの訓練もおこないます。

エージェント行動方策と目標設定モデルの訓練の過程を2つの別々のプログラムに分けることは機能的には可能ですが、私は1つのEAにまとめることにしました。実装アルゴリズムからわかるように、これら2つの過程は密接に絡み合い、大量の共通データを使用します。この場合、モデル訓練を2つの並列処理に分けるのは効率的とは言い難いです。

このEAは、環境とのインタラクションのためのEAと同様に、4つのモデルを使用し、そのうち3つはこのEAで訓練されます。

CNet                 Actor;
CNet                 StateEncoder;
CNet                 Encoder;
CNet                 Goal;

OnInit EA初期化メソッドでは、前述のEAと同様に、訓練データセットを読み込みます。

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

次にモデルを読み込みます。まず、事前に訓練されたエンコーダを開いてみます。これは、目標条件付き予測符号化アルゴリズムの最初の段階で訓練されなければなりません。このモデルがなければ、次のステージに進むことはできません。

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Cann't load Encoder model");
      return INIT_FAILED;
     }

エンコーダモデルの読み込みに成功したら、残りのモデルを開いてみます。すべてがこのEAで訓練されます。したがって、エラーが発生した場合は、新しいモデルを作成し、ランダムなパラメータで初期化します。

   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
        {
         delete actor;
         delete goal;
         delete encoder;
         return INIT_FAILED;
        }
      delete actor;
      delete goal;
      delete encoder;
      //---
     }

次に、すべてのモデルを1つのOpenCLコンテキストに移動します。また、エンコーダの訓練モードをfalseに設定し、ソースデータのマスキングを無効にしました。

次のステップは、モデル間のデータ転送時に起こりうるエラーを排除するために、読み込まれたすべてのモデルのアーキテクチャの互換性を確認することです。

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("The scope of the Encoder does not match the embedding size (%d <> %d)", EmbeddingSize, Result.Total());
      return INIT_FAILED;
     }
//---
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(),
                                                                                               (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   StateEncoder.GetLayerOutput(0, Result);
   if(Result.Total() != EmbeddingSize)
     {
      PrintFormat("Input size of State Encoder doesn't match Bottleneck (%d <> %d)", Result.Total(), EmbeddingSize);
      return INIT_FAILED;
     }
//---
   StateEncoder.getResults(Result);
   int latent_state = Result.Total();
   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Actor doesn't match output State Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }
//---
   Goal.GetLayerOutput(0, Result);
   latent_state = Result.Total();
   Encoder.getResults(Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Goal doesn't match output Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }
//---
   Goal.getResults(Result);
   if(Result.Total() != NRewards)
     {
      PrintFormat("The scope of Goal doesn't match rewards count (%d <> %d)", Result.Total(), NRewards);
      return INIT_FAILED;
     }

必要な制御をすべて渡すことに成功したら、OpenCLコンテキストに補助バッファを作成します。

   if(!bLastEncoder.BufferInit(EmbeddingSize, 0) ||
      !bGradient.BufferInit(MathMax(EmbeddingSize, AccountDescr), 0) ||
      !bLastEncoder.BufferCreate(OpenCL) ||
      !bGradient.BufferCreate(OpenCL))
     {
      PrintFormat("Error of create buffers: %d", GetLastError());
      return INIT_FAILED;
     }

学習過程開始のカスタムイベントを生成します。

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

EAの初期化解除メソッドでは、訓練済みモデルを保存し、使用された動的オブジェクトを削除します。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
     {
      Actor.Save(FileName + "Act.nnw", 0, 0, 0, TimeCurrent(), true);
      StateEncoder.Save(FileName + "StEnc.nnw", 0, 0, 0, TimeCurrent(), true);
      Goal.Save(FileName + "Goal.nnw", 0, 0, 0, TimeCurrent(), true);
     }
   delete Result;
   delete OpenCL;
  }

モデルを訓練する過程は、Trainメソッドで実装されています。メソッドの本体では、まずモデルを訓練するための軌道を選択するための確率のバッファを生成します。訓練セット内のすべての軌道を、その収益性で重み付けします。最も収益性の高いパスは、学習過程に参加する可能性が高いです。

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

次に、ローカル変数を初期化します。ここで、標準偏差のベクトルが2つあることに気づくでしょう。

   vector<float> result, target;
   matrix<float> targets;
   STD_Actor = vector<float>::Zeros(NActions);
   STD_Goal = vector<float>::Zeros(NRewards);
   int std_count = 0;
   bool Stop = false;
//---
   uint ticks = GetTickCount();

どの訓練済みモデルのアーキテクチャも回帰ブロックやスタックを持ちませんが、それでもモデルを訓練するためのループを作成します。なぜなら、訓練済みモデルの初期データは、GPTアーキテクチャを操作するエンコーダによって生成されるからです。

外側ループの本体では、軌道とその上の初期状態をサンプリングします。

   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int batch = GPTBars + 50;
      int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - PrecoderBars - batch));
      if(state <= 0)
        {
         iter--;
         continue;
        }

エンコーダのスタックとバッファの最新の結果をクリアします。

      Encoder.Clear();
      bLastEncoder.BufferInit(EmbeddingSize, 0);

このモデルではバックプロパゲーションパスを実行するつもりはありませんが、エンコーダの最後の状態を記録するためにバッファを使用しています。フィードフォワードパスには、環境との相互作用EAで実装されたように、モデルへのポインタを使用することができます。しかし、新しい軌道に移るときには、潜在状態のスタックだけでなく、モデルの結果バッファもリセットする必要があります。追加のバッファを使用する方が簡単です。

ネストされたループの本体では、訓練データセットから分析された状態データを読み込み、エンコーダモデルを使用してその凝縮された表現を生成します。

      int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(int i = state; i < end; i++)
        {
         bState.AssignArray(Buffer[tr].States[i].state);
         //---
         if(!bLastEncoder.BufferWrite() ||
            !Encoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bLastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

次に、口座状態記述バッファに、タイムスタンプのハーモニクスを補充します。

         float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
         bAccount.Clear();
         bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance);
         bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
         bAccount.Add(Buffer[tr].States[i].account[2]);
         bAccount.Add(Buffer[tr].States[i].account[3]);
         bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance);
         bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance);
         double time = (double)Buffer[tr].States[i].account[7];
         double x = time / (double)(D'2024.01.01' - D'2023.01.01');
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_MN1);
         bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_W1);
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = time / (double)PeriodSeconds(PERIOD_D1);
         bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         if(bAccount.GetIndex() >= 0)
            bAccount.BufferWrite();

分析された環境の状態の凝縮された表現は、口座の状態を記述するベクトルと組み合わされます。

         //--- State embedding
         if(!StateEncoder.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat*)GetPointer(bAccount)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

次に、Actorのフィードフォワードパスを実行するには、目標を示す必要があります。Decision Transformerの場合と同様に、この段階では環境との相互作用から得られた実際の結果を目標として使用します。エージェントの実際の行動は、方策の目標結果として使用されます。こうして、特定の環境状態における目標と行動の間につながりを築きます。ただ、1点だけ。オートエンコーダを訓練する際には、数ローソク足先の予測データを得ることを目指しました。したがって、現在の状態を凝縮した表現で表現することで、その後に続く何本かのローソク足についての予測情報を持つことが期待されます。この段階で実行されるエージェントの行動は、予測された時間内に結果を得るように設計されるべきだと考えるのは論理的です。予測期間中の総報酬を行動目標とすることもできます。しかし、現在未決済になっている取引が、なぜ予測期間終了後にしか決済されないのでしょうか。決済を早めることも、遅らせることもできます。「それ以降」の場合、予測値の先を見ることはできません。従って、予測期間終了時の結果しか取ることができません。しかし、予測期間内に値動きの方向性が変われば、取引は早めに決済されるべきです。したがって、私たちの潜在的な目標は、割引率を考慮した予測期間中の最大値となるはずです。

問題は、経験再生バッファがエピソード終了まで累積報酬を保存することです。しかし、予測データのホライズンにおける分析された状態からの報酬の総量が必要です。そこで、まず割引率を考慮せずに各ステップでの報酬を復元します。

         targets = matrix<float>::Zeros(PrecoderBars, NRewards);
         result.Assign(Buffer[tr].States[i + 1].rewards);
         for(int t = 0; t < PrecoderBars; t++)
           {
            target = result;
            result.Assign(Buffer[tr].States[i + t + 2].rewards);
            target = target - result * DiscFactor;
            targets.Row(target, t);
           }

そして、割引率を考慮しながら逆順に合計します。

         for(int t = 1; t < PrecoderBars; t++)
           {
            target = targets.Row(t - 1) + targets.Row(t) * MathPow(DiscFactor, t);
            targets.Row(target, t);
           }

得られた行列から、最大の報酬を持つ行を選択します。これが目標になります。

         result = targets.Sum(1);
         ulong row = result.ArgMax();
         target = targets.Row(row);
         bGoal.AssignArray(target);

後続の時間ステップで得られる利益(または損失)は、エージェントが前後のどちらかでおこなった取引に関連付けることができるという観察には、まったく同意します。ここには2つのポイントがあります。

以前におこなわれた取引について言及するのは、完全に正しいとは言えません。というのも、エージェントがそれをそのままにしたというのは、今この瞬間の行動だからです。したがって、その後の結果は、この行動の結果です。

その後の行動については、軌跡分析の枠組みでは個々の行動ではなく、Actor全体の行動方策を分析します。その結果、目標は当面の方策に設定され、個別の行動には設定されません。この観点から、予測期間中の最大目標を設定することは、かなり重要です。

用意された目標を考慮すると、Actorのフィードフォワードパスを実行するのに十分なデータがあります。

         //--- Actor
         if(!Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CBufferFloat*)GetPointer(bGoal)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

次に、予測された行動と、環境との相互作用の過程で実際におこなわれた行動との誤差を最小化するために、モデルのパラメータを調整する必要があります。ここでは、最大偏差に重点を置いて補足した教師あり学習法を用います。上述のアルゴリズムと同様に、まず各パラメータの予測の移動標準偏差の誤差を計算します。

         target.Assign(Buffer[tr].States[i].action);
         target.Clip(0, 1);
         Actor.getResults(result);
         vector<float> error = target - result;
         std_count = MathMin(std_count, 999);
         STD_Actor = MathSqrt((MathPow(STD_Actor, 2) * std_count + MathPow(error, 2)) / (std_count + 1));

次に、現在の誤差を閾値と比較します。バックプロパゲーションパスは、少なくとも1つのパラメータに閾値以上の偏差がある場合にのみ実行されます。

         check = MathAbs(error) - STD_Actor * STD_Multiplier;
         if(check.Max() > 0)
           {
            Result.AssignArray(CAGrad(error) + result);
            if(!Actor.backProp(Result, (CBufferFloat *)GetPointer(bGoal), (CBufferFloat *)GetPointer(bGradient)) ||
               !StateEncoder.backPropGradient(GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient)))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               break;
              }
           }

Actorのパラメータを更新した後、目標設定モデルの訓練に移ります。Actorとは異なり、エンコーダから受け取った分析された状態の凝縮された表現のみを初期データとして使用します。また、フィードフォワードパスを実行する前に追加のデータを準備する必要もありません。

         //--- Goal
         if(!Goal.feedForward((CNet *)GetPointer(Encoder), -1, (CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

モデル訓練の目標値には、Actor方策で設定した目標を使用します。ただし、少し追加することがあります。多くの作業では、訓練された方策の目標を形成する際に、実際に得られた結果に対して増加係数を使用することが推奨されています。これは、より最適な行動を選択するように行動方策を刺激するはずです。より良い結果を出すために、すぐに目標設定モデルを訓練します。そのために、目標値のベクトルを形成する際に、実績値を2倍にします。ただし、以下の点に注意してください。実際の報酬のベクトルを単純に2倍することはできません。受け取る報酬の中にはマイナスの値も含まれている可能性があり、それを2倍することは期待値を悪化させるだけだからです。したがって、まず報酬の符号を決定します。

         target=targets.Row(row);
         result = target / (MathAbs(target) + FLT_EPSILON);

この操作の結果、負の値には-1、正の値には1を含むベクトルが得られると予想されます。2から結果のベクトルの冪乗まで上げると、正の値では2、負の値では1/2となります。

        result = MathPow(vector<float>::Full(NRewards, 2), result);

これで、実績のベクトルに上記で求めた係数のベクトルを掛け合わせれば、期待報酬を2倍にすることができます。これを目標設定モデルの訓練の目標値とします。

         target = target * result;
         Goal.getResults(result);
         error = target - result;
         std_count = MathMin(std_count, 999);
         STD_Goal = MathSqrt((MathPow(STD_Goal, 2) * std_count + MathPow(error, 2)) / (std_count + 1));
         std_count++;
         check = MathAbs(error) - STD_Goal * STD_Multiplier;
         if(check.Max() > 0)
           {
            Result.AssignArray(CAGrad(error) + result);
            if(!Goal.backProp(Result, (CBufferFloat *)NULL, (CBufferFloat *)NULL))
              {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               break;
              }
           }

ここでは、閉形式を使用して最大偏差に重点を置いてモデルを最適化するというアイデアも活用しています。

この段階で、すべての訓練済みモデルのパラメータを最適化しました。エンコーダの結果を適切なバッファに保存します。

         Encoder.getResults(result);
         bLastEncoder.AssignArray(result);

学習過程の進捗状況をユーザーに知らせ、ループの次の反復に移ります。

         //---
         if(GetTickCount() - ticks > 500)
           {
            double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations);
            string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent, Actor.getRecentAverageError());
            str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Goal", percent, Goal.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

モデル訓練ループのすべての反復が完了したら、銘柄チャートのコメントフィールドを消去します。訓練結果をログに出力し、EA操作を完了します。

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

これで、アルゴリズムが使用するプログラムの説明は終わりです。この記事で使用されているすべてのプログラムの完全なコードを添付ファイルでご覧ください。この添付ファイルには、訓練済みモデルをテストするためのEAも含まれていますが、これについては今は触れません。


3.検証

MQL5を使用して目標条件付き予測符号化法を実装するために、かなり多くの作業をおこないました。この記事の大きさが、その仕事量を裏付けています。さて、次はその結果をテストします。

いつものように、EURUSD の H1 の履歴データを使用してモデルを訓練し、テストします。モデルは2023年の最初の7ヶ月間の履歴データで訓練されています。訓練したモデルをテストするために、学習履歴期間の直後である2023年8月からの履歴データを使用します。

訓練は繰り返しおこなわれました。まず、訓練データセットを収集し、これを2段階に分けて収集しました。最初の段階では、Real-ORL法で提案されたように、実シグナルデータに基づくパスを訓練セットに保存しました。次に、訓練データセットにEA「 ...\Experts\GCPC\Research.mq5」とランダム方策を使用したパスを追加しました。

このデータに対して、EA「 ...\Experts\GCPC\StudyEncoder.mq5」を用いてオートエンコーダを訓練しました。上述したように、このEAの訓練の目的上、すべてのパスは同一です。モデルの訓練は、訓練データセットの追加更新を必要としません。そこで、許容できる結果が得られるまで、マスクされたオートエンコーダを訓練します。

第2段階では、エージェントの行動方策と目標設定モデルを訓練します。ここでは、モデルを訓練し、訓練データを更新する反復法を用います。この段階で驚きました。訓練過程は非常に安定しており、結果のダイナミクスも良好であることが判明しました。訓練の過程で、訓練期間とテスト期間の両方で利益を生み出すことができる方策が得られました。


結論

この記事では、かなり興味深い目標条件付き予測符号化法を紹介しました。その主な貢献は、モデル訓練過程を2つの部分過程(軌道学習と個別方策学習)に分割したことです。軌跡を学習する際、観察された傾向を将来の状態に投影する可能性に注意が向けられ、一般的に、意思決定のためにエージェントに送信されるデータの情報量が増加します。

本稿の実践編では、提案手法のビジョンをMQL5を用いて実装し、提案手法の有効性を実際に確認しました。

しかし、この記事で紹介されているプログラムはすべて、技術デモンストレーションを目的としたものであることに、もう一度ご注意ください。これらは実際の金融市場で使用する準備ができていません。


参照文献


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

# ファイル名 種類 詳細
1 Research.mq5 EA コレクションEAの例
2 ResearchRealORL.mq5
EA
Real-ORL法による事例収集のためのEA
3 Study.mq5  EA 方策訓練EA
4 StudyEncoder.mq5 EA
オートエンコーダ訓練EA
5 Test.mq5 EA モデルをテストするEA
6 Trajectory.mqh クラスライブラリ システム状態記述の構造体
7 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
8 NeuroNet.cl コードベース OpenCLプログラムコードライブラリ

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

添付されたファイル |
MQL5.zip (757.07 KB)
母集団最適化アルゴリズム:2進数遺伝的アルゴリズム(BGA)(第1回) 母集団最適化アルゴリズム:2進数遺伝的アルゴリズム(BGA)(第1回)
この記事では、2進数遺伝的アルゴリズムやその他の集団アルゴリズムで使用されるさまざまな手法を探ります。選択、交叉、突然変異といったアルゴリズムの主な構成要素と、それらが最適化に与える影響について見ていきます。さらに、データの表示手法と、それが最適化結果に与える影響についても研究します。
ニューラルネットワークが簡単に(第70回):閉形式方策改善演算子(CFPI) ニューラルネットワークが簡単に(第70回):閉形式方策改善演算子(CFPI)
この記事では、閉形式の方策改善演算子を使用して、オフラインモードでエージェントの行動を最適化するアルゴリズムを紹介します。
リプレイシステムの開発(第38回):道を切り開く(II) リプレイシステムの開発(第38回):道を切り開く(II)
MQL5プログラマーを自認する人の多くは、この記事で概説するような基本的な知識を持っていません。MQL5は多くの人によって限定的なツールだと考えてられていますが、実際の理由は、そのような人たちが必要な知識を持っていないということです。知らないことがあっても恥じることはありません。聞かなかったことを恥じるべきです。MetaTrader 5で指標の複製を強制的に無効にするだけでは、指標とEA間の双方向通信を確保することはできません。まだこれにはほど遠いものの、チャート上でこの指標が重複していないという事実は、私たちに自信を与えてくれます。
初心者からプロまでMQL5をマスターする(第2回):基本的なデータ型と変数の使用 初心者からプロまでMQL5をマスターする(第2回):基本的なデータ型と変数の使用
初心者向け連載の続きです。この記事では、定数や変数を作成する方法、日付や色、その他の便利なデータを書き込む方法を見ていきます。曜日や線のスタイル(実線、点線など)を列挙する方法も学びます。変数と式はプログラミングの基本です。これらは99%のプログラムに間違いなく存在するので、理解することは非常に重要です。したがって、この記事はとてもプログラミング初心者の役に立つでしょう。必要なプログラミング知識レベル:前回の記事(冒頭のリンク参照)の範囲内で、ごく基本的なものです。