PyTorch教程

其实最好用的还是官方教程,建议平时使用还是查官方教程因为每次版本更新都会有一些不一样。这个教程只是自己用来梳理下Pytorch知识点,毕竟没系统学习过,都是现用现学。通过这个教程梳理一遍给自己建个知识框架

正文

PyTorch是一个基于Torch的Python开源机器学习库,用于自然语言处理等应用程序。它主要由Facebookd的人工智能小组开发,不仅能够实现强大的GPU加速,同时还支持动态神经网络,这一点是现在很多主流框架如TensorFlow都不支持的。PyTorch提供了两个高级功能:1.具有强大的GPU加速的张量计算(如Numpy)2.包含自动求导系统的深度神经网络除了Facebook之外,Twitter、GMU和Salesforce等机构都采用了PyTorch。

简介与下载

要介绍PyTorch之前,不得不说一下Torch。Torch是一个有大量机器学习算法支持的科学计算框架,是一个与Numpy类似的张量(Tensor)操作库,其特点是特别灵活,但因其采用了小众的编程语言是Lua,所以流行度不高,这也就有了PyTorch的出现。所以其实Torch是PyTorch的前身,它们的底层语言相同,只是使用了不同的上层包装语言。
PyTorch是一个基于Torch的Python开源机器学习库,用于自然语言处理等应用程序。它主要由Facebookd的人工智能小组开发,不仅能够实现强大的GPU加速,同时还支持动态神经网络,这一点是现在很多主流框架如TensorFlow都不支持的。PyTorch提供了两个高级功能:具有强大的GPU加速的张量计算(如Numpy)包含自动求导系统的深度神经网络
TensorFlow和Caffe都是命令式的编程语言,而且是静态的,首先必须构建一个神经网络,然后一次又一次使用相同的结构,如果想要改变网络的结构,就必须从头开始。但是对于PyTorch,通过反向求导技术,可以让你零延迟地任意改变神经网络的行为,而且其实现速度快。正是这一灵活性是PyTorch对比TensorFlow的最大优势。
另外,PyTorch的代码对比TensorFlow而言,更加简洁直观,底层代码也更容易看懂,这对于使用它的人来说理解底层肯定是一件令人激动的事。
所以,总结一下PyTorch的优点: 支持GPU 灵活,支持动态神经网络 底层代码易于理解 命 令式体验 * 自定义扩展
当然,现今任何一个深度学习框架都有其缺点,PyTorch也不例外,对比TensorFlow,其全面性处 于劣势,目前PyTorch还不支持快速傅里 叶、沿维翻转张量和检查无穷与非数值张量;针对移动 端、嵌入式部署以及高性能服务器端的部署其性能表现有待提升;其次因为这个框 架较新,使得 他的社区没有那么强大,在文档方面其C库大多数没有文档。

安装anaconda

Anaconda是一个用于科学计算的Python发行版,支持Linux、Mac和Window系统,提供了包管理与环境管理的功能,可以很方便地解决Python并存、切换,以及各种第三方包安装的问题。(直接去官网下载对应系统版本的安装包安装即可,网上教程很多。)

安装CUDA

如果设备包含gpu且支持CUDA加速,就去下载安装,网上教程很多。

安装PyTorch

进入PyTroch官网,点击install。(注意,绿色箭头就是官方文档,平时使用时遇到不会的可以查这)

根据自己配置选好选项,下面就有安装指令,直接在终端环境中执行即可,如果这里没有想要的版本,可以之前版本中去找(绿色箭头部分)

基础知识

PyTorch 是一个基于 Python 的科学计算包,主要定位两类人群:

  • NumPy 的替代品,可以利用 GPU 的性能进行计算。
  • 深度学习研究平台拥有足够的灵活性和速度

Tensors(张量)
Tensors 类似于 NumPy 的 ndarrays ,同时 Tensors 可以使用 GPU 进行计算。

