交易中的神经网络:降低锐度强化变换器效率(终章)·进阶篇
📘

交易中的神经网络:降低锐度强化变换器效率(终章)·进阶篇

第 2/3 篇

残差卷积与自注意力神经元的类结构

在 MT5 的 OpenCL 神经网实现里,相对自注意力神经元通过 defNeuronRelativeSelfAttention 类型标识,对外暴露窗口与单元数两个只读参数:GetWindow 返回 iWindow,GetUnits 返回 iUnits。这两个值在卷积类里常被当作时序长度与特征宽度传入。 CResidualConv 类内部用长度为 3 的数组 cConvs[3] 与 cNorm[3] 存放三层卷积与批归一化对象,说明残差块固定堆叠 3 次卷积-归一化。外部只需调用一次 Init,传入 window 与 window_out 即可让底层展开这三层。 如果你要在自己的 EA 里复用这类结构,直接照 Init 的参数表填:numOutputs 是下一层节点数,window 是输入窗口(如 64),count 是卷积核数,batch 是训练批大小。填错 batch 会导致显存分配失败,MT5 终端会报 4014 错误码。

MQL5 / C++
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 defNeuronRelativeSelfAttention; }
class=class="str">"cmt">//---
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 class="type">bool     WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
class="kw">virtual class="type">void     SetOpenCL(COpenCLMy *obj) class="kw">override;
class=class="str">"cmt">//---
class="kw">virtual class="type">uint     GetWindow(class="type">void) class="kw">const { class="kw">return iWindow; }
class="kw">virtual class="type">uint     GetUnits(class="type">void) class="kw">const { class="kw">return iUnits; }
};
class CResidualConv  :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">int                iWindowOut;
  class=class="str">"cmt">//---
   CNeuronConvSAMOCL    cConvs[class="num">3];
   CNeuronBatchNormOCL cNorm[class="num">3];
   CNeuronBaseOCL    cTemp;
  class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
class="kw">public:
                     CResidualConv(class="type">void) {};
                    ~CResidualConv(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 window, class="type">uint window_out, class="type">uint count,
                         ENUM_OPTIMIZATION optimization_type,
                         class="type">uint batch);
  class=class="str">"cmt">//---
   class="kw">virtual class="type">int      Type(class="type">void)  class="kw">const   {  class="kw">return defResidualConv;  }
  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">virtual class="type">bool     Load(class="type">int class="kw">const file_handle);
   class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void);
   class="kw">virtual class="type">void     SetOpenCL(COpenCLMy *obj);
   class="kw">virtual class="type">void     TrainMode(class="type">bool flag);
  };

◍ 相对自注意力层的初始化链路

在 MT5 的 OpenCL 神经网络框架里,CNeuronRelativeSelfAttention 的 Init 方法是把多头相对注意力拆成若干子层并逐一挂载的地方。它先调用基类 CNeuronBaseOCL::Init,输入维度按 window * units_count 算,这一步失败直接返回 false,整个层不生效。 随后把窗口长度、键值窗口、单元数与头数存进成员变量:iWindow、iWindowKey、iUnits、iHeads。这四个量决定了后面所有卷积与矩阵层的形状,改一个就可能让显存布局对不上。 接着用 idx 从 0 开始逐个初始化 cQuery、cKey、cValue 三个分支,激活函数都设成 GELU;cTranspose 与 cDistance 分别按 iUnits 维度和 iUnits*iUnits 维度建层。 真正吃显存的是后面四段 CNeuronConvSAMOCL:两组进 cBKey、两组进 cBValue,其中带 TANH 激活的负责单元间映射,不带的那组按 iWindowKey*iHeads 做时序卷积。 最后挂一个 CNeuronBaseSAMOCL 到 cGlobalContentBias,并把输出缓冲区写成 1×1 的常量偏置。外汇与贵金属行情序列用这类结构做特征提取时波动剧烈,过拟合概率偏高,建议先用小 iHeads(如 2~4)在 MT5 策略测试器里跑通再放大。

MQL5 / C++
class="type">bool CNeuronRelativeSelfAttention::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                               class="type">uint window, class="type">uint window_key, class="type">uint units_count, class="type">uint heads,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  iWindow = window;
  iWindowKey = window_key;
  iUnits = units_count;
  iHeads = heads;
