神经网络在交易中的应用:混合图序列模型(GSM++)(基础篇)
📘

神经网络在交易中的应用:混合图序列模型(GSM++)(基础篇)

第 1/2 篇

图序列模型怎么啃下金融非规则数据

传统 Transformer 进金融市场,先撞两堵墙:算起来贵,且原生结构吃不下无序图。图 Transformer 能建模长程依赖、处理非规则结构,但这两道根本坎没完全迈过去。 Dmitriy Gizlyk 在 2026 年 5 月 18 日发布的 MT5 交易系统文章里,引了 GSM++ 这套混合图序列模型。它分三步:图标记化、局部节点编码、全局依赖编码,局部和全局关系一起抓。 关键是分层图标记化——把市场数据压成紧凑序列,还留着拓扑和时间特征。比标准时间序列编码特征提得干净,大批量行情也好消化。再叠 Transformer 加循环机制的混合架构,多项任务表现超过经典图 Transformer。 实证和理论分析都指向同一结论:GSM++ 不止媲美传统图 Transformer,几个关键指标还更优。外汇和贵金属波动无序、杠杆高风险极大,这类模型只提高概率优势,落盘前务必在 MT5 用历史数据自测。

◍ GSM++ 怎么把图拆成序列喂给模型

统一图序列模型(UGS)把复杂图结构压成序列,靠三件事:标记化、局部编码、全局编码。金融市场里参与者和相互作用太多,非线性依赖和隐藏相关性靠普通时序模型抓不全,这套框架就是冲着这个来的。 标记化决定图信息能留多少。节点/边标记化把图拆成孤立元素,连接关系得靠额外位置编码补,序列长度等于节点或边数,计算复杂度高;但高频里要盯单个资产的微观异常,它反而最细。子图标记化把图切成子图降成本,适合挖资产群、资金群的隐藏网络,组合管理和套利用得上。 GSM++ 作者用的层次亲和聚类(HAC)是另一种路数:初始每节点一簇,每步合并相似度最高(编码代价最低边)的两簇,直到并成一棵树。DFS 遍历出的序列带层级位置,BFS 让相似节点紧挨着。实验上这种层次标记化加分层位置编码(最短路径+簇内位置)明显提升了图表示质量。 局部编码通常用 GNN 抓节点间局部依赖,贵金属跨市场相关性、tick 异常都能从微观结构里捞出来。全局编码才管长期依赖:Transformer 恒定敏感度但深了会表征崩溃,RNN 对开头信息敏感随距离衰减,两者都有 U 型效应——序列头尾的标记比中间更重要,排节点时重要元素得挤一起。 计数类任务证明:RNN 宽度配齐节点类数就能准确数数,纯注意力无因果依赖干不了。连通性任务里够深够大的 Transformer 最省参,局部性有限的图用紧凑 RNN 反而够用。Mamba 这类状态空间模型在严格顺序、长记忆场景(如代理序列动作)更稳,稀疏注意力能砍大图算力但得额外挑重要边。 GSM++ 框架落地就是:相似度分层标记化 + 卷积 GNN 局部编码 + Mamba/Transformer 混合全局编码。开 MT5 接行情做资产相关性图时,先想清楚用 BFS 还是 DFS 排节点,比直接堆 Transformer 更影响后面抓不抓得住结构。

「用 CNeuronMoT 落地混合标记化」

GSM++ 原框架建议用可训练聚类挑两种标记化算法再相加,这里直接换成四种标记并行跑,再用 R-MAT 里的注意力池化收口。四种标记分别是节点(单根 K 线)、边缘(相邻两根交互)、子图(三根组内连接)、单变量序列子图,实测能比双标记多覆盖约一倍的结构维度,模型在行情演变时的适应性更强。 新对象 CNeuronMoT 继承 CNeuronMHAttentionPooling,父类已经把注意力池化写好,子类不重复造轮子,只在内部挂了四个标记化部件和一个拼接层。所有内部对象声明为静态,构造析构留空,初始化全塞进 Init 方法,且要求输出维度等于输入维度。 Init 里节点卷积窗=步长=滤波器数=单元素向量长;边缘卷积窗拉到 2,会在末尾补零保长度;子图卷积窗 3;四层统一挂 SoftPlus。SoftPlus 平滑单调、导数恒正,避开 ReLU 的死神经元,回测中训练抖动明显低于 ReLU。拼接层故意不激活,留给定制空间。 前馈 feedForward 把输入同时喂给四条流,单元序列标记先转置再跑 3 元子图卷积再转回。反向 calcInputGradients 要从拼接层拿未乘激活导数的梯度,逐流补乘对应导数再回传,四流梯度累加进源数据缓冲——这里指针交换是为保留已算值,漏掉任一流都会让权重更新偏掉。 外汇与贵金属杠杆高、滑点跳空频繁,这套标记化只解决特征抽取,不预示方向。开 MT5 把附件类丢进你的 EA 模板,先拿 2023 年 XAUUSD 的 M15 跑一遍 Init 参数,看卷积窗补零后张量能否对上。

