English Русский Español 日本語 Português
preview
交易中的神经网络:二维连接空间模型(Chimera)

交易中的神经网络:二维连接空间模型(Chimera)

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

概述

时间序列建模是一项复杂的任务,广泛应用在多个领域,包括医学、金融市场、和能源系统。开发通用时间序列模型的主要挑战包括:

  1. 考虑多尺度依赖关系,包括短期自相关性、季节性、和长期趋势。这需要运用灵活且强大的架构。
  2. 多变量时间序列的自适应处理,其中变量间关系可能是动态的、且非线性。这需要参考上下文依赖性交互机制。
  3. 最小化手工数据预处理的需求,确保无需大量参数调谐,即可自动识别结构形态。
  4. 计算效率,尤其是当处理长序列时,这需要模型架构优化,以便高效利用计算资源,并降低训练成本。

经典统计方法需要大量原产数据的预处理,且往往难以充分捕捉复杂的非线性依赖关系。深度神经网络架构已展现出高度的表现力,但基于变换器的模型具有二次方计算复杂度,令其难以应用在拥有大量特征的多变量时间序列。进而,这样的模型往往难以区分季节性和长期分量,或依赖僵化的先验假设,限制了它们在各种实践场景下的适应性。

针对这些问题在论文《Chimera:多变量时间序列有效建模的二维状态空间模型》中提出了一种方式。Chimera 框架是一个二维状态空间模型(2D-SSM),沿时态轴、及可变轴两者应用线性变换。Chimera 由三个主要组成部分组成:时间维度上的状态空间模型、变量维度上的模型、以及跨维度的变换。参数化基于紧凑的对角矩阵,令其能够复现经典统计方法和现代 SSM 架构。

此外,Chimera 还协同自适应离散化技术,参考动态系统的季节性形态和特征。

Chimera 的作者评估了该框架在各种多变量时间序列任务中的绩效,包括分类、预测、和异常检测。实验结果表明,Chimera 达成的精度可与新潮方法媲美,甚至超越,同时降低了整体计算成本。



Chimera(奇美拉)算法

状态空间模型(SSM)因其简洁性和表达力强,在针对复杂依赖关系(包括自回归关系)建模方面扮演着重要角色。这些模型所代表的系统,其当前状态依赖于所观测环境的之前状态。然而,传统上,SSM 描述的系统状态依赖单一变量(例如时间)。这限制了它们在多变量时间序列中的适用性,因其依赖关系必须抓取时态和变量两者。

多变量时间序列本身就很复杂,所需方法要具备同时针对多个变量间的相互依赖关系建模能力。此类任务的经典二维状态空间模型(2D-SSM)相比现代深度学习方法面临若干局限性。以下是重点:

  1. 约束线性依赖关系。传统的 2D-SSMs 仅能模拟线性关系,这限制了它们表述真实多变量时间序列中复杂非线性依赖关系的能力。
  2. 离散模型解析度。这些模型往往有预定义的解析度,无法自动适应数据特征的变化,降低了它们在针对季节性、或可变解析度形态进行建模的效果。
  3. 难以配合大数据集。实际上,2D-SSM 往往不足以处理大体量数据,限制了其实用性。
  4. 静态参数更新。经典的更新机制是固定的,未能考虑随时间演变的动态依赖关系。这在数据不断变化、需要自适应方式的应用中是一个重大限制。

对比而言,近年来快速发展的深度学习方法,具有克服这许多局限的潜力。它们能够针对复杂的非线性依赖关系和时间动态进行建模,令其在多变量时间序列分析中颇具前景。

Chimera 中,2D-SSMs 用于针对多元时间序列建模,其中第一轴对应时间,第二轴对应变量。每个状态都取决于时间和变量。第一步是将连续的 2D-SSM 转换为离散形式,参考步长 Δ1 和 Δ2,代表沿各轴的信号解析度。使用 零阶保持ZOH)方法,原始数据可被离散化为:

其中 tv 分别代表沿时间维度和变量维度上的索引。该表达式能用更简单的形式表示。

