English Русский Español Português
preview
交易中的神经网络:多智代自适应模型(MASA)

交易中的神经网络:多智代自适应模型(MASA)

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

概述

计算机技术正在成为金融分析不可或缺的一部分,为解决复杂问题提供了创新方式。近年来,强化学习(RL)在动荡金融市场条件下的动态投资组合管理中已被证明了其有效性。然而,现有方法往往专注于最大回报,而对风险管理的重视不足,尤其是在流行病、自然灾害、和地区冲突造成的不确定性下。

为了解决这一限制,《开发具有深度强化学习的多智代和自适应框架,进行动态投资组合风险管理》的研究引入了 MASA多智代自适应)。MASA 集成了两种交互智代:第一个使用 TD3 算法优化回报,而第二个通过进化算法、或其它优化方法将风险降至最低。此外,MASA 还集成了一个市场观察器,利用深度神经网络来分析市场趋势,并提供反馈。

作者依据过去 10 年的 CSI 300 指数、道琼斯工业平均指数DJIA)、和标准普尔 500 指数的数据测试了 MASA。它们的结果表明,MASA 在投资组合管理方面优于传统的基于 RL 的方式。


1. MASA 算法

为了克服传统 RL 方式的局限性,其往往过于关注回报优化,作者提出了一种多智代自适应架构(MASA)。该结构采用两种交互式反应性智代(一种基于 RL,另一种基于交替优化算法),来建立一种全新的多智代 RL 制程。意向是权衡投资组合回报与潜在风险之间的动态平衡,特别是在波动的市场条件下。

在该架构内,基于 TD3 算法构建的 RL 智代可优化整体投资组合回报。同时,交替优化智代会调整 RL 智代生成的投资组合,采纳市场观察器提供的市场趋势评估,将风险降至最低。

这种清晰的功能分离,令模型能够不断学习和适应金融市场的底层动态。如是结果,相比仅基于 RL 的方式,MASA 在盈利能力和风险方面产生了更平衡的投资组合。

重要的是,MASA 框架在其三个交互智代间采用松散耦合、类似管道的计算模型。因此,基于多智代 RL 的一般方式确保了弹性和健壮性:即使其中一个智代出现故障,系统也能继续有效运行。

在迭代训练过程开始之前,所有相关信息都会被初始化,包括 RL 政策、以及由市场观察器智代维护的市场状态数据。

在训练期间,收集有关当前市场状态 Ot 的信息(例如,过去几个交易日底层市场的最新上升或下降趋势),以供市场观察器分析。与之并行,将先前执行的动作 At1 的最终奖励,作为 RL 算法的反馈,以便细化 RL 智代行为政策。

然后激活市场观察器来计算提议的风险边界 σs,t、以及市场向量 Vm,t,它们作为更新 RL 智代和控制器的附加特征,从而响应当前市场条件。

为了确保灵活性和适应性,MASA 框架可以结合多种方式,包括算法模型、或深度神经网络。更重要的是,基于 RL、及基于交替优化的智代都通过持续访问当前市场信息而受到内在保护,其提供了来自交易环境的最有价值的反馈。市场观察器生成的洞察仅当作辅助信息,令 RL 智代和控制器更快速地适应、并提高性能,特别是在高波动性的市场中。

在最坏的场景下,当市场观察器因“噪音”而产生误导性信号,可能影响其它智代的决策时,RL 奖励机制的自适应性,允许系统在后续训练迭代中与底层交易环境重新看齐。进而,市场观察器随时间推移自我纠正的能力,有助于减缓误导性信号的影响,确保在较长交易横向范围内保持稳定性。

实验结果表明,基于 RL 的智代和交替优化器都表现出可观的性能提升 — 即使市场观察器的实现伴以相对简单的算法方法。依据十多年区间的 CSI 300DJIA、和 S&P 500 等复杂数据集上进行测试,结果凸显出 MASA 的稳健性。

