神经网络在交易中的应用:多元时间序列的双重聚类(DUET)·进阶篇
📘

神经网络在交易中的应用:多元时间序列的双重聚类(DUET)·进阶篇

第 2/2 篇

◍ Top-K 门控的 OpenCL 核函数实现

在 MT5 的 OpenCL 神经层里,Top-K 门控靠两个核函数配合:前向 TopKgates 算每个样本在窗口内的相对 logit,反向 TopKgatesGrad 只把被选中的位置回传梯度。下面这段是前向里最硬的一段——用局部数组做分块 Top-K 比较。

MQL5 / C++
const class="type">int shift_gate = var * window + idx;
class="type">class="kw">float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE);
class="type">class="kw">float noise = IsNaNOrInf(noises[shift_gate], class="num">0);
if(noise != class="num">0)
  {
  noise *= Activation(inputs[shift_std], class="num">3);
  logit += IsNaNOrInf(noise, class="num">0);
  }
__local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
class=class="str">"cmt">//---
const class="type">uint ls = min((class="type">uint)window, (class="type">uint)LOCAL_ARRAY_SIZE);
class="type">uint bigger = class="num">0;
class="type">class="kw">float max_logit = logit;
class=class="str">"cmt">//--- Top K
class="macro">#pragma unroll
for(class="type">int i = class="num">0; i < window; i += ls)
  {
  if(idx >= i && idx < (i + ls))
    temp[idx % ls] = logit;
  barrier(CLK_LOCAL_MEM_FENCE);
  for(class="type">int i1 = class="num">0; (i1 < min((class="type">int)ls,(class="type">int)(window-i)) && bigger <= k); i1++)
    {
    if(temp[i1] > logit)
      bigger++;
    if(temp[i1] > max_logit)
      max_logit = temp[i1];
    }
  barrier(CLK_LOCAL_MEM_FENCE);
  }
if(bigger <= k)
  gates[shift_gate] = logit - max_logit;
else
  gates[shift_gate] = MIN_VALUE;
逐行看:shift_gate 把二维 (var, idx) 摊平成一维门控下标;logit 先取主输入,遇 NaN/Inf 落回 MIN_VALUE 避免核崩溃。noise 非零时乘上 std 通道经激活函数(参数 3)后的结果,相当于给 logit 加噪,这是稀疏门控随机性的来源。 局部数组 temp 大小受 LOCAL_ARRAY_SIZE 限制,循环按块长 ls 搬运,每个 workgroup 内用 barrier(CLK_LOCAL_MEM_FENCE) 保证读写不打架。内层统计 bigger(比自己大的个数)和 max_logit,最后若 bigger<=k 保留 logit-max_logit(SoftMax 前的偏移),否则写 MIN_VALUE 直接屏蔽——这就是 Top-K 稀疏化的落点。 反向核里 gate<=MIN_VALUE 的位置梯度直接置零并返回,说明被屏蔽的样本不参与权重更新;有噪声的项还要过 Deactivation 反传 std 通道梯度。开 MT5 把 kLOCAL_ARRAY_SIZE 调小(如 k=2、LOCAL_ARRAY_SIZE=64),在 EURUSD 的 M15 上跑一遍能直观看到门控稀疏率随 window 上升而下降。外汇与贵金属杠杆高,这类实验请先用模拟盘验证。

MQL5 / C++
const class="type">int shift_gate = var * window + idx;
class="type">class="kw">float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE);
class="type">class="kw">float noise = IsNaNOrInf(noises[shift_gate], class="num">0);
if(noise != class="num">0)
  {
  noise *= Activation(inputs[shift_std], class="num">3);
  logit += IsNaNOrInf(noise, class="num">0);
  }
__local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
class=class="str">"cmt">//---
const class="type">uint ls = min((class="type">uint)window, (class="type">uint)LOCAL_ARRAY_SIZE);
class="type">uint bigger = class="num">0;
class="type">class="kw">float max_logit = logit;
class=class="str">"cmt">//--- Top K
class="macro">#pragma unroll
for(class="type">int i = class="num">0; i < window; i += ls)
  {
  if(idx >= i && idx < (i + ls))
    temp[idx % ls] = logit;
  barrier(CLK_LOCAL_MEM_FENCE);
  for(class="type">int i1 = class="num">0; (i1 < min((class="type">int)ls,(class="type">int)(window-i)) && bigger <= k); i1++)
    {
    if(temp[i1] > logit)
      bigger++;
    if(temp[i1] > max_logit)
      max_logit = temp[i1];
    }
  barrier(CLK_LOCAL_MEM_FENCE);
  }
