交易中的神经网络:基于 ResNeXt 模型的多任务学习·进阶篇
📘

交易中的神经网络:基于 ResNeXt 模型的多任务学习·进阶篇

第 2/2 篇

◍ ResNeXt瓶颈层的初始化链路

在 MT5 的 OpenCL 神经网络封装里,CNeuronResNeXtBottleneck 的 Init 方法先把输出单元数算出来:units_out = (units_count - window + step - 1) / step + 1。这个整数除法决定了卷积层后续特征图的时间维度,调 step 或 window 会直接改掉显存占用量。 初始化顺序是一条硬链路:先建主卷积 CNeuronConvOCL,再依次挂 cProjectionIn、cNormalizeIn、cTransposeIn,最后进 Feature Extraction 与 Projection Out。任何一步返回 false,整个层就废了,不会半初始化。 注意 cNormalizeIn 和 cNormalizeFeature 都强制设了 LReLU 激活,而 cTransposeIn 的激活直接复用 cNormalizeIn 的,改前者就连带改后者。想验证的话,把 group_size 设成 4、groups 设成 32,看 units_out 随 window 从 3 变到 7 时卷积核申请是否线性涨。外汇与贵金属模型训练波动大,显存爆了只会静默失败,高风险。

MQL5 / C++
class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups,
                    ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int      Type(class="type">void)  class="kw">override class="kw">const   {  class="kw">return defNeuronResNeXtBottleneck;   }
  class=class="str">"cmt">//--- methods for working with files
  class="kw">virtual class="type">bool     Save(class="type">int class="kw">const file_handle) class="kw">override;
  class="kw">virtual class="type">bool     Load(class="type">int class="kw">const file_handle) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void) class="kw">override;
  class="kw">virtual class="type">void     SetOpenCL(COpenCLMy *obj) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool     WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
  };
class="type">bool CNeuronResNeXtBottleneck::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                  class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint window,
                                  class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups,
                                  ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   class="type">int units_out = ((class="type">int)units_count - (class="type">int)window + (class="type">int)step - class="num">1) / (class="type">int)step + class="num">1;
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, group_size * groups, group_size * groups,
chanels_out, units_out, class="num">1, optimization_type, batch))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Projection In
   class="type">uint index = class="num">0;
   if(!cProjectionIn.Init(class="num">0, index, OpenCL, chanels_in, chanels_in, group_size * groups,
units_count, class="num">1, optimization, iBatch))
      class="kw">return class="kw">false;
   index++;
   if(!cNormalizeIn.Init(class="num">0, index, OpenCL, cProjectionIn.Neurons(), iBatch, optimization))
      class="kw">return class="kw">false;
   cNormalizeIn.SetActivationFunction(LReLU);
   index++;
   if(!cTransposeIn.Init(class="num">0, index, OpenCL, units_count, groups, group_size, optimization, iBatch))
      class="kw">return class="kw">false;
   cTransposeIn.SetActivationFunction((ENUM_ACTIVATION)cNormalizeIn.Activation());
class=class="str">"cmt">//--- Feature Extraction
   index++;
   if(!cFeatureExtraction.Init(class="num">0, index, OpenCL, group_size * window, group_size * step, group_size,
units_out, groups, optimization, iBatch))
      class="kw">return class="kw">false;
   index++;
   if(!cNormalizeFeature.Init(class="num">0, index, OpenCL, cFeatureExtraction.Neurons(), iBatch, optimization))
      class="kw">return class="kw">false;
   cNormalizeFeature.SetActivationFunction(LReLU);
class=class="str">"cmt">//--- Projection Out
   index++;

残差分支在 OpenCL 上的前向链路

ResNeXt 的瓶颈层把一次前向拆成三段:投影入、特征抽取、投影出。每段都依赖前一段的 AsObject() 输出做输入,任何一步 FeedForward 返回 false 都会直接中断,整个神经元不再继续计算。 看 cTransposeOut 的初始化,Init 里第 2 个参数 index 决定该层在通道序列里的位置,group_size 与 units_out 共同约束显存排布;若这两个值和前面 cFeatureExtraction 的输出维度对不上,OpenCL 内核大概率报维度不匹配。 CNeuronResNeXtResidual 类在 Conv 基础上挂了四个成员:cTransposeIn、cProjectionTime、cNormalizeTime、cTransposeOut。它override 了 feedForward / updateInputWeights / calcInputGradients,意味着残差时间维的梯度回传也走独立通路,不会复用父类的卷积反向。 Type() 返回 defNeuronResNeXtResidual,这是 MT5 神经网络框架里区分层类型的整型标签。在终端里自己写诊断脚本时,可以用这一返回值过滤出残差层,单独打印其激活分布,验证是否出现梯度消失倾向。外汇与贵金属模型训练波动大,此类 GPU 层易因显存碎片失败,实操请先在模拟环境跑通再上实盘。

