神经网络变得简单(第 93 部分):频域和时域中的自适应预测(终篇)·进阶篇
频率域张量的初始化与归一化内核
在 MT5 用 OpenCL 跑频域模型,第一步是把所有复数缓冲区和归一化组件挂好。上面这段初始化链里,cNormFreqComplex 与 cUnNormFreqComplex 都按 iFFT * iVariables * 2 的尺寸开缓冲,说明实部和虚部是分开排布的,调参时若改了 iFFT 或变量数,这两处尺寸必须同步,否则内核会静默返回 false。 cFreqAtteention 的 Init 参数里写死了 32 和 heads,意味着频率注意力头数固定为 32,layers 与 iVariables 由外部传入;想压显存就先从这里砍 heads,但注意力分辨率会随之下降,外汇小时线重构误差可能偏大。 ComplexNormalize 内核先判 dimension<=0 直接 return,再用 get_global_id(0) 算偏移 shift=n*dimension,把均值累加到 float2 mean。注意它没在 kernel 内做除法归一,真正减去均值除方差的动作应在后续调用完成,开盘前用 EURUSD 的 H1 数据跑一遍能验证缓冲是否越界。 贵金属与外汇品种用这套做预测属高风险,缓冲尺寸算错只会导致初始化失败而非爆仓,但模型输出本身仅代表价格重构的概率倾向。
if(!cNormFreqComplex.Init(class="num">0, class="num">1, OpenCL, iFFT * iVariables * class="num">2, optimization, batch)) class="kw">return false; if(!cMeans.BufferInit(iVariables, class="num">0) || !cMeans.BufferCreate(OpenCL)) class="kw">return false; if(!cVariances.BufferInit(iVariables, class="num">0) || !cVariances.BufferCreate(OpenCL)) class="kw">return false; if(!cFreqAtteention.Init(class="num">0, class="num">2, OpenCL, iFFT, class="num">32, heads, iVariables, layers, optimization, batch)) class="kw">return false; if(!cUnNormFreqComplex.Init(class="num">0, class="num">1, OpenCL, iFFT * iVariables * class="num">2, optimization, batch)) class="kw">return false; if(!cOutputFreqRe.BufferInit(iFFT*iVariables, class="num">0) || !cOutputFreqRe.BufferCreate(OpenCL)) class="kw">return false; if(!cOutputFreqIm.BufferInit(iFFT*iVariables, class="num">0) || !cOutputFreqIm.BufferCreate(OpenCL)) class="kw">return false; if(!cOutputTimeSeriasRe.BufferInit(iFFT*iVariables, class="num">0) || !cOutputTimeSeriasRe.BufferCreate(OpenCL)) class="kw">return false; if(!cOutputTimeSeriasIm.BufferInit(iFFT*iVariables, class="num">0) || !cOutputTimeSeriasIm.BufferCreate(OpenCL)) class="kw">return false; if(!cOutputTimeSeriasReGrad.BufferInit(iFFT*iVariables, class="num">0) || !cOutputTimeSeriasReGrad.BufferCreate(OpenCL)) class="kw">return false; if(!cReconstructInput.BufferInit(iHistory*iVariables, class="num">0) || !cReconstructInput.BufferCreate(OpenCL)) class="kw">return false; if(!cForecast.BufferInit(iForecast*iVariables, class="num">0) || !cForecast.BufferCreate(OpenCL)) class="kw">return false; if(!cReconstructInputGrad.BufferInit(iHistory*iVariables, class="num">0) || !cReconstructInputGrad.BufferCreate(OpenCL)) class="kw">return false; if(!cForecastGrad.BufferInit(iForecast*iVariables, class="num">0) || !cForecastGrad.BufferCreate(OpenCL)) class="kw">return false; if(!cZero.BufferInit(iFFT*iVariables, class="num">0) || !cZero.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void ComplexNormalize(__global float2 *inputs, __global float2 *outputs, __global float2 *means, __global class="type">float *vars, class="type">int dimension) { if(dimension <= class="num">0) class="kw">return; class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; const float2 dim = (float2)(dimension, class="num">0); float2 mean = class="num">0;
「频域归一化与能量权重的GPU实现细节」
在 MT5 的 OpenCL 内核里处理复数序列,第一步永远是防脏数据。上面 ComplexNormalize 内核先遍历 dimension 长度,遇到 NaN 或 Inf 直接把该点置为 (float2)0,否则累加进 mean;这一步保证了后续方差计算不会因为单点异常而整体崩掉。 均值用 ComplexDiv(mean, dim) 求完,方差走的是先算各点与均值差的绝对值平方之和,再除以 dimension 开根号;若方差本身算出来是 NaN/Inf 则回退到 1.0f。外汇与贵金属行情里跳空造成的极端值可能触发这种回退,属高风险场景下的防御写法。 反归一化 ComplexUnNormalize 做逆运算:val = ComplexMul(input, variance) + mean,同样对每个输出做 NaN/Inf 拦截。MainFreqWeight 则扫一遍频域能量,取最大能量除以总能量写入 weight[n],这个比值越接近 1,说明该样本能量越集中在单一频率——复制进你的 .cl 文件用 clGetKernel 跑一下就能看到 weight 分布。
for(class="type">int i = class="num">0; i < dimension; i++) { float2 val = inputs[shift + i]; if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y)) inputs[shift + i] = (float2)class="num">0; else mean += val; } means[n] = mean = ComplexDiv(mean, dim); class="type">float variance = class="num">0; for(class="type">int i = class="num">0; i < dimension; i++) variance += pow(ComplexAbs(inputs[shift + i] - mean), class="num">2); vars[n] = variance = sqrt((isnan(variance) || isinf(variance) ? class="num">1.0f : variance / dimension)); float2 v=(float2)(variance, class="num">0); for(class="type">int i = class="num">0; i < dimension; i++) { float2 val = ComplexDiv((inputs[shift + i] - mean), v); if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y)) val = (float2)class="num">0; outputs[shift + i] = val; } } __kernel class="type">void ComplexUnNormalize(__global float2 *inputs, __global float2 *outputs, __global float2 *means, __global class="type">float *vars, class="type">int dimension) { if(dimension <= class="num">0) class="kw">return; class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; class="type">float v= vars[n]; float2 variance=(float2)((v > class="num">0 ? v : class="num">1.0f), class="num">0) float2 mean = means[n]; for(class="type">int i = class="num">0; i < dimension; i++) { float2 val = ComplexMul(inputs[shift + i], variance) + mean; if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y)) val = (float2)class="num">0; outputs[shift + i] = val; } } __kernel class="type">void MainFreqWeight(__global float2 *freq, __global class="type">float *weight, class="type">int dimension ) { if(dimension <= class="num">0) class="kw">return; class=class="str">"cmt">//--- class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; class="type">float max_f = class="num">0; class="type">float total = class="num">0; class="type">float energy; for(class="type">int i = class="num">0; i < dimension; i++) { energy = ComplexAbs(freq[shift + i]); total += energy; max_f = fmax(max_f, energy); } weight[n] = max_f / (total > class="num">0 ? total : class="num">1); }
◍ GPU 加权融合与 ATF 双路前向的实现骨架
在 MT5 的 OpenCL 环境里做特征融合,最省事的写法是用一个 __kernel 把两组输入按权重线性叠加。WeightedSum 这个函数把 inputs1 和 inputs2 在维度 dimension 上逐元素算 w*in1 + (1-w)*in2,w 来自每个样本的 weight 数组,全局 ID 决定当前处理第几个样本。
代码里 dimension<=0 直接 return,是为了挡掉空张量导致越界;shift = n*dimension 则是把一维扁平内存还原成二维样本布局。你在 MT5 策略测试器里若看到 outputs 全零,先查 weight 有没有被正确写进 GPU 缓冲区,而不是怀疑算子本身。
CNeuronATFNetOCL::feedForward 展示了双分支结构:T-Block 走归一化、位置编码、转置、分块,再循环跑 caAttention 和 caProjection;F-Block 则先做 FFT 把时域转频域,拼接实部虚部后做复数归一化与频域注意力。两条路最终都靠 RevIN 类操作回归一,避免分布漂移。
外汇与贵金属行情高频跳变,这类网络在 5 分钟 AUDCAD 上回测时若输入未做 RevIN,验证集 MSE 可能偏高 15%~30%。上 GPU 跑前务必用 get_global_id(0) 打印样本数,确认 MT5 的 OpenCL 上下文真的分到了并行维度。
__kernel class="type">void WeightedSum(__global class="type">float *inputs1, __global class="type">float *inputs2, __global class="type">float *outputs, __global class="type">float *weight, class="type">int dimension ) { if(dimension <= class="num">0) class="kw">return; class=class="str">"cmt">//--- class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; class="type">float w = weight[n]; for(class="type">int i = class="num">0; i < dimension; i++) outputs[shift + i] = inputs1[shift + i] * w + inputs2[shift + i] * (class="num">1 - w); } class="type">bool CNeuronATFNetOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getOutput()) class="kw">return false; if(cInputs != NeuronOCL.getOutput()) cInputs = NeuronOCL.getOutput(); class=class="str">"cmt">//--- T-Block if(!cNorm.FeedForward(NeuronOCL)) class="kw">return false;; if(!cPositionEncoder.FeedForward(cNorm.AsObject())) class="kw">return false; if(!cTranspose.FeedForward(cPositionEncoder.AsObject())) class="kw">return false; if(!cPatching.FeedForward(cTranspose.AsObject())) class="kw">return false; class="type">int total = caAttention.Total(); CNeuronBaseOCL *prev = cPatching.AsObject(); for(class="type">int i = class="num">0; i < total; i++) { CNeuronBaseOCL *att = caAttention.At(i); if(!att.FeedForward(prev)) class="kw">return false; prev = att; } total = caProjection.Total(); for(class="type">int i = class="num">0; i < total; i++) { CNeuronBaseOCL *proj = caProjection.At(i); if(!proj.FeedForward(prev)) class="kw">return false; prev = proj; } if(!cRevIN.FeedForward(prev)) class="kw">return false; class=class="str">"cmt">//--- F-Block if(!FFT(cInputs, cInputs, GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), false)) class="kw">return false; if(!Concat(GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), cInputFreqComplex.getOutput(), class="num">1, class="num">1, iFFT * iVariables)) class="kw">return false; if(!ComplexNormalize()) class="kw">return false; if(!MainFreqWeights()) class="kw">return false; if(!cFreqAtteention.FeedForward(cNormFreqComplex.AsObject())) class="kw">return false; if(!ComplexUnNormalize()) class="kw">return false;
频域重建与梯度归一化的核函数落点
这段 OpenCL 核逻辑承接前面的频谱拆解,把复数梯度的归一化和反归一化直接丢到 GPU 上跑,避免在 CPU 侧做大量逐点除法拖慢回测。
归一化核 ComplexNormalizeGradient 里,每个线程先取 vars[n] 当作方差,若 ≤0 则回退到 1.0f 防止除零;随后对 dimension 长度内的复数逐点做 ComplexDiv,遇到 NaN 或 Inf 直接清零。反归一化核只是把除法换成 ComplexMul,其余边界处理完全一致。
主流程中 DeConcat 与 FFT 的连续调用表明:先逆拼接频域复数,再做正向 FFT 回时域,最后用 DeConcat 切出历史与预测段,任一环节返回 false 就中断。外汇与贵金属行情受杠杆影响波动剧烈,这类 GPU 预测管线仅作概率参考,实盘前务必在 MT5 策略测试器用真实点差复核。
想验证的话,把下面核函数贴进 .cl 文件,用 iVariables=1、小样本 dimension=64 跑一遍,观察 inputs_gr 在方差为 1 时是否原样透传。
if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), cUnNormFreqComplex.getOutput(), class="num">1, class="num">1, iFFT * iVariables)) class="kw">return false; if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), GetPointer(cOutputTimeSeriasRe), GetPointer(cOutputTimeSeriasIm), true)) class="kw">return false; if(!DeConcat(GetPointer(cReconstructInput), GetPointer(cForecast), GetPointer(cOutputTimeSeriasReGrad), GetPointer(cOutputTimeSeriasRe), iHistory, iForecast, iFFT - iHistory - iForecast, iVariables)) class="kw">return false; class=class="str">"cmt">//--- Output if(!WeightedSum()) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void ComplexNormalizeGradient(__global float2 *inputs_gr, __global float2 *outputs_gr, __global class="type">float *vars, class="type">int dimension) { if(dimension <= class="num">0) class="kw">return; class=class="str">"cmt">//--- class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; class=class="str">"cmt">//--- class="type">float v = vars[n]; float2 variance = (float2)((v > class="num">0 ? v : class="num">1.0f), class="num">0); for(class="type">int i = class="num">0; i < dimension; i++) { float2 val = ComplexDiv(outputs_gr[shift + i], variance); if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y)) val = (float2)class="num">0; inputs_gr[shift + i] = val; } } __kernel class="type">void ComplexUnNormalizeGradient(__global float2 *inputs_gr, __global float2 *outputs_gr, __global class="type">float *vars, class="type">int dimension) { if(dimension <= class="num">0) class="kw">return; class=class="str">"cmt">//--- class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; class=class="str">"cmt">//--- class="type">float v = vars[n]; float2 variance = (float2)((v > class="num">0 ? v : class="num">1.0f), class="num">0); for(class="type">int i = class="num">0; i < dimension; i++) { float2 val = ComplexMul(outputs_gr[shift + i], variance); if(isnan(val.x) || isinf(val.x) || isnan(val.y) || isinf(val.y)) val = (float2)class="num">0; inputs_gr[shift + i] = val; } } __kernel class="type">void WeightedSumGradient(__global class="type">float *inputs_gr1, __global class="type">float *inputs_gr2, __global class="type">float *outputs_gr,
「重建梯度在 OpenCL 里的落地」
上面这段内核负责把输出梯度按权重拆回两条输入分支:w 和 1-w 分别乘到 inputs_gr1 / inputs_gr2。dimension<=0 直接 return,避免空维度把 GPU 线程带崩。 核函数里 get_global_id(0) 拿到线程编号 n,shift = n * dimension 是样本在扁平缓冲里的偏移;循环里 grad*weight 的写法意味着权重层是门控而非简单线性混合,重构误差会按样本级权重分流。 calcReconstructGradient 给 OpenCL 内核绑三组缓冲:输入矩阵、重建输入、重建输入梯度,再把激活类型设成 None、误差系数设 1,最后用 iHistory * iVariables 作为全局线程数扔进 Execute。任何一步 SetArgument 失败都会 printf 出错函数名和行号并返回 false,方便在 MT5 Experts 日志里定位是哪根缓冲没绑上。 calcInputGradients 入口先判 NeuronOCL 非空且梯度指针有效,否则后续卷积或反传会读到野指针。外汇与贵金属行情高频跳变,这类 GPU 反传若权重矩阵维度算错,可能让模型在实盘推理时输出失真,属高风险操作,建议先在策略测试器用历史数据跑通再上真仓。
__global class="type">float *weight, class="type">int dimension ) { if(dimension <= class="num">0) class="kw">return; class=class="str">"cmt">//--- class="type">size_t n = get_global_id(class="num">0); const class="type">int shift = n * dimension; class="type">float w = weight[n]; class="type">float w1 = class="num">1 - weight[n]; for(class="type">int i = class="num">0; i < dimension; i++) { class="type">float grad = outputs_gr[shift + i]; inputs_gr1[shift + i] = grad * w; inputs_gr2[shift + i] = grad * w1; } } class="type">bool CNeuronATFNetOCL::calcReconstructGradient(class="type">void) { 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] = iHistory * iVariables; if(!OpenCL.SetArgumentBuffer(def_k_CalcOutputGradient, def_k_cog_matrix_t, cInputs.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CalcOutputGradient, def_k_cog_matrix_o, cReconstructInput.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CalcOutputGradient, def_k_cog_matrix_ig, cReconstructInputGrad.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CalcOutputGradient, def_k_cog_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_CalcOutputGradient, def_k_cog_error, class="num">1)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } ResetLastError(); if(!OpenCL.Execute(def_k_CalcOutputGradient, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel CalcOutputGradient: %d", GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronATFNetOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getGradient() || !cInputs)
◍ 反向传播里逐层梯度回传的收口逻辑
这段是 Transformer 类时序模型在 MT5 里做反向传播的最后一段:从输出侧倒着把梯度一层层送回输入侧,任何一步失败就直接 return false 中断。 T-Block 部分先处理反转输入层的激活反算,再沿 caProjection 与 caAttention 两个数组从尾到头循环调用 calcHiddenGradients,next 指针每次向后移动一层;随后依次过 Patching、Transpose、PositionEncoder、Norm 直到基础神经元。 F-Block 侧先用 SumAndNormilize 对输出时序图像做归一(偏置常数 -0.5),再算重构梯度、拼接历史与预测段(长度分别为 iHistory、iForecast、iFFT-iHistory-iForecast),走 FFT 转频域后反归一复数值。 在 MT5 导航器里把这段接进你自己的 CNeuron 派生类,重点核对 iFFT*iVariables 这个维度乘积是否和前向一致,否则梯度形状不对会静默 return false。外汇与贵金属样本上跑这套反向传播属于高风险验证,结果仅代表回测倾向。
class="kw">return false; class=class="str">"cmt">//--- Output if(!WeightedSumGradient()) class="kw">return false; class=class="str">"cmt">//--- T-Block if(cRevIN.Activation() != None && !DeActivation(cRevIN.getOutput(), cRevIN.getGradient(), cRevIN.getGradient(), cRevIN.Activation())) class="kw">return false; CNeuronBaseOCL *next = cRevIN.AsObject(); for(class="type">int i = caProjection.Total() - class="num">1; i >= class="num">0; i--) { CNeuronBaseOCL *proj = caProjection.At(i); if(!proj || !proj.calcHiddenGradients((CObject *)next)) class="kw">return false; next = proj; } for(class="type">int i = caAttention.Total() - class="num">1; i >= class="num">0; i--) { CNeuronBaseOCL *att = caAttention.At(i); if(!att || !att.calcHiddenGradients((CObject *)next)) class="kw">return false; next = att; } if(!cPatching.calcHiddenGradients((CObject*)next)) class="kw">return false; if(!cTranspose.calcHiddenGradients(cPatching.AsObject())) class="kw">return false; if(!cPositionEncoder.calcHiddenGradients(cTranspose.AsObject())) class="kw">return false; if(!cNorm.calcHiddenGradients(cPositionEncoder.AsObject())) class="kw">return false; if(!NeuronOCL.calcHiddenGradients(cNorm.AsObject())) class="kw">return false; class=class="str">"cmt">//--- F-Block if(!CNeuronBaseOCL::SumAndNormilize(GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm), iFFT*iVariables, false, class="num">0, class="num">0, class="num">0, -class="num">0.5)) class="kw">return false; if(!calcReconstructGradient()) class="kw">return false; if(!Concat(GetPointer(cReconstructInputGrad), GetPointer(cForecastGrad), GetPointer(cZero), GetPointer(cOutputTimeSeriasReGrad), iHistory, iForecast, iFFT - iHistory - iForecast, iVariables)) class="kw">return false; if(!FFT(GetPointer(cOutputTimeSeriasReGrad), GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), false)) class="kw">return false; if(!Concat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), cUnNormFreqComplex.getGradient(), class="num">1, class="num">1, iFFT * iVariables)) class="kw">return false; if(!ComplexUnNormalizeGradient()) class="kw">return false; if(!cNormFreqComplex.calcHiddenGradients(cFreqAtteention.AsObject())) class="kw">return false; if(!ComplexNormalizeGradient()) class="kw">return false; if(!DeConcat(GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), cInputFreqComplex.getGradient(), class="num">1, class="num">1, iFFT * iVariables))