神经网络变得简单(第 73 部分):价格走势预测 AutoBot·进阶篇
📘

神经网络变得简单(第 73 部分):价格走势预测 AutoBot·进阶篇

第 2/3 篇

「用 OpenCL 给转置层跑 GPU 加速」

在 MT5 的神经网络封装里,CNeuronTransposeOCL 把矩阵转置丢给 GPU 内核,避免 CPU 端逐元素搬数据。Init 里先调基类 CNeuronBaseOCL::Init,传入 count * window 作为总元素数,再把 iWindow 与 iCount 存为成员,这两个值直接决定后面内核的全局工作维度。 Transpose 内核只用两行 id 取数:r = get_global_id(0)、c = get_global_id(1),输出写回位置是 c * rows + r,等价于把输入 r * cols + c 的行列互换。feedForward 中 global_work_size 设为 {iCount, iWindow},而反向的 calcInputGradients 把维度翻成 {iWindow, iCount},转置的梯度本身也是一次转置。 实盘接这套结构时,外汇与贵金属行情的高波动可能让批量矩阵维度频繁变化,建议在 MT5 策略测试器里先打印 iCount、iWindow 确认维度匹配,否则 OpenCL.Execute 会直接返回错误码并 printf 出 CL_ERROR_DESCRIPTION。

MQL5 / C++
class="type">uint window,
 ENUM_OPTIMIZATION optimization_type,
 class="type">uint batch)
{
 if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, count * window,
                      optimization_type, batch))
    class="kw">return false;
class=class="str">"cmt">//---
 iWindow = window;
 iCount = count;
class=class="str">"cmt">//---
 class="kw">return true;
}
__kernel class="type">void Transpose(__global class="type">float *matrix_in,    class=class="str">"cmt">///<[in] Input matrix
                        __global class="type">float *matrix_out   class=class="str">"cmt">///<[out] Output matrix
                       )
{
 const class="type">int r = get_global_id(class="num">0);
 const class="type">int c = get_global_id(class="num">1);
 const class="type">int rows = get_global_size(class="num">0);
 const class="type">int cols = get_global_size(class="num">1);
class=class="str">"cmt">//---
 matrix_out[c * rows + r] = matrix_in[r * cols + c];
}
class="type">bool CNeuronTransposeOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
{
 if(!NeuronOCL)
    class="kw">return false;
class=class="str">"cmt">//---
 class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0};
 class="type">uint global_work_size[class="num">2] = {iCount, iWindow};
 if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, NeuronOCL.getOutputIndex()))
    class="kw">return false;
 if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, Output.GetIndex()))
    class="kw">return false;
 if(!OpenCL.Execute(def_k_Transpose, class="num">2, global_work_offset, global_work_size))
   {
     class="type">class="kw">string error;
     CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error);
     printf("Error of execution kernel Transpose: %d -> %s", GetLastError(), error);
     class="kw">return false;
   }
class=class="str">"cmt">//---
 class="kw">return true;
}
class="type">bool CNeuronTransposeOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
{
 if(!NeuronOCL)
    class="kw">return false;
class=class="str">"cmt">//---
 class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0};
 class="type">uint global_work_size[class="num">2] = {iWindow, iCount};
 if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, NeuronOCL.getGradientIndex()))
    class="kw">return false;
 if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, Gradient.GetIndex()))
    class="kw">return false;
 if(!OpenCL.Execute(def_k_Transpose, class="num">2, global_work_offset, global_work_size))
   {
     class="type">class="kw">string error;

用OpenCL搭一个行情编码器的层栈

在MT5里跑价格行为模型,想把历史K线送进GPU做特征提取,得先按层把网络结构描述清楚。下面这段CreateTrajNetDescriptions函数就是干这事:先清掉旧描述,再从输入层一路堆到转置层,每一层都new一个CLayerDescription并塞进CArrayObj容器。 输入层直接用defNeuronBaseOCL,节点数等于HistoryBars乘BarDescr,激活函数设None,优化器用ADAM。紧接着接BatchNorm层,batch尺寸取MathMax(1000, GPTBars)——也就是说样本数低于1000时强制按1000跑,显存占用和收敛稳定性得自己权衡。 Embedding层把prev_count长度的窗口压成GPTBars个token,输出维度交给EmbeddingSize。后面PEOCL做位置编码,MLMHAttentionOCL才是核心:step=4、window_out=16、layers=1,多头注意力的头数隐含在窗口拆分里。最后TransposeOCL翻一下张量维度,方便下游解码。 这套层栈在外汇和贵金属行情上属于高波动数据建模,过拟合概率不低,实盘前务必用历史分笔回测验证。代码里任何一层Add失败就delete并return false,MT5终端会打出具体错因,直接照着改参数即可。

MQL5 / C++
class="type">bool CreateTrajNetDescriptions(CArrayObj *autobot)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!autobot)
     {
      autobot = new CArrayObj();
      if(!autobot)
         class="kw">return false;
     }
