神经网络变得简单(第 89 部分):频率增强分解变换器(FEDformer)·进阶篇
📡

神经网络变得简单(第 89 部分):频率增强分解变换器(FEDformer)·进阶篇

(2/3)· 标准变换器在长周期行情预测里又慢又失真,这篇拆开频率增强分解的实战改造路径

实战向 第 2/3 篇
很多交易者直接把原版变换器套到 MT5 长周期标的上,结果发现预测分布和实盘走势完全对不上。点关注机制让每一步预测各自独立,全局季节性和突发事件高频成分全被丢光。不降复杂度也不补频率视角,长序列模型在外汇贵金属上基本跑不动。

卷积权重按勒让德阶数分段灌入

这段初始化逻辑把不同阶数的 Legendre 小波权重依次塞进卷积缓冲区:每写完一组就往前跳 iWindow+1 个偏移量,顺序为 12、16、18、20 阶。若任意一次 Update 返回 false,整个流程直接 return false,说明权重装载在中途断裂,后续前传不会执行。 当 OpenCL 对象非空时,循环结束后还会调用 WeightsConv.BufferWrite() 把权重真正推到显存;这一步失败同样会中断返回。外汇与贵金属行情的高波动下,这类特征工程层若装载异常,模型对突变段的响应可能明显滞后,属高风险环节。 下面这段类声明定义了 CNeuronFEDW 的内部结构:iWindow 与 iCount 控制窗口与分量数,cWavlets 持有勒让德小波,cNorm、cSoftMax、cFF[2] 与 cReconstruct 串起归一化到重建的通路。虚函数里 feedForward / updateInputWeights / calcInputGradients 是训练必需重载项,Reconsruct 单独负责从输入缓冲重建输出。

MQL5 / C++
if(!WeightsConv.Update(shift, Legendre12(k)))
   class="kw">return false;
shift += iWindow + class="num">1;
if(!WeightsConv.Update(shift, Legendre16(k)))
   class="kw">return false;
shift += iWindow + class="num">1;
if(!WeightsConv.Update(shift, Legendre18(k)))
   class="kw">return false;
shift += iWindow + class="num">1;
if(!WeightsConv.Update(shift, Legendre20(k)))
   class="kw">return false;
 }
 if(!!OpenCL)
  if(!WeightsConv.BufferWrite())
    class="kw">return false;
class=class="str">"cmt">//---
 class="kw">return true;
 }
class CNeuronFEDW : class="kw">public CNeuronBaseOCL
 {
class="kw">protected:
  class=class="str">"cmt">//---
  class="type">uint                iWindow;
  class="type">uint                iCount;
  class=class="str">"cmt">//---
  CNeuronLegendreWavelets cWavlets;
  CNeuronBatchNormOCL    cNorm;
  CNeuronSoftMaxOCL      cSoftMax;
  CNeuronConvOCL         cFF[class="num">2];
  CNeuronBaseOCL         cReconstruct;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      Reconsruct(CBufferFloat* inputs, CBufferFloat *outputs);
class="kw">public:
                     CNeuronFEDW(class="type">void) {};
                    ~CNeuronFEDW(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 count,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  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=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void)      const       {  class="kw">return defNeuronFEDW; }
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau);
  };
