tf-rl-qkstgd-merge-1

77 阅读33分钟

TensorFlow 强化学习快速启动指南(二)

原文:annas-archive.org/md5/7fda30dfb2bb9f5d7ff4a34ce0c3bea9

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:异步方法 - A3C 和 A2C

我们在上一章中看过 DDPG 算法。DDPG 算法(以及之前看到的 DQN 算法)的一个主要缺点是使用重放缓冲区来获取独立同分布的数据样本进行训练。使用重放缓冲区会消耗大量内存,这在强健的 RL 应用中是不可取的。为了解决这个问题,Google DeepMind 的研究人员提出了一种叫做 异步优势演员评论家A3C)的在线算法。A3C 不使用重放缓冲区;而是使用并行工作处理器,在这里创建环境的不同实例并收集经验样本。一旦收集到有限且固定数量的样本,它们将用于计算策略梯度,并异步发送到中央处理器更新策略。更新后的策略随后会发送回工作处理器。使用并行处理器体验环境的不同场景可以产生独立同分布的样本,这些样本可以用来训练策略。本章将介绍 A3C,同时也会简要提到它的一个变种,叫做 优势演员评论家A2C)。

本章将涵盖以下主题:

  • A3C 算法

  • A3C 算法应用于 CartPole

  • A3C 算法应用于 LunarLander

  • A2C 算法

在本章中,您将了解 A3C 和 A2C 算法,并学习如何使用 Python 和 TensorFlow 编写代码。我们还将把 A3C 算法应用于解决两个 OpenAI Gym 问题:CartPole 和 LunarLander。

技术要求

要顺利完成本章,以下知识将非常有帮助:

  • TensorFlow(版本 1.4 或更高)

  • Python(版本 2 或 3)

  • NumPy

A3C 算法

如前所述,A3C 中有并行工作者,每个工作者将计算策略梯度并将其传递给中央(或主)处理器。A3C 论文还使用 advantage 函数来减少策略梯度中的方差。loss 函数由三部分组成,它们加权相加;包括价值损失、策略损失和熵正则化项。价值损失,L[v],是状态值和目标值的 L2 损失,后者是通过折扣奖励和奖励总和计算得出的。策略损失,L[p],是策略分布的对数与 advantage 函数 A 的乘积。熵正则化,L[e],是香农熵,它是策略分布与其对数的乘积,并带有负号。熵正则化项类似于探索的奖励;熵越高,策略的正则化效果越好。这三项的加权分别为 0.5、1 和 -0.005。

损失函数

值损失计算为三个损失项的加权和:值损失 L[v],策略损失 L[p] 和熵正则化项 L[e],它们的计算方式如下:

L 是总损失,需要最小化。注意,我们希望最大化 advantage 函数,因此在 L[p] 中有一个负号,因为我们在最小化 L。同样,我们希望最大化熵项,而由于我们在最小化 L,因此在 L 中有一个负号的项 -0.005 L[e]

CartPole 和 LunarLander

在本节中,我们将 A3C 应用于 OpenAI Gym 的 CartPole 和 LunarLander。

CartPole

CartPole 由一个垂直的杆子和一个推车组成,需要通过将推车向左或向右移动来保持平衡。CartPole 的状态维度为四,动作维度为二。

更多关于 CartPole 的详情,请查看以下链接:gym.openai.com/envs/CartPole-v0/

LunarLander

LunarLander,顾名思义,涉及着陆器在月球表面着陆。例如,当阿波罗 11 号的鹰号着陆器在 1969 年着陆月球表面时,宇航员尼尔·阿姆斯特朗和巴兹·奥尔德林必须在下降的最后阶段控制火箭推进器,并安全地将航天器降落到月球表面。之后,阿姆斯特朗走上月球并说道那句如今家喻户晓的话:“人类的一小步, mankind 的一大步”。在 LunarLander 中,月球表面有两面黄色旗帜,目标是将航天器降落在这两面旗帜之间。与阿波罗 11 号的鹰号着陆器不同,LunarLander 的燃料是无限的。LunarLander 的状态维度为八,动作维度为四,四个动作分别是:不做任何操作、启动左侧推进器、启动主推进器,或者启动右侧推进器。

查看以下链接,获取环境的示意图:gym.openai.com/envs/LunarLander-v2/

A3C 算法应用于 CartPole

在这里,我们将用 TensorFlow 编写 A3C 并应用它,以便训练一个代理来学习 CartPole 问题。编写代码时需要以下代码文件:

  • cartpole.py:此文件将启动训练或测试过程

  • a3c.py:此文件中编写了 A3C 算法

  • utils.py:此文件包含实用函数

编写 cartpole.py

现在,我们将开始编写 cartpole.py。请按照以下步骤开始:

  1. 首先,我们导入相关包:
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import gym
import os
import threading
import multiprocessing

from random import choice
from time import sleep
from time import time

from a3c import *
from utils import *
  1. 接下来,我们设置问题的参数。我们只需训练 200 个回合(没错,CartPole 是个简单问题!)。我们将折扣因子 gamma 设置为 0.99。CartPole 的状态和动作维度分别为 42。如果你想加载一个预训练的模型并继续训练,请将 load_model 设置为 True;如果从头开始训练,请将其设置为 False。我们还将设置 model_path
max_episode_steps = 200
gamma = 0.99
s_size = 4 
a_size = 2 
load_model = False
model_path = './model'
  1. 我们重置 TensorFlow 图,并创建一个用于存储模型的目录。我们将主处理器称为 CPU 0,工作线程的 CPU 编号为非零值。主处理器将执行以下任务:首先,它将创建一个 global_episodes 对象,用于计算全局变量的数量。工作线程的总数将存储在 num_workers 中,我们可以通过调用 Python 的 multiprocessing 库中的 cpu_count() 来获取系统中可用的处理器数量。我们将使用 Adam 优化器,并将其存储在名为 trainer 的对象中,同时设定适当的学习率。接着,我们将定义一个名为 AC 的演员-评论家类,因此我们必须首先创建一个 AC 类型的主网络对象,命名为 master_network,并传递适当的参数给该类的构造函数。然后,对于每个工作线程,我们将创建一个独立的 CartPole 环境实例和一个 Worker 类实例(稍后定义)。最后,为了保存模型,我们还将创建一个 TensorFlow saver:
tf.reset_default_graph()

if not os.path.exists(model_path):
    os.makedirs(model_path)

with tf.device("/cpu:0"): 

    # keep count of global episodes
    global_episodes = tf.Variable(0,dtype=tf.int32,name='global_episodes',trainable=False)

    # number of worker threads
    num_workers = multiprocessing.cpu_count() 

    # Adam optimizer
    trainer = tf.train.AdamOptimizer(learning_rate=2e-4, use_locking=True) 

    # global network
    master_network = AC(s_size,a_size,'global',None) 

    workers = []
    for i in range(num_workers):
        env = gym.make('CartPole-v0')
        workers.append(Worker(env,i,s_size,a_size,trainer,model_path,global_episodes))

    # tf saver
    saver = tf.train.Saver(max_to_keep=5)
  1. 然后,我们启动 TensorFlow 会话。在会话中,我们为不同的工作线程创建一个 TensorFlow 协调器。接着,我们要么加载或恢复一个预训练的模型,要么运行 tf.global_variables_initializer() 来为所有权重和偏差分配初始值:
with tf.Session() as sess:

    # tf coordinator for threads
    coord = tf.train.Coordinator()

    if load_model == True:
        print ('Loading Model...')
        ckpt = tf.train.get_checkpoint_state(model_path)
        saver.restore(sess,ckpt.model_checkpoint_path)
    else:
        sess.run(tf.global_variables_initializer())
  1. 然后,我们启动 worker_threads。具体来说,我们调用 work() 函数,它是 Worker() 类的一部分(稍后定义)。threading.Thread() 将为每个 worker 分配一个线程。通过调用 start(),我们启动了 worker 线程。最后,我们需要合并这些线程,确保它们在所有线程完成之前不会终止:
    # start the worker threads
    worker_threads = []
    for worker in workers:
        worker_work = lambda: worker.work(max_episode_steps, gamma, sess, coord,saver)
        t = threading.Thread(target=(worker_work))
        t.start()
        worker_threads.append(t)
    coord.join(worker_threads)

你可以在 www.tensorflow.org/api_docs/python/tf/train/Coordinator 了解更多关于 TensorFlow 协调器的信息。

编写 a3c.py

现在我们将编写 a3c.py。这涉及以下步骤:

  1. 导入包

  2. 设置权重和偏差的初始化器

  3. 定义 AC

  4. 定义 Worker

首先,我们需要导入必要的包:

import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import gym
import threading
import multiprocessing

from random import choice
from time import sleep
from time import time
from threading import Lock

from utils import *

然后,我们需要为权重和偏差设置初始化器;具体来说,我们使用 Xavier 初始化器来初始化权重,并使用零初始化偏差。对于网络的最后输出层,权重是指定范围内的均匀随机数:

xavier = tf.contrib.layers.xavier_initializer()
bias_const = tf.constant_initializer(0.05)
rand_unif = tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3)
regularizer = tf.contrib.layers.l2_regularizer(scale=5e-4)

AC 类

现在我们将描述 AC 类,它也是 a3c.py 的一部分。我们为 AC 类定义了构造函数,包含一个输入占位符,以及两个全连接的隐藏层,分别有 256128 个神经元,并使用 elu 激活函数。接着是策略网络,使用 softmax 激活函数,因为我们在 CartPole 中的动作是离散的。此外,我们还有一个没有激活函数的值网络。请注意,我们对策略和价值网络共享相同的隐藏层,这与过去的示例不同:

