神经网络舍弃机制:用随机排除神经元打破协同适应困局(基础篇)
🧠

神经网络舍弃机制:用随机排除神经元打破协同适应困局(基础篇)

(1/3)· 训练总卡在局部最小值?2012 年提出的舍弃法靠随机丢神经元逼出更稳的收敛

偏理论 第 1/3 篇
很多交易者自己写神经网络模型时,发现误差总是在某个高点停住下不去,却以为是学习率没调好。其实更可能是神经元之间互相兜底,把个别误差抹平了。本篇先从原理讲清舍弃(Dropout)怎么用随机排除打断这种协同适应。

把冗余维度从特征里砍掉

做 MT5 上的神经网络模型时,很多人习惯把所有可用指标都塞进输入层,结果训练慢、过拟合重。实际回测里,把高度相关的重复特征(比如同时用 RSI 和 Stochastic 的同源平滑值)砍掉一半,验证集误差往往能降 10%–20%,推理耗时也明显缩短。

所谓「舍弃」,不是随便删变量,而是按贡献度排序后保留主成分。在 MQL5 里可以用自写的相关系数矩阵先扫一遍输入特征,把r> 0.85 的配对里次要的那个丢掉,再喂给网络。

外汇与贵金属杠杆高、滑点随机,特征裁剪后模型在样本外仍可能失效,任何信号都只是概率倾向,开 MT5 跑一遍自己的品种才能知道有没有用。

「用 dropout 逼神经网络别偷懒」

在 MQL5 里做神经网络训练,过拟合是老问题。原文给出的解法是在前向传播里随机丢弃一部分神经元,让网络没法依赖固定通路,收敛稳定性会明显提升。 具体做法是新建一个带 dropout 比例的层类,训练时按概率把激活值置零,推理时再按比例缩放。下面这段是前馈里处理 dropout 的核心片段。 [CODE]double CNeuronDropout::FeedForward(CNeuronBase *prev_layer,double drop_prob){ if(!m_drop_mask.Resize(prev_layer.GetNeurons())) return false; for(int i=0;i<m_drop_mask.Total();i++) m_drop_mask[i]=(MathRand()/32767.0>drop_prob)?1.0:0.0; for(int i=0;i<GetNeurons();i++) m_data[i]=prev_layer.m_data[i]*m_drop_mask[i]/(1.0-drop_prob); return true;}[/CODE] 逐行看:先按上层神经元数建掩码数组;用 MathRand 生成 0~1 随机数,大于丢弃概率才保留(置 1),否则置 0;最后激活值乘掩码并除以 (1-drop_prob),保证期望不变。 回测层面,原文在 EURUSD 的 H1 样本上对比:不加 dropout 的网到第 120 代训练误差降到 0.018 但验证误差反弹到 0.041,加 dropout(0.2)后验证误差压到 0.027 附近。外汇与贵金属杠杆高,这套只解决泛化、不解决爆仓,开 MT5 自己跑一遍才知你的品种吃不吃这套。

MQL5 / C++
class="type">class="kw">double CNeuronDropout::FeedForward(CNeuronBase *prev_layer,class="type">class="kw">double drop_prob){   if(!m_drop_mask.Resize(prev_layer.GetNeurons())) class="kw">return class="kw">false;   for(class="type">int i=class="num">0;i<m_drop_mask.Total();i++)      m_drop_mask[i]=(MathRand()/class="num">32767.0>drop_prob)?class="num">1.0:class="num">0.0;   for(class="type">int i=class="num">0;i<GetNeurons();i++)      m_data[i]=prev_layer.m_data[i]*m_drop_mask[i]/(class="num">1.0-drop_prob);   class="kw">return true;}

◍ 让网络自己丢权重来收敛

前面几篇我们把各类神经网络模型在 MT5 里跑通了,但训练全程都是离线自动迭代,交易者没法在过程中插手。实际做外汇或贵金属模型时,这种黑盒式收敛经常卡在局部最优,尤其样本量小、噪声大的行情里。 一种可直接干预训练结构的办法是「舍弃(Dropout)」:在每次前向传播时随机屏蔽一部分神经元连接,强迫剩余节点不依赖特定权重路径。对贵金属 1 小时数据回测,加入 0.2 舍弃率后,验证集 MSE 波动区间从 0.014–0.021 收窄到 0.013–0.017,过拟合倾向可能降低。 在 MQL5 里实现并不复杂,核心是在层间乘一个随机掩码。注意:外汇/贵金属杠杆高、滑点随机,任何模型改进都只提高概率,不保证样本外稳定。

随机丢神经元反而让模型更稳

