English Русский 中文 Español Deutsch Português
preview
ニューラルネットワークが簡単に(第5回): OPENCLでのマルチスレッド計算

ニューラルネットワークが簡単に(第5回): OPENCLでのマルチスレッド計算

MetaTrader 5トレーディングシステム | 18 1月 2021, 08:41
716 0
Dmitriy Gizlyk
Dmitriy Gizlyk

目次


イントロダクション

以前の記事では、ニューラルネットワークの実装のいくつかのタイプについて述べてきました。 ご覧のように、ニューラルネットワークは、同じ種類のニューロンを大量に使って構築されており、その中で同じ操作が行われています。 しかし、ネットワークが持つニューロンの数が多ければ多いほど、消費するコンピューティングリソースも多くなります。 その結果、隠れ層に1つのニューロンを追加すると、前の層と次の層のすべてのニューロンとの接続を学習する必要があるため、ニューラルネットワークを訓練するのに必要な時間は指数関数的に増加します。 ニューラルネットワークの訓練時間を短縮する方法があります。 現代のコンピュータのマルチスレッド機能は、複数のニューロンを同時に計算することを可能にしています。 スレッド数の増加により時間が大幅に短縮されます。


1. マルチスレッドコンピューティングはMQL5でどのように構成されているか

メタトレーダー5のターミナルはマルチスレッドアーキテクチャを採用しています。 ターミナル内の糸の分布は厳しく規制されています。 ドキュメンテーションによると、スクリプトやエキスパートアドバイザーは個別のスレッドで立ち上げられています。 インジケータについては、各シンボルごとに個別のスレッドが用意されています。 ティック処理と履歴の同期は、インジケータのあるスレッドで行います。 これは、ターミナルがエキスパートアドバイザに対して1つのスレッドしか割り当てないことを意味します。 いくつかの計算はインジケータで行うことができ、それは追加のスレッドを提供します。 しかし、インジケータの計算が過剰になると、ティックデータの処理に関連したターミナルの動作が遅くなり、相場のコントロールが効かなくなる可能性があります。 このような状況は、EAのパフォーマンスに悪影響を及ぼす可能性があります。

しかし、解決策はあります。 メタトレーダー5の開発者は、サードパーティ製のDLLを使用する機能を提供しています。 マルチスレッドアーキテクチャ上でダイナミックライブラリを作成すると、ライブラリに実装された操作のマルチスレッド化が自動的に行われます。 ここでは、ライブラリとのデータ交換を伴うEA操作は、エキスパートアドバイザのメインスレッドに残っています。

2つ目の選択肢は、OpenCL技術を使用することです。 この場合、標準的な手段を用いて、当該技術によってサポートされるプロセッサ上とビデオカード上の両方でマルチスレッドコンピューティングを整理することができます。 このオプションでは、プログラムコードは使用するデバイスに依存しません。 このサイトには、OpenCL技術に関連した出版物が多数掲載されています。 特に、[ 記事5 ]や[記事6]では、この話題がよく取り上げられています。

そこで、OpenCLを使うことにしました。 第一に、この技術を利用する場合、ユーザーはターミナルを追加設定する必要がなく、サードパーティ製のDLLを使用するための許可を設定する必要がありません。 第2に、このようなエキスパートアドバイザーは、1つのEX5ファイルでターミナル間で転送することができます。 これにより、計算部をビデオカードに転送することができ、その機能はターミナルの動作中にアイドル状態になることが多いです。


2. ニューラルネットワークにおけるマルチスレッドコンピューティング

どの技術を使うかは選択しました。 あとは、計算をスレッドに分割する処理を決める必要があります。 フィードフォワードパス中の完全接続パーセプトロンアルゴリズムを覚えていますか? シグナルは、入力層から隠しレイヤーへ、そして出力層へと順次移動します。 計算は順次実行されなければならないので、各レイヤーにスレッドを割り当てる意味がありません。 レイヤーの計算は、前のレイヤーの結果を受信するまで開始できません。 ある層の個々のニューロンの計算は、その層の他のニューロンの計算結果には依存しません。 各ニューロンに別々のスレッドを割り当てて、あるレイヤーのすべてのニューロンを並列計算のために送ることができるということです。  

完全に接続されたパーセプトロン

