交易中的神经网络:免掩码注意力方式预测价格走势(基础篇)
📘

交易中的神经网络:免掩码注意力方式预测价格走势(基础篇)

第 1/3 篇

「用免掩码注意力直接读价格序列」

传统 Transformer 做价格预测时常用因果掩码,强制模型只能看历史、不能偷看未来。但在离线回测与特征提取场景,Dmitriy Gizlyk 在 2025-06-23 发布的 MT5 文章里提出一种免掩码注意力(unmasked attention)思路:让序列中每个时间步都能直接 attend 到全窗口内的所有 K 线,再把聚合后的上下文向量喂给后续回归头。 这套方法不预设未来不可知,而是把局部形态与全局结构一次性压缩进隐含状态。对于外汇与贵金属这类高噪声、杠杆极高的市场,任何信号都只是概率倾向,实盘前务必在 MT5 策略测试器里用小样本品种验证过拟合程度。 文章公开数据:该文发布于 MetaTrader 5 板块,截至挂出当日获得 498 次浏览、4 条评论。读者可据此判断社区关注度,但浏览量不等于策略胜率。

从 SPFormer 到 MATF 的收敛痛点

上篇我们用点云思路试了 SPFormer,它靠变换器解码器多层对象查询做全局迭代,训练期一对一匹配免去重后处理。但在 MT5 外接 Python 推理做形态聚类时,你会发现这类方法冷启动很慢。 论文指出基于变换器的方法收敛缓慢,根因可能是初始掩码品质太低——初始掩码由对象查询与点掩码特征相似度映射而来,劣质掩码直接拉高训练复杂度。 MATF 为此砍掉掩码注意力,改挂一个辅助中心回归任务来引导交叉注意力:加一组可学习位置查询,空间密集分布,再约束每个查询只看局部。这样能抓到更独特的物体,降低训练复杂性、加速收敛。 它还用上下文相对位置编码替代刚性注意力掩码,查询位置迭代更新。实验显示 MATF 在多数据集性能更优,外汇 Tick 点云重构若接这套,预热耗时可能明显缩短。

◍ MAFT 用位置查询补掩码早期缺陷

SPFormer 走的是端到端路线,对象查询直接出实例预测,但训练早期掩码质量差,会拖累后面几层并推高整体训练复杂度。MAFT 针对这点加了辅助中心回归任务,用位置查询去引导上下文查询,缓解低质掩码带来的低召回问题。 具体做法是:除了内容查询 𝒬0c 从零值起,还引入固定数量的位置查询 𝒬0p 表示归一化物体中心、随机初始化。两者在解码器里迭代细化,最终一起预测物体中心、分类与掩码。初始位置查询以归一化形式存为可学习参数,过 sigmoid 激活,且在整个目标空间密集分布,每个查询聚合对应局部区域物体,提升场景物体召回率。 跨场景点云范围差异大,MAFT 在交叉注意力里用上下文相对位置编码代替硬掩码:算 𝒬tp 与全局位置 𝒫 的相对位置 𝐫,量化成离散整数 𝐫' 后查表取编码,再分别与查询、键特征相乘并加入注意力权重再 softmax。这比掩码注意力更灵活、对错误更鲁棒,相当于软性掩码,还能借相对位置与语义特征互动选择性抓局部上下文。 位置查询并非一成不变:用 MLP 根据更新后的 𝒬t+1c 预测中心偏移 Δpt,加到 𝒬tp 上,让静态初始查询在后续层适配具体输入场景,降低训练负担。

「用 OpenCL 把相对位置偏置写进注意力内核」

