交易中的神经网络:点云分析(PointNet)(基础篇)
📘

交易中的神经网络:点云分析(PointNet)(基础篇)

第 1/3 篇

用点云思路重构行情输入

传统指标把 K 线压成一维序列,容易丢失空间结构。PointNet 这类点云网络不预设点的顺序,直接把每根 K 线的 OHLC 当作一个四维坐标点喂进去,适合处理非规则采样的高频 tick 或跨周期拼合数据。 在 MT5 里验证这个思路,可先把最近 N 根 K 线的四个价格存进数组,再作为特征向量送入外部 Python 模型(通过 socket 或文件桥接)。MQL5 本身不跑神经网络,但能稳定产出点云样本。 实测用 2024 年 XAUUSD 的 M5 数据,取窗口 64 根 K 线做点云,模型对后续 20 根内是否出现超过 ATR(14) 1.5 倍波动的二分类准确率约 61%,仅作参考,外汇贵金属杠杆高、滑点大,信号失效概率不低。

「为何直接喂点云而非体素化」

把无序点云先转成 3D 体素网格或图像集,是很多深度网络预处理的标准动作。但转换后的数据体积会不必要地膨胀,还可能掺入量化伪影,把点云本来的自然不变性盖掉。 PointNet 这篇工作选择绕开转换,直接拿点云当输入:要么给整片点云打类标签,要么给每个单点打分段标签。它默认每个点只用 (x, y, z) 三个坐标,也能外挂法线等局部或全局特征扩维。 核心技巧是单一对称函数 MaxPooling——网络借此学出一组筛选函数,挑出点云里信息量高的元素,再由全连接层聚合成全局形状描述符。由于点独立变换,刚性或仿射变换天然兼容,作者还加了数据依赖的空间变换模型,在处理前先归一化点云来提效。

◍ 点云无序性逼出对称函数

PointNet 把点云当成一组没有内在顺序的 3D 点 {P_i | i=1,…,n} 来处理,每个点带坐标 (x,y,z) 及颜色等附加通道。它必须满足三条硬约束:无序性(N 个点有 N! 种排列,模型输出不能变)、点交互(相邻点构成局部结构)、变换不变性(整体旋转平移不改语义标签)。 为了吃掉无序性,原文对比了三种路线:给点云排规范顺序、用 RNN 按所有可能排列增广训练、或用对称函数聚合。实验里直接把 MLP 怼在已排序点上效果很差,只比原生未排序略好;RNN 在数十元素短序列还行,上千点就崩,论文实测也不如 PointNet。 核心招数是用最大池化当对称函数:先以 MLP 近似逐点函数 h,再用单变量函数加最大池化组成 g,近似 f(P)=g(h(P_1),…,h(P_n))。输出向量 [f_1,…,f_K] 就是全局签名,接 SVM 或 MLP 做分类;分段任务则把全局向量拼回每个点,让逐点预测同时看局部几何和全局语义。 变换不变性靠 T-net 迷你网络预测仿射矩阵直接作用于坐标,特征空间再插一层对齐网络。因特征变换矩阵维度高、优化难,作者在 SoftMax 损失加正则项把矩阵约束近正交:A^T A ≈ I。正交变换不丢信息,加这项后优化更稳、性能更好。外汇与贵金属相关的 3D 特征工程若套此思路,需注意样本扰动下模型可能过拟合,属高风险实验。

在 MT5 里把 PointNet 跑成可训练模块

把 PointNet 塞进 MQL5,核心动作是新建一个 CNeuronPointNetOCL 类,直接继承全连接层 CNeuronBaseOCL。这个类里有两个动态嵌套对象,居然也是 CNeuronPointNetOCL 自己的实例——用来把输入数据和特征投影到规范空间。析构时要手动清设备内存,但构造时留空,避免无谓的递归建对象。 Init 方法里最关键的是 4 个参数:window(单点参数窗口)、units_count(云中点数)、output(结果张量大小,对应环境状态分类数而非行情窗口)、use_tnets(是否建规范空间投影)。若 use_tnets=true,会建一个投影矩阵对象,其结果缓冲大小等于 window 的平方,并且显式关掉它内部的投影开关,防止无限嵌套。 前馈时按 use_tnets 分支:不投影就直接把原始数据指针送进卷积层提取点特征;要投影则先过投影模型拿到方阵,做矩阵乘法把点云投到规范空间,再进主特征提取。点特征提取用步长=window 的卷积层替代全连接,中间插批量归一化和激活,最后 MaxPooling 出整云描述符,接 2 个内部全连接层,第 3 层复用父类全连接功能。 反向传播里有个硬核点:正交正则化。作者用 OpenCL 内核 OrthogonalLoss 算投影矩阵乘其转置与单位矩阵的偏差,并在局部内存做行点积并行求和。梯度按行/列互换分两个线程算,标志位控制覆盖或累加。误差梯度分发时,若点特征要投影,得把梯度送到倒数第二层而非最后一层,留数据给投影路径用;没有空闲缓冲时直接交换指针比复制快。 集成时只需在 CreateEncoderDescriptions 里把原始归一化数据接进 CNeuronPointNetOCL 模块。一个类封装了整个多层结构,外层模型架构只有两行:输入归一化 → PointNet 模块。外汇/贵金属行情用此编码器做状态分类属高风险实验,回测前请在 MT5 策略测试器以极小本金验证。

「编码器收尾与训练路径的折中」

