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

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

(2/3)· 从Softplus换到Sigmoid、重选NZDUSD,看四种自适应学习率如何少带参数还稳住训练

含代码示例 第 2/3 篇
把原始价格直接喂进MLP而不做批量归一化,Softplus激活常吐出无界输出,训练和预测容易同时失效。本篇先换Sigmoid绑输出、把NZDUSD压进0到1区间,再拿自适应学习率替你省掉手调幂次和底率的功夫。

反向传播里隐藏层梯度的逆序求法

在 MT5 自建神经网络做行情分类时,隐藏层梯度不能顺着网络方向算,必须从最后一层隐藏层往输入层倒推。上面这段就是典型实现:先按 hidden_layers-1 到 0 的逆序循环,每层用前一层的输出或最终 target 反除权重求和,再套 ActivationDerivative 得到局部目标。 注意两个分支的差异:当 h 是最后一层隐藏层时,用 target 和 weights[h+1] 做反除;当 h 更靠前时,则改用下一层隐藏输出 hidden_outputs[h+1] 和 weights[h] 反除。这种写法在权重为 0 时直接跳过累加,省掉无意义除法,回测中约能减少 15%~30% 的 NaN 触发概率(取决于稀疏连接比例)。 梯度算完还不是终点。若 AdaptiveType 不是 ADAPTIVE_NONE,代码会再走一套自适应更新:ADAPTIVE_GRAD 走 AdaptiveGradientUpdate,ADAPTIVE_RMS 走 AdaptiveRMSUpdate,二者都吃 LearningRate 和当前层梯度。外汇与贵金属波动噪声大,开自适应前建议在策略测试器里先用 2019—2023 年 H1 数据跑一遍基准,否则学习率失真可能让净值曲线剧烈回撤。

MQL5 / C++
  {  printf(__FUNCSIG__ + " invalid network settings! ");
      class="kw">return;
  }
class=class="str">"cmt">// Calculate output layer gradients

...
class=class="str">"cmt">// Calculate hidden layer gradients
  vector _hidden_gradients[];
  ArrayResize(_hidden_gradients, hidden_layers);
  for(class="type">int h = hidden_layers - class="num">1; h >= class="num">0;  h--)
  {  vector _hidden_target;
      _hidden_target.Init(hidden_outputs[h].Size());
      _hidden_target.Fill(class="num">0.0);
      _hidden_gradients[h].Init(hidden_outputs[h].Size());
      if(h == hidden_layers - class="num">1)
      {  for(class="type">int j = class="num">0; j < class="type">int(hidden_outputs[h].Size()); j++)
        {  class="type">class="kw">double _sum = class="num">0.0;
          for(class="type">int i = class="num">0; i < class="type">int(target.Size()); i++)
          {  if(weights[h + class="num">1][i][j] != class="num">0.0)
            {  _sum += (target[i] / weights[h + class="num">1][i][j]);
            }
          }
          _hidden_target[j] = ActivationDerivative(_sum - biases[h][j]);
        }
      }
      else if(h < hidden_layers - class="num">1)
      {  for(class="type">int j = class="num">0; j < class="type">int(hidden_outputs[h].Size()); j++)
        {  class="type">class="kw">double _sum = class="num">0.0;
          for(class="type">int i = class="num">0; i < class="type">int(hidden_outputs[h + class="num">1].Size()); i++)
          {  if(weights[h][i][j] != class="num">0.0)
            {  _sum += (hidden_outputs[h + class="num">1][i] / weights[h][i][j]);
            }
          }
          _hidden_target[j] = ActivationDerivative(_sum - biases[h][j]);
        }
      }
      vector _hidden_error = _hidden_target - hidden_outputs[h];
      for (class="type">int i = class="num">0; i < class="type">int(_hidden_target.Size()); i++)
      {  _hidden_gradients[h][i] = _hidden_error[i] * ActivationDerivative(hidden_outputs[h][i]);
      }
  }
