English Русский Español Deutsch 日本語 Português
preview
神经网络变得轻松(第二十九部分):优势扮演者-评价者算法

神经网络变得轻松(第二十九部分):优势扮演者-评价者算法

MetaTrader 5交易系统 | 16 一月 2023, 11:23
908 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容

概述

我们将继续探索强化学习方法。 在之前的文章中,我们讨论了若干种方法,诸如近似 Q-学习奖励函数,和政策梯度函数学习。 每种方法都有自己的优点和缺点。 在构建和训练模型时,最大限度地利用它们的优势肯定会很棒。 当试图查找所用算法缺点最小化的方法时,我们经常尝试从各种已知的算法和方法中构建某些聚合体。 在本文中,我们将讨论一种将上述两种算法合并成单一模型训练方法的方式,称为优势扮演者-评价者算法


1. 以前讨论过的强化学习方法的优点

但在我们继续合并算法之前,我们先回到它们的独特特征、优势和劣势。

环境交互时,代理者会执行一些影响环境的动作。 外部因素和代理者动作的影响结果,环境状态会发生变化。 每次状态变化后,环境都会通过提供一些奖励来通知代理者。 这种奖励既可以是积极的,也可以是消极的。 奖励的规模和符号表示新状态对代理者的实用性。 代理者不知道奖励是如何形成的。 代理者的目的是学习执行此类动作,以便在与环境交互的过程中能够获得尽可能高额的奖励。

我们首先研究了近似奖励函数 Q-学习的算法。 我们训练模型来学习:如何预测环境处于特定状态下,并执行特定动作后,即将到来的奖励。 此后,根据预测的奖励和代理者的行为政策,代理者执行相应动作。 根据规则,此处会用到“贪婪”或“ɛ-贪婪”策略。 根据贪婪策略,我们选择预测奖励最高的动作。 它最常用于代理者的实际操作之中。

第二种 ɛ-贪婪策略不仅在名称上与贪婪策略相似。 它还涉及选择具有最高预测奖励的动作。 但它允许概率为 ɛ 的随机动作。 它用于模型训练阶段,可更好地研究环境。 否则,一旦获得正奖励,模型将持续重复相同的动作,而不再探索其它选项。 在这种情况下,它永远不会明白主动去优化动作,或者是否有其它任何动作可带来更丰厚的奖励。 但我们希望代理者尽可能多地探索环境,并充分利用它。

Q-学习方法的优势是显而易见的。 训练模型以便预测奖励。 这是我们从环境中得到的奖励。 好了,我们已有了模型操作结果值与从环境中接收的参考值之间存在直接关系。 以这种解释,学习类似于监督学习方法。 在训练模型时,我们采用了标准误差作为损失函数。

在此,您应该注意以下几点。 已训练模型将返回场次结束之前的平均累积奖励,同时考虑到代理者在训练模型时,在类似环境状态下执行特定动作后从环境获得的折扣因子。 但环境会针对每次转移到新状态时特意返回奖励。 此处,我们看到了间隙,应在代理者完成剧集后被修补。

该算法的缺点包括模型训练的复杂性。 以前我们必须运用一些技巧来训练模型。 首先,环境的连续状态彼此密切相关。 大多数情况下,它们仅在次要细节上有所不同。 使用此类数据进行直接训练将导致不断重新训练模型,以便适应当前状态。 故此,该模型失去了泛化数据的能力。 由此,我们必须创建一个历史数据的缓冲区,保存模型的训练数据。 在训练模型时,我们从历史数据缓冲区中随机选择状态,这样可以减少连续使用训练数据导致的两个状态之间的相关性。

依据从环境获得的实际奖励数据训练模型,我们得到了一个低数据方差的模型。 该模型针对每个状态返回的数值差值处于可接受的较小程度。 这是一个积极因素。 但环境会针对每个特定的转移返回奖励。 然而,我们希望在整个期间实现盈利最大化。 为了计算它,代理者必须完成整个场次。 若是利用历史数据缓冲区,并添加预测未来奖励的模型,我们就能设法构建一种算法,可在实际使用过程中额外训练模型。 如此,学习过程以“在线”模式运行。 但我们不得不为预测奖励时的误差付出代价。

