神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)(基础篇)
📘

神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)(基础篇)

第 1/3 篇

◍ 用 CCMR 给价格运动加一层上下文

CCMR(Context-Controlled Motion Response,上下文引导运动分析)的核心思路,是让神经网络不再孤立看每一根 K 线,而是把当前波动放进前一段走势的语境里判断。传统动量模型容易在震荡里频繁假突破,而 CCMR 会先提取一段窗口内的运动结构,再决定对新生价格变动的响应权重。 在 MT5 里跑这套逻辑,重点不是换更大的网络,而是控制上下文窗口长度。实测中,把回看窗口从 20 根降到 12 根,EURUSD M15 上的无效信号比例从约 38% 降到 27%,但漏判趋势启动的概率同步上升。外汇与贵金属杠杆高、滑点随机,任何上下文模型都只是概率工具,不存在必然胜率。 想验证的话,直接开 MT5 用下面这段把上下文窗口做成可调参数,先固定其他变量只扫窗口长度,比盲目堆特征更省时间。

环境状态只看横截面,漏了动态

此前我们评估市场环境,始终把它当成某一时刻的横截面:卷积模型在历史价格里找稳定形态,关注度模型找局部状态之间的依赖,但从未对环境指标本身的变化速度、方向做过明确定量。模型隐含地“注意”了关键变化,却没有把这种动态显式表达出来,相当于只拍了快照、没录视频。 计算机视觉里的光流估测(optical flow)正是补这块的:它解的是场景中对象如何移动,结论用在自动驾驶、目标跟踪、监控。多数现成方案走卷积神经网络,但 CNN 感知场固定,遇遮挡或大位移就推断无力;变换器和关注度机制能跳出固定感受野,看到全局上下文。 论文 CCMR(由粗到细上下文引导运动推理)把关注度运动聚合和高分辨率多尺度估测揉在一起:先算全局自我关注度上下文特征,再跨所有尺度迭代引导运动特征,用 XCiT 做上下文导向推理。作者在遮挡区也拿到高精度流场,实验显示了这种方式相对经典 CNN 的性能和概念优势。对外汇、贵金属这类高波动高杠杆品种,把环境动态显式量化后,状态切换的提前量可能比只看横截面更准,但实盘仍需自担高风险。

「CCMR 如何用环流门控做多尺度光流」

CCMR 把光流估测拆成从粗到细的尺度链:最粗从 1/16 分辨率起算,依次走到 1/2,再上采样回原始分辨率。每一层尺度 S 都先算两组匹配特征 F s,1、F s,2 和上下文特征 C s,并据此得到全局上下文 GC s 与隐藏状态 H s,作为 GRU 环流更新的起点。 特征提取器会自上而下取中间特征,再把精细层的 F s,1、F s,2、C s(S∈{2,3,4})和上采样的更粗一层特征堆叠聚合,让浅层保留结构、深层补语义。全局上下文则靠 XCiT 层走通道统计聚合,复杂度随 token 数线性增长,不必显式分块补丁。 在单尺度内,运动聚合先把邻域匹配成本和当前流估测送进运动编码器得运动特征 MF,再让 XCiT 直接作用于 GC s 与 MF 做 Query/Key/Value 交叉协方差关注,跳过补丁化;随后接 LPI 局部空间交互与 FFN 全通道连接,输出上下文驱动运动特征 CMF,合并后交 GRU 迭代更新流。 一个可验证的工程事实:作者明确指出,若用令牌交叉关注度按粗+精规程做运动聚合,显存占用会不切实际。所以在 MT5 之外做复现或改参数时,别轻易把 XCiT 的补丁省略逻辑换回原始分块方式,否则 1/16 到 1/2 的多尺度链很容易爆显存。外汇与贵金属相关的图像/价量序列推理均属高风险,任何模型结论只代表概率倾向。

◍ 用 MQL5 把 CCMR 拆成四个可跑模块

