交易中的神经网络:基于双注意力的趋势预测模型·进阶篇
🧠

交易中的神经网络:基于双注意力的趋势预测模型·进阶篇

(2/3)·单变量序列和单层网络为何总在拐点失灵?这篇拆开双特征提取与注意力机制的内部链路

进阶 第 2/3 篇
很多交易者把 LSTM 直接套在收盘价上就以为能抓趋势,结果在震荡转趋势的节点反复被打脸。问题往往不在参数,而在只喂了单变量、且没区分长周期与短周期特征。双特征加双注意力正是冲着这个盲区来的。

TPM 编码器的前向传播与 dropout 内核装配

在自定义 TPM 编码器里,feedForward 的第一步是特征提取:cFeatureExtraction.FeedForward 若返回 false 则直接退出,保证后续张量维度合法。之后把隐藏态与记忆态做 Concat,再依次过 cConcatenated、cSoftMax 两层前向,拿到 softmax 输出索引 map。 若 bTSinRow 为 false,还会对 softmax 输出做转置,此时 map 切换为 cTranspose 的输出索引。这个分支直接影响 dropout 内核读取哪块显存缓冲,调试时容易忽略。 global_work_size[0] 被设为 (cSoftMax.Neurons() + 3) / 4,也就是按 4 个神经元一组派发 OpenCL work-item。如果你改了 softmax 神经元数,这组除法要同步复核,否则可能漏算尾部神经元。 下面连续四次 SetArgumentBuffer / SetArgument 把 dropout 内核的输入、映射、输出缓冲和维度绑好,任何一步失败都 printf 出函数名、错误码和行号。最后 OpenCL.Execute 用一维偏移与尺寸启动内核,失败同样返回 false,之后才进 LSTM 前向。

MQL5 / C++
if(!cTemp.BufferInit(variables * lenth, class="num">0) || !cTemp.BufferCreate(OpenCL))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronTPMEncoder::feedForward(CNeuronBaseOCL *NeuronOCL)
   {
class=class="str">"cmt">//--- FEATURE EXTRACTION
   if(!cFeatureExtraction.FeedForward(NeuronOCL))
      class="kw">return false;
class=class="str">"cmt">//--- Memory and Hidden
   if(!Concat(m_iHiddenState, m_iMemory, m_iHiddenState, m_iMemory,
cMemAndHidden.getOutputIndex(), class="num">1, class="num">1, class="num">0, class="num">0, Neurons()))
      class="kw">return false;
   if(!cConcatenated.FeedForward(cFeatureExtraction.AsObject(), cMemAndHidden.getOutput()))
      class="kw">return false;
   if(!cSoftMax.FeedForward(cConcatenated.AsObject()))
      class="kw">return false;
   class="type">int map = cSoftMax.getOutputIndex();
   if(!bTSinRow)
     {
       if(!cTranspose.FeedForward(cSoftMax.AsObject()))
          class="kw">return false;
       map = cTranspose.getOutputIndex();
     }
   class="type">uint global_work_offset[class="num">1] = {class="num">0};
   class="type">uint global_work_size[class="num">1];
   global_work_size[class="num">0] = class="type">int(cSoftMax.Neurons() + class="num">3) / class="num">4;
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_input, cFeatureExtraction.getOutputIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_map, map))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_out, cAttentionOut.getOutputIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.SetArgument(def_k_Dropout, def_k_dout_dimension, cSoftMax.Neurons()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.Execute(def_k_Dropout, class="num">1, global_work_offset, global_work_size))
     {
       printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
       class="kw">return false;
     }
