您应当知道的 MQL5 向导技术(第 23 部分):CNNs·进阶篇
🧠

您应当知道的 MQL5 向导技术(第 23 部分):CNNs·进阶篇

(2/3)·从填充到反向传播,用MQL5类把图像分类思路压进EA信号层

含代码示例偏理论 第 2/3 篇
把CNN直接当黑箱塞进策略的人,常忽略填充如何决定特征映射边界。单层卷积的轮转乘法若没对齐维度,后续激活池化全走样。先弄清每一步矩阵怎么变,再谈集成才稳。

卷积矩阵为何要过一道激活

卷积层吐出的特征矩阵,必须再过一次激活函数,这和常规多层感知机里的做法一致。但在图像类输入里,激活的核心作用是把非线性映射能力塞进模型,否则连二次关系都抓不住。ReLU、leaky ReLU、Sigmoid、Tanh 是常见选项,其中 ReLU 因缓解梯度消失最被常用。 ReLU 的坑在于死神经元:当网络权重配上负值输入,输出被锁成常量,输入怎么变都无响应。这种现象甚至能被训练过程固化,引发权重翘曲,直接损失模型的表示能力,反向传播时梯度流变慢甚至停摆。 leaky ReLU 给负值输入留了一条小斜率,参数 alpha 为正且可优化,神经元就不会彻底死亡,仍对学习有贡献。相比标准 ReLU,它在反向传播里梯度更平滑,训练过程倾向更稳定、更高效。外汇与贵金属信号建模属高风险,任何特征提取改进都不承诺胜率。

◍ 卷积之后怎么压噪声:池化三型与步幅账

激活后的特征图带着卷积输出,直接进入池化来筛掉冗余噪声。池化本质是把特征图的高宽压下来,顺手减轻计算负载、少让网络纠缠无用参数,同时靠最少数据抓住每个特征图的关键属性,传递一定不变性。 常用就三种:最大池化在每个补片里挑最大值汇成新矩阵,支持者认为它保住大部分紧要属性、过拟合似然更低;平均池化算补片均值,更细更不激进,忽视紧要特征的概率较小;全局池化不卷积,直接把整张特征图压成单值,常挂在多层 CNN 最后一层,每个内核对准一个数值。 池化窗口和步幅决定输出大小。步幅大于 1 时池化矩阵明显小于原图;特征图大小与窗口大小成反比。较小池化数据能显著降低激活与内存需求。下面这段 MQL5 实现里,若特征图行列均大于 2,就生成缩小 2 行 2 列的池化矩阵,步幅实际为 1(逐点滑窗)。 [CODE] 逐行拆解:函数先校验网络 validated,否就打印无效并返回;pooling 不为 NONE 才进循环。对每个输出特征图 f,若行列 >2,建 _pooled 矩阵尺寸为原图减 2,填 0。最大池化时初值设 DBL_MIN,再双层遍历原图取 fmax;平均池化则累加后除以原图总元素数。POOLING_AVERAGE 分支在算完均值后才做除法归一。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Pool&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> Ccnn::Pool()
{&nbsp;&nbsp;<span class="keyword">if</span>(!validated)
&nbsp;&nbsp; {&nbsp;&nbsp;<span class="functions">printf</span>(<span class="keyword">__FUNCSIG__</span> + <span class="class="type">class="kw">string">" network invalid! "</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">if</span>(pooling != POOLING_NONE)
&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> f = <span class="number">class="num">0</span>; f &lt; <span class="keyword">class="type">int</span>(output.Size()); f++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">matrix</span> _pooled;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(output[f].Cols() &gt; <span class="number">class="num">2</span> &amp;&amp; output[f].Rows() &gt; <span class="number">class="num">2</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;_pooled.Init(output[f].Rows() - <span class="number">class="num">2</span>, output[f].Cols() - <span class="number">class="num">2</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_pooled.Fill(<span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> g = <span class="number">class="num">0</span>; g &lt; <span class="keyword">class="type">int</span>(_pooled.Cols()); g++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> h = <span class="number">class="num">0</span>; h &lt; <span class="keyword">class="type">int</span>(_pooled.Rows()); h++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">if</span>(pooling == POOLING_MAX)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;_pooled[h][g] = <span class="macro">DBL_MIN</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; <span class="keyword">class="type">int</span>(output[f].Cols()); i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> j = <span class="number">class="num">0</span>; j &lt; <span class="keyword">class="type">int</span>(output[f].Rows()); j++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">if</span>(pooling == POOLING_MAX)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;_pooled[h][g] = <span class="functions">fmax</span>(output[f][j][i], _pooled[h][g]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span> <span class="keyword">if</span>(pooling == POOLING_AVERAGE)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;_pooled[h][g] += output[f][j][i];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(pooling == POOLING_AVERAGE)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;_pooled[h][g] /= <span class="keyword">class="type">class="kw">double</span>(output[f].Cols()) * <span class="keyword">class="type">class="kw">double</span>(output[f].Rows());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

「把特征池拷进输出缓冲」

这段片段出现在特征抽取循环的内层,负责把临时池化结果移交到对外输出的数组里。 代码逐行看:最外层右大括号收掉前面的循环或条件;output[f].Copy(_pooled);_pooled 这个临时容器里的数据按引用或值拷贝进 output 的第 f 个槽位,f 通常对应某一特征维度。 内层两个右大括号依次关闭特征遍历体与外层作用域;最末右大括号收口整个函数或方法。 在 MT5 里跑这类逻辑时,先确认 output 数组已按特征数预分配,否则 output[f] 越界会直接抛 4002 数组越界错误。

MQL5 / C++
}
            output[f].Copy(_pooled);
         }
      }
   }
}

