神经网络变得简单(第 93 部分):频域和时域中的自适应预测(终篇)(基础篇)
把行情拆成频率看趋势
多数 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 跑前请先用历史切片验证梯度流是否断裂。
<span class="keyword">class</span> CNeuronATFNetOCL : <span class="keyword">class="kw">public</span> CNeuronBaseOCL { <span class="keyword">class="kw">protected</span>: <span class="keyword">class="type">uint</span> iHistory; <span class="keyword">class="type">uint</span> iForecast; <span class="keyword">class="type">uint</span> iVariables; <span class="keyword">class="type">uint</span> iFFT; <span class="comment">class=class="str">"cmt">//--- T-Block</span> CNeuronBatchNormOCL cNorm; CNeuronTransposeOCL cTranspose; CNeuronPositionEncoder cPositionEncoder; CNeuronPatching cPatching; CLayer caAttention; CLayer caProjection; CNeuronRevINDenormOCL cRevIN; <span class="comment">class=class="str">"cmt">//--- F-Block</span> CBufferFloat *cInputs; CBufferFloat cInputFreqRe; CBufferFloat cInputFreqIm; CNeuronBaseOCL cInputFreqComplex; CBufferFloat cMainFreqWeights; CNeuronBaseOCL cNormFreqComplex; CBufferFloat cMeans; CBufferFloat cVariances; CNeuronComplexMLMHAttention cFreqAtteention; CNeuronBaseOCL cUnNormFreqComplex; CBufferFloat cOutputFreqRe; CBufferFloat cOutputFreqIm; CBufferFloat cOutputTimeSeriasRe; CBufferFloat cOutputTimeSeriasIm; CBufferFloat cOutputTimeSeriasReGrad; CBufferFloat cReconstructInput; CBufferFloat cForecast; CBufferFloat cReconstructInputGrad; CBufferFloat cForecastGrad; CBufferFloat cZero; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> 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>); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CNeuronBaseOCL *NeuronOCL); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> 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 显存爆掉或回测结果失真。
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 确认数值再排错。
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))