当前位置:   article > 正文

深度强化学习库的设计思想带你深入了解DRL:从环境、网络更新、经验池、经验池、算法基类分离度、分布式、多进程等方面评价

深度强化学习

一个最基本的深度强化学习训练流程 pipeline 应该是这样的:

  1. 初始化环境、网络、经验池
  2. 在环境中探索,并把数据存入经验池
  3. 从经验池中取出数据,更新网络参数
  4. 对训练得到的策略进行评估,循环 2、3、4 步
# initialization
env    = BuildEnv()
actor  = PolicyNetwork()
critic = ValueNetwork() 
buffer = ExperimenceRelayBuffer()

# training loop
for i in range(training_episode):

    # explore in env
    state = env.reset()
    for _ in range(max_step):
        next_state, reward, done, info_dict = env.step(action)
        buffer.append((state, reward, done, next_state))  # transition
        state = next_state
        if done:
            break

    # update network parameters
    for _ in range(...):
        batch_data = buffer.random_sample()
        Q_label    = ...
        critic_object = critic_loss = cirterion(Q_label, critic(...))  # loss function
        actor_object  = Q_value_est = critic(state, actor(...))   # Q value estimation
        Optimizer(network_parameters, object, ...).backward()

    # evaluate the policy (NOT necessary)
    if i % time_gap == 0:
        episode_return = evaluate(env, actor)
    
    if stop_training:
        break

save_model(network_parameters)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

大部分深度强化学习 DRL 算法(主要是策略梯度 policy gradient、Actor-Critic Methods)可以抽象成上面这种 **DDPG-style RL training pipeline。**它的可拓展性非常好,且方便拓展,与稳定训练。

大部分 DRL 算法,指的是 Off-policy 的 DDPG、TD3、SAC 等,以及 On-policy 的 A3C、PPO 等 及其变体。大部分算法的区别只在于:计算 Q 值、探索环境罢了。如果是 DQN 类的,那么只需要把 actor 看成是 arg max (Q1, …, Qn),critic 看成是 Q Network 即可。

在下文,DRL、state、action、reward、policy 等强化学习术语,会故意使用英语去表述,方便区分。适合阅读此这篇文章的人群:同时入门了深度学习、强化学习、深度学习框架、深度强化学习算法库设计、多进程、Python、Numpy。如果你在阅读中发现了更好的改进方案,请不要急于评论,如果这个 “改进方案” 很简单,而我没有使用,很大概率是因为这个方案与其他结构有冲突、没有提升、或者实际上很难实现。**若你有强化学习算法库、分布式深度学习算法的设计经验,请在评论时主动说明:**即便你们的意见与我相左,我也会仔细思考。

下面讲的一切,都是开源项目「深度强化学习算法库:小雅 ElegantRL」的设计理念:

  • 「小」轻量 lightweight,只需要 3 个 Python 文件,只需要安装 PyTorch、Numpy
  • 「雅」优雅 elegant,代码耦合程度极低,可以方便地进行修改
  • 在「小雅」的限制下,我会尽可能地提升算法的稳定性与训练速度,完整功能的代码都在 BetaWarning 文件夹内,测试没有 Bug 后自然会更新。我的榜样是 TD3 作者的优雅代码

因为竞争不过伯克利的 Rllib ray-project 这个 2020 年性能最好的开源 DRL 库,所以我只能避开它的生态位。Rllib ray 为了达到极致性能,它的代码变得复杂,学习成本很高,需要安装全家桶才能使用(除此以外全是优点)。如果你用不了 Rllib ray,那么你才需要考虑使用「小雅 ElegantRL」。更多讨论参见本页面的**「如何评价一个深度强化学习算法库的好坏?」**

1.算法基类的可拓展性

class AgentBaseAC:
    def __init__():
    def update_buffer():
    def update_policy():
    def select_action():
    def save_or_load_model():

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下面对深度强化学习进行分类,并举出特例,这是为了解释抽象深度学习算法框架的合理性。加入新算法时,只需要继承 AgentBaseAC 这个基类,做出尽可能少的修改即可。只要遵守编写规范,新算法可以随意地切换到多进程,多 GPU 训练模式而不用修改代码。

1.1 算法基类:将「探索环境」与「更新参数」这两个步骤分开

任何 DRL 算法都有这两个步骤,将它们分开非常重要:

def update_buffer():  # 在环境中探索,并把数据存入经验池
def update_policy():  # 从经验池中取出数据,更新网络参数

# ↓ 稳定,高效
for _ in range():
    update_buffer()
for _ in range():
    update_policy()

# ↓ 即不稳定,又不高效
for _ in range():
    update_buffer()
    update_policy()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

