English Русский 中文 Español Deutsch Português
preview
ニューラルネットワークが簡単に(第38回):不一致による自己監視型探索

ニューラルネットワークが簡単に(第38回):不一致による自己監視型探索

MetaTrader 5エキスパートアドバイザー | 16 11月 2023, 12:55
348 0
Dmitriy Gizlyk
Dmitriy Gizlyk

はじめに

探索問題は強化学習における大きな障害であり、特にエージェントが報酬を受け取るのが稀で遅延するようなケースでは、効果的な戦略を立てるのが難しくなります。この問題に対する可能な解決策のひとつは、環境モデルに基づいて「内発的」報酬を生成することです。内因性好奇心モジュールを研究する際に、同様のアルゴリズムを見てきました。ただし、作成されたアルゴリズムのほとんどは、コンピュータゲームの文脈でしか研究されていませんが、沈黙のシミュレーション環境以外では、エージェントと環境の相互作用が確率的であるため、予測モデルの訓練は困難です。環境確率の問題を解決するアプローチの中に、Deepak Pathakが彼の論文で提案した不一致による自己監視型探索アルゴリズムがあります。

このアルゴリズムは自己学習法に基づくもので、エージェントは環境との相互作用中に得た情報を用いて「内因性」報酬を生成し、戦略を更新します。このアルゴリズムは、環境と相互作用し、様々な予測を生成する複数のエージェントモデルの使用に基づいています。モデルが一致しない場合、それは「興味深い」出来事とみなされ、エージェントは環境のその空間を探索するインセンティブを与えられます。このようにして、アルゴリズムはエージェントが環境の新しい領域を探索するインセンティブを与え、将来の報酬についてより正確な予測をすることを可能にします。


1.不一致による探索のアルゴリズム

不一致に基づく探索は、エージェントが外部報酬に依存することなく、むしろモデルのアンサンブルを使用して新しい未探索の領域を見つけることによって、環境を探索することを可能にする強化学習手法です。

論文「不一致による自己監視型探索」の中で、著者らはこのアプローチについて説明し、単純な方法を提案しています。すなわち、フォワードダイナミクスモデルのアンサンブルを訓練し、アンサンブル内のモデルの予測間の矛盾や分散が最大となる行動空間を探索するようエージェントに促すというものです。

このように、エージェントは最大の期待報酬を生む行動を選択するのではなく、アンサンブル内のモデル間の不一致を最大化する行動を選択します。これによってエージェントは、アンサンブル内のモデルが不一致である状態空間の領域を探索することができ、環境に新しい未探索の領域が存在する可能性が高くなります。

この場合、アンサンブル内のすべてのモデルは平均に収束し、最終的にアンサンブルの広がりが小さくなり、エージェントは環境の状態と行動の可能な結果について、より正確な予測を得ることができます。

さらに、不一致を介した探索アルゴリズムにより、エージェントは環境との相互作用の確率性にうまく対処することができます。論文の著者がおこなった実験の結果、提案されたアプローチは確率的環境における探索を実際に改善し、内因性動機づけや不確実性モデリングの既存の手法を凌駕することが示されました。さらに、彼らのアプローチは、サンプルの値は真実のラベルではなく、モデルのアンサンブルの状態に基づいて決定される教師あり学習にも拡張可能です。

このように、不一致による探索アルゴリズムは、確率的環境における探索問題を解決する有望なアプローチです。これにより、エージェントは外部からの報酬に頼ることなく、より効率的に環境を探索することができます。これは、外部からの報酬が限られていたり、コストが高かったりする実世界への応用において特に有用です。

さらに、このアルゴリズムは、モデルの不確実性を測定し、最大化することが特に困難な、画像のような高次元データの作業を含む、様々な文脈に適用することができます。

この論文の著者らは、ロボット制御、アタリゲーム、迷路ナビゲーション課題など、いくつかの問題で提案アルゴリズムの有効性を実証しました。研究の結果、不一致による探索アルゴリズムは、速度、収束性、学習の質において、他の探索手法を凌駕することが示されました。

このように、不一致を介した探索へのこのアプローチは、強化学習の分野における重要な一歩であり、エージェントがより良く、より効率的に環境を探索し、様々なタスクでより良い結果を達成するのを助けることができます。

提案されたアルゴリズムを考えてみましょう。

環境との相互作用の過程で、エージェントは現在の状態Xtを評価し、その内部方策に導かれて、何らかの行動Atを実行します。その結果、環境の状態は新しい状態Xt+1に変化します。このようなデータのセットは経験再生バッファに保存され、将来の環境状態を予測する動的モデルのアンサンブルを訓練するために使用されます。

初期段階で将来の環境状態の独立した評価を維持するために、アンサンブル内のすべての動的モデルの重み行列はランダムな値で満たされます。学習プロセスでは、各モデルが経験リプレイバッファから独自のランダムな学習データセットを受け取ります。

アンサンブルの各モデルは、実際の環境の次の状態を予測するように訓練されています。エージェントによって十分に探索された状態空間の一部には、すべてのモデルを訓練するのに十分なデータが収集されており、その結果、モデル間の一貫性が保たれます。モデルが訓練されるにつれて、この特徴は、状態空間の見慣れないが類似した部分に汎化するはずです。しかし、新しく未開拓の領域は、どのモデルもまだそのような例で訓練されていないため、どのモデルでも予測誤差が大きくなります。その結果、次の状態の予測に不一致が生じます。従って、ここでは、この不一致を、政策の方向性に対する内因性報酬として利用します。具体的には、内因性報酬Riはアンサンブル内の異なるモデルの出力の分散として定義されます。

上記の式では、内因性報酬はシステムの将来の状態には依存しないことに注意してください。このプロパティは、後でこのメソッドを実装するときに使用します。

確率的シナリオの場合、十分な数のサンプルが与えられれば、動的予測モデルは確率的サンプルの平均を予測するように学習しなければなりません。こうすることで、アンサンブル内の出力の分散が減少し、エージェントが研究の確率的最小値にはまり込むのを防ぐことができます。これは予測誤差に基づくターゲットとは異なることに注意してください。それは、十分なサンプルの後、平均値に落ち着きます。平均値は個々の真のランダムな状態とは異なり、予測誤差は高いままであるため、エージェントは常に確率的な振る舞いに興味を持ちます。

提案アルゴリズムを使用する場合、エージェントと環境との相互作用の各ステップは、環境から受け取った報酬に関する情報だけでなく、行動を実行する際に環境の状態がどのように変化するかについてのエージェントの内部モデルを更新するのに必要な情報も提供します。これによりエージェントは、明確な外部報酬がない場合でも、環境に関する貴重な情報を引き出すことができます。

