English Русский 中文 Español Deutsch Português
preview
ニューラルネットワークが簡単に(第46回):目標条件付き強化学習(GCRL)

ニューラルネットワークが簡単に(第46回):目標条件付き強化学習(GCRL)

MetaTrader 5トレーディングシステム | 27 11月 2023, 11:33
292 0
Dmitriy Gizlyk
Dmitriy Gizlyk

はじめに

「目標条件付き強化学習」というと、少し変わった、あるいは奇妙に聞こえるかもしれません。結局のところ、強化学習の基本原理は、エージェントが環境と相互作用する間の総報酬を最大化することを目的としています。しかしこの文脈では、特定の段階、あるいは特定のシナリオの中で特定の目標を達成することに注目しています。

すでに全体的な目標をサブタスクに分解することの利点については議論し、全体的な結果を達成するために貢献するさまざまなスキルをエージェントに教える方法を探りました。この記事では、この問題を別の角度から見てみようと思います。すなわち、エージェントが特定のサブタスクを達成するための戦略とスキルを独自に選択できるように訓練することです。


1.GCRLの特徴

GCRL(goal-conditioned reinforcement learning、目標条件付き強化学習)は、複雑な強化学習問題の集合です。エージェントは、特定のシナリオでさまざまな目標を達成するように訓練されます。これまでは、エージェントは現在の環境状態に応じて、ある行動を選択するか、別の行動を選択するかを訓練していました。GCRLの場合、エージェントの行動が現在の状態だけでなく、現段階での特定のサブタスクによっても決定されるように訓練したいのです。言い換えれば、現在の状態を表すベクトルに加えて、それぞれの特定の瞬間に達成すべきサブタスクをエージェントに示す必要があります。これは、各時点でエージェントにスキルを指示する、スキルの訓練作業に非常に似ています。結局のところ、「ポジションを建てる」スキルや「ポジションを建てる」タスクを使うように指示することは、言葉遊びのように思えます。しかし、この言葉の裏には、エージェントを育成するアプローチの違いがあります。

強化学習では、ボトルネックは常に報酬関数です。従来の強化訓練と同様、スキル訓練課題では単一の目的報酬関数が使用されます。使用するスキルを示すことは、環境の状態を補完し、エージェントがそれをナビゲートするのに役立つはずです。

GCRLアプローチを使用する場合、特定のサブタスクを導入します。その成果は、エージェントが受け取る報酬に反映されるべきです。これは識別器の内部報酬と似ていますが、特定の目標(サブタスクの解決)を達成することを目的とした、明確な測定可能な指標に基づいています。

この微妙な境界線を理解するために、両方のアプローチでポジションを建てる例を見てみましょう。スキルの訓練の際には、現在の環境の状態と、ポジションが欠落している口座の状態のベクトルをスケジューラに渡しました。これにより、スケジューラは、エージェントに渡すスキル記述ベクトルを決定することができました。覚えておいでのように、口座残高の変化を報酬として使いました。注目すべきは、エージェントの訓練中、同じ報酬を適用していることです。さらに、ポジションを開いても、すぐに残高の変動に影響するわけではありません。例外は、ポジションを建てる際の手数料ですが、一般的に、ポジションを建てると、遅れてと報酬を受け取ります。

GCRLの場合、グローバルな目標報酬とともに、特定のサブタスク達成に対する追加報酬を導入します。例えば、ポジションを建てることに対して何らかの報酬を導入したり、逆にエージェントがポジションを建てるまで罰金を課したりすることができます。ここでは、そのような報酬の形成についてバランスの取れたアプローチを取る必要があります。それは、取引操作そのものから生じる可能性のある利益と損失を超えてはなりません。そうでなければ、エージェントは単にポジションを開いて「ポイントを獲得」するだけで、口座残高は0になりがちです。

それに、報酬は目の前のタスクに依存すべきです。ポジションを建てるタスクを設定するときのみ、ポジションを建てることに報酬を与え、ポジションをン開かないことにペナルティを与えます。ポジションからのエグジットポイントを探す場合、逆に、ポジションを追加したり、ポジションを長期間保有したりした場合のペナルティを導入することもできます。

GCRLで目の前のタスクを記述するためのベクトルを形成する際には、ある要件を考慮に入れることが重要です。ベクトルは、エージェントが特定の時点で達成すべきサブタスクを明示的に示すべきです。

タスク記述ベクトルには、タスクの文脈や詳細に応じて、様々な要素を含めることができます。例えば、ポジションを建てる場合、説明ベクトルには、ターゲット資産、取引量、価格制限、またはポジションを建てることに関連する他のパラメータに関する情報が含まれる場合があります。エージェントが与えられたサブタスクを正しく解釈できるように、これらの要素は明確で理解できるものでなければなりません。

さらに、タスク記述ベクトルは、エージェントがこのサブタスクの達成に最大限集中した意思決定をおこなえるように、十分な情報量を持つべきです。そのためには、エージェントが目標達成のためにどのように行動すべきかをより正確に理解するための追加データや文脈情報を含める必要があるかもしれません。

サブタスクの説明ベクトルと望ましい結果との間には、数学的な関係ではなく、論理的な関係が明白でなければなりません。通常のワンホットベクトルを使うことができます。ベクトルの各要素は別々のサブタスクに対応します。このベクトルは、現在の環境状態の説明とともにエージェントに渡されます。大事なことは、エージェントがサブタスクを明確に解釈し、サブタスクと報酬の間に内部的なつながりを構築できることです。この点で、報酬に注意を払うべきです。追加で導入される報酬は、特定のサブタスクに一致したものでなければなりません。

しかし、サブタスク記述ベクトルを形成するアプローチは他にもあります。別のサブタスクを記述するために多くの要素の組み合わせが必要な場合、スキルを訓練する方法と類似して、そのようなベクトルを形成するために別のモデルを使用することができます。このようなモデルは、さまざまなオートエンコーダやその他の利用可能な方法を使用して訓練することができます。

