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 友好很多,下面给出本文的环境:
- 安装 Anaconda,如果已经装过,在命令行查看版本
conda -V
,显示 4.10.3。 - 安装 CUDA,最好有 GPU 环境。在官网选择历史版本 11.1 下载。安装时选择自定义安装,取消一些不需要的勾选,装在 C 盘即可。在命令行查看版本
nvcc --version
,显示 11.1.105。 - 安装 PyTorch 1.9.1,官网选择 Windows 下的 pip 安装,选择对应版本后复制到命令行执行。
PyTorch 基础
PyTorch 最为常用的两个库是 torch
和 torchvision
,此外还有 torchaudio
和 torchtext
,分别用于处理不同领域的问题。下面要介绍的基础内容需要 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()
:同上,但是返回到目前为止程序运行过程中分配的最大显存量。
需要注意的是,如果在某个点上显存被释放了,使用该函数看到的将是释放后的显存使用情况。
在使用多卡训练、多卡推理的时候,需要设置可见设备,以下是常见的几种方法:
- 使用 PyTorch 进行管理:
1 |
|
- 在脚本入口使用 os 管理:
1 |
|
- 通过 argparse 传参设置:
1 |
|
- 在命令行中直接设置:
1 |
|
Tensor 对象
Tensor(张量)是 PyTorch 的核心数据结构,与 NumPy 中的 ndarrary 十分相似,表示多维矩阵,区别是 Tensor 支持 GPU 上的分布式运算,且支持「自动微分」。
Tensor 具有三个主要属性,dtype
、shape
、device
,分别表示元素的数据类型、形状参数和所属设备。前两者与 ndarray 类似,通过 .
运算符可以查看,下面是显示的区别:
1 |
|
在进行 Tensor 的数学运算时,与其他语言不同,高精度数据类型不能赋值给低精度类型,例如将 float 赋值给 int,将 non-boolen 赋值给 bool。
对于 Tensor 独有的 device
属性,需要通过字符串变量定义,可选的类型有:
1 |
|
可以在创建张量时指定其所属设备,也可以在创建后使用 to
切换,to
会返回一份新设备上的拷贝,不会覆盖旧设备,因此通常用 =
主动覆盖:
1 |
|
除了三个主要属性,张量还支持一些形如 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 |
|
样例测试如下:
1 |
|
如果不想通过 dtype
和 device
参数设置,也可以直接用以下函数创建张量,区别在于这些方法的参数可以是各个维度的大小,相当于 torch.zeros(shape, dtype=...)
:
Data type | 初始化 CPU tensor | 初始化 GPU tensor | 类型强转 |
---|---|---|---|
32-bit floating point | torch.FloatTensor() 或 torch.Tensor() | torch.cuda.FloatTensor() 或 torch.cuda.Tensor() | t.float() |
64-bit floating point | torch.DoubleTensor() | torch.cuda.DoubleTensor() | t.double() |
16-bit floating point | N/A | torch.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 |
|
同理,Tensor 也可以转化为 ndarray,只需用 t.numpy()
就能返回一个 ndarray,并且共享内存,前提是原 Tensor 不能位于 GPU 设备!
- 生成已初始化的张量、随机张量
和 Numpy 类似的初始化函数,增加了 like
类型的函数,其他参数和 torch.tensor()
一致,以下为样例:
1 |
|
- 基于范围生成数组
1 |
|
- 从外部文件导入
PyTorch 文件后缀为 .pt
、.pth
和 .pkl
,三者在使用上没有区别,都可以保存张量、模型(结构、权重参数),张量的保存和加载方法如下:
1 |
|
模型(Module)分为网络的结构和权重参数两部分,后者位于 module.stack_dict()
中,以字典的形式存储,模型的保存和加载方法有两种:
1 |
|
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)
:返回输入张量的转置(交换维度dim0
和dim1
),返回张量与输入张量共享内存,所以改变其中一个的内容会改变另一个。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)
,我们先对旧内存 x
和 y
进行了求和,产生的结果存放于新内存,然后再用 =
更新 x
的引用。
而原地操作可以理解为省略了「赋值」的过程,直接在旧内存上更改数值。这种原地操作更加节省内存,但是如果该内存可能被其他变量引用,可能导致计算一致性、自动微分出错的问题,所以在使用时应当尽量避免。例如:
1 |
|
随机采样
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=True
。dim
也可以取多个维度,以列表形式传参即可。
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),可以理解为反向图的根节点,而前面一层层的网络(weight
和 bias
)就是叶节点。在我们搭建网络进行前向传播时,反向图就已经创建,并且记录了每个节点之间用来什么操作来连接。
在我们对根节点 z
使用 z.backward()
时,沿着反向图,计算每个叶节点关于 z
的微分。这里之所以沿着反向图,就是利用了梯度链式法则。
1 |
|
在上面例子中,我们构建的反向图: \[ 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_grad
和 is_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()
。