一些代码没有讲这两个步骤分开,而是在环境中探索一步后,马上更新一次参数。下面截图来自莫烦的 DDPG TensorFlow 代码,line243 是探索环境的步骤,line255 是更新网络的步骤,入门时没问题,实际使用时就别这样用。

两个步骤分开的优点:

  • 稳定:往 Replay 里加入新东西会改变数据分布,将两个步骤分开后,随机抽样更稳定,更加适合使用「动量」更新的优化器。
  • 高效:update_buffer 需要与环境交互,用 CPU 计算 env.step,用 CPU 或 GPU 计算 policy function。update_policy 整个过程移到 GPU 里更高效。
  • 代码复用:很多 DRL 算法的 update_buffer 相同,只有 update_policy 不同。
  • 多进程兼容:分开后,update_buffer 可以并行地与环境交互并更新 replay buffer,update_policy 可以并行地计算梯度并更新参数。

1.2 算法基类:将「选择动作」独立出来

很多 DRL 算法都将「选择动作」独立出来:

def select_action(state):
    ...
    return action

  • 1
  • 2
  • 3
  • 4

深度强化学习可分为 确定策略梯度 Deterministic PG 与 随机策略梯度 Stochastic PG。从工程实现的角度看:它们探索环境的方式不同。确定策略会为 action 添加一个由人类指定的高斯噪声,随机策略会让 policy network 为 action 输出一个用于探索的 noise。此外,DQN 经常使用 epsilon-Greedy 作为作为探索手段,Noisy DQN 把 noise 挪到激活函数之前与 SAC 神似。因此,**不同的 DRL 算法有不同的 select_action。**因此在编写强化学习库时,我们经常会将 select_action 这个动作从 update_buffer 中抽离出来,避免太大改动。

随机策略会让训练 network 为 action 输出一个用于探索的 noise,特例: 随机策略 PPO 的 action noise std 是一个 trainable parameter,而不是由 policy network 输出的。我们当然可以修改 PPO 让它也像 SAC 一样 “由网络输出 action std”,但是这样会影响 PPO 的生态位,有时间再详细讲。

参见细节:pytorch 要怎么高效地将进行 numpy 与 tensor,CPU 与 GPU 数据的转换。TODO 还没写

1.3 算法基类:保存或加载模型

事实上,在深度强化学习中,我们需要时常地保存模型参数,因为 DRL 没有很好的判断过拟合的方法。因此我特地将「保存或加载模型」这个方法写在算法基类中。

在有监督的深度学习中,我们可以将数据集划分为训练集、验证集、测试集。我们在训练集上训练,看到验证集的损失上升时,就停止训练,记下此时的超参数。如下图,虽然它的横轴是容量,但是改成训练步数等其他超参数也可以。

在深度强化学习中,我们并没有训练集、测试集之分。我们在仿真环境(或者真实环境)中训练智能体,会得到一个分数(累计回报 episode return)。将这个分布画出来,就得到学习曲线 learning curve。我们用这个曲线来判断何时终止训练。DRL 算法并不是训练时间越长,得分越高,我们可以保存整个训练过程中,得分最高的策略模型。有时候环境过于复杂,重置环境后同一个策略会得到不同的分数,所以我们要多测几次让分数有统计意义(请注意,下面的折线图上每个点已经是多测几次得到的结果)。

像 OpenAI baseline 以及 hill-a/stable-baselines 缺少自动绘制 learning curve 辅助判断模型合适终止的模块,因此需要使用者自己去编写。

2.经验池 经验回放 Experience Replay Buffer


任何深度强化学习算法都需要 Replay,因为深度学习(神经网络)一定要稳定的数据才能训练。而将训练数据保存到 Buffer 里,然后随机抽样是让数据接近独立同分布的好方法。一个成熟的强化学习库一定会在这方面下功夫:复杂的环境需要管理大容量的 Buffer,并且整个训练流程有 Buffer 参与的部分都是高 IO 的操作。

class BufferArray:
    def __init__():
    def append_memo():  # 保存单个 environment transition (s, a, r, next state)
    def extend_memo():  # 保存多个 多进程时,批量收集数据时用到
    def random_sample():  # 随机采样,off-policy会用到(on-policy算法也用,但不在这里用)
    def all_sample():     # 取出全部数据,on-policy算法会用到

    def update__now_len__before_sample():  # 更新指针(高性能Buffer)
    def empty_memories__before_explore():  # 清空所有记忆,on-policy算法会用到

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

经过试验,将训练数据 buffer 放在连续内存上明显更快,对于任何 DRL 算法都是如此,所以我抛弃了 list 结构,转而在使用前创建一块内存用于存放 array,因此才会用到「更新指针」的这类操作。Replay Buffer 对整个 DRL 库的性能影响实在太大了,以至于我在这里为了性能牺牲了优雅。