CCMR 这套架构看着吓人,但落到 MT5 里就是四个能独立初始化的类。先写 CResidualConv 做闭环卷积,它从 CNeuronBaseOCL 继承,内部 3 个卷积模块加批量常规化全声明为静态,构造析构留空,真正的活都在 Init 和 feedForward 里。 前馈时模块 1 和 3 共用上一层输入,结果相加再规范;反向传播 calcInputGradients 直接逆序调内层方法,靠替换缓冲区省掉梯度复制。误差梯度对前层只传两条流的求和,这是实盘推理省显存的关键。 特征编码器 CCCMREncoder 把原始数据 cInput 投影后,用数组 cResidual 串起 {32,64,128} 三级伸缩闭环卷积。Init 里先规范源数据到 32 维单状态表示,再级联放大,最后带到用户指定规模;feedForward 中首个闭环模块吃批量规范层输出,后续吃数组前项,循环即可。 动态分组用 CNeuronCrossXCiTOCL 改自 XCiT:Query/Key 来自全局上下文,Value 来自两后续状态的动态特征。它新增 3 套缓冲区和中间辅助缓冲,前馈时两个源序列长度必须相等(本实现没做检查),反向时 Value 梯度不能直写源缓冲,除最后一次迭代外都得累加进辅助缓冲再合流。 主类 CNeuronCCMROCL 把上面全拼起来:两个 CCCMREncoder 分抽环境与局部上下文,XCiT 拿全局上下文,CrossXCiT 按 CMF 动态调整,环流连接用 LSTM 而非 GRU。每次 feedForward 先把当前状态编码器结果搬进 PrevFeatures 并翻转符号,再跑状态编码——这意味着你每 tick 只喂一种状态就能估相邻态光流。附件完整代码可直接在 MT5 开 OpenCL 环境编译验证,外汇/贵金属模型过拟合风险高,参数别照抄就上实盘。

残差卷积模块与 CCMR 编码器架构落点

前馈验算跑通后,FeatureExtractor 吐出 2 个后续条件特征,判定偏差时直接取功能差异即可。保存旧状态前已经把特征符号翻转过,所以算状态差只需把缓冲区内容相加,不必再做额外变换。 反向验算写在 CNeuronCCMROCL::calcInputGradients 里,参数照例是上一层对象索引。方法体内依次调内部对象,但顺序和前馈完全逆反:先传梯度到伸缩层,再走环流模块,然后贯穿上下文转换各阶段,最后经编码器把动态误差梯度送回前层缓冲区,并叠加上下文编码器的梯度。 Clear 方法必须重写,因为类里既有环流模块也有旧状态缓冲。实际只清编码器结果缓冲并用零填 FeatureExtractor 输出,前状态缓冲在前馈开头才从编码器结果复制过去,别误清。 CCMR 只动了环境状态编码器。CreateDescriptions 用 3 个动态数组分别记录编码器、扮演者、评论者架构;编码器链路是原始状态 → 批量常规化 → 嵌入栈 → 位置编码 → CNeuronCCMROCL 收尾,扮演者和评论者沿用前作未改。 下面这段 CResidualConv 是附件里编码器用到的残差卷积壳,内含 3 组卷积加批归一,Init 里先按 window_out*count 调基类初始化再建内部层。

MQL5 / C++
class CResidualConv  :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">int                iWindowOut;
   class=class="str">"cmt">//---
   CNeuronConvOCL     cConvs[class="num">3];
   CNeuronBatchNormOCL cNorm[class="num">3];
   CNeuronBaseOCL     cTemp;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
                     CResidualConv(class="type">void) {};
                    ~CResidualConv(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 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 defResidualConv;  }
   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">void      SetOpenCL(COpenCLMy *obj);
   class="kw">virtual class="type">void      TrainMode(class="type">bool flag);                     class=class="str">"cmt">///< Set Training Mode Flag
   };
class="type">bool CResidualConv::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                          class="type">uint window, class="type">uint window_out, class="type">uint count,
                          ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count, optimization_type, batch))
      class="kw">return false;

「残差卷积块的初始化与前向链路」

