ニューラルネットワークが簡単にできるようになった(その4)。リカレントネットワーク

11 1月 2021, 07:51
Dmitriy Gizlyk
0
140

目次


イントロダクション

これまでニューラルネットワークの研究を続けてきました。 以前、多層パーセプトロン畳み込みニューラルネットワークについて述べましたが、 これらはすべて、マルコフ過程の枠組みの中で静的なデータを扱うもので、後続のシステムの状態は現在の状態にのみ依存し、過去のシステムの状態には依存しません。 さて、今回はリカレント・ニューラル・ネットワークを検討することを提案します。 これは、時系列で動作するように設計された特殊なタイプのニューラルネットワークであり、この分野のリーダーと考えられています。


1. リカレントニューラルネットワークの特徴

以前に説明したニューラルネットワークのすべてのタイプは、あらかじめ決められた量のデータを使って動作します。 しかし、今回の場合、プライスチャートの分析データの理想的な量を決めるのは困難です。 異なるパターンが、異なる時間的インターバルで出現することがあります。 間隔自体も、必ずしも静的なものではなく、現状に応じて変化することがあります。 中にはレアなイベントもあるかもしれませんが、高確率で鍛えてくれるでしょう。 イベントが解析されたデータウィンドウ内にあると良いです。 分析された系列を超えて下落した場合、ニューラルネットワークは、たとえその瞬間に市場がこのイベントに対する反応を計算していたとしても、それを無視します。 解析されたウィンドウが増加すると、コンピューティングリソースの消費量が増加し、判断に多くの時間を必要とするようになります。

ニューラルネットワークにおけるリカレントニューロンは、時系列を扱う際にこの問題を解決するために提案されてきました。 これは、システムの現在の状態が同じニューロンの前の状態と一緒にニューロンに供給されるときに、ニューラルネットワークで短期記録を実装する試みです。 この手順は、ニューロンの出力時の値が(その前の状態を含む)すべての要因の影響を考慮に入れ、次のステップで「すべての知識」を未来の状態に転送するという前提に基づいています。 これは、これまでの経験や以前に実行した行動に基づいて行動する場合、今回の試みと似ています。 記録の持続時間と現在のニューロン状態への影響は、重みに依存します。

残念ながら、このような単純な解決策には欠点があります。 この方法では、短い時間間隔で「記録」を保存することができます。 信号の周期的な乗算を1未満の因子で行い、ニューロン活性化機能を適用することで、周期数が増えるにつれて信号が徐々に減衰していきます。 この問題を解決するために、Sepp HochreiterとJürgen Schmidhuberは1997年にLong Short-Term Memory (LSTM)アーキテクチャの使用を提案しました。 LTSMアルゴリズムは、重要なイベントが時間的に分離され、時間間隔で引き延ばされる時系列の分類・予測問題に対する最適解の一つと考えられています。

LSTMはニューロンとは言い難いです。 すでに3つの入力チャネルと3つの出力チャネルを持つニューラルネットワークです。 外部とのデータのやりとりは、2つのチャネル(入力用と出力用)のみを使用しています。 残りの4つのチャネルは、周期的な情報交換のためにペアで閉じられている(メモリ隠れた状態)。

LSTMブロックは、4つの完全に接続されたニューラル層によって相互接続された2つの主要なデータストリームを含んでいます。 すべてのニューラル層には、出力ストリームとメモリストリームのサイズに等しい数のニューロンが含まれています。

メモリデータストリームは、重要な情報を時間をかけて保存し、送信するために使用されます。 最初にゼロ値で初期化され、ニューラルネットワークの動作中に埋められます。 これは、知識を持たずに生まれてきて、生涯にわたって学習する人間に例えることができます。

隠し状態ストリームは、出力システムの状態を経時的に送信するためのものです。 データチャネルのサイズは、"メモリ "データチャネルと同じです。