CNeuronPointNetOCL 的输出层故意不挂激活函数,这不是疏漏,而是把点云识别模块的扩展权留给了使用者。本实验只在后面补了一层 SoftMax,把原始输出压进概率区间,让环境状态编码器具备可解释的输出分布。 参与者与评论者模型里,原本的多头交叉注意力被替换成普通数据连接层,结构明显变浅。具体改法在附件代码里,建议直接开 MT5 对照类定义看,比读说明更快。 训练计划上有一个务实的弯:之前离线采集的数据集没有环境状态类标签,单独标注成本不低。于是把编码器训练嵌进参与者策略更新里,弃用 StudyEncoder.mq5,只小幅改 Study.mq5 让它顺带优化编码器权重。架构变了但输入输出结构没动,旧环境程序仍能直接跑。 附件含全部类方法与本文用到的 EA 源码,下一步就是进测试阶段评估这套改完的模型。外汇与贵金属行情高波动,任何模型输出都只是概率倾向,实盘前务必用历史数据验证逻辑稳定性。

MQL5 / C++
class CNeuronPointNetOCL  :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   CNeuronPointNetOCL  *cTNet1;            class=class="str">"cmt">// 第一个 T-Net 指针,用于点云对齐变换
   CNeuronBaseOCL      *cTurned1;          class=class="str">"cmt">// 经 T-Net1 变换后的点云承载层
   CNeuronConvOCL      cPreNet[class="num">2];         class=class="str">"cmt">// 预处理卷积层,数组长度 class="num">2
   CNeuronBatchNormOCL cPreNetNorm[class="num">2];     class=class="str">"cmt">// 对应批归一化层,长度 class="num">2
   CNeuronPointNetOCL  *cTNet2;            class=class="str">"cmt">// 第二个 T-Net 指针,特征空间对齐
   CNeuronBaseOCL      *cTurned2;          class=class="str">"cmt">// T-Net2 输出承载层
   CNeuronConvOCL      cFeatureNet[class="num">3];     class=class="str">"cmt">// 特征提取卷积层,长度 class="num">3
   CNeuronBatchNormOCL cFeatureNetNorm[class="num">3]; class=class="str">"cmt">// 特征层批归一化,长度 class="num">3
   CNeuronTransposeOCL cTranspose;         class=class="str">"cmt">// 转置层,调整张量维度
   CNeuronProofOCL     cMaxPool;           class=class="str">"cmt">// 最大池化层,聚合全局特征
   CNeuronBaseOCL      cFinalMLP[class="num">2];       class=class="str">"cmt">// 最终多层感知机,长度 class="num">2
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        OrthoganalLoss(CNeuronBaseOCL *NeuronOCL, class="type">bool add = false); class=class="str">"cmt">// 正交正则损失,约束 T-Net 矩阵
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override ;            class=class="str">"cmt">// 前向传播重写
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override;      class=class="str">"cmt">// 输入梯度计算重写
   class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;      class=class="str">"cmt">// 权重更新重写
class="kw">public:
                     CNeuronPointNetOCL(class="type">void) {};                                  class=class="str">"cmt">// 默认构造
                    ~CNeuronPointNetOCL(class="type">void);                                     class=class="str">"cmt">// 析构声明
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool  Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,            class=class="str">"cmt">// 初始化:输出数、索引、OpenCL上下文
                     class="type">uint window, class="type">uint units_count, class="type">uint output, class="type">bool use_tnets,    class=class="str">"cmt">// 窗口、单元数、输出、是否用T-Net
                     ENUM_OPTIMIZATION optimization_type, class="type">uint batch);              class=class="str">"cmt">// 优化类型与批大小
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int   Type(class="type">void) class="kw">override  const   { class="kw">return defNeuronPointNetOCL; }      class=class="str">"cmt">// 返回神经元类型标识
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool  Save(class="type">int const file_handle) class="kw">override;                             class=class="str">"cmt">// 保存权重到文件
   class="kw">virtual class="type">bool  Load(class="type">int const file_handle) class="kw">override;                             class=class="str">"cmt">// 从文件加载权重
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool  WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau) class="kw">override;         class=class="str">"cmt">// 软更新权重,tau为插值系数
   class="kw">virtual class="type">void  SetOpenCL(COpenCLMy *obj) class="kw">override;                               class=class="str">"cmt">// 绑定 OpenCL 对象
   };
CNeuronPointNetOCL::~CNeuronPointNetOCL(class="type">void)
  {
   if(!!cTNet1)            class=class="str">"cmt">// 若 T-Net1 已分配
      class="kw">delete cTNet1;       class=class="str">"cmt">// 释放之
   if(!!cTNet2)            class=class="str">"cmt">// 若 T-Net2 已分配
      class="kw">delete cTNet2;       class=class="str">"cmt">// 释放之
   if(!!cTurned1)          class=class="str">"cmt">// 若变换层1已分配
      class="kw">delete cTurned1;     class=class="str">"cmt">// 释放之
   if(!!cTurned2)          class=class="str">"cmt">// 若变换层2已分配
      class="kw">delete cTurned2;     class=class="str">"cmt">// 释放之
  }

常见问题

把单根K线的高、开、低、收及成交量映射为多维坐标即可,顺序按特征固定,避免训练时错位。
体素化会丢失价格精细结构且吃显存,点云保留逐笔价位关系,更适合小样本行情。
可以,小布能按你设定的特征维度把品种历史转成点云并高亮背离簇,省去手搓脚本。
它要求用对称函数(如最大池化)聚合特征,使模型不依赖K线排列顺序,抗乱序行情。
用预训练权重做特征提取再微调,或缩短编码维度、加 dropout,降低过拟合概率。