Tensor

  • 张量是一种特殊的数据结构,与数组和矩阵非常相似。在PyTorch中,我们使用张量对模型的输入和输出以及模型的参数进行编码

  • 张量类似于NumPy的ndarray,除了张量可以在GPU或其他专用硬件上运行以加速计算

1
2
import torch
import numpy as np

Tensor Initialization

  • 张量可以通过多种方式初始化
    1. 张量可以直接从数据中创建。数据类型是自动推断的。torch.tensor(data)
      1
      2
      data = [[1, 2], [3, 4]]
      x_data = torch.tensor(data)
    2. 张量可以从NumPy中的arrays创建,反之亦然。torch.from_numpy(np_array)
      1
      2
      np_array = np.array(data)
      x_np = torch.from_numpy(np_array)
    3. 从另一个张量,新张量将保留参数张量的属性(形状、数据类型)。torch.ones_like(tensor,type) & torch.rand_like(tensor,type)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      x_ones = torch.ones_like(x_data) 
      # retains the properties of x_data
      # torch.ones_like 是 PyTorch 中的一个函数
      # 它根据给定的张量(tensor)创建一个与其形状、数据类型相同的新张量
      # 并且所有元素的值都为1
      print(f"Ones Tensor: \n {x_ones} \n")

      x_rand = torch.rand_like(x_data, dtype=torch.float)
      # overrides the datatype of x_data
      # torch.rand_like 是 PyTorch 中的一个函数
      # 它根据给定的张量(tensor)创建一个与其形状和数据类型相同的新张量
      # 其中元素是从均匀分布([0, 1))中随机采样的浮点数
      print(f"Random Tensor: \n {x_rand} \n")

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

      Random Tensor:
      tensor([[0.8823, 0.9150],
      [0.3829, 0.9593]])
    4. 随机或恒定值,用shape决定输出张量的维数。(shape)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      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}")

      out:
      Random Tensor:
      tensor([[0.3904, 0.6009, 0.2566],
      [0.7936, 0.9408, 0.1332]])

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

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

Tensor Attributes

  • 张量属性描述了它们的形状、数据类型以及存储它们的设备。tensor.shape() & tensor.dtype() & tensor.device()
1
2
3
4
5
6
7
8
9
10
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}")

out:
Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu

Tensor Operations

  • 张量操作包括转置、索引、切片、数学运算、线性代数、随机采样等。每一个都可以在GPU上运行(通常比在CPU上运行速度更快)。torch.cuda.is_available() & tensor.to(‘cuda’)
1
2
3
4
5
6
7
# We move our tensor to the GPU if available
if torch.cuda.is_available():
tensor = tensor.to('cuda')
print(f"Device tensor is stored on: {tensor.device}")

out:
Device tensor is stored on: cuda:0
  • 标准numpy类索引和切片:tensor[:,n] & tensor[n,:]
1
2
3
4
5
6
7
8
9
10
11
12
13
tensor = torch.ones(4, 4)
tensor[:,1] = 0
# tensor[:, 1] 中的 : 是一个切片操作符,表示选取张量的所有行
# 1 是指第二列(索引从 0 开始),表示选择张量的第 1 列
# tensor[:,1] = 0 表示对张量 tensor 的某一部分进行索引,并将这些部分的值设为 0
print(tensor)

out:
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])
# 对第一列置0
  • 连接张量:使用torch.cat沿给定维度连接一系列张量。另请参见torch.stack,与torch.cat略有不同
1
2
3
4
5
6
7
8
9
10
t1 = torch.cat([tensor, tensor, tensor], dim=1)
# dim=1:表示沿第 1 维(即列方向)拼接张量
# dim=0:表示沿第 0 维(即行方向)拼接张量
print(t1)

out:
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.]])
  • 乘以张量:* & @ & tensor.mul(tensor) & torch.matmul(tensor1, tensor2)
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
# This computes the element-wise product
print(f"tensor.mul(tensor) \n {tensor.mul(tensor)} \n")
# Alternative syntax:
print(f"tensor * tensor \n {tensor * tensor}")

out:
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.]])

print(f"tensor.matmul(tensor.T) \n {tensor.matmul(tensor.T)} \n")
# Alternative syntax:
print(f"tensor @ tensor.T \n {tensor @ tensor.T}")
# tensor.T 是一个简洁的语法,用来表示张量的转置

