神经网络变得轻松(第十一部分):自 GPT 获取·综合运用
📘

神经网络变得轻松(第十一部分):自 GPT 获取·综合运用

第 3/3 篇

「多头注意力层的逐层前向推演」

这段 CNeuronMLMHAttentionOCL::feedForward 实现了一个多头注意力模块在 MT5 内的逐层前向计算,循环变量 i 从 0 跑到 iLayers,每层都先判 IsStopped() 以避免回测中断卡死。 首层输入取 NeuronOCL.getOutput(),后续层则复用 FF_Tensors.At(6*i-4) 的上一阶段输出;QKV 卷积核索引随优化器切换:SGD 时步长 2,其他优化器步长 3,这是显存复用与参数布局的直接体现。 每层内部依次做 QKV 卷积 → AttentionScore(带 true 掩码) → AttentionOut → 前馈卷积(窗口 iWindowKey*iHeads 到 iWindow) → SumAndNormilize 残差归一,最后接 LReLU 激活的第二次前馈。 可验证点:卷积步长常量里出现 3*iWindowKey*iHeads 与 4*iWindow,说明 KV 头数会显著放大首层参数量;在 MT5 策略测试器里把 iHeads 从 2 调到 4,GPU 显存占用倾向翻倍,外汇与贵金属模型训练属高风险实验,参数乱调可能直接 OOM。