在该表述中:hv,t(1) — 是一个携带时间信息的隐藏状态(每个状态依赖于同一变量的前一个时间步骤),而 A1A2 分别控制依据过去跨时间和跨变量信息的重视度。之后 hv,t(2) 是一个隐藏状态,携带跨变量信息(每个状态依赖同一时间步骤的其它变量)。

时间序列数据常常从底层连续过程中抽样。在这种情况下,Δ1 能解释为时间解析度、或抽样频率。沿变量轴的离散化本身是离散的,虽不直观,但却必不可少。在 2D-SSMs,离散化与 RNN 门机制密切关联,提供模型归一化和期望属性,像是解析度不变性。

参数为 ({Ai}, {Bi}, {Ci}, kΔ1, Δ2) 的 2D 离散 SSM 在时间上的演化速度比参数为 ({Ai}, {Bi}, {Ci}, Δ1Δ2) 的 2D 离散 SSMk 倍,且 t 倍于 ({Ai}, {Bi}, {Ci}, kΔ1, Δ2)。所以,Δ1 能作为模型捕获的依赖长度的控制器。基于上述描述,我们看到时间轴上的离散化是解析度、或抽样频率的设定。较小的 Δ1 捕捉长期的进度,较大的 Δ1 捕捉季节性形态。

沿变量轴的离散化类似于 RNN 门,其中 Δ2 控制模型的上下文长度。较大的 Δ2 值结果是更小的上下文窗口,减少变量间的交互;而较小的 Δ2 值则强调内部变量的依赖关系。

为了强化表达力,并启用自回归,隐藏状态 hv,t(1) 会携带以往的时态信息。作者约束矩阵 A1A2 为结构化形式。即便简单的对角矩阵A3A4 也能有效地合并跨变量信息。

由于 2D-SSMs 沿变量维度存在因果关系(该维度缺乏内在顺序),Chimera 沿特征维度使用分离的前向和后向模块来解决信息流的限制。

类似于有效的 1D-SSMs,数据无关的行事能解释为内核K 的卷积。这样能够经由并行化实现更快的训练,并将 Chimera 与近期基于时间序列架构的卷积连接。

如早前讨论,参数 A1A2 控制依据以往跨时态和跨变异信息的重视度。类似地,Δ1B1 管控依据当前和历史输入的重视度。这些数据无关参数代表全局系统特征。但在复杂系统中,重视度取决于当前输入。因此,这些参数必须是原始数据的函数。所分析参数依赖性提供了类似变换器的机制,针对每个输入集,自适应选择和过滤无关信息。此外,根据数据,模型应可依据混合变体信息自适应学习。令参数依赖于输入数据进一步解决了这一问题,允许模型混合相关参数,并过滤掉无关参数,从而据感兴趣的变量建模。Chimera 的技术贡献之一是遵照输入函数 𝐱v,t 构造出 Bi, Ci 和 Δi

Chimera2D-SSM 堆叠,层间具有非线性。为了强化上述 2D-SSMs 的表现力和可能性,类似于深度 SSM 模型,所有参数都可加以训练,并在每层使用多个 2D-SSM,各自其责。

Chimera 遵循标准的时间序列分解,且趋势和季节形态分离。它还利用 2D-SSMs 的独特特性,有效捕捉这些分量。

下面提供了 Chimera 框架的原始可视化。

作者的 Chimera 框架可视化。



实现 MQL5 版本

在回顾了 Chimera 框架的理论层面之后,我们转去按自己对所提议方式的解释实现。在本章节里,我们利用 MQL5 编程语言能力,实证 Chimera 概念的解释。然而,在继续编码之前,我们需要仔细设计模型架构,从而确保其灵活性、高效性、和适应性,并适配各种类型的数据。

架构方案


Chimera 框架的关键成分之一是隐藏状态集合注意力矩阵 A{1,…,4}。作者曾提议使用带有扩展的对角矩阵,其可降低可学习参数的数量,且计算复杂度更低。该方式显著降低了资源消耗,并为模型训练提速。

然而,该方案亦有局限性。使用对角矩阵对模型施加了约束,因为它仅能分析序列中连续元素之间的局部依赖关系。这约束了其表现力,及捕捉复杂形态的能力。因此,按我们的解释,我们使用完全可训练矩阵。而这增加了参数的数量,但大幅强化了模型的适应性,令其能够捕捉数据中更复杂的依赖关系。

