English Русский 中文 Español Deutsch Português
preview
ニューラルネットワークが簡単に(第24部):転移学習用ツールの改善

ニューラルネットワークが簡単に(第24部):転移学習用ツールの改善

MetaTrader 5トレーディングシステム | 21 11月 2022, 10:05
208 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容

はじめに

当連載の前回の記事では、転移学習テクノロジーを利用するツールを作成しました。作業の結果、すでに訓練済みのモデルを編集できるツールを手に入れました。このツールを使用すると、事前訓練したモデルから任意の数のニューラル層を取得できます。もちろん、制限条件はあり、取得できるのは最初のデータ層から始まる連続した層のみです。このアプローチの理由は、モデルの訓練時に使用されるものと同様の初期データのみでうまく機能するというニューラルネットワークの性質にあります。

さらに、作成されたツールは訓練済みのモデルを編集できるだけではありません。完全に新しいものを作成することもできます。これにより、プログラムコードでモデルアーキテクチャを記述する必要がなくなり、ツールを使用してモデルを記述するだけで済みます。次に、作成したニューラルネットワークをファイルからアップロードすることで、モデルを追跡して使用します。これにより、プログラムコードを変更することなく、さまざまなアーキテクチャを試すことができます。プログラムの再コンパイルさえ必要ありません。モデルファイルを変更するだけです。

このような便利なツールは、できるだけ使いやすいべきです。したがって、この記事では、その使いやすさを改善しようとします。


1.ニューラル層に関する完全な情報の表示

各ニューラル層に関する情報量を増やすことで、ツールの使いやすさを向上させましょう。覚えていらっしゃると思いますが、前回の記事では、訓練済みモデルの各ニューラル層のアーキテクチャに関するすべての可能な情報を収集しましたが、ユーザーに示されたまのは、ニューラル層のタイプと出力ニューロンの数だけでした。これは、1つのモデルで作業し、そのアーキテクチャを覚える場合には問題ありませんが、多数のモデルを実験する場合、この量の情報では明らかに十分ではありません。

一方、情報が多いほど、情報ボードに多くのスペースが必要になります。おそらく、モデル情報ウィンドウに横スクロールを追加するのは良くないでしょう。したがって、各ニューラル層に関する情報を数行で表示することにしました。出力情報は読みやすくなければなりません。理解するのが難しい、文字の巨大な塊のように見えてはなりません。テキストをブロックに分割するために、2つの連続するニューラル層の説明の間に視覚的な区切りを挿入しましょう。

テキストを数行に分割するのは単純な解決策のように思えますが、実装するには非標準的なアプローチも必要でした。ポイントは、CListViewリストクラスを使用して、モデルのアーキテクチャに関する情報を表示することです。その中の各行は、リストの個別の要素を表します。また、1つの要素を複数の行に表示することと、複数の要素を1つのエンティティにグループ化することの両方をおこなうことはできません。このような機能を追加するには、アルゴリズムとクラスアーキテクチャを変更する必要があります。実際には、これにより新しいクラスのコントロールオブジェクトが作成されます。この場合、CListViewクラスから継承するか、まったく新しい要素を作成できますが、これには私が計画していなかった多大な労力が必要です。

したがって、既存のクラスを使用することにしました。クラスコードを変更することなく、いくつかの微調整をおこないました。前述のように、区切り文字を使用してテキストを個々のニューラル層のブロックに視覚的に分割します。区切り文字は、モデルアーキテクチャの説明を含むテキスト全体を個別のニューラル層のブロックに分割します。また、各ニューラル層の情報を視覚的にグループ化します。

しかし、視覚的なグループ化に加えて、リスト要素がどのニューラル層に属しているかをプログラムレベルで理解する必要もあります。前回の記事では、訓練済みモデルの別のニューラル層をマウスで選択し、新しいモデルに追加されたニューラル層のリストから選択した層を削除することで、コピーされたニューラル層の数の変更を実装しました。どちらの場合も、選択した要素と特定のニューラル層の間の対応を明確に理解する必要があります。

各要素をリストに追加するとき、そのテキストと数値を指定しました。通常、選択した要素をすばやく識別するために数値が使用されます。以前は、各要素に個別の値を指定していましたが、複数の要素に対して1つの値を使用することもできます。もちろん、このアプローチでは、リストの各要素を識別するのが難しくなります。ただし、これは今はやりません。要素のグループを識別するだけです。したがって、この機能を使用すると、単一の要素ではなく、要素のグループ全体を識別できます。

bool  AddItem( 
   const string  item,     // text 
   const long    value     // value 
   )

実際、この解決策には別の利点があります。CListViewクラスにはSelectByValueメソッドがあります。このメソッドの主な目的は、数値によって要素を選択することです。そのアルゴリズムは、リストのすべての要素の中から指定された数値を持つ最初の要素を見つけて選択することです。リスト選択変更イベントの処理を整理することで、ユーザーは選択した要素の値を読み取り、この値を持つリストから最初の要素を選択するようにクラスに依頼できます。これにより、グループの始まりが視覚化されます。これはかなり便利な機能だと思います。

