English Русский Español Deutsch 日本語 Português
preview
神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)

神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)

MetaTrader 5交易系统 | 12 六月 2024, 12:30
229 0
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

决策转换器是解决各种实际问题的强力工具。这大都通过转换器关注度方法达成的。先前的实验表明,使用转换器架构还需要长期而彻底的模型训练。而这反过来又需要准备已标记训练数据。在解决实际问题时,有时很难获得奖励,而已标记数据不能很好地扩展训练集。如果我们在预训练期间不使用奖励,模型可以获得一般化的行为模式,其可很容易地适应以后的各项任务。

在本文中,我邀请您来掌握称为预训练决策转换器(PDT)的 RL 预训练方法,它是于 2023 年 5 月的《决策转换器的未来条件下无监督预训练》一文中介绍。这种方法令 DT 有能力在没有奖励标记的情况下,使用次优数据进行训练。特别是,该方法的作者研究了一种预训练场景,其中模型首先依据先前收集的轨迹上离线训练,没有奖励标记,然后通过在线交互优调目标任务。

为了进行有效的预训练,模型必须能够在没有奖励的情况下提取多方面和通用的学习信号。在预训练期间,模型必须通过判定哪些学习信号可以与奖励相关联,来快速适应奖励任务。

PDT 仅依赖过去信息来学习未来轨迹的嵌入空间,以及未来先验条件。目标未来嵌入通过条件化动作预测,PDT 被赋予了“对未来进行推理”的能力。这种能力自然是独立于任务的,可以推广到不同的任务规范。

为了在下游任务中实现高效的在线优调,轻松令框架适应新条件,您可将每个未来嵌入与其回报相关联,这是针对每个未来嵌入,训练奖励预测网络来实现的。

我建议转到本文的下一章节,并详细研究预训练决策转换器方法。


1. 预训练决策转换器方法(PDT)

PDT 方法基于 DT 的原理。它还在分析访问状态和已完成动作的顺序之后,预测智能体的动作。同时,PDT 为 DT 算法引入了附加功能,允许依据未标记数据进行初步模型训练,即无需分析回报。这看似不可能,因为“在途回报”(未来奖励)是模型分析的序列成员之一,并充当空间中模型朝向的一种指南针。

PDT 方法的作者提议用一些潜在状态向量 Z 代替 RTG。这个思路并不新鲜,但作者为其给出了一个相当有趣的解释。在依据未标记数据进行初步训练的过程中,我们实际上会训练 3 个模型:

  • 扮演者,这是一个经典 DT,基于前一个轨迹的分析进行动作预测;
  • 目标预测模型 P(•|St) — 基于当前状态的分析预测 DT 目标(潜在状态 Z);
  • 未来编码器模型 G(•|τt+1:t+k) — “展望未来”并将其嵌入到潜在状态 Z 之中。

注意,最后 2 个模型分析不同的数据,但都返回潜在向量 Z。这在当前状态和未来状态之间构建了一种自动编码器。其潜在状态用作 DT(扮演者)的目标称谓。

不过,模型训练不同于自动编码器训练。首先,我们通过在未来轨迹和所采取的动行之间建立依赖关系,来训练未来的编码器和扮演者。我们允许 PDT 展望未来,了解一些规划界限。我们将有关后续轨迹的信息压缩为潜在状态。以这种方式,我们允许模型基于有关未来的可用信息制定决策。我们期望在初步训练期间创建具有广泛行为技能的扮演则政策,不受环境奖励的限制。

然后,我们训练一个目标预测模型,寻找当前状态与未来轨迹的学习嵌入之间的依赖关系。

这种方式令我们能够将奖励与目标出来的结果分开,为大规模的持续预学习开启了机遇。同时,当智能体的行为明显偏离预期目标时,它减少了行为不一致的问题。

虽然使用目标预测模型 P(Z|St) 对于未来的潜在变量进行采样、及生成模仿训练数据集分布的行为很实用,它不用为任何特定于任务的数据编码。因此,有必要发送 P(Z|St) 到未来嵌入的数据集,这些嵌入在下游学习期间会带来较高的未来奖励。

这导致为 DT 创建智能系统行为,条件是回报最大化。与通过分配标量目标奖励来控制回报最大化政策不同,我们需要调整目标预测模型 P(Z|St)。由于这种分布是未知的,我们使用了一个额外的奖励预测模型 F(•|Z, St) 来预测最优轨迹。奖励预测模型在下游训练过程中与所有其它模型一起学习。

类似于预训练,我们使用未来编码器来获取潜在状态,这允许梯度向后传播,从而调整潜在表示中奖励数据的编码。这允许在下游学习过程中解决任务的特殊之处。

下面是原文章中预训练决策转换器方法的可视化。


2. 利用 MQL5 实现

现在我们已研究过预训练决策转换器方法的理论方面,我们可以转到本文的实践部分,并讨论该方法在 MQL5 中的实现。在本文中,我们将专注于训练数据集收集 EA。在之前的文章中,我们研究过从决策转换器家族中构建算法的若干种选项。它们都包含类似的经验回放缓冲区。我们用它初始收集训练数据集。在我的工作中,我会用上一篇文章中收集的经验回放缓冲区。我们通过随机抽样动作来收集它,并未参考特定模型(针对出去探索方法的实现)。

