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

ニューラルネットワークが簡単に(第25部):転移学習の実践

MetaTrader 5統合 | 28 11月 2022, 13:25
190 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容


はじめに

転移学習技術の研究を続けます。前2回では、ニューラルネットワークのモデルを作成・編集するためのツールを作成しました。このツールを使えば、事前に訓練したモデルの一部を新しいモデルに移し、新しい決定層で補完することができます。この方法で作成したモデルを、新しい問題を解決するために迅速に訓練することができるようになる可能性があります。今回は、この手法のメリットを実際に評価します。また、ツールの使い勝手も確認します。


1.テスト準備全般の問題

今回は、転移学習の技術を使うことのメリットを評価したいと思います。最も良い方法は、1つの問題を解決するために2つのモデルの学習プロセスを比較することです。この目的のために、ランダム重みによって開始された1つの「純粋な」モデルを使用することにします。2つ目のモデルは、転移学習の技術を使って作成します。

教師あり学習法でこれまでのすべてのモデルをテストするときにおこなったように、フラクタル探索を問題とすることができます。しかし、転移学習のドナーモデルとして何を使うでしょうか。オートエンコーダに話を戻しましょう。転移学習のドナーとして使用しました。オートエンコーダの研究では、変分オートエンコーダの2つのモデルを作成し、訓練しました。最初のモデルでは、完全連結ニューラル層を用いてエンコーダを構築しました。2つ目では、回帰LSTMブロックに基づくエンコーダを使用しました。今回は、両方のモデルをドナーとして使用することができます。この2つのアプローチの効率性を検証することも可能です。

そこで、今回のテストに向けて、最初の基本的な決定をおこないました。ドナーモデルとして、関連するトピックの学習時に訓練した変分オートエンコーダを使用します。

2つ目の概念的問題は、モデルをどのようにテストするかということです。すべてのモデルに対して、最も平等な条件を整えなければなりません。そうすることで初めて、他の要因の影響を排除し、純粋にモデル設計の特徴の影響を評価することができます。

ここでポイントとなるのは、「デザイン性」です。本質的に異なるモデルにおいて、転移学習の効果をどのように評価するのでしょうか。実は、この状況は一義的なものではありません。オートエンコーダが何を学習するのかを覚えておきましょう。そのアーキテクチャは、モデルの出力に初期データを受け取ることを想定しています。エンコーダは、元のデータを潜在状態の「ボトルネック」に圧縮し、デコーダはデータを復元します。つまり、元データを単純に圧縮しているのです。この場合、借用したエンコーダブロック以降のモデルのアーキテクチャが参照モデルのアーキテクチャと等しければ、モデルは同一アーキテクチャであると見なすことができます。

一方、エンコーダはデータ圧縮と同時に、データの前処理をおこないます。ある機能をピックアウトし、ある機能をゼロにします。この解釈では、2つのモデルのアーキテクチャを合わせるために、ランダムな重みで初期化されたモデルの正確なコピーを作成する必要があります。

これではまだ曖昧なので、両方のアプローチでの問題解決を検証します。

次の質問は、テストツールに関するものです。従来は、EAの初期化ブロックに毎回モデルを記述して作成していたため、各モデルのテスト用に別のEAを作成していました。今は状況が違います。モデルを作成するための普遍的なツールを作成しているため、これを用いて、様々なモデルアーキテクチャを作成し、ファイルに保存することができます。そして、作成したモデルを任意のEAにアップロードして、訓練したり、使用したりすることができます。

そこで、1つのEAを作成し、その中ですべてのモデルを訓練することができるようになりました。このように、モデルをテストするために最も平等な条件を提供します。

あとは、テスト環境を決めることです。つまり、どのデータでモデルを検証するかということです。答えは明快で、モデルの訓練には、オートエンコーダを訓練するのと同じような環境を使います。ニューラルネットワークは元データに非常に敏感で、訓練されたデータでないと正しく動作しません。そのため、転移学習技術を利用するためには、ドナーモデルの訓練サンプルと同様の元データを利用する必要があります。

