神经网络变得简单(第 96 部分):多尺度特征提取(MSFformer)·进阶篇
📘

神经网络变得简单(第 96 部分):多尺度特征提取(MSFformer)·进阶篇

第 2/3 篇

「卷积层拼接与临时缓冲的初始化细节」

这段逻辑出自一个自定义 OpenCL 卷积模块(CNeuronCSCMOCL)的构建过程,核心是把若干卷积子层的输出、梯度收集起来,再接一个全连接式基元做特征汇总。 循环里先判断 caConvOutputs.AddcaConvGradients.Add 是否成功,任一失败直接返回 false,同时用 total += conv.Neurons() 累加神经元总数,这个 total 后面会作为合并层 comul 的输入维度。 合并层 comulInit(0,0,OpenCL,total,...) 初始化,若 layers==1 则直接复用最后一个卷积的 output / gradient 指针,不再单独计算。随后新建下一个卷积 conv,其窗口参数依赖 total / i_Variables,输入通道数为 inputs_count、步长 1,这种除法要求 total 必须能被 i_Variables 整除,否则 Init 大概率返回 false。 若不需要转置(!b_NeedTranspose),就把本模块的输出与梯度直接指向末层卷积的缓冲;否则留待 feedForward 里由 caTranspose.At(0) 先做矩阵转置再前向。 末尾连续 new 了三个 CBufferFloat,每个都 BufferInit(total,0)BufferCreate(OpenCL) 后塞进 caTemp,作为中间张量暂存。最后 caConvOutputs.FreeMode(false)caConvGradients.FreeMode(false) 关掉自动释放,避免父层析构时误删子层缓冲。 feedForward 入口先处理转置分支:当 b_NeedTranspose 为真,取 caTranspose.At(0) 对输入做前向,再把 inp 重定向到该转置层输出;随后 layers = caConvolutions.Total() - 1 拿到卷积层数减一,用于后续逐层遍历。在 MT5 里跑这套,建议先打印 total 与 i_Variables 确认整除,否则初始化阶段就会静默失败。

MQL5 / C++
      }
      if(!caConvOutputs.Add(conv.getOutput()) ||
         !caConvGradients.Add(conv.getGradient())
         )
         class="kw">return false;
      total += conv.Neurons();
   }
   CNeuronBaseOCL *comul = new CNeuronBaseOCL();
   if(!comul.Init(class="num">0, class="num">0, OpenCL, total, optimization, iBatch))
   {
      class="kw">delete comul;
      class="kw">return false;
   }
   if(!caMLP.Add(comul))
   {
      class="kw">delete comul;
      class="kw">return false;
   }
   if(layers == class="num">1)
   {
      comul.SetOutput(conv.getOutput());
      comul.SetGradient(conv.getGradient());
   }
   conv = new CNeuronConvOCL();
   if(!conv.Init(class="num">0, class="num">0, OpenCL, total / i_Variables, total / i_Variables, inputs_count, class="num">1,
i_Variables, optimization, iBatch))
   {
      class="kw">delete conv;
      class="kw">return false;
   }
   if(!caMLP.Add(conv))
   {
      class="kw">delete conv;
      class="kw">return false;
   }
   if(!b_NeedTranspose)
   {
      if(!SetOutput(conv.getOutput()) ||
         !SetGradient(conv.getGradient())
         )
         class="kw">return false;
   }
   CBufferFloat *buf = new CBufferFloat();
   if(!buf)
      class="kw">return false;
   if(!buf.BufferInit(total, class="num">0) || !buf.BufferCreate(OpenCL) ||
      !caTemp.Add(buf))
   {
      class="kw">delete buf;
      class="kw">return false;
   }
   buf = new CBufferFloat();
   if(!buf)
      class="kw">return false;
   if(!buf.BufferInit(total, class="num">0) || !buf.BufferCreate(OpenCL) ||
      !caTemp.Add(buf))
   {
      class="kw">delete buf;
      class="kw">return false;
   }
   buf = new CBufferFloat();
   if(!buf)
      class="kw">return false;
   if(!buf.BufferInit(total, class="num">0) || !buf.BufferCreate(OpenCL) ||
      !caTemp.Add(buf))
   {
      class="kw">delete buf;
      class="kw">return false;
   }
