神经网络变得轻松(第十二部分):舍弃·进阶篇
🧠

神经网络变得轻松(第十二部分):舍弃·进阶篇

(2/3)·训练卡在局部最小、误差降不下去?随机舍弃部分神经元可能打破协同适应僵局

实战向进阶 第 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 标志位。

MQL5 / C++
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。这意味着推理阶段不会计算输入梯度,省掉的显存往返在浅层网络上可能让单步耗时降一截。

MQL5 / C++
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 初始化。外汇与贵金属模型训练属高风险,过拟合掩盖的样本偏差可能导致实盘信号失效,参数请先在历史数据验证。

MQL5 / C++
  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 样本,能直接观察层结构变化。

MQL5 / C++
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 就能直接看到显存占用的阶跃变化。

MQL5 / C++
              {
                  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,仓位信号会失真。

MQL5 / C++
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;
     }
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到训练过程中的特征屏蔽与误差曲线,你专注决策就好。

常见问题

常见做法是从 0.2 到 0.5 之间起步做实验,p 越大屏蔽越多特征,需结合验证集误差判断,没有统一最优值。
小布盯盘内置的 AIGC 诊断可呈现训练态与推理态差异,但策略实盘仍需你在 MT5 自行部署代码,外汇贵金属属高风险。
操作期要评估所有特征求最准确独立状态,舍弃只用于训练打破协同适应,推理用全量神经元输出。
因保留神经元期望输入缩小为原 q 倍,乘 1/q 使期望回归原尺度,梯度反向传播时再乘其导数即可。
若保存方法同步扩展掩码逻辑,旧权重可能需重映射,建议本篇测试节先跑通新类存读再迁移。