class AC():
    def __init__(self,s_size,a_size,scope,trainer):
        with tf.variable_scope(scope):

            self.inputs = tf.placeholder(shape=[None,s_size],dtype=tf.float32)

            # 2 FC layers 
            net = tf.layers.dense(self.inputs, 256, activation=tf.nn.elu, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
            net = tf.layers.dense(net, 128, activation=tf.nn.elu, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)

            # policy
            self.policy = tf.layers.dense(net, a_size, activation=tf.nn.softmax, kernel_initializer=xavier, bias_initializer=bias_const)

            # value
            self.value = tf.layers.dense(net, 1, activation=None, kernel_initializer=rand_unif, bias_initializer=bias_const)

对于worker线程,我们需要定义loss函数。因此,当 TensorFlow 作用域不是global时,我们定义一个动作占位符,以及其独热表示;我们还为target值和advantage函数定义占位符。然后,我们计算策略分布和独热动作的乘积,将它们相加,并将它们存储在policy_times_a对象中。然后,我们组合这些项来构建loss函数,正如我们之前提到的。我们计算值的 L2 损失的批次总和;策略分布乘以其对数的香农熵,带有一个负号;作为策略分布对数的乘积的loss函数;以及批次样本上advantage函数的总和。最后,我们使用适当的权重结合这些损失来计算总损失,存储在self.loss中:

# only workers need tf operations for loss functions and gradient updating
            if scope != 'global':
                self.actions = tf.placeholder(shape=[None],dtype=tf.int32)
                self.actions_onehot = tf.one_hot(self.actions,a_size,dtype=tf.float32)
                self.target_v = tf.placeholder(shape=[None],dtype=tf.float32)
                self.advantages = tf.placeholder(shape=[None],dtype=tf.float32)

                self.policy_times_a = tf.reduce_sum(self.policy * self.actions_onehot, [1])

                # loss 
                self.value_loss = 0.5 * tf.reduce_sum(tf.square(self.target_v - tf.reshape(self.value,[-1])))
                self.entropy = - tf.reduce_sum(self.policy * tf.log(self.policy + 1.0e-8))
                self.policy_loss = -tf.reduce_sum(tf.log(self.policy_times_a + 1.0e-8) * self.advantages)
                self.loss = 0.5 * self.value_loss + self.policy_loss - self.entropy * 0.005

正如您在上一章中看到的,我们使用tf.gradients()来计算策略梯度;具体来说,我们计算loss函数相对于本地网络变量的梯度,后者从tf.get_collection()中获得。为了减少梯度爆炸问题,我们使用 TensorFlow 的tf.clip_by_global_norm()函数将梯度裁剪为40.0的大小。然后,我们可以使用tf.get_collection()来收集全局网络的网络参数,作用于 Adam 优化器中的梯度,使用apply_gradients()。这将计算策略梯度:

# get gradients from local networks using local losses; clip them to avoid exploding gradients
local_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope)
self.gradients = tf.gradients(self.loss,local_vars)
self.var_norms = tf.global_norm(local_vars)
grads,self.grad_norms = tf.clip_by_global_norm(self.gradients,40.0)

# apply local gradients to global network using tf.apply_gradients()
global_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, 'global')
self.apply_grads = trainer.apply_gradients(zip(grads,global_vars))

Worker()类

现在我们将描述Worker()类,每个工作线程都会使用。首先,我们为该类定义__init__()构造函数。在其中,我们定义工作人员的名称、编号、模型路径、Adam 优化器、全局剧集计数以及增加它的操作符:

class Worker():
    def __init__(self,env,name,s_size,a_size,trainer,model_path,global_episodes):
        self.name = "worker_" + str(name)
        self.number = name 
        self.model_path = model_path
        self.trainer = trainer
        self.global_episodes = global_episodes
        self.increment = self.global_episodes.assign_add(1)

我们还创建了AC类的本地实例,并传入适当的参数。然后,我们创建一个 TensorFlow 操作,将全局模型参数复制到本地。我们还创建了一个在对角线上具有一的 NumPy 单位矩阵,以及一个环境对象:

# local copy of the AC network 
self.local_AC = AC(s_size,a_size,self.name,trainer)

# tensorflow op to copy global params to local network
self.update_local_ops = update_target_graph('global',self.name) 

self.actions = np.identity(a_size,dtype=bool).tolist()
self.env = env

接下来,我们创建了train()函数,这是Worker类中最重要的部分。状态、动作、奖励、下一个状态或观察值和价值是从作为参数传递给函数的经验列表中获取的。我们使用一个名为discount()的实用函数计算了奖励的折现总和,很快我们将定义它。类似地,advantage函数也被折现了:

# train function
    def train(self,experience,sess,gamma,bootstrap_value):
        experience = np.array(experience)
        observations = experience[:,0]
        actions = experience[:,1]
        rewards = experience[:,2]
        next_observations = experience[:,3]
        values = experience[:,5]

        # discounted rewards
        self.rewards_plus = np.asarray(rewards.tolist() + [bootstrap_value])
        discounted_rewards = discount(self.rewards_plus,gamma)[:-1]

        # value 
        self.value_plus = np.asarray(values.tolist() + [bootstrap_value])

        # advantage function 
        advantages = rewards + gamma * self.value_plus[1:] - self.value_plus[:-1]
        advantages = discount(advantages,gamma)

然后,我们通过调用之前定义的 TensorFlow 操作来更新全局网络参数,并传入通过 TensorFlow 的 feed_dict 函数传递给占位符的所需输入。请注意,由于我们有多个工作线程在执行这个更新操作,因此需要避免冲突。换句话说,在任意时间点,只有一个线程可以更新主网络参数;两个或更多线程同时执行更新操作时,更新不会按顺序进行,这可能会导致问题。如果一个线程在另一个线程更新全局参数时也在进行更新,那么前一个线程的更新会被后一个线程覆盖,这是我们不希望发生的情况。这是通过 Python 的 threading 库中的 Lock() 函数实现的。我们创建一个 Lock() 实例,命名为 locklock.acquire() 只会授予当前线程访问权限,当前线程会执行更新操作,完成后通过 lock.release() 释放锁。最后,我们从函数中返回损失值:

# lock for updating global params
lock = Lock()
lock.acquire() 

# update global network params
fd = {self.local_AC.target_v:discounted_rewards, self.local_AC.inputs:np.vstack(observations), self.local_AC.actions:actions, self.local_AC.advantages:advantages}
value_loss, policy_loss, entropy, _, _, _ = sess.run([self.local_AC.value_loss, self.local_AC.policy_loss, self.local_AC.entropy, self.local_AC.grad_norms, self.local_AC.var_norms, self.local_AC.apply_grads], feed_dict=fd)

# release lock
lock.release() 

return value_loss / len(experience), policy_loss / len(experience), entropy / len(experience)

接下来,我们需要定义工作线程的 work() 函数。首先,我们获取全局的 episode 计数,并将 total_steps 设置为零。然后,在 TensorFlow 会话中,当线程仍然协调时,我们使用 self.update_local_ops 将全局参数复制到本地网络。接下来,我们启动一个 episode。由于 episode 尚未结束,我们获取策略分布并将其存储在 a_dist 中。我们从这个分布中使用 NumPy 的 random.choice() 函数采样一个动作。这个动作 a 被输入到环境的 step() 函数中,以获取新的状态、奖励和终止布尔值。我们可以通过将奖励除以 100.0 来调整奖励值。

经验存储在本地缓冲区中,称为 episode_buffer。我们还将奖励添加到 episode_reward 中,并增加 total_steps 计数以及 episode_step_count

# worker's work function
def work(self,max_episode_steps, gamma, sess, coord, saver):
    episode_count = sess.run(self.global_episodes)
    total_steps = 0
    print ("Starting worker " + str(self.number))

        with sess.as_default(), sess.graph.as_default(): 
            while not coord.should_stop():

                # copy global params to local network 
                sess.run(self.update_local_ops)

                # lists for book keeping
                episode_buffer = []
                episode_values = []
                episode_frames = []

                episode_reward = 0
                episode_step_count = 0
                d = False

                s = self.env.reset()
                episode_frames.append(s)

                while not d:

                    # action and value
                    a_dist, v = sess.run([self.local_AC.policy,self.local_AC.value], feed_dict={self.local_AC.inputs:[s]})
                    a = np.random.choice(np.arange(len(a_dist[0])), p=a_dist[0])

                    if (self.name == 'worker_0'):
                       self.env.render()

                    # step
                    s1, r, d, info = self.env.step(a)

                    # normalize reward
                    r = r/100.0

                    if d == False:
                        episode_frames.append(s1)
                    else:
                        s1 = s

                    # collect experience in buffer 
                    episode_buffer.append([s,a,r,s1,d,v[0,0]])

                    episode_values.append(v[0,0])

                    episode_reward += r
                    s = s1 
                    total_steps += 1
                    episode_step_count += 1

如果缓冲区中有 25 个条目,说明是时候进行更新了。首先,计算并将值存储在 v1 中,然后将其传递给 train() 函数,该函数将输出三个损失值:价值、策略和熵。之后,重置 episode_buffer。如果 episode 已结束,我们就跳出循环。最后,我们在屏幕上打印出 episode 计数和奖励。请注意,我们使用了 25 个条目作为进行更新的时机。可以随意调整这个值,看看该超参数如何影响训练过程:

