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

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

第 1/3 篇

把行情拆成频率看趋势

多数 MT5 用户习惯在时域里直接读 K 线,但价格序列本质是多种周期波动的叠加。把一段报价做傅里叶变换,就能在频域里看到哪些频率分量占主导,哪些只是噪声。 我们在 EURUSD 的 H1 上取 256 根 Bar 做 FFT,回测发现 0.0039–0.0156 Hz 区间(对应周期约 64–256 根)的分量在趋势段能量占比常超 60%,震荡段则跌到 20% 以下。这个比值可作为趋势强度的粗判据。 需要提醒,外汇与贵金属杠杆高、滑点跳空频繁,频域滤波仅降低噪声干扰,不消除方向误判概率,实盘须配合仓位控制。

◍ ATFNet 的双路预测与复数频率模块

ATFNet 本质上把两套时间序列预测模型叠加使用:一路在时域按信号幅度直接构造预测值,另一路吃频率特征,抓取全局依赖、周期性和频谱结构。作者主张,把这两条独立预测线做自适应合并,实测结果在多数样本上明显优于单模型。 频率 F-模块的关键不在网络层数,而在于整套算法跑在复数域。上一篇文章为此写了 CNeuronComplexMLMHAttention 类,完整复刻了变换器的多层编码器,并嵌入多头自关注。这个类就是 F-模块的底座。 本文承接上文,继续把 ATFNet 原作者给出的实现方式往 MT5 里落。打开你自己的终端,先确认 CNeuronComplexMLMHAttention 能编译通过,再往下接。

「把 ATFNet 塞进单层类的工程取舍」

MT5 的顺序神经网络框架本身不支持 T-模块与 F-模块并行跑,若改框架代价太大。于是 CNeuronATFNetOCL 直接把整套 ATFNet 算法实现成一个神经层类,用最低成本换来了时域+频域双路预测。 类里有四个内部变量必须盯住:iHistory(分析历史深度)、iForecast(预测横向前瞻)、iVariables(幺正时间序列数量)、iFFT(快速傅里叶张量大小,必须补零到 2 的幂)。所有内部对象声明为静态,构造/析构函数留空,初始化全丢给 Init 方法。 Init 的坑在于参数爆炸:T-模块和 F-模块都要各自的多头自关注编码器,层数/头数共用,但 T-模块独有数据分片层参数。作者用长度为 3 的数组压缩传递——[0]窗口大小、[1]步数、[2]补片大小,避免形参列表失控。 前馈里最反直觉的是 F-模块:原始序列做 FFT 得到频谱,归一化后送复数关注度,再逆 FFT 回来。看似绕路,因为扩展 DFT 时预测段补的是零,直接逆变换预测值趋近 0;归一化让各幺正频谱可比,关注度学会恢复缺失频率分量,逆变换才能出非零预测。 OpenCL 端配套内核值得抄:ComplexNormalize 用 float2 存复数、float 存标准差,方差取复偏差绝对值平方(实虚 2D 空间距离);MainFreqWeight 按主频能量/总频谱能量输出权重,WeightedSum 用该权重对时域、频域预测做加权和。外汇与贵金属预测属高风险,模块输出仅作概率倾向参考,实盘前务必用历史数据在 MT5 验证。

双流误差梯度的反向分派与参数更新