2.1. 模型架构

由于我们已经有了一套训练数据,我们转到下一个阶段,即无监督预训练。如上所述,在此阶段,我们将训练 3 个模型。我们从模型架构的定义开始我们的工作,其收集在 CreateDescriptions 方法中。

bool CreateDescriptions(CArrayObj *agent, CArrayObj *planner, CArrayObj *future_embedding)
  {
//---
   CLayerDescription *descr;

在参数中,该方法接收指向 3 个动态数组的指针,我们将在其中添加模型神经层的架构描述。

在方法的主体中,我们声明一个局部变量,填写一个指向神经层定义的对象指针。在该变量中,我们将“保留”一个指向我们的单独模块中正在处理的对象指针。

首先,我们将描述智能体的架构。在本例中,它是决策转换器。它取轨迹的分步描述作为输入,并累加嵌入层结果缓冲区中整个序列的嵌入。但与以前的工作不同的是,在反向传播验期间,我们必须将误差梯度传播到未来的编码器模型之中。为此,我们将用到一个小技巧。我们将整个源数据数组切分为 2 个流。主要数据量将如往常一样通过源数据层的缓冲区传递到模型。未来的嵌入将作为第二个流传递,并在串联层中组合。对于我们输入到源数据层缓冲区的未处理源数据,我们将使用批量常规化层对其进行常规化。未来的嵌入是模型操作的结果,无需常规化即可使用。

//---
   if(!agent)
     {
      agent = new CArrayObj();
      if(!agent)
         return false;
     }
//--- Agent
   agent.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = prev_count + EmbeddingSize;
   descr.step = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }

合并为单一流的数据被馈送到所表述信息嵌入的神经层。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronEmbeddingOCL;
   prev_count = descr.count = HistoryBars;
     {
      int temp[] = {BarDescr * NBarInPattern, AccountDescr, TimeDescription, NActions, EmbeddingSize};
      ArrayCopy(descr.windows, temp);
     }
   int prev_wout = descr.window_out = EmbeddingSize;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }

然后数据被传递到转换器模块。我用了一个 4 层的“蛋糕”,上面有 16 个自我关注者。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHSparseAttentionOCL;
   prev_count = descr.count = prev_count * 5;
   descr.window = EmbeddingSize;
   descr.step = 16;
   descr.window_out = 64;
   descr.layers = 4;
   descr.probability = Sparse;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }

在转换器模块之后,我用了 2 个卷积层来识别稳定的形态。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = EmbeddingSize;
   descr.step = EmbeddingSize;
   descr.window_out = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = EmbeddingSize;
   descr.step = EmbeddingSize;
   descr.window_out = 16;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }

它们后面是决策模块的 3 个完全连接层。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = NActions;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!agent.Add(descr))
     {
      delete descr;
      return false;
     }

在模型的输出端,我们有一个完全连接层,其元素数量等于智能体的动作空间。

接下来,我们将创建目标预测模型 P(Z|St)。使用层次化模型的术语,我们可以将其称为计划者(Planner)。它们的功能在结构上非常相似,尽管训练模型的方式完全不同。

作为模型源数据层的输入,我们仅提供一种形态的历史数据和指标值的描述。在我们的例子中,这数据仅来自 1 根柱线。

我同意,对于分析市场局势、和预测未来状态和动作来说,信息太少了。特别是,如果我们谈论的是朝前几步预测。但是,我们以不同的方式看待这种局势。在操作期间,我们以嵌入的形式将生成的未来预测作为输入提供给我们的扮演者。其内层将存储给定历史深度的数据。在这种境况下,更重要的是我们应关注已发生的变化,并调整扮演的行为。在创建未来嵌入时分析更深层次的历史可能会“模糊”局部变化。不过,这是我的主观意见,并非预训练决策转换器算法的需求。

   if(!planner)
     {
      planner = new CArrayObj();
      if(!planner)
         return false;
     }
//--- Planner
   planner.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = BarDescr * NBarInPattern;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
     {
      delete descr;
      return false;
     }

生成的原始数据传递到批量常规化层,将其转换为可比较的形式。

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

接下来,我决定不令模型复杂化,并采用 3 个全连接层的决策模块。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!planner.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
     {
      delete descr;
      return false;
     }

在模型的输出端,我们将向量大小缩减到嵌入大小,并调用 SoftMax 函数对结果进行常规化。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
     {
      delete descr;
      return false;
     }

我们已经定义了 2 个模型的架构方案。我们所要做的就是添加对未来编码器的描述。尽管模型的输出基本上旨在匹配以前模型的输出,但这仅在模型的最后几层中能注意到。编码器架构稍微复杂一些。首先,在未来嵌入中,我们可以在一定深度上进行规划。这意味着我们将有关后续几根蜡烛的信息输入到源数据层之中。

注意,在有关未来的数据中,我只包括有关品种价格走势和指标读数的数据。我并未包括有关帐户状态和智能体后续动作的信息。智能体的动作由其政策判定。我想专注于理解环境中的过程。虽然账户状态在某种程度上已经包含了从环境中收到的奖励信息,但在某种程度上其与未标记数据的原则相矛盾。