class=class="str">"cmt">//--- Encoder
   autobot.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(!autobot.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 = MathMax(class="num">1000,GPTBars);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!autobot.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 = defNeuronEmbeddingOCL;
     {
      class="type">int temp[] = {prev_count};
      ArrayCopy(descr.windows, temp);
     }
   prev_count = descr.count = GPTBars;
   class="type">int prev_wout = descr.window_out = EmbeddingSize;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronPEOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = class="num">1;
   descr.optimization = ADAM;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;

◍ 解码器前的多层注意力堆叠

这段构建逻辑里,从第 6 层到第 12 层连续往 autobot 里灌描述符,核心是用多头注意力(MLMHAttentionOCL)配合转置层(TransposeOCL)反复做维度置换。第 6、8、10 层结构几乎一致:count 取 prev_wout、window 取 prev_count,step 固定 4、window_out 固定 16、layers 为 1,优化器全走 ADAM。 第 7 与第 9 层是纯转置,不挂优化器,只把 count/window 对调,给注意力层制造交叉视角的输入。这种对称穿插在 GPU 上跑 OCL 内核时,显存带宽占用倾向呈现周期性抖动。 第 11 层突然切到 Concatenate,把 PrecoderBars * EmbeddingSize 作为 count,window 拉到 prev_count * prev_wout,step 等于 EmbeddingSize,激活用 LReLU。第 12 层才正式把 prev_count 重置为 PrecoderBars、prev_wout 重置为 EmbeddingSize,意味着解码器入口的张量形状在这里被锚定。 任何一层 new 失败或 Add 返回否,都立刻 delete 并 return false,调用方拿不到半残网络。开 MT5 把 step 从 4 改成 2,可能看到推理延迟下降但过拟合概率上升,贵金属品种上这种敏感度高,属典型高风险调参。

MQL5 / C++
  class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_wout;
   descr.window = prev_count;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = class="num">1;
   descr.optimization = ADAM;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_wout;
   descr.window = prev_count;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">8
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = class="num">1;
   descr.optimization = ADAM;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">9
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   descr.window = prev_wout;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">10
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLMHAttentionOCL;
   descr.count = prev_wout;
   descr.window = prev_count;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = class="num">1;
   descr.optimization = ADAM;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- Decoder
class=class="str">"cmt">//--- layer class="num">11
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConcatenate;
   descr.count = PrecoderBars * EmbeddingSize;
   descr.window = prev_count * prev_wout;
   descr.step = EmbeddingSize;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">12
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLMHAttentionOCL;
   prev_count = descr.count = PrecoderBars;
   prev_wout = descr.window = EmbeddingSize;
   descr.step = class="num">4;
   descr.window_out = class="num">16;
   descr.layers = class="num">1;
   descr.optimization = ADAM;
   if(!autobot.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }

「堆叠转置与多头注意力构建特征交叉」

这段网络组装代码连续注册了第 13 到 19 层,核心思路是用转置层调换特征与窗口维度,再交替塞入多头注意力层做跨维交互。外汇与贵金属行情里,这种结构可能更擅长捕捉不同周期粒度间的隐含关联,但高杠杆下模型过拟合风险同样偏高。 第 14 层用 defNeuronMLMHAttentionOCL,step 写死为 4,window_out 固定 16,layers 为 1,优化器选 ADAM;第 16 层复用同型注意力但 count 与 window 参数对调,形成对称交叉。 第 15 层是 Concatenate,把前层输出按 prev_count * prev_wout 拼接,step 指向 EmbeddingSize,激活用 LReLU;第 19 层收口用 defNeuronBaseOCL,节点数等于 PrecoderBars * 3,激活 None。 每一层都先 new CLayerDescription,若 Add 失败立即 delete 并返回 false,这种写法能在 MT5 实盘加载时避免内存泄漏。打开你的 EA 源码把这段粘进网络初始化函数,改一下 PrecoderBars 看推理延迟变化。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">13
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronTransposeOCL;
  descr.count = prev_count;
  descr.window = prev_wout;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">14
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronMLMHAttentionOCL;
  descr.count = prev_wout;
  descr.window = prev_count;
  descr.step = class="num">4;
  descr.window_out = class="num">16;
  descr.layers = class="num">1;
  descr.optimization = ADAM;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">15
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronConcatenate;
  descr.count = prev_count * prev_wout;
  descr.window = descr.count;
  descr.step = EmbeddingSize;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">16
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronMLMHAttentionOCL;
  descr.count = prev_count;
  descr.window = prev_wout;
  descr.step = class="num">4;
  descr.window_out = class="num">16;
  descr.layers = class="num">1;
  descr.optimization = ADAM;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">17
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronTransposeOCL;
  descr.count = prev_count;
  descr.window = prev_wout;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">18
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronMLMHAttentionOCL;
  descr.count = prev_wout;
  descr.window = prev_count;
  descr.step = class="num">4;
  descr.window_out = class="num">16;
  descr.layers = class="num">1;
  descr.optimization = ADAM;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">19
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = PrecoderBars * class="num">3;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!autobot.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//---
  class="kw">return true;
  }
