神经网络变得轻松(第十三部分):批次常规化·综合运用
「神经网络层里的批量归一化与优化器参数映射」
在 MT5 的 OpenCL 神经网络实现里,批量归一化层(CNeuronBatchNormOCL)依赖一组宏定义的张量索引来传递梯度与优化参数。比如动量项用 def_k_buom_momentum 标记为 3,而 Adam 优化器则细分出 def_k_buoa_b1=3、def_k_buoa_b2=4 两个动量乘子,分别对应一阶与二阶矩。 这些索引不是随手编的:def_k_UpdateBatchOptionsAdam 被固定为 27,用于描述批量归一化神经元的 Adam 优化选项内核;其 options 矩阵索引为 0,结构为 m*(7 或 9),m 是上一层神经元数。调参时若改了矩阵宽度,必须同步核对内核编号,否则 GPU 端会读错偏移。 CNet 构造函数通过遍历 Description 数组逐层装配。关键片段显示,当 desc.type 命中 defNeuronBatchNormOCL 时,会新建 CNeuronBatchNormOCL 对象并赋给 batch 指针;outputs 则取下一层神经元数,末层为 0。 开 MT5 把下面代码段塞进你自己的网络初始化逻辑,重点看 case 分支里 batch 指针的诞生位置,确认它处在 opencl 指针有效的作用域内,否则批量层不会走 GPU 加速。
class="macro">#define def_k_buom_matrix_g class="num">1 class=class="str">"cmt">///<-[in] Tensor of gradients at current layer class="macro">#define def_k_buom_learning_rates class="num">2 class=class="str">"cmt">///<- Learning rates class="macro">#define def_k_buom_momentum class="num">3 class=class="str">"cmt">///<- Momentum multiplier class=class="str">"cmt">//--- class="macro">#define def_k_UpdateBatchOptionsAdam class="num">27 class=class="str">"cmt">///<- Index of the kernel for Describe the process of Adam optimization options for the Batch normalization Neuron(class="macro">#CNeuronBatchNormOCL). class="macro">#define def_k_buoa_options class="num">0 class=class="str">"cmt">///<-[in] Options matrix m*(class="num">7 or class="num">9), where m - Number of neurons in previous layer class="macro">#define def_k_buoa_matrix_g class="num">1 class=class="str">"cmt">///<-[in] Tensor of gradients at current layer class="macro">#define def_k_buoa_l class="num">2 class=class="str">"cmt">///<- Learning rates class="macro">#define def_k_buoa_b1 class="num">3 class=class="str">"cmt">///<- First momentum multiplier class="macro">#define def_k_buoa_b2 class="num">4 class=class="str">"cmt">///<- Second momentum multiplier CNet::CNet(CArrayObj *Description) { if(CheckPointer(Description)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- ................ ................ ................ class=class="str">"cmt">//--- for(class="type">int i=class="num">0; i<total; i++) { prev=desc; desc=Description.At(i); if((i+class="num">1)<total) { next=Description.At(i+class="num">1); if(CheckPointer(next)==POINTER_INVALID) class="kw">return; } else next=NULL; class="type">int outputs=(next==NULL || (next.type!=defNeuron && next.type!=defNeuronBaseOCL) ? class="num">0 : next.count); temp=new CLayer(outputs); class="type">int neurons=(desc.count+(desc.type==defNeuron || desc.type==defNeuronBaseOCL ? class="num">1 : class="num">0)); if(CheckPointer(opencl)!=POINTER_INVALID) { ................ ................ ................ CNeuronBatchNormOCL *batch=NULL; class="kw">switch(desc.type) { ................ ................ ................ ................ class=class="str">"cmt">//--- case defNeuronBatchNormOCL: batch=new CNeuronBatchNormOCL();
神经网络层装配与OpenCL内核注册
在 MT5 的 MQL5 环境里用 OpenCL 跑神经网络,层对象的指针校验和内存回收必须先于任何初始化。下面这段逻辑在 switch 的 case 分支中处理批量层(batch)的挂载:若 CheckPointer 返回 POINTER_INVALID 说明层没建出来,立刻 delete 临时容器并 return,避免悬空指针进后续计算。 batch.Init 接收 outputs、偏移 0、opencl 句柄以及 desc 里的 count/window/optimization 参数,任一失败都要把 batch 和 temp 双双 delete 再退出。SetActivationFunction 按 desc.activation 指定激活函数后,temp.Add(batch) 把层塞进网络;Add 失败同样双删返回,最后把 batch 置 NULL 并 break 出分支,default 分支则直接 return 不做处理。 内核注册环节有个硬数字:opencl.SetKernelsCount(28) 表明整套前向/反向传播共预置 28 个 kernel。其中明确创建的有 BatchFeedForward、CalcHiddenGradientBath、UpdateBatchOptionsMomentum、UpdateBatchOptionsAdam 四个,命名直接对应训练与推理的不同阶段。 CNet::Load 函数在优化、回测、前向测试模式下直接返回 false,意味着实盘加载权重文件只发生在普通 EA 运行环境。外汇与贵金属杠杆交易高风险,这类 GPU 加速模型在 live 账户上的推断延迟和过拟合倾向需自行在 MT5 策略测试器验证。
if(CheckPointer(batch)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!batch.Init(outputs,class="num">0,opencl,desc.count,desc.window,desc.optimization)) { class="kw">delete batch; class="kw">delete temp; class="kw">return; } batch.SetActivationFunction(desc.activation); if(!temp.Add(batch)) { class="kw">delete batch; class="kw">delete temp; class="kw">return; } batch=NULL; class="kw">break; class=class="str">"cmt">//--- class="kw">default: class="kw">return; class="kw">break; } } class=class="str">"cmt">//--- if(CheckPointer(opencl)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- create kernels opencl.SetKernelsCount(class="num">28); class=class="str">"cmt">//--- opencl.KernelCreate(def_k_BatchFeedForward,"BatchFeedForward"); opencl.KernelCreate(def_k_CalcHiddenGradientBath,"CalcHiddenGradientBath"); opencl.KernelCreate(def_k_UpdateBatchOptionsMomentum,"UpdateBatchOptionsMomentum"); opencl.KernelCreate(def_k_UpdateBatchOptionsAdam,"UpdateBatchOptionsAdam"); class=class="str">"cmt">//--- class="kw">return; } class="type">bool CNet::Load(class="type">class="kw">string file_name,class="type">class="kw">double &error,class="type">class="kw">double &undefine,class="type">class="kw">double &forecast,class="type">class="kw">datetime &time,class="type">bool common=true) { if(MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_FORWARD) || MQLInfoInteger(MQL_OPTIMIZATION)) class="kw">return class="kw">false; class=class="str">"cmt">//--- }
◍ OpenCL 实例的惰性初始化与层加载分支
在 MT5 的神经网络 OCL 封装里,COpenCLMy 实例采用惰性创建:先用 CheckPointer 判断 opencl 是否为 POINTER_INVALID,无效才 new 一个并调 Initialize。若初始化失败立即 delete,避免悬挂指针;成功则 SetKernelsCount(28) 并批量 KernelCreate,例如 BatchFeedForward、CalcHiddenGradientBath、UpdateBatchOptionsMomentum、UpdateBatchOptionsAdam 等内核名直接映射到 GPU 计算任务。 CLayer::Load 从文件句柄恢复时,先走 CArrayObj::Load,再按 m_data[0].Type() 分流。若类型落在 defNeuronBaseOCL、defNeuronConvOCL、defNeuronBatchNormOCL 等 OCL 系列,就取 ocl 指针并用 ocl.getConnections() 填 iOutputs;否则走 cpu 分支用 getConnections().Total()。这套 switch 决定了该层后续在 GPU 还是 CPU 上跑前向。 CNeuronBaseOCL::FeedForward 对 SourceObject 做同样的类型 switch,命中 OCL 系列后转 feedForward(temp)。注意 defNeuronBatchNormOCL 在两层 switch 中都出现,说明批归一化层在加载和前向阶段都被当作标准 OCL 神经元处理,调参时若改了 BatchNorm 的实现,要同时核对这两处分支。
if(CheckPointer(opencl)==POINTER_INVALID) { opencl=new COpenCLMy(); if(CheckPointer(opencl)!=POINTER_INVALID && !opencl.Initialize(cl_program,true)) class="kw">delete opencl; else { class=class="str">"cmt">//--- create kernels opencl.SetKernelsCount(class="num">28); opencl.KernelCreate(def_k_BatchFeedForward,"BatchFeedForward"); opencl.KernelCreate(def_k_CalcHiddenGradientBath,"CalcHiddenGradientBath"); opencl.KernelCreate(def_k_UpdateBatchOptionsMomentum,"UpdateBatchOptionsMomentum"); opencl.KernelCreate(def_k_UpdateBatchOptionsAdam,"UpdateBatchOptionsAdam"); } } class="type">bool CLayer::Load(class="kw">const class="type">int file_handle) { iFileHandle=file_handle; if(!CArrayObj::Load(file_handle)) class="kw">return class="kw">false; if(CheckPointer(m_data[class="num">0])==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- CNeuronBaseOCL *ocl=NULL; CNeuronBase *cpu=NULL; class="kw">switch(m_data[class="num">0].Type()) { case defNeuronBaseOCL: case defNeuronConvOCL: case defNeuronAttentionOCL: case defNeuronMHAttentionOCL: case defNeuronMLMHAttentionOCL: case defNeuronDropoutOCL: case defNeuronBatchNormOCL: ocl=m_data[class="num">0]; iOutputs=ocl.getConnections(); class="kw">break; class="kw">default: cpu=m_data[class="num">0]; iOutputs=cpu.getConnections().Total(); class="kw">break; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronBaseOCL::FeedForward(CObject *SourceObject) { if(CheckPointer(SourceObject)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- CNeuronBaseOCL *temp=NULL; class="kw">switch(SourceObject.Type()) { case defNeuronBaseOCL: case defNeuronConvOCL: case defNeuronAttentionOCL: case defNeuronMHAttentionOCL: case defNeuronMLMHAttentionOCL: case defNeuronDropoutOCL: case defNeuronBatchNormOCL: temp=SourceObject; class="kw">return feedForward(temp);
「批归一化节点在梯度回传里的接驳点」
在 OpenCL 神经网络基类里,批归一化(BatchNorm)不是孤立层,它必须嵌进隐藏层梯度和权重更新的两条 switch 链里才跑得通。 calcHiddenGradients 中遇到 defNeuronBatchNormOCL 分支时,把 TargetObject 强转为 CNeuronBatchNormOCL 指针,再把自身指针交给 batch.calcInputGradients(temp),由批归一化层反向算输入梯度。漏掉这个 case,函数直接 return false,整条反向传播在该连接处断裂。 UpdateInputWeights 的 switch 同样要包含 defNeuronBatchNormOCL,与 Base/Conv/Attention/Dropout 等并列,统一调用 updateInputWeights(temp)。MT5 里若你自建网络加了 BatchNorm 却忘注册这两个分支,编译器不报错,但训练时权重不更新,回测曲线会停在随机水平。 两个函数开头都用 CheckPointer 判 POINTER_INVALID,指针失效立即返回 false,这是 OpenCL 封装层避免野指针挂掉 EA 的基础防护,改代码时别顺手删了。
class="kw">break; } class=class="str">"cmt">//--- class="kw">return class="kw">false; } class="type">bool CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject) { if(CheckPointer(TargetObject)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- ................ ................ ................ CNeuronBatchNormOCL *batch=NULL; class="kw">switch(TargetObject.Type()) { ................ ................ ................ case defNeuronBatchNormOCL: batch=TargetObject; temp=GetPointer(this); class="kw">return batch.calcInputGradients(temp); class="kw">break; } class=class="str">"cmt">//--- class="kw">return class="kw">false; } class="type">bool CNeuronBaseOCL::UpdateInputWeights(CObject *SourceObject) { if(CheckPointer(SourceObject)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- CNeuronBaseOCL *temp=NULL; class="kw">switch(SourceObject.Type()) { case defNeuronBaseOCL: case defNeuronConvOCL: case defNeuronAttentionOCL: case defNeuronMHAttentionOCL: case defNeuronMLMHAttentionOCL: case defNeuronDropoutOCL: case defNeuronBatchNormOCL: temp=SourceObject; class="kw">return updateInputWeights(temp); class="kw">break; } class=class="str">"cmt">//--- class="kw">return class="kw">false; }
批次常规化在 EURUSD H1 上的实测表现
把第十二篇里的常规化替换成批次常规化后,我们在 EURUSD H1 跑了回测,输入仍是最近 20 根烛条。学习率从 0.000001 直接拉到 0.001,跨度三个数量级,网络结构相应改为两层 BatchNorm 夹 Conv 与多头注意力。 预测误差曲线显示,新 EA 的图型不如旧版平滑,大概率和学习率暴增有关。但几乎整段测试里,误差都低于此前版本,说明批次常规化在收敛精度上可能更占优。 三套 EA 的命中率图型贴得很近,目前没法断言哪一套更好。外汇与贵金属属高风险品种,此类神经网络 EA 仅作技术验证,实盘前务必自行在 MT5 策略测试器复核。 下面这段是网络拓扑的构建代码,黄色高亮处即批次常规化层与无激活函数的关键配置:
class=class="str">"cmt">//--- class="num">0 CLayerDescription *desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars*class="num">12; desc.type=defNeuronBaseOCL; desc.optimization=ADAM; desc.activation=None; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">1 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars*class="num">12; desc.type=defNeuronBatchNormOCL; desc.window=class="num">100; desc.optimization=ADAM; desc.activation=TANH; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">2 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars; desc.type=defNeuronConvOCL; desc.window=class="num">12; desc.step=class="num">12; desc.window_out=class="num">24; desc.optimization=ADAM; desc.activation=None; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">3 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars*class="num">24; desc.type=defNeuronBatchNormOCL; desc.window=class="num">100; desc.optimization=ADAM; desc.activation=SIGMOID; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">4 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars; desc.type=defNeuronMLMHAttentionOCL; desc.window=class="num">24; desc.window_out=class="num">4; desc.step=class="num">8; class=class="str">"cmt">//heads desc.layers=class="num">5; desc.optimization=ADAM; desc.activation=SIGMOID;
◍ 堆叠无激活层与批归一化层的拓扑细节
这段初始化代码展示了神经网络拓扑里第 5 到第 8 层的逐层装配逻辑,每层都先 new 一个 CLayerDescription 并用 CheckPointer 判空,指针无效直接 INIT_FAILED,避免后续访问野指针导致 EA 初始化崩溃。 第 5 层是 200 个 defNeuron 节点,activation 设为 None,意味着这一层只做线性变换不做非线性映射,常用来当特征直通通道;优化器统一用 ADAM。 第 6 层同样是 200 节点,但类型换成 defNeuronBatchNormOCL,window=100 指定了批归一化的滑动窗口长度,activation 用 TANH 把输出压到 [-1,1],这种组合在显存占用和收敛稳定性之间倾向有更好平衡。 第 7 层回到 200 个普通 defNeuron 加 TANH,第 8 层收缩到 3 个节点配 SIGMOID,明显是给三分类或三维概率输出收口。实测加批次常规化类后,网络误差可能降低、学习率可以提高,但外汇与贵金属行情高频噪声大,上 MT5 跑回测前先把 window=100 和层数量当作敏感参数做网格扫描。
if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">5 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">200; desc.type=defNeuron; desc.activation=None; desc.optimization=ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">6 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">200; desc.type=defNeuronBatchNormOCL; desc.window=class="num">100; desc.optimization=ADAM; desc.activation=TANH; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">7 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">200; desc.type=defNeuron; desc.activation=TANH; desc.optimization=ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="num">8 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">3; desc.type=defNeuron; desc.activation=SIGMOID; desc.optimization=ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED;
「顺着这些方向把网络跑起来」
想把前面几篇的卷积、循环和注意力模型真正在 MT5 里用起来,得先按训练链路把参考材料过一遍。原系列从基础训练测试一路写到 OpenCL 多线程、学习率实验和自适应优化,再到注意力与 GPT 衍生结构,基本覆盖了终端内能落地的深度网络路径。 其中有两项常规化技术值得单独验证:批次常规化靠压低内部协变量偏移来加速深网收敛,层常规化则针对单层的激活分布做约束。两者在 EURUSD 的 M15 样本上都可能让过拟合概率下降,但外汇与贵金属杠杆交易本身高风险,任何网络输出都只是概率倾向,不能直接当方向依据。 开 MT5 后建议先复现第六篇的学习率扫描,再叠加第七篇的自适应优化,观察验证集损失曲线是否提前走平。能跑通这条线,后面接第八、第十篇的注意力结构才不容易训崩。
画得少,看得清
这套 GTP 架构的 EA 实际跑起来,核心就四个文件:Fractal_OCL_AttentionMLMH_b.mq5 是带 5 个关注层和 BatchNorm 的智能交易系统,输出层 3 个神经元做分类;NeuroNet.mqh 管网络搭建,NeuroNet.cl 是 OpenCL 核,NN.chm 只是离线函数手册。下载包 1728.71 KB,解压后别急着挂 EURUSD H1,先确认模型文件已生成。
日志里那行 Error of read EURUSD_PERIOD_H1_20 Fractal_OCL_AttentionMLMH_d.nnw prev Net 5004 说明 EA 启动时找不到已训练网络,多半是没先跑训练脚本就直接 OnInit。接着 cl_khr_fp64 unsupported 和 double4 to float 转换失败,指向老 Intel HD 4400 这类 GPU 不支持双精度——换设备或用新版 NeuroNet.mqh 可解。
外汇与贵金属杠杆高、滑点狠,神经网络信号只是概率倾向,实盘前务必在策略测试器用历史数据复一遍上述报错路径。真要把注意力机制用顺,先把代码库编译通、模型建出来,比调参更重要。
CS class="num">0 class="num">08:class="num">28:class="num">40.162 Fractal_OCL_AttentionMLMH_d(EURUSD,H1) EURUSD_PERIOD_H1_ class="num">20 Fractal_OCL_AttentionMLMH_d.nnw CS class="num">0 class="num">08:class="num">28:class="num">40.163 Fractal_OCL_AttentionMLMH_d(EURUSD,H1) OnInit - class="num">130 -> Error of read EURUSD_PERIOD_H1_ class="num">20 Fractal_OCL_AttentionMLMH_d.nnw prev Net class="num">5004 CS class="num">0 class="num">08:class="num">28:class="num">40.192 Fractal_OCL_AttentionMLMH_d(EURUSD,H1) OpenCL: GPU device &class="macro">#x27;Intel HD Graphics class="num">4400&class="macro">#x27; selected CS class="num">0 class="num">08:class="num">28:class="num">43.149 Fractal_OCL_AttentionMLMH_d(EURUSD,H1) class="num">1:class="num">9:class="num">26: error: OpenCL extension &class="macro">#x27;cl_khr_fp64&class="macro">#x27; is unsupported CS class="num">0 class="num">08:class="num">28:class="num">43.149 Fractal_OCL_AttentionMLMH_d(EURUSD,H1) class="num">1:class="num">55:class="num">16: error: no matching function for call to &class="macro">#x27;dot&class="macro">#x27; CS class="num">0 class="num">08:class="num">28:class="num">43.149 Fractal_OCL_AttentionMLMH_d(EURUSD,H1) c:/j/workspace/llvm/llvm/tools/clang/lib/cclang<stdin>:class="num">2199:class="num">61: note: candidate function not viable: no known conversion from &class="macro">#x27;double4&class="macro">#x27; to &class="macro">#x27;class="type">class="kw">float&class="macro">#x27; for class="num">1 st argument