无论如何,为了充分理解市场观察器的输入对应用了所提议 MASA 框架的其它两个智代的长期影响,需要针对更复杂的数据集、及不同的应用领域进行深入分析。

激活市场观察器后,RL 智代生成当前动作 At,及投资组合权重形式的 RL。然后,控制器或许会修订这些权重,其在参考自己的风险管理策略、以及观察器辨别的市场条件后应用交替优化算法。得益于这种松散耦合式、基于管道的模型,MASA 充当健壮的多智代系统(MAS),即使某个智代出现故障,也能维持操作完整性。

在其基于奖励机制的指导下,MASA 无缝适配不断变化的环境。决策置顶智代遵照市场观察器的宝贵反馈,迭代强化投资组合的回报和风险目标期望值。同时,其奖励机制协同基于熵的背离量值,促进生成的动作集的多样性,作为应对不同金融市场波动的智能和适应性策略。

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


2. 利用 MQL5 实现

在回顾了 MASA 方法的理论层面之后,我们现在转入本文的实践部分,我们将实现所提议方式解释的 MQL5 版本。

如早前所述,MASA 框架由三个智代组成。为了提高代码的可读性和清晰性,我们将为每个智代单独创建一个对象,然后将它们组合至一个统一的结构。

2.1市场观察器智代


我们首先开发市场观察器智代。MASA 作者强调,可将多种算法应用于市场分析 — 从简单的分析方法,到高级深度学习模型。市场观察器的主要任务是辨别关键趋势,从而预测即将发生的最可能的走势。

在我们的实现中,我们采用混合方式。首先,我们应用分段线性表示算法来捕捉当前的市场趋势。接下来,我们使用含相对位置编码的注意力模块分析独立单变量序列的已识别趋势之间的依赖关系。最后,在输出阶段,我们尝试使用一个 MLP 来预测所定义计划横向范围内的最可能市场行为。

市场观察器的这种复合算法封装在一个新对象 CNeuronMarketObserver 之中。其结构呈现如下。

class CNeuronMarketObserver   :  public CNeuronRMAT
  {
public:
                     CNeuronMarketObserver(void)   {};
                    ~CNeuronMarketObserver(void)   {};
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count,
                          uint heads, uint layers, uint forecast,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronMarketObserver; }
  };

该算法遵循线性结构。对于这样的结构,设计支持小比例线性模型的 CNeuronRMAT 类是适合新对象的父类。这允许我们在 Init 初始化方法内定义市场观察器的主要结构。而主要功能已由父类处理。

Init 方法的参数指定市场观察器智代架构的常量定义。这些包括:

  • window — 描述单个序列元素的向量大小(单变量时间序列的数量);
  • window_key — 内部注意力组件(查询、主键、数值)的维度;
  • units_count — 分析数据的历史深度;
  • heads — 注意力头的数量;
  • layers — 注意力层的数量;
  • forecast — 即将到来的走势的预测范围。

bool CNeuronMarketObserver::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                 uint window, uint window_key, uint units_count,
                                 uint heads, uint layers, uint forecast,
                                 ENUM_OPTIMIZATION optimization_type, uint batch)
  {
//--- Init parent object
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * forecast,
                                                   optimization_type, batch))
      return false;

在该方法中,我们首先调用基础全连接层的初始化方法,其充当代码库中所有神经层的根父类。父类方法提供对象基本接口的初始化。

此处有两个要点应当注意:首先,我们调用基类初始化方法,而非直系父级的初始化方法。这是因为市场观察器的架构与其父类的架构有很大不同。

其次,在调用父初始化方法时,我们将对象大小指定为规划横向范围,与序列元素向量大小的乘积。这与我们期望市场观察器输出的张量相匹配。

然后我们清除指向内部对象的指针的动态数组:

//--- Clear layers' array
   cLayers.Clear();
   cLayers.SetOpenCL(OpenCL);

在该阶段,准备工作已经就绪,我们可进行市场观察器智代的实际构造。

该模型期望一个多模态时间序列作为输入,表示为描述单个系统状态的向量序列(在我们的例子中为柱线图)。为了正确处理每个单变量序列,必须首先转置收入的数据。

//--- Tranpose input data
   int lay_count = 0;
   CNeuronTransposeOCL *transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, lay_count, OpenCL, units_count, window, optimization, iBatch) ||
      !cLayers.Add(transp))
     {
      delete transp;
      return false;
     }

然后我们把它们转换为分段线性表示。

//--- Piecewise linear representation
   lay_count++;
   CNeuronPLROCL *plr = new CNeuronPLROCL();
   if(!plr ||
      !plr.Init(0, lay_count, OpenCL, units_count, window, false, optimization, iBatch) ||
      !cLayers.Add(plr))
     {
      delete plr;
      return false;
     }

为了分析这些单变量序列之间的依赖关系,我们使用含有相对位置编码的注意力模块,并配以所需数量的内部层进行参数化。

//--- Self-Attention for Variables
   lay_count++;
   CNeuronRMAT *att = new CNeuronRMAT();
   if(!att ||
      !att.Init(0, lay_count, OpenCL, units_count, window_key, window, heads, layers,
                                                                optimization, iBatch) ||
      !cLayers.Add(att))
     {
      delete att;
      return false;
     }

基于注意力模块的输出,我们尝试预测每个单变量序列即将到来的数值。在此,我们用残差卷积块(CResidualConv)作为 MLP 替代品,以便独立预测每个单变量时间序列的数值。

//--- Forecast mapping
   lay_count++;
   CResidualConv *conv = new CResidualConv();
   if(!conv ||
      !conv.Init(0, lay_count, OpenCL, units_count, forecast, window, optimization, iBatch) ||
      !cLayers.Add(conv))
     {
      delete conv;
      return false;
     }

最后,将预测结果转换至原始输入数据的维度。

//--- Back transpose forecast
   lay_count++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, lay_count, OpenCL, window, forecast, optimization, iBatch) ||
      !cLayers.Add(transp))
     {
      delete transp;
      return false;
     }

为了最大限度地减少数据复制操作,我们应用了带有外部接口缓冲区的指针替换技术,以便实现高效的内存管理。

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

然后,该方法将操作的布尔结果返回给调用者,并结束。

该类的核心功能继承自其父对象。因此,市场观察器智代现在完成。附件中提供了该类的完整源代码。

2.2RL 智代


下一步是构建 RL 智代。在 MASA 框架中,该智代的操作与市场观察器并行,进行独立的市场分析,并基于其学到的政策制定决策。

MASA 作者建议基于 TD3 模型来实现 RL 智代。不过,我们采用了不同的 RL 智代架构。对于独立的环境分析,我们使用 PSformer 框架。基于所执行的分析制定决策,由配以 SAM 优化强化的轻量级感知器处理。

我们的 RL 智代在新对象 CNeuronRLAgent 中实现。其结构如下图所示。

class CNeuronRLAgent :  public   CNeuronRMAT
  {
public:
                     CNeuronRLAgent(void) {};
                    ~CNeuronRLAgent(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint units_count, uint segments, float rho,
                          uint layers, uint n_actions,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   override {  return defNeuronRLAgent;   }
  };

与市场观察器类似,我们此处所用继承自线性模型基类 CNeuronRMAT。因此,我们只需在 <Init 方法中指定新模块的架构。

bool CNeuronRLAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint units_count, uint segments,
                          float rho, uint layers, uint n_actions,
                          ENUM_OPTIMIZATION optimization_type, uint batch)
  {
//--- Init parent object
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, n_actions, optimization_type, batch))
      return false;

方法参数与市场观察器的方法参数非常相似。不过,也存在一些差异。例如,forecast 横向范围参数将替换为智代的操作空间(n_actions)。此外,其它参数还包括 segments(区段数量),和 rho(模糊系数)。

