English Русский 中文 Español Deutsch Português
preview
ニューラルネットワークが簡単に(第11部): GPTについて

ニューラルネットワークが簡単に(第11部): GPTについて

MetaTrader 5トレーディングシステム | 26 5月 2021, 15:06
1 771 0
Dmitriy Gizlyk
Dmitriy Gizlyk

目次

はじめに

GPTニューラルネットワークモデルは、2018年6月にOpenAIによって発表されてすぐに多くの言語テストで最良の結果を示しました。GDP-2は2019年に登場し、GPT-3は2020年5月に発表されました。これらのモデルは、関連するテキストを生成するニューラルネットワークの能力を実証しました。追加的な実験は音楽と画像を生成する能力に関するものでした。このようなモデルの主な欠点は、それらが関与するコンピューティングリソースに関連しています。最初のGPTを訓練するのには8つのGPUを搭載したマシンで1か月かかりました。この欠点は、新しい問題を解決するために事前訓練されたモデルを使用する可能性によって部分的に補うことができますが、モデルのサイズを考慮すると、機能を維持するにはかなりのリソースが必要です。


1. GPTモデルについて

概念的には、GPTモデルは、以前に検討されたTransformerに基づいて構築されています。主なアイデアは、事前に大量のデータに対して教師なしで訓練したモデルを比較的少量のラベル付きデータに対して微調整することです。

2段階で訓練する理由はモデルのサイズです。GPTのような最新の深層機械学習モデルには、最大で数億もの多数のパラメータが含まれます。このようなニューラルネットワークの訓練には膨大な訓練用の標本が必要です。教師あり学習を使用する場合、ラベル付き訓練セットの作成には大きな労働力がかかります。同時に、Web上には多くの異なるデジタル化されたラベルなしテキストがあり、教師なしモデルの訓練には最適です。ただし、教師なし学習の結果が教師あり学習より劣っていることがは統計によって示されているため、教師なし訓練の後で、ラベル付けされたデータの比較的小さな標本でモデルが微調整されます。

GPTは教師なし学習によって言語モデルを学習し、ラベル付きデータでさらに訓練されて特定のタスクに合わせたモデルが調整されます。したがって、事前訓練された1つのモデルを複製して微調整すればさまざまな言語タスクを実行できます。制限は教師なし学習の元のセットの言語に基づいています。 

実践により、このアプローチは広い範囲にわたる言語の問題で良い結果を生み出すことが示されています。たとえば、GPT-3モデルは、特定のトピックに関する一貫性のあるテキストを生成できます。ただし、指定されたモデルには1,750億のパラメータ、シーケンスが含まれており、570GBのデータセットで事前訓練されています。

GPTモデルは自然言語処理用に開発されたものですが、音楽や画像の生成タスクでもうまく機能しました。

理論的には、GPTモデルはデジタル化されたデータの任意のシーケンスで使用できます。唯一の前提条件は、教師なし事前学習のためのデータとリソースが十分であることです。

2. GPTと以前に検討されたTransformerの違い

以前に検討されたTransformerとGPTモデルの違いを考えてみましょう。まず、GPTモデルはデコーダのみを使用するため、エンコーダがありません。エンコーダがないので、モデルにはエンコーダ-デコーダのSelf-Attention内層がありません。次の図は、GPT Transformerブロックを示しています。 


従来のTransformerと同様に、GPTモデルのブロックは重なって構築されています。また、各ブロックには、Attentionメカニズム用の独自の重み行列と、全結合フィードフォワード層があります。ブロックの数によってモデルのサイズが決まります。ブロックスタックは非常に大きくなる可能性があります。GPT-1と最小のGPT-2(GPT-2 Small)にはブロックが12個のありますが、GPT-2 Extra Largeには48個、GPT-3には96個があります。

従来の言語モデルと同様、GPTではシーケンスでの先行する要素との関係を見つけることができるのみで、将来を見据えることはできません。ただし、Transformerとは異なり、GPTでは要素のマスキングを使用しません。代わりに、計算プロセスに変更を加えます。GPTは、後続の要素のスコア行列のAttention率をリセットします。

同時に、GPTは自己回帰モデルとして分類できます。反復ごとに1つのシーケンストークンが生成されて、結果のトークンは入力シーケンスに追加され、次の反復のためにモデルに入力されます。

従来のTransformerと同様、self-attentionメカニズム内のトークンごとに、クエリ、キー、値の3つのベクトルが生成されます。新しい反復ごとに入力シーケンスが1トークンだけ変化する自己回帰モデルでは、トークンごとにベクトルを再計算する必要はありません。したがって、GPTの各層は、シーケンスの新しい要素に対してのみベクトルを計算し、シーケンスの各要素に対してそれらを計算します。各Transformerブロックは、後で使用するためにそのベクトルを保存します。 

このアプローチにより、モデルは、最終的なトークンを受け取る前に、単語ごとにテキストを生成できます。 

もちろん、GPTモデルではMulti-Head Attentionメカニズムを使用します。


3. 実装

始める前に、アルゴリズムを簡単に繰り返します。

  1. トークンの入力シーケンスがTransformerブロックに供給されます。
  2. すべてのSelf-Attentionヘッドに対して1つのシーケンス。さらに、2〜5のアクションは、各Self-Attentionヘッドで同じです。

  3. トークンベクトルに、訓練中の重みWの対応する行列を乗算することにより、トークン(クエリ、キー、値)ごとに3つのベクトルが計算されます。

  4. クエリとキーを乗算することにより、シーケンス要素間の依存関係を決定します。この手順では、シーケンスの各要素の「クエリ」ベクトルに、シーケンスの現在の要素と以前のすべての要素のキーベクトルが乗算されます。

  5. 得られたAttentionスコアの行列は、各クエリのコンテキストでSoftMax関数を使用して正規化されます。シーケンスの後続の要素には、ゼロAttentionスコアが設定されます。
  6. 手順3と4の結果として、シーケンス内の要素の数に応じたサイズの正方行列スコアを取得します。各「クエリ」のコンテキスト内のすべての要素の合計は「1」です。 

  7. 正規化されたAttentionスコアにシーケンスの対応する要素の「値」ベクトルを乗算し、結果のベクトルを加算することにより、シーケンスの各要素のAttention補正値(Z)を取得します。

  8. 次に、すべてのAttentionヘッドの結果に基づいて、重み付けされたZベクトルを決定します。このために、すべてのAttentionヘッドからの修正された「値」ベクトルが連結されて単一のベクトルになり、訓練済みのW0行列が乗算されます。

  9. 結果のテンソルは入力シーケンスに追加され、正規化されます。

  10. Multi-Heads Self-Attentionメカニズムの後には、フィードフォワードブロックの2つの全結合層が続きます。最初の(隠れ)層には、ReLU活性化関数を使用した入力シーケンスの4倍のニューロンが含まれています。2番目の層の次元は入力シーケンスの次元と等しく、ニューロンは活性化関数を使用しません。

  11. 全結合層の結果は、FeedForwardブロックにフィードされるテンソルと合計されます。結果のテンソルは正規化されます。


3.1. モデルの新しいクラスの作成

モデルを実装するために、CNeuronBaseOCL基本クラスに基づいて新しいクラスCNeuronMLMHAttentionOCLを作成しましょう。意図的に一歩後退し、以前に作成されたAttentionクラスは使用しませんでした。これは、新しいMulti-Head Self-Attention作成原則を扱っているためです。以前の第10部で、CNeuronMHAttentionOCLクラスを作成しました。これは、4つのAttentionスレッドの順次再計算を提供します。スレッドの数はメソッドにハードコーディングされているため、スレッドの数を変更するには、クラスコードとそのメソッドの変更に関連して、かなりの労力が必要になります。

警告: 上記のように、GPTモデルは、同じ(変更不可能な)ハイパーパラメータを持つ同一のTransformerブロックのスタックを使用しますが、唯一の違いは、訓練される行列にあります。そのため、クラスの作成時に渡すことができるハイパーパラメータを使用してモデルを作成できるようにする多層ブロックを作成することにしました。これには、スタック内のTransformerブロックの繰り返し回数が含まれます。

その結果、いくつかの指定されたパラメータに基づいてモデルのほぼ全体を作成できるクラスができました。したがって、新しいクラスのprotectedブロックで、ブロックパラメータを格納するための5つの変数を宣言します。

iLayers モデル内のTransformerブロックの数
iHeads 
Self-Attentionヘッドの数
iWindow
入力ウィンドウサイズ(1つの入力シーケンストークン)
iWindowKey Query, Key, Value内部ベクトルの次元
iUnits 入力シーケンスの要素(トークン)の数