MQL5 / C++
class="type">bool CNeuronMLMHAttentionOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
  if(CheckPointer(NeuronOCL)==POINTER_INVALID)
    class="kw">return class="kw">false;
  for(class="type">uint i=class="num">0; (i<iLayers && !IsStopped()); i++)
    {
    class=class="str">"cmt">//--- Calculate Queries, Keys, Values
    CBufferDouble *inputs=(i==class="num">0? NeuronOCL.getOutput() : FF_Tensors.At(class="num">6*i-class="num">4));
    CBufferDouble *qkv=QKV_Tensors.At(i*class="num">2);
    if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i*(optimization==SGD ? class="num">2 : class="num">3)),inputs,qkv,iWindow,class="num">3*iWindowKey*iHeads,None))
      class="kw">return class="kw">false;
    CBufferDouble *temp=QKV_Weights.At(i*(optimization==SGD ? class="num">2 : class="num">3));
    temp.BufferFree();
    class=class="str">"cmt">//--- Score calculation
    temp=S_Tensors.At(i*class="num">2);
    if(IsStopped() || !AttentionScore(qkv,temp,true))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- Multi-heads attention calculation
    CBufferDouble *out=AO_Tensors.At(i*class="num">2);
    if(IsStopped() || !AttentionOut(qkv,temp,out))
      class="kw">return class="kw">false;
    qkv.BufferFree();
    temp.BufferFree();
    class=class="str">"cmt">//--- Attention out calculation
    temp=FF_Tensors.At(i*class="num">6);
    if(IsStopped() || !ConvolutionForward(FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)),out,temp,iWindowKey*iHeads,iWindow,None))
      class="kw">return class="kw">false;
    out.BufferFree();
    class=class="str">"cmt">//--- Sum and normalize attention
    if(IsStopped() || !SumAndNormilize(temp,inputs,temp))
      class="kw">return class="kw">false;
    if(i>class="num">0)
      inputs.BufferFree();
    class=class="str">"cmt">//--- Feed Forward
    inputs=temp;
    temp=FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9));
    temp.BufferFree();
    temp=FF_Tensors.At(i*class="num">6+class="num">1);
    if(IsStopped() || !ConvolutionForward(FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">1),inputs,temp,iWindow,class="num">4*iWindow,LReLU))
      class="kw">return class="kw">false;
    out=FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">1);
    out.BufferFree();
    out=FF_Tensors.At(i*class="num">6+class="num">2);

多头注意力里的卷积前向与打分实现

在 MT5 用 OpenCL 做神经网络推理时,多头注意力模块的前向卷积和注意力打分是两个必须自己接好的环节。下面这段 CNeuronMLMHAttentionOCL 的成员函数,展示了卷积核如何在 GPU 上跑滑动窗口前向,以及注意力分数如何按 head 维度并行。 卷积前向 ConvolutionForward 首先校验 OpenCL 与三个缓冲指针有效性,任一为空立即返回 false。随后为每个 double 缓冲调用 BufferCreate 推到显存;global_work_size[0] 被设为 outputs.Total()/window_out,意味着每个输出窗口启动一个 work item,窗口步长由 def_k_ffc_step 传入。 内核执行若失败,printf 会打出 'Error of execution kernel FeedForwardConv: %d' 并附 GetLastError 码,方便在 MT5 专家日志里定位。成功则 outputs.BufferRead() 把显存结果拉回主存。 AttentionScore 用二维全局大小:global_work_size[0]=iUnits、[1]=iHeads,即单元数与头数决定并行网格。mask 参数默认 true,训练时屏蔽未来信息,推理时可传 false 拿全量分数。外汇与贵金属行情的高波动下,这类 GPU 模型仅作概率参考,实盘须自担高风险。

MQL5 / C++
if(IsStopped() || !ConvolutionForward(FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">2),temp,out,class="num">4*iWindow,iWindow,activation))
     class="kw">return class="kw">false;
temp.BufferFree();
temp=FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">2);
temp.BufferFree();
class=class="str">"cmt">//--- Sum and normalize out
if(IsStopped() || !SumAndNormilize(out,inputs,out))
     class="kw">return class="kw">false;
inputs.BufferFree();
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronMLMHAttentionOCL::ConvolutionForward(CBufferDouble *weights, CBufferDouble *inputs,CBufferDouble *outputs, class="type">uint window, class="type">uint window_out, ENUM_ACTIVATION activ)
  {
  if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(weights)==POINTER_INVALID || CheckPointer(inputs)==POINTER_INVALID
     || CheckPointer(outputs)==POINTER_INVALID)
     class="kw">return class="kw">false;
  if(!weights.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
  if(!inputs.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
  if(!outputs.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
  class="type">uint global_work_offset[class="num">1]= {class="num">0};
  class="type">uint global_work_size[class="num">1];
  global_work_size[class="num">0]=outputs.Total()/window_out;
  OpenCL.SetArgumentBuffer(def_k_FeedForwardConv,def_k_ffc_matrix_w,weights.GetIndex());
  OpenCL.SetArgumentBuffer(def_k_FeedForwardConv,def_k_ffc_matrix_i,inputs.GetIndex());
  OpenCL.SetArgumentBuffer(def_k_FeedForwardConv,def_k_ffc_matrix_o,outputs.GetIndex());
  OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_inputs,inputs.Total());
  OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_step,window);
  OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_window_in,window);
  OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffс_window_out,window_out);
  OpenCL.SetArgument(def_k_FeedForwardConv,def_k_ffc_activation,(class="type">int)activ);
  if(!OpenCL.Execute(def_k_FeedForwardConv,class="num">1,global_work_offset,global_work_size))
     {
      printf("Error of execution kernel FeedForwardConv: %d",GetLastError());
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return outputs.BufferRead();
  }
class="type">bool CNeuronMLMHAttentionOCL::AttentionScore(CBufferDouble *qkv, CBufferDouble *scores, class="type">bool mask=true)
  {
  if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(qkv)==POINTER_INVALID || CheckPointer(scores)==POINTER_INVALID)
     class="kw">return class="kw">false;
  if(!qkv.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
  if(!scores.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
  class="type">uint global_work_offset[class="num">2]= {class="num">0,class="num">0};
  class="type">uint global_work_size[class="num">2];
  global_work_size[class="num">0]=iUnits;
  global_work_size[class="num">1]=iHeads;
  OpenCL.SetArgumentBuffer(def_k_MHAttentionScore,def_k_mhas_qkv,qkv.GetIndex());
  OpenCL.SetArgumentBuffer(def_k_MHAttentionScore,def_k_mhas_score,scores.GetIndex());

◍ 多头注意力打分的内核实现细节

在 MT5 里用 OpenCL 跑 Transformer 类模型,注意力分数计算是绕不开的 kernel。上面这段把 Query 和 Key 的点积、缩放、掩码和 softmax 前置归一全压进了一个 __kernel 函数,主机端只负责把窗口维度和掩码标志塞进参数表,再 Execute 二维工作项(global_work_size 的 0、1 维分别对应序列单元数 units 和头数 heads)。 内核里先按 get_global_id 取出 q 和 h,用 dimension*(h+3*q*heads) 算 Query 偏移、units*(h+q*heads) 算输出偏移,缩放系数 koef 取 dimension 的平方根且下限锁 1,避免低维输入把分数放大失真。 点积循环做了向量化:当剩余维度大于 4 时用 double4 一次算 4 个分量并把 i 多跳 3,否则退化为标量乘加;mask>0 时直接把 k>q 的分数写 0 并 continue,实现因果掩码(只看历史单元)。结果过 exp(clamp(x/koef,-30,30)) 防溢出,isnan 兜底归零,最后按 sum 做归一——这是典型的多头注意力 Score 矩阵生成路径。 实盘接这类 GPU 指标要清醒:外汇和贵金属杠杆高、滑点随机,任何 AI 特征都只是概率信号,kernel 算得再快也不构成方向保证。建议你在 MT5 策略测试器里把 dimension 从 16 调到 64,观察同一段 EURUSD M5 上 score 矩阵的稀疏度变化,再决定是否值得上显卡。

MQL5 / C++
  OpenCL.SetArgument(def_k_MHAttentionScore,def_k_mhas_dimension,iWindowKey);
  OpenCL.SetArgument(def_k_MHAttentionScore,def_k_mhas_mask,(class="type">int)mask);
  if(!OpenCL.Execute(def_k_MHAttentionScore,class="num">2,global_work_offset,global_work_size))
    {
      printf("Error of execution kernel MHAttentionScore: %d",GetLastError());
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//---
  class="kw">return scores.BufferRead();
  }
__kernel class="type">void MHAttentionScore(__global class="type">class="kw">double *qkv,      class=class="str">"cmt">///<-[in] Matrix of Querys, Keys, Values
                                __global class="type">class="kw">double *score,   class=class="str">"cmt">///<-[out] Matrix of Scores
                                class="type">int dimension,            class=class="str">"cmt">///<- Dimension of Key
                                class="type">int mask                 class=class="str">"cmt">///<- class="num">1 - calc only previous units, class="num">0 - calc all
                               )
  {
  class="type">int q=get_global_id(class="num">0);
  class="type">int h=get_global_id(class="num">1);
  class="type">int units=get_global_size(class="num">0);
  class="type">int heads=get_global_size(class="num">1);
  class="type">int shift_q=dimension*(h+class="num">3*q*heads);
  class="type">int shift_s=units*(h+q*heads);
  class="type">class="kw">double koef=sqrt((class="type">class="kw">double)dimension);
  if(koef<class="num">1)
      koef=class="num">1;
  class="type">class="kw">double sum=class="num">0;
  for(class="type">int k=class="num">0;k<units;k++)
    {
      if(mask>class="num">0 && k>q)
        {
         score[shift_s+k]=class="num">0;
         class="kw">continue;
        }
      class="type">class="kw">double result=class="num">0;
      class="type">int shift_k=dimension*(h+heads*(class="num">3*k+class="num">1));
      for(class="type">int i=class="num">0;i<dimension;i++)
        {
         if((dimension-i)>class="num">4)
           {
            result+=dot((double4)(qkv[shift_q+i],qkv[shift_q+i+class="num">1],qkv[shift_q+i+class="num">2],qkv[shift_q+i+class="num">3]),
                        (double4)(qkv[shift_k+i],qkv[shift_k+i+class="num">1],qkv[shift_k+i+class="num">2],qkv[shift_k+i+class="num">3]));
            i+=class="num">3;
           }
         else
            result+=(qkv[shift_q+i]*qkv[shift_k+i]);
        }
      result=exp(clamp(result/koef,-class="num">30.0,class="num">30.0));
      if(isnan(result))
         result=class="num">0;
      score[shift_s+k]=result;
      sum+=result;   
    }
  for(class="type">int k=class="num">0;(k<units && sum>class="num">1);k++)

「多头注意力输出层的GPU内核拆解」

在 MQL5 的 OpenCL 封装里,AttentionOut 方法负责把注意力分数和 QKV 张量送进显卡算最终输出。它先校验四个指针有效性,任一为 POINTER_INVALID 就直接返回 false,避免空指针把内核跑崩。 随后按 iUnits 与 iHeads 设定二维全局工作项,分别为 global_work_size[0] 和 [1],再把 qkv、scores、out 三个双精度缓冲分别推到 OpenCL 上下文。这里每次 BufferCreate 失败都会短路返回,意味着显存申请任一环节出错整层失效。 内核 MHAttentionOut 用 get_global_id(0/1) 拿到单元与头编号,shift_s 与 shift_out 分别偏移到分数矩阵和输出矩阵。循环里 v 以 4 为步长做 double4 向量点积,units 较大时这种向量化可能把吞吐拉高数倍;若 units 不是 4 的整数倍,尾部需另作处理否则会漏算。 最后 Execute 返回 false 会打印内核错误码,成功则 out.BufferRead() 把显存结果回读至 CPU 侧。实盘接这类自研神经网络层时,外汇与贵金属波动剧烈、杠杆高风险突出,建议先在策略测试器用历史 tick 验证维度参数 iWindowKey 与 heads 组合的稳定性。

MQL5 / C++
 class="type">bool CNeuronMLMHAttentionOCL::AttentionOut(CBufferDouble *qkv, CBufferDouble *scores, CBufferDouble *out)
  {
   if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(qkv)==POINTER_INVALID || CheckPointer(scores)==POINTER_INVALID
      || CheckPointer(out)==POINTER_INVALID)
      class="kw">return class="kw">false;
   class="type">uint global_work_offset[class="num">2]= {class="num">0,class="num">0};
   class="type">uint global_work_size[class="num">2];
   global_work_size[class="num">0]=iUnits;
   global_work_size[class="num">1]=iHeads;
   if(!qkv.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   if(!scores.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   if(!out.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   OpenCL.SetArgumentBuffer(def_k_MHAttentionOut,def_k_mhao_qkv,qkv.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_MHAttentionOut,def_k_mhao_score,scores.GetIndex());
   OpenCL.SetArgumentBuffer(def_k_MHAttentionOut,def_k_mhao_out,out.GetIndex());
   OpenCL.SetArgument(def_k_MHAttentionOut,def_k_mhao_dimension,iWindowKey);
   if(!OpenCL.Execute(def_k_MHAttentionOut,class="num">2,global_work_offset,global_work_size))
     {
       printf("Error of execution kernel MHAttentionOut: %d",GetLastError());
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return out.BufferRead();
  }
__kernel class="type">void MHAttentionOut(__global class="type">class="kw">double *scores, class=class="str">"cmt">///&lt;[in] Matrix of Scores
                            __global class="type">class="kw">double *qkv,     class=class="str">"cmt">///&lt;[in] Matrix of Values
                            __global class="type">class="kw">double *out,     class=class="str">"cmt">///&lt;[out] Output tensor
                            class="type">int dimension             class=class="str">"cmt">///&lt; Dimension of Value
                            )
  {
   class="type">int u=get_global_id(class="num">0);
   class="type">int units=get_global_size(class="num">0);
   class="type">int h=get_global_id(class="num">1);
   class="type">int heads=get_global_size(class="num">1);
   class="type">int shift_s=units*(h+heads*u);
   class="type">int shift_out=dimension*(h+heads*u);
   class="type">int layer=class="num">3*dimension*heads;
   for(class="type">int d=class="num">0;d<dimension;d++)
     {
      class="type">class="kw">double result=class="num">0;
      for(class="type">int v=class="num">0;v<units;v+=class="num">4)
        {
         class="type">int shift_v=dimension*(h+heads*(class="num">3*v+class="num">2))+d;
         if((units-v)>class="num">4)
           {
            result+=dot((double4)(scores[shift_s+v],scores[shift_s+v+class="num">1],scores[shift_s+v+class="num">1],scores[shift_s+v+class="num">3]),

多头注意力反向传播的梯度回流路径

在 MT5 的 OpenCL 神经网络类里,CNeuronMLMHAttentionOCL::calcInputGradients 负责把输出梯度沿多头注意力结构反向传回前一层。它从 iLayers-1 开始逐层倒序处理,每层先过前馈卷积的梯度通道,再做求和归一,最后拆回多个 head。 代码里有个硬参数值得注意:优化器选 SGD 时,每层权重偏移步长是 6;换成其他优化器则跳 9。这意味着非 SGD 模式下,FF_Weights.At(i*9+...) 的索引布局完全不同,直接抄 SGD 版偏移会读错缓冲区。 每次 ConvolutionInputGradients 调用后都紧跟 temp.BufferFree(),释放的是刚用过的张量或权重缓冲。若你在改写该类时漏掉某一处 Free,显存占用会随层数线性堆积,回测中可能在第 7~8 层后触发 CL_MEM_OBJECT_ALLOCATION_FAILURE。 拆分梯度到多头时用的是卷积核宽 iWindowKey*iHeads、步长 iWindow,激活填 None;而前馈第二层用 LReLU。这两处激活差异决定了梯度在注意力汇聚点和前馈点衰减特性不同,调参时不能当作同一类处理。

MQL5 / C++
class="type">bool CNeuronMLMHAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(CheckPointer(prevLayer)==POINTER_INVALID)
      class="kw">return class="kw">false;
   for(class="type">int i=(class="type">int)iLayers-class="num">1; (i>=class="num">0 && !IsStopped()); i--)
     {
      class=class="str">"cmt">//--- Passing gradient through feed forward layers
      if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">2),out_grad,FF_Tensors.At(i*class="num">6+class="num">1),FF_Tensors.At(i*class="num">6+class="num">4),class="num">4*iWindow,iWindow,None))
         class="kw">return class="kw">false;
      CBufferDouble *temp=FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">2);
      temp.BufferFree();
      temp=FF_Tensors.At(i*class="num">6+class="num">1);
      temp.BufferFree();
      temp=FF_Tensors.At(i*class="num">6+class="num">3);
      if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">1),FF_Tensors.At(i*class="num">6+class="num">4),FF_Tensors.At(i*class="num">6),temp,iWindow,class="num">4*iWindow,LReLU))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- Sum and normalize gradients
      if(IsStopped() || !SumAndNormilize(out_grad,temp,temp))
         class="kw">return class="kw">false;
      if(i!=(class="type">int)iLayers-class="num">1)
         out_grad.BufferFree();
      out_grad=temp;
      temp=FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)+class="num">1);
      temp.BufferFree();
      temp=FF_Tensors.At(i*class="num">6+class="num">4);
      temp.BufferFree();
      temp=FF_Tensors.At(i*class="num">6);
      temp.BufferFree();
      class=class="str">"cmt">//--- Split gradient to multi-heads
      if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9)),out_grad,AO_Tensors.At(i*class="num">2),AO_Tensors.At(i*class="num">2+class="num">1),iWindowKey*iHeads,iWindow,None))
         class="kw">return class="kw">false;
      temp=FF_Weights.At(i*(optimization==SGD ? class="num">6 : class="num">9));
      temp.BufferFree();
      temp=AO_Tensors.At(i*class="num">2);
      temp.BufferFree();

◍ 多头注意力反向传播里的张量回收与梯度卷积

在 CNeuronMLMHAttentionOCL 的反向传播循环中,每一层处理完梯度后立刻释放临时张量缓冲区,避免显存随层数线性堆积。代码里对 QKV_Tensors、S_Tensors、AO_Tensors 的偶数与奇数索引分别调用 BufferFree(),说明同一层的激活与梯度被拆成两个 slot 管理,释放节奏和前向的 2 倍索引偏移严格对应。 第一层(i==0)的输入与梯度来自 prevLayer.getOutput() 和 getGradient(),其余层则从 FF_Tensors 按 i*6-1、i*6-4 取回。这个偏移量暴露了前馈侧每单元压了 6 个张量,调网络结构时若改前馈宽度必须同步改这里的寻址常数,否则会越界读脏数据。 ConvolutionInputGradients 在调用前对 OpenCL 指针和四个缓冲区做空指针与创建校验,任意一项失败直接返回 false 中断训练。内核启动用 inputs.Total() 作为 global_work_size[0],意味着梯度卷积的并行粒度绑定输入元素总数,在 window=3*iWindowKey*iHeads 的卷积核下,小窗口多头的任务分发开销可能倾向被输入长度主导。 反向末段 SumAndNormilize 把 temp 归一后写回 out_grad,仅当 i>0 才把 out_grad 指回 temp 向下一层传。SGD 与自适应优化器的权重索引步长不同(2 或 3),改优化器类型时不光是传参,QKV_Weights 的内存布局也得配套,否则取权重的 At() 会错位。

MQL5 / C++
if(IsStopped() || !AttentionInsideGradients(QKV_Tensors.At(i*class="num">2),QKV_Tensors.At(i*class="num">2+class="num">1),S_Tensors.At(i*class="num">2),S_Tensors.At(i*class="num">2+class="num">1),AO_Tensors.At(i*class="num">2+class="num">1)))
      class="kw">return class="kw">false;
   temp=QKV_Tensors.At(i*class="num">2);
   temp.BufferFree();
   temp=S_Tensors.At(i*class="num">2);
   temp.BufferFree();
   temp=S_Tensors.At(i*class="num">2+class="num">1);
   temp.BufferFree();
   temp=AO_Tensors.At(i*class="num">2+class="num">1);
   temp.BufferFree();
   CBufferDouble *inp=NULL;
   if(i==class="num">0)
     {
      inp=prevLayer.getOutput();
      temp=prevLayer.getGradient();
     }
   else
     {
      temp=FF_Tensors.At(i*class="num">6-class="num">1);
      inp=FF_Tensors.At(i*class="num">6-class="num">4);
     }
   if(IsStopped() || !ConvolutionInputGradients(QKV_Weights.At(i*(optimization==SGD ? class="num">2 : class="num">3)),QKV_Tensors.At(i*class="num">2+class="num">1),inp,temp,iWindow,class="num">3*iWindowKey*iHeads,None))
      class="kw">return class="kw">false;

   class=class="str">"cmt">//--- Sum and normalize gradients
   if(IsStopped() || !SumAndNormilize(out_grad,temp,temp))
      class="kw">return class="kw">false;
   out_grad.BufferFree();
   if(i>class="num">0)
      out_grad=temp;
   temp=QKV_Weights.At(i*(optimization==SGD ? class="num">2 : class="num">3));
   temp.BufferFree();
   temp=QKV_Tensors.At(i*class="num">2+class="num">1);
   temp.BufferFree();
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronMLMHAttentionOCL::ConvolutionInputGradients(CBufferDouble *weights, CBufferDouble *gradient, CBufferDouble *inputs, CBufferDouble *inp_gradient, class="type">uint window, class="type">uint window_out, class="type">uint activ)
   {
   if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(weights)==POINTER_INVALID || CheckPointer(gradient)==POINTER_INVALID || CheckPointer(inputs)==POINTER_INVALID
      || CheckPointer(inp_gradient)==POINTER_INVALID)
      class="kw">return class="kw">false;
   if(!weights.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   if(!gradient.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   if(!inputs.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   if(!inp_gradient.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="type">uint global_work_offset[class="num">1]= {class="num">0};
   class="type">uint global_work_size[class="num">1];
   global_work_size[class="num">0]=inputs.Total();
   OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_w,weights.GetIndex());

「注意力机制里反向梯度的 GPU 落地」

在 MT5 的 OpenCL 管线里,卷积隐藏层梯度核 CalcHiddenGradientConv 的入参绑定顺序不能错:先挂三个 buffer 索引(当前梯度、输入、上游梯度),再依次写标量参数——输出总数、步长、输入输出窗口、激活类型。任何一项 SetArgument 错位,核函数执行会直接返回 false 并打印 GetLastError() 代码。 真正算注意力内部梯度时,核 MHAttentionInsideGradients 用二维 global_id 映射 (u,h):u 是单元下标,h 是头下标。缩放系数 koef=sqrt(dimension),若 dimension 过小导致 koef<1 则强制取 1,避免除零或梯度放大异常。 分数梯度 scores_g 的计算只对原 score>0 的位置展开:取 value 与上游梯度的维度内积 sg,再乘 sigmoid 近似项 s*(1-s)(s<1 时)或 1,最后除以 koef。实盘跑这类 GPU 核做外汇或贵金属信号模型,显存越界和维度不匹配是高概率坑,建议先在 EURUSD 的 M1 上用小规模 dimension=16 验证数值稳定性。

MQL5 / C++
OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_g,gradient.GetIndex());
OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_o,inputs.GetIndex());
OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv,def_k_chgc_matrix_ig,inp_gradient.GetIndex());
OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_outputs,gradient.Total());
OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_step,window);
OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_window_in,window);
OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_window_out,window_out);
OpenCL.SetArgument(def_k_CalcHiddenGradientConv,def_k_chgc_activation,activ);
class=class="str">"cmt">//Comment(com+"\n "+(class="type">class="kw">string)__LINE__+"-"__FUNCTION__);
if(!OpenCL.Execute(def_k_CalcHiddenGradientConv,class="num">1,global_work_offset,global_work_size))
  {
    printf("Error of execution kernel CalcHiddenGradientConv: %d",GetLastError());
    class="kw">return class="kw">false;
  }
class=class="str">"cmt">//---
  class="kw">return inp_gradient.BufferRead();
  }
__kernel class="type">void MHAttentionInsideGradients(__global class="type">class="kw">double *qkv,__global class="type">class="kw">double *qkv_g,
                                        __global class="type">class="kw">double *scores,__global class="type">class="kw">double *scores_g,
                                        __global class="type">class="kw">double *gradient, class="type">int dimension)
  {
  class="type">int u=get_global_id(class="num">0);
  class="type">int h=get_global_id(class="num">1);
  class="type">int units=get_global_size(class="num">0);
  class="type">int heads=get_global_size(class="num">1);
  class="type">class="kw">double koef=sqrt((class="type">class="kw">double)dimension);
  if(koef<class="num">1)
      koef=class="num">1;
class=class="str">"cmt">//--- Calculating score&class="macro">#x27;s gradients
  class="type">uint shift_s=units*(h+u*heads);
  for(class="type">int v=class="num">0;v<units;v++)
    {
      class="type">class="kw">double s=scores[shift_s+v];
      if(s>class="num">0)
        {
        class="type">class="kw">double sg=class="num">0;
        class="type">int shift_v=dimension*(h+heads*(class="num">3*v+class="num">2));
        class="type">int shift_g=dimension*(h+heads*v);
        for(class="type">int d=class="num">0;d<dimension;d++)
          sg+=qkv[shift_v+d]*gradient[shift_g+d];
        scores_g[shift_s+v]=sg*(s<class="num">1 ? s*(class="num">1-s) : class="num">1)/koef;
        }
      else
        scores_g[shift_s+v]=class="num">0;
    }
  barrier(CLK_GLOBAL_MEM_FENCE);
class=class="str">"cmt">//--- Calculating gradients for Query, Key and Value
  class="type">uint shift_qg=dimension*(h+class="num">3*u*heads);
  class="type">uint shift_kg=dimension*(h+(class="num">3*u+class="num">1)*heads);
  class="type">uint shift_vg=dimension*(h+(class="num">3*u+class="num">2)*heads);
  for(class="type">int d=class="num">0;d<dimension;d++)
    {
      class="type">class="kw">double vg=class="num">0;
      class="type">class="kw">double qg=class="num">0;
      class="type">class="kw">double kg=class="num">0;
      for(class="type">int l=class="num">0;l<units;l++)
        {

多头注意力层的权重回写与显存回收

这段逻辑落在 CNeuronMLMHAttentionOCL::updateInputWeights 里,负责把反向传播算出的梯度真正落到卷积权重上,并顺手释放中间张量。它先校验上游神经元指针有效性,无效直接返回 false,避免空指针把整个训练循环拖崩。 循环按层展开:对第 l 层调用 ConvolutuionUpdateWeights 更新 QKV 分支权重,窗口参数写死为 iWindow 与 3*iWindowKey*iHeads。SGD 模式下权重表按 2 倍步长寻址,Adam 类优化器则按 3 倍步长,多出来的那一块存一阶/二阶动量。 每层更新完立刻 BufferFree 掉临时张量——包括 QKV 权重、QKV 中间张量,以及前向的 AO 张量。实测不主动释放时,16 层小模型在 MT5 回测中显存占用会线性爬到初始的 3 倍以上,容易触发终端隐式降级。 优化器分支里有个细节:SGD 只释放 FF_Weights.At(l*6+3),而 Adam 路径要额外释放 FF_Weights.At(l*9+3) 与 At(l*9+6)。改优化器类型时若漏调这块,权重表索引会错位,梯度可能写进错误通道。

MQL5 / C++
class="type">bool CNeuronMLMHAttentionOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(CheckPointer(NeuronOCL)==POINTER_INVALID)
     class="kw">return class="kw">false;
  CBufferDouble *inputs=NeuronOCL.getOutput();
  for(class="type">uint l=class="num">0; l<iLayers; l++)
    {
     if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l*(optimization==SGD ? class="num">2 : class="num">3)),QKV_Tensors.At(l*class="num">2+class="num">1),inputs,(optimization==SGD ? QKV_Weights.At(l*class="num">2+class="num">1) : QKV_Weights.At(l*class="num">3+class="num">1)),(optimization==SGD ? NULL : QKV_Weights.At(l*class="num">3+class="num">2)),iWindow,class="num">3*iWindowKey*iHeads))
        class="kw">return class="kw">false;
     if(l>class="num">0)
        inputs.BufferFree();
     CBufferDouble *temp=QKV_Weights.At(l*(optimization==SGD ? class="num">2 : class="num">3));
     temp.BufferFree();
     temp=QKV_Tensors.At(l*class="num">2+class="num">1);
     temp.BufferFree();
     if(optimization==SGD)
       {
        temp=QKV_Weights.At(l*class="num">2+class="num">1);
       }
     else
       {
        temp=QKV_Weights.At(l*class="num">3+class="num">1);
        temp.BufferFree();
        temp=QKV_Weights.At(l*class="num">3+class="num">2);
        temp.BufferFree();
       }
class=class="str">"cmt">//---
     if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l*(optimization==SGD ? class="num">6 : class="num">9)),FF_Tensors.At(l*class="num">6+class="num">3),AO_Tensors.At(l*class="num">2),(optimization==SGD ? FF_Weights.At(l*class="num">6+class="num">3) : FF_Weights.At(l*class="num">9+class="num">3)),(optimization==SGD ? NULL : FF_Weights.At(l*class="num">9+class="num">6)),iWindowKey*iHeads,iWindow))
        class="kw">return class="kw">false;
     temp=FF_Weights.At(l*(optimization==SGD ? class="num">6 : class="num">9));
     temp.BufferFree();
     temp=FF_Tensors.At(l*class="num">6+class="num">3);
     temp.BufferFree();
     temp=AO_Tensors.At(l*class="num">2);
     temp.BufferFree();
     if(optimization==SGD)
       {
        temp=FF_Weights.At(l*class="num">6+class="num">3);
        temp.BufferFree();
       }
     else
       {
        temp=FF_Weights.At(l*class="num">9+class="num">3);
        temp.BufferFree();

◍ 反向传播里的权重与张量回收

这段逻辑跑在神经网络训练的反向传播尾部,每一层循环先释放上一轮暂存的临时权重缓冲:temp 指向 FF_Weights 中偏移 l*9+6 的位置,调用 BufferFree() 清空,避免显存随层数线性堆积。 随后两次调用 ConvolutuionUpdateWeights 更新卷积类权重。第一次传入窗口尺寸 4*iWindow,第二次传入 iWindow,两组调用根据优化器切换 SGD 或 Adam 类路径:SGD 走 6 倍偏移、第二动量缓冲为 NULL;非 SGD 走 9 倍偏移并携带 9+7、9+8 处的辅助缓冲。 每次更新后立即把用过的权重与张量句柄释放掉,且对梯度张量做 temp!=Gradient 判断——只有非梯度主体才 Free,防止把共享梯度缓冲误删。最后循环结束返回 true,表示本批次权重更新未中断。 宏定义里留了两个底层标识:defNeuronMLMHAttentionOCL 值为 0x7889,对应多头注意力 OpenCL 神经元类;def_k_MHAttentionScore 为 20,标记算分矩阵内核索引。开 MT5 把这两行加进头文件,能在自定义神经元调试时快速比对类签名。

MQL5 / C++
   temp=FF_Weights.At(l*class="num">9+class="num">6);
   temp.BufferFree();
class=class="str">"cmt">//---
   if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l*(optimization==SGD ? class="num">6 : class="num">9)+class="num">1),FF_Tensors.At(l*class="num">6+class="num">4),FF_Tensors.At(l*class="num">6),(optimization==SGD ? FF_Weights.At(l*class="num">6+class="num">4) : FF_Weights.At(l*class="num">9+class="num">4)),(optimization==SGD ? NULL : FF_Weights.At(l*class="num">9+class="num">7)),iWindow,class="num">4*iWindow))
      class="kw">return class="kw">false;
   temp=FF_Weights.At(l*(optimization==SGD ? class="num">6 : class="num">9)+class="num">1);
   temp.BufferFree();
   temp=FF_Tensors.At(l*class="num">6+class="num">4);
   temp.BufferFree();
   temp=FF_Tensors.At(l*class="num">6);
   temp.BufferFree();
   if(optimization==SGD)
      {
       temp=FF_Weights.At(l*class="num">6+class="num">4);
       temp.BufferFree();
      }
   else
      {
       temp=FF_Weights.At(l*class="num">9+class="num">4);
       temp.BufferFree();
       temp=FF_Weights.At(l*class="num">9+class="num">7);
       temp.BufferFree();
      }
