神经网络变得轻松(第二十五部分):实践迁移学习

内容

  • 概述
  • 1. 一般测试的准备问题
  • 2. 为测试创建智能系统
  • 3. 为测试创建模型
  • 4. 测试结果
  • 结束语
  • 参考文献列表
  • 本文中用到的程序

概述

我们将继续研究迁移学习技术。 在之前的两篇文章中,我们开发了一个创建和编辑神经网络模型的工具。 该工具能帮助我们将预训练模型的一部分迁移到新模型当中,并用新的决策层对其进行补充。 这种方式所具备的潜力能辅助我们在解决新问题时,更快地训练以这种方式创建的模型。 在本文中,我们将评估这种方式在实践中的好处。 我们还将验证该工具的可用性。

1. 一般测试的准备问题

在本文中,我们将评估运用迁移学习技术的益处。 最好的方式就是来解决同一个问题,比较两个模型的学习过程。 为此目的,我们采用一个由随机权重初始的“纯”模型。 而第二个模型就采用迁移学习技术创建。

我们就以搜索分形作为问题,就像我们在监督学习方法中测试之前所有模型时所做的那样。 但我们用什么作为迁移学习的供体模型呢? 我们回到自动编码器。 我们把它们当作迁移学习的供体。 在研究自动编码器时,我们创建并训练了两种变分自动编码器模型。 在第一个模型中,编码器是基于完全连接神经层构建的。 在第二个当中,我们采用的是基于递归 LSTM 模块的编码器。 这一次,我们可以同时采用这两种模型作为供体。 如此,我们就可以测试这两种方式的效率。

因此,为了准备即将到来的测试,我们制定第一个基本决策:作为供体模型,我们将采用研究相关主题时已训练过的变分自动编码器。

第二个概念问题是我们将如何测试模型。 我们必须尽最大可能为所有模式创造平等的条件。 只有这样,我们才能排除其它因素的干扰,评估模型设计特征的纯粹影响。

这里的关键点是“设计特征”。 我们如何评估基础模型不同的迁移学习的益处? 事实上,状况并不明朗。 我们回忆从自动编码器学到的东西。 因其架构,我们期望在模型的输出端接收初始数据。 编码器将原始数据压缩到潜伏状态的“瓶颈”,然后由解码器恢复数据。 也就是说,我们简单地压缩原始数据。 在这种情况下,如果在借用的编码器模块之后的模型架构等于参考模型的架构,则可以认为模型架构雷同。

另一方面,不光数据压缩,编码器还执行数据预处理。 它挑选出一些功能,并将其它归零。 按照这种解释中,为了对齐两个模型的架构,我们需要创建模型的精确副本,且已用随机权重进行初始化。

鉴于这样仍然模棱两可,我们将测试解决问题的两种方法。

下一个问题事关测试工具。 之前,我们创建了一个单独的智能系统 (EA) 来测试每个模型,而每次我们都要在 EA 的初始化模块中描述和创建模型。 现在状况则不同了。 我们曾创建了一个创建模型的通用工具。 利用它,我们可以创建各种模型架构,并将它们保存到文件之中。 然后我们可以将创建的模型加载到任何 EA,从而能对其进行训练或加以运用。

因此,现在我们可以创建一个 EA,我们在其中训练所有模型。 故此,我们要提供最公平的条件来测试模型。

现在,我们必须决定测试环境。 也就是说,我们将采用哪些数据来测试模型。 答案很清楚:为了训练模型,我们要采用类似于训练自动编码器的环境。 神经网络对源数据非常敏感,它们配合训练时的数据才能正确工作。 因此,若要运用迁移学习技术,我们必须采用类似于供体模型训练样本的源数据

现在我们已经决定了所有关键问题,我们可以继续准备测试了。

2. 为测试创建智能系统

为了测试模型,准备工作从创建一个 EA 开始。 为此目的,我们创建一个 EA 模板 “check_net.mq5”。 首先,在模板中要包含函数库:

  • NeuroNet.mqh — 我们创建神经网络的函数库
  • SymbolInfo.mqh — 访问交易品种数据的标准库
  • Oscilators.mqh — 协同振荡器操作的标准库
此外,我们还要在这里声明一个枚举,以便于处理信号。

//+------------------------------------------------------------------+  //| Includes                                                         |  //+------------------------------------------------------------------+  #include "....NeuroNet_DNGNeuroNet.mqh"  #include <TradeSymbolInfo.mqh>  #include <IndicatorsOscilators.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 启动时指定的模型超参数之一。 在本文中,我们会利用以前创建的模型。 所分析的历史深度参数必须与相应加载的模型匹配。 但由于用户可以“盲目”使用一个模型,且不知道该参数的含义,那么我们就要冒着指定参数与加载模型不匹配的风险。 为了剔除这种风险,我决定根据加载模型的源数据层的大小重新计算参数。

