交易中的神经网络:点云分析(PointNet)·进阶篇
🧠

交易中的神经网络:点云分析(PointNet)·进阶篇

(2/3)· 把 K 线、挂单、成交额揉成一组无顺序的点,网络自己挑关键元素

实战向 第 2/3 篇
不少人先把盘口转成图像或体素再喂模型,数据膨胀还吃掉自然不变性。点云不讲究排列,硬排顺序只会让同一条行情在 N! 种排列里反复训练。直接丢原始点集,反而更贴近市场本来的无序。

「PointNet 初始化里的 T-Net 与 PreNet 分支」

在 MT5 的 OpenCL 神经网络封装里,CNeuronPointNetOCL::Init 承担了两类子网络的构建:当 use_tnets 开关为真时,先拉起两套 T-Net 与对应的 Turned 层;无论开关如何,PreNet 序列都必然初始化。 T-Net 第一路用 window 作为输入窗口,输出维度被强制设为 window * window;第二路则写死 64 作为输入与输出边长,输出张量规模达到 64 * 64。这两组数字直接决定了显存占用量,调参时若把 window 从常见值 16 提到 32,第一路输出节点会由 256 跳到 1024。 PreNet 这边分两层卷积:首层从 window×window 映射到 64 通道,激活函数被显式置为 None;紧随其后的归一化层 cPreNetNorm[0] 改用 LReLU。第二层 PreNet 在 64×64 空间上再做一次 64 通道卷积,同样关掉非线性。想验证的话,把这段代码贴进 MT5 的 EA 源码,编译后看 Init 返回的 false 究竟卡在哪一行的 Print 即可。

MQL5 / C++
class="type">bool CNeuronPointNetOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                              class="type">uint window, class="type">uint units_count, class="type">uint output, class="type">bool use_tnets,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, output, optimization_type, batch))
      class="kw">return false;
class=class="str">"cmt">//--- Init T-Nets
   if(use_tnets)
     {
      if(!cTNet1)
        {
         cTNet1 = new CNeuronPointNetOCL();
         if(!cTNet1)
            class="kw">return false;
        }
      if(!cTNet1.Init(class="num">0, class="num">0, OpenCL, window, units_count, window * window, false, optimization, iBatch))
         class="kw">return false;
      if(!cTurned1)
        {
         cTurned1 = new CNeuronBaseOCL();
         if(!cTurned1)
            class="kw">return false;
        }
      if(!cTurned1.Init(class="num">0, class="num">1, OpenCL, window * units_count, optimization, iBatch))
         class="kw">return false;
      if(!cTNet2)
        {
         cTNet2 = new CNeuronPointNetOCL();
         if(!cTNet2)
            class="kw">return false;
        }
      if(!cTNet2.Init(class="num">0, class="num">2, OpenCL, class="num">64, units_count, class="num">64 * class="num">64, false, optimization, iBatch))
         class="kw">return false;
      if(!cTurned2)
        {
         cTurned2 = new CNeuronBaseOCL();
         if(!cTurned2)
            class="kw">return false;
        }
      if(!cTurned2.Init(class="num">0, class="num">3, OpenCL, class="num">64 * units_count, optimization, iBatch))
         class="kw">return false;
     }
class=class="str">"cmt">//--- Init PreNet
   if(!cPreNet[class="num">0].Init(class="num">0, class="num">0, OpenCL, window, window, class="num">64, units_count, optimization, iBatch))
      class="kw">return false;
   cPreNet[class="num">0].SetActivationFunction(None);
   if(!cPreNetNorm[class="num">0].Init(class="num">0, class="num">1, OpenCL, class="num">64 * units_count, iBatch, optimization))
      class="kw">return false;
   cPreNetNorm[class="num">0].SetActivationFunction(LReLU);
   if(!cPreNet[class="num">1].Init(class="num">0, class="num">2, OpenCL, class="num">64, class="num">64, class="num">64, units_count, optimization, iBatch))
      class="kw">return false;
   cPreNet[class="num">1].SetActivationFunction(None);

◍ PointNet 初始化与前向里的层堆叠细节

