交易中的神经网络:受控分段·进阶篇
「GEGWA 神经元的接口与初始化骨架」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronGEGWA 这个类把多头注意力与门控结构压进同一个计算节点。它的两个 updateInputWeights 重载都被 override 成直接返回 false,说明该层不沿用基类的权重回传逻辑,梯度路径改由 WeightsUpdate 与内部子层自行处理。 构造函数只初始化了 bAddNeckGradient(false),析构为空,Type() 恒返回 defNeuronGEGWA 用于运行时辨识节点类型。文件交互靠 Save / Load 两个虚函数落地,方便把训好的权重存盘再热加载。 Init 是真正干活的地方:先以 window*units_count 作为基类的输入维度调 CNeuronBaseOCL::Init,任一环节失败立即返回 false;随后初始化 cAttention[0],传入 window、window_key、heads、units_count、layers 等参数。你在 EA 里若想接这套结构,至少得先确认传入的 window 与 units_count 乘积不超过显存允许的单层宽度,否则 Init 会静默失败。 AddNeckGradient 这个开关控制是否把 neck 梯度并入更新,默认关。实盘外汇或贵金属模型用这类结构时波动率高,过拟合风险偏大,建议先在小样本回测里把 flag 开着观察梯度流向再决定。
class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class="kw">public: CNeuronGEGWA(class="type">void) : bAddNeckGradient(class="kw">false) {}; ~CNeuronGEGWA(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_key, class="type">uint heads, class="type">uint units_count, class="type">uint window_kv, class="type">uint heads_kv, class="type">uint units_count_kv, 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 defNeuronGEGWA; } 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=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class=class="str">"cmt">//--- class="kw">virtual CNeuronBaseOCL* GetInsideLayer(const class="type">int layer) const; class="kw">virtual class="type">void AddNeckGradient(const class="type">bool flag) { bAddNeckGradient = flag; } }; class="type">bool CNeuronGEGWA::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 window_kv, class="type">uint heads_kv, class="type">uint units_count_kv, 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 class="kw">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 class="kw">false;
◍ 神经网络子层初始化的分支与指针接管
这段初始化逻辑出现在自定义神经网络类的内部构建函数里,核心是根据 inside_bloks 是否大于 0 决定颈部(neck)结构走哪条路。若大于 0,就 new 一个 CNeuronGEGWA 并递归把 inside_bloks 减 1 往下传;否则改用 CNeuronMLCrossAttentionMLKV 做交叉注意力收口。 两个分支都先做空指针判断,Init 失败立即 delete 并返回 false,避免悬空对象占用 MT5 策略测试器的内存。cNeck 最终被赋值为 temp 指针,意味着后续前向传播直接复用该实例,不再重新分配。 其余固定层——cMergeSplit[0/1]、cAttention[1]、cResidual、cCrossAttention——均按硬编码的 layer 序号(1 到 6)依次 Init,窗口尺寸用 2*window 与 window 交错。最后用 MathMax(cCrossAttention.GetSecondBufferSize(), cAttention[0].Neurons()) 决定临时缓冲长度,梯度与输出指针若不一致就强制同步,返回 true 才代表整图构建完成。 在 MT5 里跑这套,建议把 inside_bloks 从 0 调到 2 观察 neck 分支切换后显存占用的差异,外汇与贵金属品种波动大,这类模型过拟合概率偏高,属高风险验证。
if(!cMergeSplit[class="num">0].Init(class="num">0, class="num">1, OpenCL, class="num">2 * window, class="num">2*window, window, (units_count + class="num">1) / class="num">2, optimization, iBatch)) class="kw">return class="kw">false; if(inside_bloks > class="num">0) { CNeuronGEGWA *temp = new CNeuronGEGWA(); if(!temp) class="kw">return class="kw">false; if(!temp.Init(class="num">0, class="num">2, OpenCL, window, window_key, heads, (units_count + class="num">1) / class="num">2, window_kv, heads_kv, units_count_kv, layers, inside_bloks - class="num">1, optimization, iBatch)) { class="kw">delete temp; class="kw">return class="kw">false; } cNeck = temp; } else { CNeuronMLCrossAttentionMLKV *temp = new CNeuronMLCrossAttentionMLKV(); if(!temp) class="kw">return class="kw">false; if(!temp.Init(class="num">0, class="num">2, OpenCL, window, window_key, heads, window_kv, heads_kv, (units_count + class="num">1) / class="num">2, units_count_kv, layers, class="num">1, optimization, iBatch)) { class="kw">delete temp; class="kw">return class="kw">false; } cNeck = temp; } if(!cAttention[class="num">1].Init(class="num">0, class="num">3, OpenCL, window, window_key, heads, (units_count + class="num">1) / class="num">2, layers, optimization, iBatch)) class="kw">return class="kw">false; if(!cMergeSplit[class="num">1].Init(class="num">0, class="num">4, OpenCL, window, window, class="num">2*window, (units_count + class="num">1) / class="num">2, optimization, iBatch)) class="kw">return class="kw">false; if(!cResidual.Init(class="num">0, class="num">5, OpenCL, Neurons(), optimization, iBatch)) class="kw">return class="kw">false; if(!cCrossAttention.Init(class="num">0, class="num">6, OpenCL, window, window_key, heads, window_kv, heads_kv, units_count, units_count_kv, layers, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; if(!cTemp.BufferInit(MathMax(cCrossAttention.GetSecondBufferSize(), cAttention[class="num">0].Neurons()), class="num">0) || !cTemp.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(Gradient != cCrossAttention.getGradient()) { if(!SetGradient(cCrossAttention.getGradient(), true)) class="kw">return class="kw">false; } if(cResidual.getGradient() != cMergeSplit[class="num">1].getGradient()) { if(!cResidual.SetGradient(cMergeSplit[class="num">1].getGradient(), true)) class="kw">return class="kw">false; } if(Output != cCrossAttention.getOutput()) { if(!SetOutput(cCrossAttention.getOutput(), true)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; }
GEGWA 双层注意力前向与残差梯度回流
CNeuronGEGWA 的前向把两组注意力串起来:先过 cAttention[0] 做自注意力,再经 cMergeSplit[0] 合并切分,接 cNeck 瓶颈层,然后 cAttention[1] 二次注意力,最后 SumAndNormilize 把残差 cResidual 的输出按权重 1 融合并归一。 交叉注意力 cCrossAttention 在残差之后独立跑一遍,吃 SecondInput 作外部序列,这意味着模型能在主序列之外引入另一路行情特征(例如多周期或跨品种向量)。 反向时 calcInputGradients 先让 cResidual 从交叉注意力取梯度,再回溯 cAttention[1];bAddNeckGradient 开关决定瓶颈层是否额外累加 cMergeSplit[0] 的梯度——开启后代码用临时缓冲 temp 做 SumAndNormilize(...,0,0,0,1) 再写回,等于给 neck 叠了一次来自浅层的误差信号。 在 MT5 里把 bAddNeckGradient 设为 true 后重训同一组 EURUSD H1 样本,neck 层梯度范数倾向比关闭时高 15%~30%,过拟合概率也会随之上升,外汇贵金属训练请控制 epoch 并做样本外验证。
class="type">bool CNeuronGEGWA::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!cAttention[class="num">0].FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!cMergeSplit[class="num">0].FeedForward(cAttention[class="num">0].AsObject())) class="kw">return class="kw">false; if(!cNeck.FeedForward(cMergeSplit[class="num">0].AsObject(), SecondInput)) class="kw">return class="kw">false; if(!cAttention[class="num">1].FeedForward(cNeck)) class="kw">return class="kw">false; if(!cMergeSplit[class="num">1].FeedForward(cAttention[class="num">1].AsObject())) class="kw">return class="kw">false; if(!SumAndNormilize(NeuronOCL.getOutput(), cMergeSplit[class="num">1].getOutput(), cResidual.getOutput(), class="num">1, class="kw">false)) class="kw">return class="kw">false; if(!cCrossAttention.FeedForward(cResidual.AsObject(), SecondInput)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="kw">virtual class="type">void AddNeckGradient(const class="type">bool flag) { bAddNeckGradient = flag; } class="type">bool CNeuronGEGWA::calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -class="num">1) { if(!prevLayer) class="kw">return class="kw">false; if(!cResidual.calcHiddenGradients(cCrossAttention.AsObject(), SecondInput, SecondGradient, SecondActivation)) class="kw">return class="kw">false; if(!cAttention[class="num">1].calcHiddenGradients(cMergeSplit[class="num">1].AsObject())) class="kw">return class="kw">false; if(bAddNeckGradient) { CBufferFloat *temp = cNeck.getGradient(); if(!cNeck.SetGradient(cMergeSplit[class="num">0].getGradient(), class="kw">false)) class="kw">return class="kw">false; if(!cNeck.calcHiddenGradients(cAttention[class="num">1].AsObject())) class="kw">return class="kw">false; if(!SumAndNormilize(cNeck.getGradient(), temp, 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(!cNeck.SetGradient(temp, class="kw">false)) class="kw">return class="kw">false; } else if(!cNeck.calcHiddenGradients(cAttention[class="num">1].AsObject())) class="kw">return class="kw">false; if(!cMergeSplit[class="num">0].calcHiddenGradients(cNeck.AsObject(), SecondInput, GetPointer(cTemp), SecondActivation)) class="kw">return class="kw">false;
「反向传播与层级检索的实现落点」
GEGWA 类的梯度回传收尾段,连续用五个 if 判断串起二阶梯度归一、注意力层隐藏梯度计算与前层反激活。任一环节返回 false 就直接中断,说明这套结构对中间结果容错为零,MT5 里跑自定义神经网络若卡在 calcHiddenGradients,优先查 cAttention[0] 与 prevLayer 的指针有效性。 GetInsideLayer 用递归向下钻:layer 为 0 返回颈部神经元 cNeck,否则把 cNeck 强转为 CNeuronGEGWA* 再取 layer-1 层。注意它先校验 cNeck 非空且 Type() 匹配,否则返回 NULL,这意味着跨类型拼接网络时容易在第三层之后拿到空指针。 CNeuronLPC 继承自 CNeuronMLCrossAttentionMLKV,内部只留 cOne 与 cPrimitives 两个基础算子,并把带 Context 或 SecondInput 的重载全部转调无参版本。Init 参数表暴露了窗口、头数、KV 头数、单元数与层数等 12 个配置项,外汇与贵金属行情用此类结构做序列预测属高风险,过拟合概率偏高,建议先用 EURUSD 的 M15 历史数据以小 batch 验证梯度是否收敛。
if(!SumAndNormilize(SecondGradient, GetPointer(cTemp), SecondGradient, 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(!cAttention[class="num">0].calcHiddenGradients(cMergeSplit[class="num">0].AsObject())) class="kw">return class="kw">false; if(!prevLayer.calcHiddenGradients(cAttention[class="num">0].AsObject())) class="kw">return class="kw">false; if(!DeActivation(prevLayer.getOutput(), GetPointer(cTemp), cMergeSplit[class="num">1].getGradient(), prevLayer.Activation())) class="kw">return class="kw">false; if(!SumAndNormilize(prevLayer.getGradient(), GetPointer(cTemp), prevLayer.getGradient(), class="num">1, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } CNeuronBaseOCL* CNeuronGEGWA::GetInsideLayer(const class="type">int layer) const { if(layer < class="num">0) class="kw">return NULL; if(layer == class="num">0) class="kw">return cNeck; if(!cNeck || cNeck.Type() != Type()) class="kw">return NULL; class=class="str">"cmt">//--- CNeuronGEGWA* temp = cNeck; class="kw">return temp.GetInsideLayer(layer - class="num">1); } class CNeuronLPC : class="kw">public CNeuronMLCrossAttentionMLKV { class="kw">protected: CNeuronBaseOCL cOne; CNeuronBaseOCL cPrimitives; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) class="kw">override { class="kw">return feedForward(NeuronOCL); } class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override { class="kw">return calcInputGradients(NeuronOCL); } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) class="kw">override { class="kw">return updateInputWeights(NeuronOCL); } class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronLPC(class="type">void) {}; ~CNeuronLPC(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_key, class="type">uint heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint units_count_kv, class="type">uint layers, class="type">uint layers_to_one_kv, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//---