From dcf2150bdbf4fb155807c034c752e04141463298 Mon Sep 17 00:00:00 2001 From: xyw5vplus1 <314528522@qq.com> Date: Wed, 10 Mar 2021 21:54:38 +0800 Subject: [PATCH 001/103] fix typo in ndarray.md --- chapter_preliminaries/ndarray.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preliminaries/ndarray.md b/chapter_preliminaries/ndarray.md index 1d5eea1a0..69e73a1ed 100644 --- a/chapter_preliminaries/ndarray.md +++ b/chapter_preliminaries/ndarray.md @@ -175,7 +175,7 @@ tf.constant([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) 这本书不是关于软件工程的。我们的兴趣不仅仅限于从数组读取和写入数据。我们想在这些数组上执行数学运算。一些最简单且最有用的操作是 *按元素*(elementwise) 操作。它们将标准标量运算符应用于数组的每个元素。对于将两个数组作为输入的函数,按元素运算将二元运算符应用于两个数组中的每对位置对应的元素。我们可以基于任何从标量到标量的函数来创建按元素函数。 -在数学表示法中,我们将通过符号 $f: \mathbb{R} \rightarrow \mathbb{R}$ 来表示 *一元* 标量运算符(只接收一个输入)。这意味着该函数从任何实数($\mathbb{R}$)映射到另一个实数。同样,我们通过符号 $f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}$ 表示 *二元* 标量运算符,这意味着该函数接收两个输入,并产生一个输出。给定同一形状的任意两个向量$\mathbf{u}$和$\mathbf{v}$ 和二元运算符 $f$,我们可以得到向量$\mathbf{c} = F(\mathbf{u},\mathbf{v})$。具体计算方法是$c_i \gets f(u_i, v_i)$ ,其中 $c_i$、u_i$ 和 $v_i$ 分别是向量$\mathbf{c}$、$\mathbf{u}$ 和 $\mathbf{v}$中的元素。在这里,我们通过将标量函数升级为按元素向量运算来生成向量值 $F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d$。 +在数学表示法中,我们将通过符号 $f: \mathbb{R} \rightarrow \mathbb{R}$ 来表示 *一元* 标量运算符(只接收一个输入)。这意味着该函数从任何实数($\mathbb{R}$)映射到另一个实数。同样,我们通过符号 $f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}$ 表示 *二元* 标量运算符,这意味着该函数接收两个输入,并产生一个输出。给定同一形状的任意两个向量$\mathbf{u}$和$\mathbf{v}$ 和二元运算符 $f$,我们可以得到向量$\mathbf{c} = F(\mathbf{u},\mathbf{v})$。具体计算方法是$c_i \gets f(u_i, v_i)$ ,其中 $c_i$、$u_i$ 和 $v_i$ 分别是向量$\mathbf{c}$、$\mathbf{u}$ 和 $\mathbf{v}$中的元素。在这里,我们通过将标量函数升级为按元素向量运算来生成向量值 $F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d$。 对于任意具有相同形状的张量,[**常见的标准算术运算符(`+`、`-`、`*`、`/` 和 `**`)都可以被升级为按元素运算**]。我们可以在同一形状的任意两个张量上调用按元素操作。在下面的例子中,我们使用逗号来表示一个具有5个元素的元组,其中每个元素都是按元素操作的结果。 From 48f46d1ced2a5fda0840d1e107d9397372e1b767 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Fri, 19 Mar 2021 11:39:38 -0700 Subject: [PATCH 002/103] rebase (#692) --- .../image-classification-dataset.md | 16 +++---- .../linear-regression-concise.md | 18 ++++---- .../linear-regression-scratch.md | 26 ++++++------ chapter_linear-networks/linear-regression.md | 18 ++++---- .../softmax-regression-concise.md | 12 +++--- .../softmax-regression-scratch.md | 42 ++++++++++--------- 6 files changed, 68 insertions(+), 64 deletions(-) diff --git a/chapter_linear-networks/image-classification-dataset.md b/chapter_linear-networks/image-classification-dataset.md index a607ad63d..63e760b33 100644 --- a/chapter_linear-networks/image-classification-dataset.md +++ b/chapter_linear-networks/image-classification-dataset.md @@ -1,6 +1,8 @@ # 图像分类数据集 :label:`sec_fashion_mnist` +(~~MNIST数据集是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。我们将使用类似但更复杂的Fashion-MNIST数据集~~) + 目前广泛使用的图像分类数据集之一是 MNIST 数据集 :cite:`LeCun.Bottou.Bengio.ea.1998`。虽然它是很不错的基准数据集,但按今天的标准,即使是简单的模型也能达到95%以上的分类准确率,因此不适合区分强模型和弱模型。如今,MNIST更像是一个健全检查,而不是一个基准。 为了提高难度,我们将在接下来的章节中讨论在2017年发布的性质相似但相对复杂的Fashion-MNIST数据集 :cite:`Xiao.Rasul.Vollgraf.2017`。 @@ -36,7 +38,7 @@ d2l.use_svg_display() ## 读取数据集 -我们可以通过框架中的内置函数将 Fashion-MNIST 数据集下载并读取到内存中。 +我们可以[**通过框架中的内置函数将 Fashion-MNIST 数据集下载并读取到内存中**]。 ```{.python .input} mnist_train = gluon.data.vision.FashionMNIST(train=True) @@ -78,6 +80,8 @@ len(mnist_train[0]), len(mnist_test[0]) mnist_train[0][0].shape ``` +[~~两个可视化数据集的函数~~] + Fashion-MNIST中包含的10个类别分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。以下函数用于在数字标签索引及其文本名称之间进行转换。 ```{.python .input} @@ -107,8 +111,6 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save return axes ``` - - ```{.python .input} #@tab pytorch def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save @@ -130,8 +132,7 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save return axes ``` -以下是训练数据集中前几个样本的图像及其相应的标签(文本形式)。 - +以下是训练数据集中前[**几个样本的图像及其相应的标签**](文本形式)。 ```{.python .input} X, y = mnist_train[:18] @@ -156,7 +157,7 @@ show_images(X, 2, 9, titles=get_fashion_mnist_labels(y)); ## 读取小批量 为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建一个。 -回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为`batch_size`。我们在训练数据迭代器中还随机打乱了所有样本。 +回顾一下,在每次迭代中,数据加载器每次都会[**读取一小批量数据,大小为`batch_size`**]。我们在训练数据迭代器中还随机打乱了所有样本。 ```{.python .input} batch_size = 256 @@ -204,7 +205,7 @@ f'{timer.stop():.2f} sec' ## 整合所有组件 -现在我们定义了 `load_data_fashion_mnist` 函数,用于获取和读取Fashion-MNIST数据集。它返回训练集和验证集的数据迭代器。此外,它还接受一个可选参数,用来将图像大小调整为另一种形状。 +现在我们[**定义 `load_data_fashion_mnist` 函数**],用于获取和读取Fashion-MNIST数据集。它返回训练集和验证集的数据迭代器。此外,它还接受一个可选参数,用来将图像大小调整为另一种形状。 ```{.python .input} def load_data_fashion_mnist(batch_size, resize=None): #@save @@ -257,6 +258,7 @@ def load_data_fashion_mnist(batch_size, resize=None): #@save tf.data.Dataset.from_tensor_slices(process(*mnist_test)).batch( batch_size).map(resize_fn)) ``` + 下面,我们通过指定`resize`参数来测试`load_data_fashion_mnist`函数的图像大小调整功能。 ```{.python .input} diff --git a/chapter_linear-networks/linear-regression-concise.md b/chapter_linear-networks/linear-regression-concise.md index abbc5fd97..55437c0b3 100644 --- a/chapter_linear-networks/linear-regression-concise.md +++ b/chapter_linear-networks/linear-regression-concise.md @@ -4,11 +4,11 @@ 在过去的几年里,出于对深度学习强烈的兴趣,许多公司、学者和业余爱好者开发了各种成熟的开源框架。通过这些框架可以自动化实现基于梯度的学习算法中重复性的工作。 在 :numref:`sec_linear_scratch` 中,我们只依赖了:(1)通过张量来进行数据存储和线性代数;(2)通过自动微分来计算梯度。实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用,现代深度学习库也为我们实现了这些组件。 -在本节中,我们将介绍如何通过使用深度学习框架的高级API来简洁地实现 :numref:`sec_linear_scratch` 中的线性回归模型。 +在本节中,我们将介绍如何(**通过使用深度学习框架来简洁地实现**) :numref:`sec_linear_scratch` 中的(**线性回归模型**)。 ## 生成数据集 -首先,我们生成与 :numref:`sec_linear_scratch` 中相同的数据集。 +与 :numref:`sec_linear_scratch` 中类似,我们首先[**生成数据集**]。 ```{.python .input} from d2l import mxnet as d2l @@ -40,7 +40,7 @@ features, labels = d2l.synthetic_data(true_w, true_b, 1000) ## 读取数据集 -我们可以调用框架中现有的API来读取数据,而不使用我们自己定义的迭代器。我们将 `features` 和 `labels` 作为API的参数传递,并在实例化数据迭代器对象时指定 `batch_size`。此外,布尔值 `is_train` 表示是否希望数据迭代器对象在每个迭代周期内打乱数据。 +我们可以[**调用框架中现有的API来读取数据**]。我们将 `features` 和 `labels` 作为API的参数传递,并在实例化数据迭代器对象时指定 `batch_size`。此外,布尔值 `is_train` 表示是否希望数据迭代器对象在每个迭代周期内打乱数据。 ```{.python .input} def load_array(data_arrays, batch_size, is_train=True): #@save @@ -86,7 +86,7 @@ next(iter(data_iter)) 当我们在 :numref:`sec_linear_scratch` 中实现线性回归时,我们明确定义了模型参数变量,并编写了计算的代码,这样通过基本的线性代数运算得到输出。但是,如果模型变得更加复杂,而且当你几乎每天都需要实现模型时,你会想简化这个过程。这种情况类似于从头开始编写自己的博客。做一两次是有益的、有启发性的,但如果每次你每需要一个博客就花一个月的时间重新发明轮子,那你将是一个糟糕的网页开发者。 -对于标准操作,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。我们首先定义一个模型变量`net`,它是一个 `Sequential` 类的实例。 `Sequential` 类为串联在一起的多个层定义了一个容器。当给定输入数据, `Sequential` 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要`Sequential`。但是由于以后几乎所有的模型都是多层的,在这里使用`Sequential`会让你熟悉标准的流水线。 +对于标准操作,我们可以[**使用框架的预定义好的层**]。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。我们首先定义一个模型变量`net`,它是一个 `Sequential` 类的实例。 `Sequential` 类为串联在一起的多个层定义了一个容器。当给定输入数据, `Sequential` 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要`Sequential`。但是由于以后几乎所有的模型都是多层的,在这里使用`Sequential`会让你熟悉标准的流水线。 回顾 :numref:`fig_single_neuron` 中的单层网络架构,这一单层被称为 *全连接层*(fully-connected layer),因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。 @@ -127,7 +127,7 @@ net = tf.keras.Sequential() net.add(tf.keras.layers.Dense(1)) ``` -## 初始化模型参数 +## (**初始化模型参数**) 在使用`net`之前,我们需要初始化模型参数。如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 @@ -186,7 +186,7 @@ net.add(tf.keras.layers.Dense(1, kernel_initializer=initializer)) :end_tab: :begin_tab:`pytorch` -计算均方误差使用的是`MSELoss`类,也称为平方 $L_2$ 范数。默认情况下,它返回所有样本损失的平均值。 +[**计算均方误差使用的是`MSELoss`类,也称为平方 $L_2$ 范数**]。默认情况下,它返回所有样本损失的平均值。 :end_tab: :begin_tab:`tensorflow` @@ -214,7 +214,7 @@ loss = tf.keras.losses.MeanSquaredError() :end_tab: :begin_tab:`pytorch` -小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch 在 `optim` 模块中实现了该算法的许多变种。当我们实例化一个 `SGD` 实例时,我们要指定优化的参数(可通过 `net.parameters()` 从我们的模型中获得)以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 `lr`值,这里设置为 0.03。 +小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch 在 `optim` 模块中实现了该算法的许多变种。当我们(**实例化 `SGD` 实例**)时,我们要指定优化的参数(可通过 `net.parameters()` 从我们的模型中获得)以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 `lr`值,这里设置为 0.03。 :end_tab: :begin_tab:`tensorflow` @@ -241,7 +241,7 @@ trainer = tf.keras.optimizers.SGD(learning_rate=0.03) 通过深度学习框架的高级API来实现我们的模型只需要相对较少的代码。 我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。 当我们需要更复杂的模型时,高级API的优势将大大增加。 -当我们有了所有的基本组件,训练过程代码与我们从零开始实现所有东西时所做的非常相似。 +当我们有了所有的基本组件,[**训练过程代码与我们从零开始实现时所做的非常相似**]。 回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(`train_data`),不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量,我们会进行以下步骤: @@ -289,7 +289,7 @@ for epoch in range(num_epochs): print(f'epoch {epoch + 1}, loss {l:f}') ``` -下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 +下面我们[**比较生成数据集的真实参数和通过有限数据训练获得的模型参数**]。 要访问参数,我们首先从 `net` 访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。 diff --git a/chapter_linear-networks/linear-regression-scratch.md b/chapter_linear-networks/linear-regression-scratch.md index 67a1b0cc1..d07ec5b4a 100644 --- a/chapter_linear-networks/linear-regression-scratch.md +++ b/chapter_linear-networks/linear-regression-scratch.md @@ -2,7 +2,7 @@ :label:`sec_linear_scratch` 在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。 -在这一节中,我们将从零开始实现整个方法,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。 +在这一节中,(**我们将从零开始实现整个方法,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器**)。 虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保你真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。 在这一节中,我们将只使用张量和自动求导。在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。 @@ -33,14 +33,15 @@ import random ## 生成数据集 -为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。 +为了简单起见,我们将[**根据带有噪声的线性模型构造一个人造数据集。**] 我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。 我们将使用低维数据,这样可以很容易地将其可视化。 在下面的代码中,我们生成一个包含1000个样本的数据集,每个样本包含从标准正态分布中采样的2个特征。我们的合成数据集是一个矩阵 $\mathbf{X}\in \mathbb{R}^{1000 \times 2}$。 -我们使用线性模型参数$\mathbf{w} = [2, -3.4]^\top$、$b = 4.2$和噪声项$\epsilon$生成数据集及其标签: +(**我们使用线性模型参数$\mathbf{w} = [2, -3.4]^\top$、$b = 4.2$和噪声项$\epsilon$生成数据集及其标签: $$\mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon.$$ +**) 你可以将 $\epsilon$ 视为捕获特征和标签时的潜在观测误差。在这里我们认为标准假设成立,即$\epsilon$服从均值为0的正态分布。 为了简化问题,我们将标准差设为0.01。下面的代码生成合成数据集。 @@ -74,7 +75,7 @@ true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 1000) ``` -注意,`features` 中的每一行都包含一个二维数据样本,`labels` 中的每一行都包含一维标签值(一个标量)。 +注意,[**`features` 中的每一行都包含一个二维数据样本,`labels` 中的每一行都包含一维标签值(一个标量)**]。 ```{.python .input} #@tab all @@ -94,8 +95,8 @@ d2l.plt.scatter(d2l.numpy(features[:, 1]), d2l.numpy(labels), 1); 回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数,该函数能打乱数据集中的样本并以小批量方式获取数据。 -在下面的代码中,我们定义一个`data_iter` 函数实现这一功能。 -该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为`batch_size`的小批量。每个小批量包含一组特征和标签。 +在下面的代码中,我们[**定义一个`data_iter` 函数, +该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为`batch_size`的小批量**]。每个小批量包含一组特征和标签。 ```{.python .input} #@tab mxnet, pytorch @@ -141,7 +142,8 @@ for X, y in data_iter(batch_size, features, labels): 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多,它可以处理存储在文件中的数据和通过数据流提供的数据。 -## 初始化模型参数 +(~~定义~~) +## (**初始化模型参数**) 在我们开始用小批量随机梯度下降优化我们的模型参数之前,我们需要先有一些参数。 在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0。 @@ -170,7 +172,7 @@ b = tf.Variable(tf.zeros(1), trainable=True) 每次更新都需要计算损失函数关于模型参数的梯度。有了这个梯度,我们就可以向减小损失的方向更新每个参数。 因为手动计算梯度很枯燥而且容易出错,所以没有人会手动计算梯度。我们使用 :numref:`sec_autograd` 中引入的自动微分来计算梯度。 -## 定义模型 +## (**定义模型**) 接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。 回想一下,要计算线性模型的输出,我们只需计算输入特征 $\mathbf{X}$ 和模型权重$\mathbf{w}$的矩阵-向量乘法后加上偏置$b$。注意,上面的$\mathbf{Xw}$ 是一个向量,而$b$是一个标量。回想一下 :numref:`subsec_broadcasting` 中描述的广播机制。当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。 @@ -182,7 +184,7 @@ def linreg(X, w, b): #@save return d2l.matmul(X, w) + b ``` -## 定义损失函数 +## [**定义损失函数**] 因为要更新模型。需要计算损失函数的梯度,所以我们应该先定义损失函数。 这里我们使用 :numref:`sec_linear_regression` 中描述的平方损失函数。 @@ -195,7 +197,7 @@ def squared_loss(y_hat, y): #@save return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 ``` -## 定义优化算法 +## (**定义优化算法**) 正如我们在 :numref:`sec_linear_regression` 中讨论的,线性回归有解析解。然而,这是一本关于深度学习的书,而不是一本关于线性回归的书。 由于这本书介绍的其他模型都没有解析解,下面我们将在这里介绍小批量随机梯度下降的工作示例。 @@ -231,7 +233,7 @@ def sgd(params, grads, lr, batch_size): #@save ## 训练 -现在我们已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分了。 +现在我们已经准备好了模型训练所有需要的要素,可以实现主要的[**训练过程**]部分了。 理解这段代码至关重要,因为在整个深度学习的职业生涯中,你会看到一遍又一遍几乎相同的训练过程。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。最后,我们调用优化算法 `sgd` 来更新模型参数。 @@ -295,7 +297,7 @@ for epoch in range(num_epochs): ``` 因为我们使用的是自己合成的数据集,所以我们知道真正的参数是什么。 -因此,我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。事实上,真实参数和通过训练学到的参数确实非常接近。 +因此,我们可以通过[**比较真实参数和通过训练学到的参数来评估训练的成功程度**]。事实上,真实参数和通过训练学到的参数确实非常接近。 ```{.python .input} #@tab all diff --git a/chapter_linear-networks/linear-regression.md b/chapter_linear-networks/linear-regression.md index a2728eee7..0363693df 100644 --- a/chapter_linear-networks/linear-regression.md +++ b/chapter_linear-networks/linear-regression.md @@ -111,7 +111,7 @@ $$\begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B} ## 矢量化加速 -在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。为了实现这一点,需要我们对计算进行矢量化,从而利用好快速线性代数库,而不是在Python中编写开销高昂的for循环。 +在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。为了实现这一点,需要(**我们对计算进行矢量化,从而利用线性代数库,而不是在Python中编写开销高昂的for循环**)。 ```{.python .input} %matplotlib inline @@ -141,7 +141,7 @@ import numpy as np import time ``` -为了说明矢量化为什么如此重要,我们考虑对两个向量相加的两种方法。 +为了说明矢量化为什么如此重要,我们考虑(**对向量相加的两种方法**)。 我们实例化两个全1的1000维向量。在一种方法中,我们将使用Python的for循环遍历向量。在另一种方法中,我们将依赖对 `+` 的调用。 ```{.python .input} @@ -151,7 +151,7 @@ a = d2l.ones(n) b = d2l.ones(n) ``` -由于在本书中我们将频繁地进行运行时间的基准测试,所以让我们定义一个计时器。 +由于在本书中我们将频繁地进行运行时间的基准测试,所以让[**我们定义一个计时器**]。 ```{.python .input} #@tab all @@ -185,7 +185,7 @@ class Timer: #@save 现在我们可以对工作负载进行基准测试。 -首先,我们使用for循环,每次执行一位的加法。 +首先,[**我们使用for循环,每次执行一位的加法**]。 ```{.python .input} #@tab mxnet, pytorch @@ -205,7 +205,7 @@ for i in range(n): f'{timer.stop():.5f} sec' ``` -然后,我们使用重载的 `+` 运算符来计算按元素的和。 +(**或者,我们使用重载的 `+` 运算符来计算按元素的和**)。 ```{.python .input} #@tab all @@ -227,7 +227,7 @@ f'{timer.stop():.5f} sec' $$p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right).$$ -下面我们定义一个Python函数来计算正态分布。 +下面[**我们定义一个Python函数来计算正态分布**]。 ```{.python .input} #@tab all @@ -236,7 +236,7 @@ def normal(x, mu, sigma): return p * np.exp(-0.5 / sigma**2 * (x - mu)**2) ``` -我们现在可视化正态分布。 +我们现在(**可视化正态分布**)。 ```{.python .input} #@tab all @@ -343,7 +343,3 @@ $$-\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/1776) :end_tab: - -```{.python .input} - -``` diff --git a/chapter_linear-networks/softmax-regression-concise.md b/chapter_linear-networks/softmax-regression-concise.md index eb1ab2cab..7d82e5f06 100644 --- a/chapter_linear-networks/softmax-regression-concise.md +++ b/chapter_linear-networks/softmax-regression-concise.md @@ -1,7 +1,9 @@ # softmax回归的简洁实现 :label:`sec_softmax_concise` -在 :numref:`sec_linear_concise` 中,我们可以发现通过深度学习框架的高级API能够使实现线性回归变得更加容易。同样地,通过深度学习框架的高级API也能更方便地实现分类模型。让我们继续使用Fashion-MNIST数据集,并保持批量大小为256,就像在 :numref:`sec_softmax_scratch` 中一样。 +在 :numref:`sec_linear_concise` 中,我们可以发现(**通过深度学习框架的高级API能够使实现**) +(~~softmax~~) +线性(**回归变得更加容易**)。同样地,通过深度学习框架的高级API也能更方便地实现分类模型。让我们继续使用Fashion-MNIST数据集,并保持批量大小为256,就像在 :numref:`sec_softmax_scratch` 中一样。 ```{.python .input} from d2l import mxnet as d2l @@ -31,7 +33,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) ## 初始化模型参数 -如我们在 :numref:`sec_softmax` 所述,softmax 回归的输出层是一个全连接层。因此,为了实现我们的模型,我们只需在 `Sequential` 中添加一个带有10个输出的全连接层。同样,在这里,`Sequential` 并不是必要的,但我们可能会形成这种习惯。因为在实现深度模型时,`Sequential`将无处不在。我们仍然以均值0和标准差0.01随机初始化权重。 +如我们在 :numref:`sec_softmax` 所述,[**softmax 回归的输出层是一个全连接层**]。因此,为了实现我们的模型,我们只需在 `Sequential` 中添加一个带有10个输出的全连接层。同样,在这里,`Sequential` 并不是必要的,但我们可能会形成这种习惯。因为在实现深度模型时,`Sequential`将无处不在。我们仍然以均值0和标准差0.01随机初始化权重。 ```{.python .input} net = nn.Sequential() @@ -81,7 +83,7 @@ $$ $$ 我们也希望保留传统的softmax函数,以备我们需要评估通过模型输出的概率。 -但是,我们没有将softmax概率传递到损失函数中,而是在交叉熵损失函数中传递未归一化的预测并同时计算softmax及其对数,这是一件聪明的事情 ["LogSumExp技巧"](https://en.wikipedia.org/wiki/LogSumExp)。 +但是,我们没有将softmax概率传递到损失函数中,而是[**在交叉熵损失函数中,传递未归一化的预测,并同时计算softmax及其对数**],这是一件聪明的事情 ["LogSumExp技巧"](https://en.wikipedia.org/wiki/LogSumExp)。 ```{.python .input} loss = gluon.loss.SoftmaxCrossEntropyLoss() @@ -99,7 +101,7 @@ loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) ## 优化算法 -在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。这与我们在线性回归例子中的相同,这说明了优化器的普适性。 +在这里,我们(**使用学习率为0.1的小批量随机梯度下降作为优化算法**)。这与我们在线性回归例子中的相同,这说明了优化器的普适性。 ```{.python .input} trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1}) @@ -117,7 +119,7 @@ trainer = tf.keras.optimizers.SGD(learning_rate=.1) ## 训练 -接下来我们调用 :numref:`sec_softmax_scratch` 中定义的训练函数来训练模型。 +接下来我们[**调用**] :numref:`sec_softmax_scratch` 中(~~之前~~)(**定义的训练函数来训练模型**)。 ```{.python .input} #@tab all diff --git a/chapter_linear-networks/softmax-regression-scratch.md b/chapter_linear-networks/softmax-regression-scratch.md index 8dc2af95e..b384a16d2 100644 --- a/chapter_linear-networks/softmax-regression-scratch.md +++ b/chapter_linear-networks/softmax-regression-scratch.md @@ -1,7 +1,7 @@ # softmax回归的从零开始实现 :label:`sec_softmax_scratch` -就像我们从零开始实现线性回归一样,我们认为softmax回归也是重要的基础,因此你应该知道如何自己实现它的细节。我们使用刚刚在 :numref:`sec_fashion_mnist` 中引入的Fashion-MNIST数据集,并设置数据迭代器的批量大小为256。 +(**就像我们从零开始实现线性回归一样,**)我们认为softmax回归也是重要的基础,因此(**你应该知道实现softmax的细节**)。我们使用刚刚在 :numref:`sec_fashion_mnist` 中引入的Fashion-MNIST数据集,并设置数据迭代器的批量大小为256。 ```{.python .input} from d2l import mxnet as d2l @@ -32,9 +32,9 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) ## 初始化模型参数 -这里的每个样本都用固定长度向量表示。原始数据集中的每个样本都是 $28 \times 28$ 的图像。在本节中,我们将展平每个图像,将它们视为长度为784的向量。在以后的章节中,我们将讨论能够利用图像空间结构的复杂策略,但现在我仅将每个像素位置视为一个特征。 +这里的每个样本都用固定长度向量表示。原始数据集中的每个样本都是 $28 \times 28$ 的图像。在本节中,我们[**将展平每个图像,将它们视为长度为784的向量。**]在以后的章节中,我们将讨论能够利用图像空间结构的复杂策略,但现在我仅将每个像素位置视为一个特征。 -回想一下,在softmax回归中,我们的输出与类别一样多。因为我们的数据集有10个类别,所以网络输出维度为 10。因此,权重将构成一个 $784 \times 10$ 的矩阵,偏置将构成一个 $1 \times 10$ 的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重 `W`,偏置初始化为0。 +回想一下,在softmax回归中,我们的输出与类别一样多。(**因为我们的数据集有10个类别,所以网络输出维度为 10**)。因此,权重将构成一个 $784 \times 10$ 的矩阵,偏置将构成一个 $1 \times 10$ 的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重 `W`,偏置初始化为0。 ```{.python .input} num_inputs = 784 @@ -67,7 +67,7 @@ b = tf.Variable(tf.zeros(num_outputs)) ## 定义softmax操作 -在实现softmax回归模型之前,让我们简要地回顾一下`sum`运算符如何沿着张量中的特定维度工作,如 :numref:`subseq_lin-alg-reduction` 和 :numref:`subseq_lin-alg-non-reduction` 所述。给定一个矩阵`X`,我们可以对所有元素求和(默认情况下),也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。如果 `X` 是一个形状为 `(2, 3)` 的张量,我们对列进行求和,则结果将是一个具有形状 `(3,)` 的向量。当调用sum运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。这将产生一个具有形状 `(1, 3)` 的二维张量。 +在实现softmax回归模型之前,让我们简要地回顾一下`sum`运算符如何沿着张量中的特定维度工作,如 :numref:`subseq_lin-alg-reduction` 和 :numref:`subseq_lin-alg-non-reduction` 所述。[**给定一个矩阵`X`,我们可以对所有元素求和**](默认情况下),也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。如果 `X` 是一个形状为 `(2, 3)` 的张量,我们对列进行求和,则结果将是一个具有形状 `(3,)` 的向量。当调用sum运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。这将产生一个具有形状 `(1, 3)` 的二维张量。 ```{.python .input} #@tab pytorch @@ -81,15 +81,17 @@ X = d2l.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) d2l.reduce_sum(X, 0, keepdims=True), d2l.reduce_sum(X, 1, keepdims=True) ``` -我们现在已经准备好实现softmax操作了。回想一下,softmax 由三个步骤组成: +我们现在已经准备好[**实现softmax**]操作了。回想一下,softmax 由三个步骤组成: (1)对每个项求幂(使用`exp`); (2)对每一行求和(小批量中每个样本是一行),得到每个样本的归一化常数; (3)将每一行除以其归一化常数,确保结果的和为1。 在查看代码之前,让我们回顾一下这个表达式: +(** $$ \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. $$ +**) 分母或归一化常数,有时也称为*配分函数*(其对数称为对数-配分函数)。该名称的起源来自 [统计物理学](https://en.wikipedia.org/wiki/Partition_function_(statistical_mechanics))中一个模拟粒子群分布的方程。 @@ -109,7 +111,7 @@ def softmax(X): return X_exp / partition # 这里应用了广播机制 ``` -正如你所看到的,对于任何随机输入,我们将每个元素变成一个非负数。此外,因为概率的要求,每行总和为1。 +正如你所看到的,对于任何随机输入,[**我们将每个元素变成一个非负数。此外,依据概率原理,每行总和为1**]。 ```{.python .input} #@tab mxnet, pytorch @@ -129,7 +131,7 @@ X_prob, tf.reduce_sum(X_prob, 1) ## 定义模型 -现在我们已经定义了softmax操作,我们可以实现softmax回归模型。下面的代码定义了输入如何通过网络映射到输出。注意,在将数据传递到我们的模型之前,我们使用 `reshape` 函数将每张原始图像展平为向量。 +现在我们已经定义了softmax操作,我们可以[**实现softmax回归模型**]。下面的代码定义了输入如何通过网络映射到输出。注意,在将数据传递到我们的模型之前,我们使用 `reshape` 函数将每张原始图像展平为向量。 ```{.python .input} #@tab all @@ -142,9 +144,9 @@ def net(X): 接下来,我们需要实现 :numref:`sec_softmax` 中引入的交叉熵损失函数。这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题。 回顾一下,交叉熵采用真实标签的预测概率的负对数似然。我们不需要使用Python的for循环迭代预测(这往往是低效的)。我们可以通过一个运算符选择所有元素。 -下面,我们一个演示数据,其中包含2个样本在3个类别的预测概率`y_hat`。以及它们对应的标签`y`。 +下面,我们[**创建一个数据`y_hat`,其中包含2个样本在3个类别的预测概率,**]它们对应的标签`y`。 有了`y`,我们知道在第一个样本中,第一类是正确的预测,而在第二个样本中,第三类是正确的预测。 -然后使用`y`作为`y_hat`中概率的索引,我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。 +然后(**使用`y`作为`y_hat`中概率的索引**),我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。 ```{.python .input} #@tab mxnet, pytorch @@ -160,7 +162,7 @@ y = tf.constant([0, 2]) tf.boolean_mask(y_hat, tf.one_hot(y, depth=y_hat.shape[-1])) ``` -现在我们只需一行代码就可以实现交叉熵损失函数。 +现在我们只需一行代码就可以[**实现交叉熵损失函数**]。 ```{.python .input} #@tab mxnet, pytorch @@ -185,14 +187,14 @@ cross_entropy(y_hat, y) 当预测与标签分类 `y` 一致时,它们是正确的。分类准确率即正确预测数量与总预测数量之比。虽然直接优化准确率可能很困难(因为准确率的计算不可导),但准确率通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总是会报告它。 -为了计算准确率,我们执行以下操作。首先,如果 `y_hat` 是矩阵,第二个维度存储每个类的预测分数。我们使用 `argmax` 获得每行中最大元素的索引来获得预测类别。然后我们将预测类别与真实 `y` 元素进行比较。由于等式运算符 `==` 对数据类型很敏感,因此我们将 `y_hat` 的数据类型转换为与 `y` 的数据类型一致。结果是一个包含 0(错)和 1(对)的张量。进行求和会得到正确预测的数量。 +为了计算准确率,我们执行以下操作。首先,如果 `y_hat` 是矩阵,第二个维度存储每个类的预测分数。我们使用 `argmax` 获得每行中最大元素的索引来获得预测类别。然后我们[**将预测类别与真实 `y` 元素进行比较**]。由于等式运算符 `==` 对数据类型很敏感,因此我们将 `y_hat` 的数据类型转换为与 `y` 的数据类型一致。结果是一个包含 0(错)和 1(对)的张量。进行求和会得到正确预测的数量。 ```{.python .input} #@tab all def accuracy(y_hat, y): #@save """计算预测正确的数量。""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: - y_hat = d2l.argmax(y_hat, axis=1) + y_hat = d2l.argmax(y_hat, axis=1) cmp = d2l.astype(y_hat, y.dtype) == y return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) ``` @@ -204,7 +206,7 @@ def accuracy(y_hat, y): #@save accuracy(y_hat, y) / len(y) ``` -同样,我们可以评估数据迭代器 `data_iter` 访问的数据集在任意模型 `net` 上的准确率。 +同样,对于任意数据迭代器 `data_iter` 可访问的数据集,[**我们可以评估在任意模型 `net` 的准确率**]。 ```{.python .input} #@tab mxnet, tensorflow @@ -229,7 +231,7 @@ def evaluate_accuracy(net, data_iter): #@save ``` 这里 `Accumulator` 是一个实用程序类,用于对多个变量进行累加。 -在上面的 `evaluate_accuracy` 函数中,我们在 `Accumulator` 实例中创建了 2 个变量,用于分别存储正确预测的数量和预测的总数量。当我们遍历数据集时,两者都将随着时间的推移而累加。 +在上面的 `evaluate_accuracy` 函数中,我们在 (**`Accumulator` 实例中创建了 2 个变量,用于分别存储正确预测的数量和预测的总数量**)。当我们遍历数据集时,两者都将随着时间的推移而累加。 ```{.python .input} #@tab all @@ -257,7 +259,7 @@ evaluate_accuracy(net, test_iter) ## 训练 -如果你看过 :numref:`sec_linear_scratch` 中的线性回归实现,softmax回归的训练过程代码应该看起来非常熟悉。在这里,我们重构训练过程的实现以使其可重复使用。首先,我们定义一个函数来训练一个迭代周期。请注意,`updater` 是更新模型参数的常用函数,它接受批量大小作为参数。它可以是封装的`d2l.sgd`函数,也可以是框架的内置优化函数。 +如果你看过 :numref:`sec_linear_scratch` 中的线性回归实现,[**softmax回归的训练**]过程代码应该看起来非常熟悉。在这里,我们重构训练过程的实现以使其可重复使用。首先,我们定义一个函数来训练一个迭代周期。请注意,`updater` 是更新模型参数的常用函数,它接受批量大小作为参数。它可以是封装的`d2l.sgd`函数,也可以是框架的内置优化函数。 ```{.python .input} def train_epoch_ch3(net, train_iter, loss, updater): #@save @@ -337,7 +339,7 @@ def train_epoch_ch3(net, train_iter, loss, updater): #@save return metric[0] / metric[2], metric[1] / metric[2] ``` -在展示训练函数的实现之前,我们定义了一个在动画中绘制数据的实用程序类。它能够简化本书其余部分的代码。 +在展示训练函数的实现之前,我们[**定义一个在动画中绘制数据的实用程序类**]。它能够简化本书其余部分的代码。 ```{.python .input} #@tab all @@ -382,7 +384,7 @@ class Animator: #@save display.clear_output(wait=True) ``` -接下来我们实现一个训练函数,它会在`train_iter` 访问到的训练数据集上训练一个模型`net`。该训练函数将会运行多个迭代周期(由`num_epochs`指定)。在每个迭代周期结束时,利用 `test_iter` 访问到的测试数据集对模型进行评估。我们将利用 `Animator` 类来可视化训练进度。 +接下来我们实现一个[**训练函数**],它会在`train_iter` 访问到的训练数据集上训练一个模型`net`。该训练函数将会运行多个迭代周期(由`num_epochs`指定)。在每个迭代周期结束时,利用 `test_iter` 访问到的测试数据集对模型进行评估。我们将利用 `Animator` 类来可视化训练进度。 ```{.python .input} #@tab all @@ -400,7 +402,7 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save assert test_acc <= 1 and test_acc > 0.7, test_acc ``` -作为一个从零开始的实现,我们使用 :numref:`sec_linear_scratch` 中定义的小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。 +作为一个从零开始的实现,我们使用 :numref:`sec_linear_scratch` 中定义的[**小批量随机梯度下降来优化模型的损失函数**],设置学习率为0.1。 ```{.python .input} #@tab mxnet, pytorch @@ -424,7 +426,7 @@ class Updater(): #@save updater = Updater([W, b], lr=0.1) ``` -现在,我们训练模型10个迭代周期。请注意,迭代周期(`num_epochs`)和学习率(`lr`)都是可调节的超参数。通过更改它们的值,我们可以提高模型的分类准确率。 +现在,我们[**训练模型10个迭代周期**]。请注意,迭代周期(`num_epochs`)和学习率(`lr`)都是可调节的超参数。通过更改它们的值,我们可以提高模型的分类准确率。 ```{.python .input} #@tab all @@ -434,7 +436,7 @@ train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater) ## 预测 -现在训练已经完成,我们的模型已经准备好对图像进行分类。给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。 +现在训练已经完成,我们的模型已经准备好[**对图像进行分类预测**]。给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。 ```{.python .input} #@tab all From e71b41facb7494c9c2201c204613b3028ebeebbf Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 19 Mar 2021 11:40:24 -0700 Subject: [PATCH 003/103] Update softmax-regression-concise.md --- chapter_linear-networks/softmax-regression-concise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_linear-networks/softmax-regression-concise.md b/chapter_linear-networks/softmax-regression-concise.md index 7d82e5f06..a871ed3f8 100644 --- a/chapter_linear-networks/softmax-regression-concise.md +++ b/chapter_linear-networks/softmax-regression-concise.md @@ -83,7 +83,7 @@ $$ $$ 我们也希望保留传统的softmax函数,以备我们需要评估通过模型输出的概率。 -但是,我们没有将softmax概率传递到损失函数中,而是[**在交叉熵损失函数中,传递未归一化的预测,并同时计算softmax及其对数**],这是一件聪明的事情 ["LogSumExp技巧"](https://en.wikipedia.org/wiki/LogSumExp)。 +但是,我们没有将softmax概率传递到损失函数中,而是[**在交叉熵损失函数中传递未归一化的预测,并同时计算softmax及其对数**],这是一件聪明的事情 ["LogSumExp技巧"](https://en.wikipedia.org/wiki/LogSumExp)。 ```{.python .input} loss = gluon.loss.SoftmaxCrossEntropyLoss() From 261fc601756bd7237269717455feb9f501748003 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Fri, 19 Mar 2021 11:45:57 -0700 Subject: [PATCH 004/103] rebase (#690) --- chapter_multilayer-perceptrons/dropout.md | 15 +++++------- chapter_multilayer-perceptrons/mlp-concise.md | 6 ++--- chapter_multilayer-perceptrons/mlp-scratch.md | 10 ++++---- chapter_multilayer-perceptrons/mlp.md | 23 +++++++++++-------- .../numerical-stability-and-init.md | 4 ++-- .../underfit-overfit.md | 22 +++++++++--------- .../weight-decay.md | 22 +++++++++--------- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/chapter_multilayer-perceptrons/dropout.md b/chapter_multilayer-perceptrons/dropout.md index 35d20e299..db971ca14 100644 --- a/chapter_multilayer-perceptrons/dropout.md +++ b/chapter_multilayer-perceptrons/dropout.md @@ -59,7 +59,7 @@ $$ 要实现单层的dropout函数,我们必须从伯努利(二元)随机变量中提取与我们的层的维度一样多的样本,其中随机变量以概率$1-p$取值$1$(保持),以概率$p$取值$0$(丢弃)。实现这一点的一种简单方式是首先从均匀分布$U[0, 1]$中抽取样本。那么我们可以保留那些对应样本大于$p$的节点,把剩下的丢弃。 -在下面的代码中,我们实现了一个`dropout_layer`函数,该函数以`dropout`的概率丢弃张量输入`X`中的元素,如上所述重新缩放剩余部分:将剩余部分除以`1.0-dropout`。 +在下面的代码中,(**我们实现 `dropout_layer` 函数,该函数以`dropout`的概率丢弃张量输入`X`中的元素**),如上所述重新缩放剩余部分:将剩余部分除以`1.0-dropout`。 ```{.python .input} from d2l import mxnet as d2l @@ -115,7 +115,7 @@ def dropout_layer(X, dropout): return tf.cast(mask, dtype=tf.float32) * X / (1.0 - dropout) ``` -我们可以通过几个例子来测试`dropout_layer`函数。在下面的代码行中,我们将输入`X`通过dropout操作,丢弃概率分别为0、0.5和1。 +我们可以通过几个例子来[**测试`dropout_layer`函数**]。在下面的代码行中,我们将输入`X`通过dropout操作,丢弃概率分别为0、0.5和1。 ```{.python .input} X = np.arange(16).reshape(2, 8) @@ -144,7 +144,7 @@ print(dropout_layer(X, 1.)) ### 定义模型参数 -同样,我们使用 :numref:`sec_fashion_mnist` 中引入的Fashion-MNIST数据集。我们定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元。 +同样,我们使用 :numref:`sec_fashion_mnist` 中引入的Fashion-MNIST数据集。我们[**定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元**]。 ```{.python .input} num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256 @@ -202,14 +202,11 @@ class Net(nn.Module): def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training = True): super(Net, self).__init__() - self.num_inputs = num_inputs self.training = is_training - self.lin1 = nn.Linear(num_inputs, num_hiddens1) self.lin2 = nn.Linear(num_hiddens1, num_hiddens2) self.lin3 = nn.Linear(num_hiddens2, num_outputs) - self.relu = nn.ReLU() def forward(self, X): @@ -258,7 +255,7 @@ class Net(tf.keras.Model): net = Net(num_outputs, num_hiddens1, num_hiddens2) ``` -### 训练和测试 +### [**训练和测试**] 这类似于前面描述的多层感知机训练和测试。 @@ -288,7 +285,7 @@ trainer = tf.keras.optimizers.SGD(learning_rate=lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) ``` -## 简洁实现 +## [**简洁实现**] 对于高级API,我们所需要做的就是在每个全连接层之后添加一个`Dropout`层,将丢弃概率作为唯一的参数传递给它的构造函数。在训练过程中,`Dropout`层将根据指定的丢弃概率随机丢弃上一层的输出(相当于下一层的输入)。当不处于训练模式时,`Dropout`层仅在测试时传递数据。 @@ -338,7 +335,7 @@ net = tf.keras.models.Sequential([ ]) ``` -接下来,我们对模型进行训练和测试。 +接下来,我们[**对模型进行训练和测试**]。 ```{.python .input} trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) diff --git a/chapter_multilayer-perceptrons/mlp-concise.md b/chapter_multilayer-perceptrons/mlp-concise.md index 9612ee208..b595613d9 100644 --- a/chapter_multilayer-perceptrons/mlp-concise.md +++ b/chapter_multilayer-perceptrons/mlp-concise.md @@ -1,7 +1,7 @@ # 多层感知机的简洁实现 :label:`sec_mlp_concise` -正如你所期待的,我们可以通过高级API更简洁地实现多层感知机。 +正如你所期待的,我们可以(**通过高级API更简洁地实现多层感知机**)。 ```{.python .input} from d2l import mxnet as d2l @@ -25,7 +25,7 @@ import tensorflow as tf ## 模型 -与softmax回归的简洁实现( :numref:`sec_softmax_concise`)相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是隐藏层,它包含256个隐藏单元并使用了ReLU激活函数。第二层是输出层。 +与softmax回归的简洁实现( :numref:`sec_softmax_concise`)相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是[**隐藏层**],它(**包含256个隐藏单元并使用了ReLU激活函数**)。第二层是输出层。 ```{.python .input} net = nn.Sequential() @@ -56,7 +56,7 @@ net = tf.keras.models.Sequential([ tf.keras.layers.Dense(10)]) ``` -训练过程实现与我们实现softmax回归时完全相同。这种模块化设计使我们能够将与和模型架构有关的内容独立出来。 +[**训练过程**]实现与我们实现softmax回归时完全相同。这种模块化设计使我们能够将与和模型架构有关的内容独立出来。 ```{.python .input} batch_size, lr, num_epochs = 256, 0.1, 10 diff --git a/chapter_multilayer-perceptrons/mlp-scratch.md b/chapter_multilayer-perceptrons/mlp-scratch.md index b9de1a7c8..23fcee037 100644 --- a/chapter_multilayer-perceptrons/mlp-scratch.md +++ b/chapter_multilayer-perceptrons/mlp-scratch.md @@ -30,7 +30,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) ## 初始化模型参数 -回想一下,Fashion-MNIST中的每个图像由$28 \times 28 = 784$个灰度像素值组成。所有图像共分为10个类别。忽略像素之间的空间结构,我们可以将每个图像视为具有784个输入特征和10个类的简单分类数据集。首先,我们将实现一个具有1个隐藏层的多层感知机,其中包含256个隐藏单元。注意,我们可以将这两个量都视为超参数。通常,我们选择2的幂次方作为层的宽度。因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。 +回想一下,Fashion-MNIST中的每个图像由$28 \times 28 = 784$个灰度像素值组成。所有图像共分为10个类别。忽略像素之间的空间结构,我们可以将每个图像视为具有784个输入特征和10个类的简单分类数据集。首先,我们将[**实现一个具有单隐藏层的多层感知机,它包含256个隐藏单元**]。注意,我们可以将这两个量都视为超参数。通常,我们选择2的幂次方作为层的宽度。因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。 我们用几个张量来表示我们的参数。注意,对于每一层我们都要记录一个权重矩阵和一个偏置向量。跟以前一样,我们要为这些参数的损失的梯度分配内存。 @@ -77,7 +77,7 @@ params = [W1, b1, W2, b2] ## 激活函数 -为了确保我们知道一切是如何工作的,我们将使用最大值函数自己实现ReLU激活函数,而不是直接调用内置的`relu`函数。 +为了确保我们知道一切是如何工作的,我们将使用最大值函数自己[**实现ReLU激活函数**],而不是直接调用内置的`relu`函数。 ```{.python .input} def relu(X): @@ -99,7 +99,7 @@ def relu(X): ## 模型 -因为我们忽略了空间结构,所以我们使用`reshape`将每个二维图像转换为一个长度为`num_inputs`的向量。我们只需几行代码就可以实现我们的模型。 +因为我们忽略了空间结构,所以我们使用`reshape`将每个二维图像转换为一个长度为`num_inputs`的向量。我们只需几行代码就可以(**实现我们的模型**)。 ```{.python .input} def net(X): @@ -146,7 +146,7 @@ def loss(y_hat, y): ## 训练 -幸运的是,多层感知机的训练过程实现与softmax回归的训练过程实现完全相同。可以直接调用`d2l`包的`train_ch3`函数(参见 :numref:`sec_softmax_scratch` ),将迭代周期数设置为10,并将学习率设置为0.1. +幸运的是,[**多层感知机的训练过程与softmax回归的训练过程完全相同**]。可以直接调用`d2l`包的`train_ch3`函数(参见 :numref:`sec_softmax_scratch` ),将迭代周期数设置为10,并将学习率设置为0.1. ```{.python .input} num_epochs, lr = 10, 0.1 @@ -168,7 +168,7 @@ updater = d2l.Updater([W1, W2, b1, b2], lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater) ``` -为了对学习到的模型进行评估,我们将在一些测试数据上应用这个模型。 +为了对学习到的模型进行评估,我们将[**在一些测试数据上应用这个模型**]。 ```{.python .input} #@tab all diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index a1ca2804c..e609ca471 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -69,6 +69,11 @@ $$ 而且,虽然一个单隐层网络能学习任何函数,但并不意味着应该尝试使用单隐藏层网络来解决所有问题。事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。我们将在后面的章节中进行更细致的讨论。 +## 激活函数 +:label:`subsec:activation-functions` + +激活函数通过计算加权和并加上偏置来确定神经元是否应该被激活。它们是将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。由于激活函数是深度学习的基础,下面(**简要介绍一些常见的激活函数**)。 + ```{.python .input} %matplotlib inline from d2l import mxnet as d2l @@ -90,15 +95,13 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` -## 激活函数 - -激活函数通过计算加权和并加上偏置来确定神经元是否应该被激活。它们是将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。由于激活函数是深度学习的基础,下面简要介绍一些常见的激活函数。 - ### ReLU函数 -最受欢迎的选择是*线性整流单元*(Rectified linear unit,*ReLU*),因为它实现简单,同时在各种预测任务中表现良好。ReLU提供了一种非常简单的非线性变换。给定元素$x$,ReLU函数被定义为该元素与$0$的最大值: +最受欢迎的选择是*线性整流单元*(Rectified linear unit,*ReLU*),因为它实现简单,同时在各种预测任务中表现良好。 +[**ReLU提供了一种非常简单的非线性变换**]。 +给定元素$x$,ReLU函数被定义为该元素与$0$的最大值: -$$\operatorname{ReLU}(x) = \max(x, 0).$$ +(**$$\operatorname{ReLU}(x) = \max(x, 0).$$**) 通俗地说,ReLU函数通过将相应的激活值设为0来仅保留正元素并丢弃所有负元素。为了直观感受一下,我们可以画出函数的曲线图。正如从图中所看到,激活函数是分段线性的。 @@ -153,9 +156,9 @@ $$\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).$$ ### sigmoid函数 -*sigmoid函数*将定义域在$\mathbb{R}$中的输入变换为区间(0, 1)上的输出。因此,sigmoid通常称为*挤压函数*(Squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值: +对于一个定义域在$\mathbb{R}$中的输入,[** *sigmoid函数*将输入变换为区间(0, 1)上的输出**]。因此,sigmoid通常称为*挤压函数*(Squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值: -$$\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.$$ +(**$$\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.$$**) 在最早的神经网络中,科学家们感兴趣的是对“激发”或“不激发”的生物神经元进行建模。因此,这一领域的先驱,如人工神经元的发明者麦卡洛克和皮茨。从他们开始就专注于阈值单元。阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。 @@ -210,9 +213,9 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of sigmoid', ### tanh函数 -与sigmoid函数类似,tanh(双曲正切)函数也能将其输入压缩转换为区间(-1, 1)上。tanh函数的公式如下: +与sigmoid函数类似,[**tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上**]。tanh函数的公式如下: -$$\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}.$$ +(**$$\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}.$$**) 下面我们绘制tanh函数。注意,当输入在0附近时,tanh函数接近线性变换。函数的形状类似于sigmoid函数,不同的是tanh函数关于坐标系原点中心对称。 diff --git a/chapter_multilayer-perceptrons/numerical-stability-and-init.md b/chapter_multilayer-perceptrons/numerical-stability-and-init.md index 711921d34..3f53f72dc 100644 --- a/chapter_multilayer-perceptrons/numerical-stability-and-init.md +++ b/chapter_multilayer-perceptrons/numerical-stability-and-init.md @@ -19,7 +19,7 @@ $$\partial_{\mathbf{W}^{(l)}} \mathbf{o} = \underbrace{\partial_{\mathbf{h}^{(L- 要么是 *梯度爆炸*(gradient exploding)问题:参数更新过大,破坏了模型的稳定收敛; 要么是 *梯度消失*(gradient vanishing)问题:参数更新过小,在每次更新时几乎不会移动,导致无法学习。 -### 梯度消失 +### (**梯度消失**) 导致梯度消失问题的一个常见的原因是跟在每层的线性运算之后的激活函数$\sigma$。从历史上看,sigmoid函数$1/(1 + \exp(-x))$( :numref:`sec_mlp` 提到过)很流行,因为它类似于阈值函数。由于早期的人工神经网络受到生物神经网络的启发,神经元要么完全激活要么完全不激活(就像生物神经元)的想法很有吸引力。让我们仔细看看sigmoid函数为什么会导致梯度消失。 @@ -67,7 +67,7 @@ d2l.plot(x.numpy(), [y.numpy(), t.gradient(y, x).numpy()], 正如你所看到的,当它的输入很大或是很小时,sigmoid函数的梯度都会消失。此外,当反向传播通过许多层时,除非我们在刚刚好的地方,这些地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。当我们的网络有很多层时,除非我们很小心,否则在某一层可能会切断梯度。事实上,这个问题曾经困扰着深度网络的训练。因此,更稳定(但在神经科学的角度看起来不太合理)的ReLU系列函数已经成为从业者的默认选择。 -### 梯度爆炸 +### [**梯度爆炸**] 相反的问题,当梯度爆炸时,可能同样令人烦恼。为了更好地说明这一点,我们生成100个高斯随机矩阵,并将它们与某个初始矩阵相乘。对于我们选择的尺度(方差$\sigma^2=1$),矩阵乘积发生爆炸。当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛。 diff --git a/chapter_multilayer-perceptrons/underfit-overfit.md b/chapter_multilayer-perceptrons/underfit-overfit.md index 0107906b9..7f75ec5f5 100644 --- a/chapter_multilayer-perceptrons/underfit-overfit.md +++ b/chapter_multilayer-perceptrons/underfit-overfit.md @@ -101,7 +101,7 @@ $$\hat{y}= \sum_{i=0}^d x^i w_i$$ ## 多项式回归 -我们现在可以通过将对数据进行多项式拟合来交互地探索这些概念。 +我们现在可以(**通过多项式拟合来交互地探索这些概念**)。 ```{.python .input} from d2l import mxnet as d2l @@ -130,10 +130,10 @@ import math ### 生成数据集 -首先,我们需要数据。给定$x$,我们将使用以下三阶多项式来生成训练和测试数据的标签: +首先,我们需要数据。给定$x$,我们将[**使用以下三阶多项式来生成训练和测试数据的标签:**] -$$y = 5 + 1.2x - 3.4\frac{x^2}{2!} + 5.6 \frac{x^3}{3!} + \epsilon \text{ where } -\epsilon \sim \mathcal{N}(0, 0.1^2).$$ +(**$$y = 5 + 1.2x - 3.4\frac{x^2}{2!} + 5.6 \frac{x^3}{3!} + \epsilon \text{ where } +\epsilon \sim \mathcal{N}(0, 0.1^2).$$**) 噪声项$\epsilon$服从均值为0且标准差为0.1的正态分布。在优化的过程中,我们通常希望避免非常大的梯度值或损失值。这就是我们将*特征*从$x^i$调整为$\frac{x^i}{i!}$的原因,这样可以避免对于很大的$i$得到特别大的指数值。我们将为训练集和测试集各合成100个样本。 @@ -154,7 +154,7 @@ labels = np.dot(poly_features, true_w) labels += np.random.normal(scale=0.1, size=labels.shape) ``` -同样,存储在`poly_features`中的单项式由gamma函数重新缩放,其中$\Gamma(n)=(n-1)!$。从生成的数据集中查看前2个样本。值1是与偏置相对应的常量特征。 +同样,存储在`poly_features`中的单项式由gamma函数重新缩放,其中$\Gamma(n)=(n-1)!$。从生成的数据集中查[**看一下前2个样本**]。值1是与偏置相对应的常量特征。 ```{.python .input} #@tab pytorch, tensorflow @@ -170,7 +170,7 @@ features[:2], poly_features[:2, :], labels[:2] ### 对模型进行训练和测试 -首先让我们实现一个函数来评估模型在给定数据集上的损失。 +首先让我们[**实现一个函数来评估模型在给定数据集上的损失**]。 ```{.python .input} #@tab mxnet, tensorflow @@ -196,7 +196,7 @@ def evaluate_loss(net, data_iter, loss): #@save return metric[0] / metric[1] ``` -现在定义训练函数。 +现在[**定义训练函数**]。 ```{.python .input} def train(train_features, test_features, train_labels, test_labels, @@ -273,7 +273,7 @@ def train(train_features, test_features, train_labels, test_labels, print('weight:', net.get_weights()[0].T) ``` -### 三阶多项式函数拟合(正态) +### [**三阶多项式函数拟合(正态)**] 我们将首先使用三阶多项式函数,它与数据生成函数的阶数相同。结果表明,该模型能有效降低训练损失和测试损失。学习到的模型参数也接近真实值$w = [5, 1.2, -3.4, 5.6]$。 @@ -284,7 +284,7 @@ train(poly_features[:n_train, :4], poly_features[n_train:, :4], labels[:n_train], labels[n_train:]) ``` -### 线性函数拟合(欠拟合) +### [**线性函数拟合(欠拟合)**] 让我们再看看线性函数拟合。在经历了早期的下降之后,进一步减少该模型的训练损失变得困难。在最后一个迭代周期完成后,训练损失仍然很高。当用来拟合非线性模式(如这里的三阶多项式函数)时,线性模型容易欠拟合。 @@ -295,13 +295,13 @@ train(poly_features[:n_train, :2], poly_features[n_train:, :2], labels[:n_train], labels[n_train:]) ``` -### 高阶多项式函数拟合(过拟合) +### [**高阶多项式函数拟合(过拟合)**] 现在,让我们尝试使用一个过于高阶的多项式来训练模型。在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。虽然训练损失可以有效地降低,但测试损失仍然很高。结果表明,复杂模型对数据造成了过拟合。 ```{.python .input} #@tab all -# Pick all the dimensions from the polynomial features +# 从多项式特征中选取所有维度 train(poly_features[:n_train, :], poly_features[n_train:, :], labels[:n_train], labels[n_train:], num_epochs=1500) ``` diff --git a/chapter_multilayer-perceptrons/weight-decay.md b/chapter_multilayer-perceptrons/weight-decay.md index 8e7a11c75..e158b568b 100644 --- a/chapter_multilayer-perceptrons/weight-decay.md +++ b/chapter_multilayer-perceptrons/weight-decay.md @@ -10,7 +10,7 @@ ## 范数与权重衰减 在之前的章节,我们已经描述了$L_2$范数和$L_1$范数,它们是$L_p$范数的特殊情况。 -*权重衰减*(通常称为$L_2$正则化),可能是最广泛使用的对参数化机器学习模型进行正则化的技术。这项技术是基于一个基本直觉,即在所有函数$f$中,函数$f = 0$(所有输入都得到值$0$)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,整个数学分支,包括函数分析和巴拿赫空间理论,都在致力于回答这个问题。 +(***权重衰减*(通常称为$L_2$正则化),可能是最广泛使用的对参数化机器学习模型进行正则化的技术**)。这项技术是基于一个基本直觉,即在所有函数$f$中,函数$f = 0$(所有输入都得到值$0$)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,整个数学分支,包括函数分析和巴拿赫空间理论,都在致力于回答这个问题。 一种简单的方法是通过线性函数$f(\mathbf{x}) = \mathbf{w}^\top \mathbf{x}$中的权重向量的某个范数来度量其复杂性,例如$\| \mathbf{w} \|^2$。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中。将原来的训练目标*最小化训练标签上的预测损失*,调整为*最小化预测损失和惩罚项之和*。 现在,如果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数$\| \mathbf{w} \|^2$。这正是我们想要的。让我们回顾一下 :numref:`sec_linear_regression` 中的线性回归例子。我们的损失由下式给出: @@ -66,10 +66,10 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` -首先,我们像以前一样生成一些数据,生成公式如下: +首先,我们[**像以前一样生成一些数据,生成公式如下:**] -$$y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where } -\epsilon \sim \mathcal{N}(0, 0.01^2).$$ +(**$$y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where } +\epsilon \sim \mathcal{N}(0, 0.01^2).$$**) 我们选择标签是关于输入的线性函数。标签同时被均值为0,标准差为0.01高斯噪声破坏。为了使过拟合的效果更加明显,我们可以将问题的维数增加到$d = 200$,并使用一个只包含20个样本的小训练集。 @@ -87,7 +87,7 @@ test_iter = d2l.load_array(test_data, batch_size, is_train=False) 在下面,我们将从头开始实现权重衰减,只需将$L_2$的平方惩罚添加到原始目标函数中。 -### 初始化模型参数 +### [**初始化模型参数**] 首先,我们将定义一个函数来随机初始化我们的模型参数。 @@ -116,7 +116,7 @@ def init_params(): return [w, b] ``` -### 定义$L_2$范数惩罚 +### (**定义$L_2$范数惩罚**) 实现这一惩罚最方便的方法是对所有项求平方后并将它们求和。 @@ -137,7 +137,7 @@ def l2_penalty(w): return tf.reduce_sum(tf.pow(w, 2)) / 2 ``` -### 定义训练代码实现 +### [**定义训练代码实现**] 下面的代码将模型拟合训练数据集,并在测试数据集上进行评估。从 :numref:`chap_linear` 以来,线性网络和平方损失没有变化,所以我们通过`d2l.linreg`和`d2l.squared_loss`导入它们。唯一的变化是损失现在包括了惩罚项。 @@ -203,7 +203,7 @@ def train(lambd): print('w的L2范数是:', tf.norm(w).numpy()) ``` -### 不使用正则化进行训练 +### [**忽略正则化直接训练**] 我们现在用`lambd = 0`禁用权重衰减后运行这个代码。注意,这里训练误差有了减少,但测试误差没有减少。这意味着出现了严重的过拟合。这是过拟合的一个典型例子。 @@ -212,7 +212,7 @@ def train(lambd): train(lambd=0) ``` -### 使用权重衰减 +### [**使用权重衰减**] 下面,我们使用权重衰减来运行代码。注意,在这里训练误差增大,但测试误差减小。这正是我们期望从正则化中得到的效果。 @@ -221,7 +221,7 @@ train(lambd=0) train(lambd=3) ``` -## 简洁实现 +## [**简洁实现**] 由于权重衰减在神经网络优化中很常用,深度学习框架为了便于使用权重衰减,便将权重衰减集成到优化算法中,以便与任何损失函数结合使用。此外,这种集成还有计算上的好处,允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。由于更新的权重衰减部分仅依赖于每个参数的当前值,因此优化器必须至少接触每个参数一次。 @@ -315,7 +315,7 @@ def train_concise(wd): print('w的L2范数:', tf.norm(net.get_weights()[0]).numpy()) ``` -这些图看起来和我们从零开始实现权重衰减时的图相同。然而,它们运行得更快,更容易实现,对于更复杂的问题,这一好处将变得更加明显。 +[**这些图看起来和我们从零开始实现权重衰减时的图相同**]。然而,它们运行得更快,更容易实现,对于更复杂的问题,这一好处将变得更加明显。 ```{.python .input} #@tab all From 8736e46a465c18e0de8c12484ddc8fd0a29cf126 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 19 Mar 2021 11:47:10 -0700 Subject: [PATCH 005/103] Update mlp.md --- chapter_multilayer-perceptrons/mlp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index e609ca471..b2c273f19 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -156,7 +156,7 @@ $$\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).$$ ### sigmoid函数 -对于一个定义域在$\mathbb{R}$中的输入,[** *sigmoid函数*将输入变换为区间(0, 1)上的输出**]。因此,sigmoid通常称为*挤压函数*(Squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值: +对于一个定义域在$\mathbb{R}$中的输入,[** *sigmoid函数*将输入变换为区间(0, 1)上的输出**]。因此,sigmoid通常称为*挤压函数*(squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值: (**$$\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.$$**) From ca58c676f5e09b63d498c64bd3c8e19c2a1f8cc2 Mon Sep 17 00:00:00 2001 From: Mu Li Date: Fri, 19 Mar 2021 13:21:15 -0700 Subject: [PATCH 006/103] Update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 52ddf2ced..b608a7662 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -64,7 +64,7 @@ stage("Build and Publish") { sh label:"Release", script:"""set -ex conda activate ${ENV_NAME} d2lbook build pkg - d2lbook deploy html pdf pkg colab sagemaker slides --s3 s3://zh-v2.d2l.ai + d2lbook deploy html pdf pkg colab sagemaker --s3 s3://zh-v2.d2l.ai """ } else { From 7deda489a4dd82910ff33007fff52a09b1f68df8 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Tue, 23 Mar 2021 14:27:06 -0700 Subject: [PATCH 007/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06a4350dd..a7ffffcb2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](http://ci.d2l.ai/job/d2l-zh/job/master/badge/icon)](http://ci.d2l.ai/job/d2l-zh/job/master/) -[第一版:zh.D2L.ai](https://zh.d2l.ai/) | [第二版预览版:zh-v2.D2L.ai](https://zh-v2.d2l.ai) | 安装和使用书中源代码:[第一版](https://zh.d2l.ai/chapter_prerequisite/install.html) [第二版](https://zh-v2.d2l.ai/chapter_installation/index.html) | 版本号: v2.0.0-alpha0 +[第一版:zh.D2L.ai](https://zh.d2l.ai/) | [第二版预览版:zh-v2.D2L.ai](https://zh-v2.d2l.ai) | 安装和使用书中源代码:[第一版](https://zh.d2l.ai/chapter_prerequisite/install.html) [第二版](https://zh-v2.d2l.ai/chapter_installation/index.html) | 当前版本: v2.0.0-alpha0
理解深度学习的最佳方法是学以致用。
From 2bcc398138a5973dbcfa31b5d3d8c21a8400eb1c Mon Sep 17 00:00:00 2001 From: Sliverwing Zhang Date: Wed, 24 Mar 2021 12:36:58 +0800 Subject: [PATCH 008/103] fix typo. (#702) --- chapter_preliminaries/ndarray.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preliminaries/ndarray.md b/chapter_preliminaries/ndarray.md index 1d5eea1a0..f74765263 100644 --- a/chapter_preliminaries/ndarray.md +++ b/chapter_preliminaries/ndarray.md @@ -3,7 +3,7 @@ 为了能够完成各种操作,我们需要某种方法来存储和操作数据。一般来说,我们需要做两件重要的事情:(1)获取数据;(2)在数据读入计算机后对其进行处理。如果没有某种方法来存储数据,那么获取数据是没有意义的。我们先尝试一个合成数据。首先,我们介绍$n$维数组,也称为 *张量*(tensor)。 -如果你使用过 Python 中最广泛使用的科学计算包 NumPy,那么你会感觉怼本部分很熟悉。无论你使用哪个框架,它的 *张量类*(在 MXNet 中为 `ndarray`,在 PyTorch 和TensorFlow中为 `Tensor`)与 Numpy 的 `ndarray` 类似,但都比Numpy 的 `ndarray`多一些重要功能。首先,GPU 很好地支持加速计算,而 NumPy 仅支持 CPU 计算。其次,张量类支持自动微分。这些功能使得张量类更适合深度学习。除非另有说明,在整本书中所说的张量指的是张量类的实例。 +如果你使用过 Python 中最广泛使用的科学计算包 NumPy,那么你会感觉对本部分很熟悉。无论你使用哪个框架,它的 *张量类*(在 MXNet 中为 `ndarray`,在 PyTorch 和TensorFlow中为 `Tensor`)与 Numpy 的 `ndarray` 类似,但都比Numpy 的 `ndarray`多一些重要功能。首先,GPU 很好地支持加速计算,而 NumPy 仅支持 CPU 计算。其次,张量类支持自动微分。这些功能使得张量类更适合深度学习。除非另有说明,在整本书中所说的张量指的是张量类的实例。 ## 入门 From ffb7f99dbb68157ec5b1c42bf3d6a0a56728dfb0 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Tue, 23 Mar 2021 21:37:11 -0700 Subject: [PATCH 009/103] minor (#703) --- chapter_introduction/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index 9507ea8d2..9302bac94 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -66,7 +66,7 @@ 如果一切顺利,经过一番训练,模型对于“片段是否包含唤醒词“的预测通常是正确的。 现在我们的模型每次听到“Alexa”这个词时都会发出“是”的声音。 -由于这里的唤醒词是任意选择的自然语言,因此我们可能需要一个足够丰富的模型族,使模型多元化,。 +由于这里的唤醒词是任意选择的自然语言,因此我们可能需要一个足够丰富的模型族,使模型多元化。 比如,模型族的另一个模型只在听到“Hey Siri”这个词时发出“是”。 理想情况下,同一个模型族应该适合于“Alexa”识别和“Hey Siri”识别,因为它们似乎是相似的任务。 相反,如果我们想处理完全不同的输入或输出,比如从图像映射到字幕,或从英语映射到中文,我们可能需要一个完全不同的模型族。 @@ -92,7 +92,7 @@ 通过这种方式,检测器最终可以学会:如果输入是猫的图片就输出一个非常大的正数,如果输入是狗的图片就会得出一个非常大的负数。 如果检测器不确定,它会输出接近于零的数...... 这个例子仅仅是机器学习常见应用的冰山一角。 -而深度学习是机器学习的一个主要分之,我们稍后将对其进行更详细的解析。 +而深度学习是机器学习的一个主要分支,我们稍后将对其进行更详细的解析。 ## 关键组件 From dd8924ea46df23842d16e780d7cebb9ce0c6e2b6 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Wed, 24 Mar 2021 10:27:33 -0700 Subject: [PATCH 010/103] typo (#705) --- chapter_linear-networks/softmax-regression-scratch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_linear-networks/softmax-regression-scratch.md b/chapter_linear-networks/softmax-regression-scratch.md index b384a16d2..6195871ee 100644 --- a/chapter_linear-networks/softmax-regression-scratch.md +++ b/chapter_linear-networks/softmax-regression-scratch.md @@ -301,7 +301,7 @@ def train_epoch_ch3(net, train_iter, loss, updater): #@save metric.add(float(l) * len(y), accuracy(y_hat, y), y.size().numel()) else: - # 使用PyTorch内置的优化器和损失函数 + # 使用定制的优化器和损失函数 l.sum().backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) From 4ae3eea70b643373f0eff55f2a43036af6f886d4 Mon Sep 17 00:00:00 2001 From: thebesttv Date: Sat, 27 Mar 2021 00:45:47 +0800 Subject: [PATCH 011/103] fix typo and change one sentence in ndarray.md (#707) * fix typo in ndarray.md * change translation in linear-algebra.md --- chapter_preliminaries/linear-algebra.md | 34 ++++++++++++------------- chapter_preliminaries/ndarray.md | 8 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/chapter_preliminaries/linear-algebra.md b/chapter_preliminaries/linear-algebra.md index b36c953c4..015e0690c 100644 --- a/chapter_preliminaries/linear-algebra.md +++ b/chapter_preliminaries/linear-algebra.md @@ -354,8 +354,8 @@ A.shape, A.sum() A.shape, tf.reduce_sum(A) ``` -默认情况下,调用求和函数会将一个张量在所有轴上汇总为一个标量。 -我们还可以[**指定求和汇总张量的轴**]。以矩阵为例。为了通过求和所有行的元素来汇总行维度(轴0),我们可以在调用函数时指定`axis=0`。 +默认情况下,调用求和函数会将一个张量沿所有轴汇总为一个标量。 +我们还可以[**指定张量求和汇总所沿的轴**]。以矩阵为例,为了通过求和所有行的元素来汇总行维度(轴0),我们可以在调用函数时指定`axis=0`。 由于输入矩阵沿0轴汇总以生成输出向量,因此输入的轴0的维数在输出形状中丢失。 ```{.python .input} @@ -540,7 +540,7 @@ tf.reduce_sum(x * y) ## 矩阵-向量积 -现在我们知道如何计算点积,我们可以开始理解 *矩阵-向量积*(matrix-vector products)。回顾分别在 :eqref:`eq_matrix_def` 和 :eqref:`eq_vec_def` 中定义和可视化的矩阵 $\mathbf{A} \in \mathbb{R}^{m \times n}$ 和向量 $\mathbf{x} \in \mathbb{R}^n$。让我们从可视化矩阵$\mathbf{A}$开始,用它的行向量表示 +现在我们知道如何计算点积,我们可以开始理解 *矩阵-向量积*(matrix-vector products)。回顾分别在 :eqref:`eq_matrix_def` 和 :eqref:`eq_vec_def` 中定义并画出的矩阵 $\mathbf{A} \in \mathbb{R}^{m \times n}$ 和向量 $\mathbf{x} \in \mathbb{R}^n$。让我们将矩阵$\mathbf{A}$用它的行向量表示 $$\mathbf{A}= \begin{bmatrix} @@ -550,7 +550,7 @@ $$\mathbf{A}= \mathbf{a}^\top_m \\ \end{bmatrix},$$ -其中每个$\mathbf{a}^\top_{i} \in \mathbb{R}^n$ 都是行向量,表示矩阵的 $i^\mathrm{th}$ 行。[**矩阵向量积 $\mathbf{A}\mathbf{x}$ 是一个长度为 $m$ 的列向量,其 $i^\mathrm{th}$ 元素是点积 $\mathbf{a}^\top_i \mathbf{x}$**]: +其中每个$\mathbf{a}^\top_{i} \in \mathbb{R}^n$ 都是行向量,表示矩阵的第 $i$ 行。[**矩阵向量积 $\mathbf{A}\mathbf{x}$ 是一个长度为 $m$ 的列向量,其第 $i$ 个元素是点积 $\mathbf{a}^\top_i \mathbf{x}$**]: $$ \mathbf{A}\mathbf{x} @@ -569,9 +569,9 @@ $$ $$ 我们可以把一个矩阵 $\mathbf{A}\in \mathbb{R}^{m \times n}$ 乘法看作是一个从 $\mathbb{R}^{n}$ 到 $\mathbb{R}^{m}$ 向量的转换。这些转换证明是非常有用的。例如,我们可以用方阵的乘法来表示旋转。 -我们将在后续章节中讲到,我们也可以使用矩阵向量乘积来描述在给定前一层的值时计算神经网络的每一层所需要的计算。 +我们将在后续章节中讲到,我们也可以使用矩阵-向量积来描述在给定前一层的值时,求解神经网络每一层所需的复杂计算。 -在代码中使用张量表示矩阵向量积,我们使用与点积相同的 `dot` 函数。当我们为矩阵 `A` 和向量 `x` 调用 `np.dot(A, x)`时,会执行矩阵向量积。注意,`A` 的列维数(沿轴1的长度)必须与 `x` 的维数(其长度)相同。 +在代码中使用张量表示矩阵-向量积,我们使用与点积相同的 `dot` 函数。当我们为矩阵 `A` 和向量 `x` 调用 `np.dot(A, x)`时,会执行矩阵-向量积。注意,`A` 的列维数(沿轴1的长度)必须与 `x` 的维数(其长度)相同。 ```{.python .input} A.shape, x.shape, np.dot(A, x) @@ -606,7 +606,7 @@ $$\mathbf{A}=\begin{bmatrix} b_{k1} & b_{k2} & \cdots & b_{km} \\ \end{bmatrix}.$$ -用行向量$\mathbf{a}^\top_{i} \in \mathbb{R}^k$ 表示矩阵$\mathbf{A}$的 $i^\mathrm{th}$ 行,并让列向量$\mathbf{b}_{j} \in \mathbb{R}^k$ 作为矩阵$\mathbf{B}$的 $j^\mathrm{th}$ 列。要生成矩阵积 $\mathbf{C} = \mathbf{A}\mathbf{B}$,最简单的方法是考虑$\mathbf{A}$的行向量和$\mathbf{B}$的列向量: +用行向量$\mathbf{a}^\top_{i} \in \mathbb{R}^k$ 表示矩阵$\mathbf{A}$的第 $i$ 行,并让列向量$\mathbf{b}_{j} \in \mathbb{R}^k$ 作为矩阵$\mathbf{B}$的第 $j$ 列。要生成矩阵积 $\mathbf{C} = \mathbf{A}\mathbf{B}$,最简单的方法是考虑$\mathbf{A}$的行向量和$\mathbf{B}$的列向量: $$\mathbf{A}= \begin{bmatrix} @@ -658,12 +658,12 @@ B = tf.ones((4, 3), tf.float32) tf.matmul(A, B) ``` -矩阵矩阵乘法可以简单地称为 **矩阵乘法**,不应与 哈达玛积 混淆。 +矩阵-矩阵乘法可以简单地称为 **矩阵乘法**,不应与 哈达玛积 混淆。 ## 范数 :label:`subsec_lin-algebra-norms` -线性代数中一些最有用的运算符是 *范数*(norms)。非正式地说,一个向量的*范数*告诉我们一个向量有多大。 +线性代数中最有用的一些运算符是 *范数*(norms)。非正式地说,一个向量的*范数*告诉我们一个向量有多大。 这里考虑的 *大小*(size) 概念不涉及维度,而是分量的大小。 在线性代数中,向量范数是将向量映射到标量的函数 $f$。向量范数要满足一些属性。 @@ -679,12 +679,12 @@ $$f(\mathbf{x} + \mathbf{y}) \leq f(\mathbf{x}) + f(\mathbf{y}).$$ $$f(\mathbf{x}) \geq 0.$$ -这是有道理的,因为在大多数情况下,任何东西的最小的*大小*是0。最后一个性质要求最小范数,并且只有由所有零组成的向量才能达到最小范数。 +这是有道理的,因为在大多数情况下,任何东西的最小的*大小*是0。最后一个性质要求范数最小为0,当且仅当向量全由0组成。 $$\forall i, [\mathbf{x}]_i = 0 \Leftrightarrow f(\mathbf{x})=0.$$ 你可能会注意到,范数听起来很像距离的度量。如果你还记得小学时的欧几里得距离(想想毕达哥拉斯定理),那么非负性的概念和三角不等式可能会给你一些启发。 -事实上,欧几里得距离是一个范数:具体而言,它是 $L_2$ 范数。假设$n$-维向量$\mathbf{x}$中的元素是$x_1, \ldots, x_n$ 的 [**$L_2$ *范数* 是向量元素平方和的平方根:**] +事实上,欧几里得距离是一个范数:具体而言,它是 $L_2$ 范数。假设$n$维向量 $\mathbf{x}$ 中的元素是$x_1, \ldots, x_n$,其 [**$L_2$ *范数* 是向量元素平方和的平方根:**] (**$$\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2},$$**) @@ -707,7 +707,7 @@ u = tf.constant([3.0, -4.0]) tf.norm(u) ``` -在深度学习中,我们更经常地使用平方 $L_2$ 范数。你还会经常遇到 [**$L_1$ 范数,它表示为向量元素的绝对值之和:**] +在深度学习中,我们更经常地使用 $L_2$ 范数的平方。你还会经常遇到 [**$L_1$ 范数,它表示为向量元素的绝对值之和:**] (**$$\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|.$$**) @@ -731,11 +731,11 @@ $L_2$ 范数和 $L_1$ 范数都是更一般的$L_p$范数的特例: $$\|\mathbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}.$$ -类似于向量的$L_2$ 范数,[**矩阵**] $\mathbf{X} \in \mathbb{R}^{m \times n}$ (**的 *弗罗贝尼乌斯范数*(Frobenius norm) 是矩阵元素的平方和的平方根:**) +类似于向量的$L_2$ 范数,[**矩阵**] $\mathbf{X} \in \mathbb{R}^{m \times n}$ (**的 *弗罗贝尼乌斯范数*(Frobenius norm) 是矩阵元素平方和的平方根:**) (**$$\|\mathbf{X}\|_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}.$$**) -弗罗贝尼乌斯范数满足向量范数的所有性质。它的行为就好像它是矩阵形向量的 $L_2$ 范数。调用以下函数将计算矩阵的弗罗贝尼乌斯范数。 +弗罗贝尼乌斯范数满足向量范数的所有性质。它就像是矩阵形向量的 $L_2$ 范数。调用以下函数将计算矩阵的弗罗贝尼乌斯范数。 ```{.python .input} np.linalg.norm(np.ones((4, 9))) @@ -757,14 +757,14 @@ tf.norm(tf.ones((4, 9))) 虽然我们不想走得太远,但我们可以对这些概念为什么有用有一些直觉。在深度学习中,我们经常试图解决优化问题: *最大化* 分配给观测数据的概率; *最小化* 预测和真实观测之间的距离。 -为物品(如单词、产品或新闻文章)分配向量表示,以便最小化相似项目之间的距离,最大化不同项目之间的距离。 +用向量表示物品(如单词、产品或新闻文章),以便最小化相似项目之间的距离,最大化不同项目之间的距离。 通常,目标,或许是深度学习算法最重要的组成部分(除了数据),被表达为范数。 ## 关于线性代数的更多信息 -就在这一部分,我们已经教会了你们所有的线性代数,你们将需要这些线性代数来理解大量的现代深度学习。 -线性代数还有很多,其中很多数学对于机器学习非常有用。例如,矩阵可以分解为因子,这些分解可以显示真实世界数据集中的低维结构。机器学习的整个子领域都侧重于使用矩阵分解及其向高阶张量的泛化来发现数据集中的结构并解决预测问题。但这本书的重点是深度学习。我们相信,一旦你开始动手尝试在真实数据集上应用了有效的机器学习模型,你会更倾向于学习更多数学。因此,虽然我们保留在后面介绍更多数学知识的权利,但我们将在这里结束这一部分。 +仅用一节,我们就教会了你所需的,用以理解大量的现代深度学习的全部线性代数。 +线性代数还有很多,其中很多数学对于机器学习非常有用。例如,矩阵可以分解为因子,这些分解可以显示真实世界数据集中的低维结构。机器学习的整个子领域都侧重于使用矩阵分解及其向高阶张量的泛化来发现数据集中的结构并解决预测问题。但这本书的重点是深度学习。我们相信,一旦你开始动手尝试并在真实数据集上应用了有效的机器学习模型,你会更倾向于学习更多数学。因此,虽然我们保留在后面介绍更多数学知识的权利,但我们这一节到此结束。 如果你渴望了解有关线性代数的更多信息,你可以参考 [线性代数运算的在线附录](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/geometry-linear-algebraic-ops.html) 或其他优秀资源 :cite:`Strang.1993,Kolter.2008,Petersen.Pedersen.ea.2008`。 diff --git a/chapter_preliminaries/ndarray.md b/chapter_preliminaries/ndarray.md index f74765263..7c0f5ac16 100644 --- a/chapter_preliminaries/ndarray.md +++ b/chapter_preliminaries/ndarray.md @@ -175,7 +175,7 @@ tf.constant([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) 这本书不是关于软件工程的。我们的兴趣不仅仅限于从数组读取和写入数据。我们想在这些数组上执行数学运算。一些最简单且最有用的操作是 *按元素*(elementwise) 操作。它们将标准标量运算符应用于数组的每个元素。对于将两个数组作为输入的函数,按元素运算将二元运算符应用于两个数组中的每对位置对应的元素。我们可以基于任何从标量到标量的函数来创建按元素函数。 -在数学表示法中,我们将通过符号 $f: \mathbb{R} \rightarrow \mathbb{R}$ 来表示 *一元* 标量运算符(只接收一个输入)。这意味着该函数从任何实数($\mathbb{R}$)映射到另一个实数。同样,我们通过符号 $f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}$ 表示 *二元* 标量运算符,这意味着该函数接收两个输入,并产生一个输出。给定同一形状的任意两个向量$\mathbf{u}$和$\mathbf{v}$ 和二元运算符 $f$,我们可以得到向量$\mathbf{c} = F(\mathbf{u},\mathbf{v})$。具体计算方法是$c_i \gets f(u_i, v_i)$ ,其中 $c_i$、u_i$ 和 $v_i$ 分别是向量$\mathbf{c}$、$\mathbf{u}$ 和 $\mathbf{v}$中的元素。在这里,我们通过将标量函数升级为按元素向量运算来生成向量值 $F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d$。 +在数学表示法中,我们将通过符号 $f: \mathbb{R} \rightarrow \mathbb{R}$ 来表示 *一元* 标量运算符(只接收一个输入)。这意味着该函数从任何实数($\mathbb{R}$)映射到另一个实数。同样,我们通过符号 $f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}$ 表示 *二元* 标量运算符,这意味着该函数接收两个输入,并产生一个输出。给定同一形状的任意两个向量$\mathbf{u}$和$\mathbf{v}$ 和二元运算符 $f$,我们可以得到向量$\mathbf{c} = F(\mathbf{u},\mathbf{v})$。具体计算方法是$c_i \gets f(u_i, v_i)$ ,其中 $c_i$、$u_i$ 和 $v_i$ 分别是向量$\mathbf{c}$、$\mathbf{u}$ 和 $\mathbf{v}$中的元素。在这里,我们通过将标量函数升级为按元素向量运算来生成向量值 $F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d$。 对于任意具有相同形状的张量,[**常见的标准算术运算符(`+`、`-`、`*`、`/` 和 `**`)都可以被升级为按元素运算**]。我们可以在同一形状的任意两个张量上调用按元素操作。在下面的例子中,我们使用逗号来表示一个具有5个元素的元组,其中每个元素都是按元素操作的结果。 @@ -219,8 +219,8 @@ tf.exp(x) 除了按元素计算外,我们还可以执行线性代数运算,包括向量点积和矩阵乘法。我们将在 :numref:`sec_linear-algebra` 中解释线性代数的重点内容(不需要先修知识)。 -[**我们也可以把多个张量连结在一起**],把它们端对端地叠起来形成一个更大的张量。 -我们也可以 *连结*(concatenate) 多个张量在一起,将它们端到端堆叠以形成更大的张量。我们只需要提供张量列表,并给出沿哪个轴连结。下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素)和按列(轴-1,形状的第二个元素)连结两个矩阵时会发生什么情况。我们可以看到,第一个输出张量的轴-0长度 ($6$) 是两个输入张量轴-0长度的总和 ($3 + 3$);第二个输出张量的轴-1长度 ($8$) 是两个输入张量轴-1长度的总和 ($4 + 4$)。 +[**我们也可以把多个张量 *连结*(concatenate) 在一起**],把它们端对端地叠起来形成一个更大的张量。 +我们只需要提供张量列表,并给出沿哪个轴连结。下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素)和按列(轴-1,形状的第二个元素)连结两个矩阵时会发生什么情况。我们可以看到,第一个输出张量的轴-0长度 ($6$) 是两个输入张量轴-0长度的总和 ($3 + 3$);第二个输出张量的轴-1长度 ($8$) 是两个输入张量轴-1长度的总和 ($4 + 4$)。 ```{.python .input} X = np.arange(12).reshape(3, 4) @@ -428,7 +428,7 @@ computation(X, Y) ## 转换为其他 Python 对象 [**转换为 NumPy 张量**]很容易,反之也很容易。转换后的结果不共享内存。 -这个小的不便实际上是非常重要的:当你在 CPU 或 GPU 上执行操作的时候,此时Python的NumPy包也希望使用相同的内存块执行其他操作时,你不希望停止计算。 +这个小的不便实际上是非常重要的:当你在 CPU 或 GPU 上执行操作的时候,如果 Python 的 NumPy 包也希望使用相同的内存块执行其他操作,你不希望停下计算来等它。 ```{.python .input} From bc9d49d7a27cd9153745ec640e62662ede4335b6 Mon Sep 17 00:00:00 2001 From: zppet Date: Sat, 27 Mar 2021 00:47:38 +0800 Subject: [PATCH 012/103] Update index.md (#704) updated for typos and E2C translation --- chapter_introduction/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index 9302bac94..fe50b8170 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -131,14 +131,14 @@ 当我们有了更多的数据,我们通常可以训练出更强大的模型,从而减少对预先设想假设的依赖。 数据集的由小变大为现代深度学习的成功奠定基础。 在没有大数据集的情况下,许多令人兴奋的深度学习模型黯然失色。 -就算一些深度学习模型在小数据集上能够工作,但其效能并不比不上传统方法。 +就算一些深度学习模型在小数据集上能够工作,但其效能并比不上传统方法。 请注意,仅仅拥有海量的数据是不够的,我们还需要正确的数据。 如果数据中充满了错误,或者如果数据的特征不能预测任务目标,那么模型很可能无效。 有一句古语很好地反映了这个现象:“输入的是垃圾,输出的也是垃圾。”(“Garbage in, garbage out.") 此外,糟糕的预测性能甚至会加倍放大事态的严重性。 在一些敏感应用中,如预测性监管、简历筛选和用于贷款的风险模型,我们必须特别警惕垃圾数据的后果。 -一种常见的问题来着不均衡的数据集,比如在一个有关医疗的训练数据集中,某些人群没有样本表示。 +一种常见的问题来自不均衡的数据集,比如在一个有关医疗的训练数据集中,某些人群没有样本表示。 想象一下,假设你要训练一个皮肤癌识别模型,但它(在训练数据集)从未见过的黑色皮肤的人群,就会顿时束手无策。 再比如,如果用“过去的招聘决策数据”来训练一个筛选简历的模型,那么机器学习模型可能会无意中捕捉到历史残留的不公正,并将其自动化。 @@ -728,7 +728,7 @@ Canny边缘检测器 :cite:`Canny.1987` 和SIFT特征提取器 :cite:`Lowe.2004` 当数据稀缺时,人们需要依靠简化对现实的假设来获得有用的模型。 当数据丰富时,可以用更准确地拟合实际情况的非参数模型来代替。 在某种程度上,这反映了物理学在上个世纪中叶随着计算机的出现所经历的进步。 -现在人们可以求助于相关偏微分方程的数值模拟,而不是用手来求解电子行为的参数近似。这导致了更精确的模型,尽管常常以牺牲可解释性为代价。 +现在人们可以借助于相关偏微分方程的数值模拟,而不是用手来求解电子行为的参数近似。这导致了更精确的模型,尽管常常以牺牲可解释性为代价。 与以前工作的另一个不同之处是接受次优解,处理非凸非线性优化问题,并且愿意在证明之前尝试。 这种在处理统计问题上新发现的经验主义,加上人才的迅速涌入,导致了实用算法的快速进步。 From 05852d21e7adff046407942a800800636e7a8c97 Mon Sep 17 00:00:00 2001 From: Linhan Wu <1002503818@qq.com> Date: Sat, 27 Mar 2021 00:49:24 +0800 Subject: [PATCH 013/103] fix 4.4.1.2. Model Complexity translation issues (#706) * fix 4.4.1.2. Model Complexity translation issues * fix typo in chapter_multilayer-perceptrons/environment.md Co-authored-by: Linhan_Wu --- chapter_multilayer-perceptrons/environment.md | 4 ++-- chapter_multilayer-perceptrons/underfit-overfit.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chapter_multilayer-perceptrons/environment.md b/chapter_multilayer-perceptrons/environment.md index cab824862..06f19ef98 100644 --- a/chapter_multilayer-perceptrons/environment.md +++ b/chapter_multilayer-perceptrons/environment.md @@ -4,7 +4,7 @@ 许多失败的机器学习部署都可以追溯到这种方式。有时,根据测试集的准确度衡量,模型表现得非常出色,但是当数据分布突然改变时,模型在部署中会出现灾难性的失败。更隐蔽的是,有时模型的部署本身就是扰乱数据分布的催化剂。例如,我们训练了一个模型来预测谁将偿还贷款或违约,发现申请人选择的鞋子与违约风险相关(牛津鞋表示偿还,运动鞋表示违约)。此后,我们可能倾向于向所有穿着牛津鞋的申请人发放贷款,并拒绝所有穿着运动鞋的申请人。 -在这种情况下,在这种情况下,我们从模式识别到决策的未经深思熟虑地跳跃,以及我们未能批判性地考虑环境可能会带来灾难性的后果。首先,一旦我们开始根据鞋类做出决定,顾客就会理解并改变他们的行为。不久,所有的申请者都会穿牛津鞋,而信用度却没有相应的提高。花点时间来理解这一点,因为机器学习的许多应用中都存在类似的问题:通过将基于模型的决策引入环境,我们可能会破坏模型。 +在这种情况下,我们从模式识别到决策的未经深思熟虑地跳跃,以及我们未能批判性地考虑环境可能会带来灾难性的后果。首先,一旦我们开始根据鞋类做出决定,顾客就会理解并改变他们的行为。不久,所有的申请者都会穿牛津鞋,而信用度却没有相应的提高。花点时间来理解这一点,因为机器学习的许多应用中都存在类似的问题:通过将基于模型的决策引入环境,我们可能会破坏模型。 虽然我们不可能在一节中完整地讨论这些主题,但我们的目的是揭示一些常见的问题,并激发必要的批判性思考,以便及早发现这些情况,减轻损害,并负责任地使用机器学习。有些解决方案很简单(要求“正确”的数据),有些在技术上很困难(实施强化学习系统),还有一些解决方案要求我们完全跳出统计预测的领域,努力解决与算法的伦理应用有关的棘手哲学问题。 @@ -55,7 +55,7 @@ 假设你想设计一个检测癌症的算法。你从健康人和病人那里收集数据,然后训练你的算法。它工作得很好,给你很高的准确性,然后你得出了你已经准备好在医疗诊断事业上取得成功的结论。请先别着急。 -产生训练数据的分布和你在实际中遇到的分布可能有很大的不同。这件事在一个不幸的初创公司身上发生过,我们中的一些作者几年前和他们合作过。他们正在研究一种血液检测方法,主要针对一种影响老年男性的疾病,并希望利用他们从病人身上采集的血液样本进行研究。然而,从健康男性身上获取血样比中系统中已有的病人身上获取要困难得多。作为补偿,这家初创公司向一所大学校园内的学生征集献血,作为开发测试的健康对照样本。然后这家初创公司问我们是否可以帮助他们建立一个用于检测疾病的分类器。 +产生训练数据的分布和你在实际中遇到的分布可能有很大的不同。这件事在一个不幸的初创公司身上发生过,我们中的一些作者几年前和他们合作过。他们正在研究一种血液检测方法,主要针对一种影响老年男性的疾病,并希望利用他们从病人身上采集的血液样本进行研究。然而,从健康男性身上获取血样比从系统中已有的病人身上获取要困难得多。作为补偿,这家初创公司向一所大学校园内的学生征集献血,作为开发测试的健康对照样本。然后这家初创公司问我们是否可以帮助他们建立一个用于检测疾病的分类器。 正如我们向他们解释的那样,用近乎完美的准确度来区分健康和患病人群确实很容易。然而,这是因为受试者在年龄、激素水平、体力活动、饮食、饮酒以及其他许多与疾病无关的因素上存在差异。这对真正的病人可能并不适用。从他们的抽样程序出发,我们可能会遇到极端的协变量偏移。此外,这种情况不太可能通过常规方法加以纠正。简言之,他们浪费了一大笔钱。 diff --git a/chapter_multilayer-perceptrons/underfit-overfit.md b/chapter_multilayer-perceptrons/underfit-overfit.md index 7f75ec5f5..5c1764230 100644 --- a/chapter_multilayer-perceptrons/underfit-overfit.md +++ b/chapter_multilayer-perceptrons/underfit-overfit.md @@ -49,7 +49,7 @@ 在本节中,为了给你一些直观的印象,我们将重点介绍几个倾向于影响模型泛化的因素: -1. 可调整参数的数量。当可调整参数(有时称为*自由度*)的数量很大时,模型往往更容易过拟合。 +1. 可调整参数的数量。当可调整参数的数量(有时称为*自由度*)很大时,模型往往更容易过拟合。 1. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。 1. 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一个或两个样本的数据集。但是,过拟合一个数百万个样本数据集需要一个极其灵活的模型。 From d2752414acd6424f07712278c0dd0d3932c5ea04 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 27 Mar 2021 00:51:12 +0800 Subject: [PATCH 014/103] =?UTF-8?q?nvcc=E7=A8=8B=E5=BA=8Fubuntu=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=91=BD=E4=BB=A4=20(#701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加ubuntu系统确实nvcc程序时的安装命令 * 添加ubuntu系统缺失nvcc程序时的安装命令 --- chapter_installation/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_installation/index.md b/chapter_installation/index.md index 50a22d19b..e35a7f3c8 100644 --- a/chapter_installation/index.md +++ b/chapter_installation/index.md @@ -106,7 +106,7 @@ pip uninstall mxnet ``` -然后,我们需要找到安装的 CUDA 版本。你可以通过 `nvcc --version` 或 `cat /usr/local/cuda/version.txt` 查看。假设你已安装 CUDA 10.1,则可以使用以下命令进行安装: +然后,我们需要找到安装的 CUDA 版本。你可以通过 `nvcc --version` 或 `cat /usr/local/cuda/version.txt` 查看。如果不存在nvcc,ubuntu系统安装命令为`sudo apt update && sudo apt install nvidia-cuda-toolkit -y`。假设你已安装 CUDA 10.1,则可以使用以下命令进行安装: ```bash From 666328acb4a56b15c46f2f03a4dee3a4290ee2b1 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 26 Mar 2021 22:16:17 -0700 Subject: [PATCH 015/103] Update index.md --- chapter_installation/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_installation/index.md b/chapter_installation/index.md index e35a7f3c8..4c5906a78 100644 --- a/chapter_installation/index.md +++ b/chapter_installation/index.md @@ -106,7 +106,7 @@ pip uninstall mxnet ``` -然后,我们需要找到安装的 CUDA 版本。你可以通过 `nvcc --version` 或 `cat /usr/local/cuda/version.txt` 查看。如果不存在nvcc,ubuntu系统安装命令为`sudo apt update && sudo apt install nvidia-cuda-toolkit -y`。假设你已安装 CUDA 10.1,则可以使用以下命令进行安装: +然后,我们需要找到安装的 CUDA 版本。你可以通过 `nvcc --version` 或 `cat /usr/local/cuda/version.txt` 查看。如果不存在nvcc,系统安装命令为`sudo apt update && sudo apt install nvidia-cuda-toolkit -y`。假设你已安装 CUDA 10.1,则可以使用以下命令进行安装: ```bash From c4e66853958acb2205e7e708bd2b87a3f736305c Mon Sep 17 00:00:00 2001 From: thebesttv Date: Mon, 29 Mar 2021 13:07:27 +0800 Subject: [PATCH 016/103] update translations in probability.md (#710) --- chapter_preliminaries/probability.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/chapter_preliminaries/probability.md b/chapter_preliminaries/probability.md index a5857c547..b4b19088f 100644 --- a/chapter_preliminaries/probability.md +++ b/chapter_preliminaries/probability.md @@ -25,9 +25,9 @@ 假设我们掷骰子,想知道看到1的几率有多大,而不是看到另一个数字。如果骰子是公平的,那么所有六个结果$\{1, \ldots, 6\}$都有相同的可能发生,因此我们将在每六次中看到一个$1$。我们可以说$1$发生的概率为$\frac{1}{6}$。 -对于我们从工厂收到的真实骰子,我们可能不知道那些比例,我们需要检查它是否有污染。调查骰子的唯一方法是多次投掷并记录结果。对于每个骰子,我们将观察到 $\{1, \ldots, 6\}$ 中的一个值。给定这些结果,我们想调查每个结果的概率。 +对于我们从工厂收到的真实骰子,我们可能不知道那些比例,我们需要检查它是否有瑕疵。调查骰子的唯一方法是多次投掷并记录结果。对于每个骰子,我们将观察到 $\{1, \ldots, 6\}$ 中的一个值。给定这些结果,我们想调查每个结果的概率。 -对于每个值,一种自然的方法是将单个计数的值除以投掷的总次数。 +对于每个值,一种自然的方法是将它出现的次数除以投掷的总次数。 这给了我们一个给定*事件*的概率的*估计值*。*大数定律*(law of large numbers)告诉我们,随着投掷次数的增加,这个估计值会越来越接近真实的潜在概率。在深入了解这里的细节之前,让我们先试一试。 首先,让我们导入必要的软件包。 @@ -61,7 +61,7 @@ import numpy as np 将概率分配给一些离散选择的分布称为*多项分布*(multinomial distribution)。稍后我们将给出*分布*(distribution)的更正式定义。但笼统来说,可以把它看作是对事件的概率分配。 为了抽取一个样本,我们只需传入一个概率向量。 -输出是另一个相同长度的向量:它在索引$i$处的值是采样结果对应于$i$的次数。 +输出是另一个相同长度的向量:它在索引$i$处的值是采样结果中$i$出现的次数。 ```{.python .input} fair_probs = [1.0 / 6] * 6 @@ -187,25 +187,25 @@ d2l.plt.legend(); 在我们掷骰子的随机实验中,我们引入了 *随机变量*(random variable) 的概念。随机变量几乎可以是任何数量,并且不是确定性的。它可以在随机实验的一组可能性中取一个值。考虑一个随机变量 $X$,其值在掷骰子的样本空间 $\mathcal{S} = \{1, 2, 3, 4, 5, 6\}$ 中。我们可以将事件 “看到一个 $5$” 表示为 $\{X = 5\}$ 或 $X = 5$,其概率表示为 $P(\{X = 5\})$ 或 $P(X = 5)$。通过 $P(X = a)$,我们区分了随机变量 $X$ 和 $X$ 可以采取的值(例如 $a$)。然而,这可能会导致繁琐的表示。 为了简化符号,一方面,我们可以将 $P(X)$ 表示为随机变量 $X$ 上的 *分布*(distribution):分布告诉我们 $X$ 获得任意值的概率。另一方面,我们可以简单用 $P(a)$ 表示随机变量取值 $a$ 的概率。由于概率论中的事件是来自样本空间的一组结果,因此我们可以为随机变量指定值的可取范围。例如,$P(1 \leq X \leq 3)$ 表示事件的概率 $\{1 \leq X \leq 3\}$,这意味着 $\{X = 1, 2, \text{or}, 3\}$。等价地,$P(1 \leq X \leq 3)$ 表示随机变量 $X$ 从 $\{1, 2, 3\}$ 中取值的概率。 -请注意,*离散* (discrete) 随机变量(如骰子的侧面)和 *连续* (continuous) 变量(如人的体重和身高)之间存在微妙的区别。问两个人是否具有完全相同的身高没有什么意义。如果我们进行足够精确的测量,你会发现这个星球上没有两个人具有完全相同的身高。事实上,如果我们采取足够精细的测量,在你起床和去睡觉时都不会得到相同的身高。因此,问一个人身高为 1.80139278297192196202 米高的概率是没有任何意义的。考虑到世界上的人口数量,这个概率几乎是 0。在这种情况下,询问某人的身高是否落入给定的区间,比如是否在 1.79 米和 1.81 米之间更有意义。在这些情况下,我们将这个看到某个数值的可能性量化为 *密度* (density)。恰好 1.80 米的高度上没有概率,但密度不是 0。在任何两个不同高度之间的区间,我们都有非零的概率。在本节的其余部分中,我们将考虑离散空间中的概率。对于连续随机变量的概率,您可以参考 :numref:`sec_random_variables`。 +请注意,*离散* (discrete) 随机变量(如骰子的侧面)和 *连续* (continuous) 变量(如人的体重和身高)之间存在微妙的区别。问两个人是否具有完全相同的身高没有什么意义。如果我们进行足够精确的测量,你会发现这个星球上没有两个人具有完全相同的身高。事实上,如果我们采取足够精细的测量,在你起床和去睡觉时都不会得到相同的身高。因此,问一个人身高为 1.80139278297192196202 米高的概率是没有任何意义的。考虑到世界上的人口数量,这个概率几乎是 0。在这种情况下,询问某人的身高是否落入给定的区间,比如是否在 1.79 米和 1.81 米之间更有意义。在这些情况下,我们将这个看到某个数值的可能性量化为 *密度* (density)。高度恰好 1.80 米没有概率,但密度不是 0。在任何两个不同高度之间的区间,我们都有非零的概率。在本节的其余部分中,我们将考虑离散空间中的概率。对于连续随机变量的概率,你可以参考 :numref:`sec_random_variables`。 ## 处理多个随机变量 -很多时候,我们会希望一次考虑多个随机变量。比如,我们可能需要对疾病和症状之间的关系进行建模。给定一个疾病和一个症状,比如 “流感” 和 “咳嗽”,会在某个患者身上,以某个概率存在或不存在关系。虽然我们可能希望这两者发生的概率都接近于零,但我们可能需要估计这些概率和它们之间的关系,以便我们可以运用我们的推断来实现更好的医疗服务。 +很多时候,我们会希望一次考虑多个随机变量。比如,我们可能需要对疾病和症状之间的关系进行建模。给定一个疾病和一个症状,比如 “流感” 和 “咳嗽”,以某个概率存在或不存在某个患者身上。虽然我们可能希望这两者发生的概率都接近于零,但我们可能需要估计这些概率和概率之间的关系,以便我们可以运用我们的推断来实现更好的医疗服务。 再举一个更复杂的例子:图像包含数百万像素,因此有数百万个随机变量。在许多情况下,图像会附带一个标签,标识图像中的对象。我们也可以将标签视为一个随机变量。我们甚至可以将所有元数据视为随机变量,例如位置、时间、光圈、焦距、ISO、对焦距离和相机类型。所有这些都是联合发生的随机变量。当我们处理多个随机变量时,会有若干个变量是我们感兴趣的。 ### 联合概率 -第一个被称为 *联合概率* (joint probability) $P(A = a, B=b)$。给定任何值 $a$ 和 $b$, 联合概率可以回答, $A=a$ 和 $B=b$ 同时满足的概率是多少? 请注意,对于任何值,对于任何 $a$ 和 $b$ 的取值,$P(A = a, B=b) \leq P(A=a)$。这点是确定的,因为要同时发生 $A=a$ 和 $B=b$,$A=a$就必须发生,$B=b$也必须发生(反之亦然)。因此,$A=a$ 和 $B=b$ 同时发生的可能性不大于 $A=a$ 或是 $B=b$ 的可能性。 +第一个被称为 *联合概率* (joint probability) $P(A = a, B=b)$。给定任何值 $a$ 和 $b$, 联合概率可以回答, $A=a$ 和 $B=b$ 同时满足的概率是多少? 请注意,对于任何 $a$ 和 $b$ 的取值,$P(A = a, B=b) \leq P(A=a)$。这点是确定的,因为要同时发生 $A=a$ 和 $B=b$,$A=a$就必须发生,$B=b$也必须发生(反之亦然)。因此,$A=a$ 和 $B=b$ 同时发生的可能性不大于 $A=a$ 或是 $B=b$ 的可能性。 ### 条件概率 -这给我们带来了一个有趣的比率:$0 \leq \frac{P(A=a, B=b)}{P(A=a)} \leq 1$。我们称这个比率为 *条件概率* (conditional probability),并用 $P(B=b \mid A=a)$ 表示它:它是 $B=b$ 的概率,前提是发生了 $A=a$。 +这给我们带来了一个有趣的比率:$0 \leq \frac{P(A=a, B=b)}{P(A=a)} \leq 1$。我们称这个比率为 *条件概率* (conditional probability),并用 $P(B=b \mid A=a)$ 表示它:它是 $B=b$ 的概率,前提是 $A=a$ 已发生。 ### 贝叶斯定理 -使用条件概率的定义,我们可以得出统计数据中最有用和最著名的方程之一:*Bayes 定理* (Bayes' theorem)。它如下所示。通过构造,我们有 *乘法规则*, $P(A, B) = P(B \mid A) P(A)$。根据对称性,这也适用于 $P(A, B) = P(A \mid B) P(B)$。假设 $P(B)>0$, 求解其中一个条件变量,我们得到 +使用条件概率的定义,我们可以得出统计学中最有用和最著名的方程之一:*Bayes 定理* (Bayes' theorem)。它如下所示。通过构造,我们有 *乘法规则*, $P(A, B) = P(B \mid A) P(A)$。根据对称性,这也适用于 $P(A, B) = P(A \mid B) P(B)$。假设 $P(B)>0$, 求解其中一个条件变量,我们得到 $$P(A \mid B) = \frac{P(B \mid A) P(A)}{P(B)}.$$ From 4e82fc6196b41ba4aeddd558a9bbeff070f5d692 Mon Sep 17 00:00:00 2001 From: Linhan Wu Date: Mon, 29 Mar 2021 13:08:56 +0800 Subject: [PATCH 017/103] fix typo and translation issues in kaggle-house-price.md (#709) * fix 4.4.1.2. Model Complexity translation issues * fix typo in chapter_multilayer-perceptrons/environment.md * fix typo and translation issues in kaggle-house-price.md * fix typo in model-construction.md * fix typo and translation issues in use-gpu.md Co-authored-by: Linhan_Wu --- chapter_deep-learning-computation/model-construction.md | 2 +- chapter_deep-learning-computation/use-gpu.md | 4 ++-- chapter_multilayer-perceptrons/kaggle-house-price.md | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/chapter_deep-learning-computation/model-construction.md b/chapter_deep-learning-computation/model-construction.md index 5ff6674b6..b48d95d63 100644 --- a/chapter_deep-learning-computation/model-construction.md +++ b/chapter_deep-learning-computation/model-construction.md @@ -327,7 +327,7 @@ net = FixedHiddenMLP() net(X) ``` -我们可以混合搭配各种组合块的方法。在下面的例子中,我们以一些想到的的方法嵌套块。 +我们可以混合搭配各种组合块的方法。在下面的例子中,我们以一些想到的方法嵌套块。 ```{.python .input} class NestMLP(nn.Block): diff --git a/chapter_deep-learning-computation/use-gpu.md b/chapter_deep-learning-computation/use-gpu.md index 30da9727d..792c813eb 100644 --- a/chapter_deep-learning-computation/use-gpu.md +++ b/chapter_deep-learning-computation/use-gpu.md @@ -23,7 +23,7 @@ :begin_tab:`pytorch` 在PyTorch中,每个数组都有一个设备(device),我们通常将其称为上下文(context)。到目前为止,默认情况下,所有变量和相关的计算都分配给CPU。有时上下文可能是GPU。当我们跨多个服务器部署作业时,事情会变得更加棘手。通过智能地将数组分配给上下文,我们可以最大限度地减少在设备之间传输数据的时间。例如,当在带有GPU的服务器上训练神经网络时,我们通常希望模型的参数在GPU上。 -接下来,我们需要确认安装了GPU版本的PyTorch。如果已经安装了Pythorch的CPU版本,我们需要先卸载它。例如,使用`pip uninstall torch`命令,然后根据你的CUDA版本安装相应的PyTorch版本。假设你安装了CUDA10.0,你可以通过`pip install torch-cu100`安装支持CUDA10.0的Pythorch版本。 +接下来,我们需要确认安装了GPU版本的PyTorch。如果已经安装了PyTorch的CPU版本,我们需要先卸载它。例如,使用`pip uninstall torch`命令,然后根据你的CUDA版本安装相应的PyTorch版本。假设你安装了CUDA10.0,你可以通过`pip install torch-cu100`安装支持CUDA10.0的PyTorch版本。 :end_tab: 要运行此部分中的程序,至少需要两个GPU。注意,对于大多数桌面计算机来说,这可能是奢侈的,但在云中很容易获得,例如,通过使用AWS EC2的多GPU实例。本节几乎所有的其他部分都不需要多个GPU。本节只是为了说明数据如何在不同的设备之间传递。 @@ -262,7 +262,7 @@ Z2 is Z 人们使用GPU来进行机器学习,因为他们希望运行速度快。但是在设备之间传输变量是缓慢的。所以我们希望你百分之百确定你想做一些缓慢的事情。如果深度学习框架只是自动复制而没有崩溃,那么你可能不会意识到你已经编写了一些缓慢的代码。 -此外,在设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。这也使得并行化变得更加困难,因为我们必须等待数据被发送(或者接收),然后才能继续进行更多的操作。这就是为什么拷贝操作要格外小心。根据经验,许多小手术比一个大手术糟糕得多。此外,除非你知道自己在做什么。否则,一次执行几个操作比代码中散布的许多单个操作要好得多。如果一个设备必须等待另一个设备才能执行其他操作,那么这样的操作可能会阻塞。这有点像排队订购咖啡,而不像通过电话预先订购时,当你在的时候发现咖啡已经准备好了。 +此外,在设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。这也使得并行化变得更加困难,因为我们必须等待数据被发送(或者接收),然后才能继续进行更多的操作。这就是为什么拷贝操作要格外小心。根据经验,多个小操作比一个大操作糟糕得多。此外,除非你知道自己在做什么。否则,一次执行几个操作比代码中散布的许多单个操作要好得多。如果一个设备必须等待另一个设备才能执行其他操作,那么这样的操作可能会阻塞。这有点像排队订购咖啡,而不像通过电话预先订购时,当你在的时候发现咖啡已经准备好了。 最后,当我们打印张量或将张量转换为NumPy格式时。如果数据不在内存中,框架会首先将其复制到内存中,这会导致额外的传输开销。更糟糕的是,它现在受制于可怕的全局解释器锁,这使得一切都得等待Python完成。 diff --git a/chapter_multilayer-perceptrons/kaggle-house-price.md b/chapter_multilayer-perceptrons/kaggle-house-price.md index ce667c6ba..d627681c2 100644 --- a/chapter_multilayer-perceptrons/kaggle-house-price.md +++ b/chapter_multilayer-perceptrons/kaggle-house-price.md @@ -249,7 +249,7 @@ def get_net(): 对于房价,就像股票价格一样,我们关心的是相对数量,而不是绝对数量。因此,我们更关心相对误差$\frac{y - \hat{y}}{y}$,而不是绝对误差$y - \hat{y}$。例如,如果我们在俄亥俄州农村地区估计一栋房子的价格时,我们的预测偏差了10万美元,在那里一栋典型的房子的价值是12.5万美元,那么我们可能做得很糟糕。另一方面,如果我们在加州豪宅区的预测出现了这个数字的偏差,这可能是一个惊人的准确预测(在那里,房价均值超过400万美元)。 -解决这个问题的一种方法是用价格预测的对数来衡量差异。事实上,这也是比赛中官方用来评价提交质量的误差指标。即将$$\delta$ for $|\log y - \log \hat{y}| \leq \delta$转换为$e^{-\delta} \leq \frac{\hat{y}}{y} \leq e^\delta$。这使得预测价格的对数与真实标签价格的对数之间出现以下均方根误差: +解决这个问题的一种方法是用价格预测的对数来衡量差异。事实上,这也是比赛中官方用来评价提交质量的误差指标。即将 $\delta$ for $|\log y - \log \hat{y}| \leq \delta$转换为$e^{-\delta} \leq \frac{\hat{y}}{y} \leq e^\delta$。这使得预测价格的对数与真实标签价格的对数之间出现以下均方根误差: $$\sqrt{\frac{1}{n}\sum_{i=1}^n\left(\log y_i -\log \hat{y}_i\right)^2}.$$ @@ -395,7 +395,7 @@ def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay, ## 模型选择 -在本例中,我们选择了一组未调优的超参数,并将其留给读者来改进模型。找到一个好的选择可能需要时间,这取决于一个人优化了多少变量。有了足够大的数据集和超参数的合适设置,$K$折交叉验证往往对多次测试具有相当的适应性。然而,如果我们尝试了不合理的大量选项,我们可能会发现验证效果不再代表真正的误差。 +在本例中,我们选择了一组未调优的超参数,并将其留给读者来改进模型。找到一个好的选择可能需要时间,这取决于一个人优化了多少变量。有了足够大的数据集和合理设置的超参数,$K$折交叉验证往往对多次测试具有相当的适应性。然而,如果我们尝试了不合理的大量选项,我们可能会发现验证效果不再代表真正的误差。 ```{.python .input} #@tab all @@ -406,7 +406,7 @@ print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, ' f'平均验证log rmse: {float(valid_l):f}') ``` -请注意,有时一组超参数的训练误差可能非常低,但$K$折交叉验证的误差要高得多。这表明我们过拟合了。在整个训练过程中,你将希望监控这训练误差和验证误差两个数字。较少的过拟合可能表明现有数据可以支撑一个更强大的模型。较大的过拟合可能意味着我们可以通过正则化技术来获益。 +请注意,有时一组超参数的训练误差可能非常低,但$K$折交叉验证的误差要高得多。这表明我们过拟合了。在整个训练过程中,你将希望监控训练误差和验证误差这两个数字。较少的过拟合可能表明现有数据可以支撑一个更强大的模型。较大的过拟合可能意味着我们可以通过正则化技术来获益。 ## 提交Kaggle的预测 @@ -438,7 +438,7 @@ train_and_pred(train_features, test_features, train_labels, test_data, num_epochs, lr, weight_decay, batch_size) ``` -接下来,如 :numref:`fig_kaggle_submit2` 中所示,我们可以提交预测到Kaggle上,并查看预测在测试集上与实际房价(标签)的比较情况。步骤非常简单: +接下来,如 :numref:`fig_kaggle_submit2` 中所示,我们可以提交预测到Kaggle上,并查看在测试集上的预测与实际房价(标签)的比较情况。步骤非常简单: * 登录Kaggle网站,访问房价预测竞赛页面。 * 点击“Submit Predictions”或“Late Submission”按钮(在撰写本文时,该按钮位于右侧)。 From b7c810e289bec57d0234f541485d7a668b464be3 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Sun, 28 Mar 2021 22:09:18 -0700 Subject: [PATCH 018/103] revise (#708) --- chapter_linear-networks/softmax-regression-concise.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter_linear-networks/softmax-regression-concise.md b/chapter_linear-networks/softmax-regression-concise.md index a871ed3f8..6f92904d2 100644 --- a/chapter_linear-networks/softmax-regression-concise.md +++ b/chapter_linear-networks/softmax-regression-concise.md @@ -43,8 +43,8 @@ net.initialize(init.Normal(sigma=0.01)) ```{.python .input} #@tab pytorch -# PyTorch不会隐式地调整输入的形状。 -# 因此,我们定义了展平层(flatten)在线性层前调整网络输入的形状 +# PyTorch不会隐式地调整输入的形状。因此, +# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状 net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) def init_weights(m): From a048729b08434bc89bf770028883e62a237a1439 Mon Sep 17 00:00:00 2001 From: Shixiang Wang Date: Mon, 29 Mar 2021 13:11:19 +0800 Subject: [PATCH 019/103] Update index.md (#711) * Update index.md * Update index.md * Update index.md * Update index.md --- chapter_introduction/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index fe50b8170..966b3930d 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -473,8 +473,8 @@ Ent - - - Ent - Ent 那么无监督学习可以回答什么样的问题呢?我们来看看下面的例子: * *聚类*(clustering)问题:没有标签的情况下,我们是否能给数据分类呢?比如,给定一组照片,我们能把它们分成风景照片、狗、婴儿、猫和山峰的照片吗?同样,给定一组用户的网页浏览记录,我们能否将具有相似行为的用户聚类吗? -* *主成分分析*(principal component analysis)问题:我们能否找到少量的参数来准确地捕捉数据的线性相关属性?比如,一个球的运动轨迹可以用球的速度、直径和质量来描述。再比如,裁缝们已经开发出了一小部分参数,这些参数相当准确地描述了人体的形状,以适应衣服的需要。另一个例子:在欧几里得空间中是否存在一种(任意结构的)对象的表示,使其符号属性能够很好地匹配?这可以用来描述实体及其关系,例如"罗马" $-$ "意大利" $+$ "法国" $=$ "巴黎"。 -* *因果关系*(causality)和*概率图模型*(probabilistic graphical models)问题:我们能否描述观察到的许多数据的根因?例如,如果我们有关于房价、污染、犯罪、地理位置、教育和工资的人口统计数据,我们能否简单地根据经验数据发现它们之间的关系? +* *主成分分析*(principal component analysis)问题:我们能否找到少量的参数来准确地捕捉数据的线性相关属性?比如,一个球的运动轨迹可以用球的速度、直径和质量来描述。再比如,裁缝们已经开发出了一小部分参数,这些参数相当准确地描述了人体的形状,以适应衣服的需要。另一个例子:在欧几里得空间中是否存在一种(任意结构的)对象的表示,使其符号属性能够很好地匹配?这可以用来描述实体及其关系,例如"罗马" $-$ "意大利" $+$ "法国" $=$ "巴黎"。 +* *因果关系*(causality)和*概率图模型*(probabilistic graphical models)问题:我们能否描述观察到的许多数据的根本原因?例如,如果我们有关于房价、污染、犯罪、地理位置、教育和工资的人口统计数据,我们能否简单地根据经验数据发现它们之间的关系? * *生成对抗性网络*(generative adversarial networks):为我们提供一种合成数据的方法,甚至像图像和音频这样复杂的结构化数据。潜在的统计机制是检查真实和虚假数据是否相同的测试,它是无监督学习的另一个重要而令人兴奋的领域。 @@ -511,7 +511,7 @@ Ent - - - Ent - Ent ### 强化学习 如果你对使用机器学习开发与环境交互并采取行动感兴趣,那么你最终可能会专注于*强化学习*(reinforcement learning)。 -这可能包括应用到机器人、对话系统,甚至开发视频游戏的人工智能(AI)。 +这可能包括应用到机器人、对话系统,甚至开发视频游戏的人工智能(AI)。 *深度强化学习*(deep reinforcement learning)将深度学习应用于强化学习的问题,是非常热门的研究领域。 突破性的深度*Q网络*(Q-network)在雅达利游戏中仅使用视觉输入就击败了人类, 以及 AlphaGo 程序在棋盘游戏围棋中击败了世界冠军,是两个突出强化学习的例子。 @@ -547,7 +547,7 @@ Ent - - - Ent - Ent 强化学习可能还必须处理部分可观测性问题。 也就是说,当前的观察结果可能无法阐述有关当前状态的所有信息。 比方说,一个清洁机器人发现自己被困在一个许多相同的壁橱的房子里。 -推断机器人的精确位置(从而推断其状态),需要在进入壁橱之前考虑它之前的观察结果。 +推断机器人的精确位置(从而推断其状态),需要在进入壁橱之前考虑它之前的观察结果。 最后,在任何时间点上,强化学习agent可能知道一个好的策略,但可能有许多更好的策略从未尝试过的。 强化学习agent必须不断地做出选择:是应该利用当前最好的策略,还是探索新的策略空间(放弃一些短期回报来换取知识)。 @@ -606,8 +606,8 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 随着时间的推移,对生物学的解释变得不再肤浅,但这个名字仍然存在。 其核心是当今大多数网络中都可以找到的几个关键原则: -* 线性和非线性处理单元的交替,通常称为*层*(layers)。 -* 使用链式规则(也称为*反向传播*(backpropagation))一次性调整网络中的全部参数。 +* 线性和非线性处理单元的交替,通常称为*层*(layers)。 +* 使用链式规则(也称为*反向传播*(backpropagation))一次性调整网络中的全部参数。 在最初的快速发展之后,神经网络的研究从1995年左右一直开始停滞不前,直到到2005年才稍有起色。 这主要是因为两个原因。 @@ -676,7 +676,7 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 * 物体识别同样也取得了长足的进步。估计图片中的物体在2010年是一项相当具有挑战性的任务。在ImageNet基准上,来自NEC实验室和伊利诺伊大学香槟分校的研究人员获得了28%的Top-5错误率 :cite:`Lin.Lv.Zhu.ea.2010` 。到2017年,这一错误率降低到2.25% :cite:`Hu.Shen.Sun.2018` 。同样,在鉴别鸟类或诊断皮肤癌方面也取得了惊人的成果。 * 游戏曾经是人类智慧的堡垒。从TD-Gammon开始,一个使用时差强化学习的五子棋游戏程序,算法和计算的进展导致了广泛应用的算法。与五子棋不同的是,国际象棋有一个复杂得多的状态空间和一组动作。深蓝公司利用大规模并行性、专用硬件和高效搜索游戏树 :cite:`Campbell.Hoane-Jr.Hsu.2002` 击败了加里·卡斯帕罗夫(Garry Kasparov)。围棋由于其巨大的状态空间,难度更大。AlphaGo在2015年达到了人类平等,使用深度学习和蒙特卡洛树抽样 :cite:`Silver.Huang.Maddison.ea.2016` 相结合。扑克中的挑战是状态空间很大,而且没有完全观察到(我们不知道对手的牌)。在扑克游戏中,库图斯使用有效的结构化策略超过了人类的表现 :cite:`Brown.Sandholm.2017` 。这说明了游戏中令人印象深刻的进步,以及先进的算法在其中发挥了关键作用的事实。 -* 人工智能进步的另一个迹象是自动驾驶汽车和卡车的出现。虽然完全自主还没有完全触手可及,但在这个方向上已经取得了很好的进展,特斯拉(Tesla)、NVIDIA和Waymo等公司的产品至少实现了部分自主。让完全自主如此具有挑战性的是,正确的驾驶需要感知、推理和将规则纳入系统的能力。目前,深度学习主要应用于这些问题的计算机视觉方面。其余部分则由工程师进行大量调整。 +* 人工智能进步的另一个迹象是自动驾驶汽车和卡车的出现。虽然完全自主还没有完全触手可及,但在这个方向上已经取得了很好的进展,特斯拉(Tesla)、NVIDIA和Waymo等公司的产品至少实现了部分自主。让完全自主如此具有挑战性的是,正确的驾驶需要感知、推理和将规则纳入系统的能力。目前,深度学习主要应用于这些问题的计算机视觉方面。其余部分则由工程师进行大量调整。 同样,上面的列表仅仅触及了机器学习对实际应用的影响之处的皮毛。 例如,机器人学、物流、计算生物学、粒子物理学和天文学最近取得的一些突破性进展至少部分归功于机器学习。 From 0f3eaed0f6e86a988af4f99f2a271bdd85ca2e42 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Mon, 29 Mar 2021 15:49:12 -0700 Subject: [PATCH 020/103] unrendered slides rebase (#712) --- chapter_multilayer-perceptrons/mlp-concise.md | 2 +- chapter_multilayer-perceptrons/mlp.md | 2 +- chapter_multilayer-perceptrons/underfit-overfit.md | 2 +- chapter_multilayer-perceptrons/weight-decay.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chapter_multilayer-perceptrons/mlp-concise.md b/chapter_multilayer-perceptrons/mlp-concise.md index b595613d9..48b90185c 100644 --- a/chapter_multilayer-perceptrons/mlp-concise.md +++ b/chapter_multilayer-perceptrons/mlp-concise.md @@ -25,7 +25,7 @@ import tensorflow as tf ## 模型 -与softmax回归的简洁实现( :numref:`sec_softmax_concise`)相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是[**隐藏层**],它(**包含256个隐藏单元并使用了ReLU激活函数**)。第二层是输出层。 +与softmax回归的简洁实现( :numref:`sec_softmax_concise`)相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是[**隐藏层**],它(**包含256个隐藏单元,并使用了ReLU激活函数**)。第二层是输出层。 ```{.python .input} net = nn.Sequential() diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index b2c273f19..0867ce67a 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -156,7 +156,7 @@ $$\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).$$ ### sigmoid函数 -对于一个定义域在$\mathbb{R}$中的输入,[** *sigmoid函数*将输入变换为区间(0, 1)上的输出**]。因此,sigmoid通常称为*挤压函数*(squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值: +[**对于一个定义域在$\mathbb{R}$中的输入,*sigmoid函数*将输入变换为区间(0, 1)上的输出**]。因此,sigmoid通常称为*挤压函数*(squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值: (**$$\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.$$**) diff --git a/chapter_multilayer-perceptrons/underfit-overfit.md b/chapter_multilayer-perceptrons/underfit-overfit.md index 5c1764230..c1f12557f 100644 --- a/chapter_multilayer-perceptrons/underfit-overfit.md +++ b/chapter_multilayer-perceptrons/underfit-overfit.md @@ -154,7 +154,7 @@ labels = np.dot(poly_features, true_w) labels += np.random.normal(scale=0.1, size=labels.shape) ``` -同样,存储在`poly_features`中的单项式由gamma函数重新缩放,其中$\Gamma(n)=(n-1)!$。从生成的数据集中查[**看一下前2个样本**]。值1是与偏置相对应的常量特征。 +同样,存储在`poly_features`中的单项式由gamma函数重新缩放,其中$\Gamma(n)=(n-1)!$。从生成的数据集中查[**看一下前2个样本**]。第一个值是与偏置相对应的常量特征。 ```{.python .input} #@tab pytorch, tensorflow diff --git a/chapter_multilayer-perceptrons/weight-decay.md b/chapter_multilayer-perceptrons/weight-decay.md index e158b568b..ab3f17962 100644 --- a/chapter_multilayer-perceptrons/weight-decay.md +++ b/chapter_multilayer-perceptrons/weight-decay.md @@ -10,7 +10,7 @@ ## 范数与权重衰减 在之前的章节,我们已经描述了$L_2$范数和$L_1$范数,它们是$L_p$范数的特殊情况。 -(***权重衰减*(通常称为$L_2$正则化),可能是最广泛使用的对参数化机器学习模型进行正则化的技术**)。这项技术是基于一个基本直觉,即在所有函数$f$中,函数$f = 0$(所有输入都得到值$0$)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,整个数学分支,包括函数分析和巴拿赫空间理论,都在致力于回答这个问题。 +(**在训练参数化机器学习模型时,*权重衰减*(通常称为$L_2$正则化)是最广泛使用的正则化的技术之一**)。这项技术是基于一个基本直觉,即在所有函数$f$中,函数$f = 0$(所有输入都得到值$0$)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,整个数学分支,包括函数分析和巴拿赫空间理论,都在致力于回答这个问题。 一种简单的方法是通过线性函数$f(\mathbf{x}) = \mathbf{w}^\top \mathbf{x}$中的权重向量的某个范数来度量其复杂性,例如$\| \mathbf{w} \|^2$。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中。将原来的训练目标*最小化训练标签上的预测损失*,调整为*最小化预测损失和惩罚项之和*。 现在,如果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数$\| \mathbf{w} \|^2$。这正是我们想要的。让我们回顾一下 :numref:`sec_linear_regression` 中的线性回归例子。我们的损失由下式给出: From 257c9ac2ab7e64783ff1f989defdf7dac1dc725d Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Mon, 29 Mar 2021 16:23:04 -0700 Subject: [PATCH 021/103] Update optimization-intro.md --- chapter_optimization/optimization-intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_optimization/optimization-intro.md b/chapter_optimization/optimization-intro.md index e0bf92eb1..c960b7c75 100644 --- a/chapter_optimization/optimization-intro.md +++ b/chapter_optimization/optimization-intro.md @@ -5,7 +5,7 @@ 对于深度学习问题,我们通常首先定义一个损失函数,一旦我们有了损失函数,我们就可以使用一个优化算法来尝试最小化损失。 在最优化中,损失函数通常被称为最优化问题的目标函数。 根据传统和惯例,大多数优化算法都与“最小化”有关。 -如果我们需要最大化一个目标,有一个简单的解决方案:只要翻转目标上的标志。 +如果我们需要最大化一个目标,有一个简单的解决方案:只要翻转目标函数前面的符号。 ## 优化与估算 From f69c97261a91091401cd032d1aa01d5f050247d8 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Tue, 30 Mar 2021 20:12:45 -0700 Subject: [PATCH 022/103] Retrigger slides (#713) * retrigger slides * retrigger * retrigger * retrigger * retrigger * retrigger --- chapter_multilayer-perceptrons/mlp-concise.md | 4 ++-- chapter_multilayer-perceptrons/weight-decay.md | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/chapter_multilayer-perceptrons/mlp-concise.md b/chapter_multilayer-perceptrons/mlp-concise.md index 48b90185c..ce46aa942 100644 --- a/chapter_multilayer-perceptrons/mlp-concise.md +++ b/chapter_multilayer-perceptrons/mlp-concise.md @@ -25,7 +25,7 @@ import tensorflow as tf ## 模型 -与softmax回归的简洁实现( :numref:`sec_softmax_concise`)相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是[**隐藏层**],它(**包含256个隐藏单元,并使用了ReLU激活函数**)。第二层是输出层。 +与softmax回归的简洁实现(:numref:`sec_softmax_concise`)相比,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。第一层是[**隐藏层**],它(**包含256个隐藏单元,并使用了ReLU激活函数**)。第二层是输出层。 ```{.python .input} net = nn.Sequential() @@ -56,7 +56,7 @@ net = tf.keras.models.Sequential([ tf.keras.layers.Dense(10)]) ``` -[**训练过程**]实现与我们实现softmax回归时完全相同。这种模块化设计使我们能够将与和模型架构有关的内容独立出来。 +[**训练过程**]的实现与我们实现softmax回归时完全相同,这种模块化设计使我们能够将与和模型架构有关的内容独立出来。 ```{.python .input} batch_size, lr, num_epochs = 256, 0.1, 10 diff --git a/chapter_multilayer-perceptrons/weight-decay.md b/chapter_multilayer-perceptrons/weight-decay.md index ab3f17962..ebc51e1dc 100644 --- a/chapter_multilayer-perceptrons/weight-decay.md +++ b/chapter_multilayer-perceptrons/weight-decay.md @@ -10,7 +10,8 @@ ## 范数与权重衰减 在之前的章节,我们已经描述了$L_2$范数和$L_1$范数,它们是$L_p$范数的特殊情况。 -(**在训练参数化机器学习模型时,*权重衰减*(通常称为$L_2$正则化)是最广泛使用的正则化的技术之一**)。这项技术是基于一个基本直觉,即在所有函数$f$中,函数$f = 0$(所有输入都得到值$0$)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,整个数学分支,包括函数分析和巴拿赫空间理论,都在致力于回答这个问题。 +(~~权重衰减是最广泛使用的正则化的技术之一~~) +在训练参数化机器学习模型时,*权重衰减*(通常称为$L_2$正则化)是最广泛使用的正则化的技术之一。这项技术是基于一个基本直觉,即在所有函数$f$中,函数$f = 0$(所有输入都得到值$0$)在某种意义上是最简单的,我们可以通过函数与零的距离来衡量函数的复杂度。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,整个数学分支,包括函数分析和巴拿赫空间理论,都在致力于回答这个问题。 一种简单的方法是通过线性函数$f(\mathbf{x}) = \mathbf{w}^\top \mathbf{x}$中的权重向量的某个范数来度量其复杂性,例如$\| \mathbf{w} \|^2$。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中。将原来的训练目标*最小化训练标签上的预测损失*,调整为*最小化预测损失和惩罚项之和*。 现在,如果我们的权重向量增长的太大,我们的学习算法可能会更集中于最小化权重范数$\| \mathbf{w} \|^2$。这正是我们想要的。让我们回顾一下 :numref:`sec_linear_regression` 中的线性回归例子。我们的损失由下式给出: @@ -66,7 +67,7 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` -首先,我们[**像以前一样生成一些数据,生成公式如下:**] +首先,我们[**像以前一样生成一些数据**],生成公式如下: (**$$y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, 0.01^2).$$**) From 3a90849e4872de45113e21e4576778aaa3e76b2e Mon Sep 17 00:00:00 2001 From: thebesttv Date: Thu, 1 Apr 2021 14:51:28 +0800 Subject: [PATCH 023/103] update translations in softmax-regression.md --- chapter_linear-networks/softmax-regression.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/chapter_linear-networks/softmax-regression.md b/chapter_linear-networks/softmax-regression.md index 27020ee2e..2bdd3553f 100644 --- a/chapter_linear-networks/softmax-regression.md +++ b/chapter_linear-networks/softmax-regression.md @@ -13,16 +13,16 @@ * 韩梅梅接下来最有可能看哪部电影? 通常,机器学习实践者用*分类*这个词来描述两个有微妙差别的问题: -(1)我们只对样本的硬性类别感兴趣,即属于哪个类别;(2)我们希望得到软性类别,即得到属于每个类别的概率。这两者的界限往往很模糊。其中的一个原因是,即使我们只关心硬任务,我们仍然使用软任务的模型。 +(1)我们只对样本的硬性类别感兴趣,即属于哪个类别;(2)我们希望得到软性类别,即得到属于每个类别的概率。这两者的界限往往很模糊。其中的一个原因是,即使我们只关心硬类别,我们仍然使用软类别的模型。 ## 分类问题 :label:`subsec_classification-problem` 让我们从一个图像分类问题开始简单尝试一下。每次输入是一个 $2\times2$ 的灰度图像。我们可以用一个标量表示每个像素值,每个图像对应四个特征 $x_1, x_2, x_3, x_4$。此外,让我们假设每个图像属于类别 “猫”,“鸡” 和 “狗” 中的一个。 -接下来,我们要选择如何表示标签。我们有两个明显的选择。也许最直接的想法是选择 $y \in \{1, 2, 3\}$,其中整数分别代表 $\{\text{狗}, \text{猫}, \text{鸡}\}$。这是在计算机上存储此类信息的好方法。如果类别间有一些自然的排序,比如说我们试图预测 $\{\text{婴儿}, \text{儿童}, \text{青少年}, \text{青年人}, \text{中年人}, \text{老年人}\}$,那么将这个问题转变为回归问题并保留这种格式是有意义的。 +接下来,我们要选择如何表示标签。我们有两个明显的选择。也许最直接的想法是选择 $y \in \{1, 2, 3\}$,其中整数分别代表 $\{\text{狗}, \text{猫}, \text{鸡}\}$。这是在计算机上存储此类信息的好方法。如果类别间有一些自然顺序,比如说我们试图预测 $\{\text{婴儿}, \text{儿童}, \text{青少年}, \text{青年人}, \text{中年人}, \text{老年人}\}$,那么将这个问题转变为回归问题并保留这种格式是有意义的。 -但是,一般的分类问题并不与类别之间的自然排序有关。幸运的是,统计学家很早以前就发明了一种表示分类数据的简单方法:*独热编码*(one-hot encoding)。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。 +但是,一般的分类问题并不与类别之间的自然顺序有关。幸运的是,统计学家很早以前就发明了一种表示分类数据的简单方法:*独热编码*(one-hot encoding)。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。 在我们的例子中,标签 $y$ 将是一个三维向量,其中 $(1, 0, 0)$ 对应于 “猫”、$(0, 1, 0)$ 对应于 “鸡”、$(0, 0, 1)$ 对应于 “狗”: $$y \in \{(1, 0, 0), (0, 1, 0), (0, 0, 1)\}.$$ @@ -50,31 +50,31 @@ $$ :label:`fig_softmaxreg` 为了更简洁地表达模型,我们仍然使用线性代数符号。 -通过向量形式表达为 $\mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b}$,这是一种更适合数学和编写代码的形式。我们已经将所有权重放到一个 $3 \times 4$ 矩阵中。对于给定数据样本的特征 $\mathbf{x}$,我们的输出是由权重与输入特征进行矩阵-向量乘法加上偏置$\mathbf{b}$得到的。 +通过向量形式表达为 $\mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b}$,这是一种更适合数学和编写代码的形式。我们已经将所有权重放到一个 $3 \times 4$ 矩阵中。对于给定数据样本的特征 $\mathbf{x}$,我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置$\mathbf{b}$得到的。 ## 全连接层的参数开销 :label:`subsec_parameterization-cost-fc-layers` 正如我们将在后续章节中看到的,在深度学习中,全连接层无处不在。 -然而,顾名思义,全连接层是“完全”连接的。这可能有很多可学习的参数。 +然而,顾名思义,全连接层是“完全”连接的,可能有很多可学习的参数。 具体来说,对于任何具有$d$个输入和$q$个输出的全连接层,参数开销为$\mathcal{O}(dq)$,在实践中可能高得令人望而却步。 -幸运的是,将$d$个输入转换为$q$个输出的成本可以减少到$\mathcal{O}(\frac{dq}{n})$,其中超参数$n$可以由我们灵活指定,以在实际应用中平衡节省参数和模型有效性 :cite:`Zhang.Tay.Zhang.ea.2021` 。 +幸运的是,将$d$个输入转换为$q$个输出的成本可以减少到$\mathcal{O}(\frac{dq}{n})$,其中超参数$n$可以由我们灵活指定,以在实际应用中平衡参数节约和模型有效性 :cite:`Zhang.Tay.Zhang.ea.2021` 。 -## softmax操作 +## softmax运算 :label:`subsec_softmax_operation` 在这里要采取的主要方法是将模型的输出视作为概率。我们将优化参数以最大化观测数据的概率。为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。 我们希望模型的输出 $\hat{y}_j$ 可以视为属于类 $j$ 的概率。然后我们可以选择具有最大输出值的类别$\operatorname*{argmax}_j y_j$作为我们的预测。例如,如果 $\hat{y}_1$、$\hat{y}_2$ 和 $\hat{y}_3$ 分别为 0.1、0.8 和 0.1,那么我们预测的类别是2,在我们的例子中代表 “鸡”。 -你可能会想是否可以将未归一化的预测 $o$ 直接视作我们感兴趣的输出。但是,将线性层的输出直接视为概率时存在一些问题:一方面,没有限制这些数字的总和为1。另一方面,根据输入的不同,它们可以为负值。这些违反了 :numref:`sec_prob` 中所说的概率基本公理。 +你可能会想能否将未归一化的预测 $o$ 直接视作我们感兴趣的输出。但是,将线性层的输出直接视为概率时存在一些问题:一方面,没有限制这些数字的总和为1。另一方面,根据输入的不同,它们可以为负值。这些违反了 :numref:`sec_prob` 中所说的概率基本公理。 -要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。此外,我们需要一个训练目标,来鼓励模型估计概率。在分类器输出0.5的所有样本中,我们希望这些样本有一半实际上属于预测的类。 +要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。此外,我们需要一个训练目标,来鼓励模型精准地估计概率。在分类器输出0.5的所有样本中,我们希望这些样本有一半实际上属于预测的类。 这个属性叫做*校准*(calibration)。 社会科学家邓肯·卢斯于1959年在*选择模型*(choice models)的背景下发明的*softmax函数*正是这样做的。 -为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负数。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式: +为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式: $$\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}$$ :eqlabel:`eq_softmax_y_and_o` @@ -90,13 +90,13 @@ $$ ## 小批量样本的矢量化 :label:`subsec_softmax_vectorization` -为了提高计算效率并且充分利用GPU,我们通常会针对小批量数据执行矢量计算。假设我们读取了一个批量的样本 $\mathbf{X}$ ,其中特征维度(输入数量)为$d$,批量大小为$n$。此外,假设我们在输出中有 $q$ 个类别。设小批量特征为 $\mathbf{X} \in \mathbb{R}^{n \times d}$ ,权重为 $\mathbf{W} \in \mathbb{R}^{d \times q}$,偏置为 $\mathbf{b} \in \mathbb{R}^{1\times q}$。softmax回归的矢量计算表达式为: +为了提高计算效率并且充分利用GPU,我们通常会针对小批量数据执行矢量计算。假设我们读取了一个批量的样本 $\mathbf{X}$ ,其中特征维度(输入数量)为$d$,批量大小为$n$。此外,假设我们在输出中有 $q$ 个类别。那么小批量特征为 $\mathbf{X} \in \mathbb{R}^{n \times d}$ ,权重为 $\mathbf{W} \in \mathbb{R}^{d \times q}$,偏置为 $\mathbf{b} \in \mathbb{R}^{1\times q}$。softmax回归的矢量计算表达式为: $$ \begin{aligned} \mathbf{O} &= \mathbf{X} \mathbf{W} + \mathbf{b}, \\ \hat{\mathbf{Y}} & = \mathrm{softmax}(\mathbf{O}). \end{aligned} $$ :eqlabel:`eq_minibatch_softmax_reg` -相对于一次处理一个样本,小批量样本的矢量化加快了 $\mathbf{X}和\mathbf{W}$ 的矩阵-向量乘法运算。由于$\mathbf{X}$ 中的每一行代表一个数据样本,且softmax操作本身可以*按行*(rowwise)执行。所以,对于$\mathbf{O}$的每一行,我们首先对所有项进行幂运算,然后通过求和对它们进行标准化。 -在 :eqref:`eq_minibatch_softmax_reg` 中$\mathbf{X} \mathbf{W} + \mathbf{b}$的求和时会使用广播,小批量的未归一化预测 $\mathbf{O}$ 和输出概率 $\hat{\mathbf{Y}}$ 都是形状为 $n \times q$ 的矩阵。 +相对于一次处理一个样本,小批量样本的矢量化加快了 $\mathbf{X}和\mathbf{W}$ 的矩阵-向量乘法。由于 $\mathbf{X}$ 中的每一行代表一个数据样本,所以softmax运算可以*按行*(rowwise)执行:对于$\mathbf{O}$的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。 +在 :eqref:`eq_minibatch_softmax_reg` 中 $\mathbf{X} \mathbf{W} + \mathbf{b}$ 的求和会使用广播,小批量的未归一化预测 $\mathbf{O}$ 和输出概率 $\hat{\mathbf{Y}}$ 都是形状为 $n \times q$ 的矩阵。 ## 损失函数 @@ -129,7 +129,7 @@ $$ l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j. $$ ### softmax及其导数 :label:`subsec_softmax_and_derivatives` -由于softmax和相关的损失函数很常见,因此值得更好地理解它的计算方式。将 :eqref:`eq_softmax_y_and_o` 代入损失 :eqref:`eq_l_cross_entropy` 中。利用softmax的定义,我们得到: +由于softmax和相关的损失函数很常见,因此值得我们更好地理解它的计算方式。将 :eqref:`eq_softmax_y_and_o` 代入损失 :eqref:`eq_l_cross_entropy` 中。利用softmax的定义,我们得到: $$ \begin{aligned} @@ -145,7 +145,7 @@ $$ \partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \mathrm{softmax}(\mathbf{o})_j - y_j. $$ -换句话说,导数是我们模型分配的概率(由softmax得到)与实际发生的情况(由独热标签向量表示)之间的差异。从这个意义上讲,与我们在回归中看到的非常相似,其中梯度是观测值$y$和估计值$\hat{y}$之间的差异。这不是巧合,在任何指数族分布(参见 [关于分布的在线附录](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/distributions.html))模型中,对数似然的梯度正是由这给出的。这使计算梯度在实践中变得容易。 +换句话说,导数是我们模型分配的概率(由softmax得到)与实际发生的情况(由独热标签向量表示)之间的差异。从这个意义上讲,与我们在回归中看到的非常相似,其中梯度是观测值$y$和估计值$\hat{y}$之间的差异。这不是巧合,在任何指数族分布(参见 [关于分布的在线附录](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/distributions.html))模型中,对数似然的梯度正是由这给出的。这使梯度计算在实践中变得容易。 ### 交叉熵损失 @@ -166,7 +166,7 @@ $$H[P] = \sum_j - P(j) \log P(j).$$ 信息论的基本定理之一指出,为了对从分布 $p$ 中随机抽取的数据进行编码,我们至少需要 $H[P]$ “纳特(nat)” 对其进行编码。“纳特”相当于位,但是对数底为$e$而不是2。因此,一个纳特是 $\frac{1}{\log(2)} \approx 1.44$ 位。 -### 惊讶 +### 惊异 你可能想知道压缩与预测有什么关系。想象一下,我们有一个要压缩的数据流。如果我们总是很容易预测下一个数据,那么这个数据很容易压缩!举一个极端的例子,数据流中的每个数据总是采用相同的值。这是一个非常无聊的数据流!由于它们总是相同的,所以很容易被预测,所以我们为了传递数据流的内容不必传输任何信息。当数据易于预测,也就易于压缩。 From 19881b5b0bedc9f952d6c3400d1dfc07555b0cda Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Sun, 4 Apr 2021 12:37:16 +0800 Subject: [PATCH 024/103] Correct some translation errors. (#717) * Correct some translation errors. Easy to read and understand. Complete some equation description. * fix error in nadaraya-waston.md * fix translation errors in attention-scoring-functions.md * nadaraya-waston.md fix errors add chinese punctuation translate the code comment * attention-scoring-functions.md fix errors add chinese punctuation translate the code comment --- .../attention-scoring-functions.md | 122 +++++++------ .../nadaraya-waston.md | 161 ++++++++---------- 2 files changed, 133 insertions(+), 150 deletions(-) diff --git a/chapter_attention-mechanisms/attention-scoring-functions.md b/chapter_attention-mechanisms/attention-scoring-functions.md index 994e2c360..7d4b7e71e 100644 --- a/chapter_attention-mechanisms/attention-scoring-functions.md +++ b/chapter_attention-mechanisms/attention-scoring-functions.md @@ -1,14 +1,15 @@ -# 注意评分功能 +# 注意力评分函数 + :label:`sec_attention-scoring-functions` -在 :numref:`sec_nadaraya-waston` 中,我们使用高斯内核来模拟查询和键之间的交互。将 :eqref:`eq_nadaraya-waston-gaussian` 中的高斯内核的指数视为 * 注意力评分函数 *(简称 * 评分函数 *),这个函数的结果基本上被输入了 softmax 操作。因此,我们获得了与键配对的值的概率分布(注意力权重)。最后,注意力集中的输出只是基于这些注意力权重的值的加权总和。 +在 :numref:`sec_nadaraya-waston` 中,我们使用高斯核来模拟查询和键之间的交互。将 :eqref:`eq_nadaraya-waston-gaussian` 中的高斯核的指数视为 * 注意力评分函数 *(简称 * 评分函数 *),这个函数的结果基本上被输入了 softmax 操作。因此,我们获得了与键配对的值的概率分布(注意力权重)。最后,注意力集中的输出只是基于这些注意力权重的值的加权总和。 -从较高层面来说,我们可以使用上述算法实例化 :numref:`fig_qkv` 中的注意机制框架。:numref:`fig_attention_output` 表示 $a$ 的注意力评分函数,说明了如何将注意力集中的输出计算为加权值和。由于注意力权重是概率分布,因此加权总和基本上是加权平均值。 +从较高层面来说,我们可以使用上述算法实例化 :numref:`fig_qkv` 中的注意力机制框架。:numref:`fig_attention_output` 表示 $a$ 的注意力评分函数,说明了如何将注意力池化的输出计算为加权值和。由于注意力权重是概率分布,因此加权和本质上是加权平均值。 ![Computing the output of attention pooling as a weighted average of values.](../img/attention-output.svg) :label:`fig_attention_output` -从数学上讲,假设我们有一个查询 $\mathbf{q} \in \mathbb{R}^q$ 和 $m$ 键值对 $(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)$,其中任何 $\mathbf{k}_i \in \mathbb{R}^k$ 和任何 $\mathbf{v}_i \in \mathbb{R}^v$。注意力池 $f$ 被实例化为值的加权总和: +从数学上讲,假设我们有一个查询 $\mathbf{q} \in \mathbb{R}^q$ 和 $m$ 个“键-值”对 $(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)$,其中任何 $\mathbf{k}_i \in \mathbb{R}^k$ 和任何 $\mathbf{v}_i \in \mathbb{R}^v$。注意力池化函数 $f$ 被实例化为值的加权总和: $$f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)) = \sum_{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i \in \mathbb{R}^v,$$ :eqlabel:`eq_attn-pooling` @@ -18,7 +19,7 @@ $$f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_ $$\alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(a(\mathbf{q}, \mathbf{k}_i))}{\sum_{j=1}^m \exp(a(\mathbf{q}, \mathbf{k}_j))} \in \mathbb{R}.$$ :eqlabel:`eq_attn-scoring-alpha` -正如我们所看到的,注意力评分功能 $a$ 的不同选择导致不同的注意力集中行为。在本节中,我们将介绍两个流行的评分功能,我们稍后将用来开发更复杂的注意力机制。 +正如我们所看到的,注意力评分函数 $a$ 的不同选择导致不同的注意力池化行为。在本节中,我们将介绍两个流行的评分函数,稍后将用来开发更复杂的注意力机制。 ```{.python .input} import math @@ -36,14 +37,14 @@ import torch from torch import nn ``` -## 蒙面 Softmax 操作 +## 掩码 Softmax 操作 -正如我们刚才提到的,softmax 运算用于输出概率分布作为注意力权重。在某些情况下,并非所有的价值都应该被纳入注意力集中。例如,为了在 :numref:`sec_machine_translation` 中高效处理微型批量,某些文本序列填充了没有意义的特殊令牌。为了将注意力集中在仅作为值的有意义的令牌上,我们可以指定一个有效的序列长度(以令牌数表示),以便在计算 softmax 时过滤掉超出此指定范围的那些。通过这种方式,我们可以在下面的 `masked_softmax` 函数中实现这样的 * 掩码 softmax 操作 *,其中任何超出有效长度的值都被掩盖为零。 +正如上面提到的,softmax 运算用于输出概率分布作为注意力权重。在某些情况下,并非所有的价值都应该被纳入注意力池化。例如,为了在 :numref:`sec_machine_translation` 中高效处理小批量数据集,某些文本序列填充了没有意义的特殊令牌。为了将注意力池化在仅作为值的有意义的令牌上,可以指定一个有效的序列长度(以令牌数表示),以便在计算 softmax 时过滤掉超出此指定范围的那些。通过这种方式,我们可以在下面的 `masked_softmax` 函数中实现这样的 * 掩码 softmax 操作 *,其中任何超出有效长度的值都被掩盖为零。 ```{.python .input} #@save def masked_softmax(X, valid_lens): - """Perform softmax operation by masking elements on the last axis.""" + """通过在最后一个轴上掩盖元素来执行 Softmax 操作""" # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor if valid_lens is None: return npx.softmax(X) @@ -53,8 +54,7 @@ def masked_softmax(X, valid_lens): valid_lens = valid_lens.repeat(shape[1]) else: valid_lens = valid_lens.reshape(-1) - # On the last axis, replace masked elements with a very large negative - # value, whose exponentiation outputs 0 + # 在最后的轴上,被掩盖的元素使用一个非常大的负值替换,从而其指数输出为 0 X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, True, value=-1e6, axis=1) return npx.softmax(X).reshape(shape) @@ -64,7 +64,7 @@ def masked_softmax(X, valid_lens): #@tab pytorch #@save def masked_softmax(X, valid_lens): - """Perform softmax operation by masking elements on the last axis.""" + """通过在最后一个轴上掩盖元素来执行 Softmax 操作""" # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor if valid_lens is None: return nn.functional.softmax(X, dim=-1) @@ -74,14 +74,13 @@ def masked_softmax(X, valid_lens): valid_lens = torch.repeat_interleave(valid_lens, shape[1]) else: valid_lens = valid_lens.reshape(-1) - # On the last axis, replace masked elements with a very large negative - # value, whose exponentiation outputs 0 + # 在最后的轴上,被掩盖的元素使用一个非常大的负值替换,从而其指数输出为 0 X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, value=-1e6) return nn.functional.softmax(X.reshape(shape), dim=-1) ``` -为了演示此函数的工作原理,请考虑由两个 $2 \times 4$ 矩阵示例组成的小批量,其中这两个示例的有效长度分别为 2 和 3 个。由于蒙面 softmax 操作,超出有效长度的值都被掩盖为零。 +为了演示此函数的工作原理,请考虑由两个 $2 \times 4$ 矩阵示例组成的小批量数据集,其中这两个示例的有效长度分别为 2 和 3 个。经过掩码 softmax 操作,超出有效长度的值都被掩盖为零。 ```{.python .input} masked_softmax(np.random.uniform(size=(2, 2, 4)), d2l.tensor([2, 3])) @@ -104,24 +103,24 @@ masked_softmax(np.random.uniform(size=(2, 2, 4)), masked_softmax(torch.rand(2, 2, 4), d2l.tensor([[1, 3], [2, 4]])) ``` -## 添加剂注意 +## 加性注意力 + :label:`subsec_additive-attention` -一般来说,当查询和键是不同长度的矢量时,我们可以使用附加注意力作为评分功能。给定查询 $\mathbf{q} \in \mathbb{R}^q$ 和关键 $\mathbf{k} \in \mathbb{R}^k$,* 加法注意 * 评分功能 +一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数。给定查询 $\mathbf{q} \in \mathbb{R}^q$ 和键 $\mathbf{k} \in \mathbb{R}^k$,* 加性注意力 * 评分函数为 $$a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R},$$ :eqlabel:`eq_additive-attn` -其中可学习的参数 $\mathbf W_q\in\mathbb R^{h\times q}$、$\mathbf W_k\in\mathbb R^{h\times k}$ 和 $\mathbf w_v\in\mathbb R^{h}$。相当于 :eqref:`eq_additive-attn`,查询和密钥被连接在一个 MLP 中,其中包含一个隐藏层,其隐藏单位的数量为 $h$,这是一个超参数。通过使用 $\tanh$ 作为激活函数和禁用偏见术语,我们将在以下内容中实现附加注意。 +其中可学习的参数 $\mathbf W_q\in\mathbb R^{h\times q}$、$\mathbf W_k\in\mathbb R^{h\times k}$ 和 $\mathbf w_v\in\mathbb R^{h}$。等价于 :eqref:`eq_additive-attn`,查询和键被连接在一个 MLP 中,其中包含一个隐藏层,其隐藏单位的数量为 $h$,这是一个超参数。在下面实现加性注意力时,使用 $\tanh$ 作为激活函数,并且禁用偏差项。 ```{.python .input} #@save class AdditiveAttention(nn.Block): - """Additive attention.""" + """加性注意力""" def __init__(self, num_hiddens, dropout, **kwargs): super(AdditiveAttention, self).__init__(**kwargs) - # Use `flatten=False` to only transform the last axis so that the - # shapes for the other axes are kept the same + # 使用' flatten=False '只转换最后一个轴,以便其他轴的形状保持不变 self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False) self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False) self.w_v = nn.Dense(1, use_bias=False, flatten=False) @@ -129,20 +128,18 @@ class AdditiveAttention(nn.Block): def forward(self, queries, keys, values, valid_lens): queries, keys = self.W_q(queries), self.W_k(keys) - # After dimension expansion, shape of `queries`: (`batch_size`, no. of - # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, - # no. of key-value pairs, `num_hiddens`). Sum them up with - # broadcasting + # 在维度扩展后, + # `queries` 的形状:(`batch_size`, 查询的个数, 1, `num_hidden`) + # `key` 的形状:(`batch_size`, 1, “键-值”对的个数, `num_hiddens`) + # 使用广播方式进行求和 features = np.expand_dims(queries, axis=2) + np.expand_dims( keys, axis=1) features = np.tanh(features) - # There is only one output of `self.w_v`, so we remove the last - # one-dimensional entry from the shape. Shape of `scores`: - # (`batch_size`, no. of queries, no. of key-value pairs) + # `self.w_v` 仅有一个输出,因此从形状中移除那个维度。 + # `scores` 的形状:(`batch_size`, 查询的个数, “键-值”对的个数) scores = np.squeeze(self.w_v(features), axis=-1) self.attention_weights = masked_softmax(scores, valid_lens) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) + # `values` 的形状:(`batch_size`, “键-值”对的个数, 维度值) return npx.batch_dot(self.dropout(self.attention_weights), values) ``` @@ -150,6 +147,7 @@ class AdditiveAttention(nn.Block): #@tab pytorch #@save class AdditiveAttention(nn.Module): + """加性注意力""" def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): super(AdditiveAttention, self).__init__(**kwargs) self.W_k = nn.Linear(key_size, num_hiddens, bias=False) @@ -159,27 +157,25 @@ class AdditiveAttention(nn.Module): def forward(self, queries, keys, values, valid_lens): queries, keys = self.W_q(queries), self.W_k(keys) - # After dimension expansion, shape of `queries`: (`batch_size`, no. of - # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, - # no. of key-value pairs, `num_hiddens`). Sum them up with - # broadcasting + # 在维度扩展后, + # `queries` 的形状:(`batch_size`, 查询的个数, 1, `num_hidden`) + # `key` 的形状:(`batch_size`, 1, “键-值”对的个数, `num_hiddens`) + # 使用广播方式进行求和 features = queries.unsqueeze(2) + keys.unsqueeze(1) features = torch.tanh(features) - # There is only one output of `self.w_v`, so we remove the last - # one-dimensional entry from the shape. Shape of `scores`: - # (`batch_size`, no. of queries, no. of key-value pairs) + # `self.w_v` 仅有一个输出,因此从形状中移除那个维度。 + # `scores` 的形状:(`batch_size`, 查询的个数, “键-值”对的个数) scores = self.w_v(features).squeeze(-1) self.attention_weights = masked_softmax(scores, valid_lens) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) + # `values` 的形状:(`batch_size`, “键-值”对的个数, 维度值) return torch.bmm(self.dropout(self.attention_weights), values) ``` -让我们用一个玩具示例来演示上面的 `AdditiveAttention` 类,其中查询、键和值的形状(批量大小、步数或令牌序列长度、特征大小)分别为(73229293618、$1$、$20$)、($10$、$2$、$2$)和(73229293615、$2$、$10$)和(73229293615、$10$)和(73229293615、$10$、$10$)和(73229293615、$10$、$10$),$4$)。注意力池输出的形状为(批量大小、查询的步骤数、值的要素大小)。 +让我们用一个小例子来演示上面的 `AdditiveAttention` 类,其中查询、键和值的形状(批量大小、步数或令牌序列长度、特征大小)分别为 $(2,1,20)$、$(2,10,2)$ 和 $(2,10,4)。注意力池化输出的形状为(批量大小、查询的步骤数、值的特征大小)。 ```{.python .input} queries, keys = d2l.normal(0, 1, (2, 1, 20)), d2l.ones((2, 10, 2)) -# The two value matrices in the `values` minibatch are identical +# `values` 的小批量数据集中,两个值矩阵是相同的 values = np.arange(40).reshape(1, 10, 4).repeat(2, axis=0) valid_lens = d2l.tensor([2, 6]) @@ -191,7 +187,7 @@ attention(queries, keys, values, valid_lens) ```{.python .input} #@tab pytorch queries, keys = d2l.normal(0, 1, (2, 1, 20)), d2l.ones((2, 10, 2)) -# The two value matrices in the `values` minibatch are identical +# `values` 的小批量数据集中,两个值矩阵是相同的 values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat( 2, 1, 1) valid_lens = d2l.tensor([2, 6]) @@ -202,7 +198,7 @@ attention.eval() attention(queries, keys, values, valid_lens) ``` -尽管加法注意力包含可学习的参数,但由于本例中每个键都是相同的,所以注意力权重是一致的,由指定的有效长度决定。 +尽管加性注意力包含可学习的参数,但由于本例中每个键都是相同的,所以注意力权重是一致的,由指定的有效长度决定。 ```{.python .input} #@tab all @@ -210,18 +206,18 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), xlabel='Keys', ylabel='Queries') ``` -## 缩放点-产品关注 +## 缩放的“点-积”注意力 -计分功能的计算效率更高的设计可以简单地是点积。但是,点积操作要求查询和键具有相同的矢量长度,比如 $d$。假设查询的所有元素和关键字都是独立的随机变量,均值和单位方差零。两个向量的点积均值为零,方差为 $d$。为确保无论矢量长度如何,点积的方差仍然是一个,* 缩放的点积注意 * 评分功能 +设计具有更高计算效率的评分函数可以使用“点-积”。但是,“点-积”操作要求查询和键具有相同的矢量长度。假设查询和键的所有元素都是独立的随机变量,并且都是零均值和单位方差。两个向量的“点-积”均值为零,方差为 $d$。为确保无论矢量长度如何,“点-积”的方差在不考虑向量长度的情况下仍然是 $1$,则 * 缩放的“点-积”注意力 * 评分函数 $$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}$$ -将点积除以 $\sqrt{d}$。在实践中,我们通常以微型批量来考虑提高效率,例如 $n$ 查询和 $m$ 键值对的计算注意力,其中查询和键的长度为 $d$,值的长度为 $v$。查询 $\mathbf Q\in\mathbb R^{n\times d}$、键 $\mathbf K\in\mathbb R^{m\times d}$ 和值 $\mathbf V\in\mathbb R^{m\times v}$ 的扩展点-产品关注度是 +将“点-积”除以 $\sqrt{d}$。在实践中,我们通常以小批量来考虑提高效率,例如 $n$ 个查询和 $m$ 个“键-值”对下计算注意力,其中查询和键的长度为 $d$,值的长度为 $v$。查询 $\mathbf Q\in\mathbb R^{n\times d}$、键 $\mathbf K\in\mathbb R^{m\times d}$ 和值 $\mathbf V\in\mathbb R^{m\times v}$ 的缩放的“点-积”注意力是 $$ \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}.$$ :eqlabel:`eq_softmax_QK_V` -在以下缩放点产品注意事项的实施中,我们使用了 dropout 进行模型正则化。 +在下面的缩放的“点-积”注意力的实现中,我们使用了 dropout 进行模型正则化。 ```{.python .input} #@save @@ -231,14 +227,13 @@ class DotProductAttention(nn.Block): super(DotProductAttention, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) - # Shape of `queries`: (`batch_size`, no. of queries, `d`) - # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) - # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) + # `queries` 的形状:(`batch_size`, 查询的个数, `d`) + # `keys` 的形状:(`batch_size`, “键-值”对的个数, `d`) + # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) + # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`, 查询的个数) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] - # Set `transpose_b=True` to swap the last two dimensions of `keys` + # 设置 `transpose_b=True` 为了交换 `keys` 的最后两个维度 scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d) self.attention_weights = masked_softmax(scores, valid_lens) return npx.batch_dot(self.dropout(self.attention_weights), values) @@ -253,20 +248,19 @@ class DotProductAttention(nn.Module): super(DotProductAttention, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) - # Shape of `queries`: (`batch_size`, no. of queries, `d`) - # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) - # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) + # `queries` 的形状:(`batch_size`, 查询的个数, `d`) + # `keys` 的形状:(`batch_size`, “键-值”对的个数, `d`) + # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) + # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`, 查询的个数) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] - # Set `transpose_b=True` to swap the last two dimensions of `keys` + # 设置 `transpose_b=True` 为了交换 `keys` 的最后两个维度 scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d) self.attention_weights = masked_softmax(scores, valid_lens) return torch.bmm(self.dropout(self.attention_weights), values) ``` -为了演示上述 `DotProductAttention` 类别,我们使用与先前玩具示例相同的键、值和有效长度进行附加注意。对于点积操作,我们将查询的特征大小与键的特征大小相同。 +为了演示上述 `DotProductAttention` 类,我们使用与先前加性注意力的小例子中相同的键、值和有效长度。对于“点-积”操作,我们将查询的特征大小与键的特征大小相同。 ```{.python .input} queries = d2l.normal(0, 1, (2, 1, 2)) @@ -283,7 +277,7 @@ attention.eval() attention(queries, keys, values, valid_lens) ``` -与加法注意力演示相同,由于 `keys` 包含无法通过任何查询区分的相同元素,因此获得了统一的注意力权重。 +与加性注意力演示相同,由于键包含的是相同的元素,而这些元素无法通过任何查询进行区分,因此获得了统一的注意力权重。 ```{.python .input} #@tab all @@ -293,14 +287,14 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), ## 摘要 -* 我们可以将注意力集中的输出计算为值的加权平均值,其中注意力评分功能的不同选择会导致不同的注意力集中行为。 -* 当查询和密钥是不同长度的矢量时,我们可以使用加法注意力评分功能。当它们相同时,缩放的点-产品注意力评分功能在计算上更有效率。 +* 可以将注意力池化的输出计算作为值的加权平均值,其中注意力评分函数的不同选择会导致不同的注意力池化行为。 +* 当查询和键是不同长度的矢量时,我们可以使用加性注意力评分函数。当它们相同时,缩放的“点-积”注意力评分函数在计算上更有效率。 ## 练习 -1. 修改玩具示例中的按键并可视化注意力重量。添加剂的注意力和缩放的点-产品的注意力是否仍然产生相同的注意力?为什么或为什么不? +1. 修改小例子中的键,并且可视化注意力权重。加性注意力和缩放的“点-积”注意力是否仍然产生相同的注意力?为什么? 1. 只使用矩阵乘法,您能否为具有不同矢量长度的查询和键设计新的评分函数? -1. 当查询和键具有相同的矢量长度时,矢量求和是否比计分函数的点积更好?为什么或为什么不? +1. 当查询和键具有相同的矢量长度时,矢量求和是否比评分函数的“点-积”更好?为什么? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/346) diff --git a/chapter_attention-mechanisms/nadaraya-waston.md b/chapter_attention-mechanisms/nadaraya-waston.md index 4ec3a5b18..0114caf94 100644 --- a/chapter_attention-mechanisms/nadaraya-waston.md +++ b/chapter_attention-mechanisms/nadaraya-waston.md @@ -1,7 +1,8 @@ -# 注意力集中:Nadaraya-Watson 内核回归 +# 注意力池化:Nadaraya-Watson 内核回归 + :label:`sec_nadaraya-waston` -现在你知道了 :numref:`fig_qkv` 框架下关注机制的主要组成部分。概括一下,查询(名义提示)和键(非自豪提示)之间的交互导致了 * 注意力集中 *。注意力集中有选择性地聚合了值(感官输入)以产生产出。在本节中,我们将更详细地介绍注意力集中,以便让您从高层次了解注意力机制在实践中的运作方式。具体来说,1964 年提出的 Nadaraya-Watson 内核回归模型是一个简单而完整的示例,用于演示具有注意机制的机器学习。 +在知道了 :numref:`fig_qkv` 框架下注意力机制的主要组成部分。重述要点,查询(自主提示)和键(非自主提示)之间的交互产生了 * 注意力池化 *。注意力池化有选择性地聚合了值(感官输入)以产生输出。在本节中,我们将更详细地介绍注意力池化,以便从高层次上了解注意力机制在实践中的运作方式。具体来说,1964 年提出的 Nadaraya-Watson 核回归模型是一个简单而完整的示例,用于演示具有注意机制的机器学习。 ```{.python .input} from d2l import mxnet as d2l @@ -20,23 +21,23 @@ from torch import nn ## 生成数据集 -为了简单起见,让我们考虑以下回归问题:给定输入-产出对 $\{(x_1, y_1), \ldots, (x_n, y_n)\}$ 的数据集,如何学习 $f$ 来预测任何新输入 $\hat{y} = f(x)$ 的输出 $\hat{y} = f(x)$? +简单起见,考虑回归问题:给定“输入-输出”对 $\{(x_1, y_1), \ldots, (x_n, y_n)\}$ 的数据集,如何学习 $f$ 来预测任何新的输入 $x$ 的输出 $\hat{y} = f(x)$? -在这里,我们根据以下非线性函数生成一个人工数据集,噪声术语 $\epsilon$: +根据下面的非线性函数生成一个人工数据集,噪声项为 $\epsilon$: -$$y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon,$$ +$$y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon,\epsilon\sim\mathcal{N}(0,0.5)$$ -其中 $\epsilon$ 服从平均值和标准差 0.5 的正态分布。同时生成了 50 个培训示例和 50 个测试示例。为了以后更好地直观地显示注意力模式,训练输入将进行排序。 +其中 $\epsilon$ 服从均值为 0 和标准差为 0.5 的正态分布。同时生成了 50 个训练样本和 50 个测试样本。为了更好地可视化注意力模式,输入的训练样本将进行排序。 ```{.python .input} -n_train = 50 # No. of training examples -x_train = np.sort(d2l.rand(n_train) * 5) # Training inputs +n_train = 50 # 训练样本的个数 +x_train = np.sort(d2l.rand(n_train) * 5) # 训练样本的输入 ``` ```{.python .input} #@tab pytorch -n_train = 50 # No. of training examples -x_train, _ = torch.sort(d2l.rand(n_train) * 5) # Training inputs +n_train = 50 # 训练样本的个数 +x_train, _ = torch.sort(d2l.rand(n_train) * 5) # 训练样本的输入 ``` ```{.python .input} @@ -44,14 +45,14 @@ x_train, _ = torch.sort(d2l.rand(n_train) * 5) # Training inputs def f(x): return 2 * d2l.sin(x) + x**0.8 -y_train = f(x_train) + d2l.normal(0.0, 0.5, (n_train,)) # Training outputs -x_test = d2l.arange(0, 5, 0.1) # Testing examples -y_truth = f(x_test) # Ground-truth outputs for the testing examples -n_test = len(x_test) # No. of testing examples +y_train = f(x_train) + d2l.normal(0.0, 0.5, (n_train,)) # 训练样本的输出 +x_test = d2l.arange(0, 5, 0.1) # 测试样本 +y_truth = f(x_test) # 测试样本的真实输出 +n_test = len(x_test) # 测试样本的个数 n_test ``` -以下函数绘制所有训练示例(由圆表示)、不带噪声项的地面真实数据生成函数 `f`(标记为 “Truth”)和学习的预测函数(标记为 “Pred”)。 +以下函数绘制所有训练样本(由圆圈表示)、不带噪声项的真实数据生成函数 `f`(标记为 “Truth”)和学习得到的预测函数(标记为 “Pred”)。 ```{.python .input} #@tab all @@ -61,14 +62,14 @@ def plot_kernel_reg(y_hat): d2l.plt.plot(x_train, y_train, 'o', alpha=0.5); ``` -## 平均池 +## 平均池化 -我们首先可能是世界上对这个回归问题的 “最愚蠢” 的估算器:使用平均汇集来计算所有训练输出的平均值: +首先,可能是世界上对这个回归问题的 “最愚蠢” 的估算器:使用平均池化来计算所有训练输出的平均值: $$f(x) = \frac{1}{n}\sum_{i=1}^n y_i,$$ :eqlabel:`eq_avg-pooling` -这如下图所示。正如我们所看到的,这个估算器确实不那么聪明。 +如下图所示,这个估算器确实不聪明。 ```{.python .input} y_hat = y_train.mean().repeat(n_test) @@ -81,21 +82,21 @@ y_hat = torch.repeat_interleave(y_train.mean(), n_test) plot_kernel_reg(y_hat) ``` -## 非参数化注意力池 +## 非参数的注意力池化 -显然,平均池忽略了输入 $x_i$。Nadaraya :cite:`Nadaraya.1964` 和 Waston :cite:`Watson.1964` 提出了一个更好的想法,根据输入位置对输出 $y_i$ 进行权衡: +显然,平均池化模型忽略了输入 $x_i$。一个更好的想法由 Nadaraya :cite:`Nadaraya.1964` 和 Waston :cite:`Watson.1964` 提出,根据输入的位置对输出 $y_i$ 进行权衡: $$f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i,$$ :eqlabel:`eq_nadaraya-waston` -其中 $K$ 是 * 内核 *。:eqref:`eq_nadaraya-waston` 中的估计器被称为 *Nadaraya-Watson 内核回归 *。在这里我们不会深入研究内核的细节。回想一下 :numref:`fig_qkv` 中的关注机制框架。从注意力的角度来看,我们可以用更广泛的 * 注意力集合 * 的形式重写 :eqref:`eq_nadaraya-waston`: +其中 $K$ 是 * 核函数 *。:eqref:`eq_nadaraya-waston` 中的估计器被称为 *Nadaraya-Watson 核回归 *。在这里我们不会深入讨论核的细节。回想一下 :numref:`fig_qkv` 中的注意力机制框架。从注意力的角度来看,我们可以用 * 注意力池化 * 的更一般的形式重写 :eqref:`eq_nadaraya-waston`: $$f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i,$$ :eqlabel:`eq_attn-pooling` -其中 $x$ 是查询,$(x_i, y_i)$ 是键值对。比较 :eqref:`eq_attn-pooling` 和 :eqref:`eq_avg-pooling`,这里的注意力集中是 $y_i$ 的加权平均值。根据查询 $x$ 和 $\alpha$ 建模的密钥 $x_i$ 之间的交互作用,将 :eqref:`eq_attn-pooling` 中的 * 注意力权重 * $\alpha(x, x_i)$ 分配给相应的值 $y_i$。对于任何查询,它在所有键值对上的注意力权重都是有效的概率分布:它们是非负数的,总和为一。 +其中 $x$ 是查询,$(x_i, y_i)$ 是“键-值”对。比较 :eqref:`eq_attn-pooling` 和 :eqref:`eq_avg-pooling`,这里的注意力池化是 $y_i$ 的加权平均值。根据被 $\alpha$ 模型化的查询 $x$ 和键 $x_i$ 之间的交互作用,将 :eqref:`eq_attn-pooling` 中的 * 注意力权重 * $\alpha(x, x_i)$ 分配给相应的值 $y_i$。对于任何查询,模型在所有“键-值”对上的注意力权重都是有效的概率分布:它们是非负数的,总和为一。 -要获得注意力集中的直觉,只需考虑一个 * 高斯内核 * 定义为 +为了获得注意力池化的直觉认识,仅需要考虑一个 * 高斯内核 * ,其定义为 $$ K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}). @@ -106,41 +107,36 @@ $$ $$\begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}(x - x_i)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}(x - x_j)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}(x - x_i)^2\right) y_i. \end{aligned}$$ :eqlabel:`eq_nadaraya-waston-gaussian` -在 :eqref:`eq_nadaraya-waston-gaussian` 中,接近给定查询 $x$ 的密钥 $x_i$ 将得到 -*通过分配给密钥的相应值 $y_i$ 的 * 更大的注意力重量 * 来进一步注意 *。 +在 :eqref:`eq_nadaraya-waston-gaussian` 中,通过给键 $x_i$ 对应的值 $y_i$ 分配更多的注意力权重使得与查询 $x$ 越接近的键能够获得更多的注意力。 -值得注意的是,Nadaraya-Watson 内核回归是一个非参数模型;因此,:eqref:`eq_nadaraya-waston-gaussian` 就是 * 非参数化注意力池 * 的示例。在下面,我们基于此非参数化关注模型绘制预测。预测的线是平稳的,并且比普通集中产生的线更接近地面真相。 +尤其是 Nadaraya-Watson 核回归是一个非参数模型;因此,:eqref:`eq_nadaraya-waston-gaussian` 就是 * 非参数的注意力池化 * 的示例。在下面,我们将基于这个非参数的注意力池化模型绘制预测。预测的线是平稳的,并且比平均池化产生的线更接近真实数据。 ```{.python .input} -# Shape of `X_repeat`: (`n_test`, `n_train`), where each row contains the -# same testing inputs (i.e., same queries) +# `X_repeat` 的形状: (`n_test`, `n_train`), +# 每一行都包含着相同的测试输入(例如:同样的查询) X_repeat = d2l.reshape(x_test.repeat(n_train), (-1, n_train)) -# Note that `x_train` contains the keys. Shape of `attention_weights`: -# (`n_test`, `n_train`), where each row contains attention weights to be -# assigned among the values (`y_train`) given each query +# `x_train` 包含着键。`attention_weights` 的形状:(`n_test`, `n_train`), +# 每一行都包含着要在给定的每个查询的值(`y_train`)之间分配的注意力权重 attention_weights = npx.softmax(-(X_repeat - x_train)**2 / 2) -# Each element of `y_hat` is weighted average of values, where weights are -# attention weights +# `y_hat` 的每个元素都是值的加权平均值,其中的权重是注意力权重 y_hat = d2l.matmul(attention_weights, y_train) plot_kernel_reg(y_hat) ``` ```{.python .input} #@tab pytorch -# Shape of `X_repeat`: (`n_test`, `n_train`), where each row contains the -# same testing inputs (i.e., same queries) +# `X_repeat` 的形状: (`n_test`, `n_train`), +# 每一行都包含着相同的测试输入(例如:同样的查询) X_repeat = d2l.reshape(x_test.repeat_interleave(n_train), (-1, n_train)) -# Note that `x_train` contains the keys. Shape of `attention_weights`: -# (`n_test`, `n_train`), where each row contains attention weights to be -# assigned among the values (`y_train`) given each query +# `x_train` 包含着键。`attention_weights` 的形状:(`n_test`, `n_train`), +# 每一行都包含着要在给定的每个查询的值(`y_train`)之间分配的注意力权重 attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1) -# Each element of `y_hat` is weighted average of values, where weights are -# attention weights +# `y_hat` 的每个元素都是值的加权平均值,其中的权重是注意力权重 y_hat = d2l.matmul(attention_weights, y_train) plot_kernel_reg(y_hat) ``` -现在让我们来看看注意力的权重。这里测试输入是查询,而训练输入是关键。由于两个输入都是排序的,我们可以看到查询键对越接近,注意力集中的注意力就越高。 +现在,让我们来观察注意力的权重。这里测试输入是查询,而训练输入是键。由于两个输入都是排过序的,观察可知“查询-键”对越接近,注意力池化的注意力权重就越高。 ```{.python .input} d2l.show_heatmaps(np.expand_dims(np.expand_dims(attention_weights, 0), 0), @@ -155,23 +151,24 @@ d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), ylabel='Sorted testing inputs') ``` -## 参数化注意力池 +## 带参数的注意力池化 -非参数 Nadaraya-Watson 内核回归具有 * 一致性 * 的好处:如果有足够的数据,此模型会收敛到最佳解决方案。尽管如此,我们可以轻松地将可学习的参数集成到注意力池中。 +非参数的 Nadaraya-Watson 核回归的 * 一致性 * :如果有足够的数据,此模型会收敛到最佳解决方案。尽管如此,我们还可以轻松地将可学习的参数集成到注意力池化中。 例如,与 :eqref:`eq_nadaraya-waston-gaussian` 略有不同,在下面的查询 $x$ 和键 $x_i$ 之间的距离乘以可学习参数 $w$: $$\begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_i)w)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.\end{aligned}$$ :eqlabel:`eq_nadaraya-waston-gaussian-para` -在本节的其余部分中,我们将通过学习 :eqref:`eq_nadaraya-waston-gaussian-para` 中注意力集中的参数来训练此模型。 +在本节的剩余部分,我们将通过学习 :eqref:`eq_nadaraya-waston-gaussian-para` 中注意力池化的参数来训练此模型。 ### 批量矩阵乘法 + :label:`subsec_batch_dot` -为了更有效地计算小批次的注意力,我们可以利用深度学习框架提供的批量矩阵乘法实用程序。 +为了更有效地计算小批量数据的注意力,我们可以利用深度学习框架提供的批量矩阵乘法应用程序。 -假设第一个微型批次包含 $n$ 矩阵 $n$,形状为 $a\times b$,第二个微型批次包含 $n$ 矩阵 $\mathbf{Y}_1, \ldots, \mathbf{Y}_n$,形状为 73229363620。它们的批量矩阵乘法得出 $n$ 矩阵 $\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n$,形状为 $a\times c$。因此,假定两个张量的形状($n$、$a$、$b$)和($b$,$c$)的形状,它们的批量矩阵乘法输出的形状为($n$、$a$、$c$)。 +假设第一个小批量包含 $n$ 个矩阵 $\mathbf{X}_1,\ldots, \mathbf{X}_n$,形状为 $a\times b$,第二个小批量包含 $n$ 个矩阵 $\mathbf{Y}_1, \ldots, \mathbf{Y}_n$,形状为 $b\times c$。它们的批量矩阵乘法得出 $n$ 个矩阵 $\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n$,形状为 $a\times c$。因此,假定两个张量的形状 $(n,a,b)$ 和 $(n,b,c)$ ,它们的批量矩阵乘法输出的形状为 $(n,a,c)$。 ```{.python .input} X = d2l.ones((2, 1, 4)) @@ -186,7 +183,7 @@ Y = d2l.ones((2, 4, 6)) torch.bmm(X, Y).shape ``` -在注意力机制的背景下,我们可以使用微型批次矩阵乘法来计算微型批次中值的加权平均值。 +在注意力机制的背景下,我们可以使用小批量矩阵乘法来计算小批量中的值的加权平均值。 ```{.python .input} weights = d2l.ones((2, 10)) * 0.1 @@ -203,7 +200,7 @@ torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1)) ### 定义模型 -使用微型批量矩阵乘法,下面我们根据 :eqref:`eq_nadaraya-waston-gaussian-para` 中的参数关注池定义 Nadaraya-Watson 内核回归的参数化版本。 +使用小批量矩阵乘法,下面根据 :eqref:`eq_nadaraya-waston-gaussian-para` 中的带参数的注意力池化来定义 Nadaraya-Watson 核回归的带参数版本。 ```{.python .input} class NWKernelRegression(nn.Block): @@ -212,13 +209,12 @@ class NWKernelRegression(nn.Block): self.w = self.params.get('w', shape=(1,)) def forward(self, queries, keys, values): - # Shape of the output `queries` and `attention_weights`: - # (no. of queries, no. of key-value pairs) + # `queries` 和 `attention_weights` 的形状:(查询个数, “键-值”对个数) queries = d2l.reshape( queries.repeat(keys.shape[1]), (-1, keys.shape[1])) self.attention_weights = npx.softmax( -((queries - keys) * self.w.data())**2 / 2) - # Shape of `values`: (no. of queries, no. of key-value pairs) + # `values` 的形状:(查询个数, “键-值”对个数) return npx.batch_dot(np.expand_dims(self.attention_weights, 1), np.expand_dims(values, -1)).reshape(-1) ``` @@ -231,53 +227,48 @@ class NWKernelRegression(nn.Module): self.w = nn.Parameter(torch.rand((1,), requires_grad=True)) def forward(self, queries, keys, values): - # Shape of the output `queries` and `attention_weights`: - # (no. of queries, no. of key-value pairs) + # `queries` 和 `attention_weights` 的形状:(查询个数, “键-值”对个数) queries = d2l.reshape( queries.repeat_interleave(keys.shape[1]), (-1, keys.shape[1])) self.attention_weights = nn.functional.softmax( -((queries - keys) * self.w)**2 / 2, dim=1) - # Shape of `values`: (no. of queries, no. of key-value pairs) + # `values` 的形状:(查询个数, “键-值”对个数) return torch.bmm(self.attention_weights.unsqueeze(1), values.unsqueeze(-1)).reshape(-1) ``` -### 培训 +### 训练模型 -在以下内容中,我们将训练数据集转换为键和值,以训练注意力模型。在参数化注意力池中,任何训练输入都会从所有训练示例中获取键值对,但用于预测其输出。 +接下来,将训练数据集转换为键和值用于训练注意力模型。在带参数的注意力池化模型中,任何训练的输入都会从所有的训练样本(除了输入样本自己)中获取“键-值”对,而输入样本自己需要用于预测其输出。 ```{.python .input} -# Shape of `X_tile`: (`n_train`, `n_train`), where each column contains the -# same training inputs +# `X_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输入 X_tile = np.tile(x_train, (n_train, 1)) -# Shape of `Y_tile`: (`n_train`, `n_train`), where each column contains the -# same training outputs +# `Y_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输出 Y_tile = np.tile(y_train, (n_train, 1)) -# Shape of `keys`: ('n_train', 'n_train' - 1) +# `keys` 的形状: ('n_train', 'n_train' - 1) keys = d2l.reshape(X_tile[(1 - d2l.eye(n_train)).astype('bool')], (n_train, -1)) -# Shape of `values`: ('n_train', 'n_train' - 1) +# `values` 的形状: ('n_train', 'n_train' - 1) values = d2l.reshape(Y_tile[(1 - d2l.eye(n_train)).astype('bool')], (n_train, -1)) ``` ```{.python .input} #@tab pytorch -# Shape of `X_tile`: (`n_train`, `n_train`), where each column contains the -# same training inputs +# `X_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输入 X_tile = x_train.repeat((n_train, 1)) -# Shape of `Y_tile`: (`n_train`, `n_train`), where each column contains the -# same training outputs +# `Y_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输出 Y_tile = y_train.repeat((n_train, 1)) -# Shape of `keys`: ('n_train', 'n_train' - 1) +# `keys` 的形状: ('n_train', 'n_train' - 1) keys = d2l.reshape(X_tile[(1 - d2l.eye(n_train)).type(torch.bool)], (n_train, -1)) -# Shape of `values`: ('n_train', 'n_train' - 1) +# `values` 的形状: ('n_train', 'n_train' - 1) values = d2l.reshape(Y_tile[(1 - d2l.eye(n_train)).type(torch.bool)], (n_train, -1)) ``` -我们使用平方损失和随机梯度下降,训练参数化注意力模型。 +训练带参数的注意力池化模型时使用平方损失函数和随机梯度下降。 ```{.python .input} net = NWKernelRegression() @@ -304,8 +295,8 @@ animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5]) for epoch in range(5): trainer.zero_grad() - # Note: L2 Loss = 1/2 * MSE Loss. PyTorch has MSE Loss which is slightly - # different from MXNet's L2Loss by a factor of 2. Hence we halve the loss + # 注意:L2 Loss = 1/2 * MSE Loss。 + # PyTorch 的 MSE Loss 与 MXNet 的 L2Loss 差一个 2 的因子,因此被减半。 l = loss(net(x_train, keys, values), y_train) / 2 l.sum().backward() trainer.step() @@ -313,13 +304,12 @@ for epoch in range(5): animator.add(epoch + 1, float(l.sum())) ``` -训练参数化注意力模型后,我们可以绘制其预测。试图使用噪点拟合训练数据集,预测线不如之前绘制的非参数对应线平滑。 +训练带参数的注意力池化模型后,绘制其预测结果。试图使用噪点拟合训练数据集,预测线不如之前绘制的非参数模型的预测线平滑。 ```{.python .input} -# Shape of `keys`: (`n_test`, `n_train`), where each column contains the same -# training inputs (i.e., same keys) +# `keys` 的形状: (`n_test`, `n_train`), 每一行包含着相同的训练输入(例如:相同的键) keys = np.tile(x_train, (n_test, 1)) -# Shape of `value`: (`n_test`, `n_train`) +# `value` 的形状: (`n_test`, `n_train`) values = np.tile(y_train, (n_test, 1)) y_hat = net(x_test, keys, values) plot_kernel_reg(y_hat) @@ -327,16 +317,15 @@ plot_kernel_reg(y_hat) ```{.python .input} #@tab pytorch -# Shape of `keys`: (`n_test`, `n_train`), where each column contains the same -# training inputs (i.e., same keys) +# `keys` 的形状: (`n_test`, `n_train`), 每一行包含着相同的训练输入(例如:相同的键) keys = x_train.repeat((n_test, 1)) -# Shape of `value`: (`n_test`, `n_train`) +# `value` 的形状: (`n_test`, `n_train`) values = y_train.repeat((n_test, 1)) y_hat = net(x_test, keys, values).unsqueeze(1).detach() plot_kernel_reg(y_hat) ``` -与非参数化注意力池相比,注意力权重较大的区域在可学习和参数化设置中变得更加锐利。 +与非参数的注意力池化模型相比,注意力权重较大的区域在可学习的和带参数的设置中变得更加尖锐。 ```{.python .input} d2l.show_heatmaps(np.expand_dims(np.expand_dims(net.attention_weights, 0), 0), @@ -353,16 +342,16 @@ d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0), ## 摘要 -* Nadaraya-Watson 内核回归是具有注意机制的机器学习示例。 -* Nadaraya-Watson 内核回归的注意力集中是训练输出的加权平均值。从注意力的角度来看,根据查询的函数和与值配对的键,将注意力权重分配给值。 -* 注意力池可以是非参数化的,也可以是参数化的。 +* Nadaraya-Watson 核回归是具有注意力机制的机器学习示例。 +* Nadaraya-Watson 核回归的注意力池化是训练输出的加权平均值。从注意力的角度来看,根据查询的函数和与值配对的键,将注意力权重分配给值。 +* 注意力池化可以是非参数的,也可以是带参数的。 ## 练习 -1. 增加培训示例的数量。你能更好地学习非参数 Nadaraya-Watson 内核回归吗? -1. 我们在参数化注意力池实验中学到的 $w$ 的价值是什么?为什么在可视化注意力权重时,它会使加权区域更加锐利? -1. 我们如何将超参数添加到非参数 Nadaraya-Watson 内核回归中以更好地预测? -1. 为本节的内核回归设计另一个参数化注意力池。训练这个新模型并可视化其注意力重量。 +1. 增加训练样本的数量。能否更好地学习非参数的 Nadaraya-Watson 核回归? +1. 在带参数的注意力池化的实验中学到的 $w$ 的价值是什么?为什么在可视化注意力权重时,它会使加权区域更加尖锐? +1. 如何将超参数添加到非参数的 Nadaraya-Watson 核回归中以实现更好地预测? +1. 为本节的核回归设计另一个带参数的注意力池化模型。训练这个新模型并可视化其注意力权重。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1598) From 248d5e314c46284720e2a4812286f485655694d0 Mon Sep 17 00:00:00 2001 From: liu-mengyang Date: Tue, 6 Apr 2021 17:42:04 +0800 Subject: [PATCH 025/103] Correct incorrect reference information in chapter_introduction/index.md --- chapter_introduction/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index 966b3930d..1ec6fe657 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -645,7 +645,7 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 与此同时,算力的增长速度已经超过了现有数据的增长速度。 这意味着统计模型需要提高内存效率(这通常是通过添加非线性来实现的),同时由于计算预算的增加,能够花费更多时间来优化这些参数。 因此,机器学习和统计的关注点从(广义的)线性模型和核方法转移到了深度神经网络。 -这也是为什么许多深度学习的中流砥柱,如多层感知机 :cite:`McCulloch.Pitts.1943` 、卷积神经网络 :cite:`LeCun.Bottou.Bengio.ea.1998` 、长短期记忆网络 :cite:`Watkins.Dayan.1992` 和Q学习 :cite:`Watkins.Dayan.1992` ,在相当长一段时间处于相对休眠状态之后,在过去十年中被“重新发现”的原因之一。 +这也是为什么许多深度学习的中流砥柱,如多层感知机 :cite:`McCulloch.Pitts.1943` 、卷积神经网络 :cite:`LeCun.Bottou.Bengio.ea.1998` 、长短期记忆网络 :cite:`Graves.Schmidhuber.2005` 和Q学习 :cite:`Watkins.Dayan.1992` ,在相当长一段时间处于相对休眠状态之后,在过去十年中被“重新发现”的原因之一。 最近十年,在统计模型、应用和算法方面的进展就像寒武纪大爆发。 事实上,最先进的技术不仅仅是应用于几十年前的算法的可用资源的结果。 From e6c0e45837ae25db2a1926ad6522e4cbab24f44a Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Wed, 7 Apr 2021 08:46:59 -0700 Subject: [PATCH 026/103] typo --- chapter_preliminaries/pandas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preliminaries/pandas.md b/chapter_preliminaries/pandas.md index 5369a47e7..ad63bdf10 100644 --- a/chapter_preliminaries/pandas.md +++ b/chapter_preliminaries/pandas.md @@ -48,7 +48,7 @@ inputs = inputs.fillna(inputs.mean()) print(inputs) ``` -[**对于 `inputs` 中的类别值或离散值,我们将 “NaN” 视为一个类别。**]由于 “巷子”(“Alley”)列只接受两种类型的类别值 “Alley” 和 “NaN”,`pandas` 可以自动将此列转换为两列 “Alley_Pave” 和 “Alley_nan”。巷子类型为 “Pave” 的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。 +[**对于 `inputs` 中的类别值或离散值,我们将 “NaN” 视为一个类别。**]由于 “巷子”(“Alley”)列只接受两种类型的类别值 “Pave” 和 “NaN”,`pandas` 可以自动将此列转换为两列 “Alley_Pave” 和 “Alley_nan”。巷子类型为 “Pave” 的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。 ```{.python .input} #@tab all From 8964b7acb26993dabc7c0e36baad8b3e9394a734 Mon Sep 17 00:00:00 2001 From: Hengyu <31696102+aekst@users.noreply.github.com> Date: Sat, 10 Apr 2021 09:01:01 +0800 Subject: [PATCH 027/103] Fix: typo (#733) --- chapter_introduction/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index 1ec6fe657..61d35f531 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -107,7 +107,7 @@ ### 数据 毋庸置疑,如果没有数据,那么数据科学毫无用武之地。 -每个数据集由一个个*样本*(example)组成,大多时候,它们遵循独立同分布(idependently and identically distributed, i.i.d.)。 +每个数据集由一个个*样本*(example)组成,大多时候,它们遵循独立同分布(independently and identically distributed, i.i.d.)。 样本有时也叫做*数据点*(data point)或者*数据实例*(data instance),通常每个样本由一组称为*特征*(features,或*协变量*(covariates))的属性组成。 机器学习模型会根据这些属性进行预测。 在上面的监督学习问题中,要预测的是一个特殊的属性,它被称为*标签*(label,或*目标*(target))。 From 9fa87431a5db017ead1d3c6e71cb2fb007e5d9d0 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Sun, 11 Apr 2021 11:44:48 +0800 Subject: [PATCH 028/103] add my name in preface/index.md (#721) --- chapter_preface/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preface/index.md b/chapter_preface/index.md index fbde76c01..a81d640e6 100644 --- a/chapter_preface/index.md +++ b/chapter_preface/index.md @@ -162,7 +162,7 @@ Hoa Nguyen, manuel-arno-korfmann-webentwicklung, aterzis-personal, nxby, Xiaotin mathresearch, mzz2017, jroberayalas, iluu, ghejc, BSharmi, vkramdev, simonwardjones, LakshKD, TalNeoran, djliden, Nikhil95, Oren Barkan, guoweis, haozhu233, pratikhack, 315930399, tayfununal, steinsag, charleybeller, Andrew Lumsdaine, Jiekui Zhang, Deepak Pathak, Florian Donhauser, Tim Gates, -Adriaan Tijsseling, Ron Medina, Gaurav Saha, Murat Semerci, [Lei Mao](https://github.com/leimao) +Adriaan Tijsseling, Ron Medina, Gaurav Saha, Murat Semerci, [Lei Mao](https://github.com/leimao), [Zhu Yuanxiang](https://zhuyuanxiang.github.io) 我们感谢Amazon Web Services,特别是Swami Sivasubramanian、Raju Gulabani、Charlie Bell和Andrew Jassy对撰写本书的慷慨支持。如果没有可用的时间、资源、与同事的讨论和不断的鼓励,这本书就不会出版。 From 4e8fbf89423affead24ddfa38d1de21a605b7773 Mon Sep 17 00:00:00 2001 From: thebesttv Date: Sun, 11 Apr 2021 12:03:06 +0800 Subject: [PATCH 029/103] update translations in mlp.md (#734) * update translations in mlp.md * Update mlp.md Co-authored-by: goldmermaid <37914843+goldmermaid@users.noreply.github.com> --- chapter_multilayer-perceptrons/mlp.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index 0867ce67a..c808ec531 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -1,33 +1,33 @@ # 多层感知机 :label:`sec_mlp` -在 :numref:`chap_linear` 中,我们介绍了softmax回归( :numref:`sec_softmax` ),然后我们从零开始实现softmax回归( :numref:`sec_softmax_scratch` ),接着使用高级API实现了算法( :numref:`sec_softmax_concise` ),并训练分类器从低分辨率图像中识别10类服装。在这个过程中,我们学习了如何处理数据,将输出转换为有效的概率分布,并应用适当的损失函数,根据模型参数最小化损失。我们已经在简单的线性模型背景下掌握了这些知识,现在我们可以开始对深度神经网络的探索,这也是本书主要涉及的比较丰富的一类模型。 +在 :numref:`chap_linear` 中,我们介绍了softmax回归( :numref:`sec_softmax` ),然后我们从零开始实现softmax回归( :numref:`sec_softmax_scratch` ),接着使用高级API实现了算法( :numref:`sec_softmax_concise` ),并训练分类器从低分辨率图像中识别10类服装。在这个过程中,我们学习了如何处理数据,将输出转换为有效的概率分布,并应用适当的损失函数,根据模型参数最小化损失。我们已经在简单的线性模型背景下掌握了这些知识,现在我们可以开始对深度神经网络的探索,这也是本书主要涉及的一类模型。 ## 隐藏层 -我们在 :numref:`subsec_linear_model`中描述了仿射变换,它是一个带有偏置项的线性变换。首先,回想一下如 :numref:`fig_softmaxreg`中所示的softmax回归的模型结构。该模型通过单个仿射变换将我们的输入直接映射到我们的输出,然后进行softmax操作。如果我们的标签确实通过仿射变换后与我们的输入数据相关,那么这种方法就足够了。但是,仿射变换中的*线性*是一个很强的假设。 +我们在 :numref:`subsec_linear_model`中描述了仿射变换,它是一个带有偏置项的线性变换。首先,回想一下如 :numref:`fig_softmaxreg`中所示的softmax回归的模型结构。该模型通过单个仿射变换将我们的输入直接映射到输出,然后进行softmax操作。如果我们的标签通过仿射变换后确实与我们的输入数据相关,那么这种方法就足够了。但是,仿射变换中的*线性*是一个很强的假设。 ### 线性模型可能会出错 -例如,线性意味着*单调*假设:特征的任何增大都会导致模型输出增大(如果对应的权重为正),或者导致模型输出减少(如果对应的权重为负)。有时这是有道理的。例如,如果我们试图预测一个人是否会偿还贷款。我们可以认为,在其他条件不变的情况下,收入较高的申请人总是比收入较低的申请人更有可能偿还贷款。但是,虽然收入与还款概率存在单调性,但它们不是线性相关的。收入从0增加到5万,可能比从100万增加到105万带来更大的还款可能性。处理这一问题的一种方法是对我们的数据进行预处理,使线性变得更合理,如,使用收入的对数作为我们的特征。 +例如,线性意味着*单调*假设:特征的任何增大都会导致模型输出增大(如果对应的权重为正),或者导致模型输出减少(如果对应的权重为负)。有时这是有道理的。例如,如果我们试图预测一个人是否会偿还贷款。我们可以认为,在其他条件不变的情况下,收入较高的申请人总是比收入较低的申请人更有可能偿还贷款。但是,虽然收入与还款概率存在单调性,但它们不是线性相关的。收入从0增加到5万,可能比从100万增加到105万带来更大的还款可能性。处理这一问题的一种方法是对我们的数据进行预处理,使线性变得更合理,如使用收入的对数作为我们的特征。 我们可以很容易地找出违反单调性的例子。例如,我们想要根据体温预测死亡率。对于体温高于37摄氏度的人来说,温度越高风险越大。然而,对于体温低于37摄氏度的人来说,温度越高风险就越低。在这种情况下,我们也可以通过一些巧妙的预处理来解决问题。例如,我们可以使用与37摄氏度的距离作为特征。 -但是,如何对猫和狗的图像进行分类呢?增加位置(13, 17)处像素的强度是否总是增加(或总是降低)图像描绘狗的可能性?对线性模型的依赖对应于一个隐含的假设,即区分猫和狗的唯一要求是评估单个像素的强度。这种方法注定会失败,因为在这样一个世界里,把一幅图像倒过来就保留了类别。 +但是,如何对猫和狗的图像进行分类呢?增加位置(13, 17)处像素的强度是否总是增加(或总是降低)图像描绘狗的可能性?对线性模型的依赖对应于一个隐含的假设,即区分猫和狗的唯一要求是评估单个像素的强度。在一个倒置图像保留类别的世界里,这种方法注定会失败。 与我们前面的例子相比,这里的线性很荒谬,而且我们难以通过简单的预处理来解决这个问题。这是因为任何像素的重要性都以复杂的方式取决于该像素的上下文(周围像素的值)。我们的数据可能会有一种表示,这种表示会考虑到我们的特征之间的相关交互作用。在此表示的基础上建立一个线性模型可能会是合适的,但我们不知道如何手动计算这么一种表示。对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器。 ### 合并隐藏层 -我们可以通过合并一个或多个隐藏层来克服线性模型的限制,用来处理更一般化的函数。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。我们可以把前$L-1$层看作表示,把最后一层看作线性预测器。这种架构通常称为*多层感知机*(multilayer perceptron),通常缩写为*MLP*。下面,我们以图的方式描述了多层感知机( :numref:`fig_mlp`)。 +我们可以通过合并一个或多个隐藏层来克服线性模型的限制,并处理更一般化的函数。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。我们可以把前$L-1$层看作表示,把最后一层看作线性预测器。这种架构通常称为*多层感知机*(multilayer perceptron),通常缩写为*MLP*。下面,我们以图的方式描述了多层感知机( :numref:`fig_mlp`)。 ![一个单隐藏层的多层感知机,具有5个隐藏单元](../img/mlp.svg) :label:`fig_mlp` -这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算;因此,这个多层感知机中的层数为2。注意,这两个层都是全连接的。每次输入都会影响隐藏层中的每个神经元,而隐藏层中的每个神经元又会影响输出层中的每个神经元。 +这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算;因此,这个多层感知机中的层数为2。注意,这两个层都是全连接的。每个输入都会影响隐藏层中的每个神经元,而隐藏层中的每个神经元又会影响输出层中的每个神经元。 然而,正如 :numref:`subsec_parameterization-cost-fc-layers` 所说,具有全连接层的多层感知机的参数开销可能会高得令人望而却步, -即使在不改变输入或输出大小的情况下,也可能促使在节省参数和模型效果之间进行权衡 :cite:`Zhang.Tay.Zhang.ea.2021`。 +即使在不改变输入或输出大小的情况下,也可能促使在参数节约和模型有效性之间进行权衡 :cite:`Zhang.Tay.Zhang.ea.2021`。 ### 从线性到非线性 @@ -65,7 +65,7 @@ $$ ### 通用近似定理 -多层感知机可以通过隐藏神经元捕捉到我们输入之间的复杂相互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意计算。例如,在一对输入上进行基本逻辑操作。多层感知机是通用近似器。即使是网络只有一个隐藏层,给定足够的神经元(可能非常多)和正确的权重,我们可以对任意函数建模,尽管实际中学习该函数是很困难的。你可能认为神经网络有点像C语言。C语言和任何其他现代编程语言一样,能够表达任何可计算的程序。但实际上,想出一个符合规范的程序才是最困难的部分。 +多层感知机可以通过隐藏神经元捕捉到我们输入之间复杂的相互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意计算。例如,在一对输入上进行基本逻辑操作。多层感知机是通用近似器。即使是网络只有一个隐藏层,给定足够的神经元(可能非常多)和正确的权重,我们可以对任意函数建模,尽管实际中学习该函数是很困难的。你可能认为神经网络有点像C语言。C语言和任何其他现代编程语言一样,能够表达任何可计算的程序。但实际上,想出一个符合规范的程序才是最困难的部分。 而且,虽然一个单隐层网络能学习任何函数,但并不意味着应该尝试使用单隐藏层网络来解决所有问题。事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。我们将在后面的章节中进行更细致的讨论。 From a9a05971727e0ab981e12ffbbe070fa64210151e Mon Sep 17 00:00:00 2001 From: tian Date: Mon, 12 Apr 2021 14:53:33 +1000 Subject: [PATCH 030/103] clarify ex3.2 (#720) --- chapter_linear-networks/softmax-regression.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_linear-networks/softmax-regression.md b/chapter_linear-networks/softmax-regression.md index 2bdd3553f..b5c5111de 100644 --- a/chapter_linear-networks/softmax-regression.md +++ b/chapter_linear-networks/softmax-regression.md @@ -199,7 +199,7 @@ $$H[P] = \sum_j - P(j) \log P(j).$$ 1. 你能设计一个更好的代码吗?提示:如果我们尝试编码两个独立的观察结果会发生什么?如果我们联合编码 $n$ 个观测值怎么办? 1. softmax是对上面介绍的映射的误用(但深度学习中的每个人都使用它)。真正的softmax被定义为 $\mathrm{RealSoftMax}(a, b) = \log (\exp(a) + \exp(b))$。 1. 证明 $\mathrm{RealSoftMax}(a, b) > \mathrm{max}(a, b)$。 - 1. 证明 $\lambda^{-1} \mathrm{RealSoftMax}(\lambda a, \lambda b)$成立,前提是 $\lambda > 0$。 + 1. 证明 $\lambda^{-1} \mathrm{RealSoftMax}(\lambda a, \lambda b) > \mathrm{max}(a, b)$成立,前提是 $\lambda > 0$。 1. 证明对于 $\lambda \to \infty$ ,有 $\lambda^{-1} \mathrm{RealSoftMax}(\lambda a, \lambda b) \to \mathrm{max}(a, b)$。 1. soft-min会是什么样子? 1. 将其扩展到两个以上的数字。 From fad45866002adfce772e0b11d2e9145a5dd19b57 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Mon, 12 Apr 2021 12:55:38 +0800 Subject: [PATCH 031/103] fix typo and translation issues in transformer.md (#732) * fix typo and translation issues in transformer.md * Update transformer.md Co-authored-by: goldmermaid <37914843+goldmermaid@users.noreply.github.com> --- chapter_attention-mechanisms/transformer.md | 148 ++++++++++---------- 1 file changed, 73 insertions(+), 75 deletions(-) diff --git a/chapter_attention-mechanisms/transformer.md b/chapter_attention-mechanisms/transformer.md index 487200def..c496d8d8f 100644 --- a/chapter_attention-mechanisms/transformer.md +++ b/chapter_attention-mechanisms/transformer.md @@ -1,21 +1,21 @@ # Transformer :label:`sec_transformer` -我们在 :numref:`subsec_cnn-rnn-self-attention` 中比较了 CNN、RNN 和 Self-Attention。值得注意的是,Self-Attention 同时享有并行计算和最短的最大路径长度。因此,自然而言,通过使用 Self-Attention 来设计深层架构是很有吸引力的。与之前仍然依赖 RNN 进行输入表示 :cite:`Cheng.Dong.Lapata.2016,Lin.Feng.Santos.ea.2017,Paulus.Xiong.Socher.2017` 的 Self-Attention 模型不同,Transformer 模型完全基于注意机制,没有任何卷积层或循环层 :cite:`Vaswani.Shazeer.Parmar.ea.2017`。尽管 Transformer 最初是应用于文本数据的序列学习,但在各种现代深度学习应用中它也普遍存在,例如语言、视觉、语音和强化学习领域。 +我们在 :numref:`subsec_cnn-rnn-self-attention` 中比较了 CNN、RNN 和自注意力。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,通过使用自注意力来设计深层架构是很有吸引力的。与之前仍然依赖 RNN 进行输入表示 :cite:`Cheng.Dong.Lapata.2016,Lin.Feng.Santos.ea.2017,Paulus.Xiong.Socher.2017` 的自注意力模型不同,Transformer 模型完全基于注意力机制,没有任何卷积层或循环层 :cite:`Vaswani.Shazeer.Parmar.ea.2017`。尽管 Transformer 最初是应用于文本数据的序列学习,但已经普遍应用在各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。 ## 模型 -作为 encoder-decoder 架构的一个实例,Transformer 的整体架构在图 :numref:`fig_transformer` 中呈现。正如我们所看到的,Transformer 由编码器和解码器组成。与 :numref:`fig_s2s_attention_details` 中 Bahdanau 对序列到序列学习的关注点不同,在将输入(源)和输出(目标)序列嵌入添加到编码器和解码器之前,这些嵌入将被添加到基于 Self-Attention 而堆叠模块的编码器和解码器中。 +作为“编码器-解码器”架构的一个实例,Transformer 的整体架构在图 :numref:`fig_transformer` 中呈现。正如所见到的,Transformer 由编码器和解码器组成。与 :numref:`fig_s2s_attention_details` 中 Bahdanau 注意力的序列到序列的学习相比,Transformer 的编码器和解码器是由基于自注意力的模块叠加而成的,输入(源)和输出(目标)序列的嵌入 (embedding) 将被叠加上位置编码,再一起输入到编码器和解码器中。 ![The Transformer architecture.](../img/transformer.svg) :width:`500px` :label:`fig_transformer` -现在我们在 :numref:`fig_transformer` 中概述了变压器架构。从高层来看,变压器编码器是由多个相同层组成的堆栈,每层都有两个子层(两个子层表示为 $\mathrm{sublayer}$)。第一个是多头自我注意力集中,第二个是位置上的前馈网络。具体来说,在编码器的自我注意中,查询、键和值都来自前一个编码器层的输出。受 :numref:`sec_resnet` ResNet 设计的启发,两个子层周围都采用了残留连接。在变压器中,对于序列中任何位置的任何输入 $\mathbf{x} \in \mathbb{R}^d$,我们要求 $\mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$,以便剩余连接 $\mathbf{x} + \mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$ 是可行的。从残留连接中添加这一点之后立即进行层规范化 :cite:`Ba.Kiros.Hinton.2016`。因此,变压器编码器为输入序列的每个位置输出 $d$ 维矢量表示。 +现在为止图 :numref:`fig_transformer` 中已经概述了 Transformer 的架构。从宏观角度来看,Transformer 的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为 $\mathrm{sublayer}$)。第一个子层是多头自注意力池化,第二个子层是基于位置的前馈网络 (positionwise feed-forward network)。具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。受 :numref:`sec_resnet` ResNet 设计的启发,每个子层都采用了残差连接 (residual connection)。在 Transformer 中,对于序列中任何位置的任何输入 $\mathbf{x} \in \mathbb{R}^d$,我们要求满足 $\mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$,以便残差连接 $\mathbf{x} + \mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d$ 是可行的。在残差连接的加法计算之后,紧接着层归一化 (layer normalization) :cite:`Ba.Kiros.Hinton.2016`。因此,对应输入序列的每个位置,Transformer 编码器输出 $d$ 维向量进行表示。 -变压器解码器也是由多个相同层组成的堆栈,具有残留连接和层标准化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入第三个子层,称为编码器解码器注意力。在编码器解码器中,查询来自前一个解码器层的输出,键和值来自 Transcoransder 编码器输出。在解码器中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能处理解码器中直到该位置的所有位置。这种 * 掩码 * 注意力保留了自动回归属性,确保预测仅依赖于已生成的输出令牌。 +Transformer 解码器也是由多个相同的使用了残差连接和层归一化的层叠加而成。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入第三个子层,称为”编码器-解码器“注意力 (encoder-decoder attention)。在“编码器-解码器”注意力中,查询来自前一个解码器层的输出,而键和值来自 Transformer 编码器的输出。在解码器自注意力中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。这种 * 掩码 * 注意力保留了自回归属性,确保预测仅依赖于已生成的输出令牌。 -我们已经描述并实施了基于 :numref:`sec_multihead-attention` 中的缩放点产品和 :numref:`subsec_positional-encoding` 中的位置编码的多头关注。在下面,我们将实现变压器模型的其余部分。 +我们已经描述并实现了基于缩放的“点-积” :numref:`sec_multihead-attention` 和位置编码 :numref:`subsec_positional-encoding` 的多头注意力。接下来,我们将实现 Transformer 模型的其余部分。 ```{.python .input} from d2l import mxnet as d2l @@ -35,9 +35,9 @@ import torch from torch import nn ``` -## 定位前馈网络 +## 基于位置的前馈网络 -位置向前馈网络使用同一个 MLP 转换所有序列位置的表示形式。这就是为什么我们称之为 * 职位 *。在下面的实现中,带有形状的输入 `X`(批量大小、时间步长或序列长度(标记为单位的序列长度、隐藏单位数或要素维度)将被双层 MLP 转换为形状的输出张量(批量大小、时间步长、`ffn_num_outputs`)。 +基于位置的前馈网络使用同一个多层感知机,对序列中的所有位置的表示进行了变换。这就是 *基于位置的* (positionwise) 的原因。在下面的实现中,输入 `X` 的形状(批量大小、时间步长或序列长度、隐单元数或特征维度)将被双层感知机转换成形状为(批量大小、时间步长、前馈网络输出单元数)的输出张量。 ```{.python .input} #@save @@ -67,7 +67,7 @@ class PositionWiseFFN(nn.Module): return self.dense2(self.relu(self.dense1(X))) ``` -以下示例显示,张量的最内层维度会改变位置向前馈网络中的输出数量。由于相同的 MLP 在所有仓位上都变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。 +下面的例子显示,张量的最内层维度的尺寸会改变成基于位置的前馈网络的输出尺寸。由于相同的多层感知机对所有位置上的输入都进行了变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。 ```{.python .input} ffn = PositionWiseFFN(4, 8) @@ -82,13 +82,13 @@ ffn.eval() ffn(d2l.ones((2, 3, 4)))[0] ``` -## 剩余连接和层规范化 +## 残差连接和层归一化 -现在让我们关注 :numref:`fig_transformer` 中的 “添加和规范” 组件。正如我们在本节开头所述,这是一个残余连接,紧接着是层规范化。两者都是有效的深度架构的关键。 +现在让我们关注 :numref:`fig_transformer` 中的 “加法和归一化” 组件。正如在本节开头所述,这是由残差连接和紧随的层归一化组成的。两者都是有效的深度架构的关键。 -在 :numref:`sec_batch_norm` 中,我们解释了如何在一个小批量内批量标准化最近和重新调整示例。图层规范化与批量规范化相同,只是前者在要素维度上进行规范化。尽管在计算机视觉中广泛应用批量规范化,但在自然语言处理任务中,批量规范化通常不如图层规范化的效果,而自然语言处理任务的输入通常是可变长度的序列。 +在 :numref:`sec_batch_norm` 中,我们解释了如何在一个小批量内通过批量标准化对样本数据进行重新中心化和重新缩放的调整。层归一化和批量归一化相同,只是前者基于特征维度进行归一化。尽管批量归一化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量归一化的效果通常不如层归一化的好。 -以下代码段通过层规范化和批量规范化比较了不同维度的规范化。 +以下代码段对比了不同维度的层归一化和批量归一化的归一化效果。 ```{.python .input} ln = nn.LayerNorm() @@ -96,7 +96,7 @@ ln.initialize() bn = nn.BatchNorm() bn.initialize() X = d2l.tensor([[1, 2], [2, 3]]) -# Compute mean and variance from `X` in the training mode +# 在训练模式下计算 `X` 的均值和方差 with autograd.record(): print('layer norm:', ln(X), '\nbatch norm:', bn(X)) ``` @@ -106,11 +106,11 @@ with autograd.record(): ln = nn.LayerNorm(2) bn = nn.BatchNorm1d(2) X = d2l.tensor([[1, 2], [2, 3]], dtype=torch.float32) -# Compute mean and variance from `X` in the training mode +# 在训练模式下计算 `X` 的均值和方差 print('layer norm:', ln(X), '\nbatch norm:', bn(X)) ``` -现在我们可以使用剩余连接实现 `AddNorm` 类,然后再进行层规范化。退学也适用于正规化。 +现在我们可以使用残差连接和层归一化来实现 `AddNorm` 类。Dropout 也被作为正规化方法用在这里。 ```{.python .input} #@save @@ -137,7 +137,7 @@ class AddNorm(nn.Module): return self.ln(self.dropout(Y) + X) ``` -剩余连接要求两个输入的形状相同,以便在加法操作后输出张量也具有相同的形状。 +残差连接要求两个输入的形状相同,以便在加法操作后输出的张量也具有相同的形状。 ```{.python .input} add_norm = AddNorm(0.5) @@ -154,7 +154,7 @@ add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape ## 编码器 -由于组装变压器所需的所有必要组件,让我们首先在编码器中实现单层。以下 `EncoderBlock` 类包含两个子层:多头自我注意力和定位前馈网络,其中两个子层周围采用残留连接,然后再进行层规范化。 +现在有了组成 Transformer 编码器的基础组件,可以先实现编码器中的一个层。下面的 `EncoderBlock` 类包含两个子层:多头自注意力和基于位置的前馈网络,围绕着这两个子层都使用了残差连接和紧随的层归一化。 ```{.python .input} #@save @@ -194,7 +194,7 @@ class EncoderBlock(nn.Module): return self.addnorm2(Y, self.ffn(Y)) ``` -正如我们所看到的,变压器编码器中的任何图层都不会改变其输入的形状。 +正如我们所看到的,Transformer 编码器中的任何层都不会改变其输入的形状。 ```{.python .input} X = d2l.ones((2, 100, 24)) @@ -213,7 +213,7 @@ encoder_blk.eval() encoder_blk(X, valid_lens).shape ``` -在下面的变压器编码器实现中,我们堆叠上述 `EncoderBlock` 类的 `num_layers` 个实例。由于我们使用的值始终在-1 和 1 之间的固定位置编码,因此我们将可学习输入嵌入的值乘以嵌入维度的平方根,以便在总结输入嵌入和位置编码之前重新缩放。 +在下面的 Transformer 编码器的实现中,我们堆叠了 `num_layers` 个 `EncoderBlock` 类的实例。由于我们使用的是值范围在-1 和 1 之间的固定位置编码,因此在与位置编码相加之前,将可以学习的输入嵌入的值乘以嵌入维度的平方根进行重新缩放。 ```{.python .input} #@save @@ -231,9 +231,9 @@ class TransformerEncoder(d2l.Encoder): use_bias)) def forward(self, X, valid_lens, *args): - # Since positional encoding values are between -1 and 1, the embedding - # values are multiplied by the square root of the embedding dimension - # to rescale before they are summed up + # 因为位置编码值在 -1 和 1 之间, + # 因此嵌入值乘以嵌入维度的平方根进行缩放, + # 然后再与位置编码相加。 X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) self.attention_weights = [None] * len(self.blks) for i, blk in enumerate(self.blks): @@ -262,9 +262,9 @@ class TransformerEncoder(d2l.Encoder): num_heads, dropout, use_bias)) def forward(self, X, valid_lens, *args): - # Since positional encoding values are between -1 and 1, the embedding - # values are multiplied by the square root of the embedding dimension - # to rescale before they are summed up + # 因为位置编码值在 -1 和 1 之间, + # 因此嵌入值乘以嵌入维度的平方根进行缩放, + # 然后再与位置编码相加。 X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) self.attention_weights = [None] * len(self.blks) for i, blk in enumerate(self.blks): @@ -274,7 +274,7 @@ class TransformerEncoder(d2l.Encoder): return X ``` -下面我们指定了超参数来创建一个双层变压器编码器。变压器编码器输出的形状是(批量大小、时间步长数、`num_hiddens`)。 +下面我们指定了超参数来创建一个双层 Transformer 编码器。Transformer 编码器输出的形状是(批量大小、时间步长、`num_hiddens`)。 ```{.python .input} encoder = TransformerEncoder(200, 24, 48, 8, 2, 0.5) @@ -292,13 +292,13 @@ encoder(d2l.ones((2, 100), dtype=torch.long), valid_lens).shape ## 解码器 -如 :numref:`fig_transformer` 所示,变压器解码器由多个相同的层组成。每个层都在以下 `DecoderBlock` 类中实现,其中包含三个子层:解码器自我注意、编码器-解码器注意力和定位前馈网络。这些子层周围使用残留连接,然后进行层规范化。 +如 :numref:`fig_transformer` 所示,Transformer 解码器由多个相同的层组成。`DecoderBlock` 类中包含三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络,每个子层都已经被实现。这些子层也都采用了残差连接和紧随的层归一化。 -正如我们在本节前面所述,在蒙版的多头解码器自我注意力(第一个子层)中,查询、键和值都来自上一个解码器层的输出。训练顺序到序列模型时,输出序列的所有位置(时间步长)的令牌都是已知的。但是,在预测期间,输出序列是通过令牌生成的;因此,在任何解码器时间步骤中,只有生成的令牌才能用于解码器的自我注意力。为了在解码器中保留自动回归,其蒙版自我注意力指定 `dec_valid_lens`,以便任何查询只参与解码器中直到查询位置的所有位置。 +正如在本节前面所述,在解码器的掩码多头自注意力(第一个子层)中,查询、键和值都来自上一个解码器层的输出。在序列到序列模型 (sequence-to-sequence models) 的训练阶段,输出序列的所有位置(时间步)的令牌都是已知的。但在预测阶段,输出序列是通过令牌一个接着一个生成的;因此,在任何解码器时间步中,只有生成的令牌才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩码自注意力指定了参数 `dec_valid_lens`,以便任何查询只会与解码器已经生成的所有位置(直到该查询位置为止)进行注意力计算。 ```{.python .input} class DecoderBlock(nn.Block): - # The `i`-th block in the decoder + """解码器中第 i 个块""" def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, i, **kwargs): super(DecoderBlock, self).__init__(**kwargs) @@ -314,11 +314,10 @@ class DecoderBlock(nn.Block): def forward(self, X, state): enc_outputs, enc_valid_lens = state[0], state[1] - # During training, all the tokens of any output sequence are processed - # at the same time, so `state[2][self.i]` is `None` as initialized. - # When decoding any output sequence token by token during prediction, - # `state[2][self.i]` contains representations of the decoded output at - # the `i`-th block up to the current time step + # 训练阶段,输出序列的所有令牌都在同一时间处理, + # 因此 `state[2][self.i]` 初始化为 `None`。 + # 预测阶段,输出序列是通过令牌一个接着一个解码的, + # 因此 `state[2][self.i]` 包含着直到当前时间步第 `i` 个块解码的输出表示 if state[2][self.i] is None: key_values = X else: @@ -327,18 +326,18 @@ class DecoderBlock(nn.Block): if autograd.is_training(): batch_size, num_steps, _ = X.shape - # Shape of `dec_valid_lens`: (`batch_size`, `num_steps`), where - # every row is [1, 2, ..., `num_steps`] + # `dec_valid_lens` 的开头: (`batch_size`, `num_steps`), + # 其中每一行是 [1, 2, ..., `num_steps`] dec_valid_lens = np.tile(np.arange(1, num_steps + 1, ctx=X.ctx), (batch_size, 1)) else: dec_valid_lens = None - # Self-attention + # 自注意力 X2 = self.attention1(X, key_values, key_values, dec_valid_lens) Y = self.addnorm1(X, X2) - # Encoder-decoder attention. Shape of `enc_outputs`: - # (`batch_size`, `num_steps`, `num_hiddens`) + # “编码器-解码器”注意力。 + # 'enc_outputs' 的开头: ('batch_size', 'num_steps', 'num_hiddens') Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens) Z = self.addnorm2(Y, Y2) return self.addnorm3(Z, self.ffn(Z)), state @@ -347,7 +346,7 @@ class DecoderBlock(nn.Block): ```{.python .input} #@tab pytorch class DecoderBlock(nn.Module): - # The `i`-th block in the decoder + """解码器中第 i 个块""" def __init__(self, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, dropout, i, **kwargs): @@ -365,11 +364,10 @@ class DecoderBlock(nn.Module): def forward(self, X, state): enc_outputs, enc_valid_lens = state[0], state[1] - # During training, all the tokens of any output sequence are processed - # at the same time, so `state[2][self.i]` is `None` as initialized. - # When decoding any output sequence token by token during prediction, - # `state[2][self.i]` contains representations of the decoded output at - # the `i`-th block up to the current time step + # 训练阶段,输出序列的所有令牌都在同一时间处理, + # 因此 `state[2][self.i]` 初始化为 `None`。 + # 预测阶段,输出序列是通过令牌一个接着一个解码的, + # 因此 `state[2][self.i]` 包含着直到当前时间步第 `i` 个块解码的输出表示 if state[2][self.i] is None: key_values = X else: @@ -377,24 +375,24 @@ class DecoderBlock(nn.Module): state[2][self.i] = key_values if self.training: batch_size, num_steps, _ = X.shape - # Shape of `dec_valid_lens`: (`batch_size`, `num_steps`), where - # every row is [1, 2, ..., `num_steps`] + # `dec_valid_lens` 的开头: (`batch_size`, `num_steps`), + # 其中每一行是 [1, 2, ..., `num_steps`] dec_valid_lens = torch.arange( 1, num_steps + 1, device=X.device).repeat(batch_size, 1) else: dec_valid_lens = None - # Self-attention + # 自注意力 X2 = self.attention1(X, key_values, key_values, dec_valid_lens) Y = self.addnorm1(X, X2) - # Encoder-decoder attention. Shape of `enc_outputs`: - # (`batch_size`, `num_steps`, `num_hiddens`) + # “编码器-解码器”注意力。 + # `enc_outputs` 的开头: (`batch_size`, `num_steps`, `num_hiddens`) Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens) Z = self.addnorm2(Y, Y2) return self.addnorm3(Z, self.ffn(Z)), state ``` -为了便于在编码器-解码器注意和剩余连接中的加法操作,解码器的特征尺寸 (`num_hiddens`) 与编码器的特征尺寸 (`num_hiddens`) 相同。 +为了便于在“编码器-解码器”注意力中进行缩放的“点-积”计算和残差连接中进行加法操作,编码器和解码器的特征维度相同,都是 (`num_hiddens`)。 ```{.python .input} decoder_blk = DecoderBlock(24, 48, 8, 0.5, 0) @@ -413,7 +411,7 @@ state = [encoder_blk(X, valid_lens), valid_lens, [None]] decoder_blk(X, state)[0].shape ``` -现在我们构建了由 `num_layers` 个 `DecoderBlock` 实例组成的整个变压器解码器。最后,一个完全连接的层计算所有 `vocab_size` 个可能的输出令牌的预测。解码器的自我注意力重量和编码器-解码器的注意权重都被存储,以供日后可视化。 +现在我们构建了由 `num_layers` 个 `DecoderBlock` 实例组成的完整的 Transformer 解码器。最后,通过一个全连接层计算所有 `vocab_size` 个可能的输出令牌的预测值。解码器的自注意力权重和“编码器-解码器”的注意力权重都被存储下来,以供日后可视化。 ```{.python .input} class TransformerDecoder(d2l.AttentionDecoder): @@ -439,10 +437,10 @@ class TransformerDecoder(d2l.AttentionDecoder): self._attention_weights = [[None] * len(self.blks) for _ in range (2)] for i, blk in enumerate(self.blks): X, state = blk(X, state) - # Decoder self-attention weights + # 解码器自注意力权重 self._attention_weights[0][ i] = blk.attention1.attention.attention_weights - # Encoder-decoder attention weights + # “编码器-解码器”自注意力权重 self._attention_weights[1][ i] = blk.attention2.attention.attention_weights return self.dense(X), state @@ -479,10 +477,10 @@ class TransformerDecoder(d2l.AttentionDecoder): self._attention_weights = [[None] * len(self.blks) for _ in range (2)] for i, blk in enumerate(self.blks): X, state = blk(X, state) - # Decoder self-attention weights + # 解码器自注意力权重 self._attention_weights[0][ i] = blk.attention1.attention.attention_weights - # Encoder-decoder attention weights + # “编码器-解码器”自注意力权重 self._attention_weights[1][ i] = blk.attention2.attention.attention_weights return self.dense(X), state @@ -492,9 +490,9 @@ class TransformerDecoder(d2l.AttentionDecoder): return self._attention_weights ``` -## 培训 +## 训练 -让我们通过遵循变压器架构来实例化编码器解码器模型。在这里,我们指定变压器编码器和变压器解码器都有 2 层,使用 4 头注意力。与 :numref:`sec_seq2seq_training` 类似,我们训练变压器模型,以便在英语-法语机器翻译数据集上进行序列到序列的学习。 +依照 Transformer 架构来实例化“编码器-解码器”模型。在这里,指定 Transformer 的编码器和解码器都是 2 层,使用 4 头注意力。与 :numref:`sec_seq2seq_training` 类似,为了进行序列到序列的学习,我们在英语到法语的机器翻译数据集上训练 Transformer 模型。 ```{.python .input} num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10 @@ -535,7 +533,7 @@ net = d2l.EncoderDecoder(encoder, decoder) d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ``` -训练结束后,我们使用变压器模型将一些英语句子翻译成法语并计算它们的 BLEU 分数。 +训练结束后,使用 Transformer 模型将一些英语句子翻译成法语,并且计算它们的 BLEU 分数。 ```{.python .input} #@tab all @@ -548,7 +546,7 @@ for eng, fra in zip(engs, fras): f'bleu {d2l.bleu(translation, fra, k=2):.3f}') ``` -让我们在将最后一个英语句子翻译成法语时可视化变压器的注意力重量。编码器自我注意权重的形状为(编码器层数、注意头数、`num_steps` 或查询数、`num_steps` 或键值对的数量)。 +在翻译最后一个英语句子时,对 Transformer 的注意力权重进行可视化。编码器自注意力权重的形状为(编码器层数、注意力头数、`num_steps`或查询个数、`num_steps` 或“键-值”对的数量)。 ```{.python .input} #@tab all @@ -558,7 +556,7 @@ enc_attention_weights = d2l.reshape( enc_attention_weights.shape ``` -在编码器的自我注意中,查询和键来自相同的输入序列。由于填充令牌不具有意义,并且输入序列的指定有效长度,因此没有查询参与填充令牌的位置。在以下内容中,将按行呈现两层多头注意力权重。每位负责人都根据查询、键和值的单独表示子空间独立出席。 +在编码器的自注意力中,查询和键来自相同的输入序列。由于填充的令牌不具有意义,因此通过指定输入序列的有效长度,避免对填充的位置计算注意力。接下来,将逐行呈现两层多头注意力权重。每个注意力头都根据查询、键和值的不同的表示子空间来表示不同的注意力。 ```{.python .input} d2l.show_heatmaps( @@ -574,7 +572,7 @@ d2l.show_heatmaps( figsize=(7, 3.5)) ``` -为了可视化解码器的自我注意力权重和编码器-解码器的注意权重,我们需要更多的数据操作。例如,我们用零填充蒙面的注意力重量。请注意,解码器自我注意权重和编码器注意权重都有相同的查询:序列开始令牌后跟输出令牌。 +为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要做更多的数据操作。例如,我们用零填充被掩码覆盖的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以序列开始令牌 (beginning-of-sequence, BOS) 开头,及后续逐个生成的令牌序列。 ```{.python .input} dec_attention_weights_2d = [d2l.tensor(head[0]).tolist() @@ -603,7 +601,7 @@ dec_self_attention_weights, dec_inter_attention_weights = \ dec_self_attention_weights.shape, dec_inter_attention_weights.shape ``` -由于解码器自我注意的自动回归属性,查询位置后没有查询参与键值对。 +由于解码器自注意力的自回归属性,查询不会对当前位置之后的“键-值”对进行注意力计算。 ```{.python .input} #@tab all @@ -614,7 +612,7 @@ d2l.show_heatmaps( titles=['Head %d' % i for i in range(1, 5)], figsize=(7, 3.5)) ``` -与编码器自我注意的情况类似,通过输入序列的指定有效长度,输出序列中的任何查询都不会参与输入序列中的填充标记。 +与编码器自注意力的情况类似,通过指定输入序列的有效长度,输出序列中的任何查询都不会与输入序列中的填充位置的令牌进行注意力计算。 ```{.python .input} #@tab all @@ -624,23 +622,23 @@ d2l.show_heatmaps( figsize=(7, 3.5)) ``` -尽管变压器架构最初是为了顺序到序列的学习而提出的,但正如我们将在本书后面发现的那样,变压器编码器或变压器解码器通常被单独用于不同的深度学习任务。 +尽管 Transformer 架构是为了序列到序列的学习而提出的,但正如我们将在本书后面提及的那样,Transformer 编码器或 Transformer 解码器通常被单独用于不同的深度学习任务中。 ## 摘要 -* 变压器是编码器解码器架构的一个实例,尽管在实践中可以单独使用编码器或解码器。 -* 在变压器中,多头自我注意力用于表示输入序列和输出序列,尽管解码器必须通过蒙版本保留自动回归属性。 -* 变压器中的残余连接和层标准化对于训练非常深入的模型都很重要。 -* 变压器模型中的向定位前馈网络使用相同的 MLP 转换所有序列位置的表示。 +* Transformer 是“编码器-解码器”架构的一个实例,尽管在实践中编码器或解码器可以单独使用。 +* 在 Transformer 中,多头自注意力用于表示输入序列和输出序列,尽管解码器必须通过掩码机制来保留自回归属性。 +* Transformer 中的残差连接和层归一化对于训练非常深度的模型很重要。 +* Transformer 模型中的基于位置的前馈网络使用相同的多层感知机,对序列的所有位置的表示进行转换。 ## 练习 -1. 在实验中训练更深的变压器。它如何影响培训速度和翻译绩效? -1. 在变压器中用添加剂注意力取代缩放的点产品注意力是不错的主意吗?为什么? -1. 对于语言建模,我们应该使用 Transor 编码器、解码器还是两者?如何设计这种方法? -1. 如果输入序列很长,变形金刚会面临什么挑战?为什么? -1. 如何提高变形金刚的计算和内存效率?Hind: you may refer to the survey paper by Tay et al. :cite:`Tay.Dehghani.Bahri.ea.2020`。 -1. 我们如何在不使用 CNN 的情况下为图像分类任务设计基于变压器的模型?Hind: you may refer to the Vision Transformer :cite:`Dosovitskiy.Beyer.Kolesnikov.ea.2021`。 +1. 在实验中训练更深的 Transformer 将如何影响训练速度和翻译效果? +1. 在 Transformer 中用可加性注意力取代缩放的“点-积”注意力是不是个好办法?为什么? +1. 对于语言模型,我们应该使用 Transformer 的编码器或者解码器还是两者都用?如何设计? +1. 如果输入序列很长, Transformer 会面临什么挑战?为什么? +1. 如何提高 Transformer 的计算和内存效率?提示:可以参考 Tay et al. 的论文 :cite:`Tay.Dehghani.Bahri.ea.2020`。 +1. 如何在不使用 CNN 的情况下为图像分类任务设计基于 Transformer 的模型?提示:可以参考 Vision Transformer :cite:`Dosovitskiy.Beyer.Kolesnikov.ea.2021`。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/348) From 893f50344625724c9cf37001251d1351e700689b Mon Sep 17 00:00:00 2001 From: thebesttv Date: Mon, 12 Apr 2021 12:58:21 +0800 Subject: [PATCH 032/103] add my name in preface/index.md and update mlp.md (#735) * add my name in preface/index.md * update translations in preface/index.md * update translations in mlp.md * update translations in preface/index.md --- chapter_multilayer-perceptrons/mlp.md | 6 +++--- chapter_preface/index.md | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index c808ec531..128d91468 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -150,7 +150,7 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of relu', 使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题(稍后将详细介绍)。 -注意,ReLU函数有许多变体,包括*参数化ReLU*(Parameterized ReLU,*pReLU*)函数 :cite:`He.Zhang.Ren.ea.2015`。该变体是为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过: +注意,ReLU函数有许多变体,包括*参数化ReLU*(Parameterized ReLU,*pReLU*)函数 :cite:`He.Zhang.Ren.ea.2015`。该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过: $$\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).$$ @@ -160,7 +160,7 @@ $$\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).$$ (**$$\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.$$**) -在最早的神经网络中,科学家们感兴趣的是对“激发”或“不激发”的生物神经元进行建模。因此,这一领域的先驱,如人工神经元的发明者麦卡洛克和皮茨。从他们开始就专注于阈值单元。阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。 +在最早的神经网络中,科学家们感兴趣的是对“激发”或“不激发”的生物神经元进行建模。因此,这一领域的先驱,如人工神经元的发明者麦卡洛克和皮茨,从他们开始就专注于阈值单元。阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。 当人们的注意力逐渐转移到基于梯度的学习时,sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。当我们想要将输出视作二分类问题的概率时,sigmoid仍然被广泛用作输出单元上的激活函数(你可以将sigmoid视为softmax的特例)。然而,sigmoid在隐藏层中已经较少使用,它在大部分时候已经被更简单、更容易训练的ReLU所取代。在后面关于循环神经网络的章节中,我们将描述利用sigmoid单元来控制时序信息流动的结构。 @@ -264,7 +264,7 @@ d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of tanh', figsize=(5, 2.5)) ``` -总结一下,我们现在知道如何结合非线性函数来构建更强表达能力的多层神经网络结构。顺便说一句,你的知识已经让你掌握了一个类似于1990年左右深度学习从业者的工具。在某些方面,你比在20世纪90年代工作的任何人都有优势。这是因为你可以利用功能强大的开源深度学习框架。你只需使用几行代码就可以快速构建模型。在以前,训练这些网络需要研究人员编写数千行的C或Fortran代码。 +总结一下,我们现在知道如何结合非线性函数来构建具有更强表达能力的多层神经网络结构。顺便说一句,你的知识已经让你掌握了一个类似于1990年左右深度学习从业者的工具。在某些方面,你比在20世纪90年代工作的任何人都有优势,因为你可以利用功能强大的开源深度学习框架,只需几行代码就可以快速构建模型。在以前,训练这些网络需要研究人员编写数千行的C或Fortran代码。 ## 小结 diff --git a/chapter_preface/index.md b/chapter_preface/index.md index a81d640e6..4805493c0 100644 --- a/chapter_preface/index.md +++ b/chapter_preface/index.md @@ -133,9 +133,9 @@ import tensorflow as tf 与本书相关,我们已经启动了一个论坛,在[discuss.d2l.ai](https://discuss.d2l.ai/)。当你对本书的任何一节有疑问时,你可以在每一节的末尾找到相关的讨论页链接。 -## 确认 +## 致谢 -我们感谢数以百计的英文和中文草稿的撰稿人。他们帮助改进了内容或提供了有价值的反馈。特别地,我们要感谢每一位英文稿的撰稿人,感谢他们为大家做得更好。他们的GitHub ID或名称是(没有特定顺序):alxnorden, avinashingit, bowen0701, brettkoonce, Chaitanya Prakash Bapat, +我们感谢中英文草稿的数百位撰稿人。他们帮助改进了内容并提供了宝贵的反馈。特别地,我们要感谢这份中文稿的每一位撰稿人,是他们的无私奉献让这本书变得更好。他们的GitHub ID或名称是(没有特定顺序):alxnorden, avinashingit, bowen0701, brettkoonce, Chaitanya Prakash Bapat, cryptonaut, Davide Fiocco, edgarroman, gkutiel, John Mitro, Liang Pu, Rahul Agarwal, Mohamed Ali Jamaoui, Michael (Stu) Stewart, Mike Müller, NRauschmayr, Prakhar Srivastav, sad-, sfermigier, Sheng Zha, sundeepteki, @@ -162,7 +162,8 @@ Hoa Nguyen, manuel-arno-korfmann-webentwicklung, aterzis-personal, nxby, Xiaotin mathresearch, mzz2017, jroberayalas, iluu, ghejc, BSharmi, vkramdev, simonwardjones, LakshKD, TalNeoran, djliden, Nikhil95, Oren Barkan, guoweis, haozhu233, pratikhack, 315930399, tayfununal, steinsag, charleybeller, Andrew Lumsdaine, Jiekui Zhang, Deepak Pathak, Florian Donhauser, Tim Gates, -Adriaan Tijsseling, Ron Medina, Gaurav Saha, Murat Semerci, [Lei Mao](https://github.com/leimao), [Zhu Yuanxiang](https://zhuyuanxiang.github.io) +Adriaan Tijsseling, Ron Medina, Gaurav Saha, Murat Semerci, [Lei Mao](https://github.com/leimao), [Zhu Yuanxiang](https://zhuyuanxiang.github.io), +[thebesttv](https://github.com/thebesttv/)。 我们感谢Amazon Web Services,特别是Swami Sivasubramanian、Raju Gulabani、Charlie Bell和Andrew Jassy对撰写本书的慷慨支持。如果没有可用的时间、资源、与同事的讨论和不断的鼓励,这本书就不会出版。 From 860f056bf8961e7404bda8b38afdb9f9b5ddb655 Mon Sep 17 00:00:00 2001 From: Linhan Wu Date: Mon, 12 Apr 2021 13:00:14 +0800 Subject: [PATCH 033/103] fix typo and translation issues in alexnet.md (#736) * fix 4.4.1.2. Model Complexity translation issues * fix typo in chapter_multilayer-perceptrons/environment.md * fix typo and translation issues in kaggle-house-price.md * fix typo in model-construction.md * fix typo and translation issues in use-gpu.md * fix typo and translation issues in alexnet.md Co-authored-by: Linhan_Wu --- chapter_convolutional-modern/alexnet.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chapter_convolutional-modern/alexnet.md b/chapter_convolutional-modern/alexnet.md index f473fa26c..9786537c7 100644 --- a/chapter_convolutional-modern/alexnet.md +++ b/chapter_convolutional-modern/alexnet.md @@ -62,7 +62,7 @@ CPU的每个核心都拥有高时钟频率的运行能力,和高达数MB的三 ## AlexNet -2012年,AlexNet横空出世。它首次证明了学习到的特征可以超越手工设计的特征。它一举了打破计算机视觉研究的现状。 +2012年,AlexNet横空出世。它首次证明了学习到的特征可以超越手工设计的特征。它一举打破了计算机视觉研究的现状。 AlexNet使用了8层卷积神经网络,并以很大的优势赢得了2012年ImageNet图像识别挑战赛。 AlexNet和LeNet的架构非常相似,如 :numref:`fig_alexnet` 所示。 @@ -81,10 +81,10 @@ AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全 ### 模型设计 在AlexNet的第一层,卷积窗口的形状是 $11\times11$。 -由于大多数ImageNet中图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 -第二层中的卷积窗形状被缩减为 $5\times5$,然后是 $3\times3$。 -此外,在第一层、第二层和第五层之后,加入窗口形状为 $3\times3$、步幅为2的最大池化层。 -此外,AlexNet的卷积通道是LeNet的10倍。 +由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 +第二层中的卷积窗口形状被缩减为 $5\times5$,然后是 $3\times3$。 +此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为 $3\times3$、步幅为2的最大池化层。 +而且,AlexNet的卷积通道数目是LeNet的10倍。 在最后一个卷积层后有两个全连接层,分别有4096个输出。 这两个巨大的全连接层拥有将近1GB的模型参数。 From 250f3cdf2305ed0e73c46e6b609eeea700468403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=A6=E9=98=B3?= <49838178+liu-mengyang@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:44:06 +0800 Subject: [PATCH 034/103] fix typo in calculus.md (#740) --- chapter_preliminaries/calculus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preliminaries/calculus.md b/chapter_preliminaries/calculus.md index bd5a39fe1..f0ea235ed 100644 --- a/chapter_preliminaries/calculus.md +++ b/chapter_preliminaries/calculus.md @@ -245,7 +245,7 @@ $$\frac{dy}{dx_i} = \frac{dy}{du_1} \frac{du_1}{dx_i} + \frac{dy}{du_2} \frac{du ## 练习 -1. 绘制函数$y = f(x) = x^3 - \frac{1}{x}$和其在$x = 1处切线的图像。 +1. 绘制函数$y = f(x) = x^3 - \frac{1}{x}$和其在$x = 1$处切线的图像。 1. 求函数$f(\mathbf{x}) = 3x_1^2 + 5e^{x_2}$的梯度。 1. 函数$f(\mathbf{x}) = \|\mathbf{x}\|_2$的梯度是什么? 1. 你可以写出函数$u = f(x, y, z)$,其中$x = x(a, b)$,$y = y(a, b)$,$z = z(a, b)$的链式法则吗? From 9fe42908603bc5d75c714398168b88aed4fed12d Mon Sep 17 00:00:00 2001 From: zppet Date: Tue, 13 Apr 2021 11:45:42 +0800 Subject: [PATCH 035/103] Translation issue in chp #5 computation GPU (#738) * Update index.md updated for typos and E2C translation * Update use-gpu.md --- chapter_deep-learning-computation/use-gpu.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter_deep-learning-computation/use-gpu.md b/chapter_deep-learning-computation/use-gpu.md index 792c813eb..163647335 100644 --- a/chapter_deep-learning-computation/use-gpu.md +++ b/chapter_deep-learning-computation/use-gpu.md @@ -197,7 +197,7 @@ Y 如果我们要计算`X + Y`,我们需要决定在哪里执行这个操作。例如,如 :numref:`fig_copyto` 所示,我们可以将`X`传输到第二个GPU并在那里执行操作。 *不要*简单地`X`加上`Y`, -因为这会导致异常。运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。由于`Y`位于第二个GPU上,所以我们需要将`X`移到那里,然后才能添加这两个GPU。 +因为这会导致异常。运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。由于`Y`位于第二个GPU上,所以我们需要将`X`移到那里,然后才能执行相加运算。 ![复制数据以在同一设备上执行操作。](../img/copyto.svg) :label:`fig_copyto` @@ -231,7 +231,7 @@ Y + Z ``` :begin_tab:`mxnet` -假设变量`Z`已经存在于第二个GPU上。如果我们还是调用`Z.copyto(gpu(1))`怎么办?它将复制并分配新的显存,即使该变量已经存在于所需的设备上。有时,根据代码运行的环境不同,两个变量可能已经存在于同一设备上。因此,我们只想在变量存在于不同设备中时进行复制。在这种情况下,我们可以调用`as_in_ctx`。如果变量已经存在于指定的设备中,则这不会进行任何操作。除非你特别想创建一个复制,否则选择`as_in_ctx`方法。 +假设变量`Z`已经存在于第二个GPU上。如果我们还是调用`Z.copyto(gpu(1))`怎么办?即使该变量已经存在于目标设备(第二个GPU)上,它仍将被复制并保存在新分配的显存中。有时,根据代码运行的环境不同,两个变量可能已经存在于同一设备上。因此,我们只想在变量存在于不同设备中时进行复制。在这种情况下,我们可以调用`as_in_ctx`。如果变量已经存在于指定的设备中,则这不会进行任何操作。除非你特别想创建一个复制,否则选择`as_in_ctx`方法。 :end_tab: :begin_tab:`pytorch` From dafe60db9c55e7fef00b0ff1dedff88847fcc839 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:03:34 -0700 Subject: [PATCH 036/103] [slides] kaggle (#742) --- .../kaggle-house-price.md | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/chapter_multilayer-perceptrons/kaggle-house-price.md b/chapter_multilayer-perceptrons/kaggle-house-price.md index d627681c2..811b6f531 100644 --- a/chapter_multilayer-perceptrons/kaggle-house-price.md +++ b/chapter_multilayer-perceptrons/kaggle-house-price.md @@ -7,7 +7,7 @@ ## 下载和缓存数据集 -在整本书中,我们将在各种下载的数据集上训练和测试模型。在这里,我们实现了几个实用函数来方便下载数据。首先,我们维护字典`DATA_HUB`,其将数据集名称的字符串映射到数据集相关的二元组上,这个二元组包含数据集的url和验证文件完整性的sha-1密钥。所有这样的数据集都托管在地址为`DATA_URL`的站点上。 +在整本书中,我们将在各种下载的数据集上训练和测试模型。在这里,我们(**实现几个函数来方便下载数据**)。首先,我们维护字典`DATA_HUB`,其将数据集名称的字符串映射到数据集相关的二元组上,这个二元组包含数据集的url和验证文件完整性的sha-1密钥。所有这样的数据集都托管在地址为`DATA_URL`的站点上。 ```{.python .input} #@tab all @@ -93,7 +93,7 @@ def download_all(): #@save 注意,竞赛数据分为训练集和测试集。每条记录都包括房屋的属性值和属性,如街道类型、施工年份、屋顶类型、地下室状况等。这些特征由各种数据类型组成。例如,建筑年份由整数表示,屋顶类型由离散类别表示,其他特征由浮点数表示。这就是现实让事情变得复杂的地方:例如,一些数据完全丢失了,缺失值被简单地标记为“NA”。每套房子的价格只出现在训练集中(毕竟这是一场比赛)。我们将希望划分训练集以创建验证集,但是在将预测结果上传到Kaggle之后,我们只能在官方测试集中评估我们的模型。在 :numref:`fig_house_pricing` 中,"Data"选项卡有下载数据的链接。 -开始之前,我们将使用`pandas`读入并处理数据,这是我们在 :numref:`sec_pandas` 中引入的。因此,在继续操作之前,您需要确保已安装`pandas`。幸运的是,如果你正在用Jupyter阅读该书,你可以在不离开笔记本的情况下安装`pandas`。 +开始之前,我们将[**使用`pandas`读入并处理数据**],这是我们在 :numref:`sec_pandas` 中引入的。因此,在继续操作之前,您需要确保已安装`pandas`。幸运的是,如果你正在用Jupyter阅读该书,你可以在不离开笔记本的情况下安装`pandas`。 ```{.python .input} # 如果pandas没有被安装,请取消下一句的注释。 @@ -161,14 +161,14 @@ print(train_data.shape) print(test_data.shape) ``` -让我们看看前四个和最后两个特征,以及前四个样本的标签(房价)。 +让我们看看[**前四个和最后两个特征,以及相应标签(房价)**]。 ```{.python .input} #@tab all print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]) ``` -我们可以看到,在每个样本中,第一个特征是ID,这有助于模型识别每个训练样本。虽然这很方便,但它不携带任何用于预测的信息。因此,在将数据提供给模型之前,我们将其从数据集中删除。 +我们可以看到,[**在每个样本中,第一个特征是ID,**]这有助于模型识别每个训练样本。虽然这很方便,但它不携带任何用于预测的信息。因此,在将数据提供给模型之前,[**我们将其从数据集中删除**]。 ```{.python .input} #@tab all @@ -177,7 +177,7 @@ all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:])) ## 数据预处理 -如上所述,我们有各种各样的数据类型。在开始建模之前,我们需要对数据进行预处理。让我们从数字特征开始。首先,我们应用启发式方法,将所有缺失的值替换为相应特征的平均值。然后,为了将所有特征放在一个共同的尺度上,我们通过将特征重新缩放到零均值和单位方差来*标准化*数据: +如上所述,我们有各种各样的数据类型。在开始建模之前,我们需要对数据进行预处理。让我们从数字特征开始。首先,我们应用启发式方法,[**将所有缺失的值替换为相应特征的平均值。**]然后,为了将所有特征放在一个共同的尺度上,我们(**通过将特征重新缩放到零均值和单位方差来*标准化*数据**): $$x \leftarrow \frac{x - \mu}{\sigma}.$$ @@ -192,7 +192,7 @@ all_features[numeric_features] = all_features[numeric_features].apply( all_features[numeric_features] = all_features[numeric_features].fillna(0) ``` -接下来,我们处理离散值。这包括诸如“MSZoning”之类的特征。我们用一次独热编码替换它们,方法与前面将多类别标签转换为向量的方式相同(请参见 :numref:`subsec_classification-problem` )。例如,“MSZoning”包含值“RL”和“Rm”。将创建两个新的指示器特征“MSZoning_RL”和“MSZoning_RM”,其值为0或1。根据独热编码,如果“MSZoning”的原始值为“RL”,则:“MSZoning_RL”为1,“MSZoning_RM”为0。`pandas`软件包会自动为我们实现这一点。 +接下来,我们[**处理离散值。**]这包括诸如“MSZoning”之类的特征。(**我们用一次独热编码替换它们**),方法与前面将多类别标签转换为向量的方式相同(请参见 :numref:`subsec_classification-problem` )。例如,“MSZoning”包含值“RL”和“Rm”。将创建两个新的指示器特征“MSZoning_RL”和“MSZoning_RM”,其值为0或1。根据独热编码,如果“MSZoning”的原始值为“RL”,则:“MSZoning_RL”为1,“MSZoning_RM”为0。`pandas`软件包会自动为我们实现这一点。 ```{.python .input} #@tab all @@ -201,7 +201,7 @@ all_features = pd.get_dummies(all_features, dummy_na=True) all_features.shape ``` -你可以看到,此转换会将特征的数量从79个增加到331个。最后,通过`values`属性,我们可以从`pandas`格式中提取NumPy格式,并将其转换为张量表示用于训练。 +你可以看到,此转换会将特征的数量从79个增加到331个。最后,通过`values`属性,我们可以[**从`pandas`格式中提取NumPy格式,并将其转换为张量表示**]用于训练。 ```{.python .input} #@tab all @@ -212,7 +212,7 @@ train_labels = d2l.tensor( train_data.SalePrice.values.reshape(-1, 1), dtype=d2l.float32) ``` -## 训练 +## [**训练**] 首先,我们训练一个带有损失平方的线性模型。毫不奇怪,我们的线性模型不会让我们在竞赛中获胜,但线性模型提供了一种健全性检查,以查看数据中是否存在有意义的信息。如果我们在这里不能做得比随机猜测更好,那么我们很可能存在数据处理错误。如果一切顺利,线性模型将作为基线模型,让我们直观地知道简单的模型离报告最好的模型有多近,让我们感觉到我们应该从更酷炫的模型中获得多少收益。 @@ -247,9 +247,9 @@ def get_net(): return net ``` -对于房价,就像股票价格一样,我们关心的是相对数量,而不是绝对数量。因此,我们更关心相对误差$\frac{y - \hat{y}}{y}$,而不是绝对误差$y - \hat{y}$。例如,如果我们在俄亥俄州农村地区估计一栋房子的价格时,我们的预测偏差了10万美元,在那里一栋典型的房子的价值是12.5万美元,那么我们可能做得很糟糕。另一方面,如果我们在加州豪宅区的预测出现了这个数字的偏差,这可能是一个惊人的准确预测(在那里,房价均值超过400万美元)。 +对于房价,就像股票价格一样,我们关心的是相对数量,而不是绝对数量。因此,[**我们更关心相对误差$\frac{y - \hat{y}}{y}$,**]而不是绝对误差$y - \hat{y}$。例如,如果我们在俄亥俄州农村地区估计一栋房子的价格时,我们的预测偏差了10万美元,在那里一栋典型的房子的价值是12.5万美元,那么我们可能做得很糟糕。另一方面,如果我们在加州豪宅区的预测出现了这个数字的偏差,这可能是一个惊人的准确预测(在那里,房价均值超过400万美元)。 -解决这个问题的一种方法是用价格预测的对数来衡量差异。事实上,这也是比赛中官方用来评价提交质量的误差指标。即将 $\delta$ for $|\log y - \log \hat{y}| \leq \delta$转换为$e^{-\delta} \leq \frac{\hat{y}}{y} \leq e^\delta$。这使得预测价格的对数与真实标签价格的对数之间出现以下均方根误差: +(**解决这个问题的一种方法是用价格预测的对数来衡量差异**)。事实上,这也是比赛中官方用来评价提交质量的误差指标。即将 $\delta$ for $|\log y - \log \hat{y}| \leq \delta$转换为$e^{-\delta} \leq \frac{\hat{y}}{y} \leq e^\delta$。这使得预测价格的对数与真实标签价格的对数之间出现以下均方根误差: $$\sqrt{\frac{1}{n}\sum_{i=1}^n\left(\log y_i -\log \hat{y}_i\right)^2}.$$ @@ -279,8 +279,7 @@ def log_rmse(y_true, y_pred): tf.math.log(y_true), tf.math.log(clipped_preds)))) ``` -与前面的部分不同,我们的训练函数将依赖于Adam优化器(我们将在后面更详细地描述它)。这个优化器的主要吸引力在于,尽管在提供无限资源进行超参数优化方面没有做得更好(有时更差),但人们发现它对初始学习率不那么敏感。 - +与前面的部分不同,[**我们的训练函数将借助Adam优化器**](我们将在后面更详细地描述它)。这个优化器的主要吸引力在于,尽管在提供无限资源进行超参数优化方面没有做得更好(有时更差),但人们发现它对初始学习率不那么敏感。 ```{.python .input} def train(net, train_features, train_labels, test_features, test_labels, @@ -347,7 +346,7 @@ def train(net, train_features, train_labels, test_features, test_labels, return train_ls, test_ls ``` -## $K$折交叉验证 +## [**$K$折交叉验证**] 你可能还记得,我们在讨论模型选择的部分( :numref:`sec_model_selection` )中介绍了$K$折交叉验证。这有助于模型选择和超参数调整。我们首先需要一个函数,在$K$折交叉验证过程中返回第$i$折的数据。它选择第$i$个切片作为验证数据,其余部分作为训练数据。注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,我们肯定会做一些更聪明的改变。但是这种改变所增加的复杂性可能会使代码看起来更乱。在这里可以忽略这些改变,因为我们的问题很简单。 @@ -370,7 +369,7 @@ def get_k_fold_data(k, i, X, y): return X_train, y_train, X_valid, y_valid ``` -当我们在$K$折交叉验证中训练$K$次后,返回训练和验证误差的平均值。 +当我们在$K$折交叉验证中训练$K$次后,[**返回训练和验证误差的平均值**]。 ```{.python .input} #@tab all @@ -393,7 +392,7 @@ def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay, return train_l_sum / k, valid_l_sum / k ``` -## 模型选择 +## [**模型选择**] 在本例中,我们选择了一组未调优的超参数,并将其留给读者来改进模型。找到一个好的选择可能需要时间,这取决于一个人优化了多少变量。有了足够大的数据集和合理设置的超参数,$K$折交叉验证往往对多次测试具有相当的适应性。然而,如果我们尝试了不合理的大量选项,我们可能会发现验证效果不再代表真正的误差。 @@ -408,7 +407,7 @@ print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, ' 请注意,有时一组超参数的训练误差可能非常低,但$K$折交叉验证的误差要高得多。这表明我们过拟合了。在整个训练过程中,你将希望监控训练误差和验证误差这两个数字。较少的过拟合可能表明现有数据可以支撑一个更强大的模型。较大的过拟合可能意味着我们可以通过正则化技术来获益。 -## 提交Kaggle的预测 +## [**提交你的Kaggle预测**] 既然我们知道应该选择什么样的超参数,我们不妨使用所有数据对其进行训练(而不是仅使用交叉验证中使用的$1-1/K$的数据)。然后,我们通过这种方式获得的模型可以应用于测试集。将预测保存在csv文件中可以简化将结果上传到Kaggle的过程。 From 4bd2c89336d88f2e93905091423b9966e3714e91 Mon Sep 17 00:00:00 2001 From: Aston Zhang Date: Thu, 15 Apr 2021 20:03:15 +0000 Subject: [PATCH 037/103] add ch12 first 3 sections --- .../async-computation.md | 213 +++++++++ .../async-computation_origin.md | 235 ++++++++++ .../auto-parallelism.md | 189 ++++++++ .../auto-parallelism_origin.md | 200 +++++++++ .../hybridize.md | 392 +++++++++++++++++ .../hybridize_origin.md | 406 ++++++++++++++++++ chapter_computational-performance/index.md | 16 + .../index_origin.md | 23 + 8 files changed, 1674 insertions(+) create mode 100644 chapter_computational-performance/async-computation.md create mode 100644 chapter_computational-performance/async-computation_origin.md create mode 100644 chapter_computational-performance/auto-parallelism.md create mode 100644 chapter_computational-performance/auto-parallelism_origin.md create mode 100644 chapter_computational-performance/hybridize.md create mode 100644 chapter_computational-performance/hybridize_origin.md create mode 100644 chapter_computational-performance/index.md create mode 100644 chapter_computational-performance/index_origin.md diff --git a/chapter_computational-performance/async-computation.md b/chapter_computational-performance/async-computation.md new file mode 100644 index 000000000..f737d1fbe --- /dev/null +++ b/chapter_computational-performance/async-computation.md @@ -0,0 +1,213 @@ +# 异步计算 +:label:`sec_async` + +今天的计算机是高度并行的系统,由多个 CPU 核心(通常是每个核心多个线程)、每个 GPU 的多个处理元素以及通常每台设备多个 GPU 组成。简而言之,我们可以同时处理许多不同的事物,通常是在不同的设备上。不幸的是,Python 不是编写并行和异步代码的好方法,至少没有一些额外的帮助。毕竟,Python 是单线程的,这在未来不太可能改变。MxNet 和 TensorFlow 等深度学习框架采用 * 异步编程 * 模型来提高性能,而 PyTorch 则使用 Python 自己的调度程序,从而实现不同的性能权衡。对于 PyTorch,默认情况下,GPU 操作是异步的。当您调用使用 GPU 的函数时,这些操作将入队到特定设备,但不一定要等到以后才执行。这使我们能够并行执行更多计算,包括 CPU 或其他 GPU 上的操作。 + +因此,了解异步编程的工作原理有助于我们通过主动降低计算需求和相互依赖性来开发更高效的程序。这使我们能够减少内存开销并提高处理器利用率。 + +```{.python .input} +from d2l import mxnet as d2l +import numpy, os, subprocess +from mxnet import autograd, gluon, np, npx +from mxnet.gluon import nn +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import numpy, os, subprocess +import torch +from torch import nn +``` + +## 通过后端进行异步 + +:begin_tab:`mxnet` +对于热身,请考虑以下玩具问题:我们想生成一个随机矩阵并将其乘以。让我们在 NumPy 和 `mxnet.np` 中这样做来看看差异。 +:end_tab: + +:begin_tab:`pytorch` +对于热身,请考虑以下玩具问题:我们想生成一个随机矩阵并将其乘以。让我们在 NumPy 和 PyTorch 张量中这样做来看看差异。请注意,PyTorch `tensor` 是在 GPU 上定义的。 +:end_tab: + +```{.python .input} +with d2l.Benchmark('numpy'): + for _ in range(10): + a = numpy.random.normal(size=(1000, 1000)) + b = numpy.dot(a, a) + +with d2l.Benchmark('mxnet.np'): + for _ in range(10): + a = np.random.normal(size=(1000, 1000)) + b = np.dot(a, a) +``` + +```{.python .input} +#@tab pytorch +# Warmup for GPU computation +device = d2l.try_gpu() +a = torch.randn(size=(1000, 1000), device=device) +b = torch.mm(a, a) + +with d2l.Benchmark('numpy'): + for _ in range(10): + a = numpy.random.normal(size=(1000, 1000)) + b = numpy.dot(a, a) + +with d2l.Benchmark('torch'): + for _ in range(10): + a = torch.randn(size=(1000, 1000), device=device) + b = torch.mm(a, a) +``` + +:begin_tab:`mxnet` +通过 MxNet 的基准输出速度快了数量级。由于两者都在同一个处理器上执行,因此必须继续进行其他事情。强制 MxNet 在返回之前完成所有后端计算会显示以前发生的情况:计算由后端执行,而前端将控制权返回给 Python。 +:end_tab: + +:begin_tab:`pytorch` +通过 PyTorch 的基准输出速度快了数量级。NumPy 点积在 CPU 处理器上执行,而 PyTorch 矩阵乘法则在 GPU 上执行,因此后者的速度预计会快得多。但是,巨大的时差表明必须发生其他事情。默认情况下,PyTorch 中的 GPU 操作是异步的。强制 PyTorch 在返回之前完成所有计算会显示以前发生的情况:计算由后端执行,而前端则将控制权返回给 Python。 +:end_tab: + +```{.python .input} +with d2l.Benchmark(): + for _ in range(10): + a = np.random.normal(size=(1000, 1000)) + b = np.dot(a, a) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +with d2l.Benchmark(): + for _ in range(10): + a = torch.randn(size=(1000, 1000), device=device) + b = torch.mm(a, a) + torch.cuda.synchronize(device) +``` + +:begin_tab:`mxnet` +广义地说,MxNet 有一个用于与用户直接交互的前端(例如通过 Python)以及系统用于执行计算的后端。如 :numref:`fig_frontends` 所示,用户可以使用各种前端语言(如 Python、R、Scala 和 C++)编写 MxNet 程序。无论使用哪种前端编程语言,MxNet 程序的执行主要发生在 C ++ 实现的后端。前端语言发布的操作将传递到后端执行。后端管理自己的线程,这些线程持续收集和执行排队任务。请注意,为此,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化彼此依赖的操作。 +:end_tab: + +:begin_tab:`pytorch` +广义地说,PyTorch 有一个用于与用户直接交互的前端(例如通过 Python)以及系统用于执行计算的后端。如 :numref:`fig_frontends` 所示,用户可以使用各种前端语言(如 Python 和 C ++)编写 PyTorch 程序。无论使用哪种前端编程语言,PyTorch 程序的执行主要发生在 C ++ 实现的后端。前端语言发布的操作将传递到后端执行。后端管理自己的线程,这些线程持续收集和执行排队任务。请注意,为此,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化彼此依赖的操作。 +:end_tab: + +![Programming language frontends and deep learning framework backends.](../img/frontends.png) +:width:`300px` +:label:`fig_frontends` + +让我们看另一个玩具示例,以便更好地理解依赖关系图。 + +```{.python .input} +x = np.ones((1, 2)) +y = np.ones((1, 2)) +z = x * y + 2 +z +``` + +```{.python .input} +#@tab pytorch +x = torch.ones((1, 2), device=device) +y = torch.ones((1, 2), device=device) +z = x * y + 2 +z +``` + +![The backend tracks dependencies between various steps in the computational graph.](../img/asyncgraph.svg) +:label:`fig_asyncgraph` + +上面的代码片段也在 :numref:`fig_asyncgraph` 中进行了说明。每当 Python 前端线程执行前三个语句之一时,它只需将任务返回到后端队列。当最后一条语句的结果需要 * 打印 * 时,Python 前端线程将等待 C ++ 后端线程完成计算变量 `z` 的结果。这种设计的一个好处是 Python 前端线程不需要执行实际的计算。因此,无论 Python 的性能如何,对程序的整体性能都没有什么影响。:numref:`fig_threading` 说明了前端和后端的交互方式。 + +![Interactions of the frontend and backend.](../img/threading.svg) +:label:`fig_threading` + +:begin_tab:`mxnet` +## 障碍和阻滞剂 + +有许多操作会迫使 Python 等待完成: +* 最明显的是,无论计算指令何时发出,`npx.waitall()` 都会等到所有计算完成。实际上,除非绝对必要,否则使用此操作符是一个坏主意,因为它可能会导致性能不佳。 +* 如果我们只想等到特定变量可用,我们可以调用 `z.wait_to_read()`。在这种情况下,MxNet 块返回到 Python,直到计算出变量 `z`。其他计算之后可能会继续进行。 + +让我们看看这在实践中是如何运作的。 +:end_tab: + +```{.python .input} +with d2l.Benchmark('waitall'): + b = np.dot(a, a) + npx.waitall() + +with d2l.Benchmark('wait_to_read'): + b = np.dot(a, a) + b.wait_to_read() +``` + +:begin_tab:`mxnet` +两项操作需要大约相同的时间才能完成。除了显而易见的阻止操作之外,我们建议您知道 * 隐式 * 阻止程序。打印变量显然需要变量可用,因此它是阻止程序。最后,由于 NumPy 没有异步概念,通过 `z.asnumpy()` 转换为 NumPy 以及通过 `z.item()` 转换为标量的操作都受到阻碍。它需要像 `print` 函数一样访问这些值。 + +经常将少量数据从 MxNet 的范围复制到 NumPy 然后会破坏本来有效的代码的性能,因为每个此类操作都需要计算图来评估获得相关术语所需的所有中间结果 * 之前 * 可以做的其他任何事情。 +:end_tab: + +```{.python .input} +with d2l.Benchmark('numpy conversion'): + b = np.dot(a, a) + b.asnumpy() + +with d2l.Benchmark('scalar conversion'): + b = np.dot(a, a) + b.sum().item() +``` + +:begin_tab:`mxnet` +## 改进计算在一个严重的多线程系统上(即使是普通笔记本电脑有 4 个或更多线程,在多插槽服务器上,这个数字可能超过 256 个),调度操作的开销可能会变得巨大。这就是为什么非常希望以异步和并行方式进行计算和调度。为了说明这样做的好处,让我们看看如果我们按顺序或异步方式多次增加一个变量,会发生什么情况。我们通过在每次添加之间插入 `wait_to_read` 障碍来模拟同步执行。 +:end_tab: + +```{.python .input} +with d2l.Benchmark('synchronous'): + for _ in range(10000): + y = x + 1 + y.wait_to_read() + +with d2l.Benchmark('asynchronous'): + for _ in range(10000): + y = x + 1 + npx.waitall() +``` + +:begin_tab:`mxnet` +Python 前端线程和 C ++ 后端线程之间稍微简化的交互可以总结如下: +1. 前端命令后端将计算任务 `y = x + 1` 插入队列。 +1. 然后,后端接收队列中的计算任务并执行实际的计算。 +1. 然后,后端将计算结果返回给前端。 +假设这三个阶段的持续时间分别为 $t_1, t_2$ 和 $t_3$。如果我们不使用异步编程,则执行 10000 个计算所需的总时间约为 $10000 (t_1+ t_2 + t_3)$。如果使用异步编程,则执行 10000 个计算所花费的总时间可以减少到 $t_1 + 10000 t_2 + t_3$(假设为 $10000 t_2 > 9999t_1$),因为前端不必等后端返回每个循环的计算结果。 + +## 摘要 + +* 深度学习框架可能会将 Python 前端与执行后端分离。这允许将命令快速异步插入到后端和相关的并行度。 +* 异步导致前端响应相当灵敏。但是,请注意不要溢出任务队列,因为这可能会导致过多的内存消耗。建议对每个微型批次进行同步,以使前端和后端保持大致同步。 +* 芯片供应商提供复杂的性能分析工具,以获得对深度学习效率的更精细的洞察。 +:end_tab: + +:begin_tab:`mxnet` +* 请注意,从 MxNet 的内存管理转换为 Python 将强制后端等到特定变量准备就绪。`print`、`asnumpy` 和 `item` 等函数都具有这样的效果。这可能是可取的,但粗心地使用同步可能会破坏性能。 +:end_tab: + +## 练习 + +:begin_tab:`mxnet` +1. 我们上面提到过,使用异步计算可以将执行 10000 次计算所需的总时间减少到 $t_1 + 10000 t_2 + t_3$。为什么我们必须在这里假设 $10000 t_2 > 9999 t_1$? +1. 衡量 `waitall` 和 `wait_to_read` 之间的差异。提示:执行许多指令并同步以获得中间结果。 +:end_tab: + +:begin_tab:`pytorch` +1. 在 CPU 上,在本节中对相同的矩阵乘法运算进行基准测试。你还能通过后端观察异步吗? +:end_tab: + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/361) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/2564) +:end_tab: diff --git a/chapter_computational-performance/async-computation_origin.md b/chapter_computational-performance/async-computation_origin.md new file mode 100644 index 000000000..4952b91b1 --- /dev/null +++ b/chapter_computational-performance/async-computation_origin.md @@ -0,0 +1,235 @@ +# Asynchronous Computation +:label:`sec_async` + +Today's computers are highly parallel systems, consisting of multiple CPU cores (often multiple threads per core), multiple processing elements per GPU, and often multiple GPUs per device. In short, we can process many different things at the same time, often on different devices. Unfortunately Python is not a great way of writing parallel and asynchronous code, at least not without some extra help. After all, Python is single-threaded and this is unlikely to change in the future. Deep learning frameworks such as MXNet and TensorFlow adopt an *asynchronous programming* model to improve performance, +while PyTorch uses Python's own scheduler leading to a different performance trade-off. +For PyTorch, by default, GPU operations are asynchronous. When you call a function that uses the GPU, the operations are enqueued to the particular device, but not necessarily executed until later. This allows us to execute more computations in parallel, including operations on the CPU or other GPUs. + +Hence, understanding how asynchronous programming works helps us to develop more efficient programs, by proactively reducing computational requirements and mutual dependencies. This allows us to reduce memory overhead and increase processor utilization. + +```{.python .input} +from d2l import mxnet as d2l +import numpy, os, subprocess +from mxnet import autograd, gluon, np, npx +from mxnet.gluon import nn +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import numpy, os, subprocess +import torch +from torch import nn +``` + +## Asynchrony via Backend + +:begin_tab:`mxnet` +For a warmup consider the following toy problem: we want to generate a random matrix and multiply it. Let us do that both in NumPy and in `mxnet.np` to see the difference. +:end_tab: + +:begin_tab:`pytorch` +For a warmup consider the following toy problem: we want to generate a random matrix and multiply it. Let us do that both in NumPy and in PyTorch tensor to see the difference. +Note that PyTorch `tensor` is defined on a GPU. +:end_tab: + +```{.python .input} +with d2l.Benchmark('numpy'): + for _ in range(10): + a = numpy.random.normal(size=(1000, 1000)) + b = numpy.dot(a, a) + +with d2l.Benchmark('mxnet.np'): + for _ in range(10): + a = np.random.normal(size=(1000, 1000)) + b = np.dot(a, a) +``` + +```{.python .input} +#@tab pytorch +# Warmup for GPU computation +device = d2l.try_gpu() +a = torch.randn(size=(1000, 1000), device=device) +b = torch.mm(a, a) + +with d2l.Benchmark('numpy'): + for _ in range(10): + a = numpy.random.normal(size=(1000, 1000)) + b = numpy.dot(a, a) + +with d2l.Benchmark('torch'): + for _ in range(10): + a = torch.randn(size=(1000, 1000), device=device) + b = torch.mm(a, a) +``` + +:begin_tab:`mxnet` +The benchmark output via MXNet is orders of magnitude faster. Since both are executed on the same processor something else must be going on. +Forcing MXNet to finish all the backend computation prior to returning shows what happened previously: computation is executed by the backend while the frontend returns control to Python. +:end_tab: + +:begin_tab:`pytorch` +The benchmark output via PyTorch is orders of magnitude faster. +NumPy dot product is executed on the CPU processor while +PyTorch matrix multiplication is executed on GPU and hence the latter +is expected to be much faster. But the huge time difference suggests something +else must be going on. +By default, GPU operations are asynchronous in PyTorch. +Forcing PyTorch to finish all computation prior to returning shows +what happened previously: computation is being executed by the backend +while the frontend returns control to Python. +:end_tab: + +```{.python .input} +with d2l.Benchmark(): + for _ in range(10): + a = np.random.normal(size=(1000, 1000)) + b = np.dot(a, a) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +with d2l.Benchmark(): + for _ in range(10): + a = torch.randn(size=(1000, 1000), device=device) + b = torch.mm(a, a) + torch.cuda.synchronize(device) +``` + +:begin_tab:`mxnet` +Broadly speaking, MXNet has a frontend for direct interactions with users, e.g., via Python, as well as a backend used by the system to perform the computation. +As shown in :numref:`fig_frontends`, users can write MXNet programs in various frontend languages, such as Python, R, Scala, and C++. Regardless of the frontend programming language used, the execution of MXNet programs occurs primarily in the backend of C++ implementations. Operations issued by the frontend language are passed on to the backend for execution. +The backend manages its own threads that continuously collect and execute queued tasks. Note that for this to work the backend must be able to keep track of the dependencies between various steps in the computational graph. Hence, it is not possible to parallelize operations that depend on each other. +:end_tab: + +:begin_tab:`pytorch` +Broadly speaking, PyTorch has a frontend for direct interaction with the users, e.g., via Python, as well as a backend used by the system to perform the computation. +As shown in :numref:`fig_frontends`, users can write PyTorch programs in various frontend languages, such as Python and C++. Regardless of the frontend programming language used, the execution of PyTorch programs occurs primarily in the backend of C++ implementations. Operations issued by the frontend language are passed on to the backend for execution. +The backend manages its own threads that continuously collect and execute queued tasks. +Note that for this to work the backend must be able to keep track of the +dependencies between various steps in the computational graph. +Hence, it is not possible to parallelize operations that depend on each other. +:end_tab: + +![Programming language frontends and deep learning framework backends.](../img/frontends.png) +:width:`300px` +:label:`fig_frontends` + +Let us look at another toy example to understand the dependency graph a bit better. + +```{.python .input} +x = np.ones((1, 2)) +y = np.ones((1, 2)) +z = x * y + 2 +z +``` + +```{.python .input} +#@tab pytorch +x = torch.ones((1, 2), device=device) +y = torch.ones((1, 2), device=device) +z = x * y + 2 +z +``` + +![The backend tracks dependencies between various steps in the computational graph.](../img/asyncgraph.svg) +:label:`fig_asyncgraph` + +The code snippet above is also illustrated in :numref:`fig_asyncgraph`. +Whenever the Python frontend thread executes one of the first three statements, it simply returns the task to the backend queue. When the last statement's results need to be *printed*, the Python frontend thread will wait for the C++ backend thread to finish computing the result of the variable `z`. One benefit of this design is that the Python frontend thread does not need to perform actual computations. Thus, there is little impact on the program's overall performance, regardless of Python's performance. :numref:`fig_threading` illustrates how frontend and backend interact. + +![Interactions of the frontend and backend.](../img/threading.svg) +:label:`fig_threading` + +:begin_tab:`mxnet` +## Barriers and Blockers + +There are a number of operations that will force Python to wait for completion: +* Most obviously `npx.waitall()` waits until all computation has completed, regardless of when the compute instructions were issued. In practice it is a bad idea to use this operator unless absolutely necessary since it can lead to poor performance. +* If we just want to wait until a specific variable is available we can call `z.wait_to_read()`. In this case MXNet blocks return to Python until the variable `z` has been computed. Other computation may well continue afterwards. + +Let us see how this works in practice. +:end_tab: + +```{.python .input} +with d2l.Benchmark('waitall'): + b = np.dot(a, a) + npx.waitall() + +with d2l.Benchmark('wait_to_read'): + b = np.dot(a, a) + b.wait_to_read() +``` + +:begin_tab:`mxnet` +Both operations take approximately the same time to complete. Besides the obvious blocking operations we recommend that you are aware of *implicit* blockers. Printing a variable clearly requires the variable to be available and is thus a blocker. Last, conversions to NumPy via `z.asnumpy()` and conversions to scalars via `z.item()` are blocking, since NumPy has no notion of asynchrony. It needs access to the values just like the `print` function. + +Copying small amounts of data frequently from MXNet's scope to NumPy and back can destroy performance of an otherwise efficient code, since each such operation requires the computational graph to evaluate all intermediate results needed to get the relevant term *before* anything else can be done. +:end_tab: + +```{.python .input} +with d2l.Benchmark('numpy conversion'): + b = np.dot(a, a) + b.asnumpy() + +with d2l.Benchmark('scalar conversion'): + b = np.dot(a, a) + b.sum().item() +``` + +:begin_tab:`mxnet` +## Improving Computation +On a heavily multithreaded system (even regular laptops have 4 threads or more and on multi-socket servers this number can exceed 256) the overhead of scheduling operations can become significant. This is why it is highly desirable to have computation and scheduling occur asynchronously and in parallel. To illustrate the benefit of doing so let us see what happens if we increment a variable by 1 multiple times, both in sequence or asynchronously. We simulate synchronous execution by inserting a `wait_to_read` barrier in between each addition. +:end_tab: + +```{.python .input} +with d2l.Benchmark('synchronous'): + for _ in range(10000): + y = x + 1 + y.wait_to_read() + +with d2l.Benchmark('asynchronous'): + for _ in range(10000): + y = x + 1 + npx.waitall() +``` + +:begin_tab:`mxnet` +A slightly simplified interaction between the Python frontend thread and the C++ backend thread can be summarized as follows: +1. The frontend orders the backend to insert the computation task `y = x + 1` into the queue. +1. The backend then receives the computation tasks from the queue and performs the actual computations. +1. The backend then returns the computation results to the frontend. +Assume that the durations of these three stages are $t_1, t_2$ and $t_3$, respectively. If we do not use asynchronous programming, the total time taken to perform 10000 computations is approximately $10000 (t_1+ t_2 + t_3)$. If asynchronous programming is used, the total time taken to perform 10000 computations can be reduced to $t_1 + 10000 t_2 + t_3$ (assuming $10000 t_2 > 9999t_1$), since the frontend does not have to wait for the backend to return computation results for each loop. + + +## Summary + +* Deep learning frameworks may decouple the Python frontend from an execution backend. This allows for fast asynchronous insertion of commands into the backend and associated parallelism. +* Asynchrony leads to a rather responsive frontend. However, use caution not to overfill the task queue since it may lead to excessive memory consumption. It is recommended to synchronize for each minibatch to keep frontend and backend approximately synchronized. +* Chip vendors offer sophisticated performance analysis tools to obtain a much more fine-grained insight into the efficiency of deep learning. +:end_tab: + +:begin_tab:`mxnet` +* Be aware of the fact that conversions from MXNet's memory management to Python will force the backend to wait until the specific variable is ready. Functions such as `print`, `asnumpy` and `item` all have this effect. This can be desirable but a careless use of synchronization can ruin performance. +:end_tab: + +## Exercises + +:begin_tab:`mxnet` +1. We mentioned above that using asynchronous computation can reduce the total amount of time needed to perform 10000 computations to $t_1 + 10000 t_2 + t_3$. Why do we have to assume $10000 t_2 > 9999 t_1$ here? +1. Measure the difference between `waitall` and `wait_to_read`. Hint: perform a number of instructions and synchronize for an intermediate result. +:end_tab: + +:begin_tab:`pytorch` +1. On the CPU, benchmark the same matrix multiplication operations in this section. Can you still observe asynchrony via the backend? +:end_tab: + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/361) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/2564) +:end_tab: diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md new file mode 100644 index 000000000..331b1e1b3 --- /dev/null +++ b/chapter_computational-performance/auto-parallelism.md @@ -0,0 +1,189 @@ +# 自动并行 +:label:`sec_auto_para` + +深度学习框架(例如,MxNet 和 PyTorch)会在后端自动构建计算图。使用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如,:numref:`sec_async` 中的 :numref:`fig_asyncgraph` 独立初始化两个变量。因此,系统可以选择并行执行它们。 + +通常,单个操作员将使用所有 CPU 或单个 GPU 上的所有计算资源。例如,`dot` 操作员将使用所有 CPU 上的所有内核(和线程),即使一台计算机上有多个 CPU 处理器也是如此。同样适用于单个 GPU。因此,并行化对于单设备计算机来说并不是那么有用。对于多台设备,事情更重要。虽然并行化通常在多个 GPU 之间最相关,但添加本地 CPU 将略微提高性能。例如,请参阅 :cite:`Hadjis.Zhang.Mitliagkas.ea.2016`,其中重点介绍了结合 GPU 和 CPU 的计算机视觉模型的训练。借助自动并行化框架的便利性,我们可以通过几行 Python 代码实现相同的目标。更广泛地说,我们对自动并行计算的讨论侧重于使用 CPU 和 GPU 的并行计算,以及计算和通信的并行化。 + +请注意,我们至少需要两个 GPU 来运行本节中的实验。 + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import np, npx +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +``` + +## GPU 上的并行计算 + +让我们首先定义要测试的参考工作负载:下面的 `run` 函数使用分配到两个变量的数据在我们选择的设备上执行 10 个矩阵-矩阵乘法:`x_gpu1` 和 `x_gpu2`。 + +```{.python .input} +devices = d2l.try_all_gpus() +def run(x): + return [x.dot(x) for _ in range(50)] + +x_gpu1 = np.random.uniform(size=(4000, 4000), ctx=devices[0]) +x_gpu2 = np.random.uniform(size=(4000, 4000), ctx=devices[1]) +``` + +```{.python .input} +#@tab pytorch +devices = d2l.try_all_gpus() +def run(x): + return [x.mm(x) for _ in range(50)] + +x_gpu1 = torch.rand(size=(4000, 4000), device=devices[0]) +x_gpu2 = torch.rand(size=(4000, 4000), device=devices[1]) +``` + +:begin_tab:`mxnet` +现在我们将函数应用于数据。为了确保缓存不会在结果中发挥作用,我们在测量之前通过对其中任何一个设备执行一次传递来预热设备。 +:end_tab: + +:begin_tab:`pytorch` +现在我们将函数应用于数据。为了确保缓存不会在结果中发挥作用,我们在测量之前通过对其中任何一个设备执行一次传递来预热设备。`torch.cuda.synchronize()` 等待 CUDA 设备上所有流中的所有内核完成。它采用 `device` 参数,我们需要同步的设备。如果设备参数为 `None`(默认值),则它使用 `current_device()` 给出的当前设备。 +:end_tab: + +```{.python .input} +run(x_gpu1) # Warm-up both devices +run(x_gpu2) +npx.waitall() + +with d2l.Benchmark('GPU1 time'): + run(x_gpu1) + npx.waitall() + +with d2l.Benchmark('GPU2 time'): + run(x_gpu2) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +run(x_gpu1) +run(x_gpu2) # Warm-up all devices +torch.cuda.synchronize(devices[0]) +torch.cuda.synchronize(devices[1]) + +with d2l.Benchmark('GPU1 time'): + run(x_gpu1) + torch.cuda.synchronize(devices[0]) + +with d2l.Benchmark('GPU2 time'): + run(x_gpu2) + torch.cuda.synchronize(devices[1]) +``` + +:begin_tab:`mxnet` +如果我们删除两个任务之间的 `waitall` 语句,系统就可以自由地在两个设备上自动并行计算。 +:end_tab: + +:begin_tab:`pytorch` +如果我们删除两个任务之间的 `synchronize` 语句,系统就可以自由地在两个设备上自动并行计算。 +:end_tab: + +```{.python .input} +with d2l.Benchmark('GPU1 & GPU2'): + run(x_gpu1) + run(x_gpu2) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +with d2l.Benchmark('GPU1 & GPU2'): + run(x_gpu1) + run(x_gpu2) + torch.cuda.synchronize() +``` + +在上述情况下,总执行时间少于各部分的总和,因为深度学习框架会自动安排两台 GPU 设备上的计算,而无需代表用户编写复杂的代码。 + +## 并行计算和通信 + +在许多情况下,我们需要在不同设备之间移动数据,比如在 CPU 和 GPU 之间,或在不同的 GPU 之间移动数据。例如,当我们想要执行分布式优化时,我们需要在多个加速器卡上聚合渐变时,就会发生这种情况。让我们通过在 GPU 上进行计算,然后将结果复制回 CPU 来模拟此操作。 + +```{.python .input} +def copy_to_cpu(x): + return [y.copyto(npx.cpu()) for y in x] + +with d2l.Benchmark('Run on GPU1'): + y = run(x_gpu1) + npx.waitall() + +with d2l.Benchmark('Copy to CPU'): + y_cpu = copy_to_cpu(y) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +def copy_to_cpu(x, non_blocking=False): + return [y.to('cpu', non_blocking=non_blocking) for y in x] + +with d2l.Benchmark('Run on GPU1'): + y = run(x_gpu1) + torch.cuda.synchronize() + +with d2l.Benchmark('Copy to CPU'): + y_cpu = copy_to_cpu(y) + torch.cuda.synchronize() +``` + +:begin_tab:`mxnet` +这有点效率低下。请注意,我们已经可以开始将 `y` 的部分复制到 CPU,而列表的其余部分仍在计算中。例如,当我们计算微型批次的梯度时,就会发生这种情况。其中一些参数的梯度将比其他参数的梯度更早提供。因此,在 GPU 仍在运行的同时开始使用 PCI-Express 总线带宽对我们有利。在两个部分之间删除 `waitall` 使我们能够模拟这种情况。 +:end_tab: + +:begin_tab:`pytorch` +这有点效率低下。请注意,我们已经可以开始将 `y` 的部分复制到 CPU,而列表的其余部分仍在计算中。例如,当我们计算微型批次上的(backprop)渐变时,就会发生这种情况。其中一些参数的梯度将比其他参数的梯度更早提供。因此,在 GPU 仍在运行的同时开始使用 PCI-Express 总线带宽对我们有利。在 PyTorch 中,`to()` 和 `copy_()` 等多个函数都承认了一个明确的 `non_blocking` 参数,在不必要的情况下,调用者可以绕过同步。设置 `non_blocking=True` 允许我们模拟此场景。 +:end_tab: + +```{.python .input} +with d2l.Benchmark('Run on GPU1 and copy to CPU'): + y = run(x_gpu1) + y_cpu = copy_to_cpu(y) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +with d2l.Benchmark('Run on GPU1 and copy to CPU'): + y = run(x_gpu1) + y_cpu = copy_to_cpu(y, True) + torch.cuda.synchronize() +``` + +两项操作所需的总时间(如预期的那样)都少于其各部分的总和。请注意,此任务与并行计算不同,因为它使用不同的资源:CPU 和 GPU 之间的总线。事实上,我们可以同时在两台设备上进行计算并进行通信。如上所述,计算和通信之间存在依赖关系:必须先计算 `y[i]`,然后才能将其复制到 CPU。幸运的是,该系统可以在计算 `y[i]` 的同时拷贝 `y[i-1]` 以减少总运行时间。 + +如 :numref:`fig_twogpu` 中所述,我们最后说明了在 CPU 和两个 GPU 上训练时,计算图及其对简单的双层 MLP 的依赖关系。手动安排由此产生的并行程序将非常痛苦。在这里,拥有基于图形的计算后端进行优化是有利的。 + +![The computational graph and its dependencies of a two-layer MLP on a CPU and two GPUs.](../img/twogpu.svg) +:label:`fig_twogpu` + +## 摘要 + +* 现代系统具有各种设备,例如多个 GPU 和 CPU。它们可以并行、异步使用。 +* 现代系统还有各种通信资源,例如 PCI Express、存储(通常是固态硬盘或通过网络)和网络带宽。它们可以并行使用以实现峰值效率。 +* 后端可以通过自动并行计算和通信来提高性能。 + +## 练习 + +1. 在本节定义的 `run` 函数中执行了八个操作。它们之间没有依赖关系。设计一个实验,看看深度学习框架是否会自动并行执行它们。 +1. 当单个操作员的工作负载足够小时,即使在单个 CPU 或 GPU 上,并行化也可以提供帮助。设计一个实验来验证这一点。 +1. 设计一个在 CPU、GPU 上使用并行计算以及两个设备之间的通信的实验。 +1. 使用 NVIDIA [Nsight](https://developer.nvidia.com/nsight-compute-2019_5) 之类的调试器来验证您的代码是否有效。 +1. 设计包含更复杂的数据依赖关系的计算任务,并运行实验以查看是否可以在提高性能的同时获得正确的结果。 + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/362) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1681) +:end_tab: diff --git a/chapter_computational-performance/auto-parallelism_origin.md b/chapter_computational-performance/auto-parallelism_origin.md new file mode 100644 index 000000000..3a87ca20b --- /dev/null +++ b/chapter_computational-performance/auto-parallelism_origin.md @@ -0,0 +1,200 @@ +# Automatic Parallelism +:label:`sec_auto_para` + + +Deep learning frameworks (e.g., MXNet and PyTorch) automatically construct computational graphs at the backend. Using a +computational graph, the system is aware of all the dependencies, +and can selectively execute multiple non-interdependent tasks in parallel to +improve speed. For instance, :numref:`fig_asyncgraph` in :numref:`sec_async` initializes two variables independently. Consequently the system can choose to execute them in parallel. + + +Typically, a single operator will use all the computational resources on all CPUs or on a single GPU. For example, the `dot` operator will use all cores (and threads) on all CPUs, even if there are multiple CPU processors on a single machine. The same applies to a single GPU. Hence parallelization is not quite so useful for single-device computers. With multiple devices things matter more. While parallelization is typically most relevant between multiple GPUs, adding the local CPU will increase performance slightly. For example, see :cite:`Hadjis.Zhang.Mitliagkas.ea.2016` that focuses on training computer vision models combining a GPU and a CPU. With the convenience of an automatically parallelizing framework we can accomplish the same goal in a few lines of Python code. More broadly, our discussion of automatic parallel computation focuses on parallel computation using both CPUs and GPUs, as well as the parallelization of computation and communication. + +Note that we need at least two GPUs to run the experiments in this section. + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import np, npx +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +``` + +## Parallel Computation on GPUs + +Let us start by defining a reference workload to test: the `run` function below performs 10 matrix-matrix multiplications on the device of our choice using data allocated into two variables: `x_gpu1` and `x_gpu2`. + +```{.python .input} +devices = d2l.try_all_gpus() +def run(x): + return [x.dot(x) for _ in range(50)] + +x_gpu1 = np.random.uniform(size=(4000, 4000), ctx=devices[0]) +x_gpu2 = np.random.uniform(size=(4000, 4000), ctx=devices[1]) +``` + +```{.python .input} +#@tab pytorch +devices = d2l.try_all_gpus() +def run(x): + return [x.mm(x) for _ in range(50)] + +x_gpu1 = torch.rand(size=(4000, 4000), device=devices[0]) +x_gpu2 = torch.rand(size=(4000, 4000), device=devices[1]) +``` + +:begin_tab:`mxnet` +Now we apply the function to the data. To ensure that caching does not play a role in the results we warm up the devices by performing a single pass on either of them prior to measuring. +:end_tab: + +:begin_tab:`pytorch` +Now we apply the function to the data. To ensure that caching does not play a role in the results we warm up the devices by performing a single pass on either of them prior to measuring. `torch.cuda.synchronize()` waits for all kernels in all streams on a CUDA device to complete. It takes in a `device` argument, the device for which we need to synchronize. It uses the current device, given by `current_device()`, if the device argument is `None` (default). +:end_tab: + +```{.python .input} +run(x_gpu1) # Warm-up both devices +run(x_gpu2) +npx.waitall() + +with d2l.Benchmark('GPU1 time'): + run(x_gpu1) + npx.waitall() + +with d2l.Benchmark('GPU2 time'): + run(x_gpu2) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +run(x_gpu1) +run(x_gpu2) # Warm-up all devices +torch.cuda.synchronize(devices[0]) +torch.cuda.synchronize(devices[1]) + +with d2l.Benchmark('GPU1 time'): + run(x_gpu1) + torch.cuda.synchronize(devices[0]) + +with d2l.Benchmark('GPU2 time'): + run(x_gpu2) + torch.cuda.synchronize(devices[1]) +``` + +:begin_tab:`mxnet` +If we remove the `waitall` statement between both tasks the system is free to parallelize computation on both devices automatically. +:end_tab: + +:begin_tab:`pytorch` +If we remove the `synchronize` statement between both tasks the system is free to parallelize computation on both devices automatically. +:end_tab: + +```{.python .input} +with d2l.Benchmark('GPU1 & GPU2'): + run(x_gpu1) + run(x_gpu2) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +with d2l.Benchmark('GPU1 & GPU2'): + run(x_gpu1) + run(x_gpu2) + torch.cuda.synchronize() +``` + +In the above case the total execution time is less than the sum of its parts, since the deep learning framework automatically schedules computation on both GPU devices without the need for sophisticated code on behalf of the user. + + + +## Parallel Computation and Communication + +In many cases we need to move data between different devices, say between the CPU and GPU, or between different GPUs. +For instance, +this occurs when we want to perform distributed optimization where we need to aggregate the gradients over multiple accelerator cards. Let us simulate this by computing on the GPU and then copying the results back to the CPU. + +```{.python .input} +def copy_to_cpu(x): + return [y.copyto(npx.cpu()) for y in x] + +with d2l.Benchmark('Run on GPU1'): + y = run(x_gpu1) + npx.waitall() + +with d2l.Benchmark('Copy to CPU'): + y_cpu = copy_to_cpu(y) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +def copy_to_cpu(x, non_blocking=False): + return [y.to('cpu', non_blocking=non_blocking) for y in x] + +with d2l.Benchmark('Run on GPU1'): + y = run(x_gpu1) + torch.cuda.synchronize() + +with d2l.Benchmark('Copy to CPU'): + y_cpu = copy_to_cpu(y) + torch.cuda.synchronize() +``` + +:begin_tab:`mxnet` +This is somewhat inefficient. Note that we could already start copying parts of `y` to the CPU while the remainder of the list is still being computed. This situation occurs, e.g., when we compute the gradient on a minibatch. The gradients of some of the parameters will be available earlier than that of others. Hence it works to our advantage to start using PCI-Express bus bandwidth while the GPU is still running. Removing `waitall` between both parts allows us to simulate this scenario. +:end_tab: + +:begin_tab:`pytorch` +This is somewhat inefficient. Note that we could already start copying parts of `y` to the CPU while the remainder of the list is still being computed. This situation occurs, e.g., when we compute the (backprop) gradient on a minibatch. The gradients of some of the parameters will be available earlier than that of others. Hence it works to our advantage to start using PCI-Express bus bandwidth while the GPU is still running. In PyTorch, several functions such as `to()` and `copy_()` admit an explicit `non_blocking` argument, which lets the caller bypass synchronization when it is unnecessary. Setting `non_blocking=True` allows us to simulate this scenario. +:end_tab: + +```{.python .input} +with d2l.Benchmark('Run on GPU1 and copy to CPU'): + y = run(x_gpu1) + y_cpu = copy_to_cpu(y) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +with d2l.Benchmark('Run on GPU1 and copy to CPU'): + y = run(x_gpu1) + y_cpu = copy_to_cpu(y, True) + torch.cuda.synchronize() +``` + +The total time required for both operations is (as expected) less than the sum of their parts. +Note that this task is different from parallel computation as it uses a different resource: the bus between the CPU and GPUs. In fact, we could compute on both devices and communicate, all at the same time. As noted above, there is a dependency between computation and communication: `y[i]` must be computed before it can be copied to the CPU. Fortunately, the system can copy `y[i-1]` while computing `y[i]` to reduce the total running time. + +We conclude with an illustration of the computational graph and its dependencies for a simple two-layer MLP when training on a CPU and two GPUs, as depicted in :numref:`fig_twogpu`. It would be quite painful to schedule the parallel program resulting from this manually. This is where it is advantageous to have a graph-based computing backend for optimization. + +![The computational graph and its dependencies of a two-layer MLP on a CPU and two GPUs.](../img/twogpu.svg) +:label:`fig_twogpu` + + +## Summary + +* Modern systems have a variety of devices, such as multiple GPUs and CPUs. They can be used in parallel, asynchronously. +* Modern systems also have a variety of resources for communication, such as PCI Express, storage (typically solid-state drives or via networks), and network bandwidth. They can be used in parallel for peak efficiency. +* The backend can improve performance through automatic parallel computation and communication. + +## Exercises + +1. Eight operations were performed in the `run` function defined in this section. There are no dependencies between them. Design an experiment to see if the deep learning framework will automatically execute them in parallel. +1. When the workload of an individual operator is sufficiently small, parallelization can help even on a single CPU or GPU. Design an experiment to verify this. +1. Design an experiment that uses parallel computation on CPUs, GPUs, and communication between both devices. +1. Use a debugger such as NVIDIA's [Nsight](https://developer.nvidia.com/nsight-compute-2019_5) to verify that your code is efficient. +1. Designing computation tasks that include more complex data dependencies, and run experiments to see if you can obtain the correct results while improving performance. + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/362) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1681) +:end_tab: diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md new file mode 100644 index 000000000..d2ad75f0e --- /dev/null +++ b/chapter_computational-performance/hybridize.md @@ -0,0 +1,392 @@ +# 编译器和口译员 +:label:`sec_hybridize` + +到目前为止,这本书专注于命令式编程,它利用 `print`、`+` 和 `if` 等陈述来改变计划的状态。考虑以下简单的命令性程序的例子。 + +```{.python .input} +#@tab all +def add(a, b): + return a + b + +def fancy_func(a, b, c, d): + e = add(a, b) + f = add(c, d) + g = add(e, f) + return g + +print(fancy_func(1, 2, 3, 4)) +``` + +Python 是一种 * 解释性语言 *。当评估上述 `fancy_func` 函数时,它会按顺序执行组成函数主体的操作 *。也就是说,它将评估 `e = add(a, b)` 并将结果存储为变量 `e`,从而改变计划的状态。接下来的两个语句 `f = add(c, d)` 和 `g = add(e, f)` 将以类似的方式执行,执行添加操作并将结果存储为变量。:numref:`fig_compute_graph` 说明了数据流。 + +![Data flow in an imperative program.](../img/computegraph.svg) +:label:`fig_compute_graph` + +尽管命令式编程很方便,但效率可能低下。一方面,即使在 `fancy_func` 中重复调用 `add` 函数,Python 也会分别执行三个函数调用。比如说,如果在 GPU 上(甚至在多个 GPU 上)执行这些操作,则 Python 解释器产生的开销可能会变得压倒性。此外,在执行 `fancy_func` 中的所有语句之前,它需要保存 `e` 和 `f` 的变量值。这是因为我们不知道在语句 `e = add(a, b)` 和 `f = add(c, d)` 执行之后,程序的其他部分是否会使用变量 `e` 和 `f`。 + +## 符号编程 + +考虑另一种选择,* 符号编程 *,其中通常只有在完全定义过程后才执行计算。该策略被多个深度学习框架使用,包括 Theano 和 TensorFlow(后者获得了必要的扩展)。它通常涉及以下步骤: + +1. 定义要执行的操作。 +1. 将操作编译成可执行程序。 +1. 提供所需的输入并调用编译后的程序进行执行。 + +这允许进行大量的优化。首先,在许多情况下,我们可以跳过 Python 解释器,从而消除性能瓶颈,该瓶颈可能会在 CPU 上与单个 Python 线程配对的多个快速 GPU 上变得显著。其次,编译器可能会优化上述代码并将其重写为 `print((1 + 2) + (3 + 4))` 甚至 `print(10)`。这是可能的,因为编译器在将其转换为机器指令之前需要查看完整的代码。例如,只要不再需要变量,它就可以释放内存(或永远不分配)。或者它可以将代码完全转换为等效的片段。为了获得更好的想法,请考虑下面的命令式编程模拟(毕竟是 Python)。 + +```{.python .input} +#@tab all +def add_(): + return ''' +def add(a, b): + return a + b +''' + +def fancy_func_(): + return ''' +def fancy_func(a, b, c, d): + e = add(a, b) + f = add(c, d) + g = add(e, f) + return g +''' + +def evoke_(): + return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))' + +prog = evoke_() +print(prog) +y = compile(prog, '', 'exec') +exec(y) +``` + +命令式(解释式)编程和符号编程之间的区别如下: + +* 命令式编程更容易。当 Python 中使用命令式编程时,大多数代码都很简单且易于编写。调试命令式编程代码也更容易。这是因为获取和打印所有相关的中间变量值或使用 Python 的内置调试工具更容易。 +* 符号编程效率更高,更容易移植。符号编程使得在编译过程中优化代码变得更加容易,同时还能够将程序移植到独立于 Python 的格式。这允许程序在非 Python 环境中运行,从而避免任何与 Python 解释器相关的潜在性能问题。 + +## 混合编程 + +历史上,大多数深度学习框架都可以选择必要的方法或象征性方法。例如,Theano、TensorFlow(受前者的启发)、Keras 和 CNTK 象征性地制定模型。相反,Chainer 和 PyTorch 采取必要的方法。在后续的修订版中,TensorFlow 2.0 和 Keras 中添加了命令模式。 + +:begin_tab:`mxnet` +在设计 Gluon 时,开发人员考虑了是否有可能将两种编程模式的好处结合起来。这导致了混合模型,允许用户使用纯粹的命令式编程进行开发和调试,同时能够将大多数程序转换为符号程序,以便在需要产品级计算性能和部署时运行。 + +实际上,这意味着我们使用 `HybridBlock` 或 `HybridSequential` 类来构建模型。默认情况下,其中任何一个都以同样的方式执行 `Block` 或 `Sequential` 类在命令式编程中执行。`HybridSequential` 类是 `HybridBlock` 的子类(就像 `Sequential` 子类 `Block`)。当调用 `hybridize` 函数时,Gluon 会将模型编译成符号编程中使用的形式。这允许人们在不牺牲模型实施方式的情况下优化计算密集型组件。我们将在下面介绍优势,重点介绍顺序模型和模块。 +:end_tab: + +:begin_tab:`pytorch` +如上所述,PyTorch 基于命令式编程并使用动态计算图。为了利用符号编程的可移植性和效率,开发人员考虑了是否有可能结合两种编程模型的优势。这导致了一个 torchscript,允许用户使用纯粹的命令式编程进行开发和调试,同时能够将大多数程序转换为符号程序,以便在需要产品级计算性能和部署时运行。 +:end_tab: + +:begin_tab:`tensorflow` +命令式编程模式现在是 Tensorflow 2 中的默认设置,对于新使用该语言的人来说,这是一个值得欢迎的变化。但是,TensorFlow 中仍然存在相同的符号编程技术和后续的计算图形,易于使用的 `tf.function` 装饰器可以访问。这为 TensorFlow 带来了命令式的编程模式,允许用户定义更直观的函数,然后使用 TensorFlow 团队称为 [autograph](https://www.tensorflow.org/api_docs/python/tf/autograph) 的功能将它们包装起来并自动将它们编译成计算图形。 +:end_tab: + +## 混合 `Sequential` 课程 + +了解混合运行方式的最简单方法是考虑具有多层的深度网络。通常情况下,Python 解释器需要为所有层执行代码才能生成指令,然后将其转发到 CPU 或 GPU。对于单个(快速)计算设备,这不会导致任何重大问题。另一方面,如果我们使用高级 8-GPU 服务器,如 AWS P3dn.24xLart 实例,Python 将难以让所有 GPU 保持繁忙。单线程 Python 解释器成为这里的瓶颈。让我们看看如何通过将 `Sequential` 替换为 `HybridSequential` 来解决代码的重要部分。我们首先定义一个简单的 MLP。 + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import np, npx +from mxnet.gluon import nn +npx.set_np() + +# Factory for networks +def get_net(): + net = nn.HybridSequential() + net.add(nn.Dense(256, activation='relu'), + nn.Dense(128, activation='relu'), + nn.Dense(2)) + net.initialize() + return net + +x = np.random.normal(size=(1, 512)) +net = get_net() +net(x) +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +from torch import nn + +# Factory for networks +def get_net(): + net = nn.Sequential(nn.Linear(512, 256), + nn.ReLU(), + nn.Linear(256, 128), + nn.ReLU(), + nn.Linear(128, 2)) + return net + +x = torch.randn(size=(1, 512)) +net = get_net() +net(x) +``` + +```{.python .input} +#@tab tensorflow +from d2l import tensorflow as d2l +import tensorflow as tf +from tensorflow.keras.layers import Dense + +# Factory for networks +def get_net(): + net = tf.keras.Sequential() + net.add(Dense(256, input_shape = (512,), activation = "relu")) + net.add(Dense(128, activation = "relu")) + net.add(Dense(2, activation = "linear")) + return net + +x = tf.random.normal([1,512]) +net = get_net() +net(x) +``` + +:begin_tab:`mxnet` +通过调用 `hybridize` 函数,我们能够在 MLP 中编译和优化计算。模型的计算结果保持不变。 +:end_tab: + +:begin_tab:`pytorch` +通过使用 `torch.jit.script` 函数转换模型,我们能够在 MLP 中编译和优化计算。模型的计算结果保持不变。 +:end_tab: + +:begin_tab:`tensorflow` +以前,tensorflow 中内置的所有函数都是作为计算图构建的,因此在默认情况下编译 JIT。但是,随着 tensorflow 2.X 的发布和急切的张量,这不再是默认行为。我们可以使用 tf.function 重新启用此功能。tf.function 更常用作函数装饰器,但是可以将其作为普通 python 函数直接调用,如下所示。模型的计算结果保持不变。 +:end_tab: + +```{.python .input} +net.hybridize() +net(x) +``` + +```{.python .input} +#@tab pytorch +net = torch.jit.script(net) +net(x) +``` + +```{.python .input} +#@tab tensorflow +net = tf.function(net) +net(x) +``` + +:begin_tab:`mxnet` +这似乎几乎太好了,无法实现:只需将一个块指定为 `HybridSequential`,编写与之前相同的代码,然后调用 `hybridize`。一旦发生这种情况,网络将被优化(我们将在下面对性能进行基准测试不幸的是,这并不适用于每个层都神奇。也就是说,如果图层继承自 `Block` 类而不是 `HybridBlock` 类,则不会对其进行优化。 +:end_tab: + +:begin_tab:`pytorch` +通过使用 `torch.jit.script` 转换模型,这似乎几乎太好了,无法实现:编写与之前相同的代码,然后简单地使用 `torch.jit.script` 转换模型。一旦发生这种情况,网络将被优化(我们将在下面对性能进行基准测试 +:end_tab: + +:begin_tab:`tensorflow` +使用 `tf.function` 转换模型为 TensorFlow 提供了令人难以置信的力量:编写与之前相同的代码,然后使用 `tf.function` 简单地转换模型。一旦发生这种情况,网络就会在 TensorFlow 的 MLIR 中间表示中构建为计算图,并在编译器级别进行了大量优化,以实现快速执行(我们将在下面对性能进行基准测试)。将 `jit_compile = True` 标志明确添加到 `tf.function()` 调用中可以启用 TensorFlow 中的 XLA(加速线性代数)功能。XLA 可以在某些情况下进一步优化 JIT 编译的代码。在没有这个明确定义的情况下,可以启用图形模式执行,但是 XLA 可以使某些大型线性代数操作(在深度学习应用程序中看到的那些操作)更快,尤其是在 GPU 环境中。 +:end_tab: + +### 混合加速 + +为了展示通过编译获得的性能提高,我们比较了在混合运动之前和之后评估 `net(x)` 所需的时间。让我们首先定义一个函数来测量这次。当我们着手衡量(和提高)绩效时,它将在整个章节中派上用场。 + +```{.python .input} +#@tab all +#@save +class Benchmark: + def __init__(self, description='Done'): + self.description = description + + def __enter__(self): + self.timer = d2l.Timer() + return self + + def __exit__(self, *args): + print(f'{self.description}: {self.timer.stop():.4f} sec') +``` + +:begin_tab:`mxnet` +现在我们可以调用两次网络,一次不用混合动力。 +:end_tab: + +:begin_tab:`pytorch` +现在我们可以调用两次网络,一次是没有 torchscript。 +:end_tab: + +:begin_tab:`tensorflow` +现在我们可以三次调用网络,一次是急切执行,一次是用图形模式执行,然后再次使用 JIT 编译的 XLA。 +:end_tab: + +```{.python .input} +net = get_net() +with Benchmark('Without hybridization'): + for i in range(1000): net(x) + npx.waitall() + +net.hybridize() +with Benchmark('With hybridization'): + for i in range(1000): net(x) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +net = get_net() +with Benchmark('Without torchscript'): + for i in range(1000): net(x) + +net = torch.jit.script(net) +with Benchmark('With torchscript'): + for i in range(1000): net(x) +``` + +```{.python .input} +#@tab tensorflow +net = get_net() +with Benchmark('Eager Mode'): + for i in range(1000): net(x) + +net = tf.function(net) +with Benchmark('Graph Mode'): + for i in range(1000): net(x) +``` + +:begin_tab:`mxnet` +如上面的结果所观察到的那样,`HybridSequential` 实例调用 `hybridize` 函数后,通过使用符号编程来提高计算性能。 +:end_tab: + +:begin_tab:`pytorch` +如上面的结果所观察到的那样,在使用 `torch.jit.script` 函数编写 `nn.Sequential` 实例脚本后,通过使用符号编程来提高计算性能。 +:end_tab: + +:begin_tab:`tensorflow` +如上面的结果所观察到的那样,在使用 `tf.function` 函数编写 tf.keras 顺序实例脚本之后,通过 tensorflow 中的图形模式执行使用符号编程,计算性能得到提高。 +:end_tab: + +### 序列化 + +:begin_tab:`mxnet` +编译模型的好处之一是我们可以序列化(保存)模型及其参数到磁盘。这使我们能够以独立于所选前端语言的方式存储模型。这使我们能够将训练有素的模型部署到其他设备,并轻松使用其他前端编程语言。同时,代码通常比命令式编程中可以实现的速度快。让我们看看 `export` 函数的实际运行。 +:end_tab: + +:begin_tab:`pytorch` +编译模型的好处之一是我们可以序列化(保存)模型及其参数到磁盘。这使我们能够以独立于所选前端语言的方式存储模型。这使我们能够将训练有素的模型部署到其他设备,并轻松使用其他前端编程语言。同时,代码通常比命令式编程中可以实现的速度快。让我们看看 `save` 函数的实际运行。 +:end_tab: + +:begin_tab:`tensorflow` +编译模型的好处之一是我们可以序列化(保存)模型及其参数到磁盘。这使我们能够以独立于所选前端语言的方式存储模型。这使我们能够将训练有素的模型部署到其他设备,轻松使用其他前端编程语言或在服务器上执行训练有素的模型。同时,代码通常比命令式编程中可以实现的速度快。允许我们在 tensorflow 中保存的低级 API 是 `tf.saved_model`。让我们看看 `saved_model` 实例在运行中。 +:end_tab: + +```{.python .input} +net.export('my_mlp') +!ls -lh my_mlp* +``` + +```{.python .input} +#@tab pytorch +net.save('my_mlp') +!ls -lh my_mlp* +``` + +```{.python .input} +#@tab tensorflow +net = get_net() +tf.saved_model.save(net, 'my_mlp') +!ls -lh my_mlp* +``` + +:begin_tab:`mxnet` +模型被分解为(大型二进制)参数文件和执行模型计算所需程序的 JSON 描述。这些文件可以由 Python 或 MxNet 支持的其他前端语言读取,例如 C++、R、Scala 和 Perl。让我们看看模型描述中的前几行。 +:end_tab: + +```{.python .input} +!head my_mlp-symbol.json +``` + +:begin_tab:`mxnet` +此前,我们证明,在调用 `hybridize` 函数之后,该模型能够实现卓越的计算性能和便携性。请注意,尽管这种混合可能会影响模型的灵活性,特别是在控制流方面。 + +此外,与需要使用 `forward` 函数的 `Block` 实例相反,对于 `HybridBlock` 实例,我们需要使用 `hybrid_forward` 函数。 +:end_tab: + +```{.python .input} +class HybridNet(nn.HybridBlock): + def __init__(self, **kwargs): + super(HybridNet, self).__init__(**kwargs) + self.hidden = nn.Dense(4) + self.output = nn.Dense(2) + + def hybrid_forward(self, F, x): + print('module F: ', F) + print('value x: ', x) + x = F.npx.relu(self.hidden(x)) + print('result : ', x) + return self.output(x) +``` + +:begin_tab:`mxnet` +上面的代码实现了一个带有 4 个隐藏单元和 2 个输出的简单网络。`hybrid_forward` 函数需要一个额外的参数 `F`。这是必要的,因为根据代码是否被混合,它将使用略有不同的库(`ndarray` 或 `symbol`)进行处理。这两个类执行的功能非常相似,MxNet 会自动确定参数。为了理解发生了什么,我们将参数作为函数调用的一部分打印出来。 +:end_tab: + +```{.python .input} +net = HybridNet() +net.initialize() +x = np.random.normal(size=(1, 3)) +net(x) +``` + +:begin_tab:`mxnet` +重复向前计算将导致相同的输出(我们省略了细节)。现在让我们看看如果我们调用 `hybridize` 函数会发生什么。 +:end_tab: + +```{.python .input} +net.hybridize() +net(x) +``` + +:begin_tab:`mxnet` +我们现在不使用 `ndarray`,而不是使用 `symbol` 模块进行 `F`。此外,尽管输入是 `ndarray` 类型,但作为编译过程的一部分,通过网络流动的数据现在已转换为 `symbol` 类型。重复函数调用会导致令人惊讶的结果: +:end_tab: + +```{.python .input} +net(x) +``` + +:begin_tab:`mxnet` +这与我们之前看到的截然不同。省略 `hybrid_forward` 中定义的所有打印语句。事实上,在混合后,`net(x)` 的执行不再涉及 Python 解释器。这意味着,忽略任何虚假的 Python 代码(例如 print 语句),以利于更简化的执行和更好的性能。相反,MxNet 直接调用 C ++ 后端。另请注意,`symbol` 模块(例如 `asnumpy`)中不支持某些功能,而就地操作(如 `a += b` 和 `a[:] = a + b`)必须重写为 `a = a + b`。尽管如此,只要速度重要,汇编模型就值得付出努力。优势可以从小百分点到速度的两倍以上,具体取决于模型的复杂性、CPU 的速度以及 GPU 的速度和数量。 +:end_tab: + +## 摘要 + +* 命令式编程使设计新模型变得容易,因为可以使用控制流编写代码,并且能够使用大量 Python 软件生态系统。 +* 符号编程要求我们先指定程序并在执行之前对其进行编译。好处是提高了性能。 + +:begin_tab:`mxnet` +* MxNet 能够根据需要结合这两种方法的优势。 +* `HybridSequential` 和 `HybridBlock` 类构建的模型可以通过调用 `hybridize` 函数将命令性程序转换为符号程序。 +:end_tab: + +## 练习 + +:begin_tab:`mxnet` +1. 将 `x.asnumpy()` 添加到本节中 `HybridNet` 类的 `hybrid_forward` 函数的第一行。执行代码并观察遇到的错误。他们为什么会发生? +1. 如果我们添加控制流程,即 `hybrid_forward` 函数中的 Python 语句 `if` 和 `for` 会发生什么? +1. 查看前几章中你感兴趣的模型。你能通过重新实现它们来提高他们的计算性能吗? +:end_tab: + +:begin_tab:`pytorch,tensorflow` +1. 查看前几章中你感兴趣的模型。你能通过重新实现它们来提高他们的计算性能吗? +:end_tab: + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/360) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/2490) +:end_tab: + +:begin_tab:`tensorflow` +[Discussions](https://discuss.d2l.ai/t/2492) +:end_tab: diff --git a/chapter_computational-performance/hybridize_origin.md b/chapter_computational-performance/hybridize_origin.md new file mode 100644 index 000000000..c78eb6990 --- /dev/null +++ b/chapter_computational-performance/hybridize_origin.md @@ -0,0 +1,406 @@ +# Compilers and Interpreters +:label:`sec_hybridize` + +So far, this book has focused on imperative programming, which makes use of statements such as `print`, `+`, and `if` to change a program's state. Consider the following example of a simple imperative program. + +```{.python .input} +#@tab all +def add(a, b): + return a + b + +def fancy_func(a, b, c, d): + e = add(a, b) + f = add(c, d) + g = add(e, f) + return g + +print(fancy_func(1, 2, 3, 4)) +``` + +Python is an *interpreted language*. When evaluating the above `fancy_func` function it performs the operations making up the function's body *in sequence*. That is, it will evaluate `e = add(a, b)` and store the results as variable `e`, thereby changing the program's state. The next two statements `f = add(c, d)` and `g = add(e, f)` will be executed similarly, performing additions and storing the results as variables. :numref:`fig_compute_graph` illustrates the flow of data. + +![Data flow in an imperative program.](../img/computegraph.svg) +:label:`fig_compute_graph` + +Although imperative programming is convenient, it may be inefficient. On one hand, even if the `add` function is repeatedly called throughout `fancy_func`, Python will execute the three function calls individually. If these are executed, say, on a GPU (or even on multiple GPUs), the overhead arising from the Python interpreter can become overwhelming. Moreover, it will need to save the variable values of `e` and `f` until all the statements in `fancy_func` have been executed. This is because we do not know whether the variables `e` and `f` will be used by other parts of the program after the statements `e = add(a, b)` and `f = add(c, d)` are executed. + +## Symbolic Programming + +Consider the alternative, *symbolic programming*, where computation is usually performed only once the process has been fully defined. This strategy is used by multiple deep learning frameworks, including Theano and TensorFlow (the latter has acquired imperative extensions). It usually involves the following steps: + +1. Define the operations to be executed. +1. Compile the operations into an executable program. +1. Provide the required inputs and call the compiled program for execution. + +This allows for a significant amount of optimization. First, we can skip the Python interpreter in many cases, thus removing a performance bottleneck that can become significant on multiple fast GPUs paired with a single Python thread on a CPU. +Second, a compiler might optimize and rewrite the above code into `print((1 + 2) + (3 + 4))` or even `print(10)`. This is possible since a compiler gets to see the full code before turning it into machine instructions. For instance, it can release memory (or never allocate it) whenever a variable is no longer needed. Or it can transform the code entirely into an equivalent piece. +To get a better idea, consider the following simulation of imperative programming (it is Python after all) below. + +```{.python .input} +#@tab all +def add_(): + return ''' +def add(a, b): + return a + b +''' + +def fancy_func_(): + return ''' +def fancy_func(a, b, c, d): + e = add(a, b) + f = add(c, d) + g = add(e, f) + return g +''' + +def evoke_(): + return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))' + +prog = evoke_() +print(prog) +y = compile(prog, '', 'exec') +exec(y) +``` + +The differences between imperative (interpreted) programming and symbolic programming are as follows: + +* Imperative programming is easier. When imperative programming is used in Python, the majority of the code is straightforward and easy to write. It is also easier to debug imperative programming code. This is because it is easier to obtain and print all relevant intermediate variable values, or use Python's built-in debugging tools. +* Symbolic programming is more efficient and easier to port. Symbolic programming makes it easier to optimize the code during compilation, while also having the ability to port the program into a format independent of Python. This allows the program to be run in a non-Python environment, thus avoiding any potential performance issues related to the Python interpreter. + + +## Hybrid Programming + +Historically most deep learning frameworks choose between an imperative or a symbolic approach. For example, Theano, TensorFlow (inspired by the former), Keras, and CNTK formulate models symbolically. Conversely, Chainer and PyTorch take an imperative approach. An imperative mode was added to TensorFlow 2.0 and Keras in later revisions. + +:begin_tab:`mxnet` +When designing Gluon, developers considered whether it would be possible to combine the benefits of both programming paradigms. This led to a hybrid model that lets users develop and debug with pure imperative programming, while having the ability to convert most programs into symbolic programs to be run when product-level computing performance and deployment are required. + +In practice this means that we build models using the `HybridBlock` or `HybridSequential` class. By default, either of them is executed in the same way the `Block` or `Sequential` class is executed in imperative programming. +The `HybridSequential` class is a subclass of `HybridBlock` (just like `Sequential` subclasses `Block`). When the `hybridize` function is called, Gluon compiles the model into the form used in symbolic programming. This allows one to optimize the computation-intensive components without sacrifices in the way a model is implemented. We will illustrate the benefits below, focusing on sequential models and blocks. +:end_tab: + +:begin_tab:`pytorch` +As mentioned above, PyTorch is based on imperative programming and uses dynamic computation graphs. In an effort to leverage the portability and efficiency of symbolic programming, developers considered whether it would be possible to combine the benefits of both programming models. This led to a torchscript that lets users develop and debug using pure imperative programming, while having the ability to convert most programs into symbolic programs to be run when product-level computing performance and deployment are required. +:end_tab: + +:begin_tab:`tensorflow` +The imperative programming paradigm is now the default in Tensorflow 2, a welcoming change for those new to the language. However, the same symbolic programming techniques and subsequent computational graphs still exist in TensorFlow, and can be accessed by the easy-to-use `tf.function` decorator. This brought the imperative programming paradigm to TensorFlow, allowed users to define more intuitive functions, then wrap them and compile them into computational graphs automatically using a feature the TensorFlow team refers to as [autograph](https://www.tensorflow.org/api_docs/python/tf/autograph). +:end_tab: + +## Hybridizing the `Sequential` Class + +The easiest way to get a feel for how hybridization works is to consider deep networks with multiple layers. Conventionally the Python interpreter will need to execute the code for all layers to generate an instruction that can then be forwarded to a CPU or a GPU. For a single (fast) computing device this does not cause any major issues. On the other hand, if we use an advanced 8-GPU server such as an AWS P3dn.24xlarge instance Python will struggle to keep all GPUs busy. The single-threaded Python interpreter becomes the bottleneck here. Let us see how we can address this for significant parts of the code by replacing `Sequential` with `HybridSequential`. We begin by defining a simple MLP. + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import np, npx +from mxnet.gluon import nn +npx.set_np() + +# Factory for networks +def get_net(): + net = nn.HybridSequential() + net.add(nn.Dense(256, activation='relu'), + nn.Dense(128, activation='relu'), + nn.Dense(2)) + net.initialize() + return net + +x = np.random.normal(size=(1, 512)) +net = get_net() +net(x) +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +from torch import nn + +# Factory for networks +def get_net(): + net = nn.Sequential(nn.Linear(512, 256), + nn.ReLU(), + nn.Linear(256, 128), + nn.ReLU(), + nn.Linear(128, 2)) + return net + +x = torch.randn(size=(1, 512)) +net = get_net() +net(x) +``` + +```{.python .input} +#@tab tensorflow +from d2l import tensorflow as d2l +import tensorflow as tf +from tensorflow.keras.layers import Dense + +# Factory for networks +def get_net(): + net = tf.keras.Sequential() + net.add(Dense(256, input_shape = (512,), activation = "relu")) + net.add(Dense(128, activation = "relu")) + net.add(Dense(2, activation = "linear")) + return net + +x = tf.random.normal([1,512]) +net = get_net() +net(x) +``` + +:begin_tab:`mxnet` +By calling the `hybridize` function, we are able to compile and optimize the computation in the MLP. The model's computation result remains unchanged. +:end_tab: + +:begin_tab:`pytorch` +By converting the model using `torch.jit.script` function, we are able to compile and optimize the computation in the MLP. The model's computation result remains unchanged. +:end_tab: + +:begin_tab:`tensorflow` +Formerly, all functions built in tensorflow were built as a computational graph, and therefore JIT compiled by default. However, with the release of tensorflow 2.X and eager tensors, this is no longer the default behavor. +We cen re-enable this functionality with tf.function. tf.function is more commonly used as a function decorator, however it is possible to call it direcly as a normal python function, shown below. The model's computation result remains unchanged. +:end_tab: + +```{.python .input} +net.hybridize() +net(x) +``` + +```{.python .input} +#@tab pytorch +net = torch.jit.script(net) +net(x) +``` + +```{.python .input} +#@tab tensorflow +net = tf.function(net) +net(x) +``` + +:begin_tab:`mxnet` +This seems almost too good to be true: simply designate a block to be `HybridSequential`, write the same code as before and invoke `hybridize`. Once this happens the network is optimized (we will benchmark the performance below). Unfortunately this does not work magically for every layer. That said, a layer will not be optimized if it inherits from the `Block` class instead of the `HybridBlock` class. +:end_tab: + +:begin_tab:`pytorch` +By converting the model using `torch.jit.script` This seems almost too good to be true: write the same code as before and simply convert the model using `torch.jit.script`. Once this happens the network is optimized (we will benchmark the performance below). +:end_tab: + +:begin_tab:`tensorflow` +Converting the model using `tf.function` gives us incredible power in TensorFlow: write the same code as before and simply convert the model using `tf.function`. Once this happens the network is built as a computational graph in TensorFlow's MLIR intermediate representation and is heavily optimized at the compiler level for rapid execution (we will benchmark the performance below). +Explicitly adding the `jit_compile = True` flag to the `tf.function()` call enables XLA (Accelerated Linear Algebra) functionality in TensorFlow. XLA can further optimize JIT compiled code in certain instances. Graph-mode execution is enabled without this explicit definition, however XLA can make certain large linear algebra operations (in the vein of those we see in deep learning applications) much faster, particularly in a GPU environment. +:end_tab: + +### Acceleration by Hybridization + +To demonstrate the performance improvement gained by compilation we compare the time needed to evaluate `net(x)` before and after hybridization. Let us define a function to measure this time first. It will come handy throughout the chapter as we set out to measure (and improve) performance. + +```{.python .input} +#@tab all +#@save +class Benchmark: + def __init__(self, description='Done'): + self.description = description + + def __enter__(self): + self.timer = d2l.Timer() + return self + + def __exit__(self, *args): + print(f'{self.description}: {self.timer.stop():.4f} sec') +``` + +:begin_tab:`mxnet` +Now we can invoke the network twice, once with and once without hybridization. +:end_tab: + +:begin_tab:`pytorch` +Now we can invoke the network twice, once with and once without torchscript. +:end_tab: + +:begin_tab:`tensorflow` +Now we can invoke the network three times, once executed eagerly, once with graph-mode execution, and again using JIT compiled XLA. +:end_tab: + +```{.python .input} +net = get_net() +with Benchmark('Without hybridization'): + for i in range(1000): net(x) + npx.waitall() + +net.hybridize() +with Benchmark('With hybridization'): + for i in range(1000): net(x) + npx.waitall() +``` + +```{.python .input} +#@tab pytorch +net = get_net() +with Benchmark('Without torchscript'): + for i in range(1000): net(x) + +net = torch.jit.script(net) +with Benchmark('With torchscript'): + for i in range(1000): net(x) +``` + +```{.python .input} +#@tab tensorflow +net = get_net() +with Benchmark('Eager Mode'): + for i in range(1000): net(x) + +net = tf.function(net) +with Benchmark('Graph Mode'): + for i in range(1000): net(x) +``` + +:begin_tab:`mxnet` +As is observed in the above results, after a `HybridSequential` instance calls the `hybridize` function, computing performance is improved through the use of symbolic programming. +:end_tab: + +:begin_tab:`pytorch` +As is observed in the above results, after an `nn.Sequential` instance is scripted using the `torch.jit.script` function, computing performance is improved through the use of symbolic programming. +:end_tab: + +:begin_tab:`tensorflow` +As is observed in the above results, after a tf.keras Sequential instance is scripted using the `tf.function` function, computing performance is improved through the use of symbolic programming via graph-mode execution in tensorflow. +:end_tab: + +### Serialization + +:begin_tab:`mxnet` +One of the benefits of compiling the models is that we can serialize (save) the model and its parameters to disk. This allows us to store a model in a manner that is independent of the front-end language of choice. This allows us to deploy trained models to other devices and easily use other front-end programming languages. At the same time the code is often faster than what can be achieved in imperative programming. Let us see the `export` function in action. +:end_tab: + +:begin_tab:`pytorch` +One of the benefits of compiling the models is that we can serialize (save) the model and its parameters to disk. This allows us to store a model in a manner that is independent of the front-end language of choice. This allows us to deploy trained models to other devices and easily use other front-end programming languages. At the same time the code is often faster than what can be achieved in imperative programming. Let us see the `save` function in action. +:end_tab: + +:begin_tab:`tensorflow` +One of the benefits of compiling the models is that we can serialize (save) the model and its parameters to disk. This allows us to store a model in a manner that is independent of the front-end language of choice. This allows us to deploy trained models to other devices and easily use other front-end programming languages or execute a trained model on a server. At the same time the code is often faster than what can be achieved in imperative programming. +The low-level API that allows us to save in tensorflow is `tf.saved_model`. +Let's see the `saved_model` instance in action. +:end_tab: + +```{.python .input} +net.export('my_mlp') +!ls -lh my_mlp* +``` + +```{.python .input} +#@tab pytorch +net.save('my_mlp') +!ls -lh my_mlp* +``` + +```{.python .input} +#@tab tensorflow +net = get_net() +tf.saved_model.save(net, 'my_mlp') +!ls -lh my_mlp* +``` + +:begin_tab:`mxnet` +The model is decomposed into a (large binary) parameter file and a JSON description of the program required to execute the model computation. The files can be read by other front-end languages supported by Python or MXNet, such as C++, R, Scala, and Perl. Let us have a look at the first few lines in the model description. +:end_tab: + +```{.python .input} +!head my_mlp-symbol.json +``` + +:begin_tab:`mxnet` +Earlier, we demonstrated that, after calling the `hybridize` function, the model is able to achieve superior computing performance and portability. Note, though that hybridization can affect model flexibility, in particular in terms of control flow. + +Besides, contrary to the `Block` instance, which needs to use the `forward` function, for a `HybridBlock` instance we need to use the `hybrid_forward` function. +:end_tab: + +```{.python .input} +class HybridNet(nn.HybridBlock): + def __init__(self, **kwargs): + super(HybridNet, self).__init__(**kwargs) + self.hidden = nn.Dense(4) + self.output = nn.Dense(2) + + def hybrid_forward(self, F, x): + print('module F: ', F) + print('value x: ', x) + x = F.npx.relu(self.hidden(x)) + print('result : ', x) + return self.output(x) +``` + +:begin_tab:`mxnet` +The code above implements a simple network with 4 hidden units and 2 outputs. The `hybrid_forward` function takes an additional argument `F`. This is needed since, depending on whether the code has been hybridized or not, it will use a slightly different library (`ndarray` or `symbol`) for processing. Both classes perform very similar functions and MXNet automatically determines the argument. To understand what is going on we print the arguments as part of the function invocation. +:end_tab: + +```{.python .input} +net = HybridNet() +net.initialize() +x = np.random.normal(size=(1, 3)) +net(x) +``` + +:begin_tab:`mxnet` +Repeating the forward computation will lead to the same output (we omit details). Now let us see what happens if we invoke the `hybridize` function. +:end_tab: + +```{.python .input} +net.hybridize() +net(x) +``` + +:begin_tab:`mxnet` +Instead of using `ndarray` we now use the `symbol` module for `F`. Moreover, even though the input is of `ndarray` type, the data flowing through the network is now converted to `symbol` type as part of the compilation process. Repeating the function call leads to a surprising outcome: +:end_tab: + +```{.python .input} +net(x) +``` + +:begin_tab:`mxnet` +This is quite different from what we saw previously. All print statements, as defined in `hybrid_forward`, are omitted. Indeed, after hybridization the execution of `net(x)` does not involve the Python interpreter any longer. This means that any spurious Python code is omitted (such as print statements) in favor of a much more streamlined execution and better performance. Instead, MXNet directly calls the C++ backend. Also note that some functions are not supported in the `symbol` module (e.g., `asnumpy`) and operations in-place such as `a += b` and `a[:] = a + b` must be rewritten as `a = a + b`. Nonetheless, compilation of models is worth the effort whenever speed matters. The benefit can range from small percentage points to more than twice the speed, depending on the complexity of the model, the speed of the CPU, and the speed and number of GPUs. +:end_tab: + +## Summary + + +* Imperative programming makes it easy to design new models since it is possible to write code with control flow and the ability to use a large amount of the Python software ecosystem. +* Symbolic programming requires that we specify the program and compile it before executing it. The benefit is improved performance. + +:begin_tab:`mxnet` +* MXNet is able to combine the advantages of both approaches as needed. +* Models constructed by the `HybridSequential` and `HybridBlock` classes are able to convert imperative programs into symbolic programs by calling the `hybridize` function. +:end_tab: + + +## Exercises + + +:begin_tab:`mxnet` +1. Add `x.asnumpy()` to the first line of the `hybrid_forward` function of the `HybridNet` class in this section. Execute the code and observe the errors you encounter. Why do they happen? +1. What happens if we add control flow, i.e., the Python statements `if` and `for` in the `hybrid_forward` function? +1. Review the models that interest you in the previous chapters. Can you improve their computational performance by reimplementing them? +:end_tab: + +:begin_tab:`pytorch,tensorflow` +1. Review the models that interest you in the previous chapters. Can you improve their computational performance by reimplementing them? +:end_tab: + + + + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/360) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/2490) +:end_tab: + +:begin_tab:`tensorflow` +[Discussions](https://discuss.d2l.ai/t/2492) +:end_tab: diff --git a/chapter_computational-performance/index.md b/chapter_computational-performance/index.md new file mode 100644 index 000000000..c3eef0003 --- /dev/null +++ b/chapter_computational-performance/index.md @@ -0,0 +1,16 @@ +# 计算性能 +:label:`chap_performance` + +在深度学习中,数据集和模型通常很大,这涉及大量计算。因此,计算性能非常重要。本章将重点介绍影响计算性能的主要因素:命令式编程、符号编程、异步计算、自动并行度和多 GPU 计算。通过研究本章,您可以进一步提高前几章中实施的模型的计算性能,例如,通过在不影响准确性的情况下缩短训练时间。 + +```toc +:maxdepth: 2 + +hybridize +async-computation +auto-parallelism +hardware +multiple-gpus +multiple-gpus-concise +parameterserver +``` diff --git a/chapter_computational-performance/index_origin.md b/chapter_computational-performance/index_origin.md new file mode 100644 index 000000000..212c59a28 --- /dev/null +++ b/chapter_computational-performance/index_origin.md @@ -0,0 +1,23 @@ +# Computational Performance +:label:`chap_performance` + +In deep learning, +datasets and models are usually large, +which involves heavy computation. +Therefore, computational performance matters a lot. +This chapter will focus on the major factors that affect computational performance: +imperative programming, symbolic programming, asynchronous computing, automatic parallellism, and multi-GPU computation. +By studying this chapter, you may further improve computational performance of those models implemented in the previous chapters, +for example, by reducing training time without affecting accuracy. + +```toc +:maxdepth: 2 + +hybridize +async-computation +auto-parallelism +hardware +multiple-gpus +multiple-gpus-concise +parameterserver +``` From 2a3161c56cae4dde1d86c08573f4f81ed3dfbe49 Mon Sep 17 00:00:00 2001 From: Mu Li Date: Fri, 16 Apr 2021 15:45:33 -0700 Subject: [PATCH 038/103] [slides] dl computation (#749) * add slides * fix CI error --- .../custom-layer.md | 10 +- .../model-construction.md | 14 +- .../parameters.md | 22 +- .../read-write.md | 12 +- chapter_deep-learning-computation/use-gpu.md | 22 +- d2l/__init__.py | 2 +- d2l/mxnet.py | 620 ++-------------- d2l/tensorflow.py | 214 ++---- d2l/torch.py | 670 +++--------------- index.md | 2 + 10 files changed, 257 insertions(+), 1331 deletions(-) diff --git a/chapter_deep-learning-computation/custom-layer.md b/chapter_deep-learning-computation/custom-layer.md index cd4d5b5fa..9f6eb2ddc 100644 --- a/chapter_deep-learning-computation/custom-layer.md +++ b/chapter_deep-learning-computation/custom-layer.md @@ -4,7 +4,7 @@ ## 不带参数的层 -首先,我们构造一个没有任何参数的自定义层。如果你还记得我们在 :numref:`sec_model_construction` 对块的介绍,这应该看起来很眼熟。下面的`CenteredLayer`类要从其输入中减去均值。要构建它,我们只需继承基础层类并实现正向传播功能。 +首先,我们(**构造一个没有任何参数的自定义层**)。如果你还记得我们在 :numref:`sec_model_construction` 对块的介绍,这应该看起来很眼熟。下面的`CenteredLayer`类要从其输入中减去均值。要构建它,我们只需继承基础层类并实现正向传播功能。 ```{.python .input} from mxnet import np, npx @@ -64,7 +64,7 @@ layer = CenteredLayer() layer(tf.constant([1, 2, 3, 4, 5])) ``` -现在,我们可以将层作为组件合并到构建更复杂的模型中。 +现在,我们可以[**将层作为组件合并到构建更复杂的模型中**]。 ```{.python .input} net = nn.Sequential() @@ -101,7 +101,7 @@ Y = net(tf.random.uniform((4, 8))) tf.reduce_mean(Y) ``` -## 带参数的图层 +## [**带参数的图层**] 既然我们知道了如何定义简单的层,那么让我们继续定义具有参数的层,这些参数可以通过训练进行调整。我们可以使用内置函数来创建参数,这些参数提供一些基本的管理功能。比如管理访问、初始化、共享、保存和加载模型参数。这样做的好处之一是,我们不需要为每个自定义层编写自定义序列化程序。 @@ -172,7 +172,7 @@ dense(tf.random.uniform((2, 5))) dense.get_weights() ``` -我们可以使用自定义层直接执行正向传播计算。 +我们可以[**使用自定义层直接执行正向传播计算**]。 ```{.python .input} dense.initialize() @@ -189,7 +189,7 @@ dense(torch.rand(2, 5)) dense(tf.random.uniform((2, 5))) ``` -我们还可以使用自定义层构建模型。我们可以像使用内置的全连接层一样使用自定义层。 +我们还可以(**使用自定义层构建模型**)。我们可以像使用内置的全连接层一样使用自定义层。 ```{.python .input} net = nn.Sequential() diff --git a/chapter_deep-learning-computation/model-construction.md b/chapter_deep-learning-computation/model-construction.md index b48d95d63..3e4aa92a7 100644 --- a/chapter_deep-learning-computation/model-construction.md +++ b/chapter_deep-learning-computation/model-construction.md @@ -14,7 +14,7 @@ 从编程的角度来看,块由*类*(class)表示。它的任何子类都必须定义一个将其输入转换为输出的正向传播函数,并且必须存储任何必需的参数。注意,有些块不需要任何参数。最后,为了计算梯度,块必须具有反向传播函数。幸运的是,在定义我们自己的块时,由于自动微分(在 :numref:`sec_autograd` 中引入)提供了一些后端实现,我们只需要考虑正向传播函数和必需的参数。 -首先,我们回顾一下多层感知机( :numref:`sec_mlp_concise` )的代码。下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接的隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接的输出层。 +(**首先,我们回顾一下多层感知机**)( :numref:`sec_mlp_concise` )的代码。下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接的隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接的输出层。 ```{.python .input} from mxnet import np, npx @@ -60,14 +60,14 @@ net(X) :end_tab: :begin_tab:`pytorch` -在这个例子中,我们通过实例化`nn.Sequential`来构建我们的模型,层的执行顺序是作为参数传递的。简而言之,`nn.Sequential`定义了一种特殊的`Module`,即在PyTorch中表示一个块的类。它维护了一个由`Module`组成的有序列表,注意,两个全连接层都是`Linear`类的实例,`Linear`类本身就是`Module`的子类。正向传播(`forward`)函数也非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。注意,到目前为止,我们一直在通过`net(X)`调用我们的模型来获得模型的输出。这实际上是`net.__call__(X)`的简写。 +在这个例子中,我们通过实例化`nn.Sequential`来构建我们的模型,层的执行顺序是作为参数传递的。简而言之,(**`nn.Sequential`定义了一种特殊的`Module`**),即在PyTorch中表示一个块的类。它维护了一个由`Module`组成的有序列表,注意,两个全连接层都是`Linear`类的实例,`Linear`类本身就是`Module`的子类。正向传播(`forward`)函数也非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。注意,到目前为止,我们一直在通过`net(X)`调用我们的模型来获得模型的输出。这实际上是`net.__call__(X)`的简写。 :end_tab: :begin_tab:`tensorflow` 在这个例子中,我们通过实例化`keras.models.Sequential`来构建我们的模型,层的执行顺序是作为参数传递的。简而言之,`Sequential`定义了一种特殊的`keras.Model`,即在Keras中表示一个块的类。它维护了一个由`Model`组成的有序列表,注意两个全连接层都是`Model`类的实例,这个类本身就是`Model`的子类。正向传播(`call`)函数也非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。注意,到目前为止,我们一直在通过`net(X)`调用我们的模型来获得模型的输出。这实际上是`net.call(X)`的简写,这是通过Block类的`__call__`函数实现的一个Python技巧。 :end_tab: -## 自定义块 +## [**自定义块**] 要想直观地了解块是如何工作的,最简单的方法可能就是自己实现一个。在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能: @@ -130,7 +130,7 @@ class MLP(tf.keras.Model): 让我们首先关注正向传播函数。注意,它以`X`作为输入,计算带有激活函数的隐藏表示,并输出其未归一化的输出值。在这个`MLP`实现中,两个层都是实例变量。要了解这为什么是合理的,可以想象实例化两个多层感知机(`net1`和`net2`),并根据不同的数据对它们进行训练。当然,我们希望它们学到两种不同的模型。 -我们在构造函数中实例化多层感知机的层,然后在每次调用正向传播函数时调用这些层。注意一些关键细节。首先,我们定制的`__init__`函数通过`super().__init__()`调用父类的`__init__`函数,省去了重复编写适用于大多数块的模版代码的痛苦。然后我们实例化两个全连接层,分别为`self.hidden`和`self.out`。注意,除非我们实现一个新的运算符,否则我们不必担心反向传播函数或参数初始化,系统将自动生成这些。让我们试一下。 +我们在构造函数中[**实例化多层感知机的层,然后在每次调用正向传播函数时调用这些层**]。注意一些关键细节。首先,我们定制的`__init__`函数通过`super().__init__()`调用父类的`__init__`函数,省去了重复编写适用于大多数块的模版代码的痛苦。然后我们实例化两个全连接层,分别为`self.hidden`和`self.out`。注意,除非我们实现一个新的运算符,否则我们不必担心反向传播函数或参数初始化,系统将自动生成这些。让我们试一下。 ```{.python .input} net = MLP() @@ -152,7 +152,7 @@ net(X) 块抽象的一个主要优点是它的多功能性。我们可以子类化块以创建层(如全连接层的类)、整个模型(如上面的`MLP`类)或具有中等复杂度的各种组件。我们在接下来的章节中充分利用了这种多功能性,比如在处理卷积神经网络时。 -## 顺序块 +## [**顺序块**] 现在我们可以更仔细地看看`Sequential`类是如何工作的。回想一下`Sequential`的设计是为了把其他模块串起来。为了构建我们自己的简化的`MySequential`,我们只需要定义两个关键函数: 1. 一种将块逐个追加到列表中的函数。 @@ -241,7 +241,7 @@ net(X) 注意,`MySequential`的用法与之前为`Sequential`类编写的代码相同(如 :numref:`sec_mlp_concise` 中所述)。 -## 在正向传播函数中执行代码 +## [**在正向传播函数中执行代码**] `Sequential`类使模型构造变得简单,允许我们组合新的结构,而不必定义自己的类。然而,并不是所有的架构都是简单的顺序结构。当需要更大的灵活性时,我们需要定义自己的块。例如,我们可能希望在正向传播函数中执行Python的控制流。此外,我们可能希望执行任意的数学运算,而不是简单地依赖预定义的神经网络层。 @@ -327,7 +327,7 @@ net = FixedHiddenMLP() net(X) ``` -我们可以混合搭配各种组合块的方法。在下面的例子中,我们以一些想到的方法嵌套块。 +我们可以[**混合搭配各种组合块的方法**]。在下面的例子中,我们以一些想到的方法嵌套块。 ```{.python .input} class NestMLP(nn.Block): diff --git a/chapter_deep-learning-computation/parameters.md b/chapter_deep-learning-computation/parameters.md index ef5f6e868..db12a6da3 100644 --- a/chapter_deep-learning-computation/parameters.md +++ b/chapter_deep-learning-computation/parameters.md @@ -8,7 +8,7 @@ * 参数初始化。 * 在不同模型组件间共享参数。 -我们首先关注具有单隐藏层的多层感知机。 +(**我们首先关注具有单隐藏层的多层感知机。**) ```{.python .input} from mxnet import init, np, npx @@ -48,7 +48,7 @@ X = tf.random.uniform((2, 4)) net(X) ``` -## 参数访问 +## [**参数访问**] 我们从已有模型中访问参数。当通过`Sequential`类定义模型时,我们可以通过索引来访问模型的任意层。这就像模型是一个列表一样。每层的参数都在其属性中。如下所示,我们可以检查第二个全连接层的参数。 @@ -68,7 +68,7 @@ print(net.layers[2].weights) 输出的结果告诉我们一些重要的事情。首先,这个全连接层包含两个参数,分别是该层的权重和偏置。两者都存储为单精度浮点数(float32)。注意,参数名称允许我们唯一地标识每个参数,即使在包含数百个层的网络中也是如此。 -### 目标参数 +### [**目标参数**] 注意,每个参数都表示为参数(parameter)类的一个实例。要对参数执行任何操作,首先我们需要访问底层的数值。有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。下面的代码从第二个神经网络层提取偏置,提取后返回的是一个参数类实例,并进一步访问该参数的值。 @@ -107,7 +107,7 @@ net[1].weight.grad() net[2].weight.grad == None ``` -### 一次性访问所有参数 +### [**一次性访问所有参数**] 当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂,因为我们需要递归整个树来提取每个子块的参数。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。 @@ -144,7 +144,7 @@ net.state_dict()['2.bias'].data net.get_weights()[1] ``` -### 从嵌套块收集参数 +### [**从嵌套块收集参数**] 让我们看看,如果我们将多个块相互嵌套,参数命名约定是如何工作的。为此,我们首先定义一个生成块的函数(可以说是块工厂),然后将这些块组合到更大的块中。 @@ -207,7 +207,7 @@ rgnet.add(tf.keras.layers.Dense(1)) rgnet(X) ``` -现在我们已经设计了网络,让我们看看它是如何组织的。 +现在[**我们已经设计了网络,让我们看看它是如何组织的。**] ```{.python .input} print(rgnet.collect_params) @@ -256,7 +256,7 @@ rgnet.layers[0].layers[1].layers[1].weights[1] 默认情况下,Keras会根据一个范围均匀地初始化权重矩阵,这个范围是根据输入和输出维度计算出的。偏置参数设置为0。TensorFlow在根模块和`keras.initializers`模块中提供了各种初始化方法。 :end_tab: -### 内置初始化 +### [**内置初始化**] 让我们首先调用内置的初始化器。下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。 @@ -322,7 +322,7 @@ net(X) net.weights[0], net.weights[1] ``` -我们还可以对某些块应用不同的初始化方法。例如,下面我们使用Xavier初始化方法初始化第一层,然后第二层初始化为常量值42。 +我们还可以[**对某些块应用不同的初始化方法**]。例如,下面我们使用Xavier初始化方法初始化第一层,然后第二层初始化为常量值42。 ```{.python .input} net[0].weight.initialize(init=init.Xavier(), force_reinit=True) @@ -363,7 +363,7 @@ print(net.layers[1].weights[0]) print(net.layers[2].weights[0]) ``` -### 自定义初始化 +### [**自定义初始化**] 有时,深度学习框架没有提供我们需要的初始化方法。在下面的例子中,我们使用以下的分布为任意权重参数$w$定义初始化方法: @@ -404,7 +404,7 @@ net[0].weight.data()[:2] #@tab pytorch def my_init(m): if type(m) == nn.Linear: - print("Init", *[(name, param.shape) + print("Init", *[(name, param.shape) for name, param in m.named_parameters()][0]) nn.init.uniform_(m.weight, -10, 10) m.weight.data *= m.weight.data.abs() >= 5 @@ -461,7 +461,7 @@ net.layers[1].weights[0] 高级用户请注意:如果要在`autograd`范围内调整参数,则需要使用`set_data`,以避免误导自动微分机制。 :end_tab: -## 参数绑定 +## [**参数绑定**] 有时我们希望在多个层间共享参数。让我们看看如何优雅地做这件事。在下面,我们定义一个稠密层,然后使用它的参数来设置另一个层的参数。 diff --git a/chapter_deep-learning-computation/read-write.md b/chapter_deep-learning-computation/read-write.md index 25b094f3e..60994e606 100644 --- a/chapter_deep-learning-computation/read-write.md +++ b/chapter_deep-learning-computation/read-write.md @@ -2,7 +2,7 @@ 到目前为止,我们讨论了如何处理数据,以及如何构建、训练和测试深度学习模型。然而,有时我们对所学的模型足够满意,我们希望保存训练的模型以备将来在各种环境中使用(可能部署进行预测)。此外,当运行一个耗时较长的训练过程时,最佳实践是定期保存中间结果(检查点),以确保在服务器电源被不小心断掉时不会损失几天的计算结果。因此,现在是时候学习如何加载和存储权重向量和整个模型。本节将讨论这些问题。 -## 加载和保存张量 +## (**加载和保存张量**) 对于单个张量,我们可以直接调用`load`和`save`函数分别读写它们。这两个函数都要求我们提供一个名称,`save`要求将要保存的变量作为输入。 @@ -53,7 +53,7 @@ x2 = np.load('x-file.npy', allow_pickle=True) x2 ``` -我们可以存储一个张量列表,然后把它们读回内存。 +我们可以[**存储一个张量列表,然后把它们读回内存。**] ```{.python .input} y = np.zeros(4) @@ -78,7 +78,7 @@ x2, y2 = np.load('xy-files.npy', allow_pickle=True) (x2, y2) ``` -我们甚至可以写入或读取从字符串映射到张量的字典。当我们要读取或写入模型中的所有权重时,这很方便。 +我们甚至可以(**写入或读取从字符串映射到张量的字典**)。当我们要读取或写入模型中的所有权重时,这很方便。 ```{.python .input} mydict = {'x': x, 'y': y} @@ -103,7 +103,7 @@ mydict2 = np.load('mydict.npy', allow_pickle=True) mydict2 ``` -## 加载和保存模型参数 +## [**加载和保存模型参数**] 保存单个权重向量(或其他张量)确实是有用的,但是如果我们想保存整个模型,并在以后加载它们。单独保存每个向量则会变得很麻烦。毕竟,我们可能有数百个参数散布在各处。因此,深度学习框架提供了内置函数来保存和加载整个网络。需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。例如,如果我们有一个3层多层感知机,我们需要单独指定结构。因为模型本身可以包含任意代码,所以模型本身难以序列化。因此,为了恢复模型,我们需要用代码生成结构,然后从磁盘加载参数。让我们从熟悉的多层感知机开始尝试一下。 @@ -158,7 +158,7 @@ X = tf.random.uniform((2, 20)) Y = net(X) ``` -接下来,我们将模型的参数存储为一个叫做“mlp.params”的文件。 +接下来,我们[**将模型的参数存储为一个叫做“mlp.params”的文件。**] ```{.python .input} net.save_parameters('mlp.params') @@ -174,7 +174,7 @@ torch.save(net.state_dict(), 'mlp.params') net.save_weights('mlp.params') ``` -为了恢复模型,我们实例化了原始多层感知机模型的一个备份。我们没有随机初始化模型参数,而是直接读取文件中存储的参数。 +为了恢复模型,我们[**实例化了原始多层感知机模型的一个备份。**]我们没有随机初始化模型参数,而是(**直接读取文件中存储的参数。**) ```{.python .input} clone = MLP() diff --git a/chapter_deep-learning-computation/use-gpu.md b/chapter_deep-learning-computation/use-gpu.md index 163647335..849122fd4 100644 --- a/chapter_deep-learning-computation/use-gpu.md +++ b/chapter_deep-learning-computation/use-gpu.md @@ -5,7 +5,7 @@ 在本节中,我们开始讨论如何利用这种计算性能进行研究。首先是使用单个GPU,然后是如何使用多个GPU和多个服务器(具有多个GPU)。 -具体来说,我们将讨论如何使用单个NVIDIA GPU进行计算。首先,确保至少安装了一个NVIDIA GPU。然后,下载[NVIDIA驱动和CUDA](https://developer.nvidia.com/cuda-downloads)并按照提示设置适当的路径。当这些准备工作完成,就可以使用`nvidia-smi`命令来查看显卡信息。 +具体来说,我们将讨论如何使用单个NVIDIA GPU进行计算。首先,确保至少安装了一个NVIDIA GPU。然后,下载[NVIDIA驱动和CUDA](https://developer.nvidia.com/cuda-downloads)并按照提示设置适当的路径。当这些准备工作完成,就可以使用`nvidia-smi`命令来(**查看显卡信息。**) ```{.python .input} #@tab all @@ -28,7 +28,7 @@ 要运行此部分中的程序,至少需要两个GPU。注意,对于大多数桌面计算机来说,这可能是奢侈的,但在云中很容易获得,例如,通过使用AWS EC2的多GPU实例。本节几乎所有的其他部分都不需要多个GPU。本节只是为了说明数据如何在不同的设备之间传递。 -## 计算设备 +## [**计算设备**] 我们可以指定用于存储和计算的设备,如CPU和GPU。默认情况下,张量是在内存中创建的,然后使用CPU计算它。 @@ -63,7 +63,7 @@ import tensorflow as tf tf.device('/CPU:0'), tf.device('/GPU:0'), tf.device('/GPU:1') ``` -我们可以查询可用gpu的数量。 +我们可以(**查询可用gpu的数量。**) ```{.python .input} npx.num_gpus() @@ -79,7 +79,7 @@ torch.cuda.device_count() len(tf.config.experimental.list_physical_devices('GPU')) ``` -现在我们定义了两个方便的函数,这两个函数允许我们在请求的GPU不存在的情况下运行代码。 +现在我们定义了两个方便的函数,[**这两个函数允许我们在请求的GPU不存在的情况下运行代码。**] ```{.python .input} def try_gpu(i=0): #@save @@ -130,7 +130,7 @@ try_gpu(), try_gpu(10), try_all_gpus() ## 张量与gpu -默认情况下,张量是在CPU上创建的。我们可以查询张量所在的设备。 +默认情况下,张量是在CPU上创建的。我们可以[**查询张量所在的设备。**] ```{.python .input} x = np.array([1, 2, 3]) @@ -151,7 +151,7 @@ x.device 需要注意的是,无论何时我们要对多个项进行操作,它们都必须在同一个设备上。例如,如果我们对两个张量求和,我们需要确保两个张量都位于同一个设备上,否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。 -### 存储在GPU上 +### [**存储在GPU上**] 有几种方法可以在GPU上存储张量。例如,我们可以在创建张量时指定存储设备。接下来,我们在第一个`gpu`上创建张量变量`X`。在GPU上创建的张量只消耗这个GPU的显存。我们可以使用`nvidia-smi`命令查看显存使用情况。一般来说,我们需要确保不创建超过GPU显存限制的数据。 @@ -173,7 +173,7 @@ with try_gpu(): X ``` -假设你至少有两个GPU,下面的代码将在第二个GPU上创建一个随机张量。 +假设你至少有两个GPU,下面的代码将在(**第二个GPU上创建一个随机张量。**) ```{.python .input} Y = np.random.uniform(size=(2, 3), ctx=try_gpu(1)) @@ -195,7 +195,7 @@ Y ### 复制 -如果我们要计算`X + Y`,我们需要决定在哪里执行这个操作。例如,如 :numref:`fig_copyto` 所示,我们可以将`X`传输到第二个GPU并在那里执行操作。 +如果我们[**要计算`X + Y`,我们需要决定在哪里执行这个操作**]。例如,如 :numref:`fig_copyto` 所示,我们可以将`X`传输到第二个GPU并在那里执行操作。 *不要*简单地`X`加上`Y`, 因为这会导致异常。运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。由于`Y`位于第二个GPU上,所以我们需要将`X`移到那里,然后才能执行相加运算。 @@ -223,7 +223,7 @@ print(X) print(Z) ``` -现在数据在同一个GPU上(`Z`和`Y`都在),我们可以将它们相加。 +[**现在数据在同一个GPU上(`Z`和`Y`都在),我们可以将它们相加。**] ```{.python .input} #@tab all @@ -266,7 +266,7 @@ Z2 is Z 最后,当我们打印张量或将张量转换为NumPy格式时。如果数据不在内存中,框架会首先将其复制到内存中,这会导致额外的传输开销。更糟糕的是,它现在受制于可怕的全局解释器锁,这使得一切都得等待Python完成。 -## 神经网络与GPU +## [**神经网络与GPU**] 类似地,神经网络模型可以指定设备。下面的代码将模型参数放在GPU上。 @@ -299,7 +299,7 @@ with strategy.scope(): net(X) ``` -让我们确认模型参数存储在同一个GPU上。 +让我们(**确认模型参数存储在同一个GPU上。**) ```{.python .input} net[0].weight.data().ctx diff --git a/d2l/__init__.py b/d2l/__init__.py index 4e5939acf..63bc06bef 100644 --- a/d2l/__init__.py +++ b/d2l/__init__.py @@ -8,4 +8,4 @@ """ -__version__ = "0.16.1" +__version__ = "2.0.0-alpha0" diff --git a/d2l/mxnet.py b/d2l/mxnet.py index 0b0e52017..f4f2ad24c 100644 --- a/d2l/mxnet.py +++ b/d2l/mxnet.py @@ -15,7 +15,6 @@ import time import zipfile from collections import defaultdict - import pandas as pd import requests from IPython import display @@ -67,7 +66,7 @@ def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # Return True if `X` (tensor or list) has 1 axis + # 如果 `X` 有一个轴,输出True def has_one_axis(X): return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and not hasattr(X[0], "__len__")) @@ -495,28 +494,28 @@ def read_time_machine(): # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def tokenize(lines, token='word'): - """Split text lines into word or character tokens.""" + """将文本行拆分为单词或字符标记。""" if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: - print('ERROR: unknown token type: ' + token) + print('错误:未知令牌类型:' + token) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md class Vocab: - """Vocabulary for text.""" + """文本词表""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: reserved_tokens = [] - # Sort according to frequencies + # 按出现频率排序 counter = count_corpus(tokens) - self.token_freqs = sorted(counter.items(), key=lambda x: x[0]) - self.token_freqs.sort(key=lambda x: x[1], reverse=True) - # The index for the unknown token is 0 + self.token_freqs = sorted(counter.items(), key=lambda x: x[1], + reverse=True) + # 未知标记的索引为0 self.unk, uniq_tokens = 0, [''] + reserved_tokens uniq_tokens += [ token for token, freq in self.token_freqs @@ -541,21 +540,21 @@ def to_tokens(self, indices): def count_corpus(tokens): """Count token frequencies.""" - # Here `tokens` is a 1D list or 2D list + # 这里的 `tokens` 是1D列表或2D列表 if len(tokens) == 0 or isinstance(tokens[0], list): - # Flatten a list of token lists into a list of tokens + # 将令牌列表展平 tokens = [token for line in tokens for token in line] return collections.Counter(tokens) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def load_corpus_time_machine(max_tokens=-1): - """Return token indices and the vocabulary of the time machine dataset.""" + """返回时光机器数据集的令牌索引和词汇表。""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # Since each text line in the time machine dataset is not necessarily a - # sentence or a paragraph, flatten all the text lines into a single list + # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, + # 所以将所有文本行展平到一个列表中 corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] @@ -564,26 +563,23 @@ def load_corpus_time_machine(max_tokens=-1): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_random(corpus, batch_size, num_steps): - """Generate a minibatch of subsequences using random sampling.""" - # Start with a random offset to partition a sequence - corpus = corpus[random.randint(0, num_steps):] - # Subtract 1 since we need to account for labels + """使用随机抽样生成一小批子序列。""" + # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 + corpus = corpus[random.randint(0, num_steps - 1):] + # 减去1,因为我们需要考虑标签 num_subseqs = (len(corpus) - 1) // num_steps - # The starting indices for subsequences of length `num_steps` + # 长度为`num_steps`的子序列的起始索引 initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # In random sampling, the subsequences from two adjacent random - # minibatches during iteration are not necessarily adjacent on the - # original sequence + # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 random.shuffle(initial_indices) def data(pos): - # Return a sequence of length `num_steps` starting from `pos` + # 返回从`pos`开始的长度为`num_steps`的序列 return corpus[pos:pos + num_steps] - num_subseqs_per_example = num_subseqs // batch_size - for i in range(0, batch_size * num_subseqs_per_example, batch_size): - # Here, `initial_indices` contains randomized starting indices for - # subsequences + num_batches = num_subseqs // batch_size + for i in range(0, batch_size * num_batches, batch_size): + # 这里,`initial_indices`包含子序列的随机起始索引 initial_indices_per_batch = initial_indices[i:i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] @@ -592,15 +588,15 @@ def data(pos): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_sequential(corpus, batch_size, num_steps): - """Generate a minibatch of subsequences using sequential partitioning.""" - # Start with a random offset to partition a sequence + """使用顺序分区生成一小批子序列。""" + # 从随机偏移量开始划分序列 offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size Xs = d2l.tensor(corpus[offset:offset + num_tokens]) Ys = d2l.tensor(corpus[offset + 1:offset + 1 + num_tokens]) Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1) num_batches = Xs.shape[1] // num_steps - for i in range(0, num_batches * num_steps, num_steps): + for i in range(0, num_steps * num_batches, num_steps): X = Xs[:, i:i + num_steps] Y = Ys[:, i:i + num_steps] yield X, Y @@ -608,7 +604,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md class SeqDataLoader: - """An iterator to load sequence data.""" + """加载序列数据的迭代器。""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): if use_random_iter: self.data_iter_fn = d2l.seq_data_iter_random @@ -624,7 +620,7 @@ def __iter__(self): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): - """Return the iterator and the vocabulary of the time machine dataset.""" + """返回时光机器数据集的迭代器和词表。""" data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab @@ -632,7 +628,7 @@ def load_data_time_machine(batch_size, num_steps, use_random_iter=False, # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md class RNNModelScratch: - """An RNN Model implemented from scratch.""" + """从零开始实现的循环神经网络模型""" def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn): self.vocab_size, self.num_hiddens = vocab_size, num_hiddens @@ -648,28 +644,28 @@ def begin_state(self, batch_size, ctx): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def predict_ch8(prefix, num_preds, model, vocab, device): - """Generate new characters following the `prefix`.""" - state = model.begin_state(batch_size=1, ctx=device) +def predict_ch8(prefix, num_preds, net, vocab, device): + """在`prefix`后面生成新字符。""" + state = net.begin_state(batch_size=1, ctx=device) outputs = [vocab[prefix[0]]] get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]], ctx=device), (1, 1)) - for y in prefix[1:]: # Warm-up period - _, state = model(get_input(), state) + for y in prefix[1:]: # 预热期 + _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # Predict `num_preds` steps - y, state = model(get_input(), state) + for _ in range(num_preds): # 预测`num_preds`步 + y, state = net(get_input(), state) outputs.append(int(y.argmax(axis=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def grad_clipping(model, theta): - """Clip the gradient.""" - if isinstance(model, gluon.Block): - params = [p.data() for p in model.collect_params().values()] +def grad_clipping(net, theta): + """裁剪梯度。""" + if isinstance(net, gluon.Block): + params = [p.data() for p in net.collect_params().values()] else: - params = model.params + params = net.params norm = math.sqrt(sum((p.grad**2).sum() for p in params)) if norm > theta: for param in params: @@ -677,62 +673,59 @@ def grad_clipping(model, theta): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def train_epoch_ch8(model, train_iter, loss, updater, device, - use_random_iter): - """Train a model within one epoch (defined in Chapter 8).""" +def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): + """训练模型一个迭代周期(定义见第8章)。""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + metric = d2l.Accumulator(2) # 训练损失之和, 标记数量 for X, Y in train_iter: if state is None or use_random_iter: - # Initialize `state` when either it is the first iteration or - # using random sampling - state = model.begin_state(batch_size=X.shape[0], ctx=device) + # 在第一次迭代或使用随机抽样时初始化`state` + state = net.begin_state(batch_size=X.shape[0], ctx=device) else: for s in state: s.detach() y = Y.T.reshape(-1) X, y = X.as_in_ctx(device), y.as_in_ctx(device) with autograd.record(): - y_hat, state = model(X, state) + y_hat, state = net(X, state) l = loss(y_hat, y).mean() l.backward() - grad_clipping(model, 1) - updater(batch_size=1) # Since the `mean` function has been invoked + grad_clipping(net, 1) + updater(batch_size=1) # 因为已经调用了`mean`函数 metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def train_ch8(model, train_iter, vocab, lr, num_epochs, device, +def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): - """Train a model (defined in Chapter 8).""" + """训练模型(定义见第8章)。""" loss = gluon.loss.SoftmaxCrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - # Initialize - if isinstance(model, gluon.Block): - model.initialize(ctx=device, force_reinit=True, - init=init.Normal(0.01)) - trainer = gluon.Trainer(model.collect_params(), 'sgd', + # 初始化 + if isinstance(net, gluon.Block): + net.initialize(ctx=device, force_reinit=True, init=init.Normal(0.01)) + trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) updater = lambda batch_size: trainer.step(batch_size) else: - updater = lambda batch_size: d2l.sgd(model.params, lr, batch_size) - predict = lambda prefix: predict_ch8(prefix, 50, model, vocab, device) - # Train and predict + updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) + predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) + # 训练和预测 for epoch in range(num_epochs): - ppl, speed = train_epoch_ch8(model, train_iter, loss, updater, device, + ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter) if (epoch + 1) % 10 == 0: animator.add(epoch + 1, [ppl]) - print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') + print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}') print(predict('time traveller')) print(predict('traveller')) # Defined in file: ./chapter_recurrent-neural-networks/rnn-concise.md class RNNModel(nn.Block): - """The RNN model.""" + """循环神经网络模型。""" def __init__(self, rnn_layer, vocab_size, **kwargs): super(RNNModel, self).__init__(**kwargs) self.rnn = rnn_layer @@ -742,9 +735,8 @@ def __init__(self, rnn_layer, vocab_size, **kwargs): def forward(self, inputs, state): X = npx.one_hot(inputs.T, self.vocab_size) Y, state = self.rnn(X, state) - # The fully-connected layer will first change the shape of `Y` to - # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is - # (`num_steps` * `batch_size`, `vocab_size`). + # 全连接层首先将`Y`的形状改为(`时间步数` * `批量大小`, `隐藏单元数`)。 + # 它的输出形状是 (`时间步数` * `批量大小`, `词表大小`)。 output = self.dense(Y.reshape(-1, Y.shape[-1])) return output, state @@ -752,485 +744,17 @@ def begin_state(self, *args, **kwargs): return self.rnn.begin_state(*args, **kwargs) -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', - '94646ad1522d915e7b0f9296181140edcf86a4f5') - -def read_data_nmt(): - """Load the English-French dataset.""" - data_dir = d2l.download_extract('fra-eng') - with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: - return f.read() - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def preprocess_nmt(text): - """Preprocess the English-French dataset.""" - def no_space(char, prev_char): - return char in set(',.!?') and prev_char != ' ' - - # Replace non-breaking space with space, and convert uppercase letters to - # lowercase ones - text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # Insert space between words and punctuation marks - out = [ - ' ' + char if i > 0 and no_space(char, text[i - 1]) else char - for i, char in enumerate(text)] - return ''.join(out) - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def tokenize_nmt(text, num_examples=None): - """Tokenize the English-French dataset.""" - source, target = [], [] - for i, line in enumerate(text.split('\n')): - if num_examples and i > num_examples: - break - parts = line.split('\t') - if len(parts) == 2: - source.append(parts[0].split(' ')) - target.append(parts[1].split(' ')) - return source, target - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def truncate_pad(line, num_steps, padding_token): - """Truncate or pad sequences.""" - if len(line) > num_steps: - return line[:num_steps] # Truncate - return line + [padding_token] * (num_steps - len(line)) # Pad - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def build_array_nmt(lines, vocab, num_steps): - """Transform text sequences of machine translation into minibatches.""" - lines = [vocab[l] for l in lines] - lines = [l + [vocab['']] for l in lines] - array = d2l.tensor([ - truncate_pad(l, num_steps, vocab['']) for l in lines]) - valid_len = d2l.reduce_sum(d2l.astype(array != vocab[''], d2l.int32), - 1) - return array, valid_len - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def load_data_nmt(batch_size, num_steps, num_examples=600): - """Return the iterator and the vocabularies of the translation dataset.""" - text = preprocess_nmt(read_data_nmt()) - source, target = tokenize_nmt(text, num_examples) - src_vocab = d2l.Vocab(source, min_freq=2, - reserved_tokens=['', '', '']) - tgt_vocab = d2l.Vocab(target, min_freq=2, - reserved_tokens=['', '', '']) - src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) - tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) - data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) - data_iter = d2l.load_array(data_arrays, batch_size) - return data_iter, src_vocab, tgt_vocab - - -# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md -class Encoder(nn.Block): - """The base encoder interface for the encoder-decoder architecture.""" - def __init__(self, **kwargs): - super(Encoder, self).__init__(**kwargs) - - def forward(self, X, *args): - raise NotImplementedError - - -# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md -class Decoder(nn.Block): - """The base decoder interface for the encoder-decoder architecture.""" - def __init__(self, **kwargs): - super(Decoder, self).__init__(**kwargs) - - def init_state(self, enc_outputs, *args): - raise NotImplementedError - - def forward(self, X, state): - raise NotImplementedError - - -# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md -class EncoderDecoder(nn.Block): - """The base class for the encoder-decoder architecture.""" - def __init__(self, encoder, decoder, **kwargs): - super(EncoderDecoder, self).__init__(**kwargs) - self.encoder = encoder - self.decoder = decoder - - def forward(self, enc_X, dec_X, *args): - enc_outputs = self.encoder(enc_X, *args) - dec_state = self.decoder.init_state(enc_outputs, *args) - return self.decoder(dec_X, dec_state) - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -class Seq2SeqEncoder(d2l.Encoder): - """The RNN encoder for sequence to sequence learning.""" - def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, - dropout=0, **kwargs): - super(Seq2SeqEncoder, self).__init__(**kwargs) - # Embedding layer - self.embedding = nn.Embedding(vocab_size, embed_size) - self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout) - - def forward(self, X, *args): - # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) - X = self.embedding(X) - # In RNN models, the first axis corresponds to time steps - X = X.swapaxes(0, 1) - state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx) - output, state = self.rnn(X, state) - # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) - # `state[0]` shape: (`num_layers`, `batch_size`, `num_hiddens`) - return output, state - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): - """The softmax cross-entropy loss with masks.""" - - # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) - # `label` shape: (`batch_size`, `num_steps`) - # `valid_len` shape: (`batch_size`,) - def forward(self, pred, label, valid_len): - # `weights` shape: (`batch_size`, `num_steps`, 1) - weights = np.expand_dims(np.ones_like(label), axis=-1) - weights = npx.sequence_mask(weights, valid_len, True, axis=1) - return super(MaskedSoftmaxCELoss, self).forward(pred, label, weights) - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """Train a model for sequence to sequence.""" - net.initialize(init.Xavier(), force_reinit=True, ctx=device) - trainer = gluon.Trainer(net.collect_params(), 'adam', - {'learning_rate': lr}) - loss = MaskedSoftmaxCELoss() - animator = d2l.Animator(xlabel='epoch', ylabel='loss', - xlim=[10, num_epochs]) - for epoch in range(num_epochs): - timer = d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens - for batch in data_iter: - X, X_valid_len, Y, Y_valid_len = [ - x.as_in_ctx(device) for x in batch] - bos = np.array([tgt_vocab['']] * Y.shape[0], - ctx=device).reshape(-1, 1) - dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing - with autograd.record(): - Y_hat, _ = net(X, dec_input, X_valid_len) - l = loss(Y_hat, Y, Y_valid_len) - l.backward() - d2l.grad_clipping(net, 1) - num_tokens = Y_valid_len.sum() - trainer.step(num_tokens) - metric.add(l.sum(), num_tokens) - if (epoch + 1) % 10 == 0: - animator.add(epoch + 1, (metric[0] / metric[1],)) - print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' - f'tokens/sec on {str(device)}') - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, - device, save_attention_weights=False): - """Predict for sequence to sequence.""" - src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ - src_vocab['']] - enc_valid_len = np.array([len(src_tokens)], ctx=device) - src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # Add the batch axis - enc_X = np.expand_dims(np.array(src_tokens, ctx=device), axis=0) - enc_outputs = net.encoder(enc_X, enc_valid_len) - dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # Add the batch axis - dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), axis=0) - output_seq, attention_weight_seq = [], [] - for _ in range(num_steps): - Y, dec_state = net.decoder(dec_X, dec_state) - # We use the token with the highest prediction likelihood as the input - # of the decoder at the next time step - dec_X = Y.argmax(axis=2) - pred = dec_X.squeeze(axis=0).astype('int32').item() - # Save attention weights (to be covered later) - if save_attention_weights: - attention_weight_seq.append(net.decoder.attention_weights) - # Once the end-of-sequence token is predicted, the generation of the - # output sequence is complete - if pred == tgt_vocab['']: - break - output_seq.append(pred) - return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def bleu(pred_seq, label_seq, k): - """Compute the BLEU.""" - pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') - len_pred, len_label = len(pred_tokens), len(label_tokens) - score = math.exp(min(0, 1 - len_label / len_pred)) - for n in range(1, k + 1): - num_matches, label_subs = 0, collections.defaultdict(int) - for i in range(len_label - n + 1): - label_subs[''.join(label_tokens[i:i + n])] += 1 - for i in range(len_pred - n + 1): - if label_subs[''.join(pred_tokens[i:i + n])] > 0: - num_matches += 1 - label_subs[''.join(pred_tokens[i:i + n])] -= 1 - score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) - return score - - -# Defined in file: ./chapter_attention-mechanisms/attention-cues.md -def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), - cmap='Reds'): - d2l.use_svg_display() - num_rows, num_cols = matrices.shape[0], matrices.shape[1] - fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, - sharex=True, sharey=True, squeeze=False) - for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): - for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): - pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) - if i == num_rows - 1: - ax.set_xlabel(xlabel) - if j == 0: - ax.set_ylabel(ylabel) - if titles: - ax.set_title(titles[j]) - fig.colorbar(pcm, ax=axes, shrink=0.6) - - -# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md -def masked_softmax(X, valid_lens): - """Perform softmax operation by masking elements on the last axis.""" - # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor - if valid_lens is None: - return npx.softmax(X) - else: - shape = X.shape - if valid_lens.ndim == 1: - valid_lens = valid_lens.repeat(shape[1]) - else: - valid_lens = valid_lens.reshape(-1) - # On the last axis, replace masked elements with a very large negative - # value, whose exponentiation outputs 0 - X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, True, - value=-1e6, axis=1) - return npx.softmax(X).reshape(shape) - - -# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md -class AdditiveAttention(nn.Block): - """Additive attention.""" - def __init__(self, num_hiddens, dropout, **kwargs): - super(AdditiveAttention, self).__init__(**kwargs) - # Use `flatten=False` to only transform the last axis so that the - # shapes for the other axes are kept the same - self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False) - self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False) - self.w_v = nn.Dense(1, use_bias=False, flatten=False) - self.dropout = nn.Dropout(dropout) - - def forward(self, queries, keys, values, valid_lens): - queries, keys = self.W_q(queries), self.W_k(keys) - # After dimension expansion, shape of `queries`: (`batch_size`, no. of - # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, - # no. of key-value pairs, `num_hiddens`). Sum them up with - # broadcasting - features = np.expand_dims(queries, axis=2) + np.expand_dims( - keys, axis=1) - features = np.tanh(features) - # There is only one output of `self.w_v`, so we remove the last - # one-dimensional entry from the shape. Shape of `scores`: - # (`batch_size`, no. of queries, no. of key-value pairs) - scores = np.squeeze(self.w_v(features), axis=-1) - self.attention_weights = masked_softmax(scores, valid_lens) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) - return npx.batch_dot(self.dropout(self.attention_weights), values) - - -# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md -class DotProductAttention(nn.Block): - """Scaled dot product attention.""" - def __init__(self, dropout, **kwargs): - super(DotProductAttention, self).__init__(**kwargs) - self.dropout = nn.Dropout(dropout) - - # Shape of `queries`: (`batch_size`, no. of queries, `d`) - # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) - # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) - def forward(self, queries, keys, values, valid_lens=None): - d = queries.shape[-1] - # Set `transpose_b=True` to swap the last two dimensions of `keys` - scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d) - self.attention_weights = masked_softmax(scores, valid_lens) - return npx.batch_dot(self.dropout(self.attention_weights), values) - - -# Defined in file: ./chapter_attention-mechanisms/bahdanau-attention.md -class AttentionDecoder(d2l.Decoder): - """The base attention-based decoder interface.""" - def __init__(self, **kwargs): - super(AttentionDecoder, self).__init__(**kwargs) - - @property - def attention_weights(self): - raise NotImplementedError - - -# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md -class MultiHeadAttention(nn.Block): - def __init__(self, num_hiddens, num_heads, dropout, use_bias=False, - **kwargs): - super(MultiHeadAttention, self).__init__(**kwargs) - self.num_heads = num_heads - self.attention = d2l.DotProductAttention(dropout) - self.W_q = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) - self.W_k = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) - self.W_v = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) - self.W_o = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) - - def forward(self, queries, keys, values, valid_lens): - # Shape of `queries`, `keys`, or `values`: - # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) - # Shape of `valid_lens`: - # (`batch_size`,) or (`batch_size`, no. of queries) - # After transposing, shape of output `queries`, `keys`, or `values`: - # (`batch_size` * `num_heads`, no. of queries or key-value pairs, - # `num_hiddens` / `num_heads`) - queries = transpose_qkv(self.W_q(queries), self.num_heads) - keys = transpose_qkv(self.W_k(keys), self.num_heads) - values = transpose_qkv(self.W_v(values), self.num_heads) - - if valid_lens is not None: - # On axis 0, copy the first item (scalar or vector) for - # `num_heads` times, then copy the next item, and so on - valid_lens = valid_lens.repeat(self.num_heads, axis=0) - - # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, - # `num_hiddens` / `num_heads`) - output = self.attention(queries, keys, values, valid_lens) - - # Shape of `output_concat`: - # (`batch_size`, no. of queries, `num_hiddens`) - output_concat = transpose_output(output, self.num_heads) - return self.W_o(output_concat) - - -# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md -def transpose_qkv(X, num_heads): - # Shape of input `X`: - # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). - # Shape of output `X`: - # (`batch_size`, no. of queries or key-value pairs, `num_heads`, - # `num_hiddens` / `num_heads`) - X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) - - # Shape of output `X`: - # (`batch_size`, `num_heads`, no. of queries or key-value pairs, - # `num_hiddens` / `num_heads`) - X = X.transpose(0, 2, 1, 3) - - # Shape of `output`: - # (`batch_size` * `num_heads`, no. of queries or key-value pairs, - # `num_hiddens` / `num_heads`) - return X.reshape(-1, X.shape[2], X.shape[3]) - -def transpose_output(X, num_heads): - """Reverse the operation of `transpose_qkv`""" - X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) - X = X.transpose(0, 2, 1, 3) - return X.reshape(X.shape[0], X.shape[1], -1) - - -# Defined in file: ./chapter_attention-mechanisms/self-attention-and-positional-encoding.md -class PositionalEncoding(nn.Block): - def __init__(self, num_hiddens, dropout, max_len=1000): - super(PositionalEncoding, self).__init__() - self.dropout = nn.Dropout(dropout) - # Create a long enough `P` - self.P = d2l.zeros((1, max_len, num_hiddens)) - X = d2l.arange(max_len).reshape(-1, 1) / np.power( - 10000, - np.arange(0, num_hiddens, 2) / num_hiddens) - self.P[:, :, 0::2] = np.sin(X) - self.P[:, :, 1::2] = np.cos(X) - - def forward(self, X): - X = X + self.P[:, :X.shape[1], :].as_in_ctx(X.ctx) - return self.dropout(X) - +# Defined in file: ./chapter_computational-performance/hybridize.md +class Benchmark: + def __init__(self, description='Done'): + self.description = description -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class PositionWiseFFN(nn.Block): - def __init__(self, ffn_num_hiddens, ffn_num_outputs, **kwargs): - super(PositionWiseFFN, self).__init__(**kwargs) - self.dense1 = nn.Dense(ffn_num_hiddens, flatten=False, - activation='relu') - self.dense2 = nn.Dense(ffn_num_outputs, flatten=False) + def __enter__(self): + self.timer = d2l.Timer() + return self - def forward(self, X): - return self.dense2(self.dense1(X)) - - -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class AddNorm(nn.Block): - def __init__(self, dropout, **kwargs): - super(AddNorm, self).__init__(**kwargs) - self.dropout = nn.Dropout(dropout) - self.ln = nn.LayerNorm() - - def forward(self, X, Y): - return self.ln(self.dropout(Y) + X) - - -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class EncoderBlock(nn.Block): - def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, - use_bias=False, **kwargs): - super(EncoderBlock, self).__init__(**kwargs) - self.attention = d2l.MultiHeadAttention(num_hiddens, num_heads, - dropout, use_bias) - self.addnorm1 = AddNorm(dropout) - self.ffn = PositionWiseFFN(ffn_num_hiddens, num_hiddens) - self.addnorm2 = AddNorm(dropout) - - def forward(self, X, valid_lens): - Y = self.addnorm1(X, self.attention(X, X, X, valid_lens)) - return self.addnorm2(Y, self.ffn(Y)) - - -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class TransformerEncoder(d2l.Encoder): - def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, - num_layers, dropout, use_bias=False, **kwargs): - super(TransformerEncoder, self).__init__(**kwargs) - self.num_hiddens = num_hiddens - self.embedding = nn.Embedding(vocab_size, num_hiddens) - self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) - self.blks = nn.Sequential() - for _ in range(num_layers): - self.blks.add( - EncoderBlock(num_hiddens, ffn_num_hiddens, num_heads, dropout, - use_bias)) - - def forward(self, X, valid_lens, *args): - # Since positional encoding values are between -1 and 1, the embedding - # values are multiplied by the square root of the embedding dimension - # to rescale before they are summed up - X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) - self.attention_weights = [None] * len(self.blks) - for i, blk in enumerate(self.blks): - X = blk(X, valid_lens) - self.attention_weights[ - i] = blk.attention.attention.attention_weights - return X + def __exit__(self, *args): + print(f'{self.description}: {self.timer.stop():.4f} sec') # Alias defined in config.ini diff --git a/d2l/tensorflow.py b/d2l/tensorflow.py index 822fc2932..aa1d597c6 100644 --- a/d2l/tensorflow.py +++ b/d2l/tensorflow.py @@ -15,7 +15,6 @@ import time import zipfile from collections import defaultdict - import pandas as pd import requests from IPython import display @@ -67,7 +66,7 @@ def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # Return True if `X` (tensor or list) has 1 axis + # 如果 `X` 有一个轴,输出True def has_one_axis(X): return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and not hasattr(X[0], "__len__")) @@ -467,8 +466,7 @@ def on_epoch_end(self, epoch, logs): print(f'{num_examples / self.timer.avg():.1f} examples/sec on ' f'{str(self.device_name)}') -def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, - device=d2l.try_gpu()): +def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): """Train a model with a GPU (defined in Chapter 6).""" device_name = device._device_name strategy = tf.distribute.OneDeviceStrategy(device_name) @@ -520,28 +518,28 @@ def read_time_machine(): # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def tokenize(lines, token='word'): - """Split text lines into word or character tokens.""" + """将文本行拆分为单词或字符标记。""" if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: - print('ERROR: unknown token type: ' + token) + print('错误:未知令牌类型:' + token) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md class Vocab: - """Vocabulary for text.""" + """文本词表""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: reserved_tokens = [] - # Sort according to frequencies + # 按出现频率排序 counter = count_corpus(tokens) - self.token_freqs = sorted(counter.items(), key=lambda x: x[0]) - self.token_freqs.sort(key=lambda x: x[1], reverse=True) - # The index for the unknown token is 0 + self.token_freqs = sorted(counter.items(), key=lambda x: x[1], + reverse=True) + # 未知标记的索引为0 self.unk, uniq_tokens = 0, [''] + reserved_tokens uniq_tokens += [ token for token, freq in self.token_freqs @@ -566,21 +564,21 @@ def to_tokens(self, indices): def count_corpus(tokens): """Count token frequencies.""" - # Here `tokens` is a 1D list or 2D list + # 这里的 `tokens` 是1D列表或2D列表 if len(tokens) == 0 or isinstance(tokens[0], list): - # Flatten a list of token lists into a list of tokens + # 将令牌列表展平 tokens = [token for line in tokens for token in line] return collections.Counter(tokens) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def load_corpus_time_machine(max_tokens=-1): - """Return token indices and the vocabulary of the time machine dataset.""" + """返回时光机器数据集的令牌索引和词汇表。""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # Since each text line in the time machine dataset is not necessarily a - # sentence or a paragraph, flatten all the text lines into a single list + # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, + # 所以将所有文本行展平到一个列表中 corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] @@ -589,26 +587,23 @@ def load_corpus_time_machine(max_tokens=-1): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_random(corpus, batch_size, num_steps): - """Generate a minibatch of subsequences using random sampling.""" - # Start with a random offset to partition a sequence - corpus = corpus[random.randint(0, num_steps):] - # Subtract 1 since we need to account for labels + """使用随机抽样生成一小批子序列。""" + # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 + corpus = corpus[random.randint(0, num_steps - 1):] + # 减去1,因为我们需要考虑标签 num_subseqs = (len(corpus) - 1) // num_steps - # The starting indices for subsequences of length `num_steps` + # 长度为`num_steps`的子序列的起始索引 initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # In random sampling, the subsequences from two adjacent random - # minibatches during iteration are not necessarily adjacent on the - # original sequence + # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 random.shuffle(initial_indices) def data(pos): - # Return a sequence of length `num_steps` starting from `pos` + # 返回从`pos`开始的长度为`num_steps`的序列 return corpus[pos:pos + num_steps] - num_subseqs_per_example = num_subseqs // batch_size - for i in range(0, batch_size * num_subseqs_per_example, batch_size): - # Here, `initial_indices` contains randomized starting indices for - # subsequences + num_batches = num_subseqs // batch_size + for i in range(0, batch_size * num_batches, batch_size): + # 这里,`initial_indices`包含子序列的随机起始索引 initial_indices_per_batch = initial_indices[i:i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] @@ -617,8 +612,8 @@ def data(pos): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_sequential(corpus, batch_size, num_steps): - """Generate a minibatch of subsequences using sequential partitioning.""" - # Start with a random offset to partition a sequence + """使用顺序分区生成一小批子序列。""" + # 从随机偏移量开始划分序列 offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size Xs = d2l.tensor(corpus[offset:offset + num_tokens]) @@ -634,7 +629,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md class SeqDataLoader: - """An iterator to load sequence data.""" + """加载序列数据的迭代器。""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): if use_random_iter: self.data_iter_fn = d2l.seq_data_iter_random @@ -650,7 +645,7 @@ def __iter__(self): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): - """Return the iterator and the vocabulary of the time machine dataset.""" + """返回时光机器数据集的迭代器和词表。""" data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab @@ -658,7 +653,7 @@ def load_data_time_machine(batch_size, num_steps, use_random_iter=False, # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md class RNNModelScratch: - """A RNN Model implemented from scratch.""" + """从零开始实现的循环神经网络模型""" def __init__(self, vocab_size, num_hiddens, init_state, forward_fn): self.vocab_size, self.num_hiddens = vocab_size, num_hiddens self.init_state, self.forward_fn = init_state, forward_fn @@ -673,23 +668,23 @@ def begin_state(self, batch_size): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def predict_ch8(prefix, num_preds, model, vocab, params): - """Generate new characters following the `prefix`.""" - state = model.begin_state(batch_size=1) +def predict_ch8(prefix, num_preds, net, vocab, params): + """在`prefix`后面生成新字符。""" + state = net.begin_state(batch_size=1) outputs = [vocab[prefix[0]]] get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]]), (1, 1)).numpy() - for y in prefix[1:]: # Warm-up period - _, state = model(get_input(), state, params) + for y in prefix[1:]: # 预热期 + _, state = net(get_input(), state, params) outputs.append(vocab[y]) - for _ in range(num_preds): # Predict `num_preds` steps - y, state = model(get_input(), state, params) + for _ in range(num_preds): # 预测`num_preds`步 + y, state = net(get_input(), state, params) outputs.append(int(y.numpy().argmax(axis=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def grad_clipping(grads, theta): - """Clip the gradient.""" + """裁剪梯度。""" theta = tf.constant(theta, dtype=tf.float32) norm = tf.math.sqrt( sum((tf.reduce_sum(grad**2)).numpy() for grad in grads)) @@ -705,26 +700,24 @@ def grad_clipping(grads, theta): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def train_epoch_ch8(model, train_iter, loss, updater, params, - use_random_iter): - """Train a model within one epoch (defined in Chapter 8).""" +def train_epoch_ch8(net, train_iter, loss, updater, params, use_random_iter): + """训练模型一个迭代周期(定义见第8章)。""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + metric = d2l.Accumulator(2) # 训练损失之和, 标记数量 for X, Y in train_iter: if state is None or use_random_iter: - # Initialize `state` when either it is the first iteration or - # using random sampling - state = model.begin_state(batch_size=X.shape[0]) + # 在第一次迭代或使用随机抽样时初始化`state` + state = net.begin_state(batch_size=X.shape[0]) with tf.GradientTape(persistent=True) as g: g.watch(params) - y_hat, state = model(X, state, params) - y = d2l.reshape(Y, (-1)) + y_hat, state = net(X, state, params) + y = d2l.reshape(tf.transpose(Y), (-1)) l = loss(y, y_hat) grads = g.gradient(l, params) grads = grad_clipping(grads, 1) updater.apply_gradients(zip(grads, params)) - # Keras loss by default returns the average loss in a batch + # Keras默认返回一个批量中的平均损失 # l_sum = l * float(d2l.size(y)) if isinstance( # loss, tf.keras.losses.Loss) else tf.reduce_sum(l) metric.add(l * d2l.size(y), d2l.size(y)) @@ -732,123 +725,40 @@ def train_epoch_ch8(model, train_iter, loss, updater, params, # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def train_ch8(model, train_iter, vocab, num_hiddens, lr, num_epochs, strategy, +def train_ch8(net, train_iter, vocab, num_hiddens, lr, num_epochs, strategy, use_random_iter=False): - """Train a model (defined in Chapter 8).""" + """训练模型(定义见第8章)。""" with strategy.scope(): params = get_params(len(vocab), num_hiddens) loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) updater = tf.keras.optimizers.SGD(lr) animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - predict = lambda prefix: predict_ch8(prefix, 50, model, vocab, params) - # Train and predict + predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, params) + # 训练和预测 for epoch in range(num_epochs): - ppl, speed = train_epoch_ch8(model, train_iter, loss, updater, params, + ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, params, use_random_iter) if (epoch + 1) % 10 == 0: print(predict('time traveller')) animator.add(epoch + 1, [ppl]) device = d2l.try_gpu()._device_name - print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') + print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}') print(predict('time traveller')) print(predict('traveller')) -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', - '94646ad1522d915e7b0f9296181140edcf86a4f5') - -def read_data_nmt(): - """Load the English-French dataset.""" - data_dir = d2l.download_extract('fra-eng') - with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: - return f.read() - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def preprocess_nmt(text): - """Preprocess the English-French dataset.""" - def no_space(char, prev_char): - return char in set(',.!?') and prev_char != ' ' - - # Replace non-breaking space with space, and convert uppercase letters to - # lowercase ones - text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # Insert space between words and punctuation marks - out = [ - ' ' + char if i > 0 and no_space(char, text[i - 1]) else char - for i, char in enumerate(text)] - return ''.join(out) - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def tokenize_nmt(text, num_examples=None): - """Tokenize the English-French dataset.""" - source, target = [], [] - for i, line in enumerate(text.split('\n')): - if num_examples and i > num_examples: - break - parts = line.split('\t') - if len(parts) == 2: - source.append(parts[0].split(' ')) - target.append(parts[1].split(' ')) - return source, target - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def truncate_pad(line, num_steps, padding_token): - """Truncate or pad sequences.""" - if len(line) > num_steps: - return line[:num_steps] # Truncate - return line + [padding_token] * (num_steps - len(line)) # Pad - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def build_array_nmt(lines, vocab, num_steps): - """Transform text sequences of machine translation into minibatches.""" - lines = [vocab[l] for l in lines] - lines = [l + [vocab['']] for l in lines] - array = d2l.tensor([ - truncate_pad(l, num_steps, vocab['']) for l in lines]) - valid_len = d2l.reduce_sum(d2l.astype(array != vocab[''], d2l.int32), - 1) - return array, valid_len - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def load_data_nmt(batch_size, num_steps, num_examples=600): - """Return the iterator and the vocabularies of the translation dataset.""" - text = preprocess_nmt(read_data_nmt()) - source, target = tokenize_nmt(text, num_examples) - src_vocab = d2l.Vocab(source, min_freq=2, - reserved_tokens=['', '', '']) - tgt_vocab = d2l.Vocab(target, min_freq=2, - reserved_tokens=['', '', '']) - src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) - tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) - data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) - data_iter = d2l.load_array(data_arrays, batch_size) - return data_iter, src_vocab, tgt_vocab - - -# Defined in file: ./chapter_attention-mechanisms/attention-cues.md -def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), - cmap='Reds'): - d2l.use_svg_display() - num_rows, num_cols = matrices.shape[0], matrices.shape[1] - fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, - sharex=True, sharey=True, squeeze=False) - for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): - for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): - pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) - if i == num_rows - 1: - ax.set_xlabel(xlabel) - if j == 0: - ax.set_ylabel(ylabel) - if titles: - ax.set_title(titles[j]) - fig.colorbar(pcm, ax=axes, shrink=0.6) +# Defined in file: ./chapter_computational-performance/hybridize.md +class Benchmark: + def __init__(self, description='Done'): + self.description = description + + def __enter__(self): + self.timer = d2l.Timer() + return self + + def __exit__(self, *args): + print(f'{self.description}: {self.timer.stop():.4f} sec') # Alias defined in config.ini diff --git a/d2l/torch.py b/d2l/torch.py index 438415571..ee3705fba 100644 --- a/d2l/torch.py +++ b/d2l/torch.py @@ -15,7 +15,6 @@ import time import zipfile from collections import defaultdict - import pandas as pd import requests from IPython import display @@ -28,6 +27,7 @@ import numpy as np import torch import torchvision +from PIL import Image from torch import nn from torch.nn import functional as F from torch.utils import data @@ -72,7 +72,7 @@ def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # Return True if `X` (tensor or list) has 1 axis + # 如果 `X` 有一个轴,输出True def has_one_axis(X): return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) and not hasattr(X[0], "__len__")) @@ -273,7 +273,7 @@ def train_epoch_ch3(net, train_iter, loss, updater): float(l) * len(y), accuracy(y_hat, y), y.size().numel()) else: - # 使用PyTorch内置的优化器和损失函数 + # 使用定制的优化器和损失函数 l.sum().backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) @@ -548,28 +548,28 @@ def read_time_machine(): # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def tokenize(lines, token='word'): - """Split text lines into word or character tokens.""" + """将文本行拆分为单词或字符标记。""" if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: - print('ERROR: unknown token type: ' + token) + print('错误:未知令牌类型:' + token) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md class Vocab: - """Vocabulary for text.""" + """文本词表""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: reserved_tokens = [] - # Sort according to frequencies + # 按出现频率排序 counter = count_corpus(tokens) - self.token_freqs = sorted(counter.items(), key=lambda x: x[0]) - self.token_freqs.sort(key=lambda x: x[1], reverse=True) - # The index for the unknown token is 0 + self.token_freqs = sorted(counter.items(), key=lambda x: x[1], + reverse=True) + # 未知标记的索引为0 self.unk, uniq_tokens = 0, [''] + reserved_tokens uniq_tokens += [ token for token, freq in self.token_freqs @@ -594,21 +594,21 @@ def to_tokens(self, indices): def count_corpus(tokens): """Count token frequencies.""" - # Here `tokens` is a 1D list or 2D list + # 这里的 `tokens` 是1D列表或2D列表 if len(tokens) == 0 or isinstance(tokens[0], list): - # Flatten a list of token lists into a list of tokens + # 将令牌列表展平 tokens = [token for line in tokens for token in line] return collections.Counter(tokens) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def load_corpus_time_machine(max_tokens=-1): - """Return token indices and the vocabulary of the time machine dataset.""" + """返回时光机器数据集的令牌索引和词汇表。""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # Since each text line in the time machine dataset is not necessarily a - # sentence or a paragraph, flatten all the text lines into a single list + # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, + # 所以将所有文本行展平到一个列表中 corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] @@ -617,26 +617,23 @@ def load_corpus_time_machine(max_tokens=-1): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_random(corpus, batch_size, num_steps): - """Generate a minibatch of subsequences using random sampling.""" - # Start with a random offset to partition a sequence - corpus = corpus[random.randint(0, num_steps):] - # Subtract 1 since we need to account for labels + """使用随机抽样生成一小批子序列。""" + # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 + corpus = corpus[random.randint(0, num_steps - 1):] + # 减去1,因为我们需要考虑标签 num_subseqs = (len(corpus) - 1) // num_steps - # The starting indices for subsequences of length `num_steps` + # 长度为`num_steps`的子序列的起始索引 initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # In random sampling, the subsequences from two adjacent random - # minibatches during iteration are not necessarily adjacent on the - # original sequence + # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 random.shuffle(initial_indices) def data(pos): - # Return a sequence of length `num_steps` starting from `pos` + # 返回从`pos`开始的长度为`num_steps`的序列 return corpus[pos:pos + num_steps] - num_subseqs_per_example = num_subseqs // batch_size - for i in range(0, batch_size * num_subseqs_per_example, batch_size): - # Here, `initial_indices` contains randomized starting indices for - # subsequences + num_batches = num_subseqs // batch_size + for i in range(0, batch_size * num_batches, batch_size): + # 这里,`initial_indices`包含子序列的随机起始索引 initial_indices_per_batch = initial_indices[i:i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] @@ -645,15 +642,15 @@ def data(pos): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_sequential(corpus, batch_size, num_steps): - """Generate a minibatch of subsequences using sequential partitioning.""" - # Start with a random offset to partition a sequence + """使用顺序分区生成一小批子序列。""" + # 从随机偏移量开始划分序列 offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size Xs = d2l.tensor(corpus[offset:offset + num_tokens]) Ys = d2l.tensor(corpus[offset + 1:offset + 1 + num_tokens]) Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1) num_batches = Xs.shape[1] // num_steps - for i in range(0, num_batches * num_steps, num_steps): + for i in range(0, num_steps * num_batches, num_steps): X = Xs[:, i:i + num_steps] Y = Ys[:, i:i + num_steps] yield X, Y @@ -661,7 +658,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md class SeqDataLoader: - """An iterator to load sequence data.""" + """加载序列数据的迭代器。""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): if use_random_iter: self.data_iter_fn = d2l.seq_data_iter_random @@ -677,7 +674,7 @@ def __iter__(self): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000): - """Return the iterator and the vocabulary of the time machine dataset.""" + """返回时光机器数据集的迭代器和词表。""" data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab @@ -685,7 +682,7 @@ def load_data_time_machine(batch_size, num_steps, use_random_iter=False, # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md class RNNModelScratch: - """A RNN Model implemented from scratch.""" + """从零开始实现的循环神经网络模型""" def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn): self.vocab_size, self.num_hiddens = vocab_size, num_hiddens @@ -701,28 +698,28 @@ def begin_state(self, batch_size, device): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def predict_ch8(prefix, num_preds, model, vocab, device): - """Generate new characters following the `prefix`.""" - state = model.begin_state(batch_size=1, device=device) +def predict_ch8(prefix, num_preds, net, vocab, device): + """在`prefix`后面生成新字符。""" + state = net.begin_state(batch_size=1, device=device) outputs = [vocab[prefix[0]]] get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]], device=device), (1, 1)) - for y in prefix[1:]: # Warm-up period - _, state = model(get_input(), state) + for y in prefix[1:]: # 预热期 + _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # Predict `num_preds` steps - y, state = model(get_input(), state) + for _ in range(num_preds): # 预测`num_preds`步 + y, state = net(get_input(), state) outputs.append(int(y.argmax(dim=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def grad_clipping(model, theta): - """Clip the gradient.""" - if isinstance(model, nn.Module): - params = [p for p in model.parameters() if p.requires_grad] +def grad_clipping(net, theta): + """裁剪梯度。""" + if isinstance(net, nn.Module): + params = [p for p in net.parameters() if p.requires_grad] else: - params = model.params + params = net.params norm = torch.sqrt(sum(torch.sum((p.grad**2)) for p in params)) if norm > theta: for param in params: @@ -730,78 +727,74 @@ def grad_clipping(model, theta): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def train_epoch_ch8(model, train_iter, loss, updater, device, - use_random_iter): - """Train a model within one epoch (defined in Chapter 8).""" +def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): + """训练模型一个迭代周期(定义见第8章)。""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + metric = d2l.Accumulator(2) # 训练损失之和, 标记数量 for X, Y in train_iter: if state is None or use_random_iter: - # Initialize `state` when either it is the first iteration or - # using random sampling - state = model.begin_state(batch_size=X.shape[0], device=device) + # 在第一次迭代或使用随机抽样时初始化`state` + state = net.begin_state(batch_size=X.shape[0], device=device) else: - if isinstance(model, nn.Module) and not isinstance(state, tuple): - # `state` is a tensor for `nn.GRU` + if isinstance(net, nn.Module) and not isinstance(state, tuple): + # `state`对于`nn.GRU`是个张量 state.detach_() else: - # `state` is a tuple of tensors for `nn.LSTM` and - # for our custom scratch implementation + # `state`对于`nn.LSTM`或对于我们从零开始实现的模型是个张量 for s in state: s.detach_() y = Y.T.reshape(-1) X, y = X.to(device), y.to(device) - y_hat, state = model(X, state) + y_hat, state = net(X, state) l = loss(y_hat, y.long()).mean() if isinstance(updater, torch.optim.Optimizer): updater.zero_grad() l.backward() - grad_clipping(model, 1) + grad_clipping(net, 1) updater.step() else: l.backward() - grad_clipping(model, 1) - # Since the `mean` function has been invoked + grad_clipping(net, 1) + # 因为已经调用了`mean`函数 updater(batch_size=1) metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md -def train_ch8(model, train_iter, vocab, lr, num_epochs, device, +def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): - """Train a model (defined in Chapter 8).""" + """训练模型(定义见第8章)。""" loss = nn.CrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - # Initialize - if isinstance(model, nn.Module): - updater = torch.optim.SGD(model.parameters(), lr) + # 初始化 + if isinstance(net, nn.Module): + updater = torch.optim.SGD(net.parameters(), lr) else: - updater = lambda batch_size: d2l.sgd(model.params, lr, batch_size) - predict = lambda prefix: predict_ch8(prefix, 50, model, vocab, device) - # Train and predict + updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) + predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) + # 训练和预测 for epoch in range(num_epochs): - ppl, speed = train_epoch_ch8(model, train_iter, loss, updater, device, + ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter) if (epoch + 1) % 10 == 0: print(predict('time traveller')) animator.add(epoch + 1, [ppl]) - print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') + print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}') print(predict('time traveller')) print(predict('traveller')) # Defined in file: ./chapter_recurrent-neural-networks/rnn-concise.md class RNNModel(nn.Module): - """The RNN model.""" + """循环神经网络模型。""" def __init__(self, rnn_layer, vocab_size, **kwargs): super(RNNModel, self).__init__(**kwargs) self.rnn = rnn_layer self.vocab_size = vocab_size self.num_hiddens = self.rnn.hidden_size - # If the RNN is bidirectional (to be introduced later), - # `num_directions` should be 2, else it should be 1. + # 如果RNN是双向的(之后将介绍),`num_directions`应该是2,否则应该是1。 if not self.rnn.bidirectional: self.num_directions = 1 self.linear = nn.Linear(self.num_hiddens, self.vocab_size) @@ -813,19 +806,18 @@ def forward(self, inputs, state): X = F.one_hot(inputs.T.long(), self.vocab_size) X = X.to(torch.float32) Y, state = self.rnn(X, state) - # The fully connected layer will first change the shape of `Y` to - # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is - # (`num_steps` * `batch_size`, `vocab_size`). + # 全连接层首先将`Y`的形状改为(`时间步数` * `批量大小`, `隐藏单元数`)。 + # 它的输出形状是 (`时间步数` * `批量大小`, `词表大小`)。 output = self.linear(Y.reshape((-1, Y.shape[-1]))) return output, state def begin_state(self, device, batch_size=1): if not isinstance(self.rnn, nn.LSTM): - # `nn.GRU` takes a tensor as hidden state + # `nn.GRU` 以张量作为隐藏状态 return torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device) else: - # `nn.LSTM` takes a tuple of hidden states + # `nn.LSTM` 以张量作为隐藏状态 return (torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device), @@ -834,519 +826,17 @@ def begin_state(self, device, batch_size=1): device=device)) -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', - '94646ad1522d915e7b0f9296181140edcf86a4f5') - -def read_data_nmt(): - """Load the English-French dataset.""" - data_dir = d2l.download_extract('fra-eng') - with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: - return f.read() - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def preprocess_nmt(text): - """Preprocess the English-French dataset.""" - def no_space(char, prev_char): - return char in set(',.!?') and prev_char != ' ' - - # Replace non-breaking space with space, and convert uppercase letters to - # lowercase ones - text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # Insert space between words and punctuation marks - out = [ - ' ' + char if i > 0 and no_space(char, text[i - 1]) else char - for i, char in enumerate(text)] - return ''.join(out) - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def tokenize_nmt(text, num_examples=None): - """Tokenize the English-French dataset.""" - source, target = [], [] - for i, line in enumerate(text.split('\n')): - if num_examples and i > num_examples: - break - parts = line.split('\t') - if len(parts) == 2: - source.append(parts[0].split(' ')) - target.append(parts[1].split(' ')) - return source, target - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def truncate_pad(line, num_steps, padding_token): - """Truncate or pad sequences.""" - if len(line) > num_steps: - return line[:num_steps] # Truncate - return line + [padding_token] * (num_steps - len(line)) # Pad - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def build_array_nmt(lines, vocab, num_steps): - """Transform text sequences of machine translation into minibatches.""" - lines = [vocab[l] for l in lines] - lines = [l + [vocab['']] for l in lines] - array = d2l.tensor([ - truncate_pad(l, num_steps, vocab['']) for l in lines]) - valid_len = d2l.reduce_sum(d2l.astype(array != vocab[''], d2l.int32), - 1) - return array, valid_len - - -# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md -def load_data_nmt(batch_size, num_steps, num_examples=600): - """Return the iterator and the vocabularies of the translation dataset.""" - text = preprocess_nmt(read_data_nmt()) - source, target = tokenize_nmt(text, num_examples) - src_vocab = d2l.Vocab(source, min_freq=2, - reserved_tokens=['', '', '']) - tgt_vocab = d2l.Vocab(target, min_freq=2, - reserved_tokens=['', '', '']) - src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) - tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) - data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) - data_iter = d2l.load_array(data_arrays, batch_size) - return data_iter, src_vocab, tgt_vocab - - -# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md -class Encoder(nn.Module): - """The base encoder interface for the encoder-decoder architecture.""" - def __init__(self, **kwargs): - super(Encoder, self).__init__(**kwargs) - - def forward(self, X, *args): - raise NotImplementedError - - -# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md -class Decoder(nn.Module): - """The base decoder interface for the encoder-decoder architecture.""" - def __init__(self, **kwargs): - super(Decoder, self).__init__(**kwargs) - - def init_state(self, enc_outputs, *args): - raise NotImplementedError - - def forward(self, X, state): - raise NotImplementedError - - -# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md -class EncoderDecoder(nn.Module): - """The base class for the encoder-decoder architecture.""" - def __init__(self, encoder, decoder, **kwargs): - super(EncoderDecoder, self).__init__(**kwargs) - self.encoder = encoder - self.decoder = decoder - - def forward(self, enc_X, dec_X, *args): - enc_outputs = self.encoder(enc_X, *args) - dec_state = self.decoder.init_state(enc_outputs, *args) - return self.decoder(dec_X, dec_state) - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -class Seq2SeqEncoder(d2l.Encoder): - """The RNN encoder for sequence to sequence learning.""" - def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, - dropout=0, **kwargs): - super(Seq2SeqEncoder, self).__init__(**kwargs) - # Embedding layer - self.embedding = nn.Embedding(vocab_size, embed_size) - self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, - dropout=dropout) - - def forward(self, X, *args): - # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) - X = self.embedding(X) - # In RNN models, the first axis corresponds to time steps - X = X.permute(1, 0, 2) - # When state is not mentioned, it defaults to zeros - output, state = self.rnn(X) - # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) - # `state` shape: (`num_layers`, `batch_size`, `num_hiddens`) - return output, state - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def sequence_mask(X, valid_len, value=0): - """Mask irrelevant entries in sequences.""" - maxlen = X.size(1) - mask = torch.arange((maxlen), dtype=torch.float32, - device=X.device)[None, :] < valid_len[:, None] - X[~mask] = value - return X - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): - """The softmax cross-entropy loss with masks.""" - - # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) - # `label` shape: (`batch_size`, `num_steps`) - # `valid_len` shape: (`batch_size`,) - def forward(self, pred, label, valid_len): - weights = torch.ones_like(label) - weights = sequence_mask(weights, valid_len) - self.reduction = 'none' - unweighted_loss = super(MaskedSoftmaxCELoss, - self).forward(pred.permute(0, 2, 1), label) - weighted_loss = (unweighted_loss * weights).mean(dim=1) - return weighted_loss - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """Train a model for sequence to sequence.""" - def xavier_init_weights(m): - if type(m) == nn.Linear: - nn.init.xavier_uniform_(m.weight) - if type(m) == nn.GRU: - for param in m._flat_weights_names: - if "weight" in param: - nn.init.xavier_uniform_(m._parameters[param]) - - net.apply(xavier_init_weights) - net.to(device) - optimizer = torch.optim.Adam(net.parameters(), lr=lr) - loss = MaskedSoftmaxCELoss() - net.train() - animator = d2l.Animator(xlabel='epoch', ylabel='loss', - xlim=[10, num_epochs]) - for epoch in range(num_epochs): - timer = d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens - for batch in data_iter: - X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch] - bos = torch.tensor([tgt_vocab['']] * Y.shape[0], - device=device).reshape(-1, 1) - dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing - Y_hat, _ = net(X, dec_input, X_valid_len) - l = loss(Y_hat, Y, Y_valid_len) - l.sum().backward() # Make the loss scalar for `backward` - d2l.grad_clipping(net, 1) - num_tokens = Y_valid_len.sum() - optimizer.step() - with torch.no_grad(): - metric.add(l.sum(), num_tokens) - if (epoch + 1) % 10 == 0: - animator.add(epoch + 1, (metric[0] / metric[1],)) - print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' - f'tokens/sec on {str(device)}') - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, - device, save_attention_weights=False): - """Predict for sequence to sequence.""" - # Set `net` to eval mode for inference - net.eval() - src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ - src_vocab['']] - enc_valid_len = torch.tensor([len(src_tokens)], device=device) - src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # Add the batch axis - enc_X = torch.unsqueeze( - torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0) - enc_outputs = net.encoder(enc_X, enc_valid_len) - dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # Add the batch axis - dec_X = torch.unsqueeze( - torch.tensor([tgt_vocab['']], dtype=torch.long, device=device), - dim=0) - output_seq, attention_weight_seq = [], [] - for _ in range(num_steps): - Y, dec_state = net.decoder(dec_X, dec_state) - # We use the token with the highest prediction likelihood as the input - # of the decoder at the next time step - dec_X = Y.argmax(dim=2) - pred = dec_X.squeeze(dim=0).type(torch.int32).item() - # Save attention weights (to be covered later) - if save_attention_weights: - attention_weight_seq.append(net.decoder.attention_weights) - # Once the end-of-sequence token is predicted, the generation of the - # output sequence is complete - if pred == tgt_vocab['']: - break - output_seq.append(pred) - return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq - - -# Defined in file: ./chapter_recurrent-modern/seq2seq.md -def bleu(pred_seq, label_seq, k): - """Compute the BLEU.""" - pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') - len_pred, len_label = len(pred_tokens), len(label_tokens) - score = math.exp(min(0, 1 - len_label / len_pred)) - for n in range(1, k + 1): - num_matches, label_subs = 0, collections.defaultdict(int) - for i in range(len_label - n + 1): - label_subs[''.join(label_tokens[i:i + n])] += 1 - for i in range(len_pred - n + 1): - if label_subs[''.join(pred_tokens[i:i + n])] > 0: - num_matches += 1 - label_subs[''.join(pred_tokens[i:i + n])] -= 1 - score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) - return score - - -# Defined in file: ./chapter_attention-mechanisms/attention-cues.md -def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), - cmap='Reds'): - d2l.use_svg_display() - num_rows, num_cols = matrices.shape[0], matrices.shape[1] - fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, - sharex=True, sharey=True, squeeze=False) - for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): - for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): - pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) - if i == num_rows - 1: - ax.set_xlabel(xlabel) - if j == 0: - ax.set_ylabel(ylabel) - if titles: - ax.set_title(titles[j]) - fig.colorbar(pcm, ax=axes, shrink=0.6) - - -# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md -def masked_softmax(X, valid_lens): - """Perform softmax operation by masking elements on the last axis.""" - # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor - if valid_lens is None: - return nn.functional.softmax(X, dim=-1) - else: - shape = X.shape - if valid_lens.dim() == 1: - valid_lens = torch.repeat_interleave(valid_lens, shape[1]) - else: - valid_lens = valid_lens.reshape(-1) - # On the last axis, replace masked elements with a very large negative - # value, whose exponentiation outputs 0 - X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, - value=-1e6) - return nn.functional.softmax(X.reshape(shape), dim=-1) - - -# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md -class AdditiveAttention(nn.Module): - def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): - super(AdditiveAttention, self).__init__(**kwargs) - self.W_k = nn.Linear(key_size, num_hiddens, bias=False) - self.W_q = nn.Linear(query_size, num_hiddens, bias=False) - self.w_v = nn.Linear(num_hiddens, 1, bias=False) - self.dropout = nn.Dropout(dropout) - - def forward(self, queries, keys, values, valid_lens): - queries, keys = self.W_q(queries), self.W_k(keys) - # After dimension expansion, shape of `queries`: (`batch_size`, no. of - # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, - # no. of key-value pairs, `num_hiddens`). Sum them up with - # broadcasting - features = queries.unsqueeze(2) + keys.unsqueeze(1) - features = torch.tanh(features) - # There is only one output of `self.w_v`, so we remove the last - # one-dimensional entry from the shape. Shape of `scores`: - # (`batch_size`, no. of queries, no. of key-value pairs) - scores = self.w_v(features).squeeze(-1) - self.attention_weights = masked_softmax(scores, valid_lens) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) - return torch.bmm(self.dropout(self.attention_weights), values) - - -# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md -class DotProductAttention(nn.Module): - """Scaled dot product attention.""" - def __init__(self, dropout, **kwargs): - super(DotProductAttention, self).__init__(**kwargs) - self.dropout = nn.Dropout(dropout) - - # Shape of `queries`: (`batch_size`, no. of queries, `d`) - # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) - # Shape of `values`: (`batch_size`, no. of key-value pairs, value - # dimension) - # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) - def forward(self, queries, keys, values, valid_lens=None): - d = queries.shape[-1] - # Set `transpose_b=True` to swap the last two dimensions of `keys` - scores = torch.bmm(queries, keys.transpose(1, 2)) / math.sqrt(d) - self.attention_weights = masked_softmax(scores, valid_lens) - return torch.bmm(self.dropout(self.attention_weights), values) - - -# Defined in file: ./chapter_attention-mechanisms/bahdanau-attention.md -class AttentionDecoder(d2l.Decoder): - """The base attention-based decoder interface.""" - def __init__(self, **kwargs): - super(AttentionDecoder, self).__init__(**kwargs) - - @property - def attention_weights(self): - raise NotImplementedError - - -# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md -class MultiHeadAttention(nn.Module): - def __init__(self, key_size, query_size, value_size, num_hiddens, - num_heads, dropout, bias=False, **kwargs): - super(MultiHeadAttention, self).__init__(**kwargs) - self.num_heads = num_heads - self.attention = d2l.DotProductAttention(dropout) - self.W_q = nn.Linear(query_size, num_hiddens, bias=bias) - self.W_k = nn.Linear(key_size, num_hiddens, bias=bias) - self.W_v = nn.Linear(value_size, num_hiddens, bias=bias) - self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias) - - def forward(self, queries, keys, values, valid_lens): - # Shape of `queries`, `keys`, or `values`: - # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) - # Shape of `valid_lens`: - # (`batch_size`,) or (`batch_size`, no. of queries) - # After transposing, shape of output `queries`, `keys`, or `values`: - # (`batch_size` * `num_heads`, no. of queries or key-value pairs, - # `num_hiddens` / `num_heads`) - queries = transpose_qkv(self.W_q(queries), self.num_heads) - keys = transpose_qkv(self.W_k(keys), self.num_heads) - values = transpose_qkv(self.W_v(values), self.num_heads) - - if valid_lens is not None: - # On axis 0, copy the first item (scalar or vector) for - # `num_heads` times, then copy the next item, and so on - valid_lens = torch.repeat_interleave(valid_lens, - repeats=self.num_heads, - dim=0) - - # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, - # `num_hiddens` / `num_heads`) - output = self.attention(queries, keys, values, valid_lens) - - # Shape of `output_concat`: - # (`batch_size`, no. of queries, `num_hiddens`) - output_concat = transpose_output(output, self.num_heads) - return self.W_o(output_concat) - - -# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md -def transpose_qkv(X, num_heads): - # Shape of input `X`: - # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). - # Shape of output `X`: - # (`batch_size`, no. of queries or key-value pairs, `num_heads`, - # `num_hiddens` / `num_heads`) - X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) - - # Shape of output `X`: - # (`batch_size`, `num_heads`, no. of queries or key-value pairs, - # `num_hiddens` / `num_heads`) - X = X.permute(0, 2, 1, 3) - - # Shape of `output`: - # (`batch_size` * `num_heads`, no. of queries or key-value pairs, - # `num_hiddens` / `num_heads`) - return X.reshape(-1, X.shape[2], X.shape[3]) - -def transpose_output(X, num_heads): - """Reverse the operation of `transpose_qkv`""" - X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) - X = X.permute(0, 2, 1, 3) - return X.reshape(X.shape[0], X.shape[1], -1) - - -# Defined in file: ./chapter_attention-mechanisms/self-attention-and-positional-encoding.md -class PositionalEncoding(nn.Module): - def __init__(self, num_hiddens, dropout, max_len=1000): - super(PositionalEncoding, self).__init__() - self.dropout = nn.Dropout(dropout) - # Create a long enough `P` - self.P = d2l.zeros((1, max_len, num_hiddens)) - X = d2l.arange(max_len, dtype=torch.float32).reshape( - -1, 1) / torch.pow( - 10000, - torch.arange(0, num_hiddens, 2, dtype=torch.float32) / - num_hiddens) - self.P[:, :, 0::2] = torch.sin(X) - self.P[:, :, 1::2] = torch.cos(X) - - def forward(self, X): - X = X + self.P[:, :X.shape[1], :].to(X.device) - return self.dropout(X) - +# Defined in file: ./chapter_computational-performance/hybridize.md +class Benchmark: + def __init__(self, description='Done'): + self.description = description -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class PositionWiseFFN(nn.Module): - def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs, - **kwargs): - super(PositionWiseFFN, self).__init__(**kwargs) - self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens) - self.relu = nn.ReLU() - self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs) + def __enter__(self): + self.timer = d2l.Timer() + return self - def forward(self, X): - return self.dense2(self.relu(self.dense1(X))) - - -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class AddNorm(nn.Module): - def __init__(self, normalized_shape, dropout, **kwargs): - super(AddNorm, self).__init__(**kwargs) - self.dropout = nn.Dropout(dropout) - self.ln = nn.LayerNorm(normalized_shape) - - def forward(self, X, Y): - return self.ln(self.dropout(Y) + X) - - -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class EncoderBlock(nn.Module): - def __init__(self, key_size, query_size, value_size, num_hiddens, - norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, - dropout, use_bias=False, **kwargs): - super(EncoderBlock, self).__init__(**kwargs) - self.attention = d2l.MultiHeadAttention(key_size, query_size, - value_size, num_hiddens, - num_heads, dropout, use_bias) - self.addnorm1 = AddNorm(norm_shape, dropout) - self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, - num_hiddens) - self.addnorm2 = AddNorm(norm_shape, dropout) - - def forward(self, X, valid_lens): - Y = self.addnorm1(X, self.attention(X, X, X, valid_lens)) - return self.addnorm2(Y, self.ffn(Y)) - - -# Defined in file: ./chapter_attention-mechanisms/transformer.md -class TransformerEncoder(d2l.Encoder): - def __init__(self, vocab_size, key_size, query_size, value_size, - num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, - num_heads, num_layers, dropout, use_bias=False, **kwargs): - super(TransformerEncoder, self).__init__(**kwargs) - self.num_hiddens = num_hiddens - self.embedding = nn.Embedding(vocab_size, num_hiddens) - self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) - self.blks = nn.Sequential() - for i in range(num_layers): - self.blks.add_module( - "block" + str(i), - EncoderBlock(key_size, query_size, value_size, num_hiddens, - norm_shape, ffn_num_input, ffn_num_hiddens, - num_heads, dropout, use_bias)) - - def forward(self, X, valid_lens, *args): - # Since positional encoding values are between -1 and 1, the embedding - # values are multiplied by the square root of the embedding dimension - # to rescale before they are summed up - X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) - self.attention_weights = [None] * len(self.blks) - for i, blk in enumerate(self.blks): - X = blk(X, valid_lens) - self.attention_weights[ - i] = blk.attention.attention.attention_weights - return X + def __exit__(self, *args): + print(f'{self.description}: {self.timer.stop():.4f} sec') # Alias defined in config.ini diff --git a/index.md b/index.md index 26ed2f36f..c3589ccdb 100644 --- a/index.md +++ b/index.md @@ -10,6 +10,7 @@ ```toc :maxdepth: 1 +chapter_preface/index chapter_installation/index chapter_notation/index ``` @@ -27,6 +28,7 @@ chapter_deep-learning-computation/index chapter_convolutional-neural-networks/index chapter_convolutional-modern/index chapter_recurrent-neural-networks/index +chapter_computational-performance/index ``` From 21caeb2ac1b08f77616fa9c7f7f691df015e4f75 Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Sun, 18 Apr 2021 06:58:37 +0200 Subject: [PATCH 039/103] translations issue in chapter_preliminaries/probability (#751) --- chapter_preliminaries/probability.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chapter_preliminaries/probability.md b/chapter_preliminaries/probability.md index b4b19088f..7a0cb0458 100644 --- a/chapter_preliminaries/probability.md +++ b/chapter_preliminaries/probability.md @@ -178,26 +178,26 @@ d2l.plt.legend(); * 对于任意事件 $\mathcal{A}$,其概率从不会是负数,即 $P(\mathcal{A}) \geq 0$; * 整个样本空间的概率为 $1$,即 $P(\mathcal{S}) = 1$; -* 对于任意事件 $\mathcal{A}_1, \mathcal{A}_2, \ldots$ 的可数序列,这些事件*互斥*(mutually exclusive)(对于所有 $i \neq j$ 都有 $\mathcal{A}_i \cap \mathcal{A}_j = \emptyset$),任何事件发生的概率等于它们各自发生的概率之和,即 $P(\bigcup_{i=1}^{\infty} \mathcal{A}_i) = \sum_{i=1}^{\infty} P(\mathcal{A}_i)$。 +* 对于*互斥*(mutually exclusive)(对于所有 $i \neq j$ 都有 $\mathcal{A}_i \cap \mathcal{A}_j = \emptyset$)事件的任意一个可数序列 $\mathcal{A}_1, \mathcal{A}_2, \ldots$ ,序列中任意一个事件发生的概率等于它们各自发生的概率之和,即 $P(\bigcup_{i=1}^{\infty} \mathcal{A}_i) = \sum_{i=1}^{\infty} P(\mathcal{A}_i)$。 这些也是概率论的公理,由科尔莫戈罗夫于 1933 年提出。有了这个公理系统,我们可以避免任何关于随机性的哲学争论;相反,我们可以用数学语言严格地推理。例如,让事件 $\mathcal{A}_1$ 为整个样本空间,且当所有$i > 1$时的$\mathcal{A}_i = \emptyset$,我们可以证明 $P(\emptyset) = 0$,即不可能发生事件的概率是 $0$。 ### 随机变量 在我们掷骰子的随机实验中,我们引入了 *随机变量*(random variable) 的概念。随机变量几乎可以是任何数量,并且不是确定性的。它可以在随机实验的一组可能性中取一个值。考虑一个随机变量 $X$,其值在掷骰子的样本空间 $\mathcal{S} = \{1, 2, 3, 4, 5, 6\}$ 中。我们可以将事件 “看到一个 $5$” 表示为 $\{X = 5\}$ 或 $X = 5$,其概率表示为 $P(\{X = 5\})$ 或 $P(X = 5)$。通过 $P(X = a)$,我们区分了随机变量 $X$ 和 $X$ 可以采取的值(例如 $a$)。然而,这可能会导致繁琐的表示。 -为了简化符号,一方面,我们可以将 $P(X)$ 表示为随机变量 $X$ 上的 *分布*(distribution):分布告诉我们 $X$ 获得任意值的概率。另一方面,我们可以简单用 $P(a)$ 表示随机变量取值 $a$ 的概率。由于概率论中的事件是来自样本空间的一组结果,因此我们可以为随机变量指定值的可取范围。例如,$P(1 \leq X \leq 3)$ 表示事件的概率 $\{1 \leq X \leq 3\}$,这意味着 $\{X = 1, 2, \text{or}, 3\}$。等价地,$P(1 \leq X \leq 3)$ 表示随机变量 $X$ 从 $\{1, 2, 3\}$ 中取值的概率。 +为了简化符号,一方面,我们可以将 $P(X)$ 表示为随机变量 $X$ 上的 *分布*(distribution):分布告诉我们 $X$ 获得任意值的概率。另一方面,我们可以简单用 $P(a)$ 表示随机变量取值 $a$ 的概率。由于概率论中的事件是来自样本空间的一组结果,因此我们可以为随机变量指定值的可取范围。例如,$P(1 \leq X \leq 3)$ 表示事件 $\{1 \leq X \leq 3\}$,即 $\{X = 1, 2, \text{or}, 3\}$的概率。等价地,$P(1 \leq X \leq 3)$ 表示随机变量 $X$ 从 $\{1, 2, 3\}$ 中取值的概率。 -请注意,*离散* (discrete) 随机变量(如骰子的侧面)和 *连续* (continuous) 变量(如人的体重和身高)之间存在微妙的区别。问两个人是否具有完全相同的身高没有什么意义。如果我们进行足够精确的测量,你会发现这个星球上没有两个人具有完全相同的身高。事实上,如果我们采取足够精细的测量,在你起床和去睡觉时都不会得到相同的身高。因此,问一个人身高为 1.80139278297192196202 米高的概率是没有任何意义的。考虑到世界上的人口数量,这个概率几乎是 0。在这种情况下,询问某人的身高是否落入给定的区间,比如是否在 1.79 米和 1.81 米之间更有意义。在这些情况下,我们将这个看到某个数值的可能性量化为 *密度* (density)。高度恰好 1.80 米没有概率,但密度不是 0。在任何两个不同高度之间的区间,我们都有非零的概率。在本节的其余部分中,我们将考虑离散空间中的概率。对于连续随机变量的概率,你可以参考 :numref:`sec_random_variables`。 +请注意,*离散* (discrete) 随机变量(如骰子的侧面)和 *连续* (continuous) 变量(如人的体重和身高)之间存在微妙的区别。问两个人是否具有完全相同的身高没有什么意义。如果我们进行足够精确的测量,你会发现这个星球上没有两个人具有完全相同的身高。事实上,如果我们采取足够精细的测量,在你起床和去睡觉时都不会得到相同的身高。因此,问一个人身高为 1.80139278297192196202 米高的概率是没有任何意义的。考虑到世界上的人口数量,这个概率几乎是 0。在这种情况下,询问某人的身高是否落入给定的区间,比如是否在 1.79 米和 1.81 米之间更有意义。在这些情况下,我们将这个看到某个数值的可能性量化为 *密度* (density)。高度恰好为 1.80 米的概率为 0,但密度不是 0。在任何两个不同高度之间的区间,我们都有非零的概率。在本节的其余部分中,我们将考虑离散空间中的概率。对于连续随机变量的概率,你可以参考 :numref:`sec_random_variables`。 ## 处理多个随机变量 -很多时候,我们会希望一次考虑多个随机变量。比如,我们可能需要对疾病和症状之间的关系进行建模。给定一个疾病和一个症状,比如 “流感” 和 “咳嗽”,以某个概率存在或不存在某个患者身上。虽然我们可能希望这两者发生的概率都接近于零,但我们可能需要估计这些概率和概率之间的关系,以便我们可以运用我们的推断来实现更好的医疗服务。 +很多时候,我们会希望一次考虑多个随机变量。比如,我们可能需要对疾病和症状之间的关系进行建模。给定一个疾病和一个症状,比如 “流感” 和 “咳嗽”,以某个概率存在或不存在于某个患者身上。虽然我们可能希望这两者发生的概率都接近于零,但我们可能需要估计这些概率以及概率之间的关系,以便我们可以运用我们的推断来实现更好的医疗服务。 再举一个更复杂的例子:图像包含数百万像素,因此有数百万个随机变量。在许多情况下,图像会附带一个标签,标识图像中的对象。我们也可以将标签视为一个随机变量。我们甚至可以将所有元数据视为随机变量,例如位置、时间、光圈、焦距、ISO、对焦距离和相机类型。所有这些都是联合发生的随机变量。当我们处理多个随机变量时,会有若干个变量是我们感兴趣的。 ### 联合概率 -第一个被称为 *联合概率* (joint probability) $P(A = a, B=b)$。给定任何值 $a$ 和 $b$, 联合概率可以回答, $A=a$ 和 $B=b$ 同时满足的概率是多少? 请注意,对于任何 $a$ 和 $b$ 的取值,$P(A = a, B=b) \leq P(A=a)$。这点是确定的,因为要同时发生 $A=a$ 和 $B=b$,$A=a$就必须发生,$B=b$也必须发生(反之亦然)。因此,$A=a$ 和 $B=b$ 同时发生的可能性不大于 $A=a$ 或是 $B=b$ 的可能性。 +第一个被称为 *联合概率* (joint probability) $P(A = a, B=b)$。给定任何值 $a$ 和 $b$, 联合概率可以回答, $A=a$ 和 $B=b$ 同时满足的概率是多少? 请注意,对于任何 $a$ 和 $b$ 的取值,$P(A = a, B=b) \leq P(A=a)$。这点是确定的,因为要同时发生 $A=a$ 和 $B=b$,$A=a$就必须发生,$B=b$也必须发生(反之亦然)。因此,$A=a$ 和 $B=b$ 同时发生的可能性不大于 $A=a$ 或是 $B=b$ 单独发生的可能性。 ### 条件概率 From b6278c95a23f8714cbf652ee839b362fa0fbe88a Mon Sep 17 00:00:00 2001 From: luzixiao <1461349565@qq.com> Date: Sun, 18 Apr 2021 12:59:07 +0800 Subject: [PATCH 040/103] fix grammar problem in index.md (#750) --- chapter_introduction/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index 61d35f531..ce6b56c9c 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -263,7 +263,7 @@ 如果这些假设成立,那么给出这两个数据样本,你就已经可以确定承包商的定价结构:每小时100美元,外加50美元上门服务费。 你看,在不经意间,你就已经理解并应用了线性回归的本质。 -以上假设有时这是不可取。 +以上假设有时并不可取。 例如,如果一些差异是由于两个特征之外的几个因素造成的。 在这些情况下,我们将尝试学习最小化”预测值和实际标签值的差异“的模型。 在本书大部分章节中,我们将关注最小化平方误差损失函数。 From e942baf26ee69e1fbbf4b5406d48d545d2810e7d Mon Sep 17 00:00:00 2001 From: nickeaglenny <69426923+nickeaglenny@users.noreply.github.com> Date: Mon, 19 Apr 2021 02:28:57 +0800 Subject: [PATCH 041/103] Update index.md (#747) * Update index.md * fix translations in index.md --- chapter_introduction/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter_introduction/index.md b/chapter_introduction/index.md index ce6b56c9c..5be1a451d 100644 --- a/chapter_introduction/index.md +++ b/chapter_introduction/index.md @@ -131,7 +131,7 @@ 当我们有了更多的数据,我们通常可以训练出更强大的模型,从而减少对预先设想假设的依赖。 数据集的由小变大为现代深度学习的成功奠定基础。 在没有大数据集的情况下,许多令人兴奋的深度学习模型黯然失色。 -就算一些深度学习模型在小数据集上能够工作,但其效能并比不上传统方法。 +就算一些深度学习模型在小数据集上能够工作,但其效能并不比传统方法高。 请注意,仅仅拥有海量的数据是不够的,我们还需要正确的数据。 如果数据中充满了错误,或者如果数据的特征不能预测任务目标,那么模型很可能无效。 @@ -656,7 +656,7 @@ agent的动作会影响后续的观察,而奖励只与所选的动作相对应 * 注意力机制解决了困扰统计学一个多世纪的问题:如何在不增加可学习参数的情况下增加系统的记忆和复杂性。研究人员通过使用只能被视为可学习的指针结构 :cite:`Bahdanau.Cho.Bengio.2014` 找到了一个优雅的解决方案。不需要记住整个文本序列(例如用于固定维度表示中的机器翻译),所有需要存储的都是指向翻译过程的中间状态的指针。这大大提高了长序列的准确性,因为模型在开始生成新序列之前不再需要记住整个序列。 * 多阶段设计。例如,存储器网络 :cite:`Sukhbaatar.Weston.Fergus.ea.2015` 和神经编程器-解释器 :cite:`Reed.De-Freitas.2015`。它们允许统计建模者描述用于推理的迭代方法。这些工具允许重复修改深度神经网络的内部状态,从而执行推理链中的后续步骤,类似于处理器如何修改用于计算的存储器。 * 另一个关键的发展是生成对抗网络 :cite:`Goodfellow.Pouget-Abadie.Mirza.ea.2014` 的发明。传统模型中,密度估计和生成模型的统计方法侧重于找到合适的概率分布和(通常是近似的)抽样算法。因此,这些算法在很大程度上受到统计模型固有灵活性的限制。生成式对抗性网络的关键创新是用具有可微参数的任意算法代替采样器。然后对这些数据进行调整,使得鉴别器(实际上是对两个样本的测试)不能区分假数据和真实数据。通过使用任意算法生成数据的能力,它为各种技术打开了密度估计的大门。驰骋的斑马 :cite:`Zhu.Park.Isola.ea.2017` 和假名人脸 :cite:`Karras.Aila.Laine.ea.2017` 的例子都证明了这一进展。即使是业余的涂鸦者也可以根据描述场景布局的草图生成照片级真实图像( :cite:`Park.Liu.Wang.ea.2019` )。 -* 在许多情况下,单个GPU不足以处理可用于训练的大量数据。在过去的十年中,构建并行和分布式训练算法的能力有了显着提高。设计可伸缩算法的关键挑战之一是深度学习优化的主力——随机梯度下降,它依赖于相对较小的小批量数据来处理。同时,小批量限制了GPU的效率。因此,在1024个GPU上进行训练,例如每批32个图像的小批量大小相当于总计约32000个图像的小批量。最近的工作,首先是由 :cite:`Li.2017` 完成的,随后是 :cite:`You.Gitman.Ginsburg.2017` 和 :cite:`Jia.Song.He.ea.2018` ,将观察大小提高到64000个,将ResNet-50模型在Imagenet数据集上的训练时间减少到不到7分钟。作为比较——最初的训练时间是按天为单位的。 +* 在许多情况下,单个GPU不足以处理可用于训练的大量数据。在过去的十年中,构建并行和分布式训练算法的能力有了显著提高。设计可伸缩算法的关键挑战之一是深度学习优化的主力——随机梯度下降,它依赖于相对较小的小批量数据来处理。同时,小批量限制了GPU的效率。因此,在1024个GPU上进行训练,例如每批32个图像的小批量大小相当于总计约32000个图像的小批量。最近的工作,首先是由 :cite:`Li.2017` 完成的,随后是 :cite:`You.Gitman.Ginsburg.2017` 和 :cite:`Jia.Song.He.ea.2018` ,将观察大小提高到64000个,将ResNet-50模型在Imagenet数据集上的训练时间减少到不到7分钟。作为比较——最初的训练时间是按天为单位的。 * 并行计算的能力也对强化学习的进步做出了相当关键的贡献。这导致了计算机在围棋、雅达里游戏、星际争霸和物理模拟(例如,使用MuJoCo)中实现超人性能的重大进步。有关如何在AlphaGo中实现这一点的说明,请参见如 :cite:`Silver.Huang.Maddison.ea.2016` 。简而言之,如果有大量的(状态、动作、奖励)三元组可用,即只要有可能尝试很多东西来了解它们之间的关系,强化学习就会发挥最好的作用。仿真提供了这样一条途径。 * 深度学习框架在传播思想方面发挥了至关重要的作用。允许轻松建模的第一代框架包括[Caffe](https://github.com/BVLC/caffe)、[Torch](https://github.com/torch)和[Theano](https://github.com/Theano/Theano)。许多开创性的论文都是用这些工具写的。到目前为止,它们已经被[TensorFlow](https://github.com/tensorflow/tensorflow)(通常通过其高级API [Keras](https://github.com/keras-team/keras)使用)、[CNTK](https://github.com/Microsoft/CNTK)、[Caffe 2](https://github.com/caffe2/caffe2)和[Apache MXNet](https://github.com/apache/incubator-mxnet)所取代。第三代工具,即用于深度学习的命令式工具,可以说是由[Chainer](https://github.com/chainer/chainer)率先推出的,它使用类似于Python NumPy的语法来描述模型。这个想法被[PyTorch](https://github.com/pytorch/pytorch)、MXNet的[Gluon API](https://github.com/apache/incubator-mxnet)和[Jax](https://github.com/google/jax)都采纳了。 From c848c64bd77c16751c572bdadbde7591248e71a7 Mon Sep 17 00:00:00 2001 From: Linhan Wu Date: Mon, 19 Apr 2021 02:29:49 +0800 Subject: [PATCH 042/103] fix typo in vgg.md (#745) * fix 4.4.1.2. Model Complexity translation issues * fix typo in chapter_multilayer-perceptrons/environment.md * fix typo and translation issues in kaggle-house-price.md * fix typo in model-construction.md * fix typo and translation issues in use-gpu.md * fix typo and translation issues in alexnet.md * fix typo in vgg.md * revert back to the original * fix typo in batch-norm.md Co-authored-by: Linhan_Wu --- chapter_convolutional-modern/batch-norm.md | 6 +++--- chapter_convolutional-modern/vgg.md | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/chapter_convolutional-modern/batch-norm.md b/chapter_convolutional-modern/batch-norm.md index e47ac5062..61b320f77 100644 --- a/chapter_convolutional-modern/batch-norm.md +++ b/chapter_convolutional-modern/batch-norm.md @@ -23,19 +23,19 @@ 批量归一化应用于单个可选层(也可以应用到所有层),其原理如下:在每次训练迭代中,我们首先归一化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。 接下来,我们应用比例系数和比例偏移。 -正是由于这个基于*批量*统计的*标准化*,才有了*批量归一化*的名称。。 +正是由于这个基于*批量*统计的*标准化*,才有了*批量归一化*的名称。 请注意,如果我们尝试使用大小为 1 的小批量应用批量归一化,我们将无法学到任何东西。 这是因为在减去均值之后,每个隐藏单元将为 0。 所以,只有使用足够大的小批量,批量归一化这种方法才是有效且稳定的。 请注意,在应用批量归一化时,批量大小的选择可能比没有批量归一化时更重要。 -从形式上来说,用 $\mathbf{x} \in \mathcal{B}$ 表示一个来自小批量 $\mathcal{B}$ 的输入,批量归一化$\mathrm{BN}$ 根据以下表达式转换 $\mathbf{x}$: +从形式上来说,用 $\mathbf{x} \in \mathcal{B}$ 表示一个来自小批量 $\mathcal{B}$ 的输入,批量归一化 $\mathrm{BN}$ 根据以下表达式转换 $\mathbf{x}$: $$\mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}_\mathcal{B}}{\hat{\boldsymbol{\sigma}}_\mathcal{B}} + \boldsymbol{\beta}.$$ :eqlabel:`eq_batchnorm` -在 :eqref:`eq_batchnorm` 中,$\hat{\boldsymbol{\mu}}_\mathcal{B}$ 是样本均值,$\hat{\boldsymbol{\mu}}_\mathcal{B}$ 是小批量 $\mathcal{B}$ 的样本标准差。 +在 :eqref:`eq_batchnorm` 中,$\hat{\boldsymbol{\mu}}_\mathcal{B}$ 是样本均值,$\hat{\boldsymbol{\sigma}}_\mathcal{B}$ 是小批量 $\mathcal{B}$ 的样本标准差。 应用标准化后,生成的小批量的平均值为 0 和单位方差为 1。 由于单位方差(与其他一些魔法数)是一个任意的选择,因此我们通常包含 *拉伸参数*(scale) $\boldsymbol{\gamma}$ 和 *偏移参数*(shift) $\boldsymbol{\beta}$,它们的形状与 $\mathbf{x}$ 相同。 diff --git a/chapter_convolutional-modern/vgg.md b/chapter_convolutional-modern/vgg.md index edd312fe5..5bca66f86 100644 --- a/chapter_convolutional-modern/vgg.md +++ b/chapter_convolutional-modern/vgg.md @@ -82,8 +82,7 @@ def vgg_block(num_convs, num_channels): :label:`fig_vgg` -VGG神经网络连续连接 :numref:`fig_vgg` 的几个 VGG 块(在 `vgg_block` 函数中定义)。其中有超参数变量 `conv_arch` 。该 -变量指定了每个VGG块里卷积层个数和输出通道数。全连接模块则与AlexNet中的相同。 +VGG神经网络连续连接 :numref:`fig_vgg` 的几个 VGG 块(在 `vgg_block` 函数中定义)。其中有超参数变量 `conv_arch` 。该变量指定了每个VGG块里卷积层个数和输出通道数。全连接模块则与AlexNet中的相同。 原始 VGG 网络有 5 个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。 第一个模块有 64 个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到 512。由于该网络使用 8 个卷积层和 3 个全连接层,因此它通常被称为 VGG-11。 From 24a5695ac66cfce2ebe55dd67934e7ddeeabdd3e Mon Sep 17 00:00:00 2001 From: Mu Li Date: Mon, 19 Apr 2021 11:37:40 -0700 Subject: [PATCH 043/103] [slides] fix linear reg slides bug --- chapter_linear-networks/linear-regression-scratch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_linear-networks/linear-regression-scratch.md b/chapter_linear-networks/linear-regression-scratch.md index d07ec5b4a..0e04e6e65 100644 --- a/chapter_linear-networks/linear-regression-scratch.md +++ b/chapter_linear-networks/linear-regression-scratch.md @@ -142,7 +142,7 @@ for X, y in data_iter(batch_size, features, labels): 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多,它可以处理存储在文件中的数据和通过数据流提供的数据。 -(~~定义~~) +[~~定义~~] ## (**初始化模型参数**) 在我们开始用小批量随机梯度下降优化我们的模型参数之前,我们需要先有一些参数。 From 3be2ac098cf3fcf0d7876967689cfdaddc6f9041 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Tue, 20 Apr 2021 02:55:50 +0800 Subject: [PATCH 044/103] +ch9 (#755) --- index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/index.md b/index.md index c3589ccdb..dc8769aa3 100644 --- a/index.md +++ b/index.md @@ -28,6 +28,7 @@ chapter_deep-learning-computation/index chapter_convolutional-neural-networks/index chapter_convolutional-modern/index chapter_recurrent-neural-networks/index +chapter_recurrent-modern/index chapter_computational-performance/index From 19550e7aba82fc47347a1ff7b6fdfe0172acfa4f Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Mon, 19 Apr 2021 21:30:01 +0200 Subject: [PATCH 045/103] translation issue in chapter_linear-networks/linear_regression_scratch (#754) --- chapter_linear-networks/linear-regression-scratch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_linear-networks/linear-regression-scratch.md b/chapter_linear-networks/linear-regression-scratch.md index 0e04e6e65..b0ae5fbfb 100644 --- a/chapter_linear-networks/linear-regression-scratch.md +++ b/chapter_linear-networks/linear-regression-scratch.md @@ -234,7 +234,7 @@ def sgd(params, grads, lr, batch_size): #@save ## 训练 现在我们已经准备好了模型训练所有需要的要素,可以实现主要的[**训练过程**]部分了。 -理解这段代码至关重要,因为在整个深度学习的职业生涯中,你会看到一遍又一遍几乎相同的训练过程。 +理解这段代码至关重要,因为在整个深度学习的职业生涯中,你会一遍又一遍地看到几乎相同的训练过程。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。最后,我们调用优化算法 `sgd` 来更新模型参数。 From 00d9a0626afe30c550d327bbdd060bdf51cd0df2 Mon Sep 17 00:00:00 2001 From: goldmermaid <37914843+goldmermaid@users.noreply.github.com> Date: Mon, 19 Apr 2021 12:30:30 -0700 Subject: [PATCH 046/103] [slides] kaggle-house-price (#743) * [slides] kaggle * [slides] kaggle * retrigger * retrigger * retrigger * retrigger * retrigger * retrigger * retrigger --- chapter_multilayer-perceptrons/kaggle-house-price.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chapter_multilayer-perceptrons/kaggle-house-price.md b/chapter_multilayer-perceptrons/kaggle-house-price.md index 811b6f531..ac32f1a05 100644 --- a/chapter_multilayer-perceptrons/kaggle-house-price.md +++ b/chapter_multilayer-perceptrons/kaggle-house-price.md @@ -161,14 +161,14 @@ print(train_data.shape) print(test_data.shape) ``` -让我们看看[**前四个和最后两个特征,以及相应标签(房价)**]。 +让我们看看[**前四个和最后两个特征,以及相应标签**](房价)。 ```{.python .input} #@tab all print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]) ``` -我们可以看到,[**在每个样本中,第一个特征是ID,**]这有助于模型识别每个训练样本。虽然这很方便,但它不携带任何用于预测的信息。因此,在将数据提供给模型之前,[**我们将其从数据集中删除**]。 +我们可以看到,(**在每个样本中,第一个特征是ID,**)这有助于模型识别每个训练样本。虽然这很方便,但它不携带任何用于预测的信息。因此,在将数据提供给模型之前,(**我们将其从数据集中删除**)。 ```{.python .input} #@tab all @@ -177,7 +177,7 @@ all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:])) ## 数据预处理 -如上所述,我们有各种各样的数据类型。在开始建模之前,我们需要对数据进行预处理。让我们从数字特征开始。首先,我们应用启发式方法,[**将所有缺失的值替换为相应特征的平均值。**]然后,为了将所有特征放在一个共同的尺度上,我们(**通过将特征重新缩放到零均值和单位方差来*标准化*数据**): +如上所述,我们有各种各样的数据类型。在开始建模之前,我们需要对数据进行预处理。让我们从数字特征开始。首先,我们应用启发式方法,[**将所有缺失的值替换为相应特征的平均值。**]然后,为了将所有特征放在一个共同的尺度上,我们(**通过将特征重新缩放到零均值和单位方差来标准化数据**): $$x \leftarrow \frac{x - \mu}{\sigma}.$$ @@ -346,9 +346,9 @@ def train(net, train_features, train_labels, test_features, test_labels, return train_ls, test_ls ``` -## [**$K$折交叉验证**] +## $K$折交叉验证 -你可能还记得,我们在讨论模型选择的部分( :numref:`sec_model_selection` )中介绍了$K$折交叉验证。这有助于模型选择和超参数调整。我们首先需要一个函数,在$K$折交叉验证过程中返回第$i$折的数据。它选择第$i$个切片作为验证数据,其余部分作为训练数据。注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,我们肯定会做一些更聪明的改变。但是这种改变所增加的复杂性可能会使代码看起来更乱。在这里可以忽略这些改变,因为我们的问题很简单。 +你可能还记得,我们在讨论模型选择的部分( :numref:`sec_model_selection` )中介绍了[**K折交叉验证**]。这有助于模型选择和超参数调整。我们首先需要一个函数,在$K$折交叉验证过程中返回第$i$折的数据。它选择第$i$个切片作为验证数据,其余部分作为训练数据。注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,我们肯定会做一些更聪明的改变。但是这种改变所增加的复杂性可能会使代码看起来更乱。在这里可以忽略这些改变,因为我们的问题很简单。 ```{.python .input} #@tab all From fd98cb875f4a216c4c31b3587a2f65d6890a2166 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Tue, 20 Apr 2021 11:18:05 +0800 Subject: [PATCH 047/103] chapter_recurrent-modern/gru (#724) * chapter_recurrent-modern/gru * Update gru.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/gru.md | 112 ++++++++++++++++---------------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/chapter_recurrent-modern/gru.md b/chapter_recurrent-modern/gru.md index 6f750fd69..d4f7a4285 100644 --- a/chapter_recurrent-modern/gru.md +++ b/chapter_recurrent-modern/gru.md @@ -1,28 +1,28 @@ -# 封闭的经常单位 (GRU) +# 门控循环单元(GRU) :label:`sec_gru` -在 :numref:`sec_bptt` 中,我们讨论了如何在 rnN 中计算梯度。特别是我们发现矩阵的长积可能会导致梯度消失或爆炸。让我们简单地考虑一下这种渐变异常在实践中的含义: +在 :numref:`sec_bptt` 中,我们讨论了如何在循环神经网络中计算梯度。特别是我们发现矩阵连续乘积可以导致梯度消失或爆炸。让我们简单思考一下这种梯度异常在实践中的意义: -* 我们可能会遇到一种情况,即早期观察对于预测未来所有观察都非常重要。考虑一下有些人为的情况,其中第一个观测值包含校验和,目标是识别序列末尾的校验和是否正确。在这种情况下,第一个令牌的影响至关重要。我们希望有一些机制将重要的早期信息存储在 * 内存单元 * 中。如果没有这种机制,我们将不得不为这一观察分配一个非常大的梯度,因为它会影响到随后的所有观察。 -* 我们可能会遇到某些代币没有相关观察的情况。例如,在解析网页时,可能会有辅助 HTML 代码,这些代码与评估页面上传达的情绪无关。我们希望有一些机制在潜在状态表示中 * 跳过 * 此类代币。 -* 我们可能会遇到序列的各个部分之间有逻辑中断的情况。例如,可能会在书中的章节之间进行过渡,或者证券的熊市和牛市之间的过渡。在这种情况下,最好有一种方法 * 重新设置 * 我们的内部州代表。 +* 我们可能会遇到这样一种情况——早期观测值对预测所有未来观测值具有非常重要的意义。考虑一个极端情况,其中第一个观测值包含一个校验和,目标是在序列的末尾辨别校验和是否正确。在这种情况下,第一个标记的影响至关重要。我们想有一些机制能够在一个记忆细胞里存储重要的早期信息。如果没有这样的机制,我们将不得不给这个观测值指定一个非常大的梯度,因为它会影响所有后续的观测值。 +* 我们可能会遇到这样的情况——一些标记没有相关的观测值。例如,在解析网页时,可能有一些辅助HTML代码与评估网页上传达的情绪无关。我们希望有一些机制来*跳过*隐状态表示中的此类标记。 +* 我们可能会遇到这样的情况——序列的各个部分之间存在逻辑中断。例如,书的章节之间可能会有一个过渡,或者证券的熊市和牛市之间可能会有一个过渡。在这种情况下,最好有一种方法来*重置*我们的内部状态表示。 -为解决这个问题,已经提出了若干方法。最早的一个是长短期记忆 :cite:`Hochreiter.Schmidhuber.1997`,我们将在 :numref:`sec_lstm` 中讨论。门控循环单元 (GRU) :cite:`Cho.Van-Merrienboer.Bahdanau.ea.2014` 是一种稍微更精简的变体,通常提供可比的性能,并且计算 :cite:`Chung.Gulcehre.Cho.ea.2014` 的速度要快得多。由于它的简单性,让我们从 GRU 开始。 +在学术界已经提出了许多方法来解决这个问题。其中最早的方法是"长-短记忆" :cite:`Hochreiter.Schmidhuber.1997` ,我们将在 :numref:`sec_lstm` 中讨论。门控循环单元(gated recurrent unit,GRU) :cite:`Cho.Van-Merrienboer.Bahdanau.ea.2014` 是一个稍微简化的变体,通常提供相当的性能,并且计算 :cite:`Chung.Gulcehre.Cho.ea.2014` 的速度明显更快。由于它的简单,让我们从门控循环单元开始。 -## 封闭的隐藏状态 +## 门控隐藏状态 -香草 rnN 和 gRU 之间的关键区别在于后者支持隐藏状态的门控。这意味着我们有专门的机制来确定隐藏状态何时应该为 * 更新 * 以及何时应该是 * 重置 *。学习了这些机制,它们解决了上述问题。例如,如果第一个令牌非常重要,我们将学会在第一次观察之后不更新隐藏状态。同样,我们将学会跳过无关紧要的临时观察。最后,我们将学习在需要时重置潜在状态。我们在下面详细讨论这个问题。 +普通的循环神经网络和门控循环单元之间的关键区别在于后者支持隐藏状态的门控(或者说选通)。这意味着有专门的机制来确定何时应该*更新*隐藏状态,以及何时应该*重置*隐藏状态。这些机制是可学习的,它们解决了上面列出的问题。例如,如果第一个标记非常重要,我们将学会在第一次观测之后不更新隐藏状态。同样,我们也可以学会跳过不相关的临时观测。最后,我们将学会在需要的时候重置隐藏状态。我们将在下面详细讨论这一点。 -### 重置 Gate 和更新门 +### 重置门和更新门 -我们需要引入的第一件事是 * 重置门 * 和 * 更新门 *。我们将它们设计成为带有 $(0, 1)$ 条目的向量,以便我们可以执行凸组合。例如,重置门将允许我们控制我们可能仍想记住的以前状态的程度。同样,更新门将允许我们控制新州有多少只是旧状态的副本。 +我们首先要介绍的是*重置门*(reset gate)和*更新门*(update gate)。我们把它们设计成$(0, 1)$区间中的向量,这样我们就可以进行凸组合。例如,重置门允许我们控制可能还想记住多少以前的状态。同样,更新门将允许我们控制新状态中有多少是旧状态的副本。 -我们首先设计这些门。考虑到当前时间步长的输入和上一个时间步的隐藏状态,:numref:`fig_gru_1` 说明了 GRU 中复位和更新门的输入。两个门的输出由两个带有 sigmoid 激活功能的完全连接层给出。 +我们从构造这些门控开始。 :numref:`fig_gru_1` 示出了在给定当前时间步的输入和前一时间步隐藏状态的情况下,用于门控循环单元中的重置门和更新门的输入。两个门的输出由具有sigmoid激活函数的两个全连接层给出。 -![Computing the reset gate and the update gate in a GRU model.](../img/gru-1.svg) +![在门控循环单元模型中计算重置门和更新门。](../img/gru-1.svg) :label:`fig_gru_1` -从数学上讲,对于给定时间步长 $t$,假设输入是微型批次 $\mathbf{X}_t \in \mathbb{R}^{n \times d}$(示例数:$n$,输入数量:$d$),前一个时间步长的隐藏状态为 $\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}$(隐藏单位数:$h$)。然后,重置门 $\mathbf{R}_t \in \mathbb{R}^{n \times h}$ 和更新门 $\mathbf{Z}_t \in \mathbb{R}^{n \times h}$ 的计算方法如下: +在数学上,对于给定的时间步$t$,假设输入是一个小批量$\mathbf{X}_t \in \mathbb{R}^{n \times d}$ (样本数:$n$,输入数:$d$),上一个时间步的隐藏状态是$\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}$(隐藏单元数:$h$)。然后,重置门$\mathbf{R}_t \in \mathbb{R}^{n \times h}$和更新门$\mathbf{Z}_t \in \mathbb{R}^{n \times h}$的计算如下: $$ \begin{aligned} @@ -31,47 +31,45 @@ $$ \end{aligned} $$ -其中 $\mathbf{W}_{xr}, \mathbf{W}_{xz} \in \mathbb{R}^{d \times h}$ 和 $\mathbf{W}_{hr}, \mathbf{W}_{hz} \in \mathbb{R}^{h \times h}$ 是重量参数,$\mathbf{b}_r, \mathbf{b}_z \in \mathbb{R}^{1 \times h}$ 是偏见。请注意,在总和期间触发广播(见 :numref:`subsec_broadcasting`)。我们使用 sigmoid 函数(如 :numref:`sec_mlp` 中所介绍的那样)将输入值转换为间隔 $(0, 1)$。 +其中$\mathbf{W}_{xr}, \mathbf{W}_{xz} \in \mathbb{R}^{d \times h}$和$\mathbf{W}_{hr}, \mathbf{W}_{hz} \in \mathbb{R}^{h \times h}$是权重参数,$\mathbf{b}_r, \mathbf{b}_z \in \mathbb{R}^{1 \times h}$是偏置参数。请注意,在求和过程中会触发广播机制(请参阅 :numref:`subsec_broadcasting` )。我们使用sigmoid函数(如:numref:`sec_mlp`中介绍的)将输入值转换到区间$(0, 1)$。 -### 候选人隐藏状态 +### 候选隐藏状态 -接下来,让我们将重置门 $\mathbf{R}_t$ 与 :eqref:`rnn_h_with_state` 中的常规潜在状态更新机制集成起来。它导致以下情况 -*候选人隐藏状态 * -$\tilde{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$ 在时间步骤 $t$: +接下来,让我们将重置门 $\mathbf{R}_t$ 与 :eqref:`rnn_h_with_state` 中的常规隐状态更新机制集成,得到在时间步$t$的候选隐藏状态$\tilde{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$。 $$\tilde{\mathbf{H}}_t = \tanh(\mathbf{X}_t \mathbf{W}_{xh} + \left(\mathbf{R}_t \odot \mathbf{H}_{t-1}\right) \mathbf{W}_{hh} + \mathbf{b}_h),$$ :eqlabel:`gru_tilde_H` -其中 $\mathbf{W}_{xh} \in \mathbb{R}^{d \times h}$ 和 $\mathbf{W}_{hh} \in \mathbb{R}^{h \times h}$ 是重量参数,$\mathbf{b}_h \in \mathbb{R}^{1 \times h}$ 是偏置,符号 $\odot$ 是哈达马德(元素)产品运营商。在这里,我们使用 tanh 形式的非线性来确保候选隐藏状态中的值保持在区间 $(-1, 1)$。 +其中$\mathbf{W}_{xh} \in \mathbb{R}^{d \times h}$和$\mathbf{W}_{hh} \in \mathbb{R}^{h \times h}$是权重参数,$\mathbf{b}_h \in \mathbb{R}^{1 \times h}$是偏置项,符号$\odot$是哈达码乘积(按元素乘积)运算符。在这里,我们使用tanh非线性激活函数来确保候选隐藏状态中的值保持在区间$(-1, 1)$中。 -结果是 * 候选人 *,因为我们仍然需要纳入更新门的操作。与 :eqref:`rnn_h_with_state` 相比,现在可以通过 $\mathbf{R}_t$ 和 $\mathbf{H}_{t-1}$ 的元素乘法来降低以前各州的影响力。每当重置门 $\mathbf{R}_t$ 中的条目接近 1 时,我们就会恢复一个香草 RNN,例如 :eqref:`rnn_h_with_state`。对于重置门 $\mathbf{R}_t$ 中接近 0 的所有条目,候选隐藏状态是以 $\mathbf{X}_t$ 作为输入的 MLP 的结果。因此,任何预先存在的隐藏状态都是 * 重置 * 为默认值。 +结果是*候选者*,因为我们仍然需要结合更新门的操作。与 :eqref:`rnn_h_with_state` 相比, :eqref:`gru_tilde_H` 中的$\mathbf{R}_t$和$\mathbf{H}_{t-1}$的元素相乘可以减少以往状态的影响。每当重置门$\mathbf{R}_t$中的项接近1时,我们恢复一个如:eqref:`rnn_h_with_state`中的循环神经网络。对于重置门$\mathbf{R}_t$中所有接近0的项,候选隐藏状态是以$\mathbf{X}_t$作为输入的多层感知机的结果。因此,任何预先存在的隐藏状态都会被*重置*为默认值。 -:numref:`fig_gru_2` 说明了应用复位门后的计算流程。 +:numref:`fig_gru_2`说明了应用重置门之后的计算流程。 -![Computing the candidate hidden state in a GRU model.](../img/gru-2.svg) +![在门控循环单元模型中计算候选隐藏状态。](../img/gru-2.svg) :label:`fig_gru_2` ### 隐藏状态 -最后,我们需要纳入更新门 $\mathbf{Z}_t$ 的效果。这决定了新的隐藏状态 $\mathbf{H}_t \in \mathbb{R}^{n \times h}$ 只是旧状态 $\mathbf{H}_{t-1}$ 的程度,以及新候选状态 $\tilde{\mathbf{H}}_t$ 的使用程度。更新门 $\mathbf{Z}_t$ 可用于此目的,只需在 $\mathbf{H}_{t-1}$ 和 $\tilde{\mathbf{H}}_t$ 之间使用元素凸组合。这将导致 GRU 的最终更新方程式: +最后,我们需要结合更新门$\mathbf{Z}_t$的效果。这确定新隐藏状态$\mathbf{H}_t \in \mathbb{R}^{n \times h}$是旧状态$\mathbf{H}_{t-1}$的程度以及新候选状态$\tilde{\mathbf{H}}_t$的使用量。更新门$\mathbf{Z}_t$可用于此目的,只需在$\mathbf{H}_{t-1}$和$\tilde{\mathbf{H}}_t$之间进行按元素的凸组合。这得出门控循环单元的最终更新公式: $$\mathbf{H}_t = \mathbf{Z}_t \odot \mathbf{H}_{t-1} + (1 - \mathbf{Z}_t) \odot \tilde{\mathbf{H}}_t.$$ -每当更新门 $\mathbf{Z}_t$ 接近 1 时,我们只需保留旧状态。在这种情况下,$\mathbf{X}_t$ 的信息基本上被忽略,实际上跳过了依赖链中的时间步长 $t$。相比之下,只要 $\mathbf{Z}_t$ 接近 0,新的潜在状态 $\mathbf{H}_t$ 就接近候选潜在状态 $\tilde{\mathbf{H}}_t$。这些设计可以帮助我们应对 rnN 中逐渐消失的渐变问题,并更好地捕获具有较长时间步长距离的序列的依赖关系。例如,如果整个子序列的所有时间步长度的更新门都接近 1,则无论子序列的长度如何,其开始时间步的旧隐藏状态都将很容易保留并传递到其末尾。 +每当更新门$\mathbf{Z}_t$接近1时,我们只保留旧状态。在这种情况下,来自$\mathbf{X}_t$的信息基本上被忽略,有效地跳过了依赖链条中的时间步$t$。相反,当$\mathbf{Z}_t$接近0时,新隐藏状态$\mathbf{H}_t$接近候选隐藏状态$\tilde{\mathbf{H}}_t$。这些设计可以帮助我们处理循环神经网络中的消失梯度问题,并更好地捕获具有大时间步长距离的序列的相关性。例如,如果整个子序列的所有时间步的更新门都接近于1,则无论序列的长度如何,在序列起始时间步的旧隐藏状态都将很容易保留并传递到序列结束。 -:numref:`fig_gru_3` 说明了更新门运行之后的计算流程。 +:numref:`fig_gru_3`说明了更新门起作用后的计算流。 -![Computing the hidden state in a GRU model.](../img/gru-3.svg) +![计算门控循环单元模型中的隐藏状态。](../img/gru-3.svg) :label:`fig_gru_3` -总之,GRU 具有以下两个区别特征: +总之,门控循环单元具有以下两个显著特征: -* 重置门有助于捕获顺序中的短期依赖关系。 -* 更新门有助于按顺序捕获长期依赖关系。 +* 重置门能够帮助捕获序列中的短期依赖关系。 +* 更新门能够帮助捕获序列中的长期依赖关系。 -## 从头开始实施 +## 从零开始实现 -为了更好地了解 GRU 模型,让我们从头开始实施它。我们首先阅读我们在 :numref:`sec_rnn_scratch` 中使用的时间机器数据集。下面给出了读取数据集的代码。 +为了更好地理解门控循环单元模型,让我们从零开始实现它。我们首先读取 :numref:`sec_rnn_scratch` 中使用的时间机器数据集。下面给出了读取数据集的代码。 ```{.python .input} from d2l import mxnet as d2l @@ -95,7 +93,7 @@ train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ### 初始化模型参数 -下一步是初始化模型参数。我们从标准差的高斯分布中绘制权重为 0.01,然后将偏置设置为 0。超参数 `num_hiddens` 定义了隐藏单位的数量。我们实例化与更新门、重置门、候选隐藏状态和输出层相关的所有权重和偏置。 +下一步是初始化模型参数。我们从标准差为0.01的高斯分布中提取权重,并将偏置项设为0。超参数`num_hiddens`定义了隐藏单元的数量。我们实例化与更新门、重置门、候选隐藏状态和输出层相关的所有权重和偏置。 ```{.python .input} def get_params(vocab_size, num_hiddens, device): @@ -109,13 +107,13 @@ def get_params(vocab_size, num_hiddens, device): normal((num_hiddens, num_hiddens)), np.zeros(num_hiddens, ctx=device)) - W_xz, W_hz, b_z = three() # Update gate parameters - W_xr, W_hr, b_r = three() # Reset gate parameters - W_xh, W_hh, b_h = three() # Candidate hidden state parameters - # Output layer parameters + W_xz, W_hz, b_z = three() # 更新门参数 + W_xr, W_hr, b_r = three() # 重置门参数 + W_xh, W_hh, b_h = three() # 候选隐藏状态参数 + # 输出层参数 W_hq = normal((num_hiddens, num_outputs)) b_q = np.zeros(num_outputs, ctx=device) - # Attach gradients + # 附加梯度 params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q] for param in params: param.attach_grad() @@ -135,13 +133,13 @@ def get_params(vocab_size, num_hiddens, device): normal((num_hiddens, num_hiddens)), d2l.zeros(num_hiddens, device=device)) - W_xz, W_hz, b_z = three() # Update gate parameters - W_xr, W_hr, b_r = three() # Reset gate parameters - W_xh, W_hh, b_h = three() # Candidate hidden state parameters - # Output layer parameters + W_xz, W_hz, b_z = three() # 更新门参数 + W_xr, W_hr, b_r = three() # 重置门参数 + W_xh, W_hh, b_h = three() # 候选隐藏状态参数 + # 输出层参数 W_hq = normal((num_hiddens, num_outputs)) b_q = d2l.zeros(num_outputs, device=device) - # Attach gradients + # 附加梯度 params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q] for param in params: param.requires_grad_(True) @@ -150,7 +148,7 @@ def get_params(vocab_size, num_hiddens, device): ### 定义模型 -现在我们将定义隐藏状态初始化函数 `init_gru_state`。就像 :numref:`sec_rnn_scratch` 中定义的 `init_rnn_state` 函数一样,此函数返回一个形状(批量大小,隐藏单位数)的张量,其值均为零。 +现在我们将定义隐藏状态初始化函数`init_gru_state`。与 :numref:`sec_rnn_scratch` 中定义的`init_rnn_state`函数一样,此函数返回一个值均为零的形状为 (批量大小, 隐藏单元数) 的张量。 ```{.python .input} def init_gru_state(batch_size, num_hiddens, device): @@ -163,7 +161,7 @@ def init_gru_state(batch_size, num_hiddens, device): return (torch.zeros((batch_size, num_hiddens), device=device), ) ``` -现在我们已经准备好定义 GRU 模型了。它的结构与基本 RNN 单元格的结构相同,只是更新方程更复杂。 +现在我们准备好定义门控循环单元模型了。其结构与基本循环神经网络单元相同,只是更新公式更为复杂。 ```{.python .input} def gru(inputs, state, params): @@ -196,9 +194,9 @@ def gru(inputs, state, params): return torch.cat(outputs, dim=0), (H,) ``` -### 训练和预测 +### 训练与预测 -培训和预测的工作方式与 :numref:`sec_rnn_scratch` 完全相同。训练后,我们分别在提供的前缀 “时间旅行者” 和 “旅行者” 之后打印训练套装上的困惑和预测顺序。 +训练和预测的工作方式与 :numref:`sec_rnn_scratch` 完全相同。训练结束后,我们打印出训练集的困惑度。同时打印前缀“time traveler”和“traveler”的预测序列上的困惑度。 ```{.python .input} #@tab all @@ -209,9 +207,9 @@ model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params, d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` -## 简明的实施 +## 简洁实现 -在高级 API 中,我们可以直接实例化 GPU 模型。这封装了我们在上面明确说明的所有配置细节。该代码要快得多,因为它使用编译的运算符而不是 Python 来解决我们之前拼出的许多细节。 +在高级API中,我们可以直接实例化门控循环单元模型。这封装了我们在上面明确介绍的所有配置细节。这段代码的速度要快得多,因为它使用编译好的运算符而不是Python来处理之前阐述的许多细节。 ```{.python .input} gru_layer = rnn.GRU(num_hiddens) @@ -228,19 +226,19 @@ model = model.to(device) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` -## 摘要 +## 小结 -* 封闭的 RNN 可以更好地捕获具有较长时间步长距离的序列的依赖关系。 -* 重置门有助于捕获顺序中的短期依赖关系。 -* 更新门有助于按顺序捕获长期依赖关系。 -* 每当复位门打开时,GRU 都会包含基本的 RNN 作为极端情况。他们还可以通过打开更新门来跳过子序列。 +* 门控循环神经网络可以更好地捕获具有长时间步距离序列上的依赖关系。 +* 重置门有助于捕获序列中的短期相互依赖关系。 +* 更新门有助于捕获序列中的长期相互依赖关系。 +* 重置门打开时,门控循环单元包含基本循环神经网络;更新门打开时,门控循环单元可以跳过子序列。 ## 练习 -1. 假设我们只想使用时间步骤 $t'$ 的输入来预测时间步骤 $t > t'$ 的输出。每个时间步长的重置和更新门的最佳值是什么? -1. 调整超参数并分析它们对运行时间、困惑和输出序列的影响。 -1. 比较 `rnn.RNN` 和 `rnn.GRU` 实现的运行时、困惑和输出字符串。 -1. 如果只实施 GRU 的一部分(例如,只有重置门或只有更新门),会发生什么情况? +1. 假设我们只想使用时间步$t'$的输入来预测时间步$t > t'$的输出。对于每个时间步,重置门和更新门的最佳值是什么? +1. 调整超参数,分析它们对运行时间、困惑度和输出顺序的影响。 +1. 比较`rnn.RNN`和`rnn.GRU`实现的运行时间、困惑度和输出字符串。 +1. 如果你只实现门控循环单元的一部分,例如,只有一个重置门或只有一个更新门,会发生什么情况? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/342) From b2d57999a61369d480cfed4fad3cb11398bed588 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Tue, 20 Apr 2021 11:50:07 +0800 Subject: [PATCH 048/103] chapter_recurrent-modern/lstm (#725) * chapter_recurrent-modern/lstm * Update lstm.md * Update lstm.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/lstm.md | 109 +++++++++++++++---------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/chapter_recurrent-modern/lstm.md b/chapter_recurrent-modern/lstm.md index d31f61571..c43b32128 100644 --- a/chapter_recurrent-modern/lstm.md +++ b/chapter_recurrent-modern/lstm.md @@ -1,22 +1,21 @@ -# 长期短期记忆(LSTM) +# 长短期记忆网络(LSTM) :label:`sec_lstm` -解决潜在变量模型中的长期信息保存和短期投入跳过的挑战已经存在很长时间了。解决这个问题的最早方法之一是长短期记忆 (LSTM) :cite:`Hochreiter.Schmidhuber.1997`。它共享了 GRU 的许多属性。有趣的是,LSTM 的设计比 GRU 略复杂,但早了将近二十年。 +长期以来,隐变量模型存在着长期信息保存和短期输入跳跃的问题。解决这一问题的最早方法之一是长短期存储器(LSTM) :cite:`Hochreiter.Schmidhuber.1997`。它有许多与门控循环单元一样的属性。有趣的是,长短期记忆网络的设计比门控循环单元稍微复杂一些,但比门控循环单元早诞生了近20年。 -## 封闭的记忆单元 +## 门控记忆单元 -可以说,LSTM 的设计灵感来自计算机的逻辑门。LSTM 引入了一个 * 内存单元 *(或简称 * 细胞 *),其形状与隐藏状态相同(有些文学将记忆细胞视为隐藏状态的一种特殊类型),旨在记录其他信息。为了控制记忆细胞,我们需要一些门。需要一个门才能从牢房里读出条目。我们将把这称为 -*输出门 *。 -需要第二个门来决定何时将数据读入单元格。我们将此称为 * 输入门 *。最后,我们需要一种机制来重置单元格的内容,由 * 忘记门 * 控制。这种设计的动机与 GRU 的动机相同,即能够通过专用机制决定何时记住以及何时忽略隐藏状态中的输入。让我们看看这在实践中是如何运作的。 +可以说,长短期记忆网络的设计灵感来自于计算机的逻辑门。长短期记忆网络引入了*存储单元*(memory cell),或简称为*单元*(cell)。(一些文献认为存储单元是隐藏状态的一种特殊类型)。它们与隐藏状态具有相同的形状,被设计为记录附加信息。为了控制存储单元,我们需要许多门。如,我们需要一个门来从单元中读出条目。我们将其称为*输出门*(output gate)。 +另外,需要一个门来决定何时将数据读入单元。我们将其称为*输入门*(input gate)。最后,我们需要一种机制来重置单元的内容,由*遗忘门*(forget gate)来管理。这种设计的动机与门控循环单元相同,即能够通过专用机制决定什么时候记忆或忽略隐藏状态中的输入。让我们看看这在实践中是如何运作的。 ### 输入门、忘记门和输出门 -就像在 GRU 中一样,输入到 LSTM 门的数据是当前时间步长的输入,也是上一个时间步的隐藏状态,如 :numref:`lstm_0` 所示。它们由三个带有 sigmoid 激活功能的完全连接层进行处理,以计算输入、忘记。和输出门的值。因此,三个门的价值在 $(0, 1)$ 的范围内。 +就像在门控循环单元中一样,送到长短期记忆网络门的数据是当前时间步的输入和前一个时间步的隐藏状态,如 :numref:`lstm_0` 所示。它们由具有sigmoid激活函数的三个全连接层处理,以计算输入门、遗忘门和输出门的值。因此,这三个门的值都在$(0, 1)$的范围内。 -![Computing the input gate, the forget gate, and the output gate in an LSTM model.](../img/lstm-0.svg) +![在长短期记忆模型中计算输入门、遗忘门和输出门。](../img/lstm-0.svg) :label:`lstm_0` -从数学上讲,假设有 $h$ 个隐藏单位,批量大小为 $n$,输入数量为 $d$。因此,输入为 $\mathbf{X}_t \in \mathbb{R}^{n \times d}$,上一个时间步长的隐藏状态是 $\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}$。相应地,时间步骤 $t$ 的门定义如下:输入门为 $\mathbf{I}_t \in \mathbb{R}^{n \times h}$,忘记门是 $\mathbf{F}_t \in \mathbb{R}^{n \times h}$,输出门为 $\mathbf{O}_t \in \mathbb{R}^{n \times h}$。它们的计算方法如下: +在数学上,假设有$h$个隐藏单元,批量大小为$n$,输入数为$d$。因此,输入为$\mathbf{X}_t \in \mathbb{R}^{n \times d}$,前一时间步的隐藏状态为$\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}$。相应地,时间步$t$的门被定义如下:输入门是$\mathbf{I}_t \in \mathbb{R}^{n \times h}$,遗忘门是$\mathbf{F}_t \in \mathbb{R}^{n \times h}$,输出门是$\mathbf{O}_t \in \mathbb{R}^{n \times h}$。它们的计算方法如下: $$ \begin{aligned} @@ -26,51 +25,51 @@ $$ \end{aligned} $$ -其中 $\mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h}$ 和 $\mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h}$ 是权重参数,$\mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h}$ 是偏置参数。 +其中$\mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h}$和$\mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h}$是权重参数,$\mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h}$是偏置参数。 -### 候选记忆细胞 +### 候选记忆单元 -接下来我们设计记忆单元。由于我们还没有指定各种门的动作,我们首先介绍了 * 候选人 * 记忆细胞 $\tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h}$。它的计算方法与上述三个门类似,但是使用 $\tanh$ 函数作为激活函数,值范围为 $(-1, 1)$。这导致在时间步骤 $t$ 时出现以下方程式: +接下来,我们设计记忆单元。由于我们还没有指定各种门的操作,所以我们首先介绍候选记忆单元(candidate memory cell)$\tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h}$。它的计算与上面描述的三个门的计算类似,但是使用$\tanh$函数(值范围为$(-1, 1)$)作为激活函数。这导致在时间步$t$处得出以下方程: $$\tilde{\mathbf{C}}_t = \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c),$$ -其中 $\mathbf{W}_{xc} \in \mathbb{R}^{d \times h}$ 和 $\mathbf{W}_{hc} \in \mathbb{R}^{h \times h}$ 是权重参数,$\mathbf{b}_c \in \mathbb{R}^{1 \times h}$ 是偏置参数。 +其中$\mathbf{W}_{xc} \in \mathbb{R}^{d \times h}$和$\mathbf{W}_{hc} \in \mathbb{R}^{h \times h}$是权重参数,$\mathbf{b}_c \in \mathbb{R}^{1 \times h}$是偏置参数。 -:numref:`lstm_1` 中显示了候选记忆细胞的快速说明。 +候选记忆单元的图示如 :numref:`lstm_1` 。 -![Computing the candidate memory cell in an LSTM model.](../img/lstm-1.svg) +![在长短期记忆模型中计算候选记忆单元。](../img/lstm-1.svg) :label:`lstm_1` -### 记忆细胞 +### 记忆单元 -在 GRU 中,我们有一种机制来管理输入和忘记(或跳过)。同样,在 LSTM 中,我们有两个专用门用于此目的:输入门 $\mathbf{I}_t$ 控制我们通过 $\tilde{\mathbf{C}}_t$ 将新数据考虑在内的程度,忘记门 $\mathbf{F}_t$ 解决了我们保留的旧记忆细胞含量 $\mathbf{C}_{t-1} \in \mathbb{R}^{n \times h}$。使用与之前相同的点乘技巧,我们得出以下更新公式: +在门控循环单元中,我们有一种机制来控制输入和遗忘(或跳过)。类似地,在长短期记忆网络中,我们有两个门用于这样的目的:输入门$\mathbf{I}_t$控制我们考虑多少来自$\tilde{\mathbf{C}}_t$的新数据,而遗忘门$\mathbf{F}_t$控制我们保留了多少旧记忆单元$\mathbf{C}_{t-1} \in \mathbb{R}^{n \times h}$的内容。使用与前面相同的按元素乘法技巧,我们得出以下更新公式: $$\mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t.$$ -如果忘记门始终大约为 1 且输入门始终大约为 0,那么过去的记忆单元 $\mathbf{C}_{t-1}$ 将随着时间的推移保存并传递到当前时间步长。引入这种设计是为了缓解渐变的消失问题,并更好地捕捉序列中的长距离依赖关系。 +如果遗忘门始终为1且输入门始终为0,则过去的记忆单元$\mathbf{C}_{t-1}$将随时间被保存并传递到当前时间步。引入这种设计是为了缓解梯度消失问题,并更好地捕获序列中的长距离依赖关系。 -因此,我们在 :numref:`lstm_2` 中得到了流程图。 +这样我们就得到了流程图,如:numref:`lstm_2`。 -![Computing the memory cell in an LSTM model.](../img/lstm-2.svg) +![在长短期记忆网络模型中计算存储单元。](../img/lstm-2.svg) :label:`lstm_2` ### 隐藏状态 -最后,我们需要定义如何计算隐藏状态 $\mathbf{H}_t \in \mathbb{R}^{n \times h}$。这是输出门发挥作用的地方。在 LSTM 中,它只是 $\tanh$ 的记忆细胞的门控版本。这确保了 $\mathbf{H}_t$ 的值始终在区间 $(-1, 1)$ 内。 +最后,我们需要定义如何计算隐藏状态$\mathbf{H}_t \in \mathbb{R}^{n \times h}$。这就是输出门发挥作用的地方。在长短期记忆网络中,它仅仅是记忆单元的$\tanh$的门控版本。这确保了$\mathbf{H}_t$的值始终在区间$(-1, 1)$内。 $$\mathbf{H}_t = \mathbf{O}_t \odot \tanh(\mathbf{C}_t).$$ -每当输出门接近 1 时,我们会有效地将所有内存信息传递给预测变量,而对于接近 0 的输出门,我们将所有信息仅保留在内存单元内,不执行进一步的处理。 +只要输出门接近1,我们就有效地将所有记忆信息传递给预测部分,而对于接近0的输出门,我们只保留存储单元内的所有信息,并且不执行进一步的处理。 -:numref:`lstm_3` 有数据流的图形说明。 +:numref:`lstm_3`提供了数据流的图形化演示。 -![Computing the hidden state in an LSTM model.](../img/lstm-3.svg) +![在长短期记忆模型中计算隐藏状态。](../img/lstm-3.svg) :label:`lstm_3` -## 从头开始实施 +## 从零开始实现 -现在让我们从头开始实施 LSTM。与 :numref:`sec_rnn_scratch` 中的实验一样,我们首先加载时间机器数据集。 +现在,让我们从头开始实现长短期记忆网络。与 :numref:`sec_rnn_scratch` 中的实验相同,我们首先加载时光机器数据集。 ```{.python .input} from d2l import mxnet as d2l @@ -92,9 +91,9 @@ batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ``` -### 初始化模型参数 +### 正在初始化模型参数 -接下来我们需要定义和初始化模型参数。与之前一样,超参数 `num_hiddens` 定义了隐藏单位的数量。我们在使用 0.01 标准差的高斯分布之后初始化权重,然后我们将偏置设置为 0。 +接下来,我们需要定义和初始化模型参数。如前所述,超参数`num_hiddens`定义隐藏单元的数量。我们按照标准差0.01的高斯分布初始化权重,并将偏置设置为0。 ```{.python .input} def get_lstm_params(vocab_size, num_hiddens, device): @@ -108,14 +107,14 @@ def get_lstm_params(vocab_size, num_hiddens, device): normal((num_hiddens, num_hiddens)), np.zeros(num_hiddens, ctx=device)) - W_xi, W_hi, b_i = three() # Input gate parameters - W_xf, W_hf, b_f = three() # Forget gate parameters - W_xo, W_ho, b_o = three() # Output gate parameters - W_xc, W_hc, b_c = three() # Candidate memory cell parameters - # Output layer parameters + W_xi, W_hi, b_i = three() # 输入门参数 + W_xf, W_hf, b_f = three() # 遗忘门参数 + W_xo, W_ho, b_o = three() # 输出门参数 + W_xc, W_hc, b_c = three() # 候选记忆单元参数 + # 输出层参数 W_hq = normal((num_hiddens, num_outputs)) b_q = np.zeros(num_outputs, ctx=device) - # Attach gradients + # 附加梯度 params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] for param in params: @@ -136,14 +135,14 @@ def get_lstm_params(vocab_size, num_hiddens, device): normal((num_hiddens, num_hiddens)), d2l.zeros(num_hiddens, device=device)) - W_xi, W_hi, b_i = three() # Input gate parameters - W_xf, W_hf, b_f = three() # Forget gate parameters - W_xo, W_ho, b_o = three() # Output gate parameters - W_xc, W_hc, b_c = three() # Candidate memory cell parameters - # Output layer parameters + W_xi, W_hi, b_i = three() # 输入门参数 + W_xf, W_hf, b_f = three() # 遗忘门参数 + W_xo, W_ho, b_o = three() # 输出门参数 + W_xc, W_hc, b_c = three() # 候选记忆单元参数 + # 输出层参数 W_hq = normal((num_hiddens, num_outputs)) b_q = d2l.zeros(num_outputs, device=device) - # Attach gradients + # 附加梯度 params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] for param in params: @@ -153,7 +152,7 @@ def get_lstm_params(vocab_size, num_hiddens, device): ### 定义模型 -在初始化函数中,LSTM 的隐藏状态需要返回值为 0 且形状为(批量大小、隐藏单位数)的 * 附加 * 内存单元格。因此,我们得到了以下状态初始化。 +在初始化函数中,长短期记忆网络的隐藏状态需要返回一个额外的记忆单元,值为0,形状为(批量大小,隐藏单元数)。因此,我们得到以下状态初始化。 ```{.python .input} def init_lstm_state(batch_size, num_hiddens, device): @@ -168,7 +167,7 @@ def init_lstm_state(batch_size, num_hiddens, device): torch.zeros((batch_size, num_hiddens), device=device)) ``` -实际模型的定义就像我们之前讨论的那样:提供三个门和一个辅助记忆细胞。请注意,只有隐藏状态才会传递到输出层。记忆单元 $\mathbf{C}_t$ 不直接参与输出计算。 +实际模型的定义与我们前面讨论的一样:提供三个门和一个额外的记忆单元。请注意,只有隐藏状态会传递到输出层。记忆单元$\mathbf{C}_t$不直接参与输出计算。 ```{.python .input} def lstm(inputs, state, params): @@ -209,7 +208,7 @@ def lstm(inputs, state, params): ### 训练和预测 -让我们通过实例化 :numref:`sec_rnn_scratch` 中引入的 `RNNModelScratch` 类来训练一个 LSTM,就像我们在 :numref:`sec_gru` 中所做的那样训练一个 LSTM。 +让我们通过实例化 :numref:`sec_gru` 中引入的 `RNNModelScratch` 类来训练一个与我们在:numref:`sec_rnn_scratch` 中所实现的相同的LSTM。 ```{.python .input} #@tab all @@ -220,9 +219,9 @@ model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params, d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` -## 简明的实施 +## 简洁实现 -使用高级 API,我们可以直接实例化 `LSTM` 模型。这封装了我们在上面明确说明的所有配置详细信息。代码要快得多,因为它使用编译的运算符而不是 Python 来处理我们之前详细说明的许多细节。 +使用高级API,我们可以直接实例化`LSTM`模型。这封装了我们上面介绍的所有细节。代码的运行速度要快得多,因为它使用编译后的运算符而不是Python来处理我们在前面详细说明的许多细节。 ```{.python .input} lstm_layer = rnn.LSTM(num_hiddens) @@ -239,21 +238,21 @@ model = model.to(device) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` -lstM 是具有非平凡状态控制的原型潜在变量自回归模型。多年来,已经提出了许多变体,例如多层、剩余连接、不同类型的正则化。但是,由于序列的长距离依赖,训练 lstM 和其他序列模型(例如 GRU)的成本相当高。稍后我们将会遇到在某些情况下可以使用的替代模型,例如变形金刚。 +长短期记忆网络是典型的具有非平凡状态控制的隐变量自回归模型。多年来已经提出了其许多变体,例如,多层、残差连接、不同类型的正则化。然而,由于序列的长距离依赖性,训练长短期记忆网络和其他序列模型(例如门控循环单元)的成本是相当高的。在后面的内容中,我们将遇到可在某些情况下使用的替代模型,如Transformer。 -## 摘要 +## 小结 -* LSTM 有三种类型的门:输入门、忘记门和控制信息流的输出门。 -* LSTM 的隐藏层输出包括隐藏状态和内存单元格。只有隐藏状态才会传递到输出层。记忆细胞完全是内部的。 -* lstM 可以缓解渐变的消失和爆炸。 +* 长短期记忆网络有三种类型的门:输入门、遗忘门和控制信息流的输出门。 +* 长短期记忆网络的隐藏层输出包括“隐藏状态”和“记忆单元”。只有隐藏状态会传递到输出层,记忆单元的信息完全储存在内部。 +* 长短期记忆网络可以缓解梯度消失和梯度爆炸。 ## 练习 -1. 调整超参数并分析它们对运行时间、困惑和输出序列的影响。 -1. 你需要如何更改模型以生成正确的单词而不是字符序列? -1. 比较给定隐藏维度的 gRU、LSTM 和常规 RNN 的计算成本。特别注意培训和推理成本。 -1. 由于候选记忆单元通过使用 $\tanh$ 函数确保值范围在 $-1$ 和 $1$ 之间,为什么隐藏状态需要再次使用 $\tanh$ 函数来确保输出值范围在 $-1$ 和 $1$ 之间? -1. 实施 LSTM 模型进行时间序列预测,而不是字符序列预测。 +1. 调整超参数,分析它们对运行时间、困惑度和输出顺序的影响。 +1. 你需要如何更改模型以生成适当的单词,而不是字符序列? +1. 比较给定隐藏维度的门控循环单元、长短期记忆网络和常规循环神经网络的计算成本。要特别注意训练和推理成本。 +1. 既然候选记忆单元通过使用$\tanh$函数来确保值范围在$-1$到$1$之间,那么为什么隐藏状态需要再次使用$\tanh$函数来确保输出值范围在$-1$到$1$之间呢? +1. 为时间序列预测而不是字符序列预测实现LSTM模型。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/343) From 915c22d7e2f6042c6055e549240426839e2fa056 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Tue, 20 Apr 2021 11:58:52 +0800 Subject: [PATCH 049/103] chapter_recurrent-modern/bi-rnn (#726) * chapter_recurrent-modern/bi-rnn * Update bi-rnn.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/bi-rnn.md | 96 +++++++++++++++--------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/chapter_recurrent-modern/bi-rnn.md b/chapter_recurrent-modern/bi-rnn.md index e6b65ca27..0fe479c42 100644 --- a/chapter_recurrent-modern/bi-rnn.md +++ b/chapter_recurrent-modern/bi-rnn.md @@ -1,31 +1,31 @@ # 双向循环神经网络 :label:`sec_bi_rnn` -在顺序学习中,到目前为止,我们假定我们的目标是根据我们目前所看到的情况,例如,在时间序列的背景下或在语言模型的背景下,对下一个输出进行建模。虽然这是一个典型的情况,但这并不是我们唯一可能遇到的情况。为了说明这个问题,请考虑以下三项任务,即在文本序列中填写空白: +在序列学习中,我们以往假设的目标是:到目前为止,在给定所观测的情况下对下一个输出进行建模。例如,在时间序列的上下文中或在语言模型的上下文中。虽然这是一个典型的情况,但这并不是我们可能遇到的唯一情况。为了说明这个问题,考虑以下三个在文本序列中填空的任务: -* 我是 `___`。 -* 我是 `___` 饿了。 -* 我饿了 `___`,我可以吃半头猪。 +* 我 `___`。 +* 我 `___` 饿了。 +* 我 `___` 饿了,我可以吃半头猪。 -根据可用信息的数量,我们可能会用非常不同的词语填写空白,例如 “快乐”、“不” 和 “非常”。显然,短语的末尾(如果可用)传达了关于要选择哪个词的重要信息。无法利用这一点的序列模型在相关任务上的表现不佳。例如,要做好命名实体识别(例如,识别 “绿色” 是指 “绿色先生” 还是指颜色),更长范围的上下文同样至关重要。为了获得解决问题的灵感,让我们绕过概率图形模型。 +根据可获得的信息量,我们可以用不同的词填空,如“很高兴”("happy")、“不”("not")和“非常”("very")。很明显,短语的结尾(如果有的话)传达了重要信息。这些信息关乎到选择哪个词来填空。不能利用这一点的序列模型将在相关任务上表现不佳。例如,如果要做好命名实体识别(例如,识别“Green”指的是“格林先生”还是绿色),更长的范围上下文同样重要。为了获得一些解决问题的灵感,让我们绕道看看概率图模型。 -## 隐藏的马尔可夫模型中的动态编程 +## 隐马尔可夫模型中的动态规划 -本小节用于说明动态编程问题。具体的技术细节对于了解深度学习模型无关紧要,但它们有助于激发人们为什么可能使用深度学习以及为什么可能选择特定的架构。 +这一小节用来说明动态规划问题。具体的技术细节对于理解深度学习模型并不重要,但它们有助于人们思考为什么要使用深度学习,以及为什么要选择特定的结构。 -如果我们想使用概率图形模型来解决问题,我们可以例如设计一个潜在变量模型,如下所示。在任何时候步骤 $t$,我们假设存在一些潜在变量 $h_t$,它控制着我们观察到的 $x_t$ 至 $P(x_t \mid h_t)$ 的排放。此外,任何过渡 $h_t \to h_{t+1}$ 都是由某种状态过渡概率 $P(h_{t+1} \mid h_{t})$ 给出的。然后,这种概率图形模型就是 :numref:`fig_hmm` 中的 * 隐藏的马尔可夫模型 *。 +如果我们想用概率图模型来解决这个问题,我们可以设计一个隐变量模型,如下所示。在任意时间步$t$,我们假设存在某个隐变量$h_t$,通过$P(x_t \mid h_t)$控制我们观测到的发射概率$x_t$。此外,任何转移$h_t \to h_{t+1}$由某个状态转移概率$P(h_{t+1} \mid h_{t})$给出。这个概率图模型就是一个隐马尔可夫模型,如 :numref:`fig_hmm` 所示。 -![A hidden Markov model.](../img/hmm.svg) +![隐马尔可夫模型。](../img/hmm.svg) :label:`fig_hmm` -因此,对于 $T$ 次观测值的序列,我们在观察状态和隐藏状态上有以下联合概率分布: +因此,对于$T$个观测值的序列,我们在观测状态和隐藏状态上具有以下联合概率分布: $$P(x_1, \ldots, x_T, h_1, \ldots, h_T) = \prod_{t=1}^T P(h_t \mid h_{t-1}) P(x_t \mid h_t), \text{ where } P(h_1 \mid h_0) = P(h_1).$$ :eqlabel:`eq_hmm_jointP` -现在假设我们观察到除了 $x_j$ 之外的所有 $x_i$,我们的目标是计算 $P(x_j \mid x_{-j})$,其中 $x_{-j} = (x_1, \ldots, x_{j-1}, x_{j+1}, \ldots, x_{T})$。由于 $P(x_j \mid x_{-j})$ 中没有潜在变量,我们考虑总结 $h_1, \ldots, h_T$ 的所有可能选择组合。如果任何 $h_i$ 可以接受 $k$ 个不同值(有限数量的状态),这意味着我们需要总和超过 $k^T$ 个术语-通常是不可能的任务!幸运的是,有一个优雅的解决方案:* 动态编程 *。 +现在假设我们观测所有$x_i$,除了$x_j$,我们的目标是计算$P(x_j \mid x_{-j})$,其中$x_{-j} = (x_1, \ldots, x_{j-1}, x_{j+1}, \ldots, x_{T})$。由于$P(x_j \mid x_{-j})$中没有隐变量,我们考虑对$h_1, \ldots, h_T$的所有可能选择组合求和。如果任何$h_i$可以接受$k$个不同的值(有限的状态数),这意味着我们需要求$k^T$项的和。而这这通常是不可能的!幸运的是,有一个优雅的解决方案——*动态规划*。 -要了解它是如何工作的,请考虑依次对潜在变量 $h_1, \ldots, h_T$ 进行总结。根据 :eqref:`eq_hmm_jointP`,这种收益率为: +要了解它是如何工作的,请依次考虑对隐变量$h_1, \ldots, h_T$求和。根据:eqref:`eq_hmm_jointP`,将得出: $$\begin{aligned} &P(x_1, \ldots, x_T) \\ @@ -39,13 +39,13 @@ $$\begin{aligned} =& \sum_{h_T} \pi_T(h_T) P(x_T \mid h_T). \end{aligned}$$ -一般来说我们有 * 转发递归 * +一般来说,我们将“前向递归”写为: $$\pi_{t+1}(h_{t+1}) = \sum_{h_t} \pi_t(h_t) P(x_t \mid h_t) P(h_{t+1} \mid h_t).$$ -递归初始化为 $\pi_1(h_1) = P(h_1)$。抽象地说,这可以写成 $\pi_{t+1} = f(\pi_t, x_t)$,其中 $f$ 是一些可学习的功能。这看起来非常像我们到目前为止在 RNN 上下文中讨论的潜在变量模型中的更新方程式! +递归被初始化为$\pi_1(h_1) = P(h_1)$。抽象地说,这可以写成$\pi_{t+1} = f(\pi_t, x_t)$,其中$f$是一些可学习的函数。这看起来非常像我们到目前为止在循环神经网络中讨论的隐变量模型中的更新方程。 -完全类似于向前递归,我们还可以用向后递归对同一组潜在变量进行总和。这产生了: +完全类似于前向递归,我们也可以用后向递归对同一组隐变量求和。这将得到: $$\begin{aligned} & P(x_1, \ldots, x_T) \\ @@ -59,31 +59,31 @@ $$\begin{aligned} =& \sum_{h_1} P(h_1) P(x_1 \mid h_1)\rho_{1}(h_{1}). \end{aligned}$$ -因此,我们可以将 * 向后递归 * 写为 +因此,我们可以将“后向递归”写为: $$\rho_{t-1}(h_{t-1})= \sum_{h_{t}} P(h_{t} \mid h_{t-1}) P(x_{t} \mid h_{t}) \rho_{t}(h_{t}),$$ -初始化 $\rho_T(h_T) = 1$。前向递归和向后递归都允许我们在 $\mathcal{O}(kT)$(线性)时间内对 $(h_1, \ldots, h_T)$ 的所有值进行总和超过 $T$ 个潜在变量,而不是按指数时间。这是使用图形模型进行概率推理的巨大好处之一。这也是一般消息传递算法 :cite:`Aji.McEliece.2000` 的一个非常特殊的实例。结合向前和向后递归,我们能够计算 +初始化$\rho_T(h_T) = 1$。前向和后向递归都允许我们在$\mathcal{O}(kT)$(线性)时间内对$(h_1, \ldots, h_T)$的所有值(而不是指数时间)求和$T$个隐变量。这是使用图模型进行概率推理的最大好处之一。它也是通用消息传递算法 :cite:`Aji.McEliece.2000` 的一个非常特殊的例子。结合前向和后向递归,我们能够计算 $$P(x_j \mid x_{-j}) \propto \sum_{h_j} \pi_j(h_j) \rho_j(h_j) P(x_j \mid h_j).$$ -请注意,抽象地说,向后递归可以写成 $\rho_{t-1} = g(\rho_t, x_t)$,其中 $g$ 是一个可学习的函数。再次,这看起来非常像一个更新方程式,只是向后运行,不像我们到目前为止在 rnN 中看到的。事实上,隐藏的马尔可夫模型受益于了解未来可用的数据。信号处理科学家区分了解和不知道未来观测作为插值法与外推法的两种情况。有关更多详细信息,请参阅本书中有关连续蒙特卡洛算法的入门章节 :cite:`Doucet.De-Freitas.Gordon.2001`。 +作为一个抽象,可以写在递归中。同样,这看起来非常像一个更新方程,只是后向运行,不像我们在循环神经网络中看到的那样。实际上,隐马尔可夫模型受益于知道未来数据何时可用。信号处理科学家将知道和不知道未来观测的两种情况区分为插值和外推。有关更多详细信息,请参阅一本书:cite:`Doucet.De-Freitas.Gordon.2001`。 ## 双向模型 -如果我们希望在 RNN 中有一种能够提供与隐藏马尔科夫模型相似的预测能力的机制,我们需要修改迄今为止看到的 RNN 设计。幸运的是,这在概念上很容易。我们不是仅在前进模式下从第一个令牌开始运行 RNN,而是从后一个令牌从后到前运行的最后一个令牌启动另一个令牌。 -*双向 rnNS* 添加一个隐藏层,向后传递信息,以便更灵活地处理此类信息。 :numref:`fig_birnn` 说明了带有单个隐藏层的双向 RNN 的体系结构。 +如果我们想在循环神经网络中有一种机制,提供与隐马尔可夫模型类似的前瞻能力,我们需要修改循环神经网络设计。幸运的是,这在概念上很容易。我们从最后一个标记开始从后向前运行循环神经网络,而不是只在前向模式下从第一个标记开始运行循环神经网络。 +*双向循环神经网络*添加了反向传递信息的隐藏层,以更灵活地处理此类信息。:numref:`fig_birnn`具有单个隐藏层的双向循环神经网络的结构。 -![Architecture of a bidirectional RNN.](../img/birnn.svg) +![双向循环神经网络的结构。](../img/birnn.svg) :label:`fig_birnn` -事实上,这与隐藏的马尔可夫模型动态编程中的向前和向后递归并不太相似。主要区别在于,在前面的情况下,这些方程式具有特定的统计含义。现在它们没有如此容易访问的解释,我们可以将它们视为通用和可学习的函数。这种转变体现了指导现代深度网络设计的许多原则:首先,使用经典统计模型的功能依赖关系类型,然后以通用形式对它们进行参数化。 +事实上,这与隐马尔可夫模型动态规划中的前向和后向递归没有太大区别。主要区别在于,在隐马尔可夫模型中的方程具有特定的统计意义。双向循环神经网络没有这样容易理解的解释,我们只能把它们当作通用的、可学习的函数。这一转变集中体现了指导现代深度网络设计的一些原则:首先,使用经典统计模型的函数依赖类型,然后将其参数化为通用形式。 ### 定义 -:cite:`Schuster.Paliwal.1997` 推出了双向 RNN。有关各种体系结构的详细讨论,另请参阅文章 :cite:`Graves.Schmidhuber.2005`。让我们来看看这样一个网络的细节。 +双向循环神经网络由 :cite:`Schuster.Paliwal.1997` 提出。有关各种结构的详细讨论,请参阅:cite:`Graves.Schmidhuber.2005`。让我们看看这样一个网络的具体情况。 -对于任何时间步骤 $t$,给定小批量输入 $\mathbf{X}_t \in \mathbb{R}^{n \times d}$(示例数:$n$,每个示例中的输入数量:$d$),让隐藏层激活函数为 $\phi$。在双向架构中,我们假设此时间步长的向前和向后隐藏状态分别为 $\overrightarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$ 和 $\overleftarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$,其中 $h$ 是隐藏单位的数量。向前和向后隐藏状态更新如下: +对于任意时间步$t$,给定一个小批量输入$\mathbf{X}_t \in \mathbb{R}^{n \times d}$(样本数:$n$,每个示例中的输入数:$d$),并且使隐藏层激活函数为$\phi$。在双向结构中,我们设该时间步的前向和反向隐藏状态分别为$\overrightarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$和$\overleftarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h}$,其中$h$是隐藏单元的数目。前向和反向隐藏状态更新如下: $$ \begin{aligned} @@ -92,25 +92,25 @@ $$ \end{aligned} $$ -其中权重 $\mathbf{W}_{xh}^{(f)} \in \mathbb{R}^{d \times h}, \mathbf{W}_{hh}^{(f)} \in \mathbb{R}^{h \times h}, \mathbf{W}_{xh}^{(b)} \in \mathbb{R}^{d \times h}, \text{ and } \mathbf{W}_{hh}^{(b)} \in \mathbb{R}^{h \times h}$ 和偏置 $\mathbf{b}_h^{(f)} \in \mathbb{R}^{1 \times h} \text{ and } \mathbf{b}_h^{(b)} \in \mathbb{R}^{1 \times h}$ 都是模型参数。 +其中,权重$\mathbf{W}_{xh}^{(f)} \in \mathbb{R}^{d \times h}, \mathbf{W}_{hh}^{(f)} \in \mathbb{R}^{h \times h}, \mathbf{W}_{xh}^{(b)} \in \mathbb{R}^{d \times h}, \text{ and } \mathbf{W}_{hh}^{(b)} \in \mathbb{R}^{h \times h}$和偏置$\mathbf{b}_h^{(f)} \in \mathbb{R}^{1 \times h} \text{ and } \mathbf{b}_h^{(b)} \in \mathbb{R}^{1 \times h}$都是模型参数。 -接下来,我们连接向前和向后隐藏状态 $\overrightarrow{\mathbf{H}}_t$ 和 $\overleftarrow{\mathbf{H}}_t$ 以获得隐藏状态 $\mathbf{H}_t \in \mathbb{R}^{n \times 2h}$ 进入输出层。在具有多个隐藏层的深双向 RNN 中,此类信息将作为 * 输入 * 传递到下一个双向层。最后,输出层计算输出 $\mathbf{O}_t \in \mathbb{R}^{n \times q}$(输出数:$q$): +接下来,我们连结前向和反向隐藏状态$\overrightarrow{\mathbf{H}}_t$和$\overleftarrow{\mathbf{H}}_t$以获得要送入输出层的隐藏状态$\mathbf{H}_t \in \mathbb{R}^{n \times 2h}$。在具有多个隐藏层的深层双向循环神经网络中,该信息作为输入传递到下一个双向层。最后,输出层计算输出$\mathbf{O}_t \in \mathbb{R}^{n \times q}$(输出数:$q$): $$\mathbf{O}_t = \mathbf{H}_t \mathbf{W}_{hq} + \mathbf{b}_q.$$ -在这里,权重矩阵 $\mathbf{W}_{hq} \in \mathbb{R}^{2h \times q}$ 和偏置 $\mathbf{b}_q \in \mathbb{R}^{1 \times q}$ 是输出层的模型参数。事实上,这两个方向可能有不同数量的隐藏单位。 +这里,权重矩阵$\mathbf{W}_{hq} \in \mathbb{R}^{2h \times q}$和偏置$\mathbf{b}_q \in \mathbb{R}^{1 \times q}$是输出层的模型参数。实际上,这两个方向可以有不同数量的隐藏单元。 -### 计算成本和应用 +### 计算成本及其应用 -双向 RNN 的主要特征之一是使用序列两端的信息来估计输出。也就是说,我们使用来自未来和过去的观测的信息来预测当前的观测。在下一个令牌预测的情况下,这不是我们想要的。毕竟,在预测下一个令牌时,我们并不奢侈地知道下一个令牌。因此,如果我们天真地使用双向 RNN,我们将无法获得很好的准确性:在训练期间,我们有过去和未来的数据来估计现在。在测试期间,我们只有过去的数据,因此准确性差。我们将在下面的实验中说明这一点。 +双向循环神经网络的一个关键特性是,使用来自序列两端的信息来估计输出。也就是说,我们使用来自未来和过去观测的信息来预测当前的观测。在预测下一个标记的情况下,这并不是我们想要的。毕竟,在预测下一个标记时,我们无法知道下一个标记。因此,如果我们天真地使用双向循环神经网络,我们将不会得到很好的准确性:在训练期间,我们利用了过去和未来的数据来估计现在。而在测试期间,我们只有过去的数据,因此准确性较差。我们将在下面的实验中说明这一点。 -为了加重伤害的侮辱,双向 RNN 也非常缓慢。造成这种情况的主要原因是,正向传播需要双向层的向前和向后递归,反向传播取决于正向传播的结果。因此,渐变将有一个非常长的依赖链。 +另一个严重问题是,双向循环神经网络非常慢。其主要原因是前向传播需要在双向层中进行前向和后向递归,并且反向传播依赖于前向传播的结果。因此,梯度将有一个非常长的依赖链。 -实际上,双向图层的使用非常少,而且仅用于少数应用程序,例如填写缺失的词、注释令牌(例如,用于命名实体识别)以及作为序列处理管道中一个步骤批发的编码序列(例如,用于机器翻译)。在 :numref:`sec_bert` 和 :numref:`sec_sentiment_rnn` 中,我们将介绍如何使用双向 RNN 对文本序列进行编码。 +实际上,双向层的使用非常少,并且应用于部分场合。例如,填充缺失的单词、标记注释(例如,用于命名实体识别)以及作为序列处理工作流中的一个步骤对序列进行编码(例如,用于机器翻译)。在 :numref:`sec_bert` 和 :numref:`sec_sentiment_rnn` 中,我们将介绍如何使用双向循环神经网络编码文本序列。 -## 为错误的应用程序训练双向 RNN +## 错误的应用 -如果我们忽略所有关于双向 RNN 使用过去和未来数据而只是将其应用于语言模型这一事实的建议,我们将得到可以接受的困惑度估计。尽管如此,如下面的实验所示,该模型预测未来代币的能力受到严重损害。尽管有合理的困惑,但即使在多次迭代之后,它也只会产生 gibberish。我们将下面的代码作为防止在错误的上下文中使用它们的警告示例。 +如果我们忽略了所有关于双向循环神经网络使用过去和未来数据的建议,而只是将其应用于语言模型,我们将得到具有可接受困惑度的估计。尽管如此,该模型预测未来标记的能力仍受到严重影响,如下面的实验所示。尽管存在合理的困惑度。但即使经过多次迭代,它也只会产生乱码。我们将下面的代码作为警告的例子,以防在错误的环境中使用它们。 ```{.python .input} from d2l import mxnet as d2l @@ -118,14 +118,14 @@ from mxnet import npx from mxnet.gluon import rnn npx.set_np() -# Load data +# 加载数据 batch_size, num_steps, device = 32, 35, d2l.try_gpu() train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) -# Define the bidirectional LSTM model by setting `bidirectional=True` +# 通过设置'bidirective=True'来定义双向LSTM模型 vocab_size, num_hiddens, num_layers = len(vocab), 256, 2 lstm_layer = rnn.LSTM(num_hiddens, num_layers, bidirectional=True) model = d2l.RNNModel(lstm_layer, len(vocab)) -# Train the model +# 训练模型 num_epochs, lr = 500, 1 d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` @@ -136,34 +136,34 @@ from d2l import torch as d2l import torch from torch import nn -# Load data +# 加载数据 batch_size, num_steps, device = 32, 35, d2l.try_gpu() train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) -# Define the bidirectional LSTM model by setting `bidirectional=True` +# 通过设置'bidirective=True'来定义双向LSTM模型 vocab_size, num_hiddens, num_layers = len(vocab), 256, 2 num_inputs = vocab_size lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, bidirectional=True) model = d2l.RNNModel(lstm_layer, len(vocab)) model = model.to(device) -# Train the model +# 训练模型 num_epochs, lr = 500, 1 d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` -由于上述原因,产出显然不能令人满意。有关更有效地使用双向 RNN 的讨论,请参阅 :numref:`sec_sentiment_rnn` 中的情绪分析应用程序。 +由于上述原因,结果显然不令人满意。有关更有效地使用双向循环神经网络的讨论,请参阅 :numref:`sec_sentiment_rnn` 中的情感分类应用。 -## 摘要 +## 小结 -* 在双向 RNN 中,每个时间步的隐藏状态由当前时间步长之前和之后的数据同时确定。 -* 在概率图形模型中,双向 RNN 与前向后算法有惊人的相似之处。 -* 双向 RNN 对于双向上下文的序列编码和观测值的估计非常有用。 -* 由于梯度链长,双向 RNN 的训练成本非常高。 +* 在双向循环神经网络中,每个时间步的隐藏状态由当前时间步前后信息同时决定。 +* 双向循环神经网络与概率图形模型中的“前向-后向”算法有着惊人的相似性。 +* 双向循环神经网络主要用于序列编码和给定双向上下文的观测估计。 +* 由于梯度链更长,双向循环神经网络的训练成本非常高。 ## 练习 -1. 如果不同的方向使用不同数量的隐藏单位,$\mathbf{H}_t$ 的形状将如何改变? -1. 设计一个带有多个隐藏层的双向 RNN。 -1. Polysemy 在自然语言中很常见。例如,“银行” 一词在 “我去银行存款” 和 “我去银行坐下来” 的上下文中有不同的含义。我们如何设计一个神经网络模型,以便在给定上下文序列和一个单词的情况下,返回上下文中单词的矢量表示形式?处理多重体学时首选哪种类型的神经体系结构? +1. 如果不同方向使用不同数量的隐藏单位,$\mathbf{H_t}$的形状会发生怎样的变化? +1. 设计一个具有多个隐藏层的双向循环神经网络。 +1. 一词多义在自然语言中很常见。例如,“bank”一词在“i went to the bank to deposit cash”和“i went to the bank to sit down”中有不同的含义。我们如何设计一个神经网络模型,使其在给定上下文序列和单词的情况下,返回该单词在上下文中的向量表示?哪种类型的神经结构更适合处理一词多义? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/339) From 52f2877a1e9b737c4bf94e76876741eed9c8e2f4 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Wed, 21 Apr 2021 00:23:43 +0800 Subject: [PATCH 050/103] chapter_recurrent-modern/deep-rnn (#731) * chapter_recurrent-modern/deep-rnn * Update deep-rnn.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/deep-rnn.md | 50 ++++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/chapter_recurrent-modern/deep-rnn.md b/chapter_recurrent-modern/deep-rnn.md index f20f97fc3..77de616f8 100644 --- a/chapter_recurrent-modern/deep-rnn.md +++ b/chapter_recurrent-modern/deep-rnn.md @@ -1,38 +1,38 @@ -# 深度反复神经网络 +# 深层循环神经网络 :label:`sec_deep_rnn` -到目前为止,我们只讨论了单向隐藏层的 rnN。在其中,潜在变量和观测值相互作用的特定功能形式是相当任意的。只要我们有足够的灵活性来模拟不同类型的互动,这不是一个大问题。但是,只要单一层,这可能是相当具有挑战性的。对于线性模型,我们通过添加更多层来解决此问题。在 rnN 中,这有点棘手,因为我们首先需要决定如何以及在哪里添加额外的非线性。 +到目前为止,我们只讨论了一个单向隐藏层的RNN。其中,隐变量和观测值如何相互作用的具体函数形式是相当任意的。只要我们有足够的灵活性来建模不同类型的交互,这就不是一个大问题。然而,对于一个单隐藏层来说,这可能是相当具有挑战性的。在线性模型的情况下,我们通过添加更多的层来解决这个问题。在循环神经网络中,这有点棘手。因为我们首先需要决定如何以及在哪里添加额外的非线性函数。 -事实上,我们可以将多层 RNN 堆叠在彼此之上。由于几个简单层的组合,这导致了灵活的机制。特别是,数据在堆栈的不同级别可能是相关的。例如,我们可能希望保持有关金融市场状况(熊市或牛市)的高级数据,而在较低水平上,我们只记录短期时间动态。 +事实上,我们可以将多层循环神经网络堆叠在一起。通过几个简单的层的组合,产生了一个灵活的机制。特别是,数据可能与堆叠的不同层级有关。例如,我们可能希望保持有关金融市场状况(熊市或牛市)的高级数据可用,而在较低级别,我们只记录较短期的时间动态。 -除了上述所有抽象讨论之外,通过查看 :numref:`fig_deep_rnn` 来理解我们感兴趣的模型系列可能是最容易的。它描述了一个带有 $L$ 个隐藏图层的深度 RNN。每个隐藏状态都会连续传递到当前图层的下一个时间步长和下一个图层的当前时间步长。 +除了以上所有的抽象讨论之外,通过 :numref:`fig_deep_rnn` 可能更容易理解我们感兴趣的模型。它描述了一个具有$L$个隐藏层的深层循环神经网络。每个隐藏状态都连续传递到当前层的下一个时间步和下一层的当前时间步。 -![Architecture of a deep RNN.](../img/deep-rnn.svg) +![深层循环神经网络的结构。](../img/deep-rnn.svg) :label:`fig_deep_rnn` -## 功能依赖 +## 函数依赖关系 -我们可以在 :numref:`fig_deep_rnn` 中描述的 $L$ 隐藏层的深层架构中正式化功能依赖关系。我们以下讨论主要集中在香草 RNN 模型上,但它也适用于其他序列模型。 +我们可以在 :numref:`fig_deep_rnn` 中描述的$L$个隐藏层的深层结构中的函数依赖关系形式化。我们下面的讨论主要集中在经典循环神经网络模型上,但它也适用于其他序列模型。 -假设我们在时间步骤 $t$ 时有一个小批量输入 $\mathbf{X}_t \in \mathbb{R}^{n \times d}$(示例数:$n$,每个示例中的输入数量:$d$)。同时,让 $l^\mathrm{th}$ 隐藏层 ($l=1,\ldots,L$) 的隐藏状态为 $\mathbf{H}_t^{(l)} \in \mathbb{R}^{n \times h}$(隐藏单位数:$h$),输出层变量为 $\mathbf{O}_t \in \mathbb{R}^{n \times q}$(输出数量:$q$)。设置 $\mathbf{H}_t^{(0)} = \mathbf{X}_t$,使用激活功能 $\phi_l$ 的 $l^\mathrm{th}$ 隐藏层的隐藏状态表示如下: +假设我们在时间步$t$有一个小批量输入$\mathbf{X}_t \in \mathbb{R}^{n \times d}$(样本数:$n$,每个样本中的输入数:$d$)。同时,将$l^\mathrm{th}$隐藏层($l=1,\ldots,L$)的隐藏状态设为$\mathbf{H}_t^{(l)} \in \mathbb{R}^{n \times h}$(隐藏单元数:$h$),输出层变量设为$\mathbf{O}_t \in \mathbb{R}^{n \times q}$(输出数:$q$)。设置$\mathbf{H}_t^{(0)} = \mathbf{X}_t$,使用激活函数$\phi_l$的第$l$个隐藏层的隐藏状态表示如下: $$\mathbf{H}_t^{(l)} = \phi_l(\mathbf{H}_t^{(l-1)} \mathbf{W}_{xh}^{(l)} + \mathbf{H}_{t-1}^{(l)} \mathbf{W}_{hh}^{(l)} + \mathbf{b}_h^{(l)}),$$ :eqlabel:`eq_deep_rnn_H` -其中权重 $\mathbf{W}_{xh}^{(l)} \in \mathbb{R}^{h \times h}$ 和 $\mathbf{W}_{hh}^{(l)} \in \mathbb{R}^{h \times h}$ 以及偏置 $\mathbf{b}_h^{(l)} \in \mathbb{R}^{1 \times h}$ 是 $l^\mathrm{th}$ 隐藏层的模型参数。 +其中,权重$\mathbf{W}_{xh}^{(l)} \in \mathbb{R}^{h \times h}$和$\mathbf{W}_{hh}^{(l)} \in \mathbb{R}^{h \times h}$以及偏置$\mathbf{b}_h^{(l)} \in \mathbb{R}^{1 \times h}$是第$l$个隐藏层的模型参数。 -最后,输出图层的计算仅基于最终 $L^\mathrm{th}$ 隐藏层的隐藏状态: +最后,输出层的计算仅基于最终第$l$个隐藏层的隐藏状态: $$\mathbf{O}_t = \mathbf{H}_t^{(L)} \mathbf{W}_{hq} + \mathbf{b}_q,$$ -其中权重 $\mathbf{W}_{hq} \in \mathbb{R}^{h \times q}$ 和偏置 $\mathbf{b}_q \in \mathbb{R}^{1 \times q}$ 是输出层的模型参数。 +其中,权重$\mathbf{W}_{hq} \in \mathbb{R}^{h \times q}$和偏置$\mathbf{b}_q \in \mathbb{R}^{1 \times q}$是输出层的模型参数。 -与 MLP 一样,隐藏层的数量 $L$ 和隐藏单位的数量 $h$ 都是超参数。换句话说,我们可以调整或指定它们。此外,通过将 :eqref:`eq_deep_rnn_H` 中的隐藏状态计算替换为 GRU 或 LSTM 的隐藏状态计算,我们可以轻松获得深度门控 RNN。 +与多层感知机一样,隐藏层的数目$L$和隐藏单元的数目$h$是超参数。换句话说,它们可以由我们调整或指定。另外,用门控循环单元或长短期记忆网络的隐藏状态计算代替 :eqref:`eq_deep_rnn_H` 的隐藏状态计算,可以很容易地得深层门控循环神经网络。 -## 简明的实施 +## 简洁实现 -幸运的是,实施多层 RNN 所需的许多物流细节都可以在高级 API 中随时获得。为了简单起见,我们只使用这些内置功能来说明实现。让我们以 LSTM 模型为例。该代码与我们之前在 :numref:`sec_lstm` 中使用的代码非常相似。事实上,唯一的区别是我们明确指定图层的数量,而不是选择单个图层的默认值。像往常一样,我们首先加载数据集。 +幸运的是,实现多层循环神经网络所需的许多细节在高级API中都是现成的。为了简单起见,我们仅说明使用此类内置函数的实现。让我们以长短期记忆网络模型为例。该代码与我们之前在 :numref:`sec_lstm` 中使用的代码非常相似。实际上,唯一的区别是我们显式地指定了层的数量,而不是单个层的默认值。像往常一样,我们从加载数据集开始。 ```{.python .input} from d2l import mxnet as d2l @@ -54,7 +54,7 @@ batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) ``` -选择超参数之类的架构决策与 :numref:`sec_lstm` 的架构决策非常相似。我们选择的输入和输出数量与我们有不同的令牌相同,即 `vocab_size`。隐藏单位的数量仍然是 256 个。唯一的区别是,我们现在通过指定值 `num_layers` 来选择一个不平凡的隐藏图层。 +选择超参数等结构决策与 :numref:`sec_lstm` 的决策非常相似。我们选择相同数量的输入和输出,因为我们有不同的标记,即`vocab_size`。隐藏单元的数量仍然是256。唯一的区别是,我们现在通过指定`num_layers`的值来指定隐藏层数。 ```{.python .input} vocab_size, num_hiddens, num_layers = len(vocab), 256, 2 @@ -73,9 +73,9 @@ model = d2l.RNNModel(lstm_layer, len(vocab)) model = model.to(device) ``` -## 训练和预测 +## 训练与预测 -从现在起,我们使用 LSTM 模型实例化两层,这个相当复杂的体系结构大大减慢了训练速度。 +因为现在我们用长短期记忆网络模型实例化了两个层,这个相当复杂的结构大大降低了训练速度。 ```{.python .input} #@tab all @@ -83,18 +83,18 @@ num_epochs, lr = 500, 2 d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) ``` -## 摘要 +## 小结 -* 在深度 RNN 中,隐藏状态信息将传递给当前图层的下一个时间步长和下一层的当前时间步长。 -* 有许多不同风格的深 RNN,例如 lstM、gRU 或香草 RNN。方便地,这些模型都作为深度学习框架的高级 API 的一部分提供。 -* 模型的初始化需要小心。总体而言,深度 RNN 需要大量的工作(例如学习率和裁剪)才能确保适当的融合。 +* 在深层循环神经网络中,隐藏状态信息被传递到当前层的下一时间步和下一层的当前时间步。 +* 有许多不同风格的深层循环神经网络,如长短期记忆网络、门控循环单元、或经典循环神经网络。这些模型在深度学习框架的高级API中都有涵盖。 +* 总体而言,深层循环神经网络需要大量的工作(如学习率和修剪)来确保适当的收敛,模型的初始化也需要谨慎。 ## 练习 -1. 尝试使用我们在 :numref:`sec_rnn_scratch` 中讨论的单层实现从头开始实施双层 RNN。 -2. 用 GRU 替换 LSTM,然后比较准确性和训练速度。 -3. 增加培训数据以包括多本书。在困惑程度上你能进行多低? -4. 在建模文本时,您是否想合并不同作者的来源?为什么这是个好主意?可能会出什么问题? +1. 尝试使用我们在 :numref:`sec_rnn_scratch` 中讨论的单层实现两层循环神经网络的从零开始实现。 +2. 用门控循环单元替换长短期记忆网络,比较精确度和训练速度。 +3. 增加训练数据以包含多本书。你的困惑度能降到多低? +4. 在为文本建模时,是否要合并不同作者的来源?为什么这是个好主意?会出什么问题? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/340) From 7431b209bc6cfa72170dcfde87c8f426d5e2a713 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Wed, 21 Apr 2021 00:37:52 +0800 Subject: [PATCH 051/103] d2l (#757) --- d2l/mxnet.py | 2303 ++++++++++++++++++++++++++++++++++++++++++--- d2l/tensorflow.py | 552 +++++++---- d2l/torch.py | 2146 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 4531 insertions(+), 470 deletions(-) diff --git a/d2l/mxnet.py b/d2l/mxnet.py index f4f2ad24c..21f34ce8c 100644 --- a/d2l/mxnet.py +++ b/d2l/mxnet.py @@ -4,22 +4,21 @@ # Defined in file: ./chapter_preface/index.md import collections -import hashlib +from collections import defaultdict +from IPython import display import math +from matplotlib import pyplot as plt import os +import pandas as pd import random import re import shutil import sys import tarfile import time -import zipfile -from collections import defaultdict -import pandas as pd import requests -from IPython import display -from matplotlib import pyplot as plt - +import zipfile +import hashlib d2l = sys.modules[__name__] @@ -30,20 +29,20 @@ # Defined in file: ./chapter_preliminaries/calculus.md def use_svg_display(): - """使用svg格式在Jupyter中显示绘图。""" + """Use the svg format to display a plot in Jupyter.""" display.set_matplotlib_formats('svg') # Defined in file: ./chapter_preliminaries/calculus.md def set_figsize(figsize=(3.5, 2.5)): - """设置matplotlib的图表大小。""" + """Set the figure size for matplotlib.""" use_svg_display() d2l.plt.rcParams['figure.figsize'] = figsize # Defined in file: ./chapter_preliminaries/calculus.md def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): - """设置matplotlib的轴。""" + """Set the axes for matplotlib.""" axes.set_xlabel(xlabel) axes.set_ylabel(ylabel) axes.set_xscale(xscale) @@ -59,17 +58,17 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): - """绘制数据点。""" + """Plot data points.""" if legend is None: legend = [] set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # 如果 `X` 有一个轴,输出True + # Return True if `X` (tensor or list) has 1 axis def has_one_axis(X): - return (hasattr(X, "ndim") and X.ndim == 1 or - isinstance(X, list) and not hasattr(X[0], "__len__")) + return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) + and not hasattr(X[0], "__len__")) if has_one_axis(X): X = [X] @@ -90,36 +89,36 @@ def has_one_axis(X): # Defined in file: ./chapter_linear-networks/linear-regression.md class Timer: - """记录多次运行时间。""" + """Record multiple running times.""" def __init__(self): self.times = [] self.start() def start(self): - """启动计时器。""" + """Start the timer.""" self.tik = time.time() def stop(self): - """停止计时器并将时间记录在列表中。""" + """Stop the timer and record the time in a list.""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): - """返回平均时间。""" + """Return the average time.""" return sum(self.times) / len(self.times) def sum(self): - """返回时间总和。""" + """Return the sum of time.""" return sum(self.times) def cumsum(self): - """返回累计时间。""" + """Return the accumulated time.""" return np.array(self.times).cumsum().tolist() # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def synthetic_data(w, b, num_examples): - """生成 y = Xw + b + 噪声。""" + """Generate y = Xw + b + noise.""" X = d2l.normal(0, 1, (num_examples, len(w))) y = d2l.matmul(X, w) + b y += d2l.normal(0, 0.01, y.shape) @@ -128,42 +127,41 @@ def synthetic_data(w, b, num_examples): # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def linreg(X, w, b): - """线性回归模型。""" + """The linear regression model.""" return d2l.matmul(X, w) + b # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def squared_loss(y_hat, y): - """均方损失。""" - return (y_hat - d2l.reshape(y, y_hat.shape))**2 / 2 + """Squared loss.""" + return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def sgd(params, lr, batch_size): - """小批量随机梯度下降。""" + """Minibatch stochastic gradient descent.""" for param in params: param[:] = param - lr * param.grad / batch_size # Defined in file: ./chapter_linear-networks/linear-regression-concise.md def load_array(data_arrays, batch_size, is_train=True): - """构造一个Gluon数据迭代器。""" + """Construct a Gluon data iterator.""" dataset = gluon.data.ArrayDataset(*data_arrays) return gluon.data.DataLoader(dataset, batch_size, shuffle=is_train) # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def get_fashion_mnist_labels(labels): - """返回Fashion-MNIST数据集的文本标签。""" - text_labels = [ - 't-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', - 'sneaker', 'bag', 'ankle boot'] + """Return text labels for the Fashion-MNIST dataset.""" + text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', + 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot'] return [text_labels[int(i)] for i in labels] # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): - """绘制图像列表。""" + """Plot a list of images.""" figsize = (num_cols * scale, num_rows * scale) _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize) axes = axes.flatten() @@ -178,13 +176,13 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def get_dataloader_workers(): - """在非Windows的平台上,使用4个进程来读取的数据。""" + """Use 4 processes to read the data except for Windows.""" return 0 if sys.platform.startswith('win') else 4 # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def load_data_fashion_mnist(batch_size, resize=None): - """下载Fashion-MNIST数据集,然后将其加载到内存中。""" + """Download the Fashion-MNIST dataset and then load it into memory.""" dataset = gluon.data.vision trans = [dataset.transforms.ToTensor()] if resize: @@ -200,17 +198,17 @@ def load_data_fashion_mnist(batch_size, resize=None): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def accuracy(y_hat, y): - """计算预测正确的数量。""" + """Compute the number of correct predictions.""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: - y_hat = d2l.argmax(y_hat, axis=1) + y_hat = d2l.argmax(y_hat, axis=1) cmp = d2l.astype(y_hat, y.dtype) == y return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def evaluate_accuracy(net, data_iter): - """计算在指定数据集上模型的精度。""" - metric = Accumulator(2) # 正确预测数、预测总数 + """Compute the accuracy for a model on a dataset.""" + metric = Accumulator(2) # No. of correct predictions, no. of predictions for X, y in data_iter: metric.add(accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] @@ -218,7 +216,7 @@ def evaluate_accuracy(net, data_iter): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Accumulator: - """在`n`个变量上累加。""" + """For accumulating sums over `n` variables.""" def __init__(self, n): self.data = [0.0] * n @@ -234,44 +232,44 @@ def __getitem__(self, idx): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def train_epoch_ch3(net, train_iter, loss, updater): - """训练模型一个迭代周期(定义见第3章)。""" - # 训练损失总和、训练准确度总和、样本数 + """Train a model within one epoch (defined in Chapter 3).""" + # Sum of training loss, sum of training accuracy, no. of examples metric = Accumulator(3) if isinstance(updater, gluon.Trainer): updater = updater.step for X, y in train_iter: - # 计算梯度并更新参数 + # Compute gradients and update parameters with autograd.record(): y_hat = net(X) l = loss(y_hat, y) l.backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.size) - # 返回训练损失和训练准确率 + # Return training loss and training accuracy return metric[0] / metric[2], metric[1] / metric[2] # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Animator: - """在动画中绘制数据。""" + """For plotting data in animation.""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): - # 增量地绘制多条线 + # Incrementally plot multiple lines if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: - self.axes = [self.axes,] - # 使用lambda函数捕获参数 - self.config_axes = lambda: d2l.set_axes(self.axes[ - 0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) + self.axes = [self.axes, ] + # Use a lambda function to capture arguments + self.config_axes = lambda: d2l.set_axes( + self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): - # 向图表中添加多个数据点 + # Add multiple data points into the figure if not hasattr(y, "__len__"): y = [y] n = len(y) @@ -295,7 +293,7 @@ def add(self, x, y): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): - """训练模型(定义见第3章)。""" + """Train a model (defined in Chapter 3).""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train loss', 'train acc', 'test acc']) for epoch in range(num_epochs): @@ -310,20 +308,20 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def predict_ch3(net, test_iter, n=6): - """预测标签(定义见第3章)。""" + """Predict labels (defined in Chapter 3).""" for X, y in test_iter: break trues = d2l.get_fashion_mnist_labels(y) preds = d2l.get_fashion_mnist_labels(d2l.argmax(net(X), axis=1)) - titles = [true + '\n' + pred for true, pred in zip(trues, preds)] - d2l.show_images(d2l.reshape(X[0:n], (n, 28, 28)), 1, n, - titles=titles[0:n]) + titles = [true +'\n' + pred for true, pred in zip(trues, preds)] + d2l.show_images( + d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) # Defined in file: ./chapter_multilayer-perceptrons/underfit-overfit.md def evaluate_loss(net, data_iter, loss): - """评估给定数据集上模型的损失。""" - metric = d2l.Accumulator(2) # 损失的总和, 样本数量 + """Evaluate the loss of a model on the given dataset.""" + metric = d2l.Accumulator(2) # Sum of losses, no. of examples for X, y in data_iter: l = loss(net(X), y) metric.add(d2l.reduce_sum(l), d2l.size(l)) @@ -337,8 +335,8 @@ def evaluate_loss(net, data_iter, loss): # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md def download(name, cache_dir=os.path.join('..', 'data')): - """下载一个DATA_HUB中的文件,返回本地文件名。""" - assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}." + """Download a file inserted into DATA_HUB, return the local filename.""" + assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}." url, sha1_hash = DATA_HUB[name] os.makedirs(cache_dir, exist_ok=True) fname = os.path.join(cache_dir, url.split('/')[-1]) @@ -352,7 +350,7 @@ def download(name, cache_dir=os.path.join('..', 'data')): sha1.update(data) if sha1.hexdigest() == sha1_hash: return fname # Hit cache - print(f'正在从{url}下载{fname}...') + print(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True, verify=True) with open(fname, 'wb') as f: f.write(r.content) @@ -361,7 +359,7 @@ def download(name, cache_dir=os.path.join('..', 'data')): # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md def download_extract(name, folder=None): - """下载并解压zip/tar文件。""" + """Download and extract a zip/tar file.""" fname = download(name) base_dir = os.path.dirname(fname) data_dir, ext = os.path.splitext(fname) @@ -370,52 +368,55 @@ def download_extract(name, folder=None): elif ext in ('.tar', '.gz'): fp = tarfile.open(fname, 'r') else: - assert False, '只有zip/tar文件可以被解压缩。' + assert False, 'Only zip/tar files can be extracted.' fp.extractall(base_dir) return os.path.join(base_dir, folder) if folder else data_dir def download_all(): - """下载DATA_HUB中的所有文件。""" + """Download all files in the DATA_HUB.""" for name in DATA_HUB: download(name) # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md -DATA_HUB['kaggle_house_train'] = (DATA_URL + 'kaggle_house_pred_train.csv', - '585e9cc93e70b39160e7921475f9bcd7d31219ce') +DATA_HUB['kaggle_house_train'] = ( + DATA_URL + 'kaggle_house_pred_train.csv', + '585e9cc93e70b39160e7921475f9bcd7d31219ce') -DATA_HUB['kaggle_house_test'] = (DATA_URL + 'kaggle_house_pred_test.csv', - 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') +DATA_HUB['kaggle_house_test'] = ( + DATA_URL + 'kaggle_house_pred_test.csv', + 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') # Defined in file: ./chapter_deep-learning-computation/use-gpu.md def try_gpu(i=0): - """如果存在,则返回gpu(i),否则返回cpu()。""" + """Return gpu(i) if exists, otherwise return cpu().""" return npx.gpu(i) if npx.num_gpus() >= i + 1 else npx.cpu() def try_all_gpus(): - """返回所有可用的GPU,如果没有GPU,则返回[cpu()]。""" + """Return all available GPUs, or [cpu()] if no GPU exists.""" devices = [npx.gpu(i) for i in range(npx.num_gpus())] return devices if devices else [npx.cpu()] # Defined in file: ./chapter_convolutional-neural-networks/conv-layer.md def corr2d(X, K): - """计算二维互相关运算。""" + """Compute 2D cross-correlation.""" h, w = K.shape Y = d2l.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): - Y[i, j] = d2l.reduce_sum((X[i:i + h, j:j + w] * K)) + Y[i, j] = d2l.reduce_sum((X[i: i + h, j: j + w] * K)) return Y # Defined in file: ./chapter_convolutional-neural-networks/lenet.md def evaluate_accuracy_gpu(net, data_iter, device=None): """Compute the accuracy for a model on a dataset using a GPU.""" - if not device: # 查询第一个参数所在的第一个设备 + if not device: # Query the first device where the first parameter is on device = list(net.collect_params().values())[0].list_ctx()[0] - metric = d2l.Accumulator(2) # 正确预测的数量,总预测的数量 + # No. of correct predictions, no. of predictions + metric = d2l.Accumulator(2) for X, y in data_iter: X, y = X.as_in_ctx(device), y.as_in_ctx(device) metric.add(d2l.accuracy(net(X), y), d2l.size(y)) @@ -423,20 +424,22 @@ def evaluate_accuracy_gpu(net, data_iter, device=None): # Defined in file: ./chapter_convolutional-neural-networks/lenet.md -def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): +def train_ch6(net, train_iter, test_iter, num_epochs, lr, + device=d2l.try_gpu()): """Train a model with a GPU (defined in Chapter 6).""" net.initialize(force_reinit=True, ctx=device, init=init.Xavier()) loss = gluon.loss.SoftmaxCrossEntropyLoss() - trainer = gluon.Trainer(net.collect_params(), 'sgd', - {'learning_rate': lr}) + trainer = gluon.Trainer(net.collect_params(), + 'sgd', {'learning_rate': lr}) animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=['train loss', 'train acc', 'test acc']) timer, num_batches = d2l.Timer(), len(train_iter) for epoch in range(num_epochs): - metric = d2l.Accumulator(3) # 训练损失之和,训练准确率之和,范例数 + # Sum of training loss, sum of training accuracy, no. of examples + metric = d2l.Accumulator(3) for i, (X, y) in enumerate(train_iter): timer.start() - # 下面是与“d2l.train_epoch_ch3”的主要不同 + # Here is the major difference from `d2l.train_epoch_ch3` X, y = X.as_in_ctx(device), y.as_in_ctx(device) with autograd.record(): y_hat = net(X) @@ -460,6 +463,7 @@ def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): # Defined in file: ./chapter_convolutional-modern/resnet.md class Residual(nn.Block): + """The Residual block of ResNet.""" def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): super().__init__(**kwargs) self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, @@ -494,32 +498,31 @@ def read_time_machine(): # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def tokenize(lines, token='word'): - """将文本行拆分为单词或字符标记。""" + """Split text lines into word or character tokens.""" if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: - print('错误:未知令牌类型:' + token) + print('ERROR: unknown token type: ' + token) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md class Vocab: - """文本词表""" + """Vocabulary for text.""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: - reserved_tokens = [] - # 按出现频率排序 + reserved_tokens = [] + # Sort according to frequencies counter = count_corpus(tokens) self.token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) - # 未知标记的索引为0 + # The index for the unknown token is 0 self.unk, uniq_tokens = 0, [''] + reserved_tokens - uniq_tokens += [ - token for token, freq in self.token_freqs - if freq >= min_freq and token not in uniq_tokens] + uniq_tokens += [token for token, freq in self.token_freqs + if freq >= min_freq and token not in uniq_tokens] self.idx_to_token, self.token_to_idx = [], dict() for token in uniq_tokens: self.idx_to_token.append(token) @@ -540,21 +543,21 @@ def to_tokens(self, indices): def count_corpus(tokens): """Count token frequencies.""" - # 这里的 `tokens` 是1D列表或2D列表 + # Here `tokens` is a 1D list or 2D list if len(tokens) == 0 or isinstance(tokens[0], list): - # 将令牌列表展平 + # Flatten a list of token lists into a list of tokens tokens = [token for line in tokens for token in line] return collections.Counter(tokens) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def load_corpus_time_machine(max_tokens=-1): - """返回时光机器数据集的令牌索引和词汇表。""" + """Return token indices and the vocabulary of the time machine dataset.""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, - # 所以将所有文本行展平到一个列表中 + # Since each text line in the time machine dataset is not necessarily a + # sentence or a paragraph, flatten all the text lines into a single list corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] @@ -563,24 +566,28 @@ def load_corpus_time_machine(max_tokens=-1): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_random(corpus, batch_size, num_steps): - """使用随机抽样生成一小批子序列。""" - # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 + """Generate a minibatch of subsequences using random sampling.""" + # Start with a random offset (inclusive of `num_steps - 1`) to partition a + # sequence corpus = corpus[random.randint(0, num_steps - 1):] - # 减去1,因为我们需要考虑标签 + # Subtract 1 since we need to account for labels num_subseqs = (len(corpus) - 1) // num_steps - # 长度为`num_steps`的子序列的起始索引 + # The starting indices for subsequences of length `num_steps` initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 + # In random sampling, the subsequences from two adjacent random + # minibatches during iteration are not necessarily adjacent on the + # original sequence random.shuffle(initial_indices) def data(pos): - # 返回从`pos`开始的长度为`num_steps`的序列 - return corpus[pos:pos + num_steps] + # Return a sequence of length `num_steps` starting from `pos` + return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): - # 这里,`initial_indices`包含子序列的随机起始索引 - initial_indices_per_batch = initial_indices[i:i + batch_size] + # Here, `initial_indices` contains randomized starting indices for + # subsequences + initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield d2l.tensor(X), d2l.tensor(Y) @@ -588,23 +595,23 @@ def data(pos): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_sequential(corpus, batch_size, num_steps): - """使用顺序分区生成一小批子序列。""" - # 从随机偏移量开始划分序列 + """Generate a minibatch of subsequences using sequential partitioning.""" + # Start with a random offset to partition a sequence offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size - Xs = d2l.tensor(corpus[offset:offset + num_tokens]) - Ys = d2l.tensor(corpus[offset + 1:offset + 1 + num_tokens]) + Xs = d2l.tensor(corpus[offset: offset + num_tokens]) + Ys = d2l.tensor(corpus[offset + 1: offset + 1 + num_tokens]) Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1) num_batches = Xs.shape[1] // num_steps for i in range(0, num_steps * num_batches, num_steps): - X = Xs[:, i:i + num_steps] - Y = Ys[:, i:i + num_steps] + X = Xs[:, i: i + num_steps] + Y = Ys[:, i: i + num_steps] yield X, Y # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md class SeqDataLoader: - """加载序列数据的迭代器。""" + """An iterator to load sequence data.""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): if use_random_iter: self.data_iter_fn = d2l.seq_data_iter_random @@ -618,17 +625,17 @@ def __iter__(self): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md -def load_data_time_machine(batch_size, num_steps, use_random_iter=False, - max_tokens=10000): - """返回时光机器数据集的迭代器和词表。""" - data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, - max_tokens) +def load_data_time_machine(batch_size, num_steps, + use_random_iter=False, max_tokens=10000): + """Return the iterator and the vocabulary of the time machine dataset.""" + data_iter = SeqDataLoader( + batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md class RNNModelScratch: - """从零开始实现的循环神经网络模型""" + """An RNN Model implemented from scratch.""" def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn): self.vocab_size, self.num_hiddens = vocab_size, num_hiddens @@ -645,15 +652,15 @@ def begin_state(self, batch_size, ctx): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def predict_ch8(prefix, num_preds, net, vocab, device): - """在`prefix`后面生成新字符。""" + """Generate new characters following the `prefix`.""" state = net.begin_state(batch_size=1, ctx=device) outputs = [vocab[prefix[0]]] - get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]], ctx=device), - (1, 1)) - for y in prefix[1:]: # 预热期 + get_input = lambda: d2l.reshape( + d2l.tensor([outputs[-1]], ctx=device), (1, 1)) + for y in prefix[1:]: # Warm-up period _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # 预测`num_preds`步 + for _ in range(num_preds): # Predict `num_preds` steps y, state = net(get_input(), state) outputs.append(int(y.argmax(axis=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) @@ -661,12 +668,12 @@ def predict_ch8(prefix, num_preds, net, vocab, device): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def grad_clipping(net, theta): - """裁剪梯度。""" + """Clip the gradient.""" if isinstance(net, gluon.Block): params = [p.data() for p in net.collect_params().values()] else: params = net.params - norm = math.sqrt(sum((p.grad**2).sum() for p in params)) + norm = math.sqrt(sum((p.grad ** 2).sum() for p in params)) if norm > theta: for param in params: param.grad[:] *= theta / norm @@ -674,12 +681,13 @@ def grad_clipping(net, theta): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): - """训练模型一个迭代周期(定义见第8章)。""" + """Train a model within one epoch (defined in Chapter 8).""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失之和, 标记数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for X, Y in train_iter: if state is None or use_random_iter: - # 在第一次迭代或使用随机抽样时初始化`state` + # Initialize `state` when either it is the first iteration or + # using random sampling state = net.begin_state(batch_size=X.shape[0], ctx=device) else: for s in state: @@ -691,7 +699,7 @@ def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): l = loss(y_hat, y).mean() l.backward() grad_clipping(net, 1) - updater(batch_size=1) # 因为已经调用了`mean`函数 + updater(batch_size=1) # Since the `mean` function has been invoked metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() @@ -699,33 +707,34 @@ def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): - """训练模型(定义见第8章)。""" + """Train a model (defined in Chapter 8).""" loss = gluon.loss.SoftmaxCrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - # 初始化 + # Initialize if isinstance(net, gluon.Block): - net.initialize(ctx=device, force_reinit=True, init=init.Normal(0.01)) - trainer = gluon.Trainer(net.collect_params(), 'sgd', - {'learning_rate': lr}) + net.initialize(ctx=device, force_reinit=True, + init=init.Normal(0.01)) + trainer = gluon.Trainer(net.collect_params(), + 'sgd', {'learning_rate': lr}) updater = lambda batch_size: trainer.step(batch_size) else: updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) - # 训练和预测 + # Train and predict for epoch in range(num_epochs): - ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, device, - use_random_iter) + ppl, speed = train_epoch_ch8( + net, train_iter, loss, updater, device, use_random_iter) if (epoch + 1) % 10 == 0: animator.add(epoch + 1, [ppl]) - print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}') + print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') print(predict('time traveller')) print(predict('traveller')) # Defined in file: ./chapter_recurrent-neural-networks/rnn-concise.md class RNNModel(nn.Block): - """循环神经网络模型。""" + """The RNN model.""" def __init__(self, rnn_layer, vocab_size, **kwargs): super(RNNModel, self).__init__(**kwargs) self.rnn = rnn_layer @@ -735,8 +744,9 @@ def __init__(self, rnn_layer, vocab_size, **kwargs): def forward(self, inputs, state): X = npx.one_hot(inputs.T, self.vocab_size) Y, state = self.rnn(X, state) - # 全连接层首先将`Y`的形状改为(`时间步数` * `批量大小`, `隐藏单元数`)。 - # 它的输出形状是 (`时间步数` * `批量大小`, `词表大小`)。 + # The fully-connected layer will first change the shape of `Y` to + # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is + # (`num_steps` * `batch_size`, `vocab_size`). output = self.dense(Y.reshape(-1, Y.shape[-1])) return output, state @@ -744,6 +754,582 @@ def begin_state(self, *args, **kwargs): return self.rnn.begin_state(*args, **kwargs) +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', + '94646ad1522d915e7b0f9296181140edcf86a4f5') + +def read_data_nmt(): + """Load the English-French dataset.""" + data_dir = d2l.download_extract('fra-eng') + with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: + return f.read() + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def preprocess_nmt(text): + """Preprocess the English-French dataset.""" + def no_space(char, prev_char): + return char in set(',.!?') and prev_char != ' ' + + # Replace non-breaking space with space, and convert uppercase letters to + # lowercase ones + text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() + # Insert space between words and punctuation marks + out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char + for i, char in enumerate(text)] + return ''.join(out) + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def tokenize_nmt(text, num_examples=None): + """Tokenize the English-French dataset.""" + source, target = [], [] + for i, line in enumerate(text.split('\n')): + if num_examples and i > num_examples: + break + parts = line.split('\t') + if len(parts) == 2: + source.append(parts[0].split(' ')) + target.append(parts[1].split(' ')) + return source, target + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def truncate_pad(line, num_steps, padding_token): + """Truncate or pad sequences.""" + if len(line) > num_steps: + return line[:num_steps] # Truncate + return line + [padding_token] * (num_steps - len(line)) # Pad + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def build_array_nmt(lines, vocab, num_steps): + """Transform text sequences of machine translation into minibatches.""" + lines = [vocab[l] for l in lines] + lines = [l + [vocab['']] for l in lines] + array = d2l.tensor([truncate_pad( + l, num_steps, vocab['']) for l in lines]) + valid_len = d2l.reduce_sum( + d2l.astype(array != vocab[''], d2l.int32), 1) + return array, valid_len + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def load_data_nmt(batch_size, num_steps, num_examples=600): + """Return the iterator and the vocabularies of the translation dataset.""" + text = preprocess_nmt(read_data_nmt()) + source, target = tokenize_nmt(text, num_examples) + src_vocab = d2l.Vocab(source, min_freq=2, + reserved_tokens=['', '', '']) + tgt_vocab = d2l.Vocab(target, min_freq=2, + reserved_tokens=['', '', '']) + src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) + tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) + data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) + data_iter = d2l.load_array(data_arrays, batch_size) + return data_iter, src_vocab, tgt_vocab + + +# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md +class Encoder(nn.Block): + """The base encoder interface for the encoder-decoder architecture.""" + def __init__(self, **kwargs): + super(Encoder, self).__init__(**kwargs) + + def forward(self, X, *args): + raise NotImplementedError + + +# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md +class Decoder(nn.Block): + """The base decoder interface for the encoder-decoder architecture.""" + def __init__(self, **kwargs): + super(Decoder, self).__init__(**kwargs) + + def init_state(self, enc_outputs, *args): + raise NotImplementedError + + def forward(self, X, state): + raise NotImplementedError + + +# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md +class EncoderDecoder(nn.Block): + """The base class for the encoder-decoder architecture.""" + def __init__(self, encoder, decoder, **kwargs): + super(EncoderDecoder, self).__init__(**kwargs) + self.encoder = encoder + self.decoder = decoder + + def forward(self, enc_X, dec_X, *args): + enc_outputs = self.encoder(enc_X, *args) + dec_state = self.decoder.init_state(enc_outputs, *args) + return self.decoder(dec_X, dec_state) + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +class Seq2SeqEncoder(d2l.Encoder): + """The RNN encoder for sequence to sequence learning.""" + def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, + dropout=0, **kwargs): + super(Seq2SeqEncoder, self).__init__(**kwargs) + # Embedding layer + self.embedding = nn.Embedding(vocab_size, embed_size) + self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout) + + def forward(self, X, *args): + # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) + X = self.embedding(X) + # In RNN models, the first axis corresponds to time steps + X = X.swapaxes(0, 1) + state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx) + output, state = self.rnn(X, state) + # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) + # `state[0]` shape: (`num_layers`, `batch_size`, `num_hiddens`) + return output, state + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): + """The softmax cross-entropy loss with masks.""" + # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) + # `label` shape: (`batch_size`, `num_steps`) + # `valid_len` shape: (`batch_size`,) + def forward(self, pred, label, valid_len): + # `weights` shape: (`batch_size`, `num_steps`, 1) + weights = np.expand_dims(np.ones_like(label), axis=-1) + weights = npx.sequence_mask(weights, valid_len, True, axis=1) + return super(MaskedSoftmaxCELoss, self).forward(pred, label, weights) + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): + """Train a model for sequence to sequence.""" + net.initialize(init.Xavier(), force_reinit=True, ctx=device) + trainer = gluon.Trainer(net.collect_params(), 'adam', + {'learning_rate': lr}) + loss = MaskedSoftmaxCELoss() + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[10, num_epochs]) + for epoch in range(num_epochs): + timer = d2l.Timer() + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + for batch in data_iter: + X, X_valid_len, Y, Y_valid_len = [ + x.as_in_ctx(device) for x in batch] + bos = np.array( + [tgt_vocab['']] * Y.shape[0], ctx=device).reshape(-1, 1) + dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing + with autograd.record(): + Y_hat, _ = net(X, dec_input, X_valid_len) + l = loss(Y_hat, Y, Y_valid_len) + l.backward() + d2l.grad_clipping(net, 1) + num_tokens = Y_valid_len.sum() + trainer.step(num_tokens) + metric.add(l.sum(), num_tokens) + if (epoch + 1) % 10 == 0: + animator.add(epoch + 1, (metric[0] / metric[1],)) + print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' + f'tokens/sec on {str(device)}') + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, + device, save_attention_weights=False): + """Predict for sequence to sequence.""" + src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ + src_vocab['']] + enc_valid_len = np.array([len(src_tokens)], ctx=device) + src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) + # Add the batch axis + enc_X = np.expand_dims(np.array(src_tokens, ctx=device), axis=0) + enc_outputs = net.encoder(enc_X, enc_valid_len) + dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) + # Add the batch axis + dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), axis=0) + output_seq, attention_weight_seq = [], [] + for _ in range(num_steps): + Y, dec_state = net.decoder(dec_X, dec_state) + # We use the token with the highest prediction likelihood as the input + # of the decoder at the next time step + dec_X = Y.argmax(axis=2) + pred = dec_X.squeeze(axis=0).astype('int32').item() + # Save attention weights (to be covered later) + if save_attention_weights: + attention_weight_seq.append(net.decoder.attention_weights) + # Once the end-of-sequence token is predicted, the generation of the + # output sequence is complete + if pred == tgt_vocab['']: + break + output_seq.append(pred) + return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def bleu(pred_seq, label_seq, k): + """Compute the BLEU.""" + pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') + len_pred, len_label = len(pred_tokens), len(label_tokens) + score = math.exp(min(0, 1 - len_label / len_pred)) + for n in range(1, k + 1): + num_matches, label_subs = 0, collections.defaultdict(int) + for i in range(len_label - n + 1): + label_subs[''.join(label_tokens[i: i + n])] += 1 + for i in range(len_pred - n + 1): + if label_subs[''.join(pred_tokens[i: i + n])] > 0: + num_matches += 1 + label_subs[''.join(pred_tokens[i: i + n])] -= 1 + score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) + return score + + +# Defined in file: ./chapter_attention-mechanisms/attention-cues.md +def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), + cmap='Reds'): + d2l.use_svg_display() + num_rows, num_cols = matrices.shape[0], matrices.shape[1] + fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, + sharex=True, sharey=True, squeeze=False) + for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): + for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): + pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) + if i == num_rows - 1: + ax.set_xlabel(xlabel) + if j == 0: + ax.set_ylabel(ylabel) + if titles: + ax.set_title(titles[j]) + fig.colorbar(pcm, ax=axes, shrink=0.6); + + +# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md +def masked_softmax(X, valid_lens): + """Perform softmax operation by masking elements on the last axis.""" + # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor + if valid_lens is None: + return npx.softmax(X) + else: + shape = X.shape + if valid_lens.ndim == 1: + valid_lens = valid_lens.repeat(shape[1]) + else: + valid_lens = valid_lens.reshape(-1) + # On the last axis, replace masked elements with a very large negative + # value, whose exponentiation outputs 0 + X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, True, + value=-1e6, axis=1) + return npx.softmax(X).reshape(shape) + + +# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md +class AdditiveAttention(nn.Block): + """Additive attention.""" + def __init__(self, num_hiddens, dropout, **kwargs): + super(AdditiveAttention, self).__init__(**kwargs) + # Use `flatten=False` to only transform the last axis so that the + # shapes for the other axes are kept the same + self.W_k = nn.Dense(num_hiddens, use_bias=False, flatten=False) + self.W_q = nn.Dense(num_hiddens, use_bias=False, flatten=False) + self.w_v = nn.Dense(1, use_bias=False, flatten=False) + self.dropout = nn.Dropout(dropout) + + def forward(self, queries, keys, values, valid_lens): + queries, keys = self.W_q(queries), self.W_k(keys) + # After dimension expansion, shape of `queries`: (`batch_size`, no. of + # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, + # no. of key-value pairs, `num_hiddens`). Sum them up with + # broadcasting + features = np.expand_dims(queries, axis=2) + np.expand_dims( + keys, axis=1) + features = np.tanh(features) + # There is only one output of `self.w_v`, so we remove the last + # one-dimensional entry from the shape. Shape of `scores`: + # (`batch_size`, no. of queries, no. of key-value pairs) + scores = np.squeeze(self.w_v(features), axis=-1) + self.attention_weights = masked_softmax(scores, valid_lens) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + return npx.batch_dot(self.dropout(self.attention_weights), values) + + +# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md +class DotProductAttention(nn.Block): + """Scaled dot product attention.""" + def __init__(self, dropout, **kwargs): + super(DotProductAttention, self).__init__(**kwargs) + self.dropout = nn.Dropout(dropout) + + # Shape of `queries`: (`batch_size`, no. of queries, `d`) + # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) + def forward(self, queries, keys, values, valid_lens=None): + d = queries.shape[-1] + # Set `transpose_b=True` to swap the last two dimensions of `keys` + scores = npx.batch_dot(queries, keys, transpose_b=True) / math.sqrt(d) + self.attention_weights = masked_softmax(scores, valid_lens) + return npx.batch_dot(self.dropout(self.attention_weights), values) + + +# Defined in file: ./chapter_attention-mechanisms/bahdanau-attention.md +class AttentionDecoder(d2l.Decoder): + """The base attention-based decoder interface.""" + def __init__(self, **kwargs): + super(AttentionDecoder, self).__init__(**kwargs) + + @property + def attention_weights(self): + raise NotImplementedError + + +# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md +class MultiHeadAttention(nn.Block): + def __init__(self, num_hiddens, num_heads, dropout, use_bias=False, + **kwargs): + super(MultiHeadAttention, self).__init__(**kwargs) + self.num_heads = num_heads + self.attention = d2l.DotProductAttention(dropout) + self.W_q = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) + self.W_k = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) + self.W_v = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) + self.W_o = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) + + def forward(self, queries, keys, values, valid_lens): + # Shape of `queries`, `keys`, or `values`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) + # Shape of `valid_lens`: + # (`batch_size`,) or (`batch_size`, no. of queries) + # After transposing, shape of output `queries`, `keys`, or `values`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) + queries = transpose_qkv(self.W_q(queries), self.num_heads) + keys = transpose_qkv(self.W_k(keys), self.num_heads) + values = transpose_qkv(self.W_v(values), self.num_heads) + + if valid_lens is not None: + # On axis 0, copy the first item (scalar or vector) for + # `num_heads` times, then copy the next item, and so on + valid_lens = valid_lens.repeat(self.num_heads, axis=0) + + # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, + # `num_hiddens` / `num_heads`) + output = self.attention(queries, keys, values, valid_lens) + + # Shape of `output_concat`: + # (`batch_size`, no. of queries, `num_hiddens`) + output_concat = transpose_output(output, self.num_heads) + return self.W_o(output_concat) + + +# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md +def transpose_qkv(X, num_heads): + # Shape of input `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). + # Shape of output `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_heads`, + # `num_hiddens` / `num_heads`) + X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) + + # Shape of output `X`: + # (`batch_size`, `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) + X = X.transpose(0, 2, 1, 3) + + # Shape of `output`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) + return X.reshape(-1, X.shape[2], X.shape[3]) + + +def transpose_output(X, num_heads): + """Reverse the operation of `transpose_qkv`""" + X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) + X = X.transpose(0, 2, 1, 3) + return X.reshape(X.shape[0], X.shape[1], -1) + + +# Defined in file: ./chapter_attention-mechanisms/self-attention-and-positional-encoding.md +class PositionalEncoding(nn.Block): + def __init__(self, num_hiddens, dropout, max_len=1000): + super(PositionalEncoding, self).__init__() + self.dropout = nn.Dropout(dropout) + # Create a long enough `P` + self.P = d2l.zeros((1, max_len, num_hiddens)) + X = d2l.arange(max_len).reshape(-1, 1) / np.power( + 10000, np.arange(0, num_hiddens, 2) / num_hiddens) + self.P[:, :, 0::2] = np.sin(X) + self.P[:, :, 1::2] = np.cos(X) + + def forward(self, X): + X = X + self.P[:, :X.shape[1], :].as_in_ctx(X.ctx) + return self.dropout(X) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class PositionWiseFFN(nn.Block): + def __init__(self, ffn_num_hiddens, ffn_num_outputs, **kwargs): + super(PositionWiseFFN, self).__init__(**kwargs) + self.dense1 = nn.Dense(ffn_num_hiddens, flatten=False, + activation='relu') + self.dense2 = nn.Dense(ffn_num_outputs, flatten=False) + + def forward(self, X): + return self.dense2(self.dense1(X)) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class AddNorm(nn.Block): + def __init__(self, dropout, **kwargs): + super(AddNorm, self).__init__(**kwargs) + self.dropout = nn.Dropout(dropout) + self.ln = nn.LayerNorm() + + def forward(self, X, Y): + return self.ln(self.dropout(Y) + X) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class EncoderBlock(nn.Block): + def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, + use_bias=False, **kwargs): + super(EncoderBlock, self).__init__(**kwargs) + self.attention = d2l.MultiHeadAttention( + num_hiddens, num_heads, dropout, use_bias) + self.addnorm1 = AddNorm(dropout) + self.ffn = PositionWiseFFN(ffn_num_hiddens, num_hiddens) + self.addnorm2 = AddNorm(dropout) + + def forward(self, X, valid_lens): + Y = self.addnorm1(X, self.attention(X, X, X, valid_lens)) + return self.addnorm2(Y, self.ffn(Y)) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class TransformerEncoder(d2l.Encoder): + def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, + num_heads, num_layers, dropout, use_bias=False, **kwargs): + super(TransformerEncoder, self).__init__(**kwargs) + self.num_hiddens = num_hiddens + self.embedding = nn.Embedding(vocab_size, num_hiddens) + self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) + self.blks = nn.Sequential() + for _ in range(num_layers): + self.blks.add( + EncoderBlock(num_hiddens, ffn_num_hiddens, num_heads, dropout, + use_bias)) + + def forward(self, X, valid_lens, *args): + # Since positional encoding values are between -1 and 1, the embedding + # values are multiplied by the square root of the embedding dimension + # to rescale before they are summed up + X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) + self.attention_weights = [None] * len(self.blks) + for i, blk in enumerate(self.blks): + X = blk(X, valid_lens) + self.attention_weights[ + i] = blk.attention.attention.attention_weights + return X + + +# Defined in file: ./chapter_optimization/optimization-intro.md +def annotate(text, xy, xytext): + d2l.plt.gca().annotate(text, xy=xy, xytext=xytext, + arrowprops=dict(arrowstyle='->')) + + +# Defined in file: ./chapter_optimization/gd.md +def train_2d(trainer, steps=20): + """Optimize a 2-dim objective function with a customized trainer.""" + # s1 and s2 are internal state variables and will + # be used later in the chapter + x1, x2, s1, s2 = -5, -2, 0, 0 + results = [(x1, x2)] + for i in range(steps): + x1, x2, s1, s2 = trainer(x1, x2, s1, s2) + results.append((x1, x2)) + return results + +def show_trace_2d(f, results): + """Show the trace of 2D variables during optimization.""" + d2l.set_figsize() + d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') + x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1), + d2l.arange(-3.0, 1.0, 0.1)) + d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') + d2l.plt.xlabel('x1') + d2l.plt.ylabel('x2') + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat', + '76e5be1548fd8222e5074cf0faae75edff8cf93f') + +def get_data_ch11(batch_size=10, n=1500): + data = np.genfromtxt(d2l.download('airfoil'), + dtype=np.float32, delimiter='\t') + data = (data - data.mean(axis=0)) / data.std(axis=0) + data_iter = d2l.load_array( + (data[:n, :-1], data[:n, -1]), batch_size, is_train=True) + return data_iter, data.shape[1]-1 + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +def train_ch11(trainer_fn, states, hyperparams, data_iter, + feature_dim, num_epochs=2): + # Initialization + w = np.random.normal(scale=0.01, size=(feature_dim, 1)) + b = np.zeros(1) + w.attach_grad() + b.attach_grad() + net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss + # Train + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + with autograd.record(): + l = loss(net(X), y).mean() + l.backward() + trainer_fn([w, b], states, hyperparams) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss),)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + return timer.cumsum(), animator.Y[0] + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +def train_concise_ch11(tr_name, hyperparams, data_iter, num_epochs=2): + # Initialization + net = nn.Sequential() + net.add(nn.Dense(1)) + net.initialize(init.Normal(sigma=0.01)) + trainer = gluon.Trainer(net.collect_params(), tr_name, hyperparams) + loss = gluon.loss.L2Loss() + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + with autograd.record(): + l = loss(net(X), y) + l.backward() + trainer.step(X.shape[0]) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss),)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + + # Defined in file: ./chapter_computational-performance/hybridize.md class Benchmark: def __init__(self, description='Done'): @@ -757,6 +1343,1451 @@ def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') +# Defined in file: ./chapter_computational-performance/multiple-gpus.md +def split_batch(X, y, devices): + """Split `X` and `y` into multiple devices.""" + assert X.shape[0] == y.shape[0] + return (gluon.utils.split_and_load(X, devices), + gluon.utils.split_and_load(y, devices)) + + +# Defined in file: ./chapter_computational-performance/multiple-gpus-concise.md +def resnet18(num_classes): + """A slightly modified ResNet-18 model.""" + def resnet_block(num_channels, num_residuals, first_block=False): + blk = nn.Sequential() + for i in range(num_residuals): + if i == 0 and not first_block: + blk.add(d2l.Residual( + num_channels, use_1x1conv=True, strides=2)) + else: + blk.add(d2l.Residual(num_channels)) + return blk + + net = nn.Sequential() + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer + net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), + nn.BatchNorm(), nn.Activation('relu')) + net.add(resnet_block(64, 2, first_block=True), + resnet_block(128, 2), + resnet_block(256, 2), + resnet_block(512, 2)) + net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) + return net + + +# Defined in file: ./chapter_computational-performance/multiple-gpus-concise.md +def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): + # Query the list of devices + devices = list(net.collect_params().values())[0].list_ctx() + metric = d2l.Accumulator(2) # num_corrected_examples, num_examples + for features, labels in data_iter: + X_shards, y_shards = split_f(features, labels, devices) + # Run in parallel + pred_shards = [net(X_shard) for X_shard in X_shards] + metric.add(sum(float(d2l.accuracy(pred_shard, y_shard)) for + pred_shard, y_shard in zip( + pred_shards, y_shards)), labels.size) + return metric[0] / metric[1] + + +# Defined in file: ./chapter_computer-vision/image-augmentation.md +def train_batch_ch13(net, features, labels, loss, trainer, devices, + split_f=d2l.split_batch): + X_shards, y_shards = split_f(features, labels, devices) + with autograd.record(): + pred_shards = [net(X_shard) for X_shard in X_shards] + ls = [loss(pred_shard, y_shard) for pred_shard, y_shard + in zip(pred_shards, y_shards)] + for l in ls: + l.backward() + # The True flag allows parameters with stale gradients, which is useful + # later (e.g., in fine-tuning BERT) + trainer.step(labels.shape[0], ignore_stale_grad=True) + train_loss_sum = sum([float(l.sum()) for l in ls]) + train_acc_sum = sum(d2l.accuracy(pred_shard, y_shard) + for pred_shard, y_shard in zip(pred_shards, y_shards)) + return train_loss_sum, train_acc_sum + + +# Defined in file: ./chapter_computer-vision/image-augmentation.md +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus(), split_f=d2l.split_batch): + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + for epoch in range(num_epochs): + # Store training_loss, training_accuracy, num_examples, num_features + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices, split_f) + metric.add(l, acc, labels.shape[0], labels.size) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpus(net, test_iter, split_f) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') + + +# Defined in file: ./chapter_computer-vision/fine-tuning.md +d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL+'hotdog.zip', + 'fba480ffa8aa7e0febbb511d181409f899b9baa5') + + +# Defined in file: ./chapter_computer-vision/bounding-box.md +def box_corner_to_center(boxes): + """Convert from (upper_left, bottom_right) to (center, width, height)""" + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + cx = (x1 + x2) / 2 + cy = (y1 + y2) / 2 + w = x2 - x1 + h = y2 - y1 + boxes = d2l.stack((cx, cy, w, h), axis=-1) + return boxes + +def box_center_to_corner(boxes): + """Convert from (center, width, height) to (upper_left, bottom_right)""" + cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + x1 = cx - 0.5 * w + y1 = cy - 0.5 * h + x2 = cx + 0.5 * w + y2 = cy + 0.5 * h + boxes = d2l.stack((x1, y1, x2, y2), axis=-1) + return boxes + + +# Defined in file: ./chapter_computer-vision/bounding-box.md +def bbox_to_rect(bbox, color): + """Convert bounding box to matplotlib format.""" + # Convert the bounding box (top-left x, top-left y, bottom-right x, + # bottom-right y) format to matplotlib format: ((upper-left x, + # upper-left y), width, height) + return d2l.plt.Rectangle( + xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], + fill=False, edgecolor=color, linewidth=2) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def multibox_prior(data, sizes, ratios): + in_height, in_width = data.shape[-2:] + device, num_sizes, num_ratios = data.ctx, len(sizes), len(ratios) + boxes_per_pixel = (num_sizes + num_ratios - 1) + size_tensor = d2l.tensor(sizes, ctx=device) + ratio_tensor = d2l.tensor(ratios, ctx=device) + # Offsets are required to move the anchor to center of a pixel + # Since pixel (height=1, width=1), we choose to offset our centers by 0.5 + offset_h, offset_w = 0.5, 0.5 + steps_h = 1.0 / in_height # Scaled steps in y axis + steps_w = 1.0 / in_width # Scaled steps in x axis + + # Generate all center points for the anchor boxes + center_h = (d2l.arange(in_height, ctx=device) + offset_h) * steps_h + center_w = (d2l.arange(in_width, ctx=device) + offset_w) * steps_w + shift_x, shift_y = d2l.meshgrid(center_w, center_h) + shift_x, shift_y = shift_x.reshape(-1), shift_y.reshape(-1) + + # Generate boxes_per_pixel number of heights and widths which are later + # used to create anchor box corner coordinates (xmin, xmax, ymin, ymax) + # concat (various sizes, first ratio) and (first size, various ratios) + w = np.concatenate((size_tensor * np.sqrt(ratio_tensor[0]), + sizes[0] * np.sqrt(ratio_tensor[1:])))\ + * in_height / in_width # handle rectangular inputs + h = np.concatenate((size_tensor / np.sqrt(ratio_tensor[0]), + sizes[0] / np.sqrt(ratio_tensor[1:]))) + # Divide by 2 to get half height and half width + anchor_manipulations = np.tile(np.stack((-w, -h, w, h)).T, + (in_height * in_width, 1)) / 2 + + # Each center point will have boxes_per_pixel number of anchor boxes, so + # generate grid of all anchor box centers with boxes_per_pixel repeats + out_grid = d2l.stack([shift_x, shift_y, shift_x, shift_y], + axis=1).repeat(boxes_per_pixel, axis=0) + + output = out_grid + anchor_manipulations + return np.expand_dims(output, axis=0) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def show_bboxes(axes, bboxes, labels=None, colors=None): + """Show bounding boxes.""" + def _make_list(obj, default_values=None): + if obj is None: + obj = default_values + elif not isinstance(obj, (list, tuple)): + obj = [obj] + return obj + labels = _make_list(labels) + colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c']) + for i, bbox in enumerate(bboxes): + color = colors[i % len(colors)] + rect = d2l.bbox_to_rect(d2l.numpy(bbox), color) + axes.add_patch(rect) + if labels and len(labels) > i: + text_color = 'k' if color == 'w' else 'w' + axes.text(rect.xy[0], rect.xy[1], labels[i], + va='center', ha='center', fontsize=9, color=text_color, + bbox=dict(facecolor=color, lw=0)) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def box_iou(boxes1, boxes2): + """Compute IOU between two sets of boxes of shape (N,4) and (M,4).""" + # Compute box areas + box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) * + (boxes[:, 3] - boxes[:, 1])) + area1 = box_area(boxes1) + area2 = box_area(boxes2) + lt = np.maximum(boxes1[:, None, :2], boxes2[:, :2]) # [N,M,2] + rb = np.minimum(boxes1[:, None, 2:], boxes2[:, 2:]) # [N,M,2] + wh = (rb - lt).clip(min=0) # [N,M,2] + inter = wh[:, :, 0] * wh[:, :, 1] # [N,M] + unioun = area1[:, None] + area2 - inter + return inter / unioun + + +# Defined in file: ./chapter_computer-vision/anchor.md +def match_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): + """Assign ground-truth bounding boxes to anchor boxes similar to them.""" + num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0] + # Element `x_ij` in the `i^th` row and `j^th` column is the IoU + # of the anchor box `anc_i` to the ground-truth bounding box `box_j` + jaccard = box_iou(anchors, ground_truth) + # Initialize the tensor to hold assigned ground truth bbox for each anchor + anchors_bbox_map = np.full((num_anchors,), -1, dtype=np.int32, ctx=device) + # Assign ground truth bounding box according to the threshold + max_ious, indices = np.max(jaccard, axis=1), np.argmax(jaccard, axis=1) + anc_i = np.nonzero(max_ious >= 0.5)[0] + box_j = indices[max_ious >= 0.5] + anchors_bbox_map[anc_i] = box_j + # Find the largest iou for each bbox + col_discard = np.full((num_anchors,), -1) + row_discard = np.full((num_gt_boxes,), -1) + for _ in range(num_gt_boxes): + max_idx = np.argmax(jaccard) + box_idx = (max_idx % num_gt_boxes).astype('int32') + anc_idx = (max_idx / num_gt_boxes).astype('int32') + anchors_bbox_map[anc_idx] = box_idx + jaccard[:, box_idx] = col_discard + jaccard[anc_idx, :] = row_discard + return anchors_bbox_map + + +# Defined in file: ./chapter_computer-vision/anchor.md +def offset_boxes(anchors, assigned_bb, eps=1e-6): + c_anc = d2l.box_corner_to_center(anchors) + c_assigned_bb = d2l.box_corner_to_center(assigned_bb) + offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:] + offset_wh = 5 * d2l.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:]) + offset = d2l.concat([offset_xy, offset_wh], axis=1) + return offset + + +# Defined in file: ./chapter_computer-vision/anchor.md +def multibox_target(anchors, labels): + batch_size, anchors = labels.shape[0], anchors.squeeze(0) + batch_offset, batch_mask, batch_class_labels = [], [], [] + device, num_anchors = anchors.ctx, anchors.shape[0] + for i in range(batch_size): + label = labels[i, :, :] + anchors_bbox_map = match_anchor_to_bbox(label[:, 1:], anchors, device) + bbox_mask = np.tile((np.expand_dims((anchors_bbox_map >= 0), + axis=-1)), (1, 4)).astype('int32') + # Initialize class_labels and assigned bbox coordinates with zeros + class_labels = d2l.zeros(num_anchors, dtype=np.int32, ctx=device) + assigned_bb = d2l.zeros((num_anchors, 4), dtype=np.float32, ctx=device) + # Assign class labels to the anchor boxes using matched gt bbox labels + # If no gt bbox is assigned to an anchor box, then let the + # class_labels and assigned_bb remain zero, i.e the background class + indices_true = np.nonzero(anchors_bbox_map >= 0)[0] + bb_idx = anchors_bbox_map[indices_true] + class_labels[indices_true] = label[bb_idx, 0].astype('int32') + 1 + assigned_bb[indices_true] = label[bb_idx, 1:] + # offset transformations + offset = offset_boxes(anchors, assigned_bb) * bbox_mask + batch_offset.append(offset.reshape(-1)) + batch_mask.append(bbox_mask.reshape(-1)) + batch_class_labels.append(class_labels) + bbox_offset = d2l.stack(batch_offset) + bbox_mask = d2l.stack(batch_mask) + class_labels = d2l.stack(batch_class_labels) + return (bbox_offset, bbox_mask, class_labels) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def offset_inverse(anchors, offset_preds): + c_anc = d2l.box_corner_to_center(anchors) + c_pred_bb_xy = (offset_preds[:, :2] * c_anc[:, 2:] / 10) + c_anc[:, :2] + c_pred_bb_wh = d2l.exp(offset_preds[:, 2:] / 5) * c_anc[:, 2:] + c_pred_bb = d2l.concat((c_pred_bb_xy, c_pred_bb_wh), axis=1) + predicted_bb = d2l.box_center_to_corner(c_pred_bb) + return predicted_bb + + +# Defined in file: ./chapter_computer-vision/anchor.md +def nms(boxes, scores, iou_threshold): + # sorting scores by the descending order and return their indices + B = scores.argsort()[::-1] + keep = [] # boxes indices that will be kept + while B.size > 0: + i = B[0] + keep.append(i) + if B.size == 1: break + iou = box_iou(boxes[i, :].reshape(-1, 4), + boxes[B[1:], :].reshape(-1, 4)).reshape(-1) + inds = np.nonzero(iou <= iou_threshold)[0] + B = B[inds + 1] + return np.array(keep, dtype=np.int32, ctx=boxes.ctx) + +def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, + pos_threshold=0.00999999978): + device, batch_size = cls_probs.ctx, cls_probs.shape[0] + anchors = np.squeeze(anchors, axis=0) + num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2] + out = [] + for i in range(batch_size): + cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4) + conf, class_id = np.max(cls_prob[1:], 0), np.argmax(cls_prob[1:], 0) + predicted_bb = offset_inverse(anchors, offset_pred) + keep = nms(predicted_bb, conf, 0.5) + # Find all non_keep indices and set the class_id to background + all_idx = np.arange(num_anchors, dtype=np.int32, ctx=device) + combined = d2l.concat((keep, all_idx)) + unique, counts = np.unique(combined, return_counts=True) + non_keep = unique[counts == 1] + all_id_sorted = d2l.concat((keep, non_keep)) + class_id[non_keep] = -1 + class_id = class_id[all_id_sorted].astype('float32') + conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] + # threshold to be a positive prediction + below_min_idx = (conf < pos_threshold) + class_id[below_min_idx] = -1 + conf[below_min_idx] = 1 - conf[below_min_idx] + pred_info = d2l.concat((np.expand_dims(class_id, axis=1), + np.expand_dims(conf, axis=1), + predicted_bb), axis=1) + out.append(pred_info) + return d2l.stack(out) + + +# Defined in file: ./chapter_computer-vision/object-detection-dataset.md +d2l.DATA_HUB['banana-detection'] = (d2l.DATA_URL + 'banana-detection.zip', + '5de26c8fce5ccdea9f91267273464dc968d20d72') + + +# Defined in file: ./chapter_computer-vision/object-detection-dataset.md +def read_data_bananas(is_train=True): + """Read the bananas dataset images and labels.""" + data_dir = d2l.download_extract('banana-detection') + csv_fname = os.path.join(data_dir, 'bananas_train' if is_train + else 'bananas_val', 'label.csv') + csv_data = pd.read_csv(csv_fname) + csv_data = csv_data.set_index('img_name') + images, targets = [], [] + for img_name, target in csv_data.iterrows(): + images.append(image.imread( + os.path.join(data_dir, 'bananas_train' if is_train else + 'bananas_val', 'images', f'{img_name}'))) + # Since all images have same object class i.e. category '0', + # the `label` column corresponds to the only object i.e. banana + # The target is as follows : (`label`, `xmin`, `ymin`, `xmax`, `ymax`) + targets.append(list(target)) + return images, np.expand_dims(np.array(targets), 1) / 256 + + +class BananasDataset(gluon.data.Dataset): + def __init__(self, is_train): + self.features, self.labels = read_data_bananas(is_train) + print('read ' + str(len(self.features)) + (f' training examples' if + is_train else f' validation examples')) + + def __getitem__(self, idx): + return (self.features[idx].astype('float32').transpose(2, 0, 1), + self.labels[idx]) + + def __len__(self): + return len(self.features) + + +def load_data_bananas(batch_size): + """Load the bananas dataset.""" + train_iter = gluon.data.DataLoader(BananasDataset(is_train=True), + batch_size, shuffle=True) + val_iter = gluon.data.DataLoader(BananasDataset(is_train=False), + batch_size) + return (train_iter, val_iter) + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar', + '4e443f8a2eca6b1dac8a6c57641b67dd40621a49') + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def read_voc_images(voc_dir, is_train=True): + """Read all VOC feature and label images.""" + txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation', + 'train.txt' if is_train else 'val.txt') + with open(txt_fname, 'r') as f: + images = f.read().split() + features, labels = [], [] + for i, fname in enumerate(images): + features.append(image.imread(os.path.join( + voc_dir, 'JPEGImages', f'{fname}.jpg'))) + labels.append(image.imread(os.path.join( + voc_dir, 'SegmentationClass', f'{fname}.png'))) + return features, labels + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], + [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128], + [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0], + [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128], + [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0], + [0, 64, 128]] + +VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat', + 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', + 'diningtable', 'dog', 'horse', 'motorbike', 'person', + 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor'] + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def build_colormap2label(): + """Build an RGB color to label mapping for segmentation.""" + colormap2label = np.zeros(256 ** 3) + for i, colormap in enumerate(VOC_COLORMAP): + colormap2label[(colormap[0]*256 + colormap[1])*256 + colormap[2]] = i + return colormap2label + +def voc_label_indices(colormap, colormap2label): + """Map an RGB color to a label.""" + colormap = colormap.astype(np.int32) + idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + + colormap[:, :, 2]) + return colormap2label[idx] + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def voc_rand_crop(feature, label, height, width): + """Randomly crop for both feature and label images.""" + feature, rect = image.random_crop(feature, (width, height)) + label = image.fixed_crop(label, *rect) + return feature, label + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +class VOCSegDataset(gluon.data.Dataset): + """A customized dataset to load VOC dataset.""" + + def __init__(self, is_train, crop_size, voc_dir): + self.rgb_mean = np.array([0.485, 0.456, 0.406]) + self.rgb_std = np.array([0.229, 0.224, 0.225]) + self.crop_size = crop_size + features, labels = read_voc_images(voc_dir, is_train=is_train) + self.features = [self.normalize_image(feature) + for feature in self.filter(features)] + self.labels = self.filter(labels) + self.colormap2label = build_colormap2label() + print('read ' + str(len(self.features)) + ' examples') + + def normalize_image(self, img): + return (img.astype('float32') / 255 - self.rgb_mean) / self.rgb_std + + def filter(self, imgs): + return [img for img in imgs if ( + img.shape[0] >= self.crop_size[0] and + img.shape[1] >= self.crop_size[1])] + + def __getitem__(self, idx): + feature, label = voc_rand_crop(self.features[idx], self.labels[idx], + *self.crop_size) + return (feature.transpose(2, 0, 1), + voc_label_indices(label, self.colormap2label)) + + def __len__(self): + return len(self.features) + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def load_data_voc(batch_size, crop_size): + """Download and load the VOC2012 semantic dataset.""" + voc_dir = d2l.download_extract('voc2012', os.path.join( + 'VOCdevkit', 'VOC2012')) + num_workers = d2l.get_dataloader_workers() + train_iter = gluon.data.DataLoader( + VOCSegDataset(True, crop_size, voc_dir), batch_size, + shuffle=True, last_batch='discard', num_workers=num_workers) + test_iter = gluon.data.DataLoader( + VOCSegDataset(False, crop_size, voc_dir), batch_size, + last_batch='discard', num_workers=num_workers) + return train_iter, test_iter + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', + '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +def read_csv_labels(fname): + """Read fname to return a name to label dictionary.""" + with open(fname, 'r') as f: + # Skip the file header line (column name) + lines = f.readlines()[1:] + tokens = [l.rstrip().split(',') for l in lines] + return dict(((name, label) for name, label in tokens)) + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +def copyfile(filename, target_dir): + """Copy a file into a target directory.""" + os.makedirs(target_dir, exist_ok=True) + shutil.copy(filename, target_dir) + +def reorg_train_valid(data_dir, labels, valid_ratio): + # The number of examples of the class with the least examples in the + # training dataset + n = collections.Counter(labels.values()).most_common()[-1][1] + # The number of examples per class for the validation set + n_valid_per_label = max(1, math.floor(n * valid_ratio)) + label_count = {} + for train_file in os.listdir(os.path.join(data_dir, 'train')): + label = labels[train_file.split('.')[0]] + fname = os.path.join(data_dir, 'train', train_file) + # Copy to train_valid_test/train_valid with a subfolder per class + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train_valid', label)) + if label not in label_count or label_count[label] < n_valid_per_label: + # Copy to train_valid_test/valid + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'valid', label)) + label_count[label] = label_count.get(label, 0) + 1 + else: + # Copy to train_valid_test/train + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train', label)) + return n_valid_per_label + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +def reorg_test(data_dir): + for test_file in os.listdir(os.path.join(data_dir, 'test')): + copyfile(os.path.join(data_dir, 'test', test_file), + os.path.join(data_dir, 'train_valid_test', 'test', + 'unknown')) + + +# Defined in file: ./chapter_computer-vision/kaggle-dog.md +d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip', + '0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d') + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +d2l.DATA_HUB['ptb'] = (d2l.DATA_URL + 'ptb.zip', + '319d85e578af0cdc590547f26231e4e31cdf1e42') + +def read_ptb(): + data_dir = d2l.download_extract('ptb') + with open(os.path.join(data_dir, 'ptb.train.txt')) as f: + raw_text = f.read() + return [line.split() for line in raw_text.split('\n')] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def subsampling(sentences, vocab): + # Map low frequency words into + sentences = [[vocab.idx_to_token[vocab[tk]] for tk in line] + for line in sentences] + # Count the frequency for each word + counter = d2l.count_corpus(sentences) + num_tokens = sum(counter.values()) + + # Return True if to keep this token during subsampling + def keep(token): + return(random.uniform(0, 1) < + math.sqrt(1e-4 / counter[token] * num_tokens)) + + # Now do the subsampling + return [[tk for tk in line if keep(tk)] for line in sentences] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def get_centers_and_contexts(corpus, max_window_size): + centers, contexts = [], [] + for line in corpus: + # Each sentence needs at least 2 words to form a "central target word + # - context word" pair + if len(line) < 2: + continue + centers += line + for i in range(len(line)): # Context window centered at i + window_size = random.randint(1, max_window_size) + indices = list(range(max(0, i - window_size), + min(len(line), i + 1 + window_size))) + # Exclude the central target word from the context words + indices.remove(i) + contexts.append([line[idx] for idx in indices]) + return centers, contexts + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +class RandomGenerator: + """Draw a random int in [0, n] according to n sampling weights.""" + def __init__(self, sampling_weights): + self.population = list(range(len(sampling_weights))) + self.sampling_weights = sampling_weights + self.candidates = [] + self.i = 0 + + def draw(self): + if self.i == len(self.candidates): + self.candidates = random.choices( + self.population, self.sampling_weights, k=10000) + self.i = 0 + self.i += 1 + return self.candidates[self.i-1] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def get_negatives(all_contexts, corpus, K): + counter = d2l.count_corpus(corpus) + sampling_weights = [counter[i]**0.75 for i in range(len(counter))] + all_negatives, generator = [], RandomGenerator(sampling_weights) + for contexts in all_contexts: + negatives = [] + while len(negatives) < len(contexts) * K: + neg = generator.draw() + # Noise words cannot be context words + if neg not in contexts: + negatives.append(neg) + all_negatives.append(negatives) + return all_negatives + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def batchify(data): + max_len = max(len(c) + len(n) for _, c, n in data) + centers, contexts_negatives, masks, labels = [], [], [], [] + for center, context, negative in data: + cur_len = len(context) + len(negative) + centers += [center] + contexts_negatives += [context + negative + [0] * (max_len - cur_len)] + masks += [[1] * cur_len + [0] * (max_len - cur_len)] + labels += [[1] * len(context) + [0] * (max_len - len(context))] + return (d2l.reshape(d2l.tensor(centers), (-1, 1)), d2l.tensor(contexts_negatives), + d2l.tensor(masks), d2l.tensor(labels)) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def load_data_ptb(batch_size, max_window_size, num_noise_words): + num_workers = d2l.get_dataloader_workers() + sentences = read_ptb() + vocab = d2l.Vocab(sentences, min_freq=10) + subsampled = subsampling(sentences, vocab) + corpus = [vocab[line] for line in subsampled] + all_centers, all_contexts = get_centers_and_contexts( + corpus, max_window_size) + all_negatives = get_negatives(all_contexts, corpus, num_noise_words) + dataset = gluon.data.ArrayDataset( + all_centers, all_contexts, all_negatives) + data_iter = gluon.data.DataLoader(dataset, batch_size, shuffle=True, + batchify_fn=batchify, + num_workers=num_workers) + return data_iter, vocab + + +# Defined in file: ./chapter_natural-language-processing-pretraining/similarity-analogy.md +d2l.DATA_HUB['glove.6b.50d'] = (d2l.DATA_URL + 'glove.6B.50d.zip', + '0b8703943ccdb6eb788e6f091b8946e82231bc4d') + +d2l.DATA_HUB['glove.6b.100d'] = (d2l.DATA_URL + 'glove.6B.100d.zip', + 'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a') + +d2l.DATA_HUB['glove.42b.300d'] = (d2l.DATA_URL + 'glove.42B.300d.zip', + 'b5116e234e9eb9076672cfeabf5469f3eec904fa') + +d2l.DATA_HUB['wiki.en'] = (d2l.DATA_URL + 'wiki.en.zip', + 'c1816da3821ae9f43899be655002f6c723e91b88') + + +# Defined in file: ./chapter_natural-language-processing-pretraining/similarity-analogy.md +class TokenEmbedding: + """Token Embedding.""" + def __init__(self, embedding_name): + self.idx_to_token, self.idx_to_vec = self._load_embedding( + embedding_name) + self.unknown_idx = 0 + self.token_to_idx = {token: idx for idx, token in + enumerate(self.idx_to_token)} + + def _load_embedding(self, embedding_name): + idx_to_token, idx_to_vec = [''], [] + data_dir = d2l.download_extract(embedding_name) + # GloVe website: https://nlp.stanford.edu/projects/glove/ + # fastText website: https://fasttext.cc/ + with open(os.path.join(data_dir, 'vec.txt'), 'r') as f: + for line in f: + elems = line.rstrip().split(' ') + token, elems = elems[0], [float(elem) for elem in elems[1:]] + # Skip header information, such as the top row in fastText + if len(elems) > 1: + idx_to_token.append(token) + idx_to_vec.append(elems) + idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec + return idx_to_token, d2l.tensor(idx_to_vec) + + def __getitem__(self, tokens): + indices = [self.token_to_idx.get(token, self.unknown_idx) + for token in tokens] + vecs = self.idx_to_vec[d2l.tensor(indices)] + return vecs + + def __len__(self): + return len(self.idx_to_token) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +def get_tokens_and_segments(tokens_a, tokens_b=None): + tokens = [''] + tokens_a + [''] + # 0 and 1 are marking segment A and B, respectively + segments = [0] * (len(tokens_a) + 2) + if tokens_b is not None: + tokens += tokens_b + [''] + segments += [1] * (len(tokens_b) + 1) + return tokens, segments + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class BERTEncoder(nn.Block): + def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, + num_layers, dropout, max_len=1000, **kwargs): + super(BERTEncoder, self).__init__(**kwargs) + self.token_embedding = nn.Embedding(vocab_size, num_hiddens) + self.segment_embedding = nn.Embedding(2, num_hiddens) + self.blks = nn.Sequential() + for _ in range(num_layers): + self.blks.add(d2l.EncoderBlock( + num_hiddens, ffn_num_hiddens, num_heads, dropout, True)) + # In BERT, positional embeddings are learnable, thus we create a + # parameter of positional embeddings that are long enough + self.pos_embedding = self.params.get('pos_embedding', + shape=(1, max_len, num_hiddens)) + + def forward(self, tokens, segments, valid_lens): + # Shape of `X` remains unchanged in the following code snippet: + # (batch size, max sequence length, `num_hiddens`) + X = self.token_embedding(tokens) + self.segment_embedding(segments) + X = X + self.pos_embedding.data(ctx=X.ctx)[:, :X.shape[1], :] + for blk in self.blks: + X = blk(X, valid_lens) + return X + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class MaskLM(nn.Block): + def __init__(self, vocab_size, num_hiddens, **kwargs): + super(MaskLM, self).__init__(**kwargs) + self.mlp = nn.Sequential() + self.mlp.add( + nn.Dense(num_hiddens, flatten=False, activation='relu')) + self.mlp.add(nn.LayerNorm()) + self.mlp.add(nn.Dense(vocab_size, flatten=False)) + + def forward(self, X, pred_positions): + num_pred_positions = pred_positions.shape[1] + pred_positions = pred_positions.reshape(-1) + batch_size = X.shape[0] + batch_idx = np.arange(0, batch_size) + # Suppose that `batch_size` = 2, `num_pred_positions` = 3, then + # `batch_idx` is `np.array([0, 0, 0, 1, 1, 1])` + batch_idx = np.repeat(batch_idx, num_pred_positions) + masked_X = X[batch_idx, pred_positions] + masked_X = masked_X.reshape((batch_size, num_pred_positions, -1)) + mlm_Y_hat = self.mlp(masked_X) + return mlm_Y_hat + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class NextSentencePred(nn.Block): + def __init__(self, **kwargs): + super(NextSentencePred, self).__init__(**kwargs) + self.output = nn.Dense(2) + + def forward(self, X): + # `X` shape: (batch size, `num_hiddens`) + return self.output(X) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class BERTModel(nn.Block): + def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens, num_heads, + num_layers, dropout, max_len=1000): + super(BERTModel, self).__init__() + self.encoder = BERTEncoder(vocab_size, num_hiddens, ffn_num_hiddens, + num_heads, num_layers, dropout, max_len) + self.hidden = nn.Dense(num_hiddens, activation='tanh') + self.mlm = MaskLM(vocab_size, num_hiddens) + self.nsp = NextSentencePred() + + def forward(self, tokens, segments, valid_lens=None, pred_positions=None): + encoded_X = self.encoder(tokens, segments, valid_lens) + if pred_positions is not None: + mlm_Y_hat = self.mlm(encoded_X, pred_positions) + else: + mlm_Y_hat = None + # The hidden layer of the MLP classifier for next sentence prediction. + # 0 is the index of the '' token + nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :])) + return encoded_X, mlm_Y_hat, nsp_Y_hat + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +d2l.DATA_HUB['wikitext-2'] = ( + 'https://s3.amazonaws.com/research.metamind.io/wikitext/' + 'wikitext-2-v1.zip', '3c914d17d80b1459be871a5039ac23e752a53cbe') + +def _read_wiki(data_dir): + file_name = os.path.join(data_dir, 'wiki.train.tokens') + with open(file_name, 'r') as f: + lines = f.readlines() + # Uppercase letters are converted to lowercase ones + paragraphs = [line.strip().lower().split(' . ') + for line in lines if len(line.split(' . ')) >= 2] + random.shuffle(paragraphs) + return paragraphs + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _get_next_sentence(sentence, next_sentence, paragraphs): + if random.random() < 0.5: + is_next = True + else: + # `paragraphs` is a list of lists of lists + next_sentence = random.choice(random.choice(paragraphs)) + is_next = False + return sentence, next_sentence, is_next + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): + nsp_data_from_paragraph = [] + for i in range(len(paragraph) - 1): + tokens_a, tokens_b, is_next = _get_next_sentence( + paragraph[i], paragraph[i + 1], paragraphs) + # Consider 1 '' token and 2 '' tokens + if len(tokens_a) + len(tokens_b) + 3 > max_len: + continue + tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b) + nsp_data_from_paragraph.append((tokens, segments, is_next)) + return nsp_data_from_paragraph + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, + vocab): + # Make a new copy of tokens for the input of a masked language model, + # where the input may contain replaced '' or random tokens + mlm_input_tokens = [token for token in tokens] + pred_positions_and_labels = [] + # Shuffle for getting 15% random tokens for prediction in the masked + # language modeling task + random.shuffle(candidate_pred_positions) + for mlm_pred_position in candidate_pred_positions: + if len(pred_positions_and_labels) >= num_mlm_preds: + break + masked_token = None + # 80% of the time: replace the word with the '' token + if random.random() < 0.8: + masked_token = '' + else: + # 10% of the time: keep the word unchanged + if random.random() < 0.5: + masked_token = tokens[mlm_pred_position] + # 10% of the time: replace the word with a random word + else: + masked_token = random.randint(0, len(vocab) - 1) + mlm_input_tokens[mlm_pred_position] = masked_token + pred_positions_and_labels.append( + (mlm_pred_position, tokens[mlm_pred_position])) + return mlm_input_tokens, pred_positions_and_labels + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _get_mlm_data_from_tokens(tokens, vocab): + candidate_pred_positions = [] + # `tokens` is a list of strings + for i, token in enumerate(tokens): + # Special tokens are not predicted in the masked language modeling + # task + if token in ['', '']: + continue + candidate_pred_positions.append(i) + # 15% of random tokens are predicted in the masked language modeling task + num_mlm_preds = max(1, round(len(tokens) * 0.15)) + mlm_input_tokens, pred_positions_and_labels = _replace_mlm_tokens( + tokens, candidate_pred_positions, num_mlm_preds, vocab) + pred_positions_and_labels = sorted(pred_positions_and_labels, + key=lambda x: x[0]) + pred_positions = [v[0] for v in pred_positions_and_labels] + mlm_pred_labels = [v[1] for v in pred_positions_and_labels] + return vocab[mlm_input_tokens], pred_positions, vocab[mlm_pred_labels] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _pad_bert_inputs(examples, max_len, vocab): + max_num_mlm_preds = round(max_len * 0.15) + all_token_ids, all_segments, valid_lens, = [], [], [] + all_pred_positions, all_mlm_weights, all_mlm_labels = [], [], [] + nsp_labels = [] + for (token_ids, pred_positions, mlm_pred_label_ids, segments, + is_next) in examples: + all_token_ids.append(np.array(token_ids + [vocab['']] * ( + max_len - len(token_ids)), dtype='int32')) + all_segments.append(np.array(segments + [0] * ( + max_len - len(segments)), dtype='int32')) + # `valid_lens` excludes count of '' tokens + valid_lens.append(np.array(len(token_ids), dtype='float32')) + all_pred_positions.append(np.array(pred_positions + [0] * ( + max_num_mlm_preds - len(pred_positions)), dtype='int32')) + # Predictions of padded tokens will be filtered out in the loss via + # multiplication of 0 weights + all_mlm_weights.append( + np.array([1.0] * len(mlm_pred_label_ids) + [0.0] * ( + max_num_mlm_preds - len(pred_positions)), dtype='float32')) + all_mlm_labels.append(np.array(mlm_pred_label_ids + [0] * ( + max_num_mlm_preds - len(mlm_pred_label_ids)), dtype='int32')) + nsp_labels.append(np.array(is_next)) + return (all_token_ids, all_segments, valid_lens, all_pred_positions, + all_mlm_weights, all_mlm_labels, nsp_labels) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +class _WikiTextDataset(gluon.data.Dataset): + def __init__(self, paragraphs, max_len): + # Input `paragraphs[i]` is a list of sentence strings representing a + # paragraph; while output `paragraphs[i]` is a list of sentences + # representing a paragraph, where each sentence is a list of tokens + paragraphs = [d2l.tokenize( + paragraph, token='word') for paragraph in paragraphs] + sentences = [sentence for paragraph in paragraphs + for sentence in paragraph] + self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ + '', '', '', '']) + # Get data for the next sentence prediction task + examples = [] + for paragraph in paragraphs: + examples.extend(_get_nsp_data_from_paragraph( + paragraph, paragraphs, self.vocab, max_len)) + # Get data for the masked language model task + examples = [(_get_mlm_data_from_tokens(tokens, self.vocab) + + (segments, is_next)) + for tokens, segments, is_next in examples] + # Pad inputs + (self.all_token_ids, self.all_segments, self.valid_lens, + self.all_pred_positions, self.all_mlm_weights, + self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs( + examples, max_len, self.vocab) + + def __getitem__(self, idx): + return (self.all_token_ids[idx], self.all_segments[idx], + self.valid_lens[idx], self.all_pred_positions[idx], + self.all_mlm_weights[idx], self.all_mlm_labels[idx], + self.nsp_labels[idx]) + + def __len__(self): + return len(self.all_token_ids) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def load_data_wiki(batch_size, max_len): + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('wikitext-2', 'wikitext-2') + paragraphs = _read_wiki(data_dir) + train_set = _WikiTextDataset(paragraphs, max_len) + train_iter = gluon.data.DataLoader(train_set, batch_size, shuffle=True, + num_workers=num_workers) + return train_iter, train_set.vocab + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-pretraining.md +def _get_batch_loss_bert(net, loss, vocab_size, tokens_X_shards, + segments_X_shards, valid_lens_x_shards, + pred_positions_X_shards, mlm_weights_X_shards, + mlm_Y_shards, nsp_y_shards): + mlm_ls, nsp_ls, ls = [], [], [] + for (tokens_X_shard, segments_X_shard, valid_lens_x_shard, + pred_positions_X_shard, mlm_weights_X_shard, mlm_Y_shard, + nsp_y_shard) in zip( + tokens_X_shards, segments_X_shards, valid_lens_x_shards, + pred_positions_X_shards, mlm_weights_X_shards, mlm_Y_shards, + nsp_y_shards): + # Forward pass + _, mlm_Y_hat, nsp_Y_hat = net( + tokens_X_shard, segments_X_shard, valid_lens_x_shard.reshape(-1), + pred_positions_X_shard) + # Compute masked language model loss + mlm_l = loss( + mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y_shard.reshape(-1), + mlm_weights_X_shard.reshape((-1, 1))) + mlm_l = mlm_l.sum() / (mlm_weights_X_shard.sum() + 1e-8) + # Compute next sentence prediction loss + nsp_l = loss(nsp_Y_hat, nsp_y_shard) + nsp_l = nsp_l.mean() + mlm_ls.append(mlm_l) + nsp_ls.append(nsp_l) + ls.append(mlm_l + nsp_l) + npx.waitall() + return mlm_ls, nsp_ls, ls + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +d2l.DATA_HUB['aclImdb'] = ( + 'http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz', + '01ada507287d82875905620988597833ad4e0903') + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +def read_imdb(data_dir, is_train): + data, labels = [], [] + for label in ('pos', 'neg'): + folder_name = os.path.join(data_dir, 'train' if is_train else 'test', + label) + for file in os.listdir(folder_name): + with open(os.path.join(folder_name, file), 'rb') as f: + review = f.read().decode('utf-8').replace('\n', '') + data.append(review) + labels.append(1 if label == 'pos' else 0) + return data, labels + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +def load_data_imdb(batch_size, num_steps=500): + data_dir = d2l.download_extract('aclImdb', 'aclImdb') + train_data = read_imdb(data_dir, True) + test_data = read_imdb(data_dir, False) + train_tokens = d2l.tokenize(train_data[0], token='word') + test_tokens = d2l.tokenize(test_data[0], token='word') + vocab = d2l.Vocab(train_tokens, min_freq=5) + train_features = np.array([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in train_tokens]) + test_features = np.array([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in test_tokens]) + train_iter = d2l.load_array((train_features, train_data[1]), batch_size) + test_iter = d2l.load_array((test_features, test_data[1]), batch_size, + is_train=False) + return train_iter, test_iter, vocab + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-rnn.md +def predict_sentiment(net, vocab, sentence): + sentence = np.array(vocab[sentence.split()], ctx=d2l.try_gpu()) + label = np.argmax(net(sentence.reshape(1, -1)), axis=1) + return 'positive' if label == 1 else 'negative' + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +d2l.DATA_HUB['SNLI'] = ( + 'https://nlp.stanford.edu/projects/snli/snli_1.0.zip', + '9fcde07509c7e87ec61c640c1b2753d9041758e4') + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +def read_snli(data_dir, is_train): + """Read the SNLI dataset into premises, hypotheses, and labels.""" + def extract_text(s): + # Remove information that will not be used by us + s = re.sub('\\(', '', s) + s = re.sub('\\)', '', s) + # Substitute two or more consecutive whitespace with space + s = re.sub('\\s{2,}', ' ', s) + return s.strip() + label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2} + file_name = os.path.join(data_dir, 'snli_1.0_train.txt' + if is_train else 'snli_1.0_test.txt') + with open(file_name, 'r') as f: + rows = [row.split('\t') for row in f.readlines()[1:]] + premises = [extract_text(row[1]) for row in rows if row[0] in label_set] + hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set] + labels = [label_set[row[0]] for row in rows if row[0] in label_set] + return premises, hypotheses, labels + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +class SNLIDataset(gluon.data.Dataset): + """A customized dataset to load the SNLI dataset.""" + def __init__(self, dataset, num_steps, vocab=None): + self.num_steps = num_steps + all_premise_tokens = d2l.tokenize(dataset[0]) + all_hypothesis_tokens = d2l.tokenize(dataset[1]) + if vocab is None: + self.vocab = d2l.Vocab(all_premise_tokens + all_hypothesis_tokens, + min_freq=5, reserved_tokens=['']) + else: + self.vocab = vocab + self.premises = self._pad(all_premise_tokens) + self.hypotheses = self._pad(all_hypothesis_tokens) + self.labels = np.array(dataset[2]) + print('read ' + str(len(self.premises)) + ' examples') + + def _pad(self, lines): + return np.array([d2l.truncate_pad( + self.vocab[line], self.num_steps, self.vocab['']) + for line in lines]) + + def __getitem__(self, idx): + return (self.premises[idx], self.hypotheses[idx]), self.labels[idx] + + def __len__(self): + return len(self.premises) + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +def load_data_snli(batch_size, num_steps=50): + """Download the SNLI dataset and return data iterators and vocabulary.""" + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('SNLI') + train_data = read_snli(data_dir, True) + test_data = read_snli(data_dir, False) + train_set = SNLIDataset(train_data, num_steps) + test_set = SNLIDataset(test_data, num_steps, train_set.vocab) + train_iter = gluon.data.DataLoader(train_set, batch_size, shuffle=True, + num_workers=num_workers) + test_iter = gluon.data.DataLoader(test_set, batch_size, shuffle=False, + num_workers=num_workers) + return train_iter, test_iter, train_set.vocab + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-attention.md +def split_batch_multi_inputs(X, y, devices): + """Split multi-input `X` and `y` into multiple devices.""" + X = list(zip(*[gluon.utils.split_and_load( + feature, devices, even_split=False) for feature in X])) + return (X, gluon.utils.split_and_load(y, devices, even_split=False)) + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-attention.md +def predict_snli(net, vocab, premise, hypothesis): + premise = np.array(vocab[premise], ctx=d2l.try_gpu()) + hypothesis = np.array(vocab[hypothesis], ctx=d2l.try_gpu()) + label = np.argmax(net([premise.reshape((1, -1)), + hypothesis.reshape((1, -1))]), axis=1) + return 'entailment' if label == 0 else 'contradiction' if label == 1 \ + else 'neutral' + + +# Defined in file: ./chapter_recommender-systems/movielens.md +d2l.DATA_HUB['ml-100k'] = ( + 'http://files.grouplens.org/datasets/movielens/ml-100k.zip', + 'cd4dcac4241c8a4ad7badc7ca635da8a69dddb83') + +def read_data_ml100k(): + data_dir = d2l.download_extract('ml-100k') + names = ['user_id', 'item_id', 'rating', 'timestamp'] + data = pd.read_csv(os.path.join(data_dir, 'u.data'), '\t', names=names, + engine='python') + num_users = data.user_id.unique().shape[0] + num_items = data.item_id.unique().shape[0] + return data, num_users, num_items + + +# Defined in file: ./chapter_recommender-systems/movielens.md +def split_data_ml100k(data, num_users, num_items, + split_mode='random', test_ratio=0.1): + """Split the dataset in random mode or seq-aware mode.""" + if split_mode == 'seq-aware': + train_items, test_items, train_list = {}, {}, [] + for line in data.itertuples(): + u, i, rating, time = line[1], line[2], line[3], line[4] + train_items.setdefault(u, []).append((u, i, rating, time)) + if u not in test_items or test_items[u][-1] < time: + test_items[u] = (i, rating, time) + for u in range(1, num_users + 1): + train_list.extend(sorted(train_items[u], key=lambda k: k[3])) + test_data = [(key, *value) for key, value in test_items.items()] + train_data = [item for item in train_list if item not in test_data] + train_data = pd.DataFrame(train_data) + test_data = pd.DataFrame(test_data) + else: + mask = [True if x == 1 else False for x in np.random.uniform( + 0, 1, (len(data))) < 1 - test_ratio] + neg_mask = [not x for x in mask] + train_data, test_data = data[mask], data[neg_mask] + return train_data, test_data + + +# Defined in file: ./chapter_recommender-systems/movielens.md +def load_data_ml100k(data, num_users, num_items, feedback='explicit'): + users, items, scores = [], [], [] + inter = np.zeros((num_items, num_users)) if feedback == 'explicit' else {} + for line in data.itertuples(): + user_index, item_index = int(line[1] - 1), int(line[2] - 1) + score = int(line[3]) if feedback == 'explicit' else 1 + users.append(user_index) + items.append(item_index) + scores.append(score) + if feedback == 'implicit': + inter.setdefault(user_index, []).append(item_index) + else: + inter[item_index, user_index] = score + return users, items, scores, inter + + +# Defined in file: ./chapter_recommender-systems/movielens.md +def split_and_load_ml100k(split_mode='seq-aware', feedback='explicit', + test_ratio=0.1, batch_size=256): + data, num_users, num_items = read_data_ml100k() + train_data, test_data = split_data_ml100k( + data, num_users, num_items, split_mode, test_ratio) + train_u, train_i, train_r, _ = load_data_ml100k( + train_data, num_users, num_items, feedback) + test_u, test_i, test_r, _ = load_data_ml100k( + test_data, num_users, num_items, feedback) + train_set = gluon.data.ArrayDataset( + np.array(train_u), np.array(train_i), np.array(train_r)) + test_set = gluon.data.ArrayDataset( + np.array(test_u), np.array(test_i), np.array(test_r)) + train_iter = gluon.data.DataLoader( + train_set, shuffle=True, last_batch='rollover', + batch_size=batch_size) + test_iter = gluon.data.DataLoader( + test_set, batch_size=batch_size) + return num_users, num_items, train_iter, test_iter + + +# Defined in file: ./chapter_recommender-systems/mf.md +def train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus(), evaluator=None, + **kwargs): + timer = d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 2], + legend=['train loss', 'test RMSE']) + for epoch in range(num_epochs): + metric, l = d2l.Accumulator(3), 0. + for i, values in enumerate(train_iter): + timer.start() + input_data = [] + values = values if isinstance(values, list) else [values] + for v in values: + input_data.append(gluon.utils.split_and_load(v, devices)) + train_feat = input_data[0:-1] if len(values) > 1 else input_data + train_label = input_data[-1] + with autograd.record(): + preds = [net(*t) for t in zip(*train_feat)] + ls = [loss(p, s) for p, s in zip(preds, train_label)] + [l.backward() for l in ls] + l += sum([l.asnumpy() for l in ls]).mean() / len(devices) + trainer.step(values[0].shape[0]) + metric.add(l, values[0].shape[0], values[0].size) + timer.stop() + if len(kwargs) > 0: # It will be used in section AutoRec + test_rmse = evaluator(net, test_iter, kwargs['inter_mat'], + devices) + else: + test_rmse = evaluator(net, test_iter, devices) + train_l = l / (i + 1) + animator.add(epoch + 1, (train_l, test_rmse)) + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'test RMSE {test_rmse:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') + + +# Defined in file: ./chapter_recommender-systems/ranking.md +class BPRLoss(gluon.loss.Loss): + def __init__(self, weight=None, batch_axis=0, **kwargs): + super(BPRLoss, self).__init__(weight=None, batch_axis=0, **kwargs) + + def forward(self, positive, negative): + distances = positive - negative + loss = - np.sum(np.log(npx.sigmoid(distances)), 0, keepdims=True) + return loss + + +# Defined in file: ./chapter_recommender-systems/ranking.md +class HingeLossbRec(gluon.loss.Loss): + def __init__(self, weight=None, batch_axis=0, **kwargs): + super(HingeLossbRec, self).__init__(weight=None, batch_axis=0, + **kwargs) + + def forward(self, positive, negative, margin=1): + distances = positive - negative + loss = np.sum(np.maximum(- distances + margin, 0)) + return loss + + +# Defined in file: ./chapter_recommender-systems/neumf.md +def hit_and_auc(rankedlist, test_matrix, k): + hits_k = [(idx, val) for idx, val in enumerate(rankedlist[:k]) + if val in set(test_matrix)] + hits_all = [(idx, val) for idx, val in enumerate(rankedlist) + if val in set(test_matrix)] + max = len(rankedlist) - 1 + auc = 1.0 * (max - hits_all[0][0]) / max if len(hits_all) > 0 else 0 + return len(hits_k), auc + + +# Defined in file: ./chapter_recommender-systems/neumf.md +def evaluate_ranking(net, test_input, seq, candidates, num_users, num_items, + devices): + ranked_list, ranked_items, hit_rate, auc = {}, {}, [], [] + all_items = set([i for i in range(num_users)]) + for u in range(num_users): + neg_items = list(all_items - set(candidates[int(u)])) + user_ids, item_ids, x, scores = [], [], [], [] + [item_ids.append(i) for i in neg_items] + [user_ids.append(u) for _ in neg_items] + x.extend([np.array(user_ids)]) + if seq is not None: + x.append(seq[user_ids, :]) + x.extend([np.array(item_ids)]) + test_data_iter = gluon.data.DataLoader( + gluon.data.ArrayDataset(*x), shuffle=False, last_batch="keep", + batch_size=1024) + for index, values in enumerate(test_data_iter): + x = [gluon.utils.split_and_load(v, devices, even_split=False) + for v in values] + scores.extend([list(net(*t).asnumpy()) for t in zip(*x)]) + scores = [item for sublist in scores for item in sublist] + item_scores = list(zip(item_ids, scores)) + ranked_list[u] = sorted(item_scores, key=lambda t: t[1], reverse=True) + ranked_items[u] = [r[0] for r in ranked_list[u]] + temp = hit_and_auc(ranked_items[u], test_input[u], 50) + hit_rate.append(temp[0]) + auc.append(temp[1]) + return np.mean(np.array(hit_rate)), np.mean(np.array(auc)) + + +# Defined in file: ./chapter_recommender-systems/neumf.md +def train_ranking(net, train_iter, test_iter, loss, trainer, test_seq_iter, + num_users, num_items, num_epochs, devices, evaluator, + candidates, eval_step=1): + timer, hit_rate, auc = d2l.Timer(), 0, 0 + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['test hit rate', 'test AUC']) + for epoch in range(num_epochs): + metric, l = d2l.Accumulator(3), 0. + for i, values in enumerate(train_iter): + input_data = [] + for v in values: + input_data.append(gluon.utils.split_and_load(v, devices)) + with autograd.record(): + p_pos = [net(*t) for t in zip(*input_data[0:-1])] + p_neg = [net(*t) for t in zip(*input_data[0:-2], + input_data[-1])] + ls = [loss(p, n) for p, n in zip(p_pos, p_neg)] + [l.backward(retain_graph=False) for l in ls] + l += sum([l.asnumpy() for l in ls]).mean()/len(devices) + trainer.step(values[0].shape[0]) + metric.add(l, values[0].shape[0], values[0].size) + timer.stop() + with autograd.predict_mode(): + if (epoch + 1) % eval_step == 0: + hit_rate, auc = evaluator(net, test_iter, test_seq_iter, + candidates, num_users, num_items, + devices) + animator.add(epoch + 1, (hit_rate, auc)) + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'test hit rate {float(hit_rate):.3f}, test AUC {float(auc):.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') + + +# Defined in file: ./chapter_recommender-systems/ctr.md +d2l.DATA_HUB['ctr'] = (d2l.DATA_URL + 'ctr.zip', + 'e18327c48c8e8e5c23da714dd614e390d369843f') + + +# Defined in file: ./chapter_recommender-systems/ctr.md +class CTRDataset(gluon.data.Dataset): + def __init__(self, data_path, feat_mapper=None, defaults=None, + min_threshold=4, num_feat=34): + self.NUM_FEATS, self.count, self.data = num_feat, 0, {} + feat_cnts = defaultdict(lambda: defaultdict(int)) + self.feat_mapper, self.defaults = feat_mapper, defaults + self.field_dims = np.zeros(self.NUM_FEATS, dtype=np.int64) + with open(data_path) as f: + for line in f: + instance = {} + values = line.rstrip('\n').split('\t') + if len(values) != self.NUM_FEATS + 1: + continue + label = np.float32([0, 0]) + label[int(values[0])] = 1 + instance['y'] = [np.float32(values[0])] + for i in range(1, self.NUM_FEATS + 1): + feat_cnts[i][values[i]] += 1 + instance.setdefault('x', []).append(values[i]) + self.data[self.count] = instance + self.count = self.count + 1 + if self.feat_mapper is None and self.defaults is None: + feat_mapper = {i: {feat for feat, c in cnt.items() if c >= + min_threshold} for i, cnt in feat_cnts.items()} + self.feat_mapper = {i: {feat: idx for idx, feat in enumerate(cnt)} + for i, cnt in feat_mapper.items()} + self.defaults = {i: len(cnt) for i, cnt in feat_mapper.items()} + for i, fm in self.feat_mapper.items(): + self.field_dims[i - 1] = len(fm) + 1 + self.offsets = np.array((0, *np.cumsum(self.field_dims).asnumpy() + [:-1])) + + def __len__(self): + return self.count + + def __getitem__(self, idx): + feat = np.array([self.feat_mapper[i + 1].get(v, self.defaults[i + 1]) + for i, v in enumerate(self.data[idx]['x'])]) + return feat + self.offsets, self.data[idx]['y'] + + +# Defined in file: ./chapter_generative-adversarial-networks/gan.md +def update_D(X, Z, net_D, net_G, loss, trainer_D): + """Update discriminator.""" + batch_size = X.shape[0] + ones = np.ones((batch_size,), ctx=X.ctx) + zeros = np.zeros((batch_size,), ctx=X.ctx) + with autograd.record(): + real_Y = net_D(X) + fake_X = net_G(Z) + # Do not need to compute gradient for `net_G`, detach it from + # computing gradients. + fake_Y = net_D(fake_X.detach()) + loss_D = (loss(real_Y, ones) + loss(fake_Y, zeros)) / 2 + loss_D.backward() + trainer_D.step(batch_size) + return float(loss_D.sum()) + + +# Defined in file: ./chapter_generative-adversarial-networks/gan.md +def update_G(Z, net_D, net_G, loss, trainer_G): + """Update generator.""" + batch_size = Z.shape[0] + ones = np.ones((batch_size,), ctx=Z.ctx) + with autograd.record(): + # We could reuse `fake_X` from `update_D` to save computation + fake_X = net_G(Z) + # Recomputing `fake_Y` is needed since `net_D` is changed + fake_Y = net_D(fake_X) + loss_G = loss(fake_Y, ones) + loss_G.backward() + trainer_G.step(batch_size) + return float(loss_G.sum()) + + +# Defined in file: ./chapter_generative-adversarial-networks/dcgan.md +d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip', + 'c065c0e2593b8b161a2d7873e42418bf6a21106c') + + # Alias defined in config.ini size = lambda a: a.size transpose = lambda a: a.T diff --git a/d2l/tensorflow.py b/d2l/tensorflow.py index aa1d597c6..02a573589 100644 --- a/d2l/tensorflow.py +++ b/d2l/tensorflow.py @@ -4,22 +4,21 @@ # Defined in file: ./chapter_preface/index.md import collections -import hashlib +from collections import defaultdict +from IPython import display import math +from matplotlib import pyplot as plt import os +import pandas as pd import random import re import shutil import sys import tarfile import time -import zipfile -from collections import defaultdict -import pandas as pd import requests -from IPython import display -from matplotlib import pyplot as plt - +import zipfile +import hashlib d2l = sys.modules[__name__] @@ -30,20 +29,20 @@ # Defined in file: ./chapter_preliminaries/calculus.md def use_svg_display(): - """使用svg格式在Jupyter中显示绘图。""" + """Use the svg format to display a plot in Jupyter.""" display.set_matplotlib_formats('svg') # Defined in file: ./chapter_preliminaries/calculus.md def set_figsize(figsize=(3.5, 2.5)): - """设置matplotlib的图表大小。""" + """Set the figure size for matplotlib.""" use_svg_display() d2l.plt.rcParams['figure.figsize'] = figsize # Defined in file: ./chapter_preliminaries/calculus.md def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): - """设置matplotlib的轴。""" + """Set the axes for matplotlib.""" axes.set_xlabel(xlabel) axes.set_ylabel(ylabel) axes.set_xscale(xscale) @@ -59,17 +58,17 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): - """绘制数据点。""" + """Plot data points.""" if legend is None: legend = [] set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # 如果 `X` 有一个轴,输出True + # Return True if `X` (tensor or list) has 1 axis def has_one_axis(X): - return (hasattr(X, "ndim") and X.ndim == 1 or - isinstance(X, list) and not hasattr(X[0], "__len__")) + return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) + and not hasattr(X[0], "__len__")) if has_one_axis(X): X = [X] @@ -90,36 +89,36 @@ def has_one_axis(X): # Defined in file: ./chapter_linear-networks/linear-regression.md class Timer: - """记录多次运行时间。""" + """Record multiple running times.""" def __init__(self): self.times = [] self.start() def start(self): - """启动计时器。""" + """Start the timer.""" self.tik = time.time() def stop(self): - """停止计时器并将时间记录在列表中。""" + """Stop the timer and record the time in a list.""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): - """返回平均时间。""" + """Return the average time.""" return sum(self.times) / len(self.times) def sum(self): - """返回时间总和。""" + """Return the sum of time.""" return sum(self.times) def cumsum(self): - """返回累计时间。""" + """Return the accumulated time.""" return np.array(self.times).cumsum().tolist() # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def synthetic_data(w, b, num_examples): - """生成 y = Xw + b + 噪声。""" + """Generate y = Xw + b + noise.""" X = d2l.zeros((num_examples, w.shape[0])) X += tf.random.normal(shape=X.shape) y = d2l.matmul(X, tf.reshape(w, (-1, 1))) + b @@ -130,26 +129,26 @@ def synthetic_data(w, b, num_examples): # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def linreg(X, w, b): - """线性回归模型。""" + """The linear regression model.""" return d2l.matmul(X, w) + b # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def squared_loss(y_hat, y): - """均方损失。""" - return (y_hat - d2l.reshape(y, y_hat.shape))**2 / 2 + """Squared loss.""" + return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def sgd(params, grads, lr, batch_size): - """小批量随机梯度下降。""" + """Minibatch stochastic gradient descent.""" for param, grad in zip(params, grads): - param.assign_sub(lr * grad / batch_size) + param.assign_sub(lr*grad/batch_size) # Defined in file: ./chapter_linear-networks/linear-regression-concise.md def load_array(data_arrays, batch_size, is_train=True): - """构造一个TensorFlow数据迭代器。""" + """Construct a TensorFlow data iterator.""" dataset = tf.data.Dataset.from_tensor_slices(data_arrays) if is_train: dataset = dataset.shuffle(buffer_size=1000) @@ -159,16 +158,15 @@ def load_array(data_arrays, batch_size, is_train=True): # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def get_fashion_mnist_labels(labels): - """返回Fashion-MNIST数据集的文本标签。""" - text_labels = [ - 't-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', - 'sneaker', 'bag', 'ankle boot'] + """Return text labels for the Fashion-MNIST dataset.""" + text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', + 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot'] return [text_labels[int(i)] for i in labels] # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): - """绘制图像列表。""" + """Plot a list of images.""" figsize = (num_cols * scale, num_rows * scale) _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize) axes = axes.flatten() @@ -183,34 +181,34 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def load_data_fashion_mnist(batch_size, resize=None): - """下载Fashion-MNIST数据集,然后将其加载到内存中。""" + """Download the Fashion-MNIST dataset and then load it into memory.""" mnist_train, mnist_test = tf.keras.datasets.fashion_mnist.load_data() - # 将所有数字除以255,使所有像素值介于0和1之间,在最后添加一个批处理维度, - # 并将标签转换为int32。 + # Divide all numbers by 255 so that all pixel values are between + # 0 and 1, add a batch dimension at the last. And cast label to int32 process = lambda X, y: (tf.expand_dims(X, axis=3) / 255, tf.cast(y, dtype='int32')) - resize_fn = lambda X, y: (tf.image.resize_with_pad(X, resize, resize) - if resize else X, y) - return (tf.data.Dataset.from_tensor_slices( - process(*mnist_train)).batch(batch_size).shuffle(len( - mnist_train[0])).map(resize_fn), - tf.data.Dataset.from_tensor_slices( - process(*mnist_test)).batch(batch_size).map(resize_fn)) + resize_fn = lambda X, y: ( + tf.image.resize_with_pad(X, resize, resize) if resize else X, y) + return ( + tf.data.Dataset.from_tensor_slices(process(*mnist_train)).batch( + batch_size).shuffle(len(mnist_train[0])).map(resize_fn), + tf.data.Dataset.from_tensor_slices(process(*mnist_test)).batch( + batch_size).map(resize_fn)) # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def accuracy(y_hat, y): - """计算预测正确的数量。""" + """Compute the number of correct predictions.""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: - y_hat = d2l.argmax(y_hat, axis=1) + y_hat = d2l.argmax(y_hat, axis=1) cmp = d2l.astype(y_hat, y.dtype) == y return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def evaluate_accuracy(net, data_iter): - """计算在指定数据集上模型的精度。""" - metric = Accumulator(2) # 正确预测数、预测总数 + """Compute the accuracy for a model on a dataset.""" + metric = Accumulator(2) # No. of correct predictions, no. of predictions for X, y in data_iter: metric.add(accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] @@ -218,7 +216,7 @@ def evaluate_accuracy(net, data_iter): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Accumulator: - """在`n`个变量上累加。""" + """For accumulating sums over `n` variables.""" def __init__(self, n): self.data = [0.0] * n @@ -234,15 +232,16 @@ def __getitem__(self, idx): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def train_epoch_ch3(net, train_iter, loss, updater): - """训练模型一个迭代周期(定义见第3章)。""" - # 训练损失总和、训练准确度总和、样本数 + """The training loop defined in Chapter 3.""" + # Sum of training loss, sum of training accuracy, no. of examples metric = Accumulator(3) for X, y in train_iter: - # 计算梯度并更新参数 + # Compute gradients and update parameters with tf.GradientTape() as tape: y_hat = net(X) - # Keras内置的损失接受的是(标签,预测),这不同于用户在本书中的实现。 - # 本书的实现接受(预测,标签),例如我们上面实现的“交叉熵” + # Keras implementations for loss takes (labels, predictions) + # instead of (predictions, labels) that users might implement + # in this book, e.g. `cross_entropy` that we implemented above if isinstance(loss, tf.keras.losses.Loss): l = loss(y, y_hat) else: @@ -253,35 +252,35 @@ def train_epoch_ch3(net, train_iter, loss, updater): updater.apply_gradients(zip(grads, params)) else: updater(X.shape[0], tape.gradient(l, updater.params)) - # Keras的`loss`默认返回一个批量的平均损失 + # Keras loss by default returns the average loss in a batch l_sum = l * float(tf.size(y)) if isinstance( loss, tf.keras.losses.Loss) else tf.reduce_sum(l) metric.add(l_sum, accuracy(y_hat, y), tf.size(y)) - # 返回训练损失和训练准确率 + # Return training loss and training accuracy return metric[0] / metric[2], metric[1] / metric[2] # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Animator: - """在动画中绘制数据。""" + """For plotting data in animation.""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): - # 增量地绘制多条线 + # Incrementally plot multiple lines if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: - self.axes = [self.axes,] - # 使用lambda函数捕获参数 - self.config_axes = lambda: d2l.set_axes(self.axes[ - 0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) + self.axes = [self.axes, ] + # Use a lambda function to capture arguments + self.config_axes = lambda: d2l.set_axes( + self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): - # 向图表中添加多个数据点 + # Add multiple data points into the figure if not hasattr(y, "__len__"): y = [y] n = len(y) @@ -305,7 +304,7 @@ def add(self, x, y): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): - """训练模型(定义见第3章)。""" + """Train a model (defined in Chapter 3).""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train loss', 'train acc', 'test acc']) for epoch in range(num_epochs): @@ -320,7 +319,7 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Updater(): - """用小批量随机梯度下降法更新参数。""" + """For updating parameters using minibatch stochastic gradient descent.""" def __init__(self, params, lr): self.params = params self.lr = lr @@ -331,20 +330,20 @@ def __call__(self, batch_size, grads): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def predict_ch3(net, test_iter, n=6): - """预测标签(定义见第3章)。""" + """Predict labels (defined in Chapter 3).""" for X, y in test_iter: break trues = d2l.get_fashion_mnist_labels(y) preds = d2l.get_fashion_mnist_labels(d2l.argmax(net(X), axis=1)) - titles = [true + '\n' + pred for true, pred in zip(trues, preds)] - d2l.show_images(d2l.reshape(X[0:n], (n, 28, 28)), 1, n, - titles=titles[0:n]) + titles = [true +'\n' + pred for true, pred in zip(trues, preds)] + d2l.show_images( + d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) # Defined in file: ./chapter_multilayer-perceptrons/underfit-overfit.md def evaluate_loss(net, data_iter, loss): - """评估给定数据集上模型的损失。""" - metric = d2l.Accumulator(2) # 损失的总和, 样本数量 + """Evaluate the loss of a model on the given dataset.""" + metric = d2l.Accumulator(2) # Sum of losses, no. of examples for X, y in data_iter: l = loss(net(X), y) metric.add(d2l.reduce_sum(l), d2l.size(l)) @@ -358,8 +357,8 @@ def evaluate_loss(net, data_iter, loss): # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md def download(name, cache_dir=os.path.join('..', 'data')): - """下载一个DATA_HUB中的文件,返回本地文件名。""" - assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}." + """Download a file inserted into DATA_HUB, return the local filename.""" + assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}." url, sha1_hash = DATA_HUB[name] os.makedirs(cache_dir, exist_ok=True) fname = os.path.join(cache_dir, url.split('/')[-1]) @@ -373,7 +372,7 @@ def download(name, cache_dir=os.path.join('..', 'data')): sha1.update(data) if sha1.hexdigest() == sha1_hash: return fname # Hit cache - print(f'正在从{url}下载{fname}...') + print(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True, verify=True) with open(fname, 'wb') as f: f.write(r.content) @@ -382,7 +381,7 @@ def download(name, cache_dir=os.path.join('..', 'data')): # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md def download_extract(name, folder=None): - """下载并解压zip/tar文件。""" + """Download and extract a zip/tar file.""" fname = download(name) base_dir = os.path.dirname(fname) data_dir, ext = os.path.splitext(fname) @@ -391,33 +390,35 @@ def download_extract(name, folder=None): elif ext in ('.tar', '.gz'): fp = tarfile.open(fname, 'r') else: - assert False, '只有zip/tar文件可以被解压缩。' + assert False, 'Only zip/tar files can be extracted.' fp.extractall(base_dir) return os.path.join(base_dir, folder) if folder else data_dir def download_all(): - """下载DATA_HUB中的所有文件。""" + """Download all files in the DATA_HUB.""" for name in DATA_HUB: download(name) # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md -DATA_HUB['kaggle_house_train'] = (DATA_URL + 'kaggle_house_pred_train.csv', - '585e9cc93e70b39160e7921475f9bcd7d31219ce') +DATA_HUB['kaggle_house_train'] = ( + DATA_URL + 'kaggle_house_pred_train.csv', + '585e9cc93e70b39160e7921475f9bcd7d31219ce') -DATA_HUB['kaggle_house_test'] = (DATA_URL + 'kaggle_house_pred_test.csv', - 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') +DATA_HUB['kaggle_house_test'] = ( + DATA_URL + 'kaggle_house_pred_test.csv', + 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') # Defined in file: ./chapter_deep-learning-computation/use-gpu.md def try_gpu(i=0): - """如果存在,则返回gpu(i),否则返回cpu()。""" + """Return gpu(i) if exists, otherwise return cpu().""" if len(tf.config.experimental.list_physical_devices('GPU')) >= i + 1: return tf.device(f'/GPU:{i}') return tf.device('/CPU:0') def try_all_gpus(): - """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]。""" + """Return all available GPUs, or [cpu(),] if no GPU exists.""" num_gpus = len(tf.config.experimental.list_physical_devices('GPU')) devices = [tf.device(f'/GPU:{i}') for i in range(num_gpus)] return devices if devices else [tf.device('/CPU:0')] @@ -425,12 +426,13 @@ def try_all_gpus(): # Defined in file: ./chapter_convolutional-neural-networks/conv-layer.md def corr2d(X, K): - """计算二维互相关运算。""" + """Compute 2D cross-correlation.""" h, w = K.shape Y = tf.Variable(tf.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))) for i in range(Y.shape[0]): for j in range(Y.shape[1]): - Y[i, j].assign(tf.reduce_sum(X[i:i + h, j:j + w] * K)) + Y[i, j].assign(tf.reduce_sum( + X[i: i + h, j: j + w] * K)) return Y @@ -440,21 +442,19 @@ class TrainCallback(tf.keras.callbacks.Callback): def __init__(self, net, train_iter, test_iter, num_epochs, device_name): self.timer = d2l.Timer() self.animator = d2l.Animator( - xlabel='epoch', xlim=[1, num_epochs], - legend=['train loss', 'train acc', 'test acc']) + xlabel='epoch', xlim=[1, num_epochs], legend=[ + 'train loss', 'train acc', 'test acc']) self.net = net self.train_iter = train_iter self.test_iter = test_iter self.num_epochs = num_epochs self.device_name = device_name - def on_epoch_begin(self, epoch, logs=None): self.timer.start() - def on_epoch_end(self, epoch, logs): self.timer.stop() - test_acc = self.net.evaluate(self.test_iter, verbose=0, - return_dict=True)['accuracy'] + test_acc = self.net.evaluate( + self.test_iter, verbose=0, return_dict=True)['accuracy'] metrics = (logs['loss'], logs['accuracy'], test_acc) self.animator.add(epoch + 1, metrics) if epoch == self.num_epochs - 1: @@ -466,7 +466,8 @@ def on_epoch_end(self, epoch, logs): print(f'{num_examples / self.timer.avg():.1f} examples/sec on ' f'{str(self.device_name)}') -def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): +def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, + device=d2l.try_gpu()): """Train a model with a GPU (defined in Chapter 6).""" device_name = device._device_name strategy = tf.distribute.OneDeviceStrategy(device_name) @@ -483,16 +484,17 @@ def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): # Defined in file: ./chapter_convolutional-modern/resnet.md class Residual(tf.keras.Model): + """The Residual block of ResNet.""" def __init__(self, num_channels, use_1x1conv=False, strides=1): super().__init__() - self.conv1 = tf.keras.layers.Conv2D(num_channels, padding='same', - kernel_size=3, strides=strides) - self.conv2 = tf.keras.layers.Conv2D(num_channels, kernel_size=3, - padding='same') + self.conv1 = tf.keras.layers.Conv2D( + num_channels, padding='same', kernel_size=3, strides=strides) + self.conv2 = tf.keras.layers.Conv2D( + num_channels, kernel_size=3, padding='same') self.conv3 = None if use_1x1conv: - self.conv3 = tf.keras.layers.Conv2D(num_channels, kernel_size=1, - strides=strides) + self.conv3 = tf.keras.layers.Conv2D( + num_channels, kernel_size=1, strides=strides) self.bn1 = tf.keras.layers.BatchNormalization() self.bn2 = tf.keras.layers.BatchNormalization() @@ -518,32 +520,31 @@ def read_time_machine(): # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def tokenize(lines, token='word'): - """将文本行拆分为单词或字符标记。""" + """Split text lines into word or character tokens.""" if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: - print('错误:未知令牌类型:' + token) + print('ERROR: unknown token type: ' + token) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md class Vocab: - """文本词表""" + """Vocabulary for text.""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: - reserved_tokens = [] - # 按出现频率排序 + reserved_tokens = [] + # Sort according to frequencies counter = count_corpus(tokens) self.token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) - # 未知标记的索引为0 + # The index for the unknown token is 0 self.unk, uniq_tokens = 0, [''] + reserved_tokens - uniq_tokens += [ - token for token, freq in self.token_freqs - if freq >= min_freq and token not in uniq_tokens] + uniq_tokens += [token for token, freq in self.token_freqs + if freq >= min_freq and token not in uniq_tokens] self.idx_to_token, self.token_to_idx = [], dict() for token in uniq_tokens: self.idx_to_token.append(token) @@ -564,21 +565,21 @@ def to_tokens(self, indices): def count_corpus(tokens): """Count token frequencies.""" - # 这里的 `tokens` 是1D列表或2D列表 + # Here `tokens` is a 1D list or 2D list if len(tokens) == 0 or isinstance(tokens[0], list): - # 将令牌列表展平 + # Flatten a list of token lists into a list of tokens tokens = [token for line in tokens for token in line] return collections.Counter(tokens) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def load_corpus_time_machine(max_tokens=-1): - """返回时光机器数据集的令牌索引和词汇表。""" + """Return token indices and the vocabulary of the time machine dataset.""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, - # 所以将所有文本行展平到一个列表中 + # Since each text line in the time machine dataset is not necessarily a + # sentence or a paragraph, flatten all the text lines into a single list corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] @@ -587,24 +588,28 @@ def load_corpus_time_machine(max_tokens=-1): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_random(corpus, batch_size, num_steps): - """使用随机抽样生成一小批子序列。""" - # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 + """Generate a minibatch of subsequences using random sampling.""" + # Start with a random offset (inclusive of `num_steps - 1`) to partition a + # sequence corpus = corpus[random.randint(0, num_steps - 1):] - # 减去1,因为我们需要考虑标签 + # Subtract 1 since we need to account for labels num_subseqs = (len(corpus) - 1) // num_steps - # 长度为`num_steps`的子序列的起始索引 + # The starting indices for subsequences of length `num_steps` initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 + # In random sampling, the subsequences from two adjacent random + # minibatches during iteration are not necessarily adjacent on the + # original sequence random.shuffle(initial_indices) def data(pos): - # 返回从`pos`开始的长度为`num_steps`的序列 - return corpus[pos:pos + num_steps] + # Return a sequence of length `num_steps` starting from `pos` + return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): - # 这里,`initial_indices`包含子序列的随机起始索引 - initial_indices_per_batch = initial_indices[i:i + batch_size] + # Here, `initial_indices` contains randomized starting indices for + # subsequences + initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield d2l.tensor(X), d2l.tensor(Y) @@ -612,24 +617,24 @@ def data(pos): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_sequential(corpus, batch_size, num_steps): - """使用顺序分区生成一小批子序列。""" - # 从随机偏移量开始划分序列 + """Generate a minibatch of subsequences using sequential partitioning.""" + # Start with a random offset to partition a sequence offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size - Xs = d2l.tensor(corpus[offset:offset + num_tokens]) - Ys = d2l.tensor(corpus[offset + 1:offset + 1 + num_tokens]) + Xs = d2l.tensor(corpus[offset: offset + num_tokens]) + Ys = d2l.tensor(corpus[offset + 1: offset + 1 + num_tokens]) Xs = d2l.reshape(Xs, (batch_size, -1)) Ys = d2l.reshape(Ys, (batch_size, -1)) num_batches = Xs.shape[1] // num_steps for i in range(0, num_batches * num_steps, num_steps): - X = Xs[:, i:i + num_steps] - Y = Ys[:, i:i + num_steps] + X = Xs[:, i: i + num_steps] + Y = Ys[:, i: i + num_steps] yield X, Y # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md class SeqDataLoader: - """加载序列数据的迭代器。""" + """An iterator to load sequence data.""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): if use_random_iter: self.data_iter_fn = d2l.seq_data_iter_random @@ -643,18 +648,19 @@ def __iter__(self): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md -def load_data_time_machine(batch_size, num_steps, use_random_iter=False, - max_tokens=10000): - """返回时光机器数据集的迭代器和词表。""" - data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, - max_tokens) +def load_data_time_machine(batch_size, num_steps, + use_random_iter=False, max_tokens=10000): + """Return the iterator and the vocabulary of the time machine dataset.""" + data_iter = SeqDataLoader( + batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md class RNNModelScratch: - """从零开始实现的循环神经网络模型""" - def __init__(self, vocab_size, num_hiddens, init_state, forward_fn): + """A RNN Model implemented from scratch.""" + def __init__(self, vocab_size, num_hiddens, + init_state, forward_fn): self.vocab_size, self.num_hiddens = vocab_size, num_hiddens self.init_state, self.forward_fn = init_state, forward_fn @@ -669,14 +675,14 @@ def begin_state(self, batch_size): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def predict_ch8(prefix, num_preds, net, vocab, params): - """在`prefix`后面生成新字符。""" + """Generate new characters following the `prefix`.""" state = net.begin_state(batch_size=1) outputs = [vocab[prefix[0]]] get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]]), (1, 1)).numpy() - for y in prefix[1:]: # 预热期 + for y in prefix[1:]: # Warm-up period _, state = net(get_input(), state, params) outputs.append(vocab[y]) - for _ in range(num_preds): # 预测`num_preds`步 + for _ in range(num_preds): # Predict `num_preds` steps y, state = net(get_input(), state, params) outputs.append(int(y.numpy().argmax(axis=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) @@ -684,10 +690,10 @@ def predict_ch8(prefix, num_preds, net, vocab, params): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def grad_clipping(grads, theta): - """裁剪梯度。""" + """Clip the gradient.""" theta = tf.constant(theta, dtype=tf.float32) - norm = tf.math.sqrt( - sum((tf.reduce_sum(grad**2)).numpy() for grad in grads)) + norm = tf.math.sqrt(sum((tf.reduce_sum(grad ** 2)).numpy() + for grad in grads)) norm = tf.cast(norm, tf.float32) new_grad = [] if tf.greater(norm, theta): @@ -701,23 +707,24 @@ def grad_clipping(grads, theta): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def train_epoch_ch8(net, train_iter, loss, updater, params, use_random_iter): - """训练模型一个迭代周期(定义见第8章)。""" + """Train a model within one epoch (defined in Chapter 8).""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失之和, 标记数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for X, Y in train_iter: if state is None or use_random_iter: - # 在第一次迭代或使用随机抽样时初始化`state` + # Initialize `state` when either it is the first iteration or + # using random sampling state = net.begin_state(batch_size=X.shape[0]) with tf.GradientTape(persistent=True) as g: g.watch(params) - y_hat, state = net(X, state, params) + y_hat, state= net(X, state, params) y = d2l.reshape(tf.transpose(Y), (-1)) l = loss(y, y_hat) grads = g.gradient(l, params) grads = grad_clipping(grads, 1) updater.apply_gradients(zip(grads, params)) - - # Keras默认返回一个批量中的平均损失 + + # Keras loss by default returns the average loss in a batch # l_sum = l * float(d2l.size(y)) if isinstance( # loss, tf.keras.losses.Loss) else tf.reduce_sum(l) metric.add(l * d2l.size(y), d2l.size(y)) @@ -727,7 +734,7 @@ def train_epoch_ch8(net, train_iter, loss, updater, params, use_random_iter): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def train_ch8(net, train_iter, vocab, num_hiddens, lr, num_epochs, strategy, use_random_iter=False): - """训练模型(定义见第8章)。""" + """Train a model (defined in Chapter 8).""" with strategy.scope(): params = get_params(len(vocab), num_hiddens) loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) @@ -735,30 +742,253 @@ def train_ch8(net, train_iter, vocab, num_hiddens, lr, num_epochs, strategy, animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, params) - # 训练和预测 + # Train and predict for epoch in range(num_epochs): - ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, params, - use_random_iter) + ppl, speed = train_epoch_ch8( + net, train_iter, loss, updater, params, use_random_iter) if (epoch + 1) % 10 == 0: print(predict('time traveller')) animator.add(epoch + 1, [ppl]) device = d2l.try_gpu()._device_name - print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}') + print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') print(predict('time traveller')) print(predict('traveller')) -# Defined in file: ./chapter_computational-performance/hybridize.md -class Benchmark: - def __init__(self, description='Done'): - self.description = description - - def __enter__(self): - self.timer = d2l.Timer() - return self - - def __exit__(self, *args): - print(f'{self.description}: {self.timer.stop():.4f} sec') +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', + '94646ad1522d915e7b0f9296181140edcf86a4f5') + +def read_data_nmt(): + """Load the English-French dataset.""" + data_dir = d2l.download_extract('fra-eng') + with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: + return f.read() + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def preprocess_nmt(text): + """Preprocess the English-French dataset.""" + def no_space(char, prev_char): + return char in set(',.!?') and prev_char != ' ' + + # Replace non-breaking space with space, and convert uppercase letters to + # lowercase ones + text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() + # Insert space between words and punctuation marks + out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char + for i, char in enumerate(text)] + return ''.join(out) + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def tokenize_nmt(text, num_examples=None): + """Tokenize the English-French dataset.""" + source, target = [], [] + for i, line in enumerate(text.split('\n')): + if num_examples and i > num_examples: + break + parts = line.split('\t') + if len(parts) == 2: + source.append(parts[0].split(' ')) + target.append(parts[1].split(' ')) + return source, target + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def truncate_pad(line, num_steps, padding_token): + """Truncate or pad sequences.""" + if len(line) > num_steps: + return line[:num_steps] # Truncate + return line + [padding_token] * (num_steps - len(line)) # Pad + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def build_array_nmt(lines, vocab, num_steps): + """Transform text sequences of machine translation into minibatches.""" + lines = [vocab[l] for l in lines] + lines = [l + [vocab['']] for l in lines] + array = d2l.tensor([truncate_pad( + l, num_steps, vocab['']) for l in lines]) + valid_len = d2l.reduce_sum( + d2l.astype(array != vocab[''], d2l.int32), 1) + return array, valid_len + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def load_data_nmt(batch_size, num_steps, num_examples=600): + """Return the iterator and the vocabularies of the translation dataset.""" + text = preprocess_nmt(read_data_nmt()) + source, target = tokenize_nmt(text, num_examples) + src_vocab = d2l.Vocab(source, min_freq=2, + reserved_tokens=['', '', '']) + tgt_vocab = d2l.Vocab(target, min_freq=2, + reserved_tokens=['', '', '']) + src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) + tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) + data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) + data_iter = d2l.load_array(data_arrays, batch_size) + return data_iter, src_vocab, tgt_vocab + + +# Defined in file: ./chapter_attention-mechanisms/attention-cues.md +def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), + cmap='Reds'): + d2l.use_svg_display() + num_rows, num_cols = matrices.shape[0], matrices.shape[1] + fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, + sharex=True, sharey=True, squeeze=False) + for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): + for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): + pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) + if i == num_rows - 1: + ax.set_xlabel(xlabel) + if j == 0: + ax.set_ylabel(ylabel) + if titles: + ax.set_title(titles[j]) + fig.colorbar(pcm, ax=axes, shrink=0.6); + + +# Defined in file: ./chapter_optimization/optimization-intro.md +def annotate(text, xy, xytext): + d2l.plt.gca().annotate(text, xy=xy, xytext=xytext, + arrowprops=dict(arrowstyle='->')) + + +# Defined in file: ./chapter_optimization/gd.md +def train_2d(trainer, steps=20): + """Optimize a 2-dim objective function with a customized trainer.""" + # s1 and s2 are internal state variables and will + # be used later in the chapter + x1, x2, s1, s2 = -5, -2, 0, 0 + results = [(x1, x2)] + for i in range(steps): + x1, x2, s1, s2 = trainer(x1, x2, s1, s2) + results.append((x1, x2)) + return results + +def show_trace_2d(f, results): + """Show the trace of 2D variables during optimization.""" + d2l.set_figsize() + d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') + x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1), + d2l.arange(-3.0, 1.0, 0.1)) + d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') + d2l.plt.xlabel('x1') + d2l.plt.ylabel('x2') + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat', + '76e5be1548fd8222e5074cf0faae75edff8cf93f') + +def get_data_ch11(batch_size=10, n=1500): + data = np.genfromtxt(d2l.download('airfoil'), + dtype=np.float32, delimiter='\t') + data = (data - data.mean(axis=0)) / data.std(axis=0) + data_iter = d2l.load_array((data[:n, :-1], data[:n, -1]), + batch_size, is_train=True) + return data_iter, data.shape[1]-1 + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +def train_ch11(trainer_fn, states, hyperparams, data_iter, + feature_dim, num_epochs=2): + # Initialization + w = tf.Variable(tf.random.normal(shape=(feature_dim, 1), + mean=0, stddev=0.01),trainable=True) + b = tf.Variable(tf.zeros(1), trainable=True) + + # Train + net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + + for _ in range(num_epochs): + for X, y in data_iter: + with tf.GradientTape() as g: + l = tf.math.reduce_mean(loss(net(X), y)) + + dw, db = g.gradient(l, [w, b]) + trainer_fn([w, b], [dw, db], states, hyperparams) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + p = n/X.shape[0] + q = p/tf.data.experimental.cardinality(data_iter).numpy() + r = (d2l.evaluate_loss(net, data_iter, loss),) + animator.add(q, r) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + return timer.cumsum(), animator.Y[0] + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=2): + # Initialization + net = tf.keras.Sequential() + net.add(tf.keras.layers.Dense(1, + kernel_initializer=tf.random_normal_initializer(stddev=0.01))) + optimizer = trainer_fn(**hyperparams) + loss = tf.keras.losses.MeanSquaredError() + # Note: L2 Loss = 1/2 * MSE Loss. TensorFlow has MSE Loss which is + # slightly different from MXNet's L2Loss by a factor of 2. Hence we halve + # the loss value to get L2Loss in TensorFlow + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + with tf.GradientTape() as g: + out = net(X) + l = loss(y, out)/2 + params = net.trainable_variables + grads = g.gradient(l, params) + optimizer.apply_gradients(zip(grads, params)) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + p = n/X.shape[0] + q = p/tf.data.experimental.cardinality(data_iter).numpy() + r = (d2l.evaluate_loss(net, data_iter, loss)/2,) + animator.add(q, r) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + + +# Defined in file: ./chapter_computer-vision/bounding-box.md +def box_corner_to_center(boxes): + """Convert from (upper_left, bottom_right) to (center, width, height)""" + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + cx = (x1 + x2) / 2 + cy = (y1 + y2) / 2 + w = x2 - x1 + h = y2 - y1 + boxes = d2l.stack((cx, cy, w, h), axis=-1) + return boxes + +def box_center_to_corner(boxes): + """Convert from (center, width, height) to (upper_left, bottom_right)""" + cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + x1 = cx - 0.5 * w + y1 = cy - 0.5 * h + x2 = cx + 0.5 * w + y2 = cy + 0.5 * h + boxes = d2l.stack((x1, y1, x2, y2), axis=-1) + return boxes + + +# Defined in file: ./chapter_computer-vision/bounding-box.md +def bbox_to_rect(bbox, color): + """Convert bounding box to matplotlib format.""" + # Convert the bounding box (top-left x, top-left y, bottom-right x, + # bottom-right y) format to matplotlib format: ((upper-left x, + # upper-left y), width, height) + return d2l.plt.Rectangle( + xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], + fill=False, edgecolor=color, linewidth=2) # Alias defined in config.ini diff --git a/d2l/torch.py b/d2l/torch.py index ee3705fba..e8c220841 100644 --- a/d2l/torch.py +++ b/d2l/torch.py @@ -4,22 +4,21 @@ # Defined in file: ./chapter_preface/index.md import collections -import hashlib +from collections import defaultdict +from IPython import display import math +from matplotlib import pyplot as plt import os +import pandas as pd import random import re import shutil import sys import tarfile import time -import zipfile -from collections import defaultdict -import pandas as pd import requests -from IPython import display -from matplotlib import pyplot as plt - +import zipfile +import hashlib d2l = sys.modules[__name__] @@ -27,29 +26,29 @@ import numpy as np import torch import torchvision -from PIL import Image from torch import nn from torch.nn import functional as F from torch.utils import data from torchvision import transforms +from PIL import Image # Defined in file: ./chapter_preliminaries/calculus.md def use_svg_display(): - """使用svg格式在Jupyter中显示绘图。""" + """Use the svg format to display a plot in Jupyter.""" display.set_matplotlib_formats('svg') # Defined in file: ./chapter_preliminaries/calculus.md def set_figsize(figsize=(3.5, 2.5)): - """设置matplotlib的图表大小。""" + """Set the figure size for matplotlib.""" use_svg_display() d2l.plt.rcParams['figure.figsize'] = figsize # Defined in file: ./chapter_preliminaries/calculus.md def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): - """设置matplotlib的轴。""" + """Set the axes for matplotlib.""" axes.set_xlabel(xlabel) axes.set_ylabel(ylabel) axes.set_xscale(xscale) @@ -65,17 +64,17 @@ def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend): def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None): - """绘制数据点。""" + """Plot data points.""" if legend is None: legend = [] set_figsize(figsize) axes = axes if axes else d2l.plt.gca() - # 如果 `X` 有一个轴,输出True + # Return True if `X` (tensor or list) has 1 axis def has_one_axis(X): - return (hasattr(X, "ndim") and X.ndim == 1 or - isinstance(X, list) and not hasattr(X[0], "__len__")) + return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list) + and not hasattr(X[0], "__len__")) if has_one_axis(X): X = [X] @@ -96,36 +95,36 @@ def has_one_axis(X): # Defined in file: ./chapter_linear-networks/linear-regression.md class Timer: - """记录多次运行时间。""" + """Record multiple running times.""" def __init__(self): self.times = [] self.start() def start(self): - """启动计时器。""" + """Start the timer.""" self.tik = time.time() def stop(self): - """停止计时器并将时间记录在列表中。""" + """Stop the timer and record the time in a list.""" self.times.append(time.time() - self.tik) return self.times[-1] def avg(self): - """返回平均时间。""" + """Return the average time.""" return sum(self.times) / len(self.times) def sum(self): - """返回时间总和。""" + """Return the sum of time.""" return sum(self.times) def cumsum(self): - """返回累计时间。""" + """Return the accumulated time.""" return np.array(self.times).cumsum().tolist() # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def synthetic_data(w, b, num_examples): - """生成 y = Xw + b + 噪声。""" + """Generate y = Xw + b + noise.""" X = d2l.normal(0, 1, (num_examples, len(w))) y = d2l.matmul(X, w) + b y += d2l.normal(0, 0.01, y.shape) @@ -134,19 +133,19 @@ def synthetic_data(w, b, num_examples): # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def linreg(X, w, b): - """线性回归模型。""" + """The linear regression model.""" return d2l.matmul(X, w) + b # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def squared_loss(y_hat, y): - """均方损失。""" - return (y_hat - d2l.reshape(y, y_hat.shape))**2 / 2 + """Squared loss.""" + return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2 # Defined in file: ./chapter_linear-networks/linear-regression-scratch.md def sgd(params, lr, batch_size): - """小批量随机梯度下降。""" + """Minibatch stochastic gradient descent.""" with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size @@ -155,17 +154,16 @@ def sgd(params, lr, batch_size): # Defined in file: ./chapter_linear-networks/linear-regression-concise.md def load_array(data_arrays, batch_size, is_train=True): - """构造一个PyTorch数据迭代器。""" + """Construct a PyTorch data iterator.""" dataset = data.TensorDataset(*data_arrays) return data.DataLoader(dataset, batch_size, shuffle=is_train) # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def get_fashion_mnist_labels(labels): - """返回Fashion-MNIST数据集的文本标签。""" - text_labels = [ - 't-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', - 'sneaker', 'bag', 'ankle boot'] + """Return text labels for the Fashion-MNIST dataset.""" + text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', + 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot'] return [text_labels[int(i)] for i in labels] @@ -177,10 +175,10 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): axes = axes.flatten() for i, (ax, img) in enumerate(zip(axes, imgs)): if torch.is_tensor(img): - # 图片张量 + # Tensor Image ax.imshow(img.numpy()) else: - # PIL图片 + # PIL Image ax.imshow(img) ax.axes.get_xaxis().set_visible(False) ax.axes.get_yaxis().set_visible(False) @@ -191,25 +189,21 @@ def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def get_dataloader_workers(): - """使用4个进程来读取的数据。""" + """Use 4 processes to read the data.""" return 4 # Defined in file: ./chapter_linear-networks/image-classification-dataset.md def load_data_fashion_mnist(batch_size, resize=None): - """下载Fashion-MNIST数据集,然后将其加载到内存中。""" + """Download the Fashion-MNIST dataset and then load it into memory.""" trans = [transforms.ToTensor()] if resize: trans.insert(0, transforms.Resize(resize)) trans = transforms.Compose(trans) - mnist_train = torchvision.datasets.FashionMNIST(root="../data", - train=True, - transform=trans, - download=True) - mnist_test = torchvision.datasets.FashionMNIST(root="../data", - train=False, - transform=trans, - download=True) + mnist_train = torchvision.datasets.FashionMNIST( + root="../data", train=True, transform=trans, download=True) + mnist_test = torchvision.datasets.FashionMNIST( + root="../data", train=False, transform=trans, download=True) return (data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers()), data.DataLoader(mnist_test, batch_size, shuffle=False, @@ -218,19 +212,19 @@ def load_data_fashion_mnist(batch_size, resize=None): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def accuracy(y_hat, y): - """计算预测正确的数量。""" + """Compute the number of correct predictions.""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: - y_hat = d2l.argmax(y_hat, axis=1) + y_hat = d2l.argmax(y_hat, axis=1) cmp = d2l.astype(y_hat, y.dtype) == y return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype))) # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def evaluate_accuracy(net, data_iter): - """计算在指定数据集上模型的精度。""" + """Compute the accuracy for a model on a dataset.""" if isinstance(net, torch.nn.Module): - net.eval() # 将模型设置为评估模式 - metric = Accumulator(2) # 正确预测数、预测总数 + net.eval() # Set the model to evaluation mode + metric = Accumulator(2) # No. of correct predictions, no. of predictions for X, y in data_iter: metric.add(accuracy(net(X), y), d2l.size(y)) return metric[0] / metric[1] @@ -238,7 +232,7 @@ def evaluate_accuracy(net, data_iter): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Accumulator: - """在`n`个变量上累加。""" + """For accumulating sums over `n` variables.""" def __init__(self, n): self.data = [0.0] * n @@ -254,54 +248,53 @@ def __getitem__(self, idx): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def train_epoch_ch3(net, train_iter, loss, updater): - """训练模型一个迭代周期(定义见第3章)。""" - # 将模型设置为训练模式 + """The training loop defined in Chapter 3.""" + # Set the model to training mode if isinstance(net, torch.nn.Module): net.train() - # 训练损失总和、训练准确度总和、样本数 + # Sum of training loss, sum of training accuracy, no. of examples metric = Accumulator(3) for X, y in train_iter: - # 计算梯度并更新参数 + # Compute gradients and update parameters y_hat = net(X) l = loss(y_hat, y) if isinstance(updater, torch.optim.Optimizer): - # 使用PyTorch内置的优化器和损失函数 + # Using PyTorch in-built optimizer & loss criterion updater.zero_grad() l.backward() updater.step() - metric.add( - float(l) * len(y), accuracy(y_hat, y), - y.size().numel()) + metric.add(float(l) * len(y), accuracy(y_hat, y), + y.size().numel()) else: - # 使用定制的优化器和损失函数 + # Using custom built optimizer & loss criterion l.sum().backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) - # 返回训练损失和训练准确率 + # Return training loss and training accuracy return metric[0] / metric[2], metric[1] / metric[2] # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md class Animator: - """在动画中绘制数据。""" + """For plotting data in animation.""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): - # 增量地绘制多条线 + # Incrementally plot multiple lines if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: - self.axes = [self.axes,] - # 使用lambda函数捕获参数 - self.config_axes = lambda: d2l.set_axes(self.axes[ - 0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) + self.axes = [self.axes, ] + # Use a lambda function to capture arguments + self.config_axes = lambda: d2l.set_axes( + self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): - # 向图表中添加多个数据点 + # Add multiple data points into the figure if not hasattr(y, "__len__"): y = [y] n = len(y) @@ -325,7 +318,7 @@ def add(self, x, y): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): - """训练模型(定义见第3章)。""" + """Train a model (defined in Chapter 3).""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train loss', 'train acc', 'test acc']) for epoch in range(num_epochs): @@ -340,20 +333,20 @@ def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): # Defined in file: ./chapter_linear-networks/softmax-regression-scratch.md def predict_ch3(net, test_iter, n=6): - """预测标签(定义见第3章)。""" + """Predict labels (defined in Chapter 3).""" for X, y in test_iter: break trues = d2l.get_fashion_mnist_labels(y) preds = d2l.get_fashion_mnist_labels(d2l.argmax(net(X), axis=1)) - titles = [true + '\n' + pred for true, pred in zip(trues, preds)] - d2l.show_images(d2l.reshape(X[0:n], (n, 28, 28)), 1, n, - titles=titles[0:n]) + titles = [true +'\n' + pred for true, pred in zip(trues, preds)] + d2l.show_images( + d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n]) # Defined in file: ./chapter_multilayer-perceptrons/underfit-overfit.md def evaluate_loss(net, data_iter, loss): - """评估给定数据集上模型的损失。""" - metric = d2l.Accumulator(2) # 损失的总和, 样本数量 + """Evaluate the loss of a model on the given dataset.""" + metric = d2l.Accumulator(2) # Sum of losses, no. of examples for X, y in data_iter: out = net(X) y = d2l.reshape(y, out.shape) @@ -369,8 +362,8 @@ def evaluate_loss(net, data_iter, loss): # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md def download(name, cache_dir=os.path.join('..', 'data')): - """下载一个DATA_HUB中的文件,返回本地文件名。""" - assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}." + """Download a file inserted into DATA_HUB, return the local filename.""" + assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}." url, sha1_hash = DATA_HUB[name] os.makedirs(cache_dir, exist_ok=True) fname = os.path.join(cache_dir, url.split('/')[-1]) @@ -384,7 +377,7 @@ def download(name, cache_dir=os.path.join('..', 'data')): sha1.update(data) if sha1.hexdigest() == sha1_hash: return fname # Hit cache - print(f'正在从{url}下载{fname}...') + print(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True, verify=True) with open(fname, 'wb') as f: f.write(r.content) @@ -393,7 +386,7 @@ def download(name, cache_dir=os.path.join('..', 'data')): # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md def download_extract(name, folder=None): - """下载并解压zip/tar文件。""" + """Download and extract a zip/tar file.""" fname = download(name) base_dir = os.path.dirname(fname) data_dir, ext = os.path.splitext(fname) @@ -402,61 +395,63 @@ def download_extract(name, folder=None): elif ext in ('.tar', '.gz'): fp = tarfile.open(fname, 'r') else: - assert False, '只有zip/tar文件可以被解压缩。' + assert False, 'Only zip/tar files can be extracted.' fp.extractall(base_dir) return os.path.join(base_dir, folder) if folder else data_dir def download_all(): - """下载DATA_HUB中的所有文件。""" + """Download all files in the DATA_HUB.""" for name in DATA_HUB: download(name) # Defined in file: ./chapter_multilayer-perceptrons/kaggle-house-price.md -DATA_HUB['kaggle_house_train'] = (DATA_URL + 'kaggle_house_pred_train.csv', - '585e9cc93e70b39160e7921475f9bcd7d31219ce') +DATA_HUB['kaggle_house_train'] = ( + DATA_URL + 'kaggle_house_pred_train.csv', + '585e9cc93e70b39160e7921475f9bcd7d31219ce') -DATA_HUB['kaggle_house_test'] = (DATA_URL + 'kaggle_house_pred_test.csv', - 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') +DATA_HUB['kaggle_house_test'] = ( + DATA_URL + 'kaggle_house_pred_test.csv', + 'fa19780a7b011d9b009e8bff8e99922a8ee2eb90') # Defined in file: ./chapter_deep-learning-computation/use-gpu.md def try_gpu(i=0): - """如果存在,则返回gpu(i),否则返回cpu()。""" + """Return gpu(i) if exists, otherwise return cpu().""" if torch.cuda.device_count() >= i + 1: return torch.device(f'cuda:{i}') return torch.device('cpu') def try_all_gpus(): - """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]。""" - devices = [ - torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())] + """Return all available GPUs, or [cpu(),] if no GPU exists.""" + devices = [torch.device(f'cuda:{i}') + for i in range(torch.cuda.device_count())] return devices if devices else [torch.device('cpu')] # Defined in file: ./chapter_convolutional-neural-networks/conv-layer.md def corr2d(X, K): - """计算二维互相关运算。""" + """Compute 2D cross-correlation.""" h, w = K.shape Y = d2l.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): - Y[i, j] = d2l.reduce_sum((X[i:i + h, j:j + w] * K)) + Y[i, j] = d2l.reduce_sum((X[i: i + h, j: j + w] * K)) return Y # Defined in file: ./chapter_convolutional-neural-networks/lenet.md def evaluate_accuracy_gpu(net, data_iter, device=None): - """使用GPU计算模型在数据集上的精度。""" + """Compute the accuracy for a model on a dataset using a GPU.""" if isinstance(net, torch.nn.Module): - net.eval() # 设置为评估模式 + net.eval() # Set the model to evaluation mode if not device: device = next(iter(net.parameters())).device - # 正确预测的数量,总预测的数量 + # No. of correct predictions, no. of predictions metric = d2l.Accumulator(2) for X, y in data_iter: if isinstance(X, list): - # BERT微调所需的(之后将介绍) + # Required for BERT Fine-tuning (to be covered later) X = [x.to(device) for x in X] else: X = X.to(device) @@ -466,12 +461,12 @@ def evaluate_accuracy_gpu(net, data_iter, device=None): # Defined in file: ./chapter_convolutional-neural-networks/lenet.md -def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): +def train_ch6(net, train_iter, test_iter, num_epochs, lr, + device=d2l.try_gpu()): """Train a model with a GPU (defined in Chapter 6).""" def init_weights(m): if type(m) == nn.Linear or type(m) == nn.Conv2d: nn.init.xavier_uniform_(m.weight) - net.apply(init_weights) print('training on', device) net.to(device) @@ -481,7 +476,7 @@ def init_weights(m): legend=['train loss', 'train acc', 'test acc']) timer, num_batches = d2l.Timer(), len(train_iter) for epoch in range(num_epochs): - # 训练损失之和,训练准确率之和,范例数 + # Sum of training loss, sum of training accuracy, no. of examples metric = d2l.Accumulator(3) net.train() for i, (X, y) in enumerate(train_iter): @@ -510,13 +505,14 @@ def init_weights(m): # Defined in file: ./chapter_convolutional-modern/resnet.md class Residual(nn.Module): - def __init__(self, input_channels, num_channels, use_1x1conv=False, - strides=1): + """The Residual block of ResNet.""" + def __init__(self, input_channels, num_channels, + use_1x1conv=False, strides=1): super().__init__() - self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, - padding=1, stride=strides) - self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, - padding=1) + self.conv1 = nn.Conv2d(input_channels, num_channels, + kernel_size=3, padding=1, stride=strides) + self.conv2 = nn.Conv2d(num_channels, num_channels, + kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides) @@ -548,32 +544,31 @@ def read_time_machine(): # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def tokenize(lines, token='word'): - """将文本行拆分为单词或字符标记。""" + """Split text lines into word or character tokens.""" if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: - print('错误:未知令牌类型:' + token) + print('ERROR: unknown token type: ' + token) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md class Vocab: - """文本词表""" + """Vocabulary for text.""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: - reserved_tokens = [] - # 按出现频率排序 + reserved_tokens = [] + # Sort according to frequencies counter = count_corpus(tokens) self.token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) - # 未知标记的索引为0 + # The index for the unknown token is 0 self.unk, uniq_tokens = 0, [''] + reserved_tokens - uniq_tokens += [ - token for token, freq in self.token_freqs - if freq >= min_freq and token not in uniq_tokens] + uniq_tokens += [token for token, freq in self.token_freqs + if freq >= min_freq and token not in uniq_tokens] self.idx_to_token, self.token_to_idx = [], dict() for token in uniq_tokens: self.idx_to_token.append(token) @@ -594,21 +589,21 @@ def to_tokens(self, indices): def count_corpus(tokens): """Count token frequencies.""" - # 这里的 `tokens` 是1D列表或2D列表 + # Here `tokens` is a 1D list or 2D list if len(tokens) == 0 or isinstance(tokens[0], list): - # 将令牌列表展平 + # Flatten a list of token lists into a list of tokens tokens = [token for line in tokens for token in line] return collections.Counter(tokens) # Defined in file: ./chapter_recurrent-neural-networks/text-preprocessing.md def load_corpus_time_machine(max_tokens=-1): - """返回时光机器数据集的令牌索引和词汇表。""" + """Return token indices and the vocabulary of the time machine dataset.""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, - # 所以将所有文本行展平到一个列表中 + # Since each text line in the time machine dataset is not necessarily a + # sentence or a paragraph, flatten all the text lines into a single list corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] @@ -617,24 +612,28 @@ def load_corpus_time_machine(max_tokens=-1): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_random(corpus, batch_size, num_steps): - """使用随机抽样生成一小批子序列。""" - # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 + """Generate a minibatch of subsequences using random sampling.""" + # Start with a random offset (inclusive of `num_steps - 1`) to partition a + # sequence corpus = corpus[random.randint(0, num_steps - 1):] - # 减去1,因为我们需要考虑标签 + # Subtract 1 since we need to account for labels num_subseqs = (len(corpus) - 1) // num_steps - # 长度为`num_steps`的子序列的起始索引 + # The starting indices for subsequences of length `num_steps` initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 + # In random sampling, the subsequences from two adjacent random + # minibatches during iteration are not necessarily adjacent on the + # original sequence random.shuffle(initial_indices) def data(pos): - # 返回从`pos`开始的长度为`num_steps`的序列 - return corpus[pos:pos + num_steps] + # Return a sequence of length `num_steps` starting from `pos` + return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): - # 这里,`initial_indices`包含子序列的随机起始索引 - initial_indices_per_batch = initial_indices[i:i + batch_size] + # Here, `initial_indices` contains randomized starting indices for + # subsequences + initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield d2l.tensor(X), d2l.tensor(Y) @@ -642,23 +641,23 @@ def data(pos): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md def seq_data_iter_sequential(corpus, batch_size, num_steps): - """使用顺序分区生成一小批子序列。""" - # 从随机偏移量开始划分序列 + """Generate a minibatch of subsequences using sequential partitioning.""" + # Start with a random offset to partition a sequence offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size - Xs = d2l.tensor(corpus[offset:offset + num_tokens]) - Ys = d2l.tensor(corpus[offset + 1:offset + 1 + num_tokens]) + Xs = d2l.tensor(corpus[offset: offset + num_tokens]) + Ys = d2l.tensor(corpus[offset + 1: offset + 1 + num_tokens]) Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1) num_batches = Xs.shape[1] // num_steps for i in range(0, num_steps * num_batches, num_steps): - X = Xs[:, i:i + num_steps] - Y = Ys[:, i:i + num_steps] + X = Xs[:, i: i + num_steps] + Y = Ys[:, i: i + num_steps] yield X, Y # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md class SeqDataLoader: - """加载序列数据的迭代器。""" + """An iterator to load sequence data.""" def __init__(self, batch_size, num_steps, use_random_iter, max_tokens): if use_random_iter: self.data_iter_fn = d2l.seq_data_iter_random @@ -672,19 +671,19 @@ def __iter__(self): # Defined in file: ./chapter_recurrent-neural-networks/language-models-and-dataset.md -def load_data_time_machine(batch_size, num_steps, use_random_iter=False, - max_tokens=10000): - """返回时光机器数据集的迭代器和词表。""" - data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, - max_tokens) +def load_data_time_machine(batch_size, num_steps, + use_random_iter=False, max_tokens=10000): + """Return the iterator and the vocabulary of the time machine dataset.""" + data_iter = SeqDataLoader( + batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md class RNNModelScratch: - """从零开始实现的循环神经网络模型""" - def __init__(self, vocab_size, num_hiddens, device, get_params, - init_state, forward_fn): + """A RNN Model implemented from scratch.""" + def __init__(self, vocab_size, num_hiddens, device, + get_params, init_state, forward_fn): self.vocab_size, self.num_hiddens = vocab_size, num_hiddens self.params = get_params(vocab_size, num_hiddens, device) self.init_state, self.forward_fn = init_state, forward_fn @@ -699,15 +698,15 @@ def begin_state(self, batch_size, device): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def predict_ch8(prefix, num_preds, net, vocab, device): - """在`prefix`后面生成新字符。""" + """Generate new characters following the `prefix`.""" state = net.begin_state(batch_size=1, device=device) outputs = [vocab[prefix[0]]] - get_input = lambda: d2l.reshape(d2l.tensor([outputs[-1]], device=device), - (1, 1)) - for y in prefix[1:]: # 预热期 + get_input = lambda: d2l.reshape(d2l.tensor( + [outputs[-1]], device=device), (1, 1)) + for y in prefix[1:]: # Warm-up period _, state = net(get_input(), state) outputs.append(vocab[y]) - for _ in range(num_preds): # 预测`num_preds`步 + for _ in range(num_preds): # Predict `num_preds` steps y, state = net(get_input(), state) outputs.append(int(y.argmax(dim=1).reshape(1))) return ''.join([vocab.idx_to_token[i] for i in outputs]) @@ -715,12 +714,12 @@ def predict_ch8(prefix, num_preds, net, vocab, device): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def grad_clipping(net, theta): - """裁剪梯度。""" + """Clip the gradient.""" if isinstance(net, nn.Module): params = [p for p in net.parameters() if p.requires_grad] else: params = net.params - norm = torch.sqrt(sum(torch.sum((p.grad**2)) for p in params)) + norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params)) if norm > theta: for param in params: param.grad[:] *= theta / norm @@ -728,19 +727,21 @@ def grad_clipping(net, theta): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): - """训练模型一个迭代周期(定义见第8章)。""" + """Train a net within one epoch (defined in Chapter 8).""" state, timer = None, d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失之和, 标记数量 + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens for X, Y in train_iter: if state is None or use_random_iter: - # 在第一次迭代或使用随机抽样时初始化`state` + # Initialize `state` when either it is the first iteration or + # using random sampling state = net.begin_state(batch_size=X.shape[0], device=device) else: if isinstance(net, nn.Module) and not isinstance(state, tuple): - # `state`对于`nn.GRU`是个张量 + # `state` is a tensor for `nn.GRU` state.detach_() else: - # `state`对于`nn.LSTM`或对于我们从零开始实现的模型是个张量 + # `state` is a tuple of tensors for `nn.LSTM` and + # for our custom scratch implementation for s in state: s.detach_() y = Y.T.reshape(-1) @@ -755,7 +756,7 @@ def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): else: l.backward() grad_clipping(net, 1) - # 因为已经调用了`mean`函数 + # Since the `mean` function has been invoked updater(batch_size=1) metric.add(l * d2l.size(y), d2l.size(y)) return math.exp(metric[0] / metric[1]), metric[1] / timer.stop() @@ -764,37 +765,38 @@ def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter): # Defined in file: ./chapter_recurrent-neural-networks/rnn-scratch.md def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False): - """训练模型(定义见第8章)。""" + """Train a model (defined in Chapter 8).""" loss = nn.CrossEntropyLoss() animator = d2l.Animator(xlabel='epoch', ylabel='perplexity', legend=['train'], xlim=[10, num_epochs]) - # 初始化 + # Initialize if isinstance(net, nn.Module): updater = torch.optim.SGD(net.parameters(), lr) else: updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size) predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device) - # 训练和预测 + # Train and predict for epoch in range(num_epochs): - ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, device, - use_random_iter) + ppl, speed = train_epoch_ch8( + net, train_iter, loss, updater, device, use_random_iter) if (epoch + 1) % 10 == 0: print(predict('time traveller')) animator.add(epoch + 1, [ppl]) - print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}') + print(f'perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}') print(predict('time traveller')) print(predict('traveller')) # Defined in file: ./chapter_recurrent-neural-networks/rnn-concise.md class RNNModel(nn.Module): - """循环神经网络模型。""" + """The RNN model.""" def __init__(self, rnn_layer, vocab_size, **kwargs): super(RNNModel, self).__init__(**kwargs) self.rnn = rnn_layer self.vocab_size = vocab_size self.num_hiddens = self.rnn.hidden_size - # 如果RNN是双向的(之后将介绍),`num_directions`应该是2,否则应该是1。 + # If the RNN is bidirectional (to be introduced later), + # `num_directions` should be 2, else it should be 1. if not self.rnn.bidirectional: self.num_directions = 1 self.linear = nn.Linear(self.num_hiddens, self.vocab_size) @@ -806,24 +808,638 @@ def forward(self, inputs, state): X = F.one_hot(inputs.T.long(), self.vocab_size) X = X.to(torch.float32) Y, state = self.rnn(X, state) - # 全连接层首先将`Y`的形状改为(`时间步数` * `批量大小`, `隐藏单元数`)。 - # 它的输出形状是 (`时间步数` * `批量大小`, `词表大小`)。 + # The fully connected layer will first change the shape of `Y` to + # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is + # (`num_steps` * `batch_size`, `vocab_size`). output = self.linear(Y.reshape((-1, Y.shape[-1]))) return output, state def begin_state(self, device, batch_size=1): if not isinstance(self.rnn, nn.LSTM): - # `nn.GRU` 以张量作为隐藏状态 - return torch.zeros((self.num_directions * self.rnn.num_layers, - batch_size, self.num_hiddens), device=device) + # `nn.GRU` takes a tensor as hidden state + return torch.zeros((self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens), + device=device) else: - # `nn.LSTM` 以张量作为隐藏状态 - return (torch.zeros((self.num_directions * self.rnn.num_layers, - batch_size, self.num_hiddens), - device=device), - torch.zeros((self.num_directions * self.rnn.num_layers, - batch_size, self.num_hiddens), - device=device)) + # `nn.LSTM` takes a tuple of hidden states + return (torch.zeros(( + self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens), device=device), + torch.zeros(( + self.num_directions * self.rnn.num_layers, + batch_size, self.num_hiddens), device=device)) + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', + '94646ad1522d915e7b0f9296181140edcf86a4f5') + +def read_data_nmt(): + """Load the English-French dataset.""" + data_dir = d2l.download_extract('fra-eng') + with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: + return f.read() + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def preprocess_nmt(text): + """Preprocess the English-French dataset.""" + def no_space(char, prev_char): + return char in set(',.!?') and prev_char != ' ' + + # Replace non-breaking space with space, and convert uppercase letters to + # lowercase ones + text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() + # Insert space between words and punctuation marks + out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char + for i, char in enumerate(text)] + return ''.join(out) + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def tokenize_nmt(text, num_examples=None): + """Tokenize the English-French dataset.""" + source, target = [], [] + for i, line in enumerate(text.split('\n')): + if num_examples and i > num_examples: + break + parts = line.split('\t') + if len(parts) == 2: + source.append(parts[0].split(' ')) + target.append(parts[1].split(' ')) + return source, target + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def truncate_pad(line, num_steps, padding_token): + """Truncate or pad sequences.""" + if len(line) > num_steps: + return line[:num_steps] # Truncate + return line + [padding_token] * (num_steps - len(line)) # Pad + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def build_array_nmt(lines, vocab, num_steps): + """Transform text sequences of machine translation into minibatches.""" + lines = [vocab[l] for l in lines] + lines = [l + [vocab['']] for l in lines] + array = d2l.tensor([truncate_pad( + l, num_steps, vocab['']) for l in lines]) + valid_len = d2l.reduce_sum( + d2l.astype(array != vocab[''], d2l.int32), 1) + return array, valid_len + + +# Defined in file: ./chapter_recurrent-modern/machine-translation-and-dataset.md +def load_data_nmt(batch_size, num_steps, num_examples=600): + """Return the iterator and the vocabularies of the translation dataset.""" + text = preprocess_nmt(read_data_nmt()) + source, target = tokenize_nmt(text, num_examples) + src_vocab = d2l.Vocab(source, min_freq=2, + reserved_tokens=['', '', '']) + tgt_vocab = d2l.Vocab(target, min_freq=2, + reserved_tokens=['', '', '']) + src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps) + tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps) + data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len) + data_iter = d2l.load_array(data_arrays, batch_size) + return data_iter, src_vocab, tgt_vocab + + +# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md +class Encoder(nn.Module): + """The base encoder interface for the encoder-decoder architecture.""" + def __init__(self, **kwargs): + super(Encoder, self).__init__(**kwargs) + + def forward(self, X, *args): + raise NotImplementedError + + +# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md +class Decoder(nn.Module): + """The base decoder interface for the encoder-decoder architecture.""" + def __init__(self, **kwargs): + super(Decoder, self).__init__(**kwargs) + + def init_state(self, enc_outputs, *args): + raise NotImplementedError + + def forward(self, X, state): + raise NotImplementedError + + +# Defined in file: ./chapter_recurrent-modern/encoder-decoder.md +class EncoderDecoder(nn.Module): + """The base class for the encoder-decoder architecture.""" + def __init__(self, encoder, decoder, **kwargs): + super(EncoderDecoder, self).__init__(**kwargs) + self.encoder = encoder + self.decoder = decoder + + def forward(self, enc_X, dec_X, *args): + enc_outputs = self.encoder(enc_X, *args) + dec_state = self.decoder.init_state(enc_outputs, *args) + return self.decoder(dec_X, dec_state) + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +class Seq2SeqEncoder(d2l.Encoder): + """The RNN encoder for sequence to sequence learning.""" + def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, + dropout=0, **kwargs): + super(Seq2SeqEncoder, self).__init__(**kwargs) + # Embedding layer + self.embedding = nn.Embedding(vocab_size, embed_size) + self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, + dropout=dropout) + + def forward(self, X, *args): + # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) + X = self.embedding(X) + # In RNN models, the first axis corresponds to time steps + X = X.permute(1, 0, 2) + # When state is not mentioned, it defaults to zeros + output, state = self.rnn(X) + # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) + # `state` shape: (`num_layers`, `batch_size`, `num_hiddens`) + return output, state + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def sequence_mask(X, valid_len, value=0): + """Mask irrelevant entries in sequences.""" + maxlen = X.size(1) + mask = torch.arange((maxlen), dtype=torch.float32, + device=X.device)[None, :] < valid_len[:, None] + X[~mask] = value + return X + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): + """The softmax cross-entropy loss with masks.""" + # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) + # `label` shape: (`batch_size`, `num_steps`) + # `valid_len` shape: (`batch_size`,) + def forward(self, pred, label, valid_len): + weights = torch.ones_like(label) + weights = sequence_mask(weights, valid_len) + self.reduction='none' + unweighted_loss = super(MaskedSoftmaxCELoss, self).forward( + pred.permute(0, 2, 1), label) + weighted_loss = (unweighted_loss * weights).mean(dim=1) + return weighted_loss + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): + """Train a model for sequence to sequence.""" + def xavier_init_weights(m): + if type(m) == nn.Linear: + nn.init.xavier_uniform_(m.weight) + if type(m) == nn.GRU: + for param in m._flat_weights_names: + if "weight" in param: + nn.init.xavier_uniform_(m._parameters[param]) + net.apply(xavier_init_weights) + net.to(device) + optimizer = torch.optim.Adam(net.parameters(), lr=lr) + loss = MaskedSoftmaxCELoss() + net.train() + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[10, num_epochs]) + for epoch in range(num_epochs): + timer = d2l.Timer() + metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + for batch in data_iter: + X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch] + bos = torch.tensor([tgt_vocab['']] * Y.shape[0], + device=device).reshape(-1, 1) + dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing + Y_hat, _ = net(X, dec_input, X_valid_len) + l = loss(Y_hat, Y, Y_valid_len) + l.sum().backward() # Make the loss scalar for `backward` + d2l.grad_clipping(net, 1) + num_tokens = Y_valid_len.sum() + optimizer.step() + with torch.no_grad(): + metric.add(l.sum(), num_tokens) + if (epoch + 1) % 10 == 0: + animator.add(epoch + 1, (metric[0] / metric[1],)) + print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' + f'tokens/sec on {str(device)}') + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, + device, save_attention_weights=False): + """Predict for sequence to sequence.""" + # Set `net` to eval mode for inference + net.eval() + src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ + src_vocab['']] + enc_valid_len = torch.tensor([len(src_tokens)], device=device) + src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) + # Add the batch axis + enc_X = torch.unsqueeze( + torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0) + enc_outputs = net.encoder(enc_X, enc_valid_len) + dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) + # Add the batch axis + dec_X = torch.unsqueeze(torch.tensor( + [tgt_vocab['']], dtype=torch.long, device=device), dim=0) + output_seq, attention_weight_seq = [], [] + for _ in range(num_steps): + Y, dec_state = net.decoder(dec_X, dec_state) + # We use the token with the highest prediction likelihood as the input + # of the decoder at the next time step + dec_X = Y.argmax(dim=2) + pred = dec_X.squeeze(dim=0).type(torch.int32).item() + # Save attention weights (to be covered later) + if save_attention_weights: + attention_weight_seq.append(net.decoder.attention_weights) + # Once the end-of-sequence token is predicted, the generation of the + # output sequence is complete + if pred == tgt_vocab['']: + break + output_seq.append(pred) + return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq + + +# Defined in file: ./chapter_recurrent-modern/seq2seq.md +def bleu(pred_seq, label_seq, k): + """Compute the BLEU.""" + pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') + len_pred, len_label = len(pred_tokens), len(label_tokens) + score = math.exp(min(0, 1 - len_label / len_pred)) + for n in range(1, k + 1): + num_matches, label_subs = 0, collections.defaultdict(int) + for i in range(len_label - n + 1): + label_subs[''.join(label_tokens[i: i + n])] += 1 + for i in range(len_pred - n + 1): + if label_subs[''.join(pred_tokens[i: i + n])] > 0: + num_matches += 1 + label_subs[''.join(pred_tokens[i: i + n])] -= 1 + score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n)) + return score + + +# Defined in file: ./chapter_attention-mechanisms/attention-cues.md +def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), + cmap='Reds'): + d2l.use_svg_display() + num_rows, num_cols = matrices.shape[0], matrices.shape[1] + fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, + sharex=True, sharey=True, squeeze=False) + for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)): + for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)): + pcm = ax.imshow(d2l.numpy(matrix), cmap=cmap) + if i == num_rows - 1: + ax.set_xlabel(xlabel) + if j == 0: + ax.set_ylabel(ylabel) + if titles: + ax.set_title(titles[j]) + fig.colorbar(pcm, ax=axes, shrink=0.6); + + +# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md +def masked_softmax(X, valid_lens): + """Perform softmax operation by masking elements on the last axis.""" + # `X`: 3D tensor, `valid_lens`: 1D or 2D tensor + if valid_lens is None: + return nn.functional.softmax(X, dim=-1) + else: + shape = X.shape + if valid_lens.dim() == 1: + valid_lens = torch.repeat_interleave(valid_lens, shape[1]) + else: + valid_lens = valid_lens.reshape(-1) + # On the last axis, replace masked elements with a very large negative + # value, whose exponentiation outputs 0 + X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, + value=-1e6) + return nn.functional.softmax(X.reshape(shape), dim=-1) + + +# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md +class AdditiveAttention(nn.Module): + def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs): + super(AdditiveAttention, self).__init__(**kwargs) + self.W_k = nn.Linear(key_size, num_hiddens, bias=False) + self.W_q = nn.Linear(query_size, num_hiddens, bias=False) + self.w_v = nn.Linear(num_hiddens, 1, bias=False) + self.dropout = nn.Dropout(dropout) + + def forward(self, queries, keys, values, valid_lens): + queries, keys = self.W_q(queries), self.W_k(keys) + # After dimension expansion, shape of `queries`: (`batch_size`, no. of + # queries, 1, `num_hiddens`) and shape of `keys`: (`batch_size`, 1, + # no. of key-value pairs, `num_hiddens`). Sum them up with + # broadcasting + features = queries.unsqueeze(2) + keys.unsqueeze(1) + features = torch.tanh(features) + # There is only one output of `self.w_v`, so we remove the last + # one-dimensional entry from the shape. Shape of `scores`: + # (`batch_size`, no. of queries, no. of key-value pairs) + scores = self.w_v(features).squeeze(-1) + self.attention_weights = masked_softmax(scores, valid_lens) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + return torch.bmm(self.dropout(self.attention_weights), values) + + +# Defined in file: ./chapter_attention-mechanisms/attention-scoring-functions.md +class DotProductAttention(nn.Module): + """Scaled dot product attention.""" + def __init__(self, dropout, **kwargs): + super(DotProductAttention, self).__init__(**kwargs) + self.dropout = nn.Dropout(dropout) + + # Shape of `queries`: (`batch_size`, no. of queries, `d`) + # Shape of `keys`: (`batch_size`, no. of key-value pairs, `d`) + # Shape of `values`: (`batch_size`, no. of key-value pairs, value + # dimension) + # Shape of `valid_lens`: (`batch_size`,) or (`batch_size`, no. of queries) + def forward(self, queries, keys, values, valid_lens=None): + d = queries.shape[-1] + # Set `transpose_b=True` to swap the last two dimensions of `keys` + scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d) + self.attention_weights = masked_softmax(scores, valid_lens) + return torch.bmm(self.dropout(self.attention_weights), values) + + +# Defined in file: ./chapter_attention-mechanisms/bahdanau-attention.md +class AttentionDecoder(d2l.Decoder): + """The base attention-based decoder interface.""" + def __init__(self, **kwargs): + super(AttentionDecoder, self).__init__(**kwargs) + + @property + def attention_weights(self): + raise NotImplementedError + + +# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md +class MultiHeadAttention(nn.Module): + def __init__(self, key_size, query_size, value_size, num_hiddens, + num_heads, dropout, bias=False, **kwargs): + super(MultiHeadAttention, self).__init__(**kwargs) + self.num_heads = num_heads + self.attention = d2l.DotProductAttention(dropout) + self.W_q = nn.Linear(query_size, num_hiddens, bias=bias) + self.W_k = nn.Linear(key_size, num_hiddens, bias=bias) + self.W_v = nn.Linear(value_size, num_hiddens, bias=bias) + self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias) + + def forward(self, queries, keys, values, valid_lens): + # Shape of `queries`, `keys`, or `values`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`) + # Shape of `valid_lens`: + # (`batch_size`,) or (`batch_size`, no. of queries) + # After transposing, shape of output `queries`, `keys`, or `values`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) + queries = transpose_qkv(self.W_q(queries), self.num_heads) + keys = transpose_qkv(self.W_k(keys), self.num_heads) + values = transpose_qkv(self.W_v(values), self.num_heads) + + if valid_lens is not None: + # On axis 0, copy the first item (scalar or vector) for + # `num_heads` times, then copy the next item, and so on + valid_lens = torch.repeat_interleave( + valid_lens, repeats=self.num_heads, dim=0) + + # Shape of `output`: (`batch_size` * `num_heads`, no. of queries, + # `num_hiddens` / `num_heads`) + output = self.attention(queries, keys, values, valid_lens) + + # Shape of `output_concat`: + # (`batch_size`, no. of queries, `num_hiddens`) + output_concat = transpose_output(output, self.num_heads) + return self.W_o(output_concat) + + +# Defined in file: ./chapter_attention-mechanisms/multihead-attention.md +def transpose_qkv(X, num_heads): + # Shape of input `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_hiddens`). + # Shape of output `X`: + # (`batch_size`, no. of queries or key-value pairs, `num_heads`, + # `num_hiddens` / `num_heads`) + X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) + + # Shape of output `X`: + # (`batch_size`, `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) + X = X.permute(0, 2, 1, 3) + + # Shape of `output`: + # (`batch_size` * `num_heads`, no. of queries or key-value pairs, + # `num_hiddens` / `num_heads`) + return X.reshape(-1, X.shape[2], X.shape[3]) + + +def transpose_output(X, num_heads): + """Reverse the operation of `transpose_qkv`""" + X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) + X = X.permute(0, 2, 1, 3) + return X.reshape(X.shape[0], X.shape[1], -1) + + +# Defined in file: ./chapter_attention-mechanisms/self-attention-and-positional-encoding.md +class PositionalEncoding(nn.Module): + def __init__(self, num_hiddens, dropout, max_len=1000): + super(PositionalEncoding, self).__init__() + self.dropout = nn.Dropout(dropout) + # Create a long enough `P` + self.P = d2l.zeros((1, max_len, num_hiddens)) + X = d2l.arange(max_len, dtype=torch.float32).reshape( + -1, 1) / torch.pow(10000, torch.arange( + 0, num_hiddens, 2, dtype=torch.float32) / num_hiddens) + self.P[:, :, 0::2] = torch.sin(X) + self.P[:, :, 1::2] = torch.cos(X) + + def forward(self, X): + X = X + self.P[:, :X.shape[1], :].to(X.device) + return self.dropout(X) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class PositionWiseFFN(nn.Module): + def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs, + **kwargs): + super(PositionWiseFFN, self).__init__(**kwargs) + self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens) + self.relu = nn.ReLU() + self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs) + + def forward(self, X): + return self.dense2(self.relu(self.dense1(X))) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class AddNorm(nn.Module): + def __init__(self, normalized_shape, dropout, **kwargs): + super(AddNorm, self).__init__(**kwargs) + self.dropout = nn.Dropout(dropout) + self.ln = nn.LayerNorm(normalized_shape) + + def forward(self, X, Y): + return self.ln(self.dropout(Y) + X) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class EncoderBlock(nn.Module): + def __init__(self, key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, + dropout, use_bias=False, **kwargs): + super(EncoderBlock, self).__init__(**kwargs) + self.attention = d2l.MultiHeadAttention( + key_size, query_size, value_size, num_hiddens, num_heads, dropout, + use_bias) + self.addnorm1 = AddNorm(norm_shape, dropout) + self.ffn = PositionWiseFFN( + ffn_num_input, ffn_num_hiddens, num_hiddens) + self.addnorm2 = AddNorm(norm_shape, dropout) + + def forward(self, X, valid_lens): + Y = self.addnorm1(X, self.attention(X, X, X, valid_lens)) + return self.addnorm2(Y, self.ffn(Y)) + + +# Defined in file: ./chapter_attention-mechanisms/transformer.md +class TransformerEncoder(d2l.Encoder): + def __init__(self, vocab_size, key_size, query_size, value_size, + num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, + num_heads, num_layers, dropout, use_bias=False, **kwargs): + super(TransformerEncoder, self).__init__(**kwargs) + self.num_hiddens = num_hiddens + self.embedding = nn.Embedding(vocab_size, num_hiddens) + self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) + self.blks = nn.Sequential() + for i in range(num_layers): + self.blks.add_module("block"+str(i), + EncoderBlock(key_size, query_size, value_size, num_hiddens, + norm_shape, ffn_num_input, ffn_num_hiddens, + num_heads, dropout, use_bias)) + + def forward(self, X, valid_lens, *args): + # Since positional encoding values are between -1 and 1, the embedding + # values are multiplied by the square root of the embedding dimension + # to rescale before they are summed up + X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens)) + self.attention_weights = [None] * len(self.blks) + for i, blk in enumerate(self.blks): + X = blk(X, valid_lens) + self.attention_weights[ + i] = blk.attention.attention.attention_weights + return X + + +# Defined in file: ./chapter_optimization/optimization-intro.md +def annotate(text, xy, xytext): + d2l.plt.gca().annotate(text, xy=xy, xytext=xytext, + arrowprops=dict(arrowstyle='->')) + + +# Defined in file: ./chapter_optimization/gd.md +def train_2d(trainer, steps=20): + """Optimize a 2-dim objective function with a customized trainer.""" + # s1 and s2 are internal state variables and will + # be used later in the chapter + x1, x2, s1, s2 = -5, -2, 0, 0 + results = [(x1, x2)] + for i in range(steps): + x1, x2, s1, s2 = trainer(x1, x2, s1, s2) + results.append((x1, x2)) + return results + +def show_trace_2d(f, results): + """Show the trace of 2D variables during optimization.""" + d2l.set_figsize() + d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') + x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1), + d2l.arange(-3.0, 1.0, 0.1)) + d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') + d2l.plt.xlabel('x1') + d2l.plt.ylabel('x2') + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +d2l.DATA_HUB['airfoil'] = (d2l.DATA_URL + 'airfoil_self_noise.dat', + '76e5be1548fd8222e5074cf0faae75edff8cf93f') + +def get_data_ch11(batch_size=10, n=1500): + data = np.genfromtxt(d2l.download('airfoil'), + dtype=np.float32, delimiter='\t') + data = torch.from_numpy((data - data.mean(axis=0)) / data.std(axis=0)) + data_iter = d2l.load_array((data[:n, :-1], data[:n, -1]), + batch_size, is_train=True) + return data_iter, data.shape[1]-1 + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +def train_ch11(trainer_fn, states, hyperparams, data_iter, + feature_dim, num_epochs=2): + # Initialization + w = torch.normal(mean=0.0, std=0.01, size=(feature_dim, 1), + requires_grad=True) + b = torch.zeros((1), requires_grad=True) + net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss + # Train + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + l = loss(net(X), y).mean() + l.backward() + trainer_fn([w, b], states, hyperparams) + n += X.shape[0] + if n % 200 == 0: + timer.stop() + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss),)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') + return timer.cumsum(), animator.Y[0] + + +# Defined in file: ./chapter_optimization/minibatch-sgd.md +def train_concise_ch11(trainer_fn, hyperparams, data_iter, num_epochs=4): + # Initialization + net = nn.Sequential(nn.Linear(5, 1)) + def init_weights(m): + if type(m) == nn.Linear: + torch.nn.init.normal_(m.weight, std=0.01) + net.apply(init_weights) + + optimizer = trainer_fn(net.parameters(), **hyperparams) + + loss = nn.MSELoss() + # Note: L2 Loss = 1/2 * MSE Loss. PyTorch has MSE Loss which is slightly + # different from MXNet's L2Loss by a factor of 2. Hence we halve the loss + # value to get L2Loss in PyTorch + animator = d2l.Animator(xlabel='epoch', ylabel='loss', + xlim=[0, num_epochs], ylim=[0.22, 0.35]) + n, timer = 0, d2l.Timer() + for _ in range(num_epochs): + for X, y in data_iter: + optimizer.zero_grad() + out = net(X) + y = y.reshape(out.shape) + l = loss(out, y)/2 + l.backward() + optimizer.step() + n += X.shape[0] + if n % 200 == 0: + timer.stop() + animator.add(n/X.shape[0]/len(data_iter), + (d2l.evaluate_loss(net, data_iter, loss)/2,)) + timer.start() + print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch') # Defined in file: ./chapter_computational-performance/hybridize.md @@ -839,6 +1455,1190 @@ def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') +# Defined in file: ./chapter_computational-performance/multiple-gpus.md +def split_batch(X, y, devices): + """Split `X` and `y` into multiple devices.""" + assert X.shape[0] == y.shape[0] + return (nn.parallel.scatter(X, devices), + nn.parallel.scatter(y, devices)) + + +# Defined in file: ./chapter_computational-performance/multiple-gpus-concise.md +def resnet18(num_classes, in_channels=1): + """A slightly modified ResNet-18 model.""" + def resnet_block(in_channels, out_channels, num_residuals, + first_block=False): + blk = [] + for i in range(num_residuals): + if i == 0 and not first_block: + blk.append(d2l.Residual(in_channels, out_channels, + use_1x1conv=True, strides=2)) + else: + blk.append(d2l.Residual(out_channels, out_channels)) + return nn.Sequential(*blk) + + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer + net = nn.Sequential( + nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2d(64), + nn.ReLU()) + net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True)) + net.add_module("resnet_block2", resnet_block(64, 128, 2)) + net.add_module("resnet_block3", resnet_block(128, 256, 2)) + net.add_module("resnet_block4", resnet_block(256, 512, 2)) + net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1))) + net.add_module("fc", nn.Sequential(nn.Flatten(), + nn.Linear(512, num_classes))) + return net + + +# Defined in file: ./chapter_computer-vision/image-augmentation.md +def train_batch_ch13(net, X, y, loss, trainer, devices): + if isinstance(X, list): + # Required for BERT Fine-tuning (to be covered later) + X = [x.to(devices[0]) for x in X] + else: + X = X.to(devices[0]) + y = y.to(devices[0]) + net.train() + trainer.zero_grad() + pred = net(X) + l = loss(pred, y) + l.sum().backward() + trainer.step() + train_loss_sum = l.sum() + train_acc_sum = d2l.accuracy(pred, y) + return train_loss_sum, train_acc_sum + + +# Defined in file: ./chapter_computer-vision/image-augmentation.md +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus()): + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + for epoch in range(num_epochs): + # Store training_loss, training_accuracy, num_examples, num_features + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices) + metric.add(l, acc, labels.shape[0], labels.numel()) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpu(net, test_iter) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') + + +# Defined in file: ./chapter_computer-vision/fine-tuning.md +d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL+'hotdog.zip', + 'fba480ffa8aa7e0febbb511d181409f899b9baa5') + + +# Defined in file: ./chapter_computer-vision/bounding-box.md +def box_corner_to_center(boxes): + """Convert from (upper_left, bottom_right) to (center, width, height)""" + x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + cx = (x1 + x2) / 2 + cy = (y1 + y2) / 2 + w = x2 - x1 + h = y2 - y1 + boxes = d2l.stack((cx, cy, w, h), axis=-1) + return boxes + +def box_center_to_corner(boxes): + """Convert from (center, width, height) to (upper_left, bottom_right)""" + cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + x1 = cx - 0.5 * w + y1 = cy - 0.5 * h + x2 = cx + 0.5 * w + y2 = cy + 0.5 * h + boxes = d2l.stack((x1, y1, x2, y2), axis=-1) + return boxes + + +# Defined in file: ./chapter_computer-vision/bounding-box.md +def bbox_to_rect(bbox, color): + """Convert bounding box to matplotlib format.""" + # Convert the bounding box (top-left x, top-left y, bottom-right x, + # bottom-right y) format to matplotlib format: ((upper-left x, + # upper-left y), width, height) + return d2l.plt.Rectangle( + xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], + fill=False, edgecolor=color, linewidth=2) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def multibox_prior(data, sizes, ratios): + in_height, in_width = data.shape[-2:] + device, num_sizes, num_ratios = data.device, len(sizes), len(ratios) + boxes_per_pixel = (num_sizes + num_ratios - 1) + size_tensor = d2l.tensor(sizes, device=device) + ratio_tensor = d2l.tensor(ratios, device=device) + # Offsets are required to move the anchor to center of a pixel + # Since pixel (height=1, width=1), we choose to offset our centers by 0.5 + offset_h, offset_w = 0.5, 0.5 + steps_h = 1.0 / in_height # Scaled steps in y axis + steps_w = 1.0 / in_width # Scaled steps in x axis + + # Generate all center points for the anchor boxes + center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h + center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w + shift_y, shift_x = torch.meshgrid(center_h, center_w) + shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1) + + # Generate boxes_per_pixel number of heights and widths which are later + # used to create anchor box corner coordinates (xmin, xmax, ymin, ymax) + # cat (various sizes, first ratio) and (first size, various ratios) + w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]), + sizes[0] * torch.sqrt(ratio_tensor[1:])))\ + * in_height / in_width # handle rectangular inputs + h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]), + sizes[0] / torch.sqrt(ratio_tensor[1:]))) + # Divide by 2 to get half height and half width + anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat( + in_height * in_width, 1) / 2 + + # Each center point will have boxes_per_pixel number of anchor boxes, so + # generate grid of all anchor box centers with boxes_per_pixel repeats + out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y], + dim=1).repeat_interleave(boxes_per_pixel, dim=0) + + output = out_grid + anchor_manipulations + return output.unsqueeze(0) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def show_bboxes(axes, bboxes, labels=None, colors=None): + """Show bounding boxes.""" + def _make_list(obj, default_values=None): + if obj is None: + obj = default_values + elif not isinstance(obj, (list, tuple)): + obj = [obj] + return obj + labels = _make_list(labels) + colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c']) + for i, bbox in enumerate(bboxes): + color = colors[i % len(colors)] + rect = d2l.bbox_to_rect(d2l.numpy(bbox), color) + axes.add_patch(rect) + if labels and len(labels) > i: + text_color = 'k' if color == 'w' else 'w' + axes.text(rect.xy[0], rect.xy[1], labels[i], + va='center', ha='center', fontsize=9, color=text_color, + bbox=dict(facecolor=color, lw=0)) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def box_iou(boxes1, boxes2): + """Compute IOU between two sets of boxes of shape (N,4) and (M,4).""" + # Compute box areas + box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) * + (boxes[:, 3] - boxes[:, 1])) + area1 = box_area(boxes1) + area2 = box_area(boxes2) + lt = torch.max(boxes1[:, None, :2], boxes2[:, :2]) # [N,M,2] + rb = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) # [N,M,2] + wh = (rb - lt).clamp(min=0) # [N,M,2] + inter = wh[:, :, 0] * wh[:, :, 1] # [N,M] + unioun = area1[:, None] + area2 - inter + return inter / unioun + + +# Defined in file: ./chapter_computer-vision/anchor.md +def match_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): + """Assign ground-truth bounding boxes to anchor boxes similar to them.""" + num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0] + # Element `x_ij` in the `i^th` row and `j^th` column is the IoU + # of the anchor box `anc_i` to the ground-truth bounding box `box_j` + jaccard = box_iou(anchors, ground_truth) + # Initialize the tensor to hold assigned ground truth bbox for each anchor + anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long, + device=device) + # Assign ground truth bounding box according to the threshold + max_ious, indices = torch.max(jaccard, dim=1) + anc_i = torch.nonzero(max_ious >= 0.5).reshape(-1) + box_j = indices[max_ious >= 0.5] + anchors_bbox_map[anc_i] = box_j + # Find the largest iou for each bbox + col_discard = torch.full((num_anchors,), -1) + row_discard = torch.full((num_gt_boxes,), -1) + for _ in range(num_gt_boxes): + max_idx = torch.argmax(jaccard) + box_idx = (max_idx % num_gt_boxes).long() + anc_idx = (max_idx / num_gt_boxes).long() + anchors_bbox_map[anc_idx] = box_idx + jaccard[:, box_idx] = col_discard + jaccard[anc_idx, :] = row_discard + return anchors_bbox_map + + +# Defined in file: ./chapter_computer-vision/anchor.md +def offset_boxes(anchors, assigned_bb, eps=1e-6): + c_anc = d2l.box_corner_to_center(anchors) + c_assigned_bb = d2l.box_corner_to_center(assigned_bb) + offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:] + offset_wh = 5 * d2l.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:]) + offset = d2l.concat([offset_xy, offset_wh], axis=1) + return offset + + +# Defined in file: ./chapter_computer-vision/anchor.md +def multibox_target(anchors, labels): + batch_size, anchors = labels.shape[0], anchors.squeeze(0) + batch_offset, batch_mask, batch_class_labels = [], [], [] + device, num_anchors = anchors.device, anchors.shape[0] + for i in range(batch_size): + label = labels[i, :, :] + anchors_bbox_map = match_anchor_to_bbox(label[:, 1:], anchors, device) + bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(1, 4) + # Initialize class_labels and assigned bbox coordinates with zeros + class_labels = torch.zeros(num_anchors, dtype=torch.long, + device=device) + assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32, + device=device) + # Assign class labels to the anchor boxes using matched gt bbox labels + # If no gt bbox is assigned to an anchor box, then let the + # class_labels and assigned_bb remain zero, i.e the background class + indices_true = torch.nonzero(anchors_bbox_map >= 0) + bb_idx = anchors_bbox_map[indices_true] + class_labels[indices_true] = label[bb_idx, 0].long() + 1 + assigned_bb[indices_true] = label[bb_idx, 1:] + # offset transformations + offset = offset_boxes(anchors, assigned_bb) * bbox_mask + batch_offset.append(offset.reshape(-1)) + batch_mask.append(bbox_mask.reshape(-1)) + batch_class_labels.append(class_labels) + bbox_offset = torch.stack(batch_offset) + bbox_mask = torch.stack(batch_mask) + class_labels = torch.stack(batch_class_labels) + return (bbox_offset, bbox_mask, class_labels) + + +# Defined in file: ./chapter_computer-vision/anchor.md +def offset_inverse(anchors, offset_preds): + c_anc = d2l.box_corner_to_center(anchors) + c_pred_bb_xy = (offset_preds[:, :2] * c_anc[:, 2:] / 10) + c_anc[:, :2] + c_pred_bb_wh = d2l.exp(offset_preds[:, 2:] / 5) * c_anc[:, 2:] + c_pred_bb = d2l.concat((c_pred_bb_xy, c_pred_bb_wh), axis=1) + predicted_bb = d2l.box_center_to_corner(c_pred_bb) + return predicted_bb + + +# Defined in file: ./chapter_computer-vision/anchor.md +def nms(boxes, scores, iou_threshold): + # sorting scores by the descending order and return their indices + B = torch.argsort(scores, dim=-1, descending=True) + keep = [] # boxes indices that will be kept + while B.numel() > 0: + i = B[0] + keep.append(i) + if B.numel() == 1: break + iou = box_iou(boxes[i, :].reshape(-1, 4), + boxes[B[1:], :].reshape(-1, 4)).reshape(-1) + inds = torch.nonzero(iou <= iou_threshold).reshape(-1) + B = B[inds + 1] + return d2l.tensor(keep, device=boxes.device) + +def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, + pos_threshold=0.00999999978): + device, batch_size = cls_probs.device, cls_probs.shape[0] + anchors = anchors.squeeze(0) + num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2] + out = [] + for i in range(batch_size): + cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4) + conf, class_id = torch.max(cls_prob[1:], 0) + predicted_bb = offset_inverse(anchors, offset_pred) + keep = nms(predicted_bb, conf, 0.5) + # Find all non_keep indices and set the class_id to background + all_idx = torch.arange(num_anchors, dtype=torch.long, device=device) + combined = torch.cat((keep, all_idx)) + uniques, counts = combined.unique(return_counts=True) + non_keep = uniques[counts == 1] + all_id_sorted = torch.cat((keep, non_keep)) + class_id[non_keep] = -1 + class_id = class_id[all_id_sorted] + conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] + # threshold to be a positive prediction + below_min_idx = (conf < pos_threshold) + class_id[below_min_idx] = -1 + conf[below_min_idx] = 1 - conf[below_min_idx] + pred_info = torch.cat((class_id.unsqueeze(1), + conf.unsqueeze(1), + predicted_bb), dim=1) + out.append(pred_info) + return d2l.stack(out) + + +# Defined in file: ./chapter_computer-vision/object-detection-dataset.md +d2l.DATA_HUB['banana-detection'] = (d2l.DATA_URL + 'banana-detection.zip', + '5de26c8fce5ccdea9f91267273464dc968d20d72') + + +# Defined in file: ./chapter_computer-vision/object-detection-dataset.md +def read_data_bananas(is_train=True): + """Read the bananas dataset images and labels.""" + data_dir = d2l.download_extract('banana-detection') + csv_fname = os.path.join(data_dir, 'bananas_train' if is_train + else 'bananas_val', 'label.csv') + csv_data = pd.read_csv(csv_fname) + csv_data = csv_data.set_index('img_name') + images, targets = [], [] + for img_name, target in csv_data.iterrows(): + images.append(torchvision.io.read_image( + os.path.join(data_dir, 'bananas_train' if is_train else + 'bananas_val', 'images', f'{img_name}'))) + # Since all images have same object class i.e. category '0', + # the `label` column corresponds to the only object i.e. banana + # The target is as follows : (`label`, `xmin`, `ymin`, `xmax`, `ymax`) + targets.append(list(target)) + return images, torch.tensor(targets).unsqueeze(1) / 256 + + +class BananasDataset(torch.utils.data.Dataset): + def __init__(self, is_train): + self.features, self.labels = read_data_bananas(is_train) + print('read ' + str(len(self.features)) + (f' training examples' if + is_train else f' validation examples')) + + def __getitem__(self, idx): + return (self.features[idx].float(), self.labels[idx]) + + def __len__(self): + return len(self.features) + + +def load_data_bananas(batch_size): + """Load the bananas dataset.""" + train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True), + batch_size, shuffle=True) + val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False), + batch_size) + return (train_iter, val_iter) + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar', + '4e443f8a2eca6b1dac8a6c57641b67dd40621a49') + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def read_voc_images(voc_dir, is_train=True): + """Read all VOC feature and label images.""" + txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation', + 'train.txt' if is_train else 'val.txt') + mode = torchvision.io.image.ImageReadMode.RGB + with open(txt_fname, 'r') as f: + images = f.read().split() + features, labels = [], [] + for i, fname in enumerate(images): + features.append(torchvision.io.read_image(os.path.join( + voc_dir, 'JPEGImages', f'{fname}.jpg'))) + labels.append(torchvision.io.read_image(os.path.join( + voc_dir, 'SegmentationClass' ,f'{fname}.png'), mode)) + return features, labels + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], + [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128], + [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0], + [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128], + [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0], + [0, 64, 128]] + +VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat', + 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', + 'diningtable', 'dog', 'horse', 'motorbike', 'person', + 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor'] + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def build_colormap2label(): + """Build an RGB color to label mapping for segmentation.""" + colormap2label = torch.zeros(256 ** 3, dtype=torch.long) + for i, colormap in enumerate(VOC_COLORMAP): + colormap2label[(colormap[0]*256 + colormap[1])*256 + colormap[2]] = i + return colormap2label + +def voc_label_indices(colormap, colormap2label): + """Map an RGB color to a label.""" + colormap = colormap.permute(1,2,0).numpy().astype('int32') + idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + + colormap[:, :, 2]) + return colormap2label[idx] + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def voc_rand_crop(feature, label, height, width): + """Randomly crop for both feature and label images.""" + rect = torchvision.transforms.RandomCrop.get_params(feature, + (height, width)) + feature = torchvision.transforms.functional.crop(feature, *rect) + label = torchvision.transforms.functional.crop(label, *rect) + return feature, label + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +class VOCSegDataset(torch.utils.data.Dataset): + """A customized dataset to load VOC dataset.""" + + def __init__(self, is_train, crop_size, voc_dir): + self.transform = torchvision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + self.crop_size = crop_size + features, labels = read_voc_images(voc_dir, is_train=is_train) + self.features = [self.normalize_image(feature) + for feature in self.filter(features)] + self.labels = self.filter(labels) + self.colormap2label = build_colormap2label() + print('read ' + str(len(self.features)) + ' examples') + + def normalize_image(self, img): + return self.transform(img.float()) + + def filter(self, imgs): + return [img for img in imgs if ( + img.shape[1] >= self.crop_size[0] and + img.shape[2] >= self.crop_size[1])] + + def __getitem__(self, idx): + feature, label = voc_rand_crop(self.features[idx], self.labels[idx], + *self.crop_size) + return (feature, voc_label_indices(label, self.colormap2label)) + + def __len__(self): + return len(self.features) + + +# Defined in file: ./chapter_computer-vision/semantic-segmentation-and-dataset.md +def load_data_voc(batch_size, crop_size): + """Download and load the VOC2012 semantic dataset.""" + voc_dir = d2l.download_extract('voc2012', os.path.join( + 'VOCdevkit', 'VOC2012')) + num_workers = d2l.get_dataloader_workers() + train_iter = torch.utils.data.DataLoader( + VOCSegDataset(True, crop_size, voc_dir), batch_size, + shuffle=True, drop_last=True, num_workers=num_workers) + test_iter = torch.utils.data.DataLoader( + VOCSegDataset(False, crop_size, voc_dir), batch_size, + drop_last=True, num_workers=num_workers) + return train_iter, test_iter + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', + '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +def read_csv_labels(fname): + """Read fname to return a name to label dictionary.""" + with open(fname, 'r') as f: + # Skip the file header line (column name) + lines = f.readlines()[1:] + tokens = [l.rstrip().split(',') for l in lines] + return dict(((name, label) for name, label in tokens)) + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +def copyfile(filename, target_dir): + """Copy a file into a target directory.""" + os.makedirs(target_dir, exist_ok=True) + shutil.copy(filename, target_dir) + +def reorg_train_valid(data_dir, labels, valid_ratio): + # The number of examples of the class with the least examples in the + # training dataset + n = collections.Counter(labels.values()).most_common()[-1][1] + # The number of examples per class for the validation set + n_valid_per_label = max(1, math.floor(n * valid_ratio)) + label_count = {} + for train_file in os.listdir(os.path.join(data_dir, 'train')): + label = labels[train_file.split('.')[0]] + fname = os.path.join(data_dir, 'train', train_file) + # Copy to train_valid_test/train_valid with a subfolder per class + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train_valid', label)) + if label not in label_count or label_count[label] < n_valid_per_label: + # Copy to train_valid_test/valid + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'valid', label)) + label_count[label] = label_count.get(label, 0) + 1 + else: + # Copy to train_valid_test/train + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train', label)) + return n_valid_per_label + + +# Defined in file: ./chapter_computer-vision/kaggle-cifar10.md +def reorg_test(data_dir): + for test_file in os.listdir(os.path.join(data_dir, 'test')): + copyfile(os.path.join(data_dir, 'test', test_file), + os.path.join(data_dir, 'train_valid_test', 'test', + 'unknown')) + + +# Defined in file: ./chapter_computer-vision/kaggle-dog.md +d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip', + '0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d') + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +d2l.DATA_HUB['ptb'] = (d2l.DATA_URL + 'ptb.zip', + '319d85e578af0cdc590547f26231e4e31cdf1e42') + +def read_ptb(): + data_dir = d2l.download_extract('ptb') + with open(os.path.join(data_dir, 'ptb.train.txt')) as f: + raw_text = f.read() + return [line.split() for line in raw_text.split('\n')] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def subsampling(sentences, vocab): + # Map low frequency words into + sentences = [[vocab.idx_to_token[vocab[tk]] for tk in line] + for line in sentences] + # Count the frequency for each word + counter = d2l.count_corpus(sentences) + num_tokens = sum(counter.values()) + + # Return True if to keep this token during subsampling + def keep(token): + return(random.uniform(0, 1) < + math.sqrt(1e-4 / counter[token] * num_tokens)) + + # Now do the subsampling + return [[tk for tk in line if keep(tk)] for line in sentences] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def get_centers_and_contexts(corpus, max_window_size): + centers, contexts = [], [] + for line in corpus: + # Each sentence needs at least 2 words to form a "central target word + # - context word" pair + if len(line) < 2: + continue + centers += line + for i in range(len(line)): # Context window centered at i + window_size = random.randint(1, max_window_size) + indices = list(range(max(0, i - window_size), + min(len(line), i + 1 + window_size))) + # Exclude the central target word from the context words + indices.remove(i) + contexts.append([line[idx] for idx in indices]) + return centers, contexts + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +class RandomGenerator: + """Draw a random int in [0, n] according to n sampling weights.""" + def __init__(self, sampling_weights): + self.population = list(range(len(sampling_weights))) + self.sampling_weights = sampling_weights + self.candidates = [] + self.i = 0 + + def draw(self): + if self.i == len(self.candidates): + self.candidates = random.choices( + self.population, self.sampling_weights, k=10000) + self.i = 0 + self.i += 1 + return self.candidates[self.i-1] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def get_negatives(all_contexts, corpus, K): + counter = d2l.count_corpus(corpus) + sampling_weights = [counter[i]**0.75 for i in range(len(counter))] + all_negatives, generator = [], RandomGenerator(sampling_weights) + for contexts in all_contexts: + negatives = [] + while len(negatives) < len(contexts) * K: + neg = generator.draw() + # Noise words cannot be context words + if neg not in contexts: + negatives.append(neg) + all_negatives.append(negatives) + return all_negatives + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def batchify(data): + max_len = max(len(c) + len(n) for _, c, n in data) + centers, contexts_negatives, masks, labels = [], [], [], [] + for center, context, negative in data: + cur_len = len(context) + len(negative) + centers += [center] + contexts_negatives += [context + negative + [0] * (max_len - cur_len)] + masks += [[1] * cur_len + [0] * (max_len - cur_len)] + labels += [[1] * len(context) + [0] * (max_len - len(context))] + return (d2l.reshape(d2l.tensor(centers), (-1, 1)), d2l.tensor(contexts_negatives), + d2l.tensor(masks), d2l.tensor(labels)) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/word-embedding-dataset.md +def load_data_ptb(batch_size, max_window_size, num_noise_words): + num_workers = d2l.get_dataloader_workers() + sentences = read_ptb() + vocab = d2l.Vocab(sentences, min_freq=10) + subsampled = subsampling(sentences, vocab) + corpus = [vocab[line] for line in subsampled] + all_centers, all_contexts = get_centers_and_contexts( + corpus, max_window_size) + all_negatives = get_negatives(all_contexts, corpus, num_noise_words) + + class PTBDataset(torch.utils.data.Dataset): + def __init__(self, centers, contexts, negatives): + assert len(centers) == len(contexts) == len(negatives) + self.centers = centers + self.contexts = contexts + self.negatives = negatives + + def __getitem__(self, index): + return (self.centers[index], self.contexts[index], self.negatives[index]) + + def __len__(self): + return len(self.centers) + + dataset = PTBDataset( + all_centers, all_contexts, all_negatives) + + data_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True, + collate_fn=batchify, + num_workers=num_workers) + return data_iter, vocab + + +# Defined in file: ./chapter_natural-language-processing-pretraining/similarity-analogy.md +d2l.DATA_HUB['glove.6b.50d'] = (d2l.DATA_URL + 'glove.6B.50d.zip', + '0b8703943ccdb6eb788e6f091b8946e82231bc4d') + +d2l.DATA_HUB['glove.6b.100d'] = (d2l.DATA_URL + 'glove.6B.100d.zip', + 'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a') + +d2l.DATA_HUB['glove.42b.300d'] = (d2l.DATA_URL + 'glove.42B.300d.zip', + 'b5116e234e9eb9076672cfeabf5469f3eec904fa') + +d2l.DATA_HUB['wiki.en'] = (d2l.DATA_URL + 'wiki.en.zip', + 'c1816da3821ae9f43899be655002f6c723e91b88') + + +# Defined in file: ./chapter_natural-language-processing-pretraining/similarity-analogy.md +class TokenEmbedding: + """Token Embedding.""" + def __init__(self, embedding_name): + self.idx_to_token, self.idx_to_vec = self._load_embedding( + embedding_name) + self.unknown_idx = 0 + self.token_to_idx = {token: idx for idx, token in + enumerate(self.idx_to_token)} + + def _load_embedding(self, embedding_name): + idx_to_token, idx_to_vec = [''], [] + data_dir = d2l.download_extract(embedding_name) + # GloVe website: https://nlp.stanford.edu/projects/glove/ + # fastText website: https://fasttext.cc/ + with open(os.path.join(data_dir, 'vec.txt'), 'r') as f: + for line in f: + elems = line.rstrip().split(' ') + token, elems = elems[0], [float(elem) for elem in elems[1:]] + # Skip header information, such as the top row in fastText + if len(elems) > 1: + idx_to_token.append(token) + idx_to_vec.append(elems) + idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec + return idx_to_token, d2l.tensor(idx_to_vec) + + def __getitem__(self, tokens): + indices = [self.token_to_idx.get(token, self.unknown_idx) + for token in tokens] + vecs = self.idx_to_vec[d2l.tensor(indices)] + return vecs + + def __len__(self): + return len(self.idx_to_token) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +def get_tokens_and_segments(tokens_a, tokens_b=None): + tokens = [''] + tokens_a + [''] + # 0 and 1 are marking segment A and B, respectively + segments = [0] * (len(tokens_a) + 2) + if tokens_b is not None: + tokens += tokens_b + [''] + segments += [1] * (len(tokens_b) + 1) + return tokens, segments + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class BERTEncoder(nn.Module): + def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, + ffn_num_hiddens, num_heads, num_layers, dropout, + max_len=1000, key_size=768, query_size=768, value_size=768, + **kwargs): + super(BERTEncoder, self).__init__(**kwargs) + self.token_embedding = nn.Embedding(vocab_size, num_hiddens) + self.segment_embedding = nn.Embedding(2, num_hiddens) + self.blks = nn.Sequential() + for i in range(num_layers): + self.blks.add_module(f"{i}", d2l.EncoderBlock( + key_size, query_size, value_size, num_hiddens, norm_shape, + ffn_num_input, ffn_num_hiddens, num_heads, dropout, True)) + # In BERT, positional embeddings are learnable, thus we create a + # parameter of positional embeddings that are long enough + self.pos_embedding = nn.Parameter(torch.randn(1, max_len, + num_hiddens)) + + def forward(self, tokens, segments, valid_lens): + # Shape of `X` remains unchanged in the following code snippet: + # (batch size, max sequence length, `num_hiddens`) + X = self.token_embedding(tokens) + self.segment_embedding(segments) + X = X + self.pos_embedding.data[:, :X.shape[1], :] + for blk in self.blks: + X = blk(X, valid_lens) + return X + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class MaskLM(nn.Module): + def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs): + super(MaskLM, self).__init__(**kwargs) + self.mlp = nn.Sequential(nn.Linear(num_inputs, num_hiddens), + nn.ReLU(), + nn.LayerNorm(num_hiddens), + nn.Linear(num_hiddens, vocab_size)) + + def forward(self, X, pred_positions): + num_pred_positions = pred_positions.shape[1] + pred_positions = pred_positions.reshape(-1) + batch_size = X.shape[0] + batch_idx = torch.arange(0, batch_size) + # Suppose that `batch_size` = 2, `num_pred_positions` = 3, then + # `batch_idx` is `torch.tensor([0, 0, 0, 1, 1, 1])` + batch_idx = torch.repeat_interleave(batch_idx, num_pred_positions) + masked_X = X[batch_idx, pred_positions] + masked_X = masked_X.reshape((batch_size, num_pred_positions, -1)) + mlm_Y_hat = self.mlp(masked_X) + return mlm_Y_hat + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class NextSentencePred(nn.Module): + def __init__(self, num_inputs, **kwargs): + super(NextSentencePred, self).__init__(**kwargs) + self.output = nn.Linear(num_inputs, 2) + + def forward(self, X): + # `X` shape: (batch size, `num_hiddens`) + return self.output(X) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert.md +class BERTModel(nn.Module): + def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input, + ffn_num_hiddens, num_heads, num_layers, dropout, + max_len=1000, key_size=768, query_size=768, value_size=768, + hid_in_features=768, mlm_in_features=768, + nsp_in_features=768): + super(BERTModel, self).__init__() + self.encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, + ffn_num_input, ffn_num_hiddens, num_heads, num_layers, + dropout, max_len=max_len, key_size=key_size, + query_size=query_size, value_size=value_size) + self.hidden = nn.Sequential(nn.Linear(hid_in_features, num_hiddens), + nn.Tanh()) + self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features) + self.nsp = NextSentencePred(nsp_in_features) + + def forward(self, tokens, segments, valid_lens=None, pred_positions=None): + encoded_X = self.encoder(tokens, segments, valid_lens) + if pred_positions is not None: + mlm_Y_hat = self.mlm(encoded_X, pred_positions) + else: + mlm_Y_hat = None + # The hidden layer of the MLP classifier for next sentence prediction. + # 0 is the index of the '' token + nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :])) + return encoded_X, mlm_Y_hat, nsp_Y_hat + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +d2l.DATA_HUB['wikitext-2'] = ( + 'https://s3.amazonaws.com/research.metamind.io/wikitext/' + 'wikitext-2-v1.zip', '3c914d17d80b1459be871a5039ac23e752a53cbe') + +def _read_wiki(data_dir): + file_name = os.path.join(data_dir, 'wiki.train.tokens') + with open(file_name, 'r') as f: + lines = f.readlines() + # Uppercase letters are converted to lowercase ones + paragraphs = [line.strip().lower().split(' . ') + for line in lines if len(line.split(' . ')) >= 2] + random.shuffle(paragraphs) + return paragraphs + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _get_next_sentence(sentence, next_sentence, paragraphs): + if random.random() < 0.5: + is_next = True + else: + # `paragraphs` is a list of lists of lists + next_sentence = random.choice(random.choice(paragraphs)) + is_next = False + return sentence, next_sentence, is_next + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _get_nsp_data_from_paragraph(paragraph, paragraphs, vocab, max_len): + nsp_data_from_paragraph = [] + for i in range(len(paragraph) - 1): + tokens_a, tokens_b, is_next = _get_next_sentence( + paragraph[i], paragraph[i + 1], paragraphs) + # Consider 1 '' token and 2 '' tokens + if len(tokens_a) + len(tokens_b) + 3 > max_len: + continue + tokens, segments = d2l.get_tokens_and_segments(tokens_a, tokens_b) + nsp_data_from_paragraph.append((tokens, segments, is_next)) + return nsp_data_from_paragraph + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _replace_mlm_tokens(tokens, candidate_pred_positions, num_mlm_preds, + vocab): + # Make a new copy of tokens for the input of a masked language model, + # where the input may contain replaced '' or random tokens + mlm_input_tokens = [token for token in tokens] + pred_positions_and_labels = [] + # Shuffle for getting 15% random tokens for prediction in the masked + # language modeling task + random.shuffle(candidate_pred_positions) + for mlm_pred_position in candidate_pred_positions: + if len(pred_positions_and_labels) >= num_mlm_preds: + break + masked_token = None + # 80% of the time: replace the word with the '' token + if random.random() < 0.8: + masked_token = '' + else: + # 10% of the time: keep the word unchanged + if random.random() < 0.5: + masked_token = tokens[mlm_pred_position] + # 10% of the time: replace the word with a random word + else: + masked_token = random.randint(0, len(vocab) - 1) + mlm_input_tokens[mlm_pred_position] = masked_token + pred_positions_and_labels.append( + (mlm_pred_position, tokens[mlm_pred_position])) + return mlm_input_tokens, pred_positions_and_labels + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _get_mlm_data_from_tokens(tokens, vocab): + candidate_pred_positions = [] + # `tokens` is a list of strings + for i, token in enumerate(tokens): + # Special tokens are not predicted in the masked language modeling + # task + if token in ['', '']: + continue + candidate_pred_positions.append(i) + # 15% of random tokens are predicted in the masked language modeling task + num_mlm_preds = max(1, round(len(tokens) * 0.15)) + mlm_input_tokens, pred_positions_and_labels = _replace_mlm_tokens( + tokens, candidate_pred_positions, num_mlm_preds, vocab) + pred_positions_and_labels = sorted(pred_positions_and_labels, + key=lambda x: x[0]) + pred_positions = [v[0] for v in pred_positions_and_labels] + mlm_pred_labels = [v[1] for v in pred_positions_and_labels] + return vocab[mlm_input_tokens], pred_positions, vocab[mlm_pred_labels] + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def _pad_bert_inputs(examples, max_len, vocab): + max_num_mlm_preds = round(max_len * 0.15) + all_token_ids, all_segments, valid_lens, = [], [], [] + all_pred_positions, all_mlm_weights, all_mlm_labels = [], [], [] + nsp_labels = [] + for (token_ids, pred_positions, mlm_pred_label_ids, segments, + is_next) in examples: + all_token_ids.append(torch.tensor(token_ids + [vocab['']] * ( + max_len - len(token_ids)), dtype=torch.long)) + all_segments.append(torch.tensor(segments + [0] * ( + max_len - len(segments)), dtype=torch.long)) + # `valid_lens` excludes count of '' tokens + valid_lens.append(torch.tensor(len(token_ids), dtype=torch.float32)) + all_pred_positions.append(torch.tensor(pred_positions + [0] * ( + max_num_mlm_preds - len(pred_positions)), dtype=torch.long)) + # Predictions of padded tokens will be filtered out in the loss via + # multiplication of 0 weights + all_mlm_weights.append( + torch.tensor([1.0] * len(mlm_pred_label_ids) + [0.0] * ( + max_num_mlm_preds - len(pred_positions)), + dtype=torch.float32)) + all_mlm_labels.append(torch.tensor(mlm_pred_label_ids + [0] * ( + max_num_mlm_preds - len(mlm_pred_label_ids)), dtype=torch.long)) + nsp_labels.append(torch.tensor(is_next, dtype=torch.long)) + return (all_token_ids, all_segments, valid_lens, all_pred_positions, + all_mlm_weights, all_mlm_labels, nsp_labels) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +class _WikiTextDataset(torch.utils.data.Dataset): + def __init__(self, paragraphs, max_len): + # Input `paragraphs[i]` is a list of sentence strings representing a + # paragraph; while output `paragraphs[i]` is a list of sentences + # representing a paragraph, where each sentence is a list of tokens + paragraphs = [d2l.tokenize( + paragraph, token='word') for paragraph in paragraphs] + sentences = [sentence for paragraph in paragraphs + for sentence in paragraph] + self.vocab = d2l.Vocab(sentences, min_freq=5, reserved_tokens=[ + '', '', '', '']) + # Get data for the next sentence prediction task + examples = [] + for paragraph in paragraphs: + examples.extend(_get_nsp_data_from_paragraph( + paragraph, paragraphs, self.vocab, max_len)) + # Get data for the masked language model task + examples = [(_get_mlm_data_from_tokens(tokens, self.vocab) + + (segments, is_next)) + for tokens, segments, is_next in examples] + # Pad inputs + (self.all_token_ids, self.all_segments, self.valid_lens, + self.all_pred_positions, self.all_mlm_weights, + self.all_mlm_labels, self.nsp_labels) = _pad_bert_inputs( + examples, max_len, self.vocab) + + def __getitem__(self, idx): + return (self.all_token_ids[idx], self.all_segments[idx], + self.valid_lens[idx], self.all_pred_positions[idx], + self.all_mlm_weights[idx], self.all_mlm_labels[idx], + self.nsp_labels[idx]) + + def __len__(self): + return len(self.all_token_ids) + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-dataset.md +def load_data_wiki(batch_size, max_len): + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('wikitext-2', 'wikitext-2') + paragraphs = _read_wiki(data_dir) + train_set = _WikiTextDataset(paragraphs, max_len) + train_iter = torch.utils.data.DataLoader(train_set, batch_size, + shuffle=True, num_workers=num_workers) + return train_iter, train_set.vocab + + +# Defined in file: ./chapter_natural-language-processing-pretraining/bert-pretraining.md +def _get_batch_loss_bert(net, loss, vocab_size, tokens_X, + segments_X, valid_lens_x, + pred_positions_X, mlm_weights_X, + mlm_Y, nsp_y): + # Forward pass + _, mlm_Y_hat, nsp_Y_hat = net(tokens_X, segments_X, + valid_lens_x.reshape(-1), + pred_positions_X) + # Compute masked language model loss + mlm_l = loss(mlm_Y_hat.reshape(-1, vocab_size), mlm_Y.reshape(-1)) *\ + mlm_weights_X.reshape(-1, 1) + mlm_l = mlm_l.sum() / (mlm_weights_X.sum() + 1e-8) + # Compute next sentence prediction loss + nsp_l = loss(nsp_Y_hat, nsp_y) + l = mlm_l + nsp_l + return mlm_l, nsp_l, l + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +d2l.DATA_HUB['aclImdb'] = ( + 'http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz', + '01ada507287d82875905620988597833ad4e0903') + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +def read_imdb(data_dir, is_train): + data, labels = [], [] + for label in ('pos', 'neg'): + folder_name = os.path.join(data_dir, 'train' if is_train else 'test', + label) + for file in os.listdir(folder_name): + with open(os.path.join(folder_name, file), 'rb') as f: + review = f.read().decode('utf-8').replace('\n', '') + data.append(review) + labels.append(1 if label == 'pos' else 0) + return data, labels + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-and-dataset.md +def load_data_imdb(batch_size, num_steps=500): + data_dir = d2l.download_extract('aclImdb', 'aclImdb') + train_data = read_imdb(data_dir, True) + test_data = read_imdb(data_dir, False) + train_tokens = d2l.tokenize(train_data[0], token='word') + test_tokens = d2l.tokenize(test_data[0], token='word') + vocab = d2l.Vocab(train_tokens, min_freq=5) + train_features = torch.tensor([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in train_tokens]) + test_features = torch.tensor([d2l.truncate_pad( + vocab[line], num_steps, vocab['']) for line in test_tokens]) + train_iter = d2l.load_array((train_features, torch.tensor(train_data[1])), + batch_size) + test_iter = d2l.load_array((test_features, torch.tensor(test_data[1])), + batch_size, + is_train=False) + return train_iter, test_iter, vocab + + +# Defined in file: ./chapter_natural-language-processing-applications/sentiment-analysis-rnn.md +def predict_sentiment(net, vocab, sentence): + sentence = torch.tensor(vocab[sentence.split()], device=d2l.try_gpu()) + label = torch.argmax(net(sentence.reshape(1, -1)), dim=1) + return 'positive' if label == 1 else 'negative' + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +d2l.DATA_HUB['SNLI'] = ( + 'https://nlp.stanford.edu/projects/snli/snli_1.0.zip', + '9fcde07509c7e87ec61c640c1b2753d9041758e4') + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +def read_snli(data_dir, is_train): + """Read the SNLI dataset into premises, hypotheses, and labels.""" + def extract_text(s): + # Remove information that will not be used by us + s = re.sub('\\(', '', s) + s = re.sub('\\)', '', s) + # Substitute two or more consecutive whitespace with space + s = re.sub('\\s{2,}', ' ', s) + return s.strip() + label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2} + file_name = os.path.join(data_dir, 'snli_1.0_train.txt' + if is_train else 'snli_1.0_test.txt') + with open(file_name, 'r') as f: + rows = [row.split('\t') for row in f.readlines()[1:]] + premises = [extract_text(row[1]) for row in rows if row[0] in label_set] + hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set] + labels = [label_set[row[0]] for row in rows if row[0] in label_set] + return premises, hypotheses, labels + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +class SNLIDataset(torch.utils.data.Dataset): + """A customized dataset to load the SNLI dataset.""" + def __init__(self, dataset, num_steps, vocab=None): + self.num_steps = num_steps + all_premise_tokens = d2l.tokenize(dataset[0]) + all_hypothesis_tokens = d2l.tokenize(dataset[1]) + if vocab is None: + self.vocab = d2l.Vocab(all_premise_tokens + all_hypothesis_tokens, + min_freq=5, reserved_tokens=['']) + else: + self.vocab = vocab + self.premises = self._pad(all_premise_tokens) + self.hypotheses = self._pad(all_hypothesis_tokens) + self.labels = torch.tensor(dataset[2]) + print('read ' + str(len(self.premises)) + ' examples') + + def _pad(self, lines): + return torch.tensor([d2l.truncate_pad( + self.vocab[line], self.num_steps, self.vocab['']) + for line in lines]) + + def __getitem__(self, idx): + return (self.premises[idx], self.hypotheses[idx]), self.labels[idx] + + def __len__(self): + return len(self.premises) + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-and-dataset.md +def load_data_snli(batch_size, num_steps=50): + """Download the SNLI dataset and return data iterators and vocabulary.""" + num_workers = d2l.get_dataloader_workers() + data_dir = d2l.download_extract('SNLI') + train_data = read_snli(data_dir, True) + test_data = read_snli(data_dir, False) + train_set = SNLIDataset(train_data, num_steps) + test_set = SNLIDataset(test_data, num_steps, train_set.vocab) + train_iter = torch.utils.data.DataLoader(train_set, batch_size, + shuffle=True, + num_workers=num_workers) + test_iter = torch.utils.data.DataLoader(test_set, batch_size, + shuffle=False, + num_workers=num_workers) + return train_iter, test_iter, train_set.vocab + + +# Defined in file: ./chapter_natural-language-processing-applications/natural-language-inference-attention.md +def predict_snli(net, vocab, premise, hypothesis): + net.eval() + premise = torch.tensor(vocab[premise], device=d2l.try_gpu()) + hypothesis = torch.tensor(vocab[hypothesis], device=d2l.try_gpu()) + label = torch.argmax(net([premise.reshape((1, -1)), + hypothesis.reshape((1, -1))]), dim=1) + return 'entailment' if label == 0 else 'contradiction' if label == 1 \ + else 'neutral' + + +# Defined in file: ./chapter_generative-adversarial-networks/gan.md +def update_D(X, Z, net_D, net_G, loss, trainer_D): + """Update discriminator.""" + batch_size = X.shape[0] + ones = torch.ones((batch_size,), device=X.device) + zeros = torch.zeros((batch_size,), device=X.device) + trainer_D.zero_grad() + real_Y = net_D(X) + fake_X = net_G(Z) + # Do not need to compute gradient for `net_G`, detach it from + # computing gradients. + fake_Y = net_D(fake_X.detach()) + loss_D = (loss(real_Y, ones.reshape(real_Y.shape)) + + loss(fake_Y, zeros.reshape(fake_Y.shape))) / 2 + loss_D.backward() + trainer_D.step() + return loss_D + + +# Defined in file: ./chapter_generative-adversarial-networks/gan.md +def update_G(Z, net_D, net_G, loss, trainer_G): + """Update generator.""" + batch_size = Z.shape[0] + ones = torch.ones((batch_size,), device=Z.device) + trainer_G.zero_grad() + # We could reuse `fake_X` from `update_D` to save computation + fake_X = net_G(Z) + # Recomputing `fake_Y` is needed since `net_D` is changed + fake_Y = net_D(fake_X) + loss_G = loss(fake_Y, ones.reshape(fake_Y.shape)) + loss_G.backward() + trainer_G.step() + return loss_G + + +# Defined in file: ./chapter_generative-adversarial-networks/dcgan.md +d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip', + 'c065c0e2593b8b161a2d7873e42418bf6a21106c') + + # Alias defined in config.ini From ba34cd7464c2b9920454ee1c49e533446d9187a8 Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Tue, 20 Apr 2021 18:38:34 +0200 Subject: [PATCH 052/103] fix translation in chapter_linear-networks/image-classification (#756) --- chapter_linear-networks/image-classification-dataset.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter_linear-networks/image-classification-dataset.md b/chapter_linear-networks/image-classification-dataset.md index 63e760b33..be305ee80 100644 --- a/chapter_linear-networks/image-classification-dataset.md +++ b/chapter_linear-networks/image-classification-dataset.md @@ -163,7 +163,7 @@ show_images(X, 2, 9, titles=get_fashion_mnist_labels(y)); batch_size = 256 def get_dataloader_workers(): #@save - """在非Windows的平台上,使用4个进程来读取的数据。""" + """在非Windows的平台上,使用4个进程来读取数据。""" return 0 if sys.platform.startswith('win') else 4 # 通过ToTensor实例将图像数据从uint8格式变换成32位浮点数格式,并除以255使得所有像素 @@ -179,7 +179,7 @@ train_iter = gluon.data.DataLoader(mnist_train.transform_first(transformer), batch_size = 256 def get_dataloader_workers(): #@save - """使用4个进程来读取的数据。""" + """使用4个进程来读取数据。""" return 4 train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True, From c2ad179bc891557601060c0d5811fdd30ce1eb6e Mon Sep 17 00:00:00 2001 From: Linhan Wu <1002503818@qq.com> Date: Wed, 21 Apr 2021 00:39:43 +0800 Subject: [PATCH 053/103] fix typo in resnet.md (#758) * fix 4.4.1.2. Model Complexity translation issues * fix typo in chapter_multilayer-perceptrons/environment.md * fix typo and translation issues in kaggle-house-price.md * fix typo in model-construction.md * fix typo and translation issues in use-gpu.md * fix typo and translation issues in alexnet.md * fix typo in vgg.md * revert back to the original * fix typo in batch-norm.md * fix typo in resnet.md Co-authored-by: Linhan_Wu --- chapter_convolutional-modern/resnet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter_convolutional-modern/resnet.md b/chapter_convolutional-modern/resnet.md index ca2aa5a84..877952b24 100644 --- a/chapter_convolutional-modern/resnet.md +++ b/chapter_convolutional-modern/resnet.md @@ -21,7 +21,7 @@ $$f^*_\mathcal{F} := \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \te 然而,如果 $\mathcal{F} \not\subseteq \mathcal{F}'$,则无法保证新的体系“更近似”。 事实上, $f^*_{\mathcal{F}'}$ 可能更糟: 如 :numref:`fig_functionclasses` 所示,对于非嵌套函数(non-nested function)类,较复杂的函数类并不总是向“真”函数 $f^*$ 靠拢(复杂度由 $\mathcal{F}_1$ 向 $\mathcal{F}_6$ 递增)。 -在 :numref:`fig_functionclasses` 的左边,虽然 $\mathcal{F}_3$ 比 $f^*$ 更接近 $f^*$,但$\mathcal{F}_6$ 却离的更远了。 +在 :numref:`fig_functionclasses` 的左边,虽然 $\mathcal{F}_3$ 比 $\mathcal{F}_1$ 更接近 $f^*$,但$\mathcal{F}_6$ 却离的更远了。 相反对于 :numref:`fig_functionclasses` 右侧的嵌套函数(nested function)类 $\mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6$,我们可以避免上述问题。 ![对于非嵌套函数类,较复杂(由较大区域表示)的函数类不能保证更接近“真”函数( $f^*$ )。这种现象在嵌套函数类中不会发生。](../img/functionclasses.svg) @@ -35,7 +35,7 @@ $$f^*_\mathcal{F} := \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \te 它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。 残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。 于是,*残差块* (residual blocks) 便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。 -凭借它,ResNet 赢得了 2015 年 ImageNet 大规模视觉识别挑战赛。。 +凭借它,ResNet 赢得了 2015 年 ImageNet 大规模视觉识别挑战赛。 ## 残差块 From 3ca9c2a814616b2f1e3d131a9fd0085d843799f6 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Wed, 21 Apr 2021 00:45:56 +0800 Subject: [PATCH 054/103] chapter_recurrent-modern/beam-search (#730) * chapter_recurrent-modern/beam-search * Update beam-search.md * Update beam-search.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/beam-search.md | 61 ++++++++++++------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/chapter_recurrent-modern/beam-search.md b/chapter_recurrent-modern/beam-search.md index 9e99fd522..6e47e1e65 100644 --- a/chapter_recurrent-modern/beam-search.md +++ b/chapter_recurrent-modern/beam-search.md @@ -1,75 +1,74 @@ # 束搜索 :label:`sec_beam-search` -在 :numref:`sec_seq2seq` 中,我们通过令牌预测了输出序列令牌,直到特殊的序列结束 “” 令牌被预测为止。在本节中,我们将从正式确定这个 * 贪婪的搜索 * 策略并探索问题开始,然后将此策略与其他备选方案进行比较: -*详尽搜索 * 和 * 束搜索 *。 +在:numref:`sec_seq2seq`中,我们逐个标记地预测输出序列令牌,直到预测出序列结束标记“<eos>”。在本节中,我们将首先对这种*贪心搜索*(greedy search)策略进行介绍,并探讨其存在的问题,然后将这种策略与其他替代策略进行比较:*穷举搜索*(exhaustive search)和*束搜索*(beam search)。 -在正式介绍贪婪搜索之前,让我们使用 :numref:`sec_seq2seq` 的相同数学符号将搜索问题正式化。在任何时间步骤 $t'$,解码器输出 $y_{t'}$ 的概率取决于 $t'$ 之前的输出子序列 $y_1, \ldots, y_{t'-1}$ 和编码输入序列信息的上下文变量 $\mathbf{c}$。要量化计算成本,用 $\mathcal{Y}$(包含 “”)表示输出词汇表。因此,这套词汇集的基数 $\left|\mathcal{Y}\right|$ 就是词汇量大小。让我们还将输出序列的最大令牌数指定为 $T'$。因此,我们的目标是从所有 $\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$ 可能的输出序列中寻找理想的输出。当然,对于所有这些输出序列,包括 “” 之后的部分将被丢弃在实际输出中。 +在正式介绍贪心搜索之前,让我们使用 :numref:`sec_seq2seq` 中相同的数学符号定义搜索问题。在任何时间步$t'$,解码器输出$y_{t'}$的概率取决于$t'$之前的输出子序列$y_1, \ldots, y_{t'-1}$和编码输入序列信息的上下文变量$\mathbf{c}$。为了量化计算成本,用$\mathcal{Y}$(它包含“<eos>”)表示输出词汇表。所以这个词汇集合的基数$\left|\mathcal{Y}\right|$就是词汇大小。我们还将输出序列的最大标记数指定为$T'$。因此,我们的目标是从所有$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$个可能的输出序列中寻找理想的输出。当然,对于所有这些输出序列,包括“<eos>”和之后的部分将在实际输出中丢弃。 -## 贪婪搜索 +## 贪心搜索 -首先,让我们来看一个简单的策略:* 贪心搜索 *。该策略已被用来预测 :numref:`sec_seq2seq` 中的序列。在贪婪搜索中,在输出序列的任何时间步骤 $t'$,我们搜索条件概率从 $\mathcal{Y}$ 起最高的令牌,即 +首先,让我们看看一个简单的策略:*贪心搜索*。该策略已用于:numref:`sec_seq2seq`的序列预测。在贪心搜索中,在输出序列的任何时间步$t'$,我们从$\mathcal{Y}$中搜索具有最高条件概率的标记,即: -$$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} P(y \mid y_1, \ldots, y_{t'-1}, \mathbf{c}),$$ +$$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} P(y \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$$ -作为输出。一旦输 出 “” 或输出序列达到最大长度 $T'$,输出序列就完成。 +一旦输出“<eos>”或输出序列达到其最大长度$T'$,输出序列即完成。 -那么贪婪的搜索会出什么问题?事实上,* 最佳序列 * 应该是最大 $\prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$ 的输出序列,这是基于输入序列生成输出序列的条件概率。不幸的是,不能保证贪婪的搜索能够获得最佳顺序。 +那么贪心搜索会出什么问题呢?实际上,*最优序列*(optimal sequence)应该是最大化$\prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$值的输出序列,这是基于输入序列生成输出序列的条件概率。不幸的是,不能保证通过贪心搜索得到最优序列。 -![At each time step, greedy search selects the token with the highest conditional probability.](../img/s2s-prob1.svg) +![在每个时间步,贪心搜索选择具有最高条件概率的标记。](../img/s2s-prob1.svg) :label:`fig_s2s-prob1` -让我们用一个例子来说明它。假设 输出字典中有四个标记 “A”、“B”、“C” 和 “”。在 :numref:`fig_s2s-prob1` 中,每个时间步长下的四个数字分别代表在 该时间步长生成 “A”、“B”、“C” 和 “” 的条件概率。在每个时间步骤中,贪婪搜索都会选择条件概率最高的令牌。因此,输出序列 “A”、“B”、“C” 和 “” 将在 :numref:`fig_s2s-prob1` 中进行预测。此输出序列的条件概率为 $0.5\times0.4\times0.4\times0.6 = 0.048$。 +让我们用一个例子来说明这一点。假设输出中有四个标记“A”、“B”、“C”和“<eos>”。 在:numref:`fig_s2s-prob1` 中,每个时间步下的四个数字分别表示在该时间步生成“A”、“B”、“C”和“<eos>”的条件概率。在每个时间步,贪心搜索选择具有最高条件概率的令牌。因此,将在 :numref:`fig_s2s-prob1` 中预测输出序列“A”、“B”、“C”和“<eos>”。这个输出序列的条件概率是$0.5\times0.4\times0.4\times0.6 = 0.048$。 -![The four numbers under each time step represent the conditional probabilities of generating "A", "B", "C", and "<eos>" at that time step. At time step 2, the token "C", which has the second highest conditional probability, is selected.](../img/s2s-prob2.svg) +![每个时间步下的四个数字表示在该时间步生成“A”、“B”、“C”和“<eos>”的条件概率。在时间步2,选择具有第二高条件概率的令牌“C”。](../img/s2s-prob2.svg) :label:`fig_s2s-prob2` -接下来,让我们看一下 :numref:`fig_s2s-prob2` 中的另一个例子。与 :numref:`fig_s2s-prob1` 不同,我们在时间步骤 2 中选择 :numref:`fig_s2s-prob2` 中的令牌 “C”,该代币的条件概率为 * 秒 * 最高。由于时间步骤 1 和 2 的输出子序列(时间步骤 3 所基于的时间步骤 1 和 2)已从 :numref:`fig_s2s-prob1` 中的 “A” 和 “B” 变为 :numref:`fig_s2s-prob2` 中的 “A” 和 “C”,因此,时间步骤 3 中每个令牌的条件概率也在 :numref:`fig_s2s-prob2` 中发生了变化。假设我们在时间步骤 3 中选择令牌 “B”。现在,时间步长 4 取决于前三个时间步长 “A”、“C” 和 “B” 的输出子序列,这与 :numref:`fig_s2s-prob1` 中的 “A”、“B” 和 “C” 不同。因此,在 :numref:`fig_s2s-prob2` 的时间步骤 4 生成每个令牌的条件概率也与 :numref:`fig_s2s-prob1` 中的不同。因此, :numref:`fig_s2s-prob2` 中输出序列 “A”、“C”、“B” 和 “” 的条件概率为 $0.5\times0.3 \times0.6\times0.6=0.054$,比 :numref:`fig_s2s-prob1` 中贪婪搜索的概率大。在此示例中, 贪婪搜索获得的输出序列 “A”、“B”、“C” 和 “” 不是最佳序列。 +接下来,让我们看看 :numref:`fig_s2s-prob2` 中的另一个例子。与 :numref:`fig_s2s-prob1` 不同,在时间步2中,我们选择 :numref:`fig_s2s-prob2` 中的令牌“C”,它具有第二高的条件概率。由于时间步3所基于的时间步1和2处的输出子序列已从 :numref:`fig_s2s-prob1` 中的“A”和“B”改变为 :numref:`fig_s2s-prob2` 中的“A”和“C”,因此时间步3处的每个标记的条件概率也在 :numref:`fig_s2s-prob2` 中改变。假设我们在时间步3选择令牌“B”。现在,时间步4以前三个时间步“A”、“C”和“B”的输出子序列为条件,这与 :numref:`fig_s2s-prob1` 中的“A”、“B”和“C”不同。因此,在 :numref:`fig_s2s-prob2` 中的时间步4生成每个标记的条件概率也不同于 :numref:`fig_s2s-prob1` 中的条件概率。结果,:numref:`fig_s2s-prob2`中的输出序列“A”、“C”、“B”和“<eos>”的条件概率为$0.5\times0.3 \times0.6\times0.6=0.054$,这大于:numref:`fig_s2s-prob1`中的贪心搜索的条件概率。在本例中,通过贪心搜索获得的输出序列“A”、“B”、“C”和“<eos>”不是最佳序列。 -## 详尽搜索 +## 穷举搜索 -如果目标是获得最佳序列,我们可以考虑使用 * 详尽无遗的搜索 *:用条件概率详尽枚举所有可能的输出序列,然后输出条件概率最高的输出序列。 +如果目标是获得最优序列,我们可以考虑使用*穷举搜索*(exhaustive search):穷举地枚举所有可能的输出序列及其条件概率,然后输出条件概率最高的一个。 -尽管我们可以使用详尽搜索来获得最佳序列,但其计算成本 $\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$ 可能会过高。例如,当 $|\mathcal{Y}|=10000$ 和 $T'=10$ 时,我们需要评估 $10000^{10} = 10^{40}$ 序列。这几乎是不可能的!另一方面,贪婪搜索的计算成本是 $\mathcal{O}(\left|\mathcal{Y}\right|T')$:它通常远低于详尽搜索。例如,当 $|\mathcal{Y}|=10000$ 和 $T'=10$ 时,我们只需要评估 $10000\times10=10^5$ 序列。 +虽然我们可以使用穷举搜索来获得最优序列,但其计算量$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$可能过高。例如,当$|\mathcal{Y}|=10000$和$T'=10$时,我们需要评估$10000^{10} = 10^{40}$序列。这几乎是不可能的。另一方面,贪心搜索的计算量是$\mathcal{O}(\left|\mathcal{Y}\right|T')$:它通常明显小于穷举搜索。例如,当$|\mathcal{Y}|=10000$和$T'=10$时,我们只需要评估$10000\times10=10^5$个序列。 ## 束搜索 -关于序列搜索策略的决策取决于一个范围,在任何极端都很容易提出问题。如果只有准确性重要呢?显然,详尽的搜索。如果只有计算成本重要,该怎么办?显然,贪婪的搜索。真实世界的应用程序通常会提出一个复杂的问题,介于这两个极端之间。 +关于序列搜索策略的决定取决于一个范围,在任何一个极端都有问题。如果只有准确性才重要呢?显然,穷举搜索。如果计算成本很重要呢?显然,贪心搜索。实际应用介于这两个极端之间。 -*Beam search * 是贪婪搜索的改进版本。它有一个名为 * 束尺寸 * 的超参数,$k$。 -在时间步骤 1,我们选择了条件概率最高的 $k$ 令牌。他们每个人都将分别成为 $k$ 个候选输出序列的第一个令牌。在后续的每个时间步中,根据上一个时间步的 $k$ 个候选输出序列,我们继续选择 $k$ 个候选输出序列,其条件概率为 $k\left|\mathcal{Y}\right|$ 个可能的选择。 +*束搜索*(beam search)是贪心搜索的改进版本。它有一个超参数,名为*束宽*(beam size)$k$。 +在时间步1,我们选择具有最高条件概率的$k$个标记。它们中的每一个将分别是$k$个候选输出序列的第一个标记。在随后的每个时间步,基于上一时间步的$k$个候选输出序列,我们继续从$k\left|\mathcal{Y}\right|$个可能的选择中选择具有最高条件概率的$k$个候选输出序列。 -![The process of beam search (beam size: 2, maximum length of an output sequence: 3). The candidate output sequences are $A$, $C$, $AB$, $CE$, $ABD$, and $CED$.](../img/beam-search.svg) +![束搜索过程(束宽:2,输出序列的最大长度:3)。候选输出序列是$A$、$C$、$AB$、$CE$、$ABD$和$CED$。](../img/beam-search.svg) :label:`fig_beam-search` -:numref:`fig_beam-search` 以示例演示了光束搜索的过程。假设输出词汇表只包含五个元素:$\mathcal{Y} = \{A, B, C, D, E\}$,其中一个是 “”。让波束大小为 2,输出序列的最大长度为 3。在时间步骤 1,假设条件概率最高 $P(y_1 \mid \mathbf{c})$ 的令牌分别为 $A$ 和 $C$。在时间步骤 2,我们计算了所有 $y_2 \in \mathcal{Y},$ +:numref:`fig_beam-search`演示了束搜索的过程。假设输出词表只包含五个元素:$\mathcal{Y} = \{A, B, C, D, E\}$,其中一个是“<eos>”。让束宽为2,输出序列的最大长度为3。在时间步1,假设具有最高条件概率$P(y_1 \mid \mathbf{c})$的标记是$A$和$C$。在时间步2,我们计算所有$y_2 \in \mathcal{Y}$: $$\begin{aligned}P(A, y_2 \mid \mathbf{c}) = P(A \mid \mathbf{c})P(y_2 \mid A, \mathbf{c}),\\ P(C, y_2 \mid \mathbf{c}) = P(C \mid \mathbf{c})P(y_2 \mid C, \mathbf{c}),\end{aligned}$$ -然后选择这十个值中最大的两个,比如 $P(A, B \mid \mathbf{c})$ 和 $P(C, E \mid \mathbf{c})$。然后在时间步骤 3,对于所有 $y_3 \in \mathcal{Y}$,我们计算 +从这十个值中选择最大的两个,比如$P(A, B \mid \mathbf{c})$和$P(C, E \mid \mathbf{c})$。然后在时间步3,对于所有$y_3 \in \mathcal{Y}$,我们计算: $$\begin{aligned}P(A, B, y_3 \mid \mathbf{c}) = P(A, B \mid \mathbf{c})P(y_3 \mid A, B, \mathbf{c}),\\P(C, E, y_3 \mid \mathbf{c}) = P(C, E \mid \mathbf{c})P(y_3 \mid C, E, \mathbf{c}),\end{aligned}$$ -然后选择这十个值中最大的两个,比如 $P(A, B, D \mid \mathbf{c})$ 和 $P(C, E, D \mid \mathbf{c}).$ 因此,我们得到了六个候选输出序列:(i) $A$; (ii) $C$; (iii) 73229293617; (iv) 73229293617; (iv) 73229293614; (v) 73229293618, $E$; (v) $A$ 17,$D$; 以及 (六) $C$、$D$、$D$。 +然后从这十个值中选择最大的两个,即$P(A, B, D \mid \mathbf{c})$和$P(C, E, D \mid \mathbf{c}).$。结果,我们得到六个候选输出序列:(1)$A$;(2)$C$;(3)$B$;(4)$C$、$E$;(5)$A$、$B$、$D$以及(6)$C$、$D$。 -最后,我们获得基于这六个序列的最终候选输出序列集(例如,丢弃包括 “” 之后的部分)。然后我们选择以下分数最高的序列作为输出序列: +最后,我们基于这六个序列(例如,包括“<eos>”和之后的丢弃部分)获得最终候选输出序列集合。然后我们选择以下得分最高的序列作为输出序列: $$ \frac{1}{L^\alpha} \log P(y_1, \ldots, y_{L}) = \frac{1}{L^\alpha} \sum_{t'=1}^L \log P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}),$$ :eqlabel:`eq_beam-search-score` -其中 $L$ 是最终候选序列的长度,$\alpha$ 通常设置为 0.75。由于在 :eqref:`eq_beam-search-score` 的总和中,较长的序列具有更多的对数术语,分母中的术语 $L^\alpha$ 将处罚长序列。 +其中$L$是最终候选序列的长度,$\alpha$通常设置为0.75。因为一个较长的序列在:eqref:`eq_beam-search-score`的总和中有更多的对数项,分母中的$L^\alpha$惩罚长序列。 -光束搜索的计算成本为 $\mathcal{O}(k\left|\mathcal{Y}\right|T')$。这种结果介于贪婪搜索和详尽搜索的结果之间。事实上,贪婪搜索可以被视为波束大小为 1 的特殊类型的光束搜索。通过灵活选择光束尺寸,光束搜索可在准确性与计算成本之间进行权衡。 +束搜索的计算量为$\mathcal{O}(k\left|\mathcal{Y}\right|T')$。这个结果介于贪心搜索和穷举搜索之间。实际上,贪心搜索可以看作是一种特殊类型的束搜索,束宽为1。通过灵活选择束宽,束搜索可以在精度和计算成本之间进行权衡。 -## 摘要 +## 小结 -* 序列搜索策略包括贪婪搜索、详尽搜索和束搜索。 -* 光束搜索通过灵活选择光束尺寸,在准确性与计算成本之间进行权衡。 +* 序列搜索策略包括贪心搜索、穷举搜索和束搜索。 +* 束搜索通过灵活选择束宽,在精度和计算成本之间找到平衡。 ## 练习 -1. 我们可以将详尽搜索视为一种特殊类型的光束搜索吗?为什么或为什么不? -1. 在 :numref:`sec_seq2seq` 中的机器翻译问题中应用束搜索。光束大小如何影响翻译结果和预测速度? -1. 我们使用语言建模在 :numref:`sec_rnn_scratch` 中用户提供的前缀生成文本。它使用哪种搜索策略?你能改进吗? +1. 我们能把穷举搜索看作一种特殊的束搜索吗? +1. 在 :numref:`sec_seq2seq` 机器翻译问题中应用束搜索。束宽如何影响结果和预测速度? +1. 在 :numref:`sec_rnn_scratch` 中,我们使用语言模型来生成用户提供前缀的文本。它使用了哪种搜索策略?你能改进一下吗? [Discussions](https://discuss.d2l.ai/t/338) From 110133fe9aae50e03fdfe19483adbec8241d3cc2 Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Tue, 20 Apr 2021 18:50:27 +0200 Subject: [PATCH 055/103] translation issue in chapter_linear-networks/linear-regression (#752) * translation issue in chapter_linear-networks/linear-regression * first two remarks reviewed. --- chapter_linear-networks/linear-regression.md | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/chapter_linear-networks/linear-regression.md b/chapter_linear-networks/linear-regression.md index 0363693df..cc3ea673b 100644 --- a/chapter_linear-networks/linear-regression.md +++ b/chapter_linear-networks/linear-regression.md @@ -27,7 +27,7 @@ $$\mathrm{price} = w_{\mathrm{area}} \cdot \mathrm{area} + w_{\mathrm{age}} \cdo 给定一个数据集,我们的目标是寻找模型的权重 $\mathbf{w}$ 和偏置 $b$,使得根据模型做出的预测大体符合数据里的真实价格。输出的预测值由输入特征通过*线性模型*的仿射变换决定,仿射变换由所选权重和偏置确定。 -有些学科往往只关注有少量特征的数据集。在这些学科中,建模时经常过通过显式地长形式表达。而在机器学习领域,我们通常使用的是高维数据集,建模时采用线性代数表示法会比较方便。当我们的输入包含 $d$ 个特征时,我们将预测结果 $\hat{y}$(通常使用 “尖角” 符号表示估计值)表示为: +有些学科往往只关注有少量特征的数据集。在这些学科中,建模时经常像这样通过长形式显式地表达。而在机器学习领域,我们通常使用的是高维数据集,建模时采用线性代数表示法会比较方便。当我们的输入包含 $d$ 个特征时,我们将预测结果 $\hat{y}$(通常使用 “尖角” 符号表示估计值)表示为: $$\hat{y} = w_1 x_1 + ... + w_d x_d + b.$$ @@ -38,14 +38,14 @@ $$\hat{y} = \mathbf{w}^\top \mathbf{x} + b.$$ 在 :eqref:`eq_linreg-y` 中,向量 $\mathbf{x}$ 对应于单个数据样本的特征。用符号表示的矩阵 $\mathbf{X} \in \mathbb{R}^{n \times d}$ 可以很方便地引用我们整个数据集的 $n$ 个样本。其中,$\mathbf{X}$ 的每一行是一个样本,每一列是一种特征。 -对于特征集合 $\mathbf{X}$ 和预测值 $\hat{\mathbf{y}} \in \mathbb{R}^n$ 可以通过矩阵-向量乘法表示为: +对于特征集合 $\mathbf{X}$ ,预测值 $\hat{\mathbf{y}} \in \mathbb{R}^n$ 可以通过矩阵-向量乘法表示为: $${\hat{\mathbf{y}}} = \mathbf{X} \mathbf{w} + b$$ -这个过程的求和过程将使用广播机制。广播机制会在 :numref:`subsec_broadcasting`)详细介绍。 +这个过程中的求和将使用广播机制(广播机制在 :numref:`subsec_broadcasting` 中有详细介绍)。 给定训练数据特征 $\mathbf{X}$ 和对应的已知标签 $\mathbf{y}$ ,线性回归的目标是找到一组权重向量 $\mathbf{w}$ 和偏置 $b$。当给定从$\mathbf{X}$的同分布中取样的新样本特征时,找到的权重向量和偏置能够使得新样本预测标签的误差尽可能小。 -虽然我们相信给定 $\mathbf{x}$ 预测 $y$ 的最佳模型会是线性的。但我们很难找到找到一个有$n$个样本的真实数据集,其中 $y^{(i)}$ 完全等于 $\mathbf{w}^\top \mathbf{x}^{(i)}+b$。无论我们使用什么手段来观察特征 $\mathbf{X}$ 和标签 $\mathbf{y}$ ,都可能会出现少量的观测误差。因此,即使确信特征与标签的潜在关系是线性的,我们也会加入一个噪声项来考虑观测误差带来的影响。 +虽然我们相信给定 $\mathbf{x}$ 预测 $y$ 的最佳模型会是线性的,但我们很难找到一个有$n$个样本的真实数据集,其中对于所有的 $1 \leq i \leq n$, $y^{(i)}$ 完全等于 $\mathbf{w}^\top \mathbf{x}^{(i)}+b$。无论我们使用什么手段来观察特征 $\mathbf{X}$ 和标签 $\mathbf{y}$ ,都可能会出现少量的观测误差。因此,即使确信特征与标签的潜在关系是线性的,我们也会加入一个噪声项来考虑观测误差带来的影响。 在我们开始寻找最好的 *模型参数*(model parameters)$\mathbf{w}$ 和 $b$ 之前,我们还需要两个东西:(1)一种模型质量的度量方式;(2)一种能够更新模型以提高模型预测质量的方法。 @@ -55,7 +55,7 @@ $${\hat{\mathbf{y}}} = \mathbf{X} \mathbf{w} + b$$ $$l^{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y^{(i)}\right)^2.$$ -常数$\frac{1}{2}$不会带来本质的差别,但这样在形式上稍微简单一些。当我们对损失函数求导后常数系数为1。由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。为了进一步说明,来看下面的例子。我们为一维情况下的回归问题绘制图像,如 :numref:`fig_fit_linreg` 所示。 +常数$\frac{1}{2}$不会带来本质的差别,但这样在形式上稍微简单一些,表现为当我们对损失函数求导后常数系数为1。由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。为了进一步说明,来看下面的例子。我们为一维情况下的回归问题绘制图像,如 :numref:`fig_fit_linreg` 所示。 ![用线性模型拟合数据。](../img/fit-linreg.svg) :label:`fig_fit_linreg` @@ -80,9 +80,9 @@ $$\mathbf{w}^* = (\mathbf X^\top \mathbf X)^{-1}\mathbf X^\top \mathbf{y}.$$ 即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。在许多任务上,那些难以优化的模型效果要更好。因此,弄清楚如何训练这些难以优化的模型是非常重要的。 -本书中我们用到一种名为*梯度下降*(gradient descent)的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在降低损失的方向上更新参数来降低误差。 +本书中我们用到一种名为*梯度下降*(gradient descent)的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差。 -梯度下降最简单的用法是计算数据集中所有样本的损失关于模型参数的导数(在这里也可以称为梯度)。但实际中的执行可能会比较慢:因为在进行一次更新之前,我们必须遍历整个数据集。因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本,这种变体叫做*小批量随机梯度下降*(minibatch stochastic gradient descent)。 +梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值)关于模型参数的导数(在这里也可以称为梯度)。但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本,这种变体叫做*小批量随机梯度下降*(minibatch stochastic gradient descent)。 在每次迭代中,我们首先随机抽样一个小批量$\mathcal{B}$,它是由固定数量的训练样本组成的。然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。最后,我们将梯度乘以一个预先确定的正数$\eta$,并从当前参数的值中减掉。 @@ -90,7 +90,7 @@ $$\mathbf{w}^* = (\mathbf X^\top \mathbf X)^{-1}\mathbf X^\top \mathbf{y}.$$ $$(\mathbf{w},b) \leftarrow (\mathbf{w},b) - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\mathbf{w},b)} l^{(i)}(\mathbf{w},b).$$ -总结一下,算法的步骤如下:(1)初始化模型参数的值,如随机初始化;(2)从数据中迭代抽取随机的小批量样本。然后在负梯度的方向上更新参数。对于平方损失和仿射变换,我们可以明确地写成如下形式: +总结一下,算法的步骤如下:(1)初始化模型参数的值,如随机初始化;(2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。对于平方损失和仿射变换,我们可以明确地写成如下形式: $$\begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b) = \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b) = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned}$$ :eqlabel:`eq_linreg_batch_update` @@ -99,9 +99,9 @@ $$\begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B} $|\mathcal{B}|$ 表示每个小批量中的样本数,这也称为*批量大小*(batch size)。$\eta$ 表示 *学习率*(learning rate)。批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的。这些可以调整但不在训练过程中更新的参数称为 *超参数*(hyperparameter)。 *调参*(hyperparameter tuning) 是选择超参数的过程。超参数通常是我们根据训练迭代结果来调整的,而训练迭代结果是在独立的*验证数据集*(validation dataset)上评估得到的。 -在训练了预先确定的若干迭代次数后(或者直到满足某些其他停止条件后),我们记录估计的模型参数,表示为$\hat{\mathbf{w}}, \hat{b}$。但是,即使我们的函数真是线性的且无噪声。我们估计得到的参数也不会是损失的精确最小值。因为算法会使得损失向最小值缓慢收敛,但不能在有限的步数内非常精确地达到最小值。 +在训练了预先确定的若干迭代次数后(或者直到满足某些其他停止条件后),我们记录下模型参数的估计值,表示为$\hat{\mathbf{w}}, \hat{b}$。但是,即使我们的函数确实是线性的且无噪声,这些估计值也不会使损失函数真正地达到最小值。因为算法会使得损失向最小值缓慢收敛,但却不能在有限的步数内非常精确地达到最小值。 -线性回归恰好是一个在整个域中只有一个最小值的学习问题。但是对于像深度神经网络这样复杂的模型来说,损失平面上通常包含许多个最小值。幸运的是,深度学习实践者很少努力寻找能够将 *训练集* 损失最小化的参数,虽然这么做原因尚未被完全理解。事实上,更难做到的是找到一组参数,这组参数能够在我们从未见过的数据上实现低的损失,这一挑战被称为 *泛化*(generalization)。 +线性回归恰好是一个在整个域中只有一个最小值的学习问题。但是对于像深度神经网络这样复杂的模型来说,损失平面上通常包含多个最小值。幸运的是,出于某种原因,深度学习实践者很少会去花费大力气寻找这样一组参数,使得在 *训练集* 上的损失达到最小。事实上,更难做到的是找到一组参数,这组参数能够在我们从未见过的数据上实现较低的损失,这一挑战被称为 *泛化*(generalization)。 ### 用学习到的模型进行预测 @@ -219,7 +219,7 @@ f'{timer.stop():.5f} sec' ## 正态分布与平方损失 :label:`subsec_normal_distribution_and_squared_loss` -接下来,我们通过对噪声分布的假设来解读平方损失目标。 +接下来,我们通过对噪声分布的假设来解读平方损失目标函数。 正态分布(normal distribution),也称为 *高斯分布*(Gaussian distribution),最早由德国数学家高斯(Gauss)应用于天文学研究。 正态分布和线性回归之间的关系很密切。 @@ -252,7 +252,7 @@ d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x', 就像我们所看到的,改变均值会产生沿 $x$ 轴的偏移,增加方差将会分散分布、降低其峰值。 -利用均方误差损失函数(简称均方损失)可以用于线性回归的一个原因是:假设观测中包含噪声,其中噪声服从正态分布。噪声正态分布如下式: +均方误差损失函数(简称均方损失)可以用于线性回归的一个原因是:我们假设了观测中包含噪声,其中噪声服从正态分布。噪声正态分布如下式: $$y = \mathbf{w}^\top \mathbf{x} + b + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, \sigma^2).$$ @@ -276,7 +276,7 @@ $$-\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma ## 从线性回归到深度网络 到目前为止,我们只谈论了线性模型。 -当神经网络涵盖了更多更为丰富的模型时,我们可以通过用神经网络的语言来表达线性模型,从而开始把线性模型看作一个神经网络。 +尽管神经网络涵盖了更多更为丰富的模型,我们依然可以用描述神经网络的方式来描述线性模型,从而把线性模型看作一个神经网络。 首先,让我们用“层”符号来重写这个模型。 ### 神经网络图 @@ -306,7 +306,7 @@ $$-\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma 当然,许多这样的单元可以通过正确连接和正确的学习算法拼凑在一起,从而产生的行为会比单独一个神经元所产生的行为更有趣、更复杂,这种想法归功于我们对真实生物神经系统的研究。 当今大多数深度学习的研究几乎没有直接从神经科学中获得灵感。我们援引斯图尔特·罗素和彼得·诺维格谁,在他们的经典人工智能教科书 -*Artificial Intelligence: A Modern Approach* :cite:`Russell.Norvig.2016`, +*Artificial Intelligence: A Modern Approach* :cite:`Russell.Norvig.2016` 中所说:虽然飞机可能受到鸟类的启发。但几个世纪以来,鸟类学并不是航空创新的主要驱动力。同样地,如今在深度学习中的灵感同样或更多地来自数学、统计学和计算机科学。 ## 小结 From b4522da9b540f018008281633e930d72dbf7d19e Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Wed, 21 Apr 2021 01:18:20 +0800 Subject: [PATCH 056/103] chapter_recurrent-modern/seq2seq (#729) * chapter_recurrent-modern/seq2seq * Update seq2seq.md * Update seq2seq.md * Update seq2seq.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/seq2seq.md | 205 ++++++++++++++-------------- 1 file changed, 101 insertions(+), 104 deletions(-) diff --git a/chapter_recurrent-modern/seq2seq.md b/chapter_recurrent-modern/seq2seq.md index 5e8344a08..58e01ca8f 100644 --- a/chapter_recurrent-modern/seq2seq.md +++ b/chapter_recurrent-modern/seq2seq.md @@ -1,16 +1,17 @@ -# 顺序到序列学习 +# 序列到序列学习(seq2seq) :label:`sec_seq2seq` -正如我们在 :numref:`sec_machine_translation` 中看到的那样,在机器翻译中,输入和输出都是一个可变长度的序列。为了解决这类问题,我们在 :numref:`sec_encoder-decoder` 中设计了一种通用的编码器解码器架构。在本节中,我们将使用两类 RNN 来设计此架构的编码器和解码器,并将其应用于机器翻译 :cite:`Sutskever.Vinyals.Le.2014,Cho.Van-Merrienboer.Gulcehre.ea.2014` 的 **序列到序列**(sequence to sequence) 学习。 +正如我们在 :numref:`sec_machine_translation` 中看到的。在机器翻译中,输入和输出都是可变长度的序列。为了解决这类问题,我们在 :numref:`sec_encoder-decoder` 中设计了一个通用的编码器-解码器结构。在本节中,我们将使用两个循环神经网络来设计此编码器-解码器结构,并将其应用于机器翻译 :cite:`Sutskever.Vinyals.Le.2014,Cho.Van-Merrienboer.Gulcehre.ea.2014` 的*序列到序列*(sequence to sequence)学习。 -遵循编码器解码器架构的设计原理,RNN 编码器可以采用可变长度序列作为输入,并将其转换为固定形状的隐藏状态。换句话说,输入(源)序列的信息被编码 RNN 编码器的隐藏状态。要通过令牌生成输出序列令牌,单独的 RNN 解码器可以根据已看到的(例如在语言建模中)或生成的令牌以及输入序列的编码信息来预测下一个令牌。:numref:`fig_seq2seq` 说明了如何使用两个 RNN 进行序列顺序学习机器翻译。 +循环神经网络编码器遵循编码器-解码器结构的设计原则,以可变长度序列作为输入,将其转换为固定形状的隐藏状态。换言之,输入(源)序列的信息在循环神经网络编码器的隐藏状态下被*编码*。为了逐个标记地生成输出序列标记,单独的循环神经网络解码器可以基于已经看到的标记(例如在语言模型任务中)或生成的标记以及输入序列的编码信息来预测下一标记。 :numref:`fig_seq2seq` 演示了如何在机器翻译中使用两个循环神经网络进行序列到序列学习。 -![Sequence to sequence learning with an RNN encoder and an RNN decoder.](../img/seq2seq.svg) +![使用循环神经网络编码器和循环神经网络解码器的序列到序列学习。](../img/seq2seq.svg) :label:`fig_seq2seq` -在 :numref:`fig_seq2seq` 中,特殊的 “<eos>” 令牌标志着序列的结束。生成此令牌后,模型可以停止进行预测。在 RNN 解码器的初始时间步,有两个特殊的设计决策。首先,特殊的序列开始 “<bos>” 令牌是输入。其次,RNN 编码器的最终隐藏状态用于启动解码器的隐藏状态。在 :cite:`Sutskever.Vinyals.Le.2014` 等设计中,这正是将编码输入序列信息输入解码器以生成输出(目标)序列的方式。在其他一些设计中,如 :cite:`Cho.Van-Merrienboer.Gulcehre.ea.2014`,编码器的最终隐藏状态也会作为每个时间步的输入的一部分送入解码器,如 :numref:`fig_seq2seq` 所示。与 :numref:`sec_language_model` 中的语言模型训练类似,我们可以允许标签成为原始输出序列,由一个标记移动:“<bos>”、“Ils”、“regardent”、“.” $\rightarrow$ “Ils”,“regardent”,“.”, “<eos>”。 +在 :numref:`fig_seq2seq` 中,特殊的“<eos>”标记表示序列的结束。一旦生成此标记,模型就可以停止进行预测。在循环神经网络解码器的初始时间步,有两个特殊的设计决策。首先,特殊序列开始标记“<bos>”是第一个输入。其次,使用循环神经网络编码器的最终隐藏状态来启动解码器的隐藏状态。在如 :cite:`Sutskever.Vinyals.Le.2014` 的设计中,编码器的最终隐藏状态也作为在每个时间步长的输入的一部分馈送到解码器中,如 :numref:`fig_seq2seq` 所示。类似于 :numref:`sec_language_model` 中的语言模型训练,我们可以允许标签是原始的输出序列,移位一个标“<bos>”、“Ils”、“regardent”、“.” $\rightarrow$ +“Ils”、“regardent”、“.”、“<eos>”。 -在下面,我们将更详细地解释 :numref:`fig_seq2seq` 的设计。我们将在 :numref:`sec_machine_translation` 中引入的英法数据集上训练这种模型,用于机器翻译。 +下面,我们将对 :numref:`fig_seq2seq` 的设计进行更详细的说明。我们将在 :numref:`sec_machine_translation` 中介绍的英-法数据集上训练这个机器翻译模型。 ```{.python .input} import collections @@ -32,21 +33,21 @@ from torch import nn ## 编码器 -从技术上讲,编码器将可变长度的输入序列转换为固定形状的 * 上下文变量 * $\mathbf{c}$,并在此上下文变量中对输入序列信息进行编码。如 :numref:`fig_seq2seq` 所示,我们可以使用 RNN 来设计编码器。 +从技术上讲,编码器将可变长度的输入序列转换成固定形状的*上下文变量*$\mathbf{c}$,并且在该上下文变量中编码输入序列信息。如:numref:`fig_seq2seq`所示,我们可以使用循环神经网络来设计编码器。 -让我们考虑一个序列示例(批量大小:1)。假设输入序列是 $x_1, \ldots, x_T$,因此 $x_t$ 是输入文本序列中的 $t^{\mathrm{th}}$ 标记。在时间步骤 $t$ 中,RNN 将 $x_t$ 的输入要素矢量 $\mathbf{x}_t$ 和从上一个时间步的隐藏状态 $\mathbf{h} _{t-1}$ 转换为当前隐藏状态 $\mathbf{h}_t$。我们可以使用函数 $f$ 来表达 RNN 循环层的转换: +让我们考虑一个序列样本(批量大小:1)。假设输入序列是$x_1, \ldots, x_T$,其中$x_t$是输入文本序列中的第$t$个标记。在时间步$t$,循环神经网络将用于$x_t$的输入特征向量$\mathbf{x}_t$和来自上一时间步的隐藏状态$\mathbf{h} _{t-1}$转换为当前隐藏状态$\mathbf{h}_t$。我们可以用一个函数$f$来表示循环神经网络层所做的变换: $$\mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}). $$ -一般来说,编码器通过自定义函数 $q$ 将隐藏状态转换为上下文变量: +通常,编码器通过定制函数$q$将所有时间步的隐藏状态转换为上下文变量: $$\mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T).$$ -例如,当选择 $q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T$(如 :numref:`fig_seq2seq`)时,上下文变量只是输入序列在最后一个时间步的隐藏状态 $\mathbf{h}_T$。 +例如,当选择$q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T$时(例如在:numref:`fig_seq2seq`中),上下文变量仅仅是输入序列在最后时间步的隐藏状态$\mathbf{h}_T$。 -到目前为止,我们使用了单向 RNN 来设计编码器,其中隐藏状态只取决于隐藏状态时间步长和之前的输入子序列。我们还可以使用双向 RNN 构建编码器。在这种情况下,隐藏状态取决于时间步长之前和之后的子序列(包括当前时间步长的输入),后者对整个序列的信息进行编码。 +到目前为止,我们已经使用了一个单向循环神经网络来设计编码器,其中隐藏状态只依赖于隐藏状态的时间步处和之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,隐藏状态取决于时间步前后的子序列(包括当前时间步处的输入),该子序列对整个序列的信息进行编码。 -现在让我们实施 RNN 编码器。请注意,我们使用 * 嵌入层 * 来获取输入序列中每个令牌的特征矢量。嵌入图层的权重是一个矩阵,其行数等于输入词汇表的大小 (`vocab_size`),列数等于要素矢量的维度 (`embed_size`)。对于任何输入令牌索引 $i$,嵌入层将获取权重矩阵的 $i^{\mathrm{th}}$ 行(从 0 开始)以返回其要素矢量。此外,我们在这里选择一个多层 GRU 来实现编码器。 +现在让我们实现循环神经网络编码器。注意,我们使用*嵌入层*(embedding layer)来获得输入序列中每个标记的特征向量。嵌入层的权重是一个矩阵,其行数等于输入词表的大小(`vocab_size`),列数等于特征向量的维数(`embed_size`)。对于任何输入标记索引$i$,嵌入层获取权重矩阵的第$i$行(从0开始)以返回其特征向量。另外,本文选择了一个多层门控循环单元来实现编码器。 ```{.python .input} #@save @@ -55,19 +56,19 @@ class Seq2SeqEncoder(d2l.Encoder): def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) - # Embedding layer + # 嵌入层 self.embedding = nn.Embedding(vocab_size, embed_size) self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout) def forward(self, X, *args): - # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) + # 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) - # In RNN models, the first axis corresponds to time steps + # 在循环神经网络模型中,第一个轴对应于时间步长 X = X.swapaxes(0, 1) state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx) output, state = self.rnn(X, state) - # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) - # `state[0]` shape: (`num_layers`, `batch_size`, `num_hiddens`) + # `output`的形状: (`num_steps`, `batch_size`, `num_hiddens`) + # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`) return output, state ``` @@ -79,24 +80,24 @@ class Seq2SeqEncoder(d2l.Encoder): def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) - # Embedding layer + # 嵌入层 self.embedding = nn.Embedding(vocab_size, embed_size) self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout) def forward(self, X, *args): - # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`) + # 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) - # In RNN models, the first axis corresponds to time steps + # 在循环神经网络模型中,第一个轴对应于时间步长 X = X.permute(1, 0, 2) - # When state is not mentioned, it defaults to zeros + # 如果未提及状态,则默认为0 output, state = self.rnn(X) - # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`) - # `state` shape: (`num_layers`, `batch_size`, `num_hiddens`) + # `output`维度: (`num_steps`, `batch_size`, `num_hiddens`) + # `state[0]`维度: (`num_layers`, `batch_size`, `num_hiddens`) return output, state ``` -:numref:`sec_rnn-concise` 中已对循环层的返回变量进行了解释。让我们仍然用一个具体的例子来说明上述编码器的实现。下面我们实例化一个双层 GRU 编码器,其隐藏单位数为 16 个。鉴于小批量序列输入 `X`(批量大小:4,时间步长数:7),所有时间步长中最后一层的隐藏状态(编码器的循环层返回 `output`)是形状的张量(时间步长数、批量大小、隐藏单位数)。 +循环神经网络层的返回变量已在 :numref:`sec_rnn-concise` 中解释。让我们仍然使用一个具体的例子来说明上述编码器实现。下面我们将实例化一个隐藏单元数为16的两层门控循环单元编码器。给定一小批量序列输入`X`(批量大小:4,时间步:7),所有时间步最后一层的隐藏状态(`output`由编码器的循环神经网络层返回)是形状为(时间步数, 批大小, 隐藏单元数)的张量。 ```{.python .input} encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -117,7 +118,7 @@ output, state = encoder(X) output.shape ``` -由于此处使用 GRU,因此最后一个时间步长的多层隐藏状态的形状为(隐藏层数、批量大小、隐藏单位数量)。如果使用 LSTM,则记忆单元信息也将包含在 `state` 中。 +由于这里使用门控循环单元,所以在最后一个时间步的多层隐藏状态的形状是(隐藏层的数量, 批量大小, 隐藏单元的数量)。如果使用长短期记忆网络,`state`中还将包含记忆单元信息。 ```{.python .input} len(state), state[0].shape @@ -131,20 +132,20 @@ state.shape ## 解码器 :label:`sec_seq2seq_decoder` -正如我们刚才提到的,编码器输出的上下文变量 $\mathbf{c}$ 对整个输入序列 $x_1, \ldots, x_T$ 进行编码。鉴于训练数据集的输出序列 $y_1, y_2, \ldots, y_{T'}$,对于每个时间步长 $t'$(符号不同于输入序列或编码器的时间步长 $t$),解码器输出 $y_{t'}$ 的概率取决于之前的输出子序列 $y_1, \ldots, y_{t'-1}$ 和上下文变量$\mathbf{c}$,即 $P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$。 +正如我们刚才提到的,编码器输出的上下文变量$\mathbf{c}$对整个输入序列$x_1, \ldots, x_T$进行编码。给定来自训练数据集的输出序列$y_1, y_2, \ldots, y_{T'}$,对于每个时间步$t'$(与输入序列或编码器的时间步$t$不同),解码器输出$y_{t'}$的概率取决于先前的输出子序列$y_1, \ldots, y_{t'-1}$和上下文变量$\mathbf{c}$,即$P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$。 -要对序列进行这种条件概率建模,我们可以使用另一个 RNN 作为解码器。在输出序列的任何时间步骤 $t^\prime$,RNN 将上一个时间步的输出 $y_{t^\prime-1}$ 和上下文变量 $\mathbf{c}$ 作为输入,然后将它们和之前的隐藏状态 $\mathbf{s}_{t^\prime-1}$ 转换为当前时间步长的隐藏状态 $\mathbf{s}_{t^\prime}$。因此,我们可以使用函数 $g$ 来表达解码器隐藏层的转换: +为了在序列上模拟这种条件概率,我们可以使用另一个循环神经网络作为解码器。在输出序列上的任何时间步$t^\prime$,循环神经网络将来自上一时间步的输出$y_{t^\prime-1}$和上下文变量$\mathbf{c}$作为其输入,然后在当前时间步将它们和上一隐藏状态$\mathbf{s}_{t^\prime-1}$转换为隐藏状态$\mathbf{s}_{t^\prime}$。因此,我们可以使用函数$g$来表示解码器的隐藏层的变换: $$\mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-1}).$$ :eqlabel:`eq_seq2seq_s_t` -获得解码器的隐藏状态后,我们可以使用输出层和 softmax 操作来计算时间步骤 $t^\prime$ 时输出的条件概率分布 $P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})$。 +在获得解码器的隐藏状态之后,我们可以使用输出层和softmax操作来计算时间步$t^\prime$处的输出的条件概率分布$P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})$。 -在 :numref:`fig_seq2seq` 之后,在按如下方式实现解码器时,我们直接使用编码器最后一个时间步的隐藏状态来初始化解码器的隐藏状态。这要求 RNN 编码器和 RNN 解码器具有相同数量的层和隐藏单位。为了进一步合并编码的输入序列信息,上下文变量会在所有时间步长与解码器输入连接起来。为了预测输出令牌的概率分布,使用完全连接的层来转换 RNN 解码器最后一层的隐藏状态。 +根据 :numref:`fig_seq2seq`,当实现解码器时,我们直接使用编码器最后一个时间步的隐藏状态来初始化解码器的隐藏状态。这要求循环神经网络编码器和循环神经网络解码器具有相同数量的层和隐藏单元。为了进一步合并经编码的输入序列信息,上下文变量在所有时间步处与解码器输入串联。为了预测输出标记的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐藏状态。 ```{.python .input} class Seq2SeqDecoder(d2l.Decoder): - """The RNN decoder for sequence to sequence learning.""" + """用于序列到序列学习的循环神经网络解码器。""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqDecoder, self).__init__(**kwargs) @@ -156,18 +157,18 @@ class Seq2SeqDecoder(d2l.Decoder): return enc_outputs[1] def forward(self, X, state): - # The output `X` shape: (`num_steps`, `batch_size`, `embed_size`) + # 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X).swapaxes(0, 1) - # `context` shape: (`batch_size`, `num_hiddens`) + # `context` 的形状: (`batch_size`, `num_hiddens`) context = state[0][-1] - # Broadcast `context` so it has the same `num_steps` as `X` + # 广播 `context`,使其具有与`X`相同的 `num_steps` context = np.broadcast_to(context, ( X.shape[0], context.shape[0], context.shape[1])) X_and_context = d2l.concat((X, context), 2) output, state = self.rnn(X_and_context, state) output = self.dense(output).swapaxes(0, 1) - # `output` shape: (`batch_size`, `num_steps`, `vocab_size`) - # `state[0]` shape: (`num_layers`, `batch_size`, `num_hiddens`) + # `output`的形状: (`batch_size`, `num_steps`, `vocab_size`) + # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`) return output, state ``` @@ -187,19 +188,19 @@ class Seq2SeqDecoder(d2l.Decoder): return enc_outputs[1] def forward(self, X, state): - # The output `X` shape: (`num_steps`, `batch_size`, `embed_size`) + # 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X).permute(1, 0, 2) - # Broadcast `context` so it has the same `num_steps` as `X` + # 广播 `context`,使其具有与`X`相同的 `num_steps` context = state[-1].repeat(X.shape[0], 1, 1) X_and_context = d2l.concat((X, context), 2) output, state = self.rnn(X_and_context, state) output = self.dense(output).permute(1, 0, 2) - # `output` shape: (`batch_size`, `num_steps`, `vocab_size`) - # `state` shape: (`num_layers`, `batch_size`, `num_hiddens`) + # `output`的形状: (`batch_size`, `num_steps`, `vocab_size`) + # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`) return output, state ``` -为了说明实现的解码器,下面我们使用上述编码器中的相同超参数对其进行实例化。正如我们所看到的,解码器的输出形状变为(批量大小、时间步长数、词汇量),张量的最后一个维度存储预测的令牌分布。 +为了说明实现的解码器,下面我们用前面提到的编码器中相同的超参数来实例化它。解码器的输出形状变为(批量大小, 时间步数, 词表大小),其中张量的最后一个维度存储预测的标记分布。 ```{.python .input} decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -220,16 +221,16 @@ output, state = decoder(X, state) output.shape, state.shape ``` -总而言之,上面的 RNN 编码器-解码器模型中的各层在 :numref:`fig_seq2seq_details` 中进行了说明。 +总之,上述循环神经网络编码器-解码器模型中的各层如 :numref:`fig_seq2seq_details` 所示。 -![Layers in an RNN encoder-decoder model.](../img/seq2seq-details.svg) +![循环神经网络编码器-解码器模型中的层。](../img/seq2seq-details.svg) :label:`fig_seq2seq_details` ## 损失函数 -在每个时间步骤中,解码器都会预测输出令牌的概率分布。与语言建模类似,我们可以应用 softmax 来获取分布并计算交叉熵损失以进行优化。回想一下 :numref:`sec_machine_translation`,特殊的填充令牌被附加到序列的末尾,因此可以将不同长度的序列高效地装入相同形状的小批次。但是,对填充令牌的预测应该从损失计算中排除。 +在每个时间步,解码器预测输出令牌的概率分布。类似于语言模型,我们可以使用softmax来获得分布,并计算交叉熵损失进行优化。回想一下 :numref:`sec_machine_translation` ,特殊的填充标记被附加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,应该将填充令牌的预测排除在损失计算之外。 -为此,我们可以使用以下 `sequence_mask` 函数以零值掩盖无关条目,以便稍后将任何不相关的预测乘以零等于零。例如,如果不包括填充标记的两个序列的有效长度分别为 1 和 2,则第一个和前两个条目之后的剩余条目将被清除为零。 +为此,我们可以使用下面的`sequence_mask`函数用零值屏蔽不相关的项,以便以后任何不相关的预测与零的乘积等于零。例如,如果两个序列(不包括填充标记)的有效长度分别为1和2,则第一项和前两项之后的剩余项将被清除为零。 ```{.python .input} X = np.array([[1, 2, 3], [4, 5, 6]]) @@ -240,7 +241,7 @@ npx.sequence_mask(X, np.array([1, 2]), True, axis=1) #@tab pytorch #@save def sequence_mask(X, valid_len, value=0): - """Mask irrelevant entries in sequences.""" + """在序列中屏蔽不相关的项。""" maxlen = X.size(1) mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:, None] @@ -251,7 +252,7 @@ X = torch.tensor([[1, 2, 3], [4, 5, 6]]) sequence_mask(X, torch.tensor([1, 2])) ``` -我们还可以掩盖最后几个轴上的所有条目。如果你愿意,你甚至可以指定用非零值替换这些条目。 +我们还可以屏蔽最后几个轴上的所有项。如果愿意,也可以指定用非零值替换这些条目。 ```{.python .input} X = d2l.ones((2, 3, 4)) @@ -264,17 +265,17 @@ X = d2l.ones(2, 3, 4) sequence_mask(X, torch.tensor([1, 2]), value=-1) ``` -现在我们可以延长 softmax 交叉熵损失,以允许掩盖不相关的预测。最初,所有预测令牌的掩码都设置为一个。一旦给出了有效长度,与任何填充令牌对应的掩码将被清除为零。最后,所有代币的损失将乘以掩码,以过滤掉对损失中填充令牌的无关预测。 +现在我们可以扩展softmax交叉熵损失来遮蔽不相关的预测。最初,所有预测标记的掩码都设置为1。一旦给定了有效长度,与填充标记对应的掩码将被设置为0。最后,将所有标记的损失乘以掩码,以过滤掉损失中填充标记的不相关预测。 ```{.python .input} #@save class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): """The softmax cross-entropy loss with masks.""" - # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) - # `label` shape: (`batch_size`, `num_steps`) - # `valid_len` shape: (`batch_size`,) + # `pred` 的形状:(`batch_size`, `num_steps`, `vocab_size`) + # `label` 的形状:(`batch_size`, `num_steps`) + # `valid_len` 的形状:(`batch_size`,) def forward(self, pred, label, valid_len): - # `weights` shape: (`batch_size`, `num_steps`, 1) + # `weights` 的形状:(`batch_size`, `num_steps`, 1) weights = np.expand_dims(np.ones_like(label), axis=-1) weights = npx.sequence_mask(weights, valid_len, True, axis=1) return super(MaskedSoftmaxCELoss, self).forward(pred, label, weights) @@ -285,9 +286,9 @@ class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): #@save class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): """The softmax cross-entropy loss with masks.""" - # `pred` shape: (`batch_size`, `num_steps`, `vocab_size`) - # `label` shape: (`batch_size`, `num_steps`) - # `valid_len` shape: (`batch_size`,) + # `pred` 的形状:(`batch_size`, `num_steps`, `vocab_size`) + # `label` 的形状:(`batch_size`, `num_steps`) + # `valid_len` 的形状:(`batch_size`,) def forward(self, pred, label, valid_len): weights = torch.ones_like(label) weights = sequence_mask(weights, valid_len) @@ -298,7 +299,7 @@ class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): return weighted_loss ``` -为了进行健全性检查,我们可以创建三个相同的序列。然后我们可以指定这些序列的有效长度分别为 4、2 和 0。因此,第一个序列的损失应该是第二个序列的两倍,而第三个序列的损失应为零损失。 +对于健全性检查,我们可以创建三个相同的序列。然后我们可以指定这些序列的有效长度分别为4、2和0。因此,第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。 ```{.python .input} loss = MaskedSoftmaxCELoss() @@ -312,15 +313,15 @@ loss(d2l.ones(3, 4, 10), d2l.ones((3, 4), dtype=torch.long), torch.tensor([4, 2, 0])) ``` -## 培训 +## 训练 :label:`sec_seq2seq_training` -在接下来的训练循环中,我们连接了特殊的序列开始令牌和原始输出序列,不包括最终令牌作为解码器的输入,如 :numref:`fig_seq2seq` 所示。这被称为 *教师forcing*,因为原始输出序列(令牌标签)被输入到解码器中。或者,我们还可以将上一个时间步中的 *预测ted* 令牌作为解码器的当前输入。 +在下面的训练代码实现中,我们将特殊的序列开始标记和原始输出序列(不包括序列结束标记)连结,作为解码器的输入,如 :numref:`fig_seq2seq` 所示。这被称为“教师强制”(teacher forcing),因为原始输出序列(标记标签)被送入解码器。或者,我们也可以将来自上一时间步的*预测*得到的标记作为当前输入送到解码器。 ```{.python .input} #@save def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """Train a model for sequence to sequence.""" + """训练序列到序列模型。""" net.initialize(init.Xavier(), force_reinit=True, ctx=device) trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr}) @@ -329,13 +330,13 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + metric = d2l.Accumulator(2) # 训练损失总和,标记数量 for batch in data_iter: X, X_valid_len, Y, Y_valid_len = [ x.as_in_ctx(device) for x in batch] bos = np.array( [tgt_vocab['']] * Y.shape[0], ctx=device).reshape(-1, 1) - dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing + dec_input = d2l.concat([bos, Y[:, :-1]], 1) # 教师强制 with autograd.record(): Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) @@ -354,7 +355,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): #@tab pytorch #@save def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): - """Train a model for sequence to sequence.""" + """训练序列到序列模型。""" def xavier_init_weights(m): if type(m) == nn.Linear: nn.init.xavier_uniform_(m.weight) @@ -371,15 +372,15 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() - metric = d2l.Accumulator(2) # Sum of training loss, no. of tokens + metric = d2l.Accumulator(2) # 训练损失总和,标记数量 for batch in data_iter: X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch] bos = torch.tensor([tgt_vocab['']] * Y.shape[0], device=device).reshape(-1, 1) - dec_input = d2l.concat([bos, Y[:, :-1]], 1) # Teacher forcing + dec_input = d2l.concat([bos, Y[:, :-1]], 1) # 教师强制 Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) - l.sum().backward() # Make the loss scalar for `backward` + l.sum().backward() d2l.grad_clipping(net, 1) num_tokens = Y_valid_len.sum() optimizer.step() @@ -391,7 +392,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): f'tokens/sec on {str(device)}') ``` -现在我们可以创建和训练 RNN 编码器解码器模型,以便在机器翻译数据集上进行序列到序列学习。 +现在在机器翻译数据集上,我们可以创建和训练一个循环神经网络编码器-解码器模型,用于序列到序列的学习。 ```{.python .input} #@tab all @@ -410,40 +411,38 @@ train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ## 预测 -要通过令牌预测输出序列令牌,在每个解码器时间步长,上一个时间步的预测令牌作为输入输入进入解码器。与训练类似,在初始时间步骤,序列开始(” “)令牌被输入到解码器中。:numref:`fig_seq2seq_predict` 中说明了这一预测过程。当预测序列结束 (” “) 令牌时,输出序列的预测将完成。 +为了逐个标记地预测输出序列标记,在每个解码器时间步处,将来自前一时间步的预测标记作为输入送入解码器。与训练类似,在初始时间步,序列开始标记(“<bos>”)被馈送到解码器。该预测过程如:numref:`fig_seq2seq_predict`所示。当序列结束标记(“<eos>”)被预测时,输出序列的预测就完成了。 -![Predicting the output sequence token by token using an RNN encoder-decoder.](../img/seq2seq-predict.svg) +![使用循环神经网络编码器-解码器逐标记地预测输出序列。](../img/seq2seq-predict.svg) :label:`fig_seq2seq_predict` -我们将在 :numref:`sec_beam-search` 中引入不同的序列生成策略。 +我们将在 :numref:`sec_beam-search` 中介绍不同的序列生成策略。 ```{.python .input} #@save def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=False): - """Predict for sequence to sequence.""" + """序列到序列模型的预测""" src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ src_vocab['']] enc_valid_len = np.array([len(src_tokens)], ctx=device) src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # Add the batch axis + # 添加批量轴 enc_X = np.expand_dims(np.array(src_tokens, ctx=device), axis=0) enc_outputs = net.encoder(enc_X, enc_valid_len) dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # Add the batch axis + # 添加批量轴 dec_X = np.expand_dims(np.array([tgt_vocab['']], ctx=device), axis=0) output_seq, attention_weight_seq = [], [] for _ in range(num_steps): Y, dec_state = net.decoder(dec_X, dec_state) - # We use the token with the highest prediction likelihood as the input - # of the decoder at the next time step + # 我们使用具有预测最高可能性的标记,作为解码器在下一时间步的输入 dec_X = Y.argmax(axis=2) pred = dec_X.squeeze(axis=0).astype('int32').item() - # Save attention weights (to be covered later) + # 保存注意力权重(稍后讨论) if save_attention_weights: attention_weight_seq.append(net.decoder.attention_weights) - # Once the end-of-sequence token is predicted, the generation of the - # output sequence is complete + # 一旦序列结束标记被预测,输出序列的生成就完成了 if pred == tgt_vocab['']: break output_seq.append(pred) @@ -455,33 +454,31 @@ def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, #@save def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=False): - """Predict for sequence to sequence.""" - # Set `net` to eval mode for inference + """序列到序列模型的预测""" + # 在预测时将`net`设置为评估模式 net.eval() src_tokens = src_vocab[src_sentence.lower().split(' ')] + [ src_vocab['']] enc_valid_len = torch.tensor([len(src_tokens)], device=device) src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['']) - # Add the batch axis + # 添加批量轴 enc_X = torch.unsqueeze( torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0) enc_outputs = net.encoder(enc_X, enc_valid_len) dec_state = net.decoder.init_state(enc_outputs, enc_valid_len) - # Add the batch axis + # 添加批量轴 dec_X = torch.unsqueeze(torch.tensor( [tgt_vocab['']], dtype=torch.long, device=device), dim=0) output_seq, attention_weight_seq = [], [] for _ in range(num_steps): Y, dec_state = net.decoder(dec_X, dec_state) - # We use the token with the highest prediction likelihood as the input - # of the decoder at the next time step + # 我们使用具有预测最高可能性的标记,作为解码器在下一时间步的输入 dec_X = Y.argmax(dim=2) pred = dec_X.squeeze(dim=0).type(torch.int32).item() - # Save attention weights (to be covered later) + # 保存注意力权重(稍后讨论) if save_attention_weights: attention_weight_seq.append(net.decoder.attention_weights) - # Once the end-of-sequence token is predicted, the generation of the - # output sequence is complete + # 一旦序列结束标记被预测,输出序列的生成就完成了 if pred == tgt_vocab['']: break output_seq.append(pred) @@ -490,23 +487,23 @@ def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, ## 预测序列的评估 -我们可以通过将预测序列与标签序列(基本真实)进行比较来评估该序列。BLEU(双语评估小学)虽然最初建议用于评估机器翻译结果 :cite:`Papineni.Roukos.Ward.ea.2002`,但已广泛用于测量不同应用的输出序列的质量。原则上,对于预测序列中的任何 $n$ 克,BLEU 会评估这 $n$ 克是否出现在标签序列中。 +我们可以通过与标签序列(真实标签)进行比较来评估预测序列。BLEU(Bilingual Evaluation Understudy)虽然最初被提出用于评估机器翻译结果 :cite:`Papineni.Roukos.Ward.ea.2002` ,但已被广泛用于测量多种应用的输出序列的质量。原则上,对于预测序列中的任意$n$元组,BLEU评估该$n$元组是否出现在标签序列中。 -用 $p_n$ 表示 $n$ 克的精度,这是预测序列和标签序列中匹配的 $n$ 克数与预测序列中 $n$ 克数的比率。解释一下,给定标签序列 $B$、$B$、$C$、$D$、$F$ 和预测序列 $F$ 和预测序列 $B$、$B$、$B$、$C$、$D$,我们有 $p_2 = 3/4$ 3624 和 $p_4 = 0$。此外,让 $\mathrm{len}_{\text{label}}$ 和 $\mathrm{len}_{\text{pred}}$ 分别成为标签序列和预测序列中的令牌数量。然后,BLEU 被定义为 +用$p_n$表示$n$元组的精度,它是预测序列和标签序列中匹配的$n$元组的数量与预测序列中$n$元组的数量的比率。详细解释一下,给定标签序列$A$、$B$、$C$、$D$、$E$、$F$和预测序列$A$、$B$、$B$、$C$、$D$,我们有$p_1 = 4/5$、$p_2 = 3/4$、$p_3 = 1/3$和$p_4 = 0$。另外,让$\mathrm{len}_{\text{label}}$和$\mathrm{len}_{\text{pred}}$分别是标签序列和预测序列中的标记数。那么,BLEU的定义是: $$ \exp\left(\min\left(0, 1 - \frac{\mathrm{len}_{\text{label}}}{\mathrm{len}_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n},$$ :eqlabel:`eq_bleu` -其中 $k$ 是匹配时间最长的 $n$ 克。 +其中$k$是最长的$n$元组进行匹配。 -根据 :eqref:`eq_bleu` 中 BLEU 的定义,只要预测的序列与标签序列相同,BLEU 就为 1。此外,由于匹配更长的 $n$ 克更困难,BLEU 将更大的权重分配给更长的 $n$ 克精度。具体来说,当 $p_n$ 固定时,$p_n^{1/2^n}$ 随着 $n$ 的增长而增加(原纸使用 $p_n^{1/n}$)。此外,由于预测较短的序列往往会获得更高的 $p_n$ 值,因此 :eqref:`eq_bleu` 中乘法项之前的系数将惩罚较短的预测序列。例如,在考虑到标签序列 $k=2$、$B$、$C$、$E$、$F$ 和预测序列 $B$(尽管 $B$)的情况下,鉴于标签序列 $B$、$B$,但罚因子 $\exp(1-6/2) \approx 0.14$ 降低了蓝效单元。 +根据 :eqref:`eq_bleu` 中BLEU的定义,当预测序列与标签序列相同时,BLEU为1。此外,由于匹配更长的$n$元组更加困难,BLEU为更长的$n$元组精度分配了更大的权重。具体来说,当$p_n$固定时,$p_n^{1/2^n}$会随着$n$的增长而增加(原始论文使用$p_n^{1/n}$)。此外,由于预测较短的序列倾向于获得较高的$p_n$值,因此 :eqref:`eq_bleu` 中乘法项之前的系数惩罚较短的预测序列。例如,当$k=2$时,给定标签序列$A$、$B$、$C$、$D$、$E$、$F$和预测序列$A$、$B$,尽管$p_1 = p_2 = 1$,惩罚因子$\exp(1-6/2) \approx 0.14$降低BLEU。 -我们按如下方式实施 BLEU 措施。 +我们实现BLEU的代码如下。 ```{.python .input} #@tab all def bleu(pred_seq, label_seq, k): #@save - """Compute the BLEU.""" + """计算BLEU""" pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') len_pred, len_label = len(pred_tokens), len(label_tokens) score = math.exp(min(0, 1 - len_label / len_pred)) @@ -522,7 +519,7 @@ def bleu(pred_seq, label_seq, k): #@save return score ``` -最后,我们使用训练有素的 RNN 编码器将一些英语句子翻译成法语并计算结果的 BLEU。 +最后,利用训练好的循环神经网络编码器-解码器模型将几个英语句子翻译成法语,并计算结果的BLEU。 ```{.python .input} #@tab all @@ -534,22 +531,22 @@ for eng, fra in zip(engs, fras): print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}') ``` -## 摘要 +## 小结 -* 按照编码器解码器架构的设计,我们可以使用两个 RNN 来设计一个用于序列到序列学习的模型。 -* 在实施编码器和解码器时,我们可以使用多层 RNN。 -* 我们可以使用掩码来过滤掉不相关的计算,例如在计算损失时。 -* 在编码器解码器培训中,教师强制方法将原始输出序列(与预测相比)输入解码器。 -* BLEU 是通过在预测序列和标签序列之间匹配 $n$ 克来评估输出序列的常用措施。 +* 根据“编码器-解码器”结构的设计,我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。 +* 在实现编码器和解码器时,我们可以使用多层循环神经网络。 +* 我们可以使用遮蔽来过滤不相关的计算,例如在计算损失时。 +* 在编码器-解码器训练中,教师强制方法将原始输出序列(而非预测结果)输入解码器。 +* BLEU是一种常用的评估自然语言处理模型的方法,它通过在预测序列和标签序列之间匹配$n$元组来实现。 ## 练习 -1. 你能调整超参数以改善翻译结果吗? -1. 在损失计算中不使用口罩重新运行实验。你观察到什么结果?为什么? -1. 如果编码器和解码器在层数或隐藏单位数量上有所不同,我们该如何初始化解码器的隐藏状态? -1. 在培训中,将教师强制改为将前一时间步骤的预测输入解码器。这对表现有何影响? -1. 通过用 LSTM 替换 GRU 来重新运行实验。 -1. 还有其他方法可以设计解码器的输出层吗? +1. 你能调整超参数来提高翻译效果吗? +1. 在损失计算中不使用遮蔽重新运行实验。你观察到什么结果? +1. 如果编码器和解码器的层数或隐藏单元数不同,如何初始化解码器的隐藏状态? +1. 在训练中,用将前一时间步的预测输入解码器来代替教师强制。这对性能有何影响? +1. 用长短期记忆网络替换门控循环单元重新运行实验。 +1. 有没有其他方法来设计解码器的输出层? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/345) From 195544632c6207b2fd452996b8046a9400db6d02 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Wed, 21 Apr 2021 01:33:51 +0800 Subject: [PATCH 057/103] chapter_recurrent-modern/machine-translation-and-dataset (#727) * chapter_recurrent-modern/machine-translation-and-dataset * Update machine-translation-and-dataset.md Co-authored-by: goldmermaid --- .../machine-translation-and-dataset.md | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/chapter_recurrent-modern/machine-translation-and-dataset.md b/chapter_recurrent-modern/machine-translation-and-dataset.md index 32834a920..18eab9ee3 100644 --- a/chapter_recurrent-modern/machine-translation-and-dataset.md +++ b/chapter_recurrent-modern/machine-translation-and-dataset.md @@ -1,16 +1,12 @@ -# 机器翻译和数据集 +# 机器翻译与数据集 :label:`sec_machine_translation` -我们使用 RNN 来设计语言模型,这是自然语言处理的关键。另一个旗舰基准是 * 机器转换 *,这是将输入序列转换为输出序列的 * 序列传感 * 模型的核心问题领域。序列转导模型在各种现代 AI 应用中发挥关键作用,将成为本章剩余部分和 :numref:`chap_attention` 的重点。为此,本节介绍机器翻译问题及其稍后将使用的数据集。 +我们已经使用循环神经网络来设计语言模型,这是自然语言处理的关键。另一个旗舰基准测试是“机器翻译”,这是将输入序列转换成输出序列的序列转换模型的的核心问题。序列转换模型在各种现代人工智能应用中发挥着至关重要的作用,将成为本章剩余部分和 :numref:`chap_attention` 的重点。为此,本节介绍机器翻译问题及其稍后将使用的数据集。 -*机器翻译 * 是指 -将序列从一种语言自动翻译为另一种语言。事实上,这个领域可能追溯到 20 世纪 40 年代数字计算机发明之后不久,特别是考虑在二战中使用计算机破解语言代码。几十年来,在使用神经网络进行端到端学习兴起之前,统计方法在该领域占主导地位 :cite:`Brown.Cocke.Della-Pietra.ea.1988,Brown.Cocke.Della-Pietra.ea.1990`。后者通常被称为 -*神经机器翻译 * -将自己与 -*统计机翻译 * -这涉及对翻译模型和语言模型等组成部分进行统计分析. +*机器翻译*指的是将序列从一种语言自动翻译成另一种语言。事实上,这个领域可能可以追溯到数字计算机发明后不久的20世纪40年代,在第二次世界大战中就使用计算机破解语言编码。几十年来,在使用神经网络进行端到端学习的兴起之前,统计学方法在这一领域一直占据主导地位 :cite:`Brown.Cocke.Della-Pietra.ea.1988,Brown.Cocke.Della-Pietra.ea.1990` 。基于神经网络的方法通常被称为*神经机器翻译*从而将自己与*统计机器翻译*区分开。 +这涉及翻译模型和语言模型等组成部分的统计分析。 -本书强调端到端学习,将重点介绍神经机器翻译方法。与我们在 :numref:`sec_language_model` 语料库中使用单一语言的语料模型问题不同,机器翻译数据集由分别使用源语言和目标语言的文本序列对组成。因此,我们需要一种不同的方法来预处理机器翻译数据集,而不是重复使用预处理程序进行语言建模。在下面,我们将展示如何将预处理的数据加载到小批中进行培训。 +这本书强调端到端的学习,将重点放在神经机器翻译方法上。与 :numref:`sec_language_model` 中的语言模型问题(语料库是单一语言的)不同,机器翻译数据集是由源语言和目标语言的文本序列对组成的。因此,我们需要一种不同的方法来预处理机器翻译数据集,而不是复用语言模型的预处理程序。在下面,我们将展示如何将预处理后的数据加载到小批量中进行训练。 ```{.python .input} from d2l import mxnet as d2l @@ -35,7 +31,7 @@ import os ## 下载和预处理数据集 -首先,我们下载一个由 [bilingual sentence pairs from the Tatoeba Project](http://www.manythings.org/anki/) 组成的英法数据集。数据集中的每一行都是一对制表符分隔的英文文本序列和翻译的法语文本序列。请注意,每个文本序列可以只是一个句子,也可以是多个句子的一段。在这个英语翻译成法语的机器翻译问题中,英语是 * 源语言 *,法语是 * 目标语言 *。 +首先,我们下载一个由[Tatoeba项目的双语句子对](http://www.manythings.org/anki/)组成的英-法数据集。数据集中的每一行都是一对制表符分隔的英文文本序列和翻译后的法语文本序列。请注意,每个文本序列可以是一个句子,也可以是包含多个句子的一段。在这个将英语翻译成法语的机器翻译问题中,英语是“源语言”(source language),法语是“目标语言”(target language)。 ```{.python .input} #@tab all @@ -54,7 +50,7 @@ raw_text = read_data_nmt() print(raw_text[:75]) ``` -下载数据集后,我们将继续对原始文本数据进行几个预处理步骤。例如,我们用空格替换不间断的空格,将大写字母转换为小写字母,然后在单词和标点符号之间插入空格。 +下载数据集后,我们对原始文本数据进行几个预处理步骤。例如,我们用单个空格代替连续多个空格,将大写字母转换为小写字母,并在单词和标点符号之间插入空格。 ```{.python .input} #@tab all @@ -76,9 +72,9 @@ text = preprocess_nmt(raw_text) print(text[:80]) ``` -## 令牌化 +## 标记化 -与 :numref:`sec_language_model` 中的字符级令牌化不同,对于机器翻译,我们更喜欢这里的字级标记化(最先进的模型可能使用更先进的标记化技术)。以下 `tokenize_nmt` 函数标记了前 `num_examples` 文本序列对,其中每个标记都是一个单词或标点符号。此函数返回两个令牌列表:`source` 和 `target`。具体来说,`source[i]` 是源语言(英语)$i^\mathrm{th}$ 文本序列中的令牌列表,`target[i]` 是目标语言(此处法语)。 +与 :numref:`sec_language_model` 中的字符级标记化不同,对于机器翻译,我们更喜欢词级标记化(最先进的模型可能使用更高级的标记化技术)。下面的`tokenize_nmt`函数对前`num_examples`个文本序列对进行标记,其中每个标记要么是一个单词,要么是一个标点符号。此函数返回两个标记列表:`source`和`target`。具体地说,`source[i]`是源语言(这里是英语)第$i$个文本序列的标记列表,`target[i]`是目标语言(这里是法语)的标记。 ```{.python .input} #@tab all @@ -99,7 +95,7 @@ source, target = tokenize_nmt(text) source[:6], target[:6] ``` -让我们绘制每个文本序列的令牌数的直方图。在这个简单的英法数据集中,大多数文本序列的令牌少于 20 个。 +让我们绘制每个文本序列的标记数量的直方图。在这个简单的英法数据集中,大多数文本序列的标记少于20个。 ```{.python .input} #@tab all @@ -112,9 +108,9 @@ for patch in patches[1].patches: d2l.plt.legend(loc='upper right'); ``` -## 词汇 +## 词表 -由于机器翻译数据集由对语言组成,因此我们可以为源语言和目标语言分别构建两个词汇表。使用单词级令牌化,词汇量大小将远远大于使用字符级标记的词汇量。为了缓解这种情况,这里我们将出现少于 2 倍的不经常的令牌视为同一个未知 (” “) 令牌。除此之外,我们还指定了额外的特殊标记,例如用于填充 (” “) 序列的小批次长度相同,以及用于标记 序列的开头 (” “) 或 end (” “)。这种特殊令牌通常用于自然语言处理任务。 +由于机器翻译数据集由语言对组成,因此我们可以分别为源语言和目标语言构建两个词表。使用词级标记化时,词汇量将明显大于使用字符级标记化时的词汇量。为了缓解这一问题,这里我们将出现次数少于2次的低标记牌视为相同的未知(“<unk>”)令牌。除此之外,我们还指定了额外的特殊标记,例如用于小批量时填充相同长度的序列(“<pad>”),以及序列的开始标记(“<bos>”)和结束标记(“<eos>”)。这样的特殊标记在自然语言处理任务中比较常用。 ```{.python .input} #@tab all @@ -126,11 +122,11 @@ len(src_vocab) ## 加载数据集 :label:`subsec_mt_data_loading` -回想一下,在语言建模中,每个序列示例,无论是一个句子的一段或多个句子的跨度,都有固定的长度。:numref:`sec_language_model` 中的 `num_steps`(时间步长或令牌数)参数指定了这一点。在机器翻译中,每个示例都是一对源文本序列和目标文本序列,其中每个文本序列可能具有不同的长度。 +回想一下,在语言模型中,每个序列样本,一个句子的一段或多个句子的跨度,都有一个固定的长度。都有固定的长度。这是由 :numref:`sec_language_model` 中的`num_steps`(时间步数或标记数)参数指定的。在机器翻译中,每个样本都是一对源和目标文本序列,其中每个文本序列可以具有不同的长度。 -为了提高计算效率,我们仍然可以通过 * 截断 * 和 * 填充 * 同时处理一批文本序列。假设同一个小批次中的每个序列都应该具有相同的长度 `num_steps`。如果文本序列的令牌少于 `num_steps`,我们将继续将特殊的 “” 令牌附加到其末尾,直到其长度达到 `num_steps`。否则,我们将通过仅获取第一个 `num_steps` 令牌并丢弃剩余的来截断文本序列。通过这种方式,每个文本序列的长度将相同,以相同形状的小批量加载。 +为了提高计算效率,我们仍然可以通过*截断*和*填充*一次处理一小批量文本序列。假设同一小批量中的每个序列应该具有相同的长度`num_steps`。如果文本序列的标记少于`num_steps`个,我们将继续在其末尾附加特殊的“<pad>”标记,直到其长度达到`num_steps`。否则,我们将截断文本序列,只取其前`num_steps`个令牌,并丢弃其余的标记。这样,每个文本序列将具有相同的长度,以便以相同形状的小批量加载。 -如前所述,以下 `truncate_pad` 函数会截断或填充文本序列。 +以下`truncate_pad`函数如前所述截断或填充文本序列。 ```{.python .input} #@tab all @@ -144,13 +140,13 @@ def truncate_pad(line, num_steps, padding_token): truncate_pad(src_vocab[source[0]], 10, src_vocab['']) ``` -现在我们定义了一个函数来将文本序列转换为小批进行训练。我们将特殊的 “” 标记附加到每个序列的末尾,以表示序列的结束。当模型通过在令牌后生成序列令牌来进行预测时,“” 令牌的生成可能表明输出序列已完成。此外,我们还记录每个文本序列的长度,不包括填充标记。我们稍后将介绍的一些模型将需要这些信息。 +现在我们定义一个函数,将文本序列转换成小批量进行训练。我们将特殊的“<eos>”标记附加到每个序列的末尾,以指示序列的结束。当模型通过一个接一个地生成序列令牌进行预测时,“<eos>”令牌的生成可以暗示输出序列是完整的。此外,我们还记录了不包括填充标记的每个文本序列的长度。我们稍后将介绍的一些模型将需要此信息。 ```{.python .input} #@tab all #@save def build_array_nmt(lines, vocab, num_steps): - """Transform text sequences of machine translation into minibatches.""" + """将机器翻译的文本序列转换成小批量。""" lines = [vocab[l] for l in lines] lines = [l + [vocab['']] for l in lines] array = d2l.tensor([truncate_pad( @@ -160,15 +156,15 @@ def build_array_nmt(lines, vocab, num_steps): return array, valid_len ``` -## 把所有东西放在一起 +## 训练模型 -最后,我们定义了 `load_data_nmt` 函数来返回数据迭代器,以及源语言和目标语言的词汇表。 +最后,我们定义`load_data_nmt`函数来返回数据迭代器,以及源语言和目标语言的词汇表。 ```{.python .input} #@tab all #@save def load_data_nmt(batch_size, num_steps, num_examples=600): - """Return the iterator and the vocabularies of the translation dataset.""" + """返回翻译数据集的迭代器和词汇表。""" text = preprocess_nmt(read_data_nmt()) source, target = tokenize_nmt(text, num_examples) src_vocab = d2l.Vocab(source, min_freq=2, @@ -182,7 +178,7 @@ def load_data_nmt(batch_size, num_steps, num_examples=600): return data_iter, src_vocab, tgt_vocab ``` -让我们阅读英法数据集中的第一个小批。 +让我们读出英语-法语数据集中的第一个小批量数据。 ```{.python .input} #@tab all @@ -195,16 +191,16 @@ for X, X_valid_len, Y, Y_valid_len in train_iter: break ``` -## 摘要 +## 小结 -* 机器翻译是指将序列从一种语言自动翻译为另一种语言。 -* 使用单词级令牌化,词汇量大小将明显大于使用字符级令牌化的词汇量。为了缓解这种情况,我们可以将不常见的令牌视为同一个未知代币。 -* 我们可以截断和填充文本序列,以便所有文本序列都具有相同的长度以小批量加载。 +* 机器翻译是指将文本序列从一种语言自动翻译成另一种语言。 +* 使用词级标记化时的词汇量,将明显大于使用字符级标记化时的词汇量。为了缓解这一问题,我们可以将低频标记视为相同的未知标记。 +* 我们可以截断和填充文本序列,以便所有文本序列都具有相同的长度,以便以小批量方式加载。 ## 练习 -1. 在 `load_data_nmt` 函数中尝试 `num_examples` 参数的不同值。这对源语言和目标语言的词汇量大小有何影响? -1. 某些语言(如中文和日语)的文本没有字边界指示符(例如空格)。对于这种情况,单词级标记化仍然是一个好主意吗?为什么或为什么不? +1. 在`load_data_nmt`函数中尝试`num_examples`参数的不同值。这对源语言和目标语言的词汇量有何影响? +1. 某些语言(例如中文和日语)的文本没有单词边界指示符(例如,空格)。对于这种情况,词级标记化仍然是个好主意吗?为什么? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/344) From 7de7cef4193ec797fa7d3623e4f2662df0729826 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Wed, 21 Apr 2021 02:22:42 +0800 Subject: [PATCH 058/103] chapter_recurrent-modern/encoder-decoder (#728) * chapter_recurrent-modern/encoder-decoder * Update encoder-decoder.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/encoder-decoder.md | 40 ++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/chapter_recurrent-modern/encoder-decoder.md b/chapter_recurrent-modern/encoder-decoder.md index 6f707b5d9..dd023e989 100644 --- a/chapter_recurrent-modern/encoder-decoder.md +++ b/chapter_recurrent-modern/encoder-decoder.md @@ -1,23 +1,23 @@ -# 编码器解码器架构 +# 编码器-解码器结构 :label:`sec_encoder-decoder` -正如我们在 :numref:`sec_machine_translation` 中所讨论的那样,机器翻译是序列转导模型的主要问题领域,其输入和输出都是可变长度序列。为了处理这种类型的输入和输出,我们可以设计一个包含两个主要组件的架构。第一个组件是 **编码器**(encoder):它采用可变长度序列作为输入,然后将其转换为具有固定形状的状态。第二个组件是 **解码器**(decoder):它将固定形状的编码状态映射到可变长度序列。这被称为 **编码器-解码器**(encoder-decoder) 体系结构,在 :numref:`fig_encoder_decoder` 中进行了描述。 +正如我们在:numref:`sec_machine_translation`中所讨论的,机器翻译是序列转换模型的一个核心问题,其输入和输出都是可变长度序列。为了处理这种类型的输入和输出,我们可以设计一个包含两个主要组件的结构。第一个组件是一个*编码器*(encoder):它接受一个可变长度的序列作为输入,并将其转换为具有固定形状的编码状态。第二个组件是*解码器*(decoder):它将固定形状的编码状态映射到可变长度序列。这被称为*编码器-解码器*(encoder-decoder)结构。如:numref:`fig_encoder_decoder`所示。 -![The encoder-decoder architecture.](../img/encoder-decoder.svg) +![编码器-解码器结构](../img/encoder-decoder.svg) :label:`fig_encoder_decoder` -让我们以英语到法语的机器翻译为例。给定英语输入序列:“They”,“are”,“watching”,“.” 这种编码器解码器架构首先将可变长度输入编码为一个状态,然后解码状态,然后通过令牌生成翻译的序列标记作为输出:“Ils”、“regardent”、“.”。由于编码器解码器体系结构构成了后续章节中不同序列转导模型的基础,因此本节将此架构转换为稍后实现的接口。 +让我们以英语到法语的机器翻译为例。给定一个英文的输入序列:“They”、“are”、“watching”、“.”,这种编码器-解码器结构首先将可变长度的输入编码成一个状态,然后对该状态进行解码,一个标记一个标记地生成翻译后的序列令牌作为输出:“Ils”、“regordent”、“.”。由于编码器-解码器结构构成了后续章节中不同序列转换模型的基础,因此本节将把该结构转换为稍后将实现的接口。 ## 编码器 -在编码器界面中,我们只需指定编码器采用可变长度序列作为输入 `X`。该实现将由继承此基础 `Encoder` 类的任何模型提供。 +在编码器接口中,我们只指定编码器采用可变长度序列作为输入`X`。实现将由任何继承这个`Encoder`基类的模型提供。 ```{.python .input} from mxnet.gluon import nn #@save class Encoder(nn.Block): - """The base encoder interface for the encoder-decoder architecture.""" + """编码器-解码器结构的基本编码器接口。""" def __init__(self, **kwargs): super(Encoder, self).__init__(**kwargs) @@ -31,7 +31,7 @@ from torch import nn #@save class Encoder(nn.Module): - """The base encoder interface for the encoder-decoder architecture.""" + """编码器-解码器结构的基本编码器接口。""" def __init__(self, **kwargs): super(Encoder, self).__init__(**kwargs) @@ -41,12 +41,12 @@ class Encoder(nn.Module): ## 解码器 -在下面的解码器界面中,我们添加了一个额外的 `init_state` 函数,将编码器输出 (`enc_outputs`) 转换为编码状态。请注意,此步骤可能需要额外的输入,例如输入的有效长度,在 :numref:`subsec_mt_data_loading` 中对此进行了解释。要通过令牌生成可变长度序列令牌,每次解码器都可能在当前时间步将输入(例如,上一个时间步生成的令牌)和编码状态映射到输出令牌时。 +在下面的解码器接口中,我们添加了一个额外的`init_state`函数来将编码器输出(`enc_outputs`)转换为编码状态。请注意,此步骤可能需要额外的输入,例如输入的有效长度,这在:numref:`subsec_mt_data_loading`中进行了解释。为了逐个标记生成可变长度标记序列,每次解码器可将输入(例如,在前一时间步生成的标记)和编码状态映射到当前时间步的输出标记。 ```{.python .input} #@save class Decoder(nn.Block): - """The base decoder interface for the encoder-decoder architecture.""" + """编码器-解码器结构的基本解码器接口。""" def __init__(self, **kwargs): super(Decoder, self).__init__(**kwargs) @@ -61,7 +61,7 @@ class Decoder(nn.Block): #@tab pytorch #@save class Decoder(nn.Module): - """The base decoder interface for the encoder-decoder architecture.""" + """编码器-解码器结构的基本解码器接口。""" def __init__(self, **kwargs): super(Decoder, self).__init__(**kwargs) @@ -72,14 +72,14 @@ class Decoder(nn.Module): raise NotImplementedError ``` -## 将编码器和解码器放在一起 +## 把编码器和解码器合并 -最后,编码器解码器架构包含编码器和解码器,并可选择附加参数。在向前传播中,编码器的输出用于产生编码状态,解码器将进一步使用此状态作为其输入之一。 +最后,编码器-解码器结构包含编码器和解码器,并包含可选的额外的参数。在前向传播中,编码器的输出产生“编码状态”,解码器将使用该状态作为其输入之一。 ```{.python .input} #@save class EncoderDecoder(nn.Block): - """The base class for the encoder-decoder architecture.""" + """编码器-解码器结构的基类。""" def __init__(self, encoder, decoder, **kwargs): super(EncoderDecoder, self).__init__(**kwargs) self.encoder = encoder @@ -95,7 +95,7 @@ class EncoderDecoder(nn.Block): #@tab pytorch #@save class EncoderDecoder(nn.Module): - """The base class for the encoder-decoder architecture.""" + """编码器-解码器结构的基类。""" def __init__(self, encoder, decoder, **kwargs): super(EncoderDecoder, self).__init__(**kwargs) self.encoder = encoder @@ -107,18 +107,18 @@ class EncoderDecoder(nn.Module): return self.decoder(dec_X, dec_state) ``` -编码器解码器架构中的 “状态” 一词可能激发了你使用带状态的神经网络来实现这种架构。在下一节中,我们将了解如何应用 RNN 来设计基于此编码器解码器架构的序列转导模型。 +编码器-解码器体系结构中的术语“状态”可能启发你使用具有状态的神经网络来实现该结构。在下一节中,我们将看到如何应用循环神经网络来设计基于这种编码器-解码器结构的序列转换模型。 -## 摘要 +## 小结 -* 编码器解码器架构可以处理同时属于可变长度序列的输入和输出,因此适用于机器翻译等序列转导问题。 -* 编码器采用可变长度序列作为输入,并将其转换为具有固定形状的状态。 +* 编码器-解码器结构可以处理可变长度序列的输入和输出,因此适用于机器翻译等序列转换问题。 +* 编码器以可变长度序列作为输入,将其转换为具有固定形状的状态。 * 解码器将固定形状的编码状态映射到可变长度序列。 ## 练习 -1. 假设我们使用神经网络来实现编码器解码器架构。编码器和解码器必须是同一类型的神经网络吗? -1. 除了机器翻译之外,你能想到另一个可以应用编码器-解码器架构的应用程序吗? +1. 假设我们使用神经网络来实现编解码结构。编码器和解码器必须是同一类型的神经网络吗? +1. 除了机器翻译,你能想到另一个可以应用编码器-解码器结构的应用吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/341) From 6c4029efb6efe34d9e57a55d32ea7e236cb89d6a Mon Sep 17 00:00:00 2001 From: goldmermaid Date: Wed, 21 Apr 2021 16:12:54 -0700 Subject: [PATCH 059/103] [slides] CNN (#753) * channels slides done * conv-layer slides done * lenet test * lenet test * lenet remove * * lenet slides * lenet slides * padding * pooling * retrigger * pooling done * lenet * channels * conv-layer * retrigger * retrigger * retrigger * retrigger --- .../channels.md | 12 +++++++----- .../conv-layer.md | 16 ++++++++-------- chapter_convolutional-neural-networks/lenet.md | 13 ++++++------- .../padding-and-strides.md | 8 ++++---- chapter_convolutional-neural-networks/pooling.md | 16 ++++++++-------- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/chapter_convolutional-neural-networks/channels.md b/chapter_convolutional-neural-networks/channels.md index 89389fb00..c7b9247e1 100644 --- a/chapter_convolutional-neural-networks/channels.md +++ b/chapter_convolutional-neural-networks/channels.md @@ -19,7 +19,7 @@ ![两个输入通道的互相关计算。](../img/conv-multi-in.svg) :label:`fig_conv_multi_in` -为了加深理解,我们将多输入通道互相关运算实现一下。 +为了加深理解,我们将(**实现一下多输入通道互相关运算**)。 简而言之,我们所做的就是对每个通道执行互相关操作,然后将结果相加。 ```{.python .input} @@ -51,7 +51,7 @@ def corr2d_multi_in(X, K): return tf.reduce_sum([d2l.corr2d(x, k) for x, k in zip(X, K)], axis=0) ``` -我们可以构造与 :numref:`fig_conv_multi_in` 中的值相对应的输入张量 `X` 和核张量 `K`,以验证互相关运算的输出。 +我们可以构造与 :numref:`fig_conv_multi_in` 中的值相对应的输入张量 `X` 和核张量 `K`,以[**验证互相关运算的输出**]。 ```{.python .input} #@tab all @@ -68,7 +68,7 @@ corr2d_multi_in(X, K) 用 $c_i$ 和 $c_o$ 分别表示输入和输出通道的数目,并让 $k_h$ 和 $k_w$ 为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为 $c_i\times k_h\times k_w$ 的卷积核张量,这样卷积核的形状是 $c_o\times c_i\times k_h\times k_w$。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。 -如下所示,我们实现一个计算多个通道的输出的互相关函数。 +如下所示,我们实现一个[**计算多个通道的输出的互相关函数**]。 ```{.python .input} #@tab all @@ -95,7 +95,8 @@ corr2d_multi_in_out(X, K) ## $1\times 1$ 卷积层 -$1 \times 1$ 卷积,即 $k_h = k_w = 1$,看起来似乎没有多大意义。毕竟,卷积的本质是有效提取相邻像素间的相关特征,而 $1 \times 1$ 卷积显然没有此作用。 +[~~1x1卷积~~] +$1 \times 1 $卷积,即 $k_h = k_w = 1$,看起来似乎没有多大意义。毕竟,卷积的本质是有效提取相邻像素间的相关特征,而 $1 \times 1$ 卷积显然没有此作用。 尽管如此,$1 \times 1$ 仍然十分流行,时常包含在复杂深层网络的设计中。下面,让我们详细地解读一下它的实际作用。 因为使用了最小窗口,$1\times 1$ 卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。 @@ -121,7 +122,8 @@ def corr2d_multi_in_out_1x1(X, K): c_o = K.shape[0] X = d2l.reshape(X, (c_i, h * w)) K = d2l.reshape(K, (c_o, c_i)) - Y = d2l.matmul(K, X) # 全连接层中的矩阵乘法 + # 全连接层中的矩阵乘法 + Y = d2l.matmul(K, X) return d2l.reshape(Y, (c_o, h, w)) ``` diff --git a/chapter_convolutional-neural-networks/conv-layer.md b/chapter_convolutional-neural-networks/conv-layer.md index 6701653b0..0fa10a332 100644 --- a/chapter_convolutional-neural-networks/conv-layer.md +++ b/chapter_convolutional-neural-networks/conv-layer.md @@ -6,7 +6,7 @@ ## 互相关运算 严格地说,卷积层所表达的运算可以被更准确地描述为 *互相关运算* (cross-correlation)。 -根据 :numref:`sec_why-conv` 中的描述,在卷积层中,输入张量和核张量通过互相关运算产生输出张量。 +根据 :numref:`sec_why-conv` 中的描述,在卷积层中,输入张量和核张量通过(**互相关运算**)产生输出张量。 首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。在 :numref:`fig_correlation` 中,输入是高度为 $3$ 、宽度为 $3$ 的二维张量(即形状为 $3 \times 3$ )。卷积核的高度和宽度都是 $2$ ,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即 $2 \times 2$ )。 @@ -75,7 +75,7 @@ def corr2d(X, K): #@save return Y ``` -通过 :numref:`fig_correlation` 的输入张量 `X` 和卷积核张量 `K` ,我们来验证一下上述二维互相关运算的输出。 +通过 :numref:`fig_correlation` 的输入张量 `X` 和卷积核张量 `K` ,我们来[**验证上述二维互相关运算的输出**]。 ```{.python .input} #@tab all @@ -90,7 +90,7 @@ corr2d(X, K) 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。 就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重 -基于上面定义的 `corr2d` 函数实现二维卷积层。在 `__init__` 构造函数中,将 `weight` 和 `bias` 声明为两个模型参数。前向传播函数调用 `corr2d` 函数并添加偏置。 +基于上面定义的 `corr2d` 函数[**实现二维卷积层**]。在 `__init__` 构造函数中,将 `weight` 和 `bias` 声明为两个模型参数。前向传播函数调用 `corr2d` 函数并添加偏置。 ```{.python .input} class Conv2D(nn.Block): @@ -138,7 +138,7 @@ class Conv2D(tf.keras.layers.Layer): ## 图像中目标的边缘检测 -如下是卷积层的一个简单应用:通过找到像素变化的位置来检测图像中不同颜色的边缘。 +如下是[**卷积层的一个简单应用:**]通过找到像素变化的位置,来(**检测图像中不同颜色的边缘**)。 首先,我们构造一个 $6\times 8$ 像素的黑白图像。中间四列为黑色($0$),其余像素为白色($1$)。 ```{.python .input} @@ -163,7 +163,7 @@ K = d2l.tensor([[1.0, -1.0]]) ``` 现在,我们对参数 `X` (输入)和 `K` (卷积核)执行互相关运算。 -如下所示,输出 `Y` 中的 $1$ 代表从白色到黑色的边缘, $-1$ 代表从黑色到白色的边缘,其他情况的输出为 $0$ +如下所示,[**输出`Y`中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘**],其他情况的输出为 $0$ ```{.python .input} #@tab all @@ -173,7 +173,7 @@ Y 现在我们将输入的二维图像转置,再进行如上的互相关运算。 其输出如下,之前检测到的垂直边缘消失了。 -不出所料,这个卷积核 `K` 只可以检测垂直边缘,无法检测水平边缘。 +不出所料,这个[**卷积核`K`只可以检测垂直边缘**],无法检测水平边缘。 ```{.python .input} #@tab all @@ -182,7 +182,7 @@ corr2d(d2l.transpose(X), K) ## 学习卷积核 -如果我们只需寻找黑白边缘,那么以上 `[1, -1]` 的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计过滤器。那么我们是否可以学习由 `X` 生成 `Y` 的卷积核呢? +如果我们只需寻找黑白边缘,那么以上 `[1, -1]` 的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计过滤器。那么我们是否可以[**学习由`X`生成`Y`的卷积核**]呢? 现在让我们看看是否可以通过仅查看“输入-输出”对来了解由 `X` 生成 `Y` 的卷积核。 我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较 `Y` 与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。 @@ -256,7 +256,7 @@ for i in range(10): print(f'batch {i+1}, loss {tf.reduce_sum(l):.3f}') ``` -在 $10$ 次迭代之后,误差已经降到足够低。现在我们来看看我们所学的卷积核的权重张量。 +在 $10$ 次迭代之后,误差已经降到足够低。现在我们来看看我们[**所学的卷积核的权重张量**]。 ```{.python .input} d2l.reshape(conv2d.weight.data(), (1, 2)) diff --git a/chapter_convolutional-neural-networks/lenet.md b/chapter_convolutional-neural-networks/lenet.md index bfa0b2d69..524f2d3d0 100644 --- a/chapter_convolutional-neural-networks/lenet.md +++ b/chapter_convolutional-neural-networks/lenet.md @@ -18,8 +18,7 @@ LeNet 被广泛用于自动取款机(ATM)机中,帮助识别处理支票 ## LeNet -总体来看,LeNet (LeNet-5) 由两个部分组成: - +总体来看,(**LeNet(LeNet-5)由两个部分组成:**)(~~卷积编码器和全连接层密集块~~) * 卷积编码器:由两个卷积层组成; * 全连接层密集块:由三个全连接层组成。 @@ -95,7 +94,7 @@ def net(): 我们对原始模型做了一点小改动,去掉了最后一层的高斯激活。除此之外,这个网络与最初的 LeNet-5 一致。 -下面,我们将一个大小为 $28 \times 28$ 的单通道(黑白)图像通过 LeNet。 通过在每一层打印输出的形状,我们可以检查模型,以确保其操作与我们期望的 :numref:`img_lenet_vert` 一致。 +下面,我们将一个大小为 $28 \times 28$ 的单通道(黑白)图像通过 LeNet。 通过在每一层打印输出的形状,我们可以[**检查模型**],以确保其操作与我们期望的 :numref:`img_lenet_vert` 一致。 ![LeNet 的简化版。](../img/lenet-vert.svg) :label:`img_lenet_vert` @@ -133,7 +132,7 @@ for layer in net().layers: ## 模型训练 -现在我们已经实现了 LeNet ,让我们看看这个模型在 Fashion-MNIST 数据集上的表现。 +现在我们已经实现了 LeNet ,让我们看看[**LeNet在Fashion-MNIST数据集上的表现**]。 ```{.python .input} #@tab all @@ -145,7 +144,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size) 如果你有机会使用GPU,可以用它加快训练。 :begin_tab:`mxnet, pytorch` -为了进行评估,我们需要对 :numref:`sec_softmax_scratch` 中描述的 `evaluate_accuracy` 函数进行轻微的修改。 +为了进行评估,我们需要[**对**] :numref:`sec_softmax_scratch` 中描述的 (**`evaluate_accuracy`函数进行轻微的修改**)。 由于完整的数据集位于内存中,因此在模型使用 GPU 计算数据集之前,我们需要将其复制到显存中。 :end_tab: @@ -182,7 +181,7 @@ def evaluate_accuracy_gpu(net, data_iter, device=None): #@save return metric[0] / metric[1] ``` -为了使用 GPU,我们还需要一点小改动。 +[**为了使用 GPU,我们还需要一点小改动**]。 与 :numref:`sec_softmax_scratch` 中定义的 `train_epoch_ch3` 不同,在进行正向和反向传播之前,我们需要将每一小批量数据移动到我们指定的设备(例如 GPU)上。 如下所示,训练函数 `train_ch6` 也类似于 :numref:`sec_softmax_scratch` 中定义的 `train_ch3` 。 @@ -319,7 +318,7 @@ def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device): return net ``` -现在,我们训练和评估 LeNet-5 模型。 +现在,我们[**训练和评估LeNet-5模型**]。 ```{.python .input} #@tab all diff --git a/chapter_convolutional-neural-networks/padding-and-strides.md b/chapter_convolutional-neural-networks/padding-and-strides.md index 48f73d673..cd0b786ee 100644 --- a/chapter_convolutional-neural-networks/padding-and-strides.md +++ b/chapter_convolutional-neural-networks/padding-and-strides.md @@ -42,7 +42,7 @@ $$(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。$$ 3. 输出与输入具有相同高度和宽度 则可以得出:输出 `Y[i, j]` 是通过以输入 `X[i, j]` 为中心,与卷积核进行互相关计算。 -比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并在所有侧边填充 1 个像素。给定高度和宽度为 $8$ 的输入,则输出的高度和宽度也是 8。 +比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并(**在所有侧边填充1个像素**)。给定高度和宽度为8的输入,则输出的高度和宽度也是8。 ```{.python .input} from mxnet import np, npx @@ -104,7 +104,7 @@ X = tf.random.uniform(shape=(8, 8)) comp_conv2d(conv2d, X).shape ``` -当卷积内核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为 $5$,宽度为 $3$ 的卷积核,高度和宽度两边的填充分别为 $2$ 和 $1$。 +当卷积内核的高度和宽度不同时,我们可以[**填充不同的高度和宽度**],使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。 ```{.python .input} conv2d = nn.Conv2D(1, kernel_size=(5, 3), padding=(2, 1)) @@ -145,7 +145,7 @@ $$\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor 如果我们设置了 $p_h=k_h-1$ 和 $p_w=k_w-1$,则输出形状将简化为 $\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor$。 更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 $(n_h/s_h) \times (n_w/s_w)$。 -下面,我们将高度和宽度的步幅设置为 $2$,从而将输入的高度和宽度减半。 +下面,我们[**将高度和宽度的步幅设置为2**],从而将输入的高度和宽度减半。 ```{.python .input} conv2d = nn.Conv2D(1, kernel_size=3, padding=1, strides=2) @@ -164,7 +164,7 @@ conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same', strides=2) comp_conv2d(conv2d, X).shape ``` -接下来,看一个稍微复杂的例子。 +接下来,看(**一个稍微复杂的例子**)。 ```{.python .input} conv2d = nn.Conv2D(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4)) diff --git a/chapter_convolutional-neural-networks/pooling.md b/chapter_convolutional-neural-networks/pooling.md index a1b0c3b99..7b70e2074 100644 --- a/chapter_convolutional-neural-networks/pooling.md +++ b/chapter_convolutional-neural-networks/pooling.md @@ -38,7 +38,7 @@ $$ 无论 `X[i, j]` 和 `X[i, j + 1]` 的值是否不同,或 `X[i, j + 1]` 和 `X[i, j + 2]` 的值是否不同,池化层始终输出 `Y[i, j] = 1`。 也就是说,使用 $2\times 2$ 最大池化层,即使在高度或宽度上移动一个元素,卷积层仍然可以识别到模式。 -在下面的代码中的 `pool2d` 函数,实现了池化层的正向传播。 +在下面的代码中的 `pool2d` 函数,我们(**实现池化层的正向传播**)。 此功能类似于 :numref:`sec_conv_layer` 中的 `corr2d` 函数。 然而,这里我们没有卷积核,输出为输入中每个区域的最大值或平均值。 @@ -86,7 +86,7 @@ def pool2d(X, pool_size, mode='max'): return Y ``` -我们可以构建 :numref:`fig_pooling` 中的输入张量 `X`,验证二维最大池化层的输出。 +我们可以构建 :numref:`fig_pooling` 中的输入张量 `X`,[**验证二维最大池化层的输出**]。 ```{.python .input} #@tab all @@ -94,14 +94,14 @@ X = d2l.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]]) pool2d(X, (2, 2)) ``` -此外,我们还可以验证平均池化层。 +此外,我们还可以(**验证平均池化层**)。 ```{.python .input} #@tab all pool2d(X, (2, 2), 'avg') ``` -## 填充和步幅 +## [**填充和步幅**] 与卷积层一样,池化层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。 下面,我们用深度学习框架中内置的二维最大池化层,来演示池化层中填充和步幅的使用。 @@ -119,7 +119,7 @@ X = d2l.reshape(d2l.arange(16, dtype=d2l.float32), (1, 4, 4, 1)) X ``` -默认情况下,深度学习框架中的步幅与池化窗口的大小相同。 +默认情况下,(**深度学习框架中的步幅与池化窗口的大小相同**)。 因此,如果我们使用形状为 `(3, 3)` 的池化窗口,那么默认情况下,我们得到的步幅形状为 `(3, 3)`。 ```{.python .input} @@ -140,7 +140,7 @@ pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3]) pool2d(X) ``` -填充和步幅可以手动设定。 +[**填充和步幅可以手动设定**]。 ```{.python .input} pool2d = nn.MaxPool2D(3, padding=1, strides=2) @@ -160,7 +160,7 @@ pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='same', pool2d(X) ``` -当然,我们可以设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度。 +当然,我们可以(**设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度**)。 ```{.python .input} pool2d = nn.MaxPool2D((2, 3), padding=(1, 2), strides=(2, 3)) @@ -182,7 +182,7 @@ pool2d(X) ## 多个通道 -在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 +在处理多通道输入数据时,[**池化层在每个输入通道上单独运算**],而不是像卷积层一样在通道上对输入进行汇总。 这意味着池化层的输出通道数与输入通道数相同。 下面,我们将在通道维度上连结张量 `X` 和 `X + 1`,以构建具有 2 个通道的输入。 From 1650462531cc1dea536298c147f05210903c9571 Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Thu, 22 Apr 2021 01:57:31 +0200 Subject: [PATCH 060/103] translation issue in autograd.md (#761) --- chapter_preliminaries/autograd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preliminaries/autograd.md b/chapter_preliminaries/autograd.md index e7a2db515..5aedf9eca 100644 --- a/chapter_preliminaries/autograd.md +++ b/chapter_preliminaries/autograd.md @@ -3,7 +3,7 @@ 正如我们在 :numref:`sec_calculus` 中所说的那样,求导是几乎所有深度学习优化算法的关键步骤。虽然求导的计算很简单,只需要一些基本的微积分,但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。 -深度学习框架通过自动计算导数,即 *自动求导* (automatic differentiation),来加快这项工作。实际中,根据我们设计的模型,系统会构建一个 *计算图* (computational graph),来跟踪数据通过若干操作组合起来产生输出。自动求导使系统能够随后反向传播梯度。 +深度学习框架通过自动计算导数,即 *自动求导* (automatic differentiation),来加快这项工作。实际中,根据我们设计的模型,系统会构建一个 *计算图* (computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动求导使系统能够随后反向传播梯度。 这里,*反向传播*(backpropagate)只是意味着跟踪整个计算图,填充关于每个参数的偏导数。 From f25cb68433a3d7724fa5bfb891542282ca119cc5 Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Thu, 22 Apr 2021 01:59:31 +0200 Subject: [PATCH 061/103] translation issue in chapter_linear-networks/softmax-regression-scratch (#760) --- chapter_linear-networks/softmax-regression-scratch.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chapter_linear-networks/softmax-regression-scratch.md b/chapter_linear-networks/softmax-regression-scratch.md index 6195871ee..a8efc6a40 100644 --- a/chapter_linear-networks/softmax-regression-scratch.md +++ b/chapter_linear-networks/softmax-regression-scratch.md @@ -32,7 +32,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) ## 初始化模型参数 -这里的每个样本都用固定长度向量表示。原始数据集中的每个样本都是 $28 \times 28$ 的图像。在本节中,我们[**将展平每个图像,将它们视为长度为784的向量。**]在以后的章节中,我们将讨论能够利用图像空间结构的复杂策略,但现在我仅将每个像素位置视为一个特征。 +和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是 $28 \times 28$ 的图像。在本节中,我们[**将展平每个图像,把它们看作长度为784的向量。**]在后面的章节中,将讨论能够利用图像空间结构的更为复杂的策略,但现在我们暂时只把每个像素位置看作一个特征。 回想一下,在softmax回归中,我们的输出与类别一样多。(**因为我们的数据集有10个类别,所以网络输出维度为 10**)。因此,权重将构成一个 $784 \times 10$ 的矩阵,偏置将构成一个 $1 \times 10$ 的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重 `W`,偏置初始化为0。 @@ -183,11 +183,11 @@ cross_entropy(y_hat, y) ## 分类准确率 -在给定预测概率分布 `y_hat`时,当我们必须输出硬预测(hard prediction)时,我们通常选择预测概率最高的类。许多应用都要求我们做出选择。如Gmail必须将电子邮件分为“Primary(主要)”、“Social(社交)”、“Updates(更新)”或“Forums(论坛)”。它可能在内部估计概率,但最终它必须在类中选择一个。 +给定预测概率分布 `y_hat`,当我们必须输出硬预测(hard prediction)时,我们通常选择预测概率最高的类。许多应用都要求我们做出选择。如Gmail必须将电子邮件分为“Primary(主要)”、“Social(社交)”、“Updates(更新)”或“Forums(论坛)”。它可能在内部估计概率,但最终它必须在类中选择一个。 当预测与标签分类 `y` 一致时,它们是正确的。分类准确率即正确预测数量与总预测数量之比。虽然直接优化准确率可能很困难(因为准确率的计算不可导),但准确率通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总是会报告它。 -为了计算准确率,我们执行以下操作。首先,如果 `y_hat` 是矩阵,第二个维度存储每个类的预测分数。我们使用 `argmax` 获得每行中最大元素的索引来获得预测类别。然后我们[**将预测类别与真实 `y` 元素进行比较**]。由于等式运算符 `==` 对数据类型很敏感,因此我们将 `y_hat` 的数据类型转换为与 `y` 的数据类型一致。结果是一个包含 0(错)和 1(对)的张量。进行求和会得到正确预测的数量。 +为了计算准确率,我们执行以下操作。首先,如果 `y_hat` 是矩阵,那么假定第二个维度存储每个类的预测分数。我们使用 `argmax` 获得每行中最大元素的索引来获得预测类别。然后我们[**将预测类别与真实 `y` 元素进行比较**]。由于等式运算符 `==` 对数据类型很敏感,因此我们将 `y_hat` 的数据类型转换为与 `y` 的数据类型一致。结果是一个包含 0(错)和 1(对)的张量。进行求和会得到正确预测的数量。 ```{.python .input} #@tab all From a6d31a2e94e34b7ee3450cbcfa54b046b48f21bf Mon Sep 17 00:00:00 2001 From: Aston Zhang Date: Fri, 23 Apr 2021 20:08:14 +0000 Subject: [PATCH 062/103] add ch12 --- .../async-computation.md | 17 +- .../async-computation_origin.md | 15 +- .../auto-parallelism.md | 4 +- chapter_computational-performance/hardware.md | 217 +++++++++ .../hardware_origin.md | 232 ++++++++++ .../hybridize.md | 16 +- .../multiple-gpus-concise.md | 274 ++++++++++++ .../multiple-gpus-concise_origin.md | 286 ++++++++++++ .../multiple-gpus.md | 365 +++++++++++++++ .../multiple-gpus_origin.md | 414 ++++++++++++++++++ .../parameterserver.md | 101 +++++ .../parameterserver_origin.md | 119 +++++ 12 files changed, 2040 insertions(+), 20 deletions(-) create mode 100644 chapter_computational-performance/hardware.md create mode 100644 chapter_computational-performance/hardware_origin.md create mode 100644 chapter_computational-performance/multiple-gpus-concise.md create mode 100644 chapter_computational-performance/multiple-gpus-concise_origin.md create mode 100644 chapter_computational-performance/multiple-gpus.md create mode 100644 chapter_computational-performance/multiple-gpus_origin.md create mode 100644 chapter_computational-performance/parameterserver.md create mode 100644 chapter_computational-performance/parameterserver_origin.md diff --git a/chapter_computational-performance/async-computation.md b/chapter_computational-performance/async-computation.md index f737d1fbe..d95250a4e 100644 --- a/chapter_computational-performance/async-computation.md +++ b/chapter_computational-performance/async-computation.md @@ -123,10 +123,11 @@ z ![Interactions of the frontend and backend.](../img/threading.svg) :label:`fig_threading` -:begin_tab:`mxnet` ## 障碍和阻滞剂 -有许多操作会迫使 Python 等待完成: +:begin_tab:`mxnet` +有许多操作会迫使 Python 等待完成: + * 最明显的是,无论计算指令何时发出,`npx.waitall()` 都会等到所有计算完成。实际上,除非绝对必要,否则使用此操作符是一个坏主意,因为它可能会导致性能不佳。 * 如果我们只想等到特定变量可用,我们可以调用 `z.wait_to_read()`。在这种情况下,MxNet 块返回到 Python,直到计算出变量 `z`。其他计算之后可能会继续进行。 @@ -144,7 +145,7 @@ with d2l.Benchmark('wait_to_read'): ``` :begin_tab:`mxnet` -两项操作需要大约相同的时间才能完成。除了显而易见的阻止操作之外,我们建议您知道 * 隐式 * 阻止程序。打印变量显然需要变量可用,因此它是阻止程序。最后,由于 NumPy 没有异步概念,通过 `z.asnumpy()` 转换为 NumPy 以及通过 `z.item()` 转换为标量的操作都受到阻碍。它需要像 `print` 函数一样访问这些值。 +两项操作需要大约相同的时间才能完成。除了显而易见的阻止操作之外,我们建议您知道 * 隐式 * 阻止程序。打印变量显然需要变量可用,因此它是阻止程序。最后,由于 NumPy 没有异步概念,通过 `z.asnumpy()` 转换为 NumPy 以及通过 `z.item()` 转换为标量的转换都会受阻。它需要像 `print` 函数一样访问这些值。 经常将少量数据从 MxNet 的范围复制到 NumPy 然后会破坏本来有效的代码的性能,因为每个此类操作都需要计算图来评估获得相关术语所需的所有中间结果 * 之前 * 可以做的其他任何事情。 :end_tab: @@ -159,8 +160,10 @@ with d2l.Benchmark('scalar conversion'): b.sum().item() ``` +## 改进计算 + :begin_tab:`mxnet` -## 改进计算在一个严重的多线程系统上(即使是普通笔记本电脑有 4 个或更多线程,在多插槽服务器上,这个数字可能超过 256 个),调度操作的开销可能会变得巨大。这就是为什么非常希望以异步和并行方式进行计算和调度。为了说明这样做的好处,让我们看看如果我们按顺序或异步方式多次增加一个变量,会发生什么情况。我们通过在每次添加之间插入 `wait_to_read` 障碍来模拟同步执行。 +在高度多线程的系统中(即使是普通笔记本电脑也有 4 个或更多线程,在多插槽服务器上,此数字可能会超过 256 个),调度操作的开销可能会变得巨大这就是为什么非常希望以异步和并行方式进行计算和调度。为了说明这样做的好处,让我们看看如果我们按顺序或异步方式多次增加一个变量,会发生什么情况。我们通过在每次添加之间插入 `wait_to_read` 障碍来模拟同步执行。 :end_tab: ```{.python .input} @@ -180,17 +183,17 @@ Python 前端线程和 C ++ 后端线程之间稍微简化的交互可以总结 1. 前端命令后端将计算任务 `y = x + 1` 插入队列。 1. 然后,后端接收队列中的计算任务并执行实际的计算。 1. 然后,后端将计算结果返回给前端。 -假设这三个阶段的持续时间分别为 $t_1, t_2$ 和 $t_3$。如果我们不使用异步编程,则执行 10000 个计算所需的总时间约为 $10000 (t_1+ t_2 + t_3)$。如果使用异步编程,则执行 10000 个计算所花费的总时间可以减少到 $t_1 + 10000 t_2 + t_3$(假设为 $10000 t_2 > 9999t_1$),因为前端不必等后端返回每个循环的计算结果。 +假设这三个阶段的持续时间分别为 $t_1, t_2$ 和 $t_3$。如果我们不使用异步编程,则执行 10000 个计算所需的总时间约为 $10000 (t_1+ t_2 + t_3)$。如果使用异步编程,则执行 10000 个计算所花费的总时间可以减少到 $t_1 + 10000 t_2 + t_3$(假设为 $10000 t_2 > 9999t_1$),因为前端不必等后端返回每个循环的计算结果。 +:end_tab: ## 摘要 * 深度学习框架可能会将 Python 前端与执行后端分离。这允许将命令快速异步插入到后端和相关的并行度。 * 异步导致前端响应相当灵敏。但是,请注意不要溢出任务队列,因为这可能会导致过多的内存消耗。建议对每个微型批次进行同步,以使前端和后端保持大致同步。 * 芯片供应商提供复杂的性能分析工具,以获得对深度学习效率的更精细的洞察。 -:end_tab: :begin_tab:`mxnet` -* 请注意,从 MxNet 的内存管理转换为 Python 将强制后端等到特定变量准备就绪。`print`、`asnumpy` 和 `item` 等函数都具有这样的效果。这可能是可取的,但粗心地使用同步可能会破坏性能。 +* 请注意,从 MxNet 的内存管理转换为 Python 将强制后端等到特定变量准备就绪。诸如 `print`、`asnumpy` 和 `item` 等函数都具有这样的效果。这可能是可取的,但粗心地使用同步可能会破坏性能。 :end_tab: ## 练习 diff --git a/chapter_computational-performance/async-computation_origin.md b/chapter_computational-performance/async-computation_origin.md index 4952b91b1..8bbd41728 100644 --- a/chapter_computational-performance/async-computation_origin.md +++ b/chapter_computational-performance/async-computation_origin.md @@ -137,16 +137,22 @@ z ![The backend tracks dependencies between various steps in the computational graph.](../img/asyncgraph.svg) :label:`fig_asyncgraph` + + The code snippet above is also illustrated in :numref:`fig_asyncgraph`. Whenever the Python frontend thread executes one of the first three statements, it simply returns the task to the backend queue. When the last statement's results need to be *printed*, the Python frontend thread will wait for the C++ backend thread to finish computing the result of the variable `z`. One benefit of this design is that the Python frontend thread does not need to perform actual computations. Thus, there is little impact on the program's overall performance, regardless of Python's performance. :numref:`fig_threading` illustrates how frontend and backend interact. ![Interactions of the frontend and backend.](../img/threading.svg) :label:`fig_threading` -:begin_tab:`mxnet` + + + ## Barriers and Blockers +:begin_tab:`mxnet` There are a number of operations that will force Python to wait for completion: + * Most obviously `npx.waitall()` waits until all computation has completed, regardless of when the compute instructions were issued. In practice it is a bad idea to use this operator unless absolutely necessary since it can lead to poor performance. * If we just want to wait until a specific variable is available we can call `z.wait_to_read()`. In this case MXNet blocks return to Python until the variable `z` has been computed. Other computation may well continue afterwards. @@ -179,8 +185,9 @@ with d2l.Benchmark('scalar conversion'): b.sum().item() ``` -:begin_tab:`mxnet` ## Improving Computation + +:begin_tab:`mxnet` On a heavily multithreaded system (even regular laptops have 4 threads or more and on multi-socket servers this number can exceed 256) the overhead of scheduling operations can become significant. This is why it is highly desirable to have computation and scheduling occur asynchronously and in parallel. To illustrate the benefit of doing so let us see what happens if we increment a variable by 1 multiple times, both in sequence or asynchronously. We simulate synchronous execution by inserting a `wait_to_read` barrier in between each addition. :end_tab: @@ -202,19 +209,21 @@ A slightly simplified interaction between the Python frontend thread and the C++ 1. The backend then receives the computation tasks from the queue and performs the actual computations. 1. The backend then returns the computation results to the frontend. Assume that the durations of these three stages are $t_1, t_2$ and $t_3$, respectively. If we do not use asynchronous programming, the total time taken to perform 10000 computations is approximately $10000 (t_1+ t_2 + t_3)$. If asynchronous programming is used, the total time taken to perform 10000 computations can be reduced to $t_1 + 10000 t_2 + t_3$ (assuming $10000 t_2 > 9999t_1$), since the frontend does not have to wait for the backend to return computation results for each loop. +:end_tab: ## Summary + * Deep learning frameworks may decouple the Python frontend from an execution backend. This allows for fast asynchronous insertion of commands into the backend and associated parallelism. * Asynchrony leads to a rather responsive frontend. However, use caution not to overfill the task queue since it may lead to excessive memory consumption. It is recommended to synchronize for each minibatch to keep frontend and backend approximately synchronized. * Chip vendors offer sophisticated performance analysis tools to obtain a much more fine-grained insight into the efficiency of deep learning. -:end_tab: :begin_tab:`mxnet` * Be aware of the fact that conversions from MXNet's memory management to Python will force the backend to wait until the specific variable is ready. Functions such as `print`, `asnumpy` and `item` all have this effect. This can be desirable but a careless use of synchronization can ruin performance. :end_tab: + ## Exercises :begin_tab:`mxnet` diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md index 331b1e1b3..15bdf9f9b 100644 --- a/chapter_computational-performance/auto-parallelism.md +++ b/chapter_computational-performance/auto-parallelism.md @@ -21,7 +21,7 @@ import torch ## GPU 上的并行计算 -让我们首先定义要测试的参考工作负载:下面的 `run` 函数使用分配到两个变量的数据在我们选择的设备上执行 10 个矩阵-矩阵乘法:`x_gpu1` 和 `x_gpu2`。 +让我们首先定义要测试的参考工作负载:下面的 `run` 函数使用分配给两个变量的数据在我们选择的设备上执行 10 次矩阵乘法:`x_gpu1` 和 `x_gpu2`。 ```{.python .input} devices = d2l.try_all_gpus() @@ -141,7 +141,7 @@ with d2l.Benchmark('Copy to CPU'): :end_tab: :begin_tab:`pytorch` -这有点效率低下。请注意,我们已经可以开始将 `y` 的部分复制到 CPU,而列表的其余部分仍在计算中。例如,当我们计算微型批次上的(backprop)渐变时,就会发生这种情况。其中一些参数的梯度将比其他参数的梯度更早提供。因此,在 GPU 仍在运行的同时开始使用 PCI-Express 总线带宽对我们有利。在 PyTorch 中,`to()` 和 `copy_()` 等多个函数都承认了一个明确的 `non_blocking` 参数,在不必要的情况下,调用者可以绕过同步。设置 `non_blocking=True` 允许我们模拟此场景。 +这有点效率低下。请注意,我们已经可以开始将 `y` 的部分复制到 CPU,而列表的其余部分仍在计算中。例如,当我们计算微型批次上的(backprop)渐变时,就会发生这种情况。其中一些参数的梯度将比其他参数的梯度更早提供。因此,在 GPU 仍在运行的同时开始使用 PCI-Express 总线带宽对我们有利。在 PyTorch 中,诸如 `to()` 和 `copy_()` 等多个函数都承认了一个明确的 `non_blocking` 参数,在不必要的情况下,调用者可以绕过同步。设置 `non_blocking=True` 允许我们模拟此场景。 :end_tab: ```{.python .input} diff --git a/chapter_computational-performance/hardware.md b/chapter_computational-performance/hardware.md new file mode 100644 index 000000000..4eae518b2 --- /dev/null +++ b/chapter_computational-performance/hardware.md @@ -0,0 +1,217 @@ +# 硬件 +:label:`sec_hardware` + +构建具有出色性能的系统需要对算法和模型有很好的了解,以捕捉问题的统计方面。同时,至少对底层硬件有一点了解也是不可或缺的。本节不能替代有关硬件和系统设计的适当课程。相反,它可以作为理解为什么某些算法比其他算法更有效以及如何实现良好吞吐量的起点。一个好的设计可以很容易地产生一个数量级的变化,而这反过来又可以在能够训练网络(例如,在一周内)和根本不能(在 3 个月内,因此错过了截止日期)之间的区别。我们首先看电脑。然后我们将放大以更仔细地查看 CPU 和 GPU。最后,我们缩小以查看服务器中心或云端中多台计算机是如何连接的。 + +![Latency Numbers that every programmer should know.](../img/latencynumbers.png) +:label:`fig_latencynumbers` + +不耐烦的读者可能能够用 :numref:`fig_latencynumbers` 来解决。它取自科林·斯科特的 [互动帖子](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html),该文章很好地概述了过去十年的进展。原来的数字来自杰夫·迪恩的 [Stanford talk from 2010](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/Stanford-DL-Nov-2010.pdf)。下面的讨论解释了这些数字的一些理由,以及它们如何指导我们设计算法。下面的讨论非常高级别和粗略。显然,它不能替代适当的课程 *,而只是为了为统计建模者提供足够的信息来做出合适的设计决策。有关计算机架构的深入概述,我们请读者参阅 :cite:`Hennessy.Patterson.2011` 或最近关于该主题的课程,例如 [Arste Asanovic] 的课程(http://inst.eecs.berkeley.edu/~cs152/sp19/)。 + +## 计算机 + +大多数深度学习研究人员和从业人员都可以使用具有相当数量的内存、计算、某种形式的加速器(如 GPU)或其倍数的计算机。计算机由以下关键组件组成: + +* 能够执行我们提供的程序的处理器(也称为 CPU)(除了运行操作系统和许多其他内容之外),通常由 8 个或更多内核组成。 +* 内存 (RAM) 用于存储和检索计算结果,例如体重矢量和激活以及训练数据。 +* 以太网网络连接(有时是多个),速度从 1 Gb/s 到 100 Gb/s。在高端服务器上,可以找到更高级的互连。 +* 用于将系统连接到一个或多个 GPU 的高速扩展总线 (PCIe)。服务器最多有 8 个加速器,通常以高级拓扑连接,而台式机系统则有 1 个或 2 个,具体取决于用户的预算和电源的大小。 +* 耐用存储,例如磁性硬盘驱动器、固态驱动器,在许多情况下都使用 PCIe 总线连接。它可以将训练数据高效地传输到系统,并根据需要存储中间检查站。 + +![Connectivity of components of a computer.](../img/mobo-symbol.svg) +:label:`fig_mobo-symbol` + +正如 :numref:`fig_mobo-symbol` 所示,大多数组件(网络、GPU 和存储)通过 PCIe 总线连接到 CPU。它由直接连接到 CPU 的多个通道组成。例如,AMD 的 Threadripper 3 有 64 个 PCIe 4.0 通道,每条通道都能在双向传输 16 Gbit/s 数据。内存直接连接到 CPU,总带宽高达 100 Gb/s。 + +当我们在计算机上运行代码时,我们需要将数据随机播放到处理器(CPU 或 GPU),执行计算,然后将结果从处理器移回 RAM 和耐用存储。因此,为了获得良好的性能,我们需要确保这种方法无缝工作,而不会任何一个系统成为主要瓶颈。例如,如果我们无法足够快地加载图像,处理器将无法做任何工作。同样,如果我们不能足够快地将矩阵移动到 CPU(或 GPU),其处理元素将会饿死。最后,如果我们想在网络中同步多台计算机,后者不应该减慢计算速度。一种选择是将沟通和计算交织在一起。让我们更详细地看看各个组件。 + +## 记忆 + +最基本的内存用于存储需要易于访问的数据。目前 CPU 内存通常是 [DDR4](https://en.wikipedia.org/wiki/DDR4_SDRAM) 种类型,每个模块提供 20—25 Gb/s 的带宽。每个模块都有一条 64 位宽的总线。通常使用对内存模块来允许多个通道。CPU 有 2 到 4 个内存通道,即它们的峰值内存带宽介于 4 0Gb/s 到 100 Gb/s 之间。通常每个渠道有两家银行。例如,AMD 的 Zen 3 Threadripper 有 8 个插槽。 + +尽管这些数字令人印象深刻,但事实上,它们只能讲述部分故事。当我们想从内存中读取一部分时,我们首先需要告诉内存模块在哪里可以找到信息。也就是说,我们首先需要将 * 地址 * 发送到 RAM。完成此操作后,我们可以选择只读一条 64 位记录或一系列记录。后者被称为 * 突发读数 *。简而言之,将地址发送到内存并设置传输大约需要 100 ns(详细信息取决于所用内存芯片的特定时序系数),每次后续传输只需 0.2 ns。简而言之,第一次读取是后续读取的 500 倍!请注意,我们每秒可以执行高达 10,000,000 次随机读取。这表明我们尽可能避免随机内存访问,而是使用突发读取(和写入)。 + +如果我们考虑到我们有多个 * 银行 *,事情就会复杂一些。每家银行可以基本上独立读取内存。这意味着两件事。一方面,只要随机读取在内存中均匀分布,有效随机读取次数最多可高 4 倍。这还意味着执行随机读取仍然是一个坏主意,因为突发读取速度也快了 4 倍。另一方面,由于内存对齐到 64 位边界,因此最好将任何数据结构与相同边界对齐。在设置适当的标志时,编译器几乎会做到这一点 [automatically](https://en.wikipedia.org/wiki/Data_structure_alignment)。鼓励好奇的读者查看关于 DRAM 的讲座,例如 [Zeshan Chishti] 的讲座 (http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf)。 + +GPU 内存受到更高的带宽要求,因为它们的处理元素比 CPU 多得多。总的来说,有两种选择可以解决这些问题。首先是使内存总线显著扩大。例如,NVIDIA 的 RTX 2080 Ti 有一条 352 位宽的总线。这允许同时传输更多信息。其次,GPU 使用特定的高性能内存。消费级设备(例如 NVIDIA 的 RTX 和 Titan 系列)通常使用 [GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM) 芯片,总带宽超过 500 Gb/s。另一种方法是使用 HBM(高带宽内存)模块。它们使用截然不同的界面,直接与专用硅片上的 GPU 连接。这使得它们非常昂贵,而且它们的使用通常仅限于高端服务器芯片,例如 NVIDIA Volta V100 系列加速器。毫不奇怪,由于前者的成本较高,GPU 内存通常比 CPU 内存小 *。出于我们的目的,他们的性能特征基本上相似,速度快得多。为了本书的目的,我们可以放心地忽略细节。它们只有在调整 GPU 内核以实现高吞吐量时才重要。 + +## 存储 + +我们看到 RAM 的一些关键特征是 * 带宽 * 和 * 延迟 *。存储设备也是如此,只是差异可能更加极端。 + +### 硬盘驱动器 + +*硬盘驱动器 *(HDD)已经使用了半个多世纪。简而言之,它们包含许多带头的旋转拼盘,可以放置在任何给定的轨道上读写。高端磁盘在 9 个磁盘上可容纳高达 16 TB。HDD 的主要优势之一是它们相对便宜。他们的许多缺点之一是他们典型的灾难性故障模式和相对较高的读取延迟。 + +要理解后者,请考虑一下 HDD 在 7,200 RPM 左右(每分钟转数)旋转的事实。如果速度快得多,由于对拼盘施加的离心力,它们就会破碎。在访问磁盘上的特定扇区时,这有一个重大缺点:我们需要等到磁盘旋转到位(我们可以移动磁盘但不能加速实际磁盘)。因此,在请求的数据可用之前,可能需要 8 毫秒以上。表达这一点的常见方法是说 HDD 可以以大约 100 个 IOP 运行(每秒输入/输出操作)。过去二十年来,这一数字基本上保持不变。更糟糕的是,增加带宽同样困难(大约为 100—200 MB/s)。毕竟,每个头都读取一条比特轨,因此比特率只能随信息密度的平方根进行缩放。因此,HDD 正在迅速降级为存档存储和非常大型数据集的低级存储。 + +### 固态硬盘 + +固态硬盘 (SSD) 使用闪存来持久存储信息。这允许快速 * 访问存储的记录。现代固态硬盘的运行速度可达 100,000 到 500,000 IOP,即比硬盘快 3 个数量级。此外,它们的带宽可以达到 1—3Gb/s,即比 HDD 快一个数量级。这些改进听起来几乎太好了,无法实现。事实上,由于固态硬盘的设计方式,它们附带了以下警告。 + +* SSD 将信息存储在块中(256 KB 或更大)。它们只能作为一个整体编写,这需要很长时间。因此,SSD 上的按位随机写入性能非常差。同样,写入数据通常需要很长时间,因为必须读取、删除区块,然后用新信息重写。到目前为止,SSD 控制器和固件已开发出算法来缓解这一尽管如此,写入速度可能会慢得多,特别是对于 QLC(四级单元)SSD。提高性能的关键是维持 * 队列 * 的操作,如果可能的话,更喜欢读取和写入大块。 +* 固态硬盘中的记忆细胞耗尽相对较快(通常在几千次写入之后就已经出现了)。磨损级保护算法能够将降解扩散到许多细胞中。也就是说,不建议使用 SSD 来交换文件或大型日志文件聚合。 +* 最后,带宽的大幅增加迫使计算机设计师将固态硬盘直接连接到 PCIe 总线。能够处理此问题的驱动器称为 nVMe(增强的非易失性存储器),最多可以使用 4 个 PCIe 通道。在 PCIe 4.0 上,这高达 8Gb/s。 + +### 云存储 + +云存储提供了一系列可配置的性能。也就是说,向虚拟机分配存储是动态的,无论是在数量还是在速度方面,都是由用户选择的。我们建议用户在延迟过高时(例如,在培训过程中使用许多小记录时)增加预配置的 IOP 数量。 + +## 中央处理器 + +中央处理单元 (CPU) 是任何计算机的核心。它们由许多关键组件组成:* 能够执行机器代码的处理器内核 *,* 总线 * 连接它们(特定拓扑结构在处理器型号、代和供应商之间有显著差异),以及 *Caches*,以实现比处理器更高的带宽和更低的延迟内存访问可以通过从主内存中读取。最后,几乎所有现代 CPU 都包含 * 矢量处理单元 *,以帮助高性能线性代数和卷数,因为它们在媒体处理和机器学习中很常见。 + +![Intel Skylake consumer quad-core CPU.](../img/skylake.svg) +:label:`fig_skylake` + +:numref:`fig_skylake` 描述了英特尔 Skylake 消费级四核 CPU。它有一个集成的 GPU、缓存和一个连接四个核心的环形总线。以太网、WiFi、蓝牙、SSD 控制器和 USB 等外围设备可以是芯片组的一部分或直接连接 (PCIe) 至 CPU。 + +### 微体系结构 + +每个处理器内核都由一组相当复杂的组件组成。尽管各代人和供应商之间的细节不同,但基本功能几乎是标准的。前端加载指令并尝试预测将采取哪条路径(例如,用于控制流程)。然后将指令从汇编代码解码为微指令。汇编代码通常不是处理器执行的最低级别的代码。相反,复杂的指令可能会被解码为一组更低级别的操作。然后,这些将由实际的执行核心处理。后者通常能够同时执行许多操作。例如,:numref:`fig_cortexa77` 的 ARM Cortex A77 核心能够同时执行多达 8 个操作。 + +![ARM Cortex A77 Microarchitecture.](../img/a77.svg) +:label:`fig_cortexa77` + +这意味着高效的程序可能能够在每个时钟周期执行多条指令,前提是它们可以独立执行。并非所有单位的创建都一样。一些专注于整数指令,而另一些则针对浮点性能进行了优化。为了提高吞吐量,处理器还可能在分支指令中同时遵循多条代码路径,然后丢弃未采用的分支的结果。这就是为什么分支预测单元(在前端)很重要,以至于只追求最有前途的途径。 + +### 矢量化 + +深度学习非常需要计算机。因此,要使 CPU 适合机器学习,需要在一个时钟周期内执行许多操作。这是通过矢量单位实现的。它们有不同的名称 : on ARM they are called NEON, on x86 they (a recent generation) are referred to as [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) units. A common aspect is that they are able to perform SIMD (single instruction multiple data) operations. :numref:`fig_neon128` 显示了如何在 ARM 上的一个时钟周期内添加 8 个短整数。 + +![128 bit NEON vectorization.](../img/neon128.svg) +:label:`fig_neon128` + +根据架构的选择,此类寄存器的长度最多为 512 位,允许最多 64 对数字的组合。例如,我们可能会乘以两个数字,然后将它们添加到第三个数字,也被称为融合乘法加。英特尔 [OpenVino](https://01.org/openvinotoolkit) 使用这些功能在服务器级 CPU 上实现深度学习的可观吞吐量。但是请注意,这个数字与 GPU 能够实现的目标完全相似。例如,NVIDIA 的 RTX 2080 Ti 拥有 4,352 个 CUDA 内核,每个内核都能随时处理此类操作。 + +### 缓存 + +考虑以下情况 : we have a modest CPU core with 4 cores as depicted in :numref:`fig_skylake`,以 2 GHz 频率运行。此外,让我们假设 IPC(每时钟指令)计数为 1,并且这些单元具有启用 256 位宽度的 AVX2。让我们进一步假设至少有一个用于 AVX2 操作的寄存器需要从内存中检索。这意味着 CPU 每时钟周期消耗 $4 \times 256 \text{ bit} = 128 \text{ bytes}$ 个数据。除非我们能够每秒向处理器传输 $2 \times 10^9 \times 128 = 256 \times 10^9$ 字节,否则处理元素将会饿死。不幸的是,这种芯片的存储器接口仅支持 20—40 Gb/s 的数据传输,即少一个数量级。修复方法是尽可能避免从内存中加载 *new* 数据,而是将其缓存在 CPU 本地。这就是缓存派上用场的地方。通常使用以下名称或概念: + +* ** 注册器 ** 严格来说不是缓存的一部分。他们帮助舞台说明。也就是说,CPU 寄存器是 CPU 可以以时钟速度访问的内存位置,而不会造成任何延迟损失。CPU 有数十个寄存器。有效地使用寄存器取决于编译器(或程序员)。例如,C 编程语言有一个 `register` 关键字。 +* **L1 缓存 ** 是抵御高内存带宽要求的第一道防线。L1 缓存很小(典型大小可能为 32—64 KB),通常分为数据缓存和指令缓存。当在 L1 缓存中找到数据时,访问速度非常快。如果在那里找不到它们,则搜索将在缓存层次结构中向下进行。 +* **L2 缓存 ** 是下一站。根据架构设计和处理器尺寸,它们可能是独家的。它们可能只能由给定的内核访问,也可能在多个内核之间共享。二级缓存更大(通常每核 256—512 KB),慢于 L1。此外,要访问 L2 中的内容,我们首先需要检查以意识到数据不在 L1 中,这会增加少量额外的延迟。 +* **L3 缓存 ** 在多个核心之间共享,可能很大。AMD 的 Epyc 3 服务器 CPU 有高达 256 MB 的高速缓存分布在多个数字中。更典型的数字在 4-8 MB 范围内。 + +预测接下来需要哪些内存元素是芯片设计中的关键优化参数之一。例如,建议以 *forward* 方向遍历内存,因为大多数缓存算法都会尝试 * 向前读取 * 而不是向后读。同样,将内存访问模式保持在本地也是提高性能的好方法。 + +添加缓存是一把双刃剑。一方面,他们确保处理器内核不会缺少数据。与此同时,它们增加芯片尺寸,耗用了本可用于提高处理能力的面积。此外,* 缓存未命中 * 可能会很昂贵。考虑 :numref:`fig_falsesharing` 中描述的最坏情况,* 虚假共享 *。当处理器 1 上的线程请求数据时,内存位置将缓存在处理器 0 上。要获得它,处理器 0 需要停止正在执行的操作,将信息写回主内存,然后让处理器 1 从内存中读取信息。在此操作期间,两个处理器都等与高效的单处理器实现相比,这样的代码在多个处理器上运行速度很可能更慢 *。这是为什么缓存大小(除了物理大小外)有实际限制的又一个原因。 + +![False sharing (image courtesy of Intel).](../img/falsesharing.svg) +:label:`fig_falsesharing` + +## GPU 和其他加速器 + +声称如果没有 GPU,深度学习就不会成功,这并不夸张。同样,可以合理地争辩说,由于深度学习,GPU 制造商的财富大幅度增加。硬件和算法的共同演变导致了这样一种情况,即深度学习更好或坏是更好的学习是最好的统计建模范式。因此,了解 GPU 和相关加速器(如 TPU :cite:`Jouppi.Young.Patil.ea.2017`)的具体优势是值得的。 + +值得注意的是,在实践中经常作出的区别:加速器已针对训练或推理进行了优化。对于后者,我们只需要计算网络中的正向传播。反向传播不需要存储中间数据。此外,我们可能不需要非常精确的计算(FP16 或 INT8 通常就足够了)。另一方面,在训练期间,所有中间结果都需要存储才能计算梯度。此外,累积梯度需要更高的精度以避免数字下溢(或溢出)。这意味着 FP16(或与 FP32 混合精度)是最低要求。所有这些都需要更快、更大的内存(HBM2 与 GDDR6)和更大的处理能力。例如,NVIDIA 的 [Turing](https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/) T4 GPU 针对推理进行了优化,而 V100 GPU 更适合培训。 + +回想一下 :numref:`fig_neon128` 中所示的矢量化。向处理器内核添加矢量单元使我们能够显著提高吞吐量。例如,在 :numref:`fig_neon128` 的示例中,我们能够同时执行 16 个操作。首先,如果我们添加的运算不仅优化了向量之间的运算,而且也优化矩阵之间的运算会怎么样?这一策略导致了张量核心(很快将涵盖)。第二,如果我们添加更多的核心怎么办?简而言之,这两种策略总结了 GPU 中的设计决策。:numref:`fig_turing_processing_block` 概述了基本的处理模块。它包含 16 个整数和 16 个浮点型单位。除此之外,两个 tensor 内核加速了与深度学习相关的少数额外操作的子集。每个流媒体多处理器由四个这样的模块组成。 + +![NVIDIA Turing processing block (image courtesy of NVIDIA).](../img/turing-processing-block.png) +:width:`150px` +:label:`fig_turing_processing_block` + +接下来,12 个流式多处理器被分为构成高端 TU102 处理器的图形处理群集。充足的内存通道和二级缓存补充了设置。:numref:`fig_turing` 提供了相关的细节。设计这样一个器件的原因之一是,可以根据需要添加或移除单个模块,以允许更紧凑的芯片和处理良率问题(可能无法激活故障模块)。幸运的是,在 CUDA 和框架代码层下,对于休闲的深度学习研究员来说,编程这些设备完全隐藏在一起。特别是,如果有可用资源,可以在 GPU 上同时执行多个程序。尽管如此,必须注意设备的局限性,以避免选择不适合设备内存的型号。 + +![NVIDIA Turing architecture (image courtesy of NVIDIA)](../img/turing.png) +:width:`350px` +:label:`fig_turing` + +值得更详细地提及的最后一个方面是 * 张量核心 *。它们是最近增加对深度学习特别有效的优化电路的趋势的一个例子。例如,TPU 为快速矩阵乘法添加了收缩压阵列 :cite:`Kung.1988`。那里的设计是为了支持极少的大型操作(第一代 TPU)。Tensor 核心在另一端。它们针对涉及 $16 \times 16$ 和 $16 \times 16$ 矩阵的小型操作进行了优化,具体取决于它们的数值精度。:numref:`fig_tensorcore` 概述了优化。 + +![NVIDIA tensor cores in Turing (image courtesy of NVIDIA).](../img/tensorcore.jpg) +:width:`400px` +:label:`fig_tensorcore` + +显然,在优化计算时,我们最终会作出某些妥协。其中之一是 GPU 不擅长处理中断和稀疏数据。尽管存在明显的例外,例如 [Gunrock](https://github.com/gunrock/gunrock) :cite:`Wang.Davidson.Pan.ea.2016`,稀疏矩阵和向量的访问模式与 GPU 出色的高带宽突发读取操作不太好。匹配这两个目标是积极研究的一个领域。例如,请参阅 [DGL](http://dgl.ai),这是一个专为图表进行深度学习而调整的图书馆。 + +## 网络和公共汽车 + +每当单个设备不足以进行优化时,我们都需要将数据传入和传出该设备来同步处理。这是网络和公共汽车派上用场的地方。我们有许多设计参数:带宽、成本、距离和灵活性。一方面,我们的 WiFi 范围相当不错,非常容易使用(毕竟没有电线),价格便宜,但它提供了相对平庸的带宽和延迟。没有任何机器学习研究人员都不会用它来构建服务器群集。在接下来的内容中,我们重点介绍了适合深度学习的互连。 + +* **PCIe** 是专用总线,用于每条通道的极高带宽点对点连接(在 PCIe 4.0 上,16 通道插槽中的 PCIe 4.0 最高可达 32 Gb/s)。延迟的顺序为个位数微秒(5 μs)。PCIe 链接很宝贵。处理器的数量有限:AMD 的 EPYC 3 有 128 个通道,英特尔的至强每芯片最多有 48 条通道;在台式机级 CPU 上,数字分别为 20(锐龙 9)和 16 条(酷睿 i9)。由于 GPU 通常有 16 条通道,因此这限制了能够以全带宽连接到 CPU 的 GPU 的数量。毕竟,他们需要与存储和以太网等其他高带宽外围设备共享链路。就像 RAM 访问一样,由于数据包开销降低,大批量传输更为可取。 +* ** Ethernet** 是连接计算机的最常用方式。虽然它比 PCIe 慢得多,但它的安装非常便宜且有弹性,并且覆盖的距离要长得多。低级服务器的典型带宽为 1 Gbit/s。高端设备(例如云中的 [C5 instances](https://aws.amazon.com/ec2/instance-types/c5/))提供 10 到 100 Gbit/s 的带宽。与以前的所有情况一样,数据传输都有巨大的间接费请注意,我们几乎从来不直接使用原始以太网,而是在物理互连之上执行的协议(例如 UDP 或 TCP/IP)。这进一步增加了开销。像 PCIe 一样,以太网设计用于连接两台设备,例如计算机和交换机。 +* **Switch ** 允许我们以任何一对设备同时进行(通常为满带宽)点对点连接的方式连接多台设备。例如,以太网交换机可能会以较高的横截面带宽连接 40 台服务器。请注意,交换机并不是传统计算机网络所独有的。即使是 PCIe 车道也可以是 [switched](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。例如,将大量 GPU 连接到主机处理器时,就会发生这种情况,就像 [P2 instances](https://aws.amazon.com/ec2/instance-types/p2/) 那样。 +* **nvlink** 在非常高带宽的互连方面是 PCIe 的替代方案。它每个链路提供高达 300 Gbit/s 的数据传输速率。服务器 GPU (Volta V100) 有六个链路,而消费级 GPU (RTX 2080 Ti) 只有一条链路,以降低 100 Gbit/s 的速率运行。我们建议使用 [NCCL](https://github.com/NVIDIA/nccl) 来实现 GPU 之间的高数据传输。 + +## 更多延迟数字 + +:numref:`table_latency_numbers` 和 :numref:`table_latency_numbers_tesla` 中的摘要来自 [Eliot Eshelman](https://gist.github.com/eshelman),他将这些数字的更新版本维持为 [GitHub gist](https://gist.github.com/eshelman/343a1c46cb3fba142c1afdcdeec17646)。 + +: 常见延迟数字。 + +| Action | Time | Notes | +| :----------------------------------------- | -----: | :---------------------------------------------- | +| L1 cache reference/hit | 1.5 ns | 4 cycles | +| Floating-point add/mult/FMA | 1.5 ns | 4 cycles | +| L2 cache reference/hit | 5 ns | 12 ~ 17 cycles | +| Branch mispredict | 6 ns | 15 ~ 20 cycles | +| L3 cache hit (unshared cache) | 16 ns | 42 cycles | +| L3 cache hit (shared in another core) | 25 ns | 65 cycles | +| Mutex lock/unlock | 25 ns | | +| L3 cache hit (modified in another core) | 29 ns | 75 cycles | +| L3 cache hit (on a remote CPU socket) | 40 ns | 100 ~ 300 cycles (40 ~ 116 ns) | +| QPI hop to a another CPU (per hop) | 40 ns | | +| 64MB memory ref. (local CPU) | 46 ns | TinyMemBench on Broadwell E5-2690v4 | +| 64MB memory ref. (remote CPU) | 70 ns | TinyMemBench on Broadwell E5-2690v4 | +| 256MB memory ref. (local CPU) | 75 ns | TinyMemBench on Broadwell E5-2690v4 | +| Intel Optane random write | 94 ns | UCSD Non-Volatile Systems Lab | +| 256MB memory ref. (remote CPU) | 120 ns | TinyMemBench on Broadwell E5-2690v4 | +| Intel Optane random read | 305 ns | UCSD Non-Volatile Systems Lab | +| Send 4KB over 100 Gbps HPC fabric | 1 μs | MVAPICH2 over Intel Omni-Path | +| Compress 1KB with Google Snappy | 3 μs | | +| Send 4KB over 10 Gbps ethernet | 10 μs | | +| Write 4KB randomly to NVMe SSD | 30 μs | DC P3608 NVMe SSD (QOS 99% is 500μs) | +| Transfer 1MB to/from NVLink GPU | 30 μs | ~33GB/s on NVIDIA 40GB NVLink | +| Transfer 1MB to/from PCI-E GPU | 80 μs | ~12GB/s on PCIe 3.0 x16 link | +| Read 4KB randomly from NVMe SSD | 120 μs | DC P3608 NVMe SSD (QOS 99%) | +| Read 1MB sequentially from NVMe SSD | 208 μs | ~4.8GB/s DC P3608 NVMe SSD | +| Write 4KB randomly to SATA SSD | 500 μs | DC S3510 SATA SSD (QOS 99.9%) | +| Read 4KB randomly from SATA SSD | 500 μs | DC S3510 SATA SSD (QOS 99.9%) | +| Round trip within same datacenter | 500 μs | One-way ping is ~250μs | +| Read 1MB sequentially from SATA SSD | 2 ms | ~550MB/s DC S3510 SATA SSD | +| Read 1MB sequentially from disk | 5 ms | ~200MB/s server HDD | +| Random Disk Access (seek+rotation) | 10 ms | | +| Send packet CA->Netherlands->CA | 150 ms | | +:label:`table_latency_numbers` + +: NVIDIA Tesla GPU 的延迟数字。 + +| Action | Time | Notes | +| :------------------------------ | -----: | :---------------------------------------- | +| GPU Shared Memory access | 30 ns | 30~90 cycles (bank conflicts add latency) | +| GPU Global Memory access | 200 ns | 200~800 cycles | +| Launch CUDA kernel on GPU | 10 μs | Host CPU instructs GPU to start kernel | +| Transfer 1MB to/from NVLink GPU | 30 μs | ~33GB/s on NVIDIA 40GB NVLink | +| Transfer 1MB to/from PCI-E GPU | 80 μs | ~12GB/s on PCI-Express x16 link | +:label:`table_latency_numbers_tesla` + +## 摘要 + +* 设备有操作开销。因此,重要的是要瞄准少量大量转账,而不是许多小转账。这适用于 RAM、SSD、网络和 GPU。 +* 矢量化是性能的关键。确保你知道加速器的具体能力。例如,一些英特尔至强 CPU 对 INT8 操作特别有用,NVIDIA Volta GPU 在 FP16 矩阵矩阵操作中表现出色,NVIDIA Timon 在 FP16、INT8 和 INT4 操作中出色。 +* 由于数据类型较小而导致的数值溢出可能是训练期间的问题(以及在推理期间较小程度上)。 +* 别名可能会显著降低性能。例如,64 位 CPU 上的内存对齐应该针对 64 位边界进行。在 GPU 上,保持卷积大小对齐是个好主意,例如,与张量核心保持一致。 +* 将算法与硬件相匹配(例如,内存占用量和带宽)。将参数调整到缓存中时,可以实现极大的加速(数量级)。 +* 我们建议您在验证实验结果之前先在纸上勾画出一种新颖算法的性能。数量级或更多的差异是令人担忧的原因。 +* 使用分析器调试性能瓶颈。 +* 培训和推理硬件在价格和性能方面有不同的甜点。 + +## 练习 + +1. 编写 C 代码来测试访问相对于外部存储器接口对齐或未对齐内存之间的速度是否存在任何差异。提示:小心缓存效果。 +1. 测试按顺序访问内存或按给定步长访问内存之间的速度差异。 +1. 你怎么能测量 CPU 上的缓存大小? +1. 您将如何在多个内存通道之间布局数据以获得最大带宽?如果你有很多小线程你会怎么布局? +1. 企业级硬盘以 10,000 rpm 的速度旋转。HDD 在读取数据之前花最坏情况的绝对最短时间是多少(你可以假设头部几乎瞬间移动)?为什么 2.5 英寸硬盘在商业服务器中变得流行(相对于 3.5 英寸和 5.25 英寸驱动器)? +1. 假设硬盘制造商将存储密度从每平方英寸 1 Tbit 增加到每平方英寸 5 Tbit。你可以在 2.5 英寸硬盘上的戒指上存储多少信息?内部和外部轨道之间有区别吗? +1. 从 8 位到 16 位数据类型将芯片的数量增加大约四倍。为什么?为什么 NVIDIA 会将 INT4 操作添加到他们的图灵 GPU 中? +1. 与向后读取相比,通过内存向前读取的速度要快多少?不同计算机和 CPU 供应商之间该数字是否有所不同?为什么?编写 C 代码然后进行试验。 +1. 你能测量磁盘的缓存大小吗?典型的硬盘是什么?SSD 需要缓存吗? +1. 测量通过以太网发送消息时的数据包开销。查找 UDP 和 TCP/IP 连接之间的区别。 +1. 直接内存访问允许 CPU 以外的设备直接向(从)内存中写入(和读取)。为什么这是个好主意? +1. 看看图灵 T4 GPU 的性能数字。为什么从 FP16 到 INT8 和 INT4 时,性能 “仅” 翻了一番? +1. 旧金山和阿姆斯特丹之间的往返旅行应该最短的时间是多少?提示:你可以假设距离是 10,000 公里。 + +[Discussions](https://discuss.d2l.ai/t/363) diff --git a/chapter_computational-performance/hardware_origin.md b/chapter_computational-performance/hardware_origin.md new file mode 100644 index 000000000..744c52819 --- /dev/null +++ b/chapter_computational-performance/hardware_origin.md @@ -0,0 +1,232 @@ +# Hardware +:label:`sec_hardware` + +Building systems with great performance requires a good understanding of the algorithms and models to capture the statistical aspects of the problem. At the same time it is also indispensable to have at least a modicum of knowledge of the underlying hardware. The current section is no substitute for a proper course on hardware and system design. Instead, it might serve as a starting point for understanding why some algorithms are more efficient than others and how to achieve good throughput. A good design can easily make a difference of an order of magnitude and, in turn, this can make the difference between being able to train a network (e.g., in a week) and not at all (in 3 months, thus missing the deadline). +We will start by looking at computers. Then we will zoom in to look more carefully at CPUs and GPUs. Lastly we zoom out to review how multiple computers are connected in a server center or in the cloud. + +![Latency Numbers that every programmer should know.](../img/latencynumbers.png) +:label:`fig_latencynumbers` + +Impatient readers may be able to get by with :numref:`fig_latencynumbers`. It is taken from Colin Scott's [interactive post](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html) that gives a good overview of the progress over the past decade. The original numbers are due to Jeff Dean's [Stanford talk from 2010](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/Stanford-DL-Nov-2010.pdf). +The discussion below explains some of the rationale for these numbers and how they can guide us in designing algorithms. The discussion below is very high level and cursory. It is clearly *no substitute* for a proper course but rather just meant to provide enough information for a statistical modeler to make suitable design decisions. For an in-depth overview of computer architecture we refer the reader to :cite:`Hennessy.Patterson.2011` or a recent course on the subject, such as the one by [Arste Asanovic](http://inst.eecs.berkeley.edu/~cs152/sp19/). + +## Computers + +Most deep learning researchers and practitioners have access to a computer with a fair amount of memory, computation, some form of an accelerator such as a GPU, or multiples thereof. A computer consists of the following key components: + +* A processor (also referred to as a CPU) that is able to execute the programs we give it (in addition to running an operating system and many other things), typically consisting of 8 or more cores. +* Memory (RAM) to store and retrieve the results from computation, such as weight vectors and activations, and training data. +* An Ethernet network connection (sometimes multiple) with speeds ranging from 1 GB/s to 100 GB/s. On high end servers more advanced interconnects can be found. +* A high speed expansion bus (PCIe) to connect the system to one or more GPUs. Servers have up to 8 accelerators, often connected in an advanced topology, while desktop systems have 1 or 2, depending on the budget of the user and the size of the power supply. +* Durable storage, such as a magnetic hard disk drive, a solid state drive, in many cases connected using the PCIe bus. It provides efficient transfer of training data to the system and storage of intermediate checkpoints as needed. + +![Connectivity of components of a computer.](../img/mobo-symbol.svg) +:label:`fig_mobo-symbol` + +As :numref:`fig_mobo-symbol` indicates, most components (network, GPU, and storage) are connected to the CPU across the PCIe bus. It consists of multiple lanes that are directly attached to the CPU. For instance AMD's Threadripper 3 has 64 PCIe 4.0 lanes, each of which is capable 16 Gbit/s data transfer in both directions. The memory is directly attached to the CPU with a total bandwidth of up to 100 GB/s. + +When we run code on a computer we need to shuffle data to the processors (CPUs or GPUs), perform computation, and then move the results off the processor back to RAM and durable storage. Hence, in order to get good performance we need to make sure that this works seamlessly without any one of the systems becoming a major bottleneck. For instance, if we cannot load images quickly enough the processor will not have any work to do. Likewise, if we cannot move matrices quickly enough to the CPU (or GPU), its processing elements will starve. Finally, if we want to synchronize multiple computers across the network, the latter should not slow down computation. One option is to interleave communication and computation. Let us have a look at the various components in more detail. + + +## Memory + +At its most basic memory is used to store data that needs to be readily accessible. At present CPU RAM is typically of the [DDR4](https://en.wikipedia.org/wiki/DDR4_SDRAM) variety, offering 20--25 GB/s bandwidth per module. Each module has a 64-bit-wide bus. Typically pairs of memory modules are used to allow for multiple channels. CPUs have between 2 and 4 memory channels, i.e., they have between 4 0GB/s and 100 GB/s peak memory bandwidth. Often there are two banks per channel. For instance AMD's Zen 3 Threadripper has 8 slots. + +While these numbers are impressive, indeed, they only tell part of the story. When we want to read a portion from memory we first need to tell the memory module where the information can be found. That is, we first need to send the *address* to RAM. Once this is accomplished we can choose to read just a single 64 bit record or a long sequence of records. The latter is called *burst read*. In a nutshell, sending an address to memory and setting up the transfer takes approximately 100 ns (details depend on the specific timing coefficients of the memory chips used), every subsequent transfer takes only 0.2 ns. In short, the first read is 500 times as expensive as subsequent ones! Note that we could perform up to 10,000,000 random reads per second. This suggests that we avoid random memory access as far as possible and use burst reads (and writes) instead. + +Matters are a bit more complex when we take into account that we have multiple *banks*. Each bank can read memory largely independently. This means two things. +On one hand, the effective number of random reads is up to 4 times higher, provided that they are spread evenly across memory. It also means that it is still a bad idea to perform random reads since burst reads are 4 times faster, too. On the other hand, due to memory alignment to 64 bit boundaries it is a good idea to align any data structures with the same boundaries. Compilers do this pretty much [automatically](https://en.wikipedia.org/wiki/Data_structure_alignment) when the appropriate flags are set. Curious readers are encouraged to review a lecture on DRAMs such as the one by [Zeshan Chishti](http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf). + +GPU memory is subject to even higher bandwidth requirements since they have many more processing elements than CPUs. By and large there are two options to address them. The first is to make the memory bus significantly wider. For instance, NVIDIA's RTX 2080 Ti has a 352-bit-wide bus. This allows for much more information to be transferred at the same time. Second, GPUs use specific high-performance memory. Consumer-grade devices, such as NVIDIA's RTX and Titan series typically use [GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM) chips with over 500 GB/s aggregate bandwidth. An alternative is to use HBM (high bandwidth memory) modules. They use a very different interface and connect directly with GPUs on a dedicated silicon wafer. This makes them very expensive and their use is typically limited to high-end server chips, such as the NVIDIA Volta V100 series of accelerators. Quite unsurprisingly, GPU memory is generally *much* smaller than CPU memory due to the higher cost of the former. For our purposes, by and large their performance characteristics are similar, just a lot faster. We can safely ignore the details for the purpose of this book. They only matter when tuning GPU kernels for high throughput. + +## Storage + +We saw that some of the key characteristics of RAM are *bandwidth* and *latency*. The same is true for storage devices, just that the differences can be even more extreme. + +### Hard Disk Drives + +*Hard disk drives* (HDDs) have been in use for over half a century. In a nutshell they contain a number of spinning platters with heads that can be positioned to read or write at any given track. High-end disks hold up to 16 TB on 9 platters. One of the key benefits of HDDs is that they are relatively inexpensive. One of their many downsides are their typically catastrophic failure modes and their relatively high read latency. + +To understand the latter, consider the fact that HDDs spin at around 7,200 RPM (revolutions per minute). If they were much faster they would shatter due to the centrifugal force exerted on the platters. This has a major downside when it comes to accessing a specific sector on the disk: we need to wait until the platter has rotated in position (we can move the heads but not accelerate the actual disks). Hence it can take over 8 ms until the requested data are available. A common way this is expressed is to say that HDDs can operate at approximately 100 IOPs (input/output operations per second). This number has essentially remained unchanged for the past two decades. Worse still, it is equally difficult to increase bandwidth (it is in the order of 100--200 MB/s). After all, each head reads a track of bits, hence the bit rate only scales with the square root of the information density. As a result, HDDs are quickly becoming relegated to archival storage and low-grade storage for very large datasets. + + +### Solid State Drives + +Solid state drives (SSDs) use flash memory to store information persistently. This allows for *much faster* access to stored records. Modern SSDs can operate at 100,000 to 500,000 IOPs, i.e., up to 3 orders of magnitude faster than HDDs. Furthermore, their bandwidth can reach 1--3GB/s, i.e., one order of magnitude faster than HDDs. These improvements sound almost too good to be true. Indeed, they come with the following caveats, due to the way SSDs are designed. + +* SSDs store information in blocks (256 KB or larger). They can only be written as a whole, which takes significant time. Consequently bit-wise random writes on SSD have very poor performance. Likewise, writing data in general takes significant time since the block has to be read, erased and then rewritten with new information. By now SSD controllers and firmware have developed algorithms to mitigate this. Nonetheless, writes can be much slower, in particular for QLC (quad level cell) SSDs. The key for improved performance is to maintain a *queue* of operations, to prefer reads and to write in large blocks if possible. +* The memory cells in SSDs wear out relatively quickly (often already after a few thousand writes). Wear-level protection algorithms are able to spread the degradation over many cells. That said, it is not recommended to use SSDs for swapping files or for large aggregations of log-files. +* Lastly, the massive increase in bandwidth has forced computer designers to attach SSDs directly to the PCIe bus. The drives capable of handling this, referred to as NVMe (Non Volatile Memory enhanced), can use up to 4 PCIe lanes. This amounts to up to 8GB/s on PCIe 4.0. + +### Cloud Storage + +Cloud storage provides a configurable range of performance. That is, the assignment of storage to virtual machines is dynamic, both in terms of quantity and in terms of speed, as chosen by users. We recommend that users increase the provisioned number of IOPs whenever latency is too high, e.g., during training with many small records. + +## CPUs + +Central processing units (CPUs) are the centerpiece of any computer. They consist of a number of key components: *processor cores* that are able to execute machine code, a *bus* connecting them (the specific topology differs significantly between processor models, generations, and vendors), and *caches* to allow for higher bandwidth and lower latency memory access than what is possible by reads from main memory. Lastly, almost all modern CPUs contain *vector processing units* to aid with high performance linear algebra and convolutions, as they are common in media processing and machine learning. + +![Intel Skylake consumer quad-core CPU.](../img/skylake.svg) +:label:`fig_skylake` + +:numref:`fig_skylake` depicts an Intel Skylake consumer-grade quad-core CPU. It has an integrated GPU, caches, and a ringbus connecting the four cores. Peripherals, such as Ethernet, WiFi, Bluetooth, SSD controller, and USB, are either part of the chipset or directly attached (PCIe) to the CPU. + + +### Microarchitecture + +Each of the processor cores consists of a rather sophisticated set of components. While details differ between generations and vendors, the basic functionality is pretty much standard. The front-end loads instructions and tries to predict which path will be taken (e.g., for control flow). Instructions are then decoded from assembly code to microinstructions. Assembly code is often not the lowest level code that a processor executes. Instead, complex instructions may be decoded into a set of more lower level operations. These are then processed by the actual execution core. Often the latter is capable of performing many operations simultaneously. For instance, the ARM Cortex A77 core of :numref:`fig_cortexa77` is able to perform up to 8 operations simultaneously. + +![ARM Cortex A77 Microarchitecture.](../img/a77.svg) +:label:`fig_cortexa77` + +This means that efficient programs might be able to perform more than one instruction per clock cycle, provided that they can be carried out independently. Not all units are created equal. Some specialize in integer instructions whereas others are optimized for floating point performance. To increase throughput, the processor might also follow multiple code paths simultaneously in a branching instruction and then discard the results of the branches not taken. This is why branch prediction units matter (on the front-end) such that only the most promising paths are pursued. + +### Vectorization + +Deep learning is extremely compute-hungry. Hence, to make CPUs suitable for machine learning, one needs to perform many operations in one clock cycle. This is achieved via vector units. They have different names: on ARM they are called NEON, on x86 they (a recent generation) are referred to as [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) units. A common aspect is that they are able to perform SIMD (single instruction multiple data) operations. :numref:`fig_neon128` shows how 8 short integers can be added in one clock cycle on ARM. + +![128 bit NEON vectorization.](../img/neon128.svg) +:label:`fig_neon128` + +Depending on architecture choices, such registers are up to 512 bits long, allowing for the combination of up to 64 pairs of numbers. For instance, we might be multiplying two numbers and adding them to a third, which is also known as a fused multiply-add. Intel's [OpenVino](https://01.org/openvinotoolkit) uses these to achieve respectable throughput for deep learning on server-grade CPUs. Note, though, that this number is entirely dwarfed by what GPUs are capable of achieving. For instance, NVIDIA's RTX 2080 Ti has 4,352 CUDA cores, each of which is capable of processing such an operation at any time. + +### Cache + +Consider the following situation: we have a modest CPU core with 4 cores as depicted in :numref:`fig_skylake` above, running at 2 GHz frequency. +Moreover, let us assume that we have an IPC (instructions per clock) count of 1 and that the units have AVX2 with 256-bit width enabled. Let us furthermore assume that at least one of the registers used for AVX2 operations needs to be retrieved from memory. This means that the CPU consumes $4 \times 256 \text{ bit} = 128 \text{ bytes}$ of data per clock cycle. Unless we are able to transfer $2 \times 10^9 \times 128 = 256 \times 10^9$ bytes to the processor per second the processing elements are going to starve. Unfortunately the memory interface of such a chip only supports 20--40 GB/s data transfer, i.e., one order of magnitude less. The fix is to avoid loading *new* data from memory as far as possible and rather to cache it locally on the CPU. This is where caches come in handy. Commonly the following names or concepts are used: + +* **Registers** are strictly speaking not part of the cache. They help stage instructions. That said, CPU registers are memory locations that a CPU can access at clock speed without any delay penalty. CPUs have tens of registers. It is up to the compiler (or programmer) to use registers efficiently. For instance the C programming language has a `register` keyword. +* **L1 caches** are the first line of defense against high memory bandwidth requirements. L1 caches are tiny (typical sizes might be 32--64 KB) and often split into data and instructions caches. When data are found in the L1 cache, access is very fast. If they cannot be found there, the search progresses down the cache hierarchy. +* **L2 caches** are the next stop. Depending on architecture design and processor size they might be exclusive. They might be accessible only by a given core or shared among multiple cores. L2 caches are larger (typically 256--512 KB per core) and slower than L1. Furthermore, to access something in L2 we first need to check to realize that the data are not in L1, which adds a small amount of extra latency. +* **L3 caches** are shared among multiple cores and can be quite large. AMD's Epyc 3 server CPUs have a whopping 256 MB of cache spread across multiple chiplets. More typical numbers are in the 4--8 MB range. + +Predicting which memory elements will be needed next is one of the key optimization parameters in chip design. For instance, it is advisable to traverse memory in a *forward* direction since most caching algorithms will try to *read ahead* rather than backwards. Likewise, keeping memory access patterns local is a good way of improving performance. + +Adding caches is a double-edge sword. On one hand they ensure that the processor cores do not starve of data. At the same time they increase chip size, using up area that otherwise could have been spent on increasing processing power. Moreover, *cache misses* can be expensive. Consider the worst case scenario, *false sharing*, as depicted in :numref:`fig_falsesharing`. A memory location is cached on processor 0 when a thread on processor 1 requests the data. To obtain it, processor 0 needs to stop what it is doing, write the information back to main memory and then let processor 1 read it from memory. During this operation both processors wait. Quite potentially such code runs *more slowly* on multiple processors when compared with an efficient single-processor implementation. This is one more reason for why there is a practical limit to cache sizes (besides their physical size). + +![False sharing (image courtesy of Intel).](../img/falsesharing.svg) +:label:`fig_falsesharing` + +## GPUs and other Accelerators + +It is not an exaggeration to claim that deep learning would not have been successful without GPUs. By the same token, it is quite reasonable to argue that GPU manufacturers' fortunes have increased significantly due to deep learning. This co-evolution of hardware and algorithms has led to a situation where for better or worse deep learning is the preferable statistical modeling paradigm. Hence it pays to understand the specific benefits that GPUs and related accelerators such as the TPU :cite:`Jouppi.Young.Patil.ea.2017`. + +Of note is a distinction that is often made in practice: accelerators are optimized either for training or inference. For the latter we only need to compute the forward propagation in a network. No storage of intermediate data is needed for backpropagation. Moreover, we may not need very precise computation (FP16 or INT8 typically suffice). On the other hand, during training all intermediate results need storage to compute gradients. Moreover, accumulating gradients requires higher precision to avoid numerical underflow (or overflow). This means that FP16 (or mixed precision with FP32) is the minimum requirement. All of this necessitates faster and larger memory (HBM2 vs. GDDR6) and more processing power. For instance, NVIDIA's [Turing](https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/) T4 GPUs are optimized for inference whereas the V100 GPUs are preferable for training. + +Recall vectorization as illustrated in :numref:`fig_neon128`. Adding vector units to a processor core allowed us to increase throughput significantly. For example, in the example in :numref:`fig_neon128` we were able to perform 16 operations simultaneously. +First, +what if we added operations that optimized not just operations between vectors but also between matrices? This strategy led to tensor cores (to be covered shortly). +Second, what if we added many more cores? In a nutshell, these two strategies summarize the design decisions in GPUs. :numref:`fig_turing_processing_block` gives an overview of a basic processing block. It contains 16 integer and 16 floating point units. In addition to that, two tensor cores accelerate a narrow subset of additional operations relevant for deep learning. Each streaming multiprocessor consists of four such blocks. + +![NVIDIA Turing processing block (image courtesy of NVIDIA).](../img/turing-processing-block.png) +:width:`150px` +:label:`fig_turing_processing_block` + +Next, 12 streaming multiprocessors are grouped into graphics processing clusters which make up the high-end TU102 processors. Ample memory channels and an L2 cache complement the setup. :numref:`fig_turing` has the relevant details. One of the reasons for designing such a device is that individual blocks can be added or removed as needed to allow for more compact chips and to deal with yield issues (faulty modules might not be activated). Fortunately programming such devices is well hidden from the casual deep learning researcher beneath layers of CUDA and framework code. In particular, more than one of the programs might well be executed simultaneously on the GPU, provided that there are available resources. Nonetheless it pays to be aware of the limitations of the devices to avoid picking models that do not fit into device memory. + +![NVIDIA Turing architecture (image courtesy of NVIDIA)](../img/turing.png) +:width:`350px` +:label:`fig_turing` + +A last aspect that is worth mentioning in more detail are *tensor cores*. They are an example of a recent trend of adding more optimized circuits that are specifically effective for deep learning. For instance, the TPU added a systolic array :cite:`Kung.1988` for fast matrix multiplication. There the design was to support a very small number (one for the first generation of TPUs) of large operations. Tensor cores are at the other end. They are optimized for small operations involving between $4 \times 4$ and $16 \times 16$ matrices, depending on their numerical precision. :numref:`fig_tensorcore` gives an overview of the optimizations. + +![NVIDIA tensor cores in Turing (image courtesy of NVIDIA).](../img/tensorcore.jpg) +:width:`400px` +:label:`fig_tensorcore` + +Obviously when optimizing for computation we end up making certain compromises. One of them is that GPUs are not very good at handling interrupts and sparse data. While there are notable exceptions, such as [Gunrock](https://github.com/gunrock/gunrock) :cite:`Wang.Davidson.Pan.ea.2016`, the access pattern of sparse matrices and vectors do not go well with the high bandwidth burst read operations where GPUs excel. Matching both goals is an area of active research. See e.g., [DGL](http://dgl.ai), a library tuned for deep learning on graphs. + + +## Networks and Buses + +Whenever a single device is insufficient for optimization we need to transfer data to and from it to synchronize processing. This is where networks and buses come in handy. We have a number of design parameters: bandwidth, cost, distance, and flexibility. +On one end we have WiFi that has a pretty good range, is very easy to use (no wires, after all), cheap but it offers comparatively mediocre bandwidth and latency. No machine learning researcher within their right mind would use it to build a cluster of servers. In what follows we focus on interconnects that are suitable for deep learning. + +* **PCIe** is a dedicated bus for very high bandwidth point-to-point connections (up to 32 GB/s on PCIe 4.0 in a 16-lane slot) per lane. Latency is in the order of single-digit microseconds (5 μs). PCIe links are precious. Processors only have a limited number of them: AMD's EPYC 3 has 128 lanes, Intel's Xeon has up to 48 lanes per chip; on desktop-grade CPUs the numbers are 20 (Ryzen 9) and 16 (Core i9) respectively. Since GPUs have typically 16 lanes, this limits the number of GPUs that can connect to the CPU at full bandwidth. After all, they need to share the links with other high bandwidth peripherals such as storage and Ethernet. Just like with RAM access, large bulk transfers are preferable due to reduced packet overhead. +* **Ethernet** is the most commonly used way of connecting computers. While it is significantly slower than PCIe, it is very cheap and resilient to install and covers much longer distances. Typical bandwidth for low-grade servers is 1 GBit/s. Higher-end devices (e.g., [C5 instances](https://aws.amazon.com/ec2/instance-types/c5/) in the cloud) offer between 10 and 100 GBit/s bandwidth. As in all previous cases data transmission has significant overheads. Note that we almost never use raw Ethernet directly but rather a protocol that is executed on top of the physical interconnect (such as UDP or TCP/IP). This adds further overhead. Like PCIe, Ethernet is designed to connect two devices, e.g., a computer and a switch. +* **Switches** allow us to connect multiple devices in a manner where any pair of them can carry out a (typically full bandwidth) point-to-point connection simultaneously. For instance, Ethernet switches might connect 40 servers at high cross-sectional bandwidth. Note that switches are not unique to traditional computer networks. Even PCIe lanes can be [switched](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches). This occurs, e.g., to connect a large number of GPUs to a host processor, as is the case for the [P2 instances](https://aws.amazon.com/ec2/instance-types/p2/). +* **NVLink** is an alternative to PCIe when it comes to very high bandwidth interconnects. It offers up to 300 Gbit/s data transfer rate per link. Server GPUs (Volta V100) have six links whereas consumer-grade GPUs (RTX 2080 Ti) have only one link, operating at a reduced 100 Gbit/s rate. We recommend to use [NCCL](https://github.com/NVIDIA/nccl) to achieve high data transfer between GPUs. + + + +## More Latency Numbers + +The summary in :numref:`table_latency_numbers` and :numref:`table_latency_numbers_tesla` are from [Eliot Eshelman](https://gist.github.com/eshelman) who maintains an updated version of the numbers as a [GitHub gist](https://gist.github.com/eshelman/343a1c46cb3fba142c1afdcdeec17646). + +:Common Latency Numbers. + +| Action | Time | Notes | +| :----------------------------------------- | -----: | :---------------------------------------------- | +| L1 cache reference/hit | 1.5 ns | 4 cycles | +| Floating-point add/mult/FMA | 1.5 ns | 4 cycles | +| L2 cache reference/hit | 5 ns | 12 ~ 17 cycles | +| Branch mispredict | 6 ns | 15 ~ 20 cycles | +| L3 cache hit (unshared cache) | 16 ns | 42 cycles | +| L3 cache hit (shared in another core) | 25 ns | 65 cycles | +| Mutex lock/unlock | 25 ns | | +| L3 cache hit (modified in another core) | 29 ns | 75 cycles | +| L3 cache hit (on a remote CPU socket) | 40 ns | 100 ~ 300 cycles (40 ~ 116 ns) | +| QPI hop to a another CPU (per hop) | 40 ns | | +| 64MB memory ref. (local CPU) | 46 ns | TinyMemBench on Broadwell E5-2690v4 | +| 64MB memory ref. (remote CPU) | 70 ns | TinyMemBench on Broadwell E5-2690v4 | +| 256MB memory ref. (local CPU) | 75 ns | TinyMemBench on Broadwell E5-2690v4 | +| Intel Optane random write | 94 ns | UCSD Non-Volatile Systems Lab | +| 256MB memory ref. (remote CPU) | 120 ns | TinyMemBench on Broadwell E5-2690v4 | +| Intel Optane random read | 305 ns | UCSD Non-Volatile Systems Lab | +| Send 4KB over 100 Gbps HPC fabric | 1 μs | MVAPICH2 over Intel Omni-Path | +| Compress 1KB with Google Snappy | 3 μs | | +| Send 4KB over 10 Gbps ethernet | 10 μs | | +| Write 4KB randomly to NVMe SSD | 30 μs | DC P3608 NVMe SSD (QOS 99% is 500μs) | +| Transfer 1MB to/from NVLink GPU | 30 μs | ~33GB/s on NVIDIA 40GB NVLink | +| Transfer 1MB to/from PCI-E GPU | 80 μs | ~12GB/s on PCIe 3.0 x16 link | +| Read 4KB randomly from NVMe SSD | 120 μs | DC P3608 NVMe SSD (QOS 99%) | +| Read 1MB sequentially from NVMe SSD | 208 μs | ~4.8GB/s DC P3608 NVMe SSD | +| Write 4KB randomly to SATA SSD | 500 μs | DC S3510 SATA SSD (QOS 99.9%) | +| Read 4KB randomly from SATA SSD | 500 μs | DC S3510 SATA SSD (QOS 99.9%) | +| Round trip within same datacenter | 500 μs | One-way ping is ~250μs | +| Read 1MB sequentially from SATA SSD | 2 ms | ~550MB/s DC S3510 SATA SSD | +| Read 1MB sequentially from disk | 5 ms | ~200MB/s server HDD | +| Random Disk Access (seek+rotation) | 10 ms | | +| Send packet CA->Netherlands->CA | 150 ms | | +:label:`table_latency_numbers` + +:Latency Numbers for NVIDIA Tesla GPUs. + +| Action | Time | Notes | +| :------------------------------ | -----: | :---------------------------------------- | +| GPU Shared Memory access | 30 ns | 30~90 cycles (bank conflicts add latency) | +| GPU Global Memory access | 200 ns | 200~800 cycles | +| Launch CUDA kernel on GPU | 10 μs | Host CPU instructs GPU to start kernel | +| Transfer 1MB to/from NVLink GPU | 30 μs | ~33GB/s on NVIDIA 40GB NVLink | +| Transfer 1MB to/from PCI-E GPU | 80 μs | ~12GB/s on PCI-Express x16 link | +:label:`table_latency_numbers_tesla` + +## Summary + +* Devices have overheads for operations. Hence it is important to aim for a small number of large transfers rather than many small ones. This applies to RAM, SSDs, networks and GPUs. +* Vectorization is key for performance. Make sure you are aware of the specific abilities of your accelerator. E.g., some Intel Xeon CPUs are particularly good for INT8 operations, NVIDIA Volta GPUs excel at FP16 matrix-matrix operations and NVIDIA Turing shines at FP16, INT8, and INT4 operations. +* Numerical overflow due to small data types can be a problem during training (and to a lesser extent during inference). +* Aliasing can significantly degrade performance. For instance, memory alignment on 64 bit CPUs should be done with respect to 64 bit boundaries. On GPUs it is a good idea to keep convolution sizes aligned, e.g., to tensor cores. +* Match your algorithms to the hardware (e.g., memory footprint, and bandwidth). Great speedup (orders of magnitude) can be achieved when fitting the parameters into caches. +* We recommend that you sketch out the performance of a novel algorithm on paper before verifying the experimental results. Discrepancies of an order-of-magnitude or more are reasons for concern. +* Use profilers to debug performance bottlenecks. +* Training and inference hardware have different sweet spots in terms of price and performance. + +## Exercises + +1. Write C code to test whether there is any difference in speed between accessing memory aligned or misaligned relative to the external memory interface. Hint: be careful of caching effects. +1. Test the difference in speed between accessing memory in sequence or with a given stride. +1. How could you measure the cache sizes on a CPU? +1. How would you lay out data across multiple memory channels for maximum bandwidth? How would you lay it out if you had many small threads? +1. An enterprise-class HDD is spinning at 10,000 rpm. What is the absolutely minimum time an HDD needs to spend worst case before it can read data (you can assume that heads move almost instantaneously)? Why are 2.5" HDDs becoming popular for commercial servers (relative to 3.5" and 5.25" drives)? +1. Assume that an HDD manufacturer increases the storage density from 1 Tbit per square inch to 5 Tbit per square inch. How much information can you store on a ring on a 2.5" HDD? Is there a difference between the inner and outer tracks? +1. Going from 8 bit to 16 bit data types increases the amount of silicon approximately by four times. Why? Why might NVIDIA have added INT4 operations to their Turing GPUs? +1. How much faster is it to read forward through memory vs. reading backwards? Does this number differ between different computers and CPU vendors? Why? Write C code and experiment with it. +1. Can you measure the cache size of your disk? What is it for a typical HDD? Do SSDs need a cache? +1. Measure the packet overhead when sending messages across the Ethernet. Look up the difference between UDP and TCP/IP connections. +1. Direct memory access allows devices other than the CPU to write (and read) directly to (from) memory. Why is this a good idea? +1. Look at the performance numbers for the Turing T4 GPU. Why does the performance "only" double as you go from FP16 to INT8 and INT4? +1. What is the shortest time it should take for a packet on a round trip between San Francisco and Amsterdam? Hint: you can assume that the distance is 10,000 km. + + +[Discussions](https://discuss.d2l.ai/t/363) diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md index d2ad75f0e..79e3fdb11 100644 --- a/chapter_computational-performance/hybridize.md +++ b/chapter_computational-performance/hybridize.md @@ -1,7 +1,7 @@ # 编译器和口译员 :label:`sec_hybridize` -到目前为止,这本书专注于命令式编程,它利用 `print`、`+` 和 `if` 等陈述来改变计划的状态。考虑以下简单的命令性程序的例子。 +到目前为止,这本书一直侧重于命令式编程,它利用 `print`、`+` 和 `if` 等语句来改变计划的状态。考虑以下简单的命令性程序的例子。 ```{.python .input} #@tab all @@ -17,12 +17,12 @@ def fancy_func(a, b, c, d): print(fancy_func(1, 2, 3, 4)) ``` -Python 是一种 * 解释性语言 *。当评估上述 `fancy_func` 函数时,它会按顺序执行组成函数主体的操作 *。也就是说,它将评估 `e = add(a, b)` 并将结果存储为变量 `e`,从而改变计划的状态。接下来的两个语句 `f = add(c, d)` 和 `g = add(e, f)` 将以类似的方式执行,执行添加操作并将结果存储为变量。:numref:`fig_compute_graph` 说明了数据流。 +Python 是一种 * 解释性语言 *。当评估上述 `fancy_func` 函数时,它会按顺序执行组成函数主体的操作 *。也就是说,它将评估 `e = add(a, b)` 并将结果存储为变量 `e`,从而改变程序的状态。接下来的两个语句 `f = add(c, d)` 和 `g = add(e, f)` 将以类似的方式执行,执行添加并将结果存储为变量。:numref:`fig_compute_graph` 说明了数据流。 ![Data flow in an imperative program.](../img/computegraph.svg) :label:`fig_compute_graph` -尽管命令式编程很方便,但效率可能低下。一方面,即使在 `fancy_func` 中重复调用 `add` 函数,Python 也会分别执行三个函数调用。比如说,如果在 GPU 上(甚至在多个 GPU 上)执行这些操作,则 Python 解释器产生的开销可能会变得压倒性。此外,在执行 `fancy_func` 中的所有语句之前,它需要保存 `e` 和 `f` 的变量值。这是因为我们不知道在语句 `e = add(a, b)` 和 `f = add(c, d)` 执行之后,程序的其他部分是否会使用变量 `e` 和 `f`。 +尽管命令式编程很方便,但效率可能低下。一方面,即使在 `fancy_func` 中重复调用 `add` 函数,Python 也会分别执行这三个函数调用。比如说,如果在 GPU 上(甚至在多个 GPU 上)执行这些操作,则 Python 解释器产生的开销可能会变得压倒性。此外,它需要保存 `e` 和 `f` 的变量值,直到 `fancy_func` 中的所有语句都被执行。这是因为我们不知道在语句 `e = add(a, b)` 和 `f = add(c, d)` 执行之后,程序的其他部分是否会使用变量 `e` 和 `f`。 ## 符号编程 @@ -253,7 +253,7 @@ with Benchmark('Graph Mode'): ``` :begin_tab:`mxnet` -如上面的结果所观察到的那样,`HybridSequential` 实例调用 `hybridize` 函数后,通过使用符号编程来提高计算性能。 +如上面的结果所观察到的那样,在 `HybridSequential` 实例调用 `hybridize` 函数之后,通过使用符号编程来提高计算性能。 :end_tab: :begin_tab:`pytorch` @@ -326,7 +326,7 @@ class HybridNet(nn.HybridBlock): ``` :begin_tab:`mxnet` -上面的代码实现了一个带有 4 个隐藏单元和 2 个输出的简单网络。`hybrid_forward` 函数需要一个额外的参数 `F`。这是必要的,因为根据代码是否被混合,它将使用略有不同的库(`ndarray` 或 `symbol`)进行处理。这两个类执行的功能非常相似,MxNet 会自动确定参数。为了理解发生了什么,我们将参数作为函数调用的一部分打印出来。 +上面的代码实现了一个带有 4 个隐藏单元和 2 个输出的简单网络。`hybrid_forward` 函数需要一个额外的参数 `F`。这是必要的,因为根据代码是否混合,它将使用略有不同的库(`ndarray` 或 `symbol`)进行处理。这两个类执行的功能非常相似,MxNet 会自动确定参数。为了理解发生了什么,我们将参数作为函数调用的一部分打印出来。 :end_tab: ```{.python .input} @@ -346,7 +346,7 @@ net(x) ``` :begin_tab:`mxnet` -我们现在不使用 `ndarray`,而不是使用 `symbol` 模块进行 `F`。此外,尽管输入是 `ndarray` 类型,但作为编译过程的一部分,通过网络流动的数据现在已转换为 `symbol` 类型。重复函数调用会导致令人惊讶的结果: +而不是使用 `ndarray`,我们现在将 `symbol` 模块用于 `F`。此外,尽管输入是 `ndarray` 类型,但作为编译过程的一部分,通过网络流动的数据现在已转换为 `symbol` 类型。重复函数调用会导致令人惊讶的结果: :end_tab: ```{.python .input} @@ -354,7 +354,7 @@ net(x) ``` :begin_tab:`mxnet` -这与我们之前看到的截然不同。省略 `hybrid_forward` 中定义的所有打印语句。事实上,在混合后,`net(x)` 的执行不再涉及 Python 解释器。这意味着,忽略任何虚假的 Python 代码(例如 print 语句),以利于更简化的执行和更好的性能。相反,MxNet 直接调用 C ++ 后端。另请注意,`symbol` 模块(例如 `asnumpy`)中不支持某些功能,而就地操作(如 `a += b` 和 `a[:] = a + b`)必须重写为 `a = a + b`。尽管如此,只要速度重要,汇编模型就值得付出努力。优势可以从小百分点到速度的两倍以上,具体取决于模型的复杂性、CPU 的速度以及 GPU 的速度和数量。 +这与我们之前看到的截然不同。省略 `hybrid_forward` 中定义的所有打印语句。事实上,混合后,`net(x)` 的执行不再涉及 Python 解释器。这意味着,忽略任何虚假的 Python 代码(例如 print 语句),以利于更简化的执行和更好的性能。相反,MxNet 直接调用 C ++ 后端。另请注意,`symbol` 模块(例如 `asnumpy`)中不支持某些功能,而就地操作(如 `a += b` 和 `a[:] = a + b`)必须重写为 `a = a + b`。尽管如此,只要速度重要,汇编模型就值得付出努力。优势可以从小百分点到速度的两倍以上,具体取决于模型的复杂性、CPU 的速度以及 GPU 的速度和数量。 :end_tab: ## 摘要 @@ -364,7 +364,7 @@ net(x) :begin_tab:`mxnet` * MxNet 能够根据需要结合这两种方法的优势。 -* `HybridSequential` 和 `HybridBlock` 类构建的模型可以通过调用 `hybridize` 函数将命令性程序转换为符号程序。 +* 由 `HybridSequential` 和 `HybridBlock` 类构建的模型可以通过调用 `hybridize` 函数将命令性程序转换为符号程序。 :end_tab: ## 练习 diff --git a/chapter_computational-performance/multiple-gpus-concise.md b/chapter_computational-performance/multiple-gpus-concise.md new file mode 100644 index 000000000..3da85c7de --- /dev/null +++ b/chapter_computational-performance/multiple-gpus-concise.md @@ -0,0 +1,274 @@ +# 多个 GPU 的简明实施 +:label:`sec_multi_gpu_concise` + +为每个新模型从头开始实施并行性并不乐趣。此外,优化同步工具以实现高性能也有很大的好处。在下面我们将展示如何使用深度学习框架的高级 API 来完成此操作。数学和算法与 :numref:`sec_multi_gpu` 中的算法相同。毫不奇怪,你需要至少两个 GPU 来运行本节的代码。 + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import autograd, gluon, init, np, npx +from mxnet.gluon import nn +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +from torch import nn +``` + +## 玩具网 + +让我们使用一个比 :numref:`sec_multi_gpu` 的 Lenet 稍有意义的网络,该网络仍然足够简单快捷地训练。我们选择了 Resnet-18 变体 :cite:`He.Zhang.Ren.ea.2016`。由于输入图像很小,我们对其进行稍微修改。特别是,与 :numref:`sec_resnet` 的区别在于,我们在开始时使用较小的卷积内核、步幅和填充。此外,我们删除了最大池层。 + +```{.python .input} +#@save +def resnet18(num_classes): + """A slightly modified ResNet-18 model.""" + def resnet_block(num_channels, num_residuals, first_block=False): + blk = nn.Sequential() + for i in range(num_residuals): + if i == 0 and not first_block: + blk.add(d2l.Residual( + num_channels, use_1x1conv=True, strides=2)) + else: + blk.add(d2l.Residual(num_channels)) + return blk + + net = nn.Sequential() + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer + net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), + nn.BatchNorm(), nn.Activation('relu')) + net.add(resnet_block(64, 2, first_block=True), + resnet_block(128, 2), + resnet_block(256, 2), + resnet_block(512, 2)) + net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) + return net +``` + +```{.python .input} +#@tab pytorch +#@save +def resnet18(num_classes, in_channels=1): + """A slightly modified ResNet-18 model.""" + def resnet_block(in_channels, out_channels, num_residuals, + first_block=False): + blk = [] + for i in range(num_residuals): + if i == 0 and not first_block: + blk.append(d2l.Residual(in_channels, out_channels, + use_1x1conv=True, strides=2)) + else: + blk.append(d2l.Residual(out_channels, out_channels)) + return nn.Sequential(*blk) + + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer + net = nn.Sequential( + nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2d(64), + nn.ReLU()) + net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True)) + net.add_module("resnet_block2", resnet_block(64, 128, 2)) + net.add_module("resnet_block3", resnet_block(128, 256, 2)) + net.add_module("resnet_block4", resnet_block(256, 512, 2)) + net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1))) + net.add_module("fc", nn.Sequential(nn.Flatten(), + nn.Linear(512, num_classes))) + return net +``` + +## 网络初始化 + +:begin_tab:`mxnet` +`initialize` 函数允许我们在我们选择的设备上初始化参数。有关初始化方法的复习,请参阅 :numref:`sec_numerical_stability`。特别方便的是,它还允许我们同时在 * 多个 * 设备上初始化网络。让我们试试这在实践中是如何运作的。 +:end_tab: + +:begin_tab:`pytorch` +我们将在训练循环中初始化网络。有关初始化方法的复习,请参阅 :numref:`sec_numerical_stability`。 +:end_tab: + +```{.python .input} +net = resnet18(10) +# Get a list of GPUs +devices = d2l.try_all_gpus() +# Initialize all the parameters of the network +net.initialize(init=init.Normal(sigma=0.01), ctx=devices) +``` + +```{.python .input} +#@tab pytorch +net = resnet18(10) +# Get a list of GPUs +devices = d2l.try_all_gpus() +# We will initialize the network inside the training loop +``` + +:begin_tab:`mxnet` +使用 :numref:`sec_multi_gpu` 中引入的 `split_and_load` 函数,我们可以划分一小批数据并将部分内容复制到 `devices` 变量提供的设备列表中。网络实例 * 自动 * 使用适当的 GPU 来计算正向传播的值。在这里,我们生成 4 个观测结果并通过 GPU 将它们拆分。 +:end_tab: + +```{.python .input} +x = np.random.uniform(size=(4, 1, 28, 28)) +x_shards = gluon.utils.split_and_load(x, devices) +net(x_shards[0]), net(x_shards[1]) +``` + +:begin_tab:`mxnet` +数据通过网络后,相应的参数将在数据通过的设备上初始化 *。这意味着初始化是基于每台设备进行的。由于我们选择了 GPU 0 和 GPU 1 进行初始化,因此网络仅在那里初始化,而不是在 CPU 上初始化。事实上,CPU 上甚至不存在这些参数。我们可以通过打印参数并观察可能出现的任何错误来验证这一点。 +:end_tab: + +```{.python .input} +weight = net[0].params.get('weight') + +try: + weight.data() +except RuntimeError: + print('not initialized on cpu') +weight.data(devices[0])[0], weight.data(devices[1])[0] +``` + +:begin_tab:`mxnet` +接下来,让我们将代码替换为在多个设备上并行工作的代码来评估准确性。这可以替代 :numref:`sec_lenet` 的 `evaluate_accuracy_gpu` 功能。主要区别在于我们在调用网络之前拆分了一个小批次。其他一切基本上是相同的。 +:end_tab: + +```{.python .input} +#@save +def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): + """Compute the accuracy for a model on a dataset using multiple GPUs.""" + # Query the list of devices + devices = list(net.collect_params().values())[0].list_ctx() + # No. of correct predictions, no. of predictions + metric = d2l.Accumulator(2) + for features, labels in data_iter: + X_shards, y_shards = split_f(features, labels, devices) + # Run in parallel + pred_shards = [net(X_shard) for X_shard in X_shards] + metric.add(sum(float(d2l.accuracy(pred_shard, y_shard)) for + pred_shard, y_shard in zip( + pred_shards, y_shards)), labels.size) + return metric[0] / metric[1] +``` + +## 培训 + +与之前一样,训练代码需要执行几个基本功能以实现高效的并行性: + +* 需要在所有设备上初始化网络参数。 +* 迭代数据集时,小批次将在所有设备之间划分。 +* 我们在不同设备之间并行计算损失及其梯度。 +* 渐变将进行聚合,并相应地更新参数。 + +最后,我们计算报告网络最终性能的准确性(同样并行)。训练例程与前几章中的实施非常相似,只是我们需要拆分和聚合数据。 + +```{.python .input} +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + ctx = [d2l.try_gpu(i) for i in range(num_gpus)] + net.initialize(init=init.Normal(sigma=0.01), ctx=ctx, force_reinit=True) + trainer = gluon.Trainer(net.collect_params(), 'sgd', + {'learning_rate': lr}) + loss = gluon.loss.SoftmaxCrossEntropyLoss() + timer, num_epochs = d2l.Timer(), 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + for epoch in range(num_epochs): + timer.start() + for features, labels in train_iter: + X_shards, y_shards = d2l.split_batch(features, labels, ctx) + with autograd.record(): + ls = [loss(net(X_shard), y_shard) for X_shard, y_shard + in zip(X_shards, y_shards)] + for l in ls: + l.backward() + trainer.step(batch_size) + npx.waitall() + timer.stop() + animator.add(epoch + 1, (evaluate_accuracy_gpus(net, test_iter),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(ctx)}') +``` + +```{.python .input} +#@tab pytorch +def train(net, num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + def init_weights(m): + if type(m) in [nn.Linear, nn.Conv2d]: + nn.init.normal_(m.weight, std=0.01) + net.apply(init_weights) + # Set the model on multiple GPUs + net = nn.DataParallel(net, device_ids=devices) + trainer = torch.optim.SGD(net.parameters(), lr) + loss = nn.CrossEntropyLoss() + timer, num_epochs = d2l.Timer(), 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + for epoch in range(num_epochs): + net.train() + timer.start() + for X, y in train_iter: + trainer.zero_grad() + X, y = X.to(devices[0]), y.to(devices[0]) + l = loss(net(X), y) + l.backward() + trainer.step() + timer.stop() + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(devices)}') +``` + +让我们看看这在实践中是如何运作的。作为热身活动,我们在单个 GPU 上训练网络。 + +```{.python .input} +train(num_gpus=1, batch_size=256, lr=0.1) +``` + +```{.python .input} +#@tab pytorch +train(net, num_gpus=1, batch_size=256, lr=0.1) +``` + +接下来我们使用 2 个 GPU 进行培训。与 :numref:`sec_multi_gpu` 中评估的 Lenet 相比,Resnet-18 的模型要复杂得多。这就是并行化显示其优势的地方。计算时间明显大于同步参数的时间。这提高了可扩展性,因为并行化的开销没有那么重要。 + +```{.python .input} +train(num_gpus=2, batch_size=512, lr=0.2) +``` + +```{.python .input} +#@tab pytorch +train(net, num_gpus=2, batch_size=512, lr=0.2) +``` + +## 摘要 + +:begin_tab:`mxnet` +* Gluon 通过提供上下文列表为跨多个设备的模型初始化提供了基元。 +:end_tab: + +* 数据将在可以找到数据的设备上自动评估。 +* 在尝试访问每台设备上的参数之前,请注意初始化每台设备上的网络。否则你会遇到错误。 +* 优化算法会自动聚合多个 GPU。 + +## 练习 + +:begin_tab:`mxnet` +1. 本节使用 Resnet-18。尝试不同的时代、批量大小和学习率。使用更多 GPU 进行计算。如果您使用 16 个 GPU(例如,在 AWS p2.16xlarge 实例上)尝试此操作会怎样? +1. 有时,不同的设备提供不同的计算能力。我们可以同时使用 GPU 和 CPU。我们应该如何划分工作?值得努力吗?为什么?为什么不? +1. 如果我们丢弃 `npx.waitall()` 会怎么样?你将如何修改训练,使你最多可以重叠两个步骤来实现并行性? +:end_tab: + +:begin_tab:`pytorch` +1. 本节使用 Resnet-18。尝试不同的时代、批量大小和学习率。使用更多 GPU 进行计算。如果您使用 16 个 GPU(例如,在 AWS p2.16xlarge 实例上)尝试此操作会怎样? +1. 有时,不同的设备提供不同的计算能力。我们可以同时使用 GPU 和 CPU。我们应该如何划分工作?值得努力吗?为什么?为什么不? +:end_tab: + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/365) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1403) +:end_tab: diff --git a/chapter_computational-performance/multiple-gpus-concise_origin.md b/chapter_computational-performance/multiple-gpus-concise_origin.md new file mode 100644 index 000000000..7afdedd6e --- /dev/null +++ b/chapter_computational-performance/multiple-gpus-concise_origin.md @@ -0,0 +1,286 @@ +# Concise Implementation for Multiple GPUs +:label:`sec_multi_gpu_concise` + +Implementing parallelism from scratch for every new model is no fun. Moreover, there is significant benefit in optimizing synchronization tools for high performance. In the following we will show how to do this using high-level APIs of deep learning frameworks. +The math and the algorithms are the same as in :numref:`sec_multi_gpu`. +Quite unsurprisingly you will need at least two GPUs to run code of this section. + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import autograd, gluon, init, np, npx +from mxnet.gluon import nn +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +from torch import nn +``` + +## A Toy Network + +Let us use a slightly more meaningful network than LeNet from :numref:`sec_multi_gpu` that is still sufficiently easy and quick to train. +We pick a ResNet-18 variant :cite:`He.Zhang.Ren.ea.2016`. Since the input images are tiny we modify it slightly. In particular, the difference from :numref:`sec_resnet` is that we use a smaller convolution kernel, stride, and padding at the beginning. +Moreover, we remove the maximum pooling layer. + +```{.python .input} +#@save +def resnet18(num_classes): + """A slightly modified ResNet-18 model.""" + def resnet_block(num_channels, num_residuals, first_block=False): + blk = nn.Sequential() + for i in range(num_residuals): + if i == 0 and not first_block: + blk.add(d2l.Residual( + num_channels, use_1x1conv=True, strides=2)) + else: + blk.add(d2l.Residual(num_channels)) + return blk + + net = nn.Sequential() + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer + net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), + nn.BatchNorm(), nn.Activation('relu')) + net.add(resnet_block(64, 2, first_block=True), + resnet_block(128, 2), + resnet_block(256, 2), + resnet_block(512, 2)) + net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) + return net +``` + +```{.python .input} +#@tab pytorch +#@save +def resnet18(num_classes, in_channels=1): + """A slightly modified ResNet-18 model.""" + def resnet_block(in_channels, out_channels, num_residuals, + first_block=False): + blk = [] + for i in range(num_residuals): + if i == 0 and not first_block: + blk.append(d2l.Residual(in_channels, out_channels, + use_1x1conv=True, strides=2)) + else: + blk.append(d2l.Residual(out_channels, out_channels)) + return nn.Sequential(*blk) + + # This model uses a smaller convolution kernel, stride, and padding and + # removes the maximum pooling layer + net = nn.Sequential( + nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2d(64), + nn.ReLU()) + net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True)) + net.add_module("resnet_block2", resnet_block(64, 128, 2)) + net.add_module("resnet_block3", resnet_block(128, 256, 2)) + net.add_module("resnet_block4", resnet_block(256, 512, 2)) + net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1))) + net.add_module("fc", nn.Sequential(nn.Flatten(), + nn.Linear(512, num_classes))) + return net +``` + +## Network Initialization + +:begin_tab:`mxnet` +The `initialize` function allows us to initialize parameters on a device of our choice. +For a refresher on initialization methods see :numref:`sec_numerical_stability`. What is particularly convenient is that it also allows us to initialize the network on *multiple* devices simultaneously. Let us try how this works in practice. +:end_tab: + +:begin_tab:`pytorch` +We will initialize the network inside the training loop. +For a refresher on initialization methods see :numref:`sec_numerical_stability`. +:end_tab: + +```{.python .input} +net = resnet18(10) +# Get a list of GPUs +devices = d2l.try_all_gpus() +# Initialize all the parameters of the network +net.initialize(init=init.Normal(sigma=0.01), ctx=devices) +``` + +```{.python .input} +#@tab pytorch +net = resnet18(10) +# Get a list of GPUs +devices = d2l.try_all_gpus() +# We will initialize the network inside the training loop +``` + +:begin_tab:`mxnet` +Using the `split_and_load` function introduced in :numref:`sec_multi_gpu` we can divide a minibatch of data and copy portions to the list of devices provided by the `devices` variable. The network instance *automatically* uses the appropriate GPU to compute the value of the forward propagation. Here we generate 4 observations and split them over the GPUs. +:end_tab: + +```{.python .input} +x = np.random.uniform(size=(4, 1, 28, 28)) +x_shards = gluon.utils.split_and_load(x, devices) +net(x_shards[0]), net(x_shards[1]) +``` + +:begin_tab:`mxnet` +Once data pass through the network, the corresponding parameters are initialized *on the device the data passed through*. +This means that initialization happens on a per-device basis. Since we picked GPU 0 and GPU 1 for initialization, the network is initialized only there, and not on the CPU. In fact, the parameters do not even exist on the CPU. We can verify this by printing out the parameters and observing any errors that might arise. +:end_tab: + +```{.python .input} +weight = net[0].params.get('weight') + +try: + weight.data() +except RuntimeError: + print('not initialized on cpu') +weight.data(devices[0])[0], weight.data(devices[1])[0] +``` + +:begin_tab:`mxnet` +Next, let us replace the code to evaluate the accuracy by one that works in parallel across multiple devices. This serves as a replacement of the `evaluate_accuracy_gpu` function from :numref:`sec_lenet`. The main difference is that we split a minibatch before invoking the network. All else is essentially identical. +:end_tab: + +```{.python .input} +#@save +def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): + """Compute the accuracy for a model on a dataset using multiple GPUs.""" + # Query the list of devices + devices = list(net.collect_params().values())[0].list_ctx() + # No. of correct predictions, no. of predictions + metric = d2l.Accumulator(2) + for features, labels in data_iter: + X_shards, y_shards = split_f(features, labels, devices) + # Run in parallel + pred_shards = [net(X_shard) for X_shard in X_shards] + metric.add(sum(float(d2l.accuracy(pred_shard, y_shard)) for + pred_shard, y_shard in zip( + pred_shards, y_shards)), labels.size) + return metric[0] / metric[1] +``` + +## Training + +As before, the training code needs to perform several basic functions for efficient parallelism: + +* Network parameters need to be initialized across all devices. +* While iterating over the dataset minibatches are to be divided across all devices. +* We compute the loss and its gradient in parallel across devices. +* Gradients are aggregated and parameters are updated accordingly. + +In the end we compute the accuracy (again in parallel) to report the final performance of the network. The training routine is quite similar to implementations in previous chapters, except that we need to split and aggregate data. + +```{.python .input} +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + ctx = [d2l.try_gpu(i) for i in range(num_gpus)] + net.initialize(init=init.Normal(sigma=0.01), ctx=ctx, force_reinit=True) + trainer = gluon.Trainer(net.collect_params(), 'sgd', + {'learning_rate': lr}) + loss = gluon.loss.SoftmaxCrossEntropyLoss() + timer, num_epochs = d2l.Timer(), 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + for epoch in range(num_epochs): + timer.start() + for features, labels in train_iter: + X_shards, y_shards = d2l.split_batch(features, labels, ctx) + with autograd.record(): + ls = [loss(net(X_shard), y_shard) for X_shard, y_shard + in zip(X_shards, y_shards)] + for l in ls: + l.backward() + trainer.step(batch_size) + npx.waitall() + timer.stop() + animator.add(epoch + 1, (evaluate_accuracy_gpus(net, test_iter),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(ctx)}') +``` + +```{.python .input} +#@tab pytorch +def train(net, num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + def init_weights(m): + if type(m) in [nn.Linear, nn.Conv2d]: + nn.init.normal_(m.weight, std=0.01) + net.apply(init_weights) + # Set the model on multiple GPUs + net = nn.DataParallel(net, device_ids=devices) + trainer = torch.optim.SGD(net.parameters(), lr) + loss = nn.CrossEntropyLoss() + timer, num_epochs = d2l.Timer(), 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + for epoch in range(num_epochs): + net.train() + timer.start() + for X, y in train_iter: + trainer.zero_grad() + X, y = X.to(devices[0]), y.to(devices[0]) + l = loss(net(X), y) + l.backward() + trainer.step() + timer.stop() + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(devices)}') +``` + +Let us see how this works in practice. As a warm-up we train the network on a single GPU. + +```{.python .input} +train(num_gpus=1, batch_size=256, lr=0.1) +``` + +```{.python .input} +#@tab pytorch +train(net, num_gpus=1, batch_size=256, lr=0.1) +``` + +Next we use 2 GPUs for training. Compared with LeNet +evaluated in :numref:`sec_multi_gpu`, +the model for ResNet-18 is considerably more complex. This is where parallelization shows its advantage. The time for computation is meaningfully larger than the time for synchronizing parameters. This improves scalability since the overhead for parallelization is less relevant. + +```{.python .input} +train(num_gpus=2, batch_size=512, lr=0.2) +``` + +```{.python .input} +#@tab pytorch +train(net, num_gpus=2, batch_size=512, lr=0.2) +``` + +## Summary + +:begin_tab:`mxnet` +* Gluon provides primitives for model initialization across multiple devices by providing a context list. +:end_tab: +* Data are automatically evaluated on the devices where the data can be found. +* Take care to initialize the networks on each device before trying to access the parameters on that device. Otherwise you will encounter an error. +* The optimization algorithms automatically aggregate over multiple GPUs. + + + +## Exercises + +:begin_tab:`mxnet` +1. This section uses ResNet-18. Try different epochs, batch sizes, and learning rates. Use more GPUs for computation. What happens if you try this with 16 GPUs (e.g., on an AWS p2.16xlarge instance)? +1. Sometimes, different devices provide different computing power. We could use the GPUs and the CPU at the same time. How should we divide the work? Is it worth the effort? Why? Why not? +1. What happens if we drop `npx.waitall()`? How would you modify training such that you have an overlap of up to two steps for parallelism? +:end_tab: + +:begin_tab:`pytorch` +1. This section uses ResNet-18. Try different epochs, batch sizes, and learning rates. Use more GPUs for computation. What happens if you try this with 16 GPUs (e.g., on an AWS p2.16xlarge instance)? +1. Sometimes, different devices provide different computing power. We could use the GPUs and the CPU at the same time. How should we divide the work? Is it worth the effort? Why? Why not? +:end_tab: + + + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/365) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1403) +:end_tab: diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md new file mode 100644 index 000000000..a31cfef36 --- /dev/null +++ b/chapter_computational-performance/multiple-gpus.md @@ -0,0 +1,365 @@ +# 在多个 GPU 上进行培训 +:label:`sec_multi_gpu` + +到目前为止,我们讨论了如何在 CPU 和 GPU 上高效训练模型。我们甚至展示了深度学习框架如何允许在 :numref:`sec_auto_para` 中自动并行化它们之间的计算和通信。我们还在 :numref:`sec_use_gpu` 中展示了如何使用 `nvidia-smi` 命令列出计算机上的所有可用 GPU。我们没有 * 讨论的是如何真正并行化深度学习训练。相反,我们顺便暗示,人们会以某种方式将数据拆分到多个设备之间并使其工作。本节将填充详细信息,并展示如何从头开始并行训练网络。有关如何利用高级 API 中功能的详细信息降级为 :numref:`sec_multi_gpu_concise`。我们假设您熟悉微型批次随机梯度下降算法,例如 :numref:`sec_minibatch_sgd` 中描述的算法。 + +## 分解问题 + +让我们从一个简单的计算机视觉问题和稍微陈旧的网络开始,例如,最后有多层复杂、集中,还有几个完全连接的层。也就是说,让我们从一个看起来与 Lenet :cite:`LeCun.Bottou.Bengio.ea.1998` 或 AlexNet :cite:`Krizhevsky.Sutskever.Hinton.2012` 非常相似的网络开始。假定多个 GPU(如果是台式机服务器,则为 2 个 GPU,AWS g4dn.12xlarge 实例上 4 个,p3.16xlarge 上的 8 个,p2.16xlarge 上 16 个),我们希望以实现良好的加速的方式对训练进行分区,同时从简单且可重复的设计选择中受益。毕竟,多个 GPU 可以同时增加 * 内存 * 和 * 计算 * 能力。简而言之,鉴于我们想要分类的一小批训练数据,我们有以下选择。 + +首先,我们可以在多个 GPU 之间对网络进行分区。也就是说,每个 GPU 都会将流入特定层的数据作为输入,跨多个后续图层处理数据,然后将数据发送到下一个 GPU。与单个 GPU 可以处理的数据相比,这使我们能够使用更大的网络处理数据。此外,每个 GPU 的内存占用可以很好地控制(占总网络占用空间的一小部分)。 + +但是,层之间的接口(以及 GPU)需要严格的同步。这可能很棘手,特别是如果计算工作负载在图层之间没有正确匹配的情况下。对于大量 GPU 来说,这个问题更加严重。图层之间的接口还需要大量的数据传输,例如激活和梯度。这可能会超过 GPU 总线的带宽。此外,计算密集型但连续的操作对于分区来说不是微不足道的。例如,请参阅 :cite:`Mirhoseini.Pham.Le.ea.2017` 以了解这方面的最佳努力。这仍然是一个困难的问题,目前尚不清楚是否有可能在非平凡的问题上实现良好的(线性)扩展。除非有出色的框架或操作系统支持将多个 GPU 链接在一起,否则我们不推荐使用。 + +其次,我们可以逐层分割工作。例如,我们可以将问题分成 4 个 GPU,而不是在单个 GPU 上计算 64 个通道,每个 GPU 都会生成 16 个通道的数据。同样,对于完全连接的层,我们可以拆分输出单元的数量。:numref:`fig_alexnet_original`(取自 :cite:`Krizhevsky.Sutskever.Hinton.2012`)说明了这种设计,该策略用于处理内存占用非常小的 GPU(当时为 2 GB)。如果通道(或单位)数量不太少,这样就可以在计算方面进行良好的缩放。此外,由于可用内存可以线性扩展,多个 GPU 可以处理越来越大的网络。 + +![Model parallelism in the original AlexNet design due to limited GPU memory.](../img/alexnet-original.svg) +:label:`fig_alexnet_original` + +但是,我们需要 * 非常大 * 数的同步或障碍操作,因为每个层都取决于所有其他层的结果。此外,需要传输的数据量可能甚至超过在 GPU 之间分布层时的数据量。因此,由于带宽成本和复杂性,我们不推荐使用此方法。 + +最后,我们可以在多个 GPU 之间对数据进行分区。这样,所有 GPU 都执行相同类型的工作,尽管观察结果不同。在每个小批量训练数据之后,梯度会在 GPU 中进行汇总。这是最简单的方法,它可以在任何情况下应用。我们只需要在每个小批次之后进行同步。也就是说,在计算其他梯度参数的同时,开始交换梯度参数是非常可取的。此外,更多的 GPU 会导致更大的小批量尺寸,从而提高训练效率。但是,添加更多 GPU 并不允许我们训练更大的模型。 + +![Parallelization on multiple GPUs. From left to right: original problem, network partitioning, layer-wise partitioning, data parallelism.](../img/splitting.svg) +:label:`fig_splitting` + +:numref:`fig_splitting` 中描述了多个 GPU 上的不同并行化方式的比较。总的来说,如果我们能够访问具有足够大内存的 GPU,数据并行性是最方便的继续方式。另请参阅 :cite:`Li.Andersen.Park.ea.2014` 以了解分布式培训的分区的详细说明。在深度学习的早期,GPU 内存曾经是一个问题。到目前为止,除了最不寻常的情况外,所有这个问题都已解决。我们将重点放在以下内容中的数据并行性。 + +## 数据并行 + +假设计算机上有 $k$ GPU。鉴于要训练的模型,每个 GPU 将独立维护一套完整的模型参数,尽管整个 GPU 的参数值是相同且同步的。例如,:numref:`fig_data_parallel` 说明了 $k=2$ 时使用数据并行性进行培训。 + +![Calculation of minibatch stochastic gradient descent using data parallelism on two GPUs.](../img/data-parallel.svg) +:label:`fig_data_parallel` + +一般来说,培训的进展情况如下: + +* 在训练的任何迭代中,只要有一个随机的微型批量,我们将批次中的示例拆分为 $k$ 部分,然后在 GPU 中均匀分布。 +* 每个 GPU 都根据分配给模型的小批次子集来计算模型参数的损耗和梯度。 +* 汇总 $k$ GPU 中每个 GPU 的局部梯度,以获得当前的微型批次随机梯度。 +* 聚合渐变将重新分配到每个 GPU。 +* 每个 GPU 都使用这个微型批次随机梯度来更新它维护的完整模型参数集。 + +请注意,在实践中,我们在 $k$ GPU 上进行培训时,我们将小批量 $k$ 倍增加 *,这样每个 GPU 的工作量就像我们只在单个 GPU 上进行培训一样。在 16-GPU 服务器上,这可能会大大增加小批量的大小,我们可能必须相应地提高学习率。另请注意,:numref:`sec_batch_norm` 中的批量标准化需要进行调整,例如,通过每个 GPU 保持单独的批量标准化系数。在下面的内容中,我们将使用玩具网络来说明多 GPU 训练。 + +```{.python .input} +%matplotlib inline +from d2l import mxnet as d2l +from mxnet import autograd, gluon, np, npx +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +%matplotlib inline +from d2l import torch as d2l +import torch +from torch import nn +from torch.nn import functional as F +``` + +## 玩具网 + +我们使用 :numref:`sec_lenet` 中引入的 Lenet(稍作修改)。我们从头开始定义它,以详细说明参数交换和同步。 + +```{.python .input} +# Initialize model parameters +scale = 0.01 +W1 = np.random.normal(scale=scale, size=(20, 1, 3, 3)) +b1 = np.zeros(20) +W2 = np.random.normal(scale=scale, size=(50, 20, 5, 5)) +b2 = np.zeros(50) +W3 = np.random.normal(scale=scale, size=(800, 128)) +b3 = np.zeros(128) +W4 = np.random.normal(scale=scale, size=(128, 10)) +b4 = np.zeros(10) +params = [W1, b1, W2, b2, W3, b3, W4, b4] + +# Define the model +def lenet(X, params): + h1_conv = npx.convolution(data=X, weight=params[0], bias=params[1], + kernel=(3, 3), num_filter=20) + h1_activation = npx.relu(h1_conv) + h1 = npx.pooling(data=h1_activation, pool_type='avg', kernel=(2, 2), + stride=(2, 2)) + h2_conv = npx.convolution(data=h1, weight=params[2], bias=params[3], + kernel=(5, 5), num_filter=50) + h2_activation = npx.relu(h2_conv) + h2 = npx.pooling(data=h2_activation, pool_type='avg', kernel=(2, 2), + stride=(2, 2)) + h2 = h2.reshape(h2.shape[0], -1) + h3_linear = np.dot(h2, params[4]) + params[5] + h3 = npx.relu(h3_linear) + y_hat = np.dot(h3, params[6]) + params[7] + return y_hat + +# Cross-entropy loss function +loss = gluon.loss.SoftmaxCrossEntropyLoss() +``` + +```{.python .input} +#@tab pytorch +# Initialize model parameters +scale = 0.01 +W1 = torch.randn(size=(20, 1, 3, 3)) * scale +b1 = torch.zeros(20) +W2 = torch.randn(size=(50, 20, 5, 5)) * scale +b2 = torch.zeros(50) +W3 = torch.randn(size=(800, 128)) * scale +b3 = torch.zeros(128) +W4 = torch.randn(size=(128, 10)) * scale +b4 = torch.zeros(10) +params = [W1, b1, W2, b2, W3, b3, W4, b4] + +# Define the model +def lenet(X, params): + h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1]) + h1_activation = F.relu(h1_conv) + h1 = F.avg_pool2d(input=h1_activation, kernel_size=(2, 2), stride=(2, 2)) + h2_conv = F.conv2d(input=h1, weight=params[2], bias=params[3]) + h2_activation = F.relu(h2_conv) + h2 = F.avg_pool2d(input=h2_activation, kernel_size=(2, 2), stride=(2, 2)) + h2 = h2.reshape(h2.shape[0], -1) + h3_linear = torch.mm(h2, params[4]) + params[5] + h3 = F.relu(h3_linear) + y_hat = torch.mm(h3, params[6]) + params[7] + return y_hat + +# Cross-entropy loss function +loss = nn.CrossEntropyLoss(reduction='none') +``` + +## 数据同步 + +为了高效的多 GPU 培训,我们需要两种基本操作。首先,我们需要有能力将参数列表分发到多个设备并附加渐变(`get_params`)。如果没有参数,就不可能在 GPU 上评估网络。其次,我们需要跨多个设备对参数进行求和的能力,即我们需要 `allreduce` 函数。 + +```{.python .input} +def get_params(params, device): + new_params = [p.copyto(device) for p in params] + for p in new_params: + p.attach_grad() + return new_params +``` + +```{.python .input} +#@tab pytorch +def get_params(params, device): + new_params = [p.clone().to(device) for p in params] + for p in new_params: + p.requires_grad_() + return new_params +``` + +让我们通过将模型参数复制到一个 GPU 来尝试一下。 + +```{.python .input} +#@tab all +new_params = get_params(params, d2l.try_gpu(0)) +print('b1 weight:', new_params[1]) +print('b1 grad:', new_params[1].grad) +``` + +由于我们还没有执行任何计算,所以有关偏差参数的梯度仍然为零。现在让我们假设我们有一个向量分布在多个 GPU 上。以下 `allreduce` 函数将所有向量加起来,并将结果广播回所有 GPU。请注意,为了实现这一点,我们需要将数据复制到累计结果的设备。 + +```{.python .input} +def allreduce(data): + for i in range(1, len(data)): + data[0][:] += data[i].copyto(data[0].ctx) + for i in range(1, len(data)): + data[0].copyto(data[i]) +``` + +```{.python .input} +#@tab pytorch +def allreduce(data): + for i in range(1, len(data)): + data[0][:] += data[i].to(data[0].device) + for i in range(1, len(data)): + data[i] = data[0].to(data[i].device) +``` + +让我们通过在不同设备上创建具有不同值的矢量来测试这一点,然后聚合它们 + +```{.python .input} +data = [np.ones((1, 2), ctx=d2l.try_gpu(i)) * (i + 1) for i in range(2)] +print('before allreduce:\n', data[0], '\n', data[1]) +allreduce(data) +print('after allreduce:\n', data[0], '\n', data[1]) +``` + +```{.python .input} +#@tab pytorch +data = [torch.ones((1, 2), device=d2l.try_gpu(i)) * (i + 1) for i in range(2)] +print('before allreduce:\n', data[0], '\n', data[1]) +allreduce(data) +print('after allreduce:\n', data[0], '\n', data[1]) +``` + +## 分发数据 + +我们需要一个简单的实用程序函数才能在多个 GPU 之间均匀分配微型批次。例如,在两个 GPU 上,我们希望将一半的数据复制到任何一个 GPU 中。由于它更方便、更简洁,我们使用深度学习框架中的内置函数在 $4 \times 5$ 矩阵上进行试用。 + +```{.python .input} +data = np.arange(20).reshape(4, 5) +devices = [npx.gpu(0), npx.gpu(1)] +split = gluon.utils.split_and_load(data, devices) +print('input :', data) +print('load into', devices) +print('output:', split) +``` + +```{.python .input} +#@tab pytorch +data = torch.arange(20).reshape(4, 5) +devices = [torch.device('cuda:0'), torch.device('cuda:1')] +split = nn.parallel.scatter(data, devices) +print('input :', data) +print('load into', devices) +print('output:', split) +``` + +为了以后重复使用,我们定义了一个分割数据和标签的 `split_batch` 函数。 + +```{.python .input} +#@save +def split_batch(X, y, devices): + """Split `X` and `y` into multiple devices.""" + assert X.shape[0] == y.shape[0] + return (gluon.utils.split_and_load(X, devices), + gluon.utils.split_and_load(y, devices)) +``` + +```{.python .input} +#@tab pytorch +#@save +def split_batch(X, y, devices): + """Split `X` and `y` into multiple devices.""" + assert X.shape[0] == y.shape[0] + return (nn.parallel.scatter(X, devices), + nn.parallel.scatter(y, devices)) +``` + +## 培训 + +现在我们可以在单个小批量上实施多 GPU 训练。其实施主要基于本节中描述的数据并行方法。我们将使用刚才讨论的辅助函数 `allreduce` 和 `split_and_load`,在多个 GPU 之间同步数据。请注意,我们不需要编写任何特定的代码即可实现并行性。由于计算图在微型批次内的设备之间没有任何依赖关系,因此它是并行 * 自动执行的。 + +```{.python .input} +def train_batch(X, y, device_params, devices, lr): + X_shards, y_shards = split_batch(X, y, devices) + with autograd.record(): # Loss is calculated separately on each GPU + ls = [loss(lenet(X_shard, device_W), y_shard) + for X_shard, y_shard, device_W in zip( + X_shards, y_shards, device_params)] + for l in ls: # Backpropagation is performed separately on each GPU + l.backward() + # Sum all gradients from each GPU and broadcast them to all GPUs + for i in range(len(device_params[0])): + allreduce([device_params[c][i].grad for c in range(len(devices))]) + # The model parameters are updated separately on each GPU + for param in device_params: + d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch +``` + +```{.python .input} +#@tab pytorch +def train_batch(X, y, device_params, devices, lr): + X_shards, y_shards = split_batch(X, y, devices) + # Loss is calculated separately on each GPU + ls = [loss(lenet(X_shard, device_W), y_shard).sum() + for X_shard, y_shard, device_W in zip( + X_shards, y_shards, device_params)] + for l in ls: # Backpropagation is performed separately on each GPU + l.backward() + # Sum all gradients from each GPU and broadcast them to all GPUs + with torch.no_grad(): + for i in range(len(device_params[0])): + allreduce([device_params[c][i].grad for c in range(len(devices))]) + # The model parameters are updated separately on each GPU + for param in device_params: + d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch +``` + +现在,我们可以定义训练功能。它与前几章中使用的略有不同:我们需要分配 GPU 并将所有模型参数复制到所有设备。显然,每个批次都使用 `train_batch` 函数来处理多个 GPU。为方便起见(以及代码的简洁性),我们在单个 GPU 上计算准确性,尽管这是 * 效率低的 *,因为其他 GPU 处于空闲状态。 + +```{.python .input} +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + # Copy model parameters to `num_gpus` GPUs + device_params = [get_params(params, d) for d in devices] + num_epochs = 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + timer = d2l.Timer() + for epoch in range(num_epochs): + timer.start() + for X, y in train_iter: + # Perform multi-GPU training for a single minibatch + train_batch(X, y, device_params, devices, lr) + npx.waitall() + timer.stop() + # Evaluate the model on GPU 0 + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( + lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(devices)}') +``` + +```{.python .input} +#@tab pytorch +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + # Copy model parameters to `num_gpus` GPUs + device_params = [get_params(params, d) for d in devices] + num_epochs = 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + timer = d2l.Timer() + for epoch in range(num_epochs): + timer.start() + for X, y in train_iter: + # Perform multi-GPU training for a single minibatch + train_batch(X, y, device_params, devices, lr) + torch.cuda.synchronize() + timer.stop() + # Evaluate the model on GPU 0 + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( + lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(devices)}') +``` + +让我们看看这在单个 GPU 上的效果。我们首先使用 256 个批量大小,学习率为 0.2。 + +```{.python .input} +#@tab all +train(num_gpus=1, batch_size=256, lr=0.2) +``` + +通过保持批量大小和学习速率不变并将 GPU 的数量增加到 2,我们可以看到,与之前的实验相比,测试准确度大致保持不变。就优化算法而言,它们是相同的。不幸的是,这里没有任何有意义的加速:模型太小了;此外,我们只有一个小数据集,在这里我们略微不完善的多 GPU 训练方法受到了巨大的 Python 开销。今后,我们将遇到更复杂的模型和更复杂的并行化方式。尽管如此,让我们看看时尚 Mnist 会发生什么。 + +```{.python .input} +#@tab all +train(num_gpus=2, batch_size=256, lr=0.2) +``` + +## 摘要 + +* 有多种方法可以将深度网络训练分成多个 GPU。我们可以在图层之间、跨图层或跨数据拆分它们。前两者需要严格编排的数据传输。数据并行性是最简单的策略。 +* 数据并行培训非常简单。但是,它增加了有效的微型批量以提高效率。 +* 在数据并行度中,数据被拆分到多个 GPU 中,其中每个 GPU 执行自己的向前和向后操作,然后聚合梯度,结果将广播回 GPU。 +* 我们可能会对较大的小批量使用略微提高的学习率。 + +## 练习 + +1. 在 $k$ GPU 上进行培训时,将小批量大小从 $b$ 更改为 $k \cdot b$,即按 GPU 的数量向上扩展。 +1. 比较不同学习率的准确性。它如何随着 GPU 的数量进行扩展? +1. 实施一个更高效的 `allreduce` 函数来聚合不同的 GPU 上的不同参数?为什么效率更高? +1. 实施多 GPU 测试精度计算。 + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/364) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1669) +:end_tab: diff --git a/chapter_computational-performance/multiple-gpus_origin.md b/chapter_computational-performance/multiple-gpus_origin.md new file mode 100644 index 000000000..df03e438e --- /dev/null +++ b/chapter_computational-performance/multiple-gpus_origin.md @@ -0,0 +1,414 @@ +# Training on Multiple GPUs +:label:`sec_multi_gpu` + +So far we discussed how to train models efficiently on CPUs and GPUs. We even showed how deep learning frameworks allow one to parallelize computation and communication automatically between them in :numref:`sec_auto_para`. We also showed in :numref:`sec_use_gpu` how to list all the available GPUs on a computer using the `nvidia-smi` command. +What we did *not* discuss is how to actually parallelize deep learning training. +Instead, we implied in passing that one would somehow split the data across multiple devices and make it work. The present section fills in the details and shows how to train a network in parallel when starting from scratch. Details on how to take advantage of functionality in high-level APIs is relegated to :numref:`sec_multi_gpu_concise`. +We assume that you are familiar with minibatch stochastic gradient descent algorithms such as the ones described in :numref:`sec_minibatch_sgd`. + + +## Splitting the Problem + +Let us start with a simple computer vision problem and a slightly archaic network, e.g., with multiple layers of convolutions, pooling, and possibly a few fully-connected layers in the end. +That is, let us start with a network that looks quite similar to LeNet :cite:`LeCun.Bottou.Bengio.ea.1998` or AlexNet :cite:`Krizhevsky.Sutskever.Hinton.2012`. +Given multiple GPUs (2 if it is a desktop server, 4 on an AWS g4dn.12xlarge instance, 8 on a p3.16xlarge, or 16 on a p2.16xlarge), we want to partition training in a manner as to achieve good speedup while simultaneously benefitting from simple and reproducible design choices. Multiple GPUs, after all, increase both *memory* and *computation* ability. In a nutshell, we have the following choices, given a minibatch of training data that we want to classify. + +First, we could partition the network across multiple GPUs. That is, each GPU takes as input the data flowing into a particular layer, processes data across a number of subsequent layers and then sends the data to the next GPU. +This allows us to process data with larger networks when compared with what a single GPU could handle. +Besides, +memory footprint per GPU can be well controlled (it is a fraction of the total network footprint). + +However, the interface between layers (and thus GPUs) requires tight synchronization. This can be tricky, in particular if the computational workloads are not properly matched between layers. The problem is exacerbated for large numbers of GPUs. +The interface between layers also +requires large amounts of data transfer, +such as activations and gradients. +This may overwhelm the bandwidth of the GPU buses. +Moreover, compute-intensive, yet sequential operations are nontrivial to partition. See e.g., :cite:`Mirhoseini.Pham.Le.ea.2017` for a best effort in this regard. It remains a difficult problem and it is unclear whether it is possible to achieve good (linear) scaling on nontrivial problems. We do not recommend it unless there is excellent framework or operating system support for chaining together multiple GPUs. + + +Second, we could split the work layer-wise. For instance, rather than computing 64 channels on a single GPU we could split up the problem across 4 GPUs, each of which generates data for 16 channels. +Likewise, for a fully-connected layer we could split the number of output units. +:numref:`fig_alexnet_original` (taken from :cite:`Krizhevsky.Sutskever.Hinton.2012`) +illustrates this design, where this strategy was used to deal with GPUs that had a very small memory footprint (2 GB at the time). +This allows for good scaling in terms of computation, provided that the number of channels (or units) is not too small. +Besides, +multiple GPUs can process increasingly larger networks since the available memory scales linearly. + +![Model parallelism in the original AlexNet design due to limited GPU memory.](../img/alexnet-original.svg) +:label:`fig_alexnet_original` + +However, +we need a *very large* number of synchronization or barrier operations since each layer depends on the results from all the other layers. +Moreover, the amount of data that needs to be transferred is potentially even larger than when distributing layers across GPUs. Thus, we do not recommend this approach due to its bandwidth cost and complexity. + +Last, we could partition data across multiple GPUs. This way all GPUs perform the same type of work, albeit on different observations. Gradients are aggregated across GPUs after each minibatch of training data. +This is the simplest approach and it can be applied in any situation. +We only need to synchronize after each minibatch. That said, it is highly desirable to start exchanging gradients parameters already while others are still being computed. +Moreover, larger numbers of GPUs lead to larger minibatch sizes, thus increasing training efficiency. +However, adding more GPUs does not allow us to train larger models. + + +![Parallelization on multiple GPUs. From left to right: original problem, network partitioning, layer-wise partitioning, data parallelism.](../img/splitting.svg) +:label:`fig_splitting` + + +A comparison of different ways of parallelization on multiple GPUs is depicted in :numref:`fig_splitting`. +By and large, data parallelism is the most convenient way to proceed, provided that we have access to GPUs with sufficiently large memory. See also :cite:`Li.Andersen.Park.ea.2014` for a detailed description of partitioning for distributed training. GPU memory used to be a problem in the early days of deep learning. By now this issue has been resolved for all but the most unusual cases. We focus on data parallelism in what follows. + +## Data Parallelism + +Assume that there are $k$ GPUs on a machine. Given the model to be trained, each GPU will maintain a complete set of model parameters independently though parameter values across the GPUs are identical and synchronized. +As an example, +:numref:`fig_data_parallel` illustrates +training with +data parallelism when $k=2$. + + +![Calculation of minibatch stochastic gradient descent using data parallelism on two GPUs.](../img/data-parallel.svg) +:label:`fig_data_parallel` + +In general, the training proceeds as follows: + +* In any iteration of training, given a random minibatch, we split the examples in the batch into $k$ portions and distribute them evenly across the GPUs. +* Each GPU calculates loss and gradient of the model parameters based on the minibatch subset it was assigned. +* The local gradients of each of the $k$ GPUs are aggregated to obtain the current minibatch stochastic gradient. +* The aggregate gradient is re-distributed to each GPU. +* Each GPU uses this minibatch stochastic gradient to update the complete set of model parameters that it maintains. + + + + +Note that in practice we *increase* the minibatch size $k$-fold when training on $k$ GPUs such that each GPU has the same amount of work to do as if we were training on a single GPU only. On a 16-GPU server this can increase the minibatch size considerably and we may have to increase the learning rate accordingly. +Also note that batch normalization in :numref:`sec_batch_norm` needs to be adjusted, e.g., by keeping a separate batch normalization coefficient per GPU. +In what follows we will use a toy network to illustrate multi-GPU training. + +```{.python .input} +%matplotlib inline +from d2l import mxnet as d2l +from mxnet import autograd, gluon, np, npx +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +%matplotlib inline +from d2l import torch as d2l +import torch +from torch import nn +from torch.nn import functional as F +``` + +## A Toy Network + +We use LeNet as introduced in :numref:`sec_lenet` (with slight modifications). We define it from scratch to illustrate parameter exchange and synchronization in detail. + +```{.python .input} +# Initialize model parameters +scale = 0.01 +W1 = np.random.normal(scale=scale, size=(20, 1, 3, 3)) +b1 = np.zeros(20) +W2 = np.random.normal(scale=scale, size=(50, 20, 5, 5)) +b2 = np.zeros(50) +W3 = np.random.normal(scale=scale, size=(800, 128)) +b3 = np.zeros(128) +W4 = np.random.normal(scale=scale, size=(128, 10)) +b4 = np.zeros(10) +params = [W1, b1, W2, b2, W3, b3, W4, b4] + +# Define the model +def lenet(X, params): + h1_conv = npx.convolution(data=X, weight=params[0], bias=params[1], + kernel=(3, 3), num_filter=20) + h1_activation = npx.relu(h1_conv) + h1 = npx.pooling(data=h1_activation, pool_type='avg', kernel=(2, 2), + stride=(2, 2)) + h2_conv = npx.convolution(data=h1, weight=params[2], bias=params[3], + kernel=(5, 5), num_filter=50) + h2_activation = npx.relu(h2_conv) + h2 = npx.pooling(data=h2_activation, pool_type='avg', kernel=(2, 2), + stride=(2, 2)) + h2 = h2.reshape(h2.shape[0], -1) + h3_linear = np.dot(h2, params[4]) + params[5] + h3 = npx.relu(h3_linear) + y_hat = np.dot(h3, params[6]) + params[7] + return y_hat + +# Cross-entropy loss function +loss = gluon.loss.SoftmaxCrossEntropyLoss() +``` + +```{.python .input} +#@tab pytorch +# Initialize model parameters +scale = 0.01 +W1 = torch.randn(size=(20, 1, 3, 3)) * scale +b1 = torch.zeros(20) +W2 = torch.randn(size=(50, 20, 5, 5)) * scale +b2 = torch.zeros(50) +W3 = torch.randn(size=(800, 128)) * scale +b3 = torch.zeros(128) +W4 = torch.randn(size=(128, 10)) * scale +b4 = torch.zeros(10) +params = [W1, b1, W2, b2, W3, b3, W4, b4] + +# Define the model +def lenet(X, params): + h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1]) + h1_activation = F.relu(h1_conv) + h1 = F.avg_pool2d(input=h1_activation, kernel_size=(2, 2), stride=(2, 2)) + h2_conv = F.conv2d(input=h1, weight=params[2], bias=params[3]) + h2_activation = F.relu(h2_conv) + h2 = F.avg_pool2d(input=h2_activation, kernel_size=(2, 2), stride=(2, 2)) + h2 = h2.reshape(h2.shape[0], -1) + h3_linear = torch.mm(h2, params[4]) + params[5] + h3 = F.relu(h3_linear) + y_hat = torch.mm(h3, params[6]) + params[7] + return y_hat + +# Cross-entropy loss function +loss = nn.CrossEntropyLoss(reduction='none') +``` + +## Data Synchronization + +For efficient multi-GPU training we need two basic operations. +First we need to have the ability to distribute a list of parameters to multiple devices and to attach gradients (`get_params`). Without parameters it is impossible to evaluate the network on a GPU. +Second, we need the ability to sum parameters across multiple devices, i.e., we need an `allreduce` function. + +```{.python .input} +def get_params(params, device): + new_params = [p.copyto(device) for p in params] + for p in new_params: + p.attach_grad() + return new_params +``` + +```{.python .input} +#@tab pytorch +def get_params(params, device): + new_params = [p.clone().to(device) for p in params] + for p in new_params: + p.requires_grad_() + return new_params +``` + +Let us try it out by copying the model parameters to one GPU. + +```{.python .input} +#@tab all +new_params = get_params(params, d2l.try_gpu(0)) +print('b1 weight:', new_params[1]) +print('b1 grad:', new_params[1].grad) +``` + +Since we did not perform any computation yet, the gradient with regard to the bias parameter is still zero. +Now let us assume that we have a vector distributed across multiple GPUs. The following `allreduce` function adds up all vectors and broadcasts the result back to all GPUs. Note that for this to work we need to copy the data to the device accumulating the results. + +```{.python .input} +def allreduce(data): + for i in range(1, len(data)): + data[0][:] += data[i].copyto(data[0].ctx) + for i in range(1, len(data)): + data[0].copyto(data[i]) +``` + +```{.python .input} +#@tab pytorch +def allreduce(data): + for i in range(1, len(data)): + data[0][:] += data[i].to(data[0].device) + for i in range(1, len(data)): + data[i] = data[0].to(data[i].device) +``` + +Let us test this by creating vectors with different values on different devices and aggregate them. + +```{.python .input} +data = [np.ones((1, 2), ctx=d2l.try_gpu(i)) * (i + 1) for i in range(2)] +print('before allreduce:\n', data[0], '\n', data[1]) +allreduce(data) +print('after allreduce:\n', data[0], '\n', data[1]) +``` + +```{.python .input} +#@tab pytorch +data = [torch.ones((1, 2), device=d2l.try_gpu(i)) * (i + 1) for i in range(2)] +print('before allreduce:\n', data[0], '\n', data[1]) +allreduce(data) +print('after allreduce:\n', data[0], '\n', data[1]) +``` + +## Distributing Data + +We need a simple utility function to distribute a minibatch evenly across multiple GPUs. For instance, on two GPUs we would like to have half of the data to be copied to either of the GPUs. +Since it is more convenient and more concise, we use the built-in function from the deep learning framework to try it out on a $4 \times 5$ matrix. + +```{.python .input} +data = np.arange(20).reshape(4, 5) +devices = [npx.gpu(0), npx.gpu(1)] +split = gluon.utils.split_and_load(data, devices) +print('input :', data) +print('load into', devices) +print('output:', split) +``` + +```{.python .input} +#@tab pytorch +data = torch.arange(20).reshape(4, 5) +devices = [torch.device('cuda:0'), torch.device('cuda:1')] +split = nn.parallel.scatter(data, devices) +print('input :', data) +print('load into', devices) +print('output:', split) +``` + +For later reuse we define a `split_batch` function that splits both data and labels. + +```{.python .input} +#@save +def split_batch(X, y, devices): + """Split `X` and `y` into multiple devices.""" + assert X.shape[0] == y.shape[0] + return (gluon.utils.split_and_load(X, devices), + gluon.utils.split_and_load(y, devices)) +``` + +```{.python .input} +#@tab pytorch +#@save +def split_batch(X, y, devices): + """Split `X` and `y` into multiple devices.""" + assert X.shape[0] == y.shape[0] + return (nn.parallel.scatter(X, devices), + nn.parallel.scatter(y, devices)) +``` + +## Training + +Now we can implement multi-GPU training on a single minibatch. Its implementation is primarily based on the data parallelism approach described in this section. We will use the auxiliary functions we just discussed, `allreduce` and `split_and_load`, to synchronize the data among multiple GPUs. Note that we do not need to write any specific code to achieve parallelism. Since the computational graph does not have any dependencies across devices within a minibatch, it is executed in parallel *automatically*. + +```{.python .input} +def train_batch(X, y, device_params, devices, lr): + X_shards, y_shards = split_batch(X, y, devices) + with autograd.record(): # Loss is calculated separately on each GPU + ls = [loss(lenet(X_shard, device_W), y_shard) + for X_shard, y_shard, device_W in zip( + X_shards, y_shards, device_params)] + for l in ls: # Backpropagation is performed separately on each GPU + l.backward() + # Sum all gradients from each GPU and broadcast them to all GPUs + for i in range(len(device_params[0])): + allreduce([device_params[c][i].grad for c in range(len(devices))]) + # The model parameters are updated separately on each GPU + for param in device_params: + d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch +``` + +```{.python .input} +#@tab pytorch +def train_batch(X, y, device_params, devices, lr): + X_shards, y_shards = split_batch(X, y, devices) + # Loss is calculated separately on each GPU + ls = [loss(lenet(X_shard, device_W), y_shard).sum() + for X_shard, y_shard, device_W in zip( + X_shards, y_shards, device_params)] + for l in ls: # Backpropagation is performed separately on each GPU + l.backward() + # Sum all gradients from each GPU and broadcast them to all GPUs + with torch.no_grad(): + for i in range(len(device_params[0])): + allreduce([device_params[c][i].grad for c in range(len(devices))]) + # The model parameters are updated separately on each GPU + for param in device_params: + d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch +``` + +Now, we can define the training function. It is slightly different from the ones used in the previous chapters: we need to allocate the GPUs and copy all the model parameters to all the devices. +Obviously each batch is processed using the `train_batch` function to deal with multiple GPUs. For convenience (and conciseness of code) we compute the accuracy on a single GPU, though this is *inefficient* since the other GPUs are idle. + +```{.python .input} +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + # Copy model parameters to `num_gpus` GPUs + device_params = [get_params(params, d) for d in devices] + num_epochs = 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + timer = d2l.Timer() + for epoch in range(num_epochs): + timer.start() + for X, y in train_iter: + # Perform multi-GPU training for a single minibatch + train_batch(X, y, device_params, devices, lr) + npx.waitall() + timer.stop() + # Evaluate the model on GPU 0 + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( + lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(devices)}') +``` + +```{.python .input} +#@tab pytorch +def train(num_gpus, batch_size, lr): + train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) + devices = [d2l.try_gpu(i) for i in range(num_gpus)] + # Copy model parameters to `num_gpus` GPUs + device_params = [get_params(params, d) for d in devices] + num_epochs = 10 + animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) + timer = d2l.Timer() + for epoch in range(num_epochs): + timer.start() + for X, y in train_iter: + # Perform multi-GPU training for a single minibatch + train_batch(X, y, device_params, devices, lr) + torch.cuda.synchronize() + timer.stop() + # Evaluate the model on GPU 0 + animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( + lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) + print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' + f'on {str(devices)}') +``` + +Let us see how well this works on a single GPU. +We first use a batch size of 256 and a learning rate of 0.2. + +```{.python .input} +#@tab all +train(num_gpus=1, batch_size=256, lr=0.2) +``` + +By keeping the batch size and learning rate unchanged and increasing the number of GPUs to 2, we can see that the test accuracy roughly stays the same compared with +the previous experiment. +In terms of the optimization algorithms, they are identical. Unfortunately there is no meaningful speedup to be gained here: the model is simply too small; moreover we only have a small dataset, where our slightly unsophisticated approach to implementing multi-GPU training suffered from significant Python overhead. We will encounter more complex models and more sophisticated ways of parallelization going forward. +Let us see what happens nonetheless for Fashion-MNIST. + +```{.python .input} +#@tab all +train(num_gpus=2, batch_size=256, lr=0.2) +``` + +## Summary + +* There are multiple ways to split deep network training over multiple GPUs. We could split them between layers, across layers, or across data. The former two require tightly choreographed data transfers. Data parallelism is the simplest strategy. +* Data parallel training is straightforward. However, it increases the effective minibatch size to be efficient. +* In data parallelism, data are split across multiple GPUs, where each GPU executes its own forward and backward operation and subsequently gradients are aggregated and results are broadcast back to the GPUs. +* We may use slightly increased learning rates for larger minibatches. + +## Exercises + +1. When training on $k$ GPUs, change the minibatch size from $b$ to $k \cdot b$, i.e., scale it up by the number of GPUs. +1. Compare accuracy for different learning rates. How does it scale with the number of GPUs? +1. Implement a more efficient `allreduce` function that aggregates different parameters on different GPUs? Why is it more efficient? +1. Implement multi-GPU test accuracy computation. + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/364) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1669) +:end_tab: diff --git a/chapter_computational-performance/parameterserver.md b/chapter_computational-performance/parameterserver.md new file mode 100644 index 000000000..1ad42cba7 --- /dev/null +++ b/chapter_computational-performance/parameterserver.md @@ -0,0 +1,101 @@ +# 参数服务器 +:label:`sec_parameterserver` + +当我们从单个 GPU 迁移到多个 GPU,然后迁移到包含多个 GPU 的多台服务器(可能全部分布在多个机架和网络交换机上)时,我们的分布式和并行训练算法需要变得更加复杂。细节很重要,因为不同的互连具有非常不同的带宽(例如,nvLink 可以在适当的设置下在 6 条链路上提供高达 100 Gb/s 的宽度,PCIe 4.0(16 通道)提供 32 Gb/s,甚至高速 100GbE 以太网也只能达到 10 Gb/s)。同时,期望统计建模人员成为网络和系统方面的专家是不合理的。 + +参数服务器的核心理念是在 :cite:`Smola.Narayanamurthy.2010` 中在分布式潜在变量模型的背景下引入的。随后在 :cite:`Ahmed.Aly.Gonzalez.ea.2012` 中对推拉语义进行了描述,随后在 :cite:`Li.Andersen.Park.ea.2014` 中对系统和开源库进行了描述。在下面我们将激励提高效率所需的组件。 + +## 数据并行培训 + +让我们回顾一下分布式培训的数据并行培训方法。我们将使用它来排除本节中的所有其他内容,因为它在实践中的实施要简单得多。几乎没有任何其他并行策略首选的用例(除了图表上的深度学习之外),因为 GPU 现在有足够的内存。:numref:`fig_parameterserver` 描述了我们在 :numref:`sec_multi_gpu` 中实施的数据并行度的变体。其中的关键方面是,渐变聚合发生在 GPU 0 上,然后再将更新的参数重新广播到所有 GPU。 + +![Left: single GPU training. Right: a variant of multi-GPU training: (1) we compute loss and gradient, (2) all gradients are aggregated on one GPU, (3) parameter update happens and the parameters are re-distributed to all GPUs.](../img/ps.svg) +:label:`fig_parameterserver` + +回想起来,对 GPU 0 进行聚合的决定似乎相当临时。毕竟,我们也许也可以在 CPU 上聚合起来。事实上,我们甚至可以决定在一个 GPU 上聚合一些参数,另一个 GPU 上的一些参数聚合起来。如果优化算法支持这一点,那么我们没有真正的理由不能。例如,如果我们有四个带有相关渐变 $\mathbf{g}_1, \ldots, \mathbf{g}_4$ 的参数向量,我们可以在一个 GPU 上聚合每个 $\mathbf{g}_i$ ($i = 1, \ldots, 4$) 的渐变。 + +这种推理似乎是武断和轻率的。毕竟,数学始终是一样的。但是,我们正在处理真实的物理硬件,其中不同的总线具有不同的带宽,如 :numref:`sec_hardware` 中所述。考虑一个真正的 4 路 GPU 服务器,如 :numref:`fig_bw_hierarchy` 中所述。如果它的连接特别好,它可能有 100 GbE 网卡。更典型的数字在 1—10 GbE 范围内,有效带宽为 100 MB/s 至 1 Gb/s。由于 CPU 的 PCIe 通道太少,无法直接连接到所有 GPU(例如,消费级英特尔 CPU 有 24 条通道),我们需要 [multiplexer](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。16x Gen3 链路上 CPU 的带宽为 16 Gb/s。这也是每个 GPU 连接到交换机的速度。这意味着设备之间的通信更有效。 + +![A 4-way GPU server.](../img/bw-hierarchy.svg) +:label:`fig_bw_hierarchy` + +为了这个论点,让我们假设渐变是 160 MB。在这种情况下,将所有剩余 3 个 GPU 的渐变发送到第四个 GPU 需要 30 毫秒(每次传输需要 10 毫秒 = 160 MB /16 Gb/s)。再增加 30 毫秒以将重量向量传送回来,我们总共达到 60 毫秒。如果我们将所有数据发送到 CPU,我们将受到 40 毫秒的罚款,因为四个 GPU 中的 * 每个 * 都需要将数据发送到 CPU,总共产生 80 毫秒。最后假设我们能够将渐变分成 4 个部分,每个 40 MB。现在我们可以将每个部分同时聚合在不同的 GPU * 上,因为 PCIe 交换机在所有链路之间提供了全带宽操作。而不是 30 毫秒,这需要 7.5 毫秒,同步操作总共产生 15 毫秒的时间。简而言之,根据我们同步参数的方式,同一操作可能需要 15 毫秒到 80 毫秒的任何时间。:numref:`fig_ps_distributed` 描述了交换参数的不同策略。 + +![Parameter synchronization strategies.](../img/ps-distributed.svg) +:label:`fig_ps_distributed` + +请注意,在提高绩效方面,我们还有另一种工具可供我们使用。: in a deep network it takes some time to compute all gradients from the top to the bottom. We can begin synchronizing gradients for some parameter groups even while we are still busy computing them for others. See e.g., :cite:`Sergeev.Del-Balso.2018` 有关如何在 [Horovod](https://github.com/horovod/horovod) 中做到这一点的详细信息。 + +## 振铃同步 + +当涉及到现代深度学习硬件的同步时,我们经常会遇到大量定制的网络连接。例如,AWS p3.16xlarge 和 NVIDIA DGX-2 实例共享 :numref:`fig_nvlink` 的连接结构。每个 GPU 都通过 PCIe 链路连接到主机 CPU,该链路最多运行时间为 16 Gb/s。此外,每个 GPU 还有 6 个 nvLink 连接,每个连接都能双向传输 300 Gbit/s。这相当于每个方向每条链路约 18 Gb/s。简而言之,总的 nvLink 带宽明显高于 PCIe 带宽。问题是如何最有效地使用它。 + +![NVLink connectivity on 8 V100 GPU servers (image courtesy of NVIDIA).](../img/nvlink.svg) +:label:`fig_nvlink` + +事实证明,最佳同步策略是将网络分解为两个环,然后使用它们直接同步数据 :cite:`Wang.Li.Liberty.ea.2018`。:numref:`fig_nvlink_twoloop` 说明,可以将网络分解为带双 NVLink 带宽的一个环(1-2-3-4-5-6-7-8-1),常规带宽。在这种情况下,设计高效的同步协议非常重要。 + +![Decomposition of the NVLink network into two rings.](../img/nvlink-twoloop.svg) +:label:`fig_nvlink_twoloop` + +考虑下面的思维实验:给定 $n$ 个计算节点(或 GPU)的环,我们可以将梯度从第一个节点发送到第二个节点。在那里,它被添加到局部渐变中并发送到第三个节点,依此类推。$n-1$ 步之后,可以在上次访问的节点中找到聚合渐变。也就是说,聚合渐变的时间随节点数量线性增长。但是,如果我们这样做,算法就非常低效。毕竟,在任何时候都只有一个节点通信。如果我们将梯度分解为 $n$ 块并开始从节点 $i$ 开始同步块 $i$,该怎么办?由于每个区块的大小为 $1/n$,所以现在的总时间是 $(n-1)/n \approx 1$。换句话说,随着我们增加戒指尺寸,聚合渐变所花费的时间 * 不会增加 *。这是一个非常惊人的结果。:numref:`fig_ringsync` 说明了 $n=4$ 节点上的步骤顺序。 + +![Ring synchronization across 4 nodes. Each node starts transmitting parts of gradients to its left neighbor until the assembled gradient can be found in its right neighbor.](../img/ringsync.svg) +:label:`fig_ringsync` + +如果我们使用同样的示例在 8 个 V100 GPU 之间同步 160 MB,我们的目标是大约 $2 \cdot 160 \mathrm{MB} / (3 \cdot 18 \mathrm{GB/s}) \approx 6 \mathrm{ms}$。尽管我们现在正在使用 8 个 GPU,但这比使用 PCIe 总线更好。请注意,实际上这些数字有点差,因为深度学习框架通常无法将通信组合为大规模突发传输。 + +请注意,有一种常见的误解是,环形同步与其他同步算法有根本不同。唯一的区别是,与简单的树相比,同步路径更加精细。 + +## 多机培训 + +在多台机器上进行分布式培训增加了进一步的挑战:我们需要与仅通过相对较低带宽的结构连接的服务器进行通信,在某些情况下,这种结构可能会慢一个数量级以上。跨设备同步非常棘手。毕竟,运行训练代码的不同机器将具有微妙的不同速度。因此,如果我们想使用同步分布式优化,我们需要 * 同步 * 它们。:numref:`fig_ps_multimachine` 说明了分布式并行训练的发生方式。 + +1. 在每台计算机上读取一批(不同)数据,分割到多个 GPU 之间,然后传输到 GPU 内存中。每个 GPU 批次上都会分别计算预测和梯度。 +2. 来自所有本地 GPU 的渐变聚合在一个 GPU 上(或者其中的一部分聚合在不同的 GPU 上)。 +3. 渐变将发送到 CPU。 +4. CPU 将渐变发送到聚合所有渐变的中央参数服务器。 +5. 然后使用聚合渐变来更新参数,更新后的参数将广播回各个 CPU。 +6. 信息被发送到一个(或多个)GPU。 +7. 更新后的参数分布在所有 GPU 中。 + +![Multi-machine multi-GPU distributed parallel training.](../img/ps-multimachine.svg) +:label:`fig_ps_multimachine` + +这些操作中的每一项似乎都相当简单。而且,事实上,它们可以在单台机器内 * 高效地执行。但是,一旦我们看到多台机器,我们可以看到中央参数服务器成为瓶颈。毕竟,每台服务器的带宽是有限的,因此对于 $m$ 个工作人员,将所有梯度发送到服务器所需的时间是 $\mathcal{O}(m)$。我们可以通过将服务器数量增加到 $n$ 来突破这一障碍。此时,每台服务器只需存储 $\mathcal{O}(1/n)$ 参数,因此更新和优化的总时间将变为 $\mathcal{O}(m/n)$。无论我们正在处理多少工作人员,匹配这两个数字都可以持续扩展。实际上,我们使用 * 相同 * 机器作为工作人员和服务器。:numref:`fig_ps_multips` 说明了设计(有关详细信息,另请参阅 :cite:`Li.Andersen.Park.ea.2014`)。特别是,确保多台机器在没有不合理的延迟的情况下工作是非常重要的。我们省略了关于障碍的详细信息,只会在下面简要介绍同步和异步更新。 + +![Top: a single parameter server is a bottleneck since its bandwidth is finite. Bottom: multiple parameter servers store parts of the parameters with aggregate bandwidth.](../img/ps-multips.svg) +:label:`fig_ps_multips` + +## 钥匙-价值存储 + +在实践中执行分布式多 GPU 培训所需的步骤并非微不足道。这就是为什么使用通用抽象是值得的,即具有重新定义更新语义的 * 键值存储 * 的抽象。 + +在许多工作人员和许多 GPU 中,梯度 $i$ 的计算可以定义为 + +$$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{g}_{ijk},$$ + +其中 $\mathbf{g}_{ijk}$ 是在工人 $k$ 的 GPU $j$ 上分割的梯度 $i$ 的一部分。此操作的关键方面是它是 * 交换减少 *,也就是说,它将许多向量变成一个矢量,应用操作的顺序无关紧要。这对我们来说非常棒,因为我们不(需要)对何时接收哪个梯度进行精细的控制。此外,请注意,此操作在不同的 $i$ 之间是独立的。 + +这使我们可以定义以下两种操作:* push *(累积渐变)和 *pull*(检索聚合渐变)。由于我们有许多不同的渐变集(毕竟,我们有很多图层),因此我们需要用键 $i$ 对渐变进行索引。与密钥价值存储的这种相似之处,例如 Dynamo :cite:`DeCandia.Hastorun.Jampani.ea.2007` 中引入的那种类似之处并非巧合。它们也满足许多类似的特征,特别是在将参数分配到多台服务器时。 + +键值存储的推拉操作描述如下: + +* **push(键、值)** 将工作线程中的特定渐变(值)发送到公共存储器。在那里,这个值是汇总的,例如,通过对其进行汇总。 +* **pull(键、value)** 从公共存储中检索聚合值,例如,在合并来自所有工作人员的梯度之后。 + +通过隐藏简单的推拉操作背后的所有同步复杂性,我们可以解决希望能够简单地表达优化的统计建模师和需要处理分布式同步固有的复杂性的系统工程师的担忧。 + +## 摘要 + +* 同步需要高度适应服务器内的特定网络基础架构和连接。这可能会对同步所需的时间产生重大影响。 +* 对于 p3 和 DGX-2 服务器来说,环形同步可能是最佳选择。对于其他人来说可能不太多。 +* 当添加多个参数服务器以增加带宽时,分层同步策略效果很好。 + +## 练习 + +1. 你能进一步增加振铃同步吗?提示:你可以双向发送消息。 +1. 是否可以允许异步通信(在计算仍在进行时)?它如何影响性能? +1. 如果我们在长时间运行的计算中丢失了服务器怎么办?我们如何设计一个 * 容错 * 机制来避免完全重新启动计算? + +[Discussions](https://discuss.d2l.ai/t/366) diff --git a/chapter_computational-performance/parameterserver_origin.md b/chapter_computational-performance/parameterserver_origin.md new file mode 100644 index 000000000..b8c90a501 --- /dev/null +++ b/chapter_computational-performance/parameterserver_origin.md @@ -0,0 +1,119 @@ +# Parameter Servers +:label:`sec_parameterserver` + +As we move from a single GPU to multiple GPUs and then to multiple servers containing multiple GPUs, possibly all spread out across multiple racks and network switches, +our algorithms for distributed and parallel training need to become much more sophisticated. Details matter since different interconnects have very different bandwidth (e.g., NVLink can offer up to 100 GB/s across 6 links in an appropriate setting, PCIe 4.0 (16-lane) offers 32 GB/s, while even high speed 100GbE Ethernet only amounts to 10 GB/s). At the same time it is unreasonable to expect that a statistical modeler be an expert in networking and systems. + +The core idea of the parameter server was introduced in :cite:`Smola.Narayanamurthy.2010` in the context of distributed latent variable models. A description of the push and pull semantics then followed in :cite:`Ahmed.Aly.Gonzalez.ea.2012` and a description of the system and an open source library followed in :cite:`Li.Andersen.Park.ea.2014`. In the following we will motivate the components needed for efficiency. + + +## Data-Parallel Training + +Let us review the data parallel training approach to distributed training. We will use this to the exclusion of all others in this section since it is significantly simpler to implement in practice. There are virtually no use cases (besides deep learning on graphs) where any other strategy for parallelism is preferred since GPUs have plenty of memory nowadays. :numref:`fig_parameterserver` describes the variant of data parallelism that we implemented in :numref:`sec_multi_gpu`. The key aspect in it is that the aggregation of gradients occurs on GPU 0 before the updated parameters are rebroadcast to all GPUs. + +![Left: single GPU training. Right: a variant of multi-GPU training: (1) we compute loss and gradient, (2) all gradients are aggregated on one GPU, (3) parameter update happens and the parameters are re-distributed to all GPUs.](../img/ps.svg) +:label:`fig_parameterserver` + +In retrospect, the decision to aggregate on GPU 0 seems rather ad-hoc. After all, we might just as well aggregate on the CPU. In fact, we could even decide to aggregate some of the parameters on one GPU and some others on another. Provided that the optimization algorithm supports this, there is no real reason for why we could not. For instance, if we have four parameter vectors with associated gradients $\mathbf{g}_1, \ldots, \mathbf{g}_4$ we could aggregate the gradients on one GPU for each $\mathbf{g}_i$ ($i = 1, \ldots, 4$). + + +This reasoning seems arbitrary and frivolous. After all, the mathematics is the same throughout. However, we are dealing with real physical hardware where different buses have different bandwidth as discussed in :numref:`sec_hardware`. +Consider a real 4-way GPU server as described in :numref:`fig_bw_hierarchy`. If it is particularly well connected, it might have a 100 GbE network card. More typical numbers are in the 1--10 GbE range with an effective bandwidth of 100 MB/s to 1 GB/s. +Since the CPUs have too few PCIe lanes to connect to all GPUs directly (e.g., consumer-grade Intel CPUs have 24 lanes) we need a [multiplexer](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches). The bandwidth from the CPU on a 16x Gen3 link is 16 GB/s. This is also the speed at which *each* of the GPUs is connected to the switch. This means that it is more effective to communicate between the devices. + +![A 4-way GPU server.](../img/bw-hierarchy.svg) +:label:`fig_bw_hierarchy` + +For the sake of the argument let us assume that the gradients are of 160 MB. In this case it takes 30 ms to send the gradients from all 3 remaining GPUs to the fourth one (each transfer takes 10 ms = 160 MB / 16 GB/s). Adding another 30 ms to transmit the weight vectors back we arrive at a total of 60 ms. +If we send all data to the CPU we incur a penalty of 40 ms since *each* of the four GPUs needs to send the data to the CPU, yielding a total of 80 ms. Lastly assume that we are able to split the gradients into 4 parts of 40 MB each. Now we can aggregate each of the parts on a different GPU *simultaneously* since the PCIe switch offers a full-bandwidth operation between all links. Instead of 30 ms this takes 7.5 ms, yielding a total of 15 ms for a synchronization operation. In short, depending on how we synchronize parameters the same operation can take anywhere from 15 ms to 80 ms. :numref:`fig_ps_distributed` depicts the different strategies for exchanging parameters. + +![Parameter synchronization strategies.](../img/ps-distributed.svg) +:label:`fig_ps_distributed` + +Note that we have yet another tool at our disposal when it comes to improving performance: in a deep network it takes some time to compute all gradients from the top to the bottom. We can begin synchronizing gradients for some parameter groups even while we are still busy computing them for others. See e.g., :cite:`Sergeev.Del-Balso.2018` for details on how to do this in [Horovod](https://github.com/horovod/horovod). + +## Ring Synchronization + +When it comes to synchronization on modern deep learning hardware we often encounter significantly bespoke network connectivity. For instance, the AWS p3.16xlarge and NVIDIA DGX-2 instances share the connectivity structure of :numref:`fig_nvlink`. Each GPU connects to a host CPU via a PCIe link which operates at best at 16 GB/s. Additionally each GPU also has 6 NVLink connections, each of which is capable of transferring 300 Gbit/s bidirectionally. This amounts to around 18 GB/s per link per direction. In short, the aggregate NVLink bandwidth is significantly higher than the PCIe bandwidth. The question is how to use it most efficiently. + +![NVLink connectivity on 8 V100 GPU servers (image courtesy of NVIDIA).](../img/nvlink.svg) +:label:`fig_nvlink` + +It turns out that the optimal synchronization strategy is to decompose the network into two rings and to use them to synchronize data directly :cite:`Wang.Li.Liberty.ea.2018`. :numref:`fig_nvlink_twoloop` illustrates that the network can be decomposed into one ring (1-2-3-4-5-6-7-8-1) with double NVLink bandwidth and into one (1-4-6-3-5-8-2-7-1) with regular bandwidth. Designing an efficient synchronization protocol in this case is nontrivial. + +![Decomposition of the NVLink network into two rings.](../img/nvlink-twoloop.svg) +:label:`fig_nvlink_twoloop` + + +Consider the following thought experiment: given a ring of $n$ computing nodes (or GPUs) we can send gradients from the first to the second node. There it is added to the local gradient and sent on to the third node, and so on. After $n-1$ steps the aggregate gradient can be found in the last-visited node. That is, the time to aggregate gradients grows linearly with the number of nodes. But if we do this the algorithm is quite inefficient. After all, at any time there is only one of the nodes communicating. What if we broke the gradients into $n$ chunks and started synchronizing chunk $i$ starting at node $i$? +Since each chunk is of size $1/n$ the total time is now $(n-1)/n \approx 1$. In other words, the time spent to aggregate gradients *does not grow* as we increase the size of the ring. This is quite an astonishing result. :numref:`fig_ringsync` illustrates the sequence of steps on $n=4$ nodes. + +![Ring synchronization across 4 nodes. Each node starts transmitting parts of gradients to its left neighbor until the assembled gradient can be found in its right neighbor.](../img/ringsync.svg) +:label:`fig_ringsync` + +If we use the same example of synchronizing 160 MB across 8 V100 GPUs we arrive at approximately $2 \cdot 160 \mathrm{MB} / (3 \cdot 18 \mathrm{GB/s}) \approx 6 \mathrm{ms}$. This is better than using the PCIe bus, even though we are now using 8 GPUs. Note that in practice these numbers are a bit worse, since deep learning frameworks often fail to assemble communication into large burst transfers. + +Note that there is a common misconception that ring synchronization is fundamentally different from other synchronization algorithms. The only difference is that the synchronization path is somewhat more elaborate when compared with a simple tree. + +## Multi-Machine Training + +Distributed training on multiple machines adds a further challenge: we need to communicate with servers that are only connected across a comparatively lower bandwidth fabric that can be over an order of magnitude slower in some cases. +Synchronization across devices is tricky. After all, different machines running training code will have subtly different speed. Hence we need to *synchronize* them if we want to use synchronous distributed optimization. :numref:`fig_ps_multimachine` illustrates how distributed parallel training occurs. + +1. A (different) batch of data are read on each machine, split across multiple GPUs and transferred to GPU memory. There predictions and gradients are computed on each GPU batch separately. +2. The gradients from all local GPUs are aggregated on one GPU (or parts of it are aggregated over different GPUs). +3. The gradients are sent to the CPUs. +4. The CPUs send the gradients to a central parameter server which aggregates all the gradients. +5. The aggregate gradients are then used to update the parameters and the updated parameters are broadcast back to the individual CPUs. +6. The information is sent to one (or multiple) GPUs. +7. The updated parameters are spread across all GPUs. + +![Multi-machine multi-GPU distributed parallel training.](../img/ps-multimachine.svg) +:label:`fig_ps_multimachine` + +Each of these operations seems rather straightforward. And, indeed, they can be carried out efficiently *within* a single machine. Once we look at multiple machines, though, we can see that the central parameter server becomes the bottleneck. After all, the bandwidth per server is limited, hence for $m$ workers the time it takes to send all gradients to the server is $\mathcal{O}(m)$. We can break through this barrier by increasing the number of servers to $n$. At this point each server only needs to store $\mathcal{O}(1/n)$ of the parameters, hence the total time for updates and optimization becomes $\mathcal{O}(m/n)$. +Matching both numbers yields constant scaling regardless of how many workers we are dealing with. In practice we use the *same* machines both as workers and as servers. :numref:`fig_ps_multips` illustrates the design (see also :cite:`Li.Andersen.Park.ea.2014` for details). +In particular, ensuring that multiple machines work without unreasonable delays is nontrivial. We omit details on barriers and will only briefly touch on synchronous and asynchronous updates below. + +![Top: a single parameter server is a bottleneck since its bandwidth is finite. Bottom: multiple parameter servers store parts of the parameters with aggregate bandwidth.](../img/ps-multips.svg) +:label:`fig_ps_multips` + +## Key--Value Stores + +Implementing the steps required for distributed multi-GPU training in practice is nontrivial. +This is why it pays to use a common abstraction, namely that of a *key--value store* with redefined update semantics. + + +Across many workers and many GPUs the computation for gradient $i$ can be defined as + +$$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{g}_{ijk},$$ + +where $\mathbf{g}_{ijk}$ is part of gradient $i$ split on GPU $j$ of worker $k$. +The key aspect in this operation is that it is a *commutative reduction*, that is, it turns many vectors into one and the order in which the operation is applied does not matter. This is great for our purposes since we do not (need to) have fine grained control over when which gradient is received. Besides, note that this operation is independent among different $i$. + +This allows us to define the following two operations: *push*, which accumulates gradients, and *pull*, which retrieves aggregate gradients. Since we have many different sets of gradients (after all, we have many layers), we need to index the gradients with a key $i$. This similarity to key--value stores, such as the one introduced in Dynamo +:cite:`DeCandia.Hastorun.Jampani.ea.2007` is not by coincidence. They, too, satisfy many similar characteristics, in particular when it comes to distributing the parameters across multiple servers. + + +The push and pull operations for key-value stores are described as follows: + +* **push(key, value)** sends a particular gradient (the value) from a worker to a common storage. There the value is aggregated, e.g., by summing it up. +* **pull(key, value)** retrieves an aggregate value from common storage, e.g., after combining the gradients from all workers. + +By hiding all the complexity about synchronization behind a simple push and pull operation we can decouple the concerns of statistical modelers who want to be able to express optimization in simple terms and the system engineers who need to deal with the complexity inherent in distributed synchronization. + +## Summary + +* Synchronization needs to be highly adaptive to specific network infrastructure and connectivity within a server. This can make a significant difference to the time it takes to synchronize. +* Ring-synchronization can be optimal for p3 and DGX-2 servers. For others possibly not so much. +* A hierarchical synchronization strategy works well when adding multiple parameter servers for increased bandwidth. + + +## Exercises + +1. Can you increase the ring synchronization even further? Hint: you can send messages in both directions. +1. Is it possible to allow asynchronous communication (while computation is still ongoing)? How does it affect performance? +1. What if we lost a server during a long-running computation? How can we design a *fault tolerance* mechanism to avoid restarting the computation fully? + + +[Discussions](https://discuss.d2l.ai/t/366) From 66dd24af5ced25ef4ac58abe8dbb36accda96c29 Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Sat, 24 Apr 2021 01:07:04 +0200 Subject: [PATCH 063/103] translation issue in mlp.md (#762) * translation issue in mlp.md * translation issue in mlp-scratch.md --- chapter_multilayer-perceptrons/mlp-scratch.md | 2 +- chapter_multilayer-perceptrons/mlp.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/chapter_multilayer-perceptrons/mlp-scratch.md b/chapter_multilayer-perceptrons/mlp-scratch.md index 23fcee037..9037a062c 100644 --- a/chapter_multilayer-perceptrons/mlp-scratch.md +++ b/chapter_multilayer-perceptrons/mlp-scratch.md @@ -30,7 +30,7 @@ train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) ## 初始化模型参数 -回想一下,Fashion-MNIST中的每个图像由$28 \times 28 = 784$个灰度像素值组成。所有图像共分为10个类别。忽略像素之间的空间结构,我们可以将每个图像视为具有784个输入特征和10个类的简单分类数据集。首先,我们将[**实现一个具有单隐藏层的多层感知机,它包含256个隐藏单元**]。注意,我们可以将这两个量都视为超参数。通常,我们选择2的幂次方作为层的宽度。因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。 +回想一下,Fashion-MNIST中的每个图像由$28 \times 28 = 784$个灰度像素值组成。所有图像共分为10个类别。忽略像素之间的空间结构,我们可以将每个图像视为具有784个输入特征和10个类的简单分类数据集。首先,我们将[**实现一个具有单隐藏层的多层感知机,它包含256个隐藏单元**]。注意,我们可以将这两个量都视为超参数。通常,我们选择2的若干次幂作为层的宽度。因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。 我们用几个张量来表示我们的参数。注意,对于每一层我们都要记录一个权重矩阵和一个偏置向量。跟以前一样,我们要为这些参数的损失的梯度分配内存。 diff --git a/chapter_multilayer-perceptrons/mlp.md b/chapter_multilayer-perceptrons/mlp.md index 128d91468..da1221858 100644 --- a/chapter_multilayer-perceptrons/mlp.md +++ b/chapter_multilayer-perceptrons/mlp.md @@ -41,9 +41,9 @@ $$ \end{aligned} $$ -注意,在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。可我们能从中得到什么好处呢?你可能会惊讶地发现:在上面定义的模型里,我们没有好处!原因很简单。上面的隐藏单元由输入的仿射函数给出,而输出(softmax操作前)只是隐藏单元的仿射函数。仿射函数的仿射函数本身就是仿射函数。但是线性模型已经能够表示任何仿射函数。 +注意,在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。可我们能从中得到什么好处呢?你可能会惊讶地发现:在上面定义的模型里,我们没有好处!原因很简单。上面的隐藏单元由输入的仿射函数给出,而输出(softmax操作前)只是隐藏单元的仿射函数。仿射函数的仿射函数本身就是仿射函数。但是我们之前的线性模型已经能够表示任何仿射函数。 -我们可以正式地确定等价性,对于任意权重值,我们只需合并隐藏层,即可产生具有参数$\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}$和$\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}$的等价单层模型: +我们可以证明这一等价性,即对于任意权重值,我们只需合并隐藏层,便可产生具有参数$\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}$和$\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}$的等价单层模型: $$ \mathbf{O} = (\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})\mathbf{W}^{(2)} + \mathbf{b}^{(2)} = \mathbf{X} \mathbf{W}^{(1)}\mathbf{W}^{(2)} + \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)} = \mathbf{X} \mathbf{W} + \mathbf{b}. @@ -58,7 +58,7 @@ $$ \end{aligned} $$ -由于$\mathbf{X}$中的每一行对应于小批量中的一个样本,即我们定义非线性函数$\sigma$以按行的方式应用于其输入,即,一次计算一个样本。我们在 :numref:`subsec_softmax_vectorization` 中以相同的方式使用了softmax符号来表示按行操作。但是在本节中,我们应用于隐藏层的激活函数通常不仅仅是按行的,而且也是按元素。 +由于$\mathbf{X}$中的每一行对应于小批量中的一个样本,出于记号习惯的考量,我们定义非线性函数$\sigma$也以按行的方式作用于其输入,即一次计算一个样本。我们在 :numref:`subsec_softmax_vectorization` 中以相同的方式使用了softmax符号来表示按行操作。但是在本节中,我们应用于隐藏层的激活函数通常不仅仅是按行的,而且也是按元素。 这意味着在计算每一层的线性部分之后,我们可以计算每个激活值,而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。 为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,例如,$\mathbf{H}^{(1)} = \sigma_1(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})$和$\mathbf{H}^{(2)} = \sigma_2(\mathbf{H}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)})$,一层叠一层,从而产生更有表达能力的模型。 @@ -127,7 +127,7 @@ y = tf.nn.relu(x) d2l.plot(x.numpy(), y.numpy(), 'x', 'relu(x)', figsize=(5, 2.5)) ``` -当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。注意,当输入值精确等于0时,ReLU函数不可导。在此时,我们默认使用左侧的导数,即当输入为0时导数为0。我们可以忽略这种情况,因为输入可能永远都不会是0。这里用上一句古老的谚语,“如果微妙的边界条件很重要,我们很可能是在做数学而非工程”,这个观点正好适用于这里。下面我们绘制ReLU函数的导数。 +当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。注意,当输入值精确等于0时,ReLU函数不可导。在此时,我们默认使用左侧的导数,即当输入为0时导数为0。我们可以忽略这种情况,因为输入可能永远都不会是0。这里用上一句古老的谚语,“如果微妙的边界条件很重要,我们很可能是在研究数学而非工程”,这个观点正好适用于这里。下面我们绘制ReLU函数的导数。 ```{.python .input} y.backward() @@ -188,7 +188,7 @@ sigmoid函数的导数为下面的公式: $$\frac{d}{dx} \operatorname{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right).$$ -sigmoid函数的导数图像如下所示。注意,当输入为0时,sigmoid函数的导数达到最大值0.25。当输入在任一方向上远离0点时,导数会更靠近0。 +sigmoid函数的导数图像如下所示。注意,当输入为0时,sigmoid函数的导数达到最大值0.25。而输入在任一方向上越远离0点,导数越接近0。 ```{.python .input} y.backward() @@ -241,7 +241,7 @@ tanh函数的导数是: $$\frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x).$$ -tanh函数的导数图像如下所示。当输入接近0时,tanh函数的导数接近最大值1。与我们在sigmoid函数图像中看到的类似,当输入在任一方向上远离0点时,导数会更靠近0。 +tanh函数的导数图像如下所示。当输入接近0时,tanh函数的导数接近最大值1。与我们在sigmoid函数图像中看到的类似,输入在任一方向上越远离0点,导数越接近0。 ```{.python .input} y.backward() From a5479decf1dea70be839ea6383085d106e784022 Mon Sep 17 00:00:00 2001 From: HelloWorld Date: Sat, 24 Apr 2021 07:07:53 +0800 Subject: [PATCH 064/103] Update lookup-api.md (#763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix one word '这' --- chapter_preliminaries/lookup-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preliminaries/lookup-api.md b/chapter_preliminaries/lookup-api.md index ebd043022..dc37999d8 100644 --- a/chapter_preliminaries/lookup-api.md +++ b/chapter_preliminaries/lookup-api.md @@ -1,6 +1,6 @@ # 查阅文档 :begin_tab:`mxnet` -由于这本书篇幅的限制,我们不可能介绍每一个 MXNet 函数和类(你可能也不希望我们这样做)。API文档、其他教程和示例提供了本书之外的大量文档。在本节中,我们为你提供了一些查看MXNet API 的指导。 +由于本书篇幅的限制,我们不可能介绍每一个 MXNet 函数和类(你可能也不希望我们这样做)。API文档、其他教程和示例提供了本书之外的大量文档。在本节中,我们为你提供了一些查看MXNet API 的指导。 :end_tab: :begin_tab:`pytorch` From 74d50911288fdc6734d23f45ff098ecc1c89d8b5 Mon Sep 17 00:00:00 2001 From: thebesttv Date: Mon, 26 Apr 2021 10:21:59 +0800 Subject: [PATCH 065/103] update translations in underfit-overfit.md (#746) * update translations in underfit-overfit.md * update underfit-overfit.md * Update underfit-overfit.md Co-authored-by: goldmermaid --- .../underfit-overfit.md | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/chapter_multilayer-perceptrons/underfit-overfit.md b/chapter_multilayer-perceptrons/underfit-overfit.md index c1f12557f..6817a9c0f 100644 --- a/chapter_multilayer-perceptrons/underfit-overfit.md +++ b/chapter_multilayer-perceptrons/underfit-overfit.md @@ -10,58 +10,57 @@ 困难在于,当我们训练模型时,我们只能访问数据中的小部分样本。最大的公开图像数据集包含大约一百万张图像。而在大部分时候,我们只能从数千或数万个数据样本中学习。在大型医院系统中,我们可能会访问数十万份医疗记录。当我们使用有限的样本时,可能会遇到这样的问题:当收集到更多的数据时,会发现之前找到的明显关系并不成立。 -将模型在训练数据上拟合得比在潜在分布中更接近的现象称为*过拟合*,用于对抗过拟合的技术称为*正则化*。在前面的章节中,你可能在用Fashion-MNIST数据集做实验时已经观察到了这种现象。在实验中调整模型结构或超参数时,如果有足够多的神经元、层数和训练迭代周期,你会观察到模型可以在训练集上达到完美的精度。虽然此时测试集的准确性会下降。 +将模型在训练数据上拟合得比在潜在分布中更接近的现象称为*过拟合*,用于对抗过拟合的技术称为*正则化*。在前面的章节中,你可能在用Fashion-MNIST数据集做实验时已经观察到了这种现象。在实验中调整模型结构或超参数时,你会发现,如果有足够多的神经元、层数和训练迭代周期,模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了。 ## 训练误差和泛化误差 为了进一步讨论这一现象,我们需要了解训练误差和泛化误差。*训练误差*(training error)是指,我们的模型在训练数据集上计算得到的误差。*泛化误差*(generalization error)是指,当我们将模型应用在同样从原始样本的分布中抽取的无限多的数据样本时,我们模型误差的期望。 -问题是,我们永远不能准确地计算出泛化误差。这是因为无限多的数据样本是一个虚构的对象。在实际中,我们只能通过将模型应用于一个独立的测试集来*估计*泛化误差,该测试集由从训练集中随机选择并保留的数据样本组成。 +问题是,我们永远不能准确地计算出泛化误差。这是因为无限多的数据样本是一个虚构的对象。在实际中,我们只能通过将模型应用于一个独立的测试集来*估计*泛化误差,该测试集由随机选取的、未曾在训练集中出现的数据样本构成。 -下面的三个思考实验将有助于更好地说明这种情况。假设一个大学生正在努力准备期末考试。一个勤奋的学生会努力做好练习,并利用往年的考试题目来测试自己的能力。尽管如此,在过去的考试题目上取得好成绩并不能保证他会在真正考试时发挥出色。例如,学生可能试图通过死记硬背考题的答案来做准备。他甚至可以完全记住过去考试的答案。另一名学生可能会通过试图理解给出某些答案的原因来做准备。在大多数情况下,后一个学生会考得更好。 +下面的三个思维实验将有助于更好地说明这种情况。假设一个大学生正在努力准备期末考试。一个勤奋的学生会努力做好练习,并利用往年的考试题目来测试自己的能力。尽管如此,在过去的考试题目上取得好成绩并不能保证他会在真正考试时发挥出色。例如,学生可能试图通过死记硬背考题的答案来做准备。他甚至可以完全记住过去考试的答案。另一名学生可能会通过试图理解给出某些答案的原因来做准备。在大多数情况下,后者会考得更好。 -类似地,考虑一个简单地使用查表法来回答问题的模型。如果允许的输入集合是离散的并且相当小,那么也许在查看许多训练样本后,该方法将执行得很好。但当面对这个模型从未见过的例子时,它表现的可能比随机猜测好不到哪去。这是因为输入空间太大了,远远不可能记住每一个可能输入的对应答案。例如,考虑$28\times28$的灰度图像。如果每个像素可以取$256$个灰度值中的一个,则有$256^{784}$个可能的图像。这意味着指甲大小的低分辨率灰度图像的数量比宇宙中的原子要多得多。即使我们可能遇到这样的数据,我们也不可能存储整个查找表。 +类似地,考虑一个简单地使用查表法来回答问题的模型。如果允许的输入集合是离散的并且相当小,那么也许在查看许多训练样本后,该方法将执行得很好。但当面对这个模型从未见过的例子时,它表现的可能比随机猜测好不到哪去。这是因为输入空间太大了,远远不可能记住每一个可能的输入所对应的答案。例如,考虑$28\times28$的灰度图像。如果每个像素可以取$256$个灰度值中的一个,则有$256^{784}$个可能的图像。这意味着指甲大小的低分辨率灰度图像的数量比宇宙中的原子要多得多。即使我们可能遇到这样的数据,我们也不可能存储整个查找表。 -最后,考虑尝试根据一些可用的上下文特征对掷硬币的结果(类别0:正面,类别1:反面)进行分类的问题。假设硬币投掷是公平的。无论我们想出什么算法,泛化误差始终是$\frac{1}{2}$。然而,对于大多数算法,我们应该期望训练误差会更低,这取决于运气。考虑数据集{0,1,1,1,0,1}。我们的算法不需要额外的特征,将倾向于总是预测*多数类*,从我们有限的样本来看,它似乎是*1*。在这种情况下,总是预测类1的模型将产生$\frac{1}{3}$的误差,这比我们的泛化误差要好得多。当我们逐渐增加数据量,正面比例明显偏离$\frac{1}{2}$的可能性将会降低,我们的训练误差将与泛化误差相匹配。 +最后,考虑尝试根据一些可用的上下文特征对掷硬币的结果(类别0:正面,类别1:反面)进行分类的问题。假设硬币是公平的。无论我们想出什么算法,泛化误差始终是$\frac{1}{2}$。然而,对于大多数算法,我们应该期望训练误差会更低(取决于运气)。考虑数据集{0,1,1,1,0,1}。我们的算法不需要额外的特征,将倾向于总是预测*多数类*,从我们有限的样本来看,它似乎是*1*。在这种情况下,总是预测类1的模型将产生$\frac{1}{3}$的误差,这比我们的泛化误差要好得多。当我们逐渐增加数据量,正面比例明显偏离$\frac{1}{2}$的可能性将会降低,我们的训练误差将与泛化误差相匹配。 ### 统计学习理论 由于泛化是机器学习中的基本问题,许多数学家和理论家毕生致力于研究描述这一现象的形式理论。在[同名定理(eponymous theorem)](https://en.wikipedia.org/wiki/Glivenko%E2%80%93Cantelli_theorem)]中,格里文科和坎特利推导出了训练误差收敛到泛化误差的速率。在一系列开创性的论文中,[Vapnik和Chervonenkis](https://en.wikipedia.org/wiki/Vapnik%E2%80%93Chervonenkis_theory)将这一理论扩展到更一般种类的函数。这项工作为统计学习理论奠定了基础。 -在有监督中,我们到目前为止都基于一个假设,并且这个假设将贯穿本书的大部分内容。即我们假设训练数据和测试数据都是从*相同的*分布中*独立*提取的。这通常被称为*独立同分布假设*(i.i.d. assumption),这意味着对数据进行采样的过程没有进行“记忆”。换句话说,抽取的第2个样本和第3个样本的相关性并不比抽取的第2个样本和第200万个样本的相关性更强。 +在我们目前已探讨、并将在之后继续探讨的标准的监督学习中,我们假设训练数据和测试数据都是从*相同的*分布中*独立*提取的。这通常被称为*独立同分布假设*(i.i.d. assumption),这意味着对数据进行采样的过程没有进行“记忆”。换句话说,抽取的第2个样本和第3个样本的相关性并不比抽取的第2个样本和第200万个样本的相关性更强。 -要成为一名优秀的机器学习科学家需要有批判性的思考,而且你应该已经从这个假设中找出漏洞,即很容易找出假设失效的情况。如果我们根据从加州大学旧金山分校医学中心的患者数据训练死亡风险预测模型,并将其应用于马萨诸塞州综合医院的患者数据,结果会怎么样?这两个数据的分布可能不完全一样。此外,抽样过程可能与时间有关。比如当我们对微博的主题进行分类时,新闻周期会使得正在讨论的话题产生时间依赖性,这违反了独立性的假设。 +要成为一名优秀的机器学习科学家需要有批判性的思考。你应该已经从这个假设中找出漏洞,即很容易找出假设失效的情况。如果我们根据从加州大学旧金山分校医学中心的患者数据训练死亡风险预测模型,并将其应用于马萨诸塞州综合医院的患者数据,结果会怎么样?这两个数据的分布可能不完全一样。此外,抽样过程可能与时间有关。比如当我们对微博的主题进行分类时,新闻周期会使得正在讨论的话题产生时间依赖性,从而违反独立性假设。 有时候我们即使轻微违背独立同分布假设,模型仍将继续运行得非常好。毕竟,几乎所有现实的应用都至少涉及到一些违背独立同分布假设的情况。然而,我们仍然有许多有用的工具已经应用于现实,如人脸识别、语音识别和语言翻译。 -有些违背独立同分布假设的行为肯定会带来麻烦。比如,如果我们试图训练一个人脸识别系统,只用来自大学生的人脸数据进行训练,然后想要将其部署为一种工具,用于监测疗养院人群中的老人。这不太可能有效,因为大学生看起来往往与老年人有很大的不同。 +有些违背独立同分布假设的行为肯定会带来麻烦。比如,我们试图只用来自大学生的人脸数据来训练一个人脸识别系统,然后想要用它来监测疗养院中的老人。这不太可能有效,因为大学生看起来往往与老年人有很大的不同。 在接下来的章节中,我们将讨论因违背独立同分布假设而引起的问题。目前,即使认为独立同分布假设是理所当然的,理解泛化也是一个困难的问题。此外,能够解释深层神经网络泛化性能的理论基础,也仍在继续困扰着学习理论领域最伟大的学者们。 -当我们训练模型时,我们试图找到一个能够尽可能拟合训练数据的函数。如果该函数灵活到可以像捕捉真实模式一样容易地捕捉到干扰的模式,那么它可能执行得“太好了”,而不会产生一个对看不见的数据进行很好概括的模型。但我们想要避免这样,或者想要至少能够控制这种现象的出现。深度学习中有许多启发式的技术旨在防止过拟合。 +当我们训练模型时,我们试图找到一个能够尽可能拟合训练数据的函数。如果该函数灵活到可以像捕捉真实模式一样容易地捕捉到干扰的模式,那么它可能执行得“太好了”,而不能产生一个对看不见的数据做到很好泛化的模型。这种情况正是我们想要避免,或起码控制的。深度学习中有许多启发式的技术旨在防止过拟合。 ### 模型复杂性 -当我们有简单的模型和大量的数据时,我们期望泛化误差与训练误差相近。当我们有更复杂的模型和更少的样本时,我们预计训练误差会下降,但泛化误差会增大。模型复杂性由什么构成是一个复杂的问题。一个模型是否能很好地泛化取决于很多因素。例如,具有更多参数的模型可能被认为更复杂。其参数有更大取值范围的模型可能更为复杂。通常,对于神经网络,我们认为需要更多训练迭代的模型比较复杂,而需要“提前停止”(early stopping)的模型(意味着具有较少训练迭代周期)就不那么复杂。 +当我们有简单的模型和大量的数据时,我们期望泛化误差与训练误差相近。当我们有更复杂的模型和更少的样本时,我们预计训练误差会下降,但泛化误差会增大。模型复杂性由什么构成是一个复杂的问题。一个模型是否能很好地泛化取决于很多因素。例如,具有更多参数的模型可能被认为更复杂。参数有更大取值范围的模型可能更为复杂。通常,对于神经网络,我们认为需要更多训练迭代的模型比较复杂,而需要“提前停止”(early stopping)的模型(意味着具有较少训练迭代周期)就不那么复杂。 -很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。就目前而言,一条简单的经验法则相当有用:统计学家认为,能够轻松解释任意事实的模型是复杂的,而表达能力有限但仍能很好地解释数据的模型可能更有现实用途。在理论上,这与波普尔的科学理论的可证伪性标准密切相关:如果一个理论能拟合数据,且有具体的测试可以用来证明它是错误的,那么它就是好的。这一点很重要,因为所有的统计估计都是*事后归纳*, -也就是说,我们在观察事实之后进行估计,因此容易受到相关谬误的影响。目前,我们将把理论放在一边,坚持更切实的问题。 +很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。就目前而言,一条简单的经验法则相当有用:统计学家认为,能够轻松解释任意事实的模型是复杂的,而表达能力有限但仍能很好地解释数据的模型可能更有现实用途。在哲学上,这与波普尔的科学理论的可证伪性标准密切相关:如果一个理论能拟合数据,且有具体的测试可以用来证明它是错误的,那么它就是好的。这一点很重要,因为所有的统计估计都是*事后归纳*,也就是说,我们在观察事实之后进行估计,因此容易受到相关谬误的影响。目前,我们将把哲学放在一边,坚持更切实的问题。 在本节中,为了给你一些直观的印象,我们将重点介绍几个倾向于影响模型泛化的因素: 1. 可调整参数的数量。当可调整参数的数量(有时称为*自由度*)很大时,模型往往更容易过拟合。 1. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。 -1. 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一个或两个样本的数据集。但是,过拟合一个数百万个样本数据集需要一个极其灵活的模型。 +1. 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。 ## 模型选择 在机器学习中,我们通常在评估几个候选模型后选择最终的模型。这个过程叫做*模型选择*。有时,需要进行比较的模型在本质上是完全不同的(比如,决策树与线性模型)。又有时,我们需要比较不同的超参数设置下的同一类模型。 -例如,我们要训练多层感知机模型,我们可能希望比较具有不同数量的隐藏层、不同数量的隐藏单元以及不同的的激活函数组合的模型。为了确定候选模型中的最佳模型,我们通常会使用验证集。 +例如,训练多层感知机模型时,我们可能希望比较具有不同数量的隐藏层、不同数量的隐藏单元以及不同的的激活函数组合的模型。为了确定候选模型中的最佳模型,我们通常会使用验证集。 ### 验证集 -原则上,在我们确定所有的超参数之前,我们不应该用到测试集。如果我们在模型选择过程中使用测试数据,可能会有过拟合测试数据的风险。那我们就麻烦大了。如果我们过拟合了我们的训练数据,还有在测试数据上的评估来判断过拟合。但是如果我们过拟合了测试数据,我们又该怎么知道呢? +原则上,在我们确定所有的超参数之前,我们不应该用到测试集。如果我们在模型选择过程中使用测试数据,可能会有过拟合测试数据的风险。那我们就麻烦大了。如果我们过拟合了训练数据,还有在测试数据上的评估来判断过拟合。但是如果我们过拟合了测试数据,我们又该怎么知道呢? 因此,我们决不能依靠测试数据进行模型选择。然而,我们也不能仅仅依靠训练数据来选择模型,因为我们无法估计训练数据的泛化误差。 @@ -71,15 +70,15 @@ ### $K$折交叉验证 -当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。这个问题的一个流行的解决方案是采用$K$折交叉验证*。这里,原始训练数据被分成$K$个不重叠的子集。然后执行$K$次模型训练和验证,每次在$K-1$个子集上进行训练,并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。最后,通过对$K$次实验的结果取平均来估计训练和验证误差。 +当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。这个问题的一个流行的解决方案是采用$K$*折交叉验证*。这里,原始训练数据被分成$K$个不重叠的子集。然后执行$K$次模型训练和验证,每次在$K-1$个子集上进行训练,并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。最后,通过对$K$次实验的结果取平均来估计训练和验证误差。 ## 欠拟合还是过拟合? -当我们比较训练和验证误差时,我们要注意两种常见的情况。首先,我们要注意这样的情况:训练误差和验证误差都很严重,但它们之间仅有一点差距。如果模型不能降低训练误差,这可能意味着我们的模型过于简单(即,表达能力不足),无法捕获我们试图学习的模式。此外,由于我们的训练和验证误差之间的*泛化误差*很小,我们有理由相信可以用一个更复杂的模型降低训练误差。这种现象被称为*欠拟合*(underfitting)。 +当我们比较训练和验证误差时,我们要注意两种常见的情况。首先,我们要注意这样的情况:训练误差和验证误差都很严重,但它们之间仅有一点差距。如果模型不能降低训练误差,这可能意味着我们的模型过于简单(即表达能力不足),无法捕获我们试图学习的模式。此外,由于我们的训练和验证误差之间的*泛化误差*很小,我们有理由相信可以用一个更复杂的模型降低训练误差。这种现象被称为*欠拟合*(underfitting)。 -另一方面,当我们的训练误差明显低于验证误差,表明了严重的*过拟合*(overfitting)。注意,*过拟合*并不总是一件坏事。特别是在深度学习领域,众所周知,最好的预测模型在训练数据上的表现往往比在保留数据上好得多。最终,我们通常更关心验证误差,而不是训练误差和验证误差之间的差距。 +另一方面,当我们的训练误差明显低于验证误差时要小心,这表明严重的*过拟合*(overfitting)。注意,*过拟合*并不总是一件坏事。特别是在深度学习领域,众所周知,最好的预测模型在训练数据上的表现往往比在保留数据上好得多。最终,我们通常更关心验证误差,而不是训练误差和验证误差之间的差距。 -我们是否过拟合可能取决于模型复杂性和可用训练数据集的大小,这两个点将在下面进行讨论。 +我们是否过拟合或欠拟合可能取决于模型复杂性和可用训练数据集的大小,这两个点将在下面进行讨论。 ### 模型复杂性 @@ -89,7 +88,7 @@ $$\hat{y}= \sum_{i=0}^d x^i w_i$$ 这只是一个线性回归问题,我们的特征是$x$的幂给出的,模型的权重是$w_i$给出的,偏置是$w_0$给出的(因为对于所有的$x$都有$x^0 = 1$)。由于这只是一个线性回归问题,我们可以使用平方误差作为我们的损失函数。 -高阶多项式函数比低阶多项式函数复杂得多。高阶多项式的参数较多,模型函数的选择范围较广。因此在固定训练数据集的情况下,高阶多项式函数相对于低阶多项式的训练误差应该始终更低(最坏情况下是相等的)。事实上,当数据样本包含了$x$的不同值时,函数阶数等于数据样本数量的多项式函数就可以很好地拟合训练集。在 :numref:`fig_capacity_vs_error` 中,我们直观地描述了多项式的阶数和欠拟合与过拟合之间的关系。 +高阶多项式函数比低阶多项式函数复杂得多。高阶多项式的参数较多,模型函数的选择范围较广。因此在固定训练数据集的情况下,高阶多项式函数相对于低阶多项式的训练误差应该始终更低(最坏也是相等)。事实上,当数据样本包含了$x$的不同值时,函数阶数等于数据样本数量的多项式函数可以完美拟合训练集。在 :numref:`fig_capacity_vs_error` 中,我们直观地描述了多项式的阶数和欠拟合与过拟合之间的关系。 ![模型复杂度对欠拟合和过拟合的影响](../img/capacity-vs-error.svg) @@ -97,7 +96,7 @@ $$\hat{y}= \sum_{i=0}^d x^i w_i$$ ### 数据集大小 -另一个需要牢记的重要因素是数据集的大小。训练数据集中的样本越少,我们就越有可能(而且更严重)遇到过拟合。随着训练数据量的增加,泛化误差通常会减小。此外,一般来说,更多的数据不会有什么坏处。对于固定的任务和数据分布,通常在模型复杂性和数据集大小之间存在关系。给出更多的数据,我们可能会尝试拟合一个更复杂的模型。能够拟合更复杂的模型可能是有益的。如果没有足够的数据,简单的模型可能更有用。对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。从一定程度上来说,深度学习目前的成功要归功于互联网公司、廉价存储、互联设备以及经济数字化带来的海量数据集。 +另一个需要牢记的重要因素是数据集的大小。训练数据集中的样本越少,我们就越有可能(且更严重地)遇到过拟合。随着训练数据量的增加,泛化误差通常会减小。此外,一般来说,更多的数据不会有什么坏处。对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。给出更多的数据,我们可能会尝试拟合一个更复杂的模型。能够拟合更复杂的模型可能是有益的。如果没有足够的数据,简单的模型可能更有用。对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。从一定程度上来说,深度学习目前的成功要归功于互联网公司、廉价存储、互联设备以及数字化经济带来的海量数据集。 ## 多项式回归 @@ -135,7 +134,7 @@ import math (**$$y = 5 + 1.2x - 3.4\frac{x^2}{2!} + 5.6 \frac{x^3}{3!} + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, 0.1^2).$$**) -噪声项$\epsilon$服从均值为0且标准差为0.1的正态分布。在优化的过程中,我们通常希望避免非常大的梯度值或损失值。这就是我们将*特征*从$x^i$调整为$\frac{x^i}{i!}$的原因,这样可以避免对于很大的$i$得到特别大的指数值。我们将为训练集和测试集各合成100个样本。 +噪声项$\epsilon$服从均值为0且标准差为0.1的正态分布。在优化的过程中,我们通常希望避免非常大的梯度值或损失值。这就是我们将*特征*从$x^i$调整为$\frac{x^i}{i!}$的原因,这样可以避免很大的$i$带来的特别大的指数值。我们将为训练集和测试集各生成100个样本。 ```{.python .input} #@tab all @@ -297,7 +296,7 @@ train(poly_features[:n_train, :2], poly_features[n_train:, :2], ### [**高阶多项式函数拟合(过拟合)**] -现在,让我们尝试使用一个过于高阶的多项式来训练模型。在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。虽然训练损失可以有效地降低,但测试损失仍然很高。结果表明,复杂模型对数据造成了过拟合。 +现在,让我们尝试使用一个阶数过高的多项式来训练模型。在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。虽然训练损失可以有效地降低,但测试损失仍然很高。结果表明,复杂模型对数据造成了过拟合。 ```{.python .input} #@tab all From f92e3e50c4005d427dea0998043187f657b7c79a Mon Sep 17 00:00:00 2001 From: Linhan Wu <1002503818@qq.com> Date: Mon, 26 Apr 2021 10:22:38 +0800 Subject: [PATCH 066/103] fix typo in sequence.md (#766) * fix 4.4.1.2. Model Complexity translation issues * fix typo in chapter_multilayer-perceptrons/environment.md * fix typo and translation issues in kaggle-house-price.md * fix typo in model-construction.md * fix typo and translation issues in use-gpu.md * fix typo and translation issues in alexnet.md * fix typo in vgg.md * revert back to the original * fix typo in batch-norm.md * fix typo in resnet.md * fix typo in sequence.md Co-authored-by: Linhan_Wu --- chapter_recurrent-neural-networks/sequence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_recurrent-neural-networks/sequence.md b/chapter_recurrent-neural-networks/sequence.md index 88b7cdb6e..1eeea492c 100644 --- a/chapter_recurrent-neural-networks/sequence.md +++ b/chapter_recurrent-neural-networks/sequence.md @@ -26,7 +26,7 @@ :width:`400px` :label:`fig_ftse100` -让我们用$x_t$表示价格。即在*时间步*(time step)$t \in \mathbb{Z}^+$时,我们观察到的价格$x_t$。请意,对于本文中的序列,$t$通常是离散的,并随整数或其子集而变化。假设一个想在$t$日股市表现良好的交易员通过以下途径预测了$x_t$: +让我们用$x_t$表示价格。即在*时间步*(time step)$t \in \mathbb{Z}^+$时,我们观察到的价格$x_t$。请注意,对于本文中的序列,$t$通常是离散的,并随整数或其子集而变化。假设一个想在$t$日股市表现良好的交易员通过以下途径预测了$x_t$: $$x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1).$$ From 3ab4585ba7135e184ec63c89deaca2e228772ace Mon Sep 17 00:00:00 2001 From: PEGASUS <32333727+PEGASUS1993@users.noreply.github.com> Date: Mon, 26 Apr 2021 10:23:26 +0800 Subject: [PATCH 067/103] Update index.md (#768) --- chapter_preface/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_preface/index.md b/chapter_preface/index.md index 4805493c0..5cef97a8c 100644 --- a/chapter_preface/index.md +++ b/chapter_preface/index.md @@ -1,6 +1,6 @@ # 序言 -几年前,没有大量深度学习科学家在大公司和初创公司开发智能产品和服务。我们中年轻人(作者)进入这个领域时,机器学习并没有在报纸上获得头条新闻。我们的父母根本不知道什么是机器学习,更不用说为什么我们可能更喜欢机器学习,而不是从事医学或法律职业。机器学习是一门具有前瞻性的学科,在现实世界的应用范围很窄。而那些应用,例如语音识别和计算机视觉,需要大量的领域知识,以至于它们通常被认为是完全独立的领域,而机器学习对于这些领域来说只是一个小组件。因此,神经网络——我们在本书中关注的深度学习模型的前身,被认为是过时的工具。 +几年前,在大公司和初创公司中,并没有大量的深度学习科学家开发智能产品和服务。我们中年轻人(作者)进入这个领域时,机器学习并没有在报纸上获得头条新闻。我们的父母根本不知道什么是机器学习,更不用说为什么我们可能更喜欢机器学习,而不是从事医学或法律职业。机器学习是一门具有前瞻性的学科,在现实世界的应用范围很窄。而那些应用,例如语音识别和计算机视觉,需要大量的领域知识,以至于它们通常被认为是完全独立的领域,而机器学习对于这些领域来说只是一个小组件。因此,神经网络——我们在本书中关注的深度学习模型的前身,被认为是过时的工具。 就在过去的五年里,深度学习给世界带来了惊喜,推动了计算机视觉、自然语言处理、自动语音识别、强化学习和统计建模等领域的快速发展。有了这些进步,我们现在可以制造比以往任何时候都更自主的汽车(不过可能没有一些公司试图让你相信的那么自主),可以自动起草普通邮件的智能回复系统,帮助人们从令人压抑的大收件箱中挖掘出来。在围棋等棋类游戏中,软件超越了世界上最优秀的人,这曾被认为是几十年后的事。这些工具已经对工业和社会产生了越来越广泛的影响,改变了电影的制作方式、疾病的诊断方式,并在基础科学中扮演着越来越重要的角色——从天体物理学到生物学。 From 27db81632ca3501c63a585a2ba6c360b3bf929ca Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Wed, 28 Apr 2021 00:02:43 +0800 Subject: [PATCH 068/103] fix errors in 11. Optimization Algorithms (#776) --- chapter_optimization/index.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/chapter_optimization/index.md b/chapter_optimization/index.md index d45ae1a9d..e0833c653 100644 --- a/chapter_optimization/index.md +++ b/chapter_optimization/index.md @@ -3,17 +3,16 @@ 到目前为止,如果你按顺序阅读本书,你已经学会使用许多优化算法来训练深度学习模型。 它们是允许我们继续更新模型参数和最小化损失函数值的工具。 -的确,很多人都愿意将优化视为“黑盒设备”,以最大程度地减少目标函数。 -对于他们来说,使用一些深度学习优化“魔法”(如“SGD”和“Adam”之类)就足够了。 +的确,很多人都愿意将优化视为“黑盒设备”,拥有一些使用深度学习优化“魔法”的知识,就能够基于简单的设置实现目标函数的最小化。 -然而,想要有效使用优化算法,还需要一些更深层次的知识。 -一方面,训练一个复杂的深度学习模型可能需要数小时、数天甚至数周的时间。优化算法的性能直接影响模型的训练效率。 +然而,优化算法对于深度学习是很重要的,因此学习一些更深层次的知识可以更好地优化。 +一方面,训练一个复杂的深度学习模型可能需要数小时、数天甚至数周的时间,而优化算法的性能将直接影响模型的训练效率。 另一方面,了解不同优化算法的原理及其超参数的作用,可以有针对性地调整超参数,提高深度学习模型的性能。 在本章中,我们将深入探讨常见的深度学习优化算法。 -在深度学习中,几乎所有的优化问题都是非凸的。 -尽管如此,在*凸*问题的背景下设计和分析算法已经被证明是非常有益的。 -正是由于这个原因,本章包括了关于凸优化的入门,已经非常简单的随机梯度下降算法在凸目标函数上的证明。 +在深度学习中,几乎所有的优化问题都是 *非凸的*(nonconvex)。 +尽管如此,在 *凸问题* 的背景下设计和分析算法已经被证明是非常有益的。 +基于这个原因,本章包括了关于凸优化的入门,和一个非常简单的随机梯度下降算法在凸目标函数上的证明。 ```toc :maxdepth: 2 @@ -29,4 +28,4 @@ rmsprop adadelta adam lr-scheduler -``` +``` \ No newline at end of file From f24541ff569131ca2666eaaec38173437bfbcb78 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Wed, 28 Apr 2021 03:53:32 +0800 Subject: [PATCH 069/103] fix errors in 9. Modern Recurrent Neural Networks (#775) --- chapter_recurrent-modern/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chapter_recurrent-modern/index.md b/chapter_recurrent-modern/index.md index e116c95ba..31229b819 100644 --- a/chapter_recurrent-modern/index.md +++ b/chapter_recurrent-modern/index.md @@ -1,11 +1,11 @@ -# 现代反复神经网络 +# 现代循环神经网络 :label:`chap_modern_rnn` -我们介绍了 RNN 的基础知识,它可以更好地处理序列数据。为了演示,我们在文本数据上实施了基于 RNN 的语言模型。但是,当从业人员现在面临着各种序列学习问题时,这些技术可能不足以使用他们。 +我们已经介绍了循环神经网络的基础知识,这种网络可以更好地处理序列数据。为了演示效果,我们在文本数据上实现了基于循环神经网络的语言模型。但是,这些技术对于从业人员面对当今各种序列学习问题时可能是不够用的。 -例如,实践中一个值得注意的问题是 RNN 的数量不稳定性。尽管我们已经应用了梯度剪切等实现技巧,但通过更复杂的序列模型设计,这个问题可以进一步缓解。具体来说,门控 RNN 在实践中更常见。我们将首先引入两个这样广泛使用的网络,即 * 门控循环单元 * (GRU) 和 * 长短期内存 * (LSTM)。此外,我们将使用迄今为止讨论的单个无向隐藏层来扩展 RNN 架构。我们将描述具有多个隐藏层的深层架构,并讨论双向设计与前向和向后重复计算。现代经常性网络经常采用这种扩张。在解释这些 RNN 变体时,我们将继续考虑 :numref:`chap_rnn` 中引入的语言建模问题。 +例如,实践中一个显著问题是循环神经网络的数值不稳定性。尽管我们已经应用了梯度裁剪等实现技巧,但是通过设计更复杂的序列模型可以进一步缓解这个问题。具体来说,在实践中更常见的门控循环神经网络。首先,我们将引入两个广泛使用的网络,即 *门控循环单元* (gated recurrent units, GRU) 和 *长短期记忆网络* (long short-term memory, LSTM)。然后,我们将基于迄今为止讨论过的一个单向隐藏层来扩展循环神经网络架构。我们将描述具有多个隐藏层的深层架构,并讨论基于前向和后向循环计算的双向设计。现代循环网络经常采用这种扩展。在解释这些循环神经网络的变体时,我们将继续考虑 :numref:`chap_rnn` 中引入的语言建模问题。 -事实上,语言建模只能揭示序列学习能够实现的一小部分。在自动语音识别、文本转语音和机器翻译等各种序列学习问题中,输入和输出都是任意长度的序列。为了解释如何适应这种类型的数据,我们将以机器翻译为例,并介绍基于 rNN 和束搜索的编码器解码器架构,以便生成序列。 +事实上,语言建模只揭示了序列学习能力的一小部分。在各种序列学习问题中,如自动语音识别、文本到语音的转换和机器翻译,输入和输出都是任意长度的序列。为了解释如何拟合这种类型的数据,我们将以机器翻译为例介绍基于循环神经网络的“编码器-解码器”架构和束搜索,并以此生成序列。 ```toc :maxdepth: 2 @@ -18,4 +18,4 @@ machine-translation-and-dataset encoder-decoder seq2seq beam-search -``` +``` \ No newline at end of file From 474b5d02a078a4c745990971b9fdfb6863e5b89d Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Tue, 27 Apr 2021 15:20:06 -0700 Subject: [PATCH 070/103] Update index.md --- index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/index.md b/index.md index dc8769aa3..438ca62cd 100644 --- a/index.md +++ b/index.md @@ -29,7 +29,6 @@ chapter_convolutional-neural-networks/index chapter_convolutional-modern/index chapter_recurrent-neural-networks/index chapter_recurrent-modern/index -chapter_computational-performance/index ``` From cfc13b97eaa309775ebf06097a3cee17fb1ed79f Mon Sep 17 00:00:00 2001 From: npudqsz <31696617+npudqsz@users.noreply.github.com> Date: Wed, 28 Apr 2021 09:42:46 +0200 Subject: [PATCH 071/103] english name of terms in underfit_overfit (#777) --- chapter_multilayer-perceptrons/underfit-overfit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_multilayer-perceptrons/underfit-overfit.md b/chapter_multilayer-perceptrons/underfit-overfit.md index 6817a9c0f..c19f549de 100644 --- a/chapter_multilayer-perceptrons/underfit-overfit.md +++ b/chapter_multilayer-perceptrons/underfit-overfit.md @@ -10,7 +10,7 @@ 困难在于,当我们训练模型时,我们只能访问数据中的小部分样本。最大的公开图像数据集包含大约一百万张图像。而在大部分时候,我们只能从数千或数万个数据样本中学习。在大型医院系统中,我们可能会访问数十万份医疗记录。当我们使用有限的样本时,可能会遇到这样的问题:当收集到更多的数据时,会发现之前找到的明显关系并不成立。 -将模型在训练数据上拟合得比在潜在分布中更接近的现象称为*过拟合*,用于对抗过拟合的技术称为*正则化*。在前面的章节中,你可能在用Fashion-MNIST数据集做实验时已经观察到了这种现象。在实验中调整模型结构或超参数时,你会发现,如果有足够多的神经元、层数和训练迭代周期,模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了。 +将模型在训练数据上拟合得比在潜在分布中更接近的现象称为*过拟合*(overfitting),用于对抗过拟合的技术称为*正则化*(regularization)。在前面的章节中,你可能在用Fashion-MNIST数据集做实验时已经观察到了这种现象。在实验中调整模型结构或超参数时,你会发现,如果有足够多的神经元、层数和训练迭代周期,模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了。 ## 训练误差和泛化误差 From e61876300f929616feb022ade51f23feb4c5c979 Mon Sep 17 00:00:00 2001 From: Aston Zhang Date: Thu, 29 Apr 2021 02:39:13 +0000 Subject: [PATCH 072/103] revise ch12 imgs --- img/a77.svg | 236 +++---- img/alexnet.svg | 42 +- img/asyncgraph.svg | 124 ++-- img/computegraph.svg | 122 ++-- img/frontends.svg | 361 ++++++++++ img/splitting.svg | 1547 +++++++++++++++++++++++++----------------- img/threading.svg | 351 +++++----- 7 files changed, 1695 insertions(+), 1088 deletions(-) create mode 100644 img/frontends.svg diff --git a/img/a77.svg b/img/a77.svg index baa0aa02d..1491911bb 100644 --- a/img/a77.svg +++ b/img/a77.svg @@ -1,5 +1,5 @@ - + @@ -119,177 +119,177 @@ - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - - + + - + - + - + - + - - + + - - - + + + - - + + - + - + - + - + - + - + - + - + - + - - + + - - + + - + - + - + - + - + - + - + - - + + - + - + - + - + - - - + + + - + - + - + @@ -303,7 +303,7 @@ - + @@ -368,47 +368,47 @@ - + - - - - - - - + + + + + + + - - + + - + - + - + - + - + - - - + + + - + - + - + @@ -448,26 +448,26 @@ - + - - - + + + - + - - + + - + - + - + @@ -486,7 +486,7 @@ - + @@ -561,7 +561,7 @@ - + @@ -584,7 +584,7 @@ - + @@ -623,7 +623,7 @@ - + @@ -660,28 +660,28 @@ - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/img/alexnet.svg b/img/alexnet.svg index 1175ae4e0..7c2cfacb4 100644 --- a/img/alexnet.svg +++ b/img/alexnet.svg @@ -217,7 +217,8 @@ - + + @@ -292,7 +293,8 @@ - + + @@ -355,7 +357,8 @@ - + + @@ -422,7 +425,8 @@ - + + @@ -485,7 +489,8 @@ - + + @@ -552,7 +557,8 @@ - + + @@ -619,7 +625,8 @@ - + + @@ -656,13 +663,13 @@ - + - + - + @@ -698,7 +705,8 @@ - + + @@ -890,7 +898,8 @@ - + + @@ -951,7 +960,8 @@ - + + @@ -1016,7 +1026,8 @@ - + + @@ -1061,7 +1072,8 @@ - + + diff --git a/img/asyncgraph.svg b/img/asyncgraph.svg index b1bfea8f3..563b8fa12 100644 --- a/img/asyncgraph.svg +++ b/img/asyncgraph.svg @@ -1,5 +1,5 @@ - + @@ -42,149 +42,125 @@ - + - + - - - - - - - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + - + + - + + - + + + + + + + - - + + - + - + - + - + - + - + + - + - - - + - + - + - + + + - + - + - + - + - + - + - - - + + + - + - + - + diff --git a/img/computegraph.svg b/img/computegraph.svg index 121d1dd16..b43811e87 100644 --- a/img/computegraph.svg +++ b/img/computegraph.svg @@ -1,5 +1,5 @@ - + @@ -42,126 +42,138 @@ - + - + - + - + + + + - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + + + + + + + + + + + + - + - - + - - + - - - - - - - - - - - - - + + - + - + - - + - + - + - + - + - + + + - + - - - + - + - + - + - + - + + - - + - + - + - + diff --git a/img/frontends.svg b/img/frontends.svg new file mode 100644 index 000000000..5d465bb98 --- /dev/null +++ b/img/frontends.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/splitting.svg b/img/splitting.svg index ce02cb584..46b73041e 100644 --- a/img/splitting.svg +++ b/img/splitting.svg @@ -15,377 +15,583 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + + + + + + + + + + + + + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - + + - + - - - - - - - - + + + + + + + + - - + - + + - + - + - + - - + + - + + - + - - + + + + + + + + + + + + + + + - + + - + - + + + + - + + - + + - - + - + + + + + + + + - + - + - + - + - - + - - + + - + + - + + + + - + + + - - + - + + - - + - - - + - + - + + + + + + + + + + + + + - + + + + + + + + + + - - + - + - + - + - + - - + - - + + - + + - + - + + + + - + + + + + + + + + + + + + - - + - + - - + + - - - + + + - + - + @@ -397,265 +603,297 @@ - + - + + + - + + + + + - + - - + + - - + - + - + - - + + - - - - + + - - + - + - - - + - + + + + + - - + - + + - + - - + + + + + - + + - + + - + - - + + - + - - + - + - + - + - + - + - - + + - - + + - + - + - + + + + + + + + + + + + + - - + - + - - + + - - - + + + - + - + - + + + - - + - + + + + + + + + + + + - + - + - + - - + + - - + + - + - + - + - + + + - - + - + + - - + - - - + - + - + + - - + + + - + - + + - - + + - + - + + - + - + + - - + - + - - + - + + - + - - + - + - + + - - + - + @@ -665,191 +903,257 @@ - - - - + + + + - + - - + + - + - - + + - - + - + + - + - + - + - - + + - + + - + - - + + + + + - + + + + + - + - + + + + + + + + + + + - + + - + + - - + - + + - + - + - + - + - - + - - + - + - + + + + + + + + + + + + - + + + + + - - + - + + + + + + + + - - + - - - + + - + + + - + + + + - + + + - - + - + + - + - + - + - - + - - + - + - + + - + + + + + + + + + + + - + + + + + - - + - + + - - + - - - + - + - + + + + + + + + + + + + + @@ -857,199 +1161,211 @@ - - + + - + - + + + - + + + + + - + + - - + - + - + - + + - + - - + - - - - + - + - - + - + - + - + + - + + - + - - + + - - + - + + - + + + + + - - + - - + - + - - + + - - - + + + - + - + - + + + - - + - + + - + - + - + - - + - - + - + - + + - + + - - + - - + - + + - - + - - - + + - + - + + - - + - + - + - - + + - - + + + - + - + + - - + + - - + + + + + + + @@ -1073,122 +1389,122 @@ - + - - + + - + - + - - + + - + - + - + - - + + - + - + - + + - + + - - + - + - - - + + - + + - - - - + + + + - - + + - + - + - + - + - - - - - - - - + + + - + - + - - - + + + + + + + - - + + - + - + - + - + @@ -1200,133 +1516,115 @@ - - - - + - + + + - - - - - - - - - - - - - - - - - - - + + - + - + - - + + - + - + - - - - + + - - + + - + - + - - - + + - + + - + + + + - - + + - + - + - + - + - - - - - - - - + + + - + - + - - - + + + + + + + - - + + - + - + - + - + @@ -1338,247 +1636,222 @@ - - - - - - - - - - - - - - - + - + + + - - - - - - - - + + - - - - - - - - + - - + + - + - + - + - + + - + - - + - + - + - + - + - + + - - - + - + - + - - + - + - + - + - + - + - + + + - + - + - + + - + - - + - - - + - + - - + - + - + - - + - + - + - + + - + + + - + - + + - + - + - + + - + - + - + - - + - + - - + - + - + - + - + - + - + - - + - + - + + - + - + - + - + - + - + + diff --git a/img/threading.svg b/img/threading.svg index 535dc1967..582c32e04 100644 --- a/img/threading.svg +++ b/img/threading.svg @@ -1,395 +1,368 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - + - + - + - - - + - + + + - + - + - - + - + + - + + - + - + - + + - - - + - + - + + - + - + + + + - - + - + + - + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - + - + + - + - - - + - - - - - - - - + - - + - + - + + - + + + - + - + + + + + + + + + + + + - - + + - - - + - + - + - - - - - - - - - - - + - + - + - + + - + + - + - + + - - - + - + - + + + + + + + + + + + + + - + + + + - + - + - + - + - + + + - + - + - - - + - + + - + - + - - - + - + + + - + - + - - + - + + - - + - + - - + - - + + + - + - + - - - + - - - - - - - - - - - - - - + From 4451279f7ec09dcef75bd165f52813956249ae3c Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Sat, 1 May 2021 03:50:32 +0800 Subject: [PATCH 073/103] Chapter optimization/optimization intro (#780) * fix errors in 11.1. Optimization and Deep Learning * according to dl-en fix errors in 11.1. Optimization and Deep Learning * fix errors in 11.1.1. Goal of Optimization * fix errors in 11.1.2. Optimization Challenges in Deep Learning * fix errors in 11.1.2.1. Local Minima * fix errors in 11.1.2.2. Saddle Points * fix errors in 11.1.2.3. Vanishing Gradients the End of fix errors in 11.1. * Update optimization-intro.md Co-authored-by: goldmermaid --- chapter_optimization/optimization-intro.md | 126 +++++++++++---------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/chapter_optimization/optimization-intro.md b/chapter_optimization/optimization-intro.md index c960b7c75..f533dbb57 100644 --- a/chapter_optimization/optimization-intro.md +++ b/chapter_optimization/optimization-intro.md @@ -1,21 +1,18 @@ -# 优化与深度学习 -在本节中,我们将讨论优化与深度学习之间的关系,以及在深度学习中使用优化所面临的挑战。 -对于深度学习问题,我们通常首先定义一个损失函数,一旦我们有了损失函数,我们就可以使用一个优化算法来尝试最小化损失。 -在最优化中,损失函数通常被称为最优化问题的目标函数。 -根据传统和惯例,大多数优化算法都与“最小化”有关。 -如果我们需要最大化一个目标,有一个简单的解决方案:只要翻转目标函数前面的符号。 +# 最优化与深度学习 -## 优化与估算 +在本节中,我们将讨论最优化与深度学习之间的关系,以及在深度学习中使用最优化所面临的挑战。 +对于深度学习问题,通常先定义一个 *损失函数*(loss function)。一旦有了损失函数,我们就可以使用一个最优化算法来尝试最小化损失。在最优化中,损失函数通常被称为最优化问题的 *目标函数*(objective function)。根据传统和惯例,大多数最优化算法都与 *最小化*(minimization)有关。如果我们需要最大化一个目标函数,有一个简单的解决方案:只要翻转目标函数前面的符号即可。 -虽然优化为深度学习提供了一种最小化损失函数的方法,但从本质上讲,优化和深度学习的目标是完全不同的。 -前者主要关注最小化目标,而后者关注在给定有限数据量的情况下找到合适的模型。 +## 优化目标 + +虽然最优化为深度学习提供了一种最小化损失函数的方法,但从本质上讲,最优化和深度学习的目标是完全不同的。 +前者主要关注最小化目标函数,而后者则关注在给定有限数据量的情况下找到合适的模型。 在 :numref:`sec_model_selection` 中,我们详细讨论了这两个目标之间的差异。 -例如,训练误差和泛化误差一般是不同的:由于优化算法的目标函数通常是基于训练数据集的损失函数,所以优化的目标是减少训练误差。 -然而,统计推断(以及深度学习)的目标是减少泛化误差。 -为了实现后者,除了使用优化算法来减少训练误差外,还需要注意过拟合问题。 -我们首先导入一些深度学习算法库。 +例如,通常情况下训练误差和泛化误差是不同的:因为最优化算法的目标函数一般是基于训练数据集的损失函数,所以最优化的目标是减少训练误差。 +然而,深度学习(或者广义上说,统计推断)的目标是减少泛化误差。 +为了实现后者的目标,除了使用最优化算法来减少训练误差外,还需要注意过拟合问题。 ```{.python .input} %matplotlib inline @@ -43,15 +40,21 @@ from mpl_toolkits import mplot3d import tensorflow as tf ``` -接下来我们定义两个函数,期望函数 $f$ 和经验函数 $g$ ,来说明这个问题。这里的 $g$ 不如 $f$ 平滑,因为我们只有有限的数据量。 +为了说明上述的不同目标,让我们考虑经验风险和风险。 +如 :numref:`subsec_empirical-risk-and-risk` 所描述的,经验风险是训练数据集上的平均损失,而风险是全体数据的期望损失。 +接下来我们定义两个函数,风险函数 $f$ 和经验风险函数 $g$。 +假设我们拥有的训练数据的数量是有限的,因此函数 $g$ 不如 $f$ 平滑。 ```{.python .input} #@tab all -def f(x): return x * d2l.cos(np.pi * x) -def g(x): return f(x) + 0.2 * d2l.cos(5 * np.pi * x) +def f(x): + return x * d2l.cos(np.pi * x) + +def g(x): + return f(x) + 0.2 * d2l.cos(5 * np.pi * x) ``` -下图说明了训练误差的最小值可能与预期误差(或测试误差)的最小值在不同的位置。 +下图说明了,在训练数据集上,经验风险的最小值可能与风险(泛化误差)的最小值不在相同的位置。 ```{.python .input} #@tab all @@ -62,24 +65,24 @@ def annotate(text, xy, xytext): #@save x = d2l.arange(0.5, 1.5, 0.01) d2l.set_figsize((4.5, 2.5)) d2l.plot(x, [f(x), g(x)], 'x', 'risk') -annotate('empirical risk', (1.0, -1.2), (0.5, -1.1)) -annotate('expected risk', (1.1, -1.05), (0.95, -0.5)) +annotate('min of\nempirical risk', (1.0, -1.2), (0.5, -1.1)) +annotate('min of risk', (1.1, -1.05), (0.95, -0.5)) ``` -## 深度学习中的优化挑战 +## 深度学习中的最优化挑战 -在本章中,我们将特别关注优化算法在最小化目标函数方面的性能,而不是模型的泛化误差。 -在 :numref:`sec_linear_regression` 中,我们区分了最优化问题的解析解和数值解。 -在深度学习中,大多数目标函数是复杂的,没有解析解。 -相反,我们必须使用数值优化算法。下面的优化算法都属于这一类。 +在本章中,我们将特别关注最优化算法在最小化目标函数方面的性能,而不是模型的泛化误差。 +在 :numref:`sec_linear_regression` 中,我们对比了最优化问题的解析解和数值解。 +在深度学习中,大多数目标函数是复杂的、没有解析解的。 +因此,我们必须使用本章所描述的数值最优化算法来代替解析算法。 -深度学习优化面临许多挑战,其中最令人烦恼的是局部极小值、鞍点和消失梯度。 -下面让我们具体了解一下这些挑战。 +深度学习的最优化面临许多挑战,其中最令人烦恼的是局部极小值、鞍点和消失梯度。 +下面我们将具体了解这些挑战。 ### 局部最小值 -对于目标函数 $f(x)$,如果 $x$ 处的 $f(x)$ 值小于 $x$ 附近任何其他点的 $f(x)$ 值,则 $f(x)$ 可以是*局部最小值*(local minimum)。 -如果 $f(x)$ 在 $x$ 处的值是目标函数在整个域上的最小值,则 $f(x)$ 是*全局最小值*(global minimum)。 +对于目标函数 $f(x)$,如果 $x$ 处的 $f(x)$ 值小于 $x$ 附近任何其他点的 $f(x)$ 值,则 $f(x)$ 可以是 *局部最小值*(local minimum)。 +如果在 $x$ 处 $f(x)$ 的值是目标函数在整个域上的最小值,则 $f(x)$ 是 *全局最小值*(global minimum)。 例如,给定函数 @@ -95,17 +98,16 @@ annotate('local minimum', (-0.3, -0.25), (-0.77, -1.0)) annotate('global minimum', (1.1, -0.95), (0.6, 0.8)) ``` -深度学习模型的目标函数通常具有许多局部最优解。 -当优化问题的数值解接近局部最优解时,当目标函数解的梯度下降接近零时,通过最终迭代得到的数值解只可能使目标函数局部最小化,而不是全局最小化。 -只有一定程度的噪声才能使参数超出局部极小值。 -事实上,这是随机梯度下降的一个有利性质,在这种情况下,小批量梯度的自然变化能够将参数从局部极小值中去除。 +通常深度学习模型的目标函数具有许多局部最优解。 +当最优化问题的数值解接近局部最优解时,会导致求解目标函数的梯度趋于或者变为零,此时通过最终迭代得到的数值解只可能使目标函数 *局部最小化*(locally),而不是 *全局最小化*(globally)。 +只有一定程度的噪声才能使参数脱离局部极小值。 +事实上,小批量随机梯度下降的一个有利性质就是基于小批量上的梯度的自然变化能够强行将参数从局部极小值中移出。 ### 鞍点 -除了局部极小值,*鞍点*(saddle point)是梯度消失的另一个原因。 -鞍点是函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。 +除了局部极小值,鞍点是梯度消失的另一个原因。*鞍点*(saddle point)也是函数的所有梯度都消失的位置,但这个位置既不是全局最小值也不是局部最小值。 考虑函数 $f(x) = x^3$,它的一阶导数和二阶导数在 $x=0$ 处消失。 -优化可能会在某个点停止,即使它不是最小值。 +即使 $x$ 不是最小值,优化也可能在这个点上停止。 ```{.python .input} #@tab all @@ -114,9 +116,9 @@ d2l.plot(x, [x**3], 'x', 'f(x)') annotate('saddle point', (0, -0.2), (-0.52, -5.0)) ``` -更高维度中的鞍点更为隐蔽,如下例所示,考虑函数 $f(x, y) = x^2 - y^2$。 +如下例所示,更高维度中的鞍点将更加隐蔽。考虑函数 $f(x, y) = x^2 - y^2$, 它的鞍点在 $(0, 0)$,这是 $y$ 的最大值,$x$ 的最小值。 -它看起来像一个马鞍,这就是这个数学性质的由来。 +而且,它看起来像一个马鞍,这也就是这个数学性质命名的原因。 ```{.python .input} #@tab all @@ -135,24 +137,24 @@ d2l.plt.xlabel('x') d2l.plt.ylabel('y'); ``` -我们假设一个函数的输入是一个$k$维向量,其输出是一个标量,因此它的Hessian矩阵将有$k$个特征值(参见 :numref:`sec_geometry-linear-algebraic-ops`)。 -函数的解可以是局部最小值、局部最大值或函数梯度为零的位置处的鞍点: +我们假设一个函数的输入是一个 $k$ 维向量,其输出是一个标量,因此它的 Hessian 矩阵将有 $k$ 个特征值(参见 :numref:`sec_geometry-linear-algebraic-ops`)。 +函数的解可以是局部最小值、局部最大值或者鞍点,解所在位置的函数梯度为零: -* 当函数的Hessian矩阵在零梯度位置的特征值都为正时,我们得到了函数的局部极小值。 -* 当函数的Hessian矩阵在零梯度位置的特征值都为负时,我们得到了函数的局部极大值。 -* 当函数的Hessian矩阵在零梯度位置的特征值为负和正时,我们得到了函数的鞍点。 +* 当函数的 Hessian 矩阵在零梯度位置的特征值都为正时,我们得到了函数的局部极小值。 +* 当函数的 Hessian 矩阵在零梯度位置的特征值都为负时,我们得到了函数的局部极大值。 +* 当函数的 Hessian 矩阵在零梯度位置的特征值有负有正时,我们得到了函数的鞍点。 -对于高维问题,至少某些特征值为负的可能性是相当高的,这使得鞍点比局部极小值更有可能。 -我们将在下一节介绍凸性时讨论这种情况的一些例外情况。简而言之,*凸函数*是那些Hessian函数的特征值从不为负的函数。 -遗憾的是,大多数深层次的学习问题并不属于这一类。然而,它是一个伟大的工具,研究优化算法。 +对于高维问题,至少某些特征值为负的可能性是相当高的,因此得到函数的鞍点比局部极小值的可能性更高。 +在下一节介绍凸性时,我们将讨论这种形势下的一些例外情况。简而言之,*凸函数* 是那些 Hessian 函数的特征值从不为负的函数。 +遗憾的是,大多数的深度学习问题都不属于这类函数。然而,它仍然是研究优化算法一个伟大的工具。 -### 消失梯度 +### 梯度消失 -*消失梯度*(vanishing gradients)可能是我们会遇到的最隐秘的问题。 -例如,假设我们想最小化函数 $f(x) = \tanh(x)$,我们恰好从 $x = 4$ 开始。 -如我们所见,$f$ 的梯度接近于零,更具体地说是$f'(x) = 1 - \tanh^2(x)$和$f'(4) = 0.0013$。 -因此,在我们取得进展之前,优化将被困很长一段时间。 -这就是为什么在引入ReLU激活函数之前,深度学习模型的训练相当棘手的原因之一。 +回忆一下我们常用的激活函数及它们的导数 :numref:`subsec_activation-functions`,*梯度消失*(vanishing gradients)可能是会遇到的最隐蔽的问题。 +举个例子,假设我们想从 $x = 4$ 开始最小化函数 $f(x) = \tanh(x)$。 +如我们所见,$f$ 的梯度接近于零,更具体地说就是$f'(x) = 1 - \tanh^2(x)$ 和 $f'(4) = 0.0013$。 +结果,在我们取得进展之前,优化将被困在那个位置很长一段时间。 +这就是为什么深度学习模型的训练在引入 ReLU 激活函数之前相当棘手的原因之一。 ```{.python .input} #@tab all @@ -162,27 +164,27 @@ annotate('vanishing gradient', (4, 1), (2, 0.0)) ``` 正如我们所看到的,深度学习的优化充满了挑战。 -幸运的是,有一个强大的算法范围,表现良好,易于使用,即使是初学者。 -此外,其实没有必要找到最佳解决方案,局部最优解甚至近似解仍然非常有用。 +幸运的是,存在一个强大的、表现良好的、即使对于初学者也易于使用的算法范围。 +此外,没有必要找到最佳解决方案,因为局部最优解甚至近似解仍然是非常有用的。 ## 小结 -* 最小化训练误差并不能保证我们找到一组最佳的参数来最小化期望误差。 -* 优化问题可能存在许多局部极小值。 -* 一般机器学习问题都不是凸性的,所以优化问题可能有许多鞍点。 -* 逐渐消失的梯度会导致优化停滞,通常问题的重新参数化会有所帮助。良好初始化的参数也可能是有益的。 +* 最小化训练误差并不能保证我们找到一组最佳的参数来最小化泛化误差。 +* 最优化问题可能存在许多局部极小值。 +* 因为通常情况下机器学习问题都不是凸性的,所以优化问题可能有许多鞍点。 +* 梯度消失会导致优化停滞。通常问题的重新参数化会有所帮助。良好的参数初始化也可能是有益的。 ## 练习 -1. 考虑一个简单的多层感知器,在隐层中有一个$d$维的隐层和一个输出。证明任何当地最低有至少 $d!$ 行为相同的等价解。 -1. 假设我们有一个对称随机矩阵 $\mathbf{M}$,其中条目 $M_{ij} = M_{ji}$ 分别来自某个概率分布 $p_{ij}$。此外,假设 $p_{ij}(x) = p_{ij}(-x)$,即分布是对称的(详见 :cite:`Wigner.1958` )。 - * 证明了特征值上的分布也是对称的。即,对于任何特征向量 $\mathbf{v}$,相关特征值 $\lambda$ 满足 $P(\lambda > 0) = P(\lambda < 0)$ 的概率。 - * 为什么上面的*不是*意味着 $P(\lambda > 0) = 0.5$? +1. 考虑一个简单的多层感知机,其有一个 $d$ 维的隐藏层和一个输出。证明对于任何局部最小值至少有 $d!$ 个行为相同的等价解。 +1. 假设我们有一个对称随机矩阵 $\mathbf{M}$,其中元素 $M_{ij} = M_{ji}$ ,并且每个元素都是基于某个概率分布 $p_{ij}$ 提取出来的。此外,假设 $p_{ij}(x) = p_{ij}(-x)$,即分布是对称的(详见 :cite:`Wigner.1958` )。 + * 证明了特征值上的分布也是对称的。即对于任何特征向量 $\mathbf{v}$,相关特征值 $\lambda$ 的概率满足 $P(\lambda > 0) = P(\lambda < 0)$ 。 + * 为什么上面的证明 *没有* 隐含 $P(\lambda > 0) = 0.5$? 1. 在深度学习优化过程中,你还能想到哪些挑战? 1. 假设你想在一个(真实的)马鞍上平衡一个(真实的)球。 * 为什么这么难? - * 你能利用这种效果也优化算法吗? + * 你能利用这种效果优化算法吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/349) From 9b5f533c7359af0b83269de7c3078c60caddffff Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Sat, 1 May 2021 12:34:21 +0800 Subject: [PATCH 074/103] Chapter recurrent neural networks/text preprocessing (#782) * fix errors in 8.2. Text Preprocessing fix errors in 8.2.1. Reading the Dataset * fix errors in 8.2.2. Tokenization * fix errors in 8.2.3. Vocabulary * fix errors in 8.2.4. Put All Things Together the end of fix errors in 8.2. * Update text-preprocessing.md * Update text-preprocessing.md Co-authored-by: goldmermaid --- .../text-preprocessing.md | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/chapter_recurrent-neural-networks/text-preprocessing.md b/chapter_recurrent-neural-networks/text-preprocessing.md index cb3fef8d1..c905c33e6 100644 --- a/chapter_recurrent-neural-networks/text-preprocessing.md +++ b/chapter_recurrent-neural-networks/text-preprocessing.md @@ -1,7 +1,10 @@ # 文本预处理 :label:`sec_text_preprocessing` -我们回顾和评估了序列数据的统计工具和预测挑战。这些数据可以有多种形式。具体来说,正如我们将在本书的许多章节中重点介绍的那样,文本是序列数据最常见例子。例如,一篇文章可以简单地看作是一个单词序列,甚至是一个字符序列。为了方便我们将来对序列数据的实验,我们将在本节中专门解释文本的常见预处理步骤。通常,这些步骤包括: +我们回顾和评估了处理序列数据时,使用的统计工具和预测时面临的挑战。 +正如我们将在本书的许多章节中重点介绍的那样,文本是序列数据最常见例子。 +例如,一篇文章可以简单地看作是一个单词序列,甚至是一个字符序列。 +为了方便将来在实验中使用序列数据,我们将在本节中专门解释文本的常见预处理步骤。通常,这些步骤包括: 1. 将文本作为字符串加载到内存中。 1. 将字符串拆分为标记(如,单词和字符)。 @@ -30,7 +33,7 @@ import re ## 读取数据集 -为了开始,我们从H.G.Well的[*时光机器*](http://www.gutenberg.org/ebooks/35)中加载文本。这是一个相当小的语料库,只有30000多个单词,但对于我们想要说明的目标来说,这足够了。现实中的文档集合可能会包含数十亿个单词。下面的函数将数据集读取到文本行组成的列表中,其中每行都是一个字符串。为简单起见,这里我们忽略标点符号和大写。 +我们从H.G.Well的 [*时光机器*](http://www.gutenberg.org/ebooks/35) 中加载文本作为开始。这是一个相当小的语料库,只有30000多个单词,但足够实现我们的目标,即介绍文本预处理。现实中的文档集合可能会包含数十亿个单词。下面的函数将数据集读取到由文本行组成的列表中,其中每行都是一个字符串。为简单起见,我们在这里忽略了标点符号和字母大写。 ```{.python .input} #@tab all @@ -52,7 +55,7 @@ print(lines[10]) ## 标记化 -以下 `tokenize` 函数将列表作为输入,列表中的每个元素是文本序列(如,文本行)。每个文本序列被拆分成一个标记列表。*标记*(token)是文本的基本单位。最后返回一个标记列表,其中每个标记都是一个字符串(string)。 +以下 `tokenize` 函数将列表作为输入,列表中的每个元素是一个文本序列(如,一条文本行)。每个文本序列被拆分成一个标记列表。*标记*(token)是文本的基本单位。最后返回一个标记列表,其中每个标记都是一个字符串(string)。 ```{.python .input} #@tab all @@ -72,7 +75,7 @@ for i in range(11): ## 词汇 -标记的字符串类型不方便模型使用,因为模型需要输入数字。现在,让我们构建一个字典,通常也叫做*词表*(Vocabulary)来将字符串标记映射到从0开始的数字索引中。为此,我们首先统计训练集中所有文档中的唯一标记,即*语料*(corpus),然后根据每个唯一标记的出现频率为其分配一个数字索引。很少出现的标记通常被移除,这可以降低复杂性。语料库中不存在或已删除的任何标记都将映射到一个特殊的未知标记 “<unk>” 。我们可以选择添加保留令牌的列表,例如“<pad>”表示填充;“<bos>”表示序列的开始;“<eos>”表示序列的结束。 +标记的字符串类型不方便模型使用,因为模型需要的输入是数字。现在,让我们构建一个字典,通常也叫做*词表*(vocabulary),用来将字符串标记映射到从 $0$ 开始的数字索引中。为此,我们首先统计训练集中所有文档中唯一的标记,称之为 *语料*(corpus),然后根据每个唯一标记的出现频率为其分配一个数字索引。很少出现的标记通常被移除,这可以降低复杂性。语料库中不存在或已删除的任何标记都将映射到一个特定的未知标记 “<unk>” 。我们可以选择增加一个列表,用于保存保留的标记,例如“<pad>”表示填充;“<bos>”表示序列的开始;“<eos>”表示序列的结束。 ```{.python .input} #@tab all @@ -110,10 +113,10 @@ class Vocab: #@save return [self.idx_to_token[index] for index in indices] def count_corpus(tokens): #@save - """Count token frequencies.""" - # 这里的 `tokens` 是1D列表或2D列表 + """统计标记的频率。""" + # 这里的 `tokens` 是 1D 列表或 2D 列表 if len(tokens) == 0 or isinstance(tokens[0], list): - # 将令牌列表展平 + # 将标记列表展平成使用标记填充的一个列表 tokens = [token for line in tokens for token in line] return collections.Counter(tokens) ``` @@ -138,17 +141,17 @@ for i in [0, 10]: ## 把所有的东西放在一起 使用上述函数,我们将所有内容打包到 `load_corpus_time_machine` 函数中,该函数返回 `corpus`(标记索引列表)和 `vocab`(时光机器语料库的词汇表)。我们在这里所做的修改是: -- 1、我们将文本 标记化为字符,而不是单词,以简化后面部分中的训练; -- 2、`corpus`是单个列表,而不是标记列表嵌套,因为时光机器数据集中的每个文本行不一定是句子或段落。 +- 1、我们将文本标记化为字符,而不是单词,以便简化后面章节中的训练; +- 2、`corpus`是单个列表,而不是使用标记列表构成的一个列表,因为时光机器数据集中的每个文本行不一定是一个句子或一个段落。 ```{.python .input} #@tab all def load_corpus_time_machine(max_tokens=-1): #@save - """返回时光机器数据集的令牌索引和词汇表。""" + """返回时光机器数据集的标记索引列表和词汇表。""" lines = read_time_machine() tokens = tokenize(lines, 'char') vocab = Vocab(tokens) - # 因为时光机器数据集中的每一个文本行不一定是一个句子或段落, + # 因为时光机器数据集中的每一个文本行,不一定是一个句子或一个段落, # 所以将所有文本行展平到一个列表中 corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: @@ -167,7 +170,7 @@ len(corpus), len(vocab) ## 练习 1. 标记化是一个关键的预处理步骤。它因语言而异。尝试找到另外三种常用的文本标记方法。 -1. 在本节的实验中,将文本标记为单词,并更改 `Vocab` 实例的 `min_freq` 参数。这对词汇量有何影响? +1. 在本节的实验中,将文本标记为单词和更改 `Vocab` 实例的 `min_freq` 参数。这对词汇量有何影响? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/2093) @@ -179,4 +182,4 @@ len(corpus), len(vocab) :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2095) -:end_tab: \ No newline at end of file +:end_tab: From 9d0caa6371356ef3ccfcc668b9c165cf47bc84a0 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Tue, 4 May 2021 00:03:12 +0800 Subject: [PATCH 075/103] add terminology for 8. Recurrent Neural Networks (#784) --- TERMINOLOGY.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/TERMINOLOGY.md b/TERMINOLOGY.md index 7e9ca42fc..5fe3910b3 100644 --- a/TERMINOLOGY.md +++ b/TERMINOLOGY.md @@ -14,18 +14,26 @@ 编码器,encoder +标记,token + +标记化,tokenize + 标签,label 步幅,stride 参数,parameter +长短期记忆网络,long short-term memory (LSTM) + 超参数,hyper-parameter 层序softmax,hierarchical softmax 成本,cost +词汇表,vocabulary + 词嵌入,word embedding 词向量,word vector @@ -54,6 +62,8 @@ 二元分类,binary classification +二元语法,bigram + 二次采样,subsample 发散,diverge @@ -132,6 +142,8 @@ 流水线,pipeline +门控循环单元,gated recurrent units (GRU) + 模型参数,model parameter 模型复杂度,model complexity @@ -156,6 +168,8 @@ 权重,weight +三元语法,trigram + 上采样,upsample 实例,instance @@ -186,6 +200,8 @@ softmax回归,softmax regression 梯度裁剪,gradient clipping +梯度消失,vanishing gradients + 填充,padding 跳字模型,skip-gram model @@ -198,7 +214,7 @@ softmax回归,softmax regression 图像,image -未知词符号,unknown token +未知词标记,unknown token 无偏估计,unbiased estimate @@ -218,18 +234,22 @@ softmax回归,softmax regression 训练误差,training error -循环神经网络,recurrent neural network +循环神经网络,recurrent neural network (RNN) 样本,example 一维梯度下降,gradient descent in one-dimensional space +一元语法,unigram + 隐藏变量,hidden variable 隐藏层,hidden layer 优化器,optimizer +语料库,corpus + 运算符,operator 真实值,ground truth From 0d61c076b9fa736c4d3fd97373a868049be53abf Mon Sep 17 00:00:00 2001 From: Aston Zhang Date: Mon, 3 May 2021 22:06:11 +0000 Subject: [PATCH 076/103] add ch13.1 ch13.2 --- chapter_computer-vision/fine-tuning.md | 303 +++++++++++ chapter_computer-vision/fine-tuning_origin.md | 373 ++++++++++++++ chapter_computer-vision/image-augmentation.md | 379 ++++++++++++++ .../image-augmentation_origin.md | 439 ++++++++++++++++ chapter_computer-vision/index.md | 25 + chapter_computer-vision/index_origin.md | 44 ++ img/finetune.svg | 477 +++++++++--------- 7 files changed, 1805 insertions(+), 235 deletions(-) create mode 100644 chapter_computer-vision/fine-tuning.md create mode 100644 chapter_computer-vision/fine-tuning_origin.md create mode 100644 chapter_computer-vision/image-augmentation.md create mode 100644 chapter_computer-vision/image-augmentation_origin.md create mode 100644 chapter_computer-vision/index.md create mode 100644 chapter_computer-vision/index_origin.md diff --git a/chapter_computer-vision/fine-tuning.md b/chapter_computer-vision/fine-tuning.md new file mode 100644 index 000000000..a2939870b --- /dev/null +++ b/chapter_computer-vision/fine-tuning.md @@ -0,0 +1,303 @@ +# 微调 +:label:`sec_fine_tuning` + +在前面的章节中,我们讨论了如何在 Fashion-Mnist 训练数据集上训练模型,只有 60000 张图像。我们还描述了 iMagenNet,这是学术界中使用最广泛的大型图像数据集,它拥有 1000 多万张图像和 1000 个对象。但是,我们通常遇到的数据集的大小介于两个数据集中的大小之间。 + +假设我们想识别图片中不同类型的椅子,然后向用户推荐购买链接。一种可能的方法是首先识别 100 把普通椅子,为每把椅子拍摄 1000 张不同角度的图像,然后在收集的影像数据集上训练一个分类模型。尽管这个椅子数据集可能大于 Fashion-Mnist 数据集,但实例数量仍然不到 iMagenet 中的十分之一。这可能会导致这个椅子数据集上适合 iMagenNet 的复杂模型过度拟合。此外,由于训练示例数量有限,训练模型的准确性可能无法满足实际要求。 + +为了解决上述问题,一个显而易见的解决方案是收集更多的数据。但是,收集和标记数据可能需要大量的时间和金钱。例如,为了收集 iMagenet 数据集,研究人员从研究资金中花费了数百万美元。尽管目前的数据收集成本已大幅降低,但这一成本仍不能忽视。 + +另一种解决方案是应用 * 传输学习 * 将从 * 源数据集 * 学到的知识传输到 * 目标数据集 *。例如,尽管 iMagenet 数据集中的大多数图像与椅子无关,但在此数据集上训练的模型可能会提取更常规的图像特征,这有助于识别边缘、纹理、形状和对象合成。这些类似的功能也可能有效地识别椅子。 + +## 步骤 + +在本节中,我们将介绍转移学习中的常见技巧 : *fine-tuning*. As shown in :numref:`fig_finetune`,微调包括以下四个步骤: + +1. 在源数据集(例如 iMagenet 数据集)上预训练神经网络模型,即 * 源模型 *。 +1. 创建一个新的神经网络模型,即 * 目标模型 *。这将复制源模型上的所有模型设计及其参数,但输出层除外。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集。我们还假设源模型的输出图层与源数据集的标签密切相关;因此不在目标模型中使用该图层。 +1. 向目标模型添加输出图层,其输出数量是目标数据集中的类别数。然后随机初始化该层的模型参数。 +1. 在目标数据集(如椅子数据集)上训练目标模型。输出图层将从头开始进行训练,而所有其他图层的参数将根据源模型的参数进行微调。 + +![Fine tuning.](../img/finetune.svg) +:label:`fig_finetune` + +当目标数据集比源数据集小得多时,微调有助于提高模型的泛化能力。 + +## 热狗识别 + +让我们通过具体案例演示微调:热狗识别。我们将在一个小型数据集上微调 ReSnet 模型,该数据集已在 iMagenet 数据集上进行了预训练。这个小型数据集包含数千张带热狗和不带热狗的图像。我们将使用微调模型来识别图像中的热狗。 + +```{.python .input} +%matplotlib inline +from d2l import mxnet as d2l +from mxnet import gluon, init, np, npx +from mxnet.gluon import nn +import os + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +%matplotlib inline +from d2l import torch as d2l +from torch import nn +import torch +import torchvision +import os +``` + +### 阅读数据集 + +我们使用的热狗数据集取自在线图片。该数据集包含 1400 张包含热狗的正面类图像以及包含其他食物的尽可能多的负面级图像。两个课程的 1000 张图片用于训练,其余的则用于测试。 + +解压下载的数据集后,我们获得了两个文件夹 `hotdog/train` 和 `hotdog/test`。这两个文件夹都有 `hotdog` 和 `not-hotdog` 个子文件夹,其中任何一个文件夹都包含相应类的图像。 + +```{.python .input} +#@tab all +#@save +d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip', + 'fba480ffa8aa7e0febbb511d181409f899b9baa5') + +data_dir = d2l.download_extract('hotdog') +``` + +我们创建两个实例来分别读取训练和测试数据集中的所有图像文件。 + +```{.python .input} +train_imgs = gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'train')) +test_imgs = gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'test')) +``` + +```{.python .input} +#@tab pytorch +train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train')) +test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test')) +``` + +下面显示了前 8 个正面示例和最后 8 张负面图片。正如你所看到的,图像的大小和纵横比有所不同。 + +```{.python .input} +#@tab all +hotdogs = [train_imgs[i][0] for i in range(8)] +not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)] +d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4); +``` + +在训练期间,我们首先从图像中裁切随机大小和随机长宽比的区域,然后将该区域缩放为 $224 \times 224$ 输入图像。在测试过程中,我们将图像的高度和宽度都缩放到 256 像素,然后裁剪中央 $224 \times 224$ 区域作为输入。此外,对于三个 RGB(红、绿和蓝)颜色通道,我们按频道 * 标准化 * 它们的价值通道。具体而言,通道的平均值将从该通道的每个值中减去,然后将结果除以该通道的标准差。 + +```{.python .input} +# Specify the means and standard deviations of the three RGB channels to +# standardize each channel +normalize = gluon.data.vision.transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + +train_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.RandomResizedCrop(224), + gluon.data.vision.transforms.RandomFlipLeftRight(), + gluon.data.vision.transforms.ToTensor(), + normalize]) + +test_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.Resize(256), + gluon.data.vision.transforms.CenterCrop(224), + gluon.data.vision.transforms.ToTensor(), + normalize]) +``` + +```{.python .input} +#@tab pytorch +# Specify the means and standard deviations of the three RGB channels to +# standardize each channel +normalize = torchvision.transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + +train_augs = torchvision.transforms.Compose([ + torchvision.transforms.RandomResizedCrop(224), + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor(), + normalize]) + +test_augs = torchvision.transforms.Compose([ + torchvision.transforms.Resize(256), + torchvision.transforms.CenterCrop(224), + torchvision.transforms.ToTensor(), + normalize]) +``` + +### 定义和初始化模型 + +我们使用在 iMagenet 数据集上预训练的 Resnet-18 作为源模型。在这里,我们指定 `pretrained=True` 以自动下载预训练的模型参数。如果首次使用此模型,则需要互联网连接才能下载。 + +```{.python .input} +pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) +``` + +```{.python .input} +#@tab pytorch +pretrained_net = torchvision.models.resnet18(pretrained=True) +``` + +:begin_tab:`mxnet` +预训练的源模型实例包含两个成员变量:`features` 和 `output`。前者包含除输出层以外的模型的所有层,后者是模型的输出层。此划分的主要目的是促进对除输出层以外所有层的模型参数进行微调。源模型的成员变量 `output` 如下所示。 +:end_tab: + +:begin_tab:`pytorch` +预训练的源模型实例包含许多要素图层和一个输出图层 `fc`。此划分的主要目的是促进对除输出层以外所有层的模型参数进行微调。下面给出了源模型的成员变量 `fc`。 +:end_tab: + +```{.python .input} +pretrained_net.output +``` + +```{.python .input} +#@tab pytorch +pretrained_net.fc +``` + +作为一个完全连接的层,它将 RESNet 的最终全球平均池输出转换为 iMagenet 数据集的 1000 个类输出。然后,我们构建一个新的神经网络作为目标模型。它的定义方式与预训练源模型的定义方式相同,只是最终图层中的输出数量被设置为目标数据集中的类数(而不是 1000 个)。 + +在下面的代码中,目标模型实例 finetune_net 的成员变量特征中的模型参数被初始化为源模型相应层的模型参数。由于功能中的模型参数是在 iMagenNet 数据集上预训练的,并且足够好,因此通常只需要较小的学习速率即可微调这些参数。 + +成员变量输出中的模型参数是随机初始化的,通常需要更高的学习速率才能从头开始训练。假设 Trainer 实例中的学习速率为,我们将迭代中成员变量输出中模型参数的学习速率设置为 10。 + +在下面的代码中,初始化目标模型实例 `finetune_net` 输出层之前的模型参数,以对源模型中相应层的参数进行建模。由于这些模型参数是通过 iMagenet 上的预训练获得的,因此它们很有效。因此,我们只能使用较小的学习速率进行 * 微调 * 这样的预训练参数。相比之下,输出层中的模型参数是随机初始化的,通常需要从头开始学习更高的学习速率。让基本学习速率为 $\eta$,学习速率 $10\eta$ 将用于迭代输出层中的模型参数。 + +```{.python .input} +finetune_net = gluon.model_zoo.vision.resnet18_v2(classes=2) +finetune_net.features = pretrained_net.features +finetune_net.output.initialize(init.Xavier()) +# The model parameters in the output layer will be iterated using a learning +# rate ten times greater +finetune_net.output.collect_params().setattr('lr_mult', 10) +``` + +```{.python .input} +#@tab pytorch +finetune_net = torchvision.models.resnet18(pretrained=True) +finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2) +nn.init.xavier_uniform_(finetune_net.fc.weight); +``` + +### 微调模型 + +首先,我们定义了一个训练函数 `train_fine_tuning`,该函数使用微调,因此可以多次调用。 + +```{.python .input} +def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5): + train_iter = gluon.data.DataLoader( + train_imgs.transform_first(train_augs), batch_size, shuffle=True) + test_iter = gluon.data.DataLoader( + test_imgs.transform_first(test_augs), batch_size) + devices = d2l.try_all_gpus() + net.collect_params().reset_ctx(devices) + net.hybridize() + loss = gluon.loss.SoftmaxCrossEntropyLoss() + trainer = gluon.Trainer(net.collect_params(), 'sgd', { + 'learning_rate': learning_rate, 'wd': 0.001}) + d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices) +``` + +```{.python .input} +#@tab pytorch +# If `param_group=True`, the model parameters in the output layer will be +# updated using a learning rate ten times greater +def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5, + param_group=True): + train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train'), transform=train_augs), + batch_size=batch_size, shuffle=True) + test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'test'), transform=test_augs), + batch_size=batch_size) + devices = d2l.try_all_gpus() + loss = nn.CrossEntropyLoss(reduction="none") + if param_group: + params_1x = [param for name, param in net.named_parameters() + if name not in ["fc.weight", "fc.bias"]] + trainer = torch.optim.SGD([{'params': params_1x}, + {'params': net.fc.parameters(), + 'lr': learning_rate * 10}], + lr=learning_rate, weight_decay=0.001) + else: + trainer = torch.optim.SGD(net.parameters(), lr=learning_rate, + weight_decay=0.001) + d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices) +``` + +我们将基本学习速率设置为小值,以便 * 微调 * 通过预训练获得的模型参数。根据之前的设置,我们将使用高十倍的学习率从头开始训练目标模型的输出层参数。 + +```{.python .input} +train_fine_tuning(finetune_net, 0.01) +``` + +```{.python .input} +#@tab pytorch +train_fine_tuning(finetune_net, 5e-5) +``` + +为了进行比较,我们定义了一个相同的模型,但是将其所有模型参数初始化为随机值。由于整个模型需要从头开始训练,因此我们可以使用更大的学习率。 + +```{.python .input} +scratch_net = gluon.model_zoo.vision.resnet18_v2(classes=2) +scratch_net.initialize(init=init.Xavier()) +train_fine_tuning(scratch_net, 0.1) +``` + +```{.python .input} +#@tab pytorch +scratch_net = torchvision.models.resnet18() +scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2) +train_fine_tuning(scratch_net, 5e-4, param_group=False) +``` + +正如我们所看到的,微调模型在同一纪元中往往表现更好,因为它的初始参数值更有效。 + +## 摘要 + +* 转移学习将从源数据集中学到的知识传输到目标数据集。微调是转移学习的常见技巧。 +* 目标模型将从源模型中复制所有模型设计及其参数,但输出层除外,并根据目标数据集对这些参数进行微调。相比之下,需要从头开始训练目标模型的输出层。 +* 通常,微调参数使用较小的学习速率,而从头开始训练输出层可以使用更大的学习速率。 + +## 练习 + +1. 继续提高 `finetune_net` 的学习率。模型的准确性如何变化? +2. 在比较实验中进一步调整 `finetune_net` 和 `scratch_net` 的超参数。它们的准确性还有不同吗? +3. 将输出层 `finetune_net` 之前的参数设置为源模型的参数,在训练期间不要 * 更新它们。模型的准确性如何变化?你可以使用以下代码。 + +```{.python .input} +finetune_net.features.collect_params().setattr('grad_req', 'null') +``` + +```{.python .input} +#@tab pytorch +for param in finetune_net.parameters(): + param.requires_grad = False +``` + +4. 事实上,`ImageNet` 数据集中有一个 “热狗” 类。可以通过以下代码获取输出层中的相应权重参数。我们怎样才能利用这个权重参数? + +```{.python .input} +weight = pretrained_net.output.weight +hotdog_w = np.split(weight.data(), 1000, axis=0)[713] +hotdog_w.shape +``` + +```{.python .input} +#@tab pytorch +weight = pretrained_net.fc.weight +hotdog_w = torch.split(weight.data, 1, dim=0)[713] +hotdog_w.shape +``` + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/368) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1439) +:end_tab: diff --git a/chapter_computer-vision/fine-tuning_origin.md b/chapter_computer-vision/fine-tuning_origin.md new file mode 100644 index 000000000..88363d449 --- /dev/null +++ b/chapter_computer-vision/fine-tuning_origin.md @@ -0,0 +1,373 @@ +# Fine-Tuning +:label:`sec_fine_tuning` + +In earlier chapters, we discussed how to train models on the Fashion-MNIST training dataset with only 60000 images. We also described ImageNet, the most widely used large-scale image dataset in academia, which has more than 10 million images and 1000 objects. However, the size of the dataset that we usually encounter is between those of the two datasets. + + +Suppose that we want to recognize different types of chairs from images, and then recommend purchase links to users. +One possible method is to first identify +100 common chairs, +take 1000 images of different angles for each chair, +and then train a classification model on the collected image dataset. +Although this chair dataset may be larger than the Fashion-MNIST dataset, +the number of examples is still less than one-tenth of +that in ImageNet. +This may lead to overfitting of complicated models +that are suitable for ImageNet on this chair dataset. +Besides, due to the limited amount of training examples, +the accuracy of the trained model +may not meet practical requirements. + + +In order to address the above problems, +an obvious solution is to collect more data. +However, collecting and labeling data can take a lot of time and money. +For example, in order to collect the ImageNet dataset, researchers have spent millions of dollars from research funding. +Although the current data collection cost has been significantly reduced, this cost still cannot be ignored. + + +Another solution is to apply *transfer learning* to transfer the knowledge learned from the *source dataset* to the *target dataset*. +For example, although most of the images in the ImageNet dataset have nothing to do with chairs, the model trained on this dataset may extract more general image features, which can help identify edges, textures, shapes, and object composition. +These similar features may +also be effective for recognizing chairs. + + +## Steps + + +In this section, we will introduce a common technique in transfer learning: *fine-tuning*. As shown in :numref:`fig_finetune`, fine-tuning consists of the following four steps: + + +1. Pretrain a neural network model, i.e., the *source model*, on a source dataset (e.g., the ImageNet dataset). +1. Create a new neural network model, i.e., the *target model*. This copies all model designs and their parameters on the source model except the output layer. We assume that these model parameters contain the knowledge learned from the source dataset and this knowledge will also be applicable to the target dataset. We also assume that the output layer of the source model is closely related to the labels of the source dataset; thus it is not used in the target model. +1. Add an output layer to the target model, whose number of outputs is the number of categories in the target dataset. Then randomly initialize the model parameters of this layer. +1. Train the target model on the target dataset, such as a chair dataset. The output layer will be trained from scratch, while the parameters of all the other layers are fine-tuned based on the parameters of the source model. + +![Fine tuning.](../img/finetune.svg) +:label:`fig_finetune` + +When target datasets are much smaller than source datasets, fine-tuning helps to improve models' generalization ability. + + +## Hot Dog Recognition + +Let us demonstrate fine-tuning via a concrete case: +hot dog recognition. +We will fine-tune a ResNet model on a small dataset, +which was pretrained on the ImageNet dataset. +This small dataset consists of +thousands of images with and without hot dogs. +We will use the fine-tuned model to recognize +hot dogs from images. + +```{.python .input} +%matplotlib inline +from d2l import mxnet as d2l +from mxnet import gluon, init, np, npx +from mxnet.gluon import nn +import os + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +%matplotlib inline +from d2l import torch as d2l +from torch import nn +import torch +import torchvision +import os +``` + +### Reading the Dataset + +The hot dog dataset we use was taken from online images. +This dataset consists of +1400 positive-class images containing hot dogs, +and as many negative-class images containing other foods. +1000 images of both classes are used for training and the rest are for testing. + + +After unzipping the downloaded dataset, +we obtain two folders `hotdog/train` and `hotdog/test`. Both folders have `hotdog` and `not-hotdog` subfolders, either of which contains images of +the corresponding class. + +```{.python .input} +#@tab all +#@save +d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip', + 'fba480ffa8aa7e0febbb511d181409f899b9baa5') + +data_dir = d2l.download_extract('hotdog') +``` + +We create two instances to read all the image files in the training and testing datasets, respectively. + +```{.python .input} +train_imgs = gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'train')) +test_imgs = gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'test')) +``` + +```{.python .input} +#@tab pytorch +train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train')) +test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test')) +``` + +The first 8 positive examples and the last 8 negative images are shown below. As you can see, the images vary in size and aspect ratio. + +```{.python .input} +#@tab all +hotdogs = [train_imgs[i][0] for i in range(8)] +not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)] +d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4); +``` + +During training, we first crop a random area of random size and random aspect ratio from the image, +and then scale this area +to a $224 \times 224$ input image. +During testing, we scale both the height and width of an image to 256 pixels, and then crop a central $224 \times 224$ area as input. +In addition, +for the three RGB (red, green, and blue) color channels +we *standardize* their values channel by channel. +Concretely, +the mean value of a channel is subtracted from each value of that channel and then the result is divided by the standard deviation of that channel. + +```{.python .input} +# Specify the means and standard deviations of the three RGB channels to +# standardize each channel +normalize = gluon.data.vision.transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + +train_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.RandomResizedCrop(224), + gluon.data.vision.transforms.RandomFlipLeftRight(), + gluon.data.vision.transforms.ToTensor(), + normalize]) + +test_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.Resize(256), + gluon.data.vision.transforms.CenterCrop(224), + gluon.data.vision.transforms.ToTensor(), + normalize]) +``` + +```{.python .input} +#@tab pytorch +# Specify the means and standard deviations of the three RGB channels to +# standardize each channel +normalize = torchvision.transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + +train_augs = torchvision.transforms.Compose([ + torchvision.transforms.RandomResizedCrop(224), + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor(), + normalize]) + +test_augs = torchvision.transforms.Compose([ + torchvision.transforms.Resize(256), + torchvision.transforms.CenterCrop(224), + torchvision.transforms.ToTensor(), + normalize]) +``` + +### Defining and Initializing the Model + +We use ResNet-18, which was pretrained on the ImageNet dataset, as the source model. Here, we specify `pretrained=True` to automatically download the pretrained model parameters. +If this model is used for the first time, +Internet connection is required for download. + +```{.python .input} +pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) +``` + +```{.python .input} +#@tab pytorch +pretrained_net = torchvision.models.resnet18(pretrained=True) +``` + +:begin_tab:`mxnet` +The pretrained source model instance contains two member variables: `features` and `output`. The former contains all layers of the model except the output layer, and the latter is the output layer of the model. +The main purpose of this division is to facilitate the fine-tuning of model parameters of all layers but the output layer. The member variable `output` of source model is shown below. +:end_tab: + +:begin_tab:`pytorch` +The pretrained source model instance contains a number of feature layers and an output layer `fc`. +The main purpose of this division is to facilitate the fine-tuning of model parameters of all layers but the output layer. The member variable `fc` of source model is given below. +:end_tab: + +```{.python .input} +pretrained_net.output +``` + +```{.python .input} +#@tab pytorch +pretrained_net.fc +``` + +As a fully-connected layer, it transforms ResNet's final global average pooling outputs into 1000 class outputs of the ImageNet dataset. +We then construct a new neural network as the target model. It is defined in the same way as the pretrained source model except that +its number of outputs in the final layer +is set to +the number of classes in the target dataset (rather than 1000). + + + + +In the following code, the model parameters in the member variable features of the target model instance finetune_net are initialized to the model parameters of the corresponding layer of the source model. Since the model parameters in the features are pre-trained on the ImageNet data set and are good enough, generally only a small learning rate is needed to fine-tune these parameters. + +The model parameters in the member variable output are initialized randomly, and generally require a larger learning rate to train from scratch. Assuming that the learning rate in the Trainer instance is η, we set the learning rate of the model parameters in the member variable output to be 10η in the iteration. + + +In the code below, the model parameters before the output layer of the target model instance `finetune_net` are initialized to model parameters of the corresponding layers from the source model. +Since these model parameters were obtained via pretraining on ImageNet, +they are effective. +Therefore, we can only use +a small learning rate to *fine-tune* such pretrained parameters. +In contrast, model parameters in the output layer are randomly initialized and generally require a larger learning rate to be learned from scratch. +Let the base learning rate be $\eta$, a learning rate of $10\eta$ will be used to iterate the model parameters in the output layer. + +```{.python .input} +finetune_net = gluon.model_zoo.vision.resnet18_v2(classes=2) +finetune_net.features = pretrained_net.features +finetune_net.output.initialize(init.Xavier()) +# The model parameters in the output layer will be iterated using a learning +# rate ten times greater +finetune_net.output.collect_params().setattr('lr_mult', 10) +``` + +```{.python .input} +#@tab pytorch +finetune_net = torchvision.models.resnet18(pretrained=True) +finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2) +nn.init.xavier_uniform_(finetune_net.fc.weight); +``` + +### Fine-Tuning the Model + +First, we define a training function `train_fine_tuning` that uses fine-tuning so it can be called multiple times. + +```{.python .input} +def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5): + train_iter = gluon.data.DataLoader( + train_imgs.transform_first(train_augs), batch_size, shuffle=True) + test_iter = gluon.data.DataLoader( + test_imgs.transform_first(test_augs), batch_size) + devices = d2l.try_all_gpus() + net.collect_params().reset_ctx(devices) + net.hybridize() + loss = gluon.loss.SoftmaxCrossEntropyLoss() + trainer = gluon.Trainer(net.collect_params(), 'sgd', { + 'learning_rate': learning_rate, 'wd': 0.001}) + d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices) +``` + +```{.python .input} +#@tab pytorch +# If `param_group=True`, the model parameters in the output layer will be +# updated using a learning rate ten times greater +def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5, + param_group=True): + train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train'), transform=train_augs), + batch_size=batch_size, shuffle=True) + test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'test'), transform=test_augs), + batch_size=batch_size) + devices = d2l.try_all_gpus() + loss = nn.CrossEntropyLoss(reduction="none") + if param_group: + params_1x = [param for name, param in net.named_parameters() + if name not in ["fc.weight", "fc.bias"]] + trainer = torch.optim.SGD([{'params': params_1x}, + {'params': net.fc.parameters(), + 'lr': learning_rate * 10}], + lr=learning_rate, weight_decay=0.001) + else: + trainer = torch.optim.SGD(net.parameters(), lr=learning_rate, + weight_decay=0.001) + d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices) +``` + +We set the base learning rate to a small value +in order to *fine-tune* the model parameters obtained via pretraining. Based on the previous settings, we will train the output layer parameters of the target model from scratch using a learning rate ten times greater. + +```{.python .input} +train_fine_tuning(finetune_net, 0.01) +``` + +```{.python .input} +#@tab pytorch +train_fine_tuning(finetune_net, 5e-5) +``` + +For comparison, we define an identical model, but initialize all of its model parameters to random values. Since the entire model needs to be trained from scratch, we can use a larger learning rate. + +```{.python .input} +scratch_net = gluon.model_zoo.vision.resnet18_v2(classes=2) +scratch_net.initialize(init=init.Xavier()) +train_fine_tuning(scratch_net, 0.1) +``` + +```{.python .input} +#@tab pytorch +scratch_net = torchvision.models.resnet18() +scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2) +train_fine_tuning(scratch_net, 5e-4, param_group=False) +``` + +As we can see, the fine-tuned model tends to perform better for the same epoch +because its initial parameter values are more effective. + + +## Summary + +* Transfer learning transfers knowledge learned from the source dataset to the target dataset. Fine-tuning is a common technique for transfer learning. +* The target model copies all model designs with their parameters from the source model except the output layer, and fine-tunes these parameters based on the target dataset. In contrast, the output layer of the target model needs to be trained from scratch. +* Generally, fine-tuning parameters uses a smaller learning rate, while training the output layer from scratch can use a larger learning rate. + + +## Exercises + +1. Keep increasing the learning rate of `finetune_net`. How does the accuracy of the model change? +2. Further adjust hyperparameters of `finetune_net` and `scratch_net` in the comparative experiment. Do they still differ in accuracy? +3. Set the parameters before the output layer of `finetune_net` to those of the source model and do *not* update them during training. How does the accuracy of the model change? You can use the following code. + +```{.python .input} +finetune_net.features.collect_params().setattr('grad_req', 'null') +``` + +```{.python .input} +#@tab pytorch +for param in finetune_net.parameters(): + param.requires_grad = False +``` + +4. In fact, there is a "hotdog" class in the `ImageNet` dataset. Its corresponding weight parameter in the output layer can be obtained via the following code. How can we leverage this weight parameter? + +```{.python .input} +weight = pretrained_net.output.weight +hotdog_w = np.split(weight.data(), 1000, axis=0)[713] +hotdog_w.shape +``` + +```{.python .input} +#@tab pytorch +weight = pretrained_net.fc.weight +hotdog_w = torch.split(weight.data, 1, dim=0)[713] +hotdog_w.shape +``` + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/368) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1439) +:end_tab: diff --git a/chapter_computer-vision/image-augmentation.md b/chapter_computer-vision/image-augmentation.md new file mode 100644 index 000000000..abc659963 --- /dev/null +++ b/chapter_computer-vision/image-augmentation.md @@ -0,0 +1,379 @@ +# 图像增强 +:label:`sec_image_augmentation` + +在 :numref:`sec_alexnet` 中,我们提到大型数据集是各种应用程序中深度神经网络成功的先决条件。 +*图片增强 * +在对训练图像进行一系列随机更改之后,会生成类似但截然不同的训练示例,从而扩大了训练集的规模。或者,图像增强的动机可能是,训练示例的随机调整使模型减少了对某些属性的依赖,从而提高了它们的泛化能力。例如,我们可以用不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,从而减少模型对物体位置的依赖性。我们还可以调整亮度和颜色等因素,以降低模型对颜色的敏感度。当时,图像增强对 AleXNet 的成功可能是必不可少的。在本节中,我们将讨论这种在计算机视觉中广泛使用的技术。 + +```{.python .input} +%matplotlib inline +from d2l import mxnet as d2l +from mxnet import autograd, gluon, image, init, np, npx +from mxnet.gluon import nn + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +%matplotlib inline +from d2l import torch as d2l +import torch +import torchvision +from torch import nn +``` + +## 常见的图像增强方法 + +在我们对常见图像增强方法的调查中,我们将以下面的 $400\times 500$ 图像作为示例。 + +```{.python .input} +d2l.set_figsize() +img = image.imread('../img/cat1.jpg') +d2l.plt.imshow(img.asnumpy()); +``` + +```{.python .input} +#@tab pytorch +d2l.set_figsize() +img = d2l.Image.open('../img/cat1.jpg') +d2l.plt.imshow(img); +``` + +大多数图像增强方法都有一定程度的随机性。为了让我们更容易观察图像增强的效果,接下来我们定义了一个辅助函数 `apply`。此函数在输入图像 `img` 上多次运行图像增强方法 `aug` 并显示所有结果。 + +```{.python .input} +#@tab all +def apply(img, aug, num_rows=2, num_cols=4, scale=1.5): + Y = [aug(img) for _ in range(num_rows * num_cols)] + d2l.show_images(Y, num_rows, num_cols, scale=scale) +``` + +### 翻转和裁剪 + +向左和向右翻转图像通常不会改变对象的类别。这是最早和最广泛使用的图像增强方法之一。接下来,我们使用 `transforms` 模块创建 `RandomFlipLeftRight` 实例,该实例以 50% 的几率左右翻转图像。 + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomFlipLeftRight()) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.RandomHorizontalFlip()) +``` + +向上和向下翻转并不像向左和向右翻转那么常见。但至少对于这个示例图像,向上和向下翻转不会妨碍识别。接下来,我们创建一个 `RandomFlipTopBottom` 实例,以 50% 的机会上下翻转图像。 + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomFlipTopBottom()) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.RandomVerticalFlip()) +``` + +在我们使用的示例图片中,猫在图像的中间,但一般情况可能并非如此。在 :numref:`sec_pooling` 中,我们解释说,池层可以降低卷积层对目标位置的敏感度。此外,我们还可以随机裁剪图像,使物体以不同比例显示在图像中的不同位置,这也可以降低模型对目标位置的灵敏度。 + +在下面的代码中,我们随机裁剪面积为 10 美元\%\ sim 100\ %$ of the original area each time, and the ratio of width to height of this area is randomly selected from $0.5\ sim 2$ 的区域。然后,区域的宽度和高度都将缩放到 200 像素。除非另有说明,本节中 $a$ 和 $b$ 之间的随机数是指从区间 $[a, b]$ 随机和均匀抽样获得的连续值。 + +```{.python .input} +shape_aug = gluon.data.vision.transforms.RandomResizedCrop( + (200, 200), scale=(0.1, 1), ratio=(0.5, 2)) +apply(img, shape_aug) +``` + +```{.python .input} +#@tab pytorch +shape_aug = torchvision.transforms.RandomResizedCrop( + (200, 200), scale=(0.1, 1), ratio=(0.5, 2)) +apply(img, shape_aug) +``` + +### 改变颜色 + +另一种增强方法是改变颜色。我们可以更改图像颜色的四个方面:亮度、对比度、饱和度和色调。在下面的示例中,我们将图像的亮度随机更改为原始图像的 50% ($1-0.5$) 和 150% ($1+0.5$) 之间的值。 + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomBrightness(0.5)) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.ColorJitter( + brightness=0.5, contrast=0, saturation=0, hue=0)) +``` + +同样,我们可以随机改变图像的色调。 + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomHue(0.5)) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.ColorJitter( + brightness=0, contrast=0, saturation=0, hue=0.5)) +``` + +我们还可以创建一个 `RandomColorJitter` 实例,并设置如何同时随机更改镜像的 `brightness`、`saturation` 和 `hue`。 + +```{.python .input} +color_aug = gluon.data.vision.transforms.RandomColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5) +apply(img, color_aug) +``` + +```{.python .input} +#@tab pytorch +color_aug = torchvision.transforms.ColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5) +apply(img, color_aug) +``` + +### 结合多个图像增强方法 + +实际上,我们将结合多种图像增强方法。例如,我们可以组合上面定义的不同图像增强方法,并通过 `Compose` 实例将它们应用到每个图像。 + +```{.python .input} +augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.RandomFlipLeftRight(), color_aug, shape_aug]) +apply(img, augs) +``` + +```{.python .input} +#@tab pytorch +augs = torchvision.transforms.Compose([ + torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug]) +apply(img, augs) +``` + +## 使用图像增强进行培训 + +让我们使用图像增强来训练模型。在这里,我们使用 CIFAR-10 数据集而不是我们之前使用的 Fashion-Mnist 数据集。这是因为 Fashion-Mnist 数据集中对象的位置和大小已规范化,而 CIFAR-10 数据集中对象的颜色和大小差异更显著。CIFAR-10 数据集中的前 32 个训练图像如下所示。 + +```{.python .input} +d2l.show_images(gluon.data.vision.CIFAR10( + train=True)[0:32][0], 4, 8, scale=0.8); +``` + +```{.python .input} +#@tab pytorch +all_images = torchvision.datasets.CIFAR10(train=True, root="../data", + download=True) +d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8); +``` + +为了在预测时获得明确的结果,我们通常只将图像增强应用于训练样本,在预测时不使用带随机操作的图像增强功能。这里我们只使用最简单的随机左右翻转。此外,我们使用 totenSor 实例将小批量图像转换为 MxNet 所需的格式,即形状为(批量大小、通道数、高度、宽度),值范围介于 0 到 1 之间,类型是 32 位浮点数。 + +为了在预测期间获得明确的结果,我们通常只对训练示例应用图像增强,在预测期间不使用随机操作的图像增强功能。这里我们只使用最简单的随机左右翻转方法。此外,我们使用 `ToTensor` 实例将一批图像转换为深度学习框架所要求的格式,即介于 0 到 1 之间的 32 位浮点数,形状为(批量大小、通道数、高度、宽度)。 + +```{.python .input} +train_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.RandomFlipLeftRight(), + gluon.data.vision.transforms.ToTensor()]) + +test_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.ToTensor()]) +``` + +```{.python .input} +#@tab pytorch +train_augs = torchvision.transforms.Compose([ + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor()]) + +test_augs = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor()]) +``` + +:begin_tab:`mxnet` +接下来,我们定义了一个辅助函数来方便读取图像和应用图像增强。Gluon 的数据集提供的 `transform_first` 函数将图像增强应用于每个训练示例的第一个元素(图像和标签),即图像。有关 `DataLoader` 的详细介绍,请参阅 :numref:`sec_fashion_mnist`。 +:end_tab: + +:begin_tab:`pytorch` +接下来,我们定义了一个辅助函数来方便读取图像和应用图像增强。PyTorch 数据集提供的 `transform` 参数应用增强来转换图像。有关 `DataLoader` 的详细介绍,请参阅 :numref:`sec_fashion_mnist`。 +:end_tab: + +```{.python .input} +def load_cifar10(is_train, augs, batch_size): + return gluon.data.DataLoader( + gluon.data.vision.CIFAR10(train=is_train).transform_first(augs), + batch_size=batch_size, shuffle=is_train, + num_workers=d2l.get_dataloader_workers()) +``` + +```{.python .input} +#@tab pytorch +def load_cifar10(is_train, augs, batch_size): + dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train, + transform=augs, download=True) + dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, + shuffle=is_train, num_workers=d2l.get_dataloader_workers()) + return dataloader +``` + +### 多 GPU 培训 + +我们在 CIFAR-10 数据集上训练 :numref:`sec_resnet` 的 Resnet-18 模型。回想一下 :numref:`sec_multi_gpu_concise` 中对多 GPU 培训的介绍。在下面,我们定义了一个函数来使用多个 GPU 来训练和评估模型。 + +```{.python .input} +#@save +def train_batch_ch13(net, features, labels, loss, trainer, devices, + split_f=d2l.split_batch): + X_shards, y_shards = split_f(features, labels, devices) + with autograd.record(): + pred_shards = [net(X_shard) for X_shard in X_shards] + ls = [loss(pred_shard, y_shard) for pred_shard, y_shard + in zip(pred_shards, y_shards)] + for l in ls: + l.backward() + # The `True` flag allows parameters with stale gradients, which is useful + # later (e.g., in fine-tuning BERT) + trainer.step(labels.shape[0], ignore_stale_grad=True) + train_loss_sum = sum([float(l.sum()) for l in ls]) + train_acc_sum = sum(d2l.accuracy(pred_shard, y_shard) + for pred_shard, y_shard in zip(pred_shards, y_shards)) + return train_loss_sum, train_acc_sum +``` + +```{.python .input} +#@tab pytorch +#@save +def train_batch_ch13(net, X, y, loss, trainer, devices): + if isinstance(X, list): + # Required for BERT fine-tuning (to be covered later) + X = [x.to(devices[0]) for x in X] + else: + X = X.to(devices[0]) + y = y.to(devices[0]) + net.train() + trainer.zero_grad() + pred = net(X) + l = loss(pred, y) + l.sum().backward() + trainer.step() + train_loss_sum = l.sum() + train_acc_sum = d2l.accuracy(pred, y) + return train_loss_sum, train_acc_sum +``` + +```{.python .input} +#@save +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus(), split_f=d2l.split_batch): + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + for epoch in range(num_epochs): + # Sum of training loss, sum of training accuracy, no. of examples, + # no. of predictions + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices, split_f) + metric.add(l, acc, labels.shape[0], labels.size) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpus(net, test_iter, split_f) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') +``` + +```{.python .input} +#@tab pytorch +#@save +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus()): + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + for epoch in range(num_epochs): + # Sum of training loss, sum of training accuracy, no. of examples, + # no. of predictions + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices) + metric.add(l, acc, labels.shape[0], labels.numel()) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpu(net, test_iter) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') +``` + +现在我们可以定义 `train_with_data_aug` 函数来使用图像增强来训练模型。此函数获取所有可用的 GPU,使用 Adam 作为优化算法,将图像增强应用于训练数据集,最后调用刚刚定义的用于训练和评估模型的 `train_ch13` 函数。 + +```{.python .input} +batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10) +net.initialize(init=init.Xavier(), ctx=devices) + +def train_with_data_aug(train_augs, test_augs, net, lr=0.001): + train_iter = load_cifar10(True, train_augs, batch_size) + test_iter = load_cifar10(False, test_augs, batch_size) + loss = gluon.loss.SoftmaxCrossEntropyLoss() + trainer = gluon.Trainer(net.collect_params(), 'adam', + {'learning_rate': lr}) + train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices) +``` + +```{.python .input} +#@tab pytorch +batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3) + +def init_weights(m): + if type(m) in [nn.Linear, nn.Conv2d]: + nn.init.xavier_uniform_(m.weight) + +net.apply(init_weights) + +def train_with_data_aug(train_augs, test_augs, net, lr=0.001): + train_iter = load_cifar10(True, train_augs, batch_size) + test_iter = load_cifar10(False, test_augs, batch_size) + loss = nn.CrossEntropyLoss(reduction="none") + trainer = torch.optim.Adam(net.parameters(), lr=lr) + train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices) +``` + +让我们使用基于随机左右翻转的图像增强来训练模型。 + +```{.python .input} +#@tab all +train_with_data_aug(train_augs, test_augs, net) +``` + +## 摘要 + +* 图像增强基于现有训练数据生成随机图像,以提高模型的概化能力。 +* 为了在预测期间获得明确的结果,我们通常只将图像增强应用于训练示例,在预测期间不会将图像增强与随机操作结合使用。 +* 深度学习框架提供了许多不同的图像增强方法,这些方法可以同时应用。 + +## 练习 + +1. 在不使用图像增强的情况下训练模型:`train_with_data_aug(test_augs, test_augs)`。比较使用和不使用图像增强时的训练和测试准确性。这个比较实验能否支持图像增强可以缓解过度拟合的论点吗?为什么? +1. 在 CIFAR-10 数据集的模型训练中结合多种不同的图像增强方法。它能提高测试准确性吗? +1. 请参阅深度学习框架的在线文档。它还提供哪些其他图像增强方法? + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/367) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1404) +:end_tab: diff --git a/chapter_computer-vision/image-augmentation_origin.md b/chapter_computer-vision/image-augmentation_origin.md new file mode 100644 index 000000000..50c0af273 --- /dev/null +++ b/chapter_computer-vision/image-augmentation_origin.md @@ -0,0 +1,439 @@ +# Image Augmentation +:label:`sec_image_augmentation` + +In :numref:`sec_alexnet`, +we mentioned that large datasets +are a prerequisite +for the success of +deep neural networks +in various applications. +*Image augmentation* +generates similar but distinct training examples +after a series of random changes to the training images, thereby expanding the size of the training set. +Alternatively, +image augmentation can be motivated +by the fact that +random tweaks of training examples +allow models to less rely on +certain attributes, thereby improving their generalization ability. +For example, we can crop an image in different ways to make the object of interest appear in different positions, thereby reducing the dependence of a model on the position of the object. +We can also adjust factors such as brightness and color to reduce a model's sensitivity to color. +It is probably true +that image augmentation was indispensable +for the success of AlexNet at that time. +In this section we will discuss this widely used technique in computer vision. + +```{.python .input} +%matplotlib inline +from d2l import mxnet as d2l +from mxnet import autograd, gluon, image, init, np, npx +from mxnet.gluon import nn + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +%matplotlib inline +from d2l import torch as d2l +import torch +import torchvision +from torch import nn +``` + +## Common Image Augmentation Methods + +In our investigation of common image augmentation methods, we will use the following $400\times 500$ image an example. + +```{.python .input} +d2l.set_figsize() +img = image.imread('../img/cat1.jpg') +d2l.plt.imshow(img.asnumpy()); +``` + +```{.python .input} +#@tab pytorch +d2l.set_figsize() +img = d2l.Image.open('../img/cat1.jpg') +d2l.plt.imshow(img); +``` + +Most image augmentation methods have a certain degree of randomness. To make it easier for us to observe the effect of image augmentation, next we define an auxiliary function `apply`. This function runs the image augmentation method `aug` multiple times on the input image `img` and shows all the results. + +```{.python .input} +#@tab all +def apply(img, aug, num_rows=2, num_cols=4, scale=1.5): + Y = [aug(img) for _ in range(num_rows * num_cols)] + d2l.show_images(Y, num_rows, num_cols, scale=scale) +``` + +### Flipping and Cropping + +Flipping the image left and right usually does not change the category of the object. +This is one of the earliest and most widely used methods of image augmentation. +Next, we use the `transforms` module to create the `RandomFlipLeftRight` instance, which flips +an image left and right with a 50% chance. + + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomFlipLeftRight()) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.RandomHorizontalFlip()) +``` + +Flipping up and down is not as common as flipping left and right. But at least for this example image, flipping up and down does not hinder recognition. +Next, we create a `RandomFlipTopBottom` instance to flip +an image up and down with a 50% chance. + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomFlipTopBottom()) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.RandomVerticalFlip()) +``` + +In the example image we used, the cat is in the middle of the image, but this may not be the case in general. +In :numref:`sec_pooling`, we explained that the pooling layer can reduce the sensitivity of a convolutional layer to the target position. +In addition, we can also randomly crop the image to make objects appear in different positions in the image at different scales, which can also reduce the sensitivity of a model to the target position. + +In the code below, we randomly crop an area with an area of $10\% \sim 100\%$ of the original area each time, and the ratio of width to height of this area is randomly selected from $0.5 \sim 2$. Then, the width and height of the region are both scaled to 200 pixels. +Unless otherwise specified, the random number between $a$ and $b$ in this section refers to a continuous value obtained by random and uniform sampling from the interval $[a, b]$. + + +```{.python .input} +shape_aug = gluon.data.vision.transforms.RandomResizedCrop( + (200, 200), scale=(0.1, 1), ratio=(0.5, 2)) +apply(img, shape_aug) +``` + +```{.python .input} +#@tab pytorch +shape_aug = torchvision.transforms.RandomResizedCrop( + (200, 200), scale=(0.1, 1), ratio=(0.5, 2)) +apply(img, shape_aug) +``` + +### Changing Colors + +Another augmentation method is changing colors. We can change four aspects of the image color: brightness, contrast, saturation, and hue. In the example below, we randomly change the brightness of the image to a value between 50% ($1-0.5$) and 150% ($1+0.5$) of the original image. + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomBrightness(0.5)) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.ColorJitter( + brightness=0.5, contrast=0, saturation=0, hue=0)) +``` + +Similarly, we can randomly change the hue of the image. + +```{.python .input} +apply(img, gluon.data.vision.transforms.RandomHue(0.5)) +``` + +```{.python .input} +#@tab pytorch +apply(img, torchvision.transforms.ColorJitter( + brightness=0, contrast=0, saturation=0, hue=0.5)) +``` + +We can also create a `RandomColorJitter` instance and set how to randomly change the `brightness`, `contrast`, `saturation`, and `hue` of the image at the same time. + +```{.python .input} +color_aug = gluon.data.vision.transforms.RandomColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5) +apply(img, color_aug) +``` + +```{.python .input} +#@tab pytorch +color_aug = torchvision.transforms.ColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5) +apply(img, color_aug) +``` + +### Combining Multiple Image Augmentation Methods + +In practice, we will combine multiple image augmentation methods. +For example, +we can combine the different image augmentation methods defined above and apply them to each image via a `Compose` instance. + +```{.python .input} +augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.RandomFlipLeftRight(), color_aug, shape_aug]) +apply(img, augs) +``` + +```{.python .input} +#@tab pytorch +augs = torchvision.transforms.Compose([ + torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug]) +apply(img, augs) +``` + +## Training with Image Augmentation + +Let us train a model with image augmentation. +Here we use the CIFAR-10 dataset instead of the Fashion-MNIST dataset that we used before. +This is because the position and size of the objects in the Fashion-MNIST dataset have been normalized, while the color and size of the objects in the CIFAR-10 dataset have more significant differences. +The first 32 training images in the CIFAR-10 dataset are shown below. + + +```{.python .input} +d2l.show_images(gluon.data.vision.CIFAR10( + train=True)[0:32][0], 4, 8, scale=0.8); +``` + +```{.python .input} +#@tab pytorch +all_images = torchvision.datasets.CIFAR10(train=True, root="../data", + download=True) +d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8); +``` + +In order to obtain a definite result when predicting, we usually only apply image augmentation to training samples, and do not use image augmentation with random operations when predicting. Here we only use the simplest random left and right flip. In addition, we use the ToTensor instance to convert the small batch of images into the format required by MXNet, that is, the shape is (batch size, number of channels, height, width), the value range is between 0 and 1, and the type is a 32-bit floating point number. + + +In order to obtain definitive results during prediction, we usually only apply image augmentation to the training example, and do not use image augmentation with random operations during prediction. +Here we only use the simplest random left-right flipping method. In addition, we use a `ToTensor` instance to convert a minibatch of images into the format required by the deep learning framework, i.e., +32-bit floating point numbers between 0 and 1 with the shape of (batch size, number of channels, height, width). + + +```{.python .input} +train_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.RandomFlipLeftRight(), + gluon.data.vision.transforms.ToTensor()]) + +test_augs = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.ToTensor()]) +``` + +```{.python .input} +#@tab pytorch +train_augs = torchvision.transforms.Compose([ + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor()]) + +test_augs = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor()]) +``` + +:begin_tab:`mxnet` +Next, we define an auxiliary function to facilitate reading the image and +applying image augmentation. +The `transform_first` function provided by Gluon's +datasets applies image augmentation to the first element of each training +example (image and label), i.e., the image. +For +a detailed introduction to `DataLoader`, please refer to :numref:`sec_fashion_mnist`. +:end_tab: + +:begin_tab:`pytorch` +Next, we define an auxiliary function to facilitate reading the image and +applying image augmentation. +The `transform` argument provided by PyTorch's +dataset applies augmentation to transform the images. +For +a detailed introduction to `DataLoader`, please refer to :numref:`sec_fashion_mnist`. +:end_tab: + +```{.python .input} +def load_cifar10(is_train, augs, batch_size): + return gluon.data.DataLoader( + gluon.data.vision.CIFAR10(train=is_train).transform_first(augs), + batch_size=batch_size, shuffle=is_train, + num_workers=d2l.get_dataloader_workers()) +``` + +```{.python .input} +#@tab pytorch +def load_cifar10(is_train, augs, batch_size): + dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train, + transform=augs, download=True) + dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, + shuffle=is_train, num_workers=d2l.get_dataloader_workers()) + return dataloader +``` + +### Multi-GPU Training + +We train the ResNet-18 model from +:numref:`sec_resnet` on the +CIFAR-10 dataset. +Recall the introduction to +multi-GPU training in :numref:`sec_multi_gpu_concise`. +In the following, +we define a function to train and evaluate the model using multiple GPUs. + +```{.python .input} +#@save +def train_batch_ch13(net, features, labels, loss, trainer, devices, + split_f=d2l.split_batch): + X_shards, y_shards = split_f(features, labels, devices) + with autograd.record(): + pred_shards = [net(X_shard) for X_shard in X_shards] + ls = [loss(pred_shard, y_shard) for pred_shard, y_shard + in zip(pred_shards, y_shards)] + for l in ls: + l.backward() + # The `True` flag allows parameters with stale gradients, which is useful + # later (e.g., in fine-tuning BERT) + trainer.step(labels.shape[0], ignore_stale_grad=True) + train_loss_sum = sum([float(l.sum()) for l in ls]) + train_acc_sum = sum(d2l.accuracy(pred_shard, y_shard) + for pred_shard, y_shard in zip(pred_shards, y_shards)) + return train_loss_sum, train_acc_sum +``` + +```{.python .input} +#@tab pytorch +#@save +def train_batch_ch13(net, X, y, loss, trainer, devices): + if isinstance(X, list): + # Required for BERT fine-tuning (to be covered later) + X = [x.to(devices[0]) for x in X] + else: + X = X.to(devices[0]) + y = y.to(devices[0]) + net.train() + trainer.zero_grad() + pred = net(X) + l = loss(pred, y) + l.sum().backward() + trainer.step() + train_loss_sum = l.sum() + train_acc_sum = d2l.accuracy(pred, y) + return train_loss_sum, train_acc_sum +``` + +```{.python .input} +#@save +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus(), split_f=d2l.split_batch): + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + for epoch in range(num_epochs): + # Sum of training loss, sum of training accuracy, no. of examples, + # no. of predictions + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices, split_f) + metric.add(l, acc, labels.shape[0], labels.size) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpus(net, test_iter, split_f) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') +``` + +```{.python .input} +#@tab pytorch +#@save +def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, + devices=d2l.try_all_gpus()): + timer, num_batches = d2l.Timer(), len(train_iter) + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], + legend=['train loss', 'train acc', 'test acc']) + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + for epoch in range(num_epochs): + # Sum of training loss, sum of training accuracy, no. of examples, + # no. of predictions + metric = d2l.Accumulator(4) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = train_batch_ch13( + net, features, labels, loss, trainer, devices) + metric.add(l, acc, labels.shape[0], labels.numel()) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[3], + None)) + test_acc = d2l.evaluate_accuracy_gpu(net, test_iter) + animator.add(epoch + 1, (None, None, test_acc)) + print(f'loss {metric[0] / metric[2]:.3f}, train acc ' + f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' + f'{str(devices)}') +``` + +Now we can define the `train_with_data_aug` function to train the model with image augmentation. +This function gets all available GPUs, +uses Adam as the optimization algorithm, +applies image augmentation to the training dataset, +and finally calls the `train_ch13` function just defined to train and evaluate the model. + +```{.python .input} +batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10) +net.initialize(init=init.Xavier(), ctx=devices) + +def train_with_data_aug(train_augs, test_augs, net, lr=0.001): + train_iter = load_cifar10(True, train_augs, batch_size) + test_iter = load_cifar10(False, test_augs, batch_size) + loss = gluon.loss.SoftmaxCrossEntropyLoss() + trainer = gluon.Trainer(net.collect_params(), 'adam', + {'learning_rate': lr}) + train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices) +``` + +```{.python .input} +#@tab pytorch +batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3) + +def init_weights(m): + if type(m) in [nn.Linear, nn.Conv2d]: + nn.init.xavier_uniform_(m.weight) + +net.apply(init_weights) + +def train_with_data_aug(train_augs, test_augs, net, lr=0.001): + train_iter = load_cifar10(True, train_augs, batch_size) + test_iter = load_cifar10(False, test_augs, batch_size) + loss = nn.CrossEntropyLoss(reduction="none") + trainer = torch.optim.Adam(net.parameters(), lr=lr) + train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices) +``` + +Let us train the model using image augmentation based on random left-right flipping. + +```{.python .input} +#@tab all +train_with_data_aug(train_augs, test_augs, net) +``` + +## Summary + +* Image augmentation generates random images based on existing training data to improve the generalization ability of models. +* In order to obtain definitive results during prediction, we usually only apply image augmentation to training examples, and do not use image augmentation with random operations during prediction. +* Deep learning frameworks provide many different image augmentation methods, which can be applied simultaneously. + + +## Exercises + +1. Train the model without using image augmentation: `train_with_data_aug(test_augs, test_augs)`. Compare training and testing accuracy when using and not using image augmentation. Can this comparative experiment support the argument that image augmentation can mitigate overfitting? Why? +1. Combine multiple different image augmentation methods in model training on the CIFAR-10 dataset. Does it improve test accuracy? +1. Refer to the online documentation of the deep learning framework. What other image augmentation methods does it also provide? + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/367) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1404) +:end_tab: diff --git a/chapter_computer-vision/index.md b/chapter_computer-vision/index.md new file mode 100644 index 000000000..7d1e366ff --- /dev/null +++ b/chapter_computer-vision/index.md @@ -0,0 +1,25 @@ +# 计算机视觉 +:label:`chap_cv` + +无论是医疗诊断、自动驾驶车辆、摄像头监控还是智能滤镜,计算机视觉领域的许多应用都与我们当前和未来的生活密切相关。近年来,深度学习一直是提高计算机视觉系统性能的变革力量。可以说,最先进的计算机视觉应用程序与深度学习几乎是不可分割的。有鉴于此,本章将重点介绍计算机视觉领域,并探讨最近在学术界和行业中具有影响力的方法和应用。 + +在 :numref:`chap_cnn` 和 :numref:`chap_modern_cnn` 中,我们研究了计算机视觉中常用的各种卷积神经网络,并将它们应用到简单的图像分类任务中。在本章开头,我们将介绍两种可以改进模型泛化的方法,即 * 图像增强 * 和 * 微调 *,并将它们应用于图像分类。由于深度神经网络可以有效地表示多个层次的图像,因此这种分层表示已成功用于各种计算机视觉任务,例如 * 对象检测 *、* 语义分段 * 和 * 样式转移 *。遵循在计算机视觉中利用分层表示的关键思想,我们将从物体检测的主要组件和技术开始。接下来,我们将展示如何使用 * 完全卷积网络 * 对图像进行语义分割。然后我们将解释如何使用风格传递技术来生成像本书封面一样的图像。最后,我们在结束本章时将本章和前几章的材料应用于两个流行的计算机视觉基准数据集。 + +```toc +:maxdepth: 2 + +image-augmentation +fine-tuning +bounding-box +anchor +multiscale-object-detection +object-detection-dataset +ssd +rcnn +semantic-segmentation-and-dataset +transposed-conv +fcn +neural-style +kaggle-cifar10 +kaggle-dog +``` diff --git a/chapter_computer-vision/index_origin.md b/chapter_computer-vision/index_origin.md new file mode 100644 index 000000000..1eddc4893 --- /dev/null +++ b/chapter_computer-vision/index_origin.md @@ -0,0 +1,44 @@ +# Computer Vision +:label:`chap_cv` + +Whether it is medical diagnosis, self-driving vehicles, camera monitoring, or smart filters, many applications in the field of computer vision are closely related to our current and future lives. +In recent years, deep learning has been +the transformative power for advancing the performance of computer vision systems. +It can be said that the most advanced computer vision applications are almost inseparable from deep learning. +In view of this, this chapter will focus on the field of computer vision, and investigate methods and applications that have recently been influential in academia and industry. + + +In :numref:`chap_cnn` and :numref:`chap_modern_cnn`, we studied various convolutional neural networks that are +commonly used in computer vision, and applied them +to simple image classification tasks. +At the beginning of this chapter, we will describe +two methods that +may improve model generalization, namely *image augmentation* and *fine-tuning*, +and apply them to image classification. +Since deep neural networks can effectively represent images in multiple levels, +such layerwise representations have been successfully +used in various computer vision tasks such as *object detection*, *semantic segmentation*, and *style transfer*. +Following the key idea of leveraging layerwise representations in computer vision, +we will begin with major components and techniques for object detection. Next, we will show how to use *fully convolutional networks* for semantic segmentation of images. Then we will explain how to use style transfer techniques to generate images like the cover of this book. +In the end, we conclude this chapter +by applying the materials of this chapter and several previous chapters on two popular computer vision benchmark datasets. + +```toc +:maxdepth: 2 + +image-augmentation +fine-tuning +bounding-box +anchor +multiscale-object-detection +object-detection-dataset +ssd +rcnn +semantic-segmentation-and-dataset +transposed-conv +fcn +neural-style +kaggle-cifar10 +kaggle-dog +``` + diff --git a/img/finetune.svg b/img/finetune.svg index 387776a63..ad26f66e1 100644 --- a/img/finetune.svg +++ b/img/finetune.svg @@ -1,5 +1,5 @@ - + @@ -36,55 +36,55 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -110,496 +110,503 @@ - + + + + + + + + + + + + + + + + - - + - + - - - + - + - + + - + + - + - + - - + + - + + - - + + - - + - + - - + + - + + - - + - - + - + + - + + - - + - + - - + - + - + - + - + - + - + - + + - + + - - + - + - - + + - + - - + - + - + - + - + - + - + + - + - + - + - + - - + + - - + + - + - + - + - + - - + + - - + + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - - - - - - - - - - - - - + + + + + + + + + + + + - - + + - + - + - - - + + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - - + + - - + + - + - - - + + + - + - - + + - + - + - + - + - + - - + + - + - + - + - - + + - + - + - + - + - + - - - - - - - - - - - + + + + + + + + + + - - + + - + - + - - + - + - + - + - + - - + + - + - + - + - - + - + - - + + - + - + - + - + - - + + - + - - - - + + + + - + - - + + - - + - - - + + + - - + - - + + - + - + From e558594cf1f5a8294512c45508c204fd58ea22d5 Mon Sep 17 00:00:00 2001 From: Aston Zhang Date: Wed, 5 May 2021 03:09:32 +0000 Subject: [PATCH 077/103] add ch13.13 13.14 --- chapter_computer-vision/kaggle-cifar10.md | 512 +++++++++++++++ .../kaggle-cifar10_origin.md | 588 ++++++++++++++++++ chapter_computer-vision/kaggle-dog.md | 435 +++++++++++++ chapter_computer-vision/kaggle-dog_origin.md | 518 +++++++++++++++ 4 files changed, 2053 insertions(+) create mode 100644 chapter_computer-vision/kaggle-cifar10.md create mode 100644 chapter_computer-vision/kaggle-cifar10_origin.md create mode 100644 chapter_computer-vision/kaggle-dog.md create mode 100644 chapter_computer-vision/kaggle-dog_origin.md diff --git a/chapter_computer-vision/kaggle-cifar10.md b/chapter_computer-vision/kaggle-cifar10.md new file mode 100644 index 000000000..6deb594d0 --- /dev/null +++ b/chapter_computer-vision/kaggle-cifar10.md @@ -0,0 +1,512 @@ +# Kaggle 上的图像分类 (CIFAR-10) +:label:`sec_kaggle_cifar10` + +到目前为止,我们一直在使用深度学习框架的高级 API 直接获取 Tensor 格式的图像数据集。但是,自定义图像数据集通常以图像文件的形式出现。在本节中,我们将从原始图像文件开始,然后逐步整理、阅读,然后将它们转换为张量格式。 + +我们在 :numref:`sec_image_augmentation` 中尝试了 CIFAR-10 数据集,这是计算机视觉中的重要数据集。在本节中,我们将运用我们在前几节中学到的知识来练习 CIFAR-10 图像分类的 Kaggle 竞赛。比赛的网址是 https://www.kaggle.com/c/cifar-10 + +:numref:`fig_kaggle_cifar10` 在竞争对手的网页上显示了信息。为了提交结果,您需要注册 Kaggle 账户。 + +![CIFAR-10 image classification competition webpage information. The competition dataset can be obtained by clicking the "Data" tab.](../img/kaggle-cifar10.png) +:width:`600px` +:label:`fig_kaggle_cifar10` + +```{.python .input} +import collections +from d2l import mxnet as d2l +import math +from mxnet import gluon, init, npx +from mxnet.gluon import nn +import os +import pandas as pd +import shutil + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +import collections +from d2l import torch as d2l +import math +import torch +import torchvision +from torch import nn +import os +import pandas as pd +import shutil +``` + +## 获取和组织数据集 + +比赛数据集分为训练集和测试集,分别包含 50000 张和 300000 张图像。在测试集中,10000 张图像将用于评估,而剩下的 290000 张图像将不会进行评估:包含它们只是为了使其难以作弊 +*手动 * 标记测试集的结果。 +此数据集中的图像都是 png 颜色(RGB 通道)图像文件,其高度和宽度均为 32 像素。这些图片共涵盖 10 个类别,即飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。:numref:`fig_kaggle_cifar10` 的左上角显示了数据集中飞机、汽车和鸟类的一些图像。 + +### 下载数据集 + +登录 Kaggle 后,我们可以点击 :numref:`fig_kaggle_cifar10` 中显示的 CIFAR-10 图像分类竞赛网页上的 “数据” 选项卡,然后单击 “全部下载” 按钮下载数据集。在 `../data` 中解压下载的文件并在其中解压缩 `train.7z` 和 `test.7z` 后,您将在以下路径中找到整个数据集: + +* `../data/cifar-10/train/[1-50000].png` +* `../data/cifar-10/test/[1-300000].png` +* `../data/cifar-10/trainLabels.csv` +* `../data/cifar-10/sampleSubmission.csv` + +`train` 和 `test` 目录分别包含训练和测试图像,`trainLabels.csv` 为训练图像提供标签,`sample_submission.csv` 是示例提交文件。 + +为了便于入门,我们提供了包含前 1000 个训练图像和 5 个随机测试图像的数据集的小规模样本。要使用 Kaggle 竞争的完整数据集,您需要将以下 `demo` 变量设置为 `False`。 + +```{.python .input} +#@tab all +#@save +d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', + '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') + +# If you use the full dataset downloaded for the Kaggle competition, set +# `demo` to False +demo = True + +if demo: + data_dir = d2l.download_extract('cifar10_tiny') +else: + data_dir = '../data/cifar-10/' +``` + +### 组织数据集 + +我们需要组织数据集来促进模型训练和测试。让我们首先阅读 csv 文件中的标签。以下函数返回一个字典,该字典将文件名的非扩展名部分映射到其标签。 + +```{.python .input} +#@tab all +#@save +def read_csv_labels(fname): + """Read `fname` to return a filename to label dictionary.""" + with open(fname, 'r') as f: + # Skip the file header line (column name) + lines = f.readlines()[1:] + tokens = [l.rstrip().split(',') for l in lines] + return dict(((name, label) for name, label in tokens)) + +labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv')) +print('# training examples:', len(labels)) +print('# classes:', len(set(labels.values()))) +``` + +接下来,我们定义 `reorg_train_valid` 函数来将验证设置从原始训练集中拆分。此函数中的参数 `valid_ratio` 是验证集中的示例数与原始训练集中示例数的比率。更具体地说,让 $n$ 作为实例最少的课堂图像数量,而 $r$ 是比率。验证集将为每个课程拆分 $\max(\lfloor nr\rfloor,1)$ 张图像。让我们以 `valid_ratio=0.1` 为例。由于最初的训练套装有 50000 张图像,因此 `train_valid_test/train` 路径中将有 45000 张图像用于训练,而其他 5000 张图像将作为路径 `train_valid_test/valid` 中设置的验证进行拆分。组织数据集后,同类的图像将被放置在同一文件夹下。 + +```{.python .input} +#@tab all +#@save +def copyfile(filename, target_dir): + """Copy a file into a target directory.""" + os.makedirs(target_dir, exist_ok=True) + shutil.copy(filename, target_dir) + +#@save +def reorg_train_valid(data_dir, labels, valid_ratio): + # The number of examples of the class that has the fewest examples in the + # training dataset + n = collections.Counter(labels.values()).most_common()[-1][1] + # The number of examples per class for the validation set + n_valid_per_label = max(1, math.floor(n * valid_ratio)) + label_count = {} + for train_file in os.listdir(os.path.join(data_dir, 'train')): + label = labels[train_file.split('.')[0]] + fname = os.path.join(data_dir, 'train', train_file) + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train_valid', label)) + if label not in label_count or label_count[label] < n_valid_per_label: + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'valid', label)) + label_count[label] = label_count.get(label, 0) + 1 + else: + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train', label)) + return n_valid_per_label +``` + +下面的 `reorg_test` 函数组织了预测期间数据加载的测试集。 + +```{.python .input} +#@tab all +#@save +def reorg_test(data_dir): + for test_file in os.listdir(os.path.join(data_dir, 'test')): + copyfile(os.path.join(data_dir, 'test', test_file), + os.path.join(data_dir, 'train_valid_test', 'test', + 'unknown')) +``` + +最后,我们使用一个函数来调用上面定义的 `read_csv_labels`、`reorg_train_valid` 和 `reorg_test` 函数。 + +```{.python .input} +#@tab all +def reorg_cifar10_data(data_dir, valid_ratio): + labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv')) + reorg_train_valid(data_dir, labels, valid_ratio) + reorg_test(data_dir) +``` + +在这里,我们只将数据集的小规模样本的批量大小设置为 4。在训练和测试 Kaggle 比赛的完整数据集时,应将 `batch_size` 设置为更大的整数,例如 128。我们将 10% 的训练示例作为调整超参数的验证集。 + +```{.python .input} +#@tab all +batch_size = 4 if demo else 128 +valid_ratio = 0.1 +reorg_cifar10_data(data_dir, valid_ratio) +``` + +## 图像增强 + +我们使用图像增强来解决过度适合问题。例如,在训练期间,可以随机水平翻转图像。我们还可以对彩色图像的三个 RGB 通道执行标准化。下面列出了您可以调整的其中一些操作。 + +```{.python .input} +transform_train = gluon.data.vision.transforms.Compose([ + # Scale the image up to a square of 40 pixels in both height and width + gluon.data.vision.transforms.Resize(40), + # Randomly crop a square image of 40 pixels in both height and width to + # produce a small square of 0.64 to 1 times the area of the original + # image, and then scale it to a square of 32 pixels in both height and + # width + gluon.data.vision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), + ratio=(1.0, 1.0)), + gluon.data.vision.transforms.RandomFlipLeftRight(), + gluon.data.vision.transforms.ToTensor(), + # Standardize each channel of the image + gluon.data.vision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +```{.python .input} +#@tab pytorch +transform_train = torchvision.transforms.Compose([ + # Scale the image up to a square of 40 pixels in both height and width + torchvision.transforms.Resize(40), + # Randomly crop a square image of 40 pixels in both height and width to + # produce a small square of 0.64 to 1 times the area of the original + # image, and then scale it to a square of 32 pixels in both height and + # width + torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), + ratio=(1.0, 1.0)), + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor(), + # Standardize each channel of the image + torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +在测试期间,我们只对图像执行标准化,以消除评估结果中的随机性。 + +```{.python .input} +transform_test = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.ToTensor(), + gluon.data.vision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +```{.python .input} +#@tab pytorch +transform_test = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +## 阅读数据集 + +接下来,我们阅读由原始图像文件组成的组织数据集。每个示例都包括一张图片和一个标签。 + +```{.python .input} +train_ds, valid_ds, train_valid_ds, test_ds = [ + gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'train_valid_test', folder)) + for folder in ['train', 'valid', 'train_valid', 'test']] +``` + +```{.python .input} +#@tab pytorch +train_ds, train_valid_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_train) for folder in ['train', 'train_valid']] + +valid_ds, test_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_test) for folder in ['valid', 'test']] +``` + +在训练期间,我们需要指定上面定义的所有图像增强操作。当验证集在超参数调整过程中用于模型评估时,不应引入图像增强的随机性。在最终预测之前,我们根据组合训练集和验证集训练模型进行训练,以充分利用所有标记的数据。 + +```{.python .input} +train_iter, train_valid_iter = [gluon.data.DataLoader( + dataset.transform_first(transform_train), batch_size, shuffle=True, + last_batch='discard') for dataset in (train_ds, train_valid_ds)] + +valid_iter = gluon.data.DataLoader( + valid_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='discard') + +test_iter = gluon.data.DataLoader( + test_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='keep') +``` + +```{.python .input} +#@tab pytorch +train_iter, train_valid_iter = [torch.utils.data.DataLoader( + dataset, batch_size, shuffle=True, drop_last=True) + for dataset in (train_ds, train_valid_ds)] + +valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False, + drop_last=True) + +test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False, + drop_last=False) +``` + +## 定义模型 + +:begin_tab:`mxnet` +在这里,我们基于 `HybridBlock` 类构建剩余块,这与 :numref:`sec_resnet` 中描述的实现略有不同。这是为了提高计算效率。 +:end_tab: + +```{.python .input} +class Residual(nn.HybridBlock): + def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): + super(Residual, self).__init__(**kwargs) + self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, + strides=strides) + self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1) + if use_1x1conv: + self.conv3 = nn.Conv2D(num_channels, kernel_size=1, + strides=strides) + else: + self.conv3 = None + self.bn1 = nn.BatchNorm() + self.bn2 = nn.BatchNorm() + + def hybrid_forward(self, F, X): + Y = F.npx.relu(self.bn1(self.conv1(X))) + Y = self.bn2(self.conv2(Y)) + if self.conv3: + X = self.conv3(X) + return F.npx.relu(Y + X) +``` + +:begin_tab:`mxnet` +接下来,我们定义 Resnet-18 模型。 +:end_tab: + +```{.python .input} +def resnet18(num_classes): + net = nn.HybridSequential() + net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), + nn.BatchNorm(), nn.Activation('relu')) + + def resnet_block(num_channels, num_residuals, first_block=False): + blk = nn.HybridSequential() + for i in range(num_residuals): + if i == 0 and not first_block: + blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) + else: + blk.add(Residual(num_channels)) + return blk + + net.add(resnet_block(64, 2, first_block=True), + resnet_block(128, 2), + resnet_block(256, 2), + resnet_block(512, 2)) + net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) + return net +``` + +:begin_tab:`mxnet` +我们在训练开始之前使用 :numref:`subsec_xavier` 中描述的 Xavier 初始化。 +:end_tab: + +:begin_tab:`pytorch` +我们定义了 :numref:`sec_resnet` 中描述的 Resnet-18 模型。 +:end_tab: + +```{.python .input} +def get_net(devices): + num_classes = 10 + net = resnet18(num_classes) + net.initialize(ctx=devices, init=init.Xavier()) + return net + +loss = gluon.loss.SoftmaxCrossEntropyLoss() +``` + +```{.python .input} +#@tab pytorch +def get_net(): + num_classes = 10 + net = d2l.resnet18(num_classes, 3) + return net + +loss = nn.CrossEntropyLoss(reduction="none") +``` + +## 定义训练功能 + +我们将根据模型在验证集上的性能选择模型并调整超参数。在下面,我们定义了模型训练函数 `train`。 + +```{.python .input} +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + trainer = gluon.Trainer(net.collect_params(), 'sgd', + {'learning_rate': lr, 'momentum': 0.9, 'wd': wd}) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'train acc', 'valid acc']) + for epoch in range(num_epochs): + metric = d2l.Accumulator(3) + if epoch > 0 and epoch % lr_period == 0: + trainer.set_learning_rate(trainer.learning_rate * lr_decay) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = d2l.train_batch_ch13( + net, features, labels.astype('float32'), loss, trainer, + devices, d2l.split_batch) + metric.add(l, acc, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[2], + None)) + if valid_iter is not None: + valid_acc = d2l.evaluate_accuracy_gpus(net, valid_iter, + d2l.split_batch) + animator.add(epoch + 1, (None, None, valid_acc)) + if valid_iter is not None: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}, ' + f'valid acc {valid_acc:.3f}') + else: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +```{.python .input} +#@tab pytorch +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, + weight_decay=wd) + scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'train acc', 'valid acc']) + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + for epoch in range(num_epochs): + net.train() + metric = d2l.Accumulator(3) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = d2l.train_batch_ch13(net, features, labels, + loss, trainer, devices) + metric.add(l, acc, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[2], + None)) + if valid_iter is not None: + valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter) + animator.add(epoch + 1, (None, None, valid_acc)) + scheduler.step() + if valid_iter is not None: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}, ' + f'valid acc {valid_acc:.3f}') + else: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +## 培训和验证模型 + +现在,我们可以训练和验证模型。以下所有超参数都可以调整。例如,我们可以增加纪元的数量。当 `lr_period` 和 `lr_decay` 分别设置为 50 和 0.1 时,优化算法的学习速率将在每 50 个纪元后乘以 0.1。只是为了示范,我们在这里只训练一个时代。 + +```{.python .input} +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.1, 5e-4 +lr_period, lr_decay, net = 50, 0.1, get_net(devices) +net.hybridize() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +```{.python .input} +#@tab pytorch +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.1, 5e-4 +lr_period, lr_decay, net = 50, 0.1, get_net() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +## 在 Kaggle 上对测试集进行分类并提交结果 + +在获得具有超参数的有前途的模型后,我们使用所有标记的数据(包括验证集)来重新训练模型并对测试集进行分类。 + +```{.python .input} +net, preds = get_net(devices), [] +net.hybridize() +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +for X, _ in test_iter: + y_hat = net(X.as_in_ctx(devices[0])) + preds.extend(y_hat.argmax(axis=1).astype(int).asnumpy()) +sorted_ids = list(range(1, len(test_ds) + 1)) +sorted_ids.sort(key=lambda x: str(x)) +df = pd.DataFrame({'id': sorted_ids, 'label': preds}) +df['label'] = df['label'].apply(lambda x: train_valid_ds.synsets[x]) +df.to_csv('submission.csv', index=False) +``` + +```{.python .input} +#@tab pytorch +net, preds = get_net(), [] +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +for X, _ in test_iter: + y_hat = net(X.to(devices[0])) + preds.extend(y_hat.argmax(dim=1).type(torch.int32).cpu().numpy()) +sorted_ids = list(range(1, len(test_ds) + 1)) +sorted_ids.sort(key=lambda x: str(x)) +df = pd.DataFrame({'id': sorted_ids, 'label': preds}) +df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x]) +df.to_csv('submission.csv', index=False) +``` + +上面的代码将生成一个 `submission.csv` 文件,其格式符合 Kaggle 竞争的要求。向 Kaggle 提交结果的方法与 :numref:`sec_kaggle_house` 中的方法类似。 + +## 摘要 + +* 将包含原始图像文件的数据集组织为所需格式后,我们可以读取它们。 + +:begin_tab:`mxnet` +* 我们可以在图像分类竞赛中使用卷积神经网络、图像增强和混合编程。 +:end_tab: + +:begin_tab:`pytorch` +* 我们可以在图像分类竞赛中使用卷积神经网络和图像增强。 +:end_tab: + +## 练习 + +1. 在这场 Kaggle 比赛中使用完整的 CIFAR-10 数据集。将 `batch_size` 和时代数分别更改为 128 和 100。看看你在这场比赛中能达到什么准确度和排名。你能进一步改进它们吗? +1. 不使用图像增强时,您能获得什么准确性? + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/379) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1479) +:end_tab: diff --git a/chapter_computer-vision/kaggle-cifar10_origin.md b/chapter_computer-vision/kaggle-cifar10_origin.md new file mode 100644 index 000000000..aaf3bd73c --- /dev/null +++ b/chapter_computer-vision/kaggle-cifar10_origin.md @@ -0,0 +1,588 @@ +# Image Classification (CIFAR-10) on Kaggle +:label:`sec_kaggle_cifar10` + +So far, we have been using high-level APIs of deep learning frameworks to directly obtain image datasets in tensor format. +However, custom image datasets +often come in the form of image files. +In this section, we will start from +raw image files, +and organize, read, then transform them +into tensor format step by step. + +We experimented with the CIFAR-10 dataset in :numref:`sec_image_augmentation`, +which is an important dataset in computer vision. +In this section, +we will apply the knowledge we learned +in previous sections +to practice the Kaggle competition of +CIFAR-10 image classification. +The web address of the competition is https://www.kaggle.com/c/cifar-10 + +:numref:`fig_kaggle_cifar10` shows the information on the competition's webpage. +In order to submit the results, +you need to register a Kaggle account. + +![CIFAR-10 image classification competition webpage information. The competition dataset can be obtained by clicking the "Data" tab.](../img/kaggle-cifar10.png) +:width:`600px` +:label:`fig_kaggle_cifar10` + +```{.python .input} +import collections +from d2l import mxnet as d2l +import math +from mxnet import gluon, init, npx +from mxnet.gluon import nn +import os +import pandas as pd +import shutil + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +import collections +from d2l import torch as d2l +import math +import torch +import torchvision +from torch import nn +import os +import pandas as pd +import shutil +``` + +## Obtaining and Organizing the Dataset + +The competition dataset is divided into +a training set and a test set, +which contain 50000 and 300000 images, respectively. +In the test set, +10000 images will be used for evaluation, +while the remaining 290000 images will not +be evaluated: +they are included just +to make it hard +to cheat with +*manually* labeled results of the test set. +The images in this dataset +are all png color (RGB channels) image files, +whose height and width are both 32 pixels. +The images cover a total of 10 categories, namely airplanes, cars, birds, cats, deer, dogs, frogs, horses, boats, and trucks. +The upper left corner of :numref:`fig_kaggle_cifar10` shows some images of airplanes, cars, and birds in the dataset. + + +### Downloading the Dataset + +After logging in to Kaggle, we can click the "Data" tab on the CIFAR-10 image classification competition webpage shown in :numref:`fig_kaggle_cifar10` and download the dataset by clicking the "Download All" button. +After unzipping the downloaded file in `../data`, and unzipping `train.7z` and `test.7z` inside it, you will find the entire dataset in the following paths: + +* `../data/cifar-10/train/[1-50000].png` +* `../data/cifar-10/test/[1-300000].png` +* `../data/cifar-10/trainLabels.csv` +* `../data/cifar-10/sampleSubmission.csv` + +where the `train` and `test` directories contain the training and testing images, respectively, `trainLabels.csv` provides labels for the training images, and `sample_submission.csv` is a sample submission file. + +To make it easier to get started, we provide a small-scale sample of the dataset that +contains the first 1000 training images and 5 random testing images. +To use the full dataset of the Kaggle competition, you need to set the following `demo` variable to `False`. + +```{.python .input} +#@tab all +#@save +d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', + '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') + +# If you use the full dataset downloaded for the Kaggle competition, set +# `demo` to False +demo = True + +if demo: + data_dir = d2l.download_extract('cifar10_tiny') +else: + data_dir = '../data/cifar-10/' +``` + +### Organizing the Dataset + +We need to organize datasets to facilitate model training and testing. +Let us first read the labels from the csv file. +The following function returns a dictionary that maps +the non-extension part of the filename to its label. + +```{.python .input} +#@tab all +#@save +def read_csv_labels(fname): + """Read `fname` to return a filename to label dictionary.""" + with open(fname, 'r') as f: + # Skip the file header line (column name) + lines = f.readlines()[1:] + tokens = [l.rstrip().split(',') for l in lines] + return dict(((name, label) for name, label in tokens)) + +labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv')) +print('# training examples:', len(labels)) +print('# classes:', len(set(labels.values()))) +``` + +Next, we define the `reorg_train_valid` function to split the validation set out of the original training set. +The argument `valid_ratio` in this function is the ratio of the number of examples in the validation set to the number of examples in the original training set. +More concretely, +let $n$ be the number of images of the class with the least examples, and $r$ be the ratio. +The validation set will split out +$\max(\lfloor nr\rfloor,1)$ images for each class. +Let us use `valid_ratio=0.1` as an example. Since the original training set has 50000 images, +there will be 45000 images used for training in the path `train_valid_test/train`, +while the other 5000 images will be split out +as validation set in the path `train_valid_test/valid`. After organizing the dataset, images of the same class will be placed under the same folder. + +```{.python .input} +#@tab all +#@save +def copyfile(filename, target_dir): + """Copy a file into a target directory.""" + os.makedirs(target_dir, exist_ok=True) + shutil.copy(filename, target_dir) + +#@save +def reorg_train_valid(data_dir, labels, valid_ratio): + # The number of examples of the class that has the fewest examples in the + # training dataset + n = collections.Counter(labels.values()).most_common()[-1][1] + # The number of examples per class for the validation set + n_valid_per_label = max(1, math.floor(n * valid_ratio)) + label_count = {} + for train_file in os.listdir(os.path.join(data_dir, 'train')): + label = labels[train_file.split('.')[0]] + fname = os.path.join(data_dir, 'train', train_file) + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train_valid', label)) + if label not in label_count or label_count[label] < n_valid_per_label: + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'valid', label)) + label_count[label] = label_count.get(label, 0) + 1 + else: + copyfile(fname, os.path.join(data_dir, 'train_valid_test', + 'train', label)) + return n_valid_per_label +``` + +The `reorg_test` function below organizes the testing set for data loading during prediction. + +```{.python .input} +#@tab all +#@save +def reorg_test(data_dir): + for test_file in os.listdir(os.path.join(data_dir, 'test')): + copyfile(os.path.join(data_dir, 'test', test_file), + os.path.join(data_dir, 'train_valid_test', 'test', + 'unknown')) +``` + +Finally, we use a function to invoke +the `read_csv_labels`, `reorg_train_valid`, and `reorg_test` functions defined above. + +```{.python .input} +#@tab all +def reorg_cifar10_data(data_dir, valid_ratio): + labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv')) + reorg_train_valid(data_dir, labels, valid_ratio) + reorg_test(data_dir) +``` + +Here we only set the batch size to 4 for the small-scale sample of the dataset. +When training and testing +the complete dataset of the Kaggle competition, +`batch_size` should be set to a larger integer, such as 128. +We split out 10% of the training examples as the validation set for tuning hyperparameters. + +```{.python .input} +#@tab all +batch_size = 4 if demo else 128 +valid_ratio = 0.1 +reorg_cifar10_data(data_dir, valid_ratio) +``` + +## Image Augmentation + +We use image augmentation to address overfitting. +For example, images can be flipped horizontally at random during training. +We can also perform standardization for the three RGB channels of color images. Below lists some of these operations that you can tweak. + +```{.python .input} +transform_train = gluon.data.vision.transforms.Compose([ + # Scale the image up to a square of 40 pixels in both height and width + gluon.data.vision.transforms.Resize(40), + # Randomly crop a square image of 40 pixels in both height and width to + # produce a small square of 0.64 to 1 times the area of the original + # image, and then scale it to a square of 32 pixels in both height and + # width + gluon.data.vision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), + ratio=(1.0, 1.0)), + gluon.data.vision.transforms.RandomFlipLeftRight(), + gluon.data.vision.transforms.ToTensor(), + # Standardize each channel of the image + gluon.data.vision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +```{.python .input} +#@tab pytorch +transform_train = torchvision.transforms.Compose([ + # Scale the image up to a square of 40 pixels in both height and width + torchvision.transforms.Resize(40), + # Randomly crop a square image of 40 pixels in both height and width to + # produce a small square of 0.64 to 1 times the area of the original + # image, and then scale it to a square of 32 pixels in both height and + # width + torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), + ratio=(1.0, 1.0)), + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor(), + # Standardize each channel of the image + torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +During testing, +we only perform standardization on images +so as to +remove randomness in the evaluation results. + +```{.python .input} +transform_test = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.ToTensor(), + gluon.data.vision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +```{.python .input} +#@tab pytorch +transform_test = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], + [0.2023, 0.1994, 0.2010])]) +``` + +## Reading the Dataset + +Next, we read the organized dataset consisting of raw image files. Each example includes an image and a label. + +```{.python .input} +train_ds, valid_ds, train_valid_ds, test_ds = [ + gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'train_valid_test', folder)) + for folder in ['train', 'valid', 'train_valid', 'test']] +``` + +```{.python .input} +#@tab pytorch +train_ds, train_valid_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_train) for folder in ['train', 'train_valid']] + +valid_ds, test_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_test) for folder in ['valid', 'test']] +``` + +During training, +we need to specify all the image augmentation operations defined above. +When the validation set +is used for model evaluation during hyperparameter tuning, +no randomness from image augmentation should be introduced. +Before final prediction, +we train the model on the combined training set and validation set to make full use of all the labeled data. + +```{.python .input} +train_iter, train_valid_iter = [gluon.data.DataLoader( + dataset.transform_first(transform_train), batch_size, shuffle=True, + last_batch='discard') for dataset in (train_ds, train_valid_ds)] + +valid_iter = gluon.data.DataLoader( + valid_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='discard') + +test_iter = gluon.data.DataLoader( + test_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='keep') +``` + +```{.python .input} +#@tab pytorch +train_iter, train_valid_iter = [torch.utils.data.DataLoader( + dataset, batch_size, shuffle=True, drop_last=True) + for dataset in (train_ds, train_valid_ds)] + +valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False, + drop_last=True) + +test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False, + drop_last=False) +``` + +## Defining the Model + +:begin_tab:`mxnet` +Here, we build the residual blocks based on the `HybridBlock` class, which is +slightly different from the implementation described in +:numref:`sec_resnet`. +This is for improving computational efficiency. +:end_tab: + +```{.python .input} +class Residual(nn.HybridBlock): + def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): + super(Residual, self).__init__(**kwargs) + self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, + strides=strides) + self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1) + if use_1x1conv: + self.conv3 = nn.Conv2D(num_channels, kernel_size=1, + strides=strides) + else: + self.conv3 = None + self.bn1 = nn.BatchNorm() + self.bn2 = nn.BatchNorm() + + def hybrid_forward(self, F, X): + Y = F.npx.relu(self.bn1(self.conv1(X))) + Y = self.bn2(self.conv2(Y)) + if self.conv3: + X = self.conv3(X) + return F.npx.relu(Y + X) +``` + +:begin_tab:`mxnet` +Next, we define the ResNet-18 model. +:end_tab: + +```{.python .input} +def resnet18(num_classes): + net = nn.HybridSequential() + net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), + nn.BatchNorm(), nn.Activation('relu')) + + def resnet_block(num_channels, num_residuals, first_block=False): + blk = nn.HybridSequential() + for i in range(num_residuals): + if i == 0 and not first_block: + blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) + else: + blk.add(Residual(num_channels)) + return blk + + net.add(resnet_block(64, 2, first_block=True), + resnet_block(128, 2), + resnet_block(256, 2), + resnet_block(512, 2)) + net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) + return net +``` + +:begin_tab:`mxnet` +We use Xavier initialization described in :numref:`subsec_xavier` before training begins. +:end_tab: + +:begin_tab:`pytorch` +We define the ResNet-18 model described in +:numref:`sec_resnet`. +:end_tab: + +```{.python .input} +def get_net(devices): + num_classes = 10 + net = resnet18(num_classes) + net.initialize(ctx=devices, init=init.Xavier()) + return net + +loss = gluon.loss.SoftmaxCrossEntropyLoss() +``` + +```{.python .input} +#@tab pytorch +def get_net(): + num_classes = 10 + net = d2l.resnet18(num_classes, 3) + return net + +loss = nn.CrossEntropyLoss(reduction="none") +``` + +## Defining the Training Function + +We will select models and tune hyperparameters according to the model's performance on the validation set. +In the following, we define the model training function `train`. + +```{.python .input} +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + trainer = gluon.Trainer(net.collect_params(), 'sgd', + {'learning_rate': lr, 'momentum': 0.9, 'wd': wd}) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'train acc', 'valid acc']) + for epoch in range(num_epochs): + metric = d2l.Accumulator(3) + if epoch > 0 and epoch % lr_period == 0: + trainer.set_learning_rate(trainer.learning_rate * lr_decay) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = d2l.train_batch_ch13( + net, features, labels.astype('float32'), loss, trainer, + devices, d2l.split_batch) + metric.add(l, acc, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[2], + None)) + if valid_iter is not None: + valid_acc = d2l.evaluate_accuracy_gpus(net, valid_iter, + d2l.split_batch) + animator.add(epoch + 1, (None, None, valid_acc)) + if valid_iter is not None: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}, ' + f'valid acc {valid_acc:.3f}') + else: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +```{.python .input} +#@tab pytorch +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, + weight_decay=wd) + scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'train acc', 'valid acc']) + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + for epoch in range(num_epochs): + net.train() + metric = d2l.Accumulator(3) + for i, (features, labels) in enumerate(train_iter): + timer.start() + l, acc = d2l.train_batch_ch13(net, features, labels, + loss, trainer, devices) + metric.add(l, acc, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[2], metric[1] / metric[2], + None)) + if valid_iter is not None: + valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter) + animator.add(epoch + 1, (None, None, valid_acc)) + scheduler.step() + if valid_iter is not None: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}, ' + f'valid acc {valid_acc:.3f}') + else: + print(f'loss {metric[0] / metric[2]:.3f}, ' + f'train acc {metric[1] / metric[2]:.3f}') + print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +## Training and Validating the Model + +Now, we can train and validate the model. +All the following hyperparameters can be tuned. +For example, we can increase the number of epochs. +When `lr_period` and `lr_decay` are set to 50 and 0.1, respectively, the learning rate of the optimization algorithm will be multiplied by 0.1 after every 50 epochs. Just for demonstration, +we only train one epoch here. + +```{.python .input} +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.1, 5e-4 +lr_period, lr_decay, net = 50, 0.1, get_net(devices) +net.hybridize() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +```{.python .input} +#@tab pytorch +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.1, 5e-4 +lr_period, lr_decay, net = 50, 0.1, get_net() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +## Classifying the Testing Set and Submitting Results on Kaggle + +After obtaining a promising model with hyperparameters, +we use all the labeled data (including the validation set) to retrain the model and classify the testing set. + +```{.python .input} +net, preds = get_net(devices), [] +net.hybridize() +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +for X, _ in test_iter: + y_hat = net(X.as_in_ctx(devices[0])) + preds.extend(y_hat.argmax(axis=1).astype(int).asnumpy()) +sorted_ids = list(range(1, len(test_ds) + 1)) +sorted_ids.sort(key=lambda x: str(x)) +df = pd.DataFrame({'id': sorted_ids, 'label': preds}) +df['label'] = df['label'].apply(lambda x: train_valid_ds.synsets[x]) +df.to_csv('submission.csv', index=False) +``` + +```{.python .input} +#@tab pytorch +net, preds = get_net(), [] +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +for X, _ in test_iter: + y_hat = net(X.to(devices[0])) + preds.extend(y_hat.argmax(dim=1).type(torch.int32).cpu().numpy()) +sorted_ids = list(range(1, len(test_ds) + 1)) +sorted_ids.sort(key=lambda x: str(x)) +df = pd.DataFrame({'id': sorted_ids, 'label': preds}) +df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x]) +df.to_csv('submission.csv', index=False) +``` + +The above code +will generate a `submission.csv` file, +whose format +meets the requirement of the Kaggle competition. +The method +for submitting results to Kaggle +is similar to that in :numref:`sec_kaggle_house`. + +## Summary + +* We can read datasets containing raw image files after organizing them into the required format. +:begin_tab:`mxnet` +* We can use convolutional neural networks, image augmentation, and hybrid programing in an image classification competition. +:end_tab: +:begin_tab:`pytorch` +* We can use convolutional neural networks and image augmentation in an image classification competition. +:end_tab: + + +## Exercises + +1. Use the complete CIFAR-10 dataset for this Kaggle competition. Change the `batch_size` and number of epochs `num_epochs` to 128 and 100, respectively. See what accuracy and ranking you can achieve in this competition. Can you further improve them? +1. What accuracy can you get when not using image augmentation? + + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/379) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1479) +:end_tab: diff --git a/chapter_computer-vision/kaggle-dog.md b/chapter_computer-vision/kaggle-dog.md new file mode 100644 index 000000000..ef5c34fcd --- /dev/null +++ b/chapter_computer-vision/kaggle-dog.md @@ -0,0 +1,435 @@ +# Kaggle 上的狗品种识别(iMagenNet 狗) + +在本节中,我们将在 Kaggle 上练习狗品种识别问题。本次比赛的网址是 https://www.kaggle.com/c/dog-breed-identification + +在这场比赛中,120 种不同品种的狗将被认可。事实上,本次比赛的数据集是 iMagenet 数据集的子集。与 :numref:`sec_kaggle_cifar10` 中 CIFAR-10 数据集中的图像不同,iMagenet 数据集中的图像在不同维度上既更高也更宽。:numref:`fig_kaggle_dog` 显示了竞争对手网页上的信息。您需要一个 Kaggle 账户才能提交结果。 + +![The dog breed identification competition website. The competition dataset can be obtained by clicking the "Data" tab.](../img/kaggle-dog.jpg) +:width:`400px` +:label:`fig_kaggle_dog` + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import autograd, gluon, init, npx +from mxnet.gluon import nn +import os + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +import torchvision +from torch import nn +import os +``` + +## 获取和组织数据集 + +比赛数据集分为训练集和测试集,其中分别包含三个 RGB(彩色)通道的 10222 和 10357 张 JPEG 图像。在训练数据集中,有 120 种犬类,如拉布拉多犬、贵宾犬、腊肠犬、萨摩耶德、哈士奇、奇娃娃和约克郡梗。 + +### 下载数据集 + +登录 Kaggle 后,您可以点击 :numref:`fig_kaggle_dog` 中显示的竞争网页上的 “数据” 选项卡,然后点击 “全部下载” 按钮下载数据集。在 `../data` 中解压下载的文件后,您将在以下路径中找到整个数据集: + +* ../data/dog-breed-identification/labels.csv +* ../data/dog-breed-identification/sample_submission.csv +* ../数据/种身份识别/火车 +* ../数据/种身份识别/测试 + +你可能已经注意到,上述结构与 :numref:`sec_kaggle_cifar10` 的 CIFAR-10 竞争对手类似,其中文件夹 `train/` 和 `test/` 分别包含训练和测试狗图像,`labels.csv` 包含训练图像的标签。同样,为了便于入门,我们提供了上面提到的数据集的一小部分示例:`train_valid_test_tiny.zip`。如果您要在 Kaggle 比赛中使用完整的数据集,则需要将下面的 `demo` 变量更改为 `False`。 + +```{.python .input} +#@tab all +#@save +d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip', + '0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d') + +# If you use the full dataset downloaded for the Kaggle competition, change +# the variable below to `False` +demo = True +if demo: + data_dir = d2l.download_extract('dog_tiny') +else: + data_dir = os.path.join('..', 'data', 'dog-breed-identification') +``` + +### 组织数据集 + +我们可以像 :numref:`sec_kaggle_cifar10` 中所做的那样组织数据集,即从原始训练集中拆分验证集,然后将图像移动到按标签分组的子文件夹中。 + +下面的 `reorg_dog_data` 函数读取训练数据标签、拆分验证集并组织训练集。 + +```{.python .input} +#@tab all +def reorg_dog_data(data_dir, valid_ratio): + labels = d2l.read_csv_labels(os.path.join(data_dir, 'labels.csv')) + d2l.reorg_train_valid(data_dir, labels, valid_ratio) + d2l.reorg_test(data_dir) + + +batch_size = 4 if demo else 128 +valid_ratio = 0.1 +reorg_dog_data(data_dir, valid_ratio) +``` + +## 图像增强 + +回想一下,这个狗品种数据集是 iMagenet 数据集的子集,其图像大于 :numref:`sec_kaggle_cifar10` 中 CIFAR-10 数据集的图像。下面列出了一些对于相对较大的图像可能有用的图像增强操作。 + +```{.python .input} +transform_train = gluon.data.vision.transforms.Compose([ + # Randomly crop the image to obtain an image with an area of 0.08 to 1 of + # the original area and height-to-width ratio between 3/4 and 4/3. Then, + # scale the image to create a new 224 x 224 image + gluon.data.vision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), + ratio=(3.0/4.0, 4.0/3.0)), + gluon.data.vision.transforms.RandomFlipLeftRight(), + # Randomly change the brightness, contrast, and saturation + gluon.data.vision.transforms.RandomColorJitter(brightness=0.4, + contrast=0.4, + saturation=0.4), + # Add random noise + gluon.data.vision.transforms.RandomLighting(0.1), + gluon.data.vision.transforms.ToTensor(), + # Standardize each channel of the image + gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +```{.python .input} +#@tab pytorch +transform_train = torchvision.transforms.Compose([ + # Randomly crop the image to obtain an image with an area of 0.08 to 1 of + # the original area and height-to-width ratio between 3/4 and 4/3. Then, + # scale the image to create a new 224 x 224 image + torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), + ratio=(3.0/4.0, 4.0/3.0)), + torchvision.transforms.RandomHorizontalFlip(), + # Randomly change the brightness, contrast, and saturation + torchvision.transforms.ColorJitter(brightness=0.4, + contrast=0.4, + saturation=0.4), + # Add random noise + torchvision.transforms.ToTensor(), + # Standardize each channel of the image + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +在预测期间,我们只使用没有随机性的图像预处理操作。 + +```{.python .input} +transform_test = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.Resize(256), + # Crop a 224 x 224 square area from the center of the image + gluon.data.vision.transforms.CenterCrop(224), + gluon.data.vision.transforms.ToTensor(), + gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +```{.python .input} +#@tab pytorch +transform_test = torchvision.transforms.Compose([ + torchvision.transforms.Resize(256), + # Crop a 224 x 224 square area from the center of the image + torchvision.transforms.CenterCrop(224), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +## 阅读数据集 + +与 :numref:`sec_kaggle_cifar10` 一样,我们可以读取由原始图像文件组成的组织数据集。 + +```{.python .input} +train_ds, valid_ds, train_valid_ds, test_ds = [ + gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'train_valid_test', folder)) + for folder in ('train', 'valid', 'train_valid', 'test')] +``` + +```{.python .input} +#@tab pytorch +train_ds, train_valid_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_train) for folder in ['train', 'train_valid']] + +valid_ds, test_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_test) for folder in ['valid', 'test']] +``` + +下面我们创建数据加载器实例的方式与 :numref:`sec_kaggle_cifar10` 相同。 + +```{.python .input} +train_iter, train_valid_iter = [gluon.data.DataLoader( + dataset.transform_first(transform_train), batch_size, shuffle=True, + last_batch='discard') for dataset in (train_ds, train_valid_ds)] + +valid_iter = gluon.data.DataLoader( + valid_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='discard') + +test_iter = gluon.data.DataLoader( + test_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='keep') +``` + +```{.python .input} +#@tab pytorch +train_iter, train_valid_iter = [torch.utils.data.DataLoader( + dataset, batch_size, shuffle=True, drop_last=True) + for dataset in (train_ds, train_valid_ds)] + +valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False, + drop_last=True) + +test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False, + drop_last=False) +``` + +## 微调预训练模型 + +同样,本次比赛的数据集是 iMagenet 数据集的子集。因此,我们可以使用 :numref:`sec_fine_tuning` 中讨论的方法在完整 iMagenet 数据集上选择预训练的模型,然后使用该模型提取图像要素,以便将其输入到定制的小规模输出网络中。深度学习框架的高级 API 提供了在 iMagenet 数据集上预训练的各种模型。在这里,我们选择预训练的 Resnet-34 模型,我们只需重复使用此模型的输出层(即提取的要素)的输入。然后,我们可以用一个可以训练的小型自定义输出网络替换原始输出层,例如堆叠两个完全连接的图层。与 :numref:`sec_fine_tuning` 中的实验不同,以下内容不重新训练用于特征提取的预训练模型。这减少了存储渐变的训练时间和内存。 + +回想一下,我们使用三个 RGB 通道的均值和标准差来对完整的 iMagenet 数据集进行图像标准化。事实上,这也符合 iMagenet 上预训练模型的标准化操作。 + +```{.python .input} +def get_net(devices): + finetune_net = gluon.model_zoo.vision.resnet34_v2(pretrained=True) + # Define a new output network + finetune_net.output_new = nn.HybridSequential(prefix='') + finetune_net.output_new.add(nn.Dense(256, activation='relu')) + # There are 120 output categories + finetune_net.output_new.add(nn.Dense(120)) + # Initialize the output network + finetune_net.output_new.initialize(init.Xavier(), ctx=devices) + # Distribute the model parameters to the CPUs or GPUs used for computation + finetune_net.collect_params().reset_ctx(devices) + return finetune_net +``` + +```{.python .input} +#@tab pytorch +def get_net(devices): + finetune_net = nn.Sequential() + finetune_net.features = torchvision.models.resnet34(pretrained=True) + # Define a new output network (there are 120 output categories) + finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256), + nn.ReLU(), + nn.Linear(256, 120)) + # Move the model to devices + finetune_net = finetune_net.to(devices[0]) + # Freeze parameters of feature layers + for param in finetune_net.features.parameters(): + param.requires_grad = False + return finetune_net +``` + +在计算损失之前,我们首先获取预训练模型的输出层的输入,即提取的要素。然后我们使用此功能作为小型自定义输出网络的输入来计算损失。 + +```{.python .input} +loss = gluon.loss.SoftmaxCrossEntropyLoss() + +def evaluate_loss(data_iter, net, devices): + l_sum, n = 0.0, 0 + for features, labels in data_iter: + X_shards, y_shards = d2l.split_batch(features, labels, devices) + output_features = [net.features(X_shard) for X_shard in X_shards] + outputs = [net.output_new(feature) for feature in output_features] + ls = [loss(output, y_shard).sum() for output, y_shard + in zip(outputs, y_shards)] + l_sum += sum([float(l.sum()) for l in ls]) + n += labels.size + return l_sum / n +``` + +```{.python .input} +#@tab pytorch +loss = nn.CrossEntropyLoss(reduction='none') + +def evaluate_loss(data_iter, net, devices): + l_sum, n = 0.0, 0 + for features, labels in data_iter: + features, labels = features.to(devices[0]), labels.to(devices[0]) + outputs = net(features) + l = loss(outputs, labels) + l_sum = l.sum() + n += labels.numel() + return l_sum / n +``` + +## 定义训练功能 + +我们将根据模型在验证集上的性能选择模型并调整超参数。模型训练功能 `train` 只迭代小型自定义输出网络的参数。 + +```{.python .input} +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + # Only train the small custom output network + trainer = gluon.Trainer(net.output_new.collect_params(), 'sgd', + {'learning_rate': lr, 'momentum': 0.9, 'wd': wd}) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'valid loss']) + for epoch in range(num_epochs): + metric = d2l.Accumulator(2) + if epoch > 0 and epoch % lr_period == 0: + trainer.set_learning_rate(trainer.learning_rate * lr_decay) + for i, (features, labels) in enumerate(train_iter): + timer.start() + X_shards, y_shards = d2l.split_batch(features, labels, devices) + output_features = [net.features(X_shard) for X_shard in X_shards] + with autograd.record(): + outputs = [net.output_new(feature) + for feature in output_features] + ls = [loss(output, y_shard).sum() for output, y_shard + in zip(outputs, y_shards)] + for l in ls: + l.backward() + trainer.step(batch_size) + metric.add(sum([float(l.sum()) for l in ls]), labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[1], None)) + if valid_iter is not None: + valid_loss = evaluate_loss(valid_iter, net, devices) + animator.add(epoch + 1, (None, valid_loss)) + if valid_iter is not None: + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'valid loss {valid_loss:.3f}') + else: + print(f'train loss {metric[0] / metric[1]:.3f}') + print(f'{metric[1] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +```{.python .input} +#@tab pytorch +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + # Only train the small custom output network + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + trainer = torch.optim.SGD((param for param in net.parameters() + if param.requires_grad), lr=lr, + momentum=0.9, weight_decay=wd) + scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'valid loss']) + for epoch in range(num_epochs): + metric = d2l.Accumulator(2) + for i, (features, labels) in enumerate(train_iter): + timer.start() + features, labels = features.to(devices[0]), labels.to(devices[0]) + trainer.zero_grad() + output = net(features) + l = loss(output, labels).sum() + l.backward() + trainer.step() + metric.add(l, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[1], None)) + if valid_iter is not None: + valid_loss = evaluate_loss(valid_iter, net, devices) + animator.add(epoch + 1, (None, valid_loss)) + scheduler.step() + if valid_iter is not None: + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'valid loss {valid_loss:.3f}') + else: + print(f'train loss {metric[0] / metric[1]:.3f}') + print(f'{metric[1] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +## 培训和验证模型 + +现在我们可以训练和验证模型了。以下超参数都是可调的。例如,可以增加纪元的数量。由于 `lr_period` 和 `lr_decay` 分别设置为 10 和 0.1,因此优化算法的学习速率将在每 10 个纪元后乘以 0.1。 + +```{.python .input} +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.01, 1e-4 +lr_period, lr_decay, net = 10, 0.1, get_net(devices) +net.hybridize() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +```{.python .input} +#@tab pytorch +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.001, 1e-4 +lr_period, lr_decay, net = 10, 0.1, get_net(devices) +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +## 在 Kaggle 上对测试集进行分类并提交结果 + +与 :numref:`sec_kaggle_cifar10` 中的最后一步类似,最终所有标记的数据(包括验证集)都用于训练模型和对测试集进行分类。我们将使用训练有素的自定义输出网络进行分类。 + +```{.python .input} +net = get_net(devices) +net.hybridize() +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +preds = [] +for data, label in test_iter: + output_features = net.features(data.as_in_ctx(devices[0])) + output = npx.softmax(net.output_new(output_features)) + preds.extend(output.asnumpy()) +ids = sorted(os.listdir( + os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))) +with open('submission.csv', 'w') as f: + f.write('id,' + ','.join(train_valid_ds.synsets) + '\n') + for i, output in zip(ids, preds): + f.write(i.split('.')[0] + ',' + ','.join( + [str(num) for num in output]) + '\n') +``` + +```{.python .input} +#@tab pytorch +net = get_net(devices) +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +preds = [] +for data, label in test_iter: + output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=0) + preds.extend(output.cpu().detach().numpy()) +ids = sorted(os.listdir( + os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))) +with open('submission.csv', 'w') as f: + f.write('id,' + ','.join(train_valid_ds.classes) + '\n') + for i, output in zip(ids, preds): + f.write(i.split('.')[0] + ',' + ','.join( + [str(num) for num in output]) + '\n') +``` + +上面的代码将生成一个 `submission.csv` 文件,以 :numref:`sec_kaggle_house` 中描述的方式提交给 Kaggle。 + +## 摘要 + +* iMagenet 数据集中的图像比 CIFAR-10 图像大(尺寸不同)。我们可能会修改不同数据集上任务的图像增强操作。 +* 要对 iMagenet 数据集的子集进行分类,我们可以利用完整 iMagenet 数据集上的预训练模型来提取要素并仅训练自定义的小规模输出网络。这将减少计算时间和内存成本。 + +## 练习 + +1. 使用填充 Kaggle 竞争数据集时,当您增加 `batch_size`(批量大小)和 `num_epochs`(时代数量)时,您能取得什么结果? +1. 如果你使用更深入的预训练模型,你会得到更好的结果吗?你如何调整超参数?你能进一步改善结果吗? + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/380) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1481) +:end_tab: diff --git a/chapter_computer-vision/kaggle-dog_origin.md b/chapter_computer-vision/kaggle-dog_origin.md new file mode 100644 index 000000000..b99514c48 --- /dev/null +++ b/chapter_computer-vision/kaggle-dog_origin.md @@ -0,0 +1,518 @@ +# Dog Breed Identification (ImageNet Dogs) on Kaggle + +In this section, we will practice +the dog breed identification problem on +Kaggle. The web address of this competition is https://www.kaggle.com/c/dog-breed-identification + +In this competition, +120 different breeds of dogs will be recognized. +In fact, +the dataset for this competition is +a subset of the ImageNet dataset. +Unlike the images in the CIFAR-10 dataset in :numref:`sec_kaggle_cifar10`, +the images in the ImageNet dataset are both higher and wider in varying dimensions. +:numref:`fig_kaggle_dog` shows the information on the competition's webpage. You need a Kaggle account +to submit your results. + + +![The dog breed identification competition website. The competition dataset can be obtained by clicking the "Data" tab.](../img/kaggle-dog.jpg) +:width:`400px` +:label:`fig_kaggle_dog` + +```{.python .input} +from d2l import mxnet as d2l +from mxnet import autograd, gluon, init, npx +from mxnet.gluon import nn +import os + +npx.set_np() +``` + +```{.python .input} +#@tab pytorch +from d2l import torch as d2l +import torch +import torchvision +from torch import nn +import os +``` + +## Obtaining and Organizing the Dataset + +The competition dataset is divided into a training set and a test set, which contain 10222 and 10357 JPEG images +of three RGB (color) channels, respectively. +Among the training dataset, +there are 120 breeds of dogs +such as Labradors, Poodles, Dachshunds, Samoyeds, Huskies, Chihuahuas, and Yorkshire Terriers. + + +### Downloading the Dataset + +After logging into Kaggle, +you can click on the "Data" tab on the +competition webpage shown in :numref:`fig_kaggle_dog` and download the dataset by clicking the "Download All" button. +After unzipping the downloaded file in `../data`, you will find the entire dataset in the following paths: + +* ../data/dog-breed-identification/labels.csv +* ../data/dog-breed-identification/sample_submission.csv +* ../data/dog-breed-identification/train +* ../data/dog-breed-identification/test + +You may have noticed that the above structure is +similar to that of the CIFAR-10 competition in :numref:`sec_kaggle_cifar10`, where folders `train/` and `test/` contain training and testing dog images, respectively, and `labels.csv` contains +the labels for the training images. +Similarly, to make it easier to get started, we provide a small sample of the dataset mentioned above: `train_valid_test_tiny.zip`. +If you are going to use the full dataset for the Kaggle competition, you need to change the `demo` variable below to `False`. + +```{.python .input} +#@tab all +#@save +d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip', + '0cb91d09b814ecdc07b50f31f8dcad3e81d6a86d') + +# If you use the full dataset downloaded for the Kaggle competition, change +# the variable below to `False` +demo = True +if demo: + data_dir = d2l.download_extract('dog_tiny') +else: + data_dir = os.path.join('..', 'data', 'dog-breed-identification') +``` + +### Organizing the Dataset + +We can organize the dataset similarly to what we did in :numref:`sec_kaggle_cifar10`, namely splitting out +a validation set from the original training set, and moving images into subfolders grouped by labels. + +The `reorg_dog_data` function below reads +the training data labels, splits out the validation set, and organizes the training set. + +```{.python .input} +#@tab all +def reorg_dog_data(data_dir, valid_ratio): + labels = d2l.read_csv_labels(os.path.join(data_dir, 'labels.csv')) + d2l.reorg_train_valid(data_dir, labels, valid_ratio) + d2l.reorg_test(data_dir) + + +batch_size = 4 if demo else 128 +valid_ratio = 0.1 +reorg_dog_data(data_dir, valid_ratio) +``` + +## Image Augmentation + +Recall that this dog breed dataset +is a subset of the ImageNet dataset, +whose images +are larger than those of the CIFAR-10 dataset +in :numref:`sec_kaggle_cifar10`. +The following +lists a few image augmentation operations +that might be useful for relatively larger images. + +```{.python .input} +transform_train = gluon.data.vision.transforms.Compose([ + # Randomly crop the image to obtain an image with an area of 0.08 to 1 of + # the original area and height-to-width ratio between 3/4 and 4/3. Then, + # scale the image to create a new 224 x 224 image + gluon.data.vision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), + ratio=(3.0/4.0, 4.0/3.0)), + gluon.data.vision.transforms.RandomFlipLeftRight(), + # Randomly change the brightness, contrast, and saturation + gluon.data.vision.transforms.RandomColorJitter(brightness=0.4, + contrast=0.4, + saturation=0.4), + # Add random noise + gluon.data.vision.transforms.RandomLighting(0.1), + gluon.data.vision.transforms.ToTensor(), + # Standardize each channel of the image + gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +```{.python .input} +#@tab pytorch +transform_train = torchvision.transforms.Compose([ + # Randomly crop the image to obtain an image with an area of 0.08 to 1 of + # the original area and height-to-width ratio between 3/4 and 4/3. Then, + # scale the image to create a new 224 x 224 image + torchvision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0), + ratio=(3.0/4.0, 4.0/3.0)), + torchvision.transforms.RandomHorizontalFlip(), + # Randomly change the brightness, contrast, and saturation + torchvision.transforms.ColorJitter(brightness=0.4, + contrast=0.4, + saturation=0.4), + # Add random noise + torchvision.transforms.ToTensor(), + # Standardize each channel of the image + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +During prediction, +we only use image preprocessing operations +without randomness. + +```{.python .input} +transform_test = gluon.data.vision.transforms.Compose([ + gluon.data.vision.transforms.Resize(256), + # Crop a 224 x 224 square area from the center of the image + gluon.data.vision.transforms.CenterCrop(224), + gluon.data.vision.transforms.ToTensor(), + gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +```{.python .input} +#@tab pytorch +transform_test = torchvision.transforms.Compose([ + torchvision.transforms.Resize(256), + # Crop a 224 x 224 square area from the center of the image + torchvision.transforms.CenterCrop(224), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) +``` + +## Reading the Dataset + +As in :numref:`sec_kaggle_cifar10`, +we can read the organized dataset +consisting of raw image files. + +```{.python .input} +train_ds, valid_ds, train_valid_ds, test_ds = [ + gluon.data.vision.ImageFolderDataset( + os.path.join(data_dir, 'train_valid_test', folder)) + for folder in ('train', 'valid', 'train_valid', 'test')] +``` + +```{.python .input} +#@tab pytorch +train_ds, train_valid_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_train) for folder in ['train', 'train_valid']] + +valid_ds, test_ds = [torchvision.datasets.ImageFolder( + os.path.join(data_dir, 'train_valid_test', folder), + transform=transform_test) for folder in ['valid', 'test']] +``` + +Below we create data loader instances +the same way +as in :numref:`sec_kaggle_cifar10`. + +```{.python .input} +train_iter, train_valid_iter = [gluon.data.DataLoader( + dataset.transform_first(transform_train), batch_size, shuffle=True, + last_batch='discard') for dataset in (train_ds, train_valid_ds)] + +valid_iter = gluon.data.DataLoader( + valid_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='discard') + +test_iter = gluon.data.DataLoader( + test_ds.transform_first(transform_test), batch_size, shuffle=False, + last_batch='keep') +``` + +```{.python .input} +#@tab pytorch +train_iter, train_valid_iter = [torch.utils.data.DataLoader( + dataset, batch_size, shuffle=True, drop_last=True) + for dataset in (train_ds, train_valid_ds)] + +valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False, + drop_last=True) + +test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False, + drop_last=False) +``` + +## Fine-Tuning a Pretrained Model + +Again, +the dataset for this competition is a subset of the ImageNet dataset. +Therefore, we can use the approach discussed in +:numref:`sec_fine_tuning` +to select a model pretrained on the +full ImageNet dataset and use it to extract image features to be fed into a +custom small-scale output network. +High-level APIs of deep learning frameworks +provide a wide range of models +pretrained on the ImageNet dataset. +Here, we choose +a pretrained ResNet-34 model, +where we simply reuse +the input of this model's output layer +(i.e., the extracted +features). +Then we can replace the original output layer with a small custom +output network that can be trained, +such as stacking two +fully-connected layers. +Different from the experiment in +:numref:`sec_fine_tuning`, +the following does +not retrain the pretrained model used for feature +extraction. This reduces training time and +memory for storing gradients. + +Recall that we +standardized images using +the means and standard deviations of the three RGB channels for the full ImageNet dataset. +In fact, +this is also consistent with the standardization operation +by the pretrained model on ImageNet. + +```{.python .input} +def get_net(devices): + finetune_net = gluon.model_zoo.vision.resnet34_v2(pretrained=True) + # Define a new output network + finetune_net.output_new = nn.HybridSequential(prefix='') + finetune_net.output_new.add(nn.Dense(256, activation='relu')) + # There are 120 output categories + finetune_net.output_new.add(nn.Dense(120)) + # Initialize the output network + finetune_net.output_new.initialize(init.Xavier(), ctx=devices) + # Distribute the model parameters to the CPUs or GPUs used for computation + finetune_net.collect_params().reset_ctx(devices) + return finetune_net +``` + +```{.python .input} +#@tab pytorch +def get_net(devices): + finetune_net = nn.Sequential() + finetune_net.features = torchvision.models.resnet34(pretrained=True) + # Define a new output network (there are 120 output categories) + finetune_net.output_new = nn.Sequential(nn.Linear(1000, 256), + nn.ReLU(), + nn.Linear(256, 120)) + # Move the model to devices + finetune_net = finetune_net.to(devices[0]) + # Freeze parameters of feature layers + for param in finetune_net.features.parameters(): + param.requires_grad = False + return finetune_net +``` + +Before calculating the loss, +we first obtain the input of the pretrained model's output layer, i.e., the extracted feature. +Then we use this feature as the input for our small custom output network to calculate the loss. + +```{.python .input} +loss = gluon.loss.SoftmaxCrossEntropyLoss() + +def evaluate_loss(data_iter, net, devices): + l_sum, n = 0.0, 0 + for features, labels in data_iter: + X_shards, y_shards = d2l.split_batch(features, labels, devices) + output_features = [net.features(X_shard) for X_shard in X_shards] + outputs = [net.output_new(feature) for feature in output_features] + ls = [loss(output, y_shard).sum() for output, y_shard + in zip(outputs, y_shards)] + l_sum += sum([float(l.sum()) for l in ls]) + n += labels.size + return l_sum / n +``` + +```{.python .input} +#@tab pytorch +loss = nn.CrossEntropyLoss(reduction='none') + +def evaluate_loss(data_iter, net, devices): + l_sum, n = 0.0, 0 + for features, labels in data_iter: + features, labels = features.to(devices[0]), labels.to(devices[0]) + outputs = net(features) + l = loss(outputs, labels) + l_sum = l.sum() + n += labels.numel() + return l_sum / n +``` + +## Defining the Training Function + +We will select the model and tune hyperparameters according to the model's performance on the validation set. The model training function `train` only +iterates parameters of the small custom output network. + +```{.python .input} +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + # Only train the small custom output network + trainer = gluon.Trainer(net.output_new.collect_params(), 'sgd', + {'learning_rate': lr, 'momentum': 0.9, 'wd': wd}) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'valid loss']) + for epoch in range(num_epochs): + metric = d2l.Accumulator(2) + if epoch > 0 and epoch % lr_period == 0: + trainer.set_learning_rate(trainer.learning_rate * lr_decay) + for i, (features, labels) in enumerate(train_iter): + timer.start() + X_shards, y_shards = d2l.split_batch(features, labels, devices) + output_features = [net.features(X_shard) for X_shard in X_shards] + with autograd.record(): + outputs = [net.output_new(feature) + for feature in output_features] + ls = [loss(output, y_shard).sum() for output, y_shard + in zip(outputs, y_shards)] + for l in ls: + l.backward() + trainer.step(batch_size) + metric.add(sum([float(l.sum()) for l in ls]), labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[1], None)) + if valid_iter is not None: + valid_loss = evaluate_loss(valid_iter, net, devices) + animator.add(epoch + 1, (None, valid_loss)) + if valid_iter is not None: + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'valid loss {valid_loss:.3f}') + else: + print(f'train loss {metric[0] / metric[1]:.3f}') + print(f'{metric[1] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +```{.python .input} +#@tab pytorch +def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay): + # Only train the small custom output network + net = nn.DataParallel(net, device_ids=devices).to(devices[0]) + trainer = torch.optim.SGD((param for param in net.parameters() + if param.requires_grad), lr=lr, + momentum=0.9, weight_decay=wd) + scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) + num_batches, timer = len(train_iter), d2l.Timer() + animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], + legend=['train loss', 'valid loss']) + for epoch in range(num_epochs): + metric = d2l.Accumulator(2) + for i, (features, labels) in enumerate(train_iter): + timer.start() + features, labels = features.to(devices[0]), labels.to(devices[0]) + trainer.zero_grad() + output = net(features) + l = loss(output, labels).sum() + l.backward() + trainer.step() + metric.add(l, labels.shape[0]) + timer.stop() + if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: + animator.add(epoch + (i + 1) / num_batches, + (metric[0] / metric[1], None)) + if valid_iter is not None: + valid_loss = evaluate_loss(valid_iter, net, devices) + animator.add(epoch + 1, (None, valid_loss)) + scheduler.step() + if valid_iter is not None: + print(f'train loss {metric[0] / metric[1]:.3f}, ' + f'valid loss {valid_loss:.3f}') + else: + print(f'train loss {metric[0] / metric[1]:.3f}') + print(f'{metric[1] * num_epochs / timer.sum():.1f} examples/sec ' + f'on {str(devices)}') +``` + +## Training and Validating the Model + +Now we can train and validate the model. +The following hyperparameters are all tunable. +For example, the number of epochs can be increased. Because `lr_period` and `lr_decay` are set to 10 and 0.1, respectively, the learning rate of the optimization algorithm will be multiplied by 0.1 after every 10 epochs. + +```{.python .input} +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.01, 1e-4 +lr_period, lr_decay, net = 10, 0.1, get_net(devices) +net.hybridize() +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +```{.python .input} +#@tab pytorch +devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.001, 1e-4 +lr_period, lr_decay, net = 10, 0.1, get_net(devices) +train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, + lr_decay) +``` + +## Classifying the Testing Set and Submitting Results on Kaggle + + +Similar to the final step in :numref:`sec_kaggle_cifar10`, +in the end all the labeled data (including the validation set) are used for training the model and classifying the testing set. +We will use the trained custom output network +for classification. + +```{.python .input} +net = get_net(devices) +net.hybridize() +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +preds = [] +for data, label in test_iter: + output_features = net.features(data.as_in_ctx(devices[0])) + output = npx.softmax(net.output_new(output_features)) + preds.extend(output.asnumpy()) +ids = sorted(os.listdir( + os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))) +with open('submission.csv', 'w') as f: + f.write('id,' + ','.join(train_valid_ds.synsets) + '\n') + for i, output in zip(ids, preds): + f.write(i.split('.')[0] + ',' + ','.join( + [str(num) for num in output]) + '\n') +``` + +```{.python .input} +#@tab pytorch +net = get_net(devices) +train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period, + lr_decay) + +preds = [] +for data, label in test_iter: + output = torch.nn.functional.softmax(net(data.to(devices[0])), dim=0) + preds.extend(output.cpu().detach().numpy()) +ids = sorted(os.listdir( + os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))) +with open('submission.csv', 'w') as f: + f.write('id,' + ','.join(train_valid_ds.classes) + '\n') + for i, output in zip(ids, preds): + f.write(i.split('.')[0] + ',' + ','.join( + [str(num) for num in output]) + '\n') +``` + +The above code +will generate a `submission.csv` file +to be submitted +to Kaggle in the same way described in :numref:`sec_kaggle_house`. + + +## Summary + + +* Images in the ImageNet dataset are larger (with varying dimensions) than CIFAR-10 images. We may modify image augmentation operations for tasks on a different dataset. +* To classify a subset of the ImageNet dataset, we can leverage pre-trained models on the full ImageNet dataset to extract features and only train a custom small-scale output network. This will lead to less computational time and memory cost. + + +## Exercises + +1. When using the fill Kaggle competition dataset, what results can you achieve when you increase the `batch_size` (batch size) and `num_epochs` (number of epochs)? +1. Do you get better results if you use a deeper pretrained model? How do you tune hyperparameters? Can you further improve the results? + +:begin_tab:`mxnet` +[Discussions](https://discuss.d2l.ai/t/380) +:end_tab: + +:begin_tab:`pytorch` +[Discussions](https://discuss.d2l.ai/t/1481) +:end_tab: From 225e6bdd72dfb45057fbaab1aee9e16d1c0d97ba Mon Sep 17 00:00:00 2001 From: HelloWorld Date: Thu, 6 May 2021 02:09:33 +0800 Subject: [PATCH 078/103] Update index.md (#786) --- chapter_computer-vision/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_computer-vision/index.md b/chapter_computer-vision/index.md index 7d1e366ff..b30c09778 100644 --- a/chapter_computer-vision/index.md +++ b/chapter_computer-vision/index.md @@ -3,7 +3,7 @@ 无论是医疗诊断、自动驾驶车辆、摄像头监控还是智能滤镜,计算机视觉领域的许多应用都与我们当前和未来的生活密切相关。近年来,深度学习一直是提高计算机视觉系统性能的变革力量。可以说,最先进的计算机视觉应用程序与深度学习几乎是不可分割的。有鉴于此,本章将重点介绍计算机视觉领域,并探讨最近在学术界和行业中具有影响力的方法和应用。 -在 :numref:`chap_cnn` 和 :numref:`chap_modern_cnn` 中,我们研究了计算机视觉中常用的各种卷积神经网络,并将它们应用到简单的图像分类任务中。在本章开头,我们将介绍两种可以改进模型泛化的方法,即 * 图像增强 * 和 * 微调 *,并将它们应用于图像分类。由于深度神经网络可以有效地表示多个层次的图像,因此这种分层表示已成功用于各种计算机视觉任务,例如 * 对象检测 *、* 语义分段 * 和 * 样式转移 *。遵循在计算机视觉中利用分层表示的关键思想,我们将从物体检测的主要组件和技术开始。接下来,我们将展示如何使用 * 完全卷积网络 * 对图像进行语义分割。然后我们将解释如何使用风格传递技术来生成像本书封面一样的图像。最后,我们在结束本章时将本章和前几章的材料应用于两个流行的计算机视觉基准数据集。 +在 :numref:`chap_cnn` 和 :numref:`chap_modern_cnn` 中,我们研究了计算机视觉中常用的各种卷积神经网络,并将它们应用到简单的图像分类任务中。在本章开头,我们将介绍两种可以改进模型泛化的方法,即 * 图像增强 * 和 * 微调 *,并将它们应用于图像分类。由于深度神经网络可以有效地表示多个层次的图像,因此这种分层表示已成功用于各种计算机视觉任务,例如 * 对象检测 *、* 语义分段 * 和 * 样式转移 *。遵循在计算机视觉中利用分层表示的关键思想,我们将从物体检测的主要组件和技术开始。接下来,我们将展示如何使用 * 完全卷积网络 * 对图像进行语义分割。然后我们将解释如何使用风格迁移技术来生成像本书封面一样的图像。最后,我们在结束本章时将本章和前几章的材料应用于两个流行的计算机视觉基准数据集。 ```toc :maxdepth: 2 From d511b06f89ad138c8a8fcc3fa86e0a127098019e Mon Sep 17 00:00:00 2001 From: HelloWorld Date: Thu, 6 May 2021 02:28:53 +0800 Subject: [PATCH 079/103] =?UTF-8?q?=E6=B6=A6=E8=89=B2=E7=BF=BB=E8=AF=91=20?= =?UTF-8?q?(#785)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update lookup-api.md fix one word '这' * 修改和一点疑问。 我发现了一些基本的排版格式,例如数字和英文,在中文中间的需要两边加上空格。**之间的是斜体。 那么**直接的空格应该怎么处理?需要在*两边都加上空格吗? **和**之间是加黑对吗? * Update index.md * Update attention-cues.md * Update attention-scoring-functions.md * Update nadaraya-waston.md * Update transformer.md * Update bahdanau-attention.md * Update multihead-attention.md * fix translate * Update hybridize.md * Update async-computation.md * Update auto-parallelism.md * Update hardware.md * Update multiple-gpus-concise.md * Update multiple-gpus.md * Update parameterserver.md * Update image-augmentation.md * Update fine-tuning.md --- .../attention-cues.md | 2 +- .../attention-scoring-functions.md | 2 +- .../bahdanau-attention.md | 34 +++++++++---------- chapter_attention-mechanisms/index.md | 12 +++---- .../multihead-attention.md | 6 ++-- .../nadaraya-waston.md | 2 +- chapter_attention-mechanisms/transformer.md | 2 +- .../async-computation.md | 2 +- .../auto-parallelism.md | 2 +- chapter_computational-performance/hardware.md | 2 +- .../hybridize.md | 4 +-- chapter_computational-performance/index.md | 2 +- .../multiple-gpus-concise.md | 2 +- .../multiple-gpus.md | 4 +-- .../parameterserver.md | 2 +- chapter_computer-vision/fine-tuning.md | 28 +++++++-------- chapter_computer-vision/image-augmentation.md | 14 ++++---- 17 files changed, 59 insertions(+), 63 deletions(-) diff --git a/chapter_attention-mechanisms/attention-cues.md b/chapter_attention-mechanisms/attention-cues.md index eadedd588..f27f43161 100644 --- a/chapter_attention-mechanisms/attention-cues.md +++ b/chapter_attention-mechanisms/attention-cues.md @@ -91,7 +91,7 @@ show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries') 在接下来的章节中,我们经常调用此函数来显示注意力权重。 -## 摘要 +## 小结 * 人类的注意力是有限、宝贵和稀缺的资源。 * 受试者使用非自主和自主提示有选择地专注注意力。前者基于显著程度,后者取决于任务。 diff --git a/chapter_attention-mechanisms/attention-scoring-functions.md b/chapter_attention-mechanisms/attention-scoring-functions.md index 7d4b7e71e..06d41be3d 100644 --- a/chapter_attention-mechanisms/attention-scoring-functions.md +++ b/chapter_attention-mechanisms/attention-scoring-functions.md @@ -285,7 +285,7 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), xlabel='Keys', ylabel='Queries') ``` -## 摘要 +## 小结 * 可以将注意力池化的输出计算作为值的加权平均值,其中注意力评分函数的不同选择会导致不同的注意力池化行为。 * 当查询和键是不同长度的矢量时,我们可以使用加性注意力评分函数。当它们相同时,缩放的“点-积”注意力评分函数在计算上更有效率。 diff --git a/chapter_attention-mechanisms/bahdanau-attention.md b/chapter_attention-mechanisms/bahdanau-attention.md index 50c7d0faf..8b8b966ef 100644 --- a/chapter_attention-mechanisms/bahdanau-attention.md +++ b/chapter_attention-mechanisms/bahdanau-attention.md @@ -1,19 +1,19 @@ -# Bahdanau 关注 +# Bahdanau 注意力 :label:`sec_seq2seq_attention` -我们在 :numref:`sec_seq2seq` 中研究了机器翻译问题,在那里我们设计了一个基于两个 RNN 的编码器解码器架构,用于顺序到序列的学习。具体来说,RNN 编码器将可变长度序列转换为固定形状的上下文变量,然后 RNN 解码器根据生成的令牌和上下文变量按令牌生成输出(目标)序列令牌。但是,即使并非所有输入(源)令牌都对解码某个标记都有用,但在每个解码步骤中仍使用编码整个输入序列的 *same* 上下文变量。 +我们在 :numref:`sec_seq2seq` 中研究了机器翻译问题,在那里我们设计了一个基于两个循环神经网络的编码器-解码器架构,用于顺序到序列的学习。具体来说,循环神经网络编码器将可变长度序列转换为固定形状的上下文变量,然后循环神经网络解码器根据生成的标记和上下文变量按标记生成输出(目标)序列标记。但是,即使并非所有输入(源)标记都对解码某个标记都有用,但在每个解码步骤中仍使用编码整个输入序列的**相同**上下文变量。 -在为给定文本序列生成手写的一个单独但相关的挑战中,格雷夫斯设计了一种可区分的注意力模型,将文本字符与更长的笔迹对齐,其中对齐方式仅向一个方向移动 :cite:`Graves.2013`。受学习对齐想法的启发,Bahdanau 等人提出了一个没有严格的单向对齐限制 :cite:`Bahdanau.Cho.Bengio.2014` 的可区分注意力模型。在预测令牌时,如果不是所有输入令牌都相关,模型将仅对齐(或参与)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力集中的输出来实现的。 +在为给定文本序列生成手写的一个单独但相关的挑战中,格雷夫斯设计了一种可区分的注意力模型,将文本字符与更长的笔迹对齐,其中对齐方式仅向一个方向移动 :cite:`Graves.2013`。受学习对齐想法的启发,Bahdanau 等人提出了一个没有严格的单向对齐限制 :cite:`Bahdanau.Cho.Bengio.2014` 的可区分注意力模型。在预测标记时,如果不是所有输入标记都相关,模型将仅对齐(或参与)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力集中的输出来实现的。 ## 模型 -在下面描述 Bahdanau 对 RNN 编码器的关注时,我们将遵循 :numref:`sec_seq2seq` 中的相同符号。新的基于注意的模型与 :numref:`sec_seq2seq` 中的模型相同,只不过 :eqref:`eq_seq2seq_s_t` 中的上下文变量 $\mathbf{c}$ 在任何解码时间步骤 $t'$ 都会被 $\mathbf{c}_{t'}$ 替换。假设输入序列中有 $T$ 个令牌,解码时间步长 $t'$ 的上下文变量是注意力集中的输出: +在下面描述 Bahdanau 注意力对循环神经网络编码器的关注时,我们将遵循 :numref:`sec_seq2seq` 中的相同符号。新的基于注意的模型与 :numref:`sec_seq2seq` 中的模型相同,只不过 :eqref:`eq_seq2seq_s_t` 中的上下文变量 $\mathbf{c}$ 在任何解码时间步骤 $t'$ 都会被 $\mathbf{c}_{t'}$ 替换。假设输入序列中有 $T$ 个标记,解码时间步长 $t'$ 的上下文变量是注意力集中的输出: $$\mathbf{c}_{t'} = \sum_{t=1}^T \alpha(\mathbf{s}_{t' - 1}, \mathbf{h}_t) \mathbf{h}_t,$$ 其中,时间步骤 $t' - 1$ 时的解码器隐藏状态 $\mathbf{s}_{t' - 1}$ 是查询,编码器隐藏状态 $\mathbf{h}_t$ 既是键,也是值,注意权重 $\alpha$ 是使用 :eqref:`eq_attn-scoring-alpha` 所定义的加法注意力评分函数计算的。 -与 :numref:`fig_seq2seq_details` 中的香草 RNN 编码器解码器架构略有不同,:numref:`fig_s2s_attention_details` 描述了巴赫达瑙关注的同一架构。 +与 :numref:`fig_seq2seq_details` 中的基础循环神经网络编码器-解码器架构略有不同,:numref:`fig_s2s_attention_details` 描述了 Bahdanau 注意力的架构。 ![Layers in an RNN encoder-decoder model with Bahdanau attention.](../img/seq2seq-attention-details.svg) :label:`fig_s2s_attention_details` @@ -32,9 +32,9 @@ import torch from torch import nn ``` -## 注意定义解码器 +## 定义注意力解码器 -要在 Bahdanau 关注的情况下实现 RNN 编码器-解码器,我们只需重新定义解码器即可。为了更方便地显示学习的注意力权重,以下 `AttentionDecoder` 类定义了具有注意机制的解码器的基本接口。 +要用 Bahdanau 注意力实现循环神经网络编码器-解码器,我们只需重新定义解码器即可。为了更方便地显示学习的注意力权重,以下 `AttentionDecoder` 类定义了具有注意机制的解码器的基本接口。 ```{.python .input} #@tab all @@ -49,7 +49,7 @@ class AttentionDecoder(d2l.Decoder): raise NotImplementedError ``` -现在让我们在接下来的 `Seq2SeqAttentionDecoder` 课程中以 Bahdanau 关注的情况下实施 RNN 解码器。解码器的状态初始化为 i) 编码器在所有时间步长的最终层隐藏状态(作为关注的键和值);ii) 最后一个时间步长的编码器全层隐藏状态(初始化解码器的隐藏状态);和 iii) 编码器有效长度(排除在注意力池中填充令牌)。在每个解码时间步骤中,解码器上一个时间步的最终层隐藏状态将用作关注的查询。因此,注意力输出和输入嵌入都连接为 RNN 解码器的输入。 +现在让我们在接下来的 `Seq2SeqAttentionDecoder` 类中以 Bahdanau 注意力实现循环神经网络解码器。初始化解码器的状态 1) 编码器在所有时间步长的最终层隐藏状态(作为注意力的键和值);2) 最后一个时间步长的编码器全层隐藏状态(初始化解码器的隐藏状态);和 3) 编码器有效长度(排除在注意力池中填充标记)。在每个解码时间步骤中,解码器上一个时间步的最终层隐藏状态将用作关注的查询。因此,注意力输出和输入嵌入都连接为循环神经网络解码器的输入。 ```{.python .input} class Seq2SeqAttentionDecoder(AttentionDecoder): @@ -151,7 +151,7 @@ class Seq2SeqAttentionDecoder(AttentionDecoder): return self._attention_weights ``` -在以下内容中,我们使用包含 7 个时间步长的 4 个序列输入的小批量测试已实施的解码器,使用 Bahdanau 的注意力。 +接下来,我们使用包含 7 个时间步长的 4 个序列输入的小批量测试我们实现的 Bahdanau 注意力解码器。 ```{.python .input} encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -180,9 +180,9 @@ output, state = decoder(X, state) output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape ``` -## 培训 +## 训练 -与 :numref:`sec_seq2seq_training` 类似,我们在这里指定超级测量器,实例化一个编码器和解码器,并在 Bahdanau 关注的情况下对这个模型进行机器翻译培训。由于新增的关注机制,这项培训比没有注意力机制的 :numref:`sec_seq2seq_training` 慢得多。 +与 :numref:`sec_seq2seq_training` 类似,我们在这里指定超参数,实例化一个 Bahdanau 注意力编码器和解码器,并对这个模型进行机器翻译训练。由于新增的注意力机制,这项训练要比没有注意力机制的 :numref:`sec_seq2seq_training` 慢得多。 ```{.python .input} #@tab all @@ -199,7 +199,7 @@ net = d2l.EncoderDecoder(encoder, decoder) d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ``` -模型训练完毕后,我们用它将几个英语句子翻译成法语并计算它们的 BLEU 分数。 +模型训练后,我们用它将几个英语句子翻译成法语并计算它们的 BLEU 分数。 ```{.python .input} #@tab all @@ -219,7 +219,7 @@ attention_weights = d2l.reshape( (1, 1, -1, num_steps)) ``` -通过将翻译最后一个英语句子时的注意力权重可视化,我们可以看到每个查询都会在键值对上分配不均匀的权重。它显示,在每个解码步骤中,输入序列的不同部分都会有选择地聚合在注意力池中。 +训练结束后通过可视化注意力权重,我们可以看到,每个查询都会在键值对上分配不同的权重。它显示,在每个解码步骤中,输入序列的不同部分被选择性地聚集在注意力池中。 ```{.python .input} # Plus one to include the end-of-sequence token @@ -236,15 +236,15 @@ d2l.show_heatmaps( xlabel='Key posistions', ylabel='Query posistions') ``` -## 摘要 +## 小结 -* 在预测令牌时,如果不是所有输入令牌都是相关的,那么具有 Bahdanau 关注的 RNN 编码器会有选择地聚合输入序列的不同部分。这是通过将上下文变量视为加法注意力池的输出来实现的。 -* 在 RNN 编码器解码器中,Bahdanau 的注意力将上一个时间步的解码器隐藏状态视为查询,编码器在所有时间步长的隐藏状态同时视为键和值。 +* 在预测标记时,如果不是所有输入标记都是相关的,那么具有 Bahdanau 注意力的循环神经网络编码器-解码器会有选择地统计输入序列的不同部分。这是通过将上下文变量视为加法注意力池的输出来实现的。 +* 在循环神经网络编码器-解码器中,Bahdanau 注意力将上一个时间步的解码器隐藏状态视为查询,在所有时间步长编码器隐藏状态同时视为键和值。 ## 练习 1. 在实验中用 LSTM 替换 GRU。 -1. 修改实验以将加法注意力评分功能替换为缩放的点积。它如何影响培训效率? +1. 修改实验以将加法注意力评分功能替换为缩放的点积。它如何影响训练效率? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/347) diff --git a/chapter_attention-mechanisms/index.md b/chapter_attention-mechanisms/index.md index 5704e04e8..d4fbbe7d1 100644 --- a/chapter_attention-mechanisms/index.md +++ b/chapter_attention-mechanisms/index.md @@ -1,15 +1,13 @@ -# 注意机制 +# 注意力机制 :label:`chap_attention` -灵长类动物视觉系统的视神经接受大量的感官输入,远远超过了大脑能够完全处理的程度。幸运的是,并非所有的刺激都是平等的。意识的聚集和集中使灵长类动物能够在复杂的视觉环境中将注意力引向感兴趣的物体,例如猎物和掠食动物。只关注一小部分信息的能力具有进化意义,使人类能够生存和成功。 +灵长类动物视觉系统的视神经接受大量的感官输入,远远超过了大脑能够完全处理的程度。幸运的是,并非所有的刺激都是平等的。意识的聚集和集中使灵长类动物能够在复杂的视觉环境中将注意力引向感兴趣的物体,例如猎物和捕食者。只关注一小部分信息的能力具有进化意义,使人类能够生存和成功。 -自 19 世纪以来,科学家们一直在研究认知神经科学领域的注意力。在本章中,我们将首先回顾一个热门框架,解释如何在视觉场景中部署注意力。受此框架中的注意线索的启发,我们将设计利用这些关注线索的模型。值得注意的是,1964 年的 Nadaraya-Waston 内核回归是具有 * 注意力机制 * 的机器学习的简单演示。 +自 19 世纪以来,科学家们一直在研究认知神经科学领域的注意力。在本章中,我们将首先回顾一个热门框架,解释如何在视觉场景中部署注意力。受此框架中的注意线索的启发,我们将设计利用这些关注线索的模型。值得注意的是,1964 年的 Nadaraya-Waston 内核回归是具有 *注意力机制* 的机器学习的简单演示。 -接下来,我们将继续介绍在深度学习中注意力模型设计中广泛使用的注意力函数。具体来说,我们将展示如何使用这些函数来设计 *Bahdanau 注意力 *,这是深度学习中的突破性注意力模型,可以双向对齐并且可以区分。 +接下来,我们将继续介绍在深度学习中注意力模型设计中广泛使用的注意力函数。具体来说,我们将展示如何使用这些函数来设计 *Bahdanau 注意力*,这是深度学习中的突破性注意力模型,可以双向对齐并且可区分。 -最后,配备了最近的 -*多头关注 * -和 * 自我关注 * 设计,我们将仅基于注意机制来描述 *Transer* 架构。自 2017 年提出建议以来,变形金刚一直在现代深度学习应用中普遍存在,例如语言、视觉、语音和强化学习领域。 +最后,配备了最近的 *多头注意力* 和 *自注意力* 设计,我们将仅基于注意机制来描述 *Transformer* 架构。自 2017 年提出建议以来,Transformers 一直在现代深度学习应用中普遍存在,例如语言、视觉、语音和强化学习领域。 ```toc :maxdepth: 2 diff --git a/chapter_attention-mechanisms/multihead-attention.md b/chapter_attention-mechanisms/multihead-attention.md index 4734add2c..225be5296 100644 --- a/chapter_attention-mechanisms/multihead-attention.md +++ b/chapter_attention-mechanisms/multihead-attention.md @@ -1,4 +1,4 @@ -# 多头关注 +# 多头注意力 :label:`sec_multihead-attention` 实际上,鉴于查询、键和值集相同,我们可能希望我们的模型将来自同一注意机制不同行为的知识结合起来,例如捕获序列内各种范围的依赖关系(例如,短范围与长距离)。因此,允许我们的注意机制共同使用查询、键和值的不同表示子空间可能是有益的。 @@ -36,7 +36,7 @@ import torch from torch import nn ``` -## 实施 +## 实现 在我们的实施过程中,我们为多头关注的每个人选择缩放的点产品注意力。为避免计算成本和参数化成本的显著增长,我们设置了 $p_q = p_k = p_v = p_o / h$。请注意,如果我们将查询、键和值的线性变换的输出数量设置为 $p_q h = p_k h = p_v h = p_o$,则可以并行计算 $h$ 头。在下面的实现中,$p_o$ 是通过参数 `num_hiddens` 指定的。 @@ -207,7 +207,7 @@ Y = d2l.ones((batch_size, num_kvpairs, num_hiddens)) attention(X, Y, Y, valid_lens).shape ``` -## 摘要 +## 小结 * 多头关注通过查询、键和值的不同表示子空间将同一注意力集中的知识结合起来。 * 要并行计算多头多头注意力,需要适当的张量操作。 diff --git a/chapter_attention-mechanisms/nadaraya-waston.md b/chapter_attention-mechanisms/nadaraya-waston.md index 0114caf94..74d72161c 100644 --- a/chapter_attention-mechanisms/nadaraya-waston.md +++ b/chapter_attention-mechanisms/nadaraya-waston.md @@ -340,7 +340,7 @@ d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0), ylabel='Sorted testing inputs') ``` -## 摘要 +## 小结 * Nadaraya-Watson 核回归是具有注意力机制的机器学习示例。 * Nadaraya-Watson 核回归的注意力池化是训练输出的加权平均值。从注意力的角度来看,根据查询的函数和与值配对的键,将注意力权重分配给值。 diff --git a/chapter_attention-mechanisms/transformer.md b/chapter_attention-mechanisms/transformer.md index c496d8d8f..80cf6928b 100644 --- a/chapter_attention-mechanisms/transformer.md +++ b/chapter_attention-mechanisms/transformer.md @@ -624,7 +624,7 @@ d2l.show_heatmaps( 尽管 Transformer 架构是为了序列到序列的学习而提出的,但正如我们将在本书后面提及的那样,Transformer 编码器或 Transformer 解码器通常被单独用于不同的深度学习任务中。 -## 摘要 +## 小结 * Transformer 是“编码器-解码器”架构的一个实例,尽管在实践中编码器或解码器可以单独使用。 * 在 Transformer 中,多头自注意力用于表示输入序列和输出序列,尽管解码器必须通过掩码机制来保留自回归属性。 diff --git a/chapter_computational-performance/async-computation.md b/chapter_computational-performance/async-computation.md index d95250a4e..456693dd9 100644 --- a/chapter_computational-performance/async-computation.md +++ b/chapter_computational-performance/async-computation.md @@ -186,7 +186,7 @@ Python 前端线程和 C ++ 后端线程之间稍微简化的交互可以总结 假设这三个阶段的持续时间分别为 $t_1, t_2$ 和 $t_3$。如果我们不使用异步编程,则执行 10000 个计算所需的总时间约为 $10000 (t_1+ t_2 + t_3)$。如果使用异步编程,则执行 10000 个计算所花费的总时间可以减少到 $t_1 + 10000 t_2 + t_3$(假设为 $10000 t_2 > 9999t_1$),因为前端不必等后端返回每个循环的计算结果。 :end_tab: -## 摘要 +## 小结 * 深度学习框架可能会将 Python 前端与执行后端分离。这允许将命令快速异步插入到后端和相关的并行度。 * 异步导致前端响应相当灵敏。但是,请注意不要溢出任务队列,因为这可能会导致过多的内存消耗。建议对每个微型批次进行同步,以使前端和后端保持大致同步。 diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md index 15bdf9f9b..b4d45594d 100644 --- a/chapter_computational-performance/auto-parallelism.md +++ b/chapter_computational-performance/auto-parallelism.md @@ -166,7 +166,7 @@ with d2l.Benchmark('Run on GPU1 and copy to CPU'): ![The computational graph and its dependencies of a two-layer MLP on a CPU and two GPUs.](../img/twogpu.svg) :label:`fig_twogpu` -## 摘要 +## 小结 * 现代系统具有各种设备,例如多个 GPU 和 CPU。它们可以并行、异步使用。 * 现代系统还有各种通信资源,例如 PCI Express、存储(通常是固态硬盘或通过网络)和网络带宽。它们可以并行使用以实现峰值效率。 diff --git a/chapter_computational-performance/hardware.md b/chapter_computational-performance/hardware.md index 4eae518b2..62057dd5e 100644 --- a/chapter_computational-performance/hardware.md +++ b/chapter_computational-performance/hardware.md @@ -187,7 +187,7 @@ GPU 内存受到更高的带宽要求,因为它们的处理元素比 CPU 多 | Transfer 1MB to/from PCI-E GPU | 80 μs | ~12GB/s on PCI-Express x16 link | :label:`table_latency_numbers_tesla` -## 摘要 +## 小结 * 设备有操作开销。因此,重要的是要瞄准少量大量转账,而不是许多小转账。这适用于 RAM、SSD、网络和 GPU。 * 矢量化是性能的关键。确保你知道加速器的具体能力。例如,一些英特尔至强 CPU 对 INT8 操作特别有用,NVIDIA Volta GPU 在 FP16 矩阵矩阵操作中表现出色,NVIDIA Timon 在 FP16、INT8 和 INT4 操作中出色。 diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md index 79e3fdb11..ac5924571 100644 --- a/chapter_computational-performance/hybridize.md +++ b/chapter_computational-performance/hybridize.md @@ -1,4 +1,4 @@ -# 编译器和口译员 +# 编译器和解释器 :label:`sec_hybridize` 到目前为止,这本书一直侧重于命令式编程,它利用 `print`、`+` 和 `if` 等语句来改变计划的状态。考虑以下简单的命令性程序的例子。 @@ -357,7 +357,7 @@ net(x) 这与我们之前看到的截然不同。省略 `hybrid_forward` 中定义的所有打印语句。事实上,混合后,`net(x)` 的执行不再涉及 Python 解释器。这意味着,忽略任何虚假的 Python 代码(例如 print 语句),以利于更简化的执行和更好的性能。相反,MxNet 直接调用 C ++ 后端。另请注意,`symbol` 模块(例如 `asnumpy`)中不支持某些功能,而就地操作(如 `a += b` 和 `a[:] = a + b`)必须重写为 `a = a + b`。尽管如此,只要速度重要,汇编模型就值得付出努力。优势可以从小百分点到速度的两倍以上,具体取决于模型的复杂性、CPU 的速度以及 GPU 的速度和数量。 :end_tab: -## 摘要 +## 小结 * 命令式编程使设计新模型变得容易,因为可以使用控制流编写代码,并且能够使用大量 Python 软件生态系统。 * 符号编程要求我们先指定程序并在执行之前对其进行编译。好处是提高了性能。 diff --git a/chapter_computational-performance/index.md b/chapter_computational-performance/index.md index c3eef0003..715712ca2 100644 --- a/chapter_computational-performance/index.md +++ b/chapter_computational-performance/index.md @@ -1,7 +1,7 @@ # 计算性能 :label:`chap_performance` -在深度学习中,数据集和模型通常很大,这涉及大量计算。因此,计算性能非常重要。本章将重点介绍影响计算性能的主要因素:命令式编程、符号编程、异步计算、自动并行度和多 GPU 计算。通过研究本章,您可以进一步提高前几章中实施的模型的计算性能,例如,通过在不影响准确性的情况下缩短训练时间。 +在深度学习中,数据集和模型通常很大,这涉及大量计算。因此,计算性能非常重要。本章将重点介绍影响计算性能的主要因素:命令式编程、符号化编程、异步计算、自动并行和多 GPU 计算。通过学习本章,你可以进一步提高前几章中实现的模型的计算性能,例如,通过在不影响准确性的情况下缩短训练时间。 ```toc :maxdepth: 2 diff --git a/chapter_computational-performance/multiple-gpus-concise.md b/chapter_computational-performance/multiple-gpus-concise.md index 3da85c7de..7ed91b0d3 100644 --- a/chapter_computational-performance/multiple-gpus-concise.md +++ b/chapter_computational-performance/multiple-gpus-concise.md @@ -242,7 +242,7 @@ train(num_gpus=2, batch_size=512, lr=0.2) train(net, num_gpus=2, batch_size=512, lr=0.2) ``` -## 摘要 +## 小结 :begin_tab:`mxnet` * Gluon 通过提供上下文列表为跨多个设备的模型初始化提供了基元。 diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md index a31cfef36..ca17833c7 100644 --- a/chapter_computational-performance/multiple-gpus.md +++ b/chapter_computational-performance/multiple-gpus.md @@ -239,7 +239,7 @@ def split_batch(X, y, devices): nn.parallel.scatter(y, devices)) ``` -## 培训 +## 训练 现在我们可以在单个小批量上实施多 GPU 训练。其实施主要基于本节中描述的数据并行方法。我们将使用刚才讨论的辅助函数 `allreduce` 和 `split_and_load`,在多个 GPU 之间同步数据。请注意,我们不需要编写任何特定的代码即可实现并行性。由于计算图在微型批次内的设备之间没有任何依赖关系,因此它是并行 * 自动执行的。 @@ -342,7 +342,7 @@ train(num_gpus=1, batch_size=256, lr=0.2) train(num_gpus=2, batch_size=256, lr=0.2) ``` -## 摘要 +## 小结 * 有多种方法可以将深度网络训练分成多个 GPU。我们可以在图层之间、跨图层或跨数据拆分它们。前两者需要严格编排的数据传输。数据并行性是最简单的策略。 * 数据并行培训非常简单。但是,它增加了有效的微型批量以提高效率。 diff --git a/chapter_computational-performance/parameterserver.md b/chapter_computational-performance/parameterserver.md index 1ad42cba7..b51e0862a 100644 --- a/chapter_computational-performance/parameterserver.md +++ b/chapter_computational-performance/parameterserver.md @@ -86,7 +86,7 @@ $$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{ 通过隐藏简单的推拉操作背后的所有同步复杂性,我们可以解决希望能够简单地表达优化的统计建模师和需要处理分布式同步固有的复杂性的系统工程师的担忧。 -## 摘要 +## 小结 * 同步需要高度适应服务器内的特定网络基础架构和连接。这可能会对同步所需的时间产生重大影响。 * 对于 p3 和 DGX-2 服务器来说,环形同步可能是最佳选择。对于其他人来说可能不太多。 diff --git a/chapter_computer-vision/fine-tuning.md b/chapter_computer-vision/fine-tuning.md index a2939870b..9e75d4c3f 100644 --- a/chapter_computer-vision/fine-tuning.md +++ b/chapter_computer-vision/fine-tuning.md @@ -1,19 +1,19 @@ # 微调 :label:`sec_fine_tuning` -在前面的章节中,我们讨论了如何在 Fashion-Mnist 训练数据集上训练模型,只有 60000 张图像。我们还描述了 iMagenNet,这是学术界中使用最广泛的大型图像数据集,它拥有 1000 多万张图像和 1000 个对象。但是,我们通常遇到的数据集的大小介于两个数据集中的大小之间。 +在前面的章节中,我们讨论了如何在 Fashion-MNIST 训练数据集上训练模型,只有 60000 张图像。我们还描述了 ImageNet,这是学术界中使用最广泛的大型图像数据集,它拥有 1000 多万张图像和 1000 个对象。但是,我们通常遇到的数据集的大小介于两个数据集中的大小之间。 -假设我们想识别图片中不同类型的椅子,然后向用户推荐购买链接。一种可能的方法是首先识别 100 把普通椅子,为每把椅子拍摄 1000 张不同角度的图像,然后在收集的影像数据集上训练一个分类模型。尽管这个椅子数据集可能大于 Fashion-Mnist 数据集,但实例数量仍然不到 iMagenet 中的十分之一。这可能会导致这个椅子数据集上适合 iMagenNet 的复杂模型过度拟合。此外,由于训练示例数量有限,训练模型的准确性可能无法满足实际要求。 +假设我们想识别图片中不同类型的椅子,然后向用户推荐购买链接。一种可能的方法是首先识别 100 把普通椅子,为每把椅子拍摄 1000 张不同角度的图像,然后在收集的影像数据集上训练一个分类模型。尽管这个椅子数据集可能大于 Fashion-MNIST 数据集,但实例数量仍然不到 ImageNet 中的十分之一。这可能会导致这个椅子数据集上适合 ImageNet 的复杂模型过度拟合。此外,由于训练示例数量有限,训练模型的准确性可能无法满足实际要求。 -为了解决上述问题,一个显而易见的解决方案是收集更多的数据。但是,收集和标记数据可能需要大量的时间和金钱。例如,为了收集 iMagenet 数据集,研究人员从研究资金中花费了数百万美元。尽管目前的数据收集成本已大幅降低,但这一成本仍不能忽视。 +为了解决上述问题,一个显而易见的解决方案是收集更多的数据。但是,收集和标记数据可能需要大量的时间和金钱。例如,为了收集 ImageNet 数据集,研究人员从研究资金中花费了数百万美元。尽管目前的数据收集成本已大幅降低,但这一成本仍不能忽视。 -另一种解决方案是应用 * 传输学习 * 将从 * 源数据集 * 学到的知识传输到 * 目标数据集 *。例如,尽管 iMagenet 数据集中的大多数图像与椅子无关,但在此数据集上训练的模型可能会提取更常规的图像特征,这有助于识别边缘、纹理、形状和对象合成。这些类似的功能也可能有效地识别椅子。 +另一种解决方案是应用 *迁移学习* 将从 *源数据集* 学到的知识迁移到 *目标数据集*。例如,尽管 ImageNet 数据集中的大多数图像与椅子无关,但在此数据集上训练的模型可能会提取更常规的图像特征,这有助于识别边缘、纹理、形状和对象合成。这些类似的功能也可能有效地识别椅子。 ## 步骤 -在本节中,我们将介绍转移学习中的常见技巧 : *fine-tuning*. As shown in :numref:`fig_finetune`,微调包括以下四个步骤: +在本节中,我们将介绍转移学习中的常见技巧 : *微调*(fine-tuning). As shown in :numref:`fig_finetune`,微调包括以下四个步骤: -1. 在源数据集(例如 iMagenet 数据集)上预训练神经网络模型,即 * 源模型 *。 +1. 在源数据集(例如 ImageNet 数据集)上预训练神经网络模型,即 * 源模型 *。 1. 创建一个新的神经网络模型,即 * 目标模型 *。这将复制源模型上的所有模型设计及其参数,但输出层除外。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集。我们还假设源模型的输出图层与源数据集的标签密切相关;因此不在目标模型中使用该图层。 1. 向目标模型添加输出图层,其输出数量是目标数据集中的类别数。然后随机初始化该层的模型参数。 1. 在目标数据集(如椅子数据集)上训练目标模型。输出图层将从头开始进行训练,而所有其他图层的参数将根据源模型的参数进行微调。 @@ -25,7 +25,7 @@ ## 热狗识别 -让我们通过具体案例演示微调:热狗识别。我们将在一个小型数据集上微调 ReSnet 模型,该数据集已在 iMagenet 数据集上进行了预训练。这个小型数据集包含数千张带热狗和不带热狗的图像。我们将使用微调模型来识别图像中的热狗。 +让我们通过具体案例演示微调:热狗识别。我们将在一个小型数据集上微调 ResNet 模型,该数据集已在 ImageNet 数据集上进行了预训练。这个小型数据集包含数千张包含热狗和不包含热狗的图像。我们将使用微调模型来识别图像中是否包含热狗。 ```{.python .input} %matplotlib inline @@ -47,7 +47,7 @@ import torchvision import os ``` -### 阅读数据集 +### 获取数据集 我们使用的热狗数据集取自在线图片。该数据集包含 1400 张包含热狗的正面类图像以及包含其他食物的尽可能多的负面级图像。两个课程的 1000 张图片用于训练,其余的则用于测试。 @@ -129,7 +129,7 @@ test_augs = torchvision.transforms.Compose([ ### 定义和初始化模型 -我们使用在 iMagenet 数据集上预训练的 Resnet-18 作为源模型。在这里,我们指定 `pretrained=True` 以自动下载预训练的模型参数。如果首次使用此模型,则需要互联网连接才能下载。 +我们使用在 ImageNet 数据集上预训练的 Resnet-18 作为源模型。在这里,我们指定 `pretrained=True` 以自动下载预训练的模型参数。如果首次使用此模型,则需要互联网连接才能下载。 ```{.python .input} pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) @@ -157,13 +157,13 @@ pretrained_net.output pretrained_net.fc ``` -作为一个完全连接的层,它将 RESNet 的最终全球平均池输出转换为 iMagenet 数据集的 1000 个类输出。然后,我们构建一个新的神经网络作为目标模型。它的定义方式与预训练源模型的定义方式相同,只是最终图层中的输出数量被设置为目标数据集中的类数(而不是 1000 个)。 +作为一个完全连接的层,它将 ResNet 的最终全球平均池输出转换为 ImageNet 数据集的 1000 个类输出。然后,我们构建一个新的神经网络作为目标模型。它的定义方式与预训练源模型的定义方式相同,只是最终图层中的输出数量被设置为目标数据集中的类数(而不是 1000 个)。 -在下面的代码中,目标模型实例 finetune_net 的成员变量特征中的模型参数被初始化为源模型相应层的模型参数。由于功能中的模型参数是在 iMagenNet 数据集上预训练的,并且足够好,因此通常只需要较小的学习速率即可微调这些参数。 +在下面的代码中,目标模型实例 finetune_net 的成员变量特征中的模型参数被初始化为源模型相应层的模型参数。由于功能中的模型参数是在 ImageNet 数据集上预训练的,并且足够好,因此通常只需要较小的学习速率即可微调这些参数。 成员变量输出中的模型参数是随机初始化的,通常需要更高的学习速率才能从头开始训练。假设 Trainer 实例中的学习速率为,我们将迭代中成员变量输出中模型参数的学习速率设置为 10。 -在下面的代码中,初始化目标模型实例 `finetune_net` 输出层之前的模型参数,以对源模型中相应层的参数进行建模。由于这些模型参数是通过 iMagenet 上的预训练获得的,因此它们很有效。因此,我们只能使用较小的学习速率进行 * 微调 * 这样的预训练参数。相比之下,输出层中的模型参数是随机初始化的,通常需要从头开始学习更高的学习速率。让基本学习速率为 $\eta$,学习速率 $10\eta$ 将用于迭代输出层中的模型参数。 +在下面的代码中,初始化目标模型实例 `finetune_net` 输出层之前的模型参数,以对源模型中相应层的参数进行建模。由于这些模型参数是通过 ImageNet 上的预训练获得的,因此它们很有效。因此,我们只能使用较小的学习速率进行 * 微调 * 这样的预训练参数。相比之下,输出层中的模型参数是随机初始化的,通常需要从头开始学习更高的学习速率。让基本学习速率为 $\eta$,学习速率 $10\eta$ 将用于迭代输出层中的模型参数。 ```{.python .input} finetune_net = gluon.model_zoo.vision.resnet18_v2(classes=2) @@ -229,7 +229,7 @@ def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5, devices) ``` -我们将基本学习速率设置为小值,以便 * 微调 * 通过预训练获得的模型参数。根据之前的设置,我们将使用高十倍的学习率从头开始训练目标模型的输出层参数。 +我们将基本学习速率设置为小值,以便 *微调* 通过预训练获得的模型参数。根据之前的设置,我们将使用高十倍的学习率从头开始训练目标模型的输出层参数。 ```{.python .input} train_fine_tuning(finetune_net, 0.01) @@ -257,7 +257,7 @@ train_fine_tuning(scratch_net, 5e-4, param_group=False) 正如我们所看到的,微调模型在同一纪元中往往表现更好,因为它的初始参数值更有效。 -## 摘要 +## 小结 * 转移学习将从源数据集中学到的知识传输到目标数据集。微调是转移学习的常见技巧。 * 目标模型将从源模型中复制所有模型设计及其参数,但输出层除外,并根据目标数据集对这些参数进行微调。相比之下,需要从头开始训练目标模型的输出层。 diff --git a/chapter_computer-vision/image-augmentation.md b/chapter_computer-vision/image-augmentation.md index abc659963..10d54b740 100644 --- a/chapter_computer-vision/image-augmentation.md +++ b/chapter_computer-vision/image-augmentation.md @@ -1,9 +1,7 @@ # 图像增强 :label:`sec_image_augmentation` -在 :numref:`sec_alexnet` 中,我们提到大型数据集是各种应用程序中深度神经网络成功的先决条件。 -*图片增强 * -在对训练图像进行一系列随机更改之后,会生成类似但截然不同的训练示例,从而扩大了训练集的规模。或者,图像增强的动机可能是,训练示例的随机调整使模型减少了对某些属性的依赖,从而提高了它们的泛化能力。例如,我们可以用不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,从而减少模型对物体位置的依赖性。我们还可以调整亮度和颜色等因素,以降低模型对颜色的敏感度。当时,图像增强对 AleXNet 的成功可能是必不可少的。在本节中,我们将讨论这种在计算机视觉中广泛使用的技术。 +在 :numref:`sec_alexnet` 中,我们提到大型数据集是各种应用程序中深度神经网络成功的先决条件。 *图片增强* 在对训练图像进行一系列随机更改之后,会生成类似但截然不同的训练示例,从而扩大了训练集的规模。或者,图像增强的动机可能是,训练示例的随机调整使模型减少了对某些属性的依赖,从而提高了它们的泛化能力。例如,我们可以用不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,从而减少模型对物体位置的依赖性。我们还可以调整亮度和颜色等因素,以降低模型对颜色的敏感度。当时,图像增强对 AleXNet 的成功可能是必不可少的。在本节中,我们将讨论这种在计算机视觉中广泛使用的技术。 ```{.python .input} %matplotlib inline @@ -131,9 +129,9 @@ color_aug = torchvision.transforms.ColorJitter( apply(img, color_aug) ``` -### 结合多个图像增强方法 +### 叠加多个图像增强方法 -实际上,我们将结合多种图像增强方法。例如,我们可以组合上面定义的不同图像增强方法,并通过 `Compose` 实例将它们应用到每个图像。 +实际应用中我们会将多个图像增广方法叠加使。例如,我们可以组合上面定义的不同图像增强方法,并通过 `Compose` 实例将它们应用到每个图像。 ```{.python .input} augs = gluon.data.vision.transforms.Compose([ @@ -148,7 +146,7 @@ augs = torchvision.transforms.Compose([ apply(img, augs) ``` -## 使用图像增强进行培训 +## 使用图像增强进行训练 让我们使用图像增强来训练模型。在这里,我们使用 CIFAR-10 数据集而不是我们之前使用的 Fashion-Mnist 数据集。这是因为 Fashion-Mnist 数据集中对象的位置和大小已规范化,而 CIFAR-10 数据集中对象的颜色和大小差异更显著。CIFAR-10 数据集中的前 32 个训练图像如下所示。 @@ -213,7 +211,7 @@ def load_cifar10(is_train, augs, batch_size): return dataloader ``` -### 多 GPU 培训 +### 多 GPU 训练 我们在 CIFAR-10 数据集上训练 :numref:`sec_resnet` 的 Resnet-18 模型。回想一下 :numref:`sec_multi_gpu_concise` 中对多 GPU 培训的介绍。在下面,我们定义了一个函数来使用多个 GPU 来训练和评估模型。 @@ -358,7 +356,7 @@ def train_with_data_aug(train_augs, test_augs, net, lr=0.001): train_with_data_aug(train_augs, test_augs, net) ``` -## 摘要 +## 小结 * 图像增强基于现有训练数据生成随机图像,以提高模型的概化能力。 * 为了在预测期间获得明确的结果,我们通常只将图像增强应用于训练示例,在预测期间不会将图像增强与随机操作结合使用。 From 4ff31430e043a49b1f080103e3a7d3bb3651322a Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Thu, 6 May 2021 02:31:20 +0800 Subject: [PATCH 080/103] Chapter recurrent neural networks/language models and dataset (#783) * fix errors in 8.3. Language Models and the Dataset * fix errors in 8.3.1. Learning a Language Model * fix errors in 8.3.2. Markov Models and n-grams * fix errors in 8.3.3. Natural Language Statistics * fix errors in 8.3.4. Reading Long Sequence Data the end of 8.3. * fix errors and add terminology for 8.3. Language Models and the Dataset --- TERMINOLOGY.md | 10 ++ .../language-models-and-dataset.md | 104 +++++++++--------- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/TERMINOLOGY.md b/TERMINOLOGY.md index 5fe3910b3..f1ad79604 100644 --- a/TERMINOLOGY.md +++ b/TERMINOLOGY.md @@ -130,6 +130,8 @@ 困惑度,perplexity +拉普拉斯平滑,Laplace smoothing + 连结,concatenate 类,class @@ -160,6 +162,8 @@ 平均池化层,average pooling layer +齐普夫定律,Zipf's law + 欠拟合,underfitting 情感分析,sentiment analysis @@ -184,8 +188,12 @@ 数据样本,data instance +顺序分区,sequential partitioning + softmax回归,softmax regression +随机采样,random sampling + 损失函数,loss function 双向循环神经网络,bidirectional recurrent neural network @@ -208,6 +216,8 @@ softmax回归,softmax regression 调参,tune hyper-parameter +停用词,stop words + 通道,channel 凸优化,convex optimization diff --git a/chapter_recurrent-neural-networks/language-models-and-dataset.md b/chapter_recurrent-neural-networks/language-models-and-dataset.md index a156937f6..9e06bd3c7 100644 --- a/chapter_recurrent-neural-networks/language-models-and-dataset.md +++ b/chapter_recurrent-neural-networks/language-models-and-dataset.md @@ -1,47 +1,49 @@ # 语言模型和数据集 :label:`sec_language_model` -在 :numref:`sec_text_preprocessing` 中,我们了解了如何将文本数据映射到标记中,其中这些标记可以被视为一系列离散的观测,例如单词或字符。假设长度为$T$的文本序列中的标记依次为$x_1, x_2, \ldots, x_T$。然后,在文本序列中,$x_t$($1 \leq t \leq T$)可以被认为是时间步$t$处的观测或标签。给定这样的文本序列,*语言模型*(language model)的目标是估计序列的联合概率 +在 :numref:`sec_text_preprocessing` 中,我们了解了如何将文本数据映射到标记中,其中这些标记可以被视为一系列离散的观测,例如单词或字符。假设长度为 $T$ 的文本序列中的标记依次为 $x_1, x_2, \ldots, x_T$。然后,在文本序列中,$x_t$($1 \leq t \leq T$) 可以被认为是时间步 $t$ 处的观测或标签。给定这样的文本序列,*语言模型*(language model)的目标是估计序列的联合概率 $$P(x_1, x_2, \ldots, x_T).$$ -语言模型非常有用。例如,一个理想的语言模型能够自己生成自然文本,只需一次给出一个标记$x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1)$。与猴子使用打字机非常不同的是,从这样的模型中出现的所有文本都将作为自然语言来传递,例如英语文本。此外,只需将文本限制在前面的对话片断上,就足以生成一个有意义的对话。显然,我们离设计这样的系统还很远,因为它需要“理解”文本,而不仅是生成在语法上合理的内容。 +语言模型是非常有用的。例如,只需要一次抽取一个标记 $x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1)$,一个理想的语言模型就能够基于自己生成自然文本。与猴子使用打字机完全不像的是,从这样的模型中出现的所有文本都将作为自然语言来传递,例如英语文本。此外,只需要基于前面的对话片断中的文本,就足以生成一个有意义的对话。显然,离设计这样的系统还很遥远,因为它需要“理解”文本,而不仅仅是生成在语法上合理的内容。 -尽管如此,语言模型即使在有限的形式下也是非常有用的。例如,在文档摘要生成算法中,“狗咬人”比“人咬狗”频繁得多,或者“我想吃奶奶”是一个相当令人不安的语句,而“我想吃,奶奶”要温和得多。 +尽管如此,即使在有限的形式下,语言模型也是非常有用的。例如,短语“to recognize speech”和“to wreck a nice beach”听起来非常相似,因为这会导致语音识别中的歧义,然而这很容易通过语言模型来解决,因为第二种翻译感觉怪怪的。同样,在文档摘要生成算法中,“狗咬人”比“人咬狗”出现机会要频繁得多,或者“我想吃奶奶”是一个相当令人不安的语句,而“我想吃,奶奶”要温和得多。 ## 学习语言模型 -显而易见的问题是,我们应该如何建模一个文档,或者一串标记。假设我们在单词级别对文本数据进行标记化。我们可以求助于我们在 :numref:`sec_sequence` 中应用于序列模型的分析。让我们从应用基本概率规则开始: +显而易见,我们面对的问题是如何对一个文档,甚至是一串标记进行建模。假设在单词级别对文本数据进行标记化,我们可以求助于在 :numref:`sec_sequence` 中对序列模型的分析。让我们从基本概率规则开始: $$P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_1, \ldots, x_{t-1}).$$ -例如,文本序列包含四个单词的概率将被给出: +例如,文本序列包含了四个单词的概率是: $$P(\text{deep}, \text{learning}, \text{is}, \text{fun}) = P(\text{deep}) P(\text{learning} \mid \text{deep}) P(\text{is} \mid \text{deep}, \text{learning}) P(\text{fun} \mid \text{deep}, \text{learning}, \text{is}).$$ -为了计算语言模型,我们需要计算单词的概率和给定前面几个单词时出现该单词的条件概率。这样的概率本质上是语言模型参数。 +为了计算语言模型,我们需要计算单词的概率和给定前面几个单词时出现该单词的条件概率。这样的概率本质上就是语言模型的参数。 -这里,我们假设训练数据集是一个大型文本语料库,比如所有维基百科条目,[古登堡计划](https://en.wikipedia.org/wiki/Project_Gutenberg),以及发布在网络上的所有文本。可以根据训练数据集中给定词的相对词频来计算词的概率。例如,可以将估计值$\hat{P}(\text{deep})$计算为任何以单词“Deep”开头的句子的概率。一种稍微不太准确的方法是统计单词“Deep”的所有出现次数,然后将其除以语料库中的单词总数。这很有效,特别是对于频繁出现的单词。接下来,我们可以尝试估计 +这里,我们假设训练数据集是一个大型文本语料库,比如,所有维基百科的条目,[古登堡计划](https://en.wikipedia.org/wiki/Project_Gutenberg),以及发布在网络上的所有文本。训练数据集中词的概率可以根据给定词的相对词频来计算。例如,可以将估计值 $\hat{P}(\text{deep})$ 计算为任何以单词“Deep”开头的句子的概率。一种稍稍不太精确的方法是统计单词“Deep”在数据集中的出现次数,然后将其除以整个语料库中的单词总数。这很有效,特别是对于频繁出现的单词。接下来,我们可以尝试估计 $$\hat{P}(\text{learning} \mid \text{deep}) = \frac{n(\text{deep, learning})}{n(\text{deep})},$$ -其中$n(x)$和$n(x, x')$分别是单个单词和连续单词对的出现次数。不幸的是,由于“深度学习”的出现频率要低得多,所以估计词对的概率要困难得多。特别是,对于一些不寻常的单词组合,可能很难找到足够的出现次数来获得准确的估计。对于三个字的组合和以后的情况,情况变得更糟了。将会有许多可能在数据集中看不到的,但又看似合理的三字组合。除非我们提供一些解决方案来将这些单词组合指定为非零计数,否则我们将无法在语言模型中使用它们。如果数据集很小,或者如果单词非常罕见,我们可能甚至找不到一次出现。 +其中 $n(x)$ 和 $n(x, x')$ 分别是单个单词和连续单词对的出现次数。不幸的是,由于“深度学习”(deep learning)的出现频率要低得多,所以估计单词对的概率要困难得多。特别是,对于一些不常见的单词组合,可能找到足够的出现次数来获得准确的估计将很困难。而对于三个字或者更多字的组合情况会变得更糟。将会存在许多合理的三字组合,但是在数据集中可能找不到。除非我们提供一些解决方案来将这些单词组合指定为非零计数,否则将无法在语言模型中使用它们。如果数据集很小,或者单词非常罕见,我们可能甚至找不到这类单词的一次出现。 -一种常见的策略是执行某种形式的*拉普拉斯平滑*(Laplace smoothing)。解决方案是在所有计数中添加一个小常量。用$n$表示训练集中的单词总数,用$m$表示唯一单词的数量。此解决方案有助于处理个例问题,例如通过: +一种常见的策略是执行某种形式的 *拉普拉斯平滑*(Laplace smoothing)。解决方案是在所有计数中添加一个小常量。用 $n$ 表示训练集中的单词总数,用 $m$ 表示唯一单词的数量。此解决方案有助于处理单元素问题,例如通过: -$$\begin{aligned} - \hat{P}(x) & = \frac{n(x) + \epsilon_1/m}{n + \epsilon_1}, \\ - \hat{P}(x' \mid x) & = \frac{n(x, x') + \epsilon_2 \hat{P}(x')}{n(x) + \epsilon_2}, \\ - \hat{P}(x'' \mid x,x') & = \frac{n(x, x',x'') + \epsilon_3 \hat{P}(x'')}{n(x, x') + \epsilon_3}. -\end{aligned}$$ +$$ +\begin{aligned} + \hat{P}(x) & = \frac{n(x) + \epsilon_1/m}{n + \epsilon_1}, \\ + \hat{P}(x' \mid x) & = \frac{n(x, x') + \epsilon_2 \hat{P}(x')}{n(x) + \epsilon_2}, \\ + \hat{P}(x'' \mid x,x') & = \frac{n(x, x',x'') + \epsilon_3 \hat{P}(x'')}{n(x, x') + \epsilon_3}. +\end{aligned} +$$ -其中,$\epsilon_1,\epsilon_2$和$\epsilon_3$是超参数。以$\epsilon_1$为例:当为$\epsilon_1 = 0$时,不应用平滑;当$\epsilon_1$接近正无穷大时,$\hat{P}(x)$接近均匀概率$1/m$。以上是其他技术可以实现的 :cite:`Wood.Gasthaus.Archambeau.ea.2011` 的一个相当原始的变体。 +其中,$\epsilon_1,\epsilon_2$ 和 $\epsilon_3$ 是超参数。以 $\epsilon_1$ 为例:当为 $\epsilon_1 = 0$ 时,不应用平滑;当 $\epsilon_1$ 接近正无穷大时,$\hat{P}(x)$ 接近均匀概率 $1/m$。上面的公式是采用其他技术实现了 :cite:`Wood.Gasthaus.Archambeau.ea.2011` 的一个相当原始的变体。 -不幸的是,像这样的模型很快就会变得笨拙,原因如下:首先,我们需要存储所有计数。第二,这完全忽略了单词的意思。例如,“猫”和“猫科动物”应该出现在相关的上下文中。很难将这些模型调整到额外的上下文中,而基于深度学习的语言模型很适合考虑到这一点。最后,长单词序列几乎肯定是新出现的,因此简单地统计过往看到单词序列频率的模型肯定表现不佳。 +不幸的是,像这样的模型很快就会因为下面的原因变得无效:首先,我们需要存储所有的计数。第二,这完全忽略了单词的意思。例如,“猫”和“猫科动物”应该出现在相关的上下文中。很难将这些模型调整到额外的上下文中,而基于深度学习的语言模型很适合考虑到这一点。最后,长单词序列几乎肯定是新出现的,因此简单地统计先前看到的单词序列频率的模型面对这种问题肯定是表现不佳的。 ## 马尔可夫模型与$n$元语法 -在我们讨论基于深度学习的解决方案之前,我们需要更多的术语和概念。回想一下我们在 :numref:`sec_sequence` 中对马尔可夫模型的讨论。让我们将其应用于语言建模。序列上的分布满足一阶马尔可夫性质$P(x_{t+1} \mid x_t, \ldots, x_1) = P(x_{t+1} \mid x_t)$的。阶数越高,对应的依赖关系就越长。这导致了我们可以应用于序列建模的许多近似: +在讨论基于深度学习的解决方案之前,我们需要更多的术语和概念。回想一下我们在 :numref:`sec_sequence` 中对马尔可夫模型的讨论。让我们将其应用于语言建模。如果 $P(x_{t+1} \mid x_t, \ldots, x_1) = P(x_{t+1} \mid x_t)$,则序列上的分布满足一阶马尔可夫性质。阶数越高,对应的依赖关系就越长。这种性质推导出了许多可以应用于序列建模的近似: $$ \begin{aligned} @@ -51,11 +53,11 @@ P(x_1, x_2, x_3, x_4) &= P(x_1) P(x_2 \mid x_1) P(x_3 \mid x_1, x_2) P(x_4 \end{aligned} $$ -涉及一个、两个和三个变量的概率公式通常分别称为“单变量模型”(unigram)、“双变量模型”(bigram)和“三变量模型”(trigram)。在下面,我们将学习如何设计更好的模型。 +涉及一个、两个和三个变量的概率公式通常分别称为“一元语法”(unigram)、“二元语法”(bigram)和“三元语法”(trigram)。下面,我们将学习如何设计更好的模型。 ## 自然语言统计 -让我们看看这是如何对真实数据起作用的。我们根据 :numref:`sec_text_preprocessing` 中介绍的时光机器数据集构建词汇表,并打印最常用的10个单词。 +让我们看看如何对真实数据起作用。根据 :numref:`sec_text_preprocessing` 中介绍的时光机器数据集构建词汇表,并打印最常用的10个单词。 ```{.python .input} from d2l import mxnet as d2l @@ -81,14 +83,13 @@ import random ```{.python .input} #@tab all tokens = d2l.tokenize(d2l.read_time_machine()) -# 因为每个文本行不一定是一个句子或一个段落, -# 我们连接所有文本行 +# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行连接到一起 corpus = [token for line in tokens for token in line] vocab = d2l.Vocab(corpus) vocab.token_freqs[:10] ``` -正如我们所看到的,最流行的词实际上看起来很无聊。它们通常被称为“停用词”(stop words),因此可以被过滤掉。尽管如此,它们仍然有意义,我们仍然会使用它们。此外,很明显,词频衰减得相当快。第$10$个最常用单词的词频还不到最流行单词词频的$1/5$。为了得到一个更好的概念,我们画出了词频图表。 +正如我们所看到的,事实上最流行的词看起来很无聊。它们通常被称为“停用词”(stop words),因此可以被过滤掉。尽管如此,它们仍然有意义,我们仍然会使用它们。此外,显而易见的是词频衰减的速度得相当地快。第 $10$ 个最常用单词的词频还不到最流行单词词频的 $1/5$。为了更好地理解,我们画出了词频图。 ```{.python .input} #@tab all @@ -96,7 +97,8 @@ freqs = [freq for token, freq in vocab.token_freqs] d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log') ``` -我们在这里看到了一些非常基本的东西:词频以一种明确的方式迅速衰减。将前几个单词作为例外处理后,所有剩余的单词大致沿着对数曲线上的一条直。这意味着单词符合*齐普夫定律*(Zipf's law),即第$i$个最常用单词的频率$n_i$为: + +在这里我们看到了一些非常基本的东西:词频以一种明确的方式迅速衰减。将前几个单词作为例外消除后,剩余的所有单词大致遵循双对数坐标图上的一条直线。这意味着单词符合 *齐普夫定律*(Zipf's law),即第 $i$ 个最常用单词的频率 $n_i$ 为: $$n_i \propto \frac{1}{i^\alpha},$$ :eqlabel:`eq_zipf_law` @@ -105,7 +107,7 @@ $$n_i \propto \frac{1}{i^\alpha},$$ $$\log n_i = -\alpha \log i + c,$$ -其中$\alpha$是表征分布的指数,$c$是常数。如果我们想要通过计数统计和平滑来建模单词,这应该已经让我们停下来了。毕竟,我们会大大高估尾部的频率,也就是所谓的不常用单词。但是其他的单词组合呢,比如二元语法、三元语法等等呢?让我们看看双字频率是否与单字频率的行为方式相同。 +其中 $\alpha$ 是表征分布的指数,$c$ 是常数。这应该让我们明白想要通过计数统计和平滑来建模单词是不可行的。结果,我们会大大高估尾部单词的频率,也就是所谓的不常用单词。但是其他的单词组合,比如二元语法、三元语法等等,又会如何呢?让我们看看二元语法的频率是否与一元语法的频率表现相同的行为方式。 ```{.python .input} #@tab all @@ -135,36 +137,37 @@ d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x', legend=['unigram', 'bigram', 'trigram']) ``` -这个数字相当令人兴奋,原因有很多。首先,除了单字词,单词序列似乎也遵循齐普夫定律,尽管 :eqref:`eq_zipf_law` 中的指数$\alpha$更小,这取决于序列长度。其次,$n$元组的数量并没有那么大。这给了我们希望,语言中有相当多的结构。第三,很多$n$元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。相反,我们将使用基于深度学习的模型。 +这张图相当令人激动,原因则有很多。首先,除了单字词,单词序列似乎也遵循齐普夫定律,尽管公式 :eqref:`eq_zipf_law` 中的指数 $\alpha$ 更小(指数的大小取决于序列的长度)。其次,词典中 $n$ 元组的数量并没有那么大,说明语言中存在相当多的结构,这给了我们应用模型希望。第三,很多 $n$ 元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。作为代替,我们将使用基于深度学习的模型。 ## 读取长序列数据 -由于序列数据本质上是连续的,我们需要解决处理这带来的问题。我们在 :numref:`sec_sequence` 以一种相当特别的方式做到了这一点。当序列变得太长而不能被模型一次全部处理时,我们可能希望拆分这样的序列以供阅读。现在让我们描述一下总体策略。在介绍该模型之前,假设我们将使用神经网络来训练语言模型,其中该网络一次处理具有预定义长度的一小批序列,例如$n$个时间步。现在的问题是如何随机读取小批量的特征和标签。 +由于序列数据本质上是连续的,我们需要在处理数据时解决这个问题。我们在 :numref:`sec_sequence` 以一种相当特定的方式做到了这一点。当序列变得太长而不能被模型一次性全部处理时,我们可能希望拆分这样的序列以供阅读。现在让我们描述一下总体策略。在介绍该模型之前,假设我们将使用神经网络来训练语言模型,其中该网络一次处理具有预定义长度的一小批序列,例如 $n$ 个时间步。现在的问题是如何随机读取小批量的特征和标签。 -首先,由于文本序列可以是任意长的,例如整个“时光机器”书,我们可以将这样长的序列划分为具有相同时间步数的子序列。当训练我们的神经网络时,子序列的小批量将被输入到模型中。假设网络一次处理$n$个时间步的子序列。 :numref:`fig_timemachine_5gram` 画出了从原始文本序列获得子序列的所有不同方式,其中$n=5$和每个时间步的标记对应于一个字符。请注意,我们有相当大的自由度,因为我们可以选择指示初始位置的任意偏移量。 +首先,由于文本序列可以是任意长的,例如整个“时光机器”书。我们可以将这样长的序列划分为具有相同时间步数的子序列。当训练我们的神经网络时,这样的小批量子序列将被输入到模型中。假设模型一次只处理具有 $n$ 个时间步的子序列。 :numref:`fig_timemachine_5gram` 画出了从原始文本序列获得子序列的所有不同的方式,其中 $n=5$ 和每个时间步的标记对应于一个字符。请注意,我们有相当大的自由度,因为我们可以选择指示初始位置的任意偏移量。 ![分割文本时,不同的偏移量会导致不同的子序列。](../img/timemachine-5gram.svg) :label:`fig_timemachine_5gram` -因此,我们应该从 :numref:`fig_timemachine_5gram` 中选择哪一个呢?其实,他们都一样好。然而,如果我们只选择一个偏移量,那么用于训练网络的所有可能子序列的覆盖范围都是有限的。因此,我们可以从随机偏移量开始划分序列,以获得*覆盖*(coverage)和*随机性*(randomness)。在下面,我们将描述如何实现*随机采样*和*顺序分区*策略。 +因此,我们应该从 :numref:`fig_timemachine_5gram` 中选择哪一个呢?事实上,他们都一样的好。然而,如果我们只选择一个偏移量,那么用于训练网络的所有可能子序列的覆盖范围将是有限的。因此,我们可以从随机偏移量开始划分序列,以同时获得 *覆盖性*(coverage)和 *随机性*(randomness)。下面,我们将描述如何实现 *随机采样*(random sampling)和 *顺序分区*(sequential partitioning)策略。 ### 随机采样 -在随机采样中,每个样本都是在原始长序列上任意捕获的子序列。迭代期间来自两个相邻随机小批量的子序列不一定在原始序列上相邻。对于语言建模,目标是根据我们到目前为止看到的标记来预测下一个标记,因此标签是原始序列移位了一个标记。 +在随机采样中,每个样本都是在原始长序列上任意捕获的子序列。在迭代期间,来自两个相邻的、随机的小批量中的子序列不一定在原始序列上相邻。对于语言建模,目标是根据到目前为止我们看到的标记来预测下一个标记,因此标签是移位了一个标记的原始序列。 -下面的代码每次从数据随机生成一个小批量。这里,参数 `batch_size` 指定每个小批量中的子序列样本数目, `num_steps` 是每个子序列中预定义的时间步数。 +下面的代码每次都从数据中随机生成一个小批量。这里,参数 `batch_size` 指定了每个小批量中的子序列样本的数目,`num_steps` 是每个子序列中预定义的时间步数。 ```{.python .input} #@tab all def seq_data_iter_random(corpus, batch_size, num_steps): #@save - """使用随机抽样生成一小批子序列。""" + """使用随机抽样生成一个小批量子序列。""" # 从随机偏移量(包括`num_steps - 1`)开始对序列进行分区 corpus = corpus[random.randint(0, num_steps - 1):] # 减去1,因为我们需要考虑标签 num_subseqs = (len(corpus) - 1) // num_steps # 长度为`num_steps`的子序列的起始索引 initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) - # 在随机抽样中,迭代过程中两个相邻随机小批量的子序列不一定在原始序列上相邻 + # 在随机抽样中, + # 迭代过程中两个相邻的、随机的小批量中的子序列不一定在原始序列上相邻 random.shuffle(initial_indices) def data(pos): @@ -180,7 +183,7 @@ def seq_data_iter_random(corpus, batch_size, num_steps): #@save yield d2l.tensor(X), d2l.tensor(Y) ``` -让我们手动生成一个从0到34的序列。我们设批量大小和时间步数分别为2和5。这意味着我们可以生成$\lfloor (35 - 1) / 5 \rfloor= 6$个特征标签子序列对。小批量大小为2时,我们只能得到3个小批量。 +让我们手动生成一个从 $0$ 到 $34$ 的序列。假设批量大小为 $2$ 和时间步数为 $5$。这意味着可以生成 $\lfloor (35 - 1) / 5 \rfloor= 6$ 个“特征-标签”子序列对。设置小批量大小为 $2$ 时,我们只能得到 $3$ 个小批量。 ```{.python .input} #@tab all @@ -191,12 +194,12 @@ for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5): ### 顺序分区 -除了对原始序列进行随机抽样外,我们还可以保证迭代过程中两个相邻小批量的子序列在原始序列上是相邻的。这种策略在对小批进行迭代时保留了拆分子序列的顺序,因此称为顺序分区。 +除了对原始序列进行随机抽样外,我们还可以保证在迭代过程中两个相邻的小批量中的子序列在原始序列上是相邻的。这种策略在基于小批量进行迭代时保留了拆分的子序列的顺序,因此称为顺序分区。 ```{.python .input} #@tab mxnet, pytorch def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save - """使用顺序分区生成一小批子序列。""" + """使用顺序分区生成一个小批量子序列。""" # 从随机偏移量开始划分序列 offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size @@ -213,7 +216,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save ```{.python .input} #@tab tensorflow def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save - """使用顺序分区生成一小批子序列。""" + """使用顺序分区生成一个小批量子序列。""" # 从随机偏移量开始划分序列 offset = random.randint(0, num_steps) num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size @@ -228,7 +231,7 @@ def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save yield X, Y ``` -使用相同的设置,让我们为通过顺序分区读取的每个小批量的子序列打印特征`X`和标签`Y`。请注意,迭代期间来自两个相邻小批量的子序列实际上在原始序列上是相邻的。 +基于相同的设置,让我们把通过顺序分区读取的每个小批量的子序列的特征 `X` 和标签 `Y` 打印出来。请注意,迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。 ```{.python .input} #@tab all @@ -254,13 +257,13 @@ class SeqDataLoader: #@save return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps) ``` -最后,我们定义了一个函数 `load_data_time_machine` ,它同时返回数据迭代器和词表,因此我们可以与其他带有 `load_data` 前缀的函数(如 :numref:`sec_fashion_mnist` 中定义的 `d2l.load_data_fashion_mnist` )类似地使用它。 +最后,我们定义了一个函数 `load_data_time_machine` ,它同时返回数据迭代器和词汇表,因此可以与其他带有 `load_data` 前缀的函数(如 :numref:`sec_fashion_mnist` 中定义的 `d2l.load_data_fashion_mnist` )类似地使用。 ```{.python .input} #@tab all def load_data_time_machine(batch_size, num_steps, #@save use_random_iter=False, max_tokens=10000): - """返回时光机器数据集的迭代器和词表。""" + """返回时光机器数据集的迭代器和词汇表。""" data_iter = SeqDataLoader( batch_size, num_steps, use_random_iter, max_tokens) return data_iter, data_iter.vocab @@ -269,22 +272,22 @@ def load_data_time_machine(batch_size, num_steps, #@save ## 小结 * 语言模型是自然语言处理的关键。 -* $n$元语法通过截断相关性,为处理长序列提供了一种方便的模型。 -* 长序列有一个问题,那就是它们很少出现或从不出现。 -* 齐普夫定律不仅规定了单字的单词分布,而且还规定了其他$n$元语法的单词分布。 -* 通过拉普拉斯平滑法可以有效地处理不常见的、结构复杂且频率不够词组。 -* 读取长序列的主要选择是随机采样和顺序分区。后者可以保证迭代过程中来自两个相邻小批量的子序列在原始序列上是相邻的。 +* $n$ 元语法,通过截断相关性,为处理长序列提供了一种方便的模型。 +* 长序列存在一个问题,那就是它们很少出现或者从不出现。 +* 齐普夫定律控制了单词的分布,这个分布不仅适用于单字单词,还适用于其他 $n$ 元语法的单词。 +* 通过拉普拉斯平滑法可以有效地处理结构丰富而频率不足的低频词组成的词组。 +* 读取长序列的主要方式是随机采样和顺序分区。后者可以保证在迭代过程中来自两个相邻的小批量中的子序列在原始序列上也是相邻的。 ## 练习 -1. 假设训练数据集中有$100,000$个单词。四元语法需要存储多少词频和多词相邻频率? -1. 你将如何模拟对话? -1. 估计“单变量”(unigram)、“双变量”(bigram)和“三变量”(trigram)的齐普夫定律指数。 -1. 您还能想到哪些其他的读取长序列数据的方法? +1. 假设训练数据集中有 $100,000$ 个单词。四元语法需要存储多少词频和多词相邻频率? +1. 你将如何将对话建模? +1. 估计“一元语法”(unigram)、“二元语法”(bigram)和“三元语法”(trigram)的齐普夫定律指数。 +1. 你还能想到哪些其他的读取长序列数据的方法? 1. 考虑一下我们用于读取长序列的随机偏移量。 1. 为什么随机偏移量是个好主意? - 1. 它真的会在文档上的序列上实现完美均匀的分布吗? - 1. 你要怎么做才能让事情变得更加统一呢? + 1. 它真的会在文档的序列上实现完美的均匀分布吗? + 1. 你要怎么做才能让事情变得更加均衡呢? 1. 如果我们希望一个序列样本是一个完整的句子,那么这在小批量抽样中会带来什么样的问题呢?我们怎样才能解决这个问题呢? :begin_tab:`mxnet` @@ -298,3 +301,4 @@ def load_data_time_machine(batch_size, num_steps, #@save :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2098) :end_tab: + From 0f77f190e0cc5ad75b2e21553511cb51277ad0a8 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Thu, 6 May 2021 02:34:21 +0800 Subject: [PATCH 081/103] Chapter_recurrent-modern/seq2seq.md (#764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix errors in 9.7.0. * fix errors 9.7.1. Encoder * fix errors 9.7.2. Decoder * fix errors in 9.7.3. Loss Function * fix errors 9.7.4. Training * fix errors 9.7.5. Prediction * fix errors 9.7.6. Evaluation fix errors 9.7.7. Summary fix errors 9.7.8. Exercises * fix invalid character in code * change RNN to 循环神经网络 * replace GRU by 门控循环单元 replace LSTM by 长短期记忆网络 --- chapter_recurrent-modern/seq2seq.md | 127 ++++++++++++++-------------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/chapter_recurrent-modern/seq2seq.md b/chapter_recurrent-modern/seq2seq.md index 58e01ca8f..65a8888e1 100644 --- a/chapter_recurrent-modern/seq2seq.md +++ b/chapter_recurrent-modern/seq2seq.md @@ -1,17 +1,17 @@ # 序列到序列学习(seq2seq) :label:`sec_seq2seq` -正如我们在 :numref:`sec_machine_translation` 中看到的。在机器翻译中,输入和输出都是可变长度的序列。为了解决这类问题,我们在 :numref:`sec_encoder-decoder` 中设计了一个通用的编码器-解码器结构。在本节中,我们将使用两个循环神经网络来设计此编码器-解码器结构,并将其应用于机器翻译 :cite:`Sutskever.Vinyals.Le.2014,Cho.Van-Merrienboer.Gulcehre.ea.2014` 的*序列到序列*(sequence to sequence)学习。 +正如我们在 :numref:`sec_machine_translation` 中看到的。在机器翻译中,输入序列和输出序列都是长度可变的。为了解决这类问题,我们在 :numref:`sec_encoder-decoder` 中设计了一个通用的”编码器-解码器“结构。在本节中,我们将使用两个循环神经网络来设计此“编码器-解码器”结构,并将其应用于机器翻译 :cite:`Sutskever.Vinyals.Le.2014,Cho.Van-Merrienboer.Gulcehre.ea.2014` 的*序列到序列*(sequence to sequence)学习。 -循环神经网络编码器遵循编码器-解码器结构的设计原则,以可变长度序列作为输入,将其转换为固定形状的隐藏状态。换言之,输入(源)序列的信息在循环神经网络编码器的隐藏状态下被*编码*。为了逐个标记地生成输出序列标记,单独的循环神经网络解码器可以基于已经看到的标记(例如在语言模型任务中)或生成的标记以及输入序列的编码信息来预测下一标记。 :numref:`fig_seq2seq` 演示了如何在机器翻译中使用两个循环神经网络进行序列到序列学习。 +遵循“编码器-解码器”结构的设计原则,循环神经网络编码器可以使用长度可变的序列作为输入,将其转换为形状固定的隐藏状态。换言之,输入序列(源)的信息被 *编码* 到循环神经网络编码器的隐藏状态中。为了一个接着一个的生成输出序列的标记,独立的循环神经网络解码器是基于输入序列的编码信息和输出序列已经生成的标记(例如在语言模型的任务中)来预测下一个标记。 :numref:`fig_seq2seq` 演示了如何在机器翻译伤中使用两个循环神经网络进行序列到序列学习。 ![使用循环神经网络编码器和循环神经网络解码器的序列到序列学习。](../img/seq2seq.svg) :label:`fig_seq2seq` -在 :numref:`fig_seq2seq` 中,特殊的“<eos>”标记表示序列的结束。一旦生成此标记,模型就可以停止进行预测。在循环神经网络解码器的初始时间步,有两个特殊的设计决策。首先,特殊序列开始标记“<bos>”是第一个输入。其次,使用循环神经网络编码器的最终隐藏状态来启动解码器的隐藏状态。在如 :cite:`Sutskever.Vinyals.Le.2014` 的设计中,编码器的最终隐藏状态也作为在每个时间步长的输入的一部分馈送到解码器中,如 :numref:`fig_seq2seq` 所示。类似于 :numref:`sec_language_model` 中的语言模型训练,我们可以允许标签是原始的输出序列,移位一个标“<bos>”、“Ils”、“regardent”、“.” $\rightarrow$ -“Ils”、“regardent”、“.”、“<eos>”。 +在 :numref:`fig_seq2seq` 中,特定的“<eos>”表示*序列结束标记*。一旦输出序列生成此标记,模型就可以停止执行预测。在循环神经网络解码器的初始化时间步,有两个特定的设计决定。首先,特定的“<bos>”表示*序列开始标记*,它是解码器的输入序列的第一个标记。其次,使用循环神经网络编码器最终的隐藏状态来初始化解码器的隐藏状态。在例如 :cite:`Sutskever.Vinyals.Le.2014` 的设计中,正是基于这种设计将输入序列的编码信息送入到解码器中来生成输出序列(目标)的。在其他一些例如 :cite:`Cho.Van-Merrienboer.Gulcehre.ea.2014` 的设计中,在每个时间步中,编码器最终的隐藏状态都作为解码器的输入序列的一部分,如 :numref:`fig_seq2seq` 所示。类似于 :numref:`sec_language_model` 中训练的语言模型,可以允许标签成为原始的输出序列,基于一个个标记“<bos>”、“Ils”、“regardent”、“.” $\rightarrow$ +“Ils”、“regardent”、“.”、“<eos>”来移动预测的位置。 -下面,我们将对 :numref:`fig_seq2seq` 的设计进行更详细的说明。我们将在 :numref:`sec_machine_translation` 中介绍的英-法数据集上训练这个机器翻译模型。 +下面,我们将对 :numref:`fig_seq2seq` 的设计进行更详细的解释,并且将在 :numref:`sec_machine_translation` 中介绍的“英-法”数据集上训练这个机器翻译模型。 ```{.python .input} import collections @@ -33,26 +33,26 @@ from torch import nn ## 编码器 -从技术上讲,编码器将可变长度的输入序列转换成固定形状的*上下文变量*$\mathbf{c}$,并且在该上下文变量中编码输入序列信息。如:numref:`fig_seq2seq`所示,我们可以使用循环神经网络来设计编码器。 +从技术上讲,编码器将长度可变的输入序列转换成形状固定的 *上下文变量* $\mathbf{c}$,并且将输入序列的信息在该上下文变量中进行编码。如:numref:`fig_seq2seq`所示,可以使用循环神经网络来设计编码器。 -让我们考虑一个序列样本(批量大小:1)。假设输入序列是$x_1, \ldots, x_T$,其中$x_t$是输入文本序列中的第$t$个标记。在时间步$t$,循环神经网络将用于$x_t$的输入特征向量$\mathbf{x}_t$和来自上一时间步的隐藏状态$\mathbf{h} _{t-1}$转换为当前隐藏状态$\mathbf{h}_t$。我们可以用一个函数$f$来表示循环神经网络层所做的变换: +让我们考虑一个序列样本(批量大小:1)。假设输入序列是 $x_1, \ldots, x_T$,其中 $x_t$ 是输入文本序列中的第 $t$ 个标记。在时间步 $t$,循环神经网络将 $x_t$(即输入特征向量 $\mathbf{x}_t$)和 $\mathbf{h} _{t-1}$(即上一时间步的隐藏状态)转换为 $\mathbf{h}_t$(即当前隐藏状态)。使用一个函数 $f$ 来描述循环神经网络层所做的变换: $$\mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}). $$ -通常,编码器通过定制函数$q$将所有时间步的隐藏状态转换为上下文变量: +总之,编码器通过选定的函数 $q$ 将所有时间步的隐藏状态转换为上下文变量: $$\mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T).$$ -例如,当选择$q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T$时(例如在:numref:`fig_seq2seq`中),上下文变量仅仅是输入序列在最后时间步的隐藏状态$\mathbf{h}_T$。 +例如,当选择 $q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T$ 时(例如在:numref:`fig_seq2seq`中),上下文变量仅仅是输入序列在最后时间步的隐藏状态 $\mathbf{h}_T$。 -到目前为止,我们已经使用了一个单向循环神经网络来设计编码器,其中隐藏状态只依赖于隐藏状态的时间步处和之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,隐藏状态取决于时间步前后的子序列(包括当前时间步处的输入),该子序列对整个序列的信息进行编码。 +到目前为止,我们使用的是一个单向循环神经网络来设计编码器,其中隐藏状态只依赖于输入子序列,这个子序列是由输入序列的开始位置到隐藏状态所在的时间步的位置(包括隐藏状态所在的时间步)组成。我们也可以使用双向循环神经网络构造编码器,其中隐藏状态依赖于两个输入子序列,两个子序列是由隐藏状态所在的时间步的位置之前的序列和之后的序列(包括隐藏状态所在的时间步),因此隐藏状态对整个序列的信息都进行了编码。 -现在让我们实现循环神经网络编码器。注意,我们使用*嵌入层*(embedding layer)来获得输入序列中每个标记的特征向量。嵌入层的权重是一个矩阵,其行数等于输入词表的大小(`vocab_size`),列数等于特征向量的维数(`embed_size`)。对于任何输入标记索引$i$,嵌入层获取权重矩阵的第$i$行(从0开始)以返回其特征向量。另外,本文选择了一个多层门控循环单元来实现编码器。 +现在,让我们实现循环神经网络编码器。注意,我们使用了 *嵌入层*(embedding layer)来获得输入序列中每个标记的特征向量。嵌入层的权重是一个矩阵,其行数等于输入词表的大小(`vocab_size`),列数等于特征向量的维度(`embed_size`)。对于任何输入标记的索引 $i$,嵌入层获取权重矩阵的第 $i$ 行(从 $0$ 开始)以返回其特征向量。另外,本文选择了一个多层门控循环单元来实现编码器。 ```{.python .input} #@save class Seq2SeqEncoder(d2l.Encoder): - """The RNN encoder for sequence to sequence learning.""" + """用于序列到序列学习的循环神经网络编码器。""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) @@ -63,7 +63,7 @@ class Seq2SeqEncoder(d2l.Encoder): def forward(self, X, *args): # 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) - # 在循环神经网络模型中,第一个轴对应于时间步长 + # 在循环神经网络模型中,第一个轴对应于时间步 X = X.swapaxes(0, 1) state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx) output, state = self.rnn(X, state) @@ -76,7 +76,7 @@ class Seq2SeqEncoder(d2l.Encoder): #@tab pytorch #@save class Seq2SeqEncoder(d2l.Encoder): - """The RNN encoder for sequence to sequence learning.""" + """用于序列到序列学习的循环神经网络编码器。""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqEncoder, self).__init__(**kwargs) @@ -88,16 +88,16 @@ class Seq2SeqEncoder(d2l.Encoder): def forward(self, X, *args): # 输出'X'的形状:(`batch_size`, `num_steps`, `embed_size`) X = self.embedding(X) - # 在循环神经网络模型中,第一个轴对应于时间步长 + # 在循环神经网络模型中,第一个轴对应于时间步 X = X.permute(1, 0, 2) # 如果未提及状态,则默认为0 output, state = self.rnn(X) - # `output`维度: (`num_steps`, `batch_size`, `num_hiddens`) - # `state[0]`维度: (`num_layers`, `batch_size`, `num_hiddens`) + # `output`的形状: (`num_steps`, `batch_size`, `num_hiddens`) + # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`) return output, state ``` -循环神经网络层的返回变量已在 :numref:`sec_rnn-concise` 中解释。让我们仍然使用一个具体的例子来说明上述编码器实现。下面我们将实例化一个隐藏单元数为16的两层门控循环单元编码器。给定一小批量序列输入`X`(批量大小:4,时间步:7),所有时间步最后一层的隐藏状态(`output`由编码器的循环神经网络层返回)是形状为(时间步数, 批大小, 隐藏单元数)的张量。 +循环神经网络层返回变量的解释可以参考 :numref:`sec_rnn-concise` 。让我们使用一个具体的例子来说明上述编码器的实现。下面将实例化一个隐藏单元数为 $16$ 的两层门控循环单元编码器。给定一小批量序列输入`X`(批量大小:4,时间步:7),最后一层的隐藏状态在完成所有时间步后输出是一个张量(`output`由编码器的循环神经网络层返回),其形状为(时间步数, 批量大小, 隐藏单元数)。 ```{.python .input} encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -118,7 +118,7 @@ output, state = encoder(X) output.shape ``` -由于这里使用门控循环单元,所以在最后一个时间步的多层隐藏状态的形状是(隐藏层的数量, 批量大小, 隐藏单元的数量)。如果使用长短期记忆网络,`state`中还将包含记忆单元信息。 +由于这里使用的是门控循环单元,所以在最后一个时间步的多层隐藏状态的形状是(隐藏层的数量, 批量大小, 隐藏单元的数量)。如果使用长短期记忆网络,`state`中还将包含记忆单元信息。 ```{.python .input} len(state), state[0].shape @@ -132,16 +132,16 @@ state.shape ## 解码器 :label:`sec_seq2seq_decoder` -正如我们刚才提到的,编码器输出的上下文变量$\mathbf{c}$对整个输入序列$x_1, \ldots, x_T$进行编码。给定来自训练数据集的输出序列$y_1, y_2, \ldots, y_{T'}$,对于每个时间步$t'$(与输入序列或编码器的时间步$t$不同),解码器输出$y_{t'}$的概率取决于先前的输出子序列$y_1, \ldots, y_{t'-1}$和上下文变量$\mathbf{c}$,即$P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$。 +正如上文提到的,编码器输出的上下文变量 $\mathbf{c}$ 对整个输入序列 $x_1, \ldots, x_T$ 进行编码。来自训练数据集的输出序列 $y_1, y_2, \ldots, y_{T'}$,对于每个时间步 $t'$(与输入序列或编码器的时间步 $t$ 不同),解码器输出 $y_{t'}$ 的概率取决于先前的输出子序列 $y_1, \ldots, y_{t'-1}$ 和上下文变量 $\mathbf{c}$,即 $P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$。 -为了在序列上模拟这种条件概率,我们可以使用另一个循环神经网络作为解码器。在输出序列上的任何时间步$t^\prime$,循环神经网络将来自上一时间步的输出$y_{t^\prime-1}$和上下文变量$\mathbf{c}$作为其输入,然后在当前时间步将它们和上一隐藏状态$\mathbf{s}_{t^\prime-1}$转换为隐藏状态$\mathbf{s}_{t^\prime}$。因此,我们可以使用函数$g$来表示解码器的隐藏层的变换: +为了在序列上将这种条件概率模型化,我们可以使用另一个循环神经网络作为解码器。在输出序列上的任何时间步 $t^\prime$,循环神经网络将来自上一时间步的输出 $y_{t^\prime-1}$ 和上下文变量 $\mathbf{c}$ 作为其输入,然后在当前时间步将它们和上一隐藏状态 $\mathbf{s}_{t^\prime-1}$ 转换为隐藏状态 $\mathbf{s}_{t^\prime}$。因此,可以使用函数 $g$ 来表示解码器的隐藏层的变换: $$\mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-1}).$$ :eqlabel:`eq_seq2seq_s_t` -在获得解码器的隐藏状态之后,我们可以使用输出层和softmax操作来计算时间步$t^\prime$处的输出的条件概率分布$P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})$。 +在获得解码器的隐藏状态之后,我们可以使用输出层和 softmax 操作来计算时间步 $t^\prime$ 处输出的条件概率分布 $P(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c})$。 -根据 :numref:`fig_seq2seq`,当实现解码器时,我们直接使用编码器最后一个时间步的隐藏状态来初始化解码器的隐藏状态。这要求循环神经网络编码器和循环神经网络解码器具有相同数量的层和隐藏单元。为了进一步合并经编码的输入序列信息,上下文变量在所有时间步处与解码器输入串联。为了预测输出标记的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐藏状态。 +根据 :numref:`fig_seq2seq`,当实现解码器时,我们直接使用编码器最后一个时间步的隐藏状态来初始化解码器的隐藏状态。这就要求循环神经网络编码器和循环神经网络解码器具有相同数量的层和隐藏单元。为了进一步包含经过编码的输入序列的信息,上下文变量在所有的时间步与解码器的输入进行拼接(concatenate)。为了预测输出标记的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐藏状态。 ```{.python .input} class Seq2SeqDecoder(d2l.Decoder): @@ -175,7 +175,7 @@ class Seq2SeqDecoder(d2l.Decoder): ```{.python .input} #@tab pytorch class Seq2SeqDecoder(d2l.Decoder): - """The RNN decoder for sequence to sequence learning.""" + """用于序列到序列学习的循环神经网络解码器。""" def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs): super(Seq2SeqDecoder, self).__init__(**kwargs) @@ -200,7 +200,7 @@ class Seq2SeqDecoder(d2l.Decoder): return output, state ``` -为了说明实现的解码器,下面我们用前面提到的编码器中相同的超参数来实例化它。解码器的输出形状变为(批量大小, 时间步数, 词表大小),其中张量的最后一个维度存储预测的标记分布。 +为了举例说明已经实现的解码器,下面我们用前面提到的编码器中相同的超参数来实例化它。如我们所见,解码器的输出形状变为(批量大小, 时间步数, 词表大小),其中张量的最后一个维度存储预测的标记分布。 ```{.python .input} decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -221,16 +221,16 @@ output, state = decoder(X, state) output.shape, state.shape ``` -总之,上述循环神经网络编码器-解码器模型中的各层如 :numref:`fig_seq2seq_details` 所示。 +总之,上述循环神经网络“编码器-解码器”模型中的各层如 :numref:`fig_seq2seq_details` 所示。 ![循环神经网络编码器-解码器模型中的层。](../img/seq2seq-details.svg) :label:`fig_seq2seq_details` ## 损失函数 -在每个时间步,解码器预测输出令牌的概率分布。类似于语言模型,我们可以使用softmax来获得分布,并计算交叉熵损失进行优化。回想一下 :numref:`sec_machine_translation` ,特殊的填充标记被附加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,应该将填充令牌的预测排除在损失计算之外。 +在每个时间步,解码器预测输出标记的概率分布。类似于语言模型,可以使用 softmax 来获得分布,并计算交叉熵损失函数来进行优化。回想一下 :numref:`sec_machine_translation` ,特定的填充标记被添加到序列的末尾,因此不同长度的序列可以以相同形状的小批量加载。但是,应该将填充标记的预测排除在损失计算之外。 -为此,我们可以使用下面的`sequence_mask`函数用零值屏蔽不相关的项,以便以后任何不相关的预测与零的乘积等于零。例如,如果两个序列(不包括填充标记)的有效长度分别为1和2,则第一项和前两项之后的剩余项将被清除为零。 +为此,我们可以使用下面的`sequence_mask`函数用零值屏蔽不相关的项,以便后面计算任何不相关的预测与零的乘积都等于零。例如,如果两个序列的有效长度(不包括填充标记)分别为1和2,则第一项和前两项之后的剩余项将被清除为零。 ```{.python .input} X = np.array([[1, 2, 3], [4, 5, 6]]) @@ -252,7 +252,7 @@ X = torch.tensor([[1, 2, 3], [4, 5, 6]]) sequence_mask(X, torch.tensor([1, 2])) ``` -我们还可以屏蔽最后几个轴上的所有项。如果愿意,也可以指定用非零值替换这些条目。 +我们还可以屏蔽最后几个轴上的所有项。如果愿意,也可以指定使用用非零值来替换这些条目。 ```{.python .input} X = d2l.ones((2, 3, 4)) @@ -265,12 +265,12 @@ X = d2l.ones(2, 3, 4) sequence_mask(X, torch.tensor([1, 2]), value=-1) ``` -现在我们可以扩展softmax交叉熵损失来遮蔽不相关的预测。最初,所有预测标记的掩码都设置为1。一旦给定了有效长度,与填充标记对应的掩码将被设置为0。最后,将所有标记的损失乘以掩码,以过滤掉损失中填充标记的不相关预测。 +现在,我们可以通过扩展 softmax 交叉熵损失函数来遮蔽不相关的预测。最初,所有预测标记的掩码都设置为1。一旦给定了有效长度,与填充标记对应的掩码将被设置为0。最后,将所有标记的损失乘以掩码,以过滤掉损失中填充标记的不相关预测。 ```{.python .input} #@save class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): - """The softmax cross-entropy loss with masks.""" + """带遮蔽的 softmax 交叉熵损失函数""" # `pred` 的形状:(`batch_size`, `num_steps`, `vocab_size`) # `label` 的形状:(`batch_size`, `num_steps`) # `valid_len` 的形状:(`batch_size`,) @@ -285,7 +285,7 @@ class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss): #@tab pytorch #@save class MaskedSoftmaxCELoss(nn.CrossEntropyLoss): - """The softmax cross-entropy loss with masks.""" + """带遮蔽的 softmax 交叉熵损失函数""" # `pred` 的形状:(`batch_size`, `num_steps`, `vocab_size`) # `label` 的形状:(`batch_size`, `num_steps`) # `valid_len` 的形状:(`batch_size`,) @@ -316,7 +316,7 @@ loss(d2l.ones(3, 4, 10), d2l.ones((3, 4), dtype=torch.long), ## 训练 :label:`sec_seq2seq_training` -在下面的训练代码实现中,我们将特殊的序列开始标记和原始输出序列(不包括序列结束标记)连结,作为解码器的输入,如 :numref:`fig_seq2seq` 所示。这被称为“教师强制”(teacher forcing),因为原始输出序列(标记标签)被送入解码器。或者,我们也可以将来自上一时间步的*预测*得到的标记作为当前输入送到解码器。 +在下面的循环训练过程中,如 :numref:`fig_seq2seq` 所示,特定的序列开始标记和原始的输出序列(不包括序列结束标记)拼接在一起作为解码器的输入。这被称为“教师强制”(teacher forcing),因为原始的输出序列(标记标签)被送入解码器。或者,将来自上一个时间步的 *预测* 得到的标记作为解码器的当前输入。 ```{.python .input} #@save @@ -330,13 +330,13 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() - metric = d2l.Accumulator(2) # 训练损失总和,标记数量 + metric = d2l.Accumulator(2) # 训练损失求和,标记数量 for batch in data_iter: X, X_valid_len, Y, Y_valid_len = [ x.as_in_ctx(device) for x in batch] - bos = np.array( - [tgt_vocab['']] * Y.shape[0], ctx=device).reshape(-1, 1) - dec_input = d2l.concat([bos, Y[:, :-1]], 1) # 教师强制 + bos = np.array([tgt_vocab['']] * Y.shape[0], + ctx=device).reshape(-1, 1) + dec_input = np.concatenate([bos, Y[:, :-1]], 1) # 教师强制 with autograd.record(): Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) @@ -348,7 +348,7 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): if (epoch + 1) % 10 == 0: animator.add(epoch + 1, (metric[0] / metric[1],)) print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' - f'tokens/sec on {str(device)}') + f'tokens/sec on {str(device)}') ``` ```{.python .input} @@ -363,24 +363,25 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): for param in m._flat_weights_names: if "weight" in param: nn.init.xavier_uniform_(m._parameters[param]) + net.apply(xavier_init_weights) net.to(device) optimizer = torch.optim.Adam(net.parameters(), lr=lr) loss = MaskedSoftmaxCELoss() net.train() animator = d2l.Animator(xlabel='epoch', ylabel='loss', - xlim=[10, num_epochs]) + xlim=[10, num_epochs]) for epoch in range(num_epochs): timer = d2l.Timer() metric = d2l.Accumulator(2) # 训练损失总和,标记数量 for batch in data_iter: X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch] bos = torch.tensor([tgt_vocab['']] * Y.shape[0], - device=device).reshape(-1, 1) - dec_input = d2l.concat([bos, Y[:, :-1]], 1) # 教师强制 + device=device).reshape(-1, 1) + dec_input = torch.cat([bos, Y[:, :-1]], 1) # 教师强制 Y_hat, _ = net(X, dec_input, X_valid_len) l = loss(Y_hat, Y, Y_valid_len) - l.sum().backward() + l.sum().backward() # 损失函数的标量进行“反传” d2l.grad_clipping(net, 1) num_tokens = Y_valid_len.sum() optimizer.step() @@ -389,10 +390,10 @@ def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device): if (epoch + 1) % 10 == 0: animator.add(epoch + 1, (metric[0] / metric[1],)) print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ' - f'tokens/sec on {str(device)}') + f'tokens/sec on {str(device)}') ``` -现在在机器翻译数据集上,我们可以创建和训练一个循环神经网络编码器-解码器模型,用于序列到序列的学习。 +现在,在机器翻译数据集上,我们可以创建和训练一个循环神经网络“编码器-解码器”模型用于序列到序列的学习。 ```{.python .input} #@tab all @@ -401,17 +402,17 @@ batch_size, num_steps = 64, 10 lr, num_epochs, device = 0.005, 300, d2l.try_gpu() train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps) -encoder = Seq2SeqEncoder( - len(src_vocab), embed_size, num_hiddens, num_layers, dropout) -decoder = Seq2SeqDecoder( - len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout) +encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers, + dropout) +decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers, + dropout) net = d2l.EncoderDecoder(encoder, decoder) train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ``` ## 预测 -为了逐个标记地预测输出序列标记,在每个解码器时间步处,将来自前一时间步的预测标记作为输入送入解码器。与训练类似,在初始时间步,序列开始标记(“<bos>”)被馈送到解码器。该预测过程如:numref:`fig_seq2seq_predict`所示。当序列结束标记(“<eos>”)被预测时,输出序列的预测就完成了。 +为了一个接着一个地预测输出序列的标记,每个解码器当前时间步的输入都将来自于前一时间步的预测标记。与训练类似,序列开始标记(“<bos>”)在初始时间步被输入到解码器中。该预测过程如:numref:`fig_seq2seq_predict`所示。当输出序列的预测遇到序列结束标记(“<eos>”)时,预测就结束了。 ![使用循环神经网络编码器-解码器逐标记地预测输出序列。](../img/seq2seq-predict.svg) :label:`fig_seq2seq_predict` @@ -487,23 +488,23 @@ def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, ## 预测序列的评估 -我们可以通过与标签序列(真实标签)进行比较来评估预测序列。BLEU(Bilingual Evaluation Understudy)虽然最初被提出用于评估机器翻译结果 :cite:`Papineni.Roukos.Ward.ea.2002` ,但已被广泛用于测量多种应用的输出序列的质量。原则上,对于预测序列中的任意$n$元组,BLEU评估该$n$元组是否出现在标签序列中。 +我们可以通过与标签序列(真实标签)进行比较来评估预测序列。虽然最初 BLEU(Bilingual Evaluation Understudy)的提出是用于评估机器翻译的结果 :cite:`Papineni.Roukos.Ward.ea.2002` ,但现在它已经被广泛用于测量多种应用的输出序列的质量。对于预测序列中的任意 $n$ 元语法(n-grams),BLEU 的评估原则是这个 $n$ 元语法是否出现在标签序列中。 -用$p_n$表示$n$元组的精度,它是预测序列和标签序列中匹配的$n$元组的数量与预测序列中$n$元组的数量的比率。详细解释一下,给定标签序列$A$、$B$、$C$、$D$、$E$、$F$和预测序列$A$、$B$、$B$、$C$、$D$,我们有$p_1 = 4/5$、$p_2 = 3/4$、$p_3 = 1/3$和$p_4 = 0$。另外,让$\mathrm{len}_{\text{label}}$和$\mathrm{len}_{\text{pred}}$分别是标签序列和预测序列中的标记数。那么,BLEU的定义是: +用 $p_n$ 表示 $n$ 元语法的精度,它是预测序列与标签序列中匹配的 $n$ 元语法的数量与预测序列中匹配的 $n$ 元语法的数量的比率。详细解释,即给定的标签序列 $A$、$B$、$C$、$D$、$E$、$F$ 和预测序列 $A$、$B$、$B$、$C$、$D$,我们有 $p_1 = 4/5$、$p_2 = 3/4$、$p_3 = 1/3$ 和 $p_4 = 0$。另外, $\mathrm{len}_{\text{label}}$ 表示标签序列中的标记数和 $\mathrm{len}_{\text{pred}}$ 表示预测序列中的标记数。那么,BLEU 的定义是: $$ \exp\left(\min\left(0, 1 - \frac{\mathrm{len}_{\text{label}}}{\mathrm{len}_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n},$$ :eqlabel:`eq_bleu` -其中$k$是最长的$n$元组进行匹配。 +其中 $k$ 是能够匹配的最长的 $n$ 元语法。 -根据 :eqref:`eq_bleu` 中BLEU的定义,当预测序列与标签序列相同时,BLEU为1。此外,由于匹配更长的$n$元组更加困难,BLEU为更长的$n$元组精度分配了更大的权重。具体来说,当$p_n$固定时,$p_n^{1/2^n}$会随着$n$的增长而增加(原始论文使用$p_n^{1/n}$)。此外,由于预测较短的序列倾向于获得较高的$p_n$值,因此 :eqref:`eq_bleu` 中乘法项之前的系数惩罚较短的预测序列。例如,当$k=2$时,给定标签序列$A$、$B$、$C$、$D$、$E$、$F$和预测序列$A$、$B$,尽管$p_1 = p_2 = 1$,惩罚因子$\exp(1-6/2) \approx 0.14$降低BLEU。 +根据 :eqref:`eq_bleu` 中 BLEU 的定义,当预测序列与标签序列相同时,BLEU 为1。而且,因为匹配的 $n$ 元语法越长则难度越大,BLEU 为更长的 $n$ 元语法的精度分配更大的权重。具体来说,当 $p_n$ 固定时,$p_n^{1/2^n}$ 会随着 $n$ 的增长而增加(原始论文使用 $p_n^{1/n}$)。此外,由于预测的序列越短获得的 $p_n$ 值越高,因此 :eqref:`eq_bleu` 中乘法项之前的系数惩罚较短的预测序列。例如,当 $k=2$ 时,给定标签序列 $A$、$B$、$C$、$D$、$E$、$F$ 和预测序列 $A$、$B$,尽管 $p_1 = p_2 = 1$,惩罚因子 $\exp(1-6/2) \approx 0.14$ 会降低 BLEU。 -我们实现BLEU的代码如下。 +BLEU 的实现代码如下。 ```{.python .input} #@tab all def bleu(pred_seq, label_seq, k): #@save - """计算BLEU""" + """计算 BLEU""" pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ') len_pred, len_label = len(pred_tokens), len(label_tokens) score = math.exp(min(0, 1 - len_label / len_pred)) @@ -519,7 +520,7 @@ def bleu(pred_seq, label_seq, k): #@save return score ``` -最后,利用训练好的循环神经网络编码器-解码器模型将几个英语句子翻译成法语,并计算结果的BLEU。 +最后,利用训练好的循环神经网络“编码器-解码器”模型将几个英语句子翻译成法语,并计算结果的 BLEU。 ```{.python .input} #@tab all @@ -536,15 +537,15 @@ for eng, fra in zip(engs, fras): * 根据“编码器-解码器”结构的设计,我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。 * 在实现编码器和解码器时,我们可以使用多层循环神经网络。 * 我们可以使用遮蔽来过滤不相关的计算,例如在计算损失时。 -* 在编码器-解码器训练中,教师强制方法将原始输出序列(而非预测结果)输入解码器。 -* BLEU是一种常用的评估自然语言处理模型的方法,它通过在预测序列和标签序列之间匹配$n$元组来实现。 +* 在“编码器-解码器”训练中,教师强制方法将原始输出序列(而非预测结果)输入解码器。 +* BLEU 是一种常用的评估自然语言处理模型的方法,它通过预测序列和标签序列之间 $n$ 元语法的匹配度来实现。 ## 练习 -1. 你能调整超参数来提高翻译效果吗? -1. 在损失计算中不使用遮蔽重新运行实验。你观察到什么结果? -1. 如果编码器和解码器的层数或隐藏单元数不同,如何初始化解码器的隐藏状态? -1. 在训练中,用将前一时间步的预测输入解码器来代替教师强制。这对性能有何影响? +1. 你能调整超参数来改善翻译效果吗? +1. 重新运行实验并在计算损失时不使用遮蔽。你观察到什么结果?为什么? +1. 如果编码器和解码器的层数或者隐藏单元数不同,那么如何初始化解码器的隐藏状态? +1. 在训练中,如果用前一时间步的预测输入到解码器来代替教师强制。这对性能有何影响? 1. 用长短期记忆网络替换门控循环单元重新运行实验。 1. 有没有其他方法来设计解码器的输出层? @@ -554,4 +555,4 @@ for eng, fra in zip(engs, fras): :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/1062) -:end_tab: +:end_tab: \ No newline at end of file From 21265eecf2f8d6ca3cb6c5efc4a6daea4d06fc06 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Thu, 6 May 2021 02:35:04 +0800 Subject: [PATCH 082/103] fix errors in recurrent-modern/encoder-decoder.md (#765) * fix errors 9.6. Encoder-Decoder Architecture fix errors 9.6.1. Encoder * fix errors 9.6.2. Decoder * fix errors 9.6.3. Putting the Encoder and Decoder Together * the end of fix errors 9.6 --- chapter_recurrent-modern/encoder-decoder.md | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/chapter_recurrent-modern/encoder-decoder.md b/chapter_recurrent-modern/encoder-decoder.md index dd023e989..86d57b082 100644 --- a/chapter_recurrent-modern/encoder-decoder.md +++ b/chapter_recurrent-modern/encoder-decoder.md @@ -1,16 +1,16 @@ # 编码器-解码器结构 :label:`sec_encoder-decoder` -正如我们在:numref:`sec_machine_translation`中所讨论的,机器翻译是序列转换模型的一个核心问题,其输入和输出都是可变长度序列。为了处理这种类型的输入和输出,我们可以设计一个包含两个主要组件的结构。第一个组件是一个*编码器*(encoder):它接受一个可变长度的序列作为输入,并将其转换为具有固定形状的编码状态。第二个组件是*解码器*(decoder):它将固定形状的编码状态映射到可变长度序列。这被称为*编码器-解码器*(encoder-decoder)结构。如:numref:`fig_encoder_decoder`所示。 +正如我们在:numref:`sec_machine_translation`中所讨论的,机器翻译是序列转换模型的一个核心问题,其输入和输出都是长度可变的序列。为了处理这种类型的输入和输出,我们可以设计一个包含两个主要组件的结构。第一个组件是一个*编码器*(encoder):它接受一个长度可变的序列作为输入,并将其转换为具有形状固定的编码状态。第二个组件是*解码器*(decoder):它将形状固定的编码状态映射到长度可变的序列。这被称为*编码器-解码器*(encoder-decoder)结构。如:numref:`fig_encoder_decoder`所示。 ![编码器-解码器结构](../img/encoder-decoder.svg) :label:`fig_encoder_decoder` -让我们以英语到法语的机器翻译为例。给定一个英文的输入序列:“They”、“are”、“watching”、“.”,这种编码器-解码器结构首先将可变长度的输入编码成一个状态,然后对该状态进行解码,一个标记一个标记地生成翻译后的序列令牌作为输出:“Ils”、“regordent”、“.”。由于编码器-解码器结构构成了后续章节中不同序列转换模型的基础,因此本节将把该结构转换为稍后将实现的接口。 +让我们以英语到法语的机器翻译为例。给定一个英文的输入序列:“They”、“are”、“watching”、“.”,这种“编码器-解码器”结构首先将长度可变的输入编码成一个状态,然后对该状态进行解码,一个标记接着一个标记地生成翻译后的序列作为输出:“Ils”、“regordent”、“.”。由于“编码器-解码器”结构是形成后续章节中不同序列转换模型的基础,因此本节将把这个结构转换为接口用于后面的代码实现。 ## 编码器 -在编码器接口中,我们只指定编码器采用可变长度序列作为输入`X`。实现将由任何继承这个`Encoder`基类的模型提供。 +在编码器接口中,我们只指定长度可变的序列作为编码器的输入`X`。任何继承这个`Encoder`基类的模型将完成代码实现。 ```{.python .input} from mxnet.gluon import nn @@ -41,7 +41,7 @@ class Encoder(nn.Module): ## 解码器 -在下面的解码器接口中,我们添加了一个额外的`init_state`函数来将编码器输出(`enc_outputs`)转换为编码状态。请注意,此步骤可能需要额外的输入,例如输入的有效长度,这在:numref:`subsec_mt_data_loading`中进行了解释。为了逐个标记生成可变长度标记序列,每次解码器可将输入(例如,在前一时间步生成的标记)和编码状态映射到当前时间步的输出标记。 +在下面的解码器接口中,我们新增一个`init_state`函数用于将编码器的输出(`enc_outputs`)转换为编码后的状态。注意,此步骤可能需要额外的输入,例如:输入序列的有效长度,这在:numref:`subsec_mt_data_loading`中进行了解释。为了逐个生成长度可变的标记序列,解码器在每个时间步都可以将输入(例如:在前一时间步生成的标记)和编码后的状态映射成当前时间步的输出标记。 ```{.python .input} #@save @@ -72,9 +72,9 @@ class Decoder(nn.Module): raise NotImplementedError ``` -## 把编码器和解码器合并 +## 合并编码器和解码器 -最后,编码器-解码器结构包含编码器和解码器,并包含可选的额外的参数。在前向传播中,编码器的输出产生“编码状态”,解码器将使用该状态作为其输入之一。 +最后,“编码器-解码器”结构包含了一个编码器和一个解码器,并且还包含了可选的额外的参数。在前向传播中,编码器的输出产生编码状态,解码器将使用该状态作为其输入之一。 ```{.python .input} #@save @@ -107,18 +107,18 @@ class EncoderDecoder(nn.Module): return self.decoder(dec_X, dec_state) ``` -编码器-解码器体系结构中的术语“状态”可能启发你使用具有状态的神经网络来实现该结构。在下一节中,我们将看到如何应用循环神经网络来设计基于这种编码器-解码器结构的序列转换模型。 +“编码器-解码器”体系结构中的术语“状态”可能会启发你使用具有状态的神经网络来实现该结构。在下一节中,我们将看到如何应用循环神经网络来设计基于这种“编码器-解码器”结构的序列转换模型。 ## 小结 -* 编码器-解码器结构可以处理可变长度序列的输入和输出,因此适用于机器翻译等序列转换问题。 -* 编码器以可变长度序列作为输入,将其转换为具有固定形状的状态。 -* 解码器将固定形状的编码状态映射到可变长度序列。 +* “编码器-解码器”结构可以处理长度可变的序列作为输入和输出,因此适用于机器翻译等序列转换问题。 +* 编码器将长度可变的序列作为输入,并将其转换为具有形状固定的状态。 +* 解码器将形状固定的编码状态映射为长度可变的序列。 ## 练习 -1. 假设我们使用神经网络来实现编解码结构。编码器和解码器必须是同一类型的神经网络吗? -1. 除了机器翻译,你能想到另一个可以应用编码器-解码器结构的应用吗? +1. 假设我们使用神经网络来实现“编码器-解码器”结构。那么编码器和解码器必须是同一类型的神经网络吗? +1. 除了机器翻译,你能想到另一个可以适用于”编码器-解码器“结构的应用吗? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/341) From 7698c20e4ae0e2b0cf0aebe65e898d4701aa2188 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Thu, 6 May 2021 02:35:15 +0800 Subject: [PATCH 083/103] Chapter recurrent modern/machine translation and dataset (#767) * fix errors in 9.5. Machine Translation and the Dataset * fix errors in 9.5.1. Downloading and Preprocessing the Dataset * fix errors in 9.5.2. Tokenization fix errors in 9.5.3. Vocabulary * fix errors in 9.5.4. Loading the Dataset * the end of fix errors 9.5 --- .../machine-translation-and-dataset.md | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/chapter_recurrent-modern/machine-translation-and-dataset.md b/chapter_recurrent-modern/machine-translation-and-dataset.md index 18eab9ee3..53f8c7e8f 100644 --- a/chapter_recurrent-modern/machine-translation-and-dataset.md +++ b/chapter_recurrent-modern/machine-translation-and-dataset.md @@ -1,12 +1,11 @@ # 机器翻译与数据集 :label:`sec_machine_translation` -我们已经使用循环神经网络来设计语言模型,这是自然语言处理的关键。另一个旗舰基准测试是“机器翻译”,这是将输入序列转换成输出序列的序列转换模型的的核心问题。序列转换模型在各种现代人工智能应用中发挥着至关重要的作用,将成为本章剩余部分和 :numref:`chap_attention` 的重点。为此,本节介绍机器翻译问题及其稍后将使用的数据集。 +我们已经使用循环神经网络来设计语言模型,这是自然语言处理的关键。另一个最佳的基准测试是“机器翻译”,这是将输入序列转换成输出序列的序列转换模型的的核心问题。序列转换模型在各种现代人工智能应用中发挥着至关重要的作用,将成为本章剩余部分和 :numref:`chap_attention` 的重点。为此,本节介绍机器翻译问题及其稍后将使用的数据集。 -*机器翻译*指的是将序列从一种语言自动翻译成另一种语言。事实上,这个领域可能可以追溯到数字计算机发明后不久的20世纪40年代,在第二次世界大战中就使用计算机破解语言编码。几十年来,在使用神经网络进行端到端学习的兴起之前,统计学方法在这一领域一直占据主导地位 :cite:`Brown.Cocke.Della-Pietra.ea.1988,Brown.Cocke.Della-Pietra.ea.1990` 。基于神经网络的方法通常被称为*神经机器翻译*从而将自己与*统计机器翻译*区分开。 -这涉及翻译模型和语言模型等组成部分的统计分析。 +*机器翻译*(machine translation)指的是将序列从一种语言自动翻译成另一种语言。事实上,这个领域可以追溯到数字计算机发明后不久的20世纪40年代,特别是在第二次世界大战中使用计算机破解语言编码。几十年来,在使用神经网络进行端到端学习的兴起之前,统计学方法在这一领域一直占据主导地位 :cite:`Brown.Cocke.Della-Pietra.ea.1988,Brown.Cocke.Della-Pietra.ea.1990` 。因为 *统计机器翻译*(statistical machine translation)涉及了翻译模型和语言模型等组成部分的统计分析,因此基于神经网络的方法通常被称为 *神经机器翻译*(neural machine translation),用于将两种翻译模型区分开来。 -这本书强调端到端的学习,将重点放在神经机器翻译方法上。与 :numref:`sec_language_model` 中的语言模型问题(语料库是单一语言的)不同,机器翻译数据集是由源语言和目标语言的文本序列对组成的。因此,我们需要一种不同的方法来预处理机器翻译数据集,而不是复用语言模型的预处理程序。在下面,我们将展示如何将预处理后的数据加载到小批量中进行训练。 +这本书强调端到端的学习,其重点放在神经机器翻译方法上。与 :numref:`sec_language_model` 中的语言模型问题(语料库是单一语言的)不同,机器翻译数据集是由源语言和目标语言的文本序列对组成的。因此,我们需要一种不同的方法来预处理机器翻译数据集,而不是复用语言模型的预处理程序。在下面,我们将展示如何将预处理后的数据加载到小批量中进行训练。 ```{.python .input} from d2l import mxnet as d2l @@ -31,7 +30,7 @@ import os ## 下载和预处理数据集 -首先,我们下载一个由[Tatoeba项目的双语句子对](http://www.manythings.org/anki/)组成的英-法数据集。数据集中的每一行都是一对制表符分隔的英文文本序列和翻译后的法语文本序列。请注意,每个文本序列可以是一个句子,也可以是包含多个句子的一段。在这个将英语翻译成法语的机器翻译问题中,英语是“源语言”(source language),法语是“目标语言”(target language)。 +首先,我们下载一个由[Tatoeba项目的双语句子对](http://www.manythings.org/anki/)组成的英-法数据集。数据集中的每一行都是一对制表符分隔的英文文本序列和翻译后的法语文本序列。请注意,每个文本序列可以是一个句子,也可以是包含多个句子的一个段落。在这个将英语翻译成法语的机器翻译问题中,英语是 *源语言*(source language),法语是 *目标语言*(target language)。 ```{.python .input} #@tab all @@ -41,7 +40,7 @@ d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip', #@save def read_data_nmt(): - """Load the English-French dataset.""" + """载入“英语-法语”数据集。""" data_dir = d2l.download_extract('fra-eng') with open(os.path.join(data_dir, 'fra.txt'), 'r') as f: return f.read() @@ -50,20 +49,20 @@ raw_text = read_data_nmt() print(raw_text[:75]) ``` -下载数据集后,我们对原始文本数据进行几个预处理步骤。例如,我们用单个空格代替连续多个空格,将大写字母转换为小写字母,并在单词和标点符号之间插入空格。 +下载数据集后,经过几个预处理步骤,我们对原始的文本数据进行处理。例如,我们用空格代替 *不间断空格*(non-breaking space),使用小写字母替换大写字母,并在单词和标点符号之间插入空格。 ```{.python .input} #@tab all #@save def preprocess_nmt(text): - """Preprocess the English-French dataset.""" + """预处理“英语-法语”数据集。""" def no_space(char, prev_char): return char in set(',.!?') and prev_char != ' ' - # Replace non-breaking space with space, and convert uppercase letters to - # lowercase ones + # 使用空格替换不间断空格 + # 使用小写字母替换大写字母 text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower() - # Insert space between words and punctuation marks + # 在单词和标点符号之间插入空格 out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char for i, char in enumerate(text)] return ''.join(out) @@ -74,13 +73,13 @@ print(text[:80]) ## 标记化 -与 :numref:`sec_language_model` 中的字符级标记化不同,对于机器翻译,我们更喜欢词级标记化(最先进的模型可能使用更高级的标记化技术)。下面的`tokenize_nmt`函数对前`num_examples`个文本序列对进行标记,其中每个标记要么是一个单词,要么是一个标点符号。此函数返回两个标记列表:`source`和`target`。具体地说,`source[i]`是源语言(这里是英语)第$i$个文本序列的标记列表,`target[i]`是目标语言(这里是法语)的标记。 +与 :numref:`sec_language_model` 中的字符级标记化不同,对于机器翻译,我们更喜欢单词级标记化(最先进的模型可能使用更高级的标记化技术)。下面的`tokenize_nmt`函数对前`num_examples`个文本序列对进行标记,其中每个标记要么是一个单词,要么是一个标点符号。此函数返回两个标记列表:`source`和`target`。具体地说,`source[i]`是源语言(这里是英语)第 $i$ 个文本序列的标记列表,`target[i]`指的是目标语言(这里是法语)的标记。 ```{.python .input} #@tab all #@save def tokenize_nmt(text, num_examples=None): - """Tokenize the English-French dataset.""" + """标记化“英语-法语”数据数据集。""" source, target = [], [] for i, line in enumerate(text.split('\n')): if num_examples and i > num_examples: @@ -95,7 +94,7 @@ source, target = tokenize_nmt(text) source[:6], target[:6] ``` -让我们绘制每个文本序列的标记数量的直方图。在这个简单的英法数据集中,大多数文本序列的标记少于20个。 +让我们绘制每个文本序列所包含的标记数量的直方图。在这个简单的英法数据集中,大多数文本序列的标记数量少于20个。 ```{.python .input} #@tab all @@ -108,9 +107,9 @@ for patch in patches[1].patches: d2l.plt.legend(loc='upper right'); ``` -## 词表 +## 词汇表 -由于机器翻译数据集由语言对组成,因此我们可以分别为源语言和目标语言构建两个词表。使用词级标记化时,词汇量将明显大于使用字符级标记化时的词汇量。为了缓解这一问题,这里我们将出现次数少于2次的低标记牌视为相同的未知(“<unk>”)令牌。除此之外,我们还指定了额外的特殊标记,例如用于小批量时填充相同长度的序列(“<pad>”),以及序列的开始标记(“<bos>”)和结束标记(“<eos>”)。这样的特殊标记在自然语言处理任务中比较常用。 +由于机器翻译数据集由语言对组成,因此我们可以分别为源语言和目标语言构建两个词汇表。使用单词级标记化时,词汇量将明显大于使用字符级标记化时的词汇量。为了缓解这一问题,这里我们将出现次数少于2次的低频率标记视为相同的未知(“<unk>”)标记。除此之外,我们还指定了额外的特定标记,例如在小批量时用于将序列填充到相同长度的填充标记(“<pad>”),以及序列的开始标记(“<bos>”)和结束标记(“<eos>”)。这些特殊标记在自然语言处理任务中比较常用。 ```{.python .input} #@tab all @@ -122,11 +121,11 @@ len(src_vocab) ## 加载数据集 :label:`subsec_mt_data_loading` -回想一下,在语言模型中,每个序列样本,一个句子的一段或多个句子的跨度,都有一个固定的长度。都有固定的长度。这是由 :numref:`sec_language_model` 中的`num_steps`(时间步数或标记数)参数指定的。在机器翻译中,每个样本都是一对源和目标文本序列,其中每个文本序列可以具有不同的长度。 +回想一下,在语言模型中,一个序列无论是只有一个句子的部分还是跨越了多个句子的范围,这个样本序列都有一个固定的长度。这个固定长度是由 :numref:`sec_language_model` 中的`num_steps`(时间步数或标记数)参数指定的。在机器翻译中,每个样本都是由源和目标组成的文本序列对,其中的每个文本序列可以具有不同的长度。 -为了提高计算效率,我们仍然可以通过*截断*和*填充*一次处理一小批量文本序列。假设同一小批量中的每个序列应该具有相同的长度`num_steps`。如果文本序列的标记少于`num_steps`个,我们将继续在其末尾附加特殊的“<pad>”标记,直到其长度达到`num_steps`。否则,我们将截断文本序列,只取其前`num_steps`个令牌,并丢弃其余的标记。这样,每个文本序列将具有相同的长度,以便以相同形状的小批量加载。 +为了提高计算效率,我们仍然可以通过 *截断* 和 *填充* 方法实现一次只处理一小批量文本序列。假设同一小批量中的每个序列应该具有相同的长度`num_steps`。如果文本序列的标记数目少于`num_steps`个,我们将继续在其末尾添加特定的“<pad>”标记,直到其长度达到`num_steps`。反之,我们将截断文本序列,只取其前`num_steps`个标记,并且丢弃剩余的标记。这样,每个文本序列将具有相同的长度,以便以相同形状的小批量进行加载。 -以下`truncate_pad`函数如前所述截断或填充文本序列。 +下面的`truncate_pad`函数如前所述地截断或填充文本序列。 ```{.python .input} #@tab all @@ -140,7 +139,7 @@ def truncate_pad(line, num_steps, padding_token): truncate_pad(src_vocab[source[0]], 10, src_vocab['']) ``` -现在我们定义一个函数,将文本序列转换成小批量进行训练。我们将特殊的“<eos>”标记附加到每个序列的末尾,以指示序列的结束。当模型通过一个接一个地生成序列令牌进行预测时,“<eos>”令牌的生成可以暗示输出序列是完整的。此外,我们还记录了不包括填充标记的每个文本序列的长度。我们稍后将介绍的一些模型将需要此信息。 +现在我们定义一个函数,将文本序列转换成小批量用于训练。我们将特定的“<eos>”标记添加到每个序列的末尾,以表示序列的结束。当模型通过一个接一个地生成序列标记进行预测时,“<eos>”标记的生成可以说明输出序列是完成了的。此外,我们还记录了不包括填充标记的每个文本序列的长度,在稍后将要介绍的一些模型会需要此信息。 ```{.python .input} #@tab all @@ -178,7 +177,7 @@ def load_data_nmt(batch_size, num_steps, num_examples=600): return data_iter, src_vocab, tgt_vocab ``` -让我们读出英语-法语数据集中的第一个小批量数据。 +让我们读出“英语-法语”数据集中的第一个小批量数据。 ```{.python .input} #@tab all @@ -194,13 +193,13 @@ for X, X_valid_len, Y, Y_valid_len in train_iter: ## 小结 * 机器翻译是指将文本序列从一种语言自动翻译成另一种语言。 -* 使用词级标记化时的词汇量,将明显大于使用字符级标记化时的词汇量。为了缓解这一问题,我们可以将低频标记视为相同的未知标记。 -* 我们可以截断和填充文本序列,以便所有文本序列都具有相同的长度,以便以小批量方式加载。 +* 使用单词级标记化时的词汇量,将明显大于使用字符级标记化时的词汇量。为了缓解这一问题,我们可以将低频标记视为相同的未知标记。 +* 通过截断和填充文本序列,以便所有的文本序列都具有相同的长度,方便以小批量方式加载。 ## 练习 1. 在`load_data_nmt`函数中尝试`num_examples`参数的不同值。这对源语言和目标语言的词汇量有何影响? -1. 某些语言(例如中文和日语)的文本没有单词边界指示符(例如,空格)。对于这种情况,词级标记化仍然是个好主意吗?为什么? +1. 某些语言(例如中文和日语)的文本没有单词边界指示符(例如,空格)。对于这种情况,单词级标记化仍然是个好主意吗?为什么? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/344) @@ -208,4 +207,4 @@ for X, X_valid_len, Y, Y_valid_len in train_iter: :begin_tab:`pytorch` [Discussions](https://discuss.d2l.ai/t/1060) -:end_tab: +:end_tab: \ No newline at end of file From 6e1fbc48d4c245828ea70d4c844fa79f3b1195ca Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Thu, 6 May 2021 02:35:52 +0800 Subject: [PATCH 084/103] Chapter recurrent modern/beam search (#769) * fix errors in 9.8. Beam Search * fix errors in 9.8.1. Greedy Search * fix errors in 9.8.2. Exhaustive Search * fix errors in 9.8.3. Beam Search the end of fix errors in 9.8. --- chapter_recurrent-modern/beam-search.md | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/chapter_recurrent-modern/beam-search.md b/chapter_recurrent-modern/beam-search.md index 6e47e1e65..13be01b4c 100644 --- a/chapter_recurrent-modern/beam-search.md +++ b/chapter_recurrent-modern/beam-search.md @@ -1,47 +1,47 @@ # 束搜索 :label:`sec_beam-search` -在:numref:`sec_seq2seq`中,我们逐个标记地预测输出序列令牌,直到预测出序列结束标记“<eos>”。在本节中,我们将首先对这种*贪心搜索*(greedy search)策略进行介绍,并探讨其存在的问题,然后将这种策略与其他替代策略进行比较:*穷举搜索*(exhaustive search)和*束搜索*(beam search)。 +在:numref:`sec_seq2seq`中,我们逐个地预测输出序列的标记,直到预测序列中出现序列结束标记“<eos>”。在本节中,我们将首先对这种 *贪心搜索*(greedy search)策略进行介绍,并探讨其存在的问题,然后对比这种策略与其他替代策略:*穷举搜索*(exhaustive search)和*束搜索*(beam search)。 -在正式介绍贪心搜索之前,让我们使用 :numref:`sec_seq2seq` 中相同的数学符号定义搜索问题。在任何时间步$t'$,解码器输出$y_{t'}$的概率取决于$t'$之前的输出子序列$y_1, \ldots, y_{t'-1}$和编码输入序列信息的上下文变量$\mathbf{c}$。为了量化计算成本,用$\mathcal{Y}$(它包含“<eos>”)表示输出词汇表。所以这个词汇集合的基数$\left|\mathcal{Y}\right|$就是词汇大小。我们还将输出序列的最大标记数指定为$T'$。因此,我们的目标是从所有$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$个可能的输出序列中寻找理想的输出。当然,对于所有这些输出序列,包括“<eos>”和之后的部分将在实际输出中丢弃。 +在正式介绍贪心搜索之前,让我们使用 :numref:`sec_seq2seq` 中相同的数学符号定义搜索问题。在任意时间步 $t'$,解码器输出 $y_{t'}$ 的概率取决于时间步 $t'$ 之前的输出子序列 $y_1, \ldots, y_{t'-1}$ 和输入序列的信息编码成的上下文变量 $\mathbf{c}$。为了量化计算成本,用 $\mathcal{Y}$(它包含“<eos>”)表示输出词汇表。所以这个词汇集合的基数 $\left|\mathcal{Y}\right|$ 就是词汇表的大小。我们还将输出序列的最大标记数指定为 $T'$。因此,我们的目标是从所有 $\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$ 个可能的输出序列中寻找理想的输出。当然,对于所有输出序列,这些序列中包含的“<eos>”及其之后的部分将在实际输出中丢弃。 ## 贪心搜索 -首先,让我们看看一个简单的策略:*贪心搜索*。该策略已用于:numref:`sec_seq2seq`的序列预测。在贪心搜索中,在输出序列的任何时间步$t'$,我们从$\mathcal{Y}$中搜索具有最高条件概率的标记,即: +首先,让我们看看一个简单的策略:*贪心搜索*。该策略已用于:numref:`sec_seq2seq`的序列预测。对于输出序列的任何时间步 $t'$,我们都将基于贪心搜索从 $\mathcal{Y}$ 中找到具有最高条件概率的标记,即: $$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} P(y \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$$ -一旦输出“<eos>”或输出序列达到其最大长度$T'$,输出序列即完成。 +一旦输出序列包含了“<eos>”或者达到其最大长度 $T'$,则输出完成。 -那么贪心搜索会出什么问题呢?实际上,*最优序列*(optimal sequence)应该是最大化$\prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$值的输出序列,这是基于输入序列生成输出序列的条件概率。不幸的是,不能保证通过贪心搜索得到最优序列。 +那么贪心搜索存在什么问题呢?实际上,*最优序列*(optimal sequence)应该是最大化 $\prod_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c})$ 值的输出序列,这是基于输入序列生成输出序列的条件概率。不幸的是,无法保证通过贪心搜索得到最优序列。 ![在每个时间步,贪心搜索选择具有最高条件概率的标记。](../img/s2s-prob1.svg) :label:`fig_s2s-prob1` -让我们用一个例子来说明这一点。假设输出中有四个标记“A”、“B”、“C”和“<eos>”。 在:numref:`fig_s2s-prob1` 中,每个时间步下的四个数字分别表示在该时间步生成“A”、“B”、“C”和“<eos>”的条件概率。在每个时间步,贪心搜索选择具有最高条件概率的令牌。因此,将在 :numref:`fig_s2s-prob1` 中预测输出序列“A”、“B”、“C”和“<eos>”。这个输出序列的条件概率是$0.5\times0.4\times0.4\times0.6 = 0.048$。 +让我们用一个例子来描述。假设输出中有四个标记“A”、“B”、“C”和“<eos>”。 在:numref:`fig_s2s-prob1` 中,每个时间步下的四个数字分别表示在该时间步生成“A”、“B”、“C”和“<eos>”的条件概率。在每个时间步,贪心搜索选择具有最高条件概率的标记。因此,将在 :numref:`fig_s2s-prob1` 中预测输出序列“A”、“B”、“C”和“<eos>”。这个输出序列的条件概率是$0.5\times0.4\times0.4\times0.6 = 0.048$。 ![每个时间步下的四个数字表示在该时间步生成“A”、“B”、“C”和“<eos>”的条件概率。在时间步2,选择具有第二高条件概率的令牌“C”。](../img/s2s-prob2.svg) :label:`fig_s2s-prob2` -接下来,让我们看看 :numref:`fig_s2s-prob2` 中的另一个例子。与 :numref:`fig_s2s-prob1` 不同,在时间步2中,我们选择 :numref:`fig_s2s-prob2` 中的令牌“C”,它具有第二高的条件概率。由于时间步3所基于的时间步1和2处的输出子序列已从 :numref:`fig_s2s-prob1` 中的“A”和“B”改变为 :numref:`fig_s2s-prob2` 中的“A”和“C”,因此时间步3处的每个标记的条件概率也在 :numref:`fig_s2s-prob2` 中改变。假设我们在时间步3选择令牌“B”。现在,时间步4以前三个时间步“A”、“C”和“B”的输出子序列为条件,这与 :numref:`fig_s2s-prob1` 中的“A”、“B”和“C”不同。因此,在 :numref:`fig_s2s-prob2` 中的时间步4生成每个标记的条件概率也不同于 :numref:`fig_s2s-prob1` 中的条件概率。结果,:numref:`fig_s2s-prob2`中的输出序列“A”、“C”、“B”和“<eos>”的条件概率为$0.5\times0.3 \times0.6\times0.6=0.054$,这大于:numref:`fig_s2s-prob1`中的贪心搜索的条件概率。在本例中,通过贪心搜索获得的输出序列“A”、“B”、“C”和“<eos>”不是最佳序列。 +接下来,让我们看看 :numref:`fig_s2s-prob2` 中的另一个例子。与 :numref:`fig_s2s-prob1` 不同,在时间步2中,我们选择 :numref:`fig_s2s-prob2` 中的标记“C”,它具有 *第二* 高的条件概率。由于时间步3所基于的时间步1和2处的输出子序列已从 :numref:`fig_s2s-prob1` 中的“A”和“B”改变为 :numref:`fig_s2s-prob2` 中的“A”和“C”,因此时间步3处的每个标记的条件概率也在 :numref:`fig_s2s-prob2` 中改变。假设我们在时间步3选择标记“B”。现在,时间步4以前三个时间步“A”、“C”和“B”的输出子序列为条件,这与 :numref:`fig_s2s-prob1` 中的“A”、“B”和“C”不同。因此,在 :numref:`fig_s2s-prob2` 中的时间步4生成每个标记的条件概率也不同于 :numref:`fig_s2s-prob1` 中的条件概率。结果,:numref:`fig_s2s-prob2`中的输出序列“A”、“C”、“B”和“<eos>”的条件概率为$0.5\times0.3 \times0.6\times0.6=0.054$,这大于:numref:`fig_s2s-prob1`中的贪心搜索的条件概率。在本例中,通过贪心搜索获得的输出序列“A”、“B”、“C”和“<eos>”不是最佳序列。 ## 穷举搜索 -如果目标是获得最优序列,我们可以考虑使用*穷举搜索*(exhaustive search):穷举地枚举所有可能的输出序列及其条件概率,然后输出条件概率最高的一个。 +如果目标是获得最优序列,我们可以考虑使用 *穷举搜索*(exhaustive search):穷举地枚举所有可能的输出序列及其条件概率,然后输出条件概率最高的一个。 -虽然我们可以使用穷举搜索来获得最优序列,但其计算量$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$可能过高。例如,当$|\mathcal{Y}|=10000$和$T'=10$时,我们需要评估$10000^{10} = 10^{40}$序列。这几乎是不可能的。另一方面,贪心搜索的计算量是$\mathcal{O}(\left|\mathcal{Y}\right|T')$:它通常明显小于穷举搜索。例如,当$|\mathcal{Y}|=10000$和$T'=10$时,我们只需要评估$10000\times10=10^5$个序列。 +虽然我们可以使用穷举搜索来获得最优序列,但其计算量 $\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$ 可能过高。例如,当 $|\mathcal{Y}|=10000$ 和 $T'=10$ 时,我们需要评估 $10000^{10} = 10^{40}$ 序列。这几乎是不可能的。另一方面,贪心搜索的计算量是 $\mathcal{O}(\left|\mathcal{Y}\right|T')$:它通常明显小于穷举搜索。例如,当 $|\mathcal{Y}|=10000$ 和 $T'=10$ 时,我们只需要评估 $10000\times10=10^5$ 个序列。 ## 束搜索 -关于序列搜索策略的决定取决于一个范围,在任何一个极端都有问题。如果只有准确性才重要呢?显然,穷举搜索。如果计算成本很重要呢?显然,贪心搜索。实际应用介于这两个极端之间。 +决定序列搜索策略取决于一个范围,在任何一个极端情况下都有问题。如果只有准确性最重要?则显然是穷举搜索。如果计算成本最重要?则显然是贪心搜索。实际应用则介于这两个极端之间。 -*束搜索*(beam search)是贪心搜索的改进版本。它有一个超参数,名为*束宽*(beam size)$k$。 -在时间步1,我们选择具有最高条件概率的$k$个标记。它们中的每一个将分别是$k$个候选输出序列的第一个标记。在随后的每个时间步,基于上一时间步的$k$个候选输出序列,我们继续从$k\left|\mathcal{Y}\right|$个可能的选择中选择具有最高条件概率的$k$个候选输出序列。 +*束搜索*(beam search)是贪心搜索的改进版本。它有一个超参数,名为 *束宽*(beam size)$k$。 +在时间步 $1$,我们选择具有最高条件概率的 $k$ 个标记。这 $k$ 个标记将分别是 $k$ 个候选输出序列的第一个标记。在随后的每个时间步,基于上一时间步的 $k$ 个候选输出序列,我们将继续从 $k\left|\mathcal{Y}\right|$ 个可能的选择中挑出具有最高条件概率的 $k$ 个候选输出序列。 ![束搜索过程(束宽:2,输出序列的最大长度:3)。候选输出序列是$A$、$C$、$AB$、$CE$、$ABD$和$CED$。](../img/beam-search.svg) :label:`fig_beam-search` -:numref:`fig_beam-search`演示了束搜索的过程。假设输出词表只包含五个元素:$\mathcal{Y} = \{A, B, C, D, E\}$,其中一个是“<eos>”。让束宽为2,输出序列的最大长度为3。在时间步1,假设具有最高条件概率$P(y_1 \mid \mathbf{c})$的标记是$A$和$C$。在时间步2,我们计算所有$y_2 \in \mathcal{Y}$: +:numref:`fig_beam-search`演示了束搜索的过程。假设输出的词汇表只包含五个元素:$\mathcal{Y} = \{A, B, C, D, E\}$,其中有一个是“<eos>”。设置束宽为2,输出序列的最大长度为3。在时间步1,假设具有最高条件概率$P(y_1 \mid \mathbf{c})$的标记是$A$和$C$。在时间步2,我们计算所有$y_2 \in \mathcal{Y}$: $$\begin{aligned}P(A, y_2 \mid \mathbf{c}) = P(A \mid \mathbf{c})P(y_2 \mid A, \mathbf{c}),\\ P(C, y_2 \mid \mathbf{c}) = P(C \mid \mathbf{c})P(y_2 \mid C, \mathbf{c}),\end{aligned}$$ @@ -49,16 +49,16 @@ $$\begin{aligned}P(A, y_2 \mid \mathbf{c}) = P(A \mid \mathbf{c})P(y_2 \mid A, \ $$\begin{aligned}P(A, B, y_3 \mid \mathbf{c}) = P(A, B \mid \mathbf{c})P(y_3 \mid A, B, \mathbf{c}),\\P(C, E, y_3 \mid \mathbf{c}) = P(C, E \mid \mathbf{c})P(y_3 \mid C, E, \mathbf{c}),\end{aligned}$$ -然后从这十个值中选择最大的两个,即$P(A, B, D \mid \mathbf{c})$和$P(C, E, D \mid \mathbf{c}).$。结果,我们得到六个候选输出序列:(1)$A$;(2)$C$;(3)$B$;(4)$C$、$E$;(5)$A$、$B$、$D$以及(6)$C$、$D$。 +从这十个值中选择最大的两个,即$P(A, B, D \mid \mathbf{c})$和$P(C, E, D \mid \mathbf{c})$。结果,我们得到六个候选输出序列:(1)$A$;(2)$C$;(3)$A,B$;(4)$C,E$;(5)$A,B,D$ ;(6)$C,E,D$。 -最后,我们基于这六个序列(例如,包括“<eos>”和之后的丢弃部分)获得最终候选输出序列集合。然后我们选择以下得分最高的序列作为输出序列: +最后,我们基于这六个序列(例如,丢弃包括“<eos>”和之后的部分)获得最终候选输出序列集合。然后我们选择以下得分最高的序列作为输出序列: $$ \frac{1}{L^\alpha} \log P(y_1, \ldots, y_{L}) = \frac{1}{L^\alpha} \sum_{t'=1}^L \log P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}),$$ :eqlabel:`eq_beam-search-score` -其中$L$是最终候选序列的长度,$\alpha$通常设置为0.75。因为一个较长的序列在:eqref:`eq_beam-search-score`的总和中有更多的对数项,分母中的$L^\alpha$惩罚长序列。 +其中 $L$ 是最终候选序列的长度,$\alpha$ 通常设置为0.75。因为一个较长的序列在:eqref:`eq_beam-search-score`的求和中会有更多的对数项,因此分母中的 $L^\alpha$ 用于惩罚长序列。 -束搜索的计算量为$\mathcal{O}(k\left|\mathcal{Y}\right|T')$。这个结果介于贪心搜索和穷举搜索之间。实际上,贪心搜索可以看作是一种特殊类型的束搜索,束宽为1。通过灵活选择束宽,束搜索可以在精度和计算成本之间进行权衡。 +束搜索的计算量为 $\mathcal{O}(k\left|\mathcal{Y}\right|T')$。这个结果介于贪心搜索和穷举搜索之间。实际上,贪心搜索可以看作是一种束宽为1的特殊类型的束搜索。通过灵活地选择束宽,束搜索可以在精度和计算成本之间进行权衡。 ## 小结 @@ -67,8 +67,8 @@ $$ \frac{1}{L^\alpha} \log P(y_1, \ldots, y_{L}) = \frac{1}{L^\alpha} \sum_{t'=1 ## 练习 -1. 我们能把穷举搜索看作一种特殊的束搜索吗? -1. 在 :numref:`sec_seq2seq` 机器翻译问题中应用束搜索。束宽如何影响结果和预测速度? -1. 在 :numref:`sec_rnn_scratch` 中,我们使用语言模型来生成用户提供前缀的文本。它使用了哪种搜索策略?你能改进一下吗? +1. 我们可以把穷举搜索看作一种特殊的束搜索吗?为什么? +1. 在 :numref:`sec_seq2seq` 的机器翻译问题中应用束搜索。束宽如何影响结果和预测速度? +1. 在 :numref:`sec_rnn_scratch` 中,我们使用语言模型来生成用户提供前缀的文本。它使用了哪种搜索策略?你能改进吗? -[Discussions](https://discuss.d2l.ai/t/338) +[Discussions](https://discuss.d2l.ai/t/338) \ No newline at end of file From d3c04cd63207355d7a0be3d6dfeadf783347fc35 Mon Sep 17 00:00:00 2001 From: "zYx.Tom" Date: Thu, 6 May 2021 02:36:37 +0800 Subject: [PATCH 085/103] Chapter recurrent neural networks/sequence (#781) * fix errors in 8.1. Sequence Models * fix errors in 8.1.1. Statistical Tools * fix errors in 8.1.1.1. Augoregressive Models * fix errors in 8.1.1.2. Markov Models * fix errors in 8.1.1.3. Causality * fix errors in 8.1.2. Training * fix errors in 8.1.3. Prediction the end of fixing errors in 8.1. --- chapter_recurrent-neural-networks/sequence.md | 111 +++++++++--------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/chapter_recurrent-neural-networks/sequence.md b/chapter_recurrent-neural-networks/sequence.md index 1eeea492c..24db23b5a 100644 --- a/chapter_recurrent-neural-networks/sequence.md +++ b/chapter_recurrent-neural-networks/sequence.md @@ -1,22 +1,22 @@ # 序列模型 :label:`sec_sequence` -想象一下你正在看Netflix(一个国外的视频网站)上的电影。作为一个优秀的Netflix用户,你决定对每一部电影都给出评价。毕竟,一部好电影就是一部好电影,你想看更多的好电影,对吗?事实证明,事情并不是那么简单。随着时间的推移,人们对电影的看法会发生很大的变化。事实上,心理学家甚至对某些效应有了命名: +想象一下你正在看 Netflix(一个国外的视频网站)上的电影。作为一个很棒的 Netflix 用户,你决定对每一部电影都给出评价。毕竟,一部好的电影值得好电影的称呼,而且你想看更多的好电影,对吧?事实证明,事情并不那么简单。随着时间的推移,人们对电影的看法会发生很大的变化。事实上,心理学家甚至对某些效应起了名字: -* 根据别人的意见,有“锚定”。例如,奥斯卡颁奖后,相应电影的评分上升,尽管它仍然是同一部电影。这种影响持续几个月,直到奖项被遗忘。结果表明,这种效应使评分提高了半个百分点以上 -:cite:`Wu.Ahmed.Beutel.ea.2017`. -* 有一种“享乐适应”,即人类迅速适应,接受一种改善或恶化的情况作为新的常态。例如,在看了很多好电影之后,人们对下一部电影同样好或更好的期望很高。因此,即使是一部普通的电影,在看过许多精彩的电影之后,也可能被认为是糟糕的。 -* 有季节性。很少有观众喜欢在八月看圣诞老人的电影。 -* 在某些情况下,由于导演或演员在制作中的不当行为,电影变得不受欢迎。 -* 有些电影在小圈子内被支持者喜爱及推崇,这是因为它们几乎滑稽可笑。 +* *锚定*(anchoring),基于其他人的意见。例如,奥斯卡颁奖后,受到关注的电影的评分会上升,尽管它还是原来那部电影。这种影响将持续几个月,直到人们忘记了这部电影曾经获得的奖项。结果表明,这种效应会使评分提高半个百分点以上 + :cite:`Wu.Ahmed.Beutel.ea.2017`. +* *享乐适应*(hedonic adaption),即人类迅速接受并且适应一种更好或者更坏的情况作为新的常态。例如,在看了很多好电影之后,人们期望下一部电影会同样好或者更好。因此,在看过许多精彩的电影之后,即使是一部普通的电影也可能被认为是糟糕的。 +* *季节性*(seasonality)。少有观众喜欢在八月看圣诞老人的电影。 +* 有时候,电影会由于导演或演员在制作中的不当行为变得不受欢迎。 +* 有些电影因为其极度糟糕只能成为小众电影。*Plan 9 from Outer Space* 和 *Troll 2* 就因为这个原因而臭名昭著的。 简而言之,电影评分决不是固定不变的。因此,使用时间动力学可以得到更准确的电影推荐 :cite:`Koren.2009` 。当然,序列数据不仅仅是关于电影评分的。下面给出了更多的场景。 -* 许多用户在打开应用程序时都有非常特殊的行为。例如,社交媒体应用在学生放学后更受欢迎。股市交易应用程序在市场开放时更常用。 -* 要预测明天的股价要比填补我们昨天错过股价的空白困难得多,尽管两者都只是估计一个数字。毕竟,先见之明比事后诸葛亮难得多。在统计学中,前者(超出已知观测值的预测)称为*外推*(extrapolation),而后者(在现有观测值之间进行估计)称为*内插*(interpolation)。 -* 音乐、语音、文本和视频在本质上都是连续的。如果我们对它们进行置换,它们就没什么意义了。文本标题“狗咬人”远没有“人咬狗”那么令人惊讶,尽管两句话词的组成完全相同。 -* 地震具有很强的相关性,即大地震发生后,很可能会有几次较小的余震,比没有强震的余震要大得多。事实上,地震是时空相关的,也就是说,余震通常发生在很短的时间跨度和很近的距离内。 -* 人类之间的互动是连续的,这可以从推特上的争吵和辩论中看出。 +* 在使用应用程序时许多用户都有很强的特定习惯。例如,在学生放学后社交媒体应用更受欢迎。在市场开放时股市交易软件更常用。 +* 预测明天的股价要比填补昨天遗失的股价的更困难,尽管两者都只是估计一个数字。毕竟,先见之明比事后诸葛亮难得多。在统计学中,前者(超出已知观测值的预测)称为 *外推*(extrapolation),而后者(在现有观测值之间进行估计)称为 *内插*(interpolation)。 +* 在本质上音乐、语音、文本和视频都是连续的。如果我们对它们进行序列重排,它们就会失去意义。文本标题“狗咬人”远没有“人咬狗”那么令人惊讶,尽管组成两句话的字完全相同。 +* 地震具有很强的相关性,即大地震发生后,很可能会有几次较小的余震,这些余震比没有强震的余震要大得多。事实上,地震是时空相关的,也就是说,余震通常发生在很短的时间跨度和很近的距离内。 +* 人类之间的互动也是连续的,这可以从推特上的争吵和辩论中看出。 ## 统计工具 @@ -26,36 +26,37 @@ :width:`400px` :label:`fig_ftse100` -让我们用$x_t$表示价格。即在*时间步*(time step)$t \in \mathbb{Z}^+$时,我们观察到的价格$x_t$。请注意,对于本文中的序列,$t$通常是离散的,并随整数或其子集而变化。假设一个想在$t$日股市表现良好的交易员通过以下途径预测了$x_t$: +让我们用 $x_t$ 表示价格。即在 *时间步*(time step)$t \in \mathbb{Z}^+$时,我们观察到的价格 $x_t$。请注意,对于本文中的序列,$t$ 通常是离散的,并随整数或其子集而变化。假设一个交易员想在 $t$ 日股市表现良好的,于是通过以下途径预测了 $x_t$: $$x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1).$$ ### 自回归模型 -为了实现这一点,我们的交易员可以使用回归模型,比如我们在 :numref:`sec_linear_concise` 中训练的模型。只有一个主要问题:输入$x_{t-1}, \ldots, x_1$的数量因$t$而异。也就是说,这个数字随着我们遇到的数据量的增加而增加,我们需要一个近似值来使这个计算变得容易处理。本章后面的大部分内容将围绕如何有效估计$P(x_t \mid x_{t-1}, \ldots, x_1)$展开。简单地说,它归结为以下两种策略。 +为了实现这一点,交易员可以使用回归模型,比如我们在 :numref:`sec_linear_concise` 中训练的模型。只有一个主要问题:输入 $x_{t-1}, \ldots, x_1$ 的数量因 $t$ 而异。也就是说,这个数字将会随着我们遇到的数据量的增加而增加,因此我们需要一个近似方法来使这个计算变得容易处理。本章后面的大部分内容将围绕着如何有效估计 $P(x_t \mid x_{t-1}, \ldots, x_1)$ 展开。简单地说,它归结为以下两种策略。 -首先,假设相当长的序列$x_{t-1}, \ldots, x_1$实际上不是必需的。在这种情况下,我们可能会满足于长度为$\tau$的一些时间跨度,并且只使用$x_{t-1}, \ldots, x_{t-\tau}$个观测。直接的好处是,现在参数的数量总是相同的,至少对于$t > \tau$。这使我们能够训练一个深层网络,如上所述。这种模型将被称为“自回归模型”(autoregressive models),因为它们实际上是在自己身上执行回归。 +第一种策略,假设在现实情况下相当长的序列 $x_{t-1}, \ldots, x_1$ 可能是不需要的,因此我们只使用观测序列 $x_{t-1}, \ldots, x_{t-\tau}$,并且满足于时间跨度为 $\tau$。现在,获得的最直接的好处就是对于 $t > \tau$ 时参数的数量总是相同的,这就使我们能够训练一个上面提及的深层网络。这种模型被称为 *自回归模型*(autoregressive models),因为它们就是对自己执行回归。 -第二种策略,如 :numref:`fig_sequence-model` 所示,是保留一些过去观测的总结$h_t$,同时除了预测$h_t$之外还更新$\hat{x}_t$。这就产生了估计$x_t$和$\hat{x}_t = P(x_t \mid h_{t})$的模型,并且更新了$h_t = g(h_{t-1}, x_{t-1})$。由于$h_t$从未被观测到,这类模型也被称为*隐变量自回归模型*(latent autoregressive models)。 +第二种策略,如 :numref:`fig_sequence-model` 所示,是保留一些过去观测的总计 $h_t$,同时除了预测 $\hat{x}_t$ 之外还更新 $h_t$。这就产生了估计 $x_t$ 和 $\hat{x}_t = P(x_t \mid h_{t})$ 的模型,并且更新了 $h_t = g(h_{t-1}, x_{t-1})$。由于 $h_t$ 从未被观测到,这类模型也被称为 *隐变量自回归模型*(latent autoregressive models)。 -![潜在自回归模型](../img/sequence-model.svg) +![隐变量自回归模型](../img/sequence-model.svg) :label:`fig_sequence-model` -这两种情况都有一个显而易见的问题,即如何生成训练数据。一个经典方法是使用历史观测来预测下一次的观测。显然,我们并不指望时间会停滞不前。然而,一个常见的假设是,虽然特定值$x_t$可能会改变,但至少序列本身的动力学不会改变。统计学家称不变的动力学为“静止的”。因此,无论我们做什么,我们都将通过以下方式获得整个序列的估计值 +这两种情况都有一个显而易见的问题,即如何生成训练数据。一个经典的方法是使用历史观测来预测下一次的观测。显然,我们并不指望时间会停滞不前。然而,一个常见的假设是序列本身的动力学不会改变,虽然特定值 $x_t$ 可能会改变。这样的假设是合理的,因为新的动力学一定受新数据影响,而我们不可能用目前所掌握的数据来预测新的动力学。统计学家称不变的动力学为 *静止的*(stationary)。因此,无论我们做什么,整个序列的估计值都将通过以下的方式获得 $$P(x_1, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_{t-1}, \ldots, x_1).$$ -注意,如果我们处理离散的对象(如单词),而不是连续的数字,则上述考虑因素仍然有效。唯一的区别是,在这种情况下,我们需要使用分类器而不是回归模型来估计$P(x_t \mid x_{t-1}, \ldots, x_1)$。 +注意,如果我们处理离散的对象(如单词),而不是连续的数字,则上述的考虑仍然有效。唯一的差别是,在这种情况下,我们需要使用分类器而不是回归模型来估计 $P(x_t \mid x_{t-1}, \ldots, x_1)$。 ### 马尔可夫模型 -回想一下,在自回归模型中,我们只使用$x_{t-1}, \ldots, x_{t-\tau}$而不是$x_{t-1}, \ldots, x_1$来估计$x_t$。只要这种近似是准确的,我们就说序列满足*马尔可夫条件*(Markov condition)。特别地,如果$\tau = 1$,我们有一个*一阶马尔可夫模型*(first-order Markov model),$P(x)$由下式给出: +回想一下,在自回归模型的逼近方法中,我们使用 $x_{t-1}, \ldots, x_{t-\tau}$ 而不是 $x_{t-1}, \ldots, x_1$ 来估计 $x_t$。只要这种近似是准确的,我们就说序列满足*马尔可夫条件*(Markov condition)。特别是,如果 $\tau = 1$,得到一个*一阶马尔可夫模型*(first-order Markov model),$P(x)$ 由下式给出: $$P(x_1, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_{t-1}) \text{ where } P(x_1 \mid x_0) = P(x_1).$$ -当$x_t$只假设离散值时,这样的模型特别好,因为在这种情况下,动态规划可以用来沿着链精确地计算值。例如,我们可以高效地计算$P(x_{t+1} \mid x_{t-1})$: +当 $x_t$ 只假设离散值时,这样的模型特别棒,因为在这种情况下,使用动态规划可以沿着马尔可夫链精确地计算结果。例如,我们可以高效地计算$P(x_{t+1} \mid x_{t-1})$: -$$\begin{aligned} +$$ +\begin{aligned} P(x_{t+1} \mid x_{t-1}) &= \frac{\sum_{x_t} P(x_{t+1}, x_t, x_{t-1})}{P(x_{t-1})}\\ &= \frac{\sum_{x_t} P(x_{t+1} \mid x_t, x_{t-1}) P(x_t, x_{t-1})}{P(x_{t-1})}\\ @@ -63,19 +64,19 @@ P(x_{t+1} \mid x_{t-1}) \end{aligned} $$ -利用这一事实,我们只需要考虑到过去观察到的非常短的历史:$P(x_{t+1} \mid x_t, x_{t-1}) = P(x_{t+1} \mid x_t)$。详细介绍动态规划超出了本节的范围。控制和强化学习算法广泛使用这些工具。 +利用这一事实,我们只需要考虑过去观察到的非常短的历史:$P(x_{t+1} \mid x_t, x_{t-1}) = P(x_{t+1} \mid x_t)$。详细介绍动态规划超出了本节的范围。控制算法和强化学习算法广泛使用这些工具。 ### 因果关系 -原则上,倒序展开$P(x_1, \ldots, x_T)$无可厚非。毕竟,通过条件作用,我们总是可以写出: +原则上,倒序展开 $P(x_1, \ldots, x_T)$ 无可厚非。毕竟,基于条件概率公式,我们总是可以写出: $$P(x_1, \ldots, x_T) = \prod_{t=T}^1 P(x_t \mid x_{t+1}, \ldots, x_T).$$ -事实上,如果我们有一个马尔可夫模型,我们也可以得到一个反向条件概率分布。然而,在许多情况下,数据存在一个自然的方向,即在时间上前进。很明显,未来的事件不能影响过去。因此,如果我们改变$x_t$,我们可能能够影响$x_{t+1}$未来发生的事情,但不能影响相反的情况。也就是说,如果我们改变$x_t$,过去事件的分布不会改变。因此,解释$P(x_{t+1} \mid x_t)$应该比解释$P(x_t \mid x_{t+1})$更容易。例如,已经表明,在某些情况下,对于某些加性噪声$\epsilon$,我们可以找到$x_{t+1} = f(x_t) + \epsilon$,而反之则不是真的 :cite:`Hoyer.Janzing.Mooij.ea.2009` 。这是个好消息,因为这通常是我们有兴趣估计的前进方向。彼得斯等人写的这本书。已经解释了关于这个主题的更多内容 :cite:`Peters.Janzing.Scholkopf.2017` 。我们仅仅触及了它的皮毛。 +事实上,如果基于一个马尔可夫模型,我们可以得到一个反向的条件概率分布。然而,在许多情况下,数据存在一个自然的方向,即在时间上是前进的。很明显,未来的事件不能影响过去。因此,如果我们改变 $x_t$,我们可能能够影响 $x_{t+1}$ 未来发生的事情,但不能影响过去。也就是说,如果我们改变 $x_t$,基于过去事件的分布不会改变。因此,解释 $P(x_{t+1} \mid x_t)$ 应该比解释 $P(x_t \mid x_{t+1})$ 更容易。例如,在某些情况下,对于某些可加性噪声 $\epsilon$,显然我们可以找到 $x_{t+1} = f(x_t) + \epsilon$,而反之则不行 :cite:`Hoyer.Janzing.Mooij.ea.2009` 。这是个好消息,因为这通常是我们有兴趣估计的前进方向。彼得斯等人写的这本书。已经解释了关于这个主题的更多内容 :cite:`Peters.Janzing.Scholkopf.2017` 。我们仅仅触及了它的皮毛。 ## 训练 -在回顾了这么多统计工具之后,让我们在实践中尝试一下。我们首先生成一些数据。为了简单起见,我们使用正弦函数和一些加性噪声来生成序列数据,时间步为$1, 2, \ldots, 1000$。 +在回顾了这么多统计工具之后,让我们在实践中尝试一下。首先,生成一些数据。为了简单起见,我们使用正弦函数和一些可加性噪声来生成序列数据,时间步为$1, 2, \ldots, 1000$。 ```{.python .input} %matplotlib inline @@ -116,7 +117,7 @@ x = d2l.sin(0.01 * time) + d2l.normal([T], 0, 0.2) d2l.plot(time, [x], 'time', 'x', xlim=[1, 1000], figsize=(6, 3)) ``` -接下来,我们需要将这样的序列转换为我们的模型可以训练的特征和标签。基于嵌入维度$\tau$,我们将数据映射为$y_t = x_t$和$\mathbf{x}_t = [x_{t-\tau}, \ldots, x_{t-1}]$。精明的读者可能已经注意到,这给我们提供的数据样本少了$\tau$个,因为我们没有足够的历史记录来记录前$\tau$个数据样本。一个简单的解决办法,特别是如果序列很长,就是丢弃这几项。或者,我们可以用零填充序列。在这里,我们仅使用前600个特征-标签对进行训练。 +接下来,我们需要将这样的序列转换为我们的模型可以训练的特征和标签。基于嵌入维度 $\tau$,我们将数据映射为 $y_t = x_t$ 和 $\mathbf{x}_t = [x_{t-\tau}, \ldots, x_{t-1}]$。精明的读者可能已经注意到,这比我们提供的数据样本少了 $\tau$ 个,因为我们没有足够的历史记录来描述前 $\tau$ 个数据样本。一个简单的解决办法,特别是序列如果够长就丢弃这几项,或者可以用零填充序列。在这里,我们仅使用前600个“特征-标签”对进行训练。 ```{.python .input} #@tab mxnet, pytorch @@ -144,10 +145,10 @@ train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True) ``` -这里的结构相当简单:只有一个多层感知机,有两个全连接层、ReLU激活函数和平方损失。 +这里的结构相当简单:只是一个多层感知机,有两个全连接层、ReLU激活函数和平方损失。 ```{.python .input} -# A simple MLP +# 一个简单的多层感知机 def get_net(): net = nn.Sequential() net.add(nn.Dense(10, activation='relu'), @@ -155,7 +156,7 @@ def get_net(): net.initialize(init.Xavier()) return net -# Square loss +# 平方损失 loss = gluon.loss.L2Loss() ``` @@ -192,7 +193,7 @@ def get_net(): loss = tf.keras.losses.MeanSquaredError() ``` -现在我们准备好训练模型了。下面的代码与前面几节中的训练代码实现基本相同,如 :numref:`sec_linear_concise` 。因此,我们不会深入探讨太多细节。 +现在我们准备好等待训练的模型了。下面的代码与前面几节中的训练代码实现方式基本相同,如 :numref:`sec_linear_concise` 。因此,我们不会深入探讨太多细节。 ```{.python .input} def train(net, train_iter, loss, epochs, lr): @@ -249,7 +250,7 @@ train(net, train_iter, loss, 5, 0.01) ## 预测 -由于训练损失很小,我们希望我们的模型能够很好地工作。让我们看看这在实践中意味着什么。首先要检查的是模型预测下一时间步发生事情的能力有多好,也就是“单步预测”(one-step-ahead prediction)。 +由于训练损失很小,我们希望模型能够很好地工作。让我们看看这在实践中意味着什么。首先是检查模型对发生在下一个时间步的事情的预测能力有多好,也就是 *单步预测*(one-step-ahead prediction)。 ```{.python .input} #@tab all @@ -258,8 +259,7 @@ d2l.plot([time, time[tau:]], [d2l.numpy(x), d2l.numpy(onestep_preds)], 'time', 'x', legend=['data', '1-step preds'], xlim=[1, 1000], figsize=(6, 3)) ``` -单步预测看起来不错,正如我们所料。即使超过了604(`n_train + tau`)的观测,这些预测看起来仍然是可信的。然而,这有一个小问题:如果我们只观察序列数据到时间步604,我们不能期望接收到所有未来提前一步预测的输入。相反,我们需要一步一步向前迈进: - +正如我们所料的单步预测效果不错。即使这些预测的时间步超过了 $604$(`n_train + tau`),其结果看起来仍然是可信的。然而有一个小问题:如果数据观察序列的时间步只到 $604$,那么我们没有期望能够接收到所有提前一步预测的未来输入。相反,我们需要一步一步地向前迈进: $$ \hat{x}_{605} = f(x_{601}, x_{602}, x_{603}, x_{604}), \\ \hat{x}_{606} = f(x_{602}, x_{603}, x_{604}, \hat{x}_{605}), \\ @@ -269,7 +269,7 @@ $$ \ldots $$ -通常,对于直到$x_t$的观测序列,其在时间步长$\hat{x}_{t+k}$处的预测输出$t+k$被称为"$k$步预测"。由于我们已经观察到了$x_{604}$,它领先$k$步的预测是$\hat{x}_{604+k}$。换句话说,我们将不得不使用我们自己的预测来进行多步预测。让我们看看这件事进行得有多顺利。 +通常,对于直到 $x_t$ 的观测序列,其在时间步长 $\hat{x}_{t+k}$ 处的预测输出 $t+k$ 被称为 *$k$ 步预测*($k$-step-ahead-prediction)。由于我们已经观察到了 $x_{604}$,它领先 $k$ 步的预测是 $\hat{x}_{604+k}$。换句话说,我们将不得不使用自己的预测来进行多步预测。让我们看看这件事进行的是否顺利。 ```{.python .input} #@tab mxnet, pytorch @@ -298,9 +298,9 @@ d2l.plot([time, time[tau:], time[n_train + tau:]], xlim=[1, 1000], figsize=(6, 3)) ``` -正如上面的例子所示,这是一个惊人的失败。在几个预测步骤之后,预测很快就会衰减到一个常数。为什么这个算法效果这么差呢?这最终是由于错误累积的事实。假设在步骤1之后,我们有一些错误$\epsilon_1 = \bar\epsilon$。现在,步骤2的*INPUT*被扰动了$\epsilon_1$,因此对于某个常数$\epsilon_2 = \bar\epsilon + c \epsilon_1$,我们会遇到一些大约$c$的误差,依此类推。误差可能会相当快地偏离真实的观测结果。这是一个普遍的现象。例如,未来24小时的天气预报往往相当准确,但超过这一点,准确率会迅速下降。我们将在本章及以后讨论改进这一点的方法。 +正如上面的例子所示,这是一个巨大的失败。在几个预测步骤之后,预测结果很快就会衰减到一个常数。为什么这个算法效果这么差呢?最终事实是由于错误的累积。假设在步骤 $1$ 之后,我们积累一些错误 $\epsilon_1 = \bar\epsilon$。现在,步骤 $2$ 的 *输入*(input)被扰动了 $\epsilon_1$,因此积累的误差是依照次序的 $\epsilon_2 = \bar\epsilon + c \epsilon_1$,其中 $c$ 为某个常数,后面的预测误差依此类推。所以一个普遍的现象是误差可能会相当快地偏离真实的观测结果。例如,未来24小时的天气预报往往相当准确,但超过这一点,准确率就会迅速下降。我们将在本章及以后讨论改进这一点的方法。 -让我们通过计算$k = 1, 4, 16, 64$的整个序列的预测来更仔细地看一下$k$步预测的困难。 +让我们通过计算 $k = 1, 4, 16, 64$ 的整个序列的预测来更仔细地看一下 $k$ 步预测的困难。 ```{.python .input} #@tab all @@ -310,27 +310,29 @@ max_steps = 64 ```{.python .input} #@tab mxnet, pytorch features = d2l.zeros((T - tau - max_steps + 1, tau + max_steps)) -# Column `i` (`i` < `tau`) are observations from `x` for time steps from -# `i + 1` to `i + T - tau - max_steps + 1` +# 列 `i` (`i` < `tau`) 是来自 `x` 的观测 +# 其时间步从 `i + 1` 到 `i + T - tau - max_steps + 1` for i in range(tau): features[:, i] = x[i: i + T - tau - max_steps + 1] -# Column `i` (`i` >= `tau`) are the (`i - tau + 1`)-step-ahead predictions for -# time steps from `i + 1` to `i + T - tau - max_steps + 1` +# 列 `i` (`i` >= `tau`) 是 (`i - tau + 1`)步的预测 +# 其时间步从 `i + 1` 到 `i + T - tau - max_steps + 1` for i in range(tau, tau + max_steps): features[:, i] = d2l.reshape(net(features[:, i - tau: i]), -1) ``` + + ```{.python .input} #@tab tensorflow features = tf.Variable(d2l.zeros((T - tau - max_steps + 1, tau + max_steps))) -# Column `i` (`i` < `tau`) are observations from `x` for time steps from -# `i + 1` to `i + T - tau - max_steps + 1` +# 列 `i` (`i` < `tau`) 是来自 `x` 的观测 +# 其时间步从 `i + 1` 到 `i + T - tau - max_steps + 1` for i in range(tau): features[:, i].assign(x[i: i + T - tau - max_steps + 1].numpy()) -# Column `i` (`i` >= `tau`) are the (`i - tau + 1`)-step-ahead predictions for -# time steps from `i + 1` to `i + T - tau - max_steps + 1` +# 列 `i` (`i` >= `tau`) 是 (`i - tau + 1`)步预测 +# 其时间步从 `i + 1` 到 `i + T - tau - max_steps + 1` for i in range(tau, tau + max_steps): features[:, i].assign(d2l.reshape(net((features[:, i - tau: i])), -1)) ``` @@ -344,23 +346,23 @@ d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps], figsize=(6, 3)) ``` -这清楚地说明了当我们试图进一步预测未来时,预测的质量是如何变化的。虽然4步预测看起来仍然不错,但超过这一点的任何预测几乎都是无用的。 +这清楚地说明了当我们试图进一步预测未来时,预测的质量是如何变化的。虽然“$4$ 步预测”看起来仍然不错,但超过这个跨度的任何预测几乎都是无用的。 ## 小结 -* 内插和外推在难度上有很大差别。因此,如果你有一个序列,在训练时始终要尊重数据的时间顺序,即永远不要对未来的数据进行训练。 -* 序列模型需要专门的统计工具进行估计。两种流行的选择是自回归模型和隐变量自回归模型。 -* 对于因果模型(例如,向前推进的时间),估计正向通常比反向容易得多。 -* 对于直到时间步$t$的观测序列,其在时间步$t+k$的预测输出是"$k$步预测"。随着我们在时间上进一步预测,增加$k$,误差会累积,预测的质量会下降。 +* 内插和外推在难度上差别很大。因此,在训练时始终要尊重你所拥有的序列数据的时间顺序,即永远不要训练未来的数据。 +* 序列模型的估计需要专门的统计工具。两种流行的选择是:自回归模型和隐变量自回归模型。 +* 对于因果模型(例如,时间是向前推进的),正向估计通常比反向估计更容易。 +* 对于直到时间步 $t$ 的观测序列,其在时间步 $t+k$ 的预测输出是"$k$步预测"。随着我们在预测时间上进一步增加 $k$,会造成误差累积,导致预测质量下降。 ## 练习 -1. 在本部分的实验中对模型进行改进。 - 1. 是否包含过去4个以上观测结果?你真的需要多少? - 1. 如果没有噪音,你需要多少过去的观察?提示:你可以把$\sin$和$\cos$写成微分方程。 +1. 改进本节实验中的模型。 + 1. 是否包含了过去4个以上的观测结果?你的真实需要是多少? + 1. 如果没有噪音,你需要多少个过去的观测结果?提示:你可以把$\sin$和$\cos$写成微分方程。 1. 你能在保持特征总数不变的情况下合并旧的观察结果吗?这能提高精确度吗?为什么? 1. 改变神经网络结构并评估其性能。 -1. 一位投资者想要找到一种好的证券来购买。他查看过去的回报,以决定哪一种可能表现良好。这一策略可能会出什么问题呢? +1. 一位投资者想要找到一种好的证券来购买。他查看过去的回报,以决定哪一种可能是表现良好的。这一策略可能会出什么问题呢? 1. 因果关系也适用于文本吗?在多大程度上? 1. 举例说明什么时候可能需要隐变量自回归模型来捕捉数据的动力学模型。 @@ -375,3 +377,4 @@ d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps], :begin_tab:`tensorflow` [Discussions](https://discuss.d2l.ai/t/2092) :end_tab: + From ef4688f0bb31f6577ca8a3e9d3c97668bf08a770 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Fri, 7 May 2021 05:04:17 +0800 Subject: [PATCH 086/103] chapter_recurrent-modern/index (#787) * chapter_recurrent-modern/index * Update index.md Co-authored-by: goldmermaid --- chapter_recurrent-modern/index.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/chapter_recurrent-modern/index.md b/chapter_recurrent-modern/index.md index 31229b819..ed45cff2c 100644 --- a/chapter_recurrent-modern/index.md +++ b/chapter_recurrent-modern/index.md @@ -1,11 +1,20 @@ # 现代循环神经网络 :label:`chap_modern_rnn` -我们已经介绍了循环神经网络的基础知识,这种网络可以更好地处理序列数据。为了演示效果,我们在文本数据上实现了基于循环神经网络的语言模型。但是,这些技术对于从业人员面对当今各种序列学习问题时可能是不够用的。 +前文中我们已经介绍了循环神经网络的基础知识,这种网络可以更好地处理序列数据。 +同时,我们在文本数据上实现了基于循环神经网络的语言模型。 +但是,对于面对当今各种序列学习问题的从业人员,这些技术可能并不够用。 -例如,实践中一个显著问题是循环神经网络的数值不稳定性。尽管我们已经应用了梯度裁剪等实现技巧,但是通过设计更复杂的序列模型可以进一步缓解这个问题。具体来说,在实践中更常见的门控循环神经网络。首先,我们将引入两个广泛使用的网络,即 *门控循环单元* (gated recurrent units, GRU) 和 *长短期记忆网络* (long short-term memory, LSTM)。然后,我们将基于迄今为止讨论过的一个单向隐藏层来扩展循环神经网络架构。我们将描述具有多个隐藏层的深层架构,并讨论基于前向和后向循环计算的双向设计。现代循环网络经常采用这种扩展。在解释这些循环神经网络的变体时,我们将继续考虑 :numref:`chap_rnn` 中引入的语言建模问题。 +例如,循环神经网络在实践中的一个常见问题是数值不稳定性。 +尽管我们已经应用了梯度裁剪等实现技巧,但是通过设计更复杂的序列模型可以进一步缓解这个问题。 +本章中,我们首先将介绍两个广泛使用的网络,即 *门控循环单元* (gated recurrent units, GRU) 和 *长短期记忆网络* (long short-term memory, LSTM)。 +然后,我们将基于单向隐藏层来扩展循环神经网络架构,现代循环网络经常采用这种扩展。 +我们将描述具有多个隐藏层的深层架构,并讨论基于前向和后向循环计算的双向设计。 +在解释这些循环神经网络的变体时,我们将继续考虑 :numref:`chap_rnn` 中引入的语言模型问题。 -事实上,语言建模只揭示了序列学习能力的一小部分。在各种序列学习问题中,如自动语音识别、文本到语音的转换和机器翻译,输入和输出都是任意长度的序列。为了解释如何拟合这种类型的数据,我们将以机器翻译为例介绍基于循环神经网络的“编码器-解码器”架构和束搜索,并以此生成序列。 +事实上,语言建模只描绘了序列学习能力的冰山一角。 +在各种序列学习问题中,如自动语音识别、文本到语音的转换和机器翻译,输入和输出都是任意长度的序列。 +本章中,我们将以机器翻译为例介绍基于循环神经网络的“编码器-解码器”结构和束搜索,并用它们来生成序列。 ```toc :maxdepth: 2 @@ -18,4 +27,4 @@ machine-translation-and-dataset encoder-decoder seq2seq beam-search -``` \ No newline at end of file +``` From 0b52a1e2e956ce716e56eb358a7b1bbb7a58b449 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:16:41 +0800 Subject: [PATCH 087/103] chapter_computational-performance/index (#792) --- chapter_computational-performance/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_computational-performance/index.md b/chapter_computational-performance/index.md index 715712ca2..525f09a63 100644 --- a/chapter_computational-performance/index.md +++ b/chapter_computational-performance/index.md @@ -1,7 +1,7 @@ # 计算性能 :label:`chap_performance` -在深度学习中,数据集和模型通常很大,这涉及大量计算。因此,计算性能非常重要。本章将重点介绍影响计算性能的主要因素:命令式编程、符号化编程、异步计算、自动并行和多 GPU 计算。通过学习本章,你可以进一步提高前几章中实现的模型的计算性能,例如,通过在不影响准确性的情况下缩短训练时间。 +在深度学习中,数据集和模型通常都很大,导致计算量也会很大。因此,计算的性能非常重要。本章将集中讨论影响计算性能的主要因素:命令式编程、符号编程、异步计算、自动并行和多GPU计算。通过学习本章,你可以进一步提高前几章中实现的那些模型的计算性能。例如,我们可以在不影响准确性的前提下,减少训练时间。 ```toc :maxdepth: 2 From 6fa3b6970686534de95ae7bb0f476428dd7db9c4 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:17:05 +0800 Subject: [PATCH 088/103] chapter_computational-performance/hybridize (#793) --- .../hybridize.md | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md index ac5924571..3d60abae6 100644 --- a/chapter_computational-performance/hybridize.md +++ b/chapter_computational-performance/hybridize.md @@ -1,7 +1,7 @@ # 编译器和解释器 :label:`sec_hybridize` -到目前为止,这本书一直侧重于命令式编程,它利用 `print`、`+` 和 `if` 等语句来改变计划的状态。考虑以下简单的命令性程序的例子。 +到目前为止,这本书主要关注命令式编程,它使用诸如`print`、`+`和`if`之类的语句来更改程序的状态。考虑下面这个简单的命令式程序的例子。 ```{.python .input} #@tab all @@ -17,22 +17,22 @@ def fancy_func(a, b, c, d): print(fancy_func(1, 2, 3, 4)) ``` -Python 是一种 * 解释性语言 *。当评估上述 `fancy_func` 函数时,它会按顺序执行组成函数主体的操作 *。也就是说,它将评估 `e = add(a, b)` 并将结果存储为变量 `e`,从而改变程序的状态。接下来的两个语句 `f = add(c, d)` 和 `g = add(e, f)` 将以类似的方式执行,执行添加并将结果存储为变量。:numref:`fig_compute_graph` 说明了数据流。 +Python是一种*解释语言*(interpreted language)。当评估上述`fancy_func`函数时,它按顺序执行函数体的操作。也就是说,它将计算`e = add(a, b)`,并将结果存储为变量`e`,从而更改程序的状态。接下来的两个语句`f = add(c, d)`和`g = add(e, f)`将类似地执行,执行加法并将结果存储为变量。:numref:`fig_compute_graph`说明了数据流。 -![Data flow in an imperative program.](../img/computegraph.svg) +![命令式编程中的数据流。](../img/computegraph.svg) :label:`fig_compute_graph` -尽管命令式编程很方便,但效率可能低下。一方面,即使在 `fancy_func` 中重复调用 `add` 函数,Python 也会分别执行这三个函数调用。比如说,如果在 GPU 上(甚至在多个 GPU 上)执行这些操作,则 Python 解释器产生的开销可能会变得压倒性。此外,它需要保存 `e` 和 `f` 的变量值,直到 `fancy_func` 中的所有语句都被执行。这是因为我们不知道在语句 `e = add(a, b)` 和 `f = add(c, d)` 执行之后,程序的其他部分是否会使用变量 `e` 和 `f`。 +尽管命令式编程很方便,但可能效率低下。一方面,即使`add`函数在`fancy_func`中被重复调用,Python也会单独执行这三个函数调用。如果在一个GPU(甚至多个GPU)上执行这些命令,那么Python解释器产生的开销可能会非常大。此外,它需要保存`e`和`f`的变量值,直到`fancy_func`中的所有语句都执行完毕。这是因为我们不知道在执行语句`e = add(a, b)`和`f = add(c, d)`之后,程序的其他部分是否会使用变量`e`和`f`。 -## 符号编程 +## 符号式编程 -考虑另一种选择,* 符号编程 *,其中通常只有在完全定义过程后才执行计算。该策略被多个深度学习框架使用,包括 Theano 和 TensorFlow(后者获得了必要的扩展)。它通常涉及以下步骤: +考虑另一种选择*符号式编程*(symbolic programming),通常只在完全定义了过程之后才执行计算。这个策略被多个深度学习框架使用,包括Theano和TensorFlow(后者已经获得了命令式编程扩展)。通常包括以下步骤: 1. 定义要执行的操作。 1. 将操作编译成可执行程序。 -1. 提供所需的输入并调用编译后的程序进行执行。 +1. 提供所需的输入并调用编译后的程序供执行。 -这允许进行大量的优化。首先,在许多情况下,我们可以跳过 Python 解释器,从而消除性能瓶颈,该瓶颈可能会在 CPU 上与单个 Python 线程配对的多个快速 GPU 上变得显著。其次,编译器可能会优化上述代码并将其重写为 `print((1 + 2) + (3 + 4))` 甚至 `print(10)`。这是可能的,因为编译器在将其转换为机器指令之前需要查看完整的代码。例如,只要不再需要变量,它就可以释放内存(或永远不分配)。或者它可以将代码完全转换为等效的片段。为了获得更好的想法,请考虑下面的命令式编程模拟(毕竟是 Python)。 +这允许进行大量优化。首先,在许多情况下,我们可以跳过Python解释器。从而消除在多个更快的GPU上与在CPU上的单个Python线程搭配使用时可能出现的性能瓶颈。其次,编译器可能会优化并将上述代码重写为`print((1 + 2) + (3 + 4))`甚至`print(10)`。这是可能的,因为编译器在将其转换为机器指令之前可以看到完整的代码。例如,只要不再需要某个变量,它就可以释放内存(或者从不分配内存)。或者它可以将代码转换为一个完全等价的片段。为了获得更好的想法,请考虑下面的命令式编程的模拟(仍然是Python)。 ```{.python .input} #@tab all @@ -60,32 +60,32 @@ y = compile(prog, '', 'exec') exec(y) ``` -命令式(解释式)编程和符号编程之间的区别如下: +命令式(解释式)编程和符号式编程的区别如下: -* 命令式编程更容易。当 Python 中使用命令式编程时,大多数代码都很简单且易于编写。调试命令式编程代码也更容易。这是因为获取和打印所有相关的中间变量值或使用 Python 的内置调试工具更容易。 -* 符号编程效率更高,更容易移植。符号编程使得在编译过程中优化代码变得更加容易,同时还能够将程序移植到独立于 Python 的格式。这允许程序在非 Python 环境中运行,从而避免任何与 Python 解释器相关的潜在性能问题。 +* 命令式编程更容易。在Python中使用命令式编程时,大多数代码都是简单易懂的。调试命令式编程代码也更容易。这是因为更容易获取和打印所有相关的中间变量值,或者使用Python的内置调试工具。 +* 符号式编程更高效,更易于移植。符号式编程使得在编译期间优化代码更加容易,同时还能够将程序移植到独立于Python的格式中。这允许程序在非Python环境中运行,从而避免了与Python解释器相关的任何潜在性能问题。 -## 混合编程 +## 混合式编程 -历史上,大多数深度学习框架都可以选择必要的方法或象征性方法。例如,Theano、TensorFlow(受前者的启发)、Keras 和 CNTK 象征性地制定模型。相反,Chainer 和 PyTorch 采取必要的方法。在后续的修订版中,TensorFlow 2.0 和 Keras 中添加了命令模式。 +历史上,大多数深度学习框架在命令式方法和符号式方法之间进行选择。例如,Theano、TensorFlow(灵感来自前者)、Keras和CNTK采用了符号式方法。相反地,Chainer和PyTorch采取了命令式方法。在后来的更新版中,tensorflow2.0和Keras增加了命令式方法。 :begin_tab:`mxnet` -在设计 Gluon 时,开发人员考虑了是否有可能将两种编程模式的好处结合起来。这导致了混合模型,允许用户使用纯粹的命令式编程进行开发和调试,同时能够将大多数程序转换为符号程序,以便在需要产品级计算性能和部署时运行。 +在设计Gluon时,开发人员考虑是否有可能将两种编程模式的优点结合起来。这得到了一个混合式方法,允许用户使用纯命令式编程进行开发和调试,同时能够将大多数程序转换为符号式程序,以便在需要产品级计算性能和部署时运行。 -实际上,这意味着我们使用 `HybridBlock` 或 `HybridSequential` 类来构建模型。默认情况下,其中任何一个都以同样的方式执行 `Block` 或 `Sequential` 类在命令式编程中执行。`HybridSequential` 类是 `HybridBlock` 的子类(就像 `Sequential` 子类 `Block`)。当调用 `hybridize` 函数时,Gluon 会将模型编译成符号编程中使用的形式。这允许人们在不牺牲模型实施方式的情况下优化计算密集型组件。我们将在下面介绍优势,重点介绍顺序模型和模块。 +实际上,这意味着我们使用`HybridBlock`或`HybridSequential`类构建模型。默认情况下,它们中的任何一个都以命令式编程中执行`Block`或`Sequential`类的相同方式执行。`HybridSequential`类是`HybridBlock`的子类(就像`Sequential`子类`Block`一样)。当`hybridize`函数被调用时,Gluon将模型编译成符号式编程中使用的形式。这允许在不牺牲模型实现方式的情况下优化计算密集型组件。我们将在下面说明这样的优点,重点是在`Sequential`和`Block`。 :end_tab: :begin_tab:`pytorch` -如上所述,PyTorch 基于命令式编程并使用动态计算图。为了利用符号编程的可移植性和效率,开发人员考虑了是否有可能结合两种编程模型的优势。这导致了一个 torchscript,允许用户使用纯粹的命令式编程进行开发和调试,同时能够将大多数程序转换为符号程序,以便在需要产品级计算性能和部署时运行。 +如上所述,PyTorch基于命令式编程并使用动态计算图。为了利用符号式编程的可移植性和效率,开发人员考虑了是否有可能将两种编程模型的优点结合起来。这就产生了torchscript,它允许用户使用纯命令式编程进行开发和调试,同时能够将大多数程序转换为符号式程序,以便在需要产品级计算性能和部署时运行。 :end_tab: :begin_tab:`tensorflow` -命令式编程模式现在是 Tensorflow 2 中的默认设置,对于新使用该语言的人来说,这是一个值得欢迎的变化。但是,TensorFlow 中仍然存在相同的符号编程技术和后续的计算图形,易于使用的 `tf.function` 装饰器可以访问。这为 TensorFlow 带来了命令式的编程模式,允许用户定义更直观的函数,然后使用 TensorFlow 团队称为 [autograph](https://www.tensorflow.org/api_docs/python/tf/autograph) 的功能将它们包装起来并自动将它们编译成计算图形。 +命令式编程现在是TensorFlow2的默认选择,对于那些刚接触该语言的人来说是一个很好的改变。然而,符号式编程技术和计算图仍然存在于TensorFlow中,并且可以通过易于使用的`tf.function`修饰符进行访问。这给TensorFlow带来了命令式编程范例,允许用户定义更直观的函数,然后使用TensorFlow团队称为[autograph](https://www.tensorflow.org/api_docs/python/tf/autograph)的特性将它们包装并自动编译成计算图。 :end_tab: -## 混合 `Sequential` 课程 +## `Sequential`的混合式编程 -了解混合运行方式的最简单方法是考虑具有多层的深度网络。通常情况下,Python 解释器需要为所有层执行代码才能生成指令,然后将其转发到 CPU 或 GPU。对于单个(快速)计算设备,这不会导致任何重大问题。另一方面,如果我们使用高级 8-GPU 服务器,如 AWS P3dn.24xLart 实例,Python 将难以让所有 GPU 保持繁忙。单线程 Python 解释器成为这里的瓶颈。让我们看看如何通过将 `Sequential` 替换为 `HybridSequential` 来解决代码的重要部分。我们首先定义一个简单的 MLP。 +要了解混合式编程的工作原理,最简单的方法是考虑具有多层的深层网络。按照惯例,Python解释器将需要为所有层执行代码以生成指令,然后可以将该指令转发到CPU或GPU。对于单个(快速)计算设备,这不会导致任何重大问题。另一方面,如果我们使用高级的8-GPU服务器,比如AWS P3dn.24xlarge实例,Python将很难让所有GPU保持忙碌。单线程Python解释器成为这里的瓶颈。让我们看看如何通过用`HybridSequential`替换代码重要部分的`Sequential`来解决问题。我们首先定义一个简单的多层感知机。 ```{.python .input} from d2l import mxnet as d2l @@ -93,7 +93,7 @@ from mxnet import np, npx from mxnet.gluon import nn npx.set_np() -# Factory for networks +# 生产网络的工厂 def get_net(): net = nn.HybridSequential() net.add(nn.Dense(256, activation='relu'), @@ -113,7 +113,7 @@ from d2l import torch as d2l import torch from torch import nn -# Factory for networks +# 生产网络的工厂 def get_net(): net = nn.Sequential(nn.Linear(512, 256), nn.ReLU(), @@ -133,7 +133,7 @@ from d2l import tensorflow as d2l import tensorflow as tf from tensorflow.keras.layers import Dense -# Factory for networks +# 生产网络的工厂 def get_net(): net = tf.keras.Sequential() net.add(Dense(256, input_shape = (512,), activation = "relu")) @@ -147,15 +147,15 @@ net(x) ``` :begin_tab:`mxnet` -通过调用 `hybridize` 函数,我们能够在 MLP 中编译和优化计算。模型的计算结果保持不变。 +通过调用`hybridize`函数,我们可以编译和优化多层感知机中的计算。模型的计算结果保持不变。 :end_tab: :begin_tab:`pytorch` -通过使用 `torch.jit.script` 函数转换模型,我们能够在 MLP 中编译和优化计算。模型的计算结果保持不变。 +通过使用`torch.jit.script`函数转换模型,我们可以编译和优化多层感知机的计算。模型的计算结果保持不变。 :end_tab: :begin_tab:`tensorflow` -以前,tensorflow 中内置的所有函数都是作为计算图构建的,因此在默认情况下编译 JIT。但是,随着 tensorflow 2.X 的发布和急切的张量,这不再是默认行为。我们可以使用 tf.function 重新启用此功能。tf.function 更常用作函数装饰器,但是可以将其作为普通 python 函数直接调用,如下所示。模型的计算结果保持不变。 +以前,tensorflow中构建的所有函数都是作为计算图构建的,因此默认情况下是JIT编译的。但是,随着TensorFlow2.X和Earge tensors的发布,这不再是默认行为。我们使用tf.function重新启用此功能。tf.function更常用作函数装饰器,但是可以直接将其作为普通python函数调用,如下所示。模型的计算结果保持不变。 :end_tab: ```{.python .input} @@ -176,20 +176,20 @@ net(x) ``` :begin_tab:`mxnet` -这似乎几乎太好了,无法实现:只需将一个块指定为 `HybridSequential`,编写与之前相同的代码,然后调用 `hybridize`。一旦发生这种情况,网络将被优化(我们将在下面对性能进行基准测试不幸的是,这并不适用于每个层都神奇。也就是说,如果图层继承自 `Block` 类而不是 `HybridBlock` 类,则不会对其进行优化。 +只需将一个块指定为`HybridSequential`,编写与之前相同的代码并调用`hybridize`。一旦发生这种情况,网络将得到优化(我们将在下面对性能进行基准测试)。不幸的是,这并不是适用于每一层。也就是说,如果一个层从`Block`类而不是`HybridBlock`类继承,它将不会得到优化。 :end_tab: :begin_tab:`pytorch` -通过使用 `torch.jit.script` 转换模型,这似乎几乎太好了,无法实现:编写与之前相同的代码,然后简单地使用 `torch.jit.script` 转换模型。一旦发生这种情况,网络将被优化(我们将在下面对性能进行基准测试 +编写与以前相同的代码,并使用`torch.jit.script`简单地转换模型。一旦发生这种情况,网络将得到优化(我们将在下面对性能进行基准测试)。 :end_tab: :begin_tab:`tensorflow` -使用 `tf.function` 转换模型为 TensorFlow 提供了令人难以置信的力量:编写与之前相同的代码,然后使用 `tf.function` 简单地转换模型。一旦发生这种情况,网络就会在 TensorFlow 的 MLIR 中间表示中构建为计算图,并在编译器级别进行了大量优化,以实现快速执行(我们将在下面对性能进行基准测试)。将 `jit_compile = True` 标志明确添加到 `tf.function()` 调用中可以启用 TensorFlow 中的 XLA(加速线性代数)功能。XLA 可以在某些情况下进一步优化 JIT 编译的代码。在没有这个明确定义的情况下,可以启用图形模式执行,但是 XLA 可以使某些大型线性代数操作(在深度学习应用程序中看到的那些操作)更快,尤其是在 GPU 环境中。 +编写与以前相同的代码,并使用`tf.function`简单地转换模型。一旦发生这种情况,网络将以TensorFlow的MLIR中间表示形式构建为一个计算图,并在编译器级别进行大量优化,以实现快速执行(我们将在下面对性能进行基准测试)。显式地将`jit_compile = True`标志添加到`tf.function()`调用可以启用TensorFlow中的XLA(加速线性代数)功能。在某些情况下,XLA可以进一步优化JIT编译代码。。在没有这种显式定义的情况下,可以启用图形模式执行,但是,XLA可以使某些大型线性代数操作(与我们在深度学习应用程序中看到的操作类似)速度更快,特别是在GPU环境中。 :end_tab: -### 混合加速 +### 通过混合式编程加速 -为了展示通过编译获得的性能提高,我们比较了在混合运动之前和之后评估 `net(x)` 所需的时间。让我们首先定义一个函数来测量这次。当我们着手衡量(和提高)绩效时,它将在整个章节中派上用场。 +为了证明通过编译获得的性能改进,我们比较了混合编程前后执行`net(x)`所需的时间。让我们先定义一个函数来度量这个时间。当我们开始衡量(和改进)性能时,它在本章中将非常有用。 ```{.python .input} #@tab all @@ -207,25 +207,25 @@ class Benchmark: ``` :begin_tab:`mxnet` -现在我们可以调用两次网络,一次不用混合动力。 +现在我们可以调用网络两次,一次启用混合式,一次没有启用混合式。 :end_tab: :begin_tab:`pytorch` -现在我们可以调用两次网络,一次是没有 torchscript。 +现在我们可以调用网络两次,一次使用torchscript,一次不使用torchscript。 :end_tab: :begin_tab:`tensorflow` -现在我们可以三次调用网络,一次是急切执行,一次是用图形模式执行,然后再次使用 JIT 编译的 XLA。 +现在我们可以调用网络三次,一次使用eager执行,一次是以图模式执行,另一次是使用JIT编译的XLA。 :end_tab: ```{.python .input} net = get_net() -with Benchmark('Without hybridization'): +with Benchmark('无混合式'): for i in range(1000): net(x) npx.waitall() net.hybridize() -with Benchmark('With hybridization'): +with Benchmark('有混合式'): for i in range(1000): net(x) npx.waitall() ``` @@ -233,49 +233,49 @@ with Benchmark('With hybridization'): ```{.python .input} #@tab pytorch net = get_net() -with Benchmark('Without torchscript'): +with Benchmark('无torchscript'): for i in range(1000): net(x) net = torch.jit.script(net) -with Benchmark('With torchscript'): +with Benchmark('有torchscript'): for i in range(1000): net(x) ``` ```{.python .input} #@tab tensorflow net = get_net() -with Benchmark('Eager Mode'): +with Benchmark('Eager模式'): for i in range(1000): net(x) net = tf.function(net) -with Benchmark('Graph Mode'): +with Benchmark('Graph模式'): for i in range(1000): net(x) ``` :begin_tab:`mxnet` -如上面的结果所观察到的那样,在 `HybridSequential` 实例调用 `hybridize` 函数之后,通过使用符号编程来提高计算性能。 +从上面的结果中可以看到,在`HybridSequential`实例调用`hybridize`函数之后,通过使用符号式编程提高了计算性能。 :end_tab: :begin_tab:`pytorch` -如上面的结果所观察到的那样,在使用 `torch.jit.script` 函数编写 `nn.Sequential` 实例脚本后,通过使用符号编程来提高计算性能。 +从上面的结果中可以看到,使用`nn.Sequential`函数编写了`torch.jit.script`实例的脚本之后,通过使用符号式编程来提高计算性能。 :end_tab: :begin_tab:`tensorflow` -如上面的结果所观察到的那样,在使用 `tf.function` 函数编写 tf.keras 顺序实例脚本之后,通过 tensorflow 中的图形模式执行使用符号编程,计算性能得到提高。 +从以上结果可以看出,在使用`tf.function`函数编写tf.keras `Sequential` 实例脚本之后,通过在tensorflow中通过图形模式执行使用符号式编程来提高计算性能。 :end_tab: ### 序列化 :begin_tab:`mxnet` -编译模型的好处之一是我们可以序列化(保存)模型及其参数到磁盘。这使我们能够以独立于所选前端语言的方式存储模型。这使我们能够将训练有素的模型部署到其他设备,并轻松使用其他前端编程语言。同时,代码通常比命令式编程中可以实现的速度快。让我们看看 `export` 函数的实际运行。 +编译模型的好处之一是我们可以将模型及其参数序列化(保存)到磁盘。这允许我们以独立于所选前端语言的方式存储模型。这使我们能够将经过训练的模型部署到其他设备上,并方便地使用其他前端编程语言。同时,代码通常比命令式编程更快。让我们看看`export`的实际功能。 :end_tab: :begin_tab:`pytorch` -编译模型的好处之一是我们可以序列化(保存)模型及其参数到磁盘。这使我们能够以独立于所选前端语言的方式存储模型。这使我们能够将训练有素的模型部署到其他设备,并轻松使用其他前端编程语言。同时,代码通常比命令式编程中可以实现的速度快。让我们看看 `save` 函数的实际运行。 +编译模型的好处之一是我们可以将模型及其参数序列化(保存)到磁盘。这允许我们以独立于所选前端语言的方式存储模型。这使我们能够将经过训练的模型部署到其他设备上,并方便地使用其他前端编程语言。同时,代码通常比命令式编程更快。让我们看看`save`的实际功能。 :end_tab: :begin_tab:`tensorflow` -编译模型的好处之一是我们可以序列化(保存)模型及其参数到磁盘。这使我们能够以独立于所选前端语言的方式存储模型。这使我们能够将训练有素的模型部署到其他设备,轻松使用其他前端编程语言或在服务器上执行训练有素的模型。同时,代码通常比命令式编程中可以实现的速度快。允许我们在 tensorflow 中保存的低级 API 是 `tf.saved_model`。让我们看看 `saved_model` 实例在运行中。 +编译模型的好处之一是我们可以将模型及其参数序列化(保存)到磁盘。这允许我们以独立于所选前端语言的方式存储模型。这使我们能够将经过训练的模型部署到其他设备上,并轻松地使用其他前端编程语言或在服务器上执行经过训练的模型。同时,代码通常比命令式编程更快。允许我们在tensorflow中保存的底层API是`tf.saved_model`。让我们来看看`saved_model`的运行情况。 :end_tab: ```{.python .input} @@ -297,7 +297,7 @@ tf.saved_model.save(net, 'my_mlp') ``` :begin_tab:`mxnet` -模型被分解为(大型二进制)参数文件和执行模型计算所需程序的 JSON 描述。这些文件可以由 Python 或 MxNet 支持的其他前端语言读取,例如 C++、R、Scala 和 Perl。让我们看看模型描述中的前几行。 +模型被分解成一个大的二进制参数文件和一个执行模型计算所需的程序的JSON描述。这些文件可以由Python或MXNet支持的其他前端语言读取,例如C++、R、Scala和Perl。让我们看看模型描述中的前几行。 :end_tab: ```{.python .input} @@ -305,9 +305,9 @@ tf.saved_model.save(net, 'my_mlp') ``` :begin_tab:`mxnet` -此前,我们证明,在调用 `hybridize` 函数之后,该模型能够实现卓越的计算性能和便携性。请注意,尽管这种混合可能会影响模型的灵活性,特别是在控制流方面。 +之前,我们演示了在调用`hybridize`函数之后,该模型能够实现优异的计算性能和可移植性。注意,混合式可能影响模型的灵活性,特别是在控制流方面。 -此外,与需要使用 `forward` 函数的 `Block` 实例相反,对于 `HybridBlock` 实例,我们需要使用 `hybrid_forward` 函数。 +此外,与`Block`实例需要使用`forward`函数不同,对于`HybridBlock`实例,我们需要使用`hybrid_forward`函数。 :end_tab: ```{.python .input} @@ -326,7 +326,7 @@ class HybridNet(nn.HybridBlock): ``` :begin_tab:`mxnet` -上面的代码实现了一个带有 4 个隐藏单元和 2 个输出的简单网络。`hybrid_forward` 函数需要一个额外的参数 `F`。这是必要的,因为根据代码是否混合,它将使用略有不同的库(`ndarray` 或 `symbol`)进行处理。这两个类执行的功能非常相似,MxNet 会自动确定参数。为了理解发生了什么,我们将参数作为函数调用的一部分打印出来。 +上面的代码实现了一个具有4个隐藏单元和2个输出的简单网络。`hybrid_forward`函数接受一个额外的参数`F`。这是必需的,因为根据代码是否已混合,它将使用稍微不同的库(`ndarray`或`symbol`)进行处理。这两个类执行非常相似的函数,MXNet自动确定参数。为了理解发生了什么,我们将参数作为函数调用的一部分打印出来。 :end_tab: ```{.python .input} @@ -337,7 +337,7 @@ net(x) ``` :begin_tab:`mxnet` -重复向前计算将导致相同的输出(我们省略了细节)。现在让我们看看如果我们调用 `hybridize` 函数会发生什么。 +重复前向计算将导致相同的输出(我们省略细节)。现在让我们看看如果调用`hybridize`函数会发生什么。 :end_tab: ```{.python .input} @@ -346,7 +346,7 @@ net(x) ``` :begin_tab:`mxnet` -而不是使用 `ndarray`,我们现在将 `symbol` 模块用于 `F`。此外,尽管输入是 `ndarray` 类型,但作为编译过程的一部分,通过网络流动的数据现在已转换为 `symbol` 类型。重复函数调用会导致令人惊讶的结果: +我们不再使用`ndarray`,而是使用`symbol`模块来表示`F`。此外,即使输入是`ndarray`类型,作为编译过程的一部分,经过网络的数据现在也转换为`symbol`类型。重复函数调用会产生令人惊讶的结果: :end_tab: ```{.python .input} @@ -354,29 +354,29 @@ net(x) ``` :begin_tab:`mxnet` -这与我们之前看到的截然不同。省略 `hybrid_forward` 中定义的所有打印语句。事实上,混合后,`net(x)` 的执行不再涉及 Python 解释器。这意味着,忽略任何虚假的 Python 代码(例如 print 语句),以利于更简化的执行和更好的性能。相反,MxNet 直接调用 C ++ 后端。另请注意,`symbol` 模块(例如 `asnumpy`)中不支持某些功能,而就地操作(如 `a += b` 和 `a[:] = a + b`)必须重写为 `a = a + b`。尽管如此,只要速度重要,汇编模型就值得付出努力。优势可以从小百分点到速度的两倍以上,具体取决于模型的复杂性、CPU 的速度以及 GPU 的速度和数量。 +这与我们以前看到的情况大不相同。`hybrid_forward`中定义的所有打印语句都被省略。实际上,在执行`net(x)`之后,不再涉及Python解释器。这意味着任何Python代码(例如print语句)都会被省略,以利于更精简的执行和更好的性能。相反,MXNet直接调用C++后端。另外请注意,`symbol`模块中不支持某些功能(例如`asnumpy`),`a += b`和`a[:] = a + b`等操作必须重写为`a = a + b`。尽管如此,尽管如此,只要速度很重要,模型的编译都是值得的。根据模型的复杂性、CPU的速度以及GPU的速度和数量,优势可以从很小的百分比到两倍以上的速度不等。 :end_tab: ## 小结 -* 命令式编程使设计新模型变得容易,因为可以使用控制流编写代码,并且能够使用大量 Python 软件生态系统。 -* 符号编程要求我们先指定程序并在执行之前对其进行编译。好处是提高了性能。 +* 命令式编程使设计新模型变得容易,因为它可以用控制流编写代码,并且能够使用大量Python软件生态。 +* 符号式编程要求我们在执行程序之前指定并编译程序。其好处是提高了性能。 :begin_tab:`mxnet` -* MxNet 能够根据需要结合这两种方法的优势。 -* 由 `HybridSequential` 和 `HybridBlock` 类构建的模型可以通过调用 `hybridize` 函数将命令性程序转换为符号程序。 +* MXNet能够根据需要结合这两种方法的优点。 +* 由`HybridSequential`和`HybridBlock`类构造的模型能够通过调用`hybridize`函数将命令式程序转换为符号式程序。 :end_tab: ## 练习 :begin_tab:`mxnet` -1. 将 `x.asnumpy()` 添加到本节中 `HybridNet` 类的 `hybrid_forward` 函数的第一行。执行代码并观察遇到的错误。他们为什么会发生? -1. 如果我们添加控制流程,即 `hybrid_forward` 函数中的 Python 语句 `if` 和 `for` 会发生什么? -1. 查看前几章中你感兴趣的模型。你能通过重新实现它们来提高他们的计算性能吗? +1. 在本节中,在`HybridNet`类的`hybrid_forward`函数的第一行中添加`x.asnumpy()`。执行代码并观察遇到的错误。为什么会这样? +1. 如果我们在`hybrid_forward`函数中添加控制流,即Python语句`if`和`for`,会发生什么? +1. 回顾前几章中你感兴趣的模型。你能通过重新实现它们来提高它们的计算性能吗? :end_tab: :begin_tab:`pytorch,tensorflow` -1. 查看前几章中你感兴趣的模型。你能通过重新实现它们来提高他们的计算性能吗? +1. 回顾前几章中你感兴趣的模型。你能通过重新实现它们来提高它们的计算性能吗? :end_tab: :begin_tab:`mxnet` From 5434ee180c73d092452e15f73daf8a067709be86 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:17:18 +0800 Subject: [PATCH 089/103] chapter_computational-performance/async-computation (#794) --- .../async-computation.md | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/chapter_computational-performance/async-computation.md b/chapter_computational-performance/async-computation.md index 456693dd9..432dcd985 100644 --- a/chapter_computational-performance/async-computation.md +++ b/chapter_computational-performance/async-computation.md @@ -1,9 +1,9 @@ # 异步计算 :label:`sec_async` -今天的计算机是高度并行的系统,由多个 CPU 核心(通常是每个核心多个线程)、每个 GPU 的多个处理元素以及通常每台设备多个 GPU 组成。简而言之,我们可以同时处理许多不同的事物,通常是在不同的设备上。不幸的是,Python 不是编写并行和异步代码的好方法,至少没有一些额外的帮助。毕竟,Python 是单线程的,这在未来不太可能改变。MxNet 和 TensorFlow 等深度学习框架采用 * 异步编程 * 模型来提高性能,而 PyTorch 则使用 Python 自己的调度程序,从而实现不同的性能权衡。对于 PyTorch,默认情况下,GPU 操作是异步的。当您调用使用 GPU 的函数时,这些操作将入队到特定设备,但不一定要等到以后才执行。这使我们能够并行执行更多计算,包括 CPU 或其他 GPU 上的操作。 +今天的计算机是高度并行的系统,由多个CPU核(通常每个核有多个线程)、每个GPU有多个处理单元,每个设备通常有多个GPU组成。简而言之,我们可以同时处理许多不同的事情,且通常是在不同的设备上。不幸的是,Python不是编写并行和异步代码的好方法,至少在没有额外帮助的情况下不是好方法。毕竟,Python是单线程的,这在将来是不太可能改变。诸如MXNet和TensorFlow之类的深度学习框架采用了一种*异步编程*(asynchronous programming)模型来提高性能,而PyTorch则使用Python自己的调度器来实现不同的性能权衡。对于PyTorch,默认情况下,GPU操作是异步的。当你调用一个使用GPU的函数时,操作会排队到特定的设备上,但不一定要等到以后才执行。这允许我们并行执行更多的计算,包括在CPU或其他GPU上的操作。 -因此,了解异步编程的工作原理有助于我们通过主动降低计算需求和相互依赖性来开发更高效的程序。这使我们能够减少内存开销并提高处理器利用率。 +因此,了解异步编程是如何工作的,通过主动减少计算需求和相互依赖,有助于我们开发更高效的程序。这使我们能够减少内存开销并提高处理器利用率。 ```{.python .input} from d2l import mxnet as d2l @@ -21,14 +21,14 @@ import torch from torch import nn ``` -## 通过后端进行异步 +## 通过后端异步处理 :begin_tab:`mxnet` -对于热身,请考虑以下玩具问题:我们想生成一个随机矩阵并将其乘以。让我们在 NumPy 和 `mxnet.np` 中这样做来看看差异。 +作为热身,考虑一个简单问题:我们要生成一个随机矩阵并将其相乘。让我们在NumPy和`mxnet.np`中都这样做,看看有什么不同。 :end_tab: :begin_tab:`pytorch` -对于热身,请考虑以下玩具问题:我们想生成一个随机矩阵并将其乘以。让我们在 NumPy 和 PyTorch 张量中这样做来看看差异。请注意,PyTorch `tensor` 是在 GPU 上定义的。 +作为热身,考虑一个简单问题:我们要生成一个随机矩阵并将其相乘。让我们在NumPy和PyTorch张量中都这样做,看看它们的区别。请注意,PyTorch的 `tensor`是在GPU上定义的。 :end_tab: ```{.python .input} @@ -45,7 +45,7 @@ with d2l.Benchmark('mxnet.np'): ```{.python .input} #@tab pytorch -# Warmup for GPU computation +# GPU计算热身 device = d2l.try_gpu() a = torch.randn(size=(1000, 1000), device=device) b = torch.mm(a, a) @@ -62,11 +62,11 @@ with d2l.Benchmark('torch'): ``` :begin_tab:`mxnet` -通过 MxNet 的基准输出速度快了数量级。由于两者都在同一个处理器上执行,因此必须继续进行其他事情。强制 MxNet 在返回之前完成所有后端计算会显示以前发生的情况:计算由后端执行,而前端将控制权返回给 Python。 +通过MXNet的基准输出快了几个数量级。由于两者都在同一处理器上执行,因此一定有其他原因。强制MXNet在返回之前完成所有后端计算。这显示了之前发生的情况:计算由后端执行,而前端将控制权返回给Python。 :end_tab: :begin_tab:`pytorch` -通过 PyTorch 的基准输出速度快了数量级。NumPy 点积在 CPU 处理器上执行,而 PyTorch 矩阵乘法则在 GPU 上执行,因此后者的速度预计会快得多。但是,巨大的时差表明必须发生其他事情。默认情况下,PyTorch 中的 GPU 操作是异步的。强制 PyTorch 在返回之前完成所有计算会显示以前发生的情况:计算由后端执行,而前端则将控制权返回给 Python。 +通过PyTorch的基准输出快了几个数量级。NumPy点积是在CPU上执行的,而PyTorch矩阵乘法是在GPU上执行的,后者的速度要快得多。但巨大的时差表明一定有其他原因。默认情况下,GPU操作在PyTorch中是异步的。强制PyTorch在返回之前完成所有计算。这显示了之前发生的情况:计算由后端执行,而前端将控制权返回给Python。 :end_tab: ```{.python .input} @@ -87,18 +87,18 @@ with d2l.Benchmark(): ``` :begin_tab:`mxnet` -广义地说,MxNet 有一个用于与用户直接交互的前端(例如通过 Python)以及系统用于执行计算的后端。如 :numref:`fig_frontends` 所示,用户可以使用各种前端语言(如 Python、R、Scala 和 C++)编写 MxNet 程序。无论使用哪种前端编程语言,MxNet 程序的执行主要发生在 C ++ 实现的后端。前端语言发布的操作将传递到后端执行。后端管理自己的线程,这些线程持续收集和执行排队任务。请注意,为此,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化彼此依赖的操作。 +从广义上讲,MXNet有一个用于与用户直接交互的前端(例如通过Python),还有一个由系统用来执行计算的后端。如 :numref:`fig_frontends` 所示,用户可以用各种前端语言编写MXNet程序,如Python、R、Scala和C++。不管使用的前端编程语言是什么,MXNet程序的执行主要发生在C++实现的后端。由前端语言发出的操作被传递到后端执行。后端管理自己的线程,这些线程不断收集和执行排队的任务。请注意,要使其工作,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化相互依赖的操作。 :end_tab: :begin_tab:`pytorch` -广义地说,PyTorch 有一个用于与用户直接交互的前端(例如通过 Python)以及系统用于执行计算的后端。如 :numref:`fig_frontends` 所示,用户可以使用各种前端语言(如 Python 和 C ++)编写 PyTorch 程序。无论使用哪种前端编程语言,PyTorch 程序的执行主要发生在 C ++ 实现的后端。前端语言发布的操作将传递到后端执行。后端管理自己的线程,这些线程持续收集和执行排队任务。请注意,为此,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化彼此依赖的操作。 +广义地说,PyTorch有一个用于与用户直接交互的前端(例如通过Python),还有一个由系统用来执行计算的后端。如 :numref:`fig_frontends` 所示,用户可以用各种前端语言编写python程序,如Python和C++。不管使用的前端编程语言,PyTorch的执行主要发生在C++实现的后端。由前端语言发出的操作被传递到后端执行。后端管理自己的线程,这些线程不断收集和执行排队的任务。请注意,要使其工作,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化相互依赖的操作。 :end_tab: -![Programming language frontends and deep learning framework backends.](../img/frontends.png) +![编程语言前端和深度学习框架后端。](../img/frontends.png) :width:`300px` :label:`fig_frontends` -让我们看另一个玩具示例,以便更好地理解依赖关系图。 +让我们看另一个简单例子,以便更好地理解依赖关系图。 ```{.python .input} x = np.ones((1, 2)) @@ -115,21 +115,21 @@ z = x * y + 2 z ``` -![The backend tracks dependencies between various steps in the computational graph.](../img/asyncgraph.svg) +![后端跟踪计算图中各个步骤之间的依赖关系。](../img/asyncgraph.svg) :label:`fig_asyncgraph` -上面的代码片段也在 :numref:`fig_asyncgraph` 中进行了说明。每当 Python 前端线程执行前三个语句之一时,它只需将任务返回到后端队列。当最后一条语句的结果需要 * 打印 * 时,Python 前端线程将等待 C ++ 后端线程完成计算变量 `z` 的结果。这种设计的一个好处是 Python 前端线程不需要执行实际的计算。因此,无论 Python 的性能如何,对程序的整体性能都没有什么影响。:numref:`fig_threading` 说明了前端和后端的交互方式。 +上面的代码片段在 :numref:`fig_asyncgraph` 中进行了说明。每当Python前端线程执行前三条语句中的一条语句时,它只是将任务返回到后端队列。当最后一个语句的结果需要被打印出来时,Python前端线程将等待C++后端线程完成变量`z`的结果计算。这种设计的一个好处是Python前端线程不需要执行实际的计算。因此,不管Python的性能如何,对程序的整体性能几乎没有影响。 :numref:`fig_threading` 演示了前端和后端如何交互。 -![Interactions of the frontend and backend.](../img/threading.svg) +![前端和后端的交互。](../img/threading.svg) :label:`fig_threading` -## 障碍和阻滞剂 +## 阻塞器(Blockers) :begin_tab:`mxnet` -有许多操作会迫使 Python 等待完成: +有许多操作将强制Python等待完成: -* 最明显的是,无论计算指令何时发出,`npx.waitall()` 都会等到所有计算完成。实际上,除非绝对必要,否则使用此操作符是一个坏主意,因为它可能会导致性能不佳。 -* 如果我们只想等到特定变量可用,我们可以调用 `z.wait_to_read()`。在这种情况下,MxNet 块返回到 Python,直到计算出变量 `z`。其他计算之后可能会继续进行。 +* 最明显的是,`npx.waitall()`等待直到所有计算完成,而不管计算指令是在什么时候发出的。在实践中,除非绝对必要,否则使用此运算符不是一个好主意,因为它可能会导致较差的性能。 +* 如果我们只想等到一个特定的变量可用,我们可以调用`z.wait_to_read()`。在这种情况下,MXNet块返回Python,直到计算出变量`z`。其他的计算很可能在之后继续。 让我们看看这在实践中是如何运作的。 :end_tab: @@ -145,9 +145,9 @@ with d2l.Benchmark('wait_to_read'): ``` :begin_tab:`mxnet` -两项操作需要大约相同的时间才能完成。除了显而易见的阻止操作之外,我们建议您知道 * 隐式 * 阻止程序。打印变量显然需要变量可用,因此它是阻止程序。最后,由于 NumPy 没有异步概念,通过 `z.asnumpy()` 转换为 NumPy 以及通过 `z.item()` 转换为标量的转换都会受阻。它需要像 `print` 函数一样访问这些值。 +两个操作的完成时间大致相同。除了明显的阻塞操作之外,我们建议您注意*隐式*阻塞器。打印变量显然要求变量可用,因此是一个阻塞器。最后,通过`z.asnumpy()`到NumPy的转换和通过`z.item()`到标量的转换是阻塞的,因为NumPy没有异步的概念。它需要像`print`函数一样访问这些值。 -经常将少量数据从 MxNet 的范围复制到 NumPy 然后会破坏本来有效的代码的性能,因为每个此类操作都需要计算图来评估获得相关术语所需的所有中间结果 * 之前 * 可以做的其他任何事情。 +频繁地将少量数据从MXNet的作用域复制到NumPy,可能会破坏原本高效代码的性能,因为每一个这样的操作都需要计算图来评估所有中间结果,以获得相关项,然后才能做其他事情。 :end_tab: ```{.python .input} @@ -163,7 +163,7 @@ with d2l.Benchmark('scalar conversion'): ## 改进计算 :begin_tab:`mxnet` -在高度多线程的系统中(即使是普通笔记本电脑也有 4 个或更多线程,在多插槽服务器上,此数字可能会超过 256 个),调度操作的开销可能会变得巨大这就是为什么非常希望以异步和并行方式进行计算和调度。为了说明这样做的好处,让我们看看如果我们按顺序或异步方式多次增加一个变量,会发生什么情况。我们通过在每次添加之间插入 `wait_to_read` 障碍来模拟同步执行。 +在高度多线程的系统上(即使普通笔记本电脑也有4个或更多线程,在多插槽服务器上,这个数字可能超过256),调度操作的开销可能会变得非常大。这就是非常希望计算和调度异步并行进行的原因。为了说明这样做的好处,让我们看看如果我们按顺序或异步多次将变量递增1会发生什么情况。我们通过在每个加法之间插入`wait_to_read`阻塞来模拟同步执行。 :end_tab: ```{.python .input} @@ -179,32 +179,32 @@ with d2l.Benchmark('asynchronous'): ``` :begin_tab:`mxnet` -Python 前端线程和 C ++ 后端线程之间稍微简化的交互可以总结如下: -1. 前端命令后端将计算任务 `y = x + 1` 插入队列。 -1. 然后,后端接收队列中的计算任务并执行实际的计算。 -1. 然后,后端将计算结果返回给前端。 -假设这三个阶段的持续时间分别为 $t_1, t_2$ 和 $t_3$。如果我们不使用异步编程,则执行 10000 个计算所需的总时间约为 $10000 (t_1+ t_2 + t_3)$。如果使用异步编程,则执行 10000 个计算所花费的总时间可以减少到 $t_1 + 10000 t_2 + t_3$(假设为 $10000 t_2 > 9999t_1$),因为前端不必等后端返回每个循环的计算结果。 +Python前端线程和C++后端线程之间的简化交互可以概括如下: +1. 前端命令后端将计算任务`y = x + 1`插入队列。 +1. 后端然后从队列接收计算任务并执行实际计算。 +1. 后端然后将计算结果返回到前端。 +假设这三个阶段的持续时间分别为$t_1, t_2$和$t_3$。如果不使用异步编程,执行10000次计算所需的总时间约为$10000 (t_1+ t_2 + t_3)$。如果使用异步编程,执行10000次计算所花费的总时间可以减少到$t_1 + 10000 t_2 + t_3$(假设$10000 t_2 > 9999t_1$),因为前端不必等待后端为每个循环返回计算结果。 :end_tab: ## 小结 -* 深度学习框架可能会将 Python 前端与执行后端分离。这允许将命令快速异步插入到后端和相关的并行度。 -* 异步导致前端响应相当灵敏。但是,请注意不要溢出任务队列,因为这可能会导致过多的内存消耗。建议对每个微型批次进行同步,以使前端和后端保持大致同步。 -* 芯片供应商提供复杂的性能分析工具,以获得对深度学习效率的更精细的洞察。 +* 深度学习框架可以将Python前端与执行后端解耦。这允许将命令快速异步插入后端。 +* 异步导致了一个相当灵活的前端。但是,请注意不要过度填充任务队列,因为它可能会导致内存消耗过多。建议对每个小批量进行同步,以保持前端和后端大致同步。 +* 芯片供应商提供了复杂的性能分析工具,以获得对深度学习效率更细粒度的洞察。 :begin_tab:`mxnet` -* 请注意,从 MxNet 的内存管理转换为 Python 将强制后端等到特定变量准备就绪。诸如 `print`、`asnumpy` 和 `item` 等函数都具有这样的效果。这可能是可取的,但粗心地使用同步可能会破坏性能。 +* 请注意,从MXNet管理的内存到Python的转换将迫使后端等待特定变量就绪。`print`、`asnumpy`和`item`等函数都具有此效果。这可能是需要的,但不小心使用同步会破坏性能。 :end_tab: ## 练习 :begin_tab:`mxnet` -1. 我们上面提到过,使用异步计算可以将执行 10000 次计算所需的总时间减少到 $t_1 + 10000 t_2 + t_3$。为什么我们必须在这里假设 $10000 t_2 > 9999 t_1$? -1. 衡量 `waitall` 和 `wait_to_read` 之间的差异。提示:执行许多指令并同步以获得中间结果。 +1. 我们上面提到,使用异步计算可以将执行10000次计算所需的总时间减少到$t_1 + 10000 t_2 + t_3$。为什么我们要假设这里是$10000 t_2 > 9999 t_1$? +1. 测量`waitall`和`wait_to_read`之间的差值。提示:执行多条指令并同步以获得中间结果。 :end_tab: :begin_tab:`pytorch` -1. 在 CPU 上,在本节中对相同的矩阵乘法运算进行基准测试。你还能通过后端观察异步吗? +1. 在CPU上,对本节中相同的矩阵乘法操作进行基准测试。你仍然可以通过后端观察异步吗? :end_tab: :begin_tab:`mxnet` From d9cf60a880684bb03c26d70d56775803be0f86ff Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:17:48 +0800 Subject: [PATCH 090/103] chapter_computational-performance/hardware (#796) --- chapter_computational-performance/hardware.md | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/chapter_computational-performance/hardware.md b/chapter_computational-performance/hardware.md index 62057dd5e..52b8fd69d 100644 --- a/chapter_computational-performance/hardware.md +++ b/chapter_computational-performance/hardware.md @@ -1,145 +1,145 @@ # 硬件 :label:`sec_hardware` -构建具有出色性能的系统需要对算法和模型有很好的了解,以捕捉问题的统计方面。同时,至少对底层硬件有一点了解也是不可或缺的。本节不能替代有关硬件和系统设计的适当课程。相反,它可以作为理解为什么某些算法比其他算法更有效以及如何实现良好吞吐量的起点。一个好的设计可以很容易地产生一个数量级的变化,而这反过来又可以在能够训练网络(例如,在一周内)和根本不能(在 3 个月内,因此错过了截止日期)之间的区别。我们首先看电脑。然后我们将放大以更仔细地查看 CPU 和 GPU。最后,我们缩小以查看服务器中心或云端中多台计算机是如何连接的。 +构建具有出色性能的系统需要很好地理解算法和模型,以捕获统计方面的问题。同时,至少对底层硬件有一定的了解也是必不可少的。本节不可替代硬件和系统设计的相关课程。相反,它可以作为理解为什么某些算法比其他算法更高效以及如何实现良好吞吐量的起点。一个好的设计可以很容易地造成性能上数量级的差异。反过来,这也可以在能够训练一个网络(例如,在1周内训练好)和根本无法训练网络(需要3个月来训练)之间产生差异。我们将从研究计算机开始。然后我们将放大以更仔细地查看CPU和GPU。最后,我们将缩小以查看在数据中心或云中多台计算机是如何连接的。 -![Latency Numbers that every programmer should know.](../img/latencynumbers.png) +![每个程序员都应该知道的延迟数字。](../img/latencynumbers.png) :label:`fig_latencynumbers` -不耐烦的读者可能能够用 :numref:`fig_latencynumbers` 来解决。它取自科林·斯科特的 [互动帖子](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html),该文章很好地概述了过去十年的进展。原来的数字来自杰夫·迪恩的 [Stanford talk from 2010](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/Stanford-DL-Nov-2010.pdf)。下面的讨论解释了这些数字的一些理由,以及它们如何指导我们设计算法。下面的讨论非常高级别和粗略。显然,它不能替代适当的课程 *,而只是为了为统计建模者提供足够的信息来做出合适的设计决策。有关计算机架构的深入概述,我们请读者参阅 :cite:`Hennessy.Patterson.2011` 或最近关于该主题的课程,例如 [Arste Asanovic] 的课程(http://inst.eecs.berkeley.edu/~cs152/sp19/)。 +不耐烦的读者也许可以通过 :numref:`fig_latencynumbers` 简单了解。它取自科林·斯科特的[互动帖子](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html)这很好地概述了过去十年的进展。原来的数字是来自杰夫迪恩的[Stanford talk from 2010](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/Stanford-DL-Nov-2010.pdf)。下面的讨论解释了这些数字的一些基本原理,以及它们如何指导我们设计算法。下面的讨论是非常笼统和粗略的。很明显,它并不能代替一门完整的课程,而只是为了给统计建模者提供足够的信息,让他们做出合适的设计决策。对于计算机体系结构的深入概述,我们建议读者参考 :cite:`Hennessy.Patterson.2011` 或关于该主题的最新课程,例如[Arste Asanovic](http://inst.eecs.berkeley.edu/~cs152/sp19/)。 ## 计算机 -大多数深度学习研究人员和从业人员都可以使用具有相当数量的内存、计算、某种形式的加速器(如 GPU)或其倍数的计算机。计算机由以下关键组件组成: +大多数深度学习研究者和实践者都可以使用一台具有相当数量的内存、计算、某种加速卡(如GPU)的计算机。计算机由以下关键部件组成: -* 能够执行我们提供的程序的处理器(也称为 CPU)(除了运行操作系统和许多其他内容之外),通常由 8 个或更多内核组成。 -* 内存 (RAM) 用于存储和检索计算结果,例如体重矢量和激活以及训练数据。 -* 以太网网络连接(有时是多个),速度从 1 Gb/s 到 100 Gb/s。在高端服务器上,可以找到更高级的互连。 -* 用于将系统连接到一个或多个 GPU 的高速扩展总线 (PCIe)。服务器最多有 8 个加速器,通常以高级拓扑连接,而台式机系统则有 1 个或 2 个,具体取决于用户的预算和电源的大小。 -* 耐用存储,例如磁性硬盘驱动器、固态驱动器,在许多情况下都使用 PCIe 总线连接。它可以将训练数据高效地传输到系统,并根据需要存储中间检查站。 +* 一个处理器(也被称为CPU),它能够执行我们给它的程序(除了运行操作系统和许多其他功能之外),通常由8个或更多核心组成。 +* 内存(RAM)用于存储和检索计算结果,如权重向量和激活,以及训练数据。 +* 一个以太网连接(有时是多个),速度从1 GB/s到100 GB/s不等。在高端服务器上可以找到更高级的互连。 +* 将系统连接到一个或多个GPU的高速扩展总线(PCIe)。服务器最多有8个加速卡,通常以高级拓扑连接,而桌面系统则有1个或2个加速卡,具体取决于用户的预算和电源的大小。 +* 耐用的存储设备,如磁盘驱动器、固态驱动器,在许多情况下使用PCIe总线连接。它可根据需要高效地将训练数据传输到系统,并根据需要存储中间检查点。 -![Connectivity of components of a computer.](../img/mobo-symbol.svg) +![计算机组件的连接。](../img/mobo-symbol.svg) :label:`fig_mobo-symbol` -正如 :numref:`fig_mobo-symbol` 所示,大多数组件(网络、GPU 和存储)通过 PCIe 总线连接到 CPU。它由直接连接到 CPU 的多个通道组成。例如,AMD 的 Threadripper 3 有 64 个 PCIe 4.0 通道,每条通道都能在双向传输 16 Gbit/s 数据。内存直接连接到 CPU,总带宽高达 100 Gb/s。 +如 :numref:`fig_mobo-symbol` 所示,大多数组件(网络、GPU和存储)通过PCIe总线连接到CPU。它由直接连接到CPU的多个通道组成。例如,AMD的Threadripper 3有64个PCIe 4.0通道,每个通道都能够双向传输16 Gbit/s的数据。内存直接连接到CPU,总带宽高达100gb/s。 -当我们在计算机上运行代码时,我们需要将数据随机播放到处理器(CPU 或 GPU),执行计算,然后将结果从处理器移回 RAM 和耐用存储。因此,为了获得良好的性能,我们需要确保这种方法无缝工作,而不会任何一个系统成为主要瓶颈。例如,如果我们无法足够快地加载图像,处理器将无法做任何工作。同样,如果我们不能足够快地将矩阵移动到 CPU(或 GPU),其处理元素将会饿死。最后,如果我们想在网络中同步多台计算机,后者不应该减慢计算速度。一种选择是将沟通和计算交织在一起。让我们更详细地看看各个组件。 +当我们在计算机上运行代码时,我们需要将数据转移到处理器(CPU或GPU)执行计算,然后将结果从处理器移回内存和持久存储器。因此,为了获得良好的性能,我们需要确保这一点能够无缝工作,而不会有任何一部分成为主要的瓶颈。例如,如果我们不能足够快地加载图像,处理器将没有任何工作要做。同样地,如果我们不能足够快地将矩阵移动到CPU(或GPU)上,CPU(或GPU)就会无法全力运行。最后,如果我们想在网络上同步多台计算机,网络不应该减慢计算速度。一种选择是交叉通信和计算。让我们更详细地看看各个组件。 -## 记忆 +## 内存 -最基本的内存用于存储需要易于访问的数据。目前 CPU 内存通常是 [DDR4](https://en.wikipedia.org/wiki/DDR4_SDRAM) 种类型,每个模块提供 20—25 Gb/s 的带宽。每个模块都有一条 64 位宽的总线。通常使用对内存模块来允许多个通道。CPU 有 2 到 4 个内存通道,即它们的峰值内存带宽介于 4 0Gb/s 到 100 Gb/s 之间。通常每个渠道有两家银行。例如,AMD 的 Zen 3 Threadripper 有 8 个插槽。 +最基本的内存用于存储需要随时访问的数据。目前,CPU的内存通常为[DDR4](https://en.wikipedia.org/wiki/DDR4_SDRAM)类型,每个模块提供20-25Gb/s的带宽。每个模块都有一条64位宽的总线。通常使用成对的内存模块来允许多个通道。CPU有2到4个内存通道,也就是说,它们的峰值内存带宽在40GB/s到100 GB/s之间。通常每个通道有两个物理BANK。例如AMD的Zen 3T Threadripper有8个插槽。 -尽管这些数字令人印象深刻,但事实上,它们只能讲述部分故事。当我们想从内存中读取一部分时,我们首先需要告诉内存模块在哪里可以找到信息。也就是说,我们首先需要将 * 地址 * 发送到 RAM。完成此操作后,我们可以选择只读一条 64 位记录或一系列记录。后者被称为 * 突发读数 *。简而言之,将地址发送到内存并设置传输大约需要 100 ns(详细信息取决于所用内存芯片的特定时序系数),每次后续传输只需 0.2 ns。简而言之,第一次读取是后续读取的 500 倍!请注意,我们每秒可以执行高达 10,000,000 次随机读取。这表明我们尽可能避免随机内存访问,而是使用突发读取(和写入)。 +虽然这些数字令人印象深刻,但实际上,它们只说明了故事的一部分。当我们想要从内存中读取一部分时,我们首先需要告诉内存模块在哪里可以找到信息。也就是说,我们首先需要将*地址*(address)发送到内存。完成后,我们可以选择只读取一条64位记录或一长串记录。后者称为“突发读取”(burst read)。简而言之,向内存发送地址并设置传输大约需要100ns(细节取决于所用内存芯片的特定定时系数),每个后续传输只需要0.2ns。简而言之,第一次读取的成本是后续读取的500倍!请注意,我们每秒最多可以执行10000000次随机读取。这表明我们尽可能避免随机内存访问,而是使用突发读取(和写入)。 -如果我们考虑到我们有多个 * 银行 *,事情就会复杂一些。每家银行可以基本上独立读取内存。这意味着两件事。一方面,只要随机读取在内存中均匀分布,有效随机读取次数最多可高 4 倍。这还意味着执行随机读取仍然是一个坏主意,因为突发读取速度也快了 4 倍。另一方面,由于内存对齐到 64 位边界,因此最好将任何数据结构与相同边界对齐。在设置适当的标志时,编译器几乎会做到这一点 [automatically](https://en.wikipedia.org/wiki/Data_structure_alignment)。鼓励好奇的读者查看关于 DRAM 的讲座,例如 [Zeshan Chishti] 的讲座 (http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf)。 +当我们考虑到我们有多个物理BANK时,事情就更复杂了。每个BANK都可以独立地读取内存。这意味着两件事。一方面,如果随机读操作均匀分布在内存中,那么有效的随机读操作次数将高达4倍。这也意味着执行随机读取仍然不是一个好主意,因为突发读取的速度也快了4倍。另一方面,由于内存对齐到64位边界,最好将任何数据结构与相同的边界对齐。当设置了适当的标志时,编译器基本上就是[自动化](https://en.wikipedia.org/wiki/Data_structure_alignment)地执行此操作。我们鼓励好奇的读者回顾一个关于DRAM的讲座,比如[Zeshan Chishti](http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf).]所做的讲座。 -GPU 内存受到更高的带宽要求,因为它们的处理元素比 CPU 多得多。总的来说,有两种选择可以解决这些问题。首先是使内存总线显著扩大。例如,NVIDIA 的 RTX 2080 Ti 有一条 352 位宽的总线。这允许同时传输更多信息。其次,GPU 使用特定的高性能内存。消费级设备(例如 NVIDIA 的 RTX 和 Titan 系列)通常使用 [GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM) 芯片,总带宽超过 500 Gb/s。另一种方法是使用 HBM(高带宽内存)模块。它们使用截然不同的界面,直接与专用硅片上的 GPU 连接。这使得它们非常昂贵,而且它们的使用通常仅限于高端服务器芯片,例如 NVIDIA Volta V100 系列加速器。毫不奇怪,由于前者的成本较高,GPU 内存通常比 CPU 内存小 *。出于我们的目的,他们的性能特征基本上相似,速度快得多。为了本书的目的,我们可以放心地忽略细节。它们只有在调整 GPU 内核以实现高吞吐量时才重要。 +GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得多。总的来说,解决这些问题有两种选择。首先是使内存总线变得更宽。例如,NVIDIA的RTX 2080 Ti有一条352位宽的总线。这样就可以同时传输更多的信息。其次,GPU使用特定的高性能内存。消费级设备,如NVIDIA的RTX和Titan系列,通常使用[GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM)芯片,总带宽超过500GB/s。另一种选择是使用HBM(高带宽存储器)模块。它们使用截然不同的接口,直接与专用硅片上的GPU连接。这使得它们非常昂贵,通常仅限于高端服务器芯片,如NVIDIA Volta V100系列加速卡。毫不奇怪,GPU内存通常比CPU内存小得多,因为前者的成本更高。就我们的目的而言,它们的性能特征大体上是相似的,只是速度要快得多。就本书而言,我们完全可以忽略细节。它们只在调整GPU核心以获得高吞吐量时才起作用。 -## 存储 +## 存储器 -我们看到 RAM 的一些关键特征是 * 带宽 * 和 * 延迟 *。存储设备也是如此,只是差异可能更加极端。 +我们看到内存的一些关键特性是*带宽*(bandwidth)和*延迟*(latency)。存储设备也是如此,只是差异可能更大。 ### 硬盘驱动器 -*硬盘驱动器 *(HDD)已经使用了半个多世纪。简而言之,它们包含许多带头的旋转拼盘,可以放置在任何给定的轨道上读写。高端磁盘在 9 个磁盘上可容纳高达 16 TB。HDD 的主要优势之一是它们相对便宜。他们的许多缺点之一是他们典型的灾难性故障模式和相对较高的读取延迟。 +硬盘驱动器(Hard disk drives,HDDs)已经使用了半个多世纪。简单说,它们包含许多旋转的盘片,这些盘片的磁头可以放置在任何给定的磁道上进行读写。高端磁盘在9个盘片上可容纳高达16 TB的容量。硬盘的主要优点之一是相对便宜。它们的许多缺点之一是典型的灾难性故障模式和相对较高的读取延迟。 -要理解后者,请考虑一下 HDD 在 7,200 RPM 左右(每分钟转数)旋转的事实。如果速度快得多,由于对拼盘施加的离心力,它们就会破碎。在访问磁盘上的特定扇区时,这有一个重大缺点:我们需要等到磁盘旋转到位(我们可以移动磁盘但不能加速实际磁盘)。因此,在请求的数据可用之前,可能需要 8 毫秒以上。表达这一点的常见方法是说 HDD 可以以大约 100 个 IOP 运行(每秒输入/输出操作)。过去二十年来,这一数字基本上保持不变。更糟糕的是,增加带宽同样困难(大约为 100—200 MB/s)。毕竟,每个头都读取一条比特轨,因此比特率只能随信息密度的平方根进行缩放。因此,HDD 正在迅速降级为存档存储和非常大型数据集的低级存储。 +要理解后者,请考虑一下硬盘驱动器的转速大约为7200 RPM(每分钟转数)。如果它们转速更快,它们就会由于施加在盘子上的离心力而碎裂。在访问磁盘上的特定扇区时,这有一个主要缺点:我们需要等待,直到盘片旋转到位(我们可以移动磁头,但不能加速实际的磁盘)。因此,在请求的数据可用之前可能需要8毫秒。一种常见的表达方式是,硬盘驱动器可以以大约100 IOPs(每秒输入/输出操作)的速度工作。这一数字在过去二十年中基本上没有变化。更糟糕的是,增加带宽同样困难(大约为100-200MB/s)。毕竟,每个磁头读取一个比特磁道,因此比特率只随信息密度的平方根缩放。因此,对于非常大的数据集,HDD正迅速降级为归档存储和低级存储。 -### 固态硬盘 +### 固态驱动器 -固态硬盘 (SSD) 使用闪存来持久存储信息。这允许快速 * 访问存储的记录。现代固态硬盘的运行速度可达 100,000 到 500,000 IOP,即比硬盘快 3 个数量级。此外,它们的带宽可以达到 1—3Gb/s,即比 HDD 快一个数量级。这些改进听起来几乎太好了,无法实现。事实上,由于固态硬盘的设计方式,它们附带了以下警告。 +固态驱动器(Solid state drives,SSD)使用闪存持久地存储信息。这允许更快地访问存储的记录。现代固态驱动器的IOPs可以达到100000到500000,即比硬盘驱动器快3个数量级。此外,它们的带宽可以达到1-3GB/s,即比硬盘驱动器快一个数量级。这些改进听起来太好了,简直难以置信。实际上,由于固态驱动器的设计方式,它们有以下注意事项: -* SSD 将信息存储在块中(256 KB 或更大)。它们只能作为一个整体编写,这需要很长时间。因此,SSD 上的按位随机写入性能非常差。同样,写入数据通常需要很长时间,因为必须读取、删除区块,然后用新信息重写。到目前为止,SSD 控制器和固件已开发出算法来缓解这一尽管如此,写入速度可能会慢得多,特别是对于 QLC(四级单元)SSD。提高性能的关键是维持 * 队列 * 的操作,如果可能的话,更喜欢读取和写入大块。 -* 固态硬盘中的记忆细胞耗尽相对较快(通常在几千次写入之后就已经出现了)。磨损级保护算法能够将降解扩散到许多细胞中。也就是说,不建议使用 SSD 来交换文件或大型日志文件聚合。 -* 最后,带宽的大幅增加迫使计算机设计师将固态硬盘直接连接到 PCIe 总线。能够处理此问题的驱动器称为 nVMe(增强的非易失性存储器),最多可以使用 4 个 PCIe 通道。在 PCIe 4.0 上,这高达 8Gb/s。 +* 固态驱动器以块(256 KB或更大)存储信息。它们只能作为一个整体来写入,这需要大量的时间。因此,固态驱动器上的按位随机写入性能非常差。同样地,写入数据通常需要大量的时间,因为块必须被读取、擦除,然后用新信息重写。到目前为止,固态驱动器的控制器和固件已经开发出了缓解这种情况的算法。尽管如此,写入速度可能会慢得多,特别是对于QLC(四层单元)固态驱动器。提高性能的关键是维护操作的“队列”。如果可能,优先读取和写入大的块。 +* 固态驱动器中的存储单元磨损得比较快(通常在几千次写入之后就已经磨损了)。磨损级别保护算法能够将退化扩展到许多单元。也就是说,不建议将固态驱动器用于交换分区文件或大型日志文件。 +* 最后,带宽的大幅增加迫使计算机设计者将固态驱动器直接连接到PCIe总线。能够处理此问题的驱动器称为NVMe(非易失性内存增强),最多可以使用4个PCIe通道。在PCIe 4.0上最高可达8GB/s。 ### 云存储 -云存储提供了一系列可配置的性能。也就是说,向虚拟机分配存储是动态的,无论是在数量还是在速度方面,都是由用户选择的。我们建议用户在延迟过高时(例如,在培训过程中使用许多小记录时)增加预配置的 IOP 数量。 +云存储提供了一系列可配置的性能。也就是说,根据用户的选择,虚拟机的存储分配在数量和速度上都是动态的。我们建议用户在延迟太高时(例如,在有许多小记录的训练期间)增加IOPs的配置数。 -## 中央处理器 +## CPU -中央处理单元 (CPU) 是任何计算机的核心。它们由许多关键组件组成:* 能够执行机器代码的处理器内核 *,* 总线 * 连接它们(特定拓扑结构在处理器型号、代和供应商之间有显著差异),以及 *Caches*,以实现比处理器更高的带宽和更低的延迟内存访问可以通过从主内存中读取。最后,几乎所有现代 CPU 都包含 * 矢量处理单元 *,以帮助高性能线性代数和卷数,因为它们在媒体处理和机器学习中很常见。 +中央处理器(CPU)是任何计算机的核心。它们由许多关键组件组成:能够执行机器代码的*处理器核心*(processor cores)、连接它们的*总线*(bus)(注意,总线会因为处理器型号、各代产品和供应商之间的特定拓扑结构有明显不同)和*缓存*(caches)(允许比从主内存读取更高的带宽和更低的延迟内存访问)。最后,几乎所有的现代CPU都包含*向量处理单元*(vector processing units)来辅助高性能线性代数和卷积运算,因为它们在媒体处理和机器学习中很常见。 -![Intel Skylake consumer quad-core CPU.](../img/skylake.svg) +![Intel Skylake消费级四核CPU。](../img/skylake.svg) :label:`fig_skylake` -:numref:`fig_skylake` 描述了英特尔 Skylake 消费级四核 CPU。它有一个集成的 GPU、缓存和一个连接四个核心的环形总线。以太网、WiFi、蓝牙、SSD 控制器和 USB 等外围设备可以是芯片组的一部分或直接连接 (PCIe) 至 CPU。 +:numref:`fig_skylake`描述了Intel Skylake消费级四核CPU。它有一个集成的GPU、缓存和一个连接四个核心的环总线。外围设备,如以太网、WiFi、蓝牙、SSD控制器和USB,要么是芯片组的一部分,要么通过PCIe直接连接到CPU。 ### 微体系结构 -每个处理器内核都由一组相当复杂的组件组成。尽管各代人和供应商之间的细节不同,但基本功能几乎是标准的。前端加载指令并尝试预测将采取哪条路径(例如,用于控制流程)。然后将指令从汇编代码解码为微指令。汇编代码通常不是处理器执行的最低级别的代码。相反,复杂的指令可能会被解码为一组更低级别的操作。然后,这些将由实际的执行核心处理。后者通常能够同时执行许多操作。例如,:numref:`fig_cortexa77` 的 ARM Cortex A77 核心能够同时执行多达 8 个操作。 +每个处理器核心都由一组相当复杂的组件组成。虽然各代产品和供应商的细节有所不同,但基本功能基本上是标准的。前端加载指令并尝试预测将采用哪条路径(例如,对于控制流)。然后将指令从汇编代码解码为微指令。汇编代码通常不是处理器执行的最低级别代码。相反,复杂指令可以被解码成一组更低级的操作。然后由实际的执行核心处理。后者通常能够同时执行许多操作。例如, :numref:`fig_cortexa77` 的ARM Cortex A77核心可以同时执行多达8个操作。 -![ARM Cortex A77 Microarchitecture.](../img/a77.svg) +![ARM Cortex A77 微体系结构](../img/a77.svg) :label:`fig_cortexa77` -这意味着高效的程序可能能够在每个时钟周期执行多条指令,前提是它们可以独立执行。并非所有单位的创建都一样。一些专注于整数指令,而另一些则针对浮点性能进行了优化。为了提高吞吐量,处理器还可能在分支指令中同时遵循多条代码路径,然后丢弃未采用的分支的结果。这就是为什么分支预测单元(在前端)很重要,以至于只追求最有前途的途径。 +这意味着高效的程序可以在每个时钟周期执行多条指令,前提是它们可以独立执行。并不是所有的单位都是平等的。一些专用于整数指令,而另一些则针对浮点性能进行了优化。为了提高吞吐量,处理器还可以在分支指令中同时遵循多个代码路径,然后丢弃未执行分支的结果。这就是为什么分支预测单元很重要(在前端),只有最有希望的路径才会被跟踪。 ### 矢量化 -深度学习非常需要计算机。因此,要使 CPU 适合机器学习,需要在一个时钟周期内执行许多操作。这是通过矢量单位实现的。它们有不同的名称 : on ARM they are called NEON, on x86 they (a recent generation) are referred to as [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) units. A common aspect is that they are able to perform SIMD (single instruction multiple data) operations. :numref:`fig_neon128` 显示了如何在 ARM 上的一个时钟周期内添加 8 个短整数。 +深度学习是非常需要计算机的。因此,为了使CPU适合机器学习,需要在一个时钟周期内执行许多操作。这是通过向量处理单元实现的。它们有不同的名称: 在ARM上叫做NEON, 在x86上被称为[AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions)。一个常见的功能是它们能够执行单指令多数据(single instruction multiple data,SIMD)操作。 :numref:`fig_neon128` 显示了如何在ARM上的一个时钟周期中完成8个整数加法。 -![128 bit NEON vectorization.](../img/neon128.svg) +![128位NEON矢量化](../img/neon128.svg) :label:`fig_neon128` -根据架构的选择,此类寄存器的长度最多为 512 位,允许最多 64 对数字的组合。例如,我们可能会乘以两个数字,然后将它们添加到第三个数字,也被称为融合乘法加。英特尔 [OpenVino](https://01.org/openvinotoolkit) 使用这些功能在服务器级 CPU 上实现深度学习的可观吞吐量。但是请注意,这个数字与 GPU 能够实现的目标完全相似。例如,NVIDIA 的 RTX 2080 Ti 拥有 4,352 个 CUDA 内核,每个内核都能随时处理此类操作。 +根据体系结构的选择,此类寄存器最长可达512位,最多可组合64对数字。例如,我们可能会将两个数字相乘,然后将它们与第三个数字相加,这也称为乘加融合(fused multiply-add)。Intel的[OpenVino](https://01.org/openvinotoolkit)使用这些处理器来获得可观的吞吐量,以便在服务器级CPU上进行深度学习。不过请注意,与GPU的能力相比,这个数字完全相形见绌。例如,NVIDIA的RTX 2080Ti拥有4352个CUDA核心,每个核心都能够在任何时候处理这样的操作。 ### 缓存 -考虑以下情况 : we have a modest CPU core with 4 cores as depicted in :numref:`fig_skylake`,以 2 GHz 频率运行。此外,让我们假设 IPC(每时钟指令)计数为 1,并且这些单元具有启用 256 位宽度的 AVX2。让我们进一步假设至少有一个用于 AVX2 操作的寄存器需要从内存中检索。这意味着 CPU 每时钟周期消耗 $4 \times 256 \text{ bit} = 128 \text{ bytes}$ 个数据。除非我们能够每秒向处理器传输 $2 \times 10^9 \times 128 = 256 \times 10^9$ 字节,否则处理元素将会饿死。不幸的是,这种芯片的存储器接口仅支持 20—40 Gb/s 的数据传输,即少一个数量级。修复方法是尽可能避免从内存中加载 *new* 数据,而是将其缓存在 CPU 本地。这就是缓存派上用场的地方。通常使用以下名称或概念: +考虑以下情况: 我们有一个中等的CPU核心,有4个核心,如 :numref:`fig_skylake` 所示,运行在2GHz频率。此外,我们假设IPC(每个时钟的指令数)计数为1,并且这些单元启用了256位宽度的AVX2。让我们进一步假设需要从存储器检索用于AVX2操作的至少一个寄存器。这意味着中央处理器每个时钟周期消耗$4 \times 256 \text{ bit} = 128 \text{ bytes}$的数据。除非我们能够每秒向处理器传输$2 \times 10^9 \times 128 = 256 \times 10^9$字节,否则处理单元将会耗尽。不幸的是,这种芯片的存储器接口仅支持20-40 Gb/s的数据传输,即少了一个数量级。解决方法是尽可能避免从内存加载新数据,而是将其本地缓存在CPU上。这就是缓存派上用场的地方。通常使用以下名称或概念: -* ** 注册器 ** 严格来说不是缓存的一部分。他们帮助舞台说明。也就是说,CPU 寄存器是 CPU 可以以时钟速度访问的内存位置,而不会造成任何延迟损失。CPU 有数十个寄存器。有效地使用寄存器取决于编译器(或程序员)。例如,C 编程语言有一个 `register` 关键字。 -* **L1 缓存 ** 是抵御高内存带宽要求的第一道防线。L1 缓存很小(典型大小可能为 32—64 KB),通常分为数据缓存和指令缓存。当在 L1 缓存中找到数据时,访问速度非常快。如果在那里找不到它们,则搜索将在缓存层次结构中向下进行。 -* **L2 缓存 ** 是下一站。根据架构设计和处理器尺寸,它们可能是独家的。它们可能只能由给定的内核访问,也可能在多个内核之间共享。二级缓存更大(通常每核 256—512 KB),慢于 L1。此外,要访问 L2 中的内容,我们首先需要检查以意识到数据不在 L1 中,这会增加少量额外的延迟。 -* **L3 缓存 ** 在多个核心之间共享,可能很大。AMD 的 Epyc 3 服务器 CPU 有高达 256 MB 的高速缓存分布在多个数字中。更典型的数字在 4-8 MB 范围内。 +* **寄存器** 严格来说,不是缓存的一部分。它们帮助传达指令。这就是说,CPU寄存器是CPU可以以时钟速度访问而不受任何延迟惩罚的存储位置。CPU有几十个寄存器。有效地使用寄存器取决于编译器(或程序员)。例如,C语言有一个`register`关键字。 +* **一级缓存**是应对高内存带宽要求的第一道防线。一级缓存很小(常见的大小可能是32-64KB),通常分为数据和指令缓存。当在一级缓存中找到数据时,访问速度非常快。如果在那里找不到它们,搜索将沿着缓存层次结构向下进行。 +* **二级缓存**是下一站。根据架构设计和处理器大小的不同,它们可能是独占的。即它们可能只能由给定的核心访问,或者在多个核心之间共享。二级缓存比一级缓存更大(通常每个核心256-512KB),而速度更慢。此外,要访问L2中的内容,我们首先需要检查以确定数据不在L1中,这会增加少量额外的延迟。 +* **L3缓存**在多个核之间共享,并且可以非常大。AMD的Epyc 3服务器CPU在多个芯片上拥有高达256MB的高速缓存。更常见的数字在4-8MB范围内。 -预测接下来需要哪些内存元素是芯片设计中的关键优化参数之一。例如,建议以 *forward* 方向遍历内存,因为大多数缓存算法都会尝试 * 向前读取 * 而不是向后读。同样,将内存访问模式保持在本地也是提高性能的好方法。 +预测下一步需要哪些存储元件是芯片设计中的关键优化参数之一。例如,建议以向前的方向遍历内存,因为大多数缓存算法将尝试*向前读取*而不是向后读取。同样,将内存访问模式保持在本地也是提高性能的一个好方法。 -添加缓存是一把双刃剑。一方面,他们确保处理器内核不会缺少数据。与此同时,它们增加芯片尺寸,耗用了本可用于提高处理能力的面积。此外,* 缓存未命中 * 可能会很昂贵。考虑 :numref:`fig_falsesharing` 中描述的最坏情况,* 虚假共享 *。当处理器 1 上的线程请求数据时,内存位置将缓存在处理器 0 上。要获得它,处理器 0 需要停止正在执行的操作,将信息写回主内存,然后让处理器 1 从内存中读取信息。在此操作期间,两个处理器都等与高效的单处理器实现相比,这样的代码在多个处理器上运行速度很可能更慢 *。这是为什么缓存大小(除了物理大小外)有实际限制的又一个原因。 +添加缓存是一把双刃剑。一方面,它们确保处理器核心不缺乏数据。同时,它们增加了芯片尺寸,消耗了原本可以用来提高处理能力的面积。此外,*缓存未命中*的代价可能会很昂贵。考虑最坏的情况,*错误共享*(false sharing),如 :numref:`fig_falsesharing` 所示。当处理器1上的线程请求数据时,内存位置缓存在处理器0上。为了获得它,处理器0需要停止它正在做的事情,将信息写回主内存,然后让处理器1从内存中读取它。在此操作期间,两个处理器都等待。与高效的单处理器实现相比,这种代码在多个处理器上运行的速度可能要慢得多。这就是为什么缓存大小(除了物理大小之外)有实际限制的另一个原因。 -![False sharing (image courtesy of Intel).](../img/falsesharing.svg) +![错误共享(图片由英特尔提供)](../img/falsesharing.svg) :label:`fig_falsesharing` -## GPU 和其他加速器 +## GPU和其他加速卡 -声称如果没有 GPU,深度学习就不会成功,这并不夸张。同样,可以合理地争辩说,由于深度学习,GPU 制造商的财富大幅度增加。硬件和算法的共同演变导致了这样一种情况,即深度学习更好或坏是更好的学习是最好的统计建模范式。因此,了解 GPU 和相关加速器(如 TPU :cite:`Jouppi.Young.Patil.ea.2017`)的具体优势是值得的。 +毫不夸张地说,如果没有GPU,深度学习就不会成功。基于同样的原因,有理由认为GPU制造商的财富由于深度学习而显著增加。这种硬件和算法的协同进化导致了这样一种情况:无论好坏,深度学习都是更可取的统计建模范式。因此,了解GPU和其他加速卡(如TPU:cite:`Jouppi.Young.Patil.ea.2017`)的具体好处是值得的。 -值得注意的是,在实践中经常作出的区别:加速器已针对训练或推理进行了优化。对于后者,我们只需要计算网络中的正向传播。反向传播不需要存储中间数据。此外,我们可能不需要非常精确的计算(FP16 或 INT8 通常就足够了)。另一方面,在训练期间,所有中间结果都需要存储才能计算梯度。此外,累积梯度需要更高的精度以避免数字下溢(或溢出)。这意味着 FP16(或与 FP32 混合精度)是最低要求。所有这些都需要更快、更大的内存(HBM2 与 GDDR6)和更大的处理能力。例如,NVIDIA 的 [Turing](https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/) T4 GPU 针对推理进行了优化,而 V100 GPU 更适合培训。 +值得注意的是,在实践中经常会有这样一个区别:加速卡是为训练或推理而优化的。对于后者,我们只需要计算网络中的前向传播。反向传播不需要存储中间数据。此外,我们可能不需要非常精确的计算(FP16或INT8通常就足够了)。另一方面,在训练过程中,所有中间结果都需要存储来计算梯度。此外,累积梯度需要更高的精度,以避免数值下溢(或溢出)。这意味着FP16(或与FP32的混合精度)是最低要求。所有这些都需要更快、更大的内存(HBM2与GDDR6相比)和更高的处理能力。例如,NVIDIA的[Turing](https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/) T4 GPU优化用于推理,而V100 GPU更适合用于训练。 -回想一下 :numref:`fig_neon128` 中所示的矢量化。向处理器内核添加矢量单元使我们能够显著提高吞吐量。例如,在 :numref:`fig_neon128` 的示例中,我们能够同时执行 16 个操作。首先,如果我们添加的运算不仅优化了向量之间的运算,而且也优化矩阵之间的运算会怎么样?这一策略导致了张量核心(很快将涵盖)。第二,如果我们添加更多的核心怎么办?简而言之,这两种策略总结了 GPU 中的设计决策。:numref:`fig_turing_processing_block` 概述了基本的处理模块。它包含 16 个整数和 16 个浮点型单位。除此之外,两个 tensor 内核加速了与深度学习相关的少数额外操作的子集。每个流媒体多处理器由四个这样的模块组成。 +回想一下如:numref:`fig_neon128`所示的矢量化。将向量处理单元添加到处理器核心可以显著提高吞吐量。例如,在 :numref:`fig_neon128` 的例子中,我们能够同时执行16个操作。首先,如果我们添加的运算不仅优化了向量之间的运算,而且优化了矩阵之间的运算,会怎么样?这个策略引入了张量核(tensor cores),这稍后将讨论。第二,如果我们增加更多的核心呢?简而言之,这两种策略总结了GPU中的设计决策。 :numref:`fig_turing_processing_block` 给出了基本处理块的概述。它包含16个整数和16个浮点单位。除此之外,两个张量核加速了与深度学习相关的附加操作的窄子集。每个流式多处理器由四个这样的块组成。 -![NVIDIA Turing processing block (image courtesy of NVIDIA).](../img/turing-processing-block.png) +![NVIDIA Turing 处理块(图片由英伟达提供)](../img/turing-processing-block.png) :width:`150px` :label:`fig_turing_processing_block` -接下来,12 个流式多处理器被分为构成高端 TU102 处理器的图形处理群集。充足的内存通道和二级缓存补充了设置。:numref:`fig_turing` 提供了相关的细节。设计这样一个器件的原因之一是,可以根据需要添加或移除单个模块,以允许更紧凑的芯片和处理良率问题(可能无法激活故障模块)。幸运的是,在 CUDA 和框架代码层下,对于休闲的深度学习研究员来说,编程这些设备完全隐藏在一起。特别是,如果有可用资源,可以在 GPU 上同时执行多个程序。尽管如此,必须注意设备的局限性,以避免选择不适合设备内存的型号。 +接下来,将12个流式多处理器分组为图形处理集群,这些集群构成了高端TU102处理器。充足的内存通道和二级缓存补充了设置。:numref:`fig_turing`有相关的细节。设计这种设备的原因之一是,可以根据需要添加或删除单个模块,以允许更紧凑的芯片和处理成品率问题(故障模块可能无法激活)。幸运的是,在CUDA和框架代码层之下,这类设备的编程对随意的深度学习研究人员隐藏得很好。特别是,如果有可用的资源,在GPU上可以同时执行多个程序。尽管如此,了解设备的局限性是值得的,以避免选择不适合设备内存的型号。 -![NVIDIA Turing architecture (image courtesy of NVIDIA)](../img/turing.png) +![NVIDIA Turing 架构(图片由英伟达提供)](../img/turing.png) :width:`350px` :label:`fig_turing` -值得更详细地提及的最后一个方面是 * 张量核心 *。它们是最近增加对深度学习特别有效的优化电路的趋势的一个例子。例如,TPU 为快速矩阵乘法添加了收缩压阵列 :cite:`Kung.1988`。那里的设计是为了支持极少的大型操作(第一代 TPU)。Tensor 核心在另一端。它们针对涉及 $16 \times 16$ 和 $16 \times 16$ 矩阵的小型操作进行了优化,具体取决于它们的数值精度。:numref:`fig_tensorcore` 概述了优化。 +最后值得一提的是*张量核*(tensor cores)。它们是最近增加更多优化电路的趋势的一个例子,这些电路对深度学习特别有效。例如,TPU添加了用于快速矩阵乘法的脉动阵列:cite:`Kung.1988`。在那里,设计是为了支持非常小数量(第一代TPU支持数量为1)的大型操作。张量核是另一个极端。它们针对$4 \times 4$和$16 \times 16$矩阵之间的小型运算进行了优化,具体取决于它们的数值精度。 :numref:`fig_tensorcore` 给出了优化的概述。 -![NVIDIA tensor cores in Turing (image courtesy of NVIDIA).](../img/tensorcore.jpg) +![NVIDIA Turing架构中的张量核心(图片由英伟达提供)](../img/tensorcore.jpg) :width:`400px` :label:`fig_tensorcore` -显然,在优化计算时,我们最终会作出某些妥协。其中之一是 GPU 不擅长处理中断和稀疏数据。尽管存在明显的例外,例如 [Gunrock](https://github.com/gunrock/gunrock) :cite:`Wang.Davidson.Pan.ea.2016`,稀疏矩阵和向量的访问模式与 GPU 出色的高带宽突发读取操作不太好。匹配这两个目标是积极研究的一个领域。例如,请参阅 [DGL](http://dgl.ai),这是一个专为图表进行深度学习而调整的图书馆。 +显然,在针对计算进行优化时,我们最终会做出某些妥协。其中之一是GPU不太擅长处理中断和稀疏数据。尽管有一些明显的例外,如[Gunrock](https://github.com/gunrock/gunrock) :cite:`Wang.Davidson.Pan.ea.2016`,但稀疏矩阵和向量的访问模式并不适合GPU擅长的高带宽突发读取操作。匹配这两个目标是一个积极研究的领域。例如[DGL](http://dgl.ai),这是一个专为图深度学习而设计的库。 -## 网络和公共汽车 +## 网络和总线 -每当单个设备不足以进行优化时,我们都需要将数据传入和传出该设备来同步处理。这是网络和公共汽车派上用场的地方。我们有许多设计参数:带宽、成本、距离和灵活性。一方面,我们的 WiFi 范围相当不错,非常容易使用(毕竟没有电线),价格便宜,但它提供了相对平庸的带宽和延迟。没有任何机器学习研究人员都不会用它来构建服务器群集。在接下来的内容中,我们重点介绍了适合深度学习的互连。 +每当单个设备不足以进行优化时,我们就需要在其中来回传输数据以同步处理。这就是网络和总线派上用场的地方。我们有许多设计参数:带宽、成本、距离和灵活性。一方面,我们有一个很好的WiFi范围,是非常容易使用(毕竟没有线缆),便宜,但它提供的带宽和延迟相对一般。没有一个头脑正常的机器学习研究人员会用它来构建服务器集群。在接下来的内容中,我们将重点关注适合深度学习的互连。 -* **PCIe** 是专用总线,用于每条通道的极高带宽点对点连接(在 PCIe 4.0 上,16 通道插槽中的 PCIe 4.0 最高可达 32 Gb/s)。延迟的顺序为个位数微秒(5 μs)。PCIe 链接很宝贵。处理器的数量有限:AMD 的 EPYC 3 有 128 个通道,英特尔的至强每芯片最多有 48 条通道;在台式机级 CPU 上,数字分别为 20(锐龙 9)和 16 条(酷睿 i9)。由于 GPU 通常有 16 条通道,因此这限制了能够以全带宽连接到 CPU 的 GPU 的数量。毕竟,他们需要与存储和以太网等其他高带宽外围设备共享链路。就像 RAM 访问一样,由于数据包开销降低,大批量传输更为可取。 -* ** Ethernet** 是连接计算机的最常用方式。虽然它比 PCIe 慢得多,但它的安装非常便宜且有弹性,并且覆盖的距离要长得多。低级服务器的典型带宽为 1 Gbit/s。高端设备(例如云中的 [C5 instances](https://aws.amazon.com/ec2/instance-types/c5/))提供 10 到 100 Gbit/s 的带宽。与以前的所有情况一样,数据传输都有巨大的间接费请注意,我们几乎从来不直接使用原始以太网,而是在物理互连之上执行的协议(例如 UDP 或 TCP/IP)。这进一步增加了开销。像 PCIe 一样,以太网设计用于连接两台设备,例如计算机和交换机。 -* **Switch ** 允许我们以任何一对设备同时进行(通常为满带宽)点对点连接的方式连接多台设备。例如,以太网交换机可能会以较高的横截面带宽连接 40 台服务器。请注意,交换机并不是传统计算机网络所独有的。即使是 PCIe 车道也可以是 [switched](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。例如,将大量 GPU 连接到主机处理器时,就会发生这种情况,就像 [P2 instances](https://aws.amazon.com/ec2/instance-types/p2/) 那样。 -* **nvlink** 在非常高带宽的互连方面是 PCIe 的替代方案。它每个链路提供高达 300 Gbit/s 的数据传输速率。服务器 GPU (Volta V100) 有六个链路,而消费级 GPU (RTX 2080 Ti) 只有一条链路,以降低 100 Gbit/s 的速率运行。我们建议使用 [NCCL](https://github.com/NVIDIA/nccl) 来实现 GPU 之间的高数据传输。 +* **PCIe**是一种专用总线,用于每个通道的高带宽点到点连接(在16通道插槽中的PCIe 4.0上高达32 GB/s)。延迟时间为个位数的微秒(5μs) PCIe链接非常宝贵。处理器的数量有限:AMD的EPYC 3有128个通道,Intel的Xeon每个芯片有48个通道;在桌面级CPU上,数字分别是20(Ryzen 9)和16(Core i9)。由于GPU通常有16个通道,这限制了可以以全带宽连接到CPU的GPU数量。毕竟,它们需要与其他高带宽外围设备(如存储和以太网)共享链路。与内存访问一样,由于减少了数据包开销,因此更适合大容量传输。 +* **以太网**是连接计算机最常用的方式。虽然它比PCIe慢得多,但它的安装成本非常低,而且具有很强的弹性,而且覆盖的距离要长得多。低级服务器的典型带宽为1 GBit/s。高端设备(如云中的[C5实例](https://aws.amazon.com/ec2/instance-types/c5/))提供10到100GBit/s的带宽。与所有以前的情况一样,数据传输有很大的开销。请注意,我们几乎从不直接使用原始以太网,而是使用在物理互连之上执行的协议(例如UDP或TCP/IP)。这进一步增加了开销。与PCIe类似,以太网旨在连接两个设备,例如计算机和交换机。 +* **交换机**允许我们以一种方式连接多个设备,该连接方式下的任何一对设备都可以同时执行(通常是全带宽)点对点连接。例如,以太网交换机可能以高带宽连接40台服务器。请注意,交换机并不是传统计算机网络所独有的。甚至PCIe通道也可以是[可交换的]](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。例如,将大量GPU连接到主机处理器时会出现这种情况,[P2实例](https://aws.amazon.com/ec2/instance-types/p2/)就是这种情况。 +* **NVLink**是PCIe的替代品,适用于非常高带宽的互连。它为每条链路提供高达300 Gbit/s的数据传输速率。服务器GPU(Volta V100)有六个链路。而消费级GPU(RTX 2080 Ti)只有一个链路,运行速度降低到100 Gbit/s。我们建议使用[NCCL](https://github.com/NVIDIA/nccl)来实现GPU之间的高速数据传输。 -## 更多延迟数字 +## 更多延迟 -:numref:`table_latency_numbers` 和 :numref:`table_latency_numbers_tesla` 中的摘要来自 [Eliot Eshelman](https://gist.github.com/eshelman),他将这些数字的更新版本维持为 [GitHub gist](https://gist.github.com/eshelman/343a1c46cb3fba142c1afdcdeec17646)。 +:numref:`table_latency_numbers`和:numref:`table_latency_numbers_tesla`中的小结来自[Eliot Eshelman](https://gist.github.com/eshelman),他们将数字的更新版本保存到[GitHub gist](https://gist.github.com/eshelman/343a1c46cb3fba142c1afdcdeec17646)。 -: 常见延迟数字。 +:常见延迟。 | Action | Time | Notes | | :----------------------------------------- | -----: | :---------------------------------------------- | @@ -176,7 +176,7 @@ GPU 内存受到更高的带宽要求,因为它们的处理元素比 CPU 多 | Send packet CA->Netherlands->CA | 150 ms | | :label:`table_latency_numbers` -: NVIDIA Tesla GPU 的延迟数字。 +:NVIDIA Tesla GPU的延迟. | Action | Time | Notes | | :------------------------------ | -----: | :---------------------------------------- | @@ -189,29 +189,29 @@ GPU 内存受到更高的带宽要求,因为它们的处理元素比 CPU 多 ## 小结 -* 设备有操作开销。因此,重要的是要瞄准少量大量转账,而不是许多小转账。这适用于 RAM、SSD、网络和 GPU。 -* 矢量化是性能的关键。确保你知道加速器的具体能力。例如,一些英特尔至强 CPU 对 INT8 操作特别有用,NVIDIA Volta GPU 在 FP16 矩阵矩阵操作中表现出色,NVIDIA Timon 在 FP16、INT8 和 INT4 操作中出色。 -* 由于数据类型较小而导致的数值溢出可能是训练期间的问题(以及在推理期间较小程度上)。 -* 别名可能会显著降低性能。例如,64 位 CPU 上的内存对齐应该针对 64 位边界进行。在 GPU 上,保持卷积大小对齐是个好主意,例如,与张量核心保持一致。 -* 将算法与硬件相匹配(例如,内存占用量和带宽)。将参数调整到缓存中时,可以实现极大的加速(数量级)。 -* 我们建议您在验证实验结果之前先在纸上勾画出一种新颖算法的性能。数量级或更多的差异是令人担忧的原因。 -* 使用分析器调试性能瓶颈。 -* 培训和推理硬件在价格和性能方面有不同的甜点。 +* 设备有运行开销。因此,重要的是要争取少量的大数据传送,而不是许多小规模的数据传送。这适用于内存、固态驱动器、网络和GPU。 +* 矢量化是性能的关键。确保您了解加速器的特定功能。例如,一些Intel Xeon CPU特别适用于INT8操作,NVIDIA Volta GPU擅长FP16矩阵操作,NVIDIA Turing擅长FP16、INT8和INT4操作。 +* 小数据类型导致的数值溢出在训练过程中可能是个问题(在推理过程中的影响程度较小)。 +* 64位CPU上的内存对齐应该按照64位边界进行。在GPU上,保持卷积大小对齐是一个好主意,例如与张量核对齐。 +* 将算法与硬件相匹配(例如,内存占用和带宽)。将参数装入缓存时,可以实现很大的加速比(数量级)。 +* 我们建议你在验证实验结果之前先在纸上勾勒出新算法的性能。数量级或更大数量级的差异是令人担忧的原因。 +* 使用调试器调试性能瓶颈。 +* 训练和推理硬件在价格和性能方面有不同的优点。 ## 练习 -1. 编写 C 代码来测试访问相对于外部存储器接口对齐或未对齐内存之间的速度是否存在任何差异。提示:小心缓存效果。 -1. 测试按顺序访问内存或按给定步长访问内存之间的速度差异。 -1. 你怎么能测量 CPU 上的缓存大小? -1. 您将如何在多个内存通道之间布局数据以获得最大带宽?如果你有很多小线程你会怎么布局? -1. 企业级硬盘以 10,000 rpm 的速度旋转。HDD 在读取数据之前花最坏情况的绝对最短时间是多少(你可以假设头部几乎瞬间移动)?为什么 2.5 英寸硬盘在商业服务器中变得流行(相对于 3.5 英寸和 5.25 英寸驱动器)? -1. 假设硬盘制造商将存储密度从每平方英寸 1 Tbit 增加到每平方英寸 5 Tbit。你可以在 2.5 英寸硬盘上的戒指上存储多少信息?内部和外部轨道之间有区别吗? -1. 从 8 位到 16 位数据类型将芯片的数量增加大约四倍。为什么?为什么 NVIDIA 会将 INT4 操作添加到他们的图灵 GPU 中? -1. 与向后读取相比,通过内存向前读取的速度要快多少?不同计算机和 CPU 供应商之间该数字是否有所不同?为什么?编写 C 代码然后进行试验。 -1. 你能测量磁盘的缓存大小吗?典型的硬盘是什么?SSD 需要缓存吗? -1. 测量通过以太网发送消息时的数据包开销。查找 UDP 和 TCP/IP 连接之间的区别。 -1. 直接内存访问允许 CPU 以外的设备直接向(从)内存中写入(和读取)。为什么这是个好主意? -1. 看看图灵 T4 GPU 的性能数字。为什么从 FP16 到 INT8 和 INT4 时,性能 “仅” 翻了一番? -1. 旧金山和阿姆斯特丹之间的往返旅行应该最短的时间是多少?提示:你可以假设距离是 10,000 公里。 +1. 编写C代码来测试访问对齐的内存和未对齐的内存之间的速度是否有任何差异。提示:小心缓存效果。 +1. 测试按顺序或给定步幅访问内存的速度差异。 +1. 如何测量CPU上的缓存大小? +1. 如何在多个内存通道中布局数据以获得最大带宽?如果你有许多细线,你会怎么布置呢? +1. 一个企业级硬盘正在以10000转/分的速度旋转。在最坏的情况下,硬盘读取数据所需的最短时间是多少(你可以假设磁头几乎是瞬间移动的)?为什么2.5英寸硬盘在商用服务器上越来越流行(相对于3.5英寸和5.25英寸硬盘)? +1. 假设HDD制造商将存储密度从每平方英寸1 Tbit增加到每平方英寸5 Tbit。在一个2.5英寸的硬盘上,一个环能存储多少信息?内轨和外轨有区别吗? +1. 从8位数据类型到16位数据类型,硅片的数量大约增加了四倍,为什么?为什么NVIDIA会在其图灵GPU中添加INT4运算? +1.在内存中向前读比向后读快多少?该数字在不同的计算机和CPU供应商之间是否有所不同?为什么?编写C代码并进行实验。 +1. 你能测量一下磁盘的缓存大小吗?典型的硬盘是多少?固态驱动器需要缓存吗? +1. 测量通过以太网发送消息时的数据包开销。查找UDP和TCP/IP连接之间的差异。 +1. 直接内存访问允许CPU以外的设备直接向内存写入(和读取)。为什么这是个好主意? +1. 看看Turing T4 GPU的性能数字。为什么从FP16到INT8和INT4的性能只翻倍? +1. 从旧金山到阿姆斯特丹的往返旅行,一个网络包需要多长时间?提示:你可以假设距离为10000公里。 [Discussions](https://discuss.d2l.ai/t/363) From 160c68c85e763d7b906932695c43cd977712324f Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:18:11 +0800 Subject: [PATCH 091/103] chapter_computational-performance/multiple-gpus-concise (#800) --- .../multiple-gpus-concise.md | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/chapter_computational-performance/multiple-gpus-concise.md b/chapter_computational-performance/multiple-gpus-concise.md index 7ed91b0d3..01b84ac02 100644 --- a/chapter_computational-performance/multiple-gpus-concise.md +++ b/chapter_computational-performance/multiple-gpus-concise.md @@ -1,7 +1,7 @@ -# 多个 GPU 的简明实施 +# 多GPU的简洁实现 :label:`sec_multi_gpu_concise` -为每个新模型从头开始实施并行性并不乐趣。此外,优化同步工具以实现高性能也有很大的好处。在下面我们将展示如何使用深度学习框架的高级 API 来完成此操作。数学和算法与 :numref:`sec_multi_gpu` 中的算法相同。毫不奇怪,你需要至少两个 GPU 来运行本节的代码。 +为每一个新模型从零开始实现并行性并不有趣。此外,优化同步工具以获得高性能也有很大的好处。下面我们将展示如何使用深度学习框架的高级API来实现这一点。数学和算法与 :numref:`sec_multi_gpu` 中的相同。毫不奇怪,你至少需要两个GPU来运行本节的代码。 ```{.python .input} from d2l import mxnet as d2l @@ -17,14 +17,14 @@ import torch from torch import nn ``` -## 玩具网 +## 简单网络 -让我们使用一个比 :numref:`sec_multi_gpu` 的 Lenet 稍有意义的网络,该网络仍然足够简单快捷地训练。我们选择了 Resnet-18 变体 :cite:`He.Zhang.Ren.ea.2016`。由于输入图像很小,我们对其进行稍微修改。特别是,与 :numref:`sec_resnet` 的区别在于,我们在开始时使用较小的卷积内核、步幅和填充。此外,我们删除了最大池层。 +让我们使用一个比 :numref:`sec_multi_gpu` 的LeNet稍微有意义的网络,它仍然足够容易和快速地训练。我们选择了ResNet-18 :cite:`He.Zhang.Ren.ea.2016`。因为输入的图像很小,所以我们稍微修改一下。与 :numref:`sec_resnet` 的区别在于,我们在开始时使用了更小的卷积核、步长和填充。此外,我们删除了最大池化层。 ```{.python .input} #@save def resnet18(num_classes): - """A slightly modified ResNet-18 model.""" + """稍加修改的ResNet-18模型。""" def resnet_block(num_channels, num_residuals, first_block=False): blk = nn.Sequential() for i in range(num_residuals): @@ -36,8 +36,7 @@ def resnet18(num_classes): return blk net = nn.Sequential() - # This model uses a smaller convolution kernel, stride, and padding and - # removes the maximum pooling layer + # 该模型使用了更小的卷积核、步长和填充,且删除了最大池化层。 net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1), nn.BatchNorm(), nn.Activation('relu')) net.add(resnet_block(64, 2, first_block=True), @@ -52,7 +51,7 @@ def resnet18(num_classes): #@tab pytorch #@save def resnet18(num_classes, in_channels=1): - """A slightly modified ResNet-18 model.""" + """稍加修改的ResNet-18模型。""" def resnet_block(in_channels, out_channels, num_residuals, first_block=False): blk = [] @@ -64,8 +63,7 @@ def resnet18(num_classes, in_channels=1): blk.append(d2l.Residual(out_channels, out_channels)) return nn.Sequential(*blk) - # This model uses a smaller convolution kernel, stride, and padding and - # removes the maximum pooling layer + # 该模型使用了更小的卷积核、步长和填充,且删除了最大池化层。 net = nn.Sequential( nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(64), @@ -83,31 +81,31 @@ def resnet18(num_classes, in_channels=1): ## 网络初始化 :begin_tab:`mxnet` -`initialize` 函数允许我们在我们选择的设备上初始化参数。有关初始化方法的复习,请参阅 :numref:`sec_numerical_stability`。特别方便的是,它还允许我们同时在 * 多个 * 设备上初始化网络。让我们试试这在实践中是如何运作的。 +`initialize`函数允许我们在所选设备上初始化参数。有关初始化方法的复习内容,请参阅 :numref:`sec_numerical_stability` 。特别方便的是,它还允许我们同时在多个设备上初始化网络。让我们在实践中尝试一下这是如何运作的。 :end_tab: :begin_tab:`pytorch` -我们将在训练循环中初始化网络。有关初始化方法的复习,请参阅 :numref:`sec_numerical_stability`。 +我们将初始化训练部分代码内的网络。有关初始化方法的复习内容,请参见 :numref:`sec_numerical_stability`。 :end_tab: ```{.python .input} net = resnet18(10) -# Get a list of GPUs +# 获取GPU列表 devices = d2l.try_all_gpus() -# Initialize all the parameters of the network +# 初始化网络的所有参数 net.initialize(init=init.Normal(sigma=0.01), ctx=devices) ``` ```{.python .input} #@tab pytorch net = resnet18(10) -# Get a list of GPUs +# 获取GPU列表 devices = d2l.try_all_gpus() -# We will initialize the network inside the training loop +# 我们将在训练代码实现中初始化网络 ``` :begin_tab:`mxnet` -使用 :numref:`sec_multi_gpu` 中引入的 `split_and_load` 函数,我们可以划分一小批数据并将部分内容复制到 `devices` 变量提供的设备列表中。网络实例 * 自动 * 使用适当的 GPU 来计算正向传播的值。在这里,我们生成 4 个观测结果并通过 GPU 将它们拆分。 +使用 :numref:`sec_multi_gpu` 中引入的 `split_and_load` 函数,我们可以切分一小批数据,并将部分数据复制到`devices`变量提供的设备列表中。网络实例自动使用适当的GPU来计算前向传播的值。在这里,我们生成4个观测值,并通过GPU将它们拆分。 :end_tab: ```{.python .input} @@ -117,7 +115,7 @@ net(x_shards[0]), net(x_shards[1]) ``` :begin_tab:`mxnet` -数据通过网络后,相应的参数将在数据通过的设备上初始化 *。这意味着初始化是基于每台设备进行的。由于我们选择了 GPU 0 和 GPU 1 进行初始化,因此网络仅在那里初始化,而不是在 CPU 上初始化。事实上,CPU 上甚至不存在这些参数。我们可以通过打印参数并观察可能出现的任何错误来验证这一点。 +一旦数据通过网络,相应的参数就会在数据通过的设备上初始化。这意味着初始化是在每个设备的基础上进行的。因为我们选择GPU 0和GPU 1进行初始化,所以网络只在那里初始化,而不是在CPU上初始化。事实上,这些参数甚至不存在于CPU上。我们可以通过打印出参数并观察可能出现的任何错误来验证这一点。 :end_tab: ```{.python .input} @@ -131,20 +129,20 @@ weight.data(devices[0])[0], weight.data(devices[1])[0] ``` :begin_tab:`mxnet` -接下来,让我们将代码替换为在多个设备上并行工作的代码来评估准确性。这可以替代 :numref:`sec_lenet` 的 `evaluate_accuracy_gpu` 功能。主要区别在于我们在调用网络之前拆分了一个小批次。其他一切基本上是相同的。 +接下来,让我们用一个在多个设备上并行工作的代码来替换评估准确性的代码。这是 :numref:`sec_lenet` 的`evaluate_accuracy_gpu`功能的替代。主要区别在于,我们在调用网络之前拆分了一个小批量。其他的基本上都是一样的。 :end_tab: ```{.python .input} #@save def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): - """Compute the accuracy for a model on a dataset using multiple GPUs.""" - # Query the list of devices + """使用多个GPU计算数据集上模型的精度。""" + # 查询设备列表 devices = list(net.collect_params().values())[0].list_ctx() - # No. of correct predictions, no. of predictions + # 正确预测的数量,预测的总数量 metric = d2l.Accumulator(2) for features, labels in data_iter: X_shards, y_shards = split_f(features, labels, devices) - # Run in parallel + # 并行运行 pred_shards = [net(X_shard) for X_shard in X_shards] metric.add(sum(float(d2l.accuracy(pred_shard, y_shard)) for pred_shard, y_shard in zip( @@ -152,16 +150,16 @@ def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch): return metric[0] / metric[1] ``` -## 培训 +## 训练 -与之前一样,训练代码需要执行几个基本功能以实现高效的并行性: +如前所述,训练代码需要执行几个基本功能才能实现高效并行: * 需要在所有设备上初始化网络参数。 -* 迭代数据集时,小批次将在所有设备之间划分。 -* 我们在不同设备之间并行计算损失及其梯度。 -* 渐变将进行聚合,并相应地更新参数。 +* 在数据集上迭代时,要将小批量划分到所有设备上。 +* 我们跨设备并行计算损失及其梯度。 +* 聚合梯度,并相应地更新参数。 -最后,我们计算报告网络最终性能的准确性(同样并行)。训练例程与前几章中的实施非常相似,只是我们需要拆分和聚合数据。 +最后我们计算精度(同样是并行地)来报告网络的最终性能。训练代码与前几章中的实现非常相似,只是我们需要拆分和聚合数据。 ```{.python .input} def train(num_gpus, batch_size, lr): @@ -199,7 +197,7 @@ def train(net, num_gpus, batch_size, lr): if type(m) in [nn.Linear, nn.Conv2d]: nn.init.normal_(m.weight, std=0.01) net.apply(init_weights) - # Set the model on multiple GPUs + # 在多个GPU上设置模型 net = nn.DataParallel(net, device_ids=devices) trainer = torch.optim.SGD(net.parameters(), lr) loss = nn.CrossEntropyLoss() @@ -220,7 +218,7 @@ def train(net, num_gpus, batch_size, lr): f'on {str(devices)}') ``` -让我们看看这在实践中是如何运作的。作为热身活动,我们在单个 GPU 上训练网络。 +让我们看看这在实践中是如何运作的。作为热身,我们在单个GPU上训练网络。 ```{.python .input} train(num_gpus=1, batch_size=256, lr=0.1) @@ -231,7 +229,7 @@ train(num_gpus=1, batch_size=256, lr=0.1) train(net, num_gpus=1, batch_size=256, lr=0.1) ``` -接下来我们使用 2 个 GPU 进行培训。与 :numref:`sec_multi_gpu` 中评估的 Lenet 相比,Resnet-18 的模型要复杂得多。这就是并行化显示其优势的地方。计算时间明显大于同步参数的时间。这提高了可扩展性,因为并行化的开销没有那么重要。 +接下来我们使用2个GPU进行训练。与 :numref:`sec_multi_gpu` 中评估的LeNet相比,ResNet-18的模型要复杂得多。这就是并行化显示其优势的地方。计算时间明显大于同步参数的时间。这提高了可伸缩性,因为并行化的开销不太相关。 ```{.python .input} train(num_gpus=2, batch_size=512, lr=0.2) @@ -245,24 +243,24 @@ train(net, num_gpus=2, batch_size=512, lr=0.2) ## 小结 :begin_tab:`mxnet` -* Gluon 通过提供上下文列表为跨多个设备的模型初始化提供了基元。 +* Gluon通过提供上下文列表,为跨多个设备的模型初始化提供原语。 :end_tab: -* 数据将在可以找到数据的设备上自动评估。 -* 在尝试访问每台设备上的参数之前,请注意初始化每台设备上的网络。否则你会遇到错误。 -* 优化算法会自动聚合多个 GPU。 +* 在可以找到数据的设备上自动评估数据。 +* 在尝试访问每台设备上的参数之前,请注意初始化该设备上的网络。否则,你将遇到错误。 +* 优化算法在多个GPU上自动聚合。 ## 练习 :begin_tab:`mxnet` -1. 本节使用 Resnet-18。尝试不同的时代、批量大小和学习率。使用更多 GPU 进行计算。如果您使用 16 个 GPU(例如,在 AWS p2.16xlarge 实例上)尝试此操作会怎样? -1. 有时,不同的设备提供不同的计算能力。我们可以同时使用 GPU 和 CPU。我们应该如何划分工作?值得努力吗?为什么?为什么不? -1. 如果我们丢弃 `npx.waitall()` 会怎么样?你将如何修改训练,使你最多可以重叠两个步骤来实现并行性? +1. 本节使用ResNet-18。尝试不同的迭代周期数、批量大小和学习率。使用更多GPU进行计算。如果使用16个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么情况? +1. 有时,不同的设备提供不同的计算能力。我们可以同时使用GPU和CPU。我们应该如何分工?值得付出努力吗?为什么呢? +1. 如果我们丢掉`npx.waitall()`会发生什么?你将如何修改训练,以使并行操作最多有两个步骤重叠? :end_tab: :begin_tab:`pytorch` -1. 本节使用 Resnet-18。尝试不同的时代、批量大小和学习率。使用更多 GPU 进行计算。如果您使用 16 个 GPU(例如,在 AWS p2.16xlarge 实例上)尝试此操作会怎样? -1. 有时,不同的设备提供不同的计算能力。我们可以同时使用 GPU 和 CPU。我们应该如何划分工作?值得努力吗?为什么?为什么不? +1. 本节使用ResNet-18。尝试不同的迭代周期数、批量大小和学习率。使用更多GPU进行计算。如果使用16个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么情况? +1. 有时,不同的设备提供不同的计算能力。我们可以同时使用GPU和CPU。我们应该如何分工?值得付出努力吗?为什么呢? :end_tab: :begin_tab:`mxnet` From 04349d4cacde9574a6d0639ed13331385d9c7f9e Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:18:25 +0800 Subject: [PATCH 092/103] chapter_computational-performance/parameterserver (#799) --- .../parameterserver.md | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/chapter_computational-performance/parameterserver.md b/chapter_computational-performance/parameterserver.md index b51e0862a..7cfab4c62 100644 --- a/chapter_computational-performance/parameterserver.md +++ b/chapter_computational-performance/parameterserver.md @@ -1,101 +1,101 @@ # 参数服务器 :label:`sec_parameterserver` -当我们从单个 GPU 迁移到多个 GPU,然后迁移到包含多个 GPU 的多台服务器(可能全部分布在多个机架和网络交换机上)时,我们的分布式和并行训练算法需要变得更加复杂。细节很重要,因为不同的互连具有非常不同的带宽(例如,nvLink 可以在适当的设置下在 6 条链路上提供高达 100 Gb/s 的宽度,PCIe 4.0(16 通道)提供 32 Gb/s,甚至高速 100GbE 以太网也只能达到 10 Gb/s)。同时,期望统计建模人员成为网络和系统方面的专家是不合理的。 +当我们从一个GPU迁移到多个GPU,然后再迁移到包含多个GPU的多个服务器时(可能全部分布在多个机架和网络交换机上),我们的分布式和并行训练算法需要变得更加复杂。细节很重要,因为不同的互连具有非常不同的带宽(例如,在适当的设置下,NVLink可以跨6条链路提供高达100 GB/s的带宽,PCIe 4.0(16通道)提供32 GB/s的带宽,而即使是高速100GbE以太网也只能提供10 GB/s)。同时,期望统计建模者成为网络和系统方面的专家是不合理的。 -参数服务器的核心理念是在 :cite:`Smola.Narayanamurthy.2010` 中在分布式潜在变量模型的背景下引入的。随后在 :cite:`Ahmed.Aly.Gonzalez.ea.2012` 中对推拉语义进行了描述,随后在 :cite:`Li.Andersen.Park.ea.2014` 中对系统和开源库进行了描述。在下面我们将激励提高效率所需的组件。 +:cite:`Smola.Narayanamurthy.2010` 在分布式隐变量模型的背景下引入了参数服务器的核心思想。接着在:cite:`Ahmed.Aly.Gonzalez.ea.2012` 中描述了Push和Pull语义,接着在 :cite:`Li.Andersen.Park.ea.2014` 中描述了系统和开源库。在下面,我们将描述提高效率所需的技术。 -## 数据并行培训 +## 数据并行训练 -让我们回顾一下分布式培训的数据并行培训方法。我们将使用它来排除本节中的所有其他内容,因为它在实践中的实施要简单得多。几乎没有任何其他并行策略首选的用例(除了图表上的深度学习之外),因为 GPU 现在有足够的内存。:numref:`fig_parameterserver` 描述了我们在 :numref:`sec_multi_gpu` 中实施的数据并行度的变体。其中的关键方面是,渐变聚合发生在 GPU 0 上,然后再将更新的参数重新广播到所有 GPU。 +让我们回顾一下分布式训练的数据并行训练方法。我们将在本节中使用它来排除所有其他内容,因为它在实践中的实现要简单得多。除了对图深度学习外,实际上没有任何场景推荐任何其他并行策略,因为GPU现在有大量的显存。:numref:`fig_parameterserver` 描述了我们在 :numref:`sec_multi_gpu` 中实现的数据并行的变体。其中关键的一点是,在将更新的参数重新广播到所有GPU之前,在GPU 0上进行梯度聚合。 -![Left: single GPU training. Right: a variant of multi-GPU training: (1) we compute loss and gradient, (2) all gradients are aggregated on one GPU, (3) parameter update happens and the parameters are re-distributed to all GPUs.](../img/ps.svg) +![左图:单GPU训练。右图:多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新分发给所有GPU。](../img/ps.svg) :label:`fig_parameterserver` -回想起来,对 GPU 0 进行聚合的决定似乎相当临时。毕竟,我们也许也可以在 CPU 上聚合起来。事实上,我们甚至可以决定在一个 GPU 上聚合一些参数,另一个 GPU 上的一些参数聚合起来。如果优化算法支持这一点,那么我们没有真正的理由不能。例如,如果我们有四个带有相关渐变 $\mathbf{g}_1, \ldots, \mathbf{g}_4$ 的参数向量,我们可以在一个 GPU 上聚合每个 $\mathbf{g}_i$ ($i = 1, \ldots, 4$) 的渐变。 +回过头来看,在GPU 0上进行聚合似乎是拍脑瓜子的决定。毕竟,我们也可以在CPU上聚合。事实上,我们甚至可以决定在一个GPU上聚合一些参数,在另一个GPU上聚合其他一些参数。如果优化算法支持这一点,我们就没有理由不这样做。例如,如果我们有四个参数向量与相关的梯度$\mathbf{g}_1, \ldots, \mathbf{g}_4$,则我们可以针对每个$\mathbf{g}_i$将梯度聚集在一个GPU上($i = 1, \ldots, 4$)。 -这种推理似乎是武断和轻率的。毕竟,数学始终是一样的。但是,我们正在处理真实的物理硬件,其中不同的总线具有不同的带宽,如 :numref:`sec_hardware` 中所述。考虑一个真正的 4 路 GPU 服务器,如 :numref:`fig_bw_hierarchy` 中所述。如果它的连接特别好,它可能有 100 GbE 网卡。更典型的数字在 1—10 GbE 范围内,有效带宽为 100 MB/s 至 1 Gb/s。由于 CPU 的 PCIe 通道太少,无法直接连接到所有 GPU(例如,消费级英特尔 CPU 有 24 条通道),我们需要 [multiplexer](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。16x Gen3 链路上 CPU 的带宽为 16 Gb/s。这也是每个 GPU 连接到交换机的速度。这意味着设备之间的通信更有效。 +这种推理似乎是武断和轻率的。毕竟,数学自始至终都是一样的。但是,我们处理的是实际的物理硬件,其中不同的总线具有不同的带宽,如 :numref:`sec_hardware` 中所述。考虑一个真正的4路GPU服务器,如 :numref:`fig_bw_hierarchy` 中所述。如果它连接得特别好,它可能有一个100 GbE网卡。更典型的数字在1-10GbE范围内,有效带宽为100 MB/s到1 GB/s。由于CPU的PCIe通道太少,无法直接连接到所有GPU(例如,消费级Intel CPU有24个通道),因此我们需要[multiplexer](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。16x Gen3链路上CPU的带宽为16Gb/s。这也是每个GPU连接到交换机的速度。这意味着设备之间的通信更有效。 -![A 4-way GPU server.](../img/bw-hierarchy.svg) +![一个4路GPU服务器](../img/bw-hierarchy.svg) :label:`fig_bw_hierarchy` -为了这个论点,让我们假设渐变是 160 MB。在这种情况下,将所有剩余 3 个 GPU 的渐变发送到第四个 GPU 需要 30 毫秒(每次传输需要 10 毫秒 = 160 MB /16 Gb/s)。再增加 30 毫秒以将重量向量传送回来,我们总共达到 60 毫秒。如果我们将所有数据发送到 CPU,我们将受到 40 毫秒的罚款,因为四个 GPU 中的 * 每个 * 都需要将数据发送到 CPU,总共产生 80 毫秒。最后假设我们能够将渐变分成 4 个部分,每个 40 MB。现在我们可以将每个部分同时聚合在不同的 GPU * 上,因为 PCIe 交换机在所有链路之间提供了全带宽操作。而不是 30 毫秒,这需要 7.5 毫秒,同步操作总共产生 15 毫秒的时间。简而言之,根据我们同步参数的方式,同一操作可能需要 15 毫秒到 80 毫秒的任何时间。:numref:`fig_ps_distributed` 描述了交换参数的不同策略。 +为了便于讨论,让我们假设梯度是160MB。在这种情况下,将所有剩余3个GPU的梯度发送到第4个GPU需要30毫秒(每次传输需要10毫秒=160 MB/16 GB/s)。再加上30毫秒将权重向量传输回来,我们得到的结果是总共60毫秒。如果我们将所有数据发送到CPU,我们将有40毫秒的惩罚,因为4个GPU每个都需要将数据发送到CPU,总共产生80毫秒。最后,假设我们能够将梯度分为4部分,每个40 MB。现在我们可以在不同的GPU上同时聚合每个部分,因为PCIe交换机在所有链路之间提供全带宽操作。这需要7.5毫秒,而不是30毫秒。因此同步操作总共需要15毫秒。简而言之,根据我们同步参数的不同,同样的操作可能需要15ms到80ms不等的时间。:numref:`fig_ps_distributed`描述了交换参数的不同策略。 -![Parameter synchronization strategies.](../img/ps-distributed.svg) +![参数同步策略](../img/ps-distributed.svg) :label:`fig_ps_distributed` -请注意,在提高绩效方面,我们还有另一种工具可供我们使用。: in a deep network it takes some time to compute all gradients from the top to the bottom. We can begin synchronizing gradients for some parameter groups even while we are still busy computing them for others. See e.g., :cite:`Sergeev.Del-Balso.2018` 有关如何在 [Horovod](https://github.com/horovod/horovod) 中做到这一点的详细信息。 +请注意,我们还可以使用另一个工具来改进性能: 在深度网络中,计算从顶部到底部的所有梯度需要一些时间。我们可以开始同步一些参数的梯度,即使我们还在忙着为其他参数计算梯度。参见 :cite:`Sergeev.Del-Balso.2018`,以了解在[Horovod](https://github.com/horovod/horovod)中如何做到这一点的详细信息。 -## 振铃同步 +## 环同步(Ring Synchronization) -当涉及到现代深度学习硬件的同步时,我们经常会遇到大量定制的网络连接。例如,AWS p3.16xlarge 和 NVIDIA DGX-2 实例共享 :numref:`fig_nvlink` 的连接结构。每个 GPU 都通过 PCIe 链路连接到主机 CPU,该链路最多运行时间为 16 Gb/s。此外,每个 GPU 还有 6 个 nvLink 连接,每个连接都能双向传输 300 Gbit/s。这相当于每个方向每条链路约 18 Gb/s。简而言之,总的 nvLink 带宽明显高于 PCIe 带宽。问题是如何最有效地使用它。 +当谈到现代深度学习硬件上的同步时,我们经常会遇到大量定制的网络连接。例如,AWS p3.16xlarge和NVIDIA DGX-2实例都使用到了 :numref:`fig_nvlink` 的连接结构。每个GPU通过PCIe链路连接到主机CPU,该链路最多只能以16 GB/s的速度运行。此外,每个GPU还具有6个NVLink连接,每个连接都能够双向传输300Gbit/s。这相当于每个方向每个链路约18 GB/s。简言之,聚合NVLink带宽明显高于PCIe带宽。问题是如何最有效地使用它。 -![NVLink connectivity on 8 V100 GPU servers (image courtesy of NVIDIA).](../img/nvlink.svg) +![在8台V100 GPU服务器上连接NVLink(图片由英伟达提供)](../img/nvlink.svg) :label:`fig_nvlink` -事实证明,最佳同步策略是将网络分解为两个环,然后使用它们直接同步数据 :cite:`Wang.Li.Liberty.ea.2018`。:numref:`fig_nvlink_twoloop` 说明,可以将网络分解为带双 NVLink 带宽的一个环(1-2-3-4-5-6-7-8-1),常规带宽。在这种情况下,设计高效的同步协议非常重要。 +结果表明,最优的同步策略是将网络分解成两个环,并用它们直接同步数据 :cite:`Wang.Li.Liberty.ea.2018` 。 :numref:`fig_nvlink_twoloop` 说明了网络可以分解为一个具有双NVLink带宽的环(1-2-3-4-5-6-7-8-1)和一个具有常规带宽的环(1-4-6-3-5-8-2-7-1)。在这种情况下,设计一个高效的同步协议是非常重要的。 -![Decomposition of the NVLink network into two rings.](../img/nvlink-twoloop.svg) +![将NVLink网络分解为两个环。](../img/nvlink-twoloop.svg) :label:`fig_nvlink_twoloop` -考虑下面的思维实验:给定 $n$ 个计算节点(或 GPU)的环,我们可以将梯度从第一个节点发送到第二个节点。在那里,它被添加到局部渐变中并发送到第三个节点,依此类推。$n-1$ 步之后,可以在上次访问的节点中找到聚合渐变。也就是说,聚合渐变的时间随节点数量线性增长。但是,如果我们这样做,算法就非常低效。毕竟,在任何时候都只有一个节点通信。如果我们将梯度分解为 $n$ 块并开始从节点 $i$ 开始同步块 $i$,该怎么办?由于每个区块的大小为 $1/n$,所以现在的总时间是 $(n-1)/n \approx 1$。换句话说,随着我们增加戒指尺寸,聚合渐变所花费的时间 * 不会增加 *。这是一个非常惊人的结果。:numref:`fig_ringsync` 说明了 $n=4$ 节点上的步骤顺序。 +考虑下面的想法:给定一个由$n$个计算节点(或GPU)组成的环,我们可以将梯度从第一个节点发送到第二个节点。在那里,它被添加到局部梯度并发送到第三个节点,依此类推。在$n-1$步之后,可以在最后访问的节点中找到聚合梯度。也就是说,聚合梯度的时间随节点数线性增长。但如果我们这样做,算法是相当低效的。毕竟,任何时候只有一个节点在通信。如果我们将梯度分为$n$个块,并从节点$i$开始同步块$i$,会怎么样?因为每个块的大小是$1/n$,所以总时间现在是$(n-1)/n \approx 1$。换句话说,当我们增大环的大小时,聚合梯度所花费的时间不会增加。这是一个相当惊人的结果。 :numref:`fig_ringsync` 说明了$n=4$个节点上的步骤顺序。 -![Ring synchronization across 4 nodes. Each node starts transmitting parts of gradients to its left neighbor until the assembled gradient can be found in its right neighbor.](../img/ringsync.svg) +![跨4个节点的环同步。每个节点开始向其左邻居发送部分梯度,直到在其右邻居中找到聚合的梯度。](../img/ringsync.svg) :label:`fig_ringsync` -如果我们使用同样的示例在 8 个 V100 GPU 之间同步 160 MB,我们的目标是大约 $2 \cdot 160 \mathrm{MB} / (3 \cdot 18 \mathrm{GB/s}) \approx 6 \mathrm{ms}$。尽管我们现在正在使用 8 个 GPU,但这比使用 PCIe 总线更好。请注意,实际上这些数字有点差,因为深度学习框架通常无法将通信组合为大规模突发传输。 +如果我们使用相同的例子,跨8个V100GPU同步160MB,我们得到的结果大约是$2 \cdot 160 \mathrm{MB} / (3 \cdot 18 \mathrm{GB/s}) \approx 6 \mathrm{ms}$。这比使用PCIe总线要好,尽管我们现在使用的是8 GPU。请注意,在实践中,这些数字要更糟一些,因为深度学习框架通常无法将通信组合成大的突发传输。 -请注意,有一种常见的误解是,环形同步与其他同步算法有根本不同。唯一的区别是,与简单的树相比,同步路径更加精细。 +请注意,有一种常见的误解,认为环同步与其他同步算法有根本的不同。唯一的区别是,与简单的树相比,同步路径稍微更精细一些。 -## 多机培训 +## 多机训练 -在多台机器上进行分布式培训增加了进一步的挑战:我们需要与仅通过相对较低带宽的结构连接的服务器进行通信,在某些情况下,这种结构可能会慢一个数量级以上。跨设备同步非常棘手。毕竟,运行训练代码的不同机器将具有微妙的不同速度。因此,如果我们想使用同步分布式优化,我们需要 * 同步 * 它们。:numref:`fig_ps_multimachine` 说明了分布式并行训练的发生方式。 +在多台机器上进行分布式训练还增加了一个挑战:我们需要与服务器通信,这些服务器只通过相对较低的带宽结构连接,在某些情况下,这种结构的速度可能会慢一个数量级。跨设备的同步很棘手。毕竟,运行训练代码的不同机器的速度会有细微的差别。因此,如果我们想使用同步分布式优化,我们需要同步这些机器。:numref:`fig_ps_multimachine`说明了分布式并行训练是如何发生的。 -1. 在每台计算机上读取一批(不同)数据,分割到多个 GPU 之间,然后传输到 GPU 内存中。每个 GPU 批次上都会分别计算预测和梯度。 -2. 来自所有本地 GPU 的渐变聚合在一个 GPU 上(或者其中的一部分聚合在不同的 GPU 上)。 -3. 渐变将发送到 CPU。 -4. CPU 将渐变发送到聚合所有渐变的中央参数服务器。 -5. 然后使用聚合渐变来更新参数,更新后的参数将广播回各个 CPU。 +1. 在每台机器上读取一批(不同的)数据,在多个GPU之间划分并传输到GPU显存。在每个GPU上分别计算预测和梯度。 +2. 来自所有本地GPU的梯度聚合在一个GPU上(或者它的一部分聚合在不同的GPU上)。 +3. 梯度被发送到CPU。 +4. CPU将梯度发送到中央参数服务器,该服务器聚合所有梯度。 +5. 然后使用聚合梯度来更新参数,并将更新后的参数广播回各个CPU。 6. 信息被发送到一个(或多个)GPU。 -7. 更新后的参数分布在所有 GPU 中。 +7. 更新的参数分布在所有GPU上。 -![Multi-machine multi-GPU distributed parallel training.](../img/ps-multimachine.svg) +![多机多GPU分布式并行训练。](../img/ps-multimachine.svg) :label:`fig_ps_multimachine` -这些操作中的每一项似乎都相当简单。而且,事实上,它们可以在单台机器内 * 高效地执行。但是,一旦我们看到多台机器,我们可以看到中央参数服务器成为瓶颈。毕竟,每台服务器的带宽是有限的,因此对于 $m$ 个工作人员,将所有梯度发送到服务器所需的时间是 $\mathcal{O}(m)$。我们可以通过将服务器数量增加到 $n$ 来突破这一障碍。此时,每台服务器只需存储 $\mathcal{O}(1/n)$ 参数,因此更新和优化的总时间将变为 $\mathcal{O}(m/n)$。无论我们正在处理多少工作人员,匹配这两个数字都可以持续扩展。实际上,我们使用 * 相同 * 机器作为工作人员和服务器。:numref:`fig_ps_multips` 说明了设计(有关详细信息,另请参阅 :cite:`Li.Andersen.Park.ea.2014`)。特别是,确保多台机器在没有不合理的延迟的情况下工作是非常重要的。我们省略了关于障碍的详细信息,只会在下面简要介绍同步和异步更新。 +这些操作似乎都相当简单。事实上,它们可以在一台机器内高效地执行。但是,一旦我们查看多台机器,我们就会发现中央参数服务器成为瓶颈。毕竟,每个服务器的带宽是有限的,因此对于$m$个工作节点来说,将所有梯度发送到服务器所需的时间是$\mathcal{O}(m)$。我们可以通过将服务器数量增加到$n$来突破这一障碍。此时,每个服务器只需要存储$\mathcal{O}(1/n)$个参数,因此更新和优化的总时间变为$\mathcal{O}(m/n)$。匹配这两个数字会产生恒定的伸缩性,而不管我们要处理多少工人。实际上,我们使用相同的机器作为工作节点和服务器。 :numref:`fig_ps_multips` 说明了设计(详见 :cite:`Li.Andersen.Park.ea.2014` )。特别是,确保多台机器在没有不合理延迟的情况下工作是非常重要的。我们省略了关于阻塞的细节,下面只简单介绍一下同步和异步更新。 -![Top: a single parameter server is a bottleneck since its bandwidth is finite. Bottom: multiple parameter servers store parts of the parameters with aggregate bandwidth.](../img/ps-multips.svg) +![上图:单参数服务器是一个瓶颈,因为它的带宽是有限的。下图:多参数服务器使用聚合带宽存储部分参数。](../img/ps-multips.svg) :label:`fig_ps_multips` -## 钥匙-价值存储 +## 键值存储 -在实践中执行分布式多 GPU 培训所需的步骤并非微不足道。这就是为什么使用通用抽象是值得的,即具有重新定义更新语义的 * 键值存储 * 的抽象。 +在实践中实现分布式多GPU培训所需的步骤绝非易事。这就是为什么使用一个公共抽象是值得的,即具有重新定义的更新语义的*键-值存储*的抽象。 -在许多工作人员和许多 GPU 中,梯度 $i$ 的计算可以定义为 +在许多工作节点和许多GPU中,梯度$i$的计算可以定义为 $$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{g}_{ijk},$$ -其中 $\mathbf{g}_{ijk}$ 是在工人 $k$ 的 GPU $j$ 上分割的梯度 $i$ 的一部分。此操作的关键方面是它是 * 交换减少 *,也就是说,它将许多向量变成一个矢量,应用操作的顺序无关紧要。这对我们来说非常棒,因为我们不(需要)对何时接收哪个梯度进行精细的控制。此外,请注意,此操作在不同的 $i$ 之间是独立的。 +其中$\mathbf{g}_{ijk}$是在工作节点$k$的GPU $j$上分划分的梯度$i$的一部分。这个运算的关键之处在于它是一个*交换归约*(commutative reduction),也就是说,它把许多向量变成一个向量,而运算的顺序并不重要。这对于我们的目的来说是非常好的,因为我们不需要对何时接收哪个梯度进行细粒度的控制。此外,请注意,此操作在不同的$i$之间是独立的。 -这使我们可以定义以下两种操作:* push *(累积渐变)和 *pull*(检索聚合渐变)。由于我们有许多不同的渐变集(毕竟,我们有很多图层),因此我们需要用键 $i$ 对渐变进行索引。与密钥价值存储的这种相似之处,例如 Dynamo :cite:`DeCandia.Hastorun.Jampani.ea.2007` 中引入的那种类似之处并非巧合。它们也满足许多类似的特征,特别是在将参数分配到多台服务器时。 +这允许我们定义以下两个操作:*push*(累积梯度)和*pull*(检索聚合梯度)。因为我们有很多不同的梯度(毕竟,我们有很多层),所以我们需要用一个键$i$索引梯度。这种与键-值存储( 如dynamo:cite:`DeCandia.Hastorun.Jampani.ea.2007` 中引入的键-值存储)的相似性并非巧合。它们也满足许多类似的特性,特别是在多个服务器之间分配参数时。 -键值存储的推拉操作描述如下: +键值存储的push-pull操作描述如下: -* **push(键、值)** 将工作线程中的特定渐变(值)发送到公共存储器。在那里,这个值是汇总的,例如,通过对其进行汇总。 -* **pull(键、value)** 从公共存储中检索聚合值,例如,在合并来自所有工作人员的梯度之后。 +* **push(key,value)**将特定的梯度(value)从工作节点发送到公共存储。在那里,通过将其相加来聚合值。 +* **pull(key,value)**从公共存储中检索聚合值,例如,在组合来自所有工作节点的梯度之后。 -通过隐藏简单的推拉操作背后的所有同步复杂性,我们可以解决希望能够简单地表达优化的统计建模师和需要处理分布式同步固有的复杂性的系统工程师的担忧。 +通过将同步的所有复杂性隐藏在一个简单的push和pull操作背后,我们可以将统计建模人员(他们希望能够用简单的术语表达优化)和系统工程师(他们需要处理分布式同步中固有的复杂性)的关注点解耦。 ## 小结 -* 同步需要高度适应服务器内的特定网络基础架构和连接。这可能会对同步所需的时间产生重大影响。 -* 对于 p3 和 DGX-2 服务器来说,环形同步可能是最佳选择。对于其他人来说可能不太多。 -* 当添加多个参数服务器以增加带宽时,分层同步策略效果很好。 +* 同步需要高度适应特定的网络基础设施和服务器内的连接。这会对同步所需的时间产生重大影响。 +* 对于p3和DGX-2服务器,环同步是最佳的。 +* 当添加多个参数服务器以增加带宽时,分层同步策略可以很好地工作。 ## 练习 -1. 你能进一步增加振铃同步吗?提示:你可以双向发送消息。 -1. 是否可以允许异步通信(在计算仍在进行时)?它如何影响性能? -1. 如果我们在长时间运行的计算中丢失了服务器怎么办?我们如何设计一个 * 容错 * 机制来避免完全重新启动计算? +1. 你能进一步提高环同步的性能吗?提示:你可以双向发送消息。 +1. 是否可以允许异步通信(而计算仍在进行)?它如何影响性能? +1. 如果我们在长时间运行的计算过程中丢失了一台服务器,该怎么办?我们如何设计一种容错机制来避免完全重新启动计算? [Discussions](https://discuss.d2l.ai/t/366) From 9758a9130402036e59cdf29e10587c0d566d3f1a Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:19:25 +0800 Subject: [PATCH 093/103] chapter_computational-performance/multiple-gpus (#797) --- .../multiple-gpus.md | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md index ca17833c7..37c9a9521 100644 --- a/chapter_computational-performance/multiple-gpus.md +++ b/chapter_computational-performance/multiple-gpus.md @@ -1,46 +1,46 @@ -# 在多个 GPU 上进行培训 +# 多GPU训练 :label:`sec_multi_gpu` -到目前为止,我们讨论了如何在 CPU 和 GPU 上高效训练模型。我们甚至展示了深度学习框架如何允许在 :numref:`sec_auto_para` 中自动并行化它们之间的计算和通信。我们还在 :numref:`sec_use_gpu` 中展示了如何使用 `nvidia-smi` 命令列出计算机上的所有可用 GPU。我们没有 * 讨论的是如何真正并行化深度学习训练。相反,我们顺便暗示,人们会以某种方式将数据拆分到多个设备之间并使其工作。本节将填充详细信息,并展示如何从头开始并行训练网络。有关如何利用高级 API 中功能的详细信息降级为 :numref:`sec_multi_gpu_concise`。我们假设您熟悉微型批次随机梯度下降算法,例如 :numref:`sec_minibatch_sgd` 中描述的算法。 +到目前为止,我们讨论了如何在CPU和GPU上高效地训练模型。我们甚至在 :numref:`sec_auto_para` 中展示了深度学习框架如何使它们之间的计算和通信自动并行化。我们还在 :numref:`sec_use_gpu` 中展示了如何使用 `nvidia-smi` 命令列出计算机上所有可用的GPU。我们没有讨论的是如何真正实现深度学习训练的并行化。我们暗示了一种方法,即以某种方式将数据分割到多个设备上,并使其正常工作。本节将详细介绍如何从零开始并行训练网络。关于如何利用高级API中的详情请参阅 :numref:`sec_multi_gpu_concise` 。我们假设你熟悉小批量随机梯度下降算法,如 :numref:`sec_minibatch_sgd` 中描述的算法。 -## 分解问题 +## 拆分问题 -让我们从一个简单的计算机视觉问题和稍微陈旧的网络开始,例如,最后有多层复杂、集中,还有几个完全连接的层。也就是说,让我们从一个看起来与 Lenet :cite:`LeCun.Bottou.Bengio.ea.1998` 或 AlexNet :cite:`Krizhevsky.Sutskever.Hinton.2012` 非常相似的网络开始。假定多个 GPU(如果是台式机服务器,则为 2 个 GPU,AWS g4dn.12xlarge 实例上 4 个,p3.16xlarge 上的 8 个,p2.16xlarge 上 16 个),我们希望以实现良好的加速的方式对训练进行分区,同时从简单且可重复的设计选择中受益。毕竟,多个 GPU 可以同时增加 * 内存 * 和 * 计算 * 能力。简而言之,鉴于我们想要分类的一小批训练数据,我们有以下选择。 +让我们从一个简单的计算机视觉问题和一个稍微旧点的网络开始,例如,有多个卷积层和池化层,最后可能有几个全连接的层。也就是说,让我们从一个看起来非常类似于 LeNet :cite:`LeCun.Bottou.Bengio.ea.1998`或AlexNet :cite:`Krizhevsky.Sutskever.Hinton.2012`的网络开始。假设有多个GPU(如果是桌面服务器则为2个,AWS g4dn.12xlarge实例上为4个,p3.16xlarge上为8个,p2.16xlarge上为16个),我们希望以一种方式对训练进行拆分,以实现良好的加速比,同时受益于简单且可重复的设计选择。毕竟,多个GPU同时增加了内存和计算能力。简而言之,给出一小批量我们想要分类的训练数据,我们有以下选择。 -首先,我们可以在多个 GPU 之间对网络进行分区。也就是说,每个 GPU 都会将流入特定层的数据作为输入,跨多个后续图层处理数据,然后将数据发送到下一个 GPU。与单个 GPU 可以处理的数据相比,这使我们能够使用更大的网络处理数据。此外,每个 GPU 的内存占用可以很好地控制(占总网络占用空间的一小部分)。 +首先,我们可以在多个GPU之间拆分网络。也就是说,每个GPU将流入特定层的数据作为输入,跨多个后续层处理数据,然后将数据发送到下一个GPU。与单个GPU所能处理的数据相比,这使我们可以用更大的网络处理数据。此外,每个GPU的显存占用可以得到很好的控制(它只是整个网络占用的一小部分)。 -但是,层之间的接口(以及 GPU)需要严格的同步。这可能很棘手,特别是如果计算工作负载在图层之间没有正确匹配的情况下。对于大量 GPU 来说,这个问题更加严重。图层之间的接口还需要大量的数据传输,例如激活和梯度。这可能会超过 GPU 总线的带宽。此外,计算密集型但连续的操作对于分区来说不是微不足道的。例如,请参阅 :cite:`Mirhoseini.Pham.Le.ea.2017` 以了解这方面的最佳努力。这仍然是一个困难的问题,目前尚不清楚是否有可能在非平凡的问题上实现良好的(线性)扩展。除非有出色的框架或操作系统支持将多个 GPU 链接在一起,否则我们不推荐使用。 +然而,层(以及GPU)之间的接口需要紧密同步。这可能很棘手,特别是在计算工作负载在层之间没有正确匹配的时候。对于大量的GPU来说,这个问题更加严重。层之间的接口还需要大量的数据传输,例如激活值和梯度。这可能会超出GPU总线的带宽。此外,计算密集型的操作顺序对于拆分来说是非常重要的。有关这方面的最佳努力,请参见:cite:`Mirhoseini.Pham.Le.ea.2017`。这仍然是一个困难的问题,目前还不清楚是否有可能在不平凡的问题上实现良好的(线性)缩放。除非有优秀的框架或操作系统支持将多个GPU连接在一起,否则我们不建议使用它。 -其次,我们可以逐层分割工作。例如,我们可以将问题分成 4 个 GPU,而不是在单个 GPU 上计算 64 个通道,每个 GPU 都会生成 16 个通道的数据。同样,对于完全连接的层,我们可以拆分输出单元的数量。:numref:`fig_alexnet_original`(取自 :cite:`Krizhevsky.Sutskever.Hinton.2012`)说明了这种设计,该策略用于处理内存占用非常小的 GPU(当时为 2 GB)。如果通道(或单位)数量不太少,这样就可以在计算方面进行良好的缩放。此外,由于可用内存可以线性扩展,多个 GPU 可以处理越来越大的网络。 +第二,我们可以拆分工作。例如,我们可以将问题分散到4个GPU,每个GPU生成16个通道的数据,而不是在单个GPU上计算64个通道。同样,对于全连接的层,我们可以拆分输出单元的数量。 :numref:`fig_alexnet_original` (来自 :cite:`Krizhevsky.Sutskever.Hinton.2012` )说明了这种设计,这种策略用于显存非常小(当时为2GB)的GPU。这允许在计算方面进行良好的缩放,前提是通道(或单元)的数量不太小。此外,由于可用显存呈线性扩展,多个GPU可以处理越来越大的网络。 -![Model parallelism in the original AlexNet design due to limited GPU memory.](../img/alexnet-original.svg) +![由于GPU显存有限,原有AlexNet设计中存在模型并行性问题。](../img/alexnet-original.svg) :label:`fig_alexnet_original` -但是,我们需要 * 非常大 * 数的同步或障碍操作,因为每个层都取决于所有其他层的结果。此外,需要传输的数据量可能甚至超过在 GPU 之间分布层时的数据量。因此,由于带宽成本和复杂性,我们不推荐使用此方法。 +但是,我们需要非常多的同步或阻塞操作,因为每一层都依赖于所有其他层的结果。此外,此外,需要传输的数据量可能比跨GPU拆分层时还要大。因此,由于其带宽成本和复杂性,我们不推荐这种方法。 -最后,我们可以在多个 GPU 之间对数据进行分区。这样,所有 GPU 都执行相同类型的工作,尽管观察结果不同。在每个小批量训练数据之后,梯度会在 GPU 中进行汇总。这是最简单的方法,它可以在任何情况下应用。我们只需要在每个小批次之后进行同步。也就是说,在计算其他梯度参数的同时,开始交换梯度参数是非常可取的。此外,更多的 GPU 会导致更大的小批量尺寸,从而提高训练效率。但是,添加更多 GPU 并不允许我们训练更大的模型。 +最后,我们可以跨多个GPU对数据进行拆分。通过这种方式,所有GPU执行相同类型的工作,尽管观察结果不同。在每个小批量的训练数据之后,梯度在GPU上聚合。这是最简单的方法,可以应用于任何情况。我们只需要在每个小批处理之后进行同步。也就是说,当其他梯度参数仍在计算时,开始交换梯度参数是非常可取的。此外,GPU数量越多,小批量越大,从而提高了训练效率。但是,添加更多GPU并不能让我们训练更大的模型。 -![Parallelization on multiple GPUs. From left to right: original problem, network partitioning, layer-wise partitioning, data parallelism.](../img/splitting.svg) +![在多个gpu上并行化。从左到右:原始问题、网络并行、分层并行、数据并行。](../img/splitting.svg) :label:`fig_splitting` -:numref:`fig_splitting` 中描述了多个 GPU 上的不同并行化方式的比较。总的来说,如果我们能够访问具有足够大内存的 GPU,数据并行性是最方便的继续方式。另请参阅 :cite:`Li.Andersen.Park.ea.2014` 以了解分布式培训的分区的详细说明。在深度学习的早期,GPU 内存曾经是一个问题。到目前为止,除了最不寻常的情况外,所有这个问题都已解决。我们将重点放在以下内容中的数据并行性。 +:numref:`fig_splitting`中描述了多个GPU上不同并行方式的比较。总的来说,数据并行是最方便的方法,只要我们能访问有足够大显存的GPU。有关分布式训练并行的详细描述,请参见 :cite:`Li.Andersen.Park.ea.2014` 。GPU显存在深度学习的早期曾经是一个问题。到目前为止,除了最不寻常的情况外,这个问题已经解决了。下面我们将重点讨论数据并行性。 -## 数据并行 +## 数据并行性 -假设计算机上有 $k$ GPU。鉴于要训练的模型,每个 GPU 将独立维护一套完整的模型参数,尽管整个 GPU 的参数值是相同且同步的。例如,:numref:`fig_data_parallel` 说明了 $k=2$ 时使用数据并行性进行培训。 +假设机器上有$k$个GPU。给定要训练的模型,每个GPU将独立地维护一组完整的模型参数,尽管GPU上的参数值是相同且同步的。例如, :numref:`fig_data_parallel` 演示了在$k=2$时使用数据并行的训练。 -![Calculation of minibatch stochastic gradient descent using data parallelism on two GPUs.](../img/data-parallel.svg) +![利用两个GPU上的数据并行计算小批量随机梯度下降。](../img/data-parallel.svg) :label:`fig_data_parallel` -一般来说,培训的进展情况如下: +一般来说,训练过程如下: -* 在训练的任何迭代中,只要有一个随机的微型批量,我们将批次中的示例拆分为 $k$ 部分,然后在 GPU 中均匀分布。 -* 每个 GPU 都根据分配给模型的小批次子集来计算模型参数的损耗和梯度。 -* 汇总 $k$ GPU 中每个 GPU 的局部梯度,以获得当前的微型批次随机梯度。 -* 聚合渐变将重新分配到每个 GPU。 -* 每个 GPU 都使用这个微型批次随机梯度来更新它维护的完整模型参数集。 +* 在训练的任何迭代中,给定一个随机的小批量,我们将该小批量中的样本分成$k$个部分,并将它们均匀地分在多个GPU上。 +* 每个GPU根据分配给它的小批量子集计算模型参数的损失和梯度。 +* 将$k$个GPU中每个GPU的局部梯度聚合以获得当前的小批量随机梯度。 +* 聚合梯度被重新分配到每个GPU。 +* 每个GPU使用这个小批量随机梯度来更新它维护的完整的模型参数集。 -请注意,在实践中,我们在 $k$ GPU 上进行培训时,我们将小批量 $k$ 倍增加 *,这样每个 GPU 的工作量就像我们只在单个 GPU 上进行培训一样。在 16-GPU 服务器上,这可能会大大增加小批量的大小,我们可能必须相应地提高学习率。另请注意,:numref:`sec_batch_norm` 中的批量标准化需要进行调整,例如,通过每个 GPU 保持单独的批量标准化系数。在下面的内容中,我们将使用玩具网络来说明多 GPU 训练。 +请注意,在实践中,当在$k$个GPU上训练时,我们将小批量大小增加$k$倍,这样每个GPU都有相同的工作量,就像我们只在单个GPU上训练一样。在16-GPU服务器上,这可以大大增加小批量大小,我们可能需要相应地提高学习速率。还请注意, :numref:`sec_batch_norm` 中的批量归一化需要调整,例如,为每个GPU保留单独的批量归一化参数。下面我们将使用一个简单网络来演示多GPU训练。 ```{.python .input} %matplotlib inline @@ -58,12 +58,12 @@ from torch import nn from torch.nn import functional as F ``` -## 玩具网 +## 简单网络 -我们使用 :numref:`sec_lenet` 中引入的 Lenet(稍作修改)。我们从头开始定义它,以详细说明参数交换和同步。 +我们使用 :numref:`sec_lenet` 中介绍的LeNet(稍加修改)。我们从零开始定义它,从而详细说明参数交换和同步。 ```{.python .input} -# Initialize model parameters +# 初始化模型参数 scale = 0.01 W1 = np.random.normal(scale=scale, size=(20, 1, 3, 3)) b1 = np.zeros(20) @@ -75,7 +75,7 @@ W4 = np.random.normal(scale=scale, size=(128, 10)) b4 = np.zeros(10) params = [W1, b1, W2, b2, W3, b3, W4, b4] -# Define the model +# 定义模型 def lenet(X, params): h1_conv = npx.convolution(data=X, weight=params[0], bias=params[1], kernel=(3, 3), num_filter=20) @@ -93,13 +93,13 @@ def lenet(X, params): y_hat = np.dot(h3, params[6]) + params[7] return y_hat -# Cross-entropy loss function +# 交叉熵损失函数 loss = gluon.loss.SoftmaxCrossEntropyLoss() ``` ```{.python .input} #@tab pytorch -# Initialize model parameters +# 初始化模型参数 scale = 0.01 W1 = torch.randn(size=(20, 1, 3, 3)) * scale b1 = torch.zeros(20) @@ -111,7 +111,7 @@ W4 = torch.randn(size=(128, 10)) * scale b4 = torch.zeros(10) params = [W1, b1, W2, b2, W3, b3, W4, b4] -# Define the model +# 定义模型 def lenet(X, params): h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1]) h1_activation = F.relu(h1_conv) @@ -125,13 +125,13 @@ def lenet(X, params): y_hat = torch.mm(h3, params[6]) + params[7] return y_hat -# Cross-entropy loss function +# 交叉熵损失函数 loss = nn.CrossEntropyLoss(reduction='none') ``` ## 数据同步 -为了高效的多 GPU 培训,我们需要两种基本操作。首先,我们需要有能力将参数列表分发到多个设备并附加渐变(`get_params`)。如果没有参数,就不可能在 GPU 上评估网络。其次,我们需要跨多个设备对参数进行求和的能力,即我们需要 `allreduce` 函数。 +对于高效的多GPU训练,我们需要两个基本操作。首先,我们需要有能力将参数列表分发给多个设备并附加梯度(`get_params`)。如果没有参数,就不可能在GPU上评估网络。第二,我们需要跨多个设备对参数求和的能力,也就是说,我们需要一个`allreduce`函数。 ```{.python .input} def get_params(params, device): @@ -150,7 +150,7 @@ def get_params(params, device): return new_params ``` -让我们通过将模型参数复制到一个 GPU 来尝试一下。 +让我们通过将模型参数复制到一个GPU来尝试一下。 ```{.python .input} #@tab all @@ -159,7 +159,7 @@ print('b1 weight:', new_params[1]) print('b1 grad:', new_params[1].grad) ``` -由于我们还没有执行任何计算,所以有关偏差参数的梯度仍然为零。现在让我们假设我们有一个向量分布在多个 GPU 上。以下 `allreduce` 函数将所有向量加起来,并将结果广播回所有 GPU。请注意,为了实现这一点,我们需要将数据复制到累计结果的设备。 +由于我们还没有进行任何计算,偏置参数的梯度仍然为零。现在假设有一个向量分布在多个GPU上。下面的`allreduce`函数将所有向量相加,并将结果广播回所有GPU。请注意,要使其工作,我们需要将数据复制到累积结果的设备。 ```{.python .input} def allreduce(data): @@ -178,7 +178,7 @@ def allreduce(data): data[i] = data[0].to(data[i].device) ``` -让我们通过在不同设备上创建具有不同值的矢量来测试这一点,然后聚合它们 +让我们通过在不同设备上创建具有不同值的向量并聚合它们来测试这一点。 ```{.python .input} data = [np.ones((1, 2), ctx=d2l.try_gpu(i)) * (i + 1) for i in range(2)] @@ -197,7 +197,7 @@ print('after allreduce:\n', data[0], '\n', data[1]) ## 分发数据 -我们需要一个简单的实用程序函数才能在多个 GPU 之间均匀分配微型批次。例如,在两个 GPU 上,我们希望将一半的数据复制到任何一个 GPU 中。由于它更方便、更简洁,我们使用深度学习框架中的内置函数在 $4 \times 5$ 矩阵上进行试用。 +我们需要一个简单的实用函数来将一个小批量均匀地分布在多个GPU上。例如,在有两个GPU的时候,我们希望将一半的数据复制到其中一个GPU。因为深度学习框架中的内置函数更方便、更简洁,所以我们使用它在$4 \times 5$矩阵上进行了尝试。 ```{.python .input} data = np.arange(20).reshape(4, 5) @@ -218,12 +218,12 @@ print('load into', devices) print('output:', split) ``` -为了以后重复使用,我们定义了一个分割数据和标签的 `split_batch` 函数。 +为了以后的复用,我们定义了一个`split_batch`函数,该函数同时拆分数据和标签。 ```{.python .input} #@save def split_batch(X, y, devices): - """Split `X` and `y` into multiple devices.""" + """将`X`和`y`拆分到多个设备上""" assert X.shape[0] == y.shape[0] return (gluon.utils.split_and_load(X, devices), gluon.utils.split_and_load(y, devices)) @@ -233,7 +233,7 @@ def split_batch(X, y, devices): #@tab pytorch #@save def split_batch(X, y, devices): - """Split `X` and `y` into multiple devices.""" + """将`X`和`y`拆分到多个设备上""" assert X.shape[0] == y.shape[0] return (nn.parallel.scatter(X, devices), nn.parallel.scatter(y, devices)) @@ -241,51 +241,51 @@ def split_batch(X, y, devices): ## 训练 -现在我们可以在单个小批量上实施多 GPU 训练。其实施主要基于本节中描述的数据并行方法。我们将使用刚才讨论的辅助函数 `allreduce` 和 `split_and_load`,在多个 GPU 之间同步数据。请注意,我们不需要编写任何特定的代码即可实现并行性。由于计算图在微型批次内的设备之间没有任何依赖关系,因此它是并行 * 自动执行的。 +现在我们可以在一个小批量上实现多GPU训练。它的实现主要基于本节描述的数据并行方法。我们将使用刚才讨论的辅助函数`allreduce`和`split_and_load`在多个GPU之间同步数据。注意,我们不需要编写任何特定的代码来实现并行性。由于计算图在小批量内的设备之间没有任何依赖关系,因此它是自动并行执行的。 ```{.python .input} def train_batch(X, y, device_params, devices, lr): X_shards, y_shards = split_batch(X, y, devices) - with autograd.record(): # Loss is calculated separately on each GPU + with autograd.record(): # 在每个GPU上分别计算损失 ls = [loss(lenet(X_shard, device_W), y_shard) for X_shard, y_shard, device_W in zip( X_shards, y_shards, device_params)] - for l in ls: # Backpropagation is performed separately on each GPU + for l in ls: # 反向传播在每个GPU上分别执行 l.backward() - # Sum all gradients from each GPU and broadcast them to all GPUs + # 将每个GPU的所有梯度相加,并将其广播到所有GPU for i in range(len(device_params[0])): allreduce([device_params[c][i].grad for c in range(len(devices))]) - # The model parameters are updated separately on each GPU + # 在每个GPU上分别更新模型参数 for param in device_params: - d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch + d2l.sgd(param, lr, X.shape[0]) # 在这里,我们使用全尺寸的小批量 ``` ```{.python .input} #@tab pytorch def train_batch(X, y, device_params, devices, lr): X_shards, y_shards = split_batch(X, y, devices) - # Loss is calculated separately on each GPU + # 在每个GPU上分别计算损失 ls = [loss(lenet(X_shard, device_W), y_shard).sum() for X_shard, y_shard, device_W in zip( X_shards, y_shards, device_params)] - for l in ls: # Backpropagation is performed separately on each GPU + for l in ls: # 反向传播在每个GPU上分别执行 l.backward() - # Sum all gradients from each GPU and broadcast them to all GPUs + # 将每个GPU的所有梯度相加,并将其广播到所有GPU with torch.no_grad(): for i in range(len(device_params[0])): allreduce([device_params[c][i].grad for c in range(len(devices))]) - # The model parameters are updated separately on each GPU + # 在每个GPU上分别更新模型参数 for param in device_params: - d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch + d2l.sgd(param, lr, X.shape[0]) # 在这里,我们使用全尺寸的小批量 ``` -现在,我们可以定义训练功能。它与前几章中使用的略有不同:我们需要分配 GPU 并将所有模型参数复制到所有设备。显然,每个批次都使用 `train_batch` 函数来处理多个 GPU。为方便起见(以及代码的简洁性),我们在单个 GPU 上计算准确性,尽管这是 * 效率低的 *,因为其他 GPU 处于空闲状态。 +现在,我们可以定义训练函数。它与前几章中使用的略有不同:我们需要分配GPU并将所有模型参数复制到所有设备。显然,每个小批量都是使用`train_batch`函数来处理多个GPU的。为了方便(和代码的简洁性),我们在一个GPU上计算精度,尽管这是低效的,因为其他GPU是空闲的。 ```{.python .input} def train(num_gpus, batch_size, lr): train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) devices = [d2l.try_gpu(i) for i in range(num_gpus)] - # Copy model parameters to `num_gpus` GPUs + # 将模型参数复制到`num_gpus`个GPU device_params = [get_params(params, d) for d in devices] num_epochs = 10 animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) @@ -293,11 +293,11 @@ def train(num_gpus, batch_size, lr): for epoch in range(num_epochs): timer.start() for X, y in train_iter: - # Perform multi-GPU training for a single minibatch + # 为单个小批量执行多GPU训练 train_batch(X, y, device_params, devices, lr) npx.waitall() timer.stop() - # Evaluate the model on GPU 0 + # 在GPU 0上评估模型 animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' @@ -309,7 +309,7 @@ def train(num_gpus, batch_size, lr): def train(num_gpus, batch_size, lr): train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) devices = [d2l.try_gpu(i) for i in range(num_gpus)] - # Copy model parameters to `num_gpus` GPUs + # 将模型参数复制到`num_gpus`个GPU device_params = [get_params(params, d) for d in devices] num_epochs = 10 animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs]) @@ -317,25 +317,25 @@ def train(num_gpus, batch_size, lr): for epoch in range(num_epochs): timer.start() for X, y in train_iter: - # Perform multi-GPU training for a single minibatch + # 为单个小批量执行多GPU训练 train_batch(X, y, device_params, devices, lr) torch.cuda.synchronize() timer.stop() - # Evaluate the model on GPU 0 + # 在GPU 0上评估模型 animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu( lambda x: lenet(x, device_params[0]), test_iter, devices[0]),)) print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch ' f'on {str(devices)}') ``` -让我们看看这在单个 GPU 上的效果。我们首先使用 256 个批量大小,学习率为 0.2。 +让我们看看这在单个GPU上运行得有多好。我们首先使用批量大小256,学习率为0.2。 ```{.python .input} #@tab all train(num_gpus=1, batch_size=256, lr=0.2) ``` -通过保持批量大小和学习速率不变并将 GPU 的数量增加到 2,我们可以看到,与之前的实验相比,测试准确度大致保持不变。就优化算法而言,它们是相同的。不幸的是,这里没有任何有意义的加速:模型太小了;此外,我们只有一个小数据集,在这里我们略微不完善的多 GPU 训练方法受到了巨大的 Python 开销。今后,我们将遇到更复杂的模型和更复杂的并行化方式。尽管如此,让我们看看时尚 Mnist 会发生什么。 +通过保持批量大小和学习率不变,并将GPU数增加到2,我们可以看到测试精度与之前的实验基本相同。在优化算法方面,它们是相同的。不幸的是,这里没有任何有意义的加速:模型实在太小了;而且,我们只有一个很小的数据集,在这个数据集中,我们实现多GPU训练的简单方法受到了巨大的Python开销的影响。在未来,我们将遇到更复杂的模型和更复杂的并行化方法。尽管如此,让我们看看Fashion-MNIST会发生什么。 ```{.python .input} #@tab all @@ -344,17 +344,17 @@ train(num_gpus=2, batch_size=256, lr=0.2) ## 小结 -* 有多种方法可以将深度网络训练分成多个 GPU。我们可以在图层之间、跨图层或跨数据拆分它们。前两者需要严格编排的数据传输。数据并行性是最简单的策略。 -* 数据并行培训非常简单。但是,它增加了有效的微型批量以提高效率。 -* 在数据并行度中,数据被拆分到多个 GPU 中,其中每个 GPU 执行自己的向前和向后操作,然后聚合梯度,结果将广播回 GPU。 -* 我们可能会对较大的小批量使用略微提高的学习率。 +* 有多种方法可以在多个GPU上拆分深度网络训练。我们可以在层之间、跨层或跨数据拆分它们。前两者需要经过严格编排的数据传输。数据并行是最简单的策略。 +* 数据并行训练非常简单。但是,它增加了有效的小批量大小以提高效率。 +* 在数据并行中,数据跨多个GPU拆分,其中每个GPU执行其自己的前向传播和反向传播,随后聚合梯度并将结果广播回GPU。 +* 对于较大的小批量,我们可以使用稍微提高的学习率。 ## 练习 -1. 在 $k$ GPU 上进行培训时,将小批量大小从 $b$ 更改为 $k \cdot b$,即按 GPU 的数量向上扩展。 -1. 比较不同学习率的准确性。它如何随着 GPU 的数量进行扩展? -1. 实施一个更高效的 `allreduce` 函数来聚合不同的 GPU 上的不同参数?为什么效率更高? -1. 实施多 GPU 测试精度计算。 +1. 在$k$个GPU上进行训练时,将批量大小从$b$更改为$k \cdot b$,即按GPU的数量进行扩展。 +1. 比较不同学习率的准确性。它如何随着GPU数量的增加而扩展? +1. 实现一个更高效的`allreduce`函数,在不同的GPU上聚合不同的参数?为什么效率更高? +1. 实现多GPU测试精度计算。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/364) From 72eae5a49e15508172c671632d17a1699ce5b6e4 Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:19:55 +0800 Subject: [PATCH 094/103] chapter_computational-performance/auto-parallelism (#795) --- .../auto-parallelism.md | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md index b4d45594d..4de200f94 100644 --- a/chapter_computational-performance/auto-parallelism.md +++ b/chapter_computational-performance/auto-parallelism.md @@ -1,11 +1,11 @@ # 自动并行 :label:`sec_auto_para` -深度学习框架(例如,MxNet 和 PyTorch)会在后端自动构建计算图。使用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如,:numref:`sec_async` 中的 :numref:`fig_asyncgraph` 独立初始化两个变量。因此,系统可以选择并行执行它们。 +深度学习框架(例如,MxNet 和 PyTorch)会在后端自动构建计算图。利用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如,:numref:`sec_async` 中的 :numref:`fig_asyncgraph` 独立初始化两个变量。因此,系统可以选择并行执行它们。 -通常,单个操作员将使用所有 CPU 或单个 GPU 上的所有计算资源。例如,`dot` 操作员将使用所有 CPU 上的所有内核(和线程),即使一台计算机上有多个 CPU 处理器也是如此。同样适用于单个 GPU。因此,并行化对于单设备计算机来说并不是那么有用。对于多台设备,事情更重要。虽然并行化通常在多个 GPU 之间最相关,但添加本地 CPU 将略微提高性能。例如,请参阅 :cite:`Hadjis.Zhang.Mitliagkas.ea.2016`,其中重点介绍了结合 GPU 和 CPU 的计算机视觉模型的训练。借助自动并行化框架的便利性,我们可以通过几行 Python 代码实现相同的目标。更广泛地说,我们对自动并行计算的讨论侧重于使用 CPU 和 GPU 的并行计算,以及计算和通信的并行化。 +通常,单个操作符将使用所有cpu或单个GPU上的所有计算资源。例如,`dot`操作符将使用所有CPU上的所有核心(和线程),即使一台机器上有多个CPU处理器。这同样适用于单个GPU。因此,对于单设备计算机来说,并行化并不是很有用。有了多个设备,并行化就重要了。虽然并行化通常在多个GPU之间,但添加本地CPU将略微提高性能。例如,请参阅 :cite:`Hadjis.Zhang.Mitliagkas.ea.2016` ,它着重于训练结合GPU和CPU的计算机视觉模型。借助自动并行化框架的便利性,我们可以在几行Python代码中实现相同的目标。更广泛地说,我们对自动并行计算的讨论集中在使用CPU和GPU的并行计算,以及计算和通信的并行化。 -请注意,我们至少需要两个 GPU 来运行本节中的实验。 +请注意,我们至少需要两个GPU来运行本节中的实验。 ```{.python .input} from d2l import mxnet as d2l @@ -19,9 +19,9 @@ from d2l import torch as d2l import torch ``` -## GPU 上的并行计算 +## 基于GPU的并行计算 -让我们首先定义要测试的参考工作负载:下面的 `run` 函数使用分配给两个变量的数据在我们选择的设备上执行 10 次矩阵乘法:`x_gpu1` 和 `x_gpu2`。 +让我们从定义一个参考工作负载用于测试开始:下面的`run`函数使用分配到两个变量(`x_gpu1`和`x_gpu2`)的数据在我们选择的设备上执行10次矩阵-矩阵乘法。 ```{.python .input} devices = d2l.try_all_gpus() @@ -43,15 +43,15 @@ x_gpu2 = torch.rand(size=(4000, 4000), device=devices[1]) ``` :begin_tab:`mxnet` -现在我们将函数应用于数据。为了确保缓存不会在结果中发挥作用,我们在测量之前通过对其中任何一个设备执行一次传递来预热设备。 +现在我们将函数应用于数据。为了确保缓存在结果中不起作用,我们通过在测量之前对其中任何一个设备执行一次传递来预热设备。 :end_tab: :begin_tab:`pytorch` -现在我们将函数应用于数据。为了确保缓存不会在结果中发挥作用,我们在测量之前通过对其中任何一个设备执行一次传递来预热设备。`torch.cuda.synchronize()` 等待 CUDA 设备上所有流中的所有内核完成。它采用 `device` 参数,我们需要同步的设备。如果设备参数为 `None`(默认值),则它使用 `current_device()` 给出的当前设备。 +现在我们将函数应用于数据。为了确保缓存在结果中不起作用,我们通过在测量之前对其中任何一个设备执行一次传递来预热设备。`torch.cuda.synchronize()`等待CUDA设备上所有流中的所有核心计算完成。它接受一个`device`参数,代表这个设备需要同步。如果device参数是`None`(默认值),它将使用`current_device()`给出的当前设备。 :end_tab: ```{.python .input} -run(x_gpu1) # Warm-up both devices +run(x_gpu1) # 预热设备 run(x_gpu2) npx.waitall() @@ -67,7 +67,7 @@ with d2l.Benchmark('GPU2 time'): ```{.python .input} #@tab pytorch run(x_gpu1) -run(x_gpu2) # Warm-up all devices +run(x_gpu2) # 预热设备 torch.cuda.synchronize(devices[0]) torch.cuda.synchronize(devices[1]) @@ -81,11 +81,11 @@ with d2l.Benchmark('GPU2 time'): ``` :begin_tab:`mxnet` -如果我们删除两个任务之间的 `waitall` 语句,系统就可以自由地在两个设备上自动并行计算。 +如果我们删除两个任务之间的`waitall`语句,系统就可以在两个设备上自动并行计算。 :end_tab: :begin_tab:`pytorch` -如果我们删除两个任务之间的 `synchronize` 语句,系统就可以自由地在两个设备上自动并行计算。 +如果我们删除两个任务之间的`synchronize`语句,系统就可以在两个设备上自动并行计算。 :end_tab: ```{.python .input} @@ -103,11 +103,11 @@ with d2l.Benchmark('GPU1 & GPU2'): torch.cuda.synchronize() ``` -在上述情况下,总执行时间少于各部分的总和,因为深度学习框架会自动安排两台 GPU 设备上的计算,而无需代表用户编写复杂的代码。 +在上述情况下,总执行时间小于其部分的总和,因为深度学习框架自动调度两个GPU设备上的计算,而不需要用户编写复杂的代码。 -## 并行计算和通信 +## 并行计算与通信 -在许多情况下,我们需要在不同设备之间移动数据,比如在 CPU 和 GPU 之间,或在不同的 GPU 之间移动数据。例如,当我们想要执行分布式优化时,我们需要在多个加速器卡上聚合渐变时,就会发生这种情况。让我们通过在 GPU 上进行计算,然后将结果复制回 CPU 来模拟此操作。 +在许多情况下,我们需要在不同的设备之间移动数据,比如在CPU和GPU之间,或者在不同的GPU之间。例如,当我们想要执行分布式优化时,需要聚合多个加速卡上的梯度时,就会遇到这种情况。让我们通过在GPU上计算,然后将结果复制回CPU来模拟这个过程。 ```{.python .input} def copy_to_cpu(x): @@ -127,25 +127,25 @@ with d2l.Benchmark('Copy to CPU'): def copy_to_cpu(x, non_blocking=False): return [y.to('cpu', non_blocking=non_blocking) for y in x] -with d2l.Benchmark('Run on GPU1'): +with d2l.Benchmark('在GPU1上运行'): y = run(x_gpu1) torch.cuda.synchronize() -with d2l.Benchmark('Copy to CPU'): +with d2l.Benchmark('复制到CPU'): y_cpu = copy_to_cpu(y) torch.cuda.synchronize() ``` :begin_tab:`mxnet` -这有点效率低下。请注意,我们已经可以开始将 `y` 的部分复制到 CPU,而列表的其余部分仍在计算中。例如,当我们计算微型批次的梯度时,就会发生这种情况。其中一些参数的梯度将比其他参数的梯度更早提供。因此,在 GPU 仍在运行的同时开始使用 PCI-Express 总线带宽对我们有利。在两个部分之间删除 `waitall` 使我们能够模拟这种情况。 +这有点低效。请注意,我们可能已经开始将`y`的部分复制到CPU,而列表的其余部分仍在计算中。这种情况会发生,例如,当我们计算一个小批量的梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时开始使用PCI-Express总线带宽对我们是有利的。删除这两个部分之间的`waitall`允许我们模拟这种情况。 :end_tab: :begin_tab:`pytorch` -这有点效率低下。请注意,我们已经可以开始将 `y` 的部分复制到 CPU,而列表的其余部分仍在计算中。例如,当我们计算微型批次上的(backprop)渐变时,就会发生这种情况。其中一些参数的梯度将比其他参数的梯度更早提供。因此,在 GPU 仍在运行的同时开始使用 PCI-Express 总线带宽对我们有利。在 PyTorch 中,诸如 `to()` 和 `copy_()` 等多个函数都承认了一个明确的 `non_blocking` 参数,在不必要的情况下,调用者可以绕过同步。设置 `non_blocking=True` 允许我们模拟此场景。 +这有点低效。请注意,我们可能已经开始将`y`的部分复制到CPU,而列表的其余部分仍在计算中。这种情况会发生,例如,当我们计算一个小批量的(Backprop)梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时开始使用PCI-Express总线带宽对我们是有利的。在PyTorch中,`to()`和`copy_()`等函数都允许显式的`non_blocking`参数,这允许调用方在不需要同步时绕过同步。设置`non_blocking=True`允许我们模拟这个场景。 :end_tab: ```{.python .input} -with d2l.Benchmark('Run on GPU1 and copy to CPU'): +with d2l.Benchmark('在GPU1上运行并复制到CPU'): y = run(x_gpu1) y_cpu = copy_to_cpu(y) npx.waitall() @@ -153,32 +153,32 @@ with d2l.Benchmark('Run on GPU1 and copy to CPU'): ```{.python .input} #@tab pytorch -with d2l.Benchmark('Run on GPU1 and copy to CPU'): +with d2l.Benchmark('在GPU1上运行并复制到CPU'): y = run(x_gpu1) y_cpu = copy_to_cpu(y, True) torch.cuda.synchronize() ``` -两项操作所需的总时间(如预期的那样)都少于其各部分的总和。请注意,此任务与并行计算不同,因为它使用不同的资源:CPU 和 GPU 之间的总线。事实上,我们可以同时在两台设备上进行计算并进行通信。如上所述,计算和通信之间存在依赖关系:必须先计算 `y[i]`,然后才能将其复制到 CPU。幸运的是,该系统可以在计算 `y[i]` 的同时拷贝 `y[i-1]` 以减少总运行时间。 +两个操作所需的总时间少于它们各部分的总和。请注意,此任务与并行计算不同,因为它使用不同的资源:CPU和GPU之间的总线。事实上,我们可以在两个设备上同时进行计算和通信。如上所述,计算和通信之间存在依赖关系:必须先计算`y[i]`,然后才能将其复制到CPU。幸运的是,系统可以在计算`y[i]`的同时复制`y[i-1]`,以减少总的运行时间。 -如 :numref:`fig_twogpu` 中所述,我们最后说明了在 CPU 和两个 GPU 上训练时,计算图及其对简单的双层 MLP 的依赖关系。手动安排由此产生的并行程序将非常痛苦。在这里,拥有基于图形的计算后端进行优化是有利的。 +最后,我们给出了一个简单的两层多层感知机在CPU和两个GPU上训练时的计算图及其依赖关系的例子,如 :numref:`fig_twogpu` 所示。手动调度由此产生的并行程序将是相当痛苦的。这就是基于图的计算后端进行优化的优势所在。 -![The computational graph and its dependencies of a two-layer MLP on a CPU and two GPUs.](../img/twogpu.svg) +![一个CPU和两个GPU上的两层多层感知机的计算图及其依赖关系。](../img/twogpu.svg) :label:`fig_twogpu` ## 小结 -* 现代系统具有各种设备,例如多个 GPU 和 CPU。它们可以并行、异步使用。 -* 现代系统还有各种通信资源,例如 PCI Express、存储(通常是固态硬盘或通过网络)和网络带宽。它们可以并行使用以实现峰值效率。 -* 后端可以通过自动并行计算和通信来提高性能。 +* 现代系统有多种设备,如多个GPU和CPU。它们可以并行、异步地使用。 +* 现代系统还拥有各种通信资源,如PCI Express、存储(通常是固态驱动器或通过网络)和网络带宽。它们可以并行使用,达到最高效率。 +* 后端可以通过自动并行计算和通信来提高性能。 ## 练习 -1. 在本节定义的 `run` 函数中执行了八个操作。它们之间没有依赖关系。设计一个实验,看看深度学习框架是否会自动并行执行它们。 -1. 当单个操作员的工作负载足够小时,即使在单个 CPU 或 GPU 上,并行化也可以提供帮助。设计一个实验来验证这一点。 -1. 设计一个在 CPU、GPU 上使用并行计算以及两个设备之间的通信的实验。 -1. 使用 NVIDIA [Nsight](https://developer.nvidia.com/nsight-compute-2019_5) 之类的调试器来验证您的代码是否有效。 -1. 设计包含更复杂的数据依赖关系的计算任务,并运行实验以查看是否可以在提高性能的同时获得正确的结果。 +1. 在本节定义的`run`函数中执行了八个操作。它们之间没有依赖关系。设计一个实验,看看深度学习框架是否会自动并行执行它们。 +1. 当如果单个操作符的工作量足够小,即使在单个CPU或GPU上,并行化也会有所帮助。设计一个实验来验证这一点。 +1. 设计一个实验,在CPU、GPU上使用并行计算,并在两个设备之间进行通信。 +1. 使用诸如NVIDIA的[Nsight](https://developer.nvidia.com/nsight-compute-2019_5)之类的调试器来验证你的代码是否有效。 +1. 设计包含更复杂数据依赖关系的计算任务,并运行实验,以查看是否可以在提高性能的同时获得正确的结果。 :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/362) From 0e45022c0dbf8363347f7356474d38dce614f8f8 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 7 May 2021 11:21:05 -0700 Subject: [PATCH 095/103] Update multiple-gpus.md --- chapter_computational-performance/multiple-gpus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md index 37c9a9521..0dc4606ea 100644 --- a/chapter_computational-performance/multiple-gpus.md +++ b/chapter_computational-performance/multiple-gpus.md @@ -20,7 +20,7 @@ 最后,我们可以跨多个GPU对数据进行拆分。通过这种方式,所有GPU执行相同类型的工作,尽管观察结果不同。在每个小批量的训练数据之后,梯度在GPU上聚合。这是最简单的方法,可以应用于任何情况。我们只需要在每个小批处理之后进行同步。也就是说,当其他梯度参数仍在计算时,开始交换梯度参数是非常可取的。此外,GPU数量越多,小批量越大,从而提高了训练效率。但是,添加更多GPU并不能让我们训练更大的模型。 -![在多个gpu上并行化。从左到右:原始问题、网络并行、分层并行、数据并行。](../img/splitting.svg) +![在多个GPU上并行化。从左到右:原始问题、网络并行、分层并行、数据并行。](../img/splitting.svg) :label:`fig_splitting` :numref:`fig_splitting`中描述了多个GPU上不同并行方式的比较。总的来说,数据并行是最方便的方法,只要我们能访问有足够大显存的GPU。有关分布式训练并行的详细描述,请参见 :cite:`Li.Andersen.Park.ea.2014` 。GPU显存在深度学习的早期曾经是一个问题。到目前为止,除了最不寻常的情况外,这个问题已经解决了。下面我们将重点讨论数据并行性。 From d58f821b3904caa928ab54f52fa33314b68a012f Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 02:22:56 +0800 Subject: [PATCH 096/103] add ch12 (#801) --- index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/index.md b/index.md index 438ca62cd..dc8769aa3 100644 --- a/index.md +++ b/index.md @@ -29,6 +29,7 @@ chapter_convolutional-neural-networks/index chapter_convolutional-modern/index chapter_recurrent-neural-networks/index chapter_recurrent-modern/index +chapter_computational-performance/index ``` From cc786e542676922d3c3ede3f9eeccb15e8175bdf Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sat, 8 May 2021 03:32:49 +0800 Subject: [PATCH 097/103] fix ch12 ref (#802) --- .../auto-parallelism.md | 2 +- chapter_computational-performance/hardware.md | 12 ++++++------ chapter_computational-performance/hybridize.md | 8 ++++---- .../multiple-gpus-concise.md | 2 +- chapter_computational-performance/multiple-gpus.md | 4 ++-- chapter_computational-performance/parameterserver.md | 10 +++++----- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md index 4de200f94..d1c16b167 100644 --- a/chapter_computational-performance/auto-parallelism.md +++ b/chapter_computational-performance/auto-parallelism.md @@ -1,7 +1,7 @@ # 自动并行 :label:`sec_auto_para` -深度学习框架(例如,MxNet 和 PyTorch)会在后端自动构建计算图。利用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如,:numref:`sec_async` 中的 :numref:`fig_asyncgraph` 独立初始化两个变量。因此,系统可以选择并行执行它们。 +深度学习框架(例如,MxNet 和 PyTorch)会在后端自动构建计算图。利用计算图,系统可以了解所有依赖关系,并且可以选择性地并行执行多个不相互依赖的任务以提高速度。例如, :numref:`sec_async` 中的 :numref:`fig_asyncgraph` 独立初始化两个变量。因此,系统可以选择并行执行它们。 通常,单个操作符将使用所有cpu或单个GPU上的所有计算资源。例如,`dot`操作符将使用所有CPU上的所有核心(和线程),即使一台机器上有多个CPU处理器。这同样适用于单个GPU。因此,对于单设备计算机来说,并行化并不是很有用。有了多个设备,并行化就重要了。虽然并行化通常在多个GPU之间,但添加本地CPU将略微提高性能。例如,请参阅 :cite:`Hadjis.Zhang.Mitliagkas.ea.2016` ,它着重于训练结合GPU和CPU的计算机视觉模型。借助自动并行化框架的便利性,我们可以在几行Python代码中实现相同的目标。更广泛地说,我们对自动并行计算的讨论集中在使用CPU和GPU的并行计算,以及计算和通信的并行化。 diff --git a/chapter_computational-performance/hardware.md b/chapter_computational-performance/hardware.md index 52b8fd69d..7cd5f9f8d 100644 --- a/chapter_computational-performance/hardware.md +++ b/chapter_computational-performance/hardware.md @@ -31,7 +31,7 @@ 虽然这些数字令人印象深刻,但实际上,它们只说明了故事的一部分。当我们想要从内存中读取一部分时,我们首先需要告诉内存模块在哪里可以找到信息。也就是说,我们首先需要将*地址*(address)发送到内存。完成后,我们可以选择只读取一条64位记录或一长串记录。后者称为“突发读取”(burst read)。简而言之,向内存发送地址并设置传输大约需要100ns(细节取决于所用内存芯片的特定定时系数),每个后续传输只需要0.2ns。简而言之,第一次读取的成本是后续读取的500倍!请注意,我们每秒最多可以执行10000000次随机读取。这表明我们尽可能避免随机内存访问,而是使用突发读取(和写入)。 -当我们考虑到我们有多个物理BANK时,事情就更复杂了。每个BANK都可以独立地读取内存。这意味着两件事。一方面,如果随机读操作均匀分布在内存中,那么有效的随机读操作次数将高达4倍。这也意味着执行随机读取仍然不是一个好主意,因为突发读取的速度也快了4倍。另一方面,由于内存对齐到64位边界,最好将任何数据结构与相同的边界对齐。当设置了适当的标志时,编译器基本上就是[自动化](https://en.wikipedia.org/wiki/Data_structure_alignment)地执行此操作。我们鼓励好奇的读者回顾一个关于DRAM的讲座,比如[Zeshan Chishti](http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf).]所做的讲座。 +当我们考虑到我们有多个物理BANK时,事情就更复杂了。每个BANK都可以独立地读取内存。这意味着两件事。一方面,如果随机读操作均匀分布在内存中,那么有效的随机读操作次数将高达4倍。这也意味着执行随机读取仍然不是一个好主意,因为突发读取的速度也快了4倍。另一方面,由于内存对齐到64位边界,最好将任何数据结构与相同的边界对齐。当设置了适当的标志时,编译器基本上就是[自动化](https://en.wikipedia.org/wiki/Data_structure_alignment)地执行此操作。我们鼓励好奇的读者回顾一个关于DRAM的讲座,比如[Zeshan Chishti](http://web.cecs.pdx.edu/~zeshan/ece585_lec5.pdf)所做的讲座。 GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得多。总的来说,解决这些问题有两种选择。首先是使内存总线变得更宽。例如,NVIDIA的RTX 2080 Ti有一条352位宽的总线。这样就可以同时传输更多的信息。其次,GPU使用特定的高性能内存。消费级设备,如NVIDIA的RTX和Titan系列,通常使用[GDDR6](https://en.wikipedia.org/wiki/GDDR6_SDRAM)芯片,总带宽超过500GB/s。另一种选择是使用HBM(高带宽存储器)模块。它们使用截然不同的接口,直接与专用硅片上的GPU连接。这使得它们非常昂贵,通常仅限于高端服务器芯片,如NVIDIA Volta V100系列加速卡。毫不奇怪,GPU内存通常比CPU内存小得多,因为前者的成本更高。就我们的目的而言,它们的性能特征大体上是相似的,只是速度要快得多。就本书而言,我们完全可以忽略细节。它们只在调整GPU核心以获得高吞吐量时才起作用。 @@ -102,17 +102,17 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 ## GPU和其他加速卡 -毫不夸张地说,如果没有GPU,深度学习就不会成功。基于同样的原因,有理由认为GPU制造商的财富由于深度学习而显著增加。这种硬件和算法的协同进化导致了这样一种情况:无论好坏,深度学习都是更可取的统计建模范式。因此,了解GPU和其他加速卡(如TPU:cite:`Jouppi.Young.Patil.ea.2017`)的具体好处是值得的。 +毫不夸张地说,如果没有GPU,深度学习就不会成功。基于同样的原因,有理由认为GPU制造商的财富由于深度学习而显著增加。这种硬件和算法的协同进化导致了这样一种情况:无论好坏,深度学习都是更可取的统计建模范式。因此,了解GPU和其他加速卡(如TPU :cite:`Jouppi.Young.Patil.ea.2017` )的具体好处是值得的。 值得注意的是,在实践中经常会有这样一个区别:加速卡是为训练或推理而优化的。对于后者,我们只需要计算网络中的前向传播。反向传播不需要存储中间数据。此外,我们可能不需要非常精确的计算(FP16或INT8通常就足够了)。另一方面,在训练过程中,所有中间结果都需要存储来计算梯度。此外,累积梯度需要更高的精度,以避免数值下溢(或溢出)。这意味着FP16(或与FP32的混合精度)是最低要求。所有这些都需要更快、更大的内存(HBM2与GDDR6相比)和更高的处理能力。例如,NVIDIA的[Turing](https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/) T4 GPU优化用于推理,而V100 GPU更适合用于训练。 -回想一下如:numref:`fig_neon128`所示的矢量化。将向量处理单元添加到处理器核心可以显著提高吞吐量。例如,在 :numref:`fig_neon128` 的例子中,我们能够同时执行16个操作。首先,如果我们添加的运算不仅优化了向量之间的运算,而且优化了矩阵之间的运算,会怎么样?这个策略引入了张量核(tensor cores),这稍后将讨论。第二,如果我们增加更多的核心呢?简而言之,这两种策略总结了GPU中的设计决策。 :numref:`fig_turing_processing_block` 给出了基本处理块的概述。它包含16个整数和16个浮点单位。除此之外,两个张量核加速了与深度学习相关的附加操作的窄子集。每个流式多处理器由四个这样的块组成。 +回想一下如 :numref:`fig_neon128` 所示的矢量化。将向量处理单元添加到处理器核心可以显著提高吞吐量。例如,在 :numref:`fig_neon128` 的例子中,我们能够同时执行16个操作。首先,如果我们添加的运算不仅优化了向量之间的运算,而且优化了矩阵之间的运算,会怎么样?这个策略引入了张量核(tensor cores),这稍后将讨论。第二,如果我们增加更多的核心呢?简而言之,这两种策略总结了GPU中的设计决策。 :numref:`fig_turing_processing_block` 给出了基本处理块的概述。它包含16个整数和16个浮点单位。除此之外,两个张量核加速了与深度学习相关的附加操作的窄子集。每个流式多处理器由四个这样的块组成。 ![NVIDIA Turing 处理块(图片由英伟达提供)](../img/turing-processing-block.png) :width:`150px` :label:`fig_turing_processing_block` -接下来,将12个流式多处理器分组为图形处理集群,这些集群构成了高端TU102处理器。充足的内存通道和二级缓存补充了设置。:numref:`fig_turing`有相关的细节。设计这种设备的原因之一是,可以根据需要添加或删除单个模块,以允许更紧凑的芯片和处理成品率问题(故障模块可能无法激活)。幸运的是,在CUDA和框架代码层之下,这类设备的编程对随意的深度学习研究人员隐藏得很好。特别是,如果有可用的资源,在GPU上可以同时执行多个程序。尽管如此,了解设备的局限性是值得的,以避免选择不适合设备内存的型号。 +接下来,将12个流式多处理器分组为图形处理集群,这些集群构成了高端TU102处理器。充足的内存通道和二级缓存补充了设置。 :numref:`fig_turing` 有相关的细节。设计这种设备的原因之一是,可以根据需要添加或删除单个模块,以允许更紧凑的芯片和处理成品率问题(故障模块可能无法激活)。幸运的是,在CUDA和框架代码层之下,这类设备的编程对随意的深度学习研究人员隐藏得很好。特别是,如果有可用的资源,在GPU上可以同时执行多个程序。尽管如此,了解设备的局限性是值得的,以避免选择不适合设备内存的型号。 ![NVIDIA Turing 架构(图片由英伟达提供)](../img/turing.png) :width:`350px` @@ -132,12 +132,12 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 * **PCIe**是一种专用总线,用于每个通道的高带宽点到点连接(在16通道插槽中的PCIe 4.0上高达32 GB/s)。延迟时间为个位数的微秒(5μs) PCIe链接非常宝贵。处理器的数量有限:AMD的EPYC 3有128个通道,Intel的Xeon每个芯片有48个通道;在桌面级CPU上,数字分别是20(Ryzen 9)和16(Core i9)。由于GPU通常有16个通道,这限制了可以以全带宽连接到CPU的GPU数量。毕竟,它们需要与其他高带宽外围设备(如存储和以太网)共享链路。与内存访问一样,由于减少了数据包开销,因此更适合大容量传输。 * **以太网**是连接计算机最常用的方式。虽然它比PCIe慢得多,但它的安装成本非常低,而且具有很强的弹性,而且覆盖的距离要长得多。低级服务器的典型带宽为1 GBit/s。高端设备(如云中的[C5实例](https://aws.amazon.com/ec2/instance-types/c5/))提供10到100GBit/s的带宽。与所有以前的情况一样,数据传输有很大的开销。请注意,我们几乎从不直接使用原始以太网,而是使用在物理互连之上执行的协议(例如UDP或TCP/IP)。这进一步增加了开销。与PCIe类似,以太网旨在连接两个设备,例如计算机和交换机。 -* **交换机**允许我们以一种方式连接多个设备,该连接方式下的任何一对设备都可以同时执行(通常是全带宽)点对点连接。例如,以太网交换机可能以高带宽连接40台服务器。请注意,交换机并不是传统计算机网络所独有的。甚至PCIe通道也可以是[可交换的]](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。例如,将大量GPU连接到主机处理器时会出现这种情况,[P2实例](https://aws.amazon.com/ec2/instance-types/p2/)就是这种情况。 +* **交换机**允许我们以一种方式连接多个设备,该连接方式下的任何一对设备都可以同时执行(通常是全带宽)点对点连接。例如,以太网交换机可能以高带宽连接40台服务器。请注意,交换机并不是传统计算机网络所独有的。甚至PCIe通道也可以是[可交换的](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。例如,将大量GPU连接到主机处理器时会出现这种情况,[P2实例](https://aws.amazon.com/ec2/instance-types/p2/)就是这种情况。 * **NVLink**是PCIe的替代品,适用于非常高带宽的互连。它为每条链路提供高达300 Gbit/s的数据传输速率。服务器GPU(Volta V100)有六个链路。而消费级GPU(RTX 2080 Ti)只有一个链路,运行速度降低到100 Gbit/s。我们建议使用[NCCL](https://github.com/NVIDIA/nccl)来实现GPU之间的高速数据传输。 ## 更多延迟 -:numref:`table_latency_numbers`和:numref:`table_latency_numbers_tesla`中的小结来自[Eliot Eshelman](https://gist.github.com/eshelman),他们将数字的更新版本保存到[GitHub gist](https://gist.github.com/eshelman/343a1c46cb3fba142c1afdcdeec17646)。 + :numref:`table_latency_numbers` 和 :numref:`table_latency_numbers_tesla` 中的小结来自[Eliot Eshelman](https://gist.github.com/eshelman),他们将数字的更新版本保存到[GitHub gist](https://gist.github.com/eshelman/343a1c46cb3fba142c1afdcdeec17646)。 :常见延迟。 diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md index 3d60abae6..637632b1a 100644 --- a/chapter_computational-performance/hybridize.md +++ b/chapter_computational-performance/hybridize.md @@ -17,12 +17,12 @@ def fancy_func(a, b, c, d): print(fancy_func(1, 2, 3, 4)) ``` -Python是一种*解释语言*(interpreted language)。当评估上述`fancy_func`函数时,它按顺序执行函数体的操作。也就是说,它将计算`e = add(a, b)`,并将结果存储为变量`e`,从而更改程序的状态。接下来的两个语句`f = add(c, d)`和`g = add(e, f)`将类似地执行,执行加法并将结果存储为变量。:numref:`fig_compute_graph`说明了数据流。 +Python是一种*解释语言*(interpreted language)。当评估上述 `fancy_func` 函数时,它按顺序执行函数体的操作。也就是说,它将计算`e = add(a, b)`,并将结果存储为变量`e`,从而更改程序的状态。接下来的两个语句`f = add(c, d)`和`g = add(e, f)`将类似地执行,执行加法并将结果存储为变量。 :numref:`fig_compute_graph` 说明了数据流。 ![命令式编程中的数据流。](../img/computegraph.svg) :label:`fig_compute_graph` -尽管命令式编程很方便,但可能效率低下。一方面,即使`add`函数在`fancy_func`中被重复调用,Python也会单独执行这三个函数调用。如果在一个GPU(甚至多个GPU)上执行这些命令,那么Python解释器产生的开销可能会非常大。此外,它需要保存`e`和`f`的变量值,直到`fancy_func`中的所有语句都执行完毕。这是因为我们不知道在执行语句`e = add(a, b)`和`f = add(c, d)`之后,程序的其他部分是否会使用变量`e`和`f`。 +尽管命令式编程很方便,但可能效率低下。一方面,即使 `add` 函数在 `fancy_func` 中被重复调用,Python也会单独执行这三个函数调用。如果在一个GPU(甚至多个GPU)上执行这些命令,那么Python解释器产生的开销可能会非常大。此外,它需要保存`e`和`f`的变量值,直到 `fancy_func` 中的所有语句都执行完毕。这是因为我们不知道在执行语句 `e = add(a, b)` 和 `f = add(c, d)` 之后,程序的其他部分是否会使用变量`e`和`f`。 ## 符号式编程 @@ -32,7 +32,7 @@ Python是一种*解释语言*(interpreted language)。当评估上述`fancy_ 1. 将操作编译成可执行程序。 1. 提供所需的输入并调用编译后的程序供执行。 -这允许进行大量优化。首先,在许多情况下,我们可以跳过Python解释器。从而消除在多个更快的GPU上与在CPU上的单个Python线程搭配使用时可能出现的性能瓶颈。其次,编译器可能会优化并将上述代码重写为`print((1 + 2) + (3 + 4))`甚至`print(10)`。这是可能的,因为编译器在将其转换为机器指令之前可以看到完整的代码。例如,只要不再需要某个变量,它就可以释放内存(或者从不分配内存)。或者它可以将代码转换为一个完全等价的片段。为了获得更好的想法,请考虑下面的命令式编程的模拟(仍然是Python)。 +这允许进行大量优化。首先,在许多情况下,我们可以跳过Python解释器。从而消除在多个更快的GPU上与在CPU上的单个Python线程搭配使用时可能出现的性能瓶颈。其次,编译器可能会优化并将上述代码重写为`print((1 + 2) + (3 + 4))`甚至`print(10)`。这是可能的,因为编译器在将其转换为机器指令之前可以看到完整的代码。例如,只要不再需要某个变量,它就可以释放内存(或者从不分配内存)。或者它可以将代码转换为一个完全等价的片段。为了获得更好的想法,请考虑下面的命令式编程的模拟。 ```{.python .input} #@tab all @@ -225,7 +225,7 @@ with Benchmark('无混合式'): npx.waitall() net.hybridize() -with Benchmark('有混合式'): +with Benchmark('混合式'): for i in range(1000): net(x) npx.waitall() ``` diff --git a/chapter_computational-performance/multiple-gpus-concise.md b/chapter_computational-performance/multiple-gpus-concise.md index 01b84ac02..ede65944c 100644 --- a/chapter_computational-performance/multiple-gpus-concise.md +++ b/chapter_computational-performance/multiple-gpus-concise.md @@ -129,7 +129,7 @@ weight.data(devices[0])[0], weight.data(devices[1])[0] ``` :begin_tab:`mxnet` -接下来,让我们用一个在多个设备上并行工作的代码来替换评估准确性的代码。这是 :numref:`sec_lenet` 的`evaluate_accuracy_gpu`功能的替代。主要区别在于,我们在调用网络之前拆分了一个小批量。其他的基本上都是一样的。 +接下来,让我们用一个在多个设备上并行工作的代码来替换评估准确性的代码。这是 :numref:`sec_lenet` 的`evaluate_accuracy_gpu`函数的替代。主要区别在于,我们在调用网络之前拆分了一个小批量。其他的基本上都是一样的。 :end_tab: ```{.python .input} diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md index 0dc4606ea..d911856f4 100644 --- a/chapter_computational-performance/multiple-gpus.md +++ b/chapter_computational-performance/multiple-gpus.md @@ -1,7 +1,7 @@ # 多GPU训练 :label:`sec_multi_gpu` -到目前为止,我们讨论了如何在CPU和GPU上高效地训练模型。我们甚至在 :numref:`sec_auto_para` 中展示了深度学习框架如何使它们之间的计算和通信自动并行化。我们还在 :numref:`sec_use_gpu` 中展示了如何使用 `nvidia-smi` 命令列出计算机上所有可用的GPU。我们没有讨论的是如何真正实现深度学习训练的并行化。我们暗示了一种方法,即以某种方式将数据分割到多个设备上,并使其正常工作。本节将详细介绍如何从零开始并行训练网络。关于如何利用高级API中的详情请参阅 :numref:`sec_multi_gpu_concise` 。我们假设你熟悉小批量随机梯度下降算法,如 :numref:`sec_minibatch_sgd` 中描述的算法。 +到目前为止,我们讨论了如何在CPU和GPU上高效地训练模型。我们甚至在 :numref:`sec_auto_para` 中展示了深度学习框架如何使它们之间的计算和通信自动并行化。我们还在 :numref:`sec_use_gpu` 中展示了如何使用 `nvidia-smi` 命令列出计算机上所有可用的GPU。我们没有讨论的是如何真正实现深度学习训练的并行化。我们暗示了一种方法,即以某种方式将数据分割到多个设备上,并使其正常工作。本节将详细介绍如何从零开始并行训练网络。关于如何利用高级API中的详情请参阅 :numref:`sec_multi_gpu_concise` 。我们假设你熟悉小批量随机梯度下降算法,如 :numref:`sec_minibatch_sgd` 中描述的算法。 ## 拆分问题 @@ -9,7 +9,7 @@ 首先,我们可以在多个GPU之间拆分网络。也就是说,每个GPU将流入特定层的数据作为输入,跨多个后续层处理数据,然后将数据发送到下一个GPU。与单个GPU所能处理的数据相比,这使我们可以用更大的网络处理数据。此外,每个GPU的显存占用可以得到很好的控制(它只是整个网络占用的一小部分)。 -然而,层(以及GPU)之间的接口需要紧密同步。这可能很棘手,特别是在计算工作负载在层之间没有正确匹配的时候。对于大量的GPU来说,这个问题更加严重。层之间的接口还需要大量的数据传输,例如激活值和梯度。这可能会超出GPU总线的带宽。此外,计算密集型的操作顺序对于拆分来说是非常重要的。有关这方面的最佳努力,请参见:cite:`Mirhoseini.Pham.Le.ea.2017`。这仍然是一个困难的问题,目前还不清楚是否有可能在不平凡的问题上实现良好的(线性)缩放。除非有优秀的框架或操作系统支持将多个GPU连接在一起,否则我们不建议使用它。 +然而,层(以及GPU)之间的接口需要紧密同步。这可能很棘手,特别是在计算工作负载在层之间没有正确匹配的时候。对于大量的GPU来说,这个问题更加严重。层之间的接口还需要大量的数据传输,例如激活值和梯度。这可能会超出GPU总线的带宽。此外,计算密集型的操作顺序对于拆分来说是非常重要的。有关这方面的最佳努力,请参见 :cite:`Mirhoseini.Pham.Le.ea.2017` 。这仍然是一个困难的问题,目前还不清楚是否有可能在特定问题上实现良好的(线性)缩放。除非有优秀的框架或操作系统支持将多个GPU连接在一起,否则我们不建议使用它。 第二,我们可以拆分工作。例如,我们可以将问题分散到4个GPU,每个GPU生成16个通道的数据,而不是在单个GPU上计算64个通道。同样,对于全连接的层,我们可以拆分输出单元的数量。 :numref:`fig_alexnet_original` (来自 :cite:`Krizhevsky.Sutskever.Hinton.2012` )说明了这种设计,这种策略用于显存非常小(当时为2GB)的GPU。这允许在计算方面进行良好的缩放,前提是通道(或单元)的数量不太小。此外,由于可用显存呈线性扩展,多个GPU可以处理越来越大的网络。 diff --git a/chapter_computational-performance/parameterserver.md b/chapter_computational-performance/parameterserver.md index 7cfab4c62..b8aa9b924 100644 --- a/chapter_computational-performance/parameterserver.md +++ b/chapter_computational-performance/parameterserver.md @@ -3,11 +3,11 @@ 当我们从一个GPU迁移到多个GPU,然后再迁移到包含多个GPU的多个服务器时(可能全部分布在多个机架和网络交换机上),我们的分布式和并行训练算法需要变得更加复杂。细节很重要,因为不同的互连具有非常不同的带宽(例如,在适当的设置下,NVLink可以跨6条链路提供高达100 GB/s的带宽,PCIe 4.0(16通道)提供32 GB/s的带宽,而即使是高速100GbE以太网也只能提供10 GB/s)。同时,期望统计建模者成为网络和系统方面的专家是不合理的。 -:cite:`Smola.Narayanamurthy.2010` 在分布式隐变量模型的背景下引入了参数服务器的核心思想。接着在:cite:`Ahmed.Aly.Gonzalez.ea.2012` 中描述了Push和Pull语义,接着在 :cite:`Li.Andersen.Park.ea.2014` 中描述了系统和开源库。在下面,我们将描述提高效率所需的技术。 +:cite:`Smola.Narayanamurthy.2010` 在分布式隐变量模型的背景下引入了参数服务器的核心思想。接着在 :cite:`Ahmed.Aly.Gonzalez.ea.2012` 中描述了Push和Pull语义,接着在 :cite:`Li.Andersen.Park.ea.2014` 中描述了系统和开源库。在下面,我们将描述提高效率所需的技术。 ## 数据并行训练 -让我们回顾一下分布式训练的数据并行训练方法。我们将在本节中使用它来排除所有其他内容,因为它在实践中的实现要简单得多。除了对图深度学习外,实际上没有任何场景推荐任何其他并行策略,因为GPU现在有大量的显存。:numref:`fig_parameterserver` 描述了我们在 :numref:`sec_multi_gpu` 中实现的数据并行的变体。其中关键的一点是,在将更新的参数重新广播到所有GPU之前,在GPU 0上进行梯度聚合。 +让我们回顾一下分布式训练的数据并行训练方法。我们将在本节中使用它来排除所有其他内容,因为它在实践中的实现要简单得多。除了对图深度学习外,实际上没有任何场景推荐任何其他并行策略,因为GPU现在有大量的显存。 :numref:`fig_parameterserver` 描述了我们在 :numref:`sec_multi_gpu` 中实现的数据并行的变体。其中关键的一点是,在将更新的参数重新广播到所有GPU之前,在GPU 0上进行梯度聚合。 ![左图:单GPU训练。右图:多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新分发给所有GPU。](../img/ps.svg) :label:`fig_parameterserver` @@ -19,12 +19,12 @@ ![一个4路GPU服务器](../img/bw-hierarchy.svg) :label:`fig_bw_hierarchy` -为了便于讨论,让我们假设梯度是160MB。在这种情况下,将所有剩余3个GPU的梯度发送到第4个GPU需要30毫秒(每次传输需要10毫秒=160 MB/16 GB/s)。再加上30毫秒将权重向量传输回来,我们得到的结果是总共60毫秒。如果我们将所有数据发送到CPU,我们将有40毫秒的惩罚,因为4个GPU每个都需要将数据发送到CPU,总共产生80毫秒。最后,假设我们能够将梯度分为4部分,每个40 MB。现在我们可以在不同的GPU上同时聚合每个部分,因为PCIe交换机在所有链路之间提供全带宽操作。这需要7.5毫秒,而不是30毫秒。因此同步操作总共需要15毫秒。简而言之,根据我们同步参数的不同,同样的操作可能需要15ms到80ms不等的时间。:numref:`fig_ps_distributed`描述了交换参数的不同策略。 +为了便于讨论,让我们假设梯度是160MB。在这种情况下,将所有剩余3个GPU的梯度发送到第4个GPU需要30毫秒(每次传输需要10毫秒=160 MB/16 GB/s)。再加上30毫秒将权重向量传输回来,我们得到的结果是总共60毫秒。如果我们将所有数据发送到CPU,我们将有40毫秒的惩罚,因为4个GPU每个都需要将数据发送到CPU,总共产生80毫秒。最后,假设我们能够将梯度分为4部分,每个40 MB。现在我们可以在不同的GPU上同时聚合每个部分,因为PCIe交换机在所有链路之间提供全带宽操作。这需要7.5毫秒,而不是30毫秒。因此同步操作总共需要15毫秒。简而言之,根据我们同步参数的不同,同样的操作可能需要15ms到80ms不等的时间。 :numref:`fig_ps_distributed` 描述了交换参数的不同策略。 ![参数同步策略](../img/ps-distributed.svg) :label:`fig_ps_distributed` -请注意,我们还可以使用另一个工具来改进性能: 在深度网络中,计算从顶部到底部的所有梯度需要一些时间。我们可以开始同步一些参数的梯度,即使我们还在忙着为其他参数计算梯度。参见 :cite:`Sergeev.Del-Balso.2018`,以了解在[Horovod](https://github.com/horovod/horovod)中如何做到这一点的详细信息。 +请注意,我们还可以使用另一个工具来改进性能: 在深度网络中,计算从顶部到底部的所有梯度需要一些时间。我们可以开始同步一些参数的梯度,即使我们还在忙着为其他参数计算梯度。参见 :cite:`Sergeev.Del-Balso.2018` ,以了解在[Horovod](https://github.com/horovod/horovod)中如何做到这一点的详细信息。 ## 环同步(Ring Synchronization) @@ -77,7 +77,7 @@ $$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{ 其中$\mathbf{g}_{ijk}$是在工作节点$k$的GPU $j$上分划分的梯度$i$的一部分。这个运算的关键之处在于它是一个*交换归约*(commutative reduction),也就是说,它把许多向量变成一个向量,而运算的顺序并不重要。这对于我们的目的来说是非常好的,因为我们不需要对何时接收哪个梯度进行细粒度的控制。此外,请注意,此操作在不同的$i$之间是独立的。 -这允许我们定义以下两个操作:*push*(累积梯度)和*pull*(检索聚合梯度)。因为我们有很多不同的梯度(毕竟,我们有很多层),所以我们需要用一个键$i$索引梯度。这种与键-值存储( 如dynamo:cite:`DeCandia.Hastorun.Jampani.ea.2007` 中引入的键-值存储)的相似性并非巧合。它们也满足许多类似的特性,特别是在多个服务器之间分配参数时。 +这允许我们定义以下两个操作:*push*(累积梯度)和*pull*(检索聚合梯度)。因为我们有很多不同的梯度(毕竟,我们有很多层),所以我们需要用一个键$i$索引梯度。这种与键-值存储( 如Dynamo :cite:`DeCandia.Hastorun.Jampani.ea.2007` 中引入的键-值存储)的相似性并非巧合。它们也满足许多类似的特性,特别是在多个服务器之间分配参数时。 键值存储的push-pull操作描述如下: From af916079564c120a9b40cbf117fb594ac657a991 Mon Sep 17 00:00:00 2001 From: HelloWorld Date: Sat, 8 May 2021 03:40:34 +0800 Subject: [PATCH 098/103] fix translate (#791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update lookup-api.md fix one word '这' * 修改和一点疑问。 我发现了一些基本的排版格式,例如数字和英文,在中文中间的需要两边加上空格。**之间的是斜体。 那么**直接的空格应该怎么处理?需要在*两边都加上空格吗? **和**之间是加黑对吗? * Update index.md * Update attention-cues.md * Update attention-scoring-functions.md * Update nadaraya-waston.md * Update transformer.md * Update bahdanau-attention.md * Update multihead-attention.md * fix translate * Update hybridize.md * Update async-computation.md * Update auto-parallelism.md * Update hardware.md * Update multiple-gpus-concise.md * Update multiple-gpus.md * Update parameterserver.md * Update image-augmentation.md * Update fine-tuning.md * Update kaggle-cifar10.md --- chapter_computer-vision/kaggle-cifar10.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/chapter_computer-vision/kaggle-cifar10.md b/chapter_computer-vision/kaggle-cifar10.md index 6deb594d0..d222ea772 100644 --- a/chapter_computer-vision/kaggle-cifar10.md +++ b/chapter_computer-vision/kaggle-cifar10.md @@ -1,11 +1,11 @@ -# Kaggle 上的图像分类 (CIFAR-10) +# 实战 Kaggle 比赛:图像分类(CIFAR-10) :label:`sec_kaggle_cifar10` 到目前为止,我们一直在使用深度学习框架的高级 API 直接获取 Tensor 格式的图像数据集。但是,自定义图像数据集通常以图像文件的形式出现。在本节中,我们将从原始图像文件开始,然后逐步整理、阅读,然后将它们转换为张量格式。 -我们在 :numref:`sec_image_augmentation` 中尝试了 CIFAR-10 数据集,这是计算机视觉中的重要数据集。在本节中,我们将运用我们在前几节中学到的知识来练习 CIFAR-10 图像分类的 Kaggle 竞赛。比赛的网址是 https://www.kaggle.com/c/cifar-10 +我们在 :numref:`sec_image_augmentation` 中尝试了 CIFAR-10 数据集,这是计算机视觉中的重要数据集。在本节中,我们将运用我们在前几节中学到的知识来练习 CIFAR-10 图像分类的 Kaggle 比赛。比赛的网址是 https://www.kaggle.com/c/cifar-10 -:numref:`fig_kaggle_cifar10` 在竞争对手的网页上显示了信息。为了提交结果,您需要注册 Kaggle 账户。 +:numref:`fig_kaggle_cifar10` 展示了该比赛的网页信息。为了便于提交结果,请先在Kaggle网站上注册账号。 ![CIFAR-10 image classification competition webpage information. The competition dataset can be obtained by clicking the "Data" tab.](../img/kaggle-cifar10.png) :width:`600px` @@ -37,11 +37,11 @@ import pandas as pd import shutil ``` -## 获取和组织数据集 +## 获取和整理数据集 比赛数据集分为训练集和测试集,分别包含 50000 张和 300000 张图像。在测试集中,10000 张图像将用于评估,而剩下的 290000 张图像将不会进行评估:包含它们只是为了使其难以作弊 *手动 * 标记测试集的结果。 -此数据集中的图像都是 png 颜色(RGB 通道)图像文件,其高度和宽度均为 32 像素。这些图片共涵盖 10 个类别,即飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。:numref:`fig_kaggle_cifar10` 的左上角显示了数据集中飞机、汽车和鸟类的一些图像。 +此数据集中的图像都是 png 颜色(RGB 通道)图像文件,其高度和宽度均为 32 像素。这些图片共涵盖 10 个类别,即飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。:numref:`fig_kaggle_cifar10` 的左上角显示了数据集中飞机、汽车和鸟类的图像。 ### 下载数据集 @@ -72,7 +72,7 @@ else: data_dir = '../data/cifar-10/' ``` -### 组织数据集 +### 整理数据集 我们需要组织数据集来促进模型训练和测试。让我们首先阅读 csv 文件中的标签。以下函数返回一个字典,该字典将文件名的非扩展名部分映射到其标签。 @@ -212,9 +212,9 @@ transform_test = torchvision.transforms.Compose([ [0.2023, 0.1994, 0.2010])]) ``` -## 阅读数据集 +## 读取数据集 -接下来,我们阅读由原始图像文件组成的组织数据集。每个示例都包括一张图片和一个标签。 +接下来,我们读取由原始图像文件组成的组织数据集。每个示例都包括一张图片和一个标签。 ```{.python .input} train_ds, valid_ds, train_valid_ds, test_ds = [ @@ -347,7 +347,7 @@ def get_net(): loss = nn.CrossEntropyLoss(reduction="none") ``` -## 定义训练功能 +## 定义训练函数 我们将根据模型在验证集上的性能选择模型并调整超参数。在下面,我们定义了模型训练函数 `train`。 @@ -428,7 +428,7 @@ def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, f'on {str(devices)}') ``` -## 培训和验证模型 +## 训练和验证模型 现在,我们可以训练和验证模型。以下所有超参数都可以调整。例如,我们可以增加纪元的数量。当 `lr_period` 和 `lr_decay` 分别设置为 50 和 0.1 时,优化算法的学习速率将在每 50 个纪元后乘以 0.1。只是为了示范,我们在这里只训练一个时代。 @@ -486,7 +486,7 @@ df.to_csv('submission.csv', index=False) 上面的代码将生成一个 `submission.csv` 文件,其格式符合 Kaggle 竞争的要求。向 Kaggle 提交结果的方法与 :numref:`sec_kaggle_house` 中的方法类似。 -## 摘要 +## 小结 * 将包含原始图像文件的数据集组织为所需格式后,我们可以读取它们。 From dee730dd9b07d7a5d6d93d9da9b3f18b7d40be35 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 7 May 2021 12:48:42 -0700 Subject: [PATCH 099/103] Update frontpage.html --- static/frontpage/frontpage.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/frontpage/frontpage.html b/static/frontpage/frontpage.html index ecdfe3150..671a5b3f5 100644 --- a/static/frontpage/frontpage.html +++ b/static/frontpage/frontpage.html @@ -212,6 +212,10 @@

《动手学深度学习》

公告

    +
  • 【新增章节】 + 现代循环神经网络和 + 计算性能两章已翻译至中文版第二版,并含多种深度学习框架的实现。 +
  • 【在线课程每周直播中】 3月20日起北京时间每周六、日下午1:00至2:30直播教学《动手学深度学习》。无需缴费或注册,欢迎参加
  • From dab13abbd2a69236c3e80c90552c6d78f293d070 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 7 May 2021 13:41:21 -0700 Subject: [PATCH 100/103] Update frontpage.html --- static/frontpage/frontpage.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/frontpage/frontpage.html b/static/frontpage/frontpage.html index 671a5b3f5..d253e0658 100644 --- a/static/frontpage/frontpage.html +++ b/static/frontpage/frontpage.html @@ -213,8 +213,7 @@

    公告

    • 【新增章节】 - 现代循环神经网络和 - 计算性能两章已翻译至中文版第二版,并含多种深度学习框架的实现。 + 现代循环神经网络计算性能两章已翻译至中文版第二版,并含多种深度学习框架的实现。
    • 【在线课程每周直播中】 3月20日起北京时间每周六、日下午1:00至2:30直播教学《动手学深度学习》。无需缴费或注册,欢迎参加! From bf1ea2395fdaea4ba1e9cf212695ab114734f480 Mon Sep 17 00:00:00 2001 From: Aston Zhang <22279212+astonzhang@users.noreply.github.com> Date: Fri, 7 May 2021 16:46:52 -0700 Subject: [PATCH 101/103] Update build.yml --- static/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/build.yml b/static/build.yml index ef0dd6939..936d550b3 100644 --- a/static/build.yml +++ b/static/build.yml @@ -5,9 +5,9 @@ dependencies: - .. # d2l - git+https://github.com/d2l-ai/d2l-book - mxnet-cu101==1.7.0 - - torch==1.8.0+cu101 + - torch==1.8.1+cu101 - -f https://download.pytorch.org/whl/torch_stable.html - - torchvision==0.9.0+cu101 + - torchvision==0.9.1+cu101 - -f https://download.pytorch.org/whl/torch_stable.html - tensorflow==2.3.1 - tensorflow-probability==0.11.1 From e3d093b30205dfdb8d18d732246ca2a9797a4d06 Mon Sep 17 00:00:00 2001 From: Aston Zhang Date: Sat, 8 May 2021 20:39:07 +0000 Subject: [PATCH 102/103] bump to 2.0.0-alpha1 --- config.ini | 2 +- d2l/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.ini b/config.ini index c73ac124f..0fbb88ac6 100644 --- a/config.ini +++ b/config.ini @@ -8,7 +8,7 @@ author = Aston Zhang, Zachary C. Lipton, Mu Li, and Alexander J. Smola copyright = 2021, All authors. Licensed under CC-BY-SA-4.0 and MIT-0. -release = 2.0.0-alpha0 +release = 2.0.0-alpha1 lang = zh diff --git a/d2l/__init__.py b/d2l/__init__.py index 63bc06bef..2bc1c3f56 100644 --- a/d2l/__init__.py +++ b/d2l/__init__.py @@ -8,4 +8,4 @@ """ -__version__ = "2.0.0-alpha0" +__version__ = "2.0.0-alpha1" From 52ff6ee209b4c9a4027cd05514b5d7da98e3fecd Mon Sep 17 00:00:00 2001 From: xiaotinghe Date: Sun, 9 May 2021 04:39:26 +0800 Subject: [PATCH 103/103] forum link (#805) --- chapter_computational-performance/async-computation.md | 4 ++-- chapter_computational-performance/auto-parallelism.md | 4 ++-- chapter_computational-performance/hardware.md | 2 +- chapter_computational-performance/hybridize.md | 6 +++--- chapter_computational-performance/multiple-gpus-concise.md | 4 ++-- chapter_computational-performance/multiple-gpus.md | 4 ++-- chapter_computational-performance/parameterserver.md | 2 +- chapter_recurrent-modern/beam-search.md | 2 +- chapter_recurrent-modern/bi-rnn.md | 4 ++-- chapter_recurrent-modern/deep-rnn.md | 4 ++-- chapter_recurrent-modern/encoder-decoder.md | 4 ++-- chapter_recurrent-modern/gru.md | 2 +- chapter_recurrent-modern/lstm.md | 4 ++-- chapter_recurrent-modern/machine-translation-and-dataset.md | 4 ++-- chapter_recurrent-modern/seq2seq.md | 4 ++-- 15 files changed, 27 insertions(+), 27 deletions(-) diff --git a/chapter_computational-performance/async-computation.md b/chapter_computational-performance/async-computation.md index 432dcd985..b80597790 100644 --- a/chapter_computational-performance/async-computation.md +++ b/chapter_computational-performance/async-computation.md @@ -208,9 +208,9 @@ Python前端线程和C++后端线程之间的简化交互可以概括如下: :end_tab: :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/361) +[Discussions](https://discuss.d2l.ai/t/2792) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/2564) +[Discussions](https://discuss.d2l.ai/t/2791) :end_tab: diff --git a/chapter_computational-performance/auto-parallelism.md b/chapter_computational-performance/auto-parallelism.md index d1c16b167..0a1ec4b96 100644 --- a/chapter_computational-performance/auto-parallelism.md +++ b/chapter_computational-performance/auto-parallelism.md @@ -181,9 +181,9 @@ with d2l.Benchmark('在GPU1上运行并复制到CPU'): 1. 设计包含更复杂数据依赖关系的计算任务,并运行实验,以查看是否可以在提高性能的同时获得正确的结果。 :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/362) +[Discussions](https://discuss.d2l.ai/t/2795) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1681) +[Discussions](https://discuss.d2l.ai/t/2794) :end_tab: diff --git a/chapter_computational-performance/hardware.md b/chapter_computational-performance/hardware.md index 7cd5f9f8d..08cdf75bd 100644 --- a/chapter_computational-performance/hardware.md +++ b/chapter_computational-performance/hardware.md @@ -214,4 +214,4 @@ GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得 1. 看看Turing T4 GPU的性能数字。为什么从FP16到INT8和INT4的性能只翻倍? 1. 从旧金山到阿姆斯特丹的往返旅行,一个网络包需要多长时间?提示:你可以假设距离为10000公里。 -[Discussions](https://discuss.d2l.ai/t/363) +[Discussions](https://discuss.d2l.ai/t/2798) diff --git a/chapter_computational-performance/hybridize.md b/chapter_computational-performance/hybridize.md index 637632b1a..dc8c9947a 100644 --- a/chapter_computational-performance/hybridize.md +++ b/chapter_computational-performance/hybridize.md @@ -380,13 +380,13 @@ net(x) :end_tab: :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/360) +[Discussions](https://discuss.d2l.ai/t/2789) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/2490) +[Discussions](https://discuss.d2l.ai/t/2788) :end_tab: :begin_tab:`tensorflow` -[Discussions](https://discuss.d2l.ai/t/2492) +[Discussions](https://discuss.d2l.ai/t/2787) :end_tab: diff --git a/chapter_computational-performance/multiple-gpus-concise.md b/chapter_computational-performance/multiple-gpus-concise.md index ede65944c..c8f7b58a4 100644 --- a/chapter_computational-performance/multiple-gpus-concise.md +++ b/chapter_computational-performance/multiple-gpus-concise.md @@ -264,9 +264,9 @@ train(net, num_gpus=2, batch_size=512, lr=0.2) :end_tab: :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/365) +[Discussions](https://discuss.d2l.ai/t/2804) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1403) +[Discussions](https://discuss.d2l.ai/t/2803) :end_tab: diff --git a/chapter_computational-performance/multiple-gpus.md b/chapter_computational-performance/multiple-gpus.md index d911856f4..5246cb06a 100644 --- a/chapter_computational-performance/multiple-gpus.md +++ b/chapter_computational-performance/multiple-gpus.md @@ -357,9 +357,9 @@ train(num_gpus=2, batch_size=256, lr=0.2) 1. 实现多GPU测试精度计算。 :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/364) +[Discussions](https://discuss.d2l.ai/t/2801) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1669) +[Discussions](https://discuss.d2l.ai/t/2800) :end_tab: diff --git a/chapter_computational-performance/parameterserver.md b/chapter_computational-performance/parameterserver.md index b8aa9b924..284035f33 100644 --- a/chapter_computational-performance/parameterserver.md +++ b/chapter_computational-performance/parameterserver.md @@ -98,4 +98,4 @@ $$\mathbf{g}_{i} = \sum_{k \in \text{workers}} \sum_{j \in \text{GPUs}} \mathbf{ 1. 是否可以允许异步通信(而计算仍在进行)?它如何影响性能? 1. 如果我们在长时间运行的计算过程中丢失了一台服务器,该怎么办?我们如何设计一种容错机制来避免完全重新启动计算? -[Discussions](https://discuss.d2l.ai/t/366) +[Discussions](https://discuss.d2l.ai/t/2807) diff --git a/chapter_recurrent-modern/beam-search.md b/chapter_recurrent-modern/beam-search.md index 13be01b4c..7e6826e6f 100644 --- a/chapter_recurrent-modern/beam-search.md +++ b/chapter_recurrent-modern/beam-search.md @@ -71,4 +71,4 @@ $$ \frac{1}{L^\alpha} \log P(y_1, \ldots, y_{L}) = \frac{1}{L^\alpha} \sum_{t'=1 1. 在 :numref:`sec_seq2seq` 的机器翻译问题中应用束搜索。束宽如何影响结果和预测速度? 1. 在 :numref:`sec_rnn_scratch` 中,我们使用语言模型来生成用户提供前缀的文本。它使用了哪种搜索策略?你能改进吗? -[Discussions](https://discuss.d2l.ai/t/338) \ No newline at end of file +[Discussions](https://discuss.d2l.ai/t/2786) \ No newline at end of file diff --git a/chapter_recurrent-modern/bi-rnn.md b/chapter_recurrent-modern/bi-rnn.md index 0fe479c42..8c976b809 100644 --- a/chapter_recurrent-modern/bi-rnn.md +++ b/chapter_recurrent-modern/bi-rnn.md @@ -166,9 +166,9 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) 1. 一词多义在自然语言中很常见。例如,“bank”一词在“i went to the bank to deposit cash”和“i went to the bank to sit down”中有不同的含义。我们如何设计一个神经网络模型,使其在给定上下文序列和单词的情况下,返回该单词在上下文中的向量表示?哪种类型的神经结构更适合处理一词多义? :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/339) +[Discussions](https://discuss.d2l.ai/t/2774) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1059) +[Discussions](https://discuss.d2l.ai/t/2773) :end_tab: diff --git a/chapter_recurrent-modern/deep-rnn.md b/chapter_recurrent-modern/deep-rnn.md index 77de616f8..126e2d20b 100644 --- a/chapter_recurrent-modern/deep-rnn.md +++ b/chapter_recurrent-modern/deep-rnn.md @@ -97,9 +97,9 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) 4. 在为文本建模时,是否要合并不同作者的来源?为什么这是个好主意?会出什么问题? :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/340) +[Discussions](https://discuss.d2l.ai/t/2771) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1058) +[Discussions](https://discuss.d2l.ai/t/2770) :end_tab: diff --git a/chapter_recurrent-modern/encoder-decoder.md b/chapter_recurrent-modern/encoder-decoder.md index 86d57b082..96bfa99d4 100644 --- a/chapter_recurrent-modern/encoder-decoder.md +++ b/chapter_recurrent-modern/encoder-decoder.md @@ -121,9 +121,9 @@ class EncoderDecoder(nn.Module): 1. 除了机器翻译,你能想到另一个可以适用于”编码器-解码器“结构的应用吗? :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/341) +[Discussions](https://discuss.d2l.ai/t/2780) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1061) +[Discussions](https://discuss.d2l.ai/t/2779) :end_tab: diff --git a/chapter_recurrent-modern/gru.md b/chapter_recurrent-modern/gru.md index d4f7a4285..f6de1de49 100644 --- a/chapter_recurrent-modern/gru.md +++ b/chapter_recurrent-modern/gru.md @@ -241,7 +241,7 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) 1. 如果你只实现门控循环单元的一部分,例如,只有一个重置门或只有一个更新门,会发生什么情况? :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/342) +[Discussions](https://discuss.d2l.ai/t/2764) :end_tab: :begin_tab:`pytorch` diff --git a/chapter_recurrent-modern/lstm.md b/chapter_recurrent-modern/lstm.md index c43b32128..110312b95 100644 --- a/chapter_recurrent-modern/lstm.md +++ b/chapter_recurrent-modern/lstm.md @@ -255,9 +255,9 @@ d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) 1. 为时间序列预测而不是字符序列预测实现LSTM模型。 :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/343) +[Discussions](https://discuss.d2l.ai/t/2766) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1057) +[Discussions](https://discuss.d2l.ai/t/2768) :end_tab: diff --git a/chapter_recurrent-modern/machine-translation-and-dataset.md b/chapter_recurrent-modern/machine-translation-and-dataset.md index 53f8c7e8f..c9fe6fc36 100644 --- a/chapter_recurrent-modern/machine-translation-and-dataset.md +++ b/chapter_recurrent-modern/machine-translation-and-dataset.md @@ -202,9 +202,9 @@ for X, X_valid_len, Y, Y_valid_len in train_iter: 1. 某些语言(例如中文和日语)的文本没有单词边界指示符(例如,空格)。对于这种情况,单词级标记化仍然是个好主意吗?为什么? :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/344) +[Discussions](https://discuss.d2l.ai/t/2777) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1060) +[Discussions](https://discuss.d2l.ai/t/2776) :end_tab: \ No newline at end of file diff --git a/chapter_recurrent-modern/seq2seq.md b/chapter_recurrent-modern/seq2seq.md index 65a8888e1..5b309ea2d 100644 --- a/chapter_recurrent-modern/seq2seq.md +++ b/chapter_recurrent-modern/seq2seq.md @@ -550,9 +550,9 @@ for eng, fra in zip(engs, fras): 1. 有没有其他方法来设计解码器的输出层? :begin_tab:`mxnet` -[Discussions](https://discuss.d2l.ai/t/345) +[Discussions](https://discuss.d2l.ai/t/2783) :end_tab: :begin_tab:`pytorch` -[Discussions](https://discuss.d2l.ai/t/1062) +[Discussions](https://discuss.d2l.ai/t/2782) :end_tab: \ No newline at end of file