共计 9232 个字符,预计需要花费 24 分钟才能阅读完成。
2017 年初,Facebook 在机器学习和科学计算工具 Torch 的基础上,针对 Python 语言发布了一个全新的机器学习工具包 PyTorch。因其在灵活性、易用性、速度方面的优秀表现,经过 2 年多的发展,目前 PyTorch 已经成为从业者最重要的研发工具之一。
内容整理于 PyTorch 深度学习基础课程。
PyTorch 基础
PyTorch 使用一种称之为 imperative / eager 的范式,即每一行代码都要求构建一个图,以定义完整计算图的一个部分。即使完整的计算图还没有构建好,我们也可以独立地执行这些作为组件的小计算图,这种动态计算图被称为「define-by-run」方法。
PyTorch 具有两个比较基础的库,所有基础操作都需要提前引入。下面我们引入基础库。
import torch
import torchvision
PyTorch 张量
PyTorch 的基本数据单元是张量(Tensor),它实际上是一种 N 维数组。
创建
创建一个未初始化 5X3 的矩阵:
x = torch.empty(5, 3)
创建一个随机初始化都矩阵:
x = torch.rand(5, 3)
创建一个 0 填充的矩阵,指定数据类型为 long:
x = torch.zeros(5, 3, dtype=torch.long)
创建一个张量并使用现有数据初始化:
x = torch.tensor([5.5, 3])
x
根据现有张量创建新张量:
x = x.new_ones(5, 3, dtype=torch.double) # new_* 方法来创建对象
x
覆盖 dtype,对象的 size 是相同的,只是值和类型发生了变化:
x = torch.randn_like(x, dtype=torch.float)
x
获取张量的 size:
x.size()
加法运算
加法 1:
y = torch.rand(5, 3)
x + y
加法 2:
torch.add(x, y)
加法 3: 提供一个输出张量作为参数
result = torch.empty(5, 3)
torch.add(x, y, out=result)
result
加法 4: 替换
y.add_(x) # 将 x 加到 y
y
关于张量的操作,还有转置,索引,切片,数学运算,线性代数,随机数等。
张量与 Numpy 的转换
将 PyTorch 张量转换为 NumPy 数组:
a = torch.ones(5)
a
b = a.numpy()
b
NumPy 数组转换成 PyTorch 张量时,可以使用 from_numpy 完成:
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
a, b
自动微分模式
PyTorch 中所有神经网络的核心是 autograd。我们先简单介绍一下这个包,然后训练一个神经网络。
autograd 为张量上的所有操作提供了自动求导。它是一个在运行时定义的框架,这意味着反向传播是根据你的代码来确定如何运行。torch.Tensor 是这个包的核心类。如果设置 .requires_grad 为 True,那么将会追踪所有对于该张量的操作。当完成计算后通过调用 .backward() 会自动计算所有的梯度,这个张量的所有梯度将会自动积累到 .grad 属性。这也就完成了自动求导的过程。
下面编写代码实际使用自动微分变量。
导入自动梯度的运算包,主要用 Variable 这个类
from torch.autograd import Variable
创建一个 Variable,包裹了一个 2 * 2 张量,将需要计算梯度属性置为 True
x = Variable(torch.ones(2, 2), requires_grad=True)
x
按照张量的方式进行计算
y = x + 2
y.grad_fn #每个 Variable 都有一个 creator(创造者节点)
也可以进行复合运算,比如求均值 mean
z = torch.mean(y * y)
z.data #.data 属性可以返回 z 所包裹的 tensor
如果需要计算导数,你可以在 Tensor 上调用 .backward()。如果 Tensor 是一个标量(即它包含一个元素数据)则不需要为 backward() 指定任何参数。但是,如果它有多个元素,你需要指定一个 gradient 参数来匹配张量的形状。
z.backward() #梯度反向传播
print(z.grad) # 无梯度信息
print(y.grad) # 无梯度信息
print(x.grad)
下面的例子中,会让矩阵 x 反复作用在向量 s 上,系统会自动记录中间的依赖关系和长路径。
s = Variable(torch.FloatTensor([[0.01, 0.02]]), requires_grad = True) #创建一个 1 * 2 的 Variable(1 维向量)x = Variable(torch.ones(2, 2), requires_grad = True) #创建一个 2 * 2 的矩阵型 Variable
for i in range(10):
s = s.mm(x) #反复用 s 乘以 x(矩阵乘法),注意 s 始终是 1 * 2 的 Variable
z = torch.mean(s) #对 s 中的各个元素求均值,得到一个 1 * 1 的 scalar(标量,即 1 * 1 张量)
然后我们得到了一个复杂的“深度”计算图。
z.backward() #在具有很长的依赖路径的计算图上用反向传播算法计算叶节点的梯度
print(x.grad) #x 作为叶节点可以获得这部分梯度信息
print(s.grad) #s 不是叶节点,没有梯度信息
神经网络
PyTorch 中,我们可以使用 torch.nn
来构建神经网络。
前面已经讲过了 autograd
,torch.nn
依赖 autograd
来定义模型并求导。nn.Module
中包含了构建神经网络所需的各个层和 forward(input)
方法,该方法返回神经网络的输出。
下面给出一个示例网络结构,该网络也是经典的 LeNet。
它是一个简单的前馈神经网络,它接受一个输入,然后一层接着一层地传递,最后输出计算的结果。
神经网络的典型训练过程如下:
- 定义包含可学习参数(权重)的神经网络模型。
- 在数据集上迭代。
- 通过神经网络处理输入。
- 计算损失(输出结果和正确值的差值大小)。
- 将梯度反向传播回网络节点。
- 更新网络的参数,一般可使用梯度下降等最优化方法。
下面,参照上面的过程完成神经网络训练。
首先,定义上图示例的神经网络结构:
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()
net
模型中必须要定义 forward
函数,backward
函数(用来计算梯度)会被 autograd
自动创建。可以在 forward
函数中使用任何针对 Tensor 的操作。
net.parameters()
返回可被学习的参数(权重)列表和值:
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
测试随机输入 $32 \times 32$。注意,网络(LeNet)期望的输入大小是 $32 \times 32$,如果使用 MNIST 数据集($28 \times 28$)来训练这个网络,请把图片大小重新调整到 $32 \times 32$。
input = torch.randn(1, 1, 32, 32)
out = net(input)
out
将所有参数的梯度缓存清零,然后进行随机梯度的的反向传播:
net.zero_grad()
out.backward(torch.randn(1, 10))
在继续之前,我们回顾一下到目前为止用到的类。
-
torch.Tensor
:自动调用backward()
实现支持自动梯度计算的多维数组,并且保存关于这个向量的梯度。 -
nn.Module
:神经网络模块。封装参数、移动到 GPU 上运行、导出、加载等。 -
nn.Parameter
:变量,当把它赋值给一个Module
时,被自动地注册为一个参数。 -
autograd.Function
:实现自动求导操作的前向和反向定义,每个变量操作至少创建一个函数节点。
至此,我们以及完成:
- 定义一个网络
- 处理输入,调用
backword
。
接下来还需要:
- 计算损失。
- 更新网络权重。
损失函数
一个损失函数接受一对 (output, target)
作为输入,计算一个值来估计网络的输出和目标值相差多少。
torch.nn
中有很多不同的 损失函数。nn.MSELoss
是一个比较简单的损失函数,它可以用来计算输出和目标间的 均方误差,例如:
output = net(input)
target = torch.randn(10) # 随机值作为样例
target = target.view(1, -1) # 使 target 和 output 的 shape 相同
criterion = nn.MSELoss()
loss = criterion(output, target)
loss
当我们添加 loss
计算之后,如果使用它 .grad_fn
属性,将得到如下所示的计算图:
<pre style=”font-size:14px; line-height:17px;” class=”hljs”>
input → conv2d → relu → maxpool2d → conv2d → relu → maxpool2d
→ view → linear → relu → linear → relu → linear
→ MSELoss
→ loss
</pre>
所以,当我们调用 loss.backward()
时,会针对整个图执行微分操作。图中所有具有 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
反向传播
调用 loss.backward()
获得反向传播的误差。但是在调用前需要清除已存在的梯度,否则梯度将被累加到已存在的梯度。现在,我们将调用 loss.backward()
,并查看 conv1
层的偏差(bias)项在反向传播前后的梯度。下方的代码只能执行一次。
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)
torch.nn
中包含了各种用来构成深度神经网络构建块的模块和损失函数,你可以阅读 官方文档。
更新权重
至此,剩下的最后一件事,那就是更新网络的权重。
在实践中最简单的权重更新规则是随机梯度下降(SGD):
$$\text{weight}=\text{weight}-\text{learning rate}*\text{gradient}$$
我们可以使用简单的 Python 代码实现这个规则:
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
# 创建优化器
optimizer = optim.SGD(net.parameters(), lr=0.01)
# 执行一次训练迭代过程
optimizer.zero_grad() # 梯度置零
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # 更新
loss
多执行几次,观察损失值的变化情况。
训练一个分类器
上面,你已经看到如何去定义一个神经网络,计算损失值和更新网络的权重。接下来,我们实现一个图像分类神经网络。
一般情况下处理图像、文本、音频和视频数据时,可以使用标准的 Python 来加载数据为 NumPy 数组。然后把这个数组转换成 torch.*Tensor
。
- 图像可以使用 Pillow, OpenCV。
- 音频可以使用 SciPy, librosa。
- 文本可以使用原始 Python 和 Cython 来加载,或者使用 NLTK 或 SpaCy 处理。
特别地,对于图像任务,PyTorch 提供了专门的包 torchvision
,它包含了处理一些基本图像数据集的方法。这些数据集包括 Imagenet, CIFAR10, MNIST 等。除了数据加载以外,torchvision
还包含了图像转换器,torchvision.datasets
和 torch.utils.data.DataLoader
数据加载器。
torchvision
不仅提供了巨大的便利,也避免了代码的重复。接下来,我们使用 CIFAR10 数据集完成分类器训练。该数据集有如下 10 个类别:airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck。CIFAR-10 的图像都是 $3 \times 32 \times 32$,即 3 个颜色通道,$32 \times 32$ 像素。
训练一个图像分类器,基本流程如下:
- 使用
torchvision
加载和归一化 CIFAR10 训练集和测试集。 - 定义一个卷积神经网络。
- 定义损失函数。
- 在训练集上训练网络。
- 在测试集上测试网络。
读取和归一化 CIFAR10
使用 torchvision
可以非常容易地加载 CIFAR10。torchvision
的输出是 [0,1]
的 PILImage 图像,我们把它转换为归一化范围为 [-1, 1]
的张量。
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)
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')
trainloader, testloader
我们可视化其中的一些训练图像。
import matplotlib.pyplot as plt
%matplotlib inline
def imshow(img):
# 展示图像的函数
img = img / 2 + 0.5 # 反向归一化
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
# 获取随机数据
dataiter = iter(trainloader)
images, labels = dataiter.next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 显示图像标签
print(''.join('%5s' % classes[labels[j]] for j in range(4)))
定义一个卷积神经网络
从之前的神经网络一节复制神经网络代码,并修改输入为 3 通道图像。
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()
net
定义损失函数和优化器
我们使用交叉熵作为损失函数,使用带动量的随机梯度下降完成参数优化。
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
optimizer
训练网路
有趣的训练过程开始了。只需在数据迭代器上循环,将数据输入给网络,并优化。由于使用了卷积神经网络,该训练时间较长,请耐心等待。
for epoch in range(1): # 迭代一次
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# 获取输入
inputs, labels = data
# 梯度置 0
optimizer.zero_grad()
# 正向传播,反向传播,优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 打印状态信息
running_loss += loss.item()
if i % 200 == 199: # 每 200 批次打印一次
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 200))
running_loss = 0.0
print('Finished Training.')
在测试集上测试网络
我们在整个训练集上进行了训练,但是需要检查网络是否从数据集中学习到有用的东西。一般情况下,可以通过预测神经网络输出的类别标签与实际情况标签进行对比来进行检测。如果预测正确,我们把该样本添加到正确预测列表。
第一步,显示测试集中的图片并熟悉图片内容。
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)
outputs
输出是 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)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d%%' %
(100 * correct / total))