おわかりのように、どちらのアプローチも非常に強力で、異なる問題を解決することができます。しかし、それぞれに欠点があります。2つのアプローチの間にさまざまな相乗効果が現れるのは偶然ではなく、それによってさらに安定したアルゴリズムを構築することが可能になります。実際、スキルの訓練中に、現在の環境の状態とエージェントのスキル(行動方策)の間に依存関係を構築しました。特定のサブタスクの達成を目的とした追加ツールを使用することは、最適な結果を得るためにエージェント戦略を調整するのに役立ちます。

そのようなアプローチの1つが適応変分GCRL (aVGCRL)です。確率的な環境では、各スキル表現の分布は一様ではないという考え方です。しかも、環境の状態によって変わることもあります。ある特定の状態では、分布の分散が最小になるような、あるスキルを持った従属性が存在します。同時に、同じ状態で他のスキルを使う可能性はそれほど明確ではなく、その分布の分散は著しく高くなります。他の環境状態では、スキル分布の分散は劇的に異なる可能性が高くなります。この効果は、前回の記事でスケジューラの訓練に使った変分オートエンコーダの分散の潜在表現から観察できます。論理的な解決策は、明示的な依存関係に焦点を当てることでしょう。aVGCRL法の著者は、各スキルの目標値からの偏差誤差を分布の分散で割ることを提案しています。明らかに、分散が小さければ小さいほど、誤差の影響は大きくなり、対応する重み付け係数は学習過程で変化します。同時に、他のスキルのランダム性は、一般的なモデルに大きな不均衡をもたらしません。


2.MQL5を使用した実装

GCRL法の実装に移って、さらによく理解しましょう。この2つの手法の共生のようなものを生み出すつもりですが、すべてを1つのモデルに統合するつもりです。

前回は、変分オートエンコーダの形をしたスケジューラとエージェントの2つのモデルを作成しました。これまでのアプローチとは異なり、エージェントはオートアンカーの潜在的な状態のみを受け取りました。私たちの論理によれば、そこには必要な情報がすべて含まれているはずです。テストでは、オートエンコーダが予測した状態を達成するようにエージェントを訓練しても、望ましい結果が得られないことが示されました。これは、予測の質が十分でなかったためかもしれません。

同時に、報酬に対する古典的なアプローチを使用することで、以前に訓練されたスケジューラを使用してエージェントの訓練プロセスを改善することができました。

この研究では、変分オートエンコーダの個別学習をやめ、そのエンコーダをエージェントモデルに直接組み込むことにしました。このアプローチは、オートエンコーダの訓練の原則にやや反すると言わざるを得ません。結局のところ、オートエンコーダを使う主なアイデアは、特定のタスクを参照することなくデータを圧縮することです。しかし現在では、同じソースデータから複数の問題を解くためにエンコーダを訓練するというタスクには直面していません。

その上、エンコーダの入力には環境の現在の状態しか供給しません。ここでの場合、これらは商品価格と分析指標のパラメータの動きの履歴データです。言い換えれば、口座ステータスに関する情報は除外します。スケジューラ(この場合はエンコーダ)は、過去のデータに基づいて使用するスキルを形成すると仮定します。これは、上昇市場、下落市場、横ばい市場で働くための方策となり得ます。

口座ステータスに関する情報に基づいて、エージェントがエントリーポイントまたはエグジットポイントを検索するためのサブタスクを作成します。

モデルをスケジューラとエージェントに分けるのは、まったく恣意的なものです。結局のところ、私たちはひとつのモデルを形成することになります。ただし、前述したように、エンコーダの入力には過去のデータしか供給しません。つまり、割り当てられたサブタスクに関する情報をモデルの真ん中に追加しなければなりません。このようなことはこれまでしませんでした。これはまったく新しい解決策ではありません。以前にも同じようなことがありました。その場合、2つのモデルを作成しました。

最初の部分は1つのモデルで解かれ、次に1つ目のモデルの出力と新しいデータを組み合わせ、2つ目のモデルの入力に投入されました。この解決策は準備しやすいですが、ひとつ重大な欠点があります。メインプログラムとOpenCLコンテキストの間で冗長な通信が発生します。最初のモデルの結果をコンテキストから取得し、2番目のモデルのために再び読み込まなければなりません。リバースパス時の誤差の勾配も同様です。単一のモデルを使用することで、これらの操作が不要になりますが、モデル操作の別の段階で新しい情報を追加することには疑問が生じます。

この問題を解決するために、新しいタイプのニューラル層CNeuronConcatenateを作成します。前回と同様、OpenCLプログラムで必要なカーネルを作成することで、それぞれの新しいニューラル層クラスの作業を開始します。まず、Concat_FeedForwardフォワードパスカーネルを作成しました。すべてのカーネルは、基となる全結合層の類似カーネルを基に作成されました。主な違いは、2つ目の情報ストリームのための追加バッファとパラメータの追加です。

Concat_FeedForwardカーネルパラメータには、1つの重み行列、2つのソースデータテンソル、結果のベクトル、3つの数値パラメータ(ソースデータテンソルのサイズと活性化関数ID)が示されています。

__kernel void Concat_FeedForward(__global float *matrix_w,
                                 __global float *matrix_i1,
                                 __global float *matrix_i2,
                                 __global float *matrix_o,
                                 int inputs1,
                                 int inputs2,
                                 int activation
                                )