また、protectedブロックで、テンソルと訓練重み行列のバッファのコレクションを格納する6つの配列を宣言します。

QKV_Tensors Query, Key, Valueテンソルおよびそれらの勾配を格納するための配列
QKV_Weights Wq、Wk、Wvの重み行列とそれらのモーメント行列のコレクションを格納するための配列
S_Tensors スコア行列とその勾配のコレクションを格納するための配列
AO_Tensors Self-Attentionメカニズムの出力テンソルとその勾配を格納するための配列
FF_Tensors フィードフォワードブロックの入力テンソル、隠れテンソル、出力テンソル、およびそれらの勾配を格納するための配列 
FF_Weights
フィードフォワードブロックの重み行列とそのモーメントを格納するための配列


クラスメソッドは、後で実装するときに検討します。

class CNeuronMLMHAttentionOCL       :  public CNeuronBaseOCL
  {
protected:
   uint              iLayers;                                     ///< Number of inner layers
   uint              iHeads;                                      ///< Number of heads
   uint              iWindow;                                     ///< Input window size
   uint              iUnits;                                      ///< Number of units
   uint              iWindowKey;                                  ///< Size of Key/Query window
//---
   CCollection       *QKV_Tensors;                                ///< The collection of tensors of Queries, Keys and Values
   CCollection       *QKV_Weights;                                ///< The collection of Matrix of weights to previous layer
   CCollection       *S_Tensors;                                  ///< The collection of Scores tensors
   CCollection       *AO_Tensors;                                 ///< The collection of Attention Out tensors
   CCollection       *FF_Tensors;                                 ///< The collection of tensors of Feed Forward output
   CCollection       *FF_Weights;                                 ///< The collection of Matrix of Feed Forward weights

///\ingroup neuron_base_ff
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);               ///< \brief Feed Forward method of calling kernel ::FeedForward().@param NeuronOCL Pointer to previos layer.
   virtual bool      ConvolutionForward(CBufferDouble *weights, CBufferDouble *inputs,CBufferDouble *outputs, uint window, uint window_out, ENUM_ACTIVATION activ);
   ///< \brief Convolution Feed Forward method of calling kernel ::FeedForwardConv().
   virtual bool      AttentionScore(CBufferDouble *qkv, CBufferDouble *scores, bool mask=true);
   ///< \brief Multi-heads attention scores method of calling kernel ::MHAttentionScore().
   virtual bool      AttentionOut(CBufferDouble *qkv, CBufferDouble *scores, CBufferDouble *out);
   ///< \brief Multi-heads attention out method of calling kernel ::MHAttentionOut().
   virtual bool      SumAndNormilize(CBufferDouble *tensor1, CBufferDouble *tensor2, CBufferDouble *out);
   ///< \brief Method sum and normalize 2 tensors by calling 2 kernels ::SumMatrix() and ::Normalize().
///\ingroup neuron_base_opt
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);        ///< Method for updating weights.\details Calling one of kernels ::UpdateWeightsMomentum() or ::UpdateWeightsAdam() in depends on optimization type (#ENUM_OPTIMIZATION).@param NeuronOCL Pointer to previos layer.
   virtual bool      ConvolutuionUpdateWeights(CBufferDouble *weights, CBufferDouble *gradient, CBufferDouble *inputs, CBufferDouble *momentum1, CBufferDouble *momentum2, uint window, uint window_out);
   ///< Method for updating weights in convolution layer.\details Calling one of kernels ::UpdateWeightsConvMomentum() or ::UpdateWeightsConvAdam() in depends on optimization type (#ENUM_OPTIMIZATION).
   virtual bool      ConvolutionInputGradients(CBufferDouble *weights, CBufferDouble *gradient, CBufferDouble *inputs, CBufferDouble *inp_gradient, uint window, uint window_out, uint activ);
   ///< Method of passing gradients through a convolutional layer.
   virtual bool      AttentionInsideGradients(CBufferDouble *qkv,CBufferDouble *qkv_g,CBufferDouble *scores,CBufferDouble *scores_g,CBufferDouble *gradient);
   ///< Method of passing gradients through attention layer.
public:
   /** Constructor */CNeuronMLMHAttentionOCL(void);
   /** Destructor */~CNeuronMLMHAttentionOCL(void);
   virtual bool      Init(uint numOutputs,uint myIndex,COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, uint layers, ENUM_OPTIMIZATION optimization_type);
   ///< Method of initialization class.@param[in] numOutputs Number of connections to next layer.@param[in] myIndex Index of neuron in layer.@param[in] open_cl Pointer to #COpenCLMy object.@param[in] window Size of in/out window and step.@param[in] units_countNumber of neurons.@param[in] optimization_type Optimization type (#ENUM_OPTIMIZATION)@return Boolen result of operations.
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer);  ///< Method to transfer gradients to previous layer @param[in] prevLayer Pointer to previous layer.
   //---
   virtual int       Type(void)   const   {  return defNeuronMLMHAttentionOCL;   }///< Identificator of class.@return Type of class
   //--- methods for working with files
   virtual bool      Save(int const file_handle);  ///< Save method @param[in] file_handle handle of file @return logical result of operation
   virtual bool      Load(int const file_handle);  ///< Load method @param[in] file_handle handle of file @return logical result of operation
  };

クラスコンストラクタでは、クラスハイパーパラメータの初期値を設定し、コレクション配列を初期化します。

CNeuronMLMHAttentionOCL::CNeuronMLMHAttentionOCL(void)   :  iLayers(0),
   iHeads(0),
   iWindow(0),
   iWindowKey(0),
   iUnits(0)
  {
   QKV_Tensors=new CCollection();
   QKV_Weights=new CCollection();
   S_Tensors=new CCollection();
   AO_Tensors=new CCollection();
   FF_Tensors=new CCollection();
   FF_Weights=new CCollection();
  }

したがって、クラスデストラクタのコレクション配列を削除します。

CNeuronMLMHAttentionOCL::~CNeuronMLMHAttentionOCL(void)
  {
   if(CheckPointer(QKV_Tensors)!=POINTER_INVALID)
      delete QKV_Tensors;
   if(CheckPointer(QKV_Weights)!=POINTER_INVALID)
      delete QKV_Weights;
   if(CheckPointer(S_Tensors)!=POINTER_INVALID)
      delete S_Tensors;
   if(CheckPointer(AO_Tensors)!=POINTER_INVALID)
      delete AO_Tensors;
   if(CheckPointer(FF_Tensors)!=POINTER_INVALID)
      delete FF_Tensors;
   if(CheckPointer(FF_Weights)!=POINTER_INVALID)
      delete FF_Weights;
  }

モデルの構築に伴うクラスの初期化は、Initメソッドで実行されます。メソッドはパラメータを受け取ります。

