PyTorch学习笔记

在实验机器上配置好了CUDA、cuDNN和PyTorch,开始上手PyTorch。持续更新。

PyTorch学习笔记

0 学习资源

PyTorch 中文手册(pytorch handbook)

该教程内容简洁易读,且支持最新的PyTorch1.0正式版。以下是我学习及梳理的笔记。

PyTorch master documentation

PyTorch官方文档

1 背景

1.1 Jupyter Notebook

上述的学习资源中包含大量的Jupyter Notebook示例,需要安装Jupyter Notebook运行。

我安装了流行的科学计算包管理平台Anaconda3,即自带安装了Jupyter Notebook。

git clone 该教程后,在该教程根目录运行命令:

1
jupyter notebook

即可在弹出的窗口中看到当前教程文件夹内的目录结构,点击打开各个ipynb即可。

1.2 PyTorch

PyTorch实际上就是基于Python的科学计算包,服务于以下两种场景:

  • 作为NumPy的替代品,可以使用GPU的强大计算能力(GPU加速的张量计算)
  • 提供最大的灵活性和高速的深度学习研究平台(包含自动求导系统的深度神经网络)
1
2
3
4
5
6
7
# 首先要引入相关的包
import torch
import numpy as np
#打印一下版本
torch.__version__

# '1.0.1'

1.2.1 与Torch, Lua的关系

Torch是一个与Numpy类似的张量(Tensor)操作库,与Numpy不同的是Torch对GPU支持的很好。

Lua是Torch的上层包装。

PyTorch和Torch使用包含所有相同性能的C库:TH, THC, THNN, THCUNN,并且它们将继续共享这些库。PyTorch和Torch都使用的是相同的底层,只是使用了不同的上层包装语言。

注:LUA虽然快,但是太小众了,所以才会有PyTorch的出现。

2 张量

张量(Tensor)是PyTorch里面基础的运算单位,与Numpy的ndarray相同都表示的是一个多维的矩阵。 与ndarray的最大区别就是,PyTorch的Tensor可以在 GPU 上运行,而 numpy 的 ndarray 只能在 CPU 上运行,在GPU上运行大大加快了运算速度。

第零阶张量 (r = 0) 为标量 (Scalar),第一阶张量 (r = 1) 为向量 (Vector), 第二阶张量 (r = 2) 则成为矩阵 (Matrix),第三阶以上的统称为多维张量。

2.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
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import torch

x = torch.empty(5, 3) # 5行3列矩阵tensor,未初始化为任何值
# tensor([[3.5573e-09, 6.2618e+22, 4.7428e+30],
# [5.0778e+31, 1.8936e+23, 7.7151e+31],
# [2.9514e+29, 5.0850e+31, 7.5338e+28],
# [1.3556e-19, 1.8037e+28, 1.4229e-08],
# [6.2618e+22, 4.7428e+30, 5.0778e+31]])

x = torch.rand(5, 3) # 5行3列矩阵tensor,填充值为[0,1)间随机浮点数
# tensor([[0.9764, 0.2893, 0.7636],
# [0.4031, 0.1581, 0.9893],
# [0.0134, 0.0472, 0.5166],
# [0.6273, 0.5841, 0.0128],
# [0.3264, 0.5328, 0.0250]])

x = torch.zeros(5, 3)
# tensor([[0., 0., 0.],
# [0., 0., 0.],
# [0., 0., 0.],
# [0., 0., 0.],
# [0., 0., 0.]])
x = torch.zeros(5, 3, dtype=torch.long) # 5行3列矩阵tensor,填充值为长整型数0
# tensor([[0, 0, 0],
# [0, 0, 0],
# [0, 0, 0],
# [0, 0, 0],
# [0, 0, 0]])

x = torch.tensor([5.5, 3]) # 根据给定数据创建并初始化张量
# tensor([5.5000, 3.0000])

one = torch.ones(2, 2) # 全1矩阵
# tensor([[1., 1.],
# [1., 1.]])

eye=torch.eye(2,2) # 单位矩阵(主对角线元素全1,其余全0)
# tensor([[1., 0.],
# [0., 1.]])

