English Русский Español Deutsch 日本語 Português
preview
交易中的神经网络:不依赖特定数据的时间序列泛化(核心模型模块)

交易中的神经网络:不依赖特定数据的时间序列泛化(核心模型模块)

MetaTrader 5交易系统 |
27 0
Dmitriy Gizlyk
Dmitriy Gizlyk

引言

在前一篇文章中,我们介绍了Mamba4Cast框架及其核心组件: 状态空间模型(SSM)模块与基于先验数据拟合的网络PFNs)机制。该框架为时间序列预测打下坚实基础,有望成为交易者分析工具包的有力补充。

Mamba4Cast的设计目标并非对每一条全新时间序列都进行冗长的预热 ,而是支持直接部署上线。借助零样本预测理念,模型无需额外训练或超参数调参,即可在真实数据上生成高质量预测。因此,交易者不再需要耗费数日去搜寻最优参数配置。

模型极高的运算速度来源于SSM模块的线性计算复杂度。Transformer架构的计算开销会随序列长度呈二次方增长,与之不同,Mamba4Cast的每一步处理均为常数时间复杂度。即便面对超长序列,也可以实现近乎瞬时的推理,同时保持极低延迟。在交易场景中,执行速度往往会决定一笔交易的成败,这一优势很明显。

除此之外,Mamba4Cast会一次性输出全部预测区间的结果,而非逐步迭代生成预测。该设计规避了自回归模型普遍存在的误差累积问题,输出的未来走势轨迹更加稳定。交易策略可直接获取完整预测视图,便于做出更可靠、依据更充分的决策。

同样重要的是基于合成场景的训练方式。模型在数百万条人工生成的时间序列样本上完成训练。这使得 Mamba4Cast形成一种普遍的直觉,能够在各类复杂场景下稳定运行。该方法提升了模型对噪声与市场突变的抗干扰能力,进而降低实际生产环境中出现意外失效的概率。

尽管底层机制能力强大,Mamba4Cast依旧具备很高的计算效率。根据框架作者的实验结果,它可以取得与主流基于Transformer的基础模型相当的预测精度,但所需计算资源大幅降低。即便在硬件资源受限的设备上也可实际运行,无需高端GPU集群,便于直接集成到交易终端当中。

正是集可直接部署、推理速度优异、全局一次性预测、抗噪声、计算轻量化于一身,让Mamba4Cast成为时间序列预测领域具备突破性的框架。

该框架作者提供的Mamba4Cast架构示意图如下。

在前一篇文章的实操部分,我们完成了时间编码对象的实现 —— 该核心组件负责为输入数据提供其在时间序列中的位置表示。该组件完成了模型的初始化流程,也是输入信号架构预处理中必不可少的一环。如果缺少该组件,框架将无法正确捕捉时间依赖关系。

今天,我们就从这一部分继续往下讲解。


数据预处理模块

开发逻辑十分明确:在执行任何计算、预测价格走势或是生成交易信号之前,必须先对输入数据做妥善处理。和所有机器学习系统一样,Mamba4Cast的性能直接取决于输入数据流的质量。倘若模型接收的是带噪声输入、尺度不统一或是结构碎片化的数据,即便是最先进的模型架构也无力弥补。正因如此,我们现在来聚焦数据预处理模块

该模块绝不仅仅是一个辅助环节。它是原始行情数据与模型可高效解析的结构化输入之间的桥梁。该环节会执行归一化、缩放、滑动窗口构建、掩码生成,最重要的是,通过额外特征通道为数据引入上下文信息。以上一系列操作将数据整理完毕,供给模型核心模块运算,为后续全部预测任务打下基础。

我们的目标不只是加载报价与指标数值。我们必须将数据转换到统一量纲,划定有效窗口边界,为缺失值生成掩码,并且在时间维度上同步全部特征通道。只有完成上述步骤,再把数据送入编码器,才能够期望模型对数据做出正确解析。

在上一篇文章的实操章节,我们已经实现了时间编码对象,它既是数据预处理的关键组件,也是Mamba4Cast架构的重要组成部分。借助该组件,模型看待一连串市场事件时,不再将其视作一堆抽象数字,而是理解为具备明确顺序与时间节律的结构化信息。

为实现该方法,我们将编写一个专用类:CMamba4CastEmbedding,它在预处理流程中处于核心地位。其结构如下所示。