class=class="str">"cmt">//--- LSTM
   if(!CNeuronLSTMOCL::feedForward(cAttentionOut.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronTPMEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
   {
   if(!NeuronOCL)
      class="kw">return false;

◍ 反向传播里卷积梯度的 OpenCL 绑定

LSTM 输出梯度先交给 CNeuronLSTMOCL::calcInputGradients 算完,任何一步返回 false 就直接中断反向传播,说明这一层对上游梯度依赖是硬性的。 下面这段把卷积隐藏层梯度核 def_k_CGConv_HiddenGradient 的参数逐个塞进 OpenCL buffer。global_work_size[0] 直接取 cSoftMax.Neurons() 的数量,意味着 GPU 上每个 softmax 神经元对应一个工作项,规模随输出维度线性扩张。 SetArgumentBuffer 连续绑定了特征提取输出、临时矩阵、转置或 softmax 的输出与梯度、以及注意力输出梯度,任意一次绑定失败都用 printf 打出函数名、错误码和行号后退出。激活函数参数用 NeuronOCL.Activation() 传入,后接一个 int(None) 的占位激活,Execute 以 1 维偏移启动。 执行完后若 bTSinRow 为真,才让 cSoftMax 向 cTranspose 算隐藏梯度;否则跳过。最后 cConcatenated 拿 softmax 对象做梯度回传,NULL 占位表示不传额外浮点缓冲。外汇与贵金属模型训练涉及杠杆与滑点,GPU 加速只解决算力,不消除过拟合风险。 别把绑定顺序当随便写的 MT5 里 OpenCL 核参数位置必须和 kernel 源码声明一一对应,调交换 def_k_cgc_matrix_s 与 def_k_cgc_matrix_sg 的绑定顺序,回测可能不报错但梯度全错,肉眼难查。

MQL5 / C++
if(!CNeuronLSTMOCL::calcInputGradients(cAttentionOut.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//---
  class="type">uint global_work_offset[class="num">1] = {class="num">0};
  class="type">uint global_work_size[class="num">1];
  global_work_size[class="num">0] = cSoftMax.Neurons();
  ResetLastError();
  if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_f, cFeatureExtraction.getOutputIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_fg, cTemp.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_s,
(bTSinRow ? cSoftMax.getOutputIndex() : cTranspose.getOutputIndex())))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_sg,
(bTSinRow ? cSoftMax.getGradientIndex() : cTranspose.getGradientIndex())))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_g, cAttentionOut.getGradientIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgument(def_k_CGConv_HiddenGradient, def_k_cgc_activationf, NeuronOCL.Activation()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgument(def_k_CGConv_HiddenGradient, def_k_cgc_activations, class="type">int(None)))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.Execute(def_k_CGConv_HiddenGradient, class="num">1, global_work_offset, global_work_size))
     {
       printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
       class="kw">return false;
     }
  if(bTSinRow)
     {
       if(!cSoftMax.calcHiddenGradients(cTranspose.AsObject()))
         class="kw">return false;
     }
  if(!cConcatenated.calcHiddenGradients((CObject*)cSoftMax.AsObject(),(CBufferFloat *)NULL,(CBufferFloat *)NULL) ||

「TPM 编码器的梯度回传与权重更新实现」

下面这段 CNeuronTPMEncoder 的反向传播收尾,先对拼接层做反激活并回传梯度,再让特征提取层算隐藏梯度,最后用临时缓冲做反激活与求和归一化,任何一步失败直接返回 false。 反向流程里 DeActivation 与 SumAndNormilize 的配合是关键:前者把输出层激活导数剥离,后者以系数 1 且不对梯度取反(false 参数)完成累加归一,避免 LSTM 门控梯度被重复缩放。 权重更新阶段分三条链路:注意力输出层、特征提取层、以及拼接层(依赖特征提取对象与记忆隐藏输出),三层权重独立更新但顺序不可颠倒,否则前层梯度引用会指向未同步状态。 CNeuronTPM 类在 LSTM 基类上挂了编码器、PLR 特征提取、SoftMax 等 8 个成员,Init 接口要求同时传入 variables、lenth、hidden_size 与 batch,在 MT5 里调参时若 hidden_size 与特征提取层维度不匹配,feedForward 会在运行时报 CL 缓冲越界。

MQL5 / C++
if(!DeActivation(cConcatenated.getOutput(), cConcatenated.getGradient(), cConcatenated.getGradient(),
cConcatenated.Activation()))
   class="kw">return false;
 if(!cFeatureExtraction.calcHiddenGradients(cConcatenated.AsObject(), cMemAndHidden.getOutput(),
 cMemAndHidden.getGradient()))
   class="kw">return false;
 if(!DeActivation(cFeatureExtraction.getOutput(), GetPointer(cTemp), GetPointer(cTemp), NeuronOCL.Activation()) ||
   !SumAndNormilize(cFeatureExtraction.getGradient(), GetPointer(cTemp), cFeatureExtraction.getGradient(), class="num">1, false))
   class="kw">return false;
 if(!NeuronOCL.calcHiddenGradients(cFeatureExtraction.AsObject()))
   class="kw">return false;
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CNeuronTPMEncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!CNeuronLSTMOCL::updateInputWeights(cAttentionOut.AsObject()))
    class="kw">return false;
  if(!cFeatureExtraction.UpdateInputWeights(NeuronOCL))
    class="kw">return false;
  if(!cConcatenated.UpdateInputWeights(cFeatureExtraction.AsObject(), cMemAndHidden.getOutput()))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class CNeuronTPM :  class="kw">public CNeuronLSTMOCL
  {
class="kw">protected:
  CNeuronTPMEncoder     cEncoder;
  CNeuronPLROCL        cFeatureExtraction;
  CNeuronBaseOCL       cMemAndHidden;
  CNeuronConcatenate   cConcatenated;
  CNeuronSoftMaxOCL    cSoftMax;
  CNeuronBaseOCL       cAttentionOut;
  CNeuronConcatenate   cAttAndFeature;
  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         updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool         calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                     CNeuronTPM(class="type">void){};
                    ~CNeuronTPM(class="type">void){};
  class="kw">virtual class="type">bool       Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
 class="type">uint variables, class="type">uint lenth, class="type">uint hidden_size, class="type">bool ts_in_row,
 ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool       Save(class="type">int const file_handle) class="kw">override;
  class="kw">virtual class="type">bool       Load(class="type">int const file_handle) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int        Type(class="type">void) class="kw">override const { class="kw">return defNeuronTPM; }
  class="kw">virtual class="type">void       SetOpenCL(COpenCLMy *obj);
  };

