English Русский Español Deutsch 日本語 Português
preview
交易中的神经网络:双曲型潜在扩散模型(终篇)

交易中的神经网络:双曲型潜在扩散模型(终篇)

MetaTrader 5交易系统 |
42 1
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

双曲几何空间能够表示离散的树状、或层次化结构,适用于各种图形学习任务。它还拥有巨大潜力,来解决图形扩散期间,非欧几里德空间中结构各向异性问题。双曲几何集成了极坐标的角度和径向维度,启用具有物理语义和可解释性的几何衡量。

在这种境况下,HypDiff 框架代表了一种生成双曲高斯噪声的先进方法,有效地解决了双曲空间内高斯分布中的可加性扰动问题。该框架的作者引入了基于角度相似性的几何约束,应用在各向异性扩散过程当中,以便保留图形的局部结构。

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

上一篇文章中,我们开始利用 MQL5 实现所提议方式。不过,工作纵深相当广泛。我们只能涵盖 OpenCL 程序端的实现模块。在本文中,我们将继续我们已开始的工作,并带领 HypDiff 框架实现达至一个合乎逻辑的结论。无论如何,在我们的实现中,与原始算法相比我们会引入某些偏差,我们的讨论会贯穿整个算法开发过程。



1. 数据投影到双曲空间之中

我们在 OpenCL 程序端的工作始于开发将原始数据投影到双曲空间(分别为 HyperProjectionHyperProjectionGrad)的前馈和反向通验内核。同样,我们从构建该功能的算法开始,实现主程序端 HypDiff 框架。为此,我们将创建一个新类 CNeuronHyperProjection,其结构如下所示。

class CNeuronHyperProjection   :  public CNeuronBaseOCL
  {
protected:
   uint              iWindow;
   uint              iUnits;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL)   { return true; }

public:
                     CNeuronHyperProjection(void) : iWindow(-1), iUnits(-1) {};
                    ~CNeuronHyperProjection(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint units_count, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronHyperProjection;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
  };

在所呈现的结构中,我们看到两个内部变量的声明,用于存储定义对象架构的常量,以及熟悉的一组可覆盖方法。不过,注意,模型参数更新方法 updateInputWeights 是作为正“存根”实现的。这是有意为之。我们开发的前馈和反向传播投影内核,实现了一种确凿定义的算法,即不包含任何可训练参数。无论如何,参数更新方法的存在对于我们模型的正确运作是必需的。因此,我们被迫重写指定的方法,始终返回正结果。

由于没有新声明的内部对象,这就允许我们将类构造函数和析构函数留空。继承对象和内部变量的初始化在 Init 方法中处理。

初始化方法的算法相当简单。如常,它的参数包括明确标识正创建对象的架构所需的核心常量。

bool CNeuronHyperProjection::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                                  uint window, uint units_count, 
                                  ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, (window + 1)*units_count, optimization_type, batch))
      return false;
   iWindow = window;
   iUnits = units_count;
//---
   return true;
  }

在方法主体中,我们立即调用父类同名方法,并把接收到的参数传递相关部分给它。如您所知,父类已实现了验证这些参数、及初始化继承对象的逻辑。我们要做的就是检查父方法执行的逻辑结果。之后,我们将从外部程序接收到的架构常量保存到内部变量之中。

如是而已。我们并未声明任何新的内部对象,且继承对象在父类方法中初始化。剩下的就是将操作的结果返回给调用程序,并退出该方法。

至于本类的前馈和反向传播方法,我建议您自行回顾一遍。两者都是简单的“包装器”,调用 OpenCL 程序的相应内核。在我们的系列文章中,已多次讲述过类似方法。相信您很清楚它们的实现逻辑。该类及其所有方法的完整代码可在附件中找到。



2. 投影到切线平面上

将原始数据投影到双曲空间之后,HypDiff 框架会构造一个编码器来生成双曲节点嵌入。我们计划使用我们函数库中的现有组件实现该功能。生成的嵌入被投影到与 k 个质心相对应的切平面上。我们已在 OpenCL 端,分别通过 LogMapLogMapGrad 内核,实现了切线映射的投影算法、和相应的梯度反向分布。然而,质心问题仍未解决。

