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

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

(3/3)·从梯度下降到LM实战,用合成数据和小规模测试补齐在线重训练的最后一块拼图

含代码示例实战向 第 3/3 篇
不少交易者拿SGD或Adam做每根K线重训练,迭代几十轮还漂在损失面外。LM这类吃二阶偏导信息的算法,几个周期就可能落进局部极小,更适合短线条件切换快的场景。

反向传播里的 Levenberg-Marquardt 实现

这段逻辑跑的是经典 LM 优化,不是朴素梯度下降。先算当前损失 old_error = act2.Loss(target, loss_func),再用二次损失求网络误差矩阵 loss = act2.Transpose() - target,随后分别取最后一层和第一层激活函数的导函数 D2、deriv_act1。 第一层误差 D1 由权重二阶转置乘 D2 得到,再逐元素乘 deriv_act1;接着 partjacobian 分别用输入数据和 act1 算 jac1、jac2,纵向拼成完整雅可比 jac。梯度 je = jac转置乘loss,其 Frobenius 范数除以样本数 Sample_ 得 gradient_NormP2。 当 gradient_NormP2 < Min_grad 时直接打印局部极小并停训,这是防止过拟合迭代的硬闸。否则进 LM 主循环:Hessian = jac转置乘jac,加 mu_*单位阵后调用 H_I.Solve(v_je) 求参数增量,比注释里写的直接求逆 H_I.Inv() 数值更稳、开销更低。 mu_ 的搜索区间被卡在 1e-20 到 1e10 之间,超出即放弃更新。外汇与贵金属行情噪声大,用 LM 训这类两层网时建议先把 Sample_ 和 Min_grad 按品种波动调一遍,再到 MT5 里单步看 gradient_NormP2 的收敛曲线。

MQL5 / C++
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">//--- current value of the loss function
  old_error = act2.Loss(target, loss_func);
class=class="str">"cmt">//--- network error(quadratic loss function)
  matrix loss = act2.Transpose() - target ;
class=class="str">"cmt">//--- derivative of the activation function of the last layer
  matrix D2;
  n2.Derivative(D2, ac_func_last);
class=class="str">"cmt">//--- derivative of the first layer activation function
  matrix deriv_act1;
  n1.Derivative(deriv_act1, ac_func);
class=class="str">"cmt">//--- first layer network error
  matrix D1 = weights2.Transpose().MatMul(D2);
  D1 = deriv_act1 * D1;
class=class="str">"cmt">//--- first partial derivatives of network errors with respect to the first layer weights
  matrix jac1;
  partjacobian(data.Transpose(),D1,jac1);
class=class="str">"cmt">//--- first partial derivatives of network errors with respect to the second layer weights
  matrix jac2;
  partjacobian(act1,D2,jac2);
class=class="str">"cmt">//--- Jacobian
  matrix j1_D1 = Matrixconcatenate(jac1,D1.Transpose(),class="num">1);
  matrix j2_D2 = Matrixconcatenate(jac2,D2.Transpose(),class="num">1);
  matrix jac  = Matrixconcatenate(j1_D1,j2_D2,class="num">1);
class=class="str">"cmt">// --- Loss function gradient
  matrix je = (jac.Transpose().MatMul(loss));
class=class="str">"cmt">//--- Euclidean norm of gradient normalized to sample size
  gradient_NormP2 = je.Norm(MATRIX_NORM_FROBENIUS)/Sample_;
  if(gradient_NormP2 < Min_grad)
    {
      Print("Local minimum. The gradient is less than the specified value.");
      break_forloop = true; class=class="str">"cmt">// stop training
      class="kw">return true;
    }
class=class="str">"cmt">//--- Hessian
  matrix Hessian = (jac.Transpose().MatMul(jac));
  matrix I=matrix::Eye(Hessian.Rows(), Hessian.Rows());
