PyTorch笔记 #1 基础操作

本文最后更新于:2023年3月22日 晚上

本文大部分内容基于 PyTorch 官方网站 的简易上手教程:Deep Learning with PyTorch: A 60 Minute Blitz,此外还参考了 官方 API 文档PyTorch 中文文档。本文将持续更新。

使用 help(函数名) 命令可以快捷查看帮助文档,在 IPython 中使用 函数名? 可以显示帮助文档,使用 函数名?? 可以显示函数的源码。

PyTorch 简介

PyTorch 是 Torch 在 Python 上的衍生,是一个针对深度学习,并且使用 GPU 和 CPU 来优化的 Tensor Library(张量库)。使用 PyTorch 深度学习框架,不仅能够实现强大的 GPU 加速,同时还支持动态神经网络。从某种程度上说,PyTorch 是在神经网络领域的 NumPy 的代替品。

PyTorch vs. Tensorflow

作为两种最热门的深度学习框架,难免被人比较,这里就我目前的了解列出几点:

  • PyTorch 在学术界更为热门,Tensorflow 在工业界更为热门。
  • PyTorch 更简单,与 NumPy 类似,与 Python 生态融合紧密,能快速实现从而验证 idea。其支持动态图计算,能更有效地处理一些科研问题。
  • Tensorflow 高度工业化,更有利于快速部署。其底层代码繁杂,但是性能更高(对研究者可能没有意义,但对工业界可能区别很大)。
  • PyTorch 的 API 设计更规范,很容易 clone 得到 module,更受研究者青睐。Tensorflow 的 API 混乱繁多、版本各异,可读性不强,难以复现研究。

PyTorch 环境

PyTorch 的安装过程也比 Tensorflow 友好很多,下面给出本文的环境:

  1. 安装 Anaconda,如果已经装过,在命令行查看版本 conda -V,显示 4.10.3。
  2. 安装 CUDA,最好有 GPU 环境。在官网选择历史版本 11.1 下载。安装时选择自定义安装,取消一些不需要的勾选,装在 C 盘即可。在命令行查看版本 nvcc --version,显示 11.1.105。
  3. 安装 PyTorch 1.9.1,官网选择 Windows 下的 pip 安装,选择对应版本后复制到命令行执行。

PyTorch 基础

PyTorch 最为常用的两个库是 torchtorchvision,此外还有 torchaudiotorchtext,分别用于处理不同领域的问题。下面要介绍的基础内容需要 import torch

Tensor 对象

Tensor(张量)是 PyTorch 的核心数据结构,与 NumPy 中的 ndarrary 十分相似,表示多维矩阵,区别是 Tensor 支持 GPU 上的分布式运算,且支持「自动微分」。

Tensor 具有三个主要属性,dtypeshapedevice,分别表示元素的数据类型、形状参数和所属设备。前两者与 ndarray 类似,通过 . 运算符可以查看,下面是显示的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> a = np.array([1, 2])
# a = array([1, 2])
# a.dtype = dtype('int32')
# a.shape = (2,)
# a.size = 2 (元素个数)
>>> t = torch.tensor(a)
# t = tensor([1, 2], dtype=torch.int32)
# t.dtype = torch.int32
# t.shape = torch.Size([2])
# t.size() = torch.Size([2])
# t.device = device(type='cpu')
>>> torch.is_tensor(a), torch.is_tensor(t)
# False True

在进行 Tensor 的数学运算时,与其他语言不同,高精度数据类型不能赋值给低精度类型,例如将 float 赋值给 int,将 non-boolen 赋值给 bool。

对于 Tensor 独有的 device 属性,需要通过字符串变量定义,可选的类型有:

1
2
3
4
5
6
>>> torch.device('cpu')		# CPU 上运行,无法使用 GPU 加速
# device(type='cpu')
>>> torch.device('cuda:0') # 指定编号,在多 GPU 时指定单卡
# device(type='cuda', index=0)
>>> torch.device('cuda') # 默认调取首个 GPU 设备
# device(type='cuda')

