1. 软性扮演者-评价者算法
在研究 SAC 算法时,赫兹量化软件应该能立即注意到它不是 TD3 方法的直系后代(反之亦然)。但它们有一些相似之处。特别是:
-
它们都是异政策算法
-
它们都利用 DDPG 方法
-
它们都用到 2 个评论者。
添加图片注释,不超过 140 字(可选)
但不像之前讨论的两种方法,SAC 使用随机扮演者政策。这允许算法探索不同的策略,并找到最优解,同时考虑到扮演者动作的最大可变性。
说到环境的随机性,赫兹量化软件明白在 S 状态下,当执行 A 动作时,我们在 [Rmin, Rmax] 内获得 R 奖励,概率为 Psa。
软性扮演者-评价者用到的扮演者具有随机政策。这意味着处于 S 状态的扮演者能够以一定的 Pa' 概率从整个动作空间中选择 A' 动作。换言之,在每个特定状态下,扮演者的政策允许赫兹量化软件不一定选择特定的最优动作,而是任何可能的行动(但具有一定程度的概率)。在训练过程中,扮演者学习获得最大奖励的概率分布。
随机扮演者政策的这一属性令我们能够探索不同的策略,并发现在运用判定性策略时可能隐藏的最优解。此外,随机扮演者政策还考虑到环境中的不确定性。在出现噪声或随机因素的情况下,该种类政策可能更具弹性和适应性,因为它们可以生成各种动作,以便有效地与环境交互。
不过,训练扮演者的随机政策也会对训练进行调整。经典强化学习旨在最大化预期回报。在训练过程中,对于每个 S 动作,赫兹量化软件选择 A* 动作,其最有可能给我们带来更大的盈利能力。这种判定性方式建立了明确的关系 St → At → St+1 ⇒ R,并且没有给随机动作留下余地。为了训练随机政略,软性扮演者-评论者算法的作者在奖励函数当中引入了熵正则化。
在这种情况下,entropy (H) 是衡量政策不确定性或多样性的指标。ɑ>0 参数是一个温度系数,令我赫兹量化软件们能够在所研究环境和动作模型之间取得平衡。
如您所知,熵是随机变量不确定性的度量,且由方程判定
注意,我们谈论的是 [0, 1] 范围内选择动作的概率的对数。在这个可接受值的间隔内,熵函数的图形正在降低,且位于正值区域。因此,选择动作的概率越低,奖励就越高,并且鼓励模型探索环境。
如您所见,有关于此,对 ɑ 超参数的选择提出了相当高的要求。目前,有多种选项可用于实现 SAC 算法。传统的固定参数方法就是其中之一。往往是,我们可以找到参数逐渐减少的实现。很容易就能看出,当 ɑ=0 时,我们得出了判定性强化学习。此外,在训练过程中,模型本身还有多种方式可以优化 ɑ 参数。
我们转入训练评论者。与 TD3 类似,SAC 并行训练 2 个评论者模型,并采用 MSE 作为损失函数。对于未来状态的预测值,从两个评论者目标模型中取较小值。但这里有两个关键区别。
第一个是上面讨论的奖励函数。赫兹量化软件对于当前和后续状态都使用熵正则化,并针对系统下一个状态的成本,参考应用一个折扣因子。
第二个区别是扮演者。SAC 不使用目标扮演者模型。若要选择当前和后续状态下的动作,会用到一个经过训练的扮演者模型。因此,我们强调,实现未来回报是利用现行政策来达成的。此外,使用单个扮演者模型可降低内存和计算资源的成本。
为了训练扮演者政策,赫兹量化软件采用 DDPG 方式。我们通过反向传播贯穿评论者模型的预测行动成本的误差梯度,来获得动作误差梯度。但不像 TD3(其中我们只用了 Critic 1 模型),SAC 的作者建议取用估算行动成本较低的模型。
此处还有一件事。在训练期间,我们更改政策,这会导致在系统特定状态下扮演者的行为发生变化。此外,随机扮演者政策的使用也有助于扮演者动作的多样性。同时,我们依据来自经验回放缓冲区的数据来训练模型,并为其它代理者动作提供奖励。在这种情况下,我们以理论假设为指导,即在训练扮演者的过程中,我们朝着最大化预测奖励的方向前进。这意味着,在任何 S 状态下,采用 π 新政策的动作成本都不低于 π 旧政策的动作成本。
这是一个非常主观的假设,但它与我们的模型训练范式完全一致。为了不累积可能的误差,我建议在训练期间更频繁地更新经验回放缓冲区,同时考虑更新扮演者政策。
使用类似于 TD3 的 τ 因子平滑目标模型的更新。
这与 TD3 方法还有一点区别。软性扮演者-评论者算法在扮演者训练和更新目标模型时不能使用延迟。在此,所有模型都会在每个训练步骤中更新。
我们总结一下软性扮演者-评论者算法:
-
在奖励函数中引入熵正则化。
-
在训练开始时,扮演者和 2 个评论者模型以随机参数进行初始化。
-
作为与环境交互的结果,将填充经验回放缓冲区。我们保持环境状态、动作、后续状态和奖励不变。
-
填满经验回放缓冲区后,我们训练模型
-
我们从经验回放缓冲区中随机提取一组数据
-
判定未来状态的动作,同时考虑扮演者的当前政策
-
使用至少 2 个目标评论者模型的当前政策来判定未来状态的预测值
-
更新评论者模型
-
更新动作政策
-
更新目标模型。
训练模型的过程是迭代的,并重复进行,直至得到所需的结果,或达到评论者损失函数图形上的最小极值。
2. 利用 MQL5 实现
在软性扮演者-评论者算法理论概述之后,我们转入利用 MQL5 实现它。我们面临的第一件事是检测特定动作的概率。实际上,对于扮演者政策的表格化实现来说,这是一个非常简单的问题。但在使用神经网络时这会造成困难。毕竟,我们不保留有关环境条件和所采取动作的统计数据。它被“硬连线”到我们模型的可定制参数之中。有关于此,我想起了分布式 Q-训练。您也许还记得,我们谈到过有关预期回报概率分布的研究。分布式 Q-学习允许我们获得给定数量的固定区间奖励值的概率分布。完全参数化的 Q-函数(FQF)模型允许我们研究区间值及其概率。
2.1创建一个新的神经层类
继承自 CNeuronFQF 类,我们将创建一个新的神经层类来实现所提出的 CNeuronSoftActorCritic 算法。新类的方法集非常标准,但它也有自己的特性。
特别是,在我们的实现中,我们决定使用自定义熵正则化参数。为此目的,添加了 cAlphas 神经层。该实现使用 CNeuronConcatenate 类型的层。为了决定比率的大小,我们将用到当前状态的嵌入,以及在输出中用到分位数分布。
此外,我们还添加了一个单独的缓冲区来记录熵值,稍后我们将在奖励函数中用到它。
添加的两个对象都声明为静态,这允许我们将类构造函数和析构函数留空。
class CNeuronSoftActorCritic : public CNeuronFQF { protected: CNeuronConcatenate cAlphas; CBufferFloat cLogProbs; virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSoftActorCritic(void) {}; ~CNeuronSoftActorCritic(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint actions, uint quantiles, uint numInputs, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool calcAlphaGradients(CNeuronBaseOCL *NeuronOCL); virtual bool GetAlphaLogProbs(vector<float> &log_probs) { return (cLogProbs.GetData(log_probs) > 0); } virtual bool CalcLogProbs(CBufferFloat *buffer); //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual int Type(void) override const { return defNeuronSoftActorCritic; } virtual void SetOpenCL(COpenCLMy *obj); };
首先,我们将查看类的 Init 初始化方法。方法参数完全重复父类相似方法的参数。我们立即在方法主体中调用父类方法。我们经常使用这种技术,因为所有必要的控制都已在父类中实现了。所有继承对象的初始化也一并执行。一次检查父类方法的结果取代了所提及操作的完全控制。我们所要做的就是初始化添加的对象。
首先,我们初始化 ɑ 比率计算层。如上所述,我们将向该模型的输入提交当前状态的嵌入,其大小将等于前一个神经层的大小。此外,我们将在当前层的输出中添加一个分位数分布,该分位数分布将包含在内部层 cQuantile2 当中(其在父类中声明和初始化)。在 cAlphas 层的输出端,我们将获得每个单独动作的温度系数。相应地,层的大小将等于动作的数量。
系数应为非负数。为了满足这个需求,我们将该层的激活函数定义为 Sigmoid。
在方法结束时,我们用零值初始化熵缓冲区。它的大小也等于动作的数量。马上在当前 OpenCL 关联环境中创建缓冲区。
bool CNeuronSoftActorCritic::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint actions, uint quantiles, uint numInputs, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronFQF::Init(numOutputs, myIndex, open_cl, actions, quantiles, numInputs, optimization_type, batch)) return false; //--- if(!cAlphas.Init(0, 0, OpenCL, actions, numInputs, cQuantile2.Neurons(), optimization_type, batch)) return false; cAlphas.SetActivationFunction(SIGMOID); //--- if(!cLogProbs.BufferInit(actions, 0) || !cLogProbs.BufferCreate(OpenCL)) return false; //--- return true; }
接下来,我们转入实现前向验算。在此,我们从父类借用训练分位数和概率分布的过程,不做任何修改。但我们必须加入一些过程,安排检测温度系数,以及计算熵值。甚至,虽然温度的计算涉及调用经由 cAlphas 层的直接验算,但检测熵值应从 “0” 开始实现。
我们必须计算扮演者每个动作的熵。在这个阶段,我们期望此处的动作不会太多。由于所有源数据都在 OpenCL 关联环境存储器当中,故逻辑上应将我们的操作转移到该环境。首先,我们将创建 OpenCL 内核程序 SAC_AlphaLogProbs,来实现此功能。
在内核参数中,我们将传递 5 个数据缓冲区和 2 个常量:
-
outputs — 结果缓冲区包含每个动作的分位数值的概率加权总和
-
quantiles — 平均分位数值(cQuantile2 内层结果缓冲区)
-
probs — 概率张量(cSoftMax 内层结果缓冲区)
-
alphas — 温度系数的向量
-
log_probs — 熵值的向量(在本例中,记录结果的缓冲区)
-
count_quants — 每个动作的分位数
-
activation — 激活函数类型。
CNeuronFQF 类在输出端不使用激活函数。我甚至要说这与类背后的观点相矛盾。毕竟,在模型训练过程中,预期奖励的分位数平均值的分布是由实际奖励本身来界定的。在我们的例子中,在自层输出端,我们期望从连续分布取得扮演者动作的确定值。由于各种技术或其它境况,代理者允许的动作界域也许会受到限制。激活函数允许我们这样做。但对于我们,在判定实际动作的概率后,获得所应用激活函数的实际概率估算非常重要。因此,我们将其实现添加到此内核之中。