校园网站群建设,广州小程序开发的公司排名,西宁专业制作网站,小程序网站建设原文#xff1a;Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow 译者#xff1a;飞龙 协议#xff1a;CC BY-NC-SA 4.0 第十八章#xff1a;强化学习
强化学习#xff08;RL#xff09;是当今最激动人心的机器学习领域之一#xff0c;也是最古老… 原文Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow 译者飞龙 协议CC BY-NC-SA 4.0 第十八章强化学习
强化学习RL是当今最激动人心的机器学习领域之一也是最古老的之一。自上世纪 50 年代以来一直存在多年来产生了许多有趣的应用特别是在游戏例如 TD-Gammon一个下棋程序和机器控制方面但很少成为头条新闻。然而一场革命发生在2013 年当时来自英国初创公司 DeepMind 的研究人员展示了一个系统可以从头开始学习玩几乎任何 Atari 游戏最终在大多数游戏中超越人类只使用原始像素作为输入而不需要任何关于游戏规则的先验知识。这是一系列惊人壮举的开始最终在 2016 年 3 月他们的系统 AlphaGo 在围棋比赛中击败了传奇职业选手李世石并在 2017 年 5 月击败了世界冠军柯洁。没有任何程序曾经接近击败这个游戏的大师更不用说世界冠军了。如今整个强化学习领域充满了新的想法具有广泛的应用范围。
那么DeepMind2014 年被 Google 以超过 5 亿美元的价格收购是如何实现所有这些的呢回顾起来似乎相当简单他们将深度学习的力量应用于强化学习领域而且效果超出了他们最疯狂的梦想。在本章中我将首先解释什么是强化学习以及它擅长什么然后介绍深度强化学习中最重要的两种技术策略梯度和深度 Q 网络包括对马尔可夫决策过程的讨论。让我们开始吧
学习优化奖励
在强化学习中软件 代理 在一个 环境 中进行 观察 和 行动并从环境中获得 奖励。其目标是学会以一种方式行动以最大化其随时间的预期奖励。如果您不介意有点拟人化您可以将积极奖励视为快乐将负面奖励视为痛苦在这种情况下“奖励”这个术语有点误导。简而言之代理在环境中行动并通过试错学习来最大化其快乐并最小化其痛苦。
这是一个非常广泛的设置可以应用于各种任务。以下是一些示例参见 图 18-1 代理程序可以是控制机器人的程序。在这种情况下环境是真实世界代理通过一组传感器如摄像头和触摸传感器观察环境其行动包括发送信号以激活电机。它可能被编程为在接近目标位置时获得积极奖励而在浪费时间或走错方向时获得负面奖励。 代理可以是控制 Ms. Pac-Man 的程序。在这种情况下环境是 Atari 游戏的模拟行动是九种可能的摇杆位置左上、下、中心等观察是屏幕截图奖励只是游戏得分。 同样代理可以是玩围棋等棋盘游戏的程序。只有在赢得比赛时才会获得奖励。 代理不必控制物理或虚拟移动的东西。例如它可以是一个智能恒温器每当接近目标温度并节省能源时获得积极奖励当人类需要调整温度时获得负面奖励因此代理必须学会预测人类需求。 代理可以观察股市价格并决定每秒买入或卖出多少。奖励显然是货币收益和损失。
请注意可能根本没有任何正面奖励例如代理可能在迷宫中四处移动在每个时间步都会获得负面奖励因此最好尽快找到出口还有许多其他适合强化学习的任务示例例如自动驾驶汽车、推荐系统、在网页上放置广告或者控制图像分类系统应该关注的位置。 图 18-1. 强化学习示例(a) 机器人(b) Ms. Pac-Man© 围棋选手(d) 恒温器(e) 自动交易员⁵
策略搜索
软件代理用来确定其行动的算法称为其策略。策略可以是一个神经网络将观察作为输入并输出要采取的行动见图 18-2。 图 18-2。使用神经网络策略的强化学习
策略可以是你能想到的任何算法并且不必是确定性的。实际上在某些情况下它甚至不必观察环境例如考虑一个机器人吸尘器其奖励是在 30 分钟内吸尘的量。它的策略可以是每秒以概率p向前移动或者以概率 1 - p随机向左或向右旋转。旋转角度将是- r和 r之间的随机角度。由于这个策略涉及一些随机性它被称为随机策略。机器人将有一个不规则的轨迹这保证了它最终会到达它可以到达的任何地方并清理所有的灰尘。问题是在 30 分钟内它会吸尘多少
你会如何训练这样的机器人你只能调整两个策略参数概率p和角度范围r。一个可能的学习算法是尝试许多不同的参数值并选择表现最好的组合参见图 18-3。这是一个策略搜索的例子这种情况下使用了一种蛮力方法。当策略空间太大时这通常是情况通过这种方式找到一组好的参数就像在一个巨大的草堆中寻找一根针。
探索政策空间的另一种方法是使用遗传算法。例如您可以随机创建第一代 100 个政策并尝试它们然后“淘汰”最差的 80 个政策并让 20 个幸存者每人产生 4 个后代。后代是其父母的副本加上一些随机变化。幸存的政策及其后代一起构成第二代。您可以继续通过这种方式迭代生成直到找到一个好的政策。 图 18-3。政策空间中的四个点左和代理的相应行为右
另一种方法是使用优化技术通过评估奖励相对于策略参数的梯度然后通过沿着梯度朝着更高奖励的方向调整这些参数。我们将在本章后面更详细地讨论这种方法称为策略梯度PG。回到吸尘器机器人您可以稍微增加p并评估这样做是否会增加机器人在 30 分钟内吸尘的量如果是那么再增加p一些否则减少p。我们将使用 TensorFlow 实现一个流行的 PG 算法但在此之前我们需要为代理创建一个环境——现在是介绍 OpenAI Gym 的时候了。
OpenAI Gym 简介
强化学习的一个挑战是为了训练一个代理程序您首先需要一个可用的环境。如果您想编写一个代理程序来学习玩 Atari 游戏您将需要一个 Atari 游戏模拟器。如果您想编写一个行走机器人那么环境就是现实世界您可以直接在该环境中训练您的机器人。然而这也有其局限性如果机器人掉下悬崖您不能简单地点击撤销。您也不能加快时间——增加计算能力不会使机器人移动得更快——而且通常来说同时训练 1000 个机器人的成本太高。简而言之在现实世界中训练是困难且缓慢的因此您通常至少需要一个模拟环境来进行引导训练。例如您可以使用类似PyBullet或MuJoCo的库进行 3D 物理模拟。
OpenAI Gym是一个工具包提供各种模拟环境Atari 游戏棋盘游戏2D 和 3D 物理模拟等您可以用它来训练代理程序比较它们或者开发新的 RL 算法。
OpenAI Gym 在 Colab 上预先安装但是它是一个较旧的版本因此您需要用最新版本替换它。您还需要安装一些它的依赖项。如果您在自己的机器上编程而不是在 Colab 上并且按照https://homl.info/install上的安装说明进行操作那么您可以跳过这一步否则请输入以下命令
# Only run these commands on Colab or Kaggle!
%pip install -q -U gym
%pip install -q -U gym[classic_control,box2d,atari,accept-rom-license]第一个%pip命令将 Gym 升级到最新版本。-q选项代表quiet它使输出更简洁。-U选项代表upgrade。第二个%pip命令安装了运行各种环境所需的库。这包括来自控制理论控制动态系统的科学的经典环境例如在小车上平衡杆。它还包括基于 Box2D 库的环境——一个用于游戏的 2D 物理引擎。最后它包括基于 Arcade Learning EnvironmentALE的环境这是 Atari 2600 游戏的模拟器。几个 Atari 游戏的 ROM 会被自动下载通过运行这段代码您同意 Atari 的 ROM 许可证。
有了这个您就可以使用 OpenAI Gym 了。让我们导入它并创建一个环境
import gymenv gym.make(CartPole-v1, render_modergb_array)在这里我们创建了一个 CartPole 环境。这是一个 2D 模拟其中一个小车可以被加速向左或向右以平衡放在其顶部的杆参见图 18-4。这是一个经典的控制任务。
提示
gym.envs.registry字典包含所有可用环境的名称和规格。 图 18-4。CartPole 环境
在创建环境之后您必须使用reset()方法对其进行初始化可以选择性地指定一个随机种子。这将返回第一个观察结果。观察结果取决于环境的类型。对于 CartPole 环境每个观察结果都是一个包含四个浮点数的 1D NumPy 数组表示小车的水平位置0.0 中心其速度正数表示向右杆的角度0.0 垂直以及其角速度正数表示顺时针。reset()方法还返回一个可能包含额外环境特定信息的字典。这对于调试或训练可能很有用。例如在许多 Atari 环境中它包含剩余的生命次数。然而在 CartPole 环境中这个字典是空的。 obs, info env.reset(seed42)obs
array([ 0.0273956 , -0.00611216, 0.03585979, 0.0197368 ], dtypefloat32)info
{}让我们调用render()方法将这个环境渲染为图像。由于在创建环境时设置了render_modergb_array图像将作为一个 NumPy 数组返回 img env.render()img.shape # height, width, channels (3 Red, Green, Blue)
(400, 600, 3)然后您可以使用 Matplotlib 的imshow()函数来显示这个图像就像往常一样。
现在让我们询问环境有哪些可能的动作 env.action_space
Discrete(2)Discrete(2)表示可能的动作是整数 0 和 1分别代表向左或向右加速。其他环境可能有额外的离散动作或其他类型的动作例如连续动作。由于杆向右倾斜obs[2] 0让我们加速小车向右 action 1 # accelerate rightobs, reward, done, truncated, info env.step(action)obs
array([ 0.02727336, 0.18847767, 0.03625453, -0.26141977], dtypefloat32)reward
1.0done
Falsetruncated
Falseinfo
{}step() 方法执行所需的动作并返回五个值
obs
这是新的观察。小车现在向右移动obs[1] 0。杆仍然向右倾斜obs[2] 0但它的角速度现在是负的obs[3] 0所以在下一步之后它可能会向左倾斜。
reward
在这个环境中无论你做什么每一步都会获得 1.0 的奖励所以目标是尽可能让情节运行更长时间。
done
当情节结束时这个值将是True。当杆倾斜得太多或者离开屏幕或者经过 200 步后在这种情况下你赢了情节就会结束。之后环境必须被重置才能再次使用。
truncated
当一个情节被提前中断时这个值将是True例如通过一个强加每个情节最大步数的环境包装器请参阅 Gym 的文档以获取有关环境包装器的更多详细信息。一些强化学习算法会将截断的情节与正常结束的情节即done为True时区别对待但在本章中我们将对它们进行相同处理。
info
这个特定于环境的字典可能提供额外的信息就像reset()方法返回的那样。
提示
当你使用完一个环境后应该调用它的close()方法来释放资源。
让我们硬编码一个简单的策略当杆向左倾斜时加速向左当杆向右倾斜时加速向右。我们将运行此策略以查看它在 500 个情节中获得的平均奖励
def basic_policy(obs):angle obs[2]return 0 if angle 0 else 1totals []
for episode in range(500):episode_rewards 0obs, info env.reset(seedepisode)for step in range(200):action basic_policy(obs)obs, reward, done, truncated, info env.step(action)episode_rewards rewardif done or truncated:breaktotals.append(episode_rewards)这段代码是不言自明的。让我们看看结果 import numpy as npnp.mean(totals), np.std(totals), min(totals), max(totals)
(41.698, 8.389445512070509, 24.0, 63.0)即使尝试了 500 次这个策略也从未成功让杆连续保持直立超过 63 步。不太好。如果你看一下本章笔记本中的模拟你会看到小车左右摆动得越来越强烈直到杆倾斜得太多。让我们看看神经网络是否能提出一个更好的策略。
神经网络策略
让我们创建一个神经网络策略。这个神经网络将以观察作为输入并输出要执行的动作就像我们之前硬编码的策略一样。更准确地说它将为每个动作估计一个概率然后我们将根据估计的概率随机选择一个动作参见图 18-5。在 CartPole 环境中只有两种可能的动作左或右所以我们只需要一个输出神经元。它将输出动作 0左的概率p当然动作 1右的概率将是 1 - p。例如如果它输出 0.7那么我们将以 70%的概率选择动作 0或者以 30%的概率选择动作 1。 图 18-5. 神经网络策略
你可能会想为什么我们根据神经网络给出的概率随机选择一个动作而不是只选择得分最高的动作。这种方法让代理人在探索新动作和利用已知效果良好的动作之间找到平衡。这里有一个类比假设你第一次去一家餐馆所有菜看起来都一样吸引人所以你随机挑选了一个。如果它很好吃你可以增加下次点它的概率但你不应该将这个概率增加到 100%否则你永远不会尝试其他菜其中一些可能比你尝试的这个更好。这个探索/利用的困境在强化学习中是核心的。
还要注意在这种特定环境中过去的动作和观察可以安全地被忽略因为每个观察包含了环境的完整状态。如果有一些隐藏状态那么您可能需要考虑过去的动作和观察。例如如果环境只透露了小车的位置而没有速度那么您不仅需要考虑当前观察还需要考虑上一个观察以估计当前速度。另一个例子是当观察是嘈杂的在这种情况下通常希望使用过去几个观察来估计最可能的当前状态。因此CartPole 问题非常简单观察是无噪声的并且包含了环境的完整状态。
以下是使用 Keras 构建基本神经网络策略的代码
import tensorflow as tfmodel tf.keras.Sequential([tf.keras.layers.Dense(5, activationrelu),tf.keras.layers.Dense(1, activationsigmoid),
])我们使用Sequential模型来定义策略网络。输入的数量是观察空间的大小——在 CartPole 的情况下是 4——我们只有五个隐藏单元因为这是一个相当简单的任务。最后我们希望输出一个单一的概率——向左移动的概率——因此我们使用具有 sigmoid 激活函数的单个输出神经元。如果有超过两种可能的动作每种动作将有一个输出神经元并且我们将使用 softmax 激活函数。
好的现在我们有一个神经网络策略它将接收观察并输出动作概率。但是我们如何训练它呢
评估动作信用分配问题
如果我们知道每一步的最佳行动是什么我们可以像平常一样训练神经网络通过最小化估计概率分布与目标概率分布之间的交叉熵来实现这将只是常规的监督学习。然而在强化学习中智能体得到的唯一指导是通过奖励而奖励通常是稀疏和延迟的。例如如果智能体设法在 100 步内平衡杆它如何知道这 100 个动作中哪些是好的哪些是坏的它只知道在最后一个动作之后杆倒了但肯定不是这个最后一个动作完全负责。这被称为信用分配问题当智能体获得奖励时它很难知道哪些动作应该得到赞扬或责备。想象一只狗表现良好几个小时后才得到奖励它会明白为什么会得到奖励吗
为了解决这个问题一个常见的策略是基于之后所有奖励的总和来评估一个动作通常在每一步应用一个折扣因子γgamma。这些折扣后的奖励之和被称为动作的回报。考虑图 18-6 中的例子。如果一个智能体连续三次向右移动并在第一步后获得10 奖励在第二步后获得 0 奖励最后在第三步后获得-50 奖励那么假设我们使用一个折扣因子γ0.8第一个动作的回报将是 10 γ × 0 γ² × (–50) –22。如果折扣因子接近 0那么未来的奖励与即时奖励相比不会占据很大比重。相反如果折扣因子接近 1那么未来的奖励将几乎和即时奖励一样重要。典型的折扣因子从 0.9 到 0.99 不等。使用折扣因子 0.95未来 13 步的奖励大约相当于即时奖励的一半因为 0.95¹³ ≈ 0.5而使用折扣因子 0.99未来 69 步的奖励相当于即时奖励的一半。在 CartPole 环境中动作具有相当短期的影响因此选择折扣因子 0.95 似乎是合理的。 图 18-6。计算动作的回报折扣未来奖励之和
当然一个好的行动可能会被几个导致杆迅速倒下的坏行动跟随导致好的行动获得较低的回报。同样一个好的演员有时可能会出演一部糟糕的电影。然而如果我们玩足够多次游戏平均而言好的行动将获得比坏行动更高的回报。我们想要估计一个行动相对于其他可能行动的平均优势有多大。这被称为行动优势。为此我们必须运行许多情节并通过减去均值并除以标准差来标准化所有行动回报。之后我们可以合理地假设具有负优势的行动是坏的而具有正优势的行动是好的。现在我们有了一种评估每个行动的方法我们准备使用策略梯度来训练我们的第一个代理。让我们看看如何。
策略梯度
正如前面讨论的PG 算法通过沿着梯度朝着更高奖励的方向优化策略的参数。一种流行的 PG 算法类别称为REINFORCE 算法由 Ronald Williams 于 1992 年提出。这里是一个常见的变体 首先让神经网络策略玩游戏多次并在每一步计算使选择的行动更有可能的梯度但暂时不应用这些梯度。 在运行了几个情节之后使用前一节中描述的方法计算每个行动的优势。 如果一个行动的优势是正的这意味着这个行动可能是好的你希望应用之前计算的梯度使这个行动在未来更有可能被选择。然而如果一个行动的优势是负的这意味着这个行动可能是坏的你希望应用相反的梯度使这个行动在未来略微减少。解决方案是将每个梯度向量乘以相应行动的优势。 最后计算所有结果梯度向量的平均值并用它执行一步梯度下降。
让我们使用 Keras 来实现这个算法。我们将训练之前构建的神经网络策略使其学会在小车上平衡杆。首先我们需要一个函数来执行一步。我们暂时假设无论采取什么行动都是正确的以便我们可以计算损失及其梯度。这些梯度将暂时保存一段时间我们稍后会根据行动的好坏来修改它们
def play_one_step(env, obs, model, loss_fn):with tf.GradientTape() as tape:left_proba model(obs[np.newaxis])action (tf.random.uniform([1, 1]) left_proba)y_target tf.constant([[1.]]) - tf.cast(action, tf.float32)loss tf.reduce_mean(loss_fn(y_target, left_proba))grads tape.gradient(loss, model.trainable_variables)obs, reward, done, truncated, info env.step(int(action))return obs, reward, done, truncated, grads让我们来看看这个函数 在GradientTape块中参见第十二章我们首先调用模型给它一个观察值。我们将观察值重塑为包含单个实例的批次因为模型期望一个批次。这将输出向左移动的概率。 接下来我们随机抽取一个介于 0 和 1 之间的浮点数并检查它是否大于left_proba。action将以left_proba的概率为False或以1 - left_proba的概率为True。一旦我们将这个布尔值转换为整数行动将以适当的概率为 0左或 1右。 现在我们定义向左移动的目标概率它是 1 减去行动转换为浮点数。如果行动是 0左那么向左移动的目标概率将是 1。如果行动是 1右那么目标概率将是 0。 然后我们使用给定的损失函数计算损失并使用 tape 计算损失相对于模型可训练变量的梯度。同样这些梯度稍后会在应用之前进行调整取决于行动的好坏。 最后我们执行选择的行动并返回新的观察值、奖励、该情节是否结束、是否截断当然还有我们刚刚计算的梯度。
现在让我们创建另一个函数它将依赖于play_one_step()函数来玩多个回合返回每个回合和每个步骤的所有奖励和梯度
def play_multiple_episodes(env, n_episodes, n_max_steps, model, loss_fn):all_rewards []all_grads []for episode in range(n_episodes):current_rewards []current_grads []obs, info env.reset()for step in range(n_max_steps):obs, reward, done, truncated, grads play_one_step(env, obs, model, loss_fn)current_rewards.append(reward)current_grads.append(grads)if done or truncated:breakall_rewards.append(current_rewards)all_grads.append(current_grads)return all_rewards, all_grads这段代码返回了一个奖励列表的列表每个回合一个奖励列表每个步骤一个奖励。它还返回了一个梯度列表的列表每个回合一个梯度列表每个梯度列表包含每个步骤的一个梯度元组每个元组包含每个可训练变量的一个梯度张量。
该算法将使用play_multiple_episodes()函数多次玩游戏例如10 次然后它将回头查看所有奖励对其进行折扣并对其进行归一化。为此我们需要几个额外的函数第一个将计算每个步骤的未来折扣奖励总和第二个将通过减去均值并除以标准差来对所有这些折扣奖励即回报在许多回合中进行归一化
def discount_rewards(rewards, discount_factor):discounted np.array(rewards)for step in range(len(rewards) - 2, -1, -1):discounted[step] discounted[step 1] * discount_factorreturn discounteddef discount_and_normalize_rewards(all_rewards, discount_factor):all_discounted_rewards [discount_rewards(rewards, discount_factor)for rewards in all_rewards]flat_rewards np.concatenate(all_discounted_rewards)reward_mean flat_rewards.mean()reward_std flat_rewards.std()return [(discounted_rewards - reward_mean) / reward_stdfor discounted_rewards in all_discounted_rewards]让我们检查一下这是否有效 discount_rewards([10, 0, -50], discount_factor0.8)
array([-22, -40, -50])discount_and_normalize_rewards([[10, 0, -50], [10, 20]],
... discount_factor0.8)
...
[array([-0.28435071, -0.86597718, -1.18910299]),array([1.26665318, 1.0727777 ])]调用discount_rewards()返回了我们预期的结果见图 18-6。您可以验证函数discount_and_normalize_rewards()确实返回了两个回合中每个动作的归一化优势。请注意第一个回合比第二个回合差得多因此它的归一化优势都是负数第一个回合的所有动作都被认为是不好的反之第二个回合的所有动作都被认为是好的。
我们几乎准备好运行算法了现在让我们定义超参数。我们将运行 150 次训练迭代每次迭代玩 10 个回合每个回合最多持续 200 步。我们将使用折扣因子 0.95
n_iterations 150
n_episodes_per_update 10
n_max_steps 200
discount_factor 0.95我们还需要一个优化器和损失函数。一个常规的 Nadam 优化器学习率为 0.01将会很好地完成任务我们将使用二元交叉熵损失函数因为我们正在训练一个二元分类器有两种可能的动作——左或右
optimizer tf.keras.optimizers.Nadam(learning_rate0.01)
loss_fn tf.keras.losses.binary_crossentropy现在我们准备构建和运行训练循环
for iteration in range(n_iterations):all_rewards, all_grads play_multiple_episodes(env, n_episodes_per_update, n_max_steps, model, loss_fn)all_final_rewards discount_and_normalize_rewards(all_rewards,discount_factor)all_mean_grads []for var_index in range(len(model.trainable_variables)):mean_grads tf.reduce_mean([final_reward * all_grads[episode_index][step][var_index]for episode_index, final_rewards in enumerate(all_final_rewards)for step, final_reward in enumerate(final_rewards)], axis0)all_mean_grads.append(mean_grads)optimizer.apply_gradients(zip(all_mean_grads, model.trainable_variables))让我们逐步走过这段代码 在每次训练迭代中此循环调用play_multiple_episodes()函数该函数播放 10 个回合并返回每个步骤中每个回合的奖励和梯度。 然后我们调用discount_and_normalize_rewards()函数来计算每个动作的归一化优势这在这段代码中称为final_reward。这提供了一个衡量每个动作实际上是好还是坏的指标。 接下来我们遍历每个可训练变量并对每个变量计算所有回合和所有步骤中该变量的梯度的加权平均权重为final_reward。 最后我们使用优化器应用这些均值梯度模型的可训练变量将被微调希望策略会有所改善。
我们完成了这段代码将训练神经网络策略并成功学会在小车上平衡杆。每个回合的平均奖励将非常接近 200。默认情况下这是该环境的最大值。成功
我们刚刚训练的简单策略梯度算法解决了 CartPole 任务但是它在扩展到更大更复杂的任务时效果不佳。事实上它具有很高的样本效率低这意味着它需要很长时间探索游戏才能取得显著进展。这是因为它必须运行多个回合来估计每个动作的优势正如我们所见。然而它是更强大算法的基础比如演员-评论家算法我们将在本章末简要讨论。
提示
研究人员试图找到即使代理最初对环境一无所知也能很好运行的算法。然而除非你在写论文否则不应该犹豫向代理注入先验知识因为这将极大加快训练速度。例如由于你知道杆应该尽可能垂直你可以添加与杆角度成比例的负奖励。这将使奖励变得不那么稀疏加快训练速度。此外如果你已经有一个相当不错的策略例如硬编码你可能希望在使用策略梯度来改进之前训练神经网络来模仿它。
现在我们将看一下另一个流行的算法家族。PG 算法直接尝试优化策略以增加奖励而我们现在要探索的算法则不那么直接代理学习估计每个状态的预期回报或者每个状态中每个动作的预期回报然后利用这些知识来决定如何行动。要理解这些算法我们首先必须考虑马尔可夫决策过程MDPs。
马尔可夫决策过程
20 世纪初数学家安德烈·马尔可夫研究了没有记忆的随机过程称为马尔可夫链。这样的过程具有固定数量的状态并且在每一步中随机从一个状态演变到另一个状态。它从状态s演变到状态s′的概率是固定的仅取决于对(s, s′)这一对而不取决于过去的状态。这就是为什么我们说该系统没有记忆。
图 18-7 显示了一个具有四个状态的马尔可夫链的示例。 图 18-7. 马尔可夫链示例
假设过程从状态s[0]开始并且有 70%的概率在下一步保持在该状态。最终它必定会离开该状态并永远不会回来因为没有其他状态指向s[0]。如果它进入状态s[1]那么它很可能会进入状态s[2]90%的概率然后立即返回到状态s[1]100%的概率。它可能在这两个状态之间交替多次但最终会陷入状态s[3]并永远留在那里因为没有出路这被称为终止状态。马尔可夫链的动态可能非常不同并且在热力学、化学、统计学等领域被广泛使用。
马尔可夫决策过程是在 20 世纪 50 年代由理查德·贝尔曼首次描述的。¹² 它们类似于马尔可夫链但有一个区别在每一步中代理可以选择几种可能的动作之一转移概率取决于所选择的动作。此外一些状态转移会产生一些奖励正面或负面代理的目标是找到一个能够随时间最大化奖励的策略。
例如MDP 在图 18-8 中表示有三个状态由圆圈表示并且在每一步最多有三种可能的离散动作由菱形表示。 图 18-8. 马尔可夫决策过程示例
如果代理从状态 s[0] 开始可以在行动 a[0]、a[1] 或 a[2] 之间选择。如果选择行动 a[1]它就会肯定留在状态 s[0]没有任何奖励。因此如果愿意它可以决定永远留在那里。但如果选择行动 a[0]它有 70%的概率获得10 的奖励并留在状态 s[0]。然后它可以一次又一次地尝试获得尽可能多的奖励但最终会进入状态 s[1]。在状态 s[1] 中它只有两种可能的行动a[0] 或 a[2]。它可以通过反复选择行动 a[0] 来保持原地或者选择移动到状态 s[2] 并获得-50 的负奖励疼。在状态 s[2] 中它别无选择只能采取行动 a[1]这很可能会将其带回状态 s[0]在途中获得40 的奖励。你明白了。通过观察这个 MDP你能猜出哪种策略会随着时间获得最多的奖励吗在状态 s[0] 中很明显行动 a[0] 是最佳选择在状态 s[2] 中代理别无选择只能采取行动 a[1]但在状态 s[1] 中不明显代理应该保持原地a[0]还是冒险前进a[2]。
贝尔曼找到了一种估计任何状态 s 的最优状态值 V(s) 的方法这是代理在到达该状态后可以期望的所有折扣未来奖励的总和假设它采取最优行动。他表明如果代理采取最优行动那么贝尔曼最优性方程适用参见方程 18-1。这个递归方程表明如果代理采取最优行动那么当前状态的最优值等于在采取一个最优行动后平均获得的奖励再加上这个行动可能导致的所有可能下一个状态的期望最优值。
方程 18-1. 贝尔曼最优性方程
V ( s ) max a ∑ s‘ T ( s , a , s’ ) [ R ( s , a , s ) γ · V ( s ) ] for all s
在这个方程中 T(s, a, s′) 是从状态 s 转移到状态 s′ 的转移概率假设代理选择行动 a。例如在图 18-8 中T(s[2], a[1], s[0]) 0.8。 R(s, a, s′) 是代理从状态 s 转移到状态 s′ 时获得的奖励假设代理选择行动 a。例如在图 18-8 中R(s[2], a[1], s[0]) 40。 γ 是折扣因子。
这个方程直接导致了一个算法可以精确估计每个可能状态的最优状态值首先将所有状态值估计初始化为零然后使用值迭代算法进行迭代更新参见方程 18-2。一个显著的结果是给定足够的时间这些估计将收敛到最优状态值对应于最优策略。
方程 18-2. 值迭代算法
V k1 ( s ) ← max a ∑ s‘ T ( s , a , s’ ) [ R ( s , a , s‘ ) γ · V k ( s’ ) ] for all s
在这个方程中V**k是算法的第k次迭代中状态s的估计值。
注意
这个算法是动态规划的一个例子它将一个复杂的问题分解成可迭代处理的可解子问题。
知道最优状态值可能很有用特别是用于评估策略但它并不能为代理提供最优策略。幸运的是贝尔曼找到了一个非常相似的算法来估计最优状态-动作值通常称为Q 值质量值。状态-动作对(s, a)的最优 Q 值记为Q**(s, a)是代理在到达状态s并选择动作a*后在看到此动作结果之前可以期望平均获得的折现未来奖励的总和假设在此动作之后它表现最佳。
让我们看看它是如何工作的。再次您首先将所有 Q 值的估计初始化为零然后使用Q 值迭代算法进行更新参见方程 18-3。
方程 18-3。Q 值迭代算法
Q k1 ( s , a ) ← ∑ s‘ T ( s , a , s’ ) [ R ( s , a , s‘ ) γ · max a’ Q k ( s‘ , a’ ) ] for all ( s , a )
一旦您有了最优的 Q 值定义最优策略π**(s)是微不足道的当代理处于状态s时它应该选择具有该状态最高 Q 值的动作π(s)argmaxaQ*(s,a)。
让我们将这个算法应用到图 18-8 中表示的 MDP 中。首先我们需要定义 MDP
transition_probabilities [ # shape[s, a, s][[0.7, 0.3, 0.0], [1.0, 0.0, 0.0], [0.8, 0.2, 0.0]],[[0.0, 1.0, 0.0], None, [0.0, 0.0, 1.0]],[None, [0.8, 0.1, 0.1], None]
]
rewards [ # shape[s, a, s][[10, 0, 0], [0, 0, 0], [0, 0, 0]],[[0, 0, 0], [0, 0, 0], [0, 0, -50]],[[0, 0, 0], [40, 0, 0], [0, 0, 0]]
]
possible_actions [[0, 1, 2], [0, 2], [1]]例如要知道在执行动作a[1]后从s[2]到s[0]的转移概率我们将查找transition_probabilities[2][1][0]为 0.8。类似地要获得相应的奖励我们将查找rewards[2][1][0]为40。要获取s[2]中可能的动作列表我们将查找possible_actions[2]在这种情况下只有动作a[1]是可能的。接下来我们必须将所有 Q 值初始化为零对于不可能的动作我们将 Q 值设置为-∞
Q_values np.full((3, 3), -np.inf) # -np.inf for impossible actions
for state, actions in enumerate(possible_actions):Q_values[state, actions] 0.0 # for all possible actions现在让我们运行 Q 值迭代算法。它重复应用方程 18-3对每个状态和每个可能的动作的所有 Q 值进行计算
gamma 0.90 # the discount factorfor iteration in range(50):Q_prev Q_values.copy()for s in range(3):for a in possible_actions[s]:Q_values[s, a] np.sum([transition_probabilities[s][a][sp]* (rewards[s][a][sp] gamma * Q_prev[sp].max())for sp in range(3)])就是这样得到的 Q 值看起来像这样 Q_values
array([[18.91891892, 17.02702702, 13.62162162],[ 0\. , -inf, -4.87971488],[ -inf, 50.13365013, -inf]])例如当代理处于状态s[0]并选择动作a[1]时预期的折现未来奖励总和约为 17.0。
对于每个状态我们可以找到具有最高 Q 值的动作 Q_values.argmax(axis1) # optimal action for each state
array([0, 0, 1])这给出了在使用折扣因子为 0.90 时这个 MDP 的最优策略在状态s[0]选择动作a[0]在状态s[1]选择动作a[0]即保持不动在状态s[2]选择动作a[1]唯一可能的动作。有趣的是如果将折扣因子增加到 0.95最优策略会改变在状态s[1]中最佳动作变为a[2]冲过火。这是有道理的因为你越重视未来的奖励你就越愿意忍受现在的一些痛苦以换取未来的幸福。
时间差异学习
具有离散动作的强化学习问题通常可以建模为马尔可夫决策过程但代理最初不知道转移概率是多少它不知道T(s, a, s′)也不知道奖励将会是什么它不知道R(s, a, s′)。它必须至少体验每个状态和每个转换一次才能知道奖励如果要对转移概率有合理的估计它必须多次体验它们。
时间差异TD学习算法与 Q 值迭代算法非常相似但经过调整以考虑代理只有对 MDP 的部分知识这一事实。通常我们假设代理最初只知道可能的状态和动作什么也不知道。代理使用一个探索策略——例如一个纯随机策略——来探索 MDP随着探索的进行TD 学习算法根据实际观察到的转换和奖励更新状态值的估计参见方程 18-4。
方程 18-4. TD 学习算法
V k1 (s)←(1-α) Vk (s)α rγ· Vk (s‘) 或者等价地 Vk1 (s)←Vk (s)α· δk (s,r, s’) 其中δk (s,r,s′ )rγ · Vk (s)- Vk(s)
在这个方程中 α是学习率例如0.01。 r γ · V**k被称为TD 目标。 δk 被称为TD 误差。
写出这个方程的第一种形式的更简洁方法是使用符号a←αb意思是a[k1] ← (1 - α) · a[k] α ·b[k]。因此方程 18-4 的第一行可以重写为V(s)←αrγ·V(s)。
提示
TD 学习与随机梯度下降有许多相似之处包括一次处理一个样本。此外就像 SGD 一样只有逐渐降低学习率它才能真正收敛否则它将继续在最优 Q 值周围反弹。
对于每个状态s该算法跟踪代理离开该状态后获得的即时奖励的平均值以及它期望获得的奖励假设它采取最优行动。
Q-Learning
同样Q-learning 算法是 Q 值迭代算法在转移概率和奖励最初未知的情况下的一种适应。Q-learning 通过观察代理玩例如随机玩并逐渐改进其对 Q 值的估计来工作。一旦它有准确的 Q 值估计或足够接近那么最优策略就是选择具有最高 Q 值的动作即贪婪策略。
方程 18-5. Q-learning 算法
Q(s,a) ←α rγ· maxa‘ Q(s’, a)
对于每个状态-动作对sa该算法跟踪代理离开状态s并采取动作a后获得的奖励r的平均值以及它期望获得的折现未来奖励的总和。为了估计这个总和我们取下一个状态s′的 Q 值估计的最大值因为我们假设目标策略将从那时开始最优地行动。
让我们实现 Q-learning 算法。首先我们需要让代理探索环境。为此我们需要一个步骤函数以便代理可以执行一个动作并获得结果状态和奖励
def step(state, action):probas transition_probabilities[state][action]next_state np.random.choice([0, 1, 2], pprobas)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 statefor 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 stepalpha alpha0 / (1 iteration * decay)Q_values[state, action] * 1 - alphaQ_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.nmodel tf.keras.Sequential([tf.keras.layers.Dense(32, activationelu, input_shapeinput_shape),tf.keras.layers.Dense(32, activationelu),tf.keras.layers.Dense(n_outputs)
])使用这个 DQN 选择动作时我们选择预测 Q 值最大的动作。为了确保代理程序探索环境我们将使用ε-贪婪策略即我们将以概率ε选择一个随机动作
def epsilon_greedy_policy(state, epsilon0):if np.random.rand() epsilon:return np.random.randint(n_outputs) # random actionelse:Q_values model.predict(state[np.newaxis], verbose0)[0]return Q_values.argmax() # optimal action according to the DQN我们将不再仅基于最新经验训练 DQN而是将所有经验存储在一个重放缓冲区或重放内存中并在每次训练迭代中从中随机抽取一个训练批次。这有助于减少训练批次中经验之间的相关性从而极大地帮助训练。为此我们将使用一个双端队列deque
from collections import dequereplay_buffer deque(maxlen2000)提示
deque是一个队列可以高效地在两端添加或删除元素。从队列的两端插入和删除项目非常快但当队列变长时随机访问可能会很慢。如果您需要一个非常大的重放缓冲区您应该使用循环缓冲区请参阅笔记本中的实现或查看DeepMind 的 Reverb 库。
每个体验将由六个元素组成一个状态s代理程序执行的动作a产生的奖励r它达到的下一个状态s′一个指示该点是否结束的布尔值done最后一个指示该点是否截断的布尔值。我们将需要一个小函数从重放缓冲区中随机抽取一批体验。它将返回六个对应于六个体验元素的 NumPy 数组
def sample_experiences(batch_size):indices np.random.randint(len(replay_buffer), sizebatch_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_rate1e-2)
loss_fn tf.keras.losses.mean_squared_errordef training_step(batch_size):experiences sample_experiences(batch_size)states, actions, rewards, next_states, dones, truncateds experiencesnext_Q_values model.predict(next_states, verbose0)max_next_Q_values next_Q_values.max(axis1)runs 1.0 - (dones | truncateds) # episode is not done or truncatedtarget_Q_values rewards runs * discount_factor * max_next_Q_valuestarget_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, axis1, keepdimsTrue)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:breakif 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 models architecture
target.set_weights(model.get_weights()) # copy the weights然后在training_step()函数中我们只需要更改一行使用目标模型而不是在线模型来计算下一个状态的 Q 值
next_Q_values target.predict(next_states, verbose0)最后在训练循环中我们必须定期将在线模型的权重复制到目标模型中例如每 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 experiencesnext_Q_values model.predict(next_states, verbose0) # ≠ target.predict()best_next_actions next_Q_values.argmax(axis1)next_mask tf.one_hot(best_next_actions, n_outputs).numpy()max_next_Q_values (target.predict(next_states, verbose0) * next_mask).sum(axis1)[...] # 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, activationelu)(input_states)
hidden2 tf.keras.layers.Dense(32, activationelu)(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, axis1,keepdimsTrue)
Q_values state_values advantages
model tf.keras.Model(inputs[input_states], outputs[Q_values])算法的其余部分与之前完全相同。事实上你可以构建一个双重决斗 DQN 并将其与优先经验重放结合起来更一般地说许多 RL 技术可以结合在一起正如 DeepMind 在一篇2017 年的论文中展示的论文的作者将六种不同的技术结合到一个名为Rainbow的代理中这在很大程度上超越了现有技术水平。
正如你所看到的深度强化学习是一个快速发展的领域还有很多东西等待探索
一些流行 RL 算法的概述
在我们结束本章之前让我们简要看一下其他几种流行的算法
AlphaGo
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的帮助下学习。
异步优势 actor-criticA3C²³
这是 DeepMind 研究人员在 2016 年引入的一个重要的 actor-critic 变体其中多个代理并行学习探索环境的不同副本。定期但异步地因此得名每个代理将一些权重更新推送到主网络然后从该网络中拉取最新的权重。因此每个代理都有助于改进主网络并从其他代理学到的知识中受益。此外DQN 估计每个动作的优势而不是估计 Q 值因此名称中的第二个 A这有助于稳定训练。
优势 actor-criticA2C
A2C 是 A3C 算法的一个变体它去除了异步性。所有模型更新都是同步的因此梯度更新是在更大的批次上执行的这使模型能够更好地利用 GPU 的性能。
软 actor-criticSAC²⁴
SAC 是由 Tuomas Haarnoja 和其他加州大学伯克利分校研究人员于 2018 年提出的 actor-critic 变体。它不仅学习奖励还要最大化其动作的熵。换句话说它试图尽可能不可预测同时尽可能获得更多奖励。这鼓励代理探索环境加快训练速度并使其在 DQN 产生不完美估计时不太可能重复执行相同的动作。这个算法展示了惊人的样本效率与所有以前的算法相反学习速度非常慢。
近端策略优化PPO
这个由 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 WinderO’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麻省理工学院出版社。
2Volodymyr Mnih 等人“使用深度强化学习玩 Atari 游戏”arXiv 预印本 arXiv:1312.56022013。
3Volodymyr Mnih 等人“通过深度强化学习实现人类水平控制”自然 5182015529–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“用于连接主义强化学习的简单统计梯度跟随算法”机器学习81992229–256。
¹² Richard Bellman“马尔可夫决策过程”数学与力学杂志6第 5 期1957679–684。
¹³ Alex Irpan 在 2018 年发表的一篇很棒的文章很好地阐述了强化学习的最大困难和局限性。
¹⁴ Hado van Hasselt 等人“双 Q 学习的深度强化学习”第 30 届 AAAI 人工智能大会论文集20152094–2100。
¹⁵ Tom Schaul 等人“优先经验重放”arXiv 预印本 arXiv:1511.059522015。
¹⁶ 也可能只是奖励有噪音此时有更好的方法来估计经验的重要性请参阅论文中的一些示例。
¹⁷ Ziyu Wang 等人“用于深度强化学习的对抗网络架构”arXiv 预印本 arXiv:1511.065812015。
¹⁸ Matteo Hessel 等人“彩虹深度强化学习改进的结合”arXiv 预印本 arXiv:1710.0229820173215–3222。
¹⁹ David Silver 等人“用深度神经网络和树搜索掌握围棋”自然5292016484–489。
²⁰ David Silver 等人“在没有人类知识的情况下掌握围棋”自然5502017354–359。
²¹ David Silver 等人“通过自我对弈掌握国际象棋和将棋的一般强化学习算法”arXiv 预印本 arXiv:1712.01815。
²² Julian Schrittwieser 等人“通过学习模型计划掌握 Atari、围棋、国际象棋和将棋”arXiv 预印本 arXiv:1911.082652019。
²³ Volodymyr Mnih 等人“深度强化学习的异步方法”第 33 届国际机器学习会议论文集20161928–1937。
²⁴ Tuomas Haarnoja 等人“软演员-评论家带有随机演员的离策略最大熵深度强化学习”第 35 届国际机器学习会议论文集20181856–1865。
²⁵ John Schulman 等人“近端策略优化算法”arXiv 预印本 arXiv:1707.063472017。
²⁶ John Schulman 等人“信任区域策略优化”第 32 届国际机器学习会议论文集20151889–1897。
²⁷ Deepak Pathak 等“由自监督预测驱动的好奇心探索”第 34 届国际机器学习会议论文集20172778–2787。
²⁸ 王锐等“配对开放式先驱者POET不断生成越来越复杂和多样化的学习环境及其解决方案”arXiv 预印本 arXiv:1901.017532019。
²⁹ 王锐等“增强 POET通过无限创造学习挑战及其解决方案的开放式强化学习”arXiv 预印本 arXiv:2003.085362020。
³⁰ Open-Ended Learning Team 等“开放式学习导致普遍能力代理”arXiv 预印本 arXiv:2107.128082021。
第十九章规模化训练和部署 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 tfX_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_formattf)通常最好将所有预处理层包含在最终导出的模型中这样一旦部署到生产环境中模型就可以以其自然形式摄取数据。这样可以避免在使用模型的应用程序中单独处理预处理工作。将预处理步骤捆绑在模型中也使得以后更新它们更加简单并限制了模型与所需预处理步骤之间不匹配的风险。
警告
由于 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_UINT8shape: (-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_FLOATshape: (-1, 10)name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
请注意函数的输入被命名为flatten_input输出被命名为dense_1。这些对应于 Keras 模型的输入和输出层名称。您还可以看到输入和输出数据的类型和形状。看起来不错
现在您已经有了一个 SavedModel下一步是安装 TF Serving。
安装和启动 TensorFlow Serving
有许多安装 TF Serving 的方法使用系统的软件包管理器使用 Docker 镜像从源代码安装等。由于 Colab 运行在 Ubuntu 上我们可以像这样使用 Ubuntu 的apt软件包管理器
url https://storage.googleapis.com/tensorflow-serving-apt
src stable tensorflow-model-server tensorflow-model-server-universal
!echo deb {url} {src} /etc/apt/sources.list.d/tensorflow-serving.list
!curl {url}/tensorflow-serving.release.pub.gpg | apt-key add -
!apt update -q apt-get install -y tensorflow-model-server
%pip install -q -U tensorflow-serving-api这段代码首先将 TensorFlow 的软件包存储库添加到 Ubuntu 的软件包源列表中。然后它下载 TensorFlow 的公共 GPG 密钥并将其添加到软件包管理器的密钥列表中以便验证 TensorFlow 的软件包签名。接下来它使用apt来安装tensorflow-model-server软件包。最后它安装tensorflow-serving-api库这是我们与服务器通信所需的库。
现在我们想要启动服务器。该命令将需要基本模型目录的绝对路径即my_mnist_model的路径而不是0001所以让我们将其保存到MODEL_DIR环境变量中
import osos.environ[MODEL_DIR] str(model_path.parent.absolute())然后我们可以启动服务器
%%bash --bg
tensorflow_model_server \--port8500 \--rest_api_port8501 \--model_namemy_mnist_model \--model_base_path${MODEL_DIR} my_server.log 21在 Jupyter 或 Colab 中%%bash --bg魔术命令将单元格作为 bash 脚本执行在后台运行。my_server.log 21部分将标准输出和标准错误重定向到my_server.log文件。就是这样TF Serving 现在在后台运行其日志保存在my_server.log中。它加载了我们的 MNIST 模型版本 1现在正在分别等待 gRPC 和 REST 请求端口分别为 8500 和 8501。
现在服务器已经启动运行让我们首先使用 REST API然后使用 gRPC API 进行查询。
通过 REST API 查询 TF Serving
让我们从创建查询开始。它必须包含您想要调用的函数签名的名称当然还有输入数据。由于请求必须使用 JSON 格式我们必须将输入图像从 NumPy 数组转换为 Python 列表
import jsonX_new X_test[:3] # pretend we have 3 new digit images to classify
request_json json.dumps({signature_name: serving_default,instances: X_new.tolist(),
})请注意JSON 格式是 100%基于文本的。请求字符串如下所示 request_json
{signature_name: serving_default, instances: [[[0, 0, 0, 0, ... ]]]}现在让我们通过 HTTP POST 请求将这个请求发送到 TF Serving。这可以使用requests库来完成它不是 Python 标准库的一部分但在 Colab 上是预安装的
import requestsserver_url http://localhost:8501/v1/models/my_mnist_model:predict
response requests.post(server_url, datarequest_json)
response.raise_for_status() # raise an exception in case of error
response response.json()如果一切顺利响应应该是一个包含单个predictions键的字典。相应的值是预测列表。这个列表是一个 Python 列表所以让我们将其转换为 NumPy 数组并将其中包含的浮点数四舍五入到第二位小数 import numpy as npy_proba np.array(response[predictions])y_proba.round(2)
array([[0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 1\. , 0\. , 0\. ],[0\. , 0\. , 0.99, 0.01, 0\. , 0\. , 0\. , 0\. , 0\. , 0\. ],[0\. , 0.97, 0.01, 0\. , 0\. , 0\. , 0\. , 0.01, 0\. , 0\. ]])万岁我们有了预测模型几乎 100%确信第一张图片是 799%确信第二张图片是 297%确信第三张图片是 1。这是正确的。
REST API 简单易用当输入和输出数据不太大时效果很好。此外几乎任何客户端应用程序都可以在没有额外依赖的情况下进行 REST 查询而其他协议并不总是那么容易获得。然而它基于 JSON这是基于文本且相当冗长的。例如我们不得不将 NumPy 数组转换为 Python 列表每个浮点数最终都表示为一个字符串。这非常低效无论是在序列化/反序列化时间方面——我们必须将所有浮点数转换为字符串然后再转回来——还是在有效载荷大小方面许多浮点数最终使用超过 15 个字符来表示这相当于 32 位浮点数超过 120 位这将导致在传输大型 NumPy 数组时出现高延迟和带宽使用。因此让我们看看如何改用 gRPC。
提示
在传输大量数据或延迟重要时最好使用 gRPC API如果客户端支持的话因为它使用紧凑的二进制格式和基于 HTTP/2 framing 的高效通信协议。
通过 gRPC API 查询 TF Serving
gRPC API 期望一个序列化的PredictRequest协议缓冲区作为输入并输出一个序列化的PredictResponse协议缓冲区。这些 protobufs 是tensorflow-serving-api库的一部分我们之前安装过。首先让我们创建请求
from tensorflow_serving.apis.predict_pb2 import PredictRequestrequest PredictRequest()
request.model_spec.name model_name
request.model_spec.signature_name serving_default
input_name model.input_names[0] # flatten_input
request.inputs[input_name].CopyFrom(tf.make_tensor_proto(X_new))这段代码创建了一个PredictRequest协议缓冲区并填充了必需的字段包括模型名称之前定义的我们想要调用的函数的签名名称最后是输入数据以Tensor协议缓冲区的形式。tf.make_tensor_proto()函数根据给定的张量或 NumPy 数组创建一个Tensor协议缓冲区这里是X_new。
接下来我们将向服务器发送请求并获取其响应。为此我们将需要grpcio库该库已预先安装在 Colab 中
import grpc
from tensorflow_serving.apis import prediction_service_pb2_grpcchannel grpc.insecure_channel(localhost:8500)
predict_service prediction_service_pb2_grpc.PredictionServiceStub(channel)
response predict_service.Predict(request, timeout10.0)代码非常简单在导入之后我们在 TCP 端口 8500 上创建一个到localhost的 gRPC 通信通道然后我们在该通道上创建一个 gRPC 服务并使用它发送一个带有 10 秒超时的请求。请注意调用是同步的它将阻塞直到收到响应或超时期限到期。在此示例中通道是不安全的没有加密没有身份验证但 gRPC 和 TF Serving 也支持通过 SSL/TLS 的安全通道。
接下来让我们将PredictResponse协议缓冲区转换为张量
output_name model.output_names[0] # dense_1
outputs_proto response.outputs[output_name]
y_proba tf.make_ndarray(outputs_proto)如果您运行此代码并打印y_proba.round(2)您将获得与之前完全相同的估计类概率。这就是全部内容只需几行代码您现在就可以远程访问您的 TensorFlow 模型使用 REST 或 gRPC。
部署新的模型版本
现在让我们创建一个新的模型版本并导出一个 SavedModel这次导出到my_mnist_model/0002目录
model [...] # build and train a new MNIST model versionmodel_version 0002
model_path Path(model_name) / model_version
model.save(model_path, save_formattf)在固定的时间间隔延迟可配置TF Serving 会检查模型目录是否有新的模型版本。如果找到一个新版本它会自动优雅地处理过渡默认情况下它会用前一个模型版本回答待处理的请求如果有的话同时用新版本处理新请求。一旦每个待处理的请求都得到回答之前的模型版本就会被卸载。您可以在 TF Serving 日志my_server.log中看到这个过程
[...]
Reading SavedModel from: /models/my_mnist_model/0002
Reading meta graph with tags { serve }
[...]
Successfully loaded servable version {name: my_mnist_model version: 2}
Quiescing servable version {name: my_mnist_model version: 1}
Done quiescing servable version {name: my_mnist_model version: 1}
Unloading servable version {name: my_mnist_model version: 1}
提示
如果 SavedModel 包含assets/extra目录中的一些示例实例您可以配置 TF Serving 在开始使用它来处理请求之前在这些实例上运行新模型。这称为模型预热它将确保一切都被正确加载避免第一次请求的长响应时间。
这种方法提供了平稳的过渡但可能会使用过多的 RAM特别是 GPU RAM通常是最有限的。在这种情况下您可以配置 TF Serving使其处理所有挂起的请求与先前的模型版本并在加载和使用新的模型版本之前卸载它。这种配置将避免同时加载两个模型版本但服务将在短时间内不可用。
正如您所看到的TF Serving 使部署新模型变得简单。此外如果您发现第二个版本的效果不如预期那么回滚到第一个版本就像删除my_mnist_model/0002目录一样简单。
提示
TF Serving 的另一个重要特性是其自动批处理能力您可以在启动时使用--enable_batching选项来激活它。当 TF Serving 在短时间内接收到多个请求时延迟可配置它会在使用模型之前自动将它们批处理在一起。通过利用 GPU 的性能这将显著提高性能。一旦模型返回预测结果TF Serving 会将每个预测结果分发给正确的客户端。通过增加批处理延迟参见--batching_parameters_file选项您可以在一定程度上牺牲一点延迟以获得更大的吞吐量。
如果您希望每秒获得许多查询您将希望在多台服务器上部署 TF Serving 并负载平衡查询请参见图 19-2。这将需要在这些服务器上部署和管理许多 TF Serving 容器。处理这一问题的一种方法是使用诸如Kubernetes之类的工具它是一个简化跨多台服务器容器编排的开源系统。如果您不想购买、维护和升级所有硬件基础设施您将希望在云平台上使用虚拟机如 Amazon AWS、Microsoft Azure、Google Cloud Platform、IBM Cloud、Alibaba Cloud、Oracle Cloud 或其他平台即服务PaaS提供商。管理所有虚拟机处理容器编排即使借助 Kubernetes 的帮助照顾 TF Serving 配置、调整和监控——所有这些都可能成为一项全职工作。幸运的是一些服务提供商可以为您处理所有这些事务。在本章中我们将使用 Vertex AI它是今天唯一支持 TPUs 的平台它支持 TensorFlow 2、Scikit-Learn 和 XGBoost并提供一套不错的人工智能服务。在这个领域还有其他几家提供商也能够提供 TensorFlow 模型的服务比如 Amazon AWS SageMaker 和 Microsoft AI Platform所以请确保也查看它们。 图 19-2。使用负载平衡扩展 TF Serving
现在让我们看看如何在云上提供我们出色的 MNIST 模型
在 Vertex AI 上创建一个预测服务
Vertex AI 是 Google Cloud PlatformGCP内的一个平台提供各种与人工智能相关的工具和服务。您可以上传数据集让人类对其进行标记将常用特征存储在特征存储中并将其用于训练或生产中使用多个 GPU 或 TPU 服务器进行模型训练并具有自动超参数调整或模型架构搜索AutoML功能。您还可以管理已训练的模型使用它们对大量数据进行批量预测为数据工作流程安排多个作业通过 REST 或 gRPC 以规模化方式提供模型服务并在名为Workbench的托管 Jupyter 环境中对数据和模型进行实验。甚至还有一个Matching Engine服务可以非常高效地比较向量即近似最近邻。GCP 还包括其他 AI 服务例如计算机视觉、翻译、语音转文本等 API。
在我们开始之前有一些设置需要处理 登录您的 Google 账户然后转到Google Cloud Platform 控制台参见图 19-3。如果您没有 Google 账户您将需要创建一个。 如果这是您第一次使用 GCP您将需要阅读并接受条款和条件。新用户可以获得免费试用包括价值 300 美元的 GCP 信用您可以在 90 天内使用截至 2022 年 5 月。您只需要其中的一小部分来支付本章中将使用的服务。注册免费试用后您仍然需要创建一个付款配置文件并输入您的信用卡号码这是用于验证目的——可能是为了避免人们多次使用免费试用但您不会被收取前 300 美元的费用之后只有在您选择升级到付费账户时才会收费。 图 19-3. Google Cloud Platform 控制台 如果您以前使用过 GCP 并且您的免费试用已经过期那么您在本章中将使用的服务将会花费一些钱。这不应该太多特别是如果您记得在不再需要这些服务时关闭它们。在运行任何服务之前请确保您理解并同意定价条件。如果服务最终花费超出您的预期我在此不承担任何责任还请确保您的计费账户是活动的。要检查请打开左上角的☰导航菜单点击计费然后确保您已设置付款方式并且计费账户是活动的。 GCP 中的每个资源都属于一个 项目。这包括您可能使用的所有虚拟机、存储的文件和运行的训练作业。当您创建一个帐户时GCP 会自动为您创建一个名为“我的第一个项目”的项目。如果您愿意可以通过转到项目设置来更改其显示名称在 ☰ 导航菜单中选择“IAM 和管理员 → 设置”更改项目的显示名称然后单击“保存”。请注意项目还有一个唯一的 ID 和编号。您可以在创建项目时选择项目 ID但以后无法更改。项目编号是自动生成的无法更更改。如果您想创建一个新项目请单击页面顶部的项目名称然后单击“新项目”并输入项目名称。您还可以单击“编辑”来设置项目 ID。确保此新项目的计费处于活动状态以便可以对服务费用进行计费如果有免费信用。 警告 请始终设置提醒以便在您知道只需要几个小时时关闭服务否则您可能会让其运行数天或数月从而产生潜在的显著成本。 现在您已经拥有 GCP 帐户和项目并且计费已激活您必须激活所需的 API。在☰导航菜单中选择“API 和服务”确保启用了 Cloud Storage API。如果需要点击启用 API 和服务找到 Cloud Storage并启用它。还要启用 Vertex AI API。
您可以继续通过 GCP 控制台完成所有操作但我建议改用 Python这样您可以编写脚本来自动化几乎任何您想要在 GCP 上完成的任务而且通常比通过菜单和表单点击更方便特别是对于常见任务。
在您使用任何 GCP 服务之前您需要做的第一件事是进行身份验证。在使用 Colab 时最简单的解决方案是执行以下代码
from google.colab import authauth.authenticate_user()认证过程基于 OAuth 2.0一个弹出窗口会要求您确认您希望 Colab 笔记本访问您的 Google 凭据。如果您接受您必须选择与 GCP 相同的 Google 帐户。然后您将被要求确认您同意授予 Colab 对 Google Drive 和 GCP 中所有数据的完全访问权限。如果您允许访问只有当前笔记本将具有访问权限并且仅在 Colab 运行时到期之前。显然只有在您信任笔记本中的代码时才应接受此操作。
警告
如果您不使用来自 https://github.com/ageron/handson-ml3 的官方笔记本则应格外小心如果笔记本的作者心怀不轨他们可能包含代码来对您的数据进行任何操作。
现在让我们创建一个 Google Cloud Storage 存储桶来存储我们的 SavedModelsGCS 的存储桶是您数据的容器。为此我们将使用预先安装在 Colab 中的google-cloud-storage库。我们首先创建一个Client对象它将作为与 GCS 的接口然后我们使用它来创建存储桶
from google.cloud import storageproject_id my_project # change this to your project ID
bucket_name my_bucket # change this to a unique bucket name
location us-central1storage_client storage.Client(projectproject_id)
bucket storage_client.create_bucket(bucket_name, locationlocation)提示
如果您想重用现有的存储桶请将最后一行替换为bucket storage_client.bucket(bucket_name)。确保location设置为存储桶的地区。
GCS 使用单个全球命名空间用于存储桶因此像“machine-learning”这样的简单名称很可能不可用。确保存储桶名称符合 DNS 命名约定因为它可能在 DNS 记录中使用。此外存储桶名称是公开的因此不要在名称中放入任何私人信息。通常使用您的域名、公司名称或项目 ID 作为前缀以确保唯一性或者只需在名称中使用一个随机数字。
如果您想要可以更改区域但请确保选择支持 GPU 的区域。此外您可能需要考虑到不同区域之间价格差异很大一些区域产生的 CO₂比其他区域多得多一些区域不支持所有服务并且使用单一区域存储桶可以提高性能。有关更多详细信息请参阅Google Cloud 的区域列表和Vertex AI 的位置文档。如果您不确定最好选择us-central1。
接下来让我们将my_mnist_model目录上传到新的存储桶。在 GCS 中文件被称为blobs或objects在幕后它们都只是放在存储桶中没有任何目录结构。Blob 名称可以是任意的 Unicode 字符串甚至可以包含斜杠(/)。GCP 控制台和其他工具使用这些斜杠来产生目录的幻觉。因此当我们上传my_mnist_model目录时我们只关心文件而不是目录。
def upload_directory(bucket, dirpath):dirpath Path(dirpath)for filepath in dirpath.glob(**/*):if filepath.is_file():blob bucket.blob(filepath.relative_to(dirpath.parent).as_posix())blob.upload_from_filename(filepath)upload_directory(bucket, my_mnist_model)这个函数现在运行良好但如果有很多文件要上传它会非常慢。通过多线程可以很容易地大大加快速度请参阅笔记本中的实现。或者如果您有 Google Cloud CLI则可以使用以下命令
!gsutil -m cp -r my_mnist_model gs://{bucket_name}/接下来让我们告诉 Vertex AI 关于我们的 MNIST 模型。要与 Vertex AI 通信我们可以使用google-cloud-aiplatform库它仍然使用旧的 AI Platform 名称而不是 Vertex AI。它在 Colab 中没有预安装所以我们需要安装它。之后我们可以导入该库并进行初始化——只需指定一些项目 ID 和位置的默认值——然后我们可以创建一个新的 Vertex AI 模型我们指定一个显示名称我们模型的 GCS 路径在这种情况下是版本 0001以及我们希望 Vertex AI 使用的 Docker 容器的 URL 来运行此模型。如果您访问该 URL 并向上导航一个级别您将找到其他可以使用的容器。这个支持带有 GPU 的 TensorFlow 2.8
from google.cloud import aiplatformserver_image gcr.io/cloud-aiplatform/prediction/tf2-gpu.2-8:latestaiplatform.init(projectproject_id, locationlocation)
mnist_model aiplatform.Model.upload(display_namemnist,artifact_urifgs://{bucket_name}/my_mnist_model/0001,serving_container_image_uriserver_image,
)现在让我们部署这个模型这样我们就可以通过 gRPC 或 REST API 查询它以进行预测。为此我们首先需要创建一个端点。这是客户端应用程序在想要访问服务时连接的地方。然后我们需要将我们的模型部署到这个端点
endpoint aiplatform.Endpoint.create(display_namemnist-endpoint)endpoint.deploy(mnist_model,min_replica_count1,max_replica_count5,machine_typen1-standard-4,accelerator_typeNVIDIA_TESLA_K80,accelerator_count1
)这段代码可能需要几分钟才能运行因为 Vertex AI 需要设置一个虚拟机。在这个例子中我们使用一个相当基本的 n1-standard-4 类型的机器查看https://homl.info/machinetypes 获取其他类型。我们还使用了一个基本的 NVIDIA_TESLA_K80 类型的 GPU查看https://homl.info/accelerators 获取其他类型。如果您选择的区域不是 us-central1那么您可能需要将机器类型或加速器类型更改为该区域支持的值例如并非所有区域都有 Nvidia Tesla K80 GPU。
注意
Google Cloud Platform 实施各种 GPU 配额包括全球范围和每个地区您不能在未经 Google 授权的情况下创建成千上万个 GPU 节点。要检查您的配额请在 GCP 控制台中打开“IAM 和管理员 → 配额”。如果某些配额太低例如如果您需要在特定地区更多的 GPU您可以要求增加它们通常需要大约 48 小时。
Vertex AI 将最初生成最少数量的计算节点在这种情况下只有一个每当每秒查询次数变得过高时它将生成更多节点最多为您定义的最大数量这种情况下为五个并在它们之间负载均衡查询。如果一段时间内 QPS 速率下降Vertex AI 将自动停止额外的计算节点。因此成本直接与负载、您选择的机器和加速器类型以及您在 GCS 上存储的数据量相关。这种定价模型非常适合偶尔使用者和有重要使用高峰的服务。对于初创公司来说也是理想的价格保持低延迟到公司真正开始运营。
恭喜您已经将第一个模型部署到云端现在让我们查询这个预测服务
response endpoint.predict(instancesX_new.tolist())我们首先需要将要分类的图像转换为 Python 列表就像我们之前使用 REST API 向 TF Serving 发送请求时所做的那样。响应对象包含预测结果表示为 Python 浮点数列表的列表。让我们将它们四舍五入到两位小数并将它们转换为 NumPy 数组 import numpy as npnp.round(response.predictions, 2)
array([[0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 0\. , 1\. , 0\. , 0\. ],[0\. , 0\. , 0.99, 0.01, 0\. , 0\. , 0\. , 0\. , 0\. , 0\. ],[0\. , 0.97, 0.01, 0\. , 0\. , 0\. , 0\. , 0.01, 0\. , 0\. ]])是的我们得到了与之前完全相同的预测结果。我们现在在云上有一个很好的预测服务我们可以从任何地方安全地查询并且可以根据 QPS 的数量自动扩展或缩小。当您使用完端点后请不要忘记将其删除以避免无谓地支付费用
endpoint.undeploy_all() # undeploy all models from the endpoint
endpoint.delete()现在让我们看看如何在 Vertex AI 上运行作业对可能非常大的数据批次进行预测。
在 Vertex AI 上运行批量预测作业
如果我们需要进行大量预测那么我们可以请求 Vertex AI 为我们运行预测作业而不是重复调用我们的预测服务。这不需要端点只需要一个模型。例如让我们在测试集的前 100 张图像上运行一个预测作业使用我们的 MNIST 模型。为此我们首先需要准备批处理并将其上传到 GCS。一种方法是创建一个文件每行包含一个实例每个实例都格式化为 JSON 值——这种格式称为 JSON Lines——然后将此文件传递给 Vertex AI。因此让我们在一个新目录中创建一个 JSON Lines 文件然后将此目录上传到 GCS
batch_path Path(my_mnist_batch)
batch_path.mkdir(exist_okTrue)
with open(batch_path / my_mnist_batch.jsonl, w) as jsonl_file:for image in X_test[:100].tolist():jsonl_file.write(json.dumps(image))jsonl_file.write(\n)upload_directory(bucket, batch_path)现在我们准备启动预测作业指定作业的名称、要使用的机器和加速器的类型和数量刚刚创建的 JSON Lines 文件的 GCS 路径以及 Vertex AI 将保存模型预测的 GCS 目录的路径
batch_prediction_job mnist_model.batch_predict(job_display_namemy_batch_prediction_job,machine_typen1-standard-4,starting_replica_count1,max_replica_count5,accelerator_typeNVIDIA_TESLA_K80,accelerator_count1,gcs_source[fgs://{bucket_name}/{batch_path.name}/my_mnist_batch.jsonl],gcs_destination_prefixfgs://{bucket_name}/my_mnist_predictions/,syncTrue # set to False if you dont want to wait for completion
)提示
对于大批量数据您可以将输入拆分为多个 JSON Lines 文件并通过gcs_source参数列出它们。
这将需要几分钟的时间主要是为了在 Vertex AI 上生成计算节点。一旦这个命令完成预测将会以类似prediction.results-00001-of-00002的文件集合中可用。这些文件默认使用 JSON Lines 格式每个值都是包含实例及其对应预测即 10 个概率的字典。实例按照输入的顺序列出。该作业还会输出prediction-errors文件如果出现问题这些文件对于调试可能会有用。我们可以使用batch_prediction_job.iter_outputs()迭代所有这些输出文件所以让我们遍历所有的预测并将它们存储在y_probas数组中
y_probas []
for blob in batch_prediction_job.iter_outputs():if prediction.results in blob.name:for line in blob.download_as_text().splitlines():y_proba json.loads(line)[prediction]y_probas.append(y_proba)现在让我们看看这些预测有多好 y_pred np.argmax(y_probas, axis1)accuracy np.sum(y_pred y_test[:100]) / 100
0.98很好98%的准确率
JSON Lines 格式是默认格式但是当处理大型实例如图像时它太冗长了。幸运的是batch_predict()方法接受一个instances_format参数让您可以选择另一种格式。它默认为jsonl但您可以将其更改为csv、tf-record、tf-record-gzip、bigquery或file-list。如果将其设置为file-list那么gcs_source参数应指向一个文本文件其中每行包含一个输入文件路径例如指向 PNG 图像文件。Vertex AI 将读取这些文件作为二进制文件使用 Base64 对其进行编码并将生成的字节字符串传递给模型。这意味着您必须在模型中添加一个预处理层来解析 Base64 字符串使用tf.io.decode_base64()。如果文件是图像则必须使用类似tf.io.decode_image()或tf.io.decode_png()的函数来解析结果如第十三章中所讨论的。
当您完成使用模型后如果需要可以通过运行mnist_model.delete()来删除它。您还可以删除在您的 GCS 存储桶中创建的目录可选地删除存储桶本身如果为空以及批量预测作业。
for prefix in [my_mnist_model/, my_mnist_batch/, my_mnist_predictions/]:blobs bucket.list_blobs(prefixprefix)for blob in blobs:blob.delete()bucket.delete() # if the bucket is empty
batch_prediction_job.delete()您现在知道如何将模型部署到 Vertex AI创建预测服务并运行批量预测作业。但是如果您想将模型部署到移动应用程序或者嵌入式设备比如加热控制系统、健身追踪器或自动驾驶汽车呢
将模型部署到移动设备或嵌入式设备
机器学习模型不仅限于在拥有多个 GPU 的大型集中式服务器上运行它们可以更接近数据源运行这被称为边缘计算例如在用户的移动设备或嵌入式设备中。去中心化计算并将其移向边缘有许多好处它使设备即使未连接到互联网时也能智能化通过不必将数据发送到远程服务器来减少延迟并减轻服务器负载并且可能提高隐私性因为用户的数据可以保留在设备上。
然而将模型部署到边缘也有其缺点。与强大的多 GPU 服务器相比设备的计算资源通常很少。一个大模型可能无法适应设备可能使用过多的 RAM 和 CPU并且可能下载时间过长。结果应用可能变得无响应设备可能会发热并迅速耗尽电池。为了避免这一切您需要制作一个轻量级且高效的模型而不会牺牲太多准确性。 TFLite库提供了几个工具帮助您将模型部署到边缘主要有三个目标 减小模型大小缩短下载时间并减少 RAM 使用量。 减少每次预测所需的计算量以减少延迟、电池使用量和发热。 使模型适应特定设备的限制。
为了减小模型大小TFLite 的模型转换器可以接受 SavedModel 并将其压缩为基于 FlatBuffers 的更轻量级格式。这是一个高效的跨平台序列化库有点像协议缓冲区最初由谷歌为游戏创建。它设计成可以直接将 FlatBuffers 加载到 RAM 中无需任何预处理这样可以减少加载时间和内存占用。一旦模型加载到移动设备或嵌入式设备中TFLite 解释器将执行它以进行预测。以下是如何将 SavedModel 转换为 FlatBuffer 并保存为 .tflite 文件的方法
converter tf.lite.TFLiteConverter.from_saved_model(str(model_path))
tflite_model converter.convert()
with open(my_converted_savedmodel.tflite, wb) as f:f.write(tflite_model)提示
您还可以使用 tf.lite.TFLiteConverter.from_keras_model(model) 将 Keras 模型直接保存为 FlatBuffer 格式。
转换器还优化模型既缩小模型大小又减少延迟。它修剪所有不需要进行预测的操作例如训练操作并在可能的情况下优化计算例如3 × a 4 ×_ a_ 5 × a 将被转换为 12 × a。此外它尝试在可能的情况下融合操作。例如如果可能的话批量归一化层最终会合并到前一层的加法和乘法操作中。要了解 TFLite 可以对模型进行多少优化可以下载其中一个预训练的 TFLite 模型例如Inception_V1_quant点击tflitepb解压缩存档然后打开优秀的Netron 图形可视化工具并上传*.pb文件以查看原始模型。这是一个庞大而复杂的图形对吧接下来打开优化后的.tflite*模型惊叹于其美丽
除了简单地使用较小的神经网络架构之外您可以减小模型大小的另一种方法是使用较小的位宽例如如果您使用半精度浮点数16 位而不是常规浮点数32 位模型大小将缩小 2 倍代价是通常很小的准确度下降。此外训练速度将更快您将使用大约一半的 GPU 内存。
TFLite 的转换器可以进一步将模型权重量化为固定点、8 位整数与使用 32 位浮点数相比这导致了四倍的大小减小。最简单的方法称为后训练量化它只是在训练后量化权重使用一种相当基本但高效的对称量化技术。它找到最大绝对权重值m然后将浮点范围–m到m映射到固定点整数范围–127 到127。例如如果权重范围从–1.5 到0.8则字节–127、0 和127 将分别对应于浮点–1.5、0.0 和1.5参见图 19-5。请注意当使用对称量化时0.0 始终映射为 0。还请注意在此示例中不会使用字节值68 到127因为它们映射到大于0.8 的浮点数。 图 19-5。从 32 位浮点数到 8 位整数使用对称量化
要执行这种训练后的量化只需在调用convert()方法之前将DEFAULT添加到转换器优化列表中
converter.optimizations [tf.lite.Optimize.DEFAULT]这种技术显著减小了模型的大小使得下载速度更快占用的存储空间更少。在运行时量化的权重在使用之前会被转换回浮点数。这些恢复的浮点数与原始浮点数并不完全相同但也不会相差太远因此精度损失通常是可以接受的。为了避免一直重新计算浮点值这样会严重减慢模型的速度TFLite 会对其进行缓存不幸的是这意味着这种技术并不会减少 RAM 的使用量也不会加快模型的速度。它主要用于减小应用程序的大小。
减少延迟和功耗的最有效方法是对激活进行量化使得计算可以完全使用整数而无需任何浮点运算。即使使用相同的位宽例如32 位整数而不是 32 位浮点数整数计算使用的 CPU 周期更少消耗的能量更少产生的热量也更少。如果还减少位宽例如降至 8 位整数可以获得巨大的加速。此外一些神经网络加速器设备如 Google 的 Edge TPU只能处理整数因此权重和激活的完全量化是强制性的。这可以在训练后完成它需要一个校准步骤来找到激活的最大绝对值因此您需要向 TFLite 提供代表性的训练数据样本不需要很大它将通过模型处理数据并测量量化所需的激活统计信息。这一步通常很快。
量化的主要问题是它会失去一点准确性这类似于在权重和激活中添加噪声。如果准确性下降太严重那么您可能需要使用量化感知训练。这意味着向模型添加虚假量化操作以便它在训练过程中学会忽略量化噪声最终的权重将更加稳健地适应量化。此外校准步骤可以在训练过程中自动处理这简化了整个过程。
我已经解释了 TFLite 的核心概念但要完全编写移动或嵌入式应用程序需要一本专门的书。幸运的是一些书籍存在如果您想了解有关为移动和嵌入式设备构建 TensorFlow 应用程序的更多信息请查看 O’Reilly 的书籍TinyML: Machine Learning with TensorFlow on Arduino and Ultra-Low Power Micro-Controllers作者是 Pete WardenTFLite 团队的前负责人和 Daniel Situnayake以及AI and Machine Learning for On-Device Development作者是 Laurence Moroney。
那么如果您想在网站中使用您的模型在用户的浏览器中直接运行呢
在网页中运行模型
在客户端即用户的浏览器中运行您的机器学习模型而不是在服务器端运行可以在许多场景下非常有用例如 当您的网络应用经常在用户的连接不稳定或缓慢的情况下使用例如徒步者的网站因此在客户端直接运行模型是使您的网站可靠的唯一方法。 当您需要模型的响应尽可能快时例如用于在线游戏。消除查询服务器进行预测的需要肯定会减少延迟并使网站更加响应。 当您的网络服务基于一些私人用户数据进行预测并且您希望通过在客户端进行预测来保护用户的隐私以便私人数据永远不必离开用户的设备。
对于所有这些场景您可以使用TensorFlow.jsTFJSJavaScript 库。该库可以在用户的浏览器中加载 TFLite 模型并直接进行预测。例如以下 JavaScript 模块导入了 TFJS 库下载了一个预训练的 MobileNet 模型并使用该模型对图像进行分类并记录预测结果。您可以在https://homl.info/tfjscode上尝试这段代码使用 Glitch.com这是一个允许您免费在浏览器中构建 Web 应用程序的网站点击页面右下角的预览按钮查看代码的运行情况
import https://cdn.jsdelivr.net/npm/tensorflow/tfjslatest;
import https://cdn.jsdelivr.net/npm/tensorflow-models/mobilenet1.0.0;const image document.getElementById(image);mobilenet.load().then(model {model.classify(image).then(predictions {for (var i 0; i predictions.length; i) {let className predictions[i].classNamelet proba (predictions[i].probability * 100).toFixed(1)console.log(className : proba %);}});
});甚至可以将这个网站转变成一个渐进式 Web 应用程序PWA这是一个遵守一系列标准的网站使其可以在任何浏览器中查看甚至可以在移动设备上作为独立应用程序安装。例如在移动设备上尝试访问https://homl.info/tfjswpa大多数现代浏览器会询问您是否想要将 TFJS 演示添加到主屏幕。如果您接受您将在应用程序列表中看到一个新图标。点击此图标将在其自己的窗口中加载 TFJS 演示网站就像常规移动应用程序一样。PWA 甚至可以配置为离线工作通过使用服务工作者这是一个在浏览器中以自己独立线程运行的 JavaScript 模块拦截网络请求使其可以缓存资源从而使 PWA 可以更快地运行甚至完全离线运行。它还可以传递推送消息在后台运行任务等。PWA 允许您管理 Web 和移动设备的单个代码库。它们还使得更容易确保所有用户运行您应用程序的相同版本。您可以在 Glitch.com 上玩这个 TFJS 演示的 PWA 代码网址是https://homl.info/wpacode。
提示
在https://tensorflow.org/js/demos上查看更多在您的浏览器中运行的机器学习模型的演示。
TFJS 还支持在您的网络浏览器中直接训练模型而且速度相当快。如果您的计算机有 GPU 卡那么 TFJS 通常可以使用它即使它不是 Nvidia 卡。实际上TFJS 将在可用时使用 WebGL由于现代网络浏览器通常支持各种 GPU 卡TFJS 实际上支持的 GPU 卡比常规的 TensorFlow 更多后者仅支持 Nvidia 卡。
在用户的网络浏览器中训练模型可以特别有用可以确保用户的数据保持私密。模型可以在中央进行训练然后在浏览器中根据用户的数据进行本地微调。如果您对这个话题感兴趣请查看联邦学习。
再次强调要全面涵盖这个主题需要一本完整的书。如果您想了解更多关于 TensorFlow.js 的内容请查看 O’reilly 图书《云端、移动和边缘的实用深度学习》Anirudh Koul 等著或《学习 TensorFlow.js》Gant Laborde 著。
现在您已经看到如何将 TensorFlow 模型部署到 TF Serving或者通过 Vertex AI 部署到云端或者使用 TFLite 部署到移动和嵌入式设备或者使用 TFJS 部署到 Web 浏览器让我们讨论如何使用 GPU 加速计算。
使用 GPU 加速计算
在第十一章中我们看了几种可以显著加快训练速度的技术更好的权重初始化、复杂的优化器等等。但即使使用了所有这些技术使用单个 CPU 的单台机器训练大型神经网络可能需要几个小时、几天甚至几周具体取决于任务。由于 GPU 的出现这种训练时间可以缩短到几分钟或几小时。这不仅节省了大量时间还意味着您可以更轻松地尝试各种模型并经常使用新数据重新训练您的模型。
在之前的章节中我们在 Google Colab 上使用了启用 GPU 的运行时。您只需从运行时菜单中选择“更改运行时类型”然后选择 GPU 加速器类型TensorFlow 会自动检测 GPU 并使用它加速计算代码与没有 GPU 时完全相同。然后在本章中您看到了如何将模型部署到 Vertex AI 上的多个启用 GPU 的计算节点只需在创建 Vertex AI 模型时选择正确的启用 GPU 的 Docker 镜像并在调用endpoint.deploy()时选择所需的 GPU 类型。但是如果您想购买自己的 GPU 怎么办如果您想在单台机器上的 CPU 和多个 GPU 设备之间分发计算参见图 19-6这是我们现在将讨论的内容然后在本章的后面部分我们将讨论如何在多个服务器上分发计算。 图 19-6。在多个设备上并行执行 TensorFlow 图
获取自己的 GPU
如果你知道你将会长时间大量使用 GPU那么购买自己的 GPU 可能是经济上合理的。你可能也想在本地训练模型因为你不想将数据上传到云端。或者你只是想购买一张用于游戏的 GPU 卡并且想将其用于深度学习。
如果您决定购买 GPU 卡那么请花些时间做出正确的选择。您需要考虑您的任务所需的 RAM 数量例如图像处理或 NLP 通常至少需要 10GB带宽即您可以将数据发送到 GPU 和从 GPU 中发送数据的速度核心数量冷却系统等。Tim Dettmers 撰写了一篇优秀的博客文章来帮助您选择我鼓励您仔细阅读。在撰写本文时TensorFlow 仅支持具有 CUDA Compute Capability 3.5的 Nvidia 卡当然还有 Google 的 TPU但它可能会将其支持扩展到其他制造商因此请务必查看TensorFlow 的文档以了解今天支持哪些设备。
如果您选择 Nvidia GPU 卡您将需要安装适当的 Nvidia 驱动程序和几个 Nvidia 库。这些包括计算统一设备架构库CUDA工具包它允许开发人员使用支持 CUDA 的 GPU 进行各种计算不仅仅是图形加速以及CUDA 深度神经网络库cuDNN一个 GPU 加速的常见 DNN 计算库例如激活层、归一化、前向和反向卷积以及池化参见第十四章。cuDNN 是 Nvidia 的深度学习 SDK 的一部分。请注意您需要创建一个 Nvidia 开发者帐户才能下载它。TensorFlow 使用 CUDA 和 cuDNN 来控制 GPU 卡并加速计算参见图 19-7。 图 19-7. TensorFlow 使用 CUDA 和 cuDNN 来控制 GPU 并加速 DNNs
安装了 GPU 卡和所有必需的驱动程序和库之后您可以使用nvidia-smi命令来检查一切是否正确安装。该命令列出了可用的 GPU 卡以及每张卡上运行的所有进程。在这个例子中这是一张 Nvidia Tesla T4 GPU 卡大约有 15GB 的可用内存并且当前没有任何进程在运行
$ nvidia-smi
Sun Apr 10 04:52:10 2022
-----------------------------------------------------------------------------
| NVIDIA-SMI 460.32.03 Driver Version: 460.32.03 CUDA Version: 11.2 |
|---------------------------------------------------------------------------
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
||
| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |
| N/A 34C P8 9W / 70W | 3MiB / 15109MiB | 0% Default |
| | | N/A |
--------------------------------------------------------------------------------------------------------------------------------------------------------
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
||
| No running processes found |
-----------------------------------------------------------------------------
要检查 TensorFlow 是否真正看到您的 GPU请运行以下命令并确保结果不为空 physical_gpus tf.config.list_physical_devices(GPU)physical_gpus
[PhysicalDevice(name/physical_device:GPU:0, device_typeGPU)]管理 GPU 内存
默认情况下TensorFlow 在第一次运行计算时会自动占用几乎所有可用 GPU 的 RAM以限制 GPU RAM 的碎片化。这意味着如果您尝试启动第二个 TensorFlow 程序或任何需要 GPU 的程序它将很快耗尽 RAM。这种情况并不像您可能认为的那样经常发生因为通常您会在一台机器上运行一个单独的 TensorFlow 程序通常是一个训练脚本、一个 TF Serving 节点或一个 Jupyter 笔记本。如果出于某种原因需要运行多个程序例如在同一台机器上并行训练两个不同的模型那么您需要更均匀地在这些进程之间分配 GPU RAM。
如果您的机器上有多个 GPU 卡一个简单的解决方案是将每个 GPU 卡分配给单个进程。为此您可以设置CUDA_VISIBLE_DEVICES环境变量以便每个进程只能看到适当的 GPU 卡。还要设置CUDA_DEVICE_ORDER环境变量为PCI_BUS_ID以确保每个 ID 始终指向相同的 GPU 卡。例如如果您有四个 GPU 卡您可以启动两个程序将两个 GPU 分配给每个程序通过在两个单独的终端窗口中执行以下命令来实现
$ CUDA_DEVICE_ORDERPCI_BUS_IDCUDA_VISIBLE_DEVICES0,1python3program_1.py*# andinanotherterminal:*$ CUDA_DEVICE_ORDERPCI_BUS_IDCUDA_VISIBLE_DEVICES3,2python3program_2.py程序 1 将只看到 GPU 卡 0 和 1分别命名为/gpu:0和/gpu:1在 TensorFlow 中程序 2 将只看到 GPU 卡 2 和 3分别命名为/gpu:1和/gpu:0注意顺序。一切都将正常工作参见图 19-8。当然您也可以在 Python 中通过设置os.environ[CUDA_DEVICE_ORDER]和os.environ[CUDA_VISIBLE_DEVICES]来定义这些环境变量只要在使用 TensorFlow 之前这样做。 图 19-8。每个程序获得两个 GPU
另一个选项是告诉 TensorFlow 只获取特定数量的 GPU RAM。这必须在导入 TensorFlow 后立即完成。例如要使 TensorFlow 只在每个 GPU 上获取 2 GiB 的 RAM您必须为每个物理 GPU 设备创建一个逻辑 GPU 设备有时称为虚拟 GPU 设备并将其内存限制设置为 2 GiB即 2,048 MiB:
for gpu in physical_gpus:tf.config.set_logical_device_configuration(gpu,[tf.config.LogicalDeviceConfiguration(memory_limit2048)])假设您有四个 GPU每个 GPU 至少有 4 GiB 的 RAM在这种情况下可以并行运行两个像这样的程序每个程序使用所有四个 GPU 卡请参见图 19-9。如果在两个程序同时运行时运行nvidia-smi命令则应该看到每个进程在每张卡上占用 2 GiB 的 RAM。 图 19-9。每个程序都可以获得四个 GPU但每个 GPU 只有 2 GiB 的 RAM
另一个选项是告诉 TensorFlow 只在需要时获取内存。同样在导入 TensorFlow 后必须立即执行此操作
for gpu in physical_gpus:tf.config.experimental.set_memory_growth(gpu, True)另一种方法是将TF_FORCE_GPU_ALLOW_GROWTH环境变量设置为true。使用这个选项TensorFlow 一旦分配了内存就不会释放它再次为了避免内存碎片化除非程序结束。使用这个选项很难保证确定性行为例如一个程序可能会崩溃因为另一个程序的内存使用量激增因此在生产环境中您可能会选择之前的选项之一。然而有一些情况下它非常有用例如当您使用一台机器运行多个 Jupyter 笔记本时其中几个使用了 TensorFlow。在 Colab 运行时TF_FORCE_GPU_ALLOW_GROWTH环境变量被设置为true。
最后在某些情况下您可能希望将一个 GPU 分成两个或更多逻辑设备。例如如果您只有一个物理 GPU比如在 Colab 运行时但您想要测试一个多 GPU 算法这将非常有用。以下代码将 GPU0 分成两个逻辑设备每个设备有 2 GiB 的 RAM同样在导入 TensorFlow 后立即执行
tf.config.set_logical_device_configuration(physical_gpus[0],[tf.config.LogicalDeviceConfiguration(memory_limit2048),tf.config.LogicalDeviceConfiguration(memory_limit2048)]
)这两个逻辑设备被称为/gpu:0和/gpu:1, 你可以像使用两个普通 GPU 一样使用它们。你可以像这样列出所有逻辑设备 logical_gpus tf.config.list_logical_devices(GPU)logical_gpus
[LogicalDevice(name/device:GPU:0, device_typeGPU),LogicalDevice(name/device:GPU:1, device_typeGPU)]现在让我们看看 TensorFlow 如何决定应该使用哪些设备来放置变量和执行操作。
将操作和变量放在设备上
Keras 和 tf.data 通常会很好地将操作和变量放在它们应该在的位置但如果您想要更多控制您也可以手动将操作和变量放在每个设备上 通常您希望将数据预处理操作放在 CPU 上并将神经网络操作放在 GPU 上。 GPU 通常具有相对有限的通信带宽因此重要的是要避免不必要的数据传输进出 GPU。 向机器添加更多的 CPU RAM 是简单且相对便宜的因此通常有很多而 GPU RAM 是内置在 GPU 中的它是一种昂贵且有限的资源因此如果一个变量在接下来的几个训练步骤中不需要它可能应该放在 CPU 上例如数据集通常应该放在 CPU 上。
默认情况下所有变量和操作都将放置在第一个 GPU 上命名为/gpu:0除非变量和操作没有 GPU 内核这些将放置在 CPU 上始终命名为/cpu:0。张量或变量的device属性告诉您它被放置在哪个设备上。 a tf.Variable([1., 2., 3.]) # float32 variable goes to the GPUa.device
/job:localhost/replica:0/task:0/device:GPU:0b tf.Variable([1, 2, 3]) # int32 variable goes to the CPUb.device
/job:localhost/replica:0/task:0/device:CPU:0您现在可以安全地忽略前缀/job:localhost/replica:0/task:0我们将在本章后面讨论作业、副本和任务。正如您所看到的第一个变量被放置在 GPU0 上这是默认设备。但是第二个变量被放置在 CPU 上这是因为整数变量没有 GPU 内核或者涉及整数张量的操作没有 GPU 内核因此 TensorFlow 回退到 CPU。
如果您想在与默认设备不同的设备上执行操作请使用tf.device()上下文 with tf.device(/cpu:0):
... c tf.Variable([1., 2., 3.])
...c.device
/job:localhost/replica:0/task:0/device:CPU:0注意
CPU 始终被视为单个设备/cpu:0即使您的计算机有多个 CPU 核心。放置在 CPU 上的任何操作如果具有多线程内核则可能在多个核心上并行运行。
如果您明确尝试将操作或变量放置在不存在或没有内核的设备上那么 TensorFlow 将悄悄地回退到默认选择的设备。当您希望能够在不具有相同数量的 GPU 的不同机器上运行相同的代码时这是很有用的。但是如果您希望获得异常可以运行tf.config.set_soft_device_placement(False)。
现在TensorFlow 如何在多个设备上执行操作呢
跨多个设备并行执行
正如我们在第十二章中看到的使用 TF 函数的一个好处是并行性。让我们更仔细地看一下这一点。当 TensorFlow 运行一个 TF 函数时它首先分析其图形找到需要评估的操作列表并计算每个操作的依赖关系数量。然后 TensorFlow 将每个具有零依赖关系的操作即每个源操作添加到该操作设备的评估队列中参见图 19-10。一旦一个操作被评估依赖于它的每个操作的依赖计数器都会减少。一旦一个操作的依赖计数器达到零它就会被推送到其设备的评估队列中。一旦所有输出都被计算出来它们就会被返回。 图 19-10. TensorFlow 图的并行执行
CPU 的评估队列中的操作被分派到一个称为inter-op 线程池的线程池中。如果 CPU 有多个核心那么这些操作将有效地并行评估。一些操作具有多线程 CPU 内核这些内核将其任务分割为多个子操作这些子操作被放置在另一个评估队列中并分派到一个称为intra-op 线程池的第二线程池中由所有多线程 CPU 内核共享。简而言之多个操作和子操作可能在不同的 CPU 核心上并行评估。
对于 GPU 来说情况要简单一些。GPU 的评估队列中的操作是按顺序评估的。然而大多数操作都有多线程 GPU 内核通常由 TensorFlow 依赖的库实现比如 CUDA 和 cuDNN。这些实现有自己的线程池它们通常会利用尽可能多的 GPU 线程这就是为什么 GPU 不需要一个跨操作线程池的原因每个操作已经占用了大部分 GPU 线程。
例如在图 19-10 中操作 A、B 和 C 是源操作因此它们可以立即被评估。操作 A 和 B 被放置在 CPU 上因此它们被发送到 CPU 的评估队列然后被分派到跨操作线程池并立即并行评估。操作 A 恰好有一个多线程内核它的计算被分成三部分在操作线程池中并行执行。操作 C 进入 GPU #0 的评估队列在这个例子中它的 GPU 内核恰好使用 cuDNN它管理自己的内部操作线程池并在许多 GPU 线程之间并行运行操作。假设 C 先完成。D 和 E 的依赖计数器被减少到 0因此两个操作都被推送到 GPU #0 的评估队列并按顺序执行。请注意即使 D 和 E 都依赖于 CC 也只被评估一次。假设 B 接下来完成。然后 F 的依赖计数器从 4 减少到 3由于不为 0它暂时不运行。一旦 A、D 和 E 完成那么 F 的依赖计数器达到 0它被推送到 CPU 的评估队列并被评估。最后TensorFlow 返回请求的输出。
TensorFlow 执行的另一个神奇之处是当 TF 函数修改状态资源例如变量时它确保执行顺序与代码中的顺序匹配即使语句之间没有显式依赖关系。例如如果您的 TF 函数包含v.assign_add(1)然后是v.assign(v * 2)TensorFlow 将确保这些操作按照这个顺序执行。
提示
您可以通过调用tf.config.threading.set_inter_op_parallelism_threads()来控制 inter-op 线程池中的线程数。要设置 intra-op 线程数请使用tf.config.threading.set_intra_op_parallelism_threads()。如果您不希望 TensorFlow 使用所有 CPU 核心或者希望它是单线程的这将非常有用。¹²
有了这些您就拥有了在任何设备上运行任何操作并利用 GPU 的能力所需的一切以下是您可以做的一些事情 您可以并行训练多个模型每个模型都在自己的 GPU 上只需为每个模型编写一个训练脚本并在并行运行时设置CUDA_DEVICE_ORDER和CUDA_VISIBLE_DEVICES以便每个脚本只能看到一个 GPU 设备。这对于超参数调整非常有用因为您可以并行训练具有不同超参数的多个模型。如果您有一台具有两个 GPU 的单台机器并且在一个 GPU 上训练一个模型需要一个小时那么并行训练两个模型每个模型都在自己专用的 GPU 上只需要一个小时。简单 您可以在单个 GPU 上训练一个模型并在 CPU 上并行执行所有预处理操作使用数据集的prefetch()方法提前准备好接下来的几批数据以便在 GPU 需要时立即使用参见第十三章。 如果您的模型接受两个图像作为输入并在使用两个 CNN 处理它们之前将它们连接起来那么如果您将每个 CNN 放在不同的 GPU 上它可能会运行得更快。 您可以创建一个高效的集成只需在每个 GPU 上放置一个不同训练过的模型这样您就可以更快地获得所有预测结果以生成集成的最终预测。
但是如果您想通过使用多个 GPU 加速训练呢
在多个设备上训练模型
训练单个模型跨多个设备有两种主要方法模型并行其中模型在设备之间分割和数据并行其中模型在每个设备上复制并且每个副本在不同的数据子集上进行训练。让我们看看这两种选择。
模型并行
到目前为止我们已经在单个设备上训练了每个神经网络。如果我们想要在多个设备上训练单个神经网络怎么办这需要将模型分割成单独的块并在不同的设备上运行每个块。不幸的是这种模型并行化实际上非常棘手其有效性确实取决于神经网络的架构。对于全连接网络从这种方法中通常无法获得太多好处。直觉上似乎将模型分割的一种简单方法是将每一层放在不同的设备上但这并不起作用因为每一层都需要等待前一层的输出才能执行任何操作。也许你可以垂直切割它——例如将每一层的左半部分放在一个设备上右半部分放在另一个设备上这样稍微好一些因为每一层的两半确实可以并行工作但问题在于下一层的每一半都需要上一层两半的输出因此会有大量的跨设备通信由虚线箭头表示。这很可能会完全抵消并行计算的好处因为跨设备通信速度很慢当设备位于不同的机器上时更是如此。 图 19-11。拆分完全连接的神经网络
一些神经网络架构如卷积神经网络参见第十四章包含仅部分连接到较低层的层因此更容易以有效的方式在设备之间分发块参见图 19-12。 图 19-12。拆分部分连接的神经网络
深度递归神经网络参见第十五章可以更有效地跨多个 GPU 进行分割。如果将网络水平分割将每一层放在不同的设备上并将输入序列输入网络进行处理那么在第一个时间步中只有一个设备会处于活动状态处理序列的第一个值在第二个时间步中两个设备会处于活动状态第二层将处理第一层的输出值而第一层将处理第二个值当信号传播到输出层时所有设备将同时处于活动状态图 19-13。尽管设备之间仍然存在大量的跨设备通信但由于每个单元可能相当复杂理论上并行运行多个单元的好处可能会超过通信惩罚。然而在实践中在单个 GPU 上运行的常规堆叠LSTM层实际上运行得更快。 图 19-13。拆分深度递归神经网络
简而言之模型并行可能会加快某些类型的神经网络的运行或训练速度但并非所有类型的神经网络都适用并且需要特别注意和调整例如确保需要进行通信的设备在同一台机器上运行。接下来我们将看一个更简单且通常更有效的选择数据并行。
数据并行
另一种并行训练神经网络的方法是在每个设备上复制它并在所有副本上同时运行每个训练步骤为每个副本使用不同的小批量。然后对每个副本计算的梯度进行平均并将结果用于更新模型参数。这被称为数据并行有时也称为单程序多数据SPMD。这个想法有许多变体让我们看看最重要的几种。
使用镜像策略的数据并行
可以说最简单的方法是在所有 GPU 上完全镜像所有模型参数并始终在每个 GPU 上应用完全相同的参数更新。这样所有副本始终保持完全相同。这被称为镜像策略在使用单台机器时特别高效参见图 19-14。 图 19-14. 使用镜像策略的数据并行
使用这种方法的棘手部分是高效地计算所有 GPU 的所有梯度的平均值并将结果分布到所有 GPU 上。这可以使用AllReduce算法来完成这是一类算法多个节点合作以高效地执行reduce 操作例如计算平均值、总和和最大值同时确保所有节点获得相同的最终结果。幸运的是有现成的实现这种算法您将会看到。
集中式参数的数据并行
另一种方法是将模型参数存储在执行计算的 GPU 设备之外称为工作器例如在 CPU 上参见图 19-15。在分布式设置中您可以将所有参数放在一个或多个仅称为参数服务器的 CPU 服务器上其唯一作用是托管和更新参数。 图 19-15. 集中式参数的数据并行
镜像策略强制所有 GPU 上的权重更新同步进行而这种集中式方法允许同步或异步更新。让我们来看看这两种选择的优缺点。
同步更新
在同步更新中聚合器会等待所有梯度可用后再计算平均梯度并将其传递给优化器优化器将更新模型参数。一旦一个副本完成计算其梯度它必须等待参数更新后才能继续下一个小批量。缺点是一些设备可能比其他设备慢因此快速设备将不得不在每一步等待慢速设备使整个过程与最慢设备一样慢。此外参数将几乎同时复制到每个设备上在梯度应用后立即这可能会饱和参数服务器的带宽。
提示
为了减少每个步骤的等待时间您可以忽略最慢几个副本通常约 10%的梯度。例如您可以运行 20 个副本但每个步骤只聚合来自最快的 18 个副本的梯度并忽略最后 2 个的梯度。一旦参数更新前 18 个副本可以立即开始工作而无需等待最慢的 2 个副本。这种设置通常被描述为有 18 个副本加上 2 个备用副本。
异步更新
使用异步更新时每当一个副本完成梯度计算后梯度立即用于更新模型参数。没有聚合它删除了“均值”步骤在图 19-15 中和没有同步。副本独立于其他副本工作。由于不需要等待其他副本这种方法每分钟可以运行更多的训练步骤。此外尽管参数仍然需要在每一步复制到每个设备但对于每个副本这发生在不同的时间因此带宽饱和的风险降低了。
使用异步更新的数据并行是一个吸引人的选择因为它简单、没有同步延迟并且更好地利用了带宽。然而尽管在实践中它表现得相当不错但它能够工作几乎令人惊讶事实上当一个副本基于某些参数值计算梯度完成时这些参数将已经被其他副本多次更新如果有N个副本则平均更新N - 1 次并且无法保证计算出的梯度仍然指向正确的方向参见图 19-16。当梯度严重过时时它们被称为过时梯度它们可以减慢收敛速度引入噪声和摆动效应学习曲线可能包含临时振荡甚至可能使训练算法发散。 图 19-16。使用异步更新时的过时梯度
有几种方法可以减少陈旧梯度的影响 降低学习率。 丢弃陈旧的梯度或将其缩小。 调整小批量大小。 在开始的几个时期只使用一个副本这被称为热身阶段。在训练开始阶段梯度通常很大参数还没有稳定在成本函数的谷底因此陈旧的梯度可能会造成更大的损害不同的副本可能会将参数推向完全不同的方向。
2016 年Google Brain 团队发表的一篇论文对各种方法进行了基准测试发现使用同步更新和一些备用副本比使用异步更新更有效不仅收敛更快而且产生了更好的模型。然而这仍然是一个活跃的研究领域所以你不应该立刻排除异步更新。
带宽饱和
无论您使用同步还是异步更新具有集中参数的数据并行仍然需要在每个训练步骤开始时将模型参数从参数服务器传递到每个副本并在每个训练步骤结束时将梯度传递到另一个方向。同样当使用镜像策略时每个 GPU 生成的梯度将需要与每个其他 GPU 共享。不幸的是通常会出现这样一种情况即添加额外的 GPU 将不会改善性能因为将数据移入和移出 GPU RAM以及在分布式设置中跨网络所花费的时间将超过通过分割计算负载获得的加速效果。在那一点上添加更多的 GPU 将只会加剧带宽饱和并实际上减慢训练速度。
饱和对于大型密集模型来说更严重因为它们有很多参数和梯度需要传输。对于小型模型来说饱和程度较轻但并行化增益有限对于大型稀疏模型也较轻因为梯度通常大部分为零可以有效传输。Google Brain 项目的发起人和负责人 Jeff Dean 报告 在将计算分布到 50 个 GPU 上时密集模型的典型加速为 25-40 倍而在 500 个 GPU 上训练稀疏模型时加速为 300 倍。正如你所看到的稀疏模型确实更好地扩展。以下是一些具体例子 神经机器翻译在 8 个 GPU 上加速 6 倍 Inception/ImageNet在 50 个 GPU 上加速 32 倍 RankBrain在 500 个 GPU 上加速 300 倍
有很多研究正在进行以缓解带宽饱和问题目标是使训练能够与可用的 GPU 数量成线性比例扩展。例如卡内基梅隆大学、斯坦福大学和微软研究团队在 2018 年提出了一个名为PipeDream的系统成功将网络通信减少了 90%以上使得可以在多台机器上训练大型模型成为可能。他们使用了一种称为管道并行的新技术来实现这一目标该技术结合了模型并行和数据并行模型被切分成连续的部分称为阶段每个阶段在不同的机器上进行训练。这导致了一个异步的管道所有机器都在很少的空闲时间内并行工作。在训练过程中每个阶段交替进行一轮前向传播和一轮反向传播它从输入队列中提取一个小批量数据处理它并将输出发送到下一个阶段的输入队列然后从梯度队列中提取一个小批量的梯度反向传播这些梯度并更新自己的模型参数并将反向传播的梯度推送到前一个阶段的梯度队列。然后它一遍又一遍地重复整个过程。每个阶段还可以独立地使用常规的数据并行例如使用镜像策略而不受其他阶段的影响。 图 19-17。PipeDream 的管道并行性
然而正如在这里展示的那样PipeDream 不会工作得那么好。要理解原因考虑在 Figure 19-17 中的第 5 个小批次当它在前向传递过程中经过第 1 阶段时来自第 4 个小批次的梯度尚未通过该阶段进行反向传播但是当第 5 个小批次的梯度流回到第 1 阶段时第 4 个小批次的梯度将已经被用来更新模型参数因此第 5 个小批次的梯度将有点过时。正如我们所看到的这可能会降低训练速度和准确性甚至使其发散阶段越多这个问题就会变得越糟糕。论文的作者提出了缓解这个问题的方法例如每个阶段在前向传播过程中保存权重并在反向传播过程中恢复它们以确保相同的权重用于前向传递和反向传递。这被称为权重存储。由于这一点PipeDream 展示了令人印象深刻的扩展能力远远超出了简单的数据并行性。
这个研究领域的最新突破是由谷歌研究人员在一篇2022 年的论文中发表的他们开发了一个名为Pathways的系统利用自动模型并行、异步团队调度等技术实现了数千个 TPU 几乎 100%的硬件利用率调度意味着组织每个任务必须运行的时间和位置团队调度意味着同时并行运行相关任务并且彼此靠近以减少任务等待其他任务输出的时间。正如我们在第十六章中看到的这个系统被用来在超过 6,000 个 TPU 上训练一个庞大的语言模型几乎实现了 100%的硬件利用率这是一个令人惊叹的工程壮举。
在撰写本文时Pathways 尚未公开但很可能在不久的将来您将能够使用 Pathways 或类似系统在 Vertex AI 上训练大型模型。与此同时为了减少饱和问题您可能会希望使用一些强大的 GPU而不是大量的弱 GPU如果您需要在多台服务器上训练模型您应该将 GPU 分组在少数且连接非常良好的服务器上。您还可以尝试将浮点精度从 32 位tf.float32降低到 16 位tf.bfloat16。这将减少一半的数据传输量通常不会对收敛速度或模型性能产生太大影响。最后如果您正在使用集中式参数您可以将参数分片分割到多个参数服务器上增加更多的参数服务器将减少每个服务器上的网络负载并限制带宽饱和的风险。
好的现在我们已经讨论了所有的理论让我们实际在多个 GPU 上训练一个模型
使用分布策略 API 进行规模训练
幸运的是TensorFlow 带有一个非常好的 API它负责处理将模型分布在多个设备和机器上的所有复杂性分布策略 API。要在所有可用的 GPU 上暂时只在单台机器上使用数据并行性和镜像策略训练一个 Keras 模型只需创建一个MirroredStrategy对象调用它的scope()方法以获取一个分布上下文并将模型的创建和编译包装在该上下文中。然后正常调用模型的fit()方法
strategy tf.distribute.MirroredStrategy()with strategy.scope():model tf.keras.Sequential([...]) # create a Keras model normallymodel.compile([...]) # compile the model normallybatch_size 100 # preferably divisible by the number of replicas
model.fit(X_train, y_train, epochs10,validation_data(X_valid, y_valid), batch_sizebatch_size)在底层Keras 是分布感知的因此在这个MirroredStrategy上下文中它知道必须在所有可用的 GPU 设备上复制所有变量和操作。如果你查看模型的权重它们是MirroredVariable类型的 type(model.weights[0])
tensorflow.python.distribute.values.MirroredVariable请注意fit() 方法会自动将每个训练批次在所有副本之间进行分割因此最好确保批次大小可以被副本数量即可用的 GPU 数量整除以便所有副本获得相同大小的批次。就是这样训练通常会比使用单个设备快得多而且代码更改确实很小。
训练模型完成后您可以使用它高效地进行预测调用predict()方法它会自动将批处理在所有副本之间分割以并行方式进行预测。再次强调批处理大小必须能够被副本数量整除。如果调用模型的save()方法它将被保存为常规模型而不是具有多个副本的镜像模型。因此当您加载它时它将像常规模型一样运行在单个设备上默认情况下在 GPU0 上如果没有 GPU 则在 CPU 上。如果您想加载一个模型并在所有可用设备上运行它您必须在分发上下文中调用tf.keras.models.load_model()
with strategy.scope():model tf.keras.models.load_model(my_mirrored_model)如果您只想使用所有可用 GPU 设备的子集您可以将列表传递给MirroredStrategy的构造函数
strategy tf.distribute.MirroredStrategy(devices[/gpu:0, /gpu:1])默认情况下MirroredStrategy类使用NVIDIA Collective Communications LibraryNCCL进行 AllReduce 均值操作但您可以通过将cross_device_ops参数设置为tf.distribute.HierarchicalCopyAllReduce类的实例或tf.distribute.ReductionToOneDevice类的实例来更改它。默认的 NCCL 选项基于tf.distribute.NcclAllReduce类通常更快但这取决于 GPU 的数量和类型因此您可能想尝试一下其他选择。
如果您想尝试使用集中式参数的数据并行性请将MirroredStrategy替换为CentralStorageStrategy
strategy tf.distribute.experimental.CentralStorageStrategy()您可以选择设置compute_devices参数来指定要用作工作器的设备列表-默认情况下将使用所有可用的 GPU-您还可以选择设置parameter_device参数来指定要存储参数的设备。默认情况下将使用 CPU或者如果只有一个 GPU则使用 GPU。
现在让我们看看如何在一组 TensorFlow 服务器上训练模型
在 TensorFlow 集群上训练模型
TensorFlow 集群是一组在并行运行的 TensorFlow 进程通常在不同的机器上并相互通信以完成一些工作例如训练或执行神经网络模型。集群中的每个 TF 进程被称为任务或TF 服务器。它有一个 IP 地址一个端口和一个类型也称为角色或工作。类型可以是worker、chief、ps参数服务器或evaluator 每个worker执行计算通常在一台或多台 GPU 的机器上。 首席执行计算任务它是一个工作者但也处理额外的工作比如编写 TensorBoard 日志或保存检查点。集群中只有一个首席。如果没有明确指定首席则按照惯例第一个工作者就是首席。 参数服务器只跟踪变量值并且通常在仅有 CPU 的机器上。这种类型的任务只能与ParameterServerStrategy一起使用。 评估者显然负责评估。这种类型并不经常使用当使用时通常只有一个评估者。
要启动一个 TensorFlow 集群必须首先定义其规范。这意味着定义每个任务的 IP 地址、TCP 端口和类型。例如以下集群规范定义了一个有三个任务的集群两个工作者和一个参数服务器参见图 19-18。集群规范是一个字典每个作业对应一个键值是任务地址IP:port的列表
cluster_spec {worker: [machine-a.example.com:2222, # /job:worker/task:0machine-b.example.com:2222 # /job:worker/task:1],ps: [machine-a.example.com:2221] # /job:ps/task:0
}通常每台机器上会有一个任务但正如这个示例所示如果需要您可以在同一台机器上配置多个任务。在这种情况下如果它们共享相同的 GPU请确保 RAM 适当分配如前面讨论的那样。
警告
默认情况下集群中的每个任务可以与其他任务通信因此请确保配置防火墙以授权这些机器之间这些端口上的所有通信如果每台机器使用相同的端口则通常更简单。 图 19-18。一个示例 TensorFlow 集群
当您开始一个任务时您必须给它指定集群规范并且还必须告诉它它的类型和索引是什么例如worker #0。一次性指定所有内容的最简单方法包括集群规范和当前任务的类型和索引是在启动 TensorFlow 之前设置TF_CONFIG环境变量。它必须是一个 JSON 编码的字典包含集群规范在cluster键下和当前任务的类型和索引在task键下。例如以下TF_CONFIG环境变量使用我们刚刚定义的集群并指定要启动的任务是 worker #0
os.environ[TF_CONFIG] json.dumps({cluster: cluster_spec,task: {type: worker, index: 0}
})提示
通常您希望在 Python 之外定义TF_CONFIG环境变量这样代码就不需要包含当前任务的类型和索引这样可以在所有工作节点上使用相同的代码。
现在让我们在集群上训练一个模型我们将从镜像策略开始。首先您需要为每个任务适当设置TF_CONFIG环境变量。集群规范中不应该有参数服务器删除集群规范中的ps键通常每台机器上只需要一个工作节点。确保为每个任务设置不同的任务索引。最后在每个工作节点上运行以下脚本
import tempfile
import tensorflow as tfstrategy tf.distribute.MultiWorkerMirroredStrategy() # at the start!
resolver tf.distribute.cluster_resolver.TFConfigClusterResolver()
print(fStarting task {resolver.task_type} #{resolver.task_id})
[...] # load and split the MNIST datasetwith strategy.scope():model tf.keras.Sequential([...]) # build the Keras modelmodel.compile([...]) # compile the modelmodel.fit(X_train, y_train, validation_data(X_valid, y_valid), epochs10)if resolver.task_id 0: # the chief saves the model to the right locationmodel.save(my_mnist_multiworker_model, save_formattf)
else:tmpdir tempfile.mkdtemp() # other workers save to a temporary directorymodel.save(tmpdir, save_formattf)tf.io.gfile.rmtree(tmpdir) # and we can delete this directory at the end!这几乎是您之前使用的相同代码只是这次您正在使用MultiWorkerMirroredStrategy。当您在第一个工作节点上启动此脚本时它们将在 AllReduce 步骤处保持阻塞但是一旦最后一个工作节点启动训练将开始并且您将看到它们以完全相同的速度前进因为它们在每一步都进行同步。
警告
在使用MultiWorkerMirroredStrategy时重要的是确保所有工作人员做同样的事情包括保存模型检查点或编写 TensorBoard 日志即使您只保留主要写入的内容。这是因为这些操作可能需要运行 AllReduce 操作因此所有工作人员必须保持同步。
这个分发策略有两种 AllReduce 实现方式基于 gRPC 的环形 AllReduce 算法用于网络通信以及 NCCL 的实现。要使用哪种最佳算法取决于工作人员数量、GPU 数量和类型以及网络情况。默认情况下TensorFlow 会应用一些启发式方法为您选择合适的算法但您可以强制使用 NCCL或 RING如下
strategy tf.distribute.MultiWorkerMirroredStrategy(communication_optionstf.distribute.experimental.CommunicationOptions(implementationtf.distribute.experimental.CollectiveCommunication.NCCL))如果您希望使用参数服务器实现异步数据并行处理请将策略更改为ParameterServerStrategy添加一个或多个参数服务器并为每个任务适当配置TF_CONFIG。请注意虽然工作人员将异步工作但每个工作人员上的副本将同步工作。
最后如果您可以访问Google Cloud 上的 TPU——例如如果您在 Colab 中设置加速器类型为 TPU——那么您可以像这样创建一个TPUStrategy
resolver tf.distribute.cluster_resolver.TPUClusterResolver()
tf.tpu.experimental.initialize_tpu_system(resolver)
strategy tf.distribute.experimental.TPUStrategy(resolver)这需要在导入 TensorFlow 后立即运行。然后您可以正常使用这个策略。
提示
如果您是研究人员您可能有资格免费使用 TPU请查看https://tensorflow.org/tfrc获取更多详细信息。
现在您可以跨多个 GPU 和多个服务器训练模型给自己一个鼓励然而如果您想训练一个非常大的模型您将需要许多 GPU跨多个服务器这将要求要么购买大量硬件要么管理大量云虚拟机。在许多情况下使用一个云服务来为您提供所有这些基础设施的配置和管理会更方便、更经济只有在您需要时才会提供。让我们看看如何使用 Vertex AI 来实现这一点。
在 Vertex AI 上运行大型训练作业
Vertex AI 允许您使用自己的训练代码创建自定义训练作业。实际上您可以几乎使用与在自己的 TF 集群上使用的相同的训练代码。您必须更改的主要内容是首席应该保存模型、检查点和 TensorBoard 日志的位置。首席必须将模型保存到 GCS使用 Vertex AI 在AIP_MODEL_DIR环境变量中提供的路径而不是将模型保存到本地目录。对于模型检查点和 TensorBoard 日志您应该分别使用AIP_CHECKPOINT_DIR和AIP_TENSORBOARD_LOG_DIR环境变量中包含的路径。当然您还必须确保训练数据可以从虚拟机访问例如在 GCS 上或者从另一个 GCP 服务如 BigQuery或直接从网络上访问。最后Vertex AI 明确设置了chief任务类型因此您应该使用resolved.task_type chief来识别首席而不是使用resolved.task_id 0
import os
[...] # other imports, create MultiWorkerMirroredStrategy, and resolverif resolver.task_type chief:model_dir os.getenv(AIP_MODEL_DIR) # paths provided by Vertex AItensorboard_log_dir os.getenv(AIP_TENSORBOARD_LOG_DIR)checkpoint_dir os.getenv(AIP_CHECKPOINT_DIR)
else:tmp_dir Path(tempfile.mkdtemp()) # other workers use temporary dirsmodel_dir tmp_dir / modeltensorboard_log_dir tmp_dir / logscheckpoint_dir tmp_dir / ckptcallbacks [tf.keras.callbacks.TensorBoard(tensorboard_log_dir),tf.keras.callbacks.ModelCheckpoint(checkpoint_dir)]
[...] # build and compile using the strategy scope, just like earlier
model.fit(X_train, y_train, validation_data(X_valid, y_valid), epochs10,callbackscallbacks)
model.save(model_dir, save_formattf)提示
如果您将训练数据放在 GCS 上您可以创建一个tf.data.TextLineDataset或tf.data.TFRecordDataset来访问它只需将 GCS 路径作为文件名例如gs://my_bucket/data/001.csv。这些数据集依赖于tf.io.gfile包来访问文件它支持本地文件和 GCS 文件。
现在您可以在 Vertex AI 上基于这个脚本创建一个自定义训练作业。您需要指定作业名称、训练脚本的路径、用于训练的 Docker 镜像、用于预测的镜像训练后、您可能需要的任何其他 Python 库以及最后 Vertex AI 应该使用作为存储训练脚本的暂存目录的存储桶。默认情况下这也是训练脚本将保存训练模型、TensorBoard 日志和模型检查点如果有的话的地方。让我们创建这个作业
custom_training_job aiplatform.CustomTrainingJob(display_namemy_custom_training_job,script_pathmy_vertex_ai_training_task.py,container_urigcr.io/cloud-aiplatform/training/tf-gpu.2-4:latest,model_serving_container_image_uriserver_image,requirements[gcsfs2022.3.0], # not needed, this is just an examplestaging_bucketfgs://{bucket_name}/staging
)现在让我们在两个拥有两个 GPU 的工作节点上运行它
mnist_model2 custom_training_job.run(machine_typen1-standard-4,replica_count2,accelerator_typeNVIDIA_TESLA_K80,accelerator_count2,
)这就是全部内容Vertex AI 将为您请求的计算节点进行配置在您的配额范围内并在这些节点上运行您的训练脚本。一旦作业完成run()方法将返回一个经过训练的模型您可以像之前创建的那样使用它您可以部署到端点或者用它进行批量预测。如果在训练过程中出现任何问题您可以在 GCP 控制台中查看日志在☰导航菜单中选择 Vertex AI → 训练点击您的训练作业然后点击查看日志。或者您可以点击自定义作业选项卡复制作业的 ID例如1234然后从☰导航菜单中选择日志记录并查询resource.labels.job_id1234。
提示
要可视化训练进度只需启动 TensorBoard并将其--logdir指向日志的 GCS 路径。它将使用应用程序默认凭据您可以使用gcloud auth application-default login进行设置。如果您喜欢Vertex AI 还提供托管的 TensorBoard 服务器。
如果您想尝试一些超参数值一个选项是运行多个作业。您可以通过在调用run()方法时设置args参数将超参数值作为命令行参数传递给您的脚本或者您可以使用environment_variables参数将它们作为环境变量传递。
然而如果您想在云上运行一个大型的超参数调整作业一个更好的选择是使用 Vertex AI 的超参数调整服务。让我们看看如何做。
Vertex AI 上的超参数调整
Vertex AI 的超参数调整服务基于贝叶斯优化算法能够快速找到最佳的超参数组合。要使用它首先需要创建一个接受超参数值作为命令行参数的训练脚本。例如您的脚本可以像这样使用argparse标准库
import argparseparser argparse.ArgumentParser()
parser.add_argument(--n_hidden, typeint, default2)
parser.add_argument(--n_neurons, typeint, default256)
parser.add_argument(--learning_rate, typefloat, default1e-2)
parser.add_argument(--optimizer, defaultadam)
args parser.parse_args()超参数调整服务将多次调用您的脚本每次使用不同的超参数值每次运行称为trial一组试验称为study。然后您的训练脚本必须使用给定的超参数值来构建和编译模型。如果需要您可以使用镜像分发策略以便每个试验在多 GPU 机器上运行。然后脚本可以加载数据集并训练模型。例如
import tensorflow as tfdef build_model(args):with tf.distribute.MirroredStrategy().scope():model tf.keras.Sequential()model.add(tf.keras.layers.Flatten(input_shape[28, 28], dtypetf.uint8))for _ in range(args.n_hidden):model.add(tf.keras.layers.Dense(args.n_neurons, activationrelu))model.add(tf.keras.layers.Dense(10, activationsoftmax))opt tf.keras.optimizers.get(args.optimizer)opt.learning_rate args.learning_ratemodel.compile(losssparse_categorical_crossentropy, optimizeropt,metrics[accuracy])return model[...] # load the dataset
model build_model(args)
history model.fit([...])提示
您可以使用我们之前提到的AIP_*环境变量来确定在哪里保存检查点、TensorBoard 日志和最终模型。
最后脚本必须将模型的性能报告给 Vertex AI 的超参数调整服务以便它决定尝试哪些超参数。为此您必须使用hypertune库在 Vertex AI 训练 VM 上自动安装
import hypertunehypertune hypertune.HyperTune()
hypertune.report_hyperparameter_tuning_metric(hyperparameter_metric_tagaccuracy, # name of the reported metricmetric_valuemax(history.history[val_accuracy]), # metric valueglobal_stepmodel.optimizer.iterations.numpy(),
)现在您的训练脚本已准备就绪您需要定义要在其上运行的机器类型。为此您必须定义一个自定义作业Vertex AI 将使用它作为每个试验的模板
trial_job aiplatform.CustomJob.from_local_script(display_namemy_search_trial_job,script_pathmy_vertex_ai_trial.py, # path to your training scriptcontainer_urigcr.io/cloud-aiplatform/training/tf-gpu.2-4:latest,staging_bucketfgs://{bucket_name}/staging,accelerator_typeNVIDIA_TESLA_K80,accelerator_count2, # in this example, each trial will have 2 GPUs
)最后您准备好创建并运行超参数调整作业
from google.cloud.aiplatform import hyperparameter_tuning as hpthp_job aiplatform.HyperparameterTuningJob(display_namemy_hp_search_job,custom_jobtrial_job,metric_spec{accuracy: maximize},parameter_spec{learning_rate: hpt.DoubleParameterSpec(min1e-3, max10, scalelog),n_neurons: hpt.IntegerParameterSpec(min1, max300, scalelinear),n_hidden: hpt.IntegerParameterSpec(min1, max10, scalelinear),optimizer: hpt.CategoricalParameterSpec([sgd, adam]),},max_trial_count100,parallel_trial_count20,
)
hp_job.run()在这里我们告诉 Vertex AI 最大化名为 accuracy 的指标这个名称必须与训练脚本报告的指标名称匹配。我们还定义了搜索空间使用对数尺度来设置学习率使用线性即均匀尺度来设置其他超参数。超参数的名称必须与训练脚本的命令行参数匹配。然后我们将最大试验次数设置为 100同时最大并行运行的试验次数设置为 20。如果你将并行试验的数量增加到比如60总搜索时间将显著减少最多可减少到 3 倍。但前 60 个试验将同时开始因此它们将无法从其他试验的反馈中受益。因此您应该增加最大试验次数来补偿例如增加到大约 140。
这将需要相当长的时间。一旦作业完成您可以使用 hp_job.trials 获取试验结果。每个试验结果都表示为一个 protobuf 对象包含超参数值和结果指标。让我们找到最佳试验
def get_final_metric(trial, metric_id):for metric in trial.final_measurement.metrics:if metric.metric_id metric_id:return metric.valuetrials hp_job.trials
trial_accuracies [get_final_metric(trial, accuracy) for trial in trials]
best_trial trials[np.argmax(trial_accuracies)]现在让我们看看这个试验的准确率以及其超参数值 max(trial_accuracies)
0.977400004863739best_trial.id
98best_trial.parameters
[parameter_id: learning_rate value { number_value: 0.001 },parameter_id: n_hidden value { number_value: 8.0 },parameter_id: n_neurons value { number_value: 216.0 },parameter_id: optimizer value { string_value: adam }
]就是这样现在您可以获取这个试验的 SavedModel可选择性地再训练一下并将其部署到生产环境中。
提示
Vertex AI 还包括一个 AutoML 服务完全负责为您找到合适的模型架构并为您进行训练。您只需要将数据集以特定格式上传到 Vertex AI这取决于数据集的类型图像、文本、表格、视频等然后创建一个 AutoML 训练作业指向数据集并指定您愿意花费的最大计算小时数。请参阅笔记本中的示例。
现在你拥有了所有需要创建最先进的神经网络架构并使用各种分布策略进行规模化训练的工具和知识可以在自己的基础设施或云上部署它们然后在任何地方部署它们。换句话说你现在拥有超能力好好利用它们
练习 SavedModel 包含什么如何检查其内容 什么时候应该使用 TF Serving它的主要特点是什么有哪些工具可以用来部署它 如何在多个 TF Serving 实例上部署模型 在查询由 TF Serving 提供的模型时何时应该使用 gRPC API 而不是 REST API TFLite 通过哪些不同的方式减小模型的大小使其能在移动设备或嵌入式设备上运行 什么是量化感知训练为什么需要它 什么是模型并行和数据并行为什么通常推荐后者 在多台服务器上训练模型时您可以使用哪些分发策略您如何选择使用哪种 训练一个模型任何您喜欢的模型并部署到 TF Serving 或 Google Vertex AI。编写客户端代码使用 REST API 或 gRPC API 查询它。更新模型并部署新版本。您的客户端代码现在将查询新版本。回滚到第一个版本。 在同一台机器上使用MirroredStrategy在多个 GPU 上训练任何模型如果您无法访问 GPU可以使用带有 GPU 运行时的 Google Colab 并创建两个逻辑 GPU。再次使用CentralStorageStrategy训练模型并比较训练时间。 在 Vertex AI 上微调您选择的模型使用 Keras Tuner 或 Vertex AI 的超参数调整服务。
这些练习的解决方案可以在本章笔记本的末尾找到网址为https://homl.info/colab3。
谢谢
在我们结束这本书的最后一章之前我想感谢您读到最后一段。我真诚地希望您阅读这本书和我写作时一样开心并且它对您的项目无论大小都有用。
如果您发现错误请发送反馈。更一般地我很想知道您的想法所以请不要犹豫通过 O’Reilly、ageron/handson-ml3 GitHub 项目或 Twitter 上的aureliengeron 与我联系。
继续前进我给你的最好建议是练习和练习尝试完成所有的练习如果你还没有这样做玩一下笔记本电脑加入 Kaggle 或其他机器学习社区观看机器学习课程阅读论文参加会议与专家会面。事情发展迅速所以尽量保持最新。一些 YouTube 频道定期以非常易懂的方式详细介绍深度学习论文。我特别推荐 Yannic Kilcher、Letitia Parcalabescu 和 Xander Steenbrugge 的频道。要了解引人入胜的机器学习讨论和更高层次的见解请务必查看 ML Street Talk 和 Lex Fridman 的频道。拥有一个具体的项目要去做也会极大地帮助无论是为了工作还是为了娱乐最好两者兼顾所以如果你一直梦想着建造某样东西就试一试吧逐步工作不要立即朝着月球开火而是专注于你的项目一步一步地构建它。这需要耐心和毅力但当你拥有一个行走的机器人或一个工作的聊天机器人或者其他你喜欢的任何东西时这将是极其有益的
我最大的希望是这本书能激发你构建一个美妙的 ML 应用程序使我们所有人受益。它会是什么样的
—Aurélien Géron
¹ A/B 实验包括在不同的用户子集上测试产品的两个不同版本以检查哪个版本效果最好并获得其他见解。
² Google AI 平台以前称为 Google ML 引擎和 Google AutoML 在 2021 年合并为 Google Vertex AI。
³ REST或 RESTfulAPI 是一种使用标准 HTTP 动词如 GET、POST、PUT 和 DELETE以及使用 JSON 输入和输出的 API。gRPC 协议更复杂但更高效数据使用协议缓冲区进行交换参见第十三章。
如果您对 Docker 不熟悉它允许您轻松下载一组打包在Docker 镜像中的应用程序包括所有依赖项和通常一些良好的默认配置然后使用Docker 引擎在您的系统上运行它们。当您运行一个镜像时引擎会创建一个保持应用程序与您自己系统良好隔离的Docker 容器但如果您愿意可以给它一些有限的访问权限。它类似于虚拟机但速度更快、更轻因为容器直接依赖于主机的内核。这意味着镜像不需要包含或运行自己的内核。
还有 GPU 镜像可用以及其他安装选项。有关更多详细信息请查看官方安装说明。
公平地说这可以通过首先序列化数据然后将其编码为 Base64然后创建 REST 请求来减轻。此外REST 请求可以使用 gzip 进行压缩从而显著减少有效负载大小。
还要查看 TensorFlow 的Graph Transform Tool用于修改和优化计算图。
例如PWA 必须包含不同移动设备大小的图标必须通过 HTTPS 提供必须包含包含应用程序名称和背景颜色等元数据的清单文件。
请查看 TensorFlow 文档获取详细和最新的安装说明因为它们经常更改。
¹⁰ 正如我们在第十二章中所看到的内核是特定数据类型和设备类型的操作实现。例如float32 tf.matmul() 操作有一个 GPU 内核但 int32 tf.matmul() 没有 GPU 内核只有一个 CPU 内核。
¹¹ 您还可以使用 tf.debugging.set_log_device_placement(True) 来记录所有设备放置情况。
¹² 如果您想要保证完美的可重现性这可能很有用正如我在这个视频中所解释的基于 TF 1。
¹³ 在撰写本文时它只是将数据预取到 CPU RAM但使用 tf.data.experimental.prefetch_to_device() 可以使其预取数据并将其推送到您选择的设备以便 GPU 不必等待数据传输而浪费时间。
如果两个 CNN 相同则称为孪生神经网络。
如果您对模型并行性感兴趣请查看Mesh TensorFlow。
这个名字有点令人困惑因为听起来好像有些副本是特殊的什么也不做。实际上所有副本都是等价的它们都努力成为每个训练步骤中最快的失败者在每一步都会变化除非某些设备真的比其他设备慢。但是这意味着如果一个或两个服务器崩溃训练将继续进行得很好。
Jianmin Chen 等人“重新审视分布式同步 SGD”arXiv 预印本 arXiv:1604.009812016。
¹⁸ Aaron Harlap 等人“PipeDream: 快速高效的管道并行 DNN 训练”arXiv 预印本 arXiv:1806.033772018。
¹⁹ Paul Barham 等人“Pathways: 异步分布式数据流 ML”arXiv 预印本 arXiv:2203.125332022。
²⁰ 有关 AllReduce 算法的更多详细信息请阅读 Yuichiro Ueno 的文章该文章介绍了深度学习背后的技术以及 Sylvain Jeaugey 的文章该文章介绍了如何使用 NCCL 大规模扩展深度学习训练。