//--- Future Embedding
   if(!future_embedding)
     {
      future_embedding = new CArrayObj();
      if(!future_embedding)
         return false;
     }
//---
   future_embedding.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = BarDescr * NBarInPattern * ValueBars;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }

如前,我们将未处理的原始数据传递到批量常规化层,以便将其转换为可比较的形式。

接下来,我用一个 4 层转换器模块,和 16 个自关注者。在该情况下,关注度模块分析各根柱线之间的依赖关系,尝试识别规划界限内的主要趋势,并过滤掉噪声分量。

根据 PDT 方法逻辑,未来状态的嵌入应该向扮演者指示正在使用的技能,和进一步动作的方向。因此,编码器操作的结果应尽可能富含信息和准确。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMLMHSparseAttentionOCL;
   prev_count = descr.count = ValueBars;
   descr.window = BarDescr * NBarInPattern;
   descr.step = 16;
   descr.window_out = 64;
   descr.layers = 4;
   descr.probability = Sparse;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }

关注度层之后是决策模块的全连接层。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }

在模型输出中,我们调用含有 SoftMax 常规化的全连接层,就像上面在未来预测模型中所做的那样。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

针对预训练智能交易系统的模型架构讲述到此结束。但在转到智能交易系统的工作之前,我想完成模型架构描述的工作。我想提醒您,在使用 PDT 方法进行优调的阶段,可以添加另一个模型,即奖励预测。由于它是在训练的后续阶段加入的,因此我决定在单独的 CreateValueDescriptions 方法中包含其架构的描述。

根据 PDT 方法论,该模型必须估测潜在伏态以及当前状态中蕴含的未来趋势。基于分析结果,有必要预测来自环境的奖励。

下游训练过程的目的,是在将来的嵌入中包含有关可能的奖励信息。因此,与预训练阶段一样,我们需要将奖励预测误差梯度传递到未来编码器模型。此处,我们将使用上面测试的方式来分离信息流。初始数据流之一将是当前状态。第二个将是未来嵌入。

我们现在必须解决的第二个问题是,现阶段理解当前状态应包括什么。当然,在优调阶段,我们使用来自训练数据集的标记数据,并且可以包括全部可用数据量。但是,大体量的输入数据令模型变得极其复杂,并增加了模型处理的成本。现阶段使用这般数据量的有效性如何?

为了预测后续状态,我们需要分析环境的先前状态。但是我们已经以嵌入的形式获得了有关未来状态的信息。

对智能体先前动作的分析可以指明正在使用的政策。但是我们需要向智能体提供信息,以便决定是否需要更改所用的技能和行为政策。

有关当前帐户状态的信息可能很有用。可用保证金的存在将表明,如果趋势有利,可以加仓。或者,如果预计趋势会发生变化,我们必须把先前持仓了结,并锁定浮动盈亏。此外,我们应该记住对于缺乏持仓的处罚,这也会影响奖励。

因此,我们将账户状态和持仓的当前描述输入到源数据层之中。

bool CreateValueDescriptions(CArrayObj *value)
  {
//---
   CLayerDescription *descr;
//---
   if(!value)
     {
      value = new CArrayObj();
      if(!value)
         return false;
     }
//--- Value
   value.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!value.Add(descr))
     {
      delete descr;
      return false;
     }

输出传递到批量常规化层。

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

 接下来,我们将 2 个数据流在串联层中合并。

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

然后,数据从全连接层传递到决策模块。

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

在模型输出中,我们得到一个预期奖励的向量。


2.2. 预训练智能交易系统

在创建所用模型的架构后,我们继续实现 PDT 方法的算法。我们从预训练智能交易系统 “...\PDT\Pretrain.mq5” 开始。如上所述,该 EA 执行 3 个模型的初步训练:扮演者、计划者、和未来编码器。

CNet                 Agent;
CNet                 Planner;
CNet                 FutureEmbedding;

在 EA 初始化方法中,我们首先加载训练数据集。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

尝试加载预训练模型,并在必要时根据上述架构初始化新模型。

//--- load models
   float temp;
   if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Planner.Load(FileName + "Pln.nnw", temp, temp, temp, dtStudied, true) ||
      !FutureEmbedding.Load(FileName + "FEm.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *agent = new CArrayObj();
      CArrayObj *planner = new CArrayObj();
      CArrayObj *future_embedding = new CArrayObj();
      if(!CreateDescriptions(agent, planner, future_embedding))
        {
         delete agent;
         delete planner;
         delete future_embedding;
         return INIT_FAILED;
        }
      if(!Agent.Create(agent) || !Planner.Create(planner) ||
         !FutureEmbedding.Create(future_embedding))
        {
         delete agent;
         delete planner;
         delete future_embedding;
         return INIT_FAILED;
        }
      delete agent;
      delete planner;
      delete future_embedding;
      //---
     }

然后,我们将所有模型转移到一个 OpenCL 关联环境当中。

