神经网络变得轻松(第十部分):多目击者关注·进阶篇
🧠

神经网络变得轻松(第十部分):多目击者关注·进阶篇

(2/3)· 单头自关注看不清的序列结构,多头并行如何从数学到代码真正落地

偏理论进阶 第 2/3 篇
只跑单头自关注时,序列里被掩盖的交叉依赖常被当成噪声丢掉。多头并行不是叠层数,而是让不同权重线程各自投票,再合并上下文。先把这件事想通,后面实现才不会写成堆砌。

◍ 多目击者注意力的 OpenCL 落地细节

把单头自关注改造成多头,第一步是砍掉 Key 张量。原 CNeuronAttentionOCL::feedForward 里调用 Key 卷积层的地方全部注释掉,Score 计算内核改用前层输出代替 Key,反向传播 calcInputGradients 与权重更新 updateInputWeights 做同样处理。这样 Query 和 Key 共用一个矩阵,维数跟输入序列对齐,训练参数直接减半。 多头结构封装在 CNeuronMHAttentionOCL 类,继承父类。保护段按目击者数声明 Querys、Values 卷积层实例,示例写死 4 个头;每个头带独立 Scores 缓冲区和 AttentionOut 全连接层,再接一个 AttentionConcatenate 串联层与 Weights0 卷积层做加权降维。Init 方法里从第二个头开始初始化,因为父类已建好第一个头的实例。 前馈时先用 OpenCL 内核把 4 个头的输出拼成单一张量,内核参数带各缓冲区窗口大小,可混搭不同窗口。拼完送 Weights0 降维,再与前层数据求平均并常规化,最后进 FeedForward 块。 误差反馈要写 DeconcatenateBuffers 内核把梯度拆回各头,再用 Sum5Matrix 内核累加——5 个输入缓冲对应 4 个头加 1 个原始梯度缓冲,乘 0.2 做平均以压制梯度衰落。calcHeadGradient 里对每个头调 AttentionInsideGradients,Query/Value 层梯度分别用 1.0 和 0.33 乘数累加,避免数值爆掉。 神经网络基类 CNet::feedForward 加了 window 和 tem 两个参数接管位置编码:用元素序号对窗口取余得 dim,取整得 pos,按前文公式累加进输入张量。同时 define 模块补新内核常量,构造函数挂新类,调度程序注册神经元类型。外汇与贵金属模型训练波动剧烈,改动后务必在 MT5 策略测试器跑一遍确认无显存越界。

MQL5 / C++
class="type">bool CNeuronAttentionOCL::feedForward(CNeuronBaseOCL *prevLayer)
  {
   if(CheckPointer(prevLayer)==POINTER_INVALID)
      class="kw">return class="kw">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]=class="num">1;
   OpenCL.SetArgumentBuffer(def_k_Normilize,def_k_norm_buffer,prevLayer.getOutputIndex());
   OpenCL.SetArgument(def_k_Normilize,def_k_norm_dimension,prevLayer.Neurons());
   if(!OpenCL.Execute(def_k_Normilize,class="num">1,global_work_offset,global_work_size))
     {
       printf("Error of execution kernel Normalize: %d",GetLastError());
       class="kw">return class="kw">false;
     }
   if(!prevLayer.Output.BufferRead())
      class="kw">return class="kw">false;
  }
class=class="str">"cmt">//---

「注意力层的前向核与梯度回传改写」

在自注意力算子的 OpenCL 实现里,前向计算先对 Query 和 Value 做 FeedForward,Key 的调用被注释掉,直接复用 prevLayer 的输出索引。这意味着当前版本把上一层激活当作 Key,省了一次独立线性映射,显存占用和核启动次数都降了,但表达容量可能受限。 前向核 AttentionScore 的全局工作项数量由 iUnits 决定,维度参数传的是 iWindow。跑完 Execute 后必须调 Scores.BufferRead() 把显存分数拉回主机,否则后续 softmax 类操作会读到脏数据。 反向的 calcInputGradients 里有个细节:用 def_k_MatrixSum 把 AttentionOut 的梯度和自身 Gradient 以 0.5 倍系数累加回写。这个 0.5 不是随便写的,它对应残差支路的等比缩放,漏掉就会导致梯度爆炸或消失。 注意力梯度核 AttentionGradients 是二维调度,global_work_size 设为 [iUnits, iWindow],Key 的梯度直接写进 prevLayer.getGradientIndex()。改网络结构时若动了 prevLayer 的维度,这里不跟着改就会越界报错。