class=class="str">"cmt">//---
  break_forloop = true;
  while(mu_ <= class="num">1e10 && mu_ > class="num">1e-20)
    {
      matrix H_I = (Hessian + mu_*I);
      class=class="str">"cmt">//--- solution via Solve
      vector v_je = je.Col(class="num">0);
      vector Updatelinsolve = -class="num">1* H_I.Solve(v_je);
      matrix Update = matrix::Zeros(Hessian.Rows(),class="num">1);
      Update.Col(Updatelinsolve,class="num">0); class=class="str">"cmt">// increment of the parameter vector
      
      class=class="str">"cmt">//--- inefficient calculation of inverse matrix
      class=class="str">"cmt">//  matrix Update = H_I.Inv();
      class=class="str">"cmt">//  Update = -class="num">1*Update.MatMul(je);
      class=class="str">"cmt">//---
      class=class="str">"cmt">//--- save the current parameters
      matrix  Prev_weights1 = weights1;
      matrix  Prev_bias1    = bias1;

◍ 反向传播里的参数回退与步长修正

这段逻辑跑在两层神经网络的训练循环里,核心动作是把上一轮的权重和偏置先存进 Prev_weights2、Prev_bias2,等本轮更新算完损失再决定要不要保留。若新误差没比旧误差小,就整体回退到上一轮参数,避免模型被坏梯度带偏。 更新时先把展平后的梯度矩阵 Update 按特征数 Features 与隐层 layer1 拆回权重和偏置:第一层 updWeight1 尺寸为 layer1×Features,updbias1 为 layer1×1;第二层 updWeight2 为 1×layer1,updbias2 为 1×1。拆完直接加到原 weights1/bias1 与 weights2/bias2 上。 新参数前向跑一遍得到 new_act2,用 Loss 函数算出 new_error。若 new_error 低于 Loss_goal 就打印完成并退出训练;否则当 new_error >= old_error 时回退参数并把 mu_ 乘 Incr_Rate 放大步长惩罚,反之乘 Decr_Rate 缩小步长。外汇与贵金属行情噪声大,这种回退机制能降低过拟合风险,但模型仍可能失效,实盘前务必在 MT5 用历史数据验证。

MQL5 / C++
  matrix  Prev_weights2 = weights2;
  matrix  Prev_bias2    = bias2;
  class=class="str">"cmt">//---
  class=class="str">"cmt">//--- update the parameters
  class=class="str">"cmt">//--- first layer
  matrix updWeight1 = matrix::Zeros(layer1,Features);
  class="type">int count =class="num">0;
  for(class="type">int j=class="num">0; j <Features; j++)
    {
     for(class="type">int i=class="num">0 ; i <layer1; i++)
       {
        updWeight1[i,j] = Update[count,class="num">0];
        count = count+class="num">1;
       }
    }
  matrix updbias1 = matrix::Zeros(layer1,class="num">1);
  for(class="type">int i =class="num">0 ; i <layer1; i++)
    {
     updbias1[i,class="num">0] = Update[count,class="num">0];
     count = count +class="num">1;
    }
    
  weights1 = weights1 + updWeight1;
  bias1 = bias1 + updbias1;
  class=class="str">"cmt">//--- second layer
  matrix updWeight2 = matrix::Zeros(class="num">1,layer1);
  for(class="type">int i =class="num">0 ; i <layer1; i++)
    {
     updWeight2[class="num">0,i] = Update[count,class="num">0];
     count = count +class="num">1;
    }
  matrix updbias2 = matrix::Zeros(class="num">1,class="num">1);
  updbias2[class="num">0,class="num">0] = Update[count,class="num">0];
  weights2 = weights2 + updWeight2;
  bias2 = bias2 + updbias2;
  class=class="str">"cmt">//--- calculate the loss function for the new parameters
  new_n1 = weights1.MatMul(data.Transpose()) + bias1.MatMul(ones_);
  new_n1.Activation(new_act1, ac_func);
  new_n2 = weights2.MatMul(new_act1) + bias2.MatMul(ones_);
  new_n2.Activation(new_act2, ac_func_last);
  class=class="str">"cmt">//--- loss function taking into account new parameters
  class="type">class="kw">double new_error = new_act2.Loss(target, loss_func);
  class=class="str">"cmt">//--- if the loss function is less than the specified threshold, terminate training
  if(new_error < Loss_goal)
    {
     break_forloop = true;
     Print("Training complete. The desired loss function value achieved");
     class="kw">return true;
    }
  break_forloop = false;
  class=class="str">"cmt">//--- correct the mu parameter
  if(new_error >= old_error)
    {
     weights1 = Prev_weights1;
     bias1     = Prev_bias1;
     weights2 = Prev_weights2;
     bias2     = Prev_bias2;
     mu_ = mu_*Incr_Rate;
    }
  else
    {
     mu_ = mu_*Decr_Rate;

「循环退出与函数返回的真值」

上面这段 MQL5 片段处在某个扫描逻辑的内层,当满足条件时执行 break 直接跳出当前循环体,避免继续无意义的后续迭代。 外层循环走完后,代码以 //--- return result 注释标记收口,最终 return true 表示该次调用判定通过。 在 MT5 里把这段接进自定义指标或 EA 的校验函数时,注意 break 只退出最邻近的 for/while,不会中断外层;若你期望整体提前返回,应改用 return 或标志位控制。 外汇与贵金属波动剧烈,这类布尔返回仅代表逻辑条件满足,不预示任何价格方向,实盘使用前请在策略测试器用历史数据验证命中率。

MQL5 / C++
      class="kw">break;
      }
     }
