神经网络在交易中的应用:用于多元时间序列预测的 LSTM 优化(DA-CG-LSTM)·进阶篇
📘

神经网络在交易中的应用:用于多元时间序列预测的 LSTM 优化(DA-CG-LSTM)·进阶篇

第 2/2 篇

◍ 反向传播里怎么拆 LSTM 四个门

在 OpenCL 内核 CSLSTM_CalcHiddenGradient 中,梯度计算的前提是先按前向逻辑重建各门值。concat.s0~s3 分别对应遗忘、输入、新内容、输出四个预激活向量,用 SIGMOID 和 TANH 还原出 fg、ig、nc、og 及当前记忆 mem。 重建出 mem 后,用 (mem - ig * nc) / fg 反推出上一时刻记忆 prev_mem,这里用 IsNaNOrInf 兜底除零,避免 GPU 上出现 NaN 污染整批权重。 梯度链式法则逐门展开:out_g 来自上层 grad_output,og_g 由 out_g * mem_t 经 SIGMOID 反激活得到;mem_g 由 out_g * og 经 TANH 反激活得到。nc_g 和 ig_g 都挂在 mem_g 上,分别乘 ig 和 nc 后再反激活。 遗忘门的处理最绕:fg 不是直接 sigmoid,而是 1 - tanh(1 - 1/σ(z)^2)。所以对 fg_g 求 ∂,要先算 fg_s_g = 2 / pow(fg_s,3) * Deactivation(-fg_g, fg, TANH),再经 SIGMOID 反激活得到最终 fg_g。最后把四个梯度打包进 float4 写回 grad_concat[shift],供卷积投影层继续回传。 这段代码对外汇或贵金属行情序列建模有参考价值,但 GPU 上跑自定义 LSTM 内核属于高风险实验,参数扰动可能引发梯度爆炸,建议先在 MT5 策略测试器用小样本验证数值稳定性。

MQL5 / C++
class="type">class="kw">float og = Activation(concat.s3, ActFunc_SIGMOID);
class="type">class="kw">float mem = IsNaNOrInf(memory[shift] * fg + ig * nc, class="num">0);
class="type">class="kw">float out = IsNaNOrInf(og * Activation(mem, ActFunc_TANH), class="num">0);
memory[shift] = mem;
output[shift] = out;
}
__kernel class="type">void CSLSTM_CalcHiddenGradient(__global class="kw">const float4* __attribute__((aligned(class="num">16))) concatenated,
                                        __global float4* __attribute__((aligned(class="num">16))) grad_concat,
                                        __global class="kw">const class="type">class="kw">float* memory,
                                        __global class="kw">const class="type">class="kw">float* grad_output
                                       )
  {
   class="type">uint id = get_global_id(class="num">0);
   class="type">uint total = get_global_size(class="num">0);
   class="type">uint idv = get_global_id(class="num">1);
   class="type">uint shift = id + total * idv;
   float4 concat = concatenated[shift];                    class=class="str">"cmt">// Pre-activation values for all class="num">4 gates
class=class="str">"cmt">// --- Forward reconstruction of gates ---
   class="type">class="kw">float fg_s = Activation(concat.s0, ActFunc_SIGMOID);
   class="type">class="kw">float fg = class="num">1.0f - Activation(class="num">1.0f - class="num">1.0f / pow(fg_s, class="num">2), ActFunc_TANH);   class=class="str">"cmt">// Forget gate(ft)
   class="type">class="kw">float ig_s = Activation(concat.s1, ActFunc_SIGMOID);
   class="type">class="kw">float ig = Activation(ig_s, ActFunc_TANH);               class=class="str">"cmt">// Input gate(it)
   class="type">class="kw">float nc = Activation(concat.s2, ActFunc_TANH);          class=class="str">"cmt">// New content(ct~)
   class="type">class="kw">float og = Activation(concat.s3, ActFunc_SIGMOID);       class=class="str">"cmt">// Output gate(ot)
   class="type">class="kw">float mem = memory[shift];                               class=class="str">"cmt">// New memory state(ct)
   class="type">class="kw">float mem_t = Activation(mem, ActFunc_TANH);             class=class="str">"cmt">// tanh(ct)
class=class="str">"cmt">// --- Reconstruct previous memory state(t-class="num">1) ---
   class="type">class="kw">float prev_mem = IsNaNOrInf((mem - ig * nc) / fg, class="num">0);
class=class="str">"cmt">// --- Gradients computation ---
   class="type">class="kw">float out_g = grad_output[shift];
class="type">class="kw">float og_g = Deactivation(out_g * mem_t, og, ActFunc_SIGMOID);
class="type">class="kw">float mem_g = Deactivation(out_g * og, mem_t, ActFunc_TANH);
class="type">class="kw">float nc_g = Deactivation(mem_g * ig, nc, ActFunc_TANH);
class="type">class="kw">float ig_g = Deactivation(Deactivation(mem_g * nc, ig, ActFunc_TANH), ig_s, ActFunc_SIGMOID);
class=class="str">"cmt">// ∂L/∂fg = ∂L/∂ct * mem_(t-class="num">1)
   class="type">class="kw">float fg_g = mem_g * prev_mem;
class=class="str">"cmt">// Derivative of the complex forget gate:
class=class="str">"cmt">// f(z) = class="num">1 - tanh(class="num">1 - class="num">1 / σ(z)^class="num">2)
   class="type">class="kw">float fg_s_g = class="num">2 / pow(fg_s, class="num">3) * Deactivation(-fg_g, fg, ActFunc_TANH);
   fg_g = Deactivation(fg_s_g, fg_s, ActFunc_SIGMOID);
class=class="str">"cmt">// --- Write back gradients ---
   grad_concat[shift] = (float4)(fg_g, ig_g, nc_g, og_g);
  }
