概述
探索问题是强化学习的一大障碍,尤其代理者得到的奖励很稀有且有滞后的处境下,这令制定有效策略变得困难。 这个问题的可能解决方案之一是基于环境模型产生“内在”奖励。 我们在研究内在好奇心模块时曾见过类似的算法。 然而,大多数已创建算法仅在计算机游戏的关联背景下进行了研究。 但在静默模拟环境之外,由于代理者-环境交互的随机性质,训练预测模型颇具挑战性。 在解决环境随机性问题的方式中,Deepak Pathak 在他的文章“凭借分歧进行自我监督探索”中提出了一种算法。
该算法基于自学习方法,其中代理者利用与环境交互期间获得的信息来生成“内在”奖励,并更新其策略。 该算法基于使用若干个代理模型,这些模型与环境交互,并生成各种预测。 如果模型有分歧,则将其视为“有趣”事件,并且激励代理者去探索环境空间。 以这种方式,该算法激励代理者探索环境的新区域,并令其对未来的奖励做出更准确的预测。
1. 凭借分歧探索算法
基于分歧的探索是一种强化学习方法,允许代理者在不依赖外部奖励的情况下探索环境,但更倾向于使用模型融汇寻找新的、未探索的区域。
在“凭借分歧进行自我监督探索”一文中,作者讲述了这种方式,并提出了一个简单的方法:训练前向动态模型的融汇,并鼓励代理者探索动作空间,其在融汇当中模型预测之间存在最大不一致或方差。
因此,代理者并非选择产生最大预期奖励的动作,代理者选择的是融汇当中模型之间分歧最大的动作。 这令代理者探索状态空间的区域,其中融汇当中的模型有分歧,以及可能存在新的和未探索的环境区域。
在这种情况下,融汇当中的所有模型都收敛到均值,最终减少融汇的差距,并为代理者提供有关环境状态和动作可能后果的更准确预测。
此外,凭借分歧进行探索的算法允许代理者成功应对与环境交互的随机性。 本文作者进行的实验结果表明,所提出的方式真实改进了随机环境中的探索,并且优于先前存在的内在动机和不确定性建模方法。 此外,他们观察到这些方式可以扩展到监督学习,其中样本的值不是基于真实标签,而是基于模型融汇的状态来判定的。
故此,凭借分歧进行探索的算法是解决随机环境探索问题的一种有前途的方法。 它允许代理者更有效地探索环境,而不必依赖外部奖励,这在外部奖励可能有限或成本不菲的实际应用程序中尤其实用。
甚而,该算法可以应用于各种环境,包括操控高维数据,譬如图像等,其中测量和最大化模型的不确定性可能特别具有挑战性。
本文的作者证明了所提出的算法在若干个问题中的有效性,包括机器人控制、雅达利游戏、和迷宫导航任务。 作为他们的研究结果,他们表明,凭借分歧进行探索的算法在速度、收敛性和学习品质方面优于其它探索方法。
因此,这种凭借分歧进行探索的方式代表了强化学习领域的重要一步,它可以帮助代理者更好、更有效地探索环境,并在各种任务中取得更好的结果。
我们来研究一下提议的算法。
在与环境交互的过程中,代理者评估当前状态 Xt ,并在其内部策略的指导下执行一些动作 At。 结果就是,环境的状态更改为新的状态 Xt+1。 一组此类数据存储在体验回放缓冲区之中,我们用它来训练预测未来环境状态的动态模型融汇。
为了在初始阶段保持对未来环境状态的独立评估,融汇当中动态模型的所有权重矩阵都填充了随机值。 在训练过程中,每个模型都会从体验回放缓冲区接收自己的随机训练数据集。
我们融汇当中的每个模型都经过训练,从而预测真实环境的下一个状态。 代理者从已充分探索的状态空间部分收集到足够的数据来训练所有模型,成果在模型之间保持一致。 由于模型已训练过,此功能应泛化到状态空间中不熟悉但相似的部分。 不过,对于所有模型,新的和未探索的区域仍有很高的预测误差,因为它们尚未基于此类样本进行过训练。 结果就是,我们在预测下一个状态方面存在分歧。 因此,我们将这种分歧作为政策方向的内在奖励。 具体来说,内在奖励 Ri 定义为融汇当中不同模型输出的方差。
请注意,在上面的公式中,内在奖励不依赖于系统的未来状态。 稍后在实现此方法时,我们会用到此性质。
在随机场景的情况下,给定足够数量的样本,动态预测模型必须学习预测随机样本的平均值。 以这种方式,融汇当中输出的离散将降低,从而防止代理者卡在所研究随机局部最小值。 请注意,这与基于预测误差的目标不同,后者经历足够多的样本后稳定在平均值。 均值与单个真实随机状态不同,并且预测误差仍然很高,这令代理者始终对随机行为感兴趣。
当使用所提议的算法时,代理者与环境交互的每个步骤不仅提供了有关从环境收到的奖励的信息,而且还提供了更新代理者内部模型所需的信息,即该模型在执行动作时环境状态如何变化。 这令代理者能提取有关环境的有价值信息,即使没有明确的外部奖励。
原文中的模型演示
内在奖励 iR 被用于训练代理者的政策,其是计算融汇当中不同模型输出的方差。 模型输出之间的分歧越大,内在奖励的价值就越高。 这令代理者去探索状态空间的新区域,其中下一个状态的预测是不确定的,并学习根据这些数据制定更好的决策。
代理者依据在与环境交互过程中收集的数据进行在线训练。 同时,在代理者与环境的每次交互后,都会更新模型的融汇,这令代理者可以在每一步更新其关于环境的内部模型,并获得对未来环境状态的更准确的预测。
2. 利用 MQL5 实现
在我们的实现中,我们不会完全重复所提议的算法,而只会运用它的主要思想,并调整它们来适配我们的任务。
我们做的第一件事就是要求一组动态模型来预测压缩(隐藏)的系统状态,类似于内在好奇心模型。 这将允许我们压缩动态模型和融汇整体的大小。
第二点是,要判定内在奖励,我们不需要知道系统的真实状态,而是需要知道动态融汇模型的预测值。 这令我们能够依据预测性奖励来刺激后续学习,还可以做出实时动作决策。 我们不会在训练代理者的策略时通过引入内在组件来扭曲外部奖励,而是允许它针对最大化外部奖励立即构建策略。 这是我们的主要目标。
然而,为了在学习过程中最大限度地学习环境,在选择代理者的动作时,我们将在预测奖励中累加动态模型针对每个可能的代理者动作的预测分歧方差。
这就引出了另一点:为了并行计算每个动作后的预测状态,我们要求动态模型根据当前状态为我们提供每个可能的代理者动作的预测,并根据可能的动作数量增加每个模型的结果层的大小。
现在我们已经定义了主要的工作方向,我们可以继续实现算法。 第一个问题是如何实现动态模型的集合。 我们之前创建的所有模型都是线性的。 可以在一个子进程和一个神经层中利用 OpenCL 工具组织并行计算。 目前还无法实现多个模型的并行计算。 为若干个模型创建计算序列会导致训练模型所花费时间显著增加。
为了解决这个问题,我决定使用我们针对多关注者的并行计算组织方法。 那一次,我们将来自所有关注者的数据组合成单个张量,并在 OpenCL 中的任务空间级别将它们划分。
我们现在不会重新制作整个函数库来解决这些问题。 在这个阶段,未来系统状态的预测值的特定准确性对于我们来说并不重要。 模型融汇能相对同步工作就足够了。 因此,在动态预测模型中,我们将使用全连接层。
首先,我们将创建 OpenCL 程序内核来组织此功能。 前馈内核 FeedForwardMultiModels 与类似的基本全连接层内核几乎相同。 但也有细微的区别。
内核参数保持不变。 它有三个数据缓冲区(权重矩阵、源数据和结果张量),以及两个常量:源数据层的大小,和激活函数。 但之前,我们指定前一层的完整大小等源数据层的大小。 现在我们期望收到当前模型的元素数量。
__kernel void FeedForwardMultiModels(__global float *matrix_w,
__global float *matrix_i,
__global float *matrix_o,
int inputs,
int activation
)
{
int i = get_global_id(0);
int outputs = get_global_size(0);
int m = get_global_id(1);
int models = get_global_size(1);
在内核主体中,我们首先识别当前线程。 您可以在此处注意到问题空间出现了第二个维度,该维度标识当前模型。 问题的整体维度则指示融汇的大小。
接下来,我们声明必要的局部变量,并在数据缓冲区中定义偏移量,同时考虑正在计算的神经元和融汇之中的当前模型。
float sum = 0;
float4 inp, weight;
int shift = (inputs + 1) * (i + outputs * m);
int shift_in = inputs * m;
int shift_out = outputs * m;
计算神经元状态和激活函数的实际数学部分保持不变。 我们只在数据缓冲区中添加了偏移调整。
for(int k = 0; k <= inputs; k = k + 4)
{
switch(inputs - k)
{
case 0:
inp = (float4)(1, 0, 0, 0);
weight = (float4)(matrix_w[shift + k], 0, 0, 0);
break;
case 1:
inp = (float4)(matrix_i[shift_in + k], 1, 0, 0);
weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], 0, 0);
break;
case 2:
inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + 1], 1, 0);
weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], 0);
break;
case 3:
inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + 1], matrix_i[shift_in + k + 2], 1);
weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], matrix_w[shift + k + 3]);
break;
default:
inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + 1], matrix_i[shift_in + k + 2],
matrix_i[shift_in + k + 3]);
weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + 1], matrix_w[shift + k + 2], matrix_w[shift + k + 3]);
break;
}
float d = dot(inp, weight);
if(isnan(sum + d))
continue;
sum += d;
}
一旦参数中指定的激活函数的值计算之后,结果则保存到 matrix_o 数据缓冲区之中。
if(isnan(sum))
sum = 0;
switch(activation)
{
case 0:
sum = tanh(sum);
break;
case 1:
sum = 1 / (1 + exp(-sum));
break;
case 2:
if(sum < 0)
sum *= 0.01f;
break;
default:
break;
}
matrix_o[shift_out + i] = sum;
}
该解决方案允许我们在一个内核中并行计算融汇之中所有模型某一层的数值。 当然,它有一个局限性:此处融汇之中所有模型的架构都是相同的,唯一的区别在于加权系数。
反向验算的情况略有不同。 该算法提供依据一组不同的训练数据集上训练融汇之中的动态模型。 我们不会为每个模型创建单独的训练包。 取而代之,在每次后向验算时,我们只从融汇中随机选择一个模型进行训练。 对于其它模型,我们将零梯度传递给前一层。 这些就是我们针对 CalcHiddenGradientMultiModels 层内的梯度分布内核算法所做的修改。
基础全连接神经层的类似内核在其参数中接收指向四个数据缓冲区的指针和两个变量。 这是权重矩阵的张量,和前一层结果的张量,用于计算激活函数的导数。 还有 2 个梯度缓冲区:当前和之前的神经层。 第一个包含接收到的误差梯度,第二个用于记录内核的结果,并将误差梯度传递到前一个神经层。 在变量中,我们指示当前层中的神经元数量,和前一层的激活函数。 对于指定的参数,我们添加训练模型的标识符,我们将在主程序一端随机选择该标识符。
__kernel void CalcHiddenGradientMultiModels(__global float *matrix_w,
__global float *matrix_g,
__global float *matrix_o,
__global float *matrix_ig,
int outputs,
int activation,
int model
)