神经网络变得简单(第 96 部分):多尺度特征提取(MSFformer)·综合运用
📘

神经网络变得简单(第 96 部分):多尺度特征提取(MSFformer)·综合运用

第 3/3 篇

◍ 多头注意力层的显存与对象初始化

在 MT5 用 OpenCL 跑自定义神经网络时,多头注意力模块的第一道坎是容器和显存缓冲的预清理。代码片段开头连续调用 caScore、caAttentionOut、caW0 的 Clear(),目的是避免上一次前向传播残留的句柄污染本轮训练,尤其在你用同一 EA 反复调参时容易忽略。 循环按 iPAMLayers 层数堆叠子结构:每层先 new 一个 CNeuronS3 做位置编码类处理,Init 参数里 iWindowIn 和 iCount 决定输入窗口与样本数,随后立即 SetActivationFunction(None)——说明该层不做非线性变换,只做线性投影。 Query 和 KV 都用 CNeuronConvOCL 实现,但 KV 通道数是 2*iWindowKey*iHeadsKV,Query 是 iWindowKey*iHeads,差了一倍的卷积核宽度。Score 缓冲通过 OpenCL.AddBuffer 直接开 sizeof(float)*iCount*iCount*iHeads 字节的 CL_MEM_READ_WRITE 显存,若返回负索引则直接 return false,这是显存不足或上下文失效的最直接信号。 注意 caAttentionOut.Add(conv) 这里有个隐蔽笔误:前面 new 的是 base(CNeuronBaseOCL),却把 conv 塞进容器,而 base 仅 delete 未 Add。你在抄这段代码到自己的指标里时,大概率会触发野指针或层缺失,建议改成 caAttentionOut.Add(base) 再验证。外汇与贵金属行情下用此类 GPU 网络做推理,高杠杆与滑点风险并存,任何层初始化失败都应视为策略不可用的硬信号。

MQL5 / C++
  caScore.Clear();
  caAttentionOut.Clear();
  caW0.Clear();
  CNeuronBaseOCL *base = NULL;
  CNeuronConvOCL *conv = NULL;
  CNeuronS3     *s3 = NULL;
  for(class="type">uint l = class="num">0; l < iPAMLayers; l++)
    {
    class=class="str">"cmt">//--- S3
    s3 = new CNeuronS3();
    if(!s3)
      class="kw">return false;
    if(!s3.Init(class="num">0, l, OpenCL, iWindowIn, iCount, optimization, iBatch) ||
       !caS3.Add(s3))
      class="kw">return false;
    s3.SetActivationFunction(None);
    class=class="str">"cmt">//--- Query
    conv = new CNeuronConvOCL();
    if(!conv)
      class="kw">return false;
    if(!conv.Init(class="num">0, class="num">0, OpenCL, iWindowIn, iWindowIn, iWindowKey*iHeads, iCount, optimization, iBatch) ||
       !caQuery.Add(conv))
      {
      class="kw">delete conv;
      class="kw">return false;
      }
    conv.SetActivationFunction(None);
    class=class="str">"cmt">//--- KV
    conv = new CNeuronConvOCL();
    if(!conv)
      class="kw">return false;
    if(!conv.Init(class="num">0, class="num">0, OpenCL, iWindowIn, iWindowIn, class="num">2*iWindowKey*iHeadsKV, iCount, optimization, iBatch) ||
       !caKV.Add(conv))
      {
      class="kw">delete conv;
      class="kw">return false;
      }
    conv.SetActivationFunction(None);
    class=class="str">"cmt">//--- Score
    class="type">int temp = OpenCL.AddBuffer(class="kw">sizeof(class="type">float) * iCount * iCount * iHeads, CL_MEM_READ_WRITE);
    if(temp < class="num">0)
      class="kw">return false;
    if(!caScore.Add(temp))
      class="kw">return false;
    class=class="str">"cmt">//--- MH Attention Out
    base = new CNeuronBaseOCL();
    if(!base)
      class="kw">return false;
    if(!base.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iHeadsKV * iCount, optimization, iBatch) ||
       !caAttentionOut.Add(conv))
      {
      class="kw">delete base;
      class="kw">return false;
      }
    base.SetActivationFunction(None);
    class=class="str">"cmt">//--- W0
    conv = new CNeuronConvOCL();
    if(!conv)
      class="kw">return false;
    if(!conv.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iHeadsKV, iWindowKey * iHeadsKV, iWindowIn, iCount,
optimization, iBatch) ||
       !caW0.Add(conv))
      {

「残差连接与多头注意力输出的底层拼装」

这段初始化逻辑里,残差分支用 CNeuronBaseOCL 接在卷积层后面,输入维度直接拉到 iWindowIn * iCount,激活函数设成 None,意味着它只做线性映射、不引入非线性扭曲。若 base 指针分配失败或 Init 不通过,立刻 delete 并返回 false,避免悬空对象拖垮后续前向。 前馈部分 cFF1 隐藏层扩到 4 * iWindowIn 再经 cFF2 压缩回 iWindowIn,cFF1 用 LReLU、cFF2 用 None,梯度指针从 cFF2.getGradient() 接出。这种「宽—窄」对称结构在行情特征提取时,可能更利于保留局部突变。 下方 OpenCL 核 MH2PyrAttentionOut 用三维全局 ID 定位 q/k/h,heads_kv 与 heads 不等时靠 h % heads_kv 做映射;score 偏移按 kunits * (q_id * heads + h) + k 计算,显存布局直接决定并行效率。外汇与贵金属波动受杠杆放大,跑这类 GPU 核前务必在小样本上验证维度匹配,否则可能爆显存或静默算错。

MQL5 / C++
  class="kw">delete conv;
  class="kw">return false;
  }
  conv.SetActivationFunction(None);
 }