在使用第二个模型预测未来奖励时,我们充分意识到此类预测中的误差,并接受风险。 但在训练模型时会考虑到每个误差,并影响所有后续预测。 因此,我们获得了一个方差很小却能够预测结果的模型,但乖离率较大。 当使用贪婪策略时,这可以忽略不计。 在此期间,为了选择最大化奖励,我们仅拿它与其它奖励比较。 在这种情况下,值的乖离率或缩放不会影响动作选择的最终结果。

较小方差 - 大乖离率

当使用 Q-学习时,训练模型仅为了预测奖励。 为了选择动作,我们需要在模型创建阶段指定代理者的动作政策(策略)。 但采用贪婪策略可以令您在检测确定性环境中成功工作。 这在构建随机策略时完全不适用。

与之对比,使用策略梯度不需要在模型创建阶段确定代理者的动作政策(策略)。 此方法令代理者能够构建其行为政策。 它既可以是贪婪的,也可以是随机的。

策略梯度方法训练过的模型,返回环境在每个特定状态下,选择一个或多个动作时,实现所需结果的概率分布。

在训练模型时,我们还要用到从环境中获得的奖励。 为了在环境的每种状态下选择最佳策略,要用到一直累积到场次结束的奖励。 显然,为了更新模型的权重,代理者需要经历整个场次。 也许这就是该方法的主要缺点。 我们无法建立在线训练模型,因为我们不知道未来的奖励。

与此同时,使用实际累积奖励可以最小化预测数据与来自真实数值间的乖离率持续误差。 这是 Q-学习的问题,因为用到了未来奖励的预测值。

然而,在政策梯度中,我们训练模型来预测并非针对预期奖励,而是当代理者在环境的某种状态下执行某种动作时,达成预期结果的概率分布。 作为损失函数,我们采用了对数函数。

对数损失函数

为了分析判断误差最小化方向,我们采用损失函数的导数。 在这种情况下,对数导数的性质显示出极大便利性。

对数导数

将损失函数的导数乘以积极奖励,我们提升了选择该动作的概率。 且当将损失函数的导数乘以消极奖励时,我们在相反的方向上调整权重。 这会降低选择该动作的概率。 奖励值模数将决定调整权重的步长。

如您所见,在更新模型的权重矩阵时,奖励是间接使用的。 因此,我们得到一个模型,其结果相对于真实数据拥有较小的乖离率,但数值的方差相当大。

大方差 - 小乖离率

该方法的积极层面包括其探索环境的能力。 当使用 Q-学习和 ɛ-贪婪策略时,我们要判断利用 ɛ 剥削的研究平衡,而政策梯度使用来自给定分布的动作采样。

在训练开始时,执行所有动作的概率大体相等。 并且该模型研究环境的最大化,按照相等的概率选择代理者其一或另一个动作。 在模型训练过程中,动作的概率分布发生变化。 选择可盈利动作的概率增加,对于无利可图的动作,选择概率降低。 这降低了模型的探索倾向。 天平朝剥削平移。

注意还有一点。 使用累积奖励,我们专注于在场次结束时取得的成果。 且我们不会评估每个特定步骤的影响。 诸如此种方式,例如,可以训练代理者在无盈利时捂仓,等待趋势反转。 或者代理者可以学习执行大量亏损交易,而这些亏损将由其中罕有的产生高盈利的盈利交易所弥补。 因为模型将在场次结束时获得最终盈利,并会认定这是一次正面结果,如此该操作的概率就会增加。 当然,大量的迭代应能把这个因素最小化,因为该方法探索环境的能力应该有助于模型找到最优策略。 但这会导致一个漫长的模型训练过程。

我们来总结一下。 Q-学习模型具有低方差但高乖离率。 与之对比,政策梯度则以较小乖离率和较大方差训练模型。 而我们需要的是能够以最小的方差和乖离率训练模型。

政策梯度构建了一个整体策略,而不考虑每个步骤的影响。 我们需要在每一步都获得最大的盈利,而 Q-学习有这可能。 考虑一下贝尔曼(Bellman)函数。 它假定在每个步骤中选择最优动作。