需要注意的是,HypDiff 框架的作者在数据准备阶段,自训练数据集定义质心。不幸的是,这种方式不适合我们的目的。这不仅仅是它的劳动强度。这种方法不适合在动态金融市场的背景下进行分析。在价格走势的技术分析中,新兴形态通常优先于特定价格值。对于在不同时间间隔内观察到的类似市场状况,不同的质心或许是相关的。由此,我们得出结论,有必要创建一个动态模型,来适配或生成质心及其参数。在我们的实现中,我们决定采用基于原始数据嵌入的质心生成模型。如是结果,我们选择将质心生成、及数据投影到相应切平面上的过程,结合到单一类中:CNeuronHyperboloids。其结构呈现如下。

class CNeuronHyperboloids  :  public CNeuronBaseOCL
  {
protected:
   uint              iWindows;
   uint              iUnits;
   uint              iCentroids;
   //---
   CLayer            cHyperCentroids;
   CLayer            cHyperCurvatures;
   //---
   int               iProducts;
   int               iDistances;
   int               iNormes;
   //---
   virtual bool      LogMap(CNeuronBaseOCL *featers, CNeuronBaseOCL *centroids, 
                            CNeuronBaseOCL *curvatures, CNeuronBaseOCL *outputs);
   virtual bool      LogMapGrad(CNeuronBaseOCL *featers, CNeuronBaseOCL *centroids, 
                                CNeuronBaseOCL *curvatures, CNeuronBaseOCL *outputs);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronHyperboloids(void) : iWindows(0), iUnits(0), iCentroids(0), 
                                                 iProducts(-1), iDistances(-1), iNormes(-1) {};
                    ~CNeuronHyperboloids(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint units_count, uint centroids, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronHyperboloids;   }
   //--- methods for working with files
   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 void      TrainMode(bool flag);
  };

在呈现的新类结构中,我们可观察到两个动态数组、及六个变量的声明,分为两组。

动态数组旨在存储指向两个嵌套模型神经层对象的指针。事实上,在我们的实现中,我们决定将生成质心参数的功能拆分为两个独立的模型。第一个模型负责生成双曲空间中质心的坐标。第二个返回相应点的空间曲率参数。

内部变量的分组也遵循逻辑解释。一组包含我们从外部程序接收的在建对象的架构参数。第二组由变量组成,即存储指向中间值缓冲区的指针,这些仅在 OpenCL 关联环境中创建,且无需将数据复制到系统的主内存当中。

所有内部对象都声明为静态,这样允许我们将类构造函数和析构函数留空。所有继承和声明对象的初始化,都在 Init 方法中实现。

bool CNeuronHyperboloids::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                               uint window, uint units_count, uint centroids,
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window*units_count*centroids, optimization_type, batch))
      return false;

如常,方法参数包括许多常量,明确定义了在建对象的架构。这些包括:

  • units_count — 所分析序列中的元素数量;
  • window — 所分析序列中单个元素的嵌入向量大小;
  • centroids — 综合分析原始数据,而由模型生成的质心数量。

在方法主体中,按照我们既定的方式,我们调用父类的同名方法,初始化继承的对象和变量。此处值得注意的是,与原始 HypDiff 算法不同,我们的实现不会将输入序列的各个元素分配至特定的质心。取而代之,为了给模型提供最大量的信息,我们生成了整个序列的投影,覆盖所有切平面。诚然,生成张量的大小,也会随生成的质心数量,成比例地增加。因此,在调用父类初始化方法时,我们指定所有三个外部提供的常量的乘积,作为所创建层的大小。

一旦父方法成功完成(将由其布尔逻辑返回值指示),我们将接收到的常量保存在内部变量之中。

   iWindows = window;
   iUnits = units_count;
   iCentroids = centroids;

在下一步中,我们将准备动态数组,来存储指向质心参数生成模型对象的指针。

   cHyperCentroids.Clear();
   cHyperCurvatures.Clear();
   cHyperCentroids.SetOpenCL(OpenCL);
   cHyperCurvatures.SetOpenCL(OpenCL);

