神经网络变得简单(第 83 部分):构象时空连续关注度转换器算法·综合运用
📘

神经网络变得简单(第 83 部分):构象时空连续关注度转换器算法·综合运用

第 3/3 篇

注意力反向传播里的梯度累加细节

这段 OpenCL 内核在做多头注意力机制的反向传播,核心是把 score 梯度往 Key 向量上回传。外层先取当前 query 位置的归一化分数 scr,再对 total 个目标位 g 做加权求和,权重来自 qkv 与输出梯度的点积。 score_grad 的更新用到了 (q==pos) 的指示函数减去 scr,最后统一除以 sqrt(dimension) 做缩放——这一步漏掉会让梯度量级随维度漂移,MT5 里用 CL 内核跑 Transformer 类信号模型时尤其要核对。 Key 梯度那段对 q==0 单独处理,是因为序列首位置没有前驱可累加,只能直接赋值;其余位置走 += 累加。下面把第一段核心循环拆开看。 HiddenGradientTimeDerivative 内核则按 pos / variable / head 三维并行,shift 计算里写死 3*heads*variables*dimension,说明每个时间步存了 Q、K、V 三组向量;dQ/dt 部分用相邻时间步的 dqkv_g 差分估计时间导数,pos 在边界时只取单侧。

MQL5 / C++
class="type">float scr = score[shift_score + q * step_score];
for(class="type">int g = class="num">0; g < total; g++)
  {
    class="type">float grad = class="num">0;
    for(class="type">int d = class="num">0; d < dimension; d++)
      grad += qkv[shift_value + d] * out_g[shift_out + d + g * step_out];
    score_grad += score[shift_score + q * step_score + g] * grad * ((class="type">float)(q == pos) - scr);
  }
score_grad /= sqrt((class="type">float)dimension);
class=class="str">"cmt">//--- Key gradient
for(class="type">int d = class="num">0; d < dimension; d++)
  {
    if(q == class="num">0)
      {
       dqkv_g[shift_key + d] = score_grad * qkv[shift_query + q * step + d];
       qkv_g[shift_key + d] = score_grad * dqkv[shift_query + q * step + d];
      }
    else
      {
       qkv_g[shift_key + d] += score_grad * dqkv[shift_query + q * step + d];
       dqkv_g[shift_key + d] += score_grad * qkv[shift_query + q * step + d];
      }
  }
}

「注意力层里 Q 与 K 梯度的差分平滑」

在 Conformer 注意力反向传播中,Q 和 K 的梯度并非直接回传,而是先按时间邻域做一阶差分再平均。以 Q 为例:遍历序列位置,若左侧有邻居就累加 dqkv_g[shift_query + i] - 左侧值,右侧有邻居则累加右侧值 - 当前值,count 记录有效邻域数,最后 grad 除以 count 得到局部斜率,写回 qkv_g。 K 的处理逻辑对称,但写回时用了 dqkv_g[shift_key + i] + grad 的叠加形式,意味着 Key 梯度在反向时同时保留原始项与差分修正项,这可能让 Key 的更新在长序列上更平滑。 下面这段是 Q/K 梯度差分核的主体循环,shift 为单头维度跨度,pos 为当前时间步,total 为序列总长: { grad += dqkv_g[shift_query + i + shift] - current; count++; } if(count > 0) grad /= count; qkv_g[shift_query + i] += grad; } //--- dK/dt { int count = 0; float grad = 0; float current = dqkv_g[shift_key + i]; if(pos > 0) { grad += current - dqkv_g[shift_key + i - shift]; count++; } if(pos < (total - 1)) { grad += dqkv_g[shift_key + i + shift] - current; count++; } if(count > 0) grad /= count; qkv_g[shift_key + i] += dqkv_g[shift_key + i] + grad; } 外层 AttentionInsideGradients 在 OpenCL 不可用时会直接返回 false,可用时需把 cQKV / cdQKV 的输出与梯度缓冲绑定到 def_k_HiddenGradientContAtt 内核。任一 SetArgumentBuffer 失败就 printf 出错函数名、错误码和行号并退出,这种细粒度报错在调 MT5 神经网络 EA 时很实用——能直接定位是哪一个缓冲没挂上。 外汇与贵金属市场波动剧烈、杠杆风险高,此类模型仅作信号辅助,实盘前务必在策略测试器用历史数据验证梯度逻辑是否如预期收敛。