x = x.new_ones(5, 3, dtype=torch.double) # new_*方法创建对象
x = torch.randn_like(x, dtype=torch.float) # 仍为5行3列的tensor对象,但值和类型发生了变化
# tensor([[1., 1., 1.],
# [1., 1., 1.],
# [1., 1., 1.],
# [1., 1., 1.],
# [1., 1., 1.]], dtype=torch.float64)
# tensor([[ 0.2814, 1.2299, 1.4216],
# [-0.2575, -1.0438, -1.2800],
# [ 1.0894, -0.2307, -1.5454],
# [-0.1985, -0.7991, 1.7902],
# [ 0.1705, 0.2637, -0.1507]])

x.size() # 输出tensor尺寸
# 或
x.shape # 与size()结果一致
# torch.Size([5, 3])

# 多维张量
y = torch.rand(2,3,4,5)
print(y.size()) # torch.Size([2, 3, 4, 5])
y
# tensor([[[[0.9071, 0.0616, 0.0006, 0.6031, 0.0714],
# [0.6592, 0.9700, 0.0253, 0.0726, 0.5360],
# [0.5416, 0.1138, 0.9592, 0.6779, 0.6501],
# [0.0546, 0.8287, 0.7748, 0.4352, 0.9232]],
#
# [[0.0730, 0.4228, 0.7407, 0.4099, 0.1482],
# [0.5408, 0.9156, 0.6554, 0.5787, 0.9775],
# [0.4262, 0.3644, 0.1993, 0.4143, 0.5757],
# [0.9307, 0.8839, 0.8462, 0.0933, 0.6688]],
#
# [[0.4447, 0.0929, 0.9882, 0.5392, 0.1159],
# [0.4790, 0.5115, 0.4005, 0.9486, 0.0054],
# [0.8955, 0.8097, 0.1227, 0.2250, 0.5830],
# [0.8483, 0.2070, 0.1067, 0.4727, 0.5095]]],
#
#
# [[[0.9438, 0.2601, 0.2885, 0.5457, 0.7528],
# [0.2971, 0.2171, 0.3910, 0.1924, 0.2570],
# [0.7491, 0.9749, 0.2703, 0.2198, 0.9472],
# [0.1216, 0.6647, 0.8809, 0.0125, 0.5513]],
#
# [[0.0870, 0.6622, 0.7252, 0.4783, 0.0160],
# [0.7832, 0.6050, 0.7469, 0.7947, 0.8052],
# [0.1755, 0.4489, 0.0602, 0.8073, 0.3028],
# [0.9937, 0.6780, 0.9425, 0.0059, 0.0451]],
#
# [[0.3851, 0.8742, 0.5932, 0.4899, 0.8354],
# [0.8577, 0.3705, 0.0229, 0.7097, 0.7557],
# [0.1505, 0.3527, 0.0843, 0.0088, 0.8741],
# [0.6041, 0.8797, 0.6189, 0.9495, 0.1479]]]])

# 标量
scalar = torch.tensor(3.1415926)
print(scalar) # tensor(3.1416)
scalar.size() # torch.Size([])
scalar.item() # 3.141592502593994

注意:

  • torch.rand方法是随机化[0,1)区间内的服从均匀分布的浮点数。
  • torch.randn方法是随机化输出零均值、单位方差的服从正态分布的浮点数。

2.2 数据类型

Tensor的基本数据类型有五种:

  • 32位浮点型:torch.FloatTensor (默认)
  • 64位整型:torch.LongTensor
  • 32位整型:torch.IntTensor
  • 16位整型:torch.ShortTensor
  • 64位浮点型:torch.DoubleTensor

除以上数字类型外,还有 byte和char型。

1
2
3
4
5
6
7
8
9
10
11
12
tensor = torch.tensor([3.1415926]) 
print(tensor) # tensor([3.1416])
tensor.size() # torch.Size([1])
tensor.item() # 3.141592502593994

tensor.long() # tensor([3])
tensor.half() # tensor([3.1406], dtype=torch.float16)
tensor.int() # tensor([3], dtype=torch.int32)
tensor.float() # tensor([3.1416])
tensor.short() # tensor([3], dtype=torch.int16)
tensor.char() # tensor([3], dtype=torch.int8)
tensor.byte() # tensor([3], dtype=torch.uint8)

2.3 基本运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
y = torch.rand(5, 3)

# 加法
print(x + y)
print(torch.add(x, y))

# 加法:提供tensor作为add的参数
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)