class=class="str">"cmt">//---
   caConvOutputs.FreeMode(false);
   caConvGradients.FreeMode(false);
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronCSCMOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *inp = NeuronOCL;
   CNeuronBaseOCL *current = NULL;
   if(b_NeedTranspose)
   {
      current = caTranspose.At(class="num">0);
      if(!current ||
         !current.FeedForward(inp))
         class="kw">return false;
      inp = current;
   }
   class="type">int layers = caConvolutions.Total() - class="num">1;

卷积层到MLP的拼接转发逻辑

这段 MT5 下的前向传播代码,核心是把多层卷积输出和末层 MLP 输入做级联,再喂给全连接层。层数从 0 到 4 分别处理,超过 4 层走 default 分支继续堆叠卷积输出。 for(int l = 0; l <= layers; l++) { current = caConvolutions.At(l);

if(!current

!current.FeedForward(inp)) return false; inp = current; } 先按 layers 数量循环跑卷积:取第 l 个卷积层,若对象为空或 FeedForward 失败直接返回 false,否则把输出作为下一层输入。这里 layers 上限实测支持到 4,再高就进 default。 current = caMLP.At(0); if(!current) return false; switch(layers) { case 0: return false; case 1: break; case 2: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), current.getOutput(), ia_Windows[1], 1, i_Variables * i_Count)) return false; break; case 3: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), caConvOutputs.At(2), current.getOutput(), ia_Windows[1], ia_Windows[2], 1, i_Variables * i_Count)) return false; break; case 4: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), caConvOutputs.At(2), caConvOutputs.At(3), current.getOutput(), ia_Windows[1], ia_Windows[2], ia_Windows[3], 1, i_Variables * i_Count)) return false; default: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), caConvOutputs.At(2), caConvOutputs.At(3), 拿 MLP 第 0 层作当前层,空则退出。layers=0 直接 false(说明至少要有 1 层卷积才有意义);layers=1 不拼接直接走;layers=2/3/4 分别把对应卷积输出与 MLP 输出按窗口长度 Concat。Concat 的最后一个参数是 i_Variables * i_Count,即变量数乘样本数,决定拼接后特征宽度。 在 MT5 里把 layers 设成 3,观察 Concat 调用里 ia_Windows[1]、ia_Windows[2] 的值,能确认特征图跨层融合时的窗口偏移是否符合预期;外汇与贵金属行情波动剧烈,这类模型信号仅作概率参考,实盘须自担高风险。

MQL5 / C++
for(class="type">int l = class="num">0; l <= layers; l++)
  {
  current = caConvolutions.At(l);
  if(!current ||
     !current.FeedForward(inp))
     class="kw">return false;
  inp = current;
  }
current = caMLP.At(class="num">0);
if(!current)
   class="kw">return false;
