神经网络变得简单(第 77 部分):交叉协方差变换器(XCiT)·综合运用
📘

神经网络变得简单(第 77 部分):交叉协方差变换器(XCiT)·综合运用

第 3/3 篇

XCiT 注意力在 OpenCL 里的落地写法

下面这段是 CNeuronXCiTOCL 类里把交叉协方差注意力(XCiT)搬上 GPU 的核心调用链。先由 XCiT() 方法把 qkv、score、out 三个显存缓冲绑给 OpenCL 内核,再按三维网格派发:global_work_size 设为 {iWindowKey, iUnits, iHeads},local_work_size 则是 {iWindowKey, iUnits, 1},意味着每个 head 占一个独立计算单元。 内核执行若失败会打印 Error of execution kernel 并取 CL_ERROR_DESCRIPTION 回显,方便在 MT5 终端直接抓异常;正常则返回 true。注意 iWindowKey、iUnits、iHeads 都是类成员,改它们等于改注意力窗口与头数,外汇与贵金属行情的高波动下过小窗口可能漏掉关键波段。 feedForward() 里循环 iLayers 层,首层输入来自 NeuronOCL.getOutput(),其后层复用 FF_Tensors 的 4*i-2 号张量;卷积前向生成 qkv 时窗口参数写死为 3*iWindowKey*iHeads,优化器选 SGD 则权重步长按 2 取,否则按 3 取。跑通后接 XCiT(qkv, temp, out) 算得分与输出,任一环节 IsStopped() 为真立即退出,避免 EA 卸载时显存操作悬空。 想验证的话,在 MT5 策略测试器里把 iWindowKey 从默认 8 调到 16,观察显存占用与回测耗时是否线性上涨,能直观确认该内核的局部内存栅栏(CLK_LOCAL_MEM_FENCE)确实按窗口维度同步。

MQL5 / C++
q[u][d + count] = class="num">0;
    }
    barrier(CLK_LOCAL_MEM_FENCE);
   }
   while(count > class="num">1);
   if((cur_r + u) < ls_d)
      score[(cur_r + u)*dimension * heads + dimension * h + d] /= q[u][class="num">0];
   barrier(CLK_LOCAL_MEM_FENCE);
   }
   class="type">int shift_out = dimension * (u * heads + h) + d;
   class="type">int shift_s = dimension * (heads * d + h);
   class="type">int shift_v = dimension * (heads * (u * class="num">3 + class="num">2) + h);
   class="type">float sum = class="num">0;
   for(class="type">int i = class="num">0; i < dimension; i++)
      sum += qkv[shift_v + i] * score[shift_s + i];
   out[shift_out] = sum;
   }
