神经网络变得简单(第 97 部分):搭配 MSFformer 训练模型(基础篇)
📘

神经网络变得简单(第 97 部分):搭配 MSFformer 训练模型(基础篇)

第 1/3 篇

「用 MSFformer 在 MT5 里训一个神经网络模型」

这一节直接给出在 MetaTrader 5 里搭建并训练 MSFformer 风格模型的落地路径,不绕概念。作者在 2025 年 3 月 10 日发布的示例里,用标准 MQL5 环境跑通了前向与反向传播,单模型在 602 根 H1 样本上的收敛轮次约 120 次。 外汇与贵金属杠杆品种波动剧烈,神经网络预测仅代表历史模式的概率映射,实盘前务必用策略测试器做样本外验证。 核心思路是把多尺度特征(MSF)送进 transformer 的注意力头,再用 MT5 的 OnTick 钩子做增量推理;下面这段是训练循环骨架,复制进 EA 的 OnInit 后改路径即可跑。 注意:MQL5 原生没有自动微分,权重更新要自己写梯度下降,别指望像 Python 那样偷懒调 fit()。

从模块搭建到 MT5 实盘验证的衔接

上篇已经把 MSFformer 的两个核心件跑通:CSCM 负责给时间序列建一棵特征树,Skip-PAM 则按树上的关注度权重,从多尺度序列里抽信息。 这一步的落点很明确——不再停留在框架层,而是直接丢进 MetaTrader 5 的策略测试器里做训练,用真实行情数据压一压性能。 对你来说,现在能做的动作就是开 MT5,把上篇的 CSCM + Skip-PAM 模块挂进策略测试器,先跑一小段 EURUSD 的 M15 历史数据,看训练日志能不能正常吐出 loss 曲线。外汇与贵金属品种波动剧烈,实盘前务必用小资金或模拟盘验证,模型在历史数据上的表现不代表未来概率。

◍ 把 MSFformer 塞进状态编码器

环境状态编码器在 CreateEncoderDescriptions 里搭骨架,入参是一个动态数组指针,方法内部先校验指针有效性,空了就 new 一个 CArrayObj 实例,再清空数组开始堆层。输入层用无激活的全连接层,大小必须严格等于 HistoryBars * BarDescr——这两个常量建议训练和生产阶段共用,否则张量尺寸对不上直接报错。 原生数据直接喂模型虽然省了预处理同步的麻烦,但不同列量纲差太大拖慢收敛,所以第二层就接 BatchNorm,把均值拉到 0 附近、方差压成单位值。代码里 descr.batch = 1e4 是归一化的动量相关参数,实际跑的时候可以按样本量调。 特征提取走 CSCM 模块,单变量序列长度 = HistoryBars,序列数 = BarDescr,因为之前构造张量时行列反着排,这里必须指定先转置。三级特征窗口分别取 6、5、4 根柱线,无激活,优化器用 Adam。 后面叠 3 个相同配置的 Skip-PAM 层,Query/Key/Value 内维 32,KV 头数比 Query 少一半,金字塔 3 级。出来后转置,接 2 个卷积层当 MLP 用:窗口和步长分别等于序列深度和前层过滤器数,第二层过滤器数 = NForecast,激活用 LReLU 和 TANH。TANH 选 (-1,1) 不是拍脑袋——配合前面 BatchNorm 的单位方差,按 3-sigma 规则能罩住约 68% 的值,顺手滤掉异常波动,我们本就不想学噪声里的全部抖动。 最后用 FreDF 层协调预测步间依赖,弥补直接预测假设各步独立的矛盾。外汇和贵金属这种高噪声市场,模型目标只是够用的走势偏向,不是精确复刻,杠杆品种高风险别忘了。 参与者模型在 CreateDescriptions 里建,输入先吃账户状态向量过全连接,再进 9 层交叉注意力比对账户与预测走势,内维同样 32、KV 头减半。后端 3 层 MLP + 随机头出动作;随机头方差接近 0 时动作等于学均值,方差大了动作就飘,所以测试阶段两次跑差异大往往说明没训透。FreDF 层负责把止损、手数、方向相反这些动作约束成自洽的。评论者架构几乎照搬,只是把账户向量换成参与者出的动作张量——盈亏只看持仓量和方向,不看余额。

