深度学习二(Pytorch物体检测实战)
文章目录
文章来源: <深度学习之PyTorch物体检测实战> 编著: 董洪义
书籍下载: www.hzbook.com
1、PyTorch基础
工欲善其事,必先利其器。掌握一个好的工具能够使学习的效率翻倍。而PyTorch作为一个优秀的动态图框架,简洁且入门快,适用于快速实现与各种算法的学习。
本章主要讲解PyTorch的基础知识,从基本数据、神经网络,再到常用的模型处理与数据处理方法,通过简单的示例将最常用的方法展示出来,力求让读者熟悉PyTorch处理神经网络的过程。
1.1、基本数据结构:Tensor
Tensor,即张量,是PyTorch中的基本操作对象,可以看做是包含单一数据类型元素的多维矩阵。从使用角度来看,Tensor与NumPy的ndarrays非常类似,相互之间也可以自由转换,只不过Tensor还支持GPU的加速。
1.1.1、Tensor数据类型
Tensor在使用时可以有不同的数据类型,官方给出了7种CPU Tensor类型与8种GPU Tensor类型,在使用时可以根据网络模型所需的精度与显存容量,合理地选取。16位半精度浮点是专为GPU上运行的模型设计的,以尽可能地节省GPU显存占用,但这种节省显存空间的方式也缩小了所能表达数据的大小。PyTorch中默认的数据类型是torch.FloatTensor,即torch.Tensor等同于torch.FloatTensor。
PyTorch可以通过set_default_tensor_type函数设置默认使用的Tensor类型,在局部使用完后如果需要其他类型,则还需要重新设置回所需的类型。
torch.set_default_tensor_type('torch.DoubleTensor')
对于Tensor之间的类型转换,可以通过type(new_type)、type_as()、int()等多种方式进行操作,尤其是type_as()函数,在后续的模型学习中可以看到,我们想保持Tensor之间的类型一致,只需要使用type_as()即可,并不需要明确具体是哪种类型。下面分别举例讲解这几种方法的使用方式。
# 创建新Tensor,默认类型为torch.FloatTensor
>>> a = torch.Tensor(2, 2)
>>> a
tensor(1.00000e-36 *
[[-4.0315, 0.0000],
[ 0.0700, 0.0000]])
# 使用int()、float()、double()等直接进行数据类型转换
>>> b = a.double()
>>> b
tensor(1.00000e-36 *
[[-4.0315, 0.0000],
[ 0.0700, 0.0000]], dtype=torch.float64)
# 使用type()函数
>>> c = a.type(torch.DoubleTensor)
>>> c
tensor(1.00000e-36 *
[[-4.0315, 0.0000],
[ 0.0700, 0.0000]], dtype=torch.float64)
# 使用type_as()函数
>>> d = a.type_as(b)
>>> d
tensor(1.00000e-36 *
[[-4.0315, 0.0000],
[ 0.0700, 0.0000]], dtype=torch.float64)
1.1.2、 Tensor的创建于维度查看
Tensor有多种创建方法,如基础的构造函数Tensor(),还有多种与NumPy十分类似的方法,如ones()、eye()、zeros()和randn()等。
从代码角度实现Tensor的多种方式创建。
# 最基础的Tensor()函数创建方法,参数为Tensor的每一维大小
>>> a=torch.Tensor(2,2)
>>> a
tensor(1.00000e-18 *
[[-8.2390, 0.0000],
[ 0.0000, 0.0000]])
>>> b = torch.DoubleTensor(2,2)
>>> b
tensor(1.00000e-310 *
[[ 0.0000, 0.0000],
[ 6.9452, 0.0000]], dtype=torch.float64)
# 使用Python的list序列进行创建
>>> c = torch.Tensor([[1, 2], [3, 4]])
>>> c
tensor([[ 1., 2.],
[ 3., 4.]])
# 使用zeros()函数,所有元素均为0
>>> d = torch.zeros(2, 2)
>>> d
tensor([[ 0., 0.],
[ 0., 0.]])
# 使用ones()函数,所有元素均为1
>>> e = torch.ones(2, 2)
>>> e
tensor([[ 1., 1.],
[ 1., 1.]])
# 使用eye()函数,对角线元素为1,不要求行列数相同,生成二维矩阵
>>> f = torch.eye(2, 2)
>>> f
tensor([[ 1., 0.],
[ 0., 1.]])
# 使用randn()函数,生成随机数矩阵
>>> g = torch.randn(2, 2)
>>> g
tensor([[-0.3979, 0.2728],
[ 1.4558, -0.4451]])
# 使用arange(start, end, step)函数,表示从start到end,间距为step,一维向量
>>> h = torch.arange(1, 6, 2)
>>> h
tensor([ 1., 3., 5.])
# 使用linspace(start, end, steps)函数,表示从start到end,一共steps份,一维向量
>>> i = torch.linspace(1, 6, 2)
>>> i
tensor([ 1., 6.])
# 使用randperm(num)函数,生成长度为num的随机排列向量
>>> j = torch.randperm(4)
>>> j
tensor([ 1, 2, 0, 3])
# PyTorch 0.4中增加了torch.tensor()方法,参数可以为Python的list、NumPy的ndarray等
>>> k = torch.tensor([1, 2, 3])
tensor([ 1, 2, 3])
对于Tensor的维度,可使用Tensor.shape或者size()函数查看每一维的大小,两者等价。
>>> a=torch.randn(2,2)
>>> a.shape # 使用shape查看Tensor维度
torch.Size([2, 2])
>>> a.size() # 使用size()函数查看Tensor维度
torch.Size([2, 2])
对于Tensor的维度,可使用Tensor.shape或者size()函数查看每一维的大小,两者等价。
# 查看Tensor中总的元素个数
>>> a.numel()
4
>>> a.nelement()
4
1.1.3、Tensor的组合与分块
组合与分块是将Tensor相互叠加或者分开,是十分常用的两个功 能,PyTorch提供了多种操作函数。
组合操作是指将不同的Tensor叠加起来,主要有torch.cat()和 torch.stack()两个函数。cat即concatenate的意思,是指沿着已有的数据的某一维度进行拼接,操作后数据的总维数不变,在进行拼接时,除了拼接的维度之外,其他维度必须相同。而torch.stack()函数指新增维度,并按照指定的维度进行叠加,具体示例如下:
# 创建两个2×2的Tensor
>>> a=torch.Tensor([[1,2],[3,4]])
>>> a
tensor([[ 1., 2.],
[ 3., 4.]])
>>> b = torch.Tensor([[5,6], [7,8]])
>>> b
tensor([[ 5., 6.],
[ 7., 8.]])
# 以第一维进行拼接,则变成4×2的矩阵
>>> torch.cat([a,b], 0)
tensor([[ 1., 2.],
[ 3., 4.],
[ 5., 6.],
[ 7., 8.]])
# 以第二维进行拼接,则变成2×4的矩阵
>>> torch.cat([a,b], 1)
tensor([[ 1., 2., 5., 6.],
[ 3., 4., 7., 8.]])
# 以第0维进行stack,叠加的基本单位为序列本身,即a与b,因此输出[a, b],输出维度为2×2×2
>>> torch.stack([a,b], 0)
tensor([[[ 1., 2.],
[ 3., 4.]],
[[ 5., 6.],
[ 7., 8.]]])
# 以第1维进行stack,叠加的基本单位为每一行,输出维度为2×2×2
>>> torch.stack([a,b], 1)
tensor([[[ 1., 2.],
[ 5., 6.]],
[[ 3., 4.],
[ 7., 8.]]])
# 以第2维进行stack,叠加的基本单位为每一行的每一个元素,输出维度为2×2×2
>>> torch.stack([a,b], 2)
tensor([[[ 1., 5.],
[ 2., 6.]],
[[ 3., 7.],
[ 4., 8.]]])
分块则是与组合相反的操作,指将Tensor分割成不同的子Tensor, 主要有torch.chunk()与torch.split()两个函数,前者需要指定分块的数量, 而后者则需要指定每一块的大小,以整型或者list来表示。具体示例如 下:
>>> a=torch.Tensor([[1,2,3],[4,5,6]])
>>> a
tensor([[ 1., 2., 3.],
[ 4., 5., 6.]])
# 使用chunk,沿着第0维进行分块,一共分两块,因此分割成两个1×3的Tensor
>>> torch.chunk(a, 2, 0)
(tensor([[ 1., 2., 3.]]), tensor([[ 4., 5., 6.]]))
# 沿着第1维进行分块,因此分割成两个Tensor,当不能整除时,最后一个的维数会小于前面的
# 因此第一个Tensor为2×2,第二个为2×1
>>> torch.chunk(a, 2, 1)
(tensor([[ 1., 2.],
[ 4., 5.]]), tensor([[ 3.],
[ 6.]]))
# 使用split,沿着第0维分块,每一块维度为2,由于第一维维度总共为2,因此相当于没有分割
>>> torch.split(a, 2, 0)
(tensor([[ 1., 2., 3.],
[ 4., 5., 6.]]),)
# 沿着第1维分块,每一块维度为2,因此第一个Tensor为2×2,第二个为2×1
>>> torch.split(a, 2, 1)
(tensor([[ 1., 2.],
[ 4., 5.]]), tensor([[ 3.],
[ 6.]]))
# split也可以根据输入的list进行自动分块,list中的元素代表了每一个块占的维度
>>> torch.split(a, [1,2], 1)
(tensor([[ 1.],
[ 4.]]), tensor([[ 2., 3.]
[ 5., 6.]]))
1.1.4、Tensor的索引与变形
索引操作与NumPy非常类似,主要包含下标索引、表达式索引、使用torch.where()与Tensor.clamp()的选择性索引。
>>> a = torch.Tensor([[0,1], [2, 3]])
>>> a
tensor([[ 0., 1.],
[ 2., 3.]])
# 根据下标进行索引
>>> a[1]
tensor([ 2., 3.])
>>> a[0,1]
tensor(1.)
# 选择a中大于0的元素,返回和a相同大小的Tensor,符合条件的置1,否则置0
>>> a>0
tensor([[ 0, 1],
[ 1, 1]], dtype=torch.uint8)
# 选择符合条件的元素并返回,等价于torch.masked_select(a, a>0)
>>> a[a>0]
tensor([ 1., 2., 3.])
# 选择非0元素的坐标,并返回
>>> torch.nonzero(a)
tensor([[ 0, 1],
[ 1, 0],
[ 1, 1]])
# torch.where(condition, x, y),满足condition的位置输出x,否则输出y
>>> torch.where(a>1, torch.full_like(a, 1), a)
tensor([[ 0., 1.],
[ 1, 1.]])
# 对Tensor元素进行限制可以使用clamp()函数,示例如下,限制最小值为1,最大值为2
>>> a.clamp(1,2)
tensor([[ 1., 1.],
[ 2., 2.]])
变形操作则是指改变Tensor的维度,以适应在深度学习的计算中, 数据维度经常变换的需求,是一种十分重要的操作。在PyTorch中主要有4类不同的变形方法。
1、view()、resize()和reshape()函数
view()、resize()和reshape()函数可以在不改变Tensor数据的前提下任 意改变Tensor的形状,必须保证调整前后的元素总数相同,并且调整前 后共享内存,三者的作用基本相同。
>>> a=torch.arange(1,5)
>>> a
tensor([ 1., 2., 3., 4.])
# 分别使用view()、resize()及reshape()函数进行维度变换
>>> b=a.view(2,2)
>>> b
tensor([[ 1., 2.],
[ 3., 4.]])
>>> c=a.resize(4,1)
>>> c
tensor([[ 1.],
[ 2.],
[ 3.],
[ 4.]])
>>> d=a.reshape(4,1)
>>> d
tensor([[ 1.],
[ 2.],
[ 3.],
[ 4.]])
# 改变了b、c、d的一个元素,a也跟着改变了,说明两者共享内存
>>> b[0,0]=0
>>> c[1,0]=0
>>> d[2,0]=0
>>> a
tensor([ 0., 0., 0., 4.])
如果想要直接改变Tensor的尺寸,可以使用resize_()的原地操作函 数。在resize_()函数中,如果超过了原Tensor的大小则重新分配内存, 多出部分置0,如果小于原Tensor大小则剩余的部分仍然会隐藏保留。
>>> c=a.resize_(2,3)
>>> c
tensor([[ 0.0000, 2.0000, 3.0000],
[ 4.0000, 0.0000, 0.0000]])
# 发现操作后a也跟着改变了
>>> a
tensor([[ 0.0000, 2.0000, 3.0000],
[ 4.0000, 0.0000, 0.0000]])
2.transpose()和permute()函数
transpose()函数可以将指定的两个维度的元素进行转置,而 permute()函数则可以按照给定的维度进行维度变换。
>>> a=torch.randn(2,2,2)
>>> a
tensor([[[-0.9268, 0.6006],
[ 1.0213, 0.5328]],
[[-0.7024, 0.7978],
[ 1.0553, -0.6524]]])
# 将第0维和第1维的元素进行转置(暂未知其因)
>>> a.transpose(0,1)
tensor([[[-0.9268, 0.6006],
[-0.7024, 0.7978]],
[[ 1.0213, 0.5328],
[ 1.0553, -0.6524]]])
# 按照第2、1、0的维度顺序重新进行元素排列(暂未知其因)
>>> a.permute(2,1,0)
tensor([[[-0.9268, -0.7024],
[ 1.0213, 1.0553]],
[[ 0.6006, 0.7978],
[ 0.5328, -0.6524]]])
3.squeeze()和unsqueeze()函数
在实际的应用中,经常需要增加或减少Tensor的维度,尤其是维度为1的情况,这时候可以使用squeeze()与unsqueeze()函数,前者用于去除size为1的维度,而后者则是将指定的维度的size变为1。
>>> a=torch.arange(1,4)
>>> a.shape
torch.Size([3])
# 将第0维变为1,因此总的维度为1、3
>>> a.unsqueeze(0).shape
torch.Size([1, 3])
# 第0维如果是1,则去掉该维度,如果不是1则不起任何作用
>>> a.unsqueeze(0).squeeze(0).shape
torch.Size([3])
4.expand()和expand_as()函数
有时需要采用复制元素的形式来扩展Tensor的维度,这时expand就 派上用场了。expand()函数将size为1的维度复制扩展为指定大小,也可 以使用expand_as()函数指定为示例Tensor的维度。
>>> a=torch.randn(2,2,1)
>>> a
tensor([[[ 0.5379],
[-0.6294]],
[[ 0.7006],
[ 1.2900]]])
# 将第2维的维度由1变为3,则复制该维的元素,并扩展为3
>>> a.expand(2,2,3)
tensor([[[ 0.5379, 0.5379, 0.5379],
[-0.6294, -0.6294, -0.6294]],
[[ 0.7006, 0.7006, 0.7006],
[ 1.2900, 1.2900, 1.2900]]])
注意:在进行Tensor操作时,有些操作如transpose()、permute()等 可能会把Tensor在内存中变得不连续,而有些操作如view()等是需要 Tensor内存连续的,这种情况下需要使用contiguous()操作先将内存变为连续的。在PyTorch v0.4版本中增加了reshape()操作,可以看做是 Tensor.contiguous().view()。
1.1.5、Tensor的排序与取极值
比较重要的是排序函数sort(),选择沿着指定维度进行排序,返回排 序后的Tensor及对应的索引位置。max()与min()函数则是沿着指定维度 选择最大与最小元素,返回该元素及对应的索引位置。
>>> a=torch.randn(3,3)
>>> a
tensor([[ 1.0179, -1.4278, -0.0456],
[-1.1668, 0.4531, -1.5196],
[-0.1498, -0.2556, -1.4915]]
# 按照第0维即按行排序,每一列进行比较,True代表降序,False代表升序
>>> a.sort(0, True)[0]
tensor([[ 1.0179, 0.4531, -0.0456],
[-0.1498, -0.2556, -1.4915],
[-1.1668, -1.4278, -1.5196]])
>>> a.sort(0, True)[1]
tensor([[ 0, 1, 0],
[ 2, 2, 2],
[ 1, 0, 1]])
# 按照第0维即按行选取最大值,即将每一列的最大值选取出来
>>> a.max(0)
(tensor([ 1.0179, 0.4531, -0.0456]), tensor([ 0, 1, 0]))
对于Tensor的单元素数学运算,如abs()、sqrt()、log()、pow()和三 角函数等,都是逐元素操作(element-wise),输出的Tensor形状与原始 Tensor形状一致。
1.1.6、Tensor的自动广播机制与向量化
PyTorch在0.2版本以后,推出了自动广播语义,即不同形状的 Tensor进行计算时,可自动扩展到较大的相同形状,再进行计算。广播机制的前提是任一个Tensor至少有一个维度,且从尾部遍历Tensor维度 时,两者维度必须相等,或其中一个要么是1要么不存在。
>>> a=torch.ones(3,1,2)
>>> b=torch.ones(2,1)
# 从尾部遍历维度,1对应2,2对应1,3对应不存在,因此满足广播条件,最后求和后的维度为[3,2,2]
>>> (a+b).size()
torch.Size([3, 2, 2])
>>> c=torch.ones(2,3)
# a与c最后一维的维度为2对应3,不满足广播条件,因此报错
>>> (a+c).size()
Traceback (most recent call last):
File "", line 1, in
RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 2
向量化操作是指可以在同一时间进行批量地并行计算,例如矩阵运算,以达到更好的计算效率的一种方式。在实际使用时,应尽量使用向量化直接对Tensor操作,避免低效率的for循环对元素逐个操作,尤其是在训练网络模型时,如果有大量的for循环,会极大地影响训练的速度。
1.1.7、Tensor的内存共享
为了实现高效计算,PyTorch提供了一些原地操作运算,即in-place operation,不经过复制,直接在原来的内存上进行计算。对于内存的共享,主要有如下3种情况。
1.通过Tensor初始化Tensor
直接通过Tensor来初始化另一个Tensor,或者通过Tensor的组合、 分块、索引、变形操作来初始化另一个Tensor,则这两个Tensor共享内存。
>>> a=torch.randn(2,2)
>>> a
tensor([[ 0.0666, -0.3389],
[ 0.8224, 0.6678]])
# 用a初始化b,或者用a的变形操作初始化c,这三者共享内存,一个变,其余的也改变了
>>> b=a
>>> c=a.view(4)
>>> b[0,0]=0
>>> c[3]=4
>>> a
tensor([[ 0.0000, -0.3389],
[ 0.8224, 4.0000]])
2.原地操作符
PyTorch对于一些操作通过加后缀“_”实现了原地操作,如add_()和 resize_()等,这种操作只要被执行,本身的Tensor则会被改变。
>>> a=torch.Tensor([[1,2],[3,4]])
# add_()函数使得a也改变了
>>> b=a.add_(a)
>>> a
tensor([[ 2., 4.],
[ 6., 8.]])
# resize_()函数使得a也发生了改变
>>> c=a.resize_(4)
>>> a
tensor([ 2., 4., 6., 8.])
3.Tensor与NumPy转换
Tensor与NumPy可以高效地进行转换,并且转换前后的变量共享内 存。在进行PyTorch不支持的操作时,甚至可以曲线救国,将Tensor转 换为NumPy类型,操作后再转为Tensor。
>>> a=torch.randn(1,2)
>>> a
tensor([[-0.3228, 1.2726]])
# Tensor转为NumPy
>>> b=a.numpy()
>>> b
array([[-0.32281783, 1.2725701 ]], dtype=float32)
# NumPy转为Tensor
>>> c=torch.from_numpy(b)
>>> c
tensor([[-0.3228, 1.2726]])
#Tensor转为list
>>> d=a.tolist()
>>> d
[[-0.3228178322315216, 1.2725701332092285]]
1.2、Autograd与计算图
基本数据Tensor可以保证完成前向传播,想要完成神经网络的训 练,接下来还需要进行反向传播与梯度更新,而PyTorch提供了自动求 导机制autograd,将前向传播的计算记录成计算图,自动完成求导。
在PyTorch 0.4版本之前,Tensor仅仅是对多维数组的抽象,使用自动求导机制需要将Tensor封装成torch.autograd.Variable类型,才能构建计算图。PyTorch 0.4版本则将Tensor与Variable进行了整合,以前Variable的使用情景都可以直接使用Tensor,变得更简单实用。
1.2.1、Tensor的自动求导:Autograd
自动求导机制记录了Tensor的操作,以便自动求导与反向传播。可以通过requires_grad参数来创建支持自动求导机制的Tensor。
>>> import torch
>>> a = torch.randn(2,2, requires_grad=True)
require_grad参数表示是否需要对该Tensor进行求导,默认为False; 设置为True则需要求导,并且依赖于该Tensor的之后的所有节点都需要求导。值得注意的是,在PyTorch 0.4对于Tensor的自动求导中,volatile参数已经被其他torch.no_grad()等函数取代了。
Tensor有两个重要的属性,分别记录了该Tensor的梯度与经历的操作。
grad:该Tensor对应的梯度,类型为Tensor,并与Tensor同维度。
grad_fn:指向function对象,即该Tensor经过了什么样的操作,用作反向传播的梯度计算,如果该Tensor由用户自己创建,则该grad_fn为None。
具体的参数使用示例如下:
>>> import torch
>>> a=torch.randn(2,2,requires_grad=True)
>>> b=torch.randn(2, 2)
# 可以看到默认的Tensor是不需要求导的,设置requires_grad为True后则需要求导
>>> a.requires_grad, b.requires_grad
True, False
# 也可以通过内置函数requires_grad_()将Tensor变为需要求导
>>> b.requires_grad_()
tensor([[ 0.0260, -0.1183],
[-1.0907, 0.8107]])
>>> b.requires_grad
True
# 通过计算生成的Tensor,由于依赖的Tensor需要求导,因此c也需要求导
>>> c = a + b
>>> c.requires_grad
True
# a与b是自己创建的,grad_fn为None,而c的grad_fn则是一个Add函数操作
>>> a.grad_fn, b.grad_fn, c.grad_fn
(None, None, < AddBackward1 object at 0x7fa7a53e04a8 >)
>>> d = c.detach()
>>> d.requires_grad
False
注意:早些版本使用.data属性来获取数据,PyTorch 0.4中建议使用Tensor.detach()函数,因为.data属性在某些情况下不安全,原因在于对.data生成的数据进行修改不会被autograd追踪。Tensor.detach()函数生成的数据默认requires_grad为False。
1.2.2、计算图
计算图是PyTorch对于神经网络的具体实现形式,包括每一个数据Tensor及Tensor之间的函数function。在此我们以z=wx+b为例,通常在神经网络中,x为输入,w与b为网络需要学习的参数,z为输出,在这一层,计算图构建方法如图所示。
在上图中,x、ω和b都是用户自己创建的,因此都为叶节点,ωx首先经过乘法算子产生中间节点y,然后与b经过加法算法产生最终输出z,并作为根节点。
Autograd的基本原理是随着每一步Tensor的计算操作,逐渐生成计算图,并将操作的function记录在Tensor的grad_fn中。在前向计算完后,只需对根节点进行backward函数操作,即可从当前根节点自动进行反向传播与梯度计算,从而得到每一个叶子节点的梯度,梯度计算遵循链式求导法则。
>>> import torch
# 生成3个Tensor变量,并作为叶节点
>>> x = torch.randn(1)
>>> w = torch.ones(1, requires_grad=True)
>>> b = torch.ones(1, requires_grad=True)
# 自己生成的,因此都为叶节点
>>> x.is_leaf, w.is_leaf, b.is_leaf
(True, True, True)
# 默认是不需要求导,关键字赋值为True后则需要求导
>>> x.requires_grad, w.requires_grad, b.requires_grad
(False, True, True)
# 进行前向计算,由计算生成的变量都不是叶节点
>>> y=w*x
>>> z=y+b
>>> y.is_leaf, z.is_leaf
(False, False)
# 由于依赖的变量有需要求导的,因此y与z都需要求导
>>> y.requires_grad, z.requires_grad
(True, True)
# grad_fn记录生成该变量经过了什么操作,如y是Mul,z是Add
>>> y.grad_fn
< MulBackward1 object at 0x7f4d4b49e208 >
>>> z.grad_fn
< AddBackward1 object at 0x7f4d4b49e0f0 >
# 对根节点调用backward()函数,进行梯度反传
>>> z.backward(retain_graph=True)
>>> w.grad
tensor([-2.2474])
>>> b.grad
tensor([ 1.])
1.2.3、Autograd注意事项
PyTorch的Autograd机制使得其可以灵活地进行前向传播与梯度计算,在实际使用时,需要注意以下3点。
动态图特性:PyTorch建立的计算图是动态的,这也是PyTorch的一大特点。动态图是指程序运行时,每次前向传播时从头开始构建计算图,这样不同的前向传播就可以有不同的计算图,也可以在前向时插入各种Python的控制语句,不需要事先把所有的图都构建出来,并且可以很方便地查看中间过程变量。
backward()函数还有一个需要传入的参数grad_variabels,其代表了根节点的导数,也可以看做根节点各部分的权重系数。因为PyTorch不允许Tensor对Tensor求导,求导时都是标量对于Tensor进行求导,因此,如果根节点是向量,则应配以对应大小的权重,并求和得到标量,再反传。如果根节点的值是标量,则该参数可以省略,默认为1。
当有多个输出需要同时进行梯度反传时,需要将retain_graph设置为True,从而保证在计算多个输出的梯度时互不影响。
1.3、神经网络工具箱torch.nn
torch.autograd库虽然实现了自动求导与梯度反向传播,但如果我们要完成一个模型的训练,仍需要手写参数的自动更新、训练过程的控制等,还是不够便利。为此,PyTorch进一步提供了集成度更高的模块化接口torch.nn,该接口构建于Autograd之上,提供了网络模组、优化器和初始化策略等一系列功能。
1.3.1、nn.Module类
nn.Module是PyTorch提供的神经网络类,并在类中实现了网络各层的定义及前向计算与反向传播机制。在实际使用时,如果想要实现某个神经网络,只需继承nn.Module,在初始化中定义模型结构与参数,在函数forward()中编写网络前向过程即可。
下面具体以一个由两个全连接层组成的感知机为例,介绍如何使用nn.Module构造模块化的神经网络。新建一个perception.py文件,内容如下:
import torch
from torch import nn
# 首先建立一个全连接的子module,继承nn.Module
class Linear(nn.Module):
def __init__(self, in_dim, out_dim):
super(Linear, self).__init__() # 调用nn.Module的构造函数
# 使用nn.Parameter来构造需要学习的参数
self.w = nn.Parameter(torch.randn(in_dim, out_dim))
self.b = nn.Parameter(torch.randn(out_dim))
# 在forward中实现前向传播过程
def forward(self, x):
x = x.matmul(self.w) # 使用Tensor.matmul实现矩阵相乘(左边行乘以右边列再相加)
y = x + self.b.expand_as(x) # 使用Tensor.expand_as()来保证矩阵形状一致
return y
# 构建感知机类,继承nn.Module,并调用了Linear的子module
class Perception(nn.Module):
def __init__(self, in_dim, hid_dim, out_dim):
super(Perception, self).__init__()
self.layer1 = Linear(in_dim, hid_dim)
self.layer2 = Linear(hid_dim, out_dim)
def forward(self, x):
x = self.layer1(x)
y = torch.sigmoid(x) # 使用torch中的sigmoid作为激活函数
y = self.layer2(y)
y = torch.sigmoid(y)
return y
编写完网络模型模块后,在终端中可以使用如下方法调用该模块
>>> import torch
>>> from perception import Perception # 调用上述模块
# 实例化一个网络,并赋值全连接中的维数,最终输出二维代表了二分类
>>> perception = Perception(2,3,2)
# 可以看到perception中包含上述定义的layer1与layer2
>>> perception
Perception(
(layer1): Linear()
(layer2): Linear()
)
# named_parameters()可以返回学习参数的迭代器,分别为参数名与参数值
>>> for name, parameter in perception.named_parameters():
... print(name, parameter)
...
layer1.w Parameter containing:
tensor([[ 0.1265, -0.6858, 0.0637],
[ 0.5424, -0.2596, -2.1362]])
layer1.b Parameter containing:
tensor([-0.1427, 1.4034, 0.1175])
layer2.w Parameter containing:
tensor([[ 0.2575, -3.6569],
[ 0.3657, -1.2370],
[ 0.7178, -0.9569]])
layer2.b Parameter containing:
tensor([ 0.2041, -0.2558])
# 随机生成数据,注意这里的4代表了样本数为4,每个样本有两维
>>> data = torch.randn(4,2)
>>> data
tensor([[ 0.1399, -0.6214],
[ 0.1325, -1.6260],
[ 0.0035, -1.0567],
[-0.6020, -0.9674]])
# 将输入数据传入perception,perception()相当于调用perception中的forward()函数
>>> output = perception(data)
>>> output
tensor([[ 0.7654, 0.0308],
[ 0.7829, 0.0386],
[ 0.7779, 0.0331],
[ 0.7781, 0.0326]])
可以看到,利用nn.Module搭建神经网络简单易实现,同时较为规范。在实际使用时,应注意如下5点。
1.nn.Parameter函数
在类的__init__()中需要定义网络学习的参数,在此使用nn.Parameter()函数定义了全连接中的ω和b,这是一种特殊的Tensor的构造方法,默认需要求导,即requires_grad为True。
2.forward()函数与反向传播
forward()函数用来进行网络的前向传播,并需要传入相应的 Tensor,例如上例的perception(data)即是直接调用了forward()。在具体底 层实现中,perception.call(data)将类的实例perception变成了可调用 对象perception(data),而在perception.call(data)中主要调用了 forward()函数,具体可参考官方代码。
3.多个Module的嵌套
在Module的搭建时,可以嵌套包含子Module,上例的Perception中调用了Linear这个类,这样的代码分布可以使网络更加模块化,提升代码的复用性。在实际的应用中,PyTorch也提供了绝大多数的网络层,如全连接、卷积网络中的卷积、池化等,并自动实现前向与反向传播。在后面的章节中会对比较重要的层进行讲解。
4.nn.Module与nn.functional库
在PyTorch中,还有一个库为nn.functional,同样也提供了很多网络层与函数功能,但与nn.Module不同的是,利用nn.functional定义的网络层不可自动学习参数,还需要使用nn.Parameter封装。nn.functional的设计初衷是对于一些不需要学习参数的层,如激活层、BN(Batch Normalization)层,可以使用nn.functional,这样这些层就不需要在nn.Module中定义了。
总体来看,对于需要学习参数的层,最好使用nn.Module,对于无参数学习的层,可以使用nn.functional,当然这两者间并没有严格的好坏之分。
5.nn.Sequential()模块
当模型中只是简单的前馈网络时,即上一层的输出直接作为下一层的输入,这时可以采用nn.Sequential()模块来快速搭建模型,而不必手动在forward()函数中一层一层地前向传播。因此,如果想快速搭建模型而不考虑中间过程的话,推荐使用nn.Sequential()模块。
在上面的例子中,Perception类中的layer1与layer2是直接传递的, 因此该Perception类可以使用nn.Sequential()快速搭建。在此新建一个perception_sequential.py文件,内容如下:
from torch import nn
class Perception(nn.Module):
def __init__(self, in_dim, hid_dim, out_dim):
super(Perception, self).__init__()
# 利用nn.Sequential()快速搭建网络模块
self.layer = nn.Sequential(
nn.Linear(in_dim, hid_dim),
nn.Sigmoid(),
nn.Linear(hid_dim, out_dim),
nn.Sigmoid()
)
def forward(self, x):
y = self.layer(x)
return y
在终端中进入上述perception_sequential.py文件的同级目录下,输入python3进入交互环境,使用如下指令即可调用该网络结构。
>>> import torch # 引入torch模块
# 从上述文件中引入Perception类
>>> from perception_sequential import Perception
>>> model = Perception(100, 1000, 10).cuda() # 构建类的实例,并表明在CUDA上
# 打印model结构,会显示Sequential中每一层的具体参数配置
>>> model
Perception(
(layer): Sequential(
(0): Linear(in_features=100, out_features=1000, bias=True)
(1): Sigmoid()
(2): Linear(in_features=1000, out_features=10, bias=True)
(3): Sigmoid()
)
)
>>> input = torch.randn(100).cuda()
>>> output = model(input) # 将输入传入实例化的模型
>>> output.shape
torch.Size([10])
1.3.2、损失函数
在深度学习中,损失反映模型最后预测结果与实际真实值之间的差距,可以用来分析训练过程的好坏、模型是否收敛等,例如均方损失、交叉熵损失等。在PyTorch中,损失函数可以看做是网络的某一层而放到模型定义中,但在实际使用时更偏向于作为功能函数而放到前向传播过程中。
PyTorch在torch.nn及torch.nn.functional中都提供了各种损失函数,通常来讲,由于损失函数不含有可学习的参数,因此这两者在功能上基本没有区别。
# 接着1.3.1节中的终端环境继续运行,来进一步求损失
>>> from torch import nn
>>> import torch.nn.functional as F
# 设置标签,由于是二分类,一共有4个样本,因此标签维度为14,每个数为0或1两个类别
>>> label = torch.Tensor([0, 1, 1, 0]).long()
# 实例化nn中的交叉熵损失类
>>> criterion = nn.CrossEntropyLoss()
# 调用交叉熵损失
>>> loss_nn = criterion(output, label)
>>> loss_nn
tensor(0.7616)
# 由于F.cross_entropy是一个函数,因此可以直接调用,不需要实例化,两者求得的损失值相同
>>> loss_functional = F.cross_entropy(output, label)
>>> loss_loss_functional
tensor(0.7616)
1.3.3、优化器nn.optim
在上述介绍中,nn.Module模块提供了网络骨架,nn.functional提供了各式各样的损失函数,而Autograd又自动实现了求导与反向传播机制,这时还缺少一个如何进行模型优化、加速收敛的模块,nn.optim应运而生。
nn.optim中包含了各种常见的优化算法,包括随机梯度下降算法SGD(Stochastic Gradient Descent,随机梯度下降)、Adam(Adaptive Moment Estimation)、Adagrad、RMSProp,这里仅对常用的SGD与Adam两种算法进行详细介绍。
1.SGD方法
梯度下降(Gradient Descent)是迭代法中的一种,是指沿着梯度下降的方向求解极小值,一般可用于求解最小二乘问题。在深度学习中, 当前更常用的是SGD算法,以一个小批次(Mini Batch)的数据为单 位,计算一个批次的梯度,然后反向传播优化,并更新参数。SGD的表 达式如式(2-1)与式(2-2)所示。
公式中,gt代表了参数的梯度,η代表了学习率(Learning Rate), 即梯度影响参数更新的程度,是训练中非常重要的一个超参数。SGD优化算法的好处主要有两点:
·分担训练压力:当前数据集通常数量较多,尺度较大,使用较大 的数据同时训练显然不现实,SGD则提供了小批量训练并优化网络的方 法,有效分担了GPU等计算硬件的压力。
·加快收敛:由于SGD一次只采用少量的数据,这意味着会有更多 次的梯度更新,在某些数据集中,其收敛速度会更快。
当然,SGD也有其自身的缺点:
·初始学习率难以确定:SGD算法依赖于一个较好的初始学习率, 但设置初始学习率并不直观,并且对于不同的任务,其初始值也不固定。
·容易陷入局部最优:SGD虽然采用了小步快走的思想,但是容易陷入局部的最优解,难以跳出。
有效解决局部最优的通常做法是增加动量(momentum),其概念来自于物理学,在此是指更新的时候一定程度上保留之前更新的方向,同时利用当前批次的梯度进行微调,得到最终的梯度,可以增加优化的稳定性,降低陷入局部最优难以跳出的风险。其函数如式(2-3)与式 (2-4)所示。
公式中的μ为动量因子,当此次梯度下降方向与上次相同时,梯度会变大,也就会加速收敛。当梯度方向不同时,梯度会变小,从而抑制梯度更新的震荡,增加稳定性。在训练的中后期,梯度会在局部极小值周围震荡,此时gt接近于0,但动量的存在使得梯度更新并不是0,从而有可能跳出局部最优解。
虽然SGD算法并不完美,但在当今的深度学习算法中仍然取得了大 量的应用,使用SGD有时能够获得性能更佳的模型。
2.Adam方法
在SGD之外,Adam是另一个较为常见的优化算法。Adam利用了梯度的一阶矩与二阶矩动态地估计调整每一个参数的学习率,是一种学习率自适应算法。
Adam的优点在于,经过调整后,每一次迭代的学习率都在一个确定范围内,使得参数更新更加平稳。此外,Adam算法可以使模型更快收敛,尤其适用于一些深层网络,或者神经网络较为复杂的场景。
下面利用PyTorch来搭建常用的优化器,传入参数包括网络中需要学习优化的Tensor对象、学习率和权值衰减等。
from torch import optim
optimizer = optim.SGD(model.parameters(), lr = 0.001, momentum = 0.9)
optimizer = optim.Adam([var1, var2], lr = 0.0001)
下面通过一个三层感知机的例子来介绍基本的优化过程。新建一个mlp.py文件,内容如下:
from torch import nn
class MLP(nn.Module):
def __init__(self, in_dim, hid_dim1, hid_dim2, out_dim):
super(MLP, self).__init__()
# 通过Sequential快速搭建三层的感知机
self.layer = nn.Sequential(
nn.Linear(in_dim, hid_dim1)
nn.ReLU(),
nn.Linear(hid_dim1, hid_dim2),
nn.ReLU(),
nn.Linear(hid_dim2, out_dim),
nn.ReLU()
)
def forward(self, x):
x = self.layer(x)
return x
在终端环境中执行如下代码:
>>> import torch
>>> from mlp import MLP
>>> from torch import optim
>>> from torch import nn;
# 实例化模型,并赋予每一层的维度
>>> model = MLP(28*28, 300, 200, 10)
>>> model # 打印model的结构,由3个全连接层组成
MLP(
(layer): Sequential(
(0): Linear(in_features=784, out_features=300, bias=True)
(1): ReLU()
(2): Linear(in_features=300, out_features=200, bias=True)
(3): ReLU()
(4): Linear(in_features=200, out_features=10, bias=True)
(5): ReLU()
)
)
# 采用SGD优化器,学习率为0.01
>>> optimizer = optim.SGD(params = model.parameters(), lr=0.01)
>>> data = torch.randn(10, 28*28)
>>> output = model(data)
# 由于是10分类,因此label元素从0到9,一共10个样本
>>> label = torch.Tensor([1,0,4,7,9,3,4,5,3,2]).long()
>>> label
tensor([ 1, 0, 4, 7, 9, 3, 4, 5, 3, 2])
# 求损失
>>> criterion = nn.CrossEntropyLoss()
>>> loss = criterion(output, label)
>>> loss
tensor(2.2762)
# 清空梯度,在每次优化前都需要进行此操作
>>> optimizer.zero_grad()
# 损失的反向传播
>>> loss.backward()
# 利用优化器进行梯度更新
>>> optimizer.step()
对于训练过程中的学习率调整,需要注意以下两点:
·不同参数层分配不同的学习率:优化器也可以很方便地实现将不同的网络层分配成不同的学习率,即对于特殊的层单独赋予学习率,其余的保持默认的整体学习率,具体示例如下:
# 对于model中需要单独赋予学习率的层,如special层,则使用'lr'关键字单独赋予
optimizer = optim.SGD(
[{'params': model.special.parameters(), 'lr': 0.001},
{'params': model.base.parameters()}], lr=0.0001)
·学习率动态调整:对于训练过程中动态的调整学习率,可以在迭代次数超过一定值后,重新赋予optim优化器新的学习率。
1.4、模型处理
模型是神经网络训练优化后得到的成果,包含了神经网络骨架及学习得到的参数。PyTorch对于模型的处理提供了丰富的工具,本节将从模型的生成、预训练模型的加载和模型保存3个方面进行介绍。
1.4.1、网络模型库:torchision.models
对于深度学习,torchvision.models库提供了众多经典的网络结构与预训练模型,例如VGG、ResNet和Inception等,利用这些模型可以快速搭建物体检测网络,不需要逐层手动实现。torchvision包与PyTorch相独立,需要通过pip指令进行安装,如下:
pip install torchvision # 适用于Python 2
pip3 install torchvision # 适用于Python 3
以VGG模型为例,在torchvision.models中,VGG模型的特征层与分类层分别用vgg.features与vgg.classifier来表示,每个部分是一个nn.Sequential结构,可以方便地使用与修改。下面讲解如何使用 torchvision.model模块。
>>> from torch import nn
>>> from torchvision import models
# 通过torchvision.model直接调用VGG16的网络结构
>>> vgg = models.vgg16()
# VGG16的特征层包括13个卷积、13个激活函数ReLU、5个池化,一共31层
>>> len(vgg.features)
31
# VGG16的分类层包括3个全连接、2个ReLU、2个Dropout,一共7层
>>> len(vgg.classifier)
7
# 可以通过出现的顺序直接索引每一层
>>> vgg.classifier[-1]
Linear(in_features=4096, out_features=1000, bias=True)
# 也可以选取某一部分,如下代表了特征网络的最后一个卷积模组
>>> vgg.features[24:]
Sequential(
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1,ceil_ mode=False)
1.4.2、加载预训练模型
对于计算机视觉的任务,包括物体检测,我们通常很难拿到很大的数据集,在这种情况下重新训练一个新的模型是比较复杂的,并且不容易调整,因此,Fine-tune(微调)是一个常用的选择。所谓Fine-tune是 指利用别人在一些数据集上训练好的预训练模型,在自己的数据集上训练自己的模型。
在具体使用时,通常有两种情况,第一种是直接利用torchvision.models中自带的预训练模型,只需要在使用时赋予pretrained 参数为True即可。
>>> from torch import nn
>>> from torchvision import models
# 通过torchvision.model直接调用VGG16的网络结构
>>> vgg = models.vgg16(pretrained=True)
第二种是如果想要使用自己的本地预训练模型,或者之前训练过的 模型,则可以通过model.load_state_dict()函数操作,具体如下:
>>> import torch
>>> from torch import nn
>>> from torchvision import models
# 通过torchvision.model直接调用VGG16的网络结构
>>> vgg = models.vgg16()
>>> state_dict = torch.load("your model path")
# 利用load_state_dict,遍历预训练模型的关键字,如果出现在了VGG中,则加载预训练参数
>>> vgg.load_state_dict({k:v for k, v in state_dict_items() if k in vgg.state_dict()})
通常来讲,对于不同的检测任务,卷积网络的前两三层的作用是非常类似的,都是提取图像的边缘信息等,因此为了保证模型训练中能够更加稳定,一般会固定预训练网络的前两三个卷积层而不进行参数的学 习。例如VGG模型,可以设置前三个卷积模组不进行参数学习,设置方式如下:
for layer in range(10):
for p in vgg[layer].parameters():
p.requires_grad = False
1.4.3、模型保存
在PyTorch中,参数的保存通过torch.save()函数实现,可保存对象包括网络模型、优化器等,而这些对象的当前状态数据可以通过自身的state_dict()函数获取。
torch.save({
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'model_path.pth')
1.5、数据处理
数据对于深度学习而言是至关重要的,丰富、完整、规范的数据集 往往能训练出效果更佳的网络模型。本节将首先介绍当前较为主流的公开数据集,然后从数据的加载、数据的GPU加速、数据的可视化3个方面介绍PyTorch的使用方法。
1.5.1、主流公开数据集
深度学习能够取得快速发展的一个原因是建立在大量数据的基础上,“数据为王”毫不夸张。世界上一些先进的研究机构与公司开源了一些公开数据集,这些数据集规模较大,质量较高,一方面方便研究者利用这些数据训练自己的模型,同时也为先进的论文研究提供了标准的评测平台。数据集随着深度学习算法的提升,其规模也不断变大,任务也渐渐地由简单到复杂。下面简要介绍在物体检测领域较为重要的3个公开数据集。
1.ImageNet数据集
ImageNet数据集首次在2009年计算机视觉与模式识别(CVPR)会议上发布,其目的是促进计算机图像识别的技术发展。ImageNet数据集从问世以来,一直被作为计算机视觉领域最重要的数据集之一,任何基于ImageNet的技术上的进步都会对计算机视觉领域产生重要的影响。 ImageNet数据集一共有1400多万张图片,共计2万多个类别,超过百万的图片有明确的类别标注,以及图像中物体的位置信息。
与ImageNet数据集对应的有一个享誉全球的ImageNet国际计算机视觉挑战赛(ILSVRC),该竞赛包含了图像分类、物体检测与定位、视频物体识别等多个领域。在2012年,Hinton带领的团队利用深度学习算 法在分类竞赛中完胜其他对手,在计算机领域引起了轰动,进而掀起了深度学习的浪潮。
随后的几年中,众多公司与知名研究机构展开了激烈的角逐,陆续诞生了众多先进的算法,而在2017年的比赛则是最后一届,以后会往更高的理解层发展。值得一提的是,通常我们在训练自己的模型时也经常使用从ImageNet上预训练得到的模型。
2.PASCAL VOC数据集
PASCAL VOC为图像分类与物体检测提供了一整套标准的数据集,并从2005年到2012年每年都举行一场图像检测竞赛。PASCAL全称为 Pattern Analysis,Statistical Modelling and Computational Learning,其中常用的数据集主要有VOC 2007与VOC 2012两个版本,VOC 2007中包含了9963张标注过的图片及24640个物体标签。在VOC 2007之上,VOC 2012进一步升级了数据集,一共有11530张图片,包含人、狗、椅子和桌子等20个物体类别,图片大小约500×375像素。VOC整体图像质量较好,标注比较完整,非常适合进行模型的性能测试.
PASCAL VOC的另一个贡献在于提供了一套标准的数据集格式,尤其是对于物体检测领域,大部分的开源物体检测算法都提供了PASCAL VOC的数据接口。对于物体检测,有3个重要的文件夹,具体如下:
·JPEGImages:包含所有训练与测试的图片。
·Annotations:存放XML格式的标签数据,每一个XML文件都对应于JPEGImages文件夹下的一张图片。
·ImageSets:对于物体检测只需要Main子文件夹,并在Main文件夹中建立Trainval.txt、train.txt、val.txt及test.txt,在各文件中记录相应的图片名即可。
3.COCO(Common Objects in Context)数据集
COCO是由微软赞助的一个大型数据集,针对物体检测、分割、图像语义理解和人体关节点等,拥有超过30万张图片,200万多个实例及80个物体类别。在ImageNet竞赛停办后,COCO挑战赛成为了当前物体检测领域最权威的一个标杆。相比PASCAL VOC,COCO数据集难度更 大,其拥有的小物体更多,物体大小的跨度也更大。
与PASCAL VOC一样,众多的开源算方法也通常会提供以COCO格式为标准的数据加载方式。为了更好地使用数据集,COCO也提供了基于Lua、Python及MATLAB的API,具体使用方法可以参考开源代码。
当然,随着自动驾驶领域的快速发展,也出现了众多自动驾驶领域 的数据集,如KITTI、Cityscape和Udacity等,具体使用方法可以查看相 关数据集官网。
1.5.2、数据加载
PyTorch将数据集的处理过程标准化,提供了Dataset基本的数据类,并在torchvision中提供了众多数据变换函数,数据加载的具体过程主要分为3步。
这3步的具体功能与实现如下:
1.继承Dataset类
对于数据集的处理,PyTorch提供了torch.utils.data.Dataset这个抽象类,在使用时只需要继承该类,并重写__len__()和__getitem()__函数, 即可以方便地进行数据集的迭代。
from torch.utils.data import Dataset
class my_data(Dataset):
# 初始化,读取数据集
def __init__(self, image_path, annotation_path, transform=None):
# 获取数据集的总大小
def __len__(self):
# 对于指定的id,读取该数据并返回
def __getitem__(self, id):
对上述类进行实例化,即可进行迭代如下:
dataset = my_data("your image path", "your annotation path") # 实例化该类
for data in dataset:
print(data)
2.数据变换与增强:torchvision.transforms
第一步虽然将数据集加载到了实例中,但在实际应用时,数据集中的图片有可能存在大小不一的情况,并且原始图片像素RGB值较大 (0~255),这些都不利于神经网络的训练收敛,因此还需要进行一些图像变换工作。PyTorch为此提供了torchvision.transforms工具包,可以方便地进行图像缩放、裁剪、随机翻转、填充及张量的归一化等操作,操作对象是PIL的Image或者Tensor。
如果需要进行多个变换功能,可以利用transforms.Compose将多个变换整合起来,并且在实际使用时,通常会将变换操作集成到Dataset的继承类中。具体示例如下:
from torchvision import transforms
# 将transforms集成到Dataset类中,使用Compose将多个变换整合到一起
dataset = my_data("your image path", "your annotation path", transforms=
transforms.Compose([
# 将图像最短边缩小至256,宽高比例不变
transforms.Resize(256)
# 以0.5的概率随机翻转指定的PIL图像
transforms.RandomHorizontalFlip()
# 将PIL图像转为Tensor,元素区间从[0, 255]归一到[0, 1]
transforms.ToTensor()
# 进行mean与std为0.5的标准化
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
]))
3.继承dataloader
经过前两步已经可以获取每一个变换后的样本,但是仍然无法进行批量处理、随机选取等操作,因此还需要torch.utils.data.Dataloader类进 一步进行封装,使用方法如下例所示,该类需要4个参数,第1个参数是 之前继承了Dataset的实例,第2个参数是批量batch的大小,第3个参数是是否打乱数据参数,第4个参数是使用几个线程来加载数据。
from torch.utils.data import Dataloader
# 使用Dataloader进一步封装Dataset
dataloader = Dataloader(dataset, batch_size=4, shuffle=True, num_workers=4)
dataloader是一个可迭代对象,对该实例进行迭代即可用于训练过程
data_iter = iter(dataloader)
for step in range(iters_per_epoch):
data = next(data_iter)
# 将data用于训练网络即可
1.5.3、GPU加速
PyTorch为数据在GPU上运行提供了非常便利的操作。首先可以使用torch.cuda.is_available()来判断当前环境下GPU是否可用,其次是对于Tensor和模型,可以直接调用cuda()方法将数据转移到GPU上运行,并且可以输入数字来指定具体转移到哪块GPU上运行。
>>> import torch
>>> from torchvision import models
>>> a = torch.randn(3,3)
>>> b = models.vgg16()
# 判断当前GPU是否可用
>>> if torch.cuda.is_available():
a = a.cuda()
# 指定将b转移到编号为1的GPU上
b = b.cuda(1)
# 使用torch.device()来指定使用哪一个GPU
>>> device = torch.device("cuda: 1")
>>> c = torch.randn(3, 3, device = device, requires_grad = True)
对于在全局指定使用哪一块GPU,官方给出了两种方法,首先是在终端执行脚本时直接指定GPU的方式,如下:
CUDA_VISIBLE_DEVICES=2 python3 train.py
其次是在脚本中利用函数指定,如下:
import torch
torch.cuda.set_device(1)
官方建议使用第一种方法,即CUDA_VISIBLE_DEVICE的方式。
在工程应用中,通常使用torch.nn.DataParallel(module,device_ids)函数来处理多GPU并行计算的问题。示例如下:
model_gpu = nn.DataParallel(model, device_ids=[0,1])
output = model_gpu(input)
多GPU处理的实现方式是,首先将模型加载到主GPU上,然后复制模型到各个指定的GPU上,将输入数据按batch的维度进行划分,分配到每个GPU上独立进行前向计算,再将得到的损失求和并反向传播更新单 个GPU上的参数,最后将更新后的参数复制到各个GPU上。
1.5.4、数据可视化
在训练神经网络时,如果想了解训练的具体情况,可以在终端中打印出各种训练信息,但这种方法不够直观,难以从整体角度判断模型的收敛情况,因此产生了各种数据可视化工具,可以在网络训练时更好地 查看训练过程中的各个损失变化情况,监督训练过程,并为进一步的参数优化与训练优化提供方向。
在PyTorch中,常用的可视化工具有TensorBoardX和Visdom。
1.TensorBoardX简介
对于TensorFlow开发者,TensorBoard是较为熟悉的一套可视化工 具,提供了数据曲线、计算图等数据的可视化功能。TensorBoard作为 独立于TensorFlow的一个工具,可以将按固定格式保存的数据可视化。
在PyTorch中,也可以使用Tensorboard_logger进行可视化,但其功能较少。Tensor-BoardX是专为PyTorch开发的一套数据可视化工具,功能与TensorBoard相当,支持曲线、图片、文本和计算图等不同形式的可视化,而且使用简单。下面以实现损失曲线的可视化为例,介绍TensorBoardX的使用方法。
通过如下命令即可完成TensorBoardX的安装。
pip3 install tensorboardX
在训练脚本中,添加如下几句指令,即可创建记录对象与数据的添加。
from tensorboardX import SummaryWriter
# 创建writer对象
writer = SummaryWriter('logs/tmp')
# 添加曲线,并且可以使用'/'进行多级标题的指定
writer.add_scalar('loss/total_loss', loss.data[0], total_iter)
writer.add_scalar('loss/rpn_loss', rpn_loss.data[0], total_iter)
添加TensorBoardX指令后,则将在logs/tmp文件夹下生成events开头的记录文件,然后使用TensorBoard在终端中开启Web服务。
tensorboard --logdir=logs/tmp/
TensorBoard 1.9.0 at http://pytorch:6006 (Press CTRL+C to quit)
在浏览器端输入上述指令下方的网址http://pytorch:6006,即可看到 数据的可视化效果。
2.Visdom简介
Visdom由Facebook团队开发,是一个非常灵活的可视化工具,可用于多种数据的创建、组织和共享,支持NumPy、Torch与PyTorch数据, 目的是促进远程数据的可视化,支持科学实验。
Visdom可以通过pip指令完成安装,如下:
pip3 install visdom
使用如下指令可以开启visdom服务,该服务基于Web,并默认使用8097端口。
python3 -m visdom.server
下面实现一个文本、曲线及图片的可视化示例,更多丰富的功能,可以查看Visdom的GitHub主页。
import torch
import visdom
# 创建visdom客户端,使用默认端口8097,环境为first,环境的作用是对可视化的空间进行分区
vis = visdom.Visdom(env='first')
# vis对象有text()、line()和image()等函数,其中的win参数代表了显示的窗格(pane)的名字
vis.text('first visdom', win='text1')
# 在此使用append为真来进行增加text,否则会覆盖之前的text
vis.text('hello PyTorch', win='text1', append=True)
# 绘制y=-i^2+20×i+1的曲线,opts可以进行标题、坐标轴标签等的配置
for i in range(20):
vis.line(X=torch.FloatTensor([i]), Y=torch.FloatTensor([-i**2+20*i+1]),
opts={'title': 'y=-x^2+20x+1'}, win='loss', update='append')
# 可视化一张随机图片
vis.image(torch.randn(3, 256, 256), win='random_image')
打开浏览器,输入http://localhost:8097,即可看到可视化的结果, 如下图所示。可以看到在first的环境下,有3个窗格,分别为代码中添加的可视化数据。