Python 深度学习第二版(GPT 重译)(一)(3)

简介: Python 深度学习第二版(GPT 重译)(一)

Python 深度学习第二版(GPT 重译)(一)(2)https://developer.aliyun.com/article/1485259

2.4.1 什么是导数?

考虑一个连续、平滑的函数 f(x) = y,将一个数字 x 映射到一个新的数字 y。我们可以以图 2.15 中的函数作为例子。

图 2.15 一个连续、平滑的函数

因为函数是连续的,x 的微小变化只会导致 y 的微小变化——这就是连续性背后的直觉。假设你将 x 增加一个小因子 epsilon_x:这会导致 y 有一个小的 epsilon_y 变化,如图 2.16 所示。

图 2.16 对于连续函数,x 的微小变化导致 y 的微小变化。

此外,因为函数是平滑的(其曲线没有任何突然的角度),当 epsilon_x 足够小,围绕某一点 p,可以将 f 近似为斜率 a 的线性函数,使得 epsilon_y 变为 a * epsilon_x

f(x + epsilon_x) = y + a * epsilon_x

显然,这种线性近似仅在 x 足够接近 p 时才有效。

斜率 ap 处被称为 f导数。如果 a 是负的,这意味着在 p 附近将 x 稍微增加会导致 f(x) 减少(如图 2.17 所示),如果 a 是正的,将 x 稍微增加会导致 f(x) 增加。此外,a 的绝对值(导数的大小)告诉你这种增加或减少会有多快发生。


图 2.17 在 p 处的 f 的导数

对于每个可导函数 f(x)可导意味着“可以求导”:例如,平滑、连续函数可以求导),都存在一个导数函数 f'(x),将 x 的值映射到这些点上 f 的局部线性近似的斜率。例如,cos(x) 的导数是 -sin(x)f(x) = a * x 的导数是 f'(x) = a,等等。

能够求导函数是在优化方面非常强大的工具,即找到使 f(x) 最小化的 x 的值的任务。如果你试图通过一个因子 epsilon_x 更新 x 以最小化 f(x),并且你知道 f 的导数,那么你的任务就完成了:导数完全描述了当你改变 xf(x) 的演变方式。如果你想减小 f(x) 的值,你只需要将 x 沿着导数的相反方向移动一点。

2.4.2 张量操作的导数:梯度

我们刚刚看的函数将标量值x转换为另一个标量值y:你可以将其绘制为二维平面上的曲线。现在想象一个将标量元组(x, y)转换为标量值z的函数:那将是一个矢量操作。你可以将其绘制为三维空间中的二维表面(由坐标x, y, z索引)。同样,你可以想象将矩阵作为输入的函数,将秩-3 张量作为输入的函数等。

导数的概念可以应用于任何这样的函数,只要它们描述的表面是连续且光滑的。张量操作(或张量函数)的导数称为梯度。梯度只是将导数的概念推广到以张量作为输入的函数。还记得对于标量函数,导数代表函数曲线的局部斜率吗?同样,张量函数的梯度代表函数描述的多维表面的曲率。它描述了当输入参数变化时函数输出如何变化。

让我们看一个基于机器学习的例子。

  • 一个输入向量x(数据集中的样本)
  • 一个矩阵W(模型的权重)
  • 一个目标y_true(模型应该学会将其与x关联起来的内容)
  • 一个损失函数loss(旨在衡量模型当前预测与y_true之间的差距)

你可以使用W计算目标候选y_pred,然后计算目标候选y_pred与目标y_true之间的损失或不匹配:

y_pred = dot(W, x)                    # ❶
loss_value = loss(y_pred, y_true)     # ❷

❶ 我们使用模型权重W来对x进行预测。

❷ 我们估计预测有多大偏差。

现在我们想要使用梯度来找出如何更新W以使loss_value变小。我们该如何做?

给定固定的输入xy_true,前述操作可以解释为将W(模型的权重)的值映射到损失值的函数:

loss_value = f(W)    # ❶

❶ f 描述了当 W 变化时损失值形成的曲线(或高维表面)。

假设当前W的值为W0。那么在点W0处的f的导数是一个张量grad(loss_value, W0),与W具有相同的形状,其中每个系数grad(loss_value, W0)[i, j]指示修改W0[i, j]时观察到的loss_value变化的方向和大小。该张量grad(loss_value, W0)是函数f(W) = loss_valueW0处的梯度,也称为“关于WW0周围的loss_value的梯度”。

偏导数

张量操作grad(f(W), W)(以矩阵W为输入)可以表示为标量函数的组合,grad_ij(f(W), w_ij),每个函数将返回loss_value = f(W)相对于W[i, j]系数的导数,假设所有其他系数都是常数。grad_ij称为相对于W[i, j]f 的偏导数

具体来说,grad(loss_value, W0)代表什么?你之前看到函数f(x)的导数可以解释为f的曲线的斜率。同样,grad(loss_value, W0)可以解释为描述loss_value = f(W)W0周围的最陡上升方向的张量,以及这种上升的斜率。每个偏导数描述了特定方向上f的斜率。

出于同样的原因,就像对于函数f(x),您可以通过将x稍微朝着导数的相反方向移动来减小f(x)的值一样,对于张量的函数f(W),您可以通过将W朝着梯度的相反方向移动来减小loss_value = f(W):例如,W1 = W0 - step * grad(f(W0), W0)(其中step是一个小的缩放因子)。这意味着沿着f的最陡上升方向的相反方向,直观上应该使您在曲线上更低。请注意,缩放因子step是必需的,因为当您接近W0时,grad(loss_value, W0)仅近似曲率,因此您不希望离W0太远。

2.4.3 随机梯度下降

鉴于可微函数,从理论上讲,可以通过分析找到其最小值:已知函数的最小值是导数为 0 的点,因此您只需找到所有导数为 0 的点,并检查这些点中哪个点的函数值最低。