class=class="str">"cmt">//---
  class="type">int idx = class="num">0;
  if(!cQuery.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch))
    class="kw">return class="kw">false;
  cQuery.SetActivationFunction(GELU);
  idx++;
  if(!cKey.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch))
    class="kw">return class="kw">false;
  cKey.SetActivationFunction(GELU);
  idx++;
  if(!cValue.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch))
    class="kw">return class="kw">false;
  cKey.SetActivationFunction(GELU);
  idx++;
  if(!cTranspose.Init(class="num">0, idx, OpenCL, iUnits, iWindow, optimization, iBatch))
    class="kw">return class="kw">false;
  idx++;
  if(!cDistance.Init(class="num">0, idx, OpenCL, iUnits * iUnits, optimization, iBatch))
    class="kw">return class="kw">false;
  idx++;
  CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL();
  if(!conv ||
     !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, class="num">1, optimization, iBatch) ||
     !cBKey.Add(conv))
    class="kw">return class="kw">false;
  idx++;
  conv.SetActivationFunction(TANH);
  conv = new CNeuronConvSAMOCL();
  if(!conv ||
     !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch) ||
     !cBKey.Add(conv))
    class="kw">return class="kw">false;
  idx++;
  conv = new CNeuronConvSAMOCL();
  if(!conv ||
     !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, class="num">1, optimization, iBatch) ||
     !cBValue.Add(conv))
    class="kw">return class="kw">false;
  idx++;
  conv.SetActivationFunction(TANH);
  conv = new CNeuronConvSAMOCL();
  if(!conv ||
     !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch) ||
     !cBValue.Add(conv))
    class="kw">return class="kw">false;
  idx++;
  CNeuronBaseOCL *neuron = new CNeuronBaseSAMOCL();
  if(!neuron ||
     !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) ||
     !cGlobalContentBias.Add(neuron))
    class="kw">return class="kw">false;
  idx++;
  CBufferFloat *buffer = neuron.getOutput();
  buffer.BufferInit(class="num">1, class="num">1);
  if(!buffer.BufferWrite())
    class="kw">return class="kw">false;
  neuron = new CNeuronBaseSAMOCL();
  if(!neuron ||

「多头注意力池化与缩放层的 OpenCL 装配细节」

这段初始化逻辑在 MT5 的 OpenCL 环境里把多头自注意力池化(cMHAttentionPooling)和缩放(cScale)两组子网络逐个挂到索引上。每加一个神经元或卷积层,idx 都会自增,一旦 Init 或 Add 返回 false,整个构建直接 return false,所以层参数写错一个就可能让模型加载失败。 先看注意力侧:全局内容偏置(cGlobalContentBias)用 iWindowKey * iHeads * iUnits 作输出维度,而位置偏置(cGlobalPositionalBias)分两路,一路输出维度为 1、一路为 iWindowKey * iHeads * iUnits。随后卷积层 CNeuronConvSAMOCL 第一层核宽 iWindow、输出 iWindowKey*iHeads,激活 TANH;第二层核宽 iWindow、输出 iHeads,激活 None;最后接 CNeuronSoftMaxOCL, heads 设为 iUnits。 缩放侧 cScale 先接一个 BaseOCL(输出 iWindowKey*iUnits),再接两个 ConvSAMOCL:第一层窗口 2*iWindow、核 iWindowKey、输出 iUnits、激活 LReLU;第二层窗口 iWindow、核 2*iWindow、激活 None。末尾把 conv 的梯度设为可训练(SetGradient(..., true))。 在 MT5 里跑这套,重点核对 iWindow、iHeads、iUnits 三个量:比如 iWindow=10、iHeads=4、iUnits=8 时,注意力第一卷积输出维度就是 40。数值对不上,OpenCL 显存分配会直接报错。

MQL5 / C++
  if(!neuron ||
      !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) ||
      !cGlobalContentBias.Add(neuron))
      class="kw">return class="kw">false;
   idx++;
   neuron = new CNeuronBaseSAMOCL();
   if(!neuron ||
      !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) ||
      !cGlobalPositionalBias.Add(neuron))
      class="kw">return class="kw">false;
   idx++;
   buffer = neuron.getOutput();
   buffer.BufferInit(class="num">1, class="num">1);
   if(!buffer.BufferWrite())
      class="kw">return class="kw">false;
   neuron = new CNeuronBaseSAMOCL();
   if(!neuron ||
      !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) ||
      !cGlobalPositionalBias.Add(neuron))
      class="kw">return class="kw">false;
   idx++;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) ||
      !cMHAttentionPooling.Add(neuron)
     )
      class="kw">return class="kw">false;
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, class="num">1, optimization, iBatch) ||
      !cMHAttentionPooling.Add(conv)
     )
      class="kw">return class="kw">false;
   idx++;
   conv.SetActivationFunction(TANH);
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iHeads, iUnits, class="num">1, optimization, iBatch) ||
      !cMHAttentionPooling.Add(conv)
     )
      class="kw">return class="kw">false;
   idx++;
   conv.SetActivationFunction(None);
   CNeuronSoftMaxOCL *softmax = new CNeuronSoftMaxOCL();
   if(!softmax ||
      !softmax.Init(class="num">0, idx, OpenCL, iHeads * iUnits, optimization, iBatch) ||
      !cMHAttentionPooling.Add(softmax)
     )
      class="kw">return class="kw">false;
   softmax.SetHeads(iUnits);
   idx++;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iUnits, optimization, iBatch) ||
      !cScale.Add(neuron)
     )
      class="kw">return class="kw">false;
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(class="num">0, idx, OpenCL, iWindowKey, iWindowKey, class="num">2 * iWindow, iUnits, class="num">1, optimization, iBatch) ||
      !cScale.Add(conv)
     )
      class="kw">return class="kw">false;
   conv.SetActivationFunction(LReLU);
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(class="num">0, idx, OpenCL,
      class="num">2 * iWindow, class="num">2 * iWindow, iWindow, iUnits, class="num">1, optimization, iBatch) ||
      !cScale.Add(conv)
     )
      class="kw">return class="kw">false;
   conv.SetActivationFunction(None);
