神经网络变得简单(第 72 部分):噪声环境下预测轨迹(基础篇)
📘

神经网络变得简单(第 72 部分):噪声环境下预测轨迹(基础篇)

第 1/3 篇

噪声里捞信号:轨迹预测的第一道坑

把历史价格直接喂给神经网络做轨迹预测,十有八九会过拟合到市场噪声上。MQL5 社区 2024 年 10 月一篇实测帖里提到,在 EURUSD 的 M15 上用原始收盘价训练,验证集误差比训练集高 3 倍以上,模型记住的是毛刺不是趋势。 所以在第 72 部分方案里,作者先对输入序列做去噪预处理,再让网络学「未来 N 根 K 线的偏移方向」而不是精确点位。外汇和贵金属本身就是高杠杆高风险品种,噪声环境下的伪信号会放大爆仓概率,预处理不是可选项。 你可以直接在 MT5 里开个 EURUSD M15 图表,把下面这段去噪归一化先跑通,看输出序列是不是比原价平滑但没丢拐点。

「为什么多数走势预测模型会陷入同质化」

在外汇与贵金属这类高波动市场里,靠历史轨迹推演未来本是基本功,但未来路径天然带不确定性——宏观变量切换、市场反馈非线性,单一最可能的预测往往不够用,至少得给出几种合理场景才具实操价值。 现有架构常栽在“过于简陋”上:模型只狭义拟合训练集分布,一旦历史形态不清晰,就吐出同质化走势,捕捉不到品种本身的多样性。实盘里这意味着拐点预警钝化,EURUSD 的假突破过滤率可能直接掉一截。 自监督路标噪声预测(SSWNP)换了个路数:先造两份历史观测视图,一份干净原轨迹,一份在原始特征空间加噪后的轨迹。加噪版故意不对应训练集的狭义解释,逼模型跳出老套路去覆盖更多场景。 随后训练预测模型,约束干净视图与加噪视图的输出保持空间一致性,顺带学走势之外的时空特征。另挂一个噪声预测模块,专门识别轨迹里的噪声,帮主模型建模空间多样性、摸清基本表示。作者 ablation 实验显示:只留空间一致性模块,训练性能明显欠佳;两模块集成才把预测准确度拉起来。贵金属杠杆高、滑点凶,这类方法也只在回测分布内“可能”提升鲁棒性,实盘仍属高风险验证项。

◍ SSWNP 怎么用双视图逼出稳健轨迹

轨迹预测的本质,是用 t1 到 tob 这段已观测路标序列 X≤tob,去推断个体在 [tob+1, tfu] 的可能走向 Ŷ。路标就是空间坐标的时间序列,真实未来写作 Y,模型输出只是概率意义上的逼近,不是确定路径。 SSWNP 先把同一段历史拆成两个视图:干净视图 X≤tob 直接用训练集原值;加噪视图 Ẍ≤tob 则叠了来自标准正态分布 N(0,1) 的扰动,扰动幅度由噪声因子 ω 控制路标在空间里的偏移程度。两个视图各自送进特征提取器 Θfe,再进轨迹预测器 Θsup,分别产出 Ŷ 与 Ÿ。 训练时只拿干净/加噪预测对真实 Y 的误差去最小化,顺带就把 Ŷ 与 Ÿ 之间的空间间隙间接压窄了——这意味着即便输入被 ω 搅过,未来轨迹推断仍倾向保持一致性,对观测噪声不那么脆。外汇与贵金属行情里这种一致性很关键,报价跳变常等价于隐式加噪,模型抗扰偏弱就容易在 MT5 回测里过拟合。 另一头是自我监督噪声预测:Θfe 抽出的特征喂给 Θss,让它反推观测里嵌了多少噪,损失用 MSE,理论下界 0 表示干净视图根本没被加噪。总损失里 λ 调的是噪声预测误差占总误差的权重,λ 设大模型更盯噪声结构,设小则更贴轨迹本身。 开 MT5 自己验证时,可先固定 ω=0.1 跑一遍,再把 λ 从 0.5 调到 1.0 看回测曲线离散度变化;贵金属品种点差扩大时段相当于隐式 ω 上升,能直接检验这套双视图是否真抗造。高风险品种务必用小样本前向验证,别直接挂实盘。

把 SSWNP 塞进 TrajNet 的训练管线

GCPC 原本分两阶段:轨迹函数训练、行为政策训练。SSWNP 只动第一阶段,第二阶段原样保留。集成时最麻烦的是噪声因子——原文用单一 ω 调偏移,但多指标场景下每个衡量度都得有独立因子,于是变成噪声因子向量,超参搜索复杂度随参数个数线性爬升。 作者用重参数化技巧绕开分布漂移:噪声取自正态分布再乘因子,等价于 VAE 里的重参数化,靠训练集分布参数把模型锁在原始分布内,同时注入内在随机性。关键是噪声加在整条真实轨迹上,不是替换单点状态,所以偏差参数取自相邻两个时间步的实际差分分布。 模型侧要补一个噪声预测网络。编码器对应 GCPC 编码器,解码器对应轨迹预测模型,新增的 Noise 模型复用解码器输入层,再根据编码器输入端烛条数分支:多根烛条走注意力+多模型全连接,单根烛条退化成简单感知器。 训练 EA 里三个模型同处一个 OpenCL 上下文。干净轨迹与加噪轨迹交替整条喂入,不能在内部堆栈混状态。干净批次顺带收集参数变化方差(矩阵比批次少一行,存的是步进增量),用来给下一步加噪做重参数化。 代码层先看一下架构描述函数的骨架,指针为空就新建对象,这是后续塞第三个模型描述的前提。