# 加法:v.xxx_(...)会改变原变量v
y.add_(x)
x.copy_(y) # 复制,会改变x
x.t_() # 转置,会改变x

2.4 基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 选取
x[:,1] # 表示矩阵tensor的第1列中所有行元素(从0计数)

# 转换尺寸
x = torch.randn(4, 4)
y = x.view(16) # 类似NumPy的reshape
z = x.view(-1, 8) # size -1 从其他维度推断
print(x.size(), y.size(), z.size())

# 读取数值 Tensor.item函数
x = torch.randn(1)
print(x) # tensor([-0.2368])
print(x.item()) # -0.23680149018764496
x = torch.randn(5,3)
print(x[0,0]) # tensor(0.4259)
print(x[0,0].item()) # 0.42591235041618347

# 求最大值
max_value, max_idx = torch.max(x, dim=1) # 沿着行取最大值

# 求和
sum_x = torch.sum(x, dim=1) # 沿着行求和

2.5 与NumPy互转

注意:Tensor和numpy对象共享内存,所以他们之间的转换很快,而且几乎不会消耗什么资源。但这也意味着,如果其中一个变了,另外一个也会随之改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Torch Tensor转NumPy Array
a = torch.ones(5) # tensor([1., 1., 1., 1., 1.])
b = a.numpy() # [1. 1. 1. 1. 1.]
# 注意!b与a保持绑定关系,如:
a.add_(1)
print(a) # tensor([2., 2., 2., 2., 2.])
print(b) # [2. 2. 2. 2. 2.]

# NumPy Array转Torch Tensor
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
# 注意!b与a保持绑定关系,如:
np.add(a, 1, out=a)
print(a) # [2. 2. 2. 2. 2.]
print(b) # tensor([2., 2., 2., 2., 2.], dtype=torch.float64)

# 所有的 Tensor 类型默认都是基于CPU, CharTensor 类型不支持到 NumPy 的转换.

2.6 CUDA Tensor

在支持CUDA的NVIDIA GPU设备上,可以在GPU上建立Tensor进行运算。

2.6.1 .cuda和.cpu方法

一般情况下可以使用.cuda方法将tensor移动到GPU,这步操作需要CUDA设备支持。

使用.cpu方法可以把tensor移动到CPU。

1
2
3
4
5
6
7
8
cpu_a=torch.rand(4, 3)
cpu_a.type() # 'torch.FloatTensor'

gpu_a=cpu_a.cuda() # .cuda方法移动tensor到GPU
gpu_a.type() # 'torch.cuda.FloatTensor'

cpu_b=gpu_a.cpu() # .cpu方法移动tensor到CPU
cpu_b.type() # 'torch.FloatTensor'

2.6.2 .to方法

使用.to 方法 可以将Tensor移动到任何设备中。

例1:

1
2
3
4
5
6
7
8
9
10
11
# is_available 函数判断是否有cuda可以使用
# ``torch.device``将张量移动到指定的设备中
if torch.cuda.is_available():
device = torch.device("cuda") # a CUDA 设备对象
y = torch.ones_like(x, device=device) # 直接在GPU上创建张量y
x = x.to(device) # 或者直接使用``.to("cuda")``将张量x移动到cuda中
z = x + y
print(z)
print(z.to("cpu", torch.double)) # ``.to`` 也会对变量的类型做更改
# tensor([0.6132], device='cuda:0')
# tensor([0.6132], dtype=torch.float64)

例2:

1
2
3
4
5
6
#使用torch.cuda.is_available()来确定是否有cuda设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device) # cuda
#将tensor传送到设备
gpu_b = cpu_b.to(device)
gpu_b.type() # 'torch.cuda.FloatTensor'

3 Autograd自动求导

深度学习的算法本质上是通过反向传播求导数,而PyTorch的autograd模块则实现了此功能。在Tensor上的所有操作,autograd都能为它们自动提供微分,避免了手动计算导数的复杂过程。

3.1 标量

我读了教程和官方文档,发现还是有个例子会更快理解。

超简单!pytorch入门教程(二):Autograd

这篇简书文章的例子有助于快速理解,值得一读。有趣的是,该文的例子中,函数值y和y对x在x1=1时的偏导均为4根号2,即5.6569。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from torch.autograd import Variable
import torch
x = Variable(torch.ones(2), requires_grad = True) # Variable是tensor的一个外包装
z = 4*x*x
y = z.norm() # y最终为标量
print(y)
# Variable containing:
# 5.6569
# [torch.FloatTensor of size 1]