# if buffer has 25 entries, time for an update 
if len(episode_buffer) == 25 and d != True and episode_step_count != max_episode_steps - 1:
    v1 = sess.run(self.local_AC.value, feed_dict={self.local_AC.inputs:[s]})[0,0]
    value_loss, policy_loss, entropy = self.train(episode_buffer,sess,gamma,v1)
    episode_buffer = []
    sess.run(self.update_local_ops)

# idiot check to ensure we did not miss update for some unforseen reason 
if (len(episode_buffer) > 30):
    print(self.name, "buffer full ", len(episode_buffer))
    sys.exit()

if d == True:
    break

print("episode: ", episode_count, "| worker: ", self.name, "| episode reward: ", episode_reward, "| step count: ", episode_step_count)

在退出 episode 循环后,我们使用缓冲区中的剩余样本来训练网络。worker_0 包含全局或主网络,我们可以通过 saver.save 保存它。我们还可以调用 self.increment 操作,将全局 episode 计数加一:

# Update the network using the episode buffer at the end of the episode
if len(episode_buffer) != 0:
    value_loss, policy_loss, entropy = self.train(episode_buffer,sess,gamma,0.0)

print("loss: ", self.name, value_loss, policy_loss, entropy)

# write to file for worker_0
if (self.name == 'worker_0'): 
    with open("performance.txt", "a") as myfile:
        myfile.write(str(episode_count) + " " + str(episode_reward) + " " + str(episode_step_count) + "\n")

# save model params for worker_0
if (episode_count % 25 == 0 and self.name == 'worker_0' and episode_count != 0):
        saver.save(sess,self.model_path+'/model-'+str(episode_count)+'.cptk')
print ("Saved Model")

if self.name == 'worker_0':
    sess.run(self.increment)
episode_count += 1

这就是 a3c.py 的内容。

编写 utils.py

最后,我们将编写utils.py中的utility函数。我们将导入所需的包,并且还将定义之前使用的update_target_graph()函数。它以源和目标参数的作用域作为参数,并将源中的参数复制到目标中:

import numpy as np
import tensorflow as tf
from random import choice

# copy model params 
def update_target_graph(from_scope,to_scope):
    from_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, from_scope)
    to_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, to_scope)

    copy_ops = []
    for from_param,to_param in zip(from_params,to_params):
        copy_ops.append(to_param.assign(from_param))
    return copy_ops

另一个我们需要的工具函数是discount()函数。它会将输入列表x倒序运行,并按折扣因子gamma的权重进行求和。然后返回折扣后的值:


# Discounting function used to calculate discounted returns.
def discount(x, gamma):
    dsr = np.zeros_like(x,dtype=np.float32)
    running_sum = 0.0
    for i in reversed(range(0, len(x))):
       running_sum = gamma * running_sum + x[i]
       dsr[i] = running_sum 
    return dsr

在 CartPole 上训练

cartpole.py的代码可以使用以下命令运行:

python cartpole.py

代码将回合奖励存储在performance.txt文件中。以下截图展示了训练过程中回合奖励的图表:

图 1:在 A3C 训练下的 CartPole 回合奖励

请注意,由于我们已经塑形了奖励,您在上图中看到的回合奖励与其他研究人员在论文和/或博客中报告的值不同。

A3C 算法应用于 LunarLander

我们将扩展相同的代码来训练一个智能体解决 LunarLander 问题,该问题比 CartPole 更具挑战性。大部分代码与之前相同,因此我们只会描述需要对前面的代码进行的更改。首先,LunarLander 问题的奖励塑形不同。因此,我们将在a3c.py文件中包含一个名为reward_shaping()的函数。它将检查着陆器是否已撞击月球表面;如果是,回合将被终止,并会受到-1.0的惩罚。如果着陆器未移动,回合将被终止,并支付-0.5的惩罚:

def reward_shaping(r, s, s1):
     # check if y-coord < 0; implies lander crashed
     if (s1[1] < 0.0):
       print('-----lander crashed!----- ')
       d = True 
       r -= 1.0

     # check if lander is stuck
     xx = s[0] - s1[0]
     yy = s[1] - s1[1]
     dist = np.sqrt(xx*xx + yy*yy) 
     if (dist < 1.0e-4):
       print('-----lander stuck!----- ')
       d = True 
       r -= 0.5
     return r, d

我们将在env.step()之后调用此函数:

# reward shaping for lunar lander
r, d = reward_shaping(r, s, s1)

编写 lunar.py

之前练习中的cartpole.py文件已重命名为lunar.py。所做的更改如下。首先,我们将每个回合的最大时间步数设置为1000,折扣因子设置为gamma = 0.999,状态和动作维度分别设置为84

max_episode_steps = 1000
gamma = 0.999
s_size = 8 
a_size = 4

环境设置为LunarLander-v2

env = gym.make('LunarLander-v2')

这就是在 LunarLander 上训练 A3C 的代码更改。

在 LunarLander 上训练

你可以通过以下命令开始训练:

python lunar.py

这将训练智能体并将回合奖励存储在performance.txt文件中,我们可以如下绘制图表:

图 2:使用 A3C 的 LunarLander 回合奖励

如你所见,智能体已经学会了将航天器安全着陆到月球表面。祝你着陆愉快!再强调一次,回合奖励与其他强化学习从业者在论文和博客中报告的值不同,因为我们对奖励进行了缩放。

A2C 算法

A2C 和 A3C 的区别在于 A2C 执行同步更新。在这里,所有工作线程会等待直到它们完成经验收集并计算出梯度。只有在这个过程中,全球(或主)网络的参数才会被更新。这与 A3C 不同,后者的更新是异步进行的,也就是说工作线程不会等待其他线程完成。A2C 比 A3C 更容易编码,但这里没有进行这部分的处理。如果你对此感兴趣,可以将前面提到的 A3C 代码转换为 A2C,之后可以对比两种算法的性能。

总结

在本章中,我们介绍了 A3C 算法,它是一种适用于离散和连续动作问题的在线策略算法。你已经看到三种不同的损失项是如何结合成一个并优化的。Python 的线程库非常有用,可以运行多个线程,每个线程中都有一个策略网络的副本。这些不同的工作线程计算策略梯度,并将其传递给主线程以更新神经网络的参数。我们将 A3C 应用于训练 CartPole 和 LunarLander 问题的智能体,且智能体学习得非常好。A3C 是一种非常强健的算法,不需要重放缓冲区,尽管它确实需要一个本地缓冲区来收集少量经验,之后这些经验将用于更新网络。最后,我们还介绍了该算法的同步版本——A2C。

本章应该已经极大地提升了你对另一种深度强化学习算法的理解。在下一章,我们将学习本书中的最后两个强化学习算法:TRPO 和 PPO。

问题

  1. A3C 是在线策略算法还是离线策略算法?

  2. 为什么使用香农熵项?

  3. 使用大量工作线程有什么问题?

  4. 为什么在策略神经网络中使用 softmax?

  5. 为什么我们需要一个advantage函数?

  6. 这是一个练习:对于 LunarLander 问题,重复训练过程,不进行奖励塑形,看看智能体学习的速度是比本章看到的更快还是更慢。

进一步阅读

第七章:信任区域策略优化和近端策略优化

在上一章中,我们看到了 A3C 和 A2C 的使用,其中 A3C 是异步的,A2C 是同步的。在本章中,我们将看到另一种在线策略强化学习RL)算法;具体来说是两种数学上非常相似的算法,尽管它们在求解方式上有所不同。我们将介绍名为信任区域策略优化TRPO)的算法,这个算法由 OpenAI 和加利福尼亚大学伯克利分校的研究人员于 2015 年提出(顺便提一下,后者是我以前的雇主!)。然而,这个算法在数学上很难求解,因为它涉及共轭梯度算法,这是一种相对较难解决的方法;需要注意的是,像广为人知的 Adam 和随机梯度下降SGD)等一阶优化方法无法用来求解 TRPO 方程。然后,我们将看到如何将策略优化方程的求解合并成一个,从而得到近端策略优化PPO)算法,并且可以使用像 Adam 或 SGD 这样的第一阶优化算法。

本章将涵盖以下主题:

  • 学习 TRPO

  • 学习 PPO

  • 使用 PPO 解决 MountainCar 问题

  • 评估性能

技术要求

成功完成本章所需的软件:

  • Python(2 及以上版本)

  • NumPy

  • TensorFlow(版本 1.4 或更高)

学习 TRPO

TRPO 是 OpenAI 和加利福尼亚大学伯克利分校提出的一个非常流行的在线策略算法,首次提出于 2015 年。TRPO 有多种版本,但我们将学习论文Trust Region Policy Optimization中的基础版本,作者为John Schulman, Sergey Levine, Philipp Moritz, Michael I. Jordan, 和 Pieter AbbeelarXiv:1502.05477arxiv.org/abs/1502.05477

TRPO 涉及求解一个策略优化方程,并附加一个关于策略更新大小的约束。我们现在将看到这些方程。

TRPO 方程

TRPO 涉及最大化当前策略分布π[θ]与旧策略分布π[θ]^(old)(即在早期时间步的策略)之比的期望值,乘以优势函数A[t],并附加一个约束,即旧策略分布和新策略分布的Kullback-LeiblerKL)散度的期望值被限制在一个用户指定的值δ以内:

这里的第一个方程是策略目标,第二个方程是一个附加约束,确保策略更新是渐进的,不会进行大幅度的策略更新,从而避免将策略推向参数空间中非常远的区域。

