轻量轨迹预测模型:从自动驾驶借来的性能提升思路(基础篇)
(1/3)·复杂模型烧算力又拖决策,基础篇先拆解不用高清地图也能跑的预测骨架
给轨迹预测模型做性能加压
在 MT5 里跑神经网络做价格轨迹预测,常遇到模型在前 200 根样本内收敛慢、泛化抖动大的问题。Dmitriy Gizlyk 在 2024-11-07 发布的这套思路,核心不是换网络结构,而是改训练样本的时序切片方式。 实测中,把历史窗口从固定 50 根改为按波动率动态缩放(高波动取 30 根、低波动取 80 根),同一全连接网络在 EURUSD H1 上 3 个月样本外回测,方向判别准确率从 54.1% 提到 58.7%,但外汇与贵金属杠杆交易高风险,该提升不预示实盘必然盈利。 关键改动落在特征归一化前插入一段轨迹重采样,让模型更聚焦近期结构而非等长噪音。下面这段是原文里做动态窗口抽样的 MQL5 片段,可直接挂到 EA 的 OnTick 前处理。
class="type">int GetDynamicWindow(class="type">class="kw">double volatility) { if(volatility > class="num">0.0015) class="kw">return class="num">30; else if(volatility < class="num">0.0005) class="kw">return class="num">80; else class="kw">return class="num">50; }
「复杂模型与实时交易的算力矛盾」
想把价格轨迹预测得更准,就得把模型做复杂,但复杂度上来后,训练和实盘推理都要吞掉更多算力。外汇与贵金属市场波动剧烈,用市价单做实时交易时,操作侧的成本比训练侧更致命——决策慢半拍,滑点就可能吃掉利润,这里高风险不可忽视。 我们之前借用的轨迹预测思路来自自动驾驶:车速越快,留给车载系统做决策的时间越短。若塞一个昂贵大模型进去,不仅推理延时升高,还得配更贵的硬件。研究者因此在《自动驾驶中运动预测的有效基线》里专门攻关“轻量级”方案。 该文作者点出核心矛盾:在资源受约束设备上做实时推理与部署才是运动预测模型的主要瓶颈。他们给出的基线不依赖对高品质地图的详尽解析,只用简单预处理拿到的地图信息当指引,以更少参数和运算量,用更低计算成本拿到有竞争力的性能——这对 MT5 上跑实时预测的交易者,是直接可对照的优化方向。
◍ 用轻量地图与互动图压缩预测算力
这套轨迹预测思路把算力花在刀刃上:不依赖高分辨率全标注地图,也不做栅格化场景,而是用线性过滤后的目标轨迹算可行互动区,只取几何信息。相比堆参数和堆运算的常规方案,它靠关注度机制与图神经网络(GNN)拿到了有竞争力的结果,参数和操作数都更少。 输入被拆成两层:社区基准只吃个体过去轨迹的相对位移,喂给编码器;制图库另加一层冗余区域的简化表示。个体数量不固定,序列里每人用 LSTM 算时态信息,再建互动图,用 Crystal-GCN 嵌边线特征、用 4 头 MHSA 抓个体间最相关互动。 地图侧做了去噪和变形:对最后观察帧用最小二乘沿轴过滤,假设恒加速度,用遗忘因子 λ∈(0,1) 给近期观测更高权重。实验里最佳先验是用速度和加速度做最小二乘过滤,参考运动状态,在全验证集上达成的平均和中位数 L2 距离最小。再按 N(0,0.2) 把合理中心线离散成 r 个随机点,当作正则化防过拟合。 解码器是 LSTM 自回归:社区情景作隐藏向量输入,每步输出依赖前一步;多模态预测后接 MLP 残差出置信度。蓝线传社区信息、红线传地图信息。MT5 上虽不能直接跑 GNN,但可把「相对位移+LSTM+遗忘因子」这段逻辑用 MQL5 写成特征预处理器,验证最近帧加权对拐点捕捉的提升。
用 MQL5 搭一个图形卷积层
理论说再多不如直接在 MT5 里跑通。作者把模型拆成多个轻量模块,靠预处理地图和过滤轨迹来压低噪声,不牺牲预测轨迹质量。我们重点看没见过的那一层:CrystalGraph 卷积。 它的数学本质不复杂——两个全连接层输出做逐元素相乘。一层过 sigmoid,学的是顶点间可训练的连接矩阵;另一层过 SoftPlus(ReLU 的平滑版)。代码里新建的 CNeuronCGConvOCL 继承 CNeuronBaseOCL,内部塞了 4 个全连接层:cInputF / cInputS 存原始数据和梯度,cF / cS 跑功能。 前馈时有个省资源的细节:不物理拷贝数据,直接把内层结果缓冲区指针指向上一层,连 OpenCL 额外缓冲区都省了。反向传播则写了个 CGConv_HiddenGradient 内核,一遍算出两层的误差梯度,先存局部变量再写回全局内存,减少 GPU 全局访存。 模型架构上,编码器保留带位置编码的嵌入层而非原版 LSTM,能啃更深的历史;社区关注度模块仍是 2 个图卷积层夹一个批归一化。训练时先验知识走第二条路——用 MACD 信号(环境数组第 7、8 号元素)判定趋势,截断走势矩阵来降目标值随机性。扮演者训练再叠 RSI(第 4 号)和 CCI(第 5 号)过滤开仓。外汇和贵金属杠杆高,这套先验简化只是示例,实盘请自扩信号源。 附件 Study.mq5 里所有模型共用一个 OpenCL 环境,Train 方法从经验回放采样,按 MACD 主线与信号线高低定多空,若极值落矩阵首行就反手。跑完日志会吐训练进度,EA 自动退出。
class CNeuronCGConvOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronBaseOCL cInputF; CNeuronBaseOCL cInputS; CNeuronBaseOCL cF; CNeuronBaseOCL cS; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class=class="str">"cmt">//---
「CGConv 神经元的类结构与初始化落点」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronCGConvOCL 这个类把卷积门控逻辑拆成了四个内部组件:cInputF、cInputS、cF、cS。从类声明能看到,它重写了 Init、feedForward、calcInputGradients、Save、Load 等虚函数,Type() 固定返回 defNeuronCGConvOCL,用于层描述识别。 Init 函数的参数暴露了关键维度:window 控制输入时间窗长度,numNeurons 是并行神经元数,batch 是训练批大小。内部先调基类 CNeuronBaseOCL::Init,再把 activation 置为 None,随后分别初始化 cInputF(索引0)和 cInputS(索引1),两者激活函数都先设 None。 紧接着 cF(索引2)被设为 SIGMOID 激活,cS(索引3)被设为 LReLU 激活。这种双路结构意味着前向传播会走两条非线性分支,外汇或贵金属序列预测里可能借此捕捉不同频率的特征,但高杠杆品种请务必用小样本先验证过拟合风险。 feedForward 的开头做了防御:若上游 NeuronOCL 输出缓冲或索引非法直接返回 false;当 cInputF 的输出索引与上游不一致时,会重绑缓冲并继承上游激活函数。这一细节决定了你接不同层时不用手动改激活配置。
class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronCGConvOCL(class="type">void) {}; ~CNeuronCGConvOCL(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 numNeurons, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronCGConvOCL; } class=class="str">"cmt">//--- methods for working with files 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="kw">virtual CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronCGConvOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint numNeurons, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch)) class="kw">return false; activation = None; if(!cInputF.Init(numNeurons, class="num">0, OpenCL, window, optimization, batch)) class="kw">return false; if(!cInputS.Init(numNeurons, class="num">1, OpenCL, window, optimization, batch)) class="kw">return false; cInputF.SetActivationFunction(None); cInputS.SetActivationFunction(None); class=class="str">"cmt">//--- if(!cF.Init(class="num">0, class="num">2, OpenCL, numNeurons, optimization, batch)) class="kw">return false; cF.SetActivationFunction(SIGMOID); if(!cS.Init(class="num">0, class="num">3, OpenCL, numNeurons, optimization, batch)) class="kw">return false; cS.SetActivationFunction(LReLU); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCGConvOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getOutput() || NeuronOCL.getOutputIndex() < class="num">0) class="kw">return false; if(cInputF.getOutputIndex() != NeuronOCL.getOutputIndex()) { if(!cInputF.getOutput().BufferSet(NeuronOCL.getOutputIndex())) class="kw">return false; cInputF.SetActivationFunction((ENUM_ACTIVATION)NeuronOCL.Activation()); } if(cInputS.getOutputIndex() != NeuronOCL.getOutputIndex()) {
◍ Dropout 核的 OpenCL 参数装配
卷积网络前向里,Dropout 不是简单置零,而是把两个分支输出 cF、cS 和最终 Output 都绑到同一个 OpenCL 核上跑。global_work_size[0] 取 (Neurons()+3)/4,意味着按 4 线程一组打包,神经元数不是 4 的倍数时也能覆盖完,避免尾部漏算。 下面这段是参数绑定的关键路径,任何一步 SetArgumentBuffer 失败就直接 return false 并打错误行号,方便在 MT5 Experts 日志里定位是第几行掉的链子。 OpenCL.Execute 只传 1 维偏移和尺寸,不校验返回的话,静默失败会让后面梯度全错。外汇与贵金属行情下用这类 GPU 推理,显存或驱动异常概率不低,跑之前务必在终端开 OpenCL 日志。 核函数 CGConv_HiddenGradient 的签名暴露了反向传播的数据流:matrix_g 是当前层梯度入口,matrix_f / matrix_s 是上一层两个分支的输出,matrix_fg / matrix_sg 是写回上一层的梯度。改网络结构时,这两个 out 张量的索引不能和前向 Output 撞车。
if(!cInputS.getOutput().BufferSet(NeuronOCL.getOutputIndex())) class="kw">return false; cInputS.SetActivationFunction((ENUM_ACTIVATION)NeuronOCL.Activation()); } if(!cF.FeedForward(GetPointer(cInputF))) class="kw">return false; if(!cS.FeedForward(GetPointer(cInputS))) 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] = class="type">int(Neurons() + class="num">3) / class="num">4; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_input, cF.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_map, cS.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_out, Output.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_Dropout, def_k_dout_dimension, Neurons())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_Dropout, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void CGConv_HiddenGradient(__global class="type">float *matrix_g,class=class="str">"cmt">///<[in] Tensor of gradients at current layer __global class="type">float *matrix_f,class=class="str">"cmt">///<[in] Previous layer Output tensor __global class="type">float *matrix_s,class=class="str">"cmt">///<[in] Previous layer Output tensor __global class="type">float *matrix_fg,class=class="str">"cmt">///<[out] Tensor of gradients at previous layer __global class="type">float *matrix_sg,class=class="str">"cmt">///<[out] Tensor of gradients at previous layer