TPM 神经元的初始化与前向传播骨架

在 MT5 的 OpenCL 神经网络封装里,CNeuronTPM 的 Init 把编码器、特征提取、记忆拼接和注意力头一次性挂到同一计算图上。注意 cEncoder 与 cFeatureExtraction 的 ts_in_row 参数是互斥传入的:前者用原始值,后者取反,这决定了时序变量是按行还是按列送进 LSTM。 初始化里 cConcatenated 被强制设为 TANH 激活,而 cSoftMax 显式 SetHeads(1),说明该模块默认只跑单头注意力,hidden_size 直接等于 softmax 的输入维度。若你改多头,这里必须同步调 cAttentionOut 与 cAttAndFeature 的通道数,否则 FeedForward 会静默返回 false。 feedForward 里先跑 Encoder 和 FeatureExtraction,再用 Concat 把隐藏态与记忆态拼成 hidden_size*2 的缓冲交给 cMemAndHidden。注意力部分由 cConcatenated 接收编码器输出与记忆拼接体,经 cSoftMax 后由 OpenCL 内核做 dropout 掩码——global_work_size[0] 取 (Neurons()+3)/4,意味着内核按 4 通道向量化打包,显存对齐不对会触发 SetArgumentBuffer 报错并打印行号。 外汇与贵金属行情下用这类 GPU 网络做推理,参数错配可能导致信号延迟或异常,属高风险用法,建议先在策略测试器以 tick 级数据跑通 Init 返回值再上实盘。

MQL5 / C++
class="type">bool CNeuronTPM::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                      class="type">uint variables, class="type">uint lenth, class="type">uint hidden_size, class="type">bool ts_in_row,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronLSTMOCL::Init(numOutputs, myIndex, open_cl, hidden_size, optimization_type, batch))
      class="kw">return false;
  if(!SetInputs(hidden_size))
      class="kw">return false;
  if(!cEncoder.Init(class="num">0, class="num">0, OpenCL, variables, lenth, hidden_size, ts_in_row, optimization, iBatch))
      class="kw">return false;
  if(!cFeatureExtraction.Init(class="num">0, class="num">1, OpenCL, variables, lenth, !ts_in_row, optimization, iBatch))
      class="kw">return false;
  if(!cMemAndHidden.Init(class="num">0, class="num">2, OpenCL, hidden_size * class="num">2, optimization, iBatch))
      class="kw">return false;
  if(!cConcatenated.Init(class="num">0, class="num">3, OpenCL, hidden_size, hidden_size, hidden_size * class="num">2, optimization, iBatch))
      class="kw">return false;
  cConcatenated.SetActivationFunction(TANH);
  if(!cSoftMax.Init(class="num">0, class="num">4, OpenCL, hidden_size, optimization, iBatch))
      class="kw">return false;
  cSoftMax.SetHeads(class="num">1);
  if(!cAttentionOut.Init(class="num">0, class="num">5, OpenCL, hidden_size, optimization, iBatch))
      class="kw">return false;
  if(!cAttAndFeature.Init(class="num">0, class="num">6, OpenCL, hidden_size, hidden_size, variables * lenth, optimization, iBatch))
      class="kw">return false;
  if(!cTemp.BufferInit(variables * lenth, class="num">0) || !cTemp.BufferCreate(OpenCL))
      class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronTPM::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