if(bigger <= k)
  gates[shift_gate] = logit - max_logit;
else
  gates[shift_gate] = MIN_VALUE;

「Top-K 门控神经元的类骨架与初始化落点」

在 MT5 的 OpenCL 神经网络扩展里,CNeuronTopKGates 这个类把多头稀疏门控塞进了一层神经元。protected 段先挂了几个核心成员:iK 记录 top-k 数值,cbNoise 是浮点缓冲用来做噪声扰动,cProjection 是卷积投影层,cGates 是基元门控层,后面三个虚函数 TopKgates / TopKgatesGradient / feedForward 等构成前向和梯度更新的重载链。 从 GetGates 能直接读出关系:门控数量等于投影层滤波器数除以 2,即 cProjection.GetFilters()/2;GetUnits 则直接返回投影层的单元数。这意味着你在调参时,gates 和 units_count 会经 2*gates 的滤波器倍数被卷积层吸收,改一个会影响另一头的宽度。 Init 函数是实打实的装配现场。它先以 gates*units_count 的总宽度调父类 CNeuronSoftMaxOCL 的初始化,再 SetHeads(units_count);随后 cProjection 以 window×window 卷积、2*gates 滤波器、units_count 单元、激活函数置为 None 起手。若 cbNoise 缓冲初始化或 OpenCL 创建失败,整层直接返回 false,模型编译会断在这一行。 开 MT5 验证时,可以把 defNeuronTopKGates 类型的层丢进自己的 EA 神经网络结构,故意把 gates 设成奇数看 GetGates 向下取整的副作用,再断点跟 Init 里的 cProjection.Init 返回码,能少走很多弯路。外汇与贵金属行情下用这类 GPU 网络做推理,仍属高风险,过拟合和实时滑点都可能吃掉回测优势。

MQL5 / C++
class="kw">protected:
  class="type">int                iK;
  CBufferFloat      cbNoise;
  CNeuronConvOCL    cProjection;
  CNeuronBaseOCL    cGates;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      TopKgates(class="type">void);
  class="kw">virtual class="type">bool      TopKgatesGradient(class="type">void);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                    CNeuronTopKGates(class="type">void) {};
                   ~CNeuronTopKGates(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 window, class="type">uint units_count, class="type">uint gates, class="type">uint top_k,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void) class="kw">override  const   { class="kw">return defNeuronTopKGates; }
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Save(class="type">int const file_handle) class="kw">override;
  class="kw">virtual class="type">bool      Load(class="type">int const file_handle) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj) class="kw">override;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">uint      GetGates(class="type">void) const { class="kw">return cProjection.GetFilters() / class="num">2; }
  class="kw">virtual class="type">uint      GetUnits(class="type">void) const { class="kw">return cProjection.GetUnits(); }
  };
class="type">bool CNeuronTopKGates::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
class="type">uint window, class="type">uint units_count, class="type">uint gates, class="type">uint top_k,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronSoftMaxOCL::Init(numOutputs, myIndex, open_cl, gates * units_count,
optimization_type, batch))
     class="kw">return class="kw">false;
  SetHeads(units_count);
  if(!cProjection.Init(class="num">0, class="num">0, OpenCL, window, window, class="num">2 * gates, units_count, class="num">1, optimization, iBatch))
     class="kw">return class="kw">false;
  cProjection.SetActivationFunction(None);
  if(!cbNoise.BufferInit(Neurons(), class="num">0) ||
     !cbNoise.BufferCreate(OpenCL))

MoE 门控与训练噪声的实现细节

这段 CNeuronTopKGates 的 feedForward 展示了混合专家(MoE)在 MT5 神经网络层里的前向逻辑:训练态下用标准正态分布给门控加噪,推理态则直接填零。 具体看,bTrain 为真时调用 MathRandomNormal(0,1,Neurons(),random) 生成均值为 0、方差为 1 的随机噪声数组,长度等于神经元数;写入 cbNoise 缓冲后参与 TopKgates 筛选。若不在训练,则 cbNoise.Fill(0) 关闭噪声,保证线上推断稳定。 CNeuronMoE 类把 cGates 与 cExperts 封装在一起,Init 参数里 experts 和 top_k 直接决定专家总数与每次激活的专家数。想在 MT5 验证,可把 top_k 设为 2、experts 设为 8,观察前向输出是否只走两个子网络。外汇与贵金属行情高波动,此类模型仅作概率参考,实盘须控仓。