可以在创建张量时指定其所属设备,也可以在创建后使用 to 切换,to 会返回一份新设备上的拷贝,不会覆盖旧设备,因此通常用 = 主动覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 初始化时设置参数
t1 = torch.tensor([0.1, 0.2], dtype=torch.float64, device=torch.device('cuda:0'))

# 也可以预先定义好设备,在初始化时更方便
my_cuda = torch.device('cuda:1')
t2 = torch.tensor([1, 2], dtype=torch.int32, device=my_cuda)

# 先定义,后根据实际条件切换参数
t3 = torch.tensor([1])
if torch.cuda.is_available(): t3 = t3.to('cuda')

# 训练时设置参数
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.Model = self.Model.to(self.device)

# 需要注意的是,如果张量是以列表形式存放的,则无法对列表使用 to(常见于文本任务)
if isinstance(TList, list):
TList = [t.to(device) for t in TList]

除了三个主要属性,张量还支持一些形如 t.func() 的「函数型属性」:

  • t.item():对于一个只含有一个元素的张量(无论多少维度),返回一个标量值,常用于打印 Loss、sum。
  • t.cpu():将张量、模型移动到 CPU 上,用于进行普通的 Python 或 NumPy 操作,常用 t.cpu().numpy()
  • t.size():返回张量的形状,等价于 t.shape。带参数时 t.size(0) 相当于 t.shape[0]
  • t.numel():返回张量中元素的个数,某些运算不需要形状相同,只需要元素个数相同,就得先判断。
  • t.detach():分离出原张量的一个拷贝,但是 requires_grad=False,二者共享内存空间。反向传播到该张量时会停止,常用于冻结网络参数、限制梯度传播距离。
  • t.is_continuous():判断张量是否连续,连续的张量在各种运算上的速度会更快。由于张量的索引、拼接等操作都是浅拷贝,本质是用一个结构体存储了「切片规则」,因此在访问时会不再连续。
  • t.continuous():开辟一个新的存储区给张量,并改变存放顺序,使其变得连续化

Tensor 构造

根据不同的需求,有各种构造 Tensor 的方法。

  • 已有容器转化张量

生成 Tensor 的基本函数是 torch.tensor(),其参数可以是 list 、tuple、另一个 Tensor 甚至 ndarray,其函数接口如下:

1
2
3
4
5
6
7
8
"""
data: 传入原数组,深拷贝数据,且创建的 Tensor 没有微分历史
dtype: 指定数据类型,默认根据原数组推断
device: 创建 Tensor 的设备,如果 data 也是 Tensor 则跟随,否则会默认在 CPU 上
requires_grad: 是否支持自动求导,默认不支持
pin_memory: 是否存于锁页内存,加载数据时可以更快地从 CPU 拷贝到 GPU,但可能导致内存爆炸
"""
torch.tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False)

样例测试如下:

1
2
3
4
5
6
torch.tensor(1)			# 创建一个零维张量(不等同于标量)
torch.tensor([0, 1]) # 创建一个一维张量
torch.tensor([[0.1, 1.2], [2.2, 3.1]]) # 创建一个二维张量
torch.tensor([[1, 2]], dtype=torch.float64, device=torch.device('cuda:0')) # 设置参数
torch.tensor([0.5, 1.5], device=torch.device('cuda:0'), requires_grad=True) # 设置参数
torch.tensor(np.array([1, 2, 3])) # 从 ndarray 直接创建

如果不想通过 dtypedevice 参数设置,也可以直接用以下函数创建张量,区别在于这些方法的参数可以是各个维度的大小,相当于 torch.zeros(shape, dtype=...)

