神经网络变得简单(第 83 部分):构象时空连续关注度转换器算法(基础篇)
📘

神经网络变得简单(第 83 部分):构象时空连续关注度转换器算法(基础篇)

第 1/3 篇

「把时空注意力塞进MT5的构象转换器」

这一节给出一种叫「构象」时空连续关注度转换器(Conformational Spatio-Temporal Continuous Attention Transformer)的思路,目标是在 MT5 里用神经网络处理多品种、多周期的价格序列,而不是传统的单根 K 线输入。作者 Dmitriy Gizlyk 在 2025 年 1 月 6 日发布,原文标注阅读量 480,说明这套方法还很小众,适合自己跑通验证。 核心改动在于注意力机制不再只看时间维,而是把「空间」(不同品种 / 不同特征通道)和「时间」做成连续坐标,用构象参数控制注意力在时空里的偏移形状。这样做可能让模型在贵金属跨周期联动上比 LSTM 更敏感,但外汇高杠杆环境下过拟合概率也更高,需谨慎。 要落地,先开 MT5 用自己的历史数据跑一小段序列,观察注意力权重是否集中在已知事件窗口(如非农、利率决议)。若权重散乱无聚焦,说明构象参数没调对,而不是算法无效。

◍ 把气象算法搬进金融行情

金融市场的不可预测性常被拿来和天气波动类比,但人类在气象建模上已经走得很远,如今短期预报的可信度远高于多数主观交易判断。既然气象学家能用连续场建模捕捉大气演化,行情的“天气”是否也能套用同类思路? 本文要落地的核心,是一套原本为天气预报设计的「构象」时空连续关注度转换器,出自论文《构象:在天气预报的视觉变换器中嵌入连续关注度》。作者把连续关注度算法,和前一篇聊过的神经 ODE 做了结合。 对交易者来说,重点不是复现论文,而是意识到:MT5 上跑的自定义指标,完全可以用连续关注度替代传统滑动窗口,去捕捉价格时空里的慢变结构。外汇与贵金属杠杆高、跳空频繁,这类方法只降低盲区,不消除风险。

用连续注意力给天气做时空微分建模

构象(Conformer)这套思路原本是为天气预报设计的,但它把「连续变化」塞进注意力机制的做法,对做高频价格行为建模的人有借鉴价值。它不再只预测某个时间戳 t 的值,而是把输入做成 (X_{N*W*H}, T) 的形态:N 是变量数(温度、风速、压力等),W*H 是空间分辨率,T 是时间步;模型学的是从 t 到 t+1 的演变,而不是单点快照。 核心改动在注意力层。传统 Transformer 算的是同一环境状态下元素间的依赖,构象改成算「不同环境状态下同一变量」的依赖:给每个样本的每变量分配独立 Q、K、V,在时间 t0 和 t1 的两份样本 X0、X1 上,算相同变量的相似性导数,再做关注度加权。这样模型能跨整个序列抓同一变量的连续交互,而不是被某个静止切面限制死。 为了保住数值稳定,作者把导数归一化单独拎出来当成架构零件,直接作用在微分后的特征上,并对比了两种常见归一化加预微分层的影响。求解器选了自适应步长的 Dormand-Prince(Dopri5),比固定步长精度更高,配合神经 ODE 层把天气随时间的极小变化也逼出来。外汇和贵金属行情同样是高动态系统,直接套物理方程成本高,这类连续潜在表示思路值得在 MT5 上做小规模验证,但杠杆市场高风险,回测不代表实盘概率。

「在 MT5 里搭一个 Conformer 神经层」