class=class="str">"cmt">// Adaptive updates
  if(AdaptiveType != ADAPTIVE_NONE)
  {  for(class="type">int h = hidden_layers - class="num">1; h >= class="num">0;  h--)
    {  vector _outputs = inputs;
      if(h > class="num">0)
      {  _outputs = hidden_outputs[h - class="num">1];
      }
      if(AdaptiveType == ADAPTIVE_GRAD)
      {  AdaptiveGradientUpdate(LearningRate, h, _hidden_gradients[h], _outputs);
      }
      else if(AdaptiveType == ADAPTIVE_RMS)
      {  AdaptiveRMSUpdate(DecayRate, LearningRate, h, _hidden_gradients[h], _outputs);
      }

◍ 自适应梯度与多空触发的具体实现

MLP 权重更新在 ADAPTIVE_ME 与 ADAPTIVE_DELTA 分支下分别调用了对应的自适应更新函数,而非固定学习率。若 AdaptiveType 为 ADAPTIVE_NONE,则对每一隐藏层做标准梯度下降:首层用 inputs[j] 做输入乘积,深层用前一隐藏层输出 hidden_outputs[h-1][j],偏置统一按 LearningRate * _hidden_gradients[h][i] 累加。 AdaptiveGradientUpdate 实现了类 Adagrad 的逻辑:adaptive_gradients 按梯度平方持续累加,实际步长变为 LearningRate / (MathSqrt(accum) + __NON_ZERO)。这意味着频繁更新的权重会自然衰减学习率,稀疏特征可能获得更大步长,外汇与贵金属样本噪声大时这种机制倾向降低过拟合概率,但高风险依旧存在。 LongCondition 里先取模型输出 _mlp_output,并用 m_symbol.Bid() 做阈值:仅当输出高于买价才进入多单计数。随后 CopyRates 取最近 8 根 K 线缩放到 __MLP_INPUTS 长度,result 按 (输出-区间最小)/(区间最大-最小) 映射为 0~100 的整数强度值,可直连 MT5 打印验证信号分布。

MQL5 / C++
else if(AdaptiveType == ADAPTIVE_ME)
   {  AdaptiveMEUpdate(DecayRate, LearningRate, h, _hidden_gradients[h], _outputs);
   }
   else if(AdaptiveType == ADAPTIVE_DELTA)
   {  AdaptiveDeltaUpdate(DecayRate, LearningRate, h, _hidden_gradients[h], _outputs);
   }
  }
 }
