强化学习之DQN进阶的三大法宝(Pytorch)
作者:互联网
三大法宝:
①:DDQN:改变Nature DQN中TD目标值中a'的产生方式。
②:Prioritized experience reply:改变从经验池采样的方式。
③:Dueling DQN:改变网络结构
本文将通过理论+实战结合的方式总结DQN与三大法宝的功能。
为了阐述清楚三种方式的各自优势:实战部分不选用NIPS DQN作为基础网络,而是用Nature DQN(后面简称DQN)。分别探索:
DQN vs DDQN:DDQN的功能
DQN vs DQN_PER :PER的功能
DQN vs Dueling DQN:改进版网络的功能
DQN vs DDQN_PER_Dueling DQN:组合的功效
一:理论部分:
1.1、DQN
1.2、DDQN
1.3、Prioritized experience reply(PER)
1.4、Dueling DQN
1.1、DQN:
DQN是基于Q-learning算法,结合神经网络来做值函数近似的算法,用于解决Q-learning难以适用于连续状态以及繁多状态下搜索困难、存储苦难的问题。
1.1.1、首先来一波论文率先提出的伪代码:(下图是2013年提出的第一代NIPS DQN,2015年在原有基础上增加了Target-Q-network,就是下面翻译部分)
1.1.2、翻译:(2015年Nature DQN算法)
1、初始化参数N(记忆库),学习率lr,贪心策略中的,衰减参数,更新步伐C
2、初始化记忆库D
3、初始化网络Net(本实验选择一个2层的网络,其中隐藏层为50个神经元的FC层),将参数W和W_指定为服从均值为0,方差为0.01高斯分布,并例化出2个相同的网络eval_net(以下简称Q1)和target_net(以下简称Q2)。
4、for episode in range(M):
5、 初始化状态observation
6、 for step in range(T):
7、 通过贪心策略选中下个动作action,其中Q(s, a)来自于eval_net的前向推理。
8、 通过环境的反馈获得observation'以及奖励值reward。
9、 将observation、observation'、action、reward打包存入记忆库D中。
10、 当记忆库存满之后,抽取batch个大小的数据(sj, sj', aj, rj)送入2个网络中
11、 以损失函数L = *( rj + * max(Q2(sj';W_)) - Q1(sj,aj;W))^2 进行训练
12、 每隔C步,更新target_net的参数W_ = W
下面这张博主画的图可以很好实现上述的伪代码过程:
1.1.3、DQN提出的原因:
Q-learning算法,使用Q表来存储动作状态值函数,通过不断尝试来更新Q表,最终达到收敛,找到了最优策略。但是其缺陷在于Q表的容量和搜索范围,当一个任务的状态数目贼多,比如围棋、图像,那么对Q表的存储量是个考验,此外,搜索也是个问题。另一方面,若任务的状态值是个连续值,比如接下去的小车位置、速度,Q表是不够用的。因此需要用值函数近似的方法,用一个非线性函数去拟合动作状态值函数,达到升级版Q表的角色。拟合这件事neual network就很擅长,输入是个状态,输出则是状态对应的各个动作的值,这不就相当于是Q表了吗,并且nn不怕你输入繁多的状态,也不怕你输入是连续值,故Q-learning将Q值存储的方式改成神经网络,那么DQN初步就形成了。当然DQN强大的原因,并不在于此。
1.1.4、DQN强大的原因:
二大优势:经验回放池 + Target-Q网络
1.1.4.1、引入经验池:就是用一个二维数组将过去的样本存储起来,供之后网络训练用,具体来说,经验池的引入:
a、首先Q-learning为off-policy算法(异策略算法,Sarsa为同策略,on-policy算法),也就是说,其生成样本<s,s_,r,a>的策略值函数更新的策略不一样,生成策略是-greedy 策略,值函数更新的策略为原始策略。故可以学习以往的、当前的、别人的样本。而经验池放的就是过去的样本,或者说过去的经验、记忆。一方面,和人类学习知识依靠于过去以往的记忆相吻合,另一方面,随机加入过去的经验会让nn更加有效率。
b、打乱样本之间的时间相关性。回顾nn中输入样本之间是无关的(除了RNN比较特殊),随机性抽取有利于消除输出结果对时间连续的偏好。
c、每次抽取batch个数据正适合nn的前向和后向传播。
1.1.4.2、引入Target-Q-network
a、这个网络的输出通过取max,乘以,加上reward,作为标签y。这是由于Q-learning是值迭代算法,r + * max(Q2(s_,a')是target Q,与Q-learning一样,DQN用这个作为标签,让Q(s,a)去无限接近标签,从而找到最优策略。
b、延迟更新网络。第一代的NIPS DQN是和Q(s,a)同时更新的,这就使得算法的稳定性降低,Q值可能一直就靠近不了target Q,但也不能不变,比如Target-Q-network刚初始化后,其产生的Q(s',a')是不准确的,就像Q-learning中Q(s',a')由于刚开始时候,也不住准确,都是0,但接下去会不断更新Q表,一段时间后,下一尝试同一个Q(s',a')出现时,此时Q(s',a')已经更新了,从第0次尝试->收敛过程中,Q(s',a')一定是在不断更新,从0到最优值。因此Target-Q-network中的网络参数不能一尘不变,一段时间后,也需要更新。
c、另外,回顾下监督学习中,标签是个不随网路参数W变动的值,因此,一定时间内,需要维持不变,既为了稳定,又为了符合监督学习的特性。
1.2、DDQN
1.2.1、DDQN提出的原因
1.2.2、产生过估计的原因
1.2.3、DDQN算法的改进
1.2.4、DDQN算法
DDQN是为了解决DQN出现Q值过度估计的问题。
1.2.1、DDQN提出的原因
如上图所示,提出DDQN的作者发现DQN算法会导致Q-eval-net的结果Q值被过度估计,也就是说通过这个网络出来的Q值比真实Q值要高出很多。
上图是论文中基于游戏Atari的实验:
1、曲线是作者通过6个随机种子去产生6次随机实验,通过跟踪某个状态动作对的Q值产生6条曲线,较深颜色的曲线是6次实验的中位线(求中间值),阴影部分是通过将6条中的上下最极端的2条通过线性插值产生。平直的直线是用训练好的policy在游戏中不断积累R,计算每个episode的衰减的return,即Gt。然后求取平均值来作为对DQN和DDQN的Q(s,a)真实值true value(而且是最优值函数)的无偏估计。
2、2条不同颜色的曲线是DQN和DDQN在训练时,从Q_eval网路中输出的Q值,不断变高是因为随着迭代加深,策略在优化,值函数在变大,最后趋于平稳说明达到收敛,顺便一提,Q值不一定单调递增,也不一定总体递增。得看具体环境,但是最终会稳定下来是确定的。寻路问题,他就是总体先下降然后再上升。比如其他环境,上图的6种游戏,那它就是逐渐上升。此外,最终的最优Q值不一定从头到尾都是几个动作中的最大。
3、如果没有过高估计的话,训练到收敛之后,Q(s,a)应该是和真实值true value是无限接近的,即上图各自颜色的直线应该与曲线的右端是重合的,这是符合TD学习的理论的。
如上图所示,Wizard of Wor和Asterix这两个游戏中,DQN的结果比较不稳定。也表明过高估计会影响到学习的性能的稳定性(DDQN打电动显然更强)。因此不稳定的问题的本质原因还是对Q值的过高估计。
1.2.2、产生过估计的原因
根本原因就是从Q-learning延续到DQN一直存在的->TD目标值中取max操作:
TD目标值:
Q-learning(DQN):
以DQN为例,如上图所示,假设Q表中Q(s',a')的四个值相等,对比DQN采用值函数近似,TD目标值的产生是通过Target-neural-network产生的,因此其值会上下有所波动,通过取max操作后,DQN的目标值变成了过大的那个柱子。
根据公式,Q(s,a)通过软更新靠近TD目标值,而TD目标值现在已经偏大,故Q(s,a)的结果也会偏高。
1.2.3、DDQN算法的改进
DDQN在DQN的基础上,仅仅只是改变了Q-target的计算方式,其余均没有改变。
DQN:
DDQN:
即:
可见DDQN的好处在于:
1、当Q_eval高估了,只要Target Q不高估,结果就正常。
2、当Target Q高估了,只要Q_eval不选那个动作,结果就正常。
1.2.4、DDQN算法
和DQN的伪代码一样,区别仅仅在第11步
1、初始化参数N(记忆库),学习率lr,贪心策略中的,衰减参数,更新步伐C
2、初始化记忆库D
3、初始化网络Net(本实验选择一个2层的网络,其中隐藏层为50个神经元的FC层),将参数W和W_指定为服从均值为0,方差为0.01高斯分布,并例化出2个相同的网络eval_net(以下简称Q1)和target_net(以下简称Q2)。
4、for episode in range(M):
5、 初始化状态observation
6、 for step in range(T):
7、 通过-贪心策略选中下个动作action,其中Q(s, a)来自于eval_net的前向推理。
8、 通过环境的反馈获得observation'以及奖励值reward。
9、 将observation、observation'、action、reward打包存入记忆库D中。
10、 当记忆库存满之后,抽取batch个大小的数据(sj, sj', aj, rj)送入2个网络中
11、 以损失函数L = *( rj + * max(Q2(sj',argmax(Q1(sj',aj';W));W_)) - Q1(sj,aj;W)) ^2 进行训练
12、 每隔C步,更新target_net的参数W_ = W
1.3、PER
1.3.1、PER的引入
1.3.2、SumTree
1.3.3、PER算法
原论文地址:https://arxiv.org/pdf/1511.05952.pdf
作者在总结中指出:PER的引入有助于提升学习效果以及学习速度。
在DQN中,我们从经验池中采样得到的batch个样本(以下简称trans)都是通过均一采样得到的,对于所有池子中的样本“一视同仁”。但是样本也有好有坏,即有的样本对网络的提升很小,有的样本对网络提升很大。具体来说,TD error大的样本,其loss就很大,通过backward propagation能加快网络收敛,而TD error很小的样本其loss很小,对反向传播的影响就很小,网络提升效益不大。故不能采用这种均一的方式,基于这个想法,就得引入按TD error大小来决定采样权重的方式,即尽可能采样到TD error大的样本,尽量少采样TD error小的样本,这就是PER的核心思想,这个思想有点类似于2D目标检测中RCNN一文提出的Hard Negative minning(难例挖掘),大概意思就是去采样那些很难区分正负样本的负样本来训练分类器,当训练器都能分类出这种困难样本了,那么训练器性能就很强了。详细可见这个博主总结的:https://blog.csdn.net/qq_36570733/article/details/83444245
换个角度想,TD error大的样本代表着其预测精度还有很大的提升空间,那么这个样本就更需要被学习。
1.3.1、PER的引入
1.3.1.1、均一采样&oracle采样
1.3.1.2、Greedy TD error prioritization采样
1.3.1.3、stochastic prioritization采样
1.3.1.4、引入Importance Sampling Weights,即重要性采样权重IS
1.3.1.1、均一采样&oracle采样
作者用一个简单的例子说明了DQN中无差别均一采样存在很大的提升空间:
如上图所示假设有一个如下图所示的environment,它有n个状态,2个动作,初始状态为1,状态转移如箭头所示,当且仅当沿绿色箭头走的时候会有1的reward,其余情况的reward均为0。那么假如采用随机策略从初始状态开始走n步,我们能够获得有用的信息(reward非0)的可能性为。也就是说,假如我们把各transition都存了起来,然后采用均匀采样来取transition,我们仅有的概率取到有用的信息,这样的话学习效率就会很低。
上图横轴代表experience replay的大小,纵轴表示学习所需的更新次数。黑色的线表示采用均匀采样得到的结果,绿色的线表示每次都选取”最好“的transition的结果。可以看到,这个效率提升是很明显的。
这里说明一下“最好”的含义:从图中可以看出最好的结果来源于即“oracle”曲线,即上帝视角的曲线,意思是:每次需要从经验数据池中选取一个batch的数据,而这个batch的数据通过更新神经网络能最小化当前经验池中所有数据的损失函数(TD error),作者通过穷举每种batch的组合,然后看能将经验池中数据的损失函数下降多少,找到最好的一组batch就是oracle的batch。
oracle曲线和uniform采样结果激励着作者去寻找基于TD误差的优先级采样策略。
1.3.1.2、Greedy TD error prioritization采样
greedy采样策略就是每次对TD进行排序,挑选TD error最大的那个batch进行训练,得到的结果如下所示:
Greedy采样策略存在丧失多样性以及减慢训练速度的问题:
1、1个较小的TD error在第一次被采样之后,以后很长时间内不会被replay。
2、会造成nn的过拟合overfitting。Greedy专注于经验的一小部分(即最大部分),那么训练网络的时候总是会频繁出现那一部分经验,因此对于训练的误差是很小的,看起来很不错。但是一旦用于测试,较小TD error的经验就会使得结果很糟糕,即过拟合现象。
3、每次排序费时费计算资源,样本少的时候自然没问题,那么当池子里样本很多的时候,排序就是个消耗时间、计算能力的事情。
1.3.1.3、stochastic prioritization采样
因此必须引入一种存在于uniform和greedy之间的采样策略。
对于多样性损失问题
作者引入了基于均一采样和贪婪采样策略之间的一种叫stochasitic sampling策略。
优势:保证样本trans被抽取的概率是按优先级单调的,同时保证对于低优先级的trans不会出现0概率抽取可能。
在存储样本的同时,我们会初始化第一个trans的优先级为1,后续的trans的优先级初始化为当前优先级内存中最大的优先级。
,α是随机采样中优先级的影响因子,α=1就代表随机采样完全按照优先级高低来决定采样到的概率大小。α=0就代表随机采样完全不按照优先级来决定采样大小,即退化成均一采样,每个样本一视同仁。
每一个样本trans被采样到的概率为:。在Morvan的代码中,这里是没有α的,其实他是将α放到了网络训练之后优先级的更新上,其实都是一样的。换句话说,如果你在P(i)加了α,那么在更新TD error的优先级就不用加α了。
Morvan的如下:
def batch_update(self, tree_idx, abs_errors): abs_errors += self.epsilon # 这里的ε不是策略的那个 clipped_errors = np.minimum(abs_errors, self.abs_err_upper) # 修剪误差值,使误差值保持在0-1 ps = np.power(clipped_errors, self.alpha) for ti, p in zip(tree_idx, ps): self.tree.update(ti, p)
,这里的ε是为了保证在p=0边缘的trans也能被采样到,避免多样性损失太多。(由于实战采用的这种,另一种变体就不介绍了)
上述代码中将TD误差限定在0-1之间,一方面是为了减小存储空间,避免根节点数值太大,另一方面,为了防止单个样本的TD误差过大,导致对训练的影响过大,也就是降低单个样本的影响,因为比如说你一个batch中一个样本TD error超大,这样会导致其余的样本对网络loss的贡献几乎忽略不计,那么网络参数的更新就会偏向于这个超大error的样本,其余样本就相当于没训练,那么显然这样的训练是失败的。
对于耗时耗资源问题,我们采用SumTree基于一定概率来选择样本,即以较大的概率选择优先级较大的样本, 以较小的概率选择优先级较小的样本。
具体操作:
分区间采样:将根节点的总优先级平均分为batch个区间,在每个区间内调用SumTree(后续介绍)寻找。
采用分区间采样而不是在一个大区间内采样batch次的原因:分区间采样可以保证各个区域都有采样到,且不影响采样偏向优先级大的范围,如果我们在长度为总优先级大小的区间内多次采样,那么很可能采样的结果都聚集在某个样本
1.3.1.4、引入Importance Sampling Weights,即重要性采样权重IS
1、随机更新对期望值的估计依赖于与预期相同的分布相对应的更新。优先重放机制引入了bias,它以一种不受控制的方式改变了这个分布,因此改变收敛结果(即使策略和状态分布是固定的),我们可以通过引入importance-sample weights来弥补,就像给优先采样擦屁股。
2、出于稳定性考虑,作者对IS除以max来使得IS处于0-1之间,根据更新的公式:,使得更新不会波动太大并且缩小了梯度下降更新的幅度:
这就是最终的IS公式。Morvan代码如下:
# n是采样的样本数,在DQN里就是batch
# 返回n个样本的叶子节点的序号,存储的trans,以及计算得到的wj,而以前仅仅只是采样trans
def sample(self, n: int):
b_idx, b_memory, ISWeights = np.empty((n,), dtype=np.int32), \
np.empty((n, self.tree.data[0].size)), \
np.empty((n, 1))
pri_seg = self.tree.total_p / n # 将总的优先级分为n个区间
self.beta = np.min([1., self.beta + self.beta_increment_per_sampling]) # 从最初开始随着采样次数上升慢慢增加
min_prob = np.min(self.tree.tree[-self.tree.capacity:]) / self.tree.total_p # 用于计算ISweight
if min_prob == 0: # 因为min_prob是分母,所以不能让他为0
min_prob = 0.00001
for i in range(n): # 24/6
a, b = pri_seg * i, pri_seg * (i + 1) # 拆解每个区间
v = np.random.uniform(a, b) # 在区间内随机采样一个优先级
idx, p, data = self.tree.get_leaf(v)
prob = p / self.tree.total_p # 样本被采样的概率
ISWeights[i, 0] = np.power(prob/min_prob, -self.beta) # 计算wj,用于loss函数中
b_idx[i], b_memory[i, :] = idx, data
return b_idx, b_memory, ISWeights
3、总结: IS的2大优势
第一大优势:
在典型的强化学习场景中,更新的无偏性在训练结束时接近收敛是最重要的,因为由于策略、状态分布和引导目标的变化,这个过程无论如何都是高度非平稳的。若没有IS修正的优先级采样,其可能会改变收敛结果:如下图所示,紫色为无SI,橘黄色为β=1的全重要性采样修正的优先级采样。
从图中可以看出β=0的无修正可能会导致最终的收敛结果出错,这是由于优先级采样引入的bias导致的。对比体现出IS在消除bias上的贡献。
第二大优势:
当在非线性函数近似下与优先重放相结合时,重要性抽样有另一个好处(例如 深层神经网络):在这里,大的更新步伐(TD error很大的时候)可能是非常破坏性的(如下图所示为DQN中梯度下降更新参数更新公式),因为梯度的一阶近似只在局部是可靠的,并且必须用学习率α来防止。 而在优先级采样中,确保多次看到高错误转换,而IS校正降低了梯度幅度。
4、论文作者通过下图实验说明了IS的影响以及启示了如何设定β:
作者选取了四种游戏进行实验,如上图所示,每幅图中,横轴代表打游戏的盘数,纵轴代表每盘游戏的得分。黄色线代表经过全重要性采样修正的优先级采样,即β=1,黑色显示均一采样(α=0,即不采用优先级采样),紫色为不采用IS进行修正的优先级采样(β=0)。
结论表明:
1、经过全重要性修正的优先级采样收敛的步数和均一采样差不多,未修正的优先级采样收敛速度是他两的4倍(还挺快的!)。
2、经过重要性采样修正的优先级采样相对无修正的优先级采样,其表现没有那么凶猛,起初学习速度相对没有那么快,比如battlezone游戏中,在step40的时候,紫色选手已经在相同时间内已经能拿到最多的分数了,是不是看起来很猛。
3、经过重要性采样修正的优先级采样相对无修正的优先级采样,其过早收敛的风险较小且最终效果更好。
4、经过重要性采样修正的优先级采样几乎在各个step阶段都强于均一采样的分数,其性能更好。
5、综上所述,我们为了避免初始学习较慢,同时为了发挥IS的优势,我可以从一个初始值β0开始,随着采样次数的增多,逐渐增大至1,代码实现:
self.beta = np.min([1., self.beta + self.beta_increment_per_sampling]) # 从最初开始随着采样次数上升慢慢增加
5、β的作用:
1、避免某些TD误差大的样本对迭代的影响过大,所以加一个.-β次方进行缩减,减少影响。
2、β的取值范围是[0,1],取值越小,则不太看重当前的权重,一视同仁,取值越大,则最终的结果越接近与当前权重。实际使用会先取一个0到1之间的值,慢慢向1靠拢。即早期鼓励探索,后期则以实际权重为准。
1.3.2、SumTree
1.3.2.1、什么是SumTree?
1.3.2.2、SumTree的工作过程
1.3.2.1、什么是SumTree?
SumTree的本质是颗二叉树,其分为2部分,一部分是父类结点(包括1个根结点),有capacity-1个(capacity为经验大小),用于存储从叶子节点传上来的样本优先级。另一部分是叶子结点,用于存储池子里各个trans的优先级及与其一一对应的trans值(s,a,r,s',is_done)。
第一部分用一个长度为capacity-1的一维数组(向量)来存储子类优先级之和。第二部分的优先级用长度为capacity的一维数组来存储。
SumTree实现的关键在于:
1、每一个父节点都是2个子节点存储的优先级之和。
2、父节点到2个子节点的搜索公式是:、。
3、子节点到父节点的回溯:。
SumTree一共三种类型:
①:②:③:
如何判断呢?
如上图所示,比如我的经验池大小capacity=10000,显然就是类型②,事实上,类型②的使用次数是最多的。
1.3.2.2、SumTree的工作过程
如上图所示:capacity显示是8,底层一排是叶子节点,每个圆圈代表一个样本,圆圈内的数字是优先级,圆圈底下的范围是采样范围,即当采样数值v在这个闭区间内,那么就采样这个样本。圆圈上的数字是这个样本在数组中的索引值。此外,由于叶子节点还需要存储trans,因此trans需要再创建一个数组,其索引需要和样本一一对应,例如索引为7的优先级对应的样本在trans数组中的索引为0,以此类推。
倒数第二排是存储来自2个子节点优先级之和。然后继续往上走,他们存储的是来自各自子节点的优先级之和。特别的,顶层的根节点存储所有叶子节点的优先级总和,也是我们分区间采样的依据。
接下来用一个具体的例子来介绍为何SumTree是按概率采样的:
首先按照分区间采样,比如说我们的batch是6那么,就会分成[0-6]、[7-13]、[14-20]、[21-27]、[28-34]、[35-41]。(42一般不做采样,代码中实现为开区间),然后我们在每个区间内进行均一采样,比如如棕色线所示,在[14-20]内采样到数据18,SumTree会自上而下搜索,首先会拿v与leftnode作比较,如果比leftnode小,那么父节点就从当前结点索引更新为lefnode索引,如果比leftnode大,则自动更新为rightnode的索引,并将优先级减去lefnode的优先级。如图所示,18<29,故parentnode=1。下一轮迭代开始,做类似的操作,18>13,故自动转移到rightnode,parentnode=4,并且v=18-13=5。继续向下走,5<12,故parentnode=9,检测到已经到底了,故退出搜索,输出索引号9(便于取出trans)、优先级12。
从上面这段分析过程我们可以看出,要想取出index=9的叶子结点,那么刚开始的采样范围要在14-25,其概率在10/42=23.8%之间。同理要想取出index=12那个优先级,那么刚开始采样范围要在31-32,其概率在2/42=4.7%。从这里看出,优先级越高,其采样概率越大,即TD error越大的样本,其被采样到的概率越大。这就是所谓的按概率采样。
Note:
1、SumTree是不可能输出叶子节点优先级p=0的,我们在经验池未存满的时候,往往是将优先级全部初始化为0,然后每存一个trans,就会相应初始化优先级为1,存满之后的存储方式为按照当前最大优先级作为初始化的优先级,具体如下:
def store(self): max_p = np.max(self.tree.tree[-self.tree.capacity:]) # 在capacity个叶子节点中找到最大的优先级(1个样本是1个叶子节点) if max_p == 0: max_p = self.abs_err_upper self.tree.add(max_p) # 存储优先级
需要注意的是:每个样本新存入的时候,其优先级暂时用max_p代替,是不准确的,当采样结束更新优先级的时候,那些新来的优先级才会更新至准确。 那么这里为什么给新来的trans设置最大的优先级呢?这是为了保证所有经验都至少被reply一次。
如果SumTree的输出p=0,那么一定是精度转换出了问题,比如存储优先级的数组是float64,而你存入的优先级是float32,那么存入的时候就会有数据的变化,具体如下(这里读者可以选择不看,这是博主在自己编写过程中出现过的问题,只是记录一下):
,将精度都改为float64后,就不会出现2个子节点之和与父节点不一致的情况了,也就是不会错误的采样到p=0,改正后如下:
2、在经验池为存满的时候,其存放叶子节点优先级的数组,存入区域全是正数,未满区域全是0,是没有负数的。从这里也可以看出,如果你采样出p=0,那么一定是索引到了未满的区域,那么你采样得到的trans一定是空的,这是不对的。
下面是Morvan改写的SumTree函数:
# 输入v是优先级
# 输出叶子节点的索引、对应的优先级、对应的trans的索引
def get_leaf(self, v):
"""
Tree structure and array storage:
Tree index:
0 -> storing priority sum
/ \
1 2
/ \ / \
3 4 5 6 -> storing priority for transitions
Array type for storing:
[0,1,2,3,4,5,6]
24 -> storing priority sum
/ \
13 11
/ \ / \
3 10 8 3 -> storing priority for transitions
"""
parent_idx = 0
while True: # the while loop is faster than the method in the reference code
cl_idx = 2 * parent_idx + 1 # 访问该节点的2个左右子节点
cr_idx = cl_idx + 1
if cl_idx >= len(self.tree): # 搜索到底部了,停止搜索
leaf_idx = parent_idx # 获取叶子节点的index
break
else: # downward search, always search for a higher priority node
if v <= self.tree[cl_idx]:
parent_idx = cl_idx
else:
v -= self.tree[cl_idx]
parent_idx = cr_idx
data_idx = leaf_idx - self.capacity + 1 # 返回每个叶子节点对应存储trans的index
return leaf_idx, self.tree[leaf_idx], data_idx
1.3.3、PER算法
介绍过SumTree后,PER算法算是解决了一大半,接下来正式介绍Prioritized experience reply。
1.3.3.1、伪代码
1.3.3.2、实战算法
1.3.3.1、伪代码
1.3.3.2、实战算法
PER的代码与DQN总体类似,不同点如上面的①②③④所示:
①:采样基于SumTree做优先概率采样,而不是均以采样。
②:除了存储trans,还需要开辟空间存储优先级,且与样本一一对应。
③:损失函数需要添加importance-sampling重要性采样权重来消除优先回放引入的误差。
④:训练结束后不仅要更新网络参数,还要更新每个样本的优先级,因为网络参数更新了,每个样本的TD error自然就要更新一下,这里虽然是更新batch个样本,但是其实还要向上回溯到根节点,其实改变了很多的优先级存储。
Note:其实正是因为①的产生,才会导致后面三项必须也要存在。
1.4、 Dueling DQN
1.4.1、主要思想
1.4.2、DuelingDQN带来的好处
1.4.3、中心化处理
1.4.1、主要思想
目标:优化神经网络结构来优化目标。
1、将Q网络分解成价值函数子网络+优势函数子网络。价值函数子网络输出只关于输入特征(状态)的标量(对不同的状态都有一个值),因为只和状态有关,所以叫V(S);优势函数子网络输出格式和之前的Q一样(对不同状态动作对都有一个值),换个写法叫A(s,a)。
2、Q网络的输出由价格函数网络的输出和优势函数网络的输出线性组合得到:
3、仅仅改变了网络结构,其余均和DQN一样,不要动。
1.4.2、DuelingDQN带来的好处
如上图所示,假设我们将net的输出看成是一张Q表的话,Dueling DQN的网络输出端就是这么计算的。
如上图所示,假如某一次从经验池中取出来的数据只有<s1,s1',r,a0>、<s1,s2',r,a1>,而并没有采样到<s1,s2',r,a2>,根据L = *( rj + * max(Q2(sj';W_)) - Q1(sj,aj;W))^2 (Q2指Target网络,Q1指eval网络),只有A(s1,a0)、A(s1,a1)被训练到了,A(s1,a2)由于没有被采样故不会被训练,这是原本DQN的算法。那么Dueling的改进在哪呢?在A(s,a)的基础上增加V(S)。我们通过训练V(S),即使A(s1,a2)没有被训练,而由于V(S1)的存在,Q(s1,a2)就会有值的更新,这样其实就相当于<s1,s2',r,a2>也被采样了。其实在做RL的时候,往往最花时间的 step 是在跟环境做互动(即采样)。通过这种方式,一定程度上增加了RL的效率。那我这是只举了3个动作,当动作很多的时候,而采样的个数又不充分,那么这种只要一改所有的值就会跟着改的特性,就会是一个比较有效率的方法。
1.4.3、中心化处理
实际上的输出结构并不是像5.5.2中的简单相加,而是,这是为什么呢?
因为有一种情况下,V(S)一直都是0,那么Dueling就和DQN一样了,毫无优势可言,为了避免这种case,我们需要A进行限制。
如何限制呢?
就和上面这个绿色公式一样,将A做中心化处理,就是让A减去他的平均值,这样一来,你会发现,经过中心化后,A的每一行(即某个状态的所有动作值函数)之和为0,如下图所示:
然后你根据上图绿色公式,就会发现V的值就是Q的平均值,也就是说这个平均值加上A,就是Q。
所以假设要让整个一行一起被更新。你就不会想要更新A的某一行,因为你不会想要update A,因为 A 的每一行的和都要是 0,所以你没有办法说,让这边的值,通通都+1,这件事是做不到的。因为它的 constrain 就是你的和永远都是要 0。所以不可以都 +1,这时候就会强迫network去更新 V 的值,然后让你可以用比较有效率的方法,去使用你的 data。
举个栗子:
如上图所示,原来A(s,a) = (7 3 2),先来个中心化->A(s,a) = (3 -1 -2),然后加上V=1,最后的Q就是Q=(4 0 -1)
Note:Dueling DQN的优势仅仅在于网络结构的改进,促成算法效果的优化,其理论支撑其实目前不是很足,当然,因为实践效果不错,所以大家也都直接用了。它没有从理论上解决DQN的不足点,只是从工程上进行了一些优化创新。
二、实战部分
2.1、实战的2个小技巧
2.2、DQN vs DDQN
2.3、DQN vs DQN_PER
2.4、DQN vs Dueling DQN
2.5、DQN vs DDQN_PER_Dueling DQN
2.1、实战的2个小技巧
2.1.1、探索率ε的设置方式
2.1.2、PER中trans的存储以及采样
2.1.1、探索率ε的设置方式
我们都知道,对于model-free的RL任务需要用到ε-greedy擦略,然后逐渐减小ε使得RL逐渐收敛,即。但是ε的初值不是那么好设定,设定太小的话,刚开始缺乏足够的探索也不好,设的太大吧,要很久才到达很小的值,收敛速度就难以保证。因此可以使用一套更加鲁棒的做法:
设定某个区间范围内的ε,比如[0.1,0.5],以及设定目标Episode=100,那么在100个Episode之前,探索率随着Episode计数增加而逐渐减小,一方面符合高探索,另一方面逐渐增加利用的概率。当100个Episode之后,此时探索的差不多了,可以瞬间把ε降到很低如0.001,然后采用之前的方法,继续逐渐减小。
2.1.2、PER中trans的存储以及采样
为什么要采取这种结构呢?一般都会删除第一个trans,而不是一整个episode?
首先前面的Episode本身其TD目标值就不准确,因此没有必要花太多代价去让Q估计值靠近Q目标值,随着trans越来越多,Episode逐渐变多,Q目标值变得准确起来,即后面的Episode价值更高,更需要网络减小TD error。因总之,这种做法更加关注于后面的样本,因为后面的样本更有价值。
2.2、DQN vs DDQN
2.2.1、基本介绍
2.2.2、奖励更改与初始位置更改
2.2.3、实验探究
2.2.1、基本介绍
我采用了gym的CartPole-v0来进行二者的对比试验,模仿DDQN的作者对Atari游戏的探索从而得出过估计曲线,博主也准备这么做。
CartPole游戏就是小车去平衡杆子,让Agent去学习策略从而让杆子平衡。触发终止状态的有三个条件:1、杆子偏路中心角度超过12°。2、小车位子超过2.4(整个屏幕4.8,左右各2.4)。3、超过200步。
原gym设置的奖励是,200步之内,只要没触发前2个条件,每一步都能得1分。那么我们的RL任务目标也就明显了。就是尽可能多的获取更多的游戏得分,想要高分,小车必须坚持下去,也就是平衡住杆子。
状态空间是Box(4, ),即4维的向量,分别是小车位置、小车速度、杆子偏离竖直的角度、杆子的角速度,用一个numpy.ndArray表示。动作空间是Discrete(2),即向左开车和向右开车,用列表[0,1]表示。
小车的初始位置是在[-0.5,0.5]之间的随机位置。
2.2.2、奖励更改与初始位置更改
为了加快小车学习,参考Morvan的奖励更改设置,以中心点和竖直方向为最高奖励,向两边位置和两边偏向分别降低分数。
其次,在论文作者对Atari的实验中,他设置了6个随即种子产生6次试验。这里我进行了简化,对gym源码中初始位置进行更改,改为固定的位置:即中心位置0,偏角度0。表示成state=np.array([0,0,0,0])。
2.2.3、实验探究
以初始位置的Q值为跟踪目标,分别用DQN和DDQN进行1000个Episodes的实验,对每一个Episode的每一步step都记录2个Q值(Q向右,Q向左)。训练结束后,进行测试500个Episode,分别计算每个Epsiode的衰减的Gt,最后进行求取平均值作为初始位置Q值的真实值的无偏估计。
DQN的训练结果:可以看出700个Episode之后,就收敛了,小车可以100%坚持到最后。
DDQN的训练结果:大致都差不多,但是DDQN收到的奖励值比DQN略高。
DQN的测试结果:500个Episode全部平衡住,无一失手!
DDQN的测试结果:无一失手+1
重点来了,现在就是展现DDQN消除DQN产生过估计的问题:
为了看得清晰,从左至右分别是逐渐放大的过程:从最右图可以得出2个结论:
1、DQN确实会产生过估计的问题,比如训练到10万步之后就开始很明显,蓝色曲线的DQN几乎全高于绿色直线的真值。而橘黄色曲线的DDQN在10万步之后一直没有超过其真实值粉红色直线。
2、从DDQN的粉红色直线和DQN的绿色直线对比来看,DDQN得分的能力更强!根据论文作者的结论,这是由于过估计产生的问题。
综上:
DDQN可以消除DQN产生的过估计问题,而且能同时提升Agent学习能力,达到更好的效果。
2.3、DQN vs DQN_PER
2.3.1、基本介绍
2.3.2、奖励更改
2.3.3、实验探究
2.3.1、基本介绍
本次实验室为了探究Prioritized Experience Reply对RL学习的影响。Prioritized Experience Reply(后面简称PER)主要是通过类似于难例挖掘的思想去尽可能挑选TD error较大的样本来训练网络,从而加快收敛的速度。为了检验这一思想,我选用了gym的MoutainCar游戏来探索。
具体环境的介绍请参考我的另一篇:https://blog.csdn.net/MR_kdcon/article/details/109699297中部分。
2.3.2、奖励更改
上图是原来默认条件下:登上山顶为0分,其余位置-1分,小车登山顶的情况,可见收敛效果惨不忍睹,故需要更改一下设置。
1、首先为了加快收敛,我将奖励改成了和小车位置成正比的设置。小车位置越是偏离-0.5(初始位置),奖励越高。
2、为了探究PER难例挖掘来提升收敛的特性,我将终止位置的奖励设成了+10,由于终止位置很少会存入经验池中,故DQN很难加以利用,而PER会有很大概率去利用它,相当于增加了很多次达到终点的经验,减少了探索原来需要的探索时间,就是加快了速度。而该经验奖励本身很诱人很高,故小车会被诱导至终点。
2.3.3、实验探究
这是DQN在MoutainCar环境中的表现,差不多100个episodes之后就开始找到登上山顶的方法了。1800个episodes开始找到最快登上山顶的方法。
这是DQN增加了优先级采样优化在MoutainCar环境中的表现,差不多100个episodes之后就开始找到登上山顶的方法了。1600个episodes开始找到了登上山顶的最快方法。从这里可见PER的优化还是增加了其收敛速度的。
为了进一步更明显说明PER的优势:即善于去采样那些TD error大的样本,增加收敛速度,参考Morvan的做法,在DQN和DQN_PER第一次拿到超大奖励+10开始计时,统计双方在100个episode之内真正到达山顶的次数以及效率(因为还有个时间限制,所以这里引入了真正到达这个说法):
上图是横轴是真正到达山顶的次数,纵轴是到达山顶需要的步数,图中数据是双方在第一次到达之后,再从0开始统计的。可以很明显看出当经验池中增加了使得TD error很大的经验样本之后,PER优化的DQN能很好的利用这个经验,100个episode中,PER率先到达20次,即加快到达山顶的时间,PER增加了收敛的效率。不仅如此,PER的引入还使得收敛次数大大增加,当PER到达20次时,DQN还只能达到5次,PER的收益是显而易见的。
综上:
PER的引入可以增加对TD error较大样本的利用,从而加快梯度下降,加速收敛。
2.4、DQN vs Dueling DQN
2.4.1、基本介绍
2.4.2、奖励更改
2.4.3、实验探究
注意:这里采用的环境是Cartpole-v0,和2.2节的环境是一样的,因此看过2.2节的可以不看2.4.1和2.4.2。
2.4.1、基本介绍
我采用了gym的CartPole-v0来进行二者的对比试验,模仿DDQN的作者对Atari游戏的探索从而得出过估计曲线,博主也准备这么做。
CartPole游戏就是小车去平衡杆子,让Agent去学习策略从而让杆子平衡。触发终止状态的有三个条件:1、杆子偏路中心角度超过12°。2、小车位子超过2.4(整个屏幕4.8,左右各2.4)。3、超过200步。
原gym设置的奖励是,200步之内,只要没触发前2个条件,每一步都能得1分。那么我们的RL任务目标也就明显了。就是尽可能多的获取更多的游戏得分,想要高分,小车必须坚持下去,也就是平衡住杆子。
状态空间是Box(4, ),即4维的向量,分别是小车位置、小车速度、杆子偏离竖直的角度、杆子的角速度,用一个numpy.ndArray表示。动作空间是Discrete(2),即向左开车和向右开车,用列表[0,1]表示。
小车的初始位置是在[-0.5,0.5]之间的随机位置。
2.4.2、奖励更改
为了加快小车学习,参考Morvan的奖励更改设置,以中心点和竖直方向为最高奖励,向两边位置和两边偏向分别降低分数。
2.4.3、实验探究
上图(左)是Dueling网络优化的DQN在CartPole上的表现情况,上图(右)是DQN在CartPole上的表现情况。左图在600个Episodes之后开始收敛,稳定在每次都坚持200步到最后。右图在700步之后才达到收敛。可见Dueling网络可以一定程度上增加收敛速度。
但是呢,Dueling的效果不一定每次都比DQN好,比如在MoutainCar上,Dueling(下图左)的表现就不如DQN(下图右):
MoutainCar本来就是难以收敛的RL任务,故在这里不讨论起收敛速度。只看达到山顶的次数来看,加了Dueling的DQN的效果远不如Nature DQN。
综上:
Dueling DQN通过2个子网络,即价值网络V和优势网络Q来增加探索的样本数,而我们知道探索是最耗时间的,因此Dueling可以一定程度上变相增加探索数量,从而可以加快收敛。
2.5、DDQN_PER_Dueling DQN
最后再来看一下DDQN+PER+Dueling的组合效果吧。DQN三大优化方式最大的优点在于可以任意两两甚至三个组合在一起。但是效果呢不能保证一定是1+1>2,比如下面这是我用三种组合方式在MoutainCar上跑的效果(奖励和上述2.3节类似):
三、总结与展望
3.1、总结
3.2、展望
3.1、总结
①:DDQN可以消除DQN产生的过估计问题,而且能同时提升Agent学习能力,达到更好的效果。
②:PER的引入可以增加对TD error较大样本的利用,从而加快梯度下降,加速收敛。但是由于引入了SumTree,采样需要乡向下搜索,更新需要向上回溯,我们的capacity设置为5W容量,故PER是很消耗程序运行时间的,故同样跑2000个Episodes,PER需要更多的时间。
③:Dueling DQN通过2个子网络,即价值网络V和优势网络Q来增加探索的样本数,而我们知道探索是最耗时间的,因此Dueling可以一定程度上变相增加探索数量,从而可以加快收敛。
④:DQN三大法宝DDQN、PER、Dueling可以互相组合,但效果不能说一定是最好的。
3.2、展望
①:本文介绍了DQN、DDQN、PER、Dueling DQN这四种主流的RL算法,还有一些小技巧比如noisy、rainbow还没用上。此外这几种算法虽然不错,但是无法处理连续动作的RL任务,故对于这类问题,value-based显然无法胜任,故需要求助于Policy-based系列算法。
②:本文用的网路都是FC网络,还可以用更高级的CNN、RNN去替代。
标签:采样,优先级,进阶,样本,PER,DDQN,Pytorch,三大法宝,DQN 来源: https://blog.csdn.net/MR_kdcon/article/details/111245496