应用于神经网络,意味着找到分析上产生最小可能损失函数的权重值的组合。这可以通过解方程grad(f(W), W) = 0来实现W。这是一个N个变量的多项式方程,其中N是模型中的系数数量。虽然对于N = 2N = 3可以解决这样的方程,但对于真实的神经网络来说,这是不可行的,因为参数数量从不少于几千个,通常可以达到几千万个。

相反,您可以使用本节开头概述的四步算法:根据随机数据批次的当前损失值逐渐修改参数。因为您正在处理可微函数,所以可以计算其梯度,这为您实现第 4 步提供了一种高效的方法。如果您根据梯度的相反方向更新权重,那么每次损失都会减少一点:

  1. 绘制一批训练样本x和相应的目标y_true
  2. x上运行模型以获得预测值y_pred(这称为前向传递)。
  3. 计算模型在批次上的损失,即y_predy_true之间的不匹配度的度量。
  4. 计算损失相对于模型参数的梯度(这称为反向传递)。
  5. 将参数稍微朝着梯度的相反方向移动,例如W -= learning_rate * gradient,从而在批次上减少一点损失。学习率(这里是learning_rate)将是一个标量因子,调节梯度下降过程的“速度”。

很简单!我们刚刚描述的是小批量随机梯度下降(mini-batch SGD)。术语随机指的是每个数据批次都是随机抽取的(随机随机的科学同义词)。图 2.18 说明了在 1D 中发生的情况,当模型只有一个参数且您只有一个训练样本时。


图 2.18 SGD 沿着 1D 损失曲线下降(一个可学习参数)

如您所见,直观上选择合理的learning_rate因子值很重要。如果太小,曲线下降将需要许多迭代,并且可能会陷入局部最小值。如果learning_rate太大,您的更新可能会使您完全随机地移动到曲线上的位置。

请注意,小批量 SGD 算法的一个变体是在每次迭代中绘制单个样本和目标,而不是绘制一批数据。这将是真正的SGD(而不是小批量SGD)。或者,走向相反的极端,您可以在所有可用数据上运行每一步,这被称为批量梯度下降。然后,每次更新将更准确,但成本更高。在这两个极端之间的有效折衷方案是使用合理大小的小批量。

尽管图 2.18 展示了在 1D 参数空间中的梯度下降,但在实践中,您将在高维空间中使用梯度下降:神经网络中的每个权重系数都是空间中的一个自由维度,可能有成千上万甚至数百万个。为了帮助您建立对损失曲面的直觉,您还可以将梯度下降可视化为 2D 损失曲面上的过程,如图 2.19 所示。但您不可能可视化训练神经网络的实际过程——您无法以人类能理解的方式表示一个 1000000 维空间。因此,要记住通过这些低维表示形成的直觉在实践中可能并不总是准确的。这在深度学习研究领域历史上一直是一个问题。

图 2.19 梯度下降在 2D 损失曲面上(两个可学习参数)

另外,还有多种 SGD 的变体,它们在计算下一个权重更新时考虑了先前的权重更新,而不仅仅是查看梯度的当前值。例如,有带有动量的 SGD,以及 Adagrad、RMSprop 等几种。这些变体被称为优化方法优化器。特别是,许多这些变体中使用的动量概念值得关注。动量解决了 SGD 的两个问题:收敛速度和局部最小值。考虑图 2.20,显示了损失作为模型参数函数的曲线。


图 2.20 一个局部最小值和一个全局最小值

如您所见,在某个参数值附近,存在一个局部最小值:在该点附近,向左移动会导致损失增加,但向右移动也是如此。如果正在通过具有较小学习率的 SGD 优化考虑的参数,则优化过程可能会卡在局部最小值处,而不是朝着全局最小值前进。

您可以通过使用动量来避免这些问题,动量从物理学中汲取灵感。在这里一个有用的心理形象是将优化过程视为一个小球沿着损失曲线滚动。如果它有足够的动量,小球就不会卡在峡谷中,最终会到达全局最小值。动量的实现是基于每一步移动小球的不仅仅是当前斜率值(当前加速度),还有当前速度(由过去加速度产生)。在实践中,这意味着根据不仅仅是当前梯度值,还有先前参数更新来更新参数w,就像在这个简单实现中一样:

past_velocity = 0. 
momentum = 0.1                # ❶
while loss > 0.01:            # ❷
    w, loss, gradient = get_current_parameters()
    velocity = past_velocity * momentum - learning_rate * gradient
    w = w + momentum * velocity - learning_rate * gradient
    past_velocity = velocity
    update_parameter(w)

❶ 恒定的动量因子

❷ 优化循环

2.4.4 链式求导:反向传播算法

在前面的算法中,我们随意假设因为一个函数是可微的,我们可以轻松计算它的梯度。但这是真的吗?在实践中如何计算复杂表达式的梯度?在我们本章开始的两层模型中,如何计算损失相对于权重的梯度?这就是反向传播算法的作用。

链式法则

反向传播是一种利用简单操作的导数(如加法、relu 或张量乘积)来轻松计算这些原子操作任意复杂组合的梯度的方法。关键是,神经网络由许多张量操作链在一起组成,每个操作都有简单的已知导数。例如,列表 2.2 中定义的模型可以表示为由变量W1b1W2b2(分别属于第一和第二个Dense层)参数化的函数,涉及原子操作dotrelusoftmax+,以及我们的损失函数loss,这些都很容易可微:

loss_value = loss(y_true, softmax(dot(relu(dot(inputs, W1) + b1), W2) + b2))

微积分告诉我们,这样的函数链可以使用以下恒等式导出,称为链式法则

考虑两个函数fg,以及组合函数fg,使得fg(x) == f(g(x))

