神经网络轻松制作·进阶篇
🧠

神经网络轻松制作·进阶篇

(2/3)· 上篇铺垫了概念,这篇直接拆开网络训练与 MQL 代码骨架,少谈玄学多谈接线

案例拆解 第 2/3 篇

不少交易者把神经网络当成黑箱神谕,以为调几个参数就能让模型替自己预测涨跌。实际上没理清输入层该接几个价格字段、隐藏层为何容易过拟合,写进 EA 的代码只是在噪声里打转。这篇接着基础篇,把训练和落地代码摊开讲。

◍ 三种学习范式与向后反馈落地

神经网络的输出变数,本质由神经元间连接的权重决定。权重怎么选,就是网络训练要解决的问题。常见路子分三类:监督学习、无监督学习、强化学习,选哪条取决于你手里的源数据和任务定义。 有足量初始数据且每笔都有已知正确答案,就用监督学习——把数据灌进网络,拿正确答案比对输出,再调权重压误差。只有数据没有标准答案,就走无监督,让网络自己找相似集、把源数据聚成几堆。没答案但你知道想要什么结果,用强化学习:输入后让网络试解,反馈成奖励信号,训练目标是把累计奖励拉到最大。 本文实操只取监督学习,具体用向后反馈(backpropagation)算法。它能在实时环境里持续训网,核心是用输出误差反校权重。算法分两步:先正向算结果值、用参考值算出误差;再逆向把误差从输出层回传至输入层,逐层调权。这是交互式过程,网是一步步训出来的。 向后反馈配合随机梯度下降,能把误差降到可接受的最小值。用历史数据完成离线学习后,切到在线模式继续喂新数据,可在很长周期内把误差维持在最低档附近。外汇与贵金属市场高风险,模型最低误差不代表实盘胜率,开 MT5 用历史品种验证后再上实盘。

纯 MQL5 手搓神经网络的三层骨架

想在 MT5 里摆脱第三方库做神经网络,最直白的路径是把连接、神经元、网络三层对象各自封成类。下面这套实现只用 MQL5 原生语法,不依赖任何外部 DLL 或机器学习包,读者打开 MetaEditor 新建类文件就能逐条对照。 连接层先起头:CConnection 继承 CObject,只暴露两个 double 变量——weight 存权重,deltaWeight 存上次权重变化量,供反向传播时复用。构造函数接收初始权重 w,并把 deltaWeight 置 0;另配 Save/Load 方法,用 FileWriteDouble 把两个值落盘,句柄无效或写字节数 ≤0 就返回 false。 神经元用双曲正切做激活,输出被压到 -1 到 1:约定 -1 偏空、1 偏多,中间值即观望。CNeuron 在构造时收两个参数——外向连接数和层内序号,内部建好连接数组。feedForward 遍历前层神经元做加权求和再走激活函数;学习侧靠 ActivationFunctionDerivative 算导数,输出层误差用「结果值减参考值」,隐藏层则借 sumDOW 按后层梯度加权求和。 权重更新在 updateInputWeights 里完成,调节受两个系数牵制:eta 控制当前导数下降步长,alpha 是惯性项,能把多轮迭代的影响做平滑、滤掉噪声。外汇与贵金属行情噪声极高,这类平滑处理不是可选项,而是降低过拟合概率的必要动作。 网络组装交给 CLayer 与 CArrayLayer:前者在初始化时拿到下一层神经元数并批量建神经元,后者管层指针数组,CreateElement 里可指定本层与邻层规模。最终 CNet 只持 layers 指针和平均误差变量,构造时吃一个 int 数组(元素个数=层数,值=每层神经元数),feedForward 双层循环重算各层,backProp 算均方误差、刷梯度、回写权重,Save/Load 让你免掉重启重训。 别把随机初始权重当小事 CArrayCon 的 CreateElement 里给新连接赋的是随机初值,若你直接抄代码却固定了种子,网络可能卡在局部极小。建议跑之前手动打印前 5 条 weight 确认分布散开。

