您应当知道的 MQL5 向导技术(第 29 部分):继续学习率与 MLP·综合运用
📘

您应当知道的 MQL5 向导技术(第 29 部分):继续学习率与 MLP·综合运用

第 3/3 篇

「动量加权如何让学习率更跟手」

ADAM 在 RMS prop 只盯梯度平方均值的基础上,额外引入梯度的一阶动量(均值)与二阶动量(方差)双轨修正,使学习率更新在高维金融序列里更平滑。当输入维度上升,梯度数组参数量同步膨胀,单纯靠平方均值容易在噪声中收敛迟缓,而动量项能让权重更新对数据分布偏移更敏感。 实测中,用 5-8-1 的三层 MLP 跑非理想设定,ADAM 相较 RMS prop 在嘈杂行情与特征突变时过渡更稳。例如房地产定价模型突遇地铁规划落地,原目标函数失效,ADAM 因权重更新内含动量,添加新特征后网络修订代价更低,MSE 随权重迭代下探更连贯。 下面这段 MT5 下的自适应均值指数更新函数,是上述逻辑的直接落点。逐行看:衰减率先平滑历史梯度平方,学习率按非零保护后的梯度均方动态缩放,再分别对权重矩阵与偏置做减法更新。 //+------------------------------------------------------------------+ // Adaptive Mean Exponential(ME) Update function //+------------------------------------------------------------------+ void Cmlp::AdaptiveMEUpdate(double DecayRate, double LearningRate, int LayerIndex, vector &Gradients, vector &Outputs) { for (int i = 0; i < int(weights[LayerIndex].Rows()); i++) { adaptive_gradients[LayerIndex][i] = (DecayRate * adaptive_gradients[LayerIndex][i]) + ((1.0 - DecayRate)*(Gradients[i] * Gradients[i])); double _learning_rate = LearningRate / (fabs(adaptive_gradients[LayerIndex][i]) + __NON_ZERO); for (int j = 0; j < int(weights[LayerIndex].Cols()); j++) { weights[LayerIndex][i][j] -= (_learning_rate * Gradients[i] * Outputs[j]); } // biases[LayerIndex][i] -= _learning_rate * Gradients[i]; } } 把 DecayRate 从 0.9 调到 0.95 在 MT5 里复跑,能直观看到动量记忆拉长后,权重曲线对近期跳空的响应倾向变钝,外汇与贵金属建模属高风险,参数敏感需自行回测验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Adaptive Mean Exponential(ME) Update function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Cmlp::AdaptiveMEUpdate(class="type">class="kw">double DecayRate, class="type">class="kw">double LearningRate, class="type">int LayerIndex, vector &Gradients, vector &Outputs)
{  for (class="type">int i = class="num">0; i < class="type">int(weights[LayerIndex].Rows()); i++)
   { adaptive_gradients[LayerIndex][i] = (DecayRate * adaptive_gradients[LayerIndex][i]) + ((class="num">1.0 - DecayRate)*(Gradients[i] * Gradients[i]));
     class="type">class="kw">double _learning_rate = LearningRate / (fabs(adaptive_gradients[LayerIndex][i]) + __NON_ZERO);
     for (class="type">int j = class="num">0; j < class="type">int(weights[LayerIndex].Cols()); j++)
     { weights[LayerIndex][i][j] -= (_learning_rate * Gradients[i] * Outputs[j]);
     }
     class=class="str">"cmt">//
     biases[LayerIndex][i] -= _learning_rate * Gradients[i];
   }
}

ADADELTA 的增量缓冲如何压住超调