MQL5 / C++
if(CheckPointer(Querys)==POINTER_INVALID || !Querys.FeedForward(prevLayer))
      class="kw">return class="kw">false;
class=class="str">"cmt">//if(CheckPointer(Keys)==POINTER_INVALID || !Keys.FeedForward(prevLayer))
class=class="str">"cmt">//   class="kw">return class="kw">false;
if(CheckPointer(Values)==POINTER_INVALID || !Values.FeedForward(prevLayer))
      class="kw">return class="kw">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]=iUnits;
  OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_querys,Querys.getOutputIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_keys,prevLayer.getOutputIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_score,Scores.GetIndex());
  OpenCL.SetArgument(def_k_AttentionScore,def_k_as_dimension,iWindow);
  if(!OpenCL.Execute(def_k_AttentionScore,class="num">1,global_work_offset,global_work_size))
     {
       printf("Error of execution kernel AttentionScore: %d",GetLastError());
       class="kw">return class="kw">false;
     }
  if(!Scores.BufferRead())
      class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Further code has no changes
class="type">bool CNeuronAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
  if(CheckPointer(prevLayer)==POINTER_INVALID)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  if(!FF2.calcInputGradients(FF1))
      class="kw">return class="kw">false;
  if(!FF1.calcInputGradients(AttentionOut))
      class="kw">return class="kw">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]=iUnits;
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,Gradient.GetIndex());
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex());
  OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow);
  OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,class="num">0.5);
  if(!OpenCL.Execute(def_k_MatrixSum,class="num">1,global_work_offset,global_work_size))
     {
       printf("Error of execution kernel MatrixSum: %d",GetLastError());
       class="kw">return class="kw">false;
     }
  class="type">class="kw">double temp[];
  if(AttentionOut.getGradient(temp)<=class="num">0)
      class="kw">return class="kw">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];
  global_work_size[class="num">0]=iUnits;
  global_work_size[class="num">1]=iWindow;
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_gradient,AttentionOut.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys,prevLayer.getOutputIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys_g,prevLayer.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys,Querys.getOutputIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys_g,Querys.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_values,Values.getOutputIndex());
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_values_g,Values.getGradientIndex());

注意力层反向传播的 GPU 内核调度

在 MT5 的 OpenCL 管线里,注意力机制的反向传播先把梯度分数塞进 AttentionGradients 内核。代码用 SetArgumentBuffer 把 Scores 缓冲绑定到 def_k_ag_scores,再 Execute 二维工作项(参数 2 表示 2D 网格),global_work_size 由 iUnits 决定,若返回失败直接 printf 错误码并退出。 紧接着的 MatrixSum 内核负责把 AttentionOut 与 prevLayer 的梯度按 iWindow 维度做加权求和,乘数固定为 1.0。注意这段代码在原文中被重复粘贴了两次(含背景色标记块与无标记块),实际部署时若不做去重,GPU 会白白多跑一遍相同计算,拖慢每根 K 线的训练回传。 被注释掉的 Keys.calcInputGradients 一段说明作者曾尝试对 Key 矩阵也回传梯度,但当前版本已禁用——如果你在贵金属 EA 里想引入 Key 侧正则,得自己把那截取消注释并补内核参数。外汇与贵金属杠杆高,这类自定义神经网络层若参数错配,可能在实盘产生非预期爆仓,建议先在策略测试器用历史数据验证梯度收敛。

MQL5 / C++
  OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_scores,Scores.GetIndex());
  if(!OpenCL.Execute(def_k_AttentionGradients,class="num">2,global_work_offset,global_work_size))
    {
      printf("Error of execution kernel AttentionGradients: %d",GetLastError());
      class="kw">return class="kw">false;
    }
  class="type">class="kw">double temp[];
  if(Querys.getGradient(temp)<=class="num">0)
    class="kw">return class="kw">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]=iUnits;
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex());
  OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow);
  OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,class="num">1.0);
  if(!OpenCL.Execute(def_k_MatrixSum,class="num">1,global_work_offset,global_work_size))
    {
      printf("Error of execution kernel MatrixSum: %d",GetLastError());
      class="kw">return class="kw">false;
    }
  class="type">class="kw">double temp[];
  if(AttentionOut.getGradient(temp)<=class="num">0)
    class="kw">return class="kw">false;
  }
