English Русский Español Português
preview
交易中的神经网络:自适应周期分块(Token生成)

交易中的神经网络:自适应周期分块(Token生成)

MetaTrader 5 — 交易系统 |
23 0
Dmitriy Gizlyk
Dmitriy Gizlyk

引言

在前一篇文章中,我们详细探讨了LightGTS(轻量级通用时间序列预测) 的理论基础。该框架出自论文《LightGTS: A Lightweight General Time Series Forecasting Model》,是当下最先进、工程实现完善的时序预测方案之一。其设计思想建立在对金融与经济数据周期性本质的深度理解之上,同时针对时序结构处理任务,对Transformer架构进行了精心重构。我们重点介绍了LightGTS如何提取周期模式,在降低训练开销的同时,即便面对异构、含噪声的数据,依然能够保证良好的泛化能力。

该框架的起点是周期分块(Period Patching)机制:将时间序列切分为与信号固有频率匹配的片段。这一频率不是人工指定的,而是模型基于快速傅里叶变换得到的频谱分析结果自动确定。每一个提取出来的分块都对应一个周期片段,其中包含重要的局部模式。正是这一段片段,通过投影转换为token。这里体现出第一个架构层面的创新:柔性投影层(Flex Projection Layer),借助权重的柔性线性变换,实现对不等长分块的处理。该投影并非简单对数据做缩放,它保证token在不同尺度与频率之间转换时表征的一致性。

编码器模块采用旋转位置编码(RoPE),为token之间的相对位置提供紧凑且稳定的表征。对于金融时序而言,序列内部元素的相对位置往往远比绝对位置重要,这一点尤为关键。随后将token送入经典堆叠的Transformer模块,每个模块均包含多头自注意力模块与前馈模块。

LightGTS作者提出的最具原创性方案之一是周期并行解码 机制,这一机制在概念上与自回归策略正好相反。模型不再逐步预测序列;而是利用隐藏表征的最后一个token(该token封装了全部历史序列信息),基于此复制并施加位置权重,一次性生成完整输出序列。该方法不仅加快预测速度,还能保证模型输出时序结构上的周期一致性。

最后,框架对解码器投影层执行柔性尺寸调整 (Flex-resize),从而使预测结果长度与待预测信号的实际长度保持一致。模型训练的本质,就是最小化预测值与目标序列之间经典的均方误差(MSE)损失函数。

由此可见,LightGTS并非只是一个经过修改的Transformer。它是一套精心设计的架构,每一个组件都针对时间序列的独有特性定制:从周期特征处理到摒弃自回归,转向全并行生成。正是这种深度适配,让该框架能够在较低计算开销下实现较高预测精度。

LightGTS框架作者的可视化图展示如下。

LightGTS框架作者的可视化图

在前一篇文章的实战章节,我们着手开发自适应周期分块算法,它是LightGTS架构的核心组件之一。文中详细分析了MQL5与OpenCL典型运行环境下无法动态分配内存带来的限制。该约束使得我们不能采用输出token数量任意可变的方案。

为此,我们做出了一个具有策略性的决定:固定分块总数,利用分块重叠来补偿单个片段长度的变化。这样一来,我们在自适应性(模型依然对时序真实周期保持敏感)与计算稳定性之间取得平衡,保障硬件资源高效利用。该方法既能忠实保留待分析数据的周期结构,又能实现内存管理的可预测性,对于高频交易模型以及资源受限环境下的部署至关重要。

我们已经实现主导频率选择算法:该算法可以有效提取输入单变量时序中的主周期分量。以此为基础,确定后续数据分段的基准尺度。今天,我们将继续推进,完成下一步工作:在OpenCL环境下实现token生成算法。

我们的目标是将每一条时间序列切分为固定数量的片段,片段尺寸根据识别出的主导频率确定,而重叠量用来适配窗口长度。全部计算必须以纯并行方式执行,使用GPU兼容代码;每个工作项(线程)负责为待分析序列的某个单变量分量生成一个token。


构建OpenCL内核

通过快速傅里叶变换识别主导频率后,就来到关键阶段:构建token生成机制,也就是生成匹配识别周期的时序片段。回顾上一节的核心任务:让模型适配当前市场频率,同时满足输出分块数量固定的要求。这一点在MQL5环境中尤为重要,因为MQL5不支持动态内存分配。

