利用 MQL5 矩阵的反向传播神经网络(基础篇)
📘

利用 MQL5 矩阵的反向传播神经网络(基础篇)

第 1/3 篇

◍ 用 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 更合适。外汇与贵金属行情具有高杠杆高风险,拿这类未训练网络跑历史价只是结构验证,不代表任何实盘胜率。

MQL5 / C++
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 是否单调下降。

MQL5 / C++
!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;
}

常见问题

先确认输入向量是列向量,权重矩阵行数等于本层神经元数、列数等于上层输出数,乘完再加偏置列,最后过激活函数即可。
每一层拿下一层传回的误差乘当前权重转置得到本层误差,再逐元素乘激活函数导数值,就是该层梯度来源。
可以,把代码或维度描述发给小布,它能指出矩阵乘法不匹配、偏置漏加等常见错误,省去手动排查。
隐藏层别用纯线性, sigmoid 或 tanh 注意初始化尺度, ReLU 类防死神经元,输出层按任务选线性或软最大。
偏置对应全 1 输入列,其梯度需从误差项单独累加而不参与权重矩阵回传,剔除后权重更新才不会被偏置维度污染。