English Русский Español Deutsch 日本語 Português
preview
神经网络变得轻松(第二十七部分):深度 Q-学习(DQN)

神经网络变得轻松(第二十七部分):深度 Q-学习(DQN)

MetaTrader 5交易系统 | 22 十二月 2022, 07:57
1 663 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容

概述

在上一篇文章中,我们开始探索强化学习方法,并构建了我们的第一个交叉熵可训练模型。 在本文中,我们将继续研究强化学习方法。 我们将继续深度Q-学习方法。 于 2013年,运用深度 Q-学习,DeepMind 团队设法创建了一个可以成功玩七款 Atari 电脑游戏的模型。 值得注意的是,对于所有 7 款游戏,它们在不更改架构或超参数的情况下采用相同的模型进行训练。 根据训练结果,该模型能够继续提升之前所分析的 6 个游戏取得的结果。 此外,在三场游戏中,该模型的表现优于人类。 这项工作的发表开启了强化学习方法发展的新阶段。 我们来研究这种方法,并尝试运用它来解决交易相关的问题。


1. Q-函数的概念

首先,我们回到上一篇文章中研究的素材。 在强化学习中,我们构建了代理者与其环境之间的交互过程。 代理者分析环境的当前状态,并执行动作更改环境状态。 在动作响应中,环境返回奖励给代理者。 代理者不知道奖励是如何形成的。 代理者的目标只是得到所分析场次的最大可能总体奖励。

请注意,代理者不会收到该动作的奖励。 它得到的是从一种状态过渡到另一种状态的奖励。 与此同时,在类似情况下执行某些动作并不能保证过渡到同一状态。 执行动作只能提供一定概率转移到预期状态。 代理者不知道状态、动作和转换的概率,以及依赖关系。 代理者必须从与环境的交互过程中学习它们。 

事实上,强化学习是基于这样的假设,即当前状态、采取的行动和奖励之间存在某种关系。 在数学术语中,有一个函数 Q,它根据状态 s 和动作 a,返回奖励 r。 它表示为 Q(s|a)。 此函数称为动作功用函数

代理者不知道此函数。 但如果它存在,那么在与环境交互的过程中,通过无限次重复动作,我们可以近似这个函数。

在实际条件下,不可能无限次地重复状态和动作。 但经足够的重复,我们就可得到可接受误差的近似函数。 Q 函数表达式的形式可以有所不同。 在上一篇文章中,在判定每个动作的功用时,我们构建了一个状态、动作、和平均奖励的依赖关系表。 还有以其它形式表达的 Q 函数,均可完全接受,或可产生更好的结果。 这些可以是决策树、神经网络、等等。

请注意,由代理者近似的 Q 函数并不能预测奖励。 它仅根据代理者过去与环境交互的经验返回预期回报。


2. 深度 Q-学习

您可能已经猜到了深度 Q-学习涉及运用神经网络来近似 Q 函数。 这种方式有什么优势? 请记住上一篇文章中交叉熵表格方法的实现。 我强调,表格方法的实现假定可能的状态和动作数量是有限的。 故此,我们通过初始数据聚类来限制可能的状态数量。 但它有那么好吗? 聚类总能产生更好的结果吗? 运用神经网络不会限制可能的状态数量。 我认为在解决交易相关问题时,这是一个极棒的优势。

最明显的第一个方法是用神经网络替换上一篇文章中的表格。 但是,不幸的是,这并不容易。 在实践中,这种方式并不像看起来那么美好。 为了实现该方法,我们需要添加一些启发式方法。

首先,我们来看看代理者训练目标。 一般来说,它的目标是总体奖励最大化。 请看下图。 代理者必须从 Start 单元格移动到 Finish 单元格。 代理者在到达 Finish 单元格时才会收到一次性奖励。 在所有其它状态,奖励均为零。

折扣因子

该示意图展示了两条路径。 对我们来说,很明显,橙色路径更短、更可取。 但就奖励最大化而言,它们是等价的。