我们方案的核心原则:token(分块)总数保持不变,分块长度随当前频率动态调整,通过调节重叠量保证完整覆盖待分析时序。这样就兼顾了灵活性与资源管控。然而,这里存在一个技术难点:若卷积窗口发生变化,权重矩阵理论上也必须随之适配 —— 要么每次重建权重矩阵,要么投影到新空间。

在原始论文中,LightGTS框架作者的做法是:权重矩阵先在固定窗口尺寸下、基于训练样本的统计量完成训练;之后针对每一个新窗口尺寸,利用摩尔 - 彭若斯伪逆(Moore–Penrose pseudoinverse)对权重做投影。该方法在数学上十分优雅,但实际实现较为繁琐。

真实的金融市场瞬息万变:今天的周期未必是明天的周期。周期会发生漂移(drifts),模型必须具备灵活适配能力。在线推理或高频交易场景下实时计算伪逆,意味着为满足数学形式严谨性牺牲运算速度与资源效率。这种开销在工程上是无法承受的。

我们提出的方案实用性更强。我们不再持续重建权重,而是采用另一种思路:使用最大允许尺寸的权重矩阵,通过零填充(zero padding)使活动窗口外的数据位置对结果贡献为零,从而让对应权重实际上不起作用。换言之,如果某段数据不在有效窗口范围内,则与0相乘,对应的权重也就不再影响结果。这一做法简单而高效。

该方法可以实现:

  • 维持权重矩阵固定结构,避开高开销运算;
  • 动态调整窗口尺寸与分块之间的步长;
  • 实时自适应市场频率波动,无需停止模型或重新计算参数。

在OpenCL环境中,全部由FeedForwardAdaptConv内核实现,每个工作项负责处理某一单变量序列对应的特定 片段 - 滤波器组合。

__kernel void FeedForwardAdaptConv(__global const float *matrix_w,
                                   __global const float *matrix_i,
                                   __global float *matrix_o,
                                   __global const float *main_freq,
                                   const int inputs,
                                   const int window_in,
                                   const int activation
                                  )
  {
   const size_t u = get_global_id(0);
   const size_t f = get_global_id(1);
   const size_t v = get_global_id(2);
   const size_t units = get_global_size(0);
   const size_t filters = get_global_size(1);
   const size_t variables = get_global_size(2);

在内核主体中,我们首先确定三维任务空间内各个工作项的索引。随后,借助单变量序列标识v,从全局缓冲区main_freq中读取当前待分析变量的主导频率。

const int freq = main_freq[v];
int window = (inputs / variables + freq - 1) / freq;

据此计算窗口大小,并结合步长与分段边界做修正。接下来,我们依据输入张量的尺寸和待生成的token数量,来确定分析窗口的步长。目标是均匀覆盖整条序列,不出现遗漏。

const int step = (int)(inputs / variables + units + 1) / (units + 2);
if(window < step)
   window = (int)((step + window - 1) / window) * window;
if(window > window_in)
   window = window_in;

接下来是关键点。窗口长度不能小于步长,否则部分数据会无法被覆盖。因此,如果检测得到的周期过小,我们会按倍数放大窗口尺寸,使其至少等于步长,同时不超过最大允许值。

随后,确定输入数组、输出数组以及权重矩阵中的偏移量。这一步是为了对全局缓冲区中的数据进行正确寻址。

const int shift_in = (u < (units - 1) ? u * step : inputs / variables - window);
const int shift_in_var =  v * inputs / variables;
const int shift_out = (u + v * units) * filters + f;
const int shift_weight = (v * filters + f) * (window_in + 1);

这里有一个值得留意的重要细节:我们需要完整覆盖整条输入序列,包括序列尾部。如果采用固定步长执行分段,且每个窗口的长度是动态确定的,那么末尾的若干数据点可能会被遗漏,无法参与计算。因此,为保证完整覆盖整条输入序列,我们将最后一个分段的起始位置设置为待分析序列总长度减去窗口大小。这样就能对最后一个窗口的偏移做出调整,确保它一定覆盖序列末尾的数据点,哪怕动态算出的窗口尺寸小于最大允许值,也不会遗漏。

接下来进入核心计算环节 —— 卷积运算,在该过程中计算每个token的最终值。第一步,读取偏置项对应的基准值,该值通过预先算好的偏移量从权重矩阵中取出。此元素作为累加基准,用来汇总分析窗口内每个数据点带来的贡献。

float sum = matrix_w[shift_weight + window_in];
for(int i = 0; i < window; i++)
   if((shift_in + i) < (inputs / variables))
      sum += IsNaNOrInf(matrix_i[shift_in_var + shift_in + i], 0) *
             matrix_w[shift_weight + i];

接下来,我们开始遍历分析窗口内的每一个元素。这里就用到了本实现方案的核心特性 —— 零填充策略。如果序列中的某个元素落在实际计算窗口之外,则直接将其排除在计算之外。这样可以避免无关数据混入计算而扭曲信号。该技术能够保证计算结果稳定,无论当前窗口尺寸如何,都可以完成精确运算。除此之外,零填充便于维持权重矩阵维度固定:所有空位都填充零值,不会对最终求和结果产生影响。

最后,调用选定的激活函数,并将结果存入输出缓冲区。

 matrix_o[shift_out] = Activation(sum, activation);
}