1つのニューロンの動作にまで踏み込んで、入力値の重み係数による積の計算を並列化することも考えられます。 しかし、結果として得られた値のさらなる集計と活性化関数値の計算は、1つのスレッドにまとめられています。 これらの操作をベクトル関数を使って1つのOpenCLカーネルで実装することにしました。

同様のアプローチは、フィードバックワードスレッドを分割するために使用されます。 実装は以下の通りです。

3. OpenCLによるマルチスレッドコンピューティングの実装

基本的なアプローチを選択したので、実装に進むことができます。 まずはカーネル(実行可能なOpenCL関数)の作成から始めてみましょう。 上記のロジックにしたがって、4つのカーネルを作成します。

3.1. フィードフォワードカーネル。

前回の記事で説明した方法と同様に、フィードフォワードパスカーネルフィードフォワードを作成してみましょう。

カーネルは各スレッドで動作する関数であることを忘れてはいけません。 カーネルを呼び出す際に、そのようなスレッドの数を設定します。 カーネル内の操作は、特定のループ内でネストされた操作です。ループの反復回数は、呼び出されたスレッドの数と同じです。したがって、フィードフォワードカーネルでは、別のニューロン状態を計算するための操作を指定することができ、メインプログラムからカーネルを呼び出すときにニューロンの数を指定することができます。

カーネルは、パラメータとして、重み行列、入力データの配列、出力データの配列、入力配列の要素数、活性化関数の種類への参照を受け取ります。 OpenCL の配列はすべて一次元であることに注意してください。 したがって、MQL5で重み係数に2次元配列を用いる場合、ここでは、2番目以降のニューロンのデータを読み取るために、初期位置のシフトを計算する必要があります。

__kernel void FeedForward(__global double *matrix_w,
                              __global double *matrix_i,
                              __global double *matrix_o,
                              int inputs, int activation)

カーネルの先頭で、計算されたニューロンのシーケンス番号を決定するスレッドのシーケンス番号を取得します。 ベクトル変数 inpweight を含むプライベートな (内部) 変数を宣言します。 また、ニューロンの重みへのシフトも定義してください。

  {
   int i=get_global_id(0);
   double sum=0.0;
   double4 inp, weight;
   int shift=(inputs+1)*i;

次に、入力された値の積の和を重みで求めるサイクルを整理します。 上述したように、4つの要素inpweightのベクトルを用いて、積の和を算出しました。 しかし、カーネルが受け取ったすべての配列が4の倍数になるわけではないので、欠落している要素はゼロの値で置き換える必要があります。 入力データ ベクトルの 1 つの "1" に注意してください - それはベイジアン バイアスの重みに対応します。

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

積和を求めた後、活性化関数を計算し、その結果を出力データ配列に書き込みます。

   switch(activation)
     {
      case 0:
        sum=tanh(sum);
        break;
      case 1:
        sum=pow((1+exp(-sum)),-1);
        break;
     }
   matrix_o[i]=sum;
  }

3.2. バックプロパゲーションカーネル。

エラー勾配を逆伝播させるために2つのカーネルを作成します。 最初のCaclOutputGradientの出力レイヤ誤差を計算します。 その論理は単純です。 得られた基準値は、活性化関数の値の範囲内で正規化されています。 そして、基準値と実際の値の差に活性化関数の微分を乗算します。 結果の値をグラデーション配列の対応するセルに書き込みます。

__kernel void CaclOutputGradient(__global double *matrix_t,
                                 __global double *matrix_o,
                                 __global double *matrix_ig,
                                 int activation)
  {
   int i=get_global_id(0);
   double temp=0;
   double out=matrix_o[i];
   switch(activation)
     {
      case 0:
        temp=clamp(matrix_t[i],-1.0,1.0)-out;
        temp=temp*(1+out)*(1-(out==1 ? 0.99 : out));
        break;
      case 1:
        temp=clamp(matrix_t[i],0.0,1.0)-out;
        temp=temp*(out==0 ? 0.01 : out)*(1-(out==1 ? 0.99 : out));
        break;
     }
   matrix_ig[i]=temp;
  }