原著論文のモデル発表

内因性報酬Riは、エージェントの方策を訓練するために使用され、アンサンブル内の異なるモデルの出力の分散として計算されます。モデルの出力の不一致が大きいほど、内因性報酬の価値は高くなります。これによりエージェントは、次の状態の予測が不確かな状態空間の新しい領域を探索し、このデータに基づいてより良い意思決定を行うように学習することができます。

エージェントは、環境との相互作用の過程で収集したデータを用いてオンラインで訓練されます。同時に、モデルのアンサンブルは、エージェントが環境と相互作用するたびに更新されるため、エージェントは各ステップで環境に関する内部モデルを更新し、将来の環境状態のより正確な予測を得ることができます。

2.MQL5を使用した実装

実装では、提案されたアルゴリズムを完全に繰り返すのではなく、その主要なアイデアのみを使用し、タスクに合わせて調整します。

まず最初におこなうのは、内因性好奇心モデルと同様に、圧縮された(隠れた)システムの状態を予測するよう、動的モデルのアンサンブルに依頼することです。これにより、動的モデルとアンサンブル全体のサイズを圧縮することができます。

2つ目のポイントは、内因性報酬を決定するためには、システムの真の状態を知る必要はなく、ダイナミックアンサンブルモデルの予測値を知る必要があるということです。これにより、予測報酬をその後の学習を促すだけでなく、リアルタイムの行動決定にも使うことができます。エージェントの方策を訓練する際に、内因性な要素を導入することで外部報酬を歪めるのではなく、外部報酬を最大化するための方策を即座に構築できるようにします。これがここでの主な目標です。

しかし、学習過程における環境の学習を最大化するために、エージェントの行動を選択する際に、各エージェントの可能な行動に対する動的モデルの予測値の不一致の分散を予測報酬に加えます。

各行動の後の予測状態を並行して計算するために、動的モデルに、現在の状態に基づいてエージェントの各行動の可能性の予測を与えるように依頼し、可能性のある行動の数に応じて各モデルの結果層のサイズを増やします。

さて、主な作業の方向性を定義したところで、アルゴリズムの実装に移りましょう。最初の問題は、動的モデルのアンサンブルをどのように実装するかです。以前作ったモデルはすべて線形でした。並列コンピューティングは、OpenCLツールを使って、1つのサブプロセスと1つのニューラル層内で構成することができます。現在のところ、複数のモデルを並列計算することはできません。複数のモデルに対して一連の計算をおこなうことは、モデルの訓練にかかる時間の大幅な増加につながります。

この問題を解決するために、Multi-Head Attentionで使った並列計算の整理法を使うことにしました。その時、私たちはすべてのアテンションヘッドからのデータを1つのテンソルにまとめ、OpenCLのタスク空間レベルで分割しました。

このような問題を解決するために、ライブラリ全体を作り直すつもりはありません。この段階では、将来のシステム状態の予測値の特定の精度は重要ではありません。モデルのアンサンブルの相対的な同期作業があれば十分でしょう。したがって、動的予測モデルでは完全連結層を使用します。

まず、この機能を整理するためにOpenCLプログラムカーネルを作成します。フィードフォワードカーネルFeedForwardMultiModelsは、基となる完全連結層の同様のカーネルとほとんど同じですが、若干の違いはあります。

カーネルパラメータには変更はありません。3つのデータバッファ(重み行列、ソースデータ、結果テンソル)と2つの定数(ソースデータ層のサイズと活性化関数)です。ただし、以前は、前の層のフルサイズをソースデータ層のサイズとして指定していたのを、現在のモデルの要素数を受け取るようにします。

__kernel void FeedForwardMultiModels(__global float *matrix_w,
                                     __global float *matrix_i,
                                     __global float *matrix_o,
                                     int inputs,
                                     int activation
                                    )
  {
   int i = get_global_id(0);
   int outputs = get_global_size(0);
   int m = get_global_id(1);
   int models = get_global_size(1);

カーネル本体では、まず現在のスレッドを特定します。ここで、現在のモデルを識別する、問題空間の2つ目の次元が出現していることにお気づきでしょう。問題全体の次元がアンサンブルの大きさを示します。

次に、必要なローカル変数を宣言し、計算対象のニューロンとアンサンブルの現在のモデルを考慮して、データバッファのオフセットを定義します。

   float sum = 0;
   float4 inp, weight;
   int shift = (inputs + 1) * (i + outputs * m);
   int shift_in = inputs * m;
   int shift_out = outputs * m;

ニューロンの状態や活性化関数を計算する実際の数学的な部分に変更はありませんでした。データバッファのオフセット調整のみを追加しました。

   for(int k = 0; k <= inputs; k = k + 4)
     {
      switch(inputs - k)
        {
         case 0:
            inp = (float4)(1, 0, 0, 0);
            weight = (float4)(matrix_w[shift + k], 0, 0, 0);
            break;
         case 1:
            inp = (float4)(matrix_i[shift_in + k], 1, 0, 0);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], 0, 0);
            break;
         case 2:
            inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + 1], 1, 0);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], 0);
            break;
         case 3:
            inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + 1], matrix_i[shift_in + k + 2], 1);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], matrix_w[shift + k + 3]);
            break;
         default:
            inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + 1], matrix_i[shift_in + k + 2],
                                                                                                  matrix_i[shift_in + k + 3]);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], matrix_w[shift + k + 3]);
            break;
        }
      float d = dot(inp, weight);
      if(isnan(sum + d))
         continue;
      sum += d;
     }

パラメータで指定された活性化関数の値が計算されると、その結果がmatrix_oデータバッファに保存されます。

   if(isnan(sum))
      sum = 0;
   switch(activation)
     {
      case 0:
         sum = tanh(sum);
         break;
      case 1:
         sum = 1 / (1 + exp(-sum));
         break;
      case 2:
         if(sum < 0)
            sum *= 0.01f;
         break;
      default:
         break;
     }
   matrix_o[shift_out + i] = sum;
  }

この解決策によって、アンサンブル内のすべてのモデルの1つの層の値を、1つのカーネルで並行して計算することができます。もちろん、これには限界があります。ここでは、アンサンブル内のすべてのモデルのアーキテクチャは同一であり、違いは重み付け係数のみです。

