神经网络变得轻松(第二十二部分):递归模型的无监督学习·综合运用
◍ LSTM 神经元的存档与重载逻辑
在 MT5 用 OpenCL 跑 LSTM 网络时,模型权值和中间状态必须能落盘和回读,否则每次重启终端都得重新训练。下面这段 CNeuronLSTMOCL 的 Save / Load 就是干这件事的底层实现。 Save 先调基类 CNeuronBaseOCL::Save 写公共字段,再用 FileWriteInteger 把输入维度 m_iInputs 以 INT_VALUE 写进句柄;随后依次对 LSTM 权重、一阶动量、二阶动量做 BufferRead 再 Save,任何一步返回 false 就整体失败。 Load 的坑在于:读回 m_iInputs 后,三类缓冲区都要先 BufferFree 再 Load,且 Load 完必须调 BufferCreate(OpenCL) 重新在显存建缓冲,漏掉这一步推理会直接读脏数据。 Load 后半段重建了 6 类 OpenCL 缓冲:m_iMemory 大小为 float*2*Neurons(),m_iConcatenated 与梯度各占 float*4*Neurons(),隐藏态为 float*Neurons(),权重梯度为 float*总权数。每个缓冲建完都查是否 <0,负数即显存申请失败。 外汇与贵金属市场高杠杆、高波动,用此类 GPU 模型做信号辅助时,回测与实盘偏差可能显著,任何加载异常都应视为模型不可信而非暂时抖动。
class="type">bool CNeuronLSTMOCL::Save(class="kw">const class="type">int file_handle) { if(!CNeuronBaseOCL::Save(file_handle)) class="kw">return class="kw">false; if(FileWriteInteger(file_handle, m_iInputs, INT_VALUE) < class="kw">sizeof(m_iInputs)) class="kw">return class="kw">false; if(!m_cWeightsLSTM.BufferRead() || !m_cWeightsLSTM.Save(file_handle)) class="kw">return class="kw">false; if(!m_cFirstMomentumLSTM.BufferRead() || !m_cFirstMomentumLSTM.Save(file_handle)) class="kw">return class="kw">false; if(!m_cSecondMomentumLSTM.BufferRead() || !m_cSecondMomentumLSTM.Save(file_handle)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronLSTMOCL::Load(class="kw">const class="type">int file_handle) { if(!CNeuronBaseOCL::Load(file_handle)) class="kw">return class="kw">false; m_iInputs = FileReadInteger(file_handle); class=class="str">"cmt">//--- m_cWeightsLSTM.BufferFree(); if(!m_cWeightsLSTM.Load(file_handle) || !m_cWeightsLSTM.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- m_cFirstMomentumLSTM.BufferFree(); if(!m_cFirstMomentumLSTM.Load(file_handle) || !m_cFirstMomentumLSTM.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- m_cSecondMomentumLSTM.BufferFree(); if(!m_cSecondMomentumLSTM.Load(file_handle) || !m_cSecondMomentumLSTM.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(m_iMemory >= class="num">0) OpenCL.BufferFree(m_iMemory); m_iMemory = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * class="num">2 * Neurons(), CL_MEM_READ_WRITE); if(m_iMemory < class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(m_iConcatenated >= class="num">0) OpenCL.BufferFree(m_iConcatenated); m_iConcatenated = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * class="num">4 * Neurons(), CL_MEM_READ_WRITE); if(m_iConcatenated < class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(m_iConcatenatedGradient >= class="num">0) OpenCL.BufferFree(m_iConcatenatedGradient); m_iConcatenatedGradient = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * class="num">4 * Neurons(), CL_MEM_READ_WRITE); if(m_iConcatenatedGradient < class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(m_iHiddenState >= class="num">0) OpenCL.BufferFree(m_iHiddenState); m_iHiddenState = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * Neurons(), CL_MEM_READ_WRITE); if(m_iHiddenState < class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(m_iWeightsGradient >= class="num">0) OpenCL.BufferFree(m_iWeightsGradient); m_iWeightsGradient = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * m_cWeightsLSTM.Total(), CL_MEM_READ_WRITE); if(m_iWeightsGradient < class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
递归自编码器的无监督预训练验证
把上篇的变分自编码器换成循环结构后,新模型存为 rnn_vae.mq5,编码器里塞进了 LSTM 递归模块。喂给递归编码器的只有最近 10 根烛条,逼模型去翻“记忆”看更早的状态。 为了规划递归训练,每轮前馈后把最老的那根烛信息从数据栈踢掉、把新烛压到栈尾,栈深由外部参数定。栈比编码器原始输入长时,自编码器就被迫利用过去状态的潜伏记忆。 测试照旧:EURUSD、H1、回看 15 年、指标默认。编码器收 10 根蜡烛,已训解码器吐 40 根。每根新蜡烛收盘后灌数据,跑 20 个学习世代后误差基本走平,损失率压到 9% 以下,潜伏态里至少留着 30 个前序迭代的信息——说明这套无监督预训练路数在递归模型上可行。 想自己复现,直接看 OnInit 里网络拓扑的搭建:输入层 10×12 节点,接 BatchNorm,再叠两层 LSTM(500 → 250)。外汇与贵金属波动剧烈,此类模型仅作特征提取辅助,实盘高风险。
class="type">int OnInit() { class=class="str">"cmt">//--- ................ ................ class=class="str">"cmt">//--- Net = new CNet(NULL); ResetLastError(); class="type">class="kw">float temp1, temp2; if(!Net || !Net.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, class="kw">false)) { printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError()); HistoryBars = iHistoryBars; CArrayObj *Topology = new CArrayObj(); if(CheckPointer(Topology) == POINTER_INVALID) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">0 CLayerDescription *desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; class="type">int prev = desc.count = class="num">10 * class="num">12; desc.type = defNeuronBaseOCL; desc.optimization = ADAM; desc.activation = None; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">1 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = prev; desc.batch = class="num">1000; desc.type = defNeuronBatchNormOCL; desc.activation = None; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">2 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; prev = desc.count = class="num">500; desc.type = defNeuronLSTMOCL; desc.activation = None; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">3 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; prev = desc.count = prev/class="num">2; desc.type = defNeuronLSTMOCL; desc.activation = None; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">4
「堆叠 LSTM 与 VAE 后的多尺度输出层」
在定义完首层 LSTM(50 个单元、无激活、ADAM 优化)之后,网络继续向下接一个 VAE 层,其神经元数直接取上层 prev 的一半,即 25 个,类型标记为 defNeuronVAEOCL,不再单独设激活与优化器。 随后连续铺设 5 层基础输出层(defNeuronBaseOCL),全部使用 TANH 激活与 ADAM 优化,神经元数按 HistoryBars 的倍数递增:1 倍、2 倍、4 倍、12 倍。若 HistoryBars 取常见值 30,则这四层规模分别为 30、60、120、360,呈明显的多时间尺度展开。 每层都先 new 出 CLayerDescription,用 CheckPointer 判空,失败即返回 INIT_FAILED;通过 Topology.Add 入栈,任一添加失败同样终止初始化。最后 delete 旧 Net 与 Topology,以 CNet(Topology) 重建网络对象,并再次用 CheckPointer 确认 Net 有效,否则后续推理会直接崩在指针上。 这种层数固定、宽度跳变的拓扑在 MT5 里跑起来显存占用可能随 HistoryBars 放大而陡增,外汇与贵金属行情的高波动特性会放大过拟合概率,实盘前务必用历史数据做交叉验证。
desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; prev = desc.count = class="num">50; desc.type = defNeuronLSTMOCL; desc.activation = None; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">5 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = prev/class="num">2; desc.type = defNeuronVAEOCL; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">6 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">7 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars * class="num">2; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">8 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars * class="num">4; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">9 desc = new CLayerDescription(); if(CheckPointer(desc) == POINTER_INVALID) class="kw">return INIT_FAILED; desc.count = (class="type">int) HistoryBars * class="num">12; desc.type = defNeuronBaseOCL; desc.activation = TANH; desc.optimization = ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class="kw">delete Net; Net = new CNet(Topology); class="kw">delete Topology; if(CheckPointer(Net) == POINTER_INVALID)
◍ 神经网络回测里的样本切分与滑窗校验
这段逻辑出现在 MT5 自定义指标或 EA 的初始化与训练分支里,核心是把网络输出按 12 维一组折算历史 bar 数:HistoryBars = temp.Total() / 12,意味着一套 12 输入特征的网络,总结果长度直接除以 12 就是可用历史样本量。 初始化失败会走 return INIT_FAILED 并把 dError 置为 FLT_MAX(约 3.4e38),这是浮点上限,用来标记无效误差而非真实训练值。成功路径返回 INIT_SUCCEEDED,训练循环才往下走。 在线推演时每次 feedForward 喂 12 维 TempData,取第 1 层输出后做滑窗:check_data 超过 check_count 就从头删到剩 check_count-12 个,再把最新 12 个输出追加进去。这样校验集始终是一个长度为 check_count 的滚动窗口。 当 (total-it) 大于 HistoryBars 时才触发 backProp(check_data) 做反向传播,说明网络只在前段样本内回灌梯度,后段留作前向验证。外汇与贵金属市场的高波动可能让这种固定 12 维窗口在跳空时失效,实盘前建议在 MT5 策略测试器里改 check_count 与 12 这个维度硬编码,观察样本外误差是否发散。
class="kw">return INIT_FAILED; dError = FLT_MAX; } else { CBufferFloat *temp; Net.getResults(temp); HistoryBars = temp.Total() / class="num">12; class="kw">delete temp; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } Net.feedForward(TempData, class="num">12, true); TempData.Clear(); if(!Net.GetLayerOutput(class="num">1, TempData)) class="kw">break; class="type">uint check_total = check_data.Total(); if(check_total >= check_count) { if(!check_data.DeleteRange(class="num">0, check_total - check_count + class="num">12)) class="kw">return; } for(class="type">int t = TempData.Total() - class="num">12 - class="num">1; t < TempData.Total(); t++) { if(!check_data.Add(TempData.At(t))) class="kw">return; } if((total-it)>(class="type">int)HistoryBars) Net.backProp(check_data);
递归自编码器的实测恢复能力
前面几节把递归自动编码器搭起来并跑了测试训练,核心验证的是用自动编码器做无监督预训练这条路是否走得通。 从实测看,模型在恢复过去 30 次迭代的历史数据时表现相当优异,说明用自编码结构榨取序列隐含特征、再喂给递归模型,是可行的工程路径。 外汇与贵金属市场高波动、易跳空,这类无监督表征能否在实盘噪声中保持泛化,仍需你在 MT5 里换品种、调迭代窗口自行复验。
「延伸阅读的技术脉络」
这一节列的是前面几篇涉及的底层方法出处,基本都围绕神经网络在行情序列上的用法。从循环网络到聚类、降维、关联规则,再到自动编码器和 VAE,覆盖了非监督与序列建模两条线。 如果你打算在 MT5 里自己复现聚类或关联规则模块,建议优先看「数据聚类」「利用 MQL5 进行数据聚类」「使用 MQL5 的关联规则」这三篇对应的思路,它们离可落地的 EA 代码最近。 外汇与贵金属杠杆高、滑点随机,任何基于历史聚类的信号都只是概率倾向,实盘前务必用策略测试器跑多品种多周期验证。
◍ 随包附带的工程文件
这套 RNN-VAE 预测方案不是单文件 EA,而是拆成了可复用的类库与 OpenCL 内核。压缩包 MQL5.zip(68.41 KB)里至少含四个关键件:rnn_vae.mq5 是训练用智能系统主体;VAE.mqh 管变分自动编码器的潜变量层;NeuroNet.mqh 负责搭网络结构;NeuroNet.cl 则是跑在显卡上的 OpenCL 代码库。 想本地复现,先把 ZIP 解进 MT5 的 MQL5 目录,编译 rnn_vae.mq5 前确认终端已识别 OpenCL 设备。有用户在 2024 年 5 月反馈,CNET::save 里 layers.Save(handle) 报错,作者建议重装 OpenCL 驱动——若重装仍第 8 条红,多半是设备句柄没拿到,需在 MT5 选项里核对 GPU 列表。 外汇与贵金属行情受杠杆与滑点影响,神经网络预测仅提供概率倾向,实盘前请用策略测试器跑至少三年 tick 数据验证过拟合程度。