入力データおよび出力スタータチャネルは、外部との情報交換を目的としています。

3つのデータストリームがアルゴリズムに供給されます。

  • 入力データには、現在のシステムの状態が記述されています。
  • メモリ隠れた状態を前の状態から受信します。

アルゴリズムの最初に、入力データ隠れた状態からの情報が単一のデータ配列に結合され、それがLSTMの4つの隠れたニューラル層すべてに供給されます。 

第1のニューロン層である「フォゲットゲート」は、メモリ内の受信データのうち、どのデータを忘れてもよいか、どのデータを記録しておくべきかを決定する。 シグモイド活性化機能を持つ完全接続型ニューラル層として実装されています。 層のニューロンの数は、メモリストリームのメモリセルの数に対応します。 層の各ニューロンは、入力時に入力データと隠れた状態ストリームの合計配列を受け取り、0(完全に忘れる)から1(メモリに保存する)までの範囲の数値を出力します。 ニューラル層の出力データとメモリストリームの要素別積は、補正されたメモリを返します。

次のステップでは、アルゴリズムは、このステップで得られたデータのうち、どのデータをメモリに格納すべきかを決定します。 このためには、以下の2つのニューラル層が使用されます。

  • 新しいコンテンツ - 双曲正接を活性化関数とする完全に接続されたニューロン層。 受信した情報を-1から1の範囲で正規化します。
  • インプットゲート - シグモイドを活性化関数として持つ完全に接続されたニューロン層。 フォゲットゲートと似ていて、どの新しいデータを記録するかを決定します。

新規コンテンツと入力ゲートの要素別積がメモリセルの値に加算されます。 これらの演算の結果、更新されたメモリ状態が得られ、それが次の反復サイクルへの入力となります。

メモリを更新した後、出力ストリームの値を生成する必要があります。 ここでは、フォゲットゲートやインプットゲートと同様に、アウトプットゲートを計算し、双曲正接を用いて現在のメモリ値を正規化します。 2つの受信データセットの要素毎の積は、LSTMから外部に出力される出力信号アレイを生成します。 同じデータ配列が次の反復サイクルに隠された状態ストリームとして渡されます。


2. リカレントネットワークトレーニングの原理

リカレントニューラルネットワークは、すでによく知られているバックプロパゲーションメソッドによって学習されます。 畳み込みニューラルネットワークの訓練と同様に、時間内のプロセスの周期性を多層パーセプトロンに分解します。 このようなパーセプトロンの各時間間隔は隠れ層として機能します。 しかし、このようなパーセプトロンの全層には1つの重みが使用されます。 したがって、重みを調整するには、全層のグラデーションの合計を取り、全層のグラデーションの合計に対して、一度重みのデルタを計算します。


3. リカレントニューラルネットワークの構築

LSTMブロックを使ってリカレント・ニューラル・ネットワークを構築します。 まず、CNeuronLSTMクラスの作成から始めましょう。 前回の記事で作成したクラスの継承構造を維持するために、CNeuronProofクラスの継承として新しいクラスを作成します。

class CNeuronLSTM    :  public CNeuronProof
  {
protected:
   CLayer            *ForgetGate;
   CLayer            *InputGate;
   CLayer            *OutputGate;
   CLayer            *NewContent;
   CArrayDouble      *Memory;
   CArrayDouble      *Input;
   CArrayDouble      *InputGradient;
   //---
   virtual bool      feedForward(CLayer *prevLayer);
   virtual bool      calcHiddenGradients(CLayer *&nextLayer);
   virtual bool      updateInputWeights(CLayer *&prevLayer);
   virtual bool      updateInputWeights(CLayer *gate, CArrayDouble *input_data);
   virtual bool      InitLayer(CLayer *layer, int numOutputs, int numOutputs);
   virtual CArrayDouble *CalculateGate(CLayer *gate, CArrayDouble *sequence);

public:
                     CNeuronLSTM(void);
                    ~CNeuronLSTM(void);
   virtual bool      Init(uint numOutputs,uint myIndex,int window, int step, int units_count);
   //---
   virtual CLayer    *getOutputLayer(void)  { return OutputLayer;  }
   virtual bool      calcInputGradients(CLayer *prevLayer) ;
   virtual bool      calcInputGradients(CNeuronBase *prevNeuron, uint index) ;
   //--- 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 defNeuronLSTM;   }
  };