想把构象(Conformer)方法跑在 MT5 上,核心是先写一个从 CNeuronBaseOCL 派生的 CNeuronConformer 类。它不靠主程序端存矩阵副本,只在 OpenCL 环境开一块 iScore 缓冲区,局部变量只存指针——这样能少搬一次显存数据。 类里用 5 个变量锁死层架构:iWindow(单参数向量长度)、iDimension(Q/K/V 维度)、iHeads(头数)、iVariables(环境状态参数量)、iCount(序列长度)。生成 QKV 用卷积层 cQKV 一次并行出 3 个实体,卷积步长等于单变量嵌入长度,过滤器数 = 变量数 × 状态数 × 3 × 头数。 连续关注度(Continuous Attention)是这层的硬骨头。它要用 Q 和 K 对时间的偏导,训练时没解析导数,就按几何意义用相邻状态差分近似:把时间步长当 1,算前一态→当前态、当前态→下一态两次转换的平均变化。TimeDerivative 内核按「状态数 × 变量数 × 头数」三维启动,首尾元素只有单边过渡,直接用现有数据不补零。 前馈验算 feedForward 里,先跑 cQKV 出实体张量,调 attentionOut 丢进两个 OpenCL 内核(先算导数再算 FeedForwardContAtt),多头结果经 cW0 降维后加回输入并归一化,后面接 cNODE 数组(常微分方程层)和 cFF 数组(前馈层),每层输入输出都累加+归一化。 反向传播靠 HiddenGradientContAtt 和 HiddenGradientTimeDerivative 两个内核,把误差梯度按 SoftMax 导数调过再回传 Q/K/V 及其时间偏导;calcInputGradients 里靠之前换好的梯度缓冲区指针,直接让下一层读 FeedForward 末层缓冲,免一次复制。所有可学参数都在内层,更新时逐个调内部对象同名方法即可。 训练模型架构上,输入按我的切法:最后一根烛条 4 数 + RSI 1 + CCI 1 + ATF 1 + MACD 2,共 9 维;CreateDescriptions 里先批量归一,再按模块做单状态嵌入,加谐波位置编码,末尾叠 5 个连续构象层,核心交叉关注度层估账户状态与编码器压缩表达的依赖。这套切分只是个人选法,你改数据窗口必须同步改嵌入窗口大小。外汇/贵金属行情跳变剧烈,这类模型过拟合风险高,上 MT5 验证前先用历史 Tick 跑小样本回测。

◍ 训练循环里怎么调扮演者和评论者

模型结构改动不影响与环境交互的流程,所以收集初始训练数据可以直接复用旧的 Conformer\Research.mq5 EA,之前攒下的数据集也能继续喂给新结构——输入分析逻辑变了,但数据结构没动。 Study.mq5 里的 Train 方法做了针对性修改。开头先按轨迹盈利能力生成选择概率向量,盈利高的验算被抽中的概率更大,这是经验回放采样前的偏好设定。 外部循环从回放缓冲区采样轨迹和初始状态,清掉编码器循环缓冲并判定最终状态;内部嵌套循环逐状态前馈:先载环境状态进编码器,再载扮演者动作让评论者评测,然后把评测值向实际奖励对齐,误差梯度反向传给编码器。 扮演者政策分两步调。第一步压低与实际动作的偏差,把政策锁在训练集分布附近;第二步关掉评论者训练模式,用它的评测误差梯度传播给扮演者做政策调整。两种情况下梯度都会进编码器,目的是把环境“视野”的信息量逼到最大。 全部参数更新后仅向用户报进度,循环耗尽后清注释、打日志并终止 EA。外汇与贵金属行情高波动,这套训练若直接上实盘需先在小布盯盘里跑 MT5 回测验证分布稳定性。