Data type初始化 CPU tensor初始化 GPU tensor类型强转
32-bit floating pointtorch.FloatTensor()torch.Tensor()torch.cuda.FloatTensor()torch.cuda.Tensor()t.float()
64-bit floating pointtorch.DoubleTensor()torch.cuda.DoubleTensor()t.double()
16-bit floating pointN/Atorch.cuda.HalfTensor()t.half()
8-bit integer (unsigned)torch.ByteTensor()torch.cuda.ByteTensor()t.byte()
8-bit integer (signed)torch.CharTensor()torch.cuda.CharTensor()t.char()
16-bit integer (signed)torch.ShortTensor()torch.cuda.ShortTensor()t.short()
32-bit integer (signed)torch.IntTensor()torch.cuda.IntTensor()t.int()
64-bit integer (signed)torch.LongTensor()torch.cuda.LongTensor()t.long()
  • 与 NumPy 的 ndarray 共享内存

使用 torch.from_numpy()可以转换,并且和原始的 ndarray 共享内存空间,且不能转移到 GPU 设备!此时如果修改 Tensor,则原始的 ndarray 也会修改,且 Tensor 不能使用变形操作。以下为样例:

1
2
3
4
5
6
7
>>> a = numpy.array([1, 2, 3])
>>> t = torch.from_numpy(a)
>>> t
# tensor([ 1, 2, 3])
>>> t[0] = -1
>>> a
# array([-1, 2, 3])

同理,Tensor 也可以转化为 ndarray,只需用 t.numpy() 就能返回一个 ndarray,并且共享内存,前提是原 Tensor 不能位于 GPU 设备!

  • 生成已初始化的张量、随机张量

和 Numpy 类似的初始化函数,增加了 like 类型的函数,其他参数和 torch.tensor() 一致,以下为样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 输入的参数是表示 shape 的元组,也可以直接传入维度(和 Numpy 不同)
t1 = torch.zeros((2, 3)) # 全为 0 的张量
t2 = torch.ones((2, 3)) # 全为 1 的张量
t3 = torch.empty((2, 3)) # 未初始化张量
t4 = torch.full((2, 3), x) # 全为 x 的张量
t5 = torch.rand((2, 3)) # 0 到 1 之间的浮点数
t6 = torch.randn((2, 3)) # 均值为 0,方差为 1 的正态分布浮点数
t7 = torch.eye(n=2, m=None) # n * m 的对角线值为 1 的二维张量,省略 m 时自动取 n

# 输入的 input 是另一个 Tensor,返回形状相同的一个初始化张量
t1_like = torch.zeros_like(input)
t2_like = torch.ones_like(input)
t3_like = torch.empty_like(input)
t4_like = torch.full_like(input, x)
t5_like = torch.rand_like(input)
t6_like = torch.randn_like(input)
  • 基于范围生成数组
1
2
3
4
5
6
7
8
# 范围在 [start, end) 之间,缺省其一则视为 [0, end),步长为 step
torch.arange(start=0, end, step=1, dtype=int64)
# 范围在 [start, end] 之间,不可缺省,且结果包含 end,步长为 step
torch.range(start, end, step=1, dtype=float32)
# 生成等差数列,[start, stop] 之间,限定总数为 steps,endpoint 表示是否包含 stop 端点
torch.linspace(start, end, steps=100, endpoint=True)
# 生成等比数列,注意范围是 [base^start, base^stop] 之间,限定总数为 num
torch.logspace(start, end, steps=100, endpoint=True, base=10.0)
  • 从外部文件导入

PyTorch 文件后缀为 .pt.pth.pkl,三者在使用上没有区别,都可以保存张量模型(结构、权重参数),张量的保存和加载方法如下:

1
2
3
4
torch.save(t, 'save_tensor.pt')		# 保存张量包括其 device 属性
t = torch.load('save_tensor.pt') # 是否加载到 GPU 由 save 之前的参数决定
t = torch.load('save_tensor.pt', map_location=torch.device('cpu')) # 加载到 CPU
t = torch.load('save_tensor.pt', map_location="cuda:0") # 加载到指定 GPU

模型(Module)分为网络的结构和权重参数两部分,后者位于 module.stack_dict() 中,以字典的形式存储,模型的保存和加载方法有两种:

1
2
3
4
5
6
7
8
9
10
# 只保存模型的权重参数,不保存模型结构
torch.save(model.state_dict(), 'save_param.pt')
model = Model(*args, **kwargs) # 需要重新 init 模型,模型是否加载到 GPU 由参数决定
model.load_state_dict(torch.load('save_param.pt', map_location="cuda:0")) # 参数加载
model.eval()

# 保存整个模型,包括网络结构和权重参数
torch.save(model, 'save_model.pt')
model = torch.load('save_model.pt') # 模型是否加载到 GPU 由 save 之前的参数决定
model.eval()

Tensor 变形

PyTorch 中实现了对 Tensor 的上百种操作,包括常见的索引、切片、变形。其中索引和切片的使用与 Numpy 类似,这里不展开介绍。主要介绍的是 Tensor 通过连接、换位等各种变形操作。下列所有对 t 的函数都可以改写成 t.func(..) 形式。

拼接与分块

  • torch.cat([a, b, c], dim=0):在指定维度上对输入的张量序列进行连接操作,要求除了指定维度 dim 外,其他维度形状必须相同。最后返回一个张量,其 dim 维度是输入的张量序列该维度长度之和
  • torch.chunk(t, chunks, dim=0):在指定维度上对输入张量进行分块操作,chunks 为分块的个数,如果不能整除则最后一块不完整,最后返回一个张量元组
  • torch.split(t, size, dim=0):在指定维度上对输入张量进行分块操作,size 为分块的大小,如果不能整除则最后一块不完整,最后返回一个张量元组
  • torch.stack([a, b, c], dim=0):沿着一个新维度对输入张量序列进行连接,要求所有张量都为相同形状。最后返回一个张量,在原形状上多出一个 dim 维度,该维度就是输入张量的个数
  • torch.unbind(t, dim=0):移除指定维度后,返回一个张量元组,包含沿着指定维切片后的各个切片,元组长度为被移除维度的取值。

压缩与扩充

  • torch.squeeze(t, dim):去除张量中大小为 1 的所有维度,返回一个张量。返回张量与输入张量共享内存。也可以指定维度删除,如果指定维度大小不为 1,则原样返回。
  • torch.unsqueeze(t, dim):在指定位置插入大小为 1 的维度,返回一个张量。返回张量与输入张量共享内存。如果 dim负值,则相当于反向索引插入。

按值切片

  • torch.index_select(t, dim, index):沿着指定维度对输入进行切片,index 需为长整型一维张量,将 dim 维度上取出 index 索引的项,返回一个张量,与原始张量不共享内存
  • torch.masked_select(t, mask):对输入进行掩码,mask 需为布尔型一维张量,形状需与输入张量显式相等或隐式相等(广播机制),返回一个一维张量,由 $mask=1$ 的元素构成。
  • torch.nonzero(t):返回一个包含输入中非零元素索引二维张量,第一维是非零元素个数,第二维是输入张量的维度数(返回一个矩阵,每行都是一个非零元素的坐标)。

维度交换与重塑

  • torch.transpose(t, dim0, dim1):返回输入张量的转置(交换维度 dim0dim1),返回张量与输入张量共享内存,所以改变其中一个的内容会改变另一个。
  • torch.permute(t, order):返回一个维度重组的张量,输入的 order 是一个 $0$ 到 $n-1$ 的元组,表示原始维度在新张量中的顺序。
  • torch.flatten(t, dim):将张量的维度展开,以 dim 维为起点的所有维度长度相乘,前面的保持不变
  • torch.reshape(t, shape):调整形状,默认将张量按行展开后填到新形状,要求规模匹配。当 shape 中某一维度取 $-1$ 时,其长度由其他维度计算。不需要满足连续性条件,功能更强。
  • torch.view(t, shape):与 reshape 的功能类似,但是只适用于满足连续性条件的张量(不能索引、转置、拼接),否则需要用 t.contiguous().view(),相当于 t.reshape()
  • torch.flip(t, dims):指定维度反转,即反序复制一份新的数据。