class="type">bool CNeuronXCiTOCL::XCiT(CBufferFloat *qkv, CBufferFloat *score, CBufferFloat *out)
  {
   if(!OpenCL || !qkv || !score || !out)
      class="kw">return false;
   class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0};
   class="type">uint global_work_size[class="num">3] = {iWindowKey, iUnits, iHeads};
   class="type">uint local_work_size[class="num">3] = {iWindowKey, iUnits, class="num">1};
   if(!OpenCL.SetArgumentBuffer(def_k_XCiTFeedForward, def_k_XCiTff_qkv, qkv.GetIndex()))
      class="kw">return false;
   if(!OpenCL.SetArgumentBuffer(def_k_XCiTFeedForward, def_k_XCiTff_score, score.GetIndex()))
      class="kw">return false;
   if(!OpenCL.SetArgumentBuffer(def_k_XCiTFeedForward, def_k_XCiTff_out, out.GetIndex()))
      class="kw">return false;
   ResetLastError();
   if(!OpenCL.Execute(def_k_XCiTFeedForward, class="num">3, global_work_offset, global_work_size,
local_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      class="type">class="kw">string error;
      CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error);
      Print(error);
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronXCiTOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(NeuronOCL) == POINTER_INVALID)
      class="kw">return false;
   for(class="type">uint i = class="num">0; (i < iLayers && !IsStopped()); i++)
     {
      class=class="str">"cmt">//--- Calculate Queries, Keys, Values
      CBufferFloat *inputs = (i == class="num">0 ? NeuronOCL.getOutput() : FF_Tensors.At(class="num">4 * i - class="num">2));
      CBufferFloat *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 false;
      class=class="str">"cmt">//--- Score calculation
      CBufferFloat *temp = S_Tensors.At(i * class="num">2);
      CBufferFloat *out = AO_Tensors.At(i * class="num">2);
      if(IsStopped() || !XCiT(qkv, temp, out))

◍ XCiT 前向块与梯度核的收口逻辑

这段代码片段处在 Transformer 类模型在 MT5 端的推理收尾段:每一层循环里先跑 LPI(局部感知卷积),再做注意力求和归一,最后走 Feed Forward。注意 LPI 权重偏移量随优化器切换——SGD 时步长是 5,Adam 类则是 7,直接决定 cLPI_Weights.At() 的寻址位置。 循环末尾 iBatchCount++ 后返回 true,说明单批次前向已跑通;若中途任意 IsStopped() 或子函数返回 false,立即退出避免 EA 占用 GPU 资源。外汇与贵金属行情跳空频繁,这类重算力推理在实盘可能拖慢 tick 响应,须在小布盯盘里限制批次大小。 下方 __kernel void XCiTInsideGradients 是 OpenCL 梯度核,用 get_global_id(0/1/2) 取三维线程索引,units = get_global_size(0) 拿到 q 维总长度。要验证这套寻址,可在 MT5 策略测试器里把 optimization 切到 SGD 跑一遍,观察 cLPI.At(i*6+1) 与权重数组是否越界。

MQL5 / C++
      class="kw">return false;
      class=class="str">"cmt">//--- Sum and normalize attention
      if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true))
         class="kw">return false;
      class=class="str">"cmt">//--- LPI
      inputs = out;
      temp = cLPI.At(i * class="num">6);
      if(IsStopped() || !ConvolutionForward(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7)),
inputs, temp, iLPIWindow, iHeads, LReLU, iLPIStep))
         class="kw">return false;
      out = cLPI.At(i * class="num">6 + class="num">1);
      if(IsStopped() || !BatchNorm(temp, cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1),
out))
         class="kw">return false;
   temp = out;
   out = cLPI.At(i * class="num">6 + class="num">2);
   if(IsStopped() ||!ConvolutionForward(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2),
temp, out, class="num">2 * iHeads, class="num">2, None, iHeads))
      class="kw">return false;
         class=class="str">"cmt">//--- Sum and normalize attention
         if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true))
            class="kw">return false;
      class=class="str">"cmt">//--- Feed Forward
      inputs = out;
      temp = FF_Tensors.At(i * class="num">4);
      if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6)),
inputs, temp, iWindow, class="num">4 * iWindow, LReLU))
            class="kw">return false;
      out = FF_Tensors.At(i * class="num">4 + class="num">1);
      if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6) + class="num">1),
temp, out, class="num">4 * iWindow, iWindow, activation))
            class="kw">return false;
         class=class="str">"cmt">//--- Sum and normalize out
         if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true))
            class="kw">return false;
      }
   iBatchCount++;
class=class="str">"cmt">//---
   class="kw">return true;
   }