在 Init 方法中,我们调用基础全连接层的初始器,指定我们 RL 智代的动作空间作为输出张量大小。

然后我们清除内部对象指针的动态数组。

//--- Clear layers' array
   cLayers.Clear();
   cLayers.SetOpenCL(OpenCL);

模型的输入数据首先传递到 PSformer,在循环中创建其所需层数。

//--- State observation
   int lay_count = 0;
   for(uint i = 0; i < layers; i++)
    {
     CNeuronPSformer *psf = new CNeuronPSformer();
     if(!psf ||
        !psf.Init(0, lay_count, OpenCL, window, units_count, segments, rho, optimization,iBatch)||
        !cLayers.Add(psf))
       {
        delete psf;
        return false;
       }
     lay_count++;
    }

然后,RL 智代通过将所执行分析的输出投喂到由卷积层和全连接层组成的决策模块,据最优动作制定决策。卷积层降维。

   CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(n_actions, lay_count, OpenCL, window, window, 1, units_count,
                                                      1, optimization, iBatch) ||
      !cLayers.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(GELU);
   lay_count++;

全连接层生成动作张量。

   CNeuronBaseSAMOCL *flat = new CNeuronBaseSAMOCL();
   if(!flat ||
      !flat.Init(0, lay_count, OpenCL, n_actions, optimization, iBatch) ||
      !cLayers.Add(flat))
     {
      delete flat;
      return false;
     }
   SetActivationFunction(SIGMOID);

注意,在这种情况下,我们不使用参与者的随机政策。不过,我们将来或许仍需用到,这个我们稍后讨论。

默认情况下,动作向量使用 sigmoid 激活函数,将数值限制在 0 到 1 之间的范围内。如果需要,可由外部程序覆盖。

如前,我们替换指向外部接口数据缓冲区的指针,然后返回初始化的布尔结果。

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

如此,RL 智代对象就完成了。该类及其所有方法的完整代码可在附件中找到。

2.3控制器


我们已构造了三个智代对象之中的两个。现在我们转向第三个组件:控制器智代。它的角色是基于由市场观察器执行的环境状态分析,评估风险,并调整 RL 智代的动作。

这里的主要区别在于控制器处理两个输入数据源。它不仅必须单独评估每个输入,还必须评估它们的相互依赖关系。以我观点,变换器解码器结构非常适合这项任务。然而,我们使用相对位置编码变体,替代标准的自注意力交叉注意力模块。

控制器的实现,作为继承自 CNeuronRMATCNeuronControlAgent 新对象。由于它可处置双输入流,故需要重新定义若干方法。新类结构如下所示。

class CNeuronControlAgent  :  public CNeuronRMAT
  {
protected:
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, 
                                 CBufferFloat *SecondInput) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, 
                                        CBufferFloat *SecondInput, 
                                        CBufferFloat *SecondGradient, 
                                        ENUM_ACTIVATION SecondActivation = None) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, 
                                        CBufferFloat *SecondInput) override;

public:
                     CNeuronControlAgent(void) {};
                    ~CNeuronControlAgent(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count, uint heads,
                          uint window_kv, uint units_kv, uint layers,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronControlAgent; }
  };

再次,内部对象的初始化是在 Init 方法中执行的,其中指定了变换器解码器的架构。

bool CNeuronControlAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                               uint window, uint window_key, uint units_count,
                               uint heads, uint window_kv, uint units_kv, uint layers,
                               ENUM_OPTIMIZATION optimization_type, uint batch)
  {
//--- Init parent object
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, 
                                                     optimization_type, batch))
      return false;
//--- Clear layers' array
   cLayers.Clear();
   cLayers.SetOpenCL(OpenCL);

如前面的情况,我们调用基础全连接层的相关方法,在主输入张量级别指定输出的维数。之后,我们清除动态指针数组。

接下来,我们继续构造控制器智代的架构。回想一下,市场观察器输出一个多模态时间序列,这是代表环境预测状态(柱线)的一连串向量。

