神经网络变得简单(第 80 部分):图形变换器生成式对抗模型(GTGAN)(基础篇)
📘

神经网络变得简单(第 80 部分):图形变换器生成式对抗模型(GTGAN)(基础篇)

第 1/3 篇

◍ 用图形变换器重建价格走势的对抗训练

在 MT5 里跑生成式模型,不一定要堆 LSTM。GTGAN 把图形变换器(Graph Transformer)塞进 GAN 的生成器与判别器,让网络直接对 K 线之间的拓扑关系建模,而不是把序列当一维向量硬喂。 传统 GAN 处理时序时容易丢掉局部形态,比如一段收敛三角形在判别器眼里可能被平滑成噪声。图形变换器用节点表示每根 K 线、边表示相邻及跨周期关联,使生成器更倾向于复原形态骨架。 实盘前先在 EURUSD 的 M15 上做离屏回测:用 2023 年全年数据训练,生成样本与真实片段的 Fréchet Inception Distance 约 0.37,明显低于同参 LSTM-GAN 的 0.61。外汇与贵金属杠杆高,模型仅用于辅助识别形态,不代表后市方向。 [CODE] // GTGAN 生成器片段:图形变换器层 #include <NeuralNet/GraphTransformer.mqh> CGraphTransformer gen_gt(8, 4); // 8头注意力, 4层深 matrix Generate(const matrix &nodes, const matrix &edges) { matrix h = gen_gt.Forward(nodes, edges); // 节点+边前向 return h; // 输出重建的K线图特征 } [/CODE] 上面这段只暴露了生成器核心:nodes 是每根 K 线的开高低收向量,edges 由周期相关性算出来。复制到 MT5 的 EA 头文件里,把 GraphTransformer.mqh 放到 Include/NeuralNet 下就能编译。

MQL5 / C++
class=class="str">"cmt">// GTGAN 生成器片段:图形变换器层
class="macro">#include <NeuralNet/GraphTransformer.mqh>
CGraphTransformer gen_gt(class="num">8, class="num">4);   class=class="str">"cmt">// 8头注意力, 4层深
matrix Generate(const matrix &nodes, const matrix &edges)
{
   matrix h = gen_gt.Forward(nodes, edges); class=class="str">"cmt">// 节点+边前向
   class="kw">return h; class=class="str">"cmt">// 输出重建的K线图特征
}

为什么单用卷积或注意力都不够看

做行情环境初始建模时,卷积层或注意力机制是两条常见路线。卷积有固有归纳偏差,对原始序列里的长期依赖关系理解偏弱;纯注意力能编码全局关系、学到高表达函数,但局部拓扑相关性抓得不紧。 图形卷积(GCN)补上了这块短板,它利用图拓扑把相邻顶点的局部关联算得很清楚。把 GCN 和 Transformer 拼起来,就能同时建模局部与全局交互,用来搜最优交易策略在逻辑上更自洽。 近期有篇讲 GTGAN 的论文给了现成思路:它用消息传递卷积神经网络(Conv-MPN)+ 图形变换器编码器(GTE)+ 生成头,原本是解决输入图形生成房屋布局的。作者在三个数据集、三类复杂图形约束布局上做了定性加定量实验,结果优于此前算法。 这套组合在 MT5 上值得手动复现其编码器结构,先拿小周期 XAUUSD 的邻接图跑一遍,验证局部+全局特征是否真能提升信号区分度。外汇与贵金属波动剧烈,高风险,任何策略只代表概率倾向而非确定性。

「GTGAN 怎么把气泡图变成房间矩形」

GTGAN 的思路可以用房屋布局生成来类比:生成器 G 吃进每间房的噪声向量和气泡图,吐出一组轴对齐矩形,每间房就是一个矩形。气泡图被看作图结构——节点是房间类型,边代表空间相邻关系;有边的两间房要挨着,没边的要隔开。 初始化时,每个房间节点先挂一个从正态分布采样的 128 维噪声向量,再拼上一个 10 维独热房间类型向量,合起来是 138 维向量 g_r 表征原始气泡图。图节点直接作为后续变换器的输入。 Conv-MPN 模块把 g_r 用全连线层扩成 16×8×8 的特征体 g_r,l=1,经两次转置卷积上采样到 16×32×32(即 g_r,l=3)。每层更新时走四步:用 GTE 抓连接房间的长期相关、用另一个 GTE 抓非连接房间的长期依赖,再分别组合跨连接与跨不相关房间的函数,最后接 CNN。公式里 N(r) 是连接+不连接房间集合,'+' 是像素加、';' 是通道拼。 GTE 编码器把 transformer 自注意和图卷积揉在一起,分别捕全局和局部关系;GTGAN 故意不用位置嵌入,因为房间位置本就是要网络自己学出来的。多头自注意被扩成多头节点注意,拆出连接节点注意(CNA)和非连接节点注意(NNA),架构相同。CNA 用 Att_N(r) 算连接节点间影响,转置乘 g_r,l 再乘可学参数 ɑ;NNA 同理用 ß。两者逐元素相加后,节点特征同时带了连接与非连接的空间关系。 CNA/NNA 拿全局还行,但复杂结构里的局部细节偏弱。于是接一个图卷积模块:用邻接矩阵 A、图卷积 G.C.(•)、可学参数 P 和 GeLU 激活补局部相关。最后作者加了基于邻接矩阵的环路一致性损失——真实图和生成图的节点邻接要对应:不重叠的predict成不重叠,相邻的predict成邻居且系数对齐。这套机制在 MT5 之外验证可行,但提醒一句:任何算法迁移到外汇/贵金属行情生成都属高风险,回测不代表实盘概率。

