【Pytorch 】笔记十:剩下的一些内容(完结)

2020-09-14 15:05:42 浏览数 (1)

1. 写在前面

疫情在家的这段时间,想系统的学习一遍 Pytorch 基础知识,因为我发现虽然直接 Pytorch 实战上手比较快,但是关于一些内部的原理知识其实并不是太懂,这样学习起来感觉很不踏实, 对 Pytorch 的使用依然是模模糊糊, 跟着人家的代码用 Pytorch 玩神经网络还行,也能读懂,但自己亲手做的时候,直接无从下手,啥也想不起来, 我觉得我这种情况就不是对于某个程序练得不熟了,而是对 Pytorch 本身在自己的脑海根本没有形成一个概念框架,不知道它内部运行原理和逻辑,所以自己写的时候没法形成一个代码逻辑,就无从下手。这种情况即使背过人家这个程序,那也只是某个程序而已,不能说会 Pytorch, 并且这种背程序的思想本身就很可怕, 所以我还是习惯学习知识先有框架(至少先知道有啥东西)然后再通过实战(各个东西具体咋用)来填充这个框架。而这个系列的目的就是在脑海中先建一个 Pytorch 的基本框架出来, 学习知识,知其然,知其所以然才更有意思;)。

今天是本系列的第十篇,也是最后一篇了, 哈哈,又是十全十美,正好,希望这十篇文章,能让你在脑海中建立一个关于 Pytorch 的框架,真正的做到 Pytorch 的入门。通过前面的 9 篇文章,我们就可以通过 Pytorch 搭建一个模型并且进行有效的训练,而模型搭建完了之后我们要保存下来,以备后面的使用,并且在大型任务中我们不可能从头自己搭建模型,往往需要模型的迁移, 为了提高训练效率,我们往往需要使用 GPU, 最后再整理一些 Pytorch 中常见的报错作为结束。所以今天的这篇内容,我们从模型的保存与加载, 模型的微调技术, GPU 使用和 Pytorch 常见报错四方面来整理。

大纲如下:

  • 模型的保存与加载
  • 模型的 finetune
  • GPU 使用
  • Pytorch 的常见报错

2. 模型的保存与加载

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

2.1 序列化与反序列化

序列化就是说内存中的某一个对象保存到硬盘当中,以二进制序列的形式存储下来,这就是一个序列化的过程。而反序列化,就是将硬盘中存储的二进制的数,反序列化到内存当中,得到一个相应的对象,这样就可以再次使用这个模型了。

序列化和反序列化的目的就是将我们的模型长久的保存。

Pytorch 中序列化和反序列化的方法:

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

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

Pytorch 的模型保存有两种方法, 一种是保存整个 Module, 另外一种是保存模型的参数。

  • 保存和加载整个 Module:torch.save (net, path), torch.load (fpath)
  • 保存模型参数:torch.save (net.state_dict (), path), net.load_state_dict (torch.load (path))

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

代码语言:javascript复制
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, ...])

下面就是保存整个模型和保存模型参数的方法:

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

代码语言:javascript复制
path_model = "./model.pkl"
net_load = torch.load (path_model)

并且我们可以直接打印出整个模型的结构:

下面看看只保留模型参数的话应该怎么再次使用:

上面就是两种模型加载与保存的方式了,使用起来也是非常简单的,推荐使用第二种。

2.3 模型断点续训练

断点续训练技术就是当我们的模型训练的时间非常长,而训练到了中途出现了一些意外情况,比如断电了,当再次来电的时候,我们肯定是希望模型在中途的那个地方继续往下训练,这就需要我们在模型的训练过程中保存一些断点,这样发生意外之后,我们的模型可以从断点处继续训练而不是从头开始。 所以模型训练过程中设置 checkpoint 也是非常重要的。

那么就有一个问题了, 这个 checkpoint 里面需要保留哪些参数呢? 我们可以再次回忆模型训练的五个步骤:数据 -> 模型 -> 损失函数 -> 优化器 -> 迭代训练。 在这五个步骤中,我们知道数据,损失函数这些是没法变得, 而在迭代训练过程中,我们模型里面的可学习参数, 优化器里的一些缓存是会变的, 所以我们需要保留这些东西。所以我们的 checkpoint 里面需要保存模型的数据,优化器的数据,还有迭代到了第几次。

