神经网络变得简单(第 83 部分):构象时空连续关注度转换器算法(基础篇)
「把时空注意力塞进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 回测验证分布稳定性。
<span class="keyword">class</span> CNeuronConformer : <span class="keyword">class="kw">public</span> CNeuronBaseOCL { <span class="keyword">class="kw">protected</span>: <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">int</span> iWindow; <span class="keyword">class="type">int</span> iDimension; <span class="keyword">class="type">int</span> iHeads; <span class="keyword">class="type">int</span> iVariables; <span class="keyword">class="type">int</span> iCount; <span class="comment">class=class="str">"cmt">//---</span> CNeuronConvOCL cQKV; CNeuronBaseOCL cdQKV; <span class="keyword">class="type">int</span> iScore; CNeuronBaseOCL cAttentionOut; CNeuronConvOCL cW0; CNeuronNODEOCL cNODE[<span class="number">class="num">3</span>]; CNeuronConvOCL cFF[<span class="number">class="num">2</span>]; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> attentionOut(<span class="keyword">class="type">void</span>); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> AttentionInsideGradients(<span class="keyword">class="type">void</span>); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> updateInputWeights(CNeuronBaseOCL *NeuronOCL); <span class="keyword">class="kw">public</span>: CNeuronConformer(<span class="keyword">class="type">void</span>) {}; ~CNeuronConformer(<span class="keyword">class="type">void</span>) {}; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Init(<span class="keyword">class="type">uint</span> numOutputs, <span class="keyword">class="type">uint</span> myIndex, COpenCLMy *open_cl, <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, <span class="keyword">class="type">uint</span> variables, <span class="keyword">class="type">uint</span> units_count, ENUM_OPTIMIZATION optimization_type, <span class="keyword">class="type">uint</span> batch); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> calcInputGradients(CNeuronBaseOCL *prevLayer); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span> Type(<span class="keyword">class="type">void</span>) <span class="keyword">const</span> { <span class="keyword">class="kw">return</span> defNeuronConformerOCL; } <span class="comment">class=class="str">"cmt">//--- methods for working with files</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> 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。
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; }