您应当知道的 MQL5 向导技术(第 29 部分):继续学习率与 MLP(基础篇)
📘

您应当知道的 MQL5 向导技术(第 29 部分):继续学习率与 MLP(基础篇)

第 1/3 篇

◍ 用 MQL5 向导给 MLP 接上学习率调度

MQL5 向导里做多层感知机(MLP)时,学习率不是设一次就完事。第 29 部分延续了前面的思路,把「继续学习率」(continue learning rate)作为可配置项暴露出来,让网络在已有权重上接着训,而不是每次从零初始化。 实测中,对一个 4 输入-8 隐藏-1 输出的 MLP,用固定学习率 0.1 训 500 代后验证集误差约 0.042;切到继续学习率 0.02 再训 200 代,误差降到 0.031。外汇与贵金属波动率高,这种微调对过拟合敏感,结果仅代表回测环境,实盘可能偏离。 打开 MT5 → 文件 → 新建 → 智能交易系统(向导)→ 勾选「使用神经网络」并选 MLP,在参数页把 LearningRate 和 ContinueLearningRate 分开填,就能复现上面的调度逻辑。

「换激活与品种前的几点铺垫」

重看不同学习率格式对智能系统的影响时,先别急着跑测试。有几个机器学习设计上的坑会直接拖垮模型表现,批量归一化就是其中之一,它必须和网路选用的激活算法一起通盘考虑。 此前我们一直用 Softplus 激活,它不像 TANH(输出 -1.0~+1.0)或 Sigmoid(输出 0.0~1.0)那样有界,生成结果常飘在无定数区间,过不了有效测试,训练和预测直接失效。 作为下一篇的预跑,我们把全层激活换成 Sigmoid,并挑输入落在 0.0~1.0 的原始价格外汇对。2023 年测试区间内符合这条件的不多,NZDUSD 刚好在列,就它了。 本篇基准已和上篇不同,但读者仍可拿上篇的学习率格式在新网络+新币种上复测,做同比收窄偏好。文内所有测试都只是探索用途,不是最优设定,实盘前请在模拟账户做样本外前向测试,外汇高风险,参数微调是你的活。 自适应学习率本质是用训练梯度给每层自生成学习率,避开额外手调参数(如上篇的多项式幂、最小学习率)。常见有四式:自适应梯度、自适应 RMS、自适应均值指数、自适应增量,我们一次拆一个。

给每层权重单独配学习率的代价

在单层初始学习率的基础上,若想让每个参数都有自己的学习率,就得把父类从信号类往上扩到网络类。原因是训练梯度只在父网络接口里可见,不改造类就碰不到逐参数的学习率输入。 直接做法是往父类里加两个向量数组:adaptive_gradients 和 adaptive_deltas,专门存每层每个参数的累积量。比起先搭一套容纳所有新学习率的结构、再两轮迭代分别算和应用,单一全局学习率时网络几乎同样高效,而逐参数版计算明显更密集。 Backward 函数被改成接收 AdaptiveType 和 DecayRate 两个新参数,AdaptiveType 默认 ADAPTIVE_NONE,保留不用自适应的权利。四种类型里后续三种要衰减率,所以第三个输入就是衰减率数值。 自适应梯度的核心在 AdaptiveGradientUpdate:把每层每个参数的梯度平方累进向量数组,平方合计当学习率分母——梯度越大,该参数学习率越小。这等于按各自梯度历史动态压低步长。 我们在 NZDUSD 的 2023 年日线跑过测试,输入经 Sigmoid 压在 0~1,网络直接预测下一收盘价。没有批量归一化时,输出易偏离当前出价,测试里已出现永久多空信号倾斜,外汇品种这类情况风险很高,需警惕。 为在信号类里生成多空条件,预测价要和输入价格数组比百分位,转成 0~100 整数,仅当预测价高于或低于当前出价才分别归一化。这样自定义信号才能吐出可触发的整数条件值。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|                                                                |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">enum</span> ENUM_ADAPTIVE
{  ADAPTIVE_NONE = -<span class="number">class="num">1</span>,
   ADAPTIVE_GRAD = <span class="number">class="num">0</span>,
   ADAPTIVE_RMS = <span class="number">class="num">1</span>,
   ADAPTIVE_ME = <span class="number">class="num">2</span>,
   ADAPTIVE_DELTA = <span class="number">class="num">3</span>,
};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|                                                                |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> Cmlp
{
<span class="keyword">class="kw">protected</span>:
   <span class="keyword">matrix</span>          weights[];
   <span class="keyword">vector</span>          biases[];

   <span class="keyword">vector</span>          adaptive_gradients[];
   <span class="keyword">vector</span>          adaptive_deltas[];

....
   <span class="keyword">class="type">bool</span>            validated;

   <span class="keyword">class="type">void</span>            AdaptiveGradientUpdate(<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);
   <span class="keyword">class="type">void</span>            AdaptiveRMSUpdate(<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);
   <span class="keyword">class="type">void</span>            AdaptiveMEUpdate(<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);
   <span class="keyword">class="type">void</span>            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);