CNet                Autobot;
class="type">int OnInit()
  {

初始化与退出时的模型装载校验

EA 在 OnInit 里先调 ResetLastError 清掉错误码,再尝试 LoadTotalBase 读取历史学习数据;若返回 false 就打印错误码并直接 INIT_FAILED,这一步决定了后续神经网络能否拿到训练底表。 接着用 Autobot.Load 装载 Traj.nnw 权重文件,三个 temp 浮点占位、dtStudied 传入学习时间戳。文件不存在时走 CreateTrajNetDescriptions 新建网络描述并 Autobot.Create 初始化,任何一步失败都 delete 临时对象并回 INIT_FAILED。 装载后必须做维度对账:getResults 输出总数应等于 PrecoderBars*3,第一层输出应等于 HistoryBars*BarDescr,否则打印不匹配数量并失败。这组硬校验能避免你改了输入窗口却忘了重训模型导致的静默错乱。 OpenCL 上下文从 Autobot.GetOpenCL 取得,Ones 与 Gradient 两个 buffer 分别用 1 和 0 初始化并挂到 OpenCL;State 按 HistoryBars*BarDescr 清零。最后 EventChartCustom 发 Init 事件通知图表,返回 INIT_SUCCEEDED。 OnDeinit 里只在非 INITFAILED、非 RECOMPILE 时调 Autobot.Save 落盘 Traj.nnw,并 delete Result 与 OpenCL。注意:外汇与贵金属杠杆交易高风险,模型装载失败可能让 EA 完全不交易,需在 MT5 日志里确认错误码后再排查。

MQL5 / C++
  ResetLastError();
  if(!LoadTotalBase())
    {
      PrintFormat("Error of load study data: %d", GetLastError());
      class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//--- load models
  class="type">float temp;
  if(!Autobot.Load(FileName + "Traj.nnw", temp, temp, temp, dtStudied, true))
    {
      Print("Init new models");
      CArrayObj *autobot = new CArrayObj();
      if(!CreateTrajNetDescriptions(autobot))
        {
          class="kw">delete autobot;
          class="kw">return INIT_FAILED;
        }
      if(!Autobot.Create(autobot))
        {
          class="kw">delete autobot;
          class="kw">return INIT_FAILED;
        }
      class="kw">delete autobot;
      class=class="str">"cmt">//---
    }
  Autobot.getResults(Result);
  if(Result.Total() != PrecoderBars * class="num">3)
    {
      PrintFormat("The scope of the Autobot does not match the precoder bars(%d <> %d)",
PrecoderBars * class="num">3, Result.Total());
      class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//---
  Autobot.GetLayerOutput(class="num">0, Result);
  if(Result.Total() != (HistoryBars * BarDescr))
    {
      PrintFormat("Input size of Autobot doesn&class="macro">#x27;t match state description(%d <> %d)",
Result.Total(), (HistoryBars * BarDescr));
      class="kw">return INIT_FAILED;
    }
  OpenCL = Autobot.GetOpenCL();
  if(!Ones.BufferInit(EmbeddingSize, class="num">1) ||
      !Gradient.BufferInit(EmbeddingSize, class="num">0) ||
      !Ones.BufferCreate(OpenCL) ||
      !Gradient.BufferCreate(OpenCL))
    {
      PrintFormat("Error of create buffers: %d", GetLastError());
      class="kw">return INIT_FAILED;
    }
  State.BufferInit(HistoryBars * BarDescr, class="num">0);
  if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"))
    {
      PrintFormat("Error of create study event: %d", GetLastError());
      class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
    Autobot.Save(FileName + "Traj.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
  class="kw">delete Result;
  class="kw">delete OpenCL;
  }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
  vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
  vector<class="type">float> result, target, inp;
  matrix<class="type">float> targets;

◍ 训练循环里的状态差分与批次截断

这段逻辑跑在蒙特卡洛式采样的主循环里,外层以 Iterations 为上限,同时受 IsStopped() 与内部 Stop 标志约束,避免在 MT5 策略测试器点停止后还空转烧 CPU。 每次迭代先用 SampleTrajectory(probability) 抽一条轨迹,再用双重 MathRand() 平方归一化挑起始 state,把随机性压到 [0,1) 且偏向低端,样本更集中在近期区段。若算出的 state 为负就 iter-- 并重抽,不浪费计数。 batch 定为 GPTBars+50,end 用 MathMin 卡在 Buffer[tr].Total - PrecoderBars 之前,确保后面塞 PrecoderBars 根预处理棒线时不越界。delta 矩阵按 (end-state-1) 行建,每行存状态向量;行号 row>0 时写的是「上一行 delta 减当前 inp」,即一阶差分而非原始状态——网络吃的是变化量。 喂网前 Autobot.Clear() 清状态,feedForward 带 Ones 偏置矩阵且 false 表示不训练只前向;任一步返回失败就 PrintFormat 打函数行号、置 Stop=true 并 break。targets 按 PrecoderBars×3 建,若 size>BarDescr 会把整段棒线 reshape 后再裁到末行 3 列,只取最新聚合特征喂给预测头。

MQL5 / C++
  matrix<class="type">float> delta;
  STE = vector<class="type">float>::Zeros(PrecoderBars * class="num">3);
  class="type">int std_count = class="num">0;
  class="type">int batch = GPTBars + class="num">50;
  class="type">bool Stop = false;
  class="type">uint ticks = GetTickCount();
  class="type">ulong size = HistoryBars * BarDescr;
  for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
    {
      class="type">int tr = SampleTrajectory(probability);
      class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) *
(Buffer[tr].Total - class="num">3 - PrecoderBars - batch));
      if(state < class="num">0)
        {
         iter--;
         class="kw">continue;
        }
      class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      Autobot.Clear();
      delta = matrix<class="type">float>::Zeros(end - state - class="num">1, Buffer[tr].States[state].state.Size());
      for(class="type">int i = state; i < end; i++)
        {
         inp.Assign(Buffer[tr].States[i].state);
         State.AssignArray(inp);
         if(i < (end - class="num">1))
           delta.Row(inp, row);
         if(row > class="num">0)
           delta.Row(delta.Row(row - class="num">1) - inp, row - class="num">1);
         if(!Autobot.feedForward((CBufferFloat*)GetPointer(State), class="num">1, false,
(CBufferFloat*)GetPointer(Ones)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }
         targets = matrix<class="type">float>::Zeros(PrecoderBars, class="num">3);
         for(class="type">int t = class="num">0; t < PrecoderBars; t++)
           {
            target.Assign(Buffer[tr].States[i + class="num">1 + t].state);
            if(size > BarDescr)
              {
               matrix<class="type">float> temp(class="num">1, size);
               temp.Row(target, class="num">0);
               temp.Reshape(size / BarDescr, BarDescr);
               temp.Resize(size / BarDescr, class="num">3);
               target = temp.Row(temp.Rows() - class="num">1);
              }
            targets.Row(target, t);
           }
         targets.Reshape(class="num">1, targets.Rows()*targets.Cols());
         target = targets.Row(class="num">0);

常见问题

先确认驱动支持 OpenCL 1.2+,在模型装载校验里打印 device 列表;老卡若不支持相关指令集,退回 CPU 路径并降低批次大小。
看训练循环里的状态差分曲线,若验证集损失早抬头就做批次截断;层栈深度减半再对比信号稳定性。
小布可加载你的层栈配置与训练日志,自动标出状态差分异常和注意力权重集中区,给出是否继续训练的提醒。
检查堆叠转置的输出通道数是否等于注意力输入维度;在初始化校验里断言 shape,不一致就补 1x1 映射层。
按品种波动调,贵金属高风险建议 64~128 样本;断点存状态差分快照,下次从快照续跑避免重头。