English Русский Español Deutsch 日本語 Português
preview
交易中的神经网络:搭配区段注意力的参数效率变换器(终篇)

交易中的神经网络:搭配区段注意力的参数效率变换器(终篇)

MetaTrader 5交易系统 |
224 4
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

上一篇文章中,我们探讨了 PSformer 框架的理论层面,其在雏形变换器架构中引入了两项关键创新:参数共享(PS)机制,以及时空区段注意力(SegAtt)。

回顾一下,PSformer 的作者提出了一种基于变换器架构的编码器,拥有两级区段注意力结构。每个级别都包括一个参数共享模块,该块由三个含有残差连接的全连接层组成。该架构降低了参数总数,同时维持模型内有效的信息交换。

区段是使用补片方法生成的,其中时间序列变量被切分为补片。跨不同变量中具有相同位置的补片被分组为区段,表示单变量补片的空间扩展。这种区段可有效地将多维时间序列组织成多个区段。

在每个区段内,注意力专注于辨别局部时空关系上,而区段之间的信息整合提升了整体预测品质。

此外,在 PSformer 框架中使用 SAM 优化方法有助于在不影响性能的情况下降低过度拟合的风险。

作者在各种数据集上进行的大量实验证实了 PSformer 的高性能。在 8 项关键预测任务中的 6 个中,相比最新潮的模型,该架构达成了具有竞争力、或更优越的结果。

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

在上一篇文章中,我们开始实现所提议方式的 MQL5 版本。我们实证了 CNeuronPSBlock 类方法的算法,其中实现参数共享模块功能。现在,我们继续我们的工作,并转到构建编码器功能。


创建 PSformer 编码器对象

在转入代码实现之前,我们先简要讨论一下算法。根据框架作者的说法,输入数据首先经由 RevIn 通验。如您所知,RevIn 模块由两块组成。在模型输入处,它对原始数据进行归一化;在输出处,它将之前提取的分布参数恢复到模型结果之中。这有助于令模型输出分布与原始数据输出分布保持一致。这无疑是预测时间序列未来值的重要因素。

在本文中,如前,我们仅用到输入数据归一化,作为单独的批量归一化层实现。原因是我们的最终目标不是预测未来的时间序列值,而是训练一个可盈利的参与者政策。由于模型一般按归一化数据表现更好,故我们对输入数据进行归一化。出于同样的原因,将来自编码器隐藏状态的归一化数据传递给参与者是合乎逻辑的。因此,当训练编码器参与者一起在环境中训练时,线性映射模块和反向 RevIn 模块都变得不必要。

当然,分阶段训练中需要映射模块和反向 RevIn 模块,其中首先训练环境状态编码器来预测所分析时间序列的后续状态,然后再分别训练参与者评论者模型。然而,即使在这种情况下,也最好将编码器的隐藏状态传递给参与者,其中包含原始数据的更紧凑和归一化的表示。

分阶段训练优缺点都有。主要优点是编码器的通用性,因为它是在生料数据上进行训练的,未与特定任务绑定。这令重复使用编码器,依据同一数据集来解决 的不同问题成为可能。

另一方面,通用模型并非总是特定任务的最优解,在于它或许无法捕捉某些特定领域的细微差别。

甚至,两个独立的训练阶段,或许总体计算成本比同时训练所有模型更高。

考虑到这些因素,我们选择了同时训练模型,减少编码器架构。

数据归一化之后,PSformer 框架应用补片和数据转换模块。作者描述了一个相当复杂的转化过程。我们试着分解一下。

该模型接收多模态时间序列作为输入。暂时省略归一化,我们只关注转换过程。

首先,将多模态时间序列划分为 M 个单变量序列。然后将每个单变量序列分成 N 个长度为 P 的相等补片。接下来,将具有雷同时间索引的补片组合成区段。这会产生 N 个大小为 M×P 的补片。 

在我们的例子中,生料数据是覆盖所分析数据集深度的历史柱线的顺序描述。换言之,我们的生料数据缓冲区包含一根柱线的 M 个描述性元素。这些后随下一根柱线的 M 个元素,依此类推。因此,为了形成一个区段,我们只需要取 P 个连续的柱线描述。显然,这不需要额外的数据转换。

