使用莱文贝格-马夸尔特(Levenberg-Marquardt,LM)算法训练多层感知器·综合运用
(3/3)·从梯度下降到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 的收敛曲线。
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 用历史数据验证。
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 或标志位控制。 外汇与贵金属波动剧烈,这类布尔返回仅代表逻辑条件满足,不预示任何价格方向,实盘使用前请在策略测试器用历史数据验证命中率。
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 对照脚本,路径需改成你本机数据位置才能直接跑。
# 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 怎么变,比看任何结论都实在。
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()