然后我们直接进行模型对象的初始化。首先,我们初始化负责生成质心坐标的模型。

于此,我们旨在构造一个线性模型,即分析输入数据之后,返回一批相关质心的坐标。然而,为此目的而使用全连接层,会导致创建大量可训练参数,并增加计算负载。卷积层的使用令我们能够减少可训练参数的数量、及计算量。甚至,将卷积层应用于独立的单变量序列似乎是一种合乎逻辑的方式。为实现这一点,我们首先需要相应地转置输入数据。

   CNeuronTransposeOCL *transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, 0, OpenCL, iUnits, iWindows, optimization, iBatch) ||
      !cHyperCentroids.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction(None);

接下来,我们添加一个卷积层,来降低单变量序列的维度。

   CNeuronConvOCL *conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, 1, OpenCL, iUnits, iUnits, iCentroids, iWindows, 1, optimization, iBatch) ||
      !cHyperCentroids.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(TANH);

在这一层中,我们在所有单变量序列中使用一组共享参数。在层的输出端,我们选用双曲正切函数作为激活函数,来引入非线性。

然后我们添加另一个卷积层,没有激活函数,但每个单变量序列都有不同的可训练参数。

   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, 2, OpenCL, iCentroids, iCentroids, iCentroids, 1, iWindows, optimization, iBatch) ||
      !cHyperCentroids.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(None);

如是结果,两个连续卷积层有效地为输入序列中的每个单变量序列形成唯一的 MLP。每个这样的 MLP 都会为所需数量的质心生成一个坐标。换言之,我们已为坐标空间的每个维度构建了一个 MLP,它们一起为指定数量的质心生成完整的坐标集。

现在,我们只需将生成的质心坐标返回至原始表示。为达成这一点,我们添加了另一个数据转置层。

   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, 3, OpenCL, iWindows, iCentroids, optimization, iBatch) ||
      !cHyperCentroids.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());

接下来,我们继续为第二个模型构造对象,其为判定质心位置处的双曲空间曲率参数。曲率参数将基于所生成质心坐标推导。可以合理地假设曲率参数仅取决于特定坐标。因为我们期望模型在训练过程中形成双曲空间的内部表示,并在其学习参数当中反映出这一点。因此,在曲率参数模型中,我们不再使用转置层。代之,我们简单地为每个质心创建一个唯一的 MLP,由两个连续的卷积层组成。

   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, 4, OpenCL, iWindows, iWindows, iWindows, iCentroids, 1, optimization, iBatch) ||
      !cHyperCurvatures.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(TANH);
//---
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, 5, OpenCL, iWindows, iWindows, 1, 1, iCentroids, optimization, iBatch) ||
      !cHyperCurvatures.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(None);

于此,我们还用双曲正切函数在模型层之间引入非线性。

在该阶段,我们完成了初始化负责生成质心参数的模型对象。剩下的就是准备支持内核将数据投影到切平面上、及分派梯度误差所需的对象。在此,我想提醒您,在开发上述内核期间,我们讨论过创建临时缓冲区来存储中间结果。这是三个数据缓冲区,每个缓冲区里包含每个“质心 - 序列元素”对应一个元素。

这些缓冲区仅用于将信息从前馈通验内核传送到梯度分派内核。相应地,仅在 OpenCL 关联环境中创建它们是合理的。换言之,在系统内存中分配这些缓冲区,并在 OpenCL 关联环境和主内存之间复制数据将是多余的。同样,在存储模型参数时无需保存这些缓冲区,因为它们会在每次前向通验期间更新。因此,在主程序这端,我们仅需声明变量来保存指向这些数据缓冲区的指针。

不过,我们仍需在 OpenCL 关联环境中创建缓冲区。为此,我们首先判定所需的数据缓冲区大小。如前所述,所有三个缓冲区共享相同的大小。

   uint size = iCentroids * iUnits * sizeof(float);
   iProducts = OpenCL.AddBuffer(size, CL_MEM_READ_WRITE);
   if(iProducts < 0)
      return false;
   iDistances = OpenCL.AddBuffer(size, CL_MEM_READ_WRITE);
   if(iDistances < 0)
      return false;
   iNormes = OpenCL.AddBuffer(size, CL_MEM_READ_WRITE);
   if(iNormes < 0)
      return false;