有两种可能方式:将 RL 智代动作与单根柱线或单变量序列对齐。我们的理解是,市场观察器只向我们提供了预测数据,实现的概率远非 100%。还有,所有值都绝对存在偏差的可能性。

我们从逻辑上思考。描述一个预测烛条的向量能给我们多少信息,假设每个元素值都可能存在不同的偏差?这个问题是有争议的,如果不了解个别预测的准确性,就很难回答。

另一方面,如果您查看单独的单变量序列,那么,除了单个值之外,您还能辨别即将到来的走势趋势。由于趋势是由一组数值形成的,因此我们能从逻辑上预期趋势的确认,即使有些元素有所偏差。

甚至,我们的多模态时间序列中的所有单变量序列都具有一定的相互依赖性。故此,当一个单变量时间序列的预测趋势被另一个单变量时间序列值确认时,这种预测的概率就会增加。

考虑到这一点,我们决定分析智代行为对单变量序列预测值的依赖性。相应地,我们首先将二级数据源投喂到准备好的神经层。

   int lay_count = 0;
   CNeuronBaseOCL *flat = new CNeuronBaseOCL();
   if(!flat ||
      !flat.Init(0, lay_count, OpenCL, window_kv * units_kv, optimization, iBatch) ||
      !cLayers.Add(flat))
     {
      delete flat;
      return false;
     }

然后我们将其重新格式化为单变量序列表示。

   lay_count++;
   CNeuronTransposeOCL *transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, lay_count, OpenCL, units_kv, window_kv, optimization, iBatch) ||
      !cLayers.Add(transp))
     {
      delete transp;
      return false;
     }
   lay_count++;

接下来,我们需要构造解码器架构。在循环中创建所需数量的层。迭代次数由初始化方法的外部参数决定。

//--- Attention Action To Observation
   for(uint i = 0; i < layers; i++)
     {
      if(units_count > 1)
        {
         CNeuronRelativeSelfAttention *self = new CNeuronRelativeSelfAttention();
         if(!self ||
            !self.Init(0, lay_count, OpenCL, window, window_key, units_count, heads,
                                                               optimization, iBatch) ||
            !cLayers.Add(self))
           {
            delete self;
            return false;
           }
         lay_count++;
        }

重点要注意,在雏形变换器解码器中,输入数据首先由自注意力模块处理,其分析源序列的各个元素之间的依赖关系。在我们的实现中,我们将该模块替换为使用相对位置编码的对应模块。不过,仅当源序列包含多个元素时,我们才会创建该模块。因为,显然,若仅有一个元素,就无必要分析依赖关系。在这种情况下,自注意力模块就是多余的。

接下来,我们创建一个交叉注意力模块,其分析两个数据源元素之间的依赖关系。

      CNeuronRelativeCrossAttention *cross = new CNeuronRelativeCrossAttention();
      if(!cross ||
         !cross.Init(0, lay_count, OpenCL, window, window_key, units_count, heads, 
                                        units_kv, window_kv, optimization, iBatch) ||
         !cLayers.Add(cross))
        {
         delete cross;
         return false;
        }
      lay_count++;

然后,每个解码器层都由一个 FeedForward 模块完成,为此我们使用一个残差卷积模块。

      CResidualConv *ffn = new CResidualConv();
      if(!ffn ||
         !ffn.Init(0, lay_count, OpenCL, window, window, units_count, optimization, iBatch) ||
         !cLayers.Add(ffn))
        {
         delete ffn;
         return false;
        }
      lay_count++;
     }

此后,我们进行下一次循环迭代,并构造以下解码器层。

如同标准变换器解码器架构,残差卷积模块的输出也会经历归一化。不过,我们或许还需要将参与者的动作空间限制在定义范围内,典型情况下其通常由激活函数处理。因此,在构造所需数量的解码器层后,我们添加一个含有指定激活函数的额外卷积层。

   CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL();
   if(!conv ||
      !conv.Init(0, lay_count, OpenCL, window, window, window, units_count, 1,
                                                       optimization, iBatch) ||
      !cLayers.Add(conv))
     {
      delete conv;
      return false;
     }
   SetActivationFunction(SIGMOID);

