神经网络变得简单(第 86 部分):U-形变换器·进阶篇
📘

神经网络变得简单(第 86 部分):U-形变换器·进阶篇

第 2/3 篇

「U型注意力层的类结构与初始化链路」

在 MT5 的 OpenCL 神经网络扩展里,CNeuronUShapeAttention 继承自 CNeuronBaseOCL,内部用两个 CNeuronMLMHAttentionOCL 做上下分支注意力,再用两个 CNeuronConvOCL 做合并与拆分卷积,cNeck 指针则承接瓶颈层。 析构时只释放 cNeck,说明注意力与卷积子层走的是栈内数组生命周期,不需要手动 delete。这种结构在 EURUSD 的 1H 回测中,若 units_count 设 64、heads 设 4,可能比单层注意力更利于捕捉跨窗口依赖。 Init 里第一道校验是基类用 window * units_count 作为输出维度初始化;随后 cAttention[0] 和 cMergeSplit[0] 依次初始化,后者卷积核参数写死为 2*window 输入、4*window 输出、半宽 (units_count+1)/2。 当 inside_bloks>0 时会 new 一个同级 CNeuronUShapeAttention 做递归下采样,units_count 翻倍、inside_bloks 减 1,直到为 0 退出。复制下面代码到 MT5 的 Include/NeuralNetwork 目录,改 units_count 即可观察显存占用的非线性增长。

MQL5 / C++
class CNeuronUShapeAttention : class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   CNeuronMLMHAttentionOCL     cAttention[class="num">2];
   CNeuronConvOCL              cMergeSplit[class="num">2];
   CNeuronBaseOCL             *cNeck;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   class=class="str">"cmt">//---
class="kw">public:
                        CNeuronUShapeAttention(class="type">void) {};
                       ~CNeuronUShapeAttention(class="type">void) { class="kw">delete cNeck; }
   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_key,
                          class="type">uint heads, class="type">uint units_count, class="type">uint layers, class="type">uint inside_bloks,
                          ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int       Type(class="type">void) const   { class="kw">return defNeuronUShapeAttention;  }
   class=class="str">"cmt">//--- methods for working with files
   class="kw">virtual class="type">bool      Save(class="type">int const file_handle);
   class="kw">virtual class="type">bool      Load(class="type">int const file_handle);
   class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void);
   class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *net, class="type">class="kw">float tau);
   class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
   };
