交易中的神经网络:使用语言模型进行时间序列预测·综合运用
「反向传播里的梯度拆解路径」
TEMPOOCL 网络在 calcInputGradients 里把误差沿前向结构原路回传,先由 cRevIn 接收来自 cSum 的梯度,再把信号拆给趋势、季节与噪声三个分支。这里 cRevIn.calcHiddenGradients(cSum.AsObject()) 一旦返回 false,整条回传直接中断,说明梯度耦合非常紧,任一层 OpenCL 缓冲异常都会让训练失效。 预测分支的回传从 acForecast[1] 开始:若其激活函数非 None,必须走 DeActivation 把输出梯度做逆变换,否则后续 acForecast[0] 拿到的梯度是错的。注意力分支则经 cTransposeAtt、cAttention 一路回传到位置编码 acPE[0],其中 acPE[0] 的 calcHiddenGradients 还显式传入了 acPE[1] 的输出与梯度,做跨层激活补偿。 PLR 与拼接缓冲两路各自独立回传:cPLR 经 Normalize、Patching 三层,Concat 输入经 Normalize、Patching 到 cConcatInput。最后 DeConcat 用参数 (1,1,1,3*iSequence*iVariables) 把总梯度劈回趋势、残差、季节三块——3 倍序列变量数这个维度,直接对应前文三类分量拼接时的通道数,调 iSequence 或 iVariables 时这里必须同步,否则梯度形状不匹配会编译期没事、运行期爆缓冲。 外汇与贵金属行情用这类模型做辅助判断属高风险,梯度回传正确不代表样本外预测可靠,仅代表网络数学闭环没断。
if(!cRevIn.FeedForward(cTransposeFrc.AsObject())) class="kw">return class="kw">false; if(!cSum.FeedForward(cRevIn.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTEMPOOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return class="kw">false; class=class="str">"cmt">//--- Devide to Trend, Seasons and Noise if(!cRevIn.calcHiddenGradients(cSum.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Forecast gradient if(!cTransposeFrc.calcHiddenGradients(cRevIn.AsObject())) class="kw">return class="kw">false; if(!acForecast[class="num">1].calcHiddenGradients(cTransposeFrc.AsObject())) class="kw">return class="kw">false; if(acForecast[class="num">1].Activation() != None && !DeActivation(acForecast[class="num">1].getOutput(), acForecast[class="num">1].getGradient(), acForecast[class="num">1].getGradient(), acForecast[class="num">1].Activation()) ) class="kw">return class="kw">false; if(!acForecast[class="num">0].calcHiddenGradients(acForecast[class="num">1].AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Attention gradient if(!cTransposeAtt.calcHiddenGradients(acForecast[class="num">0].AsObject())) class="kw">return class="kw">false; if(!cAttention.calcHiddenGradients(cTransposeAtt.AsObject())) class="kw">return class="kw">false; if(!acPE[class="num">0].calcHiddenGradients(cAttention.AsObject(), acPE[class="num">1].getOutput(), acPE[class="num">1].getGradient(), (ENUM_ACTIVATION)acPE[class="num">1].Activation())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Gradient to PLR if(!cPatchingPLR.calcHiddenGradients(acPE[class="num">1].AsObject())) class="kw">return class="kw">false; if(!cNormalizePLR.calcHiddenGradients(cPatchingPLR.AsObject())) class="kw">return class="kw">false; if(!cPLR.calcHiddenGradients(cNormalizePLR.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Gradient to Concatenate buffer of Trend, Season and Noise if(!cPatching.calcHiddenGradients(acPE[class="num">0].AsObject())) class="kw">return class="kw">false; if(!cNormalize.calcHiddenGradients(cPatching.AsObject())) class="kw">return class="kw">false; if(!cConcatInput.calcHiddenGradients(cNormalize.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- DeConcatenate if(!DeConcat(cTrend.getGradient(), cOutputTimeSeriasRe.getGradient(), cResidual.getGradient(), cConcatInput.getGradient(), class="num">1, class="num">1, class="num">1, class="num">3 * iSequence * iVariables)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Seasons if(!CutOneFromAnotherGradient()) class="kw">return class="kw">false; if(!SumAndNormilize(cOutputTimeSeriasRe.getGradient(), cTranspose[class="num">1].getGradient(), cTranspose[class="num">1].getGradient(), class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false;
反向传播里的频域梯度链
这段反向传播代码把时间序列的梯度在频域和时域之间来回搬移,任何一步返回 false 都会中断整条回传。它先对输出时间序列做隐藏梯度计算,再把实部梯度拼接到零向量与输入频域实部上,拼接长度是 iFFT - iSequence,这一步直接决定残差频段如何回流。 SumAndNormilize 调用里出现 -0.5f 的缩放系数,对应虚部输出在逆变换前的幅值修正;若你改这个常数,MT5 上跑同一组 EURUSD 1H 样本时梯度范数会明显漂移。外汇与贵金属杠杆高,频域模型过拟合后实盘回撤可能放大,调参前先用历史数据验证。 随后代码走 FFT 逆变换(最后一个参数 false),把频域复梯度拆回时域并做 ComplexUnNormalizeGradient / Normalize 的对称操作。注意力权重 cFreqAtteention 的梯度通过 cNormFreqComplex.calcHiddenGradients 回流,说明频率注意力本身也参与训练。 末尾 CutTrendAndOtherGradient 单独切出趋势项梯度,再经 NeuronOCL.calcHiddenGradients 把输入季节性残差合并。整段没有显式学习率,归一化全靠 SumAndNormilize 的末尾参数控制,复制时别漏了最后一个整型开关。
if(!cOutputTimeSeriasRe.calcHiddenGradients(cTranspose[class="num">1].AsObject())) class="kw">return class="kw">false; if(!Concat(cOutputTimeSeriasRe.getGradient(), GetPointer(cZero), GetPointer(cInputFreqRe), iSequence, iFFT - iSequence, iVariables)) class="kw">return class="kw">false; if(!SumAndNormilize(GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm), class="num">1, class="kw">false, class="num">0, class="num">0, class="num">0, -class="num">0.5f)) class="kw">return class="kw">false; if(!FFT(GetPointer(cInputFreqRe), GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), class="kw">false)) class="kw">return class="kw">false; if(!Concat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), cUnNormFreqComplex.getGradient(), class="num">1, class="num">1, iFFT * iVariables)) class="kw">return class="kw">false; if(!ComplexUnNormalizeGradient()) class="kw">return class="kw">false; if(!cNormFreqComplex.calcHiddenGradients(cFreqAtteention.AsObject())) class="kw">return class="kw">false; if(!ComplexNormalizeGradient()) class="kw">return class="kw">false; if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), cInputFreqComplex.getGradient(), class="num">1, class="num">1, iFFT * iVariables)) class="kw">return class="kw">false; if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), true)) class="kw">return class="kw">false; if(!DeConcat(cTranspose[class="num">0].getGradient(), GetPointer(cInputFreqIm), GetPointer(cInputFreqRe), iSequence, iFFT - iSequence, iVariables)) class="kw">return class="kw">false; if(!cInputSeasons.calcHiddenGradients(cTranspose[class="num">0].AsObject())) class="kw">return class="kw">false; if(!SumAndNormilize(cInputSeasons.getGradient(), cResidual.getGradient(), cInputSeasons.getGradient(), class="num">1, class="num">1, class="kw">false, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; class=class="str">"cmt">//--- trend if(!CutTrendAndOtherGradient(NeuronOCL.getGradient())) class="kw">return class="kw">false; class=class="str">"cmt">//--- input gradient if(!NeuronOCL.calcHiddenGradients(cPLR.AsObject())) class="kw">return class="kw">false; if(!SumAndNormilize(NeuronOCL.getGradient(), cInputSeasons.getGradient(), NeuronOCL.getGradient(), class="num">1, class="kw">false, 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; }
◍ 下一篇再看实盘回测
这一节把前面三篇的脉络收了一下:核心方法叫 TEMPO,思路是用预训练语言模型去跑时间序列预测,顺带提出一种分解序列的新办法,目的是让原始数据 representation 的学习效率更高。 我们在 MQL5 里已经把这套设想落了地,代码层的工作量是实打实堆出来的,但文章篇幅卡死了,没法把全部实现细节都摊开。 真正用真实历史数据跑出来的模型表现,作者留到了下一篇讲。想验证这套分解方式到底有没有用,只能等下一节把回测结果贴出来再开 MT5 对照。
「随包附带的八个工程文件」
这套 LSTM 优化方案不是只给思路,而是把完整工具链都打进了 MQL5.zip(约 2.43 MB)里。压缩包内含 8 个文件:6 个 mq5 智能系统分别管样本收集、Real ORL 采集、模型训练、编码器训练、模型测试,另有 Trajectory.mqh 描述系统状态、NeuroNet.mqh 封装建网类、NeuroNet.cl 放 OpenCL 核函数。 想在 MT5 里跑通,先加载 Research.mq5 攒样本,再丢给 Study.mq5 训模型;显存吃紧就把 StudyEncoder.mq5 单独跑编码器。外汇与贵金属行情高波动、杠杆风险大,任何模型输出只作概率参考,实盘前务必用 Test.mq5 在自有样本上回测。 文件直接挂进 MetaEditor 就能编译,不用自己补依赖。下一步可改 NeuroNet.mqh 里的层宽参数,看验证集误差怎么变——这比空谈架构有用。