English Русский Español Deutsch 日本語 Português
preview
交易中的神经网络:降低锐度强化变换器效率(终章)

交易中的神经网络:降低锐度强化变换器效率(终章)

MetaTrader 5示例 |
38 7
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

在上一篇文章中,我们领略了 SAMformer锐度感知多变量变换器)框架的理论层面。它是一种创新模型,设计用于解决传统变换器在多变量时间序列数据的长期预测任务中的固有局限性。雏形变换器的一些核心问题包括训练复杂度高、在小型数据集上的普适性能力差,以及陷入次优局部最小值。这些限制阻碍了基于变换器模型在输入数据有限、且对预测准确性要求高的场景中的适用性。

SAMformer 背后的关键思路在于它运用浅层架构,这降低了计算复杂度,并有助于防止过度拟合。其核心组件之一是锐度感知最小化(SAM)优化机制,其强化了模型对轻微参数变化的健壮性,从而提升了其普适能力,及最终预测的品质。

得益于这些特点,SAMformer 在合成和真实世界时间序列数据集上都能提供出色的预测性能。该模型在达成高精度的同时,显著降低了参数数量,令其更加高效,适合在资源受约束的环境中部署。这些优势为 SAMformer 在金融、医疗保健、供应链管理、及能源等领域的广泛应用打开了大门,其中长期预测扮演着至关重要的角色。

该框架的原始可视化提供如下。

我们已开始实现拟议的办式。在上一篇文章中,我们在 OpenCL 端引入了新的内核。我们还讨论了全连接层的强化。今天,我们将继续这项工作。



1. 配以 SAM 优化的卷积层

我们继续我们已开启的工作。下一步,我们将运用 SAM 优化能力扩展卷积层。正如您所料,我们实现的新类 CNeuronConvSAMOCL 是作为现有卷积层 CNeuronConvOCL 的子类。新对象的结构如下所示。

class CNeuronConvSAMOCL    :  public CNeuronConvOCL
  {
protected:
   float             fRho;
   //---
   CBufferFloat      cWeightsSAM;
   CBufferFloat      cWeightsSAMConv;
   //---
   virtual bool      calcEpsilonWeights(CNeuronBaseOCL *NeuronOCL);
   virtual bool      feedForwardSAM(CNeuronBaseOCL *NeuronOCL);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronConvSAMOCL(void) {  activation = GELU;   }
                    ~CNeuronConvSAMOCL(void) {};
//---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint step, uint window_out, 
                          uint units_count, uint variables, 
                          ENUM_OPTIMIZATION optimization_type, uint batch) override;
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint step, uint window_out, 
                          uint units_count, uint variables, float rho, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const         {  return defNeuronConvSAMOCL;                         }
   virtual int       Activation(void)  const    {  return (fRho == 0 ? (int)None : (int)activation);   }
   virtual int       getWeightsSAMIndex(void)   {  return cWeightsSAM.GetIndex();                      }
   //--- 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);
  };

请注意,所呈现结构包括两个存储所调整参数的缓冲区。一个缓冲区用于外出连接,类似于全连接层(cWeightsSAM)。另一个用于收入连接(cWeightsSAMConv)。注意,父类未明确包含这样的参数缓冲区的副本。事实上,外出连接权重的缓冲区是在父类全连接层中定义的。

在此,我们面临着一个设计困境:是继承具有 SAM 功能的全连接层,亦或继承现有的卷积层。在第一种情况下,我们不需要为调整后的外出连接定义新的缓冲区,因为它会被继承。然而,这会要求我们彻底重新实现卷积层的方法。

在第二种情况下,通过继承卷积层,我们保留了其所有现有功能。然而,这种方式缺乏调整后的外出权重缓冲区,而这对于后续全连接 SAM 优化层的正常运行是必要的。

我们选择了第二个继承选项,在于它只需要更少的工作量,即可实现所需的功能。