out:
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.]])
  • In-place operation(““):x.copy(y), x.t_(), x.add(n) will change x.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
print(tensor, "\n")
tensor.add_(5)
print(tensor)

out:
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.]])
  • In-place operation 可以节省一些内存,但在计算导数时可能会出现问题,因为会立即丢失历史记录。因此,不鼓励使用它们

Bridge with NumPy

  • CPU上的tensor和NumPy数组上的张量可以共享它们的底层内存位置,改变一个就会改变另一个

Tensor to NumPy array

  • t.numpy()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")

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

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

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

NumPy array to Tensor

  • torch.from_numpy(n)
1
2
3
4
5
6
7
8
9
10
n = np.ones(5)
t = torch.from_numpy(n)

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

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

A Gentle Introduction to torch.autograd

  • torch.autograd是PyTorch的自动微分引擎,为神经网络训练提供动力

Background

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

  • 训练神经网络分为两个步骤:

    1. 正向传播:在正向传播中,神经网络对正确的输出做出最佳预测。它通过每个函数运行输入数据来进行猜测
    2. 反向传播:在反向传播中,神经网络根据其猜测的误差按比例调整其参数。它通过从输出向后遍历,收集误差相对于函数参数(梯度)的导数,并使用梯度下降优化参数来实现这一点

Usage in PyTorch

  • 例子:我们从torchvision加载一个预训练的resnet18模型。我们创建了一个随机数据张量来表示具有3个通道、高度和宽度为64的单个图像,并将其相应的标签初始化为一些随机值。预训练模型中的标签具有形状(1,1000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64) # 四维tensor
labels = torch.rand(1, 1000) # 二维tensor

# forward pass:把数据输入到模型中用于预测结果
prediction = model(data) # forward pass

# 计算误差反向传播.backward():Autograd计算每个模型参数的梯度并将其存储在参数的.grad属性中
loss = (prediction - labels).sum()
# .sum()求和 .sum(dim = 0)是对行求和,.sum(dim = 1)是对列求和
loss.backward() # backward pass

# 加载优化器SGD with a learning rate of 0.01 and momentum of 0.9
# 优化器的任务就是根据这些梯度调整模型参数
# model.parameters() 返回的是模型的所有可训练参数,这些参数需要通过优化器来进行更新
optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

# 调用.step()来启动梯度下降。优化器根据存储在.grad中的梯度调整每个参数
optim.step() #gradient descent

Differentiation in Autograd

  • autograd如何收集梯度?
1
2
3
4
5
6
7
8
9
import torch

# 2. 3.代表是浮点数,requires_grad=True说明需要为这个张量计算梯度
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)

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

# 假设a和b是NN的参数,Q是误差

1.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 调用.backward()时,autograd会计算这些梯度并将其存储在相应张量的.grad属性中
# 需要在Q.backward()中显式传递一个梯度参数,因为它是一个向量。
# 梯度是一个与Q形状相同的张量,它表示Q相对于自身的梯度
# 我们也可以将Q聚合为标量,并隐式向后调用,如Q.sum().backward()
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)

# 梯度现在存放在a.grad和b.grad中
# check if collected gradients are correct
print(9*a**2 == a.grad)
print(-2*b == b.grad)

out:
tensor([True, True])
tensor([True, True])

Optional Reading - Vector Calculus using autograd

2.png

Computational Graph

  • autograd在由Function对象组成的有向无环图(DAG)中记录数据(张量)和所有执行的操作(以及由此产生的新张量)。在这个DAG中,叶子是输入张量,根是输出张量。通过从根到叶跟踪此图,您可以使用链式规则自动计算梯度

  • In a forward pass, autograd does two things simultaneously:

    1. 运行所请求的操作以计算结果张量
    2. 在DAG中保持操作的梯度函数
  • The backward pass kicks off when .backward() is called on the DAG root. autograd then:

    1. 根据每个.grad_fn计算梯度
    2. 将它们累积在各自张量的.grad属性中
    3. 使用链式规则,一直传播到叶张量
  • DAG的可视化表示。在图中,箭头指向正向传递的方向。节点表示正向传递中每个操作的反向函数。蓝色的叶节点表示我们的叶张量a和b

