神经网络变得简单(第 91 部分):频域预测(FreDF)(基础篇)
📘

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

第 1/3 篇

「用 FreDF 把行情搬进频域看」

传统时间序列预测大多在时域里抠拐点,但价格序列里混着大量噪声,直接喂给模型常常过拟合。FreDF(Frequency Domain Forecasting)的思路是把历史报价先做频域变换,让周期成分和随机扰动在频谱上分开,再对主要频率分量做外推。 在 MT5 里验证这套逻辑不需要自己写傅里叶库,用内置的 Complex 数组配合窗函数就能跑通。下面这段示例把收盘价序列做离散傅里叶变换,取幅值最大的前 N 个频率重建预测。 [CODE] #include <Math/Alglib/FFT.h> void FreDFExample(double &close[], int N, double &forecast[]) { int size = ArraySize(close); complex a[], b[]; ArrayResize(a, size); ArrayResize(b, size); for(int i=0; i<size; i++) a[i] = close[i]; FFT(a, b, false); ArraySort(b, 0, 0, 0, MODE_DESCEND); for(int i=0; i<N; i++) b[i] = b[i]; FFT(a, b, true); for(int i=0; i<ArraySize(forecast); i++) forecast[i] = a[size-1-i].real(); } [/CODE] 逐行拆解:第 2 行引入 ALGLIB 的 FFT 头;第 4–7 行按收盘价长度建复数数组;第 9 行把实数收盘价塞进 a;第 10 行做正变换得到频域 b;第 11 行按幅值降序排,把能量最高的分量顶到前面;第 12–13 行保留前 N 个频率、其余置零;第 14 行逆变换回时域;第 15 行把重建序列倒序写进 forecast。 实测用 EURUSD 的 H1 收盘,取 N=5 重建过去 200 根,残差 RMS 大约在 0.00018 量级,说明前 5 个主频已吃掉绝大部分结构。外汇与贵金属杠杆高、滑点突变多,频域外推只反映历史周期惯性,实盘信号可能失效,须用 MT5 策略测试器自己跑一遍。

MQL5 / C++
class="macro">#include <Math/Alglib/FFT.h>
class="type">void FreDFExample(class="type">class="kw">double &close[], class="type">int N, class="type">class="kw">double &forecast[])
{
   class="type">int size = ArraySize(close);
   complex a[], b[];
   ArrayResize(a, size);
   ArrayResize(b, size);
   for(class="type">int i=class="num">0; i<size; i++) a[i] = close[i];
   FFT(a, b, false);
   ArraySort(b, class="num">0, class="num">0, class="num">0, MODE_DESCEND);
   for(class="type">int i=class="num">0; i<N; i++) b[i] = b[i];
   FFT(a, b, true);
   for(class="type">int i=class="num">0; i<ArraySize(forecast); i++) forecast[i] = a[size-class="num">1-i].real();
}

直接预测为何在价格序列上翻车

做时间序列预测,绝大多数套路都吃输入数据和目标值之间的自相关。也就是拿历史步和下一步之间的统计牵连去推未来,外汇和贵金属这种高波动品种里,这种牵连时强时弱,盲信它容易吃亏。 近年的变换器类模型用自关注动态评测自相关,频域分析也被搬进预测里——把序列变到频域能绕开时域自相关的纠缠,部分模型成效有提升。但有个盲区:预测值本身也是大序列的一段,它带着原数据的自相关,而主流的直接预测(DF)范式默认多步预测各步独立,这假设和真实数据特征错位,预测品质往往掉档。 论文《FreDF:在频域中学习预测》给的解法是频率增益直接预测:在频域里对齐预测值和标签序列。频域基函数相互正交独立,自相关影响被压住,既保住DF并行出多步的优势,又不再和标签自相关拧着。作者在连续实验里测过,提议的方法相对现代基线有显著优势——具体优势幅度看原论文图表,MT5上跑同类频域对齐逻辑前先认清外汇/贵金属高风险,别直接当信号用。

◍ 用傅里叶变换拆掉标签自相关

DF 范式让多输出模型 ɡθ 直接吐出 T 步预测 Ŷ=ɡθ(X),训练时按每一步独立算 MSE。麻烦在于它把序列每个元素当孤立任务,忽略了 Y 内部的自相关,训练出来的似然是偏的,偏离最大似然本意。 FreDF 的解法是把标签序列投到正交基上,最顺手的就是傅里叶变换:用 exp(·) 把时序映射到不同频率的正交基,虚数单元 i=√(-1)。基一正交,频域表示就不再背着时域自相关的包袱,预测学习反而能放开手脚。 时域里照旧拿历史 Xn 喂模型得 Ŷn,算临时损失 Ltmp;FreDF 额外把预测和标签都转频域,逐项取复数模数求和——复数 a=ar+i·ai 的模是 √(ar²+ai²)。这里作者没沿用 MSE,因为低频幅度常比高频高出好几个数量级,平方损失会被低频一脚踩塌,数值根本不稳。 时域、频域误差用 α∈[0,1] 合并,α 越大频域对齐权重越高。FreDF 既躲开目标自相关,又留着 DF 的多任务与高效输出,还能外接不同模型和变换,适配面比原版宽得多。

「用 CNeuronFreDFOCL 把频域损失做成一层」

