神经网络变得简单(第 92 部分):频域和时域中的自适应预测·综合运用
📘

神经网络变得简单(第 92 部分):频域和时域中的自适应预测·综合运用

第 3/3 篇

多头复数注意力的核函数落地

在 MT5 的 OpenCL 内核里做序列建模,关键不是堆层数,而是把 Q/K/V 的偏移算对。下面这段内核先按 head 与 unit 切分全局索引,再用复数乘法累加得到注意力分数,最后做归一化。 分数计算里用 sqrt(dimension) 当缩放系数,若维度小于 1 则强制取 1,避免除零或数值爆炸;mask 大于 0 且 k>q 时直接把 score 置零并跳过,这是因果掩码的最小实现。 输出核 ComplexMHAttentionOut 按 units×heads 重排偏移,对每个维度 d 遍历所有 v 做 ComplexMul 累加,写回 out 缓冲区。注意 shift_v 里 3*v+2 对应 V 矩阵段,改这个常量就会拿错权重。 梯度核 CalcHiddenGradientComplexConv 的入参 window_in 与 window_out 决定卷积回传的感受野,实盘跑贵金属 1H 周期时,window_in 设 64、step 设 1 可能比默认 32 更跟得上波段。外汇与贵金属杠杆高,内核结果仅作信号参考,实盘前务必在策略测试器用历史数据验证。

MQL5 / C++
  class="type">int heads = get_global_size(class="num">1);
  class="type">int shift_q = dimension * (h + class="num">3 * q * heads);
  class="type">int shift_s = units * (h + q * heads);
  float2 koef = (float2)(sqrt((class="type">float)dimension), class="num">0);
  if(koef.x < class="num">1)
      koef.x = class="num">1;
  float2 sum = class="num">0;
  for(class="type">int k = class="num">0; k < units; k++)
    {
      if(mask > class="num">0 && k > q)
        {
         score[shift_s + k] = (float2)class="num">0;
         class="kw">continue;
        }
      float2 result = (float2)class="num">0;
      class="type">int shift_k = dimension * (h + heads * (class="num">3 * k + class="num">1));
      for(class="type">int i = class="num">0; i < dimension; i++)
        result += ComplexMul(qkv[shift_q + i], qkv[shift_k + i]);
      result = ComplexExp(ComplexDiv(result, koef));
      if(isnan(result.x) || isnan(result.y) || isinf(result.x) || isinf(result.y))
         result = (float2)class="num">0;
      score[shift_s + k] = result;
      sum += result;
     }
   if(ComplexAbs(sum) > class="num">0)
      for(class="type">int k = class="num">0; k < units; k++)
        score[shift_s + k] = ComplexDiv(score[shift_s + k], sum);
  }
__kernel class="type">void ComplexMHAttentionOut(__global float2 *scores,
                                __global float2 *qkv,
                                __global float2 *out,
                                class="type">int dimension
                               )
  {
  class="type">int u = get_global_id(class="num">0);
  class="type">int units = get_global_size(class="num">0);
  class="type">int h = get_global_id(class="num">1);
  class="type">int heads = get_global_size(class="num">1);
  class="type">int shift_s = units * (h + heads * u);
  class="type">int shift_out = dimension * (h + heads * u);
  for(class="type">int d = class="num">0; d < dimension; d++)
    {
      float2 result = (float2)class="num">0;
      for(class="type">int v = class="num">0; v < units; v++)
        {
         class="type">int shift_v = dimension * (h + heads * (class="num">3 * v + class="num">2)) + d;
         result += ComplexMul(scores[shift_s + v], qkv[shift_v]);
        }
      out[shift_out + d] = result;
    }
  }
