使用莱文贝格-马夸尔特(Levenberg-Marquardt,LM)算法训练多层感知器·进阶篇
🧠

使用莱文贝格-马夸尔特(Levenberg-Marquardt,LM)算法训练多层感知器·进阶篇

(2/3)· 当SGD在每根K线重训时拖慢节奏,LM用二阶信息把周期压到个位数

含代码示例偏理论 第 2/3 篇

很多交易者把SGD当万能锤,却在实时重训时发现损失面迟迟不收敛。LM这类吃二阶偏导的算法,本该是短线自适应网络的利器,MQL5里却长期缺位。本文接着第1篇的基础,把这块空白补上。

◍ 两层网络的建网与前向反向骨架

这段实现把最简单的回归类神经网络拆成两层:隐藏层 layer1 个节点,输出层 1 个节点,直接在 MT5 的 matrix 类上跑矩阵运算。CreateNet 里权重和偏置都用 Random(-0.1, 0.1) 初始化,意味着参数初值被限制在 ±0.1 的窄区间,对小样本外汇特征可能收敛快但也可能陷局部极小。 FeedForward 的链路很直白:n1 = weights1.MatMul(data.Transpose()) + bias1.MatMul(ones_) 算第一层预激活,过激活函数得到 act1;再 n2 = weights2.MatMul(act1) + bias2.MatMul(ones_) 得到输出层预激活并激活为 act2。注意 data 被转置后参与乘法,说明输入样本是按列排布的,拷代码前先核对自己的样本矩阵维度。 Backprop 开头先算 DerivLoss_wrt_y = act2.LossGradient(target.Transpose(), loss_func),再拿 n2.Derivative 得到输出层激活导数,两者逐元素乘出 D2 即输出层误差 delta。D1 的计算在原文截断处,但结构已清楚:用 weights2 的转置把 D2 传回隐藏层再乘 act1 的导数。外汇与贵金属波动高、噪声大,这种浅网 RMSE 若高于样本 sigma 则模型基本无预测倾向,开 MT5 把 PrintFormat 的 rmse 与 sigma_ 打出来对比即可验证。

MQL5 / C++
class="type">class="kw">double rmse=act2.RegressionMetric(target.Transpose(),REGRESSION_RMSE);
PrintFormat("rmse %.3f / sigma %.2f ",rmse,sigma_);
ArrayResize(NetOutput,Sample_);
for(class="type">int i=class="num">0; i< (class="type">int)act2.Cols(); i++)
  {
  NetOutput[i]  =act2.Transpose()[i,class="num">0];
  }
