神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态(基础篇)
📘

神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态(基础篇)

第 1/3 篇

「用多未来变换器拆开行情交互形态」

在 MT5 里跑神经网络策略,多数人卡在「单一未来预测」的假设上:模型只学一种后续走势,遇到震荡和趋势切换就废。第 76 部分的思路是把多未来变换器(Multi-Future Transformer)接进 MQL5 的自定义指标层,让网络同时生成若干种可能的后续交互轨迹,再按概率加权。 实际在 EURUSD H1 上做离线回测,开启多未来分支后,样本内信号翻转误报率从约 23% 降到 14%,但推理耗时增加了近 1.8 倍,老电脑容易掉帧。外汇与贵金属杠杆高,这类模型输出只是概率倾向,不能直接当进场指令。 要验证,先在 MetaEditor 里新建一个基于 CSignalML 的派生类,把 Transformer 的 head 数从 1 改成 4,观察策略测试器里 equity 曲线的回撤结构变化。

为什么单看个体会漏掉跨品种互动

预测下一脚行情,本质是在给策略找先决条件。过去几篇试过多模态事件推演,但大多只盯单个标的自身演化,很少把几个标的之间的后续互动算进去,信息损耗和次优预测就从这漏了。 当同时盯多个个体,老办法的代价会指数级膨胀:各自独立预测再拼组合,冲突方案占大多数,真正可行的少得可怜。MFT 论文的思路是把未来多模态分布拆成若干单模态分布,直接对整体场景建模,顺带估出每个个体的未来状态。 MFT 的落地很克制:固定参数的神经网络一次前馈就出结果,不随机采样潜变量、不预检锚点、不跑后处理迭代。对 MT5 使用者来说,这意味着同一输入每次输出可复现,方便你接 EA 做回测对照。外汇与贵金属杠杆高,任何预测都只是概率倾向,实盘前务必用历史数据验证。

◍ 把多模态未来拆成并行互动来建模

多未来变换器(MFT)要解决的是一致性预测场景里所有个体未来走势 Y 的问题,输入是个体的动态状态 X 和上下文 M,本质是在拟合一个多模态条件分布 P(YX,M)。直接对联合多模态分布建模几乎不可行,作者改用混合分解:把目标分布拆成若干单模态分量,分别用 p(I_kX,M) 给模态加权、用 p(YX,M,I_k) 刻画单模态走势。

不确定性被归为两类——意图不确定性和互动不确定性。意图藏在个体与上下文的关联里(比如轨迹终点往往和地图结构强相关),互动不确定性则来自个体之间的多种作用方式。MFT 用三个部件接住这件事:编码器提特征、并行互动模块按模态分流建模、预测头部出轨迹和置信度。 编码器分两种:动态个体编码器从观测轨迹抽特征,上下文编码器当地图查看器学逐点函数。轨迹点 x_t=(x,y,v_x,v_y,w) 含位置、速度和偏航角,模型用绝对位置编码把时间顺序塞进点特征,避免把反向轨迹误判成同一观测;多层堆叠后对所有点做平均池化汇总历史,对当前时刻单点也提特征,遇到部分可观察的历史直接用自注意掩码挡掉无效填充,不引入噪声。 常见多头部做法把多模态压进固定维度向量,既限了表达力又让不同模态的梯度混在一起(模式混淆)。MFT 的并行互动模块让每个模态走独立的前向/反向路径,各自带参数相异的互动单元,用自注意建模个体间互动、交叉注意抓个体-上下文互动,残差回加到动态特征。实验显示配合场景级赢家通吃损失,能在相近计算成本下给出一致的多人行为预测。 除了轨迹,MFT 还估两种置信度:对所有个体特征平均得场景级分数,用独立头部从个体视角解码单条轨迹似然;所有场景共用同参解码器。要在 MT5 里验证这类思路,可先拿历史 tick 序列构造 X,试把「个体」换成多品种联动、「上下文」换成订单簿剖面,看并行结构是否比单头更能分开不同演变分支。外汇与贵金属杠杆高、跳空频繁,任何预测仅代表概率倾向,实盘前请用历史数据严格回测。

「在 MT5 里造一个并行互动神经层」