前回と同様に、層のニューロン数に基づく1次元タスク空間でカーネルを起動します。これは結果バッファのサイズと同じです。カーネル本体では、スレッドIDを定義し、必要なローカル変数を宣言します。ここでは、重み係数バッファのオフセットを決定します。層の出力にある各ニューロンについて、2つのソースデータバッファと1つのベイジアンバイアスニューロンの合計サイズに等しい重み数を定義することにご注意ください。

  {
   int i = get_global_id(0);
   float sum = 0;
   float4 inp, weight;
   int shift = (inputs1 + inputs2 + 1) * i;

次に、1つのソースデータバッファの加重和を計算するサイクルを準備します。このプロセスは、全結合層のカーネルにおけるプロセスと完全に同じです。

   for(int k = 0; k < inputs1; k += 4)
     {
      switch(inputs1 - k)
        {
         case 1:
            inp = (float4)(matrix_i1[k], 0, 0, 0);
            weight = (float4)(matrix_w[shift + k], 0, 0, 0);
            break;
         case 2:
            inp = (float4)(matrix_i1[k], matrix_i1[k + 1], 0, 0);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], 0, 0);
            break;
         case 3:
            inp = (float4)(matrix_i1[k], matrix_i1[k + 1], matrix_i1[k + 2], 0);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], 0);
            break;
         default:
            inp = (float4)(matrix_i1[k], matrix_i1[k + 1], matrix_i1[k + 2], matrix_i1[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;
     }

ループの反復が完了したら、重み行列のバイアスをソースデータバッファ1個分のサイズで調整します。さらに、2つのソースデータバッファに対しても同様のサイクルを作ります。

   shift += inputs1;
   for(int k = 0; k < inputs2; k += 4)
     {
      switch(inputs2 - k)
        {
         case 1:
            inp = (float4)(matrix_i2[k], 0, 0, 0);
            weight = (float4)(matrix_w[shift + k], 0, 0, 0);
            break;
         case 2:
            inp = (float4)(matrix_i2[k], matrix_i2[k + 1], 0, 0);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], 0, 0);
            break;
         case 3:
            inp = (float4)(matrix_i2[k], matrix_i2[k + 1], matrix_i2[k + 2], 0);
            weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], 0);
            break;
         default:
            inp = (float4)(matrix_i2[k], matrix_i2[k + 1], matrix_i2[k + 2], matrix_i2[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;
     }

カーネルの最後に、ベイズバイアス要素を追加し、結果の合計をアクティブにします。そして、結果の値を結果バッファの対応する要素に保存します。

   sum += matrix_w[shift + inputs2];
//---
   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[i] = sum;
  }

バックパスカーネルを修正し、重み行列を更新する際にも、まったく同じアプローチが用いられました。詳しくはNeuroNet_DNG\NeuroNet.cl(記事に添付)をご覧ください。

カーネルを作成した後は、メインプログラムのCNeuronConcatenateクラスのコード作成に移ります。クラスメソッドのセットは極めて標準的です。

  • コンストラクタ(CNeuronConcatenate)およびデストラクタ (~CNeuronConcatenate)
  • 初期ニューラル層の初期化
  • feedForwarフォワードパス
  • calcHiddenGradients誤差勾配分布
  • updateInputWeights重み行列の更新
  • Typeオブジェクトの識別
  • ファイルのSaveおよびLoad

class CNeuronConcatenate   :  public CNeuronBaseOCL
  {
protected:
   int               i_SecondInputs;
   CBufferFloat     *ConcWeights;
   CBufferFloat     *ConcDeltaWeights;
   CBufferFloat     *ConcFirstMomentum;
   CBufferFloat     *ConcSecondMomentum;

public:
                     CNeuronConcatenate(void);
                    ~CNeuronConcatenate(void);
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, 
                          uint inputs1, uint inputs2, ENUM_OPTIMIZATION optimization_type, uint batch);
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput);
   virtual bool      calcHiddenGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput);
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual int       Type(void)        const                      {  return defNeuronConcatenate; }
   virtual void      SetOpenCL(COpenCLMy *obj);
  };

さらに、このクラスでは、追加のソースデータのサイズを記録するための変数と、重み係数を最適化するためのさまざまなメソッド用の重み行列とモーメント行列の4つのデータバッファを宣言しています。新しいバッファは、前のニューラル層と新しいソースデータとの通信プロセスを準備するために使われます。後続のニューラル層へのデータ転送は、CNeuronBaseOCL全結合層の親クラスによっておこなわれます。

クラスのコンストラクタでデータバッファを初期化します。

CNeuronConcatenate::CNeuronConcatenate(void) : i_SecondInputs(0)
  {
   ConcWeights = new CBufferFloat();
   ConcDeltaWeights = new CBufferFloat();
   ConcFirstMomentum = new CBufferFloat();
   ConcSecondMomentum = new CBufferFloat;
  }

クラスのデストラクタでは、データを消去し、オブジェクトを削除します。

CNeuronConcatenate::~CNeuronConcatenate()
  {
   if(!!ConcWeights)
      delete ConcWeights;
   if(!!ConcDeltaWeights)
      delete ConcDeltaWeights;
   if(!!ConcFirstMomentum)
      delete ConcFirstMomentum;
   if(!!ConcSecondMomentum)
      delete ConcSecondMomentum;
  }

すべての必要なデータバッファのサイズの指示は、Initオブジェクトの初期化メソッドに配置されます。このメソッドは、パラメータに必要な初期データを受け取ります。

  • numOutputs:次の層のニューロン数
  • open_cl:OpenCLコンテキスト処理オブジェクトへのポインタ
  • numNeurons:現在の層のニューロン数
  • numInputs1:前の層の要素数
  • numInputs2:追加ソースデータバッファの要素数
  • optimization_type:パラメータの最適化手法 ID
bool CNeuronConcatenate::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, 
                              uint numInputs1, uint numInputs2, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch))
      return false;

メソッド本体では、コントロールブロックの代わりに、親クラスの同様のメソッドを呼び出し、操作の結果を確認します。親クラスはすでに基本的なコントロールを実装しているので、それを繰り返す必要はありません。さらに、親クラスのメソッドは、継承されたすべてのオブジェクトと変数の初期化を実装します。したがって、追加したオブジェクトを初期化する処理だけを、このメソッドの本体で準備すればよいのです。

