
神经网络变得轻松(第二十五部分):实践迁移学习
内容
概述
我们将继续研究迁移学习技术。 在之前的两篇文章中,我们开发了一个创建和编辑神经网络模型的工具。 该工具能帮助我们将预训练模型的一部分迁移到新模型当中,并用新的决策层对其进行补充。 这种方式所具备的潜力能辅助我们在解决新问题时,更快地训练以这种方式创建的模型。 在本文中,我们将评估这种方式在实践中的好处。 我们还将验证该工具的可用性。
1. 一般测试的准备问题
在本文中,我们将评估运用迁移学习技术的益处。 最好的方式就是来解决同一个问题,比较两个模型的学习过程。 为此目的,我们采用一个由随机权重初始的“纯”模型。 而第二个模型就采用迁移学习技术创建。
我们就以搜索分形作为问题,就像我们在监督学习方法中测试之前所有模型时所做的那样。 但我们用什么作为迁移学习的供体模型呢? 我们回到自动编码器。 我们把它们当作迁移学习的供体。 在研究自动编码器时,我们创建并训练了两种变分自动编码器模型。 在第一个模型中,编码器是基于完全连接神经层构建的。 在第二个当中,我们采用的是基于递归 LSTM 模块的编码器。 这一次,我们可以同时采用这两种模型作为供体。 如此,我们就可以测试这两种方式的效率。
因此,为了准备即将到来的测试,我们制定第一个基本决策:作为供体模型,我们将采用研究相关主题时已训练过的变分自动编码器。
第二个概念问题是我们将如何测试模型。 我们必须尽最大可能为所有模式创造平等的条件。 只有这样,我们才能排除其它因素的干扰,评估模型设计特征的纯粹影响。
这里的关键点是“设计特征”。 我们如何评估基础模型不同的迁移学习的益处? 事实上,状况并不明朗。 我们回忆从自动编码器学到的东西。 因其架构,我们期望在模型的输出端接收初始数据。 编码器将原始数据压缩到潜伏状态的“瓶颈”,然后由解码器恢复数据。 也就是说,我们简单地压缩原始数据。 在这种情况下,如果在借用的编码器模块之后的模型架构等于参考模型的架构,则可以认为模型架构雷同。
另一方面,不光数据压缩,编码器还执行数据预处理。 它挑选出一些功能,并将其它归零。 按照这种解释中,为了对齐两个模型的架构,我们需要创建模型的精确副本,且已用随机权重进行初始化。
鉴于这样仍然模棱两可,我们将测试解决问题的两种方法。
下一个问题事关测试工具。 之前,我们创建了一个单独的智能系统 (EA) 来测试每个模型,而每次我们都要在 EA 的初始化模块中描述和创建模型。 现在状况则不同了。 我们曾创建了一个创建模型的通用工具。 利用它,我们可以创建各种模型架构,并将它们保存到文件之中。 然后我们可以将创建的模型加载到任何 EA,从而能对其进行训练或加以运用。
因此,现在我们可以创建一个 EA,我们在其中训练所有模型。 故此,我们要提供最公平的条件来测试模型。
现在,我们必须决定测试环境。 也就是说,我们将采用哪些数据来测试模型。 答案很清楚:为了训练模型,我们要采用类似于训练自动编码器的环境。 神经网络对源数据非常敏感,它们配合训练时的数据才能正确工作。 因此,若要运用迁移学习技术,我们必须采用类似于供体模型训练样本的源数据。
现在我们已经决定了所有关键问题,我们可以继续准备测试了。
2. 为测试创建智能系统
为了测试模型,准备工作从创建一个 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 启动时指定的模型超参数之一。 在本文中,我们会利用以前创建的模型。 所分析的历史深度参数必须与相应加载的模型匹配。 但由于用户可以“盲目”使用一个模型,且不知道该参数的含义,那么我们就要冒着指定参数与加载模型不匹配的风险。 为了剔除这种风险,我决定根据加载模型的源数据层的大小重新计算参数。
我们继续研究 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 %.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; } } }
这个完整的嵌套循环,在模型训练的一个世代内,遍历训练样本元素。 之后,我们还要实现验证,基于训练样本意外的数据,评估模型的行为。 为此,运行类似循环遍历最后 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
在第二个测试中,我们决定把模型架构之间的差异最小化,并训练两个具有相同架构的递归模型。 一个模型采用预训练的递归编码器。 第二个模型则完全采用随机权重初始化。 我们依然采用在第一个测试中所用的相同参数来训练这些模型。
测试结果如下图所示。 如您所见,预训练模型开始时误差较小。 但很快第二个模型就贴近了,且它们的数值非常接近。 这证实了之前的结论,即编码器架构对整个模型的性能有重大影响。
注意学习率。 预训练模型验算一个世代所需的时间减少了六倍。 当然,这只是纯粹的时间,不考虑自动编码器训练时间。
结束语
基于上述工作,我们可以得出结论,运用迁移学习技术提供了许多优势。 首先,这项技术确实有效。 应用它,可重用以前训练过的模型模块来解决新问题。 唯一的条件是初始数据的统一性。 在非正确输入数据上使用预训练模块难以奏效。
运用该技术减少了新模型的训练时间。 然而,请注意,我们衡量的是纯测试时间,不包括自动编码器预训练时间。 大概,如果我们加上训练自动编码器所花费的时间,时间将是相等的。 或有可能,由于解码器的架构更加复杂,“纯”模型的训练可以更快。 因此,当假设一个模块能解决各种问题时,运用迁移学习是有据可依的。 此外,当出于某种原因无法将模型作为一个整体进行训练时,它依然可以适用。 例如,模型可能非常复杂,误差梯度在学习过程中衰减,且不能到达所有层。
此外,当我们寻找最佳误差值时,模型逐渐更加复杂化,而该技术此刻可用来搜索最合适的模型。
参考文献列表
- 神经网络变得轻松(第二十部分):自动编码器
- 神经网络变得轻松(第二十一部分):变分自动编码器(VAE)
- 神经网络变得轻松(第二十二部分):递归模型的无监督学习
- 神经网络变得轻松(第二十三部分):构建迁移学习工具
- 神经网络变得轻松(第二十四部分):改进迁移学习工具
本文中用到的程序
# | 名称 | 类型 | 说明 |
---|---|---|---|
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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



