交易中的神经网络:免掩码注意力方式预测价格走势(基础篇)
「用免掩码注意力直接读价格序列」
传统 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。两个输入必须投影到同维子空间,否则距离无效。内核按二维任务空间发起,每个维度对应张量元素数,线程内先定位偏移再循环算距、写回结果。
__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, __global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">float</span> *data2, __global <span class="keyword">class="type">class="kw">float</span> *result, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> dimension ) { <span class="keyword">class="kw">const</span> class="type">size_t idx1 = get_global_id(<span class="number">class="num">0</span>);
__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, __global <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">float</span> *data2, __global <span class="keyword">class="type">class="kw">float</span> *result, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> dimension ) { <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 信号仅作概率参考,实盘前务必在策略测试器里用真实点差复验。
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">///<[in] Matrix of Querys __global class="kw">const class="type">class="kw">float *k, class=class="str">"cmt">///<[in] Matrix of Keys __global class="kw">const class="type">class="kw">float *v, class=class="str">"cmt">///<[in] Matrix of Values __global class="type">class="kw">float *score, class=class="str">"cmt">///<[out] Matrix of Scores __global class="kw">const class="type">class="kw">float *pos_bias, class=class="str">"cmt">///<[in] Position Bias __global class="type">class="kw">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">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 核做序列建模属高风险实验,回测不代表实盘概率。
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)