//---
   return true;
  }

接下来,我们在 OpenCL 内存中创建数据缓冲区,并将生成的指针存储在相应的变量之中。如常,我们检查所接收指针的有效性。

一旦所有对象都被初始化,我们将操作的逻辑结果返回给调用者,并完结方法执行。

我们工作的下一阶段是为我们的 CNeuronHyperboloids 类开发前馈通验算法。此处应当提到的是,LogMapLogMapGrad 方法都是包装器,会调用相应的 OpenCL 内核。我们将把这些留给您独立探索。

我们看看 feedForward 方法。在这个方法的参数中,我们收到一个指向神经层对象的指针,其中包含原始数据的张量。

bool CNeuronHyperboloids::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *prev = NeuronOCL;
   CNeuronBaseOCL *centroids = NULL;
   CNeuronBaseOCL *curvatures = NULL;

在方法主体中,我们首先做一些准备工作:我们将声明局部变量,临时存储指向内部神经层对象的指针。其中之一被赋值所接收指向输入数据对象的指针。另两个暂时保持空置。

注意,此刻我们不检查所接收输入数据指针的有效性。该方法在执行期间不直接访问该对象的数据缓冲区。因此,这样的检查是不必要的

接下来,我们继续为当前输入数据集生成质心坐标。为此,我们循环遍历相应的内部模型对象。

//--- Centroids
   for(int i = 0; i < cHyperCentroids.Total(); i++)
     {
      centroids = cHyperCentroids[i];
      if(!centroids ||
         !centroids.FeedForward(prev))
         return false;
      prev = centroids;
     }

在循环主体中,我们依次提取指向神经层对象的指针,并检查其有效性。然后,我们调用所提取内部对象的 feedForward 方法,向其传递来自相应局部变量的输入数据指针。成功执行内部层的前向通验之后,该对象会变成模型下一层的输入数据源。由此,我们将其指针存储在局部输入数据变量之中。

注意,该局部变量最初保存指向从外部程序接收的输入数据对象的指针。因此,在我们循环的第一次迭代期间,我们将它作为输入数据。这意味着,在内部模型层的前馈方法中检查外部数据指针的有效性。因此,所有控制点都是强制的,且来自输入对象的数据流都被保留。

我们组织一个类似的循环来判定质心点处双曲空间的曲率参数。注意,上一个循环的迭代完成之后,局部变量 prevcentroids 两者都指向质心坐标生成模型最终层的对象。由于曲率参数是根据质心坐标判定的,故我们可以放心地使用 prev 变量处理。

//--- Curvatures
   for(int i = 0; i < cHyperCurvatures.Total(); i++)
     {
      curvatures = cHyperCurvatures[i];
      if(!curvatures ||
         !curvatures.FeedForward(prev))
         return false;
      prev = curvatures;
     }

一旦成功获得所有必要的质心参数,我们就可以将输入数据投影到相应的切平面上。为达成这一点,我们调用了上一篇文章中讲述的 LogMap 内核的包装器方法。

   if(!LogMap(NeuronOCL, centroids, curvatures, AsObject()))
      return false;
//---
   return true;
  }

注意,我们传递指向当前对象的指针,作为结果接收实体。这就允许我们将操作结果保存在类的接口缓冲区之中,模型的后续神经层将访问这些缓冲区。

我们现在只需将操作的逻辑结果返回给调用者,并完结前馈通验方法。

在实现前馈方法之后,我们转到开发反向传播算法。在此,我建议专注处理梯度分哦哎的 calcInputGradients 方法。updateInputWeights 方法将留给您独自回顾。

如常,calcInputGradients 方法接收指向前一层对象的指针,我们将基于输入数据对于模型最终输出的影响,计算误差梯度,并传送到该缓冲区。

bool CNeuronHyperboloids::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

这一次,我们立即检查所接收指针的正确性。因为如果您收到的指针有误,所有后续操作都会立即失去意义。