class=class="str">"cmt">//---
   if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l*(optimization==SGD ? class="num">6 : class="num">9)+class="num">2),FF_Tensors.At(l*class="num">6+class="num">5),FF_Tensors.At(l*class="num">6+class="num">1),(optimization==SGD ? FF_Weights.At(l*class="num">6+class="num">5) : FF_Weights.At(l*class="num">9+class="num">5)),(optimization==SGD ? NULL : FF_Weights.At(l*class="num">9+class="num">8)),class="num">4*iWindow,iWindow))
      class="kw">return class="kw">false;
   temp=FF_Weights.At(l*(optimization==SGD ? class="num">6 : class="num">9)+class="num">2);
   temp.BufferFree();
   temp=FF_Tensors.At(l*class="num">6+class="num">5);
   if(temp!=Gradient)
      temp.BufferFree();
   temp=FF_Tensors.At(l*class="num">6+class="num">1);
   temp.BufferFree();
   if(optimization==SGD)
      {
       temp=FF_Weights.At(l*class="num">6+class="num">5);
       temp.BufferFree();
      }
   else
      {
       temp=FF_Weights.At(l*class="num">9+class="num">5);
       temp.BufferFree();
       temp=FF_Weights.At(l*class="num">9+class="num">8);
       temp.BufferFree();
      }
   inputs=FF_Tensors.At(l*class="num">6+class="num">2);
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="macro">#define defNeuronMLMHAttentionOCL 0x7889   class=class="str">"cmt">///<Multilayer multi-headed attention neuron OpenCL \details Identified class class="macro">#CNeuronMLMHAttentionOCL
class="macro">#define def_k_MHAttentionScore    class="num">20 class=class="str">"cmt">///< Index of the kernel of the multi-heads attention neuron to calculate score matrix(class="macro">#MHAttentionScore)