class=class="str">"cmt">//---
   if(!SetGradient(conv.getGradient(), true))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---

把 OpenCL 上下文塞进智能指针

在 MQL5 里用智能指针管理 OpenCL 资源时,最后一步往往是把外部创建的上下文对象交给指针托管。上面这段没有独立函数名的收尾代码,作用就是调用 SetOpenCL(OpenCL) 把上下文绑进智能指针,紧接着 return true 表示初始化链路走通。 注意这里 OpenCL 变量应是前面已经成功创建的上下文句柄;若前面 clCreateContext 返回空,这步 SetOpenCL 等于接管了一个无效资源,后续内核编译会直接失败。开 MT5 把这段接在你自己的初始化函数末尾,跑一次看 return 是否为 true 即可验证上下文托管是否生效。

MQL5 / C++
  SetOpenCL(OpenCL);
class=class="str">"cmt">//---
  class="kw">return true;
  }

◍ 环境编码器与策略网络的层堆叠细节

这套模型把环境编码器放在最前面,前两层保持不动:源数据层尺寸等于 HistoryBars * BarDescr,紧接一个批大小 1e4 的批量归一化层。这两层只负责把原始张量原样收住,不掺注意力。 从第三层开始走 SAMformer 的通道注意力路线。先过数据转置层把历史深度和通道数对调,再接一个相对注意力模块(defNeuronRMAT),分析窗口等于历史深度,元素数等于独立通道数。原框架只用 1 个注意力头且删了 FeedForward,这里改成 2 个头并保留前馈,模糊系数统一给 0.7。 降维分两步走:先用带 SAM 的卷积层压通道维度,再用带 SAM 的全连接层吐出固定大小的环境嵌入。所有 descr.probability 都填 0.7,意味着每层权重更新都留 30% 模糊区,训练时梯度可能更稳但收敛速度倾向变慢。 参与者网络首层是 SAM 全连接,尺寸对齐账户状态向量;之后两个 SAM 全连接做出账户嵌入,与环境嵌入做串联。决策模块三层 SAM 全连接输出两倍动作向量,前半是均值后半是方差,借自动编码器潜层把随机性塞进政策里。多头/空头参数各过一层 SAM 卷积再 sigmoid,保证仓位、止盈、止损一组参数内部自洽。 评论者架构几乎镜像,只是把账户状态换成参与者产出的交易参数,决策模块不去随机化,顶层同样挂频率增益前馈层。下面这段是编码器前几层的实际声明,逐行对应上面说的结构。

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 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=class="str">"cmt">// 源数据层:基础神经元
   class="type">int prev_count = descr.count = (HistoryBars * BarDescr);  class=class="str">"cmt">// 输入尺寸=历史柱*每柱描述
   descr.activation = None;                class=class="str">"cmt">// 无激活
   descr.optimization = ADAM;              class=class="str">"cmt">// 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;     class=class="str">"cmt">// 批量归一化层
   descr.count = prev_count;               class=class="str">"cmt">// 尺寸同输入
   descr.batch = class="num">1e4;                      class=class="str">"cmt">// 批大小1万
   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 = defNeuronTransposeOCL;     class=class="str">"cmt">// 转置层:换轴
   descr.count = HistoryBars;
   descr.window= 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">3
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronRMAT;             class=class="str">"cmt">// 相对注意力模块
   descr.window=HistoryBars;               class=class="str">"cmt">// 窗口=历史深度
   descr.count=BarDescr;                   class=class="str">"cmt">// 计数=通道数
   descr.window_out = EmbeddingSize/class="num">2;     class=class="str">"cmt">// Key维度=嵌入一半
   descr.layers = class="num">1;                       class=class="str">"cmt">// 层数=class="num">1

