Pytorch
Pytorch
💠Tensor
对tensor的操作很多,从接口的角度来划分,可以分为两类:
(1)torch.function,如torch.sum、torch.add等,
(2)tensor.function,如tensor.view、tensor.add等。
这些操作对大部分tensor都是等价的,如torch.add(x,y)与x.add(y)等价。实际使用中可以根据个人爱好选择。
如果从修改方式的角度,可以分为以下两类:
(1)不修改自身数据,如x.add(y),x的数据不变,返回一个新的tensor。
(2)修改自身数据,如x.add_(y)(运行符带下划线后缀),运算结果存在x中,x被修改。
import torch
x=torch.tensor([1,2])
y=torch.tensor([3,4])
z=x.add(y)
print(z)
print(x)
x.add_(y)
print(x)
运行结果
tensor([4, 6])
tensor([1, 2])
tensor([4, 6])
📌创建Tensor
新建tensor的方法很多,可以从列表或ndarray等类型进行构建,也可根据指定的形状构建。常见的构建tensor的方法。
函数 | 功能 |
---|---|
Tensor(*size) | 直接从参数构造一个张量,支持list、numpy数组 |
eye(row, column) | 创建指定行数、列数的二维单位tensor |
linspace(start, end, steps) | 从step到end,均匀切分成steps份 |
kogspace(start, end, steps) | 从10step,到10end,均匀切分成steps份 |
rand/randn(*size) | 生成[0, 1)均匀分布/标准正态分布数据 |
ones(*size) | 返回指定shape的张量,元素初始为1 |
zeros(*size) | 返回指定shape的张量,元素初始为0 |
ones_like(t) | 返回与t的shape相同的张量,且元素初始为1 |
zeros_like(t) | 返回与t的shape相同的张量,且元素初始为0 |
arange(start, end, step) | 在区间[start, end)上以间隔step生成一个序列张量 |
from_numpy(ndarray) | 从ndarray创建一个tensor |
下面举例说明
import torch
#根据list数据生成tensor
torch.Tensor([1,2,3,4,5,6])
#根据指定形状生成tensor
torch.Tensor(2,3)
#根据给定的tensor的形状
t=torch.Tensor([[1,2,3],[4,5,6]])
#查看tensor的形状
t.size()
#shape与size()等价方式
t.shape
#根据已有形状创建tensor
torch.Tensor(t.size())
【说明】注意torch.Tensor与torch.tensor的几点区别
①torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),torch.tensor从数据中推断数据类型。
②torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。
import torch
t1=torch.Tensor(1)
t2=torch.tensor(1)
print("t1的值{},t1的数据类型{}".format(t1,t1.type()))
print("t2的值{},t2的数据类型{}".format(t2,t2.type()))
运行结果t1的值tensor([3.5731e-20]),t1的数据类型torch.FloatTensort2的值1,t2的数据类型torch.LongTensor
根据一定规则,自动生成tensor的一些例子。
import torch
#生成一个单位矩阵
torch.eye(2,2)
#自动生成全是0的矩阵
torch.zeros(2,3)
#根据规则生成数据
torch.linspace(1,10,4)
#生成满足均匀分布随机数
torch.rand(2,3)
#生成满足标准分布随机数
torch.randn(2,3)
#返回所给数据形状相同,值全为0的张量
torch.zeros_like(torch.rand(2,3))
📌修改Tensor形状
在处理数据、构建网络层等过程中,经常需要了解Tensor的形状、修改Tensor的形状。
与修改Numpy的形状类似,修改tenor的形状也有很多类似函数,具体可参考下表。
函数 | 说明 |
---|---|
size() | 返回张量的shape属性值,与函数shape等价 |
numel(input) | 计算tensor的元素个数 |
view(*size) | 修改tensor的shape,与reshape类似,但view返回的对象与tensor共享内存,修改一个另一个同时修改。reshape将生成新的tensor,而且不要求源tensor是连续的。vier(-1)战展数组。 |
resize | 类似于view,但在size超出时会重新分配内存空间 |
item | 若tensor为单元素,则返回python的标量 |
unsqueeze | 在指定维度增加一个“1” |
squeeze | 在指定维度压缩一个“1” |
eg:
import torch
#生成一个形状为2x3的矩阵
x = torch.randn(2, 3)
#查看矩阵的形状
x.size() #结果为torch.Size([2, 3])
#查看x的维度
x.dim() #结果为2
#把x变为3x2的矩阵
x.view(3,2)
#把x展平为1维向量
y=x.view(-1)
y.shape
#添加一个维度
z=torch.unsqueeze(y,0)
#查看z的形状
z.size() #结果为torch.Size([1, 6])
#计算Z的元素个数
z.numel() #结果为6
【说明】torch.view与torch.reshpae的异同
①reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。view()只可由torch.Tensor.view()来调用。
②对于一个将要被view的Tensor,新的size必须与原来的size与stride兼容。否则,在view之前必须调用contiguous()方法。
③同样也是返回与input数据量相同,但形状不同的tensor。若满足view的条件,则不会copy,若不满足,则会copy
④如果您只想重塑张量,请使用torch.reshape。 如果您还关注内存使用情况并希望确保两个张量共享相同的数据,请使用torch.view。
📌索引操作
Tensor的索引操作与Numpy类似,一般情况下索引结果与源数据共享内存。从tensor获取元素除了可以通过索引,也可借助一些函数,常用的选择函数可参考下表。
函数 | 说明 |
---|---|
index_select(input, dim, index) | 在指定维度上选择一些行或列 |
nonzero(input) | 获取非0元素的下标 |
masked_select(input, mask) | 使用二元值进行选择 |
gather(input, dim, index) | 在指定维度上选择数据,输出的形状与index(index的类型必须是LongTensor类型的)一致 |
scatter_(input, dim, index, src) | 为gather的反操作,根据指定索引补充数据 |
eg:
import torch
#设置一个随机种子
torch.manual_seed(100)
#生成一个形状为2x3的矩阵
x = torch.randn(2, 3)
#根据索引获取第1行,所有数据
x[0,:]
#获取最后一列数据
x[:,-1]
#生成是否大于0的Byter张量
mask=x>0
#获取大于0的值
torch.masked_select(x,mask)
#获取非0下标,即行,列索引
torch.nonzero(mask)
#获取指定索引对应的值,输出根据以下规则得到
#out[i][j] = input[index[i][j]][j] # if dim == 0
#out[i][j] = input[i][index[i][j]] # if dim == 1
index=torch.LongTensor([[0,1,1]])
torch.gather(x,0,index)
index=torch.LongTensor([[0,1,1],[1,1,1]])
a=torch.gather(x,1,index)
#把a的值返回到一个2x3的0矩阵中
z=torch.zeros(2,3)
z.scatter_(1,index,a)
📌广播机制
Pytorch也支持广播规则,以下通过几个示例进行说明。
import torch
import numpy as np
A = np.arange(0, 40,10).reshape(4, 1)
B = np.arange(0, 3)
#把ndarray转换为Tensor
A1=torch.from_numpy(A) #形状为4x1
B1=torch.from_numpy(B) #形状为3
#Tensor自动实现广播
C=A1+B1
#我们可以根据广播机制,手工进行配置
#根据规则1,B1需要向A1看齐,把B变为(1,3)
B2=B1.unsqueeze(0) #B2的形状为1x3
#使用expand函数重复数组,分别的4x3的矩阵
A2=A1.expand(4,3)
B3=B2.expand(4,3)
#然后进行相加,C1与C结果一致
C1=A2+B3
📌矩阵操作
器学习和深度学习中存在大量的矩阵运算,用的比较多的有两种,一种是逐元素乘法,另外一种是点积乘法。Pytorch中常用的矩阵函数可参考下表。
函数 | 说明 |
---|---|
dot(t1, t2) | 计算张量(1D)的内积或点积 |
mm(mat1, mat2) / bmm(batch1, batch2) | 计算矩阵乘法/含batch的3D矩阵乘法 |
mv(t1, v1) | 计算矩阵与向量乘法 |
t | 转置 |
svd(t) | 计算t的SVD分解 |
【说明】
①torch的dot与Numpy的dot有点不同,torch中dot对两个为1D张量进行点积运算,Numpy中的dot无此限制。
②mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。
③转置运算会导致存储空间不连续,需要调用contiguous方法转为连续。
eg:
import torch
a=torch.tensor([2, 3])
b=torch.tensor([3, 4])
torch.dot(a,b) #运行结果为18
x=torch.randint(10,(2,3))
y=torch.randint(6,(3,4))
torch.mm(x,y)
x=torch.randint(10,(2,2,3))
y=torch.randint(6,(2,3,4))
torch.bmm(x,y)
💠Tensor与Autograd
在神经网络中,一个重要内容就是进行参数学习,而参数学习离不开求导,Pytorch是如何进行求导的呢?
现在大部分深度学习架构都有自动求导的功能,Pytorch也不列外,torch.autograd包就是用来自动求导的。autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为autograd上的两个核心类,他们相互连接并生成一个有向非循环图。接下来我们先简单介绍tensor如何实现自动求导,然后介绍计算图,最后用代码实现这些功能。
📌自动求导要点
autograd包为对tensor进行自动求导,为实现对tensor自动求导,需考虑如下事项:
(1)创建叶子节点(leaf node)的tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。requires_grad参数缺省值为False,如果要对其求导需设置为True,与之有依赖关系的节点自动变为True。
(2)可利用requires_grad_()方法修改tensor的requires_grad属性。可以调用.detach()或with torch.no_grad():将不再计算张量的梯度,跟踪张量的历史记录。这点在评估模型、测试模型阶段常常使用。
(3)通过运算创建的tensor(即非叶子节点),会自动被赋于grad_fn属性。该属性表示梯度函数。叶子节点的grad_fn为None。
(4)最后得到的tensor执行backward()函数,此时自动计算各变在量的梯度,并将累加结果保存grad属性中。计算完成后,非叶子节点的梯度自动释放。
(5)backward()函数接受参数,该参数应和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的tensor为标量(即一个数字),backward中参数可省略。
(6)反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加的。
(7)非叶子节点的梯度backward调用后即被清空。
(8)可以通过用torch.no_grad()包裹代码块来阻止autograd去跟踪那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使用。
整个过程中,Pytorch采用计算图的形式进行组织,该计算图为动态图,它的计算图在每次前向传播时,将重新构建。其他深度学习架构,如TensorFlow、Keras一般为静态图。接下来我们介绍计算图,用图的形式来描述就更直观了,该计算图为有向无环图(DAG)。
📌标量反向传播
假设x、w、b都是标量,z=wx+b,对标量z调用backward(),我们无需对backward()传入参数。以下是实现自动求导的主要步骤:
(1)定义叶子节点及算子节点
import torch
#定义输入张量x
x=torch.Tensor([2])
#初始化权重参数W,偏移量b、并设置require_grad属性为True,为自动求导
w=torch.randn(1,requires_grad=True)
b=torch.randn(1,requires_grad=True)
#实现前向传播
y=torch.mul(w,x) #等价于w*x
z=torch.add(y,b) #等价于y+b
#查看x,w,b页子节点的requite_grad属性
print("x,w,b的require_grad属性分别为:{},{},{}".format(x.requires_grad,w.requires_grad,b.requires_grad))
运行结果
x,w,b的require_grad属性分别为:False,True,True
(2)查看叶子节点、非叶子节点的其他属性
#查看非叶子节点的requres_grad属性,
print("y,z的requires_grad属性分别为:{},{}".format(y.requires_grad,z.requires_grad))
#因与w,b有依赖关系,故y,z的requires_grad属性也是:True,True
#查看各节点是否为叶子节点
print("x,w,b,y,z的是否为叶子节点:{},{},{},{},{}".format(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf))
#x,w,b,y,z的是否为叶子节点:True,True,True,False,False
#查看叶子节点的grad_fn属性
print("x,w,b的grad_fn属性:{},{},{}".format(x.grad_fn,w.grad_fn,b.grad_fn))
#因x,w,b为用户创建的,为通过其他张量计算得到,故x,w,b的grad_fn属性:None,None,None
#查看非叶子节点的grad_fn属性
print("y,z的是否为叶子节点:{},{}".format(y.grad_fn,z.grad_fn))
#y,z的是否为叶子节点:,
(3)自动求导,实现梯度方向传播,即梯度的反向传播。
#基于z张量进行梯度反向传播,执行backward之后计算图会自动清空,
z.backward()
#如果需要多次使用backward,需要修改参数retain_graph为True,此时梯度是累加的
#z.backward(retain_graph=True)
#查看叶子节点的梯度,x是叶子节点但它无需求导,故其梯度为None
print("参数w,b的梯度分别为:{},{},{}".format(w.grad,b.grad,x.grad))
#参数w,b的梯度分别为:tensor([2.]),tensor([1.]),None
#非叶子节点的梯度,执行backward之后,会自动清空
print("非叶子节点y,z的梯度分别为:{},{}".format(y.grad,z.grad))
#非叶子节点y,z的梯度分别为:None,None
📌非标量反向传播
当目标张量为标量时,调用backward()无需传入参数。目标张量一般是标量,如我们经常使用的损失值Loss,一般都是一个标量。但也有非标量的情况,后面我们介绍的Deep Dream的目标值就是一个含多个元素的张量。如何对非标量进行反向传播呢?Pytorch有个简单的规定,不让张量(tensor)对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用backward(),需要传入一个gradient参数,该参数也是张量,而且需要与调用backward()的张量形状相同。为什么要传入一个张量gradient?
传入这个参数就是为了把张量对张量求导转换为标量对张量求导。这有点拗口,我们举一个例子来说,假设目标值为loss=(y_1,y_2,…,y_m)传入的参数为v=(v_1,v_2,…,v_m),那么就可把对loss的求导,转换为对loss*vT标量的求导。即把原来∂loss/∂x得到雅可比矩阵(Jacobian)乘以张量vT,便可得到我们需要的梯度矩阵。
backward函数的格式为:
backward(gradient=None, retain_graph=None, create_graph=False)
上面说的可能有点抽象,下面我们通过一个实例进行说明。
(1)定义叶子叶子节点及计算节点
import torch
#定义叶子节点张量x,形状为1x2
x= torch.tensor([[2, 3]], dtype=torch.float, requires_grad=True)
#初始化Jacobian矩阵
J= torch.zeros(2 ,2)
#初始化目标张量,形状为1x2
y = torch.zeros(1, 2)
#定义y与x之间的映射关系:
#y1=x1**2+3*x2,y2=x2**2+2*x1
y[0, 0] = x[0, 0] ** 2 + 3 * x[0 ,1]
y[0, 1] = x[0, 1] ** 2 + 2 * x[0, 0]
(2)手工计算y对x的梯度
我们先手工计算一下y对x的梯度,为了验证Pytorch的backward的结果是否正确。
(3)调用backward获取y对x的梯度
y.backward(torch.Tensor([[1, 1]]))
print(x.grad)
#结果为tensor([[6., 9.]])
这个结果与我们手工运算的不符,显然这个结果是错误的,错在哪里呢?这个结果的计算过程是:
由此,错在v的取值错误,通过这种方式得的到并不是y对x的梯度。这里我们可以分成两步的计算。首先让v=(1,0)得到y_1对x的梯度,然后使v=(0,1),得到y_2对x的梯度。这里因需要重复使用backward(),需要使参数retain_graph=True,具体代码如下: