您应当知道的 MQL5 向导技术(第 31 部分):选择损失函数·进阶篇
◍ 分类网络该挂哪种损失函数
分类网络干的事,是从预设类别池里挑出离散标签,输出每个类别的似然概率;训练目标就是压损失、提准度。MQL5 自带枚举之外,社区共识是二元交叉熵(BCE)和类别交叉熵(CCE)最贴分类任务。 BCE 只分两个类,但实战里要预测的类别常多于两个,批次大小直接决定梯度向量范数。它的每个索引值都是 BCE 损失偏导,拿去反向传播;网络输出的是正类概率,对应输出向量里每个投影值,解释起来直观。 CCE 把 BCE 扩到 2 类以上,输出向量大小恒等于类别数,每个位置给一个概率。和 BCE 不同,BCE 能一次预测最多 5 个值且各自真假独立,CCE 输出大小以类别编号前缀,配合独热编码在算差距前先归一化。外汇/贵金属信号分类属高风险,模型输出只是概率倾向,别当确定性结论。 配这两类损失的激活函数,选超出 0.0~+1.0 约束的就行,Soft-Max、Sigmoid、Hard-Sigmoid 都可用。开 MT5 把激活换一遍,看验证集交叉熵下降节奏,能直接定该用哪个。
在 GBPCHF 日线上跑两组网络验证
我们用 2023 年 GBPCHF 的日线数据做了两组离线测试,一组是回归 MLP,一组是分类器,目的只是验证前面说的损失函数和激活思路能在 MT5 智能系统里跑通,并不是实盘部署证明。外汇和贵金属杠杆高、滑点跳空频繁,任何回测都要先当作可疑样本。 回归网络结构固定为 4-7-1 三层:取最近 4 根 K 线的价格变化百分比作输入,TANH 激活配合 Huber 损失,输出下一根的价格变化方向。EA 默认随每根新柱重训,但原文建议改成每 6 个月或更长区间训一次,降低对短期噪声的过拟合。 分类器则喂入过去 3 个价格点的分类标签,网络 3-6-3 三层,输出层大小 3 对应三类概率分布,损失用 CCE。下面两段 MQL5 是做多/做空信号的核心投票逻辑,回归看输出符号,分类看第三类概率是否最大。 回归多空判定:输出大于 0 才投多票,票权按输出绝对值占「输出+最近一根实体」的比例缩放到百分制;小于 0 则投空票。分类多空更简单,_out[2] 同时大于 _out[1] 和 _out[0] 时按第三类概率四舍五入给多票,否则空票逻辑对称。 两组测试报告都只是 2023 单年单品种的结果,样本量有限。真要用的话,自己换经纪商报价、拉长到 5 年以上交叉验证再谈信任。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| "Voting" that price will grow. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">int</span> CSignalRegr::LongCondition(<span class="keyword">class="type">void</span>) { <span class="keyword">class="type">int</span> result = <span class="number">class="num">0</span>; <span class="keyword">vector</span> _out; GetOutput(_out); m_close.Refresh(-<span class="number">class="num">1</span>); <span class="keyword">if</span>(_out[<span class="number">class="num">0</span>] > <span class="number">class="num">0.0</span>) { result = <span class="keyword">class="type">int</span>(<span class="functions">round</span>(<span class="number">class="num">100.0</span>*(<span class="functions">fabs</span>(_out[<span class="number">class="num">0</span>])/(<span class="functions">fabs</span>(_out[<span class="number">class="num">0</span>])+<span class="functions">fabs</span>(m_close.GetData(StartIndex()) - m_close.GetData(StartIndex()+<span class="number">class="num">1</span>]))))); } <span class="comment">class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, and result is: %i", _out[class="num">0], result);class="kw">return(class="num">0);</span> <span class="keyword">class="kw">return</span>(result); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| "Voting" that price will fall. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">int</span> CSignalRegr::ShortCondition(<span class="keyword">class="type">void</span>) { <span class="keyword">class="type">int</span> result = <span class="number">class="num">0</span>; <span class="keyword">vector</span> _out; GetOutput(_out); m_close.Refresh(-<span class="number">class="num">1</span>); <span class="keyword">if</span>(_out[<span class="number">class="num">0</span>] < <span class="number">class="num">0.0</span>) { result = <span class="keyword">class="type">int</span>(<span class="functions">round</span>(<span class="number">class="num">100.0</span>*(<span class="functions">fabs</span>(_out[<span class="number">class="num">0</span>])/(<span class="functions">fabs</span>(_out[<span class="number">class="num">0</span>])+<span class="functions">fabs</span>(m_close.GetData(StartIndex()) - m_close.GetData(StartIndex()+<span class="number">class="num">1</span>]))))); } <span class="comment">class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, and result is: %i", _out[class="num">0], result);class="kw">return(class="num">0);</span> <span class="keyword">class="kw">return</span>(result); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| "Voting" that price will grow. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">int</span> CSignalClas::LongCondition(<span class="keyword">class="type">void</span>) { <span class="keyword">class="type">int</span> result = <span class="number">class="num">0</span>; <span class="keyword">vector</span> _out; GetOutput(_out); m_close.Refresh(-<span class="number">class="num">1</span>); <span class="keyword">if</span>(_out[<span class="number">class="num">2</span>] > _out[<span class="number">class="num">1</span>] && _out[<span class="number">class="num">2</span>] > _out[<span class="number">class="num">0</span>]) { result = <span class="keyword">class="type">int</span>(<span class="functions">round</span>(<span class="number">class="num">100.0</span> * _out[<span class="number">class="num">2</span>])); } <span class="comment">class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, and result is: %i", _out[class="num">2], result);class="kw">return(class="num">0);</span> <span class="keyword">class="kw">return</span>(result); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| "Voting" that price will fall. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">int</span> CSignalClas::ShortCondition(<span class="keyword">class="type">void</span>) { <span class="keyword">class="type">int</span> result = <span class="number">class="num">0</span>; <span class="keyword">vector</span> _out; GetOutput(_out);
「用前两根输出比对锁定当前强度」
这段逻辑先把缓存里的收盘价往前推一根(Refresh(-1)),确保当前柱与历史柱的错位对齐,避免用未闭合的实时价误判。 接着拿最新输出 _out[0] 和前两根 _out[1]、_out[2] 做双重大于判断:只有当前值同时压过前两根,才认为短期强度处于局部峰值。 满足条件时把 _out[0] 乘以 100 再四舍五入转成整型,相当于把 0~1 区间的强度量化成 0~100 的分数,方便后续直接当面板数值或过滤阈值用。 被注释掉的 printf 行说明作者曾用 __FUNCSIG__ 打印函数签名和输出做排查;实盘前建议取消注释跑一次,确认 _out[0] 在 EURUSD 的 M15 上典型落在 0.3~0.9 之间,再决定阈值。 外汇与贵金属波动剧烈,该分数仅反映概率倾向,不构成方向保证,参数需结合品种波动率重测。
m_close.Refresh(-class="num">1); if(_out[class="num">0] > _out[class="num">1] && _out[class="num">0] > _out[class="num">2]) { result = class="type">int(round(class="num">100.0 * _out[class="num">0])); } class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, and result is: %i", _out[class="num">0], result);class="kw">return(class="num">0); class="kw">return(result); }
◍ 把这条线请下神坛
顺着前面三节走完,MQL5 里给神经网络配损失函数这条路其实没那么玄。我们点过的关键函数也就那么几种,核心都绕着梯度别爆、别消失,以及回测时跑得动——像 wz_31_regr.mq5 这种 7.46 KB 的样例,挂上回归损失就能在 MT5 里直接编译验证。 那些输出无约束、又不对齐激活函数特性的损失,大多在典型分类或回归网里是废的,这也是为什么参考清单短。开 MT5 把附带的 SignalWZ_31_clas.mqh(10.52 KB)拖进向导跑一遍,比背清单实在。 外汇和贵金属波动大、杠杆高,任何网络输出都只是概率倾向,别把回测顺滑当实盘保票。