下面通过人民币二分类的实验,模拟一个训练过程中的意外中断和恢复,看看怎么使用这个断点续训练:

我们上面发生了一个意外中断,但是我们设置了断点并且进行保存,那么我们下面就进行恢复, 从断点处进行训练,也就是上面的第 6 个 epoch 开始,我们看看怎么恢复断点训练:

所以在模型的训练过程当中, 以一定的间隔去保存我们的模型,保存断点,在断点里面不仅要保存模型的参数,还要保存优化器的参数。这样才可以在意外中断之后恢复训练。

3. 模型的 finetune

在说模型的 finetune 之前,得先知道一个概念,就是迁移学习。

迁移学习:机器学习分支, 研究源域的知识如何应用到目标域,将源任务中学习到的知识运用到目标任务当中,用来提升目标任务里模型的性能。

所以,当我们某个任务的数据比较少的时候,没法训练一个好的模型时, 就可以采用迁移学习的思路,把类似任务训练好的模型给迁移过来,由于这种模型已经在原来的任务上训练的差不多了,迁移到新任务上之后,只需要微调一些参数,往往就能比较好的应用于新的任务, 当然我们需要在原来模型的基础上修改输出部分,毕竟任务不同,输出可能不同。这个技术非常实用。但是一定要注意,类似任务上模型迁移(不要试图将一个 NLP 的模型迁移到 CV 里面去)

模型微调的步骤:

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

模型微调的训练方法:

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

好了,下面就通过一个例子,看看如何使用模型的 finetune:

下面使用训练好的 ResNet-18 进行二分类:让模型分出蚂蚁和蜜蜂:

训练集 120 张, 验证集 70 张,所以我们可以看到这里的数据太少了,如果我们新建立模型进行训练预测,估计没法训练。所以看看迁移技术, 我们用训练好的 ResNet-18 来完成这个任务。

首先我们看看 ResNet-18 的结构,看看我们需要在哪里进行改动:

下面看看具体应该怎么使用:

当然,训练时的 trick 还有第二个,就是不冻结前面的层,而是修改前面的参数学习率,因为我们的优化器里面有参数组的概念,我们可以把网络的前面和后面分成不同的参数组,使用不同的学习率进行训练,当前面的学习率为 0 的时候,就是和冻结前面的层一样的效果了,但是这种写法比较灵活

通过模型的迁移,可以发现这个任务就会完成的比较好。

4. GPU 的使用

4.1 CPU VS GPU

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

4.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 并行运算中就有用了。

4.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:

代码语言:javascript复制
 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 环境上训练保存:

下面主要是看看加载的时候是怎么报错的:

那么怎么解决这种情况呢?下面这几行代码就可以搞定了:

代码语言:javascript复制
from collections import OrderedDict
    new_state_dict = OrderedDict ()
    for k, v in state_dict_load.items ():
        namekey = k [7:] if k.startswith ('module.') else k
        new_state_dict [namekey] = v
    print ("new_state_dict:n {}".format (new_state_dict))

    net.load_state_dict (new_state_dict)

下面看看效果:

5. Pytorch 的常见报错