リバースパスの状況は少し違います。このアルゴリズムは、アンサンブルの動的モデルを異なる訓練データセットで訓練することを可能にします。モデルごとに別々の訓練パッケージを作成することはありません。その代わり、各バックワードパスでは、アンサンブルからランダムに選ばれたモデルを1つだけ訓練します。他のモデルでは、ゼロ勾配を前の層に渡します。これらは、CalcHiddenGradientMultiModels層内の勾配分布カーネルアルゴリズムに加える変更です。

ベースとなる完全連結層の同様のカーネルは、4つのデータバッファと2つの変数へのポインタをパラメータとして受け取ります。これは、重み行列のテンソルと、活性化関数の微分を計算するための前の層の結果のテンソルです。また、現在のニューラル層と前のニューラル層の2つのグラデーションバッファがあります。1番目は受信した誤差勾配を格納し、2番目はカーネルの結果を記録し、誤差勾配を前のニューラル層に転送するために使用されます。変数には、現在の層のニューロン数と前の層の活性化関数を示します。指定されたパラメータに、メインプログラムの側でランダムに選択される訓練済みモデルの識別子を追加します。

__kernel void CalcHiddenGradientMultiModels(__global float *matrix_w,
                                            __global float *matrix_g,
                                            __global float *matrix_o,
                                            __global float *matrix_ig,
                                            int outputs,
                                            int activation,
                                            int model
                                           )
  {
   

カーネル本体では、まずスレッドを特定します。フィードフォワードカーネルと同様に、2次元の問題空間を使用します。1つ目の次元では、1つのモデル内の流れを識別し、2つ目の次元では、アンサンブル内のモデルを示します。誤差勾配を収集するために、前の層のニューロンのコンテキストでカーネルを実行します。各スレッドは、1つのニューロン上の全方向からの誤差勾配を収集します。

   int i = get_global_id(0);
   int inputs = get_global_size(0);
   int m = get_global_id(1);
   int models = get_global_size(1);

1つのモデルだけに勾配を分散させますが、アンサンブル全体に対してスレッドを起動させることに注意してください。これは、他のモデルの誤差勾配をリセットする必要があるためです。次のステップでは、特定のモデルについて勾配を更新する必要があるかどうかを確認します。勾配をリセットするだけの場合、この関数だけを実行し、不要な処理をおこなわずにカーネルを終了します。

//---
   int shift_in = inputs * m;
   if(model >= 0 && model != m)
     {
      matrix_ig[shift_in + i] = 0;
      return;
     }

ここでは、将来的な可能性のために小さな抜け穴を残しておきます。更新するモデル番号に負の数を指定すると、アンサンブル内のすべてのモデルに対して勾配が計算されます。

次に、ローカル変数を宣言し、データバッファのオフセットを定義します。

//---
   int shift_out = outputs * m;
   int shift_w = (inputs + 1) * outputs * m;
   float sum = 0;
   float out = matrix_o[shift_in + i];
   float4 grad, weight;

これに続くのが誤差勾配分布の数学的部分で、基本的な完全連結したニューロンの同様の機能を完全に繰り返します。もちろん、データバッファには必要なオフセットを追加します。演算結果は前の層のグラデーションバッファに保存されます。

   for(int k = 0; k < outputs; k += 4)
     {
      switch(outputs - k)
        {
         case 1:
            weight = (float4)(matrix_w[shift_w + k * (inputs + 1) + i], 0, 0, 0);
            grad = (float4)(matrix_g[shift_out + k], 0, 0, 0);
            break;
         case 2:
            grad = (float4)(matrix_g[shift_out + k], matrix_g[shift_out + k + 1], 0, 0);
            weight = (float4)(matrix_w[shift_w + k * (inputs + 1) + i], matrix_w[shift_w + (k + 1) * (inputs + 1) + i], 0, 0);
            break;
         case 3:
            grad = (float4)(matrix_g[shift_out + k], matrix_g[shift_out + k + 1], matrix_g[shift_out + k + 2], 0);
            weight = (float4)(matrix_w[shift_w + k * (inputs + 1) + i], matrix_w[shift_w + (k + 1) * (inputs + 1) + i],
                                                                           matrix_w[shift_w + (k + 2) * (inputs + 1) + i], 0);
            break;
         default:
            grad = (float4)(matrix_g[shift_out + k], matrix_g[shift_out + k + 1], matrix_g[shift_out + k + 2], 
                                                                                                 matrix_g[shift_out + k + 3]);
            weight = (float4)(matrix_w[shift_w + k * (inputs + 1) + i], matrix_w[shift_w + (k + 1) * (inputs + 1) + i], 
                              matrix_w[shift_w + (k + 2) * (inputs + 1) + i], matrix_w[shift_w + (k + 3) * (inputs + 1) + i]);
            break;
        }
      sum += dot(grad, weight);
     }
   if(isnan(sum))
      sum = 0;
   switch(activation)
     {
      case 0:
         out = clamp(out, -1.0f, 1.0f);
         sum = clamp(sum + out, -1.0f, 1.0f) - out;
         sum = sum * max(1 - pow(out, 2), 1.0e-4f);
         break;
      case 1:
         out = clamp(out, 0.0f, 1.0f);
         sum = clamp(sum + out, 0.0f, 1.0f) - out;
         sum = sum * max(out * (1 - out), 1.0e-4f);
         break;
      case 2:
         if(out < 0)
            sum *= 0.01f;
         break;
      default:
         break;
     }
   matrix_ig[shift_in + i] = sum;
  }

次に、重み行列の更新カーネル UpdateWeightsAdamMultiModelsを修正する必要があります。誤差勾配分布カーネルと同様に、ベースとなる完全連結層の既存のカーネルパラメータにモデル識別子を追加します。

基となるニューラルネットワークの同様のカーネルが、すでに2次元のタスク空間で動いていることにご注目ください。同時に、更新しないモデルに対して操作をおこなう必要もありません。したがって、1つのモデルに対してのみカーネルを呼び出し、モデル識別子パラメータを使ってデータバッファのオフセットを決定します。それ以外は、カーネルアルゴリズムに変更はありません。アルゴリズム全体は添付ファイルにあります。

これでOpenCL側の作業は完了です。次に、MQL5ライブラリのコードで作業します。ここでは、基本クラスCNeuronBaseOCLの子孫として新しいクラスCNeuronMultiModelを作成します。

クラスメソッドのセットは非常に標準的で、クラスの初期化、ファイル操作、フィードフォワードとバックプロパゲーションパスのメソッドが含まれています。また、2つの新しい変数を導入し、アンサンブルのモデル数と訓練するモデルの識別子を記録します。後者はリターンパスごとに変化します。

