Tensor,即张量,是PyTorch中的基本操作对象,可以看做是包含单一数据类型元素的多维矩阵。从使用角度来看,Tensor与NumPy的ndarrays非常类似,相互之间也可以自由转换,只不过Tensor还支持GPU的加速。
Tensor共有7种CPU Tensor 类型和8种GPU Tensor类型,pytorch默认的数据类型是torch.FloatTensor, 即torch.Tensor等同于torch.FloatTensor。Tensor数据类型如下:
PyTorch可以通过set_default_tensor_type函数设置默认使用的Tensor类型。
torch.set_default_tensor_type('torch.DoubleTensor')
上述代码将默认的32位浮点数据类型改为了64位浮点。
可以通过type(new_type)、type_as()、int()等多种方式进行操作,尤其是type_as()函数,我们想保持Tensor之间的类型一致,只需要使用type_as()即可,并不需要明确具体是哪种类型。举例如下:
# 创建新Tensor,默认类型为torch.FloatTensor
import torch
a = torch.Tensor(2,2)
a
'''
输出:tensor([[0.0000e+00, 0.0000e+00],
[1.2784e+32, 4.5916e-41]])
‘’‘
# 使用int()、float()、double()等直接进行数据类型转换
b = a.double()
c = a.int()
b
c
'''
输出b:tensor([[0.0000e+00, 0.0000e+00],
[1.2784e+32, 4.5916e-41]], dtype=torch.float64)
输出c:tensor([[ 0, 0],
[-2147483648, 0]], dtype=torch.int32)
'''
# 使用type()函数
c = a.type(torch.DoubleTensor)
c
'''
tensor([[0.0000e+00, 0.0000e+00],
[1.2784e+32, 4.5916e-41]], dtype=torch.float64)
'''
# 使用type_as()函数
d = a.type_as(c)
d
'''
tensor([[0.0000e+00, 0.0000e+00],
[1.2784e+32, 4.5916e-41]], dtype=torch.float64)
'''
**小结:**Tensor共有7种CPU Tensor 类型和8种GPU Tensor类型,具体类型可以在1.1种的表中查到,这些数据类型可以通过int()、float()、double()、type()、type_as()等进行相互转化。
Tensor有多种创建方法,下图列举了常见的Tensor创建方法:
下面用代码演示一遍:
# 最基础的Tensor()函数创建方法,参数为Tensor的每一维大小
a = torch.Tensor(2,2)
a
‘’‘
tensor([[0.0000e+00, 0.0000e+00],
[1.2784e+32, 4.5916e-41]])
’‘’
b = torch.DoubleTensor(2,2)
b
‘’‘
tensor([[0.0000e+00, 0.0000e+00],
[6.1156e-96, 6.8212e-43]], 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.6822, -0.6350],
[ 0.1792, 0.6035]])
’‘’
# 使用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,10,3)
i
‘’‘
tensor([ 1.0000, 5.5000, 10.0000])
’‘’
# 使用randperm(num)函数,生成长度为num的随机排列向量
j = torch.randperm(4)
j
‘’‘
tensor([2, 1, 0, 3])
’‘’
拓展:查看Tensor中的元素总个数,可使用Tensor.numel() 或者 Tensor.nelement()函数,两者等价。
a = torch.Tensor(2,2)
a.numel()
4
a.nelement()
4
索引操作与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,符合条件的True,否则为False。
a > 0
'''
tensor([[False, True],
[ True, True]])
'''
# 选择符合条件的元素并返回,等价于torch.masked_select(a, a>0)
a[a>0]
'''
tensor([1., 2., 3.])
'''
# 选择非0元素的坐标,并返回
'''
tensor([[0, 1],
[1, 0],
[1, 1]])
'''
# torch.where(condition, x, y),满足condition的位置输出x位置对应的值,否则输出y位置对应的值。
# torch.full_like(a,1)的意思是在a矩阵所有的位置的元素用1填充。
torch.where(a>1, torch.full_like(a, 1), a)
'''
tensor([[0., 1.],
[1., 1.]])
'''
x = torch.randn(3,2)
x
'''
tensor([[ 0.2730, -0.7299],
[ 1.8523, 0.4764],
[-0.9825, -0.9630]])
'''
# x 和 y的大小相同
y = torch.ones(3,2)
y
'''
tensor([[1., 1.],
[1., 1.],
[1., 1.]])
'''
E = torch.where(x>0,x,y)
E
'''
tensor([[0.2730, 1.0000],
[1.8523, 0.4764],
[1.0000, 1.0000]])
'''
# 对Tensor元素进行限制可以使用clamp()函数,示例如下,限制最小值为1,最大值为2
b = a.clamp(1,2)
b
'''
tensor([[1., 1.],
[2., 2.]])
'''
变形操作则是指改变Tensor的维度,以适应在深度学习的计算中,数据维度经常变换的需求。在pytorch中主要有4类不同的变形方法,如下表所示:
view()、resize()和reshape()函数可以在不改变Tensor数据的前提下任意改变Tensor的形状,必须保证调整前后的元素总数相同,并且调整前后共享内存,三者的作用基本相同。具体操作请看下面截图:
如果想要直接改变Tensor的尺寸,可以使用resize_()的原地操作函数。在resize_()函数中,如果超过了原Tensor的大小则重新分配内存,多出部分置0,如果小于原Tensor大小则剩余的部分仍然会隐藏保留。(注意:多出部分置为0意思是相对于最开始的tensor。)
transpose()函数可以将指定的两个维度的元素进行转置,而permute()函数则可以按照给定的维度进行维度变换。在没有Batch这一维度时,pytorch默认的维度是[C,H,W],可以通过size()函数查看维度的变化。
先看transpose()函数:
先看permute()函数:
a.permute(1,0,2)d的意思是将第一个维度与第二个维度变换一下位置。即shape由(3,2,2)变为(2,3,3)。
在实际的应用中,经常需要增加或减少Tensor的维度,尤其是维度为1的情况,这时候可以使用squeeze()与unsqueeze()函数,前者用于去除size为1的维度,而后者则是将指定的维度的size变为1。
有时需要采用复制元素的形式来扩展Tensor的维度,这时expand就派上用场了。expand()函数将size为1的维度复制扩展为指定大小,也可以使用expand_as()函数指定为示例Tensor的维度。
注意:在进行Tensor操作时,有些操作如transpose()、permute()等可能会把Tensor在内存中变得不连续,而有些操作如view()等是需要Tensor内存连续的,这种情况下需要使用contiguous()操作先将内存变为连续的。在PyTorch v0.4版本中增加了reshape()操作,可以看做是Tensor.contiguous().view()。
比较重要的是排序函数sort(),选择沿着指定维度进行排序,返回排序后的Tensor及对应的索引位置。max()与min()函数则是沿着指定维度选择最大与最小元素,返回该元素及对应的索引位置。
由上可知,第0维度就是按照行排序,对每一列上的数进行升序或者降序排列;而第1维度是按列排序,对每一行上的数进行排序,另外,a.sort(0,True)[1] 可以返回每一列的index变化。
对于Tensor的单元素数学运算,如abs()、sqrt()、log()、pow()和三角函数等,都是逐元素操作(element-wise),输出的Tensor形状与原始Tensor形状一致。
PyTorch在0.2版本以后,推出了自动广播语义,即不同形状的Tensor进行计算时,可自动扩展到较大的相同形状,再进行计算。广播机制的前提是任一个Tensor至少有一个维度,且从尾部遍历Tensor维度时,两者维度必须相等,其中一个要么是1要么不存在。
向量化操作是指可以在同一时间进行批量地并行计算,例如矩阵运算,以达到更好的计算效率的一种方式。在实际使用时,应尽量使用向量化直接对Tensor操作,避免低效率的for循环对元素逐个操作,尤其是在训练网络模型时,如果有大量的for循环,会极大地影响训练的速度。
为了实现高效计算,PyTorch提供了一些原地操作运算,即in-placeoperation,不经过复制,直接在原来的内存上进行计算。对于内存的共享,主要有如下3种情况:
下面展开来分析:
1、通过Tensor初始化Tensor
直接通过Tensor来初始化另一个Tensor,或者通过Tensor的组合、分块、索引、变形操作来初始化另一个Tensor,则这两个Tensor共享内存。
2、原地操作符
PyTorch对于一些操作通过加后缀“”实现了原地操作,如add()和resize_()等,这种操作只要被执行,本身的Tensor则会被改变。
3、Tensor与Numpy转换
Tensor与NumPy可以高效地进行转换,并且转换前后的变量共享内存。在进行PyTorch不支持的操作时,甚至可以曲线救国,将Tensor转换为NumPy类型,操作后再转为Tensor。
基本数据Tensor可以保证完成前向传播,想要完成神经网络的训练,接下来还需要进行反向传播与梯度更新,而PyTorch提供了自动求导机制autograd,将前向传播的计算记录成计算图,自动完成求导。
自动求导机制记录了Tensor的操作,以便自动求导与反向传播。可以通过requires_grad参数来创建支持自动求导机制的Tensor。
require_grad参数表示是否需要对该Tensor进行求导,默认为False;设置为True则需要求导,并且依赖于该Tensor的之后的所有节点都需要求导。
由上图可以看出,requires_grad_()函数会改变Tensor的requires_grad属性并返回Tensor,修改requires_grad的操作是原位操作(in place)。其默认参数为requires_grad=True。
requires_grad=True时,自动求导会记录对Tensor的操作,requires_grad_()的主要用途是告诉自动求导开始记录对Tensor的操作。
.grad:该Tensor对应的梯度,类型为Tensor,并与Tensor同维度。
·grad_fn:指向function对象,即该Tensor经过了什么样的操作,用作反向传播的梯度计算,如果该Tensor由用户自己创建,则该grad_fn为None。
a = torch.randn(2,2,requires_grad = True)
b = torch.randn(2,2,requires_grad = True)
c = a + b
c.requires_grad
'''
Truie
'''
# a与b是自己创建的,grad_fn为None,而c的grad_fn则是一个Add函数操作
a.grad_fn, b.grad_fn, c.grad_fn
'''
(None, None, )
'''
d = c.detach()
d.requires_grad
'''
False
'''
Tensor.detach()函数生成的数据默认requires_grad为False。detach()就是截断反向传播的梯度流。detach()函数会返回一个新的Tensor对象b,并且新Tensor是与当前的计算图分离的,其requires_grad属性为False,反向传播时不会计算其梯度。b与a共享数据的存储空间,二者指向同一块内存。
注意:共享内存空间只是共享的数据部分,a.grad与b.grad是不同的。
计算图是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
x = torch.tensor([[1.,2.],[3.,4.]])
w = torch.tensor(2.,requires_grad = True)
b = torch.tensor(3.,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
l = z.mean()
y.is_leaf, z.is_leaf
‘’‘
(False, False)
’‘’
# grad_fn记录生成该变量经过了什么操作,如y是Mul,z是Add
y.grad_fn, z.grad_fn
‘’‘
(<MulBackward0 at 0x1e32ffe35f8>, <AddBackward0 at 0x1e32ffe3358>)
’‘’
l.backward(retain_graph=True)
w.grad,b.grad
‘’‘
(tensor(2.5000), tensor(1.))
’‘’
下图是上述代码的计算图:
注意:求的导数是偏导,比如对w求梯度时,就把其他参数当成已知量,仅仅把w当作变量,这是大学本科高等数学的知识,忘记的可以去补一下,这里给出w和b的偏导公式:
对于l.backward(retain_graph=True),retain_graph = True的意思是计算图中的中间变量在计算完后就会被释放。但是在平时的使用中这个参数默认都为False从而提高效率,和creat_graph的值一样。这个不常用。如果为False时,第一次backward之后的梯度会被保存下来,第二次在backward会在原有梯度的基础上累加,这是不被看好的,因此需要对梯度清0,这就用到了with torch.no_grad()。
PyTorch的Autograd机制使得其可以灵活地进行前向传播与梯度计算,在实际使用时,需要注意以下3点,如下图所示:
动态图特性: PyTorch建立的计算图是动态的,这也是PyTorch的一大特点。动态图是指程序运行时,每次前向传播时从头开始构建计算图,这样不同的前向传播就可以有不同的计算图,也可以在前向时插入各种Python的控制语句,不需要事先把所有的图都构建出来,并且可以很方便地查看中间过程变量。
backward()函数还有一个需要传入的参数grad_variabels,其代表了根节点的导数,也可以看做根节点各部分的权重系数。因为PyTorch不允许Tensor对Tensor求导,求导时都是标量对于Tensor进行求导,因此,如果根节点是向量,则应配以对应大小的权重,并求和得到标量,再反传。如果根节点的值是标量,则该参数可以省略,默认为1。
当有多个输出需要同时进行梯度反传时,需要将retain_graph设置为True,从而保证在计算多个输出的梯度时互不影响。