神经网络变得简单(第 72 部分):噪声环境下预测轨迹·综合运用
🛰️

神经网络变得简单(第 72 部分):噪声环境下预测轨迹·综合运用

(3/3)·当预测模型只会输出同质走势时,自监督路标噪声预测如何拆掉简陋预测的墙

案例拆解新手友好 第 3/3 篇

很多人在接上篇把基础架构跑通后,发现模型给出的未来轨迹几乎长一个样,换个行情就失灵。问题不在数据量,而在训练视图太干净,模型只学会了狭义解释历史。本篇把空间一致性模块和噪声预测模块接进同一套流程,看它怎么把多样性场景逼出来。

「带噪声训练样本的状态注入」

这段逻辑跑在扩散模型的反向去噪回路里,核心是先给原始状态向量叠一层正态噪声,再送进编码器。噪声幅度不是拍脑袋定的,而是用 delta 向量的标准差乘 STD_Delta_Multiplier 再加均值,等于让扰动跟着数据自身分布走。 state 的选取用了 MathRand() 平方再除以 32767 的平方,把均匀随机压成靠近 0 的偏态分布,再乘可用长度减 3 减 PrecoderBars 减 batch。若算出来小于 0 就 iter-- 并 continue,相当于跳过这批非法起点。 噪声本身由 Math::MathRandomNormal(0,1,inp_total,ar_noise) 生成,失败直接置 Stop=true 跳出。正常时 noise = mean_delta + std_delta * noise,inp 加上噪声后通过 LastEncoder.BufferWrite 和 Encoder.feedForward 走一遍,Decoder 再接 -1 步前向。 开 MT5 把 STD_Delta_Multiplier 从 1.0 调到 0.5,能明显看到 Comment 里 Decoder 的 Error 波动收窄,但去噪轨迹可能偏向保守。外汇与贵金属杠杆高,这类实验只在策略研发环境跑,实盘前务必隔离验证。

MQL5 / C++
class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / (class="num">2 * (end - state)) + iter) * class="num">100.0 / (Iterations);
class="type">class="kw">string str = StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Decoder", percent,
Decoder.getRecentAverageError());
str += StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Noise Prediction", percent,
Noise.getRecentAverageError());
Comment(str);
ticks = GetTickCount();
      }
    }
class=class="str">"cmt">//--- With noise
vector<class="type">float> std_delta = delta.Std(class="num">0) * STD_Delta_Multiplier;
vector<class="type">float> mean_delta = delta.Mean(class="num">0);
class="type">ulong inp_total = std_delta.Size();
vector<class="type">float> noise = vector<class="type">float>::Zeros(inp_total);
class="type">class="kw">double ar_noise[];
tr = SampleTrajectory(probability);
state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">3 - PrecoderBars - batch));
if(state < class="num">0)
  {
   iter--;
   class="kw">continue;
  }
