神经网络变得简单(第 93 部分):频域和时域中的自适应预测(终篇)·进阶篇
📘

神经网络变得简单(第 93 部分):频域和时域中的自适应预测(终篇)·进阶篇

第 2/3 篇

频率域张量的初始化与归一化内核

在 MT5 用 OpenCL 跑频域模型,第一步是把所有复数缓冲区和归一化组件挂好。上面这段初始化链里,cNormFreqComplex 与 cUnNormFreqComplex 都按 iFFT * iVariables * 2 的尺寸开缓冲,说明实部和虚部是分开排布的,调参时若改了 iFFT 或变量数,这两处尺寸必须同步,否则内核会静默返回 false。 cFreqAtteention 的 Init 参数里写死了 32 和 heads,意味着频率注意力头数固定为 32,layers 与 iVariables 由外部传入;想压显存就先从这里砍 heads,但注意力分辨率会随之下降,外汇小时线重构误差可能偏大。 ComplexNormalize 内核先判 dimension<=0 直接 return,再用 get_global_id(0) 算偏移 shift=n*dimension,把均值累加到 float2 mean。注意它没在 kernel 内做除法归一,真正减去均值除方差的动作应在后续调用完成,开盘前用 EURUSD 的 H1 数据跑一遍能验证缓冲是否越界。 贵金属与外汇品种用这套做预测属高风险,缓冲尺寸算错只会导致初始化失败而非爆仓,但模型输出本身仅代表价格重构的概率倾向。

MQL5 / C++
  if(!cNormFreqComplex.Init(class="num">0, class="num">1, OpenCL, iFFT * iVariables * class="num">2, optimization, batch))
    class="kw">return false;
  if(!cMeans.BufferInit(iVariables, class="num">0) || !cMeans.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cVariances.BufferInit(iVariables, class="num">0) || !cVariances.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cFreqAtteention.Init(class="num">0, class="num">2, OpenCL, iFFT, class="num">32, heads, iVariables, layers, optimization, batch))
    class="kw">return false;
  if(!cUnNormFreqComplex.Init(class="num">0, class="num">1, OpenCL, iFFT * iVariables * class="num">2, optimization, batch))
    class="kw">return false;
  if(!cOutputFreqRe.BufferInit(iFFT*iVariables, class="num">0) || !cOutputFreqRe.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cOutputFreqIm.BufferInit(iFFT*iVariables, class="num">0) || !cOutputFreqIm.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cOutputTimeSeriasRe.BufferInit(iFFT*iVariables, class="num">0) || !cOutputTimeSeriasRe.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cOutputTimeSeriasIm.BufferInit(iFFT*iVariables, class="num">0) || !cOutputTimeSeriasIm.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cOutputTimeSeriasReGrad.BufferInit(iFFT*iVariables, class="num">0) || !cOutputTimeSeriasReGrad.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cReconstructInput.BufferInit(iHistory*iVariables, class="num">0) || !cReconstructInput.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cForecast.BufferInit(iForecast*iVariables, class="num">0) || !cForecast.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cReconstructInputGrad.BufferInit(iHistory*iVariables, class="num">0) || !cReconstructInputGrad.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cForecastGrad.BufferInit(iForecast*iVariables, class="num">0) || !cForecastGrad.BufferCreate(OpenCL))
    class="kw">return false;
  if(!cZero.BufferInit(iFFT*iVariables, class="num">0) || !cZero.BufferCreate(OpenCL))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void ComplexNormalize(__global float2 *inputs,
                                __global float2 *outputs,
                                __global float2 *means,
                                __global class="type">float *vars,
                                class="type">int dimension)
  {
  if(dimension <= class="num">0)
    class="kw">return;
  class="type">size_t n = get_global_id(class="num">0);
  const class="type">int shift = n * dimension;
  const float2 dim = (float2)(dimension, class="num">0);
  float2 mean = class="num">0;

「频域归一化与能量权重的GPU实现细节」

在 MT5 的 OpenCL 内核里处理复数序列,第一步永远是防脏数据。上面 ComplexNormalize 内核先遍历 dimension 长度,遇到 NaN 或 Inf 直接把该点置为 (float2)0,否则累加进 mean;这一步保证了后续方差计算不会因为单点异常而整体崩掉。 均值用 ComplexDiv(mean, dim) 求完,方差走的是先算各点与均值差的绝对值平方之和,再除以 dimension 开根号;若方差本身算出来是 NaN/Inf 则回退到 1.0f。外汇与贵金属行情里跳空造成的极端值可能触发这种回退,属高风险场景下的防御写法。 反归一化 ComplexUnNormalize 做逆运算:val = ComplexMul(input, variance) + mean,同样对每个输出做 NaN/Inf 拦截。MainFreqWeight 则扫一遍频域能量,取最大能量除以总能量写入 weight[n],这个比值越接近 1,说明该样本能量越集中在单一频率——复制进你的 .cl 文件用 clGetKernel 跑一下就能看到 weight 分布。

MQL5 / C++
for(class="type">int i = class="num">0; i < dimension; i++)
  {
    float2 val = inputs[shift + i];
    if(isnan(val.x) || isinf(val.x) ||
       isnan(val.y) || isinf(val.y))
       inputs[shift + i] = (float2)class="num">0;
    else
       mean += val;
  }
 means[n] = mean = ComplexDiv(mean, dim);
 class="type">float variance = class="num">0;
 for(class="type">int i = class="num">0; i < dimension; i++)
    variance += pow(ComplexAbs(inputs[shift + i] - mean), class="num">2);
 vars[n] = variance = sqrt((isnan(variance) || isinf(variance) ? class="num">1.0f : variance / dimension));
 float2 v=(float2)(variance, class="num">0);
 for(class="type">int i = class="num">0; i < dimension; i++)
  {
    float2 val = ComplexDiv((inputs[shift + i] - mean), v);
    if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y))
       val = (float2)class="num">0;
    outputs[shift + i] = val;
  }
 }
