您应当知道的 MQL5 向导技术(第 23 部分):CNNs·进阶篇
(2/3)·从填充到反向传播,用MQL5类把图像分类思路压进EA信号层
卷积矩阵为何要过一道激活
卷积层吐出的特征矩阵,必须再过一次激活函数,这和常规多层感知机里的做法一致。但在图像类输入里,激活的核心作用是把非线性映射能力塞进模型,否则连二次关系都抓不住。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 分支在算完均值后才做除法归一。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Pool |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> Ccnn::Pool() { <span class="keyword">if</span>(!validated) { <span class="functions">printf</span>(<span class="keyword">__FUNCSIG__</span> + <span class="class="type">class="kw">string">" network invalid! "</span>); <span class="keyword">class="kw">return</span>; } <span class="keyword">if</span>(pooling != POOLING_NONE) { <span class="keyword">for</span>(<span class="keyword">class="type">int</span> f = <span class="number">class="num">0</span>; f < <span class="keyword">class="type">int</span>(output.Size()); f++) { <span class="keyword">matrix</span> _pooled; <span class="keyword">if</span>(output[f].Cols() > <span class="number">class="num">2</span> && output[f].Rows() > <span class="number">class="num">2</span>) { _pooled.Init(output[f].Rows() - <span class="number">class="num">2</span>, output[f].Cols() - <span class="number">class="num">2</span>); _pooled.Fill(<span class="number">class="num">0.0</span>); <span class="keyword">for</span> (<span class="keyword">class="type">int</span> g = <span class="number">class="num">0</span>; g < <span class="keyword">class="type">int</span>(_pooled.Cols()); g++) { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> h = <span class="number">class="num">0</span>; h < <span class="keyword">class="type">int</span>(_pooled.Rows()); h++) { <span class="keyword">if</span>(pooling == POOLING_MAX) { _pooled[h][g] = <span class="macro">DBL_MIN</span>; } <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < <span class="keyword">class="type">int</span>(output[f].Cols()); i++) { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> j = <span class="number">class="num">0</span>; j < <span class="keyword">class="type">int</span>(output[f].Rows()); j++) { <span class="keyword">if</span>(pooling == POOLING_MAX) { _pooled[h][g] = <span class="functions">fmax</span>(output[f][j][i], _pooled[h][g]); } <span class="keyword">else</span> <span class="keyword">if</span>(pooling == POOLING_AVERAGE) { _pooled[h][g] += output[f][j][i]; } } } <span class="keyword">if</span>(pooling == POOLING_AVERAGE) { _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()); } }
「把特征池拷进输出缓冲」
这段片段出现在特征抽取循环的内层,负责把临时池化结果移交到对外输出的数组里。
代码逐行看:最外层右大括号收掉前面的循环或条件;output[f].Copy(_pooled); 把 _pooled 这个临时容器里的数据按引用或值拷贝进 output 的第 f 个槽位,f 通常对应某一特征维度。
内层两个右大括号依次关闭特征遍历体与外层作用域;最末右大括号收口整个函数或方法。
在 MT5 里跑这类逻辑时,先确认 output 数组已按特征数预分配,否则 output[f] 越界会直接抛 4002 数组越界错误。
}
output[f].Copy(_pooled);
}
}
}
}训练节奏与 Evolve 里的梯度回传
金融场景下的反向传播不是连续跑的,训练频率由模型自身决定。部分网络可设为每季度根据收益公报调一次权重与乖离,也有模型选在每月关键财经日历发布后重训;测试中用的 CNN 类输出矩阵数量为 3,分别对应看涨、看跌、窄幅震荡,输入是价格与各周期均线的间隙矩阵。 单次训练后永久免维护在网络里理论上可能,但实盘多数情况不现实。稳妥做法是给神经网络留一份训练日历,避免权重 stale 后信号失真——外汇与贵金属杠杆高,过期模型误判的代价会被放大。 误差增量天然是矩阵,需先乘激活函数导数才算真正梯度。因输出误差、梯度与内核权重尺寸不对齐,更新时复用了前馈的卷积逻辑:按步幅(本例为 1,须与前馈一致)在窗口上累加产物。乖离虽是单值,解法只是把梯度矩阵求和再乘旧乖离并按学习率缩放。 下面这段 Evolve 把三个标准动作——算误差、求梯度、更新权重乖离——全封进以 f 为索引的内核循环,最大次数不超内核数。
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 行触发归零,循环重置后从首行重扫,这是卷积滑窗边界保护的硬约束。 外汇与贵金属行情序列用这类自定义卷积做特征提取时,高杠杆下模型误差会被放大,务必先在策略测试器用小周期样本验证矩阵维度,再上实盘。
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。外汇与贵金属属高风险品种,该数值仅说明此配置下多空信号边界模糊,实盘须自行验证。
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);