MQL5 / C++
   class="kw">return class="kw">false;
   if(!cGates.Init(class="num">0, class="num">1, OpenCL, Neurons(), optimization, iBatch))
      class="kw">return class="kw">false;
   cGates.SetActivationFunction(None);
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronTopKGates::feedForward(CNeuronBaseOCL *NeuronOCL)
   {
   if(!cProjection.FeedForward(NeuronOCL))
      class="kw">return class="kw">false;
   if(bTrain)
     {
      class="type">class="kw">double random[];
      if(!Math::MathRandomNormal(class="num">0, class="num">1, Neurons(), random))
         class="kw">return class="kw">false;
      if(!cbNoise.AssignArray(random))
         class="kw">return class="kw">false;
      if(!cbNoise.BufferWrite())
         class="kw">return class="kw">false;
     }
   else
      if(!cbNoise.Fill(class="num">0))
         class="kw">return class="kw">false;
   if(!TopKgates())
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return CNeuronSoftMaxOCL::feedForward(cGates.AsObject());
   }
class CNeuronMoE   :  class="kw">public CNeuronBaseOCL
   {
class="kw">protected:
   CNeuronTopKGates     cGates;
   CLayer               cExperts;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool        calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                     CNeuronMoE(class="type">void) {};
                     ~CNeuronMoE(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 window, class="type">uint window_out, class="type">uint units_count,
                     class="type">uint experts, class="type">uint top_k,
                     ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int         Type(class="type">void) class="kw">override  const   {  class="kw">return defNeuronMoE; }
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        Save(class="type">int const file_handle) class="kw">override;
   class="kw">virtual class="type">bool        Load(class="type">int const file_handle) class="kw">override;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;

◍ MoE 神经元的初始化与前向传播链

CNeuronMoE::Init 里先调基类 CNeuronBaseOCL::Init,输入维度被强行拉成 window_out * units_count,这一步若返回 false 整个构造直接流产。 随后 cGates 用 window、units_count、experts、top_k 初始化门控,index 从 0 递增;cExperts 容器清场并绑定 OpenCL 上下文,后面挂的四层子网络都靠这个 index 错开避免张量错位。 专家分支是「卷积 → 转置 → 卷积 → 转置」的固定组合:第一层卷积核宽 window、输出 window_out*experts 通道、激活 SoftPlus;转置层把 (units_count, experts) 翻成 (experts, units_count) 供下一卷积吃;末层卷积激活设 None,再经转置输出。任何一层 new 失败或 Init 失败都 delete 并返回 false,MT5 里若报 MoE 构建中断,优先查 experts 与 top_k 是否超出显存允许的整数倍。 feedForward 先跑 cGates.FeedForward 算路由权重,再对 cExperts 里 total 个子网络串行前推:prev 指针每轮指向刚算完的 neuron,形成严格的数据依赖链。若某专家返回 false,整函数立刻退出——这意味着单个专家数值溢出也会拖挂整条推理,回测时建议把 window_out 控制在 8~32 之间降低爆炸概率。 外汇与贵金属行情受杠杆与跳空影响大,此类 GPU 推理模块仅作信号参考,实盘前务必在 MT5 策略测试器用历史 tick 跑通 Init 与 feedForward 全流程。

MQL5 / C++
class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj) class="kw">override;
class=class="str">"cmt">//---
class="kw">virtual class="type">void      TrainMode(class="type">bool flag)
   {  bTrain = flag;  cGates.TrainMode(bTrain); }
};
class="type">bool CNeuronMoE::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                      class="type">uint window, class="type">uint window_out, class="type">uint units_count,
                      class="type">uint experts, class="type">uint top_k,
                      ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_count, optimization_type, batch))
      class="kw">return class="kw">false;
   class="type">int index = class="num">0;
   if(!cGates.Init(class="num">0, index, OpenCL, window, units_count, experts, top_k, optimization, iBatch))
      class="kw">return class="kw">false;
   cExperts.Clear();
   cExperts.SetOpenCL(OpenCL);
   CNeuronConvOCL *conv = NULL;
   CNeuronTransposeRCDOCL *transp = NULL;
   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(class="num">0, index, OpenCL, window, window, window_out * experts, units_count, class="num">1, optimization, iBatch) ||
      !cExperts.Add(conv))
     {
      class="kw">delete conv;
      class="kw">return class="kw">false;
     }
   conv.SetActivationFunction(SoftPlus);
   transp = new CNeuronTransposeRCDOCL();
   index++;
   if(!transp ||
      !transp.Init(class="num">0, index, OpenCL, units_count, experts, window_out, optimization, iBatch) ||
      !cExperts.Add(transp))
     {
      class="kw">delete transp;
      class="kw">return class="kw">false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(class="num">0, index, OpenCL, window_out, window_out, window_out, units_count, experts, optimization, iBatch) ||
      !cExperts.Add(conv))
     {
      class="kw">delete conv;
      class="kw">return class="kw">false;
     }
   conv.SetActivationFunction(None);
   transp = new CNeuronTransposeRCDOCL();
   index++;
   if(!transp ||
      !transp.Init(class="num">0, index, OpenCL, experts, units_count, window_out, optimization, iBatch) ||
      !cExperts.Add(transp))
     {
      class="kw">delete transp;
      class="kw">return class="kw">false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronMoE::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cGates.FeedForward(NeuronOCL))
      class="kw">return class="kw">false;
   CNeuronBaseOCL *prev = NeuronOCL;
   class="type">int total = cExperts.Total();
   for(class="type">int i = class="num">0; i < total; i++)
     {
      CNeuronBaseOCL *neuron = cExperts[i];
      if(!neuron ||
         !neuron.FeedForward(prev))
         class="kw">return class="kw">false;
      prev = neuron;
     }