3.png

  • DAGs在PyTorch中是动态的。每次.backward()调用后,autograd都会开始填充一个新的图

  • torch.autograd跟踪所有requires_grad标志设置为True的张量上的操作。对于不需要梯度的张量,将此属性设置为False会将其从梯度计算DAG中排除

1
2
3
4
5
6
7
8
9
10
11
12
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}")

out:
Does `a` require gradients?: False
Does `b` require gradients?: True
  • 在神经网络中,不计算梯度的参数通常被称为冻结参数。在微调中,我们冻结了大部分模型,通常只修改分类器层以对新标签进行预测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from torch import nn, optim

model = resnet18(weights=ResNet18_Weights.DEFAULT)

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

# resnet中,分类器是最后一个线性层模型fc。用一个新的线性层(未冻结)替换它,作为分类器
model.fc = nn.Linear(512, 10)

# 除了model.fc的参数外,模型中的所有参数都被冻结了。计算梯度的唯一参数是model.fc的权重和偏差
# Optimize only the classifier
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
# 尽管我们在优化器中注册了所有参数,但计算梯度的唯一参数是分类器的权重和偏差

Neural Networks

  • 神经网络可以使用 torch.nn 包来构建

  • 在了解了 autograd 之后,nn 依赖于 autograd 来定义模型并对其进行微分

  • 一个 nn.Module 包含层,以及一个返回输出的 forward(input) 方法

  • 例如这个用于分类数字图像的神经网络:

image.png

  • 这是一个简单的前馈神经网络:它接收输入,将输入通过若干层依次传递,最后给出输出

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

    1. 定义神经网络:定义具有一些可学习参数(或权重)的神经网络。
    2. 遍历输入数据集:对输入数据集进行迭代。
    3. 通过网络处理输入:将输入数据通过网络传递。
    4. 计算损失:计算损失(输出与正确值之间的差距)。
    5. 反向传播梯度:将梯度传播回网络的参数中。
    6. 网络权重:通常使用简单的更新规则来更新网络权重:weight = weight - learning_rate * gradient

Define the network

  • 定义神经网络:
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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, 5x5 square convolution
# kernel
# 第一个参数是输入通道数,第二个参数是输出通道数
# 第三个参数是kernel_size
# 输出通道数可取任意值
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 5*5 from image dimension
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, input):
# Convolution layer C1: 1 input image channel, 6 output channels,
# 5x5 square convolution, it uses RELU activation function, and
# outputs a Tensor with size (N, 6, 28, 28), where N is the size of the batch
c1 = F.relu(self.conv1(input))
# Subsampling layer S2: 2x2 grid, purely functional,
# this layer does not have any parameter, and outputs a (N, 6, 14, 14) Tensor
s2 = F.max_pool2d(c1, (2, 2))
# Convolution layer C3: 6 input channels, 16 output channels,
# 5x5 square convolution, it uses RELU activation function, and
# outputs a (N, 16, 10, 10) Tensor
c3 = F.relu(self.conv2(s2))
# Subsampling layer S4: 2x2 grid, purely functional,
# this layer does not have any parameter, and outputs a (N, 16, 5, 5) Tensor
s4 = F.max_pool2d(c3, 2)
# Flatten operation: purely functional, outputs a (N, 400) Tensor
s4 = torch.flatten(s4, 1)
# Fully connected layer F5: (N, 400) Tensor input,
# and outputs a (N, 120) Tensor, it uses RELU activation function
f5 = F.relu(self.fc1(s4))
# Fully connected layer F6: (N, 120) Tensor input,
# and outputs a (N, 84) Tensor, it uses RELU activation function
f6 = F.relu(self.fc2(f5))
# Gaussian layer OUTPUT: (N, 84) Tensor input, and
# outputs a (N, 10) Tensor
output = self.fc3(f6)
return output

net = Net()
print(net)

'''
out:
Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, 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)
)
'''
  • 只需要定义 forward 函数,backward 函数(用于计算梯度)会通过 autograd 自动定义

  • 可以在 forward 函数中使用任何 Tensor 操作

  • 模型的可学习参数可以通过 net.parameters() 返回