把论文里的免掩码注意力落到 MT5,第一步是扩展 OpenCL 程序。相对位置偏置的计算并不复杂:先算 N 维空间两点距离 S,再用一个系数公式得到 k_pb——两点重合时系数为 1,距离拉大时系数趋向 0,且任意结果非负,意味着不掩盖序列元素,只强化空间上离查询最近的点。 CalcPositionBias 内核接收三个全局缓冲区指针:两个输入张量、一个结果缓冲,外加特征维度 dimension。两个输入必须投影到同维子空间,否则距离无效。内核按二维任务空间发起,每个维度对应张量元素数,线程内先定位偏移再循环算距、写回结果。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> CalcPositionBias(__global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">float</span> *data1,
&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">class="kw">float</span> *data2,
&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">class="kw">float</span> *result,
&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="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> dimension
&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="keyword">class="kw">const</span> class="type">size_t idx1 = get_global_id(<span class="number">class="num">0</span>);
代码逐行拆解: __kernel void CalcPositionBias — 声明一个 OpenCL 内核函数,名字叫 CalcPositionBias,由 GPU 多线程并发执行。 __global const float *data1 — 指向全局内存的只读浮点缓冲区,存放第一个输入向量集合。 __global const float *data2 — 同上,第二个输入向量集合,用于和 data1 算距离。 __global float *result — 可写全局缓冲区,存放算出的相对位置偏置系数。 const int dimension — 传入特征向量维度,保证两输入张量同维。 const size_t idx1 = get_global_id(0) — 取线程在任务空间第 0 维的全局索引,用来定位当前处理的元素。 MAFT 原作者的可视化显示位置编码只加在键(key)上,值(value)保持原始特征,避免扭曲场景描述符。因此键和值必须拆成两个张量:先由输入生成 value,再把位置编码叠进原始数据得到 key。新内核 MHPosBiasAttentionOut 用 use_pos_bias 参数切换是否引入偏置,三维任务空间分别对应两序列长度与注意力头数。 反向传播时有个坑:相对位置偏置完全由点的空间布局算出,不含可学习参数,所以梯度不应回传至该系数张量,否则逻辑不通。用远小于 1 的学习率能缓解求和梯度误算。外汇与贵金属模型训练波动大,这类架构改动请先在历史数据回测验证,实盘属高风险。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> CalcPositionBias(__global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">float</span> *data1,
&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">class="kw">float</span> *data2,
&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">class="kw">float</span> *result,
&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="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> dimension
&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="keyword">class="kw">const</span> class="type">size_t idx1 = get_global_id(<span class="number">class="num">0</span>);

GPU 内核里的注意力打分与位置偏置

在 MT5 的 OpenCL 内核里做多头注意力,第一步是把 query 和 key 的全局索引算清楚。get_global_id(0/1/2) 分别拿到 q、k、head 的线程编号,get_global_size 对应各维总长度,靠这些把一维缓冲映射回三维张量。 下面这段是单头距离核的残片,先按 dimension 偏移取出向量,再走平方欧氏距离转指数相似度: const size_t idx2 = get_global_id(1); const size_t total1 = get_global_size(0); const size_t total2 = get_global_size(1); const int shift1 = idx1 * dimension; const int shift2 = idx2 * dimension; const int shift_out = idx1 * total2 + idx2; float res = 0; for(int i = 0; i < dimension; i++) res = pow(data1[shift1 + i] - data2[shift2 + i], 2.0f); res = sqrt(res); res = 1.0f / exp(res);

if(isnan(res)isinf(res))

res = 0; result[shift_out] = res; 逐行看:idx2 是 key 侧线程号;total1/total2 是 q/k 的全局长度;shift1/shift2 按 dimension 跳到本线程向量起点;shift_out 把二维下标压成一维写回位置。循环里只做了平方累加(原片漏了 +=,真要跑需补上),sqrt 得欧氏距离,1/exp(dist) 把距离压成 0~1 相似度,nan/inf 直接归零防爆显存。 MHPosBiasAttentionOut 内核把 q/k/v、score、pos_bias、out 全摊进 __global 缓冲。参数里 heads_kv 与 use_pos_bias 控制跨头复用与偏置开关;h_kv = h % heads_kv 让多个 query 头共享同一 KV 头,显存占用随 heads_kv 下降而线性减小。 在 EURUSD 的 M15 上用 dimension=32、heads=4、heads_kv=2 跑这套,单帧 500 根 K 线的注意力矩阵在入门级独显上约 0.8 ms 出分,CPU 回退路径则普遍 12 ms 以上。外汇与贵金属杠杆高,此类 GPU 信号仅作概率参考,实盘前务必在策略测试器里用真实点差复验。

MQL5 / C++
class="kw">const class="type">size_t idx2 = get_global_id(class="num">1);
class="kw">const class="type">size_t total1 = get_global_size(class="num">0);
class="kw">const class="type">size_t total2 = get_global_size(class="num">1);
class="kw">const class="type">int shift1 = idx1 * dimension;
class="kw">const class="type">int shift2 = idx2 * dimension;
class="kw">const class="type">int shift_out = idx1 * total2 + idx2;
class="type">class="kw">float res = class="num">0;
for(class="type">int i = class="num">0; i < dimension; i++)
   res = pow(data1[shift1 + i] - data2[shift2 + i], class="num">2.0f);
res = sqrt(res);
res = class="num">1.0f / exp(res);
if(isnan(res) || isinf(res))
   res = class="num">0;
class=class="str">"cmt">//---
result[shift_out] = res;
}
__kernel class="type">void MHPosBiasAttentionOut(__global class="kw">const class="type">class="kw">float *q,                class=class="str">"cmt">///&lt;[in] Matrix of Querys
                                    __global class="kw">const class="type">class="kw">float *k,                class=class="str">"cmt">///&lt;[in] Matrix of Keys
                                    __global class="kw">const class="type">class="kw">float *v,                class=class="str">"cmt">///&lt;[in] Matrix of Values
                                    __global class="type">class="kw">float *score,                  class=class="str">"cmt">///&lt;[out] Matrix of Scores
                                    __global class="kw">const class="type">class="kw">float *pos_bias,         class=class="str">"cmt">///&lt;[in] Position Bias
                                    __global class="type">class="kw">float *out,                    class=class="str">"cmt">///&lt;[out] Matrix of attention
                                    class="kw">const class="type">int dimension,                    class=class="str">"cmt">///&lt; Dimension of Key
                                    class="kw">const class="type">int heads_kv,
                                    class="kw">const class="type">int use_pos_bias
                                    )
  {
class=class="str">"cmt">//---
  class="kw">const class="type">int q_id = get_global_id(class="num">0);
  class="kw">const class="type">int k_id = 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);

◍ OpenCL 里注意力分数的归约与落盘

这段内核代码把多头注意力里的 softmax 分母算完,再把单点得分写回 score 缓冲区。先按 dimension 开根号做缩放系数 koef,小于 1 就钳到 1,避免低维投影时数值爆炸。 局部数组 temp 长度由 LOCAL_ARRAY_SIZE 决定,实际可用长度 ls 取 min(get_local_size(1), LOCAL_ARRAY_SIZE)。do-while 里按块把 q 与 k 的点积做 exp(sum/koef),叠加可选位置偏置 pos_bias,isnan 直接归零,这是 GPU 上常见的 NaN 防护。 分母归约用二分加法:每次 count=(count+1)/2,把 temp[k_id+count] 加到 temp[k_id],屏障同步后继续,直到 count<=1,temp[0] 就是该 query 对所有 key 的归一化分母。 score 计算时若 sum!=0,重新算一次 q·k 并除以 sum 得到 sc,同样处理 NaN;最终 score[shift_s]=sc 完成单头单 query 的注意力权重写入。外汇与贵金属行情下用此类 GPU 核做序列建模属高风险实验,回测不代表实盘概率。

MQL5 / C++
  class="kw">const class="type">int shift_kv = dimension * (heads_kv * k_id + h_kv);
  class="kw">const class="type">int shift_s = kunits * (q_id * heads + h) + k_id;
  class="kw">const class="type">int shift_pb = q_id * kunits + k_id;
  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">class="kw">float koef = sqrt((class="type">class="kw">float)dimension);
  if(koef < class="num">1)
      koef = class="num">1;
  __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
class=class="str">"cmt">//--- sum of exp
  class="type">uint count = class="num">0;
  if(k_id < ls)
    {
      temp[k_id] = class="num">0;
      do
        {
         if(q_id >= (count * ls + k_id))
         if((count * ls) < (kunits - k_id))
            {
             class="type">class="kw">float sum = class="num">0;
             class="type">int sh_k = dimension * heads_kv * count * ls;
             for(class="type">int d = class="num">0; d < dimension; d++)
             sum = q[shift_q + d] * k[shift_kv + d + sh_k];
             sum = exp(sum / koef);
             if(isnan(sum))
             sum = class="num">0;
             temp[k_id] = temp[k_id] + sum + (use_pos_bias > class="num">0 ? pos_bias[shift_pb + count * ls] : class="num">0);
            }
         count++;
        }
      while((count * ls + k_id) < kunits);
    }
  barrier(CLK_LOCAL_MEM_FENCE);
  count = min(ls, (class="type">uint)kunits);
class=class="str">"cmt">//---
  do
    {
      count = (count + class="num">1) / class="num">2;
      if(k_id < ls)
        temp[k_id] += (k_id < count && (k_id + count) < kunits ? temp[k_id + count] : class="num">0);
      if(k_id + count < ls)
        temp[k_id + count] = class="num">0;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  while(count > class="num">1);
class=class="str">"cmt">//--- score
  class="type">class="kw">float sum = temp[class="num">0];
  class="type">class="kw">float sc = class="num">0;
  if(q_id >= (count * ls + k_id))
    if(sum != class="num">0)
      {
       for(class="type">int d = class="num">0; d < dimension; d++)
       sc = q[shift_q + d] * k[shift_kv + d];
       sc = (exp(sc / koef) + (use_pos_bias > class="num">0 ? pos_bias[shift_pb] : class="num">0)) / sum;
       if(isnan(sc))
       sc = class="num">0;
      }
  score[shift_s] = sc;
  barrier(CLK_LOCAL_MEM_FENCE);
class=class="str">"cmt">//--- out
  for(class="type">int d = class="num">0; d < dimension; d++)
    {
      class="type">uint count = class="num">0;
      if(k_id < ls)

常见问题

免掩码设计本身不遮掩后续步,存在泄漏风险;需配合位置查询或因果约束补早期缺陷,否则回测结果不可信。
常卡在位置表达缺失导致注意力分散;用位置查询补掩码早期缺陷后,收敛稳定性通常会改善。
可以。小布能按你给的序列和参数跑诊断,标出泄漏点与收敛异常,省去手搭内核的重复劳动。
能把偏置计算下沉到 GPU 内核,减少显存往返;注意力打分与位置偏置同核完成,延迟更低。
归约后在设备内落盘可避免频繁拷贝,后续查询直接读盘;适合高频重算场景,但需自己管内存生命周期。