如前,我们声明额外的静态内部对象,允许我们将构造函数和析构函数留空。无论如何,在类构造函数中,我们将 GELU 设置为默认激活函数。剩余的初始化继承对象和新声明对象的所步骤都在 Init 方法中执行。此处,您会注意到两个名称相同但参数集不同的重写方法。我们将首先检查具有最全面参数清单的版本。

bool CNeuronConvSAMOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                             uint window_in, uint step, uint window_out, 
                             uint units_count, uint variables, float rho, 
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, window_in, step, window_out, 
                                    units_count, variables, optimization_type, batch))
      return false;

在方法参数中,我们接收主要常量,令我们能够唯一地判定正在创建的对象架构。我们即刻将几乎所有这些参数都传递至父类同名方法,其中已实现了继承对象的所有必要控制点,及初始化算法。

父类方法成功执行后,我们将模糊区域系数存储在内部变量之中。这就是唯一我们不用传递给父方法的参数。

   fRho = fabs(rho);
   if(fRho == 0)
      return true;

然后我们立即检查已存储数值。如果模糊系数为零,则 SAM 优化算法退化为基本参数优化方法。在这种情况下,所有必需的组件均由父类完成初始化。故此,我们就能返回一个成功结果。

否则,我们先用零值来初始化调整后的收入连接缓冲区。

   cWeightsSAMConv.BufferFree();
   if(!cWeightsSAMConv.BufferInit(WeightsConv.Total(), 0) ||
      !cWeightsSAMConv.BufferCreate(OpenCL))
      return false;

接下来,若有必要,我们类似地初始化调整后的外出参数缓冲区。

   cWeightsSAM.BufferFree();
   if(!Weights)
     return true;
   if(!cWeightsSAM.BufferInit(Weights.Total(), 0) ||
      !cWeightsSAM.BufferCreate(OpenCL))
      return false;
//---
   return true;
  }

注意,仅当存在外出连接参数时,才会初始化最后一个缓冲区。当卷积层后随一个全连接层时,就会发生这种情况。

成功初始化所有内部组件后,该方法将操作的逻辑结果返回给调用程序。

我们类中的第二个初始化方法完全覆盖父类方法,并具有相同的参数。然而,正如您或许已猜到的那样,它省略了模糊系数参数,这对于 SAM 优化至关重要。在方法主体中,我们分配了默认的模糊系数 0.7。该系数在讲述 SAMformer 框架的原始论文中已讲过。然后我们调用前面描述的类初始化方法。

bool CNeuronConvSAMOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,

                             uint window_in, uint step, uint window_out, 
                             uint units_count, uint variables, 
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   return CNeuronConvSAMOCL::Init(numOutputs, myIndex, open_cl, window_in, step, window_out, units_count, 
                                                                variables, 0.7f, optimization_type, batch);
  }

该方式令我们能够在前面讨论的几乎所有架构配置中,轻松地将常规卷积层与其 SAM 优化的对应层交换,简单地更改对象类型即可。

如同全连接层,所有前向通验、及梯度分布功能都继承自父类。不过,我们引入了两种调用 OpenCL 程序内核的包装方法:calcEpsilonWeightsfeedForwardSAM。第一种方法调用负责计算所调整参数的内核。第二个镜像父类的前向通验方法,但采用调整后的参数缓冲区。此处我们就不赘述这些方法的详细逻辑了。它们遵循早前所讨论相同内核的队列算法。您可在随附的源代码中探索它们的完整实现。

该类参数优化方法与全连接 SAM 优化层中的参数优化方法非常接近。不过,在这种情况下,我们不会检查前一层的类型。与全连接层不同,卷积层包含自己的处理输入数据的内部参数矩阵。因此,它使用自己的调整参数缓冲区。它从上一层需要全部,就是输入数据缓冲区,我们所有的对象都提供。

无论如何,我们要检查模糊系数值。当它为零时,SAM 优化会被有效绕过。在这种情况下,我们简单地调用父类方法。

bool CNeuronConvSAMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(fRho <= 0)
      return CNeuronConvOCL::updateInputWeights(NeuronOCL);