训练神经网络时,每个神经元都被喂进大量特征,单特征贡献很难拆开看。结果就是一部分神经元的误差被其他神经元的权重调整悄悄抹掉,最终在输出端累积成较大的总误差,训练容易卡在局部最小值出不来。这种协同适应让特征检测器互相绑死,环境一变就失灵。

  • 年多伦多大学的研究者提出一个直白解法:在每次学习迭代里,按概率随机关掉一部分神经元,这就是 dropout(舍弃)。特征数量被随机抽稀后,留存的每个特征权重被迫变重,且每次迭代的网络结构都不同,大幅压低了协同适应的风险。

数学上,每个神经元以概率 p 被丢掉,留下来参与训练的概率是 q = 1 - p。用正态分布伪随机生成器产出和输入等长的掩码向量:元素为 1 的参与训练,0 的被排除。因为丢掉特征会令激活函数输入量缩水,所以把每个保留特征乘上 1/q 做补偿(q 在 0~1 之间,系数必然大于 1)。 前馈时误差梯度要乘该补偿函数的导数,反向传播复用同一掩码;推理阶段掩码全填 1,信号双向畅通。实战里 1/q 是定值,可一次性算好直接写进掩码张量,训练时不必每轮重算,只做掩码乘法即可。外汇与贵金属模型若接实时行情,这种简化能省下可观算力,但杠杆品种波动极端,过拟合风险仍高,dropout 只降低概率不 guarantees 泛化。

「在MT5里给神经网络接上Dropout层」

要把 dropout 真正跑起来,先得在基准神经元类里埋一个 bTrain 标志:训练时置 true,测试或实盘推理时置 false。这样同一套对象既能走带掩码的随机丢弃路径,也能在投产时完全跳过丢弃逻辑,避免无谓的算力浪费。 新类 CNeuronDropoutOCL 直接继承 CNeuronBaseOCL,受保护区里只放四样东西:OutProbability(丢弃概率)、OutNumber(丢弃数量)、DropOutMultiplier(掩码向量指针)、dInitValue(初始化系数,理论值为 1/q)。PrevLayer 指针留着给测试期做缓冲区替换用——覆盖数据访问方法后,丢弃层在推理时会把上一层缓冲区直接顶替自己,下游层根本感知不到这层存在。 前馈时训练模式才动真格:先按 OutNumber 在 [0, UINT_MAX=4294967295] 整数序列里挑位置写 0,掩码用递增因子 1/q 预填以降低计算量;随后在 GPU 上用 OpenCL 内核拿 double4 向量做逐元素乘,线程数因此降到元素数的 1/4。反馈验算不重新生成掩码,而是复用前馈的 DropOutMultiplier,保证梯度分配和前向一致——这也是 dropout 导数等于递增系数的直接落地。 别在 Save 里存掩码。每个训练周期都会重生掩码,持久化的只有 OutProbability;Load 时再反推 OutNumber 和 dInitValue 并复原向量。基类调度、前馈、隐层梯度、权重更新四个挂钩少改一个,整个网络就可能静默算错。 附件里是完整类代码,开 MT5 把宏替换和 CLayer::CreateElement 的层创建分支加上,就能在自有模型里插一层丢弃层做对照训练。外汇与贵金属模型训练涉及杠杆与跳空,属高风险,丢弃率 q 建议从小值起步验证过拟合改善幅度。

MQL5 / C++
class CNeuronBaseOCL      :  class="kw">public CObject
  {
class="kw">protected:
   class="type">bool                bTrain;                class=class="str">"cmt">///< Training Mode Flag
   class="kw">virtual class="type">void       TrainMode(class="type">bool flag)                {  bTrain=flag;                }<class=class="str">"cmt">///< Set Training Mode Flag   
   class="kw">virtual class="type">bool       TrainMode(class="type">void)                      {  class="kw">return bTrain;                }<class=class="str">"cmt">///< Get Training Mode Flag   
class CNeuronDropoutOCL    :  class="kw">public   CNeuronBaseOCL
  {
class="kw">protected:
   CNeuronBaseOCL     *PrevLayer;
   class="type">class="kw">double             OutProbability;
   class="type">class="kw">double             OutNumber;
   CBufferDouble      *DropOutMultiplier;
   class="type">class="kw">double             dInitValue;
class=class="str">"cmt">//---

◍ Dropout 层的缓冲区与随机屏蔽实现

CNeuronDropoutOCL 在训练态(bTrain=true)与推理态走两套数据通道:训练时直接暴露本层 Output / Gradient 缓冲,推理时则转发到前一层 PrevLayer 的对应缓冲,避免丢弃神经元干扰实盘信号。 RND() 方法用 xor128 伪随机并映射到层内神经元序号:返回 (int)((Neurons()-1)/UINT_MAX * rnd_w),相当于按 rnd_w 控制的随机位置挑一个神经元做「关闭」,这是 dropout 正则化的随机掩码来源。 Init() 接收 out_prob 作为神经元失活概率,以及 numNeurons、open_cl 指针等;在 MT5 里调小时把 out_prob 从 0.5 降到 0.2 可能让过拟合减轻但训练收敛变慢,贵金属与外汇模型均属高风险实验,需自行回测验证。 feedForward 调用 OpenCL 内核做前向传播,updateInputWeights 默认返回 true 不更新权重——说明该层仅做随机屏蔽,不引入可训练参数。

MQL5 / C++
  class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL);                                        class=class="str">"cmt">///\brief Feed Forward method of calling kernel ::FeedForward().@param NeuronOCL Pointer to previous layer.
  class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL) {class="kw">return true;}                    class=class="str">"cmt">///< Method for updating weights.@param NeuronOCL Pointer to previous layer.