深度强化学习可分为 异策略 off-policy 与 同策略 on-policy。从工程实现的角度看:它们的 Experimence Replay Buffer 的管理方式不同。异策略的 Replay Buffer 里面可以存放来自于不同策略的 状态转移数据 state transition (s, a, r, mask, next state),如果 agent 的探索能力强,那么 Buffer 里面的数据多样性有保证,且不需要删除,直到达到内存的极限。同策略的 Buffer 里面只能存放一种策略探索得到的数据,这种策略与它需要更新的策略相同。它需要在新一轮探索开始前,将更新参数时用过的数据删除掉。

mask 是什么?参见「合并 终止状态 done 与 折扣因子 gamma」

(s, a, r, mask, next state),例外: 使用 轨迹 trajectory 进行更新的算法,可以不保存 next state,转而保存当前动作的 Log_prob 或者用于计算出 log_prob 的 noise 将用过的数据删除掉,例外: 作为 On-policy 的 PPO 算法当然要删除用过的训练数据,但是 OpenAI 基于 PPG 改进得到的 PPG (PP Gradient)算法是一种能利用 off-policy 数据的 On-policy 算法。

深度强化学习可分为 以 state1D 或者 以 state2D 为输入。任何 state 都可以 flatten 成 1D,因此在设计 Buffer 的时候,我可以将完整的 state transition( state1D,reward,action1D)保存在一块连续的内存上,而不需要将它们分开保存,从而在 random sample 时极大地加快训练速度。以 state2D 为输入的,如 雅达利游戏 Atatri Game 需要以画面截图作为输入 pixel-level state,有时候需要堆叠连续的几帧画面(视频),我们都直接把这些数据 flatten 成一维的向量,统一保存,使用的时候再使用 reshape 进行复原。

基于以上两点,我建议:

  • 无论数据如何,全部都 reshape 成一维,然后统一保存在一块连续的内存上(或者直接保存在显存内),并且使用指针。训练时一定要保存到显存内,方便快速地使用随机抽样。
  • 更新异策略时,保存 (s, a, r, mask, next state)。因为要进行随机抽样,所以一定要保存 next state。
  • 更新同策略时,以 trajectory 形式 按顺序保存 (s, a, r, mask, noise)。每个 state 的下一行就是 next state,因此不需要保存 next state。noise 用于计算新旧策略的熵。

3.强化学习与深度学习的区别


[深度学习和强化学习之间的差别有多大? - 曾伊言的回答]这里回答了理论上的差异。我这里从高性能计算的角度讲一下她们的区别:

有监督的深度学习(如:在 ImagNet 上使用监督数据训练分类器)。这个过程天生适合分布式,不同 GPU(或设备)之间可以只传递梯度(中心 或者 环式),可以用多 CPU 加快数据读取:

  1. 从磁盘中读取数据,存放到内存(可使用多进程加速,CPU workers)
  2. 对数据进行预处理,并传入 GPU 的显存
  3. random sample,在 GPU 里计算梯度,更新网络参数
  4. 循环以上内容,定时地保存 check point
  5. 适时地终止训练,避免过拟合

深度强化学习(如:DDPG-style training pipeline)。DRL 难以套用有监督 DL 的多进程加速方案,他们只有加粗的 2、3 步骤相同。在 DL 里,数据可以提前准备好,而 DRL 的数据需要与环境交互产生,并且需要严格限制交互次数。在 DL 里,我可以用训练的副产物 loss function 帮助我判断何时可以终止训练,避免过拟合,而 DRL 没有判断过拟合的机制,因此一定需要绘制出 学习曲线 帮助我们决定 “何时终止训练” 与“保存哪个策略”。

  1. agent 与环境交互,得到的零碎数据存放在内存中(一般是 CPU,或者再加上 GPU)
  2. 将数据输入传入 GPU 的显存中
  3. random sample,在 GPU 里计算梯度,更新网络参数
  4. 对策略进行评估,绘制学习曲线,保存得分高的
  5. 观察学习曲线,若分数不能更高,则终止训练

DRL 的数据需要与环境交互产生,例外: 小盘的金融 DRL 可以进行 “有监督的训练”,她的训练数据可以是提前收集的市场序列,不需要交互产生。尽管如此,实际使用时,为了追求时效性,金融 DRL 的训练数据应该是几分钟、几秒前刚刚从市场收集到的数据,也无法像 DL 一样“事先” 读取到内存或显存里。 需要严格限制交互次数: 在有监督的 DL 里,没有交互这个概念(注意,深度学习的 GAN 是强化学习的一种,参见联系对抗网络和强化学习的 AC 框架 - 论文的阅读与翻译)。在 DRL 里,交互次数少有重要意义,“少” 意味着我不用在现实世界中用坏掉那么多机器人,不需要租多核心的服务器跑那么久仿真,能更快用刚获得的金融市场数据训练出好的交易策略,等等。