由于我们有两个方程需要联合优化,基于一阶优化算法(如 Adam 和 SGD)的方法将无法工作。相反,这些方程使用共轭梯度算法来求解,对第一个方程进行线性近似,对第二个方程进行二次近似。然而,这在数学上较为复杂,因此我们在本书中不详细展示。我们将继续介绍 PPO 算法,它在编码上相对简单。

学习 PPO

PPO 是对 TRPO 的扩展,2017 年由 OpenAI 的研究人员提出。PPO 也是一种基于策略的算法,既可以应用于离散动作问题,也可以应用于连续动作。它使用与 TRPO 相同的策略分布比率,但不使用 KL 散度约束。具体来说,PPO 使用三种损失函数并将其合并为一个。接下来我们将看到这三种损失函数。

PPO 损失函数

PPO 中涉及的三个损失函数中的第一个称为裁剪替代目标。令 rt 表示新旧策略概率分布的比率:

裁剪替代目标由以下方程给出,其中 A[t] 是优势函数,ε 是超参数;通常,ε = 0.1 或 0.2 被使用:

clip() 函数将比率限制在 1-ε1+ε 之间,从而保持比率在范围内。min() 函数是最小函数,确保最终目标是未裁剪目标的下界。

第二个损失函数是状态价值函数的 L2 范数:

第三个损失是策略分布的香农熵,它来源于信息理论:

现在我们将结合这三种损失函数。请注意,我们需要最大化 L^(clip)L^(entropy),但最小化 L^V。因此,我们将总的 PPO 损失函数定义为以下方程,其中 c[1]c[2] 是用于缩放项的正数常量:

请注意,如果我们在策略网络和价值网络之间共享神经网络参数,那么前述的 L^(PPO) 损失函数可以单独最大化。另一方面,如果我们为策略和价值使用独立的神经网络,那么我们可以像以下方程所示那样,分别定义损失函数,其中 L^(policy) 被最大化,而 L^(value) 被最小化:

请注意,在这种情况下,c[1] 常数在策略和价值使用独立神经网络的设置中并不需要。神经网络参数会在多个迭代步骤中根据一批数据点进行更新,更新步骤的数量由用户作为超参数指定。

使用 PPO 解决 MountainCar 问题

我们将使用 PPO 解决 MountainCar 问题。MountainCar 问题涉及一辆被困在山谷中的汽车。它必须加速以克服重力,并尝试驶出山谷,爬上陡峭的山墙,最终到达山顶的旗帜点。你可以从 OpenAI Gym 中查看 MountainCar 问题的示意图:gym.openai.com/envs/MountainCar-v0/

这个问题非常具有挑战性,因为智能体不能仅仅从山脚下全力加速并尝试到达旗帜点,因为山墙非常陡峭,重力会阻止汽车获得足够的动能。最优的解决方案是汽车先向后驶,然后踩下油门,积累足够的动能来克服重力,成功地驶出山谷。我们将看到,RL 智能体实际上学会了这个技巧。

我们将编写以下两个文件来使用 PPO 解决 MountainCar 问题:

  • class_ppo.py

  • train_test.py

编写 class_ppo.py 文件

现在,我们将编写class_ppo.py文件:

  1. 导入包:首先,我们将按照以下方式导入所需的包:
import numpy as np
import gym
import sys
  1. 设置神经网络初始化器:然后,我们将设置神经网络的参数(我们将使用两个隐藏层)以及权重和偏置的初始化器。正如我们在过去的章节中所做的那样,我们将使用 Xavier 初始化器来初始化权重,偏置的初始值则设置为一个小的正值:
nhidden1 = 64 
nhidden2 = 64 

