MQL5 向导中的 CNN 基础:单层卷积网络从填充到前馈的拆解(基础篇)
(1/3)· 多数交易者只把 CNN 当黑箱,却不知填充与前馈如何决定特征映射的边界与信息损失
用 MQL5 向导搭一个 CNN 预测模块
MQL5 向导从 build 4410 起支持通过『深度网络』模板插入卷积层,交易者不必手写全部张量运算,也能在 MT5 策略测试器里跑图像化的行情特征提取。 实际落点很直接:把每根 K 线周围 N 根的高低收,重采样成单通道矩阵,送进 1D 卷积做局部形态捕捉,比单纯用均线交叉多一层非线性。 测试者 Stephen Njuki 在 2025-03-04 发布的样例显示,EURUSD 的 H1 上用 3 层卷积做方向分类,样本外 3 个月回测准确率约 58%,外汇品种受杠杆与滑点影响大,高波动期可能明显回撤,仅作技术验证。
「CNN 在 MQL5 里到底转了什么」
卷积神经网络(CNN)这个名字来自它的核心动作:内核矩阵在输入数据上轮转。内核承载网络权重,维度比输入小,前馈时沿输入矩阵滑动,每次迭代做一次卷积运算,这正是“卷积”二字的由来。 在 MQL5 向导环境里做 CNN 原型有个实在好处:信号类可直接挂进专家顾问跑测试,不用自己搭整套回测框架。本文打算用单层 CNN 把流程走通——填充、前馈、激活、池化这四步,多层网络只是每层重复这套动作。 单层意味着输入只被一组内核处理一次;多层时,上一层产出的特征映射会继续拆成新的特征映射,逐层捕获更细的属性。CNN 本质是个分类器,把像素或数值矩阵切成子集,每个子集对应输入里的某种关键特征,这和 GAN 从真假图里辨物不是一回事。 外汇与贵金属市场高风险,用 CNN 信号类做原型只代表一种概率倾向,实盘前务必在 MT5 用历史数据验证过拟合情况。
◍ 卷积边缘补数据这步到底图什么
把 CNN 搬到 MT5 做行情特征提取时,第一步常遇到「卷积完尺寸缩水」的问题。拿 6×6 的输入矩阵配 3×3 权重核来说,一次卷积直接产出 4×4 特征图——比原数据少了两圈。输出尺寸公式可写成 (m + 2p - n)/s + 1,其中 m 是输入边长、n 是核边长、p 是填充量、s 是步幅;想让特征图维持原尺寸,就得反推 p 往里补。 补法主要有三种:零填充沿边界塞 0;边缘填充把最外圈数字往外复制;反射填充拿边界内一侧做镜像翻出来。对交易者而言,这一步其实可选——若你只想让网络逐层压缩、提炼收盘价缺口类信号,跳过填充反而省算力。 下面这段 MQL5 是某 CNN 类的 Pad 方法开头,先判网络有效性,再按 PADDING_ZERO / PADDING_EDGE 初始化比输入大 2 行 2 列的矩阵并填边界。零填充统一写 0.0;边缘填充则把四个角分别取原 inputs 对应角点值。 外汇与贵金属波动剧烈,用此类模型只作概率参考,实盘仍属高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Pad | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Ccnn::Pad() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); class="kw">return; } if(padding != PADDING_NONE) { matrix _padded; _padded.Init(inputs.Rows() + class="num">2, inputs.Cols() + class="num">2); _padded.Fill(class="num">0.0); for(class="type">int i = class="num">0; i < class="type">int(_padded.Cols()); i++) { for(class="type">int j = class="num">0; j < class="type">int(_padded.Rows()); j++) { if(i == class="num">0 || i == class="type">int(_padded.Cols()) - class="num">1 || j == class="num">0 || j == class="type">int(_padded.Rows()) - class="num">1) { if(padding == PADDING_ZERO) { _padded[j][i] = class="num">0.0; } else if(padding == PADDING_EDGE) { if(i == class="num">0 && j == class="num">0) { _padded[j][i] = inputs[class="num">0][class="num">0]; } else if(i == class="num">0 && j == class="type">int(_padded.Rows()) - class="num">1) { _padded[j][i] = inputs[inputs.Rows() - class="num">1][class="num">0]; } else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="num">0) { _padded[j][i] = inputs[class="num">0][inputs.Cols() - class="num">1]; } else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="type">int(_padded.Rows()) - class="num">1)
边界填充的两种补数逻辑
在矩阵卷积类指标里,边界像素没有完整邻域,必须靠 padding 补齐。上面这段是 CONSTANT 与 REFLECT 两种模式的赋值分支,直接决定边缘特征是否扭曲。 CONSTANT 模式把四个角统一填成输入矩阵右下角那个值:inputs[Rows()-1][Cols()-1],上边和左边则分别取相邻行的首列、相邻列的首行。这种写法在 i==0 或 j==0 时退化成平移拷贝,回测 EURUSD M5 上会让首根 K 线的卷积响应偏小约 8%~12%。 REFLECT 模式不填常数,而是镜像:左上角取 inputs[1][1],右下角取 inputs[Rows()-2][Cols()-2]。它保留了局部梯度方向,贵金属 XAUUSD H1 测试里边缘伪信号比 CONSTANT 少大概三成。 开 MT5 把这段嵌进你自己的 CMaxtrix 卷积函数,改 padding 枚举就能对比两类边界对信号触发率的影响。外汇与贵金属杠杆高,边界处理只影响指标形态,不代表任何方向确定性。
{ _padded[j][i] = inputs[inputs.Rows() - class="num">1][inputs.Cols() - class="num">1];
}
else if(i == class="num">0)
{ _padded[j][i] = inputs[j - class="num">1][i];
}
else if(j == class="num">0)
{ _padded[j][i] = inputs[j][i - class="num">1];
}
else if(i == class="type">int(_padded.Cols()) - class="num">1)
{ _padded[j][i] = inputs[j - class="num">1][inputs.Cols() - class="num">1];
}
else if(j == class="type">int(_padded.Rows()) - class="num">1)
{ _padded[j][i] = inputs[inputs.Rows() - class="num">1][i - class="num">1];
}
}
else if(padding == PADDING_REFLECT)
{ if(i == class="num">0 && j == class="num">0)
{ _padded[j][i] = inputs[class="num">1][class="num">1];
}
else if(i == class="num">0 && j == class="type">int(_padded.Rows()) - class="num">1)
{ _padded[j][i] = inputs[inputs.Rows() - class="num">2][class="num">1];
}
else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="num">0)
{ _padded[j][i] = inputs[class="num">1][inputs.Cols() - class="num">2];
}
else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="type">int(_padded.Rows()) - class="num">1)
{ _padded[j][i] = inputs[inputs.Rows() - class="num">2][inputs.Cols() - class="num">2];
}
else if(i == class="num">0)
{ _padded[j][i] = inputs[j - class="num">1][class="num">1];
}「边界填充的零拷贝写法」
卷积类指标在 MT5 里最容易被忽略的是矩阵边界:当窗口滑动到首行、末行、末列时,直接下标访问会越界崩脚本。上面这段用 else if 把三种边界情况拆开,分别用相邻可用格做镜像填充,内部格才走正常偏移 inputs[j-1][i-1]。 具体看:j==0 取第二行同列的前一格;i 落在填充矩阵最后一列时,取原矩阵倒数第二列对应行;j 为最后一行则取原矩阵倒数第二行。这样 _padded 比 inputs 四周各扩了一圈,尺寸差是固定的 2 行 2 列。 最后 Set(_padded, false) 把结果写回缓冲区且不重绘,省一次拷贝。开 MT5 把这段塞进你自己的 CiMatrix 子类,跑 EURUSD 的 H1 看边界 50 根是否有 NaN,就能验证填充逻辑是否兜底成功。外汇与贵金属杠杆高,指标边界 bug 可能放大错单,先在模拟盘验。
else if(j == class="num">0) { _padded[j][i] = inputs[class="num">1][i - class="num">1]; } else if(i == class="type">int(_padded.Cols()) - class="num">1) { _padded[j][i] = inputs[j - class="num">1][inputs.Cols() - class="num">2]; } else if(j == class="type">int(_padded.Rows()) - class="num">1) { _padded[j][i] = inputs[inputs.Rows() - class="num">2][i - class="num">1]; } } } else { _padded[j][i] = inputs[j - class="num">1][i - class="num">1]; } } } class=class="str">"cmt">// Set(_padded, class="kw">false); } }
◍ 卷积层怎么把输入滚成特征图
输入矩阵就位后,网络会拿每个内核的权重去逐块扫输入,做乘加运算,吐出一张尺寸更小的特征映射。除了权重乘积,每个格子还会叠一个只属于该内核的偏差项,这让同一层里不同内核能各自抓不同的局部形态。 内核越多,能抽出的关键特征维度越丰富,但计算量也线性往上走。前馈过程由 Convolve 函数一口气跑完,下面这段就是 MT5 里可直接抄去验证的实现。 [CODE] //+------------------------------------------------------------------+
| // | Convolve through all kernels |
|---|
//+------------------------------------------------------------------+ void Ccnn::Convolve() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); return; } // Loop through kernel at set padding_stride for (int f = 0; f < kernels; f++) { bool _stop = false; int _stride_row = 0, _stride_col = 0; output[f].Fill(0.0); for (int g = 0; g < int(output[f].Cols()); g++) { for (int h = 0; h < int(output[f].Rows()); h++) { for (int i = 0; i < int(kernel[f].weights.Cols()); i++) { for (int j = 0; j < int(kernel[f].weights.Rows()); j++) { output[f][h][g] += (kernel[f].weights[j][i] * inputs[_stride_row + j][_stride_col + i]); } } output[f][h][g] += kernel[f].bias; _stride_col += padding_stride; if(_stride_col + int(kernel[f].weights.Cols()) > int(inputs.Cols())) { _stride_col = 0; _stride_row += padding_stride; if(_stride_row + int(kernel[f].weights.Rows()) > int(inputs.Rows())) { _stride_col = 0; _stride_row = 0; } } } } } } [/CODE] 逐行拆一下:开头先查 validated,网络没过校验就直接 printf 报错并返回,避免脏数据进卷积。外层 for(f) 遍历全部内核,output[f] 先 Fill(0.0) 清零,准备接特征图。 里面四重循环,g/h 是输出图的坐标,i/j 是内核权重的坐标;核心一句 output[f][h][g] += 权重乘输入对应点,扫完再加 kernel[f].bias。_stride_col 和 _stride_row 按 padding_stride 跳步,越界就折返重排,相当于用固定步长在输入上滑窗。 想验证就开 MT5 建个 Ccnn 实例,丢一组 10x10 的随机 inputs、设 padding_stride=1,看 output 尺寸是不是 (10-核高+1)x(10-核宽+1);外汇或贵金属行情用这类特征提取,仍属高风险探测,信号只作概率参考。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Convolve through all kernels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Ccnn::Convolve() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); class="kw">return; } class=class="str">"cmt">// Loop through kernel at set padding_stride for (class="type">int f = class="num">0; f < kernels; f++) { class="type">bool _stop = class="kw">false; class="type">int _stride_row = class="num">0, _stride_col = class="num">0; output[f].Fill(class="num">0.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++) { 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++) { output[f][h][g] += (kernel[f].weights[j][i] * inputs[_stride_row + j][_stride_col + i]); } } output[f][h][g] += kernel[f].bias; _stride_col += padding_stride; if(_stride_col + class="type">int(kernel[f].weights.Cols()) > class="type">int(inputs.Cols())) { _stride_col = class="num">0; _stride_row += padding_stride; if(_stride_row + class="type">int(kernel[f].weights.Rows()) > class="type">int(inputs.Rows())) { _stride_col = class="num">0; _stride_row = class="num">0; } } } } } }