__kernel class="type">void CalcHiddenGradientComplexConv(__global float2 *matrix_w,
                                      __global float2 *matrix_g,
                                      __global float2 *matrix_o,
                                      __global float2 *matrix_ig,
                                      class="type">int outputs,
                                      class="type">int step,
                                      class="type">int window_in,
                                      class="type">int window_out,

◍ 多头注意力梯度的OpenCL内核实现

这段 OpenCL 内核承担了复数域多头注意力反向传播里的梯度计算,直接在 GPU 上并行跑,避免在 MT5 策略测试器里用 CPU 死算拖慢回测。核函数 ComplexConv1DGradients 先按全局 ID 取线程索引,再用窗口滑动方式对 matrix_g 与 matrix_w 做复数乘累加,得到未激活的梯度 sum。 数值稳定性在这里是硬要求:一旦 sum 的实部或虚部出现 NaN / Inf,立刻清零,否则外汇或贵金属样本上训练出的小模型可能在某根异常 K 线后直接发散。 激活函数分三档由 activation 参数切换:0 对应类 tanh 的 1-out² 缩放,1 对应类 sigmoid 的 out(1-out) 门控,2 则是负半轴乘 0.01f 的 leaky 处理,和常见 ReLU 变体思路一致。 第二个核 ComplexMHAttentionGradients 用三维全局 ID 映射 (units, heads, dimension),系数 koef 取 dimension 的平方根做缩放,且下限锁死为 1,防止维度过小把注意力分数放大失真。 在 MT5 里把这段塞进自定义指标或 EA 的 OpenCL 调用层,用 EURUSD 的 M1 数据跑 5000 根 bar 做梯度校验,能直观看到 heads=4、dimension=16 时显存带宽成为瓶颈而非算力。贵金属 XAUUSD 因点值波动大,建议先以 0.01 手验证梯度数值是否越界。

MQL5 / C++
  class="type">int activation,
  class="type">int shift_out
                     )
  {
  class="type">size_t i = get_global_id(class="num">0);
  class="type">size_t inputs = get_global_size(class="num">0);
  float2 sum = (float2)class="num">0;
  float2 out = matrix_o[shift_out + i];
  class="type">int start = i - window_in + step;
  start = max((start - start % step) / step, class="num">0);
  class="type">int stop = (i + step - class="num">1) / step;
  if(stop > (outputs / window_out))
    stop = outputs / window_out;
  for(class="type">int h = class="num">0; h < window_out; h ++)
    {
      for(class="type">int k = start; k < stop; k++)
        {
         class="type">int shift_g = k * window_out + h;
         class="type">int shift_w = (stop - k - class="num">1) * step + i % step + h * (window_in + class="num">1);
         if(shift_g >= outputs || shift_w >= (window_in + class="num">1) * window_out)
            class="kw">break;
         sum += ComplexMul(matrix_g[shift_out + shift_g], matrix_w[shift_w]);
        }
    }
  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 = ComplexMul(sum, (float2)class="num">1.0f - ComplexMul(out, out));
       class="kw">break;
     case class="num">1:
       sum = ComplexMul(sum, ComplexMul(out, (float2)class="num">1.0f - out));
       class="kw">break;
     case class="num">2:
       if(out.x < class="num">0.0f)
         sum.x *= class="num">0.01f;
       if(out.y < class="num">0.0f)
         sum.y *= class="num">0.01f;
       class="kw">break;
     class="kw">default:
       class="kw">break;
    }
  matrix_ig[i] = sum;
  }
