本文整理自 PyTorch 深度学习:60分钟快速入门

原文:https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html

作者:Soumith Chintala

视频:https://www.youtube.com/embed/u7x8RXwLKcA

0x00 Why Pytorch?

PyTorch 是基于以下两个目的而打造的python科学计算框架:

  • 无缝替换NumPy,并且通过利用GPU的算力来实现神经网络的加速。
  • 通过自动微分机制,来让神经网络的实现变得更加容易。

0x01 What is Tensor?

原文: https://pytorch.org/tutorials/beginner/blitz/tensor_tutorial.html#sphx-glr-beginner-blitz-tensor-tutorial-py

张量如同数组和矩阵一样,是一种特殊的数据结构。在PyTorch中, 神经网络的输入、输出以及网络的参数等数据,都是使用张量来进行描述。

张量的使用和 Numpy 中的 ndarrays 很类似,区别在于张量可以在 GPU 或其它专用硬件上运行, 这样可以得到更快的加速效果。如果你对ndarrays很熟悉的话, 张量的使用对你来说就很容易了。如果不太熟悉的话,希望这篇有关张量API的快速入门教程能够帮到你。

1
2
import torch
import numpy as np

1. Tensor的初始化

张量有很多种不同的初始化方法, 先来看看四个简单的例子:

1. 直接生成张量

由原始数据直接生成张量, 张量类型由原始数据类型决定。

1
2
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)

2. 通过Numpy数组来生成张量

由已有的Numpy数组来生成张量(反过来也可以由张量来生成Numpy数组, 参考张量与Numpy之间的转换)。

1
2
np_array = np.array(data)
x_np = torch.from_numpy(np_array)

3. 通过已有的张量来生成新的张量

新的张量将继承已有张量的数据属性(结构、类型), 也可以重新指定新的数据类型。

1
2
3
4
5
6
x_ones = torch.ones_like(x_data)   # 保留 x_data 的属性
print(f"Ones Tensor: \n {x_ones} \n")

x_rand = torch.rand_like(x_data, dtype=torch.float) # 重写 x_data 的数据类型
int -> float
print(f"Random Tensor: \n {x_rand} \n")

输出:

1
2
3
4
5
6
7
Ones Tensor:
tensor([[1, 1],
[1, 1]])

Random Tensor:
tensor([[0.0381, 0.5780],
[0.3963, 0.0840]])

4. 通过指定数据维度来生成张量

shape是元组类型, 用来描述张量的维数, 下面3个函数通过传入shape来指定生成张量的维数。

1
2
3
4
5
6
7
8
shape = (2,3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")

输出:

1
2
3
4
5
6
7
8
9
10
11
Random Tensor:
tensor([[0.0266, 0.0553, 0.9843],
[0.0398, 0.8964, 0.3457]])

Ones Tensor:
tensor([[1., 1., 1.],
[1., 1., 1.]])

Zeros Tensor:
tensor([[0., 0., 0.],
[0., 0., 0.]])

2. Tensor的属性

从张量属性我们可以得到张量的维数、数据类型以及它们所存储的设备(CPU或GPU)。

来看一个简单的例子:

1
2
3
4
5
tensor = torch.rand(3,4)

print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")

输出:

1
2
3
Shape of tensor: torch.Size([3, 4])   # 维数
Datatype of tensor: torch.float32 # 数据类型
Device tensor is stored on: cpu # 存储设备

3. Tensor运算

有超过100种张量相关的运算操作, 例如转置、索引、切片、数学运算、线性代数、随机采样等。更多的运算可以在这里查看

所有这些运算都可以在GPU上运行:

1
2
3
# 判断当前环境GPU是否可用, 然后将tensor导入GPU内运行
if torch.cuda.is_available():
tensor = tensor.to('cuda')

光说不练假把式,接下来的例子一定要动手跑一跑。如果你对Numpy的运算非常熟悉的话, 那tensor的运算对你来说就是小菜一碟。

1. 张量的索引和切片

1
2
3
tensor = torch.ones(4, 4)
tensor[:,1] = 0 # 将第1列(从0开始)的数据全部赋值为0
print(tensor)

显示:

1
2
3
4
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])