如果启用了 SAM 优化,我们首先将误差梯度与前馈通验结果相结合,生成当前对象的目标张量:

   if(!SumAndNormilize(Gradient, Output, Gradient, iWindowOut, false, 0, 0, 0, 1))
      return false;

接下来,我们使用模糊系数更新模型参数。这涉及调用将相应内核排队的包装器。注意,卷积层和全连接层都调用同名方法。但它们排队时,会分到特定于其内部架构的不同内核。

   if(!calcEpsilonWeights(NeuronOCL))
      return false;

这同样适用于采用所调整参数的前馈方法。

   if(!feedForwardSAM(NeuronOCL))
      return false;

第二次前馈通验成功后,我们计算与目标值的偏差。

   float error = 1;
   if(!calcOutputGradients(Gradient, error))
      return false;

然后,我们调用父类方法来更新模型参数。

//---
   return CNeuronConvOCL::updateInputWeights(NeuronOCL);
  }

最后,将逻辑结果返回给调用程序,完结该方法。

关于保存训练模型的参数,需要说几句话。当保存已训练模型时,我们遵循曾讨论的全连接 SAM 层境况的相同方式。我们不保存包含所调整参数的缓冲区。取而代之,我们只将模糊系数添加到父类保存的数据之中。

bool CNeuronConvSAMOCL::Save(const int file_handle)
  {
   if(!CNeuronConvOCL::Save(file_handle))
      return false;
   if(FileWriteFloat(file_handle, fRho) < INT_VALUE)
      return false;
//---
   return true;
  }

在加载预训练模型时,我们需要准备必要的缓冲区。重点注意,为调整后的外出和接入参数创建缓冲区的准则是不同的。

首先,我们加载由父类保存的数据。

bool CNeuronConvSAMOCL::Load(const int file_handle)
  {
   if(!CNeuronConvOCL::Load(file_handle))
      return false;

接下来,我们检查文件是否包含更多数据,然后读取模糊系数。

   if(FileIsEnding(file_handle))
      return false;
   fRho = FileReadFloat(file_handle);

模糊系数为正值是初始化所调整参数缓冲区的关键条件。故此,我们检查所加载参数的数值。如果不满足该条件,我们将清除 OpenCL 关联环境和主内存中所有未用缓冲区。之后,我们以正值结果完成该方法。

   cWeightsSAMConv.BufferFree();
   cWeightsSAM.BufferFree();
   cWeightsSAMConv.Clear();
   cWeightsSAM.Clear();
   if(fRho <= 0)
      return true;

注意,当控制点对程序的运行不重要时,这就是会出现的情况之一。如前提醒,模糊系数的零值将 SAM 简化为基本优化方法。故此,在这种情况下,我们的对象回落到父类的功能。

如果满足条件,我们继续在 OpenCL 关联环境中为所调整收入参数初始化,并分配内存。

   if(!cWeightsSAMConv.BufferInit(WeightsConv.Total(), 0) ||
      !cWeightsSAMConv.BufferCreate(OpenCL))
      return false;

为所调整外出参数创建缓冲区,必须满足一个附加条件:这样的连接已存在。因此,我们在初始化之前检查指针有效性。

   if(!Weights)
     return true;

再者,缺少有效指针并非一个严重错误。它只是反映了模型的架构。因此,如果当前没有指针,我们以正值结果终止该方法。

若找到外出连接缓冲区,我们就为所调整参数,初始化并创建一个大小相似的缓冲区。

   if(!cWeightsSAM.BufferInit(Weights.Total(), 0) ||
      !cWeightsSAM.BufferCreate(OpenCL))
      return false;
//---
   return true;
  }

然后我们将操作的逻辑结果返回给调用者,并完结方法执行。

据此,我们完成了在 CNeuronConvSAMOCL 中实现 SAM 优化卷积层方法的验证。该类及其所有方法的完整代码可在附件中找到。



2. 将 SAM 加到变换器

在该阶段,我们已创建了全连接层和卷积层对象,其中包含基于 SAM 的参数优化。现在是时候将这些方法集成到变换器架构当中了。这确实与 SAMformer 框架作者所提议的一样。为了客观地评估这些技术对模型性能的影响,我们决定不创建全新的类。取而代之,我们将基于 SAM 的方式直接集成到现有类的结构中。对于基本架构,我们选择了具有相对注意力 R-MAT 的变换器。