同时,我们的矩阵方式保留了原始设计的核心理念 — 矩阵可训练,但不直接依赖于输入数据。这就令模型维持更通用,这对多变量时间序列分析任务尤为重要。

另一个关键层面是将这些矩阵集成到计算过程之中。如理论章节所讨论,注意力矩阵会乘以模型的隐藏状态,遵循类似于神经层的原理。我们提议将它们作为卷积神经网络层来实现,其中每个注意力矩阵表示为可训练的参数张量。集成到标准神经架构当中,可利用已有的优化算法。

甚至,为能同时并行计算所有四个注意力矩阵,我们将它们合并到一个单一级联张量,这也需要将两个隐藏状态矩阵合并为一个单一张量。

尽管有其优势,但该方式并非普遍适用于 2D-SSM 中的其它参数型矩阵。一个限制是固定矩阵结构,其在处理复杂多变量数据时降低了灵活性。为了提升模型表达力,我们使用上下文相关的矩阵 Bi, Ci 和 Δi,这些矩阵动态适配输入数据,允许深度分析时态依赖性。

上下文依赖矩阵自输入数据生成,令模型能够参考数据结构,并根据序列特征调整参数。该方式不仅能分析局部依赖性,还能分析全局趋势,这对预测和时间序列任务至关重要。

遵循框架作者的建议,这些矩阵通过专门的神经层实现,负责基于上下文调整参数。

下一步是在 2D-SSM 模型中组织复杂的数据交互。由于多变量数据结构需要优化过程,故需要高效的资源管理。为了满足计算效率和性能需求,我们决定实现该操作时,作为一个分离的 OpenCL 内核。

该方式提供若干优势。首先,在 GPU 上并行执行能显著加速数据处理,并降低延迟。这对于大数据尤为关键,其中顺序计算速度较慢。其次,由于硬件加速,OpenCL 支持高效并行化,能实时处理复杂的时间序列。

扩展 OpenCL 程序


架构设计完毕后,下一步就是实现其代码。首先,我们需要修改 OpenCL 程序,优化计算操作,并确保与模型组件的有效交互。我们创建一个内核 SSM2D_FeedForward,处理可训练的 2D-SSM参数与输入数据之间的复杂交互。

该方法接收指向数据的缓冲区指针,其中包含所有模型参数,及时间和变量上下文输入投影。

__kernel void SSM2D_FeedForward(__global const float *ah,
                                __global const float *b_time,
                                __global const float *b_var,
                                __global const float *px_time,
                                __global const float *px_var,
                                __global const float *c_time,
                                __global const float *c_var,
                                __global const float *delta_time,
                                __global const float *delta_var,
                                __global       float *hidden,
                                __global       float *y
                               )
  {
   const size_t n = get_local_id(0);
   const size_t d = get_global_id(1);
   const size_t n_total = get_local_size(0);
   const size_t d_total = get_global_size(1);

在内核内部,我们首先识别在二维任务空间中的当前线程。第一维对应序列长度,第二维对应特征维度。单个特征的所有序列元素都被分组为工作组。

在数据准备期间,将可训练参数和输入数据的投影对齐非常重要,然后再传给内核。

接下来,我们利用更新的信息计算两个上下文中的隐藏状态。我们会将结果保存到对应的数据缓冲区之中。

//--- Hidden state
   for(int h = 0; h < 2; h++)
     {
      float new_h = ah[(2 * n + h) * d_total + d] + ah[(2 * n_total + 2 * n + h) * d_total + d];
      if(h == 0)
         new_h += b_time[n] * px_time[n * d_total + d];
      else
         new_h += b_var[n] * px_var[n * d_total + d];
      hidden[(h * n_total + n)*d_total + d] = IsNaNOrInf(new_h, 0);
     }
   barrier(CLK_LOCAL_MEM_FENCE);

之后,我们同步工作组线程,在于后续操作需要整个分组的结果。

然后我们计算模型输出。为此,我们把上下文与计算出的隐藏状态离散矩阵相乘。为了执行该操作,我们组织一个环路,其中我们乘以时间和变量上下文中相应的矩阵元素。然后我们将来自两个上下文的结果汇总。

//--- Output
   uint shift_c = n;
   uint shift_h1 = d;
   uint shift_h2 = shift_h1 + n_total * d_total;
   float value = 0;
   for(int i = 0; i < n_total; i++)
     {
      value += IsNaNOrInf(c_time[shift_c] * delta_time[shift_c] * hidden[shift_h1], 0);
      value += IsNaNOrInf(c_var[shift_c] * delta_var[shift_c] * hidden[shift_h2], 0);
      shift_c += n_total;
      shift_h1 += d_total;
      shift_h2 += d_total;
     }

现在我们只需将接收到的数值保存到结果缓冲区的对应元素当中。

//---
   y[n * d_total + d] = IsNaNOrInf(value, 0);
  }

