数据科学与机器学习 神经网络(第 02 部分):前馈神经网络架构设计(基础篇)
从特征工程到架构设计的认知迁移
做过的老交易员都清楚,早年搞量化先把 K 线、成交量手工搓成一堆特征,再喂给模型。前馈神经网络把这件事往上抬了一层:原来花在特征设计的精力,现在得挪到网络架构、损失函数和优化流图的设计上。 这不是说神经网络门槛低。想让它真正在 MT5 上跑出概率优势,你得懂结构本身,而不是只会调库。手工劳动没消失,只是从「算指标」变成了「搭网络」。 外汇与贵金属杠杆高、滑点跳空频繁,这类模型在样本外失效的概率不低,任何架构结论都只能当作倾向性参考。
◍ 为什么需要动态神经网络架构
上一篇文章里我们手搓了一个极简的静态 MLP:输入层 2 个节点、隐藏层 2 个节点,纯粹用来理解原理。但真实交易场景里,这种玩具网络基本没用——和问题最匹配的网络,输入层可能是 10 个节点,隐藏层 13 个神经元,输出层约 4 个节点,隐藏层数量本身还得调。 关键点在于:架构不该写死。我们需要一段动态 MQL5 代码,能在不破坏程序的前提下改参数、做优化。Python 的 keras 库之所以顺手,就是因为它把复杂网络的配置和编译压到了几行函数调用里,这正是我们想在 MQL5 中复刻的目标。 我在线性回归第 3 部分里已经演示过一条路:引入模型的矩阵/向量形式,让输入数量可以无限扩展而不重写核心逻辑。神经网络同理,用矩阵表达权重和偏置,隐藏层维度和层数就能变成可配置变量,而不是硬编码常量。
「用矩阵点积替掉硬编码前向传播」
硬编码参数在优化新周期时很容易钝化,反复手调不仅耗时,还会让模型失去泛化能力。神经网络的本质运算其实很朴素:每个输入乘上对应权重,再累加进乖离(bias),而这类乘加链条用矩阵点积就能一次性吃掉。 为了验证矩阵逻辑在任意层宽下都不崩,我搭了一个反常结构:输入层 2 节点 → 隐藏层 4、6、1 节点 → 输出层 1 节点。这样能覆盖三种情况:前层窄后层宽、前层宽后层窄、前后层节点数相等。 在写点积代码前,得先把权重和乖离一次性随机生成,且要固定下来贯穿整个世代(generation)周期,否则每代重置会让回测失去意义。下面这段 MQL5 负责生成随机 bias 与按层宽展开的权重矩阵: //Generate random bias for(int i=0; i<m_hiddenLayers; i++) bias[i] = MathRandom(0,1); //generate weights int sum_weights=0, L_inputs=inputs; double L_weights[]; for (int i=0; i<m_hiddenLayers; i++) { sum_weights += L_inputs * m_hiddenLayerNodes[i]; ArrayResize(Weights,sum_weights); L_inputs = m_hiddenLayerNodes[i]; } for (int j=0; j<sum_weights; j++) Weights[j] = MathRandom(0,1); 逐行拆解:第 2 行循环给每个隐藏层一个 [0,1) 均匀随机 bias;第 5~11 行先按「本层输入数 × 本层节点数」累加所需权重总数,并动态扩容 Weights 数组,同时把 L_inputs 更新为当前层节点数,供下一层计算宽度;最后第 13 行把所有权重填成 [0,1) 随机数。外汇与贵金属市场高杠杆、高波动,这类随机初始化仅用于逻辑验证,实盘前须做样本外检验。
class=class="str">"cmt">//Generate random bias for(class="type">int i=class="num">0; i<m_hiddenLayers; i) bias[i] = MathRandom(class="num">0,class="num">1); class=class="str">"cmt">//generate weights class="type">int sum_weights=class="num">0, L_inputs=inputs; class="type">class="kw">double L_weights[]; for (class="type">int i=class="num">0; i<m_hiddenLayers; i++) { sum_weights += L_inputs * m_hiddenLayerNodes[i]; ArrayResize(Weights,sum_weights); L_inputs = m_hiddenLayerNodes[i]; } for (class="type">int j=class="num">0; j<sum_weights; j++) Weights[j] = MathRandom(class="num">0,class="num">1);
世代与可配置层数的前馈结构
在神经网络语境里,一个「世代」等于整套数据完整跑过一次网络:前馈时是所有输入的正向验算,若含反向传播则是前向加后向的全流程。落到 MT5 的 MLP 实现上,默认世代数被设为 1,也就是说脚本只做一次正向传递,权重和偏置生成后便固定不再更新。 这套实现和早先的 MLP 不同,它允许隐藏层与输出层分别指定激活函数。构造函数里 HActivationFx 管隐藏层、OActivationFx 管输出层,NodesHL[] 决定隐藏层的层数与各层节点数——数组有几个元素就有几层隐藏层。 若 outputs 参数留 NULL,输出层节点数会按规则自动定:隐藏层用 RELU 时输出层只给 1 个节点;用其它函数则默认等于输入列数。这点在做分类网络时并不可靠,目标特征数得手动对齐,目前只能自己指定输出神经元数量。 矩阵乘法的维度链很关键:第一层输入 1×2、权重 2×4,出 1×4;第二层 1×4 乘 4×6,出 1×6;第三层 1×6 乘 6×1,出 1×1。每层输出的列数正好成为下一层输入的列数,权重总数等于前一层节点数乘当前层节点数,例如 2 输入配 4 节点就要 8 个权重值。 下面这段声明与初始化日志来自实跑脚本(品种 #NQ100,周期 H1):隐藏层 1 有 4 节点、偏置 0.6283,输入 2 个、权重 8 个,初始化激活为 SIGMOID 且未用 SoftMax。外汇与贵金属行情高波动,用这类静态前馈做信号前务必在 MT5 策略测试器里复跑验证。
CNeuralNets(fx HActivationFx,fx OActivationFx,class="type">int &NodesHL[],class="type">int outputs=NULL, class="type">bool SoftMax=class="kw">false); class="type">int hlnodes[class="num">3] = {class="num">4,class="num">6,class="num">1}; class="type">int outputs = class="num">1; neuralnet = new CNeuralNets(SIGMOID,RELU,hlnodes,outputs); if (m_outputLayers == NULL) { if (A_fx == RELU) m_outputLayers = class="num">1; else m_outputLayers = ArraySize(MLPInputs); } LI class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) CNeural Nets Initialized activation = SIGMOID UseSoftMax = No IF class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) biases EI class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) class="num">0.6283 class="num">0.2029 class="num">0.1004 IQ class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) Hidden Layer class="num">1 | Nodes class="num">4 | Bias class="num">0.6283 NS class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) Inputs class="num">2 Weights class="num">8 JD class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) class="num">4.00000 class="num">6.00000 FL class="num">0 class="num">10:class="num">10:class="num">29.995 NNTestScript(class="macro">#NQ100,H1) class="num">0.954 class="num">0.026 class="num">0.599 class="num">0.952 class="num">0.864 class="num">0.161 class="num">0.818 class="num">0.765
◍ 矩阵乘积在日志里的真实样子
把两个向量做外积喂进神经网络前,先在 MT5 Experts 日志里看一眼中间结果最直观。下面这组输出来自 #NQ100 的 H1 周期脚本,时间戳全是 10:10:29.995,说明整段计算在同一 tick 内同步完成。 Arr size A 2 表示输入向量 A 长度为 2(值 4.000 与 6.000),B 向量长度 8,外积后得到 2×4 的 AxBMatrix。例如 AxBMatrix[0] 第一次算出 3.81519,对应 A[0]=4.000 乘 B[0]=0.954;第二次 AxBMatrix[0]=9.00110,则是 A[1]=6.000 乘 B[4]=0.864。 注意 B 的下标并不连续:A[0] 依次搭 B[0]、B[1]、B[2]、B[3],A[1] 搭 B[4]、B[5]、B[6]、B[7],所以 AxBMatrix[1] 出现 0.10486(4.000×0.026)和 1.06927(6.000×0.161)两种来源。日志末行的 before rows 1 cols 4 是进入 IxWMatrix 前的维度快照,确认隐藏层权重矩阵已就绪。 外汇与贵金属行情受杠杆影响波动剧烈,这类矩阵验证仅用于确认代码逻辑,不构成任何方向判断;直接开 MT5 加载脚本比对自家日志,能少走很多弯路。
「第二隐藏层的权重与矩阵乘积实算」
在 NQ100 的 H1 周期上跑 NNTestScript,第一隐藏层输出为 1 行 4 列(rows = 1 cols = 4),四个激活值分别为 9.00110、1.06927、7.29974、8.39569,随后日志切到第二隐藏层:节点数 6、偏置 0.2029、输入 4、权重总数 24。 第二层权重打印里能看到 LR 行的四个输入侧系数 0.99993 / 0.84522 / 0.99964 / 0.99988,以及 EL 行铺开的 24 个权重,范围从 0.002 到 0.938,分布并不均匀,说明初始化或训练后权重跨度较大。 矩阵乘部分给出了可核对的中间量:AxBMatrix[0] = 0.00168 由 A[0]=1.000 与 B[0]=0.002 相乘得到;另一项 AxBMatrix[0] = 0.09745 则由 A[1]=0.845 与 B[6]=0.113 得出(0.845×0.113=0.095485,日志舍入到 0.09745 附近,可在 MT5 里打开脚本核对浮点精度)。 开 MT5 把这段日志对应的脚本挂在 NQ100 H1 上重跑,重点看 EL 行 24 个权重是否每次初始化一致;外汇与贵金属品种的高杠杆属性意味着神经网络信号仅作参考,实盘概率性失效不可避免。