class CMamba4CastEmbeding :   public CNeuronBaseOCL
  {
protected:
   CNeuronConvOCL             cProjection;
   CNeuronBatchNormOCL        cNorm;
   CNeuronTSPositionEncoder   cProjectionWithTE;
   //---
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;
   virtual bool               updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CMamba4CastEmbeding(void) {};
                    ~CMamba4CastEmbeding(void) {};
   //---
   virtual bool               Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint window, uint window_out, uint units_count, uint &periods[],
                                   ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int                Type(void) override  const   {  return defMamba4CastEmbeding;   }
   //---
   virtual bool               Save(int const file_handle) override;
   virtual bool               Load(int const file_handle) override;
   //---
   virtual bool               WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void               SetOpenCL(COpenCLMy *obj) override;
  };

核心理念是将原始信息流转换为结构化、信息密度充足的表征形式,适配后续预测流程。为实现该目标,该类采用模块化设计,整合多个专用子模块,每个子模块负责一项特定的数据变换操作。

值得注意的是,CMamba4CastEmbedding类的全部内部对象均为非动态分配的成员对象。因此,构造函数与析构函数均为空实现,无需手动进行动态内存管理 —— 对象会自动完成创建与销毁。该架构设计简化了对象生命周期管理,最大程度降低内存泄漏风险,规避动态内存分配带来的潜在错误,这一点对于高性能交易系统尤为关键。

所有内部模块的初始化工作都在Init方法中完成,该方法的参数定义了待创建对象的核心属性。

bool CMamba4CastEmbeding::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                               uint window, uint window_out, uint units_count, uint &periods[],
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(periods.Size() <= 0)
      return false;
   int freqs = (int(window_out / 2 + 2 * periods.Size()) - 1) / int(2 * periods.Size());
   if(freqs <= 0)
      return false;

该方法首先校验待分析时间序列周期数组是否存在有效数据。该项校验必不可少,因为时间编码至少需要一个周期才能完成正确计算。紧接着,该方法计算freqs参数,该参数规定分配给每个周期的频率谐波数量。

该参数背后的逻辑需要展开详细说明。初始化参数之一(window_out)定义了单个时间步的嵌入维度。之前已经提到,每一片时序数据都需要用两种互补形式来表征:第一种是与时间无关的纯数值;第二种是结合由谐波分量编码得到的时序特征。最终嵌入会在同一向量中同时包含这两类表示。但分配给该表征的内存总量是固定的。因此,用户定义的嵌入维度仅有一半可用于时间编码。剩余一半预留给输入数据的标准投影变换。

现在,让我们看一下时间编码这一半维度如何分配给各个频率分量。Mamba4Cast的作者采用了与Transformer架构位置编码相类似的思路:针对每一个指定周期,同时生成正弦、余弦函数,用以描述振荡的相位与频率。由此可知,每一个频率分量都需要一组谐波函数:一个正弦和一个余弦。因此,频率分量的总数量至少需要为输入数组指定周期数量的两倍。

为避免概念混淆,这里采用一套简洁但需要仔细理解的计算公式:将window_out的一半,除以指定周期数的二倍。计算所得结果决定每个周期可以分配多少组频率分量。该数值必须始终为正。否则,模型连一组谐波对都无法生成,时间编码也就无从实现。

在实际应用中,在选择单个时间步的嵌入大小时,目标不能单纯是提升模型容量,还必须保证每一个待分析周期至少能够分配到一组谐波对。

随后,我们调用父类对应的初始化方法,完成全部继承对象与接口的初始化。

if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_count, optimization_type, batch))
   return false;

执行成功之后,我们按顺序对本类的各个内部模块执行初始化。第一个组件是cProjection,即卷积投影层,负责将输入数据映射为精简的特征表征。

int index = 0;
if(!cProjection.Init(0, index, OpenCL, window, window, window_out - 2 * freqs * periods.Size(), units_count,
                                                                                   1, optimization, iBatch))
   return false;
cProjection.SetActivationFunction(TANH);

该模块生成原始数据的嵌入向量,不纳入时间信息。我们使用TANH激活函数引入非线性,使网络能够凸显原始数值数据中隐藏的有效模式,同时降低异常值带来的影响。

紧接着,批归一化层(cNorm)对投影后的特征做调整,消除分布偏差,提升计算稳定性。

index++;
if(!cNorm.Init(0, index, OpenCL, cProjection.Neurons(), iBatch, optimization))
   return false;
cNorm.SetActivationFunction(None);

生成融合时间信息的嵌入向量由时间编码模块(cProjectionWithTE)负责完成,该模块对于捕捉季节性趋势以及市场行为的动态变化尤为重要。

   index++;
   if(!cProjectionWithTE.Init(0, index, OpenCL, window, units_count, periods, freqs, optimization, iBatch))
      return false;
   SetActivationFunction(None);