如您所知,CNeuronRMAT 类实现交替 CNeuronRelativeSelfAttentionCResidualConv 对象的线性序列。第一个实现了可反馈相对注意力机制,而第二个则包含基于反馈的卷积模块。为了集成 SAM 优化,只需将这些对象中的所有卷积层替换为启用 SAM 的卷积层即可。更新后的类结构如下所示。

class CNeuronRelativeSelfAttention   :  public CNeuronBaseOCL
  {
protected:
   uint                    iWindow;
   uint                    iWindowKey;
   uint                    iHeads;
   uint                    iUnits;
   int                     iScore;
   //---
   CNeuronConvSAMOCL          cQuery;
   CNeuronConvSAMOCL          cKey;
   CNeuronConvSAMOCL          cValue;
   CNeuronTransposeOCL     cTranspose;
   CNeuronBaseOCL          cDistance;
   CLayer                  cBKey;
   CLayer                  cBValue;
   CLayer                  cGlobalContentBias;
   CLayer                  cGlobalPositionalBias;
   CLayer                  cMHAttentionPooling;
   CLayer                  cScale;
   CBufferFloat            cTemp;
   //---
   virtual bool      AttentionOut(void);
   virtual bool      AttentionGradient(void);

   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronRelativeSelfAttention(void) : iScore(-1) {};
                    ~CNeuronRelativeSelfAttention(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key,
                          uint units_count, uint heads,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronRelativeSelfAttention; }
   //---
   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;
   //---
   virtual uint      GetWindow(void) const { return iWindow; }
   virtual uint      GetUnits(void) const { return iUnits; }
  };
class CResidualConv  :  public CNeuronBaseOCL
  {
protected:
   int               iWindowOut;
   //---
   CNeuronConvSAMOCL    cConvs[3];
   CNeuronBatchNormOCL cNorm[3];
   CNeuronBaseOCL    cTemp;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer);

public:
                     CResidualConv(void) {};
                    ~CResidualConv(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_out, uint count,
                          ENUM_OPTIMIZATION optimization_type,
                          uint batch);
   //---
   virtual int       Type(void)   const   {  return defResidualConv;   }
   //--- 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);
   virtual void      TrainMode(bool flag);
  };

注意,对于反馈卷积模块,我们仅修改类结构中的对象类型。无需修改类方法。这是可能的,因为我们重载的卷积层初始化方法配合 SAM 初始化。回想一下,CNeuronConvSAMOCL 类提供了两种初始化方法:一种搭配模糊系数作为参数,另一种没有模糊系数。没有模糊系数的方法会覆盖以前父类初始化卷积层的方法。如是结果,在初始化 CResidualConv 对象时,程序会调用我们的覆盖初始化方法,其中会自动分配默认的模糊系数,并配合 SAM 优化触发全卷积层初始化。

相对注意力模块的情况稍微复杂一些。CNeuronRelativeSelfAttention 模块具有更复杂的架构,其中包括其它嵌套的可训练偏差模型。它们的架构在对象初始化方法中定义。因此,为了启用这些内部模型的 SAM 优化,我们必须修改相对注意力模块本身的初始化方法。

方法参数维持不变,其算法的初始步骤也得以保留。生成 查询主键数值实体的对象类型已在类结构中更新。

bool CNeuronRelativeSelfAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                               uint window, uint window_key, uint units_count, uint heads, 
                                          ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      return false;
//---
   iWindow = window;
   iWindowKey = window_key;
   iUnits = units_count;
   iHeads = heads;
