使用莱文贝格-马夸尔特(Levenberg-Marquardt,LM)算法训练多层感知器·进阶篇
(2/3)· 当SGD在每根K线重训时拖慢节奏,LM用二阶信息把周期压到个位数
很多交易者把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_ 打出来对比即可验证。
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 曲线差异。
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 节奏:
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 的变化。外汇与贵金属行情高波动,合成数据结论不能直接外推到实盘,仅作算法验证用。
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 以下,再决定是否上实盘信号。外汇与贵金属杠杆高,模型输出只作概率参考,实盘前务必在策略测试器跑足够样本。
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_);