MQL5 / C++
  {
      grad += dqkv_g[shift_query + i + shift] - current;
      count++;
   }
   if(count > class="num">0)
      grad /= count;
   qkv_g[shift_query + i] += grad;
}
class=class="str">"cmt">//--- dK/dt
   {
    class="type">int count = class="num">0;
    class="type">float grad = class="num">0;
    class="type">float current = dqkv_g[shift_key + i];
    if(pos > class="num">0)
      {
       grad += current - dqkv_g[shift_key + i - shift];
       count++;
      }
    if(pos < (total - class="num">1))
      {
       grad += dqkv_g[shift_key + i + shift] - current;
       count++;
      }
    if(count > class="num">0)
      grad /= count;
    qkv_g[shift_key + i] += dqkv_g[shift_key + i] + grad;
   }

◍ 注意力梯度与时间导数的内核参数装配

在 MT5 的 OpenCL 封装里,每个 kernel 执行前都必须用 SetArgumentBuffer / SetArgument 把显存缓冲区和标量塞进去,否则 GPU 端读到的是野指针。下面这段注意力梯度核先挂了分数缓冲 iScore、注意力输出梯度 cAttentionOut.getGradientIndex(),再把维度标量 iDimension 以 int 强转写入,任何一步失败就 printf 打出函数名、GetLastError() 和 __LINE__ 然后 return false。 Execute 的第三个参数传了 3,代表三维 ND-Range;global_work_offset 固定 {0,0,0},global_work_size 则是 {iCount, iVariables, iHeads} 三轴并行规模。若 Execute 返回 false,只报函数名与错误码,不报行号——这和前面 Set 类错误形成区分,排错时先看有没有行号就能定位是装配段还是发射段。 时间导数梯度核 HGTimeDerivative 的装配逻辑同构:先绑 cQKV.getGradientIndex() 与 cdQKV.getGradientIndex() 两个梯度缓冲,再写 def_k_tddimension 标量,最后用同样的 3 维 offset/size 发射。外汇与贵金属行情下跑这类自研核,显存越界会直接让 EA 在实盘掉线,务必先在策略测试器用历史数据小样本验证再上真仓,杠杆品种高风险。

MQL5 / C++
if(!OpenCL.SetArgumentBuffer(def_k_HiddenGradientContAtt, def_k_hgcascore, iScore))
  {
   printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,
                                                                     GetLastError(), __LINE__);
   class="kw">return false;
  }
if(!OpenCL.SetArgumentBuffer(def_k_HiddenGradientContAtt, def_k_hgcaout_g,
cAttentionOut.getGradientIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,
GetLastError(), __LINE__);
   class="kw">return false;
  }
if(!OpenCL.SetArgument(def_k_HiddenGradientContAtt, def_k_hgcadimension,
                                                                              class="type">int(iDimension)))
  {
   printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,
GetLastError(), __LINE__);
   class="kw">return false;
  }
if(!OpenCL.Execute(def_k_HiddenGradientContAtt, 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">//--- Time Derivative Gradient
  {
   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] = {iCount, iVariables, iHeads};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_HGTimeDerivative, def_k_tdqkv,
cQKV.getGradientIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,
GetLastError(), __LINE__);
   class="kw">return false;
  }
   if(!OpenCL.SetArgumentBuffer(def_k_HGTimeDerivative, def_k_tddqkv,
cdQKV.getGradientIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,
                                                                     GetLastError(), __LINE__);
   class="kw">return false;
  }
   if(!OpenCL.SetArgument(def_k_HGTimeDerivative, def_k_tddimension, class="type">int(iDimension)))
  {
   printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,
                                                                     GetLastError(), __LINE__);
   class="kw">return false;
  }
   if(!OpenCL.Execute(def_k_HGTimeDerivative, class="num">3, global_work_offset, global_work_size))
  {
   printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());

Conformer 反向传播与权重更新的链路拆解