最终,我们得到适配当前市场频率的嵌入向量,具备清晰的局部上下文信息,可直接送入Transformer模块。整套方案无需高开销运算,资源占用极低,完全适配MQL5环境的各项约束条件。

在将生成的token送入一连串Transformer模块之前,我们必须保证模型不会在初始阶段就失效,且具备学习能力。这就需要完整的反向传播(backpropagation)流程 —— 在该阶段计算梯度,以此对自适应卷积滤波器进行参数调整。一旦缺少反向传播,模型就学不到东西:滤波器会困在随机初始状态,模型无法适应新的市场冲击,持续固化在错误的参数中。

执行反向传播,就像给管弦乐团调音:某件乐器走了调,就要把偏差反馈回去,给出精准的修正指引。在可变窗口自适应卷积场景下,这件事并不简单:原始数据中的单个值可能同时参与多个token的计算,必须把误差反向分发回对应的原始数据源。

正是为了解决这个问题,我们开发了OpenCL内核CalcHiddenGradientAdaptConv。该内核在二维任务空间中运行:inp轴对应原始单变量时序的各个位置,v轴对应不同的通道(即各条单变量序列)该方法确保待分析数据的每一个元素都能分配到准确的梯度。

__kernel void CalcHiddenGradientAdaptConv(__global const float *matrix_w,
                                          __global const float *matrix_i,
                                          __global float *matrix_ig,
                                          __global const float *matrix_og,
                                          __global const float *main_freq,
                                          const int outputs,
                                          const int window_in,
                                          const int window_out,
                                          const int activation
                                         )
  {
   const size_t inp = get_global_id(0);
   const size_t v = get_global_id(1);
   const size_t inputs = get_global_size(0);
   const size_t variables = get_global_size(1);

在内核内部,首先在任务空间的各个维度上确定当前工作项的位置。随后,和前向传播内核中的逻辑一样,为每一条单变量时序单独计算分段尺寸与对应步长。

const int units = outputs / (window_out * variables);
const int freq = main_freq[v];
int window = (inputs / variables + freq - 1) / freq;
const int step = (int)(inputs + units + 1) / (units + 2);
if(window < step)
   window = (int)((step + window - 1) / window) * window;
if(window > window_in)
   window = window_in;

接下来,计算数据缓冲区中目标元素对应的偏移量。

const int shift_in = v * inputs + inp;
int u = inp / step;
int shift_out_var = v * (outputs / variables);
int shift_weight_var = (v * window_out) * (window_in + 1);

之后进入误差梯度分发的核心流程。首先确定源数据缓冲区中的当前元素参与生成了哪一个token,随后汇总该输出token所有元素带来的误差梯度,并计入当前待分析元素的贡献权重。

然而,需要注意的是,由于分段之间存在重叠,源数据中的单个元素可能会参与多个不同位置token的生成。因此,我们将误差梯度归集操作放置在循环内执行。

float sum = 0;
while(u * step <= inp && u < (units - 1))
  {
   int pos = inp - u * step;
   if(pos >= window)
     {
      u++;
      continue;
     }
   int shift_out = u * window_out;
   int shift_weight = pos + shift_weight_var;
   for(int out = 0; out < window_out; out++)
     {
      if((shift_out + out) >= (outputs / variables))
         continue;
      sum += IsNaNOrInf(matrix_og[shift_out_var + shift_out + out] *
                        matrix_w[shift_weight + out * (window_in + 1)], 0);
     }
   u++;
  }

我们同样需要留意最后一个分段的生成逻辑。在误差梯度分发算法中,我们会用独立的代码块对其进行处理。

if(inp >= (inputs - window))
  {
   int pos = inp + window - inputs;
   int shift_out = (units - 1) * window_out;
   int shift_weight = pos + shift_weight_var;
   for(int out = 0; out < window_out; out++)
     {
      if((shift_out + out) >= (outputs / variables))
         continue;
      sum += IsNaNOrInf(matrix_og[shift_out_var + shift_out + out] *
                        matrix_w[shift_weight + out * (window_in + 1)], 0);
     }
  }

最后一步:利用激活函数的导数对累加得到的误差梯度总和做修正,然后存入全局数据缓冲区对应的元素位置。

 matrix_ig[shift_in] = Deactivation(sum, matrix_i[shift_in], activation);
}