__kernel class="type">void XCiTInsideGradients(__global class="type">float *qkv, __global class="type">float *qkv_g,
                              __global class="type">float *scores,
                              __global class="type">float *gradient)
   {
class=class="str">"cmt">//--- init
   const class="type">int q = get_global_id(class="num">0);
   const class="type">int d = get_global_id(class="num">1);
   const class="type">int h = get_global_id(class="num">2);
   const class="type">int units = get_global_size(class="num">0);

「XCiT 注意力反向传播里的梯度拆解」

在 MT5 的 OpenCL 内核里,Cross-Covariance Image Transformer 的反向传播把 Q、K、V 的梯度分开算,而不是像标准注意力那样直接套 softmax 链式法则。上面这段内核代码先用 get_global_size(1) 和 get_global_size(2) 拿到维度与头数,再按 heads*3*q+h 的偏移把 Q/K/V 在扁平缓冲里的起点算出来,这种内存排布能让 GPU 一次访存连续命中。 Value 的梯度最直白:把门控梯度 gradient[shift_g+i] 和对应 score 做点积,写回 qkv_g[shift_v+d]。Query 的梯度则嵌套了两层循环,内层对 dimension 做 scores[v] * val * gradient[g+v*dim] * ((k==v)-sc) 的累加,本质是在算 score 对 Q 的雅可比再乘上游梯度,维度一高这层循环就是主要耗时点。 Key 的梯度逻辑类似,但偏移改用 scr*dimension*heads,遍历的是 window key 方向上的所有 score。实际在显卡上跑,若 iWindowKey=64、iUnits=128、iHeads=4,global_work_size 就是 64*128*4=32768 个线程并发,显存带宽不够时梯度核容易掉速。 XCiTInsideGradients 这个宿主方法只做参数绑定和内核发射:把 qkv、qkvg、score、aog 四个缓冲的索引通过 SetArgumentBuffer 塞进 def_k_XCiTInsideGradients,再 Execute 三维网格。任何缓冲空指针或 SetArgumentBuffer 失败都会直接返回 false,调用层应当检查返回值而非信任静默执行。外汇与贵金属市场波动剧烈,这类自定义神经层若用于 EA 信号,请先在历史数据上验证稳定性,实盘存在较高回撤风险。

MQL5 / C++
  const class="type">int dimension = get_global_size(class="num">1);
  const class="type">int heads = get_global_size(class="num">2);
  const class="type">int shift_q = dimension * (heads * class="num">3 * q + h);
  const class="type">int shift_k = dimension * (heads * (class="num">3 * q + class="num">1) + h);
  const class="type">int shift_v = dimension * (heads * (class="num">3 * q + class="num">2) + h);
  const class="type">int shift_g = dimension * (heads * q + h);
  class="type">int shift_score = dimension * h;
  class="type">int step_score = dimension * heads;
class=class="str">"cmt">//--- Calculating Value&class="macro">#x27;s gradients
  class="type">float sum = class="num">0;
  for(class="type">int i = class="num">0; i < dimension; i ++)
      sum += gradient[shift_g + i] * scores[shift_score + d + i * step_score];
  qkv_g[shift_v + d] = sum;
class=class="str">"cmt">//--- Calculating Query&class="macro">#x27;s gradients
  class="type">float grad = class="num">0;
  class="type">float val = qkv[shift_v + d];
  for(class="type">int k = class="num">0; k < dimension; k++)
    {
      class="type">float sc_g = class="num">0;
      class="type">float sc = scores[shift_score + k];
      for(class="type">int v = class="num">0; v < dimension; v++)
        sc_g += scores[shift_score + v] * val * gradient[shift_g + v * dimension] *
((class="type">float)(k == v) - sc);
      grad += sc_g * qkv[shift_k + k];
    }
  qkv_g[shift_q] = grad;
class=class="str">"cmt">//--- Calculating Key&class="macro">#x27;s gradients
  grad = class="num">0;
  class="type">float out_g = gradient[shift_g];
  for(class="type">int scr = class="num">0; scr < dimension; scr++)
    {
      class="type">float sc_g = class="num">0;
      class="type">int shift_sc = scr * dimension * heads;
      class="type">float sc = scores[shift_sc + d];
      for(class="type">int v = class="num">0; v < dimension; v++)
        sc_g += scores[shift_sc + v] * out_g * qkv[shift_v + v] * ((class="type">float)(d == v) - sc);
      grad += sc_g * qkv[shift_q + scr];
    }
  qkv_g[shift_k + d] = grad;
  }
class="type">bool CNeuronXCiTOCL::XCiTInsideGradients(CBufferFloat *qkv, CBufferFloat *qkvg,
CBufferFloat *score, CBufferFloat *aog)
  {
  if(!OpenCL || !qkv || !qkvg || !score || !aog)
      class="kw">return false;
  class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0};
  class="type">uint global_work_size[class="num">3] = {iWindowKey, iUnits, iHeads};
  if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_qkv, qkv.GetIndex()))
      class="kw">return false;
  if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_qkv_g, qkvg.GetIndex()))
      class="kw">return false;
  if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_scores,score.GetIndex()))
      class="kw">return false;
  if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_gradient,aog.GetIndex()))
      class="kw">return false;
  ResetLastError();
  if(!OpenCL.Execute(def_k_XCiTInsideGradients, class="num">3, global_work_offset, global_work_size))
    {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      class="kw">return false;
    }