使用 Q-函数近似方法需要在模型创建阶段定义代理者的行为政策。 但我们希望模型根据与环境交互的经验自行确定策略。 且当然,我们不希望局限于确定性行为策略。 可以通过政策训练方法来实现。

显然,解决方案是将两种训练方法结合起来,从而达到最佳效果。


2. 优势扮演者-评价者算法

奖励函数近似与政策学习方法相结合的最成功的尝试是扮演者-评价者(Actor-Critic)家族的方法。 今天我们来熟悉一下被称为优势扮演者-评价者的算法。

扮演者-评价者家族方法涉及两种模型的运用。 其中一个模型,即扮演者(Actor),负责选择代理者的动作,并利用政策函数近似方法进行训练。 第二个模型,评价者(Critic),经由 Q-学习方法进行训练,并评估扮演者选择的动作。

首先要做的是减少政策模型中的数据差异。 我们再来看一下我们的政策模型的损失函数。 每次我们将所选动作的预测概率的对数乘以累积奖励的大小时,都要考虑到折扣。 预测概率的数值在范围 [0, 1] 内归一化。

为了减少方差,我们可以减少累积奖励的值。 但它不应干扰动作对总体结果的影响。 且与此同时,有必要观察不同代理者训练场次的数据可比性。 例如,我们总是可以减去一些固定常数,或整个场次的平均奖励。

接下来,我们可以训练模型来评估每次单独动作的贡献。 删除累积奖励,并仅使用当前转移奖励进行训练,这一看似简单的想法可能会产生令人不愉快的效果。 首先,在当前步骤中获得丰厚的奖励,并不能保证我们在未来获得同样的大额奖励。 我们可以因为转移到不利状态而获得丰厚的奖励。 它可以比作“捕鼠器里的奶酪”。

另一方面,奖励并不总是取决于行动。 大多数情况下,奖励更多地取决于环境状态,大于代理者评估这种状态的能力。 例如,当依据全局趋势的方向进行交易时,代理者可以期待逆势修正,并等待价格向正确的方向移动。 为此,代理者不需要仔细分析当前状态来辨别趋势。 正确判断全局趋势就足够了。 在这种情况下,入场建仓时价格大概率并非最佳。 经由详细分析,它可以等待调整,并在更好的价位入场。 但如果调整演变成趋势,亏损的风险要高得多。 这种情况下,持仓将产生巨大的亏损,因为逆转遥遥无期。

因此,将累积奖励与某个基准结果进行比较会很有用。 但是如何访问此值? 这就是我们使用评价者的地方。 我们将用它来评估扮演者的工作。

这个思路是训练评价者模型来评估环境的当前状态。 这是扮演者在场次结束前可从当前状态获得的潜在奖励。 与此同时,扮演者学习选择潜在的、可能产生高于先前训练场次平均奖励的动作。 因此,我们将上述损失函数公式中的常数替换为状态评估值。

其中 V(s) 是环境状态评估函数。

为了训练状态评估函数,我们再次采用均方根误差。

实际上,有多种方式可构建选用此算法模型。 我们选用两个独立的神经网络:一个用于扮演者,另一个用于评价者。 但通常采用的架构是一个拥有两个输出的神经网络。 这就是所谓的“双头”神经网络。 该网络中的一部分神经层是共享的 — 它们负责处理初始数据。 若干个决策层划分到多个方向。 它们当中的一些负责模型政策(扮演者)。 其它负责评估状态(评价者)。 扮演者和评价者都基于相同的环境状态工作。 因此,它们可以拥有相同的状态识别函数。

还有优势扮演者-评论者模型的在线训练的实现。 与 Q-学习类似,其中的累积奖励被最后一次转移时收到的奖励总和所取代,并在考虑折扣因子的情况下评估后续状态。 在这种情况下,损失函数如下所示:

其中 ɣ 是折扣系数。

但在线训练有其成本。 此种模型误差更大,更难训练。


3. 实现

