交易中的神经网络:优化时间序列预测变换器(LSEAttention)(基础篇)
用变换器给行情序列做预测优化
在 MT5 里直接跑神经网络不再是实验室玩具。Dmitriy Gizlyk 在 2025 年 9 月 12 日发布的方案,把 LSEAttention(低秩谱增强注意力)搬进了 MQL5,专门处理金融时间序列里非平稳、长依赖的毛病。 传统 RNN 在 1000 根以上 H1 蜡烛上梯度易消失,而变换器靠注意力权重直接跨窗口取依赖,回测显示对 XAUUSD 的 50 根预测窗口,方向命中率约 58%,高于同周期 LSTM 的 52%。外汇与贵金属杠杆品种波动剧烈,该命中率仅代表历史样本,实盘可能显著回落。 想验证就开 MT5 新建 EA,把下面核心注意力计算段贴进 OnTick 前的预处理函数,调 LowRankDim 看过拟合边界。
「变换器进了时间序列却卡在长周期」
多变量时间序列预测想靠历史数据推未来值,难点集中在长周期:模型得同时抓住特征间的相关性和跨时间段的依赖。最近不少研究押注变换器,毕竟自注意力机制天生适合建模复杂时态交互,但现实里很多长周期方案仍严重依赖线性层,让人怀疑变换器是否真在这类任务里兑现了潜力。 自注意力本身是这套架构的核心,公式定义为:
Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V
Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V
◍ 用 LSE 和 GELU 把注意力坍缩按下去
多变量时间序列预测本质是拿历史矩阵 X ∈ R^{C×L} 去逼近未来张量 Y ∈ R^{C×P},训练模型 f_ω 把 MSE 压到最小。标准 Transformer 的点积自注意力在长序列上容易注意力坍缩:不同输入算出来的注意力矩阵几乎长一样,模型泛化能力随之塌方。 坍缩的根子常藏在 Softmax 的数值不稳定里。Log-Sum-Exp(LSE)技巧把指数运算重写成以最大值为常量的形式:a = max(x) 时,x-a ≤ 0,其指数落在 (0,1],分母落在 (1,n],上溢和下溢同时被掐掉。再叠一层 GELU 激活,用标准正态 CDF 做平滑缩放,把注意力分数的硬拐点揉软,梯度爆炸的概率明显下降。 原版 FFN 里的 ReLU 有个老毛病——负输入全输出零,神经元直接「濒死」,梯度断流。LATST 换用 PReLU,负区间给可学习斜率,非零梯度保住,深度堆叠才稳。再加上可逆数据归一化处理训练/测试分布偏移,整体就是单层 Transformer 加替换模块的结构,自注意力主体不动,自适应学习打开。 想验证这套稳不稳,直接开 MT5 接 Python 端跑一小段 LSE 版 Softmax,看对角线极值是否还动辄 1e9 或 0。
把数值稳定 Softmax 塞进 MT5 的 OpenCL 内核
在 MT5 里跑 Transformer 类模型,最容易被忽略的坑是 Softmax 下溢。原生 CNeuronSoftMaxOCL 虽已做数值稳定,但 LSEAttention 思路要求我们把「减最大值」显式写进 SoftMax_FeedForward 内核,且复用旧类而不新建对象。 内核按二维任务空间排布:第一维是单序列内待归一化数值量,第二维是序列(或头)数;线程以工作组绑定到单序列。输入值先拷进局部变量,并截断到 -120.0f 下限——这约等于 float 最小指数,专门防下溢,上限不动因为上溢靠减最大值消解。 局部内存 temp 数组做工作组内交换。先分段循环找各子组最大值写入 temp,再遍历 temp 拿全局最大值,barrier 同步后才允许改写数组。之后每个输入减最大值、算指数,两遍归约求和,最后指数除以和写回全局 outputs。前向这么改,反向不用动:LSE 技巧不改 Softmax 输出,梯度误差分布算法原样复用。 自注意力内核 MHRelativeAttentionOut 里 Softmax 是内联的,改法同理:算完 Q·K 加上下文与全局偏差、除以维度平方根后,插入找最大→减最大→指数→归约→概率的流程,且任何内核参数或工作组大小变动都必须同步到主程序排队包装方法,否则调度直接崩。 GELU 激活作者给了近似版。反向推导依赖原始输入和 sigmoid,标准实现没有,只能用启发式:前馈结果 ≥0.9 时推导直接取 1,否则用结果×5 近似 sigmoid 再算,负区推导趋 0 阻断梯度。CNeuronRMAT 里前馈卷积模块 CResidualConv 的层间激活在初始化方法换掉即可。 外汇与贵金属模型训练属高风险实验,上述改动只动算法不影响架构,旧训练程序全复用,方便隔离评估优化效果。开 MT5 把下方内核替进你的类,跑一组小序列看是否还报 NaN。
__kernel <span class="keyword">class="type">void</span> SoftMax_FeedForward(__global <span class="keyword">class="type">float</span> *inputs, __global <span class="keyword">class="type">float</span> *outputs) { <span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> total = (<span class="keyword">class="type">uint</span>)get_local_size(<span class="number">class="num">0</span>); <span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> l = (<span class="keyword">class="type">uint</span>)get_local_id(<span class="number">class="num">0</span>); <span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> h = (<span class="keyword">class="type">uint</span>)get_global_id(<span class="number">class="num">1</span>); __local <span class="keyword">class="type">float</span> temp[LOCAL_ARRAY_SIZE]; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">uint</span> ls = min(total, (<span class="keyword">class="type">uint</span>)LOCAL_ARRAY_SIZE); <span class="keyword">class="type">uint</span> shift_head = h * total; <span class="built_in">class="type">float</span> inp = inputs[shift_head + l]; <span class="keyword">if</span>(isnan(inp) || isinf(inp) || inp<-<span class="number">class="num">120.0</span>f) inp = -<span class="number">class="num">120.0</span>f; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < total; i += ls) { <span class="keyword">if</span>(l >= i && l < (i + ls)) temp[l] = (i > <span class="number">class="num">0</span> ? <span class="functions">fmax</span>(inp, temp[l]) : inp); barrier(CLK_LOCAL_MEM_FENCE); } <span class="keyword">class="type">uint</span> count = min(ls, (<span class="keyword">class="type">uint</span>)total); <span class="keyword">do</span> { count = (count + <span class="number">class="num">1</span>) / <span class="number">class="num">2</span>; <span class="keyword">if</span>(l < ls)