__kernel class="type">void ComplexUnNormalize(__global float2 *inputs,
                                __global float2 *outputs,
                                __global float2 *means,
                                __global class="type">float *vars,
                                class="type">int dimension)
  {
  if(dimension <= class="num">0)
    class="kw">return;
  class="type">size_t n = get_global_id(class="num">0);
  const class="type">int shift = n * dimension;
  class="type">float v= vars[n];
  float2 variance=(float2)((v > class="num">0 ? v : class="num">1.0f), class="num">0)
  float2 mean = means[n];
  for(class="type">int i = class="num">0; i < dimension; i++)
    {
      float2 val = ComplexMul(inputs[shift + i], variance) + mean;
      if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y))
         val = (float2)class="num">0;
      outputs[shift + i] = val;
    }
  }
__kernel class="type">void MainFreqWeight(__global float2 *freq,
                            __global class="type">float *weight,
                            class="type">int dimension
                            )
  {
  if(dimension <= class="num">0)
    class="kw">return;
class=class="str">"cmt">//---
  class="type">size_t n = get_global_id(class="num">0);
  const class="type">int shift = n * dimension;
  class="type">float max_f = class="num">0;
  class="type">float total = class="num">0;
  class="type">float energy;
  for(class="type">int i = class="num">0; i < dimension; i++)
    {
      energy = ComplexAbs(freq[shift + i]);
      total += energy;
      max_f = fmax(max_f, energy);
    }
  weight[n] = max_f / (total > class="num">0 ? total : class="num">1);
  }

◍ GPU 加权融合与 ATF 双路前向的实现骨架

在 MT5 的 OpenCL 环境里做特征融合,最省事的写法是用一个 __kernel 把两组输入按权重线性叠加。WeightedSum 这个函数把 inputs1 和 inputs2 在维度 dimension 上逐元素算 w*in1 + (1-w)*in2,w 来自每个样本的 weight 数组,全局 ID 决定当前处理第几个样本。 代码里 dimension<=0 直接 return,是为了挡掉空张量导致越界;shift = n*dimension 则是把一维扁平内存还原成二维样本布局。你在 MT5 策略测试器里若看到 outputs 全零,先查 weight 有没有被正确写进 GPU 缓冲区,而不是怀疑算子本身。 CNeuronATFNetOCL::feedForward 展示了双分支结构:T-Block 走归一化、位置编码、转置、分块,再循环跑 caAttention 和 caProjection;F-Block 则先做 FFT 把时域转频域,拼接实部虚部后做复数归一化与频域注意力。两条路最终都靠 RevIN 类操作回归一,避免分布漂移。 外汇与贵金属行情高频跳变,这类网络在 5 分钟 AUDCAD 上回测时若输入未做 RevIN,验证集 MSE 可能偏高 15%~30%。上 GPU 跑前务必用 get_global_id(0) 打印样本数,确认 MT5 的 OpenCL 上下文真的分到了并行维度。

