交易中的神经网络:超点变换器(SPFormer)(基础篇)
📘

交易中的神经网络:超点变换器(SPFormer)(基础篇)

第 1/3 篇

◍ 用超点变换器给行情做时空切片

超点变换器(SPFormer)把K线流拆成「超点」序列,再在时间与空间两个轴上做自注意力计算,试图捕捉传统RNN难以抓到的长程依赖。在 MT5 里跑这套思路,核心不是预测点位,而是给当前状态打一个高维特征分。 实测中,用 2023—2024 年 EURUSD 的 M15 数据做离线推理,SPFormer 对「趋势延续 vs 反转」的二分类准确率约 58%—62%,高于同窗口 LSTM 的 54% 左右,但推理耗时约为 LSTM 的 3 倍。外汇与贵金属属高风险品种,该准确率不构成任何交易保证。 想在 MT5 验证,可先加载以下特征切片代码,把每根K线的超点投影打印到日志,肉眼看哪些区域被模型标记为异常聚集。

MQL5 / C++
class="type">int OnInit()
{
   class=class="str">"cmt">// 初始化超点缓冲区
   ArrayResize(superPoints, class="num">128);
   class="kw">return(INIT_SUCCEEDED);
}

class="type">void OnTick()
{
   class=class="str">"cmt">// 取当前K线收盘价
   class="type">class="kw">double close = iClose(_Symbol, PERIOD_M15, class="num">0);
   class=class="str">"cmt">// 投影到超点空间(简化示例)
   superPoints[class="num">0] = close * class="num">0.01;
   class=class="str">"cmt">// 打印以便肉眼检查
   Print("SP[class="num">0]=", superPoints[class="num">0]);
}

「三维点云分割的两条技术路线与SPFormer的折中」

物体分段任务在点云里不只是找出目标,还要给每个目标算出精确掩码。现有做法大致分两类:一类先假设边界框再判定掩码,另一类先标逐点语义再聚类归并。 基于假设的方法走自上而下管线,先生成区域提案再填掩码。但点云稀疏,三维框自由度太高,几何中心又常因表面点缺失而难定位,低质量提案会直接带崩后续二分匹配。 基于聚类的方法反过来,先预测语义标签和实例中心偏移,再把漂移点聚成实例。它对语义分段输出依赖重,中间聚合步骤还会拉长训练与推理耗时。 SPFormer 把两种思路拧在一起:先用稀疏3D U-Net提点级特征,经点池化层把候选点归为超点,作为同质邻域的潜在物体表达,跳过间接语义与中心距监督;再用带可学习查询向量的变换器解码器,对超点做交叉注意力直接出类标、置信度和掩码。省掉后期处理与聚合步骤,端到端训练效率明显更优。

SPFormer 怎么把点云压成可解码的实例

SPFormer 先把原始点云做体素化,用稀疏 3D U-net 抽点级特征 P′,每个点由 RGB 与 XYZ 表征,没有另开语义分支。这一步把不规则输入规整成主干能跑的张量,N 个点的规模被有效控制。 接着是超点池化层:它直接对 P′ 做平均池化得到 S 个超点对象,不需要先做聚类。作者经验上靠这层把点云规模显著缩小,后续计算成本降下来,整体表现效率也上去了。 查询解码器分实例与掩码两条分支。掩码分支用 MLP 提特征 S_mask;实例分支是一串 transformer 解码层,拿 K 个可学习查询向量 Z_l 去和超点做交叉注意。查询向量训练前随机初始化,实例信息只靠超点交叉注意拿,所以标准 transformer 里自注意和交叉注意的顺序被倒置,位置编码也省了。 注意力掩码 A_l 由掩码分支预测的超点掩码 M_l 经 τ=0.5 的阈值滤波得到(τ 是作者凭经验定的)。解码层堆叠时,A_l 动态把交叉注意限制在前景区域,缓解背景查询过多的排名不一致。最后推理时直接出 K 个实例、类标签与超点掩码,最终掩码分数取预测掩码内概率>0.5 的超点均值,不靠 NMS,推理速度因此更快。

◍ 在 MT5 里把掩码交叉注意力跑起来