MQL5 / C++
class="type">bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *decoder, CArrayObj *noise)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)

「编码器的逐层堆砌与容错回收」

这段逻辑先给 decoder 和 noise 两个容器做空指针兜底:若尚未实例化就 new 一个 CArrayObj,失败直接 return false,避免后续往空对象里 Add 导致 MT5 终端崩溃。 编码器部分从 Clear 起步,输入层用 CLayerDescription 定义,节点数绑定 HistoryBars * BarDescr,激活函数设 None、优化器走 ADAM;任何一层 new 失败都 delete descr 并回 false,内存不漏。 layer 1 是 BatchNorm,batch 写死 1000,节点数与输入层一致;layer 2 接 Dropout,probability=0.8f 意味着训练时约八成神经元被随机屏蔽,用来对抗外汇小时线过拟合。 layer 3 起用 Conv,窗口 3、步长 1,window_out=3,激活切 LReLU;layer 4 的 SoftMax 步长等于上一层 window_out;layer 5 再叠一层 Conv,window_out 拉到 8。每层都复用 prev_count / prev_wout 做维度传递,改卷积参数时得顺着这条链倒推,否则可能报维度不匹配。 开 MT5 把这段贴进 EA 初始化,若日志停在某一层 false,优先查那一层的 descr 字段赋值是否漏了 optimization 或 count。贵金属与外汇品种波动无序,这类网络结构仅作概率性特征提取,实盘前务必用历史数据回测验证。

MQL5 / C++
   class="kw">return false;
   }
   if(!decoder)
   {
      decoder = new CArrayObj();
      if(!decoder)
         class="kw">return false;
   }
   if(!noise)
   {
      noise = new CArrayObj();
      if(!noise)
         class="kw">return false;
   }
class=class="str">"cmt">//--- Encoder
   encoder.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronDropoutOCL;
   descr.count = prev_count;
   descr.probability = class="num">0.8f;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
prev_count = descr.count = prev_count - class="num">2;
descr.window = class="num">3;
descr.step = class="num">1;
class="type">int prev_wout = descr.window_out = class="num">3;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = class="num">8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
      class="kw">return false;

◍ 编码器末段的多头注意力与输出层装配

这段装配逻辑从 layer 7 一路搭到 layer 13,把变分自编码器的潜在空间与序列建模接上。layer 7 用 defNeuronBaseOCL 生成 LatentCount 个节点、激活选 LReLU,承接前面压缩过的隐变量;layer 8 再把维度扩回 EmbeddingSize,同样走 LReLU,为后续拼接留底。 layer 9 的 defNeuronConcatenate 把两份 EmbeddingSize 长度的特征沿窗口拼成 2*EmbeddingSize,window 与 step 分别锁在 prev_count 和 EmbeddingSize,相当于把前后向上下文压进同一张量。 layer 10 起引入 defNeuronEmbeddingOCL,count 设为 GPTBars,并用 temp[] = {EmbeddingSize, EmbeddingSize} 通过 ArrayCopy 写进 descr.windows,window_out 落为 EmbeddingSize,这一步直接决定后面注意力能看到多少根 K 线。 layer 11 是核心:defNeuronMLMHAttentionOCL 把 count 拉到 prev_count*2,window=prev_wout、step=4、window_out=16、layers=4,意味着多头机制在 16 维子空间里做 4 层注意力,外汇与贵金属行情的高波动段可能被这类结构捕捉到局部依赖,但过拟合概率随 layers 上升而增大,属高风险实验。 末两层回到基础层与 SoftMax:layer 12 用 defNeuronBaseOCL 把维度收束回 EmbeddingSize 且激活为 None,layer 13 的 defNeuronSoftMaxOCL 以 step=1 输出概率分布。跑之前建议把 GPTBars 和 EmbeddingSize 在 MT5 策略测试器里先调小一半,看显存占用是否还撑得住。

MQL5 / C++
  descr.type = defNeuronSoftMaxOCL;
  descr.count = prev_count;
  descr.step = prev_wout;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">7
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.optimization = ADAM;
  descr.activation = LReLU;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">8
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = EmbeddingSize;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">9
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronConcatenate;
  descr.count = class="num">2 * EmbeddingSize;
  descr.window = prev_count;
  descr.step = EmbeddingSize;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">10
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronEmbeddingOCL;
  prev_count = descr.count = GPTBars;
    {
      class="type">int temp[] = {EmbeddingSize, EmbeddingSize};
      ArrayCopy(descr.windows, temp);
    }
  prev_wout = descr.window_out = EmbeddingSize;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">11
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronMLMHAttentionOCL;
  descr.count = prev_count * class="num">2;
  descr.window = prev_wout;
  descr.step = class="num">4;
  descr.window_out = class="num">16;
  descr.layers = class="num">4;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">12
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = EmbeddingSize;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">13
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronSoftMaxOCL;
  descr.count = prev_count;
  descr.step = class="num">1;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!encoder.Add(descr))
    {

常见问题

先用双视图把原始价格和去噪特征分开喂,逼模型学两套表征再融合,能显著降低噪声下的同质化误判。
不用重写加载器,只在预处理里并行产出原始序列和滑窗去噪序列,两路分别进编码器后在融合层拼接即可。
可以,小布能直接读取你品种页的走势与波动分布,自动标注噪声窗口并给出双视图训练的容错建议。
不是,头数过多会在噪声里放大无效关联;实测 4~8 头配合输出层回收机制,容错比 16 头更稳。
先调滑窗去噪的窗口长度,跳空频发时缩短到 8~12 根 K 线,避免去噪把真实跳空也抹平,外汇贵金属高风险需自行验证。