本文概述
让我们深入研究强化学习。在本文中, 我们将使用TensorFlow, TensorBoard, Keras和OpenAI Gym等现代库解决一个具体问题。你将看到如何实现一种称为深度$ Q $学习的基本算法, 以了解其内部工作原理。关于硬件, 整个代码将在典型的PC上运行并使用所有找到的CPU内核(由TensorFlow开箱即用)。
问题称为山车:汽车位于一维轨道上, 位于两座山之间。目的是要开车到右边的山(到达旗帜)。但是, 汽车的发动机强度不足以单程通过。因此, 成功的唯一方法就是来回驱动以建立动力。
选择该问题的原因是, 它很容易在单个CPU内核上几分钟内找到具有强化学习的解决方案。但是, 它很复杂, 无法成为一个好的代表。
首先, 我将简要概述强化学习的一般功能。然后, 我们将介绍基本术语并用它们表达我们的问题。之后, 我将介绍深层的$ Q $学习算法, 并将其实现以解决该问题。
强化学习基础
用最简单的话来说, 强化学习就是通过反复试验来学习。主要角色称为”代理人”, 它将成为我们所要解决的问题。代理在环境中进行操作, 并获得新的观察结果和对该操作的奖励。导致更大奖励的行动得到了加强, 因此得名。与计算机科学中的许多其他事物一样, 这一发现也受到观察活物的启发。
下图概述了代理与环境的交互:
代理对执行的动作进行观察并得到奖励。然后, 它采取另一种措施并采取第二步。现在, 环境返回了(可能)略有不同的观察和奖励。这一直持续到达到终端状态为止, 这是通过向代理发送”完成”来发出的。观察>动作> next_observations>奖励的整个序列称为情节(或轨迹)。
回到我们的山地车:我们的车是代理商。环境是一维山脉的黑匣子世界。汽车的动作归结为一个数字:如果为正数, 则引擎将汽车向右推。如果为负, 则将汽车推向左侧。特工通过观察来感知环境:汽车的X位置和速度。如果我们想让汽车在山顶上行驶, 则可以通过一种便捷的方式定义奖励:代理商未达到目标的每一步都会获得-1的奖励。当达到目标时, 情节结束。因此, 实际上, 代理人因不在我们希望的位置而受到惩罚。他越快到达目的地, 对他越好。特工的目标是使总奖励最大化, 这是一个情节的总和。因此, 如果它在例如110步之后达到了期望的点, 它将收到-110的总回报, 这对于Mountain Car来说将是一个很好的结果, 因为如果未达到目标, 则将受到200步的惩罚(因此, 返回-200)。
这是整个问题的表述。现在, 我们可以将其提供给算法, 这些算法已经足够强大, 可以在几分钟内(如果调整得很好)解决此类问题。值得注意的是, 我们没有告诉代理商如何实现目标。我们甚至不提供任何提示(启发式)。代理商将找到一种自己取胜的方法(策略)。
搭建环境
首先, 将整个教程代码复制到磁盘上:
git clone https://github.com/AdamStelmaszczyk/rl-tutorial
cd rl-tutorial
现在, 我们需要安装将要使用的Python软件包。为了不将它们安装在你的用户空间中(并避免发生冲突), 我们将使其清洁并将其安装在conda环境中。如果尚未安装conda, 请遵循https://conda.io/docs/user-guide/install/index.html。
要创建我们的conda环境:
conda create -n tutorial python=3.6.5 -y
要激活它:
source activate tutorial
你应该在shell提示符附近看到(教程)。这意味着名称为” tutorial”的conda环境处于活动状态。从现在开始, 所有命令应在该conda环境中执行。
现在, 我们可以将所有依赖项安装在密封的conda环境中:
pip install -r requirements.txt
我们已经完成了安装, 所以让我们运行一些代码。我们不需要自己实施Mountain Car环境; OpenAI Gym库提供了该实现。让我们看看我们的环境中的一个随机代理(采取随机行动的代理):
import gym
env = gym.make('MountainCar-v0')
done = True
episode = 0
episode_return = 0.0
for episode in range(5):
for step in range(200):
if done:
if episode > 0:
print("Episode return: ", episode_return)
obs = env.reset()
episode += 1
episode_return = 0.0
env.render()
else:
obs = next_obs
action = env.action_space.sample()
next_obs, reward, done, _ = env.step(action)
episode_return += reward
env.render()
这是see.py文件;运行它, 执行:
python see.py
你应该看到一辆汽车来回随机行驶。每个情节将包括200个步骤;总收益为-200。
现在我们需要用更好的东西代替随机动作。一个人可以使用许多算法。对于入门教程, 我认为一种称为$ Q $深度学习的方法非常合适。理解该方法为学习其他方法奠定了坚实的基础。
深入的$ Q $学习
我们将使用的算法由Mnih等人于2013年首次描述。通过深度强化学习玩Atari, 并在两年后通过深度强化学习完善了人的水平控制。基于这些结果, 还进行了许多其他工作, 包括当前最先进的算法Rainbow(2017):
图片来源:https://arxiv.org/abs/1710.02298
Rainbow在许多Atari 2600游戏中都达到了超人的性能。我们将专注于基本的DQN版本, 并尽可能减少一些其他改进, 以使本教程保持合理的大小。
策略通常表示为$π(s)$, 是一种函数, 它返回在给定状态$ s $中采取个别行动的概率。因此, 例如, 针对任何状态的随机Mountain Car策略都会返回:左50%, 右50%。在游戏过程中, 我们从该策略(分发)中采样以获取实际操作。
$ Q $-学习(Q代表质量)是指表示为$Q_π(s, a)$的动作值函数。它遵循特定策略$π$, 返回给定状态$ s $的总收益, 并选择操作$ a $。总回报是一个情节(轨迹)中所有奖励的总和。
如果我们知道最优的$ Q $函数, 表示为$ Q ^ * $, 我们可以轻松解决游戏。我们只需要遵循$ Q ^ * $的最高值, 即最高预期收益的操作即可。这保证了我们将获得最高的回报。
但是, 我们通常不知道$ Q ^ * $。在这种情况下, 我们可以通过与环境的交互来近似(或”学习”)它。这是名称中的” $ Q $-学习”部分。其中也包含” deep”一词, 因为为了近似该功能, 我们将使用深度神经网络, 它们是通用函数逼近器。近似$ Q $值的深度神经网络被称为Deep Q网络(DQN)。在简单的环境中(状态的数量适合内存), 人们可以只使用一张表而不是神经网络来表示$ Q $函数, 在这种情况下, 它将被称为”表格$ Q $学习”。
因此, 我们现在的目标是近似$ Q ^ * $函数。我们将使用Bellman方程:
$ s’$是$ s $之后的状态。 $γ$(伽玛), 通常为0.99, 是折扣因子(这是一个超参数)。它对未来奖励的重视程度较小(因为与我们不完善的$ Q $相比, 即时奖励的确定性较弱)。贝尔曼方程式是深度$ Q $学习的核心。它说, 给定状态和动作的$ Q $值是采取行动$ a $后获得的报酬$ r $, 再加上我们进入$ s’$的州的最高$ Q $值。在某种意义上说, 最高的就是我们选择的动作$ a’$, 这会导致$ s’$的最高总回报。
通过Bellman方程, 我们可以使用监督学习来近似$ Q ^ * $。 $ Q $函数将由表示为$θ$(theta)的神经网络权重表示(参数化)。一个简单的实现将状态和动作作为网络输入和输出Q值。低效率是, 如果我们想知道给定状态下所有动作的$ Q $值, 我们需要调用$ Q $的次数与存在动作的次数相同。有一种更好的方法:仅将状态作为输入, 并为所有可能的操作输出$ Q $值。因此, 我们只需一次向前传递就可以获得所有动作的$ Q $值。
我们开始使用随机权重训练$ Q $网络。从环境中, 我们获得了许多过渡(或”经验”)。这些是(状态, 动作, 下一个状态, 奖励)的元组, 或者简而言之, 是($ s $, $ a $, $ s’$, $ r $)的元组。我们将数千个存储在称为”体验重播”的环形缓冲区中。然后, 我们希望贝尔曼方程式能够满足他们的需求, 从该缓冲区中采样经验。我们本可以跳过缓冲区并逐一应用经验(这称为”在线”或”在线策略”);问题在于, 后续体验彼此之间高度相关, 并且DQN在发生这种情况时训练不佳。这就是引入体验重播(“离线”, “非策略”方法)以打破这种数据关联的原因。我们最简单的环形缓冲区实现的代码可以在replay_buffer.py文件中找到, 我建议你阅读它。
首先, 由于我们的神经网络权重是随机的, 因此Bellman方程的左侧值将远离右侧。平方差将是我们的损失函数。我们将通过更改神经网络权重$θ$来最小化损失函数。让我们写下损失函数:
这是重写的贝尔曼方程式。假设我们从Mountain Car体验重播中采样了一种体验($ s $, 左, $ s’$, -1)。例如, 我们通过状态为s $的$ Q $网络进行前向传递, 而如果采取行动, 则传递给我们的值为-120。因此, $ Q(s, \ textrm {left})= -120 $。然后, 我们将$ s’$馈送到网络, 这使我们得到-例如, 左侧为-130, 右侧为-122。因此, 显然$ s’$的最佳操作是正确的, 因此$ \ textrm {max} _ {a’} Q(s’, a’)= -122 $。我们知道$ r $, 这是实际的奖励, 为-1。因此我们的$ Q $网络预测略有错误, 因为$ L(θ)= [-120-1 + 0.99⋅122] ^ 2 =(-0.22 ^ 2)= 0.0484 $。因此, 我们向后传播误差并稍微校正权重$θ$。如果我们根据相同的经验再次计算损失, 那么损失会更低。
在进行编码之前的一项重要观察。请注意, 要更新DQN, 我们将对DQN…本身进行两次正向传递。这通常会导致学习不稳定。为了缓解这种情况, 对于下一个状态的$ Q $预测, 我们不使用相同的DQN。我们使用了较旧的版本, 在代码中将其称为target_model(而不是model, 它是主要的DQN)。因此, 我们有了一个稳定的目标。我们通过将target_model设置为每1000步对模型权重进行更新。但是模型会更新每一步。
让我们看一下创建DQN模型的代码:
def create_model(env):
n_actions = env.action_space.n
obs_shape = env.observation_space.shape
observations_input = keras.layers.Input(obs_shape, name='observations_input')
action_mask = keras.layers.Input((n_actions, ), name='action_mask')
hidden = keras.layers.Dense(32, activation='relu')(observations_input)
hidden_2 = keras.layers.Dense(32, activation='relu')(hidden)
output = keras.layers.Dense(n_actions)(hidden_2)
filtered_output = keras.layers.multiply([output, action_mask])
model = keras.models.Model([observations_input, action_mask], filtered_output)
optimizer = keras.optimizers.Adam(lr=LEARNING_RATE, clipnorm=1.0)
model.compile(optimizer, loss='mean_squared_error')
return model
首先, 该功能从给定的OpenAI Gym环境中获取动作和观察空间的尺寸。例如, 有必要知道我们的网络将有多少输出。它必须等于动作数。动作是一种热门编码:
def one_hot_encode(n, action):
one_hot = np.zeros(n)
one_hot[int(action)] = 1
return one_hot
因此, (例如)左侧为[1, 0], 右侧为[0, 1]。
我们可以看到观察作为输入传递。我们还将传递action_mask作为第二个输入。为什么?在计算$ Q(s, a)$时, 我们只需要知道一个给定动作的$ Q $值, 而不是全部。 action_mask包含1个要传递给DQN输出的动作。如果action_mask的某个动作的值为0, 则相应的$ Q $值将在输出上清零。 filtered_output层正在执行此操作。如果我们想要所有的$ Q $值(用于最大计算), 我们可以只传递所有这些值。
该代码使用keras.layers.Dense定义一个完全连接的层。 Keras是在TensorFlow之上的用于更高级别抽象的Python库。在后台, Keras创建了一个TensorFlow图, 其中包含偏差, 适当的权重初始化以及其他低级内容。我们本可以使用原始的TensorFlow来定义图, 但它不会是一味的。
因此, 观察结果通过ReLU(整流线性单位)激活传递到了第一个隐藏层。 ReLU(x)只是$ \ textrm {max}(0, x)$函数。该层与另一个相同的第二层(hidden_2)完全连接。输出层将神经元的数量减少到动作的数量。最后, 我们有filtered_output, 它将输出乘以action_mask。
为了找到$θ$权重, 我们将使用名为” Adam”的优化器, 该优化器具有均方误差损失。
有了模型, 我们可以使用它来预测给定状态观测值的$ Q $值:
def predict(env, model, observations):
action_mask = np.ones((len(observations), env.action_space.n))
return model.predict(x=[observations, action_mask])
我们希望所有操作都具有$ Q $值, 因此action_mask是1的向量。
为了进行实际的训练, 我们将使用fit_batch():
def fit_batch(env, model, target_model, batch):
observations, actions, rewards, next_observations, dones = batch
# Predict the Q values of the next states. Passing ones as the action mask.
next_q_values = predict(env, target_model, next_observations)
# The Q values of terminal states is 0 by definition.
next_q_values[dones] = 0.0
# The Q values of each start state is the reward + gamma * the max next state Q value
q_values = rewards + DISCOUNT_FACTOR_GAMMA * np.max(next_q_values, axis=1)
one_hot_actions = np.array([one_hot_encode(env.action_space.n, action) for action in actions])
history = model.fit(
x=[observations, one_hot_actions], y=one_hot_actions * q_values[:, None], batch_size=BATCH_SIZE, verbose=0, )
return history.history['loss'][0]
批次包含BATCH_SIZE经验。 next_q_values是$ Q(s, a)$。 q_values是Bellman方程中的$ r +γ\ space \ textrm {max} _ {a’} Q(s’, a’)$。我们执行的操作是一种热编码, 并在调用model.fit()时作为action_mask传递给输入。 $ y $是监督学习中”目标”的常用字母。在这里, 我们传递q_values。我做q_values [:.无], 以增加数组的尺寸, 因为它必须与one_hot_actions数组的尺寸相对应。如果你想了解更多信息, 则将其称为切片符号。
我们返回损失以将其保存在TensorBoard日志文件中, 然后可视化。我们还将监视许多其他内容:每秒执行多少步, 总RAM使用量, 平均情节回报率是多少, 等等。让我们看看这些图表。
跑步
要可视化TensorBoard日志文件, 我们首先需要拥有一个。因此, 让我们进行培训:
python run.py
这将首先打印我们模型的摘要。然后它将创建一个带有当前日期的日志目录并开始培训。每2000步, 将打印一条类似于以下内容的日志行:
episode 10 steps 200/2001 loss 0.3346639 return -200.0 in 1.02s 195.7 steps/s 9.0/15.6 GB RAM
每20, 000个步骤, 我们将以10, 000个步骤评估我们的模型:
Evaluation
100%|█████████████████████████████████████████████████████████████████████████████████| 10000/10000 [00:07<00:00, 1254.40it/s]
episode 677 step 120000 episode_return_avg -136.750 avg_max_q_value -56.004
经过677集和120, 000步之后, 平均集返回值从-200提高到-136.75!绝对是学习。我留给读者的是avg_max_q_value的一项很好的练习。但这是在培训期间查看的非常有用的统计信息。
经过20万步, 我们的培训已经完成。在我的四核CPU上, 大约需要20分钟。我们可以在date-log目录中查找, 例如06-07-18-39-log。将有四个扩展名为.h5的模型文件。这是TensorFlow图权重的快照, 我们每50, 000步保存一次权重, 以供日后了解我们所学习的策略。要查看它:
python run.py --model 06-08-18-42-log/06-08-18-42-200000.h5 --view
要查看其他可能的标志:python run.py –help。
现在, 汽车在实现预期目标方面做得更好。在date-log目录中, 还存在events.out。*文件。这是TensorBoard存储其数据的文件。我们使用loggers.py中定义的最简单的TensorBoardLogger对其进行写入。要查看事件文件, 我们需要运行本地TensorBoard服务器:
tensorboard --logdir=.
–logdir只是指向其中有日期日志目录的目录, 在我们的例子中, 它将是当前目录, 因此.. TensorBoard打印它正在监听的URL。如果你打开http://127.0.0.1:6006, 应该会看到与以下内容类似的八个图:
包起来
train()完成所有训练。我们首先创建模型并重播缓冲区。然后, 在类似于see.py的循环中, 我们与环境进行交互并将体验存储在缓冲区中。重要的是我们要遵守epsilon-greedy政策。我们总是可以根据$ Q $函数选择最佳操作;但是, 这不利于探索, 这会损害整体性能。因此, 为了以epsilon概率执行探索, 我们执行随机动作:
def greedy_action(env, model, observation):
next_q_values = predict(env, model, observations=[observation])
return np.argmax(next_q_values)
def epsilon_greedy_action(env, model, observation, epsilon):
if random.random() < epsilon:
action = env.action_space.sample()
else:
action = greedy_action(env, model, observation)
return action
Epsilon设置为1%。在2000年的经验之后, 重播已足够开始训练。我们通过调用fit_batch()并从重放缓冲区中采样随机体验来做到这一点:
batch = replay.sample(BATCH_SIZE)
loss = fit_batch(env, model, target_model, batch)
每20, 000步, 我们评估并记录结果(评估为epsilon = 0, 完全为贪婪策略):
if step >= TRAIN_START and step % EVAL_EVERY == 0:
episode_return_avg = evaluate(env, model)
q_values = predict(env, model, q_validation_observations)
max_q_values = np.max(q_values, axis=1)
avg_max_q_value = np.mean(max_q_values)
print(
"episode {} "
"step {} "
"episode_return_avg {:.3f} "
"avg_max_q_value {:.3f}".format(
episode, step, episode_return_avg, avg_max_q_value, ))
logger.log_scalar('episode_return_avg', episode_return_avg, step)
logger.log_scalar('avg_max_q_value', avg_max_q_value, step)
整个代码大约300行, 而run.py包含大约250行最重要的代码。
可以注意到有很多超参数:
DISCOUNT_FACTOR_GAMMA = 0.99
LEARNING_RATE = 0.001
BATCH_SIZE = 64
TARGET_UPDATE_EVERY = 1000
TRAIN_START = 2000
REPLAY_BUFFER_SIZE = 50000
MAX_STEPS = 200000
LOG_EVERY = 2000
SNAPSHOT_EVERY = 50000
EVAL_EVERY = 20000
EVAL_STEPS = 10000
EVAL_EPSILON = 0
TRAIN_EPSILON = 0.01
Q_VALIDATION_SIZE = 10000
甚至还不是全部。还有一个网络体系结构-我们使用了两个具有32个神经元的隐藏层, ReLU激活和Adam优化器, 但是还有很多其他选择。即使是很小的变化也会对培训产生巨大影响。可以花费大量时间来调整超参数。在最近的OpenAI竞赛中, 第二名选手发现, 在进行超参数调整后, Rainbow的得分几乎可以翻番。自然, 必须记住, 过度拟合很容易。当前, 强化算法正努力将知识转移到类似环境中。目前, 我们的山地车并不适用于所有类型的山地。你实际上可以修改OpenAI Gym环境, 并查看代理可以推广多远。
另一个工作是找到比我的更好的超参数集。绝对有可能。但是, 仅进行一次培训不足以判断你的更改是否有所改善。两次训练之间通常会有很大的差异;差异很大。你将需要进行多次运行才能确定更好的方法。如果你想阅读有关重现性这样重要主题的更多信息, 我鼓励你阅读重要的深度强化学习。如果我们愿意在此问题上花费更多的计算能力, 则可以在某种程度上使此过程自动化, 而不是手动进行调整。一种简单的方法是为某些超参数准备一个有希望的值范围, 然后运行网格搜索(检查其组合), 同时并行运行训练。并行化本身就是一个大话题, 因为它对于高性能至关重要。
深入的$ Q $学习代表了使用价值迭代的一大类强化学习算法。我们试图近似$ Q $函数, 并且大多数时候我们只是以贪婪的方式使用它。还有另一个使用策略迭代的系列。他们不专注于近似$ Q $函数, 而是直接寻找最佳策略$π^ * $。要查看值迭代适合强化学习算法的范围, 请执行以下操作:
资料来源:https://github.com/NervanaSystems/coach
你的想法可能是深度强化学习看起来很脆弱。你会是对的;有很多问题。你可以参考《深度强化学习》还行不通, 而强化学习从来没有用过, 而”深度学习”仅能起到一点作用。
这结束了本教程。我们出于学习目的实施了自己的基本DQN。在某些Atari游戏中, 可以使用非常相似的代码来获得良好的性能。在实际应用中, 通常会采用经过测试的高性能实现, 例如, 一种来自OpenAI基准。如果你想了解在更复杂的环境中尝试进行深度强化学习时可能遇到的挑战, 请阅读《我们的NIPS 2017:学习跑步》方法。如果你想在有趣的比赛环境中了解更多信息, 请访问NIPS 2018 Competitions或rowai.org。
如果你要成为机器学习专家, 并且想加深在监督学习中的知识, 请查看”机器学习视频分析:识别鱼”, 进行有趣的鱼识别实验。
相关:教育蓬松鸟:强化学习教程
评论前必须登录!
注册