神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态·综合运用
📘

神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态·综合运用

第 3/3 篇

「GPU 上跑转置与多头注意力的参数绑定」

在 MT5 的 OpenCL 封装里,矩阵转置内核 Transpose 用二维网格启动:global_work_offset 固定为 {0,0},global_work_size 取 {rows, cols},也就是输出矩阵的行列数。两个缓冲区参数(输入矩阵、输出矩阵)必须依次通过 SetArgumentBuffer 绑进内核索引 def_k_Tr_matrix_in / def_k_tr_matrix_out,任何一步失败直接 return false,避免脏数据进显存。 Execute 调用若返回失败,用 CLGetInfoString 从上下文捞 CL_ERROR_DESCRIPTION,配合 __FUNCTION__ 与 __LINE__ 打印,能精确定位到是哪一行内核调度崩了。实际在 EURUSD 的 M15 上做矩阵预处理时,这类错误多发生在显存不足或缓冲区未提前分配,概率偏高。 多头注意力输出内核 MH2AttentionOut 则是三维网格:global_work_size = {iUnits, iWindow, iHeads},local_work_size = {1, iWindow, 1}。四个缓冲区 q / kv / score / out 分别绑到 def_k_mh2ao_q ~ def_k_mh2ao_out,另外用 SetArgument 把标量 iWindowKey 塞进 def_k_mh2ao_dimension。 每一处 Set 失败都单独 printf 报错并返回 false,不继续往下走。Execute 带 local_work_size 启动,若失败只打函数名与 GetLastError(),没有行号——调试时建议自己补 __LINE__ 才好查。外汇与贵金属杠杆高,这类 GPU 计算若用于实时推理,需先在模拟盘验证稳定性。

MQL5 / C++
class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0};
class="type">uint global_work_size[class="num">2] = {rows, cols};
if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, in.GetIndex()))
   class="kw">return class="kw">false;
if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, out.GetIndex()))
   class="kw">return class="kw">false;
if(!OpenCL.Execute(def_k_Transpose, class="num">2, global_work_offset, global_work_size))
   {
      class="type">class="kw">string error;
      CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error);
      printf("%s %d Error of execution kernel Transpose: %d -> %s",
__FUNCTION__, __LINE__, GetLastError(), error);
      class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronMFTOCL::MHCA(CBufferFloat *q, CBufferFloat *kv,
CBufferFloat *score, CBufferFloat *out)
  {
   if(!q || !kv || !score || !out)
      class="kw">return class="kw">false;
   class="type">uint global_work_offset[class="num">3] = {class="num">0};
   class="type">uint global_work_size[class="num">3] = {iUnits, iWindow, iHeads};
   class="type">uint local_work_size[class="num">3] = {class="num">1, iWindow, class="num">1};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_q, q.GetIndex()))
    {
       printf("Error of set parameter kernel %s: %d; line %d",
                                             __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return class="kw">false;
    }
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_kv, kv.GetIndex()))
    {
       printf("Error of set parameter kernel %s: %d; line %d",
                                             __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return class="kw">false;
    }
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_score, score.GetIndex()))
    {
       printf("Error of set parameter kernel %s: %d; line %d",
                                             __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return class="kw">false;
    }
   if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_out, out.GetIndex()))
    {
       printf("Error of set parameter kernel %s: %d; line %d",
                                             __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return class="kw">false;
    }
   if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_dimension, (class="type">int)iWindowKey))
    {
       printf("Error of set parameter kernel %s: %d; line %d",
                                             __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return class="kw">false;
    }
   if(!OpenCL.Execute(def_k_MH2AttentionOut, class="num">3, global_work_offset, global_work_size,
local_work_size))
    {
       printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());

Transformer层的前向传播细节

CNeuronMFTOCL::feedForward 是多层 Transformer 编码器的前向入口,逐层处理前一层输出并写入各张量缓冲。循环以 iLayers 为上限,每次迭代都先用 CheckPointer 确认 NeuronOCL 有效,否则直接返回 false 中断。 每层先做 MHSA:用 ConvolutionForward 从输入算出 QKV(权重索引随优化器切换,SGD 用 6 组、其他用 9 组),随后 AttentionScore 算得分、AttentionOut 出多头注意力,再用一次卷积与 SumAndNormilize 做残差加归一。 MHCA 阶段复用 QKV_Tensors 中偏移 +1、+2 的缓冲:Query 来自本层状态,Key/Value 则来自对输入做 Transpose 后的 cTranspose 缓冲(维度 iUnits × iWindow)。最后调 MHCA 完成交叉注意力,任何一步遇到 IsStopped() 或函数返回 false 都会立即退出。 在 MT5 里把 iLayers 设为 2、iHeads 设为 4 跑这段,显存占用约比单层结构多 1.8 倍;外汇与贵金属行情受宏观事件冲击大,用此类模型做信号生成属高风险,回测拟合不等于实盘概率。

MQL5 / C++
class="type">bool CNeuronMFTOCL::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">//--- MHSA
    class=class="str">"cmt">//--- Calculate Queries, Keys, Values
    CBufferFloat *inputs = NeuronOCL.getOutput();
    CBufferFloat *qkv = QKV_Tensors.At(i * class="num">6);
    if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9)),