Encoder.Clear();
Decoder.Clear();
Noise.Clear();
LastEncoder.BufferInit(EmbeddingSize, class="num">0);
end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
for(class="type">int i = state; i < end; i++)
  {
   if(!Math::MathRandomNormal(class="num">0, class="num">1, (class="type">int)inp_total, ar_noise))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   noise.Assign(ar_noise);
   noise = mean_delta + std_delta * noise;
   inp.Assign(Buffer[tr].States[i].state);
   inp = inp + noise;
   State.AssignArray(inp);
   if(!LastEncoder.BufferWrite() ||
!Encoder.feedForward((CBufferFloat*)GetPointer(State), class="num">1, false, (CBufferFloat*)GetPointer(LastEncoder)))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   if(!Decoder.feedForward(GetPointer(Encoder), -class="num">1, (CBufferFloat*)NULL))
     {

解码器误差触发反向传播的条件

这段逻辑出现在自编码器训练循环里,核心是用滚动标准差 STE 来判定预测误差是否「出格」。当 check 向量的最大值大于 0,说明当前帧重建误差超过了 STE 乘上 STE_Multiplier 的阈值,此时才把误差送进 CAGrad 做梯度裁剪并触发 Decoder 与 Encoder 的反向传播。 std_count 被钳在 999 以内,STE 按递推公式更新:新 STE 等于根号下(旧 STE 平方乘 std_count 加上当前 error 平方)除以(std_count+1)。这意味着早期样本权重衰减很快,训练后期单笔异常对阈值的拉动倾向变小。 代码里 feedForward 两次失败会直接置 Stop=true 并 break,跳出训练批次;而只有 check.Max()>0 才进反向传播分支,否则该帧只更新 STE 不调权。开 MT5 把 STE_Multiplier 从默认改到 2.5 左右,能明显看到触发反传的频率下降,模型在外汇 1H 数据上过拟合概率可能降低,但信号滞后会变大。 高危提示:外汇与贵金属杠杆交易风险极高,上述参数调整仅作技术验证,不代表任何收益预期。

MQL5 / C++
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
Stop = true;
break;
      }
      if(!Noise.feedForward(GetPointer(Encoder), -class="num">1, (CBufferFloat*)NULL))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
      target.Assign(Buffer[tr].States[i].state);
      class="type">ulong size = target.Size();
      targets = matrix<class="type">float>::Zeros(class="num">1, size);
      targets.Row(target, class="num">0);
      if(size > BarDescr)
        targets.Reshape(size / BarDescr, BarDescr);
      class="type">ulong shift = targets.Rows();
      targets.Resize(shift + PrecoderBars, class="num">3);
      for(class="type">int t = class="num">0; t < PrecoderBars; t++)
        {
         target.Assign(Buffer[tr].States[i + t].state);
         if(size > BarDescr)
           {
            matrix<class="type">float> temp(class="num">1, size);
            temp.Row(target, class="num">0);
            temp.Reshape(size / BarDescr, BarDescr);
            temp.Resize(size / BarDescr, class="num">3);
            target = temp.Row(temp.Rows() - class="num">1);
           }
         targets.Row(target, shift + t);
        }
      targets.Reshape(class="num">1, targets.Rows()*targets.Cols());
      target = targets.Row(class="num">0);
      Decoder.getResults(result);
      vector<class="type">float> error = target - result;
      std_count = MathMin(std_count, class="num">999);
      STE = MathSqrt((MathPow(STE, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1));
      vector<class="type">float> check = MathAbs(error) - STE * STE_Multiplier;
      if(check.Max() > class="num">0)
        {
         class=class="str">"cmt">//---
         Result.AssignArray(CAGrad(error) + result);
         if(!Decoder.backProp(Result, (CNet *)NULL) ||
            !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient)))

◍ 噪声估计与训练中断的实装细节

这段逻辑跑在自编码器的迭代循环里,核心是用滚动均方误差更新 STE_Noise,并据此判断是否要把误差反向灌回网络。STE_Noise 的递推公式用了样本数 std_count 做加权平均:每来一次 error,就按 (旧方差*计数 + 新误差平方)/(计数+1) 开根号,所以噪声水平会随训练慢慢漂移,而不是写死。 当 check.Max() 大于 0,说明当前某维度误差超过了噪声带,这时才执行 CAGrad(error)+result 并触发 backProp。若 Encoder 或 Noise 的反向传播返回失败,立即 PrintFormat 打出函数名与行号、置 Stop=true 并 break,避免脏梯度继续写权重。 进度显示走的是 GetTickCount 节流:距上次刷新超 500 毫秒才重算 percent 并用 Comment 同时打印 Decoder 与 Noise Prediction 的近期平均误差,精度到 15.8f。循环结束清 Comment,最后两行 PrintFormat 把两个网络的最终平均误差打到日志,紧接 ExpertRemove() 卸载 EA——整个训练段在 MT5 上跑完即自退,不占盘口。 把这段代码直接贴进你的 EA 训练函数,改 STE_Noise_Multiplier 和 500 毫秒阈值,能在 EURUSD 的 M15 上看到噪声带收敛速度明显变化;外汇与贵金属杠杆高,回测结论仅代表历史样本,实盘可能偏离。

MQL5 / C++
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
         }
       }
       target = noise;
       Noise.getResults(result);
       error = (target - result) * STE_Noise_Multiplier;
       STE_Noise = MathSqrt((MathPow(STE_Noise, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1));
       std_count++;
       check = MathAbs(error) - STE_Noise;
       if(check.Max() > class="num">0)
         {
          class=class="str">"cmt">//---
          Result.AssignArray(CAGrad(error) + result);
          if(!Noise.backProp(Result, (CNet *)NULL) ||
             !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient)))
            {
             PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
             Stop = true;
             break;
            }
         }
       Encoder.getResults(result);
       LastEncoder.AssignArray(result);
       if(GetTickCount() - ticks > class="num">500)
         {
          class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / (class="num">2 * (end - state)) + iter + class="num">0.5) * class="num">100.0 / (Iterations);
          class="type">class="kw">string str = StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Decoder", percent,