4.稳定训练

为了稳定训练,我们将训练流程分为三部分:「探索环境」、「更新参数」以及「评估模型」

前面提及的:将「探索环境」与「更新参数」这两个步骤分开,不仅能方便多进程的 DRL 训练,更重要的意义是:给「更新参数」步骤创造了数据稳定的训练环境,并且可以灵活调整数据复用次数,尽可能避免过拟合,从而稳定了 DRL 的训练。众所周知,像对抗网络、策略梯度这种双层优化结构的训练不稳定。为了追求训练快速而舍弃泛化和稳定是不可取的

「数据复用次数」详见深度强化学习调参技巧:以 D3QN、TD3、PPO、SAC 算法为例 的 off-policy【批次大小、更新次数】以及 on-policy【数据复用次数】。 「双层优化」详见 从双层优化视角理解对抗网络 GAN联系对抗网络和强化学习的 AC 框架 - 论文的阅读与翻译

我们还将「评估模型」也从独立出来。由于 DRL 并非训练越久模型越好,只有在环境简单,算法给力、调参充分(甚至是精心挑选)的情况下才能得到那种漂亮的学习曲线。评估模型可以帮助我们修改训练环境,调整 DRL 超参数。评估模型可以帮助我们修改训练环境,调整 DRL 超参数,很多 DRL 库没有这个极其重要的部分。

「学习曲线」learning curve,详见如何选择深度强化学习算法?MuZero/SAC/PPO/TD3/DDPG/DQN / 等 的【好的算法的学习曲线应该是?】

「更新参数」DRL 库的设计原则是:绝不阻塞主进程。因此我们将「探索环境」与「评估模型」分了出去。「更新参数」与「探索环境」两个进程会轮流使用 GPU,绝不让 GPU 闲着。

「探索环境」进程会把探索得到的数据通过管道发送给「更新参数」进程,为了降低 GPU 的空闲率,我们采用了一种新的采样模式,详见「双 - CPU 群 - 单 - GPU」。

「评估模型」是比较独立的进程,它将会利用最零碎的资源去完成记录任务,优先级最低。例如主进程会把需要评估的模型(actor network)发送给它,暂存在队列里。如果它来不及评估这个模型,而主进程又发来一个新的模型,那么它会在上一次的评估结束后,直接读取最新的模型:主进程不需要等待它,有评估任务它就做,没有任务它就等,并且它只使用 CPU,绝不占用宝贵的 GPU 资源。它还负责保存模型到硬盘、记录训练的临时变量的折线图,有助于在训练崩溃时定位错误、在复盘的时候调整超参数。。可以被监视的部分临时变量:

  • 智能体在环境中每轮训练的步数(均值、方差)
  • ReplayBuffer 内记忆的数量
  • DQN 类、Actor-critic 类:objectives of Q Network/Critic/Actor、Q 值
  • TD3:TwinCritc 的两个 Q 值差值的方差,动作与最近边界的距离
  • PPO:搜索得到的动作噪声方差,新旧策略的差异,clip 前后两个 objective 差值的方差
  • SAC:策略的熵,温度系数 alpha 的值,动作与最近边界的距离

即便绘制折线图不会影响到主进程的训练,但是从主进程采集并传输临时变量依然会拖慢训练速度。因此小雅默认只画出 objectives of Critic/Actor 这两个几乎不耗费时间就能统计的变量,有特别需要请自行删改。必要的时候,可以平滑折线,用来得到可视化程度高的结果。尽管使用 TensorBoard 可以很好地完成这些功能,但我因为自己用 matplotlib 实现简单,更快,更轻量,而坚持不用 TensorBoard。

对 DRL 的训练影响最大的两个因素:DRL 库的代码决定训练的上限、以及 DRL 库的使用者决定了训练的下限。尽管 DRL 库设计者可以不考虑这一部分,但我还是为此写了两篇文章:

强化学习:小雅」这个库超参数的默认值会优先保证稳定训练,值得使用者参考。

**【孪生估值网络】**Twin Critics。DoubleDQN、TD3 和 SAC 都主动地使用了 Twin Critics,事实上所有需要估算 Q 值期望的算法都能使用这个很好用的技巧(PPO 这一类本就稳定的算法不需要用 TwinCritics),尽管它可能略微增长训练时间,但是它能显著地稳定训练。然而,大家的实现略有差别。请注意下面三种计算 策略梯度(actor_objective)的方法。第一种是 TD3.2018 的方法,第二种是稳定性和计算量居中的方法,而我使用最后一种,它计算量稍大,但最稳定

criterion = nn.MSE() or nn.SmoothL1() or ...