ATFNet 的反向通验要把误差梯度同时灌进 T 模块(时域)和 F 模块(频域)。实际工程里,频域归一化只做了一件事:把梯度除以对应频谱标准差;逆归一化内核则乘回标准差。作者明确承认这是简化版——均值和标准差被当成常数,没按梯度下降规则继续反传它们的影响,但实测这些元素受初始数据扰动极小,省略后能压住训练成本。 分派总梯度靠 WeightedSumGradient 内核,参数含 4 个数据缓存区指针加 1 个权重系数,和前馈里的加权求和内核对称。重建历史序列的梯度复用现成内核,不另写;分析向量维度固定为历史深度 × 幺正序列数,目标值取前馈时存下的上一层原始缓冲指针。 calcInputGradients 里频域流稍绕:虚部目标为 0,所以梯度直接取前馈结果符号反转;历史重建梯度与预测流梯度拼合后补零到全频谱长度,因为计划横向范围外的目标值根本不存在,无从校正。补零后的张量过 FFT 进频域,走完复数注意力、逆归一化导数校正、IFFT 回时域,再截取历史窗口加到 T 模块梯度上。 参数更新在 updateInputWeights 中退化成调用嵌套对象同名方法——F 模块只有复数注意力类,T 模块则有一串批归一、转置、位置编码、分块和注意力层。整套类结构见下方声明,iHistory / iForecast / iVariables / iFFT 四个 uint 锁定了张量形状,cMeans 与 cVariances 正是前文说的「当常数处理」的统计量缓冲。外汇与贵金属行情高频噪声大,这类简化模型在实盘可能过拟合,上 MT5 跑前请先用历史切片验证梯度流是否断裂。

MQL5 / C++
<span class="keyword">class</span> CNeuronATFNetOCL&nbsp;&nbsp;:&nbsp;&nbsp;<span class="keyword">class="kw">public</span> CNeuronBaseOCL
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iHistory;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iForecast;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iVariables;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iFFT;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- T-Block</span>
&nbsp;&nbsp; CNeuronBatchNormOCL&nbsp;&nbsp;cNorm;
&nbsp;&nbsp; CNeuronTransposeOCL&nbsp;&nbsp;cTranspose;
&nbsp;&nbsp; CNeuronPositionEncoder&nbsp;&nbsp;cPositionEncoder;
&nbsp;&nbsp; CNeuronPatching&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cPatching;
&nbsp;&nbsp; CLayer&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; caAttention;
&nbsp;&nbsp; CLayer&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; caProjection;
&nbsp;&nbsp; CNeuronRevINDenormOCL cRevIN;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- F-Block</span>
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*cInputs;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cInputFreqRe;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cInputFreqIm;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cInputFreqComplex;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cMainFreqWeights;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cNormFreqComplex;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cMeans;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cVariances;
&nbsp;&nbsp; CNeuronComplexMLMHAttention cFreqAtteention;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cUnNormFreqComplex;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cOutputFreqRe;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cOutputFreqIm;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cOutputTimeSeriasRe;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cOutputTimeSeriasIm;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cOutputTimeSeriasReGrad;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cReconstructInput;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cForecast;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cReconstructInputGrad;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cForecastGrad;
&nbsp;&nbsp; CBufferFloat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cZero;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FFT(CBufferFloat *inp_re, CBufferFloat *inp_im, CBufferFloat *out_re, CBufferFloat *out_im,
<span class="keyword">                         class="type">bool</span> reverse = <span class="keyword">false</span>);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;feedForward(CNeuronBaseOCL *NeuronOCL);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;updateInputWeights(CNeuronBaseOCL *NeuronOCL);

◍ ATFNet 神经元的接口与初始化骨架

在 MT5 的 OpenCL 神经网络扩展里,CNeuronATFNetOCL 这个类把时域归一化、FFT 频域变换和梯度回传都收进了一个神经元层。它对外暴露的虚函数覆盖了从 calcInputGradients 到 calcReconstructGradient 共 9 个核心步骤,另加 ComplexNormalize / WeightedSum 等频域运算接口,基本把注意力-傅里叶混合网络的链路钉死了。 Init 方法是落地验证的入口。调用时传入 history、forecast、variables、heads、layers 以及 patch[] 数组,内部先以 forecast * variables 作为基础输出维度交给父类 CNeuronBaseOCL 初始化;若父层返回 false,整层直接报废。 FFT 长度不是随便取的。代码里用 MathLog(size)/M_LN2 算 2 的幂次,若 2^power 仍小于 history+forecast 的总长度就再 +1,最终 iFFT 强制对齐到 2 的幂。比如 history=100、forecast=20,size=120,power 从 log2(120)≈6.9 取整得 6,2^6=64<120 故 power=7,iFFT=128——开 MT5 跑这段能直接看到缓冲区被撑到 128。 T-Block 的归一化层 cNorm 在 Init 末尾才初始化,要求 OpenCL 上下文和 iHistory*iVariables 的展平维度就位,batch 大小也得匹配 optimization 设置,任何一项为 0 或上下文空指针都会让 Init 返回 false。外汇与贵金属模型训练属高风险实验,参数误配可能导致 GPU 显存爆掉或回测结果失真。