さて、重要事項がすべて決まったところで、テストの準備に移ります。


2.テスト用エキスパートアドバイザー(EA)の作成

準備作業として、まずモデルをテストするためのEAを作成します。そのために、EAテンプレート「check_net.mq5」を作成しましょう。まず、テンプレートにライブラリを組み込みます。

  • NeuroNet.mqh - ニューラルネットワークを作成するための私たちのライブラリ
  • SymbolInfo.mqh - 取引銘柄データにアクセスするための標準ライブラリ
  • Oscilators.mqh - オシレータを操作するための標準ライブラリ
また、シグナルの扱いに便利なように、ここで列挙を宣言しています。

//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+
#include "..\..\NeuroNet_DNG\NeuroNet.mqh"
#include <Trade\SymbolInfo.mqh>
#include <Indicators\Oscilators.mqh>
//---
enum ENUM_SIGNAL
  {
   Sell = -1,
   Undefine = 0,
   Buy = 1
  };

次に、EAのグローバル変数を宣言します。ここで、モデルファイル、作業時間枠、モデル訓練期間を指定します。また、使用する指標のパラメータをすべて表示します。EAメニューを見やすくするため、指標パラメータをグループ分けする予定です。

//+------------------------------------------------------------------+
//|   input parameters                                               |
//+------------------------------------------------------------------+
input int                  StudyPeriod =  2;            //Study period, years
input string               FileName = "EURUSD_i_PERIOD_H1_test_rnn";
ENUM_TIMEFRAMES            TimeFrame   =  PERIOD_CURRENT;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

次に、使用するオブジェクトのインスタンスを宣言します。動的なオブジェクトの使用は可能な限り避けました。これにより、オブジェクトの生成や関連性の確認に関連する不要な操作がなくなり、コードが少し簡素化されます。オブジェクトのネーミングは、オブジェクトのコンテンツと一致しています。これにより、変数の混乱を最小限に抑え、コードの可読性を向上させることができます。

CSymbolInfo          Symb;
CNet                 Net;
CBufferFloat        *TempData;
CiRSI                RSI;
CiCCI                CCI;
CiATR                ATR;
CiMACD               MACD;
CBufferFloat         Fractals;

また、EAのグローバル変数を宣言します。では、それぞれの機能を説明します。EAの関数のアルゴリズムを分析しながら、その目的を見ていきます。

uint                 HistoryBars =  40;            //Depth of history
MqlRates             Rates[];
float                dError;
float                dUndefine;
float                dForecast;
float                dPrevSignal;
datetime             dtStudied;
bool                 bEventStudy;

ソースデータの量をバーで表す変数がここに表示されているのがわかります。これは以前はEAの外部パラメータで指定されていました。このパラメータを非表示にしてグローバル変数として使用するのは強行手段です。前回は、EA初期化機能でモデルアーキテクチャを説明しました。つまり、このパラメータは、EA開始時にユーザーが指定したモデルのハイパーパラメータの1つだったのです。今回は、過去に作成したモデルを使用します。解析済み履歴の深さパラメータは、読み込まれたモデルに対応している必要があります。しかし、ユーザーはこのパラメータを知らずに「盲目的に」モデルを使うことができるため、指定したパラメータと読み込んだモデルとの間に不一致が生じる危険性があるのです。このリスクを排除するために、読み込まれたモデルのソースデータ層のサイズに応じてパラメータを再計算することにしました。

続いて、EA関数のアルゴリズムについて考えてみましょう。まず、EAの初期化メソッドであるOnInitから説明します。メソッド本体では、まずEAパラメータで指定したファイルからモデルを読み込みます。ここでは、これまで考えられてきたEAと同じ操作とは異なる2つのモーメントがあります。

まず、動的ポインタを使用しないので、モデルオブジェクトのインスタンスを新たに作成する必要がありません。同じ理由で、ポインタの有効性を確認する必要はありません。