親クラスには、出力ニューロンOutputLayerの層が含まれています。 アルゴリズムの動作に必要な4つのニューラル層を追加してみましょう。ForgetGateInputGateOutputGateNewContentです。 また、入力データ隠れた状態を組み合わせるために、"メモリ "データを格納するための3つの配列を追加し、入力データの誤差勾配も追加しました。 クラスメソッドの名前と機能は、先に検討したものに対応しています。 しかし、これらのコードには、アルゴリズムの動作に必要ないくつかの違いがあります。 主なメソッドをもう少し詳しく考えてみましょう。

3.1. クラスの初期化メソッド

クラスの初期化メソッドは、作成されるブロックに関する基本情報をパラメータとして受け取ります。 メソッドのパラメータ名は基底クラスから継承されていますが、現在ではいくつかのパラメータ名が異なる意味を持つようになっています。

  • numOutputs - 発信接続の数。 これらは、完全に接続された層がLSTMブロック層に続く場合に使用されます。
  • myIndex - レイヤー内のニューロンのインデックス。 ブロック識別に使用されます。
  • window - 入力データチャネルのサイズ.
  • step - 使用されません。
  • units_count - 出力チャネルの幅とブロックの隠れ層のニューロンの数. ブロックのすべてのニューロン層には、同じ数のニューロンが含まれています。 

bool CNeuronLSTM::Init(uint numOutputs,uint myIndex,int window,int step,int units_count)
  {
   if(units_count<=0)
      return false;
//--- Init Layers
   if(!CNeuronProof::Init(numOutputs,myIndex,window,step,units_count))
      return false;
   if(!InitLayer(ForgetGate,units_count,window+units_count))
      return false;
   if(!InitLayer(InputGate,units_count,window+units_count))
      return false;
   if(!InitLayer(OutputGate,units_count,window+units_count))
      return false;
   if(!InitLayer(NewContent,units_count,window+units_count))
      return false;
   if(!Memory.Reserve(units_count))
      return false;
   for(int i=0; i<units_count; i++)
      if(!Memory.Add(0))
         return false;
//---
   return true;
  }

このメソッドの内部では、まず、ブロックの各ニューロン層に少なくとも1つのニューロンが作成されていることを確認します。 そして、基底クラスの対応するメソッドを呼び出します。 メソッドが正常に終了したら、ブロックの隠れレイヤーを初期化し、レイヤーごとに繰り返す操作は別のメソッドInitLayerで提供されます。 ニューラル層の初期化が完了すると、メモリアレイはゼロ値で初期化されます。

InitLayerニューラル層初期化メソッドは、初期化されたニューラル層のオブジェクトへのポインタ、層内のニューロンの数、発信接続の数をパラメータとして受け取ります。 メソッドの先頭で、受信したポインタの有効性をチェックします。 ポインタが無効な場合は、ニューラルレイヤークラスの新しいインスタンスを作成します。 ポインタが有効であれば、ニューロンの層をクリアします。

bool CNeuronLSTM::InitLayer(CLayer *layer,int numUnits, int numOutputs)
  {
   if(CheckPointer(layer)==POINTER_INVALID)
     {
      layer=new CLayer(numOutputs);
      if(CheckPointer(layer)==POINTER_INVALID)
         return false;
     }
   else
      layer.Clear();

必要な数のニューロンでレイヤーを埋めます。 メソッドのいずれかの段階でエラーが発生した場合、falseの結果で関数を終了します。

   if(!layer.Reserve(numUnits))
      return false;
//---
   CNeuron *temp;
   for(int i=0; i<numUnits; i++)
     {
      temp=new CNeuron();
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      if(!temp.Init(numOutputs+1,i))
         return false;
      if(!layer.Add(temp))
         return false;
     }
//---
   return true;
  }