//---
   return true;
  }

在该框架的逻辑中,这套顺序初始化流程确保各个子模块获得统一且相互兼容的配置,实现时间信息与原始输入数据的高效融合。

CMamba4CastEmbedding模块的关键环节之一是执行前向传播方法,该方法驱动数据流经预处理架构的全部组件,完成前向运算。正是在这一步为后续模块打下基础:后续模块不再处理原始输入,而是使用经过精细处理的特征表征;这类表征已经同时包含原始信号的结构特征以及时间上下文信息。

bool CMamba4CastEmbeding::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!cProjection.FeedForward(NeuronOCL))
      return false;

该流程首先将输入数据送入cProjection。这个精简卷积块把原始信号映射至隐特征空间。该步骤让系统可以立刻聚焦于输入数据流中信息量最高的模式与显著变化。

随后提取得到的特征被送入cNorm。该归一化层负责将激活值缩放到稳定区间,同时消除脉冲尖峰、异常值以及分布偏移。

if(!cNorm.FeedForward(cProjection.AsObject()))
   return false;

如果缺少归一化处理,深度神经网络架构往往会遭遇梯度爆炸或梯度消失问题。这里,归一化充当稳定机制,让模型在训练与推理阶段均能够保持稳定表现。

然而,Mamba4Cast架构的独特之处在于它的第二条处理分支。原始输入数据会并行转发至负责时间编码的模块cProjectionWithTE

if(!cProjectionWithTE.FeedForward(NeuronOCL, SecondInput))
   return false;

与仅依靠卷积运算的初始投影阶段不同,该模块还接收第二路输入SecondInput。通过该输入通道,每个时间步预先计算好的时间标记被引入计算过程。这使得该模块不仅可以分析特征数值本身,还能够结合精确的时间上下文解读特征,提升模型对市场的季节性、周期性以及相位相关动态规律的敏感度。

整个流程的最后一步为特征融合操作。两条处理分支输出的表征向量拼接为一个完整张量。

   if(!Concat(cNorm.getOutput(), cProjectionWithTE.getOutput(), Output, cProjection.GetFilters(),
              cProjectionWithTE.GetWindowOut(), cProjection.GetUnits()))
      return false;
//---
   return true;
  }

最终输出为每个时间步生成稠密、多层级的特征表征,该表征同时融合原始特征信息与增强后的时间结构。正是这份完备的时序表征,构成模型后续全部运算的基础。

众所周知,仅依靠前向传播不足以完成神经网络的训练。如果要让模型适配数据,还必须执行正确的误差反向传播。该部分逻辑由calcInputGradients方法实现。本质上,它是模型内部的误差分析机制;在生成预测结果之后被触发,用于定位误差产生的位置,并确定应当如何调整模型参数。

bool CMamba4CastEmbeding::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

该方法首先校验参与运算的对象。必须确保参与梯度计算的每一个组件都已经完成正确初始化。完成该校验后,程序执行DeConcat方法,将之前拼接在一起的梯度向量拆分为两条独立信息流。

if(!DeConcat(cNorm.getGradient(), cProjectionWithTE.getGradient(), Gradient, cProjection.GetFilters(),
             cProjectionWithTE.GetWindowOut(), cProjection.GetUnits()))
   return false;

回顾前向传播流程:归一化后的特征表征与时间嵌入向量被合并为一个张量。但在反向传播阶段,必须再次把这两路信息流拆分,使得每一条处理分支能够独立完成训练。

梯度拆分完成后,正式开启主反向传播流程,将误差梯度沿着两条信息流回传至原始输入数据。首先,对不含时间信息的分支执行运算。

if(!cProjection.calcHiddenGradients(cNorm.AsObject()))
   return false;
if(!NeuronOCL.calcHiddenGradients(cProjection.AsObject()))
   return false;

计算流程切换至时间上下文分支。但在梯度回传之前,需要保留此前已经计算完成的数据。为此,该方法会临时重定向指针,使其指向原始输入对象所对应的梯度缓冲区。完成上述操作后,再执行梯度计算。

   CBufferFloat *temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false) ||
      !NeuronOCL.calcHiddenGradients(cProjectionWithTE.AsObject()) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp, cProjection.GetWindow(), false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false)
     )
      return false;
//---
   return true;
  }

