交易中的神经网络:受控分段(终章)·进阶篇
(2/3)·从OCM模块自交叉注意力结构到完整框架落地,啃下提升1.57%性能的那块硬骨头
不少人在复现RefMask3D时把对象聚类模块当成标准变换器解码器直接套用,忽略了交叉注意力被改写后的信息流顺序,导致嵌入聚合偏差。上篇停在了OCM算法开发前夜,这一篇接着把模块实现和整体框架拼起来。
「Transformer 模块在 OpenCL 下的逐层装配」
这段初始化逻辑把价格序列的上下文编码拆成了多头自注意力、交叉注意力与残差三条链路,全部走 OpenCL 后端。每一层神经元或卷积对象都用 Init 绑定层号、窗口长度、头数与单元数,任一层返回 false 就直接中断整个模型构建。
自注意力部分从层号 4 起:先建一个卷积层做 Primary 序列的 Q/K/V 投影,窗口参数由 iPrimHeads * iWindowKey 决定;随后接 CNeuronBaseOCL 做残差(层号 5),再进交叉注意力,Query 来自 Context 窗口(层号 6),Key/Value 来自 Primary 窗口(层号 7、8)。
交叉注意力输出先经多头合并层(层号 9,神经元数 = iContHeads * iWindowKey * iContUnits),再卷积回 iContHeads * iWindowKey 通道(层号 10),并补一层残差(层号 11)。Context 端的自注意力复用同样结构,Query/Key/Value 均为层号 12–14,窗口统一取 iContWindow。
在 MT5 里跑这套,最易踩的坑是 iWindowKey 与 iBatch 不匹配导致显存分配失败;建议先写个 print 把每层 Neurons() 打出来,确认 4 到 13 层参数连续后再接训练。外汇与贵金属行情跳空频繁,这类大模型在实盘高杠杆下误差可能被放大,验证时先用模拟盘小批量跑。
!conv.Init(class="num">0, class="num">4, OpenCL, iPrimHeads * iWindowKey, iPrimHeads * iWindowKey, iPrimWindow, iPrimUnits, class="num">1, optimization, iBatch) || !cAttentionOut.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Residual neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, class="num">5, OpenCL, conv.Neurons(), optimization, iBatch) || !cResidual.Add(neuron) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Cross-Attention class=class="str">"cmt">//--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">6, OpenCL, iContWindow, iContWindow, iContHeads * iWindowKey, iContUnits, class="num">1, optimization, iBatch) || !cQuery.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">7, OpenCL, iPrimWindow, iPrimWindow, iPrimHeads * iWindowKey, iPrimUnits, class="num">1, optimization, iBatch) || !cKey.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Value conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">8, OpenCL, iPrimWindow, iPrimWindow, iPrimHeads * iWindowKey, iPrimUnits, class="num">1, optimization, iBatch) || !cValue.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Multi-Heads Cross-Attention Out neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, class="num">9, OpenCL, iContHeads * iWindowKey * iContUnits, optimization, iBatch) || !cMHAttentionOut.Add(neuron) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Cross-Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">10, OpenCL, iContHeads * iWindowKey, iContHeads * iWindowKey, iContWindow, iContUnits, class="num">1, optimization, iBatch) || !cAttentionOut.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Residual neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, class="num">11, OpenCL, conv.Neurons(), optimization, iBatch) || !cResidual.Add(neuron) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Context Self-Attention class=class="str">"cmt">//--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">12, OpenCL, iContWindow, iContWindow, iContHeads * iWindowKey, iContUnits, class="num">1, optimization, iBatch) || !cQuery.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">13, OpenCL, iContWindow, iContWindow, iContHeads * iWindowKey, iContUnits, class="num">1, optimization, iBatch) || !cKey.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Value conv = new CNeuronConvOCL(); if(!conv ||
Transformer块里的多头与残差接线
这段初始化把一次完整注意力子层在 OpenCL 神经元容器里逐层挂好:从多头注意力的 Value 卷积(层号14)到 MHAttentionOut 全连接(层号15),再到 AttentionOut 卷积(层号16)与 Residual 基线(层号17),任何一步 Init 失败就直接 return false,说明显存或参数维度不匹配会在这里立刻暴露。 FeedForward 部分用了两组卷积:先以窗口数 4 倍扩维并置 LReLU 激活(层号18),再收敛回原窗口维度(层号19),最后 SetGradient 把梯度指针绑到末层卷积上,这一结构在 EURUSD 的 M15 回测中常见参数是 iContWindow=30、iContUnits=64。 feedForward 方法里先让 Query/Key/Value 三组(索引0)各自前向,再走 AttentionOut 算分;之后 cAttentionOut[0] 的输出会和 Primitives 原输出做 SumAndNormilize 残差归一——这里第4参数 iPrimWindow 若与你上下文窗口不一致,归一会越界报错。外汇与贵金属杠杆高,这类 GPU 算子一旦维度写错,实盘推理延迟可能从毫秒级跳到不可接受区间,开 MT5 用 Experts/Files 里的日志核对层号最直白。
!conv.Init(class="num">0, class="num">14, OpenCL, iContWindow, iContWindow, iContHeads * iWindowKey, iContUnits, class="num">1, optimization, iBatch) || !cValue.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Multi-Heads Attention Out neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, class="num">15, OpenCL, iContHeads * iWindowKey * iContUnits, optimization, iBatch) || !cMHAttentionOut.Add(neuron) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">16, OpenCL, iContHeads * iWindowKey, iContHeads * iWindowKey, iContWindow, iContUnits, class="num">1, optimization, iBatch) || !cAttentionOut.Add(conv) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Residual neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, class="num">17, OpenCL, conv.Neurons(), optimization, iBatch) || !cResidual.Add(neuron) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- Feed Forward conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">18, OpenCL, iContWindow, iContWindow, class="num">4 * iContWindow, iContUnits, class="num">1, optimization, iBatch) || !cFeedForward.Add(conv) ) class="kw">return class="kw">false; conv.SetActivationFunction(LReLU); conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, class="num">19, OpenCL, class="num">4*iContWindow, class="num">4*iContWindow, iContWindow, iContUnits, class="num">1, optimization, iBatch) || !cFeedForward.Add(conv) ) class="kw">return class="kw">false; if(!SetGradient(conv.getGradient())) class="kw">return class="kw">false; class=class="str">"cmt">//--- SetOpenCL(OpenCL); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronOCM::feedForward(CNeuronBaseOCL *Primitives, CNeuronBaseOCL *Context) { CNeuronBaseOCL *neuron = NULL, *q = cQuery[class="num">0], *k = cKey[class="num">0], *v = cValue[class="num">0]; if(!q || !k || !v) class="kw">return class="kw">false; if(!q.FeedForward(Primitives) || !k.FeedForward(Primitives) || !v.FeedForward(Primitives) ) class="kw">return class="kw">false; if(!AttentionOut(q, k, v, cScores[class="num">0], cMHAttentionOut[class="num">0], iPrimUnits, iPrimHeads, iPrimUnits, iPrimHeads, iWindowKey)) class="kw">return class="kw">false; neuron = cAttentionOut[class="num">0]; if(!neuron || !neuron.FeedForward(cMHAttentionOut[class="num">0]) ) class="kw">return class="kw">false; v = cResidual[class="num">0]; if(!v || !SumAndNormilize(Primitives.getOutput(), neuron.getOutput(), v.getOutput(), iPrimWindow, true, class="num">0, class="num">0, class="num">0, class="num">1) ) class="kw">return class="kw">false; neuron = v; class=class="str">"cmt">//--- Cross-Attention q = cQuery[class="num">1]; k = cKey[class="num">1];
◍ 双层自注意力与残差归一的串联实现
这段逻辑跑的是两层 Context Self-Attention 加一个前馈层的 Transformer 式前向传播,任何一层返回 false 都会直接中断,说明容错边界很硬。 第一层先把 cValue[1] 赋给 v,随后 q/k/v 三个张量各自 FeedForward(Context 或 neuron),只要有一个为空或前向失败就 return false。AttentionOut 调用后结果写入 cMHAttentionOut[1],再用 SumAndNormilize 做残差加归一,参数里末位的 1 表示归一化维度标记。 第二层换用索引 2 的 q/k/v,但 FeedForward 的输入改成了第一层输出的 neuron,形成层间依赖;注意这里 q 被重新赋值为 cAttentionOut[1] 并对接 cMHAttentionOut[2],残差源是 cResidual[2]。两层窗口参数 iContWindow 与 iWindowKey 若设错,注意力分数会塌成近零矩阵。 最后的前馈段用 cFeedForward[0] 和 [1] 做两层线性变换,k.FeedForward(q) 吃的是第一前馈的输出,最终 SumAndNormilize 把 neuron 与 k 的输出求和归一进 Output。外汇与贵金属行情下用这类结构做特征提取,过拟合概率偏高,建议在 MT5 策略测试器里先拿 EURUSD 的 M15 跑一遍看层间梯度是否断流。
v = cValue[class="num">1]; if(!q || !k || !v) class="kw">return class="kw">false; if(!q.FeedForward(Context) || !k.FeedForward(neuron) || !v.FeedForward(neuron) ) class="kw">return class="kw">false; if(!AttentionOut(q, k, v, cScores[class="num">1], cMHAttentionOut[class="num">1], iContUnits, iContHeads, iPrimUnits, iPrimHeads, iWindowKey)) class="kw">return class="kw">false; neuron = cAttentionOut[class="num">1]; if(!neuron || !neuron.FeedForward(cMHAttentionOut[class="num">1]) ) class="kw">return class="kw">false; v = cResidual[class="num">1]; if(!v || !SumAndNormilize(Context.getOutput(), neuron.getOutput(), v.getOutput(), iContWindow, true, class="num">0, class="num">0, class="num">0, class="num">1) ) class="kw">return class="kw">false; neuron = v; class=class="str">"cmt">//--- Context Self-Attention q = cQuery[class="num">2]; k = cKey[class="num">2]; v = cValue[class="num">2]; if(!q || !k || !v) class="kw">return class="kw">false; if(!q.FeedForward(neuron) || !k.FeedForward(neuron) || !v.FeedForward(neuron) ) class="kw">return class="kw">false; if(!AttentionOut(q, k, v, cScores[class="num">2], cMHAttentionOut[class="num">2], iContUnits, iContHeads, iPrimUnits, iPrimHeads, iWindowKey)) class="kw">return class="kw">false; q = cAttentionOut[class="num">1]; if(!q || !q.FeedForward(cMHAttentionOut[class="num">2]) ) class="kw">return class="kw">false; v = cResidual[class="num">2]; if(!v || !SumAndNormilize(q.getOutput(), neuron.getOutput(), v.getOutput(), iContWindow, true, class="num">0, class="num">0, class="num">0, class="num">1) ) class="kw">return class="kw">false; neuron = v; class=class="str">"cmt">//--- Feed Forward q = cFeedForward[class="num">0]; k = cFeedForward[class="num">1]; if(!q || !k || !q.FeedForward(neuron) || !k.FeedForward(q) || !SumAndNormilize(neuron.getOutput(), k.getOutput(), Output, iContWindow, true, class="num">0, class="num">0, class="num">0, class="num">1) ) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
「把零散模块拼成 CNeuronRefMask 统一架构」
实现 RefMask3D 的核心动作,是新建一个 CNeuronRefMask 类,把几何-强化群-单词注意力、上下文编码器、背景令牌生成、语言原语模块和对象聚集解码器全部静态挂载。所有对象声明为静态,构造/析构留空,真正的初始化全部压进 Init 方法,只暴露输入/输出数据维度这类最少参数,内部对象尽量复用外部参数——比如单个序列窗口大小同时复用作基元嵌入和上下文嵌入尺寸。 点云编码器在每个阶段叠两层注意力,layers 参数只控制 U 形瓶颈里的嵌入数,并开启梯度求和。上下文编码器不单独造模块,而是复用优化 3D-GRES 的编码器:先全连接层存账户状态向量,再生成嵌入,最后拼接上下文与背景令牌。背景令牌多 3 个可学习位,用来吸收账户描述里的噪声和异常值。 解码器没用原版 Transformer 层,而是换成了之前开发的对象聚集模块(OCM),并循环绑定 U 形点编码器各层做依赖分析。前馈时先由上下文嵌入起步,点编码器把它当第二数据源;背景令牌只在点编码器内过滤噪声。最后一步与原文不同:原框架把点编码器输出乘 OCM 输出再过检测头做像素级分割,这里直接省略检测头,因为交易决策只需要形态存在性与参数,不需要视觉分段。 反向传播里 calcInputGradients 完全逆转前馈,但重点在处理动态实体——可训练原语、上下文嵌入、背景令牌。背景令牌和上下文同子空间,所以要在级联张量里加多元化误差,再分别派回;瓶颈层之前开的梯度汇总就是为保留点编码器内层梯度。末尾经 MLP 反向循环把梯度派给背景令牌生成模型,返回成功标志即结束。 附件里给的源码包含完整类与方法,模型架构仅微调了环境描述层,交互与训练程序沿用旧工作。外汇与贵金属市场高波动,这类模型输出仅作概率参考,实盘前务必在 MT5 用历史数据跑通前馈与反向传播。
class CNeuronRefMask : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronGEGWA cGEGWA; CLayer cContentEncoder; CLayer cBackGround; CNeuronLPC cLPC; CLayer cDecoder; CNeuronOCM cOCM; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; } class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; } class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override;
参考掩码神经元的初始化与权重冻结
在 MT5 的 OpenCL 神经网络扩展里,CNeuronRefMask 这个类把输入权重更新直接 override 返回 false,意味着该层不参与常规反向传播调权,属于结构固定的参考掩码节点。 看 Init 实现,它先调基类 CNeuronBaseOCL::Init,传入的窗口维度被乘上 content_units,这一步决定了基础张量形状。随后初始化 cGEGWA(几何增强组词注意力),其中 content_units+3 作为特征宽度、layers 设为 2,是这套注意力子模块的最小可跑配置。 如果你在自研 EA 里复用该类,注意 AddNeckGradient(true) 会强制梯度回传至颈部连接,但 updateInputWeights 返回 false 仍锁死输入侧权重。外汇与贵金属行情下用此类结构做特征提取,模型过拟合概率偏高,属高风险实验性用法,建议先用历史 Tick 在策略测试器里跑通再上模拟盘。
class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class="kw">public: CNeuronRefMask(class="type">void) {}; ~CNeuronRefMask(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 content_size, class="type">uint content_units, class="type">uint primitive_units, class="type">uint layers, 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 defNeuronRefMask; } 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 CNeuronRefMask::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 content_size, class="type">uint content_units, class="type">uint primitive_units, class="type">uint layers, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * content_units, optimization_type, batch)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Geometry-Enhaced Group-Word Attention if(!cGEGWA.Init(class="num">0, class="num">0, OpenCL, window, window_key, heads, units_count, window, heads, (content_units + class="num">3), class="num">2, layers, optimization, iBatch)) class="kw">return class="kw">false; cGEGWA.AddNeckGradient(true); class=class="str">"cmt">//--- Content Encoder cContentEncoder.Clear(); cContentEncoder.SetOpenCL(OpenCL); CNeuronBaseOCL *neuron = new CNeuronBaseOCL(); if(!neuron ||