class=class="str">"cmt">//---

反向传播里梯度怎么穿过 XCiT 层

CNeuronXCiTOCL::calcInputGradients 负责把输出梯度沿前馈与 LPI 两条通路回传。函数先检查 prevLayer 指针有效性,无效直接返回 false,避免空指针导致 MT5 终端崩溃。 循环从最顶层 iLayers-1 向下跑到 0,每次都嵌了 IsStopped() 判断——这是实盘 EA 跑神经网络训练时必须的,不然用户点停止策略仍可能卡在 OpenCL 内核里。 前馈部分先用 ConvolutionInputGradients 透传梯度,权重偏移按优化器分 SGD 与其他:SGD 时步长 4,其他为 6,这个差异直接影响你改优化算法后缓冲区布局是否越界。 LPI 支路里卷积步长出现 2*iHeads 与 2 的组合,BatchNormInsideGradient 接在后面做归一化回传;若返回 false,说明某层张量尺寸和你设置的 iHeads 不匹配。 最后 XCiTInsideGradients 把梯度分到 QKV 与 S 张量。整段没有任何收益暗示,外汇与贵金属模型训练属高风险,参数不对只会让回测曲线更难看。

MQL5 / C++
class="type">bool CNeuronXCiTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
  if(CheckPointer(prevLayer) == POINTER_INVALID)
    class="kw">return false;
  CBufferFloat *out_grad = Gradient;
class=class="str">"cmt">//---
  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">4:class="num">6)+class="num">1),
out_grad, FF_Tensors.At(i * class="num">4),
FF_Tensors.At(i * class="num">4 + class="num">2), class="num">4 * iWindow,
iWindow, None))
       class="kw">return false;
     CBufferFloat *temp = cLPI.At(i * class="num">6 + class="num">5);
     if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6)),
FF_Tensors.At(i * class="num">4 + class="num">1), cLPI.At(i * class="num">6 + class="num">2), temp,
iWindow, class="num">4 * iWindow, LReLU))
       class="kw">return false;
     class=class="str">"cmt">//--- Sum and normalize gradients
     if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false))
       class="kw">return false;
     out_grad = temp;
     class=class="str">"cmt">//--- Passing gradient through LPI
     if(IsStopped() || !ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2),
 temp, cLPI.At(i * class="num">6 + class="num">1), cLPI.At(i * class="num">6 + class="num">4),
 class="num">2 * iHeads, class="num">2, None, class="num">0, iHeads))
       class="kw">return false;
     if(IsStopped() || !BatchNormInsideGradient(cLPI.At(i * class="num">6), cLPI.At(i * class="num">6 + class="num">3),
 cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1),
 cLPI.At(i * class="num">6 + class="num">1), cLPI.At(i * class="num">6 + class="num">4), LReLU))
       class="kw">return false;
     if(IsStopped() || !ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7)),
 cLPI.At(i * class="num">6 + class="num">3), AO_Tensors.At(i * class="num">2),
 AO_Tensors.At(i * class="num">2 + class="num">1), iLPIWindow, iHeads,
 None, class="num">0, iLPIStep))
       class="kw">return false;
     temp = AO_Tensors.At(i * class="num">2 + class="num">1);
     class=class="str">"cmt">//--- Sum and normalize gradients
     if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false))
       class="kw">return false;
     out_grad = temp;
     class=class="str">"cmt">//--- Passing gradient to query, key and value
     if(IsStopped() || !XCiTInsideGradients(QKV_Tensors.At(i * class="num">2), QKV_Tensors.At(i * class="num">2 + class="num">1),
 S_Tensors.At(i * class="num">2), temp))
       class="kw">return false;

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