MQL5 / C++
__kernel class="type">void WeightedSum(__global class="type">float *inputs1,
                              __global class="type">float *inputs2,
                              __global class="type">float *outputs,
                              __global class="type">float *weight,
                              class="type">int dimension
                              )
  {
  if(dimension <= class="num">0)
    class="kw">return;
class=class="str">"cmt">//---
  class="type">size_t n = get_global_id(class="num">0);
  const class="type">int shift = n * dimension;
  class="type">float w = weight[n];
  for(class="type">int i = class="num">0; i < dimension; i++)
    outputs[shift + i] = inputs1[shift + i] * w + inputs2[shift + i] * (class="num">1 - w);
  }
class="type">bool CNeuronATFNetOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
  if(!NeuronOCL || !NeuronOCL.getOutput())
    class="kw">return false;
  if(cInputs != NeuronOCL.getOutput())
    cInputs = NeuronOCL.getOutput();
class=class="str">"cmt">//--- T-Block
  if(!cNorm.FeedForward(NeuronOCL))
    class="kw">return false;;
  if(!cPositionEncoder.FeedForward(cNorm.AsObject()))
    class="kw">return false;
  if(!cTranspose.FeedForward(cPositionEncoder.AsObject()))
    class="kw">return false;
  if(!cPatching.FeedForward(cTranspose.AsObject()))
    class="kw">return false;
  class="type">int total = caAttention.Total();
  CNeuronBaseOCL *prev = cPatching.AsObject();
  for(class="type">int i = class="num">0; i < total; i++)
    {
     CNeuronBaseOCL *att = caAttention.At(i);
     if(!att.FeedForward(prev))
       class="kw">return false;
     prev = att;
    }
  total = caProjection.Total();
  for(class="type">int i = class="num">0; i < total; i++)
    {
     CNeuronBaseOCL *proj = caProjection.At(i);
     if(!proj.FeedForward(prev))
       class="kw">return false;
     prev = proj;
    }
  if(!cRevIN.FeedForward(prev))
    class="kw">return false;
class=class="str">"cmt">//--- F-Block
  if(!FFT(cInputs, cInputs, GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), false))
    class="kw">return false;
  if(!Concat(GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), cInputFreqComplex.getOutput(), class="num">1, class="num">1,
iFFT * iVariables))
    class="kw">return false;
  if(!ComplexNormalize())
    class="kw">return false;
  if(!MainFreqWeights())
    class="kw">return false;
  if(!cFreqAtteention.FeedForward(cNormFreqComplex.AsObject()))
    class="kw">return false;
  if(!ComplexUnNormalize())
    class="kw">return false;

频域重建与梯度归一化的核函数落点

这段 OpenCL 核逻辑承接前面的频谱拆解,把复数梯度的归一化和反归一化直接丢到 GPU 上跑,避免在 CPU 侧做大量逐点除法拖慢回测。 归一化核 ComplexNormalizeGradient 里,每个线程先取 vars[n] 当作方差,若 ≤0 则回退到 1.0f 防止除零;随后对 dimension 长度内的复数逐点做 ComplexDiv,遇到 NaN 或 Inf 直接清零。反归一化核只是把除法换成 ComplexMul,其余边界处理完全一致。 主流程中 DeConcatFFT 的连续调用表明:先逆拼接频域复数,再做正向 FFT 回时域,最后用 DeConcat 切出历史与预测段,任一环节返回 false 就中断。外汇与贵金属行情受杠杆影响波动剧烈,这类 GPU 预测管线仅作概率参考,实盘前务必在 MT5 策略测试器用真实点差复核。 想验证的话,把下面核函数贴进 .cl 文件,用 iVariables=1、小样本 dimension=64 跑一遍,观察 inputs_gr 在方差为 1 时是否原样透传。

MQL5 / C++
  if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), cUnNormFreqComplex.getOutput(), class="num">1, class="num">1,
iFFT * iVariables))
      class="kw">return false;
  if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm),
GetPointer(cOutputTimeSeriasRe), GetPointer(cOutputTimeSeriasIm), true))
      class="kw">return false;
  if(!DeConcat(GetPointer(cReconstructInput), GetPointer(cForecast), GetPointer(cOutputTimeSeriasReGrad),
GetPointer(cOutputTimeSeriasRe), iHistory, iForecast, iFFT - iHistory - iForecast, iVariables))
      class="kw">return false;
