ch07-Pytorch的训练技巧

0.引言

1.模型保存与加载

本节主要介绍序列化与反序列化,以及 PyTorch 中的模型保存于加载的两种方式,模型的断点续训练。

1.1.序列化与反序列化

模型在内存中是以对象的逻辑结构保存的,但是在硬盘中是以二进制流的方式保存的。

  • 序列化是指将内存中的数据以二进制序列的方式保存到硬盘中。PyTorch 的模型保存就是序列化。
  • 反序列化是指将硬盘中的二进制序列加载到内存中,得到模型的对象。PyTorch 的模型加载就是反序列化。

在这里插入图片描述

1.2.PyTorch 中的模型保存与加载

在这里插入图片描述

(1) 模型保存torch.save

  • torch.save(obj, f, pickle_module, pickle_protocol=2, _use_new_zipfile_serialization=False)
  • 主要参数:
    • obj:保存的对象,可以是模型。也可以是 dict。因为一般在保存模型时,不仅要保存模型,还需要保存优化器、此时对应的 epoch 等参数。这时就可以用 dict 包装起来。
    • f:输出路径

其中模型保存还有两种方式:

  • 保存整个 Module:这种方法比较耗时,保存的文件大:torch.savev(net, path)
  • 只保存模型的参数:推荐这种方法,运行比较快,保存的文件比较小
    state_sict = net.state_dict()
    torch.savev(state_sict, path)
    

下面是保存 LeNet 的例子。在网络初始化中,把权值都设置为 2020,然后保存模型。

import torch
import numpy as np
import torch.nn as nn
from common_tools import set_seed

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_(2020)

net = LeNet2(classes=2019)
# "训练"
print("训练前: ", net.features[0].weight[0, ...])
net.initialize()
print("训练后: ", net.features[0].weight[0, ...])

path_model = "./model.pkl"
path_state_dict = "./model_state_dict.pkl"
# 保存整个模型
torch.save(net, path_model)
# 保存模型参数
net_state_dict = net.state_dict()
torch.save(net_state_dict, path_state_dict)

运行完之后,文件夹中生成了model.pkl和model_state_dict.pkl,分别保存了整个网络和网络的参数

(2) 模型加载torch.load

  • torch.load(f, map_location=None, pickle_module, **pickle_load_args)
  • 主要参数:
    • f:文件路径
    • map_location:指定存在 CPU 或者 GPU。

加载模型也有两种方式:

  • 加载整个 Module

如果保存的时候,保存的是整个模型,那么加载时就加载整个模型。这种方法不需要事先创建一个模型对象,也不用知道模型的结构,代码如下:

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

print(net_load)

输出如下:

LeNet2(
  (features): Sequential(
    (0): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): ReLU()
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (classifier): Sequential(
    (0): Linear(in_features=400, out_features=120, bias=True)
    (1): ReLU()
    (2): Linear(in_features=120, out_features=84, bias=True)
    (3): ReLU()
    (4): Linear(in_features=84, out_features=2019, bias=True)
  )
)
  • 只加载模型的参数

如果保存的时候,保存的是模型的参数,那么加载时就参数。这种方法需要事先创建一个模型对象,再使用模型的load_state_dict()方法把参数加载到模型中,代码如下:

path_state_dict = "./model_state_dict.pkl"
state_dict_load = torch.load(path_state_dict)
net_new = LeNet2(classes=2019)

print("加载前: ", net_new.features[0].weight[0, ...])
net_new.load_state_dict(state_dict_load)
print("加载后: ", net_new.features[0].weight[0, ...])

1.3.模型的断点续训练

在训练过程中,可能由于某种意外原因如断点等导致训练终止,这时需要重新开始训练。断点续练是在训练过程中每隔一定次数的 epoch 就保存模型的参数和优化器的参数,这样如果意外终止训练了,下次就可以重新加载最新的模型参数和优化器的参数,在这个基础上继续训练。

下面的代码中,每隔 5 个 epoch 就保存一次,保存的是一个 dict,包括模型参数、优化器的参数、epoch。然后在 epoch 大于 5 时,就break模拟训练意外终止。关键代码如下:

    if (epoch+1) % checkpoint_interval == 0:

        checkpoint = {"model_state_dict": net.state_dict(),
                      "optimizer_state_dict": optimizer.state_dict(),
                      "epoch": epoch}
        path_checkpoint = "./checkpoint_{}_epoch.pkl".format(epoch)
        torch.save(checkpoint, path_checkpoint)