这段 CNeuronConformer 的成员函数把 Conformer 块的反向传播拆成了四条梯度链:前馈、神经 ODE、多头注意力、以及 QKV 投影。任何一步 calcInputGradients 返回 false 就直接中断,说明梯度回传是严格串行的,某一子层显存或维度不匹配会令整层失效。 calcInputGradients 里神经 ODE 部分用 for(int i=2; i>0; i--) 从 cNODE[2] 往 cNODE[0] 倒序回传,而 updateInputWeights 中对应循环是 for(int i=0; i<3; i++) 正序更新,两者指针 prev 的接力方向正好相反,写自定义层时接错顺序会拿到错位梯度。 注意力侧先由 cW0 接收 cAttentionOut 的梯度,再进 AttentionInsideGradients() 处理内部 softmax/scale,最后 cQKV 把梯度交到 prevLayer。三个 SumAndNormilize 调用都带 iDimension 与 false 参数,表明沿特征维做求和归一但不写回原缓冲,调参时改 iDimension 会直接改变梯度尺度。 updateInputWeights 先更新 cQKV 与 cW0,再循环更新 3 个 NODE 与 2 个 FF 层,共 7 个子模块权重。在 MT5 里若想冻结某子层,应在对应 if(!…UpdateInputWeights) 前加开关,而非注释掉整函数,否则前层梯度仍会被 SumAndNormilize 污染。

MQL5 / C++
class="type">bool CNeuronConformer::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
class=class="str">"cmt">//--- Feed Forward Gradient
   if(!cFF[class="num">1].calcInputGradients(GetPointer(cFF[class="num">0])))
      class="kw">return false;
   if(!cFF[class="num">0].calcInputGradients(GetPointer(cNODE[class="num">2])))
      class="kw">return false;
   if(!SumAndNormilize(Gradient, cNODE[class="num">2].getGradient(), cNODE[class="num">2].getGradient(), iDimension,
false))
      class="kw">return false;
class=class="str">"cmt">//--- Neural ODE Gradient
   CNeuronBaseOCL *prev = GetPointer(cNODE[class="num">1]);
   for(class="type">int i = class="num">2; i > class="num">0; i--)
     {
       if(!cNODE[i].calcInputGradients(prev))
          class="kw">return false;
       prev = GetPointer(cNODE[i - class="num">1]);
     }
   if(!cNODE[class="num">0].calcInputGradients(GetPointer(cW0)))
      class="kw">return false;
   if(!SumAndNormilize(cW0.getGradient(), cNODE[class="num">2].getGradient(), cW0.getGradient(),
iDimension, false))
      class="kw">return false;
class=class="str">"cmt">//--- MH Attention Gradient
   if(!cW0.calcInputGradients(GetPointer(cAttentionOut)))
      class="kw">return false;
   if(!AttentionInsideGradients())
      class="kw">return false;
class=class="str">"cmt">//--- Query, Key, Value Graddients
   if(!cQKV.calcInputGradients(prevLayer))
      class="kw">return false;
   if(!SumAndNormilize(cW0.getGradient(), prevLayer.getGradient(), prevLayer.getGradient(),
iDimension, false))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronConformer::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
class=class="str">"cmt">//--- MH Attention
   if(!cQKV.UpdateInputWeights(NeuronOCL))
      class="kw">return false;
   if(!cW0.UpdateInputWeights(GetPointer(cAttentionOut)))
      class="kw">return false;
class=class="str">"cmt">//--- Neural ODE
   CNeuronBaseOCL *prev = GetPointer(cW0);
   for(class="type">int i = class="num">0; i < class="num">3; i++)
     {
       if(!cNODE[i].UpdateInputWeights(prev))
          class="kw">return false;
       prev = GetPointer(cNODE[i]);
     }
class=class="str">"cmt">//--- Feed Forward
   for(class="type">int i = class="num">0; i < class="num">2; i++)
     {
       if(!cFF[i].UpdateInputWeights(prev))
          class="kw">return false;
       prev = GetPointer(cFF[i]);
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }

「状态向量与编码器网络搭建」

