PyTorch修炼手册
其实PyTorch这个框架非常简单,没有对比就没有伤害,大多数的语法也非常python。如果你已了解numpy的一些语法,那么上手会比较快;如果你是和我曾经一样的小白也不要紧,跟住文章来理解他是如何处理数据的,就明白背后创作者的思路了。在下面的文章里面,我们将从下面的几点去了解,学习pytorch这个深度学习框架:
- 我们应该怎么从头开始搭建一个网络,我们会用到什么pytorch的哪些工具,最后我们要怎么用这个网络,他的输入输出是什么,我们的数据要怎么处理;
- 在知道怎么使用pytorch去搭建一个神经网络之后,我们还需要注意什么,有什么使用的小技巧;
- 最后我们将学习一些高级一点的方法去优化我们网络;
几个小问题?
问题一:遇到不会的函数怎么办?
英文手册 传送门;中文手册推荐使用 传送门
问题二:有推荐的书吗,我想看书?
最近官方出了教程书籍,不看他看谁的? 有下载地址吗?想白嫖不点赞?行:非洲传送门(提取码:zk94 )
问题三:pytorch版本?
不同版本的pytorch里面的有些功能可能会不太一样,文章中的代码适用于pytorch0.4.1以上的版本。
PyTorch常见小问题
- 通道问题:不同的视觉库对于图像读取的方式不一样,图像的通道也不一样,这是一个很基础视觉问题。像是opencv的默认imread就是
H x W x C
,Pytorch的Tensor为C X H X W
,TensorFlow的两种都支持的。 - pytorch的数据形式:我们都知道高维的数据我们就把它称之为张量,在TensorFlow和pytorch里面都是称为tensor;pytorch0.4.1版本的tensor有三个属性:
tensor
数据本身,device
数据所在的设备(GPU或者是CPU),grad_fn
指向Function对象,用于反向传播的梯度计算之用,下面取出一个在训练中的张量tensor([-0.1621, 0.2753, -0.8891, ..., 0.8509, 0.3875, 0.0798],device='cuda:0', grad_fn=<GatherBackward>)
,device='cuda:0'
这里表示的是数据是在第一张显卡当中的,如果数据被存放在内存中的话则是cpu。 - tensor的转换:pytorch中的数据形式是tensor,如果像是numpy,pandas,python的list中的数据要放到神经网络中使用的话,需要将其先做转换,比如numpy数据转tensor:
b = torch.from_numpy(a)
,这在tensorFlow中也是一样的,具体的指令就得再去百度下了。
理解PyTorch的框架(基于Train的分析)
对于当时懵懂的我来说,图像的输入在哪?该如何将数据和网络结合,来训练一个自己的网络?
在pytorch中就只需要分三步,1.写好网络,2.编写数据的标签和路径索引,3.把数据送到网络。
1.1 PyTorch模型—网络架构
- 神经元零件进口: 通过继承
class Net_name(nn.Module):
这个类,就能获取pytorch库中的零件啦;(点我看更多:使用Module类来自定义模型 , 使用Module类来自定义模型) - 神经元零件预处理: 像普通地写一个类一样,先要在
__init__(self)
中先初始化需要的“零件"(如 conv、pooling、Linear、BatchNorm等层)并让他们继承Net_name
这个父类 ;其中可以通过torch.nn.Sequetial
就是一个可以按照顺序一层层地封装初始化层的容器(使用Sequential类来自定义顺序连接模型);下一步就可以在forward(self, x):
中用定义好的“组件”进行组装; - 神经元零件组装: 通过编写
def forward(self, x):
这个函数,x 为模型的输入(就是你处理好的图像的入口啦),选取上一步中你处理好的神经元零件,在函数中按照你想构建的模型来拼凑,最终return出结果,而输入也会按照forward函数中的顺序通过神经网络,实现正向传播,最后输出结果。那么到此为止,你的模型就搭建完成啦!(PyTorch之前向传播函数forward)
下面用AlexNet来感受一下:
import torch
import torch.nn as nn
class AlexNet(nn.Module): #定义网络,推荐使用Sequential,结构清晰
def __init__(self):
super(AlexNet,self).__init__()
self.conv1 = torch.nn.Sequential( #input_size = 227*227*3
torch.nn.Conv2d(in_channels=3,out_channels=96,kernel_size=11,stride=4,padding=0),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=3, stride=2) #output_size = 27*27*96
)
self.conv2 = torch.nn.Sequential( #input_size = 27*27*96
torch.nn.Conv2d(96, 256, 5, 1, 2),
torch.nn.ReLU(),
torch.nn.MaxPool2d(3, 2) #output_size = 13*13*256
)
self.conv3 = torch.nn.Sequential( #input_size = 13*13*256
torch.nn.Conv2d(256, 384, 3, 1, 1),
torch.nn.ReLU(), #output_size = 13*13*384
)
self.conv4 = torch.nn.Sequential( #input_size = 13*13*384
torch.nn.Conv2d(384, 384, 3, 1, 1),
torch.nn.ReLU(), #output_size = 13*13*384
)
self.conv5 = torch.nn.Sequential( #input_size = 13*13*384
torch.nn.Conv2d(384, 256, 3, 1, 1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(3, 2) #output_size = 6*6*256
)
#网络前向传播过程
self.dense = torch.nn.Sequential(
torch.nn.Linear(9216, 4096),
torch.nn.ReLU(),
torch.nn.Dropout(0.5),
torch.nn.Linear(4096, 4096),
torch.nn.ReLU(),
torch.nn.Dropout(0.5),
torch.nn.Linear(4096, 50)
)
def forward(self, x): #正向传播过程
conv1_out = self.conv1(x)
conv2_out = self.conv2(conv1_out)
conv3_out = self.conv3(conv2_out)
conv4_out = self.conv4(conv3_out)
conv5_out = self.conv5(conv4_out)
res = conv5_out.view(conv5_out.size(0), -1)
out = self.dense(res)
return out
1.2 PyTorch模型—网络权值初始化
每个神经元零件一开始一般都会有初始化的参数的,当你的网络很深的时候,这些看似无关痛痒的初始参数就会对你的迭代过程以及最终结果有很大的影响。那么,这时候你就需要重新对这些网络层进行统一的规划安排,权值初始化方法就出来啦。pytorch中也提供了像是Xavier和MSRA方法来供你初始化,这里说下一般的初始化方法。
- 基础步骤:先设定什么层用什么初始化方法,实例化一个模型之后,执行该函数,即可完成初始化;
- 按需定义初始化方法,例如:
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
- 初始化之后,在forward中就可以像积木一样,搭建自己整个神经网络。
2.1 PyTorch数据索引处理—Dataset 类: (主要包括数据的标签和路径索引)
pytorch数据的读取等预处理,我们都会写在class dataset_name(dataset.Dataset):
这个类里面来构造自定义数据流容器。其中我们一样可以使用python的魔法方法来实现构造、初始化以及属性访问等等功能。常见的:
__init__
我们很熟悉了,它在对象初始化的时候调用,我们一般将它理解为"构造函数";_getitem_
魔法方法,接收一个 index,然后返回图片数据和标签,这个index 通常指的是一个 list 的 index,这个 list 的每个元素就包含了图片数据的路径和标签信息;- 还有
__len__:
等等的一些构造自定义容器的方法都能使用; - dataset模版框架:
class RAMDataset(Dataset):
def __init__(image_fnames, targets):
self.targets = targets
self.images = []
for fname in tqdm(image_fnames, desc="Loading files in RAM"):
with open(fname, "rb") as f:
self.images.append(f.read())
def __len__(self):
return len(self.targets)
def __getitem__(self, index):
target = self.targets[index]
image, retval = cv2.imdecode(self.images[index], cv2.IMREAD_COLOR)
return image, target
3.1 PyTorch数据迭代处理—DataLoder:
接收来自Dataset 类的数据的标签和路径索引后,并不是直接把数据扔进去我们的神经网络中的,需要我们把数据和索引先送进一个训练时迭代用的容器:
- 将 train_data 传入,从而使 DataLoder 拥有图片的路径,继而初始化DataLoder。
- 在一个 iteration 进行时,对DataLoder进行迭代、采样,此时DataLoder就会返回来自dataset 中
def __getitem__(self, index):
的值,像是上面模版中的就是返回一组image, target ,当然你自己来选择返回对象。常见的模版:for batch, (inputs, labels) in enumerate(self.train_loader):
- class DataLoader()中再调用 class _DataLoderIter() ,获取一个 batch 的索引(来自Dataset 类 “getitem”)送到网络中开始处理,向前传播,向后传播。
# Demo of dataloader
train_loader = dataloader.DataLoader(RAMDataset, \
sampler=RandomSampler(self.trainset,args.batchid,batch_image=args.batchimage),\
batch_size=batchsize,\
num_workers=nThread)
# Demo of sampler
from torch.utils.data import sampler
class RandomSampler(sampler.Sampler):
def __init__(self, data_source, batchsize):
super(RandomSampler, self).__init__(data_source)
self.data_source = data_source
self.batchsize = batchsize
def __iter__(self):
imgs = []
for img in self.data_source:
imgs.extend(img)
return iter(imgs)
def __len__(self):
return self.batchsize
4. PyTorch训练的时候迭代一次,需要怎么做:
optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum = 0.9)
scheduler = lr_scheduler.StepLR(optimizer, step_size = 100, gamma = 0.1)
model.train() # 训练的时候需要设置.train(),推理的时候需要设置.eval()
for i in range(epoch):
for batch, (inputs, labels) in enumerate(train_loader):
inputs = inputs.to('cuda')
labels = labels.to('cuda')
optimizer.zero_grad() # 清理优化器中的梯度
outputs = model(inputs) # 输入并向前传播
loss = loss(outputs, labels)
loss.backward() # 向后传播
optimizer.step() # 更新模型的梯度
scheduler.step() # 更新模型的学习率
PyTorch代码小手册
torch
torch的写法和numpy是相近的,但是要注意的是部分写法在反向传递的时候会有问题,自己写的时候就会发现啦,多写点~
torch.sum(input, dim, out=None)
→ Tensor #与python中的sum一样
input (Tensor) – 输入张量
dim (int) – 缩减的维度,开始的时候不理解为什么英文文档里面会说这是缩减的维度,对于高维度的数组如(QxWxExR),对dim=1进行sum,那么其得到的维度就是QxExR,保留最高维度Q的形状,对W维度对应最小的元素进行求和,所以W维度就会消失,其他的函数的维度处理也是这样理解。
out (Tensor, optional) – 结果张量
print(x.sum(0))
#对一维的数求和,按列求和
print(x.sum(1))
#对二维求和按行求和
print(x.sum(2))
#将最小单位的数组元素相加即可new_features = super(_DenseLayer, self).forward(x)
最后在官方论坛上得到结果,含义是将调用所有add_module方法添加到sequence的模块的forward函数。torch.where(condition, x, y) → Tensor
对于x而言,如果其中的每个元素都满足condition,就返回x的值;如果不满足condition,就将y对应位置的元素或者y的值torch.narrow(input, dimension, start, length)
张量剪裁permute
把张量变换成不同维度,view
相当于reshape
,将元素按照行的顺序放置在新的不同大小的张量当中torch.cat(tensors, dim=0, out=None) → Tensor
将张量按照维度进行衔接torch.gesv(B, A, out=None) -> (Tensor, Tensor)
是解线性方程AX=B后得到的解- (1)
torch.unsqueeze(input, dim, out=None) → Tensor
在指定位置增加一个一维的维度
dim (int) – the index at which to insert the singleton dimension
(2)torch.squeeze(input, dim, out=None) → Tensor
在指定位置减去一个一维的维度,默认()就是把所有shape中为1的维度去掉 detach()
就是取出一个该个tensor,并且它不会再参与梯度下降
torch.nn
-
nn.Sequential
一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数。 -
torch.nn.functional.grid_sample(input, grid, mode='bilinear', padding_mode='zeros')
将其归一化到一个(-1,1)的二维平面上,outpuy_{x,y} 的像素值与input_{x0,y0} 的像素值一致, 矩阵索引通过grid矩阵保存。 grid_{x,y}=(x0,y0)
参考网站 https://blog.csdn.net/houdong1992/article/details/88122682
英文手册原文 https://pytorch.org/docs/stable/nn.html?highlight=grid_sample#torch.nn.functional.grid_sample
PyTorch_Tricks
在这里将介绍一些在实际编程中比较常用的小技巧。
- 指定GPU:
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
这个命令是GPU使用的命令,一般放在程序的开头,根据顺序表示优先使用0号设备,然后使用1号设备; - 想看网络的具体输出:这里会用到pytorch中的一个库
from torchsummary import summary
,然后写好输入(input_size 是根据你自己的网络模型的输入尺寸进行设置)summary(your_model, input_size=(channels, H, W))
; - 在test的时候需要关闭求导功能:我没在推理网络的时候,我们不需要计算梯度,这时候就需要使用
with torch.no_grad():
,后续就是写你用model测试的图片; - 当你的forward函数输出多个结果 :假如你forward函数最终return出来的结果是两个张量,那么他们会被放到一个tuple里面,
(tensor1, tensor2)
,并且每一个张量仍旧是存在显存当中的;若你想对结果进行单独的结果测试,需要进行如下的从显存中取出的操作tensor1.detch().cup()
,这时候会把你的结果放到内存当中,不影响后续张量的求导;
PyTorch_Tools
- 使用torchsummary实现网络的可视化
import torch, torchvision
from torchsummary import summary
model = torchvision.models.vgg16()
summary(model, (3, 224, 224))
Pytorch_advanced—Adjustment
终究逃不过调参,毕竟是进阶学习嘛。其实了解了pytorch的整个框架的思路之后,看代码基本上是没有问题的了,但是说到调参的话,自己又是一脸懵逼了,边学边做记录就完事了,莽鸭!
辣么,调参是怎么开始,要从框架的那一部分的代码开始呢?(pytorch version is 0.4.1)
1.1 权重参数的调整
- 加载模型
微调微调,不先加载模型怎么微调呢,第一步就是先学下如何花式加载模型:
model_dict = model.state_dict();
pre_model_dict_feat = {k:v for k,v in pre_model_dict.items() if k in model_dict};
# update the entries #
model_dict.update( pre_model_dict_feat)
# load the new state dict #
model.load_state_dict( pre_model_dict_feat )
- 不同层设置不同学习率的方法
和参考的帖子的有所不同,我的版本是0.4.1,这个版本和以往最大的不同就是变量和张量合并在一起了,可以对tensor直接操作 ,这里直接将每个参数的计算梯度的开关关上就可以了(官网说明链接)
for param in model.parameters():
param.requires_grad = False
- 加载权重参数和查看神经网络层
pytorch编写网络的方法是很直观很舒服的,每一层的网络都是class torch.nn.Module的一个子类,利用Module.children()方法返回所有直接子模块的一个iterator,官网原文Returns an iterator over immediate children modules。(children()与modules()都是返回网络模型里的组成元素,但是children()返回的是最外层的元素,modules()返回的是所有的元素,包括不同级别的子元素,children()不能返回Sequential中的Sequential。)
#这里在小小说明一下,这一步是建立在上面你一步步加载了模型之后的操作哦
#在单步调试的时候不能这么做的哦,别问我怎么知道的
#*** AttributeError: 'Tensor' object has no attribute 'modules'
#ipdb> self.lastconv.modules() ---> <generator object Module.modules at 0x7f46a04b1390>
list(nn.Sequential(nn.Linear(10, 20), nn.ReLU()).modules())
list(nn.Sequential(nn.Linear(10, 20), nn.ReLU()).children())
- 对特定层进行finetune
有两种不同的方法来得到想处理的层,第一种是全部打印出来,在选好自己想冻结的层,第二种就是先使用Module.children()方法查看网络的直接子模块;之后就是将不需要调整的模块中的参数设置为param.requires_grad = False,同时用一个list收集需要调整的模块中的参数。具体代码为:
Plan_A
1.先获取所处理的层
net = Network() # 获取自定义网络结构
for name, value in net.named_parameters():
print('name: {0},\t grad: {1}'.format(name, value.requires_grad))
2.写一个冻结层的列表
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'
]
3.冻他
net = Net.CTPN() # 获取网络结构
for name, value in net.named_parameters():
if name in no_grad:
value.requires_grad = False
else:
value.requires_grad = True
4.优化器的调整
optimizer = optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=0.01)
Plan_B
count = 0
para_optim = []#装参数的盒子
for k in model.children():
count += 1
if count > 6:
for param in k.parameters():
para_optim.append(param)#需要调整的参数
else:
for param in k.parameters():
param.requires_grad = False
optimizer = optim.RMSprop(para_optim, lr)
参考帖子:传送门1
1.2 网络调整
网络权重的参数调整训练之后,那我想在这个基础上只是利用部分网络的参数,后面接个新东西,我要怎么办呢,剑来!
- 简单的利用pytorch中的pre-train模型,比如vgg,resnet啊就不多bb了
- 部分层的参数调整
这个可以认真学一手,这里用了分类来举例,resnet网络最后一层分类层fc是对1000种类型进行划分,对于自己的数据集,如果只有9类,修改的代码如下:
import torchvision.models as models
#调用模型
model = models.resnet50(pretrained=True)
#提取fc层中固定的参数
fc_features = model.fc.in_features
#修改类别为9
model.fc = nn.Linear(fc_features, 9)
- 增减卷积层
基本的思路和第一点中利用pytorch中的pre-train模型是一样的,先拟一个和原本的网络一样的新网络,再在这个网络的基础上增加新的网络,具体还是上代码:
import torchvision.models as models
import torch
import torch.nn as nn
import math
import torch.utils.model_zoo as model_zoo
class CNN(nn.Module):
def __init__(self, block, layers, num_classes=9):
#网络初始化
def _make_layer(self, block, planes, blocks, stride=1):
#自定义层
def forward(self, x):
#再新加层的forward
return x
#加载model
resnet50 = models.resnet50(pretrained=True)
cnn = CNN(Bottleneck, [3, 4, 6, 3])#输入__init__的参数
#读取参数
pretrained_dict = resnet50.state_dict()
model_dict = cnn.state_dict()
# 将pretrained_dict里不属于model_dict的键剔除掉
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict}
# 更新现有的model_dict
model_dict.update(pretrained_dict)
# 加载我们真正需要的state_dict
cnn.load_state_dict(model_dict)
参考帖子:传送门2
2.1梯度剪裁
梯度裁剪一般用于解决 梯度爆炸(gradient explosion) 问题,而梯度爆炸问题在训练 RNN 过程中出现得尤为频繁,所以训练 RNN 基本都需要带上这个参数(在RNN中,相同的权重参数 W 会在各个时间步复用,最终 W 的梯度 g = g1 + g2 + … + gT,即各个时间步的梯度之和,即梯度往往不会消失,而在不加入梯度剪裁的情况下往往会存在梯度爆炸的;而反过来说消失问题可以通过 gradient scaling 来解决,没人这么做的原因是这么做太麻烦了,而且我们现在已经有了更好的方案(自适应学习率、LSTM/GRU)。)
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()
2.2 when you write a test
- 防止验证模型时爆显存
with torch.no_grad():
# 使用model进行预测的代码
pass
PyTorch_thinking
记录一下在使用时候的一些反思
- model.train()和model.eval()的思考:
model.eval()作用是为了固定BN和dropout层(两者都是防止过拟合的手段),使得偏置参数不随着发生变化。因为当batchsize小时,如果没有固定,会对图像的失真有很大的影响,因为在一定范围内,一般来说 Batch_Size 越大,其确定的下降方向越准,引起训练震荡越小,就越不容易过拟合,反之BS越小就月难拟合(dropout是抛弃一部分节点的操作,bn是白话,增加偏置的)。
(知乎中的一个回答:会不会可能是BN在模型中位置的问题?我用 Dropout 的时候也出现过这个问题,就是 train mode 下得到的结果要比 eval mode 下要好很多。后来发现是因为我的 Dropout 放在了卷积层,将 Dropout 放到 FC 层后就正常了。作为常用的防止过拟合的手段,Dropout 和 BN 都能起到比较好的结果,但不是在任意位置都奏效。我了解到的是,Dropout 通常在 FC 层有效,而在卷积层就不推荐用了。至于 BN,至少我用在卷积层是 ok 的。或者你可以在 BN 操作那行设置一个断点,debug 到那行的时候分别测试在同样输入下 train mode 和 eval mode 的输出,然后分析比较二者的差别,或许能给解决你的问题带来灵感。)
model.train() :启用 BatchNormalization 和 Dropout(需要对backbone的网络,比如ResNet50之类的batchnorm层设置为eval mode)
PyTorch_train_log
2019.7.15(pytorch版本0.4.1)
cuda is out of memory
在训练一段时间后,再次运行程序,发生‘cuda is out of memory’,我的电脑12g的显存,不可能不够吧,我在终端输入nvidia-smi打开显卡管理界面,果然python3的进程占用了6g左右的显存,我搜索了如何手动释放显存的指令:
sudo kill -9 PID
PID为对应的进程ID,在终端输入后文件解决。
回想为什么会发生这个错误,大概是因为我在测试的时候记录了比较大的数组,建议后续代码在最后加入torch.cuda.empty_cache()
清除没有用的显存占用。
—— 时间分割线 2019.8.13(pytorch版本0.4.1)——
RuntimeError: Subtraction, the -
operator, with a bool tensor is not support
关于这个error,其实是在重新配置环境后,使用比较高版本的pytorch所导致的,原本自己的代码使用的是pytorch=0.4.1,cuda=9.0,结果现在使用pytorch=1.0.0、1.1.0、 1.2.0以及cuda=10后就出现了这个错误,网上的解释也比较少,唯一的答案就是pytorch版本不对,但是事情没有这么简单。
由于管理员安装的是cuda=10,pytorch=0.4.1不支持这个版本的cuda,我这样需要换可以吗,答案是可以的。
解答: 使用conda的安装命令会自动匹配你需要的包,使用在使用conda install pytorch=0.4.1
cuda100,cudatoolkit=9.0,cudnn=7.6.0 会自动安装上,不需要大环境的cuda支持。
—— 时间分割线 2.19.8.27(pytorch版本0.4.1)——
RuntimeError: Expected object of type torch.cuda.FloatTensor but found type torch.FloatTensor for ar
解答:很明显嘛,就是要用cuda的数据,在相应的变量后面加上 .cuda()就好了
但是要是你想print出来的话也可以print(var.device)
就知道你的变量是cpu还是gpu了
—— 时间分割线 2.19.9.16(pytorch版本0.4.1)——
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace
解决我遇到的原因是第二种,将out+=residual这样所有的+=操作,改成out=out+residual,这种操作其实是张量自己实现累加的时候,在反向传递时loss.backward()
会不知道算哪个。
#第一种错误写法:正如我上面讲的一样,这是一种变相的显存修改方式,这样在反向传播中会出问题
for i in range(ndepth):
BV_predict[0, i, ...] = BV_predict[0, i, ...] * self.d_candi[i]
第二种错误写法:新建的torch list 本质上并没有新建一个新的内存分配,是在原来的基础上进行的浅拷贝
BV_predict_ = []
for i in range(ndepth):
BV_predict_[0, i, ...] = BV_predict[0, i, ...] * self.d_candi[i]
正确的写法:新建一种新的张量,在显存中你的目标张量就不会被覆盖,在求梯度的时候就可以反向传播了
BV_predict_ = torch.zeros_like(BV_predict)
for i in range(ndepth):
BV_predict_[0, i, ...] = BV_predict[0, i, ...] * self.d_candi[i]