次に、モデルをファイルから読み込めなかった場合、ユーザーに通知し、INIT_PARAMETERS_INCORRECTの結果で関数を終了します。また、EAを終了します。前述したように、以前に作成したいくつかのモデルと連動するEAを作成しています。つまり、デフォルトのモデルは存在しないのです。モデルがなければ、訓練するものがないので、これ以上のEA操作は意味はありません。そのため、ユーザーに通知し、EA操作を終了させます。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!Net.Load(FileName + ".nnw", dError, dUndefine, dForecast, dtStudied, false))
     {
      printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError());
      return INIT_PARAMETERS_INCORRECT;
     }

モデルの読み込みに成功したら、解析した履歴の深さのサイズを計算し、結果の値を変数HistoryBarsに保存します。また、結果層のサイズも確認します。モデルの可能な結果の数に応じて、3つのニューロンを含む必要があります。

   if(!Net.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   Net.getResults(TempData);
   if(TempData.Total() != 3)
      return INIT_PARAMETERS_INCORRECT;

すべての確認が成功したら、指標を操作するためのオブジェクトの初期化に進みます。

   if(!Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();

   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      return INIT_FAILED;

   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      return INIT_FAILED;

   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      return INIT_FAILED;

   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      return INIT_FAILED;

操作の実行を制御することを忘れないでください。

すべてのオブジェクトが初期化されたら、カスタムイベントを生成して、モデル訓練メソッドに制御を移します。カスタムイベントの生成結果をbEventStudy変数に書き込み、モデル訓練過程の開始フラグとして機能させます。

カスタムイベント生成操作により、EA初期化方法を完成させることができます。並行して、新しいティックを待つことなく、モデルの訓練過程を分析することができます。このように、モデルの学習開始時期を市場のボラティリティに依存しないようにします。

   bEventStudy = EventChartCustom(ChartID(), 1, (long)MathMax(0, MathMin(iTime(Symb.Name(), PERIOD_CURRENT,
                                  (int)(100 * Net.recentAverageSmoothingFactor * (dForecast >= 70 ? 1 : 10))), dtStudied)),

                                  0, "Init");
//---
   return(INIT_SUCCEEDED);
  }

EAの非初期化メソッドでは、EAで使用されている唯一の動的オブジェクトを削除します。これは、他の動的オブジェクトを使用しないようにしたためです。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(TempData) != POINTER_INVALID)
      delete TempData;
  }

カスタムイベントも含まれ、すべてのチャートイベントはOnChartEvent関数で処理されます。したがって、この関数では、IDで識別できるユーザーイベントの発生を待ちます。カスタムイベントIDは1000から始まります。カスタムイベントを生成する際にIDを1にしたので、この関数では、1001という識別子を持つイベントを受信するはずです。このようなイベントが発生した場合、モデルの訓練手順(Train)を呼び出します。

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == 1001)
      Train(lparam);
  }

EAの主要関数である、モデル訓練のためのTrainのアルゴリズムの構成について詳しく見てみましょう。この関数が受け取るパラメータは、訓練期間の開始日のみです。まず、この日付がEAの外部パラメータでユーザーが指定した訓練期間外でないことを確認します。受信した日付がユーザーが指定した期間と一致しない場合は、指定した訓練期間の最初に日付をシフトさせます。

void Train(datetime StartTrainBar = 0)
  {
   int count = 0;
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);
   dtStudied = MathMax(StartTrainBar, st_time);
   ulong last_tick = 0;

次に、ローカル変数を用意します。

   double prev_er = DBL_MAX;
   datetime bar_time = 0;
   bool stop = IsStopped();

次に、履歴データを読み込みます。ここでは、指標データと一緒にクォートも読み込みます。指標バッファと読み込まれた相場を同期させることが重要です。そのため、まず指定した期間の相場をダウンロードし、読み込まれたバーの数を決定し、使用するすべての指標について同じ期間を読み込みます。

   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      ExpertRemove();
      return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
      ExpertRemove();
      return;
     }
   RSI.Refresh(OBJ_ALL_PERIODS);
   CCI.Refresh(OBJ_ALL_PERIODS);
   ATR.Refresh(OBJ_ALL_PERIODS);
   MACD.Refresh(OBJ_ALL_PERIODS);