numOutputs リンクを作成するための後続の層の要素の数
myIndex 層のニューロンインデックス
open_cl OpenCLオブジェクトポインタ
window 入力ウィンドウサイズ(入力シーケンストークン)
window_key Query, Key, Value内部ベクトルの次元
heads Self-Attentionヘッド(スレッド)の数 
units_count 入力シーケンスの要素の数
layers モデルスタック内のブロック(層)の数
optimization_type 訓練中のパラメータ最適化方法
bool CNeuronMLMHAttentionOCL::Init(uint numOutputs,uint myIndex,COpenCLMy *open_cl,uint window,uint window_key,uint heads,uint units_count,uint layers,ENUM_OPTIMIZATION optimization_type)
  {
   if(!CNeuronBaseOCL::Init(numOutputs,myIndex,open_cl,window*units_count,optimization_type))
      return false;
//---
   iWindow=fmax(window,1);
   iWindowKey=fmax(window_key,1);
   iUnits=fmax(units_count,1);
   iHeads=fmax(heads,1);
   iLayers=fmax(layers,1);

メソッドの開始時に、適切なメソッドを呼び出して親クラスを初期化します。受信したOpenCLオブジェクトポインタと入力シーケンスサイズを検証するための基本的なチェックは実行しません。これらのチェックはすでに親クラスのメソッドに実装されているためです。

親クラスの初期化が成功したら、ハイパーパラメータを対応する変数に保存します。

次に、作成されるテンソルのサイズを計算します。Multi-Head Attentionを整理するために以前に変更されたアプローチに注意を払ってください。クエリ、キー、値の各ベクトルに対して個別の配列を作成することはありません。これらは、1つの配列に結合されます。さらに、Attentionヘッドごとに個別の配列を作成することはありません。代わりに、QKV(クエリ+キー+値)、スコア、およびself-attentionメカニズムの出力用の共通配列を作成します。要素は、テンソルのインデックスのレベルでシーケンスに分割されます。もちろん、このアプローチは理解するのがより困難です。また、テンソルで必要な要素を見つけるのがより難しい場合があります。ただし、Attentionヘッドの数に応じてモデルを柔軟にし、カーネルレベルでスレッドを並列化することにより、すべてのAttentionヘッドの同時再計算を整理できます。

QKV_Tensor(num)テンソルのサイズは、内部ベクトルの3つのサイズ(クエリ+キー+値)とヘッドの数の積として定義されます。重みの連結行列のサイズQKV_Weightは、入力シーケンストークンの3つのサイズの積として定義され、オフセット要素、内部ベクトルのサイズ、およびAttentionヘッドの数によって増加します。同様に、残りのテンソルのサイズを計算してみましょう。

   uint num=3*iWindowKey*iHeads*iUnits;               //Size of QKV tensor
   uint qkv_weights=3*(iWindow+1)*iWindowKey*iHeads;  //Size of weights' matrix of QKV tensor
   uint scores=iUnits*iUnits*iHeads;                  //Size of Score tensor
   uint mh_out=iWindowKey*iHeads*iUnits;              //Size of multi-heads self-attention
   uint out=iWindow*iUnits;                           //Size of our tensor
   uint w0=(iWindowKey+1)*iHeads*iWindow;             //Size W0 tensor
   uint ff_1=4*(iWindow+1)*iWindow;                   //Size of weights' matrix 1-st feed forward layer
   uint ff_2=(4*iWindow+1)*iWindow;                   //Size of weights' matrix 2-nd feed forward layer

すべてのテンソルのサイズを決定したら、ブロック内のAttention層の数でサイクルを実行して、必要なテンソルを作成します。ループ本体内には2つのネストされたループが編成されています。最初のループは、値テンソルとその勾配の配列を作成します。2つ目は、重み行列とそのモーメントの配列を作成します。最終層では、フィードフォワードブロック出力テンソルとその勾配に対して新しい配列が作成されない代わりに、親クラスの出力と勾配配列へのポインタがコレクションに追加されます。このような単純な手順により、配列間で値を転送する不必要な反復が回避されるだけでなく、不必要なメモリ消費が排除されます。 

   for(uint i=0; i<iLayers; i++)
     {
      CBufferDouble *temp=NULL;
      for(int d=0; d<2; d++)
        {
         //--- Initialize QKV tensor
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(num,0))
            return false;
         if(!QKV_Tensors.Add(temp))
            return false;
         //--- Initialize scores
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(scores,0))
            return false;
         if(!S_Tensors.Add(temp))
            return false;
         //--- Initialize multi-heads attention out
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(mh_out,0))
            return false;
         if(!AO_Tensors.Add(temp))
            return false;
         //--- Initialize attention out
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(out,0))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;
         //--- Initialize Feed Forward 1
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(4*out,0))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;
         //--- Initialize Feed Forward 2
         if(i==iLayers-1)
           {
            if(!FF_Tensors.Add(d==0 ? Output : Gradient))
               return false;
            continue;
           }
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(out,0))
            return false;
         if(!FF_Tensors.Add(temp))
            return false;
        }
      //--- Initialize QKV weights
      temp=new CBufferDouble();
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      if(!temp.Reserve(qkv_weights))
         return false;
      for(uint w=0; w<qkv_weights; w++)
        {
         if(!temp.Add(GenerateWeight()))
            return false;
        }
      if(!QKV_Weights.Add(temp))
         return false;
      //--- Initialize Weights0
      temp=new CBufferDouble();
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      if(!temp.Reserve(w0))
         return false;
      for(uint w=0; w<w0; w++)
        {
         if(!temp.Add(GenerateWeight()))
            return false;
        }
      if(!FF_Weights.Add(temp))
         return false;
      //--- Initialize FF Weights
      temp=new CBufferDouble();
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      if(!temp.Reserve(ff_1))
         return false;
      for(uint w=0; w<ff_1; w++)
        {
         if(!temp.Add(GenerateWeight()))
            return false;
        }
      if(!FF_Weights.Add(temp))
         return false;
      //---
      temp=new CBufferDouble();
      if(CheckPointer(temp)==POINTER_INVALID)
         return false;
      if(!temp.Reserve(ff_2))
         return false;
      for(uint w=0; w<ff_1; w++)
        {
         if(!temp.Add(GenerateWeight()))
            return false;
        }
      if(!FF_Weights.Add(temp))
         return false;
      //---
      for(int d=0; d<(optimization==SGD ? 1 : 2); d++)
        {
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(qkv_weights,0))
            return false;
         if(!QKV_Weights.Add(temp))
            return false;
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(w0,0))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
         //--- Initialize FF Weights
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(ff_1,0))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
         temp=new CBufferDouble();
         if(CheckPointer(temp)==POINTER_INVALID)
            return false;
         if(!temp.BufferInit(ff_2,0))
            return false;
         if(!FF_Weights.Add(temp))
            return false;
        }
     }
//---
   return true;
  }

その結果、各層について、次のテンソルの行列が得られます。

QKV_Tensor

    1. 出力
    2. 勾配

S_Tensors

    1. 出力
    2. 勾配

AO_Tensors

    1. MH出力
    2. MH勾配

FF_Tensors

    1. FF1入力(Attention出力)
    2. FF1出力
    3. FF2出力
    4. FF1入力勾配
    5. FF1勾配
    6. FF2勾配

QKV_Weights

    1. 重み
    2. デルタの重み(SGD)/最初のモメンタム(Adam)
    3. Adamの2番目のモメンタムのみ

FF_Weights

    1. 重み0
    2. FF1重み
    3. FF2重み
    4. W0デルタの重み(SGD) / 1番目のモメンタム(Adam)
    5. FF1デルタの重み(SGD) / 1番目のモメンタム(Adam)
    6. FF2デルタの重み(SGD) / 1番目のモメンタム(Adam)
    7. Adam W0の2番目のモメンタムのみ
    8. Adam FF1の2番目のモメンタムのみ
    9. Adam FF2の2番目のモメンタムのみ

配列コレクションを作成したら、「true」でメソッドを終了します。すべてのクラスとそのメソッドの完全なコードは、添付ファイルにあります。

3.2. フィードフォワード

フィードフォワードパスは、従来、feedForwardメソッドで構成されていました。このメソッドは、ニューラルネットワークの前の層へのポインタをパラメータとして受け取ります。メソッドの開始時に、受信したポインタの有効性を確認します。

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

次に、ループを編成して、ブロックのすべての層を再計算しましょう。前述の他のクラスの類似のメソッドとは異なり、このメソッドはトップレベルのメソッドです。編成された操作は、データの準備と補助メソッドの呼び出しに限定されます(これらのメソッドのロジックについては以下で説明します)。

ループの開始時に、コレクションから、現在の層に対応するQKVおよびQKV_Weightsテンソルの入力データバッファを受け取ります。次に、ConvolutionForwardを呼び出して、Query、Key、Value各ベクトルを計算します。  

   for(uint i=0; (i<iLayers && !IsStopped()); i++)
     {
      //--- Calculate Queries, Keys, Values
      CBufferDouble *inputs=(i==0? NeuronOCL.getOutput() : FF_Tensors.At(6*i-4));
      CBufferDouble *qkv=QKV_Tensors.At(i*2);
      if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i*(optimization==SGD ? 2 : 3)),inputs,qkv,iWindow,3*iWindowKey*iHeads,None))
         return false;

Attention層を増やすときに問題が発生しました。ある時点で、エラー5113 ERR_OPENCL_TOO_MANY_OBJECTSが発生しました。そのため、すべてのテンソルをGPUメモリに永続的に保存することを検討する必要がありました。したがって、操作の完了後、この手順で使用されなくなったバッファを解放します。コードでは、GPUメモリから解放されたバッファの最新データを読み取ることを忘れないでください。本稿で紹介するクラスでは、バッファデータはカーネル初期化メソッドで読み取られます。これについては後で説明します。

      CBufferDouble *temp=QKV_Weights.At(i*(optimization==SGD ? 2 : 3));
      temp.BufferFree();

本稿で紹介するクラスでは、バッファデータはカーネル初期化メソッドで読み取られます。これについては後で説明します。

      //--- Score calculation
      temp=S_Tensors.At(i*2);
      if(IsStopped() || !AttentionScore(qkv,temp,true))
         return false;
      //--- Multi-heads attention calculation
      CBufferDouble *out=AO_Tensors.At(i*2);
      if(IsStopped() || !AttentionOut(qkv,temp,out))
         return false;
      qkv.BufferFree();
      temp.BufferFree();