1
2
3
4
5
6
7
8
9
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight

'''
out:
10
torch.Size([6, 1, 5, 5])
'''
  • 让我们尝试一个随机的 32x32 输入

  • 注意:这个网络(LeNet)期望的输入大小是 32x32。要在 MNIST 数据集上使用此网络,请将数据集中的图像调整为 32x32 的大小

1
2
3
4
5
6
7
8
9
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

'''
out:
tensor([[ 0.1453, -0.0590, -0.0065, 0.0905, 0.0146, -0.0805, -0.1211, -0.0394,
-0.0181, -0.0136]], grad_fn=<AddmmBackward0>)
'''
  • 将所有参数的梯度缓冲区清零,并使用随机梯度进行反向传播:

  • torch.nn 仅支持小批量(mini-batch)。整个 torch.nn 包只支持输入为小批量样本,而不支持单个样本

  • 例如,nn.Conv2d 需要一个形状为 nSamples x nChannels x Height x Width 的四维张量作为输入

  • 如果只有一个单个样本,可以使用 input.unsqueeze(0) 添加一个伪批量维度

1
2
net.zero_grad()
out.backward(torch.randn(1, 10))
  • summary:
    1. torch.Tensor - 一种多维数组,支持自动求导操作,如 backward()。它还保存了相对于该张量的梯度
    2. nn.Module - 神经网络模块。封装参数的便捷方式,提供了将参数移动到 GPU、导出、加载等辅助功能
    3. nn.Parameter - 一种特殊的张量(Tensor),当作为属性分配给 Module 时,会自动注册为参数
    4. autograd.Function - 实现了自动求导操作的前向和反向定义。每个张量操作至少会创建一个 Function 节点,该节点连接到创建该张量的函数,并编码其历史记录

Loss Function

  • 损失函数 接收一对输入(输出值 output 和目标值 target),并计算一个值,用于估计输出值与目标值之间的差距

  • 在 nn 模块中有多种不同的损失函数。一个简单的损失函数是:nn.MSELoss,它计算输出值与目标值之间的均方误差

1
2
3
4
5
6
7
8
9
10
11
12
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)

'''
out:
tensor(1.3619, grad_fn=<MseLossBackward0>)
'''
  • 如果沿着损失函数的反向传播方向(通过其 .grad_fn 属性)追踪,您会看到一个类似以下的计算图:
    input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
    -> flatten -> linear -> relu -> linear -> relu -> linear
    -> MSELoss
    -> loss

  • 当调用 loss.backward() 时,整个计算图会相对于神经网络的参数进行微分,并且图中所有 requires_grad=True 的张量都会将其梯度累加到 .grad 属性中

  • 为了更直观地理解,让我们一步步反向追踪:

1
2
3
4
5
6
7
8
9
10
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

'''
out:
<MseLossBackward0 object at 0x7f64a56c75b0>
<AddmmBackward0 object at 0x7f64a56c40a0>
<AccumulateGrad object at 0x7f64a56c49a0>
'''

Backprop

  • 要反向传播误差,我们只需要调用 loss.backward()。但在此之前需要清除现有的梯度,否则梯度会累积到现有的梯度上。

  • 现在我们将调用 loss.backward(),并观察 conv1 的偏置梯度在反向传播前后的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

'''
out:
conv1.bias.grad before backward
None
conv1.bias.grad after backward
tensor([ 0.0081, -0.0080, -0.0039, 0.0150, 0.0003, -0.0105])
'''

Update the weights

  • 在实践中使用的最简单的更新规则是随机梯度下降法(Stochastic Gradient Descent, SGD):
1
2
3
4
5
weight = weight - learning_rate * gradient

learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
  • 其他梯度下降的方法例如 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() 手动将梯度缓冲区清零。这是因为梯度会累积,正如在反向传播部分所解释的那样

Training a Classifier

