Pytorch官方英文文档:https://pytorch.org/docs/stable/torch.html?
Pytorch中文文档:https://pytorch-cn.readthedocs.io/zh/latest/
1. 写在前面
疫情在家的这段时间,想系统的学习一遍Pytorch基础知识,因为我发现虽然直接Pytorch实战上手比较快,但是关于一些内部的原理知识其实并不是太懂,这样学习起来感觉很不踏实, 对Pytorch的使用依然是模模糊糊, 跟着人家的代码用Pytorch玩神经网络还行,也能读懂,但自己亲手做的时候,直接无从下手,啥也想不起来, 我觉得我这种情况就不是对于某个程序练得不熟了,而是对Pytorch本身在自己的脑海根本没有形成一个概念框架,不知道它内部运行原理和逻辑,所以自己写的时候没法形成一个代码逻辑,就无从下手。 这种情况即使背过人家这个程序,那也只是某个程序而已,不能说会Pytorch, 并且这种背程序的思想本身就很可怕, 所以我还是习惯学习知识先有框架(至少先知道有啥东西)然后再通过实战(各个东西具体咋用)来填充这个框架。 而这个系列的目的就是在脑海中先建一个Pytorch的基本框架出来, 学习知识,知其然,知其所以然才更有意思 ??。
今天是本系列的第十篇,也是最后一篇了, 哈哈,又是十全十美,正好,希望这十篇文章,能让你在脑海中建立一个关于Pytorch的框架,真正的做到Pytorch的入门。 通过前面的9篇文章,我们就可以通过Pytorch搭建一个模型并且进行有效的训练,而模型搭建完了之后我们要保存下来,以备后面的使用,并且在大型任务中我们不可能从头自己搭建模型,往往需要模型的迁移, 为了提高训练效率,我们往往需要使用GPU, 最后再整理一些Pytorch中常见的报错作为结束。 所以今天的这篇内容,我们从模型的保存与加载, 模型的微调技术, GPU使用和Pytorch常见报错四方面来整理。
注意,本系列都默认已经安装了Cuda,搭建好了Pytorch环境,如果你电脑是Windows,并且没有装Pytorch,那么巧了, 我之前写过一篇怎么搭建环境,可以先看看 Pytorch入门+实战系列一:Windows下的Pytorch环境手把手搭建 ??
大纲如下:
- 模型的保存与加载
- 模型的finetune
- GPU使用
- Pytorch的常见报错
Ok, let’s go!
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))
第一种方法比较懒,保存整个的模型架构, 比较费时占内存, 第二种方法是只保留模型上的可学习参数, 等建立一个新的网络结构,然后放上这些参数即可,所以推荐使用第二种。 下面通过代码看看具体怎么使用:
这里先建立一个网络模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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, ...]) |
下面就是保存整个模型和保存模型参数的方法:
通过上面,我们已经把模型保存到硬盘里面了,那么如果要用的时候,应该怎么导入呢? 如果我们保存的是整个模型的话, 那么导入的时候就非常简单, 只需要:
1 2 | path_model = "./model.pkl" net_load = torch.load(path_model) |
并且我们可以直接打印出整个模型的结构:
下面看看只保留模型参数的话应该怎么再次使用:
上面就是两种模型加载与保存的方式了,使用起来也是非常简单的,推荐使用第二种。
2.3 模型断点续训练
断点续训练技术就是当我们的模型训练的时间非常长,而训练到了中途出现了一些意外情况,比如断电了,当再次来电的时候,我们肯定是希望模型在中途的那个地方继续往下训练,这就需要我们在模型的训练过程中保存一些断点,这样发生意外之后,我们的模型可以从断点处继续训练而不是从头开始。 所以模型训练过程中设置checkpoint也是非常重要的。
那么就有一个问题了, 这个checkpoint里面需要保留哪些参数呢? 我们可以再次回忆模型训练的五个步骤: 数据 -> 模型 -> 损失函数 -> 优化器 -> 迭代训练。 在这五个步骤中,我们知道数据,损失函数这些是没法变得, 而在迭代训练过程中,我们模型里面的可学习参数, 优化器里的一些缓存是会变的, 所以我们需要保留这些东西。所以我们的checkpoint里面需要保存模型的数据,优化器的数据,还有迭代到了第几次。
下面通过人民币二分类的实验,模拟一个训练过程中的意外中断和恢复,看看怎么使用这个断点续训练:
我们上面发生了一个意外中断,但是我们设置了断点并且进行保存,那么我们下面就进行恢复, 从断点处进行训练,也就是上面的第6个epoch开始,我们看看怎么恢复断点训练:
所以在模型的训练过程当中, 以一定的间隔去保存我们的模型,保存断点,在断点里面不仅要保存模型的参数,还要保存优化器的参数。这样才可以在意外中断之后恢复训练。
3. 模型的finetune
在说模型的finetune之前,得先知道一个概念,就是迁移学习。
迁移学习: 机器学习分支, 研究源域的知识如何应用到目标域,将源任务中学习到的知识运用到目标任务当中,用来提升目标任务里模型的性能。
所以,当我们某个任务的数据比较少的时候,没法训练一个好的模型时, 就可以采用迁移学习的思路,把类似任务训练好的模型给迁移过来,由于这种模型已经在原来的任务上训练的差不多了,迁移到新任务上之后,只需要微调一些参数,往往就能比较好的应用于新的任务, 当然我们需要在原来模型的基础上修改输出部分,毕竟任务不同,输出可能不同。 这个技术非常实用。 但是一定要注意,类似任务上模型迁移(不要试图将一个NLP的模型迁移到CV里面去)
模型微调的步骤:
- 获取预训练模型参数(源任务当中学习到的知识)
- 加载模型(load_state_dict)将学习到的知识放到新的模型
- 修改输出层, 以适应新的任务
模型微调的训练方法:
- 固定预训练的参数(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函数: 转换数据类型/设备
-
tensor.to(*args, **kwargs)
1
2
3
4
5x = torch.ones((3,3))
x = x.to(torch.float64) # 转换数据类型
x = torch.ones((3,3))
x = x.to("cuda") # 设备转移 -
module.to(*args, **kwargs)
1
2
3
4
5linear = nn.Linear(2,2)
linear.to(torch.double) # 这样模型里面的可学习参数的数据类型变成float64
gpu1 = torch.device("cuda")
linear.to(gpu1) # 把模型从CPU迁移到GPU
上面两个方法的区别: 张量不执行inplace, 所以上面看到需要等号重新赋值,而模型执行inplace, 所以不用等号重新赋值。下面从代码中学习上面的两个方法:
下面看一下Module的
如果模型在GPU上, 那么数据也必须在GPU上才能正常运行。也就是说数据和模型必须在相同的设备上。
torch.cuda常用的方法:
- torch.cuda.device_count(): 计算当前可见可用的GPU数
- torch.cuda.get_device_name(): 获取GPU名称
- torch.cuda.manual_seed(): 为当前GPU设置随机种子
- torch.cuda.manual_seed_all(): 为所有可见可用GPU设置随机种子
- 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并行运算机制如何实现呢?
主要参数:
- module: 需要包装分发的模型
- device_ids: 可分发的gpu, 默认分发到所有的可见可用GPU, 通常这个参数不管它,而是在环境变量中管这个。
- output_device: 结果输出设备, 通常是输出到主GPU
下面从代码中看看多GPU并行怎么使用:
由于这里没有多GPU,所以可以看看在多GPU服务器上的一个运行结果:
下面这个代码是多GPU的时候,查看每一块GPU的缓存,并且排序作为逻辑GPU使用, 排在最前面的一般设置为我们的主GPU:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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的机器上使用这个模型,就会报上面的错误。 所以解决办法就是:
这个报错信息是出现在我们用多GPU并行运算的机制训练好了某个模型并保存,然后想再建立一个普通的模型使用保存好的这些参数,就会报这个错误。 这是因为我们在多GPU并行运算的时候,我们的模型net先进行一个并行的一个包装,这个包装使得每一层的参数名称前面会加了一个module。 这时候,如果我们想把这些参数移到我们普通的net里面去,发现找不到这种
我们首先先在多GPU的环境下,建立一个网络,并且进行包装,放到多GPU环境上训练保存:
下面主要是看看加载的时候是怎么报错的:
那么怎么解决这种情况呢? 下面这几行代码就可以搞定了:
1 2 3 4 5 6 7 8 | 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,这里面目前有一些常见的报错信息,可以查看, 也欢迎大家贡献报错信息。
-
报错:
ValueError: num_samples should be a positive interger value, but got num_samples=0
可能的原因: 传入的Dataset中的len(self.data_info)==0 , 即传入该DataLoader的dataset里没有数据。
解决方法:- 检查dataset中的路径
- 检查Dataset的__len__()函数为何输出0
-
报错:
TypeError: pic should be PIL Image or ndarry. Got
可能原因:当前操作需要PIL Image 或 ndarry数据类型, 但传入了Tensor
解决方法:- 检查transform中是否存在两次ToTensor()方法
- 检查transform中每一个操作的数据类型变化
-
报错:
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__函数中的操作 -
报错:
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
可能的原因:网络层输入数据与网络的参数不匹配
解决方法:- 检查对应网络层前后定义是否有误
- 检查输入数据shape
-
报错:
AttributeError: 'DataParallel' object has no attribute 'linear'
可能的原因:并行运算时,模型被dataparallel包装,所有module都增加一个属性 module. 因此需要通过net.module.linear 调用
解决方法:- 网络层前加入module.
-
报错:
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设备上无法直接加载
解决方法:- 需要设置map_location=“cpu”
-
报错:
AttributeError: Can't get attribute 'FooNet2' on
可能的原因:保存的网络模型在当前python脚本中没有定义
解决方法:- 提前定义该类
这个就是如果我们保存了整个网络模型需要重新加载进来的时候要注意的地方。 需要先定义网络的类。
-
报错:
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
交叉熵损失函数中会见到的。 -
报错:
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函数将数据迁移到同一个设备上 -
报错:
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显存,是内存)
解决方法:申请更大内存 -
报错:
RuntimeError: reduce failed to synchronize: device-side assert triggered
可能的原因:采用BCE损失函数的时候,input必须是0-1之间,由于模型最后没有加sigmoid激活函数,导致的。
解决方法:让模型输出的值域在[0, 1] -
报错:
RuntimeError: unexpected EOF. The file might be corrupted.
torch.load加载模型过程报错,因为模型传输过程中有问题,重新传一遍模型即可 -
报错:
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’) -
报错:
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的路依然是任重而道远,因为这些都得去练,如果想无障碍手写神经网络, 就得照着代码反复练,反复看,重点是练习手写神经网络的感觉,然后多做项目,多写代码,依然是那句话无它,唯手熟尔 ??
PS: 本次学习视频来自B站https://www.bilibili.com/video/BV1EE41177ot?from=search&seid=13894259699897815176, 时间长了有可能被和谐了。 所有代码链接:
链接:https://pan.baidu.com/s/1c5EYdd0w8j6w3g54KTxJJA
提取码:k7rh