//---
   int idx = 0;
   if(!cQuery.Init(0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, optimization, iBatch))
      return false;
   cQuery.SetActivationFunction(GELU);
   idx++;
   if(!cKey.Init(0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, optimization, iBatch))
      return false;
   cKey.SetActivationFunction(GELU);
   idx++;
   if(!cValue.Init(0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, optimization, iBatch))
      return false;
   cKey.SetActivationFunction(GELU);
   idx++;
   if(!cTranspose.Init(0, idx, OpenCL, iUnits, iWindow, optimization, iBatch))
      return false;
   idx++;
   if(!cDistance.Init(0, idx, OpenCL, iUnits * iUnits, optimization, iBatch))
      return false;

甚至,在 BKeyBValue 偏差生成模型中,我们在保留其它参数的同时,替换卷积对象类型。

   idx++;
   CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, 1, optimization, iBatch) ||
      !cBKey.Add(conv))
      return false;
   idx++;
   conv.SetActivationFunction(TANH);
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, optimization, iBatch) ||
      !cBKey.Add(conv))
      return false;
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, 1, optimization, iBatch) ||
      !cBValue.Add(conv))
      return false;
   idx++;
   conv.SetActivationFunction(TANH);
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, optimization, iBatch) ||
      !cBValue.Add(conv))
      return false;

在生成全局上下文和定位偏差的模型中,我们使用搭配 SAM 优化的全连接层。

   idx++;
   CNeuronBaseOCL *neuron = new CNeuronBaseSAMOCL();
   if(!neuron ||
      !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, 1, optimization, iBatch) ||
      !cGlobalContentBias.Add(neuron))
      return false;
   idx++;
   CBufferFloat *buffer = neuron.getOutput();
   buffer.BufferInit(1, 1);
   if(!buffer.BufferWrite())
      return false;
   neuron = new CNeuronBaseSAMOCL();
   if(!neuron ||
      !neuron.Init(0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) ||
      !cGlobalContentBias.Add(neuron))
      return false;
   idx++;
   neuron = new CNeuronBaseSAMOCL();
   if(!neuron ||
      !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, 1, optimization, iBatch) ||
      !cGlobalPositionalBias.Add(neuron))
      return false;
   idx++;
   buffer = neuron.getOutput();
   buffer.BufferInit(1, 1);
   if(!buffer.BufferWrite())
      return false;
   neuron = new CNeuronBaseSAMOCL();
   if(!neuron ||
      !neuron.Init(0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) ||
      !cGlobalPositionalBias.Add(neuron))
      return false;

对于池化操作 MLP,我们再次使用 SAM 优化方式的卷积层。

   idx++;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) ||
      !cMHAttentionPooling.Add(neuron)
     )
      return false;
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, 1, optimization, iBatch) ||
      !cMHAttentionPooling.Add(conv)
     )
      return false;
   idx++;
   conv.SetActivationFunction(TANH);
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iWindow, iWindow, iHeads, iUnits, 1, optimization, iBatch) ||
      !cMHAttentionPooling.Add(conv)
     )
      return false;
   idx++;
   conv.SetActivationFunction(None);
   CNeuronSoftMaxOCL *softmax = new CNeuronSoftMaxOCL();
   if(!softmax ||
      !softmax.Init(0, idx, OpenCL, iHeads * iUnits, optimization, iBatch) ||
      !cMHAttentionPooling.Add(softmax)
     )
      return false;
   softmax.SetHeads(iUnits);

注意,对于第一层,我们仍然使用基础全连接层。因为它仅用于存储多头注意力模块的输出。

缩放模块中也会出现类似的情况。第一层仍然是基本全连接层,在于它存储注意力权重乘以多头注意力模块输出的结果。然后是具有 SAM 优化的卷积层。

   idx++;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, idx, OpenCL, iWindowKey * iUnits, optimization, iBatch) ||
      !cScale.Add(neuron)
     )
      return false;
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, iWindowKey, iWindowKey, 2 * iWindow, iUnits, 1, optimization, iBatch) ||
      !cScale.Add(conv)
     )
      return false;
   conv.SetActivationFunction(LReLU);
   idx++;
   conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, idx, OpenCL, 
2  * iWindow, 2 * iWindow, iWindow, iUnits, 1, optimization, iBatch) ||
      !cScale.Add(conv)
     )
      return false;
   conv.SetActivationFunction(None);