まず、前のニューラル層とのデータ交換を準備するために、ランダムな値を持つ重み付け係数の行列を作成し、初期化します。重み行列のサイズは、前の層と追加されたソースデータバッファを準備するのに十分なサイズに設定されていることにご注意ください。これはまさに、フォワードパスカーネルを作るときに私たちが思い描いたアプローチです。今は、メインプログラムの側でクラスメソッドを作るときに、これを守っています。

   i_SecondInputs = (int)numInputs2;
   if(!ConcWeights)
     {
      ConcWeights = new CBufferFloat();
      if(!ConcWeights)
         return false;
     }
   int count = (int)((numInputs1 + numInputs2 + 1) * numNeurons);
   if(!ConcWeights.Reserve(count))
      return false;
   float k = (float)(1.0 / sqrt(numNeurons + 1.0));
   for(int i = 0; i < count; i++)
     {
      if(!ConcWeights.Add((2 * GenerateWeight()*k - k)*WeightsMultiplier))
         return false;
     }
   if(!ConcWeights.BufferCreate(OpenCL))
      return false;

次に、パラメータで指定された重み係数の更新方法に応じて、モーメントバッファを初期化します。覚えておいでかもしれませんが、SGDに1つのモーメントバッファを使っています。Adamメソッドを使用する場合、2つのモーメントバッファが初期化されます。未使用のオブジェクトを削除することで、利用可能なリソースをより効率的に使用できるようになります。

   if(optimization == SGD)
     {
      if(!ConcDeltaWeights)
        {
         ConcDeltaWeights = new CBufferFloat();
         if(!ConcDeltaWeights)
            return false;
        }
      if(!ConcDeltaWeights.BufferInit(count, 0))
         return false;
      if(!ConcDeltaWeights.BufferCreate(OpenCL))
         return false;
      if(!!ConcFirstMomentum)
         delete ConcFirstMomentum;
      if(!!ConcSecondMomentum)
         delete ConcSecondMomentum;
     }
   else
     {
      if(!!ConcDeltaWeights)
         delete ConcDeltaWeights;
      //---
      if(!ConcFirstMomentum)
        {
         ConcFirstMomentum = new CBufferFloat();
         if(CheckPointer(ConcFirstMomentum) == POINTER_INVALID)
            return false;
        }
      if(!ConcFirstMomentum.BufferInit(count, 0))
         return false;
      if(!ConcFirstMomentum.BufferCreate(OpenCL))
         return false;
      //---
      if(!ConcSecondMomentum)
        {
         ConcSecondMomentum = new CBufferFloat();
         if(!ConcSecondMomentum)
            return false;
        }
      if(!ConcSecondMomentum.BufferInit(count, 0))
         return false;
      if(!ConcSecondMomentum.BufferCreate(OpenCL))
         return false;
     }
//---
   return true;
  }

クラスの初期化メソッドの作業を終えたので、メイン関数の準備に移ります。まず、feedForwardパスメソッドを作成します。以前に検討したすべてのクラスのダイレクトパスメソッドとは異なり、このメソッドは、そのパラメータで、前のニューラル層と追加のソースデータバッファの2つのオブジェクトへのポインタを受け取ります。これが作成されるクラスの主な特徴であるため、これには何も驚くべき点はありません。しかしこのメソッドでは、メインプログラムの側で、作成されたクラスの外での追加作業が必要になります。これについてはもう少し後でお話します。

bool CNeuronConcatenate::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!OpenCL || !NeuronOCL || !SecondInput)
      return false;

メソッド本体では、まず受け取ったポインタの関連性を確認します。さらに、OpenCLコンテキストで動作するためのオブジェクトへのポインタの存在を確認します。少なくとも1つのポインタが欠落していれば、メソッドをfalseで終了します。

次に、追加データバッファーのサイズを確認します。十分な数の要素が含まれていなければなりません。より大きなバッファサイズを指定することも可能ですが、作業中は、バッファの最初の要素だけが、クラスが初期化されたときに指定された量だけ使われます。

   if(SecondInput.Total() < i_SecondInputs)
      return false;
   if(SecondInput.GetIndex() < 0 && !SecondInput.BufferCreate(OpenCL))
      return false;

次に、OpenCLコンテキストのデータバッファへのポインタをチェックし、必要であれば新しいバッファを作成します。

コンテキストにデータバッファへのポインタがない場合のみ、新しいバッファを作成することにご注意ください。存在する場合は、データをコンテキストに再読み込みしません。ポインタの存在は、コンテキストの中にデータが存在することを示すと考えています。したがって、メインプログラムの側でバッファの内容が変更された場合、そのデータをコンテキストにコピーする必要があります。コンテキストメモリのデータが最新であることを確認するのは、ユーザーの責任です。

次に、データバッファへのポインタと必要な定数をカーネルパラメータに渡します。この手順はすべてのカーネルで同じです。カーネルの識別子、パラメータ、対応するデータバッファへのポインタのみが変更されます。すべての数学演算は、OpenCLプログラム側のカーネル本体で指定する必要があります。

   if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_w, ConcWeights.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_i1, NeuronOCL.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_i2, SecondInput.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_o, Output.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_ConcatFeedForward, def_k_cff_inputs1, (int)NeuronOCL.Neurons()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_ConcatFeedForward, def_k_cff_inputs2, (int)i_SecondInputs))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_ConcatFeedForward, def_k_cff_activation, (int)activation))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

メソッド操作の最後に、カーネルを実行するタスク空間を指定し、それを実行キューに入れます。

   uint global_work_offset[1] = {0};
   uint global_work_size[1];
   global_work_size[0] = Output.Total();
   if(!OpenCL.Execute(def_k_ConcatFeedForward, 1, global_work_offset, global_work_size))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
//---
   return true;
  }

ここでは、バッファIDとその内容だけでなく、各ステージで呼び出されるカーネルの仕様の正しさを制御することが非常に重要です。もちろん、各ステップでの操作の正しさを管理することも忘れてはなりません。

誤差勾配を分配する方法と重み行列を更新するメソッドは、同様のアルゴリズムに基づいており、添付ファイルでご覧になることができます。ただ、誤差の勾配を分配する際には、追加ソースデータのレベルで誤差の勾配のバッファが追加されることに注意すべきです。この作業では、そのデータをダウンロードして使用することはありませんが、将来的に、2つ目のモデルによって追加的な初期データのベクトルが生成された場合には必要になるかもしれません。

