交易中的神经网络:受控分段(终章)·综合运用
📘

交易中的神经网络:受控分段(终章)·综合运用

第 3/3 篇

◍ RefMask 网络的初始化与前向骨架

下面这段是 CNeuronRefMask 类里构建内部结构的核心代码,直接决定了内容编码器、背景分支、语言原语模块和 decoder 的层数拓扑。任何一层 Init 失败都会让整个网络返回 false,MT5 策略加载时若报空指针,优先查这里。 内容编码器连续挂了三个 CNeuronBaseOCL:第一层输入维度是 window*content_units、输出 1;第二层输出 2;第三层输出 3 且输入维度多了 3 个额外单元(window*(content_units+3))。背景分支则用了 window*3 的输入和两条独立神经元。 语言原语构造 cLPC 与解码器 cDecoder 通过 CNeuronOCM 对象堆叠,decoder 里有一个 for 循环按 layers 数量动态挂层,每层 OCM 的神经元数取自 cGEGWA 内部层。最后 cOCM 的层索引被固定为 layers+8,输出和梯度指针在此绑定。 外汇与贵金属行情下用这类 GPU 加速网络做特征提取,属于高风险实验,回测不保证实盘概率稳定,建议先在 MT5 的 OpenCL 环境跑通 Init 再谈调参。

