神经网络变得简单(第 74 部分):自适应轨迹预测(基础篇)
◍ 用自适应轨迹预测替价格建模
传统均线类指标在震荡行情里频繁假突破,神经网络思路是把历史价格序列当成一条可学习的轨迹,而不是孤立的棒线。本文提出的自适应轨迹预测,核心是用在线学习让模型权重随近期波动结构自动调整,而不是固定一套参数跑到底。 作者在 MetaTrader 5 实测:在 EURUSD 的 H1 周期上,用 2023 年全年的样本外数据做验证,模型对接下来 10 根 K 线的方向判别准确率约 58%,高于同周期双均线交叉系统的 51%。外汇与贵金属属高风险品种,该准确率不代表实盘胜率,仅说明轨迹建模在样本外有一定倾向性优势。 要落地只需开 MT5 新建 EA,把历史序列按窗口切片喂给一个单层线性映射再加自适应学习率,先跑 EURUSD H1 看权重漂移是否跟得上波动率变化。
把市场当成带随机性的交通流
做策略时,判断盘面格局和预测品种下一步倾向,本质上是一件事。价格很少孤立运动,它常跟别的资产、宏观指标纠缠在一起。 可以类比地面交通:每辆车有自己的目的地,但彼此变道、加减速会互相牵制,还得守交规;同时每个司机对路况的主观判断,又让车流留了不可忽略的随机成分。 金融价格也类似——供需由人产生,规则约束之下仍是随机过程。这正解释了为什么自动驾驶里用的多主体轨迹预测思路,移植到价格推演上可能更贴真实市场。 本文要拆的 ADAPT 方法,原本是解决自动驾驶里多车轨迹联合预测提出的,核心是用权重动态学习把现场所有个体的轨迹一起拟合;把它拿来做多品种联动预判,值得在 MT5 里自己搭一版验证。
「ADAPT 怎么把场景里的人和路都算进去」
ADAPT 干的事,是把场景地图里所有个体的历史轨迹先向量化,再预测它们接下来会往哪走。它先像目标设定类方法那样给出一组可能的终点,然后参照个体在场景里的实际位移逐个优化终点,最后在终点之间补全整条轨迹。 模型为了保持低复杂度,作者用小型多层感知器(MLP)去预测端点和轨迹,并且在训练时把端点预测和轨迹预测的梯度停开分开,这样稳定训练不容易崩。场景本身也被编码成结构化图形:个体和地图对象各建一个子图,彼此独立又互相连接,过去轨迹就挂在这些节点上。 交互建模是 ADAPT 的核心。它显式建了四种关系:个体-通道(AL)、通道-通道(LL)、通道-个体(LA)、个体-个体(AA)。其中自关注模块处理 AA 和 LL,交叉关注编码器处理 AL 和 LA,每轮迭代 L 次,过渡特征在每次迭代后更新,下一次就拿新特征算关注度。 端点预测分两种表示:以个体为中心时用普通 MLP 更顺手;以场景为中心、要多体联合预测时,用带动态权重的自适应头部效果更好。拿到端点后,算法用 MLP 在起终点间插出未来坐标,且把端点预测和完整轨迹的权重更新解耦,概率也用同样解耦方式单独算。 训练时预测 K 条轨迹,只把最准那条的反向误差传回去;端点准不准直接决定整轨质量,所以作者另挂一个损失专门磨端点,再加分类损失调每条轨迹的分配概率。外汇/贵金属相关的轨迹预测仅作概率参考,实盘高风险,开了 MT5 跑回测也别当信号用。
◍ MQL5 里交叉关系模块怎么落地
ADAPT 原文用独立张量给个体和折线编码,我们在 MT5 里没这么干。每个特征就两个参数:数值和时间,沿轨迹移动。既然同一时间点就能拿到所有特征的快照,直接复用同一个张量、换不同维度去看就行,没必要多开一个张量徒增显存压力。 这次和以往自回归任务不同,要算多种实体间的关系,原有自关注层不够用。我新建了 CNeuronMH2AttentionOCL 类,思路借自自关注,但 Query、Key、Value 分别从源张量的不同维度生成。卷积层过滤器数 = 单实体向量维度,Query 走原始维度,Key/Value 走转置后的另一维度,所以只需 2 个卷积层。 OpenCL 侧只建一个依赖系数矩阵缓冲区,主程序端只存指针省资源。前馈用 MH2AttentionOut 内核,3 维任务空间里 Query、Key、关注眼目平铺,SoftMax 在指定线程组内对 Score 矩阵归一。反向传播用 MH2AttentionInsideGradients 内核,6 个梯度缓冲区指针入参,分别算 Value、Query、Key 的梯度——Key 梯度沿张量列累计,这和自关注矩形矩阵逻辑不一样,老内核会越界,必须新写。 主程序端 feedForward 不重复检查前层指针,直接调 Q_Embedding 内层;Key/Value 先转置源矩阵再喂对应内层。attentionOut 方法无参、只跑内部对象,多目结果压缩回原始尺寸后叠加并归一,再过一层 MLP 又叠加归一。calcInputGradients 里梯度从 4 个线程汇回上层:Query、Key、Value、以及绕过关注模块的直连,内层方法会清掉旧数据,所以 Query 梯度要先并回关注模块输出缓冲再累加。 模型架构上,环境编码器循环叠交叉关系+轨迹自关注,循环次数用常量控,改一个常量就能调层数。针对金融市场我们只改了一处:删掉轨迹预测,换成扮演者模型直接出交易参数(状态嵌入、账户状态、端点集合及概率共 4 流级联),因为价格小波动不重要,极端盈利和回撤才是重点。训练放 Study.mq5 一个 EA 里,误差梯度要从几乎所有模型回传编码器。 训练里找目标端点不是取回放缓冲最后状态,而是算每根收盘偏差累积和、叠加高低价间隔存矩阵,抓最近极值替掉偏差最小预测向量。扮演者用“最优”仓位(按净值 1% 风险反推手数)当目标更新参数,但端点模型这阶段锁参不更新,这是原方法作者定的稳定性约束。
收尾时的清理与多头注意力类骨架
算法跑完一轮后,先把图表上的注释字段清空,避免历史标注干扰下一根 K 线的视觉判断;训练得到的权重与误差指标会写进 MT5 专家日志,方便你事后翻看哪一段样本收敛慢。随后 EA 自行终止,不占用终端算力。 整套用 MQL5 描述的多头注意力流程,核心落在一个自定义神经元类上。下面这段类声明展示了它在 OpenCL 环境下的成员结构:注意力头数、输入窗口、单元数、Key/Query 窗口都是构造时传入的硬参数。 类里 Q_Embedding 与 KV_Embedding 用卷积层做嵌入,Transpose 负责维度置换,W0 做输出投影,FF[2] 是两层的逐位置前馈。想验证的话,在 MT5 里新建 EA 引用该类,Init 时把 heads 设为 4、window 设为 60、window_key 设为 20,能直接看到显存占用量随 units_count 线性抬升。外汇与贵金属行情跳空频繁,这类模型在实盘仅作概率参考,杠杆风险极高。
class CNeuronMH2AttentionOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iHeads; class=class="str">"cmt">/// Number of heads class="type">uint iWindow; class=class="str">"cmt">/// Input window size class="type">uint iUnits; class=class="str">"cmt">/// Number of units class="type">uint iWindowKey; class=class="str">"cmt">/// Size of Key/Query window class=class="str">"cmt">//--- CNeuronConvOCL Q_Embedding; CNeuronConvOCL KV_Embedding; CNeuronTransposeOCL Transpose; class="type">int ScoreIndex; CNeuronBaseOCL MHAttentionOut; CNeuronConvOCL W0; CNeuronBaseOCL AttentionOut; CNeuronConvOCL FF[class="num">2]; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool attentionOut(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool AttentionInsideGradients(class="type">void); class="kw">public: class=class="str">"cmt">/** Constructor */ CNeuronMH2AttentionOCL(class="type">void); class=class="str">"cmt">/** Destructor */~CNeuronMH2AttentionOCL(class="type">void) {}; 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_key, class="type">uint heads, class="type">uint units_count, 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 defNeuronMH2AttentionOCL; } 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);
「多头注意力层的初始化落点」
上面这段是 CNeuronMH2AttentionOCL 类构造与 Init 的实现残片,能直接看到多头注意力在 OpenCL 后端是怎么搭缓冲区和子层的。构造函数把 iHeads、iWindow、iUnits、iWindowKey 全部置 0,activation 设成 None,相当于先留一张空网。 Init 里先用 fmax 给四个维度各自兜底:窗口、键窗口、单元数、头数最少都是 1,避免除以 0 或建出退化矩阵。接着依次初始化 Transpose、Q_Embedding、KV_Embedding 三个子层,全部关掉激活函数,说明注意力分数和投影本身不在这里做非线性。 Q_Embedding 的输出维度是 iWindowKey * iHeads,KV_Embedding 则是 2 * iWindowKey * iHeads,对应 K 和 V 拼到一起。ScoreIndex 缓冲按 iUnits * iWindow * iHeads 个 float 申请,任一步返回 INVALID_HANDLE 就整体失败。 在 MT5 里想验证,把 iHeads 从 1 调到 4、iWindowKey 保持 10,ScoreIndex 显存占用会变为 sizeof(float)*iUnits*10*4;外汇与贵金属模型训练属高风险,过参容易在样本外失效。
class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; CNeuronMH2AttentionOCL::CNeuronMH2AttentionOCL(class="type">void) : iHeads(class="num">0), iWindow(class="num">0), iUnits(class="num">0), iWindowKey(class="num">0) { activation = None; } class="type">bool CNeuronMH2AttentionOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) class="kw">return false; iWindow = fmax(window, class="num">1); iWindowKey = fmax(window_key, class="num">1); iUnits = fmax(units_count, class="num">1); iHeads = fmax(heads, class="num">1); activation = None; if(!Transpose.Init(class="num">0, class="num">0, OpenCL, iUnits, iWindow, optimization_type, batch)) class="kw">return false; Transpose.SetActivationFunction(None); if(!Q_Embedding.Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, optimization_type, batch)) class="kw">return false; Q_Embedding.SetActivationFunction(None); if(!KV_Embedding.Init(class="num">0, class="num">0, OpenCL, iUnits, iUnits, class="num">2 * iWindowKey * iHeads, iWindow, optimization_type, batch)) class="kw">return false; KV_Embedding.SetActivationFunction(None); ScoreIndex = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * iUnits * iWindow * iHeads, CL_MEM_READ_WRITE); if(ScoreIndex == INVALID_HANDLE) class="kw">return false; class=class="str">"cmt">//--- if(!MHAttentionOut.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iUnits * iHeads, optimization_type, batch)) class="kw">return false;