2. 张量的拼接

你可以通过torch.cat方法将一组张量按照指定的维度进行拼接, 也可以参考torch.stack方法。这个方法也可以实现拼接操作, 但和torch.cat稍微有点不同。

1
2
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)

显示:

1
2
3
4
tensor([[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.]])

3. 张量的乘积和矩阵乘法

1
2
3
4
# 逐个元素相乘结果
print(f"tensor.mul(tensor): \n {tensor.mul(tensor)} \n")
# 等价写法:
print(f"tensor * tensor: \n {tensor * tensor}")

显示:

1
2
3
4
5
6
7
8
9
10
11
tensor.mul(tensor):
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])

tensor * tensor:
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])Copy

下面写法表示张量与张量的矩阵乘法:

1
2
3
print(f"tensor.matmul(tensor.T): \n {tensor.matmul(tensor.T)} \n")
# 等价写法:
print(f"tensor @ tensor.T: \n {tensor @ tensor.T}")

显示:

1
2
3
4
5
6
7
8
9
10
11
tensor.matmul(tensor.T):
tensor([[3., 3., 3., 3.],
[3., 3., 3., 3.],
[3., 3., 3., 3.],
[3., 3., 3., 3.]])

tensor @ tensor.T:
tensor([[3., 3., 3., 3.],
[3., 3., 3., 3.],
[3., 3., 3., 3.],
[3., 3., 3., 3.]])Copy

4. 自动赋值运算

自动赋值运算通常在方法后有 _ 作为后缀,例如: x.copy_(y), x.t_()操作会改变 x 的取值。

1
2
3
print(tensor, "\n")
tensor.add_(5)
print(tensor)

显示:

1
2
3
4
5
6
7
8
9
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])

tensor([[6., 5., 6., 6.],
[6., 5., 6., 6.],
[6., 5., 6., 6.],
[6., 5., 6., 6.]])

注意:

自动赋值运算虽然可以节省内存, 但在求导时会因为丢失了中间过程而导致一些问题, 所以我们并不鼓励使用它。

4. Tensor与Numpy的转化

张量和Numpy array数组在CPU上可以共用一块内存区域,改变其中一个另一个也会随之改变。

1. 由张量变换为Numpy array数组

1
2
3
4
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")

显示:

1
2
t: tensor([1., 1., 1., 1., 1.])
n: [1. 1. 1. 1. 1.]

修改张量的值,则Numpy array数组值也会随之改变。

1
2
3
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")

显示:

1
2
t: tensor([2., 2., 2., 2., 2.])
n: [2. 2. 2. 2. 2.]

2. 由Numpy array数组转为张量

1
2
n = np.ones(5)
t = torch.from_numpy(n)

修改Numpy array数组的值,则张量值也会随之改变。

1
2
3
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")

显示:

1
2
t: tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
n: [2. 2. 2. 2. 2.]

0x02 Brief introduction to torch.autograd

原文:https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html#sphx-glr-beginner-blitz-autograd-tutorial-py

torch.autograd是 PyTorch 的自动差分引擎,可为神经网络训练提供支持。在本节中,您将获得有关 Autograd 如何帮助神经网络训练的概念性理解。

1. 背景

神经网络(NN)是在某些输入数据上执行的嵌套函数的集合。 这些函数由参数(由权重和偏差组成)定义,这些参数在 PyTorch 中存储在张量中。

训练 NN 通常分为两个步骤:

  • 正向传播:在正向传播中,NN 对正确的输出进行最佳猜测。 它通过其每个函数运行输入数据以进行猜测。

  • 反向传播:在反向传播中,NN 根据其猜测中的误差调整其参数。 它通过从输出向后遍历,收集有关函数参数的误差导数(即梯度)并使用梯度下降来优化参数来实现。

    有关反向传播的更详细的演练,请查看 3Blue1Brown 的B站视频 / Youtube视频。(Youtube比B站更全一些)

2. 在 PyTorch 中的用法