y.backward() # backward()函数表示backprop
print(x.grad) # 返回y关于x的梯度向量
# Variable containing:
# 5.6569
# 5.6569
# [torch.FloatTensor of size 2]

在这个例子中,可以看到,当我们需要运行反向传播(Back Propagation)算法时,直接调取Variable.grad即可得知其梯度值。这就是PyTorch的Autograd机制自动求导的结果。

注:从0.4起, Variable 正式合并入Tensor, Variable 本来实现的自动微分功能,Tensor就能支持。读者还是可以使用Variable(tensor), 但是这个操作其实什么都没做。所以,以后的代码建议直接使用Tensor,因为官方文档中已经将Variable设置成过期模块要想使得Tensor使用autograd功能,只需要设置tensor.requries_grad=True

3.2 向量

上述的例子中,因为y是标量(scalar),所以y.backward()相当于y.backward(torch.tensor(1))。但如果y是向量时,y.backward()需要输入参数grad_tensors表示下降梯度向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch

x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000:
y = y * 2

print(y) # tensor([ 491.3611, 545.1010, -1226.0724], grad_fn=<MulBackward0>)

gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(gradients) # y是3维向量

# 另可:我们的返回值不是一个scalar,所以需要输入一个大小相同的张量作为参数,这里我们用ones_like函数根据x生成一个张量
# y.backward(torch.ones_like(x))

print(x.grad) # tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])

这篇文章做了详细的向量autograd的分析:

Pytorch中的backward - CSDN

3.3 禁用autograd

如果.requires_grad=True但是你又不希望进行autograd的计算, 那么可以将变量包裹在 with torch.no_grad()中:

1
2
3
4
5
print(x.requires_grad)			# True
print((x ** 2).requires_grad) # True

with torch.no_grad():
print((x ** 2).requires_grad) # False

这个方法在测试集测试准确率的时候回经常用到。

4 神经网络

torch.nn - PyTorch master documentation

详细文档参阅PyTorch官方文档关于nn包的信息。

4.1 定义网络

使用torch.nn包来构建神经网络。

nn包依赖autograd包来定义模型并求导。 一个nn.Module包含各个层和一个forward(input)方法,该方法返回output

除了nn别名以外,我们还引用了nn.functional,这个包中包含了神经网络中使用的一些常用函数,这些函数的特点是,不具有可学习的参数(如ReLU,pool,DropOut等),这些函数可以放在构造函数中,也可以不放,但是这里建议不放。

一般情况下我们会将nn.functional 设置为大写的F,这样缩写方便调用

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
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
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) # 输入16个卷积核,各5×5像素
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)

# 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
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight

# 10
# torch.Size([6, 1, 5, 5])

测试随机输入32×32。 注:这个网络(LeNet)期望的输入大小是32×32,如果使用MNIST数据集来训练这个网络,请把图片大小重新调整到32×32。

1
2
3
4
5
6
input = torch.randn(1, 1, 32, 32)	# samples, channels, height, width
out = net(input)
print(out)

# tensor([[-0.0204, -0.0268, -0.0829, 0.1420, -0.0192, 0.1848, 0.0723, -0.0393,
# -0.0275, 0.0867]], grad_fn=<ThAddmmBackward>)

将所有参数的梯度缓存清零,然后进行随机梯度的的反向传播:

1
2
net.zero_grad()		# 否则梯度(.grad)会累加到已存在的梯度上
out.backward(torch.randn(1, 10)) # 此处还未定义损失函数,仅用out反向传播作为示例

注意:torch.nn 只支持mini-batch输入

torch.nn 只支持小批量输入。整个 torch.nn 包都只支持小批量样本,而不支持单个样本。 例如,nn.Conv2d 接受一个4维的张量, 每一维分别是sSamples * nChannels * Height * Width(样本数*通道数*高*宽)。 如果你有单个样本,只需使用 input.unsqueeze(0) 来添加其它的维数。

4.2 损失函数

一个损失函数接受一对 (output, target) 作为输入,计算一个值来估计网络的输出和目标值相差多少。

output就是神经网络的输出结果,target就是数据的标记值。