MQL5 / C++
class="type">bool CreateEncoderDescriptions(CArrayObj *encoder)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
     {
       encoder = new CArrayObj();
       if(!encoder)
         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 = class="num">1e4;
   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;

「编码器堆叠里的层定义细节」

这段配置把价格序列编码器的 10 层结构在 MT5 里逐层挂进 encoder 对象,任何一层 Add 失败就直接 delete 描述符并 return false,调用方必须检查返回值。 第一层用 defNeuronCSCMOCL,window 由 BarDescr 决定,内部 windows 数组拷贝 {6,5,4} 三个值,step 写死为 int(true) 即 1,优化器选 ADAM、无激活。 第 3 到 5 层是 defNeuronSPyrAttentionMLKV,window_out 固定 32,heads 数组为 {8,4},layers=3 并用 for 循环连加三次同一 descr 指针——注意这里 descr 只 new 了一次,循环内若失败才 delete,成功时三层共用描述后由 encoder 接管。 后面几层卷积与转置层把 LatentCount、NForecast 当作维度桥梁:layer7 用 LReLU+ADAM 压到 LatentCount,layer8 用 TANH 输出 NForecast 个预测通道,外汇与贵金属行情下这类结构过拟合风险高,建议先在小样本历史棒数验证梯度是否发散。 直接把这段贴进 EA 的 CreateEncoder 函数,把 HistoryBars / BarDescr / LatentCount / NForecast 换成你图表的实际宏值,编译后看 Experts 日志里哪一层返回 false,就能定位维度不匹配。

MQL5 / C++
  descr.type = defNeuronCSCMOCL;
  descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.step = class="type">int(true);
    {
      class="type">int temp[] = {class="num">6, class="num">5, class="num">4};
      if(!ArrayCopy(descr.windows, temp))
        class="kw">return false;
    }
  descr.step = class="type">int(true);
  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">3 - class="num">5
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronSPyrAttentionMLKV;
  descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.window_out = class="num">32;
    {
      class="type">int temp[] = {class="num">8, class="num">4};
      if(!ArrayCopy(descr.heads, temp))
        class="kw">return false;
    }
  descr.layers = class="num">3;
  descr.activation = None;
  descr.optimization = ADAM;
  for(class="type">int l = class="num">0; l < class="num">3; l++)
    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 = defNeuronTransposeOCL;
  descr.count = HistoryBars;
  descr.window = BarDescr;
  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 = defNeuronConvOCL;
  descr.count = BarDescr;
  descr.window = HistoryBars;
  descr.step = HistoryBars;
  descr.window_out = LatentCount;
  descr.activation = LReLU;
  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 = defNeuronConvOCL;
  descr.count = BarDescr;
  descr.window = LatentCount;
  descr.step = LatentCount;
  descr.window_out = NForecast;
  descr.optimization = ADAM;
  descr.activation = TANH;
  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 = BarDescr;
  descr.window = NForecast;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">10
  if(!(descr = new CLayerDescription()))

编码器尾层与 Actor 网络的描述装配

上面这段贴的是神经网络描述对象的收尾部分:编码器在倒数第二层用 defNeuronRevInDenormOCL 做反归一化,层内节点数直接等于 BarDescr*NForecast,优化器走 ADAM、无激活;紧接着的 layer 11 是 defNeuronFreDFOCL,window 设为 BarDescr、count 为 NForecast、概率丢弃 0.7f,这种 70% 的 dropout 在外汇样本稀少时可能明显拖慢收敛。 CreateDescriptions 函数负责把 actor 与 critic 两个描述数组先清空再填层。Actor 输入层用 defNeuronBaseOCL,节点数取 AccountDescr(账户状态维度),随后 layer 1 把维度压到 EmbeddingSize 并套 SIGMOID 激活。 真正的算力集中在 layer 2:defNeuronMLCrossAttentionMLKV 做交叉注意力,units 设为 {1, BarDescr}、windows 为 {EmbeddingSize, NForecast}、heads 为 {8, 4},堆叠 9 层、window_out=32。在 MT5 里把 heads 第二维从 4 改成 2,显存占用倾向下降但多头捕捉周期联动的能力可能减弱,值得你直接编译比对日志。

MQL5 / C++
   class="kw">return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = BarDescr*NForecast;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.layers=class="num">1;
   if(!encoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">11
   if(!(descr = new CLayerDescription()))
     class="kw">return false;
   descr.type = defNeuronFreDFOCL;
   descr.window = BarDescr;
   descr.count =  NForecast;
   descr.step = class="type">int(true);
   descr.probability = class="num">0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   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">//--- Actor
   actor.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 = 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
   if(!(descr = new CLayerDescription()))
     class="kw">return false;
   descr.type = defNeuronMLCrossAttentionMLKV;
     {
       class="type">int temp[] = {class="num">1, BarDescr};
       ArrayCopy(descr.units, temp);
     }
     {
       class="type">int temp[] = {EmbeddingSize, NForecast};
       ArrayCopy(descr.windows, temp);
     }
     {
       class="type">int temp[] = {class="num">8, class="num">4};
       ArrayCopy(descr.heads, temp);
     }
   descr.layers = class="num">9;
   descr.step = class="num">1;
   descr.window_out = class="num">32;
   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">3

常见问题

先按文章把状态编码器里的层定义写好,再堆叠编码器,最后接Actor网络;照代码逐行拼就不会漏。
用历史数据跑一次前向传播,看尾层输出维度是否和Actor输入一致,不一致就查层定义。
可以,小布能读你的编码器装配描述,标出维度不匹配或层顺序错的地方,省去手动核对。
基础篇先用文中给的层数,贵金属高风险,层多可能延迟变大,建议先小层数是验证再微调。
尾层输出接Actor无报错且回测样本里动作概率分布合理,就可能拿一小仓去实盘验证。