class=class="str">"cmt">//--- Output
  if(!WeightedSum())
      class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void ComplexNormalizeGradient(__global float2 *inputs_gr,
                                         __global float2 *outputs_gr,
                                         __global class="type">float *vars,
                                         class="type">int dimension)
  {
  if(dimension <= class="num">0)
    class="kw">return;
class=class="str">"cmt">//---
  class="type">size_t n = get_global_id(class="num">0);
  const class="type">int shift = n * dimension;
class=class="str">"cmt">//---
  class="type">float v = vars[n];
  float2 variance = (float2)((v > class="num">0 ? v : class="num">1.0f), class="num">0);
  for(class="type">int i = class="num">0; i < dimension; i++)
    {
    float2 val = ComplexDiv(outputs_gr[shift + i], variance);
    if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y))
      val = (float2)class="num">0;
    inputs_gr[shift + i] = val;
    }
  }
__kernel class="type">void ComplexUnNormalizeGradient(__global float2 *inputs_gr,
              __global float2 *outputs_gr,
              __global class="type">float *vars,
              class="type">int dimension)
  {
  if(dimension <= class="num">0)
    class="kw">return;
class=class="str">"cmt">//---
  class="type">size_t n = get_global_id(class="num">0);
  const class="type">int shift = n * dimension;
class=class="str">"cmt">//---
  class="type">float v = vars[n];
  float2 variance = (float2)((v > class="num">0 ? v : class="num">1.0f), class="num">0);
  for(class="type">int i = class="num">0; i < dimension; i++)
    {
    float2 val = ComplexMul(outputs_gr[shift + i], variance);
    if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y))
      val = (float2)class="num">0;
    inputs_gr[shift + i] = val;
    }
  }
__kernel class="type">void WeightedSumGradient(__global class="type">float *inputs_gr1,
                                        __global class="type">float *inputs_gr2,
                                        __global class="type">float *outputs_gr,

「重建梯度在 OpenCL 里的落地」

上面这段内核负责把输出梯度按权重拆回两条输入分支:w 和 1-w 分别乘到 inputs_gr1 / inputs_gr2。dimension<=0 直接 return,避免空维度把 GPU 线程带崩。 核函数里 get_global_id(0) 拿到线程编号 n,shift = n * dimension 是样本在扁平缓冲里的偏移;循环里 grad*weight 的写法意味着权重层是门控而非简单线性混合,重构误差会按样本级权重分流。 calcReconstructGradient 给 OpenCL 内核绑三组缓冲:输入矩阵、重建输入、重建输入梯度,再把激活类型设成 None、误差系数设 1,最后用 iHistory * iVariables 作为全局线程数扔进 Execute。任何一步 SetArgument 失败都会 printf 出错函数名和行号并返回 false,方便在 MT5 Experts 日志里定位是哪根缓冲没绑上。 calcInputGradients 入口先判 NeuronOCL 非空且梯度指针有效,否则后续卷积或反传会读到野指针。外汇与贵金属行情高频跳变,这类 GPU 反传若权重矩阵维度算错,可能让模型在实盘推理时输出失真,属高风险操作,建议先在策略测试器用历史数据跑通再上真仓。

MQL5 / C++
__global class="type">float *weight,
              class="type">int dimension
             )
  {
   if(dimension <= class="num">0)
      class="kw">return;
class=class="str">"cmt">//---
   class="type">size_t n = get_global_id(class="num">0);
   const class="type">int shift = n * dimension;
   class="type">float w = weight[n];
   class="type">float w1 = class="num">1 - weight[n];
   for(class="type">int i = class="num">0; i < dimension; i++)
     {
       class="type">float grad = outputs_gr[shift + i];
       inputs_gr1[shift + i] = grad * w;
       inputs_gr2[shift + i] = grad * w1;
     }
  }
class="type">bool CNeuronATFNetOCL::calcReconstructGradient(class="type">void)
  {
   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] = iHistory * iVariables;
   if(!OpenCL.SetArgumentBuffer(def_k_CalcOutputGradient, def_k_cog_matrix_t, cInputs.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_CalcOutputGradient, def_k_cog_matrix_o, cReconstructInput.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_CalcOutputGradient, def_k_cog_matrix_ig, cReconstructInputGrad.GetIndex()))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   if(!OpenCL.SetArgument(def_k_CalcOutputGradient, def_k_cog_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_CalcOutputGradient, def_k_cog_error, class="num">1))
     {
       printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
       class="kw">return false;
     }
   ResetLastError();
   if(!OpenCL.Execute(def_k_CalcOutputGradient, class="num">1, global_work_offset, global_work_size))
     {
       printf("Error of execution kernel CalcOutputGradient: %d", GetLastError());
       class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronATFNetOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !NeuronOCL.getGradient() || !cInputs)

◍ 反向传播里逐层梯度回传的收口逻辑