訓練サンプルが読み込まれたら、各訓練エポックの後に、訓練サンプルの総要素数から最後の300個の要素を取り出して検証をおこなうことになります。その後、学習プロセスのループのシステムを作ります。外側のループでは、訓練エポックをカウントし、モデルの訓練プロセスを継続するかどうかを制御します。ループ本体内のフラグを更新します。

  • prev_er - 直前のエポックにおけるモデル誤差
  • stop - ユーザーによるプログラム終了のイベントを発生させる

   MqlDateTime sTime;
   int total = (int)(bars - MathMax(HistoryBars, 0) - 300);
   do
     {
      prev_er = dError;
      stop = IsStopped();

ネストされたループの中で、訓練サンプルの要素を反復し、順番にニューラルネットワークに送り込みます。入力データのシーケンスに敏感な回帰モデルを使おうとしているので、シーケンスの次の要素がランダムに選択されることは避けなければなりません。その代わりに、要素の履歴シークエンスを使うことにします。

現在の要素からデータの十分性を即座に確認し、パターンを描きます。データが足りない場合は、次の要素に移ります。

      for(int it = total; it > 1 && !stop; t--)
        {
         TempData.Clear();
         int i = it + 299;
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;

データが十分であれば、モデルに投入するパターンを形成します。また、指標バッファのデータの可用性も制御しています。指標となる数値が定義されていない場合は、次の要素に進みます。

         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!TempData.Add((float)Rates[bar_t].close - open) || !TempData.Add((float)Rates[bar_t].high - open) ||
               !TempData.Add((float)Rates[bar_t].low - open) || !TempData.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
               !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
               break;
           }
         if(TempData.Total() < (int)HistoryBars * 12)
            continue;

パターンが正常に形成された後、モデルのフィードフォワードパスメソッドを呼び出します。フィードフォワードパスの結果を即座に要求します。

         Net.feedForward(TempData, 12, true);
         Net.getResults(TempData);

得られた値を確率に変換するために、モデル結果にSortMax関数を適用します。 

         float sum = 0;
         for(int res = 0; res < 3; res++)
           {
            float temp = exp(TempData.At(res));
            sum += temp;
            TempData.Update(res, temp);
           }
         for(int res = 0; (res < 3 && sum > 0); res++)
            TempData.Update(res, TempData.At(res) / sum);
         //---
         switch(TempData.Maximum(0, 3))
           {
            case 1:
               dPrevSignal = (TempData[1] != TempData[2] ? TempData[1] : 0);
               break;
            case 2:
               dPrevSignal = -TempData[2];
               break;
            default:
               dPrevSignal = 0;
               break;
           }

その後、学習過程の情報をチャートに表示します。

         if((GetTickCount64() - last_tick) >= 250)
           {
            string s = StringFormat("Study -> Era %d -> %.2f -> Undefine %.2f%% foracast %.2f%%\n %d of %d -> %.2f%% \n
                                     Error %.2f\n%s -> %.2f ->> Buy %.5f - Sell %.5f - Undef %.5f", count, dError, 
                                     dUndefine, dForecast, total - it - 1, total, 
                                     (double)(total - it - 1.0) / (total) * 100, Net.getRecentAverageError(),
                                      EnumToString(DoubleToSignal(dPrevSignal)), dPrevSignal, TempData[1], TempData[2], TempData[0]);
            Comment(s);
            last_tick = GetTickCount64();
           }

モデル訓練過程におけるフィードフォワードパスは、バックプロパゲーションに続いておこなわれます。まず、目標値を作成し、バックプロパゲーション手法に投入します。また、すぐに学習過程の統計量を算出します。

         stop = IsStopped();
         if(!stop)
           {
            TempData.Clear();
            bool sell = (Rates[i - 1].high <= Rates[i].high && Rates[i + 1].high < Rates[i].high);
            bool buy = (Rates[i - 1].low >= Rates[i].low && Rates[i + 1].low > Rates[i].low);
            TempData.Add(!(buy || sell));
            TempData.Add(buy);
            TempData.Add(sell);
            Net.backProp(TempData);
            ENUM_SIGNAL signal = DoubleToSignal(dPrevSignal);
            if(signal != Undefine)
              {
               if((signal == Sell && sell) || (signal == Buy && buy))
                  dForecast += (100 - dForecast) / Net.recentAverageSmoothingFactor;
               else
                  dForecast -= dForecast / Net.recentAverageSmoothingFactor;
               dUndefine -= dUndefine / Net.recentAverageSmoothingFactor;
              }
            else
              {
               if(!(buy || sell))
                  dUndefine += (100 - dUndefine) / Net.recentAverageSmoothingFactor;
              }
           }
        }