这里先给出一份 Pytorch 常见错误与坑的一份文档:https://shimo.im/docs/PvgHytYygPVGJ8Hv,这里面目前有一些常见的报错信息,可以查看, 也欢迎大家贡献报错信息。

  1. 报错:ValueError: num_samples should be a positive interger value, but got num_samples=0可能的原因:传入的 Dataset 中的 len (self.data_info)==0, 即传入该 DataLoader 的 dataset 里没有数据。解决方法:
    1. 检查 dataset 中的路径
    2. 检查 Dataset 的__len__() 函数为何输出 0
  2. 报错:TypeError: pic should be PIL Image or ndarry. Got <class 'torch.Tensor'> 可能原因:当前操作需要 PIL Image 或 ndarry 数据类型, 但传入了 Tensor 解决方法:
    1. 检查 transform 中是否存在两次 ToTensor () 方法
    2. 检查 transform 中每一个操作的数据类型变化
  3. 报错:RuntimeError: invalid argument 0: Sizes of tensors must match except in dimension 0. Got 93 and 89 in dimension 1 at /Users/soumith/code/builder/wheel/pytorch-src/aten/src/TH/generic/THTensorMath.cpp:3616可能的原因:dataloader 的__getitem__函数中,返回的图片形状不一致,导致无法 stack 解决方法:检查__getitem__函数中的操作
  4. 报错:conv: RuntimeError: Given groups=1, weight of size 6 1 5 5, expected input [16, 3, 32, 32] to have 1 channels, but got 3 channels instead linear: RuntimeError: size mismatch, m1: [16 x 576], m2: [400 x 120] at ../aten/src/TH/generic/THTensorMath.cpp:752可能的原因:网络层输入数据与网络的参数不匹配 解决方法:
    1. 检查对应网络层前后定义是否有误
    2. 检查输入数据 shape
  5. 报错:AttributeError: 'DataParallel' object has no attribute 'linear'可能的原因:并行运算时,模型被 dataparallel 包装,所有 module 都增加一个属性 module. 因此需要通过 net.module.linear 调用 解决方法:
    1. 网络层前加入 module.
  6. 报错: python RuntimeError: Attempting to deserialize object on a CUDA device but torch.cuda.is_available () is False. If you are running on a CPU-only machine, please use torch.load with map_location=torch.device ('cpu') to map your storages to the CPU. 可能的原因:gpu 训练的模型保存后,在无 gpu 设备上无法直接加载 解决方法:
    1. 需要设置 map_location="cpu"
  7. 报错:AttributeError: Can't get attribute 'FooNet2' on <module '__main__' from '可能的原因:保存的网络模型在当前 python 脚本中没有定义 解决方法: 这个就是如果我们保存了整个网络模型需要重新加载进来的时候要注意的地方。 需要先定义网络的类。
    1. 提前定义该类
  8. 报错:RuntimeError: Assertion cur_target >= 0 && cur_target < n_classes' failed. at ../aten/src/THNN/generic/ClassNLLCriterion.c:94可能的原因:标签数大于等于类别数量,即不满足 cur_target < n_classes,通常是因为标签从 1 开始而不是从 0 开始 解决方法:修改 label,从 0 开始,例如:10 分类的标签取值应该是 0-9 交叉熵损失函数中会见到的。
  9. 报错:python RuntimeError: expected device cuda:0 and dtype Long but got device cpu and dtype Long Expected object of backend CPU but got backend CUDA for argument #2 'weight' 可能的原因:需计算的两个数据不在同一个设备上 解决方法:采用 to 函数将数据迁移到同一个设备上
  10. 报错:RuntimeError: DataLoader worker (pid 27) is killed by signal: Killed. Details are lost due to multiprocessing. Rerunning with num_workers=0 may give better error trace.可能原因:内存不够(不是 gpu 显存,是内存) 解决方法:申请更大内存
  11. 报错:RuntimeError: reduce failed to synchronize: device-side assert triggered可能的原因:采用 BCE 损失函数的时候,input 必须是 0-1 之间,由于模型最后没有加 sigmoid 激活函数,导致的。解决方法:让模型输出的值域在 [0, 1]
  12. 报错:RuntimeError: unexpected EOF. The file might be corrupted.torch.load 加载模型过程报错,因为模型传输过程中有问题,重新传一遍模型即可
  13. 报错:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 1: invalid start byte可能的原因:python2 保存,python3 加载,会报错 解决方法:把 encoding 改为 encoding='iso-8859-1' check_p = torch.load (path, map_location="cpu", encoding='iso-8859-1')
  14. 报错:RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same问题原因:数据张量已经转换到 GPU 上,但模型参数还在 cpu 上,造成计算不匹配问题。解决方法:通过添加 model.cuda () 将模型转移到 GPU 上以解决这个问题。或者通过添加 model.to (cuda) 解决问题

6. 总结

这篇文章到这里也就结束了,也就意味着 Pytorch 的基础知识,基本概念也都整理完毕,首先先快速回顾一下这次学习的知识,这次学习的比较杂了,把一些零零散散的知识放到这一篇文章里面。首先学习了模型的保存与加载问题,介绍了两种模型保存与加载的方法, 然后学习了模型的微调技术,这个在迁移学习中用处非常大,还介绍了迁移学习中常用的两个 trick。然后学习了如何使用 GPU 加速训练和 GPU 并行训练方式, 最后整理了 Pytorch 中常见的几种报错信息。