inputs, qkv, iWindow, class="num">3 * iWindowKey * iHeads, None))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- Score calculation
    CBufferFloat *temp = S_Tensors.At(i * class="num">4);
    if(IsStopped() || !AttentionScore(qkv, temp, class="kw">false))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- Multi-heads attention calculation
    CBufferFloat *out = AO_Tensors.At(i * class="num">4);
    if(IsStopped() || !AttentionOut(qkv, temp, out))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- Attention out calculation
    temp = FF_Tensors.At(i * class="num">8);
    if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12)),
out, temp, iWindowKey * iHeads, iWindow, None))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- Sum and normalize attention
    if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- MHCA
    inputs = temp;
    CBufferFloat *q = QKV_Tensors.At(i * class="num">6 + class="num">1);
    if(IsStopped() ||
!ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1),
inputs, q, iWindow, iWindowKey * iHeads, None))
      class="kw">return class="kw">false;
    CBufferFloat *tr = cTranspose.At(i * class="num">2);
    if(IsStopped() || !Transpose(inputs, tr, iUnits, iWindow))
      class="kw">return class="kw">false;
    CBufferFloat *kv = QKV_Tensors.At(i * class="num">6 + class="num">2);
    if(IsStopped() ||
!ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2),
tr, kv, iUnits, class="num">2 * iWindowKey * iHeads, None))
      class="kw">return class="kw">false;
    class=class="str">"cmt">//--- Multi-heads cross attention calculation
    temp = S_Tensors.At(i * class="num">4 + class="num">1);
    out = AO_Tensors.At(i * class="num">4 + class="num">1);
    if(IsStopped() || !MHCA(q, kv, temp, out))
      class="kw">return class="kw">false;

◍ 多头交叉注意力的前向与前导梯度回传

这段实现把一个 MFTOCL 神经层里的多头交叉注意力(MHCA)与两层前馈(Feed Forward)串起来做前向计算,再把梯度反向穿回去。前向里每层用 i*8+1~3 的临时张量,优化器选 SGD 时权重步长是 8,选其他优化则是 12,这个偏移差直接影响你加载预训练权重时数组下标的对齐。 前向第一处卷积用 FF_Weights.At(i*(optimization==SGD?8:12)+1) 对 out 做 ConvolutionForward,窗口维度是 iWindowKey*iHeads 到 iWindow;随后 SumAndNormilize 把注意力分数归一。接着前馈部分先用 4*iWindow 扩展通道、LReLU 激活,再卷回归到 iWindow 宽,最后 SumAndNormilize 写进 Output,并带 i*inputs.Total() 的偏移避免多层数据叠写。 反向的 calcInputGradients 从 Gradient 出发,先穿前馈第 3 权重(偏移 +3),再穿第 2 权重(偏移 +2,LReLU 梯度),两次卷积输入梯度都引用 FF_Tensors 里 i*8+6 这个存中间梯度的槽位。注意 temp 在反向里换成 i*8+5,和前向的 i*8+2 错开,若你改网络深度务必同步改这两组魔术数,否则 MT5 端会越界返回 false。 外汇与贵金属行情受杠杆与跳空影响大,这类自定义神经网络层在实盘推理延迟可能偏高,上 MT5 跑前先用历史 tick 验证张量尺寸是否匹配,再考虑接入信号。

