交易中的神经网络:基于 ResNeXt 模型的多任务学习·进阶篇
◍ ResNeXt瓶颈层的初始化链路
在 MT5 的 OpenCL 神经网络封装里,CNeuronResNeXtBottleneck 的 Init 方法先把输出单元数算出来:units_out = (units_count - window + step - 1) / step + 1。这个整数除法决定了卷积层后续特征图的时间维度,调 step 或 window 会直接改掉显存占用量。 初始化顺序是一条硬链路:先建主卷积 CNeuronConvOCL,再依次挂 cProjectionIn、cNormalizeIn、cTransposeIn,最后进 Feature Extraction 与 Projection Out。任何一步返回 false,整个层就废了,不会半初始化。 注意 cNormalizeIn 和 cNormalizeFeature 都强制设了 LReLU 激活,而 cTransposeIn 的激活直接复用 cNormalizeIn 的,改前者就连带改后者。想验证的话,把 group_size 设成 4、groups 设成 32,看 units_out 随 window 从 3 变到 7 时卷积核申请是否线性涨。外汇与贵金属模型训练波动大,显存爆了只会静默失败,高风险。
class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups, 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 defNeuronResNeXtBottleneck; } 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">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 CLayerDescription* GetLayerInfo(class="type">void) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) 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="type">bool CNeuronResNeXtBottleneck::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint window, class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { class="type">int units_out = ((class="type">int)units_count - (class="type">int)window + (class="type">int)step - class="num">1) / (class="type">int)step + class="num">1; if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, group_size * groups, group_size * groups, chanels_out, units_out, class="num">1, optimization_type, batch)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Projection In class="type">uint index = class="num">0; if(!cProjectionIn.Init(class="num">0, index, OpenCL, chanels_in, chanels_in, group_size * groups, units_count, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; index++; if(!cNormalizeIn.Init(class="num">0, index, OpenCL, cProjectionIn.Neurons(), iBatch, optimization)) class="kw">return class="kw">false; cNormalizeIn.SetActivationFunction(LReLU); index++; if(!cTransposeIn.Init(class="num">0, index, OpenCL, units_count, groups, group_size, optimization, iBatch)) class="kw">return class="kw">false; cTransposeIn.SetActivationFunction((ENUM_ACTIVATION)cNormalizeIn.Activation()); class=class="str">"cmt">//--- Feature Extraction index++; if(!cFeatureExtraction.Init(class="num">0, index, OpenCL, group_size * window, group_size * step, group_size, units_out, groups, optimization, iBatch)) class="kw">return class="kw">false; index++; if(!cNormalizeFeature.Init(class="num">0, index, OpenCL, cFeatureExtraction.Neurons(), iBatch, optimization)) class="kw">return class="kw">false; cNormalizeFeature.SetActivationFunction(LReLU); class=class="str">"cmt">//--- Projection Out index++;
残差分支在 OpenCL 上的前向链路
ResNeXt 的瓶颈层把一次前向拆成三段:投影入、特征抽取、投影出。每段都依赖前一段的 AsObject() 输出做输入,任何一步 FeedForward 返回 false 都会直接中断,整个神经元不再继续计算。 看 cTransposeOut 的初始化,Init 里第 2 个参数 index 决定该层在通道序列里的位置,group_size 与 units_out 共同约束显存排布;若这两个值和前面 cFeatureExtraction 的输出维度对不上,OpenCL 内核大概率报维度不匹配。 CNeuronResNeXtResidual 类在 Conv 基础上挂了四个成员:cTransposeIn、cProjectionTime、cNormalizeTime、cTransposeOut。它override 了 feedForward / updateInputWeights / calcInputGradients,意味着残差时间维的梯度回传也走独立通路,不会复用父类的卷积反向。 Type() 返回 defNeuronResNeXtResidual,这是 MT5 神经网络框架里区分层类型的整型标签。在终端里自己写诊断脚本时,可以用这一返回值过滤出残差层,单独打印其激活分布,验证是否出现梯度消失倾向。外汇与贵金属模型训练波动大,此类 GPU 层易因显存碎片失败,实操请先在模拟环境跑通再上实盘。
if(!cTransposeOut.Init(class="num">0, index, OpenCL, groups, units_out, group_size, optimization, iBatch)) class="kw">return class="kw">false; cTransposeOut.SetActivationFunction((ENUM_ACTIVATION)cNormalizeFeature.Activation()); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronResNeXtBottleneck::feedForward(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- Projection In if(!cProjectionIn.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!cNormalizeIn.FeedForward(cProjectionIn.AsObject())) class="kw">return class="kw">false; if(!cTransposeIn.FeedForward(cNormalizeIn.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Feature Extraction if(!cFeatureExtraction.FeedForward(cTransposeIn.AsObject())) class="kw">return class="kw">false; if(!cNormalizeFeature.FeedForward(cFeatureExtraction.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Projection Out if(!cTransposeOut.FeedForward(cNormalizeFeature.AsObject())) class="kw">return class="kw">false; class="kw">return CNeuronConvOCL::feedForward(cTransposeOut.AsObject()); } class CNeuronResNeXtResidual: class="kw">public CNeuronConvOCL { class="kw">protected: CNeuronTransposeOCL cTransposeIn; CNeuronConvOCL cProjectionTime; CNeuronBatchNormOCL cNormalizeTime; CNeuronTransposeOCL cTransposeOut; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronResNeXtResidual(class="type">void){}; ~CNeuronResNeXtResidual(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 chanels_in, class="type">uint chanels_out, class="type">uint units_in, class="type">uint units_out, 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 defNeuronResNeXtResidual; } 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">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 CLayerDescription* GetLayerInfo(class="type">void) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override;
「ResNeXt 残差与块结构的初始化链路」
CNeuronResNeXtResidual 的 Init 先把基类 CNeuronConvOCL 按 chanels_in→chanels_in→chanels_out、units_out 与卷积核尺寸 1 拉起来,任一环节失败直接返回 false。随后用局部 index 从 0 递增,依次挂接 cTransposeIn、cProjectionTime、cNormalizeTime、cTransposeOut 四个子层,每层都依赖前一层神经元数与批大小 iBatch,顺序错一处前向就无法贯通。 前向 feedForward 分两条投影轴:时间轴上先转置入 cTransposeIn,再过 cProjectionTime 与 cNormalizeTime;通道轴由 cTransposeOut 承接归一化结果,最后才交给 CNeuronConvOCL::feedForward。实测若 cNormalizeTime 的神经元数没对齐 cProjectionTime.Neurons(),会在第三步返回 false,MT5 专家日志里看不到具体层名,只能靠逐层断点排查。 CNeuronResNeXtBlock 本身不重写 Init 声明,而是聚合 cBottleneck、cResidual 与 cBuffer,把 feedForward / updateInputWeights / calcInputGradients 三个虚函数留给派生实现。想验证这套结构,开 MT5 把 iBatch 设成 32、chanels_in=64 跑一次 Init,观察 OpenCL 上下文是否报缓冲区越界。
class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override; }; class="type">bool CNeuronResNeXtResidual::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint units_in, class="type">uint units_out, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, chanels_in, chanels_in, chanels_out, units_out, class="num">1, optimization_type, batch)) class="kw">return class="kw">false; class="type">int index=class="num">0; if(!cTransposeIn.Init(class="num">0, index, OpenCL, units_in, chanels_in, optimization, iBatch)) class="kw">return class="kw">false; index++; if(!cProjectionTime.Init(class="num">0, index, OpenCL, units_in, units_in, units_out, chanels_in, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; index++; if(!cNormalizeTime.Init(class="num">0, index, OpenCL, cProjectionTime.Neurons(), iBatch, optimization)) class="kw">return class="kw">false; index++; if(!cTransposeOut.Init(class="num">0, index, OpenCL, chanels_in, units_out, optimization, iBatch)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronResNeXtResidual::feedForward(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- Projection Timeline if(!cTransposeIn.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!cProjectionTime.FeedForward(cTransposeIn.AsObject())) class="kw">return class="kw">false; if(!cNormalizeTime.FeedForward(cProjectionTime.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Projection Chanels if(!cTransposeOut.FeedForward(cNormalizeTime.AsObject())) class="kw">return class="kw">false; class="kw">return CNeuronConvOCL::feedForward(cTransposeOut.AsObject()); } class CNeuronResNeXtBlock : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iChanelsOut; CNeuronResNeXtBottleneck cBottleneck; CNeuronResNeXtResidual cResidual; CBufferFloat cBuffer; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronResNeXtBlock(class="type">void){}; ~CNeuronResNeXtBlock(class="type">void){}; class=class="str">"cmt">//---
◍ ResNeXt 残差块在 MT5 里的初始化链路
在 MT5 的 OpenCL 神经网络框架里,CNeuronResNeXtBlock 把瓶颈卷积与残差支路拼成一块可训练单元。它的类声明里先看 Type() 返回 defNeuronResNeXtBlock,这是运行时区分层类型的硬标识,调参或排查图层堆叠错误时得靠它。
Init 方法是落地关键。units_out 用 ((units_count - window + step - 1) / step + 1) 算滑动卷积后的输出长度,比如 window=3、step=1、units_count=10 时得到 8 个时间步单元。若 CNeuronBaseOCL 基类初始化失败或瓶颈层 cBottleneck 参数不匹配,函数直接返 false,块不生效。
残差支路 cResidual 初始化后,代码把它的梯度源指向 cBottleneck.getGradient() 并置共享标志,意味着两条路的反向传播梯度在此汇流。外汇与贵金属行情用这类结构做特征提取时波动剧烈,模型过拟合概率偏高,上 MT5 跑前建议先用小 batch 验证显存与梯度连通。
下面这段是类声明与 Init 实现的原文,逐行拆完你能直接抄进自己的 EA 工程里改通道数。
class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint window, class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups, 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 defNeuronResNeXtBlock; } 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">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 CLayerDescription* GetLayerInfo(class="type">void) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) 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="type">bool CNeuronResNeXtBlock::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint chanels_in, class="type">uint chanels_out, class="type">uint window, class="type">uint step, class="type">uint units_count, class="type">uint group_size, class="type">uint groups, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { class="type">int units_out = ((class="type">int)units_count - (class="type">int)window + (class="type">int)step - class="num">1) / (class="type">int)step + class="num">1; if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_out * chanels_out, optimization_type, batch)) class="kw">return class="kw">false; iChanelsOut = chanels_out; class="type">int index = class="num">0; if(!cBottleneck.Init(class="num">0, index, OpenCL, chanels_in, chanels_out, window, step, units_count, group_size, groups, optimization, iBatch)) class="kw">return class="kw">false; index++; if(!cResidual.Init(class="num">0, index, OpenCL, chanels_in, chanels_out, units_count, units_out, optimization, iBatch)) class="kw">return class="kw">false; if(!cResidual.SetGradient(cBottleneck.getGradient(), true)) class="kw">return class="kw">false; if(!SetGradient(cBottleneck.getGradient(), true)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
残差块的前向与梯度回传实现
ResNeXt 结构里,一个神经块的前向计算先把瓶颈层和残差支路分别喂入 OpenCL 神经元对象,任何一步失败直接返回 false,避免脏数据进网络。 前向收尾调用 SumAndNormilize 把两条支路的输出按通道数 iChanelsOut 做加和与归一化,末位参数传 true 表示训练态归一,四个 0/1 控制偏移与缩放区间。 梯度回传时先取上层梯度暂存到 temp,若本地 cBuffer 的 OpenCL 上下文或长度不匹配就重新按 temp 初始化,否则旧缓冲会污染反向传播。 calcInputGradients 里两次调用 calcHiddenGradients 分别作用于瓶颈与残差对象,再用 SumAndNormilize(temp, 梯度, temp, 1, false, ...) 把两路梯度合并写回,false 代表推理态不重新归一。 在 MT5 里把这段接进你自己的 CNeuronResNeXtBlock,重点核对 cBuffer.BufferInitLike 的触发条件——上下文不一致时若漏掉重初始化,GPU 显存大概率报越界。
if(!cBottleneck.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!cResidual.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!SumAndNormilize(cBottleneck.getOutput(), cResidual.getOutput(), Output, iChanelsOut, true, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; class=class="str">"cmt">//--- result class="kw">return true; } class="type">bool CNeuronResNeXtBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return class="kw">false; if(!NeuronOCL.calcHiddenGradients(cBottleneck.AsObject())) class="kw">return class="kw">false; CBufferFloat *temp = NeuronOCL.getGradient(); if(cBuffer.GetOpenCL() != OpenCL || cBuffer.Total() != temp.Total()) { if(!cBuffer.BufferInitLike(temp)) class="kw">return class="kw">false; } if(!NeuronOCL.SetGradient(GetPointer(cBuffer), class="kw">false)) class="kw">return class="kw">false; if(!NeuronOCL.calcHiddenGradients(cResidual.AsObject())) class="kw">return class="kw">false; if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; if(!NeuronOCL.SetGradient(temp, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
「下一篇要做的真实验证」
这一篇把 ResNeXt 的主干在 MT5 里搭起来了,核心思路是用一个共享编码器接多个任务头,去吃高维时间序列里的分类和回归。相比单任务网络,这种结构在样本有限时过拟合的概率更低,但代价是训练吞吐更吃显卡。 随文给了 7 个文件:Research.mq5、ResearchRealORL.mq5、Study.mq5、Test.mq5 四个智能系统,加上 Trajectory.mqh、NeuroNet.mqh、NeuroNet.cl 三个类库,ZIP 体积 2430.99 KB。想跑通的话,先挂 Research.mq5 收集样本,再用 Study.mq5 训,最后 Test.mq5 测,缺了 OpenCL 运行环境会直接编译不过。 下一篇才会把多任务框架拼完,并在真实历史数据上给出回测数字。外汇和贵金属杠杆高、滑点跳空频繁,即便模型在历史集上表现稳,实盘也可能因流动性断裂而失效,别直接拿训练权重去Live账户。