class CNeuronMultiModel : public CNeuronBaseOCL
  {
protected:
   int               iModels;
   int               iUpdateModel;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL); 
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronMultiModel(void){};
                    ~CNeuronMultiModel(void){};
   virtual bool      Init(uint numInputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, 
                                            ENUM_OPTIMIZATION optimization_type, int models);
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) {  activation = value;         }    
   //---
   virtual bool      calcHiddenGradients(CNeuronBaseOCL *NeuronOCL);   
   //---
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual int       Type(void)        const                      {  return defNeuronMultiModels; }
  };

クラスでは新しい内部オブジェクトを作らないので、コンストラクタとデストラクタは空のままです。Initクラスの初期化メソッドでメソッドを作成する作業を始めましょう。メソッドは次のパラメータを受け取ります。

  • numInputs:1つのモデルの前の層のニューロン数
  • open_cl:OpenCL オブジェクトへのポインタ
  • numNeurons:1つのモデルの層のニューロン数
  • models:アンサンブル内のモデル数

bool CNeuronMultiModel::Init(uint numInputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, 
                             ENUM_OPTIMIZATION optimization_type, int models)
  {
   if(CheckPointer(open_cl) == POINTER_INVALID || numNeurons <= 0  || models <= 0)
      return false;

メソッド本体では、OpenCLオブジェクトへのポインタが適切かどうか、層とアンサンブルの寸法が正しく指定されているかどうかを即座に確認します。その後、必要な定数を内部変数に保存します。

   OpenCL = open_cl;
   optimization = ADAM;
   iBatch = 1;
   iModels = models;

なお、重み行列の更新カーネルは、アダム法のみに対して作成しました。したがって、パラメータがどうであれ、モデルを最適化するためにこの方法を指定します。

この後、ニューラル層の結果と誤差勾配を記録するためのバッファを作成します。すべてのバッファのサイズは、アンサンブルのモデル数に比例して増加することにご注目ください。初期段階では、バッファはゼロ値で初期化されます。 

//---
   if(CheckPointer(Output) == POINTER_INVALID)
     {
      Output = new CBufferFloat();
      if(CheckPointer(Output) == POINTER_INVALID)
         return false;
     }
   if(!Output.BufferInit(numNeurons * models, 0.0))
      return false;
   if(!Output.BufferCreate(OpenCL))
      return false;
//---
   if(CheckPointer(Gradient) == POINTER_INVALID)
     {
      Gradient = new CBufferFloat();
      if(CheckPointer(Gradient) == POINTER_INVALID)
         return false;
     }
   if(!Gradient.BufferInit((numNeurons + 1)*models, 0.0))
      return false;
   if(!Gradient.BufferCreate(OpenCL))
      return false;

次に、重み行列バッファをランダムな値で初期化します。バッファのサイズは、現在のニューラル層内のすべてのアンサンブルモデルの重みを保存するのに十分な大きさでなければなりません。

//---
   if(CheckPointer(Weights) == POINTER_INVALID)
     {
      Weights = new CBufferFloat();
      if(CheckPointer(Weights) == POINTER_INVALID)
         return false;
     }
   int count = (int)((numInputs + 1) * numNeurons * models);
   if(!Weights.Reserve(count))
      return false;
   float k = (float)(1 / sqrt(numInputs + 1));
   for(int i = 0; i < count; i++)
     {
      if(!Weights.Add((2 * GenerateWeight()*k - k)*WeightsMultiplier))
         return false;
     }
   if(!Weights.BufferCreate(OpenCL))
      return false;

アダム最適化法を実行するには、モーメント1と2を記録するために2つのデータバッファを作成する必要があります。指定されたバッファのサイズは、重み行列のサイズと同様です。初期段階では、これらのバッファをゼロ値で初期化します。

//---
   if(CheckPointer(DeltaWeights) != POINTER_INVALID)
      delete DeltaWeights;
//---
   if(CheckPointer(FirstMomentum) == POINTER_INVALID)
     {
      FirstMomentum = new CBufferFloat();
      if(CheckPointer(FirstMomentum) == POINTER_INVALID)
         return false;
     }
   if(!FirstMomentum.BufferInit(count, 0))
      return false;
   if(!FirstMomentum.BufferCreate(OpenCL))
      return false;
//---
   if(CheckPointer(SecondMomentum) == POINTER_INVALID)
     {
      SecondMomentum = new CBufferFloat();
      if(CheckPointer(SecondMomentum) == POINTER_INVALID)
         return false;
     }
   if(!SecondMomentum.BufferInit(count, 0))
      return false;
   if(!SecondMomentum.BufferCreate(OpenCL))
      return false;
//---
   return true;
  }

各段階での作動プロセスの監視を忘れないでください。以上の作業がすべて成功したら、このメソッドは完了です。

初期化の後、feedForwardメソッドに移ります。パラメータでは、このメソッドは前のニューラル層のオブジェクトへのポインタだけを受け取ります。そしてメソッド本体では、受け取ったポインタの妥当性を即座に確認します。

bool CNeuronMultiModel::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(OpenCL) == POINTER_INVALID || CheckPointer(NeuronOCL) == POINTER_INVALID)
      return false;

ニューラル層アルゴリズムが提供するすべてのフィードフォワード演算を実行するために、すでにOpenCLプログラムでカーネルを作成しています。あとは、必要なデータをカーネルに転送し、その実行を呼び出します。

まず、問題空間を定義します。これまでは、2次元の問題空間を使うことにしていました。最初の次元では、1つのモデルの出力におけるニューロンの数を示し、2番目の次元では、そのようなモデルの数を指定します。クラスを初期化する際、1つのモデルの層のニューロン数を保存しませんでした。したがって、問題空間の最初の次元の大きさを決定するために、層の出力のニューロン総数をアンサンブルのモデル数で割ります。2つ目の次元は簡単です。ここでは、アンサンブルのモデル数を別の変数にしています。

   uint global_work_offset[2] = {0, 0};
   uint global_work_size[2];
   global_work_size[0] = Output.Total() / iModels;
   global_work_size[1] = iModels;