然后,PSformer 作者描述了将生料数据转换为 (M×PN。至于我们的实现,这能通过简单地转置输入数据张量来达成。

因此,在我们的例子中,PSformer 补片和转换模块被有效地简化为单个转置层。

另一个重要问题是如何构建顺序 PSformer 编码器层。有两种选择。运用基本方式,直接在模型架构描述中指定所需的层数,或创建一个生成所需数量内层的对象。

第一种方式令架构描述复杂化,但简化了编码器对象的创建。它还提供了配置编码器顺序层的灵活性。

第二种方式简化了架构描述,但编码器的实现变得复杂,所有内层都拥有相同的架构。

显然,第一个选项更适合含有少量编码器层的模型。而第二个更适合较深的模型。

为了做出决定,我们参考了关于编码器层数如何影响预测精度的原始研究。PSformer 作者在标准 ETTh1ETTm1 时间序列数据集上进行了实验,其中包含来自变换器的数据。每个数据点包括八个特征,诸如日期、油温、和六个类型的外部负载特性。ETTh1 的间隔是每小时,而 ETTm1 的间隔则是分钟。结果如下表所示。 

编码器层数对预测结果影响的初步研究

所提供的数据清晰所见,对于低混乱度的每小时数据集,使用单个编码器层就可达成最佳结果。对于噪声较大的分钟级数据集,三个编码器层被证明是最优的。由此,我们预计不会构建含有大量编码器层的模型。因此,我们选择第一种实现方式,为模型架构描述中每个编码器层配以更简单的编码器对象结构、及显式参数规范。

CNeuronPSformer 对象的完整结构如下所示。

class CNeuronPSformer   :  public CNeuronBaseSAMOCL
  {
protected:
   CNeuronTransposeOCL           acTranspose[2];
   CNeuronPSBlock                acPSBlocks[3];
   CNeuronRelativeSelfAttention  acAttention[2];
   CNeuronBaseOCL                cResidual;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronPSformer(void)   {};
                    ~CNeuronPSformer(void)   {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint units_count, uint segments, float rho, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const         {  return defNeuronPSformer;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual CLayerDescription* GetLayerInfo(void);
   virtual void      SetOpenCL(COpenCLMy *obj);
  };

如早前所述,PSformer 框架结合了 SAM 优化技术。因此,我们的新类从相应的全连接层继承基本功能。

此外,CNeuronPSformer 结构包括两个数据转置层,其中执行正向和逆向数据转换功能。

新类结构还包含三个参数共享模块,和两个相对注意力模块,我们之前已把 SAM 优化功能集成到其中。这也许是我们与原始 PSformer 算法的最大偏差。

PSformer 的作者使用参数共享模块来生成 QueryKeyValue 实体。在 PS 模块中,参数矩阵的大小为 N×N。这意味着所有三个实体都使用相同的张量。

在我们的实现中,我们稍微复杂化了编码器架构。PS 模块仅用于初步数据准备,而依赖性分析则在更高级的相对注意力模块中执行。

所有内部对象都声明为静态,允许我们将类构造函数和析构函数保持为空。所有声明和继承的对象,都在 Init 方法中执行初始化。该方法的参数包括定义正在创建层的架构的全部关键常量,即:

  • window — 描述一个序列元素的向量大小
  • units_count — 历史数据的深度(序列长度)
  • segments — 生成的区段数量
  • RHO — 模糊区域系数

bool CNeuronPSformer::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                           uint window, uint units_count, uint segments, float rho, 
                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(units_count % segments > 0)
      return false;

在初始化方法中,我们首先设置一个控制模块来验证序列长度是否能被区段数量整除。然后,我们调用父类同名方法,其已实现对常量的进一步验证、及继承对象的初始化。

   if(!CNeuronBaseSAMOCL::Init(numOutputs, myIndex, open_cl, window * units_count, rho, optimization_type, batch))
      return false;

接下来,我们判定单个区段的大小。

   uint count = Neurons() / segments;

然后我们继续初始化新声明的内部对象。首先,我们初始化输入数据转置层。转置矩阵中的行数设置为区段数,而行大小设置为区段中的元素总数。 

   if(!acTranspose[0].Init(0, 0, OpenCL, segments, count, optimization, iBatch))
      return false;
   acTranspose[0].SetActivationFunction(None);

该层未指定激活函数。

虽然转置层不需要指定激活函数,但若需要,该层的前馈算法会提供与源数据对象激活函数的同步。

接下来,我们初始化第一个参数共享模块。为此,我们调用显式对象初始化方法。

   if(!acPSBlocks[0].Init(0, 1, OpenCL, segments, segments, units_count / segments, 1, fRho, optimization, iBatch))
      return false;

然后,我们创建一个循环来初始化注意力模块,及其余的参数共享模块。

   for(int i = 0; i < 2; i++)
     {
      if(!acAttention[i].Init(0, i + 2, OpenCL, segments, segments, units_count / segments, 2, optimization, iBatch))
         return false;
      if(!acPSBlocks[i + 1].InitPS((CNeuronPSBlock*)acPSBlocks[0].AsObject()))
         return false;
     }

其余参数共享模块的初始化方式与第一个 PS 模块相同,指向共享参数缓冲区的指针、并相应地复制它们的动量。

接下来,我们初始化残差连接记录层。此处我们使用标准的全连接层,因为我们仅需其数据缓冲区来存储中间计算结果。

   if(!cResidual.Init(0, 4, OpenCL, acAttention[1].Neurons(), optimization, iBatch))
      return false;
   if(!cResidual.SetGradient(acAttention[1].getGradient(), true))
      return false;
   cResidual.SetActivationFunction((ENUM_ACTIVATION)acAttention[1].Activation());

为了减少数据复制操作,我们将该层的梯度缓冲区替换为最终注意力模块的梯度缓冲区。确保它们的激活函数是同步的。

最后,我们初始化逆向转置层。

   if(!acTranspose[1].Init(0, 5, OpenCL, count, segments, optimization, iBatch))
      return false;
   acTranspose[1].SetActivationFunction((ENUM_ACTIVATION)acPSBlocks[2].Activation());

我们还将对象的接口数据缓冲区替换为最终转置层的相应缓冲区。

   if(!SetOutput(acTranspose[1].getOutput(), true) ||
      !SetGradient(acTranspose[1].getGradient(), true))
      return false;
//---
   return true;
  }

此后,初始化方法完成,将逻辑结果返回给调用者。

下一阶段是构建前馈算法,在 feedForward 方法中实现。该方法应当相对清晰,因为主要功能都驻留在先前实现的内部对象之中。

该方法接收指向源数据对象的指针,该指针立即传递到内部转置层进行初始转换。 

bool CNeuronPSformer::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Dimension Transformation
   if(!acTranspose[0].FeedForward(NeuronOCL))
      return false;

然后,我们迭代遍历两个区段注意力模块。

//--- Segment Attention
   CObject* prev = acTranspose[0].AsObject();
   for(int i = 0; i < 2; i++)
     {
      if(!acPSBlocks[i].FeedForward(prev))
         return false;
      if(!acAttention[i].FeedForward(acPSBlocks[i].AsObject()))
         return false;
      prev = acAttention[i].AsObject();
     }

在每次循环迭代中,我们依次调用参数共享模块、及相对注意力模块的方法。

循环的所有迭代成功完成之后,我们将残差连接添加到原始数据当中,并将结果保存在内层缓冲区 cResiful 之中。

//--- Residual Add
   if(!SumAndNormilize(acTranspose[0].getOutput(), acAttention[1].getOutput(), cResidual.getOutput(),
                                                      acAttention[1].GetWindow(), false, 0, 0, 0, 1))
      return false;

但注意,对于残差连接,我们获取转换后的生料数据,即转置层之后。这可确保为剩余连接保留数据结构。

然后,生成的数据经由最终的参数共享模块通验。

//--- PS Block
   if(!acPSBlocks[2].FeedForward(cResidual.AsObject()))
      return false;

然后我们执行逆向数据变换。

//--- Inverse Transformation
   if(!acTranspose[1].FeedForward(acPSBlocks[2].AsObject()))
      return false;
//---
   return true;
  }

得益于对象接口和最终转置层之间共享缓冲区指针,逆向变换的结果将直接写入接口缓冲区,无需额外的数据复制。因此,在逆向变换之后,前馈方法简单地将布尔结果返回给调用者。

一旦前向通验完成,我们转到在 calcInputGradientsupdateInputWeights 方法中实现的后向通验过程。前者分派误差梯度,而后者更新模型参数。

calcInputGradients 方法接收指向源数据对象的指针,我们必须将反映输入数据对最终结果影响的误差梯度传递到该对象当中。

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

首先,我们检查接收指针的有效性 — 如果无效,则进一步处理毫无意义。检查通过后,我们按照前馈通验的相反顺序遍历对象的内部层顺序反向传播误差梯度。

   if(!acPSBlocks[2].calcHiddenGradients(acTranspose[1].AsObject()))
      return false;
//---
   if(!cResidual.calcHiddenGradients(acPSBlocks[2].AsObject()))
      return false;
//---
   if(!acPSBlocks[1].calcHiddenGradients(acAttention[1].AsObject()))
      return false;
   if(!acAttention[0].calcHiddenGradients(acPSBlocks[1].AsObject()))
      return false;
   if(!acPSBlocks[0].calcHiddenGradients(acAttention[0].AsObject()))
      return false;
//---
   if(!acTranspose[0].calcHiddenGradients(acPSBlocks[0].AsObject()))
      return false;

到达输入数据转换层后,我们必须添加残差连接梯度。这或许以两种方式发生,具体取决于源数据对象的激活函数。如果不存在激活函数,我们简单地将两个数据缓冲区的值相加即可。

   if(acTranspose[0].Activation() == None)
     {
      if(!SumAndNormilize(acTranspose[0].getGradient(), cResidual.getGradient(), acTranspose[0].getGradient(),
                                                               acAttention[1].GetWindow(), false, 0, 0, 0, 1))
         return false;
     }

如果存在激活函数,我们首先通过激活函数的导数调整误差梯度,然后再对数据求和。

   else
     {
      if(!DeActivation(acTranspose[0].getOutput(), cResidual.getGradient(), acTranspose[0].getPrevOutput(),
                                                                                     acTranspose[0].Activation()) ||
         !SumAndNormilize(acTranspose[0].getGradient(), acTranspose[0].getPrevOutput(), acTranspose[0].getGradient(), 
                                                                      acAttention[1].GetWindow(), false, 0, 0, 0, 1))
         return false;
     }

在方法结束时,我们进行逆向变换,将误差梯度传播到输入级,然后将布尔结果返回给调用程序。

   if(!NeuronOCL.calcHiddenGradients(acTranspose[0].AsObject()))
      return false;
//---
   return true;
  }