在 epoch 大于 5 时,就break模拟训练意外终止

    if epoch > 5:
        print("训练意外中断...")
        break

断点续训练的恢复代码如下:

path_checkpoint = "./checkpoint_4_epoch.pkl"
checkpoint = torch.load(path_checkpoint)

net.load_state_dict(checkpoint['model_state_dict'])

optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

start_epoch = checkpoint['epoch']

scheduler.last_epoch = start_epoch

需要注意的是,还要设置scheduler.last_epoch参数为保存的 epoch。模型训练的起始 epoch 也要修改为保存的 epoch。

2.模型 Finetune

  • m本节主要任务:了解transfer learning 与 model finetune

  • 详细介绍:学习模型微调(Finetune)的方法,以及认识Transfer Learning(迁移学习)与Model Finetune之间的关系。

2.1.Transfer Learning & Model Finetune

迁移学习:机器学习分支,研究源域(source domain)的知识如何应用到目标域(target domain)。

在这里插入图片描述

所谓的模型微调,其实就是模型的迁移学习,在深度学习中,通过不断的迭代,更新卷基层中的权值,这里的权值可以称之为 knowledge , 然后我们可以将这些 knowledge 进行迁移,主要目的是将这些 knowledge 运用到新的模型中,这样既可以减小由于数据量不足导致的过拟合现象,同时又能加快模型的训练速度。

在这里插入图片描述

比如做人脸识别,可以把 ImageNet 看作 source domain,人脸数据集看作 target domain。通常来说 source domain 要比 target domain 大得多。可以利用 ImageNet 训练好的网络应用到人脸识别中。

具体说来,对于卷积神经网络,我们可以把前面的卷基层,池化层看作是 feature extactor(特征提取) ,是一个非常有共性的部分。得到一系列的feature map。
而后面的全连接层,可以称之为 classifier (分类器), 与具体的任务有关。并且改变最后一个全连接层的输出来适应目标任务,训练后面 classifier 的权值,这就是 Finetune。通常 target domain 的数据比较小,不足以训练全部参数,容易导致过拟合,因此不改变 feature extractor 的权值。

在这里插入图片描述

Finetune 步骤如下:

  • 1.获取预训练模型的参数
  • 2.使用load_state_dict()把参数加载到模型中
  • 3.修改输出层
  • 4.固定 feature extractor 的参数。这部分通常有 2 种做法:
    • 1.固定卷积层的预训练参数。可以设置requires_grad=False或者lr=0
    • 2.可以通过params_group给 feature extractor 设置一个较小的学习率

下面微调 ResNet-18,用于蜜蜂和蚂蚁图片的二分类。训练集每类数据各 120 张,验证集每类数据各 70 张图片。

在这里插入图片描述

Resnet-18模型结构如下图所示:

前面四层是特征提取,接下来四层(layer1~layer4)是残差网络,然后接avgpool池化层,最后接FC分类(原模型是1000分类,ImageNet上训练的)。

在这里插入图片描述

(1) 不使用 Finetune

第一次我们首先不使用 Finetune,而是从零开始训练模型,这时只需要修改全连接层即可:

# 首先拿到 fc 层的输入个数
num_ftrs = resnet18_ft.fc.in_features
# 然后构造新的 fc 层替换原来的 fc 层
resnet18_ft.fc = nn.Linear(num_ftrs, classes)

输出如下:

use device :cpu
Training:Epoch[000/025] Iteration[010/016] Loss: 0.7192 Acc:47.50%
Valid:  Epoch[000/025] Iteration[010/010] Loss: 0.6885 Acc:51.63%
...
Valid:  Epoch[024/025] Iteration[010/010] Loss: 0.5923 Acc:70.59%

训练了 25 个 epoch 后的准确率为:70.59%。

训练的 loss 曲线如下:

在这里插入图片描述

损失值一直在0.6附近,并且得到的Accuracy只有70%

(2) 使用 Finetune

然后我们把下载的模型参数加载到模型中:

path_pretrained_model = enviroments.resnet18_path
state_dict_load = torch.load(path_pretrained_model)
resnet18_ft.load_state_dict(state_dict_load)

不冻结卷积层

这时我们不冻结卷积层,所有层都是用相同的学习率,输出如下:

use device :cpu
Training:Epoch[000/025] Iteration[010/016] Loss: 0.6299 Acc:65.62%
...
Valid:  Epoch[024/025] Iteration[010/010] Loss: 0.1808 Acc:96.08%

训练了 25 个 epoch 后的准确率为:96.08%。

训练的 loss 曲线如下:

可以看出,损失值最后收敛到在0.2附近,并且在第二个Epoch的Accuracy就达到了90%。

在这里插入图片描述

在这里插入图片描述

2.2.PyTorch中的Finetune


  • 冻结卷积层

  • 设置requires_grad=False

这里先冻结所有参数,然后再替换全连接层,相当于冻结了卷积层的参数:

for param in resnet18_ft.parameters():
 param.requires_grad = False
 # 首先拿到 fc 层的输入个数
num_ftrs = resnet18_ft.fc.in_features
# 然后构造新的 fc 层替换原来的 fc 层
resnet18_ft.fc = nn.Linear(num_ftrs, classes)

这里不提供实验结果。


  • 设置学习率为 0

这里把卷积层的学习率设置为 0,需要在优化器里设置不同的学习率。首先获取全连接层参数的地址,然后使用 filter 过滤不属于全连接层的参数,也就是保留卷积层的参数;接着设置优化器的分组学习率,传入一个 list,包含 2 个元素,每个元素是字典,对应 2 个参数组。其中卷积层的学习率设置为 全连接层的 0.1 倍。