bool  SelectByValue( 
   const long  value     // value 
   )

ここで、説明したアプローチの実装を調べてみましょう。まず、ニューラル層アーキテクチャの説明のテキスト表現を実装して、パネルに表示します。この目的のために、LayerDescriptionToStringメソッドを作成しましょう。このメソッドは、ニューラル層アーキテクチャ記述のオブジェクトへのポインタと、ニューラル層のテキスト記述が書き込まれる文字列の動的配列へのポインタをパラメータで受け取ります。配列の各要素は、モデルアーキテクチャの説明リストの個別の行です。上記の各要素は、1つのニューラル層を記述するためのリスト内の要素の個別のグループです。動的配列を使用することで、特定のニューラル層を記述する必要性に応じて、さまざまなサイズの要素グループを編成できます。

このメソッドは、配列内の要素の数を受け取ります。

int CNetCreatorPanel::LayerDescriptionToString(const CLayerDescription *layer, string& result[])
  {
   if(!layer)
      return -1;

メソッドの本体では、まず、受信したニューラル層アーキテクチャの記述へのポインタの有効性を確認します。

次に、ローカル変数を準備し、結果の動的配列をクリアします。

   string temp;
   ArrayFree(result);

次に、そのタイプに応じてニューラル層のテキスト記述を作成します。文字列の動的配列はすぐには使用しません。代わりに、説明全体が1つの文字列に書き込まれます。ただし、文字列を分割する必要がある場所に区切り文字を挿入します。この例では、バックスラッシュ「\」を使用しました。StringFormat関数を使用して、このマークアップでテキストを適切に作成しました。この関数は、最小限の労力で書式設定されたテキストを生成します。

ニューラル層アーキテクチャの書式設定された文字列記述を作成した後、StringSplit関数を使用してテキストを行に分割します。この関数は、前のステップでテキストに慎重に追加された区切り要素に従って、テキストを行に分割します。この関数を使用する便利さは、動的配列のサイズを必要なサイズに増加させることにもあります。この部分を制御する必要はありません。

   switch(layer.type)
     {
      case defNeuronBaseOCL:
         temp = StringFormat("Dense (outputs %d, \activation %s, \optimization %s)", 
                layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronConvOCL:
         temp = StringFormat("Convolution (outputs %d, \window %d, step %d, window out %d, \activation %s, \optimization %s)",
                layer.count * layer.window_out, layer.window, layer.step, layer.window_out, EnumToString(layer.activation),

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronProofOCL:
         temp = StringFormat("Proof (outputs %d, \window %d, step %d, \optimization %s)",
                layer.count, layer.window, layer.step, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronAttentionOCL:
         temp = StringFormat("Self Attention (outputs %d, \units %s, window %d, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronMHAttentionOCL:
         temp = StringFormat("Multi-Head Attention (outputs %d, \units %s, window %d, heads %s, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.step, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronMLMHAttentionOCL:
         temp = StringFormat("Multi-Layer MH Attention (outputs %d, \units %s, window %d, key size %d, \heads %s, layers %d,
                              \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.window_out, layer.step, layer.layers,

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronDropoutOCL:
         temp = StringFormat("Dropout (outputs %d, \probability %d, \optimization %s)",
                layer.count, layer.probability, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronBatchNormOCL:
         temp = StringFormat("Batchnorm (outputs %d, \batch size %d, \optimization %s)",
                layer.count, layer.batch, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronVAEOCL:
         temp = StringFormat("VAE (outputs %d)", layer.count);
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      case defNeuronLSTMOCL:
         temp = StringFormat("LSTM (outputs %d, \optimization %s)", layer.count, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;

      default:	
         temp = StringFormat("Unknown type %#x (outputs %d, \activation %s, \optimization %s)",
                layer.type, layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
     }

既知のすべてのニューラル層の説明を作成したら、未知のタイプの標準的な説明を1つ追加することを忘れないでください。したがって、未知のニューラル層の検出についてユーザーに通知し、モデルの整合性の意図しない違反から保護することができます。

メソッドの最後で、結果の配列のサイズを呼び出し元に返します。

//---
   return ArraySize(result);
  }

次に、以前の記事で既に説明したLoadModelメソッドに進みます。メソッド全体を変更するのではなく、要素をリストに追加するループの本体のみを変更します。前と同様に、ループ本体では、最初に動的配列から次の層記述オブジェクトへのポインタを取得します。受信したポインタの有効性を即座に確認します。

   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;

次に、文字列の動的配列を準備し、上記のLayerDescriptionToStringメソッドを呼び出して、ニューラル層のテキスト記述を生成します。メソッドが完了すると、文字列の説明の配列とその中の要素の数を取得します。エラーが発生した場合、メソッドは配列サイズの代わりに空の配列と-1を返します。エラーについてユーザーに通知し、メソッドを完了します。

      string items[];
      int total_items = LayerDescriptionToString(temp, items);
      if(total_items < 0)
        {
         printf("%s %d Error at layer %d: %d", __FUNCSIG__, __LINE__, i, GetLastError());
         return false;
        }

説明テキストが正常に生成されたら、最初にブロック区切り要素を追加します。次に、入れ子になったループで、ニューラル層を記述するテキスト配列の内容全体を出力します。

      if(!m_lstPTModel.AddItem(StringFormat("____ Layer %d ____", i + 1), i + 1))
         return false;
      for(int it = 0; it < total_items; it++)
         if(!m_lstPTModel.AddItem(items[it], i + 1))
            return false;
     }

グループIDを指定するときは、モデルの説明を含む動的配列のニューラル層の序数に1を追加することに注意してください。これは、配列のインデックスは0から始まるために必要です。数値識別子として0を指定すると、CListViewクラスは、リスト内の要素の総数に自動的に置き換えます。グループIDの代わりにランダムな値を受け取りたくありません。

LoadModelメソッドコードの残りの部分は変更されていません。コード全体は添付ファイルに記載されています。また、添付ファイルには、プログラムで使用されるすべてのメソッドとクラスのコードが含まれています。特に、新しいChangeNumberOfLayersモデルの説明を表示するメソッドに同様の追加が見られます。

ChangeNumberOfLayersメソッドでは、モデルに関する情報が、モデルアーキテクチャの説明を含む2つの動的配列から収集されることに注意してください。1つ目は、ドナーモデルのアーキテクチャについて説明します。そこから複製されたニューラル層の記述をとります。2番目の配列には、追加するニューラルネットワークの説明が含まれています。

モデルアーキテクチャの説明を出力した後、作成されたリストの状態の変化のイベントを処理するメソッドに進みます。

ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)

上記のように、ユーザーがリスト内の任意の行を選択すると、指定されたブロックの最初の行に選択が移動します。このために、ユーザーが選択した要素のグループIDを取得し、指定されたIDを持つ最初の要素を選択するようにプログラムに指示するだけです。この操作は、SelectByValueメソッドによって実装されます。

bool CNetCreatorPanel::OnChangeListNewModel(void)
  {
   long value = m_lstNewModel.Value();
//---
   return m_lstNewModel.SelectByValue(value);
  }

これにより、モデルアーキテクチャに関する表示情報が展開されます。情報量は最小限で十分であり、ニューラル層のタイプに固有です。そのため、ユーザーは特定のニューラル層に関する関連情報のみを見ることができます。さらに、ウィンドウを乱雑にする余分な情報はありません。


2.使用済み入力フィールドの有効化/未使用入力フィールドの無効化

次の変更は、データ入力フィールドに関するものです。奇妙に思えるかもしれませんが、想像力のためにかなり大きなフィールドが提供されてます。おそらく最初に目を引くのは、入力情報の量です。パネルには、ニューラル層アーキテクチャを記述するCLayerDescriptionクラスのすべての要素の入力フィールドが用意されています。それが悪いとは言いません。ユーザーは、層を追加する前に、指定されたすべてのデータを表示し、必要に応じていつでも任意の順序で変更できます。ただし、これらのフィールドのすべてがすべてのニューラル層に関連しているわけではないことがわかっています。

たとえば、完全に全結合ニューラル層の場合、ニューロンの数、活性化関数、およびパラメータの最適化方法の3つのパラメータのみを指定するだけで十分です。残りのパラメータは関係ありません。畳み込みニューラル層を扱う場合、入力データウィンドウのサイズとそのステップを指定する必要があります。出力要素の数は、ソースデータのバッファサイズと指定された2つのパラメータによって異なります。

再帰型LSTMブロックでは、活性化関数はブロックアーキテクチャによって定義されるため、それらを指定する必要はありません。 

ユーザーはこれらすべての特徴を知っているかもしれませんが、適切に設計されたツールは、「機械的な」エラーの可能性についてユーザーに警告する必要があります。可能な予防オプションは2つあります。パネルから無関係な要素を削除するか、単純に編集不可にすることです。

各オプションには長所と短所があります。最初のオプションの利点には、パネル上の入力フィールドの数が減ることが含まれるので、パネルをよりコンパクトにすることができます。欠点は、実装がより複雑になることです。毎回パネル上の要素を再配置する必要があるためです。同時に、オブジェクトの絶え間ない再配置は、ユーザーを混乱させ、エラーにつながる可能性があります。

私の意見では、この方法の使用は、大量のデータを入力する必要がある場合に適しています。その場合不要なオブジェクトを削除すれば、パネルがコンパクトになり、すっきりします。

要素の数が少ない場合は、2番目のオプションを使用できます。パネル上のすべての要素を一度に簡単に配置できます。さらに、不必要にパネル内を移動してユーザーを混乱させることもありません。ユーザーは自分の位置を視覚的に覚えているため、全体的なパフォーマンスが向上します。

インターフェイスパネルにすべての入力フィールドを配置しました。したがって、2番目の実装オプションが受け入れられると考えています。

アーキテクチャ上の解決策はすでにありますが、もう少し先に進みます。パネルには、ドロップダウンリストのあるフィールドと直接入力フィールドがあります。ドロップダウンフィールドでは、使用可能なオプションの1つだけを選択できますが、値入力フィールドでは、任意のテキストを物理的に入力できます。

ただし、そこでは整数値を期待しています。論理的には、作成されたニューラル層のアーキテクチャを記述するオブジェクトに情報を渡す前に、入力された情報のチェックを追加する必要があります。情報の正確性をユーザーと共有するために、入力された情報は、ユーザーがテキストを入力した直後に検証されます。検証後、ユーザーがフィールドに入力した情報を、ツールによって受け入れられた情報に置き換えます。したがって、ユーザは、入力された情報と読み取られた情報の違いを確認して、必要に応じて、さらにデータを修正できます。

そして。CLayerDescriptionクラスでニューラル層アーキテクチャを記述する場合、2つの目的を持つ要素があります。たとえば、畳み込み層とサブサンプル層のstepは、ソースデータウィンドウのステップを指定します。ただし、Attention ニューラル層を記述する際に、Attention ヘッドの数を指定するために同じパラメータが使用されます。

window_outパラメータは、畳み込み層のフィルタの数と注意ブロックの内部キー層のサイズを指定します。

インターフェイスをより使いやすくするには、適切なタイプのニューラル層を選択するときにテキストラベルを変更することをお勧めします。

インターフェイスウィンドウの再配置の問題でユーザーが混乱することがなくなります。フィールド自体は変更されず、その隣の情報だけが変更されます。ユーザーが新しいデータに注意を払わず、対応するフィールドに情報を自動的に入力しても、モデルの編成にエラーが発生することはありません。いずれの場合でも、データは層アーキテクチャの記述の目的の要素に送信されます。

上記の解決策を実装するには、一歩下がって準備作業をおこなう必要があります。

まず、インターフェイスパネルでテキストラベルを作成するときに、対応するオブジェクトへのポインタを保存していませんでした。それらのいくつかのテキストを変更する必要がある場合、オブジェクトの一般的な配列でそれらを探す必要があります。これを避けるために、CreateLabelテキストラベル作成メソッドに戻りましょう。メソッド操作の完了時に、論理結果の代わりに、作成されたオブジェクトへのポインタを返しましょう。

CLabel* CNetCreatorPanel::CreateLabel(const int id, const string text,
                                      const int x1, const int y1,
                                      const int x2, const int y2
                                     )
  {
   CLabel *tmp_label = new CLabel();
   if(!tmp_label)
      return NULL;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return NULL;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return NULL;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return NULL;
     }
//---
   return tmp_label;
  }

もちろん、すべてのラベルへのポインタを保存するわけではありません。2つのオブジェクトのみを保存します。これをおこなうには、さらに2つの変数を宣言します。オブジェクトへの動的ポインタを使用しますが、ツールクラスのデストラクタには追加しません。これらのオブジェクトは、すべてのツールオブジェクトの配列から引き続き削除されます。しかし同時に、必要なオブジェクトに直接アクセスできます。

   CLabel*           m_lbWindowOut;
   CLabel*           m_lbStepHeads;

クラスのCreateメソッドで、新しい変数へのポインタを書き込みます。メソッドには、以下に示す小さな変更が必要です。メソッドコードの残りの部分は変更されません。メソッドの完全なコードは、以下の添付ファイルに記載されています。

bool CNetCreatorPanel::Create(const long chart, const string name,
                              const int subwin, const int x1, const int y1)
  {
   if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT))
      return false;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbStepHeads = CreateLabel(8, "Step", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbStepHeads)
      return false;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbWindowOut = CreateLabel(9, "Window Out", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbWindowOut)
      return false;
//---
...............
...............
//---
   return true;
  }

準備作業の次のステップは、入力フィールドのステータスを変更するメソッドを作成することです。標準のCEditクラスには、オブジェクトのステータスを変更するためのReadOnly構造が既にあります。ただし、この方法ではステータスを視覚化することはできません。データを入力する可能性をロックするだけです。ただし、入力に使用できるオブジェクトと使用できないオブジェクトを視覚的に分離する必要があります。新しいものを発明するつもりはありません。背景色でオブジェクトを強調表示しましょう。編集可能なフィールドの背景は白になり、編集不可能なフィールドの背景色はパネルの色と一致します。

この機能は、EditReedOnlyメソッドで実装されます。メソッドパラメータで、オブジェクトへのポインタと新しいステータスフラグを渡します。メソッド本体では、受け取ったフラグを入力オブジェクトのReadOnlyメソッドに渡し、指定されたフラグに従ってオブジェクトの背景を設定します。

bool CNetCreatorPanel::EditReedOnly(CEdit& object, const bool flag)
  {
   if(!object.ReadOnly(flag))
      return false;
   if(!object.ColorBackground(flag ? CONTROLS_DIALOG_COLOR_CLIENT_BG : CONTROLS_EDIT_COLOR_BG))
      return false;
//---
   return true;
  }

次に、活性化関数に注意を向け、使用可能な活性化関数のドロップダウンリストに移動します。すべてのニューラル層タイプにドロップダウンリストが必要なわけではありません。一部のアーキテクチャは、リストによって変更できない事前定義された活性化関数タイプを提供します。この例は、LSTMブロック、サブサンプリング層、Attentionブロックです。ただし、CComboBoxクラスには、クラスの機能をブロックするメソッドはありません。したがって、回避策を使用し、使用可能な活性化関数のリストをケースバイケースで変更します。使用可能な活性化関数のリストを設定するための別のメソッドを作成します。

実際、そのようなメソッドは2つしかありません。それらの1つは一般的なもので、活性化関数を示すActivationListMainです。2つ目は空のActivationListEmptyで、これには「None」という選択肢が1つしかありません。

メソッド構築アルゴリズムを理解するために、ActivationListMainメソッドのコードを考えてみましょう。メソッドの開始時に、使用可能な活性化関数の要素の既存のリストをクリアします。次に、ItemAddメソッドとEnumToString関数を使用してループ内のリストに入力します。

ここで、活性化関数列挙の要素のエンコードは、Noneを表す-1で始まることに注意してください。次の関数である双曲線正接TANHのインデックスは0です。これは、説明のリストの入力を説明する際に前述した理由から、良くありません。ドロップダウンリストはCListViewクラスであるためです。したがって、リスト識別子のnull値を除外するには、列挙型識別子に小さな定数を追加するだけです。

使用可能な活性化関数のリストを作成したら、デフォルト値を設定してメソッドを終了します。

bool CNetCreatorPanel::ActivationListMain(void)
  {
   if(!m_cbActivation.ItemsClear())
      return false;
   for(int i = -1; i < 3; i++)
      if(!m_cbActivation.ItemAdd(EnumToString((ENUM_ACTIVATION)i), i + 2))
         return false;
   if(!m_cbActivation.SelectByValue((int)DEFAULT_ACTIVATION + 2))
      return false;
//---
   return true;
  }

必要な別のメソッドは、ユーザーの作業を少し自動化するのに役立ちます。前述のように、畳み込みモデルまたはAttentionブロックの場合、モデルの出力での要素の数は、分析された初期データのウィンドウのサイズとその移動ステップに依存します。エラーの可能性を排除し、ユーザーの手作業を減らすために、ブロック数の入力フィールドを閉じて、別のSetCountsメソッドで埋めることにしました。

このメソッドのパラメータでは、作成されたニューラル層のタイプを渡します。このメソッドは、操作のブール結果を返します。

bool CNetCreatorPanel::SetCounts(const uint position, const uint type)
  {
   const uint position = m_arAddLayers.Total();

メソッド本体では、まず前の層の出力の要素数を決定します。前の層は、ドナーモデルのアーキテクチャの記述または新しいニューラル層を追加するためのアーキテクチャの記述の2つの動的配列のいずれかにある可能性があります。最後のニューラル層をどこから取得するかは簡単に判断できます。ニューラル層は常にリストの最後に追加されます。したがって、新しいニューラル層の配列が空の場合にのみ、ドナーモデルから層を取得します。このロジックに従って、新しいニューラル層の動的配列のサイズを確認します。そのサイズに応じて、対応する配列から前のニューラル層へのポインタを要求します。

   CLayerDescription *prev;
   if(position <= 0)
     {
      if(!m_arPTModelDescription || m_spPTModelLayers.Value() <= 0)
         return false;
      prev = m_arPTModelDescription.At(m_spPTModelLayers.Value() - 1);
      if(!prev)
         return false;
     }
   else
     {
      if(m_arAddLayers.Total() < (int)position)
         return false;
      prev = m_arAddLayers.At(position - 1);
     }
   if(!prev)
      return false;

次に、前の層の結果バッファ内の要素の数をそのタイプに従ってカウントします。バッファサイズが0より大きくない場合は、falseでメソッドを終了します。

   int outputs = prev.count;
   switch(prev.type)
     {
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         outputs *= prev.window;
         break;
      case defNeuronConvOCL:
         outputs *= prev.window_out;
         break;
     }
//---
   if(outputs <= 0)
      return false;

次に、解析された初期データウィンドウサイズとそのステップの値をインターフェイスから読み取ります。また、計算結果を記録する変数を用意します。

   int counts = 0;
   int window = (int)StringToInteger(m_edWindow.Text());
   int step = (int)StringToInteger(m_edStep.Text());

要素の数は、作成されるニューラル層のタイプに応じて計算されます。畳み込み層とサブサンプリング層の要素数を計算するには、分析された入力データウィンドウのサイズとそのステップが必要です。

   switch(type)
     {
      case defNeuronConvOCL:
      case defNeuronProofOCL:
         if(step <= 0)
            break;
         counts = (outputs - window - 1 + 2 * step) / step;
         break;

Attentionブロックを使用する場合、ステップサイズはウィンドウサイズと同じです。数学的規則を使用して、数式を減らします。

      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         if(window <= 0)
            break;
         counts = (outputs + window - 1) / window;
         break;

変分オートエンコーダの潜在層を使用する場合、層のサイズは前の層のちょうど2分の1になります。

      case defNeuronVAEOCL:
         counts = outputs / 2;
         break;

他のすべてのケースでは、ニューラル層のサイズを前の層のサイズと等しくなるように設定します。これは、バッチ正規化またはドロップアウト層を宣言するときに使用できます。

      default:
         counts = outputs;
         break;
     }
//---
   return m_edCount.Text((string)counts);
  }

受け取った値を対応するインターフェイス要素に転送します。

これで、作成するニューラル層のタイプに応じてインターフェイスの変更を整理するための十分な手段が得られました。それでは、どのようにできるか見てみましょう。この機能はOnChangeNeuronTypeメソッドに実装されています。ユーザーがニューラル層のタイプを変更するたびに呼び出すため、この名前が付けられています。

指定されたメソッドにはパラメータが含まれておらず、操作の論理結果が返されます。メソッド本体では、まずユーザーが選択したニューラル層のタイプを定義します。

bool CNetCreatorPanel::OnChangeNeuronType(void)
  {
   long type = m_cbNewNeuronType.Value();

さらに、アルゴリズムは、選択したニューラル層のタイプに応じて分岐します。各ニューラル層のアルゴリズムは似ています。しかし、ほとんどすべてのニューラル層には独自のニュアンスがあります。完全に接続されたニューラル層の場合、ニューロンの数に対してアクティブな入力フィールドを1つだけ残し、可能な活性化関数の完全なリストを読み込みます。

   switch((int)type)
     {
      case defNeuronBaseOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListMain())
            return false;
         break;

畳み込み層の場合、さらに3つの入力フィールドがアクティブになります。これらには、分析されたソースデータウィンドウとそのステップのサイズ、および結果ウィンドウのサイズ(フィルタの数)が含まれます)。また、2つのテキストラベルの値を更新し、ソースデータウィンドウのサイズとステップに応じてニューラル層の要素数の再計算を再開します。1つのフィルタの要素数をカウントすることに注意してください。したがって、結果は使用するフィルタの数に依存しません。

      case defNeuronConvOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!m_lbWindowOut.Text("Window Out"))
            return false;
         if(!ActivationListMain())
            return false;
         if(!SetCounts(defNeuronConvOCL))
            return false;
         break;

サブサンプリング層については、フィルタの数と活性化関数を指定しません。ここでの実装では、サブサンプリング層の活性化関数として常に最大値を使用します。したがって、使用可能な活性化関数のリストをクリアします。ただし、畳み込み層と同様に、作成された層の要素数の計算を開始します。

      case defNeuronProofOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!SetCounts(defNeuronProofOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

LSTMブロックを宣言すると、活性化関数のリストも使用されないため、クリアします。使用できる入力フィールドはニューラル層の要素数の1つだけです。

      case defNeuronLSTMOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

ドロップアウト層を初期化するには、ニューロンのドロップアウト確率の値のみを指定する必要があります。活性化関数は使用しません。要素の数は、前のニューラル層のサイズと同じです。

      case defNeuronDropoutOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, false) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronDropoutOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

同様のアプローチがバッチ正規化層にも適用されます。ただし、ここではバッチサイズを指定します。

      case defNeuronBatchNormOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, false) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronBatchNormOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Attentionメソッドに応じて、ブロック内のAttentionヘッドとニューラル層の数の入力フィールドをアクティブにします。対応する入力フィールドのテキストラベルが変更されます。

      case defNeuronAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

      case defNeuronMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!SetCounts(defNeuronMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

      case defNeuronMLMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, false) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!m_lbWindowOut.Text("Keys size"))
            return false;
         if(!SetCounts(defNeuronMLMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

変分オートエンコーダの潜在層の場合、データを入力する必要はありません。層タイプのみを選択し、モデルに追加します。

      case defNeuronVAEOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         if(!SetCounts(defNeuronVAEOCL))
            return false;
         break;

パラメータで指定されたニューラル層のタイプが見つからない場合は、メソッドをfalseで完了します。

      default:
         return false;
         break;
     }
//---
   return true;
  }

メソッドのすべての操作が正常に完了した場合は、trueで終了します。

ここで、説明したメソッドの開始を適切なタイミングで整理する必要があります。層タイプの選択要素の値の変更に関連するイベントを使用し、適切なイベントハンドラを追加します。

EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
EVENT_MAP_END(CAppDialog)

上記のメソッドを実装することにより、選択したニューラル層のタイプに応じて入力フィールドのアクティブ化と非アクティブ化を整理しましたが、データ入力制御についても説明しました。

すべての入力フィールドで、ゼロより大きい整数が必要です。唯一の例外は、ドロップアウト層での要素のドロップアウト確率の値です。これは、0から1の間の実数値です。したがって、入力されたデータを検証するには2つのメソッドが必要です。1つは確率用で、もう1つは他のすべての要素用です。

両メソッドのアルゴリズムは非常に単純です。まず、ユーザーが入力したテキスト値を読み取り、数値に変換して、有効な値の範囲内にあるかどうかを確認します。受信した値をインターフェイスの対応するウィンドウに入力します。ユーザーは、データが正しく解釈されているかどうかのみを確認する必要があります。

bool CNetCreatorPanel::OnEndEditProbability(void)
  {
   double value = StringToDouble(m_edProbability.Text());
   return m_edProbability.Text(DoubleToString(fmax(0, fmin(1, value)), 2));
  }

bool CNetCreatorPanel::OnEndEdit(CEdit& object)
  {
   long value = StringToInteger(object.Text());
   return object.Text((string)fmax(1, value));
  }

確率値の正しさを確認するとき、入力フィールドを明確に識別することに注意してください。ただし、2番目のメソッドでオブジェクトを識別するには、関連するオブジェクトポインタをメソッドパラメータに渡します。ここに別の課題があります。提案されたイベント処理マクロには、呼び出し元オブジェクトのポインタをイベント処理メソッドに渡すための適切なマクロがありません。したがって、そのようなマクロを追加する必要があります。

#define ON_EVENT_CONTROL(event,control,handler)          if(id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) \
                                                              { handler(control); return(true); }

入力フィールドの中には、分析されたソースデータウィンドウのサイズとそのステップが含まれる場合があります。これらのパラメータは、ニューラル層の要素数に影響します。したがって、それらの値を変更するときは、作成されたニューラル層のサイズを再計算する必要があります。ただし、使用するイベント処理モデルでは、イベントごとに1つのハンドラしか許可されません。同時に、1つのハンドラを使用してさまざまなイベントを処理できます。したがって、最初にウィンドウサイズとそのステップの入力フィールドの値を確認する別のメソッドを作成しましょう。次に、選択したニューラル層のタイプを考慮して、ニューラル層のサイズを再計算するメソッドを呼び出します。

bool CNetCreatorPanel::OnChangeWindowStep(void)
  {
   if(!OnEndEdit(m_edWindow) || !OnEndEdit(m_edStep))
      return false;
   return SetCounts((uint)m_cbNewNeuronType.Value());
  }

あとは、イベントハンドラマップを完成させるだけです。これにより、適切なタイミングで適切なイベントハンドラを実行できます。

EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
ON_EVENT(ON_END_EDIT, m_edWindow, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edStep, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edProbability, OnEndEditProbability)
ON_EVENT_CONTROL(ON_END_EDIT, m_edCount, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edWindowOut, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edLayers, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edBatch, OnEndEdit)
EVENT_MAP_END(CAppDialog)


3.キーボードイベント処理の追加

転移学習ツールをより便利で使いやすいものにするために、良い仕事をしました。しかし、これらの改善はすべて、マウスやタッチパッドで使いやすくするためにインターフェイスに焦点を当てています.ただし、キーボードを使用してツールを操作する可能性は実装していません。たとえば、上矢印と下矢印を使用して、コピーするニューラル層の数を変更すると便利な場合があります。Deleteキーを押すと、作成中のモデルから選択したニューラル層を削除するメソッドを呼び出すことができます。

ここでは、このトピックについて深く掘り下げることはしません。ほんの数行のコードで、既存のイベントハンドラにキー処理を追加する方法を紹介します。

上記で提案した3つの機能はすべて、ツールコードに既に実装されています。特定のイベントが発生したときに実行されます。選択したニューラル層を削除するには、パネルに別のボタンがあります。コピーするニューラル層の数は、CSpinEditオブジェクトのボタンを使用して変更します。

技術的には、キーボードのボタンを押すことは、マウスのボタンを押すか移動することと同じイベントです。また、OnChartEvent関数によって処理されます。そのため、クラスのChartEventメソッドが呼び出されます。

キーストロークイベントが発生すると、CHARTEVENT_KEYDOWNイベントIDを受け取ります。lparam変数には、押されたキーのIDが格納されます。

このプロパティを使用して、キーボードをいじって、関心のあるすべてのキーの識別子を決定できます。たとえば、上記のキーのコードは次のとおりです。

#define KEY_UP                               38
#define KEY_DOWN                             40
#define KEY_DELETE                           46

クラスのChartEventメソッドに戻りましょう。その中で、親クラスの同様のメソッドを呼び出しました。ここで、イベントIDの確認とツールの可視性を追加する必要があります。イベントハンドラは、ツールインターフェイスが表示されている場合にのみ実行されます。ユーザーは、パネルで何が起こっているかを確認し、プロセスを視覚的に制御できる必要があります。

検証の第1段階に合格した場合は、押されたキーのコードを確認します。リストに対応するキーがある場合は、インターフェイスのパネルで同様のアクションに対応するカスタムイベントを生成します。

たとえば、Deleteが押されたときに、インターフェイスパネルでボタンクリックイベントDELETEを生成します。 

void CNetCreatorPanel::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CAppDialog::ChartEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_KEYDOWN && m_spPTModelLayers.IsVisible())
     {
      switch((int)lparam)
        {
         case KEY_UP:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 2, 0.0, m_spPTModelLayers.Name() + "Inc");
            break;
         case KEY_DOWN:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 3, 0.0, m_spPTModelLayers.Name() + "Dec");
            break;
         case KEY_DELETE:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_btDeleteLayer.Id(), 0.0, m_btDeleteLayer.Name());
            break;
        }
     }
  }

