神经网络变得简单(第 92 部分):频域和时域中的自适应预测·进阶篇
📡

神经网络变得简单(第 92 部分):频域和时域中的自适应预测·进阶篇

(2/3)· 当 DFT 频谱错位拖垮预测,动态加权融合如何补上实时序列的混合周期缺口

进阶 第 2/3 篇
不少人在做序列预测时只盯时域局部波动,忽略频域全局周期,结果遇到跨周期混合形态就失灵。更隐蔽的坑是离散傅里叶变换带来的频谱不匹配,已知数据段和整段序列的频谱对不上,预测偏差悄悄放大。

Transformer 权重矩阵与张量缓冲的初始化开销

在 MT5 里用 OpenCL 跑Transformer类模型,先得算清前馈层的权重矩阵尺寸。代码里 ff_1 = 2 * 4 * (iWindow + 1) * iWindow,ff_2 = 2 * (4 * iWindow + 1) * iWindow,两者差异来自层间维度展开方式不同;当 iWindow=64 时,ff_1 约为 33280,ff_2 约为 32896,差出近 400 个 float,显存分配不能混用。 外层按 iLayers 循环、内层 d<2 跑两遍,每一轮都为 QKV、scores、多头注意力输出、注意力输出各 new 一个 CBufferFloat 并推入对应数组。任何一步 BufferInit 或 BufferCreate(OpenCL) 失败就直接 return false,意味着单层初始化崩了整个模型就起不来。 开 MT5 把 iWindow 从 32 调到 128 试一次,你会看到 ff_1 从 8448 跳到 132096,GPU 缓冲占用近似线性放大;外汇和贵金属行情序列用这类结构做推断属高风险,过宽窗口可能让显存直接爆掉。

MQL5 / C++
class="type">uint ff_1 = class="num">2 * class="num">4 * (iWindow + class="num">1) * iWindow;        class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix class="num">1-st feed forward layer
class="type">uint ff_2 = class="num">2 * (class="num">4 * iWindow + class="num">1) * iWindow;        class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix class="num">2-nd feed forward layer
for(class="type">uint i = class="num">0; i < iLayers; i++)
  {
    CBufferFloat *temp = NULL;
    for(class="type">int d = class="num">0; d < class="num">2; d++)
      {
       class=class="str">"cmt">//--- Initilize QKV tensor
       temp = new CBufferFloat();
       if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
       if(!temp.BufferInit(num, class="num">0))
         class="kw">return false;
       if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
       if(!QKV_Tensors.Add(temp))
         class="kw">return false;
       class=class="str">"cmt">//--- Initialize scores
       temp = new CBufferFloat();
       if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
       if(!temp.BufferInit(scores, class="num">0))
         class="kw">return false;
       if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
       if(!S_Tensors.Add(temp))
         class="kw">return false;
       class=class="str">"cmt">//--- Initialize multi-heads attention out
       temp = new CBufferFloat();
       if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
       if(!temp.BufferInit(mh_out, class="num">0))
         class="kw">return false;
       if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
       if(!AO_Tensors.Add(temp))
         class="kw">return false;
       class=class="str">"cmt">//--- Initialize attention out
       temp = new CBufferFloat();
       if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
       if(!temp.BufferInit(out, class="num">0))
         class="kw">return false;
       if(!temp.BufferCreate(OpenCL))

◍ Transformer 前向张量与 QKV 权重的初始化细节

这段逻辑跑在多层循环里,每层先给 Feed Forward 1 开一个 4*out 大小的浮点缓冲,再视是否为末层决定挂 Output/Gradient 还是继续开 out 大小的 FF2 缓冲。任何一步 BufferInit、BufferCreate 或 Add 失败都直接 return false,说明显存或 OpenCL 上下文异常时会整体放弃构建。 QKV 权重初始化值得盯一眼:缩放系数 k 取 1/sqrt(iWindow+1),每个权重值由 GenerateWeight()*2*k - k 生成,等价于把随机权重约束到 [-k, k] 区间。若你的 iWindow=63,k 约为 0.125,权重绝对值上限就被压在 0.125 附近,避免多头注意力在长窗口下数值发散。 在 MT5 里把这段接进自己的 EA 时,建议先打印 qkv_weights 和 iWindow 的实际值,确认 k 与缓冲尺寸匹配;外汇与贵金属杠杆交易高风险,模型初始化错误可能让回测静默失败,实盘前务必在策略测试器跑通再上。

MQL5 / C++
      class="kw">return false;
      if(!FF_Tensors.Add(temp))
         class="kw">return false;
      class=class="str">"cmt">//--- Initialize Feed Forward class="num">1
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
      if(!temp.BufferInit(class="num">4 * out, class="num">0))
         class="kw">return false;
      if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
      if(!FF_Tensors.Add(temp))
         class="kw">return false;
      class=class="str">"cmt">//--- Initialize Feed Forward class="num">2
      if(i == iLayers - class="num">1)
        {
         if(!FF_Tensors.Add(d == class="num">0 ? Output : Gradient))
            class="kw">return false;
         class="kw">continue;
        }
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
      if(!temp.BufferInit(out, class="num">0))
         class="kw">return false;
      if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
      if(!FF_Tensors.Add(temp))
         class="kw">return false;
      }
      class=class="str">"cmt">//--- Initilize QKV weights
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
      if(!temp.Reserve(qkv_weights))
         class="kw">return false;
      class="type">float k = (class="type">float)(class="num">1 / sqrt(iWindow + class="num">1));
      for(class="type">uint w = class="num">0; w < qkv_weights; w++)
        {
         if(!temp.Add(GenerateWeight() * class="num">2 * k - k))
            class="kw">return false;
        }
      if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
      if(!QKV_Weights.Add(temp))
         class="kw">return false;
      class=class="str">"cmt">//--- Initilize Weights0
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)