与此类似,在交易中,立即获得收入,比之现在投入资金,并在遥远的将来获得收入更可取。 考虑到资金的价值:利率、通货膨胀、和许多其它变量。 我们在此也做同样的事。 为了解决这个问题,我们引入了折扣因子 ɣ,其将降低未来的奖励值。

累计奖励

折扣因子 ɣ 可在 0 到 1 的范围内。 如果折扣因子为 1,则不发生折扣。 折扣因子为 0 时,未来的奖励将被忽略。 实际上,折扣因子设置为接近 1。

但这里还有另一个问题。 理论上看起来优良的东西,在实践中也许并不总是有效。 当我们有一个完整的转换与奖励映射时,我们就可以很容易地计算出未来的奖励。 其中,我们可以选择最后回报最大的最佳路径。 但在解决实际问题时,我们不知道执行某个动作后的下一个状态会是什么。 我们也不知道奖励多少。 所有这些都已应用到紧邻的下一步。 当我们谈论至场次结束的整个道路时,情况就更加严重了。 我们无法预见未来。 为了获得下一个奖励,代理者需要执行动作。 只有在转换到新状态之后,环境才会返回奖励。 甚至于,我们没有退路可言。 我们不能回到以前的状态,并采取另一个行动,以便之后能选择最好的状态。

因此,我们将应用动态可编程方法。 特别是贝尔曼(Bellman)优化方法。 它言道,为了选择最优策略,有必要在每个步骤中选择最佳动作。 也就是说,通过选择每一步奖励最大的动作,我们将获得场次的最大累积奖励。 更新动作功用函数的数学公式如下所示。

贝尔曼(Bellman)优化

看看公式。 是否它让您想起随机梯度下降权重更新公式? 确是这样,为了更新动作功用函数的数值,我们需要函数的前期值加上一些偏差再乘以学习因子。

在所呈现的函数中还可以注意到,为了判定时间点 t 处的函数值,我们需要得到下一个时间步骤,即在点 t+1 处的动作功用函数值。 换言之,在状态 st 中,我们采取操作 at,在我们转换到状态 st+1 后,我们得到奖励 rt+1。 为了更新动作功用函数的数值,我们需要在下一步中将动作功用函数的最大值添加到奖励之中。 这就是,我们加进下一步可获得的最大预期奖励。 当然,我们的代理者无法预见未来,并判定未来的奖励。 但它可用其近似函数:处于状态 st+1,它可以计算给定状态中所有可能动作的函数值,并从所获得值中选取最大值。 在学习的过程中,其数值距最开始会很远。 但总比没有好。 随着代理者不断学习,预测误差将随之减小。


2.1. 体验回放

随机梯度下降很优秀,因为它允许根据群体之中小规模样本的数值更新函数值。 实际上,它允许我们的代理者在场次的每个步骤更新动作功用函数值。 但在监督学习中,我们所用训练样本,其状态彼此独立。 为了加强这一属性,我们每次在选择新的训练数据集之前都对群体进行洗牌。

然而现在,在监督学习中,我们的代理者在贯穿我们的环境中随时移动,执行一个动作,且每次都进入与前一个密切相关的新状态。 环顾您的四周。 无论您是走路还是坐着,并执行某些动作,您周围的环境都不会发生显著变化。 您的动作仅改变了动过打击处的一小部分。 与此类似,当代理者执行动作时,所研究环境的状态不会有太大变化。 这意味着连续的状态将在很大程度上相互关联。 我们的代理者将观察此类状态的自相关。

而困难在于,即使采用较小的训练系数也无法阻止代理者将动作功用函数调整到当前状态,从而牺牲以往记忆的经验。

在监督学习中,在大量迭代后采用独立状态可以平均模型的权重值。 在强化学习的情况下,当我们使用连接且几乎不变的状态训练模型时,模型会重新训练到当前状态。