class=class="str">"cmt">//---
  if(!Querys.calcInputGradients(prevLayer))
    class="kw">return class="kw">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]=iUnits;
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex());
  OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex());
  OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow);
  OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,class="num">1.0);
  if(!OpenCL.Execute(def_k_MatrixSum,class="num">1,global_work_offset,global_work_size))
    {
      printf("Error of execution kernel MatrixSum: %d",GetLastError());
      class="kw">return class="kw">false;
    }
  class="type">class="kw">double temp[];
  if(AttentionOut.getGradient(temp)<=class="num">0)
    class="kw">return class="kw">false;
  }

◍ 多头注意力层的类结构拆解

在 MT5 的 OpenCL 神经网络扩展里,CNeuronMHAttentionOCL 继承自 CNeuronAttentionOCL,把单头自注意力扩成了 4 个并行头。类内显式声明了 Querys2~4、Values2~4 共 6 个卷积层指针,以及 Scores2~4 三块双精度缓冲,用来分别存各头的查询、值与得分矩阵。 AttentionOut2~4 与 AttentionConcatenate 负责把四个头的输出做拼接,Weights0 则是一层后续卷积权重。从声明看,feedForward、updateInputWeights 和 calcHeadGradient 都被标记为 virtual,说明多头的前向与梯度回传逻辑在子类里重写,而非复用父类单头实现。 被注释掉的片段显示,早先版本可能在 AttentionOut 上直接取梯度并做 <=0 的截断返回,后来改为逐头由 calcHeadGradient 处理。若你在自己的 EA 里接这套结构,开 MT5 把类头文件调出来,核对 Querys/Values 指针是否随 Init 的 units_count 正确分配,是避免多头维度错配的第一步。外汇与贵金属杠杆交易高风险,任何模型结构改动都可能放大回测与实盘偏差。

MQL5 / C++
class CNeuronMHAttentionOCL   :  class="kw">public CNeuronAttentionOCL
  {
class="kw">protected:
   CNeuronConvOCL     *Querys2;            class=class="str">"cmt">///< Convolution layer for Querys Head class="num">2
   CNeuronConvOCL     *Querys3;            class=class="str">"cmt">///< Convolution layer for Querys Head class="num">3
   CNeuronConvOCL     *Querys4;            class=class="str">"cmt">///< Convolution layer for Querys Head class="num">4
   CNeuronConvOCL     *Values2;            class=class="str">"cmt">///< Convolution layer for Values Head class="num">2
   CNeuronConvOCL     *Values3;            class=class="str">"cmt">///< Convolution layer for Values Head class="num">3
   CNeuronConvOCL     *Values4;            class=class="str">"cmt">///< Convolution layer for Values Head class="num">4
   CBufferDouble      *Scores2;            class=class="str">"cmt">///< Buffer for Scores matrix Head class="num">2
   CBufferDouble      *Scores3;            class=class="str">"cmt">///< Buffer for Scores matrix Head class="num">3
   CBufferDouble      *Scores4;            class=class="str">"cmt">///< Buffer for Scores matrix Head class="num">4
   CNeuronBaseOCL     *AttentionOut2;      class=class="str">"cmt">///< Layer of Self-Attention Out
   CNeuronBaseOCL     *AttentionOut3;      class=class="str">"cmt">///< Layer of Self-Attention Out
   CNeuronBaseOCL     *AttentionOut4;      class=class="str">"cmt">///< Layer of Self-Attention Out
   CNeuronBaseOCL     *AttentionConcatenate;class=class="str">"cmt">///< Layer of Concatenate Self-Attention Out
   CNeuronConvOCL     *Weights0;           class=class="str">"cmt">///< Convolution layer for Weights0
class=class="str">"cmt">//---
   class="kw">virtual class="type">bool       feedForward(CNeuronBaseOCL *prevLayer);
   class="kw">virtual class="type">bool       updateInputWeights(CNeuronBaseOCL *prevLayer);
   class=class="str">"cmt">/// Method to transfer gradients inside Head Self-Attention
   class="kw">virtual class="type">bool       calcHeadGradient(CNeuronConvOCL *query, CNeuronConvOCL *value, CBufferDouble *score, CNeuronBaseOCL *attention, CNeuronBaseOCL *prevLayer);
class="kw">public:
   class=class="str">"cmt">/** Constructor */CNeuronMHAttentionOCL(class="type">void){};
   class=class="str">"cmt">/** Destructor */~CNeuronMHAttentionOCL(class="type">void);
   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 units_count, ENUM_OPTIMIZATION optimization_type);
   class="kw">virtual class="type">bool       calcInputGradients(CNeuronBaseOCL *prevLayer);
   class=class="str">"cmt">//---