接下来,我们需要安排反向传播过程。我们将利用相应的神经层来优化参数。然后,为了分派误差梯度,我们将创建 SSM2D_CalcHiddenGradient 内核 — 在其主体中,我们将实现一个算法与上述算法相逆。

内核参数包括指向同一矩阵集的指针,并补充误差梯度缓冲区。为了避免缓冲区数量众多造成混淆,我们使用前缀 grad_ 表示对应误差梯度的缓冲区。

__kernel void SSM2D_CalcHiddenGradient(__global const float *ah,
                                       __global       float *grad_ah,
                                       __global const float *b_time,
                                       __global       float *grad_b_time,
                                       __global const float *b_var,
                                       __global       float *grad_b_var,
                                       __global const float *px_time,
                                       __global       float *grad_px_time,
                                       __global const float *px_var,
                                       __global       float *grad_px_var,
                                       __global const float *c_time,
                                       __global       float *grad_c_time,
                                       __global const float *c_var,
                                       __global       float *grad_c_var,
                                       __global const float *delta_time,
                                       __global       float *grad_delta_time,
                                       __global const float *delta_var,
                                       __global       float *grad_delta_var,
                                       __global const float *hidden,
                                       __global const float *grad_y
                                      )
  {
//---
   const size_t n = get_global_id(0);
   const size_t d = get_local_id(1);
   const size_t n_total = get_global_size(0);
   const size_t d_total = get_local_size(1);

该内核与前向通验内核在同一任务空间执行。然而,在这种情况下,线程会按特征维度分组到工作组。

在开始计算之前,我们初始化若干个局部变量,存储中间值、及其在数据缓冲区中的偏移量。

//--- Initialize indices for data access
   uint shift_c = n;
   uint shift_h1 = d;
   uint shift_h2 = shift_h1 + n_total * d_total;
   float grad_hidden1 = 0;
   float grad_hidden2 = 0;

接下来,我们组织一个环路,根据对模型最终输出的贡献,将来自输出缓冲区的误差梯度分派给隐藏状态,以及上下文矩阵和离散化矩阵。同时,跨时间和上下文分派误差梯度。

//--- Backpropagation: compute hidden gradients from y
   for(int i = 0; i < n_total; i++)
     {
      float grad = grad_y[i * d_total + d];
      float c_t = c_time[shift_c];
      float c_v = c_var[shift_c];
      float delta_t = delta_time[shift_c];
      float delta_v = delta_var[shift_c];
      float h1 = hidden[shift_h1];
      float h2 = hidden[shift_h2];
      //-- Accumulate gradients for hidden states
      grad_hidden1 += IsNaNOrInf(grad * c_t * delta_t, 0);
      grad_hidden2 += IsNaNOrInf(grad * c_v * delta_v, 0);
      //--- Compute gradients for c_time, c_var, delta_time, delta_var
      grad_c_time[shift_c] += grad * delta_t * h1;
      grad_c_var[shift_c]  += grad * delta_v * h2;
      grad_delta_time[shift_c] += grad * c_t * h1;
      grad_delta_var[shift_c]  += grad * c_v * h2;
      //--- Update indices for the next element
      shift_c += n_total;
      shift_h1 += d_total;
      shift_h2 += d_total;
     }

然后,我们将误差梯度分派到注意力矩阵。

//--- Backpropagate through hidden -> ah, b_time, px_time
   for(int h = 0; h < 2; h++)
     {
      float grad_h = (h == 0) ? grad_hidden1 : grad_hidden2;
      //--- Store gradients in ah (considering its influence on two elements)
      grad_ah[(2 * n + h) * d_total + d] = grad_h;
      grad_ah[(2 * (n_total + n) + h) * d_total + d] = grad_h;
     }

然后把它传递给输入数据的投影。

//--- Backpropagate through px_time and px_var (influenced by b_time and b_var)
   grad_px_time[n * d_total + d] = grad_hidden1 * b_time[n];
   grad_px_var[n * d_total + d] = grad_hidden2 * b_var[n];

矩阵 Bi 的误差梯度需要跨所有维度上聚合。因此,我们首先把相应的误差梯度缓冲区清零,并同步工作组的线程。

   if(d == 0)
     {
      grad_b_time[n] = 0;
      grad_b_var[n] = 0;
     }
   barrier(CLK_LOCAL_MEM_FENCE);

然后,我们把来自工作组各个线程的数值汇总。

//--- Sum gradients over all d for b_time and b_var
   grad_b_time[n] += grad_hidden1 * px_time[n * d_total + d];
   grad_b_var[n] += grad_hidden2 * px_var[n * d_total + d];
  }