Multi-Heads Self-Attentionを計算した後、連結されたAttention出力を入力シーケンスサイズに折りたたんで、2つのベクトルを追加し、結果を正規化します。

      //--- Attention out calculation
      temp=FF_Tensors.At(i*6);
      if(IsStopped() || !ConvolutionForward(FF_Weights.At(i*(optimization==SGD ? 6 : 9)),out,temp,iWindowKey*iHeads,iWindow,None))
         return false;
      out.BufferFree();
      //--- Sum and normalize attention
      if(IsStopped() || !SumAndNormilize(temp,inputs,temp))
         return false;
      if(i>0)
         inputs.BufferFree();

Transformerのself-attentionメカニズムの後には、2つの全結合層で構成されるフィードフォワードブロックが続きます。次に、結果が入力シーケンスに追加されます。最終テンソルは正規化され、次の層に送られます。この場合、ループを終了します。

      //--- Feed Forward
      inputs=temp;
      temp=FF_Weights.At(i*(optimization==SGD ? 6 : 9));
      temp.BufferFree();
      temp=FF_Tensors.At(i*6+1);
      if(IsStopped() || !ConvolutionForward(FF_Weights.At(i*(optimization==SGD ? 6 : 9)+1),inputs,temp,iWindow,4*iWindow,LReLU))
         return false;
      out=FF_Weights.At(i*(optimization==SGD ? 6 : 9)+1);
      out.BufferFree();
      out=FF_Tensors.At(i*6+2);
      if(IsStopped() || !ConvolutionForward(FF_Weights.At(i*(optimization==SGD ? 6 : 9)+2),temp,out,4*iWindow,iWindow,activation))
         return false;
      temp.BufferFree();
      temp=FF_Weights.At(i*(optimization==SGD ? 6 : 9)+2);
      temp.BufferFree();
      //--- Sum and normalize out
      if(IsStopped() || !SumAndNormilize(out,inputs,out))
         return false;
      inputs.BufferFree();
     }
//---
   return true;
  }

完全なメソッドコードは、以下の添付ファイルに記載されています。ここで、FeedForwardメソッドから呼び出されるヘルパーメソッドについて考えてみましょう。最初に呼び出すメソッドは、ConvolutionForwardです。フィードフォワード方式の1サイクルあたり4回呼び出されます。メソッド本体では、畳み込み層のフォワードパスカーネルが呼び出されます。この場合、このメソッドは、入力シーケンスの個別のトークンごとに全結合層の役割を果たします。このソリューションについては、第8部で詳しく説明しています。前述のソリューションとは対照的に、新しいメソッドはパラメータ内のバッファへのポインタを受け取り、OpenCLカーネルにデータを転送します。したがって、メソッドの開始時に、受信したポインタの有効性を確認します。 

bool CNeuronMLMHAttentionOCL::ConvolutionForward(CBufferDouble *weights, CBufferDouble *inputs,CBufferDouble *outputs, uint window, uint window_out, ENUM_ACTIVATION activ)
  {
   if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(weights)==POINTER_INVALID || CheckPointer(inputs)==POINTER_INVALID
      || CheckPointer(outputs)==POINTER_INVALID)
      return false;

次に、GPUメモリにバッファを作成し、必要な情報を渡します。

   if(!weights.BufferCreate(OpenCL))
      return false;
   if(!inputs.BufferCreate(OpenCL))
      return false;
   if(!outputs.BufferCreate(OpenCL))
      return false;

この後に、第8部で説明されているコードが変更なしで続きます。呼び出されたカーネルは、変更なしでそのまま使用されます。

   uint global_work_offset[1]= {0};
   uint global_work_size[1];
   global_work_size[0]=outputs.Total()/window_out;
   OpenCL.SetArgumentBuffer(def_k_FeedForwardConv,def_k_ffc_matrix_w,weights.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_FeedForwardConv,def_k_ffc_matrix_i,inputs.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_FeedForwardConv,def_k_ffc_matrix_o,outputs.GetIndex());
   OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_inputs,inputs.Total());
   OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_step,window);
   OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_window_in,window);
   OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffс_window_out,window_out);
   OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_activation,(int)activ);
   if(!OpenCL.Execute(def_k_FeedForwardConv,1,global_work_offset,global_work_size))
     {
      printf("Error of execution kernel FeedForwardConv: %d",GetLastError());
      return false;
     }
//---
   return outputs.BufferRead();
  }

feedForwardメソッドコードでは後にAttentionScoreメソッドの呼び出しがあります。このメソッドは、カーネルを呼び出してAttentionスコアを計算および正規化します。これらの結果の値は、スコア行列に書き込まれます。このメソッド用に新しいカーネルが作成されました。 メソッド自体を検討した後、後で検討します。

前のメソッドと同様に、AttentionScoreは、パラメータとして、初期データバッファへのポインタと取得された値のレコードを受け取ります。そのため、メソッドの開始時に、受信したポインタの有効性を確認します。 

bool CNeuronMLMHAttentionOCL::AttentionScore(CBufferDouble *qkv, CBufferDouble *scores, bool mask=true)
  {
   if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(qkv)==POINTER_INVALID || CheckPointer(scores)==POINTER_INVALID)
      return false;

上記のロジックに従って、GPUとのデータ交換用のバッファを作成しましょう。

   if(!qkv.BufferCreate(OpenCL))
      return false;
   if(!scores.BufferCreate(OpenCL))
      return false;

準備作業が終わったら、カーネルパラメータの指定に移りましょう。このカーネルのスレッドは、入力シーケンス要素のコンテキストとAttentionヘッドのコンテキストの2つの次元で作成されます。これにより、シーケンスのすべての要素とすべてのAttentionヘッドの並列計算が提供されます。

   uint global_work_offset[2]= {0,0};
   uint global_work_size[2];
   global_work_size[0]=iUnits;
   global_work_size[1]=iHeads;
   OpenCL.SetArgumentBuffer(def_k_MHAttentionScore,def_k_mhas_qkv,qkv.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_MHAttentionScore,def_k_mhas_score,scores.GetIndex());
   OpenCL.SetArgument(def_k_MHAttentionScore,def_k_mhas_dimension,iWindowKey);
   OpenCL.SetArgument(def_k_MHAttentionScore,def_k_mhas_mask,(int)mask);

次に、カーネル呼び出しに直接進みます。計算結果は「スコア」バッファに読み込まれます。

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

呼び出されたMHAttentionScoreカーネルのロジックを見てみましょう。上に示したように、カーネルはパラメータとしてqkvソースデータ配列へのポインタと結果のスコアを記録するための配列を受け取ります。また、カーネルは、パラメータとして内部ベクトル(Query、Key)のサイズと、後続の要素のマスキングアルゴリズムを有効にするためのフラグを受け取ります。

まず、処理中のクエリqの序数とAttentionヘッドhを取得します。また、クエリとAttentionヘッドの数の次元を取得します。