最后一步是对两条信息流输出的梯度执行求和。完成之后,将全部缓冲区指针恢复至初始状态,该方法返回布尔类型运算结果,交由调用方程序处理。

这套精心设计的处理流程,能够让误差梯度在整个模块内正确分发,保障权重更新过程均衡。

各个子模块可训练参数的优化工作由updateInputWeights方法完成。权重更新通过依次调用内部对象对应的更新方法实现。该分步执行机制体现了权重隔离管理原则,可对训练过程实现细粒度控制,并且权重变更独立于系统整体架构。如果更新流程任意环节发生失败,该方法会立刻返回错误,以此保证模型状态的一致性。

updateInputWeights方法的代码见本文附件。附件中同时提供完整的CMamba4CastEmbedding类及其全部方法源码。

总而言之,CMamba4CastEmbedding类绝不只是一堆数据变换算法的集合。它是一套经过精密设计的模块,内部每一项功能都与Mamba4Cast框架的整体逻辑深度耦合。它对输入数据完成高质量预处理,使模型可以高效提取有效特征、捕捉时序动态特征,并依靠正确回传的误差梯度完成有效学习。


编码器

接下来我们开始实现Mamba4Cast框架作者提出的编码器组件。该组件承担关键作用:为后续预测任务完成特征提取与特征结构化。其架构由一组不同卷积核尺寸的卷积层堆叠而成。每一个卷积层专注提取输入数据的特定特征;各层输出经过拼接、归一化之后,送入Mamba模块。其中,块的重复堆叠次数决定特征提取的深度,使框架能够适配不同复杂度的任务。

这里重点关注卷积层堆叠部分。我们的库中早已封装卷积层对象,调用使用本身没有难度。然而,在传统实现方法中,每一个卷积层都实例化为独立对象,按顺序串行运算。当使用多种尺寸卷积核时,对象数量会大幅增加,同时串行执行会显著增加模型计算的复杂度,拉长训练耗时。

为解决该局限,我们专门设计了一个对象,支持并行处理多路卷积层。该对象可在多条执行流上同时发起计算,大幅缩短执行耗时。其内部负载分配逻辑:将同一份输入信号送入多个不同核尺寸的卷积滤波器,再把各路输出拼接为单个张量。该方案不仅减少中间对象数量,还可以大幅提升计算资源利用率,在现代多线程GPU上效果尤为明显。

该理念并非全新的构想。此前我们已经实现过CNeuronMultiWindowsConvOCL类,支持多尺寸卷积核运算。然而,该类的设计要求每一个卷积窗口都配备独立输入缓冲区。我们本次场景面临新的问题:同一条输入数据流,需要同时交由多个卷积窗口处理。

当前的难点在于:卷积核尺寸改变,卷积可执行的运算次数也会随之改变。卷积窗口越大,可以执行卷积运算的位置数量就越少。于是不同卷积核对应的计算负载出现不均衡,这给高效并行执行带来阻碍。

为解决该问题,我们采用零填充,在输入向量的头部与尾部补零。该手段对向量做长度扩展,无论卷积窗口尺寸多大,都可以使用同等数量的运算线程。最终,卷积运算的数量完全由卷积步长决定,所有卷积核使用相同步长。由此均衡各个并行线程的负载,极大简化并行处理逻辑。

OpenCL侧的核心实现逻辑封装在短小但功能强大的卷积核FeedForwardMultWinConvWPad中,该核使用多个不同长度的卷积窗口,对同一条输入数据流做并行处理。