重复

  • t.repeat(*sizes):将张量沿着指定维度重复若干次,传入参数为一系列整数,倒着执行重复。
  • torch.repeat_interleave(t, repeats):将张量中的每个元素重复 repeats 次,并按顺序展开为一维张量

常用数学函数

除了上述对 Tensor 的变形操作,还有一系列涉及计算的操作,下面列举常用的部分。以下所有对 t 的函数都可以改写成 t.func(..) 形式。更多内容可查阅 文档

原地操作

PyTorch 在大部分数学函数都封装了原地操作(In-Place Operation)版本。只需将 t.func() 形式函数名后面加上单下划线即可,例如 t.add_();或在参数中设置 inplace=True

所谓非原地操作,可以理解为「计算&赋值」的过程。例如:x = torch.add(x, y),我们先对旧内存 xy 进行了求和,产生的结果存放于新内存,然后再用 = 更新 x 的引用。

而原地操作可以理解为省略了「赋值」的过程,直接在旧内存上更改数值。这种原地操作更加节省内存,但是如果该内存可能被其他变量引用,可能导致计算一致性自动微分出错的问题,所以在使用时应当尽量避免。例如:

1
2
3
4
5
6
7
8
9
def __init__(self):
self.conv1 = nn.Conv2d(...)
self.conv2 = nn.Conv2d(...)
self.relu = nn.ReLU(inplace=True)

def forward(self, x):
x = self.conv1(x)
h = self.relu(x) # 这里的 relu 是原地操作,则 x 的值被修改
h0 = self.conv2(x) # 这里的 conv2 已经不是对输入 x 的操作了

随机采样

  • torch.manual_seed(seed):设定生成随机数的种子,使得每次随机采样的结果一致,模型具有可复现性
  • torch.bernoulli(t):采样伯努利分布,返回一个和输入相同大小的张量,输入张量的每一个元素 $p\in[0,1]$ 作为采样 $1$ 的概率。
  • torch.multinomial(t, num):采样多项式分布,t每一行作为一组多项式系数,num 为在每一行采样的元素个数。
  • torch.normal(means, std):采样正态分布,返回一个和输入相同大小的张量,输入的两个张量代表输出均值和标准差。输入的两个张量大小不必相同,符合广播机制。
  • torch.poisson(t):采样泊松分布,返回一个和输入相同大小的张量,输入张量的每一个元素非负。

以下函数只有原地操作版本,采样的形状与 t 相同:

  • t.uniform_(from, to):采样均匀分布,从 $[from, to]$ 中以等概率采样。
  • t.exponential_(lambd=1):采样指数分布,从 $f(x)=\lambda e^{-\lambda x}$ 中采样。
  • t.geometric_(p):采样几何分布,从 $f\left( X=k \right) =p^{k-1}\left( 1-p \right) $ 中采样。

普通运算

  • torch.abs(t)torch.ceil(t)torch.floor(t):作用于每个元素取绝对值、向上取整、向下取整。
  • torch.round(t):作用于每个元素,四舍五入到最近的整数。
  • torch.clamp(t, min, max):作用于每个元素夹紧到 $[min, max]$ 区间,可以只输入一侧边界。
  • torch.frac(t)torch.sign(t):返回每个元素的小数部分、符号部分。
  • torch.neg(t)torch.reciprocal(t):作用于每个元素取相反数、倒数。
  • torch.add(t, value, other):作用于每个元素加上标量值 $value$,如果输入另一个张量 $other$,该张量形状需与 t 一致,返回 $t+value\times other$。
  • torch.div(t, value)torch.mul(t, value):作用于每个元素除以、乘以标量值 $value$。
  • torch.log(t)troch.log1p(t):作用于每个元素计算自然对数、加上 $1$ 后的自然对数。
  • torch.sin(t)torch.cos(t)torch.tan(t):作用于每个元素求三角函数。
  • torch.asin(t)torch.acos(t)torch.atan(t):作用于每个元素求反三角函数。
  • torch.sinh(t)torch.cosh(t)torch.tanh(t):作用于每个元素求双曲三角函数。
  • torch.pow(base, exp):求幂次,两者中一个为标量,另一个就为张量。返回一个张量,大小和输入张量一致。
  • torch.sqrt(t)torch.rsqrt(t):作用于每个元素,求其平方根、平方根倒数。
  • torch.sigmoid(t):作用于每个元素,求其 $\text{sigmoid}$ 值。
  • torch.nan_to_num(x, nan=2.0, posinf=1.0, neginf=-1.0):替换 NaN 值和无穷值。

