利用 MQL5 矩阵的反向传播神经网络(基础篇)
◍ 用 MQL5 矩阵重写 BPNN 前馈与回传
反向传播神经网络(BPNN)是监督学习里的老牌结构,深度网络、循环网络、卷积网络都把它当基座。MQL5 早先做这类运算得逐个元素循环,代码冗长且慢;2023 年 7 月引入的矩阵/向量 API 把批次计算搬进原生层,数据按块整体处理,前馈和误差回传能压成单行矩阵表达式。 对交易者来说,这意味着你能在 MT5 里直接写一套可读的 BPNN 类,而不必外挂 Python 或第三方库。理论公式近乎原样落进源码,初学者跟一遍就能看清权重更新每一步,老手则可跳过推导直奔实盘脚本。 外汇与贵金属杠杆高、滑点随机,拿这类网络做信号前务必在策略测试器跑历史样本,验证集表现只代表概率倾向,不等于未来收益。
神经元、激活函数与单层前向传播
神经网络的最小单元是神经元,逻辑上按层堆叠,靠带权重的突触传递信号。某个神经元收到的输入先做加权合计,再经过非线性激活函数产出输出;非线性正是网络具备复杂拟合能力的根因。 MQL5 内置了多种激活函数,选型要看任务类型:sigmoid 把数据压到 [0,+1],偏分类;双曲正切映射到 [-1,+1],回归与预测场景往往更顺手。激活函数还得在全数轴有非零导数,反向传播才跑得通,sigmoid 的导数可解析为 f'(v)=f(v)(1-f(v)),计算很轻。 单层网络用数学表达就是 Y = F(XW),W 里每个元素 w_ij 管第 j 个神经元第 i 条连接。层数和每层神经元数没有闭式解,得看输入维度、样本量和分布,基本靠反复试错定。 S 形函数在 T 附近斜率最大、信号 Transfer 最好,所以常给激活函数加平移量,让工作点落到 T(多取 0)。做法是塞一个恒为 1 的伪输入 x0,配权重 w0=-T,公式变成带偏置的形式。 监督训练四步循环:小随机值初始化权重 → 喂向量前向算响应 → 算误差并按规则调权 → 扫完整个数据集直到误差低于阈值或撞最大轮数。单层调权很简单:w_ij(t+1)=w_ij(t)+ν·δ_i·x_j,ν 取 0 到 1 之间的学习率。多层网怎么调,就是反向传播要解决的问题。 开 MT5 把内置激活函数挨个挂到单层上跑一遍异或或简单回归,能直观看到 sigmoid 与 tanh 输出范围的差异,比看公式来得快。
「反向传播怎么把误差送回输入端」
全连接网络里每一层的神经元都接满前一层,误差信息必须顺着连接从输出层一层层回传到输入层,同时按权重(也就是层的“电导率”)来分配责任。卷积网络虽然只连局部区域,但反向传播这套逻辑照样能套用。 训练目标用最小二乘思路写成 E = 1/2 Σ_p Σ_j (y_jp^N − d_jp)^2,那个 1/2 只是为了让后面求导时系数消掉,本身不改变优化方向;η 是学习率,控制每次权重往梯度反方向挪多少。 梯度下降按 ∂E/∂w_ij 调权重,拆开后得到递归式:本层 δ_j^(n) 依赖上一层 δ_k^(n+1) 和连接权重 w_jk,再乘激活函数导数 dy_j/ds_j 与前一层输出 y_i。这就要求激活函数全程可微,双曲正切就满足这一点。 实际训练循环是:小随机数初始化权重 → 正向算一遍输出 → 输出层用 (y−d)·f′ 算 δ → 反向逐层递推 δ → 按 Δw = −η·δ·y_prev(或加惯性项 μ·Δw_{t-1})更新全部权重 → 喂下一个样本重来。把所有训练图按随机顺序交替喂,避免记新忘旧。 用矩阵一次性承载输入、目标和各层权重时,步骤 2–7 不再跑单个向量,而是对整个数据集同时计算,这样一个完整循环叫作一个学习世代(epoch),MT5 里写神经网络训练壳子时基本都按这个粒度排程。
◍ 激活函数怎么选才不会让网络哑火
附件里的 AF.mq5 脚本能直接在图表上画出 MQL5 原生支持的全部激活函数(蓝线)以及它们的导数(红线)。脚本会自动缩放缩略图适配窗口,想要看清细节就把图表窗口最大化再跑一遍。 SoftMax 和其它函数不一样,它不是逐元素处理输出,而是把整层数值归一化成和为 1 的概率分布,所以多重分类里基本离不开它;同一个网络里混用多种激活函数也是常态。 S 形函数有个老毛病叫梯度消失:信号一旦滑到曲线两端钝化区,权重更新量就趋近零,层数越多越容易整网瘫痪。单调递增函数则反向翻车,梯度爆发会让权重持续累加直到溢出成 NaN。外汇和贵金属行情噪声大、跳变多,用这类函数搭深网做预测时高风险会显著放大。 数据规范化、dropout、批量学习和加噪正则化都能压住上面两个问题,后面我们会挑几个在 MT5 里实测。先开 MT5 把 AF.mq5 拖进图表,确认你机器上能复现那张函数缩略图再说。
用矩阵类搭出多层网络骨架
在 MT5 里写神经网络,先别急着训,得把结构立住。MatrixNet 类用两个矩阵数组撑起整个网络:weights[i] 存第 i 层到 i+1 层的突触权重,outputs[i] 存任意层神经元输出,包括第 0 层直接灌入的输入特征。变量 n 只计隐藏层加输出层,不含输入伪层,所以构造函数里 n = ArraySize(layers) - 1,且强制 n < 2 直接 return,也就是至少得有两层才允许初始化。 权重矩阵按转置思路排:行数是前一层神经元数 +1(那个 +1 是偏置恒为 1 的移位源),列数是当前层神经元数。前馈时 outputs[i] 乘 weights[i] 得到 outputs[i+1],信号严格从左流向右。反向传播则要把较高层的误差向量乘 weights[i] 的转置,把误差回传。两个方向共用同一组矩阵,只是乘的时候一个正常用一个转置用。 随机初始化不是走过场。randomize 默认在 [-0.5, 0.5] 均匀撒点,填完所有 weights[i]。若输入数据列数和 weights[0].Rows() - 1 对不上,feedForward 会打印具体差值并退出,这条检查能帮你当场抓出特征数配错的问题。 激活函数分两套:af 管中间层,of 管输出层,构造时分别传 AF_TANH 和 AF_NONE 这类枚举。学习率 speed 对应公式里的 η,后面 backProp 里直接拿它乘 delta 调权重。getResults 默认回传 outputs 最后一层的状态,test 方法则可顺带喂入目标矩阵算均方根误差 LOSS_MSE,做图像分类时换 LOSS_CCE 更合适。外汇与贵金属行情具有高杠杆高风险,拿这类未训练网络跑历史价只是结构验证,不代表任何实盘胜率。
class MatrixNet { class="kw">protected: const class="type">int n; matrix weights[class=class="str">"cmt">/* n */]; matrix outputs[class=class="str">"cmt">/* n + class="num">1 */]; ENUM_ACTIVATION_FUNCTION af; ENUM_ACTIVATION_FUNCTION of; class="type">class="kw">double speed; class="type">bool ready; ... class="kw">public: MatrixNet(const class="type">int &layers[], const ENUM_ACTIVATION_FUNCTION f1 = AF_TANH, const ENUM_ACTIVATION_FUNCTION f2 = AF_NONE): ready(false), af(f1), of(f2), n(ArraySize(layers) - class="num">1) { if(n < class="num">2) class="kw">return; allocate(); for(class="type">int i = class="num">1; i <= n; ++i) { class=class="str">"cmt">// NB: the weights matrix is transposed, i.e. indexes [row][column] specify [synapse][neuron] weights[i - class="num">1].Init(layers[i - class="num">1] + class="num">1, layers[i]); } ... } class="kw">protected: class="type">void allocate() { ArrayResize(weights, n); ArrayResize(outputs, n + class="num">1); ... } class="kw">public: MatrixNet(const class="type">int &layers[], const ENUM_ACTIVATION_FUNCTION f1 = AF_TANH, const ENUM_ACTIVATION_FUNCTION f2 = AF_NONE): ready(false), af(f1), of(f2), n(ArraySize(layers) - class="num">1) { ... ready = true; randomize(); } class=class="str">"cmt">// NB: set values with appropriate distribution for specific activation functions class="type">void randomize(const class="type">class="kw">double from = -class="num">0.5, const class="type">class="kw">double to = +class="num">0.5) { if(!ready) class="kw">return; for(class="type">int i = class="num">0; i < n; ++i) { weights[i].Random(from, to); } } class="type">bool feedForward(const matrix &data) { if(!ready) class="kw">return false; if(data.Cols() != weights[class="num">0].Rows() - class="num">1) { PrintFormat("Column number in data %d <> Inputs layer size %d", data.Cols(), weights[class="num">0].Rows() - class="num">1); class="kw">return false; } outputs[class="num">0] = data; class=class="str">"cmt">// input the data to the network for(class="type">int i = class="num">0; i < n; ++i) { class=class="str">"cmt">// expand each layer(except the last one) with one neuron for the bias signal class=class="str">"cmt">// (there is no weight matrix to the right of the last layer, since the signal does not go further) if(!outputs[i].Resize(outputs[i].Rows(), weights[i].Rows()) ||
「反向传播里的偏置剔除与权重更新」
这段 MQL5 片段承接前文的 feedForward,把多层感知机的训练闭环补完了。backProp 一开始先校验 ready 标志与 target 维度,输出层行数或列数对不上会直接返回 false,避免拿错位矩阵做减法。 核心在反向循环:从输出层往前,每一层先求激活导数 temp,再用 (outputs[n] - target) * temp 得到整批样本的 loss 矩阵。这里 loss 是逐行对应样本,不是单点计算。 一个容易踩坑的细节是伪偏移神经元。除最后一层外,每层输出列尾都塞了一个恒 1 的偏置源,反向时若 i < n-1 就调用 loss.Resize 砍掉最后一列,否则误差会错误地回传到这个「假神经元」上。 权重更新用 delta = speed * outputs[i].转置.MatMul(loss),再 weights[i] -= delta。speed 即学习率,调大可能加快收敛但外汇小时线回测中常引发振荡,调小则迭代更稳却慢。贵金属与外汇杠杆高,实盘前务必在 MT5 用历史数据跑 test() 看 LOSS_MSE 是否单调下降。
!outputs[i].Col(vector::Ones(outputs[i].Rows()), weights[i].Rows() - class="num">1)) class="kw">return false; class=class="str">"cmt">// forward the signal from i-th layer to the(i+class="num">1)-th layer: weighted sum matrix temp = outputs[i].MatMul(weights[i]); class=class="str">"cmt">// apply the activation function, the result is received into outputs[i + class="num">1] if(!temp.Activation(outputs[i + class="num">1], i < n - class="num">1 ? af : of)) class="kw">return false; } class="kw">return true; } matrix getResults(const class="type">int layer = -class="num">1) const { class="kw">static const matrix empty = {}; if(!ready) class="kw">return empty; if(layer == -class="num">1) class="kw">return outputs[n]; if(layer < -class="num">1 || layer > n) class="kw">return empty; class="kw">return outputs[layer]; } class="type">class="kw">double test(const matrix &data, const matrix &target, const ENUM_LOSS_FUNCTION lf = LOSS_MSE) { if(!ready || !feedForward(data)) class="kw">return NaN(); class="kw">return outputs[n].Loss(target, lf); } class="type">bool backProp(const matrix &target) { if(!ready) class="kw">return false; if(target.Rows() != outputs[n].Rows() || target.Cols() != outputs[n].Cols()) class="kw">return false; class=class="str">"cmt">// output layer matrix temp; if(!outputs[n].Derivative(temp, of)) class="kw">return false; matrix loss = (outputs[n] - target) * temp; class=class="str">"cmt">// all data line by line for(class="type">int i = n - class="num">1; i >= class="num">0; --i) class=class="str">"cmt">// all layers except the output in reverse order { class=class="str">"cmt">// remove pseudo-losses in the last element which we added as an offset source class=class="str">"cmt">// since it is not a neuron and further error propagation is not applicable to it class=class="str">"cmt">// (we do it in all layers except the last one where the shift element was not added) if(i < n - class="num">1) loss.Resize(loss.Rows(), loss.Cols() - class="num">1); matrix delta = speed * outputs[i].Transpose().MatMul(loss); if(!outputs[i].Derivative(temp, af)) class="kw">return false; loss = loss.MatMul(weights[i].Transpose()) * temp; weights[i] -= delta; } class="kw">return true; }