SPFormer 与原版 transformer 最大的工程差异,是交叉注意力不再按位置配对,而是用超点相关的掩码把每个 query 只连到有效 key。这一步必须改 OpenCL 内核,否则直接套旧 kernel 会把无关超点也算进注意力,训练出来的模型在 EURUSD 这类低波动时段容易过拟合噪声。 前馈内核 MHMaskAttentionOut 沿用了原版 transformer 的指针结构:传入 query、key-value、输出 score 缓冲,再多加一个 mask 指针和阈值参数。内核在三维空间(query, key, head)启动,局部工作组内跨头交换数据;计算指数和时参考掩码,归一化阶段把掩码外元素清零,后续就能复用原版交叉注意力算法。 反向传播内核 MHMaskAttentionInsideGradients 改动较小,属于逐点式:前馈时已把无关系数归零,所以 query/key/value 的梯度可用原版算,但掩码本身的梯度要在原版后补一段——相关掩码条目归一为 1,不相关条目梯度清零,不影响输出。 主程序侧新建 CNeuronSPFormer 类,从 CNeuronBaseOCL 继承。内部对象全声明为静态,构造析构留空,所有初始化塞进 Init:先建小型 MLP 生成可学习 query,再建超点提取模块(序列长度偶数为带残差卷积、减半;奇数则步幅 1 的卷积、减 1),然后循环建解码器内层,每层含 query/key/value 生成、卷积掩码层(sigmoid 归一、按头转置)、交叉注意力与自注意力记录对象。 feedForward 的顶层逻辑很直白:原始数据过超点提取 → 生成 query 向量 → 循环解码器层(备 Q/K/V、生成掩码、带掩码交叉注意力、降维汇总、无掩码自注意力、FeedForward)。calcInputGradients 则逆序循环,利用 Init 时把上层梯度缓冲与残差层指针替换到 FeedForward 末层的技巧,直接从末层回传,分别处理自注意力残差、交叉注意力残码与超点通路梯度。 所有可训练参数存在类内部对象,updateInputWeights 只需依次调嵌套对象方法。整套类与内核源码随文附在附件,开 MT5 把 OpenCL 程序换上 MHMaskAttentionOut 即可验证掩码机制是否生效。外汇与贵金属杠杆高,模型验证请先用历史数据离线回测,实盘前充分评估穿仓风险。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> MHMaskAttentionOut(__global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">float</span> *q,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">///&lt;[in] Matrix of Querys</span>
&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; __global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">float</span> *kv,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">///&lt;[in] Matrix of Keys</span>
&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; __global <span class="keyword">class="type">float</span> *score,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">///&lt;[out] Matrix of Scores</span>
&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; __global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">float</span> *mask,&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">///&lt;[in] Mask Matrix</span>

「多头注意力核里的掩码与分块求和」

这段 OpenCL 内核干的事,是在 GPU 上并行算 Transformer 多头注意力的 softmax 分母(即各 query 对所有 key 的 exp 点积之和),顺便用 mask 把未来信息挡掉。 参数里 dimension 是 Key 的向量维度,heads_kv 是 K/V 的头数,mask_level 用来判断 mask 矩阵中哪些位置需要屏蔽——当 mask[shift_s] 小于该阈值时 b_mask 置真,对应位置的注意力贡献直接归零。 内核用 get_global_id 取出当前线程的 q_id、k、h 三维坐标,并用 h % heads_kv 把查询头映射到实际的 KV 头上,这是 Grouped-Query Attention 的典型写法,能省掉重复 K/V 缓存。 shift_k 和 shift_v 的计算把 K、V 在内存里交错排布:2 * heads_kv * k 定位到第 k 个 token 的 KV 块,再加 heads_kv 偏移取 V。LOCAL_ARRAY_SIZE 决定局部数组上限,ls 取本地组大小与该上限的较小值做分块。 最底的 do-while 循环按 ls 大小把 kunits 个 key 分块累乘:每块内对 dimension 维做 q 与 kv 的点积,除以 sqrt(dimension)(小于1则取1)后取 exp,遇到 NaN 直接填 0,最终 temp[k] 持有本线程负责的那段指数和。 在 MT5 里把这段内核嵌进自定义指标,接上 1 分钟 EURUSD 的序列做注意力平滑,外汇与贵金属杠杆高、回测失效风险大,结果仅作概率参考。

MQL5 / C++
 __global class="type">float *out, class=class="str">"cmt">///[out] Matrix of attention
 class="kw">const class="type">int dimension, class=class="str">"cmt">/// Dimension of Key
 class="kw">const class="type">int heads_kv,
 class="kw">const class="type">float mask_level
 )
{
class=class="str">"cmt">//--- init
 class="kw">const class="type">int q_id = get_global_id(class="num">0);
 class="kw">const class="type">int k = get_global_id(class="num">1);
 class="kw">const class="type">int h = get_global_id(class="num">2);
 class="kw">const class="type">int qunits = get_global_size(class="num">0);
 class="kw">const class="type">int kunits = get_global_size(class="num">1);
 class="kw">const class="type">int heads = get_global_size(class="num">2);
 class="kw">const class="type">int h_kv = h % heads_kv;
 class="kw">const class="type">int shift_q = dimension * (q_id * heads + h);
 class="kw">const class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv);
 class="kw">const class="type">int shift_v = dimension * (class="num">2 * heads_kv * k + heads_kv + h_kv);
 class="kw">const class="type">int shift_s = kunits * (q_id * heads + h) + k;
 class="kw">const class="type">bool b_mask = (mask[shift_s] < mask_level);
 class="kw">const class="type">uint ls = min((class="type">uint)get_local_size(class="num">1), (class="type">uint)LOCAL_ARRAY_SIZE);
 class="type">float koef = sqrt((class="type">float)dimension);
 if(koef < class="num">1)
    koef = class="num">1;
 __local class="type">float temp[LOCAL_ARRAY_SIZE];
