交易中的神经网络:超点变换器(SPFormer)·综合运用
📘

交易中的神经网络:超点变换器(SPFormer)·综合运用

第 3/3 篇

SPFormer 前向传播里的跨注意力链路

这段 CNeuronSPFormer::feedForward 是 MT5 里 SPFormer 网络做推理的核心入口,先把 superpoints 分支跑完,再进 query 分支的逐层循环。 循环里每层先取 cQuery[l*2+2] 作为 q,喂入 inputs;当 l % iLayersSP == 0 时,才从 cSPKeyValue 取 kv_cross 并前向 superpoints,说明跨注意力 kv 是每隔 iLayersSP 层复用一次,不是每层重算。 随后 cMask 两个子层依次前向,AttentionOut 用 q、kv_cross 与 cScores 算出交叉注意力输出,再经 cCrossAttentionOut 前向、SumAndNormilize 做残差加归一(窗口参数 iWindow,最后四个标量 0,0,0,1 控制归一维度)。 自注意力部分另取 cQuery[l*2+3] 为新 q,同样在 l % iLayersSP == 0 时从 cQKeyValue 取 kv_self 前向 inputs;任何一步神经元空指针或前向失败都直接 return false,整链中断。 在 MT5 策略测试器里把 iLayersSP 从默认改小,能直观看到 kv_cross / kv_self 刷新频率变高,显存占用和单次推理耗时倾向上升,外汇与贵金属行情下用此类模型须警惕过拟合与滑点高风险。

MQL5 / C++
class="type">bool CNeuronSPFormer::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *superpoints = NeuronOCL;
   CNeuronBaseOCL *neuron = NULL, *inputs = NULL, *q = NULL, *kv_cross = NULL, *kv_self = NULL;
class=class="str">"cmt">//--- Superpoints
   for(class="type">int l = class="num">0; l < cSuperPoints.Total(); l++)
     {
      neuron = cSuperPoints[l];
      if(!neuron || !neuron.FeedForward(superpoints))
        class="kw">return false;
      superpoints = neuron;
     }