CNeuronConcatenateクラスのメソッドを作成した後、メインプログラムから特定のニューラル層にユーザーのソースデータの追加バッファを転送するプロセスを準備します。一般的に、モデルを作成した後、ユーザーはモデル全体のフォワードパスとリバースパスの2つの方法のみで作業するようにプロセスが構成されています。ニューラル層間のデータ転送はコントロールできません。すべてのプロセスは、ライブラリ内の見えないところでおこなわれます。したがって、ユーザーは1つのフォワードパスメソッドを呼び出し、そのパラメータに2つのデータバッファを指定できるようにしなければなりません。その後、モデルは独立してデータを適切な情報フローに分配します。

現段階では、データを追加する層は1つだけにする予定です。どのニューラル層にソースデータを追加転送するかのトラッキングを追加してプロセスを複雑にしないために、バッファへのポインタをすべてのニューラル層に渡すことにしました。使い方は、クラス自体のレベルで決定されます。

ここでは、チェーンに沿った複数のメソッドに1つのパラメータを追加することについては詳しく考えません。すべてのメソッドと関数の完全なコードは、添付ファイルにあります。すべてのクラスのダイレクトパスメソッドが同じ名前で仮想宣言されているにもかかわらず、あるクラスではパラメータを追加し、あるクラスではパラメータを追加しないため、継承されたクラスのメソッドを完全に再定義することはできません。遺伝性を維持するためには、以前に作られたすべてのクラスのフォワードパスとバックワードパスのメソッドをやり直さなければなりません。これはおこなっていません。その代わり、その下にあるニューラル層のディスパッチメソッドに制御を追加しただけです。ダイレクトパスメソッドの例を見てみましょう。

CNeuronBaseOCL::FeedForwardディスパッチメソッドのパラメータに、データバッファへのポインタを追加し、デフォルト値を代入します。このトリックを使えば、前のニューラル層へのポインタだけでメソッドを使うことができます。これは、以前に作成したモデルにライブラリを使用する場合に便利で、以前に作成したプログラムを変更せずにコンパイルすることができます。

次に、現在のニューラル層のタイプを確認します。2つのスレッドからのデータを結合するクラスであれば、対応するフォワードパスメソッドを呼び出します。そうでない場合は、以前に作成したアルゴリズムを使用します。以下は、変更を加えたメソッドコードの一部です。さらに、メソッドコードも変わりませんでした。CNeuronBaseOCL::FeedForwardメソッドの完全なコードは添付ファイルにあります。そこには、修正されたリバースパスディスパッチメソッドもあります。ヌルデフォルトポインタを持つバッファも追加されました。

bool CNeuronBaseOCL::FeedForward(CObject *SourceObject, CBufferFloat *SecondInput = NULL)
  {
   if(CheckPointer(SourceObject) == POINTER_INVALID)
      return false;
//---
   CNeuronBaseOCL *temp = NULL;
   if(Type() == defNeuronConcatenate)
     {
      temp = SourceObject;
      CNeuronConcatenate *concat = GetPointer(this);
      return concat.feedForward(temp, SecondInput);
     }

情報量は多いですが、記事のサイズは限られています。というわけで、新しいCNeuronConcatenateクラスのメソッドを簡単に説明しました。このことが、考え方やアプローチの理解に悪影響を及ぼさないことを願っています。いずれにせよ、それらのアルゴリズムは、先に説明したクラスの同様の手法と大差はありません。すべてのメソッドとクラスの完全なコードは添付ファイルにあります。質問があれば、フォーラムやWebサイトの個人メッセージでお答えします。ご都合のよい連絡方法をお選びください。

検討中のGCRL強化学習法に近づき、モデルの構築と訓練のプロセスを検討します。前回同様、3つのEAを作成します。

  • 例のプライマリコレクション:GCRLResearch.mq5
  • エージェント訓練:GCRLStudyActor.mq5
  • モデル操作のテスト:GCRLTest.mq5

GCRLTrajectory.mqhインクルードファイルでモデルアーキテクチャを示します。

上述したように、モデル全体を1つのエージェントの中に組み立てます。その結果、1つのモデルのアーキテクチャーしか説明できません。CreateDescriptionsメソッドの本体では、まず動的配列オブジェクトへのポインタの関連性を確認し、必要であれば新しいオブジェクトを作成します。ニューラル層を記述するための新しいオブジェクトを追加する前に、必ず動的配列をクリアしてください。

bool CreateDescriptions(CArrayObj *actor)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
//--- Actor
   actor.Clear();

いつものように、まずソースデータ層を作成します。その後に正規化層が続きます。エンコーダの初期データは、過去のデータと指標パラメータだけであることはすでに述べました。これはニューラル層の大きさに反映されています。

//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   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;
     }

次に、前回の記事のエンコーダアーキテクチャーを完全に繰り返します。畳み込みのブロックで構成されています。その後、3つの全結合層が続き、変分オートエンコーダの潜在表現のエンコーダ層で終わります。これは完全なモデルとしては少し変わった解決策です。アルゴリズムとモデルを分ける際の慣例についてはすでに述べました。実際の結果を見てみましょう。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count - 1;
   descr.window = 2;
   descr.step = 1;
   descr.window_out = 4;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   prev_count = descr.count = prev_count;
   descr.window = 4;
   descr.step = 4;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count - 1;
   descr.window = 2;
   descr.step = 1;
   descr.window_out = 4;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   prev_count = descr.count = prev_count;
   descr.window = 4;
   descr.step = 4;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * NSkills;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NSkills;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

これでエンコーダの説明は完了です。エージェントの作成に移りましょう。そのアーキテクチャは、2つのデータストリームを組み合わせる層から始まります。最初のストリームはエンコーダ結果のサイズに等しくなります。もう1つは、タスクを記述するベクトルのサイズに等しくなります。バランス状態の記述を、目の前のタスクを記述するためのベクトルとして使います。

