常用库函数
1.1 torch.flatten(input, start_dim=0, end_dim=-1):展开start_dim到end_dim之间的dim成一维
1.2 [einops][https://ggcgarciae.github.io/einops/2-einops-for-deep-learning/].rearrange(element, pattern):贼强,用高级pattern指导张量变换
torch.cuda.amp
自动混合精度:FloatTensor & HalfTensor
安装
使用
torch.jit.script
将模型从纯Python程序转换为能够独立于Python运行的TorchScript程序
[torch.nn.DataParallel & DistributedDataParallel][https://blog.csdn.net/kuweicai/article/details/120516410]
DP和DDP都是实现数据并行方式的分布式训练,主要区别如下:
- DP是单进程多线程,DDP是采用多进程(有进程通信)
- DP只能在单机上使用,DDP单机和多机都可以使用
- DDP相比于DP训练速度要快
- DP的架构是有一个main GPU,然后一对多通信&同步其他子GPU,全程负责将切分的数据和复制的模型发布到子GPU上,通信时间和卡数目成正比,DDP的通信结构是环状worker,从一开始就分配给每个进程独立的获取数据&构建模型任务,每个GPU接收的数据量&信息量恒定,通信成本恒定([reference][https://blog.csdn.net/qiumokucao/article/details/120179961])
DP使用
- 简单的单机多卡,forward pass在多卡上做,然后汇总的主卡,模型更新只在主卡上做,然后再分发到各子GPU
- GPU利用率低
- 只需要给single model做一个封装
1
net = torch.nn.DataParallel(model,device_ids=[0,1,2])
[DDP使用][https://zhuanlan.zhihu.com/p/107139605]
每个batch以后,分发模型权重,太麻烦了,可以考虑同步梯度,把所有卡的loss同步,然后各自梯度更新(需要设置同样的seed)就好了——【重复计算好多遍】 要快过 【io通信分发】
相对复杂,核心配置参数:
- group:进程组,默认情况下,只有一个组
- world size:全局进程个数,如果是多机多卡就表示机器数量,如果是单机多卡就表示 GPU 数量
- rank:进程号,多机多卡表示机器编号,单机多卡表示 GPU编号
- local_rank:进程内GPU 编号
两种代码封装方式
- spawn
- launch:一般看到的都是torch.distributed.launch
torch.distributed.launch
用法:python3 -m torch.distributed.launch [—usage] single_training_script.py [—training_script_args]
1
2
3[-h] [--nnodes NNODES] [--node_rank NODE_RANK]
[--nproc_per_node NPROC_PER_NODE] [--master_addr MASTER_ADDR]
[--master_port MASTER_PORT] [--use_env] [-m] [--no_python]
* -h/--help:查看帮助
* --nnodes NNODES:节点数
* --node_rank NODE_RANK:当前节点的rank
* --nproc_per_node NPROC_PER_NODE:每个节点的GPU数量
* --master_addr MASTER_ADDR:node 0的IP/host name,单机多卡时候就是127.0.0.1
* --master_port MASTER_PORT:node 0的free port,用来节点间通信
* --use_env:读取环境变量的LOCAL_RANK,然后用来传递local rank
* -m:类似python -m,如果single_training_script.py被打包成python module了,可以-m调用
* --no_python:用不上
* 查看帮助:python -m torch.distributed.launch --help
指定GPU
在代码里面指定
1
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
在命令行运行脚本/文件时指定
1
2CUDA_VISIBLE_DEVICES=0,1 python3 train.py
CUDA_VISIBLE_DEVICES=0,1 sh run.sh在sh脚本中指定
1
2
3source bashrc
export CUDA_VISIBLE_DEVICES=gpu_ids && python3 train.py # 两个命令
CUDA_VISIBLE_DEVICES=gpu_ids python3 train.py # 1个命令优先级:代码>命令>脚本
============================== 分隔符 ================================
.cuda()指定
1
2
3model.cuda(gpu_id) # 只能指定一张显卡
model.cuda('cuda:'+str(gpu_ids)) # 可以多卡
model.cuda('cuda:1,2')torch.cuda.set_device()指定
1
2torch.cuda.set_device(gpu_id) # 单卡
torch.cuda.set_device('cuda:'+str(gpu_ids)) # 可指定多卡优先级:.cuda() > torch.cuda.set_device()
============================== 分隔符 ================================
另外分隔符上下两种指定方式,指定的GPU设备的效果,会叠加:
1
2
3
4
5
6
7# run shell
CUDA_VISIBLE_DEVICES=2,3,4,5 python3 train.py
# 代码内部
model.cuda(1)
loss.cuda(1)
tensor.cuda(1)- 此时代码会运行在GPU3上,因为首先指定GPU 2 3 4 5作为VISIBLE_DEVICES,内部编号0 1 2 3,然后在代码内部指定1号卡,也就是外部的3号
推荐os.environ[‘CUDA_VISIBLE_DEVICES’] = ‘0’ 方式,童叟无欺
随机种子
为了保证每次训练的可复现性,在程序开始的时候固定torch的随机种子,同时也把numpy的随机种子固定
1
2
3
4
5
6np.random.seed(0)
torch.manual_seed(0)
torch.cuda.manual_seed_all(0)
torch.backends.cudnn.deterministic = True # 每次卷积计算算法固定
torch.backends.cudnn.benchmark = False # 同上,组合使用多卡同步 BN
默认情况下,各卡用各自的数据独立计算均值和标准差
因为最开始的任务mini-batch够大
数据同步通信浪费时间
- 【QUESTION】一个疑问,滑动平均最终都是近似样本均值的吧,是不是只影响训练初期的收敛速度啊,和精度有直接影响吗??【一个解释】因为总体的数据会切分,然后分配给每个卡,这样多卡的情况下,其实不能完全保证一张卡是跑过全集的,所以可能导致每个 GPU 过拟合自己那份数据
同步BN用所有卡的数据一起计算均值和标准差,BP的时候计算全局梯度,对检测任务提升较大
1
2sync_bn = torch.nn.SyncBatchNorm(num_features, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True)张量
基本属性
1
2
3
4tensor = torch.randn(3,4,5)
print(tensor.type()) # 数据类型
print(tensor.size()) # 张量的shape,是个元组
print(tensor.dim()) # 维度的数量轴命名 & 替代axis-index
1
2
3
4
5
6
7
8
9
10
11
12
13
14# Tensor[N, C, H, W]
images = torch.randn(32, 3, 56, 56)
images.sum(dim=1)
images.select(dim=1, index=0)
# PyTorch 1.3之后
NCHW = [‘N’, ‘C’, ‘H’, ‘W’]
images = torch.randn(32, 3, 56, 56, names=NCHW)
images.sum('C')
images.select('C', index=0)
# 也可以这么设置
tensor = torch.rand(3,4,1,2,names=('C', 'N', 'H', 'W'))
# 使用align_to可以对维度方便地排序
tensor = tensor.align_to('N', 'C', 'H', 'W')数据类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 设置默认类型,pytorch中的FloatTensor远远快于DoubleTensor
torch.set_default_tensor_type(torch.FloatTensor)
# 类型转换
tensor = tensor.cuda() # cuda类型的tensor仅用于在GPU上进行计算,不能与其他类型混用
tensor = tensor.cpu() # cpu类型的tensor可以与ndarrray/PIL.Image自由转换
tensor = tensor.float()
tensor = tensor.long()
# ndarray
ndarray = tensor.cpu().numpy()
tensor = torch.from_numpy(ndarray).float()
tensor = torch.from_numpy(ndarray.copy()).float()
# PIL.Image
image = PIL.Image.fromarray(torch.clamp(tensor*255, min=0, max=255).byte().permute(1,2,0).cpu().numpy()) # byte()=uint8(), char()=int8(), [C,H,W]->[H,W,C]
image = torchvision.transforms.functional.to_pil_image(tensor)
tensor = torch.from_numpy(np.asarray(PIL.Image.open(path))).permute(2,0,1).float() / 255 # 0-1的f32, [C,H,W]
tensor = torchvision.transforms.functional.to_tensor(PIL.Image.open(path))
# scalar
value = torch.rand(1).item()张量基本操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 负步长,pytorch不支持tensor[::-1]这样的负步长操作,需要通过张量索引实现
tensor = tensor[:,:,:,torch.arange(tensor.size(3) - 1, -1, -1).long()] # [N,C,H,W] 水平翻转
# 复制张量
tensor.clone() # new memory, still in computation graph
tensor.detach() # shared memory, not in computation graph
tensor.detach.clone()() # new memory, not in computation graph
# 张量比较
torch.allclose(tensor1, tensor2) # float tensor
torch.equal(tensor1, tensor2) # int tensor
# 矩阵乘法
# Matrix multiplcation: (m*n) * (n*p) -> (m*p).
result = torch.mm(tensor1, tensor2)
# Batch matrix multiplication: (b*m*n) * (b*n*p) -> (b*m*p)
result = torch.bmm(tensor1, tensor2)
# Element-wise multiplication: (m*n) * (m*n) -> (m*n)
result = tensor1 * tensor2
result = torch.mul(tensor1, tensor2)
# xjb乘之matmul: 不限定输入几维矩阵,始终后两维进行矩阵乘法,前面的维度broadcast
a = torch.ones(2,1,3,4)
b = torch.ones(5,4,2)
c = torch.matmul(a,b) # torch.Size([2,5,3,2])
数据集Dataset, DataLoader
torch.utils.data.Dataset:Dataset可以理解为一个list,上层调用时候会传给他一个index,dataset则复制读取、变换、预处理指定文件,返回一个(input_x, target_y)-pair,主体结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class CustomDataset(torch.utils.data.Dataset):
def __init__(self):
# TODO
# 1. Initialize file path or list of file names.
pass
def __getitem__(self, index):
# TODO
# 1. Read one data from file (e.g. using numpy.fromfile, PIL.Image.open).
# 2. Preprocess the data (e.g. torchvision.Transform).
# 3. Return a data pair (e.g. image and label).
#这里需要注意的是,第一步:read one data,是一个data
pass
def __len__(self):
# You should change 0 to the total size of your dataset.
return 0
# Dataset的长度代表样本量
# DataLoader的长度代表batch steps
torch.utils.data.DataLoader:DataLoader是真正对接模型这一层,负责整合batch data,同时调整采样策略、workers、shuffle等一系列设置,用如下参数将其实例化(加粗为常用):
- dataset(Dataset): 传入的数据集
- batch_size(int, optional): 每个batch有多少个样本
- shuffle(bool, optional): 在每个epoch开始的时候,对数据进行重新排序
- sampler(Sampler, optional): 自定义从数据集中取样本的策略,如果指定这个参数,那么shuffle必须为False
- batch_sampler(Sampler, optional): 与sampler类似,但是一次只返回一个batch的indices(索引),需要注意的是,一旦指定了这个参数,那么batch_size,shuffle,sampler,drop_last就不能再制定了(互斥——Mutually exclusive)
- num_workers (int, optional): 这个参数决定了有几个进程来处理data loading。0意味着所有的数据都会被load进主进程。(默认为0)
- collate_fn (callable, optional): 将一个list的sample组成一个mini-batch的函数
- pin_memory (bool, optional): 如果设置为True,那么data loader将会在返回它们之前,将tensors拷贝到CUDA中的固定内存(CUDA pinned memory)中
- drop_last (bool, optional): 如果设置为True:这个是对最后的未完成的batch来说的,比如你的batch_size设置为64,而一个epoch只有100个样本,那么训练的时候后面的36个就被扔掉了。 如果为False(默认),那么会继续正常执行,只是最后的batch_size会小一点
- timeout(numeric, optional):如果是正数,表明等待从worker进程中收集一个batch等待的时间,若超出设定的时间还没有收集到,那就不收集这个内容了。这个numeric应总是大于等于0。默认为0
- worker_init_fn (callable, optional): 每个worker初始化函数 If not None, this will be called on eachworker subprocess with the worker id (an int in [0, num_workers - 1]) as input, after seeding and before data loading. (default: None)
采样器Sampler
所有的采样器都继承自torch.utils.data.sampler
1
2
3
4
5
6
7
8
9
10
11
12
13
14class SequentialSampler(Sampler):
r"""Samples elements sequentially, always in the same order.
Arguments:
data_source (Dataset): dataset to sample from
"""
# 产生顺序 迭代器
def __init__(self, data_source):
self.data_source = data_source
def __iter__(self):
return iter(range(len(self.data_source))) # 主要区别在这里
def __len__(self):
return len(self.data_source)
已有Sampler
* SequentialSampler(data_source):按顺序采集,data_source可以是一个Dataset,返回一个indices的生成器
- RandomSampler(data_source, replacement=False, num_samples=None):随机、有/无放回、采集指定数目的样本
- SubsetRandomSampler(indices):无放回采样,就是打乱全集,是RandomSampler的懒人常用写法
- WeightedRandomSampler(weights, num_samples, replacement=True):也是RandomSampler的衍生,样本带了权重
- BatchSampler(sampler, batch_size, drop_last):将以上Sampler包装成批索引返回
- RandomSampler(data_source, replacement=False, num_samples=None):随机、有/无放回、采集指定数目的样本
模型
nn.Module:定义模型时继承的基类,因为基类封装了train/eval/梯度回传等高级功能,就相当于keras的Model类
- 自定义层、模型、Loss都是继承这个类
迭代模型的所有子层:
1
2
3
4
5
6
7
8for layer in model.modules(): # 返回所有子层
if isinstance(layer, torch.nn.Conv2d):
torch.nn.init.kaiming_normal_(layer.weight, mode='fan_out',
nonlinearity='relu')
for layer in model.named_modules(): # 返回所有的[名字,子层]pairs
if isinstance(layer[1],nn.Conv2d):
conv_model.add_module(layer[0],layer[1])model(x) 前用 model.train() 和 model.eval() 切换网络状态
nn.ModuleList:是个List,可以把任意 nn.Module 的子类加入到这个List,而且是会自动注册到整个网络上(在computation graph上),但是用普通的python List定义则不会真正添加进网络结构里(应该跟局部定义域有关吧)
- module的执行顺序根据 forward 函数来决定
- 一个module可以在 forward 函数中被调用多次,但是参数共享
nn.Sequential:它更进一步,已经在内部实现了forward方法——定义即实现,必须按照层顺序去定义
nn.Xxx & nn.functional.xxx:如nn.Conv2d和nn.functional.conv2d,这就类似keras.Layer.Conv2d和tf.nn.conv2d,一个是封装的层,需要实例化使用,一个是函数借口,直接使用但是要传入参数
模型参数量:torch.numel
1
2total_parameters = sum(torch.numel(p) for p in model.parameters())
trained_parameters = sum(torch.numel(p) for p in model.parameters() if p.requires_grad)模型参数:
1
2
3
4
5
6
7
8
9
10model.parameters() # 生成器
model.state_dict() # dict
model.load_state_dict(torch.load('model.pth'), strict=False)
# 模型参数量:torch.numel
sum_parameters = sum(torch.numel(parameter) for parameter in model.parameters())
# 浮点运算次数:GFLOPs
model.layers[0].flops() / 1e9- 1个special case:BN层,在调用.parameters()方法的时候,可以看到BN层只有两个参数,但是实际上还有running mean & running std,这两个变量严格来说不算网络参数,而是一个数值统计,所以在state_dict()里面可以看到
以较大学习率微调全连接层,较小学习率微调卷积层
1
2
3
4
5
6model = torchvision.models.resnet18(pretrained=True)
finetuned_parameters = list(map(id, model.fc.parameters()))
conv_parameters = (p for p in model.parameters() if id(p) not in finetuned_parameters)
parameters = [{'params': conv_parameters, 'lr': 1e-3},
{'params': model.fc.parameters()}]
optimizer = torch.optim.SGD(parameters, lr=1e-2, momentum=0.9, weight_decay=1e-4)
pytorch-summary