1
2
from __future__ import print_function 
import torch

构造一个5x3矩阵,不初始化。
1
2
x = torch.empty(5, 3)
print(x)

输出
1
2
3
4
5
tensor([[ 0.0000e+00, -0.0000e+00, -1.9597e-19],
[ 3.6902e+19, 1.1210e-44, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00]])

构造一个随机初始化的矩阵:
1
x = torch.rand(5, 3)

构造一个矩阵全为 0,而且数据类型是 long.
1
x = torch.zeros(5, 3, dtype=torch.long)

构造一个张量,直接使用数据:
1
x = torch.tensor([5.5, 3])

创建一个 tensor 基于已经存在的 tensor。
1
2
3
4
5
6
x = x.new_ones(5, 3, dtype=torch.double) # new_* methods take in sizes
print(x)
x = torch.randn_like(x, dtype=torch.float)
# override dtype!
print(x)
# result has the same size

输出
1
2
3
4
5
6
7
8
9
10
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
tensor([[ 0.9130, 1.9534, 0.8636],
[ 0.3681, -1.0433, 1.0611],
[-0.8342, 0.8784, -0.2931],
[ 0.6784, 0.6056, -0.2177],
[-0.1609, 0.4570, -1.4430]])

获取它的维度信息:
1
print(x.size())

输出
1
torch.Size([5, 3])

torch.Size 是一个元组,所以它支持左右的元组操作。

加法

  • 方式1

    1
    2
    3
    x = torch.rand(5, 3)
    y = torch.rand(5, 3)
    print(x+y)

    输出

    1
    2
    3
    4
    5
    tensor([[1.7876, 0.8783, 1.0245],
    [1.3862, 1.7952, 1.2017],
    [1.5857, 1.1944, 0.9773],
    [1.4360, 1.4012, 1.0903],
    [1.4319, 1.3189, 1.4464]])
  • 方式2

    1
    torch.add(x, y)
  • 方式3:提供一个输出tensor作为参数

    1
    result = torch.empty(5, 3) torch.add(x, y, out=result)
  • 方式4:in-place
    1
    2
    # add x to y
    y.add_(x)

    任何使张量会发生变化的操作都有一个前缀‘’。例如:x.copy(y), x.t_(), 将会改变 x.

可以使用标准的 NumPy 类似的索引操作

1
print(x[:, 1])

输出
1
tensor([0.4864, 0.2592, 0.9875, 0.1290, 0.5603])

改变大小:如果想改变一个 tensor 的大小或者形状,可以使用 torch.view:

1
2
3
4
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())

输出
1
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])

如果有一个元素 tensor ,使用 .item() 来获得这个 value 。
1
2
3
x = torch.randn(1)
print(x)
print(x.item())

输出
1
2
tensor([0.2575])
0.25753161311149597

Pytorch自动微分

autograd 包是 PyTorch 中所有神经网络的核心。首先让简要地介绍它,然后手把手去训练第一个神经网络。
该 autograd 软件包为 Tensors 上的所有操作提供自动微分。它是一个由运行定义的框架,这意味着以代码运行方式定义你的后向传播,并且每次迭代都可以不同。我们 从 tensor 和 gradients 来举一些例子。

TENSOR(张量)

torch.Tensor 是包的核心类。如果将其属性 .requires_grad 设置为 True,则会开始跟踪针对 tensor 的所有操作。完成计算后,您可以调用 .backward() 来自动计算所有梯度。该张量的梯度将累积到 .grad 属性中。
要停止 tensor 历史记录的跟踪,可以调用 .detach(),它将其与计算历史记录分离,并防止将来的计算被跟踪。
要停止跟踪历史记录(和使用内存),还可以将代码块使用 with torch.no_grad(): 包装起来。在评估模型时,这是特别有用,因为模型在训练阶段具有 requires_grad = True 的可训练参数有利于调参,但在评估阶段我们不需要梯度。
还有一个类对于 autograd 实现非常重要那就是 Function。Tensor 和 Function 互相连接并构建一个非循环图,它保存整个完整的计算过程的历史信息。每个张量都有一个 .grad_fn 属性保存着创建了张量的 Function 的引用,(如果用户自己创建张量,则g rad_fn 是 None )。
如果想计算导数,可以调用 Tensor.backward()。如果 Tensor 是标量(即它包含一个元素数据),则不需要指定任何参数backward(),但是如果它有更多元素,则需要指定一个gradient 参数 来指定张量的形状。
import torch

