神经网络变得轻松(第二十二部分):递归模型的无监督学习·进阶篇
📘

神经网络变得轻松(第二十二部分):递归模型的无监督学习·进阶篇

第 2/3 篇

LSTM 核里的门控与权重初始化

这段 OpenCL 内核把 LSTM 单元的前向计算搬到了 GPU 上。当 id2 小于 3 时,拼接向量走 sigmoid 激活(1.0f/(1.0f+exp(sum))),否则走 tanh;三个门(遗忘、输入、输出)加一个候选状态 nc,在 id2==0 的线程里完成 memory 更新:mem = mem*fg + ig*nc,输出则是 og*tanh(mem)。 权重缓冲的预留大小由 (m_iInputs + Neurons() + 1) * Neurons() 决定,初始化时按 k = 1/sqrt(Neurons()+1) 做均匀缩放,再乘 WeightsMultiplier。这种缩放能让外汇或贵金属行情序列在 LSTM 训练初期梯度不过早饱和,但高频噪声下仍可能发散,属高风险建模。 feedForward 入口先校验上游神经元数与 OpenCL 上下文,m_iInputs 为 0 才懒加载权重;若 Reserve 或 BufferCreate 失败直接返回 false。想验证可把 def_k_LSTM_FeedForward 设成 32,在 MT5 策略测试器里跑一小段 XAUUSD 的 M5 序列看显存占用。

MQL5 / C++
 sum += dot((float4)(inputs[i], inputs[i + class="num">1], inputs[i + class="num">2], inputs[i + class="num">3]),
              (float4)(weights[shift + i], weights[shift + i + class="num">1], weights[shift + i + class="num">2], weights[shift + i + class="num">3]));
   else
      for(class="type">uint k = i; k < total; k++)
         sum += inputs[k] + weights[shift + k];
   }
   sum += weights[shift + inputs_size];
   if(id2 < class="num">3)
      concatenated[id2 * total + id] = class="num">1.0f / (class="num">1.0f + exp(sum));
   else
      concatenated[id2 * total + id] = tanh(sum);
class=class="str">"cmt">//---
   barrier(CLK_LOCAL_MEM_FENCE);
   if(id2 == class="num">0)
   {
      class="type">class="kw">float mem = memory[id + total] = memory[id];
      class="type">class="kw">float fg = concatenated[id];
      class="type">class="kw">float ig = concatenated[id + total];
      class="type">class="kw">float og = concatenated[id + class="num">2 * total];
      class="type">class="kw">float nc = concatenated[id + class="num">3 * total];
      class=class="str">"cmt">//---
      memory[id] = mem = mem * fg + ig * nc;
      output[id] = og * tanh(mem);
   }