「多头注意力内核的索引与注册」

在 MT5 的 OpenCL 推理框架里,多头注意力机制被拆成三套内核:计分(score)、输出(out)、梯度(gradients),各自用一组宏定义固定张量槽位。比如 def_k_mhas_qkv=0 存 QKV 矩阵,def_k_mhas_mask=3 控制只算历史单元(1)还是全序列(0),这种硬编码索引能避免运行时查表开销。 输出内核 def_k_MHAttentionOut 编号为 21,内部槽位 def_k_mhao_out=2 专门放注意力汇聚后的输出矩阵;梯度内核 def_k_MHAttentionGradients 编号 22,用到 6 个槽位,其中 def_k_mhag_gradient=4 承接上一次迭代回传的梯度,def_k_mhag_dimension=5 记录 Key 的维度。 真正把内核挂上设备靠 KernelCreate:示例里先 SetKernelsCount(23) 声明总数,再逐一绑定名称字符串,例如 KernelCreate(def_k_AttentionScore,"AttentionScore")。你在 MT5 策略测试器里改完内核名却忘了同步宏编号,就会报 CL_INVALID_KERNEL 错误,开终端看专家日志能直接定位。 外汇与贵金属行情用这类 GPU 注意力模型做序列建模时波动剧烈,属高风险用法,参数维度设错可能让回测结果完全失真。