ADADELTA 不引入新输入参数,却在训练时靠平方梯度的衰减累积加上平方权重更新的衰减累积来动态调整步长。相比 ADAM 只盯历史梯度均值与方差,它在嘈杂数据上倾向于减少越过最小值导致的震荡,因为多了一层“自适应增量”缓冲。 这层缓冲持续跟踪每次平方更新,让学习率同时参考近期梯度量级与近期更新的实际有效性。若梯度量级明显衰退,更新不会自动变得过于保守,步长会被最近更新的成效拉回合理区间。 额外累积的权重更新还提供了一种归一化机制:用最近更新尺度做分母,使优化器适配梯度场景变化的能力提升,对非稳态分布或梯度高度可变的行情样本尤为关键。我们在同品种、同周期、同时间帧下跑过 ADADELTA 学习率测试,得到了若干可盈利结果,说明其降低超参数敏感性与避免学习率衰退的特质在实盘回测中并非空谈。外汇与贵金属波动剧烈,此类模型仍属高风险,结果仅代表历史样本概率。 下面这段 MT5 类成员函数给出了单层的增量更新逻辑,可直接拷进你的 MLP 框架改参验证: //+------------------------------------------------------------------+ // Adaptive Delta Update function //+------------------------------------------------------------------+ void Cmlp::AdaptiveDeltaUpdate(double DecayRate, double LearningRate, int LayerIndex, vector &Gradients, vector &Outputs) { for (int i = 0; i < int(weights[LayerIndex].Rows()); i++) { adaptive_gradients[LayerIndex][i] = (DecayRate * adaptive_gradients[LayerIndex][i]) + ((1.0 - DecayRate)*(Gradients[i] * Gradients[i])); double _delta = (MathSqrt(adaptive_deltas[LayerIndex][i] + __NON_ZERO) / MathSqrt(adaptive_gradients[LayerIndex][i] + __NON_ZERO)) * Gradients[i]; adaptive_deltas[LayerIndex][i] = (DecayRate * adaptive_deltas[LayerIndex][i]) + ((1.0 - DecayRate) * _delta * _delta); for (int j = 0; j < int(weights[LayerIndex].Cols()); j++) { weights[LayerIndex][i][j] -= (_delta * Outputs[j]); } // Bias update with AdaDelta biases[LayerIndex][i] -= _delta; } } 逐行拆解:函数头声明衰减率、学习率、层索引与梯度、输出引用;循环遍历该层权重行数,先按衰减率混合同历史平方梯度更新 adaptive_gradients。_delta 用历史增量均方根比梯度均方根再乘梯度,构成自适应步长;随后用同样衰减率累积平方增量进 adaptive_deltas。内层循环拿 _delta 乘输出改权重,末尾偏置也减 _delta,完成一层 AdaDelta 更新。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">// Adaptive Delta Update function</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> Cmlp::AdaptiveDeltaUpdate(<span class="keyword">class="type">class="kw">double</span> DecayRate, <span class="keyword">class="type">class="kw">double</span> LearningRate, <span class="keyword">class="type">int</span> LayerIndex, <span class="keyword">vector</span> &amp;Gradients, <span class="keyword">vector</span> &amp;Outputs)
{&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; <span class="keyword">class="type">int</span>(weights[LayerIndex].Rows()); i++)
&nbsp;&nbsp; {&nbsp;&nbsp;adaptive_gradients[LayerIndex][i] = (DecayRate * adaptive_gradients[LayerIndex][i]) + ((<span class="number">class="num">1.0</span> - DecayRate)*(Gradients[i] * Gradients[i]));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> _delta = (<span class="functions">MathSqrt</span>(adaptive_deltas[LayerIndex][i] + __NON_ZERO) / <span class="functions">MathSqrt</span>(adaptive_gradients[LayerIndex][i] + __NON_ZERO)) * Gradients[i];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;adaptive_deltas[LayerIndex][i] = (DecayRate * adaptive_deltas[LayerIndex][i]) + ((<span class="number">class="num">1.0</span> - DecayRate) * _delta * _delta);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> j = <span class="number">class="num">0</span>; j &lt; <span class="keyword">class="type">int</span>(weights[LayerIndex].Cols()); j++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;weights[LayerIndex][i][j] -= (_delta * Outputs[j]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Bias update with AdaDelta</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;biases[LayerIndex][i] -= _delta;
&nbsp;&nbsp; }
}

◍ 一次轮转里的学习率反向走位

这种调度比余弦退火更直白:学习率不在全程单调递减,而是先暖身爬升到峰值,再在训练后半段冷却回落。多数常规策略喜欢开局给大学习率、随样本累积往下压,一次轮转偏偏反着来,从最低值起步。 最小和最大学习率都是预先敲定的输入参数,整条变化曲线的陡缓由总轮次(训练区间长度)直接牵着走。我们照自适应那套测试框架跑了一遍,在不带批量归一化、仅对多空条件做归一化的原生品种预测里,光靠调学习率就开出了若干做空单。 这再次说明神经网络对 learning rate 的敏感程度——外汇与贵金属杠杆高、滑点跳空频繁,这类实验结论只代表回测概率倾向,实盘须自行验证。 下面这段是 MT5 里一次轮转分支的裸实现,建议直接丢进 EA 的 epoch 循环里对照改参。

MQL5 / C++
      else if(m_learning_type == LEARNING_ONE_CYCLE)
      {  class="type">class="kw">double _cycle_position = (class="type">class="kw">double)((m_epochs - i) % (class="num">2 * m_epochs)) / (class="num">2.0 * m_epochs);
         if (_cycle_position <= class="num">0.5)
         {  _learning_rate = m_min_learning_rate + (class="num">2.0 * _cycle_position * (m_learning_rate - m_min_learning_rate));
         }
         else
         {  _learning_rate = m_learning_rate - (class="num">2.0 * (_cycle_position - class="num">0.5) * (m_learning_rate - m_min_learning_rate));
         }
      }

「画得少,看得清」

学习率作为机器学习模型训练里最敏感的参数之一,调得好坏直接左右收敛稳定性。前面几节我们把常数、阶跃、余弦退火等多种格式都跑过一遍,验证顺序创新确实能改善早期笨办法的迟滞。 但回测里一次轮转这种骨架式方案也没掉队,在 mlp_learn_r.mq5 的 7.71 KB 主文件配合 Cmlp_ad.mqh(17.49 KB)跑样本时,简单轮转对过拟合的抑制倾向不弱于复杂曲线。 别只追花活,基础轮转在 MT5 里改一行周期就能复验;外汇与贵金属波动率高,任何学习率设定都只是概率优势,实盘前请用策略测试器过一遍多品种。 附件代码依赖 Expert.mqh、TrailingNone.mqh、MoneyFixedMargin.mqh,它们已在 MQL5 的 Include\Expert 目录下,缺了 EA 起不来,补齐即可编译。

MQL5 / C++
class="macro">#include <Expert\Expert.mqh>
class="macro">#include <Expert\Trailing\Expert.mqh>
class="macro">#include <Expert\Money\MoneyFixedMargin.mqh>

常见问题

动量加权让学习率更跟手,但尾段抖动通常是动量累积未衰减,可给动量系数加衰减或调小基础学习率验证。
增量缓冲用历史梯度平方的滑动均值归一化步长,窗口过小仍会超调,建议从 10~50 根 K 线窗口回测选稳的。
小布可接入你的训练日志,自动标出一次轮转里学习率反向的时段并推送提醒,你只看异常就行。
若反向伴随损失不降反升多为参数冲突,可用分段冻结法验证;若损失同步改善则可能是自适应策略生效。
只叠画学习率与损失两条主线,其余指标用颜色淡显,做到画得少看得清,便于定位拐点。