Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)(1)https://developer.aliyun.com/article/1482457
Q-Learning
同样,Q-learning 算法是 Q 值迭代算法在转移概率和奖励最初未知的情况下的一种适应。Q-learning 通过观察代理玩(例如,随机玩)并逐渐改进其对 Q 值的估计来工作。一旦它有准确的 Q 值估计(或足够接近),那么最优策略就是选择具有最高 Q 值的动作(即,贪婪策略)。
方程 18-5. Q-learning 算法
Q(s,a) ←α r+γ· maxa‘ Q(s’, a')
对于每个状态-动作对(s,a),该算法跟踪代理离开状态s并采取动作a后获得的奖励r的平均值,以及它期望获得的折现未来奖励的总和。为了估计这个总和,我们取下一个状态s′的 Q 值估计的最大值,因为我们假设目标策略将从那时开始最优地行动。
让我们实现 Q-learning 算法。首先,我们需要让代理探索环境。为此,我们需要一个步骤函数,以便代理可以执行一个动作并获得结果状态和奖励:
def step(state, action): probas = transition_probabilities[state][action] next_state = np.random.choice([0, 1, 2], p=probas) reward = rewards[state][action][next_state] return next_state, reward
现在让我们实现代理的探索策略。由于状态空间相当小,一个简单的随机策略就足够了。如果我们运行足够长的时间,代理将多次访问每个状态,并且还将多次尝试每种可能的动作:
def exploration_policy(state): return np.random.choice(possible_actions[state])
接下来,在我们像之前一样初始化 Q 值之后,我们准备使用学习率衰减的 Q-learning 算法运行(使用幂调度,引入于第十一章):
alpha0 = 0.05 # initial learning rate decay = 0.005 # learning rate decay gamma = 0.90 # discount factor state = 0 # initial state for iteration in range(10_000): action = exploration_policy(state) next_state, reward = step(state, action) next_value = Q_values[next_state].max() # greedy policy at the next step alpha = alpha0 / (1 + iteration * decay) Q_values[state, action] *= 1 - alpha Q_values[state, action] += alpha * (reward + gamma * next_value) state = next_state
这个算法将收敛到最优的 Q 值,但需要很多迭代,可能需要相当多的超参数调整。正如您在图 18-9 中看到的那样,Q 值迭代算法(左侧)收敛得非常快,在不到 20 次迭代中,而 Q-learning 算法(右侧)需要大约 8000 次迭代才能收敛。显然,不知道转移概率或奖励使得找到最优策略变得更加困难!
图 18-9. Q 值迭代算法与 Q-learning 算法的学习曲线
Q-learning 算法被称为离策略算法,因为正在训练的策略不一定是训练过程中使用的策略。例如,在我们刚刚运行的代码中,执行的策略(探索策略)是完全随机的,而正在训练的策略从未被使用过。训练后,最优策略对应于系统地选择具有最高 Q 值的动作。相反,策略梯度算法是在策略算法:它使用正在训练的策略探索世界。令人惊讶的是,Q-learning 能够通过观察代理随机行动来学习最优策略。想象一下,在一只被蒙住眼睛的猴子是你的老师时学习打高尔夫球。我们能做得更好吗?
探索策略
当然,只有当探索策略足够彻底地探索 MDP 时,Q 学习才能起作用。尽管纯随机策略保证最终会访问每个状态和每个转换多次,但这可能需要非常长的时间。因此,更好的选择是使用ε-贪心策略(ε是 epsilon):在每一步中,它以概率ε随机行动,或以概率 1-ε贪婪地行动(即选择具有最高 Q 值的动作)。ε-贪心策略的优势(与完全随机策略相比)在于,随着 Q 值估计变得越来越好,它将花费越来越多的时间探索环境的有趣部分,同时仍然花费一些时间访问 MDP 的未知区域。通常会从较高的ε值(例如 1.0)开始,然后逐渐降低它(例如降至 0.05)。
另一种方法是鼓励探索策略尝试之前尝试过的动作,而不仅仅依赖于机会。这可以作为添加到 Q 值估计中的奖励来实现,如方程 18-6 所示。
方程 18-6. 使用探索函数的 Q 学习
Q(s,a) ←α r+γ·max a‘ f Q(s’,a‘ ),N(s’, a')
在这个方程中:
- N(s′, a′)计算动作a′在状态s′中被选择的次数。
- f(Q, N)是一个探索函数,例如f(Q, N) = Q + κ/(1 + N),其中κ是一个好奇心超参数,衡量了代理对未知的吸引力。
近似 Q 学习和深度 Q 学习
Q 学习的主要问题是它在具有许多状态和动作的大型(甚至中等大小)MDP 中无法很好地扩展。例如,假设您想使用 Q 学习来训练一个代理玩《Ms. Pac-Man》(见图 18-1)。Ms. Pac-Man 可以吃约 150 个豆子,每个豆子可以存在或不存在(即已经被吃掉)。因此,可能的状态数量大于 2¹⁵⁰ ≈ 10⁴⁵。如果您考虑所有鬼和 Ms. Pac-Man 的所有可能位置组合,可能的状态数量将大于地球上的原子数量,因此绝对无法跟踪每个单个 Q 值的估计。
解决方案是找到一个函数Qθ,它用可管理数量的参数(由参数向量θ给出)来近似任何状态-动作对(s, a)的 Q 值。这被称为近似 Q 学习。多年来,人们建议使用从状态中提取的手工制作的特征的线性组合(例如,最近的鬼的距离、它们的方向等)来估计 Q 值,但在 2013 年,DeepMind表明使用深度神经网络可以工作得更好,特别是对于复杂问题,而且不需要任何特征工程。用于估计 Q 值的 DNN 称为深度 Q 网络(DQN),并且使用 DQN 进行近似 Q 学习称为深度 Q 学习。
现在,我们如何训练一个 DQN 呢?考虑 DQN 计算给定状态-动作对(s, a)的近似 Q 值。由于贝尔曼,我们知道我们希望这个近似 Q 值尽可能接近我们在状态s中执行动作a后实际观察到的奖励r,加上从那时开始最优地玩的折现值。为了估计未来折现奖励的总和,我们只需在下一个状态s′上执行 DQN,对所有可能的动作a′。我们得到每个可能动作的近似未来 Q 值。然后我们选择最高的(因为我们假设我们将最优地玩),并对其进行折现,这给我们一个未来折现奖励总和的估计。通过将奖励r和未来折现值估计相加,我们得到状态-动作对(s, a)的目标 Q 值y(s, a),如方程 18-7 所示。
方程 18-7. 目标 Q 值
y(s,a)=r+γ·maxa‘Qθ(s’,a')
有了这个目标 Q 值,我们可以使用任何梯度下降算法运行一个训练步骤。具体来说,我们通常试图最小化估计的 Q 值Qθ和目标 Q 值y(s, a)之间的平方误差,或者使用 Huber 损失来减少算法对大误差的敏感性。这就是深度 Q 学习算法!让我们看看如何实现它来解决 CartPole 环境。
实施深度 Q 学习
我们需要的第一件事是一个深度 Q 网络。理论上,我们需要一个神经网络,将状态-动作对作为输入,并输出一个近似 Q 值。然而,在实践中,使用一个只接受状态作为输入,并为每个可能动作输出一个近似 Q 值的神经网络要高效得多。为了解决 CartPole 环境,我们不需要一个非常复杂的神经网络;几个隐藏层就足够了:
input_shape = [4] # == env.observation_space.shape n_outputs = 2 # == env.action_space.n model = tf.keras.Sequential([ tf.keras.layers.Dense(32, activation="elu", input_shape=input_shape), tf.keras.layers.Dense(32, activation="elu"), tf.keras.layers.Dense(n_outputs) ])
使用这个 DQN 选择动作时,我们选择预测 Q 值最大的动作。为了确保代理程序探索环境,我们将使用ε-贪婪策略(即,我们将以概率ε选择一个随机动作):
def epsilon_greedy_policy(state, epsilon=0): if np.random.rand() < epsilon: return np.random.randint(n_outputs) # random action else: Q_values = model.predict(state[np.newaxis], verbose=0)[0] return Q_values.argmax() # optimal action according to the DQN
我们将不再仅基于最新经验训练 DQN,而是将所有经验存储在一个重放缓冲区(或重放内存)中,并在每次训练迭代中从中随机抽取一个训练批次。这有助于减少训练批次中经验之间的相关性,从而极大地帮助训练。为此,我们将使用一个双端队列(deque
):
from collections import deque replay_buffer = deque(maxlen=2000)
提示
deque是一个队列,可以高效地在两端添加或删除元素。从队列的两端插入和删除项目非常快,但当队列变长时,随机访问可能会很慢。如果您需要一个非常大的重放缓冲区,您应该使用循环缓冲区(请参阅笔记本中的实现),或查看DeepMind 的 Reverb 库。
每个体验将由六个元素组成:一个状态s,代理程序执行的动作a,产生的奖励r,它达到的下一个状态s′,一个指示该点是否结束的布尔值(done
),最后一个指示该点是否截断的布尔值。我们将需要一个小函数从重放缓冲区中随机抽取一批体验。它将返回六个对应于六个体验元素的 NumPy 数组:
def sample_experiences(batch_size): indices = np.random.randint(len(replay_buffer), size=batch_size) batch = [replay_buffer[index] for index in indices] return [ np.array([experience[field_index] for experience in batch]) for field_index in range(6) ] # [states, actions, rewards, next_states, dones, truncateds]
让我们还创建一个函数,该函数将使用ε-贪婪策略执行一个单步操作,然后将结果体验存储在重放缓冲区中:
def play_one_step(env, state, epsilon): action = epsilon_greedy_policy(state, epsilon) next_state, reward, done, truncated, info = env.step(action) replay_buffer.append((state, action, reward, next_state, done, truncated)) return next_state, reward, done, truncated, info
最后,让我们创建一个最后一个函数,该函数将从重放缓冲区中抽取一批体验,并通过在该批次上执行单个梯度下降步骤来训练 DQN:
batch_size = 32 discount_factor = 0.95 optimizer = tf.keras.optimizers.Nadam(learning_rate=1e-2) loss_fn = tf.keras.losses.mean_squared_error def training_step(batch_size): experiences = sample_experiences(batch_size) states, actions, rewards, next_states, dones, truncateds = experiences next_Q_values = model.predict(next_states, verbose=0) max_next_Q_values = next_Q_values.max(axis=1) runs = 1.0 - (dones | truncateds) # episode is not done or truncated target_Q_values = rewards + runs * discount_factor * max_next_Q_values target_Q_values = target_Q_values.reshape(-1, 1) mask = tf.one_hot(actions, n_outputs) with tf.GradientTape() as tape: all_Q_values = model(states) Q_values = tf.reduce_sum(all_Q_values * mask, axis=1, keepdims=True) loss = tf.reduce_mean(loss_fn(target_Q_values, Q_values)) grads = tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables))
这段代码中发生了什么:
- 首先我们定义一些超参数,然后创建优化器和损失函数。
- 然后我们创建
training_step()
函数。它首先对经验进行批量采样,然后使用 DQN 来预测每个经验的下一个状态中每个可能动作的 Q 值。由于我们假设代理将会最优地进行游戏,我们只保留每个下一个状态的最大 Q 值。接下来,我们使用 Equation 18-7 来计算每个经验的状态-动作对的目标 Q 值。 - 我们希望使用 DQN 来计算每个经验状态-动作对的 Q 值,但是 DQN 还会输出其他可能动作的 Q 值,而不仅仅是代理实际选择的动作。因此,我们需要屏蔽掉所有我们不需要的 Q 值。
tf.one_hot()
函数使得将动作索引数组转换为这样的屏蔽变得可能。例如,如果前三个经验包含动作 1、1、0,那么屏蔽将以[[0, 1], [0, 1], [1, 0], ...]
开始。然后我们可以将 DQN 的输出与这个屏蔽相乘,这将将我们不想要的所有 Q 值置零。然后我们沿着轴 1 求和,去除所有零,只保留经验状态-动作对的 Q 值。这给我们了Q_values
张量,包含批量中每个经验的一个预测 Q 值。 - 接下来,我们计算损失:它是经验状态-动作对的目标和预测 Q 值之间的均方误差。
- 最后,我们执行梯度下降步骤,以最小化损失与模型可训练变量的关系。
这是最困难的部分。现在训练模型就很简单了:
for episode in range(600): obs, info = env.reset() for step in range(200): epsilon = max(1 - episode / 500, 0.01) obs, reward, done, truncated, info = play_one_step(env, obs, epsilon) if done or truncated: break if episode > 50: training_step(batch_size)
我们运行 600 个 episode,每个最多 200 步。在每一步中,我们首先计算ε-贪婪策略的epsilon
值:它将从 1 线性下降到 0.01,在不到 500 个 episode 内。然后我们调用play_one_step()
函数,该函数将使用ε-贪婪策略选择一个动作,然后执行它并记录经验到重放缓冲区。如果 episode 结束或被截断,我们退出循环。最后,如果我们超过第 50 个 episode,我们调用training_step()
函数从重放缓冲区中采样一个批次来训练模型。我们之所以在没有训练的情况下运行多个 episode,是为了给重放缓冲区一些时间来填充(如果我们不等待足够长的时间,那么重放缓冲区中将没有足够的多样性)。就是这样:我们刚刚实现了深度 Q 学习算法!
Figure 18-10 显示了代理在每个 episode 中获得的总奖励。
图 18-10. 深度 Q 学习算法的学习曲线
正如你所看到的,该算法花了一段时间才开始学习任何东西,部分原因是ε在开始时非常高。然后它的进展是不稳定的:它首先在第 220 集左右达到了最大奖励,但立即下降,然后上下几次反弹,不久后看起来它终于稳定在最大奖励附近,大约在第 320 集左右,它的得分再次急剧下降。这被称为灾难性遗忘,这是几乎所有 RL 算法面临的一个大问题之一:当代理探索环境时,它更新其策略,但它在环境的一个部分学到的东西可能会破坏它在环境的其他部分早期学到的东西。经验是相当相关的,学习环境不断变化——这对于梯度下降来说并不理想!如果增加回放缓冲区的大小,算法将不太容易受到这个问题的影响。调整学习率也可能有所帮助。但事实是,强化学习很难:训练通常不稳定,您可能需要尝试许多超参数值和随机种子,才能找到一个表现良好的组合。例如,如果您尝试将激活函数从"elu"
更改为"relu"
,性能将大大降低。
注意
强化学习因训练不稳定性和对超参数值和随机种子选择的极度敏感性而臭名昭著。正如研究人员 Andrej Karpathy 所说,“[监督学习]想要工作。[…]强化学习必须被迫工作”。您需要时间、耐心、毅力,也许还需要一点运气。这是 RL 不像常规深度学习(例如,卷积网络)那样被广泛采用的一个主要原因。但除了 AlphaGo 和 Atari 游戏之外,还有一些真实世界的应用:例如,谷歌使用 RL 来优化其数据中心成本,并且它被用于一些机器人应用、超参数调整和推荐系统中。
你可能会想为什么我们没有绘制损失。事实证明,损失是模型性能的一个很差的指标。损失可能会下降,但代理可能表现更差(例如,当代理陷入环境的一个小区域时,DQN 开始过度拟合这个区域时可能会发生这种情况)。相反,损失可能会上升,但代理可能表现更好(例如,如果 DQN 低估了 Q 值并开始正确增加其预测,代理可能表现更好,获得更多奖励,但损失可能会增加,因为 DQN 还设置了目标,这也会更大)。因此,最好绘制奖励。
到目前为止,我们一直在使用的基本深度 Q 学习算法对于学习玩 Atari 游戏来说太不稳定了。那么 DeepMind 是如何做到的呢?嗯,他们调整了算法!
深度 Q 学习变体
让我们看看一些可以稳定和加速训练的深度 Q 学习算法的变体。
固定 Q 值目标
在基本的深度 Q 学习算法中,模型既用于进行预测,也用于设置自己的目标。这可能导致类似于狗追逐自己尾巴的情况。这种反馈循环可能使网络不稳定:它可能发散、振荡、冻结等。为了解决这个问题,在他们 2013 年的论文中,DeepMind 的研究人员使用了两个 DQN 而不是一个:第一个是在线模型,它在每一步学习并用于移动代理,另一个是目标模型,仅用于定义目标。目标模型只是在线模型的一个克隆:
target = tf.keras.models.clone_model(model) # clone the model's architecture target.set_weights(model.get_weights()) # copy the weights
然后,在training_step()
函数中,我们只需要更改一行,使用目标模型而不是在线模型来计算下一个状态的 Q 值:
next_Q_values = target.predict(next_states, verbose=0)
最后,在训练循环中,我们必须定期将在线模型的权重复制到目标模型中(例如,每 50 个 episode):
if episode % 50 == 0: target.set_weights(model.get_weights())
由于目标模型更新的频率远低于在线模型,Q 值目标更加稳定,我们之前讨论的反馈循环被减弱,其影响也变得不那么严重。这种方法是 DeepMind 研究人员在 2013 年的一篇论文中的主要贡献之一,使代理能够从原始像素学习玩 Atari 游戏。为了稳定训练,他们使用了非常小的学习率 0.00025,他们每 10000 步才更新一次目标模型(而不是 50 步),并且他们使用了一个非常大的重放缓冲区,包含 100 万个经验。他们非常缓慢地减小了epsilon
,在 100 万步内从 1 减小到 0.1,并让算法运行了 5000 万步。此外,他们的 DQN 是一个深度卷积网络。
现在让我们来看看另一个 DQN 变体,它再次超越了现有技术水平。
双重 DQN
在一篇 2015 年的论文中,DeepMind 研究人员调整了他们的 DQN 算法,提高了性能并在一定程度上稳定了训练。他们将这个变体称为双重 DQN。更新基于这样一个观察:目标网络容易高估 Q 值。实际上,假设所有动作都是同样好的:目标模型估计的 Q 值应该是相同的,但由于它们是近似值,一些可能略大于其他值,纯粹是偶然的。目标模型将始终选择最大的 Q 值,这个值将略大于平均 Q 值,很可能高估真实的 Q 值(有点像在测量池的深度时计算最高随机波浪的高度)。为了解决这个问题,研究人员建议在选择下一个状态的最佳动作时使用在线模型而不是目标模型,并且仅使用目标模型来估计这些最佳动作的 Q 值。以下是更新后的training_step()
函数:
def training_step(batch_size): experiences = sample_experiences(batch_size) states, actions, rewards, next_states, dones, truncateds = experiences next_Q_values = model.predict(next_states, verbose=0) # ≠ target.predict() best_next_actions = next_Q_values.argmax(axis=1) next_mask = tf.one_hot(best_next_actions, n_outputs).numpy() max_next_Q_values = (target.predict(next_states, verbose=0) * next_mask ).sum(axis=1) [...] # the rest is the same as earlier
仅仅几个月后,DQN 算法的另一个改进被提出;我们接下来将看看这个改进。
优先经验回放
与从重放缓冲区中均匀采样经验不同,为什么不更频繁地采样重要经验呢?这个想法被称为重要性采样(IS)或优先经验回放(PER),并且是由 DeepMind 研究人员在 2015 年的一篇论文中介绍的(再次!)。
更具体地说,如果经验很可能导致快速学习进展,那么这些经验被认为是“重要的”。但是我们如何估计这一点呢?一个合理的方法是测量 TD 误差的大小δ = r + γ·V(s′) – V(s)。较大的 TD 误差表明一个转换(s, a, s′)非常令人惊讶,因此可能值得学习。当一个经验被记录在重放缓冲区中时,其优先级被设置为一个非常大的值,以确保至少被采样一次。然而,一旦被采样(并且每次被采样时),TD 误差δ被计算,并且这个经验的优先级被设置为p = |δ|(再加上一个小常数,以确保每个经验有非零的采样概率)。具有优先级p的经验被采样的概率P与p^(ζ)成正比,其中ζ是一个控制我们希望重要性采样有多贪婪的超参数:当ζ = 0 时,我们只得到均匀采样,当ζ = 1 时,我们得到完全的重要性采样。在论文中,作者使用了ζ = 0.6,但最佳值将取决于任务。
然而,有一个问题:由于样本将偏向于重要经验,我们必须在训练过程中通过根据其重要性降低经验的权重来补偿这种偏差,否则模型将只是过度拟合重要经验。明确地说,我们希望重要经验被更频繁地抽样,但这也意味着我们必须在训练过程中给它们更低的权重。为了做到这一点,我们将每个经验的训练权重定义为w = (n P)^(–β),其中n是回放缓冲区中的经验数量,β是一个超参数,控制我们想要补偿重要性抽样偏差的程度(0 表示根本不补偿,而 1 表示完全补偿)。在论文中,作者在训练开始时使用β = 0.4,并在训练结束时线性增加到β = 1。再次强调,最佳值将取决于任务,但如果你增加一个值,通常也会想要增加另一个值。
现在让我们看一下 DQN 算法的最后一个重要变体。
决斗 DQN
决斗 DQN算法(DDQN,不要与双重 DQN 混淆,尽管这两种技术可以很容易地结合在一起)是由 DeepMind 研究人员在另一篇2015 年的论文中介绍的。要理解它的工作原理,我们首先必须注意到一个状态-动作对(s, a)的 Q 值可以表示为Q(s, a) = V(s) + A(s, a),其中V(s)是状态s的值,A(s, a)是在状态s中采取动作a的优势,与该状态下所有其他可能的动作相比。此外,一个状态的值等于该状态的最佳动作a^的 Q 值(因为我们假设最优策略将选择最佳动作),所以V*(s) = Q(s, a^),这意味着A*(s, a^*) = 0。在决斗 DQN 中,模型估计了状态的值和每个可能动作的优势。由于最佳动作应该具有优势为 0,模型从所有预测的优势中减去了最大预测的优势。这里是一个使用功能 API 实现的简单 DDQN 模型:
input_states = tf.keras.layers.Input(shape=[4]) hidden1 = tf.keras.layers.Dense(32, activation="elu")(input_states) hidden2 = tf.keras.layers.Dense(32, activation="elu")(hidden1) state_values = tf.keras.layers.Dense(1)(hidden2) raw_advantages = tf.keras.layers.Dense(n_outputs)(hidden2) advantages = raw_advantages - tf.reduce_max(raw_advantages, axis=1, keepdims=True) Q_values = state_values + advantages model = tf.keras.Model(inputs=[input_states], outputs=[Q_values]) d
算法的其余部分与之前完全相同。事实上,你可以构建一个双重决斗 DQN 并将其与优先经验重放结合起来!更一般地说,许多 RL 技术可以结合在一起,正如 DeepMind 在一篇2017 年的论文中展示的:论文的作者将六种不同的技术结合到一个名为Rainbow的代理中,这在很大程度上超越了现有技术水平。
正如你所看到的,深度强化学习是一个快速发展的领域,还有很多东西等待探索!
一些流行 RL 算法的概述
在我们结束本章之前,让我们简要看一下其他几种流行的算法:
AlphaGo 使用基于深度神经网络的蒙特卡洛树搜索(MCTS)的变体,在围棋比赛中击败人类冠军。MCTS 是由 Nicholas Metropolis 和 Stanislaw Ulam 于 1949 年发明的。它在运行许多模拟之后选择最佳移动,重复地探索从当前位置开始的搜索树,并在最有希望的分支上花费更多时间。当它到达一个以前未访问过的节点时,它会随机播放直到游戏结束,并更新每个访问过的节点的估计值(排除随机移动),根据最终结果增加或减少每个估计值。AlphaGo 基于相同的原则,但它使用策略网络来选择移动,而不是随机播放。这个策略网络是使用策略梯度进行训练的。原始算法涉及另外三个神经网络,并且更加复杂,但在AlphaGo Zero 论文中被简化,使用单个神经网络来选择移动和评估游戏状态。AlphaZero 论文推广了这个算法,使其能够处理不仅是围棋,还有国际象棋和将棋(日本象棋)。最后,MuZero 论文继续改进这个算法,即使代理开始时甚至不知道游戏规则,也能胜过以前的迭代!
Actor-critic 算法
Actor-critics 是一类将策略梯度与深度 Q 网络结合的 RL 算法。一个 actor-critic 代理包含两个神经网络:一个策略网络和一个 DQN。DQN 通过从代理的经验中学习来进行正常训练。策略网络学习方式不同(并且比常规 PG 快得多):代理不是通过多个情节估计每个动作的价值,然后为每个动作总结未来折现奖励,最后对其进行归一化,而是依赖于 DQN 估计的动作值(评论家)。这有点像运动员(代理)在教练(DQN)的帮助下学习。
这是 DeepMind 研究人员在 2016 年引入的一个重要的 actor-critic 变体,其中多个代理并行学习,探索环境的不同副本。定期但异步地(因此得名),每个代理将一些权重更新推送到主网络,然后从该网络中拉取最新的权重。因此,每个代理都有助于改进主网络,并从其他代理学到的知识中受益。此外,DQN 估计每个动作的优势,而不是估计 Q 值(因此名称中的第二个 A),这有助于稳定训练。
A2C 是 A3C 算法的一个变体,它去除了异步性。所有模型更新都是同步的,因此梯度更新是在更大的批次上执行的,这使模型能够更好地利用 GPU 的性能。
SAC 是由 Tuomas Haarnoja 和其他加州大学伯克利分校研究人员于 2018 年提出的 actor-critic 变体。它不仅学习奖励,还要最大化其动作的熵。换句话说,它试图尽可能不可预测,同时尽可能获得更多奖励。这鼓励代理探索环境,加快训练速度,并使其在 DQN 产生不完美估计时不太可能重复执行相同的动作。这个算法展示了惊人的样本效率(与所有以前的算法相反,学习速度非常慢)。
这个由 John Schulman 和其他 OpenAI 研究人员开发的算法基于 A2C,但它剪切损失函数以避免过大的权重更新(这经常导致训练不稳定)。PPO 是前一个信任区域策略优化(TRPO)算法的简化版本,也是由 OpenAI 开发的。OpenAI 在 2019 年 4 月的新闻中以其基于 PPO 算法的 AI OpenAI Five 而闻名,该 AI 在多人游戏Dota 2中击败了世界冠军。
在强化学习中经常出现的问题是奖励的稀疏性,这使得学习变得非常缓慢和低效。加州大学伯克利分校的 Deepak Pathak 和其他研究人员提出了一种激动人心的方法来解决这个问题:为什么不忽略奖励,只是让代理人对探索环境感到极大的好奇心呢?奖励因此变得内在于代理人,而不是来自环境。同样,激发孩子的好奇心更有可能取得好的结果,而不仅仅是因为孩子取得好成绩而奖励他。这是如何运作的呢?代理人不断尝试预测其行动的结果,并寻找结果与其预测不符的情况。换句话说,它希望受到惊喜。如果结果是可预测的(无聊),它会去其他地方。然而,如果结果是不可预测的,但代理人注意到自己无法控制它,那么它也会在一段时间后感到无聊。只有好奇心,作者们成功地训练了一个代理人玩了很多视频游戏:即使代理人输掉了也没有惩罚,游戏重新开始,这很无聊,所以它学会了避免这种情况。
开放式学习(OEL)
OEL 的目标是训练代理人能够不断学习新颖有趣的任务,通常是通过程序生成的。我们还没有达到这一目标,但在过去几年中取得了一些惊人的进展。例如,Uber AI 团队在 2019 年发表的一篇论文介绍了POET 算法,该算法生成多个带有凸起和洞的模拟 2D 环境,并为每个环境训练一个代理人:代理人的目标是尽可能快地行走,同时避开障碍物。该算法从简单的环境开始,但随着时间的推移逐渐变得更加困难:这被称为课程学习。此外,尽管每个代理人只在一个环境中接受训练,但它必须定期与其他代理人竞争,跨所有环境。在每个环境中,获胜者被复制并取代之前的代理人。通过这种方式,知识定期在环境之间传递,并选择最具适应性的代理人。最终,这些代理人比单一任务训练的代理人更擅长行走,并且能够应对更加困难的环境。当然,这个原则也可以应用于其他环境和任务。如果您对 OEL 感兴趣,请务必查看增强 POET 论文,以及 DeepMind 在这个主题上的2021 年论文。
提示
如果您想了解更多关于强化学习的知识,请查看 Phil Winder(O’Reilly)的书籍强化学习。
本章涵盖了许多主题:策略梯度、马尔可夫链、马尔可夫决策过程、Q 学习、近似 Q 学习、深度 Q 学习及其主要变体(固定 Q 值目标、双重 DQN、对决 DQN 和优先经验重放),最后我们简要介绍了一些其他流行算法。强化学习是一个庞大且令人兴奋的领域,每天都会涌现出新的想法和算法,因此希望本章引发了您的好奇心:有一个整个世界等待您去探索!
练习
- 您如何定义强化学习?它与常规监督学习或无监督学习有何不同?
- 你能想到本章未提及的三种强化学习的可能应用吗?对于每一种,环境是什么?代理是什么?可能的行动有哪些?奖励是什么?
- 什么是折扣因子?如果修改折扣因子,最优策略会改变吗?
- 如何衡量强化学习代理的表现?
- 什么是信用分配问题?它何时发生?如何缓解它?
- 使用重放缓冲区的目的是什么?
- 什么是离策略强化学习算法?
- 使用策略梯度来解决 OpenAI Gym 的 LunarLander-v2 环境。
- 使用双重对决 DQN 训练一个代理,使其在著名的 Atari Breakout 游戏(
"ALE/Breakout-v5"
)中达到超人水平。观察结果是图像。为了简化任务,您应该将它们转换为灰度图像(即在通道轴上取平均),然后裁剪和降采样,使它们足够大以进行游戏,但不要过大。单个图像无法告诉您球和挡板的移动方向,因此您应该合并两到三个连续图像以形成每个状态。最后,DQN 应该主要由卷积层组成。 - 如果您有大约 100 美元可以花费,您可以购买一个树莓派 3 加上一些廉价的机器人组件,在树莓派上安装 TensorFlow,然后尽情玩耍!例如,可以查看 Lukas Biewald 的这篇 有趣的帖子,或者看看 GoPiGo 或 BrickPi。从简单的目标开始,比如让机器人转身找到最亮的角度(如果有光传感器)或最近的物体(如果有声纳传感器),然后朝着那个方向移动。然后您可以开始使用深度学习:例如,如果机器人有摄像头,可以尝试实现一个目标检测算法,使其检测到人并朝他们移动。您还可以尝试使用强化学习,让代理学习如何独立使用电机来实现这个目标。玩得开心!
这些练习的解答可在本章笔记本的末尾找到,网址为 https://homl.info/colab3。
(1)想了解更多细节,请务必查看 Richard Sutton 和 Andrew Barto 的关于强化学习的书籍 Reinforcement Learning: An Introduction(麻省理工学院出版社)。
(2)Volodymyr Mnih 等人,“使用深度强化学习玩 Atari 游戏”,arXiv 预印本 arXiv:1312.5602(2013)。
(3)Volodymyr Mnih 等人,“通过深度强化学习实现人类水平控制”,自然 518(2015):529–533。
(4)查看 DeepMind 系统学习 Space Invaders、Breakout 和其他视频游戏的视频,网址为 https://homl.info/dqn3。
(5)图像(a)、(d)和(e)属于公共领域。图像(b)是来自 Ms. Pac-Man 游戏的截图,由 Atari 版权所有(在本章中属于合理使用)。图像(c)是从维基百科复制的;由用户 Stevertigo 创建,并在 知识共享署名-相同方式共享 2.0 下发布。
(6)通常更好地给予表现不佳者一点生存的机会,以保留“基因池”中的一些多样性。
⁷ 如果只有一个父母,这被称为无性繁殖。有两个(或更多)父母时,这被称为有性繁殖。后代的基因组(在这种情况下是一组策略参数)是随机由其父母的基因组的部分组成的。
⁸ 用于强化学习的遗传算法的一个有趣例子是增强拓扑的神经进化(NEAT)算法。
⁹ 这被称为梯度上升。它就像梯度下降一样,但方向相反:最大化而不是最小化。
¹⁰ OpenAI 是一家人工智能研究公司,部分资金来自埃隆·马斯克。其宣称的目标是推广和发展有益于人类的友好人工智能(而不是消灭人类)。
¹¹ Ronald J. Williams,“用于连接主义强化学习的简单统计梯度跟随算法”,机器学习8(1992):229–256。
¹² Richard Bellman,“马尔可夫决策过程”,数学与力学杂志6,第 5 期(1957):679–684。
¹³ Alex Irpan 在 2018 年发表的一篇很棒的文章很好地阐述了强化学习的最大困难和局限性。
¹⁴ Hado van Hasselt 等人,“双 Q 学习的深度强化学习”,第 30 届 AAAI 人工智能大会论文集(2015):2094–2100。
¹⁵ Tom Schaul 等人,“优先经验重放”,arXiv 预印本 arXiv:1511.05952(2015)。
¹⁶ 也可能只是奖励有噪音,此时有更好的方法来估计经验的重要性(请参阅论文中的一些示例)。
¹⁷ Ziyu Wang 等人,“用于深度强化学习的对抗网络架构”,arXiv 预印本 arXiv:1511.06581(2015)。
¹⁸ Matteo Hessel 等人,“彩虹:深度强化学习改进的结合”,arXiv 预印本 arXiv:1710.02298(2017):3215–3222。
¹⁹ David Silver 等人,“用深度神经网络和树搜索掌握围棋”,自然529(2016):484–489。
²⁰ David Silver 等人,“在没有人类知识的情况下掌握围棋”,自然550(2017):354–359。
²¹ David Silver 等人,“通过自我对弈掌握国际象棋和将棋的一般强化学习算法”,arXiv 预印本 arXiv:1712.01815。
²² Julian Schrittwieser 等人,“通过学习模型计划掌握 Atari、围棋、国际象棋和将棋”,arXiv 预印本 arXiv:1911.08265(2019)。
²³ Volodymyr Mnih 等人,“深度强化学习的异步方法”,第 33 届国际机器学习会议论文集(2016):1928–1937。
²⁴ Tuomas Haarnoja 等人,“软演员-评论家:带有随机演员的离策略最大熵深度强化学习”,第 35 届国际机器学习会议论文集(2018):1856–1865。
²⁵ John Schulman 等人,“近端策略优化算法”,arXiv 预印本 arXiv:1707.06347(2017)。
²⁶ John Schulman 等人,“信任区域策略优化”,第 32 届国际机器学习会议论文集(2015):1889–1897。
²⁷ Deepak Pathak 等,“由自监督预测驱动的好奇心探索”,第 34 届国际机器学习会议论文集(2017):2778–2787。
²⁸ 王锐等,“配对开放式先驱者(POET):不断生成越来越复杂和多样化的学习环境及其解决方案”,arXiv 预印本 arXiv:1901.01753(2019)。
²⁹ 王锐等,“增强 POET:通过无限创造学习挑战及其解决方案的开放式强化学习”,arXiv 预印本 arXiv:2003.08536(2020)。
³⁰ Open-Ended Learning Team 等,“开放式学习导致普遍能力代理”,arXiv 预印本 arXiv:2107.12808(2021)。
第十九章:规模化训练和部署 TensorFlow 模型
一旦您拥有一个能够做出惊人预测的美丽模型,您会怎么处理呢?嗯,您需要将其投入生产!这可能只是在一批数据上运行模型,也许编写一个每晚运行该模型的脚本。然而,通常情况下会更加复杂。您的基础设施的各个部分可能需要在实时数据上使用该模型,这种情况下,您可能会希望将模型封装在一个 Web 服务中:这样,您的基础设施的任何部分都可以随时使用简单的 REST API(或其他协议)查询模型,正如我们在第二章中讨论的那样。但随着时间的推移,您需要定期使用新数据对模型进行重新训练,并将更新后的版本推送到生产环境。您必须处理模型版本控制,优雅地从一个模型过渡到另一个模型,可能在出现问题时回滚到上一个模型,并可能并行运行多个不同的模型来执行A/B 实验。如果您的产品变得成功,您的服务可能会开始每秒收到大量查询(QPS),并且必须扩展以支持负载。如您将在本章中看到的,一个很好的扩展服务的解决方案是使用 TF Serving,无论是在您自己的硬件基础设施上还是通过诸如 Google Vertex AI 之类的云服务。它将有效地为您提供模型服务,处理优雅的模型过渡等。如果您使用云平台,您还将获得许多额外功能,例如强大的监控工具。
此外,如果你有大量的训练数据和计算密集型模型,那么训练时间可能会变得过长。如果你的产品需要快速适应变化,那么长时间的训练可能会成为一个阻碍因素(例如,想象一下一个新闻推荐系统在推广上周的新闻)。更重要的是,长时间的训练会阻止你尝试新想法。在机器学习(以及许多其他领域),很难事先知道哪些想法会奏效,因此你应该尽可能快地尝试尽可能多的想法。加快训练的一种方法是使用硬件加速器,如 GPU 或 TPU。为了更快地训练,你可以在多台配备多个硬件加速器的机器上训练模型。TensorFlow 的简单而强大的分布策略 API 使这一切变得容易,你将会看到。
在这一章中,我们将学习如何部署模型,首先使用 TF Serving,然后使用 Vertex AI。我们还将简要介绍如何将模型部署到移动应用程序、嵌入式设备和 Web 应用程序。然后我们将讨论如何使用 GPU 加速计算,以及如何使用分布策略 API 在多个设备和服务器上训练模型。最后,我们将探讨如何使用 Vertex AI 规模化训练模型并微调其超参数。这是很多要讨论的话题,让我们开始吧!
为 TensorFlow 模型提供服务
一旦您训练了一个 TensorFlow 模型,您可以在任何 Python 代码中轻松地使用它:如果它是一个 Keras 模型,只需调用它的predict()
方法!但随着基础设施的增长,会出现一个更好的选择,即将您的模型封装在一个小型服务中,其唯一作用是进行预测,并让基础设施的其余部分查询它(例如,通过 REST 或 gRPC API)。这样可以将您的模型与基础设施的其余部分解耦,从而可以轻松地切换模型版本或根据需要扩展服务(独立于您的基础设施的其余部分),执行 A/B 实验,并确保所有软件组件依赖于相同的模型版本。这也简化了测试和开发等工作。您可以使用任何您想要的技术(例如,使用 Flask 库)创建自己的微服务,但为什么要重新发明轮子,当您可以直接使用 TF Serving 呢?
使用 TensorFlow Serving
TF Serving 是一个非常高效、经过实战验证的模型服务器,用 C++编写。它可以承受高负载,为您的模型提供多个版本,并监视模型存储库以自动部署最新版本,等等(参见图 19-1)。
图 19-1。TF Serving 可以为多个模型提供服务,并自动部署每个模型的最新版本。
假设您已经使用 Keras 训练了一个 MNIST 模型,并且希望将其部署到 TF Serving。您需要做的第一件事是将此模型导出为 SavedModel 格式,该格式在第十章中介绍。
导出 SavedModels
您已经知道如何保存模型:只需调用model.save()
。现在要对模型进行版本控制,您只需要为每个模型版本创建一个子目录。很简单!
from pathlib import Path import tensorflow as tf X_train, X_valid, X_test = [...] # load and split the MNIST dataset model = [...] # build & train an MNIST model (also handles image preprocessing) model_name = "my_mnist_model" model_version = "0001" model_path = Path(model_name) / model_version model.save(model_path, save_format="tf")
通常最好将所有预处理层包含在最终导出的模型中,这样一旦部署到生产环境中,模型就可以以其自然形式摄取数据。这样可以避免在使用模型的应用程序中单独处理预处理工作。将预处理步骤捆绑在模型中也使得以后更新它们更加简单,并限制了模型与所需预处理步骤之间不匹配的风险。
警告
由于 SavedModel 保存了计算图,因此它只能用于基于纯粹的 TensorFlow 操作的模型,不包括tf.py_function()
操作,该操作包装任意的 Python 代码。
TensorFlow 带有一个小的saved_model_cli
命令行界面,用于检查 SavedModels。让我们使用它来检查我们导出的模型:
$ saved_model_cli show --dir my_mnist_model/0001 The given SavedModel contains the following tag-sets: 'serve'
这个输出是什么意思?嗯,一个 SavedModel 包含一个或多个metagraphs。一个 metagraph 是一个计算图加上一些函数签名定义,包括它们的输入和输出名称、类型和形状。每个 metagraph 都由一组标签标识。例如,您可能希望有一个包含完整计算图的 metagraph,包括训练操作:您通常会将这个标记为"train"
。您可能有另一个包含经过修剪的计算图的 metagraph,只包含预测操作,包括一些特定于 GPU 的操作:这个可能被标记为"serve", "gpu"
。您可能还想要其他 metagraphs。这可以使用 TensorFlow 的低级SavedModel API来完成。然而,当您使用 Keras 模型的save()
方法保存模型时,它会保存一个标记为"serve"
的单个 metagraph。让我们检查一下这个"serve"
标签集:
$ saved_model_cli show --dir 0001/my_mnist_model --tag_set serve The given SavedModel MetaGraphDef contains SignatureDefs with these keys: SignatureDef key: "__saved_model_init_op" SignatureDef key: "serving_default"
这个元图包含两个签名定义:一个名为"__saved_model_init_op"
的初始化函数,您不需要担心,以及一个名为"serving_default"
的默认服务函数。当保存一个 Keras 模型时,默认的服务函数是模型的call()
方法,用于进行预测,这一点您已经知道了。让我们更详细地了解这个服务函数:
$ saved_model_cli show --dir 0001/my_mnist_model --tag_set serve \ --signature_def serving_default The given SavedModel SignatureDef contains the following input(s): inputs['flatten_input'] tensor_info: dtype: DT_UINT8 shape: (-1, 28, 28) name: serving_default_flatten_input:0 The given SavedModel SignatureDef contains the following output(s): outputs['dense_1'] tensor_info: dtype: DT_FLOAT shape: (-1, 10) name: StatefulPartitionedCall:0 Method name is: tensorflow/serving/predict
请注意,函数的输入被命名为"flatten_input"
,输出被命名为"dense_1"
。这些对应于 Keras 模型的输入和输出层名称。您还可以看到输入和输出数据的类型和形状。看起来不错!
现在您已经有了一个 SavedModel,下一步是安装 TF Serving。
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(八)(3)https://developer.aliyun.com/article/1482463