交易中的神经网络:免掩码注意力方式预测价格走势·进阶篇
(2/3)· SPFormer 收敛慢的症结在初始掩码,本篇用中心回归绕过它直接加速
不少人在复现点云变换器做行情预测时,发现训练前几百步损失几乎不动,便归咎于学习率或数据量。真实瓶颈常在初始对象掩码品质过低,迫使后续层反复修正错误引导,拖慢整体收敛。
◍ 多头注意力里的归约与 MAFT 神经元骨架
这段 OpenCL 内核片段在做一件事:把局部内存里的注意力分数按 key 维度做分段归约,再写回输出缓冲。第一段 do-while 里,count 从 0 起跳,只要 (count * ls) 小于 (kunits - k_id) 就继续累加;sh_v 的偏移量直接由 2 * dimension * heads_kv * count * ls 算出,说明每个 head 的 value 块在显存里是连续铺开的。 累加时用了三元表达式处理首轮权重:count==0 取 sc(可能是缩放常数),否则取 score 数组里对应位置。isnan(sum) 被显式清零,避免 NaN 在并行归约中污染整个 temp[k_id]——这在 MT5 的 GPU 算子里是必做的防御,否则回测时某根乱序 tick 就能让整层输出报废。 第二段是经典的并行求和树:count 先取 min(ls, kunits),每次折半,temp[k_id] 拉上 temp[k_id+count] 累加,另一半置 0,靠 CLK_LOCAL_MEM_FENCE 保证线程间可见。最终 out[shift_q + d] = temp[0],意味着每个 query 位置只保留归约后的标量。 落到 CNeuronMAFT 这个类,它继承自 CNeuronBaseOCL,成员变量暴露了窗口与单元的配置:iWindow / iUnits / iHeads 管主序列,iSPWindow / iSPUnits / iSPHeads 管 super-point 分支,iWindowKey 与 iLayers / iLayersSP 控制交叉注意力的深度。cQuery、cQKey、cQValue 与 cSPKey、cSPValue 分开声明,说明自注意力与跨注意力用了独立权重层,cScores 和 cPositionBias 则用 CArrayInt 存整数索引——开 MT5 把这类神经元挂到 EURUSD 的 M15 上,先调 iHeads 从 4 改 8,显存占用约翻倍但注意力分辨率会明显变细。外汇与贵金属杠杆交易高风险,参数改动仅影响模型结构,不预示任何收益。
do { if((count * ls) < (kunits - k_id)) { class="type">int sh_v = class="num">2 * dimension * heads_kv * count * ls; class="type">class="kw">float sum = v[shift_kv + d + sh_v] * (count == class="num">0 ? sc : score[shift_s + count * ls]); if(isnan(sum)) sum = class="num">0; temp[k_id] = (count > class="num">0 ? temp[k_id] : class="num">0) + sum; } count++; } while((count * ls + k_id) < 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_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">//--- out[shift_q + d] = temp[class="num">0]; } } class CNeuronMAFT : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iWindow; class="type">uint iUnits; class="type">uint iHeads; class="type">uint iSPWindow; class="type">uint iSPUnits; class="type">uint iSPHeads; class="type">uint iWindowKey; class="type">uint iLayers; class="type">uint iLayersSP; class=class="str">"cmt">//--- CLayer cSuperPoints; CLayer cQuery; CLayer cQPosition; CLayer cQKey; CLayer cQValue; CLayer cMHSelfAttentionOut; CLayer cSelfAttentionOut; CLayer cSPKey; CLayer cSPValue; CArrayInt cScores; CArrayInt cPositionBias; CLayer cMHCrossAttentionOut; CLayer cCrossAttentionOut;
「多头注意力层的缓冲区与接口声明」
在 MT5 用 OpenCL 跑 Transformer 类模型时,多头注意力模块需要先占好显存与内存缓冲。下面这段类成员声明给出了一个典型实现骨架:残差连接、前馈网络各占一个 CLayer,而 cTempSP、cTempQ、cTempCrossK、cTempCrossV 这组 CBufferFloat 临时缓冲,分别承接 softmax 分数、查询向量以及交叉注意力的 K/V 投影,避免每次前向都重新分配。
虚函数 CreateBuffers 负责把上面那些缓冲按实际序列长度与维度开出来;CalcPositionBias 接收 pos_q、pos_k 指针和 pos_bias 偏移量,在指定 units(如 64)与 units_kv、dimension 下计算旋转或偏置位置编码。AttentionOut 则是核心:传入 q/k/v 三个神经元对象、scores 头分数、heads 与 heads_kv(例如 8 与 8),use_pos_bias 开关决定是否注入位置偏置,输出写到 out 缓冲。
反向部分由 AttentionInsideGradients 与 calcInputGradients、updateInputWeights 覆盖基类方法完成,feedForward 也在此层 override。你在自写 EA 时若想接这套结构,直接照搬成员声明并在 CreateBuffers 里按 units*heads 分配 cTempQ 大小即可,外汇与贵金属行情高频跳变,显存规划不当可能让策略掉帧甚至崩端,属高风险操作。
CLayer cResidual; CLayer cFeedForward; CBufferFloat cTempSP; CBufferFloat cTempQ; CBufferFloat cTempCrossK; CBufferFloat cTempCrossV; class=class="str">"cmt">//--- class="kw">virtual class="type">bool CreateBuffers(class="type">void); class="kw">virtual class="type">bool CalcPositionBias(CBufferFloat *pos_q, CBufferFloat *pos_k, class="kw">const class="type">int pos_bias, class="kw">const class="type">int units, class="kw">const class="type">int units_kv, class="kw">const class="type">int dimension); class="kw">virtual class="type">bool AttentionOut(CNeuronBaseOCL *q, CNeuronBaseOCL *k, CNeuronBaseOCL *v, class="kw">const class="type">int scores, CNeuronBaseOCL *out, class="kw">const class="type">int pos_bias, class="kw">const class="type">int units, class="kw">const class="type">int heads, class="kw">const class="type">int units_kv, class="kw">const class="type">int heads_kv, class="kw">const class="type">int dimension, class="kw">const class="type">bool use_pos_bias); class="kw">virtual class="type">bool AttentionInsideGradients(CNeuronBaseOCL *q, CNeuronBaseOCL *k, CNeuronBaseOCL *v, class="kw">const class="type">int scores, CNeuronBaseOCL *out, class="kw">const class="type">int units, class="kw">const class="type">int heads, class="kw">const class="type">int units_kv, class="kw">const class="type">int heads_kv, class="kw">const class="type">int dimension); class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronMAFT(class="type">void) {};
CNeuronMAFT 的初始化与成员布局
在 MT5 的 OpenCL 神经网络框架里,CNeuronMAFT 类通过默认构造函数留空,真正的资源申请全压在 Init 方法。Init 参数表暴露了多头注意力机制的关键维度:window 与 units_count 决定输入序列长度与每头单元数,heads 控制并行注意力头数,带 _sp 后缀的一组则对应二级子路径的空间维度。 调用父类 CNeuronBaseOCL::Init 时,传入的神经元规模被显式写成 window * units_count,而非原始 window 值。这意味着底层张量按「时间窗 × 单元数」展平,若你在 EURUSD 的 M15 上取 window=60、units_count=8,实际申请的是 480 宽的特征向量,显存占用会随该乘积线性走升。 iLayers 与 iLayersSP 都用 MathMax(x, 1) 兜底,保证至少跑一层变换。随后 new 一个 CNeuronBaseOCL 作基础子层,其 Init 的第二个参数写死为 0(myIndex),输出维度压成 1,优化类型与批大小沿用类内 optimization / iBatch 成员。 初始化尾声对 getOutput 与 getWeights 两处 CBufferFloat 做 BufferInit + BufferWrite 校验:输出缓冲写固定 1×1,权重缓冲按 Total() 长度清零。任一步返回 false 都会中断 Init,实盘加载自定义模型前,建议先在策略测试器里单步跑一遍该类,确认 OpenCL 上下文与缓冲绑定无报错。外汇与贵金属杠杆品种波动剧烈,此类 GPU 算子若初始化失败可能导致信号停滞,需配合风控熔断。
~CNeuronMAFT(class="type">void) {}; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint units_count, class="type">uint heads, class="type">uint window_sp, class="type">uint units_sp, class="type">uint heads_sp, class="type">uint layers, class="type">uint layers_to_sp, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">override class="kw">const { class="kw">return defNeuronMAFT; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronMAFT::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint units_count, class="type">uint heads, class="type">uint window_sp, class="type">uint units_sp, class="type">uint heads_sp, class="type">uint layers, class="type">uint layers_to_sp, 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 = window; iUnits = units_count; iHeads = heads; iSPUnits = units_sp; iSPWindow = window_sp; iSPHeads = heads_sp; iWindowKey = window_key; iLayers = MathMax(layers, class="num">1); iLayersSP = MathMax(layers_to_sp, class="num">1); CNeuronBaseOCL *base = new CNeuronBaseOCL(); if(!base) class="kw">return false; if(!base.Init(iWindow * iUnits, class="num">0, OpenCL, class="num">1, optimization, iBatch)) class="kw">return false; CBufferFloat *buf = base.getOutput(); if(!buf || !buf.BufferInit(class="num">1, class="num">1) || !buf.BufferWrite()) class="kw">return false; buf = base.getWeights(); if(!buf || !buf.BufferInit(buf.Total(), class="num">0) || !buf.BufferWrite())
◍ 残差卷积与超点层的初始化分支
这段初始化逻辑里,SuperPoints 部分用了一个 4 轮循环来堆叠卷积结构,循环变量 r 从 0 到 3,每轮根据 iSPUnits 的奇偶性决定走残差块还是普通卷积。 当 iSPUnits 为偶数时,先把它减半,再 new 一个 CResidualConv,用 2*iSPWindow 作输入宽度、iSPWindow 作核宽去 Init;奇数时则 iSPUnits 减 1,改挂 CNeuronConvOCL,步长固定为 1。 循环结束后 layer_id 会从初始的 4 累加到 8,再补一层 CNeuronConvOCL 把通道收束到 iWindow,随后接一个 CNeuronLearnabledPE 做位置编码——任何一步 Init 或 Add 返回 false 都会直接中断整个构建。 在 MT5 里跑这套时,建议先打印 iSPUnits 初值,若它是 2 的幂,前 4 轮会全走残差分支,显存占用倾向比奇数路径更平滑。
class="kw">return false; if(!cQuery.Add(base)) class="kw">return false; base = new CNeuronBaseOCL(); if(!base || !base.Init(class="num">0, class="num">1, OpenCL, iWindow * iUnits, optimization, iBatch)) class="kw">return false; if(!cQuery.Add(base)) class="kw">return false; CNeuronLearnabledPE *pe = new CNeuronLearnabledPE(); if(!pe || !pe.Init(class="num">0, class="num">2, OpenCL, base.Neurons(), optimization, iBatch)) class="kw">return false; if(!cQuery.Add(pe)) class="kw">return false; if(!base || !base.Init(class="num">0, class="num">3, OpenCL, pe.Neurons(), optimization, iBatch)) class="kw">return false; if(!base.SetOutput(pe.getOutput())) class="kw">return false; if(!cQPosition.Add(base)) class="kw">return false; class=class="str">"cmt">//--- Init SuperPoints class="type">int layer_id = class="num">4; for(class="type">int r = class="num">0; r < class="num">4; r++) { if(iSPUnits % class="num">2 == class="num">0) { iSPUnits /= class="num">2; CResidualConv *residual = new CResidualConv(); if(!residual) class="kw">return false; if(!residual.Init(class="num">0, layer_id, OpenCL, class="num">2 * iSPWindow, iSPWindow, iSPUnits, optimization, iBatch)) class="kw">return false; if(!cSuperPoints.Add(residual)) class="kw">return false; } else { iSPUnits--; CNeuronConvOCL *conv = new CNeuronConvOCL(); if(!conv.Init(class="num">0, layer_id, OpenCL, class="num">2 * iSPWindow, iSPWindow, iSPWindow, iSPUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cSuperPoints.Add(conv)) class="kw">return false; } layer_id++; } CNeuronConvOCL *conv = new CNeuronConvOCL(); if(!conv.Init(class="num">0, layer_id, OpenCL, iSPWindow, iSPWindow, iWindow, iSPUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cSuperPoints.Add(conv)) class="kw">return false; layer_id++; pe = new CNeuronLearnabledPE(); if(!pe || !pe.Init(class="num">0, layer_id, OpenCL, conv.Neurons(), optimization, iBatch)) class="kw">return false; if(!cSuperPoints.Add(pe))
「Transformer 层内的注意力堆叠」
这段初始化逻辑跑在每层循环里,iLayers 决定堆叠深度,每进一层 layer_id 自增并在各子模块间传递,保证 OpenCL 上下文里的神经元编号不撞车。 自注意力部分先连续建三个一维卷积做 Query、Key、Value,卷积核窗口宽 iWindow、输出通道数 iWindowKey*iHeads,再接一个 CNeuronBaseOCL 做多头拼接后的线性映射,维度是 iWindowKey*iHeads*iUnits。 随后 Self-Attention Out 用卷积把 iWindowKey*iHeads 压回 iWindow 时间步,Residual 分支基元直接吃 iWindow*iUnits 的展平向量。跨境注意力开头又挂了一个和自注意力同构的 Query 卷积,说明编码器-解码器交互也走了一套独立参数。 在 MT5 里把 iHeads 从 4 调到 8,layer_id 的占用会近乎翻倍,显存吃紧时容易在 Init 返回 false 处断链,建议先小批次 iBatch=32 探一下。
layer_id++; class=class="str">"cmt">//--- Inside layers for(class="type">uint l = class="num">0; l < iLayers; l++) { class=class="str">"cmt">//--- Self-Attention class=class="str">"cmt">//--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cQuery.Add(conv)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cQKey.Add(conv)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Value conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cQValue.Add(conv)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Multy-Heads Attention Out base = new CNeuronBaseOCL(); if(!base || !base.Init(class="num">0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch)) class="kw">return false; if(!cMHSelfAttentionOut.Add(base)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Self-Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cSelfAttentionOut.Add(conv)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Residual base = new CNeuronBaseOCL(); if(!base || !base.Init(class="num">0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch)) class="kw">return false; if(!cResidual.Add(base)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Cross-Attention class=class="str">"cmt">//--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cQuery.Add(conv))
交叉注意力层收尾的残差与前馈拼接
这段构建逻辑出现在多头注意力主干之后,负责把跨注意力输出、残差连接和前馈网络逐层挂到对应的容器里。每推一层都把 layer_id 自增 1,保证 OpenCL 内核里的张量维度不串号。 当循环变量 l 能被 iLayersSP 整除时,代码会再插一对 Key/Value 卷积层:两者都用 iWindowKey * iSPHeads 作输入通道、iSPUnits 作输出通道,卷积核宽高均为 iWindow。这类插入频率直接由 iLayersSP 控制,设成 2 就代表每两层补一次 KV,显存占用可能随层数线性抬升。 跨注意力输出卷积的 shape 是 [iWindowKey*iHeads, iWindowKey*iHeads, iWindow, iUnits],紧接着一个 iWindow*iUnits 的 Base 层做残差。前馈部分先扩到 4*iWindow 通道并设 LReLU,再卷回 iWindow——4 倍扩展是 Transformer 里常见的前馈比,MT5 终端跑这类网络时 GPU 显存峰值可能卡在这一层。
class="kw">return false; layer_id++; if(l % iLayersSP == class="num">0) { class=class="str">"cmt">//--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iSPHeads, iSPUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cSPKey.Add(conv)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Value conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iSPHeads, iSPUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cSPValue.Add(conv)) class="kw">return false; layer_id++; } class=class="str">"cmt">//--- Multy-Heads Attention Out base = new CNeuronBaseOCL(); if(!base || !base.Init(class="num">0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch)) class="kw">return false; if(!cMHCrossAttentionOut.Add(base)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Cross-Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; if(!cCrossAttentionOut.Add(conv)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Residual base = new CNeuronBaseOCL(); if(!base || !base.Init(class="num">0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch)) class="kw">return false; if(!cResidual.Add(base)) class="kw">return false; layer_id++; class=class="str">"cmt">//--- Feed Forward conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, iWindow, iWindow, class="num">4 * iWindow, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; conv.SetActivationFunction(LReLU); if(!cFeedForward.Add(conv)) class="kw">return false; layer_id++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, layer_id, OpenCL, class="num">4 * iWindow, class="num">4 * iWindow, iWindow, iUnits, class="num">1, optimization, iBatch))