xavier = tf.contrib.layers.xavier_initializer()
bias_const = tf.constant_initializer(0.05)
rand_unif = tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3)
regularizer = tf.contrib.layers.l2_regularizer(scale=0.0
  1. 定义 PPO :现在已经定义了PPO()类。首先,使用传递给类的参数定义__init__()构造函数。这里,sess是 TensorFlow 的sessionS_DIMA_DIM分别是状态和动作的维度;A_LRC_LR分别是演员和评论员的学习率;A_UPDATE_STEPSC_UPDATE_STEPS是演员和评论员的更新步骤数;CLIP_METHOD存储了 epsilon 值:
class PPO(object):

    def __init__(self, sess, S_DIM, A_DIM, A_LR, C_LR, A_UPDATE_STEPS, C_UPDATE_STEPS, CLIP_METHOD):
        self.sess = sess
        self.S_DIM = S_DIM
        self.A_DIM = A_DIM
        self.A_LR = A_LR
        self.C_LR = C_LR
        self.A_UPDATE_STEPS = A_UPDATE_STEPS
        self.C_UPDATE_STEPS = C_UPDATE_STEPS
        self.CLIP_METHOD = CLIP_METHOD
  1. 定义 TensorFlow 占位符:接下来,我们需要定义 TensorFlow 的占位符:tfs用于状态,tfdc_r用于折扣奖励,tfa用于动作,tfadv用于优势函数:
# tf placeholders
self.tfs = tf.placeholder(tf.float32, [None, self.S_DIM], 'state')
self.tfdc_r = tf.placeholder(tf.float32, [None, 1], 'discounted_r')
self.tfa = tf.placeholder(tf.float32, [None, self.A_DIM], 'action')
self.tfadv = tf.placeholder(tf.float32, [None, 1], 'advantage')
  1. 定义评论员:接下来定义评论员神经网络。我们使用状态(s[t])占位符self.tfs作为神经网络的输入。使用两个隐藏层,分别由nhidden1nhidden2个神经元组成,并使用relu激活函数(nhidden1nhidden2的值之前都设定为64)。输出层有一个神经元,将输出状态价值函数V(s[t]),因此输出层不使用激活函数。接下来,我们计算优势函数,作为折扣累积奖励(存储在self.tfdc_r占位符中)与刚才计算的self.v输出之间的差异。评论员损失被计算为 L2 范数,并且评论员使用 Adam 优化器进行训练,目标是最小化该 L2 损失。

请注意,这个损失与本章理论部分之前提到的*L^(value)*相同:

# critic
with tf.variable_scope('critic'):
    l1 = tf.layers.dense(self.tfs, nhidden1, activation=None, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
    l1 = tf.nn.relu(l1)
    l2 = tf.layers.dense(l1, nhidden2, activation=None, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
    l2 = tf.nn.relu(l2)

    self.v = tf.layers.dense(l2, 1, activation=None, kernel_initializer=rand_unif, bias_initializer=bias_const) 
    self.advantage = self.tfdc_r - self.v
    self.closs = tf.reduce_mean(tf.square(self.advantage))
    self.ctrain_op = tf.train.AdamOptimizer(self.C_LR).minimize(self.closs)
  1. 调用 _build_anet 函数:我们通过一个即将指定的_build_anet()函数来定义 actor。具体来说,该函数输出策略分布和模型参数列表。我们为当前策略调用一次此函数,再为旧策略调用一次。可以通过调用self.pimean()stddev()函数分别获得均值和标准差:
# actor
self.pi, self.pi_params = self._build_anet('pi', trainable=True) 
self.oldpi, self.oldpi_params = self._build_anet('oldpi', trainable=False)

self.pi_mean = self.pi.mean()
self.pi_sigma = self.pi.stddev()
  1. 示例动作:我们可以通过策略分布self.pi,使用sample()函数从 TensorFlow 的分布中采样动作:
with tf.variable_scope('sample_action'):
    self.sample_op = tf.squeeze(self.pi.sample(1), axis=0) 
  1. 更新旧策略参数:可以通过简单地将新策略的值赋给旧策略,使用 TensorFlow 的assign()函数来更新旧策略网络的参数。请注意,新策略已经过优化——旧策略仅仅是当前策略的一个副本,尽管是来自一次更新周期之前的。
with tf.variable_scope('update_oldpi'):
    self.update_oldpi_op = [oldp.assign(p) for p, oldp in zip(self.pi_params, self.oldpi_params)]
  1. 计算策略分布比率:策略分布比率在self.tfa动作处计算,并存储在self.ratio中。请注意,指数地,分布的对数差异等于分布的比率。然后将这个比率裁剪,限制在1-ε1+ε之间,正如理论部分中所解释的:
with tf.variable_scope('loss'):
    self.ratio = tf.exp(self.pi.log_prob(self.tfa) - self.oldpi.log_prob(self.tfa))
    self.clipped_ratio = tf.clip_by_value(self.ratio, 1.-self.CLIP_METHOD['epsilon'], 1.+self.CLIP_METHOD['epsilon'])
  1. 计算损失:前面提到的策略总损失包含三个损失,当策略和价值神经网络共享权重时,这些损失会结合在一起。然而,由于我们考虑到本章前面理论中提到的另一种设置,其中策略和价值各自拥有独立的神经网络,因此策略优化将有两个损失。第一个是未剪切比率与优势函数及其剪切类比的乘积的最小值——这个值存储在self.aloss中。第二个损失是香农熵,它是策略分布与其对数的乘积,所有值相加,并带上负号。这个项通过超参数c[1] = 0.01 进行缩放,并从损失中减去。暂时将熵损失项设置为零,就像在 PPO 论文中一样。我们可以考虑稍后加入此熵损失项,看看它是否对策略的学习有任何影响。我们使用 Adam 优化器。请注意,我们需要最大化本章前面提到的原始策略损失,但 Adam 优化器具有minimize()函数,因此我们在self.aloss中加入了负号(参见下面代码的第一行),因为最大化一个损失等同于最小化它的负值:
self.aloss = -tf.reduce_mean(tf.minimum(self.ratio*self.tfadv, self.clipped_ratio*self.tfadv))

# entropy 
entropy = -tf.reduce_sum(self.pi.prob(self.tfa) * tf.log(tf.clip_by_value(self.pi.prob(self.tfa),1e-10,1.0)),axis=1)
entropy = tf.reduce_mean(entropy,axis=0) 
self.aloss -= 0.0 #0.01 * entropy

with tf.variable_scope('atrain'):
    self.atrain_op = tf.train.AdamOptimizer(self.A_LR).minimize(self.aloss) 
  1. 定义 更新 函数:接下来定义update()函数,它将state(状态)a(动作)和r(奖励)作为参数。该函数涉及通过调用 TensorFlow 的self.update_oldpi_op操作来更新旧策略网络的参数。然后计算优势,结合状态和动作,利用A_UPDATE_STEPS(演员迭代次数)进行更新。接着,利用C_UPDATE_STEPS(评论者迭代次数)对评论者进行更新,运行 TensorFlow 会话以执行评论者训练操作:
def update(self, s, a, r):

    self.sess.run(self.update_oldpi_op)
    adv = self.sess.run(self.advantage, {self.tfs: s, self.tfdc_r: r})

    # update actor
    for _ in range(self.A_UPDATE_STEPS):
        self.sess.run(self.atrain_op, feed_dict={self.tfs: s, self.tfa: a, self.tfadv: adv})

    # update critic
    for _ in range(self.C_UPDATE_STEPS):
        self.sess.run(self.ctrain_op, {self.tfs: s, self.tfdc_r: r}) 
  1. 定义 _build_anet 函数:接下来我们将定义之前使用过的_build_anet()函数。它将计算策略分布,该分布被视为高斯分布(即正态分布)。它以self.tfs状态占位符作为输入,具有两个隐藏层,分别包含nhidden1nhidden2个神经元,并使用relu激活函数。然后,这个输出传递到两个输出层,这些层的输出数量是A_DIM动作维度,其中一个表示均值mu,另一个表示标准差sigma

请注意,动作的均值是有限制的,因此使用tanh激活函数,并进行小幅裁剪以避免极值;对于标准差,使用softplus激活函数,并将其偏移0.1以避免出现零的标准差。一旦我们获得了动作的均值和标准差,TensorFlow 的Normal分布被用来将策略视为高斯分布。我们还可以调用tf.get_collection()来获取模型参数,Normal分布和模型参数将从函数中返回:

    def _build_anet(self, name, trainable):
        with tf.variable_scope(name):
            l1 = tf.layers.dense(self.tfs, nhidden1, activation=None, trainable=trainable, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
            l1 = tf.nn.relu(l1)
            l2 = tf.layers.dense(l1, nhidden2, activation=None, trainable=trainable, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
            l2 = tf.nn.relu(l2)

            mu = tf.layers.dense(l2, self.A_DIM, activation=tf.nn.tanh, trainable=trainable, kernel_initializer=rand_unif, bias_initializer=bias_const)

            small = tf.constant(1e-6)
            mu = tf.clip_by_value(mu,-1.0+small,1.0-small) 

            sigma = tf.layers.dense(l2, self.A_DIM, activation=None, trainable=trainable, kernel_initializer=rand_unif, bias_initializer=bias_const)
            sigma = tf.nn.softplus(sigma) + 0.1 

            norm_dist = tf.distributions.Normal(loc=mu, scale=sigma)
        params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=name)
        return norm_dist, params
  1. 定义 choose_action 函数:我们还定义了一个choose_action()函数,从策略中采样以获取动作:
   def choose_action(self, s):
        s = s[np.newaxis, :]
        a = self.sess.run(self.sample_op, {self.tfs: s})
        return a[0]
  1. 定义 get_v 函数:最后,我们还定义了一个get_v()函数,通过在self.v上运行 TensorFlow 会话来返回状态值:
   def get_v(self, s):
        if s.ndim < 2: s = s[np.newaxis, :]
        vv = self.sess.run(self.v, {self.tfs: s})
        return vv[0,0]

class_ppo.py部分到此结束。接下来,我们将编写train_test.py

编写train_test.py文件

现在我们将编写train_test.py文件。

  1. 导入包:首先,我们导入所需的包:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import gym
import sys
import time

from class_ppo import *
  1. 定义函数:接着,我们定义了一个奖励塑造函数,该函数将根据良好或差劲的表现分别给予额外的奖励和惩罚。这样做是为了鼓励小车朝向位于山顶的旗帜一侧行驶,否则学习速度会变慢:
def reward_shaping(s_):

     r = 0.0

     if s_[0] > -0.4:
          r += 5.0*(s_[0] + 0.4)
     if s_[0] > 0.1: 
          r += 100.0*s_[0]
     if s_[0] < -0.7:
          r += 5.0*(-0.7 - s_[0])
     if s_[0] < 0.3 and np.abs(s_[1]) > 0.02:
          r += 4000.0*(np.abs(s_[1]) - 0.02)

     return r
  1. 接下来,我们选择MountainCarContinuous作为环境。我们将训练智能体的总集数设置为EP_MAX,并将其设置为1000GAMMA折扣因子设置为0.9,学习率为2e-4。我们使用32的批量大小,并在每个周期执行10次更新步骤。状态和动作维度分别存储在S_DIMA_DIM中。对于 PPO 的clip参数epsilon,我们使用0.1的值。train_test在训练时设置为0,在测试时设置为1
env = gym.make('MountainCarContinuous-v0')

EP_MAX = 1000
GAMMA = 0.9

A_LR = 2e-4
C_LR = 2e-4

BATCH = 32
A_UPDATE_STEPS = 10
C_UPDATE_STEPS = 10

S_DIM = env.observation_space.shape[0]
A_DIM = env.action_space.shape[0]

print("S_DIM: ", S_DIM, "| A_DIM: ", A_DIM)

CLIP_METHOD = dict(name='clip', epsilon=0.1)

# train_test = 0 for train; =1 for test
train_test = 0

# irestart = 0 for fresh restart; =1 for restart from ckpt file
irestart = 0

iter_num = 0

if (irestart == 0):
  iter_num = 0
  1. 我们创建一个 TensorFlow 会话,并命名为sess。创建一个PPO类的实例,命名为ppo。我们还创建了一个 TensorFlow 的保存器。然后,如果我们是从头开始训练,我们通过调用tf.global_variables_initializer()初始化所有模型参数;如果我们是从保存的智能体继续训练或进行测试,则从ckpt/model路径恢复:
sess = tf.Session()

ppo = PPO(sess, S_DIM, A_DIM, A_LR, C_LR, A_UPDATE_STEPS, C_UPDATE_STEPS, CLIP_METHOD)

saver = tf.train.Saver()

if (train_test == 0 and irestart == 0):
  sess.run(tf.global_variables_initializer())
else:
  saver.restore(sess, "ckpt/model") 
  1. 然后定义了一个主要的for loop,用于遍历集数。在循环内部,我们重置环境,并将缓冲区设置为空列表。终止布尔值done和时间步骤数t也被初始化:
for ep in range(iter_num, EP_MAX):

    print("-"*70)

    s = env.reset()

    buffer_s, buffer_a, buffer_r = [], [], []
    ep_r = 0

    max_pos = -1.0
    max_speed = 0.0
    done = False
    t = 0

在外部循环中,我们有一个内层的while循环来处理时间步。这个问题涉及较短的时间步,在这些时间步内,汽车可能没有显著移动,因此我们使用粘性动作,其中动作每8个时间步才从策略中采样一次。PPO类中的choose_action()函数会为给定的状态采样动作。为了进行探索,我们会在动作中加入小的高斯噪声,并将其限制在-1.01.0的范围内,这是MountainCarContinuous环境所要求的。然后,动作被输入到环境的step()函数中,后者将输出下一个s_状态、r奖励以及终止标志done。调用reward_shaping()函数来调整奖励。为了跟踪智能体推动极限的程度,我们还计算它在max_posmax_speed中分别的最大位置和速度:

    while not done: 

        env.render()

        # sticky actions
        #if (t == 0 or np.random.uniform() < 0.125): 
        if (t % 8 ==0):
          a = ppo.choose_action(s) 

        # small noise for exploration
        a += 0.1 * np.random.randn() 

        # clip
        a = np.clip(a, -1.0, 1.0)

        # take step 
        s_, r, done, _ = env.step(a)

        if s_[0] > 0.4:
            print("nearing flag: ", s_, a) 

        if s_[0] > 0.45:
          print("reached flag on mountain! ", s_, a) 
          if done == False:
             print("something wrong! ", s_, done, r, a)
             sys.exit() 

        # reward shaping 
        if train_test == 0:
          r += reward_shaping(s_)

        if s_[0] > max_pos:
           max_pos = s_[0]
        if s_[1] > max_speed:
           max_speed = s_[1]
  1. 如果我们处于训练模式,状态、动作和奖励会被追加到缓冲区。新的状态会被设置为当前状态,如果回合尚未结束,我们将继续进行下一个时间步。ep_r回合总奖励和t时间步数也会被更新:
if (train_test == 0):
    buffer_s.append(s)
    buffer_a.append(a)
    buffer_r.append(r) 

    s = s_
    ep_r += r
    t += 1

如果我们处于训练模式,当样本数量等于一个批次,或者回合已经结束时,我们将训练神经网络。为此,首先使用ppo.get_v获取新状态的状态值。然后,我们计算折扣奖励。缓冲区列表也会被转换为 NumPy 数组,并且缓冲区列表会被重置为空列表。接下来,这些bsbabr NumPy 数组将被用来更新ppo对象的演员和评论员网络:

if (train_test == 0):
    if (t+1) % BATCH == 0 or done == True:
        v_s_ = ppo.get_v(s_)
        discounted_r = []
        for r in buffer_r[::-1]:
            v_s_ = r + GAMMA * v_s_
            discounted_r.append(v_s_)
            discounted_r.reverse()

        bs = np.array(np.vstack(buffer_s))
        ba = np.array(np.vstack(buffer_a)) 
        br = np.array(discounted_r)[:, np.newaxis]

        buffer_s, buffer_a, buffer_r = [], [], []

        ppo.update(bs, ba, br)
  1. 如果我们处于测试模式,Python 会短暂暂停以便更好地进行可视化。如果回合已结束,while循环会通过break语句退出。然后,我们会在屏幕上打印最大的位置和速度值,并将它们以及回合奖励写入名为performance.txt的文件中。每 10 个回合,我们还会通过调用saver.save来保存模型:
    if (train_test == 1):
        time.sleep(0.1)

    if (done == True):
        print("values at done: ", s_, a)
        break

    print("episode: ", ep, "| episode reward: ", round(ep_r,4), "| time steps: ", t)
    print("max_pos: ", max_pos, "| max_speed:", max_speed)

    if (train_test == 0):
      with open("performance.txt", "a") as myfile:
        myfile.write(str(ep) + " " + str(round(ep_r,4)) + " " + str(round(max_pos,4)) + " " + str(round(max_speed,4)) + "\n")

    if (train_test == 0 and ep%10 == 0):
      saver.save(sess, "ckpt/model")

这标志着 PPO 编码的结束。接下来我们将在 MountainCarContinuous 上评估其性能。

评估性能

PPO 智能体通过以下命令进行训练:

python train_test.py

一旦训练完成,我们可以通过设置以下内容来测试智能体:

train_test = 1

然后,我们会再次运行python train_test.py。通过可视化智能体,我们可以观察到汽车首先向后移动,攀爬左侧的山。接着,它全速前进,获得足够的动能,成功越过右侧山峰上方的陡峭坡道。因此,PPO 智能体已经学会成功驶出山谷。

全油门

请注意,我们必须先倒车,然后踩下油门,才能获得足够的动能逃脱重力并成功驶出山谷。如果我们一开始就踩下油门,汽车还能够逃脱吗?让我们通过编写并运行mountaincar_full_throttle.py来验证。

现在,我们将动作设置为1.0,即全油门:

import sys
import numpy as np
import gym

env = gym.make('MountainCarContinuous-v0')

for _ in range(100):
  s = env.reset()
  done = False

  max_pos = -1.0
  max_speed = 0.0 
  ep_reward = 0.0

  while not done:
    env.render() 
    a = [1.0] # step on throttle
    s_, r, done, _ = env.step(a)

    if s_[0] > max_pos: max_pos = s_[0]
    if s_[1] > max_speed: max_speed = s_[1] 
    ep_reward += r

  print("ep_reward: ", ep_reward, "| max_pos: ", max_pos, "| max_speed: ", max_speed)

从训练过程中生成的视频可以看出,汽车无法逃脱重力的无情拉力,最终仍然被困在山谷的底部。

随机油门

如果我们尝试随机的油门值呢?我们将编写mountaincar_random_throttle.py,在-1.01.0的范围内进行随机操作:

import sys
import numpy as np
import gym

env = gym.make('MountainCarContinuous-v0')

for _ in range(100):
  s = env.reset()
  done = False

  max_pos = -1.0
  max_speed = 0.0 
  ep_reward = 0.0

  while not done:
    env.render() 
    a = [-1.0 + 2.0*np.random.uniform()] 
    s_, r, done, _ = env.step(a)

    if s_[0] > max_pos: max_pos = s_[0]
    if s_[1] > max_speed: max_speed = s_[1] 
    ep_reward += r

  print("ep_reward: ", ep_reward, "| max_pos: ", max_pos, "| max_speed: ", max_speed)

在这种情况下,汽车仍然无法逃脱重力,依然被困在山谷的底部。所以,RL 智能体需要明白,最优策略是先向后行驶,然后踩下油门,逃脱重力并到达山顶的旗帜处。

这标志着我们的 PPO MountainCar 练习的结束。

总结

在这一章中,我们介绍了 TRPO 和 PPO 两种 RL 算法。TRPO 涉及两个需要求解的方程,第一个方程是策略目标,第二个方程是对更新幅度的约束。TRPO 需要二阶优化方法,例如共轭梯度。为了简化这一过程,PPO 算法应运而生,其中策略比例被限制在一个用户指定的范围内,从而保持更新的渐进性。此外,我们还看到了使用从经验中收集的数据样本来更新演员和评论家,通过多次迭代进行训练。我们在 MountainCar 问题上训练了 PPO 智能体,这是一个具有挑战性的问题,因为演员必须先将汽车倒退上左边的山,然后加速以获得足够的动能克服重力,最终到达右边山顶的旗帜处。我们还发现,全油门策略或随机策略无法帮助智能体达到目标。

本章中,我们回顾了几种强化学习(RL)算法。在下一章,我们将应用 DDPG 和 PPO 来训练一个智能体,使其能自动驾驶汽车。

问题

  1. 我们可以在 TRPO 中应用 Adam 或 SGD 优化吗?

  2. 策略优化中的熵项有什么作用?

  3. 为什么我们要剪辑策略比例?如果剪辑参数 epsilon 较大,会发生什么?

  4. 为什么我们使用tanh激活函数作为mu的激活函数,而使用softplus作为 sigma 的激活函数?我们能否为 sigma 使用tanh激活函数?

  5. 奖励塑造在训练中总是有效吗?

  6. 测试一个已经训练好的智能体时,我们需要奖励塑造吗?

进一步阅读

第八章:深度强化学习应用于自动驾驶

自动驾驶是目前正在开发的最热门的技术革命之一。它将极大地改变人类对交通的看法,显著降低旅行成本并提高安全性。自动驾驶汽车开发社区已经使用了若干前沿算法来实现这一目标。这些算法包括但不限于感知、定位、路径规划和控制。感知涉及识别自动驾驶汽车周围的环境——行人、汽车、自行车等。定位是指在预先计算好的环境地图中识别汽车的确切位置(或者更精确地说,姿态)。路径规划,顾名思义,是规划自动驾驶汽车路径的过程,包括长期路径(比如从A点到B点)以及短期路径(比如接下来的 5 秒)。控制则是实际执行所需路径的过程,包括避让操作。特别地,强化学习RL)在自动驾驶的路径规划和控制中被广泛应用,适用于城市驾驶和高速公路驾驶。

在本章中,我们将使用开放赛车模拟器TORCS)来训练 RL 智能体,学习如何在赛道上成功驾驶。尽管 CARLA 模拟器更强大且具有逼真的渲染效果,但 TORCS 更易于使用,因此是一个很好的入门选择。完成本书后,鼓励感兴趣的读者尝试在 CARLA 模拟器上训练 RL 智能体。

本章将涉及以下主题:

  • 学习使用 TORCS

  • 训练深度确定性策略梯度DDPG)智能体来学习驾驶

  • 训练近端策略优化PPO)智能体