为了完成反向通验,我们必须更新模型参数,以便降低预测误差。回想一下,我们运用 SAM 优化技术进行参数更新。如早前所述,SAM 需要使用扰动模型参数进行第二次前向通验,这会改变缓冲区的数值。虽然这不会影响当前层的操作,但它能扭曲后续层中的参数更新。因此,我们按逆向前馈通验顺序更新内层。这允许在修改前几层的任何缓冲区值之前,调整每个层的参数。

bool CNeuronPSformer::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!acPSBlocks[2].UpdateInputWeights(cResidual.AsObject()))
      return false;
//---
   CObject* prev = acAttention[0].AsObject();
   for(int i = 1; i >= 0; i--)
     {
      if(!acAttention[i].UpdateInputWeights(acPSBlocks[i].AsObject()))
         return false;
      if(!acPSBlocks[i].UpdateInputWeights(prev))
         return false;
      prev = acTranspose[0].AsObject();
     }
//---
   return true;
  }

关于文件操作方法,应当说几句话。由于我们用到三个参数共享模块,故无需将相同的参数保存三次。保存一次就足够了。相应地,我们有以下 Save 方法。

该方法接收一个文件句柄,首先会把它传递给父类的 Save 方法。

bool CNeuronPSformer::Save(const int file_handle)
  {
   if(!CNeuronBaseSAMOCL::Save(file_handle))
      return false;

然后我们一次性保存参数共享模块。

   if(!acPSBlocks[0].Save(file_handle))
      return false;

接下来,我们循环保存注意力模块和转置层。

   for(int i = 0; i < 2; i++)
      if(!acTranspose[i].Save(file_handle) ||
         !acAttention[i].Save(file_handle))
         return false;
//---
   return true;
  }