__kernel void MHAttentionScore(__global double *qkv,    ///<[in] Matrix of Querys, Keys, Values
                               __global double *score,  ///<[out] Matrix of Scores
                               int dimension,           ///< Dimension of Key
                               int mask                 ///< 1 - calc only previous units, 0 - calc all
                              )
  {
   int q=get_global_id(0);
   int h=get_global_id(1);
   int units=get_global_size(0);
   int heads=get_global_size(1);

得られたデータに基づいて、query配列とscore配列のシフトを決定します。

   int shift_q=dimension*(h+3*q*heads);
   int shift_s=units*(h+q*heads);

また、Scoreの補正係数を計算します。

   double koef=sqrt((double)dimension);
   if(koef<1)
      koef=1;

Attentionスコアはループで計算され、対応するAttentionヘッドの要素のシーケンス全体のキーを繰り返し処理します。

ループの開始時に、Attentionメカニズムを使用するための条件を確認します。この機能が有効になっている場合は、キーのシリアル番号を確認してください。現在のキーがシーケンスの次の要素に対応している場合は、ゼロスコアをscore配列に書き込み、次の要素に移動します。 

   double sum=0;
   for(int k=0;k<units;k++)
     {
      if(mask>0 && k>q)
        {
         score[shift_s+k]=0;
         continue;
        }

分析されたキーのAttentionスコアが計算される場合、ネストされたループを編成して、2つのベクトルの積を計算します。ループ本体での計算にはブランチがあって、1つはベクトル計算を使用し、もう1つはそのような計算を使用しません。最初のブランチは、キーベクトルの現在の位置から最後の要素までに4つ以上の要素がある場合に使用され、2番目は、キーベクトルの最後の4要素未満に使用されます。 

      double result=0;
      int shift_k=dimension*(h+heads*(3*k+1));
      for(int i=0;i<dimension;i++)
        {
         if((dimension-i)>4)
           {
            result+=dot((double4)(qkv[shift_q+i],qkv[shift_q+i+1],qkv[shift_q+i+2],qkv[shift_q+i+3]),
                        (double4)(qkv[shift_k+i],qkv[shift_k+i+1],qkv[shift_k+i+2],qkv[shift_k+i+3]));
            i+=3;
           }
         else
            result+=(qkv[shift_q+i]*qkv[shift_k+i]);
        }

Transformerアルゴリズムに従って、AttentionスコアはSoftMax関数を使用して正規化されます。この機能を実装するために、ベクトルの積の結果を補正係数で除算し、結果の値の指数を決定します。計算結果は、scoreテンソルの対応する要素に書き込まれ、指数の合計に追加される必要があります。

      result=exp(clamp(result/koef,-30.0,30.0));
      if(isnan(result))
         result=0;
      score[shift_s+k]=result;
      sum+=result;   
     }

同様に、すべての要素の指数を計算します。AttentionスコアのSoftMax正規化を完了するために、scoreテンソルのすべての要素が以前に計算された指数の合計で除算される別のループを編成します。

   for(int k=0;(k<units && sum>1);k++)
      score[shift_s+k]/=sum;
  }

ループが終了したらカーネルを終了します。
feedForwardメソッドに進み、AttentionOutヘルパーメソッドについて考えてみましょう。このメソッドは、QKV、Scores、Outの3つのテンソルへのポインタをパラメータとして受け取ります。メソッドの構造は、前に検討したものと同様です。シーケンス要素とAttentionヘッドの2つの次元でMHAttentionOutカーネルを起動します。  

bool CNeuronMLMHAttentionOCL::AttentionOut(CBufferDouble *qkv, CBufferDouble *scores, CBufferDouble *out)
  {
   if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(qkv)==POINTER_INVALID || CheckPointer(scores)==POINTER_INVALID
      || CheckPointer(out)==POINTER_INVALID)
      return false;
   uint global_work_offset[2]= {0,0};
   uint global_work_size[2];
   global_work_size[0]=iUnits;
   global_work_size[1]=iHeads;
   if(!qkv.BufferCreate(OpenCL))
      return false;
   if(!scores.BufferCreate(OpenCL))
      return false;
   if(!out.BufferCreate(OpenCL))
      return false;
//---
   OpenCL.SetArgumentBuffer(def_k_MHAttentionOut,def_k_mhao_qkv,qkv.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_MHAttentionOut,def_k_mhao_score,scores.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_MHAttentionOut,def_k_mhao_out,out.GetIndex());
   OpenCL.SetArgument(def_k_MHAttentionOut,def_k_mhao_dimension,iWindowKey);
   if(!OpenCL.Execute(def_k_MHAttentionOut,2,global_work_offset,global_work_size))
     {
      printf("Error of execution kernel MHAttentionOut: %d",GetLastError());
      return false;
     }
//---
   return out.BufferRead();
  }

以前のカーネルと同様に、MHAttentionOutは、Multi-Head Attentionを考慮して新しく作成されました。クエリ、キー、値のすべてのテンソルに単一のバッファを使用します。カーネルはパラメータとしてScores、QKV、Out各テンソルおよび値ベクトルのサイズへのポインタを受け取ります。1、2番目のバッファは元のデータを提供し、最後のバッファは結果の記録に使用されます。

また、カーネルの開始時に、処理されているクエリqの序数、Attentionヘッドh、およびクエリとAttentionヘッドの数の次元を決定します。 

__kernel void MHAttentionOut(__global double *scores, ///<[in] Matrix of Scores
                             __global double *qkv,    ///<[in] Matrix of Values
                             __global double *out,    ///<[out] Output tensor
                             int dimension            ///< Dimension of Value
                            )
  {
   int u=get_global_id(0);
   int units=get_global_size(0);
   int h=get_global_id(1);
   int heads=get_global_size(1);

次に、必要なAttentionスコアと、分析される出力値ベクトルの最初の要素の位置を決定します。さらに、QKVテンソルの1つの要素のベクトルの長さを計算します。この値は、QKVテンソルのシフトを決定するために使用されます。 

   int shift_s=units*(h+heads*u);
   int shift_out=dimension*(h+heads*u);
   int layer=3*dimension*heads;

主な計算にネストされたループを実装します。外側のループは、値のベクトルのサイズで実行されます。内側のループは、元のシーケンスの要素の数によって実行されます。外側のループの開始時に、結果の値を計算するための変数を宣言し、それをゼロ値で初期化します。内側のループは、値ベクトルのシフトの定義を開始します。後でベクトル計算を使用するため、内部ループのステップは4に等しいことに注意してください。 

   for(int d=0;d<dimension;d++)
     {
      double result=0;
      for(int v=0;v<units;v+=4)
        {
         int shift_v=dimension*(h+heads*(3*v+2))+d;

MHAttentionScoreカーネルと同様に、計算を2つのスレッドに分割します。1つはベクトル計算を使用し、
後1つは使用しません。シーケンスの長さが4の倍数でない場合、2番目のスレッドは最後の要素にのみ使用されます。

         if((units-v)>4)
           {
            result+=dot((double4)(scores[shift_s+v],scores[shift_s+v+1],scores[shift_s+v+1],scores[shift_s+v+3]),
                        (double4)(qkv[shift_v],qkv[shift_v+layer],qkv[shift_v+2*layer],qkv[shift_v+3*layer]));
           }
         else
            for(int l=0;l<(int)fmin((double)(units-v),4.0);l++)
               result+=scores[shift_s+v+l]*qkv[shift_v+l*layer];
        }
      out[shift_out+d]=result;
     }
  }

ネストされたループを終了した後、結果の値を出力テンソルの対応する要素に書き込みます。

さらに、feedForwardメソッドでは、上記のConvolutionForwardメソッドが使用されます。すべてのメソッドと関数の完全なコードは、添付ファイルにあります。


3.3. フィードバックワード

以前に検討されたすべてのクラスと同様に、フィードバックプロセスには、エラー勾配の伝播と重みの更新の2つのサブプロセスが含まれます。最初の部分は、calcInputGradientsメソッドに実装されています。2番目の部分はupdateInputWeightsに実装されています。

calcInputGradientsメソッドの構成はfeedForwardと似ています。このメソッドは、パラメータとしてエラー勾配を渡す必要があるニューラルネットワークの前の層へのポインタを受け取るため、メソッドの開始時に、受信したポインタの有効性を確認します。 

bool CNeuronMLMHAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(CheckPointer(prevLayer)==POINTER_INVALID)
      return false;

次に、ニューロンの次の層から受け取った勾配のテンソルを修正し、すべての内側の層にループを編成して、エラー勾配を順次計算します。これはフィードバックプロセスであるため、ループは逆の順序で内層を反復処理します。

   for(int i=(int)iLayers-1; (i>=0 && !IsStopped()); i--)
     {
      //--- Passing gradient through feed forward layers
      if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? 6 : 9)+2),out_grad,FF_Tensors.At(i*6+1),FF_Tensors.At(i*6+4),4*iWindow,iWindow,None))
         return false;
      CBufferDouble *temp=FF_Weights.At(i*(optimization==SGD ? 6 : 9)+2);
      temp.BufferFree();
      temp=FF_Tensors.At(i*6+1);
      temp.BufferFree();
      temp=FF_Tensors.At(i*6+3);
      if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? 6 : 9)+1),FF_Tensors.At(i*6+4),FF_Tensors.At(i*6),temp,iWindow,4*iWindow,LReLU))
         return false;

ループの開始時に、Transformerのフィードフォワードブロックのニューロンの全結合層を介したエラー勾配の伝播を計算します。この反復は、ConvolutionInputGradientsメソッドによって実行されます。メソッドの完了後にバッファを解放します。

