参考来源:
CSDN:系统学习 Pytorch 笔记十: 模型的保存加载、模型微调、GPU 使用及 Pytorch 常见报错

Content

  • 模型的保存与加载
  • 模型的 finetune
  • GPU 使用

1. 模型的保存与加载

我们的建立的模型训练好了是需要保存的,以备我们后面的使用,所以究竟如何保存模型和加载模型呢? 我们下面重点来看看, 主要分为三块: 首先介绍一下序列化和反序列化,然后介绍模型保存和加载的两种方式,最后是断点的续训练技术。

1.1 序列化与反序列化

序列化就是说内存中的某一个对象保存到硬盘当中,以二进制序列的形式存储下来,这就是一个序列化的过程。 而反序列化,就是将硬盘中存储的二进制的数,反序列化到内存当中,得到一个相应的对象,这样就可以再次使用这个模型了。
image.png
序列化和反序列化的目的就是将我们的模型长久的保存。
Pytorch 中序列化和反序列化的方法:

  • **torch.save(obj, f)**obj 表示对象, 也就是我们保存的数据,可以是模型,张量, dict 等等, f 表示输出的路径。
  • **torch.load(f, map_location)**:f表示文件的路径, map_location 指定存放位置, CPU 或者 GPU, 这个参数挺重要,在使用 GPU 训练的时候再具体说。

1.2 模型保存与加载的两种方式

Pytorch 的模型保存有两种方法,

  1. 一种是保存整个 Module 。
  2. 另外一种是保存模型的参数。
  • 保存与加载整个Moduletorch.save(net, path)torch.load(fpath)
  • 保存与加载模型参数torch.save(net.state_dict(), path)net.load_state_dict(torch.load(path))

第一种方法比较懒,保存整个的模型架构, 比较费时占内存, 第二种方法是只保留模型上的可学习参数, 等建立一个新的网络结构,然后放上这些参数即可,所以推荐使用第二种。 下面通过代码看看具体怎么使用:
这里先建立一个网络模型:

  1. import torch.nn as nn
  2. class LeNet2(nn.Module):
  3. def __init__(self, classes):
  4. super(LeNet2, self).__init__()
  5. self.features = nn.Sequential(
  6. nn.Conv2d(3, 6, 5),
  7. nn.ReLU(),
  8. nn.MaxPool2d(2, 2),
  9. nn.Conv2d(6, 16, 5),
  10. nn.ReLU(),
  11. nn.MaxPool2d(2, 2)
  12. )
  13. self.classifier = nn.Sequential(
  14. nn.Linear(16*5*5, 120),
  15. nn.ReLU(),
  16. nn.Linear(120, 84),
  17. nn.ReLU(),
  18. nn.Linear(84, classes)
  19. )
  20. def forward(self, x):
  21. x = self.features(x)
  22. x = x.view(x.size()[0], -1)
  23. x = self.classifier(x)
  24. return x
  25. def initialize(self):
  26. for p in self.parameters():
  27. p.data.fill_(20191104)
  28. ## 建立一个网络
  29. net = LeNet2(classes=2019)
  30. # "训练"
  31. print("训练前:\n ", net.features[0].weight[0, ...])
  32. net.initialize()
  33. print("训练后: \n", net.features[0].weight[0, ...])

输出结果:

  1. """
  2. 训练前:
  3. tensor([[[-0.0878, 0.0580, 0.0778, -0.0374, 0.0409],
  4. [-0.0203, 0.0520, 0.0884, -0.0282, -0.0690],
  5. [ 0.0141, 0.1042, -0.0655, 0.0342, -0.0891],
  6. [ 0.0072, 0.0619, 0.0097, -0.0418, -0.0555],
  7. [-0.0412, -0.0589, 0.0198, 0.0978, 0.0911]],
  8. [[-0.0241, 0.0064, -0.0435, 0.0198, 0.0587],
  9. [-0.0641, 0.0750, 0.0913, -0.0312, 0.0103],
  10. [ 0.0150, -0.1111, 0.0360, 0.0774, 0.1133],
  11. [-0.1051, 0.0642, -0.0230, 0.0835, -0.0559],
  12. [-0.0572, 0.0030, -0.0091, 0.0833, 0.0311]],
  13. [[ 0.0826, -0.1022, -0.0566, -0.0596, -0.0791],
  14. [ 0.0151, -0.0224, 0.1114, 0.0691, 0.0362],
  15. [-0.0781, 0.0185, 0.0248, 0.0198, -0.0768],
  16. [ 0.0741, 0.0380, -0.1133, -0.0720, 0.0230],
  17. [ 0.0582, 0.0326, -0.0382, 0.0488, -0.0289]]],
  18. grad_fn=<SelectBackward>)
  19. 训练后:
  20. tensor([[[20191104., 20191104., 20191104., 20191104., 20191104.],
  21. [20191104., 20191104., 20191104., 20191104., 20191104.],
  22. [20191104., 20191104., 20191104., 20191104., 20191104.],
  23. [20191104., 20191104., 20191104., 20191104., 20191104.],
  24. [20191104., 20191104., 20191104., 20191104., 20191104.]],
  25. [[20191104., 20191104., 20191104., 20191104., 20191104.],
  26. [20191104., 20191104., 20191104., 20191104., 20191104.],
  27. [20191104., 20191104., 20191104., 20191104., 20191104.],
  28. [20191104., 20191104., 20191104., 20191104., 20191104.],
  29. [20191104., 20191104., 20191104., 20191104., 20191104.]],
  30. [[20191104., 20191104., 20191104., 20191104., 20191104.],
  31. [20191104., 20191104., 20191104., 20191104., 20191104.],
  32. [20191104., 20191104., 20191104., 20191104., 20191104.],
  33. [20191104., 20191104., 20191104., 20191104., 20191104.],
  34. [20191104., 20191104., 20191104., 20191104., 20191104.]]],
  35. grad_fn=<SelectBackward>)
  36. """