与任何时间序列一样,状态之间的关系随着它们之间的时间增加而减小。 因此,为了解决这个问题,我们需要在训练代理者模型时使用分散在时间轴上的状态。 如果我们拥有历史数据的话,这很容易完成。 但当沿着环境移动时,我们的代理者不曾拥有这样的记忆。 它只能看到当前状态,而不能从一个状态跳到另一个状态。

那么,我们为什么不为代理者组织记忆呢? 看,要更新动作功用函数的值,我们需要以下数据集:

状态 -> 动作 -> 奖励 -> 状态

我们这样做,如此这般在沿环境移动时,代理者将必要的数据集保存到缓冲区之中。 缓冲区大小是一个超参数,由模型体系结构判定。 当缓冲区已满时,新到达的数据将取代较旧的数据。 为了训练模型,我们不会使用当前状态,但我们将使用从代理者记忆中随机选择的数据。 通过这种方式,我们最大限度地减少了各个状态之间的关系,并提高了模型概括分析数据的能力。


2.2. 使用目标网络

学习动作功用函数时应注意的另一点是该函数在下一步 maxQ(st+1|at+1) 的最大值。 请注意,这是“来自未来的数值”。 因此,我们根据近似动作功用函数获取预测值。 但在时间 t,我们不能从时间 t+1 状态更改数值。 每次我们更新函数值时,我们都会更新模型的权重,从而更改下一个预测值。

甚而,我们训练我们的代理者来获得最大的奖励。 因此,在每次模型更新迭代中,我们都会最大化预期值。 基于预测值以递归方式最大化更新的数值。 以这种方式,我们在级数中最大化我们的动作功用函数值。 这会导致高估我们的函数值,且增加了预测动作功用的误差。 这样并不太好。 因此,我们需要一个固定的机制来评估未来的动作功用。

我们可通过创建一个额外的模型来预测未来动作功用,从而解决这个问题。 但这种方式需要额外的成本,因为要训练第二个模型。 这是我们极力避免的事情。 另一方面,我们已经在训练一个执行此功能的模型。 但在权重变化后,模型应当如更新之前一样返回函数的值。 这个有争议的问题可以通过复制模型来解决。 我们简单地创建同一动作功用函数模型的两个实例。 一个实例经过训练,另一个实例则用于预测未来动作功用。

一旦动作功用函数的模型被固定,它很快就会在学习过程中变得无关紧要。 这会令进一步的训练效率低下。 为了消除这一因素的影响,我们需要在学习过程中更新预测值的模型。 第二个实例不会并行训练。 取而代之,我们从动作功用函数模型的已训练实例复制权重至具有一定周期性的第二个实例当中。 因此,仅通过训练一个模型,我们获得了动作功用函数模型的最新 2 个实例,并避免了递归造成的对预测值的高估。 

我们汇总以上内容:

  1. 为了训练代理者,我们运用神经网络。
  2. 神经网络经过训练,可预测动作功用 Q-函数的期望值。
  3. 为了令相邻状态之间的相关性最小化,学习过程使用记忆缓冲区,从中随机提取状态。
  4. 为了预测 Q-函数的未来值,准备了第二个目标网络模型,它是训练模型的“冻结”副本。
  5. 目标网络是周期性从已训练模型中复制权重矩阵来实现的。

现在,我们来查看利用 MQL5 描述方式的实现。


3. 利用 MQL5 实现

为了利用 MQL5 实现深度 Q-学习算法,我们创建 “Q-learning.mq5” EA 文件。 完整的智能系统代码可在文后附件中找到。 在此,我们只关注深度 Q-学习方法的实现。

在继续实现之前,我们来决定初始数据和奖励系统是什么。 初始数据与我们之前实验中所采用的数据相同。 那么,奖励系统呢? 我们早前研究的分形预测问题是出于人为。 当然,我们可以创建一个模型来判定最大可能的分形数。 但我们的主要目标是从交易操作中赚取最大的利润。