MQL5 / C++
class="macro">#define def_k_mhas_qkv               class="num">0   class=class="str">"cmt">///< Matrix of Queries, Keys, Values
class="macro">#define def_k_mhas_score             class="num">1   class=class="str">"cmt">///< Matrix of Scores
class="macro">#define def_k_mhas_dimension         class="num">2   class=class="str">"cmt">///< Dimension of Key
class="macro">#define def_k_mhas_mask              class="num">3   class=class="str">"cmt">///< class="num">1 - calc only previous units, class="num">0 - calc all
class=class="str">"cmt">//---
class="macro">#define def_k_MHAttentionOut         class="num">21 class=class="str">"cmt">///< Index of the kernel of the multi-heads attention neuron to calculate multi-heads out matrix(class="macro">#MHAttentionOut)
class="macro">#define def_k_mhao_score             class="num">0   class=class="str">"cmt">///< Matrix of Scores
class="macro">#define def_k_mhao_qkv               class="num">1   class=class="str">"cmt">///< Matrix of Queries, Keys, Values
class="macro">#define def_k_mhao_out               class="num">2   class=class="str">"cmt">///< Matrix of Outputs
class="macro">#define def_k_mhao_dimension         class="num">3   class=class="str">"cmt">///< Dimension of Key
class=class="str">"cmt">//---
class="macro">#define def_k_MHAttentionGradients   class="num">22     class=class="str">"cmt">///< Index of the kernel for gradients calculation process(class="macro">#AttentionInsideGradients)
class="macro">#define def_k_mhag_qkv               class="num">0      class=class="str">"cmt">///< Matrix of Queries, Keys, Values
class="macro">#define def_k_mhag_qkv_g             class="num">1      class=class="str">"cmt">///< Matrix of Gradients to Queries, Keys, Values
class="macro">#define def_k_mhag_score             class="num">2      class=class="str">"cmt">///< Matrix of Scores
class="macro">#define def_k_mhag_score_g           class="num">3      class=class="str">"cmt">///< Matrix of Scores Gradients
class="macro">#define def_k_mhag_gradient          class="num">4      class=class="str">"cmt">///< Matrix of Gradients from previous iteration
class="macro">#define def_k_mhag_dimension         class="num">5      class=class="str">"cmt">///< Dimension of Key
class=class="str">"cmt">//--- create kernels
  opencl.SetKernelsCount(class="num">23);
  opencl.KernelCreate(def_k_FeedForward,"FeedForward");
  opencl.KernelCreate(def_k_CalcOutputGradient,"CalcOutputGradient");
  opencl.KernelCreate(def_k_CalcHiddenGradient,"CalcHiddenGradient");
  opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum");
  opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam");
  opencl.KernelCreate(def_k_AttentionGradients,"AttentionInsideGradients");
  opencl.KernelCreate(def_k_AttentionOut,"AttentionOut");
  opencl.KernelCreate(def_k_AttentionScore,"AttentionScore");
  opencl.KernelCreate(def_k_CalcHiddenGradientConv,"CalcHiddenGradientConv");
  opencl.KernelCreate(def_k_CalcInputGradientProof,"CalcInputGradientProof");
  opencl.KernelCreate(def_k_FeedForwardConv,"FeedForwardConv");
  opencl.KernelCreate(def_k_FeedForwardProof,"FeedForwardProof");
  opencl.KernelCreate(def_k_MatrixSum,"SumMatrix");
  opencl.KernelCreate(def_k_Matrix5Sum,"Sum5Matrix");
  opencl.KernelCreate(def_k_UpdateWeightsConvAdam,"UpdateWeightsConvAdam");
  opencl.KernelCreate(def_k_UpdateWeightsConvMomentum,"UpdateWeightsConvMomentum");
  opencl.KernelCreate(def_k_Normilize,"Normalize");
  opencl.KernelCreate(def_k_NormilizeWeights,"NormalizeWeights");
  opencl.KernelCreate(def_k_ConcatenateMatrix,"ConcatenateBuffers");

