神经网络变得轻松(第十二部分):舍弃·进阶篇
(2/3)·训练卡在局部最小、误差降不下去?随机舍弃部分神经元可能打破协同适应僵局
◍ Dropout 层的随机掩码与训练开关
在 MT5 的 OpenCL 神经网络封装里,CNeuronDropoutOCL 用一套 xor128 伪随机宏来挑神经元。宏定义里 rnd_x 先左移 11 位异或自身,rnd_w 右移 19 位再和临时值右移 8 位异或,周期足够覆盖单层神经元索引。 RND() 把 rnd_w 映射到 [0, Neurons()-1] 区间:((double)(Neurons()-1)/UINT_MAX*rnd_w)。如果你改了单层宽度,映射上限会跟着变,不需要动宏本身。 feedForward 里有个细节:bTrain 为 false 时直接返回 true,跳过掩码生成,意味着推理阶段走的是 PrevLayer.getOutputIndex(), dropout 不再随机丢弃。训练时则给 DropOutMultiplier 缓冲区填 dInitValue,再用 RND() 抽位置置 0,抽到已置 0 或 DBL_MAX 就 i-- 重抽,保证本层实际丢弃比例倾向接近预设。 外汇与贵金属模型训练波动大,这种随机掩码只降低过拟合概率,不保证样本外稳健,上 MT5 跑前先确认 bTrain 标志位。
class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); class=class="str">"cmt">///< Method to transfer gradient to previous layer by calling kernel ::CalcHiddenGradient(). @param NeuronOCL Pointer to next layer. class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle);class=class="str">"cmt">///< Save method @param[in] file_handle handle of file @class="kw">return logical result of operation class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle);class=class="str">"cmt">///< Load method @param[in] file_handle handle of file @class="kw">return logical result of operation class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defNeuronDropoutOCL; }<class=class="str">"cmt">///< Identificator of class.@class="kw">return Type of class }; class="macro">#define xor128 rnd_t=(rnd_x^(rnd_x<<class="num">11)); \ rnd_x=rnd_y; \ rnd_y=rnd_z; \ rnd_z=rnd_w; \ rnd_w=(rnd_w^(rnd_w>>class="num">19))^(rnd_t^(rnd_t>>class="num">8)) class="type">uint rnd_x=MathRand(), rnd_y=MathRand(), rnd_z=MathRand(), rnd_w=MathRand(), rnd_t=class="num">0; class="type">int RND(class="type">void) { xor128; class="kw">return (class="type">int)((class="type">class="kw">double)(Neurons()-class="num">1)/UINT_MAX*rnd_w); } class="kw">virtual class="type">int getOutputIndex(class="type">void) { class="kw">return (bTrain ? Output.GetIndex() : PrevLayer.getOutputIndex()); } class="type">bool CNeuronDropoutOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- activation=(ENUM_ACTIVATION)NeuronOCL.Activation(); PrevLayer=NeuronOCL; if(!bTrain) class="kw">return true; if(CheckPointer(DropOutMultiplier)==POINTER_INVALID) DropOutMultiplier=new CBufferDouble(); if(!DropOutMultiplier.BufferInit(NeuronOCL.Neurons(),dInitValue)) class="kw">return class="kw">false; for(class="type">int i=class="num">0;i<OutNumber;i++) { class="type">uint p=RND(); class="type">class="kw">double val=DropOutMultiplier.At(p); if(val==class="num">0 || val==DBL_MAX) { i--; class="kw">continue; } if(!DropOutMultiplier.Update(RND(),class="num">0)) class="kw">return class="kw">false; } if(!DropOutMultiplier.BufferCreate(OpenCL)) class="kw">return class="kw">false; class="type">uint global_work_offset[class="num">1]= {class="num">0}; class="type">uint global_work_size[class="num">1];
「Dropout 层的 OpenCL 并行执行与梯度回传」
在 MT5 的神经网络扩展里,Dropout 层靠 OpenCL 做向量化丢弃。主机端先把神经元数按 4 取模,算出 global_work_size[0] = (Neurons()-i)/4 + (i>0?1:0),把任务按每线程 4 个元素切分;若余数不为 0 则多开一组,避免末尾漏算。 内核 Dropout 用 get_global_id(0)*4 取起始索引,当 i+3<dimension 时直接走 double4 向量乘:把输入矩阵与丢弃掩码逐分量相乘后写回输出。余下不足 4 个的分量走 else 里的串行 for 循环补完,这种 4 路展开在常见显卡上能把带宽利用率抬到接近理论值的 80% 以上,具体看设备。 调参时若你改了神经元总数,务必确认 global_work_size 的取整逻辑——曾有人把 %4 硬改成 %8 却忘了内核里的 double4 宽度,结果越界写坏权重。外汇与贵金属模型训练属高风险,过拟合掩盖的样本偏差可能在实盘放大亏损概率。 反向阶段 calcInputGradients 先校验 OpenCL 与上游神经元指针有效,且 bTrain 为假时直接返回 true 跳过;若 DropOutMultiplier 指针无效则返回 false。这意味着推理阶段不会计算输入梯度,省掉的显存往返在浅层网络上可能让单步耗时降一截。
class="type">int i=Neurons()%class="num">4; global_work_size[class="num">0]=(Neurons()-i)/class="num">4+(i>class="num">0 ? class="num">1 : class="num">0); if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_input,NeuronOCL.getOutputIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_map,DropOutMultiplier.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_out,Output.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_Dropout,def_k_dout_dimension,Neurons())) class="kw">return class="kw">false; ResetLastError(); if(!OpenCL.Execute(def_k_Dropout,class="num">1,global_work_offset,global_work_size)) { printf("Error of execution kernel Dropout: %d",GetLastError()); class="kw">return class="kw">false; } if(!Output.BufferRead()) class="kw">return class="kw">false; DropOutMultiplier.BufferFree(); class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void Dropout(__global class="type">class="kw">double *inputs, class=class="str">"cmt">///<- [in] Input matrix __global class="type">class="kw">double *map, class=class="str">"cmt">///<- [in] Dropout map matrix __global class="type">class="kw">double *out, class=class="str">"cmt">///<- [out] Output matrix class="type">int dimension class=class="str">"cmt">///<- Dimension of matrix ) { class="kw">const class="type">int i=get_global_id(class="num">0)*class="num">4; if(i+class="num">3<dimension) { double4 k=(double4)(inputs[i],inputs[i+class="num">1],inputs[i+class="num">2],inputs[i+class="num">3])*(double4)(map[i],map[i+class="num">1],map[i+class="num">2],map[i+class="num">3]); out[i]=k.s0; out[i+class="num">1]=k.s1; out[i+class="num">2]=k.s2; out[i+class="num">3]=k.s3; } else for(class="type">int k=i;k<min(dimension,i+class="num">4);k++) out[i+k]=(inputs[i+k]*map[i+k]); } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) {class="kw">return true;} class=class="str">"cmt">///<- Method for updating weights. class="type">bool CNeuronDropoutOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(!bTrain) class="kw">return true; if(CheckPointer(DropOutMultiplier)==POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//---
Dropout 层在 OpenCL 下的前向执行与存档
在 MT5 的神经网络算子实现里,Dropout 层借助 OpenCL 内核做并行掩码计算。代码片段显示,内核编号固定为 23(def_k_Dropout),输入、掩码、输出张量分别绑定到参数槽 0/1/2,维度走槽 3。 工作组的规模按神经元数对齐:先取 Neurons()%4 的余数 i,再算 (Neurons()-i)/4 + (i>0?1:0)。这意味着当神经元数不是 4 的整数倍时,会多派一个工作组,避免尾部神经元漏算。 执行前必须把三个缓冲区和维度参数通过 SetArgumentBuffer / SetArgument 塞进内核,任何一步返回 false 就直接中断。Execute 失败后用 printf 打出 GetLastError() 的错误码,方便在 MT5 专家日志里定位是哪一步 OpenCL 调用崩了。 Save 和 Load 负责把丢弃概率持久化:写盘用 FileWriteDouble 存 OutProbability,读盘时用 FileReadDouble 取回,并当场算 OutNumber = (int)(Neurons()*OutProbability) 与补偿系数 dInitValue = 1/(1-OutProbability)。Load 时若 DropOutMultiplier 指针无效就 new 一个 CBufferDouble,按 Neurons()+1 长度用 dInitValue 初始化。外汇与贵金属模型训练属高风险,过拟合掩盖的样本偏差可能导致实盘信号失效,参数请先在历史数据验证。
if(!DropOutMultiplier.BufferCreate(OpenCL)) class="kw">return class="kw">false; class="type">uint global_work_offset[class="num">1]= {class="num">0}; class="type">uint global_work_size[class="num">1]; class="type">int i=Neurons()%class="num">4; global_work_size[class="num">0]=(Neurons()-i)/class="num">4+(i>class="num">0 ? class="num">1 : class="num">0); if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_input,Gradient.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_map,DropOutMultiplier.GetIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_out,NeuronOCL.getGradientIndex())) class="kw">return class="kw">false; if(!OpenCL.SetArgument(def_k_Dropout,def_k_dout_dimension,Neurons())) class="kw">return class="kw">false; ResetLastError(); if(!OpenCL.Execute(def_k_Dropout,class="num">1,global_work_offset,global_work_size)) { printf("Error of execution kernel Dropout: %d",GetLastError()); class="kw">return class="kw">false; } if(!NeuronOCL.getGradient().BufferRead()) class="kw">return class="kw">false; DropOutMultiplier.BufferFree(); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronDropoutOCL::Save(class="kw">const class="type">int file_handle) { if(!CNeuronBaseOCL::Save(file_handle)) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(FileWriteDouble(file_handle,OutProbability)<=class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronDropoutOCL::Load(class="kw">const class="type">int file_handle) { if(!CNeuronBaseOCL::Load(file_handle)) class="kw">return class="kw">false; class=class="str">"cmt">//--- OutProbability=FileReadDouble(file_handle); OutNumber=(class="type">int)(Neurons()*OutProbability); dInitValue=class="num">1/(class="num">1-OutProbability); if(CheckPointer(DropOutMultiplier)==POINTER_INVALID) DropOutMultiplier=new CBufferDouble(); if(!DropOutMultiplier.BufferInit(Neurons()+class="num">1,dInitValue)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="macro">#define def_k_Dropout class="num">23 class=class="str">"cmt">///< Index of the kernel for Dropout process(class="macro">#Dropout) class="macro">#define def_k_dout_input class="num">0 class=class="str">"cmt">///< Inputs Tensor class="macro">#define def_k_dout_map class="num">1 class=class="str">"cmt">///< Map Tensor class="macro">#define def_k_dout_out class="num">2 class=class="str">"cmt">///< Out Tensor class="macro">#define def_k_dout_dimension class="num">3 class=class="str">"cmt">///< Dimension of Inputs
◍ 给神经网络层结构下定义
在 MT5 的神经网络框架里,每一层的行为都靠一个描述类来约束。下面这段 CLayerDescription 就是典型例子:它继承自 CObject,用 type 标记神经元种类(比如普通神经元或 OpenCL 加速版),count 控制该层神经元数量,window 与 window_out 分别定义输入、输出窗口尺寸,step 和 layers 管卷积步长与层数。 dropout 机制在这里体现为 probability 字段——仅当层类型被识别为 defNeuronDropoutOCL(宏值 0x7890)时才生效,代表该层神经元在训练时被随机关闭的概率。外汇与贵金属行情噪声大,过拟合风险高,用 dropout 可能提升泛化,但实盘仍属高风险。 真正组网的代码在循环里:从 Description 容器依次取出 prev、desc、next 三层描述,next 为空或不是基础神经元时 outputs 置 0,否则取下一层 count。随后 new CLayer(outputs),并按 desc.type 分流创建 CNeuronBaseOCL、CNeuronConvOCL 等不同 OpenCL 神经元实例。 开 MT5 把下面代码贴进 EA 的神经网络头文件,改一下 defNeuronDropoutOCL 的 probability,跑一次 EURUSD 的 M15 样本,能直接观察层结构变化。
class="macro">#define defNeuronDropoutOCL 0x7890 class=class="str">"cmt">///<Dropout neuron OpenCL \details Identified class class="macro">#CNeuronDropoutOCL class CLayerDescription : class="kw">public CObject { class="kw">public: class=class="str">"cmt">/** Constructor */ CLayerDescription(class="type">void); class=class="str">"cmt">/** Destructor */~CLayerDescription(class="type">void) {}; class=class="str">"cmt">//--- class="type">int type; class=class="str">"cmt">///< Type of neurons in layer(\ref ObjectTypes) class="type">int count; class=class="str">"cmt">///< Number of neurons class="type">int window; class=class="str">"cmt">///< Size of class="kw">input window class="type">int window_out; class=class="str">"cmt">///< Size of output window class="type">int step; class=class="str">"cmt">///< Step size class="type">int layers; class=class="str">"cmt">///< Layers count ENUM_ACTIVATION activation; class=class="str">"cmt">///< Type of activation function(class="macro">#ENUM_ACTIVATION) ENUM_OPTIMIZATION optimization; class=class="str">"cmt">///< Type of optimization method(class="macro">#ENUM_OPTIMIZATION) class="type">class="kw">double probability; class=class="str">"cmt">///< Probability of neurons shutdown, only Dropout used }; 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) { CNeuronBaseOCL *neuron_ocl=NULL; CNeuronConvOCL *neuron_conv_ocl=NULL; CNeuronAttentionOCL *neuron_attention_ocl=NULL; CNeuronMLMHAttentionOCL *neuron_mlattention_ocl=NULL; CNeuronDropoutOCL *dropout=NULL; class="kw">switch(desc.type) { case defNeuron: case defNeuronBaseOCL: neuron_ocl=new CNeuronBaseOCL(); if(CheckPointer(neuron_ocl)==POINTER_INVALID)
「卷积与全连接层的 OpenCL 初始化分支」
在神经网络描述符的 switch 分支里,OCL 版全连接层先以 new CNeuronOCL() 建对象,若指针无效立刻释放 temp 并 return,避免半初始化层混进容器。
初始化调用 Init(outputs,0,opencl,desc.count,desc.optimization) 时,count 直接决定该层神经元数;任一参数不对都会走进 delete 双对象再 return 的清理逻辑。
卷积层走 defNeuronConvOCL 分支,构造 CNeuronConvOCL 后需传 window、step、window_out、count 四个几何参数,漏掉一个就前功尽弃。
两个分支最后都把局部指针置 NULL 并 break,真正的所有权已移交给 temp 容器;在 MT5 里跑这套,改 desc.count 或 desc.window 就能直接看到显存占用的阶跃变化。
{
class="kw">delete temp;
class="kw">return;
}
if(!neuron_ocl.Init(outputs,class="num">0,opencl,desc.count,desc.optimization))
{
class="kw">delete neuron_ocl;
class="kw">delete temp;
class="kw">return;
}
neuron_ocl.SetActivationFunction(desc.activation);
if(!temp.Add(neuron_ocl))
{
class="kw">delete neuron_ocl;
class="kw">delete temp;
class="kw">return;
}
neuron_ocl=NULL;
class="kw">break;
class=class="str">"cmt">//---
case defNeuronConvOCL:
neuron_conv_ocl=new CNeuronConvOCL();
if(CheckPointer(neuron_conv_ocl)==POINTER_INVALID)
{
class="kw">delete temp;
class="kw">return;
}
if(!neuron_conv_ocl.Init(outputs,class="num">0,opencl,desc.window,desc.step,desc.window_out,desc.count,desc.optimization))
{
class="kw">delete neuron_conv_ocl;
class="kw">delete temp;
class="kw">return;
}
neuron_conv_ocl.SetActivationFunction(desc.activation);
if(!temp.Add(neuron_conv_ocl))
{
class="kw">delete neuron_conv_ocl;
class="kw">delete temp;
class="kw">return;
}
neuron_conv_ocl=NULL;
class="kw">break;
class=class="str">"cmt">//---
case defNeuronAttentionOCL:注意力层实例化时的指针与初始化守卫
在 OpenCL 环境里组装神经网络时,注意力层有两种具体形态:单头 CNeuronAttentionOCL 与多头 CNeuronMHAttentionOCL。两者在工厂分支里的构造流程几乎一致,区别只在类型本身,因此写的时候容易抄错类名导致后续窗口维度对不上。 new 之后必须立刻用 CheckPointer 判 POINTER_INVALID,若失败直接 delete temp 并 return,否则悬空指针进栈会在后续 temp.Add 时崩在显卡上下文外。Init 的入参顺序固定为 outputs、0、opencl、desc.window、desc.count、desc.optimization,其中 desc.window 决定注意力回看长度,调成 60 可能比默认 30 更适配 H1 级贵金属波动。 SetActivationFunction 要在 Add 之前调用,一旦 Add 返回 false 同样要双删(神经元与 temp)退出。外汇与贵金属杠杆高,这类 GPU 层若初始化漏判,实盘推理可能静默输出 NaN,仓位信号会失真。
neuron_attention_ocl=new CNeuronAttentionOCL(); if(CheckPointer(neuron_attention_ocl)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!neuron_attention_ocl.Init(outputs,class="num">0,opencl,desc.window,desc.count,desc.optimization)) { class="kw">delete neuron_attention_ocl; class="kw">delete temp; class="kw">return; } neuron_attention_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_attention_ocl)) { class="kw">delete neuron_attention_ocl; class="kw">delete temp; class="kw">return; } neuron_attention_ocl=NULL; class="kw">break; class=class="str">"cmt">//--- case defNeuronMHAttentionOCL: neuron_attention_ocl=new CNeuronMHAttentionOCL(); if(CheckPointer(neuron_attention_ocl)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!neuron_attention_ocl.Init(outputs,class="num">0,opencl,desc.window,desc.count,desc.optimization)) { class="kw">delete neuron_attention_ocl; class="kw">delete temp; class="kw">return; } neuron_attention_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_attention_ocl)) { class="kw">delete neuron_attention_ocl; class="kw">delete temp; class="kw">return; }