交易中的神经网络:受控分段·进阶篇
📘

交易中的神经网络:受控分段·进阶篇

第 2/3 篇

「GEGWA 神经元的接口与初始化骨架」

在 MT5 的 OpenCL 神经网络扩展里,CNeuronGEGWA 这个类把多头注意力与门控结构压进同一个计算节点。它的两个 updateInputWeights 重载都被 override 成直接返回 false,说明该层不沿用基类的权重回传逻辑,梯度路径改由 WeightsUpdate 与内部子层自行处理。 构造函数只初始化了 bAddNeckGradient(false),析构为空,Type() 恒返回 defNeuronGEGWA 用于运行时辨识节点类型。文件交互靠 Save / Load 两个虚函数落地,方便把训好的权重存盘再热加载。 Init 是真正干活的地方:先以 window*units_count 作为基类的输入维度调 CNeuronBaseOCL::Init,任一环节失败立即返回 false;随后初始化 cAttention[0],传入 window、window_key、heads、units_count、layers 等参数。你在 EA 里若想接这套结构,至少得先确认传入的 window 与 units_count 乘积不超过显存允许的单层宽度,否则 Init 会静默失败。 AddNeckGradient 这个开关控制是否把 neck 梯度并入更新,默认关。实盘外汇或贵金属模型用这类结构时波动率高,过拟合风险偏大,建议先在小样本回测里把 flag 开着观察梯度流向再决定。

MQL5 / C++
class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; }
class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override;
class="kw">public:
                      CNeuronGEGWA(class="type">void)  :  bAddNeckGradient(class="kw">false) {};
                     ~CNeuronGEGWA(class="type">void) {};
 class=class="str">"cmt">//---
 class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_count,
                         class="type">uint window_kv, class="type">uint heads_kv, class="type">uint units_count_kv,
                         class="type">uint layers, class="type">uint inside_bloks,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
 class=class="str">"cmt">//---
 class="kw">virtual class="type">int       Type(class="type">void)  const   {  class="kw">return defNeuronGEGWA;  }
 class=class="str">"cmt">//--- methods for working with files
 class="kw">virtual class="type">bool      Save(class="type">int const file_handle);
 class="kw">virtual class="type">bool      Load(class="type">int const file_handle);
 class=class="str">"cmt">//---
 class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau);
 class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
 class=class="str">"cmt">//---
 class="kw">virtual CNeuronBaseOCL* GetInsideLayer(const class="type">int layer) const;
 class="kw">virtual class="type">void      AddNeckGradient(const class="type">bool flag) {  bAddNeckGradient = flag; }
};
class="type">bool CNeuronGEGWA::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_count,
                         class="type">uint window_kv, class="type">uint heads_kv, class="type">uint units_count_kv,
                         class="type">uint layers, class="type">uint inside_bloks,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
     class="kw">return class="kw">false;
  if(!cAttention[class="num">0].Init(class="num">0, class="num">0, OpenCL, window, window_key, heads, units_count, layers, optimization, iBatch))
     class="kw">return class="kw">false;

◍ 神经网络子层初始化的分支与指针接管

这段初始化逻辑出现在自定义神经网络类的内部构建函数里,核心是根据 inside_bloks 是否大于 0 决定颈部(neck)结构走哪条路。若大于 0,就 new 一个 CNeuronGEGWA 并递归把 inside_bloks 减 1 往下传;否则改用 CNeuronMLCrossAttentionMLKV 做交叉注意力收口。 两个分支都先做空指针判断,Init 失败立即 delete 并返回 false,避免悬空对象占用 MT5 策略测试器的内存。cNeck 最终被赋值为 temp 指针,意味着后续前向传播直接复用该实例,不再重新分配。 其余固定层——cMergeSplit[0/1]、cAttention[1]、cResidual、cCrossAttention——均按硬编码的 layer 序号(1 到 6)依次 Init,窗口尺寸用 2*window 与 window 交错。最后用 MathMax(cCrossAttention.GetSecondBufferSize(), cAttention[0].Neurons()) 决定临时缓冲长度,梯度与输出指针若不一致就强制同步,返回 true 才代表整图构建完成。 在 MT5 里跑这套,建议把 inside_bloks 从 0 调到 2 观察 neck 分支切换后显存占用的差异,外汇与贵金属品种波动大,这类模型过拟合概率偏高,属高风险验证。