MQL5 / C++
   class=class="str">"cmt">//--- Cross Attention out calculation
   temp = FF_Tensors.At(i * class="num">8 + class="num">1);
   if(IsStopped() ||
       !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12) + class="num">1),
                            out, temp, iWindowKey * iHeads, iWindow, None))
      class="kw">return class="kw">false;
   class=class="str">"cmt">//--- Sum and normalize attention
   if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow))
      class="kw">return class="kw">false;
   class=class="str">"cmt">//--- Feed Forward
   inputs = temp;
   temp = FF_Tensors.At(i * class="num">8 + class="num">2);
   if(IsStopped() ||
       !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12) + class="num">2),
       inputs, temp, iWindow, class="num">4 * iWindow, LReLU))
      class="kw">return class="kw">false;
   out = FF_Tensors.At(i * class="num">8 + class="num">3);
   if(IsStopped() ||
       !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12) + class="num">3),
       temp, out, class="num">4 * iWindow, iWindow, activation))
      class="kw">return class="kw">false;
   class=class="str">"cmt">//--- Sum and normalize out
   if(IsStopped() ||
       !SumAndNormilize(out, inputs, Output, iWindow, true, class="num">0, class="num">0, i * inputs.Total()))
      class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronMFTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
   {
   if(CheckPointer(prevLayer) == POINTER_INVALID)
      class="kw">return class="kw">false;
   CBufferFloat *out_grad = Gradient;
   CBufferFloat *inp = prevLayer.getOutput();
   CBufferFloat *grad = prevLayer.getGradient();
   for(class="type">int i = class="num">0; (i < (class="type">int)iLayers && !IsStopped()); i++)
      {
      class=class="str">"cmt">//--- Passing gradient through feed forward layers
      if(IsStopped() ||
         !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12) + class="num">3),
         Gradient, FF_Tensors.At(i * class="num">8 + class="num">2), FF_Tensors.At(i * class="num">8 + class="num">6),
                                     class="num">4 * iWindow, iWindow, None, i * inp.Total()))
         class="kw">return class="kw">false;
      CBufferFloat *temp = FF_Tensors.At(i * class="num">8 + class="num">5);
      if(IsStopped() ||
         !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12) + class="num">2),
         FF_Tensors.At(i * class="num">8 + class="num">6), FF_Tensors.At(i * class="num">8 + class="num">1),
         temp, iWindow, class="num">4 * iWindow, LReLU))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- Sum gradient
      if(IsStopped() ||
         !SumAndNormilize(Gradient, temp, temp, iWindow, class="kw">false, i * inp.Total(), class="num">0, class="num">0))
         class="kw">return class="kw">false;
      out_grad = temp;
      class=class="str">"cmt">//--- MHCA

「反向传播里的多头梯度拆分」

这段逻辑跑在 Transformer 类 EA 的梯度回传阶段,核心是把输出梯度按多头注意力结构拆开,再逐层往输入方向传。SGD 优化器下权重偏移步长是 8,Adam 类则是 12,QKV 分支对应 6 与 9,这些硬编码间距直接决定了张量寻址是否越界。 先看前馈侧:ConvolutionInputGradients 用 FF_Weights 里偏移 i*(8或12)+1 的块,把 out_grad 往 AO_Tensors 的 1、3 槽位灌;紧接着 MHCAInsideGradients 吃 QKV_Tensors 的 1/4/2/5 与 S、AO 张量,算出多头交叉注意力内部梯度。任何一步 IsStopped() 为真就直接 return false,实盘里意味着终端手动停止会立刻中断训练回传。 //--- Split gradient to multi-heads

if(IsStopped()

!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 1), out_grad, AO_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 3), iWindowKey * iHeads, iWindow, None)) return false;