「前馈与注意力权重的显存预分配」

这段逻辑在模型初始化阶段连续三次为前馈层(FF)权重开缓冲区:第一次按 w0 长度 Reserve,第二次按 ff_1,第三次按 ff_2,每次都走 GenerateWeight()*2*k-k 做均匀初始化后推入 FF_Weights 数组。 注意第三段的 k 被重算成 1/sqrt(4*iWindow+1),和前两段用的 k 不同,意味着深层权重的初始尺度随窗口长度收缩,窗口越大初始权重越靠近 0。 最后按优化器分支循环 1 或 2 次(SGD 走 1 次,其他走 2 次),每次 new 一个 CBufferFloat 并 BufferInit 成 qkv_weights 长度的 0 缓冲,再 BufferCreate 到 OpenCL 设备。任何一步 Reserve / Add / Create 失败都直接 return false,初始化容错靠逐行短路。 在 MT5 里跑这类模型,若 ff_2 设得大(比如 iWindow=512 时 k≈0.022),权重初始值普遍落在 ±0.022 区间,可用 Print 抽样验证 BufferInit 后的实际方差。外汇与贵金属行情高波动,显存申请失败往往发生在品种切换时,实盘前务必在策略测试器用真实 tick 复现一次。

MQL5 / C++
      class="kw">return false;
      if(!temp.Reserve(w0))
         class="kw">return false;
      for(class="type">uint w = class="num">0; w < w0; w++)
        {
         if(!temp.Add(GenerateWeight() * class="num">2 * k - k))
            class="kw">return false;
        }
      if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
      if(!FF_Weights.Add(temp))
         class="kw">return false;
      class=class="str">"cmt">//--- Initilize FF Weights
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
      if(!temp.Reserve(ff_1))
         class="kw">return false;
      for(class="type">uint w = class="num">0; w < ff_1; w++)
        {
         if(!temp.Add(GenerateWeight() * class="num">2 * k - k))
            class="kw">return false;
        }
      if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
      if(!FF_Weights.Add(temp))
         class="kw">return false;
      class=class="str">"cmt">//---
      temp = new CBufferFloat();
      if(CheckPointer(temp) == POINTER_INVALID)
         class="kw">return false;
      if(!temp.Reserve(ff_2))
         class="kw">return false;
      k = (class="type">float)(class="num">1 / sqrt(class="num">4 * iWindow + class="num">1));
      for(class="type">uint w = class="num">0; w < ff_2; w++)
        {
         if(!temp.Add(GenerateWeight() * class="num">2 * k - k))
            class="kw">return false;
        }
      if(!temp.BufferCreate(OpenCL))
         class="kw">return false;
      if(!FF_Weights.Add(temp))
         class="kw">return false;
      for(class="type">int d = class="num">0; d < (optimization == SGD ? class="num">1 : class="num">2); d++)
        {
         temp = new CBufferFloat();
         if(CheckPointer(temp) == POINTER_INVALID)
            class="kw">return false;
         if(!temp.BufferInit(qkv_weights, class="num">0))
            class="kw">return false;
         if(!temp.BufferCreate(OpenCL))
            class="kw">return false;

权重缓冲与OpenCL前馈内核的落地

在 MT5 里把 Transformer 类模型的权重推到显存,靠的是 CBufferFloat 的 BufferCreate(OpenCL) 调用。上面这段初始化逻辑先给 QKV_Weights 和 FF_Weights 两个动态数组各塞了三块缓冲:QKV 合并权重、ff_1、ff_2,尺寸分别由 w0、ff_1、ff_2 三个整型变量决定,初始值填 0。 每一块 temp 在 new 出来之后都必须用 CheckPointer 判 POINTER_INVALID,否则显存申请失败会静默返回 false,模型直接起不来。BufferInit 的第二个参数 0 代表清零,不是随机初始化——这点对复现回测结果很关键。 末尾的 FeedForwardComplexConv 是个 __kernel 函数,参数里 matrix_w / matrix_i / matrix_o 都是 float2 复数指针,说明前馈卷积在频域跑;inputs、step、window_in、activation 四个 int 控制滑动窗口与激活分支。外汇与贵金属杠杆高,这类 GPU 推理若用于实盘信号,须先在策略测试器用历史 tick 验证延迟与过拟合概率。 别把 BufferCreate 当万能 OpenCL 上下文没建好时 BufferCreate 也会返回 false,但代码里只判了指针和 Init,没判 OpenCL 句柄有效性。开 MT5 跑之前,先确认终端已识别你的显卡,否则前馈内核永远进不去。

MQL5 / C++
if(!QKV_Weights.Add(temp))
   class="kw">return false;
temp = new CBufferFloat();
if(CheckPointer(temp) == POINTER_INVALID)
   class="kw">return false;
if(!temp.BufferInit(w0, class="num">0))
   class="kw">return false;
if(!temp.BufferCreate(OpenCL))
   class="kw">return false;
if(!FF_Weights.Add(temp))
   class="kw">return false;
class=class="str">"cmt">//--- Initilize FF Weights
temp = new CBufferFloat();
if(CheckPointer(temp) == POINTER_INVALID)
   class="kw">return false;
if(!temp.BufferInit(ff_1, class="num">0))
   class="kw">return false;
if(!temp.BufferCreate(OpenCL))
   class="kw">return false;
if(!FF_Weights.Add(temp))
   class="kw">return false;
temp = new CBufferFloat();
if(CheckPointer(temp) == POINTER_INVALID)
   class="kw">return false;
if(!temp.BufferInit(ff_2, class="num">0))
   class="kw">return false;
if(!temp.BufferCreate(OpenCL))
   class="kw">return false;
if(!FF_Weights.Add(temp))
   class="kw">return false;
}
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
__kernel class="type">void FeedForwardComplexConv(__global float2 *matrix_w,
                     __global float2 *matrix_i,
                     __global float2 *matrix_o,
                     class="type">int inputs,
                     class="type">int step,
                     class="type">int window_in,
                     class="type">int activation
                     )
   {

◍ 复数卷积核在GPU上的前向传播实现

这段 OpenCL 内核负责把复数权重矩阵与输入矩阵做滑动卷积,是复杂域注意力层的前向核心。每个工作项由 get_global_id(0) 和 (1) 定位批次与输出窗口,循环上界 stop 受输入长度与滑动步长 step 双重约束,避免越界读取。 卷积累加用 ComplexMul 做频域乘法,初始值取 matrix_w[shift + w_in] 的偏置项;若结果出现 NaN 或 Inf 直接清零,防止异常值在贵金属小时线回测中污染后续梯度。 激活函数按编号分支:0 为复数 tanh,1 为复数 sigmoid(用 1/(1+e^-z) 实现),2 为带 0.01 系数的复数 LeakyReLU——负部缩放精确到 0.01f,实虚部独立处理。 宿主端 ConvolutionForward 先校验四个指针与索引有效性,step 缺省回退为 window;global_work_size[0] 等于 outputs.Total()/(2*window_out),即把复数双通道压进一维缓冲。设置内核参数失败会打印错误码与行号并返回 false,开 MT5 跑自定义神经网络 EA 时可直接据此定位 OpenCL 参数绑定问题。

MQL5 / C++
class="type">size_t i = get_global_id(class="num">0);
class="type">size_t out = get_global_id(class="num">1);
class="type">size_t w_out = get_global_size(class="num">1);
class="type">int w_in = window_in;
class="type">int shift_out = w_out * i;
class="type">int shift_in = step * i;
class="type">int shift = (w_in + class="num">1) * out;
class="type">int stop = (w_in <= (inputs - shift_in) ? w_in : (inputs - shift_in));
float2 sum = matrix_w[shift + w_in];
for(class="type">int k = class="num">0; k <= stop; k ++)
    sum += ComplexMul(matrix_i[shift_in + k], matrix_w[shift + k]);
if(isnan(sum.x) || isnan(sum.y) || isinf(sum.x) || isinf(sum.y))
    sum = (float2)class="num">0;
class="kw">switch(activation)
    {
    case class="num">0:
        sum = ComplexTanh(sum);
        class="kw">break;
    case class="num">1:
        sum = ComplexDiv((float2)(class="num">1, class="num">0), (float2)(class="num">1, class="num">0) + ComplexExp(-sum));
        class="kw">break;
    case class="num">2:
        if(sum.x < class="num">0)
            sum.x *= class="num">0.01f;
        if(sum.y < class="num">0)
            sum.y *= class="num">0.01f;
        class="kw">break;
    class="kw">default:
        class="kw">break;
    }
matrix_o[out + shift_out] = sum;
}
class="type">bool CNeuronComplexMLMHAttention::ConvolutionForward(CBufferFloat *weights, CBufferFloat *inputs,
CBufferFloat *outputs, class="type">uint window, class="type">uint window_out,
ENUM_ACTIVATION activ, class="type">uint step = class="num">0)
  {
  if(CheckPointer(OpenCL) == POINTER_INVALID || CheckPointer(weights) == POINTER_INVALID ||
      CheckPointer(inputs) == POINTER_INVALID || CheckPointer(outputs) == POINTER_INVALID)
    class="kw">return false;
  if(weights.GetIndex() < class="num">0)
    class="kw">return false;
  if(inputs.GetIndex() < class="num">0)
    class="kw">return false;
  if(outputs.GetIndex() < class="num">0)
    class="kw">return false;
  if(step == class="num">0)
    step = window;
  class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0};
  class="type">uint global_work_size[class="num">2];
  global_work_size[class="num">0] = outputs.Total() / (class="num">2 * window_out);
  global_work_size[class="num">1] = window_out;
  if(!OpenCL.SetArgumentBuffer(def_k_FeedForwardComplexConv, def_k_ffc_matrix_w, weights.GetIndex()))
    {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
    }
  if(!OpenCL.SetArgumentBuffer(def_k_FeedForwardComplexConv, def_k_ffc_matrix_i, inputs.GetIndex()))
    {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);