すべての繰り返しが正常に完了した後、trueの結果でメソッドを終了します。

3.2. フィードフォワード

フィードフォワードパスは、feedForwardメソッドで実装されています。 このメソッドは、前のニューラル層へのポインタをパラメータとして受け取ります。 メソッドの開始時に、受信したポインタの妥当性と、前の層のニューロンの利用可能性をチェックします。 また、入力データに使用されている配列の妥当性も確認してください。 オブジェクトが作成されていない場合は、クラスの新しいインスタンスを作成します。 オブジェクトが既に存在する場合は、配列をクリアします。

bool CNeuronLSTM::feedForward(CLayer *prevLayer)
  {
   if(CheckPointer(prevLayer)==POINTER_INVALID || prevLayer.Total()<=0)
      return false;
   CNeuronBase *temp;
   CConnection *temp_con;
   if(CheckPointer(Input)==POINTER_INVALID)
     {
      Input=new CArrayDouble();
      if(CheckPointer(Input)==POINTER_INVALID)
         return false;
     }
   else
      Input.Clear();

次に、現在のシステムの状態に関するデータと、前回の時間間隔での状態に関するデータを組み合わせて、一つの入力データ配列入力とします。 

   int total=prevLayer.Total();
   if(!Input.Reserve(total+OutputLayer.Total()))
      return false;
   for(int i=0; i<total; i++)
     {
      temp=prevLayer.At(i);
      if(CheckPointer(temp)==POINTER_INVALID || !Input.Add(temp.getOutputVal()))
         return false;
     }
   total=OutputLayer.Total();
   for(int i=0; i<total; i++)
     {
      temp=OutputLayer.At(i);
      if(CheckPointer(temp)==POINTER_INVALID || !Input.Add(temp.getOutputVal()))
         return false;
     }
   int total_data=Input.Total();

ゲートの値を計算します。 初期化と同様に、ゲートごとに繰り返される操作を別のCalculateGateメソッドに移動させます。 このメソッドを呼び出して、処理されたゲートと初期データ配列へのポインタを入力します。

//--- Calculated forget gate
   CArrayDouble *forget_gate=CalculateGate(ForgetGate,Input);
   if(CheckPointer(forget_gate)==POINTER_INVALID)
      return false;
//--- Calculated input gate
   CArrayDouble *input_gate=CalculateGate(InputGate,Input);
   if(CheckPointer(input_gate)==POINTER_INVALID)
      return false;
//--- Calculated output gate
   CArrayDouble *output_gate=CalculateGate(OutputGate,Input);
   if(CheckPointer(output_gate)==POINTER_INVALID)
      return false;

受信データを計算し、new_content配列に正規化します。

//--- Calculated new content
   CArrayDouble *new_content=new CArrayDouble();
   if(CheckPointer(new_content)==POINTER_INVALID)
      return false;
   total=NewContent.Total();
   for(int i=0; i<total; i++)
     {
      temp=NewContent.At(i);
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      double val=0;
      for(int c=0; c<total_data; c++)
        {
         temp_con=temp.Connections.At(c);
         if(CheckPointer(temp_con)==POINTER_INVALID)
            return false;
         val+=temp_con.weight*Input.At(c);
        }
      val=TanhFunction(val);
      temp.setOutputVal(val);
      if(!new_content.Add(val))
         return false;
     }

最後に、すべての中間計算が終わったら、「メモリ」の配列を計算し、出力データを決定します。

//--- Calculated output sequences
   for(int i=0; i<total; i++)
     {
      double value=Memory.At(i)*forget_gate.At(i)+new_content.At(i)*input_gate.At(i);
      if(!Memory.Update(i,value))
         return false;
      temp=OutputLayer.At(i);
      value=TanhFunction(value)*output_gate.At(i);
      temp.setOutputVal(value);
     }

そして、中間データ配列を削除し、trueでメソッドを終了します。

   delete forget_gate;
   delete input_gate;
   delete new_content;
   delete output_gate;
//---
   return true;
  }