MQL5 / C++
  if(!cTransposeOut.Init(class="num">0, index, OpenCL, groups, units_out, group_size, optimization, iBatch))
        class="kw">return class="kw">false;
   cTransposeOut.SetActivationFunction((ENUM_ACTIVATION)cNormalizeFeature.Activation());
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronResNeXtBottleneck::feedForward(CNeuronBaseOCL *NeuronOCL)
   {
class=class="str">"cmt">//--- Projection In
   if(!cProjectionIn.FeedForward(NeuronOCL))
        class="kw">return class="kw">false;
   if(!cNormalizeIn.FeedForward(cProjectionIn.AsObject()))
        class="kw">return class="kw">false;
   if(!cTransposeIn.FeedForward(cNormalizeIn.AsObject()))
        class="kw">return class="kw">false;
class=class="str">"cmt">//--- Feature Extraction
   if(!cFeatureExtraction.FeedForward(cTransposeIn.AsObject()))
        class="kw">return class="kw">false;
   if(!cNormalizeFeature.FeedForward(cFeatureExtraction.AsObject()))
        class="kw">return class="kw">false;
class=class="str">"cmt">//--- Projection Out
   if(!cTransposeOut.FeedForward(cNormalizeFeature.AsObject()))
        class="kw">return class="kw">false;
   class="kw">return CNeuronConvOCL::feedForward(cTransposeOut.AsObject());
   }
class CNeuronResNeXtResidual:   class="kw">public CNeuronConvOCL
   {
class="kw">protected:
   CNeuronTransposeOCL      cTransposeIn;
   CNeuronConvOCL           cProjectionTime;
   CNeuronBatchNormOCL      cNormalizeTime;
   CNeuronTransposeOCL      cTransposeOut;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool          feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool          updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool          calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                     CNeuronResNeXtResidual(class="type">void){};
                    ~CNeuronResNeXtResidual(class="type">void){};
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool          Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                     class="type">uint chanels_in, class="type">uint chanels_out,
                     class="type">uint units_in, class="type">uint units_out,
                     ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int           Type(class="type">void)    class="kw">override class="kw">const   {   class="kw">return defNeuronResNeXtResidual;  }
   class=class="str">"cmt">//--- methods for working with files
   class="kw">virtual class="type">bool          Save(class="type">int class="kw">const file_handle) class="kw">override;
   class="kw">virtual class="type">bool          Load(class="type">int class="kw">const file_handle) class="kw">override;
   class=class="str">"cmt">//---
   class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void) class="kw">override;
   class="kw">virtual class="type">void          SetOpenCL(COpenCLMy *obj) class="kw">override;

「ResNeXt 残差与块结构的初始化链路」

CNeuronResNeXtResidual 的 Init 先把基类 CNeuronConvOCL 按 chanels_in→chanels_in→chanels_out、units_out 与卷积核尺寸 1 拉起来,任一环节失败直接返回 false。随后用局部 index 从 0 递增,依次挂接 cTransposeIn、cProjectionTime、cNormalizeTime、cTransposeOut 四个子层,每层都依赖前一层神经元数与批大小 iBatch,顺序错一处前向就无法贯通。 前向 feedForward 分两条投影轴:时间轴上先转置入 cTransposeIn,再过 cProjectionTime 与 cNormalizeTime;通道轴由 cTransposeOut 承接归一化结果,最后才交给 CNeuronConvOCL::feedForward。实测若 cNormalizeTime 的神经元数没对齐 cProjectionTime.Neurons(),会在第三步返回 false,MT5 专家日志里看不到具体层名,只能靠逐层断点排查。 CNeuronResNeXtBlock 本身不重写 Init 声明,而是聚合 cBottleneck、cResidual 与 cBuffer,把 feedForward / updateInputWeights / calcInputGradients 三个虚函数留给派生实现。想验证这套结构,开 MT5 把 iBatch 设成 32、chanels_in=64 跑一次 Init,观察 OpenCL 上下文是否报缓冲区越界。