在 XCiT 类神经层的反向阶段,首层与后续层的输入张量取法不同:i==0 时直接取前层输出与梯度,其余层则从 FF_Tensors 按 i*4-3 / i*4-1 偏移抽取。这个偏移规律对应每层的 4 张缓存(输入、梯度、输出、临时),写错一处就会让梯度回传错位。 优化器选择会改变权重数组的步长。SGD 下 QKV_Weights 每层占 2 个缓冲,Adam 类占 3 个;cLPI_Weights 在 SGD 占 5 个、非 SGD 占 7 个。代码里所有 At(l*(optimization==SGD?2:3)) 这类表达式就是在按优化器动态跳地址,手动改结构时务必同步改步长常数。 更新流程对每个 layer 串行调用四类卷积/归一化更新:QKV 卷积、cLPI 主卷积、BatchNorm、cLPI 输出卷积。任意一步前若 IsStopped() 为真(MT5 终端点停止或超时)立即返回 false,避免占用 GPU 资源。实盘跑这类自定义层时,建议在策略测试器里单步看哪一层先触发停止,往往能暴露张量尺寸不匹配。 外汇与贵金属行情下用此类深度学习层做信号,杠杆与滑点会放大过拟合风险,回测亮眼不等于实盘概率占优,上真实账户前先用历史 tick 跑通权重更新不报错。

MQL5 / C++
CBufferFloat *inp = NULL;
if(i == class="num">0)
  {
   inp = prevLayer.getOutput();
   temp = prevLayer.getGradient();
  }
else
  {
   temp = FF_Tensors.At(i * class="num">4 - class="num">1);
   inp = FF_Tensors.At(i * class="num">4 - class="num">3);
  }
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 false;
class=class="str">"cmt">//--- Sum and normalize gradients
if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow))
   class="kw">return false;
if(i > class="num">0)
   out_grad = temp;
   }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronXCiTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(CheckPointer(NeuronOCL) == POINTER_INVALID)
   class="kw">return 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">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 false;
   if(IsStopped() ||
!ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7)),
cLPI.At(l * class="num">6 + class="num">3), AO_Tensors.At(l * class="num">2),
(optimization==SGD ? cLPI_Weights.At(l*class="num">5+class="num">3):cLPI_Weights.At(l*class="num">7+class="num">3)),
(optimization==SGD ? NULL : cLPI_Weights.At(l * class="num">7 + class="num">5)),
iLPIWindow, iHeads, iLPIStep))
     class="kw">return false;
   if(IsStopped() ||
!BatchNormUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1),
cLPI.At(l * class="num">6 + class="num">4)))
     class="kw">return false;
   if(IsStopped() ||
!ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2),
cLPI.At(l * class="num">6 + class="num">5), cLPI.At(l * class="num">6 + class="num">1),
(optimization==SGD ? cLPI_Weights.At(l*class="num">5+class="num">4):cLPI_Weights.At(l*class="num">7+class="num">4)),
(optimization==SGD ? NULL : cLPI_Weights.At(l * class="num">7 + class="num">6)),
                                    class="num">2 * iHeads, class="num">2, iHeads))
     class="kw">return false;
   if(IsStopped() ||

「卷积权重回传与轨迹网结构搭建」

在反向传播阶段,卷积层权重更新按优化器分两套索引。SGD 模式下每层占 4 个权重槽,ADAM 占 6 个;代码里用 l*(optimization==SGD?4:6) 做基址偏移,偏置与二阶动量分别落在 +2、+4 位置,窗口长度统一用 4*iWindow 覆盖。 若 IsStopped() 触发或 ConvolutuionUpdateWeights 返回 false,函数立即 return false 中断训练,避免 MT5 终端退出时悬空计算。 CreateTrajNetDescriptions 负责装配编码器:输入层用 defNeuronBaseOCL,节点数 = HistoryBars*BarDescr,优化器强制 ADAM;随后接 BatchNorm 层,batch 取 MathMax(1000, GPTBars),实测 GPTBars 小于 1000 时仍按 1000 攒批。 Embedding 层把 prev_count 长序列压到 GPTBars 个 token,window_out = EmbeddingSize,这一步直接决定后续注意力能捕捉的隐状态维度,调 EmbeddingSize 参数可明显改变显存占用。

MQL5 / C++
if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">4 : class="num">6)),
FF_Tensors.At(l * class="num">4 + class="num">2), cLPI.At(l * class="num">6 + class="num">2),
(optimization==SGD ? FF_Weights.At(l*class="num">4+class="num">2):FF_Weights.At(l*class="num">6+class="num">2)),
(optimization==SGD ? NULL : FF_Weights.At(l * class="num">6 + class="num">4)),
iWindow, class="num">4 * iWindow))
      class="kw">return false;