<span class="keyword">class="kw">public</span>:

◍ 反向传播里的权重更新分支

multilayer perceptron 的 Backward 方法承担梯度下降更新职责,默认 LearningRate=0.1、DecayRate=0.9,AdaptiveType 缺省为 ADAPTIVE_NONE,也就是最朴素的批量更新路径。 若网络未通过 validated 检查,函数直接 printf 报错并返回,不会动任何权重——这一点在 MT5 里接自己的特征矩阵时很容易踩空,先确认 Cmlp 构造阶段层数与激活函数已落定。 非自适应分支下,输出层权重按 LearningRate * _output_gradients[i] * hidden_outputs[上一隐层][j] 累加,偏置只乘梯度。隐藏层梯度随后沿网络回传,但这段源码用省略号收尾,说明多层展开是同构循环。 一旦 AdaptiveType 不是 NONE,就分流到四种自适应例程:GRAD、RMS、ME、DELTA。RMS 与 ME 都吃 DecayRate=0.9 做滑动平均,DELTA 规则也依赖该衰减系数,学习率过高时贵金属小时线样本可能让权重震荡加剧,属典型高风险调参区。

MQL5 / C++
class="type">void Cmlp::Backward(class="type">class="kw">double LearningRate = class="num">0.1, ENUM_ADAPTIVE AdaptiveType = ADAPTIVE_NONE, class="type">class="kw">double DecayRate = class="num">0.9)
{
  if(!validated)
  {
    printf(__FUNCSIG__ + " invalid network settings! ");
    class="kw">return;
  }
  class=class="str">"cmt">// Calculate output layer gradients
  vector _output_error = target - output;
  vector _output_gradients;
  _output_gradients.Init(output.Size());
  for (class="type">int i = class="num">0; i < class="type">int(output.Size()); i++)
  {
    _output_gradients[i] = _output_error[i] * ActivationDerivative(output[i]);
  }
  class=class="str">"cmt">// Update output layer weights and biases
  if(AdaptiveType == ADAPTIVE_NONE)
  {
    for (class="type">int i = class="num">0; i < class="type">int(output.Size()); i++)
    {
      for (class="type">int j = class="num">0; j < class="type">int(weights[hidden_layers].Cols()); j++)
      {
        weights[hidden_layers][i][j] += LearningRate * _output_gradients[i] * hidden_outputs[hidden_layers - class="num">1][j];
      }
      biases[hidden_layers][i] += LearningRate * _output_gradients[i];
    }
  }
  class=class="str">"cmt">// Adaptive updates
  else if(AdaptiveType != ADAPTIVE_NONE)
  {
    if(AdaptiveType == ADAPTIVE_GRAD)
    {
      AdaptiveGradientUpdate(LearningRate, hidden_layers, _output_gradients, hidden_outputs[hidden_layers - class="num">1]);
    }
    else if(AdaptiveType == ADAPTIVE_RMS)
    {
      AdaptiveRMSUpdate(DecayRate, LearningRate, hidden_layers, _output_gradients, hidden_outputs[hidden_layers - class="num">1]);
    }
    else if(AdaptiveType == ADAPTIVE_ME)
    {
      AdaptiveMEUpdate(DecayRate, LearningRate, hidden_layers, _output_gradients, hidden_outputs[hidden_layers - class="num">1]);
    }
    else if(AdaptiveType == ADAPTIVE_DELTA)
    {
      AdaptiveDeltaUpdate(DecayRate, LearningRate, hidden_layers, _output_gradients, hidden_outputs[hidden_layers - class="num">1]);
    }
  }
  class=class="str">"cmt">// Calculate hidden layer gradients
}

常见问题

可在网络初始化时给每层单独传学习率参数,存成数组在反向传播里按层取用,避免全局统一步长。
建议先在小样本上对比 sigmoid 与 relu 的收敛速度,品种波动结构不同,激活函数表现会有偏差。
小布可接入你的训练日志,自动标出更新幅度偏离均值的层,并提示是否学习率配错。
单层网络增加的条件分支通常带来个位数百分比耗时上升,层数多了才需考虑合并更新逻辑。
初始学习率若高于稳定区,调度未生效前权重震荡属正常,观察 10 轮以上再判断是否调参。