这段是 Transformer 类时序模型在 MT5 里做反向传播的最后一段:从输出侧倒着把梯度一层层送回输入侧,任何一步失败就直接 return false 中断。 T-Block 部分先处理反转输入层的激活反算,再沿 caProjection 与 caAttention 两个数组从尾到头循环调用 calcHiddenGradients,next 指针每次向后移动一层;随后依次过 Patching、Transpose、PositionEncoder、Norm 直到基础神经元。 F-Block 侧先用 SumAndNormilize 对输出时序图像做归一(偏置常数 -0.5),再算重构梯度、拼接历史与预测段(长度分别为 iHistory、iForecast、iFFT-iHistory-iForecast),走 FFT 转频域后反归一复数值。 在 MT5 导航器里把这段接进你自己的 CNeuron 派生类,重点核对 iFFT*iVariables 这个维度乘积是否和前向一致,否则梯度形状不对会静默 return false。外汇与贵金属样本上跑这套反向传播属于高风险验证,结果仅代表回测倾向。

MQL5 / C++
  class="kw">return false;
class=class="str">"cmt">//--- Output
   if(!WeightedSumGradient())
      class="kw">return false;
class=class="str">"cmt">//--- T-Block
   if(cRevIN.Activation() != None &&
!DeActivation(cRevIN.getOutput(), cRevIN.getGradient(), cRevIN.getGradient(), cRevIN.Activation()))
      class="kw">return false;
   CNeuronBaseOCL *next = cRevIN.AsObject();
   for(class="type">int i = caProjection.Total() - class="num">1; i >= class="num">0; i--)
     {
       CNeuronBaseOCL *proj = caProjection.At(i);
       if(!proj || !proj.calcHiddenGradients((CObject *)next))
          class="kw">return false;
       next = proj;
     }
   for(class="type">int i = caAttention.Total() - class="num">1; i >= class="num">0; i--)
     {
       CNeuronBaseOCL *att = caAttention.At(i);
       if(!att || !att.calcHiddenGradients((CObject *)next))
          class="kw">return false;
       next = att;
     }
   if(!cPatching.calcHiddenGradients((CObject*)next))
      class="kw">return false;
   if(!cTranspose.calcHiddenGradients(cPatching.AsObject()))
      class="kw">return false;
   if(!cPositionEncoder.calcHiddenGradients(cTranspose.AsObject()))
      class="kw">return false;
   if(!cNorm.calcHiddenGradients(cPositionEncoder.AsObject()))
      class="kw">return false;
   if(!NeuronOCL.calcHiddenGradients(cNorm.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//--- F-Block
   if(!CNeuronBaseOCL::SumAndNormilize(GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm),
                                       GetPointer(cOutputTimeSeriasIm), iFFT*iVariables, false, class="num">0, class="num">0, class="num">0, -class="num">0.5))
      class="kw">return false;
   if(!calcReconstructGradient())
      class="kw">return false;
   if(!Concat(GetPointer(cReconstructInputGrad), GetPointer(cForecastGrad), GetPointer(cZero),
              GetPointer(cOutputTimeSeriasReGrad), iHistory, iForecast, iFFT - iHistory - iForecast, iVariables))
      class="kw">return false;
   if(!FFT(GetPointer(cOutputTimeSeriasReGrad), GetPointer(cOutputTimeSeriasIm),
           GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), false))
      class="kw">return false;
   if(!Concat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), cUnNormFreqComplex.getGradient(),
   class="num">1, class="num">1, iFFT * iVariables))
      class="kw">return false;
   if(!ComplexUnNormalizeGradient())
      class="kw">return false;
   if(!cNormFreqComplex.calcHiddenGradients(cFreqAtteention.AsObject()))
      class="kw">return false;
   if(!ComplexNormalizeGradient())
      class="kw">return false;
   if(!DeConcat(GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), cInputFreqComplex.getGradient(),
   class="num">1, class="num">1, iFFT * iVariables))

常见问题

按频率 bin 的能量分布做归一化内核初始化,把高频低能量分量缩放到与低频同量级,再喂进网络,能显著降低早期梯度爆炸概率。
能量权重和归一化系数放同一常量缓存,按 warp 对齐读写;双路前向的频域重建与梯度归一化拆成两个 kernel 避免寄存器溢出。
可以。小布能读取你的预测模型结构描述,对照频域重建核与逐层梯度收口逻辑给出差异提示,并标出可能漏掉梯度归一化的层。
频域重建会放大某些频带误差,直接回传会让低频主导更新;单独归一化可按频率能量重新分配梯度权重,收敛更稳。
原文用验证集能量比做静态融合,实盘可先锁时域权重 0.6 起调,观察频域预测残差再小幅增减,避免双向互相拖累。