这段 CNeuronPointNetOCL 的 Init 把预处理网、特征网、转置与池化、最终 MLP 逐层挂到 OpenCL 上下文上。注意 cPreNetNorm[1] 用 None 激活,而 cFeatureNetNorm[0] 和 [1] 都接了 LReLU,这种交错设定会影响梯度在批归一化后的流动形态。 特征网三层维度从 64→64→128 到 128→128→512,最后一维 512 直接喂给 cTranspose 与 cMaxPool,池化核宽高等于 units_count、通道 512。若你改 units_count,cPreNetNorm[1] 的 64*units_count 和 cFeatureNetNorm 的两处乘积都要同步,否则 Init 返回 false 导致 EA 加载失败。 feedForward 里有个分支:cTNet1 为空时只跑 cPreNet[0],否则要求 cTurned1 已置位再跑 cTNet1。实盘跑 MT5 时若发现前向中断,先打印 cTurned1 状态,大概率是没有在 turn 阶段赋值。外汇与贵金属行情高波动,这类 GPU 网推理延迟可能放大滑点风险。

MQL5 / C++
  if(!cPreNetNorm[class="num">1].Init(class="num">0, class="num">3, OpenCL, class="num">64 * units_count, iBatch, optimization))
      class="kw">return false;
   cPreNetNorm[class="num">1].SetActivationFunction(None);
class=class="str">"cmt">//--- Init Feature Net
   if(!cFeatureNet[class="num">0].Init(class="num">0, class="num">4, OpenCL, class="num">64, class="num">64, class="num">64, units_count, optimization, iBatch))
      class="kw">return false;
   cFeatureNet[class="num">0].SetActivationFunction(None);
   if(!cFeatureNetNorm[class="num">0].Init(class="num">0, class="num">5, OpenCL, class="num">64 * units_count, iBatch, optimization))
      class="kw">return false;
   cFeatureNet[class="num">0].SetActivationFunction(LReLU);
   if(!cFeatureNet[class="num">1].Init(class="num">0, class="num">6, OpenCL, class="num">64, class="num">64, class="num">128, units_count, optimization, iBatch))
      class="kw">return false;
   cFeatureNet[class="num">1].SetActivationFunction(None);
   if(!cFeatureNetNorm[class="num">1].Init(class="num">0, class="num">7, OpenCL, class="num">128 * units_count, iBatch, optimization))
      class="kw">return false;
   cFeatureNetNorm[class="num">1].SetActivationFunction(LReLU);
   if(!cFeatureNet[class="num">2].Init(class="num">0, class="num">8, OpenCL, class="num">128, class="num">128, class="num">512, units_count, optimization, iBatch))
      class="kw">return false;
   cFeatureNet[class="num">2].SetActivationFunction(None);
   if(!cFeatureNetNorm[class="num">2].Init(class="num">0, class="num">9, OpenCL, class="num">512 * units_count, iBatch, optimization))
      class="kw">return false;
   cFeatureNetNorm[class="num">2].SetActivationFunction(None);
   if(!cTranspose.Init(class="num">0, class="num">10, OpenCL, units_count, class="num">512, optimization, iBatch))
      class="kw">return false;
   if(!cMaxPool.Init(class="num">512, class="num">11, OpenCL, units_count, units_count, class="num">512, optimization, iBatch))
      class="kw">return false;