ここでのアルゴリズムはプロセス全体を通したデータフローを実装するため、エラー勾配に対しても同じプロセスを実装する必要があります。したがって、フィードフォワードブロックから取得されたエラー勾配は、ニューロンの前の層から受信されたエラー勾配と合計されます。勾配が急上昇するリスクを排除するために、2つのベクトルの合計を正規化します。これらの操作はすべて、SumAndNormilizeメソッドで実行されます。メソッドの完了後にバッファを解放します。

      //--- Sum and normalize gradients
      if(IsStopped() || !SumAndNormilize(out_grad,temp,temp))
         return false;
      if(i!=(int)iLayers-1)
         out_grad.BufferFree();
      out_grad=temp;
      temp=FF_Weights.At(i*(optimization==SGD ? 6 : 9)+1);
      temp.BufferFree();
      temp=FF_Tensors.At(i*6+4);
      temp.BufferFree();
      temp=FF_Tensors.At(i*6);
      temp.BufferFree();

さらにアルゴリズムに沿って、エラー勾配をAttentionヘッドで割ってみましょう。これは、W0行列のConvolutionInputGradientsメソッドを呼び出すことによって行われます。

      //--- Split gradient to multi-heads
      if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? 6 : 9)),out_grad,AO_Tensors.At(i*2),AO_Tensors.At(i*2+1),iWindowKey*iHeads,iWindow,None))
         return false;
      temp=FF_Weights.At(i*(optimization==SGD ? 6 : 9));
      temp.BufferFree();
      temp=AO_Tensors.At(i*2);
      temp.BufferFree();

 Attentionヘッドに沿ったさらなる勾配伝播はAttentionInsideGradientsメソッドで編成されます。

      if(IsStopped() || !AttentionInsideGradients(QKV_Tensors.At(i*2),QKV_Tensors.At(i*2+1),S_Tensors.At(i*2),S_Tensors.At(i*2+1),AO_Tensors.At(i*2+1)))
         return false;
      temp=QKV_Tensors.At(i*2);
      temp.BufferFree();
      temp=S_Tensors.At(i*2);
      temp.BufferFree();
      temp=S_Tensors.At(i*2+1);
      temp.BufferFree();
      temp=AO_Tensors.At(i*2+1);
      temp.BufferFree();

ループの最後に、前の層に渡されたエラー勾配を計算します。ここで、前の反復から受信したエラー勾配は、連結テンソルQKV_Weightsを通過し、受信したベクトルは、self-attentionメカニズムのフィードフォワードブロックからのエラー勾配と合計され、結果は、勾配の急上昇を回避するために正規化されます。

      CBufferDouble *inp=NULL;
      if(i==0)
        {
         inp=prevLayer.getOutput();
         temp=prevLayer.getGradient();
        }
      else
        {
         temp=FF_Tensors.At(i*6-1);
         inp=FF_Tensors.At(i*6-4);
        }
      if(IsStopped() || !ConvolutionInputGradients(QKV_Weights.At(i*(optimization==SGD ? 2 : 3)),QKV_Tensors.At(i*2+1),inp,temp,iWindow,3*iWindowKey*iHeads,None))
         return false;
  
      //--- Sum and normalize gradients
      if(IsStopped() || !SumAndNormilize(out_grad,temp,temp))
         return false;
      out_grad.BufferFree();
      if(i>0)
         out_grad=temp;
      temp=QKV_Weights.At(i*(optimization==SGD ? 2 : 3));
      temp.BufferFree();
      temp=QKV_Tensors.At(i*2+1);
      temp.BufferFree();
     }
//---
   return true;
  }

使用済みのデータバッファを解放することを忘れないでください。前の層のデータバッファがGPUメモリに残っていることに注意してください。

呼び出されたメソッドを見てみましょう。ご覧のとおり、最も頻繁に呼び出されるメソッドはConvolutionInputGradientsです。これは、畳み込み層の同様のメソッドに基づいており、現在のタスク用に最適化されています。このメソッドは、パラメータに、重みのテンソル、次の層の勾配、前の層の出力データ、および反復結果を格納するテンソルへのポインタを受け取ります。また、このメソッドは、パラメータとして入力および出力データウィンドウのサイズと使用される活性化関数を受け取ります。

bool CNeuronMLMHAttentionOCL::ConvolutionInputGradients(CBufferDouble *weights, CBufferDouble *gradient, CBufferDouble *inputs, CBufferDouble *inp_gradient, uint window, uint window_out, uint activ)
  {
   if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(weights)==POINTER_INVALID || CheckPointer(gradient)==POINTER_INVALID || CheckPointer(inputs)==POINTER_INVALID
      || CheckPointer(inp_gradient)==POINTER_INVALID)
      return false;

メソッドの開始時に、受信したポインタの有効性を確認し、GPUメモリにデータバッファを作成します。

   if(!weights.BufferCreate(OpenCL))
      return false;
   if(!gradient.BufferCreate(OpenCL))
      return false;
   if(!inputs.BufferCreate(OpenCL))
      return false;
   if(!inp_gradient.BufferCreate(OpenCL))
      return false;

データバッファを作成した後で、適切なOpenCLプログラムカーネルの呼び出しを実装します。ここでは、変更なしで畳み込みネットワークカーネルを使用します。

//---
   uint global_work_offset[1]= {0};
   uint global_work_size[1];
   global_work_size[0]=inputs.Total();
   OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_w,weights.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_g,gradient.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_o,inputs.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_ig,inp_gradient.GetIndex());
   OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_outputs,gradient.Total());
   OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_step,window);
   OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_window_in,window);
   OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_window_out,window_out);
   OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_activation,activ);
//Comment(com+"\n "+(string)__LINE__+"-"__FUNCTION__);
   if(!OpenCL.Execute(def_k_CalcHiddenGradientConv,1,global_work_offset,global_work_size))
     {
      printf("Error of execution kernel CalcHiddenGradientConv: %d",GetLastError());
      return false;
     }
//---
   return inp_gradient.BufferRead();
  }

ConvolutionInputGradientsメソッドからも呼び出されるAttentionInsideGradientsメソッドは、同様のアルゴリズムに従って構築されます。メソッドコードについては、添付ファイルを参照してください。ここで、すべての計算がカーネルで実行されるため、指定されたメソッドから呼び出されたOpenCLプログラムカーネルを見てみましょう。

MHAttentionInsideGradientsカーネルは、シーケンスの要素とAttentionヘッドの2次元のスレッドによって起動されます。カーネルは、連結されたQKVテンソルとその勾配のテンソル、スコア行列テンソルとその勾配、前の反復からのエラー勾配テンソル、およびキーベクトルのサイズへのポインタをパラメーターとして受け取ります。

__kernel void MHAttentionInsideGradients(__global double *qkv,__global double *qkv_g,
                                         __global double *scores,__global double *scores_g,
                                         __global double *gradient, int dimension)
  {
   int u=get_global_id(0);
   int h=get_global_id(1);
   int units=get_global_size(0);
   int heads=get_global_size(1);
   double koef=sqrt((double)dimension);
   if(koef<1)
      koef=1;

メソッドの開始時に、処理されたシーケンス要素とAttentionヘッドの序数、およびそれらのサイズを取得します。また、scores行列の更新係数を計算します。

次に、ループを編成して、scores行列のエラー勾配を計算します。ループの後にバリアを設定することで、すべてのスレッド間で計算プロセスを同期させることができます。アルゴリズムは、scores行列の勾配が完全に再計算された後でのみ、次の操作ブロックに切り替わります。

//--- Calculating score's gradients
   uint shift_s=units*(h+u*heads);
   for(int v=0;v<units;v++)
     {
      double s=scores[shift_s+v];
      if(s>0)
        {
         double sg=0;
         int shift_v=dimension*(h+heads*(3*v+2));
         int shift_g=dimension*(h+heads*v);
         for(int d=0;d<dimension;d++)
            sg+=qkv[shift_v+d]*gradient[shift_g+d];
         scores_g[shift_s+v]=sg*(s<1 ? s*(1-s) : 1)/koef;
        }
      else
         scores_g[shift_s+v]=0;
     }
   barrier(CLK_GLOBAL_MEM_FENCE);

 別のループを実装して、クエリ、キー、値のベクトルのエラー勾配を計算しましょう。

//--- Calculating gradients for Query, Key and Value
   uint shift_qg=dimension*(h+3*u*heads);
   uint shift_kg=dimension*(h+(3*u+1)*heads);
   uint shift_vg=dimension*(h+(3*u+2)*heads);
   for(int d=0;d<dimension;d++)
     {
      double vg=0;
      double qg=0;
      double kg=0;
      for(int l=0;l<units;l++)
        {
         uint shift_q=dimension*(h+3*l*heads)+d;
         uint shift_k=dimension*(h+(3*l+1)*heads)+d;
         uint shift_g=dimension*(h+heads*l)+d;
         double sg=scores_g[shift_s+l];
         
         kg+=sg*qkv[shift_q];
         qg+=sg*qkv[shift_k];
         vg+=gradient[shift_g]*scores[shift_s+l];
        }   
      qkv_g[shift_qg+d]=qg;
      qkv_g[shift_kg+d]=kg;
      qkv_g[shift_vg+d]=vg;
     }
  }

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