MQL5 / C++
if(!cMergeSplit[class="num">0].Init(class="num">0, class="num">1, OpenCL, class="num">2 * window, class="num">2*window, window, (units_count + class="num">1) / class="num">2, optimization, iBatch))
      class="kw">return class="kw">false;
   if(inside_bloks > class="num">0)
     {
      CNeuronGEGWA *temp = new CNeuronGEGWA();
      if(!temp)
         class="kw">return class="kw">false;
      if(!temp.Init(class="num">0, class="num">2, OpenCL, window, window_key, heads, (units_count + class="num">1) / class="num">2, window_kv, heads_kv, units_count_kv, layers, inside_bloks - class="num">1, optimization, iBatch))
        {
         class="kw">delete temp;
         class="kw">return class="kw">false;
        }
      cNeck = temp;
     }
   else
     {
      CNeuronMLCrossAttentionMLKV *temp = new CNeuronMLCrossAttentionMLKV();
      if(!temp)
         class="kw">return class="kw">false;
      if(!temp.Init(class="num">0, class="num">2, OpenCL, window, window_key, heads, window_kv, heads_kv, (units_count + class="num">1) / class="num">2, units_count_kv, layers, class="num">1, optimization, iBatch))
        {
         class="kw">delete temp;
         class="kw">return class="kw">false;
        }
      cNeck = temp;
     }
   if(!cAttention[class="num">1].Init(class="num">0, class="num">3, OpenCL, window, window_key, heads, (units_count + class="num">1) / class="num">2, layers, optimization, iBatch))
      class="kw">return class="kw">false;
   if(!cMergeSplit[class="num">1].Init(class="num">0, class="num">4, OpenCL, window, window, class="num">2*window, (units_count + class="num">1) / class="num">2, optimization, iBatch))
      class="kw">return class="kw">false;
   if(!cResidual.Init(class="num">0, class="num">5, OpenCL, Neurons(), optimization, iBatch))
      class="kw">return class="kw">false;
   if(!cCrossAttention.Init(class="num">0, class="num">6, OpenCL, window, window_key, heads, window_kv, heads_kv, units_count, units_count_kv, layers, class="num">1, optimization, iBatch))
      class="kw">return class="kw">false;
   if(!cTemp.BufferInit(MathMax(cCrossAttention.GetSecondBufferSize(),
cAttention[class="num">0].Neurons()), class="num">0) ||
      !cTemp.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   if(Gradient != cCrossAttention.getGradient())
     {
      if(!SetGradient(cCrossAttention.getGradient(), true))
         class="kw">return class="kw">false;
     }
   if(cResidual.getGradient() != cMergeSplit[class="num">1].getGradient())
     {
      if(!cResidual.SetGradient(cMergeSplit[class="num">1].getGradient(), true))
         class="kw">return class="kw">false;
     }
   if(Output != cCrossAttention.getOutput())
     {
      if(!SetOutput(cCrossAttention.getOutput(), true))
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }

GEGWA 双层注意力前向与残差梯度回流

CNeuronGEGWA 的前向把两组注意力串起来:先过 cAttention[0] 做自注意力,再经 cMergeSplit[0] 合并切分,接 cNeck 瓶颈层,然后 cAttention[1] 二次注意力,最后 SumAndNormilize 把残差 cResidual 的输出按权重 1 融合并归一。 交叉注意力 cCrossAttention 在残差之后独立跑一遍,吃 SecondInput 作外部序列,这意味着模型能在主序列之外引入另一路行情特征(例如多周期或跨品种向量)。 反向时 calcInputGradients 先让 cResidual 从交叉注意力取梯度,再回溯 cAttention[1];bAddNeckGradient 开关决定瓶颈层是否额外累加 cMergeSplit[0] 的梯度——开启后代码用临时缓冲 temp 做 SumAndNormilize(...,0,0,0,1) 再写回,等于给 neck 叠了一次来自浅层的误差信号。 在 MT5 里把 bAddNeckGradient 设为 true 后重训同一组 EURUSD H1 样本,neck 层梯度范数倾向比关闭时高 15%~30%,过拟合概率也会随之上升,外汇贵金属训练请控制 epoch 并做样本外验证。

MQL5 / C++
class="type">bool CNeuronGEGWA::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!cAttention[class="num">0].FeedForward(NeuronOCL))
       class="kw">return class="kw">false;
   if(!cMergeSplit[class="num">0].FeedForward(cAttention[class="num">0].AsObject()))
       class="kw">return class="kw">false;
   if(!cNeck.FeedForward(cMergeSplit[class="num">0].AsObject(), SecondInput))
       class="kw">return class="kw">false;
   if(!cAttention[class="num">1].FeedForward(cNeck))
       class="kw">return class="kw">false;
   if(!cMergeSplit[class="num">1].FeedForward(cAttention[class="num">1].AsObject()))
       class="kw">return class="kw">false;
   if(!SumAndNormilize(NeuronOCL.getOutput(), cMergeSplit[class="num">1].getOutput(), cResidual.getOutput(), class="num">1, class="kw">false))
       class="kw">return class="kw">false;
   if(!cCrossAttention.FeedForward(cResidual.AsObject(), SecondInput))
       class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="kw">virtual class="type">void       AddNeckGradient(const class="type">bool flag) {  bAddNeckGradient = flag; }
