神经网络轻松制作·综合运用
📘

神经网络轻松制作·综合运用

第 3/3 篇

◍ 神经网络类的接口与前向传播实现

在 MT5 里用 C++ 风格封装一个多层感知机,核心类 CNet 先把生命周期和资源接口定清楚。析构走空实现 ~CNet(){},对外暴露 feedForward(推前向)、backProp(反传)、getResults(取输出)、getRecentAverageError(读平滑误差)四个方法,另配 Save/Load 把权重和预测元数据落盘,common 参数默认 true 表示存到公共目录。 recentAverageSmoothingFactor 是静态因子,控制滑动误差的平滑强度;私有成员 layers 用 CArrayLayer 管理各层,recentAverageError 记录当前平均误差。建网时构造函数吃一个 topology(各层神经元数数组),循环里给每一层 CreateElement,输出数取下一层规模,最后一层输出数为 0。 feedForward 先做指针和维度校验:输入长度必须等于首层神经元数减 1(减的是偏置位),否则直接 return。随后把输入写进首层每个神经元的 outputVal,再从 layerNum=1 起逐层调用 neuron.feedForward(prevLayer) 把信号往后送;循环里全嵌了 !IsStopped() 判断,EA 被终止时能立刻跳出,避免卡死回测或实盘。 getResults 通过引用回传结果数组,调用方拿到的是最后一层的激活输出。外汇与贵金属波动剧烈、杠杆高风险大,这套网络仅作概率性信号参考,实盘前务必在 MT5 策略测试器用历史数据验证收敛与误差表现。

MQL5 / C++
~CNet(){};
class="type">void feedForward(const CArrayDouble *inputVals);
class="type">void backProp(const CArrayDouble *targetVals);
class="type">void getResults(CArrayDouble *&resultVals);
class="type">class="kw">double getRecentAverageError() const { class="kw">return recentAverageError; }
class="type">bool Save(const class="type">class="kw">string file_name, class="type">class="kw">double error, class="type">class="kw">double undefine, class="type">class="kw">double forecast, class="type">class="kw">datetime time, class="type">bool common=true);
class="type">bool Load(const class="type">class="kw">string file_name, class="type">class="kw">double &error, class="type">class="kw">double &undefine, class="type">class="kw">double &forecast, class="type">class="kw">datetime &time, class="type">bool common=true);
class=class="str">"cmt">//---
class="kw">static class="type">class="kw">double recentAverageSmoothingFactor;
class="kw">private:
CArrayLayer layers;
class="type">class="kw">double recentAverageError;
};
CNet::CNet(const CArrayInt *topology)
  {
  if(CheckPointer(topology)==POINTER_INVALID)
    class="kw">return;
class=class="str">"cmt">//---
  class="type">int numLayers=topology.Total();
  for(class="type">int layerNum=class="num">0; layerNum<numLayers; layerNum++)
    {
     class="type">uint numOutputs=(layerNum==numLayers-class="num">1 ? class="num">0 : topology.At(layerNum+class="num">1));
     if(!layers.CreateElement(topology.At(layerNum), numOutputs))
       class="kw">return;
    }
  }
class="type">void CNet::feedForward(const CArrayDouble *inputVals)
  {
  if(CheckPointer(inputVals)==POINTER_INVALID)
    class="kw">return;
class=class="str">"cmt">//---
CLayer *Layer=layers.At(class="num">0);
  if(CheckPointer(Layer)==POINTER_INVALID)
    {
     class="kw">return;
    }
  class="type">int total=inputVals.Total();
  if(total!=Layer.Total()-class="num">1)
     class="kw">return;
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0; i<total && !IsStopped(); i++)
    {
     CNeuron *neuron=Layer.At(i);
     neuron.setOutputVal(inputVals.At(i));
    }
class=class="str">"cmt">//---
  total=layers.Total();
  for(class="type">int layerNum=class="num">1; layerNum<total && !IsStopped(); layerNum++)
    {
     CArrayObj *prevLayer = layers.At(layerNum - class="num">1);
     CArrayObj *currLayer = layers.At(layerNum);
     class="type">int t=currLayer.Total()-class="num">1;
     for(class="type">int n=class="num">0; n<t && !IsStopped(); n++)
       {
        CNeuron *neuron=currLayer.At(n);
        neuron.feedForward(prevLayer);
       }
    }
  }