默认情况下,如同 RL 智代,我们使用 sigmoid 函数。不过,我们保留了从外部程序覆盖它的选项。

最后,在初始化方法的最后,我们替换接口缓冲区的指针,并向调用程序返回一个布尔值,表示操作成功。

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

新对象的初始化完成后,我们转至 feedForward 方法中构造前馈算法。

bool CNeuronControlAgent::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!SecondInput)
      return false;

在方法参数中,我们接收指向两个输入数据对象的指针。主数据流作为神经层传递,二级数据流作为数据缓冲区提供。为了便于使用,我们将专门创建的内层结果缓冲区替换为方法参数中接收的对象。

   CNeuronBaseOCL *second = cLayers[0];
   if(!second)
      return false;
   if(!second.SetOutput(SecondInput, true))
      return false;

然后,我们将二级数据源的张量转置为多模态时间序列表示,作为单变量序列。

   second = cLayers[1];
   if(!second || !second.FeedForward(cLayers[0]))
      return false;

接下来,我们按顺序迭代其余的内部神经层,调用它们的前馈方法,并传递它们两个的数据源。

   CNeuronBaseOCL *first = NeuronOCL;
   CNeuronBaseOCL *main = NULL;
   for(int i = 2; i < cLayers.Total(); i++)
     {
      main = cLayers[i];
      if(!main ||
         !main.FeedForward(first, second.getOutput()))
         return false;
      first = main;
     }
//---
   return true;
  }

所有循环迭代成功完成后,我们只需向调用者返回一个布尔结果,表示执行成功。

如是所见,前馈算法相对简单。这要归功于使用预构建模块来打造更复杂的架构。

由于使用了二级数据源,在实现误差梯度分布算法时,这种情况变得更加具有挑战性。沿着主数据路径,信息从一个内层按顺序传递到下一个内层。不过,二级数据源在所有解码器层之间共享。特别是跨所有交叉注意力模块。由此,必须从所有交叉注意力模块收集二级数据的误差梯度。我建议在代码中查看该问题的解决方案。

该逻辑在 calcInputGradients 方法中实现。方法参数包括指向两个输入数据流、及其相应误差梯度的指针。我们的任务是根据两个数据源对最终输出的贡献来分配两个数据源之间的误差梯度。

