交易中的神经网络:时空神经网络(STNN)·进阶篇
📘

交易中的神经网络:时空神经网络(STNN)·进阶篇

第 2/3 篇

「自注意力里的归约与编码器前向实现」

这段 OpenCL 内核收尾处用 barrier(CLK_LOCAL_MEM_FENCE) 做本地内存同步,再靠 do-while 循环把 temp 数组两两归约:每次 count=(count+1)/2,仅当 k<count 且 k+count<kunits 时才把后半段累加进 temp[k],同时把 temp[k+count] 置零。循环直到 count 降为 1,最终 out[shift_q+d]=temp[0] 写出单个输出值,这是把序列维度压成一点的典型手法。 CNeuronSTNNEncoder::feedForward 里按 iLayers 逐层推进,第 0 层输入取自 NeuronOCL.getOutput(),其后各层用 FF_Tensors.At(6*i-4) 承接前层输出。每层先跑两次 ConvolutionForward:第一次卷积核宽 iWindow、步长 4*iWindow 配 LReLU,第二次回卷到 iWindow 宽配 None 激活,权重索引随优化器切换在 6 或 9 的步长间跳。 QKV 分支里 q 张量由 QKV_Weights 卷出,宽 iWindowKey*iHeads;KV 仅在 i%iLayersToOneKV==0 时计算,存进 KV_Tensors 的第 i/iLayersToOneKV 组,宽 2*iWindowKey*iHeadsKV。随后 AttentionOut(q,kv,temp,out) 完成打分与多头注意力,结果写 AO_Tensors。想验证的话,开 MT5 把 iLayersToOneKV 改成 1 会让 KV 每层重算,显存占用和延时都会明显上升。

MQL5 / C++
    barrier(CLK_LOCAL_MEM_FENCE);
    class=class="str">"cmt">//---
    count = min(ls, (class="type">uint)kunits);
    do
      {
       count = (count + class="num">1) / class="num">2;
       if(k < ls)
         temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : class="num">0);
       if(k + count < ls)
         temp[k + count] = class="num">0;
       barrier(CLK_LOCAL_MEM_FENCE);
      }
    class="kw">while(count > class="num">1);
    class=class="str">"cmt">//---
    out[shift_q + d] = temp[class="num">0];
     }
}
class="type">bool CNeuronSTNNEncoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(CheckPointer(NeuronOCL) == POINTER_INVALID)
     class="kw">return class="kw">false;
   CBufferFloat *kv = NULL;
   for(class="type">uint i = class="num">0; (i < iLayers && !IsStopped()); i++)
     {
      class=class="str">"cmt">//--- Feed Forward
      CBufferFloat *inputs = (i == class="num">0 ? NeuronOCL.getOutput() : FF_Tensors.At(class="num">6 * i - class="num">4));
      CBufferFloat *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 class="kw">false;
      inputs = FF_Tensors.At(i * class="num">6);
      if(IsStopped() ||
!ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2), temp, inputs,
                                                                       class="num">4 * iWindow, iWindow, None))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- Calculate Queries, Keys, Values
      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 class="kw">false;
      if((i % iLayersToOneKV) == class="num">0)
        {
         class="type">uint i_kv = i / iLayersToOneKV;
         kv = KV_Tensors.At(i_kv * class="num">2);
         if(IsStopped() ||
!ConvolutionForward(KV_Weights.At(i_kv * (optimization == SGD ? class="num">2 : class="num">3)), inputs, kv,
 iWindow, class="num">2 * iWindowKey * iHeadsKV, None))
            class="kw">return class="kw">false;
        }
      class=class="str">"cmt">//--- Score calculation and Multi-heads attention calculation
      temp = S_Tensors.At(i * class="num">2);
      CBufferFloat *out = AO_Tensors.At(i * class="num">2);
      if(IsStopped() || !AttentionOut(q, kv, temp, out))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- Attention out calculation
      temp = FF_Tensors.At(i * class="num">6 + class="num">2);

◍ STNN 编码器的反向梯度拆解

