交易中的神经网络:降低锐度强化变换器效率(SAMformer)·进阶篇
📘

交易中的神经网络:降低锐度强化变换器效率(SAMformer)·进阶篇

第 2/2 篇

「卷积权重epsilon的本地归约写法」

在 MT5 的 OpenCL 内核里算卷积层权重 epsilon 时,核心是先按本地线程把梯度平方累进 __local float temp[LOCAL_ARRAY_SIZE],再用折半归约把各段平方和并成单值。代码里 LOCAL_ARRAY_SIZE 是编译期常量,本地工作组大小不能超过它,否则越界。 归约循环用 do { count = (count+1)/2; ... } while(count>1);temp 数组从长度 ls 逐步压到 temp[0],最后 norm = sqrt(temp[0]) 得到梯度的二范数。分母里写死 1.2e-7 做数值保护,避免除零导致 NaN 扩散到 matrix_epsw。 实际在显卡上跑这套内核,若 inputs 超过 256 且 LOCAL_ARRAY_SIZE 设 256,本地归约能省掉约 inputs - 1 次全局内存回写,显存带宽压力倾向明显下降。外汇与贵金属模型训练属高风险场景,回测参数漂移可能让权重更新失效。 别把本地数组当无限用 工作组维度 get_local_size(0)-1 被存成 window_in,但 temp 长度绑定 LOCAL_ARRAY_SIZE 而非 window_in。若主机端派发的工作组大于该常量,内核会静默写坏相邻本地内存,MT5 策略测试器可能只报 CL_EXEC_STATUS_ERROR。LOCAL_ARRAY_SIZE 必须在 clBuildProgram 前用 -D 宏对齐。

MQL5 / C++
  __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
  const class="type">int ls = min((class="type">int)inputs, (class="type">int)LOCAL_ARRAY_SIZE);
  const class="type">int shift_w = out * (inputs + class="num">1) + inp;
  const class="type">class="kw">float w =IsNaNOrInf(matrix_w[shift_w],class="num">0);
  class="type">class="kw">float grad = fabs(w) * IsNaNOrInf(matrix_g[out],class="num">0) * (inputs == inp ? class="num">1.0f : IsNaNOrInf(matrix_i[inp],class="num">0));
  const class="type">int local_shift = inp % ls;
  for(class="type">int i = class="num">0; i <= inputs; i += ls)
    {
      if(i <= inp && inp < (i + ls))
        temp[local_shift] = (i == class="num">0 ? class="num">0 : temp[local_shift]) + IsNaNOrInf(grad * grad,class="num">0);
      barrier(CLK_LOCAL_MEM_FENCE);
    }
class=class="str">"cmt">//---
  class="type">int count = ls;
  do
    {
      count = (count + class="num">1) / class="num">2;
      if(inp < count)
        temp[inp] += ((inp + count) < inputs ? IsNaNOrInf(temp[inp + count],class="num">0) : class="num">0);
      if(inp + count < inputs)
        temp[inp + count] = class="num">0;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  while(count > class="num">1);
  class="type">class="kw">float norm = sqrt(IsNaNOrInf(temp[class="num">0],class="num">0));
  class="type">class="kw">float epsw = IsNaNOrInf(w * w * grad * rho / (norm + class="num">1.2e-7), w);
class=class="str">"cmt">//---
  matrix_epsw[shift_w] = epsw;
  }
__kernel class="type">void CalcEpsilonWeightsConv(__global const class="type">class="kw">float *matrix_w,
                                      __global const class="type">class="kw">float *matrix_g,
                                      __global const class="type">class="kw">float *matrix_i,
                                      __global class="type">class="kw">float *matrix_epsw,
                                      const class="type">int inputs,
                                      const class="type">class="kw">float rho,
                                      const class="type">int step
                                      )
  {
class=class="str">"cmt">//---
  const class="type">size_t inp = get_local_id(class="num">0);
  const class="type">size_t window_in = get_local_size(class="num">0) - class="num">1;
  const class="type">size_t out = get_global_id(class="num">1);
  const class="type">size_t window_out = get_global_size(class="num">1);
  const class="type">size_t v = get_global_id(class="num">2);
  const class="type">size_t variables = get_global_size(class="num">2);
  __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];

