【一文弄懂】优先经验回放(PER)论文-算法-代码

22 篇文章 4 订阅
5 篇文章 2 订阅

【一文弄懂】优先经验回放(PER)论文-算法-代码

前言:

这是我replay buffer系列博客的第一篇,PER,接下来会整理HER和另一个叫不出名字的。

ps: 欢迎做强化的同学加群一起学习:
深度强化学习-DRL:799378128

综合评价:

如果算法足够好,没必要用PER.
如果奖励函数足够dense,也没有必要用PER.
因为用了PER速度大大降低,性能不一定会提高。
如果真实场景交互,CPU资源足够,稀疏奖励,可以试试。

继续前言唠叨

去年挖的坑,今年才填上。
最近做了真实机器人实验,发现model-free的探索效率实在是太低了。
而且单个机器人调个参数都花老大劲儿了。
最近半个月都在尝试叠buff,看看能不能把大家常说的HER,PER加上去,让强化学的更快点。

因此上上周调了HER的代码,发现盛名之下,其实难副,首先最原始的her本身就有不少缺陷。在它最擅长的任务中,它还需要花差不多1e6以上的交互回合,才能学会push任务。这和我想象的,一学就会差距太大了。

所以上周三开始捡起去年用了一半的PER,优先经验回放,听说这个也能加速学习,现在整理完了,发现也就那样~

总的来说,目前HER和PER是众多花里胡哨的提升技巧中,最成熟,听起来最make sense的trick了。

但是当时我用的PER是在莫烦的基础改的,莫烦没有实现TD3,我自己在他那个DDPG上改的TD3效果不行(当时没找到错误的原因在哪儿,应该是对TensorFlow理解不够透彻)

因此我需要重写一个基于spinningup风格的PER模块。
并且针对PER,对每个算法(DDPG, TD3,SAC)都写一个RL+PER的类。

前天测试了一下TD3-PER和TD3跑效果。
发现随机性太大!
简直离谱,具体的实验图,放到博客的最后面,有兴趣的可以看看。
下面主要是整理PER的算法和原理。

per论文简述:

参考博客:

(作为学习笔记,很多内容,我就直接复制粘贴了)
借鉴其背景知识和重要性采样— 【DRL-5】Prioritized Experience Replay
借鉴其sumtree部分— 4. [2015] [PER] Prioritized Experience Replay

借鉴其sumtree部分----RL 7.Prioritized Experience Replay经验回放
论文原文链接:
Prioritized Experience Replay

背景知识

