第二章 PyTorch基础
2.1 为何选择PyTorch
PyTorch是一个建立在Torch库之上的Python包,旨在加速深度学习应用。它提供一种类似Numpy的抽象方法来表征张量(或多维数组),可以利用GPU来加速训练。由于PyTorch采用了动态计算图(Dynamic Computational Graph)结构,且基于tape的Autograd系统的深度神经网络。 其他很多框架,比如TensorFlow(TensorFlow2.0也加入了动态网络的支持)、Caffe、CNTK、Theano等,采用静态计算图。使用PyTorch,通过一种称为Reverse-mode auto-differentiation(反向模式自动微分)的技术,可以零延迟或零成本地任意改变你的网络的行为。Torch是PyTorch中的一个重要包,它包含了多维张量的数据结构以及基 于其上的多种数学操作。
PyTorch由4个主要的包组成:
- torch:类似于Numpy的通用数组库,可将张量类型转换为 torch.cuda.TensorFloat,并在GPU上进行计算。
- torch.autograd:用于构建计算图形并自动获取梯度的包。
- torch.nn:具有共享层和损失函数的神经网络库。
- torch.optim:具有通用优化算法(如SGD、Adam等)的优化包。
2.2 安装配置
参考:win10+PyTorch-GPU安装
2.3 Jupyter Notebook环境配置
参考:Jupyter Notebook配置
2.4 Numpy和Tensor
PyTorch 的Tensor,它可以是零维(又称为标量或一个数)、一维、二维及多维的数组。Tensor自称为神经网络界的Numpy,它与Numpy相似,二者可以共享内存,且之间的转换非常方便和高效。不过它们也有不同之处,最大的区别就是Numpy会把ndarray放在CPU中进行加速运算,而由Torch产生的Tensor会 放在GPU中进行加速运算(假设当前环境有GPU)。
2.4.1 Tensor概述
对Tensor的操作很多,从接口的角度来划分,可以分为两类:
- torch.function,如torch.sum、torch.add等;
- tensor.function,如tensor.view、tensor.add等。
这些操作对大部分Tensor都是等价的,如torch.add(x,y)与x.add(y)等价。 在实际使用时,可以根据个人爱好选择。
如果从修改方式的角度来划分,可以分为以下两类:
- 不修改自身数据,如x.add(y),x的数据不变,返回一个新的 Tensor。
- 修改自身数据,如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])
2.4.2 创建Tensor
创建Tensor的方法有很多,可以从列表或ndarray等类型进行构建,也可 根据指定的形状构建。
import torch
torch.Tensor([1,2,3,4,5,6]) # 根据list数据生成tensor
torch.Tensor(2,3) # 根据指定形状生成tensor
t=torch.Tensor([[1,2,3],[4,5,6]]) # 根据给定的tensor的形状
t.size() # 查看tensor的形状
t.shape # shape与size()等价方式
torch.Tensor(t.size()) # 根据已有形状创建tensor
注意:torch.Tensor与torch.tensor的几点区别:
- torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),而torch.tensor 是从数据中推断数据类型。
- torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。
#torch.tensor与torch.Tentor的区别
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.2193e+37]),t1的数据类型torch.FloatTensor
t2的值1,t2的数据类型torch.LongTensor
2.4.3 修改Tensor形状
在处理数据、构建网络层等过程中,经常需要了解Tensor的形状、修改 Tensor的形状。与修改Numpy的形状类似,修改Tenor的形状也有很多类似函数。
import torch
#生成一个形状为2x3的矩阵
x = torch.randn(2, 3)
print(x)
#查看矩阵的形状
print(x.size()) #结果为torch.Size([2, 3])
#查看x的维度
print(x.dim()) #结果为2
#把x变为3x2的矩阵
x.view(3,2)
print(x.view(3,2))
#把x展平为1维向量
y=x.view(-1)
print(y)
print(y.shape)
#添加一个维度
z=torch.unsqueeze(y,0)
print(z)
#查看z的形状
print(z.size()) #结果为torch.Size([1, 6])
#计算Z的元素个数
z.numel() #结果为6
tensor([[ 1.3937, 1.5284, -2.1479],
[-1.1225, 0.6751, 1.5055]])
torch.Size([2, 3])
2
tensor([[ 1.3937, 1.5284],
[-2.1479, -1.1225],
[ 0.6751, 1.5055]])
tensor([ 1.3937, 1.5284, -2.1479, -1.1225, 0.6751, 1.5055])
torch.Size([6])
tensor([[ 1.3937, 1.5284, -2.1479, -1.1225, 0.6751, 1.5055]])
torch.Size([1, 6])
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。
2.4.4 索引操作
Tensor的索引操作与Numpy类似,一般情况下索引结果与源数据共享内 存。从Tensor获取元素除了可以通过索引,也可以借助一些函数。
2.4.5 广播机制
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
print(C1)
tensor([[ 0, 1, 2],
[10, 11, 12],
[20, 21, 22],
[30, 31, 32]], dtype=torch.int32)
2.4.6 逐元素操作
(这些操作均会创建新的Tensor,如果需要就地操作,可以使用这些方法 的下划线版本,例如abs_。)
2.4.7 归并操作
归并操作就是对输入进行归并或合计等操作,这类操作的输入输出形状一般并不相同,而且往往是输入大于输出形状。归并操作可以对整个Tensor,也可以沿着某个维度进行归并。
归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,缺省情况是False,即不保留。(dim=0沿着y方向)
import torch
#生成一个含6个数的向量
a=torch.linspace(0,10,6)
#使用view方法,把a变为2x3矩阵
a=a.view((2,3))
print(a)
#沿y轴方向累加,即dim=0
b=a.sum(dim=0) #b的形状为[3]
print(b)
#沿y轴方向累加,即dim=0,并保留含1的维度
b=a.sum(dim=0,keepdim=True) #b的形状为[1,3]
print(b)
tensor([[ 0., 2., 4.],
[ 6., 8., 10.]])
tensor([ 6., 10., 14.])
tensor([[ 6., 10., 14.]])
2.4.8 比较操作
比较操作一般是进行逐元素比较,有些是按指定方向比较。
2.4.9 矩阵操作
机器学习和深度学习中存在大量的矩阵运算,常用的算法有两种:一种是逐元素乘法,另外一种是点积乘法。
说明:
- Torch的dot与Numpy的dot有点不同,Torch中的dot是对两个为1D张 量进行点积运算,Numpy中的dot无此限制。
- mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。
- 转置运算会导致存储空间不连续,需要调用contiguous方法转为连 续。
import torch
a=torch.tensor([2, 3])
print(a)
b=torch.tensor([3, 4])
print(b)
print(torch.dot(a,b)) #运行结果为18
x=torch.randint(10,(2,3))
y=torch.randint(6,(3,4))
print(x)
print(y)
print(torch.mm(x,y))
x=torch.randint(10,(2,2,3))
print(x)
y=torch.randint(6,(2,3,4))
print(y)
print(torch.bmm(x,y))
tensor([2, 3])
tensor([3, 4])
tensor(18)
tensor([[7, 2, 7],
[4, 6, 6]])
tensor([[1, 3, 1, 3],
[2, 3, 2, 1],
[4, 0, 4, 1]])
tensor([[39, 27, 39, 30],
[40, 30, 40, 24]])
tensor([[[1, 3, 3],
[5, 2, 5]],
[[8, 8, 4],
[9, 4, 7]]])
tensor([[[4, 1, 3, 3],
[3, 1, 2, 3],
[0, 5, 0, 3]],
[[5, 4, 0, 2],
[2, 3, 5, 2],
[0, 0, 1, 0]]])
tensor([[[13, 19, 9, 21],
[26, 32, 19, 36]],
[[56, 56, 44, 32],
[53, 48, 27, 26]]])
2.4.10 PyTorch与Numpy比较
2.5 Tensor与Autograd
在神经网络中,一个重要内容就是进行参数学习,而参数学习离不开求导,那么PyTorch是如何进行求导的呢? 现在大部分深度学习架构都有自动求导的功能,PyTorch也不例外, torch.autograd包就是用来自动求导的。Autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为Autograd的两个核心类, 它们相互连接并生成一个有向非循环图。
2.5.1 自动求导要点
为实现对Tensor自动求导,需考虑如下事项:
- 创建叶子节点(Leaf Node)的Tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。 requires_grad参数的缺省值为False,如果要对其求导需设置为True,然后与之有依赖关系的节点会自动变为True。
- 可利用requires_grad_()方法修改Tensor的requires_grad属性。可以调用.detach()或with torch.no_grad():,将不再计算张量的梯度,跟踪张量的历史记录。这点在评估模型、测试模型阶段中常常用到。
- 通过运算创建的Tensor(即非叶子节点),会自动被赋予grad_fn属性。该属性表示梯度函数。叶子节点的grad_fn为None。
- 最后得到的Tensor执行backward()函数,此时自动计算各变量的梯度,并将累加结果保存到grad属性中。计算完成后,非叶子节点的梯度自动 释放。
- backward()函数接收参数,该参数应和调用backward()函数的Tensor 的维度相同,或者是可broadcast的维度。如果求导的Tensor为标量(即一个数字),则backward中的参数可省略。
- 反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加 的。
- 非叶子节点的梯度backward调用后即被清空。
- 可以通过用torch.no_grad()包裹代码块的形式来阻止autograd去跟踪那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使用。
在整个过程中,PyTorch采用计算图的形式进行组织,该计算图为动态图,且在每次前向传播时,将重新构建。其他深度学习架构,如 TensorFlow、Keras一般为静态图。接下来我们介绍计算图,用图的形式来描述就更直观了,该计算图为有向无环图(DAG)。
2.5.2 计算图
计算图是一种有向无环图像,用图形方式来表示算子与变量之间的关系,直观高效。如图2-9所示,圆形表示变量,矩阵表示算子。如表达式: z=wx+b,可写成两个表示式:y=wx,则z=y+b,其中x、w、b为变量,是用户创建的变量,不依赖于其他变量,故又称为叶子节点。为计算各叶子节点的梯度,需要把对应的张量参数requires_grad属性设置为True,这样就可自动跟踪其历史记录。y、z是计算得到的变量,非叶子节点,z为根节点。mul 和add是算子(或操作或函数)。由这些变量及算子,就构成一个完整的计算过程(或前向传播过程)。
我们的目标是更新各叶子节点的梯度,根据复合函数导数的链式法则, 不难算出各叶子节点的梯度。
PyTorch调用backward()方法,将自动计算各节点的梯度,这是一个反向传播过程,这个过程可用图2-9表示。且在反向传播过程中,autograd沿着图 2-10,从当前根节点z反向溯源,利用导数链式法则,计算所有叶子节点的梯度,其梯度值将累加到grad属性中。对非叶子节点的计算操作(或 Function)记录在grad_fn属性中,叶子节点的grad_fn值为None。
2.5.3 标量反向传播
假设x、w、b都是标量,z=wx+b,对标量z调用backward()方法,我们无须对backward()传入参数。以下是实现自动求导的主要步骤:
1)定义叶子节点及算子节点:
import torch
import numpy as np
#定义输入张量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的是否为叶子节点:<MulBackward0 object at 0x7f923e85dda0>,<AddBackward0 object at 0x7f923e85d9b0>
y,z的requires_grad属性分别为:True,True
x,w,b,y,z的是否为叶子节点:True,True,True,False,False
x,w,b的grad_fn属性:None,None,None
y,z的是否为叶子节点:<MulBackward0 object at 0x0000018910B29A60>,<AddBackward0 object at 0x0000018910B290D0>
3)自动求导,实现梯度方向传播,即梯度的反向传播。
#基于z张量进行梯度反向传播,执行backward之后计算图会自动清空,
#如果需要多次使用backward,需要修改参数retain_graph为True,此时梯度是累加的
#z.backward(retain_graph=True)
z.backward()
#查看叶子节点的梯度,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
2.5.4 非标量反向传播
当目标张量为标量时,可以调用backward()方法且无须传入参数。目标张量一般都是标量,如我们经常使用的损失值Loss,一般都是一个标量。但也有非标量的情况,后面将介绍的Deep Dream的目标值就是一个含多个元素的张量。那如何对非标量进行反向传播呢?PyTorch有个简单的规定,不让张量(Tensor)对张量求导,只允许标量对张量求导, 因此,如果目标张量对一个非标量调用backward(),则需要传入一个gradient 参数,该参数也是张量,而且需要与调用backward()的张量形状相同。那么为什么要传入一个张量gradient呢?传入这个参数就是为了把张量对张量的求导转换为标量对张量的求导。
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的梯度。
3)调用backward来获取y对x的梯度。
错误解法:
y.backward(torch.Tensor([[1, 1]]))
print(x.grad)
tensor([[6., 9.]])
这个结果与我们手工运算的不符,显然这个结果是错误的,那错在哪里呢?这个结果的计算过程是:
由此可见,错在v的取值,通过这种方式得到的并不是y对x的梯度。这里我们可以分成两步计算。首先让v=(1,0)得到y1对x的梯度,然后使v= (0,1),得到y2对x的梯度。这里因需要重复使用backward(),需要使参数 retain_graph=True,具体代码如下:
#生成y1对x的梯度
y.backward(torch.Tensor([[1, 0]]),retain_graph=True)
J[0]=x.grad
#梯度是累加的,故需要对x的梯度清零
x.grad = torch.zeros_like(x.grad)
#生成y2对x的梯度
y.backward(torch.Tensor([[0, 1]]))
J[1]=x.grad
#显示jacobian矩阵的值
print(J)
tensor([[4., 3.],
[2., 6.]])
2.6 使用Numpy实现机器学习
首先,我们用最原始的Numpy实现有关回归的一个机器学习任务,不用 PyTorch中的包或类。这种方法代码可能多一点,但每一步都是透明的,有 利于理解每步的工作原理。主要步骤包括:
- 首先,给出一个数组x,然后基于表达式y=3x 2+2,加上一些噪音数据 到达另一组数据y。
- 然后,构建一个机器学习模型,学习表达式y=wx 2+b的两个参数w、b。 利用数组x,y的数据为训练数据。
- 最后,采用梯度梯度下降法,通过多次迭代,学习到w、b的值。
以下为具体步骤:
1)导入需要的库。
import numpy as np
%matplotlib inline
from matplotlib import pyplot as plt
2)生成输入数据x及目标数据y。
设置随机数种子,生成同一个份数据,以便用多种方法进行比较。
np.random.seed(100)
x = np.linspace(-1, 1, 100).reshape(100,1)
y = 3*np.power(x, 2) +2+ 0.2*np.random.rand(x.size).reshape(100,1)
3)查看x、y数据分布情况。
# 画图
plt.scatter(x, y)
plt.show()
4)初始化权重参数。
# 随机初始化参数
w1 = np.random.rand(1,1)
b1 = np.random.rand(1,1)
5)训练模型。
定义损失函数,假设批量大小为800:
lr =0.001 # 学习率
for i in range(800):
# 前向传播
y_pred = np.power(x,2)*w1 + b1
# 定义损失函数
loss = 0.5 * (y_pred - y) ** 2
loss = loss.sum()
#计算梯度
grad_w=np.sum((y_pred - y)*np.power(x,2))
grad_b=np.sum((y_pred - y))
#使用梯度下降法,是loss最小
w1 -= lr * grad_w
b1 -= lr * grad_b
6)可视化结果。
plt.plot(x, y_pred,'r-',label='predict')
plt.scatter(x, y,color='blue',marker='o',label='true') # true data
plt.xlim(-1,1)
plt.ylim(2,6)
plt.legend()
plt.show()
print(w1,b1)
[[2.98927619]] [[2.09818307]]
从结果看来,学习效果还是比较理想的。
2.7 使用Tensor及Autograd实现机器学习
1)导入需要的库。
import torch as t
%matplotlib inline
from matplotlib import pyplot as plt
2)生成训练数据,并可视化数据分布情况。
t.manual_seed(100)
dtype = t.float
#生成x坐标数据,x为tenor,形状为100x1
x = t.unsqueeze(torch.linspace(-1, 1, 100), dim=1)
#生成y坐标数据,y为tenor,形状为100x1,另加上一些噪音
y = 3*x.pow(2) +2+ 0.2*torch.rand(x.size())
# 画图,把tensor数据转换为numpy数据
plt.scatter(x.numpy(), y.numpy())
plt.show()
3)初始化权重参数。
# 随机初始化参数,参数w,b为需要学习的,故需requires_grad=True
w = t.randn(1,1, dtype=dtype,requires_grad=True)
b = t.zeros(1,1, dtype=dtype, requires_grad=True)
4)训练模型。
lr =0.001 # 学习率
for ii in range(800):
# forward:计算loss
y_pred = x.pow(2).mm(w) + b
loss = 0.5 * (y_pred - y) ** 2
loss = loss.sum()
# backward:自动计算梯度
loss.backward()
# 手动更新参数,需要用torch.no_grad()更新参数
with t.no_grad():
w -= lr * w.grad
b -= lr * b.grad
# 梯度清零
w.grad.zero_()
b.grad.zero_()
5)可视化训练结果。
plt.plot(x.numpy(), y_pred.detach().numpy(),'r-',label='predict')#predict
plt.scatter(x.numpy(), y.numpy(),color='blue',marker='o',label='true') # true data
plt.xlim(-1,1)
plt.ylim(2,6)
plt.legend()
plt.show()
print(w, b)
tensor([[2.9645]], requires_grad=True) tensor([[2.1146]], requires_grad=True)
这个结果与使用Numpy实现机器学习差不多。