class=class="str">"cmt">//--- Init Final MLP
   if(!cFinalMLP[class="num">0].Init(class="num">256, class="num">12, OpenCL, class="num">512, optimization, iBatch))
      class="kw">return false;
   cFinalMLP[class="num">0].SetActivationFunction(LReLU);
   if(!cFinalMLP[class="num">1].Init(output, class="num">13, OpenCL, class="num">256, optimization, iBatch))
      class="kw">return false;
   cFinalMLP[class="num">1].SetActivationFunction(LReLU);
   SetActivationFunction(None);
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronPointNetOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
   {
class=class="str">"cmt">//--- PreNet
   if(!cTNet1)
      {
       if(!cPreNet[class="num">0].FeedForward(NeuronOCL))
          class="kw">return false;
      }
   else
      {
       if(!cTurned1)
          class="kw">return false;
       if(!cTNet1.FeedForward(NeuronOCL))
          class="kw">return false;

特征网络的前向传播链路

这段前向传播代码展示了两层特征提取分支如何在 MT5 的 OpenCL 神经元容器里接力运算。第一分支若 cTNet1 存在,会用神经元数量的平方根作为窗口做矩阵乘法转置,再喂给预网络;否则直接用 cPreNet[0] 承接上层输出。 窗口计算写死为 int window = (int)MathSqrt(cTNet1.Neurons()),这意味着神经元数最好是完全平方数,否则整除截断会改变 MatMul 的维度配对,可能在回测中触发静默的维度错配。 第二分支 cTNet2 的逻辑几乎镜像,但输入来自 cPreNetNorm[1],且多了一道 cTurned2 的空指针守卫。两个分支最终都汇入 cFeatureNet[0],之后用 uint total = cFeatureNet.Size() 驱动循环,逐层做 FeatureNet → FeatureNetNorm 的归一串联。 收尾部分把最后一层归一结果过 cTranspose、cMaxPool 再做两层 MLP 输出。任何一步 FeedForward 返回 false 或 IsStopped() 为真都会中断,实盘外汇与贵金属波动剧烈,这类 GPU 推理若在中途被终止,信号将失效,属高风险场景,需自行在 MT5 策略测试器里验证中断频率。

MQL5 / C++
  class="type">int window = (class="type">int)MathSqrt(cTNet1.Neurons());
  if(IsStopped() ||
     !MatMul(NeuronOCL.getOutput(), cTNet1.getOutput(), cTurned1.getOutput(), NeuronOCL.Neurons() / window,
window, window))
     class="kw">return false;
  if(!cPreNet[class="num">0].FeedForward(cTurned1.AsObject()))
     class="kw">return false;
   }
  if(!cPreNetNorm[class="num">0].FeedForward(cPreNet[class="num">0].AsObject()))
     class="kw">return false;
  if(!cPreNet[class="num">1].FeedForward(cPreNetNorm[class="num">0].AsObject()))
     class="kw">return false;
  if(!cPreNetNorm[class="num">1].FeedForward(cPreNet[class="num">1].AsObject()))
     class="kw">return false;
class=class="str">"cmt">//--- Feature Net
  if(!cTNet2)
   {
     if(!cFeatureNet[class="num">0].FeedForward(cPreNetNorm[class="num">1].AsObject()))
       class="kw">return false;
   }
  else
   {
     if(!cTurned2)
       class="kw">return false;
     if(!cTNet2.FeedForward(cPreNetNorm[class="num">1].AsObject()))
       class="kw">return false;
     class="type">int window = (class="type">int)MathSqrt(cTNet2.Neurons());
     if(IsStopped() ||
       !MatMul(cPreNetNorm[class="num">1].getOutput(), cTNet2.getOutput(), cTurned2.getOutput(),
cPreNetNorm[class="num">1].Neurons() / window, window, window))
       class="kw">return false;
     if(!cFeatureNet[class="num">0].FeedForward(cTurned2.AsObject()))
       class="kw">return false;
   }
  if(!cFeatureNetNorm[class="num">0].FeedForward(cFeatureNet[class="num">0].AsObject()))
     class="kw">return false;
  class="type">uint total = cFeatureNet.Size();
  for(class="type">uint i = class="num">1; i < total; i++)
   {
     if(!cFeatureNet[i].FeedForward(cFeatureNetNorm[i - class="num">1].AsObject()))
       class="kw">return false;
     if(!cFeatureNetNorm[i].FeedForward(cFeatureNet[i].AsObject()))
       class="kw">return false;
   }
  if(!cTranspose.FeedForward(cFeatureNetNorm[total - class="num">1].AsObject()))
     class="kw">return false;
  if(!cMaxPool.FeedForward(cTranspose.AsObject()))
     class="kw">return false;
  if(!cFinalMLP[class="num">0].FeedForward(cMaxPool.AsObject()))
     class="kw">return false;
  if(!cFinalMLP[class="num">1].FeedForward(cFinalMLP[class="num">0].AsObject()))
     class="kw">return false;
  if(!CNeuronBaseOCL::feedForward(cFinalMLP[class="num">1].AsObject()))
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }

「正交约束在 GPU 核里的梯度回流」