第2のカーネルでは、隠れ層ニューロンCaclHiddenGradientの誤差勾配を計算します。 カーネルの構築は、前述のフィードフォワードカーネルと似ています。 ベクトル演算も使用しています。 違いは、フィードフォワードパスで前の層の出力値の代わりに次の層の勾配ベクトルを使用していることと、異なる重み行列を使用していることです。 また、活性化関数を計算するのではなく、得られた和に活性化関数の微分を乗算します。 カーネルコードは以下の通りです。 

__kernel void CaclHiddenGradient(__global double *matrix_w,
                              __global double *matrix_g,
                              __global double *matrix_o,
                              __global double *matrix_ig,
                              int outputs, int activation)
  {
   int i=get_global_id(0);
   double sum=0;
   double out=matrix_o[i];
   double4 grad, weight;
   int shift=(outputs+1)*i;
   for(int k=0;k<outputs;k+=4)
     {
      switch(outputs-k)
        {
         case 0:
           grad=(double4)(1,0,0,0);
           weight=(double4)(matrix_w[shift+k],0,0,0);
           break;
         case 1:
           grad=(double4)(matrix_g[k],1,0,0);
           weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],0,0);
           break;
         case 2:
           grad=(double4)(matrix_g[k],matrix_g[k+1],1,0);
           weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],matrix_w[shift+k+2],0);
           break;
         case 3:
           grad=(double4)(matrix_g[k],matrix_g[k+1],matrix_g[k+2],1);
           weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],matrix_w[shift+k+2],matrix_w[shift+k+3]);
           break;
         default:
           grad=(double4)(matrix_g[k],matrix_g[k+1],matrix_g[k+2],matrix_g[k+3]);
           weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],matrix_w[shift+k+2],matrix_w[shift+k+3]);
           break;
        }
      sum+=dot(grad,weight);
     }
   switch(activation)
     {
      case 0:
        sum=clamp(sum+out,-1.0,1.0);
        sum=(sum-out)*(1+out)*(1-(out==1 ? 0.99 : out));
        break;
      case 1:
        sum=clamp(sum+out,0.0,1.0);
        sum=(sum-out)*(out==0 ? 0.01 : out)*(1-(out==1 ? 0.99 : out));
        break;
     }
   matrix_ig[i]=sum;
  }

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

UpdateWeightsという重みを更新するための別のカーネルを作成してみましょう。 個々の重みを更新する手順は、1つのニューロン内の他の重みに依存せず、他のニューロンからの重みにも依存しません。 これにより、1つの層の全ニューロンの全重みを同時に並列計算するためのタスクを送信することができます。 この場合、スレッドの2次元空間内で1つのカーネルを実行し、1次元はニューロンのシリアル番号を示し、2次元はニューロン内の接続数を示します。 これはカーネルコードの最初の2行で示されており、2次元でスレッドIDを受信します。  

__kernel void UpdateWeights(__global double *matrix_w,
                                __global double *matrix_g,
                                __global double *matrix_i,
                                __global double *matrix_dw,
                                int inputs, double learning_rates, double momentum)
  {
   int i=get_global_id(0);
   int j=get_global_id(1);
   int wi=i*(inputs+1)+j; 
   double delta=learning_rates*matrix_g[i]*(j<inputs ? matrix_i[j] : 1) + momentum*matrix_dw[wi];
   matrix_dw[wi]=delta;
   matrix_w[wi]+=delta;
  };

次に、重みの配列の中で更新された重みのシフトを決定し、デルタ(変化)を計算し、その結果の値をデルタの配列に追加して、現在の重みに追加します。

すべてのカーネルは別のファイルNeuroNet.clに置かれ、メインプログラムへのリソースとして接続されます。

#resource "NeuroNet.cl" as string cl_program

3.4. メインプログラムのクラスを作成します。

カーネルを作成したら、MQL5に戻り、メインプログラムコードの操作を始めましょう。 メインプログラムとカーネル間のデータは、1次元配列のバッファを通して交換されます(これは記事 [5] で説明されています)。 このようなバッファをメインプログラム側で整理するために、CBufferDoubleクラスを作成してみましょう。 このクラスは、OpenCLで作業するためのクラスのオブジェクトへの参照と、OpenCLで作成したときに受け取るバッファのインデックスを含んでいます。 