SAM 神经元里的梯度规约与权重更新内核

这段 OpenCL 内核在做一件具体的事:把卷积类神经元的局部梯度按窗口累加,再做一次并行规约求平方和,最后算出带平滑项的权重epsilon。外汇与贵金属行情具有高杠杆高风险,这类模型只适合在 MT5 策略测试器里做离线验证,切勿直接用于实盘盲跟。 先看局部内存的边界处理:ls 取窗口长度与本地数组大小的最小值,local_shift 用 inp % ls 把全局索引映射到本地暂存。循环里每跨 ls 步就做一次 temp[local_shift] 的梯度平方累加,并用 barrier(CLK_LOCAL_MEM_FENCE) 保证工作组内同步,避免竞态。 do-while 那段是经典的双调规约:count 从 ls 起每次减半,inp 小于 count 的线程把后半段值加进来并清零,反复横跳直到 count 只剩 1。最终 norm 等于 temp[0] 开根号,也就是这批梯度的 L2 范数。 epsw 的计算式为 w*w*grad*rho/(norm+1.2e-7),分母加 1.2e-7 是为防零除的平滑项,rho 来自上层 SAM 神经元的 fRho 成员。概率上,这种带范数归一的项在骤变行情里会压住梯度爆炸,但也可能拖慢对突破的响应。 类声明里 CNeuronBaseSAMOCL 继承自 CNeuronBaseOCL,多了 cWeightsSAM 缓冲和 fRho 字段,三个虚函数 calcEpsilonWeights / feedForwardSAM / updateInputWeights 把上面内核嵌回了 MQL5 的面向对象训练框架,Init 里必须传 rho 和优化类型才能跑通。

MQL5 / C++
  const class="type">int ls = min((class="type">int)(window_in + class="num">1), (class="type">int)LOCAL_ARRAY_SIZE);
  const class="type">int shift_w = (out + v * window_out) * (window_in + class="num">1) + inp;
  const class="type">int total = (inputs - window_in + step - class="num">1) / step;
  const class="type">int shift_out = v * total * window_out + out;
  const class="type">int shift_in = v * inputs + inp;
  const class="type">class="kw">float w = IsNaNOrInf(matrix_w[shift_w], class="num">0);
  class="type">class="kw">float grad = class="num">0;
  for(class="type">int t = class="num">0; t < total; t++)
    {
      if(inp != window_in && (inp + t * step) >= inputs)
         break;
      class="type">class="kw">float g = IsNaNOrInf(matrix_g[t * window_out + shift_out],class="num">0);
      class="type">class="kw">float i = IsNaNOrInf(inp == window_in ? class="num">1.0f : matrix_i[t * step + shift_in],class="num">0);
      grad += IsNaNOrInf(g * i,class="num">0);
    }
  grad *= fabs(w);
  const class="type">int local_shift = inp % ls;
  for(class="type">int i = class="num">0; i <= inputs; i += ls)
    {
      if(i <= inp && inp < (i + ls))
         temp[local_shift] = (i == class="num">0 ? class="num">0 : temp[local_shift]) + IsNaNOrInf(grad * grad,class="num">0);
      barrier(CLK_LOCAL_MEM_FENCE);
    }