强化学习模型先把单根 K 线的原始信息压进状态数组:sState.state[shift+1] 存实体上沿相对开盘价的偏移(high-open),shift+2 存下沿偏移(low-open),shift+3 把 tick_volume 除以 1000 做缩放,后续 shift+4~+8 依次塞入 rsi、cci、atr、macd、sign 五个指标值。这样每根 bar 用 9 个 float 描述,HistoryBars 根 bar 拼起来就是编码器输入维度 HistoryBars*BarDescr。 CreateDescriptions 负责把 encoder/actor/critic 三套网络的结构在内存里建出来。函数开头先判空,任一指针为空就 new 一个 CArrayObj,分配失败直接返回 false,避免后面空指针崩在 GPU 端。 编码器第一层是输入层,类型 defNeuronBaseOCL,节点数等于 HistoryBars*BarDescr,激活函数 None,优化器 ADAM;第二层接 BatchNorm(defNeuronBatchNormOCL),batch 取 MathMax(1000, GPTBars),用来稳训练时的数值分布。 第三层用 Embedding(defNeuronEmbeddingOCL),windows 数组设为 {4,1,1,1,2},输出节点数 GPTBars,window_out 设为 EmbeddingSize/2;第四层 Conv(defNeuronConvOCL)把通道扩到 5 倍、窗口与步长对齐上层 window_out,最终 window_out 拉到 EmbeddingSize。外汇与贵金属杠杆高、滑点跳空频繁,这套结构在实盘前务必用 MT5 策略测试器跑过小样本回测再上。

MQL5 / C++
sState.state[shift + class="num">1] = (class="type">float)(Rates[b].high - open);
sState.state[shift + class="num">2] = (class="type">float)(Rates[b].low - open);
sState.state[shift + class="num">3] = (class="type">float)(Rates[b].tick_volume / class="num">1000.0f);
sState.state[shift + class="num">4] = rsi;
sState.state[shift + class="num">5] = cci;
sState.state[shift + class="num">6] = atr;
sState.state[shift + class="num">7] = macd;
sState.state[shift + class="num">8] = sign;

