
神经网络变得轻松(第四十三部分):无需奖励函数精通技能
概述
强化学习是一种强大的机器学习方式,它允许代理者通过与环境交互、并以奖励函数的形式接收反馈,如此来自行学习。 然而,强化学习的一个关键挑战是需要定义一个奖励函数,即把代理者的期望行为形式化。
判定奖励函数可能是一门复杂的艺术,尤其是在需要多个目标、或存在模棱两可情况的任务之中。 此外,某些任务也许没有明确的奖励函数,这令传统的强化学习方法难以应用。
在本文中,我们引入了 “多样性就是您所需要的一切” 这一概念,它允许您在没有明确奖励函数的情况下教导模型一项技能。 动作的多样性、对环境的探索、以及最大化与环境交互的可变性,是训练代理者有效行为的关键因素。
这种方式为没有奖励函数的训练提供了新的视角,也许在解决难以或不可能辨别显式奖励函数的复杂问题时有大用。
1. “多样性就是您所需要的一切” 概念
在现实生活中,为了让执行者顺利执行某些功能,需要一定的知识和技能。 类似地,在训练模型时,我们努力培养解决给定问题所需的技能。
在强化训练中,激励模型的主要工具是奖励函数。 它允许代理者明白其行动有多么成功。 然而,奖励往往是罕见的,需要额外的方式来寻找最优解。 我们已经研究过一些鼓励模型去探索其环境的方法,但它们并不总是有效的。
按照传统方式训练的模型专业面太窄,只能解决特定问题。 随着问题的形式发生微小的变化,即使现有的技能也许仍有用,但也需对模型进行彻底的重新训练。 当环境发生变化时,也会发生同样的事情。
该问题的一个可能的答案是采用由若干个模块组成的分层模型。 在此类模型中,我们针对不同的技能创建单独的模型,并由一个调度器来管理这些技能的使用。 训练调度器令我们能够运用以前训练过的技能来解决新问题。 然而,这引发了对预先训练技能的充分性和品质的质疑,因为解决新问题可能需要额外的技能。
“多样性就是您所需要的一切” 概念建议采用拥有独立技能和调度器的分层模型。 它强调最大程度的动作变化、以及对环境的探索,令代理者能够有效地训练和适应。 通过教导多样化和独特的技能,该模型变得更加灵活和适应,具有在不同状况使用不同策略的能力。 当辨别显性奖励具有挑战性时,这种方式就很实用,允许模型自主探索和找到新的解。
这个概念背后的中心思想是将多样性当作训练工具。 模型的动作和行为的多样性,令其能够探索状态空间,并发现新的可能性。 多样性并不局限于随机或无效的动作,而是瞄准在不同状况下发现可应用的不同策略。
“多样性就是您所需要的一切” 概念意味着多样性是无需明确奖励函数,即可成功训练的关键组成部分。 依据技能多样性训练的模型,变得更加灵活和适应性,能够根据关联环境和任务需求采用不同的策略。
难以判定显式奖励函数、或无法访问时,这种方法在解决复杂问题方面具有潜在的应用价值。 它允许模型独立探索环境,学习各种技能和策略,从而发现新的路径和解。
“多样性就是您所需要的一切” 概念背后的另一个假设是,模型的当前状态不仅取决于所选择的具体行动,还取决于所采用的技能。 也就是说,该模型不是简单地将动作和状态关联起来,而是学习将某些状态与某些技能相关联。
概念算法由两个阶段组成。 首先,技能多样性的无指导学习与特定任务无关,如此能够对环境进行彻底探索,并扩展代理者的行为工具箱。 接下来是监督强化学习阶段,旨在实现模型在解决既定目标时的最大效率。
在第一阶段,我们训练技能模型。 模型的输入包括环境的当前状态,和选择所要应用的特定技能。 该模型生成相应的动作,随后会执行。 该动作的结果是转换到环境的新状态。 在这个阶段,我们只对这个新状态感兴趣,未用到外部奖励。
取而代之,我们使用一个鉴别器模型,该模型基于新状态,尝试判定在上一步中用到了哪种技能。 鉴别器结果与应用技能对应的独热向量之间的交叉熵,既是对我们技能模型的奖励。
技能模型使用强化学习方法(譬如,角色-评论家)进行训练。 另一方面,鉴别器模型是采用经典监督学习方法进行训练的。
在技能模型训练开头,我们采用固定的基本技能,其不依赖于当前状态。 这是由于我们还没有关于技能对应不同状态有效性的信息。 我们的工作就是学习这些技能。 在开发模型架构时,我们检测将要训练的技能数量。
在技能模型训练过程中,代理者会根据从环境中接收到的信息,主动探索并完成每项技能。 我们将技能 ID 随机提供给模型,如此它就能学习和填充每个技能,独立于其它。
该模型使用所学习技能的 ID,和环境的当前状态,来判定要执行的相应动作。 它学习将某些技能与特定状态相关联,并为每个技能选择动作。
重点注意的是,在训练开始时,该模型没有关于技能或其在特定条件下有效性的先验知识。 它独立研究,并判定训练过程中技能和状态之间的联系。 在这种情况下,用到一个奖励函数,其根据所用技能,促进了代理者行为多样性的最大化。
一旦技能模型训练阶段完成,我们就移到下一个阶段,即监督强化学习。 在这一步中,我们训练一个调度器模型,目标是在给定目标、或在特定任务中获得的奖励最大化。 而在进行时,我们可以采用固定的技能模型,它可以加快调度器模型的训练过程。
因此,以两步方式训练技能模型,从无监督技能完成开始,到监督强化学习结束,允许模型在各种任务中独立学习和使用技能。
注意,在我们的方式中,我们已修改了出自前面所讨论分层模型当中的分层决策过程。 之前,我们用到多个代理者,每个代理者都自带技能。 代理者提出动作选项,然后调度器评估这些选项,并做出最终决定。
我们在当前方式中修改了此顺序。 现在,调度器首先分析当前形势,并决定选择相应的技能。 然后,代理者基于所选技能决定相应的动作。
因此,我们颠倒了分层过程:现在调度器决定所用技能,然后代理者执行与所选技能相对应的动作。 这种改变令我们能够根据当前形势有效地管理和运用技能。
2. 利用 MQL5 实现
现在我们移到具体实现我们的操作。 如同上一篇文章,我们首先创建一个样本数据库,我们将用该数据库来训练模型。 数据收集由 “DIAYN\Research.mq5” EA 运作,它是上一篇文章中 EA 的改编版本。 不过,当前的算法有一些差异。
我们所做的第一处更改与模型的架构有关。 我们对架构进行了修改,以满足 “多样性就是您所需要的一切” 概念产生的新要求和思路。
我们在训练过程中用到了三种模型:
- 代理者(技能)模型。 它负责根据环境的当前状态教导和实现各种技能。
- 一个调度器,它基于对形势的评估做出决策,并选择适当的技能来完成任务。 调度器与技能模型协同工作,并指导更高级别的决策。
- 鉴别器仅在技能模型训练期间使用,不会实时使用。 它用于提供反馈,并计算训练期间的奖励。
重点注意的是,技能模型和调度器是独立操作,和解决问题时所用的主要模型。 鉴别器仅用于改善技能模型的训练,不用于系统的实际操作。
bool CreateDescriptions(CArrayObj *actor, CArrayObj *scheduler, CArrayObj *discriminator) { //--- if(!actor) { actor = new CArrayObj(); if(!actor) return false; } //--- if(!scheduler) { scheduler = new CArrayObj(); if(!scheduler) return false; } //--- if(!discriminator) { scheduler = new CArrayObj(); if(!scheduler) return false; }
根据 “多样性就是您所需要的一切” 算法,为代理者模型(技能模型)提供一个输入数据缓冲区,其中包含当前状态的描述,和正在使用的技能标识符。 在我们的工作关联环境中,我们传达以下信息:
- 价格走势和指标的历史数据:该数据提供有关市场过去价格走势,和各种指标值的信息。 它们为代理者模型的决策提供了重要的关联环境。
- 有关当前账户余额和持仓的信息:该数据包括有关当前账户余额、持仓、持仓量规模、和其它财务参数的信息。 它们帮助代理者模型参考当前形势和约束做出决策。
- 独热(One-hot)技能 ID 向量:该向量是正在使用的技能 ID 的二进制表示形式。 它指示代理者模型在给定状态下理当应用的特定技能。
为了处理此类输入,需要一个足够大小的源数据层,致使代理者模型获得有关市场状况、财务数据、和所选技能的所有必要信息,从而做出最佳决策。
//--- Actor actor.Clear(); CLayerDescription *descr; //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (int)(HistoryBars * BarDescr + AccountDescr + NSkills); descr.window = 0; descr.activation = None; descr.optimization = ADAM; if(!actor.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(!actor.Add(descr)) { delete descr; return false; }
准备好的原始数据可用卷积层模块进行处理。
卷积层是深度学习模型架构中的关键组件,尤其是在图像和序列处理任务当中。 它们允许从源数据中提取空间和局部依赖项。
在 “多样性就是您所需要的一切” 算法的情况下,卷积层可应用于历史价格走势数据和指标,以便提取重要的形态和趋势。 这有助于代理者掌握不同时间步骤之间的关系,并基于检测到的形态做出决策。
每个卷积层由 4 个过滤器组成,这些过滤器使用特定窗口扫描输入数据。 应用卷积运算的结果是一组特征映射图,其突显出数据的重要特征。 这种转换允许代理者模型在强化学习任务的关联环境中检测并参考数据的重要特征。
卷积层为代理者模型提供了 “查看”、和关注数据含义层面的能力,这是 “多样性是您所需要的一切” 决策和执行动作的重要一步。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count - 1; descr.window = 2; descr.step = 1; descr.window_out = 4; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; prev_count = descr.count = prev_count; descr.window = 4; descr.step = 4; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count - 1; descr.window = 2; descr.step = 1; descr.window_out = 4; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
贯穿卷积层模块之后,数据在决策模块中进行处理,决策模块由三个全连接层组成。 通过全连接层传递数据,代理者模型能够学习复杂的依赖关系,并发现数据不同方面之间的关系。
决策模块的输出使用 FQF(全参数化分位数函数)。 该模型用于评估未来奖励、或目标变量分布的分位数。 它令代理者模型不仅可以获得平均值的估值,还可以预测各种分位数,这对于随机条件下的不确定性建模和决策非常实用。
利用完全参数化的 FQF 模型作为决策模块的输出,令代理者模型能够做出更灵活、更准确的预测,这些预测可在 “多样性是您所需要的一切” 概念框架内选择最优动作。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 256; descr.optimization = ADAM; descr.activation = TANH; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 256; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 128; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronFQF; descr.count = NActions; descr.window_out = 32; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
调度器模型针对环境的当前状态执行分类,来判定所要采用的技能。 与代理者模型不同,调度器具有简化的体系结构,无需用到卷积层进行数据预处理,从而节省了资源。
调度器的输入数据与代理者的输入数据类似,除了技能识别向量之外。 调度接收环境当前状态的描述,包括历史价格走势数据、指标、以及有关当前账户状态、和持仓的信息。
环境状态的分类,和所用技能的判定,是通过全连接层传递数据,及 FQF 模块来执行的。 调用 SoftMax 函数对结果进行常规化。 这导致了一个概率向量,反映出属于每个可能技能的状态概率。
因此,调度器模型允许人们根据环境的当前状态判定应采用哪种技能。 这进一步帮助代理者模型做出相应的决策,并根据 “多样性是您所需要的一切” 的概念选择最优动作。
//--- Scheduler scheduler.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (HistoryBars * BarDescr + AccountDescr); descr.window = 0; descr.activation = None; descr.optimization = ADAM; if(!scheduler.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(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 256; descr.optimization = ADAM; descr.activation = TANH; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 256; descr.optimization = ADAM; descr.activation = LReLU; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronFQF; descr.count = NSkills; descr.window_out = 32; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSoftMaxOCL; descr.count = NSkills; descr.step = 1; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; }
为了技能多样化,我们用到第三种模型 — 鉴别器。 它的任务是为众多意外动作进行奖励,这有助于代理者行为的多样性。 该模型的精度要求不高,因此我们决定进一步简化其架构,并取消 FQF 模块。
在鉴别器架构中,我们仅采用常规化层和全连接层。 这样可以降低计算资源,同时保持模型的分类能力。 在模型的输出中,我们调用 SoftMax 函数来获取属于不同技能的动作概率。
//--- Discriminator discriminator.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (HistoryBars * BarDescr + AccountDescr); descr.window = 0; descr.activation = None; descr.optimization = ADAM; if(!discriminator.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(!discriminator.Add(descr)) { delete descr; return false; } //--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 256; descr.optimization = ADAM; descr.activation = TANH; if(!discriminator.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 256; descr.optimization = ADAM; descr.activation = LReLU; if(!discriminator.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = NSkills; descr.optimization = ADAM; descr.activation = None; if(!discriminator.Add(descr)) { delete descr; return false; } //--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSoftMaxOCL; descr.count = NSkills; descr.step = 1; descr.optimization = ADAM; if(!discriminator.Add(descr)) { delete descr; return false; } //--- return true; }
在定义好模型架构之后,我们就可转到为训练规划收集数据的过程。 在数据收集的第一阶段,我们仅用到代理者模型,因为我们尚无有关环境的任何第一手信息。 代之,我们可以有效地使用随机生成的技能识别向量,其可与未经训练的模型生成对比结果。 这样我们就能显著降低所用的计算资源。
在 OnTick 方法里规划数据收集的直接过程。 在方法开始时,我们检查是否新柱线开盘事件发生,如果是,我们加载历史数据。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(!IsNewBar()) return; //--- int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) return; //--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh();
与上一篇文章类似,我们将有关当前状态的信息加载到两个数组之中:历史数据状态数组,和符合 sState 结构的有关帐户状态的信息数组。
MqlDateTime sTime; for(int b = 0; b < (int)HistoryBars; b++) { float open = (float)Rates[b].open; TimeToStruct(Rates[b].time, sTime); float rsi = (float)RSI.Main(b); float cci = (float)CCI.Main(b); float 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; //--- sState.state[b * 12] = (float)Rates[b].close - open; sState.state[b * 12 + 1] = (float)Rates[b].high - open; sState.state[b * 12 + 2] = (float)Rates[b].low - open; sState.state[b * 12 + 3] = (float)Rates[b].tick_volume / 1000.0f; sState.state[b * 12 + 4] = (float)sTime.hour; sState.state[b * 12 + 5] = (float)sTime.day_of_week; sState.state[b * 12 + 6] = (float)sTime.mon; sState.state[b * 12 + 7] = rsi; sState.state[b * 12 + 8] = cci; sState.state[b * 12 + 9] = atr; sState.state[b * 12 + 10] = macd; sState.state[b * 12 + 11] = sign; }
sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE); sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY); sState.account[2] = (float)AccountInfoDouble(ACCOUNT_MARGIN_FREE); sState.account[3] = (float)AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); sState.account[4] = (float)AccountInfoDouble(ACCOUNT_PROFIT); //--- double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0; int total = PositionsTotal(); for(int i = 0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) continue; switch((int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += PositionGetDouble(POSITION_PROFIT); break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += PositionGetDouble(POSITION_PROFIT); break; } } sState.account[5] = (float)buy_value; sState.account[6] = (float)sell_value; sState.account[7] = (float)buy_profit; sState.account[8] = (float)sell_profit;
我们将生成的结构保存到样本数据库中,供后续训练模型。 若要将源数据传输到代理者模型,我们需要创建一个数据缓冲区。 在本例中,我们首先将历史数据加载到该缓冲区之中。
State1.AssignArray(sState.state);
为了确保具有不同账户规模的模型更稳定、均衡有效地运行,决定将有关账户状态的信息转换为相对单位。 为此,我们将对帐户状态指标进行一些修改。
我们将用余额变化因子,替代绝对余额值。 这将允许余额随时间相对变化。
我们还将用净值与余额比率替换净值指标。 这将有助于参考净值相对于余额的相对比例,并令该指标在不同账户之间更具可比性。
此外,我们还将添加净值变化相对于余额的比率,这令我们能够参考相对净值随时间的变化。
最后,我们将引入累计盈亏余额比率,以便审计累计交易结果相对于账户余额的相对等级。
这些变化将创建一个更通用的模型,可以在不同规模的账户里有效处理,并审计它们的相对健康状况。
State1.Add((sState.account[0] - prev_balance) / prev_balance); State1.Add(sState.account[1] / prev_balance); State1.Add((sState.account[1] - prev_equity) / prev_equity); State1.Add(sState.account[3] / 100.0f); State1.Add(sState.account[4] / prev_balance); State1.Add(sState.account[5]); State1.Add(sState.account[6]); State1.Add(sState.account[7] / prev_balance); State1.Add(sState.account[8] / prev_balance);
为了完成模型的数据准备工作,我们将创建一个随机的独热向量,作为技能标识符。 独热向量是一个二进制向量,其中只有一个元素是 1,其余元素是 0。 这允许模型基于特定技能对应的元素值来区分和识别不同的技能。
生成随机的独热向量可确保每个数据样本中的技能 ID 是多样化和不同的。 这与我们的 “多样性就是您所需要的一切” 理念是一致的。
vector<float> one_hot = vector<float>::Zeros(NSkills); int skill=(int)MathRound(MathRand()/32767.0*(NSkills-1)); one_hot[skill] = 1; State1.AddArray(one_hot);
在这个阶段,我们将准备好的初始数据传输到 Actor 模型,并在模型中执行前向验算。 前向验算是将输入数据传递到模型各层,并生成相应输出值的过程。
执行前向验算后,我们获得模型输出,这些输出表示由 Actor 模型判定的每个动作的概率。 为了选择要执行的动作,我们基于获得的概率对可能的动作之一进行抽样(随机选择,参考概率)。
动作采样允许 Actor 根据每个技能尽可能多地探索环境。 这提升了模型可采取动作的多样性,并有助于避免过于频繁地选择相同的动作。 这种方式为模型提供了更大的灵活性,和适应不同状况环境的能力。
if(!Actor.feedForward(GetPointer(State1), 1, false)) return; int act = Actor.getSample();
该方法的进一步代码取自以前的 EA 版本,没有任何变化。 EA 的完整代码,包括其所有方法,都可以在附件中找到。
在之前的文章中已经详述了如何收集样本数据库,因此我们不再啰嗦,而是立即转入开发训练模型的 “DIAYN\Study.mq5” EA。 我们主要延用以前的代码,但针对调用 Train 训练方法进行了重大修改。
我们略微偏离了方法作者提出的原始算法。 在我们的 EA 中,我们并行训练技能模型和调度器。 当然,根据 “多样性就是您所需要的一切” 概念,鉴别器也是与它们一起训练的。
如此,我们努力实现模型技能和行为的多样性,从而获得更多的可持续和有效结果。
如同以前一样,模型训练发生在循环之内。 该循环的迭代次数在 EA 外部参数中确定。
在训练循环的每次迭代中,我们从样本数据库中随机选择一次验算和状态。 选择状态后,我们将有关价格走势和指标的历史数据加载到数据缓冲区当中,类似于在数据收集 EA 中完成的方式。
//+------------------------------------------------------------------+ //| Train function | //+------------------------------------------------------------------+ void Train(void) { int total_tr = ArraySize(Buffer); uint ticks = GetTickCount(); for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++) { int tr = (int)(((double)MathRand() / 32767.0) * (total_tr - 1)); int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2)); State1.AssignArray(Buffer[tr].States[i].state);
我们还将账户状态和持仓数据添加到同一数据缓冲区中。 如早前所述,我们将这些数据转换为相对单位,从而模型在不同规模账户中更加稳健。 这令我们能够统一模型中账户状态和持仓的表示,并确保它们在训练中的可比性。
float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0]; float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1]; State1.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance); State1.Add(Buffer[tr].States[i].account[1] / PrevBalance); State1.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity); State1.Add(Buffer[tr].States[i].account[3] / 100.0f); State1.Add(Buffer[tr].States[i].account[4] / PrevBalance); State1.Add(Buffer[tr].States[i].account[5]); State1.Add(Buffer[tr].States[i].account[6]); State1.Add(Buffer[tr].States[i].account[7] / PrevBalance); State1.Add(Buffer[tr].States[i].account[8] / PrevBalance);
准备好的数据对于调度器模型来说已经足够了,我们可以执行贯穿模型的前向验算,来判定要使用的技能。
if(IsStopped()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; } if(!Scheduler.feedForward(GetPointer(State1), 1, false)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; }
执行贯穿调度器模型的前向验算,并获得概率向量之后,我们形成一个独热技能识别向量。 在此我们已有两种选项供选择技能:贪婪式选择,即选择概率最高的技能;以及抽样,即我们基于概率随机选择技能。
在训练阶段,建议用抽样来最大限度地探索环境。 这令模型能够探索不同的技能,并发现隐藏的能力,以及最优策略。 在训练期间,抽样有助于避免过早地收敛到特定技能,并允许进行更多样化的探索活动,从而促进更灵活和适应性更强的训练模式。
int skill = Scheduler.getSample(); SchedulerResult = vector<float>::Zeros(NSkills); SchedulerResult[skill] = 1; State1.AddArray(SchedulerResult);
生成的技能识别向量会被添加到源数据缓冲区当中,该缓冲区将传递到代理者模型的输入。 此后,将执行贯穿代理者模型的正向验算,来生成动作。 从模型中获得的概率分布用于动作抽样。
从概率分布中进行动作抽样,代理者模型可以基于每个动作的概率做出各种决策。 这样可以鼓励探索不同的策略和行为选项,也有助于模型避免过早地固化于特定动作。
if(IsStopped()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; } if(!Actor.feedForward(GetPointer(State1), 1, false)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; } int action = Actor.getSample();
在执行代理者模型的前向验算之后,我们继续为鉴别器模型的前向验算形成数据缓冲区,其中会定义系统的下一个状态。 与上一步类似,我们首先将历史数据加载到缓冲区之中。 在这种情况下,我们只需将样本数据库中的历史数据复制到数据缓冲区中即可,因为这些指标不依赖于模型和所用的技能。
State1.AssignArray(Buffer[tr].States[i + 1].state);
我们在叙述帐户状态时遇到一些困难。 我们不能简单地从样本数据库中提取数据,因为它与所选动作罕有匹配。 同样,我们不能简单地替换样本数据库中的动作,因为鉴别器将分析接收的状态作为输入,并将其与所用的技能进行比较。 这就是间隙出现的地方。
不过,重点注意的是,鉴别器的输出仅用作奖励函数。 在描述新的帐户余额状态时,我们不需要很高的精度。 取而代之,我们需要不同活动之间的可比性数据。 因此,我们可以基于之前的状态近似估算账户状态值,同时参考最后一根蜡烛的大小和所选动作。 我们已经拥有了计算所需的所有数据。
在第一阶段,我们从之前的状态复制账户数据,并计算当价格移动到最后一根蜡烛的数值时多头持仓的利润。 此处,我们不考虑持仓的具体交易量、及其方向。 我们稍后会考虑这些参数。
vector<float> account; account.Assign(Buffer[tr].States[i].account); int bar = (HistoryBars - 1) * BarDescr; double cl_op = Buffer[tr].States[i + 1].state[bar]; double prof_1l = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_PROFIT) * cl_op / SymbolInfoDouble(_Symbol, SYMBOL_POINT);
然后,我们基于所选动作调整帐户信息。 最简单的情况是平仓。 我们只需将累计损益添加到当前账户余额中即可。 结果值随即转移至净值和可用保证金元素,其余指标重置为零。
在执行交易操作时,我们需要增加相应持仓量。 考虑到所有交易都是以最小手数进行的,我们将按最小手数增加相应持仓量。
为了计算每个方向的累计盈亏,我们将先前计算的一手仓位的利润乘以相应持仓量。 由于多头仓位的利润之前已计算,因此我们将该数值加进先前累计的多头仓位利润,并减去空头仓位的利润。 账户上的总利润是按不同方向的利润相加而获得的。
净值计算为余额和累计利润的总和。
保证金指标保持不变,因为对于最小手数,这样的变化是微不足道的。
在持仓的情况下,方式类似,只是仓量变化而已。
switch(action) { case 0: account[5] += (float)SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); account[7] += account[5] * (float)prof_1l; account[8] -= account[6] * (float)prof_1l; account[4] = account[7] + account[8]; account[1] = account[0] + account[4]; break; case 1: account[6] += (float)SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); account[7] += account[5] * (float)prof_1l; account[8] -= account[6] * (float)prof_1l; account[4] = account[7] + account[8]; account[1] = account[0] + account[4]; break; case 2: account[0] += account[4]; account[1] = account[0]; account[2] = account[0]; for(bar = 3; bar < AccountDescr; bar++) account[bar] = 0; break; case 3: account[7] += account[5] * (float)prof_1l; account[8] -= account[6] * (float)prof_1l; account[4] = account[7] + account[8]; account[1] = account[0] + account[4]; break; }
在调整余额状态和持仓数据后,我们将它们添加到数据缓冲区之中。 在这种情况下,如前,我们将它们的数值转换为相对单位,并直接传递到鉴别器模型。
PrevBalance = Buffer[tr].States[i].account[0]; PrevEquity = Buffer[tr].States[i].account[1]; State1.Add((account[0] - PrevBalance) / PrevBalance); State1.Add(account[1] / PrevBalance); State1.Add((account[1] - PrevEquity) / PrevEquity); State1.Add(account[3] / 100.0f); State1.Add(account[4] / PrevBalance); State1.Add(account[5]); State1.Add(account[6]); State1.Add(account[7] / PrevBalance); State1.Add(account[8] / PrevBalance); //--- if(!Discriminator.feedForward(GetPointer(State1), 1, false)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; }
在鉴别器前向验算之后,我们将其结果与一个独热向量进行比较,该向量包含代理者前向验算中所用技能的识别。
Discriminator.getResults(DiscriminatorResult);
Actor.getResults(ActorResult);
ActorResult[action] = DiscriminatorResult.Loss(SchedulerResult, LOSS_CCE);
通过比较两个向量获得的交叉熵值,被用作所选动作的奖励。 这种奖励允许我们回传代理者模型,并更新其权重,从而改进未来的动作选择。
Result.AssignArray(ActorResult); State1.AddArray(SchedulerResult); if(!Actor.backProp(Result, DiscountFactor, GetPointer(State1), 1, false)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; }
独热识别向量,表示正在使用的技能,是训练鉴别器模型时的目标值。 我们用这个向量作为目标来训练鉴别器,从而根据所选技能正确分类系统状态。
Result.AssignArray(SchedulerResult); if(!Discriminator.backProp(Result)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; }
我们仅使用帐户余额变化作为调度器的奖励。 我们计算该量化值的精度,并将其作为相对值表示。 不过,与仅针对所选动作获得奖励的代理者不同,我们将调度器的奖励基于所选每个技能的概率分配到所有技能。 因此,调度器的奖励根据技能的选择概率在技能之间分配。
Result.AssignArray(SchedulerResult * ((account[0] - PrevBalance) / PrevBalance)); if(!Scheduler.backProp(Result, DiscountFactor, GetPointer(State1), 1, false)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; }
在完成学习周期的每次迭代后,我们会生成一条信息消息,其中包含有关学习过程的数据。 该消息显示在图表上,以便可视化该过程。 然后,我们转进到下一次迭代,继续训练过程。
if(GetTickCount() - ticks > 500) { string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Scheduler", iter * 100.0 / (double)(Iterations), Scheduler.getRecentAverageError()); str += StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Discriminator", iter * 100.0 / (double)(Iterations), Discriminator.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } }
完成训练过程后,我们会在图表上执行消息清理,删除以前的信息数据。 然后着手关闭 EA。
Comment(""); //--- PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Scheduler", Scheduler.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Discriminator", Discriminator.getRecentAverageError()); ExpertRemove(); //--- }
附件包含 EA 中用到的所有方法和函数的完整代码。 请参阅它获取有关详细信息。
3. 测试
该模型是依据 EURUSD 金融产品 2023 年前四个月的 H1 时间帧的历史数据上训练的。 在训练过程中,发现代理者模型操作中存在非指示性错误,其与奖励政策相关,可能会导致奖励无限增长。 不过,训练过程仍受调度器和鉴别器模型的性能控制。
该过程的第二个特征是调度器的选择与所执行的动作之间没有直接关系。 计划者的选择对策略选择的影响大于对具体动作的影响。 这意味着计划者判定整体决策方式,而具体动作则由代理者模型基于当前状态和所选技能进行选择。
为了测试训练模型的性能,我们采用 2023 年 5 月前两周的数据,这些数据不包括在训练集中,但紧随训练区间。 这种方式令我们能够在新数据上评估模型的性能,同时数据保持可比性,因为训练集和测试集之间没有时间间隔。
为了进行测试,我们使用了改编后的 “DIAYN\Test.mq5” EA。 所做的修改仅影响根据模型体系结构准备数据的算法,和源数据准备过程。 模型直接验算的调用顺序也已更改。 该过程的构建方式类似于前面讲述的收集样本和训练模型数据库的智能系统。 附件中提供了详细的 EA 代码。
已训练模型的测试结果就是,达成了小额盈利,盈利因子为 1.61,恢复因子为 3.21。 在测试期间的 240 根柱线之内,该模型进行了 119 笔交易,其中近 55% 的交易以盈利平仓。
调度器在达成这些结果方面发挥了重要作用,它均匀地分配了所有技能的使用。 重点注意的是,采用了贪婪式策略选择动作和技能。 该模型基于当前状态选择最有利可图的动作。
结束语
本文介绍了一种基于 DIAYN(Diversity Is All You Need — 多样性就是您所需要的一切)方法训练交易模型的方式,其允许在不绑定特定任务的情况下以各种技能训练模型。
该模型依据 EURUSD 金融产品 2023 年前四个月的 H1 时间帧的历史数据进行了训练。
在训练期间,发现调度器的选择与执行的动作之间没有直接关系。 不过,训练过程仍然受到控制,并展现出模型具有一定的可盈利交易能力。
训练完成后,在未包含在训练集之外的新数据上测试模型。 测试结果显示小额盈利,盈利因子为 1.61,恢复因子为 3.21。 然而,为了获得更稳定、更佳的结果,需要对模型策略进行进一步的优化和改进。
该模型的一个重要方面是调度器,它均匀地分配了所有技能的使用。 这凸显了制定有效的决策制定策略,从而达成成功交易结果的重要性。
总的来说,所提出的基于 DIAYN 方法训练交易模型的方式,为自动交易的发展提供了有趣的前景。 对这种方式的进一步研究和改进,也许会导致更有效和更有利可图的交易模型。
参考文献列表
本文中用到的程序
# | 名称 | 类型 | 说明 |
---|---|---|---|
1 | Research.mq5 | 智能交易系统 | 样本收集 EA |
2 | Study.mql5 | 智能交易系统 | 模型训练 EA |
3 | Test.mq5 | 智能交易系统 | 模型测试 EA |
4 | Trajectory.mqh | 类库 | 系统状态定义结构 |
5 | FQF.mqh | 类库 | 完全参数化模型的工作安排类库 |
6 | NeuroNet.mqh | 类库 | 用于创建神经网络的类库 |
7 | NeuroNet.cl | 代码库 | OpenCL 程序代码库 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/12698
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


晚上好,
如果您想按时间进行调整,可以添加会话识别的单次热向量,并将其与源数据向量连接起来。
第二种选择是在源数据中添加时间嵌入。可以根据所需的周期进行配置。对于交易时段,一天的周期即可。对于季节性,可以将其设置为一年。