这种遍历所有token的实现方式,可以保证输入数据的每一个数据点都能获得对应的误差梯度,即便该数据点同时对多个token产生贡献。这使得我们的自适应滤波器能够基于整批市场数据进行学习,而不是仅依靠孤立的片段。

然而,误差梯度分发只完成了一半工作。真正关键的环节在后面,我们利用这些梯度更新模型参数。可以类比园丁:收获结束后,他判断哪些树木需要修剪、哪些需要施肥,让枝条在下一季结出更多果实。同理,我们的优化算法会使用计算得到的梯度调整权重,从而最小化整体预测误差。

在此情况下,模型参数更新逻辑在OpenCL内核UpdateWeightsAdaptConvAdam内部实现。这不仅仅是一个技术步骤,更是整个反向传播流程的收尾环节 —— 模型在此处从误差中学习,并向着优化目标迭代。

__kernel void UpdateWeightsAdaptConvAdam(__global float *matrix_w,
                                         __global const float *matrix_og,
                                         __global const float *matrix_i,
                                         __global float *matrix_m,
                                         __global float *matrix_v,
                                         __global float *main_freq,
                                         const int inputs,
                                         const int outputs,
                                         const float l,
                                         const float b1,
                                         const float b2
                                        )
  {
   const size_t id_in = get_global_id(0);    // input shift
   const size_t id_out = get_global_id(1);   // filter shift
   const size_t id_v = get_global_id(2);     // variable
   const size_t window_in = get_global_size(0) - 1;
   const size_t window_out = get_global_size(1);
   const size_t variables = get_global_size(2);

该内核的任务空间被构建为包含三个坐标轴的多级阶段,每个坐标轴在计算流程中都有明确的作用。第一维是输入数据分析窗口(分段)内的位置(id_in);第二维是滤波器编号,即该对象输出token中的一个特定位置(id_out);第三维是单变量时序索引(id_v),代表一路独立的输入数据通道。

这种三维划分不只是图架构上的方便,更是有意的策略安排。它实现了任务的完全解耦:权重矩阵中每一个可训练参数,都严格作用于输入序列对应的分段上下文,并绑定指定滤波器与通道。可以把每个滤波器想象成一名独立分析师:各看各的图表,互不干扰。这样能够防止不同时序之间的信号混杂,避免跨序列失真。这一点在金融数据场景下尤为关键,因为噪声与不稳定性是金融数据的常态。

这就像一台配有多枚独立物镜的显微镜:每个滤波器只盯着自己的那段数据,即便信号波动极其微弱,也能做出高精度的参数调整。如果某一个通道表现为剧烈快速的震荡,而另一个通道呈现平稳缓慢的趋势,算法也能分别对待这两类通道,而不会丢失细节。事实上,每个权重都会针对自身的局部任务单独训练,就像大系统内部的一个独立模型。

在内核主体中,我们首先在三维任务空间定位当前工作项,以此选出可训练参数矩阵中的单个元素用于后续运算。

紧接着,基于主导频率main_freq[id_v],我们计算该权重所作用的当前分段尺寸window。

const int units = outputs / (window_out * variables);
const int freq = main_freq[id_v];
int window = (inputs / variables + freq - 1) / freq;
const int step = (int)(inputs / variables + units + 1) / (units + 2);
if(window < step)
   window = (int)((step + window - 1) / window) * window;
if(window > window_in)
   window = window_in;

该方法可以确保:

  • 参数隔离 —— 每个权重仅作用于与其绑定的输入数据;
  • 全并行 —— 各个工作项之间互不干扰,因为它们操作权重矩阵中不同的元素;
  • 高精度 —— 滤波器与数据频率保持同步,仅处理相关数据段。

我们不能忘记,所有计算都在通用框架内执行:该权重矩阵是按数据最大分析窗口设计的。但在实际运算中,每个独立数据段的大小往往小于这个最大值。为避免在无用计算上消耗宝贵的GPU算力与资源,每个工作项在最开始就会检查参数是否属于当前数据段,并直接终止不需要的工作项。

if(id_in != window_in &&
   id_in >= window)
   return;

这样一来,整体性能大幅提升,模型运行速度显著加快,就像目标明确的赛车手,直接舍弃所有多余弯道,选择通往终点的最短路径。

下一步是计算全局数据缓冲区中的偏移量,缺少这一步,任何工作项都无法定位所需的数据元素。

const int shift_in_var = id_v * inputs / variables;
const int shift_out_var = id_v * outputs / variables;
const int shift_weight = (id_v * window_out + id_out) *
                         (window_in + 1) + id_in;
const bool bias = (id_in == window_in);

这项简单却至关重要的技术,保证每个滤波器参数在单位时间内只处理属于自己的那部分数据,从而提升整个模型的运算效率与可预测性。

接下来,我们进入最核心环节之一 —— 计算所选参数的误差梯度。梯度并非抽象数值,它就像一座真实的灯塔,告诉我们权重该往哪个方向、调整多少,才能让模型得到更精准的预测结果。

为得到当前待分析参数的真实贡献,我们遍历整条单变量序列,并累加该参数在各个输出token上产生的全部响应。

float grad = 0;
for(int u = 0; u < (units - 1); u++)
  {
   const int shift_in_loc = id_in + u * step;
   if(shift_in_loc >= (inputs / variables))
      continue;
   float inp = (bias ? 1 : IsNaNOrInf(matrix_i[shift_in_var + shift_in_loc], 0));
   grad += IsNaNOrInf(inp * matrix_og[shift_out_var + u * window_out + id_out], 0);
  }

不要忽略最后一个数据段的构造细节。我们会在单独的代码块中对其进行处理。

{
 const int shift_in_loc = id_in + inputs / variables - window;
 if(shift_in_loc < (inputs / variables))
   {
    float inp = (bias ? 1 : IsNaNOrInf(matrix_i[shift_in_var + shift_in_loc], 0));
    grad += IsNaNOrInf(inp * matrix_og[shift_out_var + (units - 1) * window_out + id_out], 0);
   }
}

这种梯度收集方法完备且全面。我们不会遗漏输入数据中的每一处响应,最终得到用于权重调整的最精准方向向量。正是这种细致的反向追踪机制,保证模型不会困在局部误差里,而是能够稳定、平滑地适应不断变化的金融市场节奏。

接下来登场的是Adam优化算法:它综合了动量法(Momentum)的梯度平滑与RMSProp的自适应步长优势。它用到两个辅助数组:matrix_m存储一阶矩(梯度的累积),matrix_v存储二阶矩(梯度平方的累积)。

float mt = IsNaNOrInf(clamp(b1 * matrix_m[shift_weight] + (1 - b1) * grad, -1.0e5f, 1.0e5f), 0);
float vt = IsNaNOrInf(clamp(b2 * matrix_v[shift_weight] + (1 - b2) * pow(grad, 2), 1.0e-6f, 1.0e6f), 1.0e-6f);
float weight = clamp(matrix_w[shift_weight] + IsNaNOrInf(l * mt / sqrt(vt), 0), -MAX_WEIGHT, MAX_WEIGHT);

参数值会综合考虑变化方向(mt)与梯度波动幅度(vt)后完成更新。更新后的参数值会写回全局数据缓冲区。

 matrix_w[shift_weight] = weight;
 matrix_m[shift_weight] = mt;
 matrix_v[shift_weight] = vt;
}