class=class="str">"cmt">//--- Residual
  base = new CNeuronBaseOCL();
  if(!base)
   class="kw">return false;
  if(!base.Init(class="num">0, class="num">0, OpenCL, iWindowIn * iCount, optimization, iBatch) ||
     !caW0.Add(conv))
   {
    class="kw">delete base;
    class="kw">return false;
   }
  base.SetActivationFunction(None);
class=class="str">"cmt">//--- FeedForward
  if(!cFF1.Init(class="num">0, class="num">0, OpenCL, iWindowIn, iWindowIn, class="num">4 * iWindowIn, iCount, optimization, iBatch))
   class="kw">return false;
  cFF1.SetActivationFunction(LReLU);
  if(!cFF2.Init(class="num">0, class="num">0, OpenCL, class="num">4 * iWindowIn, class="num">4 * iWindowIn, iWindowIn, iCount, optimization, iBatch))
   class="kw">return false;
  cFF2.SetActivationFunction(None);
  if(!SetGradient(cFF2.getGradient()))
   class="kw">return false;
  SetActivationFunction(None);
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void MH2PyrAttentionOut(__global class="type">float *q,
                                 __global class="type">float *kv,
                                 __global class="type">float *score,
                                 __global class="type">float *out,
                                 const class="type">int dimension,
                                 const class="type">int heads_kv,
                                 const class="type">int window
                                 )
  {
class=class="str">"cmt">//--- init
  const class="type">int q_id = get_global_id(class="num">0);
  const class="type">int k = get_global_id(class="num">1);
  const class="type">int h = get_global_id(class="num">2);
  const class="type">int qunits = get_global_size(class="num">0);
  const class="type">int kunits = get_global_size(class="num">1);
  const class="type">int heads = get_global_size(class="num">2);
  const class="type">int h_kv = h % heads_kv;
  const class="type">int shift_q = dimension * (q_id * heads + h);
  const class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv);
  const class="type">int shift_v = dimension * (class="num">2 * heads_kv * k + heads_kv + h_kv);
  const class="type">int shift_s = kunits * (q_id * heads + h) + k;

正文