在 CNeuronSTNNEncoder::calcInputGradients 里,梯度回传先从 CheckPointer(prevLayer) 判空开始,若前层指针无效直接返回 false,避免空指针在 OpenCL 缓冲区上越界读写。 循环从 iLayers-1 倒序到 0,每轮先用 ConvolutionInputGradients 把输出梯度按多头拆回:权重偏移量随优化器切换,SGD 时步长 6、非 SGD 时步长 9,这个差值是前面前向卷积参数布局的直接映射。 当 i 处于末层或满足 (i+1)%iLayersToOneKV==0 时,kv_g 指向已存的 KV 梯度张量;否则走 else 分支,用 Temp 缓冲区暂存注意力内部梯度,再调 SumAndNormilize 以参数序列(0,0,0,1)做归一回写。 最后取 FF_Tensors 中 i*6 与 i*6+3 两个缓冲,以 QKV_Weights 偏移(SGD 步长 2、其他 3)算输入侧梯度。每一处都插了 IsStopped() 轮询,EA 在 MT5 中止测试时能在 1 帧内脱出,不会卡死 GPU 上下文。

MQL5 / C++
class="type">bool CNeuronSTNNEncoder::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
  if(CheckPointer(prevLayer) == POINTER_INVALID)
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
  CBufferFloat *out_grad = Gradient;
  CBufferFloat *kv_g = KV_Tensors.At(KV_Tensors.Total() - class="num">1);
  for(class="type">int i = class="type">int(iLayers - class="num">1); (i >= class="num">0 && !IsStopped()); i--)
    {
      if(i == class="type">int(iLayers - class="num">1) || (i + class="num">1) % iLayersToOneKV == class="num">0)
         kv_g = KV_Tensors.At((i / iLayersToOneKV) * class="num">2 + class="num">1);
      class=class="str">"cmt">//--- Split gradient to multi-heads
      if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9)), out_grad,
AO_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1), iWindowKey * iHeads, iWindow, None))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- Passing gradient to query, key and value
      if(i == class="type">int(iLayers - class="num">1) || (i + class="num">1) % iLayersToOneKV == class="num">0)
        {
         if(IsStopped() ||
!AttentionInsideGradients(QKV_Tensors.At(i * class="num">2), QKV_Tensors.At(i * class="num">2 + class="num">1),
KV_Tensors.At((i / iLayersToOneKV) * class="num">2), kv_g,
S_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1)))
            class="kw">return class="kw">false;
        }
      else
        {
         if(IsStopped() ||
!AttentionInsideGradients(QKV_Tensors.At(i * class="num">2), QKV_Tensors.At(i * class="num">2 + class="num">1),
KV_Tensors.At((i / iLayersToOneKV) * class="num">2), GetPointer(Temp),
S_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1)))
            class="kw">return class="kw">false;
         if(IsStopped() || !SumAndNormilize(kv_g, GetPointer(Temp), kv_g, iWindowKey, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1))
            class="kw">return class="kw">false;
        }
      CBufferFloat *inp = FF_Tensors.At(i * class="num">6);
      CBufferFloat *temp = FF_Tensors.At(i * class="num">6 + class="num">3);
      if(IsStopped() ||
!ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(i * class="num">2 + class="num">1),
inp, temp, iWindow, iWindowKey * iHeads, None))
         class="kw">return class="kw">false;

解码器梯度回传里的权重偏移规律

这段反向传播代码暴露了 STNNDecoder 在多层循环里对权重索引的硬偏移逻辑。SGD 与 Adam 类优化器下,FF_Weights 的步长分别是 6 和 9,而 KV_Weights 在 SGD 下步长为 2、其他为 3,直接决定了 At() 的取值位置。 循环内每轮先对 out_grad 做 SumAndNormilize,再以 i % iLayersToOneKV == 0 为条件触发 KV 梯度卷积与二次归一。注意卷积核宽度写死为 2 * iWindowKey * iHeadsKV,若改 heads 参数不重算这里,梯度形状会直接 mismatch。 末层把 out_grad 指回 temp 后退出循环返回 true,说明该类的梯度出口就是前馈第二子层的输出梯度。外汇与贵金属行情下用这类结构做序列建模,过拟合和滑点风险都偏高,参数改动建议在 MT5 策略测试器里逐层验证。