nn包中有很多不同的损失函数nn.MSELoss是一个比较简单的损失函数,它计算输出和目标间的均方误差, 例如:

1
2
3
4
5
6
7
8
output = net(input)			# 1×10 tensor
target = torch.randn(10) # 随机值作为样例
target = target.view(1, -1) # 使target和output的shape相同,转换为1×10 tensor
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)
# tensor(1.3172, grad_fn=<MseLossBackward>)

当反向传播计算loss时,读取.grad_fn属性,就可以看到一个图(graph):

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

此时,如果调用loss.backward()函数,整个图都会去求loss的微分,图中属性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

4.3 反向传播

调用loss.backward()获得反向传播的误差。

但是在调用前需要清除已存在的梯度,否则梯度将被累加到已存在的梯度。

现在,我们将调用loss.backward(),并查看conv1层的偏差(bias)项在反向传播前后的梯度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
net.zero_grad()     # 清除梯度

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

loss.backward()

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

# conv1.bias.grad before backward
# tensor([0., 0., 0., 0., 0., 0.]) 反向传播前梯度为0
# conv1.bias.grad after backward
# tensor([ 0.0074, -0.0249, -0.0107, 0.0326, -0.0017, -0.0059]) 反向传播后各bias参数出现了梯度值

4.4 更新权重

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

1
weight = weight - learning_rate * gradient

我们可以使用简单的Python代码实现这个规则:

1
2
3
learning_rate = 0.01
for f in net.parameters(): # 遍历net对象中的所有参数进行更新
f.data.sub_(f.grad.data * learning_rate)

但是当使用神经网络是想要使用各种不同的更新规则时,比如SGD、Nesterov-SGD、Adam、RMSProp等,PyTorch中构建了一个包torch.optim实现了所有的这些规则。 使用它们非常简单:

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

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

for input in input_batches:
# in your training loop:
optimizer.zero_grad() # 清空累加的梯度值,和net.zero_grad()效果一致
output = net(input) # 每次一个mini-batch的数据
loss = criterion(output, target)
loss.backward()
optimizer.step() # Does the update

5 数据加载与预处理

PyTorch通过torch.utils.data对一般常用的数据加载进行了封装,可以很容易地实现多线程数据预读和批量加载。 并且torchvision已经预先实现了常用图像数据集,包括CIFAR-10、ImageNet、COCO、MNIST、LSUN等数据集,可通过torchvision.datasets方便的调用。

5.1 Dataset

Dataset是一个抽象类, 为了能够方便的读取,需要将要使用的数据包装为Dataset类。 自定义的Dataset需要继承它并且实现两个成员方法:

  1. __getitem__() 该方法定义每次怎么获取数据
  2. __len__() 该方法返回数据集的总长度

下面我们使用kaggle上的一个竞赛bluebook for bulldozers自定义一个数据集,为了方便介绍,我们使用里面的数据字典来做说明(因为条数少)

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
from torch.utils.data import Dataset
import pandas as pd

# 定义一个数据集类
class BulldozerDataset(Dataset):
""" 数据集演示 """
def __init__(self, csv_file):
"""实现初始化方法,在初始化的时候将数据读载入"""
self.df = pd.read_csv(csv_file) # 初始化DataFrame对象
def __len__(self):
'''
返回df的长度
'''
return len(self.df)
def __getitem__(self, idx):
'''
根据IDX返回一列数据
'''
return self.df.iloc[idx].SalePrice


# 实例化和使用数据集对象
ds_demo = BulldozerDataset('median_benchmark.csv')

len(ds_demo) # 实现了__len__方法所以可以直接使用len获取数据总数
ds_demo[0] # 用索引可以直接访问对应的数据

# 11573
# 24000.0

5.2 DataLoader

torch.utils.data.DataLoader