//---
   if(!SetGradient(conv.getGradient(), true))
      return false;
//---
   SetOpenCL(OpenCL);
//---
   return true;
  }

据此,我们得出结论,将 SAM 优化方法集成到具有相对注意力的变换器之中。附件中提供了更新对象的完整代码。



3. 模型架构

我们已创建新对象,并更新了某些现有对象。下一步是调整整体模型架构。不同于近期的一些文章,今天的架构变化更为广泛。我们从环境编码器的架构开始,该架构已在 CreateEncoderDescriptions 方法中实现。如前,该方法接收指向记录模型层序列的动态数组指针。

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

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

我们保持前 2 层不变。这些是源数据层、和批量归一化层。这些层的大小雷同,且必须足够记录原始数据张量。

//--- 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;
     }

接下来,SAMformer 框架的作者提议使用通道注意力。因此,我们使用数据转置层来帮助我们将原始数据表示为一个注意力通道序列。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = HistoryBars;
   descr.window= BarDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

然后我们使用相对注意力模块,我们已在其中添加了 SAM 优化方法。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRMAT;
   descr.window=HistoryBars;
   descr.count=BarDescr;
   descr.window_out = EmbeddingSize/2;                // Key Dimension
   descr.layers = 1;                                  // Layers
   descr.step = 2;                                    // Heads
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

此处应当注意两个要点。首先,我们使用通道注意力。因此,分析窗口等于所分析历史的深度,且元素数量与独立通道数量匹配。其次,正如 SAMformer 框架作者所提议那样,我们仅用一个注意力层。不过,与原始实现不同的是,我们用了两个注意力头。我们还保留了 FeedForward 模块。尽管,框架作者只用到一个注意力头,并删除了 FeedForward 组件。

接下来,我们必须将输出张量的维数降低到所需的大小。这分两个阶段完成。首先,我们应用搭配 SAM 优化的卷积层来降低各个通道的维度。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvSAMOCL;
   descr.count = BarDescr;
   descr.window = HistoryBars;
   descr.step = HistoryBars;
   descr.window_out = LatentCount/BarDescr;
   descr.probability = 0.7f;
   descr.activation = GELU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

然后,我们使用搭配 SAM 优化的全连接层,来获得给定大小的当前环境状态的一般嵌入。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.probability = 0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

在这两种情况下,我们都用 descr.probability 来指定模糊区域系数。

该方法返回操作的逻辑结果给调用方并完结。模型架构本身则经参数提供的动态数组指针返回。

定义环境编码器的架构之后,我们继续描述参与者评论者层。这两个模型的描述都是在 CreateDescriptions 方法中生成的。由于该方法构建两个单独的模型描述,故其参数包括两个指向动态数组的指针。

bool CreateDescriptions(CArrayObj *&actor, CArrayObj *&critic)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

在该方法内,我们验证所提供指针的有效性,并在必要时创建新的动态数组。

我们从参与者架构开始。该模型的第一层作为搭配 SAM 优化的全连接层实现。它的大小与交易账户的状态描述向量匹配。

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   int prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.probability=0.7f;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

值得注意的是,此处我们用到 SAM 优化的全连接层来记录输入数据。在环境编码器中,在类似的位置用到一个基础全连接层。这种差异是由于存在后续搭配 SAM 优化的全连接层,这需要前一层提供调整参数的缓冲区,以便正确操作。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

如同环境编码器,我们利用 descr.probability 来设置模糊区域系数。对于所有模型,我们统一应用 0.7 的系数。

两个连续的 SAM 优化全连接层创建当前交易账户状态的嵌入,然后将其与相应的环境状态嵌入串联起来。这种串联由专用的数据串联层执行。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = EmbeddingSize;
   descr.step = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

结果会被传递给一个决策模块,其由三个 SAM 优化全连接层组成。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = 2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

在最后一层的输出中,我们生成一个张量,其大小是参与者目标动作向量的两倍。这种设计令我们能够将随机性并入动作之中。如前,我们使用自动编码器的潜在状态层来达成这一点。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

