神经网络变得简单(第 91 部分):频域预测(FreDF)·进阶篇
📡

神经网络变得简单(第 91 部分):频域预测(FreDF)·进阶篇

(2/3)·多数多步预测模型默认输出步独立,却忽略标签自相关,频域对齐正是破局点

实战向进阶 第 2/3 篇

直接预测范式同步生成多步结果,却悄悄假设各步相互独立。可预测值本就是原序列的延续,自带强自相关。这种假设与数据的错位,正是很多模型样本外表现滑坡的暗坑。

◍ 频率域神经层的缓冲与梯度核初始化

在 MT5 用 OpenCL 跑频率域深度网络时,FFT 输入长度必须凑成 2 的幂。下面这段逻辑会先拿 MathPow(2, power) 和 size 比对,不等就 power++,最终 iFFTin 取到最近的 2 的幂次,比如 size=500 时会被顶到 512。 随后按 bTranspose 决定 n 是窗口数还是样本数,给 9 组复数缓冲(实/虚各一)做 BufferInit(iFFTin * n, 0) 并推到 OpenCL 显存;任何一组失败直接 return false,整套前向传播就不会启动。 误差反传的两个核心 kernel 很轻量:GradientMSA 在全局 id 处算 matrix_g[i] = matrix_t[i] - matrix_o[i],即目标减输出;CumulativeGradient 则用 alpha 做指数滑动,gradient_out[i] = alpha * gradient_freq[i] + (1-alpha) * gradient_tmp[i]。把 alpha 从 0.9 调到 0.99,梯度累积的惯性会明显变长,外汇 1 分钟级信号可能更平滑但也更滞后。 开 MT5 把这段代码塞进自定义神经层类,先打印 iFFTin 确认对齐了 2 的幂,再小步改 alpha 观察显存占用和回测曲线,贵金属与外汇杠杆高,验证时请用模拟盘。

MQL5 / C++
if(MathPow(class="num">2, power) != size)
     power++;
 iFFTin = class="type">uint(MathPow(class="num">2, power));