class CBufferDouble     :  public CArrayDouble
  {
protected:
   COpenCLMy         *OpenCL;
   int               m_myIndex;           
public:
                     CBufferDouble(void);
                    ~CBufferDouble(void);
//---
   virtual bool      BufferInit(uint count, double value);
   virtual bool      BufferCreate(COpenCLMy *opencl);
   virtual bool      BufferFree(void);
   virtual bool      BufferRead(void);
   virtual bool      BufferWrite(void);
   virtual int       GetData(double &values[]);
   virtual int       GetData(CArrayDouble *values);
   virtual int       GetIndex(void)                        {  return m_myIndex;      }
//---
   virtual int       Type(void)                      const { return defBufferDouble; }
  };

OpenCL バッファの作成時にそのハンドルが返されることに注意してください。 このハンドルは、COpenCLクラスのm_buffers配列に格納されます。 m_myIndex変数には、指定された配列のインデックスのみが格納されます。 これは、COpenCLクラスの操作全体が、カーネルやバッファハンドルではなく、そのようなインデックスの指定を使用するためです。 また、COpenCLクラスの操作アルゴリズムは、使用されるバッファの数を最初に指定し、特定のインデックスを持つバッファをさらに作成する必要があることにも注意してください。 今回の場合は、ニューラルレイヤーを作成する際に動的にバッファを追加します。 そのため、COpenCLMyクラスはCOpenCLから派生しています。 このクラスには、追加のメソッドが1つだけ含まれています。 そのコードは添付ファイルにあります。

バッファを操作するための以下のメソッドがCBufferDoubleクラスに作成されています。

  • BufferInit - 指定された値でバッファ配列を初期化.
  • BufferCreate - OpenCL でバッファを作成
  • BufferFree - OpenCL でバッファを削除
  • BufferRead - OpenCL バッファから配列にデータを読み込み
  • BufferWrite - 配列から OpenCL バッファにデータを書き込む
  • GetData - 要求に応じて配列データを取得する これは、配列と CArrayDouble クラスにデータを返すための 2 つのバリアントで実装されています。
  • GetIndex - バッファのインデックスを返す

すべてのメソッドのアーキテクチャは非常にシンプルで、それらのコードは1-2行で行われます。 すべてのメソッドのフルコードは、以下の添付ファイルで提供されています。

3.5. OpenCLで作業するためのベース・ニューロン・クラスを作成します。

ここでは、主な追加機能と演算アルゴリズムを含む CNeuronBaseOCL クラスを考えてみましょう。 完全に接続されたニューラル層全体の作業が含まれているため、作成されたオブジェクトをニューロンと名付けることは困難です。 先に検討した畳み込み層やLSTMブロックについても同様です。 しかし、このアプローチでは、以前に構築されたニューラルネットワークアーキテクチャを保存することができます。

ClassCNeuronBaseOCLには、COpenCLMy クラス・オブジェクトへのポインタと 4 つのバッファが含まれています。

class CNeuronBaseOCL    :  public CObject
  {
protected:
   COpenCLMy         *OpenCL;
   CBufferDouble     *Output;
   CBufferDouble     *Weights;
   CBufferDouble     *DeltaWeights;
   CBufferDouble     *Gradient;

また、学習係数と運動量係数、層内のニューロンの序数、活性化関数の種類も宣言します。

   const double      eta;
   const double      alpha;
//---
   int               m_myIndex;
   ENUM_ACTIVATION   activation;

保護されたブロックに、フィードフォワード、隠れ勾配計算、重み行列の更新の3つのメソッドを追加します。

   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcHiddenGradients(CNeuronBaseOCL *NeuronOCL);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);

公開ブロックでは、クラス・コンストラクタとデストラクタ、ニューロンの初期化メソッド、活性化関数の指定メソッドを宣言します。

public:
                     CNeuronBaseOCL(void);
                    ~CNeuronBaseOCL(void);
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons);
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) {  activation=value; }

ニューロンからのデータへの外部アクセスのために、バッファのインデックスを取得するメソッド(カーネルを呼び出すときに使用されます)と、バッファから現在の情報を配列の形で受け取るメソッドを宣言します。 また、ニューロンの数や活性化機能をポーリングする方法も追加してください。

   virtual int       getOutputIndex(void)          {  return Output.GetIndex();        }
   virtual int       getGradientIndex(void)        {  return Gradient.GetIndex();      }
   virtual int       getWeightsIndex(void)         {  return Weights.GetIndex();       }
   virtual int       getDeltaWeightsIndex(void)    {  return DeltaWeights.GetIndex();  }