上記のCalculateGateメソッドでは、重みの行列に初期データベクトルを乗算した後、シグモイド活性化関数によるデータ正規化を行います。 このメソッドは、ニューラル層のオブジェクトと元のデータシーケンスへのポインタ2をパラメータとして受け取ります。 まず、受信したポインタの有効性を確認します。

CArrayDouble *CNeuronLSTM::CalculateGate(CLayer *gate,CArrayDouble *sequence)
  {
   CNeuronBase *temp;
   CConnection *temp_con;
   CArrayDouble *result=new CArrayDouble();
   if(CheckPointer(gate)==POINTER_INVALID)
      return NULL;

次に、すべてのニューロンを通るループを実装します。 

   int total=gate.Total();
   int total_data=sequence.Total();
   for(int i=0; i<total; i++)
     {
      temp=gate.At(i);
      if(CheckPointer(temp)==POINTER_INVALID)
        {
         delete result;
         return NULL;
        }


ニューロン・オブジェクトへのポインタの有効性を確認した後、ニューロンの重みをすべて入れ子にした ループを実行し、初期データ配列の対応する要素による重みの積の和を計算します。

      double val=0;
      for(int c=0; c<total_data; c++)
        {
         temp_con=temp.Connections.At(c);
         if(CheckPointer(temp_con)==POINTER_INVALID)
           {
            delete result;
            return NULL;
           }
         val+=temp_con.weight*(sequence.At(c)==DBL_MAX ? 1 : sequence.At(c));
        }

結果として得られた生成物の合計は、活性化関数に渡されます。 結果はニューロンの出力に書き込まれ、配列に追加されます。 レイヤー内のすべてのニューロンの反復処理に成功したら、結果の配列を返してメソッドを終了します。 いずれかの計算段階でエラーが発生した場合、このメソッドは空の値を返します。

      val=SigmoidFunction(val);
      temp.setOutputVal(val);
      if(!result.Add(val))
        {
         delete result;
         return NULL;
        }
     }
//---
   return result;
  }

3.3. 誤差勾配計算。

誤差勾配はcalcHiddenGradientsメソッドで計算されますが、これはパラメータでニューロンの次の層へのポインタを受け取るものです。 メソッドの開始時に、エラー・グラデーションのシーケンスを格納するために使用される先に作成されたオブジェクトと元のデータとの関連性をチェックします。 オブジェクトがまだ作成されていない場合は、新しいインスタンスを作成します。 オブジェクトが既に存在する場合は、配列をクリアします。 また、内部変数やクラスインスタンスも宣言します。

