pytorch基础
1.1 基本数据:Tensor
Tensor,即张量,是PyTorch中的基本操作对象,可以看做是包含 单一数据类型元素的多维矩阵。从使用角度来看,Tensor与NumPy的 ndarrays非常类似,相互之间也可以自由转换,只不过Tensor还支持 GPU的加速。
本节首先介绍Tensor的基本数据类型,然后讲解创建Tensor的多种 方法,最后依次介绍Tensor在实际使用中的常见操作,如变形、组合、 排序等。
1.1.1 Tensor数据类型
Tensor在使用时可以有不同的数据类型,如表2.1所示,官方给出了 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()等,图2.1列 举了常见的Tensor创建方法。
下面从代码角度实现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.numel()或者 Tensor.nelement()函数,两者等价。
# 查看Tensor中总的元素个数
>>> a.numel() 4
>>> a.nelement() 4
1.1.3组合与分块
组合与分块是将Tensor相互叠加或者分开,是十分常用的两个功能,PyTorch提供了多种操作函数,如图2.2所示。
组合操作是指将不同的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.]])
# 以第二维进行拼接,则变成24的矩阵
>>> 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.4Tensor的索引与变形
索引操作与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类不同的变形方法,如表2.2所示。
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.], [ 0.], [ 0.], [ 4.]])
>>> d=a.reshape(4,1)
>>> d tensor([[ 1.], [ 0.], [ 0.], [ 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.5Tensor的排序与取极值
比较重要的是排序函数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形状一致。
对于类似求和、求均值、求方差、求距离等需要多个元素完成的操作,往往需要沿着某个维度进行计算,在Tensor中属于归并操作,输出形状小于输入形状。由于比较简单且与NumPy极为相似,在此就不详细展开。
1.1.6Tensor的自动广播机制与向量化
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 "<stdin>", line 1, in <module> 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.7Tensor的内存共享
为了实现高效计算,PyTorch提供了一些原地操作运算,即in-placeoperation,不经过复制,直接在原来的内存上进行计算。对于内存的共享,主要有如下3种情况,如图2.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为输出,在这一层,计算图构建方法如图2.4所示。
在图2.4中,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.3Autograd注意事项
PyTorch的Autograd机制使得其可以灵活地进行前向传播与梯度计算,在实际使用时,需要注意以下3点,如图2.5所示。
动态图特性: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.1nn.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()函数,具体可参考官方代码。
nn.Module可以自动利用Autograd机制实现反向传播,不需要自己手动实现。
3.多个Module的嵌套
在Module的搭建时,可以嵌套包含子Module,上例的Perception中调用了Linear这个类,这样的代码分布可使网络更加模块化,提升代码的复用性。在实际的应用中,PyTorch也提供了绝大多数的网络层,如全连接、卷积网络中的卷积、池化等,并自动实现前向与反向传播。在后面的章节中会对比较重要的层进行讲解。
4.nn.Module与nn.functional库
在PyTorch中,还有一个库为nn.functional,同样也提供了很多网络层与函数功能,但与nn.Module不同的是,利用nn.functional定义的网络层不可自动学习参数,还需要使用nn.Parameter封装。nn.functional的设计初衷是对于一些不需要学习参数的层,如激活层、BN(BatchNormalization)层,可以使用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中都提供了各种损失函数,通常来讲,由于损失函数不含有可学习的参数,因此这两者在功能上基本没有区别。
# 接着2.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()
对于训练过程中的学习率调整,需要注意以下两点:
·不同参数层分配不同的学习率:优化器也可以很方便地实现将不同的网络层分配成不同的学习率,即对于特殊的层单独赋予学习率,其余的保持默认的整体学习率,具体示例如下:
·学习率动态调整:对于训练过程中动态的调整学习率,可以在迭代次数超过一定值后,重新赋予optim优化器新的学习率。