1.5.4. 框架基础(Pytorch)#

PyTorch是一个建立在Torch库之上的Python包,旨在加速深度学习应用。PyTorch提供了一种类似于NumPy的抽象方法来表征张量(或多维数组),利用GPU来加速训练;同时,PyTorch采用动态计算图结构,可低延迟甚至是零延迟的改变网络行为。

PyTorch主要由4个包组成:

  • torch:与NumPy类似的通用包,可将张量类型转换为可在GPU上进行计算的类型。

  • torch.autograd:能构建计算图形并自动获取梯度的包。

  • torch.nn:具有共享层和损失函数等功能的神经网络操作包。

  • torch.optim:具有通用优化算法的包。

1.导入torch包

PyTorch深度学习框架在导入时其包的名称为torch,如下示例可查看当前框架的版本号。

chapter_1_5_4_01.py#
1# 导入PyTorch包,以下代码均含此行,本书为节省篇幅省略此行
2import torch
3
4# 查看其版本号,不同电脑版本不同其结果可能不同
5print(torch.__version__)
1.13.1+cu116
  1. 创建张量(Tensor)

几何代数中定义的张量(Tensor)是基于向量和矩阵的推广,可以将标量视为零阶张量,向量视为一阶张量,矩阵视为二阶张量。标量是一个单独的数;向量是一列数,且这些数是有序排列的;矩阵是二维数组,其中每一个元素被两个索引所确定。

张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数,可理解为一个n维数值阵列。通俗来讲,可以将任意一张彩色图片表示为一个三阶张量,其三个维度分别是图片的高度、宽度和色彩数据;同时,也可以用四阶张量表示一个包含多张图片的数据集,其四个维度分别是图片在数据集中的编号与图片的高度、宽度和色彩数据。

PyTorch的Tensor可以是零维(又称为标量或一个数)、一维(行或列)、二维(又称矩阵)及多维的数组。与NumPy中的ndarray相似,其最大区别是NumPy会把ndarray放在CPU中进行运算,而PyTorch的Tensor会放在GPU中进行加速运算。其中常见创建Tensor的方法如表1-3所示(* size表示可以接收多个参数)。

表 1-3 创建Tensor 的常见方法#

函数

功能

tensor(*size)

直接从参数构造一个张量,支持List、Numpy数组

eye(row, column)

创建指定行数、列数的二维Tensor

linspace(stat,end,steps)

将区间[start,end)均分成steps份

logspace(stat,end,steps)

将区间[10^start,10^end)均分成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

arrange(stat,end,step)

在区间[start,end)上以间隔step生成一个序列张量

from_Numpy(ndarray)

从ndarray创建一个Tensor

以下示例将演示如何用torch创建张量:

chapter_1_5_4_02.py#
 1import torch
 2import numpy as np
 3
 4# 创建一维Tensor
 5tsr1 = torch.tensor(np.arange(1, 10, 1))
 6print(tsr1)
 7# 创建指定形状的Tensor
 8tsr2 = torch.tensor((2, 3))
 9print("tsr2 size:", tsr2.size())