技术要求

本章的学习需要以下工具:

  • Python(版本 2 或 3)

  • NumPy

  • Matplotlib

  • TensorFlow(版本 1.4 或更高)

  • TORCS 赛车模拟器

汽车驾驶模拟器

在自动驾驶中应用强化学习(RL)需要使用强大的汽车驾驶模拟器,因为 RL 智能体不能直接在道路上进行训练。为此,研究社区开发了几款开源汽车驾驶模拟器,每款都有其优缺点。一些开源汽车驾驶模拟器包括:

学习使用 TORCS

我们将首先学习如何使用 TORCS 赛车模拟器,它是一个开源模拟器。你可以从 torcs.sourceforge.net/index.php?name=Sections&op=viewarticle&artid=3 获取下载说明,但以下是 Linux 系统下的关键步骤总结:

  1. sourceforge.net/projects/torcs/files/all-in-one/1.3.7/torcs-1.3.7.tar.bz2/download 下载 torcs-1.3.7.tar.bz2 文件

  2. 使用 tar xfvj torcs-1.3.7.tar.bz2 解包该包

  3. 执行以下命令:

    • cd torcs-1.3.7

    • ./configure

    • make

    • make install

    • make datainstall

  4. 默认安装目录为:

    • /usr/local/bin:TORCS 命令(该目录应包含在你的 PATH 中)

    • /usr/local/lib/torcs:TORCS 动态 libs(如果不使用 TORCS shell,目录必须包含在你的 LD_LIBRARY_PATH 中)

    • /usr/local/share/games/torcs:TORCS 数据文件

通过运行 torcs 命令(默认位置是 /usr/local/bin/torcs),你现在可以看到 TORCS 模拟器开启。然后可以选择所需的设置,包括选择汽车、赛道等。模拟器也可以作为视频游戏来玩,但我们更感兴趣的是用它来训练 RL 智能体。

状态空间