__kernel void FeedForwardMultWinConvWPad(__global const float *matrix_w,
                                         __global const float *matrix_i,
                                         __global float *matrix_o,
                                         __global const int *windows_in,
                                         const int inputs,
                                         const int step,
                                         const int window_out,
                                         const int activation
                                        )
  {
   const size_t id = get_global_id(0);
   const size_t id_w = get_global_id(1);
   const size_t v = get_global_id(2);
   const size_t outputs = get_global_size(0);
   const size_t windows_total = get_global_size(1);

每一条执行线程由三个坐标标识:

  • id —— 输出张量元素的索引;
  • id_w —— 卷积窗口的索引;
  • v —— 单变量输入流的标识符。

该线程组织方式,能够使用不同运算参数,对多个卷积窗口、多个位置以及多条输入序列实现并行处理。

第一步,卷积核根据当前窗口标识符,从windows_in数组中读取,得到当前卷积窗口的长度window_in

int window_in = windows_in[id_w];

接下来,根据当前输出张量索引,计算卷积窗口在输入缓冲区中的起始偏移量。偏移量的计算需要保证卷积窗口的中心与id位置对齐。由于窗口尺寸各不相同,使用表达式 (window_in + 1) / 2来实现正确的中心对齐。

int window_in = windows_in[id_w];
int mid_win = (window_in + 1) / 2;
int shift_in = id * step - mid_win;
int shift_in_var = v * inputs;

随后通过变量shift_in_var,在全局输入缓冲区中定位到对应的输入序列。

下一步计算shift_weight,即可训练参数全局数组内的偏移量。由于各个卷积窗口的权重是连续存储的,存储时同时兼顾权重长度与偏置(bias)系数,因此需要累加当前窗口id_w之前所有权重块的大小。通过这种方式,就可以精准定位当前卷积权重的起始存储位置。

int shift_weight = 0;
for(int w = 0; w < id_w; w++)
   shift_weight += (windows_in[w] + 1) * window_out;

随后针对每一个输出通道w_out开启嵌套循环,该输出通道对应卷积运算后输出缓冲区里的嵌入维度。

   for(int w_out = 0; w_out < window_out; w_out++)
     {
      float sum = matrix_w[shift_weight + window_in];
      //---
      for(int w = 0; w < window_in; w++)
         if((shift_in + w) >= 0 && (shift_in + w) < inputs)
            sum += IsNaNOrInf(matrix_i[shift_in_var + shift_in + w] * matrix_w[shift_weight + w], 0);
      //---
      int shift_out = (v * outputs + id) * window_out + w_out;
      matrix_o[shift_out] = Activation(sum, activation);
      shift_weight += window_in + 1;
     }
  }

针对每一个输出通道,初始化变量sum,该变量首先读取权重块的末尾元素,也就是偏置值。随后将输入值与对应权重的乘积累加到该变量上。运算过程中,算法会显式校验当前输入索引是否超出有效输入范围。如果越界,则跳过本次运算,该行为等价于执行零填充。

完成窗口内的循环运算后,运算结果送入激活函数处理。激活类型由 activation参数指定,通过辅助封装函数Activation调用激活函数。考虑全部所需偏移量之后,最终数值写入全局输出缓冲区matrix_o

最后,shift_weight指针向前偏移:偏移量等于当前窗口大小再加一个偏置元素,使指针定位到下一个滤波器的权重起始位置。因此,每条执行线程顺序处理全部输出通道;而所有卷积窗口、全部输入序列的计算全程保持完全并行。

该套算法兼具灵活性与极高执行效率。多尺度卷积同时作用于输入数据,生成完备的多尺度输入表征。依靠零填充,无需手动截断序列或者后处理来对齐嵌入维度。全部逻辑都在GPU运行时动态完成。

该实现体现一条重要设计原则:高性能并非依靠简化算法实现,而是通过精细的计算任务划分、充分利用并行执行、精准编排数据变换流水线的每一个环节来达成。

接下来的主要阶段是计算相对于原始输入数据的误差梯度。与前向传播不同,反向传播的实现难度要大得多。此时不再是简单应用一组滤波器;我们必须把每一个滤波器的贡献反向回传,重构输入层的梯度,同时正确处理各类偏移,保证张量的一致性。

该算法由OpenCL卷积核CalcHiddenGradientMultWinConvWPad实现。它的作用是聚合不同长度卷积窗口,全部输出通道产生的误差梯度。换言之,该核重构回传到原始输入序列的误差梯度,同时兼顾激活函数的非线性特性以及卷积滤波器的结构。