class="type">bool CreateDescriptions(CArrayObj *encoder, CArrayObj *actor, CArrayObj *critic)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
     {
       encoder = new CArrayObj();
       if(!encoder)
         class="kw">return false;
     }
   if(!actor)
     {
       actor = new CArrayObj();
       if(!actor)
         class="kw">return false;
     }
   if(!critic)
     {
       critic = new CArrayObj();
       if(!critic)
         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[] = {class="num">4, class="num">1, class="num">1, class="num">1, class="num">2};
       ArrayCopy(descr.windows, temp);
     }
   prev_count = descr.count = GPTBars;
   class="type">int prev_wout = descr.window_out = EmbeddingSize / class="num">2;
   if(!encoder.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 = defNeuronConvOCL;
   descr.count = prev_count * class="num">5;
   descr.step = descr.window = prev_wout;
   prev_wout = descr.window_out = EmbeddingSize;
   if(!encoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }

◍ 编码器与策略网络的分层堆法

这段构建逻辑把编码器(encoder)和策略网络(actor)拆成不同层来组装,每一层都用 CLayerDescription 描述神经元类型、数量与窗口参数,任一层 Add 失败就 delete 并返回 false,避免半吊子模型加载进 MT5。 编码器第 4 层先放一个 PEOCL 神经元层,window 直接取上一层输出窗口的 5 倍(prev_wout * 5);随后用 for 循环叠 5 个 ConformerOCL 层,每层 step=4、layers=5、window_out 绑定 EmbeddingSize,这种堆叠倾向增强局部时序特征的抽取能力。 actor 网络从 AccountDescr 个输入节点起步(激活 None、ADAM 优化),第 1 层映射到 EmbeddingSize 并走 SIGMOID;第 2–4 层是 3 个 CrossAttenOCL 交叉注意力层,units 设为 {1, GPTBars*5}、windows 设为 {EmbeddingSize, EmbeddingSize},window_out=16、step=4,把账户状态与行情序列做跨域对齐。 最后 actor 第 5 层用 SIGMOID 压到 LatentCount 个潜变量输出,作为动作分布。外汇与贵金属杠杆高,这类自定义网络若参数错配,回测曲线可能严重过拟合,上 MT5 前建议先单步打印各层 descr 字段确认维度链不断。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronPEOCL;
   descr.count = prev_count;
   descr.window = prev_wout * class="num">5;
   if(!encoder.Add(descr))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
   for(class="type">int i = class="num">0; i < class="num">5; i++)
      {
       if(!(descr = new CLayerDescription()))
          class="kw">return false;
       descr.type = defNeuronConformerOCL;
       descr.count = prev_count;
       descr.window = prev_wout;
       descr.step = class="num">4;
       descr.window_out = EmbeddingSize;
       descr.layers = class="num">5;
       if(!encoder.Add(descr))
         {
          class="kw">delete descr;
          class="kw">return false;
         }
      }
class=class="str">"cmt">//--- Actor
   actor.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.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 = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">2-class="num">4
   for(class="type">int i = class="num">0; i < class="num">3; i++)
      {
       if(!(descr = new CLayerDescription()))
          class="kw">return false;
       descr.type = defNeuronCrossAttenOCL;
         {
          class="type">int temp[] = {class="num">1, GPTBars * class="num">5};
          ArrayCopy(descr.units, temp);
         }
         {
          class="type">int temp[] = {EmbeddingSize, EmbeddingSize};
          ArrayCopy(descr.windows, temp);
         }
       descr.window_out = class="num">16;
       descr.step = class="num">4;
       descr.activation = None;
       descr.optimization = ADAM;
       if(!actor.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 = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
      {

Actor与Critic网络层的堆叠细节

这段构建逻辑把强化学习的双网络拆得很直白:Actor 在第 6 层用 defNeuronBaseOCL 铺了 2*NActions 个无激活神经元,第 7 层接 defNeuronVAEOCL 做动作潜变量输出,优化器统一挂 ADAM。 Critic 侧从输入层 NActions 起步,第二层压到 EmbeddingSize 并套 SIGMOID;真正吃算力的是 layer 2-4 的循环——连续 3 层 defNeuronCrossAttenOCL,units 设成 {1, GPTBars*5}、windows 设成 {EmbeddingSize, EmbeddingSize},window_out=16、step=4,相当于在时序横截面上做交叉注意力滑动。 任何一层 new CLayerDescription 失败或 Add 返回 false 都会先 delete 再 return false,避免野指针。开 MT5 把 GPTBars 从默认改到 20,units 第二项会跳到 100,显存占用可能明显上升,建议先在策略测试器跑小样本。外汇与贵金属杠杆高,这类模型过拟合后实盘回撤概率偏大,参数别直接照搬。

MQL5 / C++
   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 = defNeuronBaseOCL;
   descr.count = class="num">2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.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 = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- Critic
   critic.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.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 = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">2-class="num">4
   for(class="type">int i = class="num">0; i < class="num">3; i++)
     {
      if(!(descr = new CLayerDescription()))
         class="kw">return false;
      descr.type = defNeuronCrossAttenOCL;
        {
         class="type">int temp[] = {class="num">1, GPTBars * class="num">5};
         ArrayCopy(descr.units, temp);
        }
        {
         class="type">int temp[] = {EmbeddingSize, EmbeddingSize};
         ArrayCopy(descr.windows, temp);
        }
      descr.window_out = class="num">16;
      descr.step = class="num">4;
      descr.activation = None;
      descr.optimization = ADAM;
      if(!critic.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 = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))

「Critic 尾部层装配与训练采样循环」

Critic 网络在层 6 之后继续挂了两层:层 6 用 LatentCount 个 SIGMOID 激活的神经元做隐变量映射,优化器选 ADAM;层 7 直接以 NRewards 个无激活(None)神经元输出奖励估计,同样走 ADAM。任一层 Add 失败就 delete 描述符并返回 false,避免悬空对象拖垮后续训练。 训练函数 Train 里先用 GetProbTrajectories 按 0.9 温度系数抽轨迹概率,再用双重 MathRand 平方归一化挑起始 state,偏移量上限由 Buffer[tr].Total - 2 - PrecoderBars - batch 卡死。batch 固定为 GPTBars + 48,state 小于等于 0 时直接 iter-- 并 continue 跳过本轮。 每个 batch 内从 state 跑到 end(end 取 state+batch 与 Buffer[tr].Total - PrecoderBars 的较小值),先清 Encoder 再做状态前向;Encoder.feedForward 若失败打函数名加行号并置 Stop 跳出。Critic 接 bActions 与前层 Encoder 指针做前向,成功后才把下一帧 rewards 赋给 result、下下帧赋给 target,供后续时序差分更新。外汇与贵金属行情下用这套 RL 结构回测,过拟合与滑点风险偏高,参数须自分样本验证。

MQL5 / C++
   class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!critic.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 = defNeuronBaseOCL;
   descr.count = NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
   vector<class="type">float> result, target;
   class="type">bool Stop = false;
class=class="str">"cmt">//---
   class="type">uint ticks = GetTickCount();
     class="type">int tr = SampleTrajectory(probability);
     class="type">int batch = GPTBars + class="num">48;
     class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) *
(Buffer[tr].Total - class="num">2 - PrecoderBars - batch));
     if(state <= class="num">0)
       {
        iter--;
        class="kw">continue;
       }
     Encoder.Clear();
     class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
     for(class="type">int i = state; i < end; i++)
       {
        bState.AssignArray(Buffer[tr].States[i].state);
        class=class="str">"cmt">//--- State Encoder
        if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false,
(CBufferFloat*)NULL))
          {
           PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
           Stop = true;
           class="kw">break;
          }
        class=class="str">"cmt">//--- Critic
        bActions.AssignArray(Buffer[tr].States[i].action);
        if(bActions.GetIndex() >= class="num">0)
          bActions.BufferWrite();
        if(!Critic.feedForward((CBufferFloat*)GetPointer(bActions), class="num">1, false,
GetPointer(Encoder)))
          {
           PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
           Stop = true;
           class="kw">break;
          }
        result.Assign(Buffer[tr].States[i + class="num">1].rewards);
        target.Assign(Buffer[tr].States[i + class="num">2].rewards);

◍ 账户状态与时间周期特征的策略网络喂入

这段逻辑处在强化学习训练回路里,Critic 完成反向传播后,紧接着要构造给 Actor 用的账户特征向量 bAccount。它先取上一条状态(i-1,越界时取 0)的余额与净值,再算当前相对变化率,把 8 个原始账户字段归一化后压进缓冲区。 时间维度被拆成四条周期正弦/余弦:以 2023 全年秒数(约 31536000s)为基准的年周期、月线 PeriodSeconds(PERIOD_MN1)、周线 PERIOD_W1、日线 PERIOD_D1,分别用 MathSin / MathCos(2πx) 编码,x 为 0 时直接给 0 避免除零。这样 Actor 的前馈输入同时带资金曲线斜率和季节节律。 bAccount.GetIndex()>=0 才执行 BufferWrite(),随后 Actor.feedForward 以 bAccount 为输入、Encoder 为上下文做推理;任一步返回 false 就打印函数名加行号。外汇与贵金属杠杆高,这类特征若用于实盘策略,回测过拟合概率偏大,建议先开 MT5 用脚本打印 bAccount 各维度数值核对归一化范围。

MQL5 / C++
        result = result - target * DiscFactor;
        Result.AssignArray(result);
        Critic.TrainMode(true);
        if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder)) ||
           !Encoder.backPropGradient((CBufferFloat*)NULL))
          {
          PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
          Stop = true;
          class="kw">break;
          }
        class=class="str">"cmt">//--- Policy
        class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
        class="type">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
        bAccount.Clear();
        bAccount.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
        bAccount.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
        bAccount.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
        bAccount.Add(Buffer[tr].States[i].account[class="num">2]);
        bAccount.Add(Buffer[tr].States[i].account[class="num">3]);
        bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
        bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
        bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
        class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
        class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
        bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
        x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
        bAccount.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
        x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
        bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
        x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
        bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
        if(bAccount.GetIndex() >= class="num">0)
           bAccount.BufferWrite();
        class=class="str">"cmt">//--- Actor
        if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false,
                                                            GetPointer(Encoder)))
          {
          PrintFormat("%s -> %d", __FUNCTION__, __LINE__);

训练循环里的断点与进度回显

这段片段处在 Actor-Critic 训练主循环内部,每一轮都先让 Critic 对 Actor 做前向评估,任一环节失败就把 Stop 置 true 并 break,避免错误权重继续回传。 具体看,Critic.feedForward 接收 Actor 与 Encoder 指针,返回 false 时打印函数名与行号后直接退出本轮;Actor.backProp 与 Encoder.backPropGradient 同理,NULL 梯度表示编码器不再向上求梯度。 Critic 切回非训练模式后,用 Result 做反向传播,Actor.backPropGradient 带 -1 步长与 false 标志,Encoder 仍吃空梯度。若耗时超 500 毫秒(GetTickCount 差值),才计算 percent 进度并用 Comment 输出 Actor/Critic 的近期平均误差,精度到 15.8f。 循环结束清掉 Comment,打印最终 Actor 与 Critic 的 getRecentAverageError(10.7f),随后 ExpertRemove 卸载智能交易,整个自学习过程在 MT5 中跑完即停。外汇与贵金属行情高波动,这类离线训练结果仅供策略验证,实盘落地前须自行压力测试。

MQL5 / C++
            Stop = true;
            class="kw">break;
            }
            if(!Critic.feedForward((CNet *)GetPointer(Actor), -class="num">1, (CNet*)GetPointer(Encoder)))
            {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               class="kw">break;
            }
            if(!Actor.backProp(GetPointer(bActions), GetPointer(Encoder)) ||
               !Encoder.backPropGradient((CBufferFloat*)NULL))
            {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               class="kw">break;
            }
            Critic.TrainMode(false);
            if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder)) ||
               !Actor.backPropGradient((CNet *)GetPointer(Encoder), -class="num">1, -class="num">1, false) ||
               !Encoder.backPropGradient((CBufferFloat*)NULL))
            {
               PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
               Stop = true;
               class="kw">break;
            }
            if(GetTickCount() - ticks > class="num">500)
            {
               class="type">class="kw">double percent = (class="type">class="kw">double(i - state) /
((end - state)) + iter) * class="num">100.0 / (Iterations);
               class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Actor",
percent, Actor.getRecentAverageError());
               str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Critic",