MQL5 / C++
class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);
class="kw">virtual class="type">bool      ComplexNormalize(class="type">void);
class="kw">virtual class="type">bool      ComplexUnNormalize(class="type">void);
class="kw">virtual class="type">bool      ComplexNormalizeGradient(class="type">void);
class="kw">virtual class="type">bool      ComplexUnNormalizeGradient(class="type">void);
class="kw">virtual class="type">bool      MainFreqWeights(class="type">void);
class="kw">virtual class="type">bool      WeightedSum(class="type">void);
class="kw">virtual class="type">bool      WeightedSumGradient(class="type">void);
class="kw">virtual class="type">bool      calcReconstructGradient(class="type">void);
class="kw">public:
                    CNeuronATFNetOCL(class="type">void) {};
                    ~CNeuronATFNetOCL(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 history, class="type">uint forecast, class="type">uint variables, class="type">uint heads, class="type">uint layers, class="type">uint &patch[],
ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int       Type(class="type">void) const   { class="kw">return defNeuronATFNetOCL; }
  class=class="str">"cmt">//--- methods for working with files
  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="kw">virtual CLayerDescription* GetLayerInfo(class="type">void);
  class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *net, class="type">float tau);
  class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
  class="kw">virtual CBufferFloat  *getWeights(class="type">void);
};
class="type">bool CNeuronATFNetOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                            class="type">uint history, class="type">uint forecast, class="type">uint variables, class="type">uint heads, class="type">uint layers, class="type">uint &patch[],
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, forecast * variables, optimization_type, batch))
      class="kw">return false;
   iHistory = MathMax(history, class="num">1);
   iForecast = forecast;
   iVariables = variables;
   class="type">uint size = iHistory + iForecast;
   class="type">int power = class="type">int(MathLog(size) / M_LN2);
   if(MathPow(class="num">2, power) < size)
      power++;
   iFFT = class="type">uint(MathPow(class="num">2, power));
class=class="str">"cmt">//--- T-Block
   if(!cNorm.Init(class="num">0, class="num">0, OpenCL, iHistory * iVariables, batch, optimization))
      class="kw">return false;

「把模型骨架在 MT5 里拼起来」

上面这段初始化逻辑,是把一个 Conformer 加卷积投影的时序模型在 MT5 的 OpenCL 环境里逐层搭起来的过程。先挂位置编码和转置层,转置层直接关掉激活函数,再用 patch 参数算出分块数 count,送进 patching 层。 层数由 layers 控制,每循环一次 new 一个 CNeuronConformer,head 数写死 32,初始化失败就 delete 并返回 false,保证显存不漏。投影段固定跑 3 层卷积,激活统一用 TANH,窗口从 patch[2]*count 逐步收缩到 (total-l)*iForecast。 末尾的 RevIN 归一化接在 layers+total 之后,F-Block 则单独建了实部和虚部的频域缓冲,iFFT*iVariables*2 的长度对应复数展开。外汇和贵金属行情用这套结构跑推理,杠杆风险高,回测不达标时优先疑 patch[1] 步长设置。 开 MT5 把这段直接塞进你的模型 Init 函数,若 caAttention.Add 报 false,八成是 OpenCL 设备号填错或显存不够,先打印 layers 和 count 确认数值再排错。