DataLoader为我们提供了对Dataset读取操作,常用参数有:

  • batch_size (每个batch的大小)
  • shuffle (是否进行shuffle操作,即在每轮epoch时是否对数据重新洗牌)
  • num_workers (加载数据的时候使用几个子进程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
dl = torch.utils.data.DataLoader(ds_demo, batch_size=10, shuffle=True, num_workers=0)

# 读取单批数据示例
idata = iter(dl)
print(next(idata))
# tensor([24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000.,
# 24000.], dtype=torch.float64)

# 遍历所有批数据示例
for i, data in enumerate(dl):
print(i,data)
break # 这里只循环一遍
# 0 tensor([24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000.,
# 24000.], dtype=torch.float64)

5.3 torchvision

torchvision是PyTorch中专门用来处理图像的库。

5.3.1 torchvision.datasets

torchvision.datasets可以理解为PyTorch团队自定义的Dataset,这些Dataset帮我们提前处理好了很多的图片数据集,我们拿来就可以直接使用:

  • MNIST
  • COCO
  • Captions
  • Detection
  • LSUN
  • ImageFolder
  • Imagenet-12
  • CIFAR
  • STL10
  • SVHN
  • PhotoTour

我们可以直接使用,示例如下:

1
2
3
4
5
6
import torchvision.datasets as datasets
trainset = datasets.MNIST(root='./data', # 表示 MNIST 数据的加载的目录
train=True, # 表示是否加载数据库的训练集,false的时候加载测试集
download=True, # 表示是否自动下载 MNIST 数据集
transform=None) # 表示是否需要对数据进行预处理,none为不进行预处理

5.3.2 torchvision.models

torchvision不仅提供了常用图片数据集,还提供了训练好的模型,可以加载之后,直接使用,或者在进行迁移学习 torchvision.models模块的 子模块中包含以下模型结构。

  • AlexNet
  • VGG
  • ResNet
  • SqueezeNet
  • DenseNet
1
2
3
#我们直接可以使用训练好的模型,当然这个与datasets相同,都是需要从服务器下载的
import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)

5.3.3 torchvision.transforms

transforms模块提供了一般的图像转换操作类,用作数据的处理和增广。

1
2
3
4
5
6
7
8
from torchvision import transforms as transforms
transform = transforms.Compose([
transforms.RandomCrop(32, padding=4), #先四周填充0,在吧图像随机裁剪成32*32
transforms.RandomHorizontalFlip(), #图像一半的概率翻转,一半的概率不翻转
transforms.RandomRotation((-45,45)), #随机旋转
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.229, 0.224, 0.225)), #R,G,B每层的归一化用到的均值和方差
])

肯定有人会问:(0.485, 0.456, 0.406), (0.2023, 0.1994, 0.2010) 这几个数字是什么意思?

官方的这个帖子有详细的说明: https://discuss.pytorch.org/t/normalization-in-the-mnist-example/457/21 这些都是根据ImageNet训练的归一化参数,可以直接使用,我们认为这个是固定值就可以

6 图像分类实例

基于CIFAR10数据集,实现一个图像分类器实例。

训练一个典型的图像分类分类器依次按照下列顺序进行:

  1. 使用torchvision加载和归一化CIFAR10训练集和测试集
  2. 定义一个卷积神经网络
  3. 定义损失函数
  4. 在训练集上训练网络
  5. 在测试集上测试网络

6.1 处理数据

一般情况下处理图像、文本、音频和视频数据时,可以使用标准的Python包来加载数据到一个numpy数组中。 然后把这个数组转换成 torch.*Tensor

  • 图像可以使用 Pillow, OpenCV
  • 音频可以使用 scipy, librosa
  • 文本可以使用原始Python和Cython来加载,或者使用 NLTK或 SpaCy 处理

特别的,对于图像任务,我们创建了一个包 torchvision,它包含了处理一些基本图像数据集的方法。这些数据集包括 Imagenet, CIFAR10, MNIST 等。除了数据加载以外,torchvision 还包含了图像转换器, torchvision.datasetstorch.utils.data.DataLoader

torchvision包不仅提供了巨大的便利,也避免了代码的重复。

在这个教程中,我们使用CIFAR10数据集,它有如下10个类别 :‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。CIFAR-10的图像都是 3x32x32大小的,即,3颜色通道,32x32像素。

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

trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) # 配置CIFAR10训练集
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2) # 配置数据加载器,每个mini-batch含4张图

testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) # 配置CIFAR10测试集
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2) # 配置数据加载器,测试时也是4个图一个mini-batch

classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') # 共10分类

可以通过matplotlib库来绘图查看数据集中的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import matplotlib.pyplot as plt
import numpy as np

# 展示图像的函数
def imshow(img):
img = img / 2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))