「多头注意力类的析构与初始化落点」

在 MT5 的 OpenCL 神经网络封装里,CNeuronMHAttentionOCL 通过 Type() 返回 defNeuronMHAttentionOCL 常量来标识自身类型,并暴露 Save/Load 两个虚函数供文件系统序列化。 析构函数逐个检查 Querys2/3/4、Values2/3/4、Scores2/3/4、Weights0、AttentionOut2/3/4 以及 AttentionConcatenate 的指针有效性,非 POINTER_INVALID 才 delete,避免重复释放导致 EA 崩溃。 Init 先调用基类 CNeuronAttentionOCL::Init 完成基础卷积层装配,失败直接返回 false;随后惰性创建 Querys2 卷积层,以参数 window 同时作为核宽、步长与输入宽,units_count 作为输出通道,并强制 SetActivationFunction(None) 保留原始线性投影。 外汇与贵金属行情下用此类做 GPU 推理属高风险,参数 window 设错可能让显存分配直接失败,建议在策略测试器先以 window=12、units_count=8 跑通再上实盘。

MQL5 / C++
class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defNeuronMHAttentionOCL; }class=class="str">"cmt">///< Identificator of class.@class="kw">return Type of class
class=class="str">"cmt">//--- methods for working with files
class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle); class=class="str">"cmt">///< Save method @param[in] file_handle handle of file @class="kw">return logical result of operation
class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle); class=class="str">"cmt">///< Load method @param[in] file_handle handle of file @class="kw">return logical result of operation
};
CNeuronMHAttentionOCL::~CNeuronMHAttentionOCL(class="type">void)
  {
  if(CheckPointer(Querys2)!=POINTER_INVALID)
      class="kw">delete Querys2;
  if(CheckPointer(Querys3)!=POINTER_INVALID)
      class="kw">delete Querys3;
  if(CheckPointer(Querys4)!=POINTER_INVALID)
      class="kw">delete Querys4;
  if(CheckPointer(Values2)!=POINTER_INVALID)
      class="kw">delete Values2;
  if(CheckPointer(Values3)!=POINTER_INVALID)
      class="kw">delete Values3;
  if(CheckPointer(Values4)!=POINTER_INVALID)
      class="kw">delete Values4;
  if(CheckPointer(Scores2)!=POINTER_INVALID)
      class="kw">delete Scores2;
  if(CheckPointer(Scores3)!=POINTER_INVALID)
      class="kw">delete Scores3;
  if(CheckPointer(Scores4)!=POINTER_INVALID)
      class="kw">delete Scores4;
  if(CheckPointer(Weights0)!=POINTER_INVALID)
      class="kw">delete Weights0;
  if(CheckPointer(AttentionOut2)!=POINTER_INVALID)
      class="kw">delete AttentionOut2;
  if(CheckPointer(AttentionOut3)!=POINTER_INVALID)
      class="kw">delete AttentionOut3;
  if(CheckPointer(AttentionOut4)!=POINTER_INVALID)
      class="kw">delete AttentionOut4;
  if(CheckPointer(AttentionConcatenate)!=POINTER_INVALID)
      class="kw">delete AttentionConcatenate;
  }