重みは、以前に検討されたfeedForwardメソッドとcalcInputGradientsメソッドの原則によって構築されたupdateInputWeightsメソッドで更新されます。畳み込みネットワークの重みを更新する1つのConvolutuionUpdateWeightsヘルパーメソッドのみが、このメソッド内で順番に呼び出されます。

bool CNeuronMLMHAttentionOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(NeuronOCL)==POINTER_INVALID)
      return false;
   CBufferDouble *inputs=NeuronOCL.getOutput();
   for(uint l=0; l<iLayers; l++)
     {
      if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l*(optimization==SGD ? 2 : 3)),QKV_Tensors.At(l*2+1),inputs,(optimization==SGD ? QKV_Weights.At(l*2+1) : QKV_Weights.At(l*3+1)),(optimization==SGD ? NULL : QKV_Weights.At(l*3+2)),iWindow,3*iWindowKey*iHeads))
         return false;
      if(l>0)
         inputs.BufferFree();
      CBufferDouble *temp=QKV_Weights.At(l*(optimization==SGD ? 2 : 3));
      temp.BufferFree();
      temp=QKV_Tensors.At(l*2+1);
      temp.BufferFree();
      if(optimization==SGD)
        {
         temp=QKV_Weights.At(l*2+1);
        }
      else
        {
         temp=QKV_Weights.At(l*3+1);
         temp.BufferFree();
         temp=QKV_Weights.At(l*3+2);
         temp.BufferFree();
        }
//---
      if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l*(optimization==SGD ? 6 : 9)),FF_Tensors.At(l*6+3),AO_Tensors.At(l*2),(optimization==SGD ? FF_Weights.At(l*6+3) : FF_Weights.At(l*9+3)),(optimization==SGD ? NULL : FF_Weights.At(l*9+6)),iWindowKey*iHeads,iWindow))
         return false;
      temp=FF_Weights.At(l*(optimization==SGD ? 6 : 9));
      temp.BufferFree();
      temp=FF_Tensors.At(l*6+3);
      temp.BufferFree();
      temp=AO_Tensors.At(l*2);
      temp.BufferFree();
      if(optimization==SGD)
        {
         temp=FF_Weights.At(l*6+3);
         temp.BufferFree();
        }
      else
        {
         temp=FF_Weights.At(l*9+3);
         temp.BufferFree();
         temp=FF_Weights.At(l*9+6);
         temp.BufferFree();
        }
//---
      if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l*(optimization==SGD ? 6 : 9)+1),FF_Tensors.At(l*6+4),FF_Tensors.At(l*6),(optimization==SGD ? FF_Weights.At(l*6+4) : FF_Weights.At(l*9+4)),(optimization==SGD ? NULL : FF_Weights.At(l*9+7)),iWindow,4*iWindow))
         return false;
      temp=FF_Weights.At(l*(optimization==SGD ? 6 : 9)+1);
      temp.BufferFree();
      temp=FF_Tensors.At(l*6+4);
      temp.BufferFree();
      temp=FF_Tensors.At(l*6);
      temp.BufferFree();
      if(optimization==SGD)
        {
         temp=FF_Weights.At(l*6+4);
         temp.BufferFree();
        }
      else
        {
         temp=FF_Weights.At(l*9+4);
         temp.BufferFree();
         temp=FF_Weights.At(l*9+7);
         temp.BufferFree();
        }
//---
      if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l*(optimization==SGD ? 6 : 9)+2),FF_Tensors.At(l*6+5),FF_Tensors.At(l*6+1),(optimization==SGD ? FF_Weights.At(l*6+5) : FF_Weights.At(l*9+5)),(optimization==SGD ? NULL : FF_Weights.At(l*9+8)),4*iWindow,iWindow))
         return false;
      temp=FF_Weights.At(l*(optimization==SGD ? 6 : 9)+2);
      temp.BufferFree();
      temp=FF_Tensors.At(l*6+5);
      if(temp!=Gradient)
         temp.BufferFree();
      temp=FF_Tensors.At(l*6+1);
      temp.BufferFree();
      if(optimization==SGD)
        {
         temp=FF_Weights.At(l*6+5);
         temp.BufferFree();
        }
      else
        {
         temp=FF_Weights.At(l*9+5);
         temp.BufferFree();
         temp=FF_Weights.At(l*9+8);
         temp.BufferFree();
        }
      inputs=FF_Tensors.At(l*6+2);
     }
//---
   return true;
  }

すべてのクラスとそのメソッドの完全なコードは、添付ファイルにあります。

3.4. ニューラルネットワーク基本クラスの変更

これまでのすべての記事と同様に、新しいクラスを作成した後、基本クラスに変更を加えて、ネットワークが適切に動作するようにします。

新しいクラス識別子を追加しましょう。

#define defNeuronMLMHAttentionOCL 0x7889   ///<Multilayer multi-headed attention neuron OpenCL \details Identified class #CNeuronMLMHAttentionOCL

また、defineブロックには、OpenCLプログラムの新しいカーネルを操作するための定数を追加します。

#define def_k_MHAttentionScore    20 ///< Index of the kernel of the multi-heads attention neuron to calculate score matrix (#MHAttentionScore)
#define def_k_mhas_qkv            0  ///< Matrix of Queries, Keys, Values
#define def_k_mhas_score          1  ///< Matrix of Scores
#define def_k_mhas_dimension      2  ///< Dimension of Key
#define def_k_mhas_mask           3  ///< 1 - calc only previous units, 0 - calc all
//---
#define def_k_MHAttentionOut      21 ///< Index of the kernel of the multi-heads attention neuron to calculate multi-heads out matrix (#MHAttentionOut)
#define def_k_mhao_score          0  ///< Matrix of Scores
#define def_k_mhao_qkv            1  ///< Matrix of Queries, Keys, Values
#define def_k_mhao_out            2  ///< Matrix of Outputs
#define def_k_mhao_dimension      3  ///< Dimension of Key
//---
#define def_k_MHAttentionGradients  22    ///< Index of the kernel for gradients calculation process (#AttentionInsideGradients)
#define def_k_mhag_qkv              0     ///< Matrix of Queries, Keys, Values
#define def_k_mhag_qkv_g            1     ///< Matrix of Gradients to Queries, Keys, Values
#define def_k_mhag_score            2     ///< Matrix of Scores
#define def_k_mhag_score_g          3     ///< Matrix of Scores Gradients
#define def_k_mhag_gradient         4     ///< Matrix of Gradients from previous iteration
#define def_k_mhag_dimension        5     ///< Dimension of Key

また、ニューラルネットワーククラスコンストラクタに新しいカーネルの宣言を追加しましょう。

//--- create kernels
   opencl.SetKernelsCount(23);
   opencl.KernelCreate(def_k_FeedForward,"FeedForward");
   opencl.KernelCreate(def_k_CalcOutputGradient,"CalcOutputGradient");
   opencl.KernelCreate(def_k_CalcHiddenGradient,"CalcHiddenGradient");
   opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum");
   opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam");
   opencl.KernelCreate(def_k_AttentionGradients,"AttentionInsideGradients");
   opencl.KernelCreate(def_k_AttentionOut,"AttentionOut");
   opencl.KernelCreate(def_k_AttentionScore,"AttentionScore");
   opencl.KernelCreate(def_k_CalcHiddenGradientConv,"CalcHiddenGradientConv");
   opencl.KernelCreate(def_k_CalcInputGradientProof,"CalcInputGradientProof");
   opencl.KernelCreate(def_k_FeedForwardConv,"FeedForwardConv");
   opencl.KernelCreate(def_k_FeedForwardProof,"FeedForwardProof");
   opencl.KernelCreate(def_k_MatrixSum,"SumMatrix");
   opencl.KernelCreate(def_k_Matrix5Sum,"Sum5Matrix");
   opencl.KernelCreate(def_k_UpdateWeightsConvAdam,"UpdateWeightsConvAdam");
   opencl.KernelCreate(def_k_UpdateWeightsConvMomentum,"UpdateWeightsConvMomentum");
   opencl.KernelCreate(def_k_Normilize,"Normalize");
   opencl.KernelCreate(def_k_NormilizeWeights,"NormalizeWeights");
   opencl.KernelCreate(def_k_ConcatenateMatrix,"ConcatenateBuffers");
   opencl.KernelCreate(def_k_DeconcatenateMatrix,"DeconcatenateBuffers");
   opencl.KernelCreate(def_k_MHAttentionGradients,"MHAttentionInsideGradients");
   opencl.KernelCreate(def_k_MHAttentionScore,"MHAttentionScore");
   opencl.KernelCreate(def_k_MHAttentionOut,"MHAttentionOut");

