交易中的神经网络:搭配区段注意力的参数效率变换器(PSformer)·进阶篇
📘

交易中的神经网络:搭配区段注意力的参数效率变换器(PSformer)·进阶篇

第 2/2 篇

「卷积SAMOCL层的参数共享与残差块封装」

在自定义神经网络层 CNeuronConvSAMOCL 里,ReplaceBuffer 方法负责把当前缓冲指针替换为外部 master 缓冲,且先判断是否为同一对象以避免无谓释放。若旧缓冲存在,会调用 BufferFree 并 delete,这一步直接决定显存/内存是否泄漏,MT5 回测中频繁调参时容易在这里卡顿。 InitPS 函数做的是「用已训练好的 master 层初始化自身」:先把 alpha、iBatch、iWindow、iStep 等 12 个成员变量逐个拷贝,再对 Output、PrevOutput、Gradient 做形状对齐的缓冲初始化。随后对 Weights、DeltaWeights 及一阶/二阶动量等 8 组缓冲统一走 ReplaceBuffer,实现权重指针共享而非深拷贝。 当 master.cWeightsSAM.Total() > 0 时,才初始化 SAM 注意力权重缓冲;若 fRho > 0 才处理 cWeightsSAMConv。这种条件初始化意味着:若你喂进来的 master 没开 SAM(Total 为 0),子层里对应缓冲会被主动 Free 并 Clear,省掉无效计算。 CNeuronPSBlock 直接继承该类,内部用长度为 2 的 acConvolution 数组加一个 cResidual 残差单元,把两次卷积与残差连接打包。你在 MT5 里想验证共享权重是否生效,可在 InitPS 后打印 Weights 与 master.Weights 的指针地址,两者相同即证明省了一次拷贝。外汇与贵金属模型训练波动大,此类结构虽省资源,但过拟合概率仍高,需自行交叉验证。

MQL5 / C++
class="type">void CNeuronConvSAMOCL::ReplaceBuffer(CBufferFloat *&buffer, CBufferFloat *master)
  {
   if(buffer==master)
     class="kw">return;
   if(!!buffer)
     {
       buffer.BufferFree();
       class="kw">delete buffer;
     }
class=class="str">"cmt">//---
   buffer = master;
  }
class="type">bool CNeuronConvSAMOCL::InitPS(CNeuronConvSAMOCL *master)
  {
   if(!master ||
      master.Type() != Type()
     )
       class="kw">return false;
   alpha = master.alpha;
   iBatch = master.iBatch;
   t = master.t;
   m_myIndex = master.m_myIndex;
   activation = master.activation;
   optimization = master.optimization;
   iWindow = master.iWindow;
   iStep = master.iStep;
   iWindowOut = master.iWindowOut;
   iVariables = master.iVariables;
   bTrain = master.bTrain;
   fRho = master.fRho;
   if(!InitBufferLike(Output, master.Output))
       class="kw">return false;
   if(!!master.getPrevOutput())
     if(!InitBufferLike(PrevOutput, master.getPrevOutput()))
         class="kw">return false;
   if(!InitBufferLike(Gradient, master.Gradient))
       class="kw">return false;
   ReplaceBuffer(Weights, master.Weights);
   ReplaceBuffer(DeltaWeights, master.DeltaWeights);
   ReplaceBuffer(FirstMomentum, master.FirstMomentum);
   ReplaceBuffer(SecondMomentum, master.SecondMomentum);
   ReplaceBuffer(WeightsConv, master.WeightsConv);
   ReplaceBuffer(DeltaWeightsConv, master.DeltaWeightsConv);
   ReplaceBuffer(FirstMomentumConv, master.FirstMomentumConv);
   ReplaceBuffer(SecondMomentumConv, master.SecondMomentumConv);
   if(master.cWeightsSAM.Total() > class="num">0)
     {
       CBufferFloat *buf = GetPointer(cWeightsSAM);
       if(!InitBufferLike(buf, GetPointer(master.cWeightsSAM)))
           class="kw">return false;
     }
   else
     {
       cWeightsSAM.BufferFree();
       cWeightsSAM.Clear();
     }
   if(fRho > class="num">0)
     {
       CBufferFloat *buf = GetPointer(cWeightsSAMConv);
       if(!InitBufferLike(buf, GetPointer(master.cWeightsSAMConv)))
           class="kw">return false;
     }
   else
     {
       cWeightsSAMConv.BufferFree();
       cWeightsSAMConv.Clear();
     }
   SetOpenCL(master.OpenCL);
class=class="str">"cmt">//---
   class="kw">return true;
  }
class CNeuronPSBlock :  class="kw">public CNeuronConvSAMOCL
  {
class="kw">protected:
   CNeuronConvSAMOCL acConvolution[class="num">2];
   CNeuronBaseOCL    cResidual;
   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 *NeuronOCL);