percent, Critic.getRecentAverageError());
               Comment(str);
               ticks = GetTickCount();
            }
            }
      }
   Comment("");
class=class="str">"cmt">//---
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor",
Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic",
Critic.getRecentAverageError());
   ExpertRemove();
class=class="str">"cmt">//---
   }

「EURUSD H1 上的真实样本回测」

把前面几篇攒好的样本喂进 MT5 策略测试器,用 2023 年前 7 个月的 EURUSD H1 真实历史做训练,再用同年 8 月的数据做样本外测试,架构和训练算法改过之后单次迭代成本略增,但学习过程比早期版本更稳,迭代次数反而能压下来。 测试模型在 8 月跑了 34 笔交易,18 笔盈利,胜率 52.94%;平均盈利单比平均亏损单高出 52.47%,单笔最大盈利超过同等亏损 2 倍多。 整段测试盈利因子 1.72,净值曲线向上,但最高净值回撤 17.12%、余额回撤 8.96%——外汇品种杠杆高,这种回撤在实盘可能放大,上 MT5 用同样样本复跑前先想清楚仓位。

◍ 把这套变换器搬进 MT5 后的真实边界

前面用 MQL5 把“时空常数关注度变换器”跑通了,训练和测试集都出了盈利曲线,表面看像是把气象领域的连续关注度算法接进了行情序列。但必须说清楚:样本内盈利不等于样本外能活,外汇和贵金属杠杆高、滑点跳空频繁,这类模型在实盘只代表一种概率倾向。 原文作者把神经 ODE 和持续注意力绑在一起,本意是天气预报,我们只是借它的状态演化结构处理价格张量。代码层能复现,不代表参数迁移后还稳。 提醒一句:本文所有程序只是演示方法,不是可跟单信号。真要验证,自己开 MT5 用EURUSD的M15跑一遍训练,看测试集外推折损再决定。