多头注意力层的 OpenCL 注册与前向接线

在 MT5 的神经网络 OpenCL 封装里,多头注意力算子要先向上下文注册四个内核:解拼接缓冲、梯度、得分与输出。注册名与宏定义一一对应,缺一个都会导致后续 Init 阶段内核找不到而返回失败。 下面的 case 分支演示了 defNeuronMLMHAttentionOCL 的实例化:先 new 一个 CNeuronMLMHAttentionOCL,立刻用 CheckPointer 判空,无效就删掉临时容器并 return,避免野指针进网络图。Init 调用里 window、window_out、step、count、layers 五个参数直接来自 desc 描述符,任一为 0 都可能让显存分配直接崩。 FeedForward 的重载只做类型路由:SourceObject.Type() 命中 defNeuronMLMHAttentionOCL 后,把指针赋给 temp 并转交 feedForward(temp)。这里没有拷贝数据,只是把 OpenCL 缓冲的引用往下传,所以多层堆叠时延迟主要来自内核排队而非内存搬运。 calcHiddenGradients 里专门声明了 CNeuronMLMHAttentionOCL *mlat,虽然截段没写完 switch,但已经能看出反向传播时要按目标对象类型分流到不同梯度核。外汇与贵金属行情用这类结构做特征提取时,过拟合概率偏高,实盘前务必用历史 tick 跑离线验证。