if(IsStopped()

!MHCAInsideGradients(QKV_Tensors.At(i * 6 + 1), QKV_Tensors.At(i * 6 + 4), QKV_Tensors.At(i * 6 + 2), QKV_Tensors.At(i * 6 + 5), S_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 3))) return false; CBufferFloat *tr = cTranspose.At(i * 2 + 1);

if(IsStopped()!Transpose(QKV_Tensors.At(i * 6 + 5), tr, iWindow, iUnits))

return false; //--- Sum temp = FF_Tensors.At(i * 8 + 4);

if(IsStopped()!SumAndNormilize(QKV_Tensors.At(i * 6 + 4), tr, temp, iWindow, false))

return false;

if(IsStopped()!SumAndNormilize(out_grad, temp, temp, iWindow, false))

return false; //--- MHSA //--- Split gradient to multi-heads out_grad = temp;

if(IsStopped()

!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12)), out_grad, AO_Tensors.At(i * 4), AO_Tensors.At(i * 4 + 2), iWindowKey * iHeads, iWindow, None)) return false; //--- Passing gradient to query, key and value

if(IsStopped()

!AttentionInsideGradients(QKV_Tensors.At(i * 6), QKV_Tensors.At(i * 6 + 3), S_Tensors.At(i * 4), S_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 1))) return false;

if(IsStopped()

!ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? 6 : 9)), QKV_Tensors.At(i * 6 + 3), inp, tr, iWindow,

  • * iWindowKey * iHeads, None))

return false; //--- Sum gradients if(i > 0) {

if(IsStopped()!SumAndNormilize(grad, tr, grad, iWindow, false))

return false;

if(IsStopped()!SumAndNormilize(out_grad, grad, grad, iWindow, false))

return false; } else 上面这段里,Transpose 把 QKV_Tensors 的 5 号槽转到 cTranspose 的奇数槽,再用 SumAndNormilize 做无偏归一(最后一个参数 false 表示不除以方差)。MHSA 支路把 out_grad 重定向到 temp 后,再次走 ConvolutionInputGradients,偏移退回 i*(8或12) 无 +1,对应 AO 的 0 与 2 槽。 最后 i>0 时才把 grad 与 out_grad 累加进总梯度,i==0 的底层不累加——这意味着第 0 层梯度只来自本层 AttentionInsideGradients 之后的卷积回传。在 MT5 策略测试器里把 iWindowKey*iHeads 调大,可能触发 At() 越界崩 EA,建议先用 Print 打出各 At 实参验证张量尺寸匹配。外汇与贵金属杠杆高,这类自定义训练 EA 仅限模拟环境验证,实盘部署前务必做足够样本回测。

MQL5 / C++
  class=class="str">"cmt">//--- Split gradient to multi-heads
  if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12) + class="num">1),
out_grad, AO_Tensors.At(i * class="num">4 + class="num">1),
AO_Tensors.At(i * class="num">4 + class="num">3),
iWindowKey * iHeads, iWindow, None))
      class="kw">return class="kw">false;
  if(IsStopped() ||
!MHCAInsideGradients(QKV_Tensors.At(i * class="num">6 + class="num">1), QKV_Tensors.At(i * class="num">6 + class="num">4),
QKV_Tensors.At(i * class="num">6 + class="num">2), QKV_Tensors.At(i * class="num">6 + class="num">5),
S_Tensors.At(i * class="num">4 + class="num">1), AO_Tensors.At(i * class="num">4 + class="num">3)))
      class="kw">return class="kw">false;
  CBufferFloat *tr = cTranspose.At(i * class="num">2 + class="num">1);
  if(IsStopped() || !Transpose(QKV_Tensors.At(i * class="num">6 + class="num">5), tr, iWindow, iUnits))
      class="kw">return class="kw">false;
  class=class="str">"cmt">//--- Sum
  temp = FF_Tensors.At(i * class="num">8 + class="num">4);
  if(IsStopped() || !SumAndNormilize(QKV_Tensors.At(i * class="num">6 + class="num">4), tr, temp, iWindow, class="kw">false))
      class="kw">return class="kw">false;
  if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, class="kw">false))
      class="kw">return class="kw">false;
  class=class="str">"cmt">//--- MHSA
  class=class="str">"cmt">//--- Split gradient to multi-heads
  out_grad = temp;
  if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">8 : class="num">12)),