bool CNeuronLSTM::calcHiddenGradients(CLayer *&nextLayer)
  {
   if(CheckPointer(InputGradient)==POINTER_INVALID)
     {
      InputGradient=new CArrayDouble();
      if(CheckPointer(InputGradient)==POINTER_INVALID)
         return false;
     }
   else
      InputGradient.Clear();
//---
   int total=OutputLayer.Total();
   CNeuron *temp;
   CArrayDouble *MemoryGradient=new CArrayDouble();
   CNeuron *gate;
   CConnection *con;

次に、次のニューロン層から来たニューロンの出力層の誤差勾配を計算します。

   for(int i=0; i<total; i++)
     {
      temp=OutputLayer.At(i);
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      temp.setGradient(temp.sumDOW(nextLayer));
     }

結果として得られた勾配をLSTMのすべての内部ニューロン層に拡張します。

   if(CheckPointer(MemoryGradient)==POINTER_INVALID)
      return false;
   if(!MemoryGradient.Reserve(total))
      return false;
   for(int i=0; i<total; i++)
     {
      temp=OutputLayer.At(i);
      gate=OutputGate.At(i);
      if(CheckPointer(gate)==POINTER_INVALID)
         return false;
      double value=temp.getGradient()*gate.getOutputVal();
      value=TanhFunctionDerivative(Memory.At(i))*value;
      if(i>=MemoryGradient.Total())
        {
         if(!MemoryGradient.Add(value))
            return false;
        }
      else
        {
         value=MemoryGradient.At(i)+value;
         if(!MemoryGradient.Update(i,value))
            return false;
        }
      gate.setGradient(gate.getOutputVal()!=0 && temp.getGradient()!=0 ? temp.getGradient()*temp.getOutputVal()*SigmoidFunctionDerivative(gate.getOutputVal())/gate.getOutputVal() : 0);
      //--- Calcculated gates and new content gradients
      gate=ForgetGate.At(i);
      if(CheckPointer(gate)==POINTER_INVALID)
         return false;
      gate.setGradient(gate.getOutputVal()!=0 && value!=0? value*SigmoidFunctionDerivative(gate.getOutputVal()) : 0);
      gate=InputGate.At(i);
      temp=NewContent.At(i);
      if(CheckPointer(gate)==POINTER_INVALID)
         return false;
      gate.setGradient(gate.getOutputVal()!=0 && value!=0 ? value*temp.getOutputVal()*SigmoidFunctionDerivative(gate.getOutputVal()) : 0);
      temp.setGradient(temp.getOutputVal()!=0 && value!=0 ? value*gate.getOutputVal()*TanhFunctionDerivative(temp.getOutputVal()) : 0);
     }

内側のニューラル層の勾配を計算した後、初期データのシーケンスに対する誤差勾配を計算します。

//--- Calculated input gradients
   int total_inp=temp.getConnections().Total();
   for(int n=0; n<total_inp; n++)
     {
      double value=0;
      for(int i=0; i<total; i++)
        {
         temp=ForgetGate.At(i);
         con=temp.getConnections().At(n);
         value+=temp.getGradient()*con.weight;
         //---
         temp=InputGate.At(i);
         con=temp.getConnections().At(n);
         value+=temp.getGradient()*con.weight;
         //---
         temp=OutputGate.At(i);
         con=temp.getConnections().At(n);
         value+=temp.getGradient()*con.weight;
         //---
         temp=NewContent.At(i);
         con=temp.getConnections().At(n);
         value+=temp.getGradient()*con.weight;
        }
      if(InputGradient.Total()>=n)
        {
         if(!InputGradient.Add(value))
            return false;
        }
      else
         if(!InputGradient.Update(n,value))
            return false;
     }

すべてのグラデーションを計算したら、不要なオブジェクトを削除し、trueでメソッドを終了します。

   delete MemoryGradient;
//---
   return true;
  }


理論的な部分では、シーケンスを時間内に展開し、各時間段階での誤差勾配を計算する必要があることを述べましたが、以下の点に注意してください。 これは、使用される訓練係数が1よりもはるかに小さいので、ここでは行われていません、そして、以前の時間間隔に対する誤差勾配の影響は、アルゴリズムの全体的な性能を向上させるために無視できるほど小さいでしょう。 

3.4. ウェイトを更新しました。

当然のことながら、誤差勾配を取得した後は、全てのLSTMニューラル層の重みを補正する必要があります。 このタスクはupdateInputWeightsメソッドで実装されており、パラメータで前のニューラル層へのポインタを受け取っています。 前のレイヤーへのポインタの入力は、継承構造を保持するためにのみ実装されていることに注意してください。