之前重用经验(experience,transition转换五元组[s, a, r, s’, d]都是从经验池中均匀采样,忽略了经验的重要程度,文中提到的优先经验回放框架按经验重要性增大其被采样到的概率,希望越重要的经验使用次数越多,从而增加学习效率。
文中应用算法:DQN
效果:相比传统经验池机制,Atari 49游戏中41胜,8负

Prioritized Experience Replay的想法可能来自Prioritized sweeping,这是经典强化学习时代就已经存在的想法了,Sutton那本书上也有说过。

所以Prioritized Experience Replay实际上就是把这个想法应用在DQN上,做了一个“微小的改动”,然而论文莫名的长,性能的提升也相当大。

注意,per最先是用在DQN上的!17年有一个小伙儿用在了DDPG上,测试的任务比较简单,但是测试的超参数比较多,可以作为参考。
我今天要做的是将TD3加上PER,之前也有人做过,但好像也没有放出测试结果。

首先,让我们来回忆一下DQN的Experience Replay是怎么做的,其实无非两点

  • 保持队列的结构,先进先出,当长度到达定值后开始踢人
  • 从整个Buffer中均匀分布随机选择
    然而,在DQN数以千计的样本,也就是 [s, a, r, s’] 中,一定有一些样本可以帮助DQN更快的收敛;或者说,存在一些样本,如果我们先学了这些样本,后面的就不用再学了。

如果我们的样本出现的顺序可以是提前设计好的,那么就一定可以加速训练的过程。

当然,实际上想找到某个“出现顺序”,使得训练速度最快,这涉及到一个复杂的全局搜索,有那工夫还不如直接随机训练呢,但是作者给我们准备了一个例子,用来观察样本次序的重要性。

A MOTIVATING EXAMPLE

对于这种新算法,老喜欢先整一个motivation example了,或者toy study,HER也是。
有了这玩意儿,算法在极端情况下一跑,效果拔群,就会给读者一种make sense的好印象,值得学习。

在这里插入图片描述

上图是一个简单的环境,每一个箭头代表了一次状态转移,它们的概率都是1/2。

也就是说,想得到唯一的奖赏,需要从状态 [n] 走绿色的箭头走到状态 [1]。如果我们从状态 [1] 出发,随机游走,出现这种情况的可能是 [1/(2^n)] 。

可以看出,这是一个reward sparse的问题,上面我跑的结果不明显,可能是因为我没有用sparse奖励?

如果按照DQN的方法来,需要漫长的训练,而如果有一个最优的次序,论文中称为“oracle”,则可以指数级的缩减时间。

然而,问题在于,这只是一个小的toy example,我们可以全局搜索去找这个oracle,然而当我们处理实际问题的时候,如何去找这个次序呢?

methods:PER

这就是Prioritized Experience Replay要解决的问题,它从传统RL那里继承了使用TD-error的想法。

TD-error就是指在时序差分中这个当前Q值和它目标Q值的差值。

比如我们有一个样本 [s, a, r, s’] ,那么它的TD-error应该是

在这里插入图片描述

当然,考虑到DQN的Target Network,应该是

[公式]

再考虑到Double DQN,应该是

在这里插入图片描述

总之,无论你用哪种方法,你都要算出一个 y ,而TD-error就是

[公式]

在DQN中,我们训练的目标就是让 δ \delta δ的期望尽可能小,这样来看,我们根据 δ \delta δ 选择训练次序是有道理的。

当然,实际上我们应该是按照 ∣ δ ∣ |\delta| δ 的大小来选择。

从某种角度,我们可以说TD-error表示某个转移有多么“令人惊讶”或“出乎意料”。它指出了当前的Q值和下一步应该追求的Q值差距有多大。

既然按照 a b s ( δ ) abs(\delta) abs(δ) 选择次序,那最简单的方法显然就是greedy,我们可以维护一个二元堆,实现一个优先队列。

取出的时间复杂度是 O ( 1 ) O(1) O(1),更新的时间复杂度是 O ( n ) O(n) O(n) 。老数据结构了。

但是用绝对的次序看起来不太好,比如

很可能一个 ( s , a ) (s, a) (s,a) 在第一次访问的时候 ∣ δ ∣ |\delta| δ 很小,因此被排到后面,但是之后的网络调整让这个的 ∣ δ ∣ |\delta| δ变得很大,然而我们很难再调整它了(必须把它前面的都调整完)
r r r是随机的时候(也就是环境具有随机性), ∣ δ ∣ |\delta| δ 的波动就很剧烈,这会让训练的过程变得很敏感。
greedy的选择次序很可能让神经网络总是更新某一部分样本,也就是“经验的一个子集”,这可能陷入局部最优,也可能导致过拟合的发生(尤其是在函数近似的过程中)。
总之,我们应该寻找一种更具有随机性的方法。

两种确定优先度方法:

其实这个过渡还是比较自然的,我们有两种方法可以做到这一点。这两种方法都需要下面的计算式
在这里插入图片描述
也就是说,对于Buffer中的每一个样本,我们都计算出一个概率出来,根据这个概率来采样。

proportional prioritization中,我们直接根据 ∣ δ ∣ |\delta| δ 决定概率
[公式]
而rank-based prioritization中,我们根据rank来决定概率
在这里插入图片描述
这个 rank(i) 就是第 i 个样本在全体样本中排在多少位,按照对应的 ∣ δ ∣ |\delta| δ 由大到小排列。
这种方式更具鲁棒性,因为其对异常点不敏感,主要是因为异常点的TD-error过大或过小对rank值没有太大影响

优点: 其重尾性、厚尾性、heavy-tail property保证采样多样性
分层采样使mini-batch的梯度稳定
缺点: 当在稀疏奖励场景想要使用TD-error分布结构时,会造成性能下降
实际上,在作者们的实验中,这两种方法的表现大致相同。

比较
根据文中实验,两种方式效果基本相同,但不同场景可能一个效果很好,一个效果一般般。作者猜想效果相同的原因可能是因为对奖励和TD-error大量使用clip操作,消除了异常值,作者本以为Rank-based更具鲁棒性的。

Overhead is similar to rank-based prioritization.

两者开销相同。

New transitions arrive without a known TD-error, so we put them at maximal priority in order to guarantee that all experience is seen at least once.

新的经验被存入经验池时不需计算TD-error,直接将其设置为当前经验池中最大的TD-error,保证其至少被抽中一次。
既然使用TD-error作为衡量可学习的度量,那么完全可以用贪婪的方式,选取TD-error最大的几个进行学习,但这会有几个问题:

由于只有在经验被重放之后,这个经验的TD-error才被更新,导致初始TD-error比较小的经验长时间不被使用,甚至永远不被使用。
贪婪策略聚焦于一小部分TD-error比较高的经验,当使用值函数近似时,这些经验的TD-error减小速度很慢,导致这些经验被高频重复使用,致使样本缺乏多样性而过拟合。
文中提到使用随机采样方法 stochastic sampling method在贪婪策略与均匀采样之间“差值”来解决上述问题,其实它是一个在样本使用上的trade-off,由超参数[公式]控制

使用优先经验回放还有一个问题是改变了状态的分布,我们知道DQN中引入经验池是为了解决数据相关性,使数据(尽量)独立同分布的问题。但是使用优先经验回放又改变了状态的分布,这样势必会引入偏差bias,对此,文中使用偏差退火——重要性采样结合退火因子,来消除引入的偏差。

在这里插入图片描述

rank的形式没有看到代码实现,也没有看到解析,下面主要讲比例排序的,基于sumtree

sumtree 代码实现

如果每次抽样都需要针对 p 对所有样本排序, 这将会是一件非常消耗计算能力的事. 可以采用更高级的算法——SumTree方法。

SumTree是一种树形结构,每片树叶存储每个样本的优先级P,每个树枝节点只有两个分叉,节点的值是两个分叉的和,所以SumTree的顶端就是所有p的和。正如下面图片(来自Jaromír Janisch), 最下面一层树叶存储样本的p, 叶子上一层最左边的 13 = 3 + 10, 按这个规律相加, 顶层的 root 就是全部p的和了:

在这里插入图片描述
抽样时, 我们会将 p 的总和除以 batch size, 分成 batch size 那么多区间, (n=sum ( p) / batch_size). 如果将所有 node 的 priority 加起来是42的话, 我们如果抽6个样本, 这时的区间拥有的 priority 可能是这样:

[0-7], [7-14], [14-21], [21-28], [28-35], [35-42]

然后在每个区间里随机选取一个数. 比如在第区间 [21-28] 里选到了24, 就按照这个 24 从最顶上的42开始向下搜索. 首先看到最顶上 42 下面有两个 child nodes, 拿着手中的24对比左边的 child 29, 如果 左边的 child 比自己手中的值大, 那我们就走左边这条路, 接着再对比 29 下面的左边那个点 13, 这时, 手中的 24 比 13 大, 那我们就走右边的路, 并且将手中的值根据 13 修改一下, 变成 24-13 = 11. 接着拿着 11 和 13 左下角的 12 比, 结果 12 比 11 大, 那我们就选 12 当做这次选到的 priority, 并且也选择 12 对应的数据。

换个例子,所有的经验回放样本只保存在最下面的叶子节点上面,一个节点一个样本。内部节点不保存样本数据。而叶子节点除了保存数据以外,还要保存该样本的优先级,就是图中的显示的数字。对于内部节点每个节点只保存自己的儿子节点的优先级值之和,如图中内部节点上显示的数字。

这样保存有什么好处呢?主要是方便采样。以上面的树结构为例,根节点是42,如果要采样一个样本,那么我们可以在[0,42]之间做均匀采样,采样到哪个区间,就是哪个样本。比如我们采样到了26, 在(25-29)这个区间,那么就是第四个叶子节点被采样到。而注意到第三个叶子节点优先级最高,是12,它的区间13-25也是最长的,会比其他节点更容易被采样到。

如果要采样两个样本,我们可以在[0,21],[21,42]两个区间做均匀采样,方法和上面采样一个样本类似。

重要性采样:

除了经验回放池,现在我们的Q网络的算法损失函数也有优化,之前我们的损失函数是:

在这里插入图片描述
现在我们新的考虑了样本优先级的损失函数是:

在这里插入图片描述
这里面的区别在于,多了一个 ω j \omega_j ωj

使用了Prioritized Experience Replay之后,样本的分布就被改变了,这可能导致我们的模型收敛到不同的值(把机器学习看作一种密度估计就会很容易理解,当然我没理解…)。

与此同时,我们又不想放弃Prioritized Experience Replay带来的速度提升,那就只能在采样上下功夫。

我们可以使用重要性采样,这样既保证每个样本被选到的概率是不同的(从而提升训练速度),又可以保证它们对梯度下降的影响是相同的(从而保证收敛到相同的结果)。

我们可以设计一个重要性采样的权重 ω j \omega_j ωj
在这里插入图片描述
这里的 N N N就是Buffer里的样本数,而 β \beta β是一个超参数,用来决定你有多大的程度想抵消Prioritized Experience Replay对收敛结果的影响。
如果 β = 1 \beta=1 β=1 ,则代表完全抵消掉影响,这样的话Prioritized Experience Replay和DQN中的Experience Replay就没区别了。

重要性采样ISweight及化简 :

为什么要用重要性采样,原理,可以见我未来的博客…
这个坑先挖着。。。
也可以直接看李宏毅大佬的b站课程-ppo那一节。

这个 ISweight 到底怎么算. 需要提到的一点是, 代码中的计算方法是经过了简化的, 将 paper 中的步骤合并了一些.
比如
prob = p / self.tree.total_p;
ISWeights = np.power(prob/min_prob, -self.beta)
在这里插入图片描述
下面是莫烦的推导, 在paper 中,
I S W e i g h t = w j = ( N ∗ P j ) − b e t a / m a x i ( w i ) ISWeight =w_j= (N*P_j)^{-beta}/max_i(w_i) ISWeight=wj=(NPj)beta/maxi(wi)
里面的 m a x i ( w i ) max_i(w_i) maxi(wi) 是为了 normalize ISWeight,
在这里插入图片描述

这里面的意思是我当前选择的样本是j,我拿到的权重稀疏是 w j w_j wj,但是我要归一化一下,想到的法子是除以所有样本中最大的那个权重样本i,那么用 m a x i ( w i ) max_{i}(w_i) maxi(wi)来表示。
单纯的 importance sampling 就是 ( N ∗ P j ) − b e t a (N*P_j)^{-beta} (NPj)beta,
m a x i ( w i ) = m a x i [ ( N ∗ P i ) − b e t a ] max_i(w_i) = max_i[(N*P_i)^{-beta}] maxi(wi)=maxi[(NPi)beta].

如果将这两个式子合并,
I S W e i g h t = ( N ∗ P j ) − b e t a / m a x i [ ( N ∗ P i ) − b e t a ] ISWeight = (N*P_j)^{-beta} / max_i[(N*P_i)^{-beta} ] ISWeight=(NPj)beta/maxi[(NPi)beta]

而且如果将
m a x i [ ( N ∗ P i ) − b e t a ] max_i[(N*P_i)^{-beta}] maxi[(NPi)beta] 中的 (-beta) 提出来,
这就变成了
[ m i n i ( N ∗ P i ) ] − b e t a [min_i(N*P_i) ] ^ {-beta} [mini(NPi)]beta

看出来了吧, 有的东西可以抵消掉的. 最后
I S W e i g h t = ( P j / m i n i ( P i ) ) − b e t a ISWeight = (P_j / min_i(P_i))^{-beta} ISWeight=(Pj/mini(Pi))beta
这样我们就有了代码中的样子.
或者直接看下面的公式:
在这里插入图片描述

ISWeights计算代码:

因此ISWeights的计算代码是这样的:

def sample(self, n):
        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       # priority segment
        # beta是逐渐增加的,最终为1
        self.beta = np.min([1., self.beta + self.beta_increment_per_sampling])  # max = 1

        min_prob = np.min(self.tree.tree[-self.tree.capacity:]) / self.tree.total_p     # for later calculate ISweight
        for i in range(n):
            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)
            b_idx[i], b_memory[i, :] = idx, data
        return b_idx, b_memory, ISWeights