class="type">bool CNeuronUShapeAttention::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key,
                                  class="type">uint heads, class="type">uint units_count, class="type">uint layers, class="type">uint inside_bloks,
                                  ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      class="kw">return false;
   if(!cAttention[class="num">0].Init(class="num">0, class="num">0, OpenCL, window, window_key, heads, units_count, layers, optimization, iBatch))
      class="kw">return false;
   if(!cMergeSplit[class="num">0].Init(class="num">0, class="num">1, OpenCL, class="num">2 * window, class="num">2 * window, class="num">4 * window, (units_count + class="num">1) / class="num">2,
                          optimization, iBatch))
      class="kw">return false;
   if(inside_bloks > class="num">0)
     {
      CNeuronUShapeAttention *temp = new CNeuronUShapeAttention();
      if(!temp)
         class="kw">return false;
      if(!temp.Init(class="num">0, class="num">2, OpenCL, window, window_key, heads, class="num">2 * units_count, layers, inside_bloks - class="num">1,

U型注意力网络的初始化与前向链路

这段实现展示了一个名为 CNeuronUShapeAttention 的自定义神经网络层,结构呈 U 型:两次注意力模块(cAttention[0]、cAttention[1])中间夹一个卷积瓶颈 cNeck,前后各接一个合并/拆分层。Init 里第二层注意力用 window_key 作键维度、heads 个头,输出通道固定为 2*units_count,这种对称设计在时序特征抽取里常见于把局部与全局依赖都吃进来。 初始化时若 temp->Init(0,2,OpenCL,window,window,window,2*units_count,optimization,iBatch) 返回失败会立刻 delete 并回 false,说明每层张量描述符(窗口大小、批大小 iBatch)必须和 OpenCL 上下文严格匹配,否则整层构建直接流产。 feedForward 的调用顺序就是 U 型数据流:注意力0 → 合并拆分0 → 瓶颈 → 注意力1 → 合并拆分1 → 残差求和归一(SumAndNormilize 带参数 1,false)。最后一步把输入 NeuronOCL 的输出与末层输出做加和归一,意味着网络默认带 skip connection,梯度不会在深层轻易消失。 calcInputGradients 反向走的是严格逆序,从 cAttention[1] 一路回传至 prevLayer,且同样用 SumAndNormilize 把梯度累加。在 MT5 里跑这类层时,把 iBatch 设成 32 或 64 通常比 1 更能压住 OpenCL 内核启动开销,但显存占用会随 window*window*2*units_count 线性涨,黄金分钟线小窗口先试 16 窗口较稳。外汇与贵金属模型训练属高风险实验,过拟合会让实盘信号失效。

MQL5 / C++
if(!cAttention[class="num">1].Init(class="num">0, class="num">3, OpenCL, window, window_key, heads, class="num">2 * units_count, layers, optimization, iBatch))
   class="kw">return false;
 if(!cMergeSplit[class="num">1].Init(class="num">0, class="num">4, OpenCL, class="num">2 * window, class="num">2 * window, window, units_count, optimization, iBatch))
   class="kw">return false;
 if(Gradient != cMergeSplit[class="num">1].getGradient())
   SetGradient(cMergeSplit[class="num">1].getGradient());
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CNeuronUShapeAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
  if(!cAttention[class="num">0].FeedForward(NeuronOCL))
    class="kw">return false;
  if(!cMergeSplit[class="num">0].FeedForward(cAttention[class="num">0].AsObject()))
    class="kw">return false;
  if(!cNeck.FeedForward(cMergeSplit[class="num">0].AsObject()))
    class="kw">return false;
  if(!cAttention[class="num">1].FeedForward(cNeck))
    class="kw">return false;
  if(!cMergeSplit[class="num">1].FeedForward(cAttention[class="num">1].AsObject()))
    class="kw">return false;
  if(!SumAndNormilize(NeuronOCL.getOutput(), cMergeSplit[class="num">1].getOutput(), Output, class="num">1, false))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronUShapeAttention::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
  if(!prevLayer)
    class="kw">return false;
  if(!cAttention[class="num">1].calcHiddenGradients(cMergeSplit[class="num">1].AsObject()))
    class="kw">return false;
  if(!cNeck.calcHiddenGradients(cAttention[class="num">1].AsObject()))
    class="kw">return false;
  if(!cMergeSplit[class="num">0].calcHiddenGradients(cNeck.AsObject()))
    class="kw">return false;
  if(!cAttention[class="num">0].calcHiddenGradients(cMergeSplit[class="num">0].AsObject()))
    class="kw">return false;
  if(!prevLayer.calcHiddenGradients(cAttention[class="num">0].AsObject()))
    class="kw">return false;
  if(!SumAndNormilize(prevLayer.getGradient(), Gradient, prevLayer.getGradient(), class="num">1, false))

◍ U型注意力层的权重回传与存取实现

在 MT5 的 OpenCL 神经网络封装里,CNeuronUShapeAttention 的 updateInputWeights 方法按顺序串起 5 个内部层:两次注意力、两次合并拆分、以及中间的 neck 层。任何一层返回 false 就立刻中断,说明这套 U 型结构对权重更新顺序强依赖,调参时若某一子层学习率异常,整个网络回传会静默失败。 Save 与 Load 方法暴露了持久化结构:循环 2 次写/读 cAttention 与 cMergeSplit,再单独处理 cNeck。Load 里先用 FileReadInteger 读类型标签,若已有 cNeck 且类型不匹配就 delete 重建——这意味着你换模型拓扑后直接读旧文件,可能悄悄丢掉不兼容的 neck 层而不报错。 验证方式很直接:在 MT5 终端新建 EA,include 相关神经网络头文件,给 CNeuronUShapeAttention 实例分别调用 Save/Load 并打印 file_handle 返回值;若 Load 后 cNeck.Type() 与你 new 的类型不一致,基本能锁定是旧缓存文件在作怪。外汇与贵金属品种上跑这类模型,杠杆风险高,回测与实盘偏差可能放大,任何信号都只是概率倾向。

MQL5 / C++
  class="kw">return false;
  if(!DeActivation(prevLayer.getOutput(), prevLayer.getGradient(), prevLayer.getGradient(),
prevLayer.Activation()))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronUShapeAttention::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!cAttention[class="num">0].UpdateInputWeights(NeuronOCL))
    class="kw">return false;
  if(!cMergeSplit[class="num">0].UpdateInputWeights(cAttention[class="num">0].AsObject()))
    class="kw">return false;
  if(!cNeck.UpdateInputWeights(cMergeSplit[class="num">0].AsObject()))
    class="kw">return false;
  if(!cAttention[class="num">1].UpdateInputWeights(cNeck))
    class="kw">return false;
  if(!cMergeSplit[class="num">1].UpdateInputWeights(cAttention[class="num">1].AsObject()))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronUShapeAttention::Save(const class="type">int file_handle)
  {
  if(!CNeuronBaseOCL::Save(file_handle))
    class="kw">return false;
  for(class="type">int i = class="num">0; i < class="num">2; i++)
    {
    if(!cAttention[i].Save(file_handle))
      class="kw">return false;
    if(!cMergeSplit[i].Save(file_handle))
      class="kw">return false;
    }
  if(!cNeck.Save(file_handle))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronUShapeAttention::Load(const class="type">int file_handle)
  {
  if(!CNeuronBaseOCL::Load(file_handle))
    class="kw">return false;
  for(class="type">int i = class="num">0; i < class="num">2; i++)
    {
    if(!LoadInsideLayer(file_handle, cAttention[i].AsObject()))
      class="kw">return false;
    if(!LoadInsideLayer(file_handle, cMergeSplit[i].AsObject()))
      class="kw">return false;
    }
  class="type">int type = FileReadInteger(file_handle);
  if(!!cNeck)
    {
    if(cNeck.Type() != type)
      class="kw">delete cNeck;
    }
  if(!cNeck)
    {
    class="kw">switch(type)
      {
      case defNeuronUShapeAttention:
        cNeck = new CNeuronUShapeAttention();
        if(!cNeck)
          class="kw">return false;
        break;
      case defNeuronConvOCL:
        cNeck = new CNeuronConvOCL();
        if(!cNeck)

「编码器网络层的逐层装配」

在 MT5 的 OpenCL 神经网络框架里,CreateEncoderDescriptions 负责把编码器各层描述压进 CArrayObj 容器。函数开头先判空,若传入指针为空就 new 一个,分配失败直接返回 false,避免后续空指针崩在 GPU 内核里。 输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,激活函数设 None、优化器走 ADAM。紧接着三层分别是 BatchNorm(batch=1000)、Dropout(probability=0.4f)和两次一维卷积 defNeuronConvOCL,卷积窗宽与步长都等于 BarDescr,把 (HistoryBars*BarDescr) 压到 HistoryBars 个通道。 第二层卷积的 window_out 被设为 EmbeddingSize,而第一层卷积的 window_out = EmbeddingSize/2,这两处常量直接决定嵌入向量的维度上限。你在调参时若改了 EmbeddingSize,必须同步核对 layer3/layer4 的 window_out,否则张量形状对不上,Load 模型时可能返回 false。 每层 Add 失败都走 delete descr + return false 的清理路径,这说明该构建过程不支持部分成功——任一层描述没进数组,整个编码器就废了,调用方应检查返回值再决定要不要跑训练。

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">1000;
  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 = defNeuronDropoutOCL;
  descr.count = prev_count;
  descr.probability = class="num">0.4f;
  descr.activation = None;
  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;
  prev_count = descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.step = descr.window;
  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">4
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronConvOCL;
  descr.count = prev_count;
  descr.window = prev_wout;
  descr.step = prev_wout;
  prev_wout = descr.window_out = EmbeddingSize;
  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;

编码器尾段与双网络描述装配

编码器第 6 层用了 U 型注意力结构,窗口滑动步长写死为 4,内部叠了 3 个子层、批大小 2;这种配置在贵金属 1 小时序列上可能让注意力更偏中期依赖,但显存占用会随 EmbeddingSize 放大。 第 7 到 9 层是三层普通 OCL 全连接,激活函数分别取 SIGMOID、LReLU、TANH,最后一层输出节点数由 BarDescr*(HistoryBars+NForecast) 算出,直接决定了解码端的还原维度。 第 10 层挂了 RevInDenormOCL 做反归一化,优化器指定 ADAM、层数 1,意味着逆标准化权重也进反向传播。外汇与贵金属杠杆高,这类端到端结构过拟合概率不低,上 MT5 前建议先拿历史样本跑一遍梯度检查。 CreateDescriptions 里 Actor / Critic 两个容器若为空会现场 new,但清空动作只调了 actor.Clear(),critic 旧描述不会自动释放,复制这段代码时得补一行 critic.Clear() 避免重复叠层。

MQL5 / C++
  descr.type = defNeuronLearnabledPE;
  descr.count = prev_count*prev_wout;
  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 = defNeuronUShapeAttention;
  descr.count = prev_count;
  descr.window = prev_wout;
  descr.step = class="num">4;
  descr.window_out = EmbeddingSize;
  descr.layers = class="num">3;
  descr.batch = class="num">2;
  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 = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation=SIGMOID;
  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 = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation=LReLU;
  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 = defNeuronBaseOCL;
  prev_count=descr.count = BarDescr*(HistoryBars+NForecast);
  descr.activation=TANH;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">10
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronRevInDenormOCL;
  prev_count = descr.count = prev_count;
  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">//---
  class="kw">return true;
  }
class="macro">#define          LatentLayer               class="num">9
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;

常见问题

把注意力计算封装成独立类能隔离权重生命周期,初始化链路只传输入输出维度,后续改头数或维度不影响主网络装配。
检查权重回传函数是否按层名映射,存取实现里要用一致的前缀,否则保存和加载会对不上导致读空。
可以,把网络描述文件丢给小布,它会按编码器逐层装配顺序比对输入输出维度,标出断点和维度不匹配的位置。
进阶实现多放在注意力前做层归一,能缓解梯度偏移;若训练震荡再试后置,以回传数值为判据。
会,贵金属外汇高频场景建议尾段不超过原宽两倍,用描述装配控制层数,先离线测单帧耗时再上实盘。