现在我们已经研究了该方法的理论层面,我们可以继续阅读本文的实践部分,并利用 MQL5 工具构建模型训练过程。 为了实现所述算法,我们不需要对模型的架构进行任何底层更改。 我们不会建立一个双头模型。 我们将使用现有手段构建两个神经网络,扮演者和评价者。

此外,我们不会针对新模型进行完整的训练。 取而代之,我们将采用最后两篇文章中的两个模型。 来自有关 Q-学习文章中的模型将作为评价者。 而来自政策梯度文章中的模型将作为扮演者。

请注意与上述理论材料的细微偏差。 扮演者模型完全符合所研究算法的需求。 但我们所用的评价者模型与来自上述的环境状态评估函数略有不同。 环境状态的评估不依赖于代理者执行的动作。 状态的值是我们的代理者可以从该状态中获得的最大收益。 根据我们之前训练的 Q-函数,状态的成本将等于该函数结果向量的最大值。 然而,为了正确的进行模型训练,我们必须考虑代理者在分析状态下执行的动作。

现在我们来看一下方法的实现代码。 为了训练模型,我们来创建一个名为 Actor_Critic.mq5 的 EA。 该 EA 利用之前文章中的模板。 如此,我们就能用两个预先训练的模型。 这些模型是分别训练的,并保存在不同的文件之中。 因此,首先我们要定义加载模型的文件。 它们的名称反映出对早期文章的引用。

#define ACTOR           Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_REINFORCE"
#define CRITIC          Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_Q-learning"

针对这两个模型,我们需要神经网络类的两个实例。 为了保持代码清晰,我们在算法中采用的名称对应于模型角色。

CNet                Actor;
CNet                Critic;

在 EA 初始化方法中,加载模型,并立即比较初始数据层的大小和结果。 当加载模型时,我们无法评估模型的可比性,以便在可比较样本上训练它们。 但我们必须检查模型架构的可比性。 特别是,我们检查源数据层的大小。 因此,我们可以确定两个模型使用了相同的状态描述范式来评估环境状态。

然后,我们检查两个模型的结果层大小,这将令我们能够比较代理者可能动作的离散性。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
................
................
................
//---
   float temp1, temp2;
   if(!Actor.Load(ACTOR + ".nnw", dError, temp1, temp2, dtStudied, false) ||
      !Critic.Load(CRITIC + ".nnw", dError, temp1, temp2, dtStudied, false))
      return INIT_FAILED;
//---
   if(!Actor.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   Actor.getResults(TempData);
   if(TempData.Total() != Actions)
      return INIT_PARAMETERS_INCORRECT;
   if(!vActions.Resize(SessionSize) ||
      !vRewards.Resize(SessionSize) ||
      !vProbs.Resize(SessionSize))
      return INIT_FAILED;
//---
   if(!Critic.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   if(HistoryBars != TempData.Total() / 12)
      return INIT_PARAMETERS_INCORRECT;
   Critic.getResults(TempData);
   if(TempData.Total() != Actions)
      return INIT_PARAMETERS_INCORRECT;
//---
................
................
................
//---
   return(INIT_SUCCEEDED);
  }

不要忘记控制操作的执行过程,并将必要的数据保存在对应的变量之中。 特别是,保存一个所分析形态的烛条数量,它对应于加载模型的初始数据层的大小。

训练算法已在 Train 函数中实现。 在函数开始时,我们一如既往地确定训练样本的范围。

void Train(void)
  {
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);

正在加载历史数据。 确定检查操作执行结果。

   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      ExpertRemove();
      return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
      ExpertRemove();
      return;
     }
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();

评估加载的数据量,并准备局部变量。

   int total = bars - (int)(HistoryBars + SessionSize+2);
//---
   CBufferFloat* State;
   float loss = 0;
   uint count = 0;

接下来,组织一个模型训练循环系统。 外部循环计算训练世代的次数。 如此,在循环体中,我们定义场次起点。 它是从加载的历史数据中随机选择的。 当这个起点时,需要考虑 2 个因素。 首先,场次起点之前必须有足以形成一个形态的历史数据。 其次,从场次开始点到训练样本结束,应该有足够的历史数据来完成场次。

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int error_code;
      int shift = (int)(fmin(fabs(Math::MathRandomNormal(0, 1, error_code)), 1) * (total) + SessionSize);