就如同前馈通验,我们声明局部变量,临时存储指向内部模型对象的指针。不过,这一次我们将立即提取指向内部模型最后一层的指针。  

   CObject *next = NULL;
   CNeuronBaseOCL *centroids = cHyperCentroids[-1];
   CNeuronBaseOCL *curvatures = cHyperCurvatures[-1];

之后,我们经由将原始数据投影到切线平面上的操作,调用梯度分派内核的包装方法。

   if(!LogMapGrad(prevLayer, centroids, curvatures, AsObject()))
      return false;

然后,我们根据内部模型分派误差梯度,以便判定质心点处的超空间曲率,从而创建模型神经层的逆向枚举循环。

//--- Curvatures
   for(int i = cHyperCurvatures.Total() - 2; i >= 0; i--)
     {
      next = curvatures;
      curvatures = cHyperCurvatures[i];
      if(!curvatures ||
         !curvatures.calcHiddenGradients(next))
         return false;
     }

然后我们需把来自曲率判定模型的误差梯度,传递给质心坐标生成模型。但此处我们注意到,质心坐标生成模型最后一层的缓冲区已经包含了数据投影到切平面上的操作所产生的误差梯度。我们希望保留这些数值。在这种情况下,我们诉诸替换指向数据缓冲区的指针。首先,我们将质心坐标生成模型最后一层的误差梯度缓冲区的当前指针保存在局部变量之中,并在必要时通过神经层激活函数的导数来调整数值。

   CBufferFloat *temp = centroids.getGradient();
   if(centroids.Activation()!=None)
      if(!DeActivation(centroids.getOutput(),temp,temp,centroids.Activation()))
        return false;
   if(!centroids.SetGradient(centroids.getPrevOutput(), false) ||
      !centroids.calcHiddenGradients(curvatures.AsObject()) ||
      !SumAndNormilize(temp, centroids.getGradient(), temp, iWindows, false, 0, 0, 0, 1) ||
      !centroids.SetGradient(temp, false)
     )
      return false;

然后我们暂时将其替换为相应大小的未使用的缓冲区。我们调用质心坐标生成模型最后一层的误差梯度分派方法,将其传递给质心点处的超空间曲率判定模型的第一层,作为后续对象。我们将两个数据缓冲区的数值求和,并返回指向其原始状态的指针。记住要控制所有操作的执行。

我们已在模型最后一层缓冲区中得到了判定质心坐标的总误差梯度,现在我们能够创建一个循环,遍历模型的神经层反向迭代。在这个循环中,我们根据模型层对最终结果的贡献,规划模型层之间误差梯度的分派。

//--- Centroids
   for(int i = cHyperCentroids.Total() - 2; i >= 0; i--)
     {
      next = centroids;
      centroids = cHyperCentroids[i];
      if(!centroids ||
         !centroids.calcHiddenGradients(next))
         return false;
     }

最后,我们将累积误差梯度传播到输入数据级别。但在此,我们再次面临保留先前累积的误差梯度的问题。故此,我们用数据缓冲区代替输入数据对象。

   temp = prevLayer.getGradient();
   if(prevLayer.Activation()!=None)
      if(!DeActivation(prevLayer.getOutput(),temp,temp,prevLayer.Activation()))
        return false;
   if(!prevLayer.SetGradient(prevLayer.getPrevOutput(), false) ||
      !prevLayer.calcHiddenGradients(centroids.AsObject()) ||
      !SumAndNormilize(temp, prevLayer.getGradient(), temp, iWindows, false, 0, 0, 0, 1) ||
      !prevLayer.SetGradient(temp, false)
     )
      return false;
//---
   return true;
  }

然后我们将操作的结果返回给调用者,并退出该方法。

我们回顾新类 CNeuronHyperboloids 的方法实现至此完结。您能在附件中找到该类的完整代码、及其所有方法。



3. 构建 HypDiff 框架

我们已完成了 HypDiff 框架各个新部件的开发,现到了构造代表框架顶层实现的统一对象的阶段。为达成这一点,我们创建了一个新类 CNeuronHypDiff,其结构如下所示。