循环完成后,我们将布尔结果返回给调用者,并结束该方法。

注意,当保存时,我们也省略了残差连接层。它没有可训练参数。因此,在该过程中不会丢失任何信息。

不过,在恢复之前保存的对象时,我们还必须恢复所有对象的结构和功能。包括保存期间跳过的那些。因此,我建议仔细查看恢复对象功能的 Load 方法。

在方法参数中,我们收到包含先前保存数据的文件句柄。我们立即将收到的句柄传递给父类的 Load 方法,其会恢复继承的对象。

bool CNeuronPSformer::Load(const int file_handle)
  {
   if(!CNeuronBaseSAMOCL::Load(file_handle))
      return false;

然后,我们按照保存时的确切顺序,恢复所保存对象。

   if(!LoadInsideLayer(file_handle, acPSBlocks[0].AsObject()))
      return false;
   for(int i = 0; i < 2; i++)
      if(!LoadInsideLayer(file_handle, acTranspose[i].AsObject()) ||
         !LoadInsideLayer(file_handle, acAttention[i].AsObject()))
         return false;

然后我们需要恢复保存期间遗失的对象的功能。首先,我们将恢复参数共享模块。一个是从文件加载的,其它的则基于加载的模块进行初始化,并复制指向共享参数缓冲区、及动量的指针。

   for(int i = 1; i < 3; i++)
      if(!acPSBlocks[i].InitPS((CNeuronPSBlock*)acPSBlocks[0].AsObject()))
         return false;

我们还初始化了残差连接记录层。它的大小等于最终相对注意力模块的输出张量。

   if(!cResidual.Init(0, 4, OpenCL, acAttention[1].Neurons(), optimization, iBatch))
      return false;
   if(!cResidual.SetGradient(acAttention[1].getGradient(), true))
      return false;
   cResidual.SetActivationFunction((ENUM_ACTIVATION)acAttention[1].Activation());

我们替换梯度缓冲区指针,从而剔除不必要的复制操作,并同步激活函数。

最后,我们将对象的接口缓冲区替换为最后一个转置层的接口缓冲区。

   if(!SetOutput(acTranspose[1].getOutput(), true) ||
      !SetGradient(acTranspose[1].getGradient(), true))
      return false;
//---
   return true;
  }