10# 创建元素全为0的5×6二维Tensor
11xx = torch.zeros(5, 6)
12print("xx size", xx.size())
13# 创建空(杂乱数据值)的5×6二维Tensor
14yy = torch.empty(5, 6)
15print("yy size", yy.size())
16# 创建随机数组成的5×6二维Tensor,可指定生成方式和值范围
17zz = torch.rand(5, 6)
18
19print("zz size", zz.size())
tensor([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=torch.int32)
tsr2 size: torch.Size([2])
xx size torch.Size([5, 6])
yy size torch.Size([5, 6])
zz size torch.Size([5, 6])
  1. Tensor 基本计算

Tensor可以进行加减乘除等基本运算,其运算及相应的显示结果如下所示。

chapter_1_5_4_03.py#
 1import torch
 2
 3# 创建元素全为0的5×6二维Tensor
 4xx = torch.zeros(5, 6)
 5# 创建元素全为1的5×6二维Tensor
 6yy = torch.ones(5, 6)
 7
 8print("xx+yy=", xx + yy)
 9print("xx-yy=", xx - yy)
10print("xx*yy=", xx * yy)
11print("xx/yy=", xx / yy)
xx+yy= tensor([[1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.]])
xx-yy= tensor([[-1., -1., -1., -1., -1., -1.],
        [-1., -1., -1., -1., -1., -1.],
        [-1., -1., -1., -1., -1., -1.],
        [-1., -1., -1., -1., -1., -1.],
        [-1., -1., -1., -1., -1., -1.]])
xx*yy= tensor([[0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.]])
xx/yy= tensor([[0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.]])
  1. Tensor 形状改变

张量的形状可以通过reshape操作改变,代码示例如下。

chapter_1_5_4_04.py#
1import torch
2
3xx = torch.zeros(5, 6)
4print("xx size:", xx.size())
5yy = xx.reshape(30)
6print("yy size:", yy.size())
7zz = xx.reshape(5, 3, 2)
8print("zz size:", zz.size())
xx size: torch.Size([5, 6])
yy size: torch.Size([30])
zz size: torch.Size([5, 3, 2])

修改形状的常用函数如表1-4所示。

表 1-4 Tensor 修改形状常用函数#

函数

说明

size()

返回张量的shape属性值,与函数shape(0.4版新增)等价

numel(input)

计算Tensor的元素个数

view(*shape)

修改Tensor的shape,与Reshape(0.4版新增)类似,但View返回的对象与源Tensor共享内存,修改一个,另一个同时修改。Reshape将生成新的Tensor,而且不要求源Tensor是连续的。view(-1)展平数组

resize

类似于view,但在size超出时会重新分配内存空间

item

若Tensor为单元素,则返回Python的标量

unsqueeze

在指定维度增加一个“1”

squeeze

在指定维度压缩一个“1”

  1. 与Numpy 之间的转换

PyTorch中的张量可以和NumPy之间的表示进行转换,其示例代码如下。

chapter_1_5_4_05.py#
1import torch
2
3xx = torch.zeros(5, 6)
4yy = xx.numpy()
5print("yy type=", type(yy))
6zz = torch.from_numpy(yy)
7print("zz type=", type(zz))
yy type= <class 'numpy.ndarray'>
zz type= <class 'torch.Tensor'>
  1. 张量中元素访问

和Python数组一样,张量也可以通过索引和切片访问元素,其中常见的元素访问函数如表1-5所示。

表1-5 常用的元素访问函数#

函数

说明

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的反操作,根据指定索引补充数据

Tensor元素访问示意如下:

chapter_1_5_4_06.py#
1import torch
2import numpy as np
3
4xx = torch.from_numpy(np.arange(0, 30, 1)).reshape(5, 6)
5print(xx[0:2, :])
6print(xx[:, 1:3])
7print(xx[0:2, 1:3])
tensor([[ 0,  1,  2,  3,  4,  5],
        [ 6,  7,  8,  9, 10, 11]], dtype=torch.int32)
tensor([[ 1,  2],
        [ 7,  8],
        [13, 14],
        [19, 20],
        [25, 26]], dtype=torch.int32)
tensor([[1, 2],
        [7, 8]], dtype=torch.int32)
  1. 广播机制

PyTorch的广播机制与NumPy类似,使用示例如下所示。

chapter_1_5_4_07.py#
 1import torch
 2import numpy as np
 3
 4arrA = np.arange(0, 50, 10).reshape(5, 1)
 5arrB = np.arange(0, 3, 1)
 6tsrA1 = torch.from_numpy(arrA)
 7print("tsrA1 size:", tsrA1.size())
 8tsrB1 = torch.from_numpy(arrB)
 9print("tsrB1 size:", tsrB1.size())
10tsrC1 = tsrA1 + tsrB1
11print("tsrC1 size:", tsrC1.size())
12# 广播的实现过程
13tsrB2 = tsrB1.unsqueeze(0)
14tsrA2 = tsrA1.expand(5, 3)
15tsrB3 = tsrB2.expand(5, 3)
16tsrC2 = tsrA2 + tsrB3
17print("tsrC2 size:", tsrC2.size(), " equal:", tsrC2 == tsrC1)
tsrA1 size: torch.Size([5, 1])
tsrB1 size: torch.Size([3])
tsrC1 size: torch.Size([5, 3])
tsrC2 size: torch.Size([5, 3])  equal: tensor([[True, True, True],
        [True, True, True],
        [True, True, True],
        [True, True, True],
        [True, True, True]])
  1. 逐元素运算

PyTorch的逐元素运算操作与NumPy类似,其常见的函数如表1-6所示。

表1-6 常见逐元素运算函数#

函数

说明

abs/add

绝对值 / 加法

addcdiv(t, v, t1, t2)

t1与t2按元素逐个相除后,乘v加t

addcmul(t, v, t1, t2)

t1与t2按元素逐个相乘后,乘v加t

ceil/floor

向上取整 / 向下取整

clamp(t, min, max)

将张量元素限制在指定区间[min,max]

exp/log/pow

指数 / 对数 / 幂

mul(或 *)/neg

逐元素乘法 / 取反

sigmoid/tanh/softmax

激活函数

sign/sqrt

取符号 / 开根号

逐元素运算函数使用示例如下所示:

chapter_1_5_4_08.py#
1import torch
2import numpy as np
3
4tsrA1 = torch.from_numpy(np.arange(1, 5, 1))
5tsrA2 = torch.from_numpy(np.arange(1, 5, 1).reshape(4, 1))
6tsrA3 = torch.from_numpy(np.arange(3, 7, 1))
7print(torch.add(tsrA1, tsrA3))
8print(torch.addcmul(tsrA1, tsrA2, tsrA3, value=10.0))
9print(torch.clamp(tsrA1, 0, 3))
tensor([ 4,  6,  8, 10], dtype=torch.int32)
tensor([[ 31,  42,  53,  64],
        [ 61,  82, 103, 124],
        [ 91, 122, 153, 184],
        [121, 162, 203, 244]], dtype=torch.int32)
tensor([1, 2, 3, 3], dtype=torch.int32)
  1. 数据预处理

深度学习需要处理大量数据,而在此过程中一般需进行数据预处理,代码示例如下,原始数据及处理后的数据结果如图1-26所示。

图 1-26 原始数据及处理后的数据结果

图 1-26 原始数据及处理后的数据结果#

chapter_1_5_4_09.py#
 1import os
 2import pandas as pd
 3
 4
 5def createDataFile():
 6    os.makedirs(os.path.join('..', 'data'), exist_ok=True)
 7    dataFile = os.path.join('..', 'data', 'demo_data.csv')
 8    with open(dataFile, 'w') as f:
 9        f.write('Year,PCCount,GDP\n')
10        f.write('2000,500,298900\n')
11        f.write('2006,NAN,498900\n')
12        f.write('2008,1500,586900\n')
13        f.write('2016,NAN,12356789\n')
14        f.write('2020,19500,2335600\n')
15        f.write('2022,NAN,5668500\n')
16        return dataFile
17
18
19def readDataFile(dataFile):
20    data = pd.read_csv(dataFile)
21    print(data)
22    inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
23    # 第[1,3)列与第3列内容分别存到两变量
24    inputs = inputs.fillna(inputs.mean)
25    print(inputs)
26    print(outputs)
27
28
29dataFile = createDataFile()
30readDataFile(dataFile)
   Year PCCount       GDP
0  2000     500    298900
1  2006     NAN    498900
2  2008    1500    586900
3  2016     NAN  12356789
4  2020   19500   2335600
5  2022     NAN   5668500
   Year PCCount
0  2000     500
1  2006     NAN
2  2008    1500
3  2016     NAN
4  2020   19500
5  2022     NAN
0      298900
1      498900
2      586900
3    12356789
4     2335600
5     5668500
Name: GDP, dtype: int64
  1. Autograd 自动求导

现今大部分深度学习框架(PyTorch、TensorFlow等)都具备自动求导功能,在PyTorch中是靠torch.autograd包实现自动求导。该包为张量操作提供了自动求导功能,其包括torch.Function和autograd两个主要类。

在自动求导的整个过程中,PyTorch采用计算图的形式进行组织,该计算图为动态图,且在每次前向传播时将重新构建,其中自动求导包括的主要步骤如下:

  • (1)创建叶子节点的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() 函数接收的参数应和调用本函数的Tensor的维度相同(或是可广播为相同的维度)。如果求导的Tensor为标量,则backward() 中的参数可省略。

  • (6)反向传播的中间缓存会被清空, 如果需要进行多次反向传播, 需要指定backward中的参数retain_graph值为True,多次反向传播时,梯度会累加。

  • (7)非叶子节点的梯度,在backward()函数调用后即被清空。

  • (8)可用torch.no_grad()包裹代码块的方式,以阻止autograd去跟踪requires_grad值为True的张量的历史记录。

  1. Backward反向传播

在PyTorch中的backward() 函数,通过反向传播过程可自动计算各叶子节点的梯度,同时其叶子节点的梯度值将累加到grad属性中,非叶子节点的计算操作将记录在grad_fn属性中。此处以z=wx+b(中间变量y=wx)为例介绍其实现的主要步骤。

chapter_1_5_4_10.py#
 1import torch
 2
 3x = torch.tensor([2])
 4w = torch.randn(1, requires_grad=True)
 5b = torch.randn(1, requires_grad=True)
 6y = torch.mul(w, x)
 7z = torch.add(y, b)
 8
 9# x,w,b叶子节点的值
10print("x,w,b的require_grad值为:{},{},{}".format(x.requires_grad,
11                                                w.requires_grad,
12                                                b.requires_grad))
13
14# 查看叶子节点、非叶子节点的其他属性:
15print("y,z的requires_grad值分别为:{},{}".format(y.requires_grad,
16                                                z.requires_grad))
17# 非叶子节点的requires_grad值
18# 说明:因与w,b有依赖关系,故y,z的requires_grad属性也是:True,True
19print("x,w,b,y,z的叶子节点属性:{},{},{},{},{}".format(x.is_leaf,
20                                                      w.is_leaf,
21                                                      b.is_leaf,
22                                                      y.is_leaf,
23                                                      z.is_leaf))
24# 查看各节点是否叶子节点
25print("x,w,b的grad_fn属性:{},{},{}".format(x.grad_fn,
26                                           w.grad_fn,
27                                           b.grad_fn))
28# 叶子节点的grad_fn属性
29# 说明:因x,w,b为用户创建的,故grad_fn属性为None
30print("y,z的叶子节点属性:{},{}".format(y.grad_fn == None,
31                                       z.grad_fn == None))
32# y,z是否为叶子节点
33# 自动求导,实现梯度的反向传播:
34z.backward()
35# 基于z张量进行梯度反向传播,如果需要多次使用backward
36# 需要修改参数retain_graph为True,此时梯度是累加的
37print(z)
38print("w,b,x的梯度分别为:{},{},{}".format(w.grad,
39                                          b.grad, x.grad))
40# 说明:x是叶子节点但它无须求导,故其梯度为None
41print("非叶子节点y,z的梯度分别为:{},{}".format(y.retain_grad(),
42                                               z.retain_grad()))
43# 说明:当执行backward之后,非叶子节点的梯度会自动清空
x,w,b的require_grad值为:False,True,True
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的叶子节点属性:False,False
tensor([2.1398], grad_fn=<AddBackward0>)
w,b,x的梯度分别为:tensor([2.]),tensor([1.]),None
非叶子节点y,z的梯度分别为:None,None