class=class="str">"cmt">//---
      if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">4 : class="num">6) + class="num">1),
FF_Tensors.At(l * class="num">4 + class="num">3), FF_Tensors.At(l * class="num">4),
(optimization==SGD ? FF_Weights.At(l*class="num">4+class="num">3):FF_Weights.At(l*class="num">6+class="num">3)),
(optimization==SGD ? NULL : FF_Weights.At(l * class="num">6 + class="num">5)),
class="num">4 * iWindow, iWindow))
        class="kw">return false;
      inputs = FF_Tensors.At(l * class="num">4 + class="num">1);
   }
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 false;
     }
   if(!endpoints)
     {
      endpoints = new CArrayObj();
      if(!endpoints)
        class="kw">return false;
     }
   if(!probability)
     {
      probability = new CArrayObj();
      if(!probability)
        class="kw">return false;
     }
class=class="str">"cmt">//--- Encoder
   encoder.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
     class="kw">return 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 false;
     }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
     class="kw">return 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 false;
     }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
     class="kw">return 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;

编码器堆叠里的后段层定义

这段逻辑紧接前面已建好的前两层,从 layer 3 开始往 encoder 里继续塞描述符。每一层都先 new 一个 CLayerDescription,失败就直接 return false,避免半吊子对象挂进容器。 layer 3 用 defNeuronPEOCL,节点数和窗口直接吃上一层传下来的 prev_count 与 prev_wout;layer 4 和 layer 6 都是 defNeuronCGConvOCL,count 算成 prev_count * prev_wout,window 设成自身 count,相当于把整层拉平做卷积。 layer 5 插了个 defNeuronBatchNormOCL 做批归一,batch 取 MathMax(1000, GPTBars),激活函数关成 None,优化器走 ADAM;这一步的 batch 下限 1000 意味着小样本回测时也会按千根 K 线对齐。 后面 layer 7 的 defNeuronXCiTOCL 把 step 设 4、window_out 设 3、layers 设 1;layer 8 的 defNeuronMFTOCL 才是真正出预报的,window_out 拉到 16、layers 用 NForecast 控制多步预测深度。 layer 9 用 defNeuronTransposeOCL 做维度转置,window 乘上 NForecast 把多步摊开。最后 endpoints.Clear() 清掉旧端点,再 new 一个 defNeuronBaseOCL 作为 Input layer 收口——外汇与贵金属行情下用这套结构跑预测,参数敏感度高,实盘前务必在 MT5 策略测试器里用小资金验证。

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

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

这段逻辑在神经网络描述对象上连续挂了两套子结构:endpoints 负责卷积预测分支,probability 负责概率输出分支,两者都靠 CLayerDescription 动态 new 出来再 Add 进容器。 先看 endpoints 的三层卷积。第 0 层用 defNeuronConvOCL,节点数算出来是 (prev_count * prev_wout) * NForecast,激活函数留 None,优化器统一 ADAM;若 Add 失败就 delete 并回 false。第 1 层 count 变成 NForecast * prev_wout,window 取 prev_count、step 等于 window、window_out 设 LatentCount,激活改 SIGMOID。第 2 层 count 回到 NForecast,window 扩到 LatentCount * prev_wout,window_out 硬编码为 3,激活又置 None。 probability 分支从清空开始,先直接把 endpoints 的第 0 层描述引用挂进去,不 new。随后第 1 层用 defNeuronConcatenate,count = LatentCount,window = prev_count * prev_wout * NForecast,step = 3 * NForecast,激活 SIGMOID。第 2、3 层是 defNeuronBaseOCL,分别保持 LatentCount 与 NForecast 节点,激活用 LReLU 和 None。最后第 4 层上 defNeuronSoftMaxOCL,count = NForecast、step = 1,把输出压成概率分布。 任何一次 Add 返回否都走 delete + return false,只有全部挂完才 return true。在 MT5 里把 NForecast、LatentCount、prev_count、prev_wout 打印出来,就能核对每层张量维度是否对得上,外汇与贵金属模型训练属高风险,维度错配会直接让后续推理失效。

