2 PyTorch基础

2 PyTorch基础

2.1Numpy与Tensor

第1章已经介绍了Numpy,了解到其存取数据非常方便,而且还拥有大 量的函数,所以深得数据处理、机器学习者喜爱。这节我们将介绍PyTorch 的Tensor,它可以是零维(又称为标量或一个数)、一维、二维及多维的数 组。Tensor自称为神经网络界的Numpy,它与Numpy相似,二者可以共享内 存,且之间的转换非常方便和高效。不过它们也有不同之处,最大的区别就 是Numpy会把ndarray放在CPU中进行加速运算,而由Torch产生的Tensor会 放在GPU中进行加速运算(假设当前环境有GPU。

2.1.1 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)

2.1.2 创建Tensor

创建Tensor的方法有很多,可以从列表或ndarray等类型进行构建,也可根据指定的形状构建。常见的创建Tensor的方法可参考表2-1。

在这里插入图片描述
下面举例说明。

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的几点区别:
1)torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),而torch.tensor是从数据中推断数据类型。

2)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()))

下面是根据一定规则,自动生成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))

2.1.3 修改Tensor形状

在处理数据、构建网络层等过程中,经常需要了解Tensor的形状、修改
Tensor的形状。与修改Numpy的形状类似,修改Tenor的形状也有很多类似
函数,具体可参考表2-2。

在这里插入图片描述
以下为一些实例:

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的异同
1)reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。
但view()只可由torch.Tensor.view()来调用。
2)对于一个将要被view的Tensor,新的size必须与原来的size与stride兼
容。否则,在view之前必须调用contiguous()方法。
3)同样也是返回与input数据量相同,但形状不同的Tensor。若满足
view的条件,则不会copy,若不满足,则会copy。
4)如果你只想重塑张量,请使用torch.reshape。如果你还关注内存使用
情况并希望确保两个张量共享相同的数据,请使用torch.view。

2.1.4 索引操作

Tensor的索引操作与Numpy类似,一般情况下索引结果与源数据共享内
存。从Tensor获取元素除了可以通过索引,也可以借助一些函数,常用的选
择函数可参考表2-3。

在这里插入图片描述

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)

2.1.5 广播机制

在1.7节中介绍了Numpy的广播机制,广播机制是向量运算的重要技
巧。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,unsqueeze()函数为增加一个维度相对应的squeeze()函数为减小一个维度
#使用expand函数重复数组,分别的4x3的矩阵
A2=A1.expand(4,3)
B3=B2.expand(4,3)
#然后进行相加,C1与C结果一致
C1=A2+B3

2.1.6 逐元素操作

与Numpy一样,Tensor也有逐元素操作(Element-Wise),且操作内容相似,但使用函数可能不尽相同。大部分数学运算都属于逐元素操作,其输入与输出的形状相同。常见的逐元素操作可参考表2-4。

【说明】
这些操作均会创建新的Tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。

在这里插入图片描述

import torch
t = torch.randn(1, 3)
t1 = torch.randn(3, 1)
t2 = torch.randn(1, 3)
#t+0.1*(t1/t2)
torch.addcdiv(t, 0.1, t1, t2)
#计算sigmoid
torch.sigmoid(t)
#将t限制在[0,1]之间
torch.clamp(t,0,1)
#t+2进行就地运算
t.add_(2)

2.1.7 归并操作

归并操作顾名思义,就是对输入进行归并或合计等操作,这类操作的输入输出形状一般并不相同,而且往往是输入大于输出形状。归并操作可以对整个Tensor,也可以沿着某个维度进行归并。常见的归并操作可参考表2-5。
在这里插入图片描述
归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,缺省情况是False,即不保留。以下为归并操作的部分代码:

import torch
#生成一个含6个数的向量
a=torch.linspace(0,10,6)
#使用view方法,把a变为2x3矩阵
a=a.view((2,3))
#沿y轴方向累加,即dim=0
b=a.sum(dim=0) #b的形状为[3]
#沿y轴方向累加,即dim=0,并保留含1的维度
b=a.sum(dim=0,keepdim=True) #b的形状为[1,3]

2.1.8 比较操作

比较操作一般是进行逐元素比较,有些是按指定方向比较。常用的比较
函数可参考表2-6。

在这里插入图片描述

2.1.9 矩阵操作

机器学习和深度学习中存在大量的矩阵运算,常用的算法有两种:一种是逐元素乘法,另外一种是点积乘法。PyTorch中常用的矩阵函数可参考表2-7。

在这里插入图片描述
机器学习和深度学习中存在大量的矩阵运算,常用的算法有两种:一种是逐元素乘法,另外一种是点积乘法。PyTorch中常用的矩阵函数可参考表2-7。

在这里插入图片描述
【说明】
1)Torch的dot与Numpy的dot有点不同,Torch中的dot是对两个为1D张
量进行点积运算,Numpy中的dot无此限制。
2)mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。
3)转置运算会导致存储空间不连续,需要调用contiguous方法转为连
续。

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)

2.1.10 PyTorch与Numpy比较

PyTorch与Numpy有很多类似的地方,并且有很多相同的操作函数名称,或虽然函数名称不同但含义相同;当然也有一些虽然函数名称相同,但含义不尽相同。有些很容易混淆,下面我们把一些主要的区别进行汇总,具体可参考表2-8。

在这里插入图片描述
2.2 Tensor与Autograd

在神经网络中,一个重要内容就是进行参数学习,而参数学习离不开求
导,那么PyTorch是如何进行求导的呢?
现在大部分深度学习架构都有自动求导的功能,PyTorch也不例外,torch.autograd包就是用来自动求导的。Autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为Autograd的两个核心类,它们相互连接并生成一个有向非循环图。接下来我们先简单介绍Tensor如何实现自动求导,然后介绍计算图,最后用代码来实现这些功能。

2.2.1自动求导要点

为实现对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)。

2.2.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.2.3 标量反向传播

假设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

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))

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))

2.2.3 标量反向传播

在2.2.3节中介绍了当目标张量为标量时,可以调用backward()方法且无须传入参数。目标张量一般都是标量,如我们经常使用的损失值Loss,一般都是一个标量。但也有非标量的情况,后面将介绍的Deep Dream的目标值就是一个含多个元素的张量。那如何对非标量进行反向传播呢?PyTorch有个简单的规定,不让张量(Tensor)对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用backward(),则需要传入一个gradient参数,该参数也是张量,而且需要与调用backward()的张量形状相同。那么为什么要传入一个张量gradient呢?

在这里插入图片描述
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的结果是否正确。

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)

2.3 计算图

前面已经介绍了Numpy、Tensor的基础内容,对如何使用Numpy、Tensor操作数组有了一定认识。为了加深大家对使用PyTorch完成机器学习、深度学习的理解,本章剩余章节将分别用Numpy、Tensor、autograd、nn及optimal来实现同一个机器学习任务,比较它们之间的异同及各自优缺点,从而使读者加深对PyTorch的理解。

首先,我们用最原始的Numpy实现有关回归的一个机器学习任务,不用PyTorch中的包或类。这种方法代码可能多一点,但每一步都是透明的,有利于理解每步的工作原理。主要步骤包括:

首先,给出一个数组x,然后基于表达式y=3x2+2,加上一些噪音数据到达另一组数据y。

然后,构建一个机器学习模型,学习表达式y=wx2+b的两个参数w、b。利用数组x,y的数据为训练数据。

最后,采用梯度梯度下降法,通过多次迭代,学习到w、b的值。

以下为具体步骤:

1)导入需要的库。

# -*- coding: utf-8 -*-
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()

运行结果如图2-11所示。

在这里插入图片描述
4)初始化权重参数。

# 随机初始化参数
w1 = np.random.rand(1,1)
b1 = np.random.rand(1,1)

5)训练模型
在这里插入图片描述
在这里插入图片描述

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.3使用Tensor及Antograd实现机器学习

2.2节可以说是纯手工完成一个机器学习任务,数据用Numpy表示,梯度及学习是自己定义并构建学习模型。这种方法适合于比较简单的情况,如果稍微复杂一些,代码量将几何级增加。那是否有更方便的方法呢?本节我们将使用PyTorch的一个自动求导的包——antograd,利用这个包及对应的Tensor,便可利用自动反向传播来求梯度,无须手工计算梯度。以下是具体实现代码。

1)导入需要的库。

import torch as t
%matplotlib inline
from matplotlib import pyplot as plt

2)生成训练数据,并可视化数据分布情况。

t.manual_seed(100)
dtype = t.float
#生成x坐标数据,x为tenor,需要把x的形状转换为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):
# 前向传播,并定义损失函数loss
y_pred = x.pow(2).mm(w) + b
loss = 0.5 * (y_pred - y) ** 2
loss = loss.sum()
# 自动计算梯度,梯度存放在grad属性中
loss.backward()
# 手动更新参数,需要用torch.no_grad(),使上下文环境中切断自动求导的计算
with t.no_grad():
w -= lr * w.grad
b -= lr * b.grad
# 梯度清零
w.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)

在这里插入图片描述
2.4 小结

本章主要介绍PyTorch的基础知识,这些内容是后续章节的重要支撑。首先介绍了PyTorch的安装配置,然后介绍了PyTorch的重要数据结构Tensor。Tensor类似于Numpy的数据结构,但Tensor提供GPU加速及自动求导等技术。最后分别用Numpy、Tensor、Autograd、等技术实现同一个机器学习任务。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值