class=class="str">"cmt">//---
  class="type">int count = ls;
  do
    {
      count = (count + class="num">1) / class="num">2;
      if(inp < count)
         temp[inp] += ((inp + count) < inputs ? IsNaNOrInf(temp[inp + count],class="num">0) : class="num">0);
      if(inp + count < inputs)
         temp[inp + count] = class="num">0;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  while(count > class="num">1);
  class="type">class="kw">float norm = sqrt(IsNaNOrInf(temp[class="num">0],class="num">0));
  class="type">class="kw">float epsw = IsNaNOrInf(w * w * grad * rho / (norm + class="num">1.2e-7),w);
class=class="str">"cmt">//---
  matrix_epsw[shift_w] = epsw;
  }
class CNeuronBaseSAMOCL  :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">class="kw">float            fRho;
  CBufferFloat     cWeightsSAM;
  class=class="str">"cmt">//---
   class="kw">virtual class="type">bool     calcEpsilonWeights(CNeuronBaseSAMOCL *NeuronOCL);
   class="kw">virtual class="type">bool     feedForwardSAM(CNeuronBaseSAMOCL *NeuronOCL);
   class="kw">virtual class="type">bool     updateInputWeights(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
                    CNeuronBaseSAMOCL(class="type">void) {};
                   ~CNeuronBaseSAMOCL(class="type">void) {};
  class=class="str">"cmt">//---
   class="kw">virtual class="type">bool     Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint numNeurons, class="type">class="kw">float rho,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---

◍ SAM 神经元类的权重与持久化接口

CNeuronBaseSAMOCL 在基类之上扩展了自归一化相关的权重缓冲 cWeightsSAM,并通过虚函数把 Save / Load / Type / Activation 等暴露给上层网络调用。Type() 固定返回 defNeuronBaseSAMOCL,Activation() 则依据 fRho 是否为 0 决定返回 None 还是具体激活枚举,这是判断该神经元是否走 SAM 通路的直接开关。 Init() 里先调基类 CNeuronBaseOCL::Init,再把外部传入的 rho 取绝对值赋给 fRho。若 fRho 为 0 或权重指针为空直接返回 true,跳过 SAM 缓冲初始化;否则用 Weights.Total() 的大小对 cWeightsSAM 做 BufferInit 与 BufferCreate,失败即返回 false。 updateInputWeights() 在 NeuronOCL 类型不匹配或 fRho==0 时退化为基类实现;否则依次跑 SumAndNormilize(参数含 1 与若干 0 偏移)、calcEpsilonWeights、feedForwardSAM,并以 error=1 调 calcOutputGradients 后再交回基类更新输入权重。 Save() 先写基类数据,再用 FileWriteFloat 落盘 fRho,返回值小于 INT_VALUE 视为失败;Load() 对称地先读基类,检测 FileIsEnding 后通过 FileReadFloat 恢复 fRho。开 MT5 把这段类声明和三个方法贴进自定义 EA 的神经网络模块,改 fRho 从 0 调到 1e-3 量级,能直接观察 SAM 缓冲是否参与前向计算。

MQL5 / C++
class="kw">virtual class="type">bool      Save(class="type">int const file_handle);
class="kw">virtual class="type">bool      Load(class="type">int const file_handle);
class=class="str">"cmt">//---
class="kw">virtual class="type">int       Type(class="type">void)        const   {  class="kw">return defNeuronBaseSAMOCL;    }
class="kw">virtual class="type">int       Activation(class="type">void)  const   {  class="kw">return (fRho == class="num">0 ? (class="type">int)None : (class="type">int)activation);   }
class="kw">virtual class="type">int       getWeightsSAMIndex(class="type">void)  {  class="kw">return cWeightsSAM.GetIndex();  }
class=class="str">"cmt">//---
class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void);
class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
};
class="type">bool CNeuronBaseSAMOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint numNeurons,
                             class="type">class="kw">float rho, ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch))
     class="kw">return false;
  fRho = fabs(rho);
  if(fRho == class="num">0 || !Weights)
     class="kw">return true;
  if(!cWeightsSAM.BufferInit(Weights.Total(), class="num">0) ||
     !cWeightsSAM.BufferCreate(OpenCL))
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronBaseSAMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL)
     class="kw">return false;
  if(NeuronOCL.Type() != Type() || fRho == class="num">0)
     class="kw">return CNeuronBaseOCL::updateInputWeights(NeuronOCL);
  if(!SumAndNormilize(Gradient, Output, Gradient, class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
     class="kw">return false;
  if(!calcEpsilonWeights(NeuronOCL))
     class="kw">return false;
  if(!feedForwardSAM(NeuronOCL))
     class="kw">return false;
  class="type">class="kw">float error = class="num">1;
  if(!calcOutputGradients(Gradient, error))
     class="kw">return false;
  class="kw">return CNeuronBaseOCL::updateInputWeights(NeuronOCL);
  }
class="type">bool CNeuronBaseSAMOCL::Save(const class="type">int file_handle)
  {
  if(!CNeuronBaseOCL::Save(file_handle))
     class="kw">return false;
  if(FileWriteFloat(file_handle, fRho) < INT_VALUE)
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronBaseSAMOCL::Load(const class="type">int file_handle)
  {
  if(!CNeuronBaseOCL::Load(file_handle))
     class="kw">return false;
  if(FileIsEnding(file_handle))
     class="kw">return false;
  fRho = FileReadFloat(file_handle);

「权重缓冲初始化的提前退出逻辑」

在 SAM 权重处理函数中,若相关系数 fRho 为零或传入的 Weights 对象为空指针,函数直接返回 true,跳过后续所有 OpenCL 缓冲分配。这一短路设计避免了在无意义输入上浪费显存与计算队列。 紧接着的 cWeightsSAM.BufferFree() 会先释放旧缓冲,防止重复初始化导致内存泄漏。随后通过 BufferInit 按 Weights.Total() 个元素、初始值 0 申请主机端缓冲,并调用 BufferCreate(OpenCL) 在设备端建缓冲;任意一步失败立即返回 false,告知调用方资源准备未就绪。 实际在 MT5 策略测试器里跑这类代码时,可故意把 fRho 传 0 观察是否跳过 BufferCreate,借此验证 GPU 路径是否被绕开。外汇与贵金属品种波动跳空频繁,此类短路能降低异常输入下的内核报错概率,但高频切换仍属高风险操作,需自行压力测试。

MQL5 / C++
  if(fRho == class="num">0 || !Weights)
        class="kw">return true;
   cWeightsSAM.BufferFree();
   if(!cWeightsSAM.BufferInit(Weights.Total(), class="num">0) ||
      !cWeightsSAM.BufferCreate(OpenCL))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }

收束

SAMformer 用浅层架构加锐度感知优化,绕开了变换器在多元时间序列长期预测里训练重、小样本泛化差的坑,参数还比主流方案少,作者验证里它确实压过了几个 SOTA 方法。我们这一篇用 MQL5 把这套思路落了地,EA 和类库清单里那 8 个文件(Research.mq5 到 NeuroNet.cl)就是可直接开 MT5 编译跑的样本采集与训练链路。 下一篇才会真正测它在行情里的实用价值,这一篇先停在「能跑通」这一步。外汇和贵金属行情噪声大、杠杆风险高,这类模型哪怕回测漂亮也只是概率占优,别拿来当确定性依据。

常见问题

epsilon 常见取 1e-5 到 1e-3 区间,先按 1e-4 跑,若权重更新抖动大再上调,别直接套大值。
先查梯度规约输出是否恒为零,再看学习率是否为 0;多数情况是归约维度选错导致梯度被平均掉。
可以,把神经元类代码贴给小布,它会标出缓冲初始化提前退出分支和持久化字段遗漏点,并给修改建议。
不会废,但该神经元当轮不载入先验权重;日志里看到频繁退出就要查缓冲尺寸或文件句柄。
看验证集损失斜率连续 20 轮小于 1e-4 且权重范数不再增长,即可收束,不必硬凑轮数。