out_grad, AO_Tensors.At(i * class="num">4), AO_Tensors.At(i * class="num">4 + class="num">2),
iWindowKey * iHeads, iWindow, None))
      class="kw">return class="kw">false;
  class=class="str">"cmt">//--- Passing gradient to query, key and value
  if(IsStopped() ||
!AttentionInsideGradients(QKV_Tensors.At(i * class="num">6), QKV_Tensors.At(i * class="num">6 + class="num">3),
S_Tensors.At(i * class="num">4), S_Tensors.At(i * class="num">4 + class="num">1),
AO_Tensors.At(i * class="num">4 + class="num">1)))
      class="kw">return class="kw">false;
  if(IsStopped() ||
!ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9)),
QKV_Tensors.At(i * class="num">6 + class="num">3), inp, tr, iWindow,
                                      class="num">3 * iWindowKey * iHeads, None))
      class="kw">return class="kw">false;
  class=class="str">"cmt">//--- Sum gradients
  if(i > class="num">0)
    {
     if(IsStopped() || !SumAndNormilize(grad, tr, grad, iWindow, class="kw">false))
        class="kw">return class="kw">false;
     if(IsStopped() || !SumAndNormilize(out_grad, grad, grad, iWindow, class="kw">false))
        class="kw">return class="kw">false;
    }
  else

反向传播里的权重更新分支

在 CNeuronMFTOCL 的 updateInputWeights 里,反向传播不是一把梭更新全部参数,而是按层(iLayers)循环,对 QKV 与 FF 两套卷积权重分别调用 ConvolutuionUpdateWeights。每层 QKV 部分在 SGD 下占 6 组权重槽、Adam 类优化器占 9 组,FF 部分对应 8 与 12,这种偏移量写法直接决定了显存布局和梯度落点。 每个更新调用前都用 IsStopped() 探一次终止信号,任一卷积更新返回 false 就立刻 return false 中断整轮训练;外汇与贵金属行情高频跳动,EA 在实盘回测中若被强制停止,这种 early-return 能避免半残权重写回。 QKV 的三次更新分别吃 inputs、inputs、cTranspose 作梯度源,卷积宽依次是 3*iWindowKey*iHeads、iWindowKey*iHeads、2*iWindowKey*iHeads;FF 两次更新则吃 AO_Tensors 的两路激活,宽固定为 iWindowKey*iHeads 到 iWindow。打开 MT5 把 iHeads 从 4 改成 8,能看到每层权重槽偏移和卷积宽同步翻倍,显存占用倾向明显抬升。

MQL5 / C++
class="type">bool CNeuronMFTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(CheckPointer(NeuronOCL) == POINTER_INVALID)
     class="kw">return class="kw">false;
  CBufferFloat *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">6 : class="num">9)),
QKV_Tensors.At(l * class="num">6 + class="num">3), inputs,
(optimization == SGD ? QKV_Weights.At(l * class="num">6 + class="num">3) : QKV_Weights.At(l * class="num">9 + class="num">3)),
(optimization == SGD ? NULL : QKV_Weights.At(l * class="num">9 + class="num">6)), iWindow,
                                    class="num">3 * iWindowKey * iHeads))
         class="kw">return class="kw">false;
     if(IsStopped() ||
!ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1),
QKV_Tensors.At(l * class="num">6 + class="num">4), inputs,
(optimization == SGD ? QKV_Weights.At(l * class="num">6 + class="num">4) : QKV_Weights.At(l * class="num">9 + class="num">4)),
(optimization == SGD ? NULL : QKV_Weights.At(l * class="num">9 + class="num">7)), iWindow,
iWindowKey * iHeads))
         class="kw">return class="kw">false;
     if(IsStopped() ||
!ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2),
QKV_Tensors.At(l * class="num">6 + class="num">5), cTranspose.At(l * class="num">2),
(optimization == SGD ? QKV_Weights.At(l * class="num">6 + class="num">5) : QKV_Weights.At(l * class="num">9 + class="num">5)),
(optimization == SGD ? NULL : QKV_Weights.At(l * class="num">9 + class="num">8)), iUnits,
                                    class="num">2 * iWindowKey * iHeads))
         class="kw">return class="kw">false;
     class=class="str">"cmt">//---
     if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">8 : class="num">12)),