sumtree代码:

class SumTree(object):
    """
    This SumTree code is a modified version and the original code is from:
    https://github.com/jaara/AI-blog/blob/master/SumTree.py

    Story data with its priority in the tree.
    """
    data_pointer = 0

    def __init__(self, capacity):
        self.capacity = capacity  # for all priority values
        self.tree = np.zeros(2 * capacity - 1)
        # [--------------Parent nodes-------------][-------leaves to recode priority-------]
        #             size: capacity - 1                       size: capacity
        self.data = np.zeros(capacity, dtype=object)  # for all transitions
        # [--------------data frame-------------]
        #             size: capacity

    def add(self, p, data):
        tree_idx = self.data_pointer + self.capacity - 1
        self.data[self.data_pointer] = data  # update data_frame
        self.update(tree_idx, p)  # update tree_frame

        self.data_pointer += 1
        if self.data_pointer >= self.capacity:  # replace when exceed the capacity
            self.data_pointer = 0

    def update(self, tree_idx, p):
        change = p - self.tree[tree_idx]
        self.tree[tree_idx] = p
        # then propagate the change through tree
        while tree_idx != 0:    # this method is faster than the recursive loop in the reference code
            tree_idx = (tree_idx - 1) // 2
            self.tree[tree_idx] += change

    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]
        """
        parent_idx = 0
        while True:     # the while loop is faster than the method in the reference code
            cl_idx = 2 * parent_idx + 1         # this leaf's left and right kids
            cr_idx = cl_idx + 1
            if cl_idx >= len(self.tree):        # reach bottom, end search
                leaf_idx = parent_idx
                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
        return leaf_idx, self.tree[leaf_idx], self.data[data_idx]

    @property
    def total_p(self):
        return self.tree[0]  # the root

因为 SumTree 有特殊的数据结构, 所以两者都能用一个一维 np.array 来存储。

  • capacity 表示这个SumTree能存多少组数据。
  • self.tree = np.zeros(2 * capacity - 1)中,前capacity - 1个节点为非叶子节点,后capacity个为叶子节点。self.data = - np.zeros(capacity, dtype=object)为所有的优先级,共capacity个。
  • add()函数是:当有新 sample 时, 添加进 tree 和 data。
  • get_leaf(self, v)函数是:根据选取的 v 点抽取样本。
    说了这么多这个class怎么用呢?

这个class说白了就是个存储器,用来存取样本,只不过这个存储器Memory长成了一个树的样子。

我们知道,一个样本不仅包含优先级信息,还包括其他的数据。所以我用self.tree.add(maxp, transition)
这句话完成对它的存储,句中max_p为优先级信息,transition为其他的数据。

self.tree存储优先级信息,self.data存储数据信息。
所以,当要采样的时候,我拿一个随机数v,输入到get_leaf(self, v)这个函数里面:

  • 采样得到的样本是:leaf_idx。
  • 这个样本的优先级是:self.tree[leaf_idx]。
  • 这个样本存储的数据是:self.data[data_idx]。
    最后要明确的是每个信息都可以看成是1个3元组:(tree_idx,p优先级,data数据)。

DQN-PER-Memory代码:

我不复制了,太长了,有需要的直接看下面链接就行了。
https://zhuanlan.zhihu.com/p/160186240

TD3-PER代码:

装上gym就可以直接跑的,欢迎点个star~

https://github.com/kaixindelele/DRL-tensorflow/blob/master/td3_sp/TD3_per_class.py

相比普通的TD3,修改的内容有:

1.增加计算abs_error的句柄:
self.abs_errors = tf.abs(min_q_targ - self.q1)
2.更新q值的时候,增加重要性采样的系数
self.q_loss = self.ISWeights * (q1_loss + q2_loss)
3.修改存储transition,因为per_buffer里面,存的格式是列表,不需要拆解transition。
self.replay_buffer.store(transition)
4.修改更新参数的函数,对buffer的采样,数据格式也不一样:
tree_idx, batch_memory, ISWeights = self.replay_buffer.sample(batch_size=batch_size)
但这个其实改的可以很少。

TD3+PER的一些简单测试

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 27
    点赞
  • 179
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
张正友标定法是一种常用的相机标定方法,广泛应用于计算机视觉领域。该方法通过采集一系列已知的三维物体在相机坐标系下的二维投影点,来计算相机内外参数矩阵,从而实现相机的几何校正和测量。 具体步骤如下: 1. 初始化标定板:选择一个特定的标定板,例如棋盘格,然后在每个方格的交叉点上贴上黑白相间的标志。 2. 放置标定板:将标定板放置在计算机视觉系统所见范围内,保证标定板能够在不同角度、位置下被相机观察到。 3. 拍摄标定图像:使用相机对标定板进行拍摄,至少需要12-20幅图像,图像应该包含不同的姿态和视角。 4. 检测标志物:从每个标定图像中提取特征点,通常使用角点检测算法来检测标志物的位置。 5. 计算相机参数:根据提取的特征点,通过最小二乘法来计算相机的内部参数(焦距、主点坐标)和外部参数(旋转矩阵、平移向量)。 6. 优化结果:根据计算得到的相机参数,利用优化算法来进一步提高标定的精度。 7. 验证标定结果:使用标定结果对图像进行校正,并测量标定板上的特征点,通过计算误差指标来验证标定结果的准确性。 总之,张正友标定法通过采集已知物体在相机坐标系下的二维投影点,实现了相机参数的计算和校正,对于计算机视觉中的三维重建、目标检测等任务具有重要意义。掌握这种标定方法可以帮助我们更好地理解相机成像过程,提高图像处理和计算机视觉算法的精度和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hehedadaq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值