class="kw">public:
                    CNeuronPSBlock(class="type">void) {};
                   ~CNeuronPSBlock(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 units_count,

PSBlock 的初始化与参数共享细节

CNeuronPSBlock 是卷积类网络里带残差通路的一个子块,它的 Init 方法先调用基类 CNeuronConvSAMOCL::Init,把 window、window_out、units_count 等参数透传下去,再分别初始化两段卷积 acConvolution[0] 和 acConvolution[1]。 第一段卷积 acConvolution[0] 激活函数被设为 GELU,窗口尺寸沿用 iWindow 到 iWindowOut;第二段 acConvolution[1] 激活函数为 None,且窗口反向从 iWindowOut 回到 iWindow,这种对称结构在时序特征提取里常用于压缩后再还原。 残差连接 cResidual 直接复用 acConvolution[1] 的神经元数量,并通过 SetGradient 把梯度源指向第二段卷积的输出梯度,激活函数同样置为 None,避免额外非线性破坏残差恒等映射。 InitPS 方法负责从 master 块做参数共享:循环 2 次把 master 的两段卷积对象分别喂给当前块的 acConvolution[i].InitPS,最后再独立初始化残差层。在 MT5 里跑自定义神经网络 EA 时,若改了卷积层数,这里写死的 i<2 循环和索引 2 的残差层必须同步改,否则编译能过但推理会越界。

MQL5 / C++
class="type">bool CNeuronPSBlock::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                      class="type">uint window, class="type">uint window_out, class="type">uint units_count,
                      class="type">uint variables, class="type">float rho,
                      ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronConvSAMOCL::Init(numOutputs, myIndex, open_cl, window, window, window_out,
                               units_count, variables, rho, optimization_type, batch))
      class="kw">return false;
   if(!acConvolution[class="num">0].Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, iWindowOut, units_count,
                             iVariables, fRho, optimization, iBatch))
      class="kw">return false;
   acConvolution[class="num">0].SetActivationFunction(GELU);
   if(!acConvolution[class="num">1].Init(class="num">0, class="num">1, OpenCL, iWindowOut, iWindowOut, iWindow, units_count,
                             iVariables, fRho, optimization, iBatch))
      class="kw">return false;
   acConvolution[class="num">1].SetActivationFunction(None);
   if(!cResidual.Init(class="num">0, class="num">2, OpenCL, acConvolution[class="num">1].Neurons(), optimization, iBatch))
      class="kw">return false;
   if(!cResidual.SetGradient(acConvolution[class="num">1].getGradient(), true))
      class="kw">return false;
   cResidual.SetActivationFunction(None);
   SetActivationFunction(None);
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronPSBlock::InitPS(CNeuronPSBlock *master)
  {
   if(!CNeuronConvSAMOCL::InitPS((CNeuronConvSAMOCL*)master))
      class="kw">return false;
   for(class="type">int i = class="num">0; i < class="num">2; i++)
      if(!acConvolution[i].InitPS(master.acConvolution[i].AsObject()))
         class="kw">return false;
   if(!cResidual.Init(class="num">0, class="num">2, OpenCL, acConvolution[class="num">1].Neurons(), optimization, iBatch))
      class="kw">return false;
  }

◍ 残差块的前向、回传与权重更新链路

上面这段 CNeuronPSBlock 的成员函数,把 PixelShuffle 风格残差单元在 OpenCL 下的三类核心动作拆开了:前向推理、梯度回传、权重更新。每一环都靠布尔短路返回,只要任一子模块失败就整体中断,调试时若 feedForward 莫名返 false,优先查 acConvolution[0] 和 [1] 的句柄是否为空。 前向部分先推第一层卷积,再把它的输出喂给第二层卷积;随后 SumAndNormilize 用窗口参数 iWindow 与末位常数 1 做归一融合,最后才走 cResidual 的卷积前向。注意 cResidual 在初始化时被强制 SetActivationFunction(None),意味着残差支路本身不带非线性,非线性全压在主卷积路径上。 calcInputGradients 里有个分叉:当输入神经元激活函数为 None 时,直接把残差梯度叠加进输入梯度;否则先走 DeActivation 用输出与残差梯度算反向激活,再叠加。这个判断决定了你换不同激活函数时,梯度流是否会多一次元素级变换。 权重更新顺序和前向相反——先更新残差支路,再倒序更新两层卷积的输入权重。若你在 MT5 里改了卷积层数量或窗口尺寸 iWindow,必须同步核对这三处 UpdateInputWeights 的调用对象,否则会出现梯度写穿到错误缓冲区的问题。外汇与贵金属品种上跑这类 GPU 网络,显存溢出会直接让 EA 在实时 tick 中断,属高风险操作。