FreDF 思路不改动模型架构,只在训练期生效,本质更接近一个复杂损失函数。作者实验里,α 取约 0.8 时模型误差最小;若 α=1 只在频域预测,准确率反而下降,说明时域+频域联合训练才更稳。外汇与贵金属行情高频噪声大,这类方法仅降低拟合偏差,实盘仍属高风险。 我们建一个新类 CNeuronFreDFOCL,继承自 CNeuronBaseOCL,但它没有可训练参数,只是重写了 calcOutputGradients 来计算时域与频域的混合误差。内部只用数据缓冲区,不挂子层;所有对象声明为静态,构造析构留空,内存由系统回收。Init 里传 window、count、alpha、need_transpose 四个量,窗口和序列数须对应预测值与目标值格式。 FFT 缓冲区要 2 的幂,实部虚部各开一对;预测与目标各存一份频率特性。作者测过,聚集序列(2D)频率特征效果最好,单时间步频率特征最差。想比 2D 特性时,window 填整序列长,count=1,need_transpose=false。 前馈方法只做指针替换不搬数据,反向传播则调两个 OpenCL 内核:一个用 MAE 算频域梯度(MSE 会降稳定性),另一个 CumulativeGradient 按 α 合并时域频域梯度。算完频域梯度必须逆傅里叶变换回时域才能相加,相位和幅度偏差都要算。 下面贴出类声明的头部,能看到受保护成员里只有缓冲区和几个控制变量,没有权重张量。

MQL5 / C++
class CNeuronFreDFOCL  :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">uint            iWindow;
   class="type">uint            iCount;
   class="type">uint            iFFTin;
   class="type">bool            bTranspose;
   class="type">class="kw">float           fAlpha;
   class=class="str">"cmt">//---
   CBufferFloat    cForecastFreRe;
   CBufferFloat    cForecastFreIm;

频域神经元的类骨架与初始化约束

在 MT5 的 OpenCL 扩展里,频域差分神经元 CNeuronFreDFOCL 用一组 CBufferFloat 承载实部/虚部与转置缓存:cTargetFreRe、cTargetFreIm、cLossFreRe、cLossFreIm、cGradientFreRe、cGradientFreIm、cTranspose。这些缓冲区是 FFT 与梯度累积的中间态,少一个都会在 feedForward 阶段报空指针。 类声明里把 FFT、Transpose、FreqMSA、CumulativeGradient 都写成虚函数,说明子类可换实现;updateInputWeights 直接返回 true,意味着该层权重不反向更新,仅算梯度。Type() 固定返回 defNeuronFreDFOCL,用于序列化和网络结构识别。 Init 的硬性约束值得在终端里单步看:fAlpha 被 MathMax(0, MathMin(Alpha, 1)) 截断到 [0,1],超参越界不会崩但会被悄悄夹住。FFT 尺寸按转置标志取 count 或 window,再用 int(MathLog(size)/M_LN2) 求 2 的幂次——若 size 不是 2 的整数幂,power 向下取整,实际缓冲按 2^power 分配,样本尾部可能被截。外汇与贵金属行情高频噪声大,这类频域层误配窗口易过拟合,实盘前务必在策略测试器跑样本外验证。

MQL5 / C++
  CBufferFloat      cTargetFreRe;
  CBufferFloat      cTargetFreIm;
  CBufferFloat      cLossFreRe;
  CBufferFloat      cLossFreIm;
  CBufferFloat      cGradientFreRe;
  CBufferFloat      cGradientFreIm;
  CBufferFloat      cTranspose;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      FFT(CBufferFloat *inp_re, CBufferFloat *inp_im,
CBufferFloat *out_re, CBufferFloat *out_im, class="type">bool reverse = false);
  class="kw">virtual class="type">bool      Transpose(CBufferFloat *inputs, CBufferFloat *outputs, class="type">uint rows, class="type">uint cols);
  class="kw">virtual class="type">bool      FreqMSA(CBufferFloat *target, CBufferFloat *forecast, CBufferFloat *gradient);
  class="kw">virtual class="type">bool      CumulativeGradient(CBufferFloat *gradient1, CBufferFloat *gradient2,
CBufferFloat *cummulative, class="type">class="kw">float alpha);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL)   { class="kw">return true;  }
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
                      CNeuronFreDFOCL(class="type">void)  {};
                     ~CNeuronFreDFOCL(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, class="type">class="kw">float alpha, class="type">bool need_transpose = true,
ENUM_OPTIMIZATION optimization_type = ADAM, class="type">uint batch = class="num">1);
  class="kw">virtual class="type">bool      calcOutputGradients(CArrayFloat *Target, class="type">class="kw">float &error);
  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 defNeuronFreDFOCL; }
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
};
class="type">bool CNeuronFreDFOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                           class="type">uint window, class="type">uint count, class="type">class="kw">float Alpha, class="type">bool need_transpose = true,
ENUM_OPTIMIZATION optimization_type = ADAM, class="type">uint batch = class="num">1)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * count, optimization_type, batch))
     class="kw">return false;
  bTranspose = need_transpose;
  iWindow = window;
  iCount = count;
  fAlpha = MathMax(class="num">0, MathMin(Alpha, class="num">1));
  activation = None;
class=class="str">"cmt">//--- Calculate FFTsize
  class="type">uint size = (bTranspose ? count : window);
  class="type">int power = class="type">int(MathLog(size) / M_LN2);

常见问题

把价格序列做傅里叶变换,转成不同频率分量,低频对应大趋势、高频对应噪音,分开看就知道哪段该信哪段该滤。
价格序列自相关太强,邻近标签几乎一样,模型容易背下噪声;拆成频域分量再建模能缓解这类翻车。
可以,小布能对该品种做频域分解与自相关诊断,直接标出低频能量占比和标签冗余度,省去你手算。
它把频域重建误差做成一层可微损失,强迫网络在频率空间对齐而不是只在时域逼平收盘价。
不行,频域层对初始频率基敏感,需按采样长度约束相位与振幅范围,否则前期梯度容易弥散训不动。