Python 深度学习第二版(GPT 重译)(一)(1)https://developer.aliyun.com/article/1485258
这里是测试数据:
>>> test_images.shape (10000, 28, 28) >>> len(test_labels) 10000 >>> test_labels array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)
工作流程如下:首先,我们将向神经网络提供训练数据train_images
和train_labels
。然后网络将学习将图像和标签关联起来。最后,我们将要求网络为test_images
生成预测,并验证这些预测是否与test_labels
中的标签匹配。
让我们构建网络—再次提醒您,您不必完全理解这个示例的所有内容。
列表 2.2 网络架构
from tensorflow import keras from tensorflow.keras import layers model = keras.Sequential([ layers.Dense(512, activation="relu"), layers.Dense(10, activation="softmax") ])
神经网络的核心构建块是层。您可以将层视为数据的过滤器:一些数据进入,以更有用的形式输出。具体来说,层从输入的数据中提取表示,希望这些表示对手头的问题更有意义。大部分深度学习都是将一些简单层链接在一起,这些层将实现一种渐进数据精炼形式。深度学习模型就像是数据处理的筛子,由一系列越来越精细的数据过滤器(层)组成。
在这里,我们的模型由两个Dense
层的序列组成,这些层是密集连接(也称为全连接)的神经层。第二(也是最后)层是一个 10 路softmax 分类层,这意味着它将返回一个总和为 1 的 10 个概率分数数组。每个分数将是当前数字图像属于我们的 10 个数字类别之一的概率。
为了使模型准备好进行训练,我们需要在编译步骤中选择另外三个事项:
- 优化器—模型将根据其看到的训练数据更新自身的机制,以提高其性能。
- 损失函数—模型如何能够衡量其在训练数据上的表现,从而如何能够引导自己朝着正确的方向前进。
- 在训练和测试过程中监控的指标—在这里,我们只关心准确率(被正确分类的图像的比例)。
损失函数和优化器的确切目的将在接下来的两章中明确。
列表 2.3 编译步骤
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
在训练之前,我们将通过重新调整数据的形状以及缩放数据,使所有值都在[0,
1]
区间内来预处理数据。之前,我们的训练图像存储在一个形状为(60000,
28,
28)
的uint8
类型数组中,值在[0,
255]
区间内。我们将其转换为一个形状为(60000,
28
*
28)
的float32
数组,值在 0 到 1 之间。
列表 2.4 准备图像数据
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
现在我们准备训练模型,在 Keras 中通过调用模型的fit()
方法来完成——我们将模型与其训练数据拟合。
列表 2.5 “拟合”模型
>>> model.fit(train_images, train_labels, epochs=5, batch_size=128) Epoch 1/5 60000/60000 [===========================] - 5s - loss: 0.2524 - acc: 0.9273 Epoch 2/5 51328/60000 [=====================>.....] - ETA: 1s - loss: 0.1035 - acc: 0.9692
在训练过程中显示两个量:模型在训练数据上的损失和模型在训练数据上的准确率。我们很快就在训练数据上达到了 0.989(98.9%)的准确率。
现在我们有了一个经过训练的模型,我们可以使用它来预测新数字的类别概率—这些图像不是训练数据的一部分,就像测试集中的那些图像一样。
列表 2.6 使用模型进行预测
>>> test_digits = test_images[0:10] >>> predictions = model.predict(test_digits) >>> predictions[0] array([1.0726176e-10, 1.6918376e-10, 6.1314843e-08, 8.4106023e-06, 2.9967067e-11, 3.0331331e-09, 8.3651971e-14, 9.9999106e-01, 2.6657624e-08, 3.8127661e-07], dtype=float32)
数组中索引i
处的每个数字对应于数字图像test_digits[0]
属于类别i
的概率。
这个第一个测试数字在索引 7 处具有最高的概率分数(0.99999106,接近 1),因此根据我们的模型,它必须是一个 7:
>>> predictions[0].argmax() 7 >>> predictions[0][7] 0.99999106
我们可以检查测试标签是否一致:
>>> test_labels[0] 7
我们的模型在对这些以前从未见过的数字进行分类时,平均表现如何?让我们通过计算整个测试集上的平均准确率来检查。
列表 2.7 在新数据上评估模型
>>> test_loss, test_acc = model.evaluate(test_images, test_labels) >>> print(f"test_acc: {test_acc}") test_acc: 0.9785
测试集准确率为 97.8%—这比训练集准确率(98.9%)要低得多。训练准确率和测试准确率之间的差距是过拟合的一个例子:机器学习模型在新数据上的表现往往不如在其训练数据上。过拟合是第三章的一个核心主题。
这就结束了我们的第一个示例——你刚刚看到如何构建和训练一个神经网络来对手写数字进行分类,只需不到 15 行的 Python 代码。在本章和下一章中,我们将详细介绍我们刚刚预览的每个移动部分,并澄清幕后发生的事情。你将了解张量,这些数据存储对象进入模型;张量操作,层是由什么组成的;以及梯度下降,它允许你的模型从训练示例中学习。
2.2 神经网络的数据表示
在前面的示例中,我们从存储在多维 NumPy 数组中的数据开始,也称为张量。一般来说,所有当前的机器学习系统都使用张量作为它们的基本数据结构。张量对于这个领域是基础的——以至于 TensorFlow 就是以它们命名的。那么什么是张量?
从本质上讲,张量是数据的容器——通常是数值数据。因此,它是一个数字的容器。你可能已经熟悉矩阵,它们是秩为 2 的张量:张量是对矩阵到任意数量的维度的泛化(请注意,在张量的上下文中,维度通常被称为轴)。
2.2.1 标量(秩为 0 的张量)
只包含一个数字的张量称为标量(或标量张量,或秩为 0 的张量,或 0D 张量)。在 NumPy 中,float32
或float64
数字是标量张量(或标量数组)。你可以通过ndim
属性显示 NumPy 张量的轴数;标量张量有 0 个轴(ndim
==
0
)。张量的轴数也称为其秩。这是一个 NumPy 标量:
>>> import numpy as np >>> x = np.array(12) >>> x array(12) >>> x.ndim 0
2.2.2 向量(秩为 1 的张量)
一组数字称为向量,或秩为 1 的张量,或 1D 张量。秩为 1 的张量被称为具有一个轴。以下是一个 NumPy 向量:
>>> x = np.array([12, 3, 6, 14, 7]) >>> x array([12, 3, 6, 14, 7]) >>> x.ndim 1
这个向量有五个条目,因此被称为5 维向量。不要混淆 5D 向量和 5D 张量!一个 5D 向量只有一个轴,并且沿着轴有五个维度,而一个 5D 张量有五个轴(并且可以有任意数量的维度沿着每个轴)。维度可以表示沿着特定轴的条目数(如我们的 5D 向量的情况),或者张量中轴的数量(比如 5D 张量),这有时可能会令人困惑。在后一种情况下,从技术上讲,谈论秩为 5 的张量更正确(张量的秩是轴的数量),但是不明确的符号5D 张量是常见的。
2.2.3 矩阵(秩为 2 的张量)
一组向量是一个矩阵,或秩为 2 的张量,或 2D 张量。矩阵有两个轴(通常称为行和列)。你可以将矩阵视为一个数字矩形网格。这是一个 NumPy 矩阵:
>>> x = np.array([[5, 78, 2, 34, 0], [6, 79, 3, 35, 1], [7, 80, 4, 36, 2]]) >>> x.ndim 2
第一个轴的条目称为行,第二个轴的条目称为列。在前面的示例中,[5,
78,
2,
34,
0]
是x
的第一行,[5,
6,
7]
是第一列。
2.2.4 秩为 3 及更高秩的张量
如果你将这些矩阵打包到一个新数组中,你将得到一个秩为 3 的张量(或 3D 张量),你可以将其视为一个数字立方体。以下是一个 NumPy 秩为 3 的张量:
>>> x = np.array([[[5, 78, 2, 34, 0], [6, 79, 3, 35, 1], [7, 80, 4, 36, 2]], [[5, 78, 2, 34, 0], [6, 79, 3, 35, 1], [7, 80, 4, 36, 2]], [[5, 78, 2, 34, 0], [6, 79, 3, 35, 1], [7, 80, 4, 36, 2]]]) >>> x.ndim 3
通过在数组中打包秩为 3 的张量,你可以创建一个秩为 4 的张量,依此类推。在深度学习中,你通常会处理秩为 0 到 4 的张量,尽管如果处理视频数据可能会升到 5。
2.2.5 关键属性
一个张量由三个关键属性定义:
- 轴的数量(秩)—例如,一个秩为 3 的张量有三个轴,一个矩阵有两个轴。这在 Python 库(如 NumPy 或 TensorFlow)中也被称为张量的
ndim
。 - 形状—这是一个描述张量沿着每个轴有多少维度的整数元组。例如,前面的矩阵示例的形状为
(3,
5)
,而秩为 3 的张量示例的形状为(3,
3,
5)
。一个向量的形状有一个单一元素,如(5,)
,而一个标量的形状为空,()
。 - 数据类型(通常在 Python 库中称为
dtype
)—这是张量中包含的数据类型;例如,张量的类型可以是float16
、float32
、float64
、uint8
等。在 TensorFlow 中,您也可能会遇到string
张量。
为了更具体地说明这一点,让我们回顾一下在 MNIST 示例中处理的数据。首先,我们加载 MNIST 数据集:
from tensorflow.keras.datasets import mnist (train_images, train_labels), (test_images, test_labels) = mnist.load_data()
接下来,我们显示张量train_images
的轴数,ndim
属性:
>>> train_images.ndim 3
这是它的形状:
>>> train_images.shape (60000, 28, 28)
这是它的数据类型,dtype
属性:
>>> train_images.dtype uint8
因此,我们这里有一个 8 位整数的秩-3 张量。更准确地说,它是一个由 60,000 个 28×28 整数矩阵组成的数组。每个这样的矩阵都是一个灰度图像,系数介于 0 和 255 之间。
让我们使用 Matplotlib 库(Colab 中预装的著名 Python 数据可视化库)显示这个秩-3 张量中的第四个数字;参见图 2.2。
图 2.2 数据集中的第四个样本
列表 2.8 显示第四个数字
import matplotlib.pyplot as plt digit = train_images[4] plt.imshow(digit, cmap=plt.cm.binary) plt.show()
当然,相应的标签是整数 9:
>>> train_labels[4]
2.2.6 在 NumPy 中操作张量
在先前的示例中,我们使用语法train_images[i]
沿着第一个轴选择了一个特定的数字。在张量中选择特定元素称为张量切片。让我们看看您可以在 NumPy 数组上进行的张量切片操作。
以下示例选择了第 10 到第 100 个数字(不包括第 100 个)并将它们放入形状为(90,
28,
28)
的数组中:
>>> my_slice = train_images[10:100] >>> my_slice.shape (90, 28, 28)
这等同于更详细的表示法,它为每个张量轴上的切片指定了起始索引和停止索引。请注意,:
等同于选择整个轴:
>>> my_slice = train_images[10:100, :, :] # ❶ >>> my_slice.shape (90, 28, 28) >>> my_slice = train_images[10:100, 0:28, 0:28] # ❷ >>> my_slice.shape (90, 28, 28)
❶ 等同于前面的示例
❷ 也等同于前面的示例
通常,您可以在每个张量轴上选择任意两个索引之间的切片。例如,为了选择所有图像右下角的 14×14 像素,您可以这样做:
my_slice = train_images[:, 14:, 14:]
也可以使用负索引。与 Python 列表中的负索引类似,它们表示相对于当前轴末尾的位置。为了将图像裁剪为中心 14×14 像素的补丁,您可以这样做:
my_slice = train_images[:, 7:-7, 7:-7]
2.2.7 数据批次的概念
通常,在深度学习中您会遇到的所有数据张量中的第一个轴(轴 0,因为索引从 0 开始)将是样本轴(有时称为样本维度)。在 MNIST 示例中,“样本”是数字的图像。
此外,深度学习模型不会一次处理整个数据集;相反,它们将数据分成小批次。具体来说,这是我们 MNIST 数字的一个批次,批量大小为 128:
batch = train_images[:128]
这是下一个批次:
batch = train_images[128:256]
和第n批次:
n = 3 batch = train_images[128 * n:128 * (n + 1)]
在考虑这样一个批量张量时,第一个轴(轴 0)被称为批量轴或批量维度。这是您在使用 Keras 和其他深度学习库时经常遇到的术语。
2.2.8 数据张量的现实世界示例
让我们通过几个类似于您以后会遇到的示例来更具体地说明数据张量。您将处理的数据几乎总是属于以下类别之一:
- 向量数据—形状为
(samples,
features)
的秩-2 张量,其中每个样本是一个数值属性(“特征”)向量 - 时间序列数据或序列数据—形状为
(samples,
timesteps,
features)
的秩-3 张量,其中每个样本是一个长度为timesteps
的特征向量序列 - 图像—形状为
(samples,
height,
width,
channels)
的秩-4 张量,其中每个样本是一个像素网格,每个像素由一组值(“通道”)表示 - 视频—形状为
(samples,
frames,
height,
width,
channels)
的秩-5 张量,其中每个样本是一个图像序列(长度为frames
)
2.2.9 向量数据
这是最常见的情况之一。在这样的数据集中,每个单个数据点可以被编码为一个向量,因此数据的批次将被编码为一个二阶张量(即向量数组),其中第一个轴是样本轴,第二个轴是特征轴。
让我们看两个例子:
- 一个人们的精算数据集,我们考虑每个人的年龄、性别和收入。每个人可以被描述为一个包含 3 个值的向量,因此一个包含 10 万人的完整数据集可以存储在形状为
(100000, 3)
的二阶张量中。 - 一个文本文档数据集,我们通过每个单词在文档中出现的次数(在一个包含 2 万个常见单词的字典中)来表示每个文档。每个文档可以被编码为一个包含 2 万个值的向量(字典中每个单词的计数),因此一个包含 500 个文档的完整数据集可以存储在形状为
(500, 20000)
的张量中。
2.2.10 时间序列数据或序列数据
每当数据中涉及时间(或序列顺序的概念)时,将其存储在具有显式时间轴的三阶张量中是有意义的。每个样本可以被编码为一系列向量(一个二阶张量),因此数据的批次将被编码为一个三阶张量(见图 2.3)。
图 2.3 一个三阶时间序列数据张量
按照惯例,时间轴始终是第二轴(索引为 1 的轴)。让我们看几个例子:
- 一个股票价格数据集。每分钟,我们存储股票的当前价格、过去一分钟内的最高价格和最低价格。因此,每分钟被编码为一个三维向量,整个交易日被编码为形状为
(390, 3)
的矩阵(一个交易日有 390 分钟),250 天的数据可以存储在形状为(250, 390, 3)
的三阶张量中。在这里,每个样本将是一天的数据。 - 一个推文数据集,我们将每条推文编码为一个由 128 个唯一字符组成的字母表中的 280 个字符序列。在这种情况下,每个字符可以被编码为一个大小为 128 的二进制向量(除了在对应字符的索引处有一个 1 条目外,其他都是全零向量)。然后,每条推文可以被编码为形状为
(280, 128)
的二阶张量,100 万条推文的数据集可以存储在形状为(1000000, 280, 128)
的张量中。
2.2.11 图像数据
图像通常具有三个维度:高度、宽度和颜色深度。尽管灰度图像(如我们的 MNIST 数字)只有一个颜色通道,因此可以存储在二阶张量中,但按照惯例,图像张量始终是三阶的,对于灰度图像有一个一维颜色通道。因此,一个包含 128 个尺寸为 256×256 的灰度图像的批次可以存储在形状为(128, 256, 256, 1)
的张量中,而一个包含 128 个彩色图像的批次可以存储在形状为(128, 256, 256, 3)
的张量中(见图 2.4)。
图 2.4 一个四阶图像数据张量
图像张量的形状有两种约定:通道最后约定(在 TensorFlow 中是标准的)和通道优先约定(越来越不受青睐)。
通道最后的约定将颜色深度轴放在最后:(样本数, 高度, 宽度, 颜色深度)
。与此同时,通道优先的约定将颜色深度轴放在批次轴之后:(样本数, 颜色深度, 高度, 宽度)
。使用通道优先的约定,前面的例子将变为(128, 1, 256, 256)
和(128, 3, 256, 256)
。Keras API 支持这两种格式。
2.2.12 视频数据
视频数据是少数几种需要使用五阶张量的真实世界数据之一。视频可以被理解为一系列帧,每一帧都是一幅彩色图像。因为每一帧可以存储在一个三阶张量中(height,
width,
color_ depth)
,一系列帧可以存储在一个四阶张量中(frames,
height,
width,
color_depth)
,因此一批不同视频可以存储在一个形状为(samples,
frames,
height,
width,
color_depth)
的五阶张量中。
例如,一个 60 秒、144 × 256 的 YouTube 视频剪辑,每秒采样 4 帧,将有 240 帧。四个这样的视频剪辑批次将存储在一个形状为(4,
240,
144,
256,
3)
的张量中。总共有 106,168,320 个值!如果张量的dtype
是float32
,每个值将以 32 位存储,因此张量将表示 405 MB。非常庞大!在现实生活中遇到的视频要轻得多,因为它们不是以float32
存储的,通常会被大幅压缩(例如 MPEG 格式)。
2.3 神经网络的齿轮:张量操作
就像任何计算机程序最终都可以简化为对二进制输入进行的一小组二进制操作(AND、OR、NOR 等)一样,深度神经网络学习到的所有变换都可以简化为应用于数值数据张量的一小组张量操作(或张量函数)。例如,可以对张量进行加法、乘法等操作。
在我们的初始示例中,我们通过将Dense
层堆叠在一起来构建我们的模型。一个 Keras 层实例看起来像这样:
keras.layers.Dense(512, activation="relu")
这一层可以被解释为一个函数,它以一个矩阵作为输入并返回另一个矩阵——输入张量的新表示。具体来说,函数如下(其中W
是矩阵,b
是向量,都是该层的属性):
output = relu(dot(input, W) + b)
让我们详细解释一下。这里有三个张量操作:
- 输入张量和名为
W
的张量之间的点积(dot
) - 结果矩阵和向量
b
之间的加法(+
) - 一个
relu
操作:relu(x)
是max(x,
0)
;relu
代表“修正线性单元”
注意 尽管本节完全涉及线性代数表达式,但这里不会找到任何数学符号。我发现,如果将数学概念表达为简短的 Python 代码片段而不是数学方程式,那么没有数学背景的程序员更容易掌握。因此,我们将在整个过程中使用 NumPy 和 TensorFlow 代码。
2.3.1 逐元素操作
relu
操作和加法都是逐元素操作:这些操作独立应用于所考虑张量中的每个条目。这意味着这些操作非常适合于高度并行的实现(矢量化实现,这个术语来自于 20 世纪 70-90 年代的矢量处理器超级计算机架构)。如果你想编写一个逐元素操作的朴素 Python 实现,你会使用一个for
循环,就像这个逐元素relu
操作的朴素实现中所示:
def naive_relu(x): assert len(x.shape) == 2 # ❶ x = x.copy() # ❷ for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] = max(x[i, j], 0) return x
❶ x 是一个二阶 NumPy 张量。
❷ 避免覆盖输入张量。
你可以对加法做同样的操作:
def naive_add(x, y): assert len(x.shape) == 2 # ❶ assert x.shape == y.shape x = x.copy() # ❷ for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] += y[i, j] return x
❶ x 和 y 是二阶 NumPy 张量。
❷ 避免覆盖输入张量。
在同样的原则下,你可以进行逐元素乘法、减法等操作。
在实践中,处理 NumPy 数组时,这些操作也作为优化良好的内置 NumPy 函数可用,它们本身将繁重的工作委托给基本线性代数子程序(BLAS)实现。BLAS 是低级别、高度并行、高效的张量操作例程,通常用 Fortran 或 C 实现。
因此,在 NumPy 中,你可以进行以下逐元素操作,速度将非常快:
import numpy as np z = x + y # ❶ z = np.maximum(z, 0.) # ❷
❶ 逐元素加法
❷ 逐元素 relu
让我们实际计算一下时间差异:
import time x = np.random.random((20, 100)) y = np.random.random((20, 100)) t0 = time.time() for _ in range(1000): z = x + y z = np.maximum(z, 0.) print("Took: {0:.2f} s".format(time.time() - t0))
这需要 0.02 秒。与此同时,朴素版本需要惊人的 2.45 秒:
t0 = time.time() for _ in range(1000): z = naive_add(x, y) z = naive_relu(z) print("Took: {0:.2f} s".format(time.time() - t0))
同样,在 GPU 上运行 TensorFlow 代码时,通过完全向量化的 CUDA 实现执行元素级操作,可以最好地利用高度并行的 GPU 芯片架构。
2.3.2 广播
我们之前天真的实现naive_add
仅支持具有相同形状的秩为 2 的张量的加法。但在之前介绍的Dense
层中,我们添加了一个秩为 2 的张量和一个向量。当被加的两个张量的形状不同时,加法会发生什么?
在可能的情况下,如果没有歧义,较小的张量将被广播以匹配较大张量的形状。广播包括两个步骤:
- 轴(称为广播轴)被添加到较小的张量中,以匹配较大张量的
ndim
。 - 较小的张量沿着这些新轴重复,以匹配较大张量的完整形状。
让我们看一个具体的例子。考虑形状为(32,
10)
的X
和形状为(10,)
的y
:
import numpy as np X = np.random.random((32, 10)) # ❶ y = np.random.random((10,)) # ❷
❶ X 是一个形状为(32, 10)的随机矩阵。
❷ y 是一个 NumPy 向量。
首先,我们向y
添加一个空的第一个轴,其形状变为(1,
10)
:
y = np.expand_dims(y, axis=0) # ❶
❶ y 的形状现在是(1, 10)。
然后,我们沿着这个新轴重复y
32 次,这样我们就得到了一个形状为(32,
10)
的张量Y
,其中Y[i,
:]
==
y
,对于i
在 range(0,
32)
:
Y = np.concatenate([y] * 32, axis=0) # ❶
❶ 沿着轴 0 重复 y 32 次,得到形状为(32, 10)的 Y。
此时,我们可以继续添加X
和Y
,因为它们具有相同的形状。
在实现方面,不会创建新的秩为 2 的张量,因为那样会非常低效。重复操作完全是虚拟的:它发生在算法级别而不是内存级别。但想象向量沿着新轴重复 10 次是一个有用的心理模型。以下是天真实现的样子:
def naive_add_matrix_and_vector(x, y): assert len(x.shape) == 2 # ❶ assert len(y.shape) == 1 # ❷ assert x.shape[1] == y.shape[0] x = x.copy() # ❸ for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] += y[j] return x
❶ x 是一个秩为 2 的 NumPy 张量。
❷ y 是一个 NumPy 向量。
❸ 避免覆盖输入张量。
使用广播,如果一个张量的形状为(a,
b,
...
n,
n
+
1,
...
m)
,另一个张量的形状为(n,
n
+
1,
...
m)
,通常可以执行元素级操作。广播将自动发生在轴a
到n
-
1
。
以下示例通过广播将两个不同形状的张量应用于元素级maximum
操作:
import numpy as np x = np.random.random((64, 3, 32, 10)) # ❶ y = np.random.random((32, 10)) # ❷ z = np.maximum(x, y) # ❸
❶ x 是一个形状为(64, 3, 32, 10)的随机张量。
❷ y 是一个形状为(32, 10)的随机张量。
❸ 输出 z 的形状与 x 相同,为(64, 3, 32, 10)。
2.3.3 张量积
张量积或点积(不要与逐元素乘积,即*
运算符混淆)是最常见、最有用的张量操作之一。
在 NumPy 中,使用np.dot
函数进行张量积(因为张量积的数学表示通常是一个点):
x = np.random.random((32,)) y = np.random.random((32,)) z = np.dot(x, y)
在数学表示中,您会用一个点(•)表示该操作:
z = x • y
从数学上讲,点操作是什么?让我们从两个向量x
和y
的点积开始。计算如下:
def naive_vector_dot(x, y): assert len(x.shape) == 1 # ❶ assert len(y.shape) == 1 # ❶ assert x.shape[0] == y.shape[0] z = 0. for i in range(x.shape[0]): z += x[i] * y[i] return z
❶ x 和 y 是 NumPy 向量。
您可能已经注意到两个向量之间的点积是一个标量,只有元素数量相同的向量才适用于点积。
您还可以计算矩阵x
和向量y
之间的点积,返回一个向量,其中系数是y
和x
的行之间的点积。您可以按如下方式实现它:
def naive_matrix_vector_dot(x, y): assert len(x.shape) == 2 # ❶ assert len(y.shape) == 1 # ❷ assert x.shape[1] == y.shape[0] # ❸ z = np.zeros(x.shape[0]) # ❹ for i in range(x.shape[0]): for j in range(x.shape[1]): z[i] += x[i, j] * y[j] return z
❶ x 是一个 NumPy 矩阵。
❷ y 是一个 NumPy 向量。
❸ x 的第一个维度必须与 y 的第 0 维度相同!
❹ 此操作返回一个与 y 形状相同的 0 向量。
您还可以重用我们之前编写的代码,这突显了矩阵-向量乘积与向量乘积之间的关系:
def naive_matrix_vector_dot(x, y): z = np.zeros(x.shape[0]) for i in range(x.shape[0]): z[i] = naive_vector_dot(x[i, :], y) return z
请注意,只要两个张量中的一个的ndim
大于 1,dot
就不再是对称的,也就是说dot(x,
y)
不等同于dot(y,
x)
。
当然,点积可以推广到具有任意数量轴的张量。最常见的应用可能是两个矩阵之间的点积。只有当 x.shape[1]
==
y.shape[0]
时,你才能计算两个矩阵 x
和 y
的点积(dot(x,
y)
)。结果是一个形状为 (x.shape[0],
y.shape[1])
的矩阵,其中系数是 x
的行和 y
的列之间的向量积。这是一个简单的实现:
def naive_matrix_dot(x, y): assert len(x.shape) == 2 # ❶ assert len(y.shape) == 2 # ❶ assert x.shape[1] == y.shape[0] # ❷ z = np.zeros((x.shape[0], y.shape[1])) # ❸ for i in range(x.shape[0]): # ❹ for j in range(y.shape[1]): # ❺ row_x = x[i, :] column_y = y[:, j] z[i, j] = naive_vector_dot(row_x, column_y) return z
❶ x 和 y 是 NumPy 矩阵。
❷ x 的第一个维度必须与 y 的第 0 维度相同!
❸ 此操作返回一个具有特定形状的零矩阵。
❹ 迭代 x 的行 . . .
❺ . . . 并在 y 的列上。
要理解点积形状兼容性,有助于通过将输入和输出张量对齐来可视化它们,如图 2.5 所示。
图 2.5 矩阵点积框图
在图中,x
、y
和 z
被描绘为矩形(系数的字面框)。因为 x
的行和 y
的列必须具有相同的大小,所以 x
的宽度必须与 y
的高度匹配。如果你继续开发新的机器学习算法,你可能会经常画这样的图。
更一般地,你可以按照前面为 2D 情况概述的相同形状兼容性规则,计算更高维度张量之间的点积:
(a, b, c, d) • (d,) → (a, b, c) (a, b, c, d) • (d, e) → (a, b, c, e)
等等。
2.3.4 张量重塑
理解的第三种张量操作是张量重塑。虽然在我们第一个神经网络示例中的Dense
层中没有使用它,但在将手写数字数据输入模型之前对数据进行预处理时使用了它:
train_images = train_images.reshape((60000, 28 * 28))
重塑张量意味着重新排列其行和列以匹配目标形状。显然,重塑后的张量与初始张量具有相同数量的系数。通过简单的例子最容易理解重塑:
>>> x = np.array([[0., 1.], [2., 3.], [4., 5.]]) >>> x.shape (3, 2) >>> x = x.reshape((6, 1)) >>> x array([[ 0.], [ 1.], [ 2.], [ 3.], [ 4.], [ 5.]]) >>> x = x.reshape((2, 3)) >>> x array([[ 0., 1., 2.], [ 3., 4., 5.]])
常见的重塑的一个特殊情况是转置。转置矩阵意味着交换其行和列,使得 x[i,
:]
变为 x[:,
i]
:
>>> x = np.zeros((300, 20)) # ❶ >>> x = np.transpose(x) >>> x.shape (20, 300)
❶ 创建一个形状为 (300, 20) 的全零矩阵
2.3.5 张量操作的几何解释
因为张量操作中的张量内容可以被解释为某个几何空间中点的坐标,所以所有张量操作都有几何解释。例如,让我们从以下向量开始:
A = [0.5, 1]
这是二维空间中的一个点(参见图 2.6)。通常将向量描绘为连接原点和点的箭头,如图 2.7 所示。
图 2.6 二维空间中的一个点
图 2.7 作为箭头的二维空间中的点
让我们考虑一个新点,B
=
[1,
0.25]
,我们将其添加到之前的点上。这是通过将向量箭头链接在一起几何地完成的,结果位置是代表前两个向量之和的向量(参见图 2.8)。如你所见,将向量 B 添加到向量 A 表示将点 A 复制到一个新位置,其距离和方向从原始点 A 确定为向量 B。如果你将相同的向量加法应用于平面上的一组点(一个“对象”),你将在一个新位置创建整个对象的副本(参见图 2.9)。因此,张量加法表示平移对象(在不扭曲对象的情况下移动对象)到某个方向的某个距离。
图 2.8 两个向量之和的几何解释
一般来说,诸如平移、旋转、缩放、倾斜等基本几何操作可以表示为张量操作。以下是一些例子:
- 平移:正如你刚刚看到的,向点添加一个向量将使点沿着固定方向移动固定量。应用于一组点(如 2D 对象),这称为“平移”(见图 2.9)。
图 2.9 2D 平移作为向量相加 - 旋转:通过角度θ逆时针旋转 2D 向量(见图 2.10)可以通过与 2 × 2 矩阵
R
=
[[cos(theta),
-sin(theta)],
[sin(theta),
cos(theta)]]
进行点积实现。
图 2.10 2D 旋转(逆时针)作为点积 - 缩放:图像的垂直和水平缩放(见图 2.11)可以通过与 2 × 2 矩阵
S
=
[[horizontal_factor,
0],
[0,
vertical_factor]]
进行点积实现(请注意,这样的矩阵称为“对角矩阵”,因为它只在从左上到右下的“对角线”上有非零系数)。
图 2.11 2D 缩放作为点积 - 线性变换:与任意矩阵进行点积实现了线性变换。请注意,前面列出的缩放和旋转按定义都是线性变换。
- 仿射变换:仿射变换(见图 2.12)是线性变换(通过与某些矩阵进行点积实现)和平移(通过向量相加实现)的组合。你可能已经意识到,这正是
Dense
层实现的y
=
W
•
x
+
b
计算!没有激活函数的Dense
层就是一个仿射层。
图 2.12 平面中的仿射变换 - 带有
relu
激活的 Dense 层:关于仿射变换的一个重要观察是,如果你重复应用许多次,最终仍然得到一个仿射变换(因此你可以一开始就应用那一个仿射变换)。让我们尝试两次:affine2(affine1(x))
=
W2
•
(W1
•
x
+
b1)
+
b2
=
(W2
•
W1)
•
x
+
(W2
•
b1
+
b2)
。这是一个仿射变换,其中线性部分是矩阵W2
•
W1
,平移部分是向量W2
•
b1
+
b2
。因此,一个完全由Dense
层组成且没有激活函数的多层神经网络等效于单个Dense
层。这种“深度”神经网络实际上只是一个伪装的线性模型!这就是为什么我们需要激活函数,比如relu
(在图 2.13 中展示)。由于激活函数,一系列Dense
层可以实现非常复杂、非线性的几何变换,为你的深度神经网络提供非常丰富的假设空间。我们将在下一章更详细地讨论这个想法。
图 2.13 仿射变换后跟relu
激活
2.3.6 深度学习的几何解释
你刚刚学到神经网络完全由张量操作链组成,而这些张量操作只是输入数据的简单几何变换。由此可见,你可以将神经网络解释为在高维空间中非常复杂的几何变换,通过一系列简单步骤实现。
在 3D 中,以下心理形象可能会有所帮助。想象两张彩纸:一张红色,一张蓝色。将它们叠在一起。现在将它们一起揉成一个小球。那个揉皱的纸球就是你的输入数据,每张纸是分类问题中的一个数据类别。神经网络的目的是找出一个可以展开纸球的变换,使得两个类别再次清晰可分(见图 2.14)。通过深度学习,这将被实现为对 3D 空间的一系列简单变换,就像你可以用手指在纸球上一次移动一次一样。
图 2.14 展开复杂数据流形
展开纸团就是机器学习的目的:在高维空间中找到复杂、高度折叠数据流形的整洁表示(流形是一个连续的表面,就像我们折叠的纸张)。此时,你应该对为什么深度学习擅长这一点有很好的直觉:它采用逐步将复杂的几何变换分解为一长串基本变换的方法,这几乎就是人类展开纸团时会遵循的策略。深度网络中的每一层应用一个能稍微解开数据的变换,而深层堆叠的层使得一个极其复杂的解开过程变得可行。
2.4 神经网络的引擎:基于梯度的优化
正如你在前一节中看到的,我们第一个模型示例中的每个神经层将其输入数据转换如下:
output = relu(dot(input, W) + b)
在这个表达式中,W
和b
是层的属性的张量。它们被称为层的权重或可训练参数(分别是kernel
和bias
属性)。这些权重包含了模型从训练数据中学到的信息。
最初,这些权重矩阵被填充了小的随机值(这一步被称为随机初始化)。当W
和b
是随机的时候,当然没有理由期望relu(dot(input,
W)
+
b)
会产生任何有用的表示。得到的表示是毫无意义的,但它们是一个起点。接下来要做的是逐渐调整这些权重,基于一个反馈信号。这种逐渐调整,也称为训练,就是机器学习的学习过程。
这发生在所谓的训练循环中,其工作方式如下。重复这些步骤直到损失看起来足够低:
- 绘制一批训练样本
x
和相应的目标y_true
。 - 在
x
上运行模型(称为前向传播)以获得预测值y_pred
。 - 计算模型在批次上的损失,这是
y_pred
和y_true
之间的不匹配度的度量。 - 更新模型的所有权重,以稍微减少这一批次上的损失。
最终,你会得到一个在训练数据上损失非常低的模型:预测值y_pred
与期望目标y_true
之间的匹配度很低。模型已经“学会”将其输入映射到正确的目标。从远处看,这可能看起来像魔术,但当你将其简化为基本步骤时,它其实很简单。
第一步听起来足够简单——只是 I/O 代码。第二步和第三步仅仅是应用少量张量操作,所以你可以纯粹根据你在前一节中学到的内容来实现这些步骤。困难的部分在于第四步:更新模型的权重。给定模型中的一个单独权重系数,你如何计算这个系数应该增加还是减少,以及增加多少?
一个天真的解决方案是冻结模型中除了正在考虑的一个标量系数之外的所有权重,并尝试不同的值来调整这个系数。假设系数的初始值是 0.3。在一批数据上进行前向传播后,模型在该批次上的损失为 0.5。如果你将系数的值更改为 0.35 并重新运行前向传播,损失增加到 0.6。但如果你将系数降低到 0.25,损失降至 0.4。在这种情况下,似乎通过减小系数-0.05 来有助于最小化损失。这将需要对模型中的所有系数重复进行。
但这样的方法将非常低效,因为你需要为每个单独的系数(通常有成千上万甚至数百万个)计算两次前向传播(这是昂贵的)。幸运的是,有一个更好的方法:梯度下降。
梯度下降是现代神经网络的优化技术。这是其要点。我们模型中使用的所有函数(如 dot
或 +
)以平滑连续的方式转换其输入:例如,如果你看 z
=
x
+
y
,那么 y
的微小变化只会导致 z
的微小变化,如果你知道 y
变化的方向,你就可以推断出 z
变化的方向。从数学上讲,你会说这些函数是可导的。如果你将这些函数链接在一起,你得到的更大函数仍然是可导的。特别是,这适用于将模型系数映射到批量数据上的模型损失的函数:模型系数的微小变化导致损失值的微小、可预测的变化。这使你能够使用一种称为梯度的数学运算符描述损失随着你将模型系数朝不同方向移动而变化的方式。如果你计算这个梯度,你可以使用它来移动系数(一次性全部更新,而不是逐个更新),朝着减小损失的方向移动系数。
如果你已经知道可导的含义和梯度是什么,你可以跳到第 2.4.3 节。否则,接下来的两节将帮助你理解这些概念。
Python 深度学习第二版(GPT 重译)(一)(3)https://developer.aliyun.com/article/1485260