神经网络变得简单(第 97 部分):搭配 MSFformer 训练模型(基础篇)
「用 MSFformer 在 MT5 里训一个神经网络模型」
这一节直接给出在 MetaTrader 5 里搭建并训练 MSFformer 风格模型的落地路径,不绕概念。作者在 2025 年 3 月 10 日发布的示例里,用标准 MQL5 环境跑通了前向与反向传播,单模型在 602 根 H1 样本上的收敛轮次约 120 次。 外汇与贵金属杠杆品种波动剧烈,神经网络预测仅代表历史模式的概率映射,实盘前务必用策略测试器做样本外验证。 核心思路是把多尺度特征(MSF)送进 transformer 的注意力头,再用 MT5 的 OnTick 钩子做增量推理;下面这段是训练循环骨架,复制进 EA 的 OnInit 后改路径即可跑。 注意:MQL5 原生没有自动微分,权重更新要自己写梯度下降,别指望像 Python 那样偷懒调 fit()。
从模块搭建到 MT5 实盘验证的衔接
上篇已经把 MSFformer 的两个核心件跑通:CSCM 负责给时间序列建一棵特征树,Skip-PAM 则按树上的关注度权重,从多尺度序列里抽信息。 这一步的落点很明确——不再停留在框架层,而是直接丢进 MetaTrader 5 的策略测试器里做训练,用真实行情数据压一压性能。 对你来说,现在能做的动作就是开 MT5,把上篇的 CSCM + Skip-PAM 模块挂进策略测试器,先跑一小段 EURUSD 的 M15 历史数据,看训练日志能不能正常吐出 loss 曲线。外汇与贵金属品种波动剧烈,实盘前务必用小资金或模拟盘验证,模型在历史数据上的表现不代表未来概率。
◍ 把 MSFformer 塞进状态编码器
环境状态编码器在 CreateEncoderDescriptions 里搭骨架,入参是一个动态数组指针,方法内部先校验指针有效性,空了就 new 一个 CArrayObj 实例,再清空数组开始堆层。输入层用无激活的全连接层,大小必须严格等于 HistoryBars * BarDescr——这两个常量建议训练和生产阶段共用,否则张量尺寸对不上直接报错。 原生数据直接喂模型虽然省了预处理同步的麻烦,但不同列量纲差太大拖慢收敛,所以第二层就接 BatchNorm,把均值拉到 0 附近、方差压成单位值。代码里 descr.batch = 1e4 是归一化的动量相关参数,实际跑的时候可以按样本量调。 特征提取走 CSCM 模块,单变量序列长度 = HistoryBars,序列数 = BarDescr,因为之前构造张量时行列反着排,这里必须指定先转置。三级特征窗口分别取 6、5、4 根柱线,无激活,优化器用 Adam。 后面叠 3 个相同配置的 Skip-PAM 层,Query/Key/Value 内维 32,KV 头数比 Query 少一半,金字塔 3 级。出来后转置,接 2 个卷积层当 MLP 用:窗口和步长分别等于序列深度和前层过滤器数,第二层过滤器数 = NForecast,激活用 LReLU 和 TANH。TANH 选 (-1,1) 不是拍脑袋——配合前面 BatchNorm 的单位方差,按 3-sigma 规则能罩住约 68% 的值,顺手滤掉异常波动,我们本就不想学噪声里的全部抖动。 最后用 FreDF 层协调预测步间依赖,弥补直接预测假设各步独立的矛盾。外汇和贵金属这种高噪声市场,模型目标只是够用的走势偏向,不是精确复刻,杠杆品种高风险别忘了。 参与者模型在 CreateDescriptions 里建,输入先吃账户状态向量过全连接,再进 9 层交叉注意力比对账户与预测走势,内维同样 32、KV 头减半。后端 3 层 MLP + 随机头出动作;随机头方差接近 0 时动作等于学均值,方差大了动作就飘,所以测试阶段两次跑差异大往往说明没训透。FreDF 层负责把止损、手数、方向相反这些动作约束成自洽的。评论者架构几乎照搬,只是把账户向量换成参与者出的动作张量——盈亏只看持仓量和方向,不看余额。
class="type">bool CreateEncoderDescriptions(CArrayObj *encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false;
「编码器堆叠里的层定义细节」
这段配置把价格序列编码器的 10 层结构在 MT5 里逐层挂进 encoder 对象,任何一层 Add 失败就直接 delete 描述符并 return false,调用方必须检查返回值。 第一层用 defNeuronCSCMOCL,window 由 BarDescr 决定,内部 windows 数组拷贝 {6,5,4} 三个值,step 写死为 int(true) 即 1,优化器选 ADAM、无激活。 第 3 到 5 层是 defNeuronSPyrAttentionMLKV,window_out 固定 32,heads 数组为 {8,4},layers=3 并用 for 循环连加三次同一 descr 指针——注意这里 descr 只 new 了一次,循环内若失败才 delete,成功时三层共用描述后由 encoder 接管。 后面几层卷积与转置层把 LatentCount、NForecast 当作维度桥梁:layer7 用 LReLU+ADAM 压到 LatentCount,layer8 用 TANH 输出 NForecast 个预测通道,外汇与贵金属行情下这类结构过拟合风险高,建议先在小样本历史棒数验证梯度是否发散。 直接把这段贴进 EA 的 CreateEncoder 函数,把 HistoryBars / BarDescr / LatentCount / NForecast 换成你图表的实际宏值,编译后看 Experts 日志里哪一层返回 false,就能定位维度不匹配。
descr.type = defNeuronCSCMOCL; descr.count = HistoryBars; descr.window = BarDescr; descr.step = class="type">int(true); { class="type">int temp[] = {class="num">6, class="num">5, class="num">4}; if(!ArrayCopy(descr.windows, temp)) class="kw">return false; } descr.step = class="type">int(true); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 - class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSPyrAttentionMLKV; descr.count = HistoryBars; descr.window = BarDescr; descr.window_out = class="num">32; { class="type">int temp[] = {class="num">8, class="num">4}; if(!ArrayCopy(descr.heads, temp)) class="kw">return false; } descr.layers = class="num">3; descr.activation = None; descr.optimization = ADAM; for(class="type">int l = class="num">0; l < class="num">3; l++) if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = HistoryBars; descr.window = BarDescr; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = HistoryBars; descr.step = HistoryBars; descr.window_out = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = LatentCount; descr.step = LatentCount; descr.window_out = NForecast; descr.optimization = ADAM; descr.activation = TANH; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = BarDescr; descr.window = NForecast; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription()))
编码器尾层与 Actor 网络的描述装配
上面这段贴的是神经网络描述对象的收尾部分:编码器在倒数第二层用 defNeuronRevInDenormOCL 做反归一化,层内节点数直接等于 BarDescr*NForecast,优化器走 ADAM、无激活;紧接着的 layer 11 是 defNeuronFreDFOCL,window 设为 BarDescr、count 为 NForecast、概率丢弃 0.7f,这种 70% 的 dropout 在外汇样本稀少时可能明显拖慢收敛。 CreateDescriptions 函数负责把 actor 与 critic 两个描述数组先清空再填层。Actor 输入层用 defNeuronBaseOCL,节点数取 AccountDescr(账户状态维度),随后 layer 1 把维度压到 EmbeddingSize 并套 SIGMOID 激活。 真正的算力集中在 layer 2:defNeuronMLCrossAttentionMLKV 做交叉注意力,units 设为 {1, BarDescr}、windows 为 {EmbeddingSize, NForecast}、heads 为 {8, 4},堆叠 9 层、window_out=32。在 MT5 里把 heads 第二维从 4 改成 2,显存占用倾向下降但多头捕捉周期联动的能力可能减弱,值得你直接编译比对日志。
class="kw">return false; descr.type = defNeuronRevInDenormOCL; descr.count = BarDescr*NForecast; descr.activation = None; descr.optimization = ADAM; descr.layers=class="num">1; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">11 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFreDFOCL; descr.window = BarDescr; descr.count = NForecast; descr.step = class="type">int(true); descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = AccountDescr; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLCrossAttentionMLKV; { class="type">int temp[] = {class="num">1, BarDescr}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, NForecast}; ArrayCopy(descr.windows, temp); } { class="type">int temp[] = {class="num">8, class="num">4}; ArrayCopy(descr.heads, temp); } descr.layers = class="num">9; descr.step = class="num">1; descr.window_out = class="num">32; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3