内容
- 概述
- 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
在第二个测试中,我们决定把模型架构之间的差异最小化,并训练两个具有相同架构的递归模型。 一个模型采用预训练的递归编码器。 第二个模型则完全采用随机权重初始化。 我们依然采用在第一个测试中所用的相同参数来训练这些模型。
测试结果如下图所示。 如您所见,预训练模型开始时误差较小。 但很快第二个模型就贴近了,且它们的数值非常接近。 这证实了之前的结论,即编码器架构对整个模型的性能有重大影响。
注意学习率。 预训练模型验算一个世代所需的时间减少了六倍。 当然,这只是纯粹的时间,不考虑自动编码器训练时间。
结束语
基于上述工作,我们可以得出结论,运用迁移学习技术提供了许多优势。 首先,这项技术确实有效。 应用它,可重用以前训练过的模型模块来解决新问题。 唯一的条件是初始数据的统一性。 在非正确输入数据上使用预训练模块难以奏效。
运用该技术减少了新模型的训练时间。 然而,请注意,我们衡量的是纯测试时间,不包括自动编码器预训练时间。 大概,如果我们加上训练自动编码器所花费的时间,时间将是相等的。 或有可能,由于解码器的架构更加复杂,“纯”模型的训练可以更快。 因此,当假设一个模块能解决各种问题时,运用迁移学习是有据可依的。 此外,当出于某种原因无法将模型作为一个整体进行训练时,它依然可以适用。 例如,模型可能非常复杂,误差梯度在学习过程中衰减,且不能到达所有层。
此外,当我们寻找最佳误差值时,模型逐渐更加复杂化,而该技术此刻可用来搜索最合适的模型。
参考文献列表
- 神经网络变得轻松(第二十部分):自动编码器
- 神经网络变得轻松(第二十一部分):变分自动编码器(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
MyFxtops迈投(www.myfxtops.com)-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。