理論の部分では、サブタスクの分離可能性の必要性について話しました。この単純化されたスキームでは、2つのサブタスクのみを使用します。

  • ポジションへのエントリの検索
  • ポジションからのエグジットの検索

口座状況の説明の構成でポジションを示しました。したがって、未決済ポジションの数量が「0」の場合、ポジションを建てることがタスクとなります。そうでなければ、エグジットを探すことになります。アイデアはシンプルで、ワンホットベクトルを使うことを連想させます。唯一の違いはポジションの数量です。最小ロットを使用し、複数のポジションを同時に建てることができるため、「1」になることはほとんどありません。

//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 256;
   descr.window=prev_count;
   descr.step=AccountDescr;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

口座の状態を説明する際には、相対的な単位を使用して、正規化されたデータに近い値を期待します。したがって、ここではバッチ正規化層は使用しません。

次に、2つの全結合層からなる意思決定ブロックと、完全にパラメータ化されたFQF分位関数のブロックが来ます。おわかりのように、前回の記事のエージェントでも同様の意思決定ブロックを使用しています。各ニューラル層の解の主な特性と特徴についてはそこですでに述べました。

//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFQF;
   descr.count = NActions;
   descr.window_out = 32;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

モデルのアーキテクチャを説明した後、例のプライマリデータベース「GCRLResearch.mq5」を収集するロボットの作成に移ります。このEAのアルゴリズムは、記事間で実質的に変化することはありません。その詳細な考察は、本稿の範囲外としておきます。完全なEAコードは添付ファイルにあります。ここでは、GCRL法の使用による変化について簡単に述べるだけにします。

まず第一に、最新モデルの欠点のひとつが、ポジションの長期保持であったことを思い出しましょう。口座の状態を表すベクトルには、各方向のポジションの量と累積利益が含まれていることがわかります。しかし、ポジションを建てるタイミングは示されていません。このプロセスを制御するためにエージェントを訓練したいのであれば、適切な参照点を与えるべきです。

エージェントの行動範囲では、すべてのポジションを閉じるという選択肢しかありません。したがって、ロングとショートのポジションを保有する時間を分ける必要はないと思います。すべてのポジションに共通のパラメータを1つ導入しましょう。同時に、時間だけでなく、ポジションの量や累積損益にも依存するパラメータを作成したいと考えました。

このような指標として、累積損益の絶対値の合計をポジションの期間で加重することを提案します。これによって、ポジションを建てる時間、出来高、市場のボラティリティ(利益を通じて間接的に)に指標を適応させることができます。利益の絶対値を使うことで、利益のあるポジションと利益のないポジションの相互吸収的な影響を排除することができます。 

 上記を考慮して、EAのOnTickメソッドでおこなわれる口座状態の記述プロセスを調整します。

口座ステータス記述の最初の2つの要素に、口座残高と資金の指標を格納します。情報量を減らし、その質を向上させるために証拠金指標の表示を断念しました。、今回のタスクの文脈では情報量が少ないためです。ただし、その後の作業で追加される可能性がないわけではありません。

ポジションを建てる時間は秒単位で考慮され、H1時間枠を使用します。さっそく、ポジションの有効時間を時間単位で調整するための乗数を決めてみましょう。ここでは、上記の式を使ってポジションを保持した場合のペナルティを計算する変数を追加します。ただし、保有によるペナルティがポジションからの収入を上回ることは避けたいため、1時間ごとに累積利益の1/10の罰金を課すことにします。上の式で利益の絶対値を使えば、利益のあるポジションと利益のないポジションの両方にペナルティを課すことができます。

現在時刻をローカル変数に保存し、ポジションを検索するループを開始します。ループ本体では、ポジションの数量と、各方向の累積利益/損失、およびポジションを保有することによるペナルティの合計を計算します。

   sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY);
//---
   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   double position_discount = 0;
   double multiplyer = 1.0 / (60.0 * 60.0 * 10.0);
   int total = PositionsTotal();
   datetime current = TimeCurrent();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += PositionGetDouble(POSITION_PROFIT);
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += PositionGetDouble(POSITION_PROFIT);
            break;
        }
      position_discount -= (current - PositionGetInteger(POSITION_TIME)) * multiplyer*MathAbs(PositionGetDouble(POSITION_PROFIT));
     }
   sState.account[2] = (float)buy_value;
   sState.account[3] = (float)sell_value;
   sState.account[4] = (float)buy_profit;
   sState.account[5] = (float)sell_profit;
   sState.account[6] = (float)position_discount;

ループの反復が完了したら、結果の値を適切な配列要素に保存して、例のデータベースに書き込みます。

データをモデルに渡す前に、相対単位フィールドに変換します。

   State.AssignArray(sState.state);
   Account.Clear();
   float PrevBalance = (Base.Total <= 0 ? sState.account[0] : Base.States[Base.Total - 1].account[0]);
   float PrevEquity = (Base.Total <= 0 ? sState.account[1] : Base.States[Base.Total - 1].account[1]);
   Account.Add((sState.account[0] - PrevBalance) / PrevBalance);
   Account.Add(sState.account[1] / PrevBalance);
   Account.Add((sState.account[1] - PrevEquity) / PrevEquity);
   Account.Add(sState.account[2]);
   Account.Add(sState.account[3]);
   Account.Add(sState.account[4] / PrevBalance);
   Account.Add(sState.account[5] / PrevBalance);
   Account.Add(sState.account[6] / PrevBalance);

ダイレクトパスメソッドを説明する際に強調したように、OpenCLコンテキストメモリ内の追加ソースデータバッファのデータの関連性の責任はユーザーにあります。したがって、口座情報バッファを更新した後、その内容をコンテキストメモリに転送します。その後、エージェントのダイレクトパスメソッドを呼び出し、両方のデータバッファへのポインタを渡します。

   if(Account.GetIndex()>=0)
      if(!Account.BufferWrite())
         return;
   if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
      return;

エージェントの行動をサンプリングして実行するブロックは、類似のEAからそのまま引き継いだものであり、ここではその説明を省略します。

