交易中的神经网络:搭配区段注意力的参数效率变换器(PSformer)·进阶篇
「卷积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 的指针地址,两者相同即证明省了一次拷贝。外汇与贵金属模型训练波动大,此类结构虽省资源,但过拟合概率仍高,需自行交叉验证。
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 的残差层必须同步改,否则编译能过但推理会越界。
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 中断,属高风险操作。
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 系列跑通再说。
<span class="keyword">class</span> CNeuronBaseOCL : <span class="keyword">class="kw">public</span> CObject { ... <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL); <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); }. ... } 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));?