方法结束时将操作的逻辑结果返回给调用者。

此刻,我们对 CNeuronPSformer 编码器对象的工作已经完成。您可在附件中找到该类、及其所有方法的完整代码。


模型架构

在构造了实现 PSformer 框架作者提议方式的对象之后,我们转到描述可训练模型的架构。首先,我们对环境状态编码器感兴趣,我们将在其中实现所提议方式。

环境状态编码器的架构在 CreateEncoderDescriptions 方法中定义。在方法参数中,我们传递一个指向动态数组对象的指针,记录模型架构的描述。

bool CreateEncoderDescriptions(CArrayObj *&encoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }

在方法主体中,我们检查接收指针的相关性,并在必要时创建动态数组的新实例。

如前,生料数据层是标准的全连接层。它的大小必须足以捕获指定分析深度的完整历史数据张量。

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

接下来是神对生料数据初始预处理的批量归一化层。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

然后将归一化数据传递给 PSformer 编码器。在本文中,我们用到拥有雷同架构的三个顺序 PSformer 编码器层。为了定义所需的编码器层数,我们的循环迭代计数等于所需编码器深度。在循环主体中,每次迭代时,我们都会创建一个 CNeuronPSformer 对象的描述。

//--- layer 2 - 4
   for(int i = 0; i < 3; i++)
     {
      if(!(descr = new CLayerDescription()))
         return false;
      descr.type = defNeuronPSformer;
      descr.window = BarDescr;
      descr.count = HistoryBars;
      descr.window_out = Segments;
      descr.probability = Rho;
      descr.batch = 1e4;
      descr.activation = None;
      descr.optimization = ADAM;
      if(!encoder.Add(descr))
        {
         delete descr;
         return false;
        }
     }

PSformer 编码器之后,我们应用一个由一个卷积层和一个全连接层组成的映射模块。所有神经层都适配 SAM 优化。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvSAMOCL;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   descr.window_out = int(LatentCount / descr.count);
   descr.probability = Rho;
   descr.activation = GELU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.probability = Rho;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

一旦完成环境状态编码器的架构描述,我们将布尔结果返回给调用者,并退出该方法。

附件中提供了描述编码器架构的该方法的完整代码,以及之前文章中未更改的参与者评论者模型架构。

我们还未加修改地继承了环境交互和模型训练程序。在附件中也可找到它们。我们现在进入最后阶段 — 评估所实现技术在真实历史数据上的有效性。


测试

我们已经做了相当多的工作来实现 PSformer 框架作者提议方式的 MQL5 版本。现在来到最激动人心的阶段 — 评估它们在真实历史数据上的有效性。

重点要强调,这是对我们所实现技术的评估,不仅仅是原作者提议的那些。因为我们的实现包含了与原始 PSformer 框架的某些偏离。

我们依据 EURUSD 2023 年全年 H1 时间帧的历史数据训练了模型。如常,所有指标参数都设置为默认值。