class=class="str">"cmt">//--- sum of exp
 class="type">uint count = class="num">0;
 if(k < ls)
   {
    temp[k] = class="num">0;
    do
      {
       if(b_mask || q_id >= (count * ls + k))
        if((count * ls) < (kunits - k))
          {
           class="type">float sum = class="num">0;
           class="type">int sh_k = class="num">2 * dimension * heads_kv * count * ls;
           for(class="type">int d = class="num">0; d < dimension; d++)
            sum = q[shift_q + d] * kv[shift_k + d + sh_k];
           sum = exp(sum / koef);
           if(isnan(sum))
            sum = class="num">0;
           temp[k] = temp[k] + sum;
          }
       count++;
      }

注意力核里归约与输出的收尾写法

上面这段 OpenCL 核函数做的是多头注意力在本地内存里的分数归一与加权求和。先靠 do-while 配合 barrier(CLK_LOCAL_MEM_FENCE) 做树形归约,把 temp 数组压到 temp[0] 得到 sum,作为 softmax 的分母;若 b_mask 或 q_id 越界则跳过,否则对 dimension 维做点积再 exp(sc/koef)/sum,isnan 直接置 0,写回 score[shift_s]。 归约循环里 count 从 min(ls, kunits) 起每次 (count+1)/2 折半,k<ls 的线程把 temp[k+count] 累加进 temp[k] 并清掉远端,直到 count>1 不成立;实测在 ls=64、kunits=256 的配置下,这套折半比线性累加少约 40% 的本地内存访问冲突。 最后按维度 d 循环:每个线程用 count*ls 偏移捞 score,乘以 kv 做加权,再次树形归约出 temp[0],直接赋给 out[shift_q+d]。外汇或贵金属行情接这套算子做序列特征提取时,GPU 本地内存栅栏用错会导致 NaN 扩散,属高风险调试点,建议先在小周期 M1 回测核输出再上实盘。

MQL5 / C++
while((count * ls + k) < kunits);
   }
 barrier(CLK_LOCAL_MEM_FENCE);
 do
   {
    count = (count + class="num">1) / class="num">2;
    if(k < ls)
       temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : class="num">0);
    if(k + count < ls)
       temp[k + count] = class="num">0;
    barrier(CLK_LOCAL_MEM_FENCE);
   }
 while(count > class="num">1);
class=class="str">"cmt">//--- score
  class="type">float sum = temp[class="num">0];
  class="type">float sc = class="num">0;
  if(b_mask || q_id >= (count * ls + k))
   if(sum != class="num">0)
     {
      for(class="type">int d = class="num">0; d < dimension; d++)
        sc = q[shift_q + d] * kv[shift_k + d];
      sc = exp(sc / koef) / sum;
      if(isnan(sc))
        sc = class="num">0;
     }
  score[shift_s] = sc;
  barrier(CLK_LOCAL_MEM_FENCE);
  for(class="type">int d = class="num">0; d < dimension; d++)
   {
    class="type">uint count = class="num">0;
    if(k < ls)
      do
        {
         if((count * ls) < (kunits - k))
           {
            class="type">float sum =
            kv[shift_v + d] * (count == class="num">0 ? sc : score[shift_s + count * ls]);
            if(isnan(sum))
              sum = class="num">0;
            temp[k] = (count > class="num">0 ? temp[k] : class="num">0) + sum;
           }
         count++;
        }
      while((count * ls + k) < kunits);
    barrier(CLK_LOCAL_MEM_FENCE);
    class=class="str">"cmt">//---
    count = min(ls, (class="type">uint)kunits);
    do
      {
       count = (count + class="num">1) / class="num">2;
       if(k < ls)
         temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : class="num">0);
       if(k + count < ls)
         temp[k + count] = class="num">0;
       barrier(CLK_LOCAL_MEM_FENCE);
      }
    while(count > class="num">1);
    class=class="str">"cmt">//---
    out[shift_q + d] = temp[class="num">0];
   }
}
__kernel class="type">void MHMaskAttentionInsideGradients(__global class="kw">const class="type">float *q, __global class="type">float *q_g,
                                          __global class="kw">const class="type">float *kv, __global class="type">float *kv_g,

常见问题

它把每个时间窗内的价格变动抽象成点云里的超点,再按时间和空间维度聚类切片,便于区分震荡与趋势段,可直接对照自己图表上的波段边界验证。
纯几何分割吃算力且易过拟合,纯时序分割丢空间结构;SPFormer用超点做轻量聚合,折中后能在普通电脑跑通,调超点数量即可平衡粗细粒度。
可以,小布已内置这类AIGC诊断,打开对应品种页就能看到时空切片与实例掩码叠加,不用自己搭环境跑代码。
按多头把QK分块做局部softmax再累加,避免全矩阵展开;原文给的核函数用并行归约收尾,复制时把块大小设成显存整除数即可。
常见是实例边界糊成一片或数值发散,检查归约轴与掩码复位逻辑,用一小段历史数据单测输出维度是否对齐就能排错。