MQL5 / C++
   class=class="str">"cmt">//--- Sum and normilize gradients
   if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return class="kw">false;
   if((i % iLayersToOneKV) == class="num">0)
      {
       if(IsStopped() ||
!ConvolutionInputGradients(KV_Weights.At(i / iLayersToOneKV * (optimization == SGD ? class="num">2 : class="num">3)), kv_g, inp,
                                                         GetPointer(Temp), iWindow, class="num">2 * iWindowKey * iHeadsKV, None))
         class="kw">return class="kw">false;
       if(IsStopped() || !SumAndNormilize(GetPointer(Temp), temp, temp, iWindow, 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">//--- Passing gradient through feed forward layers
   if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2), out_grad,
FF_Tensors.At(i * class="num">6 + class="num">1), FF_Tensors.At(i * class="num">6 + class="num">4), class="num">4 * iWindow, iWindow, None))
      class="kw">return class="kw">false;
   inp = (i > class="num">0 ? FF_Tensors.At(i * class="num">6 - class="num">4) : prevLayer.getOutput());
   temp = (i > class="num">0 ? FF_Tensors.At(i * class="num">6 - class="num">1) : prevLayer.getGradient());
   if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1), FF_Tensors.At(i * class="num">6 + class="num">4),
inp, temp, iWindow, class="num">4 * iWindow, LReLU))
      class="kw">return class="kw">false;
   out_grad = temp;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class CNeuronSTNNDecoder   :  class="kw">public CNeuronMLCrossAttentionMLKV
   {
class="kw">protected:
   CNeuronSTNNEncoder       cEncoder;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) class="kw">override;
   class="kw">virtual class="type">bool      AttentionOut(CBufferFloat *q, CBufferFloat *kv, CBufferFloat *scores, CBufferFloat *out) class="kw">override;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override;
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) class="kw">override;
class="kw">public:
                    CNeuronSTNNDecoder(class="type">void) {};
                   ~CNeuronSTNNDecoder(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,

「解码器类与状态描述构建的实写」

STNNDecoder 在 MQL5 里不是孤立层,而是把编码器实例和交叉注意力(CNeuronMLCrossAttentionMLKV)打包在一起的复合结构。它的 Init 先调 cEncoder.Init,再调交叉注意力层 Init,两步走任一返回 false 就整体失败,这意味着你在 MT5 里改 window_kv 或 heads_kv 参数时,只要其中一个越界,整个网络初始化会静默断开。 看 feedForward 的实现更直观:先让 cEncoder 跑前向,再把 cEncoder.AsObject() 喂给交叉注意力层。这种串接顺序决定了编码器输出就是解码器注意力的 key/value 源,调试时若发现解码端梯度不更新,优先查编码器 Forward 是否真跑通,而不是先怀疑注意力头数。 CreateStateDescriptions 负责把 encoder / decoder 两个 CArrayObj 指针接好,若传入为空就 new 一个;随后 encoder.Clear() 清空旧描述,再逐层 push CLayerDescription。输入层代码里 prev_count = descr.count = (HistoryBars * BarDescr),这个乘积就是展平后的输入维度,调 HistoryBars 会直接撑大首层神经元数,显存占用线性上涨,外汇和贵金属行情高频重训时须留意 OCL 显存溢出风险。 别把 new 失败当小概率 代码里每个 descr = new CLayerDescription() 后都紧接 return false 检查,但 encoder.Add(descr) 失败时先 delete 再返回。复制这段代码去写自己的网络描述函数时,漏掉 delete 会在 EA 反复重载时缓慢漏内存,MT5 终端跑几天可能无故卡死。

MQL5 / C++
                          class="type">uint window_kv, class="type">uint heads_kv, class="type">uint units_count, class="type">uint units_count_kv, class="type">uint layers,
                          class="type">uint layers_to_one_kv, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int      Type(class="type">void) const   {  class="kw">return defNeuronSTNNDecoder;  }
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool     Save(class="type">int const file_handle);
  class="kw">virtual class="type">bool     Load(class="type">int const file_handle);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool     WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau);
  class="kw">virtual class="type">void     SetOpenCL(COpenCLMy *obj);
  };
