ニューラルネットワークが簡単に(第5回): OPENCLでのマルチスレッド計算
目次
- イントロダクション
- 1. マルチスレッドコンピューティングはMQL5でどのように構成されているか
- 2. ニューラルネットワークにおけるマルチスレッドコンピューティング
- 3. OpenCLによるマルチスレッドコンピューティングの実装
- 3.1. フィードフォワードカーネル
- 3.2. バックプロパゲーションカーネル
- 3.3. 重みの更新
- 3.4. メインプログラムのクラス作成
- 3.5. OpenCLで作業するためのベース・ニューロン・クラスの作成
- 3.6. CNetクラスでの追加
- 4. テスト
- 結論
- リンク
- 記事内で使用しているプログラム
イントロダクション
以前の記事では、ニューラルネットワークの実装のいくつかのタイプについて述べてきました。 ご覧のように、ニューラルネットワークは、同じ種類のニューロンを大量に使って構築されており、その中で同じ操作が行われています。 しかし、ネットワークが持つニューロンの数が多ければ多いほど、消費するコンピューティングリソースも多くなります。 その結果、隠れ層に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)
カーネルの先頭で、計算されたニューロンのシーケンス番号を決定するスレッドのシーケンス番号を取得します。 ベクトル変数 inp と weight を含むプライベートな (内部) 変数を宣言します。 また、ニューロンの重みへのシフトも定義してください。
{ int i=get_global_id(0); double sum=0.0; double4 inp, weight; int shift=(inputs+1)*i;
次に、入力された値の積の和を重みで求めるサイクルを整理します。 上述したように、4つの要素inpとweightのベクトルを用いて、積の和を算出しました。 しかし、カーネルが受け取ったすべての配列が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に計算を転送することで、エキスパートアドバイザのコードを変更する必要はありません。
一般的には、この方向性のさらなる発展が期待できます。
リンク
- ニューラルネットワークが簡単に
- ニューラルネットワークを簡単に作成(パート2):ネットワークのトレーニングとテスト
- ニューラルネットワークが容易に作り出した(パート3):畳み込みネットワーク
- ニューラルネットワークが簡単にできるようになった(その4)。リカレントネットワーク
- OpenCL:平行世界への橋
- OpenCL: ナイーブなプログラミングからより洞察力のあるプログラミングへ
記事内で使用しているプログラム
# | 名前 | タイプ | 詳細 |
---|---|---|---|
1 | Fractal_OCL.mq5 | エキスパートアドバイザー | OpenCL技術を用いた分類ニューラルネットワーク(出力層の3つのニューロン)を持つエキスパートアドバイザー |
2 | NeuroNet.mqh | クラスライブラリ | ニューラルネットワークを作成するためのクラスのライブラリ |
3 | NeuroNet.cl | コードベース | OpenCL プログラムコードライブラリ |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/8435
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索