神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ)(基础篇)
「正文」
神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ) MetaTrader 5 — 交易系统 | 25 十一月 2024, 13:16 444 0 Dmitriy Gizlyk
◍ 从静态环境到动态走势的跨越
此前多数模型把市场环境当成静态快照处理,这契合马尔可夫过程假设——状态只依赖当前输入,不估测变化动态。就连之前用过的 DFFT 也是为静态图像目标检测设计的,对价格序列里的时序演化无能为力。 但实盘里,价格变化的动态本身常携带预警信号:拐头加速度、波动率收敛后的扩张,往往先于方向选择。把 K 线流视作视频帧序列,借视频目标检测的思路去抓「运动中的结构」,比单帧分析更贴近行情本质。 近期论文《FAQ:用于基于变换器的检测视频中对象的特征聚合查询》给出一条可行路径:用相邻帧特征聚合来生成并优化查询向量,提升变换器类模型的检测质量。其实验显示,这种时序聚合在视频检测任务上稳定优于单帧基线,思路也能平移到图像与视频通用的检测器上。 对外汇、贵金属这类高杠杆品种,动态特征聚合可能提高信号前瞻概率,但样本外失效与滑点风险始终存在,任何方法都只是概率工具。
把随机查询改成跟着帧走的动态聚合
| 视频对象检测里用变换器做查询聚合,最早朴素做法是把相邻帧的查询直接均化:ΔQ = Σ w_i Q_i,w 是可学习权重。原版权重按输入帧特征的余弦相似性生成,公式里 α、β 是映射函数, | · | 做常规化,识别概率写成 P_v = f(I, ΔQ_v)。 |
|---|
问题出在相邻查询 Q_i 是随机初始化的,不和对应帧 I_i 绑定,快速移动时给不出足够的时态或语义信息,聚合权重虽和特征 F、F_i 相关,但随机查询数量没约束,性能会掉。作者试过直接从帧特征 F_i 生成 Q_i,实验表明难训练且结果更差。 替代方案是保留随机初始化,但拆出「基本查询 Q_b」和「动态查询 Q_d」:动态查询由基本查询经映射函数 M(F, F_i) 生成。先把 Q_b 按 r 组划分,每组用同一权重 V 做加权平均得到动态查询;V 本身由全局池化 A 提取帧全局特征、再经映射 G 投到查询维度得来。 训练时动态和基本查询用相同权重分别聚合,出预测 P_d、P_b,再算两者双向一致误差,用超参数 γ 平衡。实盘/推理阶段只留动态查询 Q_d 和 P_d 做最终结果,模型复杂度仅微增。外汇或贵金属相关的视频帧时序检测若套这套,需留意高频跳动下的高风险。
「用 MQL5 搭一个带解码器的 FAQ 动态查询模型」
FAQ 原论文作者认为解码器无效而将其剔除,但在 MQL5 实现里我们反向加回一个解码器,用来验证它在动态查询实体下的实际作用。核心落点是新建 CNeuronFAQOCL 类生成动态查询,再建 CNeuronCrossAttention 类做跨张量注意力,模型架构在 CreateDescriptions 里拼装。 动态查询类 CNeuronFAQOCL 继承自 CNeuronBaseOCL,内部挂了 5 个静态神经层:卷积层扫相邻环境状态形态,常规化层稳训练,压缩层出聚合权重并过 sigmoid 锁在 [0,1],全连接层把聚合系数乘成可训练的基础查询,最后用 CNeuronXCiTOCL 做 transformer 式聚合。缓冲区直接替换,免掉多余复制。 交叉注意力类 CNeuronCrossAttention 继承 CNeuronMH2AttentionOCL 的部分功能,但评估的是两个不同张量(动态查询 vs 编码器压缩状态)的依赖。新增 iWindow_K、iUnits_K 两变量和辅助层 cContext 指针;Init 里绕开直接父类、调 CNeuronBaseOCL::Init 再补初始化。前馈收 2 个层指针,复用父类 OpenCL 内核只改任务空间数组,反向传播也靠缓冲区替换省拷贝。 模型架构沿用 DFFT 的编码器,解码器吃编码器位置编码层的潜在数据(删局部栈嵌入、加位置标签,类似视频帧序列),出动态查询和交叉注意力。Actor 接解码器输出再拼账户状态过两层全连接,输出加随机性;Critic 基本原样只换数据源到解码器。 实盘交互用 Research.mq5 / ResearchRealORL.mq5 / Test.mq5 三个 EA。Research.mq5 在 OnTick 检测新柱,下载历史→填环境缓冲→收账户持仓→送编码器→解码器→Actor,预测动作剔清算后下单,结果写经验回放。训练用 Study.mq5,外层循环计 batch、内层按历史时序喂数据,Actor 反向传播梯度先到解码器再分两路回编码器潜在层,Critic 优化后梯度广播全部参与模型。外汇与贵金属杠杆高,这套 RL 架构过拟合或实盘漂移的概率不低,建议先 MT5 用附件代码跑 Research.mq5 看回放写入是否正常。
◍ FAQOCL 注意力单元的内部装配
CNeuronFAQOCL 是继承自 CNeuronBaseOCL 的复合神经元类,把卷积、批归一化与交叉投影打包进一个前向模块。类内受保护成员包含 cF(卷积)、cWv(值投影)、cNormV(批归一)、cQd 与 cDQd(查询/交叉投影),分别承担特征抽取与注意力权重生成。 Init 方法的实参暴露了结构约束:cF 初始化时卷积核宽度写死为 3*window,输出通道 8,且输入维度用 fmax((int)input_units-2, 1) 兜底。这意味着当 input_units 小于 3 时,卷积实际输入维度会被钳制为 1,特征萃取能力可能显著退化。 以下为类声明与 Init 前半段的原码,可直接贴入 MT5 的 Include/NeuroNet 路径做静态编译验证。注意 activation 在基类 Init 后被显式置为 None,说明该层不做非线性激活,激活推后到子模块 cF.SetActivationFunction(None) 之后处理。
class CNeuronFAQOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class=class="str">"cmt">//--- CNeuronConvOCL cF; CNeuronBaseOCL cWv; CNeuronBatchNormOCL cNormV; CNeuronBaseOCL cQd; CNeuronXCiTOCL cDQd; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class=class="str">"cmt">//--- class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronFAQOCL(class="type">void) {}; ~CNeuronFAQOCL(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 heads, class="type">uint units_count, class="type">uint input_units, 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 defNeuronFAQOCL; } 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 CNeuronFAQOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint heads, class="type">uint units_count, class="type">uint input_units, 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; activation = None; if(!cF.Init(class="num">0, class="num">0, OpenCL, class="num">3 * window, window, class="num">8, fmax((class="type">int)input_units - class="num">2, class="num">1), optimization_type, batch)) class="kw">return false; cF.SetActivationFunction(None);
交叉注意力层的初始化与前向链路
这段实现展示了在 MT5 的 OpenCL 环境里搭一个 FAQOCL 网络时,各子层怎么串起来。cNormV 用 8 个单元、窗口 1,隐藏维度按 input_units 推算,最少留 1 个单元再乘 8;cWv 输出维度是 units_count*window_out、激活走 SIGMOID,而 cQd、cDQd 均设 None 激活,说明它们只做线性投影与多头聚合。 初始化里若 Output 或 Gradient 指针和 cDQd 内部缓冲区不一致,就先释放旧缓冲再接管新指针,避免重复 delete 引发崩溃。这个细节在拷贝神经网络结构时很容易漏,直接表现为回测时 MT5 终端随机闪退。 feedForward 的调用顺序是 cF → cNormV → cWv → cQd → cDQd,每一层失败立即返回 false,意味着任一层 OpenCL 内核编译或显存分配出错,整个前向直接中断。你可以在 OnInit 里打印各 Init 返回值,定位是哪一层在老显卡上挂掉。 CNeuronCrossAttention 继承自多头注意力类,多了 iWindow_K、iUnits_K 和 cContext 指针,并重载了接收 Context 神经元或浮点缓冲的 feedForward。外汇与贵金属行情用这类结构做跨周期特征融合时,过拟合概率偏高,建议小样本先跑通再上实盘历史数据。
if(!cNormV.Init(class="num">8, class="num">1, OpenCL, fmax((class="type">int)input_units - class="num">2, class="num">1) * class="num">8, batch, optimization_type)) class="kw">return false; cNormV.SetActivationFunction(None); if(!cWv.Init(units_count * window_out, class="num">2, OpenCL, class="num">8, optimization_type, batch)) class="kw">return false; cWv.SetActivationFunction(SIGMOID); if(!cQd.Init(class="num">0, class="num">4, OpenCL, units_count * window_out, optimization_type, batch)) class="kw">return false; cQd.SetActivationFunction(None); if(!cDQd.Init(class="num">0, class="num">5, OpenCL, window_out, class="num">3, heads, units_count, class="num">3, optimization_type, batch)) class="kw">return false; cDQd.SetActivationFunction(None); if(Output != cDQd.getOutput()) { Output.BufferFree(); class="kw">delete Output; Output = cDQd.getOutput(); } if(Gradient != cDQd.getGradient()) { Gradient.BufferFree(); class="kw">delete Gradient; Gradient = cDQd.getGradient(); } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFAQOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- if(!cF.FeedForward(NeuronOCL)) class="kw">return false; if(!cNormV.FeedForward(GetPointer(cF))) class="kw">return false; if(!cWv.FeedForward(GetPointer(cNormV))) class="kw">return false; if(!cQd.FeedForward(GetPointer(cWv))) class="kw">return false; if(!cDQd.FeedForward(GetPointer(cQd))) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronCrossAttention : class="kw">public CNeuronMH2AttentionOCL { class="kw">protected: class="type">uint iWindow_K; class="type">uint iUnits_K; CNeuronBaseOCL *cContext; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CNeuronBaseOCL *Context); class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context); class="kw">virtual class="type">bool attentionOut(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CNeuronBaseOCL *Context); class="kw">virtual class="type">bool AttentionInsideGradients(class="type">void); class="kw">public: CNeuronCrossAttention(class="type">void) {}; ~CNeuronCrossAttention(class="type">void) { class="kw">delete cContext; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,