__kernel void CalcHiddenGradientMultWinConvWPad(__global const float *matrix_w,
                                                __global const float *matrix_i,
                                                __global float *matrix_ig,
                                                __global const float *matrix_og,
                                                __global const int *windows_in,
                                                const int outputs,
                                                const int step,
                                                const int window_out,
                                                const int activation
                                               )
  {
   const size_t id_x = get_global_id(0);
   const size_t id_loc = get_local_id(1);
   const size_t id_win = id_loc / window_out;
   const size_t id_f = id_loc % window_out;
   const size_t v = get_global_id(2);
   const size_t inputs = get_global_size(0);
   const size_t size_loc = get_local_size(1);
   const size_t windows_total = size_loc / window_out;

卷积核中的每一条线程负责计算输入层梯度的一个元素:

  • id_x —— 输入序列内部的位置索引;
  • id_loc —— 本地工作组标识符,该标识符可拆解为id_win(窗口索引)与id_f(输出缓冲区内的滤波器索引);
  • v —— 输入序列的标识符。
该卷积核同时持有这些参数:输入总长度(inputs)、输出元素数量(outputs),以及卷积窗口总数量(windows_total)。

第一步计算shift_weight,用于定位当前卷积窗口与对应通道的权重块的精确起始位置。和前向传播逻辑一致,算法遍历所有排在前面的窗口,累加它们的大小,直到在全局权重数组matrix_w中定位到正确位置。

   __local float temp[LOCAL_ARRAY_SIZE];
   const uint ls = min((uint)size_loc, (uint)LOCAL_ARRAY_SIZE);
//---
   int window_in = windows_in[id_win];
   int shift_weight = id_f * (window_in + 1);
   for(int w = 0; w < id_win; w++)
      shift_weight += (windows_in[w] + 1) * window_out;

接下来,程序确定输出缓冲区中,当前输入元素能够参与运算的区域,即shift_out。该数值下限为0,代表输出位置的最小下标;该输出位置对应的卷积窗口仍可以包含当前输入元素id_x。

int shift_out = max((int)((id_x - window_in) / step), 0);

但仅完成这一步还不够,随后开启循环,遍历所有在前向传播过程中会用到该输入元素的输出项out

float grad = 0;
int mid_win = (window_in + 1) / 2;
for(int out = shift_out; out < outputs; out++)
  {
   int shift_in = out * step - mid_win;
   if(shift_in > id_x)
      break;
   int shift_w = id_x - shift_in;
   if(shift_w >= window_in)
      continue;
   int shift_g = ((v * outputs + out) * windows_total + id_win) * window_out + id_f;
   grad += IsNaNOrInf(matrix_w[shift_w + shift_weight] * matrix_og[shift_g], 0);
  }

每一轮迭代中,卷积核会判断id_x是否落在当前卷积窗口范围内。如果在窗口内,则计算窗口内部的局部偏移量shift_w,以及对应的输出梯度位置shift_g。将对应的滤波器权重与输出梯度缓冲区matrix_og相乘,得到该卷积运算对输入梯度的贡献值。把各项贡献累加至变量grad

此时,每条执行线程仅得到一份部分梯度。但同一个工作组内的多条线程,在处理不同滤波器或输出通道时,可能对应同一个输入位置id_x。因此,在本地内存中引入中间数组temp,并开启带同步屏障的求和循环。每条线程首先将自身的部分运算结果存入temp

for(int i = 0; i < size_loc; i += ls)
  {
   if(i <= id_loc && (i + ls) > id_loc)
      temp[id_loc % ls] = (i == 0 ? 0 : temp[id_loc % ls]) + grad;
   barrier(CLK_LOCAL_MEM_FENCE);
  }

随后借助并行归约折叠算法,按照2的幂次对数组执行求和运算,最终把所有通道和所有窗口贡献的总梯度存放至temp[0]中。

uint count = ls;
do
  {
   count = (count + 1) / 2;
   if(id_loc < count && (id_loc + count) < ls)
     {
      temp[id_loc] += temp[id_loc + count];
      temp[id_loc + count] = 0;
     }
   barrier(CLK_LOCAL_MEM_FENCE);
  }
while(count > 1);

最终,只有满足id_loc == 0的线程,才会将计算得到的梯度结果写入全局输入梯度缓冲区matrix_ig。写入之前,会调用Deactivation函数,该函数对相应的输入激活值matrix_i执行激活函数导数运算。该步骤必不可少:梯度必须根据激活函数做修正;否则,模型的学习过程将会出错。

 if(id_loc == 0)
    matrix_ig[v * inputs + id_x] = Deactivation(temp[0], matrix_i[v * inputs + id_x], activation);
}

这是反向传播中计算量最大的环节之一。通过将计算任务按窗口、通道进行拆分,再配合本地内存,即便参数量巨大,依然可以实现很高的运算效率。结合前向传播流程,该算法不仅具备模块化、可扩展的特性,还能够处理金融时间序列中高度复杂的依赖关系,同时兼顾计算精度与运行性能。

这套卷积运算流程的最后一步为权重优化。在该阶段,已经计算完成的梯度会转化为实际的参数更新量。该过程采用Adam优化算法,并做扩展适配,兼容不同尺寸的卷积窗口与零填充逻辑。整套逻辑封装在UpdateWeightsMultWinConvAdamWPad卷积核中。