MQL5 / C++
  if(!cResidual.SetGradient(acConvolution[class="num">1].getGradient(), true))
        class="kw">return false;
   cResidual.SetActivationFunction(None);
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronPSBlock::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!acConvolution[class="num">0].FeedForward(NeuronOCL))
        class="kw">return false;
   if(!acConvolution[class="num">1].FeedForward(acConvolution[class="num">0].AsObject()))
      class="kw">return false;
   if(!SumAndNormilize(NeuronOCL.getOutput(), acConvolution[class="num">1].getOutput(), cResidual.getOutput(),
iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return false;
   if(!CNeuronConvSAMOCL::feedForward(cResidual.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronPSBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
     class="kw">return false;
   if(!CNeuronConvSAMOCL::calcInputGradients(cResidual.AsObject()))
      class="kw">return false;
   if(!acConvolution[class="num">0].calcHiddenGradients(acConvolution[class="num">1].AsObject()))
      class="kw">return false;
   if(!NeuronOCL.calcHiddenGradients(acConvolution[class="num">0].AsObject()))
      class="kw">return false;
   if(NeuronOCL.Activation() == None)
     {
      if(!SumAndNormilize(NeuronOCL.getGradient(), cResidual.getGradient(), NeuronOCL.getGradient(),
iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
     }
   else
     {
      if(!DeActivation(NeuronOCL.getOutput(), cResidual.getGradient(), cResidual.getPrevOutput(),
NeuronOCL.Activation()) ||
      !SumAndNormilize(NeuronOCL.getGradient(), cResidual.getPrevOutput(), NeuronOCL.getGradient(),
iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronPSBlock::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!CNeuronConvSAMOCL::updateInputWeights(cResidual.AsObject()))
      class="kw">return false;
   if(!acConvolution[class="num">1].UpdateInputWeights(acConvolution[class="num">0].AsObject()))
      class="kw">return false;
   if(!acConvolution[class="num">0].UpdateInputWeights(NeuronOCL))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }

「未完的 PSformer 落地与下一篇落点」

PSformer 的核心两块已经拆清:参数共享模块(PS)压住模型体量,区段时空注意力(SegAtt)同时吃局部与全局依赖。作者在原文里点明,这两样配合能在不牺牲预测品质的前提下把参数个数降下来,对 MT5 上跑时序模型算是省显存的一条路。 本篇实践只开了头,MQL5 侧的自研解释还没写完。已挂出的程序清单里,Research.mq5 到 Test.mq5 共 5 个智能系统分别管样本采集、Real-ORL 采集、训练、编码器训练和测试,另配 Trajectory.mqh 与 NeuroNet.mqh 两个类库加一份 OpenCL 代码库 NeuroNet.cl,想复现得先把这套 ZIP(2171.23 KB)下全。 代码里那个被读者 youwei_qing 揪出的点值得记一笔:CNeuronBaseOCL 的双参 feedForward 实际只调了单参版本,SecondInput 没进计算图。下面这段就是原类与方法声明的节选,双参重载直接 return 单参调用,属于显性空转。 下一篇才会拿真实历史数据回测验证,外汇与贵金属行情高波动,这类未经验证的架构直接上实盘风险偏高,建议先跟着把 Research 系列跑通再说。

MQL5 / C++
<span class="keyword">class</span> CNeuronBaseOCL : <span class="keyword">class="kw">public</span> CObject
{
&nbsp;&nbsp; ...
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { <span class="keyword">class="kw">return</span> feedForward(NeuronOCL); }.
&nbsp;&nbsp; ...
}
Actor.feedForward((CBufferFloat*)<span class="functions">GetPointer</span>(bAccount), <span class="number">class="num">1</span>, <span class="macro">false</span>, <span class="functions">GetPointer</span>(Encoder),LatentLayer); ?
Encoder.feedForward((CBufferFloat*)<span class="functions">GetPointer</span>(bState), <span class="number">class="num">1</span>, <span class="macro">false</span>, <span class="functions">GetPointer</span>(bAccount));?

常见问题

参数共享能大幅压低模型参数量,避免过拟合;不共享会让每层独立学权重,显存和训练成本翻倍,小样本下容易跑偏。
残差块内卷积核建议用正交或Kaiming初始化,偏置置零;坑在于共享参数若初始化尺度不一,前向数值会爆炸或消失。
小布可接入你的训练日志,自动标出梯度消失或权重更新异常块,并提示该调哪层学习率,省去手查链路。
先打印各PSBlock输入输出均值,回传看梯度范数是否逐层衰减;断点通常在共享权重更新没广播到的分支。
下一篇落点是把PSBlock拼成完整变换器做行情区段预测,含实盘滑点与外汇贵金属高风险下的概率校准。