创建一个张量,设置 requiresgrad=True 来跟踪与它相关的计算

1
2
x = torch.ones(2, 2, requires_grad=True)
print(x)

输出
1
2
tensor([[1., 1.],
[1., 1.]], requires_grad=True)

针对张量做一个操作
1
2
y=x+2 
print(y)

输出
1
2
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)

y 作为操作的结果被创建,所以它有 grad_fn
1
print(y.grad_fn)

1
<AddBackward0 object at 0x7fc3692d57d0>

针对 y 做更多的操作:
1
2
3
z=y*y*3 
out = z.mean()
print(z, out)

输出
1
2
3
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>)
tensor(27., grad_fn=<MeanBackward0>)

.requires_grad
( … ) 会改变张量的 requires_grad 标记。如果没有提供相对应参数,输入的标记默认为 False。
1
2
3
4
5
6
7
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

输出
1
2
3
False
True
<SumBackward0 object at 0x7fd8c209b350>

梯度

现在后向传播,因为输出包含了一个标量,out.backward() 等同于 out.backward(torch.tensor(1.))。

1
2
3
4
5
6
x = torch.ones(2, 2, requires_grad=True)
y=x+2
z=y*y*3
out = z.mean()
out.backward() # 反向传播
print(x.grad) # 打印梯度 d(out)/d(x)

输出
1
2
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])

原理解释,可以不看

来看一个雅可比向量积的例子:

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

输出
1
tensor([696.7436, 937.0305,  42.3632], grad_fn=<MulBackward0>)

现在在这种情况下,y 不再是一个标量。torch.autograd 不能够直接计算整个雅可比,但是如果我们只想要雅可比向量积,只需要简单的传递向量给 backward 作为参数。
1
2
3
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)

输出
1
tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])

可以通过将代码包裹在 with torch.no_grad(),来停止对从跟踪历史中的 .requires_grad=True 的张量自动求导。
1
2
3
4
5
print(x.grad)
print(x.requires_grad)
print((x ** 2).requires_grad)
with torch.no_grad():
print((x ** 2).requires_grad)

输出
1
2
3
True
True
False

autograd 和 Function 的官方文档

PyTorch神经网络

神经网络可以通过 torch.nn 包来构建。
现在对于自动梯度(autograd)有一些了解,神经网络是基于自动梯度 (autograd)来定义一些模型。 一个 nn.Module 包括层和一个方法 forward(input) 它会返回输出(output)。
例如,看一下卷积神经网络做数字图片识别:

这是一个简单的前馈神经网络,它接收输入,让输入一个接着一个的通过一些层,最后给出输出。
一个典型的神经网络训练过程包括以下几点:

  1. 定义一个包含可训练参数的神经网络
  2. 迭代整个输入
  3. 通过神经网络处理输入
  4. 计算损失(loss)
  5. 反向传播梯度到神经网络的参数
  6. 更新网络的参数,典型的用一个简单的更新方法:weight = weight - learning_rate *gradient

定义神经网络

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
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)
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=(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)
)

刚定义了一个前馈函数,然后反向传播函数被自动通过 autograd 定义了。可以使用任何张量操作在前馈函数上。
一个模型可训练的参数可以通过调用 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, 5, 5])