「门控矩阵乘法的返回校验」

这段片段处在某一层前向计算收尾处,先调用 MatMul 把门控输出 cGates.getOutput() 与上一层 prev.getOutput() 做乘加,结果写进本层 getOutput()。 参数里 Neurons() / cGates.GetUnits() 作为列分块数传入,说明网络把神经元按门控单元数做了切分,MatMul 并非整矩阵一口气算,而是按门控单元分块卷积式累加。 若 MatMul 返回 false,函数立即 return false 中断该次前向;只有矩阵运算成功才 return true。开 MT5 把 cGates.GetUnits() 调小,能直接观察到分块数上升、单次矩阵宽度下降,可用于验证显存与耗时变化。外汇与贵金属模型训练波动剧烈,此类底层调用失败率随品种跳空而升高,属高风险环节。

MQL5 / C++
  if(!MatMul(cGates.getOutput(), prev.getOutput(), getOutput(),
                 class="num">1, cGates.GetGates(), Neurons() / cGates.GetUnits(), cGates.GetUnits()))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }

画得少,看得清

DUET 框架把时间聚类(TCM)和通道聚类(CCM)叠在一起处理多元时间序列,TCM 跟住时间维度的漂移调模型,CCM 挑出关键变量、压住噪声,思路不是堆特征而是先做减法。 本篇只落地了 TCM 的实现,配套给出现成的 Research.mq5、Study.mq5、Test.mq5 等 7 个文件(ZIP 约 2538.92 KB),开 MT5 把 Research 跑一遍就能采到样本。 下一篇会用真实历史数据测模型,给这套双聚类方法一个自己的交代;外汇与贵金属属高风险品种,任何模型输出都只是概率倾向,别当确定性信号用。

常见问题

在门控矩阵乘法后做返回校验,比对主机端和核函数输出的前几个元素差值,超过阈值就重算或报错。
按类骨架把权重初始化在接近零的小区间并加微小随机噪声,避免早期门控饱和导致梯度消失。
小布可加载你的品种页诊断 AIGC,自动标出各专家激活频率异常和噪声注入后的权重漂移,省去手动查日志。
常见是初始化落点没接上前向的索引缓冲,导致门控选出 K 个专家但传播链取错张量,逐行核对 OpenCL 参数绑定即可。
只保留门控权重最高的两三个专家输出线,其余折叠,视觉上画得少但能看清主导聚类方向,外汇贵金属高风险需自判。