その後、メソッドを終了します。次に、既存のイベントハンドラとメソッドを使用して、生成されたイベントをプログラムに処理させます。

もちろん、このアプローチは、プログラムに適切なハンドラがある場合にのみ可能です。ただし、新しいイベントハンドラを作成して、それらに固有のイベントを生成することはできます。


結論

この記事では、ユーザーインターフェイスの使いやすさを向上させるためのさまざまなオプションについて説明しました。記事に添付されているツールをテストすることで、アプローチの品質を評価できます。このツールがお役に立てば幸いです。関連するフォーラムスレッドで、ツールの改善に関するご意見やご要望を共有していただければ幸いです。

参考文献リスト

  1. ニューラルネットワークが簡単に(第20部):オートエンコーダ
  2. ニューラルネットワークが簡単に(第21部):変分オートエンコーダ(VAE)
  3. ニューラルネットワークが簡単に(第22部):回帰モデルの教師なし学習
  4. ニューラルネットワークが簡単に(第23部):転移学習用ツールの構築

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

# 名前 タイプ 詳細
1 NetCreator.mq5 EA   モデル構築ツール
2 NetCreatotPanel.mqh クラスライブラリ ツールを作成するためのクラスライブラリ
3 NeuroNet.mqh クラスライブラリ ニューラルネットワークを作成するためのクラスのライブラリ
4 NeuroNet.cl コードベース OpenCLプログラムコードライブラリ

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