MQL5 / C++
class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
};
class="type">bool CNeuronResNeXtResidual::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                  class="type">uint chanels_in, class="type">uint chanels_out,
                                  class="type">uint units_in, class="type">uint units_out,
                                  ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
{
 if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, chanels_in, chanels_in, chanels_out,
                          units_out, class="num">1, optimization_type, batch))
    class="kw">return class="kw">false;
 class="type">int index=class="num">0;
 if(!cTransposeIn.Init(class="num">0, index, OpenCL, units_in, chanels_in, optimization, iBatch))
    class="kw">return class="kw">false;
 index++;
 if(!cProjectionTime.Init(class="num">0, index, OpenCL, units_in, units_in, units_out, chanels_in, class="num">1, optimization, iBatch))
    class="kw">return class="kw">false;
 index++;
 if(!cNormalizeTime.Init(class="num">0, index, OpenCL, cProjectionTime.Neurons(), iBatch, optimization))
    class="kw">return class="kw">false;
 index++;
 if(!cTransposeOut.Init(class="num">0, index, OpenCL, chanels_in, units_out, optimization, iBatch))
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
 class="kw">return true;
}
class="type">bool CNeuronResNeXtResidual::feedForward(CNeuronBaseOCL *NeuronOCL)
{
class=class="str">"cmt">//--- Projection Timeline
 if(!cTransposeIn.FeedForward(NeuronOCL))
    class="kw">return class="kw">false;
 if(!cProjectionTime.FeedForward(cTransposeIn.AsObject()))
    class="kw">return class="kw">false;
 if(!cNormalizeTime.FeedForward(cProjectionTime.AsObject()))
    class="kw">return class="kw">false;
class=class="str">"cmt">//--- Projection Chanels
 if(!cTransposeOut.FeedForward(cNormalizeTime.AsObject()))
    class="kw">return class="kw">false;
 class="kw">return CNeuronConvOCL::feedForward(cTransposeOut.AsObject());
}
class CNeuronResNeXtBlock :  class="kw">public CNeuronBaseOCL
{
class="kw">protected:
 class="type">uint                iChanelsOut;
 CNeuronResNeXtBottleneck cBottleneck;
 CNeuronResNeXtResidual  cResidual;
 CBufferFloat             cBuffer;
 class=class="str">"cmt">//---
 class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
 class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
 class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                     CNeuronResNeXtBlock(class="type">void){};
                    ~CNeuronResNeXtBlock(class="type">void){};
 class=class="str">"cmt">//---

◍ ResNeXt 残差块在 MT5 里的初始化链路

在 MT5 的 OpenCL 神经网络框架里,CNeuronResNeXtBlock 把瓶颈卷积与残差支路拼成一块可训练单元。它的类声明里先看 Type() 返回 defNeuronResNeXtBlock,这是运行时区分层类型的硬标识,调参或排查图层堆叠错误时得靠它。 Init 方法是落地关键。units_out 用 ((units_count - window + step - 1) / step + 1) 算滑动卷积后的输出长度,比如 window=3、step=1、units_count=10 时得到 8 个时间步单元。若 CNeuronBaseOCL 基类初始化失败或瓶颈层 cBottleneck 参数不匹配,函数直接返 false,块不生效。 残差支路 cResidual 初始化后,代码把它的梯度源指向 cBottleneck.getGradient() 并置共享标志,意味着两条路的反向传播梯度在此汇流。外汇与贵金属行情用这类结构做特征提取时波动剧烈,模型过拟合概率偏高,上 MT5 跑前建议先用小 batch 验证显存与梯度连通。 下面这段是类声明与 Init 实现的原文,逐行拆完你能直接抄进自己的 EA 工程里改通道数。

MQL5 / C++
class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                      class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint window,
                      class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups,
                      ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void)    class="kw">override class="kw">const   {  class="kw">return defNeuronResNeXtBlock;  }
  class=class="str">"cmt">//--- methods for working with files
  class="kw">virtual class="type">bool      Save(class="type">int class="kw">const file_handle) class="kw">override;
  class="kw">virtual class="type">bool      Load(class="type">int class="kw">const file_handle) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void) class="kw">override;
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
  };
class="type">bool CNeuronResNeXtBlock::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                               class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint window,
                               class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups,
                               ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   class="type">int units_out = ((class="type">int)units_count - (class="type">int)window + (class="type">int)step - class="num">1) / (class="type">int)step + class="num">1;
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_out * chanels_out, optimization_type, batch))
      class="kw">return class="kw">false;
   iChanelsOut = chanels_out;
   class="type">int index = class="num">0;
   if(!cBottleneck.Init(class="num">0, index, OpenCL, chanels_in, chanels_out, window, step, units_count,
                        group_size, groups, optimization, iBatch))
      class="kw">return class="kw">false;
   index++;
   if(!cResidual.Init(class="num">0, index, OpenCL, chanels_in, chanels_out, units_count, units_out, optimization, iBatch))
      class="kw">return class="kw">false;
   if(!cResidual.SetGradient(cBottleneck.getGradient(), true))
      class="kw">return class="kw">false;
   if(!SetGradient(cBottleneck.getGradient(), true))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }

残差块的前向与梯度回传实现

ResNeXt 结构里,一个神经块的前向计算先把瓶颈层和残差支路分别喂入 OpenCL 神经元对象,任何一步失败直接返回 false,避免脏数据进网络。 前向收尾调用 SumAndNormilize 把两条支路的输出按通道数 iChanelsOut 做加和与归一化,末位参数传 true 表示训练态归一,四个 0/1 控制偏移与缩放区间。 梯度回传时先取上层梯度暂存到 temp,若本地 cBuffer 的 OpenCL 上下文或长度不匹配就重新按 temp 初始化,否则旧缓冲会污染反向传播。 calcInputGradients 里两次调用 calcHiddenGradients 分别作用于瓶颈与残差对象,再用 SumAndNormilize(temp, 梯度, temp, 1, false, ...) 把两路梯度合并写回,false 代表推理态不重新归一。 在 MT5 里把这段接进你自己的 CNeuronResNeXtBlock,重点核对 cBuffer.BufferInitLike 的触发条件——上下文不一致时若漏掉重初始化,GPU 显存大概率报越界。

MQL5 / C++
  if(!cBottleneck.FeedForward(NeuronOCL))
      class="kw">return class="kw">false;
  if(!cResidual.FeedForward(NeuronOCL))
      class="kw">return class="kw">false;
  if(!SumAndNormilize(cBottleneck.getOutput(), cResidual.getOutput(), Output, iChanelsOut, true, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- result
  class="kw">return true;
  }
class="type">bool CNeuronResNeXtBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL)
    class="kw">return class="kw">false;
  if(!NeuronOCL.calcHiddenGradients(cBottleneck.AsObject()))
      class="kw">return class="kw">false;
  CBufferFloat *temp = NeuronOCL.getGradient();
  if(cBuffer.GetOpenCL() != OpenCL ||
      cBuffer.Total() != temp.Total())
     {
      if(!cBuffer.BufferInitLike(temp))
        class="kw">return class="kw">false;
     }
  if(!NeuronOCL.SetGradient(GetPointer(cBuffer), class="kw">false))
      class="kw">return class="kw">false;
  if(!NeuronOCL.calcHiddenGradients(cResidual.AsObject()))
      class="kw">return class="kw">false;
  if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return class="kw">false;
  if(!NeuronOCL.SetGradient(temp, class="kw">false))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }

「下一篇要做的真实验证」

这一篇把 ResNeXt 的主干在 MT5 里搭起来了,核心思路是用一个共享编码器接多个任务头,去吃高维时间序列里的分类和回归。相比单任务网络,这种结构在样本有限时过拟合的概率更低,但代价是训练吞吐更吃显卡。 随文给了 7 个文件:Research.mq5、ResearchRealORL.mq5、Study.mq5、Test.mq5 四个智能系统,加上 Trajectory.mqh、NeuroNet.mqh、NeuroNet.cl 三个类库,ZIP 体积 2430.99 KB。想跑通的话,先挂 Research.mq5 收集样本,再用 Study.mq5 训,最后 Test.mq5 测,缺了 OpenCL 运行环境会直接编译不过。 下一篇才会把多任务框架拼完,并在真实历史数据上给出回测数字。外汇和贵金属杠杆高、滑点跳空频繁,即便模型在历史集上表现稳,实盘也可能因流动性断裂而失效,别直接拿训练权重去Live账户。

常见问题

按原文链路用正态小方差初始化卷积核,偏置置零,并在残差分支末端做尺度缩放,能降低早期梯度爆炸概率。
小批量下内核启动开销占比高,建议合并多个分支到同一kernel并调大batch,才可能看到加速。
可以,小布能读取你的模型结构描述并比对初始化链路,标出漏设偏置或缩放异常的块,省去手查。
容易漏掉捷径路径的恒等映射梯度直通,导致深层网络退化,需在反向函数里显式加回输入梯度。
优先用近三年贵金属与外汇小时线,按品种切分训练集验证泛化,注意外汇贵金属高风险,结论仅作概率参考。