随包附带的七个工程文件

这套 LSTM 多元时间序列预测方案不是只给思路,而是把可跑的源码全部打进 MQL5.zip(1093.34 KB)随文发布。压缩包里按职责拆成七块:Research.mq5 与 ResearchRealORL.mq5 是两个样本收集 EA,前者捞基础样本,后者用 Real-ORL 方法补示例;Study.mq5 管模型训练,Test.mq5 管模型测试;Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网类,NeuroNet.cl 则是 OpenCL 端的算子库。 想在 MT5 里复现,直接把 zip 解到 MQL5 目录对应子夹即可,EA 与类库路径已分好。外汇与贵金属行情高波动、高杠杆,模型在历史集上表现不代表未来概率,上线前务必用 Test.mq5 在品种实况里跑一轮压力验证。 七个文件各管一段链路,改其中任一类库都可能影响整链收敛,调参时一次只动一处。

常见问题

检查每个时间步的梯度是否都加回总梯度,漏掉任一步都会让更新偏小。建议逐时间步打印梯度再求和验证。
常用 3~5 步滑动平均,窗口过大会吞掉突变信号。先用 3 步跑一遍看收敛是否变平。
可以,把模型结构发给小布,它能对照 Conformer 反向传播链路标出梯度断点和参数装配遗漏。
先固定状态向量维度再向上堆编码器,每层输出 shape 打印出来对齐。别在堆完才调维度。
深层梯度被稀释,试试减少堆叠或在中间加残差直连。分层堆法别超过 4 层先测稳定性。