//---
   COpenCL *opcl = Agent.GetOpenCL();
   Planner.SetOpenCL(opcl);
   FutureEmbedding.SetOpenCL(opcl);

在此,我们执行覆盖模型架构的最小所需控制。

   Agent.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the worker does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }

初始化初步训练过程的启动,其在 Train 方法中实现。

//---
   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

在 EA 的逆初始化方法中,我们必须保存已训练的模型。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   Agent.Save(FileName + "Act.nnw", 0, 0, 0, TimeCurrent(), true);
   Planner.Save(FileName + "Pln.nnw", 0, 0, 0, TimeCurrent(), true);
   FutureEmbedding.Save(FileName + "FEm.nnw", 0, 0, 0, TimeCurrent(), true);
   delete Result;
  }

模型训练过程在 Train 方法中实现。在该方法主体中,我们首先判定经验回放缓冲区的大小。

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();

接下来,我们为模型训练过程创建一个嵌套循环系统。外部循环仅限于 EA 外部参数中指定的训练迭代次数。在这个循环的主体中,我们首先从经验回放缓冲区中采样一个轨迹,并沿着所选轨迹采样一个单独的环境状态,以便开始学习过程。

   bool StopFlag = false;
   for(int iter = 0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++)
     {
      int tr = (int)((MathRand() / 32767.0) * (total_tr - 1));
      int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * MathMax(Buffer[tr].Total - 2 * HistoryBars - ValueBars,
                                                                            MathMin(Buffer[tr].Total, 20 + ValueBars)));
      if(i < 0)
        {
         iter--;
         continue;
        }

之后,我们运行一个嵌套循环来按顺序训练 DT 模型。

      Actions = vector<float>::Zeros(NActions);
      for(int state = i; state < MathMin(Buffer[tr].Total - ValueBars, i + HistoryBars * 3); state++)
        {
         //--- History data
         State.AssignArray(Buffer[tr].States[state].state);
         if(!Planner.feedForward(GetPointer(State), 1, false))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }

在循环主体中,我们将历史价格走势数据,和指标读数写入源数据缓冲区。注意,该数据实际上足以运行计划者模型的前馈验算。该操作将首先运行。之后,我们继续填充扮演者的源数据缓冲区。添加帐户状态。

         //--- Account description
         float PrevBalance = (state == 0 ? Buffer[tr].States[state].account[0] : Buffer[tr].States[state - 1].account[0]);
         float PrevEquity = (state == 0 ? Buffer[tr].States[state].account[1] : Buffer[tr].States[state - 1].account[1]);
         State.Add((Buffer[tr].States[state].account[0] - PrevBalance) / PrevBalance);
         State.Add(Buffer[tr].States[state].account[1] / PrevBalance);
         State.Add((Buffer[tr].States[state].account[1] - PrevEquity) / PrevEquity);
         State.Add(Buffer[tr].States[state].account[2]);
         State.Add(Buffer[tr].States[state].account[3]);
         State.Add(Buffer[tr].States[state].account[4] / PrevBalance);
         State.Add(Buffer[tr].States[state].account[5] / PrevBalance);
         State.Add(Buffer[tr].States[state].account[6] / PrevBalance);

添加智能体的时间戳和上次动作。

         //--- Time label
         double x = (double)Buffer[tr].States[state].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
         State.Add((float)MathSin(2.0 * M_PI * x));
         x = (double)Buffer[tr].States[state].account[7] / (double)PeriodSeconds(PERIOD_MN1);
         State.Add((float)MathCos(2.0 * M_PI * x));
         x = (double)Buffer[tr].States[state].account[7] / (double)PeriodSeconds(PERIOD_W1);
         State.Add((float)MathSin(2.0 * M_PI * x));
         x = (double)Buffer[tr].States[state].account[7] / (double)PeriodSeconds(PERIOD_D1);
         State.Add((float)MathSin(2.0 * M_PI * x));
         //--- Prev action
         State.AddArray(Actions);

现在,为了执行扮演者的前馈验算,我们只需要未来嵌入。好吧,我们有一个计划者结果的缓冲区,但在这个阶段,未经训练的模型结果没有任何条件限制。根据 PDT 算法,我们需要加载有关后续状态的信息,并为接收到的数据生成一个嵌入。

         //--- Target
         Result.AssignArray(Buffer[tr].States[state + 1].state);
         for(int s = 1; s < ValueBars; s++)
            Result.AddArray(Buffer[tr].States[state + 1].state);
         if(!FutureEmbedding.feedForward(Result, 1, false))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }

编码器操作结果将输入到第二个扮演者模型之中。接下来,我们调用其直接验算。

         FutureEmbedding.getResults(Result);
         //--- Policy Feed Forward
         if(!Agent.feedForward(GetPointer(State), 1, false, Result))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }

针对所有用到的模型执行前向验算之后,我们继续训练它们。首先,我们调用计划者模型的反向传播方法(预测未来)。这个顺序与我们刚刚输入到扮演者中的目标结果向量的准备情况有关。

         //--- Planner Study
         if(!Planner.backProp(Result, NULL, NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }

接下来,转到扮演者目标回报的筹备。为此,我们使用经验回放缓冲区中的动作,这样会导致已知后果。

         //--- Policy study
         Actions.Assign(Buffer[tr].States[state].action);
         vector<float> result;
         Agent.getResults(result);
         Result.AssignArray(CAGrad(Actions - result) + result);

目标值准备就绪之后,我们执行扮演者的反向传播,并立即将误差梯度传递到未来编码器模型当中。

         if(!Agent.backProp(Result, GetPointer(FutureEmbedding)) ||
            !FutureEmbedding.backPropGradient((CBufferFloat *)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }

之后,我们只需要通知用户操作的进度,然后转到新的迭代。

         //---
         if(GetTickCount() - ticks > 500)
           {
            string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Agent", iter * 100.0 / 
                                                        (double)(Iterations), Agent.getRecentAverageError());
            str += StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Planner", iter * 100.0 / 
                                                        (double)(Iterations), Planner.getRecentAverageError());
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

当循环系统的所有迭代完成后,我们清除图表注释字段。在流水账日志中显示训练结果,并启动 EA 终止。

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Agent", Agent.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Planner", Planner.getRecentAverageError());
   ExpertRemove();
//---
  }

所有其它 EA 方法都已转移,并未修改来自早前文章中讨论过的训练 EA “...\Study.mq5”。故此,我们现在不再赘述。您可以在附件中找到该程序的完整代码。我们正在转入下一阶段的工作。


2.3. 优调 EA

在完成预训练算法的实现后,我们继续创建优调 EA “...\PDT\FineTune.mq5” 的工作,在其中我们将构建一个模型下游训练的算法。

EA 与之前的那个有大约 90% 相近。因此,我们不会详细研究它的所有方法。我们只会看看所做的更改。

如本文的理论部分所述,现阶段的 PDT 方法允许模型优化,从而解决问题。这意味着我们将使用标记数据,和环境奖励来优化智能体的政策。因此,我们在学习过程中添加了另一个外部奖励预测模型。

CNet                 RTG;

请注意,我们只添加了一个模型,而来自以前 EA 中的模型保持不变。

在优调 EA 中,我保留了一个机制,即在无法加载预训练模型的情况下创建智能体、计划者、和未来编码器的新模型。因此,用户可以从头开始训练模型。同时,在 EA 初始化方法中,加载和初始化新的外部奖励预测模型(如有必要)被安排到一个单独的模块之中。从预训练转向优调时,我们将会有之前 EA 中训练的模型。至于奖励预测模型,我们将依据随机参数对其进行初始化。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
........
........
//---
   if(!RTG.Load(FileName + "RTG.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *rtg = new CArrayObj();
      if(!CreateValueDescriptions(rtg))
        {
         delete rtg;
         return INIT_FAILED;
        }
      if(!RTG.Create(rtg))
        {
         delete rtg;
         return INIT_FAILED;
        }
      delete rtg;
      //---
     }
//---
   COpenCL *opcl = Agent.GetOpenCL();
   Planner.SetOpenCL(opcl);
   FutureEmbedding.SetOpenCL(opcl);
   RTG.SetOpenCL(opcl);
//---
   RTG.getResults(Result);
   if(Result.Total() != NRewards)
     {
      PrintFormat("The scope of the RTG does not match the rewards count (%d <> %d)", NRewards, Result.Total());
      return INIT_FAILED;
     }
//---
........
........
//---
   return(INIT_SUCCEEDED);
  }

 接下来,我们将所有模型转移到单个 OpenCL 关联环境中,并检查所添加模型的结果层是否与分解的奖励向量的维度匹配。

还对训练模型 Train 方法进行了一些补充。在智能体的前馈验算之后,我们添加了调用奖励预测模型的前馈验算。如上讨论,对于模型的输入,我们提供一个描述帐户状态和未来嵌入的向量。

........
........
         //--- Policy Feed Forward
         if(!Agent.feedForward(GetPointer(State), 1, false, Result))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }
         //--- Return-To-Go
         Account.AssignArray(Buffer[tr].States[state + 1].account);
         if(!RTG.feedForward(GetPointer(Account), 1, false, Result))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }
........
........

在更新智能体参数后,模型参数也会更新。模型优化算法几乎完全雷同。通过调用模型的反向传播方法,我们将误差梯度传播到未来编码模型,然后更新其参数。唯一的区别在于目标值。这种方式允许我们训练智能体动作的依赖性,以及收到的外部奖励对于未来嵌入的依赖性。