以这种上下文状况,采用下一根蜡烛的大小作为奖励大小是完全有意义的。 当然,奖励的符号必须与所执行的操作相对应。 在简化模型中,我们有两种交易操作:买入和卖出。 我们也可能出位。

在此,我们不会以判定持仓量、加仓、或部分平仓来令模型复杂化。 我们假设代理者可拥有固定手数的持仓。 此外,代理者可以清仓,并离场观望。

此外,在奖励政策方面,我们必须明白,训练结果在很大程度上取决于精心准备的奖励系统。 强化学习的实践提供了大量示例,若其选择了不正确的奖励政策,则会导致出乎意料的结果。 模型可能学会得出错误的结论。 它也可能陷入试图获得最大奖励,却总也无法达到预期结果的困境。 例如,我们可以奖励模型开仓和平仓。 但若此奖励超过从交易中累积的利润,则模型可以学习简单地开仓和平仓。 那么因,该模型的奖励最大化,而我们的亏损最大化。

另一方面,如果我们惩罚开仓和平仓模型,类似于一次操作的佣金,则模型可以简单地学会在场外观望。 没有利润,但也没有损失。

考虑到全部上述所言,我决定创建一个具有三种可能动作的模型:买入、卖出、场外观望。

代理者将预测每根新烛条的预期走势方向,并在不考虑前期动作的情况下选择一个动作。 因此,为了简化模型,我们不会在代理者中输入有关它是否有持仓或持仓方向的信息。 相应地,代理者不会跟踪开仓和平仓。 开仓和平仓也不会给予奖励。

为了尽量减少“场外观望”的时间,我们将惩罚没有持仓的情况。 但这样的惩罚应低于持仓亏损的惩罚。

故此,此为代理者的奖励政策:

  1. 一笔盈利持仓获得与烛条主体大小相等的奖励(分析每根烛条的系统状态;我们处于从烛条开盘到收盘的位置)。
  2. “场外观望”状态按照烛台实体的大小(烛台主体大小带负号表示亏损)作为惩罚。
  3. 亏损持仓受到双倍烛台主体大小(亏损 + 利润损失)的惩罚。

现在我们已经定义了奖励系统,我们可以直接转到方法实现。

如上所述,我们的模型将用到两个神经网络。 为此,我们需要创建两个对象来操控神经网络。 我们将训练 StudyNet,而 TargetNet 则用于预测 Q-函数的未来值。

CNet                StudyNet;
CNet                TargetNet;


为了规划深度 Q-学习方法的操作,我们还需要新的外部变量来判定构建和训练模型的超参数。

  • Batch — 权重更新批量大小
  • UpdateTarget — 在复制到“冻结”模型之前,预测未来 Q-函数值的已训练模型的权重矩阵的更新次数
  • Iterations — 训练期间已训练模型更新的迭代总数
  • DiscountFactor — 未来奖励折扣因子
input int                  Batch =  100;
input int                  UpdateTarget = 20;
input int                  Iterations = 1000;
input double               DiscountFactor =   0.9;


神经网络模型的创建将在此 EA 之外实现。 为了创建它,我们将取用与迁移学习相关的文章中的工具。 这种方式将允许我们采用各种架构进行实验,而无需修改 EA。 因此,在 EA 初始化方法中,仅实现加载先前创建的模型。

//---
   float temp1, temp2;
   if(!StudyNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false) ||
      !TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
      return INIT_FAILED;


请注意,由于我们使用同一模型的两个实例,因此两个模型都是从同一文件加载的。