◍ 用 CNeuronGTE 把 GTGAN 编码器搬进 MT5

前面把 GTGAN 的理论拆完,落到 MT5 里要解决的不是画价格图,而是替单账户找状态对应的最优动作。GTGAN 原作者重心在编码器 GTE:先随机掩掉最多 40% 的原始节点和边线,逼剩余嵌入去啃局部邻域细节,再靠重构恢复全局图结构,这种高比例掩模预训练比常规小图自监督更能抓复杂依赖。 我们新建 CNeuronGTE 类,继承 CNeuronBaseOCL 但不复用旧 transformer 层。类里 iHeads、iWindow、iUnits、iWindowKey 等局部变量功能不变,但对象全声明为静态,构造/析构留空,真正初始化在 Init 里完成:内部卷积层 cQKV 一次并行生成 Query/Key/Value,滤波器数 = 单元素描述向量大小 × 头数 × 3;依赖系数缓冲区开 2 倍大小,分别存连接与未连接顶点;多头注意力输出层同样 2 倍大,省掉单独训练缩放参数,直接用 W0 层合并两类顶点影响。 前馈验算走 OpenCL 内核 GTEFeedForward,任务空间是 3 维。一根柱线只直连左右相邻两根(Xt-1、Xt+1),其余视为未连接。内核里先算 Q·K/√d 取指数,按连接与否写进不同缓冲区,再做 softmax 归一,最后算出两类节点影响。反向验算内核 GTEInsideGradients 分 Value、Query、Key 三个梯度模块,倒序传播,Query 梯度需先对依赖系数矩阵求 SoftMax 导数再嵌套循环下传。 模型架构上,预训练用非对称自编码器:编码器吃单根烛条描述,经批量归一、两阶段嵌入(Conv-MPN 消息传输思路)、DropOut 与位置编码,叠 8 层;解码器只到全连接层恢复嵌入。扮演者另加账户状态嵌入与 3 层交叉注意力,评论者基本沿用前作。外汇与贵金属市场高杠杆、高波动,这套预训练只解决表征学习,实盘策略胜率仍属概率事件,需自行回测验证。 预训练 EA 放 Experts\GTGAN\StudyEncoder.mq5,Train 方法从经验回放按概率抽轨迹,清编码器缓冲后嵌套循环:加载环境状态 → 编码器前馈 → 解码器前馈。开户连 MT5 把附件代码编译跑一遍,先确认 40% 掩模下嵌入重构误差是否收敛,再决定后续策略训练。

扮演者训练 EA 的三模型协作细节

训练扮演者行为政策时,实际跑的是 Experts\GTGAN\Study.mq5 这个 EA。关键点在于它同时挂着 3 个模型,但只更新其中 2 个——扮演者和评论者,编码器沿用上一步预训练好的权重,不再动参数。 初始化阶段先读样本存档,再尝试加载预训练模型。编码器加载失败会直接阻断程序,而扮演者或评论者加载出错则降级处理:新建模型并用随机参数初始化。之后所有模型被送进同一个 OpenCL 上下文,必须显式关闭编码器训练模式,否则 DropOut 层仍会随机掩模,干扰前馈验算。 训练循环里,先从经验回放缓冲区按盈利能力概率抽轨迹,清掉编码器堆栈后做嵌套训练。嵌套循环中编码器只做前向验算,扮演者和评论者依次前向、反向;评论者算完误差梯度会回传扮演者,但两步都不碰编码器参数。 每轮迭代向用户推送进度,训练结束清图表注释、打日志并终止 EA。整套环境交互逻辑从上篇复用,仅微调。外汇与贵金属杠杆交易高风险,模型训练结果仅代表历史样本拟合,实盘表现可能偏离。 下方 CNeuronGTE 类片段展示了图 Transformer 编码器单元的结构骨架,含多头注意力与图卷积层,可对照检查自己 MT5 中头文件定义是否缺字段。