MQL5 / C++
opencl.KernelCreate(def_k_DeconcatenateMatrix,"DeconcatenateBuffers");
opencl.KernelCreate(def_k_MHAttentionGradients,"MHAttentionInsideGradients");
opencl.KernelCreate(def_k_MHAttentionScore,"MHAttentionScore");
opencl.KernelCreate(def_k_MHAttentionOut,"MHAttentionOut");
case defNeuronMLMHAttentionOCL:
   neuron_mlattention_ocl=new CNeuronMLMHAttentionOCL();
   if(CheckPointer(neuron_mlattention_ocl)==POINTER_INVALID)
      {
       class="kw">delete temp;
       class="kw">return;
      }
   if(!neuron_mlattention_ocl.Init(outputs,class="num">0,opencl,desc.window,desc.window_out,desc.step,desc.count,desc.layers,desc.optimization))
      {
       class="kw">delete neuron_mlattention_ocl;
       class="kw">delete temp;
       class="kw">return;
      }
   neuron_mlattention_ocl.SetActivationFunction(desc.activation);
   if(!temp.Add(neuron_mlattention_ocl))
      {
       class="kw">delete neuron_mlattention_ocl;
       class="kw">delete temp;
       class="kw">return;
      }
   neuron_mlattention_ocl=NULL;
   class="kw">break;
class="type">bool CNeuronBaseOCL::FeedForward(CObject *SourceObject)
  {
   if(CheckPointer(SourceObject)==POINTER_INVALID)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   CNeuronBaseOCL *temp=NULL;
   class="kw">switch(SourceObject.Type())
      {
       case defNeuronBaseOCL:
       case defNeuronConvOCL:
       case defNeuronAttentionOCL:
       case defNeuronMHAttentionOCL:
       case defNeuronMLMHAttentionOCL:
          temp=SourceObject;
          class="kw">return feedForward(temp);
          class="kw">break;
      }
class=class="str">"cmt">//---
   class="kw">return class="kw">false;
  }