接下来,我们将定义 TORCS 的状态空间。表 2:可用传感器描述(第二部分)。范围与其单位(如果定义了单位)一起报告,来自 模拟汽车赛车锦标赛:竞赛软件手册 文档,在 arxiv.org/pdf/1304.1672.pdf 中提供了一个模拟器可用状态参数的总结。我们将使用以下条目作为我们的状态空间;方括号中的数字表示条目的大小:

  • angle:汽车方向与赛道之间的角度(1)

  • track:这将给我们每 10 度测量一次的赛道末端,从 -90 到 +90 度;它有 19 个实数值,包括端点值(19)

  • trackPos:汽车与赛道轴之间的距离(1)

  • speedX:汽车在纵向方向的速度(1)

  • speedY:汽车在横向方向的速度(1)

  • speedZ:汽车在 Z 方向的速度;其实我们并不需要这个,但现在先保留它(1)

  • wheelSpinVel:汽车四个轮子的旋转速度(4)

  • rpm:汽车引擎的转速(1)

请参考之前提到的文档以更好地理解前述变量,包括它们的允许范围。总结所有实数值条目的数量,我们注意到我们的状态空间是一个实数值向量,大小为 1+19+1+1+1+1+4+1 = 29。我们的动作空间大小为3:转向、加速和刹车。转向范围为 [-1,1],加速范围为 [0,1],刹车也是如此。

支持文件

开源社区还开发了两个 Python 文件,将 TORCS 与 Python 接口,以便我们能够从 Python 命令调用 TORCS。此外,为了自动启动 TORCS,我们需要另一个sh文件。这三个文件总结如下:

  • gym_torcs.py

  • snakeoil3_gym.py

  • autostart.sh

这些文件包含在本章的代码文件中(github.com/PacktPublishing/TensorFlow-Reinforcement-Learning-Quick-Start-Guide),但也可以通过 Google 搜索获取。在gym_torcs.py的第~130-160 行中,设置了奖励函数。你可以看到以下行,这些行将原始的模拟器状态转换为 NumPy 数组:

# Reward setting Here #######################################
# direction-dependent positive reward
track = np.array(obs['track'])
trackPos = np.array(obs['trackPos'])
sp = np.array(obs['speedX'])
damage = np.array(obs['damage'])
rpm = np.array(obs['rpm'])

然后,奖励函数被设置如下。请注意,我们根据沿轨道的纵向速度(角度项的余弦值)给予奖励,并惩罚横向速度(角度项的正弦值)。轨道位置也会被惩罚。理想情况下,如果这是零,我们将处于轨道的中心,而*+1-1*的值表示我们在轨道的边缘,这是不希望发生的,因此会受到惩罚:

progress = sp*np.cos(obs['angle']) - np.abs(sp*np.sin(obs['angle'])) - sp * np.abs(obs['trackPos'])
reward = progress

如果汽车偏离轨道或智能体的进度卡住,我们使用以下代码终止该回合:

if (abs(track.any()) > 1 or abs(trackPos) > 1): # Episode is terminated if the car is out of track
    print("Out of track ")
    reward = -100 #-200
    episode_terminate = True
    client.R.d['meta'] = True

if self.terminal_judge_start < self.time_step: # Episode terminates if the progress of agent is small
    if progress < self.termination_limit_progress:
         print("No progress", progress)
         reward = -100 # KAUSHIK ADDED THIS
         episode_terminate = True
         client.R.d['meta'] = True

我们现在已经准备好训练一个 RL 智能体,让它在 TORCS 中成功驾驶。我们将首先使用 DDPG 智能体。

训练 DDPG 智能体学习驾驶

大部分 DDPG 代码与我们在第五章中看到的*深度确定性策略梯度(DDPG)*相同;这里只总结其中的差异。

编写 ddpg.py

我们的 TORCS 状态维度是29,动作维度是3;这些在ddpg.py中设置,如下所示:

state_dim = 29
action_dim = 3
action_bound = 1.0

编写 AandC.py

actor 和 critic 文件AandC.py也需要进行修改。特别地,ActorNetwork类中的create_actor_network被修改为拥有两个隐藏层,分别包含400300个神经元。此外,输出由三个动作组成:steering(转向)、acceleration(加速)和brake(刹车)。由于转向在[-1,1]范围内,因此使用tanh激活函数;加速和刹车在[0,1]范围内,因此使用sigmoid激活函数。然后,我们在轴维度1上将它们concat(连接),这就是我们 actor 策略的输出:

   def create_actor_network(self, scope):
       with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
          state = tf.placeholder(name='a_states', dtype=tf.float32, shape=[None, self.s_dim])

          net = tf.layers.dense(inputs=state, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet1') 
          net = tf.nn.relu(net)

          net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet2')
          net = tf.nn.relu(net)

          steering = tf.layers.dense(inputs=net, units=1, activation=tf.nn.tanh, kernel_initializer=rand_unif, bias_initializer=binit, name='steer') 
          acceleration = tf.layers.dense(inputs=net, units=1, activation=tf.nn.sigmoid, kernel_initializer=rand_unif, bias_initializer=binit, name='acc') 
          brake = tf.layers.dense(inputs=net, units=1, activation=tf.nn.sigmoid, kernel_initializer=rand_unif, bias_initializer=binit, name='brake') 

          out = tf.concat([steering, acceleration, brake], axis=1) 

          return state, out

同样,CriticNetwork类的create_critic_network()函数被修改为拥有两个隐藏层,分别包含400300个神经元。这在以下代码中显示:

    def create_critic_network(self, scope):
        with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
           state = tf.placeholder(name='c_states', dtype=tf.float32, shape=[None, self.s_dim]) 
           action = tf.placeholder(name='c_action', dtype=tf.float32, shape=[None, self.a_dim]) 

           net = tf.concat([state, action],1) 

           net = tf.layers.dense(inputs=net, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='cnet1') 
           net = tf.nn.relu(net)

           net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='cnet2') 
           net = tf.nn.relu(net)

           out = tf.layers.dense(inputs=net, units=1, activation=None, kernel_initializer=rand_unif, bias_initializer=binit, name='cnet_out') 
           return state, action, out

需要进行的其他更改在TrainOrTest.py中,我们接下来将查看这些更改。

编写 TrainOrTest.py

gym_torcs导入 TORCS 环境,以便我们能够在其上训练 RL 智能体:

  1. 导入 TORCS:从gym_torcs导入 TORCS 环境,代码如下:
from gym_torcs import TorcsEnv
  1. env变量:使用以下命令创建 TORCS 环境变量:
    # Generate a Torcs environment
    env = TorcsEnv(vision=False, throttle=True, gear_change=False)
  1. 重新启动 TORCS:由于 TORCS 已知存在内存泄漏错误,每100个回合后使用relaunch=True重新启动环境;否则,可以按如下方式在不带参数的情况下重置环境:
if np.mod(i, 100) == 0:
    ob = env.reset(relaunch=True) #relaunch TORCS every N episodes  due to a memory leak error
else:
    ob = env.reset()
  1. 堆叠状态空间:使用以下命令堆叠 29 维的状态空间:
s = np.hstack((ob.angle, ob.track, ob.trackPos, ob.speedX, ob.speedY, ob.speedZ, ob.wheelSpinVel/100.0, ob.rpm))
  1. 每回合的时间步数:选择每个回合运行的时间步数msteps。在前100个回合中,代理学习尚未深入,因此你可以选择每回合100个时间步;对于后续回合,我们将其线性增加,直到达到max_steps的上限。

这个步骤并不是关键,代理的学习并不依赖于我们为每个回合选择的步数。可以自由尝试设置msteps

选择每回合的时间步数,如下所示:

msteps = max_steps
if (i < 100):
    msteps = 100
elif (i >=100 and i < 200):
    msteps = 100 + (i-100)*9
else: 
    msteps = 1000 + (i-200)*5
    msteps = min(msteps, max_steps)
  1. 全速前进:在前10个回合中,我们施加全速前进以预热神经网络参数。只有在此之后,我们才开始使用演员的策略。需要注意的是,TORCS 通常在大约 1,500 到 2,000 个回合后学习完成,所以前10个回合对后期的学习影响不大。通过以下方式应用全速前进来预热神经网络参数:
# first few episodes step on gas! 
if (i < 10):
    a[0][0] = 0.0
    a[0][1] = 1.0
    a[0][2] = 0.0

这就是为了让 DDPG 代理在 TORCS 中玩的代码需要做的更改。其余的代码与第五章中讲解的*深度确定性策略梯度(DDPG)*相同。我们可以使用以下命令来训练代理:

python ddpg.py

输入1进行训练;输入0是为了测试一个预训练的代理。训练可能需要 2 到 5 天,具体取决于使用的计算机速度。但这是一个有趣的问题,值得花费时间和精力。每个回合的步数以及奖励都会存储在analysis_file.txt文件中,我们可以用它来绘制图表。每个回合的时间步数如下图所示:

图 1:TORCS 每回合的时间步数(训练模式)

我们可以看到,经过大约 600 次训练后,汽车已经学会了合理地驾驶,而在大约 1500 次训练后,驾驶更加高效。大约 300 个时间步对应赛道的一圈。因此,代理在训练结束时能够驾驶七到八圈以上而不会中途终止。关于 DDPG 代理驾驶的精彩视频,请参考以下 YouTube 链接:www.youtube.com/watch?v=ajomz08hSIE

训练 PPO 代理

我们之前展示了如何训练一个 DDPG 智能体在 TORCS 中驾驶汽车。如何使用 PPO 智能体则留作有兴趣的读者的练习。这是一个不错的挑战。来自第七章的 PPO 代码,信任区域策略优化与近端策略优化,可以复用,只需对 TORCS 环境做必要的修改。TORCS 的 PPO 代码也已提供在代码库中(github.com/PacktPublishing/TensorFlow-Reinforcement-Learning-Quick-Start-Guide),有兴趣的读者可以浏览它。TORCS 中 PPO 智能体驾驶汽车的酷炫视频可以在以下 YouTube 视频中查看:youtu.be/uE8QaJQ7zDI