尝试下随机生成一个 32x32 的输入。注意:期望的输入维度是 32x32 。为了使用这个网络可以用在 MNIST 数据集上,需要把MNIST数据集中的图片维度修改为 32x32。
1
2
3
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

输出
1
2
tensor([[-0.0129, -0.0196, -0.0611,  0.0103, -0.1252,  0.0885, -0.0800, -0.1244,
0.0245, -0.0126]], grad_fn=<AddmmBackward0>)

把所有参数梯度缓存器置零,用随机的梯度来反向传播
1
2
net.zero_grad() 
out.backward(torch.randn(1, 10))

目前完成了神经网络对定义、输入以及调用反向传播,接下来还剩下计算损失和更新权重。

损失函数

一个损失函数需要一对输入:模型输出和目标,然后计算一个值来评估输出距离目标有多远。
有一些不同的损失函数在 nn 包中。一个简单的损失函数就是 nn.MSELoss ,这计算了均方误差。
例如:

1
2
3
4
5
6
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.6168, grad_fn=<MseLossBackward0>)

现在,如果跟随损失到反向传播路径,可以使用它的 .grad_fn 属性,将会看到一个这样的计算图:
1
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
<MseLossBackward0 object at 0x7fa5c36e2c10>
<AddmmBackward0 object at 0x7fa5bfecb350>
<AccumulateGrad object at 0x7fa5c36e2c10>

反向传播

为了实现反向传播损失,我们所有需要做的事情仅仅是使用 loss.backward()。你需要清空现存的梯度,要不然帝都将会和现存的梯度累计到一起。
现在调用 loss.backward() ,然后看一下 con1 的偏置项在反向传播之前和之后的变化。

1
2
3
4
5
6
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
None
conv1.bias.grad after backward
tensor([ 0.0002, -0.0006, 0.0030, 0.0019, 0.0030, -0.0004])

更新网络

剩下的事情就是更新神经网络的参数。
最简单的更新规则就是随机梯度下降。

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)

尽管如此,如果是用神经网络,想使用不同的更新规则,类似于 SGD, Nesterov-SGD, Adam, RMSProp, 等。为了让这可行,pytorch建立了一个小包:torch.optim 实现了所有的方法。使用它非常的简单。
1
2
3
4
5
6
7
8
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

PyTorch之图像分类

数据处理

通常来说,当处理图像,文本,语音或者视频数据时,可以使用标准 python 包将数据加载成 numpy 数组格式,然后将这个数组转换成 torch.*Tensor

  • 对于图像,可以用 Pillow,OpenCV
  • 对于语音,可以用 scipy,librosa
  • 对于文本,可以直接用 Python 或 Cython 基础数据加载模块,或者用 NLTK 和 SpaCy

特别是对于视觉,Pytorch已经创建了一个叫做 totchvision 的包,该包含有支持加载类似Imagenet, CIFAR10,MNIST 等公共数据集的数据加载模块 torchvision.datasets 和支持加载图像数据数据转换模块 torch.utils.data.DataLoader。提供了极大的便利,并且避免了编写“样板代码”。
以下案例,将使用CIFAR10数据集,它包含十个类别:‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。CIFAR-10 中的图像尺寸为33232,也就是RGB的3层颜色 通道,每层通道内的尺寸为32*32。

(碎碎念, 第一次学习神经网络时,还是把数据集直接拉成32323的特征向量直接用全连接做的分类,别说,效果还不错,尤其加入dropout后)

训练一个图像分类器

步骤如下:

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

加载并归一化 CIFAR10 使用 torchvision ,用它来加载 CIFAR10 数据非常简单。

1
2
3
import torch
import torchvision
import torchvision.transforms as transforms

torchvision 数据集的输出是范围在[0,1]之间的 PILImage,所以先将他们转换成归一化范围为[-1,1]之 间的张量 Tensors。
1
2
3
4
5
6
7
8
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') # 类别标签
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)

可视化展示一些训练图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)))
plt.show()