训练节奏与 Evolve 里的梯度回传

金融场景下的反向传播不是连续跑的,训练频率由模型自身决定。部分网络可设为每季度根据收益公报调一次权重与乖离,也有模型选在每月关键财经日历发布后重训;测试中用的 CNN 类输出矩阵数量为 3,分别对应看涨、看跌、窄幅震荡,输入是价格与各周期均线的间隙矩阵。 单次训练后永久免维护在网络里理论上可能,但实盘多数情况不现实。稳妥做法是给神经网络留一份训练日历,避免权重 stale 后信号失真——外汇与贵金属杠杆高,过期模型误判的代价会被放大。 误差增量天然是矩阵,需先乘激活函数导数才算真正梯度。因输出误差、梯度与内核权重尺寸不对齐,更新时复用了前馈的卷积逻辑:按步幅(本例为 1,须与前馈一致)在窗口上累加产物。乖离虽是单值,解法只是把梯度矩阵求和再乘旧乖离并按学习率缩放。 下面这段 Evolve 把三个标准动作——算误差、求梯度、更新权重乖离——全封进以 f 为索引的内核循环,最大次数不超内核数。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Evolve pass through the neural network to update kernel              |
class=class="str">"cmt">//| and biases using gradient descent                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Ccnn::Evolve(class="type">class="kw">double LearningRate = class="num">0.05)
{  if(!validated)
   {  printf(__FUNCSIG__ + " network invalid! ");
      class="kw">return;
   }

   for(class="type">int f = class="num">0; f < kernels; f++)
   {  matrix _output_error = target[f] - output[f];
      class=class="str">"cmt">// Calculate output layer gradients
      matrix _output_gradients;
      _output_gradients.Init(output[f].Rows(),output[f].Cols());
      for (class="type">int g = class="num">0; g < class="type">int(output[f].Rows()); g++)
      {  for (class="type">int h = class="num">0; h < class="type">int(output[f].Cols()); h++)
         {  _output_gradients[g][h] =  LeakyReLUDerivative(output[f][g][h]) * _output_error[g][h];
         }
      }

      class=class="str">"cmt">// Update output layer kernel weights and biases
      class="type">int _stride_row = class="num">0, _stride_col = class="num">0;
      for (class="type">int g = class="num">0; g < class="type">int(output[f].Cols()); g++)
      {  for (class="type">int h = class="num">0; h < class="type">int(output[f].Rows()); h++)
         {  class="type">class="kw">double _bias_sum = class="num">0.0;
            for (class="type">int i = class="num">0; i < class="type">int(kernel[f].weights.Cols()); i++)
            {  for (class="type">int j = class="num">0; j < class="type">int(kernel[f].weights.Rows()); j++)
               {  kernel[f].weights[j][i] += (LearningRate * _output_gradients[_stride_row + j][_stride_col + i]); class=class="str">"cmt">// output[f][_stride_row + j][_stride_col + i]);
                  _bias_sum += _output_gradients[_stride_row + j][_stride_col + i];
               }
            }
            kernel[f].bias += LearningRate * _bias_sum;
            _stride_col += padding_stride;
            if(_stride_col + class="type">int(kernel[f].weights.Cols()) > class="type">int(_output_gradients.Cols()))
            {  _stride_col = class="num">0;
               _stride_row += padding_stride;

◍ 卷积步长越界后的归零处理

在 MT5 里手写卷积反向传播时,行方向步长加卷积核权重行数一旦超过输出梯度矩阵的行数,就必须把行列步长同时重置为 0,否则会直接越界读矩阵导致 EA 崩溃。 上面这段判断逻辑嵌在多层循环末尾,实际跑下来若输入特征图 64 行、核高 3、步长 1,则第 62 行触发归零,循环重置后从首行重扫,这是卷积滑窗边界保护的硬约束。 外汇与贵金属行情序列用这类自定义卷积做特征提取时,高杠杆下模型误差会被放大,务必先在策略测试器用小周期样本验证矩阵维度,再上实盘。

MQL5 / C++
   if(_stride_row + class="type">int(kernel[f].weights.Rows()) > class="type">int(_output_gradients.Rows()))
   {  _stride_col = class="num">0;
      _stride_row = class="num">0;
   }

「把 CNN 塞进信号类的两个接口约定」

在自定义信号里调用 CNN 类,实质只需定清楚两件事:输入矩阵填什么、输出矩阵期待什么标签。输入侧已经明确——用当前收盘价减去一组移动平均线的值(默认 25 条不同周期 MA),通过 GetOutput 塞进输入矩阵。 输出侧不那么直白。我们想让模型映射出看涨或看跌,最简方案是只保留这两类标签,横盘可暂弃,但源码留了口子让你自己加。衡量方式是在每个输入数据点之后,追踪不同跨度的最高价变化(看涨)与最低价变化(看跌),分别写进两个输出矩阵。 第三个输出矩阵记的是横盘程度,用匹配跨度的收盘价波动幅度表达。每根新柱都捕获目标并重训,是一种高频做法;你也可以改成月度或季度重训以降低开销。 训练完做预测时,从三个输出矩阵各取中位数:看涨矩阵希望拿到较大正值,看跌矩阵希望较大负值,横盘矩阵则绝对值越小越平。GetOutput 返回的浮点若低于 0.5 偏空、高于 0.5 偏多。 实测单层 CNN、3 个 3x3 内核、5x5 输入、EURJPY 日线填充 3x3 输出,输出值紧贴 ±0.5。外汇与贵金属属高风险品种,该数值仅说明此配置下多空信号边界模糊,实盘须自行验证。

MQL5 / C++
class="type">class="kw">double CSignalCNN::GetOutput()
{  class="type">int _index = class="num">5;
   matrix _inputs;
   vector _ma, _h, _l, _c;
   _inputs.Init(m_input_size, m_input_size);
   for(class="type">int g = class="num">0; g < m_epochs; g++)
   {  for(class="type">int h = m_train_set - class="num">1; h >= class="num">0; h--)
      {  _inputs.Fill(class="num">0.0);
         _index = class="num">0;
         for(class="type">int i = class="num">0; i < m_input_size; i++)
         {  for(class="type">int j = class="num">0; j < m_input_size; j++)
            {  if(_ma.CopyIndicatorBuffer(m_ma[_index].Handle(), class="num">0, h, __KERNEL + class="num">1))
               {  _inputs[i][j] = _c[class="num">0] - _ma[class="num">0];
                  _index++;
               }
            }
         }
         class=class="str">"cmt">//
         ...
      }
   }
      ...

}
class=class="str">"cmt">// 另一段同函数节选
class="type">class="kw">double CSignalCNN::GetOutput()
{ 
   ...
   for(class="type">int g = class="num">0; g < m_epochs; g++)
   {  for(class="type">int h = m_train_set - class="num">1; h >= class="num">0; h--)
      {  _inputs.Fill(class="num">0.0);
         _index = class="num">0;
         ...
         class=class="str">"cmt">//
         _h.CopyRates(m_symbol.Name(), m_period, class="num">2, h, __KERNEL + class="num">1);
         _l.CopyRates(m_symbol.Name(), m_period, class="num">4, h, __KERNEL + class="num">1);
         _c.CopyRates(m_symbol.Name(), m_period, class="num">8, h, __KERNEL + class="num">1);
让小布替你跑这套
这些CNN步骤的诊断与信号类组装,小布盯盘的AIGC已内置,打开对应品种页即可看到矩阵维度校验,你把精力留给决策本身。

常见问题

CNN通过内核轮转把输入拆成特征映射,本质捕获关键属性做子集分类;不像GAN从假造辨真,它不重建数据,只切分属性,故偏分类器。
不是必选。填充沿输入边缘加边界,主要抵消前馈时内核轮转导致的维度收缩,若容忍输出变小可跳过,但单层映射会丢失边缘信息。
可以。小布内置的AIGC会把自定义信号类的矩阵运算做维度检查,你导入EA后就能在品种页看卷积层输出是否符合预期。
单层只跑一次填充前馈激活池化;多层中每层的四个步骤重复,高层特征映射会继续拆为新映射,池化压缩的是逐级抽象后的子集。
本文的反向传播侧重演化思路,未训练时它用于按误差微调内核权重雏形,让前馈输出的特征映射逐步贴近设定分类边界。