class="type">bool CNeuronFEDW::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                       class="type">uint window, class="type">uint count,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {

「小波与归一化层在 Init 里的接线细节」

这段 CNeuronFEDW::Init 展示了在 MT5 中用 OpenCL 搭建小波特征提取网络的初始化顺序:先调基类 CNeuronBaseOCL::Init 拿到输出数与索引,再依次把 cWavlets、cNorm、cSoftMax、cReconstruct 以及两层 cFF 全接上。任何一步 Init 返回 false 就直接退出,避免半残网络进训练循环。 cWavlets 用 iWindow 和 iCount 指定窗口与序列数,激活函数设为 None,说明它只做线性小波变换不参与非线性映射。cNorm 的隐藏层维度写死 9 * iCount、第二参数 1000,是给后续 softmax 前做幅度压制的经验值。 cSoftMax.SetHeads(iCount) 把多头概率拆回序列维度,cReconstruct 用 iWindow 做单步重建。两层 cFF 的神经元数呈 4*iWindow → 4*iWindow → iWindow 的瓶颈结构,仅第一层用 LReLU,其余全 None。 Reconsruct 方法里 global_work_size[0] = sequence.Total() 直接拿序列长度当 GPU 并行规模,随后把 cWavlets 权重、probability、sequence 分别绑到 def_k_CalcHiddenGradientConv 内核的参数槽。哪次 SetArgumentBuffer 失败就 printf 出函数名、GetLastError 和 __LINE__,方便在 MT5 Experts 日志里定位是第几个绑定断的。

MQL5 / C++
if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * count,
optimization_type, batch))
    class="kw">return false;
 iWindow = window;
 iCount = count;
 if(!cWavlets.Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, iCount, optimization, iBatch))
    class="kw">return false;
 cWavlets.SetActivationFunction(None);
 if(!cNorm.Init(class="num">0, class="num">1, OpenCL, class="num">9 * iCount, class="num">1000,optimization))
    class="kw">return false;
 cNorm.SetActivationFunction(None);
 if(!cSoftMax.Init(class="num">0, class="num">1, OpenCL, class="num">9 * iCount, optimization, iBatch))
    class="kw">return false;
 cSoftMax.SetHeads(iCount);
 cSoftMax.SetActivationFunction(None);
 if(!cReconstruct.Init(class="num">0, class="num">2, OpenCL, iWindow, optimization, iBatch))
    class="kw">return false;
 cReconstruct.SetActivationFunction(None);
 if(!cFF[class="num">0].Init(class="num">0, class="num">3, OpenCL, iWindow, iWindow, class="num">4 * iWindow, iCount, optimization, iBatch))
    class="kw">return false;
 cFF[class="num">0].SetActivationFunction(LReLU);
 if(!cFF[class="num">1].Init(class="num">0, class="num">4, OpenCL, class="num">4 * iWindow, class="num">4 * iWindow, iWindow, iCount, optimization, iBatch))
    class="kw">return false;
 SetActivationFunction(None);
 if(Gradient != cFF[class="num">1].getGradient())
    SetGradient(cFF[class="num">1].getGradient());
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CNeuronFEDW::Reconsruct(CBufferFloat *sequence, CBufferFloat *probability)
  {
  class="type">uint global_work_offset[class="num">1] = {class="num">0};
  class="type">uint global_work_size[class="num">1];
  global_work_size[class="num">0] = sequence.Total();
  if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_w,
cWavlets.GetWeightsConv().GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_g, probability.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_o, probability.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_ig, sequence.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
  if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_outputs, probability.Total()))
     {

◍ 卷积梯度核的参数绑定与前向链路

在 OpenCL 加速的神经网络层里,CalcHiddenGradientConv 内核的参数必须在执行前逐一对齐。下面这段把 step、window_in、window_out 等 6 个参数通过 SetArgument 写进内核,其中 window_out 被硬编码为 9,activation 取 None,任何一步返回 false 都会用 printf 抛出 __FUNCTION__ 与 __LINE__ 定位。 内核真正跑起来靠 OpenCL.Execute,全局工作项数量由 global_work_size 控制,偏移量走 global_work_offset;只要执行失败就直接退出,不再往下走。这套写法在 MT5 里调试时,若终端日志出现 'Error of execution kernel' 且错误码非 0,优先怀疑显存参数越界而非算法本身。 前向传播则是一条串行的对象链:小波层 cWavlets 先过,接归一化 cNorm,再进 cSoftMax,随后 Reconsruct 用重建输出和 softmax 输出做重构。SumAndNormilize 把结果按 iWindow 窗口聚合(末参 1 表示某种归一开关),最后两层全连接 cFF[0]、cFF[1] 依次前推,任何一环返回 false 即中断。外汇与贵金属行情下用此类 GPU 模型做特征提取,回测过拟合概率偏高,实盘前务必做样本外验证。

MQL5 / C++
   if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_step, (class="type">int)iWindow))
   {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
   }
   if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_window_in, (class="type">int)iWindow))
   {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
   }
   if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_window_out, (class="type">int)class="num">9))
   {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
   }
   if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_activation, (class="type">int)None))
   {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
   }
   if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_shift_out, (class="type">int)class="num">0))
   {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return false;
   }
   if(!OpenCL.Execute(def_k_CalcHiddenGradientConv, class="num">1, global_work_offset, global_work_size))
   {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      class="kw">return false;
   }