下面就是保存整个模型和保存模型参数的方法:
image.png
通过上面,我们已经把模型保存到硬盘里面了,那么如果要用的时候,应该怎么导入呢? 如果我们保存的是整个模型的话, 那么导入的时候就非常简单, 只需要:

  1. path_model = "./model.pkl"
  2. net_load = torch.load(path_model)

并且我们可以直接打印出整个模型的结构:
image.png
下面看看只保留模型参数的话应该怎么再次使用:
image.png
上面就是两种模型加载与保存的方式了,使用起来也是非常简单的,推荐使用第二种。

1.3 模型断点续训练

断点续训练技术就是当我们的模型训练的时间非常长,而训练到了中途出现了一些意外情况,比如断电了,当再次来电的时候,我们肯定是希望模型在中途的那个地方继续往下训练,这就需要我们在模型的训练过程中保存一些断点,这样发生意外之后,我们的模型可以从断点处继续训练而不是从头开始。 所以模型训练过程中设置 checkpoint/检查点 也是非常重要的。
那么就有一个问题了, 这个 checkpoint 里面需要保留哪些参数呢? 我们可以再次回忆模型训练的五个步骤: **数据 -> 模型 -> 损失函数 -> 优化器 -> 迭代训练**。 在这五个步骤中,我们知道数据,损失函数这些是没法变得, 而在迭代训练过程中,我们模型里面的可学习参数, 优化器里的一些缓存是会变的, 所以我们需要保留这些东西。所以我们的 checkpoint 里面需要保存模型的数据,优化器的数据,还有迭代到了第几次。
image.png

下面通过人民币二分类的实验,模拟一个训练过程中的意外中断和恢复,看看怎么使用这个断点续训练:
image.png
我们上面发生了一个意外中断,但是我们设置了断点并且进行保存,那么我们下面就进行恢复, 从断点处进行训练,也就是上面的第 6epoch 开始,我们看看怎么恢复断点训练:
image.png
所以在模型的训练过程当中, 以一定的间隔去保存我们的模型,保存断点,在断点里面不仅要保存模型的参数,还要保存优化器的参数。这样才可以在意外中断之后恢复训练。

2. 模型的 finetune

在说模型的 finetune 之前,得先知道一个概念,就是迁移学习。
image.png
迁移学习: 机器学习分支, 研究源域的知识如何应用到目标域,将源任务中学习到的知识运用到目标任务当中,用来提升目标任务里模型的性能。
所以,当我们某个任务的数据比较少的时候,没法训练一个好的模型时, 就可以采用迁移学习的思路,把类似任务训练好的模型给迁移过来,由于这种模型已经在原来的任务上训练的差不多了,迁移到新任务上之后,只需要微调一些参数,往往就能比较好的应用于新的任务, 当然我们需要在原来模型的基础上修改输出部分,毕竟任务不同,输出可能不同。 这个技术非常实用。 但是一定要注意,类似任务上模型迁移(不要试图将一个 NLP 的模型迁移到 CV 里面去)
image.png
模型微调的步骤:

  1. 获取预训练模型参数(源任务当中学习到的知识)
  2. 加载模型(load_state_dict)将学习到的知识放到新的模型
  3. 修改输出层, 以适应新的任务

模型微调的训练方法:

  • 固定预训练的参数(requires_grad=False; lr=0)
  • Features Extractor 较小学习率(params_group)

好了,下面就通过一个例子,看看如何使用模型的finetune:
下面使用训练好的 ResNet-18 进行二分类: 让模型分出蚂蚁和蜜蜂:
image.png
训练集 120 张, 验证集 70 张,所以我们可以看到这里的数据太少了,如果我们新建立模型进行训练预测,估计没法训练。所以看看迁移技术, 我们用训练好的 ResNet-18 来完成这个任务。

首先我们看看 ResNet-18 的结构,看看我们需要在哪里进行改动:
image.png
下面看看具体应该怎么使用:
image.png
当然,训练时的 trick(技巧) 还有第二个,就是不冻结前面的层,而是修改前面的参数学习率,因为我们的优化器里面有参数组的概念,我们可以把网络的前面和后面分成不同的参数组,使用不同的学习率进行训练,当前面的学习率为 0 的时候,就是和冻结前面的层一样的效果了,但是这种写法比较灵活。
image.png
通过模型的迁移,可以发现这个任务就会完成的比较好。