class=class="str">"cmt">//---
 class="type">uint n = (bTranspose ? iWindow : iCount);
 if(!cForecastFreRe.BufferInit(iFFTin * n, class="num">0) || !cForecastFreRe.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cForecastFreIm.BufferInit(iFFTin * n, class="num">0) || !cForecastFreIm.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cTargetFreRe.BufferInit(iFFTin * n, class="num">0) || !cTargetFreRe.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cTargetFreIm.BufferInit(iFFTin * n, class="num">0) || !cTargetFreIm.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cLossFreRe.BufferInit(iFFTin * n, class="num">0) || !cLossFreRe.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cLossFreIm.BufferInit(iFFTin * n, class="num">0) || !cLossFreIm.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cGradientFreRe.BufferInit(iFFTin * n, class="num">0) || !cGradientFreRe.BufferCreate(OpenCL))
    class="kw">return false;
 if(!cGradientFreIm.BufferInit(iFFTin * n, class="num">0) || !cGradientFreIm.BufferCreate(OpenCL))
    class="kw">return false;
     if(!cTranspose.BufferInit(iWindow * iCount, class="num">0) || !cTranspose.BufferCreate(OpenCL))
       class="kw">return false;
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CNeuronFreDFOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL || !NeuronOCL.getOutput())
     class="kw">return false;
  if(NeuronOCL.getOutput() != Output)
    {
      Output.BufferFree();
      class="kw">delete Output;
      Output = NeuronOCL.getOutput();
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void GradientMSA(__global class="type">class="kw">float *matrix_t,
                       __global class="type">class="kw">float *matrix_o,
                       __global class="type">class="kw">float *matrix_g
                       )
  {
  class="type">int i = get_global_id(class="num">0);
  matrix_g[i] = matrix_t[i] - matrix_o[i];
  }
__kernel class="type">void CumulativeGradient(__global class="type">class="kw">float *gradient_freq,
                            __global class="type">class="kw">float *gradient_tmp,
                            __global class="type">class="kw">float *gradient_out,
                            class="type">class="kw">float alpha
                            )
  {
  class="type">int i = get_global_id(class="num">0);
  gradient_out[i] = alpha * gradient_freq[i] + (class="num">1 - alpha) * gradient_tmp[i];
  }
class="type">bool CNeuronFreDFOCL::calcOutputGradients(CArrayFloat *Target, class="type">class="kw">float &error)
  {

「梯度同步与频域损失回传的判错链」

这段逻辑是训练循环里最容易被忽略的守门员:任何一步数组长度不对或指针写入失败,整个梯度更新直接返 false,MT5 上表现为指标算到一半不刷新。 先看前几行判错:Target 为空、或者 Target 样本数少于 Output,立即退出;两者相等时把 Target 整段 AssignArray 给 Gradient,不等则逐点 Update 覆盖已有梯度缓冲。 随后 BufferWrite 落盘,若 bTranspose 为真,先对 Output 和 Gradient 做转置再跑 FFT(实/虚部分别进 cForecastFreRe/Im 与 cTargetFreRe/Im);不为真就直接对原数组 FFT。这里转置与否会影响 iWindow 与 iCount 的传参顺序,调参时若发现频谱错位,八成是这两行传反了。 频域里用 FreqMSA 算目标与预测的实数/虚数残差,存进 cLossFreRe/Im,同时对 Gradient 与 Output 做另一路 FreqMSA。最后逆 FFT 把损失谱变回 cGradientFreRe/Im,若开了转置再转置回来跑 CumulativeGradient 累加,学习率 fAlpha 在此生效。 开 MT5 把这段塞进你的 EA 训练函数,故意把 Target.Total() 设成比 Output 小 1,能直接验证第一道 return false 是否如预期卡住,避免静默训练出废模型。外汇与贵金属品种上跑这类频域梯度请务必用小资金验证,高频噪声可能导致梯度爆炸。

MQL5 / C++
if(!Target)
   class="kw">return false;
if(Target.Total() < Output.Total())
   class="kw">return false;
if(Target.Total() == Output.Total())
   {
     if(!Gradient.AssignArray(Target))
         class="kw">return false;
   }
else
   {
     for(class="type">int i = class="num">0; i < Output.Total(); i++)
       {
         if(!Gradient.Update(i, Target.At(i)))
             class="kw">return false;
       }
   }
if(!Gradient.BufferWrite())
   class="kw">return false;
if(bTranspose)
   {
     if(!Transpose(Output, GetPointer(cTranspose), iWindow, iCount))
         class="kw">return false;
     if(!FFT(GetPointer(cTranspose), NULL, GetPointer(cForecastFreRe), GetPointer(cForecastFreIm), false))
         class="kw">return false;
     if(!Transpose(Gradient, GetPointer(cTranspose), iWindow, iCount))
         class="kw">return false;
     if(!FFT(GetPointer(cTranspose), NULL, GetPointer(cTargetFreRe), GetPointer(cTargetFreIm), false))
         class="kw">return false;
   }
else
   {
     if(!FFT(Output, NULL, GetPointer(cForecastFreRe), GetPointer(cForecastFreIm), false))
         class="kw">return false;
     if(!FFT(Gradient, NULL, GetPointer(cTargetFreRe), GetPointer(cTargetFreIm), false))
         class="kw">return false;
   }
if(!FreqMSA(GetPointer(cTargetFreRe), GetPointer(cForecastFreRe), GetPointer(cLossFreRe)))
     class="kw">return false;
if(!FreqMSA(GetPointer(cTargetFreIm), GetPointer(cForecastFreIm), GetPointer(cLossFreIm)))
     class="kw">return false;
if(!FreqMSA(Gradient, Output, Gradient))
     class="kw">return false;
if(!FFT(GetPointer(cLossFreRe), GetPointer(cLossFreIm), GetPointer(cGradientFreRe), GetPointer(cGradientFreIm), true))
     class="kw">return false;
if(bTranspose)
   {
     if(!Transpose(GetPointer(cGradientFreRe), GetPointer(cTranspose), iCount, iWindow))
         class="kw">return false;
     if(!CumulativeGradient(GetPointer(cTranspose), Gradient, Gradient, fAlpha))
         class="kw">return false;
   }
else

频率域神经元的序列化与重载细节

这段 CNeuronFreDFOCL 的实现把梯度累积、输入梯度计算和模型存取拆成了三个独立方法,核心都在和 OpenCL 缓冲区以及文件句柄打交道。 calcInputGradients 先判空指针,再调 DeActivation 用上一层输出、梯度、本层梯度与激活函数反推输入梯度;任何一步返回 false 都会直接中断,避免脏梯度进网络。 Save 方法依次落盘 iWindow、iCount、iFFTin、bTranspose 四个整型和 fAlpha 一个浮点,每个 FileWrite 都校验写入字节数是否达到 INT_VALUE 或 sizeof(float),少写即返 false。 Load 更苛刻:每读一个字段都用 FileIsEnding 探尾,防止截断文件导致参数错乱;读完 bTranspose 后按 (bTranspose ? iWindow : iCount) 算 n,给四个复数缓冲各分配 iFFTin * n 长度并绑 OpenCL。 在 MT5 里跑这套类,若训练中途报 Load 失败,优先查文件是否少写了 fAlpha 之后的字节,而不是怀疑网络结构。外汇与贵金属模型训练属高风险实验,参数误读可能让预测倾向完全偏移。

MQL5 / C++
if(!CumulativeGradient(GetPointer(cGradientFreRe), Gradient, Gradient, fAlpha))
     class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronFreDFOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL)
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), Gradient, NeuronOCL.Activation());
  }
