神经网络变得轻松(第七部分):自适应优化方法·综合运用
📘

神经网络变得轻松(第七部分):自适应优化方法·综合运用

第 3/3 篇

神经网络层装配时的指针与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 在加载阶段崩掉,建议先在策略测试器开可视化日志验证每层指针。

MQL5 / C++
  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。 这种边界处理写错,轻则层间维度对不上编译报错,重则模型静默跑出错位特征。外汇与贵金属行情序列接这类结构时波动跳空多,维度错配会让回测信号失真,实盘属高风险操作,参数改动前务必在策略测试器单步验证。

MQL5 / C++
         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 样本,能直接看到节点数跳变。

MQL5 / C++
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 加速仅降低计算耗时,不降低策略失效风险。

MQL5 / C++
 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 即可:

MQL5 / C++
      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 指数升率、记录损失,取损失陡升前的最快下降点。

MQL5 / C++
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 根以内看截图密度再决定参数。

MQL5 / C++
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));

常见问题

先按滑窗步进公式算清特征图尺寸再乘通道数,别直接拿输入像素数当输出计数,否则层装配会越界报错。
多数情况是内核函数名没按层类型登记,或指针偏移和前层输出维度对不上,核对分支注册表即可定位。
可以,小布能读取你的层配置并标出指针越界与未注册内核分支,直接给修正建议省去手动排查。
文中实测在分形识别任务上过了旧基线,具体以你的验证集误差下降且稳定收敛为准,无统一阈值。
用(输入尺寸-核尺寸)/步进+1逐维算,写个小脚本批量跑比心算稳,避免装配时维度连锁错。