因此,每个滤波器参数都会参考历史更新的丰富上下文来完成自适应调整。这使模型既能快速响应局部误差,又能避免参数剧烈震荡,实现平稳的自适应。这类精细调优在处理波动的金融时间序列时尤为关键,因为在此场景下,哪怕一次多余的数据扰动,都可能引发过拟合或泛化能力下降。

至此,程序的OpenCL端工作已经完成。完整代码参见本文附件。


创建对象

我们已经逐步讲解了如何在OpenCL程序内核中实现可变窗口自适应卷积、动态token生成、梯度计算以及权重更新。然而,单纯的计算逻辑仅仅占一半工作量。想要让这套架构在完整模型中实时运行,就必须将其妥善集成到主程序内。

到了最有意思的部分 —— 集成。就像一支优秀的管弦乐团,起决定作用的不只是独奏乐手(内核)的技艺,还需要指挥精准调度:在合适时机启动各个流程、管控模块之间的交互,并保证整首乐曲的完整性。

在此情况下,CNeuronAdaptConv类就充当指挥的角色。它统筹全部流程:先做主频率分析,再执行自适应卷积,然后反向传播,最后用Adam优化器更新权重。它不只是OpenCL程序的简单包装层,而是一个功能完备的控制模块,负责决策、串联各个计算阶段,并保证迭代之间状态可持续保留。

