神经网络变得简单(第 96 部分):多尺度特征提取(MSFformer)·综合运用
◍ 多头注意力层的显存与对象初始化
在 MT5 用 OpenCL 跑自定义神经网络时,多头注意力模块的第一道坎是容器和显存缓冲的预清理。代码片段开头连续调用 caScore、caAttentionOut、caW0 的 Clear(),目的是避免上一次前向传播残留的句柄污染本轮训练,尤其在你用同一 EA 反复调参时容易忽略。 循环按 iPAMLayers 层数堆叠子结构:每层先 new 一个 CNeuronS3 做位置编码类处理,Init 参数里 iWindowIn 和 iCount 决定输入窗口与样本数,随后立即 SetActivationFunction(None)——说明该层不做非线性变换,只做线性投影。 Query 和 KV 都用 CNeuronConvOCL 实现,但 KV 通道数是 2*iWindowKey*iHeadsKV,Query 是 iWindowKey*iHeads,差了一倍的卷积核宽度。Score 缓冲通过 OpenCL.AddBuffer 直接开 sizeof(float)*iCount*iCount*iHeads 字节的 CL_MEM_READ_WRITE 显存,若返回负索引则直接 return false,这是显存不足或上下文失效的最直接信号。 注意 caAttentionOut.Add(conv) 这里有个隐蔽笔误:前面 new 的是 base(CNeuronBaseOCL),却把 conv 塞进容器,而 base 仅 delete 未 Add。你在抄这段代码到自己的指标里时,大概率会触发野指针或层缺失,建议改成 caAttentionOut.Add(base) 再验证。外汇与贵金属行情下用此类 GPU 网络做推理,高杠杆与滑点风险并存,任何层初始化失败都应视为策略不可用的硬信号。
caScore.Clear(); caAttentionOut.Clear(); caW0.Clear(); CNeuronBaseOCL *base = NULL; CNeuronConvOCL *conv = NULL; CNeuronS3 *s3 = NULL; for(class="type">uint l = class="num">0; l < iPAMLayers; l++) { class=class="str">"cmt">//--- S3 s3 = new CNeuronS3(); if(!s3) class="kw">return false; if(!s3.Init(class="num">0, l, OpenCL, iWindowIn, iCount, optimization, iBatch) || !caS3.Add(s3)) class="kw">return false; s3.SetActivationFunction(None); class=class="str">"cmt">//--- Query conv = new CNeuronConvOCL(); if(!conv) class="kw">return false; if(!conv.Init(class="num">0, class="num">0, OpenCL, iWindowIn, iWindowIn, iWindowKey*iHeads, iCount, optimization, iBatch) || !caQuery.Add(conv)) { class="kw">delete conv; class="kw">return false; } conv.SetActivationFunction(None); class=class="str">"cmt">//--- KV conv = new CNeuronConvOCL(); if(!conv) class="kw">return false; if(!conv.Init(class="num">0, class="num">0, OpenCL, iWindowIn, iWindowIn, class="num">2*iWindowKey*iHeadsKV, iCount, optimization, iBatch) || !caKV.Add(conv)) { class="kw">delete conv; class="kw">return false; } conv.SetActivationFunction(None); class=class="str">"cmt">//--- Score class="type">int temp = OpenCL.AddBuffer(class="kw">sizeof(class="type">float) * iCount * iCount * iHeads, CL_MEM_READ_WRITE); if(temp < class="num">0) class="kw">return false; if(!caScore.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- MH Attention Out base = new CNeuronBaseOCL(); if(!base) class="kw">return false; if(!base.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iHeadsKV * iCount, optimization, iBatch) || !caAttentionOut.Add(conv)) { class="kw">delete base; class="kw">return false; } base.SetActivationFunction(None); class=class="str">"cmt">//--- W0 conv = new CNeuronConvOCL(); if(!conv) class="kw">return false; if(!conv.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iHeadsKV, iWindowKey * iHeadsKV, iWindowIn, iCount, optimization, iBatch) || !caW0.Add(conv)) {
「残差连接与多头注意力输出的底层拼装」
这段初始化逻辑里,残差分支用 CNeuronBaseOCL 接在卷积层后面,输入维度直接拉到 iWindowIn * iCount,激活函数设成 None,意味着它只做线性映射、不引入非线性扭曲。若 base 指针分配失败或 Init 不通过,立刻 delete 并返回 false,避免悬空对象拖垮后续前向。 前馈部分 cFF1 隐藏层扩到 4 * iWindowIn 再经 cFF2 压缩回 iWindowIn,cFF1 用 LReLU、cFF2 用 None,梯度指针从 cFF2.getGradient() 接出。这种「宽—窄」对称结构在行情特征提取时,可能更利于保留局部突变。 下方 OpenCL 核 MH2PyrAttentionOut 用三维全局 ID 定位 q/k/h,heads_kv 与 heads 不等时靠 h % heads_kv 做映射;score 偏移按 kunits * (q_id * heads + h) + k 计算,显存布局直接决定并行效率。外汇与贵金属波动受杠杆放大,跑这类 GPU 核前务必在小样本上验证维度匹配,否则可能爆显存或静默算错。
class="kw">delete conv; class="kw">return false; } conv.SetActivationFunction(None); } class=class="str">"cmt">//--- Residual base = new CNeuronBaseOCL(); if(!base) class="kw">return false; if(!base.Init(class="num">0, class="num">0, OpenCL, iWindowIn * iCount, optimization, iBatch) || !caW0.Add(conv)) { class="kw">delete base; class="kw">return false; } base.SetActivationFunction(None); class=class="str">"cmt">//--- FeedForward if(!cFF1.Init(class="num">0, class="num">0, OpenCL, iWindowIn, iWindowIn, class="num">4 * iWindowIn, iCount, optimization, iBatch)) class="kw">return false; cFF1.SetActivationFunction(LReLU); if(!cFF2.Init(class="num">0, class="num">0, OpenCL, class="num">4 * iWindowIn, class="num">4 * iWindowIn, iWindowIn, iCount, optimization, iBatch)) class="kw">return false; cFF2.SetActivationFunction(None); if(!SetGradient(cFF2.getGradient())) class="kw">return false; SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void MH2PyrAttentionOut(__global class="type">float *q, __global class="type">float *kv, __global class="type">float *score, __global class="type">float *out, const class="type">int dimension, const class="type">int heads_kv, const class="type">int window ) { class=class="str">"cmt">//--- init const class="type">int q_id = get_global_id(class="num">0); const class="type">int k = get_global_id(class="num">1); const class="type">int h = get_global_id(class="num">2); const class="type">int qunits = get_global_size(class="num">0); const class="type">int kunits = get_global_size(class="num">1); const class="type">int heads = get_global_size(class="num">2); const class="type">int h_kv = h % heads_kv; const class="type">int shift_q = dimension * (q_id * heads + h); const class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv); const class="type">int shift_v = dimension * (class="num">2 * heads_kv * k + heads_kv + h_kv); const class="type">int shift_s = kunits * (q_id * heads + h) + k;
正文
<span class="keyword">const</span> <span class="keyword">uint</span> ls = min((<span class="keyword">uint</span>)get_local_size(<span class="number">1</span>), (<span class="keyword">uint</span>)LOCAL_ARRAY_SIZE); <span class="keyword">const</span> <span class="keyword">int</span> delta_win = (window + <span class="number">1</span>) / <span class="number">2</span>; <span class="keyword">float</span> koef = <span class="functions">sqrt</span>((<span class="keyword">float</span>)dimension); <span class="keyword">if</span>(koef < <span class="number">1</span>) koef = <span class="number">1</span>; __local <span class="keyword">float</span> temp[LOCAL_ARRAY_SIZE]; <span class="comment">//--- sum of exp</span> <span class="keyword">uint</span> count = <span class="number">0</span>; <span class="keyword">if</span>(k < ls) <span class="keyword">do</span> { <span class="keyword">if</span>((count * ls) < (kunits - k)) { <span class="keyword">float</span> sum = <span class="number">0</span>; <span class="keyword">if</spa
◍ 稀疏金字塔注意力的内核归约与前向链路
上面这段 OpenCL 内核尾部在做局部内存的并行归约:先以 CLK_LOCAL_MEM_FENCE 屏障保证 temp[] 写透,再用 do-while 把 count 折半,把 temp[k+count] 累加进 temp[k] 并清零上半段,最终 out[shift_q+d]=temp[0] 拿到单层注意力输出。这种折半归约把长度为 ls 的局部求和从 O(ls) 降到 O(log2 ls),在 ls=64 时理论步数由 63 缩到 6。 feedForward 里按层循环走完 Mix→Query→KV→PAM→W0 五段:每层先取 caS3 做混合,再分别从 caQuery、caKV 抽 q 与 kv 缓冲,丢给 AttentionOut 算稀疏注意力,最后经 caW0 投影。任一环节 FeedForward 返回 false 就整体退出,说明该网络对前层对象空指针零容忍。 末层 caW0.At(iPAMLayers) 出来后接 SumAndNormilize,把残差输入 NeuronOCL.getOutput() 与当前 prev 做加和归一,iWindowIn 控制窗口、true 标记训练态。想在 MT5 验证,可直接在 Expert Advisor 里实例化 CNeuronSPyrAttentionOCL 并单步跟 caAttentionOut 的 out 缓冲,看 temp[0] 是否随 ls 折半稳定收敛。外汇与贵金属行情下用此类模型信号属于高风险,实盘前务必用历史 tick 回测。
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]; } } class="type">bool CNeuronSPyrAttentionOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL *prev = NeuronOCL; CNeuronBaseOCL *current = NULL; CBufferFloat *q = NULL; CBufferFloat *kv = NULL; for(class="type">uint l = class="num">0; l < iPAMLayers; l++) { class=class="str">"cmt">//--- Mix current = caS3.At(l); if(!current || !current.FeedForward(prev.AsObject()) ) class="kw">return false; prev = current; class=class="str">"cmt">//--- Query current = caQuery.At(l); if(!current || !current.FeedForward(prev.AsObject()) ) class="kw">return false; q = current.getOutput(); class=class="str">"cmt">//--- Key and Value current = caKV.At(l); if(!current || !current.FeedForward(prev.AsObject()) ) class="kw">return false; kv = current.getOutput(); class=class="str">"cmt">//--- PAM current = caAttentionOut.At(l); if(!current || !AttentionOut(q, kv, caScore.At(l), current.getOutput(), iPAMLayers - l)) class="kw">return false; prev = current; class=class="str">"cmt">//--- W0 current = caW0.At(l); if(!current || !current.FeedForward(prev.AsObject()) ) class="kw">return false; prev = current; } class=class="str">"cmt">//--- Residual current = caW0.At(iPAMLayers); if(!SumAndNormilize(NeuronOCL.getOutput(), prev.getOutput(), current.getOutput(), iWindowIn, true))
「残差连接下的梯度回传链路」
这段 CNeuronSPyrAttentionOCL::calcInputGradients 负责把输出端的梯度一层层反推到前层,金字塔式注意力里残差结构让回传路径不只一条。 先对 FeedForward 支路调 cFF1.calcHiddenGradients(cFF2.AsObject()) 和 current.calcHiddenGradients(cFF1.AsObject()),任一失败立即返回 false,保证前向两张网权重同步更新。 残差部分用 SumAndNormilize(getGradient(), next.getGradient(), current.getGradient(), iWindowIn, false) 把上层梯度和本层相加再归一,iWindowIn 控制窗口长度,false 表示反向不重写输出缓冲。 主循环从 l = iPAMLayers-1 降到 0:每轮先算 W0 的 AttentionOut 梯度,再进 AttentionInsideGradients 推 Query/Key/Value,其中 caScore.At(l) 存注意力分数;Query 支路额外 Concat 残差,Key/Value 支路用 SumAndNormilize 合并梯度。 循环外最后对 prevLayer 做 DeActivation 并把 residual 合进 prevLayer 梯度,若 iWindowIn 设错会导致归一长度偏移、梯度幅值异常,开 MT5 把 iWindowIn 打印出来比对前层输出维度即可验证。
class="kw">return false; class=class="str">"cmt">//---FeedForward if(!cFF1.FeedForward(current.AsObject()) || !cFF2.FeedForward(cFF1.AsObject()) ) class="kw">return false; class=class="str">"cmt">//--- Residual if(!SumAndNormilize(current.getOutput(), cFF2.getOutput(), getOutput(), iWindowIn, true)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronSPyrAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!prevLayer) class="kw">return false; CNeuronBaseOCL *next = NULL; CNeuronBaseOCL *current = NULL; CNeuronBaseOCL *q = NULL; CNeuronBaseOCL *kv = NULL; class=class="str">"cmt">//--- FeedForward current = caW0.At(iPAMLayers); if(!current || !cFF1.calcHiddenGradients(cFF2.AsObject()) || !current.calcHiddenGradients(cFF1.AsObject()) ) class="kw">return false; next = current; class=class="str">"cmt">//--- Residual current = caW0.At(iPAMLayers - class="num">1); if(!SumAndNormilize(getGradient(), next.getGradient(), current.getGradient(), iWindowIn, false)) class="kw">return false; CBufferFloat *residual = next.getGradient(); next = current; for(class="type">int l = class="type">int(iPAMLayers - class="num">1); l >= class="num">0; l--) { class=class="str">"cmt">//--- W0 current = caAttentionOut.At(l); if(!current || !current.calcHiddenGradients(next.AsObject()) ) class="kw">return false; class=class="str">"cmt">//--- MH Attention q = caQuery.At(l); kv = caKV.At(l); if(!q || !kv || !AttentionInsideGradients(q.getOutput(), q.getGradient(), kv.getOutput(), kv.getGradient(), caScore.At(l), current.getGradient()) ) class="kw">return false; class=class="str">"cmt">//--- Query current = caS3.At(l); if(!current || !current.calcHiddenGradients(q.AsObject()) || !Concat(current.getGradient(), current.getGradient(), residual, iWindowIn,class="num">0, iCount) ) class="kw">return false; class=class="str">"cmt">//--- Key and Value if(!current || !current.calcHiddenGradients(kv.AsObject()) || !SumAndNormilize(current.getGradient(), residual, current.getGradient(), iWindowIn, false) ) class="kw">return false; next = current; class=class="str">"cmt">//--- S3 current = (l == class="num">0 ? prevLayer : caW0.At(l - class="num">1)); if(!current || !current.calcHiddenGradients(next.AsObject()) ) class="kw">return false; next = current; } current = caW0.At(iPAMLayers - class="num">1); if(!DeActivation(prevLayer.getOutput(), current.getGradient(), residual, prevLayer.Activation()) || !SumAndNormilize(prevLayer.getGradient(), residual, prevLayer.getGradient(), iWindowIn, false)
金字塔注意力层的权重回传链路
这段逻辑属于一个叫 CNeuronSPyrAttentionOCL 的类的 updateInputWeights 方法,职责是把上一层算出的梯度按金字塔注意力的结构逐层往前传。它遍历 iPAMLayers 层,每一层里依次处理 S3、Query、Key/Value、W0 四个子模块,任何一个子模块的 UpdateInputWeights 返回失败就整体返回 false。 具体来看,prev 指针初始指向传入的 NeuronOCL,先取 caS3 的第 l 层做权重更新,成功后才把 prev 换成 S3 的输出;接着 Query 层以 S3 输出为前层,Key/Value 层复用同一 prev,而 W0 层的前层被硬性指定为 caAttentionOut.At(l) 而非上一层的 KV 输出,这里和前面几层的串联方式不一样,调试时容易看错。 所有金字塔层跑完后,prev 被设为 caW0 的最后一层输出,再连续更新 cFF1 和 cFF2 两个前馈层;只要 cFF1 或 cFF2 任一更新失败就返回 false,全过才返回 true。在 MT5 里挂这类自定义神经网络 EA 时,若日志出现权重更新中断,优先对照这段检查 caAttentionOut 的层数索引是否越界。
class="type">bool CNeuronSPyrAttentionOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL *prev = NeuronOCL; CNeuronBaseOCL *current = NULL; for(class="type">uint l = class="num">0; l < iPAMLayers; l++) { class=class="str">"cmt">//--- S3 current = caS3.At(l); if(!current || !current.UpdateInputWeights(prev) ) class="kw">return false; class=class="str">"cmt">//--- Query prev = current; current = caQuery.At(l); if(!current || !current.UpdateInputWeights(prev) ) class="kw">return false; class=class="str">"cmt">//--- Key and Value current = caKV.At(l); if(!current || !current.UpdateInputWeights(prev) ) class="kw">return false; class=class="str">"cmt">//--- W0 prev = caAttentionOut.At(l); current = caW0.At(l); if(!current || !current.UpdateInputWeights(prev) ) class="kw">return false; prev = current; } class=class="str">"cmt">//--- FeedForward prev = caW0.At(iPAMLayers); if(!cFF1.UpdateInputWeights(prev) || !cFF2.UpdateInputWeights(cFF1.AsObject()) ) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; }
◍ 下一篇再看回测结果
这一节把 MSFformer(多尺度特征变换器)的来路交代清楚了:它出自《基于多尺度特征提取进行时间序列预测》那篇论文,核心是用改进的金字塔关注度架构,加上从输入里抽不同尺度特征的新办法。 我们在实践部分已经把两个主模块写进了 MT5 的自定义指标工程,代码能直接编译跑通,但训练与预测效果还没在这篇亮出来。 外汇与贵金属行情受宏观事件冲击大、波动跳空频繁,这类多尺度模型在样本外大概率会衰减,实盘前务必用历史数据自己回一遍。 下一篇会直接给模块的回测曲线和参数敏感区间,到时候你可以照着把周期切到 M15 对照黄金 tick 数据复现。
「记住这一条就够了」
这套 LSTM 优化方案落地到 MT5,靠的是 8 个文件分工:6 个 EA(Research / ResearchRealORL / Study / StudyEncoder / Test 加上前文训练用脚本)负责采样、训练与验证,Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 与 NeuroNet.cl 把网络创建和 OpenCL 加速拆成类库与内核。 要复现,先下 MQL5.zip(约 1.39 MB)把上述文件塞进对应目录,用 Research 跑样本、Study 训模型、Test 看多元序列预测偏差;外汇与贵金属波动剧烈,模型输出只是概率倾向,实盘前务必在策略测试器用历史数据核验过拟合程度。 真要把它用起来,核心就一句:别只调网络层数,采样 EA 的窗口与特征构造才是预测胜率的天花板。