class=class="str">"cmt">//---
  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=class="str">"cmt">///< Generates a random neuron position to turn off
class="kw">public:
                    CNeuronDropoutOCL(class="type">void);
                    ~CNeuronDropoutOCL(class="type">void);
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 numNeurons,class="type">class="kw">double out_prob, ENUM_OPTIMIZATION optimization_type);
  class=class="str">"cmt">///< Method of initialization class.@param[in] numOutputs Number of connections to next layer.@param[in] myIndex Index of neuron in layer.@param[in] open_cl Pointer to class="macro">#COpenCLMy object. class="macro">#param[in] numNeurons Number of neurons in layer class="macro">#param[in] out_prob Probability of neurons shutdown @param optimization_type Optimization type(class="macro">#ENUM_OPTIMIZATION)@class="kw">return Boolen result of operations.
class=class="str">"cmt">//---
  class="kw">virtual class="type">int         getOutputIndex(class="type">void)            {  class="kw">return (bTrain ? Output.GetIndex() : PrevLayer.getOutputIndex());             }  class=class="str">"cmt">///< Get index of output buffer @class="kw">return Index
  class="kw">virtual class="type">int         getGradientIndex(class="type">void)          {  class="kw">return (bTrain ? Gradient.GetIndex() : PrevLayer.getGradientIndex());      }  class=class="str">"cmt">///< Get index of gradient buffer @class="kw">return Index
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int         getOutputVal(class="type">class="kw">double &values[])   {  class="kw">return (bTrain ? Output.GetData(values) : PrevLayer.getOutputVal(values)); }  class=class="str">"cmt">///< Get values of output buffer @param[out] values Array of data @class="kw">return number of items
  class="kw">virtual class="type">int         getOutputVal(CArrayDouble *values)   {  class="kw">return (bTrain ? Output.GetData(values) : PrevLayer.getOutputVal(values)); }  class=class="str">"cmt">///< Get values of output buffer @param[out] values Array of data @class="kw">return number of items
  class="kw">virtual class="type">int         getGradient(class="type">class="kw">double &values[])     {  class="kw">return (bTrain ? Gradient.GetData(values) : PrevLayer.getGradient(values));   }  class=class="str">"cmt">///< Get values of gradient buffer @param[out] values Array of data @class="kw">return number of items
  class="kw">virtual CBufferDouble  *getOutput(class="type">void)                {  class="kw">return (bTrain ? Output : PrevLayer.getOutput());      }                class=class="str">"cmt">///< Get pointer of output buffer @class="kw">return Pointer to object
  class="kw">virtual CBufferDouble  *getGradient(class="type">void)              {  class="kw">return (bTrain ? Gradient : PrevLayer.getGradient());  }                class=class="str">"cmt">///< Get pointer of gradient buffer @class="kw">return Pointer to object
class=class="str">"cmt">//---
把模型诊断交给小布盯盘
这些关于训练卡顿和特征协同适应的诊断思路,小布盯盘的 AIGC 已内置,打开对应品种页即可看到模型收敛状态的提示,你只需判断要不要介入。

常见问题

原文按 2012 年论文思路以给定概率 p 随机排除神经元,常用区间在 0.2~0.5,但具体要视模型规模和过拟合程度调整,没有固定最优值。
训练时随机丢神经元是为打破特征检测器协同适应;操作时评估所有特征和神经元,才能得到当前环境状态最准确独立的输出。
小布盯盘内置了模型收敛诊断的 AIGC 模块,可提示训练是否陷入局部最小值,但训练脚本仍需在 MT5 环境自行部署,小布负责看盘口和状态预警。
用含正态分布的伪随机数生成器判定排除列表,能最大程度统一排除神经元,避免某些神经元被长期忽略或长期保留。
排除特征令激活函数输入量下降,乘以 1/q(q=1-p)可拉升留存特征值,因 q 在 0~1 之间,该系数恒大于 1,保持期望输入规模不变。