神经网络变得简单(第 91 部分):频域预测(FreDF)(基础篇)
「用 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 策略测试器自己跑一遍。
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 按 α 合并时域频域梯度。算完频域梯度必须逆傅里叶变换回时域才能相加,相位和幅度偏差都要算。 下面贴出类声明的头部,能看到受保护成员里只有缓冲区和几个控制变量,没有权重张量。
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 分配,样本尾部可能被截。外汇与贵金属行情高频噪声大,这类频域层误配窗口易过拟合,实盘前务必在策略测试器跑样本外验证。
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);