# 首先获取全连接层参数的地址
fc_params_id = list(map(id, resnet18_ft.fc.parameters()))     # 返回的是parameters的 内存地址
# 然后使用 filter 过滤不属于全连接层的参数,也就是保留卷积层的参数
base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())
# 设置优化器的分组学习率,传入一个 list,包含 2 个元素,每个元素是字典,对应 2 个参数组
optimizer = optim.SGD([{'params': base_params, 'lr': 0}, {'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)

这里不提供实验结果。


  • 使用分组学习率

这里不冻结卷积层,而是对卷积层使用较小的学习率,对全连接层使用较大的学习率,需要在优化器里设置不同的学习率。首先获取全连接层参数的地址,然后使用 filter 过滤不属于全连接层的参数,也就是保留卷积层的参数;接着设置优化器的分组学习率,传入一个 list,包含 2 个元素,每个元素是字典,对应 2 个参数组。其中卷积层的学习率设置为 全连接层的 0.1 倍。

# 首先获取全连接层参数的地址
fc_params_id = list(map(id, resnet18_ft.fc.parameters()))     # 返回的是parameters的 内存地址
# 然后使用 filter 过滤不属于全连接层的参数,也就是保留卷积层的参数
base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())
# 设置优化器的分组学习率,传入一个 list,包含 2 个元素,每个元素是字典,对应 2 个参数组
optimizer = optim.SGD([{'params': base_params, 'lr': LR*0}, {'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)

这里不提供实验结果。


  • 使用 GPU 的 tips

PyTorch 模型使用 GPU,可以分为 3 步:

  • 首先获取 device:device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  • 把模型加载到 device:model.to(device)
  • 在 data_loader 取数据的循环中,把每个 mini-batch 的数据和 label 加载到 device:inputs, labels = inputs.to(device), labels.to(device)

3.使用 GPU 训练模型

本节主要介绍 GPU 的使用。

3.1.CPU与GPU

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

二者的结构图如下,可以看到绿色部分(计算单元)GPU明显要比CPU要多。

在这里插入图片描述

GPU的ALU(算术运算单元)比CPU多,而CPU中的缓存区多,用于加速程序的运行,两者适用于不同的任务,计算密集型的程序和易于并行的程序通常在GPU上完成。

3.2.数据迁移至GPU

在数据运算时,两个数据进行运算,那么它们必须同时存放在同一个设备,要么同时是 CPU,要么同时是 GPU。而且数据和模型都要在同一个设备上。数据和模型可以使用to()方法从一个设备转移到另一个设备。而数据的to()方法还可以转换数据类型。

在这里插入图片描述

  • 从 CPU 到 GPU
    device = torch.device("cuda")
    tensor = tensor.to(device)
    module.to(device)
    
  • 从 GPU 到 CPU
    device = torch.device(cpu)
    tensor = tensor.to("cpu")
    module.to("cpu")
    

  • .to()函数:转换数据类型设备
    在这里插入图片描述
x=torch.ones((3,3))#定义一个张量
x=x.to(torch.float64)#把默认的float32转换为float64
x=torch.ones(3,3)#定义一个张量
x=x.to("cuda")#迁移到GPU

linear=nn.Linear(2,2)#定义一个module
linear.to(torch.double)#把module中所有的参数从默认的float32转换为float64(double就是float64)

gpu1=torch.device("cuda")#定义设备
linear.to(gpu1)#迁移到gpu
  • 可以看到上面两个例子中,tensor是需要用等号进行赋值的,而module是直接执行to函数即可。

  • tensor和module的 to()方法的区别是:tensor.to()执行的不是 inplace 操作,因此需要赋值;module.to()执行的是 inplace 操作。

  • tensor.to() 和 module.to()

首先导入库,获取 GPU 的 device

import torch
import torch.nn as nn
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  • 下面的代码是执行Tensor的to()方法
x_cpu = torch.ones((3, 3))
print("x_cpu:\ndevice: {} is_cuda: {} id: {}".format(x_cpu.device, x_cpu.is_cuda, id(x_cpu)))

x_gpu = x_cpu.to(device)
print("x_gpu:\ndevice: {} is_cuda: {} id: {}".format(x_gpu.device, x_gpu.is_cuda, id(x_gpu)))

输出如下:

x_cpu:
device: cpu is_cuda: False id: 1415020820304
x_gpu:
device: cpu is_cuda: True id: 2700061800153

可以看到Tensor的to()方法不是 inplace 操作,x_cpu和x_gpu的内存地址不一样。

  • 下面代码执行的是Module的to()方法
net = nn.Sequential(nn.Linear(3, 3))

print("\nid:{} is_cuda: {}".format(id(net), next(net.parameters()).is_cuda))

net.to(device)
print("\nid:{} is_cuda: {}".format(id(net), next(net.parameters()).is_cuda))

输出如下:

id:2325748158192 is_cuda: False
id:2325748158192 is_cuda: True

可以看到Module的to()方法是 inplace 操作,内存地址一样。


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,此方法不推荐使用

  • os.environ.setdefault(“CUDA_VISIBLE_DEVICES”, “2”, “3”):设置可见 GPU

在 PyTorch 中,有物理 GPU 可以逻辑 GPU 之分,可以设置它们之间的对应关系。

在这里插入图片描述

在上图中,如果执行了os.environ.setdefault("CUDA_VISIBLE_DEVICES", "2", "3"),那么可见 GPU 数量只有 2 个。对应关系如下:

在这里插入图片描述

如果执行了os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0", "3", "2"),那么可见 GPU 数量只有 3 个。对应关系如下:

在这里插入图片描述

设置的原因是可能系统中有很多用户和任务在使用 GPU,设置 GPU 编号,可以合理分配 GPU。通常默认gpu0为主 GPU。主 GPU 的概念与多 GPU 的分发并行机制有关。

3.3. 多 GPU 的分发并行

通常而言,多GPU的并行运算有三个步骤:分发 → 并行运算 →结果回收

  • 分发:由主GPU分发数据到各GPU
  • 并行运算:各GPU分别进行运算
  • 结果回收:各GPU将运算得到的结果发回主GPU

PyTorch实现:

  • torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

  • 功能:包装模型,实现分发并行机制。可以把数据平均分发到各个 GPU 上,每个 GPU 实际的数据量为 b a t c h _ s i z e G P U 数量 \frac {batch\_size}{GPU数量} GPU数量batch_size ,实现并行计算。

  • 主要参数:

    • module:需要包装分发的模型
    • device_ids:可分发的 GPU,默认分发到所有可见可用的 GPU
    • output_device:结果输出设备

需要注意的是:使用 DataParallel 时,device 要指定某个 GPU 为 主 GPU,否则会报错:

RuntimeError: module must have its parameters and buffers on device cuda:1 (device_ids[0]) but found one of them on device: cuda:2

这是因为,使用多 GPU 需要有一个主 GPU,来把每个 batch 的数据分发到每个 GPU,并从每个 GPU 收集计算好的结果。如果不指定主 GPU,那么数据就直接分发到每个 GPU,会造成有些数据在某个 GPU,而另一部分数据在其他 GPU,计算出错。

下面的代码设置两个可见 GPU,batch_size 为 2,那么每个 GPU 每个 batch 拿到的数据数量为 8,在模型的前向传播中打印数据的数量。

# 设置 2 个可见 GPU
    gpu_list = [0,1]
    gpu_list_str = ','.join(map(str, gpu_list))
    os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str)
    # 这里注意,需要指定一个 GPU 作为主 GPU。
    # 否则会报错:module must have its parameters and buffers on device cuda:1 (device_ids[0]) but found one of them on device: cuda:2
    # 参考:https://stackoverflow.com/questions/59249563/runtimeerror-module-must-have-its-parameters-and-buffers-on-device-cuda1-devi
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    batch_size = 16

    # data
    inputs = torch.randn(batch_size, 3)
    labels = torch.randn(batch_size, 3)

    inputs, labels = inputs.to(device), labels.to(device)

    # model
    net = FooNet(neural_num=3, layers=3)
    net = nn.DataParallel(net)
    net.to(device)

    # training
    for epoch in range(1):

        outputs = net(inputs)

        print("model outputs.size: {}".format(outputs.size()))

    print("CUDA_VISIBLE_DEVICES :{}".format(os.environ["CUDA_VISIBLE_DEVICES"]))
    print("device_count :{}".format(torch.cuda.device_count()))

输出如下:

batch size in forward: 8
model outputs.size: torch.Size([16, 3])
CUDA_VISIBLE_DEVICES :0,1
device_count :2

下面的代码是根据 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")

其中nvidia-smi -q -d Memory是查询所有 GPU 的内存信息,-q表示查询,-d是指定查询的内容。

nvidia-smi -q -d Memory | grep -A4 GPU是截取 GPU 开始的 4 行,如下:

Attached GPUs                       : 2
GPU 00000000:1A:00.0
    FB Memory Usage
        Total                       : 24220 MiB
        Used                        : 845 MiB
        Free                        : 23375 MiB
--
GPU 00000000:68:00.0
    FB Memory Usage
        Total                       : 24217 MiB
        Used                        : 50 MiB
        Free                        : 24167 MiB

nvidia-smi -q -d Memory | grep -A4 GPU | grep Free是提取Free所在的行,也就是提取剩余内存的信息,如下:

        Free                        : 23375 MiB
        Free                        : 24167 MiB

nvidia-smi -q -d Memory | grep -A4 GPU | grep Free > tmp.txt是把剩余内存的信息保存到tmp.txt中。

[int(x.split()[2]) for x in open('tmp.txt', 'r').readlines()]是用列表表达式对每行进行处理。

假设x=" Free : 23375 MiB",那么x.split()默认以空格分割,结果是:

['Free', ':', '23375', 'MiB']

x.split()[2]的结果是23375。

假设gpu_memory=['5','9','3']np.argsort(gpu_memory)的结果是array([2, 0, 1], dtype=int64),是从小到大取排好序后的索引。np.argsort(gpu_memory)[::-1]的结果是array([1, 0, 2], dtype=int64),也就是把元素的顺序反过来。

在 Python 中,list[<start>:<stop>:<step>]表示从start到stop取出元素,间隔为step,step=-1表示从stop到start取出元素。start默认为第一个元素的位置,stop默认为最后一个元素的位置。

‘,’.join(map(str, gpu_list))的结果是’1,0,2’。

最后os.environ.setdefault(“CUDA_VISIBLE_DEVICES”, gpu_list_str)就是根据 GPU 剩余内存从大到小设置对应关系,这样默认最大剩余内存的 GPU 为主 GPU。

在这里插入图片描述

提高 GPU 的利用率
nvidia-smi命令查看可以 GPU 的利用率,如下图所示。

在这里插入图片描述

上面的截图中,有两张显卡(GPU),其中上半部分显示的是显卡的信息下半部分显示的是每张显卡运行的进程。可以看到编号为 0 的 GPU 运行的是 PID 为 14383 进程。Memory Usage表示显存的使用率,编号为 0 的 GPU 使用了 16555 MB 显存,显存的利用率大概是70% 左右。Volatile GPU-Util表示计算 GPU 实际运算能力的利用率,编号为 0 的 GPU 只有 27% 的使用率。

虽然使用 GPU 可以加速训练模型,但是如果 GPU 的 Memory Usage 和 Volatile GPU-Util 太低,表示并没有充分利用 GPU。

因此,使用 GPU 训练模型,需要尽量提高 GPU 的 Memory Usage 和 Volatile GPU-Util 这两个指标,可以更进一步加速你的训练过程。

下面谈谈如何提高这两个指标。

Memory Usage
这个指标是由数据量主要是由模型大小,以及数据量的大小决定的。

模型大小是由网络的参数和网络结构决定的,模型越大,训练反而越慢。

我们主要调整的是每个 batch 训练的数据量的大小,也就是 batch_size。

在模型结构固定的情况下,尽量将batch size设置得比较大,充分利用 GPU 的内存。

Volatile GPU-Util
上面设置比较大的 batch size可以提高 GPU 的内存使用率,却不一定能提高 GPU 运算单元的使用率。

从前面可以看到,我们的数据首先读取到 CPU 中的,并在循环训练的时候,通过tensor.to()方法从 CPU 加载到 CPU 中,如下代码所示。

# 遍历 train_loader 取数据
for i, data in enumerate(train_loader):
    inputs, labels = data
    inputs = inputs.to(device) # 把数据从 CPU 加载到 GPU
    labels = labels.to(device) # 把数据从 CPU 加载到 GPU
    .
    .
    .

如果batch size得比较大,那么在 Dataset和 DataLoader ,CPU 处理一个 batch 的数据就会很慢,这时你会发现Volatile GPU-Util的值会在 0%,20%,70%,95%,0% 之间不断变化。

nvidia-smi命令查看可以 GPU 的利用率,但不能动态刷新显示。如果你想每隔一秒刷新显示 GPU 信息,可以使用watch -n 1 nvidia-smi 。

其实这是因为 GPU 处理数据非常快,而 CPU 处理数据较慢。GPU 每接收到一个 batch 的数据,使用率就跳到逐渐升高,处理完这个 batch 的数据后,使用率又逐渐降低,等到 CPU 把下一个 batch 的数据传过来。

解决方法是:设置 Dataloader的两个参数:

  • num_workers:默认只使用一个 CPU 读取和处理数据。可以设置为 4、8、16 等参数。但线程数并不是越大越好。因为,多核处理需要把数据分发到每个 CPU,处理完成后需要从多个 CPU 收集数据,这个过程也是需要时间的。如果设置num_workers过大,分发和收集数据等操作占用了太多时间,反而会降低效率。
  • pin_memory:如果内存较大,建议设置为 True。
    • 设置为 True,表示把数据直接映射到 GPU 的相关内存块上,省掉了一点数据传输时间。
    • 设置为 False,表示从 CPU 传入到缓存 RAM 里面,再给传输到 GPU 上。

GPU模型加载出现的报错与解决

  • 报错1:

如果模型是在 GPU 上保存的,在无 GPU 设备上加载模型时torch.load(path_state_dict),会出现下面的报错:

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":torch.load(path_state_dict, map_location="cpu")

  • 报错2:

如果模型经过net = nn.DataParallel(net)包装后,那么所有网络层的名称前面都会加上mmodule.。保存模型后再次加载时没有使用nn.DataParallel()包装,就会加载失败,因为state_dict中参数的名称对应不上。

Missing key(s) in state_dict: xxxxxxxxxx

Unexpected key(s) in state_dict:xxxxxxxxxx

解决方法是加载参数后,遍历 state_dict 的参数,如果名字是以module.开头,则去掉module.。代码如下:


from collections import OrderedDict
new_state_dict = OrderedDict()
for k, v in state_dict.items():
    namekey = k[7:] if k.startswith('module.') else k
    new_state_dict[namekey] = v

然后再把参数加载到模型中。

4.pytorch常见报错

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值