class CNeuronHypDiff :  public CNeuronRMAT
  {
public:
                     CNeuronHypDiff(void) {};
                    ~CNeuronHypDiff(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count,
                          uint heads, uint layers, uint centroids,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronHypDiff; }
   //---
   virtual uint      GetWindow(void) const override
     {
      CNeuronRMAT* neuron = cLayers[1];
      return (!neuron ? 0 : neuron.GetWindow() - 1);
     }
   virtual uint      GetUnits(void) const override
     {
      CNeuronRMAT* neuron = cLayers[1];
      return (!neuron ? 0 : neuron.GetUnits());
     }
  };

正如从新类的结构所见,其核心功能继承自 CNeuronRMAT 对象。该基本对象提供了规划小型线性模型操作的功能,这对于实现 HypDiff 框架来说完全足够了。因此,在该阶段,覆盖对象初始化方法足矣,并为嵌入式模型指定正确的架构。所有其它进程都已由父类方法覆盖。

在初始化方法参数中,我们收到主要常量,允许确凿无误地解释在建对象的架构。

bool CNeuronHypDiff::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint window_key, uint units_count, 
                          uint heads, uint layers, uint centroids, 
                          ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      return false;

在方法主体之内,我们立即调用基础神经层对象的相应方法,其中实现了核心接口的初始化。我们有意避免在该阶段调用直接父类的初始化方法,因为我们在建的嵌入式模型的架构有很大不同。

接下来,我们准备继承的动态数组,存储指向内部对象的指针。

   cLayers.Clear();
   cLayers.SetOpenCL(OpenCL);
   int layer = 0;

然后我们直接进行构造 HypDiff 框架的内部架构。

传递到模型中的输入数据,首先被投影到双曲空间当中。为此目的,我们添加了之前创建的 CNeuronHyperProjection 类的实例。

//--- Projection
   CNeuronHyperProjection *lorenz = new CNeuronHyperProjection();
   if(!lorenz ||
      !lorenz.Init(0, layer, OpenCL, window, units_count, optimization, iBatch) ||
      !cLayers.Add(lorenz))
     {
      delete lorenz;
      return false;
     }
   layer++;

HypDiff 框架随后需要一个双曲编码器,旨在为所分析图行的节点生成嵌入。原始框架作者在该阶段采用了图行神经模型与卷积层相结合。在我们的实现中,我们将采用相对位置编码的变换器来替换图行神经网络。

//--- Encoder
   CNeuronRMAT *rmat = new CNeuronRMAT();
   if(!rmat ||
      !rmat.Init(0, layer, OpenCL, window + 1, window_key, units_count, heads, layers, optimization, iBatch) ||
      !cLayers.Add(rmat))
     {
      delete rmat;
      return false;
     }
   layer++;
//---
   CNeuronConvOCL *conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, layer, OpenCL, window + 1, window + 1, 2 * window, units_count, 1, optimization, iBatch) ||
      !cLayers.Add(conv))
     {
      delete conv;
      return false;
     }
   layer++;
   conv.SetActivationFunction(TANH);
//---
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, layer, OpenCL, 2 * window, 2 * window, 3, units_count, 1, optimization, iBatch) ||
      !cLayers.Add(conv))
     {
      delete conv;
      return false;
     }
   layer++;

重点要是要,当生成的嵌入被投影到切平面上时,我们执行全部数据投影到所有切平面,如此显著提升所处理的信息量。为了部分减轻这种方式的负面影响,我们降低了每个节点嵌入的维度。

然后生成的数据嵌入必须被投影到质心的切平面上。质心生成、以及输入数据投影到相应切线空间的功能,均已在 CNeuronHyperboloids 类中实现。此刻,将该对象的实例添加到我们的线性模型中就足够了。

//--- LogMap projecction
   CNeuronHyperboloids *logmap = new CNeuronHyperboloids();
   if(!logmap ||
      !logmap.Init(0, layer, OpenCL, 3, units_count, centroids, optimization, iBatch) ||
      !cLayers.Add(logmap))
     {
      delete logmap;
      return false;
     }
   layer++;