让我们来看一个训练步骤。 对于此示例,我们从torchvision加载了经过预训练的 resnet18 模型。

我们创建一个随机数据张量来表示具有 3 个通道的单个图像,高度&宽度为 64,其对应的label初始化为一些随机值。

1
2
3
4
import torch, torchvision
model = torchvision.models.resnet18(pretrained=True)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)

我们通过模型的每一层运行输入数据以进行预测,这是正向传播

1
prediction = model(data) # forward pass

接下来,我们使用模型的预测和对应的标签来计算误差(loss),并通过网络反向传播此误差。

当我们在误差张量上调用.backward()时,开始反向传播。 然后,Autograd 会为每个模型参数计算梯度并将其存储在参数的.grad属性中。

1
2
loss = (prediction - labels).sum()
loss.backward() # backward pass

下一步,我们需要加载一个优化器,并在优化器中注册模型的所有参数。本例中优化器为 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
2
3
4
5
6
7
8
x = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)

a = x + y
print(f"Does `a` require gradients? : {a.requires_grad}")
b = x + z
print(f"Does `b` require gradients?: {b.requires_grad}")

显示:

1
2
Does `a` require gradients? : False
Does `b` require gradients?: True

冻结参数的一个常见用例是调整预训练网络。在微调中,我们冻结了大部分模型,通常仅修改分类器层以对新标签进行预测。

让我们来看一个例子来说明这一点。 和以前一样,我们加载一个预训练的 resnet18 模型,并冻结所有参数。

1
2
3
4
5
6
7
from torch import nn, optim

model = torchvision.models.resnet18(pretrained=True)

# Freeze all the parameters in the network
for param in model.parameters():
param.requires_grad = False

假设我们要在具有 10 个标签的新数据集中微调模型。 因此我们在 resnet 的最后增加一个一个线性层 model.fc 作为分类器。

1
model.fc = nn.Linear(512, 10)

现在,除了model.fc的参数外,模型中的所有参数都将冻结。 计算梯度的唯一参数是model.fc的权重和偏差。

1
2
# Optimize only the classifier
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)Copy

请注意,尽管我们在优化器中注册了所有参数,但唯一可计算梯度的参数(因此会在梯度下降中进行更新)是分类器的权重和偏差。

torch.no_grad()中的上下文管理器也可以实现相同的冻结功能。

5. 扩展阅读

以下各节详细介绍了 Autograd 的工作原理,可以选择跳过它们。

Autograd 的原理

在本节,让我们来看看autograd是如何收集梯度的。

首先我们用 requires_grad=True 来创建两个张量 $a$ 和 $b$。 这向autograd发出信号,应跟踪对它们的所有操作。

1
2
3
4
import torch

a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)Copy

接下来我们从 $a$ 和 $b$ 创建另一个张量 $Q = 3a^3 - b^2$。

1
Q = 3*a**3 - b**2

我们假设 ab 是神经网络的参数,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
2
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)

现在,梯度已经保存在a.gradb.grad中了。

1
2
3
# check if collected gradients are correct
print(9*a**2 == a.grad)
print(-2*b == b.grad)

显示:

1
2
tensor([True, True])
tensor([True, True])

0x03 How to define a Neural Network

原文:https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#sphx-glr-beginner-blitz-neural-networks-tutorial-py

现在您已经了解了autograd,可以使用torch.nn包构建神经网络。nn依赖于autograd来定义模型并对其进行微分。 nn.Module包含了各种神经网络层,以及从 input 生成 output 的方法: forward()

神经网络的典型训练过程如下:

  1. 定义具有一些可学习参数(或权重)的神经网络
  2. 遍历输入数据集,通过网络处理输入
  3. 计算损失(输出正确的距离有多远)
  4. 将梯度反向传播回网络参数
  5. 通常使用简单的更新规则来更新网络的权重:weight = weight - learning_rate * gradient

1. 定义神经网络

下面我们定义一个简单地CNN网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):