What about data?

  • 通常当需要处理图像、文本、音频或视频数据时,可以使用标准的 Python 包将数据加载到 NumPy 数组中。然后可以将该数组转换为 torch.*Tensor:

    1. 对于图像,可以使用诸如 Pillow 和 OpenCV 等包。
    2. 对于音频,可以使用诸如 scipy 和 librosa 等包。
    3. 对于文本,可以使用基于原始 Python 或 Cython 的加载方式,或者使用 NLTK 和 SpaCy 等工具。
  • 对于视觉任务, torchvision 包提供了常见数据集(如 ImageNet、CIFAR10、MNIST 等)的数据加载器,以及针对图像的数据转换工具,例如 torchvision.datasets 和 torch.utils.data.DataLoader。

  • 在本教程中,将使用 CIFAR10 数据集。它包含以下类别: ‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’

image.png

  • CIFAR-10 中的图像尺寸为 3x32x32,即 32x32 像素大小的 3 通道彩色图像

Training an image classifier

  • 实现步骤:
    1. 使用 torchvision 加载并标准化 CIFAR10 的训练和测试数据集
    2. 定义一个卷积神经网络(Convolutional Neural Network, CNN)
    3. 定义一个损失函数
    4. 在训练数据上训练网络
    5. 在测试数据上测试网络

Load and normalize CIFAR10

  • 使用torchvision加载 CIFAR10

  • torchvision 数据集的输出是范围在 [0, 1] 的 PILImage 图像。我们将它们转换为范围在 [-1, 1] 的归一化张量

  • 注意:如果在 Windows 上运行并遇到 BrokenPipeError 错误,可以尝试将 torch.utils.data.DataLoader() 的 num_worker 参数设置为 0

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
38
39
40
41
42
43
44
45
46
import torch
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

batch_size = 4

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

'''
out:
0%| | 0.00/170M [00:00<?, ?B/s]
0%| | 688k/170M [00:00<00:24, 6.88MB/s]
5%|4 | 7.93M/170M [00:00<00:03, 45.4MB/s]
11%|# | 18.5M/170M [00:00<00:02, 73.0MB/s]
17%|#6 | 28.4M/170M [00:00<00:01, 83.1MB/s]
22%|##2 | 38.2M/170M [00:00<00:01, 88.4MB/s]
28%|##8 | 48.3M/170M [00:00<00:01, 92.6MB/s]
34%|###4 | 58.0M/170M [00:00<00:01, 94.0MB/s]
40%|###9 | 67.9M/170M [00:00<00:01, 95.5MB/s]
46%|####5 | 78.3M/170M [00:00<00:00, 98.1MB/s]
52%|#####1 | 88.1M/170M [00:01<00:00, 97.8MB/s]
57%|#####7 | 98.0M/170M [00:01<00:00, 98.1MB/s]
63%|######3 | 108M/170M [00:01<00:00, 98.6MB/s]
69%|######9 | 118M/170M [00:01<00:00, 98.1MB/s]
75%|#######4 | 128M/170M [00:01<00:00, 98.6MB/s]
81%|######## | 138M/170M [00:01<00:00, 97.3MB/s]
87%|########6 | 148M/170M [00:01<00:00, 98.7MB/s]
93%|#########2| 158M/170M [00:01<00:00, 97.9MB/s]
98%|#########8| 168M/170M [00:01<00:00, 98.5MB/s]
100%|##########| 170M/170M [00:01<00:00, 92.7MB/s]
'''
  • 可以通过如下方法打印训练图片:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import matplotlib.pyplot as plt
import numpy as np

# functions to show an image

def imshow(img):
img = img / 2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()

# get some random training images
dataiter = iter(trainloader)
images, labels = next(dataiter)

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))

'''
out:
frog plane deer car
'''

image.png

Define a Convolutional Neural Network

  • 将之前 神经网络部分 的神经网络代码复制过来,并修改它以处理 3 通道图像(而不是原本定义的 1 通道图像)
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
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = torch.flatten(x, 1) # flatten all dimensions except batch
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x


net = Net()

Define a Loss function and optimizer

  • 使用Classification Cross-Entropy loss and SGD with momentum
1
2
3
4
import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

Train the network

  • 只需要遍历数据迭代器,将输入数据传递给网络并进行优化即可:
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
38
39
40
for epoch in range(2):  # loop over the dataset multiple times

running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data

# zero the parameter gradients
optimizer.zero_grad()

# forward + backward + optimize
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()

# print statistics
running_loss += loss.item()
if i % 2000 == 1999: # print every 2000 mini-batches
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0