例題収集のためのEAのOnTick関数の変更の説明の最後に、報酬関数について少し述べておく必要があります。これまでと同様、報酬関数の基本は、口座残高の変化の相対的価値ですが、GCRL法では、ローカルな目標を達成することでさらなる報酬を得ることができます。私たちの場合は罰則を使います。ポジションを閉じる作業では、上記で計算した累積利益と累積損失の絶対値の加重合計の指標を毎回差し引きます。そうすることで、大きな利益や損失が累積したポジションを保有することに、できる限りペナルティを課します。これは、エージェントがポジションを閉じることを促すはずです。同時に、累積利益が少ないポジションは大きなペナルティを発生させありません。これにより、エージェントは利益の蓄積を期待することができます。

   float reward = Account[0];
   if((buy_value+sell_value)>0)
     reward+=(float)position_discount;
   else
     reward-=atr;
   if(!Base.Add(sState, act, reward))
      ExpertRemove();
//---
  }

未決済のポジションがない場合は、エージェントに取引を勧めます。この場合、ATR指標の現在値に相当するペナルティが与えられます。

それ以外は、EAのアルゴリズムに変更はありません。すべてのコードは添付ファイルにあります。

例のデータベース「GCRLResearch.mq5」を収集するEAの作業が完了したら、ストラテジーテスターの低速最適化モードで起動します。それでは、エージェント訓練EA「GCRL\StudyActor.mq5」に移りましょう。

この作業では、例データベースに保存された行動と報酬のみでエージェントを訓練します。前回の記事のように、他の行動に対する予測報酬は計算しません。その代わりに、目の前のタスクに応じてポリシーを構築するようエージェントに教えることに焦点を当てます。例のデータベースがある歴史的な期間のパスを含んでいるという事実を利用します。しかし、例のデータベースを収集する段階でランダムに選択された行動の数のために、1つの歴史的瞬間の各パスで、我々はエージェントの異なる行動とその後の報酬でポジションと累積損益の異なるセットを受け取ることになります。つまり、エージェントに様々なローカルタスクを設定することで、ある歴史的瞬間からモデルのフォワードパスやバックワードパスを何度もおこなうことができます。これにより、ある瞬間を何度も再生し、環境を探索するような効果が得られます。

同一の歴史的状態を探すために資源と時間を浪費することはありません。単純に過去のデータの定常性を利用しましょう。結局のところ、すべてのテストエージェントが、ある歴史的瞬間からスタートし、同じ数のステップ(ロウソク足)を通過したことに気づくのは簡単です。ただし、ストップアウトによりテストが中止された場合は例外となります。しかし、すべてのパスにおけるNステップは、常にある歴史的瞬間に対応します。これが、エージェント訓練の土台となるものです。

例によって、モデルの訓練はGCRLStudyActor.mq5 EAのTrain関数でおこなわれます。関数の最初に、例のデータベースを通過させることで定量化します。そして最初のループを構成し、最大ステップ数のパスを見つけます。具体的な通路は保存せず、ステップ数だけを保存して、訓練のために特定の歴史的瞬間をサンプリングする際に使用します。

void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   int total_steps = 0;
   for(int tr = 0; tr < total_tr; tr++)
     {
      if(Buffer[tr].Total > total_steps)
         total_steps = Buffer[tr].Total;
     }

次に、2つの入れ子ループのシステムを準備します。最初のループは、モデル訓練の反復回数に基づいています。このループの本体で、この訓練反復のための履歴モーメントを1つサンプリングします。ネストされたループの中で、利用可能なすべてのパスを繰り返し、その中にサンプリングされたステートがあるかどうかを確認します。

   uint ticks = GetTickCount();