def __init__(self):
super(Net, self).__init__()
# 1 input image channel, 6 output channels, 3x3 square convolution
# kernel
self.conv1 = nn.Conv2d(1, 6, 3)
self.conv2 = nn.Conv2d(6, 16, 3)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 6 * 6, 120) # 6*6 from image dimension
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# Max pooling over a (2, 2) window
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# If the size is a square you can only specify a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] # all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features

net = Net()
print(net)

输出:

1
2
3
4
5
6
7
Net(
(conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(3, 3), stride=(1, 1))
(fc1): Linear(in_features=576, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)Copy

我们只需要定义好 forward 函数,就可以使用 autograd 自动定义backward函数来计算梯度。 在forward函数中可以进行任何张量操作。

模型的可学习参数由net.parameters()返回

1
2
3
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight

输出:

1
2
10
torch.Size([6, 1, 3, 3])

2. 处理输入

让我们尝试一个32x32随机输入。 注意:该网络的预期输入大小(LeNet)为32x32。 要在 MNIST 数据集上使用此网络,请将图像从数据集中调整为32x32

1
2
3
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

输出:

1
2
tensor([[ 0.1002, -0.0694, -0.0436,  0.0103,  0.0488, -0.0429, -0.0941, -0.0146,
-0.0031, -0.0923]], grad_fn=<AddmmBackward>)

使用随机梯度将所有参数和反向传播的梯度缓冲区归零:

1
2
net.zero_grad()
out.backward(torch.randn(1, 10))

注意:

  • torch.nn仅支持小批量而不是单个样本的输入。

  • 例如,nn.Conv2d将采用nSamples x nChannels x Height x Width的 4D 张量。如果您只有一个样本,只需使用input.unsqueeze(0)添加一个假批量尺寸

3. 损失函数与计算图

损失函数采用一对(输出,目标)输入,并计算一个值,该值估计输出与目标之间的距离。

nn包下有几种不同的损失函数。 一个简单的损失是:nn.MSELoss,它计算输入和目标之间的均方误差。

例如:

1
2
3
4
5
6
7
output = net(input)
target = torch.randn(10) # a dummy target, for example
target = target.view(1, -1) # make it the same shape as output
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

输出:

1
tensor(0.4969, grad_fn=<MseLossBackward>)

现在,如果使用.grad_fn属性向后跟随loss,您将看到一个计算图,如下所示:

1
2
3
4
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss

因此,当我们调用loss.backward()时,整个图将被微分,并且图中具有 requires_grad=True 的所有张量将随梯度累积其.grad张量。

为了说明,让我们向后走几步:

1
2
3
print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU

输出:

1
2
3
<MseLossBackward object at 0x7f1ba05a1ba8>
<AddmmBackward object at 0x7f1ba05a19e8>
<AccumulateGrad object at 0x7f1ba05a19e8>

4. 反向传播误差

要反向传播误差,我们要做的只是对loss.backward()。 不过,您需要清除现有的梯度,否则梯度将累积到现有的梯度中。

现在,我们将其称为loss.backward(),然后看一下向后前后conv1的偏差梯度。

1
2
3
4
5
6
7
8
9
net.zero_grad()     # zeroes the gradient buffers of all parameters

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

输出:

1
2
3
4
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0111, -0.0064, 0.0053, -0.0047, 0.0026, -0.0153])

5. 更新权重

实践中使用的最简单的更新规则是随机梯度下降(SGD):

1
weight = weight - learning_rate * gradient

我们可以使用简单的 Python 代码实现此目标:

1
2
3
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)Copy

但是,在使用神经网络时,您可能希望使用各种不同的更新规则,例如 SGD,Nesterov-SGD,Adam,RMSProp 等。

为实现此目的,我们构建了一个小包装:torch.optim,可实现所有这些方法。 使用它非常简单:

1
2
3
4
5
6
7
8
9
10
11
import torch.optim as optim

# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)

# in your training loop:
optimizer.zero_grad() # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # Does the update

注意观察如何使用optimizer.zero_grad()将梯度缓冲区手动设置为零。 这是因为如反向传播部分中所述累积了梯度。

0x04 Practice: training a image classifier