MQL5 / C++
   !neuron.Init(window * content_units, class="num">1, OpenCL, content_size, optimization, iBatch) ||
   !cContentEncoder.Add(neuron)
   )
      class="kw">return class="kw">false;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(class="num">0, class="num">2, OpenCL, window * content_units, optimization, iBatch) ||
      !cContentEncoder.Add(neuron)
   )
      class="kw">return class="kw">false;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(class="num">0, class="num">3, OpenCL, window * (content_units + class="num">3), optimization, iBatch) ||
      !cContentEncoder.Add(neuron)
   )
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Background
   cBackGround.Clear();
   cBackGround.SetOpenCL(OpenCL);
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(window * class="num">3, class="num">4, OpenCL, content_size, optimization, iBatch) ||
      !cBackGround.Add(neuron)
   )
      class="kw">return class="kw">false;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(class="num">0, class="num">5, OpenCL, window * class="num">3, optimization, iBatch) ||
      !cBackGround.Add(neuron)
   )
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Linguistic Primitive Construction
   if(!cLPC.Init(class="num">0, class="num">6, OpenCL, window, window_key, heads, heads, primitive_units, content_units, class="num">2, class="num">1,
optimization, iBatch))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Decoder
   cDecoder.Clear();
   cDecoder.SetOpenCL(OpenCL);
   CNeuronOCM *ocm = new CNeuronOCM();
   if(!ocm ||
      !ocm.Init(class="num">0, class="num">7, OpenCL, window, window_key, units_count, heads, window, primitive_units, heads,
optimization, iBatch) ||
      !cDecoder.Add(ocm)
   )
      class="kw">return class="kw">false;
   for(class="type">uint i = class="num">0; i < layers; i++)
     {
      neuron = cGEGWA.GetInsideLayer(i);
      ocm = new CNeuronOCM();
      if(!ocm || !neuron ||
         !ocm.Init(class="num">0, i + class="num">8, OpenCL, window, window_key, neuron.Neurons() / window, heads, window,
primitive_units, heads, optimization, iBatch) ||
         !cDecoder.Add(ocm)
         )
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- Object Cluster Module
   if(!cOCM.Init(class="num">0, layers + class="num">8, OpenCL, window, window_key, primitive_units, heads, window, content_units,
heads, optimization, iBatch))
      class="kw">return class="kw">false;
   if(!SetOutput(cOCM.getOutput()) ||
      !SetGradient(cOCM.getGradient())
   )
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronRefMask::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!SecondInput)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Context Encoder
   CNeuronBaseOCL *context = cContentEncoder[class="num">0];
   if(context.getOutput() != SecondInput)
     {
      if(!context.SetOutput(SecondInput, true))

「RefMask 前向传播与梯度回传的骨架」

下面这段 CNeuronRefMask 的实现把文档级 Transformer 的推理链路拆得很直白:训练态下背景编码器要逐层 FeedForward,推理态只取最后一层,省掉中间计算。 内容编码器从索引 1 跑到 content_total-1,每层都拿前一层输出做前向;任意一层指针为空或传播失败就直接 return false,说明该网络对层间拓扑连续性零容忍。 梯度回传侧,calcInputGradients 先校验神经元与梯度缓冲指针,再把 content_encoder[0] 的梯度重定向到 SecondGradient,并切换激活函数;若你改了 OCM 的输入梯度逻辑,必须从 cDecoder 末层反推到 context,否则反向图会断。 外汇与贵金属行情序列用这类结构做特征抽取时,过拟合概率偏高,建议先在 MT5 用小规模样本跑通前向再扩维。

MQL5 / C++
      class="kw">return class="kw">false;
   }
   class="type">int content_total = cContentEncoder.Total();
   for(class="type">int i = class="num">1; i < content_total - class="num">1; i++)
   {
      context = cContentEncoder[i];
      if(!context ||
         !context.FeedForward(cContentEncoder[i - class="num">1])
         )
         class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Background Encoder
   CNeuronBaseOCL *background = NULL;
   if(bTrain)
   {
      for(class="type">int i = class="num">1; i < cBackGround.Total(); i++)
      {
         background = cBackGround[i];
         if(!background ||
            !background.FeedForward(cBackGround[i - class="num">1])
            )
            class="kw">return class="kw">false;
      }
   }
   else
   {
      background = cBackGround[cBackGround.Total() - class="num">1];
      if(!background)
         class="kw">return class="kw">false;
   }
   CNeuronBaseOCL *neuron = cContentEncoder[content_total - class="num">1];
   if(!neuron ||
      !Concat(context.getOutput(), background.getOutput(), neuron.getOutput(), context.Neurons(),
background.Neurons(), class="num">1))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Geometry-Enhaced Group-Word Attention
   if(!cGEGWA.FeedForward(NeuronOCL, neuron.getOutput()))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Linguistic Primitive Construction
   if(!cLPC.FeedForward(context))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Decoder
   CNeuronOCM *decoder = cDecoder[class="num">0];
   if(!decoder.feedForward(GetPointer(cGEGWA), GetPointer(cLPC)))
      class="kw">return class="kw">false;
   for(class="type">int i = class="num">1; i < cDecoder.Total(); i++)
   {
      decoder = cDecoder[i];
      if(!decoder.feedForward(cGEGWA.GetInsideLayer(i - class="num">1), cDecoder[i - class="num">1]))
         class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Object Cluster Module
   if(!cOCM.feedForward(decoder, context))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronRefMask::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
CBufferFloat *SecondGradient,
ENUM_ACTIVATION SecondActivation = None)
   {
   if(!NeuronOCL || !SecondGradient)
      class="kw">return class="kw">false;
   CNeuronBaseOCL *neuron = cContentEncoder[class="num">0];
   if(!neuron)
      class="kw">return class="kw">false;
   if(neuron.getGradient() != SecondGradient)
   {
      if(!neuron.SetGradient(SecondGradient))
         class="kw">return class="kw">false;
      neuron.SetActivationFunction(SecondActivation);
   }
class=class="str">"cmt">//--- Object Cluster Module
   CNeuronBaseOCL *context = cContentEncoder[cContentEncoder.Total() - class="num">2];
   if(!cOCM.calcInputGradients(cDecoder[cDecoder.Total() - class="num">1], context))

逆向传播里的梯度回传链路

这段反向传播代码把解码器、语境编码器与背景网络的梯度逐层往回送,任何一层 calcHiddenGradients 或 DeConcat 失败就直接 return false,训练不会继续。 解码器循环从 cDecoder.Total()-1 倒序到 1,用 calcInputGradients 把误差传给前一层;索引 0 的解码器则把梯度算到 cGEGWA 和 cLPC 上,这是几何增强注意力的入口。 语境原语构建段先把 context 梯度取出,再用 SetGradient(PrevOutput,false) 重置,随后 FeedForward 并 SumAndNormilize,归一化系数写死为 1、偏移 0,说明这一步不做缩放只做累加。 背景与内容编码器分别走 DeActivation 和 calcHiddenGradients:背景从倒数第二层循环到 1,内容编码器从 Total()-3 到 0,跳过最后两层是因为它们已在前面 GEGWA 段处理过。 在 MT5 里把这段贴进你自己的 CNet 派生类,把 cContentEncoder.Total()-3 改成实际层数减 2 做断点,能直接看出哪一层先返回 false,外汇与贵金属模型训练高风险,梯度爆炸可能让回测失效。

MQL5 / C++
  class="kw">return class="kw">false;
class=class="str">"cmt">//--- Decoder
   CNeuronOCM *decoder = NULL;
   for(class="type">int i = cDecoder.Total() - class="num">1; i > class="num">0; i--)
     {
       decoder = cDecoder[i];
       if(!decoder.calcInputGradients(cGEGWA.GetInsideLayer(i - class="num">1), cDecoder[i - class="num">1]))
         class="kw">return class="kw">false;
     }
   decoder = cDecoder[class="num">0];
   if(!decoder.calcInputGradients(GetPointer(cGEGWA), GetPointer(cLPC)))
     class="kw">return class="kw">false;
class=class="str">"cmt">//--- Linguistic Primitive Construction
   CBufferFloat *context_grad = context.getGradient();
   if(!context.SetGradient(PrevOutput, class="kw">false))
     class="kw">return class="kw">false;
   if(!cLPC.FeedForward(context) ||
      !SumAndNormilize(context_grad, context.getGradient(), context_grad, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)
     )
     class="kw">return class="kw">false;
class=class="str">"cmt">//--- Geometry-Enhaced Group-Word Attention
   neuron = cContentEncoder[cContentEncoder.Total() - class="num">1];
   if(!neuron ||
      !NeuronOCL.calcHiddenGradients((CObject*)GetPointer(cGEGWA), neuron.getOutput(), neuron.getGradient(),
(ENUM_ACTIVATION)neuron.Activation()))
     class="kw">return class="kw">false;
   if(!DiversityLoss(neuron, cOCM.GetContextWindow(), neuron.Neurons() / cOCM.GetContextWindow(), true))
     class="kw">return class="kw">false;
   CNeuronBaseOCL *background = cBackGround[cBackGround.Total() - class="num">1];
   if(!background ||
      !DeConcat(context.getGradient(), background.getGradient(), neuron.getGradient(), context.Neurons(),
background.Neurons(), class="num">1) ||
      !DeActivation(context.getOutput(), context.getGradient(), context.getGradient(), context.Activation()) ||
      !SumAndNormilize(context_grad, context.getGradient(), context_grad, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1) ||
      !context.SetGradient(context_grad, class="kw">false)
     )
     class="kw">return class="kw">false;
class=class="str">"cmt">//--- Context Encoder
   for(class="type">int i = cContentEncoder.Total() - class="num">3; i >= class="num">0; i--)
     {
       context = cContentEncoder[i];
       if(!context ||
          !context.calcHiddenGradients(cContentEncoder[i + class="num">1])
         )
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- Background
   if(!DeActivation(background.getOutput(), background.getGradient(), background.getGradient(),
background.Activation()))
     class="kw">return class="kw">false;
   for(class="type">int i = cBackGround.Total() - class="num">2; i > class="num">0; i--)
     {
       background = cBackGround[i];
       if(!background ||
          !background.calcHiddenGradients(cBackGround[i + class="num">1])
         )
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }

◍ 用2024年1月数据验模型成色

模型架构改动不波及输入输出结构,所以前面在 H1 上取的 EURUSD 2023 全年真实历史训练集能直接复用,指标全部走默认值。训练是离线跑的,但我们会按参与者政策不断把新局次补进数据集再重训,直到性能够用。 准备本文时我们写了一套挺有意思的参与者政策,拿它跑 2024 年 1 月行情做测试——这段测试期根本没进训练集,等于逼模型在没见过的数据上干活,最贴近实盘用法。 结果这一个月模型下了 21 单,14 单盈利,胜率 66% 出头。多空两边盈利单占比都压过亏损单,而且盈利单平均利润是亏损单平均亏损的 2 倍,最大盈利差不多是最大亏损的 3 倍,余额曲线是肉眼可见的向上台阶。 样本只有 21 笔,远谈不上证明长期有效,外汇和高杠杆贵金属本就高风险,这套思路只是显出了可挖的空间,值得接着调。

「画得少,看得清」

前面两篇把 RefMask3D 思路落进 MQL5 时做了本地化改动,跑出来的样本和训练痕迹说明这条路径值得继续挖,但眼下这套程序只是演示骨架,直接挂真仓不合适,外汇和贵金属的高风险不会因为模型新颖就消失。 从日志看,Study EA 在 EURUSD H1 上训到 295 步时 Critic 损失降到 0.0005726、Actor 损失 0.0803357,随后主动退出;同时报了 19968 字节内存泄漏和两处 deprecated 警告,说明类库还没收干净。 想自己复现,就把 Research / Study / Test 三个 EA 加 NeuroNet 系列类库丢进 MT5,先跑 EURUSD H1 看训练曲线,别急着接实盘。

MQL5 / C++
class="num">2024.10.class="num">08 class="num">21:class="num">28:class="num">01.820 Study(EURUSD,H1)                RefMaskAct.nnw
class="num">2024.10.class="num">08 class="num">21:class="num">28:class="num">01.896 Study(EURUSD,H1)                RefMaskCrt.nnw
class="num">2024.10.class="num">08 class="num">22:class="num">48:class="num">49.440 Study(EURUSD,H1)                Train -> class="num">294 -> Actor          class="num">0.0803357
class="num">2024.10.class="num">08 class="num">22:class="num">48:class="num">49.440 Study(EURUSD,H1)                Train -> class="num">295 -> Critic         class="num">0.0005726
class="num">2024.10.class="num">08 class="num">22:class="num">48:class="num">49.440 Study(EURUSD,H1)                ExpertRemove() function called
class="num">2024.10.class="num">08 class="num">22:class="num">48:class="num">49.558 Study(EURUSD,H1)                class="num">14 undeleted dynamic objects found:
class="num">2024.10.class="num">08 class="num">22:class="num">48:class="num">49.558 Study(EURUSD,H1)                  class="num">14 objects of class &class="macro">#x27;CBufferFloat&class="macro">#x27;
class="num">2024.10.class="num">08 class="num">22:class="num">48:class="num">49.558 Study(EURUSD,H1)                class="num">19968 bytes of leaked memory found
Series.mqh
ArrayDouble.mqh
&class="macro">#x27;NeuroNet.cl&class="macro">#x27; as &class="macro">#x27;class="kw">const class="type">class="kw">string cl_program&class="macro">#x27;                                        class="num">1
deprecated behavior, hidden method calling will be disabled in a future MQL compiler version     NeuroNet.mqh   class="num">30478   class="num">22
deprecated behavior, hidden method calling will be disabled in a future MQL compiler version     NeuroNet.mqh   class="num">30700   class="num">22
code generated                                                                         class="num">1
class="num">0 errors, class="num">2 warnings, class="num">6344 msec elapsed, cpu=&class="macro">#x27;X64 Regular&class="macro">#x27;                             class="num">3

常见问题

用较小的随机值做受控分段初始化,并固定掩码维度,避免早期梯度爆炸;可先跑几个 batch 看 loss 是否平稳。
断点常在掩码拼接处,需保证每一段输出 shape 对齐;建议单独打印各分段梯度范数定位。
可以,小布能按你的品种加载受控分段模型,自动标注梯度异常区间并推送提醒,你只看结论就行。
重点看分段命中率和回撤比,样本外准确率若低于55%说明过拟合,需调掩码层。
只保留通过显著性检验的掩码段,其余折叠;视觉上仅留2~3条主参考线,避免图表噪音。