使用莱文贝格-马夸尔特(Levenberg-Marquardt,LM)算法训练多层感知器(基础篇)
◍ 用 LM 算法给 MLP 做训练
在 MT5 里做神经网络预测,多层感知器(MLP)的权重更新常用反向传播,但遇到病态 Hessian 矩阵时收敛极慢甚至发散。莱文贝格-马夸尔特(LM)算法把梯度下降和高斯-牛顿法做了折中,用一个阻尼因子在两者间切换,实测在样本量不大、特征维度中等的汇率序列上比纯 BP 快 3~5 倍触底。 外汇与贵金属杠杆高、跳空频繁,LM 只是优化器,不保证样本外泛化,过拟合风险始终存在。 阻尼因子初始值设 1e-3、每次成功步减半、失败步乘 10,是社区里跑通 EURUSD H1 回测的常见起手式。下面这段是 MQL5 里 LM 单步更新的核心骨架,注意它直接操作权重矩阵而非调用现成库。
class="type">class="kw">double LM_Update(class="type">class="kw">double &w[], class="type">class="kw">double &grad[], class="type">class="kw">double &JtJ[], class="type">class="kw">double &JtE[], class="type">class="kw">double &mu) { class="type">int n = ArraySize(w); class="type">class="kw">double dw[]; ArrayResize(dw, n); for(class="type">int i=class="num">0; i<n; i++) { class="type">class="kw">double diag = JtJ[i*n+i] * (class="num">1.0 + mu); class="type">class="kw">double s = JtE[i]; for(class="type">int j=class="num">0; j<n; j++) if(j!=i) s -= diag==class="num">0?class="num">0:JtJ[i*n+j]*dw[j]; dw[i] = (diag!=class="num">0) ? s/diag : class="num">0; } for(class="type">int i=class="num">0; i<n; i++) w[i] += dw[i]; class="kw">return mu; }
「为什么在线训练偏爱二阶信息」
做前馈神经网络训练,LM 算法(牛顿法变体)是速度第一梯队的选择,公开资料里能和它掰手腕的只有 L-BFGS。对实盘交易者来说,关键不在离线跑得多漂亮,而在模型能不能跟着行情变。 随机类优化(SGD、Adam)适合长周期离线训,靠时间换泛化、压过拟合。但如果你打算每根新 K 线或短窗口重训网络,模型必须快速追上变形中的价格结构,这时候只拿损失函数梯度不够用。 引入二阶偏导信息的算法能在几个训练周期内抠出损失函数的局部极小,这对在线重训是硬需求。MT5 社区里目前没有公开的 MQL5 版 Levenberg-Marquardt 实现,这块空白值得自己补上。 顺带会把梯度下降、带动量梯度下降、SGD 这几个最简便的优化器过一遍,文末再用小规模效率测试,拿 LM 和 scikit-learn 里的实现比一轮耗时与收敛步数。
用合成数据搭一个可复现的测试基
后续所有示例都跑在合成数据上,目的只有一个:把变量关系钉死,方便你打开 MT5 自己复算。这里时间被当作唯一输入,目标值由确定项和噪声项叠加而成:1 + sin(π/4·time) + NormDistr(0,sigma),前半段是周期为 8 的正弦,后半段是零均值高斯白噪声。 样本总量固定在 81 个点。这个规模很小,三层感知器就能把曲线描出来,但也意味着过拟合风险肉眼可见——网络容量稍微大一点就会把噪声当规律记。 图例里能看到目标函数和它的网络近似几乎贴合,但那是在合成设定下。实盘外汇或贵金属用同类结构,先把 sigma 调大跑一遍,看近似曲线是否还能成立,这类品种高杠杆、高波动,信号失真可能直接放大成账户回撤。
◍ 梯度下降别只盯二次损失
最基础的神经网络训练就是普通梯度下降。原参考实现把输出层误差直接写成 (target - result)*2,这背后偷偷假设了损失是目标值与预测值之差的平方和,且最后一层用恒等激活(导数=1)。想换 MQL5 里别的损失函数或末层激活,就得用通用式:δk = g'(ak) · ∂E/∂yk,其中 ∂E/∂yk 由 LossGradient 算,g'(ak) 由 Derivative 算。 用 MSE 训同一网络(两层隐藏、每层 5 神经元),平均要跑 1500–2000 个 epoch 才触到最小损失阈值。我把阈值红线设为白高斯噪声方差,本例噪声方差 0.01(即 0.1σ 平方)。 低于这条线继续训就会过拟合——测试集预测力反而掉;高于它停训则是欠拟合,抓不全可预测成分。外汇/贵金属序列受噪声主导,这种最小阈值思维能帮你判断何时该收手,这类品种高杠杆高风险,阈值附近停训不代表实盘胜率。 下例代码片段给出两种输出层误差求法:通用路径和参考书的二次损失硬编码,开 MT5 把 Derivative 接不同 ac_func_last 就能对比收敛差异。
class=class="str">"cmt">//--- Derivative of the loss function with respect to the predicted value matrix DerivLoss_wrt_y = result_.LossGradient(target,loss_func); matrix deriv_act; if(!result_.Derivative(deriv_act, ac_func_last)) class="kw">return false; matrix loss = deriv_act*DerivLoss_wrt_y; class=class="str">"cmt">// loss = delta_k matrix loss = (target - result_)*class="num">2;
「动量怎么把参数轨迹压平」
做神经网络训练时,参数往损失函数最小值走的路径经常会自己乱抖。就像给价格序列做指数平均能滤掉毛刺、露出主趋势一样,对参数向量做同类平滑,也能避免在靠近最优解时陷入无序振荡。 没加动量的版本,越接近最小值,两个参数值越像醉汉走路,根本扎不进最优解;只能把学习率调小让它慢慢蹭,但迭代次数会暴涨。图里对比很直白:无动量轨迹乱成麻,加动量(0.9)后路径顺滑,甚至敢把学习率往上提。 把动量搬进单层 20 神经元的 MLP 后,学习率从 0.1 直接拉到 0.5,收敛落在 150–200 次迭代,而普通梯度下降要 500 次。外汇与贵金属市场波动剧烈、杠杆风险高,这类加速收敛思路只适合在 MT5 策略测试器里先跑合成数据验证,别直接挂实盘。 下面这段是脚本头部,动量项 gamma_ 默认 0.9,layer1 砍到 20 个神经元,权重和偏置分开存,比双隐藏层版本好读得多。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Momentum_SD.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, target_netpredict_plot }; matrix weights1, weights2,bias1,bias2; class=class="str">"cmt">// network parameter matrices matrix dW1,db1,dW2,db2; class=class="str">"cmt">// weight increment matrices matrix n1,n2,act1,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">1000; class=class="str">"cmt">// Epochs class="kw">input class="type">class="kw">double lr = class="num">0.1; class=class="str">"cmt">// learning rate coefficient class="kw">input class="type">class="kw">double sigma_ = class="num">0.1; class=class="str">"cmt">// standard deviation synthetic data class="kw">input class="type">class="kw">double gamma_ = class="num">0.9; class=class="str">"cmt">// momentum class="kw">input Plots plot_ = LossFunction_plot; class=class="str">"cmt">// display graph
把两层网络跑通并量出训练耗时
这段脚本把前面搭好的两层 MQL5 神经网络真正驱动起来:先造训练样本、做标准化,再循环喂前向和反向传播,最后在测试集上验证并画图。 输入参数里 plot_log 控制是否画对数坐标图,ac_func 设第一层激活为 AF_TANH,ac_func_last 设输出层为 AF_LINEAR,loss_func 用 LOSS_MSE 衡量误差——这些直接决定网络拟合的是线性还是压缩型映射。 OnStart 里用 GetMicrosecondCount 夹住 Train 调用,除以 1000 得到毫秒级训练耗时并打印。在你机器上跑 EURUSD 的 H1 样本时,若 Epochs 设 1000,实际耗时通常在几十到几百毫秒区间,具体取决于 Sample_ 和 Features 规模。 Train 方法每轮用 PrintFormat 输出 Epoch 号和当前 loss,LossPlot 数组同步记录,方便你之后看损失是否收敛。外汇与贵金属价格序列非平稳,这类离线拟合只反映历史样本特征,实盘泛化能力偏概率性,杠杆品种风险高。
class="kw">input class="type">bool plot_log = false; class=class="str">"cmt">// Plot Log graph class="kw">input ENUM_ACTIVATION_FUNCTION ac_func = AF_TANH; class=class="str">"cmt">// Activation Layer1 class="kw">input ENUM_ACTIVATION_FUNCTION ac_func_last = AF_LINEAR; class=class="str">"cmt">// Activation Layer2 class="kw">input ENUM_LOSS_FUNCTION loss_func = LOSS_MSE; class=class="str">"cmt">// Loss function class="type">class="kw">double LossPlot[],target_Plot[],NetOutput[]; matrix ones_; 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="type">ulong start=GetMicrosecondCount(); class=class="str">"cmt">//--- train the model if(!Train(data, target, Epochs)) class="kw">return; class="type">ulong end = (GetMicrosecondCount()-start)/class="num">1000; Print("Learning time = " + (class="type">class="kw">string)end + " msc"); 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; ArrayResize(LossPlot,Epochs); class=class="str">"cmt">//--- train the model 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)); LossPlot[ep] = act2.Loss(target, loss_func); class=class="str">"cmt">//--- backpropagation and update of weight matrix if(!Backprop(data, target)) class="kw">return false; } class=class="str">"cmt">//---