内容概述1。Q函数的概念2。深度Q学习2。1。体验回放2。2。使用目标网络3。利用MQL5实现4。测试结束语参考文献列表本文中用到的程序概述 在上一篇文章中,我们开始探索强化学习方法,并构建了我们的第一个交叉熵可训练模型。在本文中,我们将继续研究强化学习方法。我们将继续深度Q学习方法。于2013年,运用深度Q学习,DeepMind团队设法创建了一个可以成功玩七款Atari电脑游戏的模型。值得注意的是,对于所有7款游戏,它们在不更改架构或超参数的情况下采用相同的模型进行训练。根据训练结果,该模型能够继续提升之前所分析的6个游戏取得的结果。此外,在三场游戏中,该模型的表现优于人类。这项工作的发表开启了强化学习方法发展的新阶段。我们来研究这种方法,并尝试运用它来解决交易相关的问题。 1。Q函数的概念 首先,我们回到上一篇文章中研究的素材。在强化学习中,我们构建了代理者与其环境之间的交互过程。代理者分析环境的当前状态,并执行动作更改环境状态。在动作响应中,环境返回奖励给代理者。代理者不知道奖励是如何形成的。代理者的目标只是得到所分析场次的最大可能总体奖励。 请注意,代理者不会收到该动作的奖励。它得到的是从一种状态过渡到另一种状态的奖励。与此同时,在类似情况下执行某些动作并不能保证过渡到同一状态。执行动作只能提供一定概率转移到预期状态。代理者不知道状态、动作和转换的概率,以及依赖关系。代理者必须从与环境的交互过程中学习它们。 事实上,强化学习是基于这样的假设,即当前状态、采取的行动和奖励之间存在某种关系。在数学术语中,有一个函数Q,它根据状态s和动作a,返回奖励r。它表示为Q(sa)。此函数称为动作功用函数。 代理者不知道此函数。但如果它存在,那么在与环境交互的过程中,通过无限次重复动作,我们可以近似这个函数。 在实际条件下,不可能无限次地重复状态和动作。但经足够的重复,我们就可得到可接受误差的近似函数。Q函数表达式的形式可以有所不同。在上一篇文章中,在判定每个动作的功用时,我们构建了一个状态、动作、和平均奖励的依赖关系表。还有以其它形式表达的Q函数,均可完全接受,或可产生更好的结果。这些可以是决策树、神经网络、等等。 请注意,由代理者近似的Q函数并不能预测奖励。它仅根据代理者过去与环境交互的经验返回预期回报。 2。深度Q学习 您可能已经猜到了深度Q学习涉及运用神经网络来近似Q函数。这种方式有什么优势?请记住上一篇文章中交叉熵表格方法的实现。我强调,表格方法的实现假定可能的状态和动作数量是有限的。故此,我们通过初始数据聚类来限制可能的状态数量。但它有那么好吗?聚类总能产生更好的结果吗?运用神经网络不会限制可能的状态数量。我认为在解决交易相关问题时,这是一个极棒的优势。 最明显的第一个方法是用神经网络替换上一篇文章中的表格。但是,不幸的是,这并不容易。在实践中,这种方式并不像看起来那么美好。为了实现该方法,我们需要添加一些启发式方法。 首先,我们来看看代理者训练目标。一般来说,它的目标是总体奖励最大化。请看下图。代理者必须从Start单元格移动到Finish单元格。代理者在到达Finish单元格时才会收到一次性奖励。在所有其它状态,奖励均为零。 该示意图展示了两条路径。对我们来说,很明显,橙色路径更短、更可取。但就奖励最大化而言,它们是等价的。 与此类似,在交易中,立即获得收入,比之现在投入资金,并在遥远的将来获得收入更可取。考虑到资金的价值:利率、通货膨胀、和许多其它变量。我们在此也做同样的事。为了解决这个问题,我们引入了折扣因子,其将降低未来的奖励值。 折扣因子可在0到1的范围内。如果折扣因子为1,则不发生折扣。折扣因子为0时,未来的奖励将被忽略。实际上,折扣因子设置为接近1。 但这里还有另一个问题。理论上看起来优良的东西,在实践中也许并不总是有效。当我们有一个完整的转换与奖励映射时,我们就可以很容易地计算出未来的奖励。其中,我们可以选择最后回报最大的最佳路径。但在解决实际问题时,我们不知道执行某个动作后的下一个状态会是什么。我们也不知道奖励多少。所有这些都已应用到紧邻的下一步。当我们谈论至场次结束的整个道路时,情况就更加严重了。我们无法预见未来。为了获得下一个奖励,代理者需要执行动作。只有在转换到新状态之后,环境才会返回奖励。甚至于,我们没有退路可言。我们不能回到以前的状态,并采取另一个行动,以便之后能选择最好的状态。 因此,我们将应用动态可编程方法。特别是贝尔曼(Bellman)优化方法。它言道,为了选择最优策略,有必要在每个步骤中选择最佳动作。也就是说,通过选择每一步奖励最大的动作,我们将获得场次的最大累积奖励。更新动作功用函数的数学公式如下所示。 看看公式。是否它让您想起随机梯度下降权重更新公式?确是这样,为了更新动作功用函数的数值,我们需要函数的前期值加上一些偏差再乘以学习因子。 在所呈现的函数中还可以注意到,为了判定时间点t处的函数值,我们需要得到下一个时间步骤,即在点t1处的动作功用函数值。换言之,在状态st中,我们采取操作at,在我们转换到状态st1后,我们得到奖励rt1。为了更新动作功用函数的数值,我们需要在下一步中将动作功用函数的最大值添加到奖励之中。这就是,我们加进下一步可获得的最大预期奖励。当然,我们的代理者无法预见未来,并判定未来的奖励。但它可用其近似函数:处于状态st1,它可以计算给定状态中所有可能动作的函数值,并从所获得值中选取最大值。在学习的过程中,其数值距最开始会很远。但总比没有好。随着代理者不断学习,预测误差将随之减小。 2。1。体验回放 随机梯度下降很优秀,因为它允许根据群体之中小规模样本的数值更新函数值。实际上,它允许我们的代理者在场次的每个步骤更新动作功用函数值。但在监督学习中,我们所用训练样本,其状态彼此独立。为了加强这一属性,我们每次在选择新的训练数据集之前都对群体进行洗牌。 然而现在,在监督学习中,我们的代理者在贯穿我们的环境中随时移动,执行一个动作,且每次都进入与前一个密切相关的新状态。环顾您的四周。无论您是走路还是坐着,并执行某些动作,您周围的环境都不会发生显著变化。您的动作仅改变了动过打击处的一小部分。与此类似,当代理者执行动作时,所研究环境的状态不会有太大变化。这意味着连续的状态将在很大程度上相互关联。我们的代理者将观察此类状态的自相关。 而困难在于,即使采用较小的训练系数也无法阻止代理者将动作功用函数调整到当前状态,从而牺牲以往记忆的经验。 在监督学习中,在大量迭代后采用独立状态可以平均模型的权重值。在强化学习的情况下,当我们使用连接且几乎不变的状态训练模型时,模型会重新训练到当前状态。 与任何时间序列一样,状态之间的关系随着它们之间的时间增加而减小。因此,为了解决这个问题,我们需要在训练代理者模型时使用分散在时间轴上的状态。如果我们拥有历史数据的话,这很容易完成。但当沿着环境移动时,我们的代理者不曾拥有这样的记忆。它只能看到当前状态,而不能从一个状态跳到另一个状态。 那么,我们为什么不为代理者组织记忆呢?看,要更新动作功用函数的值,我们需要以下数据集: 状态动作奖励状态 我们这样做,如此这般在沿环境移动时,代理者将必要的数据集保存到缓冲区之中。缓冲区大小是一个超参数,由模型体系结构判定。当缓冲区已满时,新到达的数据将取代较旧的数据。为了训练模型,我们不会使用当前状态,但我们将使用从代理者记忆中随机选择的数据。通过这种方式,我们最大限度地减少了各个状态之间的关系,并提高了模型概括分析数据的能力。 2。2。使用目标网络 学习动作功用函数时应注意的另一点是该函数在下一步maxQ(st1at1)的最大值。请注意,这是来自未来的数值。因此,我们根据近似动作功用函数获取预测值。但在时间t,我们不能从时间t1状态更改数值。每次我们更新函数值时,我们都会更新模型的权重,从而更改下一个预测值。 甚而,我们训练我们的代理者来获得最大的奖励。因此,在每次模型更新迭代中,我们都会最大化预期值。基于预测值以递归方式最大化更新的数值。以这种方式,我们在级数中最大化我们的动作功用函数值。这会导致高估我们的函数值,且增加了预测动作功用的误差。这样并不太好。因此,我们需要一个固定的机制来评估未来的动作功用。 我们可通过创建一个额外的模型来预测未来动作功用,从而解决这个问题。但这种方式需要额外的成本,因为要训练第二个模型。这是我们极力避免的事情。另一方面,我们已经在训练一个执行此功能的模型。但在权重变化后,模型应当如更新之前一样返回函数的值。这个有争议的问题可以通过复制模型来解决。我们简单地创建同一动作功用函数模型的两个实例。一个实例经过训练,另一个实例则用于预测未来动作功用。 一旦动作功用函数的模型被固定,它很快就会在学习过程中变得无关紧要。这会令进一步的训练效率低下。为了消除这一因素的影响,我们需要在学习过程中更新预测值的模型。第二个实例不会并行训练。取而代之,我们从动作功用函数模型的已训练实例复制权重至具有一定周期性的第二个实例当中。因此,仅通过训练一个模型,我们获得了动作功用函数模型的最新2个实例,并避免了递归造成的对预测值的高估。 我们汇总以上内容:为了训练代理者,我们运用神经网络。神经网络经过训练,可预测动作功用Q函数的期望值。为了令相邻状态之间的相关性最小化,学习过程使用记忆缓冲区,从中随机提取状态。为了预测Q函数的未来值,准备了第二个目标网络模型,它是训练模型的冻结副本。目标网络是周期性从已训练模型中复制权重矩阵来实现的。 现在,我们来查看利用MQL5描述方式的实现。 3。利用MQL5实现 为了利用MQL5实现深度Q学习算法,我们创建Qlearning。mq5EA文件。完整的智能系统代码可在文后附件中找到。在此,我们只关注深度Q学习方法的实现。 在继续实现之前,我们来决定初始数据和奖励系统是什么。初始数据与我们之前实验中所采用的数据相同。那么,奖励系统呢?我们早前研究的分形预测问题是出于人为。当然,我们可以创建一个模型来判定最大可能的分形数。但我们的主要目标是从交易操作中赚取最大的利润。 以这种上下文状况,采用下一根蜡烛的大小作为奖励大小是完全有意义的。当然,奖励的符号必须与所执行的操作相对应。在简化模型中,我们有两种交易操作:买入和卖出。我们也可能出位。 在此,我们不会以判定持仓量、加仓、或部分平仓来令模型复杂化。我们假设代理者可拥有固定手数的持仓。此外,代理者可以清仓,并离场观望。 此外,在奖励政策方面,我们必须明白,训练结果在很大程度上取决于精心准备的奖励系统。强化学习的实践提供了大量示例,若其选择了不正确的奖励政策,则会导致出乎意料的结果。模型可能学会得出错误的结论。它也可能陷入试图获得最大奖励,却总也无法达到预期结果的困境。例如,我们可以奖励模型开仓和平仓。但若此奖励超过从交易中累积的利润,则模型可以学习简单地开仓和平仓。那么因,该模型的奖励最大化,而我们的亏损最大化。 另一方面,如果我们惩罚开仓和平仓模型,类似于一次操作的佣金,则模型可以简单地学会在场外观望。没有利润,但也没有损失。 考虑到全部上述所言,我决定创建一个具有三种可能动作的模型:买入、卖出、场外观望。 代理者将预测每根新烛条的预期走势方向,并在不考虑前期动作的情况下选择一个动作。因此,为了简化模型,我们不会在代理者中输入有关它是否有持仓或持仓方向的信息。相应地,代理者不会跟踪开仓和平仓。开仓和平仓也不会给予奖励。 为了尽量减少场外观望的时间,我们将惩罚没有持仓的情况。但这样的惩罚应低于持仓亏损的惩罚。 故此,此为代理者的奖励政策:一笔盈利持仓获得与烛条主体大小相等的奖励(分析每根烛条的系统状态;我们处于从烛条开盘到收盘的位置)。场外观望状态按照烛台实体的大小(烛台主体大小带负号表示亏损)作为惩罚。亏损持仓受到双倍烛台主体大小(亏损利润损失)的惩罚。 现在我们已经定义了奖励系统,我们可以直接转到方法实现。 如上所述,我们的模型将用到两个神经网络。为此,我们需要创建两个对象来操控神经网络。我们将训练StudyNet,而TargetNet则用于预测Q函数的未来值。CNetStudyNet;CNetTargetNet; 为了规划深度Q学习方法的操作,我们还需要新的外部变量来判定构建和训练模型的超参数。Batch权重更新批量大小UpdateTarget在复制到冻结模型之前,预测未来Q函数值的已训练模型的权重矩阵的更新次数Iterations训练期间已训练模型更新的迭代总数DiscountFactor未来奖励折扣因子inputintBatch100;inputintUpdateTarget20;inputintIterations1000;inputdoubleDiscountFactor0。9; 神经网络模型的创建将在此EA之外实现。为了创建它,我们将取用与迁移学习相关的文章中的工具。这种方式将允许我们采用各种架构进行实验,而无需修改EA。因此,在EA初始化方法中,仅实现加载先前创建的模型。floattemp1,temp2;if(!StudyNet。Load(FileName。nnw,dError,temp1,temp2,dtStudied,false)!TargetNet。Load(FileName。nnw,dError,temp1,temp2,dtStudied,false))returnINITFAILED; 请注意,由于我们使用同一模型的两个实例,因此两个模型都是从同一文件加载的。 采用不同架构方案的可能性不仅意味着所采用隐藏层的不同架构及其大小,还意味着调整所分析历史深度的可能性。之前,我们在EA代码中创建模型,历史深度则由外部参数判定。现在,我们可以通过源数据层的大小来判定所分析的历史深度。EA将根据源数据层大小进行分析判定。只有所分析历史的每根烛条的神经元数量和结果层的大小保持不变。因为这些参数在结构上与所用的指标和可预测操作的数量有关。if(!StudyNet。GetLayerOutput(0,TempData))returnINITFAILED;HistoryBarsTempData。Total()12;StudyNet。getResults(TempData);if(TempData。Total()!Actions)returnINITPARAMETERSINCORRECT; 我们之前不曾讨论过深度Q学习模型中原始层的大小。如上所述,Q函数根据状态和所执行的动作返回预期奖励。为了判定最有用的动作,我们需要计算当前状态下所有可能动作的函数值。运用神经网络可以创建结果层,其中神经元的数量等于所有可能动作的数量。在这种情况下,结果层的每个神经元将负责预测特定动作的功用。神经网络的一次验算就能提供所有动过的功用值。然后我们只需要选择最大值。 EA初始化函数的其余部分保持不变。附件中提供了其完整代码。 模型训练过程将在Train函数中创建。在函数体的开头,判定训练场次的大小,并加载历史数据。这类似于前面研究的有监督和无监督学习算法中的过程。voidTrain(void){MqlDateTimestarttime;TimeCurrent(starttime);starttime。yearStudyPeriod;if(starttime。year0)starttime。year1900;datetimesttimeStructToTime(starttime);intbarsCopyRates(Symb。Name(),TimeFrame,sttime,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训练样本的大小usetarget目标网络所用的标志,来预测未来奖励inttotalbars(int)HistoryBars240;boolusetargetfalse; 我们用到usetarget标志,因为我们需要在TargetNet模型首次更新之前禁用预测未来奖励。这实际上是一个非常微妙的观点。在初始步骤中,以随机权重初始化模型。因此,所有预测值都是随机的。最有可能的是,它们与真实值相去甚远。采用此类随机值可能会令模型学习过程失真。在这种情况下,模型将近似的不是真实的奖励值,而是嵌入在模型本身中的随机值。因此,在TargetNet模型第一次迭代更新之前,我们应该剔除这种噪音。 接下来,实现代理者训练循环系统。外部循环将计算更新代理者权重矩阵的迭代总数。for(intiter0;(iterIterations!IsStopped());iterUpdateTarget){inti0; 在嵌套循环中,我们将计算权重更新批次大小,和TargetNet实现之前的更新次数。这里应该注意的是,我们模型中的权重在反向传播验算的每次迭代中都会更新。因此,使用更新批次看起来不太正确,因为对于我们的模型,它始终设置为1。然而,为了平衡TargetNet实现之间的已处理状态数,其频率将等于封包大小与实现之间的更新次数的乘积。 在循环体中,我们随机判定当前模型训练迭代的系统状态。我们还清除缓冲区,以写入两个后续状态。第一个状态将用于训练模型的前馈验算。第二个将用于TargetNet。中的预测Q函数值。for(intbatch0;batchBatchUpdateTarget;batch){i(int)((MathRand()MathRand()MathPow(32767,2))(total));State1。Clear();State2。Clear();intri(int)HistoryBars;if(rbars)continue; 然后,在嵌套循环中,用历史数据填充准备好的缓冲区。为避免不必要的操作,在填充第二个状态缓冲区之前,请检查TargetNet标志的使用情况。仅在必要时才会填充缓冲区。for(intb0;b(int)HistoryBars;b){intbartrb;floatopen(float)Rates〔bart〕。open;TimeToStruct(Rates〔bart〕。time,sTime);floatrsi(float)RSI。Main(bart);floatcci(float)CCI。Main(bart);floatatr(float)ATR。Main(bart);floatmacd(float)MACD。Main(bart);floatsign(float)MACD。Signal(bart);if(rsiEMPTYVALUEcciEMPTYVALUEatrEMPTYVALUEmacdEMPTYVALUEsignEMPTYVALUE)continue;if(!State1。Add((float)Rates〔bart〕。closeopen)!State1。Add((float)Rates〔bart〕。highopen)!State1。Add((float)Rates〔bart〕。lowopen)!State1。Add((float)Rates〔bart〕。tickvolume1000。0f)!State1。Add(sTime。hour)!State1。Add(sTime。dayofweek)!State1。Add(sTime。mon)!State1。Add(rsi)!State1。Add(cci)!State1。Add(atr)!State1。Add(macd)!State1。Add(sign))break;if(!usetarget)continue;bart;open(float)Rates〔bart〕。open;TimeToStruct(Rates〔bart〕。time,sTime);rsi(float)RSI。Main(bart);cci(float)CCI。Main(bart);atr(float)ATR。Main(bart);macd(float)MACD。Main(bart);sign(float)MACD。Signal(bart);if(rsiEMPTYVALUEcciEMPTYVALUEatrEMPTYVALUEmacdEMPTYVALUEsignEMPTYVALUE)continue;if(!State2。Add((float)Rates〔bart〕。closeopen)!State2。Add((float)Rates〔bart〕。highopen)!State2。Add((float)Rates〔bart〕。lowopen)!State2。Add((float)Rates〔bart〕。tickvolume1000。0f)!State2。Add(sTime。hour)!State2。Add(sTime。dayofweek)!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)HistoryBars12(usetargetState2。Total()(int)HistoryBars12))continue;if(!StudyNet。feedForward(GetPointer(State1),12,true))return;if(usetarget){if(!TargetNet。feedForward(GetPointer(State2),12,true))return;TargetNet。getResults(TempData);} 前馈验算完成后,我们从环境中获得奖励,并根据上面定义的奖励政策为反向传播验算准备目标缓冲区。 请注意以下两个时刻。第一个点,我们检查TargetNet标志的使用。仅在结果为正面的情况下添加预测值。如果将标志设置为false,则Q函数的预测值应设置为0。 第二个点则是从贝尔曼方程偏转。您还记得,贝尔曼方程采用未来奖励的最大值。以这种方式,训练的模型能赚取最大的利润。当然,这种方式可以带来最大的盈利能力。但在交易的情况下,当价格图表充斥着很多噪音时,这会导致交易数量的增加。甚至,噪声降低了预报的品质。这可与在每根新蜡烛尝试预测进行比较。着可能导致几乎在每根新蜡烛都开仓和平仓,取代判定趋势,并在趋势方向上开仓。 为了消除上述因素的影响,我决定从贝尔曼方程中转移。为了更新Q函数模型,我将采用单向值。最大值则仅用于场外观望动作。Rewards。Clear();doublerewardRates〔i1240〕。closeRates〔i1240〕。open;if(reward0){if(!Rewards。Add((float)(reward(usetarget?DiscountFactorTempData。At(0):0)))!Rewards。Add((float)(2(usetarget?rewardDiscountFactorTempData。At(1):0)))!Rewards。Add((float)(reward(usetarget?DiscountFactorTempData。At(TempData。Maximum(0,3)):0))))return;}elseif(!Rewards。Add((float)(2reward(usetarget?DiscountFactorTempData。At(0):0)))!Rewards。Add((float)(reward(usetarget?DiscountFactorTempData。At(1):0)))!Rewards。Add((float)(reward(usetarget?DiscountFactorTempData。At(TempData。Maximum(0,3)):0))))return; 准备好奖励缓冲区后,在已训练模型中运行反向传播验算。再次检查操作执行结果。if(!StudyNet。backProp(GetPointer(Rewards)))return;} 这样就完成了嵌套循环的操作,计算代理者训练的迭代次数。完成后,更新TargetNet模型。我们的模型没有权重交换方法。我决定不发明任何新东西。取而代之,我们将采用现有机制来保存和加载模型。在这种情况下,我们得到了模型及其所有内容的精确副本。 因此,将已训练模型保存到文件中,并将保存的模型从该文件加载到TargetNet。不要忘记检查操作结果。if(!StudyNet。Save(FileName。nnw,StudyNet。getRecentAverageError(),0,0,Rates〔i〕。time,false))return;floattemp1,temp2;if(!TargetNet。Load(FileName。nnw,dError,temp1,temp2,dtStudied,false))return;usetargettrue;PrintFormat(Iterationd,loss。5f,iter,StudyNet。getRecentAverageError());} 成功更新TargetNet模型后,更改其使用标志,将包含信息的消息打印到日志,然后继续执行外部循环的下一次迭代。 一旦训练过程完成后,清除注释,并启动关闭模型训练EA。Comment();ExpertRemove();} 完整的智能系统代码可在文后附件中找到。 4。测试 该方法已基于过去2年的H1时间帧EURUSD数据进行了测试。在之前的所有实验中都采用相同的数据。指标采用默认参数。 出于测试目的,创建了以下架构的卷积模型:初始数据层,240个元素(20根蜡烛,每根蜡烛含12个神经元)。卷积层,输入数据窗口24(2根蜡烛),步长12(1根蜡烛),6个滤波器输出。卷积层,输入数据窗口2,步长1,2个过滤器。卷积层,输入数据窗口3,步长1,2个过滤器。卷积层,输入数据窗口3,步长1,2个过滤器。含有1000个元素的完全连接神经层。含有1000个元素的完全连接神经层。由3个元素组成的完全连接层(3个操作的结果层)。 从2到7层由sigmoid激活。对于结果层,双曲正切用作激活函数。 下图显示了误差动态图。如您从图中可见,在学习过程中,预测预期奖励的误差正在迅速减少。经过500次迭代后,它变得接近0。模型经过1000次迭代训练过程后以0。00105的误差结束。 结束语 在本文中,我们继续研究强化学习方法。我们研究了DeepMind团队在2013年引入的深度Q学习方法。这项工作的发表开启了强化学习方法发展的新阶段。该方法展示了训练模型构建策略的可能性前景。神hi,运用一个模型就可训练它来解决各种问题,而无需对其架构或超参数进行结构更改。这些是训练算法优于EA结果的第一次实验。 我们已见识到利用MQL5实现该方法。模型测试结果证明了运用该方法构建工作交易模型的可能性。 参考文献列表运用深度强化学习玩转Atari神经网络变得轻松(第二十五部分):实践迁移学习神经网络变得轻松(第二十六部分):强化学习本文中用到的程序 名称 类型 说明 1hrQlearning。mq5 EA 训练模型的智能系统 2hrNeuroNet。mqh 类库 创建神经网络模型的类库 3hrNeuroNet。cl 代码库 创建神经网络模型的OpenCL程序代码库