class=class="str">"cmt">//--- class="kw">return result
   class="kw">return true;
   }

1000周期训练反而让LM记住了噪声

把前面的对数尺度损失函数拉出来看,情形完全反转:算法在仅 6 次迭代时就触到了最小边界,说明收敛极快。 若把训练拉长到 1000 个周期,LM 网络开始出现典型过拟合——它不再学价格结构,而是直接记住高斯白噪声。图例里能清楚看到权重被噪声绑架。 在训练集与测试集分别跑性能统计:训练集 RMSE = 0.168,测试集 RMSE = 0.267。两者缺口 0.099,就是过拟合实打实的代价。 外汇与贵金属行情噪声占比高、样本少,拿 MT5 内置神经网络这么硬训,过拟合概率偏大,属高风险验证行为。

◍ 千点数据上 LM 与 sklearn 优化器正面对比

拿两个特征、1000 个数据点做压力测试,LM_BigData 脚本和 Python 的 sklearn 同台跑。为公平起见,Python 端把损失函数额外乘了 2,因为库里默认返回的是均方误差再除以 2:return ((y_true - y_pred) ** 2).mean() / 2。LM 端损失目标阈值锁在 0.01,Python 端靠迭代次数凑到相近水平。 单隐藏层 20 个神经元时,SGD 损失 0.00278、耗时 11459 毫秒;Adam 损失 0.03363、耗时 8581 毫秒;L-BFGS 损失 0.02770、耗时 277 毫秒(无损失曲线可绘);自写 LM 损失 0.00846、耗时 117 毫秒。LM 在耗时上碾压,损失也低于 Adam 和 L-BFGS,仅略逊 SGD。 把规模拉到 100 个神经元(对应 401 个网络参数),L-BFGS 损失 0.00847、耗时 671 毫秒;LM 损失 0.00206、耗时 1253 毫秒。此时 LM 损失反超且幅度明显,但耗时翻了一倍多。参数不超 400 或隐藏层不超 100 神经元时,LM 大概率是最快选择;再往上加,L-BFGS 可能逐渐占优。外汇与贵金属行情高噪声,实盘套这类小网络前请在 MT5 用历史 tick 重跑验证。 下方是 Python 对照脚本,路径需改成你本机数据位置才能直接跑。