采用不同架构方案的可能性不仅意味着所采用隐藏层的不同架构及其大小,还意味着调整所分析历史深度的可能性。 之前,我们在 EA 代码中创建模型,历史深度则由外部参数判定。 现在,我们可以通过源数据层的大小来判定所分析的历史深度。 EA 将根据源数据层大小进行分析判定。 只有所分析历史的每根烛条的神经元数量和结果层的大小保持不变。 因为这些参数在结构上与所用的指标和可预测操作的数量有关。

   if(!StudyNet.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   StudyNet.getResults(TempData);
   if(TempData.Total() != Actions)
      return INIT_PARAMETERS_INCORRECT;


我们之前不曾讨论过深度 Q-学习模型中原始层的大小。 如上所述,Q-函数根据状态和所执行的动作返回预期奖励。 为了判定最有用的动作,我们需要计算当前状态下所有可能动作的函数值。 运用神经网络可以创建结果层,其中神经元的数量等于所有可能动作的数量。 在这种情况下,结果层的每个神经元将负责预测特定动作的功用。 神经网络的一次验算就能提供所有动过的功用值。 然后我们只需要选择最大值。

EA 初始化函数的其余部分保持不变。 附件中提供了其完整代码。

模型训练过程将在 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();


由于我们采用历史数据来训练模型,因此无需创建记忆缓冲区。 我们可以简单地将所有历史数据当作单独的记忆缓冲区。 但如果模型是实时训练的,我们就需要添加记忆缓冲区,并对其进行管理。

接下来,准备辅助变量:

  • total — 训练样本的大小
  • use_target — 目标网络所用的标志,来预测未来奖励

   int total = bars - (int)HistoryBars - 240;
   bool use_target = false;


我们用到 use_target 标志,因为我们需要在 Target Net 模型首次更新之前禁用预测未来奖励。 这实际上是一个非常微妙的观点。 在初始步骤中,以随机权重初始化模型。 因此,所有预测值都是随机的。 最有可能的是,它们与真实值相去甚远。 采用此类随机值可能会令模型学习过程失真。 在这种情况下,模型将近似的不是真实的奖励值,而是嵌入在模型本身中的随机值。 因此,在 Target Net 模型第一次迭代更新之前,我们应该剔除这种噪音。

接下来,实现代理者训练循环系统。 外部循环将计算更新代理者权重矩阵的迭代总数。

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter += UpdateTarget)
     {
      int i = 0;


在嵌套循环中,我们将计算权重更新批次大小,和 Target Net实现之前的更新次数。 这里应该注意的是,我们模型中的权重在反向传播验算的每次迭代中都会更新。 因此,使用更新批次看起来不太正确,因为对于我们的模型,它始终设置为 1。 然而,为了平衡 Target Net 实现之间的已处理状态数,其频率将等于封包大小与实现之间的更新次数的乘积。

在循环体中,我们随机判定当前模型训练迭代的系统状态。 我们还清除缓冲区,以写入两个后续状态。 第一个状态将用于训练模型的前馈验算。 第二个将用于 Target Net.中的预测 Q-函数值。

      for(int batch = 0; batch < Batch * UpdateTarget; batch++)
        {
         i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
         State1.Clear();
         State2.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;


然后,在嵌套循环中,用历史数据填充准备好的缓冲区。 为避免不必要的操作,在填充第二个状态缓冲区之前,请检查 Target Net 标志的使用情况。 仅在必要时才会填充缓冲区。

         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)
               continue;
            //---
            if(!State1.Add((float)Rates[bar_t].close - open) || !State1.Add((float)Rates[bar_t].high - open) ||
               !State1.Add((float)Rates[bar_t].low - open) || !State1.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State1.Add(sTime.hour) || !State1.Add(sTime.day_of_week) || !State1.Add(sTime.mon) ||
               !State1.Add(rsi) || !State1.Add(cci) || !State1.Add(atr) || !State1.Add(macd) || !State1.Add(sign))
               break;
            if(!use_target)
               continue;
            //---
            bar_t --;
            open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            rsi = (float)RSI.Main(bar_t);
            cci = (float)CCI.Main(bar_t);
            atr = (float)ATR.Main(bar_t);
            macd = (float)MACD.Main(bar_t);
            sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State2.Add((float)Rates[bar_t].close - open) || !State2.Add((float)Rates[bar_t].high - open) ||
               !State2.Add((float)Rates[bar_t].low - open) || !State2.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State2.Add(sTime.hour) || !State2.Add(sTime.day_of_week) || !State2.Add(sTime.mon) ||
               !State2.Add(rsi) || !State2.Add(cci) || !State2.Add(atr) || !State2.Add(macd) || !State2.Add(sign))
               break;
           }