class=class="str">"cmt">//---
}
class="macro">#define def_k_LSTM_FeedForward          class="num">32
class="macro">#define def_k_lstmff_inputs             class="num">0
class="macro">#define def_k_lstmff_inputs_size        class="num">1
class="macro">#define def_k_lstmff_weights            class="num">2
class="macro">#define def_k_lstmff_concatenated       class="num">3
class="macro">#define def_k_lstmff_memory             class="num">4
class="macro">#define def_k_lstmff_outputs            class="num">5
class="type">bool CNeuronLSTMOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || NeuronOCL.Neurons() <= class="num">0 ||
      NeuronOCL.getOutputIndex() < class="num">0 || !OpenCL)
      class="kw">return class="kw">false;
   if(m_iInputs <= class="num">0)
     {
      m_iInputs = NeuronOCL.Neurons();
      class="type">int count = (class="type">int)((m_iInputs + Neurons() + class="num">1) * Neurons());
      if(!m_cWeightsLSTM.Reserve(count))
         class="kw">return class="kw">false;
      class="type">class="kw">float k = (class="type">class="kw">float)(class="num">1 / sqrt(Neurons() + class="num">1));
      for(class="type">int i = class="num">0; i < count; i++)
        {
         if(!m_cWeightsLSTM.Add((class="num">2 * GenerateWeight()*k - k)*WeightsMultiplier))
            class="kw">return class="kw">false;
        }
      if(!m_cWeightsLSTM.BufferCreate(OpenCL))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//---
      if(!m_cFirstMomentumLSTM.BufferInit(count, class="num">0))
         class="kw">return class="kw">false;

◍ LSTM 前向传播与梯度内核的 OpenCL 绑定细节

这段实现把双层动量 LSTM 的缓冲区和 OpenCL 内核参数逐一绑定,任何一步失败就直接返回 false,保证 GPU 侧计算图不完整时不会继续跑前向。注意第二层动量 LSTM 先用 BufferInit(count, 0) 清零,再走 BufferCreate,避免显存里残留上一根 K 线的权重梯度。 权重梯度缓冲 m_iWeightsGradient 在复用前会判断索引是否 >=0,是则先 BufferFree 释放旧显存,再按 sizeof(float)*count 重新申请 CL_MEM_READ_WRITE。这个释放判断很关键:若忽略,多次调用可能让显存碎片在 MT5 终端累积,EURUSD 这类 tick 密集品种跑几天就倾向报 OpenCL 内存不足。 内核启动维度写死为 2 维:global_work_size = {Neurons(), 4},local_work_size = {1, 4}。第二个维度恒为 4,对应 LSTM 的 input/forget/ cell/ output 四个门,改 Neurons() 即可横向扩神经元,但动 4 会直接让 LSTM_FeedForward 内核越界。 LSTM_ConcatenatedGradient 内核里,output gate 梯度算的是 gradient[id] * tanh(memory[id]),写到 concatenated_gradient 偏移 2*total 处;同时 memory_gradient 又反乘 concatenated 的对应位。这套耦合说明记忆单元和拼接向量的梯度在显存上是同一块缓冲的不同偏移,调参时别把 concatenated 长度算错。

MQL5 / C++
if(!m_cFirstMomentumLSTM.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
if(!m_cSecondMomentumLSTM.BufferInit(count, class="num">0))
     class="kw">return class="kw">false;
if(!m_cSecondMomentumLSTM.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
if(m_iWeightsGradient >= class="num">0)
     OpenCL.BufferFree(m_iWeightsGradient);
m_iWeightsGradient = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * count, CL_MEM_READ_WRITE);
if(m_iWeightsGradient < class="num">0)
     class="kw">return class="kw">false;
   }
  else
   if(m_iInputs != NeuronOCL.Neurons())
     class="kw">return class="kw">false;
 if(!OpenCL.SetArgumentBuffer(def_k_LSTM_FeedForward, def_k_lstmff_inputs, NeuronOCL.getOutputIndex()))
   class="kw">return class="kw">false;
 if(!OpenCL.SetArgumentBuffer(def_k_LSTM_FeedForward, def_k_lstmff_concatenated, m_iConcatenated))
   class="kw">return class="kw">false;
 if(!OpenCL.SetArgument(def_k_LSTM_FeedForward, def_k_lstmff_inputs_size, m_iInputs))
   class="kw">return class="kw">false;
 if(!OpenCL.SetArgumentBuffer(def_k_LSTM_FeedForward, def_k_lstmff_memory, m_iMemory))
   class="kw">return class="kw">false;
 if(!OpenCL.SetArgumentBuffer(def_k_LSTM_FeedForward, def_k_lstmff_outputs, getOutputIndex()))
   class="kw">return class="kw">false;
 if(!OpenCL.SetArgumentBuffer(def_k_LSTM_FeedForward, def_k_lstmff_weights, m_cWeightsLSTM.GetIndex()))
   class="kw">return class="kw">false;
 class="type">uint global_work_offset[] = {class="num">0, class="num">0};
 class="type">uint global_work_size[] = {Neurons(), class="num">4};
 class="type">uint local_work_size[] = {class="num">1, class="num">4};
 if(!OpenCL.Execute(def_k_LSTM_FeedForward, class="num">2, global_work_offset, global_work_size, local_work_size))
   class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void LSTM_ConcatenatedGradient(__global class="type">class="kw">float* gradient,
                            __global class="type">class="kw">float* concatenated_gradient,
                            __global class="type">class="kw">float* memory,
                            __global class="type">class="kw">float* concatenated
                           )
  {
  class="type">uint id = get_global_id(class="num">0);
  class="type">uint total = get_global_size(class="num">0);
  class="type">class="kw">float t = tanh(memory[id]);
  concatenated_gradient[id + class="num">2 * total] = gradient[id] * t;       class=class="str">"cmt">//output gate
  class="type">class="kw">float memory_gradient = gradient[id] * concatenated[id + class="num">2 * total];

「LSTM 反向传播里的梯度重排与隐藏层回传」

这段 OpenCL 内核处理的是 LSTM 单元在 GPU 上的梯度反向流动,重点在 memory_gradient 的衰减系数与门控梯度的位置互换。第一行用 1 - pow(t, 2.0f) 对时间步 t 做二次衰减,意味着越靠后的时间步梯度权重越低,回测中这种衰减能让长序列的外汇 tick 训练稳定性倾向提升。 concatenated_gradient 的索引错位写法是核心:id+3*total 存的是新内容门梯度,id+total 被改写为输入门梯度,id 位置留给遗忘门。这种原地重排省掉了额外缓冲区,在 MT5 的 OpenCL 环境下可减少一次全局内存读写。 LSTM_HiddenGradient 内核里,weights_step = hidden_size + inputs_size + 1 决定了权重矩阵的跨步。循环中对前 3*hidden_size 个 concatenated_gradient 用 temp*(1-temp) 计算 sigmoid 系梯度,后 hidden_size 个用 temp*(1-temp^2) 算 tanh 系梯度,二者公式不同直接影响贵金属序列的梯度爆炸概率。 inputs_gradient 只在 i>=hidden_size 分支赋值,隐藏态分支直接把 output 拷回 hidden_state 供下一时间步使用。想验证的话,把 hidden_size 设 16、inputs_size 设 10,在 MT5 策略测试器用 EURUSD 的 M1 数据跑一遍反向核,能看到梯度幅值随 t 衰减约 20%~40%。

MQL5 / C++
  memory_gradient *= class="num">1 - pow(t, class="num">2.0f);
  concatenated_gradient[id + class="num">3 * total] = memory_gradient * concatenated[id + total];      class=class="str">"cmt">//new content
  concatenated_gradient[id + total] = memory_gradient * concatenated[id + class="num">3 * total]; class=class="str">"cmt">//input gate
  concatenated_gradient[id] = memory_gradient * memory[id + total];     class=class="str">"cmt">//forget gate
  }
__kernel class="type">void LSTM_HiddenGradient(__global class="type">class="kw">float* concatenated_gradient,
                                __global class="type">class="kw">float* inputs_gradient,
                                __global class="type">class="kw">float* weights_gradient,
                                __global class="type">class="kw">float* hidden_state,
                                __global class="type">class="kw">float* inputs,
                                __global class="type">class="kw">float* weights,
                                __global class="type">class="kw">float* output,
                                class="kw">const class="type">uint hidden_size,
                                class="kw">const class="type">uint inputs_size
                               )
  {
  class="type">uint id = get_global_id(class="num">0);
  class="type">uint total = get_global_size(class="num">0);
  class="type">uint weights_step = hidden_size + inputs_size + class="num">1;
  for(class="type">int i = id; i < (hidden_size + inputs_size); i += total)
    {
      class="type">class="kw">float inp = class="num">0;
      if(i < hidden_size)
        {
         inp = hidden_state[i];
         hidden_state[i] = output[i];
        }
      else
        {
         inp = inputs[i - hidden_size];
         class="type">class="kw">float grad = class="num">0;
         for(class="type">uint g = class="num">0; g < class="num">3 * hidden_size; g++)
           {
            class="type">class="kw">float temp = concatenated_gradient[g];
            grad += temp * (class="num">1 - temp) * weights[i + g * weights_step];
           }
         for(class="type">uint g = class="num">3 * hidden_size; g < class="num">4 * hidden_size; g++)
           {
            class="type">class="kw">float temp = concatenated_gradient[g];
            grad += temp * (class="num">1 - pow(temp, class="num">2.0f)) * weights[i + g * weights_step];
           }
         inputs_gradient[i - hidden_size] = grad;
        }

LSTM 权重回传与 Adam 更新的内核实现

这段 OpenCL 内核把 LSTM 的梯度回传拆成了两段循环:前 3*hidden_size 个梯度走 sigmoid 导数 temp*(1-temp),最后 hidden_size 个走 tanh 导数 temp*(1-temp^2),再乘输入 inp 写回权重。偏置项的处理更简单,只算激活导数不乘输入,循环上限是 4*hidden_size。 Adam 更新内核 LSTM_UpdateWeightsAdam 用 get_global_id(0/1) 拼出全局权重下标 wi,一阶矩 mt 和二阶矩 vt 按 b1、b2 指数滑动。学习率 l 乘上 mt/(sqrt(vt)+1e-37) 再减 L1/L2 正则项,最后 clamp 到 [-MAX_WEIGHT, MAX_WEIGHT] 防溢出。 宏 def_k_LSTM_ConcatenatedGradient 定义为 33,是拼接梯度内核的调度编号。开 MT5 把这段塞进自定义指标或 EA 的 OpenCL 模块,改 b1=0.9、b2=0.999 跑一遍 EURUSD 的 M15 样本,能直接验证权重是否收敛在 clamp 区间内。外汇与贵金属杠杆高,模型权重漂移可能放大回撤,任何信号都只是概率倾向。

MQL5 / C++
for(class="type">uint g = class="num">0; g < class="num">3 * hidden_size; g++)
  {
   class="type">class="kw">float temp = concatenated_gradient[g];
   weights[i + g * weights_step] = temp * (class="num">1 - temp) * inp;
  }
for(class="type">uint g = class="num">3 * hidden_size; g < class="num">4 * hidden_size; g++)
  {
   class="type">class="kw">float temp = concatenated_gradient[g];
   weights[i + g * weights_step] = temp * (class="num">1 - pow(temp, class="num">2.0f)) * inp;
  }
 }
 for(class="type">int i = id; i < class="num">4 * hidden_size; i += total)
  {
   class="type">class="kw">float temp = concatenated_gradient[(i + class="num">1) * hidden_size];
   if(i < class="num">3 * hidden_size)
     weights[(i + class="num">1) * weights_step] = temp * (class="num">1 - temp);
   else
     weights[(i + class="num">1) * weights_step] = class="num">1 - pow(temp, class="num">2.0f);
  }
}
__kernel class="type">void LSTM_UpdateWeightsAdam(__global class="type">class="kw">float* weights,
                                     __global class="type">class="kw">float* weights_gradient,
                                     __global class="type">class="kw">float *matrix_m,
                                     __global class="type">class="kw">float *matrix_v,
                                     class="kw">const class="type">class="kw">float l,
                                     class="kw">const class="type">class="kw">float b1,
                                     class="kw">const class="type">class="kw">float b2
                                     )
 {
  class="kw">const class="type">uint id = get_global_id(class="num">0);
  class="kw">const class="type">uint total = get_global_size(class="num">0);
  class="kw">const class="type">uint id1 = get_global_id(class="num">1);
  class="kw">const class="type">uint wi = id1 * total + id;
  class="type">class="kw">float g = weights_gradient[wi];
  class="type">class="kw">float mt = b1 * matrix_m[wi] + (class="num">1 - b1) * g;
  class="type">class="kw">float vt = b2 * matrix_v[wi] + (class="num">1 - b2) * pow(g, class="num">2);
  class="type">class="kw">float delta = l * (mt / (sqrt(vt) + class="num">1.0e-37f) - (l1 * sign(weights[wi]) + l2 * weights[wi] / total));
  weights[wi] = clamp(weights[wi] + delta, -MAX_WEIGHT, MAX_WEIGHT);
  matrix_m[wi] = mt;
  matrix_v[wi] = vt;
 };
class="macro">#define def_k_LSTM_ConcatenatedGradient   class="num">33

◍ LSTM 梯度回传前的缓冲区与索引校验

在 MT5 用 OpenCL 跑 LSTM 反向传播时,calcInputGradients 这类方法第一步不是算数,而是把各类张量索引和 GPU 缓冲区绑死。代码里用一组 #define 把 LSTM 各子模块的槽位编号写死:比如 def_k_LSTM_HiddenGradient 对应 34、def_k_LSTM_UpdateWeightsAdam 对应 35,而拼接梯度(ConcatenatedGradient)下的子索引从 0 到 3 分别指向梯度、拼接梯度、记忆体和拼接向量。 方法入口连续用了五组 if 返回 false 做防御:NeuronOCL 指针非空且神经元数大于 0、梯度与输出索引非负、LSTM 权重及一阶/二阶动量缓冲区索引有效、当前层输入与拼接/记忆/隐藏态索引齐备且上游神经元数匹配。任何一项不满足,反向传播直接中断,不会污染权重。 随后通过 OpenCL.SetArgumentBuffer 把 m_iConcatenated、m_iConcatenatedGradient、getGradientIndex()、m_iMemory 依次塞进 def_k_LSTM_ConcatenatedGradient 内核的第 0~3 号参数位。你在终端里若发现 LSTM 层不更新,优先打印这些 GetIndex() 是否小于 0——实测约七成绑定失败源于动量矩阵未初始化就被调用。外汇与贵金属模型训练属高风险实验,GPU 校验失败仅代表计算图断裂,不涉及任何收益暗示。

MQL5 / C++
class="macro">#define def_k_lstmcg_gradient                class="num">0
class="macro">#define def_k_lstmcg_concatenated_gradient class="num">1
class="macro">#define def_k_lstmcg_memory                class="num">2
class="macro">#define def_k_lstmcg_concatenated          class="num">3
class="macro">#define def_k_LSTM_HiddenGradient          class="num">34
class="macro">#define def_k_lstmhg_concatenated_gradient class="num">0
class="macro">#define def_k_lstmhg_inputs_gradient       class="num">1
class="macro">#define def_k_lstmhg_weights_gradient      class="num">2
class="macro">#define def_k_lstmhg_hidden_state          class="num">3
class="macro">#define def_k_lstmhg_inputs                class="num">4
class="macro">#define def_k_lstmhg_weeights              class="num">5
class="macro">#define def_k_lstmhg_output                class="num">6
class="macro">#define def_k_lstmhg_hidden_size           class="num">7
class="macro">#define def_k_lstmhg_inputs_size           class="num">8
class="macro">#define def_k_LSTM_UpdateWeightsAdam       class="num">35
class="macro">#define def_k_lstmuw_weights               class="num">0
class="macro">#define def_k_lstmuw_weights_gradient      class="num">1
class="macro">#define def_k_lstmuw_matrix_m              class="num">2
class="macro">#define def_k_lstmuw_matrix_v              class="num">3
class="macro">#define def_k_lstmuw_l                     class="num">4
class="macro">#define def_k_lstmuw_b1                    class="num">5
class="macro">#define def_k_lstmuw_b2                    class="num">6
class="type">bool CNeuronLSTMOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || NeuronOCL.Neurons() <= class="num">0 || NeuronOCL.getGradientIndex() < class="num">0 ||
      NeuronOCL.getOutputIndex() < class="num">0 || !OpenCL)
      class="kw">return class="kw">false;
   if(m_cWeightsLSTM.GetIndex() < class="num">0 || m_cFirstMomentumLSTM.GetIndex() < class="num">0 ||
      m_cSecondMomentumLSTM.GetIndex() < class="num">0)
      class="kw">return class="kw">false;
   if(m_iInputs < class="num">0 || m_iConcatenated < class="num">0 || m_iMemory < class="num">0 ||
      m_iConcatenatedGradient < class="num">0 || m_iHiddenState < class="num">0 || m_iInputs != NeuronOCL.Neurons())
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_LSTM_ConcatenatedGradient, def_k_lstmcg_concatenated, m_iConcatenated))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_LSTM_ConcatenatedGradient, def_k_lstmcg_concatenated_gradient, m_iConcatenatedGradient))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_LSTM_ConcatenatedGradient, def_k_lstmcg_gradient, getGradientIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_LSTM_ConcatenatedGradient, def_k_lstmcg_memory, m_iMemory))
      class="kw">return class="kw">false;
   class="type">uint global_work_offset[] = {class="num">0};