到这里为止,关于 Pytorch 的基本知识结束, 下面也对这十篇文章进行一个梳理和总结,这十篇文章的逻辑其实也非常简单,就是围绕着机器学习模型训练的五大步骤进行展开的:首先是先学习了一下《Pytorch 的基本知识》知道了什么是张量, 然后学习了《自动求导系统,计算图机制》, 对 Pytorch 有了一个基本的了解之后,我们就开始学习《Pytorch 的数据读取机制》,在里面知道了 DataLoader 和 Dataset,还学习了图像预处理的模块 transform。接着学习《模型模块》,知道了如何去搭建一个模型,一个模型是怎么去进行初始化的,还学习了容器,《常用网络层的使用》。再往后就是《网络层的权重初始化方法和 8 种损失函数》, 有了损失函数之后,接着就开始学习《各种优化器》帮助我们更新参数,还有学习率调整的各种策略。有了数据,模型,损失,优化器,就可以迭代训练模型了,所以在迭代训练过程中学习了《Tensorboard》这个非常强大的可视化工具,可以帮助我们更好的监控模型训练的效果,这里面还顺带介绍了点高级技术 hook 机制。然后学习了《正则化和标准化技术》, 正则化可以帮助缓解模型的过拟合,这里面学习了 L1,L2 和 Dropout 的原理和使用,而标准化可以更好的解决数据尺度不平衡的问题,这里面有 BN, LN, IN, GN 四种标准化方法,并对比了它们的不同及应用场景。 最后我们以一篇杂记作为收尾,杂记里面学习了模型的保存加载,模型微调,如何使用 GPU 以及常用的报错。这就是这十篇文章的一个逻辑了。

希望这些知识能帮助你真正的入门 Pytorch,在脑海中建立一个 Pytorch 学习框架,掌握 Pytorch 的内部运行机制, 学习知识,知其然,更要知其所以然,这样在以后用起来的时候才能体会更加深刻。这十篇文章用了大约半个月的时间整理总结, 学习完之后,收获很多,当然这种收获不是立马就能用 Pytorch 训练一个神经网络出来,立即用 Pytorch 搞定一个项目,而是 Pytorch 在我脑海中不是那么的陌生了,慢慢的变得熟悉起来, 从 DataLoader 和 Dataset 的运行机制,差不多对 Pytorch 的数据读取有了一个了解,从各种模型搭建的过程,权重初始化,损失函数有哪些怎么用,优化器的运行原理渐渐的熟悉了一个模型应该怎么去训练。这样过来一遍之后,真的能深入了解每一个细节,也知道了模型训练中出现的一些问题,比如权重初始化不适当就容易出现梯度消失和爆炸,在代码中的结果就是容易 nan。再比如损失不下降反而上升, 这有可能是学习率过大导致的, 还有各种技术及原理,真的是收获颇多,也希望你也有所收获吧。

这十篇文章虽然里面图片很多,还有各种调试, 整体看起来还是挺乱的, 相信大家看起来也心烦意乱,能坚持看完的并不会太多,但依然希望能有所帮助,即使没法看完一遍,等遇到问题了,当做查阅的手册也可以,反正我是这样的,这十篇文章写得过程中没有注意各种排版啥的,依然是以详细为主,所以为了说明原理,我用了各种调试,各种图片,这样回来看的时候就能很容易记起来,毕竟只看一遍肯定是记不住的,我后期依然会回来查阅观看。

踏踏实实的搞定这十篇文章,相信对 Pytorch 真正入门了,那么接下来就可以去用 Pytorch 做一些项目了,想象一下,当你无障碍读懂大佬的 Pytorch 代码,当你无障碍用 Pytorch 复现论文,无障碍用 Pytorch 实现项目, 理直气壮对面试官说熟悉 Pytorch,那是多么的爽, 哈哈, 爽一下即可, 先别睡,Pytorch 的路依然是任重而道远,因为这些都得去练,如果想无障碍手写神经网络, 就得照着代码反复练,反复看,重点是练习手写神经网络的感觉,然后多做项目,多写代码,依然是那句话无它,唯手熟尔;)

所有代码链接:

链接:https://pan.baidu.com/s/1c5EYdd0w8j6w3g54KTxJJA 提取码:k7rh

0 人点赞