神经网络变得简单(第 85 部分):多变元时间序列预测(基础篇)
「用多变元序列喂饱预测网络」
很多交易者习惯只用收盘价一条线去训练神经网络,但在 MT5 里把成交量、均线差、波动率一起作为特征维度输入,模型对下一根 K 线的方向判断往往更稳。原文在 2025 年 1 月 7 日发布的这套思路,核心就是把单变量预测扩成多变元时间序列。 具体做法是在 EA 里构造一个二维特征矩阵:每一行是一个历史窗口,每一列是一种指标。相比只看价格,加入多维度后网络学到的权重不再过度拟合某一根影线。 开 MT5 新建一个 EA,把下面这段特征拼接逻辑先跑通,你就能直观看到样本维度从 1×N 变成 M×N 后的矩阵规模变化。外汇与贵金属杠杆高,多变元模型只降低过拟合概率,不消除爆仓风险。
变换器预测时序的掩码实验疑云
做交易策略,本质是在赌自己对下一步价格走势的判断。深度学习里基于变换器架构的模型,这两年被视为长周期时间序列预测的一把利器,但效率到底如何,仍有争议。 之前多数变换器靠自关注度机制抓时间步之间的长期依赖。可部分研究指出,跨期关注度模型常常没真正学到跨期依赖,某些场景下简单线性模型反而更稳。 有篇论文做了个硬核实验:把历史序列随机掩蔽,用“0”替换掉单独数据,看模型性能掉多少。对时间依赖敏感的模型,缺了正确历史数据会明显滑坡。结果令人意外——部分交叉关注度变换器即便被替换掉 80% 历史数据,预测性能几乎没变。 这种“不敏感”叫我态度矛盾。从黑匣子角度看,你根本不知道它参照了哪段、忽略了哪段,对序列变化麻木当然危险。但在外汇、贵金属这种高噪音、高风险的随机市场里,过滤掉非典型异常波动,反而可能帮人看清主干趋势。 论文还提到,多元序列里不同变量常随时间的推移出现相关形态,用关注度学变量间依赖,比学时间步更靠谱。原变换器提取趋势偏弱,线性模型补这个短板,于是作者把两者集成,兼顾趋势抽取与表达力。
◍ 客户端算法怎么把变量依赖和趋势拆开吃
客户端思路的核心转移,是把模型注意力从“时间步怎么变”挪到“变量之间怎么互相牵扯”,同时塞进一个线性模块专门抓趋势。它没全盘推翻老办法,但每个模块的留或删都跑了测试,用模型有效性说话,不是拍脑袋。 为了解决分布乖离,作者用了可逆归一化(RevIN):进模型前先剥掉原始序列的统计信息,等模型吐出预测后再把统计信息贴回去。这一招通常能稳住训练、提升预测品质,上一节已拆过原理。 数据进来先转置,让后续分析按“变量项”而不是“时间步”走。转置后的数据直接喂给变换器编码器(多层多头自关注度 MHA + 前馈 FFN),但要注意:跳过了嵌入层,位置编码也删了。测试表明嵌入层会扭曲时间信息、拉低性能,而变量间本就没有统一时间轴,位置编码纯属多余。 编码器提完特征,不经过经典解码器——论文发现加解码器反而整体变差——直接进预测层出每个变量的预测。与此同时,一条并行的线性模块在啃单通道、单变量的趋势。两者输出相加,线性那边还乘了可学习权重。最后结果再转置回原始排列、恢复统计信息。 一句话落点:线性模块管趋势,变换器管非线性与跨变量依赖,两端相加才是客户端的最终预测。想验证的话,在 MT5 里用 Python 桥接跑一份多变量收盘价,先 RevIN 再转置,对比加不加解码器的回测 MAE,差异应该肉眼可见。外汇与贵金属波动受杠杆与事件驱动,多变量依赖结构随时断裂,这套算法实盘前务必小资金压力测试。
「用 MQL5 把客户端方法落成神经层」
把前面讲过的客户端方法真正跑起来,第一步是写一个 CNeuronClientOCL 类,它继承自 CNeuronBaseOCL,把关注度模块和线性预测模块都塞进同一个层里。类里所有内部对象都声明成静态,构造和析构留空,内存清理直接交给系统,Init 方法负责把架构参数一次性传进去。 模块分两条并行流:一边是变换器编码器(CNeuronMLMHAttentionOCL),用 window、window_key、heads、count、at_layers 描述;另一边是卷积层动态数组 cLinearModel[],用 mlp[] 数组定义层数,每个元素代表该层输出向量的大小。客户端方法要求分析变量间依赖,所以输入前要转置——序列元素数等于变量数,单元素向量大小等于历史深度。 线性模块末尾多加一层卷积(过滤器、窗口、步幅都为 1)替代原文里“可学习乘数”的奇怪做法;归一化后均值 0 方差 1,预测值用 tanh 约束。两个输出流都 tanh 激活,误差梯度靠共用内部缓冲区直接写,省掉互相分派的开销,feedForward 和 calcInputGradients 就是按这个逻辑前馈、反传。
class CNeuronClientOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="kw">static CNeuronMLMHAttentionOCL cTransformerEncoder; class=class="str">"cmt">// 变换器编码器,多头关注度 class="kw">static CNeuronConvOCL cProjection; class=class="str">"cmt">// 卷积预测层 class="kw">static CNeuronConvOCL cLinearModel[]; class=class="str">"cmt">// 线性模块动态数组 class="kw">public: class="type">bool Init(class="type">int window, class="type">int window_key, class="type">int heads, class="type">int count, class="type">int at_layers, class="type">int &mlp[]); class="type">bool feedForward(CNeuronBaseOCL *prev); class="type">bool calcInputGradients(CNeuronBaseOCL *prev); };
class CNeuronClientOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="kw">static CNeuronMLMHAttentionOCL cTransformerEncoder; class=class="str">"cmt">// 变换器编码器,多头关注度 class="kw">static CNeuronConvOCL cProjection; class=class="str">"cmt">// 卷积预测层 class="kw">static CNeuronConvOCL cLinearModel[]; class=class="str">"cmt">// 线性模块动态数组 class="kw">public: class="type">bool Init(class="type">int window, class="type">int window_key, class="type">int heads, class="type">int count, class="type">int at_layers, class="type">int &mlp[]); class="type">bool feedForward(CNeuronBaseOCL *prev); class="type">bool calcInputGradients(CNeuronBaseOCL *prev); };
训练收尾时评论者梯度与EA自停
评论者网络在第二步里先对扮演者动作给出评估,随后关闭自身学习模式,只负责把『动作评估值』与『该状态下实际可能收益』之间的偏差梯度向后传播。这里有个隐含约束:训练过程中扮演者策略应持续收敛,更新参数后拿到的期望奖励不应劣于与环境交互时的实测奖励。 每完成一次迭代,程序会把训练进度推给用户在图表上查看,并进入下一轮。需要盯紧的是每一步操作都不能漏——尤其迭代全部跑完后,要清掉图表注释、把结果写进终端日志,然后主动终止EA。外汇与贵金属行情高波动,这类自停逻辑能避免过拟合模型在实盘误触发。 下面这段 CNeuronClientOCL 类定义了评论者的骨架,Init 里先校验 MLP 层数再扩数组,是训练收尾前网络初始化的关键入口。 class CNeuronClientOCL : public CNeuronBaseOCL // 评论者类继承自基础OCL神经元 { protected: //--- Attention CNeuronMLMHAttentionOCL cTransformerEncoder; // 多头注意力编码器 CNeuronConvOCL cProjection; // 投影卷积层 //--- Linear model CNeuronConvOCL cLinearModel[]; // 线性模型卷积层数组 //--- CNeuronBaseOCL cInput; // 输入基础层 //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); // 前向传播 virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); // 更新输入权重 virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); // 计算输入梯度 public: CNeuronClientOCL(void) {}; // 构造函数 ~CNeuronClientOCL(void) {}; // 析构函数 //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, // 初始化方法 uint window, uint window_key, uint heads, uint at_layers, uint count, uint &mlp[], ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronClientOCL; } // 返回神经元类型 //--- virtual bool Save(int const file_handle); // 保存权重 virtual bool Load(int const file_handle); // 加载权重 //--- virtual void SetOpenCL(COpenCLMy *obj); // 绑定OpenCL对象 virtual void TrainMode(bool flag); // 切换训练模式 //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); // 软更新权重 }; bool CNeuronClientOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint at_layers, uint count, uint &mlp[], ENUM_OPTIMIZATION optimization_type, uint batch) { uint mlp_layers = mlp.Size(); // 取MLP层数 if(mlp_layers == 0) // 层数为0 return false; // 直接返回失败 if(ArrayResize(cLinearModel, mlp_layers + 1) != (mlp_layers + 1)) // 扩数组多留1层 return false; // 扩容不符预期则失败
class CNeuronClientOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class=class="str">"cmt">//--- Attention CNeuronMLMHAttentionOCL cTransformerEncoder; CNeuronConvOCL cProjection; class=class="str">"cmt">//--- Linear model CNeuronConvOCL cLinearModel[]; class=class="str">"cmt">//--- CNeuronBaseOCL cInput; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class="kw">public: CNeuronClientOCL(class="type">void) {}; ~CNeuronClientOCL(class="type">void) {}; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint at_layers, class="type">uint count, class="type">uint &mlp[], ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronClientOCL; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle); class="kw">virtual class="type">bool Load(class="type">int const file_handle); class=class="str">"cmt">//--- class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class="kw">virtual class="type">void TrainMode(class="type">bool flag); class=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau); }; class="type">bool CNeuronClientOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint at_layers, class="type">uint count, class="type">uint &mlp[], ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { class="type">uint mlp_layers = mlp.Size(); if(mlp_layers == class="num">0) class="kw">return false; if(ArrayResize(cLinearModel, mlp_layers + class="num">1) != (mlp_layers + class="num">1)) class="kw">return false;