PyTorch笔记 #1 基础操作

本文最后更新于:2024年7月8日 上午

本文大部分内容基于 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

CUDA 模块

CUDA(Compute Unified Device Architecture)是一种并行计算平台,它允许开发者利用 Nvidia GPU 的并行计算能力来加速应用程序的运行。与传统的 CPU 相比,GPU 在处理大规模并行计算任务时具有显著的优势。

torch.cuda 模块是 PyTorch 用于与 CUDA 相关功能交互的核心模块,支持使用 GPU 加速计算任务。因此,该模型的首要功能就是 GPU 设备管理

  • torch.cuda.is_available():返回 true/false,表示当前系统上是否有可用的 GPU。
  • torch.cuda.current_device():返回当前所选 GPU 设备的索引。
  • torch.cuda.device_count():返回系统中可用的 GPU 数量,会受到程序可见 GPU 设置的影响。
  • torch.cuda.get_device_name(index):返回指定设备的名称,index 为 GPU 号。

此外,还可以进行内存管理,在代码的关键部分插入以下语句,即可监控显存占用:

  • torch.cuda.memory_allocated():返回当前分配给张量的 GPU 显存字节数,张量需要移动到 CUDA device 上才可以查看。torch.cuda.memory_allocated()/1024/1024/1024 即可得到显存 GB 数。
  • torch.cuda.max_memory_allocated():同上,但是返回到目前为止程序运行过程中分配的最大显存量

需要注意的是,如果在某个点上显存被释放了,使用该函数看到的将是释放后的显存使用情况。

在使用多卡训练、多卡推理的时候,需要设置可见设备,以下是常见的几种方法:

  1. 使用 PyTorch 进行管理
1
2
3
4
5
6
7
8
9
10
num_gpus = torch.cuda.device_count()

# 指定第一张卡作为默认显卡
if num_gpus > 0:
torch.cuda.set_device(0)
print("Current CUDA device set to GPU at index 0.")
else:
print("No CUDA devices available.")

# 也可以在初始化张量、模型的时候进行设置,详见下方
  1. 在脚本入口使用 os 管理
1
2
3
4
5
import os

if __name__ == "__main__":
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
main()
  1. 通过 argparse 传参设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
import argparse

def parse_config():
parser = argparse.ArgumentParser(description='Set visible CUDA devices')
parser.add_argument('--gpus', type=str, default="0,1")
args = parser.parse_args()
return args

if __name__ == "__main__":
args = parse_config()
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpus
main()

# 运行时采用 python main.py --gpus 0,1
  1. 在命令行中直接设置
1
CUDA_VISIBLE_DEVICES=0,1 python main.py

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( X=k ) =p^{k-1}( 1-p ) $ 中采样。

普通运算

  • 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.exp(t):作用于每个元素,求其 \(e^x\) 值。
  • 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=\left[ \begin{matrix} 1& 0\\ -1& 1\\ \end{matrix} \right] \quad\quad z=\sum_i{\sum_j{x_{i,j}^{2}}} \] 反向传播的过程: \[ \frac{\partial z}{\partial x_{i,j}}=2x_{i,j}\quad\quad \frac{\partial z}{\partial x}=\left[ \begin{matrix} 2& 0\\ -2& 2\\ \end{matrix} \right] \]

动态计算图

首先了解这个反向图中每个叶节点 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日
许可协议