一个最基本的深度强化学习训练流程 pipeline 应该是这样的:
- 初始化环境、网络、经验池
- 在环境中探索,并把数据存入经验池
- 从经验池中取出数据,更新网络参数
- 对训练得到的策略进行评估,循环 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)
大部分深度强化学习 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():
下面对深度强化学习进行分类,并举出特例,这是为了解释抽象深度学习算法框架的合理性。加入新算法时,只需要继承 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()
一些代码没有讲这两个步骤分开,而是在环境中探索一步后,马上更新一次参数。下面截图来自莫烦的 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
深度强化学习可分为 确定策略梯度 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算法会用到
经过试验,将训练数据 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 加快数据读取:
- 从磁盘中读取数据,存放到内存(可使用多进程加速,CPU workers)
- 对数据进行预处理,并传入 GPU 的显存
- random sample,在 GPU 里计算梯度,更新网络参数
- 循环以上内容,定时地保存 check point
- 适时地终止训练,避免过拟合
深度强化学习(如:DDPG-style training pipeline)。DRL 难以套用有监督 DL 的多进程加速方案,他们只有加粗的 2、3 步骤相同。在 DL 里,数据可以提前准备好,而 DRL 的数据需要与环境交互产生,并且需要严格限制交互次数。在 DL 里,我可以用训练的副产物 loss function 帮助我判断何时可以终止训练,避免过拟合,而 DRL 没有判断过拟合的机制,因此一定需要绘制出 学习曲线 帮助我们决定 “何时终止训练” 与“保存哪个策略”。
- agent 与环境交互,得到的零碎数据存放在内存中(一般是 CPU,或者再加上 GPU)
- 将数据输入传入 GPU 的显存中
- random sample,在 GPU 里计算梯度,更新网络参数
- 对策略进行评估,绘制学习曲线,保存得分高的
- 观察学习曲线,若分数不能更高,则终止训练
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 库设计者可以不考虑这一部分,但我还是为此写了两篇文章:
- 如何选择深度强化学习算法?MuZero/SAC/PPO/TD3/DDPG/DQN / 等
- 深度强化学习调参技巧:以 D3QN、TD3、PPO、SAC 算法为例
「强化学习:小雅」这个库超参数的默认值会优先保证稳定训练,值得使用者参考。
**【孪生估值网络】**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))
估值网络的参数共享 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
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
一些知名的 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
编写自定义的训练环境,请参考 深度强化学习调参技巧:以 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「更新参数」的分布式方案:「单轮环式与延迟更新」
写在前面:深度强化学习恰好在这里和有监督的深度学习差异极大,导致无法直接拿深度学习的分布式方案过来用。我先粗略讲有监督的深度学习的分布式方案:
- 磁盘数据,在 CPU,↓ 加载
- 内存数据,在 CPU 或 GPU,↓ 预处理、封装
- 批次数据,在 GPU,↓ 随机采样
- 训练阶段:前向传播,得到损失, ↓ 计算梯度
- 训练阶段:后向传播,更新参数
加粗的地方就是多进程、分布式可以发力的地方。预处理阶段可以很容易做到并行。分布式算法可以在较高频率地传输梯度这一个数据量较小的值,或者可以较低频率地传输整个模型的参数。大 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
合并 终止状态 done 与 折扣因子 gamma,使用 mask 代替。保存在 Replay Buffer 中的是 mask,不再保存 float(done)。修改后,不需要 float(done),不需要减法,不需要乘以两个数:
mask = gamma if done else 0.0 q_value = reward + mask * q_next
- 将 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)
由于实际训练的时候,还是会将 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-
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)
GPU tensor to CPU numpy, 只能用以下的方法,这种方法最快
tensor.detach().cpu().numpy() 不能用 data,因为这个很旧,功能已被 .detach() 替代 detach() 不让PyTorch框架去追踪张量的梯度,所以在放在最前 cpu() 把张量从GPU显存中传输到CPU内存 numpy() 把张量tensor变成数组array
详见: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-ca4a
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(),以及 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,最明显的是:
- 莫烦的强化学习代码,星星 6000,2017 年后没有大更新
- OpenAI baselines,星星 11000,2018 年后没有大更新
- hill-a stable-baselines,星星 2800,还在使用 TensorFlow 1(2020 年),没有未来
莫烦的代码在没有竞品的时代填补了空白,切实地帮助了很多人。若能阅读英文,现在有 OpenAI 的 SpinningUp 是更好的选择。可至今还有人那种莫烦的代码问:为什么换了 random seed 就跑不出来。
baselines 不好用,所以才会有 stable-baselines。但是 stable-baselines 也不够 stable,如果将它对比 Rllib ray-project 就能很明显地体会到,可惜 Rllib ray 的门槛太高,很少人发声。stable-baselines 还在使用 TensorFlow1,TensorFLow 1 的确很快,但是静态图有诸多使用限制。
今天的文章
深度强化学习库的设计思想带你深入了解DRL:从环境、网络更新、经验池、经验池、算法基类分离度、分布式、多进程等方面评价分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/81138.html