回想一下,自动编码器的潜在层将输入张量分成两部分:第一部分包含输出序列中每个元素的分布平均值,第二部分包含相应分布的方差。在决策模块中训练这些均值和方差,令我们能够通过自动编码器的潜在层约束生成的随机值的范围,从而将随机性引入参与者的政策当中。

值得补充的是,自动编码器的潜在层为输出序列的每个元素生成独立值。然而,在我们的例子中,我们期望一组连贯的参数来执行交易:持仓规模、止盈水平、和止损水平。为了确保这些交易参数之间的一致性,我们采用了 SAM 优化的卷积层,分别分析多头和空头交易的参数。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvSAMOCL;
   descr.count = NActions / 3;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 3;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

为了限制该层的输出域,我们调用 sigmoid 激活函数。

而我们的参与者模型的最后一步是频率提升前馈预测层(CNeuronFreDFOCL),它允许模型的结果与频域中的目标值进行匹配。

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFreDFOCL;
   descr.window = NActions;
   descr.count =  1;
   descr.step = int(false);
   descr.probability = 0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

评论者模型拥有类似的架构。然而,我们没有说明传递给参与者的账户状态,取而代之是向模型提供参与者生成的交易操作的参数。我们还用 2 个全连接层、并搭配 SAM 优化来获得交易操作嵌入。

//--- Critic
   critic.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   prev_count = descr.count = NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

交易操作嵌入在数据串联层中与环境状态嵌入相结合。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = EmbeddingSize;
   descr.step = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

然后我们用 3 个搭配 SAM 优化的连续全连接层的决策模块。但与参与者不同的是,在这种情况下,未用到结果的随机性质。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseSAMOCL;
   descr.count = NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   descr.probability=0.7f;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

在评论者模型之上,我们添加了一个具有频率增益的前向预测层。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFreDFOCL;
   descr.window = NRewards;
   descr.count =  1;
   descr.step = int(false);
   descr.probability = 0.7f;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

模型架构描述完成生成后,该方法将操作的逻辑结果返回给调用方,并终止。架构描述本身通过方法参数接收到的动态数组指针返回。

我们在模型构造方面的工作至此完结。完整的架构可在附件中找到。那里面,您还可找到环境交互和模型训练程序的完整源代码,这些是从之前的工作中继承而来的,未经修改。



4. 测试

我们已经开展了大量工作来实现 SAMformer 框架作者提议的方式。现在是时候评估我们的实现在真实历史数据上的有效性了。如前,模型训练是依据 EURUSD 金融产品涵盖 2023 全年的真实历史数据上进行的。贯穿实验,我们采用 H1 时间帧。所有指标参数均按其默认值设置。

如前所述,负责环境交互和模型训练的程序维持不变。这令我们能够复用之前创建的训练数据集,进行模型的初始训练。甚至,由于选择了 R-MAT 框架作为纳入 SAM 优化的基线,因此我们决定在模型训练期间不更新训练集。自然地,我们预计这种选择会对模型性能产生负面影响。不过,这排除了训练数据集变化带来的任何影响,可更直接地与基线模型进行比较。

所有三个模型的训练都是同时进行的。训练后的参与者政策的测试结果如下所示。测试是在 2024 年 1 月的真实历史数据上进行的,所有其它训练参数维持不变。

在检查结果之前,我想提一下关于模型训练的若干点。首先,SAM 优化所固有的平滑损失局面。这反过来又令我们能够参考更高的学习率。在早期的工作中,我们主要使用 3.0e-04 的学习率,而在本例中,我们将其提高到 1.0e-03。

其次,仅用单个注意力层减少了可训练参数的总数,有助于抵消 SAM 优化所需的额外前馈通验引入的计算开销。

训练的结果如是,我们获得了一个能够在训练数据集之外产生盈利的政策。测试期间,模型执行了 19 笔交易,其中 11 笔盈利(57.89%)。相比之下,我们之前实现的 R-MAT 模型,在同一时期执行了 15 笔交易,其中 9 笔交易盈利 (60.0%)。值得注意的是,新模型的总回报率几乎是基线的两倍。 