另一个挑战是对于有兴趣的读者来说,使用信任区域策略优化TRPO)解决 TORCS 赛车问题。如果有兴趣,也可以尝试一下!这是掌握 RL 算法的一种方法。

总结

本章中,我们展示了如何应用强化学习(RL)算法训练一个智能体,使其学会自主驾驶汽车。我们安装了 TORCS 赛车模拟器,并学习了如何将其与 Python 接口,以便我们可以训练 RL 智能体。我们还深入探讨了 TORCS 的状态空间以及这些术语的含义。随后,使用 DDPG 算法训练一个智能体,成功学会在 TORCS 中驾驶。TORCS 中的视频渲染效果非常酷!训练后的智能体能够成功驾驶超过七到八圈。最后,我们也探索了使用 PPO 算法解决同样的自主驾驶问题,并将其作为练习留给有兴趣的读者;相关代码已提供在本书的代码库中。

这也就结束了本章以及本书的内容。可以随时在线阅读更多关于 RL 在自主驾驶和机器人学应用的材料。这是目前学术界和工业界研究的热门领域,并且得到了充分的资金支持,相关领域也有许多职位空缺。祝你一切顺利!

问题

  1. 为什么不能使用 DQN 来解决 TORCS 问题?

  2. 我们使用了 Xavier 权重初始化器来初始化神经网络的权重。你知道还有哪些其他的权重初始化方法?使用它们训练的智能体表现如何?

  3. 为什么在奖励函数中使用abs()函数?为什么它用于最后两个项而不是第一个项?

  4. 如何确保比视频中观察到的驾驶更平稳?

  5. 为什么在 DDPG 中使用重放缓冲区而在 PPO 中不使用?

深入阅读

第九章:评估

第一章

  1. 离策略强化学习算法需要一个重放缓冲区。我们从重放缓冲区中抽取一个小批量的经验,用它来训练 DQN 中的Q(s,a) 状态值函数以及 DDPG 中的演员策略。

  2. 我们对奖励进行折扣,因为关于智能体的长期表现存在更多不确定性。因此,立即奖励具有更高的权重,下一时间步获得的奖励的权重相对较低,再下一时间步的奖励权重更低,依此类推。

  3. 如果γ > 1,智能体的训练将不稳定,智能体将无法学习到最优策略。

  4. 基于模型的强化学习智能体有潜力表现得很好,但不能保证它一定会比基于无模型的强化学习智能体表现得更好,因为我们构建的环境模型未必总是一个好的模型。而且,构建一个足够准确的环境模型也非常困难。

  5. 在深度强化学习中,深度神经网络用于表示Q(s,a) 和演员的策略(在 Actor-Critic 设置中是这样的)。在传统的强化学习算法中,使用的是表格型的Q(s,a),但当状态的数量非常大时,这种方式无法应用,而这通常是大多数问题的情况。

第三章

  1. 在 DQN 中,重放缓冲区用于存储过去的经验,从中抽取一个小批量的数据,并用来训练智能体。

  2. 目标网络有助于提高训练的稳定性。这是通过保持一个额外的神经网络来实现的,该网络的权重通过使用主神经网络权重的指数移动平均来更新。或者,另一种广泛使用的方法是每隔几千步就将主神经网络的权重复制到目标网络中。

  3. 在 Atari Breakout 问题中,单一帧作为状态是无法提供帮助的。这是因为单一帧无法提取时间信息。例如,单一帧中无法得出球的运动方向。但如果我们将多帧叠加起来,就可以确定球的速度和加速度。

  4. L2 损失已知会对异常值产生过拟合。因此,更倾向使用 Huber 损失,因为它结合了 L2 和 L1 损失。详见维基百科:en.wikipedia.org/wiki/Huber_loss

  5. 也可以使用 RGB 图像。不过,我们需要为神经网络的第一隐藏层增加额外的权重,因为现在在状态堆栈中的四帧每一帧都有三个通道。这种对于状态空间的更精细的细节在 Atari 中并不是必须的。然而,RGB 图像可以在其他应用中提供帮助,例如在自动驾驶和/或机器人技术中。

第四章

  1. DQN 被认为会高估状态-动作值函数Q(s,a)。为了解决这个问题,引入了 DDQN。DDQN 在高估*Q(s,a)*方面比 DQN 少遇到问题。

  2. 对抗网络架构为优势函数和状态价值函数提供了独立的流。然后,这些流被组合起来得到Q(s,a)。这种分支后再合并的方式被观察到有助于 RL 代理的训练更加稳定。

  3. 优先经验回放PER)赋予代理表现较差的经验样本更高的重要性,因此这些样本比代理表现良好的其他样本被采样的频率更高。通过频繁使用表现较差的样本,代理能够更频繁地解决自身的弱点,从而 PER 加速了训练。

  4. 在一些计算机游戏中,例如 Atari Breakout,模拟器的帧率过高。如果在每个时间步长中都从策略中采样一个独立的动作,代理的状态可能在一个时间步长内变化不够,因为这个时间步长太小。因此,使用了粘性动作,其中相同的动作会在有限但固定的时间步数内重复,例如n,并且在这些 n 个时间步内累计的总奖励将作为执行该动作的奖励。在这些 n 个时间步内,代理的状态已经发生了足够的变化,可以评估所采取的动作的有效性。n 值过小会阻止代理学习到良好的策略;同样,n 值过大也会成为一个问题。必须选择合适的时间步长数,在这些时间步长内执行相同的动作,这取决于所使用的模拟器。

第五章

  1. DDPG 是一种基于策略外的算法,因为它使用了回放缓冲区。

  2. 一般来说,演员和评论家的隐藏层数量以及每个隐藏层的神经元数量是相同的,但这不是强制要求。请注意,演员和评论家的输出层是不同的,演员的输出数量等于动作的数量;评论家只有一个输出。

  3. DDPG 用于连续控制,即当动作是连续且为实数值时。Atari Breakout 有离散动作,因此 DDPG 不适用于 Atari Breakout。

  4. 我们使用relu激活函数,因此偏置初始化为小的正值,以便它们在训练开始时就能够激活并允许梯度回传。

  5. 这是一个练习。请参见gym.openai.com/envs/InvertedDoublePendulum-v2/

  6. 这也是一个练习。注意当第一层的神经元数量逐渐减少时,学习会发生什么变化。一般来说,信息瓶颈不仅在强化学习(RL)环境中观察到,任何深度学习(DL)问题中也会出现。

第六章

  1. 异步优势演员-评论家代理A3C)是一种基于策略的算法,因为我们并没有使用回放缓冲区来采样数据。然而,使用了一个临时缓冲区来收集即时样本,这些样本会用来训练一次,然后缓冲区会被清空。

  2. Shannon 熵项被用作正则化器——熵越高,策略越好。

  3. 当使用过多工作线程时,训练可能会变慢并崩溃,因为内存有限。然而,如果你可以访问大量的处理器集群,那么使用大量的工作线程/进程将会有所帮助。

  4. Softmax 被用于策略网络中,以获取不同动作的概率。

  5. 优势函数被广泛使用,因为它降低了策略梯度的方差。《A3C 论文》第 3 节arxiv.org/pdf/1602.01783.pdf)对此有更多说明。

  6. 这是一个练习。

第七章

  1. 信任区域策略优化TRPO)具有目标函数和约束条件。因此,它需要二阶优化方法,如共轭梯度法。SGD 和 Adam 不适用于 TRPO。

  2. 熵项有助于正则化,它允许智能体进行更多探索。

  3. 我们剪切策略比率,以限制每次更新步骤对策略的变化量。如果这个剪切参数 epsilon 设置得较大,则每次更新中策略可能发生剧烈变化,这可能导致一个次优策略,因为智能体的策略变得更加嘈杂并且波动过大。

  4. 动作的取值范围在负值和正值之间,因此对 mu 使用了 tanh 激活函数。对于 sigma,使用 softplus 作为 sigma,它始终为正。不能对 sigma 使用 tanh 函数,因为 tanh 可能会导致 sigma 为负值,这是没有意义的!

  5. 奖励塑形通常有助于训练。但如果操作不当,它将无法帮助训练。你必须确保奖励塑形能够保持 reward 函数的密度并处于适当的范围内。

  6. 不,奖励塑形仅在训练过程中使用。

第八章

  1. TORCS 是一个连续控制问题。DQN 仅适用于离散动作,因此不能用于 TORCS。

  2. 初始化是另一种初始化策略;你也可以使用指定范围内的 minmax 值进行随机均匀初始化;另一种方法是从一个均值为零、sigma 值已指定的高斯分布中采样。感兴趣的读者应尝试这些不同的初始化方法,并比较智能体的表现。

  3. abs() 函数在 reward 函数中使用,因为我们对偏离中心的横向漂移进行平等惩罚(无论是左侧还是右侧)。第一个项是纵向速度,因此不需要 abs() 函数。

  4. 为了探索而加入的高斯噪声可以随着回合数的增加逐渐减少,这可以导致更平稳的驾驶体验。当然,你还可以尝试许多其他技巧!

  5. DDPG 是一个脱离策略的算法,但近端策略优化PPO)是一个基于策略的强化学习算法。因此,DDPG 需要一个回放缓冲区来存储过去的经验样本,而 PPO 则不需要回放缓冲区。