これにより、モデル訓練の1エポック内で、訓練サンプル要素に対するネストされたループが完了します。その後、訓練サンプルに含まれないデータに対するモデルの挙動を評価するバリデーションを実施する予定です。これをおこなうには、最後の300個の要素に対して同様のループを実行しますが、フィードフォワードパスを使用します。検証中は、バックプロパゲーションパスの実行と重み行列の更新は必要ありません。

      count++;
      for(int i = 0; i < 300; i++)
        {
         TempData.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;
         //---
         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!TempData.Add((float)Rates[bar_t].close - open) || !TempData.Add((float)Rates[bar_t].high - open) ||
               !TempData.Add((float)Rates[bar_t].low - open) || !TempData.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
               !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
               break;
           }
         if(TempData.Total() < (int)HistoryBars * 12)
            continue;
         Net.feedForward(TempData, 12, true);
         Net.getResults(TempData);
         //---
         float sum = 0;
         for(int res = 0; res < 3; res++)
           {
            float temp = exp(TempData.At(res));
            sum += temp;
            TempData.Update(res, temp);
           }
         for(int res = 0; (res < 3 && sum > 0); res++)
            TempData.Update(res, TempData.At(res) / sum);
         //---
         switch(TempData.Maximum(0, 3))
           {
            case 1:
               dPrevSignal = (TempData[1] != TempData[2] ? TempData[1] : 0);
               break;
            case 2:
               dPrevSignal = (TempData[1] != TempData[2] ? -TempData[2] : 0);
               break;
            default:
               dPrevSignal = 0;
               break;
           }

検証フィードフォワードパスの後、モデルのシグナルをチャート上に出力し、その性能を視覚的に評価できるようにします。

         if(DoubleToSignal(dPrevSignal) == Undefine)
            DeleteObject(Rates[i].time);
         else
            DrawObject(Rates[i].time, dPrevSignal, Rates[i].high, Rates[i].low);
        }

各エポックの終了時に、モデルの現在の状態を保存します。ここでは、学習過程のダイナミクスを制御するために、現在のモデル誤差もファイルに追加します。

      if(!stop)
        {
         dError = Net.getRecentAverageError();
         Net.Save(FileName + ".nnw", dError, dUndefine, dForecast, Rates[0].time, false);
         printf("Era %d -> error %.2f %% forecast %.2f", count, dError, dForecast);
         int h = FileOpen(FileName + ".csv", FILE_READ | FILE_WRITE | FILE_CSV);
         if(h != INVALID_HANDLE)
           {
            FileSeek(h, 0, SEEK_END);
            FileWrite(h, eta, count, dError, dUndefine, dForecast);
            FileFlush(h);
            FileClose(h);
           }
        }
     }
   while(!(dError < 0.01 && (prev_er - dError) < 0.01) && !stop);

次に、最後の訓練エポックにおけるモデルの誤差の変化を評価し、訓練を継続するかどうかを決定する必要があります。もし訓練を継続することに決めたら、新しいエポックに対してループの繰り返しがおこなわれます。

モデルの訓練過程が終了したら、グラフのコメント領域をクリアして、EA補完を初期化します。この時点で、EAはモデルの訓練タスクを終了しており、それ以上メモリに保持する必要はありません。

   Comment("");
   ExpertRemove();
  }

