神经网络舍弃机制:用随机排除神经元打破协同适应困局(基础篇)
(1/3)· 训练总卡在局部最小值?2012 年提出的舍弃法靠随机丢神经元逼出更稳的收敛
把冗余维度从特征里砍掉
做 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 自己跑一遍才知你的品种吃不吃这套。
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 建议从小值起步验证过拟合改善幅度。
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 不更新权重——说明该层仅做随机屏蔽,不引入可训练参数。
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">//---