タスク空間を定義した後、必要な初期データをカーネルパラメータに渡します。操作実行結果を必ず確認します。

   if(!OpenCL.SetArgumentBuffer(def_k_FFMultiModels, def_k_ff_matrix_w, getWeightsIndex()))
     {
      printf("Error of set parameter kernel FeedForward: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_FFMultiModels, def_k_ff_matrix_i, NeuronOCL.getOutputIndex()))
     {
      printf("Error of set parameter kernel FeedForward: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_FFMultiModels, def_k_ff_matrix_o, Output.GetIndex()))
     {
      printf("Error of set parameter kernel FeedForward: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_FFMultiModels, def_k_ff_inputs, NeuronOCL.Neurons() / iModels))
     {
      printf("Error of set parameter kernel FeedForward: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_FFMultiModels, def_k_ff_activation, (int)activation))
     {
      printf("Error of set parameter kernel FeedForward: %d; line %d", GetLastError(), __LINE__);
      return false;
     }

カーネルの指定には、新しく作成したカーネルのIDを使っていることにご注目ください。パラメータを指定するには、基となる完全連結層の対応するカーネルの識別子を使用します。これは、すべてのカーネルパラメータとそのシーケンスを保存することで可能となります。

すべてのパラメータを渡したら、あとはカーネルを実行キューに送るだけです。

   if(!OpenCL.Execute(def_k_FFMultiModels, 2, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel FeedForward: %d", GetLastError());
      return false;
     }
//---
   return true;
  }

すべての操作の結果を確認し、メソッドを終了します。

次に、バックプロパゲーションメソッドでの作業に移ります。まず、誤差勾配分布メソッドcalcHiddenGradientsを見てみましょう。ダイレクトパスと同様、メソッドのパラメータには、前のニューラル層のオブジェクトへのポインタを受け取ります。メソッドの本体では即座に、受け取ったポインタの妥当性を確認します。

bool CNeuronMultiModel::calcHiddenGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(OpenCL) == POINTER_INVALID || CheckPointer(NeuronOCL) == POINTER_INVALID)
      return false;

次のステップは、問題空間を定義することです。ここではすべてがフィードフォワード方式に似ています。

   uint global_work_offset[2] = {0, 0};
   uint global_work_size[2];
   global_work_size[0] = NeuronOCL.Neurons() / iModels;
   global_work_size[1] = iModels;

次に、初期データをカーネルパラメータに渡します。

   if(!OpenCL.SetArgumentBuffer(def_k_HGMultiModels, def_k_chg_matrix_w, getWeightsIndex()))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_HGMultiModels, def_k_chg_matrix_g, getGradientIndex()))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_HGMultiModels, def_k_chg_matrix_o, NeuronOCL.getOutputIndex()))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_HGMultiModels, def_k_chg_matrix_ig, NeuronOCL.getGradientIndex()))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_HGMultiModels, def_k_chg_outputs, Neurons() / iModels))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_HGMultiModels, def_k_chg_activation, NeuronOCL.Activation()))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }

おわかりのように、これはOpenCLプログラムカーネルの作業を整理するためのかなり標準的なアルゴリズムで、すでに何度も実装しています。ただし、訓練のためにモデル識別子を渡すことには微妙なニュアンスがあります。訓練のためにランダムなモデル番号を選ばなければなりません。そのために、擬似乱数生成器を使います。ただし、次のステップで重み行列を更新しなければならないのは、このモデルのためであることを忘れてはなりません。したがって、結果のランダムなモデル識別子を、先に作成したiUpdateModel変数に保存します。重み行列を更新するときに、その値を使うことができます。

   iUpdateModel = (int)MathRound(MathRand() / 32767.0 * (iModels - 1));
   if(!OpenCL.SetArgument(def_k_HGMultiModels, def_k_chg_model, iUpdateModel))
     {
      printf("Error of set parameter kernel calcHiddenGradients: %d; line %d", GetLastError(), __LINE__);
      return false;
     }

すべてのパラメータをうまく渡したら、カーネルを実行キューに送り、メソッドを終了します。

   if(!OpenCL.Execute(def_k_HGMultiModels, 2, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel CalcHiddenGradient: %d", GetLastError());
      return false;
     }
//---
   return true;
  }

重み行列の更新アルゴリズムは、カーネルの準備とキューイングのステップを完全に繰り返すもので、落とし穴はないので、ここでは詳しい説明は省略します。完全なEAコードは添付ファイルにあります。

ファイルを扱うには、SaveメソッドとLoadメソッドを使います。これらのアルゴリズムは非常に単純です。新しいクラスでは、アンサンブルのモデル数と学習済みモデルの識別子の2つの変数だけを作成します。最初の変数だけが、保存する必要のあるハイパーパラメータを含んでいます。継承されたすべてのオブジェクトと変数を保存するプロセスは、親クラスのメソッドの中ですでに整理されています。このクラスは必要なコントロールも提供します。したがって、データを保存するには、まず親クラスの同様のメソッドを呼び出して、1つのハイパーパラメータの値だけを保存すればよくなります。

bool CNeuronMultiModel::Save(const int file_handle)
  {
   if(!CNeuronBaseOCL::Save(file_handle))
      return false;
   if(FileWriteInteger(file_handle, iModels) <= 0)
      return false;
//---
   return true;
  }

ファイルからのデータ読み込みも同様におこなわれます。

これで、新しいクラスのコードを使った作業は完了です。すべてのメソッドの完全なコードは添付ファイルにあります。

このクラスを使う前に、ライブラリのコードでさらにいくつかのアクションを実行する必要があります。まず最初に、カーネルと追加されたパラメータを識別するための定数を作成する必要があります。

#define def_k_FFMultiModels             46 ///< Index of the kernel of the multi-models neuron to calculate feed forward
#define def_k_HGMultiModels             47 ///< Index of the kernel of the multi-models neuron to calculate hiden gradient
#define def_k_chg_model                 6  ///< Number of model to calculate
#define def_k_UWMultiModels             48 ///< Index of the kernel of the multi-models neuron to update weights
#define def_k_uwa_model                 9  ///< Number of model to update

そして、次を追加します。

  • CNet::Createメソッドで新しい型のニューラル層を作成するためのブロック
  • CLayer::CreateElementメソッドでの新しい層の型
  • ニューラルネットワーク基本クラスのフィードフォワードディスパッチメソッドでの新しい型
  • バックプロパゲーションのディスパッチメソッド CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject) での新しい型

複数の独立した完全連結層を並列に操作するクラスを構築し、モデルのアンサンブルを作成できるようにしましたが、これはほんの一部分であり、不一致を通じた研究のアルゴリズム全体ではありません。完全なアルゴリズムを実装するために、内因性好奇心モジュールと同様に、CEVDモデルの新しいクラスを作成します。クラスの構造には多くの共通点があります。これは、メソッド名や変数名に見ることができます。経験リプレイバッファCReplayBufferがあります。2つの内部モデルcTargetNetとcForwardNetがありますが、逆モデルはありません。cForwardNetとして、モデルのアンサンブルを使用します。いつものように、違いは細部にあります。