class CNeuronCGLSTMOCL : class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   CNeuronBaseOCL     cConcatenateInputs;
   CNeuronConvOCL     cProjection;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool       CSLSTM_feedForward(class="type">void);

「CGLSTM 神经元的接口与初始化链路」

在 MT5 的 OpenCL 神经网络框架里,CNeuronCGLSTMOCL 类把卷积投影和 LSTM 门控拼到同一计算图。它重写了 feedForward、updateInputWeights、calcInputGradients 等虚函数,说明前向传播和反向传播都走定制内核,而不是标准全连接那套。 看 Init 的重载实现,先以 count*variables 调基类 CNeuronBaseOCL::Init,再把激活函数设为 None——卷积与投影层都不在单层内做非线性变换,留给学生后续门控处理。cConcatenateInputs 的输入维度是 (count+window)*variables,cProjection 则以 count+window 为卷积宽、输出 count*4 通道,对应 LSTM 四个门。 这种结构在 EURUSD 的 M15 回测中,若 window=12、count=24、variables=5,参数量约为 (12+24)*5*24*4 ≈ 276480 个浮点权重,显存占用随 batch 线性增长。外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据验证过拟合倾向。 直接把下面代码贴进 MetaEditor 的 .mqh 派生类,能跑通说明你的 OpenCL 环境已就绪;跑不通大概率是 count 与 variables 传参顺序反了。

MQL5 / C++
class="kw">virtual class="type">bool      CSLSTM_CalcHiddenGradient(class="type">void);
class=class="str">"cmt">//---
class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                  CNeuronCGLSTMOCL(class="type">void) {};
                 ~CNeuronCGLSTMOCL(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 numNeurons,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch) class="kw">override;
class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
class="type">uint count, class="type">uint window, class="type">uint variables,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
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">int       Type(class="type">void) class="kw">override class="kw">const   { class="kw">return defNeuronCGLSTMOCL; }
class="kw">virtual class="type">bool      Clear(class="type">void) class="kw">override;
class="kw">virtual CBufferFloat *getLSTMWeights(class="type">void) { class="kw">return cProjection.GetWeightsConv(); }
class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj) class="kw">override;
class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
};
class="type">bool CNeuronCGLSTMOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                            class="type">uint count, class="type">uint window, class="type">uint variables,
                            ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, count * variables, optimization_type, batch))
      class="kw">return class="kw">false;
   SetActivationFunction(None);
   if(!cConcatenateInputs.Init(class="num">0, class="num">0, OpenCL, (count + window) * variables, optimization, iBatch))
      class="kw">return class="kw">false;
   cConcatenateInputs.SetActivationFunction(None);
   if(!cProjection.Init(class="num">0, class="num">1, OpenCL, count + window, count + window, count * class="num">4, class="num">1, variables, optimization, iBatch))
      class="kw">return class="kw">false;
   cProjection.SetActivationFunction(None);
   if(!Clear())
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronCGLSTMOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)

CGLSTM 前向传播与梯度回传的拆层逻辑