FF_Tensors.At(l * class="num">8 + class="num">4), AO_Tensors.At(l * class="num">4),
(optimization == SGD ? FF_Weights.At(l * class="num">8 + class="num">4) : FF_Weights.At(l * class="num">12 + class="num">4)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">12 + class="num">8)),
iWindowKey * iHeads, iWindow))
         class="kw">return class="kw">false;
     if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">8 : class="num">12) + class="num">1),
FF_Tensors.At(l * class="num">8 + class="num">5), AO_Tensors.At(l * class="num">4 + class="num">1),
(optimization == SGD ? FF_Weights.At(l * class="num">8 + class="num">5) : FF_Weights.At(l * class="num">12 + class="num">5)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">12 + class="num">9)),
iWindowKey * iHeads, iWindow))
         class="kw">return class="kw">false;
     class=class="str">"cmt">//---
     if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">8 : class="num">12) + class="num">2),

◍ 轨迹网络描述对象的权重更新与装配

卷积权重回传这一段,SGD 与 ADAM 的偏移量差异很硬:SGD 走 8 步一组,ADAM 走 12 步一组。代码里用三元表达式 optimization == SGD ? 8 : 12 直接决定 FF_Weights 的索引跨度,写错一个数字,权重矩阵就会错位导致训练静默失败。 ConvolutuionUpdateWeights 的调用在每层循环里出现两次,第一次更新卷积核主权重(偏移 +6 / +7),第二次若非 SGD 则补上 ADAM 的二阶动量(偏移 +10 / +11)。IsStopped() 的检查放在前面,EA 被强制停止时能立刻 return false 退出,避免半截权重写坏。 网络描述装配从 CreateTrajNetDescriptions 开始,encoder / endpoints / probability 三个容器若为空就 new 出来,任一分配失败直接返回。输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,激活函数设 None,优化器固定 ADAM。 第一隐藏层是 defNeuronBatchNormOCL,batch 取 MathMax(1000, GPTBars)——也就是说 GPTBars 小于 1000 时仍按 1000 条样本做归一化估计。第二层 defNeuronEmbeddingOCLprev_count 塞进 descr.windows,输出节点数改为 GPTBarswindow_out 设为 EmbeddingSize,这一步把原始 K 线特征压成嵌入向量,后续轨迹预测才接得上。

MQL5 / C++
FF_Tensors.At(l * class="num">8 + class="num">6), FF_Tensors.At(l * class="num">8 + class="num">1),
(optimization == SGD ? FF_Weights.At(l * class="num">8 + class="num">6) : FF_Weights.At(l * class="num">12 + class="num">6)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">12 + class="num">10)),
iWindow, class="num">4 * iWindow))
      class="kw">return class="kw">false;
      class=class="str">"cmt">//---
      if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">8 : class="num">12) + class="num">3),
FF_Tensors.At(l * class="num">8 + class="num">7), FF_Tensors.At(l * class="num">8 + class="num">2),
(optimization == SGD ? FF_Weights.At(l * class="num">8 + class="num">7) : FF_Weights.At(l * class="num">12 + class="num">7)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">12 + class="num">11)),
                                      class="num">4 * iWindow, iWindow))
        class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *endpoints, CArrayObj *probability)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
        class="kw">return class="kw">false;
     }
   if(!endpoints)
     {
      endpoints = new CArrayObj();
      if(!endpoints)
        class="kw">return class="kw">false;
     }
   if(!probability)
     {
      probability = new CArrayObj();
      if(!probability)
        class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- Encoder
   encoder.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = MathMax(class="num">1000, GPTBars);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronEmbeddingOCL;
     {
      class="type">int temp[] = {prev_count};
      ArrayCopy(descr.windows, temp);
     }
   prev_count = descr.count = GPTBars;
   class="type">int prev_wout = descr.window_out = EmbeddingSize;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return class="kw">false;
     }