q_label          = reward + min(Critic1(s, a), Critic2(s, a)).deatch()
critic_objective = criterion(q_label, Critic1(s, a)) + criterion(q_label, Critic2(s, a))

a                = Actor(s)
actor_objective  = Critic1(s, a)
actor_objective  = Critic1(s, a) + Critic2(s, a)
actor_objective  = min(Critic1(s, a), Critic2(s, a))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

估值网络的参数共享 TODO 2021-1-26 16:48:28 :没必要让两个网络有不同;两个网络也可以最大限度地共享参数,没必要独立地创建两个网络。

【信赖系数】trust coefficient。这个技巧是我在 2019 年底偶然想到的,它可以用于任何双层优化问题,如 Generator-Critic (Discriminator)、Actor-Critic。它会计算 Critic 的 loss function value,如果这个 value 很小,证明 Critic 对 Q 值的估计很准确,它提供的梯度值得我们信赖,因此我们可以用更大的步长去更新 Actor 网络。只要把 loss_critic 用一个单调递减函数映射到 (0, 1) 区间就能得到信赖系数。在软更新后得到稳定的数值,就可以用信赖系数去调整学习率了。

**【改进温度系数】**我们对 alpha 的初始值进行调整,减小了预热时间。详见 The alpha loss calculating of SAC is different from other repo · Issue #10 · Yonv1943/ElegantRL

**【log 形式的张量】**我们不直接优化 alpha,而是优化它的 log 形式,因为它在目标函数中位于系数位置。同理,我们不直接输出 动作的方差,而是输出动作方差的 log 形式,为了在极大值,极小值处的调整也能保持平缓。对 log 形式的张量,我们需要裁减:

self.log_std_min = -20
self.log_std_max = 2

a_std_log = self.net__std_log(x).clamp(self.log_std_min, self.log_std_max)  # PPO
a_std_log = self.dec_d(a_).clamp(self.log_std_min, self.log_std_max)        # SAC

# SAC alpha_log
self.alpha_optimizer.step()
with torch.no_grad():
    self.alpha_log[:] = self.alpha_log.clamp(-16, 2)  # todo (-16, 1)
alpha = self.alpha_log.exp().detach()

# 系数(倍数)的范围
exp(-20) = 2e-9   # min
exp(-16) = 1e-7   # min
exp(  1) = 2.718  # max
exp(  2) = 7.389  # max

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5.env 修改建议

下面是一个勉强可以在 2020 年最好的 DRL 库 RLlib ray-project 以及比 OpenAI baselines 稳定一点点的 stable-baselines 使用的环境。如果你想要使用 RLlib ray 的多进程、分布式计算等其他功能,你需要自行阅读他们官网的 Document、和 Github 的 Demo。

import gym
class DemoGymEnv(gym.Env):
    def __init__(self):
        self.observation_space = gym.spaces.Box(low=-np.inf, high=+np.inf, shape=(4,))  # state_dim = 4
        self.action_space = gym.spaces.Box(low=-1, high=1, shape=(2,))  # action_dim = 2

    def reset(self):
        state = rd.randn(4)  # for example
        return state

    def step(self, action):
        state = rd.randn(4)  # for example
        reward = 1
        done = False
        return state, reward, done, dict()  # gym.Env requires it to be a dict, even an empty dict

    def render(self, mode='human'):
        pass

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

一些知名的 DRL 库需要 gym 只是为了规范化 state 和 action,比如 gym.spaces.Box。env 的建立,不需要 OpenAI 的 gym 库,只要我们能告诉 DRL 库:state_dim 和 action_dim 这些信息。「强化学习:小雅」并不逼迫使用者安装 gym 库,我只想把这些东西弄简单。一个合格的环境只需要有 reset、step 这两个方法,没那么复杂,然后直接在 init 里写上环境的信息,方便 DRL 库根据具体任务创建合适的网络。 target_reward 是目标分数,达到了目标分数后就视为通关了,不清楚的情况下可以随便写一个数值。

class DemoEnv: 
    def __init__(self,):
        self.env_name = 'DemoEnv-v1'
        self.state_dim = int(...)
        self.action_dim = int(...)
        self.if_discrete = bool(...)
        self.target_reward = float(...)

    def reset(self):
        ...
        return state

    def step(self, actions):
        ...
        return state, reward, done, None

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

编写自定义的训练环境,请参考 深度强化学习调参技巧:以 D3QN、TD3、PPO、SAC 算法为例 的【训练环境怎么写?循序渐进,三个阶段】。

5.1「探索环境」的多进程 rollout 方案:「双 - CPU 群 - 单 - GPU

  • 每一步探索都会产生 (state, action, reward, others),状态转移数组 transition tuple
  • 他们可以按时间顺序组成轨迹 trajectory。在 model-free RL 里,ReplayBuffer 里面的这些 “记忆” 可以用来描绘「策略」与「环境」的互动关系。