PointNet 类结构里,正交损失(Orthogonal Loss)用来逼权重矩阵接近正交,减少特征冗余。这段 OpenCL 核把对称位置的乘积求和,再反传梯度,全程在本地内存做归约,避免反复读全局显存。 核函数开头先用 get_global_id(0) 取行号 r、get_local_id(1) 取列号 c,LOCAL_ARRAY_SIZE 决定共享内存 Temp 的长度。若 data 里出现 inf 或 nan,直接置 0,防止梯度爆炸把整个 batch 带崩。 归约部分用 do-while 把 Temp 两两相加,count 每次 (count+1)/2 收缩,直到剩 1 项得到 sum。损失写成 -pow((r==c) - sum, 2.0f),即单位矩阵偏差的平方负值;梯度 g 再乘 value2 后写回 grad,add==1 时累加、否则覆盖。 calcInputGradients 里依次对 cFinalMLP[1]、cFinalMLP[0]、cMaxPool、cTranspose 调梯度计算,任何一层返回 false 就整体退出。在 MT5 里接自己的特征网络时,若显存报错,先查 Temp 长度是否小于 cols 导致越界。

MQL5 / C++
__kernel class="type">void OrthoganalLoss(__global const class="type">float *data,
                              __global class="type">float *grad,
                              const class="type">int add
                              )
  {
  const class="type">size_t r = get_global_id(class="num">0);
  const class="type">size_t c = get_local_id(class="num">1);
  const class="type">size_t cols = get_local_size(class="num">1);
  __local class="type">float Temp[LOCAL_ARRAY_SIZE];
  class="type">uint ls = min((class="type">uint)cols, (class="type">uint)LOCAL_ARRAY_SIZE);
  const class="type">int shift1 = r * cols + c;
  const class="type">int shift2 = c * cols + r;
  class="type">float value1 = data[shift1];
  class="type">float value2 = (shift1==shift2 ? value1 : data[shift2]);
  if(isinf(value1) || isnan(value1))
     value1 = class="num">0;
  if(isinf(value2) || isnan(value2))
     value2 = class="num">0;
  class="type">float v2 = value1 * value2;
  if(isinf(v2) || isnan(v2))
     v2 = class="num">0;
  for(class="type">int i = class="num">0; i < cols; i += ls)
     {
     class=class="str">"cmt">//---
     if(i <= c && (i + ls) > c)
        Temp[c - i] = (i == class="num">0 ? class="num">0 : Temp[c - i]) + v2;
     barrier(CLK_LOCAL_MEM_FENCE);
     }
  class="type">uint count = min(ls, (class="type">uint)cols);
  do
     {
     count = (count + class="num">1) / class="num">2;
     if(c < ls)
        Temp[c] += (c < count && (c + count) < cols ? Temp[c + count] : class="num">0);
     if(c + count < ls)
        Temp[c + count] = class="num">0;
     barrier(CLK_LOCAL_MEM_FENCE);
     }
  while(count > class="num">1);
  const class="type">float sum = Temp[class="num">0];
  class="type">float loss = -pow((class="type">float)(r == c) - sum, class="num">2.0f);
  class="type">float g = (class="num">2 * (sum - (class="type">float)(r == c))) * loss;
  g = value2 * g;
  if(isinf(g) || isnan(g))
     g = class="num">0;
  if(add == class="num">1)
     grad[shift1] += g;
  else
     grad[shift1] = g;
  }
class="type">bool CNeuronPointNetOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL)
     class="kw">return false;
  if(!CNeuronBaseOCL::calcInputGradients(cFinalMLP[class="num">1].AsObject()))
     class="kw">return false;
  if(!cFinalMLP[class="num">0].calcHiddenGradients(cFinalMLP[class="num">1].AsObject()))
     class="kw">return false;
  if(!cMaxPool.calcHiddenGradients(cFinalMLP[class="num">0].AsObject()))
     class="kw">return false;
  if(!cTranspose.calcHiddenGradients(cMaxPool.AsObject()))
     class="kw">return false;
  class="type">uint total = cFeatureNet.Size();

◍ 反向传播里的梯度链式回传

这段逻辑跑的是多层网络的反向传播,从特征网络顶层一路把隐藏层梯度算到底。循环从 total-1 往下到 1,每一层都拿归一化层的对象去算隐藏梯度,任何一步返回 false 就直接中断,说明梯度爆炸或对象为空时训练会立刻停。 cTNet2 存在时分支更复杂:先用 MathSqrt(cTNet2.Neurons()) 开出 window,把神经元数开方当卷积窗口。MatMulGrad 做矩阵乘的梯度回传,输入维度是 cPreNetNorm[1].Neurons()/window,三方矩阵都是 window×window,这种分块能压住全连接层的显存峰值。 OrthoganalLoss 带 true 参数做正交约束的梯度附加,逼权重彼此去相关,对过拟合倾向有一定抑制。随后 SumAndNormilize 把 cPreNet 与 cPreNetNorm 的梯度合并归一,标志这一支回传收口。 cTNet1 支线同理,NeuronOCL 作为输出层代理,把梯度经 cTurned1 回传给 cPreNet[0]。开 MT5 把这段塞进 EA 的 train 函数,若日志频繁卡在某一行 return false,优先查对应层的 AsObject() 是否拿到了空指针。

