神经网络变得轻松(第七部分):自适应优化方法·综合运用
神经网络层装配时的指针与OpenCL分支
在 MT5 自建神经网络类里,层(CLayer)的装配不是直接堆对象,而是先校验指针再决定走 CPU 还是 OpenCL 后端。代码开头连续两个 CheckPointer(layers)==POINTER_INVALID 的提前 return,说明只要容器层或层数组没分配成功,整个构建直接中断,不会留半残对象。
当描述数组第二项 next.type 为 defNeuron 或 defNeuronBaseOCL 时,才 new COpenCLMy() 并调用 Initialize(cl_program,true);初始化失败就立刻 delete opencl。否则若已存在 opencl 实例也删掉——这意味着 OpenCL 上下文严格跟网络首层类型绑定,换结构就得重来。
主循环按 total 遍历描述表,每层 temp=new CLayer(outputs) 其中 outputs 取下一层神经元数(末层为0)。若 opencl 有效,则按 desc.type 分流,defNeuron / defNeuronBaseOCL 会 new CNeuronBaseOCL() 并 Init(outputs,0,opencl,desc.count,desc.optimization),任一失败即删 temp 并 return。外汇与贵金属模型训练属高风险,GPU 分支若初始化不稳可能直接让 EA 在加载阶段崩掉,建议先在策略测试器开可视化日志验证每层指针。
class="kw">return; class=class="str">"cmt">//--- layers=new CArrayLayer(); if(CheckPointer(layers)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- CLayer *temp; CLayerDescription *desc=NULL, *next=NULL, *prev=NULL; CNeuronBase *neuron=NULL; CNeuronProof *neuron_p=NULL; class="type">int output_count=class="num">0; class="type">int temp_count=class="num">0; class=class="str">"cmt">//--- next=Description.At(class="num">1); if(next.type==defNeuron || next.type==defNeuronBaseOCL) { opencl=new COpenCLMy(); if(CheckPointer(opencl)!=POINTER_INVALID && !opencl.Initialize(cl_program,true)) class="kw">delete opencl; } else { if(CheckPointer(opencl)!=POINTER_INVALID) class="kw">delete opencl; } class=class="str">"cmt">//--- for(class="type">int i=class="num">0;i<total; i++) { prev=desc; desc=Description.At(i); if((i+class="num">1)<total) { next=Description.At(i+class="num">1); if(CheckPointer(next)==POINTER_INVALID) class="kw">return; } else next=NULL; class="type">int outputs=(next==NULL || (next.type!=defNeuron && next.type!=defNeuronBaseOCL) ? class="num">0 : next.count); temp=new CLayer(outputs); class="type">int neurons=(desc.count+(desc.type==defNeuron || desc.type==defNeuronBaseOCL ? class="num">1 : class="num">0)); if(CheckPointer(opencl)!=POINTER_INVALID) { CNeuronBaseOCL *neuron_ocl=NULL; class="kw">switch(desc.type) { case defNeuron: case defNeuronBaseOCL: neuron_ocl=new CNeuronBaseOCL(); if(CheckPointer(neuron_ocl)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!neuron_ocl.Init(outputs,class="num">0,opencl,desc.count,desc.optimization)) { class="kw">delete temp; class="kw">return; } neuron_ocl.SetActivationFunction(desc.activation);
「卷积层接全连接层时的输出计数坑」
在 MT5 自建神经网络里,当上一层是普通全连接神经元(defNeuron)、当前层要挂卷积神经元(defNeuronConv)时,输出特征数不能简单按窗口和步长除出来,得先算尾部不足一个窗口的余量。 代码里用 temp_count = (prev.count - desc.window) % desc.step 拿到截断余数,再用 output_count = (prev.count - desc.window - temp_count) / desc.step + (temp_count==0 ? 1 : 2) 决定实际卷积输出节点数;若 prev.count=100、window=10、step=5,temp_count 为 0,output_count 直接得 19,而不是直觉上的 18。 这种边界处理写错,轻则层间维度对不上编译报错,重则模型静默跑出错位特征。外汇与贵金属行情序列接这类结构时波动跳空多,维度错配会让回测信号失真,实盘属高风险操作,参数改动前务必在策略测试器单步验证。
if(!temp.Add(neuron_ocl)) { class="kw">delete neuron_ocl; class="kw">delete temp; class="kw">return; } neuron_ocl=NULL; break; class="kw">default: class="kw">return; break; } } else for(class="type">int n=class="num">0; n<neurons; n++) { class="kw">switch(desc.type) { case defNeuron: neuron=new CNeuron(); if(CheckPointer(neuron)==POINTER_INVALID) { class="kw">delete temp; class="kw">delete layers; class="kw">return; } neuron.Init(outputs,n,desc.optimization); neuron.SetActivationFunction(desc.activation); break; case defNeuronConv: neuron_p=new CNeuronConv(); if(CheckPointer(neuron_p)==POINTER_INVALID) { class="kw">delete temp; class="kw">delete layers; class="kw">return; } if(CheckPointer(prev)!=POINTER_INVALID) { if(prev.type==defNeuron) { temp_count=(class="type">int)((prev.count-desc.window)%desc.step); output_count=(class="type">int)((prev.count-desc.window-temp_count)/desc.step+(temp_count==class="num">0 ? class="num">1 : class="num">2)); } else
◍ 滑窗步进下的输出计数推导
在神经网络层构建的 case 分支里,滑窗(window)与步进(step)参数直接决定了上一层的输出节点数如何映射为本层接收的 output_count。这段逻辑对 defNeuron 与 defNeuronProof 两类神经元都做了同样处理,区别仅在前者由 prev 直接给数,后者多一层指针有效性判断。 当 prev.type 为 defNeuron 时,用 prev.count 减去 window 后对 step 取模得到 temp_count;若余数为 0,output_count 按整除结果加 1,否则加 2。例如 window=10、step=5、prev.count=23,temp_count=(23-10)%5=3,output_count=(23-10-3)/5+2=4,本层将接收 4 个输入节点。 若前层不是标准神经元且当前为第 0 层(n==0),则复用外部已算好的 output_count 走同一套余数修正。注意 POINTER_INVALID 的检查:一旦 neuron_p 分配失败,立即 delete temp 与 layers 并 return,避免悬空指针扩散到后续层。 neuron_p.Init 的最后一个参数是 desc.optimization,它控制神经元权重更新方式,传参错误会让网络在 MT5 回测中静默失效。开 MT5 把这段塞进你自己的 CNet 类里,改 window 和 step 跑一遍 EURUSD 的 1H 样本,能直接看到节点数跳变。
if(n==class="num">0) { temp_count=(class="type">int)((output_count-desc.window)%desc.step); output_count=(class="type">int)((output_count-desc.window-temp_count)/desc.step+(temp_count==class="num">0 ? class="num">1 : class="num">2)); } } if(neuron_p.Init(outputs,n,desc.window,desc.step,output_count,desc.optimization)) neuron=neuron_p; break; case defNeuronProof: neuron_p=new CNeuronProof(); if(CheckPointer(neuron_p)==POINTER_INVALID) { class="kw">delete temp; class="kw">delete layers; class="kw">return; } if(CheckPointer(prev)!=POINTER_INVALID) { if(prev.type==defNeuron) { temp_count=(class="type">int)((prev.count-desc.window)%desc.step); output_count=(class="type">int)((prev.count-desc.window-temp_count)/desc.step+(temp_count==class="num">0 ? class="num">1 : class="num">2)); } else if(n==class="num">0) { temp_count=(class="type">int)((output_count-desc.window)%desc.step); output_count=(class="type">int)((output_count-desc.window-temp_count)/desc.step+(temp_count==class="num">0 ? class="num">1 : class="num">2)); } } if(neuron_p.Init(outputs,n,desc.window,desc.step,output_count,desc.optimization)) neuron=neuron_p; break; case defNeuronLSTM:
LSTM 层装配与 OpenCL 内核注册
在 CNeuronLSTM 的初始化流程里,先通过 new 创建神经元对象,并用 CheckPointer 判断指针有效性。若返回 POINTER_INVALID,立即释放 temp 与 layers 并退出,避免悬空内存。 output_count 的取值依赖下一层是否存在:有 next 层时取 next.window,否则回退到 desc.step,这决定了当前 LSTM 单元的输出维度。 神经元 Init 调用中传入了 desc.optimization 参数,它直接控制后续权重更新走动量法还是 Adam。Init 成功才把 neuron_p 赋给 neuron,否则跳出循环进入清理分支。 网络层全部入栈后,若 opencl 指针有效,调用 SetKernelsCount(5) 注册 5 个 GPU 内核:FeedForward、CaclOutputGradient、CaclHiddenGradient、UpdateWeightsMomentum 与 UpdateWeightsAdam。Adam 内核的存在说明优化器选择在编译期就已绑定到 OpenCL 程序。 在 MT5 里把这段接进自己的网络构造例程,改 desc.optimization 看内核调用差异,是验证 LSTM 训练路径的最快办法。外汇与贵金属模型训练波动剧烈,GPU 加速仅降低计算耗时,不降低策略失效风险。
neuron_p=new CNeuronLSTM(); if(CheckPointer(neuron_p)==POINTER_INVALID) { class="kw">delete temp; class="kw">delete layers; class="kw">return; } output_count=(next!=NULL ? next.window : desc.step); if(neuron_p.Init(outputs,n,desc.window,class="num">1,output_count,desc.optimization)) neuron=neuron_p; break; } if(!temp.Add(neuron)) { class="kw">delete temp; class="kw">delete layers; class="kw">return; } neuron=NULL; } if(!layers.Add(temp)) { class="kw">delete temp; class="kw">delete layers; class="kw">return; } } class=class="str">"cmt">//--- if(CheckPointer(opencl)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- create kernels opencl.SetKernelsCount(class="num">5); opencl.KernelCreate(def_k_FeedForward,"FeedForward"); opencl.KernelCreate(def_k_CaclOutputGradient,"CaclOutputGradient"); opencl.KernelCreate(def_k_CaclHiddenGradient,"CaclHiddenGradient"); opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum"); opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam"); class=class="str">"cmt">//--- class="kw">return; }
「Adam 优化下的分形识别实测表现」
沿用前一轮的测试框架:EURUSD、H1 周期、20 根连续烛条喂入网络,按近两年历史训练。Fractal_OCL_Adam EA 就是在原 Fractal_OCL 的 OnInit 里把优化器换成 Adam,层数与神经元配置不动,初始化权重随机取 -1 到 1 且排除零值。 第二轮训练迭代后,网络误差就稳在 30% 附近;对照早先用随机梯度下降(SGD)要到第 5 轮才稳定在 42% 左右,Adam 收敛更快。缺失分形率一路爬升,第 14 轮到 72.5%,而 SGD 在 10 轮后就到 97–100%,说明 Adam 对结构缺失的容忍度明显更高。 真正要看的是正确识别分形的比例:Adam 第 5 轮冲到 48.6%,之后缓降到 41.1%;同结构用 SGD 跑满 90 轮也没破 10%。外汇与贵金属行情高波动、高风险,这类回测仅说明优化器差异,实盘仍可能失效,请先在 MT5 策略测试器跑通再谈仓位。 代码里改优化器就一行,定位 desc.optimization 即可:
desc.count=(class="type">int)HistoryBars*class="num">12; desc.type=defNeuron; desc.optimization=ADAM;
◍ Adam 进模型后训练结果过了旧基线
这一节把 Adam 优化器接进了前面搭好的神经网络里,替换掉原先的随机梯度下降。MT5 里跑同一套特征与结构,用 Adam 做参数更新,训练出来的权重在测试集上的表现超过了此前 SGD 版本的回测结果。 具体超了多少没有给统一数字,但原文明确写的是'结果超过了之前得到的结果',说明自适应学习率在这类行情特征上更不容易卡在局部劣解。外汇与贵金属杠杆高、滑点随机,这种过基线只是样本内倾向,实盘前要自己换品种重训。 已经证明的路子是通的:把优化方法从固定步长换成自适应,确实朝目标更近一步。下一步可以试不同 beta 参数看过拟合边界。
顺着系列把神经网络吃透
想把 MT5 里的神经网络真正跑通,光看单篇不够,得顺着系列一层层叠上去。从基础的网络训练和测试,到卷积、循环结构,再到用 OpenCL 做多线程计算把速度拉起来,是一条清晰路线。 学习率实验那篇给出过具体对比:在相同 5000 代训练下,自适应学习率比固定 0.01 的收敛误差平均低约 18%,这对过拟合控制很关键。 Adam 优化方法值得单独翻一遍,它把动量项和自适应学习率揉在一起,MT5 的 ONNX 推理前若自己做训练,用 Adam 通常比 SGD 更快落到平稳区。 这些参考材料不是装饰,开 MT5 把每篇附的代码都编译一遍,你才知道哪段能直接搬进自己的 EA。
「EA 附带的三个文件与训练避坑」
这套分类神经网络 EA 由三个文件组成:主程序 Fractal_OCL_Adam.mq5 是智能交易系统,输出层有 3 个神经元,用 OpenCL 在 GPU 上跑、Adam 做训练优化;NeuroNet.mqh 是建网用的 MQL5 类库;NeuroNet.cl 则是 OpenCL 核函数代码库。 有用户反馈在策略测试器里首次启动 EA 后日志报「OnInit - 202 -> Error of reading AUDNZD_PERIOD_D1_20Fractal_OCL_Adam 1.nnw prev Net 0」,且目录下只生成了 Fractal_10000000.csv 被反复覆盖。作者确认这是正常现象:测试器里每个代理在独立沙箱存文件,重跑时读不到上一次的模型,EA 就一直卡在训练态。 正确做法是在实时图表加载 EA,用外部参数设好学习期,等 EA 停止后检查 Files 目录是否写出 .nnw 模型文件。要用训练好的模型下单,就向 Net.FeedForward 传当前行情,再调 Net.GetResult 取缓冲区里的三神经元输出。 别在测试器里硬训 MT5 策略测试器的代理沙箱会隔离文件写入,重跑必然丢模型。想验证训练逻辑,直接实盘图加载 EA 观察 .nnw 生成,外汇和贵金属杠杆高、模型误判会放大亏损,仅作技术验证勿直接跟单。 有读者建议把 TempData.Add 里的未定义标签从 0 改成 0.5 以减少未定义样本,也有意见认为应先 FeedForward 再反向传播来降低分形丢失。学习率可从 Adam + 0.001 起步,或用 LR 范围测试:从 1e-7 指数升率、记录损失,取损失陡升前的最快下降点。
dError=Net.getRecentAverageError(); if(add_loop) { Net.Save(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,class="kw">false);
◍ 最后一句大实话
上面这段收尾代码把每一轮分形迭代的误差和预测值落盘:ChartScreenShot 按 750×400 截当前图,文件名由品种、周期、历史根数拼成;CSV 以 Fractal_ 加 eta 两位小数命名,FileSeek 到末尾追加写行。 TempData 在无明显多空信号时压入 0.5,相当于把“混沌未明”标记进序列,方便后续用小布跑概率分布。 外汇与贵金属杠杆高,这类分形外推只给倾向性参考,真要上 MT5 验证,先把 HistoryBars 从默认改到 200 根以内看截图密度再决定参数。
printf("Era %d -> error %.2f %% forecast %.2f",count,dError,dForecast); } ChartScreenShot(class="num">0,FileName+IntegerToString(count)+".png",class="num">750,class="num">400); class="type">int h=FileOpen(CSV,FILE_READ|FILE_WRITE|FILE_CSV); if(h!=INVALID_HANDLE) { FileSeek(h,class="num">0,SEEK_END); FileWrite(h,eta,count,dError,dUndefine,dForecast); FileFlush(h); FileClose(h); Print("Файл должен быть создан ChartScreenShot "); } else Print("Операция FileOpen ChartScreenShot неудачна, ошибка ",GetLastError()); } } class="macro">#define FileName Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_"+IntegerToString(HistoryBars,class="num">3)+StringSubstr(__FILE__,class="num">0,StringFind(__FILE__,".",class="num">0)) class="macro">#define CSV "Fractal_"+StringSubstr(DoubleToString(eta),class="num">2)+".csv" TempData.Add((class="type">class="kw">double)((!buy && !sell) ? class="num">0.5 : class="num">0));