塌缩运算

塌缩运算中,指定的维度 dim塌缩消失,如果想要输入输出维度个数与输入相同(即保持该维度为 $1$),则需要加上参数 keepdims=Truedim 也可以取多个维度,以列表形式传参即可。

  • torch.mean(t, dim)torch.std(t, dim)torch.std(t, dim):返回给定维度上的所有元素的均值、标准差、方差,指定维度会塌缩。
  • torch.sum(t, dim)torch.prod(t, dim):返回给定维度上的所有元素的和、积,指定维度会塌缩。
  • torch.cumsum(t, dim)torch.cumprod(t, dim):返回给定维度上的前缀和、积,形状保持不变。
  • torch.norm(t, p=2, dim):返回给定维度的 $p$ 范数,指定维度会塌缩,不加 dim 则结果塌缩到零维张量。
  • torch.dist(t1, t2, p=2):返回 $t_1-t_2$ 的 $p$ 范数,两个输入张量形状相同,结果塌缩到零维张量。
  • torch.max(t, dim)torch.min(t, dim):返回给定维度上的最大值、最小值,指定维度会塌缩。同时也会返回最值所在的索引,相当于 argmax
  • torch.max(t1, t2)torch.min(t1, t2):相应位置的元素对比,返回最小值到输出张量。
  • torch.argmax(t, dim)torch.argmin(t, dim):求给定维度上最值的索引,指定维度会塌缩,如果有多个最值则会返回第一个出现的位置。
  • torch.all(t)torch.any(t):输入一个布尔型张量,判断是否全 True、含有 True,返回一个 True 或 False 的张量。通常会将 t 写作一个二元运算表达式,如 x == y

矩阵运算

  • mat1 @ mat2矩阵点乘,要求第一个矩阵的列数等于第二个矩阵的行数。
    • torch.mm(mat1, mat2)mat1.mm(mat2)mat1.matmul(mat)矩阵点乘,要求同上。
    • torch.mv(mat, vec)mat.mv(vec)矩阵点乘向量,矩阵 $n\times m$,向量长度是 $m$,不区分行列向量
    • torch.dot(vec1, vec2)vec1.dot(vec2)向量点乘,输入不能是多维张量,返回一个零维张量。
    • torch.bmm(mat1, mat2)批量矩阵点乘,要求矩阵大小为 $(n,a,b)$ 和 $(n,b,c)$,返回 $(n,a,c)$。
  • mat1 * mat2对应位置相乘,即 Hadmard 积,要求两个矩阵各个维度长度相等
    • torch.mul(t1, t2)对应位置相乘,不限制张量维度数,各个维度长度相等即可。
  • torch.t(mat)mat.t()mat.T矩阵转置。等价于 torch.ranspose(t, 0, 1)
  • torch.inverse(mat)mat.inverse():返回输入方阵的逆矩阵。
  • torch.diag(t):对角化,有两种模式:
    • 如果输入是一个向量(一维张量),则返回一个以 t 为对角线元素的方阵;
    • 如果输入是一个矩阵(二维张量),则返回一个包含 t 对角线元素的张量。
  • torch.trace(mat):返回输入二维矩阵对角线元素的和(迹)。
  • torch.tril(mat, k=0):返回输入二维矩阵的下三角部分,其他部分为 $0$,k 控制是否包含主对角线
    • k=0 含主对角线;k>0 含主对角线之上 $k$ 条;k<0 不含主对角线之下。
  • torch.triu(mat, k=0):返回输入二维矩阵的上三角部分,其他部分为 $0$,k 控制是否包含主对角线,同上。
  • torch.svd(mat):返回输入矩阵的 SVD 分解,分别返回 $U,S,V$ 三个张量。

