pytorch笔记十-模型得保存加载、模型微调、GPU使用及pytorch常见报错
通过前面得文章,我们就可以通过pytorch搭建一个模型并且进行有效得训练,而模型搭建完了之后我们要保存下来,以备后面得使用,并且在大型任务中我们不可能从头自己搭建模型,往往需要模型得迁移,为了提高训练效率,我们往往需要使用GPU,最后再整理一些pytorch中常见得报错作为结束。所以今天得这篇内容,我们从模型得保存于加载,模型得微调技术,GPU使用和pytorch常见报错四方面来整理。
1.模型得保存与加载
我们建立得模型得训练好了是需要保存得,以备我们后面得使用,所以究竟如何保存模型和加载模型呢?我们下面重点来看看,主要分为三块:首先介绍一下序列化和反序列化,然后介绍模型保存和加载得两种方式,最后是断点得续训练技术。
1.1序列化与反序列化
序列化就是说内存中某个对象保存到硬盘当中,以二进制序列的形式存储下来,这就是一个序列化得过程。而反序列化,就是将硬盘中存储得二进制得数,反序列化到内存当中,得到一个相应得对象,这样就可以再次使用这个模型了。
序列化和反序列化得目的就是将我们得模型长久得保存。
pytorch中序列化和反序列得方法:
-torch.save(obj,f):obj表示对象,也就是我们保存得数据,可以是模型,张量,dict等等,f表示输出得路径
-torch.load(f, map_location):f表示文件得路径,map_location指定存放位置,CPU或者GPU,这个参数挺重要,再使用GPU训练得时候再具体说。
1.2 模型保存与加载得两种方式
pytorch得模型保存有两种方式,一种是保存整个Module,另外一种保存模型得参数。
-保存和加载整个Moudle:torch.save(net,path),torch.load(fpath)
-保存模型参数:torch.save(net.state_dict(),path),net.load_state_dict(torch.load(path))
第一种方法比较懒,保存整个得模型架构,比较费时占内存,第二种方法是只保留模型上得可学习参数,等建立一个新得网络结构,然后放上这些参数即可,所以推荐使用第二种。下面通过代码看看怎么使用。
这里先建立一个网络模型:
class LeNet2(nn.Module):
def __init__(self, classes):
super(LeNet2, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2)
self.classifier = nn.Sequential(
nn.Linear(16*5*5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, classes)
def forward(self, x):
x = self.features(x)
x = x.view(x.size()[0], -1)
x = self.classifier(x)
return x
def initialize(self):
for p in self.parameters():
p.data.fill_(20191104)
## 建立一个网络
net = LeNet2(classes=2019)
# "训练"
print("训练前: ", net.features[0].weight[0, ...])
net.initialize()
print("训练后: ", net.features[0].weight[0, ...])
下面就是保存整个模型和保存模型参数得方法:
通过上面,我们已经把模型保存到硬盘里面了,那么如果要用得时候,应该怎么导入呢?如果我们保存得是整个模型得化,那么导入得时候就非常简单,只需要:
path_model = "./model.pkl"
net_load = torch.load(path_model)
并且我们可以直接打印出整个模型得结构:
下面看看只保留模型参数得化应该怎么再次使用:
上面就是两种模型加载与保存得方式了,使用起来也是非常简单得,推荐使用第二种。
1.3 模型断点续训练
断点续训练技术就是当我们得模型训练得事件非常长,而训练到了中途出现了一些意外情况,比如断电了,当再次来电得时候,我们肯定是希望模型再中途得那个地方继续往下训练,这就需要我们再模型得训练过程中保存一些断点,这样发生意外之后,我们得模型可以再断点处继续训练而不是从头开始。所以模型训练中设置checkpoint也是非常重要得。
那么就有一个问题了,这个checkpoint里面需要保留哪些参数呢?我们可以再次回忆模型训练得五个步骤:数据->模型->损失函数->优化器->迭代训练。再这五个步骤中,我们知道数据,损失函数这些是没法变得,而再迭代训练过程中,我们模型里面得可学习参数,优化器里面得一些缓存时会变得,所以我们需要保留这些东西。所以我们得checkpoint里面需要保存模型得数据,优化器得数据,还有迭代到了第几次。
下面通过人民币二分类得实验,模拟一个训练过程中得意外中断和恢复,看看怎么使用这个断点续训练:
我们上面发生了一个意外中断,但是我们设置了断点并且进行保存,那么我们下面就进行恢复,从断点处进行训练,也就是上面得第6个epoch开始,我们看看怎么恢复断点训练:
所以再模型得训练过程当中,以一定得间隔去保存我们得模型,保存断点,再断点里面不仅要保存模型得参数,还要保存优化器得参数。这样才可以再意外中断之后恢复训练。
2.模型的finetune
在说模型得finetune之前,得先知道一个概念,就是迁移学习。
迁移学习:机器学习分支, 研究源域得这是如何应用到目标域 ,将源任务中学习到得知识运用到目标任务当中,用来提升目标任务里面模型的性能。
所以,当我们某个任务得数据比较少得时候,没法训练一个好得模型时,就可以采用迁移学习得思路,把类似任务训练好得模型给迁移过来,由于这种模型已经再原来得任务上训练的差不多了,迁移到新任务上之后,只需要微调一些参数,往往就能比较好得应用于新的任务,当然我们需要再原来模型的基础上修改输出部分,毕竟任务不同,输出可能不同。这个技术非常实用。但是一定要注意,类似任务上模型迁移(不要试图将一个NLP的模型迁移到CV里面去)
模型微调的步骤:
1.获取预训练模型参数(源任务当中学习到的知识)
2.加载模型(load_state_dict)将学习到的知识放到新的模型
3.修改输出层,以适应新的任务
模型微调的训练方法:
-固定预训练的参数(requires_grad=False, lr=0)
-Features Extactor较小学习率(params_group)
好,下面就通过一个例子,看看如何使用模型的finetune:
下面使用训练好的resnet-18进行二分类:让模型分出蚂蚁和蜜蜂:
训练集120张,验证集70张,所以我们可以看到这里的数据太少了,如果我们新建立模型进行训练预测,估计没法训练。所以看看迁移技术,我们用训练好的resnet-18来完成这个任务。
首先我们看看resnet-18的结构,看看我们需要再哪里进行改动:
下面看看具体怎么使用:
上面代码感觉有个小错误,就是冻结层那里貌似时冻结了所有的层,其实应该时冻结卷积层(特征提取层),全连接层的参数不能冻结掉,如果都冻结了,那还咋训练,所以在遍历网络参数的时候,应该有个判断(if时卷积层参数)或者遍历resnet18_ft.features.parameters().
当然,训练时的trick还有第二个,就是不冻结前面的层,而是修改前面的参数学习率,因为我们的优化器里面有参数组的概念,我们可以把网络的前面和后面分成不同的参数组,使用不同的学习率进行训练,当前面的学习率为0的时候胡,就是和冻结前面的层一样的效果了,但是这种写法比较灵活。
通过模型的迁移,可以发现这个任务就会完成的比较好。
3. GPU的使用
3.1 GPU VS GPU
CPU(central processing unit,中央处理器):主要包括控制器和运算器
GPU(Graphics Processing Unit, 图形处理器):处理统一的,无依赖的大规模数据运算
3.2 数据迁移至GPU
首先,这个数据主要有两种:Tensor和Module
-CPU->GPU:data.to("cpu")
-GPU->CPU:data.to("cuda")
to函数:转换数据类型/设备
1.tensor.to(* args, **kwargs)
x = torch.ones((3,3))
x = x.to(torch.float64) # 转换数据类型
x = torch.ones((3,3))
x = x.to("cuda") # 设备转移
2.module.to(*args,**kwargs)
linear = nn.Linear(2,2)
linear.to(torch.double) # 这样模型里面的可学习参数的数据类型变成float64
gpu1 = torch.device("cuda")
linear.to(gpu1) # 把模型从CPU迁移到GPU
上面两个方法的区别:张量 不执行inplace ,所以上面看到需要等号重新赋值,而模型 执行inplace ,所以不用等号重新弄赋值。下面从代码中学习上面的两个方法:
下面看一下Module的to函数:
如果模型再GPU上,那么数据也必须再GPU上才能正常运行。也就是说 数据和模型必须在相同的设备上 。
torch.cuda常用的方法:
1.torch.cuda.device_count():计算当前可见可用的GPU数
2.torch.cuda.get_device_name():获取GPU名称
3.torch.cuda.manual_seed():为当前GPU设置随机种子
4.torch.cuda.manual_seed_all():为所有可见可用GPU设置随机种子
5.torch.cuda.set_device():设置主GPU(默认GPU)为哪一个物理GPU(不推荐)
推荐的方式是设置系统的环境变量:os.environ.setdefault('CUDA_VISIBLE_DEVICES', '2,3')通过这个方法合理的分配GPU,使得多个人使用的时候不冲突。但是这里要注意一下,这里的2,3指的是物理GPU的2,3.但是在逻辑GPU上,这里表示的0,1.这里看下对应关系:
那么假设我这个地方设置的物理GPU的可见顺序是0,3,2呢?物理GPU和逻辑GPU如何对应?
这个到底干啥用呢?在逻辑GPU中,我们有个主GPU的概念,通常指的是GPU0.而这个主GPU的概念,在多GPU并行运算中就有用了。
3.3 多GPU并行运算
多GPU并行运算,简单的说就是我有很多块GPU,比如4块,而这里面有个主GPU,当拿到样本数据之后,比如主GPU拿到了16个样本,那么它会经过16/4=4的运算,把数据分成4份,自己留一份,然后把那3份分发到另外3块GPU上进行运算,等其他的GPU运算完了之后,主GPU再把结果收回来负责整合。这时候看到主GPU的作用了把。多GPU并行运算可以大大节省时间。所以,多GPU并行运算的三步:分发->并行计算->收回结果整合。
pytorch中的多GPU并行运算机制如何实现呢?
torch.nn.DataParallel:包装模型,实现分发并行机制
主要参数:
-module:需要包装分发的模型
-device_ids:可分发的gpu,默认分发到所有的 可见可用 GPU,通常这个参数不管它,而是再环境变量中管这个。
-output_device:结果输出设备,通常输出到主GPU
下面从代码中看看多GPU并行怎么使用:
由于这里没有多GPU,所以可以看看再多GPU服务器上的一个运行结果:
下面这个代码是多GPU的时候,查看每一块GPU的缓存,并且排序作为逻辑GPU使用,排在最前面的一般设置为我们的主GPU:
def get_gpu_memory():
import platform
if 'Windows' != platform.system():
import os
os.system('nvidia-smi -q -d Memory | grep -A4 GPU | grep Free > tmp.txt')
memory_gpu = [int(x.split()[2]) for x in open('tmp.txt', 'r').readlines()]
os.system('rm tmp.txt')
else:
memory_gpu = False
print("显存计算功能暂不支持windows操作系统")
return memory_gpu
gpu_memory = get_gpu_memory()
if not gpu_memory:
print("\ngpu free memory: {}".format(gpu_memory))
gpu_list = np.argsort(gpu_memory)[::-1]
gpu_list_str = ','.join(map(str, gpu_list))
os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
在GPU模型加载当中常见的两个问题:
这个报错是我们的模型是以cuda的形式进行保存的,也就是在GPU上训练完保存的,保存完了之后我们想在一个没有GPU的机器上使用这个模型,就会报上面的错误。所以解决办法就是:torch.load(path_state_dict, map_location='cpu'),这样既可以在CPU设备上加载GPU上保存的模型了。
这个报错信息是出现在我们用多GPU并行运算的机制训练好了某个模型并保存,然后想再建立一个普通的模型使用保存好的这些参数,就会报这个错误。这是因为我们在多GPU并行运算的时候,我们的模型net先进行一个并行的一个包装,这个包装使得每一层的参数名称前面会加了一个module.这时候,如果我们想把这些参数移到我们普通的net里面去,发现找不到这种module.开头的这些参数,即匹配不上,因为我们普通的net里面的参数是没有前面的module的。这时候我们就需要重新创建一个字典,把名字改了之后再导入。
我们首先在多GPU的环境下,建立一个网络,并且进行包装,放到多GPU环境上训练保存:
下面主要看看加载的时候是怎么报错的:
那么怎么解决这种情况呢?下面这几行代码就可以搞定:
from collections import OrderedDict
new_state_dict = OrderedDict()