下面讨论「探索环境」的进程要如何从环境中收集这些 trajectory 并存放到 ReplayBuffer 内,再提供给神经网络去训练。

actor network 用 CPU 运行还是 GPU? 我选择用 GPU

方案 1. 用单个进程运行 env 和 actor,探索出 trajectory 后,统一收集到 ReplayBuffer。方案 1.1 如果 actor 用 CPU,那么 state 和 action 不需要在内存和显存间传输,在网络很小的情况下(3 层,256 宽),运行速度最快,可惜 CPU 占用率非常高,而且 GPU 闲着。方案 1.2 如果 act 用 GPU,那么无论网络如何,它的运行速度也都快,可惜开多个这样的线程会很占用显存,导致 GPU 利用率低。

用多个进程运行多个 env,合成一个 batch 后,传给 actor,多个 env 一齐运行一步,就将状态转移数组收集到 ReplayBuffer。方案 2.1 如果 actor 用 CPU,那么 GPU 闲着。方案 2.2 如果 actor 用 GPU,那么在等待凑满 batch 期间,GPU 闲着,而 GPU 的 actor 计算 action 时,CPU 的 env 闲着。

刚入门的人都很容易想到这四种简单方法,他们都有明显缺点。我分析:

  • 尽管 batch size=1 很容易把设备的使用率提高到 100%,但是这种计算方式非常低效,决不能采用。因此我选择合成一个 batch 的方案。
  • 为了不让 GPU 闲着,我选择让 actor 在 GPU 中运行。
  • CPU 无法用半精度加速(甚至 float64 改成 float32 也不能加速,只能减少内存使用),而 GPU 可以加速。(强化学习需要快速拟合而不能用深层网络 > 10,和半精度很配)

其他很简单但是我没有提的方案,并非不到,而是性能差,不值一提。

双 - CPU 群 - 单 - GPU」经过分析后,我选择改进方案 2.2。解决它的等待问题:很容易,我只要将运行了 env 的 CPU 进程分成两群。CPU 群 1 的 env.step 运行完成后,GPU 的 actor 马上处理这一 batch,然后返回 actions 给 CPU 群 1 的 env。此时 CPU 群 2 的 env.step 也准备好了,GPU 不需要等待即可马上返回 actions(见下图,GPU 那一竖线被黑线占满了)。我在 2020-09 就想到了这个方案,可惜知道 2020-11 才有机会验证。

双 - CPU 群 - 单 - GPU」的适应性非常好。人类可以根据自己环境的运行速度,选择一个 GPU 要对应多少个 CPU(一般是 4+4 个)。并且无论有多少个 GPU 都能让它们满负荷运行。如果存在数量较少的纯 CPU 的计算节点,那么直接将「双 - CPU 群 - 单 - GPU」的 GPU 换成 CPU 即可高效采样。如果纯 CPU 节点数量众多,那么可以使用方案 1.1,进行最快速度的采样。

5.2「更新参数」的分布式方案:「单轮环式与延迟更新」

写在前面:深度强化学习恰好在这里和有监督的深度学习差异极大,导致无法直接拿深度学习的分布式方案过来用。我先粗略讲有监督的深度学习的分布式方案:

  1. 磁盘数据,在 CPU,↓ 加载
  2. 内存数据,在 CPU 或 GPU,↓ 预处理、封装
  3. 批次数据,在 GPU,↓ 随机采样
  4. 训练阶段:前向传播,得到损失, ↓ 计算梯度
  5. 训练阶段:后向传播,更新参数

加粗的地方就是多进程、分布式可以发力的地方。预处理阶段可以很容易做到并行。分布式算法可以在较高频率地传输梯度这一个数据量较小的值,或者可以较低频率地传输整个模型的参数。大 batch size 更新很稳(DRL 更需要稳),好处很多,可惜人类因为单卡的限制而无法享受,分布式可以很好地回应这个需求:只需要选择合适的传输方案。

无论传输的是梯度,还是网络参数,都与以上几种刚入门的人很容易想到的方案相关:

  • 最简单的集中式会将负载都集中在中心的那个 GPU,GPU 少的时候可用
  • 层级式在 GPU 多的时候可用,但是多轮传播中,总有某一层 GPU 闲着。
  • 多轮环式需要更新多轮,慢(因此我特地多画了一圈箭头)
  • 并发环式比较复杂,可以变出很多花样,较快。(但我要选哪种花样呢?)

如果传输梯度,那么每一次随机批次梯度下降的时候,我都需要更新让 GPU 相互传输一次梯度,也许在同一台服务器中可以这么做,但是分布式服务器即便在同一个局域网,网线还是比主板总线慢得多。因此我选择传递网络参数,并降低通信频率