Autograd 自动微分

Tensor 支持自动微分,这在神经网络的反向传播中十分便利,只需要在创建张量时设置 requires_grad=True,就会为该张量进行 Autograd。如果张量已经创建,则可以用原地操作t.requires_grad_(True) 进行设置。需要注意的是,梯度只能用于计算浮点张量

Autograd 的原理是创建一个反向图,跟踪应用于他们的每个操作,使用所谓的动态计算图(DCG)计算梯度。这个图的叶节点是输入张量,根节点是输出张量。梯度是通过跟踪从根到叶的图形,并使用链式法则将每个梯度相乘来计算的。

梯度反向传播

神经网络可以理解为一个「精心调整以输出所需结果的复合数学函数」,而调整就是通过「反向传播」完成的。反向传播是用来计算损失函数的梯度输入参数的梯度的整个过程,以便以后更新权值,最终减少损失。

现在这个复合数学函数用一张反向图表示,这个数学函数(前向传播)最终的输出值(Loss),可以理解为反向图的根节点,而前面一层层的网络(weightbias)就是叶节点。在我们搭建网络进行前向传播时,反向图就已经创建,并且记录了每个节点之间用来什么操作来连接。

在我们对根节点 z 使用 z.backward() 时,沿着反向图,计算每个叶节点关于 z 的微分。这里之所以沿着反向图,就是利用了梯度链式法则。

1
2
3
4
5
6
>>> x = torch.tensor([[1., 0.], [-1., 1.]], requires_grad=True)
>>> z = x.pow(2).sum()
>>> z.backward()
>>> x.grad
# tensor([[ 2., 0.],
# [-2., 2.]])

在上面例子中,我们构建的反向图:

反向传播的过程:

动态计算图

首先了解这个反向图中每个叶节点 x 的构成:

  • data:该叶节点持有的数据,就是张量本身。
  • requires_grad:如果为 True,则从 x 开始跟踪所有的操作,形成一个用于 x 梯度计算的向后图。
  • grad:保存梯度值,当调用 out.backward() 时,这里的值就是 $\partial out/\partial x$。
  • grad_fn:用来计算梯度的后向函数,PyTorch 自动推导,打印张量可以看到。
  • is_leaf:当显式创建一个张量时,该值为 True;当张量被显式赋值时,该值也为 True。

在调用 backward() 时,只计算 requires_gradis_leaf 同时为真的节点的梯度。

PyTorch 是如何实现这个计算过程的呢?其实在调用 z.backward() 函数时,会有一个外部梯度隐式参与:z.backward(torch.tensor(1.0)),这个值作为 z.grad 沿着反向图的每一个节点,计算 x.grad = grad_fn(z.grad),以此类推,直到遍历完所有叶节点。

当输出不是一个标量的时候,这个外部梯度必须显式指定,且形状和 z 相同,例如初始化为全一张量:z.backward(torch.ones_like(z)

这些数值会一直保留在张量中,如果反向图有变化,需要重新反向传播,此时需要先用原地操作x.grad.zero_() 将梯度清零,否则会自动累计上一次的值。在神经网络中也会用类似的 optimizer.zero_grad()


PyTorch笔记 #1 基础操作
https://hwcoder.top/PyTorch-Note-1
作者
Wei He
发布于
2022年11月10日
许可协议