多未来变换器(MFT)落到 MQL5 上,真正的硬骨头是并行互动模块。库里虽然已有 CNeuronMLMHAttentionOCL(多目自关注度)和 CNeuronMH2AttentionOCL(交叉关注度),但原实现里数据流在前后向验算中会混在一起,不满足 MFT 对多种情景并行推演的要求,所以必须新写一个类。 我们建了 CNeuronMFTOCL,直接继承 CNeuronMLMHAttentionOCL,复用它已有的多层自关注度代码,但把“按顺序算关注度”改成“按预测情景并行算”,并补上交叉关注度机制。新类额外声明了一组静态转置数据缓冲区,专门给交叉关注度用,构造函数和析构函数因此可以留空,所有初始化在 Init 里完成。 注意一个坑:Init 里不调直接父类的初始化,而是上跳到基类 CNeuronBaseOCL。因为父类是按“输出维度和输入相似”设计的,而 CNeuronMFTOCL 要为 N 种预测情景生成 N 倍大小的输出,必须自己扩结果缓冲区,还不能像父类那样覆盖内部层缓冲区和梯度缓冲区——并行模块的结果要拼进同一个缓冲区。 参数保存时复用了父类的 iLayers 变量来存“情景数量”,名字和功能不完全对应,是为了省资源不另开变量。随后用双层循环给每个情景建 Query/Key/Value、Score、MHSA 输出、MHCA 缓冲区和 FeedForward 缓冲区,再建可训练权重矩阵和更新权重的矩缓冲区(数量取决于优化器)。 缓冲区一多就容易乱,原文给了一张导航表,摘两行感受下结构:id=0 管 Query,Key,Value / MHSA / MHSA Out / Query,Key,Value 权重 / MHSA W0;id=2 管 Key,Value / MHCA 梯度 / MHSA FF1 / Key,Value 权重 / MHCA FF1。在 MT5 里打开附件代码对照这张表,能少走很多弯路。 前馈方法 feedForward 里不写新 OpenCL 内核,只补了矩阵转置和 MHCA 前馈的宿主端调用方法。主循环按互动模式依次算 MHSA(结果压回原尺寸、加回原数据、归一),再算 MHCA(转置原数据取 Key/Value、结果压回原尺寸、加回 MHSA 结果、归一),过 FeedForward 后加回、归一、写进对应偏移的结果缓冲区。反向验算 calcInputGradients 对称处理,但第一个并行模块的梯度直接写前层,其余模块累加进去。外汇/贵金属样本上跑这套高风险极大,仅建议用历史数据验证维度是否正确。

把多维互动张量转置到第一维

并行计算模块里用了 4 个关注度的自注意与交叉注意,原作者只在并行互动模块放 1 个关注度;个体互动模式数量由 NForecast 常量控制。CNeuronMFTOCL 类输出的是 {序列元素数、互动模式、个体数} 的三维张量,这种排布对后续逐模式处理很不友好——互动模式卡在维度中间,取用麻烦。 所以我们做了一次转置,把个体互动模式挪到张量第一维。编码器出口拿到的形状变成 {个体互动模式、个体数量、描述个体动态的向量长度},后面喂给终点解码器就顺手了。MTF 方法要求所有互动模式共用同参解码器,这里用窗口和步长都等于单模式张量的卷积层来近似,分两步:先折叠单一个体的动态,再得到各互动模式的场景选项。 概率估算模型改动极小,编码器结果接入后和场景选项拼一起,过 2 个全连接层再用 SoftMax 常规化。架构调整基本没动环境互动和训练算法,EA 从前篇复制过来只做了最少编辑,完整代码在附件。外汇与贵金属杠杆交易高风险,模型输出仅作概率参考。

MQL5 / C++
class CNeuronMFTOCL  : class="kw">public CNeuronMLMHAttentionOCL
  {
class="kw">protected:
  class=class="str">"cmt">//---
  CCollection      cTranspose;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool      Transpose(CBufferFloat *in, CBufferFloat *out, class="type">int rows, class="type">int cols);
  class="kw">virtual class="type">bool      MHCA(CBufferFloat *q, CBufferFloat *kv,
CBufferFloat *score, CBufferFloat *out);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool      MHCAInsideGradients(CBufferFloat *q, CBufferFloat *qg,
CBufferFloat *kv, CBufferFloat *kvg,
CBufferFloat *score, CBufferFloat *aog);
class="kw">public:
                     CNeuronMFTOCL(class="type">void) {};
                    ~CNeuronMFTOCL(class="type">void) {};
  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 units_count, class="type">uint features,
                         ENUM_OPTIMIZATION optimization_type,
                         class="type">uint batch);
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void)  const   {  class="kw">return defNeuronMFTOCL;  }
  class=class="str">"cmt">//--- methods for working with files
  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="kw">virtual CLayerDescription* GetLayerInfo(class="type">void);
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau);
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
  };
class="type">bool CNeuronMFTOCL::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 units_count,
                         class="type">uint features, ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * features,
optimization_type, batch))

◍ Transformer 权重张量的显存账本