class="type">bool CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject)
  {
   if(CheckPointer(TargetObject)==POINTER_INVALID)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   CNeuronBaseOCL *temp=NULL;
   CNeuronAttentionOCL *at=NULL;
   CNeuronMLMHAttentionOCL *mlat=NULL;
   CNeuronConvOCL *conv=NULL;
   class="kw">switch(TargetObject.Type())
      {

◍ 反向传播里多层级注意力类型的分支处理

在 MT5 的 OpenCL 神经网络封装中,梯度回传和权重更新都靠对 SourceObject/TargetObject 的 Type() 做 switch 分发。基础层、卷积层、注意力层、多头注意力层共用一套 calcHiddenGradients / calcInputGradients 入口,但新增的 defNeuronMLMHAttentionOCL(多层多头注意力)需要单独挂分支,否则回传链会直接落到函数末尾的 return false。 看 calcInputGradients 的分发:defNeuronBaseOCL 直接把 TargetObject 当 temp 算隐藏层梯度;卷积与注意力类则先 GetPointer(this) 再调对应对象的 calcInputGradients。defNeuronMLMHAttentionOCL 被标黄补进同一 switch,逻辑与多头注意力一致——拿 this 指针交给 mlat.calcInputGradients(temp) 算输入梯度。 UpdateInputWeights 更粗暴:base/conv/attention/MHAttention/MLMHAttention 五个 case 全部并案处理,统一转 temp 后调 updateInputWeights(temp)。若你自己在 EA 里扩展了新层类型却忘了在这里加 case,反向传播会在运行时静默返回 false,训练损失不下降但 MT5 不会报错。 验证方法:在策略测试器里给神经网络加一层 MLMHAttention,断点打在 switch 的 case defNeuronMLMHAttentionOCL,观察是否进入 updateInputWeights;外汇与贵金属品种下跑此类模型属高风险,过拟合概率偏高,需以样本外窗口复核。

MQL5 / C++
   case defNeuronBaseOCL:
         temp=TargetObject;
         class="kw">return calcHiddenGradients(temp);
         class="kw">break;
      case defNeuronConvOCL:
         conv=TargetObject;
         temp=GetPointer(this);
         class="kw">return conv.calcInputGradients(temp);
         class="kw">break;
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
         at=TargetObject;
         temp=GetPointer(this);
         class="kw">return at.calcInputGradients(temp);
         class="kw">break;
      case defNeuronMLMHAttentionOCL:
         mlat=TargetObject;
         temp=GetPointer(this);
         class="kw">return mlat.calcInputGradients(temp);
         class="kw">break;
      }
class=class="str">"cmt">//---
   class="kw">return class="kw">false;
   }
class="type">bool CNeuronBaseOCL::UpdateInputWeights(CObject *SourceObject)
  {
  if(CheckPointer(SourceObject)==POINTER_INVALID)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  CNeuronBaseOCL *temp=NULL;
  class="kw">switch(SourceObject.Type())
    {
      case defNeuronBaseOCL:
      case defNeuronConvOCL:
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         temp=SourceObject;
         class="kw">return updateInputWeights(temp);
         class="kw">break;
    }
class=class="str">"cmt">//---
  class="kw">return class="kw">false;
  }

「深网与浅网在同一数据集上的训练拉锯」

我们用两款改造过的 EA 来验证新关注区架构:Fractal_OCL_AttentionMLMH(5 层、8 个自关注目击者)和 v2(12 层、12 个自关注目击者),底子来自前一篇的 EA,只换了关注区域模块。 测试喂入的数据没变:EURUSD 的 H1 图表,取最后 20 根烛条历史,神经网络直接读这组序列。外汇与贵金属杠杆品种波动剧烈,回测结论仅代表历史样本,实盘存在显著高风险。 参数多的模型明显更吃训练时间。头几轮迭代里,浅网结果更稳;但拉长训练后,v2 的数值反超——第 33 个迭代起,它的误差跌破浅网水平并持续压住。 遗漏形态率也走类似曲线。v2 开局漏掉超 50% 形态,到第 27 个迭代收敛在 3–5%;浅网虽然曲线平滑,却恒定漏 10–16%。两款在形态预测准确率上几乎打平,都落在 22–23%。

轻量关注机制够用就好

前面几节把关注神经元类拆完,思路其实和 OpenAI 的 GPT 架构同宗,只是我们没去复刻完整版。 完整 GPT 的训练和推理都要吃掉大量时间与算力,个人在 MT5 上跑不现实。 但就「在神经网络里养一个能下单的机器人」这个目标而言,手头这个精简关注类已经能顶事。 真要验证,把前面给的神经元前向代码接进你的 EA 样本回路,用 EURUSD 的 M15 历史跑一遍分类,就能看出它比普通全连接省多少冗余权重。

◍ 顺着这些线索继续深挖

想自己搭一套能在 MT5 里跑的行情识别模型,光看指标公式不够,得把神经网络那套工程链路摸透。上面列的几篇材料把从基础训练测试、卷积与循环结构,到 OpenCL 多线程、学习率实验和自适应优化都拆开了讲,属于同一技术脉络的延伸阅读。 其中「学习率实验」和「自适应优化方法」两篇最贴近实盘:学习率设错,回测里 90% 样本准确率可能掉到 60% 以下;Adam 类优化器在 5000 根 H1 蜡烛的小样本上收敛速度通常比纯 SGD 快 3~5 倍,但过拟合概率也更高。 外汇与贵金属杠杆高、跳空频繁,这类模型只适合做概率辅助,不能直接当信号源。建议先在本机用文中 OpenCL 方案跑通一个卷积网络,再决定要不要接实盘 tick 数据。

「随包附带的代码与文件」

这套 GTP 架构分类网络实盘框架,发布时一并给了 5 个文件:两个 EA(Fractal_OCL_AttentionMLMH.mq5 带 5 个关注层,v2 扩到 12 个关注层,输出层均为 3 神经元做分类),NeuroNet.mqh 负责建网,NeuroNet.cl 是 OpenCL 核,NN.chm 为编译好的本地帮助。 有用户在最新 MT5 上跑原码训练 GBPUSD,误差不降反升,从约 50% 爬到 70% 且后续 epoch 无改善;另一例是 include 内 OpenCL 配置失败,排查后确认其机器 GPU 不支持该核,仅 CPU 可用——换设备前先确认 OpenCL 支持层级。 压缩包 MQL5.zip 约 2306 KB,解压后直接丢进 MT5 的 MQL5 目录即可编译验证。外汇与贵金属杠杆高、模型误判会放大亏损,任何神经网络信号都只作概率参考,实盘前务必用策略测试器跑历史数据。

MQL5 / C++
class="macro">#define FileName      Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_"+IntegerToString(HistoryBars,class="num">3)+StringSubstr(__FILE__,class="num">0,StringFind(__FILE__,".",class="num">0))
class="macro">#define FileName      Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_"+IntegerToString(HistoryBars)+StringSubstr(__FILE__,class="num">0,StringFind(__FILE__,".",class="num">0))

常见问题

每层先按头拆分查询、键、值张量,各头独立算相似度再拼接;你可直接复用文中卷积前向与打分内核,把多周期特征送进不同头验证权重分布。
内核里用共享临时缓冲存相似度、避免每头开独立大矩阵;反向传播阶段靠张量回收复用梯度内存,照抄回收路径能压住峰值占用。
可以。小布盯盘的 AIGC 已内置这类张量推演诊断,打开对应品种页即可让它替你核对各头梯度回流是否异常,你只管看结论。
优先查梯度卷积的归一化除零和张量回收后的野指针;文中梯度回流路径标了每步缩放因子,对照调 epsilon 即可避坑。
不用手调,内核按头数自动算偏移;但若你改了头维度,需同步改输出层内核的步长参数,否则特征会错位。