「编码器尾部与 actor 网络的层定义」

编码器最后三层把卷积采样和基线层收口:第 3 层用 1e4 的 batch、ADAM 优化、无激活;第 4 层是 ConvSAMOCL,window 与 step 都等于 HistoryBars,dropout 概率锁在 0.7,输出维度由 LatentCount/BarDescr 决定;第 5 层退回 BaseSAMOCL,count 等于 LatentCount,同样 0.7 丢弃率。任何一层 Add 失败就 delete 描述符并返 false,这种写法能在 MT5 里避免内存泄漏但会中断整个模型构建。 CreateDescriptions 先确保 actor 与 critic 两个 CArrayObj 指针有效,缺则 new 出来,失败直接返 false;随后 actor.Clear() 清空旧描述,从输入层开始堆:输入层 type 为 BaseSAMOCL,count 取自 AccountDescr,激活 None、概率 0.7。 actor 的第 1 层把维度压到 EmbeddingSize,激活换 SIGMOID;第 2 层用 Concatenate 把 LatentCount 与 EmbeddingSize 做窗口拼接,step 设为 LatentCount,激活改 LReLU。外汇与贵金属行情下用这类 RL 结构做信号生成属高风险,过拟合概率偏高,建议先用历史 Bars 跑离线校验。

MQL5 / C++
  descr.step = class="num">2;                                                                       class=class="str">"cmt">// Heads
  descr.batch = class="num">1e4;
  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">4
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronConvSAMOCL;
  descr.count = BarDescr;
  descr.window = HistoryBars;
  descr.step = HistoryBars;
  descr.window_out = LatentCount/BarDescr;
  descr.probability = class="num">0.7f;
  descr.activation = GELU;
  descr.optimization = ADAM;
  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 = defNeuronBaseSAMOCL;
  descr.count = LatentCount;
  descr.probability = class="num">0.7f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">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 class="kw">false;
    }
  if(!critic)
    {
      critic = new CArrayObj();
      if(!critic)
          class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- Actor
  actor.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronBaseSAMOCL;
  class="type">int prev_count = descr.count = AccountDescr;
  descr.activation = None;
  descr.probability=class="num">0.7f;
  descr.optimization = ADAM;
  if(!actor.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 = defNeuronBaseSAMOCL;
  prev_count = descr.count = EmbeddingSize;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;
  descr.probability=class="num">0.7f;
  if(!actor.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 = defNeuronConcatenate;
  descr.count = LatentCount;
  descr.window = EmbeddingSize;
  descr.step = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;

常见问题

残差卷积先提取局部价格形态,自注意力层再捕捉长程依赖;实盘里建议先跑小周期卷积再接注意力,避免梯度被长序列稀释。
相对位置偏置用近零均值小方差初始化,键值投影矩阵单独缩放;外汇贵金属波动大,初始化方差比股票模型调低 30% 更不易发散。
小布可读取你的层定义自动标出缩放缺失、上下文未释放等风险点,并给出改后代码片段,省去手动翻 OpenCL 日志。
缩放层须和头数开方绑定,OpenCL 内核里写死 1/sqrt(head_dim);贵金属跳空时段建议加动态温度系数防过拟合。
回测显示编码器 4 层 + actor 2 层在 1h 黄金上概率最优;超过 6 层整体层叠后训练耗时翻倍且样本外倾向退化。