我们继续研究 EA 函数的算法。 我们从 EA 初始化方法开始 — OnInit。 在方法主体中,我们首先从 EA 参数中指定的文件里加载模型。 在之前研究的 EA 中,即便来自相同操作,却有两处不同。

首先,由于我们未采用动态指针,故我们不需要创建模型对象的新实例。 出于同样的原因,我们就不需要检查指针的有效性。

其次,如果无法从文件中读取模型,则通知用户,并以 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 %.2fn%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;                }             }          }    

这个完整的嵌套循环,在模型训练的一个世代内,遍历训练样本元素。 之后,我们还要实现验证,基于训练样本意外的数据,评估模型的行为。 为此,运行类似循环遍历最后 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. 为测试创建模型

现在我们已经创建了模型测试工具,我们需要为测试准备基础。 即,我们需要创建所要训练的模型。 此刻无需编程,因为我们在前两篇文章中已经实现了所需的编码。 现在,我们利用结果的优点,并用我们的工具创建模型。

因此,我们运行之前创建的 NetCreator EA。 在其中,采用基于 LSTM 模块的递归编码器打开已预训练的自动编码器模型。 在之前,我们已将其保存在 “EURUSD_i_PERIOD_H1_rnn_vae.nnw” 文件当中。 我们仅用到来自该模型中的编码器。 在预训练模型的左侧模块中,找到变分自动编码器(VAE)的潜伏状态层。 在我的案例中,它是第八个。 因此,我只复制供体模型的前七个神经层。

该工具提供了三种方式来选择复制所需的层数。 您可以用“传输层”区域中的按钮,或箭头键 ↑ 和 ↓。 替代方法,您简单地单击供体模型描述中最后复制的描述即可。

复制层数发生变化的同时,在工具右侧模块中,所创建模型的描述也会发生变化。 我认为这很便利且信息丰富。 您可立即看到您的动作如何影响正在创建的模型的架构。

接下来,我们需要为特定的学习任务补充带有若干个决策神经层的新模型。 我尝试不令这部分复杂化,因为这些测试的主要目的是评估方法的有效性。 我添加了两个由 500 个元素组成的全连接层,和一个双曲正切作为激活函数。

事实证明,添加新的神经层是一项非常简单的任务。 首先,选择神经层的类型。 它是一个与“密集”对应的完全连接神经层。 指定层中的神经元数量、激活函数、和参数更新方法。 如果您选择了一个不同类型的神经层,则您需要填写相应的字段。 指定全部所需数据后,单击“添加层”。

另一个便利在于,如果您需要添加若干个相同的神经层,则无需重新输入数据。 简单地再次单击添加层。 这就是我如何做的。 若要添加第二层,我没有输入任何数据,而只是单击添加新层按钮。

结果层也是完全连接的,并且包含三个元素,符合上面创建的 EA 需求。 结果图层的激活函数则采用 Sigmoid。

我们之前的神经层也是完全连接的。 故此,我们只能更改神经元的数量和激活函数。 然后我们就能把新层加入模型当中。

现在,将新模型保存到文件之中。 为此,请按“保存模型”按钮,并指定新模型的文件名 “EURUSD_i_PERIOD_H1_test_rnn.nnw”。 请注意,您指定的文件名可以不带扩展名。 系统会自动添加正确的扩展名。

整个模型创建过程于下面的 gif 中示意。

神经网络变得轻松(第二十五部分):实践迁移学习

第一个模型已准备就绪。 现在,我们继续创建第二个模型。 作为第二个模型的供体,我们从 “EURUSD_i_PERIOD_H1_vae.nnw” 文件中加载带有完全连接编码器的变分自动编码器。 此处会带来一个惊喜。 加载新的供体模型后,我们没有删除已添加的神经层。 故此,它们会自动添加到所加载的模型当中。 我们只需要选择从供体模型复制到新模型的神经层数。 如此,我们的新模型现已准备就绪。

基于最后的自动编码器模型,我创建的模型不是一个,而是两个。 第一个模型模拟第一种情况。 我采用的是来自供体模型中的编码器,并添加了之前创建的三层。 对于第二个模型,我仅从供体模型中提取了源数据层和批量常规化层。 然后我也添加了相同的三个完全连接神经层。 最后一个模型将作为训练新模型的指南。 我决定采用预训练的批量常规化层用于准备原始输入数据。 这应该会增加新模型的收敛性。 甚至,我们取消了数据压缩。 我们可以假设最后一个模型完全由随机权重填充。

正如我们上面所讨论的,有不同的方式来评估预训练模型的架构影响。 这就是为什么我创建了另一个测试模型。 我所用的创建新模型的架构,是带有 LSTM 模块的自动编码器,并在新模型中完全复制它。 但这一次,我未从供体模型中复制编码器。 因此,我得到了一个完全雷同的模型架构,但采用随机权重进行初始化。

