神经网络轻松制作·进阶篇
(2/3)· 上篇铺垫了概念,这篇直接拆开网络训练与 MQL 代码骨架,少谈玄学多谈接线
不少交易者把神经网络当成黑箱神谕,以为调几个参数就能让模型替自己预测涨跌。实际上没理清输入层该接几个价格字段、隐藏层为何容易过拟合,写进 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 确认分布散开。
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 不落盘——这意味着重载模型后学习率要代码内重设。
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 上回测收敛步数可能有明显差异,但外汇高杠杆品种请先开模拟盘验证风险。
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 做离线回测,参数错了模型可能完全不收敛。
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);