Decoder.getRecentAverageError());
          str += StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Noise Prediction", percent,
Noise.getRecentAverageError());
          Comment(str);
          ticks = GetTickCount();
         }
       }
     }
   Comment("");
class=class="str">"cmt">//---
   PrintFormat("%s -> %d -> %-20s %class="num">10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError());
   PrintFormat("%s -> %d -> %-20s %class="num">10.7f", __FUNCTION__, __LINE__, "Noise Prediction", Noise.getRecentAverageError());
   ExpertRemove();
class=class="str">"cmt">//---
   }

「在 MT5 里跑通自监督路标模型」

把“自监督路标噪声预测”接进之前的轨迹函数训练 EA,再配上目标条件预测编码,预测质量有可能比旧版更好。验证的唯一办法是丢进 MetaTrader 5 策略测试器,用真实行情跑一遍。 模型和前文一致,吃 EURUSD H1 的历史数据。训练窗口取 2023 年前 7 个月,测试窗口紧接其后用 2023 年 8 月,避免未来函数嫌疑。 因为模型架构和数据结构没动,之前 GCPC 方法生成的经验回放缓冲区能直接复用。复制一份 “SSWNP.bd” 就能跳过采样,进训练。 训练分两段:第一段只喂历史价和指标进编码器,训练轨迹函数,这时账户状态、持仓都不参与,所以所有轨迹等价,现有样本够用;第二段做行为政策搜索,要交替训模型并补采样本,才能逼近不同账户状态下的较优动作。 实测结果是,训出的模型在训练段和 8 月测试段都出了盈利。外汇和贵金属高杠杆品种回测盈利不代表实盘概率,开 MT5 用同周期同品种复跑是唯一确证方式。

演示归演示,别直接搬去实盘

这套自监督路标噪声预测(SSWNP)由空间一致性模块和噪声预测模块组成,核心思路是往历史轨迹里注入噪声,逼模型在随机约束下给出更多样的未来路径。我们在 GCPC 结构里接入后跑通了测试,证实了方法本身在轨迹预测上的有效性。 但必须说清:文中所有程序只用于演示技术链路,没有经过真实行情下的风控校验。外汇和贵金属杠杆高、跳空频繁,直接拿去下单可能迅速放大亏损。 如果你在 MT5 里自己复现,建议先关掉自动交易,用历史数据看注入噪声后的路径分布是否更贴合震荡段,再谈下一步。

◍ 把这条线请下神坛

这套 LSTM 优化系列走到这里,落地的不是某个‘圣杯’指标,而是一组可复用的工程文件。样本收集靠 Research.mq5 与 ResearchRealORL.mq5,策略训练交给 Study.mq5 和 StudyEncoder.mq5,模型验证由 Test.mq5 跑通,底层状态与网络结构则分别封装在 Trajectory.mqh、NeuroNet.mqh 与 NeuroNet.cl 里,整套压缩包 MQL5.zip 体积 789.06 KB。 真正值得做的,是把这些 EA 和类库拖进 MT5 的 MetaEditor 逐一看一遍,尤其注意 NeuroNet.cl 里 OpenCL 内核的并行写法——它决定了训练在显卡上的实际吞吐。外汇与贵金属市场高杠杆、高波动,任何模型都只是概率工具,回测顺滑不代表实盘能活。 线画完了,神坛留给人去拜,你只管把代码编译跑起来,用真实 tick 验证它什么时候会哑火。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多轨迹分布的散点,你专注判断哪条场景更值得跟。

常见问题

干净视图就是训练集里原始的历史路标序列,加噪视图是在原特征空间里给过去轨迹叠了噪声的版本,用来打破模型对训练数据的狭义记忆。
论文实验显示单独用空间一致性模块时训练性能欠佳,两个模块集成才能兼顾时空特征与空间多样性建模。
按固定时间步抽取坐标点存成序列即可,文末程序给出了具体的存取与加噪函数范例。
目前小布内置的是分布散点诊断,完整 SSWNP 推理需自行部署文中所用程序,再把输出接入品种页做可视化。
比例过高会偏离原始形态,模型可能学到噪声而非走势,实际调参倾向从小系数起步做交叉验证。