用历史数据成功填充缓冲区之后,检查它们的大小,并执行两个模型的前馈验算。 不要忘记检查操作结果。

         if(IsStopped())
           {
            ExpertRemove();
            return;
           }
         if(State1.Total() < (int)HistoryBars * 12 ||
            (use_target && State2.Total() < (int)HistoryBars * 12))
            continue;
         if(!StudyNet.feedForward(GetPointer(State1), 12, true))
            return;
         if(use_target)
           {
            if(!TargetNet.feedForward(GetPointer(State2), 12, true))
               return;
            TargetNet.getResults(TempData);
           }


前馈验算完成后,我们从环境中获得奖励,并根据上面定义的奖励政策为反向传播验算准备目标缓冲区。

请注意以下两个时刻。 第一个点,我们检查 Target Net 标志的使用。 仅在结果为正面的情况下添加预测值。 如果将标志设置为 false,则 Q-函数的预测值应设置为 0。

第二个点则是从贝尔曼方程偏转。 您还记得,贝尔曼方程采用未来奖励的最大值。 以这种方式,训练的模型能赚取最大的利润。 当然,这种方式可以带来最大的盈利能力。 但在交易的情况下,当价格图表充斥着很多噪音时,这会导致交易数量的增加。 甚至,噪声降低了预报的品质。 这可与在每根新蜡烛尝试预测进行比较。 着可能导致几乎在每根新蜡烛都开仓和平仓,取代判定趋势,并在趋势方向上开仓。

为了消除上述因素的影响,我决定从贝尔曼方程中转移。 为了更新 Q-函数模型,我将采用单向值。 最大值则仅用于“场外观望”动作。

         Rewards.Clear();
         double reward = Rates[i - 1 + 240].close - Rates[i - 1 + 240].open;
         if(reward >= 0)
           {
            if(!Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-2 * (use_target ? reward + DiscountFactor * TempData.At(1) : 0)))
               ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;
           }
         else
            if(!Rewards.Add((float)(2 * reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(1) : 0))) ||
               !Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;


准备好奖励缓冲区后,在已训练模型中运行反向传播验算。 再次检查操作执行结果。

         if(!StudyNet.backProp(GetPointer(Rewards)))
            return;
        }


这样就完成了嵌套循环的操作,计算代理者训练的迭代次数。 完成后,更新 Target Net 模型。 我们的模型没有权重交换方法。 我决定不发明任何新东西。 取而代之,我们将采用现有机制来保存和加载模型。 在这种情况下,我们得到了模型及其所有内容的精确副本。

因此,将已训练模型保存到文件中,并将保存的模型从该文件加载到 TargetNet。 不要忘记检查操作结果。

      if(!StudyNet.Save(FileName + ".nnw", StudyNet.getRecentAverageError(), 0, 0, Rates[i].time, false))
         return;
      float temp1, temp2;
      if(!TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
         return;
      use_target = true;
      PrintFormat("Iteration %d, loss %.5f", iter, StudyNet.getRecentAverageError());
     }


成功更新 TargetNet 模型后,更改其使用标志,将包含信息的消息打印到日志,然后继续执行外部循环的下一次迭代。

一旦训练过程完成后,清除注释,并启动关闭模型训练 EA。

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


完整的智能系统代码可在文后附件中找到。


4. 测试

该方法已基于过去 2 年的 H1 时间帧 EURUSD 数据进行了测试。 在之前的所有实验中都采用相同的数据。 指标采用默认参数。