这些操作的结果会写入相应的全局数据缓冲区,完成内核的执行。

我们在 OpenCL 端的实现工作至此完毕。完整的源代码已在附件中提供。

2D-SSM 对象


完成 OpenCL 侧操作之后,下一步是在主程序中构建 2D-SSM 结构。我们创建了 CNeuron2DSSMOCL 类,在其内实现了必要的算法。新类结构如下所示。 

class CNeuron2DSSMOCL  :  public CNeuronBaseOCL
  {
protected:
   uint                 iWindowOut;
   uint                 iUnitsOut;
   CNeuronBaseOCL       cHiddenStates;
   CLayer               cProjectionX_Time;
   CLayer               cProjectionX_Variable;
   CNeuronConvOCL       cA;
   CNeuronConvOCL       cB_Time;
   CNeuronConvOCL       cB_Variable;
   CNeuronConvOCL       cC_Time;
   CNeuronConvOCL       cC_Variable;
   CNeuronConvOCL       cDelta_Time;
   CNeuronConvOCL       cDelta_Variable;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      feedForwardSSM2D(void);
   //---
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradientsSSM2D(void);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuron2DSSMOCL(void)  {};
                    ~CNeuron2DSSMOCL(void)  {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window_in, uint window_out, uint units_in, uint units_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuron2DSSMOCL; }
   //---
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual bool      Clear(void) override;
  };

在该对象结构中,我们见到一套熟悉的虚拟覆盖方法,以及相对数量的内部对象。对象数量并不意外。它由模型架构决定。部分情况下,从这些对象的名称就能推断出它们的用途。在类方法的实现期间,将提供每个对象功能的更详细描述。

所有内部对象都声明为静态,允许我们将类构造和析构函数留空。该方式的优点之前已讨论过。这些声明和继承对象的初始化均在 Init 方法中执行。

bool CNeuron2DSSMOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                           uint window_in, uint window_out, uint units_in, uint units_out,
                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_out,
                                                       optimization_type, batch))
      return false;
   SetActivationFunction(None);

该方法接收若干常量作为参数,定义所创建对象的架构。这些包括输入数据的维度和期望输出:分别是 {units_in, window_in} 和 {units_out, window_out}。

在方法内,我们首先以期望的输出维度调用父类的方法。父类方法已实现了继承对象和接口所需的控制模块和初始化算法。执行成功后,我们将结果张量维度存储在内部变量之中。

   iWindowOut = window_out;
   iUnitsOut = units_out;

如早前所述,在 OpenCL 端构造内核时,两个上下文的输入投影必须具被可比形状。在我们的实现中,我们会将它们与结果张量的维数对齐。我们首先创建时间-上下文输入投影模型。

为了保留多变量时间序列单位序列中的信息,我们把一维序列执行独立投影到目标大小。重点要注意,以矩阵形式接收输入数据,其中行对应时间步骤。因此,为了方便处理单元序列,我们首先转置输入矩阵。