「卷积核参数绑定与多头注意力入口」

这段 OpenCL 封装把复数卷积前向传播的最后几个内核参数一次性塞进 GPU。先绑定输出缓冲 outputs.GetIndex(),再把输入长度按实部/虚部对半切(inputs.Total()/2)传给 def_k_ffc_inputs,步长 step 与输入窗 window、输出窗 window_out 紧随其后,激活类型 activ 占用 def_k_ffc_activation-1 这个槽位。 任何一个 SetArgument 失败都会用 printf 打出函数名、错误码和行号然后返回 false,方便在 MT5 Experts 日志里直接定位是哪一行参数没挂上。 OpenCL.Execute 用二维全局工作项(offset 与 size 由调用方算好)启动内核,失败时通过 CLGetInfoString 取上下文错误描述再打印,比单纯错误码更易读。 紧随其后的 __kernel void ComplexMHAttentionScore 是多头注意力分数核的入口,入参为复数张量 qkv、输出 score、维度 dimension 与掩码 mask;核内用 get_global_id(0/1) 拿查询序号 q 和头序号 hget_global_size(0) 给出总单元数,后续缩放点积倾向在这两个维度上并行。 在 MT5 里跑这套时,外汇与贵金属行情的高波动可能让窗口参数失配,建议先以小周期离线回测核对 windowwindow_out 的边界。

