交易中的神经网络:将全局信息注入独立通道(InjectTST)·综合运用
(3/3)·从分段高速公路到通道标识符,一篇走完多模态时序预测的收尾工程
◍ 多头注意力里的 OpenCL 参数绑定与 KV 拼接
在 MT5 的 OpenCL 神经网络封装里,内核参数必须逐个绑定,否则后续执行直接返回 false。上面这段把 MH2AttentionOut 内核的 heads_kv 和 mask 参数塞进去,mask 传 0 表示暂不启用掩码,Execute 的第三个维度工作项设为 3,对应多头输出的并行粒度。 feedForward 里按层循环算 Q/K/V,每 iLayersToOneKV 层才重新算一次 KV 并做 Concat。注意 Concat 调用把 k 和 v 在特征维拼接,长度分别是 iWindowKey*iHeadsKV*iVariables,若 iHeadsKV 与 iHeads 不一致,显存布局会错位,黄金 1 分钟图回测时可能直接崩内核。 实操上建议把 iLayersToOneKV 设为 1 先跑通,再逐步放大观察 GPU 占用;外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必在策略测试器用裸数据验证缓冲区尺寸。
if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_heads_kv, (class="type">int)(iHeadsKV * iVariables))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_mask, class="num">0)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_MH2AttentionOut, class="num">3, global_work_offset, global_work_size, local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMVMHAttentionMLKV::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) class="kw">return false; CBufferFloat *kv = NULL; for(class="type">uint i = class="num">0; (i < iLayers && !IsStopped()); i++) { class=class="str">"cmt">//--- Calculate Queries, Keys, Values CBufferFloat *inputs = (i == class="num">0 ? NeuronOCL.getOutput() : FF_Tensors.At(class="num">6 * i - class="num">4)); CBufferFloat *q = QKV_Tensors.At(i * class="num">2); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), inputs, q, iWindow, iWindowKey * iHeads, None)) class="kw">return false; if((i % iLayersToOneKV) == class="num">0) { class="type">uint i_kv = i / iLayersToOneKV; kv = KV_Tensors.At(i_kv * class="num">2); CBufferFloat *k = K_Tensors.At(i_kv * class="num">2); CBufferFloat *v = V_Tensors.At(i_kv * class="num">2); if(IsStopped() || !ConvolutionForward(K_Weights.At(i_kv * (optimization == SGD ? class="num">2 : class="num">3)), inputs, k, iWindow, iWindowKey * iHeadsKV, None)) class="kw">return false; if(IsStopped() || !ConvolutionForward(V_Weights.At(i_kv * (optimization == SGD ? class="num">2 : class="num">3)), inputs, v, iWindow, iWindowKey * iHeadsKV, None)) class="kw">return false; if(IsStopped() || !Concat(k, v, kv, iWindowKey * iHeadsKV * iVariables, iWindowKey * iHeadsKV * iVariables, iUnits)) class="kw">return false; } class=class="str">"cmt">//--- Score calculation and Multi-heads attention calculation CBufferFloat *temp = S_Tensors.At(i * class="num">2);
Transformer 残差块的前向流转
下面这段循环是时序 Transformer 里单层前向的核心:先取注意力输出缓冲,做多头注意力,再走两次卷积前馈与残差归一。注意循环里每一小步都检查 IsStopped(),在 MT5 策略测试器点停止时能立刻中断,避免 GPU 任务空跑。
卷积权重偏移量随优化器切换:SGD 时每层占 6 个权重块,Adam 类则占 9 个,代码用 optimization == SGD ? 6 : 9 直接算地址。前馈分两段,第一段把窗口维扩到 4 倍窗口并用 LReLU,第二段收回原窗口并套用户指定的 activation,这种「膨胀—压缩」结构是 TST 类模型容量来源之一。
紧随其后的 CNeuronInjectTST 类把 patching、可学习位置编码、通道独立/混合注意力、跨注意力全塞进一个神经元派生类里。它重写 feedForward / calcInputGradients / updateInputWeights 三个虚函数,意味着梯度与权重更新逻辑完全自定义,不是标准全连接层。
在 MT5 里验证时,把 window=30、heads=3 这类参数丢进 Init,看 feedForward 是否在每层返回 true;若某层卷积报 false,优先查 FF_Weights 索引是否越了 i*(优化器?6:9)+2 的界。外汇与贵金属行情跳空多,这类深结构过拟合概率偏高,实盘前务必用不同年份数据回测。
CBufferFloat *out = AO_Tensors.At(i * class="num">2); if(IsStopped() || !AttentionOut(q, kv, temp, out)) class="kw">return false; class=class="str">"cmt">//--- Attention out calculation temp = FF_Tensors.At(i * class="num">6); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9)), out, temp, iWindowKey * iHeads, iWindow, None)) class="kw">return false; class=class="str">"cmt">//--- Sum and normilize attention if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow, true)) class="kw">return false; class=class="str">"cmt">//--- Feed Forward inputs = temp; temp = FF_Tensors.At(i * class="num">6 + class="num">1); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1), inputs, temp, iWindow, class="num">4 * iWindow, LReLU)) class="kw">return false; out = FF_Tensors.At(i * class="num">6 + class="num">2); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2), temp, out, class="num">4 * iWindow, iWindow, activation)) class="kw">return false; class=class="str">"cmt">//--- Sum and normilize out if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class CNeuronInjectTST : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronPatching cPatching; CNeuronLearnabledPE cCIPosition; CNeuronLearnabledPE cCMPosition; CNeuronMVMHAttentionMLKV cChanelIndependentAttention; CNeuronMLMHAttentionMLKV cChanelMixAttention; CNeuronMVCrossAttentionMLKV cGlobalInjectionAttention; CBufferFloat cTemp; 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=class="str">"cmt">//--- class="kw">public: CNeuronInjectTST(class="type">void) {}; ~CNeuronInjectTST(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 heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint layers, class="type">uint layers_to_one_kv,
「TST注入神经元的初始化链路」
在 MT5 的 OpenCL 神经网络框架里,CNeuronInjectTST 把时间序列处理拆成了 6 个子模块串联,从补丁嵌入到全局注入注意力一气呵成。Init 方法先调基类 CNeuronBaseOCL::Init,传入的神经元规模是 window * units_count * variables,这一步决定了显存占用的下限。 下面这段是 Init 的实体,逐层构造若任一子模块返回 false 则整体 abort,实战中若你改了 window 或 variables 参数却忘了同步 cTemp 的 BufferInit 尺寸,会在 cTemp.BufferCreate 阶段直接崩。 所有子模块统一 SetActivationFunction(None),说明该神经元不做非线性激活,纯粹靠注意力权重做特征重组;外汇与贵金属行情的高波动下,这种线性通道可能放大噪声,调参时建议先用小 batch(如 32)在 EURUSD 的 M15 上验证梯度是否发散。 feedForward 入口只展示了 cPatching.FeedForward(NeuronOCL) 的判错,后续各子模块的前向调用必须按顺序接上,否则输出张量维度对不上 SetOutput 绑定的 cGlobalInjectionAttention.getOutput()。
class="type">bool CNeuronInjectTST::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint layers, class="type">uint layers_to_one_kv, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables, optimization_type, batch)) class="kw">return false; SetActivationFunction(None); if(!cPatching.Init(class="num">0, class="num">0, OpenCL, window, window, window, units_count, variables, optimization, iBatch)) class="kw">return false; cPatching.SetActivationFunction(None); if(!cCIPosition.Init(class="num">0, class="num">1, OpenCL, window * units_count * variables, optimization, iBatch)) class="kw">return false; cCIPosition.SetActivationFunction(None); if(!cCMPosition.Init(class="num">0, class="num">2, OpenCL, window * units_count * variables, optimization, iBatch)) class="kw">return false; cCMPosition.SetActivationFunction(None); if(!cChanelIndependentAttention.Init(class="num">0, class="num">3, OpenCL, window, window_key, heads, heads_kv, units_count, layers, layers_to_one_kv, variables, optimization, iBatch)) class="kw">return false; cChanelIndependentAttention.SetActivationFunction(None); if(!cChanelMixAttention.Init(class="num">0, class="num">4, OpenCL, window * variables, window_key, heads, heads_kv, units_count, layers, layers_to_one_kv, optimization, iBatch)) class="kw">return false; cChanelMixAttention.SetActivationFunction(None); if(!cGlobalInjectionAttention.Init(class="num">0, class="num">5, OpenCL, window, window_key, heads, window * variables, heads_kv, units_count, units_count, layers, layers_to_one_kv, variables, class="num">1, optimization, iBatch)) class="kw">return false; cGlobalInjectionAttention.SetActivationFunction(None); if(!SetOutput(cGlobalInjectionAttention.getOutput(), true) || !SetGradient(cGlobalInjectionAttention.getGradient(), true) ) class="kw">return false; if(!cTemp.BufferInit(cPatching.Neurons(), class="num">0) || !cTemp.BufferCreate(OpenCL) ) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronInjectTST::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cPatching.FeedForward(NeuronOCL))
◍ 编码器里的分层前向与结构描述
这段逻辑把多头注意力里的几条位置通道串起来做前向:独立位置通道、混合位置通道、全局注入通道只要有一路 FeedForward 失败就直接 return false,全部通过才返回 true。这种短路写法在 MT5 跑神经网络推理时,能避免无效层继续占显存。 CreateEncoderDescriptions 负责把编码器各层结构塞进 CArrayObj。输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,激活函数 None,优化器 ADAM;第一层接 defNeuronBatchNormOCL,batch 设了 1e4,对外汇小时线这类中等样本量可能倾向更稳定。 第二层是 defNeuronInjectTST,window = PatchSize 决定切片窗,window_out = 8 是 Key 窗;单元数按 (HistoryBars + window - 1) / window 算。代码里 temp 数组头两个值 4 和 2,分别是 Heads 与 Heads KV——调这两个值会直接改注意力头数,贵金属回测时建议先小步试。 外汇与贵金属杠杆高,模型结构改动后务必用历史数据验证,勿盲信某组参数。
class="kw">return false; if(!cCIPosition.FeedForward(cPatching.AsObject()) || !cCMPosition.FeedForward(cPatching.AsObject()) ) class="kw">return false; if(!cChanelIndependentAttention.FeedForward(cCIPosition.AsObject())) class="kw">return false; if(!cChanelMixAttention.FeedForward(cCMPosition.AsObject())) class="kw">return false; if(!cGlobalInjectionAttention.FeedForward(cCIPosition.AsObject(), cCMPosition.getOutput())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *&encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronInjectTST; descr.window = PatchSize; class=class="str">"cmt">//Patch window descr.window_out = class="num">8; class=class="str">"cmt">//Window Key prev_count = descr.count = (HistoryBars + descr.window - class="num">1) / descr.window; class=class="str">"cmt">//Units { class="type">int temp[] = { class="num">4, class=class="str">"cmt">//Heads class="num">2, class=class="str">"cmt">//Heads KV
编码器末段层的堆叠与参数落点
上面这段是时序编码器后半段的层定义,从 layer 3 一直到 layer 8,每层都用 CLayerDescription 新建对象再塞进 encoder。任何一层 Add 失败就 delete 并 return false,这种写法在 MT5 里能避免内存泄漏但会中断整个模型装配。 descr.layers = 4 和 descr.step = 2 出现在 layer 2 收尾处,表示每 2 层做一次 KV 投影,batch 设成 1e4(即 10000)样本。外汇与贵金属行情的高噪声特征下,这种批量规模可能让显存占用陡增,实盘前应在策略测试器里先跑小样本。 layer 3 用 defNeuronTransposeOCL 做转置,window 直接等于 PatchSize * BarDescr;layer 4 和 layer 5 是卷积层 defNeuronConvOCL,激活分别用 LReLU 和 TANH,输出维度锁在 NForecast。TANH 把预测值压到 [-1,1],对应归一化后的价格变动,回测时若看到输出长期贴边就说明学习率可能偏大。 layer 7 的 defNeuronRevInDenormOCL 负责反归一化,count = BarDescr * NForecast 且 layers=1,是还原行情量纲的关键。最后 layer 8 起手用了 defNeuronFreDFOCL,属于频域分解层,具体窗口和 count 没在这段给全,开 MT5 把完整源码调出来补完即可验证。
BarDescr class=class="str">"cmt">//Variables }; ArrayCopy(descr.heads, temp); } descr.layers = class="num">4; class=class="str">"cmt">//Layers descr.step = class="num">2; class=class="str">"cmt">//Layers to class="num">1 KV descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = PatchSize * BarDescr; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = PatchSize * BarDescr; descr.window = prev_count; descr.window_out = NForecast; descr.activation = LReLU; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = PatchSize * NForecast; descr.window_out = NForecast; descr.activation = TANH; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = BarDescr; descr.window = NForecast; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronRevInDenormOCL; descr.count = BarDescr * NForecast; descr.activation = None; descr.optimization = ADAM; descr.layers = class="num">1; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFreDFOCL;
「给编码器喂入带概率窗口的描述结构」
这段片段演示了如何把一个带预测窗口与概率权重的描述对象塞进编码器。descr.window 设为 BarDescr 表示以 K 线描述为时间窗,descr.count 取 NForecast 控制向前预测的步数。 descr.step 用 int(true) 强转得到 1,意味着单步推进;descr.probability 设为 0.7f,代表该描述在解码时享有七成置信权重,低于此值的分支可能被剪枝。 descr.optimization 选 ADAM 做参数更新,activation 留 None 即线性直通。若 encoder.Add(descr) 返回失败,立即 delete 释放并回 false,避免悬空指针;成功才 return true。在 MT5 里把 NForecast 从 10 改到 20,能直接观察预测跨度拉长后内存占用的变化。
descr.window = BarDescr; descr.count = NForecast; descr.step = class="type">int(true); descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
◍ EURUSD 上的两阶段验证结果
环境状态编码器只吃历史价格、不受个体动作干扰,所以先单独训到预测误差停滞。本实验训练集取 EURUSD 2023 年 H1 周期数据,预测轨迹相对真实走向整体向上偏移、波动被平滑,但大方向跟目标轨迹一致,单看编码器并非最准。 第二阶段把参与者与评论者迭代训练:先跑若干局,再按参与者当前策略实际拿到的奖励回写数据集,让评论者奖励函数更贴真实回报,从而反推策略调整。 为验参与者政策,在 MT5 策略测试器用环境交互 EA 跑 2024 年 1 月数据、其余参数不动。59 笔成交中 30 笔盈利,最大与平均盈利均大于对应亏损,盈利因子 1.05;但余额曲线无清晰上行、回撤超 33%,外汇品种高杠杆下这种回撤倾向吞掉多数利润。 小布提醒:盈利因子刚过 1 不代表策略可实盘,33% 回撤在贵金属/外汇里可能触发强平,建议自己换 2024 其他月份重测一遍再下结论。
三阶段训练可能是漏掉的那块
前面用 MQL5 把 InjectTST 接进了环境状态编码器,独立通道注入全局信息的逻辑跑通了,但实盘前回测显示长期预测误差比预期高了一截,成果没达到设计目标。 直接拿端到端方式训环境状态预测模型,大概率不是原作者设想的用法。InjectTST 论文里推荐的是三阶段训练:先预训练通道级表征,再做全局注入对齐,最后微调预测头,我们跳过了前两步。 外汇与贵金属行情受宏观事件扰动,这类 Transformer 长期外推本身高风险,直接训出的权重在样本外容易漂移。下一步建议在 MT5 里把训练脚本拆成三阶段重跑,对比单阶段与三阶段的 30 根 K 线预测 MAE,看差距是否来自训练流程而非结构本身。
「随包附带的八个文件」
这套 LSTM 预测方案不是只给理论,作者把整套工程文件打进 MQL5.zip(1588.34 KB)随文发布,里面按职责拆成了三类。 样本收集和训练各跑各的 EA:Research.mq5 负责基础样本采集,ResearchRealORL.mq5 用 Real ORL 方法补样本,Study.mq5 训模型、StudyEncoder.mq5 单独训编码器,Test.mq5 做模型回测验证。 底层依赖是三个库:Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网类,NeuroNet.cl 是 OpenCL 核函数,直接决定 GPU 上能不能跑起来。 做外汇或贵金属预测时,把这些 mqh 挂进你的 MT5 包含目录,先单跑 Research.mq5 看样本写入速度,再决定要不要上 Real ORL——这类多元时序模型对行情突变的捕捉能力可能偏弱,杠杆品种高风险,参数没跑稳前别直接挂实盘。