//---
   int index = 0;
   CNeuronConvOCL *conv = NULL;
   CNeuronTransposeOCL *transp = NULL;
//--- Projection Time
   cProjectionX_Time.Clear();
   cProjectionX_Time.SetOpenCL(OpenCL);
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, index, OpenCL, units_in, window_in, optimization, iBatch) ||
      !cProjectionX_Time.Add(transp))
     {
      delete transp;
      return false;
     }

然后仅应用卷积层来调整单变量序列的维度。

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, units_in, units_in, iUnitsOut, window_in, 1,
                                                       optimization, iBatch) ||
      !cProjectionX_Time.Add(conv))
     {
      delete conv;
      return false;
     }

接下来,我们沿特征维度投影数据。为此,我们执行逆转置

   index++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, index, OpenCL, window_in, iUnitsOut, optimization, iBatch) ||
      !cProjectionX_Time.Add(transp))
     {
      delete transp;
      return false;
     }

并应用一个卷积投影层。

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window_in, window_in, iWindowOut, iUnitsOut, 1,
                                                          optimization, iBatch) ||
      !cProjectionX_Time.Add(conv))
     {
      delete conv;
      return false;
     }

类似地,我们创建特征上下文输入投影,先沿可变轴投影,然后沿时间轴转置和投影。

//--- Projection Variables
   cProjectionX_Variable.Clear();
   cProjectionX_Variable.SetOpenCL(OpenCL);
   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window_in, window_in, iUnitsOut, units_in, 1,
                                                        optimization, iBatch) ||
      !cProjectionX_Variable.Add(conv))
     {
      delete conv;
      return false;
     }
   index++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, index, OpenCL, units_in, iUnitsOut, optimization, iBatch) ||
      !cProjectionX_Variable.Add(transp))
     {
      delete transp;
      return false;
     }
   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, units_in, units_in, iWindowOut, iUnitsOut, 1,
                                                        optimization, iBatch) ||
      !cProjectionX_Variable.Add(conv))
     {
      delete conv;
      return false;
     }

输入投影模型初始化之后,我们转到其它内部对象。我们首先初始化隐藏状态对象。该对象仅作为数据容器使用,不包含可训练参数。然而,它必须足够大,以便存储两种上下文的隐藏状态数据。

//--- HiddenState
   index++;
   if(!cHiddenStates.Init(0, index, OpenCL, 2 * iUnitsOut * iWindowOut, optimization, iBatch))
      return false;

接下来,我们初始化隐藏状态注意力矩阵。如前所述,所有四个矩阵都是在单一卷积层内实现。这支持并行执行。

重点要注意,该层的输出应提供隐藏状态与四个独立矩阵相乘:两个在时间上下文中操作,两个在特征上下文中操作。为达成这一点,我们定义了含有两倍滤波器数量的卷积层作为输入窗口,对应两个注意力矩阵。并指定处理两个独立序列的层,分别对应时间和特征上下文。回忆一下,卷积层为独立序列使用独立的滤波矩阵。这种设置产生了四个注意力矩阵,每对在不同的上下文中操作。

//--- A*H
   index++;
   if(!cA.Init(0, index, OpenCL, iWindowOut, iWindowOut, 2 * iWindowOut, iUnitsOut, 2,
                                                                optimization, iBatch))
      return false;

太大的注意力参数会导致梯度膨胀。故此,我们在随机初始化后将参数缩放十倍。

if(!SumAndNormilize(cA.GetWeightsConv(), cA.GetWeightsConv(), cA.GetWeightsConv(),
                                               iWindowOut, false, 0, 0, 0, 0.05f))
   return false;

下一步是生成自适应上下文相关矩阵 Bi, Ci 和 Δi,在我们的实现中,其是输入数据的函数。这些矩阵由卷积层生成,取得对应上下文的输入投影,并输出所需的矩阵。

//--- B
   index++;
   if(!cB_Time.Init(0, index, OpenCL, iWindowOut, iWindowOut, 1, iUnitsOut, 1, 
                                                         optimization, iBatch))
      return false;
   cB_Time.SetActivationFunction(TANH);
   index++;
   if(!cB_Variable.Init(0, index, OpenCL, iWindowOut, iWindowOut, 1, iUnitsOut, 1, 
                                                            optimization, iBatch))
      return false;
   cB_Variable.SetActivationFunction(TANH);