# get some random training images
dataiter = iter(trainloader) # 迭代读取训练数据 因为之前batchsize设置的4 所以一次会输出四张照片
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))) #输出类别

输出
horse   dog  bird   cat
定义一个卷积神经网络 在这之前先 从神经网络章节 复制神经网络,并修改它为3通道的图片(在此 之前它被定义为1通道)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()

定义一个损失函数和优化器,使用分类交叉熵Cross-Entropy 作损失函数,动量SGD做优化器。
1
2
3
import torch.optim as optim 
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

训练网络 这里事情开始变得有趣,只需要在数据迭代器上循环传给网络和优化器输入就可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
print('Finished Training')

输出
1
2
3
4
5
6
7
8
9
10
11
12
13
[1,  2000] loss: 2.161
[1, 4000] loss: 1.806
[1, 6000] loss: 1.684
[1, 8000] loss: 1.605
[1, 10000] loss: 1.535
[1, 12000] loss: 1.475
[2, 2000] loss: 1.379
[2, 4000] loss: 1.361
[2, 6000] loss: 1.346
[2, 8000] loss: 1.312
[2, 10000] loss: 1.289
[2, 12000] loss: 1.271
Finished Training

在测试集上测试网络已经通过训练数据集对网络进行了2次训练,但是我们需要检查网络是否已经学到了东西。
我们将用神经网络的输出作为预测的类标来检查网络的预测性能,用样本的真实类标来校对。如果预测是正确的,我们将样本添加到正确预测的列表里。
好的,第一步,让我们从测试集中显示一张图像来熟悉它。
1
2
3
4
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))) #输出类别


类别
1
GroundTruth: cat  ship  ship plane

现在看看 神经网络认为这些样本应该预测成什么:
1
2
3
outputs = net(images)
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))

输出
1
Predicted:   bird  ship truck  ship

效果挺一般的,在整个数据集上看下效果如何
1
2
3
4
5
6
7
8
9
10
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))

输出
1
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
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]))

输出
1
2
3
4
5
6
7
8
9
10
Accuracy of plane : 63 %
Accuracy of car : 77 %
Accuracy of bird : 32 %
Accuracy of cat : 40 %
Accuracy of deer : 37 %
Accuracy of dog : 55 %
Accuracy of frog : 71 %
Accuracy of horse : 67 %
Accuracy of ship : 46 %
Accuracy of truck : 55 %

接下来介绍,如何把数据和模型放在gpu上通过gpu加速训练
首先定义一个cuda设备

1
2
3
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Assume that we are on a CUDA machine, then this should print a CUDA device:
print(device)

将模型和数据放在CUDA上很方便,直接to(device)即可
1
2
net.to(device)
inputs, labels = inputs.to(device), labels.to(device)

Pytorch支持多gpu并行计算,详细可以参考官网的文档
数据并行自动拆分了你的数据并且将任务单发送到多个 GPU 上。当每一个模型都完成自己的任务之后,DataParallel 收集并且合并这些结果,然后再返回给你。

PyTorch 之迁移学习

实际中,基本没有人会从零开始(随机初始化)训练一个完整的卷积网络,因为相对于网络,很难得到一个足够大的数据集网络很深, 需要足够大数据集。通常的做法是在一个很大的数据集上进行预训练得到卷积网络ConvNet, 然后将这个ConvNet的参数作为目标任务的初始化参数或者固定这些参数。

转移学习的两个主要场景

  • 微调Convnet:使用预训练的网络(如在imagenet 1000上训练而来的网络)来初始化自己的网络,而不是随机初始化。其他的训练步骤不变。
  • Convnet看成固定的特征提取器:首先固定ConvNet除了最后的全连接层外的其他所有层。最后的全连接层被替换成一个新的随机初始化的层,只有这个新的层会被训练只有这层 参数会在反向传播时更新