MQL5 / C++
for(class="type">uint i = total - class="num">1; i > class="num">0; i--)
  {
    if(!cFeatureNet[i].calcHiddenGradients(cFeatureNetNorm[i].AsObject()))
      class="kw">return false;
    if(!cFeatureNetNorm[i - class="num">1].calcHiddenGradients(cFeatureNet[i].AsObject()))
      class="kw">return false;
  }
  if(!cFeatureNet[class="num">0].calcHiddenGradients(cFeatureNetNorm[class="num">0].AsObject()))
    class="kw">return false;
  if(!cTNet2)
    {
      if(!cPreNetNorm[class="num">1].calcHiddenGradients(cFeatureNet[class="num">0].AsObject()))
        class="kw">return false;
    }
  else
    {
      if(!cTurned2)
        class="kw">return false;
      if(!cTurned2.calcHiddenGradients(cFeatureNet[class="num">0].AsObject()))
        class="kw">return false;
      class="type">int window = (class="type">int)MathSqrt(cTNet2.Neurons());
      if(IsStopped() ||
        !MatMulGrad(cPreNetNorm[class="num">1].getOutput(), cPreNet[class="num">1].getGradient(), cTNet2.getOutput(),
cTNet2.getGradient(), cTurned2.getGradient(), cPreNetNorm[class="num">1].Neurons() / window,
window, window))
        class="kw">return false;
      if(!OrthoganalLoss(cTNet2.AsObject(), true))
        class="kw">return false;
      if(!cPreNetNorm[class="num">1].calcHiddenGradients((CObject*)cTNet2))
        class="kw">return false;
      if(!SumAndNormilize(cPreNetNorm[class="num">1].getGradient(), cPreNet[class="num">1].getGradient(), cPreNetNorm[class="num">1].getGradient(),
class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
    }
  if(!cPreNet[class="num">1].calcHiddenGradients(cPreNetNorm[class="num">1].AsObject()))
    class="kw">return false;
  if(!cPreNetNorm[class="num">0].calcHiddenGradients(cPreNet[class="num">1].AsObject()))
    class="kw">return false;
  if(!cPreNet[class="num">0].calcHiddenGradients(cPreNetNorm[class="num">0].AsObject()))
    class="kw">return false;
  if(!cTNet1)
    {
      if(!NeuronOCL.calcHiddenGradients(cPreNet[class="num">0].AsObject()))
        class="kw">return false;
    }
      if(!cTurned1)
        class="kw">return false;
      if(!cTurned1.calcHiddenGradients(cPreNet[class="num">0].AsObject()))
        class="kw">return false;
      class="type">int window = (class="type">int)MathSqrt(cTNet1.Neurons());
      if(IsStopped() ||
        !MatMulGrad(NeuronOCL.getOutput(), NeuronOCL.getGradient(), cTNet1.getOutput(), cTNet1.getGradient(),
cTurned1.getGradient(), NeuronOCL.Neurons() / window, window, window))
让小布替你跑这套
把多品种盘口点云抽取和 PointNet 推理的重复劳动交给小布盯盘,你专注决策。这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到。

常见问题

默认用价格坐标 (x,y,z) 做三维,可加成交量、买卖挂单量差、时间熵等通道;外汇贵金属波动受宏观事件影响,建议先小样本验证附加维度是否带来过拟合。
它作为单一对称函数,对无序点集做排列不变聚合,网络借其挑出信息量高的点,编码成全局描述符,避免受输入顺序干扰。
目前小布内置的是通用盘口点云诊断,自定义权重需走本地推理后回传特征;高频重训建议用脚本批量生成点云再交小布可视化。
刚性旋转加平移的仿射矩阵较常用,先依数据依赖变换把点云对齐到主成分轴,再进 PointNet,能缓解不同品种量纲差异带来的训练偏移。
CNN 吃固定栅格易漏掉稀疏挂单结构,点云保留原始距离关系,对突发的薄盘拉升可能更敏感,但样本少时概率倾向不稳定,属高风险信号。