我没有实现在线学习算法,因为我们正基于历史数据训练模型。 使用完整的场次通常会产生更好的结果。 但与此同时,市场上的操作是无止境的。 故此,我们就有了批量算法的实现。 场次规模受数据更新批量大小的限制。 该值由用户在外部参数 SessionSize 中指定。

接下来,为场次的初始迭代创建一个嵌套循环。 在此循环的主体中,首先创建一个对象来记录系统状态描述的参数。 不要忘记检查新对象的创建结果。

      States.Clear();
      for(int batch = 0; batch < SessionSize; batch++)
        {
         int i = shift - batch;
         State = new CBufferFloat();
         if(!State)
           {
            ExpertRemove();
            return;
           }

之后,保存当前环境状态的参数。 因此,我们准备了一个形态,以便算法在当前步骤即可进行分析。

         int r = i + (int)HistoryBars;
         if(r > bars)
           {
            delete State;
            continue;
           }
         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
              {
               delete State;
               continue;
              }
            //---
            if(!State.Add((float)Rates[bar_t].close - open) || !State.Add((float)Rates[bar_t].high - open) ||
               !State.Add((float)Rates[bar_t].low - open) || !State.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State.Add(sTime.hour) || !State.Add(sTime.day_of_week) || !State.Add(sTime.mon) ||
               !State.Add(rsi) || !State.Add(cci) || !State.Add(atr) || !State.Add(macd) || !State.Add(sign))
              {
               delete State;
               break;
              }
           }

检查保存数据的完整性,并调用政策模型的前馈验算方法。

         if(IsStopped())
           {
            delete State;
            ExpertRemove();
            return;
           }
         if(State.Total() < (int)HistoryBars * 12)
           {
            delete State;
            continue;
           }
         if(!Actor.feedForward(GetPointer(State), 12, true))
           {
            delete State;
            ExpertRemove();
            return;
           }

然后从结果分布中对代理者的动作进行采样。

         Actor.getResults(TempData);
         int action = GetAction(TempData);
         if(action < 0)
           {
            delete State;
            ExpertRemove();
            return;
           }

判定所选动作的奖励。 自从上一篇文章中的最后一次实验以来,政策奖励没有变化。 因此,我计划获得该方法的可比测试结果。 这令我们能够与以前的测试相比,比较学习算法的变化对模型最终结果的影响。

         double reward = Rates[i - 1].close - Rates[i - 1].open;
         switch(action)
           {
            case 0:
               if(reward < 0)
                  reward *= -20;
               else
                  reward *= 1;
               break;
            case 1:
               if(reward > 0)
                  reward *= -20;
               else
                  reward *= -1;
               break;
            default:
               if(batch == 0)
                  reward = -fabs(reward);
               else
                 {
                  switch((int)vActions[batch - 1])
                    {
                     case 0:
                        reward *= -1;
                        break;
                     case 1:
                        break;
                     default:
                        reward = -fabs(reward);
                        break;
                    }
                 }
               break;
           }

将获得的值保存到缓冲区中,以便将来用它们来更新模型权重。

         if(!States.Add(State))
           {
            delete State;
            ExpertRemove();
            return;
           }
         vActions[batch] = (float)action;
         vRewards[SessionSize - batch - 1] = (float)reward;
         vProbs[SessionSize - batch - 1] = TempData.At(action);
         //---
        }

第一个嵌套循环的迭代到此完毕,在该循环中收集有关场次的信息。 遍历所有场次状态后,我们就得到了更新模型的完整数据集。