........
........
         //--- Policy study
         Actions.Assign(Buffer[tr].States[state].action);
         vector<float> result;
         Agent.getResults(result);
         Result.AssignArray(CAGrad(Actions - result) + result);
         if(!Agent.backProp(Result, GetPointer(FutureEmbedding)) ||
            !FutureEmbedding.backPropGradient((CBufferFloat *)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }
         //--- Return To Go study
         vector<float> target;
         target.Assign(Buffer[tr].States[state + 1].rewards);
         result.Assign(Buffer[tr].States[state + ValueBars].rewards);
         target = target - result * MathPow(DiscFactor, ValueBars);
         Result.AssignArray(target);
         if(!RTG.backProp(Result, GetPointer(FutureEmbedding)) ||
            !FutureEmbedding.backPropGradient((CBufferFloat *)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            StopFlag = true;
            break;
           }
........
........

我们的局部更改就此完成。附件中提供了 EA 和本文中用到的所有程序的完整代码。

2.4. 测试训练模型的 EA

在上面讨论的 EA 中训练模型后,我们需要取训练数据集中未包含的历史数据来测试所生成模型的性能。为了实现这个功能,我们创建一个新的 EA “...\PDT\Test.mq5”。与上面讨论的 EA 不同,在该 EA 中,模型是离线训练的,测试 EA 与环境在线交互。这反映在其算法的构造中。

在 EA 的 OnInit 初始化方法中,我们首先初始化所分析指标的对象。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();
//---
   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      return INIT_FAILED;
//---
   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      return INIT_FAILED;
//---
   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      return INIT_FAILED;
//---
   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      return INIT_FAILED;
   if(!RSI.BufferResize(NBarInPattern) || !CCI.BufferResize(NBarInPattern) ||
      !ATR.BufferResize(NBarInPattern) || !MACD.BufferResize(NBarInPattern))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return INIT_FAILED;
     }

创建交易操作对象。

//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      return INIT_FAILED;

加载已训练模型。在此,我们只用到 2 个模型:扮演者和计划者。与以前的 EA 不同,加载模型时的错误会导致 EA 中断。因为我们没有在其中实现在线模型训练。

//--- load models
   float temp;
   if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Planner.Load(FileName + "Pln.nnw", temp, temp, temp, dtStudied, true))
     {
      PrintFormat("Can't load pretrained model");
      return INIT_FAILED;
     }

模型成功加载后,我们将它们转移到单个 OpenCL 关联环境中,并执行必要的最低限度架构控制。

   Planner.SetOpenCL(Agent.GetOpenCL());
   Agent.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the Actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
//---
   Agent.GetLayerOutput(0, Result);
   if(Result.Total() != (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions))
     {
      PrintFormat("Input size of Actor doesn't match state description (%d <> %d)", Result.Total(), 
                              (NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions));
      return INIT_FAILED;
     }
   Agent.Clear();

在方法的最后,我们用初始值初始化变量。

   AgentResult = vector<float>::Zeros(NActions);
   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
//---
   return(INIT_SUCCEEDED);
  }

与环境交互的过程是在 OnTick 跳价处理方法中实现的。在方法主体中,我们首先检查新柱线开盘事件的发生。这是因为我们所有的模型都分析收盘烛条,并在新柱线开盘时执行交易操作。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(!IsNewBar())
      return;

接下来,我们向终端请求所分析历史深度的必要数据。在本例中,我所说的历史深度是指一个形态的大小,在我们的例子中是一根柱线。由智能体分析的历史深度以嵌入的形式包含在其潜在状态当中,并且不会在每根柱线上重复处理。

   int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), NBarInPattern, Rates);
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();

接下来,我们需要将接收到的数据传输到缓冲区,以便传递给模型。

//--- History data
   float atr = 0;
   for(int b = 0; b < NBarInPattern; b++)
     {
      float open = (float)Rates[b].open;
      float rsi = (float)RSI.Main(b);
      float cci = (float)CCI.Main(b);
      atr = (float)ATR.Main(b);
      float macd = (float)MACD.Main(b);
      float sign = (float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         continue;
      //---
      int shift = b * BarDescr;
      sState.state[shift] = (float)(Rates[b].close - open);
      sState.state[shift + 1] = (float)(Rates[b].high - open);
      sState.state[shift + 2] = (float)(Rates[b].low - open);
      sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f);
      sState.state[shift + 4] = rsi;
      sState.state[shift + 5] = cci;
      sState.state[shift + 6] = atr;
      sState.state[shift + 7] = macd;
      sState.state[shift + 8] = sign;
     }
   bState.AssignArray(sState.state);

收到的历史数据足以运行一次计划者的前馈验算。

   if(!Planner.feedForward(GetPointer(bState), 1, false))
      return;

不过,若要令智能体完全正常运行,我们需要额外的数据。首先,我们将有关帐户状态的信息添加到缓冲区当中,我们首先从终端请求该信息。

//--- Account description
   sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY);
//---
   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   double position_discount = 0;
   double multiplyer = 1.0 / (60.0 * 60.0 * 10.0);
   int total = PositionsTotal();
   datetime current = TimeCurrent();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit);
     }
   sState.account[2] = (float)buy_value;
   sState.account[3] = (float)sell_value;
   sState.account[4] = (float)buy_profit;
   sState.account[5] = (float)sell_profit;
   sState.account[6] = (float)position_discount;
   sState.account[7] = (float)Rates[0].time;
//---
   bState.Add((float)((sState.account[0] - PrevBalance) / PrevBalance));
   bState.Add((float)(sState.account[1] / PrevBalance));
   bState.Add((float)((sState.account[1] - PrevEquity) / PrevEquity));
   bState.Add(sState.account[2]);
   bState.Add(sState.account[3]);
   bState.Add((float)(sState.account[4] / PrevBalance));
   bState.Add((float)(sState.account[5] / PrevBalance));
   bState.Add((float)(sState.account[6] / PrevBalance));