添付されたファイル |
MQL5.zip (74.22 KB)
DoEasy-コントロール(第15部):TabControl WinFormsオブジェクト — 複数行のタブヘッダー、タブ処理メソッド DoEasy-コントロール(第15部):TabControl WinFormsオブジェクト — 複数行のタブヘッダー、タブ処理メソッド
この記事では、TabControl WinFormオブジェクトの作業を続けます。タブフィールドオブジェクトクラスを作成して複数の行にタブヘッダーを配置できるようにし、オブジェクトタブを処理するメソッドを追加します。
Bears Powerによる取引システムの設計方法を学ぶ Bears Powerによる取引システムの設計方法を学ぶ
最も人気のあるテクニカル指標によって取引システムを設計する方法を学ぶ連載の新しい記事へようこそ。この新しい記事では、Bears Power(ベアーパワー)テクニカル指標によって取引システムを設計する方法を学びます。
Bulls Powerによる取引システムの設計方法を学ぶ Bulls Powerによる取引システムの設計方法を学ぶ
最も人気のあるテクニカル指標によって取引システムを設計する方法を学ぶ連載の新しい記事へようこそ。この新しい記事では、Bulls Power(ブルパワー )テクニカル指標によって取引システムを設計する方法を学びます。
DoEasy-コントロール(第14部):グラフィック要素に名前を付けるための新しいアルゴリズム。TabControl WinFormsオブジェクトへの作業の継続 DoEasy-コントロール(第14部):グラフィック要素に名前を付けるための新しいアルゴリズム。TabControl WinFormsオブジェクトへの作業の継続
この記事では、カスタムグラフィックを構築するためのすべてのグラフィック要素に名前を付けるための新しいアルゴリズムを作成し、TabControl WinFormsオブジェクトの開発を継続する予定です。