PyTorch技巧(持续更新)
查看模型每层输出详情
from torchsummary import summary
summary(your_model, input_size=(channels, H, W))
input_size是根据自己的网络模型的输入尺寸进行设置
梯度裁剪(Gradient Clipping)
import torch.nn as nn
outputs = model(data)
loss = loss_fn(outputs, target)
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=20, norm_type=2)
optimizer.step()
nn.utils.clip_grad_norm_的参数:
- parameters 一个基于变量的迭代器,会进行梯度归一化
- max_norm 梯度的最大范数
- norm_type 规定范数的类型,默认为2
扩展单张图片的维度
view()实现
import cv2
import torch
image = cv2.imread(img_path)
image = torch.tensor(image)
print(image.size)
img = image.view(1, *image.size())
print(img.size())
np.newaxis实现
import cv2
import torch
image = cv2.imread(img_path)
print(image.shape)
img = image[np.newaxis, :, :, :]
print(img.shape)
unsqueeze()实现
import cv2
import torch
iamge = cv2.imread(img_path)
image = torch.tensor(image)
print(img.size())
img = image.unsqueeze(dim=0)
print(img.size())
独热编码
在PyTorch中使用交叉熵损失函数的时候会自动把label转换成onehot,所以不用手动转化,而使用MSE需要手动转化成onehot编码
import torch.nn.functional as F
import torch
tensor = torch.arange(0, 5)
one_hot = F.one_hot(tensor)
# 输出:
# tensor([[1, 0, 0],
# [0, 1, 0],
# [0, 0, 1],
# [1, 0, 0],
# [0, 1, 0]])
F.one_hot会自己检测不同类别个数,生成对应独热编码,我们也可以自己指定类别数:
tensor = torch.arange(0, 5) % 3 # tensor([0, 1, 2, 0, 1])
one_hot = F.one_hot(tensor, num_classes=5)
# 输出:
# tensor([[1, 0, 0, 0, 0],
# [0, 1, 0, 0, 0],
# [0, 0, 1, 0, 0],
# [1, 0, 0, 0, 0],
# [0, 1, 0, 0, 0]])
防止验证模型时爆显存
验证模型时不需要求导,即不需要梯度计算,关闭autograd,可以提高速度,节约内存,如果不关闭可能会爆显存
with torch.no_grad():
# 使用model进行预测的代码
pass
对于使用torch.cuda.empty_cache()
的原因,是因为随着Pytorch的训练,无用的临时变量可能会越来越多,导致out of memory
。
意思就是Pytorch的缓存分配器会事先分配一些固定的显存,即使实际上tensors并没有使用完这些显存,这些显存也不能被其他应用使用。这个分配过程由第一次CUDA内存访问触发的。
而torch.cuda.empty_cache()
的作用就是释放缓存分配器当前持有的且未占用的缓存显存,以便这些显存可以被其他GPU应用程序中使用,注意使用此命令不会释放tensors占用的显存。
监控工具
sudo apt-get install htop #监控内存(-d为更新频率)
htop -d=0.1
watch -n 0.1 nvidia-smi #监控显存(-n为更新频率,每0.1s更新一次)
Pytorch-Memory-Utils监控显存占用
显存占用
显存占用=模型参数+计算产生的中间变量
减少显存占用的方法
- inplace替换
- 用del一边计算一边清除中间变量
- 减少batch_size,避免用全连接,多用下采样
- 因为每次迭代都会引入点临时变量,会导致训练速度越来越慢,基本呈线性增长。但是如果周期性的使用torch.cuda.empty_cache()的话就可以解决这个问题。
冻结某些层的参数
在加载预训练模型的时候,我们有时想冻结前面几层,使其参数在训练过程中不发生变化。
我们需要先知道每一层的名字,通过以下代码打印:
net = Network() #获取自定义网络结构
for name, value in net.named_parameters():
print('name: {0},\t grad: {1}'.format(name, value.requires_grad))
假设前几层信息如下:
name: cnn.VGG_16.convolution1_1.weight, grad: True
name: cnn.VGG_16.convolution1_1.bias, grad: True
name: cnn.VGG_16.convolution1_2.weight, grad: True
name: cnn.VGG_16.convolution1_2.bias, grad: True
name: cnn.VGG_16.convolution2_1.weight, grad: True
name: cnn.VGG_16.convolution2_1.bias, grad: True
name: cnn.VGG_16.convolution2_2.weight, grad: True
name: cnn.VGG_16.convolution2_2.bias, grad: True
后面的True表示该层的参数可训练,然后我们定义一个要冻结的层的列表
no_grad = [
'cnn.VGG_16.convolution1_1.weight',
'cnn.VGG_16.convolution1_1.bias',
'cnn.VGG_16.convolution1_2.weight',
'cnn.VGG_16.convolution1_2.bias'
]
冻结方法如下:
net = Net.CTPN() # 获取网络结构
for name, value in net.named_parameters():
if name in no_grad:
value.requires_grad = False
else:
value.requires_grad = True
最后在定义优化器时,只对requires_grad为True的层的参数进行更新
optimizer = optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=0.01)
显式指定model.train()和model.eval()
我们的模型中经常会有一些子模型,其在训练时候和测试时候的参数是不同的,比如dropout中的丢弃率和Batch Normalization中的
γ
\gamma
γ和
β
\beta
β等,这时我们需要显式地指定不同阶段,在pytorch中我们通过model.train()
和model.eval()
进行显式指定(因为BN的running_mean等并非nn.Parameter,用requires_grad冻不住,需要调用BN的.eval())
对不同层使用不同学习率
以如下模型为例:
net = Network() # 获取自定义网络结构
for name, value in net.named_parameters():
print('name: {}'.format(name))
# 输出:
# name: cnn.VGG_16.convolution1_1.weight
# name: cnn.VGG_16.convolution1_1.bias
# name: cnn.VGG_16.convolution1_2.weight
# name: cnn.VGG_16.convolution1_2.bias
# name: cnn.VGG_16.convolution2_1.weight
# name: cnn.VGG_16.convolution2_1.bias
# name: cnn.VGG_16.convolution2_2.weight
# name: cnn.VGG_16.convolution2_2.bias
对convolution1和convolution2设置不同的学习率,首先将它们分开,放到不同的列表中:
conv1_params = []
conv2_params = []
for name, params in net.named_parameters():
if "convolution1" in name:
conv1_params += [params]
else:
conv2_params += [params]
# 然后在优化器中进行如下操作:
optimizer = optim.Adam(
[
{"params": conv1_params, 'lr':0.01},
{"params": conv2_params, 'lr':0.001},
],
weight_decay = 1e-3
)
我们将模型划分为两部分,存放到一个列表里,每部分就对应上面的一个字典,在字典里设置不同的学习率,当着两部分有相同的其他参数时,就将参数放到列表外面作为全局参数,如上面的‘weight_decay’。
也可以在列表外设置一个全局学习率,当各部分字典里设置了局部学习率时,就使用该学习率,否则就使用列表外的全局学习率。
retain_graph使用
在对一个损失进行反向传播时,在pytorch中调用out.backward()即可实现,
对loss进行反向传播就可以求得损失函数对于学习参数的梯度,在.backward()
中,
backward(gradient=None, retain_graph=None, create_graph=False)
这里我们关注的是retain_graph
这个参数,这个参数如果为False或者None
则在反向传播完后,就释放掉构建出来的graph,如果为True则不对graph进行释放。
但是我们已经计算完梯度了,为什么还要保存graph呢?这里举个例子,比如在对抗生成网络GAN中需要先对某个模块比如生成器进行训练,后对判别器进行训练,这个时候整个网络就会存在两个以上的loss,
G_loss = ...
D_loss = ...
opt.zero_grad() #对所有梯度清0
D_loss.backward(retain_graph=True) #保存graph结构,后续使用
opt.step() # 更新梯度,只更新D的梯度,因为只有D的梯度不为0
opt.zero_grad() # 对所有梯度清0
G_loss.backward(retain_graph=False) #不保存graph结构,可以释放graph
# 下一个迭代中通过forward还可以build出来
opt.step() #更新梯度,只更新G的,因为只有G的不为0
这个时候就可以对网络中多个loss进行分步训练了。