//+------------------------------------------------------------------+
//| Exploration via Disagreement                                     |
//+------------------------------------------------------------------+
class CEVD : protected CNet
  {
protected:
   uint              iMinBufferSize;
   uint              iStateEmbedingLayer;
   double            dPrevBalance;
   bool              bUseTargetNet;
   bool              bTrainMode;
   //---
   CNet              cTargetNet;
   CReplayBuffer     cReplay;
   CNet              cForwardNet;

   virtual bool      AddInputData(CArrayFloat *inputVals);

public:
                     CEVD();
                     CEVD(CArrayObj *Description, CArrayObj *Forward);
   bool              Create(CArrayObj *Description, CArrayObj *Forward);
                    ~CEVD();
   int               feedForward(CArrayFloat *inputVals, int window = 1, bool tem = true);
   bool              backProp(int batch, float discount = 0.999f);
   int               getAction(int state_size = 0);    
   float             getRecentAverageError() { return recentAverageError; }
   bool              Save(string file_name, bool common = true);
   bool              Save(string dqn, string forward, bool common = true);
   virtual bool      Load(string file_name, bool common = true);
   bool              Load(string dqn, string forward, uint state_layer, bool common = true);
   //---
   virtual int       Type(void)   const   {  return defEVD;   }
   virtual bool      TrainMode(bool flag) { bTrainMode = flag; return (CNet::TrainMode(flag) && cForwardNet.TrainMode(flag));}
   virtual bool      GetLayerOutput(uint layer, CBufferFloat *&result)
     { return        CNet::GetLayerOutput(layer, result); }
   //---
   virtual void      SetStateEmbedingLayer(uint layer) { iStateEmbedingLayer = layer; }
   virtual void      SetBufferSize(uint min, uint max);
  };

bTrainMode変数を追加し、アルゴリズムを操作と訓練のプロセスに分けます。各モデル更新パッケージの前にcTargetNetを常に更新する必要がなくなったので、bUseTargetNetフラグを追加します。また、メソッドのアルゴリズムにも変更を加えました。まず必要なことから始めていきます。

フィードフォワードメソッドとエージェント行動決定メソッドで、アルゴリズムが実行と訓練のプロセスに分割されました。これは、訓練中、エージェントに可能な限り環境を探索させたいからです。逆に、実行中は不必要なリスクを排除し、内部の方策だけに従いたいのです。これがどのように実装されるか見てみましょう。

フィードフォワードメソッドの始まりは、対応する内因性好奇心ブロックメソッドの始まりを繰り返します。パラメータでは、システムの初期状態を受け取ります。口座の状態や未決済のポジションに関するデータを補足します。次に、訓練済みモデルのフィードフォワードメソッドを呼び出します。

int CEVD::feedForward(CArrayFloat *inputVals, int window = 1, bool tem = true)
  {
   if(!AddInputData(inputVals))
      return -1;
//---
   if(!CNet::feedForward(inputVals, window, tem))
      return -1;

ただし、その後、行動選択アルゴリズムは訓練と実行の2つの流れに分かれます。訓練モードでは、訓練済みモデルから環境の隠れた(圧縮された)状態を読み取り、動的モデルのアンサンブルをフィードフォワードで通過させます。内部好奇心モジュールとは異なり、ある特定の行動ではなく、可能な行動範囲全体について一度に状態予報を見るということを思い出してください。そして、アンサンブルのフォワードパスが成功した後にのみ、最適な行動を決定するメソッドを呼び出します。この方法については、後で少し説明します。

   int action = -1;
   if(bTrainMode)
     {
      CBufferFloat *state;
      //if(!GetLayerOutput(1, state))
      //   return -1;
      if(!GetLayerOutput(iStateEmbedingLayer, state))
         return -1;
      if(!cForwardNet.feedForward(state, 1, false))
        {
         delete state;
         return -1;
        }
      double balance = AccountInfoDouble(ACCOUNT_BALANCE);
      double reward = (dPrevBalance == 0 ? 0 : balance - dPrevBalance);
      dPrevBalance = balance;
      action = getAction(state.Total());
      delete state;
      if(action < 0 || action > 3)
         return -1;
      if(!cReplay.AddState(inputVals, action, reward))
         return -1;
     }

行動が正常に定義されたら、そのステートセットを経験リプレイバッファに追加します。

実行モードでは、無駄な行動はおこなわず、エージェントの内因性方策に基づいて最適な行動を決定し、メソッドを完了させます。

   else
      action = getAction();
//---
   return action;
  }

最適な行動を決定するアルゴリズムも、訓練と実行の2つのブランチに分かれています。

int CEVD::getAction(int state_size = 0)
  {
   CBufferFloat *temp;
//--- get the result of the trained model.
   CNet::getResults(temp);
   if(!temp)
      return -1;

メソッドの最初に、訓練済みモデルのフォワードパスの結果を読み込みます。そして、モデルの訓練のために、この値を、可能性のある行動ごとに動的モデルのアンサンブルによる予測の分散の値で調整します。そのために、まずアンサンブルの結果をベクトルにアップロードし、そのベクトルを行列に変換します。結果の行列では、各行が個別の行動に対する予測システム状態を表します。ここでの行列は、すべてのアンサンブルモデルからの予測値を含んでいます。結果を処理する便宜上、行列を水平方向に分割し、より小さなサイズの複数の等しい行列にします。このような行列の数は、アンサンブルのモデル数に等しくなります。このような行列はそれぞれ、エージェントの可能な行動範囲に対応する行の次元を持ちます。

ここで行列演算を使い、まず個々の状態成分の個々の行動の平均の行列を求めることができます。そして、予測行列の平均からの偏差の分散を計算することができます。各行動の平均分散を学習済みモデルの予測報酬値に加算します。この時点で、探索と搾取のバランスを取るためのファクターを使うことができます。環境の探索を最大化するためには、期待報酬に注目せず、予測値の分散だけを利用すればよくなります。こうすることで、エージェントの方策に影響を与えることなく、環境から可能な限り多くのことを学習するよう、モデルにインセンティブを与えます。

//--- in training mode, make allowances for "curiosity"
   if(bTrainMode && state_size > 0)
     {
      vector<float> model;
      matrix<float> forward;
      cForwardNet.getResults(model);
      forward.Init(1, model.Size());
      forward.Row(model, 0);
      temp.GetData(model);
      //---
      int actions = (int)model.Size();
      forward.Reshape(forward.Cols() / state_size, state_size);
      matrix<float> ensemble[];
      if(!forward.Hsplit(forward.Rows() / actions, ensemble))
         return -1;
      matrix<float> means = ensemble[0];
      int total = ArraySize(ensemble);
      for(int i = 1; i < total; i++)
         means += ensemble[i];
      means = means / total;
      for(int i = 0; i < total; i++)
         ensemble[i] -= means;
      means = MathPow(ensemble[0], 2.0);
      for(int i = 1 ; i < total; i++)
         means += MathPow(ensemble[i], 2.0);
      model += means.Sum(1) / total;
      temp.AssignArray(model);
     }

モデルの実行中は、調整は行わず、期待報酬の最大化という原則に基づいて最適な行動を決定します。

//---
   return temp.Argmax();
  }