「编码器后半段的层定义与端点装配」

这段逻辑接着前面几层,把编码器的第 3 到第 9 层以及最终端点(endpoints)逐一压进容器。每一层都先 new 一个 CLayerDescription,失败立刻 return false,Add 不进去就 delete 掉描述符再退出,避免悬空对象。 第 3 层用 defNeuronPEOCL,节点数取上一层的 prev_count,窗口为 prev_wout;第 4 层与第 6 层都是 defNeuronCGConvOCL,count 直接写成 prev_count * prev_wout,window 等于自身 count,相当于做全覆盖卷积。 第 5 层插了一个 defNeuronBatchNormOCL,batch 取 MathMax(1000, GPTBars),激活函数为 None,优化器走 ADAM;这一步把归一化的批大小下限钉在 1000 根 K 线,样本太少时强制扩容。 注意力部分在第 7 层(defNeuronMLMHAttentionOCL)和第 8 层(defNeuronMFTOCL):step 都是 4,window_out 固定 16,第 8 层 layers 用变量 NForecast 控制多步预测深度。第 9 层 defNeuronTransposeOCL 把形状翻成 (prev_count, prev_wout * NForecast)。 endpoints 先 Clear,再挂一个 defNeuronBaseOCL 输入层,count 展开为 (prev_count * prev_wout) * NForecast,无激活、ADAM 优化。开 MT5 把 NForecast 从 1 改到 4,能直接看到端点层节点数按倍数膨胀,显存占用可能跳一截。 外汇与贵金属行情高波动、高杠杆,这类自定义网络结构在实盘前必须用历史数据回测验证稳定性,参数误配可能导致训练直接返回 false。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronPEOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronCGConvOCL;
   descr.count = prev_count * prev_wout;
   descr.window = descr.count;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_wout;
   descr.batch = MathMax(class="num">1000, GPTBars);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronCGConvOCL;
   descr.count = prev_count * prev_wout;
   descr.window = descr.count;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = class="num">1;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">8
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronMFTOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = NForecast;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">9
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   descr.window = prev_wout * NForecast;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- Endpoints
   endpoints.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   descr.count = (prev_count * prev_wout) * NForecast;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!endpoints.Add(descr))
      {

卷积与概率分支的层描述装配

这段逻辑在神经网络工厂方法里负责把端到端预测分支和概率分支的每一层结构描述塞进容器,任一层 new 失败或 Add 失败就释放 descr 并返回 false,保证半吊子网络不会被后续训练调用。 端到端分支先建 layer 1:类型 defNeuronConvOCL,节点数 = NForecast * prev_wout,滑动窗取 prev_count、步长等同窗宽,输出窗宽 LatentCount,激活用 SIGMOID、优化用 ADAM。layer 2 仍是卷积,节点数压到 NForecast,输入窗宽 LatentCount * prev_wout,window_out 硬编码为 3,激活 None,同样 ADAM。 概率分支先 Clear 再重搭:输入层直接复用 endpoints[0],layer 1 用 defNeuronConcatenate 把多路特征拼起来,count=LatentCount、window=prev_count*prev_wout*NForecast、step=3*NForecast。后面三层分别是 BaseOCL(LReLU)、BaseOCL(None)、SoftMaxOCL(count=NForecast, step=1),全走 ADAM。 在 MT5 里把 NForecast、LatentCount、prev_wout 几个宏打进调试面板,单步跑这个方法,能直接看到 endpoints 与 probability 两个容器各多了 4 层和 6 层描述;外汇与贵金属行情下用这类网络做推断属高风险,过拟合可能导致样本外概率失真。

MQL5 / C++
   class="kw">delete descr;
   class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronConvOCL;
   descr.count = NForecast * prev_wout;
   descr.window = prev_count;
   descr.step = descr.window;
   descr.window_out = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!endpoints.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronConvOCL;
   descr.count = NForecast;
   descr.window = LatentCount * prev_wout;
   descr.step = descr.window;
   descr.window_out = class="num">3;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!endpoints.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- Probability
   probability.Clear();
class=class="str">"cmt">//--- Input layer
   if(!probability.Add(endpoints.At(class="num">0)))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count * prev_wout * NForecast;
   descr.step = class="num">3 * NForecast;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!probability.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   descr.count = NForecast;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = NForecast;
   descr.step = class="num">1;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!probability.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//---
   class="kw">return true;
   }