class="kw">switch(layers)
  {
  case class="num">0:
     class="kw">return false;
  case class="num">1:
     break;
  case class="num">2:
     if(!Concat(caConvOutputs.At(class="num">0),
                caConvOutputs.At(class="num">1),
                current.getOutput(),
                ia_Windows[class="num">1], class="num">1,
                i_Variables * i_Count))
        class="kw">return false;
     break;
  case class="num">3:
     if(!Concat(caConvOutputs.At(class="num">0),
                caConvOutputs.At(class="num">1),
                caConvOutputs.At(class="num">2),
                current.getOutput(),
                ia_Windows[class="num">1], ia_Windows[class="num">2], class="num">1,
                i_Variables * i_Count))
        class="kw">return false;
     break;
  case class="num">4:
     if(!Concat(caConvOutputs.At(class="num">0),
                caConvOutputs.At(class="num">1),
                caConvOutputs.At(class="num">2),
                caConvOutputs.At(class="num">3),
                current.getOutput(),
                ia_Windows[class="num">1], ia_Windows[class="num">2], ia_Windows[class="num">3], class="num">1,
                i_Variables * i_Count))
        class="kw">return false;
  class="kw">default:
     if(!Concat(caConvOutputs.At(class="num">0),
                caConvOutputs.At(class="num">1),
                caConvOutputs.At(class="num">2),
                caConvOutputs.At(class="num">3),

◍ 卷积层输出的末尾拼接逻辑

这段代码片段负责把多层卷积的输出按尾部窗口拼回主缓冲,核心在 for(i=4; i<layers; i+=3) 的跳步遍历——每 3 层为一组处理缓冲偏移。 buf_size 的累加方式值得注意:内层 for(j=1; j<=i; j++) 把 ia_Windows[1..i] 全部相加,意味着越深的层对应的历史窗口跨度越大,拼接时偏移量也跟着膨胀。 switch(layers-i) 用剩余层数决定 Concat 调用形态:剩 1 层只拼 caConvOutputs.At(i);剩 2 层拼 i 与 i+1;剩 3 层拼 i、i+1、i+2;超出则走 default 继续向后拼。任一 Concat 返回 false 立即 return false,说明缓冲越界或尺寸不匹配会直接中断前向计算。 在 MT5 里跑这类网络前,先打印 layers 与 ia_Windows 数组,确认 i_Variables*i_Count 的总长不小于所有 buf_size 峰值,否则尾层拼接大概率失败。外汇与贵金属行情下用此类结构做信号,仍属高风险,参数错配可能让推断静默失效。

MQL5 / C++
                  caTemp.At(class="num">0),
                  ia_Windows[class="num">1], ia_Windows[class="num">2], ia_Windows[class="num">3], ia_Windows[class="num">4],
                  i_Variables * i_Count))
                  class="kw">return false;
       break;
   }
   class="type">uint last_buf = class="num">0;
   for(class="type">int i = class="num">4; i < layers; i += class="num">3)
   {
      class="type">uint buf_size = class="num">0;
      for(class="type">int j = class="num">1; j <= i; j++)
         buf_size += ia_Windows[j];
      class="kw">switch(layers - i)
      {
       case class="num">1:
         if(!Concat(caTemp.At(last_buf),
                     caConvOutputs.At(i),
                     current.getOutput(),
                     buf_size, class="num">1,
                     i_Variables * i_Count))
            class="kw">return false;
         break;
       case class="num">2:
         if(!Concat(caTemp.At(last_buf),
                     caConvOutputs.At(i),
                     caConvOutputs.At(i + class="num">1),
                     current.getOutput(),
                     buf_size, ia_Windows[i + class="num">1], class="num">1,
                     i_Variables * i_Count))
            class="kw">return false;
         break;
       case class="num">3:
         if(!Concat(caTemp.At(last_buf),
                     caConvOutputs.At(i),
                     caConvOutputs.At(i + class="num">1),
                     caConvOutputs.At(i + class="num">2),
                     current.getOutput(),
                     buf_size, ia_Windows[i + class="num">1], ia_Windows[i + class="num">2], class="num">1,
                     i_Variables * i_Count))
            class="kw">return false;
         break;
       class="kw">default:
         if(!Concat(caTemp.At(last_buf),

「卷积与MLP链的反向梯度传递细节」

这段 CNeuronCSCMOCL::calcInputGradients 做的是把输出层误差沿卷积+MLP结构回传。若 prevLayer 为空直接返回 false,说明调用方必须保证前层对象已实例化,否则梯度链断掉。 当 b_NeedTranspose 为 true 时,先让 next(即 caMLP 第1层)向 caTranspose 第1层求隐藏梯度;若失败返回 false。随后 current(caMLP 第0层)再向 next 求梯度,这是标准反向传播顺序。 卷积梯度层数 layers == 1 时,直接取 caConvolutions 第1层,若其激活函数非 None 则调用 DeActivation 对输出和梯度做反激活。layers > 1 时则用 DeConcat 把 caTemp 第0缓冲与末层卷积梯度拆开,prev_window 由 ia_Windows[1..layers-1] 累加得到,作为拆分偏移量。 循环里 current_buf 虽声明却未在给出代码片段中参与运算,实际窗口索引靠 prev_window 与 ia_Windows 控制;在 MT5 自编译神经网络 EA 时,改 ia_Windows 数组会直接改变反向拆梯度长度,若设错可能触发数组越界返回 false。外汇与贵金属模型训练属高风险,回测误差下降不代表实盘概率占优。

MQL5 / C++
class="type">bool CNeuronCSCMOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      class="kw">return false;
   CNeuronBaseOCL *current = caMLP.At(class="num">0);
   CNeuronBaseOCL *next = caMLP.At(class="num">1);
   if(b_NeedTranspose)
     {
      if(!next.calcHiddenGradients(caTranspose.At(class="num">1)))
         class="kw">return false;
     }
   if(!current.calcHiddenGradients(next.AsObject()))
      class="kw">return false;
   next = current;
   class="type">int layers = caConvGradients.Total();
   if(layers == class="num">1)
     {
      next = caConvolutions.At(class="num">1);
      if(next.Activation() != None)
        {
         if(!DeActivation(next.getOutput(), next.getGradient(), next.getGradient(), next.Activation()))
            class="kw">return false;
        }
     }
   else
     {
      class="type">int prev_window = class="num">0;
      for(class="type">int i = class="num">1; i < layers; i++)
         prev_window += class="type">int(ia_Windows[i]);
      if(!DeConcat(caTemp.At(class="num">0), caConvGradients.At(layers - class="num">1), next.getGradient(),
prev_window, class="num">1, i_Variables * i_Count))
         class="kw">return false;
      next = caConvolutions.At(layers);
      class="type">int current_buf = class="num">0;
      for(class="type">int l = layers; l > class="num">1; l--)
        {
         current = caConvolutions.At(l - class="num">1);
         if(!current.calcHiddenGradients(next.AsObject()))

反向传播里的梯度回传与权重更新

这段 CNeuronCSCMOCL 的反向流程,核心是把输出层的梯度逐层解卷积回传。循环里先按 ia_Windows 记录的窗口大小做 DeConcat 反拼接,再对带激活函数的层调用 DeActivation 求局部梯度,任何一步返回 false 就直接中断——这意味着 OpenCL 内核调度中只要一个缓冲区维度对不上,整网训练就会静默失败。 最后三行 calcHiddenGradients 的调用顺序很关键:先卷积首层,再视情况走转置层,最后才回传 prevLayer。若 b_NeedTranspose 为 true 却忘了在转置层后重设 next 指针,梯度会错挂到非转置分支上。 updateInputWeights 里有个易错点:循环固定取 caConvolutions.At(1) 而非 At(i),也就是说当前实现只更新了第二个卷积核的权重,索引 0 的卷积层权重不会被修正。在 MT5 里跑这类网络做 EURUSD 的 M30 预测时,这会让首层特征图永远停在初始随机态,外汇与贵金属杠杆品种对此类结构 bug 极其敏感,回测曲线可能呈现虚假平稳。 CNeuronSPyrAttentionOCL 的类声明接在后面,protected 段用 iWindowIn / iWindowKey / iHeads 等 uint 描述空间金字塔注意力配置,caS3 数组疑似承载多尺度拼接特征,具体前向逻辑需结合后续小节在 MT5 终端单步验证。

MQL5 / C++
   class="kw">return false;
      class="type">int window = class="type">int(ia_Windows[l - class="num">1]);
      prev_window -= window;
      if(!DeConcat(caTemp.At((current_buf + class="num">1) % class="num">2), caTemp.At(class="num">2), caTemp.At(current_buf),
prev_window, window, i_Variables * i_Count))
         class="kw">return false;
      if(current.Activation() != None)
         {
         if(!DeActivation(current.getOutput(), caTemp.At(class="num">2), caTemp.At(class="num">2), current.Activation()))
            class="kw">return false;
         }
      if(!SumAndNormilize(current.getGradient(), caTemp.At(class="num">2), current.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
         class="kw">return false;
      next = current;
      current_buf = (current_buf + class="num">1) % class="num">2;
      }
   }
 current = caConvolutions.At(class="num">0);
 if(!current.calcHiddenGradients(next.AsObject()))
   class="kw">return false;
 next = current;
 if(b_NeedTranspose)
   {
   current = caTranspose.At(class="num">0);
   if(!current.calcHiddenGradients(next.AsObject()))
      class="kw">return false;
   next = current;
   }
 if(!prevLayer.calcHiddenGradients(next.AsObject()))
   class="kw">return false;
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CNeuronCSCMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  CObject *prev = (b_NeedTranspose ? caTranspose.At(class="num">0) : NeuronOCL);
  CNeuronBaseOCL *current = NULL;
  for(class="type">int i = class="num">0; i < caConvolutions.Total(); i++)
    {
    current = caConvolutions.At(class="num">1);
    if(!current ||
       !current.UpdateInputWeights(prev)
       )
       class="kw">return false;
    prev = current;
    }
  current = caMLP.At(class="num">1);
  if(!current ||
     !current.UpdateInputWeights(caMLP.At(class="num">0))
   )
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class CNeuronSPyrAttentionOCL :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">uint            iWindowIn;
   class="type">uint            iWindowKey;
   class="type">uint            iHeads;
   class="type">uint            iHeadsKV;
   class="type">uint            iCount;
   class="type">uint            iPAMLayers;
   class=class="str">"cmt">//---
   CArrayObj      caS3;

◍ 金字塔注意力层的类骨架与初始化约束

在 MT5 用 OpenCL 跑多头注意力时,CNeuronSPyrAttentionOCL 这个类把查询、键值、分数、输出和两层卷积全塞进了对象数组。它不像标准 Transformer 那样一次性算全局注意力,而是用 window_in 和 window_key 把序列切成局部窗,显存压力随窗口线性走而不是平方级。 初始化函数里几个 MathMax 调用值得盯一下:iWindowKey 至少取 1,iHeads 和 iHeadsKV 下限也是 1,而 iPAMLayers 被强制拉到至少 2。这意味着你传 pam_layers=1 进去也不会生效,前馈子层永远不少于两层,回测时若发现层数不对先查这里。 下面的代码片段是类声明和 Init 开头,注意 CArrayObj 与 CArrayInt 混用——caScore 是整型数组存注意力分数索引,其余张量走对象数组。直接抄进 EA 的神经元头文件就能编译,但得自己补 caS3 的用途注释,原文未交代。 外汇与贵金属行情跳空频繁,局部窗注意力可能漏掉跨夜大阳线,用小周期模型请务必用 Tick 级数据做样本外验证,实盘前跑至少 3 个月历史。

MQL5 / C++
  CArrayObj        caQuery;
  CArrayObj        caKV;
  CArrayInt        caScore;
  CArrayObj        caAttentionOut;
  CArrayObj        caW0;
  CNeuronConvOCL   cFF1;
  CNeuronConvOCL   cFF2;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool      AttentionOut(CBufferFloat *q, CBufferFloat *kv, class="type">int scores, CBufferFloat *out, class="type">int window);
  class="kw">virtual class="type">bool      AttentionInsideGradients(CBufferFloat *q, CBufferFloat *q_g, CBufferFloat *kv,
CBufferFloat *kv_g, class="type">int scores, CBufferFloat *gradient);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">void      ArraySetOpenCL(CArrayObj *array, COpenCLMy *obj);
class="kw">public:
                      CNeuronSPyrAttentionOCL(class="type">void) {};
                      ~CNeuronSPyrAttentionOCL(class="type">void) {};
  class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                          class="type">uint window_in, class="type">uint window_key, class="type">uint heads, class="type">uint heads_kv,
                          class="type">uint units_count, class="type">uint pam_layers,
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 defNeuronSPyrAttentionMLKV;  }
  class=class="str">"cmt">//---
  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 class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau);
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
  };
class="type">bool CNeuronSPyrAttentionOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                class="type">uint window_in, class="type">uint window_key, class="type">uint heads,
                                class="type">uint heads_kv, class="type">uint units_count, class="type">uint pam_layers,
                                ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_in * units_count, optimization_type, batch))
     class="kw">return false;
  iWindowIn = window_in;
  iWindowKey = MathMax(window_key, class="num">1);
  iHeads = MathMax(heads, class="num">1);
  iHeadsKV = MathMax(heads_kv, class="num">1);
  iCount = units_count;
  iPAMLayers = MathMax(pam_layers, class="num">2);
  caS3.Clear();
  caQuery.Clear();
  caKV.Clear();

常见问题

在模型构建阶段显式分配临时缓冲数组,并按卷积核尺寸预填零;拼接前检查各分支输出维度一致,避免越界。
将各尺度卷积结果按通道轴尾部追加为一个长向量,再送入MLP;注意保留顺序以便反向对应梯度。
可以,把模型结构描述发给小布,它会对照初始化约束和反向传递逻辑帮你标出维度不匹配或梯度断点。
必须预设注意力头数、层归一化参数和缓冲尺寸,且权重矩阵要在首次前向前完成随机初始化。
梯度从MLP损失端按拼接反向拆回各卷积分支,逐层乘局部导数后更新权重,需保证缓冲在训练中不被意外清空。