集中式和层级式需要合并计算,然后分发,更慢了,给中心节点带来巨大压力,因此我不选择。最后我选择了多轮环视,它的缺点是 “多轮导致的慢”。我的解决方案是**「单轮环式与延迟更新」**:

TD3 算法提出 TwinCritic、Smoothing policy 之余,还提出 Delay Update,详见强化学习算法 TD3 论文的翻译与解读 ,于是我猜想:梯度也许需要综合每个节点的计算成果,但是网络参数没有必要把每个节点都计算进去,因为 TD3 证明了它可以延迟更新。于是多轮环式可以直接改成单轮环式。

「单轮环式与延迟更新」结构非常灵活,简洁。我们可以根据网络参数大小调整每轮环式更新的延迟间隔:如果通信足够快,那么更新延迟可以尽可能缩短:

  • 在同一台服务器的多张 GPU 卡中高频率地传输梯度
  • 在不同服务器之间低频率地传输网络参数
  • 在不同服务器之间低频率地传输 off-policy 算法的部分 被更新的 ReplayBuffer
  • 在不同服务器之间低频率地传输 on-policy 算法的全部 被更新的 ReplayBuffer,可以直接使用并发环式,因为这个的频率非常低,而且 on-policy 的 ReplayBuffer 比 on-policy 小多了。

后期我可能会在延迟更新的基础上,尝试并发的环式:让每个节点随机地接收 n 个节点传来的数据(不能换成每个节点随机分发给 n 个节点,避免造成不平衡接收)。

6.半精度(很容易做成 switch on/off 模式)


DRL 和半精度相性非常好,可以做到在网络内部全称使用半精度,天作之和:

  • 强化学习需要快速拟合而不能用深层网络 > 10,半精度也在深层网络容易梯度消失。
  • 强化学习和批归一化不兼容(写于 2020 年),半精度在计算批归一化的时候也需要转换回 float32。详见强化学习需要批归一化 (Batch Norm) 吗?

可以用简单的几行代码实现,因此可以做成 switch on/off 模式,不会背离 小雅的 “小”,GPU 有 TensorCore 的情况下提速很快。此外,只有用 GPU 才有必要用半精度。「评估模型」辅线程不需要用 GPU,因此也不需要用半精度(甚至因为 CPU 的制程,float64 改为 float32 都不会加快速度,只会节省内存)

automatic mixed-precision training - PyTroch 1.6+ 实现半精度可以不需要 NVIDIA ApeX

  • 【高性能的 DRL 库细节】

下面这些细节,只改进一处地方,不一定都会有肉眼看得见的性能提升。但如果全部都改了,其性能提升会非常明显。有很多方法是「强化学习库:小雅 ElegantRL」第一个使用的。

  • 合并 终止状态 done 与 折扣因子 gamma

有很大改进空间的旧方法:

next_state, reward, done, info_dict = env.step(action)
assert isinstance(done, bool)
assert isinstance(gamma, float)

q_value = reward + (1-float(done)) * gamma * q_next

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

合并 终止状态 done 与 折扣因子 gamma,使用 mask 代替。保存在 Replay Buffer 中的是 mask,不再保存 float(done)。修改后,不需要 float(done),不需要减法,不需要乘以两个数:

mask = gamma if done else 0.0

q_value = reward + mask * q_next

  • 1
  • 2
  • 3
  • 4
  • 将 Buffer 保存在一块连续的内存上

比如 PyTorch 官网提供的强化学习 Buffer 保存例子(2020 年),使用了 NamedTuple,此外还有其他方案,如下:

Method       Used Times (second)   Detail
List         24                    list()
NamedTuple   20                    collections.namedtuple
Array(CPU)   13                    numpy.array/torch.tensor(CPU)
Array(GPU)   13                    torch.tensor (GPU)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

由于实际训练的时候,还是会将 Buffer 的数据传到 GPU,因此 Array (GPU) 才是最快的。我还做了很多实验,只是没有全放出来。请注意,看似这一块与深度学习很像,但是很多显而易见的深度学习方案很不适合在 DRL 中使用,不要以为这里存在小改进就能提升性能的空间。

尽管可以直接通过理论推理结论,但我还是做了实验去证明理论分析的结果是可靠的。详细请移步:DRL 的经验回放 (Experiment Replay Buffer) 用 Numpy 实现让它更快一点 ←这篇文章是我一年前写的 2020-01,半年前发在知乎 2020-06,我现在说 “我有多年的 DRL 库开源经验”,应该可以让人信服。对于我给出实验结果的结论,如果有不同意见请展示实验结果与开源代码,不要只用语言

7.高性能的 PyTorch