メソッドの完全なコードは、以下の添付ファイルに記載されています。

リバースパスの方法についてもう少し詳しく説明します。モデル動作中の不必要な反復を排除するため、モデル訓練フラグがない場合、バックワードパスメソッドは即座にその作業を完了します。これにより、EAコードを変更することなく、モデルの訓練モードからテストモードに素早く切り替えることができます。

bool CEVD::backProp(int batch, float discount = 0.999000f)
  {
//---
   if(cReplay.Total() < (int)iMinBufferSize || !bTrainMode)
      return true;

コントロールブロックを渡した後、必要なローカル変数を作成します。

//---
   CBufferFloat *state1, *state2, *targetVals = new CBufferFloat();
   vector<float> target, actions, st1, st2, result;
   matrix<float> forward;
   double reward;
   int action;

そして、準備作業の後、メソッドパラメータで指定されたパッケージサイズでモデルの訓練サイクルを編成します。

//--- training loop in the batch size
   for(int i = 0; i < batch; i++)
     {
      //--- get a random state and the buffer replay
      if(!cReplay.GetRendomState(state1, action, reward, state2))
         return false;
      //--- feed forward pass of the training model ("current" state)
      if(!CNet::feedForward(state1, 1, false))
         return false;

ループ本体では、まず経験リプレイバッファからランダムな状態のセットを取得し、その結果得られた状態で訓練モデルのフィードフォワードパスを実行します。

      getResults(target);
      //--- unload state embedding
      if(!GetLayerOutput(iStateEmbedingLayer, state1))
         return false;
      //--- target net feed forward
      if(!cTargetNet.feedForward(state2, 1, false))
         return false;

訓練モデルに対してフィードフォワードパスを実行した後、結果と隠れ状態を保存します。

ターゲットネットを使って、同様の方法で後続のシステム状態の埋め込みを得ます。

      //--- reward adjustment
      if(bUseTargetNet)
        {
         cTargetNet.getResults(targetVals);
         reward += discount * targetVals.Maximum();
        }
      target[action] = (float)reward;
      if(!targetVals.AssignArray(target))
         return false;
      //--- backpropagation pass of the model being trained
      CNet::backProp(targetVals);

必要に応じて、システムの外部報酬をターゲットネットの予測値に調整し、訓練モデルのバックプロパゲーションパスを実行します。

次のステップでは、上記で得られた2つの状態の埋め込みを用いて、モデルのアンサンブルを訓練します。

      //--- forward net feed forward pass - next state prediction
      if(!cForwardNet.feedForward(state1, 1, false))
         return false;
      //--- download "future" state embedding
      if(!cTargetNet.GetLayerOutput(iStateEmbedingLayer, state2))
         return false;

まず、最初の状態埋め込みでモデルのアンサンブルをフィードフォワードで通過させます。

そして、フィードフォワードパスの結果をダウンロードし、それに基づいて、完璧な行動のベクトルをターゲットネットを使って得られた後続状態の埋め込みに置き換えて目標値を作成します。

そのために、モデルのアンサンブルのダイレクトパスの結果を、状態の埋め込みに等しい列数を持つ行列に変換します。この行列には、モデルのアンサンブル全体の結果が含まれています。そこで、すべてのアンサンブルモデルにおいて、ループを実装し、予測状態を完全行動の目標状態に置き換えます。

      //--- prepare targets for forward net
      cForwardNet.getResults(result);
      forward.Init(1, result.Size());
      forward.Row(result, 0);
      forward.Reshape(result.Size() / state2.Total(), state2.Total());
      int ensemble = (int)(forward.Rows() / target.Size());
      //--- copy the target state to the ensemble goals matrix
      state2.GetData(st2);
      for(int r = 0; r < ensemble; r++)
         forward.Row(st2, r * target.Size() + action);

一見したところ、すべてのモデルでターゲットの状態を置き換えることは、異なるデータでアンサンブルモデルを訓練するという考え方に反します。しかし、CNeuronMultiModelクラスのバックワードパスメソッドでランダムなモデル選択を整理したことを思い出してください。この段階では、どのモデルが訓練されるかはわかりません。そのため、全モデルに目標値を用意しています。訓練用のモデルは後で選択します。

      //--- backpropagation pass of foward net
      targetVals.AssignArray(forward);
      cForwardNet.backProp(targetVals);
     }
//---
   delete state1;
   delete state2;
   delete targetVals;
//---
   return true;
  }

訓練サイクルの反復の最後に、準備されたデータを使ってダイナミックフォワードモデルのアンサンブルのリバースパスを実行します。目標値を用意する際には、個々の行動の目標値のみを変更したことにご注目ください。残りは予想値のレベルにとどめました。これにより、バックプロパゲーションパスを実行するときに、特定の行動の誤差勾配だけを得ることができます。それ以外の方向では、誤差はゼロになると予想します。

ループの反復が成功したら、不要なオブジェクトを削除してメソッドを終了します。

クラスの残りのメソッドは、内因性好奇心モジュールの対応するメソッドと同様に構築されます。完全なEAコードは添付ファイルにあります。


3.テスト

必要なクラスとそのメソッドを作成したら、次にその作業をテストします。作成したクラスの機能をテストするために、エキスパートアドバイザー(EA) EVDRL-learning.mq5を作成します。前回と同様に、前回の記事を基にEAを作成します。今回は、訓練モデルのアーキテクチャには変更を加えません。その代わりに、使用するモデルのクラスを変更します。内因性好奇心モジュールを、不一致による探求のブロックに置き換えてみましょう。

//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+
#include "EVD.mqh"
...........
...........
...........
...........
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CEVD                 StudyNet;

また、モデルのアーキテクチャを記述するメソッドにも変更を加えます。インバースモデルのアーキテクチャーに関する記述を削除し、フォワードモデルのアーキテクチャーに変更を加えます。最後の1つは、少し考えてみる価値があります。以前は、フォワードモデルには隠れ層が1つのパーセプトロンを使っていました。アンサンブルモデル用に同様のアーキテクチャを作ってみましょう。