__kernel void UpdateWeightsMultWinConvAdamWPad(__global float *matrix_w,
                                               __global const float *matrix_og,
                                               __global const float *matrix_i,
                                               __global float *matrix_m,
                                               __global float *matrix_v,
                                               __global const int *windows_in,
                                               const int windows_total,
                                               const int window_out,
                                               const int inputs,
                                               const int step,
                                               const int outputs,
                                               const float l,
                                               const float b1,
                                               const float b2
                                              )
  {
   const size_t i = get_global_id(0);  // weight shift
   const size_t v = get_local_id(1);   // variable
   const size_t variables = get_local_size(1);

该卷积核中的每一条线程负责更新一个特定参数i,该参数既可以是滤波器系数,也可以是偏置。变量v用于标识输入序列,variables指定序列的总数量。二者互相配合,能够对全部输入序列的梯度执行累加,以此获得更加稳定的参数更新效果。

执行开始时,每条线程根据自身的参数索引i,判断该参数隶属于哪一个滤波器、哪一个输出通道,以及具体的权重位置。实现方式为遍历全部卷积窗口,同时维护累计偏移量shift_before,该偏移量记录每个窗口在可训练参数线性数组中的位置。

   __local float temp[LOCAL_ARRAY_SIZE];
   const uint ls = min((uint)variables, (uint)LOCAL_ARRAY_SIZE);
//---
   int step_out = window_out * windows_total;
//---
   int shift_before = 0;
   int window = 0;
   int number_w = 0;
   for(int w = 0; w < windows_total; w++)
     {
      int win = windows_in[w];
      if(shift_before <= i &&
         (win + 1)*window_out > (i - shift_before))
        {
         window = win;
         number_w = w;
        }
      else
         shift_before += (win + 1) * window_out;
     }

定位到索引i所对应的窗口之后,我们确定以下变量:

  • window —— 滤波器尺寸(窗口宽度);
  • number_w —— 窗口索引;
  • id_f —— 滤波器索引;
  • shift_in —— 相对于窗口起始位置的偏移量(如果该偏移等于窗口尺寸,则代表该元素为偏置项 );
  • bias —— 逻辑标志位,用于判断当前处理的元素是否为偏置参数。

int shift_in = (i - shift_before) % (window + 1);
int shift_in_var = v * inputs;
bool bias = (shift_in == window);
int mid_win = (window + 1) / 2;
int id_f = (i - shift_before) / (window + 1);
int shift_out = number_w * window_out + id_f;
int shift_out_var = v * outputs * step_out;

对于普通滤波器系数,卷积核会遍历每一个输出位置out。在每一个输出位置上,校验对应的输入元素是否落在有效输入范围之内。如果合法,则执行标准梯度计算:将输出梯度缓冲区matrix_og与对应的输入激活值matrix_i相乘,并将乘积累加至变量grad

float grad = 0;
if(!bias)
  {
   for(int out = 0; out < outputs; out++)
     {
      int in = out * step - mid_win + shift_in;
      if(in >= inputs)
         break;
      if(in < 0)
         continue;
      //---
      grad += IsNaNOrInf(matrix_og[shift_out_var + shift_out + out * step_out] * matrix_i[shift_in_var + in], 0);
     }
  }
else
  {
   for(int out = 0; out < outputs; out++)
      grad += IsNaNOrInf(matrix_og[shift_out_var + shift_out + out * step_out], 0);
  }

对于偏置参数,不需要使用输入激活值。卷积核直接对全部位置上的输出梯度做累加即可。

下一阶段再次借助temp本地数组完成梯度累加。其目的是聚合全部输入序列上的梯度,同时降低个别异常值带来的影响。和之前逻辑一致,采用并行归约树配合同步屏障,在每个工作组内部高效完成求和运算。只有满足v == 0的线程(第一条线程)执行最终的权重更新操作。

//--- sum
   for(int s = 0; s < (int)variables; s += ls)
     {
      if(v >= s && v < (s + ls))
         temp[v % ls] = (s == 0 ? 0 : temp[v % ls]) + grad;
      barrier(CLK_LOCAL_MEM_FENCE);
     }
//---
   uint count = ls;
   do
     {
      count = (count + 1) / 2;
      if(v < count && (v + count) < ls)
        {
         temp[v] += temp[v + count];
         temp[v + count] = 0;
        }
      barrier(CLK_LOCAL_MEM_FENCE);
     }
   while(count > 1);

此时Adam优化器开始生效:

  1. 一阶矩估计量mt,通过系数b1做指数平滑完成更新。
  2. 代表梯度方差的二阶矩估计量vt,通过系数b2完成更新。
  3. 权重更新方式:对mt / sqrt(vt)做归一化,再乘以学习率l得到更新步长。
  4. 全部数值均通过clamp(截断约束)做限制,避免出现数值不稳定、权重爆炸以及除零错误。

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

最后,将更新后的参数值写回全局内存。

结合前文介绍的前向传播与反向传播流程,这套机制让该卷积模块具备完全可微性,能够集成到深度神经网络架构之中。更为重要的是,它具备优秀的可扩展性,可以轻松适配任意数量的卷积窗口与输出通道。这使得模型在处理真实场景下的非线性、非平稳时间序列时拥有极强的灵活性。

在主程序内部,带零填充的多窗口卷积全部功能被封装于CNeuronMultiWindowsConvWPadOCL类中。该类继承自基础卷积层CNeuronConvOCL,作为接口层负责调度以上全部OpenCL卷积核。

类结构如下所示。

class CNeuronMultiWindowsConvWPadOCL    :  public CNeuronConvOCL
  {
protected:
   int               aiWindows[];
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronMultiWindowsConvWPadOCL(void) {  activation = SoftPlus;  iWindow = -1; }
                    ~CNeuronMultiWindowsConvWPadOCL(void) {};
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &windows[],
                          uint step, uint window_out, uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronMultiWindowsConvWPadOCL;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual uint               GetWindow(void) const { return aiWindows[0]; }
   virtual uint               GetWindowsSize(void) const { return aiWindows.Size(); }
   virtual uint               GetWindowOut(void) const { return iWindowOut; }
   virtual uint               GetUnits(void) const { return Neurons()/(iVariables*GetWindowsSize()*iWindowOut); }
  };

该类对与OpenCL交互的全部复杂逻辑做了抽象封装,让多窗口卷积模块可以无缝集成到整体模型架构当中。该类完整源代码以及全部方法的实现代码,均放在文章附件中,供有兴趣深入研究实现细节的读者查阅。

遗憾的是,每一篇文章都存在篇幅限制,本文的可用篇幅至此已经完结。所实现各项技术的性能评估将放在下一篇文章中展开。


结论

本文继续完成Mamba4Cast框架作者提出的算法实现工作,重点围绕两大核心组件:时间感知输入嵌入与带零填充的多窗口卷积。CMamba4CastEmbedding类演示了如何将原始特征投影与谐波时间编码在同一个模块内部完成融合。同时,三组OpenCL卷积核:FeedForwardMultWinConvWPadCalcHiddenGradientMultWinConvWPadUpdateWeightsMultWinConvAdamWPad,展示如何高效并行处理多个卷积窗口,并且在整个训练流程中维持完整可微分特性。

我们的工作不只关注性能指标本身。通过严谨的偏移量管理、执行线程之间均衡的负载分配,以及借助本地归约完成高效梯度聚合,该卷积模块真正具备了大规模扩展能力。与此同时,整体逻辑保持清晰透明:CNeuronMultiWindowsConvWPadOCL类封装所有OpenCL平台相关实现细节,这套方案可以轻松嵌入任意模块。

在下一篇文章中,我们会把全部已开发组件组装为完整模型,使用真实历史市场数据开展训练,并对上述技术方案的实际效果做实证评估。


相关链接


本文中用到的程序

# 名称 类型 说明
1 Research.mq5 EA 用于收集数据集的EA
2 ResearchRealORL.mq5
EA
基于Real-ORL方法采集数据集的EA
3 Study.mq5 EA 用于离线模型训练的EA
4 StudyOnline.mq5
EA
用于在线模型训练的EA
4 Test.mq5 EA 用于模型测试的EA
5 Trajectory.mqh 类库 系统状态与模型架构描述结构
6 NeuroNet.mqh 类库 用于创建神经网络的类库
7 NeuroNet.cl OpenCL程序代码

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

附加的文件 |
MQL5.zip (2754.59 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
从基础到中级:对象事件(三) 从基础到中级:对象事件(三)
在本文中,我们将为下一篇文章所涵盖的内容奠定基础。我们还将探讨如何使 OBJ_LABEL 对象完全可交互,以便进行编辑和移动。换句话说,我们无需打开“对象属性”对话框,即可更改 OBJ_LABEL 对象的文本和位置。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
开发多币种 EA(第 28 部分):添加平仓管理器 开发多币种 EA(第 28 部分):添加平仓管理器
在并行运行多个策略时,您可能希望定期关闭所有持仓,并重新启动策略。现有代码仅允许通过手动操作来实现这一行为。让我们尝试将这一部分自动化。