И создание нового типа нейронов в конструкторе нейронной сети.

            case defNeuronMLMHAttentionOCL:
               neuron_mlattention_ocl=new CNeuronMLMHAttentionOCL();
               if(CheckPointer(neuron_mlattention_ocl)==POINTER_INVALID)
                 {
                  delete temp;
                  return;
                 }
               if(!neuron_mlattention_ocl.Init(outputs,0,opencl,desc.window,desc.window_out,desc.step,desc.count,desc.layers,desc.optimization))
                 {
                  delete neuron_mlattention_ocl;
                  delete temp;
                  return;
                 }
               neuron_mlattention_ocl.SetActivationFunction(desc.activation);
               if(!temp.Add(neuron_mlattention_ocl))
                 {
                  delete neuron_mlattention_ocl;
                  delete temp;
                  return;
                 }
               neuron_mlattention_ocl=NULL;
               break;

また、新しいクラスのニューロンの処理をCNeuronBaseOCLニューロンの基本クラスのディスパッチメソッドに追加しましょう。

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

bool CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject)
  {
   if(CheckPointer(TargetObject)==POINTER_INVALID)
      return false;
//---
   CNeuronBaseOCL *temp=NULL;
   CNeuronAttentionOCL *at=NULL;
   CNeuronMLMHAttentionOCL *mlat=NULL;
   CNeuronConvOCL *conv=NULL;
   switch(TargetObject.Type())
     {
      case defNeuronBaseOCL:
         temp=TargetObject;
         return calcHiddenGradients(temp);
         break;
      case defNeuronConvOCL:
         conv=TargetObject;
         temp=GetPointer(this);
         return conv.calcInputGradients(temp);
         break;
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
         at=TargetObject;
         temp=GetPointer(this);
         return at.calcInputGradients(temp);
         break;
      case defNeuronMLMHAttentionOCL:
         mlat=TargetObject;
         temp=GetPointer(this);
         return mlat.calcInputGradients(temp);
         break;
     }
//---
   return false;
  }

bool CNeuronBaseOCL::UpdateInputWeights(CObject *SourceObject)
  {
   if(CheckPointer(SourceObject)==POINTER_INVALID)
      return false;
//---
   CNeuronBaseOCL *temp=NULL;
   switch(SourceObject.Type())
     {
      case defNeuronBaseOCL:
      case defNeuronConvOCL:
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         temp=SourceObject;
         return updateInputWeights(temp);
         break;
     }
//---
   return false;
  }

すべてのクラスとそのメソッドの完全なコードは、添付ファイルにあります。


4. テスト

新しいアーキテクチャをテストするために、Fractal_OCL_AttentionMLMHとFractal_OCL_AttentionMLMH_v2の2つのエキスパートアドバイザーが作成されました。これらのEAは、前の記事のEAに基づいて作成されており、Attentionブロックのみが置き換えられています。Fractal_OCL_AttentionMLMH EAには、8つのself-attentionヘッドを備えた5層ブロックがあります。2番目のEAでは、12個のself-attentionヘッドを備えた12層ブロックを使用します。

ニューラルネットワークの新しいクラスは、以前のテストで使用されたものと同じデータセットでテストされました(H1時間枠のEURUSD、最後の20個のローソク足の履歴データをニューラルネットワークに供給)。

テスト結果で、より多くのパラメータがより長い訓練期間を必要とするという仮定が確認されました。最初の訓練エポックでは、パラメータが少ないエキスパートアドバイザーがより安定した結果を示していますが、訓練期間が延長されると、多数のパラメータを持つエキスパートアドバイザーがより良い値を示すようになります。一般に、33エポック後、Fractal_OCL_AttentionMLMH_v2のエラーはFractal_OCL_AttentionMLMH EAのエラーレベルを下回り、さらに低いままでした。

欠落したパターンパラメータは同様の結果を示しました。訓練の開始時には、Fractal_OCL_AttentionMLMH_v2 EAの不均衡なパラメータがパターンの50%以上を見逃していましたが、訓練をさらに進めると、この値は減少し、27エポック後に3〜5%で安定しました。パラメータのより少ないEAはよりスムーズな結果を示すと同時にパターンの10〜16%を見逃しました。  

 

パターン予測の精度に関しては、両方のエキスパートアドバイザーが22〜23%のレベルで同等の結果を示しました。

 


終わりに

本稿では、OpenAIによって提示されたGPTアーキテクチャに類似した、新しいクラスのAttentionニューロンを作成しました。もちろん、これらのアーキテクチャの訓練と操作では時間とリソースを大量に消費するため、これらのアーキテクチャを完全な形で繰り返し訓練することは不可能です。ただし、ここで作成したオブジェクトは、自動売買ロボット作成の目的を持つニューラルネットワークでうまく使用できます。 


参照文献

  1. ニューラルネットワークが簡単に
  2. ニューラルネットワークが簡単に(第2回): ネットワークの訓練とテスト
  3. ニューラルネットワークが簡単に(第3回): コンボリューションネットワーク
  4. ニューラルネットワークが簡単に(第4回): リカレントネットワーク
  5. ニューラルネットワークが簡単に(第5回): OPENCLでのマルチスレッド計算
  6. ニューラルネットワークが簡単に(第6回): ニューラルネットワークの学習率を実験する
  7. ニューラルネットワークが簡単に(第7回): 適応的最適化法
  8. ニューラルネットワークが簡単に(第8回): アテンションメカニズム
  9. ニューラルネットワークが簡単に(第9部): 作業の文書化
  10. ニューラルネットワークが簡単に(第10回): Multi-Head Attention
  11. 教師なし学習による言語理解の改善(英語)
  12. より良い言語モデルとその影響(英語)
  13. GPT3の仕組み - 視覚化とアニメーション


記事で使用されたプログラム

# 名称 種類 説明
1 Fractal_OCL_AttentionMLMH.mq5   エキスパートアドバイザー GTPアーキテクチャを使用した分類ニューラルネットワーク(出力層に3つのニューロン)と5つのAttention層を備えたエキスパートアドバイザー
2 Fractal_OCL_AttentionMLMH_v2.mq5 エキスパートアドバイザー   GTPアーキテクチャを使用した分類ニューラルネットワーク(出力層に3つのニューロン)と12のAttention層を備えたエキスパートアドバイザー 
3 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
4 NeuroNet.cl コードベース OpenCLプログラムコードライブラリ
5 NN.chm HTMLヘルプ コンパイル済みのCHMファイル


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

添付されたファイル |
MQL5.zip (2306.29 KB)
DoEasyライブラリでのその他のクラス(第67部): チャットオブジェクトクラス DoEasyライブラリでのその他のクラス(第67部): チャットオブジェクトクラス
本稿では、(単一の取引製品チャートの)チャートオブジェクトクラスを作成し、MQL5シグナルオブジェクトのコレクションクラスを改善して、コレクションに格納されている各シグナルオブジェクトでリストの更新時にすべてのパラメータが更新されるようにします。
パターン検索への総当たり攻撃アプローチ(第IV部): 最小限の機能 パターン検索への総当たり攻撃アプローチ(第IV部): 最小限の機能
本稿では、前の記事で設定した目標に基づいて改良された総当たり攻撃バージョンについてお話します。エキスパートアドバイザーをこの方法で取得した設定で使用して、このトピックをできるだけ広くカバーするようにします。新しいプログラムバージョンも添付されています。
MVCデザインパターンとその可能なアプリケーション MVCデザインパターンとその可能なアプリケーション
本稿では、人気高いMVCパターンと、MQLプログラムでの使用の可能性、長所、短所について説明します。アイデアは、既存コードをモデル、ビュー、コントローラの3つの別々のコンポーネントに分割することです。
グリッドおよびマーチンゲール取引システムでの機械学習 - あなたはそれに賭けますか グリッドおよびマーチンゲール取引システムでの機械学習 - あなたはそれに賭けますか
本稿では、グリッドおよびマーチンゲール取引に適用される機械学習手法について説明します。驚いたことに、世界中のネットではこのアプローチはほとんどまたはまったくカバーされていません。記事を読んだ後は、自分自身の自動売買ボットを作成することができるでしょう。