//---
   virtual int       getOutputVal(double &values[])   {  return Output.GetData(values);      }
   virtual int       getOutputVal(CArrayDouble *values)   {  return Output.GetData(values);  }
   virtual int       getGradient(double &values[])    {  return Gradient.GetData(values);    }
   virtual int       getWeights(double &values[])     {  return Weights.GetData(values);     }
   virtual int       Neurons(void)                    {  return Output.Total();              }
   virtual ENUM_ACTIVATION Activation(void)           {  return activation;                  }

そして、もちろん、フィードフォワードパス、誤差勾配計算、重み行列の更新のためのディスパッチ方法を作成します。 データを保存したり、読み込んだりするための仮想関数の書き換えを忘れないようにしましょう。 

   virtual bool      feedForward(CObject *SourceObject);
   virtual bool      calcHiddenGradients(CObject *TargetObject);
   virtual bool      calcOutputGradients(CArrayDouble *Target);
   virtual bool      updateInputWeights(CObject *SourceObject);
//---
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual int       Type(void)        const                      {  return defNeuronBaseOCL;                  }
  };

メソッドを構築するためのアルゴリズムを考えてみましょう。 クラスのコンストラクタとデストラクタはわりとシンプルです。 このコードは添付ファイルにあります。 クラスの初期化関数を見てみましょう。 このメソッドは、次の層のニューロンの数、ニューロンの序数、COpenCLMy クラス・オブジェクトへのポインタ、作成するニューロンの数をパラメータとして受け取ります。

このメソッドは、パラメータで COpenCLMy クラス・オブジェクトへのポインタを受け取り、クラス内のオブジェクトをインスタンス化しないことに注意してください。 これにより、EA操作中にCOpenCLMyオブジェクトのインスタンスが1つだけ使用されるようになります。 すべてのカーネルとデータバッファは1つのオブジェクトに作成されるので、ニューラルネットワークのレイヤー間でデータを渡す時間を無駄にする必要はありません。 同じデータバッファに直接アクセスできるようになります。

メソッドの開始時に、COpenCLMyクラス・オブジェクトへのポインタの有効性を確認し、少なくとも1つのニューロンが作成されていることを確認します。 次に、バッファオブジェクトのインスタンスを作成し、配列を初期値で初期化し、OpenCLでバッファを作成します。 「出力」バッファのサイズは、作成するニューロンの数と等しく、グラディエントバッファのサイズは1要素大きいです。 重み行列とそのデルタバッファのサイズは、グラデーションバッファのサイズと次の層のニューロンの数との積に等しいです。 本製品は出力レイヤに対して "0 "となるため、このレイヤに対してバッファは作成されません。

bool CNeuronBaseOCL::Init(uint numOutputs,uint myIndex,COpenCLMy *open_cl,uint numNeurons)
  {
   if(CheckPointer(open_cl)==POINTER_INVALID || numNeurons<=0)
      return false;
   OpenCL=open_cl;
//---
   if(CheckPointer(Output)==POINTER_INVALID)
     {
      Output=new CBufferDouble();
      if(CheckPointer(Output)==POINTER_INVALID)
         return false;
     }
   if(!Output.BufferInit(numNeurons,1.0))
      return false;
   if(!Output.BufferCreate(OpenCL))
      return false;
//---
   if(CheckPointer(Gradient)==POINTER_INVALID)
     {
      Gradient=new CBufferDouble();
      if(CheckPointer(Gradient)==POINTER_INVALID)
         return false;
     }
   if(!Gradient.BufferInit(numNeurons+1,0.0))
      return false;
   if(!Gradient.BufferCreate(OpenCL))
      return false;
//---
   if(numOutputs>0)
     {
      if(CheckPointer(Weights)==POINTER_INVALID)
        {
         Weights=new CBufferDouble();
         if(CheckPointer(Weights)==POINTER_INVALID)
            return false;
        }
      int count=(int)((numNeurons+1)*numOutputs);
      if(!Weights.Reserve(count))
         return false;
      for(int i=0;i<count;i++)
        {
         double weigh=(MathRand()+1)/32768.0-0.5;
         if(weigh==0)
            weigh=0.001;
         if(!Weights.Add(weigh))
            return false;
        }
      if(!Weights.BufferCreate(OpenCL))
         return false;
   //---
      if(CheckPointer(DeltaWeights)==POINTER_INVALID)
        {
         DeltaWeights=new CBufferDouble();
         if(CheckPointer(DeltaWeights)==POINTER_INVALID)
            return false;
        }
      if(!DeltaWeights.BufferInit(count,0))
         return false;
      if(!DeltaWeights.BufferCreate(OpenCL))
         return false;
     }
//---
   return true;
  }