MQL5 / C++
<span class="keyword">class</span> CNeuronConformer&nbsp;&nbsp;:&nbsp;&nbsp;<span class="keyword">class="kw">public</span> CNeuronBaseOCL
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iWindow;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iDimension;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iHeads;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iVariables;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iCount;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cQKV;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cdQKV;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iScore;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cAttentionOut;
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cW0;
&nbsp;&nbsp; CNeuronNODEOCL&nbsp;&nbsp;&nbsp;&nbsp;cNODE[<span class="number">class="num">3</span>];
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cFF[<span class="number">class="num">2</span>];
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;feedForward(CNeuronBaseOCL *NeuronOCL);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;attentionOut(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AttentionInsideGradients(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;updateInputWeights(CNeuronBaseOCL *NeuronOCL);
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CNeuronConformer(<span class="keyword">class="type">void</span>) {};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~CNeuronConformer(<span class="keyword">class="type">void</span>) {};
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Init(<span class="keyword">class="type">uint</span> numOutputs, <span class="keyword">class="type">uint</span> myIndex, COpenCLMy *open_cl,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> window, <span class="keyword">class="type">uint</span> window_key, <span class="keyword">class="type">uint</span> heads,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> variables, <span class="keyword">class="type">uint</span> units_count,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ENUM_OPTIMIZATION optimization_type,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> batch);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;calcInputGradients(CNeuronBaseOCL *prevLayer);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type(<span class="keyword">class="type">void</span>)&nbsp;&nbsp; <span class="keyword">const</span>&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">class="kw">return</span> defNeuronConformerOCL;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- methods for working with files</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Save(<span class="keyword">class="type">int</span> <span class="keyword">const</span> file_handle);

Conformer 神经元初始化与显存分配

在 MT5 的 OpenCL 神经网络框架里,CNeuronConformer::Init 负责把多头注意力与前馈模块一次性铺开。它先调用基类 CNeuronBaseOCL::Init,输入维度直接拉到 window * variables * units_count,这意味着显存占用随四个参数连乘膨胀,调参时先算清楚这张表。 cQKV 作为 Query/Key/Value 的合并层,输出宽度是 3 * window_key * heads,而 cdQKV 反向梯度缓冲则按 3 * iDimension * iHeads * iVariables * iCount 申请。若 heads=4、window_key=16、variables=1、units_count=8,仅这一项正向缓冲就吃掉 1536 个 float,约 6KB,小显存卡容易在 iScore 的 AddBuffer 返回负值后直接 init 失败。 循环里 cNODE[0..2] 承接位置编码类子层,cFF[0] 把窗口维扩到 4 倍再经 cFF[1] 压回,梯度索引以最后一层 cFF[1] 为准。下面这段 Init 主体可直接贴进你的 .mqh 做验证,跑不通就查 OpenCL 上下文是否提前 Bind。

MQL5 / C++
class="type">bool CNeuronConformer::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 variables, class="type">uint units_count,
                  ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * variables * units_count,
optimization_type, batch))
     class="kw">return false;
  if(!cQKV.Init(class="num">0, class="num">0, OpenCL, window, window, class="num">3 * window_key * heads, variables * units_count,
optimization, iBatch))
     class="kw">return false;
  iWindow = class="type">int(fmax(window, class="num">1));
  iDimension = class="type">int(fmax(window_key, class="num">1));
  iHeads = class="type">int(fmax(heads, class="num">1));
  iVariables = class="type">int(fmax(variables, class="num">1));
  iCount = class="type">int(fmax(units_count, class="num">1));
  if(!cdQKV.Init(class="num">0, class="num">1, OpenCL, class="num">3 * iDimension * iHeads * iVariables * iCount, optimization,
iBatch))
     class="kw">return false;
  iScore = OpenCL.AddBuffer(class="kw">sizeof(class="type">float) * iCount * iHeads * iVariables * iCount,
CL_MEM_READ_WRITE);
  if(iScore < class="num">0)
     class="kw">return false;
  if(!cAttentionOut.Init(class="num">0, class="num">2, OpenCL, iDimension * iHeads * iVariables * iCount,
optimization, iBatch))
     class="kw">return false;
  if(!cW0.Init(class="num">0, class="num">3, OpenCL, iDimension * iHeads, iDimension * iHeads, iWindow,
 iVariables * iCount, optimization, iBatch))
     class="kw">return false;
  for(class="type">int i = class="num">0; i < class="num">3; i++)
     if(!cNODE[i].Init(class="num">0, class="num">4 + i, OpenCL, iWindow, iVariables, iCount, optimization, iBatch))
        class="kw">return false;
  if(!cFF[class="num">0].Init(class="num">0, class="num">7, OpenCL, iWindow, iWindow, class="num">4 * iWindow, iVariables * iCount,
optimization, iBatch))
     class="kw">return false;
  if(!cFF[class="num">1].Init(class="num">0, class="num">8, OpenCL, class="num">4 * iWindow, class="num">4 * iWindow, iWindow, iVariables * iCount,
optimization, iBatch))
     class="kw">return false;
  if(getGradientIndex() != cFF[class="num">1].getGradientIndex())
     SetGradientIndex(cFF[class="num">1].getGradientIndex());
class=class="str">"cmt">//---
  class="kw">return true;
  }

常见问题

先在行情数据上构造时空切片作为输入,再用连续注意力做微分建模,别直接套天气参数,先在小周期上跑通再放大周期。
初始化时按批次动态分配显存,降低单批序列长度,并复用中间激活值,能显著压低占用。
可以,小布能直接读你导出的注意力分布图,标出异常集中的时空区域,提醒你大概率过拟合的位置。
给两者设不同的学习率,评论者更新慢半拍,扮演者探索步长别一次拉太大,观察奖励曲线是否平滑。
连续注意力不固定感受野,能随行情波动自适应聚焦异常时段,卷积则容易漏掉突变点,但算力更高。