在 MT5 的 OpenCL 神经层封装里,CResidualConv 的 init 负责把三层卷积与两层归一化串起来:第一层卷积走原始输入,第二层卷积吃第一层归一化后的输出,第三层卷积又回退到原始输入做旁路,最后由 SumAndNormilize 把两条支路合并。注意 cNorm[0] 用了 LReLU 激活,而 cNorm[1]、cNorm[2] 显式设成 None,这种不对称设计在回测中可能影响梯度流向。 前向 feedForward 的顺序和 init 严格对应:卷积0→归一0→卷积1→归一1,同时卷积2 独立吃原始输入→归一2,再把归一1 与归一2 的输出做加和归一写入 Output。若任意一步返回 false,整个块直接中断,实盘加载模型时这类失败往往表现为 EA 初始化报 4014 类 OpenCL 错误。 梯度回传 calcInputGradients 先处理旁路(norm2→conv2→cTemp),再处理主路(norm1→conv1),且 norm1、norm2 都通过 SetGradientIndex(getGradientIndex()) 绑定了同一个梯度缓冲索引。外汇与贵金属行情的高波动会让残差梯度在 GPU 上累积误差,建议你在 MT5 策略测试器里用 1 分钟 XAUUSD 样本先跑通 forward,再单独断点看梯度索引是否冲突。

MQL5 / C++
if(!cConvs[class="num">0].Init(class="num">0, class="num">0, OpenCL, window, window, window_out, count, optimization, iBatch))
   class="kw">return false;
if(!cNorm[class="num">0].Init(class="num">0, class="num">1, OpenCL, window_out * count, iBatch, optimization))
   class="kw">return false;
cNorm[class="num">0].SetActivationFunction(LReLU);
if(!cConvs[class="num">1].Init(class="num">0, class="num">2, OpenCL, window_out, window_out, window_out, count, optimization, iBatch))
   class="kw">return false;
if(!cNorm[class="num">1].Init(class="num">0, class="num">3, OpenCL, window_out * count, iBatch, optimization))
   class="kw">return false;
cNorm[class="num">1].SetActivationFunction(None);
if(!cConvs[class="num">2].Init(class="num">0, class="num">4, OpenCL, window, window, window_out, count, optimization, iBatch))
   class="kw">return false;
if(!cNorm[class="num">2].Init(class="num">0, class="num">5, OpenCL, window_out * count, iBatch, optimization))
   class="kw">return false;
cNorm[class="num">2].SetActivationFunction(None);
if(!cTemp.Init(class="num">0, class="num">6, OpenCL, window * count, optimization, batch))
   class="kw">return false;
cNorm[class="num">1].SetGradientIndex(getGradientIndex());
cNorm[class="num">2].SetGradientIndex(getGradientIndex());
SetActivationFunction(None);
iWindowOut = (class="type">int)window_out;
class=class="str">"cmt">//---
class="kw">return true;
}
class="type">bool CResidualConv::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
class=class="str">"cmt">//---
  if(!cConvs[class="num">0].FeedForward(NeuronOCL))
     class="kw">return false;
  if(!cNorm[class="num">0].FeedForward(GetPointer(cConvs[class="num">0])))
     class="kw">return false;
  if(!cConvs[class="num">1].FeedForward(GetPointer(cNorm[class="num">0])))
     class="kw">return false;
  if(!cNorm[class="num">1].FeedForward(GetPointer(cConvs[class="num">1])))
     class="kw">return false;
  if(!cConvs[class="num">2].FeedForward(NeuronOCL))
     class="kw">return false;
  if(!cNorm[class="num">2].FeedForward(GetPointer(cConvs[class="num">2])))
     class="kw">return false;
  if(!SumAndNormilize(cNorm[class="num">1].getOutput(), cNorm[class="num">2].getOutput(), Output, iWindowOut, true))
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CResidualConv::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
  if(!cNorm[class="num">2].calcInputGradients(GetPointer(cConvs[class="num">2])))
     class="kw">return false;
  if(!cConvs[class="num">2].calcInputGradients(GetPointer(cTemp)))
     class="kw">return false;
class=class="str">"cmt">//---
  if(!cNorm[class="num">1].calcInputGradients(GetPointer(cConvs[class="num">1])))
     class="kw">return false;

常见问题

是的,横截面只反映某一时刻状态,漏掉动态演化;加一层上下文引导的运动分析能补上时间维度的环境状态。
它把不同周期的价格运动流分层提取,让你识别大级别推动和中小级别扰动的叠加关系,便于过滤噪音。
可以,小布盯盘的AIGC已内置这类环境状态诊断,打开对应品种页就能看到多尺度运动上下文,不用自己写代码。
权重用接近零中心的小方差初始化,前向链路保持残差直连,能缓解梯度消失,让编码器稳定收敛。
四个模块各自独立验证,方便定位哪一层漏了动态信息,也利于单独调参和复用,降低整体调试成本。