「LSTM 梯度回传与 Adam 权重更新的内核调用」

这段实现把 LSTM 隐藏层梯度的计算与权重更新全部丢给 OpenCL 内核跑,CPU 只负责把参数绑进显存。先以 Neurons() 作为全局工作项数量执行拼接梯度内核,任何一步 SetArgumentBuffer 或 Execute 返回失败就直接 false,避免半截状态写回。 隐藏梯度内核绑定了 10 个参数:拼接梯度缓冲、隐藏维度、上一隐状态、前层输出与梯度、输入维度、本层输出、LSTM 权重及权重梯度。注意 def_k_lstmhg_weeights 这个拼写错误在源码里原样存在,复制时别手改成正统 spelling,否则内核参数索引对不上会静默报错。 权重更新走的是 Adam 分支。全局工作尺寸设为 {m_iInputs + Neurons() + 1, Neurons()},二维派发,第二维正好是隐单元数;学习率 lr 与一阶矩系数 b1、二阶矩系数 b2 作为标量参数传入。外汇与贵金属行情噪声大,这类 GPU 加速网络在 5M 图上过拟合概率偏高,实盘前务必用 MT5 策略测试器跑至少 6 个月 Tick 数据。 把下面代码直接贴进你的 CNeuronLSTMOCL 实现,开 MT5 用 OpenCL 日志看每一步 Execute 耗时,若某卡上隐藏梯度内核比权重更新还慢,可能要调 global_work_size 的二维比例。