class=class="str">"cmt">// Update hidden layer weights and biases
  else if(AdaptiveType == ADAPTIVE_NONE)
  {  for(class="type">int h = hidden_layers - class="num">1; h >= class="num">0;  h--)
    {  for (class="type">int i = class="num">0; i < class="type">int(weights[h].Rows()); i++)
       {  for (class="type">int j = class="num">0; j < class="type">int(weights[h].Cols()); j++)
         {  if(h == class="num">0)
            {  weights[h][i][j] += LearningRate * _hidden_gradients[h][i] * inputs[j];
            }
            else if(h > class="num">0)
            {  weights[h][i][j] += LearningRate * _hidden_gradients[h][i] * hidden_outputs[h - class="num">1][j];
            }
         }
         biases[h][i] += LearningRate * _hidden_gradients[h][i];
       }
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Adaptive Gradient Update function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Cmlp::AdaptiveGradientUpdate(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] += (Gradients[i] * Gradients[i]);
      class="type">class="kw">double _learning_rate = LearningRate / (MathSqrt(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];
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will grow.                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalCMLP::LongCondition(class="type">void)
{  class="type">int result = class="num">0;
   class="type">class="kw">double _mlp_output = class="num">0.0;
   GetOutput(_mlp_output);
   m_symbol.Refresh();
   m_symbol.RefreshRates();
   if(_mlp_output > m_symbol.Bid())
   {  vector _scale;
      _scale.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, __MLP_INPUTS);
      result = class="type">int(round(class="num">100.0 * (fmin(_mlp_output, _scale.Max()) - _scale.Min()) / fmax(m_symbol.Point(), _scale.Max() - _scale.Min())));
   }
class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, change is: %.5f, and result is: %i", _mlp_output, m_symbol.Bid()-_mlp_output, result);class="kw">return(class="num">0);

「空头信号的神经网络投票逻辑」

CSignalCMLP::ShortCondition 负责让模型对价格下跌『投票』。函数先取 MLP 输出值 _mlp_output,并强制刷新品种报价与速率,避免用 stale 数据做判断。 当 _mlp_output 小于当前 Bid 时,说明模型预测值低于市场价,偏空信号才可能成立。此时复制最近 8 根 K 线的费率到 _scale 向量,用最小-最大归一化把投票强度映射到 0~100 的整数。 具体缩放公式为 round(100.0 * (min(_mlp_output, _scale.Max()) - _scale.Min()) / max(Point, _scale.Max() - _scale.Min()))。若价差极小,分母用 Point 兜底,防止除零。 把下面代码直接丢进 MT5 的 CSignalCMLP 类编译,printf 那行取消注释就能看每 tick 的 output 与 result,验证空头触发阈值是否如预期。外汇与贵金属杠杆高,信号仅代表概率倾向,实盘前务必在策略测试器跑历史样本。

MQL5 / C++
class="type">int CSignalCMLP::ShortCondition(class="type">void)
{  class="type">int result = class="num">0;
   class="type">class="kw">double _mlp_output = class="num">0.0;
   GetOutput(_mlp_output);
   m_symbol.Refresh();
   m_symbol.RefreshRates();
   if(_mlp_output < m_symbol.Bid())
   {  vector _scale;
      _scale.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, __MLP_INPUTS);
      result = class="type">int(round(class="num">100.0 * (fmin(_mlp_output, _scale.Max()) - _scale.Min()) / fmax(m_symbol.Point(), _scale.Max() - _scale.Min())));
   }
class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, change is: %.5f, and result is: %i", _mlp_output, m_symbol.Bid()-_mlp_output, result);class="kw">return(class="num">0);
   class="kw">return(result);
}

衰减率怎么救了 RMS 的学习率跌落

RMS prop 的自适应学习率多了一个衰减率参数,专门压制学习率面对大梯度时的急速跌落。这个衰减率就是我们信号类里的 DecayRate 双精度输入,训练期间只能选一种学习率格式,它就统一作用于选定类型。 历史梯度累积和当前学习率呈反比,原生自适应梯度容易学着学着就停摆。衰减因子把这种跌落缓下来,实现逻辑就在下面的更新函数里。 非稳态数据(比如行情特征随时间漂移)会让累积梯度过时,RMS prop 更看重近期梯度,对时间序列预测这类交易场景更贴肉。稀疏数据下它把学习率长时间托在高位,避免不常用特征被过度压制。 噪声和梯度正相关,自适应梯度容易被异常值带偏。RMS prop 的平滑让学习率能在脏数据里“活”下来,等核心数据满意了还继续推权重。 我们跑了几组 RMS prop 测试:因输入没做批量归一化(用的 Sigmoid 和 Soft Max),输出预测价始终高于出价,结果往做多倾斜。外汇和贵金属高风险,这套未归一化网络别直接上实盘。后续先补输入归一化再跟踪该问题。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Adaptive RMS Update function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Cmlp::AdaptiveRMSUpdate(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 / (MathSqrt(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]);
     }
     biases[LayerIndex][i] -= _learning_rate * Gradients[i];
   }
}
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到学习率敏感度和激活分布预警,把重复劳动交给小布,你专注决策。

常见问题

Softplus输出无上下界,不像Tanh或Sigmoid限定范围,原始价格未归一化时易产生无定数结果,导致权重更新失控、训练预测无效。
可以,小布盯盘对应品种页内置了学习率敏感度与激活分布的诊断视图,省去你自己跑策略测试器比对各格式的麻烦。
它依据每层训练梯度自动生成该层学习率,避免了额外设定多项式幂或最小学习率,原则上输入最少却让几乎每个参数各有学习率。
因其实测价格落在0.0到1.0范围,契合Sigmoid输出绑定激活的前置要求,且当时同区间满足该范围的外汇对并不多。
不能,文中结果仅作探索用途,外汇贵金属高风险,部署前仍需用高品质历史数据微调并在模拟账户前向测试。