class=class="str">"cmt">//---
   class="kw">return true;
}
class="type">bool CNeuronFEDW::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cWavlets.FeedForward(NeuronOCL.AsObject()))
      class="kw">return false;
   if(!cNorm.FeedForward(cWavlets.AsObject()))
      class="kw">return false;
   if(!cSoftMax.FeedForward(cNorm.AsObject()))
      class="kw">return false;
   if(!Reconsruct(cReconstruct.getOutput(), cSoftMax.getOutput()))
      class="kw">return false;
   if(!SumAndNormilize(NeuronOCL.getOutput(), cReconstruct.getOutput(), cReconstruct.getOutput(),
iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return false;
   if(!cFF[class="num">0].FeedForward(cReconstruct.AsObject()))
      class="kw">return false;
   if(!cFF[class="num">1].FeedForward(cFF[class="num">0].AsObject()))
      class="kw">return false;

反向传播里的张量暂存与权重回写

FEDW 神经元类的梯度计算函数 calcInputGradients 里,先用 SumAndNormilize 把重构分支的梯度做归一,随后立刻把 cReconstruct 的输出指针暂存到 temp_r、把 cWavlets 的输出指针暂存到 temp_w。这一步不是多余动作——后面 cReconstruct 被强行塞入梯度当作前向输入跑了一遍 FeedForward,若不暂存,原本的激活值就丢了,回写时 temp_r / temp_w 能把现场还原。 注意 cWavlets.FeedForward(cReconstruct.AsObject()) 这行:它借重构梯度当输入推了一次小波层前向,但紧接着用 SetOutput(temp_w) 把小波输出覆盖回去,等于只借通道算梯度、不污染权重态。这种「借输出算梯度再还原」的写法在 OpenCL 后端里很常见,能省一次 kernel 调度。 updateInputWeights 只回写了三条路径:cFF[0] 接重构对象、cFF[1] 接 cFF[0]、cNorm 接 cWavlets。也就是说编码器最前端的输入层权重并不在本函数内更新,而是由外部 NeuronOCL 的 calcHiddenGradients 承接——如果你在 MT5 里改了网络拓扑却忘了在外层调更新,可能出现前层梯度累积却永不落盘的现象。 CreateEncoderDescriptions 里输入层 descr.count 直接等于 HistoryBars * BarDescr,优化器写死 ADAM、激活函数为 None。想验证的话,开 MT5 把 HistoryBars 调到 200、BarDescr 设 5,输入维度就是 1000 个浮点,显存占用和收敛速度会明显和默认 64*3 不同。外汇与贵金属行情高波动,这类自编码结构仅作特征压缩实验,实盘信号务必自带风控。

MQL5 / C++
if(!SumAndNormilize(cFF[class="num">1].getOutput(), cReconstruct.getOutput(), getOutput(), iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronFEDW::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
   {
   if(!NeuronOCL)
      class="kw">return false;
   if(!cFF[class="num">0].calcHiddenGradients(cFF[class="num">1].AsObject()))
      class="kw">return false;
   if(!cReconstruct.calcHiddenGradients(cFF[class="num">0].AsObject()))
      class="kw">return false;
   if(!SumAndNormilize(Gradient, cReconstruct.getGradient(), cReconstruct.getGradient(), iWindow, false))
      class="kw">return false;
   CBufferFloat *temp_r = cReconstruct.getOutput();
   if(!cReconstruct.SetOutput(cReconstruct.getGradient(), false))
      class="kw">return false;
   CBufferFloat *temp_w = cWavlets.getOutput();
   if(!cWavlets.SetOutput(cSoftMax.getGradient(), false))
      class="kw">return false;
   if(!cWavlets.FeedForward(cReconstruct.AsObject()))
      class="kw">return false;
   if(!cWavlets.SetOutput(temp_w, false))
      class="kw">return false;
   if(!cReconstruct.SetOutput(temp_r, false))
      class="kw">return false;
   if(!cNorm.calcHiddenGradients(cSoftMax.AsObject()))
      class="kw">return false;
   if(!cWavlets.calcHiddenGradients(cNorm.AsObject()))
      class="kw">return false;
   if(!NeuronOCL.calcHiddenGradients(cWavlets.AsObject()))
      class="kw">return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), cReconstruct.getGradient(), NeuronOCL.getGradient(),
iWindow, false))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronFEDW::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
   {
   if(!cFF[class="num">0].UpdateInputWeights(cReconstruct.AsObject()))
      class="kw">return false;
   if(!cFF[class="num">1].UpdateInputWeights(cFF[class="num">0].AsObject()))
      class="kw">return false;
   if(!cNorm.UpdateInputWeights(cWavlets.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CreateEncoderDescriptions(CArrayObj *encoder)
   {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!encoder)
      {
      encoder = new CArrayObj();
      if(!encoder)
         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))
      {

「编码器十六层的栈式装配细节」

这段装配逻辑把价格序列编码器的十六层结构一次性压进 encoder 对象,每层都先 new 一个 CLayerDescription,失败就 delete 并返 false,确保 MT5 端内存不漏。 第一层用 defNeuronBatchNormOCL 做批归一,batch 写死 10000、优化器 ADAM,倾向用于抹平不同品种量纲差异;第二层 defNeuronTransposeOCL 把 HistoryBars 长度重排到 BarDescr 维度。 第三到十二层是十次循环塞入的 defNeuronFEDW,window 锁 HistoryBars、count 为 BarDescr,循环里任一层 Add 失败立即回收 descr 退出,这是实盘加载时最容易卡住的十连判。 第十三层 defNeuronTiDEOCL 较特殊:windows 数组按 {HistoryBars, 2*EmbeddingSize, EmbeddingSize, 2*EmbeddingSize, NForecast} 拷入,step=4,若 ArrayCopy 返回值 ≤0 直接返 false,说明嵌入维度参数错一处整网就废。 第十四到十五层分别是 defNeuronConvOCL(步长与输出窗同为 NForecast)和 defNeuronTransposeOCL,把预测窗拉回 BarDescr 结构;第十六层 defNeuronRevInDenormOCL 做反归一,count = BarDescr*NForecast、layers=1、优化器 ADAM。 开 MT5 把这段直接贴进你的 CNet 派生类,重点核对 EmbeddingSize 与 NForecast 宏定义,二者不匹配时第十三层 ArrayCopy 必然失败。

MQL5 / C++
   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">10000;
   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 = defNeuronTransposeOCL;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">3-class="num">12
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronFEDW;
   descr.count = BarDescr;
   descr.window = HistoryBars;
   descr.activation = None;
   for(class="type">int i = class="num">0; i < class="num">10; i++)
      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 = defNeuronTiDEOCL;
   descr.count = BarDescr;
   descr.window = HistoryBars;
   descr.window_out = NForecast;
   descr.step = class="num">4;
      {
      class="type">int windows[] = {HistoryBars, class="num">2 * EmbeddingSize, EmbeddingSize, class="num">2 * EmbeddingSize, NForecast};
      if(ArrayCopy(descr.windows, windows) <= class="num">0)
         class="kw">return false;
      }
   descr.activation = None;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">14
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   descr.count = BarDescr;
   descr.window = NForecast;
   descr.step = NForecast;
   descr.window_out = NForecast;
   descr.activation = None;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">15
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = BarDescr;
   descr.window = NForecast;
   if(!encoder.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">16
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = BarDescr * NForecast;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.layers = class="num">1;
让小布替你跑这套频域诊断
这些频域分解与随机傅里叶子集的对照诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到分布偏差和复杂度估算,你只管判断信号是否值得跟。

常见问题

因为点关注对每个时间步独立建模,没有保留整体的统计属性和季节性,长期看分布就会漂移,外汇贵金属高风险品种上更明显。
时间序列里某些关键变化来自高频事件,全砍高频会丢信息;论文证明随机含高低的子集在理论和实证上都能更好表征序列。
目前小布内置的是基于同思路的频域分解诊断与复杂度提示,不暴露具体模型权重,但能帮你快速看分布拟合与算力负担。
论文里是用傅里叶/小波增强模块替代原有关注模块来捕获频率结构,实际替换后计算成本从二次降到线性,但效果依赖子集选择策略。
标准变换器内存随序列长度二次增长,FEDformer 靠固定随机分量数控到线性,实盘加载更稳,但仍需警惕滑点外的过拟合风险。