3. GPU的使用

3.1 CPU VS GPU

CPU(Central Processing Unit, 中央处理器): 主要包括控制器和运算器
GPU(Graphics Processing Unit, 图形处理器): 处理统一的, 无依赖的大规模数据运算
image.png

3.2 数据迁移至GPU

首先, 这个数据主要有两种: Tensor和Module

  • CPU -> GPUdata.to("cpu")
  • GPU -> CPUdata.to("cuda")

**to** 函数: 转换数据类型/设备

1. tensor.to(*args, **kwargs)

  1. x = torch.ones((3,3))
  2. x = x.to(torch.float64) # 转换数据类型
  3. x = torch.ones((3,3))
  4. x = x.to("cuda") # 设备转移

2. module.to(*args, **kwargs)

  1. linear = nn.Linear(2,2)
  2. linear.to(torch.double) # 这样模型里面的可学习参数的数据类型变成float64
  3. gpu1 = torch.device("cuda")
  4. linear.to(gpu1) # 把模型从CPU迁移到GPU

上面两个方法的区别: 张量不执行 **inplace**, 所以上面看到需要等号重新赋值,而模型执行 **inplace**, 所以不用等号重新赋值。下面从代码中学习上面的两个方法:
image.png
下面看一下 Module 的 to 函数:
image.png
如果模型在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 。 这里看一个对应关系吧:
image.png
那么假设我这个地方设置的物理 GPU 的可见顺序是 0,3,2 呢? 物理 GPU 与逻辑 GPU 如何对应?
image.png
这个到底干啥用呢? 在逻辑 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**:包装模型,实现分发并行机制。
image.png
主要参数:

  • module:需要包装分发的模型。
  • device_ids:可分发的 gpu ,默认分发到所有的可见可用 GPU,通常这个参数不管它,而是在环境变量中管这个。
  • output_device:结果输出设备, 通常是输出到主 GPU 。

下面从代码中看看多 GPU 并行怎么使用:
image.png
由于这里没有多 GPU ,所以可以看看在多 GPU 服务器上的一个运行结果:
image.png
下面这个代码是多 GPU 的时候,查看每一块 GPU 的缓存,并且排序作为逻辑 GPU 使用, 排在最前面的一般设置为我们的主 GPU :

  1. def get_gpu_memory():
  2. import platform
  3. if 'Windows' != platform.system():
  4. import os
  5. os.system('nvidia-smi -q -d Memory | grep -A4 GPU | grep Free > tmp.txt')
  6. memory_gpu = [int(x.split()[2]) for x in open('tmp.txt', 'r').readlines()]
  7. os.system('rm tmp.txt')
  8. else:
  9. memory_gpu = False
  10. print("显存计算功能暂不支持windows操作系统")
  11. return memory_gpu
  12. gpu_memory = get_gpu_memory()
  13. if not gpu_memory:
  14. print("\ngpu free memory: {}".format(gpu_memory))
  15. gpu_list = np.argsort(gpu_memory)[::-1]
  16. gpu_list_str = ','.join(map(str, gpu_list))
  17. os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str)
  18. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

在 GPU 模型加载当中常见的两个问题:
image.png
这个报错是我们的模型是以 cuda 的形式进行保存的,也就是在 GPU 上训练完保存的,保存完了之后我们想在一个没有 GPU 的机器上使用这个模型,就会报上面的错误。 所以解决办法就是:**torch.load(path_state_dict, map_location="cpu")**,这样既可以在 CPU 设备上加载 GPU 上保存的模型了。
image.png
这个报错信息是出现在我们用多 GPU 并行运算的机制训练好了某个模型并保存,然后想再建立一个普通的模型使用保存好的这些参数,就会报这个错误。 这是因为我们在多 GPU 并行运算的时候,我们的模型 net 先进行一个并行的一个包装,这个包装使得每一层的参数名称前面会加了一个 module 。 这时候,如果我们想把这些参数移到我们普通的 net 里面去,发现找不到这种 module. 开头的这些参数,即匹配不上,因为我们普通的 net 里面的参数是没有前面的 module 的。这时候我们就需要重新创建一个字典,把名字改了之后再导入。
我们首先先在多 GPU 的环境下,建立一个网络,并且进行包装,放到多 GPU 环境上训练保存:
image.png
下面主要是看看加载的时候是怎么报错的:
image.png
那么怎么解决这种情况呢? 下面这几行代码就可以搞定了:

  1. from collections import OrderedDict
  2. new_state_dict = OrderedDict()
  3. for k, v in state_dict_load.items():
  4. namekey = k[7:] if k.startswith('module.') else k
  5. new_state_dict[namekey] = v
  6. print("new_state_dict:\n{}".format(new_state_dict))
  7. net.load_state_dict(new_state_dict)

下面看看效果:
image.png