print('Finished Training')

'''
out:
[1, 2000] loss: 2.144
[1, 4000] loss: 1.835
[1, 6000] loss: 1.677
[1, 8000] loss: 1.573
[1, 10000] loss: 1.526
[1, 12000] loss: 1.447
[2, 2000] loss: 1.405
[2, 4000] loss: 1.363
[2, 6000] loss: 1.341
[2, 8000] loss: 1.340
[2, 10000] loss: 1.315
[2, 12000] loss: 1.281
Finished Training
'''
  • 可以通过下面的代码保存模型:
1
2
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

Test the network on the test data

  • 我们已经让网络在训练数据集上训练了 2 轮。但我们需要检查网络是否学到了任何东西

  • 通过预测神经网络输出的类别标签,并将其与真实标签进行比较来验证这一点。如果预测正确,我们将该样本添加到正确预测的列表中

  • 第一步:从测试集中显示一张图像,以便熟悉数据

1
2
3
4
5
6
7
8
9
10
11
dataiter = iter(testloader)
images, labels = next(dataiter)

# print images
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))

'''
out:
GroundTruth: cat ship ship plane
'''

image.png

  • 加载之前保存的模型(注意:在这里保存和重新加载模型并不是必需的,我们只是为了演示如何操作)
1
2
3
4
5
6
7
net = Net()
net.load_state_dict(torch.load(PATH, weights_only=True))

'''
out:
<All keys matched successfully>
'''
  • 现在让我们看看神经网络认为上述示例属于哪些类别,输出是 10 个类别的值。某个类别的值越高,网络就越认为图像属于该特定类别
1
2
3
4
5
6
7
8
9
10
outputs = net(images)
_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}'
for j in range(4)))

'''
out:
Predicted: cat ship truck ship
'''
  • 神经网络在整个数据集上的操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
correct = 0
total = 0
# since we're not training, we don't need to calculate the gradients for our outputs
with torch.no_grad():
for data in testloader:
images, labels = data
# calculate outputs by running images through the network
outputs = net(images)
# the class with the highest energy is what we choose as prediction
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print(f'Accuracy of the network on the 10000 test images: {100 * correct // total} %')

'''
out:
Accuracy of the network on the 10000 test images: 54 %
'''
  • 看起来比随机猜测要好得多,随机猜测的准确率是 10%(从 10 个类别中随机选择一个类别)。看来网络确实学到了一些东西

  • 下面来看看神经网络在各个类别上的预测

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
# prepare to count predictions for each class
correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0 for classname in classes}

# again no gradients needed
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predictions = torch.max(outputs, 1)
# collect the correct predictions for each class
for label, prediction in zip(labels, predictions):
if label == prediction:
correct_pred[classes[label]] += 1
total_pred[classes[label]] += 1

# print accuracy for each class
for classname, correct_count in correct_pred.items():
accuracy = 100 * float(correct_count) / total_pred[classname]
print(f'Accuracy for class: {classname:5s} is {accuracy:.1f} %')

'''
out:
Accuracy for class: plane is 37.9 %
Accuracy for class: car is 62.2 %
Accuracy for class: bird is 45.6 %
Accuracy for class: cat is 29.2 %
Accuracy for class: deer is 50.3 %
Accuracy for class: dog is 45.9 %
Accuracy for class: frog is 60.1 %
Accuracy for class: horse is 70.3 %
Accuracy for class: ship is 82.9 %
Accuracy for class: truck is 63.1 %
'''

Training on GPU

  • 就像将张量(Tensor)转移到 GPU 上一样,也可以将神经网络转移到 GPU 上

  • 首先,如果我们有可用的 CUDA 设备,我们将设备定义为第一个可见的 CUDA 设备

1
2
3
4
5
6
7
8
9
10
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# Assuming that we are on a CUDA machine, this should print a CUDA device:

print(device)

'''
out:
cuda:0
'''
  • 本节的其余部分假设 device 是一个 CUDA 设备。然后,这些方法会递归地遍历所有模块,并将它们的参数和缓冲区转换为 CUDA 张量

  • 请记住还需要在每一步将输入和目标数据也发送到 GPU:

1
2
3
net.to(device)

inputs, labels = data[0].to(device), data[1].to(device)