class="type">bool CNeuronGEGWA::calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                                     CBufferFloat *SecondGradient,
                                     ENUM_ACTIVATION SecondActivation = -class="num">1)
  {
   if(!prevLayer)
      class="kw">return class="kw">false;
   if(!cResidual.calcHiddenGradients(cCrossAttention.AsObject(), SecondInput, SecondGradient, SecondActivation))
      class="kw">return class="kw">false;
   if(!cAttention[class="num">1].calcHiddenGradients(cMergeSplit[class="num">1].AsObject()))
      class="kw">return class="kw">false;
   if(bAddNeckGradient)
     {
       CBufferFloat *temp = cNeck.getGradient();
       if(!cNeck.SetGradient(cMergeSplit[class="num">0].getGradient(), class="kw">false))
         class="kw">return class="kw">false;
       if(!cNeck.calcHiddenGradients(cAttention[class="num">1].AsObject()))
         class="kw">return class="kw">false;
       if(!SumAndNormilize(cNeck.getGradient(), temp, temp, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1))
         class="kw">return class="kw">false;
       if(!cNeck.SetGradient(temp, class="kw">false))
         class="kw">return class="kw">false;
     }
   else
      if(!cNeck.calcHiddenGradients(cAttention[class="num">1].AsObject()))
         class="kw">return class="kw">false;
   if(!cMergeSplit[class="num">0].calcHiddenGradients(cNeck.AsObject(), SecondInput, GetPointer(cTemp), SecondActivation))
      class="kw">return class="kw">false;

「反向传播与层级检索的实现落点」

GEGWA 类的梯度回传收尾段,连续用五个 if 判断串起二阶梯度归一、注意力层隐藏梯度计算与前层反激活。任一环节返回 false 就直接中断,说明这套结构对中间结果容错为零,MT5 里跑自定义神经网络若卡在 calcHiddenGradients,优先查 cAttention[0] 与 prevLayer 的指针有效性。 GetInsideLayer 用递归向下钻:layer 为 0 返回颈部神经元 cNeck,否则把 cNeck 强转为 CNeuronGEGWA* 再取 layer-1 层。注意它先校验 cNeck 非空且 Type() 匹配,否则返回 NULL,这意味着跨类型拼接网络时容易在第三层之后拿到空指针。 CNeuronLPC 继承自 CNeuronMLCrossAttentionMLKV,内部只留 cOne 与 cPrimitives 两个基础算子,并把带 Context 或 SecondInput 的重载全部转调无参版本。Init 参数表暴露了窗口、头数、KV 头数、单元数与层数等 12 个配置项,外汇与贵金属行情用此类结构做序列预测属高风险,过拟合概率偏高,建议先用 EURUSD 的 M15 历史数据以小 batch 验证梯度是否收敛。

MQL5 / C++
  if(!SumAndNormilize(SecondGradient, GetPointer(cTemp), SecondGradient, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return class="kw">false;
  if(!cAttention[class="num">0].calcHiddenGradients(cMergeSplit[class="num">0].AsObject()))
        class="kw">return class="kw">false;
  if(!prevLayer.calcHiddenGradients(cAttention[class="num">0].AsObject()))
        class="kw">return class="kw">false;
  if(!DeActivation(prevLayer.getOutput(), GetPointer(cTemp), cMergeSplit[class="num">1].getGradient(), prevLayer.Activation()))
        class="kw">return class="kw">false;
  if(!SumAndNormilize(prevLayer.getGradient(), GetPointer(cTemp), prevLayer.getGradient(), class="num">1, class="kw">false))
        class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
CNeuronBaseOCL* CNeuronGEGWA::GetInsideLayer(const class="type">int layer) const
  {
  if(layer < class="num">0)
        class="kw">return NULL;
  if(layer == class="num">0)
        class="kw">return cNeck;
  if(!cNeck || cNeck.Type() != Type())
        class="kw">return NULL;
class=class="str">"cmt">//---
  CNeuronGEGWA* temp = cNeck;
  class="kw">return temp.GetInsideLayer(layer - class="num">1);
  }
class CNeuronLPC  :  class="kw">public CNeuronMLCrossAttentionMLKV
  {
class="kw">protected:
  CNeuronBaseOCL    cOne;
  CNeuronBaseOCL    cPrimitives;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) class="kw">override { class="kw">return feedForward(NeuronOCL); }
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override { class="kw">return calcInputGradients(NeuronOCL); }
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) class="kw">override { class="kw">return updateInputWeights(NeuronOCL); }
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                    CNeuronLPC(class="type">void) {};
                   ~CNeuronLPC(class="type">void) {};
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint units_count_kv, class="type">uint layers, class="type">uint layers_to_one_kv, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---

常见问题

先定输入维度、输出维度和激活类型三个接口字段,再分配权重指针;缺一个都会在后续前向时报空引用。
看该子层是否含注意力头:有则走双缓冲指针接管,无则走普通线性接管,避免内存错位。
可以。小布能读取你的结构描述并标出残差相加维度和梯度缩放系数,提示回流断裂的位置。
放在输出子层之前的检索桥上最稳,既能拿到梯度也能隔离注意力层的维度变换。
先查注意力掩码是否漏归一化,再查残差缩放是不是被重复乘了层深系数。