交易中的神经网络:场景感知物体检测(HyperDet3D)·综合运用
(3/3)· 前两层铺垫完点云与骨干网络,本篇把 HyperDet3D 的条件化推理与实盘测试一次讲透
◍ 注意力层里权重缓冲的失败短路
这段 CNeuronMLMHSceneConditionAttention 的初始化与前向代码,核心逻辑是「任何一步缓冲创建或指针检查失败就立即 return false」,不抛异常、不续跑。对外汇/贵金属模型来说,这种硬短路能避免脏张量进 GPU,但也可能让 EA 在 MT5 启动时静默失效,需配合日志排查。 初始化时按层循环建 CBufferFloat:第一层若 d==0 或优化器为 ADAM,缓冲长度取 ff_2,否则取 iWindow;随后 BufferInit 清零、BufferCreate 上 OpenCL、加入 FF_Weights 列表。任一步返回 false 即终止,Temp 缓冲则按 MathMax((num_q+num_kv)*iWindow, out) 初始化。 前向 feedForward 中,Scene-Specific 与 Scene-Agnostic 两套子网络交替跑;i==0 时输入取 NeuronOCL 输出,之后取 FF_Tensors 第 6*i-4 项。当 i % iLayersToOneKV == 0 才计算 kv 权重,并用 ElementMult 融合 ss 与 sa 输出到 Temp。 别把 return false 当无害 MT5 策略测试器里若看到初始化中途退出却无报错,八成是某层 BufferCreate(OpenCL) 在显存不足时失败。开 MT5 的 Experts 日志并把 OpenCL 设备显存占用盯住,能直接定位是第几层断的。
class="kw">return class="kw">false; if(!temp.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!FF_Weights.Add(temp)) class="kw">return class="kw">false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return class="kw">false; if(!temp.BufferInit((d == class="num">0 || optimization == ADAM ? ff_2 : iWindow), class="num">0)) class="kw">return class="kw">false; if(!temp.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!FF_Weights.Add(temp)) class="kw">return class="kw">false; } } if(!Temp.BufferInit(MathMax((num_q + num_kv)*iWindow, out), class="num">0)) class="kw">return class="kw">false; if(!Temp.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- SetOpenCL(OpenCL); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMLMHSceneConditionAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL *ss = NULL, *sa = NULL; CBufferFloat *q_weights = NULL, *kv_weights = NULL, *q = NULL, *kv = NULL; for(class="type">uint i = class="num">0; i < iLayers; i++) { class=class="str">"cmt">//--- Scene-Specific ss = cSceneSpecific[i * class="num">2]; if(!ss.FeedForward(NeuronOCL)) class="kw">return class="kw">false; ss = cSceneSpecific[i * class="num">2 + class="num">1]; if(!ss.FeedForward(cSceneSpecific[i * class="num">2])) class="kw">return class="kw">false; class=class="str">"cmt">//--- Scene-Agnostic sa = cSceneAgnostic[i * class="num">2 + class="num">1]; if(bTrain && !sa.FeedForward(cSceneAgnostic[i * class="num">2])) class="kw">return class="kw">false; CBufferFloat *inputs = (i == class="num">0 ? NeuronOCL.getOutput() : FF_Tensors.At(class="num">6 * i - class="num">4)); q_weights = QKV_Weights[i * class="num">2]; q = QKV_Tensors[i * class="num">2]; if(i % iLayersToOneKV == class="num">0) { if(IsStopped() || !ElementMult(ss.getOutput(), sa.getOutput(), GetPointer(Temp))) class="kw">return class="kw">false; kv_weights = KV_Weights[(i / iLayersToOneKV) * class="num">2]; kv = KV_Tensors[(i / iLayersToOneKV) * class="num">2]; if(IsStopped() || !DeConcat(q_weights, kv_weights, GetPointer(Temp), iHeads, class="num">2 * iHeadsKV, iWindow * iWindowKey))
「多头注意力层的前向计算细节」
这段逻辑跑在自定义神经网络层的前向传播里,核心是把输入张量依次做矩阵乘、注意力打分和前馈卷积。每层循环里先用 MatMul 把 inputs 和 kv_weights 算出 kv 缓冲,若优化器不是 SGD 则权重偏移按 9 组计、SGD 则按 6 组计,这个差异直接影响后面 ConvolutionForward 取权重的下标。 注意力部分调用 AttentionOut 得到输出,随后接两次 ConvolutionForward:第一次用 None 激活、卷积核维度为 iWindowKey*iHeads 到 iWindow;第二次用 LReLU 把通道扩到 4*iWindow 再降回 iWindow。每一步都包了 IsStopped() 检查,说明在 MT5 策略测试器里终止回测会立刻中断计算,不会卡死。 SumAndNormilize 在注意力与前馈后各做一次,最后一个参数传 true 代表做归一化。若你想改网络深度,直接调 iLayers 并注意 FF_Tensors 每层的 6 个缓冲槽位(SGD 下)或 9 个(非 SGD)即可,外汇与贵金属行情下用此类模型请意识到过拟合与实盘滑点风险可能显著。
class="kw">return class="kw">false; if(IsStopped() || !MatMul(inputs, kv_weights, kv, iUnits, iWindow, class="num">2 * iHeadsKV * iWindowKey, class="num">1)) class="kw">return class="kw">false; } else { if(IsStopped() || !ElementMult(ss.getOutput(), sa.getOutput(), q_weights)) class="kw">return class="kw">false; } if(IsStopped() || !MatMul(inputs, q_weights, q, iUnits, iWindow, iHeads * iWindowKey, class="num">1)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Score calculation and Multi-heads attention calculation CBufferFloat *temp = S_Tensors[i * class="num">2]; CBufferFloat *out = AO_Tensors[i * class="num">2]; if(IsStopped() || !AttentionOut(q, kv, temp, out)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Attention out calculation temp = FF_Tensors[i * class="num">6]; if(IsStopped() || !ConvolutionForward(FF_Weights[i * (optimization == SGD ? class="num">6 : class="num">9)], out, temp, iWindowKey * iHeads, iWindow, None)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Sum and normilize attention if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow, true)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Feed Forward inputs = temp; temp = FF_Tensors[i * class="num">6 + class="num">1]; if(IsStopped() || !ConvolutionForward(FF_Weights[i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1], inputs, temp, iWindow, class="num">4 * iWindow, LReLU)) class="kw">return class="kw">false; out = FF_Tensors[i * class="num">6 + class="num">2]; if(IsStopped() || !ConvolutionForward(FF_Weights[i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2], temp, out, class="num">4 * iWindow, iWindow, activation)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Sum and normilize out if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- result class="kw">return true; } class="type">bool CNeuronMLMHSceneConditionAttention::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer) == POINTER_INVALID) class="kw">return class="kw">false; CBufferFloat *out_grad = Gradient; CBufferFloat *kv_g = KV_Tensors[KV_Tensors.Total() - class="num">1]; CNeuronBaseOCL *ss = NULL, *sa = NULL; for(class="type">int i = class="type">int(iLayers - class="num">1); (i >= class="num">0 && !IsStopped()); i--) {
Transformer 反向传播里的梯度回传分支
这段逻辑处在多层注意力网络的反向阶段,核心是按层序号 i 决定 KV 梯度来源:当 i 是最后一层,或者 (i+1) 能被 iLayersToOneKV 整除时,直接从已存的 KV_Tensors 取梯度;否则走临时缓冲 Temp 并做累加归一。两种路径都依赖 IsStopped() 轮询,避免在 MT5 策略测试器中途终止时还继续算梯度。
前向权重的偏移量随优化器切换:SGD 时每层占 6 个权重槽,Adam 类占 9 个,代码里用 optimization == SGD ? 6 : 9 实时寻址。卷积梯度函数 ConvolutionInputGradients 被连续调用两次,分别处理 4*iWindow 与 iWindow 的通道维度反转,中间插一个 SumAndNormilize 把多头输出压回单头维度。
多头拆分那步用 iWindowKey * iHeads 作输入宽、iWindow 作输出宽,None 激活代表线性透传;随后 AttentionInsideGradients 把 Query/Key/Value 的梯度算出来。若你要在自己的 EA 里改头数,记住 iWindowKey*iHeads 的总宽必须和前向一致,否则回传会越界报错。外汇与贵金属品种上跑这类模型,参数敏感度高,实盘前务必用历史数据验证梯度数值稳定。
if(i == class="type">int(iLayers - class="num">1) || (i + class="num">1) % iLayersToOneKV == class="num">0) kv_g = KV_Tensors[(i / iLayersToOneKV) * class="num">2 + class="num">1]; class=class="str">"cmt">//--- Passing gradient through feed forward layers if(IsStopped() || !ConvolutionInputGradients(FF_Weights[i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2], out_grad, FF_Tensors[i * class="num">6 + class="num">1], FF_Tensors[i * class="num">6 + class="num">4], class="num">4 * iWindow, iWindow, None)) class="kw">return class="kw">false; CBufferFloat *temp = FF_Tensors[i * class="num">6 + class="num">3]; if(IsStopped() || !ConvolutionInputGradients(FF_Weights[i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1], FF_Tensors[i * class="num">6 + class="num">4], FF_Tensors[i * class="num">6], temp, iWindow, class="num">4 * iWindow, LReLU)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Sum and normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Sum and normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, class="kw">false)) class="kw">return class="kw">false; out_grad = temp; class=class="str">"cmt">//--- Split gradient to multi-heads if(IsStopped() || !ConvolutionInputGradients(FF_Weights[i * (optimization == SGD ? class="num">6 : class="num">9)], out_grad, AO_Tensors[i * class="num">2], AO_Tensors[i * class="num">2 + class="num">1], iWindowKey * iHeads, iWindow, None)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Passing gradient to query, key and value sa = cSceneAgnostic[i * class="num">2 + class="num">1]; ss = cSceneSpecific[i * class="num">2 + class="num">1]; if(i == class="type">int(iLayers - class="num">1) || (i + class="num">1) % iLayersToOneKV == class="num">0) { if(IsStopped() || !AttentionInsideGradients(QKV_Tensors[i * class="num">2], QKV_Tensors[i * class="num">2 + class="num">1], KV_Tensors[(i / iLayersToOneKV) * class="num">2], kv_g, S_Tensors[i * class="num">2], AO_Tensors[i * class="num">2 + class="num">1])) class="kw">return class="kw">false; } else { if(IsStopped() || !AttentionInsideGradients(QKV_Tensors[i * class="num">2], QKV_Tensors[i * class="num">2 + class="num">1], KV_Tensors[i / iLayersToOneKV * class="num">2], GetPointer(Temp), S_Tensors[i * class="num">2], AO_Tensors[i * class="num">2 + class="num">1])) class="kw">return class="kw">false; if(IsStopped() || !SumAndNormilize(kv_g, GetPointer(Temp), kv_g, iWindowKey, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; }
◍ 反向传播里输入与梯度的分支取法
在多层注意力结构里,第 0 层和其他层的输入指针取法完全不同:i==0 时直接拿 prevLayer 的输出和梯度,否则从 FF_Tensors 按 i*6-1、i*6-4 偏移取临时张量和输入。这个 6 的步长对应每层在前向时压入的 6 个张量槽位,改网络宽度时若动了排布必须同步改这里。 KV 分支不是每层都算。代码用 (i % iLayersToOneKV)==0 决定本层是否复用跨层 KV 权重,命中时才跑 MatMulGrad 与 SumAndNormilize,并把 KV 梯度 Concat 进 QKV 梯度。若你设 iLayersToOneKV=2,则偶数层才进这段,奇数层只走 ElementMultGrad 直接写 QKV_Weights 梯度。 所有重计算都包了 IsStopped() 提前返回,EA 在 MT5 上被用户点停止会立刻中断反向传播,避免卡死。复制下面这段到你的 CNeuralLayer::calcGradients 里对照,能直接看出梯度回传路径是否断了。
CBufferFloat *inp = NULL; if(i == class="num">0) { inp = prevLayer.getOutput(); temp = prevLayer.getGradient(); } else { temp = FF_Tensors.At(i * class="num">6 - class="num">1); inp = FF_Tensors.At(i * class="num">6 - class="num">4); } if(IsStopped() || !MatMulGrad(inp, temp, QKV_Weights[i * class="num">2], QKV_Weights[i * class="num">2 + class="num">1], QKV_Tensors[i * class="num">2 + class="num">1], iUnits, iWindow, iHeads * iWindowKey, class="num">1)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Sum and normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; class=class="str">"cmt">//--- if((i % iLayersToOneKV) == class="num">0) { if(IsStopped() || !MatMulGrad(inp, GetPointer(Temp), KV_Weights[i / iLayersToOneKV * class="num">2], KV_Weights[i / iLayersToOneKV * class="num">2 + class="num">1], KV_Tensors[i / iLayersToOneKV * class="num">2 + class="num">1], iUnits, iWindow, class="num">2 * iHeadsKV * iWindowKey, class="num">1)) class="kw">return class="kw">false; if(IsStopped() || !SumAndNormilize(GetPointer(Temp), temp, temp, iWindow, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; if(!Concat(QKV_Weights[i * class="num">2 + class="num">1], KV_Weights[i / iLayersToOneKV * class="num">2 + class="num">1], ss.getGradient(), iHeads, class="num">2 * iHeadsKV, iWindow * iWindowKey)) class="kw">return class="kw">false; if(!ElementMultGrad(ss.getOutput(), ss.getGradient(), sa.getOutput(), sa.getGradient(), ss.getGradient(), ss.Activation(), sa.Activation())) class="kw">return class="kw">false; } else { if(!ElementMultGrad(ss.getOutput(), ss.getGradient(), sa.getOutput(), sa.getGradient(), QKV_Weights[i * class="num">2 + class="num">1], ss.Activation(), sa.Activation())) class="kw">return class="kw">false; } if(i > class="num">0) out_grad = temp; }
「超det网络类的梯度回传与结构声明」
这段代码片段承接前文的梯度计算逻辑,先是在前层梯度回传里做了一系列中断保护:若线程被 IsStopped() 中止,或隐藏层梯度 calcHiddenGradients 返回失败,或 SumAndNormilize 归一化输入梯度出错,都直接 return false,避免脏梯度写回。 最后通过 prevLayer.SetGradient(inp_grad, false) 把归一化后的梯度提交给前层,整段 calcInputGradients 返回 true 表示反向传播本层收尾。 随后声明的 CNeuronHyperDet 类继承自 CNeuronPointNet2OCL,内部挂了 2 组局部注意力、2 组局部-全局交叉注意力、2 组全局场景条件注意力,以及对应的可学习位置编码 caLocalPE[2] 与 caGlobalPE[2];还包含一个拼接层 cConcatenate 和卷积缩放层 cScale。 析构里只 delete 了 cbTemp 这个临时浮点缓冲,其余子层对象依赖父级生命周期管理。Init 接口暴露了 window、units_count、batch 等参数,在 MT5 上跑这类网络时,batch 设 32 与设 128 的显存占用可能差出 3 倍以上,调参前先确认显卡剩余 OCL 内存。
class="kw">return class="kw">false; if(IsStopped() || !prevLayer.calcHiddenGradients(ss, NULL)) class="kw">return class="kw">false; if(IsStopped() || !SumAndNormilize(prevLayer.getGradient(), inp_grad, inp_grad, iWindow, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; } if(!prevLayer.SetGradient(inp_grad, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronHyperDet : class="kw">public CNeuronPointNet2OCL { class="kw">protected: CNeuronMLMHSparseAttention caLocalAttention[class="num">2]; CNeuronMLCrossAttentionMLKV caLocalGlobalAttention[class="num">2]; CNeuronMLMHSceneConditionAttention caGlobalAttention[class="num">2]; CNeuronLearnabledPE caLocalPE[class="num">2]; CNeuronLearnabledPE caGlobalPE[class="num">2]; CNeuronBaseOCL cConcatenate; CNeuronConvOCL cScale; class=class="str">"cmt">//--- CBufferFloat *cbTemp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override ; class=class="str">"cmt">//--- 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: CNeuronHyperDet(class="type">void) {}; ~CNeuronHyperDet(class="type">void) { class="kw">delete cbTemp; } 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 units_count, class="type">uint output, class="type">bool use_tnets, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronHyperDet; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int const file_handle) 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="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; };
EURUSD H1 上跑出的测试账本
我们用 EURUSD 2023 全年 H1 真实历史训练模型,指标全取默认值,训练算法沿用前一篇设定,本小节只盯测试端产出。 测试切到 2024 年 1 月、完全脱离训练集的数据。模型在该月跑了 41 笔交易,胜率 56%;最大盈利单是最大亏损单的 2.4 倍,平均盈利比平均亏损高 67%,盈利因子 2.14,锋锐比率 20.65。 整段测试期账户利润约 1%,净值最大回撤压在 0.34% 以内,余额回撤更低,敞口始终维持 1%–2%,曲线呈稳步抬升。 样本仅一个月、成交仅 41 笔,长期稳定性还不敢下结论。外汇与贵金属属高风险品种,实盘前建议拿更长的历史集重训并做更宽窗口的验证。
◍ 画得少,看得清
把条件化场景超网络塞进变换器,本质就是让探测器权重跟着场景特征走,而不是一套参数打全场。MQL5 里用 Research.mq5 到 Test.mq5 四个 EA 串起采样、训练、测试闭环,外加 Trajectory.mqh 与 NeuroNet 系列类库扛结构和算力,工程链路是通的。 回测只证明架构潜力,离实盘还有距离——外汇和贵金属的高风险不会因为模型更高级就消失,直接挂真账户前建议先用策略测试器把 Test.mq5 跑满不同波动段。 留个实在的余味:复杂网络容易画满曲线吓自己,真要盯盘,反而是少画几条、把场景先验调对更重要。