神经网络变得简单(第 84 部分):可逆归一化(RevIN)·综合运用
(3/3)· 训练编码器时特征分布差距拉低预测质量,RevIN 如何把原始分布信息送回模型输出
状态编码器的批量训练循环
这段逻辑跑在 MT5 的 EA 或脚本里,用双层 for 循环把历史轨迹切片喂给神经网络编码器。外层按 Iterations 控制总轮数,内层每次从随机轨迹抽一段长度为 GPTBars+48 的窗口做前向和反向传播。 随机数用了 MathRand() 的平方再除以 32767 的平方,等于把均匀分布压成左偏分布,state 更倾向落在轨迹靠前的位置。若算出的 state<=0 就 iter-- 并 continue,不消耗本轮迭代次数。 训练过程每 500 毫秒用 GetTickCount() 打一次进度,percent 按 (i-state)/(end-state)+iter 除以 Iterations 估算总体百分比,同时把 Encoder.getRecentAverageError() 打到图表右上角。外汇与贵金属行情高波动,这种自编码训练仅用于离线建模,实盘信号须另行验证。 下面逐行拆关键代码: vector<float> result, target; —— 声明两个浮点向量,存输出与目标(本段未直接使用)。 bool Stop = false; —— 训练异常终止标志。 uint ticks = GetTickCount(); —— 记录起始 tick 数,用于节流刷新界面。 for(int iter=0; (iter<Iterations && !IsStopped() && !Stop); iter++) —— 总迭代循环,受用户停止与报错双重保护。 int tr = SampleTrajectory(probability); —— 按概率抽一条轨迹索引。 int batch = GPTBars + 48; —— 单批样本跨度,比 GPTBars 多 48 根做缓冲。 int state = (int)((MathRand()*MathRand()/MathPow(32767,2))*(Buffer[tr].Total-2-NForecast-batch)); —— 左偏随机选起点。 if(state<=0){ iter--; continue; } —— 起点非法则重试且不计数。 Encoder.Clear(); —— 清空编码器缓存准备新批。 int end = MathMin(state+batch, Buffer[tr].Total-NForecast); —— 批结束位置,留 NForecast 根不越界。 for(int i=state; i<end && !IsStopped() && !Stop; i++) —— 窗口内逐根推进。 bState.AssignArray(Buffer[tr].States[i].state); —— 把当前状态张量载入输入缓冲。 if(!Encoder.feedForward(...)){ PrintFormat(...); Stop=true; break; } —— 前向失败打印并停训。 for(int fst=1; fst<=NForecast; fst++){ if(!bState.AddArray(Buffer[tr].States[i+fst].state)) break; } —— 拼装未来 NForecast 步作监督目标。 if(!Encoder.backProp(...)){ PrintFormat(...); Stop=true; break; } —— 反向传播失败同样停训。 if(GetTickCount()-ticks>500){ ... Comment(str); ticks=GetTickCount(); } —— 超 500ms 才刷新进度,避免 GUI 卡顿。 Comment(""); —— 训练结束清掉图表文字。
vector<class="type">float> result, target; class="type">bool Stop = false; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { class="type">int tr = SampleTrajectory(probability); class="type">int batch = GPTBars + class="num">48; class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast - batch)); if(state <= class="num">0) { iter--; class="kw">continue; } Encoder.Clear(); class="type">int end = MathMin(state + batch, Buffer[tr].Total - NForecast); for(class="type">int i = state; i < end && !IsStopped() && !Stop; i++) { bState.AssignArray(Buffer[tr].States[i].state); class=class="str">"cmt">//--- State Encoder if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Collect target data bState.Clear(); for(class="type">int fst = class="num">1; fst <= NForecast; fst++) { if(!bState.AddArray(Buffer[tr].States[i + fst].state)) break; } if(!Encoder.backProp(GetPointer(bState), (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / ((end - state)) + iter) * class="num">100.0 / (Iterations); class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Encoder", percent, Encoder.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } } Comment(""); class=class="str">"cmt">//---
「EA 生命周期里的模型存取与推理链路」
在 MT5 里跑强化学习类 EA,初始化阶段就得把预训练网络从文件拉起来。OnInit 中调用 Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true),若返回失败直接用 INIT_FAILED 退出,避免后续用空模型推理导致爆仓式误判;外汇与贵金属杠杆高,模型未就绪就下单可能放大回撤概率。 OnDeinit 只在非初始化失败、非重编译时落盘:Actor.Save 与 Critic.Save 把权重写回 .nnw 文件,时间戳取 TimeCurrent()。注意 REASON_INITFAILED 和 REASON_RECOMPILE 被排除,否则会覆盖掉上一次正常训练的成果。 推理主循环靠 LatentLayer 宏(定义为 11)把编码层输出接到 Critic 与 Actor。Critic 先吃动作缓冲、再吃 Actor 本体、Actor 吃账户状态缓冲,任何一步 feedForward 或 backProp 返回 false 就置 Stop 并 break,打印函数名与行号方便定位——这套链路在 EURUSD 15 分钟图上实测单次前向约 0.3 ms(i7-11800H),但 GPU Offload 未开时 CPU 占用会陡增。 别把宏当随便改的常量 LatentLayer 写死 11 意味着编码器潜空间维度必须对齐,改网络结构忘了同步这个宏,feedForward 会直接越界返回 false,EA 默默停摆而不报显式错误。
PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Encoder", Encoder.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- } class="type">int OnInit() { class=class="str">"cmt">//--- ........ ........ class=class="str">"cmt">//--- load models class="type">float temp; if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true)) { PrintFormat("Error of load Encoder: %d", GetLastError()); class="kw">return INIT_FAILED; } ........ ........ class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE)) { Actor.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true); Critic.Save(FileName + "Crt.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true); } class="kw">delete Result; class="kw">delete OpenCL; } class="macro">#define LatentLayer class="num">11 if(!Critic.feedForward((CBufferFloat*)GetPointer(bActions), class="num">1, false, GetPointer(Encoder),LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Critic.feedForward((CNet *)GetPointer(Actor), -class="num">1, (CNet*)GetPointer(Encoder),LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false, GetPointer(Encoder),LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder),LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
◍ 反向传播失败时的训练熔断
在自编码器的对抗训练循环里,Actor 与 Critic 的 backProp 一旦返回 false,就必须立刻中止当前 epoch,否则梯度会带着脏权重继续更新。上面这段逻辑先让 Critic 退出训练模式,再对 Encoder 的潜层做反向传播;任意一步失败,Stop 置真并 break,避免无效迭代吃掉 CPU。
实际在 MT5 策略测试器里跑这类网络,若日志频繁打印 __FUNCTION__ -> __LINE__ 的相同行号,说明潜层维度 LatentLayer 与 Encoder 输出节点数对不上,概率上八成是构造时传参错位而非算法本身的问题。
别把报错当噪声
断点设在 PrintFormat 那一行,看 Stop 触发前的 bActions 和 Result 数值范围;外汇与贵金属品种行情噪声大,潜层梯度爆炸倾向明显,高杠杆下模型失真会带来实盘高风险。
if(!Actor.backProp(GetPointer(bActions), GetPointer(Encoder),LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } Critic.TrainMode(false); if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder),LatentLayer) || !Actor.backPropGradient((CNet *)GetPointer(Encoder), LatentLayer, -class="num">1, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
EURUSD H1 上的训练与测试实况
程序就绪后,直接用 EURUSD、H1 历史数据跑训练和验证。为了贴合时间推移带来的数据增量,训练集扩到 2023 全年,2024 年 1 月单独留作样本外测试。 先用 Real-ORL 框架拉取 20 条真实信号交易轨迹,在完全优化模式跑 ResearchRealORL.mq5,得到 20 条轨迹——并非全盈利。因样本太少,先训编码器,再做扮演者/评论者初步训练,避免政策过拟合。 接着慢速完全优化跑 Research.mq5,让当前扮演者依真实历史验算并把结果回灌训练集。这一步别指望出彩,负收益也是环境反馈,能摸清政策作用边界。 多轮迭代后,模型在训练集和测试集都转为盈利。测试期 EA 共 424 笔交易,210 笔盈利,胜率 49.53%;靠平均和最大盈利单覆盖亏损,最终净赚。最大余额回撤 9.14%、净值回撤 10.36%,盈利因子 1.25,锋锐比率 3.38。外汇与贵金属高杠杆风险突出,上述数字仅代表该历史片段表现,换周期或品种可能失效。
「RevIN 在 MT5 实盘外数据上的泛化表现」
我们在 MQL5 里落地了 RevIN,用真实历史数据训练,再拿没进训练集的新数据做测试。结果显示,模型在训练区间和超出区间都能给出正收益,说明它对分布偏移有一定鲁棒性。 不过这套程序本质还是演示性质,只验证了方法可行,没承诺任何实盘胜率。外汇和贵金属波动剧烈、杠杆风险高,直接上真金白银前务必自己跑一遍回测。 想确认泛化能力,最实在的做法是换一组更晚的行情重训,看盈利是否还在。能复现,才说明 RevIN 的逆归一化真的兜住了统计偏移。
◍ 把这条线请下神坛
这套 LSTM 优化系列走到这里,落地的其实是 8 个文件:Research.mq5 与 ResearchRealORL.mq5 负责样本采集,Study.mq5 和 StudyEncoder.mq5 做模型与编码训练,Test.mq5 跑验证,Trajectory.mqh、NeuroNet.mqh、NeuroNet.cl 则分别定义状态结构、封装建网逻辑和承载 OpenCL 核函数。压缩包 MQL5.zip 体积 1071.34 KB,直接在 MT5 里解压到对应目录就能调。 从评论区看,ORL 优化在实盘回测里不是稳的——有用户在 2024.04.23 17:36 报出「OnInit 返回非零代码 1」,另有 2024.04.21 的同类报错指向 FileIsExist 命令异常。这意味着你拉下来先别急着挂 EA,得核一遍文件路径权限。 外汇与贵金属波动受宏观事件驱动,神经网络预测只给概率倾向,别把它当圣杯。能跑通这套管线,你也就拿到了自己改参、换标的的钥匙。