チャート上にラベルを表示したり、削除したりする補助的な機能は、まさに以前検討したEAで使用したものですので、ここではそのアルゴリズムの繰り返しはしません。すべてのEA関数のコードは、添付ファイルにあります。


3.テスト用モデルの作成

モデルテストツールを作成したところで、テスト用のベースを準備する必要があります。必要なコーディングは前回の2稿で実装したので、ここではプログラミングは不要です。では、その結果を活かして、ツールを使ってモデルを作ってみましょう。

先に作成したNetCreator EAを実行します。その中で、LSTMブロックに基づく回帰エンコーダを使って、事前訓練したオートエンコーダモデルを開きます。前回は、「EURUSD_i_PERIOD_H1_rnn_vae.nnw」ファイルに保存しました。このモデルからエンコーダのみを使用することになります。事前訓練済みモデルの左ブロックで、変分オートエンコーダ(VAE)の潜在的状態層を見つけます。私の場合、8番目です。ドナーモデルの最初の7つのニューラル層だけをコピーすることにします。

このツールでは、コピーに必要な層の数を選択する方法が3つ用意されています。[Transfer Layers(層の転送)]領域のボタン、または矢印キー↑、↓を使用できますが、ドナーモデルの説明の中で、最後にコピーされた後の説明をクリックするだけでもOKです。

コピーした層の数が変わると同時に、ツールの右ブロックにある作成したモデルの説明文も変化します。便利で参考になると思います。自分のアクションが作成中のモデルのアーキテクチャにどのような影響を与えるかを即座に確認することができます。

次に、特定の学習タスクのために、新しいモデルにいくつかの神経決定層を補充する必要があります。このテストは、手法の効果を評価するのが主な目的なので、この部分は複雑にしないように心がけました。500要素の完全連結層を2層、活性化関数として双曲線正接を追加しています。

新しいニューラル層を追加するのは、非常に簡単な作業であることがわかりました。まず、ニューラル層の種類を選択します。完全連結神経層は「密」に相当します。層のニューロン数、活性化関数、パラメータ更新方法を指定します。異なるタイプのニューラル層を選択した場合は、適切なフィールドに記入します。必要なデータをすべて指定したら、[ADD LAYER(層を追加)] をクリックします。

また、同じニューラル層を複数追加する必要がある場合、データを再入力する必要がないのも便利な点です。もう一度、[ADD LAYER(層を追加)]をクリックするだけです。私が使ったのはこれです。2つ目の層を追加するには、何も入力せず、単に新規層を追加するボタンをクリックしました。

上記で作成したEAの要求に合わせて、結果層も完全連結型で、3つの要素で構成されています。結果層の活性化関数としてシグモイドを使用します。

これまでのニューラル層も完全連結型だったため、ニューロンの数と活性化関数を変えるだけで大丈夫です。そして、その層をモデルに追加します。

次に、新しいモデルをファイルに保存します。これをおこなうには、[SAVE MODEL(モデルを保存)]ボタンを押して、新しいモデルのファイル名 EURUSD_i_PERIOD_H1_test_rnn.nnw を指定します。なお、ファイル名は拡張子なしで指定することができます。正しい拡張子は自動的に追加されます。

モデル作成の全プロセスを下のGIFにビジュアル化しました。

モデル作成ツールの使い方

初めてのモデルが完成しました。では、2つ目のモデルの作成に移りましょう。2つ目のモデルのドナーとして、EURUSD_i_PERIOD_H1_vae.nnw ファイルから完全連結型エンコーダを持つ変分オートエンコーダを読み込んでみましょう。ここで、もうひとつの驚きがあります。新しいドナーモデルを読み込んた後、追加されたニューラル層は削除していません。読み込んだモデルに自動的に追加されます。ドナーモデルから新しいモデルにコピーするニューラル層の数を選択するだけで大丈夫です。これで、新しいモデルが出来上がりました。