MQL5 / C++
class CNeuronMoT  :  class="kw">public CNeuronMHAttentionPooling
  {
class="kw">protected:
   CNeuronConvOCL        cNodesTokenizer;
   CNeuronConvOCL        cEdgesTokenizer;
   CNeuronConvOCL        cSubGraphsTokenizer;
   CLayer                cUnitarSubGraphsTokenizer;
   CNeuronBaseOCL        cConcatenate;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                    CNeuronMoT(class="type">void){};
                   ~CNeuronMoT(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 units_count,

MoT 神经元的初始化与张量拼接链路

CNeuronMoT 在 Init 里先调用父类 CNeuronMHAttentionPooling::Init,注意第 4 个参数写死为 4,这决定了多头注意力池化的头数,改这里会直接改变子图特征维度。 随后依次初始化三个 Tokenizer:cNodesTokenizer、cEdgesTokenizer、cSubGraphsTokenizer,输入窗口分别用 iWindow、2*iWindow、3*iWindow,激活函数全设 SoftPlus,索引 index 每次自增以保证图层不冲突。 接着拼了一段子图编码:先 Transpose(iUnits×iWindow) 再 Conv(核 3×1、步长 1、输出 iUnits×iWindow),最后再 Transpose 回去,conv 的激活也是 SoftPlus,transp 则复用 conv 的激活枚举。 cConcatenate 接收 4*iWindow*iUnits 的扁平向量,激活设 None,这意味着 MoT 把节点、边、子图及卷积结果在通道维直接堆叠,下游拿到的是未压缩的联合表征。 feedForward 入口只给了 CNeuronBaseOCL* 形参,具体前向计算需结合父类实现;在 MT5 里把 iWindow 调到 64、iUnits 调到 32,可观察 Concatenate 输出维度变为 8192,显存占用会明显跳升。

MQL5 / C++
class="kw">virtual class="type">int Type(class="type">void) class="kw">const class="kw">override { class="kw">return defNeuronMoT; }
class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle) class="kw">override;
class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle) class="kw">override;
class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override;
};
class="type">bool CNeuronMoT::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                     class="type">uint window, class="type">uint units_count,
                     ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronMHAttentionPooling::Init(numOutputs, myIndex, open_cl, window, units_count, class="num">4, optimization_type, batch))
      class="kw">return class="kw">false;
   class="type">int index = class="num">0;
   if(!cNodesTokenizer.Init(class="num">0, index, OpenCL, iWindow, iWindow, iWindow, iUnits, class="num">1, optimization, iBatch))
      class="kw">return class="kw">false;
   cNodesTokenizer.SetActivationFunction(SoftPlus);
   index++;
   if(!cEdgesTokenizer.Init(class="num">0, index, OpenCL, class="num">2 * iWindow, iWindow, iWindow, iUnits, class="num">1, optimization, iBatch))
      class="kw">return class="kw">false;
   cEdgesTokenizer.SetActivationFunction(SoftPlus);
   index++;
   if(!cSubGraphsTokenizer.Init(class="num">0, index, OpenCL, class="num">3 * iWindow, iWindow, iWindow, iUnits, class="num">1, optimization, iBatch))
      class="kw">return class="kw">false;
   cSubGraphsTokenizer.SetActivationFunction(SoftPlus);
   cUnitarSubGraphsTokenizer.Clear();
   cUnitarSubGraphsTokenizer.SetOpenCL(OpenCL);
   CNeuronConvOCL *conv = NULL;
   CNeuronTransposeOCL *transp = NULL;
   index++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(class="num">0, index, OpenCL, iUnits, iWindow, optimization, iBatch) ||
      !cUnitarSubGraphsTokenizer.Add(transp))
    {
     class="kw">delete transp;
     class="kw">return class="kw">false;
    }
   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(class="num">0, index, OpenCL, class="num">3, class="num">1, class="num">1, iUnits, iWindow, optimization, iBatch) ||
      !cUnitarSubGraphsTokenizer.Add(conv))
    {
     class="kw">delete conv;
     class="kw">return class="kw">false;
    }
   conv.SetActivationFunction(SoftPlus);
   index++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(class="num">0, index, OpenCL, iWindow, iUnits, optimization, iBatch) ||
      !cUnitarSubGraphsTokenizer.Add(transp))
    {
     class="kw">delete transp;
     class="kw">return class="kw">false;
    }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
   index++;
   if(!cConcatenate.Init(class="num">0, index, OpenCL, class="num">4 * iWindow * iUnits, optimization, iBatch))
      class="kw">return class="kw">false;
   cConcatenate.SetActivationFunction(None);
   class="kw">return true;
   }
class="type">bool CNeuronMoT::feedForward(CNeuronBaseOCL *NeuronOCL)
  {

常见问题

把资产间关系建成图、把时间变化建成序列,GSM++用图卷积抓关联、用序列层抓时序,非规则样本也能统一进模型。
不会,它先做图标记化保留邻接信息,再按路径展开成序列,空间和时间两块在混合层才融合。
可以,小布能基于品种相关性和时序杂乱度给出适配提示,省去你手动判读非规则程度的功夫。
必须对齐,图通道和序列通道的隐维要在 Init 里设成一致,否则后面 Concat 会直接报错。
在拼接前逐层打印 shape,重点看图投影层和序列投影层输出,多数错位发生在两路映射未归一到同一隐维。