メソッドの開始時に、受信したポインタの有効性と初期データ配列の利用可能性をチェックします。 ポインタの検証が成功したら、内側のニューラル層の重みの調整に進みます。 ここでも、繰り返しの動作は別のupdateInputWeightsメソッドに移され、パラメータは特定のニューラル層と初期データ配列へのポインタを渡します。 ここで、ヘルパーメソッドはニューロン層ごとに逐次呼び出されます。

bool CNeuronLSTM::updateInputWeights(CLayer *&prevLayer)
  {
   if(CheckPointer(prevLayer)==POINTER_INVALID || CheckPointer(Input)==POINTER_INVALID)
      return false;
//---
   if(!updateInputWeights(ForgetGate,Input) || !updateInputWeights(InputGate,Input) || !updateInputWeights(OutputGate,Input)
      || !updateInputWeights(NewContent,Input))
     {
      return false;
     }
//---
   return true;
  }

updateInputWeights(CLayer *gate,CArrayDouble *input_data)メソッドで行われた操作を考えてみましょう。 メソッドの開始時に、パラメータで受け取ったポインタの有効性を確認し、内部変数を宣言します。

bool CNeuronLSTM::updateInputWeights(CLayer *gate,CArrayDouble *input_data)
  {
   if(CheckPointer(gate)==POINTER_INVALID || CheckPointer(input_data)==POINTER_INVALID)
      return false;
   CNeuronBase *neuron;
   CConnection *con;
   int total_n=gate.Total();
   int total_data=input_data.Total();

レイヤー内のすべてのニューロンとニューロン内の重みを反復処理するために入れ子になったループを配置し、重み行列を補正します。 重み調整の式は、CNeuron::updateInputWeights(CArrayObj *&prevLayer)で考えたものと同じです。 ただし、前回はニューロン接続を使って次の層と接続していましたが、今回はニューロン接続を使って前の層と接続していますので、ここでは前回作成した方法を使うことはできません。

   for(int n=0; n<total_n; n++)
     {
      neuron=gate.At(n);
      if(CheckPointer(neuron)==POINTER_INVALID)
         return false;
      for(int i=0; i<total_data; i++)
        {
         con=neuron.getConnections().At(i);
         if(CheckPointer(con)==POINTER_INVALID)
            return false;
         double data=input_data.At(i);
         con.weight+=con.deltaWeight=(neuron.getGradient()!=0 && data!=0 ? eta*neuron.getGradient()*(data!=DBL_MAX ? data : 1) : 0)+alpha*con.deltaWeight;
        }
     }
//---
   return true;
  }


重み行列を更新したら、trueでメソッドを終了します。

クラスを作成したら、CNeuronBase 基底クラスのディスパッチャが新しいクラスのインスタンスを正しく扱えるように、少し調整してみましょう。 すべてのメソッドと関数のフルコードは添付ファイルにあります。


4. テスト

新しく作成したLSTMブロックは、前回の記事で畳み込みネットワークのテストに使ったのと同じ条件でテストしました。 Fractal_LSTM Expert Advisorをテスト用に作成しました。 本質的には、前の記事と同じFractal_convです。 しかし、OnInit関数では、ネットワーク構造指定ブロックにおいて、畳み込み層とサブサンプル層が、4つのLSTMブロックの層に置き換えられています(畳み込みネットワークの4つのフィルタを類推して)。

      //---
      desc=new CLayerDescription();
      if(CheckPointer(desc)==POINTER_INVALID)
         return INIT_FAILED;
      desc.count=4;
      desc.type=defNeuronLSTM;
      desc.window=(int)HistoryBars*12;
      desc.step=(int)HistoryBars/2;
      if(!Topology.Add(desc))
         return INIT_FAILED;

その他、EAコードに変更はありません。 添付ファイルでEAのコードとクラス全体を検索してください。