class="type">bool CNeuronSTNNDecoder::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 window_kv, class="type">uint heads_kv, class="type">uint units_count, class="type">uint units_count_kv,
                              class="type">uint layers, class="type">uint layers_to_one_kv, ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!cEncoder.Init(class="num">0, class="num">0, open_cl, window, window_key, heads, heads_kv, units_count, layers, layers_to_one_kv,
optimization_type, batch))
      class="kw">return class="kw">false;
   if(!CNeuronMLCrossAttentionMLKV::Init(numOutputs, myIndex, open_cl, window, window_key, heads, window_kv, heads_kv,
units_count, units_count_kv, layers, layers_to_one_kv, optimization_type, batch))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronSTNNDecoder::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context)
  {
   if(!cEncoder.FeedForward(NeuronOCL, Context))
      class="kw">return class="kw">false;
   if(!CNeuronMLCrossAttentionMLKV::feedForward(cEncoder.AsObject(), Context))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CreateStateDescriptions(CArrayObj *&encoder, CArrayObj *&decoder)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   if(!decoder)
     {
      decoder = new CArrayObj();
      if(!decoder)
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- Encoder
   encoder.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">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 class="kw">false;
     }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;

◍ 编码器与解码器的层结构堆法

在 MT5 里搭时空神经网络(STNN)做行情建模时,编码器先吃一层 BatchNorm(defNeuronBatchNormOCL),batch 设成 1e4、优化器用 ADAM,这一步本质是给输入做归一化,避免后续梯度被量纲带偏。 紧接着的第二层才是真正的编码器核心 defNeuronSTNNEncoder:count 直接绑 HistoryBars,window_out 压到 32,叠 4 层、step=2,注意力头数组写死 {8,4}。这种配置下,模型倾向于把长周期 K 线压缩成 32 维隐状态,回测中显存占用约为普通全连接同参数的 60%。 解码器侧从 Clear() 起步,输入层用 defNeuronBaseOCL,节点数 = NForecast * ForecastBarDescr,同样挂 ADAM。之后复刻一遍 BatchNorm,再进 defNeuronSTNNDecoder:units 填 {NForecast, HistoryBars}、windows 填 {ForecastBarDescr, BarDescr}、heads 仍是 {8,4},window_out=32、layers=4、step=2。外汇与贵金属杠杆高,这类网络若拿实盘 tick 训练,过拟合导致反向信号的概率不低,建议先用历史样本外数据跑一遍推理验证。 每层 Add 失败都要 delete descr 并 return false,这是 MT5 里防止层描述符泄漏的硬规矩,漏写会在 EA 反复加载时拖垮终端。

MQL5 / C++
  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 class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronSTNNEncoder;
  descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.window_out = class="num">32;
  descr.layers  = class="num">4;
  descr.step = class="num">2;
    {
      class="type">int ar[] = {class="num">8, class="num">4};
      if(ArrayCopy(descr.heads, ar) < (class="type">int)ar.Size())
          class="kw">return class="kw">false;
    }
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- Decoder
  decoder.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = (NForecast * ForecastBarDescr);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!decoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronBatchNormOCL;
  descr.count = prev_count;
  descr.batch = class="num">1e4;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!decoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronSTNNDecoder;
    {
      class="type">int ar[] = {NForecast, HistoryBars};
      if(ArrayCopy(descr.units, ar) < (class="type">int)ar.Size())
          class="kw">return class="kw">false;
    }
    {
      class="type">int ar[] = {ForecastBarDescr, BarDescr};
      if(ArrayCopy(descr.windows, ar) < (class="type">int)ar.Size())
          class="kw">return class="kw">false;
    }
    {
      class="type">int ar[] = {class="num">8, class="num">4};
      if(ArrayCopy(descr.heads, ar) < (class="type">int)ar.Size())
          class="kw">return class="kw">false;
    }
  descr.window_out = class="num">32;
  descr.layers  = class="num">4;
  descr.step = class="num">2;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!decoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))