&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">uint</span> ls = min((<span class="keyword">uint</span>)get_local_size(<span class="number">1</span>), (<span class="keyword">uint</span>)LOCAL_ARRAY_SIZE); &nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">int</span> delta_win = (window + <span class="number">1</span>) / <span class="number">2</span>; &nbsp;&nbsp; <span class="keyword">float</span> koef = <span class="functions">sqrt</span>((<span class="keyword">float</span>)dimension); &nbsp;&nbsp; <span class="keyword">if</span>(koef &lt; <span class="number">1</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;koef = <span class="number">1</span>; &nbsp;&nbsp; __local <span class="keyword">float</span> temp[LOCAL_ARRAY_SIZE]; <span class="comment">//--- sum of exp</span> &nbsp;&nbsp; <span class="keyword">uint</span> count = <span class="number">0</span>; &nbsp;&nbsp; <span class="keyword">if</span>(k &lt; ls) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">do</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>((count * ls) &lt; (kunits - k)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">float</span> sum = <span class="number">0</span>; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</spa

◍ 稀疏金字塔注意力的内核归约与前向链路

上面这段 OpenCL 内核尾部在做局部内存的并行归约:先以 CLK_LOCAL_MEM_FENCE 屏障保证 temp[] 写透,再用 do-while 把 count 折半,把 temp[k+count] 累加进 temp[k] 并清零上半段,最终 out[shift_q+d]=temp[0] 拿到单层注意力输出。这种折半归约把长度为 ls 的局部求和从 O(ls) 降到 O(log2 ls),在 ls=64 时理论步数由 63 缩到 6。 feedForward 里按层循环走完 Mix→Query→KV→PAM→W0 五段:每层先取 caS3 做混合,再分别从 caQuery、caKV 抽 q 与 kv 缓冲,丢给 AttentionOut 算稀疏注意力,最后经 caW0 投影。任一环节 FeedForward 返回 false 就整体退出,说明该网络对前层对象空指针零容忍。 末层 caW0.At(iPAMLayers) 出来后接 SumAndNormilize,把残差输入 NeuronOCL.getOutput() 与当前 prev 做加和归一,iWindowIn 控制窗口、true 标记训练态。想在 MT5 验证,可直接在 Expert Advisor 里实例化 CNeuronSPyrAttentionOCL 并单步跟 caAttentionOut 的 out 缓冲,看 temp[0] 是否随 ls 折半稳定收敛。外汇与贵金属行情下用此类模型信号属于高风险,实盘前务必用历史 tick 回测。

MQL5 / C++
sum = kv[shift_v + d] * (count == class="num">0 ? sc : score[shift_s + count * ls]);
if(isnan(sum))
   sum = class="num">0;
}
temp[k] = (count > class="num">0 ? temp[k] : class="num">0) + sum;
}
count++;
}
while((count * ls + k) < kunits);
barrier(CLK_LOCAL_MEM_FENCE);
class=class="str">"cmt">//---
count = min(ls, (class="type">uint)kunits);
do
  {
   count = (count + class="num">1) / class="num">2;
   if(k < ls)
      temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : class="num">0);
   if(k + count < ls)
      temp[k + count] = class="num">0;
   barrier(CLK_LOCAL_MEM_FENCE);
  }
while(count > class="num">1);
class=class="str">"cmt">//---
out[shift_q + d] = temp[class="num">0];
}
}
class="type">bool CNeuronSPyrAttentionOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *prev = NeuronOCL;
   CNeuronBaseOCL *current = NULL;
   CBufferFloat   *q = NULL;
   CBufferFloat   *kv = NULL;
   for(class="type">uint l = class="num">0; l < iPAMLayers; l++)
     {
      class=class="str">"cmt">//--- Mix
      current = caS3.At(l);
      if(!current ||
         !current.FeedForward(prev.AsObject())
        )
         class="kw">return false;
      prev = current;
      class=class="str">"cmt">//--- Query
      current = caQuery.At(l);
      if(!current ||
         !current.FeedForward(prev.AsObject())
        )
         class="kw">return false;
      q = current.getOutput();
      class=class="str">"cmt">//--- Key and Value
      current = caKV.At(l);
      if(!current ||
         !current.FeedForward(prev.AsObject())
        )
         class="kw">return false;
      kv = current.getOutput();
      class=class="str">"cmt">//--- PAM
      current = caAttentionOut.At(l);
      if(!current || !AttentionOut(q, kv, caScore.At(l), current.getOutput(), iPAMLayers - l))
         class="kw">return false;
      prev = current;
      class=class="str">"cmt">//--- W0
      current = caW0.At(l);
      if(!current ||
         !current.FeedForward(prev.AsObject())
        )
         class="kw">return false;
      prev = current;
     }
class=class="str">"cmt">//--- Residual
   current = caW0.At(iPAMLayers);
   if(!SumAndNormilize(NeuronOCL.getOutput(), prev.getOutput(), current.getOutput(), iWindowIn, true))

「残差连接下的梯度回传链路」