MQL5 / C++
# Eugene
# [MQL5官方文档]
class="kw">import numpy as np
class="kw">import time
class="kw">import matplotlib.pyplot as plt
class="kw">import pandas as pd
from sklearn.neural_network class="kw">import MLPRegressor
# here is your path to the data
df = pd.read_csv(r&class="macro">#x27;C:\Users\Evgeniy\AppData\Local\Programs\Python\Python39\Data.csv&class="macro">#x27;,delimiter=&class="macro">#x27;;&class="macro">#x27;)
X = df.to_numpy()
df1 = pd.read_csv(r&class="macro">#x27;C:\Users\Evgeniy\AppData\Local\Programs\Python\Python39\Target.csv&class="macro">#x27;)
y = df1.to_numpy()
y = y.reshape(-class="num">1)
start = time.time()
&class="macro">#x27;&class="macro">#x27;&class="macro">#x27;
clf = MLPRegressor(solver=&class="macro">#x27;sgd&class="macro">#x27;, alpha=class="num">0.0,
                  hidden_layer_sizes=(class="num">20),
                  activation=&class="macro">#x27;tanh&class="macro">#x27;,
                  max_iter=class="num">700,batch_size=class="num">10,
                  learning_rate_init=class="num">0.01,momentum=class="num">0.9,
                  shuffle = False,n_iter_no_change = class="num">2000, tol = class="num">0.000001)
&class="macro">#x27;&class="macro">#x27;&class="macro">#x27;
&class="macro">#x27;&class="macro">#x27;&class="macro">#x27;
clf = MLPRegressor(solver=&class="macro">#x27;adam&class="macro">#x27;, alpha=class="num">0.0,
                  hidden_layer_sizes=(class="num">20),

「最后说句实在话」

上面那段 MLPRegressor 的拟合与诊断代码,把两种求解器放在同一脚本里对比:adam 版跑满 max_iter=3000、batch_size=100,lbfgs 版只迭代 300 次且 alpha=0.0。两者都输出 training time(毫秒)、loss 与 n_iter_,并把 loss_curve_ 取对数画出来,你能直接看收敛斜率差异。 外汇与贵金属杠杆高、滑点凶,神经网络回归出的价位只是概率倾向,别拿它当入场指令。开 MT5 把这段丢进 Python 环境跑一遍,换个 hidden_layer_sizes=(50) 或 (200),看 iter 和 loss 怎么变,比看任何结论都实在。

MQL5 / C++
activation=&class="macro">#x27;tanh&class="macro">#x27;,
max_iter=class="num">3000,batch_size=class="num">100,
learning_rate_init=class="num">0.01,
n_iter_no_change = class="num">2000, tol = class="num">0.000001)
&class="macro">#x27;&class="macro">#x27;&class="macro">#x27;
#&class="macro">#x27;&class="macro">#x27;&class="macro">#x27;
clf = MLPRegressor(solver=&class="macro">#x27;lbfgs&class="macro">#x27;, alpha=class="num">0.0,
              hidden_layer_sizes=(class="num">100),
              activation=&class="macro">#x27;tanh&class="macro">#x27;,max_iter=class="num">300,
              tol = class="num">0.000001)
#&class="macro">#x27;
clf.fit(X, y)
end = time.time() - start      # training time
print("learning time  =",end*class="num">1000)
print("solver = ",clf.solver);
print("loss = ",clf.loss_*class="num">2)
print("iter = ",clf.n_iter_)
class="macro">#print("n_layers_ = ",clf.n_layers_)
class="macro">#print("n_outputs_ = ",clf.n_outputs_)
class="macro">#print("out_activation_ = ",clf.out_activation_)
coef = clf.coefs_
class="macro">#print("coefs_ = ",coef)
inter = clf.intercepts_
class="macro">#print("intercepts_ = ",inter)
plt.plot(np.log(pd.DataFrame(clf.loss_curve_)))
plt.title(clf.solver)
plt.xlabel(&class="macro">#x27;Epochs&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Loss&class="macro">#x27;)
plt.show()
把重复劳动交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到网络重训练耗时与损失曲线,你只管判断信号是否还值得跟。

常见问题

在线场景每根K线或短时窗口就要重训,二阶信息如LM用海森近似,几个周期可能收敛,SGD类随机法周期多、易卡在平坦区。
LossGradient算损失对预测值的偏导,Derivative算激活函数导数值,两者相乘得到输出层误差δ,是反向传播通用入口。
确定性周期加高斯白噪能隔离过拟合干扰,直观看网络能否抓周期分量,便于比对不同优化器近似质量。
小布内置AIGC诊断可展示品种页上的训练耗时与损失变化,但模型代码仍需在MQL5端按本文示例部署,小布负责看板不替你写EA。
小规模测试里LM周期少、单轮重,L-BFGS内存省;实盘样本量上来后倾向按点差与重训频率实测取舍,无绝对优劣。