class="type">bool CNeuronMHAttentionOCL::Init(class="type">uint numOutputs,class="type">uint myIndex,COpenCLMy *open_cl,class="type">uint window,class="type">uint units_count,ENUM_OPTIMIZATION optimization_type)
  {
  if(!CNeuronAttentionOCL::Init(numOutputs,myIndex,open_cl,window,units_count,optimization_type))
      class="kw">return class="kw">false;
  if(CheckPointer(Querys2)==POINTER_INVALID)
     {
      Querys2=new CNeuronConvOCL();
      if(CheckPointer(Querys2)==POINTER_INVALID)
        class="kw">return class="kw">false;
      if(!Querys2.Init(class="num">0,class="num">6,open_cl,window,window,window,units_count,optimization_type))
        class="kw">return class="kw">false;
      Querys2.SetActivationFunction(None);
     }
class=class="str">"cmt">//---

卷积层指针的惰性初始化套路

这段逻辑出现在某个基于 OpenCL 的神经网络模块里,核心动作是:对象指针无效时才 new,有效就跳过。Querys3 到 Values4 共 8 个 CNeuronConvOCL 实例,初始化参数里第二维编号从 7 递增到 11(7、8、9、10、11),其余 window、units_count、optimization_type 均共用外层变量。 每个卷积层 Init 失败或指针无效都直接 return false,且统一调 SetActivationFunction(None)——说明这几层是有意去掉非线性激活的线性卷积,可能用于特征抽取而非分类输出。 Scores2 是 CBufferDouble,分配长度为 units_count*units_count 并填 0.0,随后 BufferCreate 挂到 OpenCL 上下文。若你本地 units_count=64,这块显存缓冲就是 4096 个 double,约 32KB,可在 MT5 策略测试器里打印 CheckPointer 返回值验证是否真的走了 new 分支。 外汇与贵金属行情受杠杆与跳空影响大,这类 GPU 推理模块在实盘前务必用历史数据做前向传播一致性核对,参数偏差可能导致信号失真。

MQL5 / C++
  if(CheckPointer(Querys3)==POINTER_INVALID)
    {
      Querys3=new CNeuronConvOCL();
      if(CheckPointer(Querys3)==POINTER_INVALID)
         class="kw">return class="kw">false;
      if(!Querys3.Init(class="num">0,class="num">7,open_cl,window,window,window,units_count,optimization_type))
         class="kw">return class="kw">false;
      Querys3.SetActivationFunction(None);
    }
class=class="str">"cmt">//---
  if(CheckPointer(Querys4)==POINTER_INVALID)
    {
      Querys4=new CNeuronConvOCL();
      if(CheckPointer(Querys4)==POINTER_INVALID)
         class="kw">return class="kw">false;
      if(!Querys4.Init(class="num">0,class="num">8,open_cl,window,window,window,units_count,optimization_type))
         class="kw">return class="kw">false;
      Querys4.SetActivationFunction(None);
    }
  if(CheckPointer(Values2)==POINTER_INVALID)
    {
      Values2=new CNeuronConvOCL();
      if(CheckPointer(Values2)==POINTER_INVALID)
         class="kw">return class="kw">false;
      if(!Values2.Init(class="num">0,class="num">9,open_cl,window,window,window,units_count,optimization_type))
         class="kw">return class="kw">false;
      Values2.SetActivationFunction(None);
    }
class=class="str">"cmt">//---
  if(CheckPointer(Values3)==POINTER_INVALID)
    {
      Values3=new CNeuronConvOCL();
      if(CheckPointer(Values3)==POINTER_INVALID)
         class="kw">return class="kw">false;
      if(!Values3.Init(class="num">0,class="num">10,open_cl,window,window,window,units_count,optimization_type))
         class="kw">return class="kw">false;
      Values3.SetActivationFunction(None);
    }
class=class="str">"cmt">//---
  if(CheckPointer(Values4)==POINTER_INVALID)
    {
      Values4=new CNeuronConvOCL();
      if(CheckPointer(Values4)==POINTER_INVALID)
         class="kw">return class="kw">false;
      if(!Values4.Init(class="num">0,class="num">11,open_cl,window,window,window,units_count,optimization_type))
         class="kw">return class="kw">false;
      Values4.SetActivationFunction(None);
    }
class=class="str">"cmt">//---
  if(CheckPointer(Scores2)==POINTER_INVALID)
    {
      Scores2=new CBufferDouble();
      if(CheckPointer(Scores2)==POINTER_INVALID)
         class="kw">return class="kw">false;
    }
  if(!Scores2.BufferInit(units_count*units_count,class="num">0.0))
     class="kw">return class="kw">false;
  if(!Scores2.BufferCreate(OpenCL))
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
把多头权重诊断交给小布
这些多头关注层的权重初始化与梯度表现,小布盯盘的 AIGC 已内置诊断,打开对应品种页即可看到各头收敛差异,你只需判断哪几个头在提供有效信号。

常见问题

单头只需一组 Wq/Wk/Wv,多头并行要维护多组并串联后再乘 W0,训练参数量上升,但能捕捉不同子空间关联,推理时运算略增。
可以,小布盯盘支持导入自定义指标与 EA 的上下文诊断,多头层的输出张量会按头拆分展示,方便你比对各头稳定性。
因该乘积不依赖输入序列,对同一关注块所有迭代恒定,提前合并能省去每步重复矩阵乘,降低浮点开销。
必要,多头关注本身不含顺序信息,位置编码补回序列时序结构,否则并行线程会把输入当集合处理,丢失趋势先后。