pytorch
pytorch是深度学习的常用开源工具库,基于pytorch的python代码可读性较强,在目前主流的深度学习论文中逐渐占据主导地位。pytorch中最核心的概念是张量(tensor),它是一种高效的数据结构,可以简单理解为高维数组,在深度学习中应用非常广泛;事实上,pytorch的诸多方法的处理对象,大多都是张量。
本文结合笔者目前较为浅薄的编程、实验经历,对一些实用的、经常接触的以及比较经典的库方法进行记录与总结。
import torch
import numpy as np
张量的创建
最朴素的创建方法:torch.tensor()
- torch.tensor(data: Any, dtype: _dtype | None = None, device: Device = None, requires_grad: _bool = False)
- torch.LongTensor(*args: List)
这个方法的功能和用法非常显然,我之前也经常使用,可以将指定的列表数据转换为指定的张量类型。
a = torch.tensor([1, 5, 3], dtype=torch.float32)
print(a)
b = torch.LongTensor([2, 9, 1])
print(b)
'''
输出:
tensor([1., 5., 3.])
tensor([2, 9, 1])
'''
最常用的创建方法:torch.rand()
- torch.rand(size: Tuple)
- torch.rand(*size: int)
- torch.rand_like(input: Tensor)
之所以常用,是因为这个方法是随机创建,接收的参数是一个元组,当然也可直接传入多个整型数字作为要创建的张量维度信息。
a = torch.rand(2, 3)
print(a)
b = torch.rand(size=(2, 3))
print(b)
c = torch.rand_like(a)
print(c)
'''
输出:
tensor([[0.4019, 0.6014, 0.1614],
[0.5914, 0.9730, 0.2322]])
tensor([[0.9385, 0.0407, 0.6528],
[0.8274, 0.8461, 0.6536]])
tensor([[0.2846, 0.3948, 0.3780],
[0.1335, 0.9324, 0.5800]])
'''
最初始的创建方法:torch.zeros()/torch.ones()
- torch.zeros(size: Tuple) 或 torch.zeros(*size: int)
- torch.ones(size: Tuple) 或 torch.ones(*size: int)
这两个方法分别用于创建全零张量和全一张量,当然也是接收多个整形数据参数,作为要创建的张量维度信息。这种方法多用于后续初始化或者创建掩码矩阵。
a = torch.zeros(2, 3)
print(a)
b = torch.ones(size=(3, 2))
print(b)
'''
输出:
tensor([[0., 0., 0.],
[0., 0., 0.]])
tensor([[1., 1.],
[1., 1.],
[1., 1.]])
'''
最单位化的创建方法:torch.eye()
- torch.eye(n: int)
这个方法可以用于创建单位矩阵。其中参数n即为方阵维度。常用于创建独热向量。
a = torch.eye(5)
print(a)
'''
输出:
tensor([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
'''
基于ndarray创建tensor:torch.from_numpy()
- torch.from_numpy(ndarray: np.ndarray)
接收np.ndarray类型的数据,将其转换为tensor并返回。
a = np.random.rand(2, 3)
print(a)
b = torch.from_numpy(a)
print(b)
'''
输出:
[[0.34550967 0.61193832 0.97132549]
[0.33592651 0.08636327 0.62067825]]
tensor([[0.3455, 0.6119, 0.9713],
[0.3359, 0.0864, 0.6207]], dtype=torch.float64)
'''
张量的变形
在深度学习中,张量的形变非常普遍,常用的方法如下。
最朴素的变形方法:reshape()
- torch.reshape(input: Tensor, shape: Sequence)
- tensor.reshape(shape: Sequence) 或 tensor.reshape(*size: int)
顾名思义,这个方法将指定的张量进行变形;值得注意的是,参数中各个维度允许出现-1,表示该维度长度将由其他维度推导得来。
a = torch.rand(2, 4)
b = torch.reshape(a, shape=(4, 2))
print(b.shape)
c = a.reshape(1, -1) # 第二维度长度可以由2*4/1=8计算得来
print(c.shape)
'''
输出:
torch.Size([4, 2])
torch.Size([1, 8])
'''
展平方法:flatten()
- torch.flatten(input: Tensor, start_dim: int=0, end_dim: int=-1)
- tensor.flatten(start_dim: int=0, end_dim: int=-1)
将张量的指定两个维度之间的形状展平。换言之,如果张量形状为$(d_0, d_1, \dots,d_{N-1})$,调用方法flatten(n, m)后,张量形状将变为$(d_0,d_1,\dots,d_{n-1},d_n\times d_{n+1}\times \cdots\times d_{N-m},d_{N-m+1},\dots, d_{N-1})$。
a = torch.rand(2, 2, 4, 3)
b = a.flatten(start_dim=1, end_dim=2)
print(b.shape)
c = a.flatten()
print(c.shape)
'''
输出:
torch.Size([2, 8, 3])
torch.Size([48])
'''
删除维度:squeeze()
- torch.squeeze(input: Tensor, dim: int)
- tensor.squeeze(dim: int)
将张量指定维度删除;该维度长度必须为1,否则会报错。
a = torch.rand(2, 1, 4)
b = a.squeeze(1)
print(b.shape)
'''
输出:
torch.Size([2, 4])
'''
增加维度:unsqueeze()
- torch.unsqueeze(input: Tensor, dim: int)
- tensor.unsqueeze(dim: int)
在张量指定位置新增一个维度,且该新增的维度长度为1。
a = torch.rand(2, 3)
b = a.unsqueeze(2)
print(b.shape)
'''
输出:
torch.Size([2, 3, 1])
'''
另一种变形方法:view()
- tensor.view(*size: int)
这个方法同样可以对张量进行变形,它的用法和reshape几乎一致。
张量的翻转:permute()
- torch.permute(input: Tensor, dims: _size)
- tensor.permute(dims: _size) 或 tensor.permute(*dim: int)
对张量的各个维度进行调整。假设张量$\mathbf{Z}$形状为$(d_0,d_1,\dots,d_{N-1})$,调用permute($c_0,c_1,\dots,c_{N-1}$),则调整后的张量维度为$(d_{c_0},d_{c_1},\dots,d_{c_{N-1}})$,其中$c_0,c_1,\dots,c_{N-1}$为$0,1,\dots,N-1$的一个排列。
a = torch.rand(2, 4, 8, 6)
b = a.permute(0, 2, 3, 1)
print(b.shape)
c = torch.permute(a, (2, 3, 1, 0))
print(c.shape)
'''
输出:
torch.Size([2, 8, 6, 4])
torch.Size([8, 6, 4, 2])
'''
张量的转置:transpose()
- torch.transpose(input: Tensor, dim0: int, dim1: int)
- tensor.transpose(dim0: int, dim1: int)
将张量的两个指定维度进行转置。
a = torch.rand(2, 4, 8)
b = a.transpose(1, 2)
print(b.shape)
'''
torch.Size([2, 8, 4])
'''
张量的拼接
最通用的拼接方法:torch.concat()
- torch.concat(tensors: Tuple[Tensor, …] | List[Tensor], dim: _int = 0)
- torch.concatenate(tensors: Tuple[Tensor, …] | List[Tensor], dim: _int = 0)
tensor_list = []
for i in range(5):
tensor_list.append(torch.rand(2, 3))
# 列表中包含5个形状为(2, 3)的张量
a = torch.concat(tensor_list, dim=0)
print(a.shape)
'''
输出:
torch.Size([10, 3])
'''
特殊的拼接方法:torch.vstack()/torch.hstack()
- torch.vstack(tensors: Sequence[Tensor, …])
- torch.hstack(tensors: Sequence[Tensor, …])
这两个方法是torch.concat()的特殊情况:前者等价于参数dim=0,后者等价于参数dim=1。
tensor_list = []
for i in range(5):
tensor_list.append(torch.rand(2, 1, 3))
# 列表中包含5个形状为(2, 1, 3)的张量
a = torch.vstack(tensor_list) # 等价于torch.concat(tensor_list, dim=0)
print(a.shape)
b = torch.hstack(tensor_list) # 等价于torch.concat(tensor_list, dim=1)
print(b.shape)
'''
输出:
torch.Size([10, 1, 3])
torch.Size([2, 5, 3])
'''
张量的梯度去除与转换
张量从计算图脱离:tensor.detach()
- tensor.detach()
这个方法将张量从计算图中脱离开来,在此后的计算中无需梯度回传。
张量的位置变换:tensor.to()/tensor.cpu()
- tensor.to(device)
- tensor.cpu()
将张量转移到指定位置(cpu内存或者cuda显存),而tensor.cpu()直接将张量移动到内存。
张量转换为ndarray:tensor.numpy()
- tensor.numpy()
将张量转换为np.ndarray类型数组。由于numpy与matplotlib等库有非常方便的接口,因此该方法常用于后续的文件存储、图表绘制等等。不过,要注意该张量必须脱离计算图、位于内存,因此常用以下语句:
# tensor_data为张量
ndarray_data = tensor_data.detach().cpu().numpy()
张量的条件语句
条件选取:torch.where()
- torch.where(condition: Tensor, input: Tensor, other: Tensor) -> Tensor
- torch.where(condition: Tensor) -> List[Tensor]
这个函数类似C语言中的条件语句xxx ? xxx : xxx
:对于满足condition的部分,取input中的对应元素,否则取other中的对应元素。
举个实际例子,比如要编程实现下式
而往往在深度学习中以一个批量(张量)而不是单标量作为输入,记为$\mathbf{X}\in \mathbb{R}^{B}$,故下面的代码可以实现批量运算。
part = torch.rand(8) - 0.5
print(part)
result = torch.where(0 > part, torch.zeros_like(part), part)
print(result)
'''
输出:
tensor([-0.4765, -0.0570, 0.3925, 0.2088, 0.1372, -0.4504, -0.4224, -0.3690])
tensor([0.0000, 0.0000, 0.3925, 0.2088, 0.1372, 0.0000, 0.0000, 0.0000])
'''
张量与求导
张量的导数:tensor.backward()/requires_grad
- tensor.backward()
这个方法用于张量求导:将对基于该张量的需要计算导数的张量(设置requires_grad=True)全部求导数。
举个例子,我们知道对于一维向量$\mathbf{x}\in\mathbb{R}^d$,其内积的导数为
下面通过实际的例子验证一下。
x = torch.randn(size=(1, 6), requires_grad=True) # 一维向量,长度为6,注意要设置requires_grad=True,且这个参数默认为False,必须显式声明
h = x @ x.T # 向量内积
h.backward() # 求导
print('x = {}'.format(x))
print('h = {}'.format(h))
print('dh/dx = {}'.format(x.grad))
'''
输出:
x = tensor([[ 0.8951, 0.2311, 0.7308, 0.7671, -1.4649, 0.3229]],
requires_grad=True)
h = tensor([[4.2275]], grad_fn=<MmBackward0>)
dh/dx = tensor([[ 1.7903, 0.4622, 1.4616, 1.5342, -2.9299, 0.6458]])
'''
torch.no_grad()
通常与with语句连用:
with torch.no_grad():
# ...
with语句块内,不会再对张量进行自动求导,这样会大大节省内存。
模型的保存与加载
通常有两种方案对模型进行保存与加载。在下面的方案介绍中,Model为torch内置的模型类,model为该类的实例,path为模型存储路径。
方案一:仅保存模型参数(状态字典)
- 模型保存:torch.save(model.state_dict(), path)
- 模型加载:
- model = Model(*params)
- model.load_state_dict(torch.load(path))
方案二:保存整个模型
- 模型保存:torch.save(model, path)
- 模型加载:
- import Model(一定要先导入模型类)
- model = torch.load(path)
加载模型后,建议立即调用model.eval(),将模型设置为测试模式。