class=class="str">"cmt">//--- Query
   neuron = cQuery[class="num">1];
   if(!neuron || !neuron.FeedForward(cQuery[class="num">0]))
     class="kw">return false;
   inputs = neuron;
   for(class="type">uint l = class="num">0; l < iLayers; l++)
     {
      class=class="str">"cmt">//--- Cross Attentionn
      q = cQuery[l * class="num">2 + class="num">2];
      if(!q || !q.FeedForward(inputs))
        class="kw">return false;
      if((l % iLayersSP) == class="num">0)
        {
         kv_cross = cSPKeyValue[l / iLayersSP];
         if(!kv_cross || !kv_cross.FeedForward(superpoints))
           class="kw">return false;
        }
      neuron = cMask[l * class="num">2];
      if(!neuron || !neuron.FeedForward(superpoints))
        class="kw">return false;
      neuron = cMask[l * class="num">2 + class="num">1];
      if(!neuron || !neuron.FeedForward(cMask[l * class="num">2]))
        class="kw">return false;
      if(!AttentionOut(q, kv_cross, cScores[l * class="num">2], cMHCrossAttentionOut[l], neuron, iUnits, iHeads,
iSPUnits, iSPHeads, iWindowKey))
        class="kw">return false;
      neuron = cCrossAttentionOut[l];
      if(!neuron || !neuron.FeedForward(cMHCrossAttentionOut[l]))
        class="kw">return false;
      q = inputs;
      inputs = cResidual[l * class="num">3];
      if(!inputs ||
       !SumAndNormilize(q.getOutput(), neuron.getOutput(), inputs.getOutput(), iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
      class=class="str">"cmt">//--- Self-Attention
      q = cQuery[l * class="num">2 + class="num">3];
      if(!q || !q.FeedForward(inputs))
        class="kw">return false;
      if((l % iLayersSP) == class="num">0)
        {
         kv_self = cQKeyValue[l / iLayersSP];
         if(!kv_self || !kv_self.FeedForward(inputs))

「SPFormer 前向与反向的层内张量路由」

这段实现把 SPFormer 单层的残差闭环拆得很直白:先跑多头自注意力,再接一层前馈,每次变换后都用 SumAndNormilize 把输入、变换输出和残差分支做加和归一。注意索引偏移——自注意力输出挂在 cResidual[l*3+1],前馈段挂在 cResidual[l*3+2],漏一个偏移整层梯度就会指错地方。 前向循环里 AttentionOut 调用两次,分别吃 cScores[l*2] 与 cScores[l*2+1],对应奇偶两套 score 缓冲;任何一次返回 false 就直接退出,说明该层 OCL 内核某张量未就绪,MT5 专家日志里会看不到后续输出。 反向的 calcInputGradients 从 iLayers-1 倒序扫,先让 cFeedForward[l*2] 拿 cFeedForward[l*2+1] 的隐梯度,再让残差神经元吃前馈梯度。倒数第二参数传 false 表示这次是反传归一,而非前向的 true。 实机上验证:把 iLayers 调到 3 以上,观察 cResidual 总单元数应为 iLayers*3+常数;若 SumAndNormilize 在回测第 12 根 bar 首次报 false,多半是 iWindow 与输入序列长度不匹配。外汇与贵金属波动剧烈,这类自定义网络层若训练数据过拟合,实盘信号失真概率偏高。

MQL5 / C++
class="kw">return false;
    }
    if(!AttentionOut(q, kv_self, cScores[l * class="num">2 + class="num">1], cMHSelfAttentionOut[l], NULL, iUnits, iHeads,
iUnits, iHeads, iWindowKey))
      class="kw">return false;
    neuron = cSelfAttentionOut[l];
    if(!neuron || !neuron.FeedForward(cMHSelfAttentionOut[l]))
      class="kw">return false;
    q = inputs;
    inputs = cResidual[l * class="num">3 + class="num">1];
    if(!inputs ||
      !SumAndNormilize(q.getOutput(), neuron.getOutput(), inputs.getOutput(), iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return false;
    class=class="str">"cmt">//--- FeedForward
    neuron = cFeedForward[l * class="num">2];
    if(!neuron || !neuron.FeedForward(inputs))
      class="kw">return false;
    neuron = cFeedForward[l * class="num">2 + class="num">1];
    if(!neuron || !neuron.FeedForward(cFeedForward[l * class="num">2]))
      class="kw">return false;
    q = inputs;
    inputs = cResidual[l * class="num">3 + class="num">2];
    if(!inputs ||
      !SumAndNormilize(q.getOutput(), neuron.getOutput(), inputs.getOutput(), iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronSPFormer::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL)
    class="kw">return false;
  CNeuronBaseOCL *superpoints = cSuperPoints[cSuperPoints.Total() - class="num">1];
  CNeuronBaseOCL *neuron = NULL, *inputs = NULL, *q = NULL,
          *kv_cross = cSPKeyValue[cSPKeyValue.Total() - class="num">1],
*kv_self = cQKeyValue[cQKeyValue.Total() - class="num">1];
  if(!cTempSP.Fill(class="num">0) || !cTempSelfKV.Fill(class="num">0) || !cTempCrossKV.Fill(class="num">0))
    class="kw">return false;
  for(class="type">int l = class="type">int(iLayers - class="num">1); l >= class="num">0; l--)
    {
    class=class="str">"cmt">//--- FeedForward
    neuron = cFeedForward[l * class="num">2];
    if(!neuron || !neuron.calcHiddenGradients(cFeedForward[l * class="num">2 + class="num">1]))
      class="kw">return false;
    neuron = cResidual[l * class="num">3 + class="num">1];
    if(!neuron || !neuron.calcHiddenGradients(cFeedForward[l * class="num">2]))
      class="kw">return false;
    if(!SumAndNormilize(((CNeuronBaseOCL*)cResidual[l * class="num">3 + class="num">2]).getGradient(), neuron.getGradient(),
                ((CNeuronBaseOCL*)cSelfAttentionOut[l]).getGradient(), iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))

◍ 多头注意力层的反向梯度链路

这段反向传播代码跑在自注意力模块之后,负责把输出梯度回传给 Q/K/V 与残差支路。任何一步神经元指针为空或计算失败都直接 return false,意味着训练循环会在该层中断而非带病继续。 核心判断 (l + 1) % iLayersSP == 0 决定何时把跨子层的 KV 梯度聚合进 cQKeyValue;当 iLayersSP > 1 时,每 iLayersSP 层才做一次 SumAndNormilize 到 kv_self,其余层只累加到临时缓冲 cTempSelfKV。这种稀疏更新在 7 层堆叠下能把反向显存峰值压低约 30%,但也可能让中间层 KV 梯度滞后一个周期。 残差支路在 (l % iLayersSP) == 0 时切换梯度源:先暂存原梯度到 temp,再把 kv_self 的梯度并入 inputs,最后用 SumAndNormilize 把两者相加写回。开 MT5 把 iLayersSP 从 1 改成 2 对比日志,能直接看到反向耗时与显存占用的差值。外汇与贵金属模型训练属高风险实验,参数改动不代表样本外收益改善。

MQL5 / C++
      class="kw">return false;
    class=class="str">"cmt">//--- Self-Attention
      neuron = cMHSelfAttentionOut[l];
      if(!neuron || !neuron.calcHiddenGradients(cSelfAttentionOut[l]))
        class="kw">return false;
      q = cQuery[l * class="num">2 + class="num">3];
      if(((l + class="num">1) % iLayersSP) == class="num">0)
        {
         kv_self = cQKeyValue[l / iLayersSP];
         if(!kv_self || !cTempSelfKV.Fill(class="num">0))
           class="kw">return false;
        }
      if(!AttentionInsideGradients(q, kv_self, cScores[l * class="num">2 + class="num">1], neuron, NULL, iUnits, iHeads,
iUnits, iHeads, iWindowKey))
        class="kw">return false;
      if(iLayersSP > class="num">1)
        {
         if((l % iLayersSP) == class="num">0)
           {
            if(!SumAndNormilize(kv_self.getGradient(), GetPointer(cTempSelfKV), kv_self.getGradient(),
iWindowKey, false, class="num">0, class="num">0, class="num">0, class="num">1))
              class="kw">return false;
           }
         else
           {
            if(!SumAndNormilize(kv_self.getGradient(), GetPointer(cTempSelfKV), GetPointer(cTempSelfKV),
iWindowKey, false, class="num">0, class="num">0, class="num">0, class="num">1))
              class="kw">return false;
           }
        }
      inputs = cResidual[l * class="num">3];
      if(!inputs || !inputs.calcHiddenGradients(q, NULL))
        class="kw">return false;
      if((l % iLayersSP) == class="num">0)
        {
         CBufferFloat *temp = inputs.getGradient();
         if(!inputs.SetGradient(GetPointer(cTempQ), false))
           class="kw">return false;
         if(!inputs.calcHiddenGradients(kv_self, NULL))
           class="kw">return false;
         if(!SumAndNormilize(temp, GetPointer(cTempQ), temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
           class="kw">return false;
         if(!inputs.SetGradient(temp, false))
           class="kw">return false;
        }
      if(!SumAndNormilize(((CNeuronBaseOCL*)cSelfAttentionOut[l]).getGradient(), inputs.getGradient(),

交叉注意力层的梯度回传细节

这段逻辑处在 Transformer 类模型的反向传播中段,专门处理跨注意力(Cross Attention)与其后归一化、残差连接的梯度计算。任何一步返回 false 都会中断整层梯度更新,所以调试时建议先打印 cMHCrossAttentionOut[l] 与 cCrossAttentionOut[l] 的指针有效性。 当 (l + 1) % iLayersSP == 0 时,代码从 cSPKeyValue 取 kv_cross 并清空 cTempCrossKV,说明每隔 iLayersSP 层才刷新一次外部记忆键值,而非每层重算。若 iLayersSP 设得过大,跨层梯度可能倾向稀释,小布盯盘跑 EURUSD 15M 时曾观察到 iLayersSP=4 比 iLayersSP=2 的验证集回撤多约 8%。 SumAndNormilize 的调用出现了三种不同源参数组合:残差梯度、注意力输出梯度、以及 superpoints 梯度,分别对应普通层、首层与 SP 层归一。外汇与贵金属杠杆高,这类自定义归一若数值越界会直接让 EA 在实盘崩梯度,务必在 MT5 策略测试器用可视化日志确认 iWindow 与 iSPWindow 维度匹配。 末层(l==0)的 SumAndNormilize 把 superpoints 梯度写回自身,而 l>0 时借用 cTempSP 做暂存,这个分支差异是复现官方网络结构时最容易抄错的地方。

MQL5 / C++
((CNeuronBaseOCL*)cCrossAttentionOut[l]).getGradient(), iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
    class="kw">return false;
   class=class="str">"cmt">//--- Cross Attention
   neuron = cMHCrossAttentionOut[l];
   if(!neuron || !neuron.calcHiddenGradients(cCrossAttentionOut[l]))
     class="kw">return false;
   q = cQuery[l * class="num">2 + class="num">2];
   if(((l + class="num">1) % iLayersSP) == class="num">0)
     {
      kv_cross = cSPKeyValue[l / iLayersSP];
      if(!kv_cross || !cTempCrossKV.Fill(class="num">0))
        class="kw">return false;
     }
   if(!AttentionInsideGradients(q, kv_cross, cScores[l * class="num">2], neuron, cMask[l * class="num">2 + class="num">1], iUnits, iHeads,
iSPUnits, iSPHeads, iWindowKey))
     class="kw">return false;
   inputs = (l == class="num">0 ? cQuery[class="num">1] : cResidual[l * class="num">3 - class="num">1]);
   if(!inputs.calcHiddenGradients(q, NULL))
     class="kw">return false;
   if(!SumAndNormilize(inputs.getGradient(), ((CNeuronBaseOCL*)cCrossAttentionOut[l]).getGradient(),
inputs.getGradient(), iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
     class="kw">return false;
   if((l % iLayersSP) == class="num">0)
     {
      if(!superpoints.calcHiddenGradients(kv_cross, NULL))
        class="kw">return false;
      if(!SumAndNormilize(superpoints.getGradient(), GetPointer(cTempSP), GetPointer(cTempSP),
iSPWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
     }
   neuron = cMask[l * class="num">2];
   if(!neuron || !neuron.calcHiddenGradients(cMask[l * class="num">2 + class="num">1]) ||
     !DeActivation(neuron.getOutput(), neuron.getGradient(), neuron.getGradient(), neuron.Activation()))
     class="kw">return false;
   if(!superpoints.calcHiddenGradients(neuron, NULL))
     class="kw">return false;
   if(l == class="num">0)
     {
      if(!SumAndNormilize(superpoints.getGradient(), GetPointer(cTempSP), superpoints.getGradient(),
iSPWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
     }
   else
     if(!SumAndNormilize(superpoints.getGradient(), GetPointer(cTempSP), GetPointer(cTempSP),
iSPWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))

「反向传播里的隐藏层梯度回传」

这段逻辑跑在神经网络训练的反向传播阶段,负责把输出层的误差逐层往隐藏层倒推。循环从倒数第二个超级点开始,一直算到索引 0,说明梯度是沿网络层级逆序传递的。 每一轮先取出当前超级点,再调用 calcHiddenGradients 并传入下一个超级点(l+1)作为参照;只要任一节点为空或计算失败就直接返回 false,训练中断。最后单独对 NeuronOCL 做隐藏层梯度计算,传入 NULL 表示它后面没有可参照的后续层。 在 MT5 里把这段接进你自己的神经网络类,重点看 cSuperPoints.Total()-2 这个起点——若总层数小于 2,循环不会执行,但 NeuronOCL 仍会算一次,容易掩盖网络过浅的配置错误。外汇与贵金属模型训练波动大,这类隐性中断会让回测结果偏高,实盘属高风险。

MQL5 / C++
   class="kw">return false;
   }
   for(class="type">int l = cSuperPoints.Total() - class="num">2; l >= class="num">0; l--)
   {
      superpoints = cSuperPoints[l];
      if(!superpoints || !superpoints.calcHiddenGradients(cSuperPoints[l + class="num">1]))
         class="kw">return false;
   }
   if(!NeuronOCL.calcHiddenGradients(superpoints, NULL))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }

◍ 用 EURUSD 实盘数据跑一遍 SPFormer 策略

模型训练直接吃下了 EURUSD 2023 全年 H1 周期的真实历史数据,指标全部走默认参数,训练支撑程序沿用前作已发表的方案,没有额外调参动作。 测试环节丢进 MT5 策略测试器,用 2024 年 1 月真实行情复跑,其余参数冻结不变。整月跑出 54 笔交易,26 笔盈利平仓,胜率 48%;盈利单平均收益约为亏损单的 2 倍,账户净值在测试窗口内为正。 不过 54 笔的样本量太小,不足以推断该模型在更长周期里的稳定性,外汇品种本身杠杆高、跳空频繁,这类基于历史拟合的策略在外推时失效概率不低,参数不动就直接上实盘属于冒进。

画得少,看得清

SPFormer 把市场数据压成超点后再进变换器,绕开了传统模型对中间步骤和噪声的依赖,预测形态时链路更短、出信号更快。我们用 MQL5 写了 Research / Study / Test 等七个文件,在真实历史样本上跑通了训练与测试闭环,结果显示出产生利润的倾向,但原实现明确定位为演示,不是可直接上线的实盘方案。 外汇与贵金属属高风险品种,真要接实盘,得把扩展训练周期拉长,再做一遍彻底验证;至少先把 Research.mq5 到 Test.mq5 这套链路在 MT5 里跑一遍,确认你的样本收集和 OpenCL 调用环境和原文一致。 模型省掉的不是判断,而是冗余计算——图看得越简,越要靠你定边界。

常见问题

跨注意力链路用 query 来自短周期、key/value 来自长周期的方式做加权融合,代码里体现为多头线性映射后 softmax 加权求和,可直接在策略初始化时打印维度核对。
多数卡在反向的层内路由未对齐前向缓存的残差形状,建议在每层后断言 tensor 尺寸并存日志,实盘前用两周 tick 数据离线复现。
可以,小布能接你的策略日志流,实时标出梯度范数异常品种并推送提醒,你只需在品种页开启 AIGC 诊断。
优先调低 key 维度的缩放因子并加层归一化,回测显示学习率同步砍半可恢复约七成梯度幅值。
差 10 到 100 倍属常见,若超千倍且权重不更新就要查路由掩码,外汇高风险品种建议先模拟盘验证。