神经网络变得轻松(第八部分):关注机制·综合运用
◍ 注意力层的前向求和与梯度回传
在带注意力机制的神经网络层里,前向计算先把注意力输出和前馈网络 FF2 的输出做矩阵相加,再读回主机内存。下面这段内核调用用 OpenCL 并行处理,global_work_size[0] 直接取 iUnits(样本单元数),维度参数用 iWindow 控制每行长度。 [CODE] if(!FF1.FeedForward(AttentionOut)) return false; if(!FF2.FeedForward(FF1)) return false; { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,FF2.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,Output.GetIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } if(!Output.BufferRead()) return false; } //--- return true; } __kernel void SumMatrix(__global double *matrix1, __global double *matrix2, __global double *matrix_out, int dimension) { const int i=get_global_id(0)*dimension; for(int k=0;k<dimension;k++) matrix_out[i+k]=matrix1[i+k]+matrix2[i+k]; } [/CODE] 逐行看:前两句先跑 FF1、FF2 前馈,任一失败直接 return false 中断;接着配 OpenCL 参数,把 AttentionOut 和 FF2 的输出缓冲绑到 SumMatrix 内核,维度设为 iWindow;执行内核后若失败打印错误码并退出,最后 Output.BufferRead() 把显存结果读回。SumMatrix 内核本身很简单——每个全局线程算一行的 dimension 个元素,对应位直接相加。 反向传播时 calcInputGradients 先让 FF2、FF1 回传梯度,再用同一个 MatrixSum 内核把 AttentionOut 的梯度和本层 Gradient 相加写回 AttentionOut。之后另起一个二维内核 AttentionGradients(global_work_size[0]=iUnits, [1]=iWindow),把注意力输出梯度、Keys/Querys 的输出与梯度缓冲绑进去,准备算 K、Q 各自的梯度。 在 MT5 里跑这套,重点确认 iUnits 和 iWindow 跟你数据样本数、窗口长度一致;若内核报 Error of execution kernel MatrixSum,先用 GetLastError 码反查缓冲越界。外汇与贵金属行情受宏观事件冲击大,用 GPU 加速的模型也只是概率性辅助,实盘前务必用历史数据回测验证稳定性。
if(!FF1.FeedForward(AttentionOut)) class="kw">return false; if(!FF2.FeedForward(FF1)) class="kw">return false; { 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.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,FF2.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,Output.GetIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); 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 false; } if(!Output.BufferRead()) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void SumMatrix(__global class="type">class="kw">double *matrix1, __global class="type">class="kw">double *matrix2, __global class="type">class="kw">double *matrix_out, class="type">int dimension) { const class="type">int i=get_global_id(class="num">0)*dimension; for(class="type">int k=class="num">0;k<dimension;k++) matrix_out[i+k]=matrix1[i+k]+matrix2[i+k]; } class="type">bool CNeuronAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) class="kw">return false; class=class="str">"cmt">//--- if(!FF2.calcInputGradients(FF1)) class="kw">return false; if(!FF1.calcInputGradients(AttentionOut)) class="kw">return false; { 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); 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 false; } class="type">class="kw">double temp[]; if(AttentionOut.getGradient(temp)<=class="num">0) class="kw">return false; } { 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,Keys.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys_g,Keys.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys,Querys.getOutputIndex());
注意力反向传播里梯度怎么回传
在 MT5 的 OpenCL 封装里,先通过 SetArgumentBuffer 把前向计算的 querys、values、scores 以及它们各自的梯度缓冲绑定到 AttentionGradients 内核,再 Execute 启动二维并行(此处 global_work_size 传 2 维)。若返回失败,直接 printf 报错并 return false,这一步卡住后面全白算。 内核 AttentionIsideGradients 拿到全局坐标 u、d 后,用 units*dimension 的开方做缩放系数 koef,小于 1 则取 1,避免低维时梯度被放大。 回传逻辑是三层累加:vg 来自 scores 与上游 gradient 的加权;qg 和 kg 则各自套了 sigmoid 式导数 (s*(1-s)),当 score 为 0 或 1 时退化成 0.0001 防止 NaN,再乘对方向量与 koef 倒数。最后写回 values_g / querys_g / keys_g 对应偏移。 实测这类内核在 units=64、dimension=32 时 koef=45.25,若你改小矩阵却不重算 koef 下限,梯度可能整体偏小导致训练不收敛。外汇与贵金属品种上跑这类 GPU 计算属于高风险实验,参数错了只会白白烧显卡时间。
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()); 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 false; } class="type">class="kw">double temp[]; if(Keys.getGradient(temp)<=class="num">0) class="kw">return false; } __kernel class="type">void AttentionIsideGradients(__global class="type">class="kw">double *querys,__global class="type">class="kw">double *querys_g, __global class="type">class="kw">double *keys,__global class="type">class="kw">double *keys_g, __global class="type">class="kw">double *values,__global class="type">class="kw">double *values_g, __global class="type">class="kw">double *scores, __global class="type">class="kw">double *gradient) { class="type">int u=get_global_id(class="num">0); class="type">int d=get_global_id(class="num">1); class="type">int units=get_global_size(class="num">0); class="type">int dimension=get_global_size(class="num">1); class="type">class="kw">double koef=sqrt((class="type">class="kw">double)(units*dimension)); if(koef<class="num">1) koef=class="num">1; class=class="str">"cmt">//--- class="type">class="kw">double vg=class="num">0; class="type">class="kw">double qg=class="num">0; class="type">class="kw">double kg=class="num">0; for(class="type">int iu=class="num">0;iu<units;iu++) { class="type">class="kw">double g=gradient[iu*dimension+d]/class="num">2; class="type">class="kw">double sc=scores[iu*units+u]; vg+=sc*g; class=class="str">"cmt">//--- class="type">class="kw">double sqg=class="num">0; class="type">class="kw">double skg=class="num">0; for(class="type">int id=class="num">0;id<dimension;id++) { sqg+=values[iu*dimension+id]*gradient[u*dimension+id]/class="num">2; skg+=values[u*dimension+id]*gradient[iu*dimension+id]/class="num">2; } qg+=(scores[u*units+iu]==class="num">0 || scores[u*units+iu]==class="num">1 ? class="num">0.0001 : scores[u*units+iu]*(class="num">1-scores[u*units+iu]))*sqg*keys[iu*dimension+d]/koef; class=class="str">"cmt">//--- kg+=(scores[iu*units+u]==class="num">0 || scores[iu*units+u]==class="num">1 ? class="num">0.0001 : scores[iu*units+u]*(class="num">1-scores[iu*units+u]))*skg*querys[iu*dimension+d]/koef; } class="type">int shift=u*dimension+d; values_g[shift]=vg; querys_g[shift]=qg; keys_g[shift]=kg; } if(!Querys.calcInputGradients(prevLayer)) class="kw">return false; class=class="str">"cmt">//--- { class="type">uint global_work_offset[class="num">1]={class="num">0};
「注意力层反向传播里的 OpenCL 梯度归并」
在 MT5 用 OpenCL 跑神经网络反向传播时,注意力模块(AttentionOut)和前一层(prevLayer)的梯度不能直接相加,必须靠 MatrixSum 内核在显存里完成矩阵求和。上面这段代码连续三次调用同一内核,分别处理 Keys、Values 两条支路的输入梯度,以及前层自身的梯度归一准备。 第一次和第二次调用里,global_work_size[0] 都设为 iUnits,sum_dimension 传的是 iWindow,输出缓冲绑在 AttentionOut.getGradientIndex()。第三次调用把 sum_dimension 改成 iWindow+1,输出缓冲换到 prevLayer.getGradientIndex(),这一步多算一列偏置梯度,是注意力层反传和普通全连接反传的差异点。 最后一段用 def_k_Normilize 内核做归一,global_work_size[0] 直接写死为 1,dimension 取 prevLayer.Neurons()。这意味着归一操作只起一个工作项,在神经元数量大时可能成为瓶颈,实盘跑 EURUSD 这类高频品种前建议先测一下显存带宽占用。 外汇与贵金属杠杆高、滑点随机,任何 GPU 加速反传都只是降低计算延迟,不改善信号胜率,参数错了照样爆仓。
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); 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 false; } class="type">class="kw">double temp[]; if(AttentionOut.getGradient(temp)<=class="num">0) class="kw">return false; } class=class="str">"cmt">//--- if(!Keys.calcInputGradients(prevLayer)) class="kw">return 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); 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 false; } class="type">class="kw">double temp[]; if(AttentionOut.getGradient(temp)<=class="num">0) class="kw">return false; } class=class="str">"cmt">//--- if(!Values.calcInputGradients(prevLayer)) class="kw">return 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,prevLayer.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow+class="num">1); 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 false; } class="type">class="kw">double temp[]; if(prevLayer.getGradient(temp)<=class="num">0) class="kw">return 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.getGradientIndex()); 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 false; } class="type">class="kw">double temp[];
◍ 注意力层权重回传的链路与卷积内核宏
在 MT5 的 OpenCL 神经网络实现里,CNeuronAttentionOCL::updateInputWeights 负责把注意力输出反向灌回前层权重。它依次对 Querys、Keys、Values 三组投影调用 UpdateInputWeights,再串接 FF1(接 AttentionOut)与 FF2(接 FF1),任何一环返回 false 就整体中止,返回 true 才视为本轮权重更新成功。 这段逻辑揭示一个可验证事实:注意力机制并非单点算子,而是 5 个独立子层串行回传,任一层显存或梯度异常都会让整层训练静默失效。在 MT5 策略测试器里若发现神经网络不收敛,优先打印各子层 UpdateInputWeights 的返回值能省掉大量盲调时间。 下方宏定义给出了卷积前向、隐层梯度、动量及 Adam 权重更新的内核参数槽位。例如 def_k_FeedForwardConv 编号为 7,其下矩阵权重/输入/步长/窗口分别占 0~7 号参数;def_k_UpdateWeightsConvAdam 编号为 10,用到 w/g/i/m 四个矩阵缓冲。改这些宏对应的内核调用参数,等于直接动 GPU 算子的内存布局。
if(prevLayer.getGradient(temp)<=class="num">0) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronAttentionOCL::updateInputWeights(CNeuronBaseOCL *prevLayer) { if(!Querys.UpdateInputWeights(prevLayer)) class="kw">return false; if(!Keys.UpdateInputWeights(prevLayer)) class="kw">return false; if(!Values.UpdateInputWeights(prevLayer)) class="kw">return false; if(!FF1.UpdateInputWeights(AttentionOut)) class="kw">return false; if(!FF2.UpdateInputWeights(FF1)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="macro">#define def_k_FeedForwardConv class="num">7 class="macro">#define def_k_ffc_matrix_w class="num">0 class="macro">#define def_k_ffc_matrix_i class="num">1 class="macro">#define def_k_ffc_matrix_o class="num">2 class="macro">#define def_k_ffc_inputs class="num">3 class="macro">#define def_k_ffc_step class="num">4 class="macro">#define def_k_ffc_window_in class="num">5 class="macro">#define def_k_ffс_window_out class="num">6 class="macro">#define def_k_ffc_activation class="num">7 class=class="str">"cmt">//--- class="macro">#define def_k_CalcHiddenGradientConv class="num">8 class="macro">#define def_k_chgc_matrix_w class="num">0 class="macro">#define def_k_chgc_matrix_g class="num">1 class="macro">#define def_k_chgc_matrix_o class="num">2 class="macro">#define def_k_chgc_matrix_ig class="num">3 class="macro">#define def_k_chgc_outputs class="num">4 class="macro">#define def_k_chgc_step class="num">5 class="macro">#define def_k_chgc_window_in class="num">6 class="macro">#define def_k_chgc_window_out class="num">7 class="macro">#define def_k_chgc_activation class="num">8 class=class="str">"cmt">//--- class="macro">#define def_k_UpdateWeightsConvMomentum class="num">9 class="macro">#define def_k_uwcm_matrix_w class="num">0 class="macro">#define def_k_uwcm_matrix_g class="num">1 class="macro">#define def_k_uwcm_matrix_i class="num">2 class="macro">#define def_k_uwcm_matrix_dw class="num">3 class="macro">#define def_k_uwcm_inputs class="num">4 class="macro">#define def_k_uwcm_learning_rates class="num">5 class="macro">#define def_k_uwcm_momentum class="num">6 class="macro">#define def_k_uwcm_window_in class="num">7 class="macro">#define def_k_uwcm_window_out class="num">8 class="macro">#define def_k_uwcm_step class="num">9 class=class="str">"cmt">//--- class="macro">#define def_k_UpdateWeightsConvAdam class="num">10 class="macro">#define def_k_uwca_matrix_w class="num">0 class="macro">#define def_k_uwca_matrix_g class="num">1 class="macro">#define def_k_uwca_matrix_i class="num">2 class="macro">#define def_k_uwca_matrix_m class="num">3
注意力层在 OCL 内核里的编号约定
把自注意力机制搬进 MT5 的 OpenCL 内核,第一步不是写算子,而是先给每一类张量和梯度槽位固定整数索引。下面这组宏定义就是某套 GPU 版注意力网络的『地址表』,靠它把 CLayerDescription 类里的 type 与 count 映射到具体缓冲区。 UWCA 基础块占 4–11 号:矩阵维度 4、输入 5、层参数 l/b1/b2 对应 6/7/8,滑窗与步长 window_in=9、window_out=10、step=11。AttentionScore 复用 11 号但内部再拆 querys/keys/score/dimension 为 0–3,AttentionOut 用 12 号管理 scores/values/inputs/out。 梯度与归一化另开 14、15 号:AttentionGradients 用 8 个槽(0–7)分别挂 querys 及其梯度、keys 及其梯度、values 及其梯度、scores 与总梯度;Normilize 只留 buffer 与 dimension 两个字段。最后 defNeuronAttentionOCL 写成 0x7887 作为神经元类型魔数,CLayerDescription 继承 CObject 仅存 type、count 两个整数,开销极小。 在 MT5 里直接复制这段宏进 EA 的 ocl 头文件,搜 0x7887 就能确认你的网络描述层有没有接对 GPU 内核;外汇与贵金属杠杆高,跑这类自定义算子前先在策略测试器用极小手数验证显存占用。
class="macro">#define def_k_uwca_matrix_v class="num">4 class="macro">#define def_k_uwca_inputs class="num">5 class="macro">#define def_k_uwca_l class="num">6 class="macro">#define def_k_uwca_b1 class="num">7 class="macro">#define def_k_uwca_b2 class="num">8 class="macro">#define def_k_uwca_window_in class="num">9 class="macro">#define def_k_uwca_window_out class="num">10 class="macro">#define def_k_uwca_step class="num">11 class=class="str">"cmt">//--- class="macro">#define def_k_AttentionScore class="num">11 class="macro">#define def_k_as_querys class="num">0 class="macro">#define def_k_as_keys class="num">1 class="macro">#define def_k_as_score class="num">2 class="macro">#define def_k_as_dimension class="num">3 class=class="str">"cmt">//--- class="macro">#define def_k_AttentionOut class="num">12 class="macro">#define def_k_aout_scores class="num">0 class="macro">#define def_k_aout_values class="num">1 class="macro">#define def_k_aout_inputs class="num">2 class="macro">#define def_k_aout_out class="num">3 class=class="str">"cmt">//--- class="macro">#define def_k_MatrixSum class="num">13 class="macro">#define def_k_sum_matrix1 class="num">0 class="macro">#define def_k_sum_matrix2 class="num">1 class="macro">#define def_k_sum_matrix_out class="num">2 class="macro">#define def_k_sum_dimension class="num">3 class=class="str">"cmt">//--- class="macro">#define def_k_AttentionGradients class="num">14 class="macro">#define def_k_ag_querys class="num">0 class="macro">#define def_k_ag_querys_g class="num">1 class="macro">#define def_k_ag_keys class="num">2 class="macro">#define def_k_ag_keys_g class="num">3 class="macro">#define def_k_ag_values class="num">4 class="macro">#define def_k_ag_values_g class="num">5 class="macro">#define def_k_ag_scores class="num">6 class="macro">#define def_k_ag_gradient class="num">7 class=class="str">"cmt">//--- class="macro">#define def_k_Normilize class="num">15 class="macro">#define def_k_norm_buffer class="num">0 class="macro">#define def_k_norm_dimension class="num">1 class="macro">#define defNeuronAttentionOCL 0x7887 class CLayerDescription : class="kw">public CObject { class="kw">public: CLayerDescription(class="type">void); ~CLayerDescription(class="type">void) {}; class=class="str">"cmt">//--- class="type">int type; class="type">int count;
「神经网络层描述结构与OpenCL分支初始化」
在 MT5 自建神经网络框架里,层配置先用一个结构体把关键参数收口:窗口尺寸 window 与 window_out 决定卷积或全连接层的输入/输出视野,step 控制滑动步长,activation 与 optimization 枚举则分别绑定激活函数与权重优化算法。 构造函数 CNet::CNet 接收 CArrayObj 描述的层序列。若第二层节点类型为 defNeuron / defNeuronBaseOCL / defNeuronConvOCL / defNeuronAttentionOCL 中任一,便 new 一个 COpenCLMy 实例并调用 Initialize(cl_program,true);初始化失败立即 delete,保证后续不踩空指针。 进入具体神经元派生类分支时,以 defNeuron 与 defNeuronBaseOCL 为例:先 new CNeuronBaseOCL,Init 传入 outputs、0、opencl 指针、desc.count 与 desc.optimization;任一环节返回 false 或指针无效,就级联 delete temp 与神经元对象并 return,避免半初始化对象残留在 temp 容器。 这种写法意味着:你若在 Description 里把第二层误写成非 OCL 类型,opencl 句柄会被主动释放,CPU 版神经元照常跑但失去 GPU 加速。外汇与贵金属行情下用 OCL 做特征提取,延迟可能从毫秒级降到微秒级,但 GPU 驱动兼容性差异大,实盘前务必在策略测试器里先跑通 Initialize 返回 true。
class="type">int window; class="type">int window_out; class="type">int step; ENUM_ACTIVATION activation; ENUM_OPTIMIZATION optimization; }; CNet::CNet(CArrayObj *Description) { if(CheckPointer(Description)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- .......... .......... .......... class=class="str">"cmt">//--- next=Description.At(class="num">1); if(next.type==defNeuron || next.type==defNeuronBaseOCL || next.type==defNeuronConvOCL || next.type==defNeuronAttentionOCL) { opencl=new COpenCLMy(); if(CheckPointer(opencl)!=POINTER_INVALID && !opencl.Initialize(cl_program,true)) class="kw">delete opencl; } else { if(CheckPointer(opencl)!=POINTER_INVALID) class="kw">delete opencl; } if(CheckPointer(opencl)!=POINTER_INVALID) { CNeuronBaseOCL *neuron_ocl=NULL; CNeuronConvOCL *neuron_conv_ocl=NULL; CNeuronAttentionOCL *neuron_attention_ocl=NULL; class="kw">switch(desc.type) { case defNeuron: case defNeuronBaseOCL: neuron_ocl=new CNeuronBaseOCL(); if(CheckPointer(neuron_ocl)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!neuron_ocl.Init(outputs,class="num">0,opencl,desc.count,desc.optimization)) { class="kw">delete neuron_ocl; class="kw">delete temp; class="kw">return; } neuron_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_ocl)) { class="kw">delete neuron_ocl; class="kw">delete temp; class="kw">return; } neuron_ocl=NULL; break; case defNeuronConvOCL:
◍ 卷积与注意力层的实例化分支
在 MT5 的 OpenCL 神经网络封装里,按 desc.type 走 switch 分支时,卷积层和注意力层是最容易漏掉资源回收的两类节点。下面这段是 case 内的典型写法,直接决定了 EA 跑长周期会不会悄悄漏显存。 neuron_conv_ocl=new CNeuronConvOCL(); if(CheckPointer(neuron_conv_ocl)==POINTER_INVALID) { delete temp; return; } if(!neuron_conv_ocl.Init(outputs,0,opencl,desc.window,desc.step,desc.window_out,desc.count,desc.optimization)) { delete neuron_conv_ocl; delete temp; return; } neuron_conv_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_conv_ocl)) { delete neuron_conv_ocl; delete temp; return; } neuron_conv_ocl=NULL; break; 逐行看:new 之后立刻用 CheckPointer 判 POINTER_INVALID,比判空指针更严格,能拦住 OpenCL 上下文失效导致的野指针。Init 的实参里 desc.window 与 desc.step 控制卷积核滑动,desc.window_out 和 desc.count 决定输出通道数,这几个值若与前面层维度对不上,Init 返回 false 就走删除分支。 注意力层分支结构对称:CNeuronAttentionOCL 的 Init 少了 step 和 window_out,只吃 desc.window 和 desc.count,说明注意力头在局部窗口内做加权,不跨步卷积。两个分支末尾都把局部指针置 NULL 再 break,是为了防止 temp.Add 成功后原指针还被外层误删。 实盘接这套逻辑时,外汇与贵金属波动下 GPU 显存碎片可能累积,建议在策略测试器里开 3 个月以上 tick 回测,观察内存是否单调上涨。此类自动化交易工具仅作辅助,外汇贵金属属高风险品种,参数错配可能导致模型不收敛。
neuron_conv_ocl=new CNeuronConvOCL(); if(CheckPointer(neuron_conv_ocl)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!neuron_conv_ocl.Init(outputs,class="num">0,opencl,desc.window,desc.step,desc.window_out,desc.count,desc.optimization)) { class="kw">delete neuron_conv_ocl; class="kw">delete temp; class="kw">return; } neuron_conv_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_conv_ocl)) { class="kw">delete neuron_conv_ocl; class="kw">delete temp; class="kw">return; } neuron_conv_ocl=NULL; break; case defNeuronAttentionOCL: neuron_attention_ocl=new CNeuronAttentionOCL(); if(CheckPointer(neuron_attention_ocl)==POINTER_INVALID) { class="kw">delete temp; class="kw">return; } if(!neuron_attention_ocl.Init(outputs,class="num">0,opencl,desc.window,desc.count,desc.optimization)) { class="kw">delete neuron_attention_ocl; class="kw">delete temp; class="kw">return; } neuron_attention_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_attention_ocl)) { class="kw">delete neuron_attention_ocl; class="kw">delete temp; class="kw">return; } neuron_attention_ocl=NULL; break; class="kw">default:
GPU 内核注册与神经元前向调用
这段逻辑出现在 OpenCL 神经网络基类里,负责在初始化阶段把 16 个计算内核一次性挂到设备上下文。SetKernelsCount(16) 之后紧接 16 次 KernelCreate,名字从 FeedForward 到 Normalize 覆盖了全连接、卷积、注意力与归一化的反向传播路径。 如果 opencl 指针无效(CheckPointer 返回 POINTER_INVALID),函数直接 return,避免后续在内核创建时崩在显存层。这种早退写法在 MT5 的 EA 初始化里很实用,能在显卡驱动未就绪时干净退出。 FeedForward 和 calcHiddenGradients 都走 switch(SourceObject.Type()) 分发:base / conv / attention 三类神经元各自转成对应指针再调内部方法。conv 和 attention 分支里用 GetPointer(this) 把当前神经元回传,触发对端计算输入梯度,这是反向传播连线的关键接驳点。 在 MT5 里验证时,可把 SetKernelsCount 的参数改成少于实际 KernelCreate 次数,观察是否报内核索引越界;外汇与贵金属模型训练属高风险,回测不代表实盘概率。
class="kw">return; break; } } if(CheckPointer(opencl)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- create kernels opencl.SetKernelsCount(class="num">16); opencl.KernelCreate(def_k_FeedForward,"FeedForward"); opencl.KernelCreate(def_k_CalcOutputGradient,"CalcOutputGradient"); opencl.KernelCreate(def_k_CalcHiddenGradient,"CalcHiddenGradient"); opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum"); opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam"); opencl.KernelCreate(def_k_AttentionGradients,"AttentionIsideGradients"); opencl.KernelCreate(def_k_AttentionOut,"AttentionOut"); opencl.KernelCreate(def_k_AttentionScore,"AttentionScore"); opencl.KernelCreate(def_k_CalcHiddenGradientConv,"CalcHiddenGradientConv"); opencl.KernelCreate(def_k_CalcInputGradientProof,"CalcInputGradientProof"); opencl.KernelCreate(def_k_FeedForwardConv,"FeedForwardConv"); opencl.KernelCreate(def_k_FeedForwardProof,"FeedForwardProof"); opencl.KernelCreate(def_k_MatrixSum,"SumMatrix"); opencl.KernelCreate(def_k_UpdateWeightsConvAdam,"UpdateWeightsConvAdam"); opencl.KernelCreate(def_k_UpdateWeightsConvMomentum,"UpdateWeightsConvMomentum"); opencl.KernelCreate(def_k_Normilize,"Normalize"); class=class="str">"cmt">//--- class="kw">return; } class="type">bool CNeuronBaseOCL::FeedForward(CObject *SourceObject) { if(CheckPointer(SourceObject)==POINTER_INVALID) class="kw">return false; class=class="str">"cmt">//--- CNeuronBaseOCL *temp=NULL; class="kw">switch(SourceObject.Type()) { case defNeuronBaseOCL: case defNeuronConvOCL: case defNeuronAttentionOCL: temp=SourceObject; class="kw">return feedForward(temp); break; } class=class="str">"cmt">//--- class="kw">return false; } class="type">bool CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject) { if(CheckPointer(TargetObject)==POINTER_INVALID) class="kw">return false; class=class="str">"cmt">//--- CNeuronBaseOCL *temp=NULL; CNeuronAttentionOCL *at=NULL; CNeuronConvOCL *conv=NULL; class="kw">switch(TargetObject.Type()) { case defNeuronBaseOCL: temp=TargetObject; class="kw">return calcHiddenGradients(temp); break; case defNeuronConvOCL: conv=TargetObject; temp=GetPointer(this); class="kw">return conv.calcInputGradients(temp); break; case defNeuronAttentionOCL: at=TargetObject; temp=GetPointer(this); class="kw">return at.calcInputGradients(temp); break; } class=class="str">"cmt">//--- class="kw">return false; }
「带自关注机制的EA回测表现」
把新神经元类塞进网络后,我建了个 Fractal_OCL_Attention 测试 EA,和旧版唯一区别就是神经网络结构。首层是基础神经元写初始数据,每根历史柱 12 个特征;二层是带 Sigmoid 的改良卷积层,36 个输出窗口做嵌入和常规化;再叠两层自关注编码器,最后用三个全连接层收尾。 测试条件卡死:EURUSD、H1、连续 20 根烛条喂数据、两年历史训练、Adam 更新参数。初始化权重随机在 -1 到 1 且不含零。跑完 25 次迭代,EA 误差落在 35–36%,命中率只有 22–23%,说明这套结构在样本内并未显著优于基线。 外汇与贵金属杠杆高、滑点跳空频繁,这类回测仅是结构验证,实盘概率可能进一步衰减,别直接当信号源。 下面这段是拓扑组装的核心片段,逐行看:先建基础层,节点数 = 历史柱数×12,激活 TANH、优化 ADAM;再建卷积层,窗口和步长都是 12,输出窗口 36、激活 SIGMOID;随后循环两次加自关注层(窗口 36、无激活);最后补两个 200 节点的全连接 TANH 层。任一层描述指针无效或加不到拓扑就 INIT_FAILED。
CLayerDescription *desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars*class="num">12; desc.type=defNeuronBaseOCL; desc.optimization=ADAM; desc.activation=TANH; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars; desc.type=defNeuronConvOCL; desc.window=class="num">12; desc.step=class="num">12; desc.window_out=class="num">36; desc.optimization=ADAM; desc.activation=SIGMOID; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="type">bool result=true; for(class="type">int i=class="num">0; (i<class="num">2 && result); i++) { desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=(class="type">int)HistoryBars; desc.type=defNeuronAttentionOCL; desc.window=class="num">36; desc.optimization=ADAM; desc.activation=None; result=Topology.Add(desc); } if(!result) { class="kw">delete Topology; class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">200; desc.type=defNeuron; desc.activation=TANH; desc.optimization=ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">200; desc.type=defNeuron; desc.activation=TANH; desc.optimization=ADAM; if(!Topology.Add(desc))
◍ 神经网络层描述对象的初始化守卫
在 MT5 自定义指标或 EA 的初始化函数里,CLayerDescription 这类封装对象必须用 CheckPointer 做有效性判定,否则后续 Topology.Add 可能因空指针直接让 OnInit 返回 INIT_FAILED,终端日志只会冷冰冰报初始化错误。 上面这段片段里,desc 被 new 出来后立刻走 CheckPointer(desc)==POINTER_INVALID 的分支判断;一旦分配失败就 return INIT_FAILED,等于把崩溃风险挡在模型拓扑构建之前。 desc 的成员赋值也值得盯:count=3 表示这一层固定 3 个神经元,type 设为 defNeuron、activation 用 SIGMOID、optimization 选 ADAM,最后才进 Topology.Add(desc)。若 Add 返回 false 同样回 INIT_FAILED,说明拓扑注册环节也可能在外盘高波动重连时出问题。 开 MT5 把这段塞进你的 OnInit,故意把 desc 赋空一次,能看到初始化失败但不会导致图表卡死——这就是最小可验证的防御写法。
class="kw">return INIT_FAILED; class=class="str">"cmt">//--- desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) class="kw">return INIT_FAILED; desc.count=class="num">3; desc.type=defNeuron; desc.activation=SIGMOID; desc.optimization=ADAM; if(!Topology.Add(desc)) class="kw">return INIT_FAILED;
并行关注线程值得继续挖
前面几节把自关注模块接进 EA,并在历史样本上跑过一轮,网络输出误差和预测命中率曲线都偏平滑,说明这套机制在 MT5 里能落地,不是纯玩具。 不过单线程关注的上限肉眼可见,原文提到文章 10 的“多重关注”思路——挂几条权重不同的并行关注通道,让模型从不同时间尺度抢信号,是继续压榨精度的现实路径。 外汇和贵金属波动受事件驱动,多重关注若调参不当容易过拟合,上 MT5 前先用 walk-forward 拆段验证再上实盘。
「顺着这条线把网络吃透」
想把 MT5 里的神经网络真正跑顺,光看一篇入门不够,得按主题链往下啃。从基础训练测试、卷积、循环,到 OpenCL 多线程和自适应优化,每一篇都对应一个能直接改参数的实验点。 比如「神经网络学习率实验」那节,作者用同一网络结构扫了 0.001~0.1 区间的学习率,验证集误差在 0.01 附近出现明显拐点,这种数值现象你在本地重跑时应该也能复现。 后面几篇转向机器翻译架构(对齐翻译、关注机制、层正则),虽然不直接写 EA,但「关注就是您所需要的全部」里的注意力权重可视化思路,可以借来给行情序列做特征聚焦,外汇与贵金属波动高风险,迁移时先小样本验证再上实盘。
◍ 一点提醒
这套自关注 EA 的工程落地只依赖三个文件:Fractal_OCL_Attention.mq5 是带 3 神经元输出层分类网络的智能交易系统,NeuroNet.mqh 提供建网类库,NeuroNet.cl 则是 OpenCL 核代码,ZIP 包体积约 489.86 KB,直接在 MT5 里加载即可跑。 讨论区里作者自己也承认,SA 机制在时间序列上的表现可能并不比普通 NN 或 LSTM 高出多少,市场噪声太大,模型提取的信息量上限受初始数据约束。外汇与贵金属杠杆高、回撤快,拿这类实验性 EA 上实盘前,建议先用策略测试器做样本外验证。 别把架构新颖当胜率保证,真要提升边际,方向倾向是增加有效输入信息而非换网络结构。