神经网络变得简单(第 91 部分):频域预测(FreDF)·进阶篇
(2/3)·多数多步预测模型默认输出步独立,却忽略标签自相关,频域对齐正是破局点
直接预测范式同步生成多步结果,却悄悄假设各步相互独立。可预测值本就是原序列的延续,自带强自相关。这种假设与数据的错位,正是很多模型样本外表现滑坡的暗坑。
◍ 频率域神经层的缓冲与梯度核初始化
在 MT5 用 OpenCL 跑频率域深度网络时,FFT 输入长度必须凑成 2 的幂。下面这段逻辑会先拿 MathPow(2, power) 和 size 比对,不等就 power++,最终 iFFTin 取到最近的 2 的幂次,比如 size=500 时会被顶到 512。 随后按 bTranspose 决定 n 是窗口数还是样本数,给 9 组复数缓冲(实/虚各一)做 BufferInit(iFFTin * n, 0) 并推到 OpenCL 显存;任何一组失败直接 return false,整套前向传播就不会启动。 误差反传的两个核心 kernel 很轻量:GradientMSA 在全局 id 处算 matrix_g[i] = matrix_t[i] - matrix_o[i],即目标减输出;CumulativeGradient 则用 alpha 做指数滑动,gradient_out[i] = alpha * gradient_freq[i] + (1-alpha) * gradient_tmp[i]。把 alpha 从 0.9 调到 0.99,梯度累积的惯性会明显变长,外汇 1 分钟级信号可能更平滑但也更滞后。 开 MT5 把这段代码塞进自定义神经层类,先打印 iFFTin 确认对齐了 2 的幂,再小步改 alpha 观察显存占用和回测曲线,贵金属与外汇杠杆高,验证时请用模拟盘。
if(MathPow(class="num">2, power) != size) power++; iFFTin = class="type">uint(MathPow(class="num">2, power)); class=class="str">"cmt">//--- class="type">uint n = (bTranspose ? iWindow : iCount); if(!cForecastFreRe.BufferInit(iFFTin * n, class="num">0) || !cForecastFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cForecastFreIm.BufferInit(iFFTin * n, class="num">0) || !cForecastFreIm.BufferCreate(OpenCL)) class="kw">return false; if(!cTargetFreRe.BufferInit(iFFTin * n, class="num">0) || !cTargetFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cTargetFreIm.BufferInit(iFFTin * n, class="num">0) || !cTargetFreIm.BufferCreate(OpenCL)) class="kw">return false; if(!cLossFreRe.BufferInit(iFFTin * n, class="num">0) || !cLossFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cLossFreIm.BufferInit(iFFTin * n, class="num">0) || !cLossFreIm.BufferCreate(OpenCL)) class="kw">return false; if(!cGradientFreRe.BufferInit(iFFTin * n, class="num">0) || !cGradientFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cGradientFreIm.BufferInit(iFFTin * n, class="num">0) || !cGradientFreIm.BufferCreate(OpenCL)) class="kw">return false; if(!cTranspose.BufferInit(iWindow * iCount, class="num">0) || !cTranspose.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFreDFOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getOutput()) class="kw">return false; if(NeuronOCL.getOutput() != Output) { Output.BufferFree(); class="kw">delete Output; Output = NeuronOCL.getOutput(); } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void GradientMSA(__global class="type">class="kw">float *matrix_t, __global class="type">class="kw">float *matrix_o, __global class="type">class="kw">float *matrix_g ) { class="type">int i = get_global_id(class="num">0); matrix_g[i] = matrix_t[i] - matrix_o[i]; } __kernel class="type">void CumulativeGradient(__global class="type">class="kw">float *gradient_freq, __global class="type">class="kw">float *gradient_tmp, __global class="type">class="kw">float *gradient_out, class="type">class="kw">float alpha ) { class="type">int i = get_global_id(class="num">0); gradient_out[i] = alpha * gradient_freq[i] + (class="num">1 - alpha) * gradient_tmp[i]; } class="type">bool CNeuronFreDFOCL::calcOutputGradients(CArrayFloat *Target, class="type">class="kw">float &error) {
「梯度同步与频域损失回传的判错链」
这段逻辑是训练循环里最容易被忽略的守门员:任何一步数组长度不对或指针写入失败,整个梯度更新直接返 false,MT5 上表现为指标算到一半不刷新。 先看前几行判错:Target 为空、或者 Target 样本数少于 Output,立即退出;两者相等时把 Target 整段 AssignArray 给 Gradient,不等则逐点 Update 覆盖已有梯度缓冲。 随后 BufferWrite 落盘,若 bTranspose 为真,先对 Output 和 Gradient 做转置再跑 FFT(实/虚部分别进 cForecastFreRe/Im 与 cTargetFreRe/Im);不为真就直接对原数组 FFT。这里转置与否会影响 iWindow 与 iCount 的传参顺序,调参时若发现频谱错位,八成是这两行传反了。 频域里用 FreqMSA 算目标与预测的实数/虚数残差,存进 cLossFreRe/Im,同时对 Gradient 与 Output 做另一路 FreqMSA。最后逆 FFT 把损失谱变回 cGradientFreRe/Im,若开了转置再转置回来跑 CumulativeGradient 累加,学习率 fAlpha 在此生效。 开 MT5 把这段塞进你的 EA 训练函数,故意把 Target.Total() 设成比 Output 小 1,能直接验证第一道 return false 是否如预期卡住,避免静默训练出废模型。外汇与贵金属品种上跑这类频域梯度请务必用小资金验证,高频噪声可能导致梯度爆炸。
if(!Target) class="kw">return false; if(Target.Total() < Output.Total()) class="kw">return false; if(Target.Total() == Output.Total()) { if(!Gradient.AssignArray(Target)) class="kw">return false; } else { for(class="type">int i = class="num">0; i < Output.Total(); i++) { if(!Gradient.Update(i, Target.At(i))) class="kw">return false; } } if(!Gradient.BufferWrite()) class="kw">return false; if(bTranspose) { if(!Transpose(Output, GetPointer(cTranspose), iWindow, iCount)) class="kw">return false; if(!FFT(GetPointer(cTranspose), NULL, GetPointer(cForecastFreRe), GetPointer(cForecastFreIm), false)) class="kw">return false; if(!Transpose(Gradient, GetPointer(cTranspose), iWindow, iCount)) class="kw">return false; if(!FFT(GetPointer(cTranspose), NULL, GetPointer(cTargetFreRe), GetPointer(cTargetFreIm), false)) class="kw">return false; } else { if(!FFT(Output, NULL, GetPointer(cForecastFreRe), GetPointer(cForecastFreIm), false)) class="kw">return false; if(!FFT(Gradient, NULL, GetPointer(cTargetFreRe), GetPointer(cTargetFreIm), false)) class="kw">return false; } if(!FreqMSA(GetPointer(cTargetFreRe), GetPointer(cForecastFreRe), GetPointer(cLossFreRe))) class="kw">return false; if(!FreqMSA(GetPointer(cTargetFreIm), GetPointer(cForecastFreIm), GetPointer(cLossFreIm))) class="kw">return false; if(!FreqMSA(Gradient, Output, Gradient)) class="kw">return false; if(!FFT(GetPointer(cLossFreRe), GetPointer(cLossFreIm), GetPointer(cGradientFreRe), GetPointer(cGradientFreIm), true)) class="kw">return false; if(bTranspose) { if(!Transpose(GetPointer(cGradientFreRe), GetPointer(cTranspose), iCount, iWindow)) class="kw">return false; if(!CumulativeGradient(GetPointer(cTranspose), Gradient, Gradient, fAlpha)) class="kw">return false; } else
频率域神经元的序列化与重载细节
这段 CNeuronFreDFOCL 的实现把梯度累积、输入梯度计算和模型存取拆成了三个独立方法,核心都在和 OpenCL 缓冲区以及文件句柄打交道。 calcInputGradients 先判空指针,再调 DeActivation 用上一层输出、梯度、本层梯度与激活函数反推输入梯度;任何一步返回 false 都会直接中断,避免脏梯度进网络。 Save 方法依次落盘 iWindow、iCount、iFFTin、bTranspose 四个整型和 fAlpha 一个浮点,每个 FileWrite 都校验写入字节数是否达到 INT_VALUE 或 sizeof(float),少写即返 false。 Load 更苛刻:每读一个字段都用 FileIsEnding 探尾,防止截断文件导致参数错乱;读完 bTranspose 后按 (bTranspose ? iWindow : iCount) 算 n,给四个复数缓冲各分配 iFFTin * n 长度并绑 OpenCL。 在 MT5 里跑这套类,若训练中途报 Load 失败,优先查文件是否少写了 fAlpha 之后的字节,而不是怀疑网络结构。外汇与贵金属模型训练属高风险实验,参数误读可能让预测倾向完全偏移。
if(!CumulativeGradient(GetPointer(cGradientFreRe), Gradient, Gradient, fAlpha)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFreDFOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; class=class="str">"cmt">//--- class="kw">return DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), Gradient, NeuronOCL.Activation()); } class="type">bool CNeuronFreDFOCL::Save(const class="type">int file_handle) { if(!CNeuronBaseOCL::Save(file_handle)) class="kw">return false; if(FileWriteInteger(file_handle, class="type">int(iWindow)) < INT_VALUE) class="kw">return false; if(FileWriteInteger(file_handle, class="type">int(iCount)) < INT_VALUE) class="kw">return false; if(FileWriteInteger(file_handle, class="type">int(iFFTin)) < INT_VALUE) class="kw">return false; if(FileWriteInteger(file_handle, class="type">int(bTranspose)) < INT_VALUE) class="kw">return false; if(FileWriteFloat(file_handle, fAlpha) < class="kw">sizeof(class="type">class="kw">float)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFreDFOCL::Load(const class="type">int file_handle) { if(!CNeuronBaseOCL::Load(file_handle)) class="kw">return false; if(FileIsEnding(file_handle)) class="kw">return false; iWindow = class="type">uint(FileReadInteger(file_handle)); if(FileIsEnding(file_handle)) class="kw">return false; iCount = class="type">uint(FileReadInteger(file_handle)); if(FileIsEnding(file_handle)) class="kw">return false; iFFTin = class="type">uint(FileReadInteger(file_handle)); if(FileIsEnding(file_handle)) class="kw">return false; bTranspose = class="type">bool(FileReadInteger(file_handle)); if(FileIsEnding(file_handle)) class="kw">return false; fAlpha = FileReadFloat(file_handle); class="type">uint n = (bTranspose ? iWindow : iCount); if(!cForecastFreRe.BufferInit(iFFTin * n, class="num">0) || !cForecastFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cForecastFreIm.BufferInit(iFFTin * n, class="num">0) || !cForecastFreIm.BufferCreate(OpenCL)) class="kw">return false; if(!cTargetFreRe.BufferInit(iFFTin * n, class="num">0) || !cTargetFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cTargetFreIm.BufferInit(iFFTin * n, class="num">0) || !cTargetFreIm.BufferCreate(OpenCL)) class="kw">return false;
◍ 频域缓冲与网络层的初始化落点
在 MT5 的 OpenCL 计算封装里,先给四个频域复数缓冲(实部/虚部各两组)按 iFFTin * n 的长度做 BufferInit 并绑定 OpenCL,任何一组失败就直接 return false,这套前置不通后面全白做。 若 bTranspose 为真,转置缓冲按 iWindow * iCount 初始化并上 OpenCL;否则主动 BufferFree + Clear 释放,避免旧显存残留拖慢下一轮训练。 网络结构描述里,第 17 层统一用 defNeuronFreDFOCL 类型、activation 设 None、optimization 走 ADAM,probability 固定 0.8f。编码器窗口取 BarDescr、count 为 NForecast、step 为 int(true);Actor 窗口 NActions、count 1、step int(false);Critic 窗口 NRewards、count 1、step int(false)——三组只在 window 与 step 上有差异。 这类 0.8 的 dropout 概率加上 None 激活,倾向用于频域直接输出不归一化的原始信号,外汇与贵金属行情高频噪声大,实盘前务必在 MT5 策略测试器用小样本验证显存占用与回测稳定性,高风险。
if(!cLossFreRe.BufferInit(iFFTin * n, class="num">0) || !cLossFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cLossFreIm.BufferInit(iFFTin * n, class="num">0) || !cLossFreIm.BufferCreate(OpenCL)) class="kw">return false; if(!cGradientFreRe.BufferInit(iFFTin * n, class="num">0) || !cGradientFreRe.BufferCreate(OpenCL)) class="kw">return false; if(!cGradientFreIm.BufferInit(iFFTin * n, class="num">0) || !cGradientFreIm.BufferCreate(OpenCL)) class="kw">return false; if(bTranspose) { if(!cTranspose.BufferInit(iWindow * iCount, class="num">0) || !cTranspose.BufferCreate(OpenCL)) class="kw">return false; } else { cTranspose.BufferFree(); cTranspose.Clear(); } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *encoder) { class=class="str">"cmt">//--- ........ ........ class=class="str">"cmt">//--- layer class="num">17 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFreDFOCL; descr.window = BarDescr; descr.count = NForecast; descr.step = class="type">int(true); descr.probability = class="num">0.8f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return false; } class=class="str">"cmt">//--- Actor ......... ......... class=class="str">"cmt">//--- layer class="num">17 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFreDFOCL; descr.window = NActions; descr.count = class="num">1; descr.step = class="type">int(false); descr.probability = class="num">0.8f; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Critic ......... ......... class=class="str">"cmt">//--- layer class="num">17 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFreDFOCL; descr.window = NRewards; descr.count = class="num">1; descr.step = class="type">int(false); descr.probability = class="num">0.8f; descr.activation = None; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//---
「函数收尾的返回约定」
这段 MQL5 片段只做了一件事:在条件满足后向调用方回传布尔值 true,标志着当前校验或分支逻辑已正常走通。 在 EA 或指标里,这类 return true 常出现在参数合法性检查、订单预处理通过的末尾;若前面任一环节失败,往往提前 return false 中断。 实盘中外挂这类开关函数,能帮助你在 MT5 策略测试器里快速定位“为什么这单没发出来”——把返回点打在日志上即可。外汇与贵金属杠杆高,任何逻辑分支漏判都可能放大滑点风险,返回值的语义必须自己逐行核对。
class="kw">return true; }