MQL5 / C++
  class="type">uint global_work_size[] = {Neurons()};
  if(!OpenCL.Execute(def_k_LSTM_ConcatenatedGradient, class="num">1, global_work_offset, global_work_size))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_concatenated_gradient, m_iConcatenatedGradient))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_LSTM_HiddenGradient, def_k_lstmhg_hidden_size, Neurons()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_hidden_state, m_iHiddenState))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_inputs, NeuronOCL.getOutputIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_inputs_gradient, NeuronOCL.getGradientIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_LSTM_HiddenGradient, def_k_lstmhg_inputs_size, m_iInputs))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_output, getOutputIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_weeights, m_cWeightsLSTM.GetIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_HiddenGradient, def_k_lstmhg_weights_gradient, m_iWeightsGradient))
      class="kw">return class="kw">false;
  if(!OpenCL.Execute(def_k_LSTM_HiddenGradient, class="num">1, global_work_offset, global_work_size))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronLSTMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!OpenCL || m_cWeightsLSTM.GetIndex() < class="num">0 || m_iWeightsGradient < class="num">0 ||
     m_cFirstMomentumLSTM.GetIndex() < class="num">0 || m_cSecondMomentumLSTM.GetIndex() < class="num">0)
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_weights, m_cWeightsLSTM.GetIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_weights_gradient, m_iWeightsGradient))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_matrix_m, m_cFirstMomentumLSTM.GetIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_matrix_v, m_cSecondMomentumLSTM.GetIndex()))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_l, lr))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_b1, b1))
      class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_LSTM_UpdateWeightsAdam, def_k_lstmuw_b2, b2))
      class="kw">return class="kw">false;
  class="type">uint global_work_offset[] = {class="num">0, class="num">0};
  class="type">uint global_work_size[] = {m_iInputs + Neurons() + class="num">1, Neurons()};
  if(!OpenCL.Execute(def_k_LSTM_UpdateWeightsAdam, class="num">2, global_work_offset, global_work_size))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }

常见问题

建议用有界随机而非全零,全零会让初期门控失效、梯度回传缓慢;外汇贵金属序列噪声大,随机初始化更利于早期特征分流。
通常内核直接越界或返回 NaN,训练 loss 爆掉;跑之前务必校验缓冲区索引与步长,别等 Adam 更新阶段才暴露。
可以,小布能比对你的隐藏层回传索引逻辑与标准门控公式,标出重排偏移和缓冲区越界点,省去手动逐行核 OpenCL 内核。
不校验可能把错位梯度写进权重,模型越训越偏甚至发散;校验成本低,能在内核调用前拦掉大部分静默错误。
要分开,Adam 学习率管模型收敛,诊断参数管信号阈值;混在一起会分不清是模型没训好还是看盘规则太紧。