前回のオートエンコーダモデルを基盤に、1つだけでなく2つのモデルを作りました。1つ目のモデルは、アナログです。ドナーモデルのエンコーダを使い、先に作成した3つの層を追加しました。2つ目のモデルは、ドナーモデルからソースデータ層とバッチ正規化層のみを取り出したものです。そして、そこに同じように3つの完全連結ニューラル層を追加しました。前回のモデルは、新しいモデルを訓練するためのガイドとなります。生入力データの準備には、事前に訓練したバッチ正規化層を使うことにしました。これによって、新モデルの収束性が高まるはずです。さらに、データ圧縮も排除しています。最後のモデルは完全にランダムな重みで埋め尽くされていると考えて大丈夫です。

これまで述べてきたように、事前訓練済みモデルのアーキテクチャの影響を評価する方法は様々です。そのため、テスト用にもう1つモデルを作りました。LSTMブロックによるオートエンコーダを使って新しく作ったモデルのアーキテクチャを使い、新しいモデルで完全に再現しました。ただし、今回は、ドナーモデルからエンコーダをコピーしたわけではありません。このように、完全に同一のモデルアーキテクチャを得たのですが、ランダムな重みで初期化されています。


4.検証結果

さて、テストに必要なモデルをすべて作成したので、それらを訓練することにしましょう。

教師あり学習により、前回使用した訓練パラメータを維持したままでモデルを訓練しました。モデルは、H1時間枠のEURUSDを使用して、過去2年間の時間間隔で訓練しました。指標はデフォルトのパラメータで使用しました。

実験の純度を高めるため、すべてのモデルは1つのターミナルで異なるチャートに対して同時に訓練しました。

複数のモデルを同時に訓練するのは好ましくないと言わざるを得ません。それぞれの学習率が大幅に低下します。計算処理を並列化し、利用可能なリソースを活用するために、モデルにはOpenCLが使用されています。複数のモデルの並列訓練では、利用可能なリソースはすべてのモデルで共有されます。つまり、それぞれが限られたリソースを利用できるわけなので、学習時間が長くなります。ただし、今回は、モデルを訓練する際に同じような条件を確保するために、意図的にこのような方法をとっています。

テスト1

最初のテストでは、事前に訓練したエンコーダを持つ2つのモデルと、借用されたバッチ正規化層と2つの完全連結隠れ層を持つ小さな完全連結型モデル1つを使用しました。

モデルテストの結果は、以下のグラフのとおりです。

モデル学習ダイナミクスの比較

グラフからわかるように、回帰エンコーダを事前訓練したモデルが最も良い性能を示しています。その誤差は、実質的に最初の訓練エポックから著しく速い速度で減少しました。

完全連結型エンコーダを用いたモデルでも、学習過程で誤差の減少が見られたが、その速度は低いものでした。

ランダムな値で初期化された2つの隠れ層を持つ完全連結型モデルは、全く訓練されていないように見えます。提示されたグラフによると、エラーが出たまま止まっているようです。

完全連結型モデルエラーダイナミクス

よく見ると、誤差が小さくなる傾向があることに気づきますが、この削減のスピードはかなり緩やかなものです。明らかに、このような問題を解決するには、このようなモデルは単純すぎます。

このことから、やはりモデルの性能は、事前に訓練させたエンコーダによる初期データの処理に大きく影響されると結論付けることができます。このようなエンコーダのアーキテクチャは、モデル全体の動作に大きな影響を与えます。

モデルの訓練率については、別途言及したいと思います。もちろん、1エポック通過にかかる時間は最も単純なモデルで最短だという結果も出ていますが、回帰エンコーダを使ったモデルの学習率は、それに近いものでした。これには、さまざまな要因が影響していると私は考えています。

まず、回帰モデルのアーキテクチャにより、解析するデータウィンドウを4倍に削減することができました。そのため、ニューラル間結合の数も減少しました。その結果、その処理コストを削減することができたのです。同時に、回帰アーキテクチャは、バックプロパゲーションパスのための追加リソースコストを意味します。これにより、結果的にモデルの再訓練のコストを削減することができました。

完全連結型エンコーダを用いたモデルでは、学習速度が遅くなることがわかりました。

テスト2