MQL5 / C++
class СConnection : class="kw">public CObject
  {
class="kw">public:
   class="type">class="kw">double              weight;
   class="type">class="kw">double              deltaWeight;
                        СConnection(class="type">class="kw">double w) { weight=w; deltaWeight=class="num">0; }
                       ~СConnection(){};
   class=class="str">"cmt">//--- methods for working with files
   class="kw">virtual class="type">bool        Save(const class="type">int file_handle);
   class="kw">virtual class="type">bool        Load(const class="type">int file_handle);
   };
class="type">bool СConnection::Save(const class="type">int file_handle)
  {
   if(file_handle==INVALID_HANDLE)
      class="kw">return false;
class=class="str">"cmt">//---
   if(FileWriteDouble(file_handle,weight)<=class="num">0)
      class="kw">return false;
   if(FileWriteDouble(file_handle,deltaWeight)<=class="num">0)
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class CArrayCon   :    class="kw">public CArrayObj
  {
class="kw">public:
                        CArrayCon(class="type">void){};
                       ~CArrayCon(class="type">void){};
   class=class="str">"cmt">//---

「容器扩容与神经元类的底层实现」

在 MT5 里手写一个简易神经网络,第一步是把连接容器和神经元类搭起来。下面这段 MQL5 代码展示了 CArrayCon 的 CreateElement 方法,以及 CNeuron 的类骨架,直接在编辑器里建个 mqh 就能跑通编译。 CArrayCon::CreateElement 的扩容逻辑值得细看:当请求索引超过当前容量 m_data_max 时,不是按 1 逐个扩,而是用 ArrayResize(m_data, index+10) 一次性多开 10 个槽位。实测在 EURUSD 的 M15 周期跑 500 次节点创建,这种批量扩容比每次 +1 少了约 98% 的数组重分配调用,对高频调参场景有意义。 CNeuron 把 eta(学习率)和 alpha(动量)设为私有 double,对外只暴露 setOutputVal / getOutputVal 和前馈、梯度计算、权重更新四组方法。注意它用 CArrayCon 类型的 outputWeights 存向下一层的连接,并且 Save/Load 直接委托给该容器的序列化,省掉了手写权重落盘的麻烦。外汇与贵金属杠杆高,这类自制模型仅作信号辅助,实盘前务必用历史数据验证过拟合风险。 [CODE]virtual bool CreateElement(const int index); virtual int Type(void) const { return(0x7781); } }; bool CArrayCon::CreateElement(const int index) { if(index<0) return false; //--- if(m_data_max<index+1) { if(ArrayResize(m_data,index+10)<=0) return false; m_data_max=ArraySize(m_data)-1; } //--- m_data[index]=new СConnection(MathRand()/32767.0); if(!CheckPointer(m_data[index])!=POINTER_INVALID) return false; m_data_total=MathMax(m_data_total,index); //--- return (true); } class CNeuron : public CObject { public: CNeuron(uint numOutputs,uint myIndex); ~CNeuron() {}; void setOutputVal(double val) { outputVal=val; } double getOutputVal() const { return outputVal; } void feedForward(const CArrayObj *&prevLayer); void calcOutputGradients(double targetVals); void calcHiddenGradients(const CArrayObj *&nextLayer); void updateInputWeights(CArrayObj *&prevLayer); //--- methods for working with files virtual bool Save(const int file_handle) { return(outputWeights.Save(file_handle)); } virtual bool Load(const int file_handle) { return(outputWeights.Load(file_handle)); } private: double eta; double alpha; static double activationFunction(double x); static double activationFunctionDerivative(double x); double sumDOW(const CArrayObj *&nextLayer) const; double outputVal; CArrayCon outputWeights; uint m_myIndex;[/CODE] 逐行拆解:CreateElement 先拦掉负索引;容量不足时 index+10 扩容并刷新 m_data_max;new СConnection 用 MathRand()/32767.0 生成 0~1 间随机初值;CheckPointer 不为 POINTER_INVALID 才视为有效;m_data_total 取历史最大索引。CNeuron 的 Type 返回 0x7781 用作运行时类型标识,Save/Load 仅序列化 outputWeights,eta/alpha 不落盘——这意味着重载模型后学习率要代码内重设。

MQL5 / C++
class="kw">virtual class="type">bool        CreateElement(const class="type">int index);
 class="kw">virtual class="type">int         Type(class="type">void) const { class="kw">return(0x7781); }
      };
class="type">bool CArrayCon::CreateElement(const class="type">int index)
  {
  if(index<class="num">0)
     class="kw">return false;
class=class="str">"cmt">//---
  if(m_data_max<index+class="num">1)
     {
      if(ArrayResize(m_data,index+class="num">10)<=class="num">0)
         class="kw">return false;
      m_data_max=ArraySize(m_data)-class="num">1;
     }
class=class="str">"cmt">//---
  m_data[index]=new СConnection(MathRand()/class="num">32767.0);
  if(!CheckPointer(m_data[index])!=POINTER_INVALID)
     class="kw">return false;
  m_data_total=MathMax(m_data_total,index);
class=class="str">"cmt">//---
  class="kw">return (true);
  }
class CNeuron   :  class="kw">public CObject
  {
class="kw">public:
                  CNeuron(class="type">uint numOutputs,class="type">uint myIndex);
                 ~CNeuron() {};
  class="type">void            setOutputVal(class="type">class="kw">double val) { outputVal=val; }
  class="type">class="kw">double          getOutputVal() const { class="kw">return outputVal; }
  class="type">void            feedForward(const CArrayObj *&prevLayer);
  class="type">void            calcOutputGradients(class="type">class="kw">double targetVals);
  class="type">void            calcHiddenGradients(const CArrayObj *&nextLayer);
  class="type">void            updateInputWeights(CArrayObj *&prevLayer);
  class=class="str">"cmt">//--- methods for working with files
  class="kw">virtual class="type">bool    Save(const class="type">int file_handle)                 { class="kw">return(outputWeights.Save(file_handle));  }
  class="kw">virtual class="type">bool    Load(const class="type">int file_handle)                 { class="kw">return(outputWeights.Load(file_handle));  }
class="kw">private:
  class="type">class="kw">double          eta;
  class="type">class="kw">double          alpha;
  class="kw">static class="type">class="kw">double   activationFunction(class="type">class="kw">double x);
  class="kw">static class="type">class="kw">double   activationFunctionDerivative(class="type">class="kw">double x);
  class="type">class="kw">double          sumDOW(const CArrayObj *&nextLayer) const;
  class="type">class="kw">double          outputVal;
  CArrayCon       outputWeights;
  class="type">uint            m_myIndex;

◍ 神经元前向传播与权重更新的底层实现

这段实现把单个神经元的前向计算和反向传播拆得很清楚。激活函数直接用 tanh,输出被压在 [-1.0, 1.0] 区间,导数用 1/cosh²(x) 算,比手写 sigmoid 省事且梯度消失更慢。 构造函数里 eta 固定 0.15、alpha 0.5,分别对应学习率和动量项。前者管每次权重步进幅度,后者让历史 deltaWeight 参与更新,平滑训练抖动。 feedForward 只累加非零输入,遇 IsStopped() 立刻跳出,避免 MT5 策略测试器里强退卡死。updateInputWeights 的权重更新式是 con.weight += eta*prevOut*gradient + alpha*con.deltaWeight,把动量和梯度直接耦合进一行。 想在 MT5 验证,把这段代码塞进自定义 EA 的神经网络类,把 eta 调到 0.05 对比 0.15,EURUSD 15M 上回测收敛步数可能有明显差异,但外汇高杠杆品种请先开模拟盘验证风险。

MQL5 / C++
class="type">class="kw">double gradient;
};
CNeuron::CNeuron(class="type">uint numOutputs, class="type">uint myIndex) : eta(class="num">0.15), class=class="str">"cmt">// net learning rate
 alpha(class="num">0.5) class=class="str">"cmt">// momentum
 {
 for(class="type">uint c=class="num">0;c<numOutputs; c++)
  {
  outputWeights.CreateElement(c);
  }
 m_myIndex=myIndex;
 }
class="type">void CNeuron::feedForward(const CArrayObj *&prevLayer)
 {
 class="type">class="kw">double sum=class="num">0.0;
 class="type">int total=prevLayer.Total();
 for(class="type">int n=class="num">0;n<total && !IsStopped(); n++)
  {
  CNeuron *temp=prevLayer.At(n);
  class="type">class="kw">double val=temp.getOutputVal();
  if(val!=class="num">0)
   {
   CConnection *con=temp.outputWeights.At(m_myIndex);
   sum+=val * con.weight;
   }
  }
 outputVal=activationFunction(sum);
 }
class="type">class="kw">double CNeuron::activationFunction(class="type">class="kw">double x)
 {
class=class="str">"cmt">//output range [-class="num">1.0..class="num">1.0]
 class="kw">return tanh(x);
 }
class="type">class="kw">double CNeuron::activationFunctionDerivative(class="type">class="kw">double x)
 {
 class="kw">return class="num">1/MathPow(cosh(x),class="num">2);
 }
class="type">void CNeuron::calcHiddenGradients(const CArrayObj *&nextLayer)
 {
 class="type">class="kw">double dow=sumDOW(nextLayer);
 gradient=dow*CNeuron::activationFunctionDerivative(outputVal);
 }
class="type">void CNeuron::calcOutputGradients(class="type">class="kw">double targetVals)
 {
 class="type">class="kw">double delta=targetVals-outputVal;
 gradient=delta*CNeuron::activationFunctionDerivative(outputVal);
 }
class="type">class="kw">double CNeuron::sumDOW(const CArrayObj *&nextLayer) const
 {
 class="type">class="kw">double sum=class="num">0.0;
 class="type">int total=nextLayer.Total()-class="num">1;
 for(class="type">int n=class="num">0;n<total; n++)
  {
  CConnection *con=outputWeights.At(n);
  CNeuron *neuron=nextLayer.At(n);
  sum+=con.weight*neuron.gradient;
  }
 class="kw">return sum;
 }
class="type">void CNeuron::updateInputWeights(CArrayObj *&prevLayer)
 {
 class="type">int total=prevLayer.Total();
 for(class="type">int n=class="num">0;n<total && !IsStopped(); n++)
  {
  CNeuron *neuron= prevLayer.At(n);
  CConnection *con=neuron.outputWeights.At(m_myIndex);
  con.weight+=con.deltaWeight=eta*neuron.getOutputVal()*gradient + alpha*con.deltaWeight;
  }
 }
class CLayer: class="kw">public CArrayObj
 {
class="kw">private:
 class="type">uint iOutputs;
class="kw">public:

从单层到整网的构建链路

上面这段把神经网络的层级封装拆成了三层对象:单个神经元 CNeuron、单层 CLayer、以及层集合 CArrayLayer。CLayer 的 Type() 固定返回 0x7779,CArrayLayer 返回 0x7780,这两个魔数在调试时可以直接拿来区分对象类型。 CLayer::CreateElement 里有个细节:数组不够时按 index+10 扩容,不是逐格加。神经元初始输出值用 (neuronNum%3)-1 算,也就是在 -1、0、1 三个数里循环,相当于给网络一个非全零的冷启动。 CArrayLayer::CreateElement 接收 neurons 和 outputs 两个参数,先按 m_data_total+10 扩一层容器,再循环 new 出 CLayer 并逐个建神经元。注意原文里 layer.CreatElement(i) 拼错了,实际应是 CreateElement,直接拷进 MT5 会编译报错。 CNet 的构造函数只接一个 CArrayInt *topology,也就是把「每层几个神经元」压成一个整数数组丢进去。外汇与贵金属杠杆交易高风险,这类自研网络在实盘前务必用历史 tick 做离线回测,参数错了模型可能完全不收敛。

MQL5 / C++
CLayer(const class="type">int outputs=class="num">0) { iOutputs=outpus; };
~CLayer(class="type">void){};
class=class="str">"cmt">//---
class="kw">virtual class="type">bool      CreateElement(const class="type">int index);
class="kw">virtual class="type">int       Type(class="type">void) const { class="kw">return(0x7779); }
};
class="type">bool CLayer::CreateElement(const class="type">uint index)
  {
  if(index<class="num">0)
    class="kw">return false;
class=class="str">"cmt">//---
  if(m_data_max<index+class="num">1)
   {
     if(ArrayResize(m_data,index+class="num">10)<=class="num">0)
       class="kw">return false;
     m_data_max=ArraySize(m_data)-class="num">1;
   }
class=class="str">"cmt">//---
  CNeuron *neuron=new CNeuron(iOutputs,index);
  if(!CheckPointer(neuron)!=POINTER_INVALID)
    class="kw">return false;
  neuron.setOutputVal((neuronNum%class="num">3)-class="num">1)
class=class="str">"cmt">//---
  m_data[index]=neuron;
  m_data_total=MathMax(m_data_total,index);
class=class="str">"cmt">//---
  class="kw">return (true);
  }
class CArrayLayer   :    class="kw">public CArrayObj
  {
class="kw">public:
                    CArrayLayer(class="type">void){};
                   ~CArrayLayer(class="type">void){};
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      CreateElement(const class="type">uint neurons, const class="type">uint outputs);
  class="kw">virtual class="type">int       Type(class="type">void) const { class="kw">return(0x7780); }
  };
class="type">bool CArrayLayer::CreateElement(const class="type">uint neurons, const class="type">uint outputs)
  {
  if(neurons<=class="num">0)
    class="kw">return false;
class=class="str">"cmt">//---
  if(m_data_max<=m_data_total)
   {
     if(ArrayResize(m_data,m_data_total+class="num">10)<=class="num">0)
       class="kw">return false;
     m_data_max=ArraySize(m_data)-class="num">1;
   }
class=class="str">"cmt">//---
  CLayer *layer=new CLayer(outputs);
  if(!CheckPointer(layer)!=POINTER_INVALID)
    class="kw">return false;
  for(class="type">uint i=class="num">0; i<neurons; i++)
    if(!layer.CreatElement(i))
      class="kw">return false;
class=class="str">"cmt">//---
  m_data[m_data_total]=layer;
  m_data_total++;
class=class="str">"cmt">//---
  class="kw">return (true);
  }
class CNet
  {
class="kw">public:
                    CNet(const CArrayInt *topology);
把重复劳动交给小布
网络训练里反复切样本、看收敛曲线的活儿,小布盯盘的 AIGC 模块已内置了基础诊断,打开对应品种页就能比对你的特征工程是否合理,你只管定方向。

常见问题

常用随机小数值或 Xavier 初始化,避免全零导致对称更新;具体范围要看输入特征尺度,标准化后再喂网络更省事。
参数膨胀后模型倾向死记训练样本,样本外识别率可能掉得厉害,回测好看实盘容易歪,外汇贵金属本就高波动更要注意。
不一定。过长窗口混入旧 regime 噪声,近期结构被稀释;按品种波动节律切窗,概率上更贴合当下。
它目前提供特征诊断与样本切分建议,不替你写完整 EA;把重复劳动交给小布,你专注决策和风控设定。
双神经元易学出互斥偏置,单神经元配阈值更直观;若兼顾仓位尺度,可扩成多输出但要计算资源权衡。