接下来,我们添加一个时间戳和智能体的最后动作。

//--- Time label
   double x = (double)Rates[0].time / (double)(D'2024.01.01' - D'2023.01.01');
   bState.Add((float)MathSin(2.0 * M_PI * x));
   x = (double)Rates[0].time / (double)PeriodSeconds(PERIOD_MN1);
   bState.Add((float)MathCos(2.0 * M_PI * x));
   x = (double)Rates[0].time / (double)PeriodSeconds(PERIOD_W1);
   bState.Add((float)MathSin(2.0 * M_PI * x));
   x = (double)Rates[0].time / (double)PeriodSeconds(PERIOD_D1);
   bState.Add((float)MathSin(2.0 * M_PI * x));
//--- Prev action
   bState.AddArray(AgentResult);

在单独的缓冲区中,我们接收计划者先前执行的前馈验算的结果,并调用智能体的前馈方法。

//--- Return to go
   Planner.getResults(Result);
//---
   if(!Agent.feedForward(GetPointer(bState), 1, false, Result))
      return;

然后,我们更新下一根柱线操作所需的变量。

//---
   PrevBalance = sState.account[0];
   PrevEquity = sState.account[1];

初始数据分析的第一阶段已经完成。我们转到与环境互动的阶段。在此,我们接收智能体的前馈验算结果,并将它们解码到将至操作的向量。如常,我们排除重叠的成交量,并将差值留在更可能的移动方向上。

   vector<float> temp;
   Agent.getResults(temp);
//---
   double min_lot = Symb.LotsMin();
   double step_lot = Symb.LotsStep();
   double stops = MathMax(Symb.StopsLevel(), 1) * Symb.Point();
   if(temp[0] >= temp[3])
     {
      temp[0] -= temp[3];
      temp[3] = 0;
     }
   else
     {
      temp[3] -= temp[0];
      temp[0] = 0;
     }
   float delta = MathAbs(AgentResult - temp).Sum();
   AgentResult = temp;

然后,我们根据预测值调整我们在市场中的仓位。首先,我们调整多头持仓。

//--- buy control
   if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= stops || (temp[2] * MaxSL * Symb.Point()) <= stops)
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }
   else
     {
      double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot;
      double buy_tp = Symb.NormalizePrice(Symb.Ask() + temp[1] * MaxTP * Symb.Point());
      double buy_sl = Symb.NormalizePrice(Symb.Ask() - temp[2] * MaxSL * Symb.Point());
      if(buy_value > 0)
         TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);
      if(buy_value != buy_lot)
        {
         if(buy_value > buy_lot)
            ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
         else
            Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp);
        }
     }

对于相反方向重复这些操作。

//--- sell control
   if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= stops || (temp[5] * MaxSL * Symb.Point()) <= stops)
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
   else
     {
      double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;;
      double sell_tp = Symb.NormalizePrice(Symb.Bid() - temp[4] * MaxTP * Symb.Point());
      double sell_sl = Symb.NormalizePrice(Symb.Bid() + temp[5] * MaxSL * Symb.Point());
      if(sell_value > 0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
        {
         if(sell_value > sell_lot)
            ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
         else
            Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);
        }
     }

我们基于与环境交互的结果组成一个结构,并将其保存到一条轨迹之中,稍后将其添加到经验回放缓冲区之中,以便后续模型政策优化。

//---
   int shift = BarDescr * (NBarInPattern - 1);
   sState.rewards[0] = bState[shift];
   sState.rewards[1] = bState[shift + 1] - 1.0f;
   if((buy_value + sell_value) == 0)
      sState.rewards[2] -= (float)(atr / PrevBalance);
   else
      sState.rewards[2] = 0;
   for(ulong i = 0; i < NActions; i++)
      sState.action[i] = AgentResult[i];
   if(!Base.Add(sState))
      ExpertRemove();
  }

我们利用 MQL5 实现预训练决策转换器方法的工作到此结束。在附件中找到所有用到程序的完整代码。


3. 测试

在完成所研究方法的实现后,我们需要训练模型,并依据历史数据测试其性能。如常,我们选用更具波动性的金融产之一 EURUSD 和 H1 时间帧来训练和测试模型。这些模型是依据 2023 年前 7 个月的历史数据训练的。为了测试训练模型的性能,我采用了 2023 年 8 月的历史数据。

在开始初步模型训练之前,我们需要收集一个训练数据集。如前所述,于此我们借用来自上一篇文章中的训练数据集。您可以阅读它,以便获取更多详情。我创建了训练数据集文件的副本,并将其保存为 “PDT.bd”。

之后,我实时启动了预训练 EA。

我要提醒您,所有模型训练 EA 都于在线图表上运行。然而,整个学习过程是离线进行的,无需执行交易操作。

请注意,在该阶段您需要耐心等待。预训练过程相当耗时。我的电脑运行了一天多。

接下来,我们转入优调过程。此处,方法作者谈及有关在线学习。在策略测试器中,我交替依据测试训练间隔进行短期下游训练。但最初,我们必须使用先前收集的训练数据集来“预热”模型。