class=class="str">"cmt">//--- Encoder
  if(!cEncoder.FeedForward(NeuronOCL))
      class="kw">return false;
class=class="str">"cmt">//--- FEATURE EXTRACTION
  if(!cFeatureExtraction.FeedForward(NeuronOCL))
      class="kw">return false;
class=class="str">"cmt">//--- Memory and Hidden
  if(!Concat(m_iHiddenState, m_iMemory, m_iHiddenState, m_iMemory,
cMemAndHidden.getOutputIndex(), class="num">1, class="num">1, class="num">0, class="num">0, Neurons()))
      class="kw">return false;
class=class="str">"cmt">//--- Attention
  if(!cConcatenated.FeedForward(cEncoder.AsObject(), cMemAndHidden.getOutput()))
      class="kw">return false;
  if(!cSoftMax.FeedForward(cConcatenated.AsObject()))
      class="kw">return false;
  class="type">uint global_work_offset[class="num">1] = {class="num">0};
  class="type">uint global_work_size[class="num">1];
  global_work_size[class="num">0] = class="type">int(cSoftMax.Neurons() + class="num">3) / class="num">4;
  ResetLastError();
  if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_input, cEncoder.getOutputIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return false;
   }

◍ 编码器前向链路与层描述落地

把 dropout 核的参数绑完之后,真正跑前向时先调 Execute 并给 global_work_size 传 1 维数组,任何一步返回 false 都会在终端打印含 __LINE__ 的错误行号,方便在 MT5 策略测试器里直接定位 OpenCL 内核哪一行没绑上。 注意力输出先喂给 cAttAndFeature.FeedForward,再走 CNeuronLSTMOCL::feedForward;这两行若返回 false 则整个 encode 提前退出。外汇与贵金属行情下用这套 GPU 链路做特征提取,延迟可能随显存占用波动,属高风险实验性做法。 CreateEncoderDescriptions 里输入层节点数等于 HistoryBars * BarDescr,优化器统一用 ADAM;第二层 BatchNorm 的 batch 写死 1e4,意味着训练时按一万样本估算均值方差,小样本回测可能偏估。 第三层用了 defNeuronTPM,window 取 BarDescr、window_out 取 HistoryBars、step 强转 int(false) 即 0,相当于不滑动窗口铺满历史条数。改 LatentCount 或 HistoryBars 后必须重跑该函数重建描述,否则旧编码器维度对不上会直接 false。

MQL5 / C++
  if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_map, cSoftMax.getOutputIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_out, cAttentionOut.getOutputIndex()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.SetArgument(def_k_Dropout, def_k_dout_dimension, cSoftMax.Neurons()))
    {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
    }
  if(!OpenCL.Execute(def_k_Dropout, class="num">1, global_work_offset, global_work_size))
    {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      class="kw">return false;
    }
class=class="str">"cmt">//--- Attention and Features
  if(!cAttAndFeature.FeedForward(cAttentionOut.AsObject(), cFeatureExtraction.getOutput()))
    class="kw">return false;
class=class="str">"cmt">//--- LSTM
  if(!CNeuronLSTMOCL::feedForward(cAttAndFeature.AsObject()))
    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">//--- Encoder
  encoder.Clear();
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 = defNeuronTPM;
  descr.count = LatentCount;
  descr.window = BarDescr;
  descr.window_out = HistoryBars;
  descr.step = class="type">int(false);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
把特征工程交给小布盯盘跑批
这些分段回归与 CNN 特征提取的预处理,小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同时间框架的特征切片,你只需判断注意力权重集中的区间是否和自己的价格行为信号吻合。

常见问题

一类是分段线性回归切出的长期时态子序列特征,一类是 CNN 从独立时间点抽出的短期空间市场特征,两者并行输入后续注意力层。
编码器阶段注意力筛选最相关的短期行情特征,解码器阶段再把筛选结果与长期特征融合,倾向提升拐点处方向判断的概率。
价格受利率、情绪等多因素非线性驱动,单变量缺乏上下文,模型难以区分噪声与结构性变化,预测稳定性可能明显下降。
目前小布提供的是特征切片与权重可视,不包含用户自定义训练,但可作为你验证模型假设的辅助盘口视图,外汇贵金属仍属高风险。
LSTM 模块构成编码器与解码器主干,负责时序状态传递,注意力机制叠加其上做自适应特征选择,而非替代递归结构。