feedForward ディスパッチャー メソッドは、CNeuronBase クラスの同じ名前のメソッドに似ています。 ここで指定するニューロンの種類は 1 つだけですが、後で追加できる種類は増えます。

bool CNeuronBaseOCL::feedForward(CObject *SourceObject)
  {
   if(CheckPointer(SourceObject)==POINTER_INVALID)
      return false;
//---
   CNeuronBaseOCL *temp=NULL;
   switch(SourceObject.Type())
     {
      case defNeuronBaseOCL:
        temp=SourceObject;
        return feedForward(temp);
        break;
     }
//---
   return false;
  }

OpenCL カーネルは、feedForward(CNeuronBaseOCL *NeuronOCL)メソッドで直接呼び出されます。 メソッドの開始時に、COpenCLMyクラスオブジェクトへのポインタと、受信したニューラルネットワークの前の層へのポインタの妥当性をチェックします。

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

トレッド空間の一次元性を示し、必要なスレッドの数をニューロンの数と同じに設定します。

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

次に、使用するデータバッファへのポインタとカーネル操作のための引数を設定します。

   OpenCL.SetArgumentBuffer(def_k_FeedForward,def_k_ff_matrix_w,NeuronOCL.getWeightsIndex());
   OpenCL.SetArgumentBuffer(def_k_FeedForward,def_k_ff_matrix_i,NeuronOCL.getOutputIndex());
   OpenCL.SetArgumentBuffer(def_k_FeedForward,def_k_ff_matrix_o,Output.GetIndex());
   OpenCL.SetArgument(def_k_FeedForward,def_k_ff_inputs,NeuronOCL.Neurons());
   OpenCL.SetArgument(def_k_FeedForward,def_k_ff_activation,(int)activation);

その後、カーネルを呼び出します。

   if(!OpenCL.Execute(def_k_FeedForward,1,global_work_offset,global_work_size))
      return false;

COpenCL::Executeメソッドはカーネルを起動せず、キューに入れるだけです。 実行自体は、カーネルの結果を読み込もうとした時に発生します。 そのため、メソッドを終了する前に処理結果を配列にロードしなければなりません。

   Output.BufferRead();
//---
   return true;
  }

他のカーネルを起動するための方法は、上記のアルゴリズムに似ています。 すべてのメソッドとクラスのフルコードは添付ファイルにあります。

3.6. CNetクラスでの追加

必要なクラスがすべて作成できたら、メインのニューラルネットワークのCNetクラスを調整してみましょう。

クラスのコンストラクタでは、COpenCLMyクラスのインスタンスの作成と初期化を追加する必要があります。 デストラクタ内のクラスオブジェクトを削除することを忘れないでください。 

   opencl=new COpenCLMy();
   if(CheckPointer(opencl)!=POINTER_INVALID && !opencl.Initialize(cl_program,true))
      delete opencl;

また、コンストラクタでは、ニューロンをレイヤーに追加するブロックの中に、先に作成したCNeuronBaseOCLクラスのオブジェクトを作成して初期化するコードを追加します。

      if(CheckPointer(opencl)!=POINTER_INVALID)
        {
         CNeuronBaseOCL *neuron_ocl=NULL;
         switch(desc.type)
           {
            case defNeuron:
            case defNeuronBaseOCL:
              neuron_ocl=new CNeuronBaseOCL();
              if(CheckPointer(neuron_ocl)==POINTER_INVALID)
                {
                 delete temp;
                 return;
                }
              if(!neuron_ocl.Init(outputs,0,opencl,desc.count))
                {
                 delete temp;
                 return;
                }
              neuron_ocl.SetActivationFunction(desc.activation);
              if(!temp.Add(neuron_ocl))
                {
                 delete neuron_ocl;
                 delete temp;
                 return;
                }
              neuron_ocl=NULL;
              break;
            default:
              return;
              break;
           }
        }