def fg(x):
    x1 = g(x)
    y = f(x1)
    return y

然后链式法则表明grad(y, x) == grad(y, x1) * grad(x1, x)。只要您知道fg的导数,就可以计算fg的导数。链式法则之所以被命名为链式法则,是因为当您添加更多中间函数时,它开始看起来像一个链条:

def fghj(x):
    x1 = j(x)
    x2 = h(x1)
    x3 = g(x2)
    y = f(x3)
    return y
grad(y, x) == (grad(y, x3) * grad(x3, x2) *
               grad(x2, x1) * grad(x1, x))

将链式法则应用于神经网络梯度值的计算会产生一种称为反向传播的算法。让我们看看具体是如何工作的。

使用计算图进行自动微分

计算图的方式思考反向传播是一种有用的方式。计算图是 TensorFlow 和深度学习革命的核心数据结构。它是操作的有向无环图 - 在我们的情况下,是张量操作。例如,图 2.21 显示了我们第一个模型的图表示。

图 2.21 我们两层模型的计算图表示

计算图在计算机科学中是一个非常成功的抽象,因为它使我们能够将计算视为数据:可计算表达式被编码为一种可用作另一个程序的输入或输出的机器可读数据结构。例如,您可以想象一个接收计算图并返回实现相同计算的大规模分布式版本的新计算图的程序 - 这意味着您可以分发任何计算而无需自己编写分发逻辑。或者想象一个接收计算图并可以自动生成其表示的表达式的导数的程序。如果您的计算表达为显式图数据结构而不是.py 文件中的 ASCII 字符行,这些事情要容易得多。

为了清楚地解释反向传播,让我们看一个计算图的真正基本的例子(见图 2.22)。我们将考虑图 2.21 的简化版本,其中只有一个线性层,所有变量都是标量。我们将取两个标量变量wb,一个标量输入x,并对它们应用一些操作将它们组合成输出y。最后,我们将应用一个绝对值误差损失函数:loss_val = abs(y_true - y)。由于我们希望以最小化loss_val的方式更新wb,我们有兴趣计算grad(loss_val, b)grad(loss _val, w)


图 2.22 计算图的基本示例

让我们为图中的“输入节点”设置具体值,也就是说,输入x、目标y_truewb。我们将这些值从顶部传播到图中的所有节点,直到达到loss_val。这是前向传递(见图 2.23)。

图 2.23 运行前向传递

现在让我们“反转”图表:对于图表中从AB的每条边,我们将创建一个从BA的相反边,并问,当A变化时B变化多少?也就是说,grad(B, A)是多少?我们将用这个值注释每个反转边。这个反向图代表了反向传递(见图 2.24)。


图 2.24 运行反向传播

我们有以下内容:

  • grad(loss_val, x2) = 1,因为当x2变化一个 epsilon 时,loss_val = abs(4 - x2)也会变化相同的量。
  • grad(x2, x1) = 1,因为当x1变化一个 epsilon 时,x2 = x1 + b = x1 + 1也会变化相同的量。
  • grad(x2, b) = 1,因为当b变化一个 epsilon 时,x2 = x1 + b = 6 + b也会变化相同的量。
  • grad(x1, w) = 2,因为当w变化一个 epsilon 时,x1 = x * w = 2 * w也会变化2 * epsilon

链式法则关于这个反向图的含义是,你可以通过乘以连接两个节点路径上的每个边的导数来获得一个节点相对于另一个节点的导数。例如,grad(loss_val, w) = grad(loss_val, x2) * grad(x2, x1) * grad(x1, w)(见图 2.25)。


图 2.25 从loss_valw的反向图路径

通过将链式法则应用于我们的图表,我们得到了我们要找的内容:

  • grad(loss_val, w) = 1 * 1 * 2 = 2
  • grad(loss_val, b) = 1 * 1 = 1

注意:如果在反向图中存在多条连接两个感兴趣节点ab的路径,我们可以通过对所有路径的贡献求和来得到grad(b, a)

通过这样,你刚刚看到了反向传播的过程!反向传播简单地是将链式法则应用于计算图。没有更多了。反向传播从最终损失值开始,从顶层向底层向后计算每个参数对损失值的贡献。这就是“反向传播”这个名字的由来:我们在计算图中“反向传播”不同节点的损失贡献。

如今,人们在现代框架中实现神经网络,这些框架能够进行自动微分,例如 TensorFlow。自动微分是使用你刚刚看到的计算图实现的。自动微分使得能够检索任意可微张量操作组合的梯度成为可能,而无需额外工作,只需编写前向传播。在 2000 年代我用 C 语言编写我的第一个神经网络时,我不得不手动编写梯度。现在,由于现代自动微分工具,你永远不必自己实现反向传播。算你运气好!

TensorFlow 中的梯度磁带

你可以利用 TensorFlow 强大的自动微分功能的 API 是GradientTape。它是一个 Python 范围,将在其中运行的张量操作“记录”为计算图(有时称为“磁带”)。然后可以使用此图检索任何输出相对于任何变量或一组变量(tf.Variable类的实例)的梯度。tf.Variable是一种特定类型的张量,用于保存可变状态,例如神经网络的权重始终是tf.Variable实例。

import tensorflow as tf
x = tf.Variable(0.)                      # ❶
with tf.GradientTape() as tape:          # ❷
    y = 2 * x + 3                        # ❸
grad_of_y_wrt_x = tape.gradient(y, x)    # ❹

❶ 实例化一个初始值为 0 的标量变量。

❷ 打开一个 GradientTape 范围。

❸ 在范围内,对我们的变量应用一些张量操作。

❹ 使用磁带检索输出 y 相对于我们的变量 x 的梯度。

GradientTape与张量操作一起工作:

x = tf.Variable(tf.random.uniform((2, 2)))     # ❶
with tf.GradientTape() as tape:
    y = 2 * x + 3 
grad_of_y_wrt_x = tape.gradient(y, x)          # ❷

❶ 实例化一个形状为(2, 2)且初始值全为零的变量。

grad_of_y_wrt_x是一个形状为(2, 2)(像 x 一样)的张量,描述了 y = 2 * a + 3 在 x = [[0, 0], [0, 0]]周围的曲率。

它也适用于变量列表:

W = tf.Variable(tf.random.uniform((2, 2)))
b = tf.Variable(tf.zeros((2,)))
x = tf.random.uniform((2, 2)) 
with tf.GradientTape() as tape:
    y = tf.matmul(x, W) + b                         # ❶
grad_of_y_wrt_W_and_b = tape.gradient(y, [W, b])    # ❷

❶ matmul 是在 TensorFlow 中表示“点积”的方式。

❷ grad_of_y_wrt_W_and_b 是两个张量列表,形状与 W 和 b 相同。

你将在下一章学习关于梯度带的知识。

2.5 回顾我们的第一个例子

你已经接近本章的结束,现在应该对神经网络背后的运作有一个大致的了解。在本章开始时是一个神奇的黑匣子,现在已经变成了一个更清晰的画面,如图 2.26 所示:模型由相互链接的层组成,将输入数据映射到预测结果。损失函数然后将这些预测与目标进行比较,产生一个损失值:衡量模型预测与预期值匹配程度的指标。优化器使用这个损失值来更新模型的权重。


图 2.26 网络、层、损失函数和优化器之间的关系

让我们回到本章的第一个例子,并根据你学到的知识来逐一审查每个部分。

这是输入数据:

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255 
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255

现在你明白了输入图像存储在 NumPy 张量中,这里格式化为(60000, 784)(训练数据)和(10000, 784)(测试数据)的float32张量。

这是我们的模型:

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])

现在你明白了这个模型由两个Dense层的链条组成,每个层对输入数据应用了一些简单的张量操作,并且这些操作涉及权重张量。权重张量是属于层的属性,是模型的知识所在。

这是模型编译步骤:

model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

现在你明白了sparse_categorical_crossentropy是用作学习权重张量的反馈信号的损失函数,训练阶段将尝试最小化它。你还知道这种损失的减少是通过小批量随机梯度下降来实现的。具体规则由作为第一个参数传递的rmsprop优化器定义。

最后,这是训练循环:

model.fit(train_images, train_labels, epochs=5, batch_size=128)

现在你明白了当你调用fit时会发生什么:模型将开始在 128 个样本的小批量数据上进行 5 次迭代(每次迭代所有训练数据都被称为epoch)。对于每个批次,模型将计算损失相对于权重的梯度(使用源自微积分链式法则的反向传播算法),并将权重朝着减少该批次损失值的方向移动。

在这 5 个 epoch 之后,模型将执行 2,345 次梯度更新(每个 epoch 469 次),并且模型的损失将足够低,以至于模型能够以高准确度对手写数字进行分类。

在这一点上,你已经了解了大部分关于神经网络的常识。让我们通过逐步在 TensorFlow 中“从头开始”重新实现那个第一个例子来证明它。

2.5.1 在 TensorFlow 中从头开始重新实现我们的第一个例子

有什么比从头开始实现一切更能展示出完全、明确的理解呢?当然,“从头开始”在这里是相对的:我们不会重新实现基本的张量操作,也不会实现反向传播。但我们会降到一个低到几乎不使用任何 Keras 功能的水平。

如果你现在还不理解这个例子中的每一个细节,不要担心。下一章将更详细地深入探讨 TensorFlow API。现在,只需尝试理解正在发生的事情的要点——这个例子的目的是帮助你通过具体实现来澄清对深度学习数学的理解。让我们开始吧!

一个简单的 Dense 类

你之前学过Dense层实现以下输入转换,其中Wb是模型参数,activation是逐元素函数(通常是relu,但对于最后一层可能是softmax):

output = activation(dot(W, input) + b)

让我们实现一个简单的 Python 类NaiveDense,它创建两个 TensorFlow 变量Wb,并公开一个__call__()方法,应用前述转换。