MQL5 / C++
  class="kw">return false;
  }
  if(!OpenCL.SetArgumentBuffer(def_k_FeedForwardComplexConv, def_k_ffc_matrix_o, outputs.GetIndex()))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_inputs, (class="type">int)(inputs.Total() / class="num">2)))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_step, (class="type">int)step))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_window_in, (class="type">int)window))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffс_window_out, (class="type">int)window_out))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_activation - class="num">1, (class="type">int)activ))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return false;
  }
  if(!OpenCL.Execute(def_k_FeedForwardComplexConv, class="num">2, global_work_offset, global_work_size))
  {
    class="type">class="kw">string error;
    CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error);
    printf("Error of execution kernel %s: %s", __FUNCSIG__, error);
    class="kw">return false;
  }
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void ComplexMHAttentionScore(__global float2 *qkv,
                                     __global float2 *score,
                                     class="type">int dimension,
                                     class="type">int mask
                                    )
  {
  class="type">int q = get_global_id(class="num">0);
  class="type">int h = get_global_id(class="num">1);
  class="type">int units = get_global_size(class="num">0);
把双域诊断交给小布盯盘
这些频域能量权重与时域残差的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到各周期分量的实时占比,你只管判断策略切换时机。

常见问题

普通 DFT 会把已知数据段的频谱当整段序列频谱,跨周期时错位明显;扩展 DFT 把原始离散频谱和完整时间序列对齐,特定频率表示更准,预测偏差倾向更小。
不同品种或周期频道全局形态不同,混频谱会互相干扰;频道无关制程各自处理单变量序列,防止频谱混杂,模型在外汇贵金属多周期上更稳。
谐波序列成群出现含大量信息难提取,CSA 从多频率响应组合收信息,有效引出对频域表示的关注,缓解特征抽取挑战。
目前小布内置的是双域分量诊断与权重可视化,完整模型训练需自行部署;但看盘口时它能提示时域/频域主导切换,省去手动算频谱。
按原始数据周期性等级算主谐波序列能量,动态生成两域模块权重,周期形态不同则侧重不同域,处理混合周期更顺。