bool CNeuronControlAgent::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
                              CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1)
  {
   if(!NeuronOCL || !SecondGradient)
      return false;

在方法内,我们首先验证接收的指针,因为我们无法将数据传递给不存在的对象。

正如前向通验,二级数据源表示为缓冲区。我们将相应内层的指针替换为对应的内层。

   CNeuronBaseOCL *main = cLayers[0];
   if(!main)
      return false;
   if(!main.SetGradient(SecondGradient, true))
      return false;
   main.SetActivationFunction(SecondActivation);
//---
   CNeuronBaseOCL *second = cLayers[1];
   if(!second)
      return false;
   second.SetActivationFunction(SecondActivation);

在该阶段,我们还将内层和后续转置层的激活函数、与输入数据的激活函数同步。这就确保了正确的梯度传播。

转置层现在充当二级数据源。为方便起见,我们将指向其接口对象的指针存储在局部变量之中。

   CBufferFloat *second_out = second.getOutput();
   CBufferFloat *second_gr = second.getGradient();
   CBufferFloat *temp = second.getPrevOutput();
   if(!second_gr.Fill(0))
      return false;

我们清除其梯度缓冲区中任何先前累积的数值。

然后我们向后迭代遍历内部神经层。注意,在循环中,我们仅与解码器对象打交道。

由于前两个内部对象是为处理二级数据源而保留的。

   for(int i = cLayers.Total() - 2; i >= 2; i--)
     {
      main = cLayers[i];
      if(!main)
         return false;

对于每个解码器层,我们从数组中提取其指针,并对其进行验证。

由于并非所有解码器模块都配以两个数据源运作,故算法分支会取决于对象类型。对于交叉注意力模块,我们首先将当前层的二级数据源误差梯度传递到临时存储缓冲区当中,然后将这些数值与先前存储的结果一起累加。

      if(cLayers[i + 1].Type() == defNeuronRelativeCrossAttention)
        {
         if(!main.calcHiddenGradients(cLayers[i + 1], second_out, temp, SecondActivation) ||
            !SumAndNormilize(temp, second_gr, second_gr, 1, false, 0, 0, 0, 1))
            return false;
        }

至于其它模块,我们简单地沿主路径传播误差梯度。然后我们转到循环的下一次迭代。

      else
        {
         if(!main.calcHiddenGradients(cLayers[i + 1]))
            return false;
        }
     }

所有迭代完成后,我们将累积的梯度传播回输入数据源。首先,我们沿主路径将梯度传递到第一个数据源。

   if(!NeuronOCL.calcHiddenGradients(main.AsObject(), second_out, temp, SecondActivation))
      return false;

不过,回忆一下,第一个解码器层或许是自注意力交叉注意力模块。在后一种情况下,用到两个数据源。故此,我们必须检查对象类型,并在必要时添加第二个数据源的累积梯度。

   if(main.Type() == defNeuronRelativeCrossAttention)
     {
      if(!SumAndNormilize(temp, second_gr, second_gr, 1, false, 0, 0, 0, 1))
         return false;
     }

最后,我们将完整的累积梯度沿二级路径传递至相应的输入源。

   main = cLayers[0];
   if(!main.calcHiddenGradients(second.AsObject()))
      return false;
//---
   return true;
  }

该方法返回逻辑结果至调用程序,并结束。

updateInputWeights 方法中实现的参数更新算法相对简单。我们循环遍历包含可训练参数的内部对象,调用它们相应的更新方法。我们在此就不赘述了。但重点要注意,构造的对象架构使用含有 SAM 优化参数的模块。因此,必须按逆顺序迭代遍历内部对象。

据此,我们对于实现控制器智代方法算法的讨论完毕。附件中提供了新类及其所有方法的完整代码。

我们今天取得了实质性进展,尽管我们的工作尚未完成。我们将短暂休息,在下一篇文章中,我们将由该项目得出合乎逻辑的结论。


结束语

我们研究了一种在动荡的金融市场条件下进行投资组合管理的创新方法 — 多智代自适应(MASA)框架。所提议框架成功地结合了用于收益优化的 RL 算法、用于风险最小化的自适应优化方法、以及用于趋势分析的市场观察器模块的优点。

在实践章节,我们为提议的每个智代都实现了 MQL5 版本独立模块。在下一篇文章中,我们将会把它们集成到一个完整的系统当中,并依据真实历史数据评估已实现解决方案的性能。


参考

文章中所用程序

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

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

附加的文件 |
MQL5.zip (2195.85 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
从基础到中级:联合(二) 从基础到中级:联合(二)
今天我们有一篇非常有趣的文章。我们将研究联合并尝试解决之前讨论的问题。我们还将探讨在应用程序中使用联合时可能出现的一些不寻常的情况。此处提供的材料仅用于教学目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
价格行为分析工具包开发(第七部分):信号脉冲智能交易系统(EA) 价格行为分析工具包开发(第七部分):信号脉冲智能交易系统(EA)
借助“信号脉冲(Signal Pulse)”这款MQL5智能交易系统(EA),释放多时间框架分析的潜力。该EA整合了布林带(Bollinger Bands)和随机震荡器(Stochastic Oscillator),以提供准确、高概率的交易信号。了解如何实施这一策略,并使用自定义箭头有效直观地显示买入和卖出机会。非常适合希望借助多时间框架的自动化分析来提升自身判断能力的交易者。