前言
理论部分,点击DRL论文阅读(三)查看
本文的代码主要参考了莫烦和Fisher’s的代码,在他们的基础之上,修改为自己习惯能看懂的代码
Prioritized DQN不同于DQN/DDQN的是,它关注的是经验回放池中那些很少但有用的正面信息
一、实验环境
使用MountainCar环境来对算法进行测试
和之前有所不同的是,这次我们不需要重度改变它的reward。所以只要没有拿到小旗子,reward = -1,拿到小旗子时,定义reward = 10。比起之前的DQN,这个reward定义更加准确。如果使用这种reward定义方式,可以想象Nature DQN会花很久的时间学习,因为记忆库中只有很少很少的+10 的reward可以学习,正负样本不一样,而使用Prioritized DQN,就会重视这种少量的,但值得学习的样本。
二、Prioritized Experience Replay
伪代码
在该算法中,我们batch抽样时,并不是随机抽样,而是按照Memory中的样本的优先级来抽,这样能更有效的找到我们需要学习的样本。
SumTree
由于使用贪婪法来选取优先经验的时间复杂度太高,同时还有其他问题,所以我们用$P(i) = \frac{p_i^\alpha}{\sum_k p_k^\alpha}$来定义某个片段的选取概率,其中我们采用比例优先的方式来计算$p_i$,即$p_i = |\delta_i| + \epsilon$,$\delta_i$为TD-Error,$\epsilon$为一个很小的正数,为了避免TD-Error为0的特殊边缘例子也能被采样到,并用SumTree这种数据结构来表示每个存储的片段。
SumTree是一种二叉树类型的数据结构,所有叶子节点存储优先级$p_i$,所有父节点为子节点之和,所以这棵树的根节点为所有叶子节点的和,如下图所示:
抽样时,我们会将$p_i$的总和除以batch_size,分成batch_size个区间,如上图例子所示,一共有3、10、12、4、1、2、8、2
等八个优先级节点$p_i$,如果设置batch_size为6,则会分成6个区间,每个区间为:[0-7],[7-14],[14-21],[21-28],[28-35],[35-41]
,在分别在这6个区间中均匀的随机选取一个数,从根节点依次往下搜索。
如果在第4个区间[21-28]抽取到了24,则将24与根节点的左节点进行比较,因为24 < 29,所以继续向左搜索,将24与29的左节点比较,发现24 > 13,则继续向右搜索,同时 24 - 13 = 11。将11与16的左节点比较,11 < 12,因为12已经是叶子节点,则搜索完毕,选择12这个优先级。
图中叶子节点下面的括号中的区间表示该优先级可以被搜索到的范围,由此可见优先级大的被搜索到的概率就高,同时优先级小的,也有一定的概率被选中。
三、代码部分
没有按照文中,与Double DQN结合,而是与Nature DQN相结合
若想要看全部代码,直接查看所有代码
3.1 代码组成
代码由两部分组成,分别为prioritized.py 和run_MountainCar.py
(1)prioritized.py
这个代码中主要包含三个类:SumTree、Memory(prioritized)、DQNPrioritizedReplay
后面又重新添加了Double DQN的prioritized.py,这里将不对这个进行说明,毕竟代码改动很少
3.2 网络-prioritized.py
3.2.1 SumTree有效抽样
1 | class SumTree(object): |
3.2.2 Memory(Prioritized的存储,DQN不采用)
1 | class Memory(object): |
在伪代码中,重要性采样的权重
$w_j = \frac{(N P(j))^{-\beta}}{max_i(w_i)} = \frac{(N P(j))^{-\beta}}{max_i((N * P(i))^{-\beta})} = \frac{(P(j))^{-\beta}}{max_i((P(i))^{-\beta})} = (\frac {p_j}{min_iP(i)})^{-\beta}$
且伪代码中的第十二行赋值实际是$p_j = (|\delta_i| + \epsilon)^\alpha $,如果采用rank-based,则为$rank(i)^{-\alpha}$,我们的代码中$\alpha$设置为了1,因此才会是
1 | td_error += 0.01 |
去更新TD-Error
在莫烦的代码中,$\alpha$设置的为0.6,因此更新为这样
1 | def batch_update(self, tree_idx, abs_errors): |
3.3.3 DQNPrioritizedReplay训练
只说明添加了Prioritized之后的代码部分
1 | class DQNPrioritizedReplay: |
这里的Prioritized DQN和Nature DQN的experience存储方式不同。
但突然想到了一个问题:
Nature DQN是在Memory里面有很多个数据,从里面去均匀随机抽取
而Prioritized DQN好像只存储了mini-batch个数据,并且对这些数据进行替换更新,但是在更新过程中,会不会存在将重要的数据给替换掉了呢?如这重要数据就是正面奖励的数据,而用负面奖励数据去替换掉了?
四、结果显示
在训练的时候,将env.render()注释掉,首先训练过程会很快
其次是没有注释时,小车好像很难到达旗子那里去,不知道env.render()的影响为什么这么大
下面的图片结果都是在注释掉的情况下训练产生的,没有注释时,时间太长,且一直找不到旗子
注释下,几分钟就可以完成训练(CPU)
而没注释,20分钟了可能5个episode都没有完成
(1)DQN下的训练
某一次训练
另一次训练结果
(2)Double DQN下的训练
没有加seed
某一次训练
某一次训练
但是在输出中,步数是不止这么一点的,还不太清楚是为什么没有达到6w步(显示),但是能看出Prioritized 在找到一次正面奖励之后,还是比DQN要训练的快的。
1 | #======================== Double DQN ====================================== |
从图中可以看出,我们都从两种方法最初拿到第一个R += 10
奖励的时候算起,看看经历过一次R += 10
后,他们有没有好好利用这次的奖励,可以看出,有Prioritized replay的可以高效利用这些不常拿到的奖励,并好好学习他们。所以 Prioritized replay会更快结束每个episode,很快就到达了小旗子。
总结
对代码中seed函数不是太能理解,反正是对随机数产生变化的函数