结束语

SAMformer 框架为多变量时间序列长期预测背景下变换器架构的关键局限性提供了有效的解决方案。传统的变换器面临着重大挑战,包括训练复杂性高\、及普适能力差,尤其是当应对小型训练数据集时。

SAMformer 的核心优势在于其浅层架构、与锐度感知最小化(SAM)的集成。这些方式有助于模型避免较差的局部最小值,提高训练稳定性和准确性,并提供卓越的普适性能。

在我们工作的实施部分,我们利用 MQL5 实现了我们自己对这些方法的解释,并在真实的历史数据上训练了模型。测试结果验证了所提议方式的有效性,表明它们的集成可在不产生额外训练成本的情况下,强化基线模型的性能。在某些情况下,它甚至可令您降低训练成本。


参考

  • SAMformer:配合锐度感知最小化和通道级注意力,解锁变换器在时间序列预测中的潜力
  • 锐度感知最小化,有效提升普适性
  • 本系列的其它文章

  • 文章中所用程序

    # 名称 类型 说明
    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/16403

    附加的文件 |
    MQL5.zip (2147 KB)
    最近评论 | 前往讨论 (7)
    Evgeny Belyaev
    Evgeny Belyaev | 24 11月 2024 在 23:03
    Dmitriy Gizlyk #:

    俄罗斯银行的年收入(单位:多拉尔)。除以 12 并进行比较。

    人民币 6 元,人民币债券 10 多元。

    Dmitriy Gizlyk
    Dmitriy Gizlyk | 25 11月 2024 在 01:46
    Evgeny Belyaev #:

    人民币 6 元,人民币债券 10 元以上。

    但文章中给出了欧元兑美元的测试结果和美元的测试结果。同时,存款的负载为 1-2%,没有人说它是圣杯。

    Evgeny Belyaev
    Evgeny Belyaev | 26 11月 2024 在 23:42
    Dmitriy Gizlyk #:

    但文章给出了欧元兑美元的测试结果和美元的测试结果。同时,存款的负载为 1-2%,没有人说它是圣杯。

    好吧。银行中的英镑上限为 5%。

    Khaled Ali E Msmly
    Khaled Ali E Msmly | 12 8月 2025 在 11:07
    文章写得很好,谢谢。
    Ivan Butko
    Ivan Butko | 13 8月 2025 在 09:25
    dsplab #:

    每月总利润为 0.35%?把钱存入银行不是更有利可图吗?

    .





    开发回放系统(第 73 部分):不寻常的通信(二) 开发回放系统(第 73 部分):不寻常的通信(二)
    在本文中,我们将探讨如何在指标和服务之间实时传输信息,并了解为什么在更改时间框架时可能会出现问题以及如何解决这些问题。作为奖励,您将可以访问回放/模拟应用程序的最新版本。
    大爆炸-大坍缩(BBBC)算法 大爆炸-大坍缩(BBBC)算法
    本文介绍了大爆炸-大坍缩方法,该方法包含两个关键阶段:随机点的循环生成,以及将这些点压缩至最优解。该方法结合了探索与精炼过程,使我们能够逐步找到更优的解,并开拓新的优化可能性。
    交易中的多项式模型 交易中的多项式模型
    本文将介绍正交多项式。正交多项式的应用,可以成为更准确、更有效地分析市场信息的基础,从而帮助交易者做出更明智的决策。
    重构经典策略(第十三部分):最小化均线交叉的滞后性 重构经典策略(第十三部分):最小化均线交叉的滞后性
    在我们交易者社区中,均线交叉策略已是广为人知,然而,自该策略诞生以来,其核心思想却几乎一成未变。在本次讨论中,我们将为您呈现对原策略的一项微调,其目的在于最小化该交易策略中存在的滞后性。所有原策略的爱好者们,不妨根据我们今天将要探讨的见解,来重新审视并改进这一策略。通过使用两条周期相同的移动平均线,我们可以在不违背策略基本原则的前提下,显著减少交易策略的滞后。