如早前所述,环境状态编码器、参与者和评论者模型是同时训练的。至于初始训练,我们采用了在处理以前模型时收集的数据集,并随着训练的进行定期更新。

经过模型训练和数据集更新的多次迭代,我们获得了一个能够在训练和测试数据集上均产生盈利的政策。

训练后的参与者政策依据 EURUSD 的 2024 年 1 月历史数据进行了测试,所有其它参数保持不变。测试结果呈现如下。

在测试期间,该模型执行了 21 笔交易,即平均每个交易日大约一笔交易。其中,14 笔实现盈利,占 66% 以上。平均盈利交易比平均亏损交易高出 38%。

余额图展示,月度前二十几日呈明显上升趋势。

总体而言,结果展示出巨大的潜力。依据更大数据集的进一步改进和额外训练,该模型可用于实时交易。


结束语

我们探索了 PSformer 框架,该框架以其在时间序列预测方面的高精度、及高效的计算资源占用率而闻名。PSformer 的关键架构元素包括参数共享(PS)模块,和时空区段注意力(SegAtt)机制。这些元素可对局部和全局时间序列依赖关系进行有效建模,同时在不牺牲预测品质的情况下降低参数数量。

我们利用 MQL5 实现了我们自己对所提议方式的解释。我们使用这些方法训练模型,并在训练集之外的历史数据上对其进行测试。结果表明经训练模型具有明显的潜力。


参考


  • PSformer:搭配区段注意力的参数高效变换器,进行时间序列预测
  • 本系列的其它文章

  • 文章中所用程序

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

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

    附加的文件 |
    MQL5.zip (2171.22 KB)
    最近评论 | 前往讨论 (4)
    FSAmasterAccount
    FSAmasterAccount | 16 8月 2025 在 17:11
    我一直从 math.math/mqh 文件中得到错误信息。如果有任何解决方案,我将不胜感激。
    Dmitriy Gizlyk
    Dmitriy Gizlyk | 18 8月 2025 在 07:29
    FSAmasterAccount MathPow替换 MathPow,这样就可以访问编译器函数而不是类中声明的函数。
    Khaled Ali E Msmly
    Khaled Ali E Msmly | 17 9月 2025 在 11:48

    当我编译 Research.mq5 文件时,出现以下错误

    当我编译 ResearchRealORL.mq5 文件时,我得到这个错误


    当我编译 Study.mq5 文件时,我得到了这个错误

    几乎重复同样的错误,我做错了什么?


    当我编译 Test.mq5 文件时,我得到了这个错误


    TahianaBE
    TahianaBE | 5 10月 2025 在 12:11
    FSAmasterAccount #:
    我一直从 math.math/mqh 文件中得到错误信息。如果有任何解决方案,我将不胜感激。
    您好。
    您成功修复了错误吗?不幸的是,我也遇到了这些错误。我已经按照作者的建议做了,但错误仍然存在。
    开发回放系统(第 74 部分):新 Chart Trade(一) 开发回放系统(第 74 部分):新 Chart Trade(一)
    在本文中,我们将修改本系列关于 Chart Trade 中显示的最后一段代码。这些变化对于使代码适应当前的回放/模拟系统模型是必要的。此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
    分析交易所价格的二进制代码(第一部分):技术分析的新视角 分析交易所价格的二进制代码(第一部分):技术分析的新视角
    本文提出了一种基于将价格波动转换为二进制代码的技术分析创新方法。作者展示了市场行为的各个方面——从简单的价格波动到复杂形态——如何被编码为一系列的0和1。
    价格行为分析工具包开发(第七部分):信号脉冲智能交易系统(EA) 价格行为分析工具包开发(第七部分):信号脉冲智能交易系统(EA)
    借助“信号脉冲(Signal Pulse)”这款MQL5智能交易系统(EA),释放多时间框架分析的潜力。该EA整合了布林带(Bollinger Bands)和随机震荡器(Stochastic Oscillator),以提供准确、高概率的交易信号。了解如何实施这一策略,并使用自定义箭头有效直观地显示买入和卖出机会。非常适合希望借助多时间框架的自动化分析来提升自身判断能力的交易者。
    《精通日志记录(第二部分):格式化日志》 《精通日志记录(第二部分):格式化日志》
    在本文中,我们将探讨如何在类库中创建和应用日志格式化工具。我们将从格式化工具的基本结构讲起,一直到样例的实现。到本文结束时,您将掌握在该库中格式化日志的必要知识,并理解其背后的工作原理。