该方式类似于 RNN 门。我们使用双曲切线作为 Bi 和 Ci 的激活函数,允许正依赖和负依赖关系。

//--- C
   index++;
   if(!cC_Time.Init(0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, 1,
                                                                optimization, iBatch))
      return false;
   cC_Time.SetActivationFunction(TANH);
   index++;
   if(!cC_Variable.Init(0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, 1,
                                                                    optimization, iBatch))
      return false;
   cC_Variable.SetActivationFunction(TANH);

Δi 矩阵实现可训练离散化,且不得包含负值。为此,我们使用 SoftPlus 作为激活函数,它是 ReLU 的平滑模拟。

//--- Delta
   index++;
   if(!cDelta_Time.Init(0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, 1,
                                                                    optimization, iBatch))
      return false;
   cDelta_Time.SetActivationFunction(SoftPlus);
   index++;
   if(!cDelta_Variable.Init(0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, 1,
                                                                        optimization, iBatch))
      return false;
   cDelta_Variable.SetActivationFunction(SoftPlus);
//---
   return true;
  }

所有内部对象初始化之后,方法会返回一个逻辑结果至调用程序。

我们今天取得了显著进展,但工作尚未完成。建议继续下一篇文章前稍事休息,下一篇我们将完成必要对象的构造,将它们整合进模型,并据真实历史数据测试已实现方式的有效性。



结束语

本文探讨了 Chimera 二维状态空间模型框架,该框架引入了针对多变量时间序列建模的新方法,它在时间维度和特征维度上都存在依赖关系。Chimera 采用二维状态空间模型(2D-SSM),能够高效捕捉长期趋势、以及季节性形态。

在实施章节,我们开始按照我们对框架的解释实现 MQL5 版本。尽管已有进展,但实现尚未完工。在下一篇文章中,我们将继续构建所提议方式,并验证已实现方案在真实历史数据集上的有效性。


参考


文章中所用程序

#名称类型说明
1Research.mq5智能系统收集样本的智能系统
2ResearchRealORL.mq5
智能系统
利用 Real-ORL 方法收集样本的智能系统
3Study.mq5智能系统模型训练智能系统
4Test.mq5智能系统模型测试智能系统
模型测试智能系统Trajectory.mqh类库系统状态和模型架构描述结构
6NeuroNet.mqh类库创建神经网络的类库
7NeuroNet.cl代码库OpenCL 程序代码

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

附加的文件 |
MQL5.zip (2458.91 KB)
从新手到专家:使用 MQL5 制作动画新闻标题(二) 从新手到专家:使用 MQL5 制作动画新闻标题(二)
今天,我们又向前迈进了一步,整合了一个外部新闻 API 作为我们的 News Headline EA 的头条新闻来源。在这个阶段,我们将探索各种新闻来源 —— 包括成熟的和新兴的 —— 并学习如何有效地访问它们的 API。我们还将介绍如何将检索到的数据解析成适合在我们的 EA 交易中显示的格式。加入讨论,我们将探索直接在图表上访问新闻标题和经济日历的好处,所有这些都在一个紧凑、不干扰用户的界面中。
基于机器学习构建均值回归策略 基于机器学习构建均值回归策略
本文提出了另一种基于机器学习的原创交易系统构建方法,该方法运用聚类分析和交易标注来设计均值回归策略。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
从新手到专家:使用 MQL5 制作动画新闻标题(一) 从新手到专家:使用 MQL5 制作动画新闻标题(一)
在 MetaTrader 5 终端上进行交易时,新闻可访问性是一个关键因素。虽然有很多新闻 API 可用,但许多交易者在访问这些 API 并将其有效集成到他们的交易环境中时仍面临挑战。在本次讨论中,我们的目标是开发一种简化的解决方案,将新闻直接呈现在图表上 —— 也就是最需要新闻的地方。我们将通过构建一个新闻标题 EA 来实现这一目标,该 EA 可以监控并显示来自 API 源的实时新闻更新。