class="type">void CNet::getResults(CArrayDouble *&resultVals)
  {

反向传播里误差与梯度的实际流向

这段 CNet::backProp 做的是神经网络训练的核心回退:先拿输出层和目标值算均方根误差,再逐层反推梯度并更新权重。外汇与贵金属行情噪声大,这类模型过拟合概率高,实盘前务必用历史数据做交叉验证。 误差计算循环里用 error/=total 再 sqrt(error),得到的是输出层神经元的平均 RMS 误差,并借 recentAverageSmoothingFactor 做平滑,recentAverageError 会随训练步数缓变而非跳变。 梯度分两段算:输出层直接拿 targetVals 调 calcOutputGradients;隐藏层从倒数第二层循环到第一层,靠下一层对象算 calcHiddenGradients。最后权重更新从输出层回退到输入层,每层调 updateInputWeights(prevLayer)。 所有循环都嵌了 !IsStopped() 判定,EA 在 MT5 点停止时会安全跳出,不会卡死在多层回退中。复制下面代码到 MT5 的 CNet 类方法里,接好神经元接口就能跑通单步训练。

MQL5 / C++
if(CheckPointer(resultVals)==POINTER_INVALID)
  {
   resultVals=new CArrayDouble();
  }
 resultVals.Clear();
 CArrayObj *Layer=layers.At(layers.Total()-class="num">1);
 if(CheckPointer(Layer)==POINTER_INVALID)
  {
   class="kw">return;
  }
 class="type">int total=Layer.Total()-class="num">1;
 for(class="type">int n=class="num">0; n<total; n++)
  {
   CNeuron *neuron=Layer.At(n);
   resultVals.Add(neuron.getOutputVal());
  }
}
class="type">void CNet::backProp(const CArrayDouble *targetVals)
  {
  if(CheckPointer(targetVals)==POINTER_INVALID)
    class="kw">return;
  CArrayObj *outputLayer=layers.At(layers.Total()-class="num">1);
  if(CheckPointer(outputLayer)==POINTER_INVALID)
    class="kw">return;
class=class="str">"cmt">//---
  class="type">class="kw">double error=class="num">0.0;
  class="type">int total=outputLayer.Total()-class="num">1;
  for(class="type">int n=class="num">0; n<total && !IsStopped(); n++)
    {
    CNeuron *neuron=outputLayer.At(n);
    class="type">class="kw">double delta=targetVals[n]-neuron.getOutputVal();
    error+=delta*delta;
    }
  error/= total;
  error = sqrt(error);
  recentAverageError+=(error-recentAverageError)/recentAverageSmoothingFactor;
class=class="str">"cmt">//---
  for(class="type">int n=class="num">0; n<total && !IsStopped(); n++)
    {
    CNeuron *neuron=outputLayer.At(n);
    neuron.calcOutputGradients(targetVals.At(n));
    }
class=class="str">"cmt">//---
  for(class="type">int layerNum=layers.Total()-class="num">2; layerNum>class="num">0; layerNum--)
    {
    CArrayObj *hiddenLayer=layers.At(layerNum);
    CArrayObj *nextLayer=layers.At(layerNum+class="num">1);
    total=hiddenLayer.Total();
    for(class="type">int n=class="num">0; n<total && !IsStopped();++n)
      {
       CNeuron *neuron=hiddenLayer.At(n);
       neuron.calcHiddenGradients(nextLayer);
      }
    }
class=class="str">"cmt">//---
  for(class="type">int layerNum=layers.Total()-class="num">1; layerNum>class="num">0; layerNum--)
    {
    CArrayObj *layer=layers.At(layerNum);
    CArrayObj *prevLayer=layers.At(layerNum-class="num">1);
    total=layer.Total()-class="num">1;
    for(class="type">int n=class="num">0; n<total && !IsStopped(); n++)
      {
       CNeuron *neuron=layer.At(n);
       neuron.updateInputWeights(prevLayer);
      }
    }
  }

「感知器只是起点」

这套在家搭神经网络的做法,落点其实只是冰山一角。文中实现的感知器,是 Frank Rosenblatt 在 1957 年提出的模型,到今天已经过了 60 多年,期间衍生出的网络结构多到数不清。 但别小看它——感知器在 MT5 里跑回归或分类任务,依然能给出可用的信号,你可以直接把前几节的代码挂上 EURUSD 的 H1 复盘验证。 想往深了走,得自己啃专业资料,单靠这几篇篇幅根本铺不开全部 AI 概念。外汇和贵金属行情受杠杆与跳空影响,用任何模型都先想到爆仓风险。

◍ 随文附带的感知器类库

这篇系列里真正能直接落地的东西,是附在文末的 NeuroNet.mqh 类库,体积 38.34 KB,封装了创建多层感知器网络的基础类。想跑通前文那些 LSTM 之外的轻量实验,直接把该文件丢进 MT5 的 MQL5/Include 目录就能 include 调用。 评论区有人踩过坑:原文件里部分方法声明带了 const 修饰,在较新 build 下编译会直接报错。解决办法是把对应语句里的 const 去掉再编译,社区里已有人验证可正常通过。外汇与贵金属行情受杠杆和跳空影响,用这类网络做信号生成前务必先开模拟盘验证,实盘误判概率不低。 类库本身只提供网络结构,不绑定任何行情数据接口。你要喂什么特征、怎么标准化输入,得自己接 ICustom 或 CopyBuffer 取序列,这一步没人替你定。

常见问题

把输入乘权重加偏置再过激活函数逐层往前推,文里给了完整的类接口和逐行拆解,照着抄进你的策略就能跑。
误差从输出层拿预测减真实标签算起,按链式法则逐层乘局部梯度回传,权重更新用的就是这些回传梯度。
可以,把你的网络结构和样本丢给小布,它能帮你跑前向和反向核对数值流向,省去手动debug的时间。
感知器只能分线性,实盘价格大多非线性,文里也说了它只是起点,真用得上多层前向加反向的结构。
类库是独立接口封装,直接include后实例化调前向方法即可,文末有最小调用示例可照抄。