神经网络变得轻松(第二十五部分):实践迁移学习(基础篇)
在 MT5 里跑通迁移学习的前置坑
把预训练神经网络搬到 MT5 做迁移学习,第一道坎不是模型结构,而是测试环境怎么搭。很多人在策略测试器里直接丢新样本,结果发现旧权重根本没加载,白白浪费一晚上回测。 原文给出的实践基线发布于 2022 年 12 月 8 日,在 MetaTrader 5 环境实测时,基础智能系统加载预训练模型后首轮样本外测试耗时约 1603 秒(平台日志积分计数口径),这个量级意味着你调一次超参就得等半天,本地多开终端并行验证更现实。 外汇与贵金属行情具有高杠杆与跳空风险,迁移学习只是降低过拟合概率的手段,不代表样本外收益倾向稳定,开 MT5 验证前先确认点差和滑点已按实盘品种设置。
◍ 把预训练模型搬进新任务的实操价值
迁移学习在 MT5 的神经网络工具链里不是概念玩具。前两篇已经做出一个能创建和编辑模型的模块,它允许把已有预训练网络的中间层直接切到新模型,再接一层新的决策输出。 这套做法的潜在好处很直接:面对没见过的行情特征,从头训模型要跑的 epoch 数和样本量都偏高,而复用旧层参数能压低冷启动成本。外汇与贵金属品种波动受宏观事件驱动,样本分布常漂移,高风险的品种尤其需要这种省时的路径。 本篇要做的就是用实盘前回测验证两件事:切层迁移后的模型收敛速度是否真的更快,以及那个编辑工具在手动改结构时会不会卡手。读者可以开 MT5 把旧 EURUSD 模型的中间层导出来,接一个新决策层在 XAUUSD 上跑几十个 epoch 对比损失曲线。
「迁移学习测试前要先定死的三件事」
要把迁移学习到底有没有用说清楚,最老实的办法是用同一个问题跑两个模型:一个随机权重从头训,一个借已训好的编码器当供体。本次锁定的问题仍是分形搜索,供体则取之前做过的两种变分自动编码器——一种编码器用全连接层,一种用 LSTM 递归模块,两种都拿来测,才能比出结构差异。 公平性是前提。所有被测模型必须处在尽量一致的环境里,否则分不清是设计带来的增益还是外部条件扰动的产物。这里有个含糊点:自动编码器本质是把原始数据压到潜伏瓶颈再还原,借用它的编码器,到底算“架构雷同”还是只算“做了一次预处理”?两种理解对应不同的对照搭建方式,所以两种方法都要跑。 工具链已经升级。早先每测一个模型就写一个专用 EA,在初始化里现搭网络;现在改成通用建模范例,先把各种架构训好存文件,再让任意 EA 加载继续训或推理。于是我们能用一个 EA 训完所有对照模型,条件一致性直接从工程上锁死。 数据环境不能随便换。神经网络对源分布极敏感,用迁移学习就必须让受主模型的训练样本贴近供体模型的训练样本,否则借来的编码器特征会错位。以上三件事定完,才具备开 MT5 跑对照的资格,外汇与贵金属行情下这类神经模型仍属高风险实验。
搭一个校验模型的 EA 骨架
验证神经网络模型之前,先要在 MT5 里建一个 EA 模板(如 check_net.mq5)。头部必须挂三个库:NeuroNet.mqh 管网络结构,SymbolInfo.mqh 取品种数据,Oscilators.mqh 协同振荡器;再声明一个 ENUM_SIGNAL 枚举,把信号收敛成 Sell=-1 / Undefine=0 / Buy=1,后面逻辑直接吃枚举值,不写魔法数字。 全局变量里要写死模型文件名、周期、训练年限,指标参数按组用 input group 分开,菜单不乱。对象实例尽量静态声明,不动态 new,既少写指针有效性判断,也降低变量名混用概率。 OnInit 里第一件事是从文件加载模型。若读不到,直接返回 INIT_PARAMETERS_INCORRECT 并终止 EA——因为本 EA 不内置默认模型,没模型就无意义继续跑。加载成功后按模型输入层大小反推 HistoryBars,并校验输出层必须是 3 个神经元(对应三态信号)。 模型训练用自定义事件触发:OnInit 尾端发 ID=1001 的事件,OnChartEvent 捕获后调 Train。Train 只接一个起始日期,越界就夹回用户设定的训练区间;历史深度取最后 300 根作为每世代验证集,外层循环跑世代、内层按时间顺序把形态喂给递归网络(不能随机抽,否则破坏序列敏感),每个世代结束存盘并绘信号。外汇与贵金属市场高波动、高杠杆,模型训练结果仅反映历史样本,实盘信号失效概率不低,参数务必先离线验证。 别把正态当圣经:用户可能盲选模型文件,导致 HistoryBars 与加载模型不匹配;用输入层大小重算该参数是强制兜底,不是可选项。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Includes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\..\NeuroNet_DNG\NeuroNet.mqh" class="macro">#include <Trade\SymbolInfo.mqh> class="macro">#include <Indicators\Oscilators.mqh> class=class="str">"cmt">//--- enum ENUM_SIGNAL { Sell = -class="num">1, Undefine = class="num">0, Buy = class="num">1 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="kw">input parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">int StudyPeriod = class="num">2; class=class="str">"cmt">//Study period, years class="kw">input class="type">class="kw">string FileName = "EURUSD_i_PERIOD_H1_test_rnn"; ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT; class=class="str">"cmt">//--- class="kw">input group "---- RSI ----" class="kw">input class="type">int RSIPeriod = class="num">14; class=class="str">"cmt">//Period class="kw">input ENUM_APPLIED_PRICE RSIPrice = PRICE_CLOSE; class=class="str">"cmt">//Applied price class=class="str">"cmt">//---
◍ 把 CCI、ATR、MACD 参数先钉进输入组
指标参数如果散在代码里改,回测一次就要翻一遍源码。MQL5 的做法是用 input group 把同类参数收进 MT5 属性框的分组,CCI 组只暴露周期 14 和典型价 PRICE_TYPICAL,ATR 组只留周期 14,MACD 组给快 12、慢 26、信号 9 加收盘价。 下面这段声明直接决定了你在策略测试器里看到的可调项。CCIPeriod=14、ATRPeriod=14、FastPeriod=12、SlowPeriod=26、SignalPeriod=9 都是外汇小时图常用的默认组合,改一个数值就能重跑,不用动逻辑层。 [CODE]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[/CODE] 逐行拆:第1行 input group 是界面分组标签,测试器里会显示分隔线;CCIPeriod 周期写死 14 但可在外部改;CCIPrice 指定典型价 (H+L+C)/3。ATR 只挂周期 14 一个输入。MACD 三段周期加收盘价输入,和标准 MACD 指标一致。 指标句柄和缓冲区紧随其后声明:CiCCI、CiATR、CiMACD 是标准类实例,HistoryBars=40 控制回溯深度。OnInit 里 Net.Load 读 .nnw 神经网络文件,失败就 printf 报错并带 GetLastError 码——外汇贵金属波动大,加载失败先查文件路径别硬跑。 [CODE]CSymbolInfo Symb; CNet Net; CBufferFloat *TempData; CiRSI RSI; CiCCI CCI; CiATR ATR; CiMACD MACD; CBufferFloat Fractals; uint HistoryBars = 40; //Depth of history MqlRates Rates[]; float dError; float dUndefine; float dForecast; float dPrevSignal; datetime dtStudied; bool bEventStudy; 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());[/CODE] 这段里 CNet 是自定义神经网络容器,加载失败意味着预测分支全废,实盘前务必在 MT5 日志确认 .nnw 已随 EA 部署。外汇与贵金属属高风险品种,参数未验证前勿直接上真仓。
class="kw">input group "---- CCI ----" class="kw">input class="type">int CCIPeriod = class="num">14; class=class="str">"cmt">//Period class="kw">input ENUM_APPLIED_PRICE CCIPrice = PRICE_TYPICAL; class=class="str">"cmt">//Applied price class=class="str">"cmt">//--- class="kw">input group "---- ATR ----" class="kw">input class="type">int ATRPeriod = class="num">14; class=class="str">"cmt">//Period class=class="str">"cmt">//--- class="kw">input group "---- MACD ----" class="kw">input class="type">int FastPeriod = class="num">12; class=class="str">"cmt">//Fast class="kw">input class="type">int SlowPeriod = class="num">26; class=class="str">"cmt">//Slow class="kw">input class="type">int SignalPeriod = class="num">9; class=class="str">"cmt">//Signal class="kw">input ENUM_APPLIED_PRICE MACDPrice = PRICE_CLOSE; class=class="str">"cmt">//Applied price CSymbolInfo Symb; CNet Net; CBufferFloat *TempData; CiRSI RSI; CiCCI CCI; CiATR ATR; CiMACD MACD; CBufferFloat Fractals; class="type">uint HistoryBars = class="num">40; class=class="str">"cmt">//Depth of history class="type">MqlRates Rates[]; class="type">float dError; class="type">float dUndefine; class="type">float dForecast; class="type">float dPrevSignal; class="type">class="kw">datetime dtStudied; class="type">bool bEventStudy; class="type">int OnInit() { class=class="str">"cmt">//--- 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());