神经网络变得轻松(第十三部分):批次常规化·进阶篇
批归一化层在 GPU 上的前向实现
这段代码来自一个批量归一化神经元的 feedForward 方法,核心是在 OpenCL 环境下对每个神经元做标准化与激活。先算方差:用上一帧的 options[shift+1] 乘 (batch-1) 再加 delt 平方,若原方差大于 0 则除以 batch 得到当前 variance,随后 nx = delt / sqrt(variance + 1e-6),那个 1e-6 就是防除零的极小值。 激活分支里 case 0 走 tanh(clamp(res,-20,20)),case 1 走 sigmoid 同样做了 ±20 截断,case 2 是带 0.01 系数的 leaky 风格(res<0 时乘 0.01)。clamp 到 ±20 很关键,避免 exp/tanh 在 GPU 上溢出爆 NaN。 feedForward 入口先校验 OpenCL 与上游神经元指针有效,iBatchSize<=1 时直接把上游激活类型透传并返回,跳过归一化。若 BatchOptions 缓冲未建,按 Neurons()*(优化器==SGD?7:9) 分配——SGD 每条神经元 7 个双精度槽,其他优化器 9 个,这是存 mean/variance/nx 及动量项用的。 之后把输入、options、输出三个缓冲绑到 OpenCL 内核 def_k_BatchFeedForward,并设 batch 大小与 optimization 枚举为参数,global_work_size[0]=Neurons() 意味着一个神经元一个工作项并行。你在 MT5 里接自己的网络时,把 iBatchSize 调到 ≥2 才会真正进归一化核,否则这层等于透明转发。
class="type">class="kw">double variance=options[shift+class="num">1]*((class="type">class="kw">double)batch-class="num">1.0)+pow(delt,class="num">2); if(options[shift+class="num">1]>class="num">0) variance/=(class="type">class="kw">double)batch; class="type">class="kw">double nx=delt/sqrt(variance+class="num">1e-6); class=class="str">"cmt">//--- if(options[shift+class="num">3]==class="num">0) options[shift+class="num">3]=class="num">1; class=class="str">"cmt">//--- class="type">class="kw">double res=options[shift+class="num">3]*nx+options[shift+class="num">4]; class="kw">switch(activation) { case class="num">0: res=tanh(clamp(res,-class="num">20.0,class="num">20.0)); class="kw">break; case class="num">1: res=class="num">1/(class="num">1+exp(-clamp(res,-class="num">20.0,class="num">20.0))); class="kw">break; case class="num">2: if(res<class="num">0) res*=class="num">0.01; class="kw">break; class="kw">default: class="kw">break; } class=class="str">"cmt">//--- options[shift]=mean; options[shift+class="num">1]=variance; options[shift+class="num">2]=nx; output[n]=res; } class="type">bool CNeuronBatchNormOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- PrevLayer=NeuronOCL; if(iBatchSize<=class="num">1) { activation=(ENUM_ACTIVATION)NeuronOCL.Activation(); class="kw">return true; } class=class="str">"cmt">//--- if(CheckPointer(BatchOptions)==POINTER_INVALID) { class="type">int count=Neurons()*(optimization==SGD ? class="num">7 : class="num">9); BatchOptions=new CBufferDouble(); if(!BatchOptions.BufferInit(count,class="num">0)) class="kw">return class="kw">false; } if(!BatchOptions.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">1]= {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0]=Neurons(); if(!OpenCL.SetArgumentBuffer(def_k_BatchFeedForward,def_k_bff_inputs,NeuronOCL.getOutputIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_BatchFeedForward,def_k_bff_options,BatchOptions.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_BatchFeedForward,def_k_bff_output,Output.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_BatchFeedForward,def_k_bff_batch,iBatchSize)) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_BatchFeedForward,def_k_bff_optimization,(class="type">int)optimization))
◍ 批归一化下的隐藏层梯度反传
在 GPU 上做批量前馈之后,紧接着要把当前层的梯度按批归一化路径回传到上一层。这段 OpenCL 内核只在 batch>1 时干活,单样本直接 return,避免除零和无效同步。 内核先按全局 id 取神经元索引 n,再用 optimization 标志算偏移 shift:标准模式占 7 个参数,带额外优化占 9 个。随后从输入张量 matrix_i 取该神经元输出 inp,乘以 options[shift+3] 得到未归一化梯度 gnx。 归一化梯度拆解很直白:temp 是 1/sqrt(方差+1e-6);对均值的梯度 gmu = -temp*gnx;对方差的梯度 gvar 含 inp 与方差的 3/2 次幂分母;最终输入梯度 gx 由归一化项、均值项、方差项三部分叠加,其中方差项带 (batch-1)/batch 的平方因子。 数值防御上,若 gx 算出 NaN 就强制置 0,防止污染整批反向传播。激活类型用 switch 分发,case 0 起手处理第一种激活的梯度修正。 在 MT5 里把这段挂到 CalcHiddenGradientBatch 内核跑 EURUSD 的 H1 批训练,若 batch=32、optimization=0,shift 固定为 7,可直接打印 gx 核对 NaN 触发频率是否随学习率上升而走高。外汇与贵金属杠杆高,回测结论仅代表历史概率,实盘须控仓。
if(!OpenCL.SetArgument(def_k_BatchFeedForward,def_k_bff_activation,(class="type">int)activation)) class="kw">return class="kw">false; ResetLastError(); class=class="str">"cmt">//--- if(!OpenCL.Execute(def_k_BatchFeedForward,class="num">1,global_work_offset,global_work_size)) { printf("Error of execution kernel Batch Feed Forward: %d",GetLastError()); class="kw">return class="kw">false; } if(!Output.BufferRead() || !BatchOptions.BufferRead()) class="kw">return class="kw">false; BatchOptions.BufferFree(); class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void CalcHiddenGradientBatch(__global class="type">class="kw">double *options, class=class="str">"cmt">///<[in] Options matrix m*(class="num">7 or class="num">9), where m - Number of neurons in previous layer __global class="type">class="kw">double *matrix_g, class=class="str">"cmt">///<[in] Tensor of gradients at current layer __global class="type">class="kw">double *matrix_i, class=class="str">"cmt">///<[in] Tensor of previous layer output __global class="type">class="kw">double *matrix_ig, class=class="str">"cmt">///<[out] Tensor of gradients at previous layer class="type">uint activation, class=class="str">"cmt">///< Activation type(class="macro">#ENUM_ACTIVATION) class="type">int batch, class=class="str">"cmt">///< Batch size class="type">int optimization class=class="str">"cmt">///< Optimization type ) { if(batch<=class="num">1) class="kw">return; class=class="str">"cmt">//--- class="type">int n=get_global_id(class="num">0); class="type">int shift=n*(optimization==class="num">0 ? class="num">7 : class="num">9); class=class="str">"cmt">//--- class="type">class="kw">double inp=matrix_i[n]; class="type">class="kw">double gnx=matrix_g[n]*options[shift+class="num">3]; class="type">class="kw">double temp=class="num">1/sqrt(options[shift+class="num">1]+class="num">1e-6); class="type">class="kw">double gmu=(-temp)*gnx; class="type">class="kw">double gvar=(options[shift]*inp)/(class="num">2*pow(options[shift+class="num">1]+class="num">1.0e-6,class="num">3/class="num">2))*gnx; class="type">class="kw">double gx=temp*gnx+gmu/batch+gvar*class="num">2*inp/batch*pow((class="type">class="kw">double)(batch-class="num">1)/batch,class="num">2.0); class=class="str">"cmt">//--- if(isnan(gx)) gx=class="num">0; class="kw">switch(activation) { case class="num">0:
「批归一化层在 GPU 上的梯度回传实现」
这段 CNeuronBatchNormOCL::calcInputGradients 是神经网络里批归一化(BatchNorm)层反向传播的核心。它把上层神经元传来的梯度,按当前批次统计量和激活函数类型,折算成前一层应得的输入梯度,全程交给 OpenCL 内核在显卡上并行算。 函数开头先做指针与批次规模校验:若 OpenCL 上下文或上游神经元指针无效直接返回 false;若 iBatchSize<=1 则跳过 GPU 计算,仅确认前层指针存在便返回。BatchOptions 缓冲区创建失败也会中断,避免空参数进内核。 随后把输出索引、批次选项、梯度矩阵、上游梯度索引、激活类型、批次大小、优化模式共 7 个参数通过 SetArgumentBuffer / SetArgument 绑进 def_k_CalcHiddenGradientBatch 内核,工作维度设为 1,global_work_size 等于神经元总数 Neurons()。任何一步绑定失败都返回 false,保证内核不会拿到野指针。 内核真正执行后用 BufferRead 把算好的梯度从显存读回主存,并释放批次临时缓冲。外汇与贵金属市场使用这类自定义 AI 模型做信号生成时波动剧烈、杠杆风险高,回测通过不代表实盘概率占优,建议先在 MT5 策略测试器以极小手数验证梯度方向是否合理。
gx=clamp(gx+inp,-class="num">1.0,class="num">1.0)-inp; gx=gx*(class="num">1-pow(inp==class="num">1 || inp==-class="num">1 ? class="num">0.99999999 : inp,class="num">2)); class="kw">break; case class="num">1: gx=clamp(gx+inp,class="num">0.0,class="num">1.0)-inp; gx=gx*(inp==class="num">0 || inp==class="num">1 ? class="num">0.00000001 : (inp*(class="num">1-inp))); class="kw">break; case class="num">2: if(inp<class="num">0) gx*=class="num">0.01; class="kw">break; class="kw">default: class="kw">break; } matrix_ig[n]=clamp(gx,-MAX_GRADIENT,MAX_GRADIENT); } class="type">bool CNeuronBatchNormOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(iBatchSize<=class="num">1) class="kw">return (CheckPointer(PrevLayer)!=POINTER_INVALID); class=class="str">"cmt">//--- if(CheckPointer(BatchOptions)==POINTER_INVALID || !BatchOptions.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">1]= {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0]=Neurons(); if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientBatch,def_k_bchg_matrix_i,NeuronOCL.getOutputIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientBatch,def_k_bchg_options,BatchOptions.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientBatch,def_k_bchg_matrix_g,Gradient.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientBatch,def_k_bchg_matrix_ig,NeuronOCL.getGradientIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_CalcHiddenGradientBatch,def_k_bchg_activation,NeuronOCL.Activation())) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_CalcHiddenGradientBatch,def_k_bchg_batch,iBatchSize)) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_CalcHiddenGradientBatch,def_k_bchg_optimization,(class="type">int)optimization)) class="kw">return class="kw">false; ResetLastError(); class=class="str">"cmt">//--- if(!OpenCL.Execute(def_k_CalcHiddenGradientBatch,class="num">1,global_work_offset,global_work_size)) { printf("Error of execution kernel Batch CalcHiddenGradient: %d",GetLastError()); class="kw">return class="kw">false; } if(!NeuronOCL.getGradient().BufferRead()) class="kw">return class="kw">false; BatchOptions.BufferFree(); class=class="str">"cmt">//--- class="kw">return true; }
动量项与Adam在GPU核里的权重更新写法
在 MT5 的 OpenCL 层做神经网络训练,权重更新被拆成两个 kernel:一个走经典动量,一个走 Adam。两者都按上一层的神经元数 m 展开,每个神经元在 options 矩阵里占 7 或 9 个 double 槽位,由 get_global_id(0) 算出全局序号 n 再乘偏移。 动量核里 shift=n*7,梯度取 matrix_g[n],delta 用 learning_rates*grad 乘当前权重变化基(options[shift+2] 与常数 1)再加 momentum 乘历史速度(options[shift+5]、[shift+6])。更新前用 isnan 挡掉脏数,权重写回时 clamp 到 ±MAX_WEIGHT,避免外汇样本里极端跳空把权值炸飞。 Adam 核 shift=n*9,多了 b1、b2 两个动量系数。一阶矩 mt 直接按 b1*历史 + (1-b1)*当前梯度缩放 算出来,缩放因子仍是 options[shift+2] 对权重、常数 1 对偏置。想验证可把这段代码贴进 EA 的 CL 上下文,改 b1=0.9、b2=0.999 看 EURUSD 的 M15 回测收敛速度,贵金属同样高风险,权值漂移可能放大滑点。
__kernel class="type">void UpdateBatchOptionsMomentum(__global class="type">class="kw">double *options, class=class="str">"cmt">///<-[in,out] Options matrix m*class="num">7, where m - Number of neurons in previous layer __global class="type">class="kw">double *matrix_g, class=class="str">"cmt">///<-[in] Tensor of gradients at current layer class="type">class="kw">double learning_rates, class=class="str">"cmt">///<- Learning rates class="type">class="kw">double momentum class=class="str">"cmt">///<- Momentum multiplier ) { class="kw">const class="type">int n=get_global_id(class="num">0); class="kw">const class="type">int shift=n*class="num">7; class="type">class="kw">double grad=matrix_g[n]; class=class="str">"cmt">//--- double2 delta=learning_rates*grad*(double2)(options[shift+class="num">2],class="num">1) + momentum*(double2)(options[shift+class="num">5],options[shift+class="num">6]); if(!isnan(delta.s0) && !isnan(delta.s1)) { options[shift+class="num">5]=delta.s0; options[shift+class="num">3]=clamp(options[shift+class="num">3]+delta.s0,-MAX_WEIGHT,MAX_WEIGHT); options[shift+class="num">6]=delta.s1; options[shift+class="num">4]=clamp(options[shift+class="num">4]+delta.s1,-MAX_WEIGHT,MAX_WEIGHT); } }; __kernel class="type">void UpdateBatchOptionsAdam(__global class="type">class="kw">double *options, class=class="str">"cmt">///<-[in,out] Options matrix m*class="num">9, where m - Number of neurons in previous layer __global class="type">class="kw">double *matrix_g, class=class="str">"cmt">///<-[in] Tensor of gradients at current layer class="kw">const class="type">class="kw">double l, class=class="str">"cmt">///<- Learning rates class="kw">const class="type">class="kw">double b1, class=class="str">"cmt">///<- First momentum multiplier class="kw">const class="type">class="kw">double b2 class=class="str">"cmt">///<- Second momentum multiplier ) { class="kw">const class="type">int n=get_global_id(class="num">0); class="kw">const class="type">int shift=n*class="num">9; class="type">class="kw">double grad=matrix_g[n]; class=class="str">"cmt">//--- double2 mt=b1*(double2)(options[shift+class="num">5],options[shift+class="num">6])+(class="num">1-b1)*(double2)(grad*options[shift+class="num">2],grad);
◍ BatchNorm 层里动量项与 Adam 项的权重更新分支
在 MT5 的 OpenCL 神经网络实现里,批归一化层(CNeuronBatchNormOCL)的 updateInputWeights 方法会根据优化器类型走不同内核。当 optimization==SGD 时调用 UpdateBatchOptionsMomentum,否则走 UpdateBatchOptionsAdam,两者都依赖 BatchOptions 缓冲与梯度矩阵在显存中的索引绑定。 权重更新核心不是直接写权重,而是先算二阶动量 vt 与步长 delta,再做截断。下面这段内核侧的等价逻辑把 options 缓冲里偏移 3~8 的槽位分别当作 w、m、v 来读写,1.0e-8 的 epsilon 防止除以零: 如果 batch 尺寸小于等于 1,函数直接短路返回,意味着单样本推断不会触发动量或 Adam 的状态累积;Neurons() 决定 global_work_size[0],也就是并行内核的一维线程数。 实盘接 AIGC 模型时,外汇与贵金属杠杆高、跳空频繁,这类归一化层若学习率 eta 或 lr 设得偏激,权重可能在一两根 M5 棒内冲到 MAX_WEIGHT 截断值,建议开 MT5 把 def_k_buom_learning_rates 与 def_k_buoa_l 打日志对比两次迭代的 options[shift+3] 漂移量。
double2 vt=b2*(double2)(options[shift+class="num">5],options[shift+class="num">6])+(class="num">1-b2)*pow((double2)(grad*options[shift+class="num">2],grad),class="num">2); double2 delta=l*mt/sqrt(vt+class="num">1.0e-8); if(isnan(delta.s0) || isnan(delta.s1)) class="kw">return; double2 weight=clamp((double2)(options[shift+class="num">3],options[shift+class="num">4])+delta,-MAX_WEIGHT,MAX_WEIGHT); class=class="str">"cmt">//--- if(!isnan(weight.s0) && !isnan(weight.s1)) { options[shift+class="num">3]=weight.s0; options[shift+class="num">4]=weight.s1; options[shift+class="num">5]=mt.s0; options[shift+class="num">6]=mt.s1; options[shift+class="num">7]=vt.s0; options[shift+class="num">8]=vt.s1; } };
「BatchNorm 神经元的 OpenCL 内核参数约定」
在 MT5 用 OpenCL 加速神经网络时,批归一化层(CNeuronBatchNormOCL)的前向、反向与优化内核都靠一组宏定义来绑定参数槽位。比如 Adam 优化更新里,先向内核塞入 b2 参数,失败就直接返回 false,再清错误码执行内核,global_work_size 为 1 的轻量调度说明这是按批次粒度跑的。 #define defNeuronBatchNormOCL 0x7891 给类打标识,内核索引从 24 起排:def_k_BatchFeedForward=24 管前向,def_k_CalcHiddenGradientBatch=25 管隐层梯度回传,def_k_UpdateBatchOptionsMomentum=26 管动量优化。每个内核的参数槽用 0、1、2… 编号,例如前向里 0 是输入张量、2 是输出张量、3 是 batch size、5 是激活类型。 反向梯度内核 def_k_CalcHiddenGradientBatch 的槽位值得在 MT5 里对照看:1 是当前层梯度输入,3 是上一层梯度输出,4 是激活类型,6 是优化类型。若你改了自定义激活或优化器,必须保证调用 SetArgument 时的索引和这些宏一致,否则 BufferRead 读回的结果可能静默错乱。 开 MT5 导航到 MQL5/Include/Neural 相关头文件,搜 def_k_bff_ 和 def_k_bchg_ 前缀,把这套索引表存成注释贴到自己的 EA 里,调参时能少踩不少坑。外汇与贵金属杠杆交易高风险,这类 GPU 加速逻辑仅用于研究验证,实盘前务必充分回测。
if(!OpenCL.SetArgument(def_k_UpdateBatchOptionsAdam,def_k_buoa_b2,b2)) class="kw">return class="kw">false; ResetLastError(); class=class="str">"cmt">//--- if(!OpenCL.Execute(def_k_UpdateBatchOptionsAdam,class="num">1,global_work_offset,global_work_size)) { printf("Error of execution kernel UpdateBatchOptionsAdam %d",GetLastError()); class="kw">return class="kw">false; } } class=class="str">"cmt">//--- if(!BatchOptions.BufferRead()) class="kw">return class="kw">false; BatchOptions.BufferFree(); class=class="str">"cmt">//--- class="kw">return true; } class="macro">#define defNeuronBatchNormOCL 0x7891 class=class="str">"cmt">///<Batchnorm neuron OpenCL \details Identified class class="macro">#CNeuronBatchNormOCL class="macro">#define def_k_BatchFeedForward class="num">24 class=class="str">"cmt">///< Index of the kernel for Batch Normalization Feed Forward process(class="macro">#CNeuronBathcNormOCL) class="macro">#define def_k_bff_inputs class="num">0 class=class="str">"cmt">///< Inputs data tensor class="macro">#define def_k_bff_options class="num">1 class=class="str">"cmt">///< Tensor of variables class="macro">#define def_k_bff_output class="num">2 class=class="str">"cmt">///< Tensor of output data class="macro">#define def_k_bff_batch class="num">3 class=class="str">"cmt">///< Batch size class="macro">#define def_k_bff_optimization class="num">4 class=class="str">"cmt">///< Optimization type class="macro">#define def_k_bff_activation class="num">5 class=class="str">"cmt">///< Activation type class=class="str">"cmt">//--- class="macro">#define def_k_CalcHiddenGradientBatch class="num">25 class=class="str">"cmt">///< Index of the Kernel of the Batch neuron to transfer gradient to previous layer(class="macro">#CNeuronBatchNormOCL) class="macro">#define def_k_bchg_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_bchg_matrix_g class="num">1 class=class="str">"cmt">///<[in] Tensor of gradients at current layer class="macro">#define def_k_bchg_matrix_i class="num">2 class=class="str">"cmt">///<[in] Tensor of previous layer output class="macro">#define def_k_bchg_matrix_ig class="num">3 class=class="str">"cmt">///<[out] Tensor of gradients at previous layer class="macro">#define def_k_bchg_activation class="num">4 class=class="str">"cmt">///< Activation type(class="macro">#ENUM_ACTIVATION) class="macro">#define def_k_bchg_batch class="num">5 class=class="str">"cmt">///< Batch size class="macro">#define def_k_bchg_optimization class="num">6 class=class="str">"cmt">///< Optimization type class=class="str">"cmt">//--- class="macro">#define def_k_UpdateBatchOptionsMomentum class="num">26 class=class="str">"cmt">///< Index of the kernel for Describe the process of SGD optimization options for the Batch normalization Neuron(class="macro">#CNeuronBatchNormOCL). class="macro">#define def_k_buom_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