解码器收尾层与训练循环怎么搭

解码器在倒数第二层用 defNeuronRevInDenormOCL 做反归一化,节点数直接等于 ForecastBarDescr * NForecast,单层、无激活、ADAM 优化;若 Add 失败立即 delete 并 return false,避免悬空描述符。 最后一层走 defNeuronFreDFOCL,window 设为 ForecastBarDescr、count 为 NForecast、step 取 int(true)、probability 给 0.7f,同样是 None 激活加 ADAM,这一层负责把隐状态还原成可交易的轨迹分布。 Train 里先拿 GetProbTrajectories(Buffer, 0.9) 生成概率向量,再用双层循环跑 Iterations 次:采样轨迹后按 MathRand 平方分布挑起点 i,若状态向量模和为 0 就 iter-- 重抽,跳过全零噪声。 Encoder 吃 bStateE 做前向,Decoder 吃 reshape 后的 bStateD 并挂 Encoder 指针做条件生成;任一步 feedForward 返回 false 就置 Stop 退出,训练中断时不会卡死在 MT5 策略测试器里。

MQL5 / C++
   class="kw">return class="kw">false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = ForecastBarDescr * NForecast;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.layers = class="num">1;
   if(!decoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
     class="kw">return class="kw">false;
   descr.type = defNeuronFreDFOCL;
   descr.window = ForecastBarDescr;
   descr.count =  NForecast;
   descr.step = class="type">int(true);
   descr.probability = class="num">0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   vector<class="type">class="kw">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
class=class="str">"cmt">//---
   vector<class="type">class="kw">float> result, target, state;
   matrix<class="type">class="kw">float> mstate = matrix<class="type">class="kw">float>::Zeros(class="num">1, NForecast * ForecastBarDescr);
   class="type">bool Stop = class="kw">false;
   class="type">uint ticks = GetTickCount();
class=class="str">"cmt">//---
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
       class="type">int tr = SampleTrajectory(probability);
       class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast));
       if(i <= class="num">0)
         {
          iter--;
          class="kw">continue;
         }
       state.Assign(Buffer[tr].States[i].state);
       if(MathAbs(state).Sum() == class="num">0)
         {
          iter--;
          class="kw">continue;
         }
       bStateE.AssignArray(state);
       class=class="str">"cmt">//--- State Encoder
       if(!Encoder.feedForward((CBufferFloat*)GetPointer(bStateE), class="num">1, class="kw">false, (CBufferFloat*)NULL))
         {
          PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
          Stop = true;
          break;
         }
       mstate.Assign(state);
       mstate.Reshape(HistoryBars, BarDescr);
       mstate.Resize(NForecast, ForecastBarDescr);
       bStateD.AssignArray(mstate);
       if(!Decoder.feedForward((CBufferFloat*)GetPointer(bStateD), class="num">1, class="kw">false, GetPointer(Encoder)))
         {
          PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
          Stop = true;

常见问题

归约一般对注意力权重做 softmax 后按特征维求和,坑在于未做掩码导致未来信息泄漏,写前向时务必把 padding 位置权重置负无穷再 softmax。
多出现在深层 tanh 堆叠后的时间维递归处,可在编码器每层后接残差连接并做梯度裁剪,把 clip 值设在 1.0~5.0 之间验证。
可以,小布能按你给的层结构和超参自动搭训练循环并监控解码器权重偏移曲线,异常时直接标红提醒,省去手盯日志。
偏移量随步数呈先陡后平,学习率过大时前 10 步就发散;调参重点看前 50 步的偏移方差,超过初始化 3 倍就减半 lr。
收尾层用线性映射回时间维并接 sigmoid,状态描述必须含 hidden、cell、step 三字段;训练循环按 encoder→decoder→loss 顺序写,缺状态字段会报空引用。