在输出端,我们获得跨多个平面的输入数据的投影。现在就能用最初为欧几里德模型开发的定向扩散算法来处理这些算法。在我们的实现中,我们为此目的而用到了 CNeuronDiffusion 对象。

//--- Diffusion model
  CNeuronDiffusion *diff = new CNeuronDiffusion();
  if(!diff ||
     !diff.Init(0, layer, OpenCL, 3, window_key, heads, units_count*centroids, 2, layers, optimization, iBatch) ||
     !cLayers.Add(diff))
    {
     delete diff;
     return false;
    }
  layer++;

此处需要注意的一个关键层面是,我们未将单个序列元素的各种投影合并到单一实体当中。相较之,我们的扩散模型将每个投影视为一个独立的对象。如此行事,我们令模型能够学习同一序列的不同关联投影,并形成底层数据的三维视图。

另一个值得注意的隐含细节是噪声的注入。我们不欲令模型复杂化,故未选择尝试匹配同一序列元素投影之间的噪声。添加噪声的行为本身就意味着原始输入在某个邻域内的模糊形式。而在不同的投影中引入不同的噪点,我们达成“模糊”的立体化。

在扩散模型的输出中,我们期望获得跨多个投影的输入数据的去噪表示。此处就是我们的实现与原始 HypDiff 框架最彻底的偏离之处。在原版中,作者将数据反向投影到双曲空间中,并用费米-狄拉克解码器重造了原始图行表示。我们的目标是获得输入数据的潜在丰富信息表示,即可传递给参与者模型,以便学习代理人行为的可盈利政策。因此,取代解码,我们应用基于依赖关系的池化层来为每个序列元素派生统一的表示。

//--- Pooling
   CNeuronMHAttentionPooling *pooling = new CNeuronMHAttentionPooling();
   if(!pooling ||
      !pooling.Init(0, layer, OpenCL, 3, units_count, centroids, optimization, iBatch) ||
      !cLayers.Add(pooling))
     {
      delete pooling;
      return false;
     }
   layer++;

将结果张量的大小更改为输入数据的级别。

//--- Resize to source size
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, layer, OpenCL, 3, 3, window, units_count, 1, optimization, iBatch) ||
      !cLayers.Add(conv))
     {
      delete conv;
      return false;
     }

现在我们只需将接口数据缓冲区的指针替换为模型最后一层的相应缓冲区。然后我们就完成了类初始化方法的工作。

//---
   if(!SetOutput(conv.getOutput(), true) ||
      !SetGradient(conv.getGradient(), true))
      return false;
//---
   return true;
  }

我们利用 MQL5 实现 HypDiff 框架的解释到此完结。本文中讨论的所有类和方法的完整源代码可在附件中找到。您还可找到环境交互和模型训练程序的代码,其与之前的工作未加变化。

关于可训练模型的架构的最后几点评论。参与者和评论者模型架构保持不变。不过,我们对环境状态编码器模型进行了轻微修改。如前,该模型的输入数据经由批量归一化层进行初始预处理。

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

之后,它们立即被传递到我们的双曲潜在扩散模型之中。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronHypDiff;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.window_out = BarDescr;
   descr.layers=2;
   descr.step=10;                                        // centroids
   {
      int temp[] = {4};                                  // Heads
      if(ArrayCopy(descr.heads, temp) < (int)temp.Size())
         return false;
   }
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

上述双曲潜在扩散模型的算法是一个相当复杂和综合的过程。因此,我们排除了进一步的数据处理。我们仅用一个全连接层将数据降低至所需的张量大小,且将其输入到参与者模型之中。

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

此刻,我们结束了 HypDiff 框架方式的实现,并转到最令人期待的阶段 — 依据真实历史数据的实际成果评估。



4. 测试

我们已利用 MQL5 实现了 HypDiff 框架,现在进入最后阶段 — 训练模型,并评估出品的参与者政策。我们遵循之前工作中描述的训练算法,同时训练三个模型:状态编码器参与者、和评论者编码器分析市场环境。参与者基于学到的政策制定交易决策。评论者评估参与者动作,并指导政策完善。

