神经网络变得简单(第 89 部分):频率增强分解变换器(FEDformer)·进阶篇
(2/3)· 标准变换器在长周期行情预测里又慢又失真,这篇拆开频率增强分解的实战改造路径
卷积权重按勒让德阶数分段灌入
这段初始化逻辑把不同阶数的 Legendre 小波权重依次塞进卷积缓冲区:每写完一组就往前跳 iWindow+1 个偏移量,顺序为 12、16、18、20 阶。若任意一次 Update 返回 false,整个流程直接 return false,说明权重装载在中途断裂,后续前传不会执行。 当 OpenCL 对象非空时,循环结束后还会调用 WeightsConv.BufferWrite() 把权重真正推到显存;这一步失败同样会中断返回。外汇与贵金属行情的高波动下,这类特征工程层若装载异常,模型对突变段的响应可能明显滞后,属高风险环节。 下面这段类声明定义了 CNeuronFEDW 的内部结构:iWindow 与 iCount 控制窗口与分量数,cWavlets 持有勒让德小波,cNorm、cSoftMax、cFF[2] 与 cReconstruct 串起归一化到重建的通路。虚函数里 feedForward / updateInputWeights / calcInputGradients 是训练必需重载项,Reconsruct 单独负责从输入缓冲重建输出。
if(!WeightsConv.Update(shift, Legendre12(k))) class="kw">return false; shift += iWindow + class="num">1; if(!WeightsConv.Update(shift, Legendre16(k))) class="kw">return false; shift += iWindow + class="num">1; if(!WeightsConv.Update(shift, Legendre18(k))) class="kw">return false; shift += iWindow + class="num">1; if(!WeightsConv.Update(shift, Legendre20(k))) class="kw">return false; } if(!!OpenCL) if(!WeightsConv.BufferWrite()) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronFEDW : class="kw">public CNeuronBaseOCL { class="kw">protected: class=class="str">"cmt">//--- class="type">uint iWindow; class="type">uint iCount; class=class="str">"cmt">//--- CNeuronLegendreWavelets cWavlets; CNeuronBatchNormOCL cNorm; CNeuronSoftMaxOCL cSoftMax; CNeuronConvOCL cFF[class="num">2]; CNeuronBaseOCL cReconstruct; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); class=class="str">"cmt">//--- class="kw">virtual class="type">bool Reconsruct(CBufferFloat* inputs, CBufferFloat *outputs); class="kw">public: CNeuronFEDW(class="type">void) {}; ~CNeuronFEDW(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, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); 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 defNeuronFEDW; } class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau); }; class="type">bool CNeuronFEDW::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) {
「小波与归一化层在 Init 里的接线细节」
这段 CNeuronFEDW::Init 展示了在 MT5 中用 OpenCL 搭建小波特征提取网络的初始化顺序:先调基类 CNeuronBaseOCL::Init 拿到输出数与索引,再依次把 cWavlets、cNorm、cSoftMax、cReconstruct 以及两层 cFF 全接上。任何一步 Init 返回 false 就直接退出,避免半残网络进训练循环。 cWavlets 用 iWindow 和 iCount 指定窗口与序列数,激活函数设为 None,说明它只做线性小波变换不参与非线性映射。cNorm 的隐藏层维度写死 9 * iCount、第二参数 1000,是给后续 softmax 前做幅度压制的经验值。 cSoftMax.SetHeads(iCount) 把多头概率拆回序列维度,cReconstruct 用 iWindow 做单步重建。两层 cFF 的神经元数呈 4*iWindow → 4*iWindow → iWindow 的瓶颈结构,仅第一层用 LReLU,其余全 None。 Reconsruct 方法里 global_work_size[0] = sequence.Total() 直接拿序列长度当 GPU 并行规模,随后把 cWavlets 权重、probability、sequence 分别绑到 def_k_CalcHiddenGradientConv 内核的参数槽。哪次 SetArgumentBuffer 失败就 printf 出函数名、GetLastError 和 __LINE__,方便在 MT5 Experts 日志里定位是第几个绑定断的。
if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * count, optimization_type, batch)) class="kw">return false; iWindow = window; iCount = count; if(!cWavlets.Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, iCount, optimization, iBatch)) class="kw">return false; cWavlets.SetActivationFunction(None); if(!cNorm.Init(class="num">0, class="num">1, OpenCL, class="num">9 * iCount, class="num">1000,optimization)) class="kw">return false; cNorm.SetActivationFunction(None); if(!cSoftMax.Init(class="num">0, class="num">1, OpenCL, class="num">9 * iCount, optimization, iBatch)) class="kw">return false; cSoftMax.SetHeads(iCount); cSoftMax.SetActivationFunction(None); if(!cReconstruct.Init(class="num">0, class="num">2, OpenCL, iWindow, optimization, iBatch)) class="kw">return false; cReconstruct.SetActivationFunction(None); if(!cFF[class="num">0].Init(class="num">0, class="num">3, OpenCL, iWindow, iWindow, class="num">4 * iWindow, iCount, optimization, iBatch)) class="kw">return false; cFF[class="num">0].SetActivationFunction(LReLU); if(!cFF[class="num">1].Init(class="num">0, class="num">4, OpenCL, class="num">4 * iWindow, class="num">4 * iWindow, iWindow, iCount, optimization, iBatch)) class="kw">return false; SetActivationFunction(None); if(Gradient != cFF[class="num">1].getGradient()) SetGradient(cFF[class="num">1].getGradient()); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFEDW::Reconsruct(CBufferFloat *sequence, CBufferFloat *probability) { class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0] = sequence.Total(); if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_w, cWavlets.GetWeightsConv().GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_g, probability.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_o, probability.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CalcHiddenGradientConv, def_k_chgc_matrix_ig, sequence.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_outputs, probability.Total())) {
◍ 卷积梯度核的参数绑定与前向链路
在 OpenCL 加速的神经网络层里,CalcHiddenGradientConv 内核的参数必须在执行前逐一对齐。下面这段把 step、window_in、window_out 等 6 个参数通过 SetArgument 写进内核,其中 window_out 被硬编码为 9,activation 取 None,任何一步返回 false 都会用 printf 抛出 __FUNCTION__ 与 __LINE__ 定位。 内核真正跑起来靠 OpenCL.Execute,全局工作项数量由 global_work_size 控制,偏移量走 global_work_offset;只要执行失败就直接退出,不再往下走。这套写法在 MT5 里调试时,若终端日志出现 'Error of execution kernel' 且错误码非 0,优先怀疑显存参数越界而非算法本身。 前向传播则是一条串行的对象链:小波层 cWavlets 先过,接归一化 cNorm,再进 cSoftMax,随后 Reconsruct 用重建输出和 softmax 输出做重构。SumAndNormilize 把结果按 iWindow 窗口聚合(末参 1 表示某种归一开关),最后两层全连接 cFF[0]、cFF[1] 依次前推,任何一环返回 false 即中断。外汇与贵金属行情下用此类 GPU 模型做特征提取,回测过拟合概率偏高,实盘前务必做样本外验证。
if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_step, (class="type">int)iWindow)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_window_in, (class="type">int)iWindow)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_window_out, (class="type">int)class="num">9)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_activation, (class="type">int)None)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CalcHiddenGradientConv, def_k_chgc_shift_out, (class="type">int)class="num">0)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_CalcHiddenGradientConv, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFEDW::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cWavlets.FeedForward(NeuronOCL.AsObject())) class="kw">return false; if(!cNorm.FeedForward(cWavlets.AsObject())) class="kw">return false; if(!cSoftMax.FeedForward(cNorm.AsObject())) class="kw">return false; if(!Reconsruct(cReconstruct.getOutput(), cSoftMax.getOutput())) class="kw">return false; if(!SumAndNormilize(NeuronOCL.getOutput(), cReconstruct.getOutput(), cReconstruct.getOutput(), iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; if(!cFF[class="num">0].FeedForward(cReconstruct.AsObject())) class="kw">return false; if(!cFF[class="num">1].FeedForward(cFF[class="num">0].AsObject())) class="kw">return false;
反向传播里的张量暂存与权重回写
FEDW 神经元类的梯度计算函数 calcInputGradients 里,先用 SumAndNormilize 把重构分支的梯度做归一,随后立刻把 cReconstruct 的输出指针暂存到 temp_r、把 cWavlets 的输出指针暂存到 temp_w。这一步不是多余动作——后面 cReconstruct 被强行塞入梯度当作前向输入跑了一遍 FeedForward,若不暂存,原本的激活值就丢了,回写时 temp_r / temp_w 能把现场还原。 注意 cWavlets.FeedForward(cReconstruct.AsObject()) 这行:它借重构梯度当输入推了一次小波层前向,但紧接着用 SetOutput(temp_w) 把小波输出覆盖回去,等于只借通道算梯度、不污染权重态。这种「借输出算梯度再还原」的写法在 OpenCL 后端里很常见,能省一次 kernel 调度。 updateInputWeights 只回写了三条路径:cFF[0] 接重构对象、cFF[1] 接 cFF[0]、cNorm 接 cWavlets。也就是说编码器最前端的输入层权重并不在本函数内更新,而是由外部 NeuronOCL 的 calcHiddenGradients 承接——如果你在 MT5 里改了网络拓扑却忘了在外层调更新,可能出现前层梯度累积却永不落盘的现象。 CreateEncoderDescriptions 里输入层 descr.count 直接等于 HistoryBars * BarDescr,优化器写死 ADAM、激活函数为 None。想验证的话,开 MT5 把 HistoryBars 调到 200、BarDescr 设 5,输入维度就是 1000 个浮点,显存占用和收敛速度会明显和默认 64*3 不同。外汇与贵金属行情高波动,这类自编码结构仅作特征压缩实验,实盘信号务必自带风控。
if(!SumAndNormilize(cFF[class="num">1].getOutput(), cReconstruct.getOutput(), getOutput(), iWindow, true, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFEDW::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; if(!cFF[class="num">0].calcHiddenGradients(cFF[class="num">1].AsObject())) class="kw">return false; if(!cReconstruct.calcHiddenGradients(cFF[class="num">0].AsObject())) class="kw">return false; if(!SumAndNormilize(Gradient, cReconstruct.getGradient(), cReconstruct.getGradient(), iWindow, false)) class="kw">return false; CBufferFloat *temp_r = cReconstruct.getOutput(); if(!cReconstruct.SetOutput(cReconstruct.getGradient(), false)) class="kw">return false; CBufferFloat *temp_w = cWavlets.getOutput(); if(!cWavlets.SetOutput(cSoftMax.getGradient(), false)) class="kw">return false; if(!cWavlets.FeedForward(cReconstruct.AsObject())) class="kw">return false; if(!cWavlets.SetOutput(temp_w, false)) class="kw">return false; if(!cReconstruct.SetOutput(temp_r, false)) class="kw">return false; if(!cNorm.calcHiddenGradients(cSoftMax.AsObject())) class="kw">return false; if(!cWavlets.calcHiddenGradients(cNorm.AsObject())) class="kw">return false; if(!NeuronOCL.calcHiddenGradients(cWavlets.AsObject())) class="kw">return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cReconstruct.getGradient(), NeuronOCL.getGradient(), iWindow, false)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronFEDW::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cFF[class="num">0].UpdateInputWeights(cReconstruct.AsObject())) class="kw">return false; if(!cFF[class="num">1].UpdateInputWeights(cFF[class="num">0].AsObject())) class="kw">return false; if(!cNorm.UpdateInputWeights(cWavlets.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) {
「编码器十六层的栈式装配细节」
这段装配逻辑把价格序列编码器的十六层结构一次性压进 encoder 对象,每层都先 new 一个 CLayerDescription,失败就 delete 并返 false,确保 MT5 端内存不漏。 第一层用 defNeuronBatchNormOCL 做批归一,batch 写死 10000、优化器 ADAM,倾向用于抹平不同品种量纲差异;第二层 defNeuronTransposeOCL 把 HistoryBars 长度重排到 BarDescr 维度。 第三到十二层是十次循环塞入的 defNeuronFEDW,window 锁 HistoryBars、count 为 BarDescr,循环里任一层 Add 失败立即回收 descr 退出,这是实盘加载时最容易卡住的十连判。 第十三层 defNeuronTiDEOCL 较特殊:windows 数组按 {HistoryBars, 2*EmbeddingSize, EmbeddingSize, 2*EmbeddingSize, NForecast} 拷入,step=4,若 ArrayCopy 返回值 ≤0 直接返 false,说明嵌入维度参数错一处整网就废。 第十四到十五层分别是 defNeuronConvOCL(步长与输出窗同为 NForecast)和 defNeuronTransposeOCL,把预测窗拉回 BarDescr 结构;第十六层 defNeuronRevInDenormOCL 做反归一,count = BarDescr*NForecast、layers=1、优化器 ADAM。 开 MT5 把这段直接贴进你的 CNet 派生类,重点核对 EmbeddingSize 与 NForecast 宏定义,二者不匹配时第十三层 ArrayCopy 必然失败。
class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">10000; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = HistoryBars; descr.window = BarDescr; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3-class="num">12 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFEDW; descr.count = BarDescr; descr.window = HistoryBars; descr.activation = None; for(class="type">int i = class="num">0; i < class="num">10; i++) if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">13 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTiDEOCL; descr.count = BarDescr; descr.window = HistoryBars; descr.window_out = NForecast; descr.step = class="num">4; { class="type">int windows[] = {HistoryBars, class="num">2 * EmbeddingSize, EmbeddingSize, class="num">2 * EmbeddingSize, NForecast}; if(ArrayCopy(descr.windows, windows) <= class="num">0) class="kw">return false; } descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">14 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = NForecast; descr.step = NForecast; descr.window_out = NForecast; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">15 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = BarDescr; descr.window = NForecast; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">16 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronRevInDenormOCL; descr.count = BarDescr * NForecast; descr.activation = None; descr.optimization = ADAM; descr.layers = class="num">1;