这段 CNeuronCGLSTMOCL 的实现把隐藏单元数、输入窗口和变量数在每次调用时现算:hidden 直接取投影滤波数的四分之一,inputs 用窗口长减掉 hidden,variables 来自投影对象的变量数。这种写法意味着改 GetFilters 或 GetWindow 的返回值,会立刻牵动 Concat 与 DeConcat 的维度配对。 前向部分先拼输入再做投影前推,最后交给 CSLSTM_feedForward 收口;任何一步返回 false 就整体中断,不抛异常只走布尔短路。 反向的 calcInputGradients 里,先算隐藏梯度再拆回输入梯度,若上游 NeuronOCL 带了非 None 激活,还要过一遍 DeActivation 把梯度按激活函数反推。外汇与贵金属行情序列用这类结构时波动跳变频繁,梯度数值可能瞬间发散,上 MT5 跑前建议先小窗口回测。 让小布替你跑这套:把 GetFilters 改成 8 的倍数以外的值,看 Concat 是否报维度错,比读文档更快定位耦合点。

MQL5 / C++
   class="kw">return class="kw">false;
class="type">int hidden = (class="type">int)cProjection.GetFilters() / class="num">4;
class="type">int inputs = (class="type">int)cProjection.GetWindow() - hidden;
class="type">int variables = (class="type">int)cProjection.GetVariables();
if(!Concat(NeuronOCL.getOutput(), getOutput(), cConcatenateInputs.getOutput(), inputs, hidden, variables))
   class="kw">return class="kw">false;
if(!cProjection.FeedForward(cConcatenateInputs.AsObject()))
   class="kw">return class="kw">false;
class="kw">return CSLSTM_feedForward();
}
class="type">bool CNeuronCGLSTMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   class="kw">return cProjection.UpdateInputWeights(cConcatenateInputs.AsObject());
  }
class="type">bool CNeuronCGLSTMOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
       class="kw">return class="kw">false;
class="type">int hidden = (class="type">int)cProjection.GetFilters() / class="num">4;
class="type">int inputs = (class="type">int)cProjection.GetWindow() - hidden;
class="type">int variables = (class="type">int)cProjection.GetVariables();
if(!CSLSTM_CalcHiddenGradient())
   class="kw">return class="kw">false;
if(!cConcatenateInputs.calcHiddenGradients(cProjection.AsObject()))
   class="kw">return class="kw">false;
if(!DeConcat(NeuronOCL.getGradient(), getPrevOutput(), cConcatenateInputs.getGradient(),
inputs, hidden, variables))
   class="kw">return class="kw">false;
  if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(),
NeuronOCL.Activation()))
         class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }

◍ 未完的架构与下一步落点

DA-CG-LSTM 这套框架和传统 LSTM 最大的差异,在于把 CG-LSTM 与双重注意力机制叠在一起,长期依赖和短期波动能同时被抓出来,而不是像旧模型那样顾一头丢一头。我们在 MQL5 里已经把 CG-LSTM 模块跑通,但整套训练到实盘测试的链路还没收口。 目前随文附的 MQL5.zip(约 2.69 MB)里,Research.mq5 到 Test.mq5 共 5 个 EA 分别管采样、离线/在线训练和测试,Trajectory.mqh 与 NeuroNet.mqh 两个类库定义了状态结构和网络创建逻辑,NeuroNet.cl 则是 OpenCL 端的算子代码。想复现的,先把这些文件按 Experts\DACGLSTM\ 和 Experts\NeuroNet_DNG\ 路径摆好。 作者明确说下一篇才给可训练模型的具体参数(输入维度、神经元数、优化器),所以现在直接拿去训容易卡在架构描述层。外汇与贵金属行情受杠杆和跳空影响,这类实验性模型仅适合在策略测试器里验证,实盘前务必自担高风险。 真要接着啃,建议先在 MT5 用 Study.mq5 跑一轮离线训练,观察 loss 曲线是否随 CG 门控收敛得比标准 LSTM 快——这一步不需要等下篇,现在就能动手。

常见问题

先把细胞状态 Ct 的误差沿时间步回传,再分别乘遗忘门 sigmoid 导数和输出门 tanh 导数拆开,各门梯度独立更新权重。
权重用正交或 Glorot 初始化,偏置清零,遗忘门偏置可设 1~2 降低早期遗忘,能缓解梯度消失。
小布可加载你的时间序列样本,按拆层逻辑跑前向并回传梯度,直接标出数值异常层,省去手算核对。
各序列先统一采样频率和缺失填充,再按时间窗口拼成三维输入,维度不一致会导致梯度回传错位。
容易把候选值 tanh 与输入门乘积误加到隐藏态而非细胞态,应严格按 Ct = ft*Ct-1 + it*gt 更新后再算 ht。