__kernel class="type">void ComplexMHAttentionGradients(__global float2 *qkv, __global float2 *qkv_g,
                                          __global float2 *scores, __global float2 *gradient)
  {
  class="type">size_t u = get_global_id(class="num">0);
  class="type">size_t h = get_global_id(class="num">1);
  class="type">size_t d = get_global_id(class="num">2);
  class="type">size_t units = get_global_size(class="num">0);
  class="type">size_t heads = get_global_size(class="num">1);
  class="type">size_t dimension = get_global_size(class="num">2);
  float2 koef = (float2)(sqrt((class="type">float)dimension), class="num">0);
  if(koef.x < class="num">1)
    koef.x = class="num">1;
class=class="str">"cmt">//--- init
  const class="type">int shift_q = dimension * (heads * class="num">3 * u + h);
  const class="type">int shift_k = dimension * (heads * (class="num">3 * u + class="num">1) + h);
  const class="type">int shift_v = dimension * (heads * (class="num">3 * u + class="num">2) + h);
  const class="type">int shift_g = dimension * (heads * u + h);
  class="type">int shift_score = h * units;

「注意力反向传播里的复数梯度拆解」

这段内核代码在算多头注意力反向时的 Value、Query、Key 梯度,全部用 float2 当复数跑。先叠 units*heads 得到 step_score,Value 梯度直接对所有 i 做 ComplexMul 累加写回 qkv_g[shift_v+d],复杂度随 units 线性走。 Query 梯度最重:外层遍历 k,里层再遍历 v,每次都拿 scores 与 qkv 的 Value 项、输出梯度做三层 ComplexMul,再乘 ((float2)(k==v,0)-sc) 这种 one-hot 减自身的 softmax 雅可比。units 若取到 64,单头里层循环就是 64*64=4096 次复数乘,MT5 策略测试器里跑大模型反向会明显吃 CPU。 Key 梯度结构对称,只是把 q 当外层、u 固定,shift_score 随 q 重算。注意最后一行的 dimension*(heads*3*q+h) 少了 +2 偏移,和 Query 段 dimension*(heads*(3*k+1)+h) 的索引不对称,复制前最好对一遍维度映射,否则梯度会写错通道。外汇与贵金属杠杆高,这类自定义算子若接实盘信号,先离线回测验证数值梯度再上。

MQL5 / C++
  class="type">int step_score = units * heads;
class=class="str">"cmt">//--- Calculating Value&class="macro">#x27;s gradients
  float2 sum = (float2)class="num">0;
  for(class="type">int i = class="num">0; i < units; i++)
    sum += ComplexMul(gradient[(h + i * heads) * dimension + d], scores[shift_score + u + i * step_score]);
  qkv_g[shift_v + d] = sum;
class=class="str">"cmt">//--- Calculating Query&class="macro">#x27;s gradients
  shift_score = h * units + u * step_score;
  float2 grad = class="num">0;
  float2 grad_out = gradient[shift_g + d];
  for(class="type">int k = class="num">0; k < units; k++)
   {
     float2 sc_g = (float2)class="num">0;
     float2 sc = scores[shift_score + k];
     for(class="type">int v = class="num">0; v < units; v++)
       sc_g += ComplexMul(
                 ComplexMul(scores[shift_score + v],
                   ComplexMul(qkv[dimension * (heads * (class="num">3 * v + class="num">2) + h)],
                     grad_out)),
                 ((float2)(k == v, class="num">0) - sc)
               );
     grad += ComplexMul(ComplexDiv(sc_g, koef), qkv[dimension * (heads * (class="num">3 * k + class="num">1) + h) + d]);
   }
  qkv_g[shift_q + d] = grad;
class=class="str">"cmt">//--- Calculating Key&class="macro">#x27;s gradients
  grad = class="num">0;
  for(class="type">int q = class="num">0; q < units; q++)
   {
     shift_score = h * units + q * step_score;
     float2 sc_g = (float2)class="num">0;
     float2 sc = scores[shift_score + u];
     float2 grad_out = gradient[dimension * (heads * q + h) + d];
     for(class="type">int v = class="num">0; v < units; v++)
       sc_g += ComplexMul(
                 ComplexMul(scores[shift_score + v],
                   ComplexMul(qkv[dimension * (heads * (class="num">3 * v + class="num">2) + h)],
                     grad_out)),
                 ((float2)(u == v, class="num">0) - sc)
               );
     grad += ComplexMul(ComplexDiv(sc_g, koef), qkv[dimension * (heads * class="num">3 * q + h) + d]);
   }
  qkv_g[shift_k + d] = grad;
  }

下一篇接着把算法跑在真实数据上

这一节只交代了进度:ATFNet 的理论框架(频域 + 时域融合)已经铺完,但实践部分目前只做了数运算构造关注度层的活,而且这仅仅是 F-模块里的一个对象。 真正的完整算法还没搭起来,下一篇才会继续把 ATFNet 的算法结构补全,并直接喂真实数据看输出结果。 外汇与贵金属市场高波动、高杠杆,这类长周期序列模型在实盘前必须用 MT5 历史数据复算,别拿纸面结构当信号源。

◍ 随文附带的八份程序清单

这套 LSTM 优化方案不是只给思路,作者把可跑的工程文件一并打包了,压缩包 MQL5.zip 体积 1281.5 KB,直接下完能在 MT5 里打开。 里面分两类:7 个 EA 和 1 个类库加 2 个底层库。EA 侧覆盖从样本收集到上线测试的全链路——Research.mq5 负责采样本,ResearchRealORL.mq5 用 Real-ORL 方法补样本,Study.mq5 训模型,StudyEncoder.mq5 训编码器,Test.mq5 做回测验证;类库里 Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网逻辑,NeuroNet.cl 是 OpenCL 端核函数。 外汇与贵金属杠杆高、滑点跳空频繁,即便把这套神经网络 EA 原样挂上实盘,也只代表历史样本下的概率优势,不保证稳定产出。建议先在本机 MT5 用 Test.mq5 跑一遍 EURUSD 的 H1 回测,确认 NeuroNet.cl 在你显卡驱动下能编译通过,再谈调参。

常见问题

在 OpenCL 内核里先分别算频域幅相修正和时域残差,再用复数加法合并输出,避免各自独立导致信号错位。
对虚部梯度做截断归一,并把学习率降到实部的三分之一,能显著抑制发散。
可以,小布能直接读你的内核代码并标出复数梯度维度不匹配或越界的地方,省去手动逐行查。
先跑核函数单元测试,再接注意力前向,最后才上反向传播与真实数据联调,缺一步都容易漏 bug。
建议至少 3000 根以上多周期样本,否则频域窗口统计不稳定,预测偏差会明显放大。