さらに、コンストラクタにOpenCLでのカーネルの作成を追加しました。

   if(CheckPointer(opencl)==POINTER_INVALID)
      return;
//--- create kernels
   opencl.SetKernelsCount(4);
   opencl.KernelCreate(def_k_FeedForward,"FeedForward");
   opencl.KernelCreate(def_k_CaclOutputGradient,"CaclOutputGradient");
   opencl.KernelCreate(def_k_CaclHiddenGradient,"CaclHiddenGradient");
   opencl.KernelCreate(def_k_UpdateWeights,"UpdateWeights");

CNet::feedForwardメソッドでバッファにソースデータの書き込みを追加します。

     {
      CNeuronBaseOCL *neuron_ocl=current.At(0);
      double array[];
      int total_data=inputVals.Total();
      if(ArrayResize(array,total_data)<0)
         return false;
      for(int d=0;d<total_data;d++)
         array[d]=inputVals.At(d);
      if(!opencl.BufferWrite(neuron_ocl.getOutputIndex(),array,0,0,total_data))
         return false;
     }

また、新しく作成したクラスCNeuronBaseOCLの適切なメソッド呼び出しを追加します。

   for(int l=1; l<layers.Total(); l++)
     {
      previous=current;
      current=layers.At(l);
      if(CheckPointer(current)==POINTER_INVALID)
         return false;
      //---
      if(CheckPointer(opencl)!=POINTER_INVALID)
        {
         CNeuronBaseOCL *current_ocl=current.At(0);
         if(!current_ocl.feedForward(previous.At(0)))
            return false;
         continue;
        }

バックプロパゲーション プロセスでは、新しいメソッドCNet::backPropOCL を作成します。 そのアルゴリズムは、最初の記事で説明したメインメソッドCNet::backPropに似ています

void CNet::backPropOCL(CArrayDouble *targetVals)
  {
   if(CheckPointer(targetVals)==POINTER_INVALID || CheckPointer(layers)==POINTER_INVALID || CheckPointer(opencl)==POINTER_INVALID)
      return;
   CLayer *currentLayer=layers.At(layers.Total()-1);
   if(CheckPointer(currentLayer)==POINTER_INVALID)
      return;
//---
   double error=0.0;
   int total=targetVals.Total();
   double result[];
   CNeuronBaseOCL *neuron=currentLayer.At(0);
   if(neuron.getOutputVal(result)<total)
      return;
   for(int n=0; n<total && !IsStopped(); n++)
     {
      double target=targetVals.At(n);
      double delta=(target>1 ? 1 : target<-1 ? -1 : target)-result[n];
      error+=delta*delta;
     }
   error/= total;
   error = sqrt(error);
   recentAverageError+=(error-recentAverageError)/recentAverageSmoothingFactor;

   if(!neuron.calcOutputGradients(targetVals))
      return;;
//--- Calc Hidden Gradients
   CObject *temp=NULL;
   total=layers.Total();
   for(int layerNum=total-2; layerNum>0; layerNum--)
     {
      CLayer *nextLayer=currentLayer;
      currentLayer=layers.At(layerNum);
      neuron=currentLayer.At(0);
      neuron.calcHiddenGradients(nextLayer.At(0));
     }
//---
   CLayer *prevLayer=layers.At(total-1);
   for(int layerNum=total-1; layerNum>0; layerNum--)
     {
      currentLayer=prevLayer;
      prevLayer=layers.At(layerNum-1);
      neuron=currentLayer.At(0);
      neuron.updateInputWeights(prevLayer.At(0));
     }
  }

getResultメソッドにいくつかの小さな変更が加えられました。

   if(CheckPointer(opencl)!=POINTER_INVALID && output.At(0).Type()==defNeuronBaseOCL)
     {
      CNeuronBaseOCL *temp=output.At(0);
      temp.getOutputVal(resultVals);
      return;
     }

すべてのメソッドと関数のフルコードは添付ファイルにあります。

4. テスト

作成されたクラス操作は、前のテストで使用したのと同じ条件でテストされました。 Fractal_OCL EAは、以前に作成したFractal_2の完全なアナログです。 ニューラルネットワークのテストトレーニングは、H1時間枠のEURUSDペアで行われました。 ローソク足20本のデータをニューラルネットワークに入力した。 過去2年間のデータを用いてトレーニングを行った。 OpenCLをサポートしたCPUデバイス「Intel(R) Core(TM)2 Duo CPU T5750 @ 2.00GHz」で実験を行いました。

5時間27分のテストで、OpenCL技術を使用したEAは75のトレーニングエポックを実行しました。 これは、12,405本のキャンドルのエポックのために平均4分22秒でした。 OpenCL技術を使用していない同じエキスパートアドバイザでも、同じノートパソコンで同じニューラルネットワークアーキテクチャを使用した場合、1エポックあたり平均40分48秒の時間を費やします。 つまり、OpenCLを使うと学習プロセスが9.35倍速くなるということです。


結論

この論文では、ニューラルネットワークのマルチスレッド計算を整理するためにOpenCL技術を利用できる可能性を示しました。 テストでは、同じCPUでほぼ10倍の性能向上が示されています。 GPUを使用することで、アルゴリズムの性能をさらに向上させることが期待できます。この場合、互換性のあるGPUに計算を転送することで、エキスパートアドバイザのコードを変更する必要はありません。

一般的には、この方向性のさらなる発展が期待できます。


リンク

  1. ニューラルネットワークが簡単に
  2. ニューラルネットワークを簡単に作成(パート2):ネットワークのトレーニングとテスト
  3. ニューラルネットワークが容易に作り出した(パート3):畳み込みネットワーク
  4. ニューラルネットワークが簡単にできるようになった(その4)。リカレントネットワーク
  5. OpenCL:平行世界への橋
  6. OpenCL: ナイーブなプログラミングからより洞察力のあるプログラミングへ

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

# 名前 タイプ 詳細
1 Fractal_OCL.mq5  エキスパートアドバイザー OpenCL技術を用いた分類ニューラルネットワーク(出力層の3つのニューロン)を持つエキスパートアドバイザー
2 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
3 NeuroNet.cl コードベース OpenCL プログラムコードライブラリ


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

添付されたファイル |
MQL5.zip (396.86 KB)
ニューラルネットワークが簡単に(第6回): ニューラルネットワークの学習率を実験する ニューラルネットワークが簡単に(第6回): ニューラルネットワークの学習率を実験する
これまで、様々な種類のニューラルネットワークをその実装とともに考察してきました。 すべての場合において、ニューラルネットワークは、学習率を選択する必要があるグラディエントディーセント法を用いてトレーニングされました。 今回は、正しく選択されたレートの重要性とニューラルネットワーク学習への影響を例を用いて示したいと思います。
TDシーケンシャルと一連のMurray-Gannレベルを使用したチャートの分析 TDシーケンシャルと一連のMurray-Gannレベルを使用したチャートの分析
TDシーケンシャル(トーマス・デマークのシーケンシャル)は、価格変動のバランスの変化を示すのが得意です。これは、そのシグナルをレベル指標(Murreyレベルなど)と組み合わせると特に明白になります。本稿は、主に初心者や「聖杯」を見つけることができない人を対象としています。また、他のフォーラムでは見たことのないレベル構築の機能をいくつか提示するので、おそらく上級トレーダーにも役立つでしょう... 提案や合理的な批判は大歓迎です...
取引システムの開発と分析への最適なアプローチ 取引システムの開発と分析への最適なアプローチ
本稿では、資金を投資するためのシステムまたはシグナルを選択する際に使用する基準を示すとともに、取引システムの開発への最適なアプローチを説明し、外国為替取引におけるこの問題の重要性を強調します。
グリッドとマルチンゲール - それらは何でありどのように使用するか グリッドとマルチンゲール - それらは何でありどのように使用するか
本稿では、グリッドとマルチンゲールとは何か、そしてそれらに共通するものについて詳しく説明しようと思います。また、これらの戦略が実際にどれほど実行可能であるかの分析を試みます。本稿には、数学セクションと実用セクションがあります。