「Pytorch」基础概念与入门
本文整理自 PyTorch 深度学习:60分钟快速入门
原文:https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
0x00 Why Pytorch?
PyTorch 是基于以下两个目的而打造的python科学计算框架:
- 无缝替换NumPy,并且通过利用GPU的算力来实现神经网络的加速。
- 通过自动微分机制,来让神经网络的实现变得更加容易。
0x01 What is Tensor?
张量如同数组和矩阵一样,是一种特殊的数据结构。在PyTorch
中, 神经网络的输入、输出以及网络的参数等数据,都是使用张量来进行描述。
张量的使用和 Numpy
中的 ndarrays
很类似,区别在于张量可以在 GPU
或其它专用硬件上运行, 这样可以得到更快的加速效果。如果你对ndarrays
很熟悉的话, 张量的使用对你来说就很容易了。如果不太熟悉的话,希望这篇有关张量API
的快速入门教程能够帮到你。
1 | import torch |
1. Tensor的初始化
张量有很多种不同的初始化方法, 先来看看四个简单的例子:
1. 直接生成张量
由原始数据直接生成张量, 张量类型由原始数据类型决定。
1 | data = [[1, 2], [3, 4]] |
2. 通过Numpy数组来生成张量
由已有的Numpy
数组来生成张量(反过来也可以由张量来生成Numpy
数组, 参考张量与Numpy之间的转换)。
1 | np_array = np.array(data) |
3. 通过已有的张量来生成新的张量
新的张量将继承已有张量的数据属性(结构、类型), 也可以重新指定新的数据类型。
1 | x_ones = torch.ones_like(x_data) # 保留 x_data 的属性 |
输出:
1 | Ones Tensor: |
4. 通过指定数据维度来生成张量
shape
是元组类型, 用来描述张量的维数, 下面3个函数通过传入shape
来指定生成张量的维数。
1 | shape = (2,3,) |
输出:
1 | Random Tensor: |
2. Tensor的属性
从张量属性我们可以得到张量的维数、数据类型以及它们所存储的设备(CPU或GPU)。
来看一个简单的例子:
1 | tensor = torch.rand(3,4) |
输出:
1 | Shape of tensor: torch.Size([3, 4]) # 维数 |
3. Tensor运算
有超过100种张量相关的运算操作, 例如转置、索引、切片、数学运算、线性代数、随机采样等。更多的运算可以在这里查看。
所有这些运算都可以在GPU上运行:
1 | # 判断当前环境GPU是否可用, 然后将tensor导入GPU内运行 |
光说不练假把式,接下来的例子一定要动手跑一跑。如果你对Numpy的运算非常熟悉的话, 那tensor的运算对你来说就是小菜一碟。
1. 张量的索引和切片
1 | tensor = torch.ones(4, 4) |
显示:
1 | tensor([[1., 0., 1., 1.], |
2. 张量的拼接
你可以通过torch.cat
方法将一组张量按照指定的维度进行拼接, 也可以参考torch.stack
方法。这个方法也可以实现拼接操作, 但和torch.cat
稍微有点不同。
1 | t1 = torch.cat([tensor, tensor, tensor], dim=1) |
显示:
1 | tensor([[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.], |
3. 张量的乘积和矩阵乘法
1 | # 逐个元素相乘结果 |
显示:
1 | tensor.mul(tensor): |
下面写法表示张量与张量的矩阵乘法:
1 | print(f"tensor.matmul(tensor.T): \n {tensor.matmul(tensor.T)} \n") |
显示:
1 | tensor.matmul(tensor.T): |
4. 自动赋值运算
自动赋值运算通常在方法后有 _
作为后缀,例如: x.copy_(y)
, x.t_()
操作会改变 x
的取值。
1 | print(tensor, "\n") |
显示:
1 | tensor([[1., 0., 1., 1.], |
注意:
自动赋值运算虽然可以节省内存, 但在求导时会因为丢失了中间过程而导致一些问题, 所以我们并不鼓励使用它。
4. Tensor与Numpy的转化
张量和Numpy array
数组在CPU上可以共用一块内存区域,改变其中一个另一个也会随之改变。
1. 由张量变换为Numpy array数组
1 | t = torch.ones(5) |
显示:
1 | t: tensor([1., 1., 1., 1., 1.]) |
修改张量的值,则Numpy array
数组值也会随之改变。
1 | t.add_(1) |
显示:
1 | t: tensor([2., 2., 2., 2., 2.]) |
2. 由Numpy array数组转为张量
1 | n = np.ones(5) |
修改Numpy array
数组的值,则张量值也会随之改变。
1 | np.add(n, 1, out=n) |
显示:
1 | t: tensor([2., 2., 2., 2., 2.], dtype=torch.float64) |
0x02 Brief introduction to torch.autograd
torch.autograd
是 PyTorch 的自动差分引擎,可为神经网络训练提供支持。在本节中,您将获得有关 Autograd 如何帮助神经网络训练的概念性理解。
1. 背景
神经网络(NN)是在某些输入数据上执行的嵌套函数的集合。 这些函数由参数(由权重和偏差组成)定义,这些参数在 PyTorch 中存储在张量中。
训练 NN 通常分为两个步骤:
正向传播:在正向传播中,NN 对正确的输出进行最佳猜测。 它通过其每个函数运行输入数据以进行猜测。
反向传播:在反向传播中,NN 根据其猜测中的误差调整其参数。 它通过从输出向后遍历,收集有关函数参数的误差导数(即梯度)并使用梯度下降来优化参数来实现。
有关反向传播的更详细的演练,请查看 3Blue1Brown 的B站视频 / Youtube视频。(Youtube比B站更全一些)
2. 在 PyTorch 中的用法
让我们来看一个训练步骤。 对于此示例,我们从torchvision
加载了经过预训练的 resnet18 模型。
我们创建一个随机数据张量来表示具有 3 个通道的单个图像,高度&宽度为 64,其对应的label
初始化为一些随机值。
1 | import torch, torchvision |
我们通过模型的每一层运行输入数据以进行预测,这是正向传播。
1 | prediction = model(data) # forward pass |
接下来,我们使用模型的预测和对应的标签来计算误差(loss
),并通过网络反向传播此误差。
当我们在误差张量上调用.backward()
时,开始反向传播。 然后,Autograd 会为每个模型参数计算梯度并将其存储在参数的.grad
属性中。
1 | loss = (prediction - labels).sum() |
下一步,我们需要加载一个优化器,并在优化器中注册模型的所有参数。本例中优化器为 SGD,学习率为 0.01,动量为 0.9。
1 | optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9) |
最后,我们调用.step()
启动梯度下降。 优化器通过.grad
中存储的梯度来调整每个参数。
1 | optim.step() # gradient descent |
至此,您已经具备了训练神经网络所需的一切。
3. 计算图
从概念上讲,Autograd 在由函数对象组成的有向无环图(DAG)中记录数据(张量)和所有已执行的操作(以及由此产生的新张量)。 在此 DAG 中,叶子是输入张量,根是输出张量。 通过从根到叶跟踪此图,可以使用链式规则自动计算梯度。
在正向传播中,Autograd 同时执行两项操作:
- 运行请求的操作以计算结果张量;
- 在 DAG 中维护操作的梯度函数。
当在 DAG 的输出结点(一般是loss
)上调用 .backward()
时,反向传播开始。Autograd 将执行:
- 从每个
.grad_fn
计算梯度,并将它们累积在各自的张量的.grad
属性中; - 使用链式规则,一直传播到叶子张量。
4. 冻结参数
在 NN 中,不计算梯度的参数通常称为冻结参数。 如果事先知道您不需要这些参数的梯度,则“冻结”模型的一部分很有用(通过减少自动梯度计算,这会带来一些表现优势)。
torch.autograd
跟踪所有将其requires_grad
标志设置为True
的张量的操作。 对于不需要梯度的张量,将此属性设置为False
会将其从梯度计算 DAG 中排除。
即使只有一个输入张量具有requires_grad=True
,操作的输出张量也将需要梯度。
1 | x = torch.rand(5, 5) |
显示:
1 | Does `a` require gradients? : False |
冻结参数的一个常见用例是调整预训练网络。在微调中,我们冻结了大部分模型,通常仅修改分类器层以对新标签进行预测。
让我们来看一个例子来说明这一点。 和以前一样,我们加载一个预训练的 resnet18 模型,并冻结所有参数。
1 | from torch import nn, optim |
假设我们要在具有 10 个标签的新数据集中微调模型。 因此我们在 resnet 的最后增加一个一个线性层 model.fc
作为分类器。
1 | model.fc = nn.Linear(512, 10) |
现在,除了model.fc
的参数外,模型中的所有参数都将冻结。 计算梯度的唯一参数是model.fc
的权重和偏差。
1 | # Optimize only the classifier |
请注意,尽管我们在优化器中注册了所有参数,但唯一可计算梯度的参数(因此会在梯度下降中进行更新)是分类器的权重和偏差。
torch.no_grad()
中的上下文管理器也可以实现相同的冻结功能。
5. 扩展阅读
以下各节详细介绍了 Autograd 的工作原理,可以选择跳过它们。
Autograd的原理
反向模式自动微分 的示例实现
Autograd 的原理
在本节,让我们来看看autograd
是如何收集梯度的。
首先我们用 requires_grad=True
来创建两个张量 $a$ 和 $b$。 这向autograd
发出信号,应跟踪对它们的所有操作。
1 | import torch |
接下来我们从 $a$ 和 $b$ 创建另一个张量 $Q = 3a^3 - b^2$。
1 | Q = 3*a**3 - b**2 |
我们假设 a
和 b
是神经网络的参数,Q
是误差。 在 NN 训练中,我们想要相对于参数的误差,即偏导数:
$$\frac{\partial Q}{\partial a} = 9a^2$$
$$\frac{\partial Q}{\partial b} = -2b$$
当我们在Q
上调用.backward()
时,Autograd 将计算这些梯度并将其存储在各个张量的.grad
属性中。
我们需要在Q.backward()
中显式传递gradient
参数,因为它是向量。 gradient
是与Q
形状相同的张量,它表示Q
相对于本身的梯度,即 $$\frac{\partial Q}{\partial Q} = 1$$
同样,我们也可以将Q
聚合为一个标量,然后隐式地向后调用,例如Q.sum().backward()
。
1 | external_grad = torch.tensor([1., 1.]) |
现在,梯度已经保存在a.grad
和b.grad
中了。
1 | # check if collected gradients are correct |
显示:
1 | tensor([True, True]) |
0x03 How to define a Neural Network
现在您已经了解了autograd
,可以使用torch.nn
包构建神经网络。nn
依赖于autograd
来定义模型并对其进行微分。 nn.Module
包含了各种神经网络层,以及从 input
生成 output
的方法: forward()
。
神经网络的典型训练过程如下:
- 定义具有一些可学习参数(或权重)的神经网络
- 遍历输入数据集,通过网络处理输入
- 计算损失(输出正确的距离有多远)
- 将梯度反向传播回网络参数
- 通常使用简单的更新规则来更新网络的权重:
weight = weight - learning_rate * gradient
1. 定义神经网络
下面我们定义一个简单地CNN网络。
1 | import torch |
输出:
1 | Net( |
我们只需要定义好 forward
函数,就可以使用 autograd
自动定义backward
函数来计算梯度。 在forward
函数中可以进行任何张量操作。
模型的可学习参数由net.parameters()
返回
1 | params = list(net.parameters()) |
输出:
1 | 10 |
2. 处理输入
让我们尝试一个32x32
随机输入。 注意:该网络的预期输入大小(LeNet)为32x32
。 要在 MNIST 数据集上使用此网络,请将图像从数据集中调整为32x32
。
1 | input = torch.randn(1, 1, 32, 32) |
输出:
1 | tensor([[ 0.1002, -0.0694, -0.0436, 0.0103, 0.0488, -0.0429, -0.0941, -0.0146, |
使用随机梯度将所有参数和反向传播的梯度缓冲区归零:
1 | net.zero_grad() |
注意:
torch.nn
仅支持小批量而不是单个样本的输入。例如,
nn.Conv2d
将采用nSamples x nChannels x Height x Width
的 4D 张量。如果您只有一个样本,只需使用input.unsqueeze(0)
添加一个假批量尺寸。
3. 损失函数与计算图
损失函数采用一对(输出,目标)输入,并计算一个值,该值估计输出与目标之间的距离。
nn
包下有几种不同的损失函数。 一个简单的损失是:nn.MSELoss
,它计算输入和目标之间的均方误差。
例如:
1 | output = net(input) |
输出:
1 | tensor(0.4969, grad_fn=<MseLossBackward>) |
现在,如果使用.grad_fn
属性向后跟随loss
,您将看到一个计算图,如下所示:
1 | input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d |
因此,当我们调用loss.backward()
时,整个图将被微分,并且图中具有 requires_grad=True
的所有张量将随梯度累积其.grad
张量。
为了说明,让我们向后走几步:
1 | print(loss.grad_fn) # MSELoss |
输出:
1 | <MseLossBackward object at 0x7f1ba05a1ba8> |
4. 反向传播误差
要反向传播误差,我们要做的只是对loss.backward()
。 不过,您需要清除现有的梯度,否则梯度将累积到现有的梯度中。
现在,我们将其称为loss.backward()
,然后看一下向后前后conv1
的偏差梯度。
1 | net.zero_grad() # zeroes the gradient buffers of all parameters |
输出:
1 | conv1.bias.grad before backward |
5. 更新权重
实践中使用的最简单的更新规则是随机梯度下降(SGD):
1 | weight = weight - learning_rate * gradient |
我们可以使用简单的 Python 代码实现此目标:
1 | learning_rate = 0.01 |
但是,在使用神经网络时,您可能希望使用各种不同的更新规则,例如 SGD,Nesterov-SGD,Adam,RMSProp 等。
为实现此目的,我们构建了一个小包装:torch.optim
,可实现所有这些方法。 使用它非常简单:
1 | import torch.optim as optim |
注意观察如何使用optimizer.zero_grad()
将梯度缓冲区手动设置为零。 这是因为如反向传播部分中所述累积了梯度。