2回目のテストでは、モデル間のアーキテクチャの違いを最小化し、同じアーキテクチャの2つの回帰モデルを訓練させることにしました。1つ目のモデルは、事前に訓練された回帰エンコーダを使用します。2つ目のモデルはランダムな重みで完全に初期化されています。これらのモデルの訓練には、最初のテストで使用したのと同じパラメータを使用しました。

テスト結果は下のチャートのとおりです。ご覧の通り、事前に訓練させたモデルの方が、誤差が少ない状態で始まっていますが、すぐに2番目のモデルが追いつき、さらに両者の値はかなり接近しています。これは、エンコーダのアーキテクチャがモデル全体の性能に大きな影響を与えるという、先の結論を裏付けるものです。

回帰モデルの学習ダイナミクスの比較

学習率に注目します。事前訓練されたモデルは、1つのエポックを通過するのに必要な時間が6倍少なくなっています。もちろん、これはオートエンコーダの訓練を考慮しない純粋な時間です。


結論

以上の作業から、転移学習の技術を使用することで、多くの利点が得られると結論付けることができます。まず、この技術は本当に機能します。その応用として、過去に訓練したモデルブロックを再利用し、新たな問題を解決することができます。唯一の条件は、初期データの単一性です。適切でない入力データに対して、事前に訓練させたブロックを使用してもうまくいきません。

技術の活用により、新しいモデルの訓練時間を短縮することができます。ただし、測定しているのはオートエンコーダの事前訓練を含まない、純粋なテスト時間です。おそらく、オートエンコーダを訓練する時間を足せば、同じ時間になるはずです。あるいは、デコーダーのアーキテクチャがより複雑なため、 「純粋な」モデルの訓練はさらに高速におこなえるかもしれません。したがって、1つのブロックが様々な問題を解決するために使われる場合、転移学習の利用は正当化されます。また、何らかの理由でモデル全体の訓練ができない場合にも適しています。例えば、モデルが非常に複雑になり、学習過程で誤差勾配が減衰し、全層に到達しないことがあります。

また、最適な誤差の値を求めて徐々にモデルを複雑にしていき、最適なモデルを探す場合にも応用できる技術です。


参考文献リスト

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

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

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


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

添付されたファイル |
MQL5.zip (78.84 KB)
市場の数学:利益、損失、コスト 市場の数学:利益、損失、コスト
この記事では、手数料やスワップなど、あらゆる取引の総損益を計算する方法を紹介します。最も正確な数学的モデルを提供し、それを使ってコードを書き、標準と比較するつもりです。そのほか、利益を計算するMQL5のメイン関数の内部にも入り込み、仕様から必要な値をすべて突き詰めてみます。
一からの取引エキスパートアドバイザーの開発(第28部):未来に向かって(III) 一からの取引エキスパートアドバイザーの開発(第28部):未来に向かって(III)
私たちの発注システムが対応できていないタスクがまだ1つありますが、最終的に解決する予定です。MetaTrader 5は、注文値の作成と修正を可能にするチケットのシステムを備えています。アイデアは、同じチケットシステムをより高速かつ効率的にするエキスパートアドバイザー(EA)を持つことです。
データサイエンスと機械学習—ニューラルネットワーク(第02回):フィードフォワードNNアーキテクチャの設計 データサイエンスと機械学習—ニューラルネットワーク(第02回):フィードフォワードNNアーキテクチャの設計
フィードフォワード(予測制御)ニューラルネットワークについて説明する前に、少し説明しておくことがあって、設計もその1つです。入力、隠れ層の数、および各ネットワークのノードに対する柔軟なニューラルネットワークを構築および設計する方法を見てみましょう。
MQL5での行列およびベクトル演算 MQL5での行列およびベクトル演算
行列とベクトルがMQL5に導入され、数学的な解決策による効率的な操作が可能になりました。これらの新しい型は、数学表記に近い簡潔でわかりやすいコードを作成するための組み込みメソッドを提供します。配列は広範な機能を提供しますが、行列の方がはるかに効率的である場合が多くあります。