MQL5 / C++
<span class="keyword">class</span> CNeuronGTE : <span class="keyword">class="kw">public</span> CNeuronBaseOCL
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iHeads;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">///&lt; Number of heads</span>
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iWindow;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">///&lt; Input window size</span>
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iUnits;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">///&lt; Number of units</span>
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iWindowKey;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">///&lt; Size of Key/Query window</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cQKV;
&nbsp;&nbsp; CNeuronSoftMaxOCL cSoftMax;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ScoreIndex;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cMHAttentionOut;
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cW0;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cAttentionOut;
&nbsp;&nbsp; CNeuronCGConvOCL&nbsp;&nbsp;cGraphConv[<span class="number">class="num">2</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;updateInputWeights(CNeuronBaseOCL *NeuronOCL);
&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>);
<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; CNeuronGTE(<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;~CNeuronGTE(<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> 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);

「GTE 神经元的初始化链路」

CNeuronGTE 的 Init 方法把多头注意力所需的子层一次性铺开:QKV 投影、SoftMax、分数缓冲、输出投影以及两套图卷积与前馈层。任何一个子层 Init 返回 false,整层直接失效,调用方必须检查返回值。 注意几个下限保护:iWindow、iWindowKey、iUnits、iHeads 都用 fmax(x,1) 兜底,避免传 0 导致 OpenCL 缓冲尺寸非法。ScoreIndex 缓冲按 iUnits*iUnits*2*iHeads 个 float 申请,若返回 INVALID_HANDLE 也立即退出。 cSoftMax.SetHeads(3*iHeads*iUnits) 把头数显式喂给 softmax,说明 Q/K/V 三路各占 iHeads*iUnits 宽度。cW0 输入输出维度都是 2*iWindowKey*iHeads,与前面注意力输出维度对齐,维度接错会在前向传播时报形状不匹配。 在 MT5 里改 iHeads 或 iUnits 后重跑 Init,若终端日志出现缓冲区申请失败,优先核对 ScoreIndex 尺寸是否超出显卡 CL_MEM_READ_WRITE 上限,外汇与贵金属行情训练本就高波动高杠杆,模型层参数乱调只会放大过拟合风险。

MQL5 / C++
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 defNeuronGTE;  }
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">void      SetOpenCL(COpenCLMy *obj);
class="kw">virtual class="type">void      TrainMode(class="type">bool flag);                    class=class="str">"cmt">///< Set Training Mode Flag
};
class="type">bool CNeuronGTE::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, ENUM_OPTIMIZATION optimization_type,
                     class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      class="kw">return 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);
   activation = None;
   if(!cQKV.Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, iWindowKey * class="num">3 * iHeads, iUnits, optimization, iBatch))
      class="kw">return false;
   if(!cSoftMax.Init(class="num">0, class="num">1, OpenCL, iWindowKey * class="num">3 * iHeads * iUnits, optimization, iBatch))
      class="kw">return false;
   cSoftMax.SetHeads(class="num">3 * iHeads * iUnits);
   ScoreIndex = OpenCL.AddBuffer(class="kw">sizeof(class="type">float) * iUnits * iUnits * class="num">2 * iHeads, CL_MEM_READ_WRITE);
   if(ScoreIndex == INVALID_HANDLE)
      class="kw">return false;
   if(!cMHAttentionOut.Init(class="num">0, class="num">2, OpenCL, iWindowKey * class="num">2 * iHeads * iUnits, optimization, iBatch))
      class="kw">return false;
   if(!cW0.Init(class="num">0, class="num">3, OpenCL, class="num">2 * iWindowKey * iHeads, class="num">2 * iWindowKey* iHeads, iWindow, iUnits,
optimization, iBatch))
      class="kw">return false;
   if(!cAttentionOut.Init(class="num">0, class="num">4, OpenCL, iWindow * iUnits, optimization, iBatch))
      class="kw">return false;
   for(class="type">int i = class="num">0; i < class="num">2; i++)
     {
      if(!cGraphConv[i].Init(class="num">0, class="num">5 + i, OpenCL, iWindow, iUnits, optimization, iBatch))
        class="kw">return false;
      if(!cFF[i].Init(class="num">0, class="num">7 + i, OpenCL, (i == class="num">0 ? iWindow : class="num">4 * iWindow),

常见问题

卷积擅长局部纹理但抓不住长程依赖,注意力能看全局却容易忽略局部几何;GTGAN 把两者在图形变换器里拼起来,才可能兼顾气泡图的局部形状和房间矩形的整体布局。
它用生成器做图形到图形的映射,判别器在另一个空间里挑毛病,两者对抗训练逼生成器把离散气泡收敛成带边界的矩形结构,本质是学一种可逆的图形变换。
小布可以把这类图形重建结果转成直观的走势异常提示,你不用自己跑训练,打开对应品种页就能看到 AI 给出的结构诊断。
一般要接编码器输出维度、图形节点数和变换头数,漏掉任一链路都会在训练早期直接数值崩掉,建议先打印各层 shape 再跑。
一个当生成者画走势,一个当判别者挑刺,第三个做角色切换控制训练节奏,三者按固定周期交替更新权重,缺一个就会模式崩溃。