神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ)(基础篇)
📘

神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ)(基础篇)

第 1/3 篇

「正文」

神经网络变得简单(第 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) 之后处理。

MQL5 / C++
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。外汇与贵金属行情用这类结构做跨周期特征融合时,过拟合概率偏高,建议小样本先跑通再上实盘历史数据。

MQL5 / C++
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,

常见问题

静态环境假设价格独立同分布,但走势是连续帧相关的;把查询改成跟随当前状态帧动态聚合,才能捕捉转折概率。
需要状态编码器、帧跟随的查询生成器、交叉注意力层和解码器;先跑通前向链路再调窗口长度。
小布可接入你的品种页,自动标注状态帧并跑轻量回测,把聚合前后的信号偏移直接画给你看。
权重别全零起步,用近帧缩放初始化;前向时先算查询-键相似度再归一化,避免早期梯度消失。
建议状态帧取 8~12 根、聚合头 2~4 个;贵金属波动大,先小样本验证再放大周期,注意高风险。