PyTorch 60分钟入门
PyTorch简介
这是一个基于Python的科学计算包,主要针对两类人群:
- 替代Numpy以发挥GPU的强大能力
- 一个提供最大灵活性和速度的深度学习研究平台
基础
张量(Tensors)
Tensors类似于numpy的ndarray,但是带了一些附加的功能,例如可以使用GPU加速计算等等。
1 | from __future__ import print_function |
构建一个未初始化的5*3的矩阵:
1 | x = torch.empty(5, 3) |
tensor([[-1.0593e-05, 4.5849e-41, 3.4723e-37],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 7.1941e+28],
[ 3.1036e+27, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 3.4568e-37]])
构建一个随机初始化的5*3的矩阵:
1 | x = torch.rand(5, 3) |
tensor([[ 0.7556, 0.7558, 0.2999],
[ 0.7304, 0.3527, 0.1911],
[ 0.8654, 0.4880, 0.1987],
[ 0.5456, 0.9359, 0.2071],
[ 0.1025, 0.5249, 0.3758]])
构建一个初始化为零类型为long的5*3的矩阵:
1 | x = torch.zeros(5, 3, dtype=torch.long) |
tensor([[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0],
[ 0, 0, 0]])
从数据构造一个张量
1 | x = torch.tensor([5.5, 3]) |
tensor([ 5.5000, 3.0000])
根据现有张量创建张量。这些方法将重用输入张量的属性,例如dtype,除非用户提供了新的值
1 | print(x) #打印之前的x值 |
tensor([ 5.5000, 3.0000])
tensor([[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]], dtype=torch.float64)
tensor([[-1.4230, -0.7907, -0.0556],
[-0.9403, -0.2937, 1.9447],
[ 0.2958, 0.9914, -0.9550],
[ 1.2439, -0.1390, 0.2889],
[-0.1790, -0.0003, 0.5241]])
获取尺寸
1 | print(x.size()) |
torch.Size([5, 3])
torch.Size 实际上是一个元组(tuple),因此它支持所有的元祖(tuple)的操作。
操作(Operations)
Pytorch具有100多种操作符(加减乘除,转置,索引,切片,等等),在这里我们以最简单的加法操作,了解Pytorch的操作方法。
- 加法:语法1
1 | y = torch.rand(5, 3) |
tensor([[-1.1514, -0.5880, -0.0083],
[-0.4967, 0.2964, 2.5860],
[ 0.7163, 1.0643, 0.0210],
[ 1.8021, 0.6697, 0.8263],
[ 0.3601, 0.3765, 1.3859]])
- 加法:语法2
1 | print(torch.add(x, y)) |
tensor([[-1.1514, -0.5880, -0.0083],
[-0.4967, 0.2964, 2.5860],
[ 0.7163, 1.0643, 0.0210],
[ 1.8021, 0.6697, 0.8263],
[ 0.3601, 0.3765, 1.3859]])
- 加法:提供输出张量作为参数
1 | result = torch.empty(5, 3) |
tensor([[-1.1514, -0.5880, -0.0083],
[-0.4967, 0.2964, 2.5860],
[ 0.7163, 1.0643, 0.0210],
[ 1.8021, 0.6697, 0.8263],
[ 0.3601, 0.3765, 1.3859]])
- 加法:就地解决((in-place))
1 | # adds x to y |
tensor([[-1.1514, -0.5880, -0.0083],
[-0.4967, 0.2964, 2.5860],
[ 0.7163, 1.0643, 0.0210],
[ 1.8021, 0.6697, 0.8263],
[ 0.3601, 0.3765, 1.3859]])
任何就地改变一个tensor的操作都以
_
为后缀。例如:x.copy_(y)
,x.t_()
,都会改变x。
Numpy与Torch张量的相互转换
Torch的Tensor和Numpy的数组会共享它们的底层存储位置,改变其中一个,另外一个也会改变。
Torch张量转换成Numpy数组
1 | a = torch.ones(5) # 创建一个torch张量 |
tensor([ 1., 1., 1., 1., 1.])
[1. 1. 1. 1. 1.]
tensor([ 2., 2., 2., 2., 2.])
[2. 2. 2. 2. 2.]
Numpy数组T转换成orch张量
1 | import numpy as np #导入numpy |
[2. 2. 2. 2. 2.]
tensor([ 2., 2., 2., 2., 2.], dtype=torch.float64)
CUDA张量(CUDA Tensors)
可以使用.to方法将张量移动到任何设备上。
1 | # 我们使用 ``torch.device`` 对象 将张量移入和移出GPU |
tensor([[-0.4230, 0.2093, 0.9444],
[ 0.0597, 0.7063, 2.9447],
[ 1.2958, 1.9914, 0.0450],
[ 2.2439, 0.8610, 1.2889],
[ 0.8210, 0.9997, 1.5241]], device='cuda:0')
tensor([[-0.4230, 0.2093, 0.9444],
[ 0.0597, 0.7063, 2.9447],
[ 1.2958, 1.9914, 0.0450],
[ 2.2439, 0.8610, 1.2889],
[ 0.8210, 0.9997, 1.5241]], dtype=torch.float64)
Autograd:自动求导
在PyTorch中所有神经网络的核心是autograd
软件包。我们先来简单介绍一下这个,然后再构建第一个神经网络。autograd
包为Tensors上的所有操作提供了自动求导。它是一个运行过程中定义的框架(define-by-run),这意味着反向传播是由代码的运行方式来定义的,并且每一次迭代都可能不同。
张量(Tensor)->0.4版本前是Variable
torch.Tensor
是包的中心类。如果你将属性.requires_grad
设置为True
,它将开始追踪所有的操作。当你完成了计算过程,你可以调用.backward()
,之后所有的梯度计算都是自动的。Tensor
的梯度将累积到.grad
属性中。
要停止跟踪历史记录的Tensor
,可以调用.detach()
将其从计算历史记录中分离出来,并防止跟踪将来的计算。
为了防止跟踪历史记录(和使用内存),你也可以用torch.no_grad()
包装代码块。 这在评估模型时特别有用,因为该模型可能具有require_grad = True
的可训练参数,但我们不需要梯度值。
还有一个类对于autograd
实现非常重要:一个Function
。
Tensor
和Function
是相互关联的,并建立一个非循环图,它编码构建了完整的计算过程。 每个变量都有一个.grad_fn
属性,该属性反应在已创建Tensor
的函数上(用户创建的Tensor
除外 - 它们的grad_fn
为None
)。
如果你想计算导数,可以在Tensor
上调用.backward()
。如果Tensor
是个标量(一个单元素数据),那么你不用为backward()
指定任何参数,然而如果它有多个元素,你需要指定一个gradient
参数,它是一个匹配尺寸的Tensor
。
1 | import torch |
tensor([[ 1., 1.],
[ 1., 1.]])
tensor([[ 3., 3.],
[ 3., 3.]])
<AddBackward0 object at 0x7f181420a978>
True
tensor([[ 27., 27.],
[ 27., 27.]]) tensor(27.)
<MulBackward0 object at 0x7f180409e400>
True
.requires_grad_(...)
就地更改现有张量的requires_grad
标志。如果没有给出,函数输入标志默认为True
。需要注意的是:python 的默认参数,调用的时候,test( ) 与 test(True)等价跟内部flag默认值无关。从打印看,内部flag默认值是False,但是输出结果flag为True
1 | a = torch.randn(2, 2) # 创建一个2*2的张量a |
False
True
<SumBackward0 object at 0x7f17b4c2d518>
True
False
False
True
梯度(Gradients)
让我们使用反向传播out.backward()
,它等同于out.backward(torch.Tensor(1)
)。
1 | x = torch.ones(2, 2, requires_grad=True) # 创建一个张量并设置`requires_grad = True`来跟踪计算 |
tensor([[ 4.5000, 4.5000],
[ 4.5000, 4.5000]])
4.5矩阵的计算过程如下所示:
我们还可以使用autograd做一些疯狂的事情!
1 | x = torch.randn(3, requires_grad=True) |
tensor([-0.3905, 1.3533, 1.0339])
<class 'torch.Tensor'>
<class 'torch.Tensor'>
tensor(3.4944)
tensor([ -399.9199, 1385.7303, 1058.7094])
tensor([ 102.4000, 1024.0000, 0.1024])
我们还可以通过使用torch.no_grad()
包装代码块来停止autograd
跟踪在张量上的历史记录,其中require_grad = True
:
1 | print(x.requires_grad) |
True
True
False
神经网络
神经网络可以使用torch.nn
包来构建。
前面的学习大致了解了autograd
,nn
依赖于autograd
来定义模型并进行求导。一个nn.Module
包含多个神经网络层,以及一个forward(input)
方法来返回output
。
例如,看看以下这个分类数字图像的网络:
它是一个简单的前馈网络。它将输入逐步地传递给多个层,然后给出输出。
一个典型的神经网络训练过程如下:
- 定义一个拥有可学习参数(或权重)的神经网络
- 在输入数据集上进行迭代
- 在网络中处理输入数据
- 计算损失(输出离分类正确有多大距离)
- 梯度反向传播给网络的参数
- 更新网络的权重,通常使用一个简单的更新规则(SGD):
weight = weight + learning_rate * gradient
定义网络结构
1 | import torch |
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 | params = list(net.parameters()) |
10
torch.Size([6, 1, 5, 5])
让我们尝试一个随机的32x32
输入!
注意:这个网络(LeNet)的预期输入大小是32x32
。要在MNIST
数据集上使用此网络,请将数据集中的图像调整为32x32
。
1 | input = torch.randn(1, 1, 32, 32) |
tensor([[-0.0819, 0.1214, 0.0144, -0.0429, 0.0046, 0.0520, -0.0673,
0.0878, -0.1724, -0.1151]])
将梯度缓冲区中所有的参数置0,并使用随机的梯度进行反向传播:
1 | net.zero_grad() |
torch.nn
仅支持mini-batch
。整个的torch.nn
包仅支持小批量的数据,而不是一个单独的样本。例如,nn.Conv2d
应传入一个4D
的Tensor
,维度为(nSamples X nChannels X Height X Width
)。如果你有一个单独的样本,使用input.unsqueeze(0)
来添加一个伪批维度。
回顾:
torch.Tensor
一个支持autograd
操作(如backward()
)的多维数组nn.Module
神经网络模块。封装参数的便捷方式,帮助者将它们移动到GPU,导出,加载等。nn.Parameter
一种Tensor
,当给Module
赋值时自动注册一个参数。autograd.Function
实现一个autograd
操作的forward
和backward
定义。每一个Tensor
操作,创建至少一个Function
节点,来连接那些创建Tensor
的函数,并且记录其历史。
在这里,我们涵盖了:
- 定义神经网络
- 处理输入并调用
backward
定义损失函数
一个损失函数以一个(output
, target
)对为输入,然后计算一个值用以估计输出结果离目标结果多远。
在nn的包里存在定义了多种损失函数。一个简单的损失函数:nn.MSELoss
它计算输出与目标的均方误差。
1 | output = net(input) |
tensor(38.9289)
现在,如果使用其.grad_fn
属性反向追踪损失,您将看到一个如下所示的计算图:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
因此,当我们调用loss.backward()
时,损失对应的整个图都被求导,并且图中所有的Tensor
都会带有累积了梯度的.grad
属性requres_grad=True
。
1 | print(loss.grad_fn) # MSELoss |
1 | <MseLossBackward object at 0x000002AE1A0953C8> |
反向传播
要进行反向传播,我们只需要调用loss.backward()
。注意:需要清除现有的梯度,否则梯度将累积到现有梯度。
现在我们将调用loss.backward()
,并看看conv1
偏置在backward
之前和之后的梯度变化。
1 | net.zero_grad() # 清除现有的梯度 |
conv1.bias.grad before backward
tensor([ 0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0383, 0.1029, 0.0044, 0.1332, 0.0659, -0.0402])
权值更新
实践中最简单的更新规则是随机梯度下降(SGD):weight = weight - learning_rate * gradient
1 | learning_rate = 0.01 |
然而,当使用神经网络时,希望使用各种不同的更新规则,例如SGD
,Nesterov-SGD
,Adam
,RMSProp
等等。为了实现这一点,Pytorch构建一个优化包:torch.optim
,来实现所有的方法。使用非常简单:
1 | import torch.optim as optim |
注意:在观察梯度变化时,首先要通过
optimizer.zero_grad()
清除现有的梯度,否则梯度将累积到现有梯度。
训练分类器
前面的教程中我们已经学习了如何定义神经网络,计算损失并更新网络的权重。接下来,我们完整的训练一个神经网络模型,并测试其性能。
数据集说明
一般来说,当在处理图像,文本,音频或视频数据时,可以使用标准的python
包将数据加载到一个numpy
数组中。然后将这个数组转换成torch.Tensor
。
- 图像的话,可以用
Pillow
,OpenCV
。 - 声音处理可以用
scipy
和librosa
。 - 文本的处理使用原生
Python
或者Cython
以及NLTK
和SpaCy
都可以。
特别是对于图像,PyTorch
创建了一个名为torchvision
的软件包,该软件包具有常用数据集(如Imagenet
,CIFAR10
,MNIST
等)的数据加载器torchvision.datasets
,以及用于图像的数据转换器torch.utils.data.DataLoader
。这提供了巨大的便利并避免了编写样板代码。
本教程使用CIFAR10数据集。 我们要进行的分类的类别有:’airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。 这个数据集中的图像都是3通道,32x32像素的图片。
训练一个图像分类器
我们要按顺序做这几个步骤:
- 使用torchvision来读取并预处理CIFAR10数据集
- 定义一个卷积神经网络
- 定义一个代价函数
- 在神经网络中训练训练集数据
- 使用测试集数据测试神经网络
1.加载和归一化CIFAR10
torchvision
加载的数据集的输出是范围[0,1]的PILImage
图像。我们将它们转换为归一化范围[-1,1]的张量。
1 | import torch |
Files already downloaded and verified
Files already downloaded and verified
我们来从中找几张图片看看。
1 | import matplotlib.pyplot as plt |
deer cat dog ship
2.定义卷积神经网络结构
1 | import torch.nn as nn |
3.定义损失函数和优化器
1 | import torch.optim as optim |
4.训练网络
1 | for epoch in range(2): # 训练集迭代次数 |
[1, 2000] loss: 2.199
[1, 4000] loss: 1.887
[1, 6000] loss: 1.707
[1, 8000] loss: 1.614
[1, 10000] loss: 1.536
[1, 12000] loss: 1.504
[2, 2000] loss: 1.449
[2, 4000] loss: 1.411
[2, 6000] loss: 1.372
[2, 8000] loss: 1.349
[2, 10000] loss: 1.325
[2, 12000] loss: 1.306
Finished Training
5.测试网络
我们已经训练了两遍了。 此时需要测试一下到底结果如何。
通过对比神经网络给出的分类和已知的类别结果,可以得出正确与否。如果预测的正确,我们可以将样本加入正确预测的结果的列表中。
好的第一步,让我们展示几张照片来熟悉一下。
1 | dataiter = iter(testloader) |
GroundTruth: cat ship ship plane
现在让我们看看神经网络认为这些例子是什么:
1 | outputs = net(images) |
tensor([[-0.4905, -1.5664, 1.0641, 2.4226, 0.1196, 1.9381, 0.9795,
-0.4404, -1.7645, -1.6992],
[ 6.1866, 5.8665, -2.2267, -3.2581, -2.6794, -4.9095, -4.2326,
-5.3548, 6.9980, 2.7097],
[ 1.9322, 3.0127, -1.2481, -1.1180, -1.4086, -1.7913, -1.8129,
-1.9674, 2.3132, 1.7559],
[ 3.6228, 0.1119, 0.6089, -1.5255, -0.5566, -2.7542, -1.1817,
-3.3743, 4.5489, -0.5763]])
输出是10类对应的数值。一个类对应的数值越高,网络认为这个图像就是越接近这个类。那么,让我们得到最高数值对应的类:
1 | _, predicted = torch.max(outputs, 1) |
Predicted: cat ship car ship
结果看起来挺好。
看看神经网络在整个数据集上的表现结果如何:
1 | correct = 0 |
Accuracy of the network on the 10000 test images: 54 %
从结果上看,神经网络输出的结果比随机要好,随机选择的话从十个中选择一个出来,准确率大概只有10%。
看上去神经网络学到了点东西。
我们看一下那么到底哪些类别表现良好又是哪些类别不太行呢?
1 | class_correct = list(0. for i in range(10)) |
Accuracy of plane : 59 %
Accuracy of car : 82 %
Accuracy of bird : 44 %
Accuracy of cat : 44 %
Accuracy of deer : 48 %
Accuracy of dog : 43 %
Accuracy of frog : 59 %
Accuracy of horse : 52 %
Accuracy of ship : 73 %
Accuracy of truck : 40 %