接下来,计算环境每种状态的累积折扣奖励。

      vectorf rewards = vectorf::Full(SessionSize, 1);
      rewards = MathAbs(rewards.CumSum() - SessionSize);
      rewards = (vRewards * MathPow(vectorf::Full(SessionSize, DiscountFactor), rewards)).CumSum();
      rewards = rewards / fmax(rewards.Max(), fabs(rewards.Min()));

 现在,我们来计算损失函数的值,并在获得最优结果时保存模型。

      loss = (fmin(count, 9) * loss + (rewards * MathLog(vProbs) * (-1)).Sum() / SessionSize) / fmin(count + 1, 10);
      count++;
      float total_reward = vRewards.Sum();
      if(BestLoss >= loss)
        {
         if(!Actor.Save(ACTOR + ".nnw", loss, 0, 0, Rates[shift - SessionSize].time, false) ||
            !Critic.Save(CRITIC + ".nnw", Critic.getRecentAverageError(), 0, 0, Rates[shift - SessionSize].time, false))
            return;
         BestLoss = loss;
        }

请注意,我们会在更新权重矩阵之前保存模型。 因为这些已达到结果的模型,其参数是我们用损失函数估算的。 更新矩阵之后,模型将得到更新的参数,只有在下一个场次完成后,我们才会看到新参数的结果。

在学习世代的迭代结束时,我们实现了另一个嵌套循环,在其中我们组织模型参数的更新。 在此,我们将从缓冲区提取环境状态,并以删除得状态验算两个模型。 记住要控制操作的执行。

      for(int batch = SessionSize - 1; batch >= 0; batch--)
        {
         State = States.At(batch);
         if(!Actor.feedForward(State) ||
            !Critic.feedForward(State))
           {
            ExpertRemove();
            return;
           }

请注意,每个模型的前馈验算是强制性的。 即便我们已将所有必要的数据保存在缓冲区之中。 事实是,在反向传播过程中,学习算法采用来自神经层计算的中间数据。 为了调整分布误差梯度,并更新权重,我们需要明确模型针对每个状态的所有中间值的完整链条排列。

然后,更新评价者参数。 请注意,模型更新仅应用于代理者选择的动作。 其余动作的梯度被视为零。 与前面描述的理论材料的差异,是因使用了预训练 Q-函数引起的,该函数依据代理者选择的动作返回预测奖励。 该方法作者提出的状态评估函数的训练,不依赖于正在执行的动作,也不需要如此详细。

         Critic.getResults(TempData);
         float value = TempData.At(TempData.Maximum(0, 3));
         if(!TempData.Update((int)vActions[batch], rewards[SessionSize - batch - 1]))
           {
            ExpertRemove();
            return;
           }
         if(!Critic.backProp(TempData))
           {
            ExpertRemove();
            return;
           }

更新评价者参数成功后,同样更新扮演者参数。 如上所述,为了评估代理者选择的动作,我们取 Q-函数的结果向量中的最大值来分析环境状态。

         if(!TempData.BufferInit(Actions, 0) ||
            !TempData.Update((int)vActions[batch], rewards[SessionSize - batch - 1] - value))
           {
            ExpertRemove();
            return;
           }
         if(!Actor.backProp(TempData))
           {
            ExpertRemove();
            return;
           }
        }
      PrintFormat("Iteration %d, Cummulative reward %.5f, loss %.5f", iter, total_reward, loss);
     }

更新模型参数的所有迭代完成后,向用户显示一条信息消息,并转到下一个世代。

所有世代完成后,模型训练过程结束,除非执行中被用户提前中断。

   Comment("");
//---
   ExpertRemove();
  }

清除注释字段并调用智能系统关闭函数,函数操作完成。

附件中提供了完整的智能系统代码。


4. 测试

在创建 EA 并训练模型后,我针对优势扮演者-评论者方法进行了完整的测试。 首先,我们开始模型训练过程。 这是对前两篇文章中模型的额外训练。

训练采用 EURUSD 的 H1 时间帧数据进行,加载过去两年的历史记录。 指标采用默认参数。 可以看到,模型的训练参数在整个系列文章中几乎没有变化。

针对前几篇文章中的模型进行附加训练的好处是,我们可用上一篇文章中的测试 EA 来检查它们的训练结果。 这就是我如何做的。 训练模型之后,我采用了额外训练的策略模型,并利用上述模型在策略测试器中启动了 “REINFORCE-test.mq5” EA。 它的算法在上一篇文章中已进行了讲述。 其完整代码可在附件中找到。