该新对象的结构如下。

class CNeuronAdaptConv    :  public CNeuronConvOCL
  {
protected:
   CBufferFloat      bMainFreq;
   //---
   virtual bool      FFT(CBufferFloat *inp_re, CBufferFloat *inp_im,
                         CBufferFloat *out_re, CBufferFloat *out_im, 
                         uint variables, bool reverse = false);
   virtual bool      PeriodsFinding(CBufferFloat *inp_re, CBufferFloat *inp_im, 
                                    CBufferFloat *main_freq, uint variables);
   virtual bool      AdaptiveConvolution(CNeuronBaseOCL *NeuronOCL, CBufferFloat *main_freq);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAdaptConv(void) {};
                    ~CNeuronAdaptConv(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                          uint window_out, uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronAdaptConv;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj)   override;
  };

由此可见,在CNeuronAdaptConv类内部,仅声明了一个自有缓冲区:bMainFreq,用于存储主频率。运行所需的其余所有对象均继承自父卷积层类CNeuronConvOCL,这种设计能够复用通用逻辑、减少代码冗余。

缓冲区bMainFreq作为类的持久成员存在,而构造函数与析构函数本身为空。该缓冲区以及所有继承对象的初始化流程由Init方法处理,其参数提供一组常量,能够唯一确定待创建对象的架构。

bool CNeuronAdaptConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                            uint window, uint window_out, uint units_count, uint variables,
                            ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, window, window, window_out,
                                    units_count, variables, optimization_type, batch))
      return false;

该方法的算法逻辑非常简单。首先把全部校验与初始化工作交给父类的基础逻辑——就像信任一位熟知各类参数与缓冲区的导师,让我们的代码省去了繁琐的常规操作。这让我们的代码摆脱了不必要的常规事务。剩下的工作就只需完成主频率缓冲区的初始化。

   bMainFreq.BufferFree();
   if(!bMainFreq.BufferInit(iVariables, 1) ||
      !bMainFreq.BufferCreate(OpenCL))
      return false;
//---
   return true;
  }

接下来,将该方法的布尔返回值交还给调用程序。

需要说明的是,这个新对象的大部分方法仅仅是包装器,用于将对应内核的执行加入任务队列,采用的就是你已经熟悉的这套算法:

  • FFT —— 通过快速傅里叶变换将时间序列分解为各频率分量;
  • PeriodsFinding —— 提取主导频率;
  • AdaptiveConvolution —— 自适应卷积的前向传播。

由于快速傅里叶变换与主导频率搜索不涉及可训练参数,也无需误差反传,因此与之相关的调用基本都只是对相应内核的封装。与之不同,前向传播方法feedForward有明显区别,它在一次前向过程中串联了多个处理步骤。