//---
   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total_steps - 2));
      for(int tr = 0; tr < total_tr; tr++)
        {
         if(i >= (Buffer[tr].Total - 1))
            continue;

この条件が存在する場合、保存されたデータを使用してエージェントを訓練し、次のパスに進みます。

         State.AssignArray(Buffer[tr].States[i].state);
         float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
         Account.Clear();
         Account.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
         Account.Add(Buffer[tr].States[i].account[1] / PrevBalance);
         Account.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
         Account.Add(Buffer[tr].States[i].account[2]);
         Account.Add(Buffer[tr].States[i].account[3]);
         Account.Add(Buffer[tr].States[i].account[4] / PrevBalance);
         Account.Add(Buffer[tr].States[i].account[5] / PrevBalance);
         Account.Add(Buffer[tr].States[i].account[6] / PrevBalance);
         //---
         if(Account.GetIndex()>=0)
            Account.BufferWrite();
         if(!Actor.feedForward(GetPointer(State), 1, false,GetPointer(Account)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            break;
           }
         //---
      ActorResult = vector<float>::Zeros(NActions);
      ActorResult[Buffer[tr].Actions[i]] = Buffer[tr].Revards[i];
      Result.AssignArray(ActorResult);
      if(!Actor.backProp(Result, 0, NULL, 1, false,GetPointer(Account),GetPointer(Gradient)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         break;
        }
         if(GetTickCount() - ticks > 500)
           {
            string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Actor", 
                                       iter * 100.0 / (double)(Iterations),
                                       Actor.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

従って、個々の状態は、ローカルサブタスクの異なる定式化を持つパスの数という点で、エージェントによって再生されます。エージェントの行動には、環境の状態だけでなく、ローカルなサブタスクも考慮する必要があることを示すべきです。覚えておいでかもしれませんが、例題のデータベースを収集する際、各ステップでローカルタスクを完了できなかった場合のペナルティを追加しました。各パスにおいて、ある歴史的瞬間に対して異なる報酬が与えられ、それがパスのローカルなサブタスクに対応します。

エキスパートアドバイザーの残りのコードに変更はありません。この記事で使用されているすべてのプログラムの完全なコードは、添付ファイルでご覧ください。


3.検証

EAに関する作業を終えたら、モデルの訓練と得られた結果のテストに移ります。モデルの訓練パラメータは変更しません。前回と同様、モデルはEURUSD H1の履歴データで学習されます。指標のパラメータはデフォルトで使用されます。エージェントは、2023年の4ヶ月間で訓練しました。訓練の質と、2023年6月1日から18日の間に新しいデータに取り組むエージェントの能力を確認しました。

テスト結果は以下のスクリーンショットに示されています。ご覧のように、このモデルをテストして利益を上げることができました。残高チャートでは、成長の段階があり、平坦な動きがあります。下降がなくてよかったです。一般的に、12日間の取引で、プロフィットファクターは2.2、リカバリーファクターは1.47でした。EAは220回の取引をおこない、そのうち53%以上が黒字決算でした。さらに、平均的な収益ポジションは、平均的な不採算ポジションのほぼ2倍です。残念ながら、EAはロングポジションしか建てませんでした。すでに同様の効果に遭遇しています。適用されたアプローチではこの問題は解決しませんでした。

テストグラフ

テスト結果

ポジション保持時間

GCRL方式を採用するプラス面には、ポジションを維持する時間の短縮があります。テスト中、最大ポジション保持時間は21時間15分でした。ポジションの平均保有時間は5時間49分でした。覚えておいでかもしれませんが、ポジションを閉じる作業を完了しなかった場合、保有1時間ごとに累積利益の1/10のペナルティを設定しています。つまり、10時間拘束した時点で、違約金がポジションからの収入を上回ったのです。


結論

本稿では、GCRL(goal-conditioned reinforcement learning、目標条件付き強化学習)の手法を紹介しました。この方法の特徴は、ローカルなサブタスクとその達成に対する報酬を導入していることです。これにより、1つのグローバルなタスクをいくつかの小さなタスクに分割し、その達成に向けて一歩一歩進んでいくことができます。

このアプローチには多くの利点があります。タスクをより小さく、管理しやすい要素に分解することで、学習の複雑さを軽減します。これにより、意思決定プロセスが簡素化され、エージェントの訓練速度が向上します。

GCRLはエージェントの汎化能力の向上にも役立ちます。エージェントは、異なるローカルなサブタスクを解決するために学習するにつれて、異なるコンテキストで適用できるスキルと戦略のセットを開発します。

最後に、GCRLはエージェントの目標と目的を柔軟に定義することができます。ニーズや環境条件に応じて、ローカルのサブタスクを選択し、変更することができます。これにより、エージェントはさまざまな状況に適応し、自分のスキルを効果的に使って目標を達成することができます。

MQL5を使ってこの方法を実装しました。また、モデルを訓練し、訓練セット外のデータで訓練結果を確認しました。テスト結果は、まだ未解決の問題があることを示していました。特に、EAは一方向にのみポジションを建てましたが、同時に、これは、テスト中に利益を上げることを妨げるものでもありませんでした。

また、ポジション保持時間が短くなったことも特筆すべき点です。これは、エージェントがポジションを建てて閉じるという2つのローカルタスクの解決に取り組んでいることを示す。

一般的に、テスト結果は肯定的で、新しい解決策を見つけるためにこの方法を使用することができます。


参考文献リスト

  • Variational Empowerment as Representation Learning for Goal-Based Reinforcement Learning
  • ニューラルネットワークが簡単に(第43回):報酬機能なしでスキルを習得する
  • ニューラルネットワークが簡単に(第44回):ダイナミクスを意識したスキルの習得
  • ニューラルネットワークが簡単に(第45回):状態探索スキルの訓練

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

    # 名前 種類 詳細
    1 Research.mq5 EA コレクションEAの例
    StudyActor.mq5  EA エージェント訓練 EA
    3 Test.mq5 EA モデルテストEA
    4 Trajectory.mqh クラスライブラリ システム状態記述構造
    5 FQF.mqh クラスライブラリ 完全にパラメータ化されたモデルの作業を整理するためのクラスライブラリ
    6 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
    7 NeuroNet.cl コードベース OpenCLプログラムコードライブラリ
    8 VAE.mqh
    クラスライブラリ
    変分オートエンコーダ潜在層クラスライブラリ

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

    添付されたファイル |
    MQL5.zip (615.73 KB)
    ニューラルネットワークが簡単に(第47回):連続行動空間 ニューラルネットワークが簡単に(第47回):連続行動空間
    この記事では、エージェントのタスクの範囲を拡大します。訓練の過程には、どのような取引戦略にも不可欠な資金管理とリスク管理の側面も含まれます。
    ニューラルネットワークが簡単に(第45回):状態探索スキルの訓練 ニューラルネットワークが簡単に(第45回):状態探索スキルの訓練
    明示的な報酬関数なしに有用なスキルを訓練することは、階層的強化学習における主な課題の1つです。前回までに、この問題を解くための2つのアルゴリズムを紹介しましたが、環境調査の完全性についての疑問は残されています。この記事では、スキル訓練に対する異なるアプローチを示します。その使用は、システムの現在の状態に直接依存します。
    ニューラルネットワークが簡単に(第48回):Q関数値の過大評価を減らす方法 ニューラルネットワークが簡単に(第48回):Q関数値の過大評価を減らす方法
    前回は、連続的な行動空間でモデルを学習できるDDPG法を紹介しました。しかし、他のQ学習法と同様、DDPGはQ関数値を過大評価しやすくなります。この問題によって、しばしば最適でない戦略でエージェントを訓練することになります。この記事では、前述の問題を克服するためのいくつかのアプローチを見ていきます。
    ニューラルネットワークが簡単に(第44回):ダイナミクスを意識したスキルの習得 ニューラルネットワークが簡単に(第44回):ダイナミクスを意識したスキルの習得
    前回は、様々なスキルを学習するアルゴリズムを提供するDIAYN法を紹介しました。習得したスキルはさまざまな仕事に活用できます。しかし、そのようなスキルは予測不可能なこともあり、使いこなすのは難しくなります。この記事では、予測可能なスキルを学習するアルゴリズムについて見ていきます。