神经网络变得轻松(第十部分):多目击者关注·进阶篇
(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 策略测试器跑一遍确认无显存越界。
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 的维度,这里不跟着改就会越界报错。
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 侧正则,得自己把那截取消注释并补内核参数。外汇与贵金属杠杆高,这类自定义神经网络层若参数错配,可能在实盘产生非预期爆仓,建议先在策略测试器用历史数据验证梯度收敛。
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 正确分配,是避免多头维度错配的第一步。外汇与贵金属杠杆交易高风险,任何模型结构改动都可能放大回测与实盘偏差。
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 跑通再上实盘。
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 推理模块在实盘前务必用历史数据做前向传播一致性核对,参数偏差可能导致信号失真。
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">//---