你好,迪米特里、
我非常欣赏这一系列文章!谢谢您!但请帮助我解决这个问题:
如果我进行跟踪,发现加载失败发生在 NeuroNet.mqh 中的这一行:
如果加载第 23 部分中名为 "EURUSD_i_PERIOD_H1_test_rnn.nnw "的模型,似乎可以正常工作,但该模型只有两层。这是不正确的。我错过了什么吗?
如果我按本文所述加载 "EURUSD_PERIOD_H1_rnn_vae.nn "文件,会收到 "加载模型出错 "和 "文件已损坏 "的信息:
如果我从第 23 部分加载名为 "EURUSD_i_PERIOD_H1_test_rnn.nnw "的模型,它似乎可以工作,但只有两层。这是不正确的。我错过了什么吗?
您好,
您可以下载 最新版本的文件。要加载 "EURUSD_PERIOD_H1_rnn_vae.nnw "文件,您需要用新的 NeuroNet.mqh 库重新编译 NetCreator。在最后一个模型中,我们将 CBufferDouble 替换为 CBufferFloat。并添加了一些层类型。
您可以下载最新版本的听证 文件。
在最新版本的 NeuroNet.mqh 中,第 2501 页有一个重要警告:
第 2501 页 if(inputs.AssingnArray(input Vals) || ...... )
已过时的行为,隐藏方法调用q 将在未来的 MQL 编译器版本中禁用。
在最新版本的 NetCreatorPanel.mqh 中,有 21 条重要警告。从第 940 页开始
第 935 页 字符串 temp;
第 936 页 ArrayFree(result);
第 937 页 switch(layr.type);
第 938 页 {
第 939 页 case defNeuronBaseOCL :
p 940 temp = StringFormat ("Dense (outputs %d, \ activation %s, \ optimisation %s)", ..... )
警告- 'a' 字符转义序列无法识别
o "未识别的字符转义序列
由于所有这些警告,"_rnn.nnw "文件将无法加载!
在旧版本(第 24 部分)中,在出现错误的萨满教之后,同样的文件"_rnn.nnw "被加载到 NetCreatorPanel.mqh 中,并且可以创建一个新网络。
但无法进行测试。在 check_net 文件中有两个重要警告!
函数 Train 第 222 页和第 307 页
第 219 页 for(int res = 0; (res <3 && sum >0); res++)
p 220 TempData.Update(res, TempData, At(res) /sum);
第 222 页 switch(TempData.Maximum(0,3))
警告 - 过时行为,在未来的 MQL 编译器版本中将禁用隐藏方法调用。
日志显示失败,代码为 32767(参数不正确)
我的 MetaTrader 版本是 5120。
建议有可能用警告来解决问题吗?在我看来,对于像我这样的傻瓜来说,这是一个
是关键一课,不掌握它,唯一能做的就是停止。