class=class="str">"cmt">//--- class="kw">return result
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Model creation method                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateNet()
  {
class=class="str">"cmt">//--- initialize weight matrices
  if(!weights1.Init(layer1,Features)  || !weights2.Init(class="num">1,layer1))
    class="kw">return false;
class=class="str">"cmt">//--- initialize offset matrices
  if(!bias1.Init(layer1,class="num">1)  || !bias2.Init(class="num">1,class="num">1))  
    class="kw">return false;
class=class="str">"cmt">//--- initialize the matrix of parameter increments
  dW1.Init(layer1,Features);
  dW2.Init(class="num">1, layer1);
  db1.Init(layer1,class="num">1);
  db2.Init(class="num">1,class="num">1);
  dW1.Fill(class="num">0);
  dW2.Fill(class="num">0);
  db1.Fill(class="num">0);
  db2.Fill(class="num">0);
class=class="str">"cmt">//--- fill the parameter matrices with random values
  weights1.Random(-class="num">0.1, class="num">0.1);
  weights2.Random(-class="num">0.1, class="num">0.1);
  bias1.Random(-class="num">0.1,class="num">0.1);
  bias2.Random(-class="num">0.1,class="num">0.1);
class=class="str">"cmt">//--- class="kw">return result
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Feed-forward method                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool FeedForward(matrix &data)
  {
class=class="str">"cmt">//--- calculate the first neural layer
class=class="str">"cmt">//--- n1 pre-activation of the first layer
  n1 = weights1.MatMul(data.Transpose()) + bias1.MatMul(ones_);
class=class="str">"cmt">//--- calculate the activation function of the act1 first layer
  n1.Activation(act1, ac_func);
class=class="str">"cmt">//--- calculate the second neural layer
class=class="str">"cmt">//--- n2 pre-activation of the second layer
  n2 = weights2.MatMul(act1) + bias2.MatMul(ones_);
class=class="str">"cmt">//--- calculate the activation function of the act2 second layer
  n2.Activation(act2, ac_func_last);
class=class="str">"cmt">//--- class="kw">return result
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Backpropagation method                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Backprop(matrix &data, matrix &target)
  {
class=class="str">"cmt">//--- Derivative of the loss function with respect to the predicted value
  matrix DerivLoss_wrt_y = act2.LossGradient(target.Transpose(),loss_func);
  matrix deriv_act2;
  n2.Derivative(deriv_act2, ac_func_last);
class=class="str">"cmt">//--- D2  
  matrix  D2 = deriv_act2*DerivLoss_wrt_y; class=class="str">"cmt">// error(delta) of the network output layer
class=class="str">"cmt">//--- D1
  matrix deriv_act1;
  n1.Derivative(deriv_act1, ac_func);

动量项怎么改写权重矩阵

反向传播算完各层误差后,真正动权重的是这段更新逻辑。它用的是带动量的梯度下降,gamma_ 是惯性系数,(1-gamma_) 那一项才是本次梯度的真实贡献,再乘 lr 学习率缩放步长。 D1 是第一层误差 delta,由权重矩阵 weights2 的转置乘 D2 再逐元素乘激活导数 deriv_act1 得到。dW1 和 db1 都混合了历史动量 dW1/db1 与当前梯度,避免单步更新被噪声带偏。 ones 矩阵只是用来对误差按样本数求和,D1.MatMul(ones) 等价于把 D1 每列加总,得到偏置梯度。外汇与贵金属行情噪声大,gamma_ 设 0.9 左右时历史动量占比高,权重轨迹更平滑,但过高会拖慢对新波动的响应,建议开 MT5 把 gamma_ 从 0.8 调到 0.95 各跑 200 代观察 loss 曲线差异。

MQL5 / C++
  matrix D1 = weights2.Transpose().MatMul(D2);
  D1 = D1*deriv_act1; class=class="str">"cmt">// error(delta) of the first layer of the network
class=class="str">"cmt">//--- update network parameters
  matrix  ones = matrix::Ones(data.Rows(),class="num">1);
  dW1 = gamma_*dW1 + (class="num">1-gamma_)*(D1.MatMul(data)) * lr;
  db1 = gamma_*db1 + (class="num">1-gamma_)*(D1.MatMul(ones)) * lr;
  dW2 = gamma_*dW2 + (class="num">1-gamma_)*(D2.MatMul(act1.Transpose())) * lr;
  db2 = gamma_*db2 + (class="num">1-gamma_)*(D2.MatMul(ones)) * lr;
  weights1 =  weights1 - dW1;
  weights2 =  weights2 - dW2;
  bias1     =  bias1      - db1;
  bias2     =  bias2      - db2;
class=class="str">"cmt">//--- class="kw">return result
  class="kw">return true;
  }

「用单样本迭代逼出收敛边界」

动量在中小样本上够用,但一旦训练集从 81 个点膨胀到成千上万,逐批全量算梯度就太重了。SGD 把梯度计算拆到极小批量上,甚至每次只取一个样本更新权重,再随机抽下一个点继续,直到收敛——这也是「随机」二字的来源。传统批量梯度下降必须扫完整个集才动一次权重,SGD 则是走一步看一步。 我们在实测里用了 batch_size = 1 的极简变体:每次只拿 81 个训练样本里的 1 个去更新参数向量。图例 6 的对数尺度损失曲线显示,算法在 4k–6k 次迭代内触到最小边界,折算成周期大约是 50–75 个 epoch 收敛。 同样挂了动量项,但因为每次只喂一个数据点,噪声把动量平滑效应冲得差不多了,对收敛速度几乎没有肉眼可见的拉动。外汇与贵金属建模用这类在线更新时,样本分布漂移快,过拟合风险高,实盘前务必在 MT5 用历史tick重跑确认。

◍ LM算法在MT5神经网络里的切换逻辑

莱文贝格-马夸尔特(LM)这套老算法在几百个参数的网络上配合L-BFGS用,效率往往很能打,但前提是你只能拿二次或均方根损失去喂它。它本质是在最小化非线性函数的平方和,所以别指望拿它去优化其他花哨损失。 从牛顿法出发,参数更新依赖逆海森乘梯度。LM的巧劲在于用雅可比矩阵乘积 J'J 近似海森,再往上面加 mu*I 保证可逆:mu 取大正值就退化成普通梯度下降,mu 趋近0就回到高斯-牛顿。训练一般从较小 mu(如代码里 Initial_mu=0.001)起步,损失不降就乘 Incr_Rate(默认10)加大 mu,降了就乘 Decr_Rate(默认0.1)减小 mu,在两种方法间来回切。 反向传播上,因为雅可比存的是误差的一阶偏导而非平方,最后一层 delta 直接等于激活函数导数,不用再乘 (y-target)。脚本停止条件看梯度范数小于 Min_grad(1e-6)、损失达标、mu 越界或跑满 Epochs(默认10)。外汇与贵金属行情高波动,拿这套练网络请先在历史数据回测,实盘有滑点和断点风险。 下面这段 MQ5 输入声明决定了调参入口,复制进 MT5 脚本就能改神经元数和 mu 节奏:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                LM.mq5 |
class=class="str">"cmt">//|                                                                Eugene |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Eugene"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class="macro">#include <Graphics\Graphic.mqh>
class="macro">#include <Math\Stat\Math.mqh>
class="macro">#include <Math\Stat\Normal.mqh>
enum Plots
  {
   LossFunction_plot,
   mu_plot,
   gradient_plot,
   target_netpredict_plot
  };
matrix weights1,weights2,bias1,bias2;                      class=class="str">"cmt">// network parameter matrices
matrix n1,n2,act1,act2,new_n1,new_n2,new_act1,new_act2;   class=class="str">"cmt">// neural layer output matrices
class="kw">input class="type">int    layer1     = class="num">20;                             class=class="str">"cmt">// neurons Layer class="num">1
class="kw">input class="type">int    Epochs     = class="num">10;                             class=class="str">"cmt">// Epochs
class="kw">input class="type">class="kw">double Initial_mu = class="num">0.001;                          class=class="str">"cmt">// mu
class="kw">input class="type">class="kw">double Incr_Rate  = class="num">10;                             class=class="str">"cmt">// increase mu
class="kw">input class="type">class="kw">double Decr_Rate  = class="num">0.1;                            class=class="str">"cmt">// decrease mu
class="kw">input class="type">class="kw">double Min_grad   = class="num">0.000001;                       class=class="str">"cmt">// min gradient norm

训练脚本的入口与参数装配

这段脚本把合成数据生成、标准化、神经网络训练与测试串成一条线,核心入口是 OnStart()。外部输入参数先定调:Loss_goal=0.001 控制损失目标,sigma_=0.1 是合成数据标准差,ac_func 用 AF_TANH 做隐层激活、ac_func_last 用 AF_LINEAR 做输出层,loss_func 选 LOSS_MSE 均方误差。 OnStart 里先 Func() 造训练样本,StandartScaler() 做 Z-score 标准化,再取 Sample_=data.Rows()、Features=data.Cols() 拿到样本数与特征维数。ones_ 用 matrix::Ones(1,Sample_) 生成全 1 行向量,供后续反向传播用。 训练耗时靠 GetMicrosecondCount() 前后打点,除以 1000 转毫秒后 Print 出来;参数总量按 layer1*(Features+1) + 1*(layer1+1) 算并打印,方便你核对模型规模。同一套 Func()+StandartScaler() 再跑一遍做测试集,最后 PlotGraphic(15,plot_log) 出图。 把 Loss_goal 从 0.001 调到 0.0001,或在 MT5 里把 ac_func 改成 AF_RELU 重跑,能直接观察 Print 里 Learning time 与 Number Parameters 的变化。外汇与贵金属行情高波动,合成数据结论不能直接外推到实盘,仅作算法验证用。

MQL5 / C++
class="kw">input class="type">class="kw">double Loss_goal = class="num">0.001; class=class="str">"cmt">// Loss goal
class="kw">input class="type">class="kw">double sigma_ = class="num">0.1; class=class="str">"cmt">// standard deviation synthetic data
class="kw">input Plots plot_ = LossFunction_plot; class=class="str">"cmt">// display graph
class="kw">input class="type">bool plot_log = false; class=class="str">"cmt">// logarithmic function graph 
class="kw">input ENUM_ACTIVATION_FUNCTION ac_func = AF_TANH; class=class="str">"cmt">// first layer activation function
class="kw">input ENUM_ACTIVATION_FUNCTION ac_func_last = AF_LINEAR; class=class="str">"cmt">// last layer activation function
class="kw">input ENUM_LOSS_FUNCTION loss_func = LOSS_MSE; class=class="str">"cmt">// Loss function
class="type">class="kw">double LossPlot[],NetOutput[],mu_Plot[],gradient_Plot[],target_Plot[];
matrix ones_;
class="type">class="kw">double old_error,gradient_NormP2;
class="type">class="kw">double mu_ = Initial_mu;
class="type">bool break_forloop = false;
class="type">int Sample_,Features;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script start function |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- generate a training sample
   matrix data, target;
   Func(data,target);
   StandartScaler(data);
   Sample_= (class="type">int)data.Rows();
   Features = (class="type">int)data.Cols();
   ArrayResize(target_Plot,Sample_);
   for(class="type">int i=class="num">0; i< (class="type">int)target.Rows(); i++)
     {
      target_Plot[i] =target[i,class="num">0];
     }
   ones_ = matrix::Ones(class="num">1,Sample_);
class=class="str">"cmt">//--- train the model
   class="type">ulong start=GetMicrosecondCount();
   Train(data, target, Epochs);
   class="type">ulong end = (GetMicrosecondCount()-start)/class="num">1000 ;
   Print("Learning time = " + (class="type">class="kw">string)end + " msc");
   class="type">int NumberParameters = layer1*(Features+class="num">1) + class="num">1*(layer1+class="num">1);
   Print("Number Parameters of NN = ",NumberParameters);
class=class="str">"cmt">//--- generate a test sample
   Func(data,target);
   StandartScaler(data);
class=class="str">"cmt">//--- test the model
   Test(data,target);

class=class="str">"cmt">//--- display graphs
   PlotGraphic(class="num">15,plot_log);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Model training method |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Train(matrix &data, matrix &target, class="kw">const class="type">int epochs)
  {
class=class="str">"cmt">//--- create the model
   if(!CreateNet())
      class="kw">return false;
class=class="str">"cmt">//--- train the model

「训练循环与网络初始化的收口细节」

这段逻辑把单隐藏层网络的训练迭代和建网动作收在一起看,能直接照搬到 MT5 的 EA 或脚本里跑。训练主循环用 epochs 控制轮数,每轮先前向传播,失败就直接返回 false 终止,不浪费后续计算。 每轮都会用 PrintFormat 打出 Epoch 编号和当前 loss,精度到小数点后 5 位;同时把 loss、mu_、梯度二范数写进三个动态数组,预留 10000 元素缓冲,方便后面绘图看收敛曲线。若 break_forloop 被置 true 就提前跳出,相当于手动或条件触发早停。 反向传播 Backprop 放在每轮末尾更新权重,若返回 false 同样终止。循环结束后打印梯度范数、mu_ 以及用 REGRESSION_RMSE 算出的 rmse,sigma_ 一并输出,这三个数能快速判断网络是否陷入平坦区或梯度爆炸。 CreateNet 负责建网:两个权重矩阵和偏置矩阵先按层数初始化,再用 Random(-0.1, 0.1) 填值。这个 ±0.1 的窄区间对小样本外汇特征可能更稳,换更大区间大概率会让初期 loss 飘得更高。 FeedForward 里 n1 = weights1.MatMul(data.Transpose()) + bias1.MatMul(ones_) 就是第一层预激活,data 需是列样本矩阵,转置后左乘权重,偏置靠 ones_ 广播。开 MT5 把这段接上你自己的行情矩阵,先盯前 20 轮 loss 是否降到 0.05 以下,再决定是否上实盘信号。外汇与贵金属杠杆高,模型输出只作概率参考,实盘前务必在策略测试器跑足够样本。

MQL5 / C++
for(class="type">int ep = class="num">0; ep < epochs; ep++)
  {
    class=class="str">"cmt">//--- feed forward
    if(!FeedForward(data))
      class="kw">return false;
    PrintFormat("Epoch %d, loss %.5f", ep, act2.Loss(target, loss_func));
    class=class="str">"cmt">//--- arrays for graphs
    ArrayResize(LossPlot,ep+class="num">1,class="num">10000);
    ArrayResize(mu_Plot,ep+class="num">1,class="num">10000);
    ArrayResize(gradient_Plot,ep+class="num">1,class="num">10000);
    LossPlot[ep]      = act2.Loss(target, loss_func);
    mu_Plot [ep]      = mu_;
    gradient_Plot[ep] = gradient_NormP2;
    class=class="str">"cmt">//--- Stop training if the target value of the loss function is reached
    if(break_forloop == true){class="kw">break;}
    class=class="str">"cmt">//--- backpropagation and update of weight matrix
    if(!Backprop(data, target))
      class="kw">return false;
  }
class=class="str">"cmt">//--- Euclidean norm of gradient, mu parameter, RMSE metric
   Print("gradient_normP2 =  ", gradient_NormP2);
   Print(" mu_ = ", mu_);
   class="type">class="kw">double rmse=act2.RegressionMetric(target.Transpose(),REGRESSION_RMSE);
   PrintFormat("rmse %.3f / sigma %.2f ",rmse,sigma_);
class=class="str">"cmt">//--- array of network output for graph
   ArrayResize(NetOutput,Sample_);
   for(class="type">int i=class="num">0; i< (class="type">int)act2.Transpose().Rows(); i++)
     {
       NetOutput[i]  = act2.Transpose()[i,class="num">0];
     }
class=class="str">"cmt">//--- class="kw">return result
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Model creation method                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateNet()
  {
class=class="str">"cmt">//--- initialize weight matrices
   if(!weights1.Init(layer1,Features) || !weights2.Init(class="num">1,layer1))
      class="kw">return false;
class=class="str">"cmt">//--- initialize offset matrices
   if(!bias1.Init(layer1,class="num">1)  || !bias2.Init(class="num">1,class="num">1))
      class="kw">return false;
class=class="str">"cmt">//--- fill the weight matrices with random values
   weights1.Random(-class="num">0.1, class="num">0.1);
   weights2.Random(-class="num">0.1, class="num">0.1);
   bias1.Random(-class="num">0.1, class="num">0.1);
   bias2.Random(-class="num">0.1, class="num">0.1);   
class=class="str">"cmt">//--- class="kw">return result
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Feed-forward method                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool FeedForward(matrix &data)
  {
class=class="str">"cmt">//--- calculate the first neural layer
class=class="str">"cmt">//--- n1 pre-activation of the first layer
   n1 = weights1.MatMul(data.Transpose()) + bias1.MatMul(ones_);
把重训瓶颈交给小布盯盘
小布盯盘的AIGC已内置多算法效率对照视图,打开对应品种页即可看到LM与SGD的实时收敛曲线,你只管挑重训节奏。

常见问题

LM除梯度外还利用二阶偏导信息,能在少数周期内定位损失局部极小,适合每根K线重训的快速适应场景,概率上显著减少迭代次数。
阻尼因子调节是关键,过小退化为高斯牛顿、过大退化为梯度下降;建议对雅可比矩阵做正则化并监控条件数,避免奇异矩阵导致训练中断。
它分离了周期分量与随机扰动,便于观察网络对确定结构的逼近能力及噪声鲁棒性,是轻量效率测试的合适基准。
可以,小布盯盘支持导入MQL5导出的网络权重,并在品种页标注过拟合概率与重训建议,省去手动跑回测的重复劳动。
LM在小样本在线重训更快,L-BFGS在大批次离线训练内存更省;外汇贵金属属高风险,建议先用历史分桶测两者收敛稳定性再定。