import tensorflow as tf
class NaiveDense:
    def __init__(self, input_size, output_size, activation):
        self.activation = activation
        w_shape = (input_size, output_size)                                # ❶
        w_initial_value = tf.random.uniform(w_shape, minval=0, maxval=1e-1)
        self.W = tf.Variable(w_initial_value)
        b_shape = (output_size,                                            # ❷
        b_initial_value = tf.zeros(b_shape)
        self.b = tf.Variable(b_initial_value)
    def __call__(self, inputs)::                                           # ❸
        return self.activation(tf.matmul(inputs, self.W) + self.b)
    @property
    def weights(self):                                                     # ❹
        return [self.W, self.b]

❶ 创建一个形状为(input_size, output_size)的矩阵 W,用随机值初始化。

❷ 创建一个形状为(output_size,)的向量 b,用零初始化。

❸ 应用前向传播。

❹ 用于检索层权重的便利方法

一个简单的 Sequential 类

现在,让我们创建一个NaiveSequential类来链接这些层。它包装了一系列层,并公开一个__call__()方法,简单地按顺序在输入上调用底层层。它还具有一个weights属性,方便跟踪层的参数。

class NaiveSequential:
    def __init__(self, layers):
        self.layers = layers
    def __call__(self, inputs):
        x = inputs
        for layer in self.layers:
           x = layer(x)
        return x
    @property 
    def weights(self):
       weights = []
       for layer in self.layers:
           weights += layer.weights
       return weights

使用这个NaiveDense类和这个NaiveSequential类,我们可以创建一个模拟的 Keras 模型:

model = NaiveSequential([
    NaiveDense(input_size=28 * 28, output_size=512, activation=tf.nn.relu),
    NaiveDense(input_size=512, output_size=10, activation=tf.nn.softmax)
]) 
assert len(model.weights) == 4 

一个批生成器

接下来,我们需要一种方法以小批量迭代 MNIST 数据。这很容易:

import math
class BatchGenerator:
    def __init__(self, images, labels, batch_size=128):
        assert len(images) == len(labels)
        self.index = 0
        self.images = images
        self.labels = labels
        self.batch_size = batch_size
        self.num_batches = math.ceil(len(images) / batch_size)
    def next(self):
        images = self.images[self.index : self.index + self.batch_size]
        labels = self.labels[self.index : self.index + self.batch_size]
        self.index += self.batch_size
        return images, labels

2.5.2 运行一个训练步骤

这个过程中最困难的部分是“训练步骤”:在一个数据批次上运行模型后更新模型的权重。我们需要

  1. 计算模型对批次中图像的预测。
  2. 计算这些预测的损失值,给定实际标签。
  3. 计算损失相对于模型权重的梯度。
  4. 将权重沿着梯度相反的方向移动一小步。

要计算梯度,我们将使用在第 2.4.4 节中介绍的 TensorFlow GradientTape对象:

def one_training_step(model, images_batch, labels_batch):
    with tf.GradientTape() as tape:                                         # ❶
        predictions = model(images_batch)                                   # ❶
        per_sample_losses = tf.keras.losses.sparse_categorical_crossentropy(# ❶
            labels_batch, predictions)                                      # ❶
        average_loss = tf.reduce_mean(per_sample_losses)                    # ❶
    gradients = tape.gradient(average_loss, model.weights)                  # ❷
    update_weights(gradients, model.weights)                                # ❸
    return average_loss

❶ 运行“前向传播”(在 GradientTape 范围内计算模型的预测)。

❷ 计算损失相对于权重的梯度。输出梯度是一个列表,其中每个条目对应于模型权重列表中的一个权重。

❸ 使用梯度更新权重(我们将很快定义这个函数)。

正如你已经知道的,“权重更新”步骤的目的(由前面的update_weights函数表示)是将权重向“减少此批次上的损失”的方向移动一点。移动的大小由“学习率”确定,通常是一个小量。实现这个update_weights函数的最简单方法是从每个权重中减去gradient * learning_rate

learning_rate = 1e-3 
def update_weights(gradients, weights):
    for g, w in zip(gradients, weights):
        w.assign_sub(g * learning_rate)      # ❶

assign_sub是 TensorFlow 变量的-=的等效操作。

在实践中,你几乎永远不会手动实现这样的权重更新步骤。相反,你会使用 Keras 中的Optimizer实例,就像这样:

from tensorflow.keras import optimizers
optimizer = optimizers.SGD(learning_rate=1e-3)
def update_weights(gradients, weights):
    optimizer.apply_gradients(zip(gradients, weights))

现在我们的每批训练步骤已经准备好,我们可以继续实现整个训练时期。

2.5.3 完整的训练循环

训练的一个时期简单地包括对训练数据中的每个批次重复进行训练步骤,完整的训练循环只是一个时期的重复:

def fit(model, images, labels, epochs, batch_size=128):
    for epoch_counter in range(epochs):
        print(f"Epoch {epoch_counter}")
        batch_generator = BatchGenerator(images, labels)
        for batch_counter in range(batch_generator.num_batches):
            images_batch, labels_batch = batch_generator.next()
            loss = one_training_step(model, images_batch, labels_batch)
            if batch_counter % 100 == 0:
                print(f"loss at batch {batch_counter}: {loss:.2f}")

让我们来试一下:

from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255  
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255 
fit(model, train_images, train_labels, epochs=10, batch_size=128)

2.5.4 评估模型

我们可以通过对测试图像的预测取argmax,并将其与预期标签进行比较来评估模型:

predictions = model(test_images)
predictions = predictions.numpy()                  # ❶
predicted_labels = np.argmax(predictions, axis=1)
matches = predicted_labels == test_labels
print(f"accuracy: {matches.mean():.2f}")

❶ 在 TensorFlow 张量上调用.numpy()会将其转换为 NumPy 张量。

完成了!正如你所看到的,手动完成“几行 Keras 代码可以完成的工作”需要做很多工作。但是因为你已经经历了这些步骤,现在应该对在调用fit()时神经网络内部发生的事情有一个清晰的理解。拥有这种低级别的心智模型,了解代码在幕后执行的操作,将使你更能利用 Keras API 的高级功能。

摘要

  • 张量构成现代机器学习系统的基础。它们具有各种dtyperankshape
  • 你可以通过张量操作(如加法、张量积或逐元素乘法)来操作数值张量,这可以被解释为编码几何变换。总的来说,深度学习中的一切都可以被解释为几何解释。
  • 深度学习模型由一系列简单的张量操作组成,由权重参数化,它们本身也是张量。模型的权重是存储其“知识”的地方。
  • 学习意味着找到一组值,使模型的权重最小化给定一组训练数据样本及其对应目标的损失函数
  • 学习是通过随机抽取数据样本及其目标,并计算模型参数相对于批次上的损失的梯度来实现的。然后,模型参数向相反方向移动一点(移动的大小由学习率定义)。这被称为小批量随机梯度下降
  • 整个学习过程之所以可能,是因为神经网络中的所有张量操作都是可微的,因此可以应用导数的链式法则来找到将当前参数和当前数据批次映射到梯度值的梯度函数。这被称为反向传播
  • 你将经常在未来章节中看到的两个关键概念是损失优化器。这是在开始向模型输入数据之前需要定义的两件事。
  • 损失是在训练过程中你将尝试最小化的量,因此它应该代表你尝试解决的任务的成功度量。
  • 优化器指定了损失的梯度将如何用于更新参数的确切方式:例如,可以是 RMSProp 优化器、带动量的 SGD 等。

三、Keras 和 TensorFlow 简介

本章内容包括

  • 仔细研究 TensorFlow、Keras 及它们之间的关系
  • 设置深度学习工作空间
  • 深入了解核心深度学习概念如何转化为 Keras 和 TensorFlow

本章旨在为您提供开始实践深度学习所需的一切。我将为您快速介绍 Keras(keras.io)和 TensorFlow(tensorflow.org),这是本书中将使用的基于 Python 的深度学习工具。您将了解如何设置深度学习工作空间,使用 TensorFlow、Keras 和 GPU 支持。最后,基于您在第二章中对 Keras 和 TensorFlow 的初步接触,我们将回顾神经网络的核心组件以及它们如何转化为 Keras 和 TensorFlow 的 API。

到本章结束时,您将准备好进入实际的现实世界应用程序,这将从第四章开始。

3.1 什么是 TensorFlow?

TensorFlow 是一个基于 Python 的免费、开源的机器学习平台,主要由 Google 开发。与 NumPy 类似,TensorFlow 的主要目的是使工程师和研究人员能够在数值张量上操作数学表达式。但是 TensorFlow 在以下方面远远超出了 NumPy 的范围:

  • 它可以自动计算任何可微表达式的梯度(正如您在第二章中看到的),使其非常适合机器学习。
  • 它不仅可以在 CPU 上运行,还可以在 GPU 和 TPU 上运行,高度并行的硬件加速器。
  • 在 TensorFlow 中定义的计算可以轻松地分布到许多机器上。
  • TensorFlow 程序可以导出到其他运行时,例如 C++、JavaScript(用于基于浏览器的应用程序)或 TensorFlow Lite(用于在移动设备或嵌入式设备上运行的应用程序)等。这使得 TensorFlow 应用程序在实际环境中易于部署。

重要的是要记住,TensorFlow 远不止是一个单一的库。它实际上是一个平台,拥有庞大的组件生态系统,其中一些由 Google 开发,一些由第三方开发。例如,有用于强化学习研究的 TF-Agents,用于工业强度机器学习工作流管理的 TFX,用于生产部署的 TensorFlow Serving,以及预训练模型的 TensorFlow Hub 存储库。这些组件共同涵盖了非常广泛的用例,从前沿研究到大规模生产应用。

TensorFlow 的扩展性相当不错:例如,奥克岭国家实验室的科学家们已经使用它在 IBM Summit 超级计算机的 27000 个 GPU 上训练了一个 1.1 艾克斯佛洛普的极端天气预测模型。同样,谷歌已经使用 TensorFlow 开发了非常计算密集的深度学习应用程序,例如下棋和围棋代理 AlphaZero。对于您自己的模型,如果有预算,您可以实际上希望在小型 TPU 架或在 Google Cloud 或 AWS 上租用的大型 GPU 集群上扩展到约 10 petaFLOPS。这仍然约占 2019 年顶级超级计算机峰值计算能力的 1%!

3.2 什么是 Keras?

Keras 是一个基于 TensorFlow 的 Python 深度学习 API,提供了一种方便的方式来定义和训练任何类型的深度学习模型。Keras 最初是为研究而开发的,旨在实现快速的深度学习实验。

通过 TensorFlow,Keras 可以在不同类型的硬件上运行(见图 3.1)—GPU、TPU 或普通 CPU,并且可以无缝地扩展到数千台机器。


图 3.1 Keras 和 TensorFlow:TensorFlow 是一个低级张量计算平台,而 Keras 是一个高级深度学习 API

Keras 以优先考虑开发者体验而闻名。它是为人类而设计的 API,而不是为机器。它遵循减少认知负荷的最佳实践:提供一致简单的工作流程,最小化常见用例所需的操作数量,并在用户出错时提供清晰可行的反馈。这使得 Keras 对初学者易于学习,对专家使用高效。

截至 2021 年底,Keras 已经拥有超过一百万用户,包括学术研究人员、工程师、数据科学家、初创公司和大公司的研究生和爱好者。Keras 在 Google、Netflix、Uber、CERN、NASA、Yelp、Instacart、Square 等公司中被使用,以及数百家从事各行各业各种问题的初创公司。你的 YouTube 推荐源自 Keras 模型。Waymo 自动驾驶汽车是使用 Keras 模型开发的。Keras 也是 Kaggle 上的热门框架,大多数深度学习竞赛都是使用 Keras 赢得的。

由于 Keras 拥有庞大且多样化的用户群,它不会强迫你遵循单一的“正确”模型构建和训练方式。相反,它支持各种不同的工作流程,从非常高级到非常低级,对应不同的用户配置文件。例如,你有多种构建模型和训练模型的方式,每种方式都代表着可用性和灵活性之间的某种权衡。在第五章中,我们将详细审查这种工作流程的一部分。你可以像使用 Scikit-learn 一样使用 Keras——只需调用 fit(),让框架自行处理——或者像使用 NumPy 一样使用它——完全控制每一个细节。

这意味着你现在学习的所有内容在你成为专家后仍然是相关的。你可以轻松入门,然后逐渐深入到需要从头开始编写更多逻辑的工作流程中。在从学生转变为研究人员,或者从数据科学家转变为深度学习工程师时,你不必切换到完全不同的框架。

这种哲学与 Python 本身的哲学非常相似!有些语言只提供一种编写程序的方式——例如,面向对象编程或函数式编程。而 Python 是一种多范式语言:它提供了一系列可能的使用模式,它们都可以很好地协同工作。这使得 Python 适用于各种非常不同的用例:系统管理、数据科学、机器学习工程、Web 开发……或者只是学习如何编程。同样,你可以将 Keras 视为深度学习的 Python:一种用户友好的深度学习语言,为不同用户配置文件提供各种工作流程。

3.3 Keras 和 TensorFlow:简史

Keras 比 TensorFlow 早八个月发布。它于 2015 年 3 月发布,而 TensorFlow 则于 2015 年 11 月发布。你可能会问,如果 Keras 是建立在 TensorFlow 之上的,那么在 TensorFlow 发布之前它是如何存在的?Keras 最初是建立在 Theano 之上的,Theano 是另一个提供自动微分和 GPU 支持的张量操作库,是最早的之一。Theano 在蒙特利尔大学机器学习算法研究所(MILA)开发,从许多方面来看是 TensorFlow 的前身。它开创了使用静态计算图进行自动微分和将代码编译到 CPU 和 GPU 的想法。

在 TensorFlow 发布后的 2015 年底,Keras 被重构为多后端架构:可以使用 Keras 与 Theano 或 TensorFlow,而在两者之间切换就像更改环境变量一样简单。到 2016 年 9 月,TensorFlow 达到了技术成熟的水平,使其成为 Keras 的默认后端选项成为可能。2017 年,Keras 添加了两个新的后端选项:CNTK(由微软开发)和 MXNet(由亚马逊开发)。如今,Theano 和 CNTK 已经停止开发,MXNet 在亚马逊之外并不广泛使用。Keras 又回到了基于 TensorFlow 的单一后端 API。

多年来,Keras 和 TensorFlow 之间建立了一种共生关系。在 2016 年和 2017 年期间,Keras 成为了开发 TensorFlow 应用程序的用户友好方式,将新用户引入 TensorFlow 生态系统。到 2017 年底,大多数 TensorFlow 用户都是通过 Keras 或与 Keras 结合使用。2018 年,TensorFlow 领导层选择了 Keras 作为 TensorFlow 的官方高级 API。因此,Keras API 在 2019 年 9 月发布的 TensorFlow 2.0 中占据了重要位置——这是 TensorFlow 和 Keras 的全面重新设计,考虑了四年多的用户反馈和技术进步。

到这个时候,你一定迫不及待地想要开始实践运行 Keras 和 TensorFlow 代码了。让我们开始吧。

3.4 设置深度学习工作空间

在开始开发深度学习应用程序之前,你需要设置好你的开发环境。强烈建议,尽管不是绝对必要的,你应该在现代 NVIDIA GPU 上运行深度学习代码,而不是在计算机的 CPU 上运行。一些应用程序——特别是使用卷积网络进行图像处理的应用程序——在 CPU 上会非常慢,即使是快速的多核 CPU。即使对于可以在 CPU 上运行的应用程序,使用最新 GPU 通常会使速度提高 5 到 10 倍。

要在 GPU 上进行深度学习,你有三个选择:

  • 在你的工作站上购买并安装一块物理 NVIDIA GPU。
  • 使用 Google Cloud 或 AWS EC2 上的 GPU 实例。
  • 使用 Colaboratory 提供的免费 GPU 运行时,这是 Google 提供的托管笔记本服务(有关“笔记本”是什么的详细信息,请参见下一节)。

Colaboratory 是最简单的入门方式,因为它不需要购买硬件,也不需要安装软件——只需在浏览器中打开一个标签页并开始编码。这是我们推荐在本书中运行代码示例的选项。然而,Colaboratory 的免费版本只适用于小型工作负载。如果你想扩大规模,你将不得不使用第一或第二个选项。

如果你还没有可以用于深度学习的 GPU(一块最新的高端 NVIDIA GPU),那么在云中运行深度学习实验是一个简单、低成本的方式,让你能够扩展到更大的工作负载,而无需购买任何额外的硬件。如果你正在使用 Jupyter 笔记本进行开发,那么在云中运行的体验与本地运行没有任何区别。

但是,如果你是深度学习的重度用户,这种设置在长期内甚至在几个月内都是不可持续的。云实例并不便宜:在 2021 年中期,你将为 Google Cloud 上的 V100 GPU 每小时支付 2.48 美元。与此同时,一块可靠的消费级 GPU 的价格在 1500 到 2500 美元之间——即使这些 GPU 的规格不断改进,价格也保持相对稳定。如果你是深度学习的重度用户,请考虑设置一个带有一块或多块 GPU 的本地工作站。

另外,无论您是在本地运行还是在云端运行,最好使用 Unix 工作站。虽然在 Windows 上直接运行 Keras 在技术上是可能的,但我们不建议这样做。如果您是 Windows 用户,并且想在自己的工作站上进行深度学习,最简单的解决方案是在您的机器上设置一个 Ubuntu 双系统引导,或者利用 Windows Subsystem for Linux(WSL),这是一个兼容层,使您能够从 Windows 运行 Linux 应用程序。这可能看起来有点麻烦,但从长远来看,这将为您节省大量时间和麻烦。

3.4.1 Jupyter 笔记本:运行深度学习实验的首选方式

Jupyter 笔记本是运行深度学习实验的绝佳方式,特别是本书中的许多代码示例。它们在数据科学和机器学习社区中被广泛使用。笔记本是由 Jupyter Notebook 应用程序生成的文件(jupyter.org),您可以在浏览器中编辑。它结合了执行 Python 代码的能力和用于注释您正在进行的操作的丰富文本编辑功能。笔记本还允许您将长实验分解为可以独立执行的较小部分,这使得开发交互式,并且意味着如果实验的后期出现问题,您不必重新运行之前的所有代码。

我建议使用 Jupyter 笔记本来开始使用 Keras,尽管这不是必需的:您也可以运行独立的 Python 脚本或在诸如 PyCharm 这样的 IDE 中运行代码。本书中的所有代码示例都作为开源笔记本提供;您可以从 GitHub 上下载它们:github.com/fchollet/deep-learning-with-python-notebooks

3.4.2 使用 Colaboratory

Colaboratory(简称 Colab)是一个免费的 Jupyter 笔记本服务,无需安装,完全在云端运行。实际上,它是一个网页,让您可以立即编写和执行 Keras 脚本。它为您提供免费(但有限)的 GPU 运行时,甚至还有 TPU 运行时,因此您不必购买自己的 GPU。Colaboratory 是我们推荐用于运行本书中代码示例的工具。

使用 Colaboratory 的第一步

要开始使用 Colab,请访问 colab.research.google.com 并单击 New Notebook 按钮。您将看到图 3.2 中显示的标准笔记本界面。

图 3.2 一个 Colab 笔记本

您会在工具栏中看到两个按钮:+ Code 和 + Text。它们分别用于创建可执行的 Python 代码单元格和注释文本单元格。在代码单元格中输入代码后,按 Shift-Enter 将执行它(参见图 3.3)。


图 3.3 创建一个代码单元格

在文本单元格中,您可以使用 Markdown 语法(参见图 3.4)。按 Shift-Enter 在文本单元格上将渲染它。


图 3.4 创建一个文本单元格

文本单元格对于为您的笔记本提供可读的结构非常有用:使用它们为您的代码添加部分标题和长说明段落或嵌入图像。笔记本旨在成为一种多媒体体验!

使用 pip 安装软件包

默认的 Colab 环境已经安装了 TensorFlow 和 Keras,因此您可以立即开始使用它,无需任何安装步骤。但是,如果您需要使用 pip 安装某些内容,您可以在代码单元格中使用以下语法进行安装(请注意,该行以 ! 开头,表示这是一个 shell 命令而不是 Python 代码):

!pip install package_name

使用 GPU 运行时

要在 Colab 中使用 GPU 运行时,请在菜单中选择 Runtime > Change Runtime Type,并选择 GPU 作为硬件加速器(参见图 3.5)。

图 3.5 使用 Colab 的 GPU 运行时

如果 GPU 可用,TensorFlow 和 Keras 将自动在 GPU 上执行,所以在选择了 GPU 运行时后,你无需做其他操作。

你会注意到在硬件加速器下拉菜单中还有一个 TPU 运行时选项。与 GPU 运行时不同,使用 TensorFlow 和 Keras 的 TPU 运行时需要在代码中进行一些手动设置。我们将在第十三章中介绍这个内容。目前,我们建议你选择 GPU 运行时,以便跟随本书中的代码示例。

现在你有了一个开始在实践中运行 Keras 代码的方法。接下来,让我们看看你在第二章学到的关键思想如何转化为 Keras 和 TensorFlow 代码。

3.5 TensorFlow 的第一步

正如你在之前的章节中看到的,训练神经网络围绕着以下概念展开:

  • 首先,低级张量操作——支撑所有现代机器学习的基础设施。这转化为 TensorFlow API:
  • 张量,包括存储网络状态的特殊张量(变量
  • 张量操作,如加法、relumatmul
  • 反向传播,一种计算数学表达式梯度的方法(在 TensorFlow 中通过GradientTape对象处理)
  • 其次,高级深度学习概念。这转化为 Keras API:
  • ,这些层组合成一个模型
  • 一个损失函数,定义用于学习的反馈信号
  • 一个优化器,确定学习如何进行
  • 指标用于评估模型性能,如准确度
  • 执行小批量随机梯度下降的训练循环

在上一章中,你已经初步接触了一些对应的 TensorFlow 和 Keras API:你已经简要使用了 TensorFlow 的Variable类、matmul操作和GradientTape。你实例化了 Keras 的Dense层,将它们打包成一个Sequential模型,并用fit()方法训练了该模型。

现在让我们深入了解如何使用 TensorFlow 和 Keras 在实践中处理所有这些不同概念。

Python 深度学习第二版(GPT 重译)(一)(4)https://developer.aliyun.com/article/1485261

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
13天前
|
机器学习/深度学习 人工智能 算法
Python 深度学习第二版(GPT 重译)(一)(1)
Python 深度学习第二版(GPT 重译)(一)
45 1
|
13天前
|
机器学习/深度学习 存储 算法框架/工具
Python 深度学习第二版(GPT 重译)(一)(2)
Python 深度学习第二版(GPT 重译)(一)
59 4
|
机器学习/深度学习 TensorFlow API
Python 深度学习第二版(GPT 重译)(一)(4)
Python 深度学习第二版(GPT 重译)(一)
30 3
|
13天前
|
机器学习/深度学习 算法框架/工具 计算机视觉
Python 深度学习第二版(GPT 重译)(三)(4)
Python 深度学习第二版(GPT 重译)(三)
21 5
|
机器学习/深度学习 搜索推荐 TensorFlow
Python 深度学习第二版(GPT 重译)(二)(2)
Python 深度学习第二版(GPT 重译)(二)
80 1
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习第二版(GPT 重译)(四)(2)
Python 深度学习第二版(GPT 重译)(四)
27 2
|
机器学习/深度学习 搜索推荐 TensorFlow
Python 深度学习第二版(GPT 重译)(二)(4)
Python 深度学习第二版(GPT 重译)(二)
47 1
Python 深度学习第二版(GPT 重译)(二)(4)
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习第二版(GPT 重译)(二)(1)
Python 深度学习第二版(GPT 重译)(二)
72 1
|
机器学习/深度学习 存储 计算机视觉
Python 深度学习第二版(GPT 重译)(四)(1)
Python 深度学习第二版(GPT 重译)(四)
24 3
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习第二版(GPT 重译)(二)(3)
Python 深度学习第二版(GPT 重译)(二)
40 1