训练是采用 EURUSD 金融产品 2023 年全年的真实历史数据进行的,时间帧为 H1。所有指标参数均按其默认值设置。

训练过程是迭代的,包括对训练数据集的定期更新。

为了验证经过训练的政策的有效性,我们采用 2024 年第一季度的历史数据。测试结果呈现如下。

数据显示,该模型在测试期间成功产生了盈利。三个月内总共执行了 23 笔交易,这是一个相对较少的数字。超过 56% 的交易以盈利了结。每笔交易的最大盈利和平均盈利大约是亏损的两倍。

然而,更令人信服的见解来自交易的详情细分。在三个月的测试中,该模型只有两个月盈利。二月份完全无盈利。2024 年 1 月,8 笔交易中有 7 笔盈利 — 唯一的亏损发生在当月的最后一笔交易当中。这一结果支持了前面所述的假设,即在模型部署的第一个月之后,一年期的训练样本代表性有限。

周内几天的绩效分析还显示,人们明显偏好在周四和周五交易。



结束语

双曲几何的应用有助于解决图性结构数据的离散性质、与扩散模型的连续性之间固有冲突所带来的挑战。HypDiff 框架引入了一种生成双曲高斯噪声的强化方法,解决了有关与双曲空间中高斯分布的可加性不一致的问题。为了在各向异性扩散期间保持局部结构,施加了基于角度相似性的几何约束。

在我们工作的实践部分,我们利用 MQL5 实现了对这些方法的解释,并运用提议技术在真实历史数据上训练了模型。我们还评估了参与者依据训练集之外数据学到的政策。结果证明了所提议方法的潜力,并为提高模型性能指明了可能的方向。


参考

文章中所用程序

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

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

附加的文件 |
MQL5.zip (2101.97 KB)
最近评论 | 前往讨论 (1)
Mulukas
Mulukas | 9 7月 2025 在 07:43
它在技术上令人印象深刻,但实际效果却相当一般。
在MQL5中创建交易管理员面板(第八部分):分析面板 在MQL5中创建交易管理员面板(第八部分):分析面板
今天,我们将深入探讨如何在管理员面板EA的一个集成专用窗口中,加入有用的交易指标。本次讨论的重点是使用MQL5实现一个分析面板,并强调其所提供数据对交易管理员的价值。其影响主要体现在教学意义上,因为整个开发过程能提炼出宝贵的经验教训,使新手和经验丰富的开发者都能从中受益。此功能展示了我们开发的系列工具在为交易经理配备先进软件工具方面所提供的无限可能。此外,作为对交易管理员面板能力的持续扩展,我们将探讨PieChart(饼图)和ChartCanvas(图表画布)类的实现。
使用MQL5经济日历进行交易(第五部分):添加响应式控件和过滤按钮的增强型仪表盘 使用MQL5经济日历进行交易(第五部分):添加响应式控件和过滤按钮的增强型仪表盘
在本文中,我们创建了用于货币对过滤、重要性级别过滤、时间过滤以及取消选项的按钮,以改进仪表盘的控制功能。通过编程让这些按钮能够动态响应用户操作,实现无缝交互。我们还对其行为进行了自动化处理,以便在仪表盘上实时反映变化。这样就提升了面板的整体功能性、灵活性和响应速度。
市场轮廓指标 (第二部分):基于画布的优化与渲染 市场轮廓指标 (第二部分):基于画布的优化与渲染
本文探讨了一种优化后的市场轮廓指标,该版本用基于 CCanvas 类对象(即画布)的渲染,取代了原先使用多个图形对象进行渲染的方式。
MQL5 交易工具包(第 3 部分):开发挂单管理 EX5 库 MQL5 交易工具包(第 3 部分):开发挂单管理 EX5 库
了解如何在 MQL5 代码或项目中开发和实现全面的挂单 EX5库。本文将向您展示如何创建一个全面的挂单管理 EX5 库,并通过构建交易面板或图形用户界面(GUI)来指导您导入和实现它。EA 交易订单面板将允许用户直接从图表窗口上的图形界面打开、监控和删除与指定幻数相关的挂单。