出于测试目的,创建了以下架构的卷积模型:

  1. 初始数据层,240 个元素(20 根蜡烛,每根蜡烛含 12 个神经元)。
  2. 卷积层,输入数据窗口 24(2 根蜡烛),步长 12(1 根蜡烛),6 个滤波器输出。
  3. 卷积层,输入数据窗口 2,步长 1,2 个过滤器。
  4. 卷积层,输入数据窗口 3,步长 1,2 个过滤器。
  5. 卷积层,输入数据窗口 3,步长 1,2 个过滤器。
  6. 含有 1000 个元素的完全连接神经层。
  7. 含有 1000 个元素的完全连接神经层。
  8. 由 3 个元素组成的完全连接层(3 个操作的结果层)。

从 2 到 7 层由 sigmoid 激活。 对于结果层,双曲正切用作激活函数。

下图显示了误差动态图。 如您从图中可见,在学习过程中,预测预期奖励的误差正在迅速减少。 经过 500 次迭代后,它变得接近 0。 模型经过 1000 次迭代训练过程后以 0.00105 的误差结束。

DQN 模型测试图


结束语

在本文中,我们继续研究强化学习方法。 我们研究了 DeepMind 团队在 2013 年引入的深度 Q-学习方法。 这项工作的发表开启了强化学习方法发展的新阶段。 该方法展示了训练模型构建策略的可能性前景。 神hi,运用一个模型就可训练它来解决各种问题,而无需对其架构或超参数进行结构更改。 这些是训练算法优于 EA 结果的第一次实验。

我们已见识到利用 MQL5 实现该方法。 模型测试结果证明了运用该方法构建工作交易模型的可能性。


参考文献列表

  1. 运用深度强化学习玩转 Atari
  2. 神经网络变得轻松(第二十五部分):实践迁移学习
  3. 神经网络变得轻松(第二十六部分):强化学习

本文中用到的程序

# 名称 类型 说明
1 Q-learning.mq5 EA 训练模型的智能系统 
2 NeuroNet.mqh 类库 创建神经网络模型的类库
3 NeuroNet.cl 代码库
创建神经网络模型的 OpenCL 程序代码库


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

附加的文件 |
MQL5.zip (66.7 KB)
学习如何基于 VIDYA 设计交易系统 学习如何基于 VIDYA 设计交易系统
欢迎阅读我们的关于学习如何依据最流行的技术指标设计交易系统系列的新篇章,在本文中,我们将学习一种新的技术工具,并学习如何依据可变指数动态平均线(VIDYA)设计交易系统。
DoEasy. 控件(第 16 部分):TabControl WinForms 对象 — 多行选项卡标题,拉伸标题适配容器 DoEasy. 控件(第 16 部分):TabControl WinForms 对象 — 多行选项卡标题,拉伸标题适配容器
在本文中,我将继续开发 TabControl,并针对设置标题大小的所有模式,实现选项卡标题在控件所有四个侧边的排列:正常、固定、和靠右填充。
神经网络变得轻松(第二十八部分):政策梯度算法 神经网络变得轻松(第二十八部分):政策梯度算法
我们继续研究强化学习方法。 在上一篇文章中,我们领略了深度 Q-学习方法。 按这种方法,已训练模型依据在特定情况下采取的行动来预测即将到来的奖励。 然后,根据政策和预期奖励执行动作。 但并不总是能够近似 Q-函数。 有时它的近似不会产生预期的结果。 在这种情况下,近似方法不应用于功用函数,而是应用于动作的直接政策(策略)。 其中一种方法是政策梯度。
神经网络变得轻松(第二十六部分):强化学习 神经网络变得轻松(第二十六部分):强化学习
我们继续研究机器学习方法。 自本文,我们开始另一个大话题,强化学习。 这种方式允许为模型设置某些策略来解决问题。 我们可以预期,强化学习的这种特性将为构建交易策略开辟新的视野。