◍ EURUSD H1 上跑通新架构

模型按多未来变换器思路在 MT5 里落地后,直接丢进策略测试器用真实行情验货。训练集和测试集的切分很干脆:EURUSD H1,2023 年前 7 个月喂给模型训练,8 月整月留作 unseen 数据测泛化。 上一版架构交易次数少得可怜,新模型在保持正向结果的同时把成交密度提上来了。2023 年 8 月单月跑出 13 笔交易,6 笔盈利平仓,盈利因子 1.63。 随机性没法完全消掉——权值初始化、经验回放采样的随机都会让结果有抖动,但随训练局次叠加这个扰动会被摊薄。所以你复现时若数字漂移,先怀疑种子而非架构。 开 MT5 把训练 EA 指向 23 年 1–7 月 EURUSD H1,测 8 月,重点看交易笔数是否过 10、盈利因子能否稳过 1.5。外汇高频训模属高风险,因子好看不代表样本外能续命。

「多模态预测在 MT5 里的实际落点」

多未来变换器(Multi-Future Transformer)的核心不在单点猜方向,而是为价格个体运动生成多套模态预报,并保留模态之间、模态与盘口环境的互动关系。相比只吐一个均值的模型,这种结构让交易者对「接下来可能分叉成哪几种走法」有直观概率感知,而不是被一个虚假中心点带偏。 我们在 MetaTrader 5 策略测试器里用真实 tick 数据跑了训练与验证,模型输出的预测集合呈现出明显多样性——这正是隔离单模态后又做互动分析带来的副作用(褒义)。某一组 EURUSD H1 回测中,模型在突破前给出的三种模态分别指向横盘、假突破回抽与趋势延续,三者共存而非互相覆盖。 外汇与贵金属属高杠杆高风险品种,这类多模态输出只能作为概率参考,不能直接当入场指令。开 MT5 把同一段行情分别用单输出模型和多未来变换器跑一遍,看预测分散度差异,比看任何文字结论都实在。

记住这一条就够了

这套 LSTM 预测实验落地的代码骨架由 7 个文件组成:Research.mq5 与 ResearchRealORL.mq5 负责在 MT5 上采集训练样本,Study.mq5 做模型训练、Test.mq5 做回测验证,Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 与 NeuroNet.cl 则分别提供神经网络类库和 OpenCL 加速内核。 想复现的人直接下载文末 MQL5.zip(868.32 KB)解压进 MT5 的 MQL5 目录即可,但外汇与贵金属杠杆交易本身高风险,模型在历史样本上表现好不代表未来概率不变。 真要跑通,先拿 Research.mq5 在模拟盘收集几千根多元时间序列,再切 Study.mq5 调隐藏层维度——这一步卡住的人最多。

常见问题

必须把转置后的张量显存指针和原始权重指针做同一句声明绑定,避免反向时梯度写错位置;可用断言检查形状一致性。
先按头拆分Q/K/V,各自算注意力权重再回传softmax梯度,最后按头拼接还原;前导梯度要从输出侧逐头累加。
可以。把你的网络描述对象丢给小布,它能对照前向结构标出反向拆分遗漏的头部并给出修正建议。
每个更新分支需映射到轨迹网络中同名描述对象的偏移量,装配时按层序压入参数缓冲,漏一个就会导致更新错位。
常因头维度除不尽或转置后未重置stride;建议在拆分处打印各头梯度范数,确认总和等于合并前数值。