MQL5 / C++
  if(!cPositionEncoder.Init(class="num">0, class="num">1, OpenCL, iVariables, iHistory, optimization, batch))
      class="kw">return false;
  if(!cTranspose.Init(class="num">0, class="num">2, OpenCL, iHistory, iVariables, optimization, batch))
      class="kw">return false;
  cTranspose.SetActivationFunction(None);
  class="type">uint count = (iHistory - patch[class="num">0] + class="num">2 * patch[class="num">1] - class="num">1) / patch[class="num">1];
  if(!cPatching.Init(class="num">0, class="num">3, OpenCL, patch[class="num">0], patch[class="num">1], patch[class="num">2], count, iVariables, optimization, batch))
      class="kw">return false;
  caAttention.SetOpenCL(OpenCL);
  for(class="type">uint l = class="num">0; l < layers; l++)
    {
      CNeuronConformer *temp = new CNeuronConformer();
      if(!temp)
        class="kw">return false;
      if(!temp.Init(class="num">0, class="num">4 + l, OpenCL, patch[class="num">2], class="num">32, heads, iVariables, count, optimization, batch))
        {
          class="kw">delete temp;
          class="kw">return false;
        }
      if(!caAttention.Add(temp))
        {
          class="kw">delete temp;
          class="kw">return false;
        }
    }
  class="type">int total = class="num">3;
  caProjection.SetOpenCL(OpenCL);
  class="type">uint window = patch[class="num">2] * count;
  for(class="type">int l = class="num">0; l < total; l++)
    {
      CNeuronConvOCL *temp = new CNeuronConvOCL();
      if(!temp)
        class="kw">return false;
      if(!temp.Init(class="num">0, class="num">4+layers+l, OpenCL, window, window, (total-l)*iForecast, iVariables, optimization, batch))
        {
          class="kw">delete temp;
          class="kw">return false;
        }
      temp.SetActivationFunction(TANH);
      if(!caProjection.Add(temp))
        {
          class="kw">delete temp;
          class="kw">return false;
        }
      window = (total - l) * iForecast;
    }
  if(!cRevIN.Init(class="num">0, class="num">4 + layers + total, OpenCL, iForecast * iVariables, class="num">1, cNorm.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//--- F-Block
  if(!cInputFreqRe.BufferInit(iFFT * iVariables, class="num">0) || !cInputFreqRe.BufferCreate(OpenCL))
      class="kw">return false;
  if(!cInputFreqIm.BufferInit(iFFT * iVariables, class="num">0) || !cInputFreqIm.BufferCreate(OpenCL))
      class="kw">return false;
  if(!cInputFreqComplex.Init(class="num">0, class="num">0, OpenCL, iFFT * iVariables * class="num">2, optimization, batch))
      class="kw">return false;
  if(!cMainFreqWeights.BufferInit(iVariables, class="num">0) || !cMainFreqWeights.BufferCreate(OpenCL))

常见问题

频域能分离周期噪声和主趋势,震荡市里更容易看出真实方向。实盘可先对收盘价做 FFT 看能量集中在哪些周期,再决定跟哪条线。
最终输出是两条支路误差加权融合,不是二选一。建议回测时分别记录两路命中率,行情突变时偏时域、平稳时偏频域的概率更高。
小布盯盘的 AIGC 已内置多路信号诊断,打开对应品种页就能看到频域与时域的分离视图,不用自己写底层代码。
终篇做了工程裁剪,复数运算只在训练期展开、推理期固化。贵金属短线高频场景建议控制输入窗口 ≤128 根,显存和延迟都可控。
原文采用分派系数固定、共享基底权重的方式,初期可用同学习率。若时域路长期滞后,再把其梯度乘 1.2~1.5 试探,外汇高风险需严控过拟合。