下面是利用PyTorch进行迁移学习步骤,要解决的问题是训练一个模型来对蚂蚁和蜜蜂进行分类。

  1. 导入相关的包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # License: BSD
    # Author: Sasank Chilamkurthy
    from __future__ import print_function, division
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.optim import lr_scheduler
    import numpy as np
    import torchvision
    from torchvision import datasets, models, transforms
    import matplotlib.pyplot as plt
    import time
    import os
    import copy
    plt.ion() # interactive mode
  2. 加载数据
    今天要解决的问题是训练一个模型来分类蚂蚁ants和蜜蜂bees。ants和bees各有约120张训练图片。每个类有75张验证图片。从零开始在如此小的数据集上进行训练通常是很难泛化的。由于我们使用迁移学习,模型的泛化能力会相当好。 该数据集是imagenet的一个非常小的子集。从此处下载数据,并将其解压缩到当前目录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #训练集数据扩充和归一化
    #在验证集上仅需要归一化
    data_transforms = {
    'train': transforms.Compose([transforms.RandomResizedCrop(224), #随机裁剪一个area然后再resize
    transforms.RandomHorizontalFlip(), #随机水平翻转
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    }
    data_dir = './data/hymenoptera_data'
    image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),data_transforms[x]) for x in ['train', 'val']}
    dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4, shuffle=True, num_workers=4) for x in ['train', 'val']}
    dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
    class_names = image_datasets['train'].classes
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  3. 可视化部分图像数据
    可视化部分训练图像,以便了解数据扩充。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def imshow(inp, title=None):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
    plt.title(title)
    plt.pause(0.001) # pause a bit so that plots are updated

    # 获取一批训练数据
    inputs, classes = next(iter(dataloaders['train']))
    # 批量制作网格
    out = torchvision.utils.make_grid(inputs)
    imshow(out, title=[class_names[x] for x in classes])

  4. 模型训练
    编写一个通用函数来训练模型。下面将说明: 调整学习速率 保存最好的模型
    下面的参数scheduler是一个来自 torch.optim.lr_scheduler 的学习速率调整类的对象(LR scheduler object)。
    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
    def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()
    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    for epoch in range(num_epochs):
    print('Epoch {}/{}'.format(epoch, num_epochs - 1))
    print('-' * 10)
    # 每个epoch都有一个训练和验证阶段
    for phase in ['train', 'val']:
    if phase == 'train':
    scheduler.step()
    model.train() # Set model to training mode
    else:
    model.eval() # Set model to evaluate mode
    running_loss = 0.0
    running_corrects = 0
    # 迭代数据.
    for inputs, labels in dataloaders[phase]:
    inputs = inputs.to(device)
    labels = labels.to(device)
    # 零参数梯度
    optimizer.zero_grad()
    # 前向
    # track history if only in train
    with torch.set_grad_enabled(phase == 'train'):
    outputs = model(inputs)
    _, preds = torch.max(outputs, 1)
    loss = criterion(outputs, labels)
    # 后向+仅在训练阶段进行优化
    if phase == 'train':
    loss.backward()
    optimizer.step()
    # 统计
    running_loss += loss.item() * inputs.size(0)
    running_corrects += torch.sum(preds == labels.data)
    epoch_loss = running_loss / dataset_sizes[phase]
    epoch_acc = running_corrects.double() / dataset_sizes[phase]
    print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
    # 深度复制mo
    if phase == 'val' and epoch_acc > best_acc:
    best_acc = epoch_acc
    best_model_wts = copy.deepcopy(model.state_dict())
    print()
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))
    # 加载最佳模型权重
    model.load_state_dict(best_model_wts)
    return model
  5. 可视化模型的预测结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #一个通用的展示少量预测图片的函数
    def visualize_model(model, num_images=6):
    was_training = model.training
    model.eval()
    images_so_far = 0
    fig = plt.figure()
    with torch.no_grad():
    for i, (inputs, labels) in enumerate(dataloaders['val']):
    inputs = inputs.to(device)
    labels = labels.to(device)
    outputs = model(inputs)
    _, preds = torch.max(outputs, 1)
    for j in range(inputs.size()[0]):
    images_so_far += 1
    ax = plt.subplot(num_images//2, 2, images_so_far)
    ax.axis('off')
    ax.set_title('predicted: {}'.format(class_names[preds[j]]))
    imshow(inputs.cpu().data[j])
    if images_so_far == num_images:
    model.train(mode=was_training)
    return
    model.train(mode=was_training)

场景1:微调ConvNet

  1. 加载预训练模型并重置最终完全连接的图层。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    model_ft = models.resnet18(pretrained=True)
    num_ftrs = model_ft.fc.in_features
    model_ft.fc = nn.Linear(num_ftrs, 2)
    model_ft = model_ft.to(device)
    criterion = nn.CrossEntropyLoss()
    # 观察所有参数都正在优化
    optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
    # 每7个epochs衰减LR通过设置gamma=0.1
    exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
  2. 训练和评估模型
    训练模型 该过程在CPU上需要大约15-25分钟,但是在GPU上,它只需不到一分钟。
    1
    model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,num_epochs=25)
    输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    Epoch 0/24
    ----------
    train Loss: 0.6256 Acc: 0.6926
    val Loss: 0.4177 Acc: 0.8105

    Epoch 1/24
    ----------
    train Loss: 0.5404 Acc: 0.7992
    val Loss: 0.4884 Acc: 0.8758

    Epoch 2/24
    ----------
    train Loss: 0.4987 Acc: 0.7951
    val Loss: 0.2829 Acc: 0.9150
    .
    .
    .
    Epoch 24/24
    ----------
    train Loss: 0.2817 Acc: 0.8730
    val Loss: 0.1910 Acc: 0.9150

    Training complete in 1m 12s
    Best val Acc: 0.954248
  3. 模型评估效果可视化
    1
    visualize_model(model_ft)