至于优调期间,我需要连续数十次下游训练和测试迭代,这也需要时间和精力。

然而,学习结果并不那么乐观。作为训练的结果,我得到了一个模型,其按最小手数交易,并取得了不同的成功率。在历史的某些部分,余额曲线展示出明显的上升趋势。在另一些部分,则明显下降。泛泛来说,依据训练数据和新集合,模型的结果都接近于 0。

积极的方面包括模型将获得的经验转移到新数据的能力,这可以通过训练集的历史数据集,和后续历史间隔上的测试结果的可比性得到证实。此外,您可以看到盈利交易的规模远大于亏损交易的规模。在两个历史数据段中,我们观察到平均获胜交易的规模超过了最大亏损。然而,所有积极方面都被盈利交易的低占比所抵消,在两个历史区间中,盈利交易的份额都恰恰低于 40%。 

依据新数据的测试结果 依据新数据的测试结果

依据 2023 年 8 月的历史数据(新数据)上测试模型时,该模型执行了 18 笔交易。其中只有 39% 以盈利了结。同时,最大盈利交易为 11.26,几乎是最大亏损 4.76 的 3 倍。平均盈利交易为 5.15,平均亏损交易为 3.19。测试期间的盈利系数为 1.03。

显然,为了增加盈利交易的份额,我们需要额外分析获得的结果,并优调模型。该方法展现出潜力,但需要长时间的模型训练。


结束语

在本文中,我们讲述了预训练决策转换器(PDT)方法,该方法为决策转换器强化学习提供了一种无监督的预训练算法。基于模型训练过程中对未来状态的知识,PDT 能够从训练数据中提取丰富的先验知识。这些知识可以在模型下游训练期间进一步优调和调整,因为 PDT 将每个未来机会与相应的回报相关联,并选择一个具有最大预测奖励的机会。这有助于做出最优决策。

然而,与前面讨论的 DT 和 ODT 相比,PDT 需要更多的训练时间和计算资源,由于可用资源有限,这可能会导致实际困难。此外,训练模型的目标在学习的各种行为,及其一致性之间进行了权衡。方法作者的实际实验表明,最优值取决于具体数据集。此外,还可以应用其它技术来改进未来状态的编码。

我不能否认方法作者的结论。我们的实践经验充分证实了这一点。训练模型是一个相当耗费精力、且劳动密集型的过程。为了最大限度地开发各种智能体技能,我们需要一个相当大规模的训练数据集。当然,我们使用未标记数据进行预训练,这令数据收集过程更容易。但是问题在于,收集和处理数据、以及训练模型的资源可用性。


参考

  • 决策转换器的未来条件无监督预训练
  • 神经网络变得简单(第 58 部分):决策转换器(DT)
  • 神经网络变得简单(第 62 部分):在层次化模型中运用决策转换器


  • 文中所用程序

    # 名称 类型 说明
    1 Faza1.mq5 EA 样本收集 EA
    2 Pretrain.mq5 EA 预训练智能交易系统
    3 FineTune.mq5 EA 优调 EA
    4 Test.mq5 EA 模型测试 EA
    5 Trajectory.mqh 类库 系统状态定义结构
    6 NeuroNet.mqh 类库 创建神经网络的类库
    7 NeuroNet.cl 代码库 OpenCL 程序代码库



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

    附加的文件 |
    MQL5.zip (476 KB)
    开发具有 RestAPI 集成的 MQL5 强化学习代理(第 1 部分):如何在 MQL5 中使用 RestAPI 开发具有 RestAPI 集成的 MQL5 强化学习代理(第 1 部分):如何在 MQL5 中使用 RestAPI
    在本文中,我们将讨论 API(Application Programming Interface,应用程序编程接口)对于不同应用程序和软件系统之间交互的重要性。我们将看到 API 在简化应用程序间交互方面的作用,使它们能够有效地共享数据和功能。
    群体优化算法:带电系统搜索(CSS)算法 群体优化算法:带电系统搜索(CSS)算法
    在本文中,我们将探讨另一种受无生命自然启发的优化算法--带电系统搜索(Charged System Search,CSS)算法。本文旨在介绍一种基于物理和力学原理的新的优化算法。
    神经网络变得简单(第 64 部分):保守加权行为克隆(CWBC)方法 神经网络变得简单(第 64 部分):保守加权行为克隆(CWBC)方法
    据前几篇文章中所执行测试的结果,我们得出的结论是,训练策略的最优性很大程度上取决于所采用的训练集。在本文中,我们将熟悉一种相当简单,但有效的方法来选择轨迹,并据其训练模型。
    如何利用 MQL5 创建简单的多币种智能交易系统(第 3 部分):添加交易品种、前缀和/或后缀、以及交易时段 如何利用 MQL5 创建简单的多币种智能交易系统(第 3 部分):添加交易品种、前缀和/或后缀、以及交易时段
    若干交易员同事发送电子邮件或评论了如何基于经纪商提供的名称里带有前缀和/或后缀的品种使用此多币种 EA,以及如何在该多币种 EA 上实现交易时区或交易时段。