一、参考资料
PyTorch官方源码:GitHub - pytorch/pytorch: Tensors and Dynamic neural networks in Python with strong GPU acceleration
PyTorch零基础入门教程,CSDN专栏
PyTorch 专栏,CSDN专栏
二、相关介绍
1. 静态图
为了加速计算,一些框架会使用对神经网络“先编译,后执行”的静态图来描述网络。静态图的缺点是难以描述控制流(比如 if-else 分支语句和 for 循环语句),直接对其引入控制语句会导致产生不同的计算图。比如循环执行 n 次 a=a+b,对于不同的 n,会生成不同的计算图。
2. 推理模式
像dropout
或batchnorm
这样的运算符在推断和训练模式下的行为会有所不同。
# 将模型转换为推理模式
torch_model.eval()
torch_model.train(False)
4. ATen 算子
ATen 是 PyTorch 内置的 C++ 张量计算库,PyTorch 算子在底层绝大多数计算都是用 ATen 实现的。
5. Graph
Graph 拥有许多的 Node,这些 Node 由一个 Block 管理。所有 Node 组织成双向链表的形式,方便插入删除,其中返回值节点“Return Node”会作为这个双向链表的“哨兵”。双向链表通常会被拓扑排序,保证执行的正确性。
6. Block
编译原理中,Block基本块表示一系列不包含任何跳转指令的指令序列,由于基本块内的内容可以保证是顺序执行的,因此很多的优化都会以基本块作为前提。
Block
表示一个 Node 的有序列表,代表输入的 Node 的kind=Param
,代表输出的 Node 的kind=Return
。
实际上 Graph 本身隐含一个 root Block 对象,用来管理所有的 Node。部分 Node 可能还会存在 sub Block。
7. pass
TorchScript 解读(二):Torch jit tracer 实现解析
pass
是一个来源于编译原理的概念,一个 TorchScript 的 pass 会接收一种中间表示(IR),遍历图中所有元素进行某种变换,生成满足某种条件的新 IR。
TorchScript 中定义了许多 pass 来优化 Graph。比如对于常规编译器很常见的 DeadCodeElimination(DCE),CommonSubgraphElimination(CSE)等等;也有一些针对深度学习的融合优化,比如 FuseConvBN 等;还有针对特殊任务的 pass,ONNX 的导出就是其中一类 pass。
三、分布式训练
在分布式模式下,需要在每个 epoch 开始时调用 set_epoch()
方法,然后再创建 DataLoader迭代器,以使 shuffle
操作能够在多个 epoch 中正常工作。 否则,dataloader迭代器产生的数据将始终使用相同的顺序。
sampler = DistributedSampler(dataset) if is_distributed else None
loader = DataLoader(dataset, shuffle=(sampler is None),
sampler=sampler)
for epoch in range(start_epoch, n_epochs):
if is_distributed:
sampler.set_epoch(epoch)
train(loader)
测试代码
Pytorch DistributedDataParallel 数据采样 shuffle - 知乎 (zhihu.com)
数据长度16,两张卡,每张卡8个数据,batch size 2,两个epoch,一个gpu 4次输出为一个 epoch。可以看到 cuda 1 和 cuda 0的结果是重复的。
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
torch.distributed.init_process_group(backend="nccl")
input_size = 5
output_size = 2
batch_size = 2
data_size = 16
local_rank = torch.distributed.get_rank()
torch.cuda.set_device(local_rank)
device = torch.device("cuda", local_rank)
class RandomDataset(Dataset):
def __init__(self, size, length, local_rank):
self.len = length
self.data = torch.stack([torch.ones(5), torch.ones(5)*2,
torch.ones(5)*3,torch.ones(5)*4,
torch.ones(5)*5,torch.ones(5)*6,
torch.ones(5)*7,torch.ones(5)*8,
torch.ones(5)*9, torch.ones(5)*10,
torch.ones(5)*11,torch.ones(5)*12,
torch.ones(5)*13,torch.ones(5)*14,
torch.ones(5)*15,torch.ones(5)*16]).to('cuda')
self.local_rank = local_rank
def __getitem__(self, index):
return self.data[index]
def __len__(self):
return self.len
dataset = RandomDataset(input_size, data_size, local_rank)
sampler = DistributedSampler(dataset)
rand_loader = DataLoader(dataset=dataset,
batch_size=batch_size,
sampler=sampler)
e = 0
while e < 2:
t = 0
# 设置set_epoch(),可实现每次epoch每个GPU拿到的数据不同
# sampler.set_epoch(e)
for data in rand_loader:
print(data)
e+=1
四、新特性
8比特优化器
一训练就显存爆炸?Facebook 推出 8 比特优化器,两行代码拯救你的显存!
超大模型训练
五、PyTorch库
1. TorchServe
TorchServe 是 PyTorch 中将模型部署到生产环境的首选解决方案。它是一个性能良好且可扩展的工具,用 HTTP 或 HTTPS API 封装模型。
七、常用API
torch.nn.Module.register_buffer()
PyTorch nn.Module中的self.register_buffer()解析
功能:训练不更新,最后可保存。定义一组参数,该组参数的特别之处在于,模型训练时不会更新(即调用 optimizer.step() 后该组参数不会变化,只可人为地改变它们的值),但是保存模型时,该组参数又作为模型参数不可或缺的一部分被保存。
import torch.nn as nn
import torch
class net(nn.Module):
def __init__(self):
super(net,self).__init__()
self.register_buffer("a",torch.ones(2,3)) #从此,self.a其实就是torch.ones(2,3)。
def forward(self,x):
return x+self.a #使用
解释:
register_buffer的作用是将 torch.ones(2,3)
这个tensor注册到模型的 buffers()
属性中,并命名为a,这代表a对应的是一个 持久态,不会梯度更新,但是能被模型的state_dict记录下来。可以理解为模型的常数。
注意,没有保存到模型的 buffers() 或 parameters() 属性中的参数是不会被记录到state_dict中的,在 buffers() 中的参数默认不会有梯度,parameters() 中的参数默认有梯度。
requires_grad=False
不会注册到模型参数中model.parameters()
会注册到模型model.state_dict()中
torch.onnx.export()
torch.onnx.export
中需要的模型实际上是一个torch.jit.ScriptModule
。
trace跟踪模式通过实际运行一遍模型的方法导出模型的静态图,即无法识别出模型中的控制流(如循环);script脚本模式则能通过解析模型来正确记录所有的控制流。
import torch
import torchvision
def export_onnx():
model = torchvision.models.mobilenet_v2()
model_name = "mobilenet_v2"
model.eval() # 若存在batchnorm、dropout层则一定要eval()!!!!再export
BATCH_SIZE = 1
dummy_input = torch.randn(BATCH_SIZE, 3, 224, 224)
# 保存trace模型
model_trace = torch.jit.trace(model, dummy_input)
model_trace.save(f"{model_name}.pt")
model_onnx = torch.jit.load(f"{model_name}.pt")
model_onnx.eval()
# 保存onnx模型
torch.onnx.export(model_onnx,
dummy_input,
f"{model_name}.onnx",
opset_version=13,
do_constant_folding=True,
input_names=["input_0"],
output_names=["output_0"]
)
if __name__ == '__main__':
export_onnx()
import torch
class Model(torch.nn.Module):
def __init__(self, n):
super().__init__()
self.n = n
self.conv = torch.nn.Conv2d(3, 3, 3)
def forward(self, x):
for i in range(self.n):
x = self.conv(x)
return x
models = [Model(2), Model(3)]
model_names = ['model_2', 'model_3']
for model, model_name in zip(models, model_names):
dummy_input = torch.rand(1, 3, 10, 10)
dummy_output = model(dummy_input)
# 跟踪模式与直接 torch.onnx.export(model, ...)等价
model_trace = torch.jit.trace(model, dummy_input)
torch.onnx.export(model_trace, dummy_input, f'{model_name}_trace.onnx', example_outputs=dummy_output)
# 脚本模式必须先调用 torch.jit.script
model_script = torch.jit.script(model)
torch.onnx.export(model_script, dummy_input, f'{model_name}_script.onnx', example_outputs=dummy_output)
scatter()
target.scatter(dim, index, src)
按照指定的dim轴方向和index对应位置关系,用src张量中的元素逐个映射到target张量中的元素。
参数解释
target
:即目标张量;src
:即源张量,将该张量上的元素逐个映射到目标张量上;dim
:指定轴方向,定义了填充方式。对于二维张量,dim=0
表示逐列进行行填充,而dim=1
表示逐列进行行填充;index
: 按照轴方向,在target
张量中需要填充的位置;
import torch
a = torch.arange(10).reshape(2,5).float()
b = torch.zeros(3, 5))
b_= b.scatter(dim=0, index=torch.LongTensor([[1, 2, 1, 1, 2], [2, 0, 2, 1, 0]]),src=a)
print(b_)
# tensor([[0, 6, 0, 0, 9],
# [0, 0, 2, 8, 0],
# [5, 1, 7, 0, 4]])
整个函数的操作过程见下面的示意图。因为设定了dim=0
,所以会逐列将source
中的元素按照index
中的位置信息,放入target
张量中。
scatter函数的一个典型应用就是在分类问题中,将目标标签转换为one-hot编码形式,如:
labels = torch.LongTensor([1,3])
targets = torch.zeros(2, 5)
targets.scatter(dim=1, index=labels.unsqueeze(-1), src=torch.tensor(1))
# 注意dim=1,即逐样本的进行列填充
# 返回值为 tensor([[0, 1, 0, 0, 0],
# [0, 0, 0, 1, 0]])
squeeze()
pytorch学习 中 torch.squeeze() 和torch.unsqueeze()的用法
功能:压缩维度,删除维度为1的维度。
import torch
"""
a = tensor([[[[1.],
[1.]]]])
"""
a = torch.ones([1, 1, 2, 1]) # 维度(1, 1, 2, 1)
# 删掉a中所有维度为1的,不为1的维度没有影响
"""
a = tensor([1., 1.])
"""
a = torch.squeeze(a) # 维度(2, )
"""
b = tensor([[[[1.],
[1.]]]])
"""
b = torch.ones([1, 1, 2, 1]) # 维度(1, 1, 2, 1)
"""
tensor([[[1.],
[1.]]])
"""
# 如果第N维度为1,则删除该维度
b = b.squeeze(0) # 维度(1, 2, 1)
"""
tensor([[1.],
[1.]])
"""
# 如果第N维度为1,则删除该维度
b = torch.squeeze(b, 0) # 维度(2, 1)
unsqueeze()
功能:在指定位置扩充维度。
函数原型:unsqueeze(dim)
import torch
# tensor([3])
a = torch.tensor([3]) # 维度 (1,)
# tensor([[3]])
# 在a的0位置加上一个维度为1的维度
a = a.unsqueeze(0) # 维度 (1, 1)
# tensor([[[3]]])
# 在a的0位置加上一个维度为1的维度
b = torch.unsqueeze(a, 0) # 维度 (1, 1, 1)
torch.cuda
# Use the GPU if there is one, otherwise CPU
device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.eval()
在模型推理之前必须调用 model.eval()
,将 dropout 和 BN层设置为eval模式。如果不这样做,将会产生不一致的推断结果。
# 保存模型
torch.save(model.state_dict(), '/PATH/TO/params.pt')
# 加载模型
# 实例化model模型,重构模型结构
model = My_model(*args, **kwargs)
# 根据模型结构,加载模型参数
model.load_state_dict(torch.load('/PATH/TO/params.pt'))
model.eval()