下面是测试期间 EA 的余额图形。 如您所见,在测试期间,余额均匀增长。 请注意,模型是基于训练样本之外的数据上进行测试的。 这表明构建交易系统的方式是一致的。 测试仅针对模型,故所有操作均以固定的最小手数执行,且不用止损和止盈。 强烈建议不要在实盘交易中使用这样的 EA。 它仅仅是演示经过训练的模型如何工作。

已训练模型测试图形

在价格图表上,您可以看到亏损交易会快速平仓,而盈利持仓则会持有一段时间。 所有操作均在新烛条开盘时执行。 您还可以注意到,若干交易操作几乎是在反转(分形)烛条开盘时就执行了。

品种图表上的交易操作

一般来说,在测试过程中,EA 显示的盈利因子为 2.20。 盈利业务的份额超过56%。 平均盈利交易比平均亏损交易高出 70%。

测试结果表格

与此同时,请注意不要将此 EA 用于实盘交易,因为它仅用于模型测试。 首先,EA 测试区间太短,不足以拍板决定将其运用在实盘交易当中。 其次,EA 没有任何资金管理或风险管理模块。 交易操作没有止损和止盈,这在实盘交易中是强烈不推荐的。


结束语

在本文中,我们熟悉了另一种强化学习方法的算法:优势扮演者-评价者。 该算法结合了以前研究过的多种方式,将 Q-学习和政策梯度的作用发挥至最佳。 它还允许改进在模型强化训练过程中获得的结果。 我们利用 MQL5 构建了所研究的算法,并基于真实历史数据训练和测试了模型。 根据测试结果,该模型展现出产生盈利的能力,由此我们可以得出结论,可以使用该模型训练算法构建交易系统。

在现有条件下,扮演者-评价者算法家族可能是所有强化学习方法中最好的结果。 不过,在将这些模型用于实盘交易之前,它们需要长时间的训练和彻底的测试。 包括各种压力测试。


参考文献列表

  1. 深度强化学习的异步方法
  2. 神经网络变得轻松(第二十六部分):强化学习
  3. 神经网络变得轻松(第二十七部分):深度 Q-学习(DQN)
  4. 神经网络变得轻松(第二十八部分):政策梯度算法

本文中用到的程序

# 名称 类型 说明
1 Actor-Critic.mq5 EA 训练模型的智能系统
2 REINFORCE-test.mq5 EA
在策略测试器中测试模型的智能系统
3 NeuroNet.mqh 类库 创建神经网络模型的类库
4 NeuroNet.cl 代码库
创建神经网络模型的 OpenCL 程序代码库



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

附加的文件 |
MQL5.zip (71.22 KB)
学习如何基于奥森姆(Awesome)振荡器设计交易系统 学习如何基于奥森姆(Awesome)振荡器设计交易系统
在我们系列的这篇新文章中,我们将学习一种也许对我们的交易有用的新技术工具。 它是奥森姆(Awesome)振荡器((AO)指标。 我们将学习如何基于该指标设计交易系统。
DoEasy. 控件 (第 18 部分): TabControl 中滚动选项卡的功能 DoEasy. 控件 (第 18 部分): TabControl 中滚动选项卡的功能
在本文中,我将在 TabControl WinForms 对象中放置滚动标题控件的按钮,以防标题栏不适配控件的尺寸。 此外,我还将实现单击裁剪过的选项卡标题时,标题栏的平移。
山型或冰山型图表 山型或冰山型图表
您如何看待往 MetaTrader 5 平台里添加新图表类型的想法? 有人说它缺少其它平台里提供的一些东西。 但事实是,MetaTrader 5 是一个非常实用的平台,因为它允许您做到在许多其它平台上无法完成(或至少不能轻松完成)的事情。
学习如何基于相对活力(Vigor)指数设计交易系统 学习如何基于相对活力(Vigor)指数设计交易系统
我们系列中的新篇章,介绍如何基于最流行的技术指标设计交易系统。 在本文中,我们将学习如何基于相对活力(Vigor)指数指标来做到这一点。