# 获取随机数据
dataiter = iter(trainloader) # 配置迭代器从数据加载器中读取数据
images, labels = dataiter.next() # 读取下一项数据

# 展示图像
imshow(torchvision.utils.make_grid(images)) # make_grid把mini-batch的图片张量变成图片网格
# 显示图像标签
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

6.2 定义卷积神经网络模型

从之前的神经网络一节复制神经网络代码,并修改为输入3通道图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5) # 3通道(彩色图片),6个5×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 = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

net = Net()

6.3 定义损失函数与优化器

我们使用交叉熵作为损失函数,使用带动量的随机梯度下降。

1
2
3
4
import torch.optim as optim

criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

6.4 训练神经网络模型

有趣的时刻开始了。 我们只需在数据迭代器上循环,将数据输入给网络,并优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for epoch in range(2):  # 多轮(epoch)训练
running_loss = 0.0
for i, data in enumerate(trainloader, 0): # 多mini-batch循环
# 获取输入
inputs, labels = data

# 梯度置0(清空累加grad)
optimizer.zero_grad()

# 正向传播,反向传播,优化
outputs = net(inputs) # 模型正向传播,inputs=>outputs
loss = criterion(outputs, labels) # 调用交叉熵损失
loss.backward() # 对损失进行反向传播
optimizer.step() # 对参数梯度下降一步

# 打印状态信息
running_loss += loss.item()
if i % 2000 == 1999: # 每2000批次打印一次
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0 # running_loss是2000个batch的loss和

print('Finished Training')

6.5 测试神经网络模型

我们在整个训练集上进行了2轮(epoch)训练,但是我们需要检查网络是否从数据集中学习到有用的东西。 通过预测神经网络输出的类别标签与实际情况标签进行对比来进行检测。 如果预测正确,我们把该样本添加到正确预测列表。 第一步,显示测试集中的图片并熟悉图片内容。

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
dataiter = iter(testloader)
images, labels = dataiter.next()

# 显示图片
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

# 对模型输入图片
outputs = net(images)

# 输出是10个标签的分值。
# 一个类别的分值越大,神经网络越认为它是这个类别。所以让我们得到最高能量的标签。
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4))) # 列表推导式

# 统计模型在整个测试集上的测试结果
correct = 0
total = 0
with torch.no_grad(): # 禁用梯度计算
for data in testloader: # 加载测试数据
images, labels = data
outputs = net(images) # 正向传播
_, predicted = torch.max(outputs.data, 1) # 计算mini-batch的预测结果
total += labels.size(0) # 统计已测试图片个数
correct += (predicted == labels).sum().item() # 统计预测正确的个数

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

# Accuracy of the network on the 10000 test images: 9 %

结果看起来不错,至少比随机选择要好,随机选择的正确率为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
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item() # 统计每类的正确数
class_total[label] += 1 # 统计每类的预测总数

for i in range(10):
print('Accuracy of %5s : %2d %%' % (
classes[i], 100 * class_correct[i] / class_total[i]))

# Accuracy of plane : 99 %
# Accuracy of car : 0 %
# Accuracy of bird : 0 %
# Accuracy of cat : 0 %
# Accuracy of deer : 0 %
# Accuracy of dog : 0 %
# Accuracy of frog : 0 %
# Accuracy of horse : 0 %
# Accuracy of ship : 0 %
# Accuracy of truck : 0 %

6.6 在GPU上训练

6.6.1 检查GPU支持

1
2
3
4
5
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 确认我们的电脑支持CUDA,然后显示CUDA信息:
print(device)

# device(type='cuda', index=0)

本节的其余部分假定device是CUDA设备。

6.6.2 神经网络模型载入CUDA

torch.nn.Module.to将递归遍历所有模块并将模块的参数和缓冲区转换成CUDA张量:

1
net.to(device)		# 把神经网络模型载入CUDA

torch.nn.Module.to

6.6.3 输入数据载入CUDA

记住:inputs 和 targets 也要转换。

1
inputs, labels = inputs.to(device), labels.to(device)

torch.Tensor.to

为什么我们没注意到GPU的速度提升很多?那是因为网络非常的小。

实践: 尝试增加你的网络的宽度(第一个nn.Conv2d的第2个参数,第二个nn.Conv2d的第一个参数,它们需要是相同的数字),看看你得到了什么样的加速。