bool CNeuronAdaptConv::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

在该方法参数中,我们接收源数据对象的指针,并立刻校验其有效性。接下来,我们调用FFT方法,将获取的数据分解为各个频率分量。

if(!FFT(NeuronOCL.getOutput(), NULL, Output, PrevOutput, iVariables, false))
   return false;

从得到的频谱中,我们提取每条单变量序列的主导频率。

if(!PeriodsFinding(Output, PrevOutput, GetPointer(bMainFreq), iVariables))
   return false;

最后,我们调用自适应卷积方法,生成所需的token。

 return AdaptiveConvolution(NeuronOCL, GetPointer(bMainFreq));
}

最后把方法执行结果返回给调用方。

因此,CNeuronAdaptConv类是整个计算流水线真正的“指挥大师”:它本身实现简洁,却具备出色的调度与同步能力,只在必要的环节才亲自介入。


结论

在本文中,我们完成了一套完整的时间序列处理流水线开发,将频谱分析与自适应卷积整合为一套一体化算法。我们展示了如何借助FFT与主导频率检测,识别各个数据通道的节律特征;再基于该信息灵活调整分段宽度,同时保证模型输出端的token数量固定不变。

重点讲解了在MQL5与OpenCL环境下的具体实现。我们完整梳理了全部流程:从频谱分析、数据分块,到反向传播,再通过Adam优化器完成权重更新。每个阶段都封装为小巧且独立的内核,CNeuronAdaptConv控制类负责协调、同步各个内核的运行,如同计算乐团的指挥家。

得益于精心设计的架构 —— 每个GPU线程仅处理自身对应的权重与数据片段,我们实现了高效并行,工作项之间互不干扰。零填充与严谨的偏移寻址机制保证信息不会丢失,各通道数据不会混淆。自适应Adam优化器通过精细考量一阶矩与二阶矩,保障模型训练平滑且稳定。

在下一篇文章中,我们将探讨如何把生成的这些token应用到改进后的Transformer堆叠结构中。


参考文献

文中所用的程序

# 名称 类型 描述
1 Study.mq5 智能交易系统(EA) 用于离线模型训练的EA
2 StudyOnline.mq5 智能交易系统(EA) 用于在线模型训练的EA
3 Test.mq5 智能交易系统(EA) 用于模型测试的EA
4 Trajectory.mqh 类库 用于描述系统状态与模型架构的结构体
5 NeuroNet.mqh 类库 用于构建神经网络的类库
6 NeuroNet.cl 库 OpenCL程序代码库

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/18629

附加的文件 |
MQL5.zip (2881.34 KB)
科学家群体优化(CoSO):算法实现 科学家群体优化(CoSO):算法实现
我们继续讨论“科学家群体优化”这一主题。不应将 CoSO 视为现成的解决方案,而应将其视为一个有前景的研究平台。经过适当开发,CoSO 可以在那些适应性和应变能力至关重要、计算时间不是关键的任务中找到自己的定位。
MQL5 中的量子神经网络(第二部分):基于 ALGLIB 马尔可夫矩阵的神经网络反向传播训练 MQL5 中的量子神经网络(第二部分):基于 ALGLIB 马尔可夫矩阵的神经网络反向传播训练
本文介绍了一种创新的量子神经网络架构,用于结合量子力学原理与现代机器学习方法进行算法交易。该系统引入了量子效应(共振、干涉、退相干)、不同时间尺度的多级记忆、结合 ALGLIB 库的马尔可夫链以及自适应参数控制。整个实现过程使用 MQL5 内置的矩阵/向量类型完成,从而降低了在 MetaTrader 5 中实现该系统的门槛。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
交易中的神经网络:自适应周期分块(LightGTS) 交易中的神经网络:自适应周期分块(LightGTS)
本文将为大家介绍创新的自适应分块技术,这是一种在考虑时序数据内在周期性的前提下,对时间序列做灵活分块的方法。我们还会讲解一种高效编码技术,该技术在处理不同尺度的数据时,可以保留重要语义特征。金融市场数据具备复杂、多尺度的典型特点,上述两类方法为此类数据的精准处理开辟了新路径,能够显著提升预测结果的稳定性与可靠性。