1 什么是 PyTorch?
PyTorch 是一个基于 python 的科学计算包,其特点是:
- 替代 Numpy 以使用 GPU 的运算能力
- 一个深度学习的研究平台,提供最大化的灵活性和速度
1.1 入门
1.1.1 张量
Tensors(张量)与 Numpy 的 ndarrays 类似,但是其支持在 GPU 上使用来加速计算。
代码语言:javascript复制from __future__ import print_function
import torch
下面给出一些张量的使用案例:
创建一个没有初始化的 5*3 矩阵:
代码语言:javascript复制x = torch.empty(5, 3)
print(x)
# Output
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
创建一个随机初始化的矩阵:
代码语言:javascript复制x = torch.rand(5, 3)
print(x)
# Output
tensor([[0.5173, 0.7670, 0.7260],
[0.8115, 0.4518, 0.3867],
[0.6968, 0.8738, 0.8632],
[0.6845, 0.5106, 0.0381],
[0.4081, 0.1894, 0.7733]])
构造一个填满 0 且数据类型为 long 的矩阵:
代码语言:javascript复制x = torch.zeros(5, 3, dtype=torch.long)
print(x)
# Output
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
直接从数据构造张量:
代码语言:javascript复制x = torch.tensor([5.5, 3])
print(x)
# Output
tensor([5.5000, 3.0000])
根据已有的张量建立新的张量:
代码语言:javascript复制x = x.new_ones(5, 3, dtype=torch.double) # new_* methods take in sizes
print(x)
# Output
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
x = torch.randn_like(x, dtype=torch.float) # override dtype & has the same size
print(x)
# Output
tensor([[ 1.5360, -1.1798, 0.5190],
[-0.3277, 0.9304, 1.9112],
[-0.0136, 0.9135, -1.1442],
[-0.2182, -0.7076, -0.9888],
[-0.1883, -0.2476, 1.6226]])
获取张量的形状:
代码语言:javascript复制print(x.size())
# Output
torch.Size([5, 3])
torch.Size
本质上是一个 tuple,所以支持 tuple 的一切操作。
1.1.2 运算
pytorch 支持多种运算,而每种运算又有着很多种语法。下面给出一些关于加法运算的例子:
「加法」:形式一:
代码语言:javascript复制y = torch.rand(5, 3)
print(x y)
「加法」:形式二:
代码语言:javascript复制print(torch.add(x, y))
「加法」:给定一个输出张量作为参数:
代码语言:javascript复制result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)
「加法」:原地操作(in-place):
代码语言:javascript复制# adds x to y
y.add_(x)
print(y)
任何一个 in-place 改变张量的操作后面都固定一个 _
,例如 x.copy_(y)
、x.t_()
将更改 x
。
上面四种形式的加法均会输出同样的结果:
代码语言:javascript复制# Output
tensor([[ 2.5202, -0.4201, 0.5584],
[ 0.0592, 1.3295, 2.8773],
[ 0.2609, 1.2124, -1.0648],
[-0.1337, -0.1146, -0.3190],
[-0.1339, 0.1608, 1.9009]])
下面再给出一些其他操作的例子:
使用像标准的 NumPy 一样的各种索引操作:
代码语言:javascript复制print(x[:, 1])
# Output
tensor([-1.1798, 0.9304, 0.9135, -0.7076, -0.2476])
改变张量的形状:
代码语言:javascript复制x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8) # the size -1 is inferred from other dimensions
print(x.size(), y.size(), z.size())
# Output
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
如果是仅包含一个元素的张量,可以使用 .item()
来得到对应的数值:
x = torch.randn(1)
print(x)
print(x.item())
# Output
tensor([-0.5599])
-0.5598675012588501
1.2 Numpy 桥
我们可以轻而易举地在 numpy 数组和 pytorch 张量之间相互转换,两者将共享它们的底层内存位置,更改一个将引起另一个的改变。(需要注意,前提条件是张量位于 CPU ,且 CharTensor 不支持转换)
张量转数组:
代码语言:javascript复制a = torch.ones(5)
print(a)
# Output
tensor([1., 1., 1., 1., 1.])
b = a.numpy()
print(b)
# Output
[1. 1. 1. 1. 1.]
a.add_(1) # both a and b will change
print(a)
print(b)
# Output
tensor([2., 2., 2., 2., 2.])
[2. 2. 2. 2. 2.]
数组转张量:
代码语言:javascript复制import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)
# Output
[2. 2. 2. 2. 2.]
tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
1.3 CUDA 上的张量
张量可以使用 .to
方法移动到任何设备(device)上:
# let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():
device = torch.device("cuda") # a CUDA device object
y = torch.ones_like(x, device=device) # directly create a tensor on GPU
x = x.to(device) # or just use strings ``.to("cuda")``
z = x y
print(z)
print(z.to("cpu", torch.double)) # ``.to`` can also change dtype together!
程序输出:
代码语言:javascript复制tensor([2.0897], device='cuda:0')
tensor([2.0897], dtype=torch.float64) # double equals to float64
2 Autograd:自动求导
在 PyTorch 中,所有神经网络的核心都是 autograde
包,它是一个在运行时定义的框架,为张量上的所有运算提供了「自动求导」机制。下面将通过一些例子介绍自动求导包的基本操作。
2.1 张量
torch.Tensor
是这个包的核心类。如果我们设置其属性 .requires_grad
为 True
,那么它将会追踪对于该张量的所有操作。当完成计算(前向传播)后,调用 .backward()
函数,即可自动计算所有的梯度。该张量的所有梯度将会自动累加到 .grad
属性。如果 Tensor
是一个标量(即只包含一个元素的数据),则不需要指定任何参数但是如果它有更多的元素,否则需要指定一个 gradient
参数,其形状与输出(该张量)匹配。
有时候,我们可能不再需要追踪一个张量的梯度(例如在进行模型评估时),这时可以使用 .detach()
方法或将代码块包装在 with torch.no_grad():
中,来防止跟踪历史记录和使用内存。
此外,还有一个类对 autograd 的实现非常重要:Function
。该类和 Tensor
类互相连接构成了一个无环图,编码完整的计算历史,以便进行梯度计算。每个张量都有一个 .grad_fn
属性,它引用了一个创建了这个张量的 Function
,除非这个张量是用户直接手动创建的,即这个张量的 grad_fn
是 None
。
下面通过一个实际的例子对上述操作进行展示。首先创建一个张量并设置 requires_grad=True
来追踪其计算历史:
import torch
x = torch.ones(2, 2, requires_grad=True)
print(x)
# Output
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
对这个张量进行一次运算:
代码语言:javascript复制y = x 2
print(y)
# Output
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)
y
是计算结果,其具有 grad_fn
属性。
我们对 y
进行更多的运算:
z = y * y * 3
out = z.mean()
print(z, out)
# Output
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
2.2 梯度
现在开始进行反向传播,由于 out
是一个标量,因此 out.backward()
和 out.backward(torch.tensor(1.))
等价。
out.backward()
print(x.grad)
# Output
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
上述结果对应的求导过程为:我们有
以及
,因此
,从而得到
。
在数学上,若有向量值函数
,那么
相对于
的梯度是一个雅可比矩阵:
对于非标量的张量,自动求导包实际上求的是「雅克比向量」。雅克比向量即给定任意向量
,计算乘积
。如果
恰好是一个标量函数
的导数,即
,那么根据链式法则,雅可比向量积应该是
对
的导数:
下面给出一个雅克比向量积的例子:
代码语言:javascript复制x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000: # 计算 L2 范数(平方和开根号)等价于 torch.sqrt(torch.sum(torch.pow(y, 2)))
y = y * 2
print(y)
# Output
tensor([601.5915, -307.0995, -745.0810], grad_fn=<MulBackward0>)
在本例中 y
不再是一个标量,torch.autograd
不能直接计算完整的雅可比矩阵,但如果我们只想要雅可比向量积,只需要将向量作为参数传给 backward
即可:
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)
# Output
tensor([1.0240e 02, 1.0240e 03, 1.0240e-01])
3 神经网络
本章将介绍如何用 PyTorch 构建一个神经网络。
我们可以使用 torch.nn
来构建网络,nn
包依赖于 autograd
包来定义模型并对它们求导。一个 nn.Module
包含各个层和一个 forward(input)
方法用来计算并返回 output
一个神经网络的典型训练过程如下:
- 定义包含可学习参数(权重)的神经网络
- 在输入数据集上进行迭代
- 通过网络处理输入得到输出
- 计算损失函数
- 将梯度反向传播给网络的参数
- 更新网络的权重
下面将通过代码介绍上述训练过程。
3.1 定义网络
通过如下代码定义网络:
代码语言:javascript复制import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module): # 继承nn.Module类,并实现forward方法
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)
我们只需要定义 forward
函数,backward
函数会在使用 autograd
时自动定义。注意 view()
方法用于 reshape 张量的形状,-1 表示该维度基于其他维度来决定。
输出:
代码语言:javascript复制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)
)
一个模型的可学习参数可以通过 net.parameters()
返回:
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
# Output
10 # include the bias(intercept)
torch.Size([6, 1, 3, 3])
3.2 计算输出
下面我们尝试一个随机的 32*32 输入,来计算网络的输出:
代码语言:javascript复制input = torch.randn(1, 1, 32, 32)
out = net(input) # equal to net.foward(input)
print(out)
# Output
ensor([[-0.0252, -0.1307, 0.0111, 0.0837, 0.0856, 0.2049, 0.1760, -0.1589,
-0.0831, 0.1625]], grad_fn=<AddmmBackward>)
注意:torch.nn
只支持小批量处理,不支持单独样本。例如 nn.Conv2d
接受一个4维的张量,即 nSamples x nChannels x Height x Width
。如果是一个单独的样本,只需要使用 input.unsqueeze(0)
来添加一个“假的” 批大小维度。
3.3 损失函数
一个损失函数接受一对 (output, target) 作为输入,计算一个值来估计网络的输出和目标值相差多少。nn
包中有很多不同的损失函数,其中比较简单的是 nn.MSELoss
,即输出与目标的均方误差:
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)
# Output
tensor(1.2990, grad_fn=<MseLossBackward>)
如果使用 loss
的 .grad_fn
属性跟踪反向传播过程,会看到如下的计算图:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
当我们调用 loss.backward()
,整张图开始关于 loss
微分,图中所有设置了 requires_grad=True
的张量的 .grad
属性将累积梯度张量:
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
# Output
<MseLossBackward object at 0x7f97fcafbb00>
<AddmmBackward object at 0x7f97fcafbcf8>
<AccumulateGrad object at 0x7f97fcafbcf8>
3.4 反向传播
我们只需要调用 loss.backward()
来反向传播权重。注意需要清零现有的梯度,否则梯度将会与已有的梯度累加。
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)
输出:
代码语言:javascript复制conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([-0.0333, -0.0440, 0.0102, 0.0238, 0.0056, 0.0075])
3.5 更新权重
最简单的更新规则是随机梯度下降法(SGD):
代码语言:javascript复制weight = weight - learning_rate * gradient
由于只接受批量数据,因此实际上这是一种小批量梯度下降。我们可以通过如下代码实现:
代码语言:javascript复制learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
而在实际应用中,我们可能希望使用各种不同的更新规则,如 SGD、Nesterov-SGD、Adam、RMSprop 等,PyTorch 提供了一个较小的包 torch.optim
,它实现了所有的这些方法:
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()
手动清除梯度缓存。
4 训练分类器
4.1 数据
一般来说,当我们需要处理图片、文本、音频或视频数据时,我们首先使用标准的 python 库将数据载入到一个 numpy 数组中,然后将该数组转化为一个 torch 张量:
- 对于图片,标准库有 Pillow、OpenCV 等
- 对于音频,标准库有 scipy、librosa 等
- 对于文本,可以使用原生的 Python 或 Cython 载入,也可以使用 NLTK、SpaCy 等包
对于视觉(图片)方面,我们创建了一个叫做 torchvision
的包,其中包含了针对 Imagenet、CIFAR10、MINST 等常用数据集的数据加载器,以及对图片变形的操作。
下面的例子中将使用 CIFAR10 数据集,其有如下的分类。图片的数据大小为 3x32x32:
4.2 训练一个图片分类器
我们将进行如下操作:
- 通过
torchvision
加载 CIFAR10 训练和测试数据集,并对其进行标准化 - 定义卷积神经网络
- 定义损失函数
- 基于训练数据训练网络
- 基于测试数据测试网络
4.2.1 加载并标准化 CIFAR10
使用 torchvision
,加载 CIFAR10 非常的简单:
import torch
import torchvision
import torchvision.transforms as transforms
torchvision 输出的数据集为 [0,1] 之间的 PILImage,我们将其标准化为 [-1,1] 之间的张量:
代码语言:javascript复制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)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, 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=4, shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
下面查看一下部分训练图片:
代码语言:javascript复制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 = dataiter.next()
# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
输出:
4.2.2 定义卷积神经网络
基于之前介绍的神经网络定义,对其稍作修改,可以得到如下的定义:
代码语言:javascript复制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)
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()
4.2.3 定义损失函数和优化器
我们使用分类的「交叉熵」作为损失函数,选择「使用动量的随机梯度下降」作为优化器:
代码语言:javascript复制import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
4.2.4 训练网络
下面开始进行训练。我们只需要遍历训练数据的迭代器,将输入喂给网络并进行优化即可,代码十分的简单:
代码语言:javascript复制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('[%d, ]] loss: %.3f' %
(epoch 1, i 1, running_loss / 2000))
running_loss = 0.0
print('Finished Training')
训练过程的输出如下:
代码语言:javascript复制[1, 2000] loss: 2.154
[1, 4000] loss: 1.853
[1, 6000] loss: 1.645
[1, 8000] loss: 1.582
[1, 10000] loss: 1.513
[1, 12000] loss: 1.473
[2, 2000] loss: 1.404
[2, 4000] loss: 1.380
[2, 6000] loss: 1.344
[2, 8000] loss: 1.347
[2, 10000] loss: 1.307
[2, 12000] loss: 1.298
Finished Training
我们可以通过下面的代码快速存储训练好的模型:
代码语言:javascript复制PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)
4.2.5 使用测试数据测试网络
训练完成后,我们需要在测试集上测试分类效果。首先展示一下测试集数据:
代码语言:javascript复制dataiter = iter(testloader)
images, labels = dataiter.next()
# print images
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
输出:
下面读取模型并查看模型的预测结果:
代码语言:javascript复制net = Net()
net.load_state_dict(torch.load(PATH))
outputs = net(images)
_, predicted = torch.max(outputs, 1) # 得到最高概率的下标
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
for j in range(4)))
输出结果为:
代码语言:javascript复制Predicted: dog ship ship plane
下面再计算一下整个测试集上的表现:
代码语言:javascript复制correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total = labels.size(0)
correct = (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
# Output
Accuracy of the network on the 10000 test images: 55 %
这看起来比随机选择(10%)要好很多。下面再看一下模型在每个类上的表现:
代码语言:javascript复制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 : - %%' % (
classes[i], 100 * class_correct[i] / class_total[i]))
输出结果为:
代码语言:javascript复制Accuracy of plane : 67 %
Accuracy of car : 54 %
Accuracy of bird : 31 %
Accuracy of cat : 29 %
Accuracy of deer : 41 %
Accuracy of dog : 54 %
Accuracy of frog : 64 %
Accuracy of horse : 57 %
Accuracy of ship : 61 %
Accuracy of truck : 70 %
4.3 在 GPU 上训练
与将一个张量传递给 GPU 一样,我们可以将神经网络转移到 GPU 上。首先定义一个 cuda 设备:
代码语言:javascript复制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)
# Output
cuda:0
然后调用如下方法,其会递归遍历所有模块,将它们的参数和缓冲区转换为 CUDA 张量:
代码语言:javascript复制net.to(device)
注意需要将输入和标签在每一步都送进 GPU 中:
代码语言:javascript复制inputs, labels = data[0].to(device), data[1].to(device)
由于网络本身不大,所以在 GPU 上训练也不会带来过多的速度提升。