MQL5 / C++
  descr.count = (prev_count * prev_wout) * NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!endpoints.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
      class="kw">return 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 false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
      class="kw">return 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 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 false;
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
      class="kw">return 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 false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!probability.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!probability.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">4
  if(!(descr = new CLayerDescription()))
      class="kw">return 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 false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }

「XCiT 模型在 EURUSD H1 的实测表现」

沿用前作的智能系统与已采集数据集,我们把旧文件 “MFT.bd” 改名为 “XCiT.bd” 即可直接训练;若手头没有数据,先按《利用过去的经验解决新问题》从真实信号采集,再用 Experts\XCiT\Research.mq5 做随机验算补足,最后跑 Study.mq5 训模型。 模型在 EURUSD H1 上以默认指标参数训练,数据取自 2023 年前 7 个月。训练迭代相同的情况下,时间开销较之前降了将近 2%,这是交叉协方差结构带来的直接收益。 有效性用 2023 年 8 月行情评估,该段未进训练集且紧接训练区。结果和前一篇文章接近,但交易次数略增的同时盈利因子有抬升——外汇与贵金属杠杆高、回测外推有失效可能,MT5 里换品种复跑才能确认边际改善是否稳定。

一层替换带来的训练耗时变化

把 XCiT 的交叉协方差注意力层塞进原有模型,我们只动了其中一层,其余结构原封不动。在 MT5 用真实历史数据跑训练时,相同训练迭代次数下,训练时间略有减少——这不是数量级飞跃,但说明该架构在序列建模上的开销确实更克制。 作者侧实验覆盖图像分类、对象检测、语义分割,精度与可扩展性在长序列小令牌规模下成立;移到金融序列后,效率改善虽小,却可能暗示更好的泛化倾向。 外汇与贵金属行情高波动、高杠杆,文中程序仅作方法验证,未对实盘撮合与滑点做优化。开 MT5 用你自己的品种复跑一遍,比信任何结论都实在。

◍ 记住这一条就够了

这套 LSTM 多元时间序列预测方案落到 MT5 实盘前,先认清楚交付物边界:文末附带的 MQL5.zip(927.41 KB)里只含 7 个源文件——Research.mq5、ResearchRealORL.mq5、Study.mq5、Test.mq5 四个 EA 分别管样本采集、Real-ORL 采集、训练与测试,Trajectory.mqh 与 NeuroNet.mqh 定义状态结构和建网类,NeuroNet.cl 则是 OpenCL 端算力内核。 它们解决的是「用历史数据训出预测模型」的工程问题,不替你下单,也不含任何资金曲线承诺。外汇与贵金属杠杆高、滑点跳空频繁,直接挂 Trajectory 结构里的状态去跑回测,可能和实盘偏差明显。 真要验证,先开 MT5 把 Research.mq5 丢进策略测试器跑一小段历史,确认样本写入正常,再谈后续调参。

常见问题

先核对局部工作大小是否整除特征维度,再确认交叉协方差矩阵缓存是否按批大小预分配,最后看转置核的步长是否与通道数匹配。
多半是交叉协方差归一化分母接近零导致梯度除零放大,可在协方差计算后加一个极小值 epsilon 再做倒数。
可以,小布能直接加载你的核代码做静态分析,自动标出前向块与反向梯度核的收口位置并提示权重更新分支的潜在越界。
把卷积回传与轨迹网分别放在独立命令队列,权重更新前用事件同步屏障,避免两个分支的局部内存互相覆盖。
用一组固定随机输入跑前向存快照,再跑反向比对数值梯度与解析梯度误差,误差超 1e-4 就回头查收口条件判断。