class="type">bool CNeuronFreDFOCL::Save(const class="type">int file_handle)
  {
  if(!CNeuronBaseOCL::Save(file_handle))
    class="kw">return false;
  if(FileWriteInteger(file_handle, class="type">int(iWindow)) < INT_VALUE)
    class="kw">return false;
  if(FileWriteInteger(file_handle, class="type">int(iCount)) < INT_VALUE)
    class="kw">return false;
  if(FileWriteInteger(file_handle, class="type">int(iFFTin)) < INT_VALUE)
    class="kw">return false;
  if(FileWriteInteger(file_handle, class="type">int(bTranspose)) < INT_VALUE)
    class="kw">return false;
  if(FileWriteFloat(file_handle, fAlpha) < class="kw">sizeof(class="type">class="kw">float))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronFreDFOCL::Load(const class="type">int file_handle)
  {
  if(!CNeuronBaseOCL::Load(file_handle))
    class="kw">return false;
  if(FileIsEnding(file_handle))
    class="kw">return false;
  iWindow = class="type">uint(FileReadInteger(file_handle));
  if(FileIsEnding(file_handle))
    class="kw">return false;
  iCount = class="type">uint(FileReadInteger(file_handle));
  if(FileIsEnding(file_handle))
    class="kw">return false;
  iFFTin = class="type">uint(FileReadInteger(file_handle));
  if(FileIsEnding(file_handle))
    class="kw">return false;
  bTranspose = class="type">bool(FileReadInteger(file_handle));
  if(FileIsEnding(file_handle))
    class="kw">return false;
  fAlpha = FileReadFloat(file_handle);
  class="type">uint n = (bTranspose ? iWindow : iCount);
  if(!cForecastFreRe.BufferInit(iFFTin * n, class="num">0) || !cForecastFreRe.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cForecastFreIm.BufferInit(iFFTin * n, class="num">0) || !cForecastFreIm.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cTargetFreRe.BufferInit(iFFTin * n, class="num">0) || !cTargetFreRe.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cTargetFreIm.BufferInit(iFFTin * n, class="num">0) || !cTargetFreIm.BufferCreate(OpenCL))
    class="kw">return false;

◍ 频域缓冲与网络层的初始化落点

在 MT5 的 OpenCL 计算封装里,先给四个频域复数缓冲(实部/虚部各两组)按 iFFTin * n 的长度做 BufferInit 并绑定 OpenCL,任何一组失败就直接 return false,这套前置不通后面全白做。 若 bTranspose 为真,转置缓冲按 iWindow * iCount 初始化并上 OpenCL;否则主动 BufferFree + Clear 释放,避免旧显存残留拖慢下一轮训练。 网络结构描述里,第 17 层统一用 defNeuronFreDFOCL 类型、activation 设 None、optimization 走 ADAM,probability 固定 0.8f。编码器窗口取 BarDescr、count 为 NForecast、step 为 int(true);Actor 窗口 NActions、count 1、step int(false);Critic 窗口 NRewards、count 1、step int(false)——三组只在 window 与 step 上有差异。 这类 0.8 的 dropout 概率加上 None 激活,倾向用于频域直接输出不归一化的原始信号,外汇与贵金属行情高频噪声大,实盘前务必在 MT5 策略测试器用小样本验证显存占用与回测稳定性,高风险。

MQL5 / C++
  if(!cLossFreRe.BufferInit(iFFTin * n, class="num">0) || !cLossFreRe.BufferCreate(OpenCL))
      class="kw">return false;
  if(!cLossFreIm.BufferInit(iFFTin * n, class="num">0) || !cLossFreIm.BufferCreate(OpenCL))
      class="kw">return false;
  if(!cGradientFreRe.BufferInit(iFFTin * n, class="num">0) || !cGradientFreRe.BufferCreate(OpenCL))
      class="kw">return false;
  if(!cGradientFreIm.BufferInit(iFFTin * n, class="num">0) || !cGradientFreIm.BufferCreate(OpenCL))
      class="kw">return false;
  if(bTranspose)
    {
      if(!cTranspose.BufferInit(iWindow * iCount, class="num">0) || !cTranspose.BufferCreate(OpenCL))
        class="kw">return false;
    }
  else
    {
      cTranspose.BufferFree();
      cTranspose.Clear();
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateEncoderDescriptions(CArrayObj *encoder)
  {
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//--- layer class="num">17
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronFreDFOCL;
  descr.window = BarDescr;
  descr.count =  NForecast;
  descr.step = class="type">int(true);
  descr.probability = class="num">0.8f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!actor)
    {
      actor = new CArrayObj();
      if(!actor)
        class="kw">return false;
    }
  if(!critic)
    {
      critic = new CArrayObj();
      if(!critic)
        class="kw">return false;
    }
class=class="str">"cmt">//--- Actor
.........
.........
class=class="str">"cmt">//--- layer class="num">17
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronFreDFOCL;
  descr.window = NActions;
  descr.count =  class="num">1;
  descr.step = class="type">int(false);
  descr.probability = class="num">0.8f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- Critic
.........
.........
class=class="str">"cmt">//--- layer class="num">17
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronFreDFOCL;
  descr.window = NRewards;
  descr.count =  class="num">1;
  descr.step = class="type">int(false);
  descr.probability = class="num">0.8f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!critic.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---

「函数收尾的返回约定」

这段 MQL5 片段只做了一件事:在条件满足后向调用方回传布尔值 true,标志着当前校验或分支逻辑已正常走通。 在 EA 或指标里,这类 return true 常出现在参数合法性检查、订单预处理通过的末尾;若前面任一环节失败,往往提前 return false 中断。 实盘中外挂这类开关函数,能帮助你在 MT5 策略测试器里快速定位“为什么这单没发出来”——把返回点打在日志上即可。外汇与贵金属杠杆高,任何逻辑分支漏判都可能放大滑点风险,返回值的语义必须自己逐行核对。

MQL5 / C++
  class="kw">return true;
}
把频域诊断交给小布
小布盯盘的 AIGC 已内置多步预测的自相关诊断,打开对应品种页即可看到标签序列在频域的能量分布,你只需判断模型是否踩中错位区。

常见问题

傅里叶基级彼此正交独立,转至频域后标签自相关被有效削弱,能缓解 DF 范式假设与数据特征的矛盾,倾向提升训练似然合理性。
目前小布内置的是自相关错位诊断与频域能量视图,模型训练仍需自行在 MT5 内实现,但重复盯盘劳动可交给小布。
直接预测省去误差累积,但忽略步间相关;递归预测保留依赖却放大波动。FreDF 类方法可能兼顾两者优势,实盘概率占优需回测。
跳空属非平稳冲击,正交基表示倾向降低平滑段自相关干扰,但极端缺口仍可能偏离,外汇贵金属高风险需分段验证。