もちろん、各LSTMブロックに4つの内部ニューラル層を使用していることや、アルゴリズム自体の複雑さが性能に影響しているため、このようなニューラルネットワークの速度は、以前に検討されていた畳み込みネットワークに比べてやや劣ります。 しかし、リカレントネットワークの二乗平均平方根の誤差ははるかに小さい。


リカレントニューラルネットワークの訓練の過程で、目標打率グラフは、顕著な、ほぼ直線的な上昇傾向を示しています。

予測されたフラクタルへの稀なポインタのみが価格チャート上に表示されます。 前のテストでは、価格チャートは予測ラベルだらけでした。

リカレントニューラルネットワークのテスト

結論

本稿では、リカレントニューラルネットワークのアルゴリズムを検討し、LSTMブロックを構築し、作成したニューラルネットワークの動作を実データを用いて検証しました。 以前に検討したタイプのニューラルネットワークと比較して、リカレントネットワークは、フィードフォワードパスと学習プロセスの両方において、より多くのリソースと労力を必要とします。 それにもかかわらず、彼らはより良い結果を示しており、それは実施されたテストによって確認されています。

リンク

  1. ニューラルネットワークが簡単に
  2. ニューラルネットワークが簡単に(後編)。ネットワークのトレーニングとテスト
  3. ニューラルネットワークが簡単に(その3)。コンボリューションネットワーク
  4. LSTMネットワークの理解

記事内で使用しているプログラム

# 名前 タイプ 詳細
1 Fractal.mq5   エキスパートアドバイザー  回帰ニューラルネットワーク(出力層に1ニューロン)を持つエキスパートアドバイザー
2 Fractal_2.mq5  エキスパートアドバイザー  分類ニューラルネットワーク(出力層に3つのニューロン)を持つエキスパートアドバイザー
3 NeuroNet.mqh  クラスライブラリ  ニューラルネットワーク(パーセプトロン)を作成するためのクラスのライブラリ
4 ractal_conv.mq5  エキスパートアドバイザー  畳み込みニューラルネットワーク(出力層に3つのニューロン)を用いたエキスパートアドバイザー
5 Fractal_LSTM.mq5   エキスパートアドバイザー  リカレント・ニューラル・ネットワーク(出力層に3つのニューロン)を用いたエキスパート・アドバイザー


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

添付されたファイル |
MQL5.zip (32.06 KB)
ニューラルネットワークが簡単にできるように(その3)。コンボリューションネットワーク ニューラルネットワークが簡単にできるように(その3)。コンボリューションネットワーク

ニューラルネットワークの話題の続きとして、畳み込み型ニューラルネットワークの考察を提案します。 この種のニューラルネットワークは、通常、視覚的なイメージの分析に適用されます。 本稿では、これらのネットワークの金融市場への応用について考察します。

継続的なウォークフォワード最適化(その8)。プログラムの改善と修正 継続的なウォークフォワード最適化(その8)。プログラムの改善と修正

本連載では、ユーザーや読者の皆様からのご意見・ご要望をもとに、プログラムを修正しています。 この記事では、オートオプティマイザーの新バージョンを掲載しています。 このバージョンでは、要求された機能を実装し、他の改善点を提供しています。

パターン検索への総当たり攻撃アプローチ パターン検索への総当たり攻撃アプローチ

本稿では、市場パターンを検索し、特定されたパターンに基づいてエキスパートアドバイザーを作成し、これらのパターンが有効であるかどうかを確認します。

DoEasyライブラリの時系列(第54部): 抽象基本指標の子孫クラス DoEasyライブラリの時系列(第54部): 抽象基本指標の子孫クラス

本稿では、基本抽象指標の子孫オブジェクトのクラスの作成について検討しています。このようなオブジェクトは、指標EAを作成し、さまざまな指標と価格のデータ値統計を収集および取得する機能へのアクセスを備えています。また、プログラムで作成された各指標のプロパティとデータにアクセスできる指標オブジェクトコレクションを作成します。