問題を素直に解く場合、すべてのモデルに十分なバッファサイズを持つ初期データの層と、モデルアンサンブルの新しいCNeuronMultiModelクラスの連続する2つの層を作成しなければなりません。しかし、どのアンサンブルモデルも同じシステム状態を使用していることに注目してみましょう。つまり、このようなアンサンブルを維持するためには、アンサンブルに含まれるモデルの数だけ、ソースデータ層で毎回1セットのデータを繰り返す必要があります。私の意見では、これはOpenCLコンテキストのメモリの非効率的な使い方で、ソースデータの大きなバッファの連結に費やされる追加時間が発生し、同時にデバイスのRAMからOpenCLコンテキストのメモリへの大量のデータの転送に費やされる時間が増加します。

すべてのモデルが、システム状態のコピーを1つだけ含む小さなデータバッファにアクセスするようにする方が、はるかに効率的でしょう。ただし、CNeuronMultiModelクラスのフィードフォワードメソッドを作成する際に、そのようなオプションは用意しませんでした。

基本的な完全連結層のアーキテクチャを見てみましょう。この層では、各ニューロンは、この層の他のニューロンとは独立した、独自の重みベクトルを持ちます。実際には、これはニューロン1個分の独立したモデルのアンサンブルです。つまり、アンサンブルのすべてのモデルの隠れ層として、1つの基本的な完全連結層を使うことができます。アンサンブルの全モデルにデータを供給するのに十分な大きさのニューラル層を実装すればよいのです。

したがって、フォワードモデルのアンサンブルでは、100個の要素からなるソースデータ層を作成します。これは、メインモデルから受け取るシステム状態の圧縮された表現のサイズです。この場合、可能な行動の全範囲についてモデルから予測状態を受け取ることが期待されるため、行動ベクトルは追加しません。

次に、5つのモデルのアンサンブルを使用します。隠れ層として、1000要素(1モデルあたり200ニューロン)の完全連結層を1つ作成します。

これに続くのが、新しいモデルアンサンブル層です。ここでは、ニューラル層について以下のように記述します。

  • descr.type:ニューラルネットワークのタイプ、 defNeuronMultiModels;
  • descr.count:モデルあたりのニューロンの数 - 400(4つの可能な行動の各状態を記述するための100個の要素)
  • descr.window:1モデルの前の層のニューロン数、 200
  • descr.step:アンサンブルのモデル数、5
  • descr.activation:活性化関数、TANH(双曲線正接、メインモデルの埋め込み層の活性化関数に対応しなければならない)
  • descr.optimization:最適化方法、ADAM(このタイプのニューラル層で唯一可能な方法)
bool CreateDescriptions(CArrayObj *Description, CArrayObj *Forward)
  {
//---
...........
...........
//---
   if(!Forward)
     {
      Forward = new CArrayObj();
      if(!Forward)
         return false;
     }
//--- Model
...........
...........
...........
...........
//--- Forward
   Forward.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!Forward.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 1000;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!Forward.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 400;
   descr.window = 200;
   descr.step = 5;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!Forward.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

条件を変えずにモデルを訓練し、テストしました(EURUSDペア、H1時間枠、デフォルトの指標パラメータ)。

テスト訓練の結果から言えるのは、アンサンブルモデルの訓練は、単一のフォワードモデルの訓練よりも時間がかかるということです。この場合、最初のうちはモデルがかなり無秩序に行動を実行するのがわかるでしょう。学習の過程で、このランダム性は減少します。

全体として、このモデルはテスト中に利益を上げることができました。

テストグラフ

検査結果


結論

強化モデルを訓練する際、環境からの学習は依然として重要な問題です。この記事では、この問題に対する別のアプローチを紹介しました。不一致による探求です。エージェントは、環境との相互作用の過程で自ら収集したデータをもとに、方策最適化法を用いてオンラインで学習します。同時に、エージェントが環境と相互作用するたびに、モデルのアンサンブルは更新され、これによりエージェントは各ステップで内部環境モデルを更新し、将来の環境状態についてより正確な予測を得ることができます。

モデルを作成し、MetaTrader 5のストラテジーテスターで実際のデータを使ってテストしました。モードはテスト中に利益を生み出しました。この結果は、この方向でのさらなる発展が有望であることを示唆しています。同時に、このモデルはかなり短期間で訓練され、テストされた。このモデルを実際の取引で使用するためには、さらに過去のデータでモデルを訓練する必要があります。


参照文献

  1. 不一致による探求
  2. ニューラルネットワークが簡単に(第35回):内因性好奇心モジュール
  3. ニューラルネットワークを簡単に(第36回):関係強化学習
  4. ニューラルネットワークを簡単に(その37):疎な注意

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

# ファイル名 タイプ 詳細
1 EVDRL-learning.mq5 EA モデルを訓練するEA
2 EVD.mqh クラスライブラリ 不一致ライブラリクラスによる探求
2 ICM.mqh クラスライブラリ 内因性好奇心モジュールライブラリクラス
3 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
4 NeuroNet.cl コードベース OpenCLプログラムコードライブラリ

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

添付されたファイル |
MQL5.zip (206.95 KB)
ニューラルネットワークが簡単に(第39回):Go-Explore、探検への異なるアプローチ ニューラルネットワークが簡単に(第39回):Go-Explore、探検への異なるアプローチ
強化学習モデルにおける環境の研究を続けます。この記事では、モデルの訓練段階で効果的に環境を探索することができる、もうひとつのアルゴリズム「Go-Explore」を見ていきます。
リプレイシステムの開発—市場シミュレーション(第7回):最初の改善(II) リプレイシステムの開発—市場シミュレーション(第7回):最初の改善(II)
前回の記事では、可能な限り最高の安定性を確保するために、レプリケーションシステムにいくつかの修正を加え、テストを追加しました。また、このシステムのコンフィギュレーションファイルの作成と使用も開始しました。
リプレイシステムの開発 - 市場シミュレーション(第8回):指標のロック リプレイシステムの開発 - 市場シミュレーション(第8回):指標のロック
この記事では、MQL5言語を使用しながら指標をロックする方法を見ていきます。非常に興味深く素晴らしい方法でそれをおこないます。
MQL5の圏論(第18回):ナチュラリティスクエア(自然性の四角形) MQL5の圏論(第18回):ナチュラリティスクエア(自然性の四角形)
この記事では、圏論の重要な柱である自然変換を紹介します。一見複雑に見える定義に注目し、次に本連載の「糧」であるボラティリティ予測について例と応用を掘り下げていきます。