4. 测试结果

现在我们已经创建了测试所需的所有模型,我们即将训练它们。

我们采用监督学习训练模型,保留以前所用的训练参数。 这些模型基于过去两年的时间段内进行训练,时间帧为 H1,品种 EURUSD。 指标采用默认参数。

为了实验的纯净,所有模型在同一终端中的不同图表上同时进行训练。

我必须要说,同时训练若干个模型并不可取。 这显著降低了它们当中每个模型的学习率。 OpenCL 在模型中利用可用资源执行并行化计算过程。 在多模型的并行训练期间,可用资源在所有模型之间共享。 因此,它们当中每一个都只能访问有限的资源。 这增加了学习时间。 但这一次是有意为之,从而确保在训练模型时处于相似的条件。

测试 1

对于第一次测试,我们采用了两个带有预训练编码器的模型,以及一个带有带有借用的批量常规化层,和 2 个完全连接隐藏层的小型全连接模型。

模型测试结果如下图所示。

神经网络变得轻松(第二十五部分):实践迁移学习

正如您在所示图表中所见,采用预训练的递归编码器的模型显示出最佳性能。 它的误差实际上从第一次训练世代开始,就以明显更快的速率下降。

带有完全连接编码器的模型在学习过程中也显示出误差降低,但速率较慢。

带有两个隐藏层的完全连接模型,采用随机值初始化,看起来根本没有经过训练。 根据图表表现,误差似乎停滞在原地。

神经网络变得轻松(第二十五部分):实践迁移学习

经过仔细检查,我们可以注意到误差降低的趋势。 尽管这种降低的速率要迟缓得多。 显而易见,这样的模型对于解决该类问题来说太简陋了。

有基于此,我们可以得出结论,模型的性能仍然受到预训练编码器所依据的初始数据的极大影响。 这种编码器的架构对整个模型的操作有重大影响。

我想单独提一提模型训练率。 当然,最简单的模型会显示验算一个世代的时间最短。 但是带有递归编码器的模型的学习率非常接近于此。 依我观点,这是受到许多因素的影响。

首先,递归模型的架构允许所分析数据窗口减少 4 倍。 因此,神经元间连接的数量也减少了。 结果就是,它们的处理成本也降低了。 同时,递归架构意味着额外的反向传播验算资源成本。 但我们已针对预训练的神经层禁用了反向传播验算。 这最终降低了模型重训练的成本。

带有完全连接编码器的模型显示出较慢的学习速度率。

测试 2

在第二个测试中,我们决定把模型架构之间的差异最小化,并训练两个具有相同架构的递归模型。 一个模型采用预训练的递归编码器。 第二个模型则完全采用随机权重初始化。 我们依然采用在第一个测试中所用的相同参数来训练这些模型。

测试结果如下图所示。 如您所见,预训练模型开始时误差较小。 但很快第二个模型就贴近了,且它们的数值非常接近。 这证实了之前的结论,即编码器架构对整个模型的性能有重大影响。

神经网络变得轻松(第二十五部分):实践迁移学习

注意学习率。 预训练模型验算一个世代所需的时间减少了六倍。 当然,这只是纯粹的时间,不考虑自动编码器训练时间。

结束语

基于上述工作,我们可以得出结论,运用迁移学习技术提供了许多优势。 首先,这项技术确实有效。 应用它,可重用以前训练过的模型模块来解决新问题。 唯一的条件是初始数据的统一性。 在非正确输入数据上使用预训练模块难以奏效。

运用该技术减少了新模型的训练时间。 然而,请注意,我们衡量的是纯测试时间,不包括自动编码器预训练时间。 大概,如果我们加上训练自动编码器所花费的时间,时间将是相等的。 或有可能,由于解码器的架构更加复杂,“纯”模型的训练可以更快。 因此,当假设一个模块能解决各种问题时,运用迁移学习是有据可依的。 此外,当出于某种原因无法将模型作为一个整体进行训练时,它依然可以适用。 例如,模型可能非常复杂,误差梯度在学习过程中衰减,且不能到达所有层。

此外,当我们寻找最佳误差值时,模型逐渐更加复杂化,而该技术此刻可用来搜索最合适的模型。

参考文献列表

  1. 神经网络变得轻松(第二十部分):自动编码器
  2. 神经网络变得轻松(第二十一部分):变分自动编码器(VAE)
  3. 神经网络变得轻松(第二十二部分):递归模型的无监督学习
  4. 神经网络变得轻松(第二十三部分):构建迁移学习工具
  5. 神经网络变得轻松(第二十四部分):改进迁移学习工具

本文中用到的程序

# 名称 类型 说明
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

附加的文件 |

下载ZIP
MQL5.zip (78.84 KB)

 


 

MyFxtops迈投(www.myfxtops.com)-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投