这段 CNeuronSPyrAttentionOCL::calcInputGradients 负责把输出端的梯度一层层反推到前层,金字塔式注意力里残差结构让回传路径不只一条。 先对 FeedForward 支路调 cFF1.calcHiddenGradients(cFF2.AsObject()) 和 current.calcHiddenGradients(cFF1.AsObject()),任一失败立即返回 false,保证前向两张网权重同步更新。 残差部分用 SumAndNormilize(getGradient(), next.getGradient(), current.getGradient(), iWindowIn, false) 把上层梯度和本层相加再归一,iWindowIn 控制窗口长度,false 表示反向不重写输出缓冲。 主循环从 l = iPAMLayers-1 降到 0:每轮先算 W0 的 AttentionOut 梯度,再进 AttentionInsideGradients 推 Query/Key/Value,其中 caScore.At(l) 存注意力分数;Query 支路额外 Concat 残差,Key/Value 支路用 SumAndNormilize 合并梯度。 循环外最后对 prevLayer 做 DeActivation 并把 residual 合进 prevLayer 梯度,若 iWindowIn 设错会导致归一长度偏移、梯度幅值异常,开 MT5 把 iWindowIn 打印出来比对前层输出维度即可验证。

MQL5 / C++
  class="kw">return false;
class=class="str">"cmt">//---FeedForward
   if(!cFF1.FeedForward(current.AsObject()) ||
       !cFF2.FeedForward(cFF1.AsObject())
    )
       class="kw">return false;
