《动手学深度学习》 Pytorch ver. Part A
- 《动手学深度学习》原书地址:https://github.com/d2l-ai/d2l-zh
- 《动手学深度学习》(Pytorch ver.):https://tangshusen.me/Dive-into-DL-PyTorch/#/
知识架构:
本文的主要作用是在阅读过程中做一些摘录。对于「机器学习」领域,reta 虽然曾尝试从各个领域入门,也尝试训过一些模型,但是还是缺少系统性、结构性的学习。希望阅读本书能带来更多的收获吧。
与前面的一些笔记相比,本文更加侧重于「实践」。也就是说切实地提升自己的代码能力。
Part A 包含:
- § 1 深度学习简介
- § 2 预备知识:Pytorch
- § 3 深度学习基础
- 线性回归,Softmax 回归,多层感知机三类基本模型
- 权重衰减和 Dropout 两类应对过拟合的方法
- § 4 深度学习计算
- 构造 Pytorch 模型的方式
- 模型参数的访问、初始化与共享
- 自定义 Layer
- 读取与存储
- GPU 计算
深度学习简介
- 机器学习与深度学习的关系
机器学习研究如何使计算机系统利用经验改善性能。它是人工智能领域的分支,也是实现人工智能的一种手段。
在机器学习的众多研究方向中,表征学习关注如何自动找出表示数据的合适方式,以便更好地将输入变换为正确的输出。
而本书要重点探讨的深度学习是具有多级表示的表征学习方法。
在每一级(从原始数据开始),深度学习通过简单的函数将该级的表示变换为更高级的表示。因此,深度学习模型也可以看作是由许多简单函数复合而成的函数。当这些复合的函数足够多时,深度学习模型就可以表达非常复杂的变换。
- 深度学习的一个外在特点:End-to-end
深度学习的一个外在特点是端到端的训练。也就是说,并不是将单独调试的部分拼凑起来组成一个系统,而是将整个系统组建好之后一起训练。
比如说,计算机视觉科学家之前曾一度将特征抽取与机器学习模型的构建分开处理,像是Canny边缘探测和SIFT特征提取曾占据统治性地位达10年以上,但这也就是人类能找到的最好方法了。
当深度学习进入这个领域后,这些特征提取方法就被性能更强的自动优化的逐级过滤器替代了。
预备知识: Pytorch
数据操作
- 对 Tensor 的操作:Tensor 的创建
函数 | 功能 |
---|---|
Tensor(*sizes) |
基础构造函数 |
tensor(data,) |
类似 np.array 的构造函数 |
ones(*sizes) |
全 1 Tensor |
zeros(*sizes) |
全 0 Tensor |
eye(*sizes) |
对角线为 1,其他为 0 |
arange(s,e,step) |
从 s 到 e,步长为 step |
linspace(s,e,steps) |
从 s 到 e,均匀切分成 steps 份 |
rand/randn(*sizes) |
均匀/标准分布 |
normal(mean,std)/uniform(from,to) |
正态分布/均匀分布 |
randperm(m) |
随机排列 |
- 对 Tensor 进行操作时注意其可能的数据共享
如使用 view()
改变 Tensor 的形状的时候,注意返回的新 Tensor 与源 Tensor 虽然可能有不同的 size,但是是共享 data 的。
所以如果我们想返回一个真正新的副本(即不共享 data 内存)该怎么办呢?Pytorch 还提供了一个 reshape()
可以改变形状,但是此函数并不能保证返回的是其拷贝,所以不推荐使用。推荐先用 clone
创造一个副本然后再使用 view
。
注:虽然 view
返回的 Tensor
与源 Tensor
是共享 data
的,但是依然是一个新的 Tensor
(因为 Tensor
除了包含 data
外还有一些其他属性),二者 id
(内存地址)并不一致。
另外一个常用的函数就是 item()
, 它可以将一个标量 Tensor
转换成一个 Python Number。
- 广播机制 Broadcasting
当对两个形状不同的 Tensor
按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个 Tensor
形状相同后再按元素运算。例如:
1 | import torch |
Tensor
与ndarray
的转换
很容易用 numpy()
和 from_numpy()
将 Tensor
和 NumPy 中的数组相互转换。但是需要注意的一点是:两个函数所产生的 Tensor
和 NumPy 中的数组共享相同的内存(所以它们之间的转换很快),改变其中一个时另一个也会改变。
与之相对比,还有一个常用的将 NumPy 中的 array 转换成 Tensor
的方法就是 torch.tensor()
, 需要注意的是,此方法总是会进行数据拷贝(就会消耗更多的时间和空间),所以返回的 Tensor
和原来的数据不再共享内存。
所有在CPU上的 Tensor
(除了 CharTensor
)都支持与 NumPy 数组相互转换。
Tensor
on GPU
用方法 to()
可以将 Tensor
在 CPU 和 GPU(需要硬件支持)之间相互移动。
1 | # 以下代码只有在PyTorch GPU版本上才会执行 |
Autograd
Tensor
的requires_grad
与Function
上一节介绍的 Tensor
是这个包的核心类,如果将其属性 requires_grad
设置为 True
,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。
完成计算后,可以调用 backward()
来完成所有梯度计算。此 Tensor
的梯度将累积到 .grad
属性中。
如果不想要被继续追踪,可以调用 .detach()
将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,这样梯度就传不过去了。此外,还可以用 with torch.no_grad()
将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True
)的梯度。
Function
是另外一个很重要的类。Tensor
和 Function
互相结合就可以构建一个记录有整个计算过程的有向无环图。每个 Tensor
都有一个 grad_fn
属性,该属性即创建该 Tensor
的 Function
, 就是说该 Tensor
是不是通过某些运算得到的,若是,则 grad_fn
返回一个与这些运算相关的对象,否则是 None。
1 | 2, 2, requires_grad=True) x = torch.ones( |
注意 x 是直接创建的,所以它没有 grad_fn
, 而 y 是通过一个加法操作创建的,所以它有一个为 <AddBackward>
的 grad_fn
。像 x 这种直接创建的称为叶子节点,叶子节点对应的 grad_fn
是 None
。
1 | 2, 2) # 缺失情况下默认 requires_grad = False a = torch.randn( |
backward()
因为 out
是一个标量,所以调用 backward()
时不需要指定求导变量:
1 | # 等价于 out.backward(torch.tensor(1.)) out.backward() |
本质上,反向传播的过程是在计算一系列 Jacobi 矩阵的乘积。
注意:grad 在反向传播过程中是累加的,这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。
1 | # 再来反向传播一次,注意grad是累加的 |
注:PyTorch 的 backward
为什么有一个 grad_variables
参数?
假设 x 经过一番计算得到 y,那么 y.backward(w)
求的不是 y 对 x 的导数,而是 l = torch.sum(y*w)
对 x 的导数。w 可以视为 y 的各分量的权重,也可以视为遥远的损失函数 l 对 y 的偏导数(这正是函数说明文档的含义)。特别地,若 y 为标量,w 取默认值 1.0,才是按照我们通常理解的那样,求 y 对 x 的导数。
- 中断梯度传播
1 | x = torch.tensor(1.0, requires_grad=True) |
可以看到,上面的 y2
是没有 grad_fn
而且 y2.requires_grad=False
的,而 y3
是有 grad_fn
的。如果我们将y3
对x
求梯度的话:
1 | y3.backward() |
正如我们所理解的,$y_3=y_1+y_2=x^2+x^3$,其中 $y_2$ 的梯度不被回传,因此$\frac{\mathrm{d} y_3}{\mathrm{d} x}=2x $
此外,如果我们想要修改 Tensor
的数值,但是又不希望被 autograd
记录(即不会影响反向传播),那么我么可以对 tensor.data
进行操作。
1 | x = torch.ones(1,requires_grad=True) |
深度学习基础
基础模型
Linear Regression
首先,回归问题的输出是连续值,而分类问题的输出是离散值,这是二者的区别。
- 模型定义:假设我们采集的样本数为 $n$,索引为 $i$ 的样本的特征为 $x_1^{(i)}$ 和 $x_2^{(i)}$,标签为 $y^{(i)}$。对于索引为 $i$ 的房屋,线性回归模型的房屋价格预测表达式为 $\hat{y}^{(i)}=x_1^{(i)}w_1+x_2^{(i)}w_2+b$
- 损失函数:$\ell^{(i)}(w_1,w_2,b)=\frac{1}{2}(\hat{y}^{(i)}−y^{(i)})^2$, $\ell(w_1,w_2,b)=\frac{1}{n}{\textstyle \sum_{i=1}^{n}}\ell^{(i)}(w_1,w_2,b)=\frac{1}{n}{\textstyle \sum_{i=1}^{n}}\frac{1}{2}(x_1^{(i)}w_1+x_2^{(i)}w_2+b−y^{(i)})^2$
在模型训练中,我们希望找出一组模型参数,记为 $w_1^{}$, $w_2^{}$, $b^{}$,来使训练样本平均损失最小:,来使训练样本平均损失最小: $w_1^{}$, $w_2^{}$, $b^{} = \underset{w_1, w_2, b}{\arg\min} \ell(w_1, w_2, b) $
- 优化算法
当模型和损失函数形式较为简单时,上面的误差最小化问题的解可以直接用公式表达出来。这类解叫作解析解。本节使用的线性回归和平方误差刚好属于这个范畴。然而,大多数深度学习模型并没有解析解,只能通过优化算法有限次迭代模型参数来尽可能降低损失函数的值。这类解叫作数值解。
在求数值解的优化算法中,小批量随机梯度下降(Mini-batch SGD, mini-batch stochastic gradient descent)在深度学习中被广泛使用。它的算法很简单:先选取一组模型参数的初始值,如随机选取;接下来对参数进行多次迭代,使每次迭代都可能降低损失函数的值。在每次迭代中,先随机均匀采样一个由固定数目训练数据样本所组成的小批量(mini-batch)$\mathcal{B} $,然后求小批量中数据样本的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数 learning_rate $\eta$ 的乘积作为模型参数在本次迭代的减小量。
在训练本节讨论的线性回归模型的过程中,模型的每个参数将作如下迭代:
在上式中,$\left | \mathcal{B} \right | $ 代表每个小批量中的样本个数(批量大小,batch size),$\eta$ 称作学习率(learning rate)并取正数。需要强调的是,这里的批量大小和学习率的值是人为设定的,并不是通过模型训练学出的,因此叫作超参数(hyperparameter)。我们通常所说的“调参”指的正是调节超参数,例如通过反复试错来找到超参数合适的值。
实现
- 生成数据集
1 | num_inputs = 2 |
- 读取数据
1 | import torch.utils.data as Data |
1 | for X, y in data_iter: |
- 定义模型
首先,导入 torch.nn
模块。实际上,“nn”是neural networks(神经网络)的缩写。
顾名思义,该模块定义了大量神经网络的层。之前我们已经用过了 autograd
,而 nn
就是利用 autograd
来定义模型。
nn
的核心数据结构是 Module
,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承 nn.Module
,撰写自己的网络/层。
一个 nn.Module
实例应该包含一些层以及返回输出的前向传播(forward)方法。下面先来看看如何用 nn.Module
实现一个线性回归模型。
1 | class LinearNet(nn.Module): |
事实上我们还可以用nn.Sequential
来更加方便地搭建网络,Sequential
是一个有序的容器,网络层将按照在传入Sequential
的顺序依次被添加到计算图中。
1 | # 写法一 |
可以通过 net.parameters()
来查看模型所有的可学习参数,此函数将返回一个生成器。
1 | for param in net.parameters(): |
输出:
1 | Parameter containing: |
注意:torch.nn
仅支持输入一个 batch 的样本,而不支持单个样本输入,如果只有单个样本,可使用 input.unsqueeze(0)
来添加一维。
附:unsqueeze()
的使用:
1 | 1,5,3)) a = torch.randn(( |
- 初始化模型参数
1 | from torch.nn import init |
- 定义损失函数
PyTorch 在 nn
模块中提供了各种损失函数,这些损失函数可看作是一种特殊的层,PyTorch 也将这些损失函数实现为 nn.Module
的子类。我们现在使用它提供的均方误差损失作为模型的损失函数。
1 |
|
- 定义优化算法
同样,我们也无须自己实现小批量随机梯度下降算法。torch.optim
模块提供了很多常用的优化算法比如 SGD、Adam 和 RMSProp 等。下面我们创建一个用于优化 net
所有参数的优化器实例,并指定学习率为 0.03 的小批量随机梯度下降(SGD)为优化算法。
1 | import torch.optim as optim |
输出:
1 | SGD ( |
我们还可以为不同子网络设置不同的学习率,这在 fine-tune 时经常用到。例:
1 | optimizer =optim.SGD([ |
有时候我们不想让学习率固定成一个常数,那如何调整学习率呢?主要有两种做法。一种是修改 optimizer.param_groups
中对应的学习率,另一种是更简单也是较为推荐的做法 —— 新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的 optimizer。但是后者对于使用动量的优化器(如 Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。
1 | # 调整学习率 |
- 训练模型
通过调用 optim
实例的 step
函数来迭代模型参数。按照小批量随机梯度下降的定义,我们在 step
函数中指明批量大小,从而对批量中样本梯度求平均。
1 | num_epochs = 3 |
输出:
1 | epoch 1, loss: 0.000457 |
Softmax Regression
线性回归模型适用于输出为连续值的情景。
在另一类情景中,模型输出可以是一个像图像类别这样的离散值。对于这样的离散值预测问题,我们可以使用诸如 softmax 回归在内的分类模型。
和线性回归不同,softmax 回归的输出单元从一个变成了多个,且引入了 softmax 运算使输出更适合离散值的预测和训练。本节以 softmax 回归模型为例,介绍神经网络中的分类模型。
softmax 回归跟线性回归一样将输入特征与权重做线性叠加。与线性回归的一个主要不同在于,softmax 回归的输出值个数等于标签里的类别数。
- 模型定义
其中:
- 损失函数:Cross Entropy交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。
假设训练数据集的样本数为 n,交叉熵损失函数定义为 $\ell(\mathbf{\Theta})=\frac{1}{n}\sum_{i=1}^{n}H(\mathbf{y}^{(i)},\mathbf{\hat{y}}^{(i)}).$ 其中$\mathbf{\Theta}$代表模型参数。
注意:这里的交叉熵是 H = - [ 实际值 * log(预测值) ] 的求和。而 log(预测值) 却又可以替换为是预测时的 logits oi,二者之间只差了一个系数。因此背后实现可以直接用 logits 参与简化计算。
数据集与相关包的介绍:Fashion-MNIST
本节我们将使用 torchvision 包,它是服务于 PyTorch 深度学习框架的,主要用来构建计算机视觉模型。 torchvision 主要由以下几部分构成:
torchvision.datasets
: 一些加载数据的函数及常用的数据集接口;torchvision.models
: 包含常用的模型结构(含预训练模型),例如 AlexNet、VGG、ResNet 等;torchvision.transforms
: 常用的图片变换,例如裁剪、旋转等;torchvision.utils
: 其他的一些有用的方法。
感知机
- 仅添加隐藏层:即便再添加更多的隐藏层,将线性隐藏层间彼此相接依然只能与仅含输出层的单层神经网络等价。
上述问题的根源在于全连接层只是对数据做仿射变换(affine transformation),而多个仿射变换的叠加仍然是一个仿射变换。
解决问题的一个方法是引入非线性变换,例如对隐藏变量使用按元素运算的非线性函数进行变换,然后再作为下一个全连接层的输入。
这个非线性函数被称为激活函数(activation function)。下面我们介绍几个常用的激活函数。
- $Relu(x)=\mathrm{max}(0,x).$
- $sigmoid(x)=\sigma(x)=\frac{1}{1+e^{−x}}.$
- $\tanh(x)=\frac{1−\exp(−2x)}{1+\exp(−2x)}.$
多层感知机就是含有至少一个隐藏层的由全连接层组成的神经网络,且每个隐藏层的输出通过激活函数进行变换。多层感知机的层数和各隐藏层中隐藏单元个数都是超参数。
应对过拟合
L2 Regularization
- 权重衰减是一种应对过拟合的方法
- 权重衰减等价于 $L_2$ 范数正则化(regularization)。正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。我们先描述 $L_2$ 范数正则化,再解释它为何又称权重衰减。
$L_2$ 范数正则化在模型原损失函数基础上添加 $L_2$ 范数惩罚项,从而得到训练所需要最小化的函数。
$L_2$ 范数惩罚项指的是模型权重参数每个元素的平方和与一个正的常数的乘积。即定义新的 Loss Function 为:
为什么 $L_2$ Regularization 能起到“权重衰减”的作用呢?我们考虑 Optimizer 的迭代方式…
这是因为如果我们对损失函数求梯度,就必然带有了和原有的 $w$ 有关的一项。然后我们在做优化迭代的过程中,$L_2$ 范数正则化令权重 $w_1$ 和 $w_2$ 先自乘小于 1 的数,再减去不含惩罚项的梯度。因此,$L_2$ 范数正则化又叫权重衰减。
权重衰减通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能对过拟合有效。实际场景中,我们有时也在惩罚项中添加偏差元素的平方和。
实现
- 定义 L2 范数惩罚项
1 | def l2_penalty(w): |
- Train (From scratch)
1 | def fit_and_plot(lambd): |
- Train (Simple)
这里我们直接在构造优化器实例时通过 weight_decay
参数来指定权重衰减超参数。默认下,PyTorch 会对权重和偏差同时衰减。我们可以分别对权重和偏差构造优化器实例,从而只对权重衰减。
1 | def fit_and_plot_pytorch(wd): |
Dropout
除了前一节介绍的权重衰减以外,深度学习模型常常使用丢弃法(Dropout)来应对过拟合问题。丢弃法有一些不同的变体。本节中提到的丢弃法特指倒置丢弃法(inverted dropout)。
当对某个隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为 $p$,那么有 $p$ 的概率其输出 $h_i$ 会被清零,有 $1 - p$ 的概率 $h_i$ 会除以 $1 − p$ 做拉伸。
丢弃概率是丢弃法的超参数。具体来说,设随机变量 $\xi_i$ 为 0 和 1 的概率分别为 $p$ 和 $1 − p$ 。使用丢弃法时我们计算新的隐藏单元 $h_i^′=\frac{\xi_i}{1−p}h_i$,由于 $E(\xi_i)=1−p$,因此
即丢弃法不改变其输入的期望值。
- 实现(Simple)
1 | net = nn.Sequential( |
- 数值稳定性
深度模型有关数值稳定性的典型问题是衰减(vanishing)和爆炸(explosion)。
当神经网络的层数较多时,模型的数值稳定性容易变差。假设一个层数为 $L$ 的多层感知机的第 $l$ 层 $\mathbf H^{(l)}$ 的权重参数为 $\mathbf W^{(l)}$,输出层 $\mathbf H^{(l)}$ 的权重参数为 $\mathbf W^{(l)}$。
为了便于讨论,不考虑偏差参数,且设所有隐藏层的激活函数为恒等映射 ϕ(x)=x。
给定输入 X,多层感知机的第 l 层的输出 $\mathbf H^{(l)}=\mathbf{XW}^{(1)}\mathbf{W}^{(2)}\dots\mathbf{W}^{(l)}$。
此时,如果层数l较大,$\mathbf H^{(l)}$ 的计算可能会出现衰减或爆炸。
举个例子,假设输入和所有层的权重参数都是标量,如权重参数为 0.2 和 5,多层感知机的第 30 层输出为输入 $\mathbf X$ 分别与 $0.2^{30}\approx 1\times10^{−21}$(衰减)和 $5^{30}\approx9\times 10^{20}$(爆炸)的乘积。
类似地,当层数较多时,梯度的计算也更容易出现衰减或爆炸。
- 模型初始化
在神经网络中,通常需要随机初始化模型参数。下面我们来解释这样做的原因。
考虑多层感知机模型。如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。
在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。
之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有 1 个隐藏单元在发挥作用。
因此,正如在前面的实验中所做的那样,我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。
PyTorch 中
nn.Module
的模块参数都采取了较为合理的初始化策略。
深度学习计算
模型构造
- 可以通过继承
Module
类来构造模型,重载__init__
函数和forward
函数。
1 | class MLP(nn.Module): |
Sequential
、ModuleList
、ModuleDict
类都继承自Module
类。
Sequential
1 | net = MySequential( |
ModuleList
1 | net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()]) |
ModuleList
仅仅是一个储存各种模块的列表,这些模块之间没有联系也没有顺序(所以不用保证相邻层的输入输出维度匹配),而且没有实现 forward
功能需要自己实现,所以上面执行 net(torch.zeros(1, 784))
会报NotImplementedError
;而 Sequential
内的模块需要按照顺序排列,要保证相邻层的输入输出大小相匹配,内部 forward
功能已经实现。此外,ModuleList
不同于一般的 Python 的 list
,加入到 ModuleList
里面的所有模块的参数会被自动添加到整个网络中。
ModuleDict
ModuleDict
接收一个子模块的字典作为输入, 然后也可以类似字典那样进行添加访问操作。
1 | net = nn.ModuleDict({ |
和 ModuleList
一样,ModuleDict
实例仅仅是存放了一些模块的字典,并没有定义 forward
函数需要自己定义。同样,ModuleDict
也与Python的 Dict
有所不同,ModuleDict
里的所有模块的参数会被自动添加到整个网络中。
- 与
Sequential
不同,ModuleList
和ModuleDict
并没有定义一个完整的网络,它们只是将不同的模块存放在一起,需要自己定义forward
函数。 - 虽然
Sequential
等类可以使模型构造更加简单,但直接继承Module
类可以极大地拓展模型构造的灵活性。
1 | class FancyMLP(nn.Module): |
模型参数的访问、初始化与共享
本节用到的模型:
1 | import torch |
- 访问模型参数
对于 Sequential
实例(派生自 Module
)中含模型参数的层,我们可以通过 Module
类的 parameters()
或者 named_parameters
方法来访问所有参数(以迭代器的形式返回),后者除了返回参数 Tensor
外还会返回其名字。
1 | print(type(net.named_parameters())) |
接下来访问单层的参数。对于使用 Sequential
类构造的神经网络,我们可以通过方括号 []
来访问网络的任一层。索引 0 表示隐藏层为 Sequential
实例最先添加的层。
1 | for name, param in net[0].named_parameters(): |
返回的 param
的类型为 torch.nn.parameter.Parameter
,其实这是 Tensor
的子类,和 Tensor
不同的是如果一个 Tensor
是 Parameter
,那么它会自动被添加到模型的参数列表里。
1 | class MyModel(nn.Module): |
- 初始化模型参数
PyTorch 中 nn.Module
的模块参数都采取了较为合理的初始化策略,但我们经常需要使用其他方法来初始化权重。
PyTorch 的 init
模块里提供了多种预设的初始化方法。在下面的例子中,我们将权重参数初始化成均值为 0、标准差为 0.01 的正态分布随机数,并依然将偏差参数清零。
1 | for name, param in net.named_parameters(): |
下面使用常数来初始化权重参数。
1 | for name, param in net.named_parameters(): |
有时候我们需要的初始化方法并没有在 init
模块中提供。这时,可以实现一个初始化方法,从而能够像使用其他初始化方法那样使用它。
首先参考 normal_ 的实现:
1 | def normal_(tensor, mean=0, std=1): |
类似地,我们可以实现自定义的初始化方法:
1 | def init_weight_(tensor): |
此外,我们还可以通过直接改变这些参数的 data
来达到改写模型参数值的同时不会影响梯度的效果。
- 共享模型参数
在有些情况下,我们希望在多个层之间共享模型参数。
共享模型参数的方法: Module
类的 forward
函数里多次调用同一个层。
此外,如果我们传入 Sequential
的模块是同一个 Module
实例的话参数也是共享的。
1 | linear = nn.Linear(1, 1, bias=False) |
因为模型参数里包含了梯度,所以在反向传播计算时,这些共享的参数的梯度是累加的。
自定义 Layer
本节将介绍如何使用 Module
来自定义层,从而可以被重复调用。
- 不含模型参数的自定义层
事实上,自定义层和自定义模型类似,因为我们可以直接把一个 Packed 的模型视为是一个 Layer。
举个例子,下面的 CenteredLayer
类通过继承 Module
类自定义了一个将输入减掉均值后输出的层,并将层的计算定义在了 forward
函数里。这个层里不含模型参数。
1 | import torch |
然后,我们就可以实例化这个 Layer,然后做 Forward Feeding.
1 | layer = CenteredLayer() |
- 含模型参数的自定义层
之前我们介绍了 Parameter
类其实是 Tensor
的子类,如果一个 Tensor
是 Parameter
,那么它会自动被添加到模型的参数列表里。 // 在这里可以推测这个参数列表是 nn.Module
的数据成员…?
所以在自定义含模型参数的层时,我们应该将参数定义成 Parameter
。
除了直接定义成 Parameter
类外,还可以使用 ParameterList
和 ParameterDict
分别定义参数的列表和字典。
ParameterList
ParameterList
接收一个 Parameter
实例的列表作为输入然后得到一个参数列表,使用的时候可以用索引来访问某个参数,另外也可以使用 append
和 extend
在列表后面新增参数。
1 | class MyDense(nn.Module): |
Output:
1 | MyDense( |
ParameterDict
ParameterDict
接收一个 Parameter
实例的字典作为输入然后得到一个参数字典,然后可以按照字典的规则使用了。
例如使用 update()
新增参数,使用 keys()
返回所有键值,使用 items()
返回所有键值对等等。
1 | class MyDictDense(nn.Module): |
Output:
1 | MyDictDense( |
于是我们可以根据不同的 key 进行不同的 forward feeding.
1 | x = torch.ones(1, 4) |
读取与存储
到目前为止,我们介绍了如何处理数据以及如何构建、训练和测试深度学习模型。
然而在实际中,我们有时需要把训练好的模型部署到很多不同的设备。
在这种情况下,我们可以把内存中训练好的模型参数存储在硬盘上供后续读取使用。
- 读写
Tensor
我们可以直接使用 save
函数和 load
函数分别存储和读取 Tensor
。
save
使用 Python 的 pickle 库将对象进行序列化,然后将序列化的对象保存到硬盘。
使用 save
可以保存各种对象,包括 nn.Module
, Tensor
, dict
等等。
而 load
使用 unpickle 工具将 pickle 的对象文件反序列化为内存。
1 | import torch |
- 读写模型
PyTorch 中保存和加载训练模型有两种常见的方法:
- 仅保存和加载模型参数(
state_dict
); - 保存和加载整个模型。
保存和加载模型的 state_dict()
成员(Recommended)
保存:
1 | torch.save(model.state_dict(), PATH) # 推荐的文件后缀名是 pt 或 pth |
加载:
1 | model = TheModelClass(*args, **kwargs) |
GPU 计算
到目前为止,我们一直在使用 CPU 计算。
对复杂的神经网络和大规模的数据来说,使用 CPU 来计算可能不够高效。
在本节中,我们将介绍如何使用单块 NVIDIA GPU 来计算。
可以通过 nvidia-smi
命令来查看显卡信息。
1 | Wed Jan 26 11:48:28 2022 |
- 计算设备
PyTorch 可以指定用来存储和计算的设备,如使用内存的 CPU 或者使用显存的 GPU。
默认情况下,PyTorch 会将数据创建在内存,然后利用 CPU 来计算。
用 torch.cuda.is_available()
查看 GPU 是否可用:
1 | import torch |
GPU 的相关信息查询:
1 | torch.cuda.device_count() # 查看 GPU 数量,输出 1 |
Tensor
的 GPU 计算
默认情况下,Tensor
会被存在内存上。因此,之前我们每次打印 Tensor
的时候看不到 GPU 相关标识。
1 | x = torch.tensor([1, 2, 3]) |
使用 .cuda()
可以将CPU上的 Tensor
转换(复制)到GPU上。
如果有多块GPU,我们用 .cuda(i)
来表示第 i 块 GPU 及相应的显存(i 从 0 开始)且 cuda(0)
和 cuda()
等价。
1 | x = x.cuda(0) |
可以通过 Tensor
的 device
属性来查看该 Tensor
所在的设备。
1 |
|
可以直接在创建的时候就指定设备。
1 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
如果对在 GPU 上的数据进行运算,那么结果还是存放在 GPU 上。
1 | y = x**2 |
需要注意的是,存储在不同位置中的数据是不可以直接进行计算的。即存放在 CPU 上的数据不可以直接与存放在 GPU 上的数据进行运算,位于不同 GPU 上的数据也是不能直接进行计算的。
- 模型的 GPU 计算
同 Tensor
类似,PyTorch 模型也可以用类似的方式转移到 GPU 上。
.cuda(i)
.cpu()
.to(device)
我们也可以通过检查模型的参数的 device
属性来查看存放模型的设备。
同样的,需要保证模型输入的 Tensor
和模型都在同一设备上,否则会报错。