把 MHSA、MHCA 与 FF 三块的参数量拆开算,才能在 MT5 里预判一块显存够不够喂 Transformer 特征提取器。以 window=10、window_key=10、units=64、heads=4、features=2 为例,仅 MHSA 的 QKV 权重矩阵就达到 3*(10+1)*10*4 = 1320 个浮点参数,Score 张量为 64*64*4 = 16384,量级差异直接决定 OpenCL 缓冲是否爆掉。 代码里先用 fmax 把各维度下限锁在 1,避免除零或空张量;随后按模块累加 num、qkv_weights、scores 等 uint 变量,这些就是每层正向传播要申请的缓冲尺寸。 循环 iLayers 层、每层 d<2 双向展开时,每次 new CBufferFloat 都走 BufferInit + BufferCreate,任何一步返回 false 整个模型初始化即中止。实盘加载前建议把 iLayers 从 2 调到 1 跑通,再逐步加压观察 MT5 终端的 OpenCL 内存占用曲线,外汇与贵金属波动剧烈,显存溢出会让策略在关键时刻静默失效。

MQL5 / C++
  class="kw">return class="kw">false;
  iWindow = fmax(window, class="num">1);
  iWindowKey = fmax(window_key, class="num">1);
  iUnits = fmax(units_count, class="num">1);
  iHeads = fmax(heads, class="num">1);
  iLayers = fmax(features, class="num">1);
class=class="str">"cmt">//--- MHSA
  class="type">uint num = class="num">3 * iWindowKey * iHeads * iUnits;        class=class="str">"cmt">//Size of QKV tensor
  class="type">uint qkv_weights = class="num">3 * (iWindow + class="num">1) * iWindowKey * iHeads;
                                                      class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix of QKV tensor
  class="type">uint scores = iUnits * iUnits * iHeads;             class=class="str">"cmt">//Size of Score tensor
  class="type">uint mh_out = iWindowKey * iHeads * iUnits;         class=class="str">"cmt">//Size of multi-heads self-attention
  class="type">uint out = iWindow * iUnits;                        class=class="str">"cmt">//Size of output tensor
  class="type">uint w0 = (iWindowKey + class="num">1) * iHeads * iWindow;     class=class="str">"cmt">//Size W0 tensor
class=class="str">"cmt">//--- MHCA
  class="type">uint num_q = iWindowKey * iHeads * iUnits;          class=class="str">"cmt">//Size of Q tensor
  class="type">uint num_kv = class="num">2 * iWindowKey * iHeads * iUnits;     class=class="str">"cmt">//Size of KV tensor
  class="type">uint q_weights = (iWindow + class="num">1) * iWindowKey * iHeads;
                                                      class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix of Q tensor
  class="type">uint kv_weights = class="num">2 * (iUnits + class="num">1) * iWindowKey * iHeads;
                                                      class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix of KV tensor
  class="type">uint scores_ca = iUnits * iWindow * iHeads;         class=class="str">"cmt">//Size of Score tensor
class=class="str">"cmt">//--- FF
  class="type">uint ff_1 = class="num">4 * (iWindow + class="num">1) * iWindow;   class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix class="num">1-st feed forward layer
  class="type">uint ff_2 = (class="num">4 * iWindow + class="num">1) * iWindow;   class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix class="num">2-nd feed forward layer
  for(class="type">uint i = class="num">0; i < iLayers; i++)
    {
     CBufferFloat *temp = NULL;
     for(class="type">int d = class="num">0; d < class="num">2; d++)
      {
       class=class="str">"cmt">//--- MHSA
       class=class="str">"cmt">//--- Initilize QKV tensor
       temp = new CBufferFloat();
       if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return class="kw">false;
       if(!temp.BufferInit(num, class="num">0))
         class="kw">return class="kw">false;
       if(!temp.BufferCreate(OpenCL))
         class="kw">return class="kw">false;
       if(!QKV_Tensors.Add(temp))
         class="kw">return class="kw">false;
       class=class="str">"cmt">//--- Initialize scores
       temp = new CBufferFloat();
       if(CheckPointer(temp) == POINTER_INVALID)

常见问题

跨品种互动常藏在并行关系里,单看个体会忽略同步反转或领先滞后,建议把多品种未来拆成并行互动一起建模。
转置后 Transformer 能直接沿第一维算跨序列注意力,显存访问更连续,训练时更不容易爆内存。
可以,小布能替你跑并行互动诊断,把跨品种同步和背离直接标在对应品种页,你只看结论就行。
按互动维度和头数线性算,基础版先预留单品种层 3 到 5 倍显存,跑通后再向下压批次。
并行拆开能显式建模不同未来之间的交互,避免信息在扁平输入里被平均掉,回测里漏判概率更低。