class=class="str">"cmt">//--- Residual
   if(!SumAndNormilize(current.getOutput(), cFF2.getOutput(), getOutput(), iWindowIn, true))
       class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronSPyrAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
   {
   if(!prevLayer)
       class="kw">return false;
   CNeuronBaseOCL *next = NULL;
   CNeuronBaseOCL *current = NULL;
   CNeuronBaseOCL *q = NULL;
   CNeuronBaseOCL *kv = NULL;
class=class="str">"cmt">//--- FeedForward
   current = caW0.At(iPAMLayers);
   if(!current ||
       !cFF1.calcHiddenGradients(cFF2.AsObject()) ||
       !current.calcHiddenGradients(cFF1.AsObject())
    )
       class="kw">return false;
   next = current;
class=class="str">"cmt">//--- Residual
   current = caW0.At(iPAMLayers - class="num">1);
   if(!SumAndNormilize(getGradient(), next.getGradient(), current.getGradient(), iWindowIn, false))
       class="kw">return false;
   CBufferFloat *residual = next.getGradient();
   next = current;
   for(class="type">int l = class="type">int(iPAMLayers - class="num">1); l >= class="num">0; l--)
     {
     class=class="str">"cmt">//--- W0
     current = caAttentionOut.At(l);
     if(!current ||
         !current.calcHiddenGradients(next.AsObject())
       )
        class="kw">return false;
     class=class="str">"cmt">//--- MH Attention
     q = caQuery.At(l);
     kv = caKV.At(l);
     if(!q || !kv ||
         !AttentionInsideGradients(q.getOutput(), q.getGradient(), kv.getOutput(), kv.getGradient(),
caScore.At(l), current.getGradient())
       )
        class="kw">return false;
     class=class="str">"cmt">//--- Query
     current = caS3.At(l);
     if(!current ||
         !current.calcHiddenGradients(q.AsObject()) ||
         !Concat(current.getGradient(), current.getGradient(), residual, iWindowIn,class="num">0, iCount)
       )
        class="kw">return false;
     class=class="str">"cmt">//--- Key and Value
     if(!current ||
         !current.calcHiddenGradients(kv.AsObject()) ||
         !SumAndNormilize(current.getGradient(), residual, current.getGradient(), iWindowIn, false)
       )
        class="kw">return false;
     next = current;
     class=class="str">"cmt">//--- S3
     current = (l == class="num">0 ? prevLayer : caW0.At(l - class="num">1));
     if(!current ||
         !current.calcHiddenGradients(next.AsObject())
       )
        class="kw">return false;
     next = current;
     }
   current = caW0.At(iPAMLayers - class="num">1);
   if(!DeActivation(prevLayer.getOutput(), current.getGradient(), residual, prevLayer.Activation()) ||
      !SumAndNormilize(prevLayer.getGradient(), residual, prevLayer.getGradient(), iWindowIn, false)

金字塔注意力层的权重回传链路

这段逻辑属于一个叫 CNeuronSPyrAttentionOCL 的类的 updateInputWeights 方法,职责是把上一层算出的梯度按金字塔注意力的结构逐层往前传。它遍历 iPAMLayers 层,每一层里依次处理 S3、Query、Key/Value、W0 四个子模块,任何一个子模块的 UpdateInputWeights 返回失败就整体返回 false。 具体来看,prev 指针初始指向传入的 NeuronOCL,先取 caS3 的第 l 层做权重更新,成功后才把 prev 换成 S3 的输出;接着 Query 层以 S3 输出为前层,Key/Value 层复用同一 prev,而 W0 层的前层被硬性指定为 caAttentionOut.At(l) 而非上一层的 KV 输出,这里和前面几层的串联方式不一样,调试时容易看错。 所有金字塔层跑完后,prev 被设为 caW0 的最后一层输出,再连续更新 cFF1 和 cFF2 两个前馈层;只要 cFF1 或 cFF2 任一更新失败就返回 false,全过才返回 true。在 MT5 里挂这类自定义神经网络 EA 时,若日志出现权重更新中断,优先对照这段检查 caAttentionOut 的层数索引是否越界。

MQL5 / C++
class="type">bool CNeuronSPyrAttentionOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  CNeuronBaseOCL *prev = NeuronOCL;
  CNeuronBaseOCL *current = NULL;
  for(class="type">uint l = class="num">0; l < iPAMLayers; l++)
    {
    class=class="str">"cmt">//--- S3
    current = caS3.At(l);
    if(!current ||
       !current.UpdateInputWeights(prev)
       )
       class="kw">return false;
    class=class="str">"cmt">//--- Query
    prev = current;
    current = caQuery.At(l);
    if(!current ||
       !current.UpdateInputWeights(prev)
       )
       class="kw">return false;
    class=class="str">"cmt">//--- Key and Value
    current = caKV.At(l);
    if(!current ||
       !current.UpdateInputWeights(prev)
       )
       class="kw">return false;
    class=class="str">"cmt">//--- W0
    prev = caAttentionOut.At(l);
    current = caW0.At(l);
    if(!current ||
       !current.UpdateInputWeights(prev)
       )
       class="kw">return false;
    prev = current;
    }
class=class="str">"cmt">//--- FeedForward
  prev = caW0.At(iPAMLayers);
  if(!cFF1.UpdateInputWeights(prev) ||
     !cFF2.UpdateInputWeights(cFF1.AsObject())
     )
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }

◍ 下一篇再看回测结果

这一节把 MSFformer(多尺度特征变换器)的来路交代清楚了:它出自《基于多尺度特征提取进行时间序列预测》那篇论文,核心是用改进的金字塔关注度架构,加上从输入里抽不同尺度特征的新办法。 我们在实践部分已经把两个主模块写进了 MT5 的自定义指标工程,代码能直接编译跑通,但训练与预测效果还没在这篇亮出来。 外汇与贵金属行情受宏观事件冲击大、波动跳空频繁,这类多尺度模型在样本外大概率会衰减,实盘前务必用历史数据自己回一遍。 下一篇会直接给模块的回测曲线和参数敏感区间,到时候你可以照着把周期切到 M15 对照黄金 tick 数据复现。

「记住这一条就够了」

这套 LSTM 优化方案落地到 MT5,靠的是 8 个文件分工:6 个 EA(Research / ResearchRealORL / Study / StudyEncoder / Test 加上前文训练用脚本)负责采样、训练与验证,Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 与 NeuroNet.cl 把网络创建和 OpenCL 加速拆成类库与内核。 要复现,先下 MQL5.zip(约 1.39 MB)把上述文件塞进对应目录,用 Research 跑样本、Study 训模型、Test 看多元序列预测偏差;外汇与贵金属波动剧烈,模型输出只是概率倾向,实盘前务必在策略测试器用历史数据核验过拟合程度。 真要把它用起来,核心就一句:别只调网络层数,采样 EA 的窗口与特征构造才是预测胜率的天花板。

常见问题

先打印各注意力头权重矩阵维度,确认是否重复创建了全量特征对象;用懒加载只保留当前尺度所需张量,显存通常能降三成以上。
检查残差相加前是否漏了层归一化,以及注意力输出与输入通道数是否严格对齐;对齐后震荡多会明显收敛。
可以。小布能按你给的层级结构自动生成拼装校验脚本,并标出梯度回传断点,省去手动逐层查错。
多是归约时未按尺度重置索引偏移,导致高层特征错位;在前向入口显式传入每层 stride 即可对齐。
看金字塔层自身缩放因子是否过小,残差只保底层梯度,高层仍靠本层权重回传;调大初始化方差常能恢复。