场景2:ConvNet作为固定特征提取器

在这里需要冻结除最后一层之外的所有网络。通过设置 requires_grad == Falsebackward() 来冻结参数,这样在反向传播backward()的时候他们的梯度就不会被计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
model_conv = torchvision.models.resnet18(pretrained=True)
for param in model_conv.parameters():
param.requires_grad = False
# Parameters of newly constructed modules have requires_grad=True by default
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, 2)
model_conv = model_conv.to(device)
criterion = nn.CrossEntropyLoss()
# Observe that only parameters of final layer are being optimized as
# opposed to before.
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)

训练与评估

  1. 训练模型 在CPU上,与前一个场景相比,这将花费大约一半的时间,因为不需要为大多数网 络计算梯度。但需要计算转发。
    1
    model_conv = train_model(model_conv, criterion, optimizer_conv, exp_lr_scheduler, num_epochs=25)
    输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    Epoch 0/24
    ----------
    train Loss: 0.6128 Acc: 0.6926
    val Loss: 0.3360 Acc: 0.8562

    Epoch 1/24
    ----------
    train Loss: 0.6937 Acc: 0.7049
    val Loss: 0.2079 Acc: 0.9412

    Epoch 2/24
    ----------
    train Loss: 0.5440 Acc: 0.7623
    val Loss: 0.4410 Acc: 0.8170
    .
    .
    .
    Epoch 24/24
    ----------
    train Loss: 0.3450 Acc: 0.8607
    val Loss: 0.1902 Acc: 0.9608

    Training complete in 0m 56s
    Best val Acc: 0.960784
  2. 模型评估效果可视化
    1
    2
    3
    visualize_model(model_conv)
    plt.ioff()
    plt.show()

混合前端的seq2seq模型部署

这节将介绍如何是seq2seq模型转换为PyTorch可用的前端混合Torch脚本。我们要转换的模型来自于聊天机器人教程Chatbot tutorial