【如何高效地将 GPU 里张量转化成 CPU 数组?】

【Differences between .data and .detach】

https://github.com/pytorch/pytorch/issues/6990#issuecomment-384680164

Any in-place change on x.detach() will cause errors when x is needed in backward, so .detach() is a safer way for the exclusion of subgraphs from gradient computation.

用 .detach() 比 .data 安全,用 .detach() 会在计算图中将这个节点排除,因而它可以在张量发生改变的时候及时报错,而 .data 会继续算出一个错误的梯度 而不主动报错。

把 GPU 里面的张量转化成 CPU 里的数组,有两种方法:

.data.cpu().numpy()

.detach().cpu().numpy() # 比较安全

.mean().item() # 只能处理一个数值

注意上面几个东西的顺序,很重要

========================

【如何高效地将 CPU 数组转化成 GPU 里的张量?】只能用这种方法,它最快:

t

device = torch.device('cuda')
reward = 1.1
mask = 0.0 if done else gamma
state = np.array((2.2, 3.3), dtype=np.float32)
action = np.array((4.4, 5.5), dtype=np.float32)

array = np.hstack((reward, mask, state, action))
tensor = torch.tensor(array, dtype=torch.float32).cuda()  # slowest and bad
tensor = torch.tensor(array, dtype=torch.float32).to(device)  # slower
tensor = torch.tensor(array, dtype=torch.float32, device=device)
tensor = torch.as_tensor(array, dtype=torch.float32, device=device)  # faster

tensor = torch.as_tensor(array, device=device)  # fastest !!!!!!!!!!!!!!!!!!!!!!!!
tensor = torch.as_tensor((reward, mask, *state), device=device)  # slower
tensor = torch.from_numpy(array).to(device)  # slower


# 以下三种等效
tensor = net(tensor.detach())
tensor = net(tensor).detach()
with torch.no_grad():
    tensor = net(tensor)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

GPU tensor to CPU numpy, 只能用以下的方法,这种方法最快

tensor.detach().cpu().numpy()

不能用 data,因为这个很旧,功能已被 .detach() 替代
detach() 不让PyTorch框架去追踪张量的梯度,所以在放在最前
cpu() 把张量从GPU显存中传输到CPU内存
numpy() 把张量tensor变成数组array

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

详见:https://discuss.pytorch.org/t/cpu-detach-numpy-vs-data-cpu-numpy/20036/4

7 Tips To Maximize PyTorch Performance - William Falcon - May 12, 2020

https://towardsdatascience.com/7-tips-for-squeezing-maximum-performance-from-pytorch-ca4a40951259

PyTorch Lightning

PyTorchLightning/pytorch-lightning

  • PPO 的 GAE,保存 noise 而非 log prob
  • SAC 的 init alpha log,std log, log prob, auto alpha 的正负号
  • TD3 的 noise vector
  • DDPG 的 OU noise 与超参数哲学
  • DQN 的 epi-greedy,noisy 轮盘赌,竞标赛
  • Mod SAC trusted lambda, dense Net, reward scale (重点
  • norm PPO 的 q norm,state,
  • env 的 norm action float32 astype
  • twin critic pg,min(QQ),以及 share para,写进同一个 forward
  • note book 1943

8.如何评价一个深度强化学习算法库的好坏?

**我还会写一篇「如何评价一个深度强化学习算法库的好坏?」**无论按谁的标准,客观事实是 伯克利的 Rllib ray-project 是 2020 年(写于 2021 年)最好的开源 DRL 库,它支持全平台(PyTorch、TensorFlow1、2、Keras),支持多进程 rollout(且其机制很完善),支持真正的分布式 DRL 训练(基于 Redis),训练稳定且快。唯三的劣势是:它的使用门槛高(我需要 3 天才能入门),前置软件多(你需要安装全家桶),代码耦合高(你不容易按自己需求去改)。

如果追求极致的性能,在 2021 年我不推荐除了伯克利的 Rllib ray-project 以外的其他库。其他开源库只是起步早,而在没有竞品的时代在 github 收获了很多 star,最明显的是:

莫烦的代码在没有竞品的时代填补了空白,切实地帮助了很多人。若能阅读英文,现在有 OpenAI 的 SpinningUp 是更好的选择。可至今还有人那种莫烦的代码问:为什么换了 random seed 就跑不出来。

baselines 不好用,所以才会有 stable-baselines。但是 stable-baselines 也不够 stable,如果将它对比 Rllib ray-project 就能很明显地体会到,可惜 Rllib ray 的门槛太高,很少人发声。stable-baselines 还在使用 TensorFlow1,TensorFLow 1 的确很快,但是静态图有诸多使用限制。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/593493
推荐阅读
相关标签
  

闽ICP备14008679号